claude-code-infinite 1.0.20 → 1.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +165 -30
- package/dist/auto-update-runner.d.ts +52 -0
- package/dist/auto-update-runner.d.ts.map +1 -0
- package/dist/auto-update-runner.js +307 -0
- package/dist/auto-update-runner.js.map +1 -0
- package/dist/auto-update-setup.d.ts +38 -0
- package/dist/auto-update-setup.d.ts.map +1 -0
- package/dist/auto-update-setup.js +72 -0
- package/dist/auto-update-setup.js.map +1 -0
- package/dist/auto-update.d.ts +170 -0
- package/dist/auto-update.d.ts.map +1 -0
- package/dist/auto-update.js +401 -0
- package/dist/auto-update.js.map +1 -0
- package/dist/claude-env.d.ts +14 -1
- package/dist/claude-env.d.ts.map +1 -1
- package/dist/claude-env.js +17 -1
- package/dist/claude-env.js.map +1 -1
- package/dist/cli-lifecycle.d.ts +4 -0
- package/dist/cli-lifecycle.d.ts.map +1 -1
- package/dist/cli-lifecycle.js +18 -0
- package/dist/cli-lifecycle.js.map +1 -1
- package/dist/cli-main.js +139 -19
- package/dist/cli-main.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/count-tokens.d.ts +38 -3
- package/dist/count-tokens.d.ts.map +1 -1
- package/dist/count-tokens.js +94 -19
- package/dist/count-tokens.js.map +1 -1
- package/dist/diagnostics-schema.d.ts +44 -0
- package/dist/diagnostics-schema.d.ts.map +1 -0
- package/dist/diagnostics-schema.js +162 -0
- package/dist/diagnostics-schema.js.map +1 -0
- package/dist/diagnostics.d.ts +116 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +316 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/first-tree.d.ts +11 -0
- package/dist/first-tree.d.ts.map +1 -0
- package/dist/first-tree.js +45 -0
- package/dist/first-tree.js.map +1 -0
- package/dist/hooks.d.ts +37 -0
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +69 -9
- package/dist/hooks.js.map +1 -1
- package/dist/memtree-finder.d.ts +11 -0
- package/dist/memtree-finder.d.ts.map +1 -1
- package/dist/memtree-finder.js +18 -0
- package/dist/memtree-finder.js.map +1 -1
- package/dist/memtree-links.d.ts +2 -0
- package/dist/memtree-links.d.ts.map +1 -1
- package/dist/memtree-links.js +4 -0
- package/dist/memtree-links.js.map +1 -1
- package/dist/memtree-mcp-config.d.ts.map +1 -1
- package/dist/memtree-mcp-config.js +4 -2
- package/dist/memtree-mcp-config.js.map +1 -1
- package/dist/memtree-mcp.d.ts +24 -4
- package/dist/memtree-mcp.d.ts.map +1 -1
- package/dist/memtree-mcp.js +47 -14
- package/dist/memtree-mcp.js.map +1 -1
- package/dist/memtree-tools.d.ts +17 -1
- package/dist/memtree-tools.d.ts.map +1 -1
- package/dist/memtree-tools.js +50 -2
- package/dist/memtree-tools.js.map +1 -1
- package/dist/memtree.d.ts +17 -1
- package/dist/memtree.d.ts.map +1 -1
- package/dist/memtree.js +62 -19
- package/dist/memtree.js.map +1 -1
- package/dist/notices.d.ts +6 -1
- package/dist/notices.d.ts.map +1 -1
- package/dist/notices.js +13 -1
- package/dist/notices.js.map +1 -1
- package/dist/prefix-store.js +3 -0
- package/dist/prefix-store.js.map +1 -1
- package/dist/proxy.d.ts +45 -3
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +193 -54
- package/dist/proxy.js.map +1 -1
- package/dist/reqlog.d.ts +27 -1
- package/dist/reqlog.d.ts.map +1 -1
- package/dist/reqlog.js.map +1 -1
- package/dist/request-sizing.d.ts +21 -2
- package/dist/request-sizing.d.ts.map +1 -1
- package/dist/request-sizing.js +60 -7
- package/dist/request-sizing.js.map +1 -1
- package/dist/resume-hint.d.ts +25 -0
- package/dist/resume-hint.d.ts.map +1 -0
- package/dist/resume-hint.js +36 -0
- package/dist/resume-hint.js.map +1 -0
- package/dist/token-estimate.d.ts +35 -0
- package/dist/token-estimate.d.ts.map +1 -0
- package/dist/token-estimate.js +97 -0
- package/dist/token-estimate.js.map +1 -0
- package/dist/update-check.d.ts +4 -3
- package/dist/update-check.d.ts.map +1 -1
- package/dist/update-check.js +4 -3
- package/dist/update-check.js.map +1 -1
- package/dist/update-lock.d.ts +68 -0
- package/dist/update-lock.d.ts.map +1 -0
- package/dist/update-lock.js +176 -0
- package/dist/update-lock.js.map +1 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -17,10 +17,27 @@ follows it down to the original messages.
|
|
|
17
17
|
* **Search across every session.** Claude can find what was decided, tried or ruled out in any of
|
|
18
18
|
your past sessions, by exact words or by meaning, filtered by project and date
|
|
19
19
|
* **Browse each session's MemTree** in your browser, from the link `ccc` prints in the terminal
|
|
20
|
-
* **
|
|
21
|
-
full
|
|
22
|
-
([details](#long-session-benchmark))
|
|
20
|
+
* **Never loses anything vs Claude Code.** Sessions are sent word for word until near the context
|
|
21
|
+
limit; past it, MemTree matched full context (69 vs 69) and beat `/compact` by 20 ([benchmark](#long-session-benchmark))
|
|
23
22
|
* **Unlimited-length sessions**, with relevant past information recalled automatically
|
|
23
|
+
* **A local proxy, so no slowdown.** Requests go straight from your machine to Anthropic and
|
|
24
|
+
MemTree indexes in the background; only the occasional compression step, when a session
|
|
25
|
+
outgrows the context budget, waits on MemTree
|
|
26
|
+
|
|
27
|
+
The magic starts past the context limit. Keep one session going for days and it learns how you
|
|
28
|
+
work: your priorities, the nuances of your projects, your company and your environment. Where
|
|
29
|
+
`/compact` would wipe that detail, MemTree keeps it, so the thread becomes an expert in you. Let it
|
|
30
|
+
orchestrate dozens of subagents on your behalf.
|
|
31
|
+
|
|
32
|
+
> [!IMPORTANT]
|
|
33
|
+
> Requires a Claude Pro or Max subscription.
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<picture>
|
|
37
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/crizCraig/claude-code-infinite/main/docs/images/benchmark-dark.svg">
|
|
38
|
+
<img width="600" alt="Long-session benchmark, judged score out of 100: Claude Code Infinite 69, full context 69, /compact 49" src="https://raw.githubusercontent.com/crizCraig/claude-code-infinite/main/docs/images/benchmark-light.svg">
|
|
39
|
+
</picture>
|
|
40
|
+
</p>
|
|
24
41
|
|
|
25
42
|
## Search across sessions
|
|
26
43
|
|
|
@@ -67,14 +84,11 @@ tokens; `/compact`'s exclude the one-time `/compact` step. This is one task fami
|
|
|
67
84
|
|
|
68
85
|
* [node.js 20.3 or newer](https://nodejs.org/en/download/)
|
|
69
86
|
* [Claude Code (the terminal version)](https://code.claude.com/docs/en/quickstart)
|
|
70
|
-
* **Claude
|
|
87
|
+
* **A Claude Pro or Max subscription**
|
|
71
88
|
|
|
72
|
-
|
|
89
|
+
<sub>Also works with an Anthropic API key, but that's far costlier than Pro or Max.</sub>
|
|
73
90
|
|
|
74
|
-
|
|
75
|
-
> No Claude subscription? Use an Anthropic API key exactly as with vanilla Claude Code
|
|
76
|
-
> (`ANTHROPIC_API_KEY` or a Console login). `ccc` forwards it unchanged to api.anthropic.com, and
|
|
77
|
-
> you pay Anthropic per token.
|
|
91
|
+
## Setup
|
|
78
92
|
|
|
79
93
|
1. Install (Node ≥ 20.3; no git needed)
|
|
80
94
|
```bash
|
|
@@ -96,6 +110,32 @@ The tool supports multiple environments (this selects the MemTree compression AP
|
|
|
96
110
|
|
|
97
111
|
Each environment maintains its own separate API key.
|
|
98
112
|
|
|
113
|
+
## Commands
|
|
114
|
+
|
|
115
|
+
Type these inside a `ccc` session (the `ccc:` prefix is optional). `ccc` answers them
|
|
116
|
+
locally, so they cost no model turn.
|
|
117
|
+
|
|
118
|
+
| Command | What it does |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| `/ccc:memtree` | Lists the MemTree commands and links your usage dashboard. |
|
|
121
|
+
| `/ccc:memtree-view` | Shows the link to this session's MemTree page. |
|
|
122
|
+
| `/ccc:memtree-compact [tokens]` | Compacts the session on your next message (to half the budget, or a target such as `400k`) and keeps the compressed history. |
|
|
123
|
+
| `/ccc:memtree-compact off` | Goes back to sending the conversation whole. |
|
|
124
|
+
|
|
125
|
+
Environment variables, set when launching `ccc`:
|
|
126
|
+
|
|
127
|
+
| Variable | What it does |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `CCC_COMPACT_TARGET=500k` | Compaction target for every session (useful for headless `-p` runs); `off` starts every session with compaction off. |
|
|
130
|
+
| `CCC_MEMTREE_LINK` | Where the MemTree link appears: `turn` (default, end of a turn with a new page), `message` (under every reply), `stop` (end of every turn), `success` (only after an optimized turn), or `off`. |
|
|
131
|
+
| `CCC_AUTO_COMPACT=1` | Restores Claude Code's own auto-compaction setting ([details](#claude-code-auto-compaction)). |
|
|
132
|
+
| `CCC_NO_UPDATE_CHECK=1` | Turns off the update check, and with it automatic updates ([details](#upgrading)). |
|
|
133
|
+
| `CCC_DISABLE_AUTOUPDATE=1` | Keeps the update notice but never installs ([details](#automatic-updates)). |
|
|
134
|
+
| `CCC_NO_DIAGNOSTICS=1` | Turns off diagnostics ([details](#diagnostics)). So do `DO_NOT_TRACK=1`, Claude Code's `DISABLE_TELEMETRY=1` or `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1`, and `"diagnostics": false` in `~/.claude-code-infinite/config.json`. |
|
|
135
|
+
|
|
136
|
+
Links are clickable in terminals that support OSC 8 hyperlinks (iTerm2, Ghostty, WezTerm,
|
|
137
|
+
kitty, VS Code, JetBrains, Windows Terminal); elsewhere you get the plain URL.
|
|
138
|
+
|
|
99
139
|
## Claude Code auto-compaction
|
|
100
140
|
|
|
101
141
|
`ccc` disables Claude Code's automatic conversation compaction for the Claude process it launches. MemTree manages the context sent to the model, so letting Claude Code independently summarize the full local transcript can compact a conversation MemTree already reduced. Manual `/compact` remains available.
|
|
@@ -105,6 +145,13 @@ above a fixed 100k-token threshold, even when the active model has a 1M window
|
|
|
105
145
|
and auto-compaction is disabled. `ccc` suppresses that resume-time
|
|
106
146
|
recommendation as well; it does not disable the manual `/compact` command.
|
|
107
147
|
|
|
148
|
+
With auto-compaction off, Claude Code still refuses a request itself, without
|
|
149
|
+
sending it, once its own estimate reaches the window minus 23k (977k on a 1M
|
|
150
|
+
window). MemTree compresses at 900k, so a single large tool round could reach that
|
|
151
|
+
refusal before `ccc` ever saw the request. `ccc` sets
|
|
152
|
+
`CLAUDE_CODE_BLOCKING_LIMIT_OVERRIDE` (unless you exported your own) so such a
|
|
153
|
+
request reaches the proxy, which compresses it mid-turn.
|
|
154
|
+
|
|
108
155
|
A session whose last response reached the verified local refusal threshold
|
|
109
156
|
(for example one sent whole while MemTree was unavailable) would otherwise be
|
|
110
157
|
refused locally with "Context limit reached" on every prompt after a resume.
|
|
@@ -141,24 +188,27 @@ Claude Code ──▶ localhost proxy (ccc)
|
|
|
141
188
|
- Anthropic requests go directly from the local proxy to api.anthropic.com using the authentication Claude Code supplied. PolyChat never sees that credential or Anthropic traffic.
|
|
142
189
|
- If MemTree is unreachable, slow, or your MemTree plan needs payment, `ccc` degrades to a transparent passthrough so your session is never interrupted.
|
|
143
190
|
|
|
144
|
-
###
|
|
145
|
-
|
|
146
|
-
In interactive sessions, `ccc` reports these MemTree states as display-only lines in Claude Code:
|
|
191
|
+
### Diagnostics
|
|
147
192
|
|
|
148
|
-
|
|
149
|
-
- `⚠ MemTree degraded — this turn ran uncompressed` when a blocking compression call fails or times out.
|
|
150
|
-
- `⚠ MemTree is off — payment required…` once when compression and indexing are disabled for payment.
|
|
151
|
-
- `• MemTree` with the page on its own line below it, once at the end of a user turn, when the turn used a MemTree index whose link has not been shown yet (the first one of the session, then each newly finished index), unless the turn's success line already showed it. The line comes from the `Stop` hook, so it lands after the turn's last message. When MemTree passed the conversation through whole because it already fits the model's budget, the label line reads `• MemTree · /memtree-compact to compact session`. The URL always gets a line to itself, so a long link does not wrap the label or note. The displayed link follows the server's newest completed tree for this session, preserving its pinned namespace and own/served selection. If that lookup is unavailable, it falls back to the compression page; that page can show a completed prefix while its own tree builds. Open it in a browser, read it as JSON with `ccc fetch <url>`, or, from an agent inside the session, `GET $ANTHROPIC_BASE_URL/memtree/<short id>.json` on the loopback proxy without handling the key. Every MemTree call carries Claude Code's session id, so `GET $ANTHROPIC_BASE_URL/memtree/sessions/<session id>.json` lists every page from one session, newest first (the session id is the `--resume` id and the name of the transcript under `~/.claude/projects/`). `/memtree` lists the MemTree commands. Type `/memtree-view` at any time to print the session's latest link. Both also end with a `• Usage` link to your usage dashboard on polychat (`/usage` on the same host as MemTree: `https://app.polychat.co/usage` in production, the staging or local host otherwise); it opens only with your browser signed in to polychat as the same account. `/memtree-compact` is MemTree's replacement for Claude Code's `/compact`: your next message is sent compressed to half the budget (or `/memtree-compact 400k` for another target), and from then on the session follows the compaction rules under [How it works](#how-it-works) with that target. `/memtree-compact off` goes back to sending the conversation whole. `CCC_COMPACT_TARGET=500k` sets the target of every compaction for every session from launch, for benchmarks and headless (`-p`) runs, where the slash command is unavailable; it does not compact by itself, the budget still decides when, and `/memtree-compact off` still turns compaction off for one session. `CCC_COMPACT_TARGET=off` starts every session in the `/memtree-compact off` state instead (`/memtree-compact [N]` still turns it back on). `ccc` answers these commands locally, so they cost no model turn and nothing is added to the conversation; the compaction setting lasts for the life of the `ccc` process. When a session starts and already has a page, which in practice means a resumed one (`ccc --resume`, or `/resume` inside a running session), its latest link is shown right away: `ccc` remembers the newest page per session in `~/.claude-code-infinite/memtree-links.json` (most recent 200 sessions) and a `SessionStart` hook prints it. `CCC_MEMTREE_LINK` changes where the link appears: `turn` (default), `message` (under every finished assistant message, green when the index is new, dim otherwise), `stop` (every turn, on `Stop`), `success` (only on the line below `✓ MemTree · conversation optimized`), or `off`.
|
|
193
|
+
`ccc` sends a few diagnostics to MemTree so we can see why a session fails to start or stalls:
|
|
152
194
|
|
|
153
|
-
|
|
195
|
+
- versions (ccc, Claude Code, Node), how ccc was installed (npm, npx, linked), OS and CPU
|
|
196
|
+
architecture, terminal type and locale;
|
|
197
|
+
- lifecycle events (first run, sign-in prompt shown, key accepted or rejected, session start,
|
|
198
|
+
the MemTree-paused notice shown, update hint or background update result);
|
|
199
|
+
- errors as codes, status numbers, sizes and timings (an Anthropic 401, a MemTree 402, a
|
|
200
|
+
compression timeout).
|
|
154
201
|
|
|
155
|
-
|
|
202
|
+
Never sent: your prompts or any conversation content, file paths, project or repository names,
|
|
203
|
+
hostnames, usernames, environment values, keys or tokens, or error message text. Each event is
|
|
204
|
+
checked against a fixed allowlist on your machine before it is queued, and again by the server.
|
|
205
|
+
They are tied to your MemTree key when you have one, otherwise to a random install id in
|
|
206
|
+
`~/.claude-code-infinite/install_id`. Sending is batched in the background and never slows a
|
|
207
|
+
request; a failed send is dropped.
|
|
156
208
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
`
|
|
160
|
-
|
|
161
|
-
Claude Code currently displays the original assistant text instead of `MessageDisplay` replacements while verbose mode is enabled. Turn verbose mode off to see the inline MemTree line.
|
|
209
|
+
Turn diagnostics off with any of: `CCC_NO_DIAGNOSTICS=1`, `"diagnostics": false` in
|
|
210
|
+
`~/.claude-code-infinite/config.json`, `DO_NOT_TRACK=1`, or Claude Code's own
|
|
211
|
+
`DISABLE_TELEMETRY=1` / `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1`.
|
|
162
212
|
|
|
163
213
|
### Memory tools
|
|
164
214
|
|
|
@@ -173,7 +223,7 @@ Interactive `ccc` sessions also get a small MCP server, `memtree`, with four rea
|
|
|
173
223
|
|
|
174
224
|
When you send a message, we retrieve relevant details and summaries from the prior messages in your thread. These details and summaries populate a **memory message**. Following the memory message, we append a compressed version of your recent message history. The resulting context-window is dramatically smaller, allowing Claude to process your request with much greater efficacy, lower latency, and reduced cost.
|
|
175
225
|
|
|
176
|
-
**Compaction keeps a stable prefix.** While a conversation fits the model's budget (
|
|
226
|
+
**Compaction keeps a stable prefix.** While a conversation fits the model's budget (900k tokens for Opus 5.5, where plain Claude Code would auto-compact at 967k), it is sent whole. When a request reaches the budget, `ccc` compresses it once, to half the budget (or your `/memtree-compact N`), and keeps those compressed bytes as the session's prefix. Every later request, human turns included, is that same prefix followed by the messages that came after it, verbatim, with no compression call (MemTree still indexes in the background). Because the prefix does not change, Anthropic reads it from the prompt cache on every turn after the one that compacted, instead of writing the whole compressed context again. `ccc` compresses again only when prefix plus newer turns reach the budget (measured from the size Anthropic reported for the previous request, plus an estimate for what was added), or when the messages the prefix stands for change (rewind, edit, fork, `/clear`). If that recompression fails, the turn is sent on the old prefix rather than the whole history. The prefix is also kept on disk, one file per session under `~/.claude-code-infinite/stable-prefixes/` (mode 600 in a private directory, at most 64 sessions and 256 MB, oldest dropped first), so the first turn after `ccc --resume` rides it instead of waiting on a fresh compression. A stored prefix belongs to the MemTree backend and key that wrote it (identified by a hash, never the key itself) and is never used with another. It is used only if the resumed history still matches what it replaced; a main-thread history that no longer matches discards it, while side requests (such as Claude Code's web-search helper) leave it alone. Only the main thread's prefix is stored, never a subagent's. The budget is the one MemTree reports for the model; until the server reports one, `ccc` uses 90% of the context window on Opus 5.5, Opus 5, Opus 4.8 and Fable 5 and 5.1 with a 1M window, and 80% otherwise. `CCC_BUDGET_TOKENS=60k` overrides it for testing. Every `requests.jsonl` line carries the `pid` of the `ccc` process that wrote it, which tells apart concurrent sessions sharing the log. The same rule holds inside a tool loop, so a long single turn (a headless `-p` run, or a long agentic loop) is compacted as it grows rather than only when you next type: each tool request under the budget goes out with no compression call, the one that reaches it is compressed once and becomes the stable prefix, and later tool requests and your next message ride that prefix until it reaches the budget again. `requests.jsonl` shows the `compaction` of each main-thread human turn and of every tool turn (budget, its source, the size estimate, and the reason for a compaction: `budget`, `prefix-mismatch`, `manual` or `target-change`); a human turn sent on the stable prefix is `turnType: "followup-prefix"`, a tool turn `tool-memory` (on its route) or `tool-prefix` (on the prefix directly), and a tool turn that compacted `tool-recompressed`.
|
|
177
227
|
|
|
178
228
|
That compressed context stays in force for the rest of the turn: the tool loop it kicks off, and the matching Count Tokens calls, are routed through the same compressed prefix. The Count Tokens part matters — Claude Code sizes its context from those replies, so counting the uncompressed history would make it auto-compact a conversation memory had already shrunk. A mismatched or resumed conversation shape drops the route rather than grafting one session's prefix onto another.
|
|
179
229
|
|
|
@@ -256,6 +306,30 @@ features, and their output, often the biggest source of tokens, is exactly what
|
|
|
256
306
|
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
307
|
|
|
258
308
|
|
|
309
|
+
## Releasing
|
|
310
|
+
|
|
311
|
+
Releases are staged by GitHub Actions through npm Trusted Publishing (OIDC), so no npm
|
|
312
|
+
token, passkey or browser is needed to build and upload them, and each one is approved by a
|
|
313
|
+
maintainer before it goes live:
|
|
314
|
+
|
|
315
|
+
1. On a clean `main`, run `./publish.sh` (or `./publish.sh minor` / `major`). It builds,
|
|
316
|
+
checks that `dist/` is committed, runs `npm version`, and pushes the commit and the
|
|
317
|
+
`vX.Y.Z` tag.
|
|
318
|
+
2. The tag push runs [`.github/workflows/publish.yml`](.github/workflows/publish.yml): `npm ci`,
|
|
319
|
+
the test suite with `NO_COLOR=1` and with `FORCE_COLOR=1`, a check that the rebuilt
|
|
320
|
+
`dist/` matches the commit and that the tag equals `package.json`'s version, then
|
|
321
|
+
`npm stage publish --provenance`.
|
|
322
|
+
3. Approve the staged version (2FA) on
|
|
323
|
+
[npmjs.com](https://www.npmjs.com/package/claude-code-infinite) under the **Staged
|
|
324
|
+
Packages** tab, or with `npm stage list claude-code-infinite` and
|
|
325
|
+
`npm stage approve <stage-id>`. Until then it is not installable.
|
|
326
|
+
|
|
327
|
+
The trusted publisher on npmjs.com is bound to the workflow file name `publish.yml` with no
|
|
328
|
+
GitHub environment, and allows only staged publishing; renaming the workflow breaks it.
|
|
329
|
+
`./publish.sh --local` is the fallback: it publishes directly from your machine with npm web
|
|
330
|
+
auth, then pushes the commit and tag; the workflow still tests that tag but skips staging,
|
|
331
|
+
since the version is already published.
|
|
332
|
+
|
|
259
333
|
## Troubleshooting
|
|
260
334
|
|
|
261
335
|
### Anthropic auth errors (401s, login prompts)
|
|
@@ -269,11 +343,12 @@ If you see an inline "⚠ MemTree degraded — this turn ran uncompressed" notic
|
|
|
269
343
|
## Upgrading
|
|
270
344
|
|
|
271
345
|
`ccc` checks npm at startup (bounded to 1.5 seconds, silent if offline, and cached for a
|
|
272
|
-
day in `~/.claude-code-infinite/update-check.json`)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
is
|
|
346
|
+
day in `~/.claude-code-infinite/update-check.json`). Prereleases are never offered, and
|
|
347
|
+
print (`-p`) or non-TTY runs skip the check. When a newer release exists and `ccc` can
|
|
348
|
+
update itself, it does (see [Automatic updates](#automatic-updates)); otherwise it shows
|
|
349
|
+
one line under the MemTree banner inside Claude Code, e.g.
|
|
350
|
+
`• ccc 1.0.21 is available (you have 1.0.20) · npm i -g claude-code-infinite@latest`.
|
|
351
|
+
To upgrade by hand:
|
|
277
352
|
|
|
278
353
|
```bash
|
|
279
354
|
npm i -g claude-code-infinite@latest
|
|
@@ -281,4 +356,64 @@ npm i -g claude-code-infinite@latest
|
|
|
281
356
|
|
|
282
357
|
Set `CCC_NO_UPDATE_CHECK=1` (or the older `CCC_SKIP_UPDATE_CHECK=1`), or add
|
|
283
358
|
`"updateCheck": false` to `~/.claude-code-infinite/config.json`, to disable the check
|
|
284
|
-
(air-gapped or CI runs).
|
|
359
|
+
(air-gapped or CI runs). That also turns off automatic updates.
|
|
360
|
+
|
|
361
|
+
## Automatic updates
|
|
362
|
+
|
|
363
|
+
Like Claude Code, `ccc` installs a newer release for you in the background. A few
|
|
364
|
+
seconds after an interactive session starts, if the update check found a newer
|
|
365
|
+
release, `ccc` runs
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
npm install --global --prefix=<your npm prefix> claude-code-infinite@<that version> --no-fund --no-audit
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
in a detached process, so the session never waits for it and quitting `ccc` does not
|
|
372
|
+
stop it. When it has finished, the next session start inside Claude Code (a new
|
|
373
|
+
conversation, `/clear`, or a resume) shows, once:
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
✔ ccc 1.0.21 installed · restart ccc to use it
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
The session keeps running the version it started with until you restart `ccc`. If the
|
|
380
|
+
install fails, you get the usual `• ccc 1.0.21 is available … · npm i -g
|
|
381
|
+
claude-code-infinite@latest` line instead, and `ccc` waits a day before trying again; it
|
|
382
|
+
tries each version at most twice. The npm output of the last attempt is in
|
|
383
|
+
`~/.claude-code-infinite/auto-update.log`, and its result in `update-state.json` next to
|
|
384
|
+
it. Only one session installs at a time (`update.lock.d` there). If an installer was
|
|
385
|
+
killed in a way `ccc` cannot verify, that lock stays held rather than risk two installs
|
|
386
|
+
at once; after an hour `ccc` shows the update line instead, and deleting
|
|
387
|
+
`~/.claude-code-infinite/update.lock.d` clears it.
|
|
388
|
+
|
|
389
|
+
**When it doesn't.** `ccc` updates itself only when it is a plain global npm install
|
|
390
|
+
(`npm i -g`) whose files are in `npm root -g` and writable by you. It never uses `sudo`.
|
|
391
|
+
In every other case it only shows the update line:
|
|
392
|
+
|
|
393
|
+
- `npm link`ed or git checkouts (a development copy is never touched),
|
|
394
|
+
- `npx` and project-local installs,
|
|
395
|
+
- pnpm, yarn, bun and Volta global installs (update with your tool's own command),
|
|
396
|
+
- a global prefix that needs `sudo` to write, or `ccc` running as root,
|
|
397
|
+
- Windows,
|
|
398
|
+
- print (`-p`) and non-TTY runs, which never install anything.
|
|
399
|
+
|
|
400
|
+
`ccc --debug` prints which install method was detected and what it decided.
|
|
401
|
+
|
|
402
|
+
**Sessions running during an update.** npm replaces `ccc`'s files while other `ccc`
|
|
403
|
+
sessions may be running. They are unaffected: each `ccc` loads all of its code when it
|
|
404
|
+
starts, and the Claude Code hooks it registers either call into that running `ccc` over
|
|
405
|
+
localhost or run files in its own temporary plugin directory, never the installed
|
|
406
|
+
package. The one exception is the `memtree` MCP server, which Claude Code starts from the
|
|
407
|
+
installed files: a server started after the update (for example, Claude Code restarting
|
|
408
|
+
it with `/mcp`) runs the new version against the old session. It keeps working, and if a
|
|
409
|
+
tool call fails it says that the versions differ and to restart `ccc`. If Claude Code
|
|
410
|
+
starts it in the few seconds while npm is writing the files, it can fail to start; `/mcp`
|
|
411
|
+
reconnects it.
|
|
412
|
+
|
|
413
|
+
**Turning it off.** Any of these keeps the update line but never installs:
|
|
414
|
+
|
|
415
|
+
- `CCC_DISABLE_AUTOUPDATE=1`,
|
|
416
|
+
- `"autoUpdate": false` in `~/.claude-code-infinite/config.json`,
|
|
417
|
+
- Claude Code's own `DISABLE_AUTOUPDATER=1`.
|
|
418
|
+
|
|
419
|
+
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"}
|