@ricsam/r5d-worker 0.0.122 → 0.0.124

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 (67) hide show
  1. package/README.md +6 -3
  2. package/dist/cjs/command-launcher.cjs +76 -2
  3. package/dist/cjs/control-command-policy.cjs +177 -0
  4. package/dist/cjs/main.cjs +541 -247
  5. package/dist/cjs/package.json +1 -1
  6. package/dist/cjs/project-checkout-garbage.cjs +32 -3
  7. package/dist/cjs/project-workspace-state.cjs +51 -35
  8. package/dist/cjs/project-worktrees.cjs +79 -34
  9. package/dist/cjs/recovery-journal-protocol.cjs +56 -0
  10. package/dist/cjs/recovery-journal-runtime.cjs +133 -0
  11. package/dist/cjs/recovery-journal-thread.cjs +9 -0
  12. package/dist/cjs/recovery-journal.cjs +735 -0
  13. package/dist/cjs/recovery-store.cjs +8 -0
  14. package/dist/cjs/session-file-mutations.cjs +61 -0
  15. package/dist/cjs/working-tree-mirror.cjs +1 -0
  16. package/dist/cjs/workspace-command-sync-policy.cjs +37 -8
  17. package/dist/cjs/workspace-filesystem-executor-thread.cjs +36 -0
  18. package/dist/cjs/workspace-filesystem-executor.cjs +327 -0
  19. package/dist/cjs/workspace-filesystem-job-types.cjs +134 -0
  20. package/dist/cjs/workspace-filesystem-jobs.cjs +57 -0
  21. package/dist/cjs/workspace-git-sync.cjs +275 -201
  22. package/dist/cjs/workspace-mount-hold-fence.cjs +120 -0
  23. package/dist/mjs/command-launcher.mjs +75 -2
  24. package/dist/mjs/control-command-policy.mjs +146 -0
  25. package/dist/mjs/main.mjs +549 -253
  26. package/dist/mjs/package.json +1 -1
  27. package/dist/mjs/project-checkout-garbage.mjs +30 -2
  28. package/dist/mjs/project-workspace-state.mjs +51 -35
  29. package/dist/mjs/project-worktrees.mjs +74 -34
  30. package/dist/mjs/recovery-journal-protocol.mjs +30 -0
  31. package/dist/mjs/recovery-journal-runtime.mjs +112 -0
  32. package/dist/mjs/recovery-journal-thread.mjs +8 -0
  33. package/dist/mjs/recovery-journal.mjs +687 -0
  34. package/dist/mjs/recovery-store.mjs +8 -0
  35. package/dist/mjs/session-file-mutations.mjs +37 -0
  36. package/dist/mjs/working-tree-mirror.mjs +1 -0
  37. package/dist/mjs/workspace-command-sync-policy.mjs +37 -8
  38. package/dist/mjs/workspace-filesystem-executor-thread.mjs +38 -0
  39. package/dist/mjs/workspace-filesystem-executor.mjs +287 -0
  40. package/dist/mjs/workspace-filesystem-job-types.mjs +106 -0
  41. package/dist/mjs/workspace-filesystem-jobs.mjs +51 -0
  42. package/dist/mjs/workspace-git-sync.mjs +264 -202
  43. package/dist/mjs/workspace-mount-hold-fence.mjs +95 -0
  44. package/dist/types/command-launcher.d.ts +42 -0
  45. package/dist/types/control-command-policy.d.ts +53 -0
  46. package/dist/types/main.d.ts +31 -19
  47. package/dist/types/project-checkout-garbage.d.ts +19 -2
  48. package/dist/types/project-workspace-state.d.ts +9 -9
  49. package/dist/types/project-worktrees.d.ts +49 -5
  50. package/dist/types/recovery-journal-protocol.d.ts +35 -0
  51. package/dist/types/recovery-journal-runtime.d.ts +12 -0
  52. package/dist/types/recovery-journal-stall-fixture.d.ts +1 -0
  53. package/dist/types/recovery-journal-thread.d.ts +1 -0
  54. package/dist/types/recovery-journal.d.ts +246 -0
  55. package/dist/types/recovery-store.d.ts +6 -0
  56. package/dist/types/session-file-mutations.d.ts +22 -0
  57. package/dist/types/workspace-command-sync-policy.d.ts +22 -7
  58. package/dist/types/workspace-filesystem-executor-thread.d.ts +1 -0
  59. package/dist/types/workspace-filesystem-executor.d.ts +123 -0
  60. package/dist/types/workspace-filesystem-job-types.d.ts +246 -0
  61. package/dist/types/workspace-filesystem-jobs.d.ts +7 -0
  62. package/dist/types/workspace-git-sync.d.ts +113 -7
  63. package/dist/types/workspace-mount-hold-fence.d.ts +42 -0
  64. package/package.json +1 -1
  65. package/dist/cjs/project-snapshot-recovery-runner.cjs +0 -171
  66. package/dist/mjs/project-snapshot-recovery-runner.mjs +0 -135
  67. package/dist/types/project-snapshot-recovery-runner.d.ts +0 -10
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var workspace_mount_hold_fence_exports = {};
20
+ __export(workspace_mount_hold_fence_exports, {
21
+ WorkspaceMountHoldAbortedError: () => WorkspaceMountHoldAbortedError,
22
+ WorkspaceMountHoldFence: () => WorkspaceMountHoldFence
23
+ });
24
+ module.exports = __toCommonJS(workspace_mount_hold_fence_exports);
25
+ class WorkspaceMountHoldAbortedError extends Error {
26
+ constructor(reason) {
27
+ super(reason);
28
+ this.name = "WorkspaceMountHoldAbortedError";
29
+ }
30
+ }
31
+ class WorkspaceMountHoldFence {
32
+ holds = /* @__PURE__ */ new Map();
33
+ waiters = /* @__PURE__ */ new Set();
34
+ now;
35
+ constructor(options = {}) {
36
+ this.now = options.now ?? (() => Date.now());
37
+ }
38
+ /** Take one hold on every listed mount; the returned release is idempotent. */
39
+ hold(mountIds, description) {
40
+ const ids = [...new Set(mountIds)];
41
+ for (const id of ids) {
42
+ const existing = this.holds.get(id);
43
+ if (existing) {
44
+ existing.count += 1;
45
+ continue;
46
+ }
47
+ this.holds.set(id, { count: 1, description, sinceMs: this.now() });
48
+ }
49
+ let released = false;
50
+ return () => {
51
+ if (released) return;
52
+ released = true;
53
+ for (const id of ids) {
54
+ const existing = this.holds.get(id);
55
+ if (!existing) continue;
56
+ existing.count -= 1;
57
+ if (existing.count === 0) this.holds.delete(id);
58
+ }
59
+ this.wake();
60
+ };
61
+ }
62
+ isHeld(mountIds) {
63
+ return mountIds.some((id) => this.holds.has(id));
64
+ }
65
+ /** Human-readable holder of the first held mount among `mountIds`, for diagnostics. */
66
+ describeHold(mountIds) {
67
+ for (const id of mountIds) {
68
+ const hold = this.holds.get(id);
69
+ if (hold) return `${hold.description} (held for ${Math.round((this.now() - hold.sinceMs) / 1e3)}s)`;
70
+ }
71
+ return null;
72
+ }
73
+ /** Every held mount id, sorted, for status reporting. */
74
+ heldMountIds() {
75
+ return [...this.holds.keys()].sort();
76
+ }
77
+ /**
78
+ * Resolves in the same turn as the release that leaves none of `mountIds`
79
+ * held, so the caller can re-check and reserve without an interleaving
80
+ * hold. Resolves immediately when nothing is held. Rejects with
81
+ * WorkspaceMountHoldAbortedError when `signal` aborts first.
82
+ */
83
+ waitForRelease(mountIds, options = {}) {
84
+ if (options.signal?.aborted) {
85
+ return Promise.reject(new WorkspaceMountHoldAbortedError(abortReason(options.signal)));
86
+ }
87
+ if (!this.isHeld(mountIds)) return Promise.resolve();
88
+ return new Promise((resolve, reject) => {
89
+ const waiter = { mountIds, resolve, reject, signal: options.signal };
90
+ if (options.signal) {
91
+ const signal = options.signal;
92
+ waiter.onAbort = () => {
93
+ this.waiters.delete(waiter);
94
+ reject(new WorkspaceMountHoldAbortedError(abortReason(signal)));
95
+ };
96
+ signal.addEventListener("abort", waiter.onAbort, { once: true });
97
+ }
98
+ this.waiters.add(waiter);
99
+ });
100
+ }
101
+ wake() {
102
+ for (const waiter of [...this.waiters]) {
103
+ if (this.isHeld(waiter.mountIds)) continue;
104
+ this.waiters.delete(waiter);
105
+ if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener("abort", waiter.onAbort);
106
+ waiter.resolve();
107
+ }
108
+ }
109
+ }
110
+ function abortReason(signal) {
111
+ const reason = signal.reason;
112
+ if (reason instanceof Error) return reason.message;
113
+ if (typeof reason === "string" && reason) return reason;
114
+ return "Workspace mount reservation was aborted";
115
+ }
116
+ // Annotate the CommonJS export names for ESM import in node:
117
+ 0 && (module.exports = {
118
+ WorkspaceMountHoldAbortedError,
119
+ WorkspaceMountHoldFence
120
+ });
@@ -1,4 +1,11 @@
1
1
  import { spawn } from "node:child_process";
