@steipete/oracle 0.8.6 → 0.10.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 (181) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +130 -45
  3. package/dist/bin/oracle-cli.js +613 -379
  4. package/dist/bin/oracle-mcp.js +2 -2
  5. package/dist/bin/oracle.js +165 -279
  6. package/dist/scripts/agent-send.js +31 -31
  7. package/dist/scripts/check.js +6 -6
  8. package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
  9. package/dist/scripts/docs-list.js +30 -30
  10. package/dist/scripts/git-policy.js +25 -23
  11. package/dist/scripts/run-cli.js +8 -8
  12. package/dist/scripts/runner.js +203 -195
  13. package/dist/scripts/test-browser.js +21 -18
  14. package/dist/scripts/test-remote-chrome.js +20 -20
  15. package/dist/src/bridge/connection.js +18 -18
  16. package/dist/src/bridge/userConfigFile.js +7 -7
  17. package/dist/src/browser/actions/assistantResponse.js +149 -101
  18. package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
  19. package/dist/src/browser/actions/attachments.js +246 -150
  20. package/dist/src/browser/actions/domEvents.js +2 -2
  21. package/dist/src/browser/actions/modelSelection.js +314 -104
  22. package/dist/src/browser/actions/navigation.js +161 -136
  23. package/dist/src/browser/actions/promptComposer.js +100 -64
  24. package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
  25. package/dist/src/browser/actions/thinkingTime.js +207 -110
  26. package/dist/src/browser/chromeLifecycle.js +62 -60
  27. package/dist/src/browser/config.js +34 -15
  28. package/dist/src/browser/constants.js +17 -12
  29. package/dist/src/browser/cookies.js +19 -19
  30. package/dist/src/browser/detect.js +62 -62
  31. package/dist/src/browser/domDebug.js +1 -1
  32. package/dist/src/browser/index.js +452 -303
  33. package/dist/src/browser/modelStrategy.js +1 -1
  34. package/dist/src/browser/pageActions.js +5 -5
  35. package/dist/src/browser/policies.js +16 -13
  36. package/dist/src/browser/profileState.js +44 -39
  37. package/dist/src/browser/prompt.js +72 -42
  38. package/dist/src/browser/promptSummary.js +5 -5
  39. package/dist/src/browser/providerDomFlow.js +17 -0
  40. package/dist/src/browser/providers/chatgptDomProvider.js +49 -0
  41. package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +254 -0
  42. package/dist/src/browser/providers/index.js +2 -0
  43. package/dist/src/browser/reattach.js +67 -34
  44. package/dist/src/browser/reattachHelpers.js +31 -26
  45. package/dist/src/browser/sessionRunner.js +37 -25
  46. package/dist/src/browser/utils.js +9 -9
  47. package/dist/src/browserMode.js +1 -1
  48. package/dist/src/cli/bridge/claudeConfig.js +16 -16
  49. package/dist/src/cli/bridge/client.js +28 -20
  50. package/dist/src/cli/bridge/codexConfig.js +16 -16
  51. package/dist/src/cli/bridge/doctor.js +47 -39
  52. package/dist/src/cli/bridge/host.js +58 -56
  53. package/dist/src/cli/browserConfig.js +65 -45
  54. package/dist/src/cli/browserDefaults.js +27 -26
  55. package/dist/src/cli/bundleWarnings.js +1 -1
  56. package/dist/src/cli/clipboard.js +11 -2
  57. package/dist/src/cli/detach.js +7 -4
  58. package/dist/src/cli/dryRun.js +29 -25
  59. package/dist/src/cli/duplicatePromptGuard.js +3 -3
  60. package/dist/src/cli/engine.js +9 -9
  61. package/dist/src/cli/errorUtils.js +1 -1
  62. package/dist/src/cli/fileSize.js +11 -0
  63. package/dist/src/cli/format.js +2 -2
  64. package/dist/src/cli/help.js +28 -28
  65. package/dist/src/cli/hiddenAliases.js +3 -3
  66. package/dist/src/cli/markdownBundle.js +12 -8
  67. package/dist/src/cli/markdownRenderer.js +15 -15
  68. package/dist/src/cli/notifier.js +77 -67
  69. package/dist/src/cli/options.js +145 -87
  70. package/dist/src/cli/oscUtils.js +1 -1
  71. package/dist/src/cli/promptRequirement.js +2 -2
  72. package/dist/src/cli/renderOutput.js +1 -1
  73. package/dist/src/cli/rootAlias.js +1 -1
  74. package/dist/src/cli/runOptions.js +37 -25
  75. package/dist/src/cli/sessionCommand.js +31 -21
  76. package/dist/src/cli/sessionDisplay.js +182 -79
  77. package/dist/src/cli/sessionLineage.js +60 -0
  78. package/dist/src/cli/sessionRunner.js +118 -90
  79. package/dist/src/cli/sessionTable.js +28 -24
  80. package/dist/src/cli/stdin.js +22 -0
  81. package/dist/src/cli/tagline.js +121 -124
  82. package/dist/src/cli/tui/index.js +140 -127
  83. package/dist/src/cli/writeOutputPath.js +5 -5
  84. package/dist/src/config.js +7 -7
  85. package/dist/src/gemini-web/browserSessionManager.js +80 -0
  86. package/dist/src/gemini-web/client.js +81 -64
  87. package/dist/src/gemini-web/executionMode.js +16 -0
  88. package/dist/src/gemini-web/executor.js +327 -169
  89. package/dist/src/gemini-web/index.js +1 -1
  90. package/dist/src/mcp/server.js +16 -12
  91. package/dist/src/mcp/tools/consult.js +81 -64
  92. package/dist/src/mcp/tools/sessionResources.js +12 -12
  93. package/dist/src/mcp/tools/sessions.js +26 -17
  94. package/dist/src/mcp/types.js +5 -5
  95. package/dist/src/mcp/utils.js +15 -7
  96. package/dist/src/oracle/background.js +15 -15
  97. package/dist/src/oracle/claude.js +53 -25
  98. package/dist/src/oracle/client.js +84 -46
  99. package/dist/src/oracle/config.js +124 -58
  100. package/dist/src/oracle/errors.js +38 -38
  101. package/dist/src/oracle/files.js +69 -45
  102. package/dist/src/oracle/finishLine.js +10 -8
  103. package/dist/src/oracle/format.js +3 -3
  104. package/dist/src/oracle/gemini.js +37 -30
  105. package/dist/src/oracle/logging.js +7 -7
  106. package/dist/src/oracle/markdown.js +28 -28
  107. package/dist/src/oracle/modelResolver.js +16 -16
  108. package/dist/src/oracle/multiModelRunner.js +12 -12
  109. package/dist/src/oracle/oscProgress.js +8 -8
  110. package/dist/src/oracle/promptAssembly.js +6 -3
  111. package/dist/src/oracle/request.js +23 -15
  112. package/dist/src/oracle/run.js +172 -140
  113. package/dist/src/oracle/runUtils.js +8 -5
  114. package/dist/src/oracle/tokenEstimate.js +6 -6
  115. package/dist/src/oracle/tokenStats.js +5 -5
  116. package/dist/src/oracle/tokenStringifier.js +5 -5
  117. package/dist/src/oracle.js +12 -12
  118. package/dist/src/oracleHome.js +3 -3
  119. package/dist/src/remote/client.js +25 -25
  120. package/dist/src/remote/health.js +20 -20
  121. package/dist/src/remote/remoteServiceConfig.js +9 -9
  122. package/dist/src/remote/server.js +129 -118
  123. package/dist/src/sessionManager.js +81 -75
  124. package/dist/src/sessionStore.js +3 -3
  125. package/dist/src/version.js +10 -10
  126. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  127. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  128. package/dist/vendor/oracle-notifier/README.md +2 -0
  129. package/package.json +69 -65
  130. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  131. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  132. package/vendor/oracle-notifier/README.md +2 -0
  133. package/dist/markdansi/types/index.js +0 -4
  134. package/dist/oracle/bin/oracle-cli.js +0 -472
  135. package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
  136. package/dist/oracle/src/browser/actions/attachments.js +0 -82
  137. package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
  138. package/dist/oracle/src/browser/actions/navigation.js +0 -75
  139. package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
  140. package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
  141. package/dist/oracle/src/browser/config.js +0 -33
  142. package/dist/oracle/src/browser/constants.js +0 -40
  143. package/dist/oracle/src/browser/cookies.js +0 -210
  144. package/dist/oracle/src/browser/domDebug.js +0 -36
  145. package/dist/oracle/src/browser/index.js +0 -331
  146. package/dist/oracle/src/browser/pageActions.js +0 -5
  147. package/dist/oracle/src/browser/prompt.js +0 -88
  148. package/dist/oracle/src/browser/promptSummary.js +0 -20
  149. package/dist/oracle/src/browser/sessionRunner.js +0 -80
  150. package/dist/oracle/src/browser/utils.js +0 -62
  151. package/dist/oracle/src/browserMode.js +0 -1
  152. package/dist/oracle/src/cli/browserConfig.js +0 -44
  153. package/dist/oracle/src/cli/dryRun.js +0 -59
  154. package/dist/oracle/src/cli/engine.js +0 -17
  155. package/dist/oracle/src/cli/errorUtils.js +0 -9
  156. package/dist/oracle/src/cli/help.js +0 -70
  157. package/dist/oracle/src/cli/markdownRenderer.js +0 -15
  158. package/dist/oracle/src/cli/options.js +0 -103
  159. package/dist/oracle/src/cli/promptRequirement.js +0 -14
  160. package/dist/oracle/src/cli/rootAlias.js +0 -30
  161. package/dist/oracle/src/cli/sessionCommand.js +0 -77
  162. package/dist/oracle/src/cli/sessionDisplay.js +0 -270
  163. package/dist/oracle/src/cli/sessionRunner.js +0 -94
  164. package/dist/oracle/src/heartbeat.js +0 -43
  165. package/dist/oracle/src/oracle/client.js +0 -48
  166. package/dist/oracle/src/oracle/config.js +0 -29
  167. package/dist/oracle/src/oracle/errors.js +0 -101
  168. package/dist/oracle/src/oracle/files.js +0 -220
  169. package/dist/oracle/src/oracle/format.js +0 -33
  170. package/dist/oracle/src/oracle/fsAdapter.js +0 -7
  171. package/dist/oracle/src/oracle/oscProgress.js +0 -60
  172. package/dist/oracle/src/oracle/request.js +0 -48
  173. package/dist/oracle/src/oracle/run.js +0 -444
  174. package/dist/oracle/src/oracle/tokenStats.js +0 -39
  175. package/dist/oracle/src/oracle/types.js +0 -1
  176. package/dist/oracle/src/oracle.js +0 -9
  177. package/dist/oracle/src/sessionManager.js +0 -205
  178. package/dist/oracle/src/version.js +0 -39
  179. package/dist/scripts/chrome/browser-tools.js +0 -295
  180. package/dist/src/browser/profileSync.js +0 -141
  181. /package/dist/{oracle/src/browser/types.js → src/gemini-web/executionClients.js} +0 -0
