claude-code-infinite 1.0.19 → 1.0.21

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 (73) hide show
  1. package/README.md +95 -5
  2. package/dist/auto-update-runner.d.ts +52 -0
  3. package/dist/auto-update-runner.d.ts.map +1 -0
  4. package/dist/auto-update-runner.js +307 -0
  5. package/dist/auto-update-runner.js.map +1 -0
  6. package/dist/auto-update-setup.d.ts +38 -0
  7. package/dist/auto-update-setup.d.ts.map +1 -0
  8. package/dist/auto-update-setup.js +72 -0
  9. package/dist/auto-update-setup.js.map +1 -0
  10. package/dist/auto-update.d.ts +170 -0
  11. package/dist/auto-update.d.ts.map +1 -0
  12. package/dist/auto-update.js +401 -0
  13. package/dist/auto-update.js.map +1 -0
  14. package/dist/cc-request.d.ts +17 -0
  15. package/dist/cc-request.d.ts.map +1 -1
  16. package/dist/cc-request.js +22 -0
  17. package/dist/cc-request.js.map +1 -1
  18. package/dist/cli-main.js +35 -7
  19. package/dist/cli-main.js.map +1 -1
  20. package/dist/config.d.ts +4 -0
  21. package/dist/config.d.ts.map +1 -1
  22. package/dist/config.js.map +1 -1
  23. package/dist/count-tokens.d.ts +38 -3
  24. package/dist/count-tokens.d.ts.map +1 -1
  25. package/dist/count-tokens.js +88 -19
  26. package/dist/count-tokens.js.map +1 -1
  27. package/dist/memtree-finder.d.ts +11 -0
  28. package/dist/memtree-finder.d.ts.map +1 -1
  29. package/dist/memtree-finder.js +18 -0
  30. package/dist/memtree-finder.js.map +1 -1
  31. package/dist/memtree-mcp-config.d.ts.map +1 -1
  32. package/dist/memtree-mcp-config.js +4 -2
  33. package/dist/memtree-mcp-config.js.map +1 -1
  34. package/dist/memtree-mcp.d.ts +24 -4
  35. package/dist/memtree-mcp.d.ts.map +1 -1
  36. package/dist/memtree-mcp.js +47 -14
  37. package/dist/memtree-mcp.js.map +1 -1
  38. package/dist/memtree-tools.d.ts +17 -1
  39. package/dist/memtree-tools.d.ts.map +1 -1
  40. package/dist/memtree-tools.js +50 -2
  41. package/dist/memtree-tools.js.map +1 -1
  42. package/dist/memtree.d.ts +10 -1
  43. package/dist/memtree.d.ts.map +1 -1
  44. package/dist/memtree.js +54 -19
  45. package/dist/memtree.js.map +1 -1
  46. package/dist/notices.d.ts +6 -1
  47. package/dist/notices.d.ts.map +1 -1
  48. package/dist/notices.js +16 -4
  49. package/dist/notices.js.map +1 -1
  50. package/dist/proxy.d.ts +17 -1
  51. package/dist/proxy.d.ts.map +1 -1
  52. package/dist/proxy.js +137 -53
  53. package/dist/proxy.js.map +1 -1
  54. package/dist/reqlog.d.ts +10 -0
  55. package/dist/reqlog.d.ts.map +1 -1
  56. package/dist/reqlog.js.map +1 -1
  57. package/dist/request-sizing.d.ts +21 -2
  58. package/dist/request-sizing.d.ts.map +1 -1
  59. package/dist/request-sizing.js +60 -7
  60. package/dist/request-sizing.js.map +1 -1
  61. package/dist/token-estimate.d.ts +35 -0
  62. package/dist/token-estimate.d.ts.map +1 -0
  63. package/dist/token-estimate.js +97 -0
  64. package/dist/token-estimate.js.map +1 -0
  65. package/dist/update-check.d.ts +23 -4
  66. package/dist/update-check.d.ts.map +1 -1
  67. package/dist/update-check.js +79 -12
  68. package/dist/update-check.js.map +1 -1
  69. package/dist/update-lock.d.ts +68 -0
  70. package/dist/update-lock.d.ts.map +1 -0
  71. package/dist/update-lock.js +176 -0
  72. package/dist/update-lock.js.map +1 -0
  73. package/package.json +1 -1
