start-command 0.25.5 → 0.27.0
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.
- package/CHANGELOG.md +17 -1
- package/README.md +2 -1
- package/bunfig.toml +2 -2
- package/eslint.config.mjs +1 -1
- package/package.json +2 -2
- package/src/bin/cli.js +48 -67
- package/src/lib/args-parser.js +88 -7
- package/src/lib/execution-control.js +317 -0
- package/src/lib/isolation.js +22 -4
- package/src/lib/status-formatter.js +153 -1
- package/src/lib/usage.js +74 -0
- package/test/args-parser-control.js +71 -0
- package/test/{args-parser.test.js → args-parser.js} +28 -4
- package/test/cli.js +260 -0
- package/test/docker-autoremove.js +175 -0
- package/test/execution-control.js +253 -0
- package/test/{isolation.test.js → isolation.js} +4 -2
- package/test/merge-changesets.mjs +154 -0
- package/test/release-name.mjs +117 -0
- package/test/{screen-integration.test.js → screen-integration.js} +1 -1
- package/test/{ssh-integration.test.js → ssh-integration.js} +1 -1
- package/test/{status-query.test.js → status-query.js} +73 -0
- package/test/{substitution.test.js → substitution.js} +1 -2
- package/test/{user-manager.test.js → user-manager.js} +17 -0
- package/test/cli.test.js +0 -218
- package/test/docker-autoremove.test.js +0 -164
- package/test/release-name.test.mjs +0 -34
- /package/test/{args-parser-shell.test.js → args-parser-shell.js} +0 -0
- /package/test/{echo-integration.test.js → echo-integration.js} +0 -0
- /package/test/{execution-store.test.js → execution-store.js} +0 -0
- /package/test/{failure-handler.test.js → failure-handler.js} +0 -0
- /package/test/{isolation-cleanup.test.js → isolation-cleanup.js} +0 -0
- /package/test/{isolation-log-utils.test.js → isolation-log-utils.js} +0 -0
- /package/test/{isolation-stacking.test.js → isolation-stacking.js} +0 -0
- /package/test/{output-blocks.test.js → output-blocks.js} +0 -0
- /package/test/{public-exports.test.js → public-exports.js} +0 -0
- /package/test/{regression-84.test.js → regression-84.js} +0 -0
- /package/test/{regression-89.test.js → regression-89.js} +0 -0
- /package/test/{regression-91.test.js → regression-91.js} +0 -0
- /package/test/{sequence-parser.test.js → sequence-parser.js} +0 -0
- /package/test/{session-name-status.test.js → session-name-status.js} +0 -0
- /package/test/{version.test.js → version.js} +0 -0
package/src/lib/isolation.js
CHANGED
|
@@ -545,9 +545,13 @@ function runInDocker(command, options = {}) {
|
|
|
545
545
|
|
|
546
546
|
try {
|
|
547
547
|
if (options.detached) {
|
|
548
|
-
const dockerArgs = ['run', '-d'
|
|
548
|
+
const dockerArgs = ['run', '-d'];
|
|
549
|
+
if (options.keepAlive) {
|
|
550
|
+
dockerArgs.push('-i', '-t');
|
|
551
|
+
}
|
|
552
|
+
dockerArgs.push('--name', containerName);
|
|
549
553
|
if (options.autoRemoveDockerContainer) {
|
|
550
|
-
dockerArgs.
|
|
554
|
+
dockerArgs.push('--rm');
|
|
551
555
|
}
|
|
552
556
|
|
|
553
557
|
if (options.user) {
|
|
@@ -576,9 +580,23 @@ function runInDocker(command, options = {}) {
|
|
|
576
580
|
);
|
|
577
581
|
}
|
|
578
582
|
|
|
579
|
-
const
|
|
583
|
+
const dockerResult = spawnSync('docker', dockerArgs, {
|
|
580
584
|
encoding: 'utf8',
|
|
581
|
-
|
|
585
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
if (dockerResult.error) {
|
|
589
|
+
throw dockerResult.error;
|
|
590
|
+
}
|
|
591
|
+
if (dockerResult.status !== 0) {
|
|
592
|
+
const dockerError =
|
|
593
|
+
dockerResult.stderr.trim() ||
|
|
594
|
+
dockerResult.stdout.trim() ||
|
|
595
|
+
`docker exited with code ${dockerResult.status}`;
|
|
596
|
+
throw new Error(dockerError);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const containerId = dockerResult.stdout.trim();
|
|
582
600
|
|
|
583
601
|
if (options.logPath) {
|
|
584
602
|
const loggerScript = [
|
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
escapeForLinksNotation,
|
|
14
14
|
formatAsNestedLinksNotation,
|
|
15
15
|
} = require('./output-blocks');
|
|
16
|
+
const { collectProcessIds } = require('./execution-control');
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Check if a detached isolation session is still running
|
|
@@ -165,6 +166,47 @@ function attachCurrentTime(record) {
|
|
|
165
166
|
return wrapped;
|
|
166
167
|
}
|
|
167
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Wrap a record so its serialized form includes best-effort process IDs.
|
|
171
|
+
* The original record is not mutated.
|
|
172
|
+
* @param {Object} record - Execution record
|
|
173
|
+
* @returns {Object} Record-like object with processIds in toObject()
|
|
174
|
+
*/
|
|
175
|
+
function attachProcessIds(record) {
|
|
176
|
+
const processIds = collectProcessIds(record);
|
|
177
|
+
if (!processIds) {
|
|
178
|
+
return record;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const wrapped = Object.create(Object.getPrototypeOf(record));
|
|
182
|
+
Object.assign(wrapped, record);
|
|
183
|
+
wrapped.processIds = processIds;
|
|
184
|
+
|
|
185
|
+
const sourceToObject =
|
|
186
|
+
typeof record.toObject === 'function'
|
|
187
|
+
? () => record.toObject()
|
|
188
|
+
: () => ({ ...record });
|
|
189
|
+
|
|
190
|
+
wrapped.toObject = function () {
|
|
191
|
+
const base = sourceToObject();
|
|
192
|
+
const ordered = {};
|
|
193
|
+
let inserted = false;
|
|
194
|
+
for (const [key, value] of Object.entries(base)) {
|
|
195
|
+
ordered[key] = value;
|
|
196
|
+
if (key === 'pid') {
|
|
197
|
+
ordered.processIds = processIds;
|
|
198
|
+
inserted = true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (!inserted) {
|
|
202
|
+
ordered.processIds = processIds;
|
|
203
|
+
}
|
|
204
|
+
return ordered;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
return wrapped;
|
|
208
|
+
}
|
|
209
|
+
|
|
168
210
|
/**
|
|
169
211
|
* Format execution record as Links Notation (indented style)
|
|
170
212
|
* Uses nested Links notation for object values (like options) instead of JSON
|
|
@@ -273,6 +315,86 @@ function formatRecord(record, format) {
|
|
|
273
315
|
}
|
|
274
316
|
}
|
|
275
317
|
|
|
318
|
+
function sortRecordsByStartTimeDesc(records) {
|
|
319
|
+
return [...records].sort(
|
|
320
|
+
(a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime()
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function indentBlock(block, spaces) {
|
|
325
|
+
const prefix = ' '.repeat(spaces);
|
|
326
|
+
return block
|
|
327
|
+
.split('\n')
|
|
328
|
+
.map((line) => `${prefix}${line}`)
|
|
329
|
+
.join('\n');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function prepareRecordForList(record) {
|
|
333
|
+
return attachCurrentTime(attachProcessIds(enrichDetachedStatus(record)));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Format execution records as a Links Notation list.
|
|
338
|
+
* @param {Object[]} records - Execution records
|
|
339
|
+
* @returns {string} Links Notation formatted list
|
|
340
|
+
*/
|
|
341
|
+
function formatRecordListAsLinksNotation(records) {
|
|
342
|
+
const lines = ['executions', ` count ${records.length}`];
|
|
343
|
+
|
|
344
|
+
if (records.length === 0) {
|
|
345
|
+
lines.push(' records ()');
|
|
346
|
+
return lines.join('\n');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
lines.push(' records');
|
|
350
|
+
for (const record of records) {
|
|
351
|
+
lines.push(indentBlock(formatRecordAsLinksNotation(record), 4));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return lines.join('\n');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Format execution records as human-readable text.
|
|
359
|
+
* @param {Object[]} records - Execution records
|
|
360
|
+
* @returns {string} Human-readable list
|
|
361
|
+
*/
|
|
362
|
+
function formatRecordListAsText(records) {
|
|
363
|
+
const lines = ['Executions', `${'='.repeat(50)}`, `Count: ${records.length}`];
|
|
364
|
+
|
|
365
|
+
for (const record of records) {
|
|
366
|
+
lines.push('', formatRecordAsText(record));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return lines.join('\n');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Format execution records based on format type.
|
|
374
|
+
* @param {Object[]} records - Execution records
|
|
375
|
+
* @param {string} format - Output format (links-notation, json, text)
|
|
376
|
+
* @returns {string} Formatted output string
|
|
377
|
+
*/
|
|
378
|
+
function formatRecordList(records, format) {
|
|
379
|
+
switch (format) {
|
|
380
|
+
case 'links-notation':
|
|
381
|
+
return formatRecordListAsLinksNotation(records);
|
|
382
|
+
case 'json':
|
|
383
|
+
return JSON.stringify(
|
|
384
|
+
{
|
|
385
|
+
count: records.length,
|
|
386
|
+
executions: records.map((record) => record.toObject()),
|
|
387
|
+
},
|
|
388
|
+
null,
|
|
389
|
+
2
|
|
390
|
+
);
|
|
391
|
+
case 'text':
|
|
392
|
+
return formatRecordListAsText(records);
|
|
393
|
+
default:
|
|
394
|
+
throw new Error(`Unknown output format: ${format}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
276
398
|
/**
|
|
277
399
|
* Handle status query and output the result
|
|
278
400
|
* @param {Object} store - ExecutionStore instance
|
|
@@ -294,8 +416,9 @@ function queryStatus(store, identifier, outputFormat) {
|
|
|
294
416
|
try {
|
|
295
417
|
// Enrich detached execution status with live session check
|
|
296
418
|
const enrichedRecord = enrichDetachedStatus(record);
|
|
419
|
+
const withProcessIds = attachProcessIds(enrichedRecord);
|
|
297
420
|
// Attach currentTime so callers can see how long an executing command has been running
|
|
298
|
-
const withCurrentTime = attachCurrentTime(
|
|
421
|
+
const withCurrentTime = attachCurrentTime(withProcessIds);
|
|
299
422
|
return {
|
|
300
423
|
success: true,
|
|
301
424
|
output: formatRecord(withCurrentTime, outputFormat || 'links-notation'),
|
|
@@ -305,12 +428,41 @@ function queryStatus(store, identifier, outputFormat) {
|
|
|
305
428
|
}
|
|
306
429
|
}
|
|
307
430
|
|
|
431
|
+
/**
|
|
432
|
+
* Handle execution list query and output the result
|
|
433
|
+
* @param {Object} store - ExecutionStore instance
|
|
434
|
+
* @param {string|null} outputFormat - Output format (links-notation, json, text)
|
|
435
|
+
* @returns {{success: boolean, output?: string, error?: string}}
|
|
436
|
+
*/
|
|
437
|
+
function listExecutions(store, outputFormat) {
|
|
438
|
+
if (!store) {
|
|
439
|
+
return { success: false, error: 'Execution tracking is disabled.' };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
const records = sortRecordsByStartTimeDesc(
|
|
444
|
+
store.getAll().map(prepareRecordForList)
|
|
445
|
+
);
|
|
446
|
+
return {
|
|
447
|
+
success: true,
|
|
448
|
+
output: formatRecordList(records, outputFormat || 'links-notation'),
|
|
449
|
+
};
|
|
450
|
+
} catch (err) {
|
|
451
|
+
return { success: false, error: err.message };
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
308
455
|
module.exports = {
|
|
309
456
|
formatRecordAsLinksNotation,
|
|
310
457
|
formatRecordAsText,
|
|
311
458
|
formatRecord,
|
|
459
|
+
formatRecordListAsLinksNotation,
|
|
460
|
+
formatRecordListAsText,
|
|
461
|
+
formatRecordList,
|
|
312
462
|
queryStatus,
|
|
463
|
+
listExecutions,
|
|
313
464
|
isDetachedSessionAlive,
|
|
314
465
|
enrichDetachedStatus,
|
|
315
466
|
attachCurrentTime,
|
|
467
|
+
attachProcessIds,
|
|
316
468
|
};
|
package/src/lib/usage.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Print usage information */
|
|
2
|
+
function printUsage() {
|
|
3
|
+
console.log(`Usage: $ [options] [--] <command> | $ --status <uuid> [--output-format <fmt>] | $ --list [--output-format <fmt>] | $ --stop <id> | $ --terminate <id>
|
|
4
|
+
|
|
5
|
+
Options:
|
|
6
|
+
--isolated, -i <env> Run in isolated environment (screen, tmux, docker, ssh)
|
|
7
|
+
--attached, -a Run in attached mode (foreground)
|
|
8
|
+
--detached, -d Run in detached mode (background)
|
|
9
|
+
--session, -s <name> Session name for isolation
|
|
10
|
+
--session-id <uuid> Session UUID for tracking (auto-generated if not provided)
|
|
11
|
+
--session-name <uuid> Alias for --session-id
|
|
12
|
+
--image <image> Docker image (required for docker isolation)
|
|
13
|
+
--endpoint <endpoint> SSH endpoint (required for ssh isolation, e.g., user@host)
|
|
14
|
+
--isolated-user, -u [name] Create isolated user with same permissions
|
|
15
|
+
--keep-user Keep isolated user after command completes
|
|
16
|
+
--keep-alive, -k Keep isolation environment alive after command exits
|
|
17
|
+
--auto-remove-docker-container Auto-remove docker container after exit
|
|
18
|
+
--shell <shell> Shell to use in isolation environments: auto, bash, zsh, sh (default: auto)
|
|
19
|
+
--use-command-stream Use command-stream library for execution (experimental)
|
|
20
|
+
--status <id> Show status of execution by UUID or session name (--output-format: links-notation|json|text)
|
|
21
|
+
--list List all tracked executions (--output-format: links-notation|json|text)
|
|
22
|
+
--stop <id> Send CTRL+C/SIGINT to a detached isolated execution
|
|
23
|
+
--terminate <id> Terminate a detached isolated execution immediately
|
|
24
|
+
--cleanup Clean up stale "executing" records (crashed/killed processes)
|
|
25
|
+
--cleanup-dry-run Show stale records that would be cleaned up (without cleaning)
|
|
26
|
+
--version, -v Show version information
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
$ echo "Hello World"
|
|
30
|
+
$ bun test
|
|
31
|
+
$ --isolated tmux -- bun start
|
|
32
|
+
$ -i screen -d bun start
|
|
33
|
+
$ --isolated docker --image oven/bun:latest -- bun install
|
|
34
|
+
$ --isolated ssh --endpoint user@remote.server -- ls -la
|
|
35
|
+
$ --isolated-user -- npm test # Create isolated user
|
|
36
|
+
$ -u myuser -- npm start # Custom username
|
|
37
|
+
$ -i screen --isolated-user -- npm test # Combine with process isolation
|
|
38
|
+
$ --isolated-user --keep-user -- npm start
|
|
39
|
+
$ --list # List stored execution records
|
|
40
|
+
$ --list --output-format json # List stored records as JSON
|
|
41
|
+
$ --stop my-screen-session # Ask detached execution to stop gracefully
|
|
42
|
+
$ --terminate my-screen-session # Terminate detached execution immediately
|
|
43
|
+
$ --use-command-stream echo "Hello" # Use command-stream library`);
|
|
44
|
+
console.log('');
|
|
45
|
+
console.log('Piping with $:');
|
|
46
|
+
console.log(' echo "hi" | $ agent # Preferred - pipe TO $ command');
|
|
47
|
+
console.log(
|
|
48
|
+
' $ \'echo "hi" | agent\' # Alternative - quote entire pipeline'
|
|
49
|
+
);
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log('Quoting for special characters:');
|
|
52
|
+
console.log(" $ 'npm test && npm build' # Wrap for logical operators");
|
|
53
|
+
console.log(" $ 'cat file > output.txt' # Wrap for redirections");
|
|
54
|
+
console.log('');
|
|
55
|
+
console.log('Features:');
|
|
56
|
+
console.log(' - Logs all output to temporary directory');
|
|
57
|
+
console.log(' - Displays timestamps and exit codes');
|
|
58
|
+
console.log(
|
|
59
|
+
' - Auto-reports failures for NPM packages (when gh is available)'
|
|
60
|
+
);
|
|
61
|
+
console.log(' - Natural language command aliases (via substitutions.lino)');
|
|
62
|
+
console.log(' - Process isolation via screen, tmux, or docker');
|
|
63
|
+
console.log('');
|
|
64
|
+
console.log('Alias examples:');
|
|
65
|
+
console.log(' $ install lodash npm package -> npm install lodash');
|
|
66
|
+
console.log(
|
|
67
|
+
' $ install 4.17.21 version of lodash npm package -> npm install lodash@4.17.21'
|
|
68
|
+
);
|
|
69
|
+
console.log(
|
|
70
|
+
' $ clone https://github.com/user/repo repository -> git clone https://github.com/user/repo'
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { printUsage };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
const { describe, it } = require('node:test');
|
|
4
|
+
const assert = require('assert');
|
|
5
|
+
const { parseArgs } = require('../src/lib/args-parser');
|
|
6
|
+
|
|
7
|
+
describe('control options', () => {
|
|
8
|
+
it('should parse --stop with UUID or session name', () => {
|
|
9
|
+
const result = parseArgs(['--stop', 'my-session']);
|
|
10
|
+
assert.strictEqual(result.wrapperOptions.stop, 'my-session');
|
|
11
|
+
assert.strictEqual(result.command, '');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should parse --stop=value format', () => {
|
|
15
|
+
const result = parseArgs(['--stop=my-session']);
|
|
16
|
+
assert.strictEqual(result.wrapperOptions.stop, 'my-session');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should parse --terminate with UUID or session name', () => {
|
|
20
|
+
const result = parseArgs(['--terminate', 'my-session']);
|
|
21
|
+
assert.strictEqual(result.wrapperOptions.terminate, 'my-session');
|
|
22
|
+
assert.strictEqual(result.command, '');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('should parse --terminate=value format', () => {
|
|
26
|
+
const result = parseArgs(['--terminate=my-session']);
|
|
27
|
+
assert.strictEqual(result.wrapperOptions.terminate, 'my-session');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should throw error for missing --stop argument', () => {
|
|
31
|
+
assert.throws(() => {
|
|
32
|
+
parseArgs(['--stop']);
|
|
33
|
+
}, /requires a UUID or session name argument/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should throw error for empty --stop=value argument', () => {
|
|
37
|
+
assert.throws(() => {
|
|
38
|
+
parseArgs(['--stop=']);
|
|
39
|
+
}, /requires a UUID or session name argument/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should throw error for missing --terminate argument', () => {
|
|
43
|
+
assert.throws(() => {
|
|
44
|
+
parseArgs(['--terminate']);
|
|
45
|
+
}, /requires a UUID or session name argument/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should throw error for empty --terminate=value argument', () => {
|
|
49
|
+
assert.throws(() => {
|
|
50
|
+
parseArgs(['--terminate=']);
|
|
51
|
+
}, /requires a UUID or session name argument/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should reject combining query and control modes', () => {
|
|
55
|
+
assert.throws(() => {
|
|
56
|
+
parseArgs(['--status', 'uuid-here', '--stop', 'my-session']);
|
|
57
|
+
}, /Cannot combine --status, --list, --stop, --terminate, or --cleanup/);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should reject output-format with control modes', () => {
|
|
61
|
+
assert.throws(() => {
|
|
62
|
+
parseArgs(['--stop', 'my-session', '--output-format', 'json']);
|
|
63
|
+
}, /--output-format option is only valid with --status or --list/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should default control options to null', () => {
|
|
67
|
+
const result = parseArgs(['echo', 'hello']);
|
|
68
|
+
assert.strictEqual(result.wrapperOptions.stop, null);
|
|
69
|
+
assert.strictEqual(result.wrapperOptions.terminate, null);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -817,13 +817,13 @@ describe('status option', () => {
|
|
|
817
817
|
it('should throw error for missing UUID argument', () => {
|
|
818
818
|
assert.throws(() => {
|
|
819
819
|
parseArgs(['--status']);
|
|
820
|
-
}, /requires a UUID argument/);
|
|
820
|
+
}, /requires a UUID or session name argument/);
|
|
821
821
|
});
|
|
822
822
|
|
|
823
823
|
it('should throw error for --status with -flag as next argument', () => {
|
|
824
824
|
assert.throws(() => {
|
|
825
825
|
parseArgs(['--status', '--output-format']);
|
|
826
|
-
}, /requires a UUID argument/);
|
|
826
|
+
}, /requires a UUID or session name argument/);
|
|
827
827
|
});
|
|
828
828
|
|
|
829
829
|
it('should default status to null', () => {
|
|
@@ -832,6 +832,30 @@ describe('status option', () => {
|
|
|
832
832
|
});
|
|
833
833
|
});
|
|
834
834
|
|
|
835
|
+
describe('list option', () => {
|
|
836
|
+
it('should parse --list flag', () => {
|
|
837
|
+
const result = parseArgs(['--list']);
|
|
838
|
+
assert.strictEqual(result.wrapperOptions.list, true);
|
|
839
|
+
assert.strictEqual(result.command, '');
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
it('should parse --list with JSON output format', () => {
|
|
843
|
+
const result = parseArgs(['--list', '--output-format', 'json']);
|
|
844
|
+
assert.strictEqual(result.wrapperOptions.list, true);
|
|
845
|
+
assert.strictEqual(result.wrapperOptions.outputFormat, 'json');
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
it('should normalize output format with --list', () => {
|
|
849
|
+
const result = parseArgs(['--list', '--output-format', 'TEXT']);
|
|
850
|
+
assert.strictEqual(result.wrapperOptions.outputFormat, 'text');
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it('should default list to false', () => {
|
|
854
|
+
const result = parseArgs(['echo', 'hello']);
|
|
855
|
+
assert.strictEqual(result.wrapperOptions.list, false);
|
|
856
|
+
});
|
|
857
|
+
});
|
|
858
|
+
|
|
835
859
|
describe('output-format option', () => {
|
|
836
860
|
it('should parse --output-format with --status', () => {
|
|
837
861
|
const uuid = '12345678-1234-1234-1234-123456789abc';
|
|
@@ -874,7 +898,7 @@ describe('output-format option', () => {
|
|
|
874
898
|
it('should throw error for output-format without status', () => {
|
|
875
899
|
assert.throws(() => {
|
|
876
900
|
parseArgs(['--output-format', 'json', '--', 'npm', 'test']);
|
|
877
|
-
}, /--output-format option is only valid with --status/);
|
|
901
|
+
}, /--output-format option is only valid with --status or --list/);
|
|
878
902
|
});
|
|
879
903
|
|
|
880
904
|
it('should accept all valid output formats', () => {
|
|
@@ -928,4 +952,4 @@ describe('cleanup options', () => {
|
|
|
928
952
|
});
|
|
929
953
|
});
|
|
930
954
|
|
|
931
|
-
// Shell option tests moved to args-parser-shell.
|
|
955
|
+
// Shell option tests moved to args-parser-shell.js
|