@@ -3,50 +3,50 @@
3
3
  * Sweetistics runner wrapper: enforces timeouts, git policy, and trash-safe deletes before dispatching any repo command.
4
4
  * When you tweak its behavior, add a short note to AGENTS.md via `./scripts/committer "docs: update AGENTS for runner" "AGENTS.md"` so other agents know the new expectations.
5
5
  */
6
- import { spawn } from 'node:child_process';
7
- import { cpSync, existsSync, renameSync, rmSync } from 'node:fs';
8
- import { constants as osConstants } from 'node:os';
9
- import { basename, isAbsolute, join, normalize, resolve } from 'node:path';
10
- import process from 'node:process';
11
- import { analyzeGitExecution, evaluateGitPolicies, } from './git-policy';
6
+ import { spawn } from "node:child_process";
7
+ import { cpSync, existsSync, renameSync, rmSync } from "node:fs";
8
+ import { constants as osConstants } from "node:os";
9
+ import { basename, isAbsolute, join, normalize, resolve } from "node:path";
10
+ import process from "node:process";
11
+ import { analyzeGitExecution, evaluateGitPolicies, } from "./git-policy";
12
12
  const DEFAULT_TIMEOUT_MS = 5 * 60 * 1000;
13
13
  const EXTENDED_TIMEOUT_MS = 20 * 60 * 1000;
14
14
  const LONG_TIMEOUT_MS = 25 * 60 * 1000; // Build + full-suite commands (Next.js build, test:all) routinely spike past 20 minutes—give them explicit headroom before tmux escalation.
15
15
  const LINT_TIMEOUT_MS = 30 * 60 * 1000;
16
16
  const LONG_RUN_REPORT_THRESHOLD_MS = 60 * 1000;
17
- const ENABLE_DEBUG_LOGS = process.env.RUNNER_DEBUG === '1';
17
+ const ENABLE_DEBUG_LOGS = process.env.RUNNER_DEBUG === "1";
18
18
  const MAX_SLEEP_SECONDS = 30;
19
19
  const WRAPPER_COMMANDS = new Set([
20
- 'sudo',
21
- '/usr/bin/sudo',
22
- 'env',
23
- '/usr/bin/env',
24
- 'command',
25
- '/bin/command',
26
- 'nohup',
27
- '/usr/bin/nohup',
20
+ "sudo",
21
+ "/usr/bin/sudo",
22
+ "env",
23
+ "/usr/bin/env",
24
+ "command",
25
+ "/bin/command",
26
+ "nohup",
27
+ "/usr/bin/nohup",
28
28
  ]);
29
29
  const SUMMARY_STYLE = resolveSummaryStyle(process.env.RUNNER_SUMMARY_STYLE);
30
30
  // biome-ignore format: keep each keyword on its own line for grep-friendly diffs.
31
31
  const LONG_SCRIPT_KEYWORDS = [
32
- 'build',
33
- 'test:all',
34
- 'test:browser',
35
- 'test:e2e',
36
- 'test:e2e:headed',
37
- 'vitest.browser',
38
- 'vitest.browser.config.ts',
32
+ "build",
33
+ "test:all",
34
+ "test:browser",
35
+ "test:e2e",
36
+ "test:e2e:headed",
37
+ "vitest.browser",
38
+ "vitest.browser.config.ts",
39
39
  ];
40
- const EXTENDED_SCRIPT_KEYWORDS = ['lint', 'test', 'playwright', 'check', 'docker'];
41
- const SINGLE_TEST_SCRIPTS = new Set(['test:file']);
42
- const SINGLE_TEST_FLAGS = new Set(['--run', '--filter']);
43
- const TEST_BINARIES = new Set(['vitest', 'playwright', 'jest']);
44
- const LINT_BINARIES = new Set(['eslint', 'biome', 'oxlint', 'knip']);
40
+ const EXTENDED_SCRIPT_KEYWORDS = ["lint", "test", "playwright", "check", "docker"];
41
+ const SINGLE_TEST_SCRIPTS = new Set(["test:file"]);
42
+ const SINGLE_TEST_FLAGS = new Set(["--run", "--filter"]);
43
+ const TEST_BINARIES = new Set(["vitest", "playwright", "jest"]);
44
+ const LINT_BINARIES = new Set(["eslint", "biome", "oxlint", "knip"]);
45
45
  let cachedTrashCliCommand;
