Skip to content

Commit 85819ae

Browse files
committed
feat(extensions): services are dead, long live extensions
closes #146 - add new Extensions API - refactor setup command to be less of a piece of crap - cleanup lots of things
1 parent bbb7945 commit 85819ae

20 files changed

+509
-83
lines changed

lib/bootstrap.js

+11-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const path = require('path');
55
const abbrev = require('abbrev');
66
const createDebug = require('debug');
77
const Command = require('./command');
8+
const findExtensions = require('./utils/find-extensions');
89

910
const debug = createDebug('ghost-cli:bootstrap');
1011

@@ -40,9 +41,17 @@ module.exports = {
4041
process.exit(1);
4142
}
4243

44+
let extensions = findExtensions();
45+
debug(`Found ${ extensions.length } extensions: ${ extensions.map((ext) => ext.pkg.name).join(', ') }`);
46+
4347
debug('loading built-in commands');
4448
let commands = discoverCommands({}, __dirname);
4549

50+
debug('loading commands from extensions');
51+
extensions.forEach((extension) => {
52+
commands = discoverCommands(commands, extension.dir, extension.name);
53+
});
54+
4655
debug(`discovered commands: ${ Object.keys(commands).join(', ') }`);
4756
let abbreviations = abbrev(Object.keys(commands));
4857
let firstArg = argv.shift();
@@ -64,7 +73,7 @@ module.exports = {
6473
}
6574

