codealmanac 0.1.6 → 0.1.7

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 (43) hide show
  1. package/dist/chunk-2JJTTN7P.js +539 -0
  2. package/dist/chunk-2JJTTN7P.js.map +1 -0
  3. package/dist/chunk-3C5SY5SE.js +1239 -0
  4. package/dist/chunk-3C5SY5SE.js.map +1 -0
  5. package/dist/chunk-4CODZRHH.js +19 -0
  6. package/dist/chunk-4CODZRHH.js.map +1 -0
  7. package/dist/chunk-7JUX4ADQ.js +38 -0
  8. package/dist/chunk-7JUX4ADQ.js.map +1 -0
  9. package/dist/chunk-A6PUCAVJ.js +145 -0
  10. package/dist/chunk-A6PUCAVJ.js.map +1 -0
  11. package/dist/chunk-AXFPUHBN.js +227 -0
  12. package/dist/chunk-AXFPUHBN.js.map +1 -0
  13. package/dist/chunk-FM3VRDK7.js +20 -0
  14. package/dist/chunk-FM3VRDK7.js.map +1 -0
  15. package/dist/chunk-H6WU6PYH.js +441 -0
  16. package/dist/chunk-H6WU6PYH.js.map +1 -0
  17. package/dist/chunk-P3LDTCLB.js +34 -0
  18. package/dist/chunk-P3LDTCLB.js.map +1 -0
  19. package/dist/chunk-QHQ6YH7U.js +81 -0
  20. package/dist/chunk-QHQ6YH7U.js.map +1 -0
  21. package/dist/chunk-Z4MWLVS2.js +355 -0
  22. package/dist/chunk-Z4MWLVS2.js.map +1 -0
  23. package/dist/chunk-Z6MBJ3D2.js +203 -0
  24. package/dist/chunk-Z6MBJ3D2.js.map +1 -0
  25. package/dist/cli-AIH5QQ5H.js +393 -0
  26. package/dist/cli-AIH5QQ5H.js.map +1 -0
  27. package/dist/codealmanac.js +32 -5
  28. package/dist/codealmanac.js.map +1 -1
  29. package/dist/doctor-6FN5JO5F.js +15 -0
  30. package/dist/doctor-6FN5JO5F.js.map +1 -0
  31. package/dist/hook-CRJMWSSO.js +12 -0
  32. package/dist/hook-CRJMWSSO.js.map +1 -0
  33. package/dist/register-commands-PZMQNGCH.js +2644 -0
  34. package/dist/register-commands-PZMQNGCH.js.map +1 -0
  35. package/dist/uninstall-NBEZNNKM.js +12 -0
  36. package/dist/uninstall-NBEZNNKM.js.map +1 -0
  37. package/dist/update-IL243I4E.js +10 -0
  38. package/dist/update-IL243I4E.js.map +1 -0
  39. package/dist/wiki-EHZ7LG7R.js +238 -0
  40. package/dist/wiki-EHZ7LG7R.js.map +1 -0
  41. package/package.json +1 -1
  42. package/dist/cli-GTEC5PC7.js +0 -6237
  43. package/dist/cli-GTEC5PC7.js.map +0 -1
