@cruxy/cli 0.14.0 → 0.16.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/dist/agent/loop.d.ts +14 -0
- package/dist/agent/loop.js +47 -1
- package/dist/agent/session.d.ts +11 -1
- package/dist/agent/session.js +14 -1
- package/dist/brand/index.d.ts +1 -0
- package/dist/brand/index.js +1 -0
- package/dist/brand/voice.d.ts +74 -0
- package/dist/brand/voice.js +73 -0
- package/dist/cli/commands/checkpoint.js +1 -1
- package/dist/cli/commands/hooks.d.ts +8 -0
- package/dist/cli/commands/hooks.js +83 -0
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/pr.js +1 -1
- package/dist/cli/commands/rollback.js +1 -1
- package/dist/cli/commands/run.js +13 -3
- package/dist/cli/commands/skills.js +2 -2
- package/dist/cli/program.js +5 -2
- package/dist/cli/repl.d.ts +2 -1
- package/dist/cli/repl.js +54 -3
- package/dist/cli/session-factory.d.ts +2 -2
- package/dist/cli/session-factory.js +4 -2
- package/dist/config/schema.d.ts +81 -30
- package/dist/config/schema.js +22 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +9 -0
- package/dist/errors/constructors.d.ts +16 -0
- package/dist/errors/constructors.js +57 -0
- package/dist/errors/types.d.ts +11 -0
- package/dist/errors/types.js +19 -0
- package/dist/hooks/config.d.ts +21 -0
- package/dist/hooks/config.js +253 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/runner.d.ts +76 -0
- package/dist/hooks/runner.js +114 -0
- package/dist/hooks/service.d.ts +38 -0
- package/dist/hooks/service.js +49 -0
- package/dist/hooks/slash.d.ts +48 -0
- package/dist/hooks/slash.js +58 -0
- package/dist/hooks/trust.d.ts +46 -0
- package/dist/hooks/trust.js +106 -0
- package/dist/hooks/types.d.ts +147 -0
- package/dist/hooks/types.js +61 -0
- package/dist/onboarding/steps.js +1 -1
- package/dist/tools/shell/exec.d.ts +53 -0
- package/dist/tools/shell/exec.js +128 -0
- package/dist/tools/shell/run-command.d.ts +4 -0
- package/dist/tools/shell/run-command.js +26 -116
- package/package.json +1 -1
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { z } from "zod";
|
|
2
|
+
import { runGatedShell } from "./exec.js";
|
|
3
3
|
/**
|
|
4
4
|
* Run an arbitrary shell command in the project root. The highest-risk tool we
|
|
5
5
|
* ship: it is gated on `ctx.approve` (a denial runs nothing) and bounded by a
|
|
6
6
|
* timeout that kills the whole process tree plus a cap on captured output.
|
|
7
|
+
*
|
|
8
|
+
* Gate + execution live in the shared {@link runGatedShell} (also used by the
|
|
9
|
+
* C.19 hook runner — the single, un-bypassable shell path); this tool only maps
|
|
10
|
+
* the structured result back onto its `ToolResult` framing.
|
|
7
11
|
*/
|
|
8
12
|
export const runCommandTool = {
|
|
9
13
|
name: "run_command",
|
|
@@ -14,129 +18,35 @@ export const runCommandTool = {
|
|
|
14
18
|
.describe("The shell command to run (executed via the system shell)."),
|
|
15
19
|
}),
|
|
16
20
|
async execute(input, ctx) {
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
command: input.command,
|
|
22
|
-
});
|
|
23
|
-
if (!decision.allow) {
|
|
21
|
+
// A thrown CRUXY_E_APPROVAL_REQUIRED (non-interactive) / sandbox coded error
|
|
22
|
+
// propagates from runGatedShell — do not catch (fail loud).
|
|
23
|
+
const outcome = await runGatedShell(input.command, ctx);
|
|
24
|
+
if (!outcome.approved) {
|
|
24
25
|
return {
|
|
25
26
|
ok: false,
|
|
26
|
-
error:
|
|
27
|
+
error: outcome.rejection ?? "command denied by the user",
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
|
-
|
|
30
|
-
// never on the host; absent → host, unchanged. There is no fallback path — a
|
|
31
|
-
// sandbox that can't run throws a coded error (see runSandboxed) rather than
|
|
32
|
-
// silently reaching runBounded.
|
|
33
|
-
return ctx.sandbox
|
|
34
|
-
? runSandboxed(input.command, ctx)
|
|
35
|
-
: runBounded(input.command, ctx);
|
|
30
|
+
return mapExecResult(outcome.exec, ctx);
|
|
36
31
|
},
|
|
37
32
|
};
|
|
38
33
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* `
|
|
34
|
+
* Map a {@link ShellExecResult} onto the tool's `ToolResult` — the same "exit
|
|
35
|
+
* code N" framing, truncation note, and timeout / spawn-error messages as
|
|
36
|
+
* before the shared-path refactor. `exitCode ?? signal ?? "unknown"` reproduces
|
|
37
|
+
* both the sandbox (`exitCode ?? "unknown"`, signal always null) and host
|
|
38
|
+
* (`code ?? signal ?? "unknown"`) framings from the original tool.
|
|
44
39
|
*/
|
|
45
|
-
function
|
|
40
|
+
function mapExecResult(r, ctx) {
|
|
46
41
|
const { timeoutMs, maxOutputBytes } = ctx.config.shell;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (result.timedOut) {
|
|
56
|
-
return { ok: false, error: `timed out after ${timeoutMs}ms` };
|
|
57
|
-
}
|
|
58
|
-
const exit = result.exitCode ?? "unknown";
|
|
59
|
-
let output = `exit code ${exit}\n${result.output}`;
|
|
60
|
-
if (result.outputTruncated) {
|
|
61
|
-
output += `\n… [output truncated at ${maxOutputBytes} bytes]`;
|
|
62
|
-
}
|
|
63
|
-
return { ok: true, output };
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
/** Spawn the command, capture bounded output, and enforce the timeout. */
|
|
67
|
-
function runBounded(command, ctx) {
|
|
68
|
-
const { timeoutMs, maxOutputBytes } = ctx.config.shell;
|
|
69
|
-
return new Promise((resolve) => {
|
|
70
|
-
// `detached` makes the child its own process-group leader so the whole tree
|
|
71
|
-
// (the shell plus anything it spawns) can be killed on timeout.
|
|
72
|
-
const child = spawn(command, {
|
|
73
|
-
shell: true,
|
|
74
|
-
cwd: ctx.cwd,
|
|
75
|
-
detached: true,
|
|
76
|
-
});
|
|
77
|
-
const chunks = [];
|
|
78
|
-
let captured = 0;
|
|
79
|
-
let truncated = false;
|
|
80
|
-
const capture = (buf) => {
|
|
81
|
-
if (truncated)
|
|
82
|
-
return;
|
|
83
|
-
const room = maxOutputBytes - captured;
|
|
84
|
-
if (buf.length <= room) {
|
|
85
|
-
chunks.push(buf);
|
|
86
|
-
captured += buf.length;
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
if (room > 0) {
|
|
90
|
-
chunks.push(buf.subarray(0, room));
|
|
91
|
-
captured += room;
|
|
92
|
-
}
|
|
93
|
-
truncated = true;
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
child.stdout?.on("data", capture);
|
|
97
|
-
child.stderr?.on("data", capture);
|
|
98
|
-
// A single guard so the timeout-kill and the natural close can't both fire.
|
|
99
|
-
let settled = false;
|
|
100
|
-
const timer = setTimeout(() => {
|
|
101
|
-
if (settled)
|
|
102
|
-
return;
|
|
103
|
-
settled = true;
|
|
104
|
-
killTree(child.pid);
|
|
105
|
-
resolve({ ok: false, error: `timed out after ${timeoutMs}ms` });
|
|
106
|
-
}, timeoutMs);
|
|
107
|
-
child.on("error", (err) => {
|
|
108
|
-
if (settled)
|
|
109
|
-
return;
|
|
110
|
-
settled = true;
|
|
111
|
-
clearTimeout(timer);
|
|
112
|
-
resolve({ ok: false, error: err.message });
|
|
113
|
-
});
|
|
114
|
-
child.on("close", (code, signal) => {
|
|
115
|
-
if (settled)
|
|
116
|
-
return;
|
|
117
|
-
settled = true;
|
|
118
|
-
clearTimeout(timer);
|
|
119
|
-
const exit = code ?? signal ?? "unknown";
|
|
120
|
-
let output = `exit code ${exit}\n${Buffer.concat(chunks).toString("utf8")}`;
|
|
121
|
-
if (truncated) {
|
|
122
|
-
output += `\n… [output truncated at ${maxOutputBytes} bytes]`;
|
|
123
|
-
}
|
|
124
|
-
resolve({ ok: true, output });
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Kill the command's entire process group. POSIX-specific (negative pid targets
|
|
130
|
-
* the group); fine on our darwin/linux targets. Swallows errors — the process
|
|
131
|
-
* may already be gone.
|
|
132
|
-
*/
|
|
133
|
-
function killTree(pid) {
|
|
134
|
-
if (pid === undefined)
|
|
135
|
-
return;
|
|
136
|
-
try {
|
|
137
|
-
process.kill(-pid, "SIGKILL");
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
// Already exited, or no group — nothing to kill.
|
|
42
|
+
if (r.timedOut)
|
|
43
|
+
return { ok: false, error: `timed out after ${timeoutMs}ms` };
|
|
44
|
+
if (r.spawnError !== undefined)
|
|
45
|
+
return { ok: false, error: r.spawnError };
|
|
46
|
+
const exit = r.exitCode ?? r.signal ?? "unknown";
|
|
47
|
+
let output = `exit code ${exit}\n${r.output}`;
|
|
48
|
+
if (r.truncated) {
|
|
49
|
+
output += `\n… [output truncated at ${maxOutputBytes} bytes]`;
|
|
141
50
|
}
|
|
51
|
+
return { ok: true, output };
|
|
142
52
|
}
|