start-command 0.32.1 → 0.34.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 +32 -137
- package/src/lib/args-parser-queries.js +228 -0
- package/src/lib/args-parser.js +38 -179
- package/src/lib/detached-finalize.js +281 -0
- package/src/lib/docker-cleanup.js +240 -39
- package/src/lib/docker-post-mortem.js +289 -0
- package/src/lib/execution-attach.js +245 -0
- package/src/lib/execution-resume-all.js +211 -0
- package/src/lib/execution-resume.js +455 -0
- package/src/lib/execution-store.js +93 -6
- package/src/lib/exit-reason.js +264 -0
- package/src/lib/failure-handler.js +43 -41
- package/src/lib/isolation-log-utils.js +66 -1
- package/src/lib/isolation.js +28 -15
- 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 +233 -69
- 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/echo-integration.js +20 -13
- package/test/execution-attach.js +221 -0
- package/test/execution-resume.js +433 -0
- package/test/execution-store.js +57 -2
- 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-170.js +321 -0
- package/test/regression-171.js +562 -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.34.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 22c1158: Persist the terminal state of a detached Docker execution and record the container post-mortem. The completion watcher now writes `status`, `exitCode`, `oomKilled` and a real `endTime` back into the store instead of leaving the record `executing` forever, so `--status` stops calling the clock at query time and reporting a different "finish" on every call. `endTime` carries its provenance in `endTimeSource` (`docker-finished-at`, `log-footer`, `observed-at`); a stale record keeps `endTime` empty and records `staleDetectedAt` instead of a fabricated finish time. The single `docker inspect` now also collects `StartedAt`, `FinishedAt` and `State.Error`, and both the detached and the attached path write a post-mortem block for a kept container and a one-line note for a removed one. Signal decoding (`128+n`) is shared by the log and `--status`, so `137` reads as `137 (SIGKILL - 128+9)` everywhere.
|
|
8
|
+
|
|
9
|
+
## 0.33.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
15
|
+
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.
|
|
16
|
+
|
|
17
|
+
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.
|
|
18
|
+
|
|
19
|
+
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.
|
|
20
|
+
|
|
21
|
+
Also hardens every external command the package spawns. `failure-handler` and
|
|
22
|
+
`ExecutionStore.execClink` interpolated data - the failing command's own name,
|
|
23
|
+
the log path, recorded command text - into shell strings; they now pass an
|
|
24
|
+
argument vector, so a backtick, `$(...)` or `'` in a command is no longer
|
|
25
|
+
executed by a shell. As part of that, `gh issue create` receives the report
|
|
26
|
+
body verbatim: its newlines previously reached GitHub as the two characters
|
|
27
|
+
`\n`. Session ids and isolation usernames are now drawn from `crypto` rather
|
|
28
|
+
than `Math.random()`.
|
|
29
|
+
|
|
3
30
|
## 0.32.1
|
|
4
31
|
|
|
5
32
|
### Patch 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.34.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,13 @@ 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 {
|
|
38
|
-
const { uploadExecutionLog } = require('../lib/log-uploader');
|
|
36
|
+
const { reconcileFinalizedRecord } = require('../lib/detached-finalize');
|
|
37
|
+
const { dispatchQueryCommand, hasQueryMode } = require('../lib/query-commands');
|
|
39
38
|
const { printVersion } = require('../lib/version');
|
|
40
39
|
const { createStartBlock, createFinishBlock } = require('../lib/output-blocks');
|
|
41
40
|
const { runWithBunSpawn, runWithNodeSpawn } = require('../lib/spawn-helpers');
|
|
42
41
|
const { printUsage } = require('../lib/usage');
|
|
42
|
+
const { buildDisplayCommand, getCommandName } = require('../lib/shell-utils');
|
|
43
43
|
|
|
44
44
|
// Configuration from environment variables
|
|
45
45
|
const config = {
|
|
@@ -192,47 +192,12 @@ try {
|
|
|
192
192
|
|
|
193
193
|
const { wrapperOptions, command: parsedCommand } = parsedArgs;
|
|
194
194
|
|
|
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
|
-
}
|
|
195
|
+
// Query/control modes (--status, --list, --attach, --resume, ...) address an
|
|
196
|
+
// execution that already exists, so they never need a command of their own.
|
|
197
|
+
const isQueryMode = hasQueryMode(wrapperOptions);
|
|
233
198
|
|
|
234
199
|
// Check if no command was provided
|
|
235
|
-
if (!parsedCommand || parsedCommand.trim() === '') {
|
|
200
|
+
if (!isQueryMode && (!parsedCommand || parsedCommand.trim() === '')) {
|
|
236
201
|
console.error('Error: No command provided');
|
|
237
202
|
printUsage();
|
|
238
203
|
process.exit(1);
|
|
@@ -242,7 +207,7 @@ if (!parsedCommand || parsedCommand.trim() === '') {
|
|
|
242
207
|
let command = parsedCommand;
|
|
243
208
|
let substitutionResult = null;
|
|
244
209
|
|
|
245
|
-
if (!config.disableSubstitutions) {
|
|
210
|
+
if (!config.disableSubstitutions && parsedCommand && parsedCommand.trim()) {
|
|
246
211
|
substitutionResult = processCommand(parsedCommand, {
|
|
247
212
|
customLinoPath: config.substitutionsPath,
|
|
248
213
|
verbose: config.verbose,
|
|
@@ -267,6 +232,14 @@ const sessionId = wrapperOptions.sessionId || generateUUID();
|
|
|
267
232
|
|
|
268
233
|
// Main execution
|
|
269
234
|
(async () => {
|
|
235
|
+
if (isQueryMode) {
|
|
236
|
+
const queryExitCode = await dispatchQueryCommand(wrapperOptions, {
|
|
237
|
+
store: getExecutionStore(),
|
|
238
|
+
command: command && command.trim() ? command : null,
|
|
239
|
+
});
|
|
240
|
+
process.exit(queryExitCode === null ? 0 : queryExitCode);
|
|
241
|
+
}
|
|
242
|
+
|
|
270
243
|
// Check if running in isolation mode or with user isolation
|
|
271
244
|
if (hasIsolation(wrapperOptions) || wrapperOptions.user) {
|
|
272
245
|
await runWithIsolation(
|
|
@@ -289,93 +262,6 @@ const sessionId = wrapperOptions.sessionId || generateUUID();
|
|
|
289
262
|
}
|
|
290
263
|
})();
|
|
291
264
|
|
|
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
265
|
/**
|
|
380
266
|
* Run command in isolation mode
|
|
381
267
|
* @param {object} options - Wrapper options
|
|
@@ -519,7 +405,7 @@ async function runWithIsolation(
|
|
|
519
405
|
createStartBlock({
|
|
520
406
|
sessionId,
|
|
521
407
|
timestamp: startTime,
|
|
522
|
-
command: cmd,
|
|
408
|
+
command: buildDisplayCommand(cmd),
|
|
523
409
|
extraLines,
|
|
524
410
|
deferCommand,
|
|
525
411
|
})
|
|
@@ -584,6 +470,9 @@ async function runWithIsolation(
|
|
|
584
470
|
...buildDockerRuntimeMetadata(options),
|
|
585
471
|
shell: options.shell,
|
|
586
472
|
logPath: logFilePath,
|
|
473
|
+
// Lets the detached completion watcher write the terminal state back
|
|
474
|
+
// into this record once the container is gone (issue #170.1).
|
|
475
|
+
executionId: executionRecord ? executionRecord.uuid : null,
|
|
587
476
|
});
|
|
588
477
|
} else if (createdUser) {
|
|
589
478
|
// Run directly as the created user (no isolation environment)
|
|
@@ -619,7 +508,11 @@ async function runWithIsolation(
|
|
|
619
508
|
executionRecord.complete(exitCode);
|
|
620
509
|
}
|
|
621
510
|
try {
|
|
622
|
-
store.save(
|
|
511
|
+
store.save(
|
|
512
|
+
mode === 'detached'
|
|
513
|
+
? reconcileFinalizedRecord(store, executionRecord)
|
|
514
|
+
: executionRecord
|
|
515
|
+
);
|
|
623
516
|
} catch (err) {
|
|
624
517
|
if (config.verbose) {
|
|
625
518
|
console.error(
|
|
@@ -680,7 +573,7 @@ async function runWithIsolation(
|
|
|
680
573
|
*/
|
|
681
574
|
async function runDirect(cmd, sessionId) {
|
|
682
575
|
// Get the command name (first word of the actual command to execute)
|
|
683
|
-
const commandName = cmd
|
|
576
|
+
const commandName = getCommandName(cmd);
|
|
684
577
|
|
|
685
578
|
// Determine the shell based on the platform
|
|
686
579
|
const isWindows = process.platform === 'win32';
|
|
@@ -740,8 +633,8 @@ async function runDirect(cmd, sessionId) {
|
|
|
740
633
|
// Print start block with session ID
|
|
741
634
|
const displayCommand =
|
|
742
635
|
substitutionResult && substitutionResult.matched
|
|
743
|
-
? `${parsedCommand} -> ${cmd}`
|
|
744
|
-
: cmd;
|
|
636
|
+
? `${buildDisplayCommand(parsedCommand)} -> ${buildDisplayCommand(cmd)}`
|
|
637
|
+
: buildDisplayCommand(cmd);
|
|
745
638
|
console.log(
|
|
746
639
|
createStartBlock({
|
|
747
640
|
sessionId,
|
|
@@ -830,7 +723,7 @@ async function runDirectWithCommandStream(
|
|
|
830
723
|
const { $, raw } = await getCommandStream();
|
|
831
724
|
|
|
832
725
|
// Get the command name (first word of the actual command to execute)
|
|
833
|
-
const commandName = cmd
|
|
726
|
+
const commandName = getCommandName(cmd);
|
|
834
727
|
|
|
835
728
|
// Determine the shell based on the platform
|
|
836
729
|
const isWindows = process.platform === 'win32';
|
|
@@ -890,7 +783,9 @@ async function runDirectWithCommandStream(
|
|
|
890
783
|
|
|
891
784
|
// Print start block with session ID
|
|
892
785
|
const displayCmd =
|
|
893
|
-
subResult && subResult.matched
|
|
786
|
+
subResult && subResult.matched
|
|
787
|
+
? `${buildDisplayCommand(parsedCmd)} -> ${buildDisplayCommand(cmd)}`
|
|
788
|
+
: buildDisplayCommand(cmd);
|
|
894
789
|
console.log(
|
|
895
790
|
createStartBlock({
|
|
896
791
|
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
|
+
};
|