@integrity-labs/agt-cli 0.28.1153 → 0.28.1154

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,718 @@
1
+ import {
2
+ CLAUDE_AGENT_PROCESS_PATTERN,
3
+ parsePsRows
4
+ } from "./chunk-XWVM4KPK.js";
5
+ import {
6
+ effectiveClaudeJsonPath
7
+ } from "./chunk-EL5MWGGH.js";
8
+ import {
9
+ FORCED_UPDATE_HARD_DEADLINE_MS
10
+ } from "./chunk-PQ6K7IGT.js";
11
+
12
+ // src/lib/claude-code-updater.ts
13
+ import { execFile, execFileSync as execFileSync2 } from "child_process";
14
+ import { existsSync, readFileSync as readFileSync2, realpathSync } from "fs";
15
+ import { homedir } from "os";
16
+ import { promisify } from "util";
17
+
18
+ // src/lib/disk-guard.ts
19
+ import { readdirSync, readFileSync, statSync, statfsSync } from "fs";
20
+ import { execFileSync } from "child_process";
21
+ var GIB = 1024 * 1024 * 1024;
22
+ var DISK_LOW_FREE_RATIO = 0.1;
23
+ var DISK_LOW_FREE_BYTES = 2 * GIB;
24
+ var DISK_LOW_FREE_CAP_BYTES = 5 * GIB;
25
+ function diskLowThresholdBytes(totalBytes) {
26
+ return Math.min(DISK_LOW_FREE_CAP_BYTES, Math.max(DISK_LOW_FREE_BYTES, totalBytes * DISK_LOW_FREE_RATIO));
27
+ }
28
+ var SELF_UPDATE_MIN_FREE_BYTES = 1 * GIB;
29
+ var DISK_GUARD_INTERVAL_MS = 60 * 60 * 1e3;
30
+ function readDiskStat(path = "/", statfs = statfsSync) {
31
+ try {
32
+ const s = statfs(path);
33
+ const bsize = Number(s.bsize);
34
+ return { freeBytes: Number(s.bavail) * bsize, totalBytes: Number(s.blocks) * bsize };
35
+ } catch {
36
+ return null;
37
+ }
38
+ }
39
+ function diskVerdict(stat) {
40
+ if (!stat || !Number.isFinite(stat.freeBytes) || !Number.isFinite(stat.totalBytes) || stat.totalBytes <= 0) {
41
+ return "unknown";
42
+ }
43
+ return stat.freeBytes < diskLowThresholdBytes(stat.totalBytes) ? "low" : "ok";
44
+ }
45
+ var SAFE_CLEANUP_STEPS = [
46
+ { bin: "journalctl", args: ["--vacuum-size=200M"], needsRoot: true, packageCache: false },
47
+ { bin: "npm", args: ["cache", "clean", "--force"], needsRoot: false, packageCache: true },
48
+ { bin: "pnpm", args: ["store", "prune"], needsRoot: false, packageCache: true }
49
+ ];
50
+ var NON_ROOT_EXTRA_STEPS = [
51
+ { bin: "npm", args: ["cache", "clean", "--force"], needsRoot: true, packageCache: true }
52
+ ];
53
+ async function runSafeCleanup(deps) {
54
+ const timeout = deps.timeoutMs ?? CLEANUP_STEP_TIMEOUT_MS;
55
+ const results = [];
56
+ const steps = deps.isRoot ? SAFE_CLEANUP_STEPS : [...SAFE_CLEANUP_STEPS, ...NON_ROOT_EXTRA_STEPS];
57
+ for (const step of steps) {
58
+ const label = `${step.needsRoot && !deps.isRoot ? "sudo " : ""}${step.bin} ${step.args.join(" ")}`;
59
+ if (step.packageCache) {
60
+ const busy = deps.packageManagerBusy ? await deps.packageManagerBusy().catch(() => true) : false;
61
+ if (busy) {
62
+ results.push({ step: label, outcome: "skipped", detail: "an npm/pnpm install is running" });
63
+ continue;
64
+ }
65
+ }
66
+ const [cmd, args] = deps.isRoot || !step.needsRoot ? [step.bin, step.args] : ["sudo", [...step.bin === "journalctl" ? ["-n"] : ["-n", "-H"], step.bin, ...step.args]];
67
+ try {
68
+ await deps.exec(cmd, args, { timeout });
69
+ results.push({ step: label, outcome: "ran" });
70
+ } catch (err) {
71
+ const e = err;
72
+ const stderr = e.stderr ? String(e.stderr) : "";
73
+ const missing = e.code === "ENOENT" || /command not found|not found/i.test(stderr);
74
+ results.push({
75
+ step: label,
76
+ outcome: missing ? "missing" : "failed",
77
+ detail: (stderr || e.message || "").trim().slice(0, 200)
78
+ });
79
+ }
80
+ }
81
+ return results;
82
+ }
83
+ function mb(bytes) {
84
+ return `${Math.round(bytes / (1024 * 1024))}MB`;
85
+ }
86
+ function summarize(results) {
87
+ return results.map((r) => `${r.step}=${r.outcome}`).join(", ");
88
+ }
89
+ var holdEpisode = null;
90
+ var busyEpisode = null;
91
+ var forcedHoldAnnounced = false;
92
+ var forcedBusyAnnounced = false;
93
+ function rearmForcedDiskHoldAnnounce() {
94
+ forcedHoldAnnounced = false;
95
+ forcedBusyAnnounced = false;
96
+ }
97
+ var cacheLockTail = Promise.resolve();
98
+ var cacheLockHolders = 0;
99
+ var PACKAGE_CACHE_LOCK_MAX_HOLD_MS = 10 * 60 * 1e3;
100
+ var PACKAGE_CACHE_IDLE_MAX_WAIT_MS = 5 * 60 * 1e3;
101
+ var CLEANUP_STEP_TIMEOUT_MS = 12e4;
102
+ var SELF_UPDATE_LOCK_MAX_HOLD_MS = 12 * 60 * 1e3;
103
+ var SELF_UPDATE_TIMEOUTS = {
104
+ brewUpdateMs: 6e4,
105
+ brewOutdatedMs: 3e4,
106
+ brewUpgradeMs: 12e4,
107
+ npmInstallMs: 18e4,
108
+ smokeCheckMs: 15e3
109
+ };
110
+ var SELF_UPDATE_BUSY_CEILING_MS = 2 * 60 * 60 * 1e3;
111
+ var BUSY_HOLD_RELOG_MS = 60 * 60 * 1e3;
112
+ var INSTALL_VERBS = /* @__PURE__ */ new Set(["install", "i", "ci", "add", "update", "upgrade", "up"]);
113
+ var NOT_AN_INSTALL = /* @__PURE__ */ new Set(["run", "run-script", "exec", "x", "dlx", "test", "start"]);
114
+ var PM_BINS = /^(npm|pnpm)$/;
115
+ var PM_SCRIPTS = /^(npm|npm-cli\.js|pnpm|pnpm\.c?js)$/;
116
+ var basenameOf = (p) => p.slice(p.lastIndexOf("/") + 1);
117
+ function isPackageInstallArgv(argv) {
118
+ let args = argv.filter((a) => a.length > 0);
119
+ if (args.length === 1 && args[0].includes(" ")) args = args[0].split(/\s+/);
120
+ if (args.length === 0) return false;
121
+ let rest;
122
+ const bin = basenameOf(args[0]);
123
+ if (PM_BINS.test(bin)) rest = args.slice(1);
124
+ else if (/^node(js)?$/.test(bin) && args[1] && PM_SCRIPTS.test(basenameOf(args[1]))) rest = args.slice(2);
125
+ else return false;
126
+ const sub = rest.find((a) => !a.startsWith("-"));
127
+ if (!sub || NOT_AN_INSTALL.has(sub)) return false;
128
+ if (INSTALL_VERBS.has(sub)) return true;
129
+ const next = rest.slice(rest.indexOf(sub) + 1).find((a) => !a.startsWith("-"));
130
+ return next !== void 0 && INSTALL_VERBS.has(next);
131
+ }
132
+ function anyPackageInstallRunning(deps = {}) {
133
+ let pids;
134
+ try {
135
+ pids = (deps.listPids ?? (() => readdirSync("/proc").filter((d) => /^\d+$/.test(d))))();
136
+ } catch (err) {
137
+ if (err.code !== "ENOENT") throw err;
138
+ pids = null;
139
+ }
140
+ if (pids === null) {
141
+ const lines = (deps.listProcessArgs ?? defaultPsArgs)();
142
+ return lines.some((line) => isPackageInstallArgv(line.trim().split(/\s+/)));
143
+ }
144
+ const readCmdline = deps.readCmdline ?? ((pid) => {
145
+ try {
146
+ return readFileSync(`/proc/${pid}/cmdline`, "utf8");
147
+ } catch {
148
+ return null;
149
+ }
150
+ });
151
+ const readRootId = deps.readRootId ?? ((pid) => {
152
+ try {
153
+ const st = statSync(`/proc/${pid}/root/`);
154
+ return `${st.dev}:${st.ino}`;
155
+ } catch {
156
+ return null;
157
+ }
158
+ });
159
+ const self = String(deps.selfPid ?? process.pid);
160
+ const selfRoot = readRootId("self");
161
+ for (const pid of pids) {
162
+ if (pid === self) continue;
163
+ const raw = readCmdline(pid);
164
+ if (!raw || !isPackageInstallArgv(raw.split("\0"))) continue;
165
+ const root = readRootId(pid);
166
+ if (selfRoot && root && root !== selfRoot) continue;
167
+ return true;
168
+ }
169
+ return false;
170
+ }
171
+ function defaultPsArgs() {
172
+ return execFileSync("ps", ["-A", "-o", "args="], { encoding: "utf8", timeout: 5e3 }).split("\n").filter(Boolean);
173
+ }
174
+ async function acquirePackageCacheLock(opts = {}) {
175
+ const prev = cacheLockTail;
176
+ let releaseTail;
177
+ cacheLockTail = new Promise((r) => releaseTail = r);
178
+ let abandoned = false;
179
+ let waitTimer;
180
+ const acquired = await new Promise((resolve) => {
181
+ void prev.then(() => {
182
+ if (abandoned) {
183
+ releaseTail();
184
+ return;
185
+ }
186
+ if (waitTimer) clearTimeout(waitTimer);
187
+ resolve(true);
188
+ });
189
+ if (opts.maxWaitMs !== void 0) {
190
+ waitTimer = setTimeout(() => {
191
+ abandoned = true;
192
+ resolve(false);
193
+ }, opts.maxWaitMs);
194
+ waitTimer.unref?.();
195
+ }
196
+ });
197
+ if (!acquired) return null;
198
+ cacheLockHolders += 1;
199
+ let released = false;
200
+ const maxHold = opts.maxHoldMs ?? PACKAGE_CACHE_LOCK_MAX_HOLD_MS;
201
+ const releaseOnce = () => {
202
+ if (released) return;
203
+ released = true;
204
+ clearTimeout(holdTimer);
205
+ cacheLockHolders -= 1;
206
+ releaseTail();
207
+ };
208
+ const holdTimer = setTimeout(() => {
209
+ (opts.log ?? console.warn)(
210
+ `[disk-guard] package-cache lock held ${Math.round(maxHold / 6e4)}m by ${opts.label ?? "a holder"} that is still running; releasing it so a stalled install cannot block the self-update or cleanup forever [ENG-11669]`
211
+ );
212
+ releaseOnce();
213
+ }, maxHold);
214
+ holdTimer.unref?.();
215
+ return releaseOnce;
216
+ }
217
+ async function withPackageCacheLock(fn, opts = {}) {
218
+ const release = await acquirePackageCacheLock(opts);
219
+ try {
220
+ return await fn();
221
+ } finally {
222
+ release();
223
+ }
224
+ }
225
+ async function exclusiveCleanup(cleanup) {
226
+ return withPackageCacheLock(cleanup, { label: "the disk-guard cleanup" });
227
+ }
228
+ async function ensureDiskForSelfUpdate(deps) {
229
+ const busyHold = (why) => {
230
+ const now = deps.nowMs ?? Date.now();
231
+ let first = false;
232
+ if (busyEpisode === null) {
233
+ busyEpisode = { since: now, loggedAt: now };
234
+ first = true;
235
+ }
236
+ const announceForced = deps.force === true && !forcedBusyAnnounced;
237
+ if (announceForced) forcedBusyAnnounced = true;
238
+ if (first || announceForced || now - busyEpisode.loggedAt >= BUSY_HOLD_RELOG_MS) {
239
+ const heldMin = Math.round((now - busyEpisode.since) / 6e4);
240
+ busyEpisode.loggedAt = now;
241
+ deps.log(
242
+ `[disk-guard] HELD${deps.force ? ' (forced "Update CLI now")' : ""} self-update: ` + (why === "lock" ? "a package install or cache cleanup has held the package cache for too long; " : "another npm/pnpm install is running on this host; ") + "not starting `npm install -g` alongside it. Retrying on a later check" + (first ? " (further busy holds this episode are logged hourly)" : `. STILL held after ${heldMin}m: if no install should be running, find and stop the stuck npm/pnpm process`) + " [ENG-11669]"
243
+ );
244
+ }
245
+ return { ok: false, freeBytes: null, cleaned: false, reason: "package_cache_busy" };
246
+ };
247
+ const isBusyRaw = async () => deps.packageManagerBusy ? await deps.packageManagerBusy().catch(() => true) : false;
248
+ const isBusy = async () => {
249
+ if (!await isBusyRaw()) {
250
+ busyEpisode = null;
251
+ return false;
252
+ }
253
+ const now = deps.nowMs ?? Date.now();
254
+ if (busyEpisode && now - busyEpisode.since >= SELF_UPDATE_BUSY_CEILING_MS) {
255
+ deps.log(
256
+ `[disk-guard] self-update has deferred to another npm/pnpm install for ${Math.round((now - busyEpisode.since) / 6e4)}m, past the ceiling; proceeding anyway so a stuck or looping install cannot block updates forever [ENG-11669]`
257
+ );
258
+ return false;
259
+ }
260
+ return true;
261
+ };
262
+ const release = await acquirePackageCacheLock({
263
+ maxWaitMs: deps.idleWaitMs ?? PACKAGE_CACHE_IDLE_MAX_WAIT_MS,
264
+ maxHoldMs: SELF_UPDATE_LOCK_MAX_HOLD_MS,
265
+ label: "the agt self-update",
266
+ log: deps.log
267
+ });
268
+ if (!release) return busyHold("lock");
269
+ try {
270
+ if (await isBusy()) {
271
+ release();
272
+ return busyHold("pgrep");
273
+ }
274
+ const room = await ensureDiskForSelfUpdateLocked(deps);
275
+ if (!room.ok) {
276
+ release();
277
+ return room;
278
+ }
279
+ if (await isBusy()) {
280
+ release();
281
+ return busyHold("pgrep");
282
+ }
283
+ busyEpisode = null;
284
+ return { ...room, release };
285
+ } catch (err) {
286
+ release();
287
+ throw err;
288
+ }
289
+ }
290
+ async function ensureDiskForSelfUpdateLocked(deps) {
291
+ const nowMs = deps.nowMs ?? Date.now();
292
+ const before = deps.readStat();
293
+ if (!before) {
294
+ deps.log("[disk-guard] could not read free space on the install filesystem; proceeding with the self-update unchecked [ENG-11669]");
295
+ return { ok: true, freeBytes: null, cleaned: false };
296
+ }
297
+ if (before.freeBytes >= SELF_UPDATE_MIN_FREE_BYTES) {
298
+ if (holdEpisode) deps.log(`[disk-guard] self-update hold cleared: ${mb(before.freeBytes)} free [ENG-11669]`);
299
+ holdEpisode = null;
300
+ forcedHoldAnnounced = false;
301
+ return { ok: true, freeBytes: before.freeBytes, cleaned: false };
302
+ }
303
+ if (holdEpisode && nowMs - holdEpisode.lastCleanupMs < DISK_GUARD_INTERVAL_MS) {
304
+ if (deps.force && !forcedHoldAnnounced) {
305
+ forcedHoldAnnounced = true;
306
+ deps.log(
307
+ `[disk-guard] HELD (forced "Update CLI now"): only ${mb(before.freeBytes)} free for the install (need ${mb(SELF_UPDATE_MIN_FREE_BYTES)}); cleanup already ran this hour and did not make room. Free disk on this host; the request stays pending and applies once there is room (said once per request) [ENG-11669]`
308
+ );
309
+ }
310
+ return { ok: false, freeBytes: before.freeBytes, cleaned: true };
311
+ }
312
+ const firstHold = holdEpisode === null;
313
+ if (firstHold) {
314
+ deps.log(
315
+ `[disk-guard] only ${mb(before.freeBytes)} free for the install (need ${mb(SELF_UPDATE_MIN_FREE_BYTES)} to self-update); running safe cleanup first [ENG-11669]`
316
+ );
317
+ }
318
+ const results = await deps.cleanup();
319
+ const after = deps.readStat();
320
+ const freeAfter = after?.freeBytes ?? before.freeBytes;
321
+ if (freeAfter >= SELF_UPDATE_MIN_FREE_BYTES) {
322
+ deps.log(`[disk-guard] cleanup freed space: ${mb(before.freeBytes)} -> ${mb(freeAfter)} (${summarize(results)}) [ENG-11669]`);
323
+ holdEpisode = null;
324
+ return { ok: true, freeBytes: freeAfter, cleaned: true };
325
+ }
326
+ holdEpisode = { lastCleanupMs: nowMs };
327
+ if (!firstHold && deps.force && !forcedHoldAnnounced) {
328
+ forcedHoldAnnounced = true;
329
+ deps.log(
330
+ `[disk-guard] HELD (forced "Update CLI now"): still only ${mb(freeAfter)} free after cleanup (need ${mb(SELF_UPDATE_MIN_FREE_BYTES)}; ${summarize(results)}). Free disk on this host, then retry [ENG-11669]`
331
+ );
332
+ }
333
+ if (firstHold) {
334
+ deps.log(
335
+ `[disk-guard] HELD self-update: still only ${mb(freeAfter)} free after cleanup (${summarize(results)}). Keeping the current agt binary rather than risk an install that deletes it and then fails with ENOSPC. Free disk on this host (or grow the volume) and the update applies on a later check. (further holds this episode are not logged) [ENG-11669]`
336
+ );
337
+ }
338
+ return { ok: false, freeBytes: freeAfter, cleaned: true };
339
+ }
340
+ function tightestDiskStat(paths, read = readDiskStat) {
341
+ let best = null;
342
+ for (const p of [...new Set(paths)]) {
343
+ const s = read(p);
344
+ if (s && (best === null || s.freeBytes < best.freeBytes)) best = s;
345
+ }
346
+ return best;
347
+ }
348
+ var lastGuardRunMs = null;
349
+ var guardBlindLogged = false;
350
+ async function maybeRunDiskGuard(deps) {
351
+ if (lastGuardRunMs !== null && deps.nowMs - lastGuardRunMs < DISK_GUARD_INTERVAL_MS) {
352
+ return { ran: false, reason: "throttled" };
353
+ }
354
+ lastGuardRunMs = deps.nowMs;
355
+ const before = deps.readStat();
356
+ const verdict = diskVerdict(before);
357
+ if (verdict === "unknown" || !before) {
358
+ if (!guardBlindLogged) {
359
+ guardBlindLogged = true;
360
+ deps.log(
361
+ "[disk-guard] could not read free space on /; the hourly disk guard is blind (no cleanup, no host_disk_low) until it can. Said once until the reading comes back [ENG-11669]"
362
+ );
363
+ }
364
+ return { ran: false, reason: "unknown" };
365
+ }
366
+ if (guardBlindLogged) {
367
+ guardBlindLogged = false;
368
+ deps.log("[disk-guard] free space on / is readable again; the hourly disk guard is back [ENG-11669]");
369
+ }
370
+ if (verdict !== "low") return { ran: false, reason: "ok" };
371
+ const results = await exclusiveCleanup(deps.cleanup);
372
+ const after = deps.readStat();
373
+ const stillLow = diskVerdict(after) === "low";
374
+ deps.log(
375
+ `[disk-guard] / was low (${mb(before.freeBytes)} free of ${mb(before.totalBytes)}); safe cleanup ran (${summarize(results)}); now ${after ? mb(after.freeBytes) : "unknown"} free.` + (stillLow ? " STILL LOW: something other than caches is filling this disk. Investigate before it fills: `du -xh --max-depth=2 / | sort -rh | head`. Never delete /root/.claude/projects (agent transcripts). [ENG-11669]" : " [ENG-11669]")
376
+ );
377
+ return { ran: true, beforeBytes: before.freeBytes, afterBytes: after?.freeBytes ?? null, stillLow };
378
+ }
379
+
380
+ // src/lib/claude-code-updater.ts
381
+ var execFileAsync = promisify(execFile);
382
+ var NPM_PKG = "@anthropic-ai/claude-code";
383
+ var CLAUDE_CODE_SECURITY_FLOOR = "2.1.308";
384
+ var BELOW_FLOOR_RECHECK_MS = 6e4;
385
+ var DEFAULT_INTERVAL_MS = 6 * 60 * 60 * 1e3;
386
+ var DEFAULT_VERIFY_MISMATCH_THRESHOLD = 5;
387
+ function intervalMs() {
388
+ const raw = Number.parseInt(process.env["AGT_CLAUDE_UPDATE_INTERVAL_MS"] ?? "", 10);
389
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_INTERVAL_MS;
390
+ }
391
+ function verifyMismatchThreshold() {
392
+ const raw = Number.parseInt(process.env["AGT_CLAUDE_UPDATE_VERIFY_THRESHOLD"] ?? "", 10);
393
+ return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_VERIFY_MISMATCH_THRESHOLD;
394
+ }
395
+ function parseVersion(output) {
396
+ const m = output.match(/\d+\.\d+\.\d+[A-Za-z0-9.+-]*/);
397
+ return m ? m[0] : null;
398
+ }
399
+ function isConcreteVersion(spec) {
400
+ return /^\d+\.\d+\.\d+[A-Za-z0-9.+-]*$/.test(spec.trim());
401
+ }
402
+ function compareVersions(a, b) {
403
+ const re = /^(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?/;
404
+ const ma = re.exec(a.trim());
405
+ const mb2 = re.exec(b.trim());
406
+ if (!ma || !mb2) return null;
407
+ for (let i = 1; i <= 3; i++) {
408
+ const d = Number(ma[i]) - Number(mb2[i]);
409
+ if (d !== 0) return d < 0 ? -1 : 1;
410
+ }
411
+ const preA = ma[4] !== void 0;
412
+ const preB = mb2[4] !== void 0;
413
+ if (preA === preB) return 0;
414
+ return preA ? -1 : 1;
415
+ }
416
+ function shouldRunClaudeCodeUpdater(input) {
417
+ if (!input.desiredVersion) return false;
418
+ if (isBelowSecurityFloor(input.installedVersion, input.floor)) return true;
419
+ return input.windowGate === "proceed";
420
+ }
421
+ function isBelowSecurityFloor(version, floor = CLAUDE_CODE_SECURITY_FLOOR) {
422
+ if (!version) return false;
423
+ const parsed = parseVersion(version);
424
+ if (!parsed) return false;
425
+ const cmp = compareVersions(parsed, floor);
426
+ return cmp !== null && cmp < 0;
427
+ }
428
+ var WELL_KNOWN_CLAUDE_PATHS = [
429
+ "/usr/bin/claude",
430
+ // Anthropic native autoupdater (Linux deb/rpm)
431
+ "/usr/local/bin/claude",
432
+ // npm global on macOS Intel + Linux without nvm
433
+ "/opt/homebrew/bin/claude",
434
+ // Homebrew on macOS Apple Silicon
435
+ "/home/linuxbrew/.linuxbrew/bin/claude"
436
+ // Linuxbrew (used on agt-aws-* hosts)
437
+ ];
438
+ var lastCheckAt = 0;
439
+ var multiInstallWarned = false;
440
+ var verifyMismatchState = null;
441
+ var belowFloorBusySince = null;
442
+ function _resetClaudeCodeUpdaterState() {
443
+ lastCheckAt = 0;
444
+ multiInstallWarned = false;
445
+ verifyMismatchState = null;
446
+ belowFloorBusySince = null;
447
+ }
448
+ function defaultPsSnapshot() {
449
+ return execFileSync2("ps", ["-eo", "pid,ppid,args"], { encoding: "utf-8", maxBuffer: 16 * 1024 * 1024 });
450
+ }
451
+ function defaultReadClaudeJson(path) {
452
+ try {
453
+ return readFileSync2(path, "utf-8");
454
+ } catch {
455
+ return null;
456
+ }
457
+ }
458
+ function hostHasLiveAgent(psOutput) {
459
+ return parsePsRows(psOutput).some((r) => CLAUDE_AGENT_PROCESS_PATTERN.test(r.args));
460
+ }
461
+ function readsAsNativeInstall(raw) {
462
+ if (!raw) return false;
463
+ try {
464
+ const parsed = JSON.parse(raw);
465
+ return parsed.installMethod === "native";
466
+ } catch {
467
+ return false;
468
+ }
469
+ }
470
+ function isNativeInstallPath(realPath) {
471
+ return /\/claude\/versions\/\d+\.\d+\.\d+[^/]*$/.test(realPath);
472
+ }
473
+ async function resolveNativeOwnership(run, realPath, claudeJson) {
474
+ const configSaysNative = readsAsNativeInstall(claudeJson);
475
+ let resolved = null;
476
+ const override = process.env["CLAUDE_PATH"];
477
+ if (override) {
478
+ resolved = override;
479
+ } else {
480
+ try {
481
+ const { stdout } = await run("which", ["claude"]);
482
+ resolved = stdout.trim().split("\n")[0]?.trim() || null;
483
+ } catch {
484
+ resolved = null;
485
+ }
486
+ }
487
+ if (resolved) {
488
+ try {
489
+ const real = realPath(resolved);
490
+ return { native: isNativeInstallPath(real), binary: real, configSaysNative };
491
+ } catch {
492
+ }
493
+ }
494
+ return { native: configSaysNative, binary: null, configSaysNative };
495
+ }
496
+ async function resolveInstallTargetPath(run) {
497
+ try {
498
+ const { stdout } = await run("npm", ["prefix", "-g"]);
499
+ const prefix = stdout.trim();
500
+ if (!prefix) return null;
501
+ return `${prefix}/bin/claude`;
502
+ } catch {
503
+ return null;
504
+ }
505
+ }
506
+ async function probeClaudeInstalls(run, fileExists, home) {
507
+ const candidates = new Set(WELL_KNOWN_CLAUDE_PATHS);
508
+ candidates.add(`${home}/.claude/local/claude`);
509
+ try {
510
+ const { stdout } = await run("which", ["claude"]);
511
+ const p = stdout.trim().split("\n")[0]?.trim();
512
+ if (p) candidates.add(p);
513
+ } catch {
514
+ }
515
+ const results = [];
516
+ for (const p of candidates) {
517
+ if (!fileExists(p)) continue;
518
+ let version = null;
519
+ try {
520
+ const { stdout } = await run(p, ["--version"]);
521
+ version = parseVersion(stdout);
522
+ } catch {
523
+ }
524
+ results.push({ path: p, version });
525
+ }
526
+ return results;
527
+ }
528
+ async function maybeUpdateClaudeCode(deps) {
529
+ const now = deps.now ? deps.now() : Date.now();
530
+ const run = deps.runCommand ?? ((cmd, args) => execFileAsync(cmd, args, { maxBuffer: 16 * 1024 * 1024 }));
531
+ const ps = deps.psSnapshot ?? defaultPsSnapshot;
532
+ const fileExists = deps.fileExists ?? existsSync;
533
+ const readClaudeJson = deps.readClaudeJson ?? defaultReadClaudeJson;
534
+ const realPath = deps.realPath ?? realpathSync;
535
+ const home = (deps.homeDir ?? homedir)();
536
+ const { log } = deps;
537
+ const floor = deps.securityFloor ?? CLAUDE_CODE_SECURITY_FLOOR;
538
+ let forcedUnderLiveAgents = false;
539
+ if (now - lastCheckAt < intervalMs()) {
540
+ return { action: "throttled" };
541
+ }
542
+ lastCheckAt = now;
543
+ try {
544
+ const spec = (deps.desiredVersion || "latest").trim();
545
+ if (!multiInstallWarned) {
546
+ const installs = await probeClaudeInstalls(run, fileExists, home);
547
+ if (installs.length > 1) {
548
+ const summary = installs.map((i) => `${i.path}=${i.version ?? "unknown"}`).join(", ");
549
+ log(`[claude-code-updater] multiple claude installs detected: ${summary}`);
550
+ }
551
+ multiInstallWarned = true;
552
+ }
553
+ const claudeJsonPath = effectiveClaudeJsonPath(home);
554
+ const ownership = await resolveNativeOwnership(run, realPath, readClaudeJson(claudeJsonPath));
555
+ if (!ownership.native && ownership.configSaysNative) {
556
+ log(
557
+ `[claude-code-updater] ${claudeJsonPath} says installMethod=native but the binary this host runs (${ownership.binary}) is not a native install \u2014 ignoring it and converging to the pin`
558
+ );
559
+ }
560
+ if (ownership.native) {
561
+ log("[claude-code-updater] native install detected, deferring to Anthropic autoupdater");
562
+ verifyMismatchState = null;
563
+ return { action: "skipped_native_install", target: spec };
564
+ }
565
+ let target;
566
+ if (isConcreteVersion(spec)) {
567
+ target = spec;
568
+ } else {
569
+ const { stdout } = await run("npm", ["view", `${NPM_PKG}@${spec}`, "version"]);
570
+ const resolved = parseVersion(stdout);
571
+ if (!resolved) {
572
+ log(`[claude-code-updater] could not resolve ${NPM_PKG}@${spec} to a version; skipping`);
573
+ return { action: "failed", reason: "unresolved_target" };
574
+ }
575
+ target = resolved;
576
+ }
577
+ const targetVsFloor = compareVersions(target, floor);
578
+ if (targetVsFloor !== null && targetVsFloor < 0) {
579
+ log(
580
+ `[claude-code-updater] target ${target} is below the security floor ${floor} (ENG-11641: CVE-2026-111497 + emSign root) \u2014 raising the target to the floor`
581
+ );
582
+ target = floor;
583
+ }
584
+ let current = deps.currentVersion ?? null;
585
+ if (!current) {
586
+ try {
587
+ const { stdout } = await run("claude", ["--version"]);
588
+ current = parseVersion(stdout);
589
+ } catch {
590
+ current = null;
591
+ }
592
+ }
593
+ const belowFloor = isBelowSecurityFloor(current, floor);
594
+ if (!belowFloor) belowFloorBusySince = null;
595
+ if (current && current === target) {
596
+ if (verifyMismatchState && verifyMismatchState.target === target) {
597
+ verifyMismatchState = null;
598
+ }
599
+ return { action: "up_to_date", version: target };
600
+ }
601
+ if (hostHasLiveAgent(ps())) {
602
+ if (!belowFloor) {
603
+ log(`[claude-code-updater] host busy (live agent) \u2014 deferring update to ${target} to a later cycle`);
604
+ return { action: "deferred_busy", target };
605
+ }
606
+ if (belowFloorBusySince === null) belowFloorBusySince = now;
607
+ const waitedMs = now - belowFloorBusySince;
608
+ if (waitedMs < FORCED_UPDATE_HARD_DEADLINE_MS) {
609
+ lastCheckAt = now - intervalMs() + BELOW_FLOOR_RECHECK_MS;
610
+ log(
611
+ `[claude-code-updater] host busy (live agent) and ${current} is below the security floor ${floor} \u2014 deferring update to ${target}; will force it in ${Math.ceil((FORCED_UPDATE_HARD_DEADLINE_MS - waitedMs) / 6e4)}m if the npm install slot is the binary agents run`
612
+ );
613
+ return { action: "deferred_busy", target };
614
+ }
615
+ const slotPath = await resolveInstallTargetPath(run);
616
+ let slotVersion = null;
617
+ if (slotPath) {
618
+ try {
619
+ slotVersion = parseVersion((await run(slotPath, ["--version"])).stdout);
620
+ } catch {
621
+ slotVersion = null;
622
+ }
623
+ }
624
+ if (!slotPath) {
625
+ belowFloorBusySince = null;
626
+ log(
627
+ `[claude-code-updater] ERROR security_floor_blocked: deadline reached but \`npm prefix -g\` did not resolve \u2014 cannot prove an install would reach the binary agents run; not forcing an install under live agents`
628
+ );
629
+ return { action: "deferred_busy", target };
630
+ }
631
+ const slotIsTheRunningBinary = slotVersion !== null && isBelowSecurityFloor(slotVersion, floor) && slotVersion === current;
632
+ if (!slotIsTheRunningBinary) {
633
+ belowFloorBusySince = null;
634
+ log(
635
+ `[claude-code-updater] ERROR security_floor_blocked: PATH claude reports ${current} (below the security floor ${floor}) but the npm install slot ${slotPath} reports ${slotVersion ?? "no readable version"} \u2014 cannot show the install would reach the binary agents run (multi-install, ENG-5798); not forcing an install under live agents, investigate the host's claude install slots`
636
+ );
637
+ return { action: "deferred_busy", target };
638
+ }
639
+ log(
640
+ `[claude-code-updater] security floor deadline reached: ${current} < ${floor} and host still busy after ${Math.round(waitedMs / 6e4)}m \u2014 installing ${target} anyway (ENG-11641)`
641
+ );
642
+ forcedUnderLiveAgents = true;
643
+ }
644
+ const installTargetPath = await resolveInstallTargetPath(run);
645
+ log(`[claude-code-updater] updating Claude Code ${current ?? "(none)"} \u2192 ${target}`);
646
+ await withPackageCacheLock(() => run("npm", ["install", "-g", `${NPM_PKG}@${target}`]), {
647
+ label: "the Claude Code install",
648
+ log
649
+ });
650
+ const verifyTarget = installTargetPath ?? "claude";
651
+ let installed = null;
652
+ try {
653
+ const { stdout } = await run(verifyTarget, ["--version"]);
654
+ installed = parseVersion(stdout);
655
+ } catch {
656
+ installed = null;
657
+ }
658
+ if (installed !== target) {
659
+ belowFloorBusySince = null;
660
+ log(
661
+ `[claude-code-updater] post-install verify mismatch: expected ${target}, got ${installed ?? "unknown"} from ${verifyTarget}`
662
+ );
663
+ if (!verifyMismatchState || verifyMismatchState.target !== target) {
664
+ verifyMismatchState = { target, count: 1, alarmed: false };
665
+ } else {
666
+ verifyMismatchState.count += 1;
667
+ }
668
+ const threshold = verifyMismatchThreshold();
669
+ if (verifyMismatchState.count >= threshold && !verifyMismatchState.alarmed) {
670
+ verifyMismatchState.alarmed = true;
671
+ log(
672
+ `[claude-code-updater] ERROR verify_mismatch_stuck: ${verifyMismatchState.count} consecutive verify failures for target ${target} (verify path: ${verifyTarget}) \u2014 install is not taking effect, investigate host install slots`
673
+ );
674
+ return { action: "failed", reason: "verify_mismatch_threshold" };
675
+ }
676
+ return { action: "failed", reason: "verify_mismatch" };
677
+ }
678
+ verifyMismatchState = null;
679
+ belowFloorBusySince = null;
680
+ log(`[claude-code-updater] updated Claude Code ${current ?? "(none)"} \u2192 ${installed}`);
681
+ if (forcedUnderLiveAgents) {
682
+ log(
683
+ `[claude-code-updater] WARNING installed ${installed} under live agents: running sessions keep the previous ${current ?? "unknown"} binary until they restart. framework_version now reports ${installed}, which does NOT mean the live sessions are patched`
684
+ );
685
+ }
686
+ return { action: "updated", from: current, to: installed };
687
+ } catch (err) {
688
+ if (forcedUnderLiveAgents) belowFloorBusySince = null;
689
+ log(`[claude-code-updater] update attempt failed: ${err instanceof Error ? err.message : String(err)}`);
690
+ return { action: "failed", reason: "exception" };
691
+ }
692
+ }
693
+
694
+ export {
695
+ readDiskStat,
696
+ runSafeCleanup,
697
+ rearmForcedDiskHoldAnnounce,
698
+ SELF_UPDATE_TIMEOUTS,
699
+ anyPackageInstallRunning,
700
+ withPackageCacheLock,
701
+ ensureDiskForSelfUpdate,
702
+ tightestDiskStat,
703
+ maybeRunDiskGuard,
704
+ CLAUDE_CODE_SECURITY_FLOOR,
705
+ parseVersion,
706
+ isConcreteVersion,
707
+ compareVersions,
708
+ shouldRunClaudeCodeUpdater,
709
+ isBelowSecurityFloor,
710
+ WELL_KNOWN_CLAUDE_PATHS,
711
+ _resetClaudeCodeUpdaterState,
712
+ hostHasLiveAgent,
713
+ readsAsNativeInstall,
714
+ isNativeInstallPath,
715
+ probeClaudeInstalls,
716
+ maybeUpdateClaudeCode
717
+ };
718
+ //# sourceMappingURL=chunk-PG343LR6.js.map