package/README.md CHANGED
@@ -256,6 +256,30 @@ features, and their output, often the biggest source of tokens, is exactly what
256
256
  For `ccc --resume <id>`, an automatic, best-effort repair can unblock a transcript whose last response already exceeds Claude Code’s local refusal limit. It changes only that response’s usage totals and the iteration Claude Code counts, after writing an exclusive, uniquely named backup under `~/.claude-code-infinite/transcript-backups`. Earlier iterations and conversation content stay intact. The installed Claude Code 2.1.288 computes its blocking limit as the context window minus the output allowance (capped at 20,000) minus 3,000 tokens: 977,000 for the supported 1M models. Other versions use the conservative full-window threshold. The window comes from ccc’s model table, explicit `[1m]` transcript model signals, and any `--model` or `ANTHROPIC_MODEL` override, conservatively keeping the largest supported window. A successful response with input already beyond that inferred window disproves the inference and is never repaired. Unknown model overrides (including unresolved aliases), models or usage shapes, threshold overrides, possible running writers, changed snapshots, backup collisions, and filesystem errors all skip repair and continue the original resume. Process and file checks are bounded; other running JavaScript runtimes may conservatively prevent repair.
257
257
 
258
258
 
259
+ ## Releasing
260
+
261
+ Releases are staged by GitHub Actions through npm Trusted Publishing (OIDC), so no npm
262
+ token, passkey or browser is needed to build and upload them, and each one is approved by a
263
+ maintainer before it goes live:
264
+
265
+ 1. On a clean `main`, run `./publish.sh` (or `./publish.sh minor` / `major`). It builds,
266
+ checks that `dist/` is committed, runs `npm version`, and pushes the commit and the
267
+ `vX.Y.Z` tag.
268
+ 2. The tag push runs [`.github/workflows/publish.yml`](.github/workflows/publish.yml): `npm ci`,
269
+ the test suite with `NO_COLOR=1` and with `FORCE_COLOR=1`, a check that the rebuilt
270
+ `dist/` matches the commit and that the tag equals `package.json`'s version, then
271
+ `npm stage publish --provenance`.
272
+ 3. Approve the staged version (2FA) on
273
+ [npmjs.com](https://www.npmjs.com/package/claude-code-infinite) under the **Staged
274
+ Packages** tab, or with `npm stage list claude-code-infinite` and
275
+ `npm stage approve <stage-id>`. Until then it is not installable.
276
+
277
+ The trusted publisher on npmjs.com is bound to the workflow file name `publish.yml` with no
278
+ GitHub environment, and allows only staged publishing; renaming the workflow breaks it.
279
+ `./publish.sh --local` is the fallback: it publishes directly from your machine with npm web
280
+ auth, then pushes the commit and tag; the workflow still tests that tag but skips staging,
281
+ since the version is already published.
282
+
259
283
  ## Troubleshooting
260
284
 
261
285
  ### Anthropic auth errors (401s, login prompts)
@@ -268,12 +292,78 @@ If you see an inline "⚠ MemTree degraded — this turn ran uncompressed" notic
268
292
 
269
293
  ## Upgrading
270
294
 
271
- `ccc` checks npm at startup (bounded to two seconds, silent if offline) and, when a
272
- newer release exists, shows one line under the MemTree banner inside Claude Code with
273
- the upgrade command. Upgrading is never automatic:
295
+ `ccc` checks npm at startup (bounded to 1.5 seconds, silent if offline, and cached for a
296
+ day in `~/.claude-code-infinite/update-check.json`). Prereleases are never offered, and
297
+ print (`-p`) or non-TTY runs skip the check. When a newer release exists and `ccc` can
298
+ update itself, it does (see [Automatic updates](#automatic-updates)); otherwise it shows
299
+ one line under the MemTree banner inside Claude Code, e.g.
300
+ `• ccc 1.0.21 is available (you have 1.0.20) · npm i -g claude-code-infinite@latest`.
301
+ To upgrade by hand:
274
302
 
275
303
  ```bash
276
- npm install -g claude-code-infinite
304
+ npm i -g claude-code-infinite@latest
305
+ ```
306
+
307
+ Set `CCC_NO_UPDATE_CHECK=1` (or the older `CCC_SKIP_UPDATE_CHECK=1`), or add
308
+ `"updateCheck": false` to `~/.claude-code-infinite/config.json`, to disable the check
309
+ (air-gapped or CI runs). That also turns off automatic updates.
310
+
311
+ ## Automatic updates
312
+
313
+ Like Claude Code, `ccc` installs a newer release for you in the background. A few
314
+ seconds after an interactive session starts, if the update check found a newer
315
+ release, `ccc` runs
316
+
317
+ ```bash
318
+ npm install --global --prefix=<your npm prefix> claude-code-infinite@<that version> --no-fund --no-audit
319
+ ```
320
+
321
+ in a detached process, so the session never waits for it and quitting `ccc` does not
322
+ stop it. When it has finished, the next session start inside Claude Code (a new
323
+ conversation, `/clear`, or a resume) shows, once:
324
+
325
+ ```
326
+ ✔ ccc 1.0.21 installed · restart ccc to use it
277
327
  ```
278
328
 
279
- Set `CCC_SKIP_UPDATE_CHECK=1` to disable the check (air-gapped or CI runs).
329
+ The session keeps running the version it started with until you restart `ccc`. If the
330
+ install fails, you get the usual `• ccc 1.0.21 is available … · npm i -g
331
+ claude-code-infinite@latest` line instead, and `ccc` waits a day before trying again; it
332
+ tries each version at most twice. The npm output of the last attempt is in
333
+ `~/.claude-code-infinite/auto-update.log`, and its result in `update-state.json` next to
334
+ it. Only one session installs at a time (`update.lock.d` there). If an installer was
335
+ killed in a way `ccc` cannot verify, that lock stays held rather than risk two installs
336
+ at once; after an hour `ccc` shows the update line instead, and deleting
337
+ `~/.claude-code-infinite/update.lock.d` clears it.
338
+
339
+ **When it doesn't.** `ccc` updates itself only when it is a plain global npm install
340
+ (`npm i -g`) whose files are in `npm root -g` and writable by you. It never uses `sudo`.
341
+ In every other case it only shows the update line:
342
+
343
+ - `npm link`ed or git checkouts (a development copy is never touched),
344
+ - `npx` and project-local installs,
345
+ - pnpm, yarn, bun and Volta global installs (update with your tool's own command),
346
+ - a global prefix that needs `sudo` to write, or `ccc` running as root,
347
+ - Windows,
348
+ - print (`-p`) and non-TTY runs, which never install anything.
349
+
350
+ `ccc --debug` prints which install method was detected and what it decided.
351
+
352
+ **Sessions running during an update.** npm replaces `ccc`'s files while other `ccc`
353
+ sessions may be running. They are unaffected: each `ccc` loads all of its code when it
354
+ starts, and the Claude Code hooks it registers either call into that running `ccc` over
355
+ localhost or run files in its own temporary plugin directory, never the installed
356
+ package. The one exception is the `memtree` MCP server, which Claude Code starts from the
357
+ installed files: a server started after the update (for example, Claude Code restarting
358
+ it with `/mcp`) runs the new version against the old session. It keeps working, and if a
359
+ tool call fails it says that the versions differ and to restart `ccc`. If Claude Code
360
+ starts it in the few seconds while npm is writing the files, it can fail to start; `/mcp`
361
+ reconnects it.
362
+
363
+ **Turning it off.** Any of these keeps the update line but never installs:
364
+
365
+ - `CCC_DISABLE_AUTOUPDATE=1`,
366
+ - `"autoUpdate": false` in `~/.claude-code-infinite/config.json`,
367
+ - Claude Code's own `DISABLE_AUTOUPDATER=1`.
368
+
369
+ The update-check opt-outs above turn off both the check and automatic updates.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The detached background installer for auto-update.ts, as source text that
3
+ * ccc runs with `node -e RUNNER_SOURCE <config json>`.
4
+ *
5
+ * Self-contained on purpose: it requires only Node built-ins and reads no file
6
+ * from the ccc package, because the npm install it runs replaces that package.
7
+ * It outlives the ccc that started it (detached, own process group), records
8
+ * the result in update-state.json and releases the lock by its token.
9
+ *
10
+ * Ownership: the launcher created this runner's lock entry and records the
11
+ * runner's pid in it right after spawning; the runner waits for that handoff,
12
+ * exits without touching anything if the entry is gone, and re-checks the
13
+ * entry before every npm command. Once stopping (abort or lost lock) it never
14
+ * starts another command.
15
+ *
16
+ * Steps: re-validate the version; confirm `npm root -g` is the directory this
17
+ * ccc lives in and that the entry there is not a symlink (an `npm link`); run
18
+ * `npm install --global --prefix=<that prefix> claude-code-infinite@<version>
19
+ * --no-fund --no-audit` (argv, never a shell); confirm the package.json now
20
+ * says that version. Any failure is recorded with a short reason.
21
+ *
22
+ * Bounded: each npm command runs in its own process group, recorded in the
23
+ * lock entry. When it ends (or times out) the whole group is reaped: SIGTERM,
24
+ * then SIGKILL, until no member is left, so a descendant that outlives npm
25
+ * cannot go on writing the package. The lock is released only once the group
26
+ * is confirmed gone; otherwise the entry stays live for as long as the group
27
+ * is (update-lock.ts). A hard deadline and SIGTERM/SIGINT/SIGHUP to the runner
28
+ * kill the group, record a failure and exit, so the lock's stale age is never
29
+ * reached by a live runner.
30
+ */
31
+ /** What ccc passes the runner, as JSON in argv[1]. */
32
+ export interface RunnerConfig {
33
+ npm: string;
34
+ packageName: string;
35
+ version: string;
36
+ packageDir: string;
37
+ npmRoot: string;
38
+ prefix: string;
39
+ stateFile: string;
40
+ /** update-lock.ts directory; this runner owns `<lockDir>/<token>.json`. */
41
+ lockDir: string;
42
+ logFile: string;
43
+ token: string;
44
+ timeoutMs: number;
45
+ /** Test-only: how long to wait for the launcher's handoff (default 10 s). */
46
+ handoffMs?: number;
47
+ /** Test-only: SIGTERM→SIGKILL and SIGKILL→give-up waits (default 10 s, 5 s). */
48
+ killGraceMs?: number;
49
+ pipeGraceMs?: number;
50
+ }
51
+ export declare const RUNNER_SOURCE: string;
52
+ //# sourceMappingURL=auto-update-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-update-runner.d.ts","sourceRoot":"","sources":["../src/auto-update-runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,sDAAsD;AACtD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,aAAa,QAmRzB,CAAC"}
@@ -0,0 +1,307 @@
1
+ /**
2
+ * The detached background installer for auto-update.ts, as source text that
3
+ * ccc runs with `node -e RUNNER_SOURCE <config json>`.
4
+ *
5
+ * Self-contained on purpose: it requires only Node built-ins and reads no file
6
+ * from the ccc package, because the npm install it runs replaces that package.
7
+ * It outlives the ccc that started it (detached, own process group), records
8
+ * the result in update-state.json and releases the lock by its token.
9
+ *
10
+ * Ownership: the launcher created this runner's lock entry and records the
11
+ * runner's pid in it right after spawning; the runner waits for that handoff,
12
+ * exits without touching anything if the entry is gone, and re-checks the
13
+ * entry before every npm command. Once stopping (abort or lost lock) it never
14
+ * starts another command.
15
+ *
16
+ * Steps: re-validate the version; confirm `npm root -g` is the directory this
17
+ * ccc lives in and that the entry there is not a symlink (an `npm link`); run
18
+ * `npm install --global --prefix=<that prefix> claude-code-infinite@<version>
19
+ * --no-fund --no-audit` (argv, never a shell); confirm the package.json now
20
+ * says that version. Any failure is recorded with a short reason.
21
+ *
22
+ * Bounded: each npm command runs in its own process group, recorded in the
23
+ * lock entry. When it ends (or times out) the whole group is reaped: SIGTERM,
24
+ * then SIGKILL, until no member is left, so a descendant that outlives npm
25
+ * cannot go on writing the package. The lock is released only once the group
26
+ * is confirmed gone; otherwise the entry stays live for as long as the group
27
+ * is (update-lock.ts). A hard deadline and SIGTERM/SIGINT/SIGHUP to the runner
28
+ * kill the group, record a failure and exit, so the lock's stale age is never
29
+ * reached by a live runner.
30
+ */
31
+ export const RUNNER_SOURCE = String.raw `"use strict";
32
+ const { spawn } = require("node:child_process");
33
+ const fs = require("node:fs");
34
+ const os = require("node:os");
35
+ const path = require("node:path");
36
+
37
+ const VERSION_RE = /^(0|[1-9]\d{0,8})\.(0|[1-9]\d{0,8})\.(0|[1-9]\d{0,8})$/;
38
+ const ROOT_TIMEOUT_MS = 60000;
39
+ const cfg = JSON.parse(process.argv[1]);
40
+ const KILL_GRACE_MS = cfg.killGraceMs || 10000;
41
+ const PIPE_GRACE_MS = cfg.pipeGraceMs || 5000;
42
+ const ENTRY = path.join(cfg.lockDir, cfg.token + ".json");
43
+ const HANDOFF_MS = cfg.handoffMs || 10000;
44
+ let logFd = null;
45
+ let group = null;
46
+ let done = false;
47
+ // Set first thing on abort or a lost lock: no command starts after it.
48
+ let stopping = null;
49
+
50
+ // Hard deadline: whatever npm does, this process kills its group, records a
51
+ // result and exits, so a live holder never outlasts the lock's stale age.
52
+ const DEADLINE_MS = ROOT_TIMEOUT_MS + cfg.timeoutMs + 2 * (2 * KILL_GRACE_MS + 2 * PIPE_GRACE_MS) + 30000;
53
+ setTimeout(() => abort("runner deadline passed"), DEADLINE_MS);
54
+ for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) {
55
+ process.on(signal, () => abort("runner stopped by " + signal));
56
+ }
57
+
58
+ function note(line) {
59
+ try {
60
+ if (logFd !== null) fs.writeSync(logFd, "[" + new Date().toISOString() + "] " + line + "\n");
61
+ } catch {}
62
+ }
63
+
64
+ function sleep(ms) {
65
+ return new Promise((resolve) => setTimeout(resolve, ms));
66
+ }
67
+
68
+ function groupAlive(pgid) {
69
+ try {
70
+ process.kill(-pgid, 0);
71
+ return true;
72
+ } catch (err) {
73
+ return err.code === "EPERM";
74
+ }
75
+ }
76
+
77
+ function signalGroup(pgid, signal) {
78
+ try { process.kill(-pgid, signal); } catch {}
79
+ }
80
+
81
+ async function goneWithin(pgid, ms) {
82
+ const until = Date.now() + ms;
83
+ while (groupAlive(pgid)) {
84
+ if (Date.now() >= until) return false;
85
+ await sleep(100);
86
+ }
87
+ return true;
88
+ }
89
+
90
+ // Every member of the command's group, not only npm: SIGTERM, then SIGKILL.
91
+ async function reapGroup(pgid) {
92
+ if (!groupAlive(pgid)) return true;
93
+ signalGroup(pgid, "SIGTERM");
94
+ if (await goneWithin(pgid, KILL_GRACE_MS)) return true;
95
+ signalGroup(pgid, "SIGKILL");
96
+ return goneWithin(pgid, PIPE_GRACE_MS);
97
+ }
98
+
99
+ // The lock entry names the group while it may be alive (update-lock.ts).
100
+ // "spawning" is set before every npm spawn and cleared only once its group is
101
+ // recorded: if this runner dies in between, the entry stays live (fail
102
+ // closed) because an unrecorded npm may be running (update-lock.ts).
103
+ function recordGroup(pgid, spawning) {
104
+ const entry = readEntry();
105
+ if (!entry) {
106
+ stopping = stopping || "lost the install lock";
107
+ if (pgid !== null) signalGroup(pgid, "SIGKILL"); // started without the lock: stop it
108
+ return false;
109
+ }
110
+ try {
111
+ entry.pid = process.pid;
112
+ entry.runner = process.pid;
113
+ if (pgid === null) delete entry.group; else entry.group = pgid;
114
+ if (spawning) entry.spawning = true; else delete entry.spawning;
115
+ const tmp = path.join(cfg.lockDir, "." + cfg.token + "." + process.pid + ".tmp");
116
+ fs.writeFileSync(tmp, JSON.stringify(entry), { mode: 0o600 });
117
+ fs.renameSync(tmp, ENTRY);
118
+ return true;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ function readEntry() {
125
+ try {
126
+ const entry = JSON.parse(fs.readFileSync(ENTRY, "utf8"));
127
+ return entry && entry.token === cfg.token ? entry : null;
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ // The launcher records this runner's pid in the entry right after spawning
134
+ // it; until then the entry is not provably ours. Gone means another session
135
+ // took the lock: leave everything to it.
136
+ async function awaitHandoff() {
137
+ const until = Date.now() + HANDOFF_MS;
138
+ for (;;) {
139
+ const entry = readEntry();
140
+ if (!entry) return "lost";
141
+ if (entry.runner === process.pid) return "ok";
142
+ if (Date.now() >= until) return "timeout";
143
+ await sleep(50);
144
+ }
145
+ }
146
+
147
+ // Resolves when the command is over: it closed, it exited (its pipes are not
148
+ // waited on for long), or it timed out. The caller then reaps its group.
149
+ function run(command, args, timeoutMs, capture) {
150
+ return new Promise((resolve) => {
151
+ let out = "";
152
+ let settled = false;
153
+ let child;
154
+ const timers = [];
155
+ const finish = (result) => {
156
+ if (settled) return;
157
+ settled = true;
158
+ timers.forEach(clearTimeout);
159
+ resolve(Object.assign({ out, pgid: child ? child.pid : null }, result));
160
+ };
161
+ if (stopping || !recordGroup(null, true)) {
162
+ finish({ code: null, error: stopping || "lost the install lock" });
163
+ return;
164
+ }
165
+ try {
166
+ child = spawn(command, args, {
167
+ cwd: os.homedir(),
168
+ env: process.env,
169
+ stdio: ["ignore", "pipe", "pipe"],
170
+ detached: true,
171
+ });
172
+ } catch (err) {
173
+ recordGroup(null, false);
174
+ finish({ code: null, error: String(err) });
175
+ return;
176
+ }
177
+ if (child.pid) {
178
+ group = child.pid;
179
+ recordGroup(group, false);
180
+ } else {
181
+ recordGroup(null, false); // spawn failed; its 'error' event follows
182
+ }
183
+ timers.push(setTimeout(() => finish({ code: null, timedOut: true }), timeoutMs));
184
+ const logChunk = (chunk) => {
185
+ try { if (logFd !== null) fs.writeSync(logFd, chunk); } catch {}
186
+ };
187
+ child.stdout.on("data", (chunk) => {
188
+ if (capture && out.length < 65536) out += chunk.toString("utf8");
189
+ logChunk(chunk);
190
+ });
191
+ child.stderr.on("data", logChunk);
192
+ child.on("error", (err) => finish({ code: null, error: String(err) }));
193
+ child.on("exit", (code, signal) => {
194
+ timers.push(setTimeout(() => finish({ code, signal }), PIPE_GRACE_MS));
195
+ });
196
+ child.on("close", (code, signal) => finish({ code, signal }));
197
+ });
198
+ }
199
+
200
+ // Run a command and make sure nothing it started is left running.
201
+ async function runReaped(command, args, timeoutMs, capture) {
202
+ const result = await run(command, args, timeoutMs, capture);
203
+ if (result.pgid) {
204
+ if (!(await reapGroup(result.pgid))) {
205
+ const err = new Error("could not stop npm's processes (group " + result.pgid + ")");
206
+ err.groupAlive = true;
207
+ throw err;
208
+ }
209
+ group = null;
210
+ recordGroup(null, false);
211
+ }
212
+ return result;
213
+ }
214
+
215
+ function realpath(p) {
216
+ try { return fs.realpathSync(p); } catch { return null; }
217
+ }
218
+
219
+ function describe(result, what) {
220
+ if (result.error) return what + " could not run: " + result.error;
221
+ if (result.timedOut) return what + " timed out";
222
+ if (result.code !== 0) return what + " exited with " + (result.code === null ? result.signal : result.code);
223
+ return null;
224
+ }
225
+
226
+ async function install() {
227
+ if (!VERSION_RE.test(cfg.version)) throw new Error("refusing version " + JSON.stringify(cfg.version));
228
+ const rootRun = await runReaped(cfg.npm, ["root", "--global"], ROOT_TIMEOUT_MS, true);
229
+ const rootFailure = describe(rootRun, "npm root -g");
230
+ if (rootFailure) throw new Error(rootFailure);
231
+ const npmRoot = realpath(rootRun.out.trim());
232
+ if (npmRoot === null || npmRoot !== realpath(cfg.npmRoot)) {
233
+ throw new Error("npm root -g is " + rootRun.out.trim() + ", not " + cfg.npmRoot + ": wrong prefix");
234
+ }
235
+ const entry = path.join(npmRoot, cfg.packageName);
236
+ if (fs.lstatSync(entry).isSymbolicLink()) throw new Error(entry + " is a symlink (npm link)");
237
+ if (realpath(entry) !== realpath(cfg.packageDir)) throw new Error(entry + " is not this ccc");
238
+ const spec = cfg.packageName + "@" + cfg.version;
239
+ const args = ["install", "--global", "--prefix=" + cfg.prefix, spec, "--no-fund", "--no-audit"];
240
+ note("npm " + args.join(" "));
241
+ const failure = describe(await runReaped(cfg.npm, args, cfg.timeoutMs, false), "npm install");
242
+ if (failure) throw new Error(failure);
243
+ const pkg = JSON.parse(fs.readFileSync(path.join(cfg.packageDir, "package.json"), "utf8"));
244
+ if (pkg.version !== cfg.version) throw new Error("installed version is " + pkg.version);
245
+ }
246
+
247
+ function record(status, error) {
248
+ let state = {};
249
+ try { state = JSON.parse(fs.readFileSync(cfg.stateFile, "utf8")) || {}; } catch {}
250
+ state.last = { version: cfg.version, status, at: Date.now(), packageDir: cfg.packageDir };
251
+ if (error) state.last.error = String(error).slice(0, 300);
252
+ const tmp = cfg.stateFile + "." + process.pid + ".tmp";
253
+ try {
254
+ fs.writeFileSync(tmp, JSON.stringify(state), { mode: 0o600 });
255
+ fs.renameSync(tmp, cfg.stateFile);
256
+ } catch {
257
+ try { fs.rmSync(tmp, { force: true }); } catch {}
258
+ }
259
+ }
260
+
261
+ // Record, release the lock unless npm's group may still be alive (its entry
262
+ // then stays live exactly as long as the group: update-lock.ts), and exit.
263
+ function finishRun(status, error) {
264
+ if (done) return;
265
+ done = true;
266
+ note(status === "ok" ? "installed " + cfg.version : "failed: " + error);
267
+ // Without the lock the state belongs to whoever holds it now.
268
+ if (readEntry()) record(status, error);
269
+ if (group === null || !groupAlive(group)) {
270
+ try { fs.rmSync(ENTRY, { force: true }); } catch {}
271
+ } else {
272
+ note("npm's processes are still exiting; the lock stays held until they are gone");
273
+ }
274
+ try { if (logFd !== null) fs.closeSync(logFd); } catch {}
275
+ process.exit(status === "ok" ? 0 : 1);
276
+ }
277
+
278
+ async function abort(reason) {
279
+ if (done || stopping) return;
280
+ stopping = reason;
281
+ if (group !== null) {
282
+ signalGroup(group, "SIGKILL");
283
+ await goneWithin(group, 2000);
284
+ }
285
+ finishRun("failed", reason);
286
+ }
287
+
288
+ (async () => {
289
+ const handoff = await awaitHandoff();
290
+ if (handoff === "lost") process.exit(1); // not ours: record nothing, touch nothing
291
+ if (handoff === "timeout") {
292
+ stopping = "the launcher never handed over the install lock";
293
+ finishRun("failed", stopping);
294
+ return;
295
+ }
296
+ recordGroup(null, false); // the lock's holder is this runner from now on
297
+ try { logFd = fs.openSync(cfg.logFile, "w", 0o600); } catch {}
298
+ note("installing " + cfg.packageName + "@" + cfg.version + " into " + cfg.prefix);
299
+ try {
300
+ await install();
301
+ finishRun("ok");
302
+ } catch (err) {
303
+ finishRun("failed", err && err.message ? err.message : String(err));
304
+ }
305
+ })();
306
+ `;
307
+ //# sourceMappingURL=auto-update-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-update-runner.js","sourceRoot":"","sources":["../src/auto-update-runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAuBH,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmRtC,CAAC"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Launch-time glue for auto-update.ts: decide what this interactive launch
3
+ * does about an available update, what the SessionStart banner says now, and
4
+ * when the background install starts (after Claude Code is up, never on the
5
+ * startup path).
6
+ */
7
+ import { AutoUpdateNotices, type AutoUpdateConfig, type AutoUpdateDecision, type InstallMethod, type SpawnLike, type StartInstallResult } from "./auto-update.js";
8
+ import type { UpdateAvailable } from "./update-check.js";
9
+ export interface AutoUpdateSetupOptions {
10
+ update: UpdateAvailable | null;
11
+ currentVersion: string;
12
+ config: AutoUpdateConfig;
13
+ configDir: string;
14
+ color: boolean;
15
+ env?: NodeJS.ProcessEnv;
16
+ packageDir?: string;
17
+ now?: () => number;
18
+ spawnImpl?: SpawnLike;
19
+ platform?: NodeJS.Platform;
20
+ uid?: number;
21
+ }
22
+ export interface AutoUpdateSetup {
23
+ decision: AutoUpdateDecision;
24
+ /** Detected only when there is an update to act on. */
25
+ method: InstallMethod | null;
26
+ /** The banner's hint line: shown unless an install is about to run. */
27
+ bannerUpdate: UpdateAvailable | null;
28
+ /** The banner's "✔ installed" line, when an earlier install already landed. */
29
+ bannerInstalled: string | null;
30
+ /** The SessionStart line for later hooks in this process (proxy option). */
31
+ notices: AutoUpdateNotices;
32
+ /** Start the planned install after `delayMs`; a no-op unless planned. */
33
+ scheduleInstall(delayMs?: number, onResult?: (result: StartInstallResult) => void): void;
34
+ /** One line naming the install method and the decision, for --debug. */
35
+ debugLine: string;
36
+ }
37
+ export declare function setupAutoUpdate(opts: AutoUpdateSetupOptions): AutoUpdateSetup;
38
+ //# sourceMappingURL=auto-update-setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-update-setup.d.ts","sourceRoot":"","sources":["../src/auto-update-setup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAEL,iBAAiB,EASjB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,SAAS,EACd,KAAK,kBAAkB,EACxB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEzD,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,eAAe,GAAG,IAAI,CAAC;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,gBAAgB,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,uDAAuD;IACvD,MAAM,EAAE,aAAa,GAAG,IAAI,CAAC;IAC7B,uEAAuE;IACvE,YAAY,EAAE,eAAe,GAAG,IAAI,CAAC;IACrC,+EAA+E;IAC/E,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,4EAA4E;IAC5E,OAAO,EAAE,iBAAiB,CAAC;IAC3B,yEAAyE;IACzE,eAAe,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,kBAAkB,KAAK,IAAI,GAAG,IAAI,CAAC;IACzF,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,eAAe,CA4C7E"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Launch-time glue for auto-update.ts: decide what this interactive launch
3
+ * does about an available update, what the SessionStart banner says now, and
4
+ * when the background install starts (after Claude Code is up, never on the
5
+ * startup path).
6
+ */
7
+ import { spawn } from "node:child_process";
8
+ import { AUTO_UPDATE_DELAY_MS, AutoUpdateNotices, PACKAGE_DIR, detectInstallMethod, liveLockSince, planAutoUpdate, readInstalledVersion, readUpdateState, startAutoUpdateInstall, updateFiles, } from "./auto-update.js";
9
+ import { updateInstalledNoticeText, updateNoticeText } from "./notices.js";
10
+ export function setupAutoUpdate(opts) {
11
+ const env = opts.env ?? process.env;
12
+ const packageDir = opts.packageDir ?? PACKAGE_DIR;
13
+ const now = opts.now ?? Date.now;
14
+ const files = updateFiles(opts.configDir);
15
+ const method = opts.update ? detectMethod(opts, env, packageDir) : null;
16
+ const decision = method
17
+ ? planAutoUpdate({
18
+ update: opts.update,
19
+ method,
20
+ state: readUpdateState(files.stateFile),
21
+ env,
22
+ config: opts.config,
23
+ now: now(),
24
+ installedVersion: readInstalledVersion(method.packageDir),
25
+ lockHeldSince: liveLockSince(files.lockDir, now()),
26
+ })
27
+ : { action: "skip", reason: "no update available" };
28
+ const notices = new AutoUpdateNotices({
29
+ currentVersion: opts.currentVersion,
30
+ packageDir: method?.packageDir ?? packageDir,
31
+ stateFile: files.stateFile,
32
+ startedAt: now(),
33
+ installedText: (version) => updateInstalledNoticeText(version, opts.color),
34
+ hintText: (update) => updateNoticeText(update, opts.color),
35
+ });
36
+ if (decision.action === "installed")
37
+ notices.markShown();
38
+ return {
39
+ decision,
40
+ method,
41
+ bannerUpdate: decision.action === "skip" ? opts.update : null,
42
+ bannerInstalled: decision.action === "installed" ? decision.version : null,
43
+ notices,
44
+ scheduleInstall(delayMs = AUTO_UPDATE_DELAY_MS, onResult) {
45
+ if (decision.action !== "install")
46
+ return;
47
+ const spawnImpl = opts.spawnImpl ?? spawn;
48
+ const timer = setTimeout(() => {
49
+ const result = startAutoUpdateInstall({ decision, files, spawnImpl, env, now });
50
+ onResult?.(result);
51
+ }, delayMs);
52
+ timer.unref();
53
+ },
54
+ debugLine: debugLine(method, decision),
55
+ };
56
+ }
57
+ function detectMethod(opts, env, packageDir) {
58
+ return detectInstallMethod({
59
+ packageDir,
60
+ env,
61
+ platform: opts.platform,
62
+ uid: opts.uid ?? process.getuid?.(),
63
+ });
64
+ }
65
+ function debugLine(method, decision) {
66
+ const how = method ? `install method ${method.kind} (${method.detail})` : "no update to install";
67
+ const what = decision.action === "install" ? `installing ${decision.version} in the background`
68
+ : decision.action === "installed" ? `${decision.version} already installed, restart to use it`
69
+ : `not installing: ${decision.reason}`;
70
+ return `Auto-update: ${how}; ${what}`;
71
+ }
72
+ //# sourceMappingURL=auto-update-setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-update-setup.js","sourceRoot":"","sources":["../src/auto-update-setup.ts"],"names":[],"mappings":"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"}