@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.
Files changed (49) hide show
  1. package/dist/agent/loop.d.ts +14 -0
  2. package/dist/agent/loop.js +47 -1
  3. package/dist/agent/session.d.ts +11 -1
  4. package/dist/agent/session.js +14 -1
  5. package/dist/brand/index.d.ts +1 -0
  6. package/dist/brand/index.js +1 -0
  7. package/dist/brand/voice.d.ts +74 -0
  8. package/dist/brand/voice.js +73 -0
  9. package/dist/cli/commands/checkpoint.js +1 -1
  10. package/dist/cli/commands/hooks.d.ts +8 -0
  11. package/dist/cli/commands/hooks.js +83 -0
  12. package/dist/cli/commands/init.js +1 -1
  13. package/dist/cli/commands/pr.js +1 -1
  14. package/dist/cli/commands/rollback.js +1 -1
  15. package/dist/cli/commands/run.js +13 -3
  16. package/dist/cli/commands/skills.js +2 -2
  17. package/dist/cli/program.js +5 -2
  18. package/dist/cli/repl.d.ts +2 -1
  19. package/dist/cli/repl.js +54 -3
  20. package/dist/cli/session-factory.d.ts +2 -2
  21. package/dist/cli/session-factory.js +4 -2
  22. package/dist/config/schema.d.ts +81 -30
  23. package/dist/config/schema.js +22 -0
  24. package/dist/constants.d.ts +9 -0
  25. package/dist/constants.js +9 -0
  26. package/dist/errors/constructors.d.ts +16 -0
  27. package/dist/errors/constructors.js +57 -0
  28. package/dist/errors/types.d.ts +11 -0
  29. package/dist/errors/types.js +19 -0
  30. package/dist/hooks/config.d.ts +21 -0
  31. package/dist/hooks/config.js +253 -0
  32. package/dist/hooks/index.d.ts +6 -0
  33. package/dist/hooks/index.js +6 -0
  34. package/dist/hooks/runner.d.ts +76 -0
  35. package/dist/hooks/runner.js +114 -0
  36. package/dist/hooks/service.d.ts +38 -0
  37. package/dist/hooks/service.js +49 -0
  38. package/dist/hooks/slash.d.ts +48 -0
  39. package/dist/hooks/slash.js +58 -0
  40. package/dist/hooks/trust.d.ts +46 -0
  41. package/dist/hooks/trust.js +106 -0
  42. package/dist/hooks/types.d.ts +147 -0
  43. package/dist/hooks/types.js +61 -0
  44. package/dist/onboarding/steps.js +1 -1
  45. package/dist/tools/shell/exec.d.ts +53 -0
  46. package/dist/tools/shell/exec.js +128 -0
  47. package/dist/tools/shell/run-command.d.ts +4 -0
  48. package/dist/tools/shell/run-command.js +26 -116
  49. 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
- // Approve BEFORE anything runs; a denial executes nothing. A thrown
18
- // CRUXY_E_APPROVAL_REQUIRED (non-interactive) propagates — do not catch.
19
- const decision = await ctx.requestApproval({
20
- kind: "shell",
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: decision.feedback ?? "command denied by the user",
27
+ error: outcome.rejection ?? "command denied by the user",
27
28
  };
28
29
  }
29
- // Substrate is chosen SOLELY by ctx.sandbox: present → run in the box (C.16),
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
- * Run inside the sandbox and map the neutral ExecResult onto the IDENTICAL
40
- * ToolResult the host path produces same "exit code N" framing, same
41
- * truncation note, same timeout message so the tool is substrate-agnostic.
42
- * A container-start / image failure throws a coded CruxyError from
43
- * `sandbox.exec` and propagates; we deliberately do not catch it (fail loud).
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 runSandboxed(command, ctx) {
40
+ function mapExecResult(r, ctx) {
46
41
  const { timeoutMs, maxOutputBytes } = ctx.config.shell;
47
- return ctx
48
- .sandbox.exec(command, {
49
- cwd: ctx.cwd,
50
- timeoutMs,
51
- maxOutputBytes,
52
- capture: "head",
53
- })
54
- .then((result) => {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cruxy/cli",
3
- "version": "0.14.0",
3
+ "version": "0.16.0",
4
4
  "description": "an agentic coding CLI",
5
5
  "type": "module",
6
6
  "bin": {