46
46
  (async () => {
47
47
  const commandArgs = parseArgs(process.argv.slice(2));
48
48
  if (commandArgs.length === 0) {
49
- printUsage('Missing command to execute.');
49
+ printUsage("Missing command to execute.");
50
50
  process.exit(1);
51
51
  }
52
52
  const workspaceDir = process.cwd();
@@ -63,7 +63,7 @@ let cachedTrashCliCommand;
63
63
  enforceGitPolicies(interception.gitContext);
64
64
  await runCommand(context);
65
65
  })().catch((error) => {
66
- console.error('[runner] Unexpected failure:', error instanceof Error ? error.message : String(error));
66
+ console.error("[runner] Unexpected failure:", error instanceof Error ? error.message : String(error));
67
67
  process.exit(1);
68
68
  });
69
69
  // Parses the runner CLI args and rejects unsupported flags early.
@@ -75,16 +75,16 @@ function parseArgs(argv) {
75
75
  commandArgs.push(token);
76
76
  continue;
77
77
  }
78
- if (token === '--') {
78
+ if (token === "--") {
79
79
  parsingOptions = false;
80
80
  continue;
81
81
  }
82
- if (token === '--help' || token === '-h') {
82
+ if (token === "--help" || token === "-h") {
83
83
  printUsage();
84
84
  process.exit(0);
85
85
  }
86
- if (token === '--timeout' || token.startsWith('--timeout=')) {
87
- console.error('[runner] --timeout is no longer supported; rely on the automatic timeouts.');
86
+ if (token === "--timeout" || token.startsWith("--timeout=")) {
87
+ console.error("[runner] --timeout is no longer supported; rely on the automatic timeouts.");
88
88
  process.exit(1);
89
89
  }
90
90
  parsingOptions = false;
@@ -95,7 +95,7 @@ function parseArgs(argv) {
95
95
  // Computes the timeout tier for the provided command tokens.
96
96
  function determineEffectiveTimeoutMs(commandArgs) {
97
97
  const strippedTokens = stripWrappersAndAssignments(commandArgs);
98
- if (isTestRunnerSuiteInvocation(strippedTokens, 'integration')) {
98
+ if (isTestRunnerSuiteInvocation(strippedTokens, "integration")) {
99
99
  return EXTENDED_TIMEOUT_MS;
100
100
  }
101
101
  if (referencesIntegrationSpec(strippedTokens)) {
@@ -122,10 +122,10 @@ function shouldExtendTimeout(commandArgs) {
122
122
  if (!first) {
123
123
  return false;
124
124
  }
125
- if (first === 'pnpm') {
125
+ if (first === "pnpm") {
126
126
  return shouldExtendViaPnpm(rest);
127
127
  }
128
- if (first === 'bun') {
128
+ if (first === "bun") {
129
129
  return shouldExtendViaBun(rest);
130
130
  }
131
131
  if (shouldExtendForScript(first) || TEST_BINARIES.has(first.toLowerCase())) {
@@ -141,16 +141,19 @@ function shouldExtendViaPnpm(rest) {
141
141
  if (!subcommand) {
142
142
  return false;
143
143
  }
144
- if (subcommand === 'run') {
144
+ if (subcommand === "run") {
145
145
  const script = rest[1];
146
- return typeof script === 'string' && shouldExtendForScript(script);
146
+ return typeof script === "string" && shouldExtendForScript(script);
147
147
  }
148
- if (subcommand === 'exec') {
148
+ if (subcommand === "exec") {
149
149
  const execTarget = rest[1];
150
- if (execTarget && (shouldExtendForScript(execTarget) || TEST_BINARIES.has(execTarget.toLowerCase()))) {
150
+ if (execTarget &&
151
+ (shouldExtendForScript(execTarget) || TEST_BINARIES.has(execTarget.toLowerCase()))) {
151
152
  return true;
152
153
  }
153
- return rest.slice(1).some((token) => shouldExtendForScript(token) || TEST_BINARIES.has(token.toLowerCase()));
154
+ return rest
155
+ .slice(1)
156
+ .some((token) => shouldExtendForScript(token) || TEST_BINARIES.has(token.toLowerCase()));
154
157
  }
155
158
  return shouldExtendForScript(subcommand);
156
159
  }
@@ -162,14 +165,14 @@ function shouldExtendViaBun(rest) {
162
165
  if (!subcommand) {
163
166
  return false;
164
167
  }
165
- if (subcommand === 'run') {
168
+ if (subcommand === "run") {
166
169
  const script = rest[1];
167
- return typeof script === 'string' && shouldExtendForScript(script);
170
+ return typeof script === "string" && shouldExtendForScript(script);
168
171
  }
169
- if (subcommand === 'test') {
172
+ if (subcommand === "test") {
170
173
  return true;
171
174
  }
172
- if (subcommand === 'x' || subcommand === 'bunx') {
175
+ if (subcommand === "x" || subcommand === "bunx") {
173
176
  const execTarget = rest[1];
174
177
  if (execTarget && TEST_BINARIES.has(execTarget.toLowerCase())) {
175
178
  return true;
@@ -194,10 +197,10 @@ function shouldUseLintTimeout(commandArgs) {
194
197
  if (!first) {
195
198
  return false;
196
199
  }
197
- if (first === 'pnpm') {
200
+ if (first === "pnpm") {
198
201
  return shouldUseLintTimeoutViaPnpm(rest);
199
202
  }
200
- if (first === 'bun') {
203
+ if (first === "bun") {
201
204
  return shouldUseLintTimeoutViaBun(rest);
202
205
  }
203
206
  return LINT_BINARIES.has(first.toLowerCase());
@@ -210,11 +213,11 @@ function shouldUseLintTimeoutViaPnpm(rest) {
210
213
  if (!subcommand) {
211
214
  return false;
212
215
  }
213
- if (subcommand === 'run') {
216
+ if (subcommand === "run") {
214
217
  const script = rest[1];
215
- return typeof script === 'string' && script.startsWith('lint');
218
+ return typeof script === "string" && script.startsWith("lint");
216
219
  }
217
- if (subcommand === 'exec') {
220
+ if (subcommand === "exec") {
218
221
  const execTarget = rest[1];
219
222
  if (execTarget && LINT_BINARIES.has(execTarget.toLowerCase())) {
220
223
  return true;
@@ -231,11 +234,11 @@ function shouldUseLintTimeoutViaBun(rest) {
231
234
  if (!subcommand) {
232
235
  return false;
233
236
  }
234
- if (subcommand === 'run') {
237
+ if (subcommand === "run") {
235
238
  const script = rest[1];
236
- return typeof script === 'string' && script.startsWith('lint');
239
+ return typeof script === "string" && script.startsWith("lint");
237
240
  }
238
- if (subcommand === 'x' || subcommand === 'bunx') {
241
+ if (subcommand === "x" || subcommand === "bunx") {
239
242
  return rest.slice(1).some((token) => LINT_BINARIES.has(token.toLowerCase()));
240
243
  }
241
244
  return LINT_BINARIES.has(subcommand.toLowerCase());
@@ -253,13 +256,13 @@ function isSingleTestInvocation(commandArgs) {
253
256
  if (!first) {
254
257
  return false;
255
258
  }
256
- if (first === 'pnpm') {
259
+ if (first === "pnpm") {
257
260
  return isSingleTestViaPnpm(rest);
258
261
  }
259
- if (first === 'bun') {
262
+ if (first === "bun") {
260
263
  return isSingleTestViaBun(rest);
261
264
  }
262
- if (first === 'vitest') {
265
+ if (first === "vitest") {
263
266
  return rest.some((token) => SINGLE_TEST_FLAGS.has(token));
264
267
  }
265
268
  return SINGLE_TEST_SCRIPTS.has(first);
@@ -272,11 +275,11 @@ function isSingleTestViaPnpm(rest) {
272
275
  if (!subcommand) {
273
276
  return false;
274
277
  }
275
- if (subcommand === 'run') {
278
+ if (subcommand === "run") {
276
279
  const script = rest[1];
277
- return typeof script === 'string' && SINGLE_TEST_SCRIPTS.has(script);
280
+ return typeof script === "string" && SINGLE_TEST_SCRIPTS.has(script);
278
281
  }
279
- if (subcommand === 'exec') {
282
+ if (subcommand === "exec") {
280
283
  return rest.slice(1).some((token) => SINGLE_TEST_FLAGS.has(token));
281
284
  }
282
285
  return SINGLE_TEST_SCRIPTS.has(subcommand);
@@ -289,31 +292,31 @@ function isSingleTestViaBun(rest) {
289
292
  if (!subcommand) {
290
293
  return false;
291
294
  }
292
- if (subcommand === 'run') {
295
+ if (subcommand === "run") {
293
296
  const script = rest[1];
294
- return typeof script === 'string' && SINGLE_TEST_SCRIPTS.has(script);
297
+ return typeof script === "string" && SINGLE_TEST_SCRIPTS.has(script);
295
298
  }
296
- if (subcommand === 'test') {
299
+ if (subcommand === "test") {
297
300
  return true;
298
301
  }
299
- if (subcommand === 'x' || subcommand === 'bunx') {
302
+ if (subcommand === "x" || subcommand === "bunx") {
300
303
  return rest.slice(1).some((token) => SINGLE_TEST_FLAGS.has(token));
301
304
  }
302
305
  return false;
303
306
  }
304
307
  // Normalizes potential file paths/flags to aid comparison across shells.
305
308
  function normalizeForPathComparison(token) {
306
- return token.replaceAll('\\', '/');
309
+ return token.replaceAll("\\", "/");
307
310
  }
308
311
  // Heuristically checks if a CLI token references an integration spec.
309
312
  function tokenReferencesIntegrationTest(token) {
310
313
  const normalized = normalizeForPathComparison(token);
311
- if (normalized.includes('tests/integration/')) {
314
+ if (normalized.includes("tests/integration/")) {
312
315
  return true;
313
316
  }
314
- if (normalized.startsWith('--run=') || normalized.startsWith('--include=')) {
315
- const value = normalized.split('=', 2)[1] ?? '';
316
- return value.includes('tests/integration/');
317
+ if (normalized.startsWith("--run=") || normalized.startsWith("--include=")) {
318
+ const value = normalized.split("=", 2)[1] ?? "";
319
+ return value.includes("tests/integration/");
317
320
  }
318
321
  return false;
319
322
  }
@@ -324,7 +327,7 @@ function referencesIntegrationSpec(tokens) {
324
327
  if (!token) {
325
328
  continue;
326
329
  }
327
- if (token === '--run' || token === '--include') {
330
+ if (token === "--run" || token === "--include") {
328
331
  const next = tokens[index + 1];
329
332
  if (next && tokenReferencesIntegrationTest(next)) {
330
333
  return true;
@@ -391,8 +394,9 @@ function isTestRunnerSuiteInvocation(tokens, suite) {
391
394
  if (!token) {
392
395
  continue;
393
396
  }
394
- const normalizedToken = token.replace(/^[./\\]+/, '');
395
- if (normalizedToken === 'scripts/test-runner.ts' || normalizedToken.endsWith('/scripts/test-runner.ts')) {
397
+ const normalizedToken = token.replace(/^[./\\]+/, "");
398
+ if (normalizedToken === "scripts/test-runner.ts" ||
399
+ normalizedToken.endsWith("/scripts/test-runner.ts")) {
396
400
  const suiteToken = tokens[index + 1]?.toLowerCase();
397
401
  if (suiteToken === normalizedSuite) {
398
402
  return true;
@@ -413,7 +417,7 @@ function shouldUseLongTimeout(commandArgs) {
413
417
  }
414
418
  const rest = tokens.slice(1);
415
419
  const matches = (token) => matchesScriptKeyword(token, LONG_SCRIPT_KEYWORDS);
416
- if (first === 'pnpm') {
420
+ if (first === "pnpm") {
417
421
  if (rest.length === 0) {
418
422
  return false;
419
423
  }
@@ -421,7 +425,7 @@ function shouldUseLongTimeout(commandArgs) {
421
425
  if (!subcommand) {
422
426
  return false;
423
427
  }
424
- if (subcommand === 'run') {
428
+ if (subcommand === "run") {
425
429
  const script = rest[1];
426
430
  if (script && matches(script)) {
427
431
  return true;
@@ -455,20 +459,20 @@ async function runCommand(context) {
455
459
  const child = spawn(command, args, {
456
460
  cwd: context.workspaceDir,
457
461
  env,
458
- stdio: ['inherit', 'pipe', 'pipe'],
462
+ stdio: ["inherit", "pipe", "pipe"],
459
463
  });
460
464
  if (isRunnerTmuxSession()) {
461
- const childPidInfo = typeof child.pid === 'number' ? ` (pid ${child.pid})` : '';
465
+ const childPidInfo = typeof child.pid === "number" ? ` (pid ${child.pid})` : "";
462
466
  console.error(`[runner] Watching ${commandLabel}${childPidInfo}. Wait for the closing sentinel before moving on.`);
463
467
  }
464
468
  const removeSignalHandlers = registerSignalForwarding(child);
465
469
  if (child.stdout) {
466
- child.stdout.on('data', (chunk) => {
470
+ child.stdout.on("data", (chunk) => {
467
471
  process.stdout.write(chunk);
468
472
  });
469
473
  }
470
474
  if (child.stderr) {
471
- child.stderr.on('data', (chunk) => {
475
+ child.stderr.on("data", (chunk) => {
472
476
  process.stderr.write(chunk);
473
477
  });
474
478
  }
@@ -482,15 +486,15 @@ async function runCommand(context) {
482
486
  console.error(`[runner] Command exceeded ${formatDuration(context.timeoutMs)}; sending SIGTERM.`);
483
487
  }
484
488
  if (!child.killed) {
485
- child.kill('SIGTERM');
489
+ child.kill("SIGTERM");
486
490
  killTimer = setTimeout(() => {
487
491
  if (!child.killed) {
488
- child.kill('SIGKILL');
492
+ child.kill("SIGKILL");
489
493
  }
490
494
  }, 5_000);
491
495
  }
492
496
  }, context.timeoutMs);
493
- child.once('error', (error) => {
497
+ child.once("error", (error) => {
494
498
  clearTimeout(timeout);
495
499
  if (killTimer) {
496
500
  clearTimeout(killTimer);
@@ -498,7 +502,7 @@ async function runCommand(context) {
498
502
  removeSignalHandlers();
499
503
  reject(error);
500
504
  });
501
- child.once('exit', (code, signal) => {
505
+ child.once("exit", (code, signal) => {
502
506
  clearTimeout(timeout);
503
507
  if (killTimer) {
504
508
  clearTimeout(killTimer);
@@ -521,7 +525,7 @@ async function runCommand(context) {
521
525
  process.exit(exitCode);
522
526
  }
523
527
  catch (error) {
524
- console.error('[runner] Failed to launch command:', error instanceof Error ? error.message : String(error));
528
+ console.error("[runner] Failed to launch command:", error instanceof Error ? error.message : String(error));
525
529
  process.exit(1);
526
530
  return;
527
531
  }
@@ -533,16 +537,16 @@ async function runCommandWithoutTimeout(context) {
533
537
  const child = spawn(command, args, {
534
538
  cwd: context.workspaceDir,
535
539
  env,
536
- stdio: 'inherit',
540
+ stdio: "inherit",
537
541
  });
538
542
  const removeSignalHandlers = registerSignalForwarding(child);
539
543
  try {
540
544
  const exitCode = await new Promise((resolve, reject) => {
541
- child.once('error', (error) => {
545
+ child.once("error", (error) => {
542
546
  removeSignalHandlers();
543
547
  reject(error);
544
548
  });
545
- child.once('exit', (code, signal) => {
549
+ child.once("exit", (code, signal) => {
546
550
  removeSignalHandlers();
547
551
  resolve(code ?? exitCodeFromSignal(signal));
548
552
  });
@@ -552,7 +556,7 @@ async function runCommandWithoutTimeout(context) {
552
556
  process.exit(exitCode);
553
557
  }
554
558
  catch (error) {
555
- console.error('[runner] Failed to launch command:', error instanceof Error ? error.message : String(error));
559
+ console.error("[runner] Failed to launch command:", error instanceof Error ? error.message : String(error));
556
560
  process.exit(1);
557
561
  }
558
562
  }
@@ -564,9 +568,9 @@ function buildExecutionParams(commandArgs, workspaceDir) {
564
568
  let commandStarted = false;
565
569
  for (const token of commandArgs) {
566
570
  if (!commandStarted && isEnvAssignment(token)) {
567
- const [key, ...rest] = token.split('=');
571
+ const [key, ...rest] = token.split("=");
568
572
  if (key) {
569
- env[key] = rest.join('=');
573
+ env[key] = rest.join("=");
570
574
  }
571
575
  continue;
572
576
  }
@@ -574,7 +578,7 @@ function buildExecutionParams(commandArgs, workspaceDir) {
574
578
  args.push(token);
575
579
  }
576
580
  if (args.length === 0 || !args[0]) {
577
- printUsage('Missing command to execute.');
581
+ printUsage("Missing command to execute.");
578
582
  process.exit(1);
579
583
  }
580
584
  const [command, ...restArgs] = args;
@@ -584,13 +588,10 @@ function injectWorkspaceBinDirs(env, workspaceDir) {
584
588
  if (ENABLE_DEBUG_LOGS) {
585
589
  console.error(`[runner] Checking workspace bin dirs under ${workspaceDir}`);
586
590
  }
587
- const binCandidates = [
588
- join(workspaceDir, 'node_modules', '.bin'),
589
- join(workspaceDir, 'bin'),
590
- ];
591
- const existingPath = env.PATH ?? process.env.PATH ?? '';
591
+ const binCandidates = [join(workspaceDir, "node_modules", ".bin"), join(workspaceDir, "bin")];
592
+ const existingPath = env.PATH ?? process.env.PATH ?? "";
592
593
  const existingSegments = existingPath
593
- .split(':')
594
+ .split(":")
594
595
  .map((segment) => segment.trim())
595
596
  .filter((segment) => segment.length > 0);
596
597
  const additions = [];
@@ -607,14 +608,14 @@ function injectWorkspaceBinDirs(env, workspaceDir) {
607
608
  return;
608
609
  }
609
610
  if (ENABLE_DEBUG_LOGS) {
610
- console.error(`[runner] Prepending workspace PATH entries: ${additions.join(', ')}`);
611
+ console.error(`[runner] Prepending workspace PATH entries: ${additions.join(", ")}`);
611
612
  }
612
613
  const merged = [...additions, ...existingSegments];
613
- env.PATH = merged.join(':');
614
+ env.PATH = merged.join(":");
614
615
  }
615
616
  // Forwards termination signals to the child and returns an unregister hook.
616
617
  function registerSignalForwarding(child) {
617
- const signals = ['SIGINT', 'SIGTERM'];
618
+ const signals = ["SIGINT", "SIGTERM"];
618
619
  const handlers = new Map();
619
620
  for (const signal of signals) {
620
621
  const handler = () => {
@@ -637,7 +638,7 @@ function exitCodeFromSignal(signal) {
637
638
  return 0;
638
639
  }
639
640
  const code = osConstants.signals[signal];
640
- if (typeof code === 'number') {
641
+ if (typeof code === "number") {
641
642
  return 128 + code;
642
643
  }
643
644
  return 1;
@@ -664,8 +665,8 @@ async function resolveCommandInterception(context) {
664
665
  // Runs the shared git policy analyzers before dispatching the command.
665
666
  function enforceGitPolicies(gitContext) {
666
667
  const evaluation = evaluateGitPolicies(gitContext);
667
- const hasConsentOverride = process.env.RUNNER_THE_USER_GAVE_ME_CONSENT === '1';
668
- if (gitContext.subcommand === 'rebase' && !hasConsentOverride) {
668
+ const hasConsentOverride = process.env.RUNNER_THE_USER_GAVE_ME_CONSENT === "1";
669
+ if (gitContext.subcommand === "rebase" && !hasConsentOverride) {
669
670
  console.error('git rebase requires the user to explicitly type "rebase" in chat. Once they do, rerun with RUNNER_THE_USER_GAVE_ME_CONSENT=1 in the same command (e.g. RUNNER_THE_USER_GAVE_ME_CONSENT=1 ./runner git rebase --continue).');
670
671
  process.exit(1);
671
672
  }
@@ -676,16 +677,16 @@ function enforceGitPolicies(gitContext) {
676
677
  if (evaluation.requiresExplicitConsent || evaluation.isDestructive) {
677
678
  if (hasConsentOverride) {
678
679
  if (ENABLE_DEBUG_LOGS) {
679
- const reason = evaluation.isDestructive ? 'destructive git command' : 'guarded git command';
680
+ const reason = evaluation.isDestructive ? "destructive git command" : "guarded git command";
680
681
  console.error(`[runner] Proceeding with ${reason} because RUNNER_THE_USER_GAVE_ME_CONSENT=1.`);
681
682
  }
682
683
  }
683
684
  else {
684
685
  if (evaluation.isDestructive) {
685
- console.error(`git ${gitContext.subcommand ?? ''} can overwrite or discard work. Confirm with the user first, then re-run with RUNNER_THE_USER_GAVE_ME_CONSENT=1 if they approve.`);
686
+ console.error(`git ${gitContext.subcommand ?? ""} can overwrite or discard work. Confirm with the user first, then re-run with RUNNER_THE_USER_GAVE_ME_CONSENT=1 if they approve.`);
686
687
  }
687
688
  else {
688
- console.error(`Using git ${gitContext.subcommand ?? ''} requires consent. Set RUNNER_THE_USER_GAVE_ME_CONSENT=1 after verifying with the user, or ask them explicitly before proceeding.`);
689
+ console.error(`Using git ${gitContext.subcommand ?? ""} requires consent. Set RUNNER_THE_USER_GAVE_ME_CONSENT=1 after verifying with the user, or ask them explicitly before proceeding.`);
689
690
  }
690
691
  process.exit(1);
691
692
  }
@@ -701,7 +702,9 @@ async function maybeHandleFindInvocation(context) {
701
702
  if (!findPlan) {
702
703
  return false;
703
704
  }
704
- const moveResult = await movePathsToTrash(findPlan.paths, context.workspaceDir, { allowMissing: false });
705
+ const moveResult = await movePathsToTrash(findPlan.paths, context.workspaceDir, {
706
+ allowMissing: false,
707
+ });
705
708
  if (moveResult.missing.length > 0) {
706
709
  for (const path of moveResult.missing) {
707
710
  console.error(`find: ${path}: No such file or directory`);
@@ -728,7 +731,9 @@ async function maybeHandleRmInvocation(context) {
728
731
  return false;
729
732
  }
730
733
  try {
731
- const moveResult = await movePathsToTrash(rmPlan.targets, context.workspaceDir, { allowMissing: rmPlan.force });
734
+ const moveResult = await movePathsToTrash(rmPlan.targets, context.workspaceDir, {
735
+ allowMissing: rmPlan.force,
736
+ });
732
737
  reportMissingForRm(moveResult.missing, rmPlan.force);
733
738
  if (moveResult.errors.length > 0) {
734
739
  for (const error of moveResult.errors) {
@@ -746,7 +751,7 @@ async function maybeHandleRmInvocation(context) {
746
751
  }
747
752
  // Applies git-specific rm protections before the command executes.
748
753
  async function maybeHandleGitRm(gitContext) {
749
- if (gitContext.command?.name !== 'rm' || !gitContext.invocation) {
754
+ if (gitContext.command?.name !== "rm" || !gitContext.invocation) {
750
755
  return false;
751
756
  }
752
757
  const gitRmPlan = parseGitRmArguments(gitContext.invocation.argv, gitContext.command);
@@ -806,7 +811,7 @@ async function maybeHandleSleepInvocation(context) {
806
811
  if (adjustments.length === 0) {
807
812
  return false;
808
813
  }
809
- console.error(`[runner] sleep arguments exceed ${MAX_SLEEP_SECONDS}s; clamping (${adjustments.join(', ')}).`);
814
+ console.error(`[runner] sleep arguments exceed ${MAX_SLEEP_SECONDS}s; clamping (${adjustments.join(", ")}).`);
810
815
  context.commandArgs = adjustedArgs;
811
816
  return false;
812
817
  }
@@ -819,10 +824,10 @@ async function maybeHandleTmuxInvocation(context) {
819
824
  if (!candidate) {
820
825
  return false;
821
826
  }
822
- if (basename(candidate) !== 'tmux') {
827
+ if (basename(candidate) !== "tmux") {
823
828
  return false;
824
829
  }
825
- console.error('[runner] Detected tmux invocation; executing command without runner timeout guardrails.');
830
+ console.error("[runner] Detected tmux invocation; executing command without runner timeout guardrails.");
826
831
  await runCommandWithoutTimeout(context);
827
832
  return true;
828
833
  }
@@ -835,8 +840,8 @@ function parseSleepDurationSeconds(token) {
835
840
  if (!Number.isFinite(value)) {
836
841
  return null;
837
842
  }
838
- const unit = match[2]?.toLowerCase() ?? '';
839
- const multiplier = unit === 'm' ? 60 : unit === 'h' ? 60 * 60 : unit === 'd' ? 60 * 60 * 24 : 1;
843
+ const unit = match[2]?.toLowerCase() ?? "";
844
+ const multiplier = unit === "m" ? 60 : unit === "h" ? 60 * 60 : unit === "d" ? 60 * 60 * 24 : 1;
840
845
  return value * multiplier;
841
846
  }
842
847
  function formatSleepArgument(seconds) {
@@ -849,12 +854,12 @@ function formatSleepDuration(seconds) {
849
854
  return `${seconds.toFixed(2)}s`;
850
855
  }
851
856
  function isSleepBinary(token) {
852
- return token === 'sleep' || token.endsWith('/sleep');
857
+ return token === "sleep" || token.endsWith("/sleep");
853
858
  }
854
859
  // Detects `git find` invocations that need policy enforcement.
855
860
  function extractFindInvocation(commandArgs) {
856
861
  for (const [index, token] of commandArgs.entries()) {
857
- if (token === 'find' || token.endsWith('/find')) {
862
+ if (token === "find" || token.endsWith("/find")) {
858
863
  return { index, argv: commandArgs.slice(index) };
859
864
  }
860
865
  }
@@ -866,14 +871,14 @@ function extractRmInvocation(commandArgs) {
866
871
  return null;
867
872
  }
868
873
  const wrappers = new Set([
869
- 'sudo',
870
- '/usr/bin/sudo',
871
- 'env',
872
- '/usr/bin/env',
873
- 'command',
874
- '/bin/command',
875
- 'nohup',
876
- '/usr/bin/nohup',
874
+ "sudo",
875
+ "/usr/bin/sudo",
876
+ "env",
877
+ "/usr/bin/env",
878
+ "command",
879
+ "/bin/command",
880
+ "nohup",
881
+ "/usr/bin/nohup",
877
882
  ]);
878
883
  let index = 0;
879
884
  while (index < commandArgs.length) {
@@ -881,7 +886,7 @@ function extractRmInvocation(commandArgs) {
881
886
  if (!token) {
882
887
  break;
883
888
  }
884
- if (token.includes('=') && !token.startsWith('-')) {
889
+ if (token.includes("=") && !token.startsWith("-")) {
885
890
  index += 1;
886
891
  continue;
887
892
  }
@@ -895,10 +900,10 @@ function extractRmInvocation(commandArgs) {
895
900
  if (!commandToken) {
896
901
  return null;
897
902
  }
898
- const isRmCommand = commandToken === 'rm' ||
899
- commandToken.endsWith('/rm') ||
900
- commandToken === 'rm.exe' ||
901
- commandToken.endsWith('\\rm.exe');
903
+ const isRmCommand = commandToken === "rm" ||
904
+ commandToken.endsWith("/rm") ||
905
+ commandToken === "rm.exe" ||
906
+ commandToken.endsWith("\\rm.exe");
902
907
  if (!isRmCommand) {
903
908
  return null;
904
909
  }
@@ -906,25 +911,25 @@ function extractRmInvocation(commandArgs) {
906
911
  }
907
912
  // Expands guarded find expressions into an explicit delete plan for review.
908
913
  async function buildFindDeletePlan(findArgs, workspaceDir) {
909
- if (!findArgs.some((token) => token === '-delete')) {
914
+ if (!findArgs.some((token) => token === "-delete")) {
910
915
  return null;
911
916
  }
912
- if (findArgs.some((token) => token === '-exec' || token === '-execdir' || token === '-ok' || token === '-okdir')) {
913
- console.error('Runner cannot safely translate find invocations that combine -delete with -exec/-ok. Run the command manually after reviewing the paths.');
917
+ if (findArgs.some((token) => token === "-exec" || token === "-execdir" || token === "-ok" || token === "-okdir")) {
918
+ console.error("Runner cannot safely translate find invocations that combine -delete with -exec/-ok. Run the command manually after reviewing the paths.");
914
919
  process.exit(1);
915
920
  }
916
921
  const printableArgs = [];
917
922
  for (const token of findArgs) {
918
- if (token === '-delete') {
923
+ if (token === "-delete") {
919
924
  continue;
920
925
  }
921
926
  printableArgs.push(token);
922
927
  }
923
- printableArgs.push('-print0');
928
+ printableArgs.push("-print0");
924
929
  const proc = Bun.spawn(printableArgs, {
925
930
  cwd: workspaceDir,
926
- stdout: 'pipe',
927
- stderr: 'pipe',
931
+ stdout: "pipe",
932
+ stderr: "pipe",
928
933
  });
929
934
  const [exitCode, stdoutBuf, stderrBuf] = await Promise.all([
930
935
  proc.exited,
@@ -942,7 +947,7 @@ async function buildFindDeletePlan(findArgs, workspaceDir) {
942
947
  }
943
948
  process.exit(exitCode);
944
949
  }
945
- const matches = stdoutBuf.split('\0').filter((entry) => entry.length > 0);
950
+ const matches = stdoutBuf.split("\0").filter((entry) => entry.length > 0);
946
951
  if (matches.length === 0) {
947
952
  return { paths: [] };
948
953
  }
@@ -952,7 +957,7 @@ async function buildFindDeletePlan(findArgs, workspaceDir) {
952
957
  const absolute = isAbsolute(match) ? match : resolve(workspaceDir, match);
953
958
  const canonical = normalize(absolute);
954
959
  if (canonical === workspaceCanonical) {
955
- console.error('Refusing to trash the current workspace via find -delete. Narrow your find predicate.');
960
+ console.error("Refusing to trash the current workspace via find -delete. Narrow your find predicate.");
956
961
  process.exit(1);
957
962
  }
958
963
  if (!uniquePaths.has(canonical)) {
@@ -975,19 +980,19 @@ function parseRmArguments(argv) {
975
980
  if (token === undefined) {
976
981
  break;
977
982
  }
978
- if (!treatAsTarget && token === '--') {
983
+ if (!treatAsTarget && token === "--") {
979
984
  treatAsTarget = true;
980
985
  index += 1;
981
986
  continue;
982
987
  }
983
- if (!treatAsTarget && token.startsWith('-') && token.length > 1) {
984
- if (token.includes('f')) {
988
+ if (!treatAsTarget && token.startsWith("-") && token.length > 1) {
989
+ if (token.includes("f")) {
985
990
  force = true;
986
991
  }
987
- if (token.includes('i') || token === '--interactive') {
992
+ if (token.includes("i") || token === "--interactive") {
988
993
  return null;
989
994
  }
990
- if (token === '--help' || token === '--version') {
995
+ if (token === "--help" || token === "--version") {
991
996
  return null;
992
997
  }
993
998
  index += 1;
@@ -1006,7 +1011,7 @@ function parseRmArguments(argv) {
1006
1011
  function parseGitRmArguments(argv, command) {
1007
1012
  const stagingOptions = [];
1008
1013
  const paths = [];
1009
- const optionsExpectingValue = new Set(['--pathspec-from-file']);
1014
+ const optionsExpectingValue = new Set(["--pathspec-from-file"]);
1010
1015
  let allowMissing = false;
1011
1016
  let treatAsPath = false;
1012
1017
  let index = command.index + 1;
@@ -1015,16 +1020,16 @@ function parseGitRmArguments(argv, command) {
1015
1020
  if (token === undefined) {
1016
1021
  break;
1017
1022
  }
1018
- if (!treatAsPath && token === '--') {
1023
+ if (!treatAsPath && token === "--") {
1019
1024
  treatAsPath = true;
1020
1025
  index += 1;
1021
1026
  continue;
1022
1027
  }
1023
- if (!treatAsPath && token.startsWith('-') && token.length > 1) {
1024
- if (token === '--cached' || token === '--dry-run' || token === '-n') {
1028
+ if (!treatAsPath && token.startsWith("-") && token.length > 1) {
1029
+ if (token === "--cached" || token === "--dry-run" || token === "-n") {
1025
1030
  return null;
1026
1031
  }
1027
- if (token === '--ignore-unmatch' || token === '--force' || token === '-f') {
1032
+ if (token === "--ignore-unmatch" || token === "--force" || token === "-f") {
1028
1033
  allowMissing = true;
1029
1034
  stagingOptions.push(token);
1030
1035
  index += 1;
@@ -1041,21 +1046,21 @@ function parseGitRmArguments(argv, command) {
1041
1046
  }
1042
1047
  continue;
1043
1048
  }
1044
- if (!token.startsWith('--')) {
1045
- const flags = token.slice(1).split('');
1049
+ if (!token.startsWith("--")) {
1050
+ const flags = token.slice(1).split("");
1046
1051
  const retainedFlags = [];
1047
1052
  for (const flag of flags) {
1048
- if (flag === 'n') {
1053
+ if (flag === "n") {
1049
1054
  return null;
1050
1055
  }
1051
- if (flag === 'f') {
1056
+ if (flag === "f") {
1052
1057
  allowMissing = true;
1053
1058
  continue;
1054
1059
  }
1055
1060
  retainedFlags.push(flag);
1056
1061
  }
1057
1062
  if (retainedFlags.length > 0) {
1058
- stagingOptions.push(`-${retainedFlags.join('')}`);
1063
+ stagingOptions.push(`-${retainedFlags.join("")}`);
1059
1064
  }
1060
1065
  index += 1;
1061
1066
  continue;
@@ -1111,10 +1116,13 @@ async function movePathsToTrash(paths, baseDir, options) {
1111
1116
  try {
1112
1117
  const cliArgs = [trashCliCommand, ...existing.map((item) => item.absolute)];
1113
1118
  const proc = Bun.spawn(cliArgs, {
1114
- stdout: 'ignore',
1115
- stderr: 'pipe',
1119
+ stdout: "ignore",
1120
+ stderr: "pipe",
1116
1121
  });
1117
- const [exitCode, stderrText] = await Promise.all([proc.exited, readProcessStream(proc.stderr)]);
1122
+ const [exitCode, stderrText] = await Promise.all([
1123
+ proc.exited,
1124
+ readProcessStream(proc.stderr),
1125
+ ]);
1118
1126
  if (exitCode === 0) {
1119
1127
  return { missing, errors: [] };
1120
1128
  }
@@ -1132,7 +1140,7 @@ async function movePathsToTrash(paths, baseDir, options) {
1132
1140
  if (!trashDir) {
1133
1141
  return {
1134
1142
  missing,
1135
- errors: ['Unable to locate macOS Trash directory (HOME/.Trash).'],
1143
+ errors: ["Unable to locate macOS Trash directory (HOME/.Trash)."],
1136
1144
  };
1137
1145
  }
1138
1146
  const errors = [];
@@ -1160,7 +1168,7 @@ async function movePathsToTrash(paths, baseDir, options) {
1160
1168
  }
1161
1169
  // Resolves a potentially relative path against the workspace root.
1162
1170
  function resolvePath(baseDir, input) {
1163
- if (input.startsWith('/')) {
1171
+ if (input.startsWith("/")) {
1164
1172
  return input;
1165
1173
  }
1166
1174
  return resolve(baseDir, input);
@@ -1171,7 +1179,7 @@ function getTrashDirectory() {
1171
1179
  if (!home) {
1172
1180
  return null;
1173
1181
  }
1174
- const trash = join(home, '.Trash');
1182
+ const trash = join(home, ".Trash");
1175
1183
  if (!existsSync(trash)) {
1176
1184
  return null;
1177
1185
  }
@@ -1184,14 +1192,14 @@ function buildTrashTarget(trashDir, absolutePath) {
1184
1192
  let attempt = 0;
1185
1193
  let candidate = join(trashDir, baseName);
1186
1194
  while (existsSync(candidate)) {
1187
- candidate = join(trashDir, `${baseName}-${timestamp}${attempt > 0 ? `-${attempt}` : ''}`);
1195
+ candidate = join(trashDir, `${baseName}-${timestamp}${attempt > 0 ? `-${attempt}` : ""}`);
1188
1196
  attempt += 1;
1189
1197
  }
1190
1198
  return candidate;
1191
1199
  }
1192
1200
  // Determines whether a rename failed because the devices differ.
1193
1201
  function isCrossDeviceError(error) {
1194
- return error instanceof Error && 'code' in error && error.code === 'EXDEV';
1202
+ return (error instanceof Error && "code" in error && error.code === "EXDEV");
1195
1203
  }
1196
1204
  // Normalizes trash/rename errors into a readable string.
1197
1205
  function formatTrashError(error) {
@@ -1205,11 +1213,11 @@ async function stageGitRm(workDir, plan) {
1205
1213
  if (plan.paths.length === 0) {
1206
1214
  return;
1207
1215
  }
1208
- const args = ['git', 'rm', '--cached', '--quiet', ...plan.stagingOptions, '--', ...plan.paths];
1216
+ const args = ["git", "rm", "--cached", "--quiet", ...plan.stagingOptions, "--", ...plan.paths];
1209
1217
  const proc = Bun.spawn(args, {
1210
1218
  cwd: workDir,
1211
- stdout: 'inherit',
1212
- stderr: 'inherit',
1219
+ stdout: "inherit",
1220
+ stderr: "inherit",
1213
1221
  });
1214
1222
  const exitCode = await proc.exited;
1215
1223
  if (exitCode !== 0) {
@@ -1221,18 +1229,18 @@ async function findTrashCliCommand() {
1221
1229
  if (cachedTrashCliCommand !== undefined) {
1222
1230
  return cachedTrashCliCommand;
1223
1231
  }
1224
- const candidateNames = ['trash-put', 'trash'];
1232
+ const candidateNames = ["trash-put", "trash"];
1225
1233
  const searchDirs = new Set();
1226
1234
  if (process.env.PATH) {
1227
- for (const segment of process.env.PATH.split(':')) {
1235
+ for (const segment of process.env.PATH.split(":")) {
1228
1236
  if (segment && segment.length > 0) {
1229
1237
  searchDirs.add(segment);
1230
1238
  }
1231
1239
  }
1232
1240
  }
1233
- const homebrewPrefix = process.env.HOMEBREW_PREFIX ?? '/opt/homebrew';
1234
- searchDirs.add(join(homebrewPrefix, 'opt', 'trash', 'bin'));
1235
- searchDirs.add('/usr/local/opt/trash/bin');
1241
+ const homebrewPrefix = process.env.HOMEBREW_PREFIX ?? "/opt/homebrew";
1242
+ searchDirs.add(join(homebrewPrefix, "opt", "trash", "bin"));
1243
+ searchDirs.add("/usr/local/opt/trash/bin");
1236
1244
  const candidatePaths = new Set();
1237
1245
  for (const name of candidateNames) {
1238
1246
  candidatePaths.add(name);
@@ -1242,9 +1250,9 @@ async function findTrashCliCommand() {
1242
1250
  }
1243
1251
  for (const candidate of candidatePaths) {
1244
1252
  try {
1245
- const proc = Bun.spawn([candidate, '--help'], {
1246
- stdout: 'ignore',
1247
- stderr: 'ignore',
1253
+ const proc = Bun.spawn([candidate, "--help"], {
1254
+ stdout: "ignore",
1255
+ stderr: "ignore",
1248
1256
  });
1249
1257
  const exitCode = await proc.exited;
1250
1258
  if (exitCode === 0 || exitCode === 1) {
@@ -1264,12 +1272,12 @@ async function findTrashCliCommand() {
1264
1272
  // Consumes a child process stream to completion for logging/error output.
1265
1273
  async function readProcessStream(stream) {
1266
1274
  if (!stream) {
1267
- return '';
1275
+ return "";
1268
1276
  }
1269
1277
  try {
1270
1278
  const candidate = stream;
1271
1279
  if (candidate.text) {
1272
- return (await candidate.text()) ?? '';
1280
+ return (await candidate.text()) ?? "";
1273
1281
  }
1274
1282
  }
1275
1283
  catch {
@@ -1279,22 +1287,22 @@ async function readProcessStream(stream) {
1279
1287
  if (stream instanceof ReadableStream) {
1280
1288
  return await new Response(stream).text();
1281
1289
  }
1282
- if (typeof stream === 'object' && stream !== null) {
1290
+ if (typeof stream === "object" && stream !== null) {
1283
1291
  return await new Response(stream).text();
1284
1292
  }
1285
1293
  }
1286
1294
  catch {
1287
1295
  // ignore errors and return empty string
1288
1296
  }
1289
- return '';
1297
+ return "";
1290
1298
  }
1291
1299
  // Shows CLI usage plus optional error messaging.
1292
1300
  function printUsage(message) {
1293
1301
  if (message) {
1294
1302
  console.error(`[runner] ${message}`);
1295
1303
  }
1296
- console.error('Usage: runner [--] <command...>');
1297
- console.error('');
1304
+ console.error("Usage: runner [--] <command...>");
1305
+ console.error("");
1298
1306
  console.error(`Defaults: ${formatDuration(DEFAULT_TIMEOUT_MS)} timeout for most commands, ${formatDuration(EXTENDED_TIMEOUT_MS)} when lint/test suites are detected.`);
1299
1307
  }
1300
1308
  // Pretty-prints a millisecond duration for logs.
@@ -1323,56 +1331,56 @@ function formatDuration(durationMs) {
1323
1331
  }
1324
1332
  function resolveSummaryStyle(rawValue) {
1325
1333
  if (!rawValue) {
1326
- return 'compact';
1334
+ return "compact";
1327
1335
  }
1328
1336
  const normalized = rawValue.trim().toLowerCase();
1329
1337
  switch (normalized) {
1330
- case 'minimal':
1331
- return 'minimal';
1332
- case 'verbose':
1333
- return 'verbose';
1334
- case 'short':
1335
- return 'compact';
1338
+ case "minimal":
1339
+ return "minimal";
1340
+ case "verbose":
1341
+ return "verbose";
1342
+ case "short":
1343
+ return "compact";
1336
1344
  default:
1337
- return 'compact';
1345
+ return "compact";
1338
1346
  }
1339
1347
  }
1340
1348
  function formatCompletionSummary(options) {
1341
1349
  const { exitCode, elapsedMs, timedOut, commandLabel } = options;
1342
- const durationText = typeof elapsedMs === 'number' ? formatDuration(elapsedMs) : null;
1350
+ const durationText = typeof elapsedMs === "number" ? formatDuration(elapsedMs) : null;
1343
1351
  // biome-ignore lint/nursery/noUnnecessaryConditions: switch makes the formatter easier to scan.
1344
1352
  switch (SUMMARY_STYLE) {
1345
- case 'minimal': {
1353
+ case "minimal": {
1346
1354
  const parts = [`${exitCode}`];
1347
1355
  if (durationText) {
1348
1356
  parts.push(durationText);
1349
1357
  }
1350
1358
  if (timedOut) {
1351
- parts.push('timeout');
1359
+ parts.push("timeout");
1352
1360
  }
1353
- return `[runner] ${parts.join(' · ')}`;
1361
+ return `[runner] ${parts.join(" · ")}`;
1354
1362
  }
1355
- case 'verbose': {
1356
- const elapsedPart = durationText ? `, elapsed ${durationText}` : '';
1357
- const timeoutPart = timedOut ? '; timed out' : '';
1363
+ case "verbose": {
1364
+ const elapsedPart = durationText ? `, elapsed ${durationText}` : "";
1365
+ const timeoutPart = timedOut ? "; timed out" : "";
1358
1366
  return `[runner] Finished ${commandLabel} (exit ${exitCode}${elapsedPart}${timeoutPart}).`;
1359
1367
  }
1360
1368
  default: {
1361
- const elapsedPart = durationText ? ` in ${durationText}` : '';
1362
- const timeoutPart = timedOut ? ' (timeout)' : '';
1369
+ const elapsedPart = durationText ? ` in ${durationText}` : "";
1370
+ const timeoutPart = timedOut ? " (timeout)" : "";
1363
1371
  return `[runner] exit ${exitCode}${elapsedPart}${timeoutPart}`;
1364
1372
  }
1365
1373
  }
1366
1374
  }
1367
1375
  // Joins the command args in a shell-friendly way for log display.
1368
1376
  function formatDisplayCommand(commandArgs) {
1369
- return commandArgs.map((token) => (token.includes(' ') ? `"${token}"` : token)).join(' ');
1377
+ return commandArgs.map((token) => (token.includes(" ") ? `"${token}"` : token)).join(" ");
1370
1378
  }
1371
1379
  // Tells whether the runner is already executing inside the tmux guard.
1372
1380
  function isRunnerTmuxSession() {
1373
1381
  const value = process.env.RUNNER_TMUX;
1374
1382
  if (value) {
1375
- return value !== '0' && value.toLowerCase() !== 'false';
1383
+ return value !== "0" && value.toLowerCase() !== "false";
1376
1384
  }
1377
1385
  return Boolean(process.env.TMUX);
1378
1386
  }