start-command 0.32.0 → 0.33.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 +27 -0
- package/README.md +3 -0
- package/eslint.config.mjs +8 -0
- package/package.json +7 -3
- package/src/bin/cli.js +23 -136
- package/src/lib/args-parser-queries.js +228 -0
- package/src/lib/args-parser.js +38 -179
- package/src/lib/docker-cleanup.js +80 -5
- package/src/lib/execution-attach.js +245 -0
- package/src/lib/execution-resume-all.js +210 -0
- package/src/lib/execution-resume.js +454 -0
- package/src/lib/execution-store.js +47 -4
- package/src/lib/exit-reason.js +234 -0
- package/src/lib/failure-handler.js +43 -41
- package/src/lib/isolation-log-utils.js +58 -0
- package/src/lib/isolation.js +13 -10
- package/src/lib/query-commands.js +202 -0
- package/src/lib/screen-isolation.js +2 -1
- package/src/lib/session-probe.js +121 -0
- package/src/lib/shell-utils.js +164 -10
- package/src/lib/status-formatter.js +130 -57
- package/src/lib/usage.js +13 -1
- package/src/lib/user-manager.js +18 -1
- package/test/args-parser-attach-resume.js +159 -0
- package/test/args-parser-control.js +3 -3
- package/test/args-parser.js +1 -1
- package/test/check-file-size.js +91 -0
- package/test/ci-workflow-invariants.js +191 -0
- package/test/docker-network-integration.js +22 -14
- package/test/echo-integration.js +20 -13
- package/test/execution-attach.js +221 -0
- package/test/execution-resume.js +433 -0
- package/test/execution-store.js +52 -0
- package/test/exit-reason.js +209 -0
- package/test/failure-handler.js +107 -0
- package/test/load-command-stream.mjs +83 -0
- package/test/regression-162.js +320 -0
- package/test/regression-164.js +308 -0
- package/test/regression-165.js +310 -0
- package/test/regression-84.js +8 -3
- package/test/release-name.mjs +57 -0
- package/test/session-probe.js +150 -0
- package/test/user-manager.js +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.33.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add `--attach`, `--resume`, `--resume-all` and `--list --running` so a detached isolated session can be re-entered, continued, or repaired after a supervisor restart, and surface an `exitReason` hint when a log shows memory exhaustion.
|
|
8
|
+
|
|
9
|
+
Keep argument boundaries when rebuilding the command from `argv`: `start node -e "console.log('hi')"` and `start echo "a b"` now reach the shell with their quoting intact instead of being re-split by the inner `bash -c`. A single argument still runs verbatim as a shell script, so `start 'ls | wc -l'` keeps working; in the multi-argument form a quoted operator such as `start echo a '&&' echo b` is now a literal word.
|
|
10
|
+
|
|
11
|
+
Surface `memoryExhausted` and `memoryExhaustedReason` in `--status` when the log shows the runtime aborted on its own memory limit: a Node/V8 heap-limit abort dies below the container limit, so `oomKilled` stays `false` and the only evidence is the `FATAL ERROR` line the runtime printed. The log tail is now scanned with a 64 KiB window (V8 prints a long native stack trace after the marker), the observation covers attached sessions too, and the kept-container footer no longer asserts a bare `oomKilled=false` next to a fatal marker for exit codes 134/139.
|
|
12
|
+
|
|
13
|
+
Fix the release automation breaking on Node 24 with `$ is not a function`. `use-m` returns the raw CommonJS namespace `{ default, 'module.exports' }` instead of the callable default, so `const { $ } = await use('command-stream')` destructured `undefined` and every release job died at its first shell command. All eight affected scripts now go through the shared `scripts/load-command-stream.mjs` loader, which resolves either namespace shape and, when it cannot, throws an error naming the keys it actually saw.
|
|
14
|
+
|
|
15
|
+
Also hardens every external command the package spawns. `failure-handler` and
|
|
16
|
+
`ExecutionStore.execClink` interpolated data - the failing command's own name,
|
|
17
|
+
the log path, recorded command text - into shell strings; they now pass an
|
|
18
|
+
argument vector, so a backtick, `$(...)` or `'` in a command is no longer
|
|
19
|
+
executed by a shell. As part of that, `gh issue create` receives the report
|
|
20
|
+
body verbatim: its newlines previously reached GitHub as the two characters
|
|
21
|
+
`\n`. Session ids and isolation usernames are now drawn from `crypto` rather
|
|
22
|
+
than `Math.random()`.
|
|
23
|
+
|
|
24
|
+
## 0.32.1
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- 5568403: Keep Docker multi-network regression coverage hermetic while verifying the default bridge route.
|
|
29
|
+
|
|
3
30
|
## 0.32.0
|
|
4
31
|
|
|
5
32
|
### Minor Changes
|
package/README.md
CHANGED
package/eslint.config.mjs
CHANGED
|
@@ -76,6 +76,14 @@ export default [
|
|
|
76
76
|
__filename: 'readonly',
|
|
77
77
|
fetch: 'readonly',
|
|
78
78
|
Bun: 'readonly',
|
|
79
|
+
// Web-standard globals Node exposes; used by the repository-level
|
|
80
|
+
// scripts/ directory, which the root config lints (issue #168).
|
|
81
|
+
AbortController: 'readonly',
|
|
82
|
+
URL: 'readonly',
|
|
83
|
+
setTimeout: 'readonly',
|
|
84
|
+
clearTimeout: 'readonly',
|
|
85
|
+
setInterval: 'readonly',
|
|
86
|
+
clearInterval: 'readonly',
|
|
79
87
|
},
|
|
80
88
|
},
|
|
81
89
|
rules: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "start-command",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Gamification of coding, execute any command with ability to auto-report issues on GitHub",
|
|
5
5
|
"main": "src/bin/cli.js",
|
|
6
6
|
"exports": {
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"format": "prettier --write .",
|
|
18
18
|
"format:check": "prettier --check .",
|
|
19
19
|
"check:file-size": "bun ../scripts/check-file-size.mjs",
|
|
20
|
-
"
|
|
20
|
+
"lint:scripts": "cd .. && eslint .",
|
|
21
|
+
"format:check:scripts": "cd .. && prettier --check \"scripts/**/*.{js,mjs,cjs}\"",
|
|
22
|
+
"check": "bun run lint && bun run lint:scripts && bun run format:check && bun run format:check:scripts && bun run check:file-size",
|
|
21
23
|
"prepare": "cd .. && husky .husky || true",
|
|
22
24
|
"changeset": "changeset",
|
|
23
25
|
"changeset:version": "bun ../scripts/changeset-version.mjs",
|
|
@@ -70,6 +72,8 @@
|
|
|
70
72
|
"lino-objects-codec": "^0.4.0"
|
|
71
73
|
},
|
|
72
74
|
"overrides": {
|
|
73
|
-
"
|
|
75
|
+
"brace-expansion": "^5.0.9",
|
|
76
|
+
"flatted": "^3.4.2",
|
|
77
|
+
"js-yaml": "^4.3.2"
|
|
74
78
|
}
|
|
75
79
|
}
|
package/src/bin/cli.js
CHANGED
|
@@ -33,13 +33,12 @@ const {
|
|
|
33
33
|
} = require('../lib/user-manager');
|
|
34
34
|
const { handleFailure } = require('../lib/failure-handler');
|
|
35
35
|
const { ExecutionStore, ExecutionRecord } = require('../lib/execution-store');
|
|
36
|
-
const {
|
|
37
|
-
const { ControlAction, controlExecution } = require('../lib/execution-control');
|
|
38
|
-
const { uploadExecutionLog } = require('../lib/log-uploader');
|
|
36
|
+
const { dispatchQueryCommand, hasQueryMode } = require('../lib/query-commands');
|
|
39
37
|
const { printVersion } = require('../lib/version');
|
|
40
38
|
const { createStartBlock, createFinishBlock } = require('../lib/output-blocks');
|
|
41
39
|
const { runWithBunSpawn, runWithNodeSpawn } = require('../lib/spawn-helpers');
|
|
42
40
|
const { printUsage } = require('../lib/usage');
|
|
41
|
+
const { buildDisplayCommand, getCommandName } = require('../lib/shell-utils');
|
|
43
42
|
|
|
44
43
|
// Configuration from environment variables
|
|
45
44
|
const config = {
|
|
@@ -192,47 +191,12 @@ try {
|
|
|
192
191
|
|
|
193
192
|
const { wrapperOptions, command: parsedCommand } = parsedArgs;
|
|
194
193
|
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
process.exit(0);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Handle --list flag
|
|
202
|
-
if (wrapperOptions.list) {
|
|
203
|
-
handleListQuery(wrapperOptions.outputFormat);
|
|
204
|
-
process.exit(0);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// Handle --upload-log flag
|
|
208
|
-
if (wrapperOptions.uploadLog) {
|
|
209
|
-
const exitCode = handleUploadLogQuery(wrapperOptions.uploadLog);
|
|
210
|
-
process.exit(exitCode);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Handle --stop flag
|
|
214
|
-
if (wrapperOptions.stop !== null && wrapperOptions.stop !== undefined) {
|
|
215
|
-
handleControlQuery(wrapperOptions.stop, ControlAction.STOP);
|
|
216
|
-
process.exit(0);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Handle --terminate flag
|
|
220
|
-
if (
|
|
221
|
-
wrapperOptions.terminate !== null &&
|
|
222
|
-
wrapperOptions.terminate !== undefined
|
|
223
|
-
) {
|
|
224
|
-
handleControlQuery(wrapperOptions.terminate, ControlAction.TERMINATE);
|
|
225
|
-
process.exit(0);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Handle --cleanup flag
|
|
229
|
-
if (wrapperOptions.cleanup) {
|
|
230
|
-
handleCleanup(wrapperOptions.cleanupDryRun);
|
|
231
|
-
process.exit(0);
|
|
232
|
-
}
|
|
194
|
+
// Query/control modes (--status, --list, --attach, --resume, ...) address an
|
|
195
|
+
// execution that already exists, so they never need a command of their own.
|
|
196
|
+
const isQueryMode = hasQueryMode(wrapperOptions);
|
|
233
197
|
|
|
234
198
|
// Check if no command was provided
|
|
235
|
-
if (!parsedCommand || parsedCommand.trim() === '') {
|
|
199
|
+
if (!isQueryMode && (!parsedCommand || parsedCommand.trim() === '')) {
|
|
236
200
|
console.error('Error: No command provided');
|
|
237
201
|
printUsage();
|
|
238
202
|
process.exit(1);
|
|
@@ -242,7 +206,7 @@ if (!parsedCommand || parsedCommand.trim() === '') {
|
|
|
242
206
|
let command = parsedCommand;
|
|
243
207
|
let substitutionResult = null;
|
|
244
208
|
|
|
245
|
-
if (!config.disableSubstitutions) {
|
|
209
|
+
if (!config.disableSubstitutions && parsedCommand && parsedCommand.trim()) {
|
|
246
210
|
substitutionResult = processCommand(parsedCommand, {
|
|
247
211
|
customLinoPath: config.substitutionsPath,
|
|
248
212
|
verbose: config.verbose,
|
|
@@ -267,6 +231,14 @@ const sessionId = wrapperOptions.sessionId || generateUUID();
|
|
|
267
231
|
|
|
268
232
|
// Main execution
|
|
269
233
|
(async () => {
|
|
234
|
+
if (isQueryMode) {
|
|
235
|
+
const queryExitCode = await dispatchQueryCommand(wrapperOptions, {
|
|
236
|
+
store: getExecutionStore(),
|
|
237
|
+
command: command && command.trim() ? command : null,
|
|
238
|
+
});
|
|
239
|
+
process.exit(queryExitCode === null ? 0 : queryExitCode);
|
|
240
|
+
}
|
|
241
|
+
|
|
270
242
|
// Check if running in isolation mode or with user isolation
|
|
271
243
|
if (hasIsolation(wrapperOptions) || wrapperOptions.user) {
|
|
272
244
|
await runWithIsolation(
|
|
@@ -289,93 +261,6 @@ const sessionId = wrapperOptions.sessionId || generateUUID();
|
|
|
289
261
|
}
|
|
290
262
|
})();
|
|
291
263
|
|
|
292
|
-
function handleStatusQuery(uuid, outputFormat) {
|
|
293
|
-
const result = queryStatus(getExecutionStore(), uuid, outputFormat);
|
|
294
|
-
if (result.success) {
|
|
295
|
-
console.log(result.output);
|
|
296
|
-
} else {
|
|
297
|
-
console.error(`Error: ${result.error}`);
|
|
298
|
-
process.exit(1);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function handleListQuery(outputFormat) {
|
|
303
|
-
const result = listExecutions(getExecutionStore(), outputFormat);
|
|
304
|
-
if (result.success) {
|
|
305
|
-
console.log(result.output);
|
|
306
|
-
} else {
|
|
307
|
-
console.error(`Error: ${result.error}`);
|
|
308
|
-
process.exit(1);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function handleUploadLogQuery(identifier) {
|
|
313
|
-
const result = uploadExecutionLog(getExecutionStore(), identifier);
|
|
314
|
-
if (result.success) {
|
|
315
|
-
return result.exitCode || 0;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
console.error(`Error: ${result.error}`);
|
|
319
|
-
return result.exitCode || 1;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function handleControlQuery(identifier, action) {
|
|
323
|
-
const result = controlExecution(getExecutionStore(), identifier, action);
|
|
324
|
-
if (result.success) {
|
|
325
|
-
console.log(result.output);
|
|
326
|
-
} else {
|
|
327
|
-
console.error(`Error: ${result.error}`);
|
|
328
|
-
process.exit(1);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Handle --cleanup flag
|
|
334
|
-
* Cleans up stale "executing" records (processes that crashed or were killed)
|
|
335
|
-
* @param {boolean} dryRun - If true, just report what would be cleaned
|
|
336
|
-
*/
|
|
337
|
-
function handleCleanup(dryRun) {
|
|
338
|
-
const store = getExecutionStore();
|
|
339
|
-
if (!store) {
|
|
340
|
-
console.error('Error: Execution tracking is disabled.');
|
|
341
|
-
process.exit(1);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const result = store.cleanupStale({ dryRun });
|
|
345
|
-
|
|
346
|
-
if (result.errors.length > 0) {
|
|
347
|
-
for (const error of result.errors) {
|
|
348
|
-
console.error(`Error: ${error}`);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (result.records.length === 0) {
|
|
353
|
-
console.log('No stale records found.');
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (dryRun) {
|
|
358
|
-
console.log(
|
|
359
|
-
`Found ${result.records.length} stale record(s) that would be cleaned up:\n`
|
|
360
|
-
);
|
|
361
|
-
} else {
|
|
362
|
-
console.log(`Cleaned up ${result.cleaned} stale record(s):\n`);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
for (const record of result.records) {
|
|
366
|
-
const startTime = new Date(record.startTime).toLocaleString();
|
|
367
|
-
console.log(` UUID: ${record.uuid}`);
|
|
368
|
-
console.log(` Command: ${record.command}`);
|
|
369
|
-
console.log(` Started: ${startTime}`);
|
|
370
|
-
console.log(` PID: ${record.pid || 'N/A'}`);
|
|
371
|
-
console.log('');
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (dryRun) {
|
|
375
|
-
console.log('Run with --cleanup to actually clean up these records.');
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
264
|
/**
|
|
380
265
|
* Run command in isolation mode
|
|
381
266
|
* @param {object} options - Wrapper options
|
|
@@ -519,7 +404,7 @@ async function runWithIsolation(
|
|
|
519
404
|
createStartBlock({
|
|
520
405
|
sessionId,
|
|
521
406
|
timestamp: startTime,
|
|
522
|
-
command: cmd,
|
|
407
|
+
command: buildDisplayCommand(cmd),
|
|
523
408
|
extraLines,
|
|
524
409
|
deferCommand,
|
|
525
410
|
})
|
|
@@ -680,7 +565,7 @@ async function runWithIsolation(
|
|
|
680
565
|
*/
|
|
681
566
|
async function runDirect(cmd, sessionId) {
|
|
682
567
|
// Get the command name (first word of the actual command to execute)
|
|
683
|
-
const commandName = cmd
|
|
568
|
+
const commandName = getCommandName(cmd);
|
|
684
569
|
|
|
685
570
|
// Determine the shell based on the platform
|
|
686
571
|
const isWindows = process.platform === 'win32';
|
|
@@ -740,8 +625,8 @@ async function runDirect(cmd, sessionId) {
|
|
|
740
625
|
// Print start block with session ID
|
|
741
626
|
const displayCommand =
|
|
742
627
|
substitutionResult && substitutionResult.matched
|
|
743
|
-
? `${parsedCommand} -> ${cmd}`
|
|
744
|
-
: cmd;
|
|
628
|
+
? `${buildDisplayCommand(parsedCommand)} -> ${buildDisplayCommand(cmd)}`
|
|
629
|
+
: buildDisplayCommand(cmd);
|
|
745
630
|
console.log(
|
|
746
631
|
createStartBlock({
|
|
747
632
|
sessionId,
|
|
@@ -830,7 +715,7 @@ async function runDirectWithCommandStream(
|
|
|
830
715
|
const { $, raw } = await getCommandStream();
|
|
831
716
|
|
|
832
717
|
// Get the command name (first word of the actual command to execute)
|
|
833
|
-
const commandName = cmd
|
|
718
|
+
const commandName = getCommandName(cmd);
|
|
834
719
|
|
|
835
720
|
// Determine the shell based on the platform
|
|
836
721
|
const isWindows = process.platform === 'win32';
|
|
@@ -890,7 +775,9 @@ async function runDirectWithCommandStream(
|
|
|
890
775
|
|
|
891
776
|
// Print start block with session ID
|
|
892
777
|
const displayCmd =
|
|
893
|
-
subResult && subResult.matched
|
|
778
|
+
subResult && subResult.matched
|
|
779
|
+
? `${buildDisplayCommand(parsedCmd)} -> ${buildDisplayCommand(cmd)}`
|
|
780
|
+
: buildDisplayCommand(cmd);
|
|
894
781
|
console.log(
|
|
895
782
|
createStartBlock({
|
|
896
783
|
sessionId,
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query and control option parsing for the start-command wrapper.
|
|
3
|
+
*
|
|
4
|
+
* These options never launch a new command from scratch: they address an
|
|
5
|
+
* already tracked execution by UUID or session name. They are kept in their own
|
|
6
|
+
* module so `args-parser.js` stays focused on the launch options.
|
|
7
|
+
*
|
|
8
|
+
* Query modes: --status, --list [--running]
|
|
9
|
+
* Control modes: --upload-log, --stop, --terminate, --attach [--read-only],
|
|
10
|
+
* --resume [-- <command>], --resume-all, --cleanup
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Valid output formats for --status/--list and the machine-readable
|
|
15
|
+
* --resume/--resume-all summaries.
|
|
16
|
+
*/
|
|
17
|
+
const VALID_OUTPUT_FORMATS = ['links-notation', 'json', 'text'];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Default values for every query/control option.
|
|
21
|
+
* @returns {object} Fresh defaults (never share the same object between parses)
|
|
22
|
+
*/
|
|
23
|
+
function createQueryOptionDefaults() {
|
|
24
|
+
return {
|
|
25
|
+
status: null, // UUID/session name to show status for
|
|
26
|
+
list: false, // List all tracked execution records
|
|
27
|
+
running: false, // Restrict --list to executions that are still running
|
|
28
|
+
uploadLog: null, // UUID/session name whose stored log should be uploaded
|
|
29
|
+
outputFormat: null, // Output format (links-notation, json, text)
|
|
30
|
+
stop: null, // UUID/session name to stop gracefully
|
|
31
|
+
terminate: null, // UUID/session name to terminate immediately
|
|
32
|
+
attach: null, // UUID/session name whose terminal should be attached
|
|
33
|
+
readOnly: false, // Follow --attach output without forwarding stdin
|
|
34
|
+
resume: null, // UUID/session name to resume (optionally with a new command)
|
|
35
|
+
resumeAll: false, // Re-attach/resume every execution still marked running
|
|
36
|
+
cleanup: false, // Clean up stale "executing" records
|
|
37
|
+
cleanupDryRun: false, // Show what would be cleaned without cleaning
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Options that take a `<uuid-or-session-name>` argument, mapped to the wrapper
|
|
43
|
+
* option they populate.
|
|
44
|
+
*/
|
|
45
|
+
const IDENTIFIER_OPTIONS = {
|
|
46
|
+
'--status': 'status',
|
|
47
|
+
'--upload-log': 'uploadLog',
|
|
48
|
+
'--stop': 'stop',
|
|
49
|
+
'--terminate': 'terminate',
|
|
50
|
+
'--attach': 'attach',
|
|
51
|
+
'--resume': 'resume',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Boolean flags with no argument.
|
|
56
|
+
*/
|
|
57
|
+
const FLAG_OPTIONS = {
|
|
58
|
+
'--list': (options) => {
|
|
59
|
+
options.list = true;
|
|
60
|
+
},
|
|
61
|
+
'--running': (options) => {
|
|
62
|
+
options.running = true;
|
|
63
|
+
},
|
|
64
|
+
'--read-only': (options) => {
|
|
65
|
+
options.readOnly = true;
|
|
66
|
+
},
|
|
67
|
+
'--resume-all': (options) => {
|
|
68
|
+
options.resumeAll = true;
|
|
69
|
+
},
|
|
70
|
+
'--cleanup': (options) => {
|
|
71
|
+
options.cleanup = true;
|
|
72
|
+
},
|
|
73
|
+
'--cleanup-dry-run': (options) => {
|
|
74
|
+
options.cleanup = true;
|
|
75
|
+
options.cleanupDryRun = true;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function hasValue(value) {
|
|
80
|
+
return value !== null && value !== undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Parse a single query/control option.
|
|
85
|
+
* @param {string[]} args - Arguments array
|
|
86
|
+
* @param {number} index - Current index
|
|
87
|
+
* @param {object} options - Options object to populate
|
|
88
|
+
* @returns {number} Number of arguments consumed (0 if not recognized)
|
|
89
|
+
*/
|
|
90
|
+
function parseQueryOption(args, index, options) {
|
|
91
|
+
const arg = args[index];
|
|
92
|
+
|
|
93
|
+
const flag = FLAG_OPTIONS[arg];
|
|
94
|
+
if (flag) {
|
|
95
|
+
flag(options);
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const identifierKey = IDENTIFIER_OPTIONS[arg];
|
|
100
|
+
if (identifierKey) {
|
|
101
|
+
if (index + 1 < args.length && !args[index + 1].startsWith('-')) {
|
|
102
|
+
options[identifierKey] = args[index + 1];
|
|
103
|
+
return 2;
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`Option ${arg} requires a UUID or session name argument`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const [name, key] of Object.entries(IDENTIFIER_OPTIONS)) {
|
|
109
|
+
const prefix = `${name}=`;
|
|
110
|
+
if (arg.startsWith(prefix)) {
|
|
111
|
+
const value = arg.slice(prefix.length);
|
|
112
|
+
if (!value) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Option ${name} requires a UUID or session name argument`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
options[key] = value;
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --output-format <format>
|
|
123
|
+
if (arg === '--output-format') {
|
|
124
|
+
if (index + 1 < args.length && !args[index + 1].startsWith('-')) {
|
|
125
|
+
options.outputFormat = args[index + 1].toLowerCase();
|
|
126
|
+
return 2;
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`Option ${arg} requires a format argument`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --output-format=<value>
|
|
132
|
+
if (arg.startsWith('--output-format=')) {
|
|
133
|
+
options.outputFormat = arg.split('=')[1].toLowerCase();
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Modes that address one stored execution and therefore cannot be combined.
|
|
142
|
+
* @param {object} options - Parsed wrapper options
|
|
143
|
+
* @returns {string[]} Names of the modes that are active
|
|
144
|
+
*/
|
|
145
|
+
function activeQueryModes(options) {
|
|
146
|
+
const modes = [];
|
|
147
|
+
if (hasValue(options.status)) {
|
|
148
|
+
modes.push('--status');
|
|
149
|
+
}
|
|
150
|
+
if (options.list) {
|
|
151
|
+
modes.push('--list');
|
|
152
|
+
}
|
|
153
|
+
if (hasValue(options.uploadLog)) {
|
|
154
|
+
modes.push('--upload-log');
|
|
155
|
+
}
|
|
156
|
+
if (hasValue(options.stop)) {
|
|
157
|
+
modes.push('--stop');
|
|
158
|
+
}
|
|
159
|
+
if (hasValue(options.terminate)) {
|
|
160
|
+
modes.push('--terminate');
|
|
161
|
+
}
|
|
162
|
+
if (hasValue(options.attach)) {
|
|
163
|
+
modes.push('--attach');
|
|
164
|
+
}
|
|
165
|
+
if (hasValue(options.resume)) {
|
|
166
|
+
modes.push('--resume');
|
|
167
|
+
}
|
|
168
|
+
if (options.resumeAll) {
|
|
169
|
+
modes.push('--resume-all');
|
|
170
|
+
}
|
|
171
|
+
if (options.cleanup) {
|
|
172
|
+
modes.push('--cleanup');
|
|
173
|
+
}
|
|
174
|
+
return modes;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Formats are only meaningful for modes that print a machine-readable report.
|
|
179
|
+
*/
|
|
180
|
+
const OUTPUT_FORMAT_MODES = ['--status', '--list', '--resume', '--resume-all'];
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Validate query/control options.
|
|
184
|
+
* @param {object} options - Parsed wrapper options
|
|
185
|
+
* @throws {Error} When the combination is not supported
|
|
186
|
+
*/
|
|
187
|
+
function validateQueryOptions(options) {
|
|
188
|
+
if (hasValue(options.outputFormat)) {
|
|
189
|
+
if (!VALID_OUTPUT_FORMATS.includes(options.outputFormat)) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
`Invalid output format: "${options.outputFormat}". Valid options are: ${VALID_OUTPUT_FORMATS.join(', ')}`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const modes = activeQueryModes(options);
|
|
197
|
+
|
|
198
|
+
if (modes.length > 1) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
'Cannot combine --status, --list, --upload-log, --stop, --terminate, --attach, --resume, --resume-all, or --cleanup in the same invocation'
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (
|
|
205
|
+
hasValue(options.outputFormat) &&
|
|
206
|
+
!modes.some((mode) => OUTPUT_FORMAT_MODES.includes(mode))
|
|
207
|
+
) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
'--output-format option is only valid with --status, --list, --resume, or --resume-all'
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (options.running && !options.list) {
|
|
214
|
+
throw new Error('--running option is only valid with --list');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (options.readOnly && !hasValue(options.attach)) {
|
|
218
|
+
throw new Error('--read-only option is only valid with --attach');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
VALID_OUTPUT_FORMATS,
|
|
224
|
+
activeQueryModes,
|
|
225
|
+
createQueryOptionDefaults,
|
|
226
|
+
parseQueryOption,
|
|
227
|
+
validateQueryOptions,
|
|
228
|
+
};
|