start-command 0.32.1 → 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.
Files changed (42) hide show
  1. package/CHANGELOG.md +21 -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 +23 -136
  6. package/src/lib/args-parser-queries.js +228 -0
  7. package/src/lib/args-parser.js +38 -179
  8. package/src/lib/docker-cleanup.js +80 -5
  9. package/src/lib/execution-attach.js +245 -0
  10. package/src/lib/execution-resume-all.js +210 -0
  11. package/src/lib/execution-resume.js +454 -0
  12. package/src/lib/execution-store.js +47 -4
  13. package/src/lib/exit-reason.js +234 -0
  14. package/src/lib/failure-handler.js +43 -41
  15. package/src/lib/isolation-log-utils.js +58 -0
  16. package/src/lib/isolation.js +13 -10
  17. package/src/lib/query-commands.js +202 -0
  18. package/src/lib/screen-isolation.js +2 -1
  19. package/src/lib/session-probe.js +121 -0
  20. package/src/lib/shell-utils.js +164 -10
  21. package/src/lib/status-formatter.js +130 -57
  22. package/src/lib/usage.js +13 -1
  23. package/src/lib/user-manager.js +18 -1
  24. package/test/args-parser-attach-resume.js +159 -0
  25. package/test/args-parser-control.js +3 -3
  26. package/test/args-parser.js +1 -1
  27. package/test/check-file-size.js +91 -0
  28. package/test/ci-workflow-invariants.js +191 -0
  29. package/test/echo-integration.js +20 -13
  30. package/test/execution-attach.js +221 -0
  31. package/test/execution-resume.js +433 -0
  32. package/test/execution-store.js +52 -0
  33. package/test/exit-reason.js +209 -0
  34. package/test/failure-handler.js +107 -0
  35. package/test/load-command-stream.mjs +83 -0
  36. package/test/regression-162.js +320 -0
  37. package/test/regression-164.js +308 -0
  38. package/test/regression-165.js +310 -0
  39. package/test/regression-84.js +8 -3
  40. package/test/release-name.mjs +57 -0
  41. package/test/session-probe.js +150 -0
  42. package/test/user-manager.js +23 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
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
+
3
24
  ## 0.32.1
4
25
 
5
26
  ### 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.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
- "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,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 { queryStatus, listExecutions } = require('../lib/status-formatter');
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
- // 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
- }
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.split(' ')[0];
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.split(' ')[0];
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 ? `${parsedCmd} -> ${cmd}` : cmd;
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
+ };