prae-cli 0.9.2 → 0.10.1

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.
@@ -0,0 +1,272 @@
1
+ import { t as parsePack } from "./src-CZYvvEn5.js";
2
+ import { d as generateKeypair, u as fingerprint } from "./src-CuiWC80o.js";
3
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
4
+ import { delimiter, dirname, join, resolve } from "node:path";
5
+ import { homedir } from "node:os";
6
+
7
+ //#region src/claude-code/init.ts
8
+ /** What marks a hook command as PRAE's own: how `--remove` finds it. */
9
+ const MARKER = " hook claude-code ";
10
+ /** Seconds Claude Code waits for the hook before it gives up on it. */
11
+ const HOOK_TIMEOUT = 10;
12
+ const isPrae = (h) => typeof h.command === "string" && `${h.command} `.includes(MARKER);
13
+ /**
14
+ * The node the hook runs with, by a path that survives a Node upgrade: the
15
+ * first `node` on PATH that is this very binary (`/opt/homebrew/bin/node`
16
+ * rather than its versioned Cellar target). A hook whose executable has
17
+ * gone cannot start, and Claude Code lets the call through when a hook
18
+ * cannot start.
19
+ */
20
+ function stableNode(execPath, path = "") {
21
+ let real = execPath;
22
+ try {
23
+ real = realpathSync(execPath);
24
+ } catch {}
25
+ for (const dir of path.split(delimiter)) {
26
+ if (dir === "") continue;
27
+ try {
28
+ if (realpathSync(join(dir, "node")) === real) return join(dir, "node");
29
+ } catch {}
30
+ }
31
+ return execPath;
32
+ }
33
+ /** POSIX single-quoting: the command runs through a shell. */
34
+ const quote = (s) => `'${s.replaceAll("'", `'\\''`)}'`;
35
+ /** `obj` with `key` replaced in place (or appended), or removed when `value` is undefined. */
36
+ function put(obj, key, value) {
37
+ const out = {};
38
+ for (const [k, v] of Object.entries(obj)) if (k !== key) out[k] = v;
39
+ else if (value !== void 0) out[k] = value;
40
+ if (value !== void 0 && !(key in obj)) out[key] = value;
41
+ return out;
42
+ }
43
+ /** Settings without PRAE's handler; containers it emptied are dropped. */
44
+ function withoutPrae(settings) {
45
+ const hooks = settings.hooks;
46
+ const groups = hooks?.PreToolUse;
47
+ if (hooks === void 0 || !Array.isArray(groups)) return settings;
48
+ const kept = groups.map((g) => Array.isArray(g.hooks) && g.hooks.some(isPrae) ? {
49
+ ...g,
50
+ hooks: g.hooks.filter((h) => !isPrae(h))
51
+ } : g).filter((g) => !Array.isArray(g.hooks) || g.hooks.length > 0);
52
+ if (kept.length === groups.length && kept.every((g, i) => g === groups[i])) return settings;
53
+ const next = put(hooks, "PreToolUse", kept.length === 0 ? void 0 : kept);
54
+ return put(settings, "hooks", Object.keys(next).length === 0 ? void 0 : next);
55
+ }
56
+ /**
57
+ * Settings with PRAE's handler as the one PRAE entry, replacing any old one.
58
+ *
59
+ * Enforce adds `onFailure: "block"`: from Claude Code 2.1.295 a hook that
60
+ * cannot start or times out then blocks the call instead of letting it
61
+ * through. Earlier versions ignore the field (2.1.233 checked: the hook
62
+ * still runs, and a hook that cannot start still lets the call through).
63
+ */
64
+ function withPrae(settings, command, enforce = false) {
65
+ const base = withoutPrae(settings);
66
+ const hooks = base.hooks ?? {};
67
+ const groups = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
68
+ const entry = {
69
+ matcher: "*",
70
+ hooks: [{
71
+ type: "command",
72
+ command,
73
+ timeout: HOOK_TIMEOUT,
74
+ ...enforce ? { onFailure: "block" } : {}
75
+ }]
76
+ };
77
+ return put(base, "hooks", put(hooks, "PreToolUse", [...groups, entry]));
78
+ }
79
+ /** A line diff (LCS), `+`/`-` on changed lines, two lines of context. */
80
+ function lineDiff(before, after) {
81
+ const a = before.split("\n");
82
+ const b = after.split("\n");
83
+ const w = b.length + 1;
84
+ const lcs = new Uint32Array((a.length + 1) * w);
85
+ for (let i$1 = a.length - 1; i$1 >= 0; i$1 -= 1) for (let j$1 = b.length - 1; j$1 >= 0; j$1 -= 1) lcs[i$1 * w + j$1] = a[i$1] === b[j$1] ? (lcs[(i$1 + 1) * w + j$1 + 1] ?? 0) + 1 : Math.max(lcs[(i$1 + 1) * w + j$1] ?? 0, lcs[i$1 * w + j$1 + 1] ?? 0);
86
+ const ops = [];
87
+ let i = 0;
88
+ let j = 0;
89
+ while (i < a.length || j < b.length) if (i < a.length && j < b.length && a[i] === b[j]) {
90
+ ops.push(` ${a[i]}`);
91
+ i += 1;
92
+ j += 1;
93
+ } else if (j < b.length && (i === a.length || (lcs[i * w + j + 1] ?? 0) >= (lcs[(i + 1) * w + j] ?? 0))) {
94
+ ops.push(`+ ${b[j]}`);
95
+ j += 1;
96
+ } else {
97
+ ops.push(`- ${a[i]}`);
98
+ i += 1;
99
+ }
100
+ const changed = (k) => ops.slice(Math.max(0, k - 2), k + 3).some((op) => !op.startsWith(" "));
101
+ const out = [];
102
+ for (const [k, op] of ops.entries()) if (changed(k)) out.push(op);
103
+ else if (out.at(-1) !== " …") out.push(" …");
104
+ return out.join("\n");
105
+ }
106
+ function flag(argv, name) {
107
+ const at = argv.indexOf(`--${name}`);
108
+ if (at === -1) return void 0;
109
+ const value = argv[at + 1];
110
+ if (value === void 0 || value.startsWith("--")) throw new Error(`--${name} needs a value`);
111
+ return value;
112
+ }
113
+ /** The settings file to edit: always named, never guessed. */
114
+ function settingsTarget(argv, cwd) {
115
+ const explicit = flag(argv, "settings");
116
+ const scope = flag(argv, "scope");
117
+ if (explicit !== void 0 && scope !== void 0) throw new Error("give --settings <path> or --scope, not both");
118
+ if (explicit !== void 0) return resolve(cwd, explicit);
119
+ if (scope === "user") return join(homedir(), ".claude", "settings.json");
120
+ if (scope === "project") return join(cwd, ".claude", "settings.json");
121
+ throw new Error("init claude-code needs a target: --settings <path>, --scope user (~/.claude/settings.json) or --scope project (./.claude/settings.json)");
122
+ }
123
+ function readSettings(path) {
124
+ if (!existsSync(path)) return {
125
+ text: "",
126
+ json: {},
127
+ indent: " "
128
+ };
129
+ const text = readFileSync(path, "utf8");
130
+ let json;
131
+ try {
132
+ json = text.trim() === "" ? {} : JSON.parse(text);
133
+ } catch (e) {
134
+ throw new Error(`${path} is not JSON (${e.message}); not touching it`);
135
+ }
136
+ if (json === null || typeof json !== "object" || Array.isArray(json)) throw new Error(`${path} is not a JSON object; not touching it`);
137
+ const indent = text.match(/^[ \t]+(?=")/m)?.[0] ?? " ";
138
+ return {
139
+ text,
140
+ json,
141
+ indent
142
+ };
143
+ }
144
+ /** Work out the change; touches nothing. */
145
+ function planInit(argv, context) {
146
+ const settings = settingsTarget(argv, context.cwd);
147
+ const read = readSettings(settings);
148
+ const render = (json) => Object.keys(json).length === 0 && read.text === "" ? "" : `${JSON.stringify(json, null, read.indent)}\n`;
149
+ if (argv.includes("--remove")) {
150
+ const next = withoutPrae(read.json);
151
+ return {
152
+ settings,
153
+ before: read.text,
154
+ after: next === read.json ? read.text : render(next)
155
+ };
156
+ }
157
+ if (argv.includes("--shadow") && argv.includes("--enforce")) throw new Error("--shadow or --enforce, not both");
158
+ const dir = resolve(context.cwd, flag(argv, "dir") ?? join(homedir(), ".prae", "claude-code"));
159
+ const given = flag(argv, "pack");
160
+ let pack;
161
+ let copyPack;
162
+ if (given !== void 0) {
163
+ pack = resolve(context.cwd, given);
164
+ let text;
165
+ try {
166
+ text = readFileSync(pack, "utf8");
167
+ } catch {
168
+ throw new Error(`cannot read ${pack}`);
169
+ }
170
+ const parsed = parsePack(text);
171
+ if (!parsed.ok) throw new Error(`${pack} is not a valid pack: ${parsed.errors.map((e) => `${e.path || "<root>"}: ${e.message}`).join("; ")}`);
172
+ } else {
173
+ if (context.baseline === void 0) throw new Error("no --pack given and no bundled baseline found");
174
+ pack = join(dir, "prae-baseline.prae.json");
175
+ copyPack = context.baseline;
176
+ }
177
+ const ledger = resolve(context.cwd, flag(argv, "ledger") ?? join(dir, "ledger.jsonl"));
178
+ const key = join(dir, "prae-signing.pem");
179
+ const mode = argv.includes("--enforce") ? "--enforce" : "--shadow";
180
+ const command = [
181
+ quote(context.node),
182
+ quote(context.bin),
183
+ "hook",
184
+ "claude-code",
185
+ mode,
186
+ "--pack",
187
+ quote(pack),
188
+ "--ledger",
189
+ quote(ledger),
190
+ "--key",
191
+ quote(key),
192
+ "--settings",
193
+ quote(settings)
194
+ ].join(" ");
195
+ return {
196
+ settings,
197
+ before: read.text,
198
+ after: render(withPrae(read.json, command, mode === "--enforce")),
199
+ command,
200
+ dir,
201
+ pack,
202
+ ledger,
203
+ key,
204
+ ...copyPack === void 0 ? {} : { copyPack }
205
+ };
206
+ }
207
+ /** Write what the plan says: the state directory, key and pack, then settings. */
208
+ function applyInit(plan) {
209
+ let print;
210
+ if (plan.dir !== void 0 && plan.key !== void 0) {
211
+ mkdirSync(plan.dir, {
212
+ recursive: true,
213
+ mode: 448
214
+ });
215
+ const pub = plan.key.replace(/\.pem$/, ".pub.pem");
216
+ if (!existsSync(plan.key)) {
217
+ const keys = generateKeypair();
218
+ writeFileSync(plan.key, keys.privateKey, { mode: 384 });
219
+ writeFileSync(pub, keys.publicKey);
220
+ }
221
+ if (existsSync(pub)) print = fingerprint(readFileSync(pub, "utf8"));
222
+ if (plan.copyPack !== void 0 && plan.pack !== void 0 && !existsSync(plan.pack)) copyFileSync(plan.copyPack, plan.pack);
223
+ }
224
+ if (plan.after !== plan.before) {
225
+ mkdirSync(dirname(plan.settings), { recursive: true });
226
+ writeFileSync(plan.settings, plan.after);
227
+ }
228
+ return print === void 0 ? {} : { fingerprint: print };
229
+ }
230
+ /** Whether a script path is inside npm's temporary npx cache (`…/_npx/<hash>/…`). */
231
+ function fromNpxCache(bin) {
232
+ return /[\\/]_npx[\\/]/.test(bin);
233
+ }
234
+ /** `prae init claude-code …`: print the diff; with --apply, write it. */
235
+ function initClaudeCode(argv, context) {
236
+ const plan = planInit(argv, context);
237
+ const removing = argv.includes("--remove");
238
+ if (!removing && fromNpxCache(context.bin)) console.error(`prae: warning: this prae runs from npm's temporary npx cache (${context.bin}).\n The hook entry would point there, and npm may clear that cache: the hook would
239
+ stop running, and Claude Code lets a call through when its hook cannot start.
240
+ Install it to stay: npm install -g prae-cli, then run prae init claude-code again.`);
241
+ console.log(`settings ${plan.settings}`);
242
+ if (!removing) {
243
+ console.log(`mode ${argv.includes("--enforce") ? "enforce · deny and ask are returned to Claude Code; a gate error refuses the call" : "shadow · every verdict sealed, every call proceeds"}`);
244
+ console.log(`pack ${plan.pack}${plan.copyPack === void 0 ? "" : " (the built-in baseline, copied here)"}`);
245
+ console.log(`ledger ${plan.ledger}`);
246
+ console.log(`signing key ${plan.key} (generated if absent; 0600)`);
247
+ }
248
+ if (plan.after === plan.before) {
249
+ console.log(removing ? "\nNo PRAE hook entry in this file; nothing to remove." : "\nThe settings already hold exactly this entry.");
250
+ if (!removing && argv.includes("--apply")) applyInit(plan);
251
+ return;
252
+ }
253
+ console.log(`\n--- ${plan.settings}\n+++ ${plan.settings} (after)`);
254
+ console.log(lineDiff(plan.before, plan.after));
255
+ if (!argv.includes("--apply")) {
256
+ console.log("\nNothing was written. Re-run with --apply to write it.");
257
+ return;
258
+ }
259
+ const { fingerprint: print } = applyInit(plan);
260
+ console.log("\nwritten.");
261
+ if (removing) {
262
+ console.log("Only PRAE’s hook entry was removed. The ledger and key stay where they");
263
+ console.log("are: they are the evidence of what ran.");
264
+ return;
265
+ }
266
+ if (print !== void 0) console.log(`ledger key fingerprint ${print}`);
267
+ console.log("New Claude Code sessions run every tool call through PRAE.");
268
+ console.log(`Read the record: prae report ${quote(plan.ledger ?? "")} --out report.html`);
269
+ }
270
+
271
+ //#endregion
272
+ export { HOOK_TIMEOUT, MARKER, applyInit, fromNpxCache, initClaudeCode, lineDiff, planInit, quote, settingsTarget, stableNode, withPrae, withoutPrae };
@@ -1,4 +1,5 @@
1
- import { c as renderNav, l as renderShell, s as esc, t as navFor } from "./views-D4UEfbzt.js";
1
+ import "./src-CZYvvEn5.js";
2
+ import { c as renderNav, l as renderShell, s as esc, t as navFor } from "./views-C7DD-Oif.js";
2
3
 
3
4
  //#region src/console/instructions.ts
4
5
  const STYLE = `
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "./evaluate-BqLxhYiP.js";
1
+ import { i as evaluate } from "./src-CZYvvEn5.js";
2
2
  import { a as scan, n as MAX_SCAN_BYTES } from "./src-B25SHWcz.js";
3
3
  import { readFile, readdir, stat } from "node:fs/promises";
4
4
  import { resolve, sep } from "node:path";