2
+ function commandLaunchLane(request) {
3
+ if (request.commandClass === "control") {
4
+ if (request.kind !== "exec_start") throw new Error("Control commands must be exec_start launches");
5
+ return "control";
6
+ }
7
+ return request.kind === "exec" && request.workspaceEffect === "none" ? "utility" : "general";
8
+ }
2
9
  class CommandLauncherUnavailableError extends Error {
3
10
  constructor(message, cause) {
4
11
  super(message, { cause });
@@ -86,12 +93,14 @@ class WorkerCommandLauncher {
86
93
  initialized = false;
87
94
  closePromise;
88
95
  sequence = 0;
96
+ capacity;
89
97
  async initialize() {
90
98
  this.assertHealthy();
91
99
  if (this.initialized) return;
92
100
  if (this.child) {
93
101
  try {
94
- await this.rpc("initialize");
102
+ const result = await this.rpc("initialize");
103
+ this.capacity = parseCapacity(result.capacity);
95
104
  } catch (error) {
96
105
  this.fail(error instanceof Error ? error : new Error(String(error)));
97
106
  await this.close();
@@ -103,19 +112,63 @@ class WorkerCommandLauncher {
103
112
  get activeIds() {
104
113
  return [...this.launches.keys()];
105
114
  }
115
+ /** The lane totals the launcher declared at initialization; undefined for a direct worker or an older launcher. */
116
+ get declaredCapacity() {
117
+ return this.capacity;
118
+ }
119
+ get supportsControlLane() {
120
+ return this.initialized && (this.mode === "direct" || (this.capacity?.controlSlots ?? 0) > 0);
121
+ }
122
+ /** Current admissions per lane, for the worker's capacity report. */
123
+ capacityReport() {
124
+ const bounded = this.child !== void 0;
125
+ const lanes = {
126
+ general: { slots: bounded ? this.capacity?.generalSlots ?? null : null, active: [], queued: [] },
127
+ utility: { slots: bounded ? this.capacity?.utilitySlots ?? null : null, active: [], queued: [] },
128
+ control: { slots: bounded ? this.capacity?.controlSlots ?? null : null, active: [], queued: [] }
129
+ };
130
+ for (const record of this.launches.values()) {
131
+ if (record.released) continue;
132
+ const entry = {
133
+ id: record.request.id,
134
+ ...record.request.sessionId ? { sessionId: record.request.sessionId } : {},
135
+ since: record.since
136
+ };
137
+ (record.phase === "active" ? lanes[record.lane].active : lanes[record.lane].queued).push(entry);
138
+ }
139
+ return { bounded, controlRuntimeMs: bounded ? this.capacity?.controlRuntimeMs ?? null : null, lanes };
140
+ }
141
+ notifyCapacityChange() {
142
+ try {
143
+ this.options.onCapacityChange?.();
144
+ } catch {
145
+ }
146
+ }
106
147
  async acquire(request) {
107
148
  this.assertHealthy();
108
149
  if (!this.initialized) throw new Error("Command launcher is not initialized");
109
150
  request.assertAdmission();
110
151
  if (this.launches.has(request.id)) throw new Error(`Duplicate command launch identity: ${request.id}`);
152
+ const lane = commandLaunchLane(request);
153
+ if (lane === "control" && !this.supportsControlLane)
154
+ throw new Error("Update the worker command launcher before running control commands");
111
155
  let rejectAdmission;
112
156
  const cancellation = new Promise((_resolve, reject) => {
113
157
  rejectAdmission = reject;
114
158
  });
115
159
  void cancellation.catch(() => {
116
160
  });
117
- const record = { request, revoked: false, released: false, rejectAdmission };
161
+ const record = {
162
+ request,
163
+ lane,
164
+ phase: this.child ? "queued" : "active",
165
+ since: (/* @__PURE__ */ new Date()).toISOString(),
166
+ revoked: false,
167
+ released: false,
168
+ rejectAdmission
169
+ };
118
170
  this.launches.set(request.id, record);
171
+ this.notifyCapacityChange();
119
172
  let prefix = [];
120
173
  let admissionTimer;
121
174
  try {
@@ -141,6 +194,9 @@ class WorkerCommandLauncher {
141
194
  }
142
195
  }, 100);
143
196
  prefix = await Promise.race([acquisition, cancellation]);
197
+ record.phase = "active";
198
+ record.since = (/* @__PURE__ */ new Date()).toISOString();
199
+ this.notifyCapacityChange();
144
200
  }
145
201
  request.assertAdmission();
146
202
  this.assertHealthy();
@@ -240,6 +296,7 @@ class WorkerCommandLauncher {
240
296
  }
241
297
  record.released = true;
242
298
  this.launches.delete(record.request.id);
299
+ this.notifyCapacityChange();
243
300
  })();
244
301
  }
245
302
  assertHealthy() {
@@ -301,6 +358,21 @@ class WorkerCommandLauncher {
301
358
  return this.failure;
302
359
  }
303
360
  }
361
+ function parseCapacity(value) {
362
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
363
+ const candidate = value;
364
+ const numbers = ["generalSlots", "utilitySlots", "controlSlots", "controlRuntimeMs", "queueTimeoutMs"];
365
+ if (!numbers.every((key) => typeof candidate[key] === "number" && Number.isSafeInteger(candidate[key]) && candidate[key] >= 0)) {
366
+ throw new Error("Command launcher initialize returned an invalid capacity");
367
+ }
368
+ return {
369
+ generalSlots: candidate.generalSlots,
370
+ utilitySlots: candidate.utilitySlots,
371
+ controlSlots: candidate.controlSlots,
372
+ controlRuntimeMs: candidate.controlRuntimeMs,
373
+ queueTimeoutMs: candidate.queueTimeoutMs
374
+ };
375
+ }
304
376
  async function createWorkerCommandLauncher(options = {}) {
305
377
  const launcher = new WorkerCommandLauncher({ ...options, argv: parseCommandLauncher(options.configuration) });
306
378
  await launcher.initialize();
@@ -310,6 +382,7 @@ export {
310
382
  CommandLaunchCancelledError,
311
383
  CommandLauncherUnavailableError,
312
384
  WorkerCommandLauncher,
385
+ commandLaunchLane,
313
386
  commandLauncherEnvironment,
314
387
  createWorkerCommandLauncher,
315
388
  parseCommandLauncher
@@ -0,0 +1,146 @@
1
+ const CONTROL_COMMAND_PROGRAM = "r5dctl";
2
+ const CONTROL_COMMAND_RUNTIME_MS = 12e4;
3
+ const CONTROL_COMMAND_MAX_ARGV = 64;
4
+ const CONTROL_COMMAND_MAX_LENGTH = 4096;
5
+ const CONTROL_GLOBAL_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set(["-p", "--project", "-s", "--session", "-b", "--branch"]);
6
+ const CONTROL_GLOBAL_FLAGS = /* @__PURE__ */ new Set(["--json", "-h", "--help", "-v", "--version"]);
7
+ const REFUSED_ANYWHERE = /* @__PURE__ */ new Set(["-f", "--follow", "--watch", "--base-url", "--config", "--token", "--api-key"]);
8
+ const CONTROL_VERBS = {
9
+ session: /* @__PURE__ */ new Set(["status", "stop", "prompt", "claim", "claims", "process-log"]),
10
+ sessions: /* @__PURE__ */ new Set(["recent"]),
11
+ describe: /* @__PURE__ */ new Set(["project", "branch", "session", "branch-operation", "branch-deletion"]),
12
+ get: /* @__PURE__ */ new Set(["projects", "branches", "sessions", "envs", "env"]),
13
+ conversation: /* @__PURE__ */ new Set(["overview", "inspect-node", "inspect-work"]),
14
+ ps: /* @__PURE__ */ new Set(["list", "history", "inspect", "stop"]),
15
+ workspace: /* @__PURE__ */ new Set(["status"]),
16
+ auth: /* @__PURE__ */ new Set(["status"]),
17
+ create: /* @__PURE__ */ new Set(["session"]),
18
+ update: /* @__PURE__ */ new Set(["session"]),
19
+ delete: /* @__PURE__ */ new Set(["session"]),
20
+ "answer-questions": null,
21
+ "answer-env-request": null
22
+ };
23
+ const CODE_LOADING_ENV_PATTERN = /^(NODE_OPTIONS|NODE_PATH|NODE_REPL_EXTERNAL_MODULE|BUN_OPTIONS|BUN_CONFIG_[A-Z_]+|LD_PRELOAD|LD_LIBRARY_PATH|LD_AUDIT|DYLD_[A-Z_]+|PATH|PATHEXT|PYTHONPATH|PERL5OPT|RUBYOPT|SHELL|ENV|BASH_ENV)$/i;
24
+ const BARE_WORD = /^[A-Za-z0-9_@%+=:,./-]+$/;
25
+ const DOUBLE_QUOTED_FORBIDDEN = /[$`\\!]/;
26
+ function tokenizePlainInvocation(command) {
27
+ if (command.length > CONTROL_COMMAND_MAX_LENGTH) return { reason: "command is longer than the control lane allows" };
28
+ if (/[\r\n]/.test(command)) return { reason: "multi-line commands are shell scripts" };
29
+ const argv = [];
30
+ let index = 0;
31
+ while (index < command.length) {
32
+ const char = command[index];
33
+ if (char === " " || char === " ") {
34
+ index += 1;
35
+ continue;
36
+ }
37
+ let word = "";
38
+ let quotedParts = 0;
39
+ let bareParts = 0;
40
+ while (index < command.length && command[index] !== " " && command[index] !== " ") {
41
+ const current = command[index];
42
+ if (current === "'") {
43
+ const end = command.indexOf("'", index + 1);
44
+ if (end < 0) return { reason: "unterminated single quote" };
45
+ word += command.slice(index + 1, end);
46
+ quotedParts += 1;
47
+ index = end + 1;
48
+ continue;
49
+ }
50
+ if (current === '"') {
51
+ const end = command.indexOf('"', index + 1);
52
+ if (end < 0) return { reason: "unterminated double quote" };
53
+ const inner = command.slice(index + 1, end);
54
+ if (DOUBLE_QUOTED_FORBIDDEN.test(inner)) return { reason: "double-quoted text would be expanded by the shell" };
55
+ word += inner;
56
+ quotedParts += 1;
57
+ index = end + 1;
58
+ continue;
59
+ }
60
+ let bare = "";
61
+ while (index < command.length && !/[\s'"]/.test(command[index])) {
62
+ bare += command[index];
63
+ index += 1;
64
+ }
65
+ if (!BARE_WORD.test(bare)) return { reason: `shell syntax in "${bare}" is not part of a plain invocation` };
66
+ word += bare;
67
+ bareParts += 1;
68
+ }
69
+ if (quotedParts + bareParts > 1) return { reason: "adjacent quoted and bare text is shell concatenation" };
70
+ argv.push(word);
71
+ if (argv.length > CONTROL_COMMAND_MAX_ARGV) return { reason: "too many arguments for a control command" };
72
+ }
73
+ if (argv.length === 0) return { reason: "empty command" };
74
+ return { argv };
75
+ }
76
+ function assertControlCommandArgv(argv) {
77
+ if (argv.length === 0 || argv.length > CONTROL_COMMAND_MAX_ARGV) throw new Error("Control commands are a single r5dctl invocation");
78
+ if (argv[0] !== CONTROL_COMMAND_PROGRAM) throw new Error(`Control commands must start with ${CONTROL_COMMAND_PROGRAM}`);
79
+ if (argv.reduce((length, argument) => length + (typeof argument === "string" ? argument.length : 0) + 1, 0) > CONTROL_COMMAND_MAX_LENGTH)
80
+ throw new Error("Control command arguments exceed the length limit");
81
+ for (const argument of argv) {
82
+ if (typeof argument !== "string" || argument.includes("\0") || /[\r\n]/.test(argument)) throw new Error("Control command arguments must be plain text");
83
+ if (REFUSED_ANYWHERE.has(argument.split("=", 1)[0])) throw new Error(`Control command option is not allowed (${argument.split("=", 1)[0]})`);
84
+ if (argument === "--") throw new Error("Control commands cannot use `--`");
85
+ }
86
+ let position = 1;
87
+ while (position < argv.length) {
88
+ const argument = argv[position];
89
+ if (CONTROL_GLOBAL_FLAGS.has(argument)) {
90
+ position += 1;
91
+ continue;
92
+ }
93
+ if (CONTROL_GLOBAL_FLAGS_WITH_VALUE.has(argument)) {
94
+ if (position + 1 >= argv.length) throw new Error(`Missing value for ${argument}`);
95
+ position += 2;
96
+ continue;
97
+ }
98
+ if (argument.startsWith("-")) throw new Error(`Global flag ${argument} is not allowed in a control command`);
99
+ break;
100
+ }
101
+ const verb = argv[position];
102
+ if (verb === void 0) throw new Error("Control commands need a coordination verb (for example `r5dctl ps list`)");
103
+ const allowed = Object.hasOwn(CONTROL_VERBS, verb) ? CONTROL_VERBS[verb] : void 0;
104
+ if (allowed === void 0) throw new Error(`r5dctl ${verb} is not a coordination command`);
105
+ if (allowed === null) return;
106
+ const subject = argv[position + 1];
107
+ if (subject === void 0 || !allowed.has(subject)) {
108
+ throw new Error(`r5dctl ${verb}${subject ? ` ${subject}` : ""} is not a coordination command`);
109
+ }
110
+ }
111
+ function resolveControlCommandExecutable(trustedPath, which, program = CONTROL_COMMAND_PROGRAM) {
112
+ const resolved = trustedPath ? which(program, { PATH: trustedPath }) : null;
113
+ if (!resolved) throw new Error(`${program} is not installed on the worker's trusted PATH; control commands are unavailable until it is`);
114
+ return resolved;
115
+ }
116
+ function controlCommandEnvironment(environment, trustedPath) {
117
+ const result = {};
118
+ for (const [key, value] of Object.entries(environment)) {
119
+ if (value === void 0 || CODE_LOADING_ENV_PATTERN.test(key)) continue;
120
+ result[key] = value;
121
+ }
122
+ if (trustedPath !== void 0) result.PATH = trustedPath;
123
+ return result;
124
+ }
125
+ function classifyControlCommand(command) {
126
+ const tokenized = tokenizePlainInvocation(command.trim());
127
+ if ("reason" in tokenized) return { control: false, reason: tokenized.reason };
128
+ const { argv } = tokenized;
129
+ if (argv[0] !== CONTROL_COMMAND_PROGRAM) return { control: false, reason: "not an r5dctl invocation" };
130
+ try {
131
+ assertControlCommandArgv(argv);
132
+ } catch (error) {
133
+ return { control: false, reason: error instanceof Error ? error.message : String(error) };
134
+ }
135
+ return { control: true, argv };
136
+ }
137
+ export {
138
+ CONTROL_COMMAND_MAX_ARGV,
139
+ CONTROL_COMMAND_MAX_LENGTH,
140
+ CONTROL_COMMAND_PROGRAM,
141
+ CONTROL_COMMAND_RUNTIME_MS,
142
+ assertControlCommandArgv,
143
+ classifyControlCommand,
144
+ controlCommandEnvironment,
145
+ resolveControlCommandExecutable
146
+ };