@@ -0,0 +1,355 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/hook.ts
4
+ import { existsSync as existsSync2 } from "fs";
5
+ import { mkdir as mkdir2, readFile as readFile2, rename, writeFile } from "fs/promises";
6
+ import path2 from "path";
7
+
8
+ // src/commands/hook/script.ts
9
+ import { existsSync } from "fs";
10
+ import { copyFile, mkdir, readFile } from "fs/promises";
11
+ import { homedir } from "os";
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+ function resolveSettingsPath(options) {
15
+ if (options.settingsPath !== void 0) return options.settingsPath;
16
+ return path.join(homedir(), ".claude", "settings.json");
17
+ }
18
+ async function copyToStableHooksDir(bundledPath, options) {
19
+ const stableHooksDir = options.stableHooksDir ?? path.join(homedir(), ".claude", "hooks");
20
+ const dest = path.join(stableHooksDir, "almanac-capture.sh");
21
+ try {
22
+ await mkdir(stableHooksDir, { recursive: true });
23
+ const srcBytes = await readFile(bundledPath);
24
+ let needsCopy = true;
25
+ if (existsSync(dest)) {
26
+ try {
27
+ const destBytes = await readFile(dest);
28
+ if (srcBytes.equals(destBytes)) needsCopy = false;
29
+ } catch {
30
+ }
31
+ }
32
+ if (needsCopy) {
33
+ await copyFile(bundledPath, dest);
34
+ }
35
+ return { ok: true, path: dest };
36
+ } catch (err) {
37
+ const msg = err instanceof Error ? err.message : String(err);
38
+ return {
39
+ ok: false,
40
+ error: `could not copy hook script to ${dest}: ${msg}`
41
+ };
42
+ }
43
+ }
44
+ function resolveHookScriptPath(options) {
45
+ if (options.hookScriptPath !== void 0) {
46
+ return { ok: true, path: options.hookScriptPath };
47
+ }
48
+ const here = path.dirname(fileURLToPath(import.meta.url));
49
+ const candidates = [
50
+ // Bundled: `.../codealmanac/dist/codealmanac.js` → `../hooks/…`
51
+ path.resolve(here, "..", "hooks", "almanac-capture.sh"),
52
+ // Source after ts-node-style module layout or nested dist helpers.
53
+ path.resolve(here, "..", "..", "hooks", "almanac-capture.sh"),
54
+ // Source: `.../codealmanac/src/commands/hook/script.ts` → `../../../hooks/…`
55
+ path.resolve(here, "..", "..", "..", "hooks", "almanac-capture.sh"),
56
+ // Defensive nested fallback.
57
+ path.resolve(here, "..", "..", "..", "..", "hooks", "almanac-capture.sh")
58
+ ];
59
+ for (const candidate of candidates) {
60
+ if (existsSync(candidate)) {
61
+ return { ok: true, path: candidate };
62
+ }
63
+ }
64
+ return {
65
+ ok: false,
66
+ error: `could not locate hooks/almanac-capture.sh. Tried:
67
+ ` + candidates.map((c) => ` - ${c}`).join("\n")
68
+ };
69
+ }
70
+
71
+ // src/commands/hook.ts
72
+ var HOOK_TIMEOUT_SECONDS = 10;
73
+ function isOurCommandPath(command) {
74
+ return command.endsWith("almanac-capture.sh");
75
+ }
76
+ function classifyEntry(raw) {
77
+ if (raw === null || typeof raw !== "object") {
78
+ return { kind: "unknown", entry: raw };
79
+ }
80
+ const obj = raw;
81
+ if (Array.isArray(obj.hooks)) {
82
+ const matcher = typeof obj.matcher === "string" ? obj.matcher : "";
83
+ const hooks = [];
84
+ for (const h of obj.hooks) {
85
+ if (h !== null && typeof h === "object") {
86
+ const ho = h;
87
+ if (ho.type === "command" && typeof ho.command === "string") {
88
+ const cmd = {
89
+ type: "command",
90
+ command: ho.command
91
+ };
92
+ if (typeof ho.timeout === "number") cmd.timeout = ho.timeout;
93
+ hooks.push(cmd);
94
+ }
95
+ }
96
+ }
97
+ return { kind: "wrapped", entry: { matcher, hooks } };
98
+ }
99
+ if (obj.type === "command" && typeof obj.command === "string") {
100
+ const cmd = {
101
+ type: "command",
102
+ command: obj.command
103
+ };
104
+ if (typeof obj.timeout === "number") cmd.timeout = obj.timeout;
105
+ return { kind: "legacy", entry: cmd };
106
+ }
107
+ return { kind: "unknown", entry: raw };
108
+ }
109
+ function isOurWrapped(entry) {
110
+ return entry.hooks.some((h) => isOurCommandPath(h.command));
111
+ }
112
+ async function runHookInstall(options = {}) {
113
+ const bundled = resolveHookScriptPath(options);
114
+ if (!bundled.ok) {
115
+ return { stdout: "", stderr: `almanac: ${bundled.error}
116
+ `, exitCode: 1 };
117
+ }
118
+ const script = options.hookScriptPath !== void 0 ? bundled : await copyToStableHooksDir(bundled.path, options);
119
+ if (!script.ok) {
120
+ return { stdout: "", stderr: `almanac: ${script.error}
121
+ `, exitCode: 1 };
122
+ }
123
+ const settingsPath = resolveSettingsPath(options);
124
+ const settings = await readSettings(settingsPath);
125
+ const existing = (settings.hooks?.SessionEnd ?? []).slice();
126
+ const preserved = [];
127
+ let oursAlready = null;
128
+ const staleCount = { n: 0 };
129
+ for (const raw of existing) {
130
+ const c = classifyEntry(raw);
131
+ if (c.kind === "wrapped") {
132
+ if (!isOurWrapped(c.entry)) {
133
+ preserved.push(raw);
134
+ continue;
135
+ }
136
+ const exactMatch = c.entry.hooks.some(
137
+ (h) => h.command === script.path
138
+ );
139
+ if (exactMatch && oursAlready === null) {
140
+ oursAlready = c.entry;
141
+ } else {
142
+ staleCount.n += 1;
143
+ }
144
+ } else if (c.kind === "legacy") {
145
+ if (isOurCommandPath(c.entry.command)) {
146
+ staleCount.n += 1;
147
+ } else {
148
+ preserved.push(raw);
149
+ }
150
+ } else {
151
+ preserved.push(raw);
152
+ }
153
+ }
154
+ const foreignLegacy = preserved.filter((raw) => {
155
+ const c = classifyEntry(raw);
156
+ return c.kind === "legacy";
157
+ });
158
+ if (foreignLegacy.length > 0) {
159
+ const lines = foreignLegacy.map((raw) => {
160
+ const c = classifyEntry(raw);
161
+ if (c.kind === "legacy") return ` - ${c.entry.command}`;
162
+ return " - <unrecognized>";
163
+ }).join("\n");
164
+ return {
165
+ stdout: "",
166
+ stderr: `almanac: SessionEnd has a foreign legacy entry:
167
+ ${lines}
168
+ Remove or rewrap it manually in ${settingsPath} before installing.
169
+ `,
170
+ exitCode: 1
171
+ };
172
+ }
173
+ if (oursAlready !== null && staleCount.n === 0) {
174
+ return {
175
+ stdout: `almanac: SessionEnd hook already installed at ${script.path}
176
+ `,
177
+ stderr: "",
178
+ exitCode: 0
179
+ };
180
+ }
181
+ const fresh = {
182
+ matcher: "",
183
+ hooks: [
184
+ {
185
+ type: "command",
186
+ command: script.path,
187
+ timeout: HOOK_TIMEOUT_SECONDS
188
+ }
189
+ ]
190
+ };
191
+ const newEntries = [...preserved, fresh];
192
+ settings.hooks = { ...settings.hooks ?? {}, SessionEnd: newEntries };
193
+ await writeSettings(settingsPath, settings);
194
+ return {
195
+ stdout: `almanac: SessionEnd hook installed
196
+ script: ${script.path}
197
+ settings: ${settingsPath}
198
+ `,
199
+ stderr: "",
200
+ exitCode: 0
201
+ };
202
+ }
203
+ async function runHookUninstall(options = {}) {
204
+ const settingsPath = resolveSettingsPath(options);
205
+ if (!existsSync2(settingsPath)) {
206
+ return {
207
+ stdout: `almanac: SessionEnd hook not installed (no settings file)
208
+ `,
209
+ stderr: "",
210
+ exitCode: 0
211
+ };
212
+ }
213
+ const settings = await readSettings(settingsPath);
214
+ const existing = (settings.hooks?.SessionEnd ?? []).slice();
215
+ const kept = [];
216
+ let removed = 0;
217
+ for (const raw of existing) {
218
+ const c = classifyEntry(raw);
219
+ if (c.kind === "wrapped") {
220
+ const innerKept = c.entry.hooks.filter(
221
+ (h) => !isOurCommandPath(h.command)
222
+ );
223
+ const innerRemoved = c.entry.hooks.length - innerKept.length;
224
+ removed += innerRemoved;
225
+ if (innerKept.length === 0) {
226
+ if (innerRemoved === 0) kept.push(raw);
227
+ } else if (innerRemoved === 0) {
228
+ kept.push(raw);
229
+ } else {
230
+ kept.push({ matcher: c.entry.matcher, hooks: innerKept });
231
+ }
232
+ } else if (c.kind === "legacy") {
233
+ if (isOurCommandPath(c.entry.command)) {
234
+ removed += 1;
235
+ } else {
236
+ kept.push(raw);
237
+ }
238
+ } else {
239
+ kept.push(raw);
240
+ }
241
+ }
242
+ if (removed === 0) {
243
+ return {
244
+ stdout: `almanac: SessionEnd hook not installed
245
+ `,
246
+ stderr: "",
247
+ exitCode: 0
248
+ };
249
+ }
250
+ if (settings.hooks !== void 0) {
251
+ if (kept.length === 0) {
252
+ const { SessionEnd: _dropped, ...rest } = settings.hooks;
253
+ void _dropped;
254
+ settings.hooks = rest;
255
+ } else {
256
+ settings.hooks = { ...settings.hooks, SessionEnd: kept };
257
+ }
258
+ if (Object.keys(settings.hooks).length === 0) {
259
+ delete settings.hooks;
260
+ }
261
+ }
262
+ await writeSettings(settingsPath, settings);
263
+ return {
264
+ stdout: `almanac: SessionEnd hook removed
265
+ `,
266
+ stderr: "",
267
+ exitCode: 0
268
+ };
269
+ }
270
+ async function runHookStatus(options = {}) {
271
+ const script = resolveHookScriptPath(options);
272
+ const settingsPath = resolveSettingsPath(options);
273
+ if (!existsSync2(settingsPath)) {
274
+ return {
275
+ stdout: `SessionEnd hook: not installed
276
+ settings: ${settingsPath} (does not exist)
277
+ ` + (script.ok ? `script would be: ${script.path}
278
+ ` : ""),
279
+ stderr: "",
280
+ exitCode: 0
281
+ };
282
+ }
283
+ const settings = await readSettings(settingsPath);
284
+ const existing = settings.hooks?.SessionEnd ?? [];
285
+ let ourCommand = null;
286
+ const foreignSummary = [];
287
+ for (const raw of existing) {
288
+ const c = classifyEntry(raw);
289
+ if (c.kind === "wrapped") {
290
+ for (const h of c.entry.hooks) {
291
+ if (isOurCommandPath(h.command)) {
292
+ ourCommand ??= h.command;
293
+ } else {
294
+ foreignSummary.push(h.command);
295
+ }
296
+ }
297
+ } else if (c.kind === "legacy") {
298
+ if (isOurCommandPath(c.entry.command)) {
299
+ ourCommand ??= c.entry.command;
300
+ } else {
301
+ foreignSummary.push(c.entry.command);
302
+ }
303
+ }
304
+ }
305
+ if (ourCommand === null) {
306
+ const foreignLines = foreignSummary.map((c) => ` - ${c}`).join("\n");
307
+ return {
308
+ stdout: `SessionEnd hook: not installed
309
+ settings: ${settingsPath}
310
+ ` + (foreignSummary.length > 0 ? `(${foreignSummary.length} foreign entr${foreignSummary.length === 1 ? "y" : "ies"} present:
311
+ ${foreignLines})
312
+ ` : "") + (script.ok ? `script would be: ${script.path}
313
+ ` : ""),
314
+ stderr: "",
315
+ exitCode: 0
316
+ };
317
+ }
318
+ return {
319
+ stdout: `SessionEnd hook: installed
320
+ script: ${ourCommand}
321
+ settings: ${settingsPath}
322
+ `,
323
+ stderr: "",
324
+ exitCode: 0
325
+ };
326
+ }
327
+ async function readSettings(settingsPath) {
328
+ if (!existsSync2(settingsPath)) return {};
329
+ try {
330
+ const raw = await readFile2(settingsPath, "utf8");
331
+ if (raw.trim().length === 0) return {};
332
+ const parsed = JSON.parse(raw);
333
+ if (parsed === null || typeof parsed !== "object") return {};
334
+ return parsed;
335
+ } catch (err) {
336
+ const msg = err instanceof Error ? err.message : String(err);
337
+ throw new Error(`failed to read ${settingsPath}: ${msg}`);
338
+ }
339
+ }
340
+ async function writeSettings(settingsPath, settings) {
341
+ const dir = path2.dirname(settingsPath);
342
+ await mkdir2(dir, { recursive: true });
343
+ const tmp = `${settingsPath}.almanac-tmp-${process.pid}`;
344
+ const body = `${JSON.stringify(settings, null, 2)}
345
+ `;
346
+ await writeFile(tmp, body, "utf8");
347
+ await rename(tmp, settingsPath);
348
+ }
349
+
350
+ export {
351
+ runHookInstall,
352
+ runHookUninstall,
353
+ runHookStatus
354
+ };
355
+ //# sourceMappingURL=chunk-Z4MWLVS2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/hook.ts","../src/commands/hook/script.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { mkdir, readFile, rename, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport {\n copyToStableHooksDir,\n resolveHookScriptPath,\n resolveSettingsPath,\n type ScriptResolution,\n} from \"./hook/script.js\";\n\n/**\n * `almanac hook install|uninstall|status` — wires the bundled\n * `hooks/almanac-capture.sh` into `~/.claude/settings.json` as a\n * `SessionEnd` hook.\n *\n * Design notes:\n *\n * - **Schema.** Claude Code validates `settings.json` against a strict\n * schema: each entry in an event array (like `SessionEnd`) is a\n * `{matcher, hooks: [...]}` container, and the actual command objects\n * live in the nested `hooks` array. v0.1.0–v0.1.4 wrote command objects\n * directly at the event-array level; newer Claude Code versions now\n * reject that shape. We produce the wrapped form on install, and when\n * encountering a legacy unwrapped entry that we recognize as ours (by\n * `command` ending in `almanac-capture.sh`) we migrate it on next\n * install. `SessionEnd` never uses the `matcher` field to discriminate\n * anything — we always emit an empty `matcher: \"\"` (matches\n * everything, which is what session-end lifecycle hooks want).\n *\n * - **Idempotent.** `install` twice leaves one entry, not two. We match by\n * `command` string equality on the inner `hooks[]` entries. If the user\n * replaces our absolute path with a symlink pointing at the same\n * script, we'll treat it as foreign. That's acceptable; the `status`\n * output shows the path we'd use, so the user can reconcile manually.\n *\n * - **Refuse foreign entries.** If `SessionEnd` is already populated with\n * a command we don't recognize, we print the existing value and exit\n * non-zero. Claude Code lets users wire their own hooks (notifications,\n * git autocommit scripts, etc.) and silently replacing them would be\n * rude. Foreign wrapped containers that don't reference our script are\n * preserved byte-for-byte.\n *\n * - **Atomic write.** `settings.json` is small but heavily touched by\n * Claude Code. Writing via tmp-file + rename avoids corrupting the file\n * if we crash mid-write.\n *\n * - **Non-interactive.** No prompts, no confirmations. The caller is\n * already making an intentional choice by running `almanac hook\n * install`.\n */\n\nexport interface HookCommandOptions {\n /**\n * Override the hook script path. Production code leaves this undefined\n * and we resolve the bundled `hooks/almanac-capture.sh`. Tests pass a\n * fixture path to avoid depending on the runtime-install layout.\n */\n hookScriptPath?: string;\n /**\n * Override `~/.claude/settings.json`. Tests sandbox this to a tmpdir;\n * production code leaves it undefined.\n */\n settingsPath?: string;\n /**\n * Override the stable hooks directory where we copy the script.\n * Defaults to `~/.claude/hooks/`. Tests sandbox this to a tmpdir.\n *\n * Bug #1 fix: we always copy the bundled script to this stable path\n * before writing it into settings.json. This way the settings entry\n * points at a user-owned location that survives npm version bumps,\n * npx cache evictions, and nvm version switches — instead of an\n * ephemeral path inside ~/.npm/_npx/<sha>/... or the nvm-versioned\n * node_modules/.\n */\n stableHooksDir?: string;\n}\n\nexport interface HookCommandResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\nconst HOOK_TIMEOUT_SECONDS = 10;\n\n/** A single command invocation inside a wrapper's `hooks[]` array. */\ninterface HookCommand {\n type: \"command\";\n command: string;\n timeout?: number;\n}\n\n/** A wrapped SessionEnd entry per Claude Code's schema. */\ninterface WrappedEntry {\n matcher: string;\n hooks: HookCommand[];\n}\n\n/**\n * What we read from `settings.hooks.SessionEnd`. During a read we may\n * encounter the legacy unwrapped shape (`HookCommand` directly) written\n * by v0.1.0–v0.1.4 — we recognize and migrate it. Unknown entries we\n * can't classify are preserved as-is via `unknown`.\n */\ntype RawEntry = WrappedEntry | HookCommand | unknown;\n\n/**\n * Claude Code's `settings.json` is a free-form JSON object; we only care\n * about the `hooks.SessionEnd` array. Preserve everything else verbatim\n * so we don't drop user settings when we write the file back.\n */\ntype SettingsJson = Record<string, unknown> & {\n hooks?: Record<string, RawEntry[] | undefined>;\n};\n\n/**\n * Heuristic: does this command path look like one we installed?\n *\n * We match on the filename `almanac-capture.sh` regardless of the parent\n * directory. This covers:\n * - the stable path: `~/.claude/hooks/almanac-capture.sh`\n * - legacy paths from v0.1.0–v0.1.5: inside the nvm node_modules or\n * npx cache\n * The stable path is what new installs produce; legacy paths are what\n * we migrate when the user runs `almanac hook install` again.\n */\nfunction isOurCommandPath(command: string): boolean {\n return command.endsWith(\"almanac-capture.sh\");\n}\n\n/**\n * Classify a raw SessionEnd entry. Wrapped entries are the canonical\n * shape; unwrapped-command entries are legacy output from v0.1.0–v0.1.4.\n * Anything else (random user JSON) is `unknown` and we leave it alone.\n */\ntype Classified =\n | { kind: \"wrapped\"; entry: WrappedEntry }\n | { kind: \"legacy\"; entry: HookCommand }\n | { kind: \"unknown\"; entry: unknown };\n\nfunction classifyEntry(raw: RawEntry): Classified {\n if (raw === null || typeof raw !== \"object\") {\n return { kind: \"unknown\", entry: raw };\n }\n const obj = raw as Record<string, unknown>;\n if (Array.isArray(obj.hooks)) {\n // Wrapped shape. `matcher` may be absent in hand-edited files; treat\n // absent as \"\" so we don't throw on slightly malformed input.\n const matcher = typeof obj.matcher === \"string\" ? obj.matcher : \"\";\n const hooks: HookCommand[] = [];\n for (const h of obj.hooks as unknown[]) {\n if (h !== null && typeof h === \"object\") {\n const ho = h as Record<string, unknown>;\n if (ho.type === \"command\" && typeof ho.command === \"string\") {\n const cmd: HookCommand = {\n type: \"command\",\n command: ho.command,\n };\n if (typeof ho.timeout === \"number\") cmd.timeout = ho.timeout;\n hooks.push(cmd);\n }\n }\n }\n return { kind: \"wrapped\", entry: { matcher, hooks } };\n }\n if (obj.type === \"command\" && typeof obj.command === \"string\") {\n // Legacy unwrapped shape — v0.1.0–v0.1.4 wrote this form.\n const cmd: HookCommand = {\n type: \"command\",\n command: obj.command as string,\n };\n if (typeof obj.timeout === \"number\") cmd.timeout = obj.timeout;\n return { kind: \"legacy\", entry: cmd };\n }\n return { kind: \"unknown\", entry: raw };\n}\n\n/** True when the entry references our script and is safely ours to manage. */\nfunction isOurWrapped(entry: WrappedEntry): boolean {\n return entry.hooks.some((h) => isOurCommandPath(h.command));\n}\n\nexport async function runHookInstall(\n options: HookCommandOptions = {},\n): Promise<HookCommandResult> {\n const bundled = resolveHookScriptPath(options);\n if (!bundled.ok) {\n return { stdout: \"\", stderr: `almanac: ${bundled.error}\\n`, exitCode: 1 };\n }\n\n // Copy the bundled hook script to a stable user-owned location before\n // writing that path into settings.json. This is the Bug #1 fix:\n //\n // OLD behavior: settings.json pointed at the bundled path (inside\n // ~/.nvm/versions/node/<ver>/lib/node_modules/codealmanac/hooks/... or\n // ~/.npm/_npx/<sha>/node_modules/codealmanac/hooks/...). When the user\n // switches Node versions or the npx cache is evicted, the path breaks\n // silently and captures stop firing.\n //\n // NEW behavior: we copy almanac-capture.sh to ~/.claude/hooks/ (same\n // directory Claude Code uses for its own built-in hooks, always present)\n // and point settings.json there. The stable path is independent of\n // Node version and npm cache state. When the user upgrades codealmanac,\n // `almanac hook install` copies a fresh script and updates settings.json\n // if the path changed.\n //\n // When `hookScriptPath` is explicitly provided (test injection), the\n // caller has already specified the destination path — skip the copy and\n // use that path directly. The stable-copy concern only applies to the\n // production flow where we resolved from the bundled package layout.\n const script: ScriptResolution = options.hookScriptPath !== undefined\n ? bundled // already the caller-provided path, no copy needed\n : await copyToStableHooksDir(bundled.path, options);\n if (!script.ok) {\n return { stdout: \"\", stderr: `almanac: ${script.error}\\n`, exitCode: 1 };\n }\n\n const settingsPath = resolveSettingsPath(options);\n const settings = await readSettings(settingsPath);\n const existing = (settings.hooks?.SessionEnd ?? []).slice();\n\n // Walk existing entries and split them into buckets:\n // - `preserved` — foreign wrapped/unknown entries we leave alone.\n // - `oursAlready` — a wrapped entry that already points at OUR exact\n // script path (makes install a no-op).\n // - `oursStale` — a wrapped or legacy entry that references our\n // capture script but at a different absolute path\n // (old install, `npm i` moved us) or in the legacy\n // unwrapped shape. We'll collapse these into a\n // single fresh entry at the new path.\n const preserved: RawEntry[] = [];\n let oursAlready: WrappedEntry | null = null;\n const staleCount = { n: 0 };\n\n for (const raw of existing) {\n const c = classifyEntry(raw);\n if (c.kind === \"wrapped\") {\n if (!isOurWrapped(c.entry)) {\n preserved.push(raw);\n continue;\n }\n // Entry belongs to us. Does it already point at the exact script\n // path? If every command in its `hooks[]` that looks like ours is\n // already at `script.path`, it's up to date.\n const exactMatch = c.entry.hooks.some(\n (h) => h.command === script.path,\n );\n if (exactMatch && oursAlready === null) {\n oursAlready = c.entry;\n } else {\n staleCount.n += 1;\n }\n } else if (c.kind === \"legacy\") {\n if (isOurCommandPath(c.entry.command)) {\n // Legacy unwrapped entry of ours — always migrate to wrapped.\n staleCount.n += 1;\n } else {\n // Foreign legacy entry (user had their own script before\n // settings.json required wrapping). Leave it alone.\n preserved.push(raw);\n }\n } else {\n // Unknown shape — we can't classify it. Preserve verbatim.\n preserved.push(raw);\n }\n }\n\n // If every non-ours entry is a foreign unwrapped command (not a\n // wrapped one) we refuse to touch the file — Claude Code's newer\n // schema will already reject such files, but surfacing it here lets\n // the user clean up before we stack our entry on top. Wrapped foreign\n // entries are fine to leave alongside ours.\n const foreignLegacy = preserved.filter((raw) => {\n const c = classifyEntry(raw);\n return c.kind === \"legacy\";\n });\n if (foreignLegacy.length > 0) {\n const lines = foreignLegacy\n .map((raw) => {\n const c = classifyEntry(raw);\n if (c.kind === \"legacy\") return ` - ${c.entry.command}`;\n return \" - <unrecognized>\";\n })\n .join(\"\\n\");\n return {\n stdout: \"\",\n stderr:\n `almanac: SessionEnd has a foreign legacy entry:\\n${lines}\\n` +\n `Remove or rewrap it manually in ${settingsPath} before installing.\\n`,\n exitCode: 1,\n };\n }\n\n if (oursAlready !== null && staleCount.n === 0) {\n return {\n stdout: `almanac: SessionEnd hook already installed at ${script.path}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n // Build the fresh wrapped entry and append to preserved foreign\n // entries. Stale entries of ours are dropped (we only ever want a\n // single active entry; multiple copies of the capture hook would\n // double-fire on session end).\n const fresh: WrappedEntry = {\n matcher: \"\",\n hooks: [\n {\n type: \"command\",\n command: script.path,\n timeout: HOOK_TIMEOUT_SECONDS,\n },\n ],\n };\n\n const newEntries: RawEntry[] = [...preserved, fresh];\n\n settings.hooks = { ...(settings.hooks ?? {}), SessionEnd: newEntries };\n await writeSettings(settingsPath, settings);\n\n return {\n stdout:\n `almanac: SessionEnd hook installed\\n` +\n ` script: ${script.path}\\n` +\n ` settings: ${settingsPath}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\nexport async function runHookUninstall(\n options: HookCommandOptions = {},\n): Promise<HookCommandResult> {\n const settingsPath = resolveSettingsPath(options);\n\n if (!existsSync(settingsPath)) {\n return {\n stdout: `almanac: SessionEnd hook not installed (no settings file)\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n const settings = await readSettings(settingsPath);\n const existing = (settings.hooks?.SessionEnd ?? []).slice();\n\n const kept: RawEntry[] = [];\n let removed = 0;\n\n for (const raw of existing) {\n const c = classifyEntry(raw);\n if (c.kind === \"wrapped\") {\n // Filter out our command(s) from the inner hooks array. Keep\n // anything else in the array intact — a foreign wrapper that\n // happened to include our script alongside its own commands\n // (unusual, but survivable) loses our entry and keeps theirs.\n const innerKept = c.entry.hooks.filter(\n (h) => !isOurCommandPath(h.command),\n );\n const innerRemoved = c.entry.hooks.length - innerKept.length;\n removed += innerRemoved;\n if (innerKept.length === 0) {\n // Only drop the outer wrapper when it was entirely ours. A\n // foreign wrapper that never contained our script stays verbatim\n // below (handled by `innerRemoved === 0`, which leaves\n // `innerKept.length === c.entry.hooks.length`, hence we fall\n // through to the else-branch).\n if (innerRemoved === 0) kept.push(raw);\n // else: fully owned by us, drop the container.\n } else if (innerRemoved === 0) {\n // Untouched foreign wrapper — preserve the raw object to keep\n // any fields (like matcher) byte-for-byte.\n kept.push(raw);\n } else {\n // Partial: rebuild with just the kept inner entries, preserving\n // the original matcher string.\n kept.push({ matcher: c.entry.matcher, hooks: innerKept });\n }\n } else if (c.kind === \"legacy\") {\n if (isOurCommandPath(c.entry.command)) {\n removed += 1;\n } else {\n kept.push(raw);\n }\n } else {\n kept.push(raw);\n }\n }\n\n if (removed === 0) {\n return {\n stdout: `almanac: SessionEnd hook not installed\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n if (settings.hooks !== undefined) {\n if (kept.length === 0) {\n // Empty SessionEnd array confuses some linters; drop the key when\n // nothing's left.\n const { SessionEnd: _dropped, ...rest } = settings.hooks;\n void _dropped;\n settings.hooks = rest;\n } else {\n settings.hooks = { ...settings.hooks, SessionEnd: kept };\n }\n\n // If `hooks` itself is now empty (user had only our SessionEnd entry\n // and no other hook categories), drop the `hooks` key entirely so\n // uninstall leaves the settings file in the same shape it would be\n // in had we never run install. An empty `\"hooks\": {}` is an obvious\n // breadcrumb in commit diffs.\n if (Object.keys(settings.hooks).length === 0) {\n delete settings.hooks;\n }\n }\n\n await writeSettings(settingsPath, settings);\n\n return {\n stdout: `almanac: SessionEnd hook removed\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\nexport async function runHookStatus(\n options: HookCommandOptions = {},\n): Promise<HookCommandResult> {\n const script = resolveHookScriptPath(options);\n const settingsPath = resolveSettingsPath(options);\n\n if (!existsSync(settingsPath)) {\n return {\n stdout:\n `SessionEnd hook: not installed\\n` +\n `settings: ${settingsPath} (does not exist)\\n` +\n (script.ok ? `script would be: ${script.path}\\n` : \"\"),\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n const settings = await readSettings(settingsPath);\n const existing = settings.hooks?.SessionEnd ?? [];\n\n // Walk the array looking for any entry (wrapped or legacy) that\n // references our capture script. Gathering foreign entries separately\n // lets us show them to the user if nothing of ours was found.\n let ourCommand: string | null = null;\n const foreignSummary: string[] = [];\n for (const raw of existing) {\n const c = classifyEntry(raw);\n if (c.kind === \"wrapped\") {\n for (const h of c.entry.hooks) {\n if (isOurCommandPath(h.command)) {\n ourCommand ??= h.command;\n } else {\n foreignSummary.push(h.command);\n }\n }\n } else if (c.kind === \"legacy\") {\n if (isOurCommandPath(c.entry.command)) {\n ourCommand ??= c.entry.command;\n } else {\n foreignSummary.push(c.entry.command);\n }\n }\n }\n\n if (ourCommand === null) {\n const foreignLines = foreignSummary\n .map((c) => ` - ${c}`)\n .join(\"\\n\");\n return {\n stdout:\n `SessionEnd hook: not installed\\n` +\n `settings: ${settingsPath}\\n` +\n (foreignSummary.length > 0\n ? `(${foreignSummary.length} foreign entr${foreignSummary.length === 1 ? \"y\" : \"ies\"} present:\\n${foreignLines})\\n`\n : \"\") +\n (script.ok ? `script would be: ${script.path}\\n` : \"\"),\n stderr: \"\",\n exitCode: 0,\n };\n }\n\n return {\n stdout:\n `SessionEnd hook: installed\\n` +\n `script: ${ourCommand}\\n` +\n `settings: ${settingsPath}\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\n// ─── Settings JSON helpers ───────────────────────────────────────────\n\nasync function readSettings(settingsPath: string): Promise<SettingsJson> {\n if (!existsSync(settingsPath)) return {};\n try {\n const raw = await readFile(settingsPath, \"utf8\");\n if (raw.trim().length === 0) return {};\n const parsed = JSON.parse(raw) as unknown;\n if (parsed === null || typeof parsed !== \"object\") return {};\n return parsed as SettingsJson;\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`failed to read ${settingsPath}: ${msg}`);\n }\n}\n\nasync function writeSettings(\n settingsPath: string,\n settings: SettingsJson,\n): Promise<void> {\n const dir = path.dirname(settingsPath);\n await mkdir(dir, { recursive: true });\n\n // Atomic write: JSON.stringify → tmp file → rename. `rename` within the\n // same filesystem is atomic on POSIX; Claude Code never sees a partial\n // file. Formatted with 2-space indent to match the existing settings.\n const tmp = `${settingsPath}.almanac-tmp-${process.pid}`;\n const body = `${JSON.stringify(settings, null, 2)}\\n`;\n await writeFile(tmp, body, \"utf8\");\n await rename(tmp, settingsPath);\n}\n","import { existsSync } from \"node:fs\";\nimport { copyFile, mkdir, readFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport interface HookPathOptions {\n hookScriptPath?: string;\n settingsPath?: string;\n stableHooksDir?: string;\n}\n\nexport type ScriptResolution =\n | { ok: true; path: string }\n | { ok: false; error: string };\n\nexport function resolveSettingsPath(options: HookPathOptions): string {\n if (options.settingsPath !== undefined) return options.settingsPath;\n return path.join(homedir(), \".claude\", \"settings.json\");\n}\n\n/**\n * Copy the bundled hook script to `~/.claude/hooks/almanac-capture.sh`.\n *\n * This stable, user-owned destination survives Node version switches and\n * npm/npx cache evictions. The copy is idempotent: if bytes already match\n * we skip writing so repeated setup runs do not bump mtimes.\n */\nexport async function copyToStableHooksDir(\n bundledPath: string,\n options: HookPathOptions,\n): Promise<ScriptResolution> {\n const stableHooksDir =\n options.stableHooksDir ?? path.join(homedir(), \".claude\", \"hooks\");\n const dest = path.join(stableHooksDir, \"almanac-capture.sh\");\n\n try {\n await mkdir(stableHooksDir, { recursive: true });\n const srcBytes = await readFile(bundledPath);\n let needsCopy = true;\n if (existsSync(dest)) {\n try {\n const destBytes = await readFile(dest);\n if (srcBytes.equals(destBytes)) needsCopy = false;\n } catch {\n // Can't read dest — overwrite.\n }\n }\n if (needsCopy) {\n await copyFile(bundledPath, dest);\n }\n return { ok: true, path: dest };\n } catch (err: unknown) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n ok: false,\n error: `could not copy hook script to ${dest}: ${msg}`,\n };\n }\n}\n\n/**\n * Locate the bundled `hooks/almanac-capture.sh`. Mirrors\n * `resolvePromptsDir` from `src/agent/prompts.ts`: two plausible layouts\n * (installed dist vs. source dev), probe each.\n */\nexport function resolveHookScriptPath(\n options: HookPathOptions,\n): ScriptResolution {\n if (options.hookScriptPath !== undefined) {\n return { ok: true, path: options.hookScriptPath };\n }\n\n const here = path.dirname(fileURLToPath(import.meta.url));\n\n const candidates = [\n // Bundled: `.../codealmanac/dist/codealmanac.js` → `../hooks/…`\n path.resolve(here, \"..\", \"hooks\", \"almanac-capture.sh\"),\n // Source after ts-node-style module layout or nested dist helpers.\n path.resolve(here, \"..\", \"..\", \"hooks\", \"almanac-capture.sh\"),\n // Source: `.../codealmanac/src/commands/hook/script.ts` → `../../../hooks/…`\n path.resolve(here, \"..\", \"..\", \"..\", \"hooks\", \"almanac-capture.sh\"),\n // Defensive nested fallback.\n path.resolve(here, \"..\", \"..\", \"..\", \"..\", \"hooks\", \"almanac-capture.sh\"),\n ];\n\n for (const candidate of candidates) {\n if (existsSync(candidate)) {\n return { ok: true, path: candidate };\n }\n }\n\n return {\n ok: false,\n error:\n `could not locate hooks/almanac-capture.sh. Tried:\\n` +\n candidates.map((c) => ` - ${c}`).join(\"\\n\"),\n };\n}\n"],"mappings":"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","names":["existsSync","mkdir","readFile","path","existsSync","readFile","path","mkdir"]}
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ checkForUpdate,
4
+ isNewer,
5
+ readConfig,
6
+ readState,
7
+ writeConfig,
8
+ writeState
9
+ } from "./chunk-AXFPUHBN.js";
10
+
11
+ // src/commands/update.ts
12
+ import { spawn } from "child_process";
13
+ import { createRequire } from "module";
14
+ async function runUpdate(opts = {}) {
15
+ if (opts.enableNotifier === true) {
16
+ return await toggleNotifier(true, opts);
17
+ }
18
+ if (opts.disableNotifier === true) {
19
+ return await toggleNotifier(false, opts);
20
+ }
21
+ if (opts.dismiss === true) {
22
+ return await dismissLatest(opts);
23
+ }
24
+ if (opts.check === true) {
25
+ return await forceCheck(opts);
26
+ }
27
+ return await installLatest(opts);
28
+ }
29
+ async function dismissLatest(opts) {
30
+ const state = await readState(opts.statePath);
31
+ if (state.latest_version.length === 0) {
32
+ return {
33
+ stdout: "codealmanac: no pending update to dismiss. Run `almanac update --check` to query the registry.\n",
34
+ stderr: "",
35
+ exitCode: 0
36
+ };
37
+ }
38
+ const installed = opts.installedVersion ?? readInstalledVersion();
39
+ if (!isNewer(state.latest_version, installed)) {
40
+ return {
41
+ stdout: `codealmanac: already on latest (${installed}); nothing to dismiss.
42
+ `,
43
+ stderr: "",
44
+ exitCode: 0
45
+ };
46
+ }
47
+ if (state.dismissed_versions.includes(state.latest_version)) {
48
+ return {
49
+ stdout: `codealmanac: ${state.latest_version} already dismissed.
50
+ `,
51
+ stderr: "",
52
+ exitCode: 0
53
+ };
54
+ }
55
+ const next = {
56
+ ...state,
57
+ dismissed_versions: [...state.dismissed_versions, state.latest_version]
58
+ };
59
+ await writeState(next, opts.statePath);
60
+ return {
61
+ stdout: `codealmanac: dismissed ${state.latest_version}. The nag banner will not show for this version.
62
+ Run \`almanac update\` to upgrade, or \`almanac update --enable-notifier\` to re-enable nags.
63
+ `,
64
+ stderr: "",
65
+ exitCode: 0
66
+ };
67
+ }
68
+ async function forceCheck(opts) {
69
+ const installed = opts.installedVersion ?? readInstalledVersion();
70
+ const checkFn = opts.checkFn ?? checkForUpdate;
71
+ const result = await checkFn({
72
+ installedVersion: installed,
73
+ force: true,
74
+ statePath: opts.statePath,
75
+ now: opts.now
76
+ });
77
+ if (result.fetchFailed) {
78
+ return {
79
+ stdout: "",
80
+ stderr: `codealmanac: could not reach registry.npmjs.org (timeout or network error).
81
+ Installed: ${installed}. No cached latest available.
82
+ `,
83
+ exitCode: 1
84
+ };
85
+ }
86
+ const latest = result.state.latest_version;
87
+ if (latest.length === 0) {
88
+ return {
89
+ stdout: `codealmanac: installed ${installed}; registry did not report a latest tag.
90
+ `,
91
+ stderr: "",
92
+ exitCode: 0
93
+ };
94
+ }
95
+ if (isNewer(latest, installed)) {
96
+ const dismissed = result.state.dismissed_versions.includes(latest) ? " (dismissed \u2014 banner suppressed; `almanac update` still installs)" : "";
97
+ return {
98
+ stdout: `codealmanac ${latest} available (you're on ${installed})${dismissed}.
99
+ Run: almanac update
100
+ `,
101
+ stderr: "",
102
+ exitCode: 0
103
+ };
104
+ }
105
+ return {
106
+ stdout: `codealmanac: up to date (${installed}).
107
+ `,
108
+ stderr: "",
109
+ exitCode: 0
110
+ };
111
+ }
112
+ async function toggleNotifier(enable, opts) {
113
+ const config = await readConfig(opts.configPath);
114
+ const next = { ...config, update_notifier: enable };
115
+ await writeConfig(next, opts.configPath);
116
+ return {
117
+ stdout: enable ? "codealmanac: update notifier enabled. The pre-command banner will show when a new version is available.\n" : "codealmanac: update notifier disabled. No more pre-command banners. Run `almanac update --check` to see status.\n",
118
+ stderr: "",
119
+ exitCode: 0
120
+ };
121
+ }
122
+ async function installLatest(opts) {
123
+ const spawnFn = opts.spawnFn ?? spawn;
124
+ const installed = opts.installedVersion ?? readInstalledVersion();
125
+ const spawnOpts = { stdio: "inherit" };
126
+ return await new Promise((resolve) => {
127
+ const child = spawnFn(
128
+ "npm",
129
+ ["i", "-g", "codealmanac@latest"],
130
+ spawnOpts
131
+ );
132
+ child.on("error", (err) => {
133
+ if (err.code === "ENOENT") {
134
+ resolve({
135
+ stdout: "",
136
+ stderr: "codealmanac: `npm` not found on PATH. Install Node.js + npm, or install codealmanac via your package manager.\n",
137
+ exitCode: 1
138
+ });
139
+ return;
140
+ }
141
+ resolve({
142
+ stdout: "",
143
+ stderr: `codealmanac: failed to run npm: ${err.message}
144
+ `,
145
+ exitCode: 1
146
+ });
147
+ });
148
+ child.on("exit", async (code, _signal) => {
149
+ const exitCode = code ?? 1;
150
+ if (exitCode !== 0) {
151
+ const hint = `codealmanac: npm install failed (exit ${exitCode}).
152
+ If you see "EACCES" above, try: sudo npm i -g codealmanac@latest
153
+ Or install with a version manager (nvm, volta, fnm) to avoid sudo.
154
+ `;
155
+ resolve({ stdout: "", stderr: hint, exitCode });
156
+ return;
157
+ }
158
+ try {
159
+ const state = await readState(opts.statePath);
160
+ const now = opts.now ?? (() => Math.floor(Date.now() / 1e3));
161
+ await writeState(
162
+ {
163
+ last_check_at: now(),
164
+ installed_version: state.latest_version || installed,
165
+ latest_version: state.latest_version || installed,
166
+ dismissed_versions: state.dismissed_versions
167
+ },
168
+ opts.statePath
169
+ );
170
+ } catch {
171
+ }
172
+ resolve({
173
+ stdout: "codealmanac: updated.\n",
174
+ stderr: "",
175
+ exitCode: 0
176
+ });
177
+ });
178
+ });
179
+ }
180
+ function readInstalledVersion() {
181
+ try {
182
+ const require2 = createRequire(import.meta.url);
183
+ const pkg = require2("../../package.json");
184
+ if (typeof pkg.version === "string" && pkg.version.length > 0) {
185
+ return pkg.version;
186
+ }
187
+ } catch {
188
+ }
189
+ try {
190
+ const require2 = createRequire(import.meta.url);
191
+ const pkg = require2("../package.json");
192
+ if (typeof pkg.version === "string" && pkg.version.length > 0) {
193
+ return pkg.version;
194
+ }
195
+ } catch {
196
+ }
197
+ return "unknown";
198
+ }
199
+
200
+ export {
201
+ runUpdate
202
+ };
203
+ //# sourceMappingURL=chunk-Z6MBJ3D2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/update.ts"],"sourcesContent":["import { spawn, type SpawnOptions } from \"node:child_process\";\nimport { createRequire } from \"node:module\";\n\nimport { checkForUpdate } from \"../update/check.js\";\nimport {\n readConfig,\n writeConfig,\n type GlobalConfig,\n} from \"../update/config.js\";\nimport { isNewer } from \"../update/semver.js\";\nimport { readState, writeState } from \"../update/state.js\";\n\n/**\n * `almanac update` — manual upgrade command, the counterpart to the\n * persistent nag banner.\n *\n * Default action: shell out to `npm i -g codealmanac@latest` with\n * inherited stdio so the user sees real-time download/install/permission\n * output. Synchronous in the user's terminal — no background install,\n * no mid-invocation swap (see the pair review's Tier-B design for\n * rationale).\n *\n * Flags:\n * --dismiss — mark the current `latest_version` as \"don't nag about\n * this one again\". No install. Writes state and exits.\n * --check — force a registry query regardless of the 24h cache.\n * Shows the result and exits. No install.\n * --enable-notifier / --disable-notifier — flip the global\n * `update_notifier` config. Default is enabled; after\n * `--disable-notifier` the banner won't show even when a new\n * version is available.\n */\n\nexport interface UpdateOptions {\n dismiss?: boolean;\n check?: boolean;\n enableNotifier?: boolean;\n disableNotifier?: boolean;\n\n // ─── Test injection points ──────────────────────────────────────\n /** Override state file path (tests point at a tmpdir). */\n statePath?: string;\n /** Override config file path (tests point at a tmpdir). */\n configPath?: string;\n /** Override the installed version report. */\n installedVersion?: string;\n /**\n * Replace `checkForUpdate` — tests inject a stub that returns a\n * canned state without hitting the registry.\n */\n checkFn?: typeof checkForUpdate;\n /** Replace `spawn` for tests (install path shouldn't run npm). */\n spawnFn?: typeof spawn;\n /** Clock for deterministic `last_check_at` assertions. */\n now?: () => number;\n}\n\nexport interface UpdateResult {\n stdout: string;\n stderr: string;\n exitCode: number;\n}\n\nexport async function runUpdate(\n opts: UpdateOptions = {},\n): Promise<UpdateResult> {\n // Precedence: config toggles > --dismiss > --check > install.\n // Config toggles are disjoint from the other flags (you'd never\n // `update --dismiss --disable-notifier`), but if someone does we\n // apply them in order and take the last action as the \"command\"\n // that sets the exit code.\n if (opts.enableNotifier === true) {\n return await toggleNotifier(true, opts);\n }\n if (opts.disableNotifier === true) {\n return await toggleNotifier(false, opts);\n }\n if (opts.dismiss === true) {\n return await dismissLatest(opts);\n }\n if (opts.check === true) {\n return await forceCheck(opts);\n }\n return await installLatest(opts);\n}\n\n// ─── --dismiss ────────────────────────────────────────────────────\n\nasync function dismissLatest(opts: UpdateOptions): Promise<UpdateResult> {\n const state = await readState(opts.statePath);\n // Nothing to dismiss when we don't know of a newer version. Silently\n // no-op with a message — more helpful than pretending to write state\n // that no future banner would consult.\n if (state.latest_version.length === 0) {\n return {\n stdout:\n \"codealmanac: no pending update to dismiss. \" +\n \"Run `almanac update --check` to query the registry.\\n\",\n stderr: \"\",\n exitCode: 0,\n };\n }\n const installed = opts.installedVersion ?? readInstalledVersion();\n if (!isNewer(state.latest_version, installed)) {\n return {\n stdout: `codealmanac: already on latest (${installed}); nothing to dismiss.\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n if (state.dismissed_versions.includes(state.latest_version)) {\n return {\n stdout: `codealmanac: ${state.latest_version} already dismissed.\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n const next = {\n ...state,\n dismissed_versions: [...state.dismissed_versions, state.latest_version],\n };\n await writeState(next, opts.statePath);\n return {\n stdout:\n `codealmanac: dismissed ${state.latest_version}. The nag banner ` +\n `will not show for this version.\\n` +\n `Run \\`almanac update\\` to upgrade, or \\`almanac update --enable-notifier\\` to re-enable nags.\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\n// ─── --check ───────────────────────────────────────────────────────\n\nasync function forceCheck(opts: UpdateOptions): Promise<UpdateResult> {\n const installed = opts.installedVersion ?? readInstalledVersion();\n const checkFn = opts.checkFn ?? checkForUpdate;\n const result = await checkFn({\n installedVersion: installed,\n force: true,\n statePath: opts.statePath,\n now: opts.now,\n });\n if (result.fetchFailed) {\n return {\n stdout: \"\",\n stderr:\n `codealmanac: could not reach registry.npmjs.org (timeout or network error).\\n` +\n `Installed: ${installed}. No cached latest available.\\n`,\n exitCode: 1,\n };\n }\n const latest = result.state.latest_version;\n if (latest.length === 0) {\n return {\n stdout: `codealmanac: installed ${installed}; registry did not report a latest tag.\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n if (isNewer(latest, installed)) {\n const dismissed = result.state.dismissed_versions.includes(latest)\n ? \" (dismissed — banner suppressed; `almanac update` still installs)\"\n : \"\";\n return {\n stdout:\n `codealmanac ${latest} available (you're on ${installed})${dismissed}.\\n` +\n `Run: almanac update\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n }\n return {\n stdout: `codealmanac: up to date (${installed}).\\n`,\n stderr: \"\",\n exitCode: 0,\n };\n}\n\n// ─── --enable/--disable-notifier ──────────────────────────────────\n\nasync function toggleNotifier(\n enable: boolean,\n opts: UpdateOptions,\n): Promise<UpdateResult> {\n const config = await readConfig(opts.configPath);\n const next: GlobalConfig = { ...config, update_notifier: enable };\n await writeConfig(next, opts.configPath);\n return {\n stdout:\n enable\n ? \"codealmanac: update notifier enabled. \" +\n \"The pre-command banner will show when a new version is available.\\n\"\n : \"codealmanac: update notifier disabled. \" +\n \"No more pre-command banners. Run `almanac update --check` to see status.\\n\",\n stderr: \"\",\n exitCode: 0,\n };\n}\n\n// ─── default: install ─────────────────────────────────────────────\n\nasync function installLatest(opts: UpdateOptions): Promise<UpdateResult> {\n const spawnFn = opts.spawnFn ?? spawn;\n const installed = opts.installedVersion ?? readInstalledVersion();\n\n // Inherit stdio so npm's progress bar, permission prompts, and\n // peer-dep warnings land in the user's terminal verbatim. No\n // wrapping, no capture — npm output is its own contract.\n const spawnOpts: SpawnOptions = { stdio: \"inherit\" };\n\n return await new Promise<UpdateResult>((resolve) => {\n const child = spawnFn(\n \"npm\",\n [\"i\", \"-g\", \"codealmanac@latest\"],\n spawnOpts,\n );\n\n // Two failure modes need distinct messaging:\n // - ENOENT: npm isn't on PATH. Rare on dev laptops, common in\n // stripped-down CI containers. Tell the user what we tried to\n // run so they can diagnose.\n // - EACCES / exit code 243 / etc.: npm ran but couldn't write\n // to the global prefix. Suggest sudo; don't try it ourselves\n // (silently escalating privileges would be a trust violation,\n // and the pair review explicitly rejected it).\n child.on(\"error\", (err: NodeJS.ErrnoException) => {\n if (err.code === \"ENOENT\") {\n resolve({\n stdout: \"\",\n stderr:\n \"codealmanac: `npm` not found on PATH. \" +\n \"Install Node.js + npm, or install codealmanac via your package manager.\\n\",\n exitCode: 1,\n });\n return;\n }\n resolve({\n stdout: \"\",\n stderr: `codealmanac: failed to run npm: ${err.message}\\n`,\n exitCode: 1,\n });\n });\n\n child.on(\"exit\", async (code, _signal) => {\n const exitCode = code ?? 1;\n if (exitCode !== 0) {\n // Check for the common EACCES cause. npm prints \"EACCES\" to\n // stderr, which we don't have (inherited stdio), so we rely\n // on exit code heuristics + a generic hint.\n const hint =\n `codealmanac: npm install failed (exit ${exitCode}).\\n` +\n `If you see \"EACCES\" above, try: sudo npm i -g codealmanac@latest\\n` +\n `Or install with a version manager (nvm, volta, fnm) to avoid sudo.\\n`;\n resolve({ stdout: \"\", stderr: hint, exitCode });\n return;\n }\n // On success, refresh the state file so the next command's\n // banner reflects that we're current. We can't read the new\n // version out of our own process (we're still running the old\n // build); we record what the state file's latest_version was,\n // on the assumption that npm installed that version.\n try {\n const state = await readState(opts.statePath);\n const now =\n opts.now ?? (() => Math.floor(Date.now() / 1000));\n await writeState(\n {\n last_check_at: now(),\n installed_version: state.latest_version || installed,\n latest_version: state.latest_version || installed,\n dismissed_versions: state.dismissed_versions,\n },\n opts.statePath,\n );\n } catch {\n // Non-fatal: the next `almanac` invocation will re-run the\n // background check and refresh state properly.\n }\n resolve({\n stdout: \"codealmanac: updated.\\n\",\n stderr: \"\",\n exitCode: 0,\n });\n });\n });\n}\n\nfunction readInstalledVersion(): string {\n // Dev layout: `src/commands/update.ts` → `../../package.json`.\n // Bundled layout: `dist/codealmanac.js` → `../package.json`. We try\n // both so the version lookup works from both. (Same approach as\n // `cli.ts` and `doctor.ts`, which hit the same ambiguity.)\n try {\n const require = createRequire(import.meta.url);\n const pkg = require(\"../../package.json\") as { version?: unknown };\n if (typeof pkg.version === \"string\" && pkg.version.length > 0) {\n return pkg.version;\n }\n } catch {\n // Fall through.\n }\n try {\n const require = createRequire(import.meta.url);\n const pkg = require(\"../package.json\") as { version?: unknown };\n if (typeof pkg.version === \"string\" && pkg.version.length > 0) {\n return pkg.version;\n }\n } catch {\n // Fall through.\n }\n return \"unknown\";\n}\n"],"mappings":"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","names":["require"]}