@steipete/oracle 0.9.0 → 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.
- package/LICENSE +1 -1
- package/README.md +61 -48
- package/dist/bin/oracle-cli.js +455 -402
- package/dist/bin/oracle-mcp.js +2 -2
- package/dist/bin/oracle.js +165 -279
- package/dist/scripts/agent-send.js +31 -31
- package/dist/scripts/check.js +6 -6
- package/dist/scripts/debug/extract-chatgpt-response.js +10 -10
- package/dist/scripts/docs-list.js +30 -30
- package/dist/scripts/git-policy.js +25 -23
- package/dist/scripts/run-cli.js +8 -8
- package/dist/scripts/runner.js +203 -195
- package/dist/scripts/test-browser.js +21 -18
- package/dist/scripts/test-remote-chrome.js +20 -20
- package/dist/src/bridge/connection.js +18 -18
- package/dist/src/bridge/userConfigFile.js +7 -7
- package/dist/src/browser/actions/assistantResponse.js +149 -101
- package/dist/src/browser/actions/attachmentDataTransfer.js +49 -47
- package/dist/src/browser/actions/attachments.js +246 -150
- package/dist/src/browser/actions/domEvents.js +2 -2
- package/dist/src/browser/actions/modelSelection.js +275 -117
- package/dist/src/browser/actions/navigation.js +161 -137
- package/dist/src/browser/actions/promptComposer.js +100 -64
- package/dist/src/browser/actions/remoteFileTransfer.js +10 -10
- package/dist/src/browser/actions/thinkingTime.js +207 -110
- package/dist/src/browser/chromeLifecycle.js +62 -60
- package/dist/src/browser/config.js +34 -15
- package/dist/src/browser/constants.js +17 -12
- package/dist/src/browser/cookies.js +19 -19
- package/dist/src/browser/detect.js +62 -62
- package/dist/src/browser/domDebug.js +1 -1
- package/dist/src/browser/index.js +390 -295
- package/dist/src/browser/modelStrategy.js +1 -1
- package/dist/src/browser/pageActions.js +5 -5
- package/dist/src/browser/policies.js +16 -13
- package/dist/src/browser/profileState.js +44 -39
- package/dist/src/browser/prompt.js +72 -42
- package/dist/src/browser/promptSummary.js +5 -5
- package/dist/src/browser/providerDomFlow.js +1 -1
- package/dist/src/browser/providers/chatgptDomProvider.js +9 -9
- package/dist/src/browser/providers/geminiDeepThinkDomProvider.js +51 -42
- package/dist/src/browser/providers/index.js +2 -2
- package/dist/src/browser/reattach.js +67 -34
- package/dist/src/browser/reattachHelpers.js +31 -26
- package/dist/src/browser/sessionRunner.js +37 -25
- package/dist/src/browser/utils.js +9 -9
- package/dist/src/browserMode.js +1 -1
- package/dist/src/cli/bridge/claudeConfig.js +16 -16
- package/dist/src/cli/bridge/client.js +28 -20
- package/dist/src/cli/bridge/codexConfig.js +16 -16
- package/dist/src/cli/bridge/doctor.js +47 -39
- package/dist/src/cli/bridge/host.js +58 -56
- package/dist/src/cli/browserConfig.js +62 -48
- package/dist/src/cli/browserDefaults.js +27 -26
- package/dist/src/cli/bundleWarnings.js +1 -1
- package/dist/src/cli/clipboard.js +11 -2
- package/dist/src/cli/detach.js +2 -2
- package/dist/src/cli/dryRun.js +29 -25
- package/dist/src/cli/duplicatePromptGuard.js +3 -3
- package/dist/src/cli/engine.js +9 -9
- package/dist/src/cli/errorUtils.js +1 -1
- package/dist/src/cli/fileSize.js +3 -3
- package/dist/src/cli/format.js +2 -2
- package/dist/src/cli/help.js +28 -28
- package/dist/src/cli/hiddenAliases.js +3 -3
- package/dist/src/cli/markdownBundle.js +7 -7
- package/dist/src/cli/markdownRenderer.js +15 -15
- package/dist/src/cli/notifier.js +77 -67
- package/dist/src/cli/options.js +127 -106
- package/dist/src/cli/oscUtils.js +1 -1
- package/dist/src/cli/promptRequirement.js +2 -2
- package/dist/src/cli/renderOutput.js +1 -1
- package/dist/src/cli/rootAlias.js +1 -1
- package/dist/src/cli/runOptions.js +32 -28
- package/dist/src/cli/sessionCommand.js +31 -21
- package/dist/src/cli/sessionDisplay.js +95 -81
- package/dist/src/cli/sessionLineage.js +6 -2
- package/dist/src/cli/sessionRunner.js +103 -93
- package/dist/src/cli/sessionTable.js +26 -23
- package/dist/src/cli/stdin.js +22 -0
- package/dist/src/cli/tagline.js +121 -124
- package/dist/src/cli/tui/index.js +139 -128
- package/dist/src/cli/writeOutputPath.js +5 -5
- package/dist/src/config.js +7 -7
- package/dist/src/gemini-web/browserSessionManager.js +19 -15
- package/dist/src/gemini-web/client.js +76 -70
- package/dist/src/gemini-web/executionMode.js +6 -8
- package/dist/src/gemini-web/executor.js +98 -93
- package/dist/src/gemini-web/index.js +1 -1
- package/dist/src/mcp/server.js +16 -12
- package/dist/src/mcp/tools/consult.js +51 -47
- package/dist/src/mcp/tools/sessionResources.js +12 -12
- package/dist/src/mcp/tools/sessions.js +26 -17
- package/dist/src/mcp/types.js +5 -5
- package/dist/src/mcp/utils.js +15 -7
- package/dist/src/oracle/background.js +15 -15
- package/dist/src/oracle/claude.js +53 -25
- package/dist/src/oracle/client.js +50 -41
- package/dist/src/oracle/config.js +96 -66
- package/dist/src/oracle/errors.js +38 -38
- package/dist/src/oracle/files.js +55 -46
- package/dist/src/oracle/finishLine.js +10 -8
- package/dist/src/oracle/format.js +3 -3
- package/dist/src/oracle/gemini.js +37 -33
- package/dist/src/oracle/logging.js +7 -7
- package/dist/src/oracle/markdown.js +28 -28
- package/dist/src/oracle/modelResolver.js +16 -16
- package/dist/src/oracle/multiModelRunner.js +12 -12
- package/dist/src/oracle/oscProgress.js +8 -8
- package/dist/src/oracle/promptAssembly.js +6 -3
- package/dist/src/oracle/request.js +16 -13
- package/dist/src/oracle/run.js +156 -134
- package/dist/src/oracle/runUtils.js +8 -5
- package/dist/src/oracle/tokenEstimate.js +6 -6
- package/dist/src/oracle/tokenStats.js +5 -5
- package/dist/src/oracle/tokenStringifier.js +5 -5
- package/dist/src/oracle.js +12 -12
- package/dist/src/oracleHome.js +3 -3
- package/dist/src/remote/client.js +25 -25
- package/dist/src/remote/health.js +20 -20
- package/dist/src/remote/remoteServiceConfig.js +9 -9
- package/dist/src/remote/server.js +129 -118
- package/dist/src/sessionManager.js +77 -75
- package/dist/src/sessionStore.js +3 -3
- package/dist/src/version.js +10 -10
- package/dist/vendor/oracle-notifier/README.md +2 -0
- package/package.json +66 -62
- package/vendor/oracle-notifier/README.md +2 -0
- package/dist/markdansi/types/index.js +0 -4
- package/dist/oracle/bin/oracle-cli.js +0 -472
- package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
- package/dist/oracle/src/browser/actions/attachments.js +0 -82
- package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
- package/dist/oracle/src/browser/actions/navigation.js +0 -75
- package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
- package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
- package/dist/oracle/src/browser/config.js +0 -33
- package/dist/oracle/src/browser/constants.js +0 -40
- package/dist/oracle/src/browser/cookies.js +0 -210
- package/dist/oracle/src/browser/domDebug.js +0 -36
- package/dist/oracle/src/browser/index.js +0 -331
- package/dist/oracle/src/browser/pageActions.js +0 -5
- package/dist/oracle/src/browser/prompt.js +0 -88
- package/dist/oracle/src/browser/promptSummary.js +0 -20
- package/dist/oracle/src/browser/sessionRunner.js +0 -80
- package/dist/oracle/src/browser/types.js +0 -1
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/profileSync.js +0 -141
package/dist/scripts/runner.js
CHANGED
|
@@ -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
|
|
7
|
-
import { cpSync, existsSync, renameSync, rmSync } from
|
|
8
|
-
import { constants as osConstants } from
|
|
9
|
-
import { basename, isAbsolute, join, normalize, resolve } from
|
|
10
|
-
import process from
|
|
11
|
-
import { analyzeGitExecution, evaluateGitPolicies, } from
|
|
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 ===
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 = [
|
|
41
|
-
const SINGLE_TEST_SCRIPTS = new Set([
|
|
42
|
-
const SINGLE_TEST_FLAGS = new Set([
|
|
43
|
-
const TEST_BINARIES = new Set([
|
|
44
|
-
const LINT_BINARIES = new Set([
|
|
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(
|
|
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(
|
|
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 ===
|
|
82
|
+
if (token === "--help" || token === "-h") {
|
|
83
83
|
printUsage();
|
|
84
84
|
process.exit(0);
|
|
85
85
|
}
|
|
86
|
-
if (token ===
|
|
87
|
-
console.error(
|
|
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,
|
|
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 ===
|
|
125
|
+
if (first === "pnpm") {
|
|
126
126
|
return shouldExtendViaPnpm(rest);
|
|
127
127
|
}
|
|
128
|
-
if (first ===
|
|
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 ===
|
|
144
|
+
if (subcommand === "run") {
|
|
145
145
|
const script = rest[1];
|
|
146
|
-
return typeof script ===
|
|
146
|
+
return typeof script === "string" && shouldExtendForScript(script);
|
|
147
147
|
}
|
|
148
|
-
if (subcommand ===
|
|
148
|
+
if (subcommand === "exec") {
|
|
149
149
|
const execTarget = rest[1];
|
|
150
|
-
if (execTarget &&
|
|
150
|
+
if (execTarget &&
|
|
151
|
+
(shouldExtendForScript(execTarget) || TEST_BINARIES.has(execTarget.toLowerCase()))) {
|
|
151
152
|
return true;
|
|
152
153
|
}
|
|
153
|
-
return rest
|
|
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 ===
|
|
168
|
+
if (subcommand === "run") {
|
|
166
169
|
const script = rest[1];
|
|
167
|
-
return typeof script ===
|
|
170
|
+
return typeof script === "string" && shouldExtendForScript(script);
|
|
168
171
|
}
|
|
169
|
-
if (subcommand ===
|
|
172
|
+
if (subcommand === "test") {
|
|
170
173
|
return true;
|
|
171
174
|
}
|
|
172
|
-
if (subcommand ===
|
|
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 ===
|
|
200
|
+
if (first === "pnpm") {
|
|
198
201
|
return shouldUseLintTimeoutViaPnpm(rest);
|
|
199
202
|
}
|
|
200
|
-
if (first ===
|
|
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 ===
|
|
216
|
+
if (subcommand === "run") {
|
|
214
217
|
const script = rest[1];
|
|
215
|
-
return typeof script ===
|
|
218
|
+
return typeof script === "string" && script.startsWith("lint");
|
|
216
219
|
}
|
|
217
|
-
if (subcommand ===
|
|
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 ===
|
|
237
|
+
if (subcommand === "run") {
|
|
235
238
|
const script = rest[1];
|
|
236
|
-
return typeof script ===
|
|
239
|
+
return typeof script === "string" && script.startsWith("lint");
|
|
237
240
|
}
|
|
238
|
-
if (subcommand ===
|
|
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 ===
|
|
259
|
+
if (first === "pnpm") {
|
|
257
260
|
return isSingleTestViaPnpm(rest);
|
|
258
261
|
}
|
|
259
|
-
if (first ===
|
|
262
|
+
if (first === "bun") {
|
|
260
263
|
return isSingleTestViaBun(rest);
|
|
261
264
|
}
|
|
262
|
-
if (first ===
|
|
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 ===
|
|
278
|
+
if (subcommand === "run") {
|
|
276
279
|
const script = rest[1];
|
|
277
|
-
return typeof script ===
|
|
280
|
+
return typeof script === "string" && SINGLE_TEST_SCRIPTS.has(script);
|
|
278
281
|
}
|
|
279
|
-
if (subcommand ===
|
|
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 ===
|
|
295
|
+
if (subcommand === "run") {
|
|
293
296
|
const script = rest[1];
|
|
294
|
-
return typeof script ===
|
|
297
|
+
return typeof script === "string" && SINGLE_TEST_SCRIPTS.has(script);
|
|
295
298
|
}
|
|
296
|
-
if (subcommand ===
|
|
299
|
+
if (subcommand === "test") {
|
|
297
300
|
return true;
|
|
298
301
|
}
|
|
299
|
-
if (subcommand ===
|
|
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(
|
|
314
|
+
if (normalized.includes("tests/integration/")) {
|
|
312
315
|
return true;
|
|
313
316
|
}
|
|
314
|
-
if (normalized.startsWith(
|
|
315
|
-
const value = normalized.split(
|
|
316
|
-
return value.includes(
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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: [
|
|
462
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
459
463
|
});
|
|
460
464
|
if (isRunnerTmuxSession()) {
|
|
461
|
-
const childPidInfo = typeof 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(
|
|
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(
|
|
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(
|
|
489
|
+
child.kill("SIGTERM");
|
|
486
490
|
killTimer = setTimeout(() => {
|
|
487
491
|
if (!child.killed) {
|
|
488
|
-
child.kill(
|
|
492
|
+
child.kill("SIGKILL");
|
|
489
493
|
}
|
|
490
494
|
}, 5_000);
|
|
491
495
|
}
|
|
492
496
|
}, context.timeoutMs);
|
|
493
|
-
child.once(
|
|
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(
|
|
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(
|
|
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:
|
|
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(
|
|
545
|
+
child.once("error", (error) => {
|
|
542
546
|
removeSignalHandlers();
|
|
543
547
|
reject(error);
|
|
544
548
|
});
|
|
545
|
-
child.once(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 = [
|
|
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 ===
|
|
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 ===
|
|
668
|
-
if (gitContext.subcommand ===
|
|
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 ?
|
|
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 ??
|
|
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 ??
|
|
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, {
|
|
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, {
|
|
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 !==
|
|
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) !==
|
|
827
|
+
if (basename(candidate) !== "tmux") {
|
|
823
828
|
return false;
|
|
824
829
|
}
|
|
825
|
-
console.error(
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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(
|
|
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 ===
|
|
899
|
-
commandToken.endsWith(
|
|
900
|
-
commandToken ===
|
|
901
|
-
commandToken.endsWith(
|
|
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 ===
|
|
914
|
+
if (!findArgs.some((token) => token === "-delete")) {
|
|
910
915
|
return null;
|
|
911
916
|
}
|
|
912
|
-
if (findArgs.some((token) => token ===
|
|
913
|
-
console.error(
|
|
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 ===
|
|
923
|
+
if (token === "-delete") {
|
|
919
924
|
continue;
|
|
920
925
|
}
|
|
921
926
|
printableArgs.push(token);
|
|
922
927
|
}
|
|
923
|
-
printableArgs.push(
|
|
928
|
+
printableArgs.push("-print0");
|
|
924
929
|
const proc = Bun.spawn(printableArgs, {
|
|
925
930
|
cwd: workspaceDir,
|
|
926
|
-
stdout:
|
|
927
|
-
stderr:
|
|
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(
|
|
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(
|
|
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(
|
|
984
|
-
if (token.includes(
|
|
988
|
+
if (!treatAsTarget && token.startsWith("-") && token.length > 1) {
|
|
989
|
+
if (token.includes("f")) {
|
|
985
990
|
force = true;
|
|
986
991
|
}
|
|
987
|
-
if (token.includes(
|
|
992
|
+
if (token.includes("i") || token === "--interactive") {
|
|
988
993
|
return null;
|
|
989
994
|
}
|
|
990
|
-
if (token ===
|
|
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([
|
|
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(
|
|
1024
|
-
if (token ===
|
|
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 ===
|
|
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 ===
|
|
1053
|
+
if (flag === "n") {
|
|
1049
1054
|
return null;
|
|
1050
1055
|
}
|
|
1051
|
-
if (flag ===
|
|
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:
|
|
1115
|
-
stderr:
|
|
1119
|
+
stdout: "ignore",
|
|
1120
|
+
stderr: "pipe",
|
|
1116
1121
|
});
|
|
1117
|
-
const [exitCode, stderrText] = await Promise.all([
|
|
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: [
|
|
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,
|
|
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 &&
|
|
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 = [
|
|
1216
|
+
const args = ["git", "rm", "--cached", "--quiet", ...plan.stagingOptions, "--", ...plan.paths];
|
|
1209
1217
|
const proc = Bun.spawn(args, {
|
|
1210
1218
|
cwd: workDir,
|
|
1211
|
-
stdout:
|
|
1212
|
-
stderr:
|
|
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 = [
|
|
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 ??
|
|
1234
|
-
searchDirs.add(join(homebrewPrefix,
|
|
1235
|
-
searchDirs.add(
|
|
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,
|
|
1246
|
-
stdout:
|
|
1247
|
-
stderr:
|
|
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 ===
|
|
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(
|
|
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
|
|
1334
|
+
return "compact";
|
|
1327
1335
|
}
|
|
1328
1336
|
const normalized = rawValue.trim().toLowerCase();
|
|
1329
1337
|
switch (normalized) {
|
|
1330
|
-
case
|
|
1331
|
-
return
|
|
1332
|
-
case
|
|
1333
|
-
return
|
|
1334
|
-
case
|
|
1335
|
-
return
|
|
1338
|
+
case "minimal":
|
|
1339
|
+
return "minimal";
|
|
1340
|
+
case "verbose":
|
|
1341
|
+
return "verbose";
|
|
1342
|
+
case "short":
|
|
1343
|
+
return "compact";
|
|
1336
1344
|
default:
|
|
1337
|
-
return
|
|
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 ===
|
|
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
|
|
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(
|
|
1359
|
+
parts.push("timeout");
|
|
1352
1360
|
}
|
|
1353
|
-
return `[runner] ${parts.join(
|
|
1361
|
+
return `[runner] ${parts.join(" · ")}`;
|
|
1354
1362
|
}
|
|
1355
|
-
case
|
|
1356
|
-
const elapsedPart = durationText ? `, elapsed ${durationText}` :
|
|
1357
|
-
const timeoutPart = timedOut ?
|
|
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 ?
|
|
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(
|
|
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 !==
|
|
1383
|
+
return value !== "0" && value.toLowerCase() !== "false";
|
|
1376
1384
|
}
|
|
1377
1385
|
return Boolean(process.env.TMUX);
|
|
1378
1386
|
}
|