6675
let aliases = Object.keys(abbreviations).filter((key) => abbreviations[key] === commandName);
67-
CommandClass.configure(commandName, aliases, yargs, {});
76+
CommandClass.configure(commandName, aliases, yargs, extensions);
6877
argv.unshift('help');
6978
});
7079
} else if (abbreviations[firstArg]) {
@@ -78,7 +87,7 @@ module.exports = {
7887
}
7988

8089
let aliases = Object.keys(abbreviations).filter((key) => abbreviations[key] === commandName);
81-
CommandClass.configure(commandName, aliases, yargs);
90+
CommandClass.configure(commandName, aliases, yargs, extensions);
8291
argv.unshift(commandName);
8392
} else {
8493
console.error(`Unrecognized command: '${ firstArg }'. Run \`ghost help\` for usage.`);

lib/command.js

+9-16
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ const kebabCase = require('lodash/kebabCase');
1313
const UI = require('./ui');
1414
const System = require('./system');
1515
const Config = require('./utils/config');
16-
const ServiceManager = require('./services');
1716

1817
const debug = createDebug('ghost-cli:command');
1918

@@ -45,7 +44,7 @@ class Command {
4544
* @param {Array<String>} aliases Aliases from abbrev
4645
* @param {Yargs} yargs Yargs instance
4746
*/
48-
static configure(commandName, aliases, yargs) {
47+
static configure(commandName, aliases, yargs, extensions) {
4948
if (!this.description) {
5049
throw new Error(`Command ${ commandName } must have a description!`);
5150
}
@@ -63,15 +62,15 @@ class Command {
6362
describe: this.description,
6463
builder: (commandArgs) => {
6564
debug(`building options for ${ commandName }`);
66-
commandArgs = this.configureOptions(commandName, commandArgs);
65+
commandArgs = this.configureOptions(commandName, commandArgs, extensions);
6766
if (this.configureSubcommands) {
68-
commandArgs = this.configureSubcommands(commandName, commandArgs);
67+
commandArgs = this.configureSubcommands(commandName, commandArgs, extensions);
6968
}
7069
return commandArgs;
7170
},
7271

7372
handler: (argv) => {
74-
this._run(commandName, argv);
73+
this._run(commandName, argv, extensions);
7574
}
7675
});
7776
}
@@ -100,7 +99,7 @@ class Command {
10099
* @param {Object} args Parsed arguments
101100
* @param {Object} context Various contextual dependencies
102101
*/
103-
static _run(commandName, argv) {
102+
static _run(commandName, argv, extensions) {
104103
if (!this.global) {
105104
checkValidInstall(commandName);
106105
}
@@ -112,15 +111,10 @@ class Command {
112111
verbose: verbose,
113112
allowPrompt: argv.prompt
114113
});
115-
let system = new System(ui);
116-
let service = ServiceManager.load(ui, system);
114+
let system = new System(ui, extensions);
117115

118116
system.setEnvironment(argv.development || process.env.NODE_ENV === 'development', true);
119-
120-
// TODO: This is intended to maintain maximum compatability with the pre-yargs
121-
// version of Ghost-CLI - so this will look ugly until the service manager part
122-
// can be refactored.
123-
let commandInstance = new this(ui, service, system);
117+
let commandInstance = new this(ui, system);
124118
debug(`running command ${ commandName }`);
125119

126120
if (commandInstance.cleanup) {
@@ -143,9 +137,8 @@ class Command {
143137
/**
144138
* Constructs the command instance
145139
*/
146-
constructor(ui, service, system) {
140+
constructor(ui, system) {
147141
this.ui = ui;
148-
this.service = service;
149142
this.system = system;
150143
}
151144

@@ -161,7 +154,7 @@ class Command {
161154
throw new Error('Provided command class does not extend the Command class');
162155
}
163156

164-
let cmdInstance = new CommandClass(this.ui, this.service, this.system);
157+
let cmdInstance = new CommandClass(this.ui, this.system);
165158
return cmdInstance.run(argv || {});
166159
}
167160
}

lib/commands/config/index.js

+6-14
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ const advancedOptions = require('./advanced');
1010
const errors = require('../../errors');
1111

1212
class ConfigCommand extends Command {
13-
constructor(ui, service, system) {
14-
super(ui, service, system);
13+
constructor(ui, system) {
14+
super(ui, system);
1515

16-
this.instance = this.system.getInstance();
17-
this.instance.loadConfig(true);
18-
this.config = this.instance.config;
16+
let instance = this.system.getInstance();
17+
instance.loadConfig(true);
18+
this.config = instance.config;
1919
}
2020

2121
handleAdvancedOptions(argv) {
@@ -64,9 +64,6 @@ class ConfigCommand extends Command {
6464
}
6565

6666
this.config.save();
67-
68-
let pname = argv.pname || url.parse(this.config.get('url')).hostname.replace(/\./g, '-');
69-
this.instance.name = pname; // Set instance name
7067
});
7168
}
7269

@@ -138,11 +135,6 @@ class ConfigCommand extends Command {
138135

139136
ConfigCommand.description = 'Configure a Ghost instance';
140137
ConfigCommand.params = '[key] [value]';
141-
ConfigCommand.options = Object.assign({
142-
pname: {
143-
description: 'Ghost process name',
144-
type: 'string'
145-
}
146-
}, advancedOptions);
138+
ConfigCommand.options = advancedOptions;
147139

148140
module.exports = ConfigCommand;

lib/commands/doctor/checks/setup.js

+3-8
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ module.exports = [{
3838
});
3939
}
4040

41-
return promise.then(() => { return {continue: true}; }).catch((error) => {
41+
return promise.then(() => { return {yes: true}; }).catch((error) => {
4242
// If the error caught is not a SystemError, something went wrong with execa,
4343
// so throw a ProcessError instead
4444
if (!(error instanceof errors.SystemError)) {
@@ -57,14 +57,9 @@ module.exports = [{
5757
'yellow'
5858
);
5959

60-
return context.ui.prompt({
61-
type: 'confirm',
62-
name: 'continue',
63-
message: chalk.blue('Continue anyways?'),
64-
default: true
65-
});
60+
return context.ui.confirm(chalk.blue('Continue anyways?'), true);
6661
}).then(
67-
(answers) => answers.continue || Promise.reject(new errors.SystemError(
62+
(answer) => answer.yes || Promise.reject(new errors.SystemError(
6863
`Setup was halted. Ghost is installed but not fully setup.${os.EOL}` +
6964
'Fix any errors shown and re-run `ghost setup`, or run `ghost setup --no-stack`.'
7065
))

lib/commands/install.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ const yarnInstall = require('../tasks/yarn-install');
1717
const SetupCommand = require('./setup');
1818

1919
class InstallCommand extends Command {
20-
static configureOptions(commandName, yargs) {
21-
yargs = super.configureOptions(commandName, yargs);
22-
return SetupCommand.configureOptions('setup', yargs);
20+
static configureOptions(commandName, yargs, extensions) {
21+
yargs = super.configureOptions(commandName, yargs, extensions);
22+
return SetupCommand.configureOptions('setup', yargs, extensions);
2323
}
2424

2525
run(argv) {

lib/commands/run.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ class RunCommand extends Command {
1616
instance.loadConfig(true);
1717

1818
process.env.paths__contentPath = path.join(process.cwd(), 'content');
19-
this.service.setConfig(instance.config);
2019

2120
this.child = spawn(process.execPath, ['current/index.js'], {
2221
cwd: process.cwd(),
@@ -30,11 +29,11 @@ class RunCommand extends Command {
3029

3130
this.child.on('message', (message) => {
3231
if (message.started) {
33-
this.service.process.success();
32+
instance.process.success();
3433
return;
3534
}
3635

37-
this.service.process.error(message.error);
36+
instance.process.error(message.error);
3837
});
3938
}
4039

lib/commands/setup.js

+93-26
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,41 @@
11
'use strict';
2-
const path = require('path');
2+
const url = require('url');
3+
const path = require('path');
4+
const get = require('lodash/get');
5+
const omit = require('lodash/omit');
36

4-
const setupChecks = require('./doctor/checks/setup');
5-
const StartCommand = require('./start');
6-
const ConfigCommand = require('./config');
7-
const Command = require('../command');
7+
const errors = require('../errors');
8+
const Command = require('../command');
9+
const setupChecks = require('./doctor/checks/setup');
10+
const StartCommand = require('./start');
11+
const ConfigCommand = require('./config');
812

913
class SetupCommand extends Command {
10-
static configureOptions(commandName, yargs) {
11-
yargs = super.configureOptions(commandName, yargs);
12-
return ConfigCommand.configureOptions('config', yargs);
14+
static configureOptions(commandName, yargs, extensions) {
15+
extensions.forEach((extension) => {
16+
let options = get(extension, 'config.options.setup', false);
17+
if (!options) {
18+
return;
19+
}
20+
21+
Object.assign(this.options, omit(options, Object.keys(this.options)));
22+
});
23+
24+
yargs = super.configureOptions(commandName, yargs, extensions);
25+
return ConfigCommand.configureOptions('config', yargs, extensions);
26+
}
27+
28+
constructor(ui, system) {
29+
super(ui, system);
30+
31+
this.stages = [];
32+
}
33+
34+
addStage(name, fn) {
35+
this.stages.push({
36+
name: name,
37+
fn: fn
38+
});
1339
}
1440

1541
run(argv) {
@@ -32,41 +58,78 @@ class SetupCommand extends Command {
3258
this.system.setEnvironment(true, true);
3359
}
3460

35-
return this.ui.listr([{
36-
title: 'Configuring Ghost',
37-
task: () => this.runCommand(ConfigCommand, argv)
38-
}, {
61+
let initialStages = [{
3962
title: 'Running setup checks',
4063
skip: () => !argv.stack,
4164
task: () => this.ui.listr(setupChecks, false)
4265
}, {
43-
title: 'Finishing setup',
44-
task: (ctx) => {
66+
title: 'Setting up instance',
67+
task: () => {
4568
let instance = this.system.getInstance();
4669
instance.loadConfig();
70+
instance.name = argv.pname || url.parse(instance.config.get('url')).hostname.replace(/\./g, '-');
4771
this.system.addInstance(instance);
48-
49-
this.service.setConfig(instance.config);
50-
return this.service.callHook('setup', ctx);
5172
}
52-
}], {setup: true}).then(() => {
53-
let promise = argv.start ? Promise.resolve({start: true}) : this.ui.prompt({
54-
type: 'confirm',
55-
name: 'start',
56-
message: 'Do you want to start Ghost?',
57-
default: true
73+
}];
74+
75+
if (argv.stage) {
76+
return this.system.hook('setup', this, argv).then(() => {
77+
let stage = this.stages.find(stage => stage.name === argv.stage);
78+
79+
if (!stage) {
80+
throw new errors.SystemError(`No setup stage '${argv.stage} exists`);
81+
}
82+
83+
return this.ui.listr([{
84+
title: `Setting up ${stage.name}`,
85+
task: (ctx, task) => stage.fn(argv, Object.assign(ctx, {single: true}), task)
86+
}]);
5887
});
88+
}
89+
90+
return this.ui.run(() => this.runCommand(ConfigCommand, argv), 'Configuring Ghost').then(
91+
() => this.system.hook('setup', this, argv)
92+
).then(() => {
93+
let tasks = initialStages.concat(this.stages.map((stage) => {
94+
return {
95+
title: `Setting up ${stage.name}`,
96+
task: (ctx, task) => {
97+
if (argv[`setup-${stage.name}`] === false) {
98+
return task.skip();
99+
}
59100

60-
return promise.then((answer) => {
61-
if (answer.start) {
62-
return this.runCommand(StartCommand, argv);
101+
if (!argv.prompt) {
102+
// Prompt has been disabled and there has not been a `--no-setup-<stagename>`
103+
// flag passed, so we will automatically run things
104+
return stage.fn(argv, ctx, task);
105+
}
106+
107+
return this.ui.confirm(`Do you wish to set up ${stage.name}?`, true).then((res) => {
108+
if (!res.yes) {
109+
return task.skip();
110+
}
111+
112+
return stage.fn(argv, ctx, task);
113+
});
114+
}
115+
};
116+
}));
117+
118+
return this.ui.listr(tasks, {setup: true}).then(() => {
119+
if (!argv.prompt || argv.start) {
120+
return Promise.resolve({yes: argv.start});
63121
}
122+
123+
return this.ui.confirm('Do you want to start Ghost?', true);
124+
}).then((res) => {
125+
return res.yes && this.runCommand(StartCommand, argv);
64126
});
65127
});
66128
}
67129
}
68130

69131
SetupCommand.description = 'Setup an installation of Ghost (after it is installed)';
132+
SetupCommand.params = '[stage]';
70133
SetupCommand.options = {
71134
stack: {
72135
description: 'Check the system stack on setup',
@@ -82,6 +145,10 @@ SetupCommand.options = {
82145
name: 'start',
83146
description: 'Automatically start Ghost without prompting',
84147
type: 'boolean'
148+
},
149+
pname: {
150+
description: 'Ghost instance name',
151+
type: 'string'
85152
}
86153
};
87154

0 commit comments

Comments
 (0)