navy 7.0.0-alpha.1 → 7.0.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _chai = require("chai");
|
|
4
|
+
var _commander = require("commander");
|
|
5
|
+
/* eslint-env mocha */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* These tests check that the commander configuration the navy CLI relies on
|
|
9
|
+
* (positional options + per-subcommand option declarations) rejects options
|
|
10
|
+
* that a subcommand does not declare, using the exact error format commander
|
|
11
|
+
* emits by default.
|
|
12
|
+
*
|
|
13
|
+
* We build a minimal program with the same shape as packages/navy/src/cli/program.js
|
|
14
|
+
* rather than loading that module, because importing program.js has side effects
|
|
15
|
+
* (config reading, action wiring) that are not relevant to argument parsing.
|
|
16
|
+
*/
|
|
17
|
+
describe('cli/program unknown option handling', function () {
|
|
18
|
+
function buildProgram() {
|
|
19
|
+
const program = new _commander.Command();
|
|
20
|
+
program.exitOverride();
|
|
21
|
+
program.configureOutput({
|
|
22
|
+
writeErr: () => {},
|
|
23
|
+
writeOut: () => {}
|
|
24
|
+
});
|
|
25
|
+
program.enablePositionalOptions();
|
|
26
|
+
program.option('-e, --navy [env]', 'set the navy name to be used', 'dev');
|
|
27
|
+
program.command('status').option('--json', 'output JSON instead of a table').action(() => {});
|
|
28
|
+
program.command('start [services...]').option('-e, --navy [env]', 'set the navy name to be used', 'dev').action(() => {});
|
|
29
|
+
return program;
|
|
30
|
+
}
|
|
31
|
+
describe('a subcommand that does not declare the option', function () {
|
|
32
|
+
it('should throw a commander error for an unknown short option', function () {
|
|
33
|
+
const program = buildProgram();
|
|
34
|
+
(0, _chai.expect)(() => program.parse(['node', 'navy', 'status', '-e', 'foo'])).to.throw(/unknown option '-e'/);
|
|
35
|
+
});
|
|
36
|
+
it('should produce a non-zero exit code on the thrown error', function () {
|
|
37
|
+
const program = buildProgram();
|
|
38
|
+
try {
|
|
39
|
+
program.parse(['node', 'navy', 'status', '-e', 'foo']);
|
|
40
|
+
_chai.expect.fail('expected parse to throw');
|
|
41
|
+
} catch (ex) {
|
|
42
|
+
(0, _chai.expect)(ex.exitCode).to.be.greaterThan(0);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
it('should emit the canonical commander unknown-option message', function () {
|
|
46
|
+
const program = buildProgram();
|
|
47
|
+
try {
|
|
48
|
+
program.parse(['node', 'navy', 'status', '-e', 'foo']);
|
|
49
|
+
_chai.expect.fail('expected parse to throw');
|
|
50
|
+
} catch (ex) {
|
|
51
|
+
(0, _chai.expect)(ex.message).to.equal("error: unknown option '-e'");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
it('should still accept declared options on the same subcommand', function () {
|
|
55
|
+
const program = buildProgram();
|
|
56
|
+
(0, _chai.expect)(() => program.parse(['node', 'navy', 'status', '--json'])).to.not.throw();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('a subcommand that declares the option', function () {
|
|
60
|
+
it('should accept the option after the subcommand name', function () {
|
|
61
|
+
const program = buildProgram();
|
|
62
|
+
(0, _chai.expect)(() => program.parse(['node', 'navy', 'start', '-e', 'foo'])).to.not.throw();
|
|
63
|
+
});
|
|
64
|
+
it('should accept the option before the subcommand name (parent inherits)', function () {
|
|
65
|
+
const program = buildProgram();
|
|
66
|
+
(0, _chai.expect)(() => program.parse(['node', 'navy', '-e', 'foo', 'start'])).to.not.throw();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -27,6 +27,8 @@ describe('cli/program', function () {
|
|
|
27
27
|
actionsByCommand = {};
|
|
28
28
|
helpHandlersByCommand = {};
|
|
29
29
|
fakeProgram = {
|
|
30
|
+
option: sandbox.stub().returnsThis(),
|
|
31
|
+
enablePositionalOptions: sandbox.stub().returnsThis(),
|
|
30
32
|
command(spec) {
|
|
31
33
|
const name = spec.split(' ')[0];
|
|
32
34
|
const cmd = {
|
|
@@ -150,6 +152,14 @@ describe('cli/program', function () {
|
|
|
150
152
|
});
|
|
151
153
|
(0, _chai.expect)(consoleLogStub.called).to.equal(true);
|
|
152
154
|
});
|
|
155
|
+
it('should register a global -e, --navy option on the root program', function () {
|
|
156
|
+
loadModule();
|
|
157
|
+
(0, _chai.expect)(fakeProgram.option.calledWith('-e, --navy [env]', _sinon.default.match.string, 'dev')).to.equal(true);
|
|
158
|
+
});
|
|
159
|
+
it('should enable positional options so unknown subcommand options are rejected', function () {
|
|
160
|
+
loadModule();
|
|
161
|
+
(0, _chai.expect)(fakeProgram.enablePositionalOptions.calledOnce).to.equal(true);
|
|
162
|
+
});
|
|
153
163
|
});
|
|
154
164
|
describe('lazyRequire wrapped action', function () {
|
|
155
165
|
it('should pass through arguments to the underlying module', async function () {
|
|
@@ -332,6 +342,18 @@ describe('cli/program', function () {
|
|
|
332
342
|
(0, _chai.expect)(navyStub.start.firstCall.args[0]).to.eql(['web', 'api']);
|
|
333
343
|
(0, _chai.expect)(navyStub.ensurePluginsLoaded.calledOnce).to.equal(true);
|
|
334
344
|
});
|
|
345
|
+
it('should merge optsWithGlobals so global navy is used when Commander passes the command', async function () {
|
|
346
|
+
navyStub.start = sandbox.stub().resolves();
|
|
347
|
+
const fakeCommand = {
|
|
348
|
+
optsWithGlobals: () => ({
|
|
349
|
+
navy: 'from-global'
|
|
350
|
+
})
|
|
351
|
+
};
|
|
352
|
+
await actionsByCommand.start(['web'], {
|
|
353
|
+
navy: 'subcommand-default'
|
|
354
|
+
}, fakeCommand);
|
|
355
|
+
(0, _chai.expect)(getNavyStub.firstCall.args[0]).to.equal('from-global');
|
|
356
|
+
});
|
|
335
357
|
it('should pass undefined when called with an empty service list', async function () {
|
|
336
358
|
navyStub.start = sandbox.stub().resolves();
|
|
337
359
|
await actionsByCommand.start([], {
|
|
@@ -436,6 +458,8 @@ describe('cli/program', function () {
|
|
|
436
458
|
function createCapturingProgram() {
|
|
437
459
|
capturedActions = {};
|
|
438
460
|
return {
|
|
461
|
+
option: sandbox.stub().returnsThis(),
|
|
462
|
+
enablePositionalOptions: sandbox.stub().returnsThis(),
|
|
439
463
|
command(spec) {
|
|
440
464
|
const name = spec.split(' ')[0];
|
|
441
465
|
const cmd = {
|
package/lib/cli/program.js
CHANGED
|
@@ -11,6 +11,7 @@ var _errors = require("../errors");
|
|
|
11
11
|
var _config = require("../config");
|
|
12
12
|
var _driverLogging = require("../driver-logging");
|
|
13
13
|
var _configProvider = require("../config-provider");
|
|
14
|
+
var _mergeActionOptions = require("./util/merge-action-options");
|
|
14
15
|
const loadingLabelMap = {
|
|
15
16
|
destroy: 'Destroying services...',
|
|
16
17
|
start: 'Starting services...',
|
|
@@ -28,6 +29,23 @@ const loadingLabelMap = {
|
|
|
28
29
|
function removeFirstLineFromStackTrace(stack) {
|
|
29
30
|
return stack?.split('\n')?.slice(1)?.join('\n');
|
|
30
31
|
}
|
|
32
|
+
function normaliseLazyRequireArgs(args) {
|
|
33
|
+
if (args.length === 0) return args;
|
|
34
|
+
const last = args[args.length - 1];
|
|
35
|
+
if (!last || typeof last.optsWithGlobals !== 'function') {
|
|
36
|
+
return args;
|
|
37
|
+
}
|
|
38
|
+
const command = last;
|
|
39
|
+
const rest = args.slice(0, -1);
|
|
40
|
+
if (rest.length === 0) {
|
|
41
|
+
return rest;
|
|
42
|
+
}
|
|
43
|
+
const lastOpt = rest[rest.length - 1];
|
|
44
|
+
if (lastOpt && typeof lastOpt === 'object' && !Array.isArray(lastOpt)) {
|
|
45
|
+
return [...rest.slice(0, -1), (0, _mergeActionOptions.mergeActionOptions)(lastOpt, command)];
|
|
46
|
+
}
|
|
47
|
+
return rest;
|
|
48
|
+
}
|
|
31
49
|
function wrapper(res) {
|
|
32
50
|
if (res.catch) {
|
|
33
51
|
res.catch(ex => {
|
|
@@ -62,11 +80,21 @@ function basicCliWrapper(fnName, wrapperOpts = {}) {
|
|
|
62
80
|
} = require('../navy');
|
|
63
81
|
|
|
64
82
|
// commander v12 invokes action handlers with a trailing Command instance
|
|
65
|
-
// appended after the parsed options. Strip it
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
83
|
+
// appended after the parsed options. Strip it and merge global options
|
|
84
|
+
// (e.g. `navy -e dev start`) into the opts object.
|
|
85
|
+
let command = null;
|
|
86
|
+
if (args.length > 0) {
|
|
87
|
+
const last = args[args.length - 1];
|
|
88
|
+
if (last && typeof last.optsWithGlobals === 'function') {
|
|
89
|
+
command = last;
|
|
90
|
+
args = args.slice(0, -1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
let opts = args.length === 0 ? maybeServices : args[args.length - 1];
|
|
69
94
|
const otherArgs = args.slice(0, args.length - 1);
|
|
95
|
+
if (command && opts && typeof opts === 'object' && !Array.isArray(opts)) {
|
|
96
|
+
opts = (0, _mergeActionOptions.mergeActionOptions)(opts, command);
|
|
97
|
+
}
|
|
70
98
|
const envName = opts.navy;
|
|
71
99
|
if (wrapperOpts.serviceBasedAlias && maybeServices.length) {
|
|
72
100
|
console.log(`This command should not be called with a list of services. calling '${wrapperOpts.serviceBasedAlias}' instead`);
|
|
@@ -102,10 +130,17 @@ function basicCliWrapper(fnName, wrapperOpts = {}) {
|
|
|
102
130
|
function lazyRequire(path) {
|
|
103
131
|
return function (...args) {
|
|
104
132
|
const mod = require(path);
|
|
105
|
-
|
|
133
|
+
const normalised = normaliseLazyRequireArgs(args);
|
|
134
|
+
return wrapper((mod.default || mod)(...normalised));
|
|
106
135
|
};
|
|
107
136
|
}
|
|
108
137
|
const defaultNavy = process.env.NAVY_NAME || (0, _config.getConfig)().defaultNavy;
|
|
138
|
+
|
|
139
|
+
// Strictly separate parent and subcommand options so an option that isn't
|
|
140
|
+
// declared on a subcommand (e.g. `navy status -e foo`) is reported as
|
|
141
|
+
// unknown rather than silently absorbed by the parent program.
|
|
142
|
+
_commander.program.enablePositionalOptions();
|
|
143
|
+
_commander.program.option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy);
|
|
109
144
|
const importCommand = _commander.program.command('import').option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy).description('Imports docker compose configuration from the current working directory and initialises a new navy').action(lazyRequire('./import'));
|
|
110
145
|
(0, _configProvider.getImportCommandLineOptions)().forEach(opt => importCommand.option(...opt));
|
|
111
146
|
_commander.program.command('launch [services...]').option('-e, --navy [env]', `set the navy name to be used [${defaultNavy}]`, defaultNavy).description('Launches the given services in a navy').action(lazyRequire('./launch')).on('--help', () => console.log(`
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
4
|
+
var _chai = require("chai");
|
|
5
|
+
var _sinon = _interopRequireDefault(require("sinon"));
|
|
6
|
+
var _mergeActionOptions = require("../merge-action-options");
|
|
7
|
+
/* eslint-env mocha */
|
|
8
|
+
|
|
9
|
+
describe('cli/util/merge-action-options', function () {
|
|
10
|
+
it('should return parsed opts when command is missing', function () {
|
|
11
|
+
const opts = {
|
|
12
|
+
navy: 'dev'
|
|
13
|
+
};
|
|
14
|
+
(0, _chai.expect)((0, _mergeActionOptions.mergeActionOptions)(opts, null)).to.equal(opts);
|
|
15
|
+
});
|
|
16
|
+
it('should return parsed opts when optsWithGlobals is missing', function () {
|
|
17
|
+
const opts = {
|
|
18
|
+
navy: 'dev'
|
|
19
|
+
};
|
|
20
|
+
(0, _chai.expect)((0, _mergeActionOptions.mergeActionOptions)(opts, {})).to.equal(opts);
|
|
21
|
+
});
|
|
22
|
+
it('should merge optsWithGlobals so global navy wins over subcommand defaults', function () {
|
|
23
|
+
const command = {
|
|
24
|
+
optsWithGlobals: () => ({
|
|
25
|
+
navy: 'dev',
|
|
26
|
+
json: true
|
|
27
|
+
})
|
|
28
|
+
};
|
|
29
|
+
const merged = (0, _mergeActionOptions.mergeActionOptions)({
|
|
30
|
+
navy: 'default',
|
|
31
|
+
json: false
|
|
32
|
+
}, command);
|
|
33
|
+
(0, _chai.expect)(merged.navy).to.equal('dev');
|
|
34
|
+
(0, _chai.expect)(merged.json).to.equal(true);
|
|
35
|
+
});
|
|
36
|
+
it('should call optsWithGlobals on the command', function () {
|
|
37
|
+
const spy = _sinon.default.stub().returns({
|
|
38
|
+
navy: 'x'
|
|
39
|
+
});
|
|
40
|
+
(0, _mergeActionOptions.mergeActionOptions)({
|
|
41
|
+
navy: 'y'
|
|
42
|
+
}, {
|
|
43
|
+
optsWithGlobals: spy
|
|
44
|
+
});
|
|
45
|
+
(0, _chai.expect)(spy.calledOnce).to.equal(true);
|
|
46
|
+
});
|
|
47
|
+
it('should let an explicit subcommand option beat the inherited parent default', function () {
|
|
48
|
+
const command = {
|
|
49
|
+
optsWithGlobals: () => ({
|
|
50
|
+
navy: 'parent-default'
|
|
51
|
+
}),
|
|
52
|
+
getOptionValueSource: key => key === 'navy' ? 'cli' : 'default'
|
|
53
|
+
};
|
|
54
|
+
const merged = (0, _mergeActionOptions.mergeActionOptions)({
|
|
55
|
+
navy: 'from-subcommand'
|
|
56
|
+
}, command);
|
|
57
|
+
(0, _chai.expect)(merged.navy).to.equal('from-subcommand');
|
|
58
|
+
});
|
|
59
|
+
it('should keep the parent value when the subcommand value came from its default', function () {
|
|
60
|
+
const command = {
|
|
61
|
+
optsWithGlobals: () => ({
|
|
62
|
+
navy: 'from-parent'
|
|
63
|
+
}),
|
|
64
|
+
getOptionValueSource: () => 'default'
|
|
65
|
+
};
|
|
66
|
+
const merged = (0, _mergeActionOptions.mergeActionOptions)({
|
|
67
|
+
navy: 'subcommand-default'
|
|
68
|
+
}, command);
|
|
69
|
+
(0, _chai.expect)(merged.navy).to.equal('from-parent');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.mergeActionOptions = mergeActionOptions;
|
|
7
|
+
/**
|
|
8
|
+
* Merge Commander's per-command opts with inherited globals (e.g. `navy -e dev ps`).
|
|
9
|
+
* `optsWithGlobals()` has "globals overwrite locals" semantics, which makes parent
|
|
10
|
+
* defaults win over child defaults. With positional options enabled, however, a
|
|
11
|
+
* value supplied after the subcommand name (e.g. `navy ps -e dev`) is parsed onto
|
|
12
|
+
* the subcommand, not the parent, so we re-apply any option whose source on the
|
|
13
|
+
* subcommand is non-default to keep the explicit value.
|
|
14
|
+
*/
|
|
15
|
+
function mergeActionOptions(parsedOpts, command) {
|
|
16
|
+
if (!command || typeof command.optsWithGlobals !== 'function') {
|
|
17
|
+
return parsedOpts;
|
|
18
|
+
}
|
|
19
|
+
const merged = {
|
|
20
|
+
...command.optsWithGlobals()
|
|
21
|
+
};
|
|
22
|
+
if (typeof command.getOptionValueSource === 'function') {
|
|
23
|
+
for (const key of Object.keys(parsedOpts)) {
|
|
24
|
+
const source = command.getOptionValueSource(key);
|
|
25
|
+
if (source && source !== 'default') {
|
|
26
|
+
merged[key] = parsedOpts[key];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return merged;
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "navy",
|
|
3
|
-
"version": "7.0.0-alpha.
|
|
3
|
+
"version": "7.0.0-alpha.3",
|
|
4
4
|
"description": "Quick and powerful development environments using Docker and Docker Compose",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -49,6 +49,5 @@
|
|
|
49
49
|
"resolve": "^1.20.0",
|
|
50
50
|
"strip-ansi": "^6.0.1",
|
|
51
51
|
"www-authenticate": "^0.6.3"
|
|
52
|
-
}
|
|
53
|
-
"gitHead": "c977d36ee1e962134aee34d5fa20071cd863a619"
|
|
52
|
+
}
|
|
54
53
|
}
|