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.
Files changed (46) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +3 -0
  3. package/eslint.config.mjs +8 -0
  4. package/package.json +7 -3
  5. package/src/bin/cli.js +32 -137
  6. package/src/lib/args-parser-queries.js +228 -0
  7. package/src/lib/args-parser.js +38 -179
  8. package/src/lib/detached-finalize.js +281 -0
  9. package/src/lib/docker-cleanup.js +240 -39
  10. package/src/lib/docker-post-mortem.js +289 -0
  11. package/src/lib/execution-attach.js +245 -0
  12. package/src/lib/execution-resume-all.js +211 -0
  13. package/src/lib/execution-resume.js +455 -0
  14. package/src/lib/execution-store.js +93 -6
  15. package/src/lib/exit-reason.js +264 -0
  16. package/src/lib/failure-handler.js +43 -41
  17. package/src/lib/isolation-log-utils.js +66 -1
  18. package/src/lib/isolation.js +28 -15
  19. package/src/lib/query-commands.js +202 -0
  20. package/src/lib/screen-isolation.js +2 -1
  21. package/src/lib/session-probe.js +121 -0
  22. package/src/lib/shell-utils.js +164 -10
  23. package/src/lib/status-formatter.js +233 -69
  24. package/src/lib/usage.js +13 -1
  25. package/src/lib/user-manager.js +18 -1
  26. package/test/args-parser-attach-resume.js +159 -0
  27. package/test/args-parser-control.js +3 -3
  28. package/test/args-parser.js +1 -1
  29. package/test/check-file-size.js +91 -0
  30. package/test/ci-workflow-invariants.js +191 -0
  31. package/test/echo-integration.js +20 -13
  32. package/test/execution-attach.js +221 -0
  33. package/test/execution-resume.js +433 -0
  34. package/test/execution-store.js +57 -2
  35. package/test/exit-reason.js +209 -0
  36. package/test/failure-handler.js +107 -0
  37. package/test/load-command-stream.mjs +83 -0
  38. package/test/regression-162.js +320 -0
  39. package/test/regression-164.js +308 -0
  40. package/test/regression-165.js +310 -0
  41. package/test/regression-170.js +321 -0
  42. package/test/regression-171.js +562 -0
  43. package/test/regression-84.js +8 -3
  44. package/test/release-name.mjs +57 -0
  45. package/test/session-probe.js +150 -0
  46. 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
@@ -24,6 +24,9 @@ $ ls -la
24
24
  $ bun test
25
25
  $ git status
26
26
  $ --list
27
+ $ --attach <id>
28
+ $ --resume <id> -- <command>
29
+ $ --resume-all
27
30
  ```
28
31
 
29
32
  See the project-wide [README](../README.md), [docs/USAGE.md](../docs/USAGE.md),
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.32.1",
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
- "check": "bun run lint && bun run format:check && bun run check:file-size",
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
- "flatted": "^3.4.2"
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 { queryStatus, listExecutions } = require('../lib/status-formatter');
37
- const { ControlAction, controlExecution } = require('../lib/execution-control');
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
- // Handle --status flag
196
- if (wrapperOptions.status) {
197
- handleStatusQuery(wrapperOptions.status, wrapperOptions.outputFormat);
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(executionRecord);
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.split(' ')[0];
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.split(' ')[0];
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 ? `${parsedCmd} -> ${cmd}` : cmd;
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
+ };