infra-kit 0.34.0 → 0.36.0
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/dist/{boot-LC2ESPY6.js → boot-ST2534PA.js} +2 -2
- package/dist/boot-ST2534PA.js.map +7 -0
- package/dist/{chunk-5ZBDB3U7.js → chunk-3JFITLZG.js} +2 -2
- package/dist/chunk-3JFITLZG.js.map +7 -0
- package/dist/{chunk-SAHE3MZJ.js → chunk-55F372Z7.js} +1 -1
- package/dist/{chunk-SAHE3MZJ.js.map → chunk-55F372Z7.js.map} +2 -2
- package/dist/chunk-BPOPODWW.js +15 -0
- package/dist/chunk-BPOPODWW.js.map +7 -0
- package/dist/{chunk-I23MDR4X.js → chunk-GCOYOWGH.js} +1 -1
- package/dist/{chunk-I23MDR4X.js.map → chunk-GCOYOWGH.js.map} +2 -2
- package/dist/{chunk-XES5EOEE.js → chunk-PP44RGIU.js} +2 -2
- package/dist/chunk-PP44RGIU.js.map +7 -0
- package/dist/{chunk-7A4LVTRJ.js → chunk-QS7RKE5P.js} +2 -2
- package/dist/chunk-QS7RKE5P.js.map +7 -0
- package/dist/{chunk-AWFGP6GY.js → chunk-REDCBY4O.js} +1 -1
- package/dist/{chunk-AWFGP6GY.js.map → chunk-REDCBY4O.js.map} +2 -2
- package/dist/{chunk-QCTAEXMN.js → chunk-SBG3HRSC.js} +6 -6
- package/dist/{chunk-QCTAEXMN.js.map → chunk-SBG3HRSC.js.map} +2 -2
- package/dist/{chunk-U7VGROH3.js → chunk-TCQXTDVZ.js} +2 -2
- package/dist/{chunk-U7VGROH3.js.map → chunk-TCQXTDVZ.js.map} +1 -1
- package/dist/{chunk-MWGE43QS.js → chunk-TGEAIKD6.js} +2 -2
- package/dist/{chunk-MWGE43QS.js.map → chunk-TGEAIKD6.js.map} +2 -2
- package/dist/{chunk-DVXTZPQ5.js → chunk-UTQ4KTO6.js} +1 -1
- package/dist/{chunk-DVXTZPQ5.js.map → chunk-UTQ4KTO6.js.map} +2 -2
- package/dist/{chunk-TVG47UHG.js → chunk-V2ZLQPLI.js} +1 -1
- package/dist/{chunk-TVG47UHG.js.map → chunk-V2ZLQPLI.js.map} +2 -2
- package/dist/cli.js +145 -127
- package/dist/cli.js.map +4 -4
- package/dist/dev-server.js +5 -5
- package/dist/dev-server.js.map +2 -2
- package/dist/{dev-wizard-run-452R76HN.js → dev-wizard-run-PAHX7EMH.js} +2 -2
- package/dist/dev-wizard-run-PAHX7EMH.js.map +7 -0
- package/dist/{env-load-IXDGWGPV.js → env-load-XBKASGXP.js} +2 -2
- package/dist/mcp-proxy.js +1 -1
- package/dist/mcp-proxy.js.map +2 -2
- package/dist/{persistent-ink-dev-ui-ZSOS3CF4.js → persistent-ink-dev-ui-D6ALK2P3.js} +2 -2
- package/dist/update-check.js +1 -1
- package/package.json +3 -3
- package/dist/boot-LC2ESPY6.js.map +0 -7
- package/dist/chunk-467ZSXYU.js +0 -15
- package/dist/chunk-467ZSXYU.js.map +0 -7
- package/dist/chunk-5ZBDB3U7.js.map +0 -7
- package/dist/chunk-7A4LVTRJ.js.map +0 -7
- package/dist/chunk-XES5EOEE.js.map +0 -7
- package/dist/dev-wizard-run-452R76HN.js.map +0 -7
- /package/dist/{env-load-IXDGWGPV.js.map → env-load-XBKASGXP.js.map} +0 -0
- /package/dist/{persistent-ink-dev-ui-ZSOS3CF4.js.map → persistent-ink-dev-ui-D6ALK2P3.js.map} +0 -0
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/dev/managed-child.ts", "../src/dev/signal-shutdown.ts", "../src/lib/errors/is-prompt-cancellation.ts", "../src/integrations/orca/run-orca.ts", "../src/integrations/orca/availability.ts", "../src/integrations/orca/realpath-for-orca-cwd.ts", "../src/integrations/orca/ensure-repo.ts", "../src/integrations/orca/is-worktree-listed.ts", "../src/integrations/orca/caller-inside-target.ts", "../src/integrations/orca/list-terminals-by-cwd.ts", "../src/integrations/orca/close-worktree-terminals.ts", "../src/integrations/orca/open-worktree-terminals.ts", "../src/integrations/orca/terminal-title.ts", "../src/dev/health-probe.ts", "../src/lib/app-release/app-release.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * @fileoverview\n *\n * Shared supervision for the dev-server's long-lived, detached child processes\n * (`turbo watch build`, `turbo run dev`). Both are deep trees\n * (`sh \u2192 pnpm \u2192 node \u2192 turbo \u2192 \u2026`) spawned `detached` so they form their own process\n * group; teardown signals the whole GROUP (`process.kill(-pid, \u2026)`), not just the wrapper.\n *\n * Signalling turbo's group is NOT enough on its own: turbo puts every task it runs\n * (`vite`, `tsc -b`, \u2026) into a process group of its OWN, so `kill(-turboPid, \u2026)` reaches\n * turbo but never its tasks. Turbo normally forwards the signal and reaps them, but when it\n * is slower than the grace window a bare `SIGKILL` on turbo's group kills the only process\n * that knows the task groups \u2014 stranding orphaned vite servers that hold their ports until\n * the next run fails with `EADDRINUSE`.\n *\n * So teardown snapshots the descendant process groups BEFORE signalling (once turbo dies its\n * children reparent to init and can no longer be found by walking `ppid`), waits for the whole\n * set to exit, and only then force-kills whatever is left.\n *\n * The same fact \u2014 reparenting blinds the `ppid` walk \u2014 is why a snapshot taken only at teardown is not\n * enough. When turbo dies on its OWN (a crash, an OOM kill) there is no teardown to snapshot from, and\n * the tasks it never reaped are already unreachable. So the snapshot is instead kept ROLLING for the\n * child's whole life, and each group is stamped with its leader's `lstart`: a pgid is a bare integer the\n * kernel will happily recycle, and the leader's start time is the only proof that the group we are about\n * to SIGKILL is still the one we saw.\n *\n * That rolling snapshot lives in a per-child closure, which is exactly what the SECOND-SIGNAL force path\n * (`killDescendantGroupsNow`, used by both `signal-shutdown`'s force-quit and `crash-barrier`'s fatal\n * path) cannot reach on its own \u2014 it only ever does a live `ppid` walk from `process.pid`, the same walk\n * that goes blind the moment a child dies. So every supervised child's snapshot is also published to a\n * module-level `registry` (see below), letting that force path find a descendant which reparented to init\n * before ITS parent's `exit` even fired \u2014 the one case a live walk structurally cannot cover.\n */\nimport { execFile, execFileSync } from 'node:child_process'\nimport type { ChildProcess } from 'node:child_process'\nimport process from 'node:process'\n\n/** Handle to a supervised child; `kill()` reaps its whole process tree and resolves when gone. */\nexport interface ManagedChild {\n kill: () => Promise<void>\n}\n\n/**\n * Grace before escalating SIGTERM \u2192 SIGKILL. Long enough for turbo to stop its tasks itself\n * (typically well under a second) and short enough that Ctrl-C still feels immediate. Escalating\n * is safe now that every task group is killed as a set, so this need not cover the worst case.\n * The happy path never waits the full window \u2014 teardown returns as soon as the groups are gone.\n */\nconst DEFAULT_GRACE_MS = 5000\n\n/** Poll interval while waiting for the doomed process groups to exit. */\nconst POLL_MS = 100\n\n/** How long to wait for the kernel to reap a group after SIGKILL before giving up on confirmation. */\nconst REAP_TIMEOUT_MS = 2000\n\n/** Absolute path to `ps` \u2014 present on both macOS and Linux, and immune to `PATH` substitution. */\nconst PS_BIN = '/bin/ps'\n\n/**\n * How often the live process table is sampled while a supervised child is alive, to keep\n * {@link ManagedChild}'s group snapshot current. See {@link superviseChild} for why a snapshot taken\n * only at kill-time is too late. Async (`execFile`, not `execFileSync`), so it never blocks the event\n * loop the in-process backends share.\n */\nconst SAMPLE_INTERVAL_MS = 1000\n\n/** `ps` columns: the `ppid` walk needs pid/ppid/pgid, and the reuse guard needs the leader's `lstart`. */\nconst PS_ARGS = ['-eo', 'pid=,ppid=,pgid=,lstart='] as const\n\n/** Cap on `ps` output; a busy machine's full process table stays far below this. */\nconst PS_MAX_BUFFER = 8 << 20\n\n/** One row of `ps -eo pid=,ppid=,pgid=,lstart=`. */\nexport interface ProcRow {\n pid: number\n ppid: number\n pgid: number\n /** The process's start time as `ps` prints it (`Mon Jul 13 13:21:14 2026`); `''` when unavailable. */\n lstart: string\n}\n\n/**\n * The subset of a {@link ProcRow} that describes the process TREE. The `ppid` walk needs nothing else,\n * so the functions that only walk take this \u2014 `lstart` is the reuse guard's concern, not the topology's.\n */\nexport type TopologyRow = Pick<ProcRow, 'pid' | 'ppid' | 'pgid'>\n\n/**\n * Parse `ps -eo pid=,ppid=,pgid=,lstart=`, skipping any line that doesn't start with three integers.\n * `lstart` is whitespace-separated and multi-token, so it is everything after the third column.\n */\nexport const parseProcRows = (raw: string): ProcRow[] => {\n return raw.split('\\n').flatMap((line) => {\n const [rawPid, rawPpid, rawPgid, ...rest] = line.trim().split(/\\s+/)\n\n if (rawPid == null || rawPpid == null || rawPgid == null) return []\n\n const pid = Number(rawPid)\n const ppid = Number(rawPpid)\n const pgid = Number(rawPgid)\n\n if (!Number.isInteger(pid) || !Number.isInteger(ppid) || !Number.isInteger(pgid)) return []\n\n return [{ pid, ppid, pgid, lstart: rest.join(' ') }]\n })\n}\n\n/**\n * A process group, stamped with its LEADER's start time at the moment the group was observed.\n *\n * The start time is the whole point. A pgid on its own is a bare integer: by the time we come to reap\n * it the kernel may have recycled it onto an unrelated process, and `kill(-pgid, \u2026)` would then destroy\n * a stranger's group. The leader's `lstart` is the identity that survives \u2014 a recycled pgid necessarily\n * has a NEWER birth time than the one recorded, so it is refused.\n */\nexport interface GroupSnapshot {\n pgid: number\n leaderStart: string\n}\n\n/** Map every process group in `rows` to its leader's start time (the leader is the pid equal to the pgid). */\nconst leaderStarts = (rows: ProcRow[]): Map<number, string> => {\n const starts = new Map<number, string>()\n\n for (const row of rows) {\n if (row.pid === row.pgid) starts.set(row.pgid, row.lstart)\n }\n\n return starts\n}\n\n/**\n * {@link collectDoomedGroups}, but each group is stamped with its leader's start time so it can be\n * validated later. Groups whose leader is not in `rows` are dropped: with no leader there is nothing to\n * stamp, so nothing could prove the group is still the one we saw, and the reuse guard must fail closed.\n *\n * @example\n * const rows = [\n * { pid: 10, ppid: 1, pgid: 10, lstart: 'Mon Jul 13 13:00:00 2026' },\n * { pid: 20, ppid: 10, pgid: 20, lstart: 'Mon Jul 13 13:00:01 2026' },\n * ]\n * snapshotGroups(10, rows) // => [{ pgid: 10, leaderStart: '\u202613:00:00\u2026' }, { pgid: 20, leaderStart: '\u202613:00:01\u2026' }]\n */\nexport const snapshotGroups = (rootPid: number, rows: ProcRow[], excludePgid?: number): GroupSnapshot[] => {\n const starts = leaderStarts(rows)\n\n return collectDoomedGroups(rootPid, rows, excludePgid).flatMap((pgid) => {\n const leaderStart = starts.get(pgid)\n\n if (leaderStart == null || leaderStart === '') return []\n\n return [{ pgid, leaderStart }]\n })\n}\n\n/**\n * pgids in `snapshot` whose leader's start time still matches `rows` \u2014 the reuse guard shared by every\n * path that reaps from a snapshot instead of a live walk: {@link reapSnapshot} (the per-child crash/kill\n * reap) and {@link collectForceReapTargets} (the force-reap path, reaping every REGISTERED child's\n * snapshot). Extracted so the two can never drift apart \u2014 a registry-sourced pgid must clear the exact\n * same check a per-child reap does before either signals it.\n */\nconst validSnapshotPgids = (snapshot: readonly GroupSnapshot[], rows: ProcRow[]): number[] => {\n const starts = leaderStarts(rows)\n\n return snapshot.flatMap(({ pgid, leaderStart }) => {\n return starts.get(pgid) === leaderStart ? [pgid] : []\n })\n}\n\n/**\n * SIGKILL every snapshotted group whose leader is STILL the same process, and return the pgids killed.\n *\n * A group whose leader has since exited is skipped (nothing to kill, and no way to prove ownership of\n * whatever else may share the pgid). A group whose leader's start time no longer matches is a recycled\n * pgid belonging to a stranger \u2014 skipped, loudly and deliberately. This is the guard that makes reaping\n * from a stale snapshot safe, and it is why the snapshot records `lstart` at all.\n */\nexport const reapSnapshot = (snapshot: readonly GroupSnapshot[], rows: ProcRow[]): number[] => {\n return validSnapshotPgids(snapshot, rows).flatMap((pgid) => {\n try {\n process.kill(-pgid, 'SIGKILL')\n } catch {\n // Raced us to exit (ESRCH) \u2014 nothing to kill.\n return []\n }\n\n return [pgid]\n })\n}\n\n/**\n * Distinct process groups of every transitive descendant of `rootPid`, plus `rootPid`'s own\n * group. Group `0`/`1` and `excludePgid` (our own group \u2014 a detached child can never share it,\n * but a mis-parsed row must never make us signal ourselves) are filtered out.\n *\n * @example\n * const rows = [{ pid: 10, ppid: 1, pgid: 10 }, { pid: 20, ppid: 10, pgid: 20 }]\n * collectDoomedGroups(10, rows, 5) // => [10, 20]\n */\nexport const collectDoomedGroups = (rootPid: number, rows: TopologyRow[], excludePgid?: number): number[] => {\n const childrenOf = new Map<number, TopologyRow[]>()\n\n for (const row of rows) {\n const siblings = childrenOf.get(row.ppid)\n\n if (siblings) siblings.push(row)\n else childrenOf.set(row.ppid, [row])\n }\n\n const groups = new Set<number>([rootPid])\n const seen = new Set<number>([rootPid])\n const queue = [rootPid]\n\n for (let i = 0; i < queue.length; i += 1) {\n for (const child of childrenOf.get(queue[i] ?? -1) ?? []) {\n if (seen.has(child.pid)) continue\n seen.add(child.pid)\n groups.add(child.pgid)\n queue.push(child.pid)\n }\n }\n\n return [...groups].filter((pgid) => {\n return pgid > 1 && pgid !== excludePgid\n })\n}\n\n/**\n * Snapshot the live process table; an empty list on failure degrades to group-only teardown.\n * `ps` is invoked by absolute path (not via `PATH`, unlike the `pnpm` the engines shell out to)\n * so a writable `PATH` entry can never substitute the binary we hand a kill list to.\n */\nconst snapshotProcRows = (): ProcRow[] => {\n try {\n return parseProcRows(execFileSync(PS_BIN, [...PS_ARGS], { encoding: 'utf8', maxBuffer: PS_MAX_BUFFER }))\n } catch {\n return []\n }\n}\n\n/**\n * {@link snapshotProcRows}, off the event loop. The rolling sampler runs on a timer for the whole life\n * of a dev session, and the in-process backends share this loop \u2014 a synchronous `ps` every second would\n * stall every request that lands during it. Resolves to `[]` on failure, degrading to the previous\n * snapshot rather than clobbering it.\n */\nconst snapshotProcRowsAsync = async (): Promise<ProcRow[]> => {\n return new Promise((resolve) => {\n execFile(PS_BIN, [...PS_ARGS], { encoding: 'utf8', maxBuffer: PS_MAX_BUFFER }, (error, stdout) => {\n resolve(error ? [] : parseProcRows(stdout))\n })\n })\n}\n\n/** Does `pid` currently exist and name a direct child of `parentPid`? Guards against pid reuse. */\nexport const isChildOf = (pid: number, parentPid: number, rows: TopologyRow[]): boolean => {\n return rows.some((row) => {\n return row.pid === pid && row.ppid === parentPid\n })\n}\n\n/**\n * The groups to signal for `rootPid`'s tree, excluding our own group so a mis-parsed `ps` row can\n * never make the dev-server signal itself.\n */\nconst doomedGroupsOf = (rootPid: number, rows: ProcRow[]): number[] => {\n const ownPgid = rows.find((row) => {\n return row.pid === process.pid\n })?.pgid\n\n return collectDoomedGroups(rootPid, rows, ownPgid)\n}\n\n/** Is the process group led by `pgid` still alive? (`kill(-pgid, 0)` throws ESRCH when gone.) */\nconst groupAlive = (pgid: number): boolean => {\n try {\n process.kill(-pgid, 0)\n\n return true\n } catch {\n return false\n }\n}\n\n/** Send `signal` to every named process group, ignoring groups that already exited. */\nconst signalGroups = (pgids: number[], signal: NodeJS.Signals): void => {\n for (const pgid of pgids) {\n try {\n process.kill(-pgid, signal)\n } catch {\n // Already gone (ESRCH) \u2014 nothing to signal.\n }\n }\n}\n\n/**\n * Live accessors into every currently-supervised child's rolling {@link GroupSnapshot} list \u2014 populated by\n * {@link superviseChild} on spawn, removed only once that child's own groups are CONFIRMED gone (see\n * `deregisterAfter` inside it, not the child's bare `exit` event). This is what lets\n * {@link killDescendantGroupsNow} \u2014 the second-signal force path and the crash-barrier's fatal path \u2014\n * reach a descendant that reparented to init before ITS parent's `exit` fired: a live `ppid` walk from\n * `process.pid` cannot see such a descendant (see the module doc block for why), but a snapshot recorded\n * while the parent was still alive can.\n *\n * INVARIANT \u2014 never regress this: a registry-sourced pgid is signalled ONLY after\n * {@link validSnapshotPgids} confirms its leader's start time still matches a FRESH `ps` snapshot. An\n * ungated union would SIGKILL a recycled pgid \u2014 a hazard the live walk cannot have, since it only ever\n * names processes that are alive right now.\n */\nconst registry = new Set<() => readonly GroupSnapshot[]>()\n\n/**\n * The pgids {@link killDescendantGroupsNow} should signal: every group in the live `ppid` walk from\n * `rootPid` (unchanged semantics \u2014 see {@link doomedGroupsOf}), UNIONED with every registry-sourced\n * snapshot that survives {@link validSnapshotPgids} against the same `rows`. Live-walk pgids are not\n * lstart-gated \u2014 they carry no snapshot to validate against, and are live by construction; only the\n * registry-sourced half needs the reuse guard.\n *\n * Pulled out of `killDescendantGroupsNow` as a pure function so the union/dedupe logic can be tested\n * against a fake process table and fake registry snapshots, without spawning real processes or waiting on\n * a real `ps`.\n *\n * @example\n * // pgid 20 is unreachable from rootPid (reparented to init) but was recorded before its parent died.\n * collectForceReapTargets(1000, freshRows, [[{ pgid: 20, leaderStart: '\u2026' }]]) // => [20]\n */\nexport const collectForceReapTargets = (\n rootPid: number,\n rows: ProcRow[],\n registrySnapshots: readonly (readonly GroupSnapshot[])[],\n): number[] => {\n const live = doomedGroupsOf(rootPid, rows)\n const fromRegistry = registrySnapshots.flatMap((snapshot) => {\n return validSnapshotPgids(snapshot, rows)\n })\n\n return [...new Set([...live, ...fromRegistry])]\n}\n\n/**\n * SIGKILL every process group descended from this process, right now \u2014 plus every REGISTERED\n * supervised-child snapshot that still validates, so a group that reparented to init before its parent's\n * `exit` fired is not left behind just because the live `ppid` walk can no longer see it.\n *\n * The force-quit path (a second Ctrl-C) abandons the graceful teardown, but it must not abandon the\n * CHILDREN: they hold the dev ports, and the next start would 502 against their stale portless\n * aliases. So this is the blunt counterpart to {@link superviseChild}'s teardown \u2014 no SIGTERM grace,\n * no escalation, no waiting for the kernel to reap. Straight to SIGKILL.\n *\n * Synchronous by contract: the caller exits the process on the very next line, so anything deferred\n * to a later tick would never run. Cost is one `ps` (tens of ms) plus a `kill` per group; that is the\n * whole latency budget a user who just hit Ctrl-C twice is willing to spend.\n */\nexport const killDescendantGroupsNow = (): void => {\n const rows = snapshotProcRows()\n const registrySnapshots = [...registry].map((getGroups) => {\n return getGroups()\n })\n\n signalGroups(collectForceReapTargets(process.pid, rows, registrySnapshots), 'SIGKILL')\n}\n\n/** Resolve after `ms`, used to pace the teardown polling loop. */\nconst sleep = async (ms: number): Promise<void> => {\n return new Promise((resolve) => {\n setTimeout(resolve, ms)\n })\n}\n\n/** Poll `pgids` until every group has exited or `timeoutMs` elapses; returns the survivors. */\nconst waitForExit = async (pgids: number[], timeoutMs: number): Promise<number[]> => {\n const deadline = Date.now() + timeoutMs\n let alive = pgids\n\n while (alive.length > 0 && Date.now() < deadline) {\n alive = alive.filter(groupAlive)\n if (alive.length === 0) break\n await sleep(POLL_MS)\n }\n\n return alive\n}\n\n/**\n * Called when a supervised child dies on its OWN \u2014 a natural `exit` or a spawn `error` that was\n * NOT initiated by {@link ManagedChild.kill}. `detail` is a short human description of the cause.\n * The runner uses it to surface a silently-dead `turbo watch`/`turbo run dev` engine (which stops\n * rebuilds/HMR with no other signal) instead of leaving the session looking healthy.\n */\nexport type UnexpectedExitHandler = (detail: string) => void\n\n/**\n * Wrap a detached child: `unref()` it (so it never keeps the loop alive) and return a handle\n * whose `kill()` reaps the child AND every process group turbo spawned beneath it.\n *\n * Snapshot first, signal second: after turbo exits its tasks reparent to init, so the `ppid`\n * walk that finds them must run while turbo is still alive. Then SIGTERM every doomed group,\n * poll until all are gone, and SIGKILL the survivors. Resolves once nothing is left.\n *\n * `onUnexpectedExit` (optional) fires when the child dies WITHOUT `kill()` having been called \u2014\n * the \"engine died silently\" case; the exit our own teardown causes never reaches it.\n */\n// The snapshot-first order covers the ORDERLY death, where we choose the moment and can walk the tree\n// first. It does not cover turbo CRASHING, because reparenting happens at termination \u2014 strictly before\n// Node hands us the `exit` event \u2014 so by then the walk is already blind. A rolling snapshot (see `sample`)\n// is therefore kept for the child's whole life, and both the crash path and a `kill()` on an already-dead\n// child reap from it, validating each group against its leader's start time so a recycled pgid is never\n// killed.\n//\n// A `killing` latch, set at the top of `kill()` before any signal goes out, is what suppresses\n// `onUnexpectedExit` for our own teardown, so it reports only genuine crashes.\n//\n// The `error` listener is load-bearing beyond reporting: a ChildProcess that emits `error` (e.g. `pnpm`\n// ENOENT) with no listener throws as an uncaught exception and would take the whole dev session down.\nexport function superviseChild(\n child: ChildProcess,\n graceMs: number = DEFAULT_GRACE_MS,\n onUnexpectedExit?: UnexpectedExitHandler,\n /**\n * Test-only seam: overrides the `ps` snapshot used by this child's own reap ATTEMPTS (the crash-time\n * reap in the `exit` handler, and `kill()`'s reaps) \u2014 never the rolling sampler, which always uses the\n * real one. Defaults to the real synchronous `ps` call for every production caller. Lets a test simulate\n * a transient `ps` failure (an empty read) without needing to race the real one.\n */\n snapshotRowsOverride: () => ProcRow[] = snapshotProcRows,\n): ManagedChild {\n // Don't keep the parent event loop alive on the child; the runner owns lifecycle via kill().\n child.unref()\n\n // Latched by kill() before it signals, so the child's own teardown exit is not misreported as a crash.\n let killing = false\n // Set by the `exit` listener, so a sampler callback still in flight cannot clobber the final snapshot.\n let exited = false\n /** Last known process groups beneath this child, refreshed while it is alive. See the sampler below. */\n let groups: GroupSnapshot[] = []\n let sampler: NodeJS.Timeout | null = null\n\n /** Live accessor registered in {@link registry} for the whole life of this child \u2014 see `deregisterAfter`. */\n const getGroups = (): readonly GroupSnapshot[] => {\n return groups\n }\n\n registry.add(getGroups)\n\n const stopSampling = (): void => {\n if (sampler) clearInterval(sampler)\n sampler = null\n }\n\n /**\n * Drop this child's accessor from {@link registry} once every group it ever recorded is CONFIRMED gone\n * \u2014 never merely because `exit` fired or a reap attempt ran. A reap attempt can fail to kill anything\n * for a legitimate reason (a stale/empty `ps` read) while the descendant is still very much alive, and\n * removing the entry THEN would strand it: nothing else remembers those pgids once this closure is gone.\n * Bounded by `REAP_TIMEOUT_MS` so a group this genuinely cannot reap (a foreign process now holding a\n * recycled pgid) does not pin a dead entry in the registry forever.\n */\n const deregisterAfter = async (): Promise<void> => {\n await waitForExit(\n groups.map((g) => {\n return g.pgid\n }),\n REAP_TIMEOUT_MS,\n )\n registry.delete(getGroups)\n }\n\n /**\n * Turn a fresh `ps` read into an updated {@link groups}. An empty result is discarded rather than\n * stored: `ps` failing, or racing the child's own death, must degrade to the previous snapshot, never\n * erase it. Shared by the synchronous first sample below and the async rolling {@link sample}, so the\n * two can never compute the group set differently.\n */\n const applySample = (rows: ProcRow[]): void => {\n const pid = child.pid\n\n if (pid == null || exited || killing || rows.length === 0) return\n\n const ownPgid = rows.find((row) => {\n return row.pid === process.pid\n })?.pgid\n const next = snapshotGroups(pid, rows, ownPgid)\n\n if (next.length > 0) groups = next\n }\n\n /**\n * Refresh {@link groups} while the child is alive.\n *\n * This is what makes a crash survivable. Reaping walks `ppid` from the child \u2014 but a process that\n * dies has ALREADY reparented its children to init by the time Node delivers `exit`, so a walk\n * started from the corpse finds only the corpse's own group. Verified: while turbo is alive the walk\n * yields `[turbo, vite]`; inside the `exit` handler it yields `[turbo]`, and vite (`ppid=1`, still\n * running, still holding its port) is unreachable forever. There is no window to race \u2014 the only way\n * to know vite's group after turbo dies is to have written it down BEFORE turbo died.\n *\n * Async (`snapshotProcRowsAsync`) so the rolling tick never blocks the event loop the in-process\n * backends share \u2014 see the FIRST sample below for why that leaves a narrow window this function alone\n * cannot close.\n */\n const sample = async (): Promise<void> => {\n if (child.pid == null || exited || killing) return\n\n applySample(await snapshotProcRowsAsync())\n }\n\n // Synchronous, not `void sample()`: a child that crashes within `sample()`'s first async `ps` round\n // trip (tens of ms) would otherwise have no snapshot recorded at all, and the `exit` handler's\n // `reapSnapshot(groups, \u2026)` would run against an empty `groups` and reap nothing \u2014 the exact\n // spawn\u2192first-snapshot window W11 closes. One small `ps` exec, before this function ever returns,\n // costs the same one-time block on the shared event loop that `killDescendantGroupsNow`'s synchronous\n // `ps` call already spends on every force-quit \u2014 paid once per spawn instead of once per Ctrl-C.\n applySample(snapshotProcRows())\n\n sampler = setInterval(() => {\n void sample()\n }, SAMPLE_INTERVAL_MS)\n // The timer must never be the reason the process stays alive \u2014 it outlives nothing.\n sampler.unref()\n\n const reportUnexpected = (detail: string): void => {\n if (killing) return\n onUnexpectedExit?.(detail)\n }\n\n child.on('exit', (code, signal) => {\n exited = true\n stopSampling()\n\n // The child died WITHOUT us killing it \u2014 a turbo crash, or an OOM kill. Turbo puts each of its\n // tasks in a process group of its OWN, and a turbo that dies this way reaps none of them, so\n // `vite`/`tsc` survive holding their ports. Nothing can find them by walking (see `sample`), and\n // the `kill()` below would bail on an already-exited child. Reaping from the snapshot here is the\n // only thing standing between a crashed engine and a permanently orphaned dev server.\n //\n // `deregisterAfter` (not a bare delete) \u2014 this reap attempt reading a stale/empty `ps` snapshot must\n // not be mistaken for the descendants actually being gone; the registry entry is the only remaining\n // record of them until a later confirmation (or force-reap) proves they are.\n if (!killing) {\n reapSnapshot(groups, snapshotRowsOverride())\n void deregisterAfter()\n }\n\n reportUnexpected(`exited unexpectedly (code=${code ?? 'null'}, signal=${signal ?? 'null'})`)\n })\n child.on('error', (error: Error) => {\n reportUnexpected(`failed to spawn: ${error.message}`)\n })\n\n return {\n kill: async (): Promise<void> => {\n // Latch FIRST: the child WILL emit `exit` during the reap below, and that exit is ours, not a crash.\n killing = true\n stopSampling()\n\n const pid = child.pid\n\n if (pid == null) {\n // Never spawned a real pid \u2014 nothing was ever tracked, so nothing to confirm.\n registry.delete(getGroups)\n\n return\n }\n\n // Once the child is reaped the OS may recycle `pid` onto an unrelated process, and `kill(-pid, \u2026)`\n // would then destroy a stranger's group \u2014 so an exited child is never walked. It is not simply\n // skipped either: a turbo that CRASHED left its task groups running, and this is the Ctrl-C that\n // has to clear them. The snapshot is the only surviving handle on those groups, and `reapSnapshot`\n // re-validates each one against its leader's start time, so a recycled pgid is refused.\n if (child.exitCode !== null || child.signalCode !== null) {\n reapSnapshot(groups, snapshotRowsOverride())\n await deregisterAfter()\n\n return\n }\n\n const rows = snapshotRowsOverride()\n\n // The cheap check above misses a child reaped just now, so confirm `pid` is still ours: a\n // detached child keeps us as its parent, so a foreign `ppid` means the pid was recycled.\n // Skipped when `ps` failed (empty `rows`) \u2014 then only the guard above stands. The registry entry is\n // deliberately left alone here: this state is genuinely ambiguous (did the pid really get\n // recycled, or did `ps` just fail?), and the safe default is to let a later reap re-decide with a\n // fresh read, not to guess by dropping the one record we have.\n if (rows.length > 0 && !isChildOf(pid, process.pid, rows)) return\n\n const doomed = doomedGroupsOf(pid, rows)\n\n // Negative pid \u2192 the whole group (detached made the child a group leader). Turbo forwards\n // this to its tasks; the extra task groups are signalled directly in case it doesn't.\n signalGroups(doomed, 'SIGTERM')\n\n const stragglers = await waitForExit(doomed, graceMs)\n\n if (stragglers.length === 0) {\n // Every doomed group (this child's own, plus every task group the live walk found) exited\n // within grace \u2014 confirmed by the `waitForExit` above, so no further wait is needed here.\n registry.delete(getGroups)\n\n return\n }\n\n // Re-snapshot before forcing: turbo is still alive (that's why we're here), so its tree is\n // still walkable, and `turbo watch build` may have started a task AFTER the first snapshot.\n // Union with the known stragglers, whose groups may already have left turbo's subtree.\n const late = doomedGroupsOf(pid, snapshotRowsOverride())\n const forced = [...new Set([...stragglers, ...late])].filter(groupAlive)\n\n // Killing turbo's group alone would orphan exactly these, so they are killed as a set.\n signalGroups(forced, 'SIGKILL')\n\n // A SIGKILLed leader lingers as an unreaped zombie for a few ms, and `kill(pgid, 0)` still\n // succeeds against it. Wait for the kernel to finish so callers that exit the process\n // immediately after `kill()` resolves aren't racing the teardown they just awaited.\n await waitForExit(forced, REAP_TIMEOUT_MS)\n registry.delete(getGroups)\n },\n }\n}\n", "/**\n * @fileoverview\n *\n * Signal-driven graceful shutdown, shared by every long-lived dev entry point.\n *\n * Three invariants:\n *\n * 1. Only a REPEAT OF THE SAME SIGNAL is an escape hatch. It is tempting to treat any second\n * signal as \"the user pressed Ctrl-C again, get out now\" \u2014 that was the old rule, and it\n * silently orphaned every child. A process manager in front of us relays its own signal after\n * the TTY's, so ONE Ctrl-C under `pnpm exec` arrives as SIGINT *then* SIGTERM:\n *\n * $ pnpm exec node -e \"process.on('SIGINT',\u2026); process.on('SIGTERM',\u2026)\"\n * ^C GOT SIGINT\n * GOT SIGTERM <- pnpm relaying, NOT a second keypress\n *\n * Under the old rule that relay force-quit the teardown a few hundred ms in and abandoned the\n * turbo/vite children (which sit in their own process groups). Keying the escape on the FIRST\n * signal's type fixes it without losing the hatch: a real second Ctrl-C re-sends SIGINT, and a\n * supervisor escalating SIGTERM -> SIGTERM still force-quits. `npm`/`yarn` relay the same way,\n * so this must live here, in the library, not in one consumer's `dev` script.\n * 2. Force-quitting must still not orphan. Even a genuine double Ctrl-C leaves children holding\n * ports (the next start then 502s), so the force path SIGKILLs the descendant process groups\n * before exiting. Bounded and synchronous \u2014 no grace, no escalation, no waiting for reaping.\n * 3. A signal-terminated process must not report exit code 0. The old handlers ran\n * `finally { process.exit(0) }`, which also swallowed a rejecting teardown without a word.\n * Here the rejection goes to stderr and the code stays `128 + signo` \u2014 even on a failed\n * teardown, because the process genuinely WAS signal-terminated, no conventional code means\n * \"signal-terminated but cleanup failed\", and inventing one would break the SIGTERM -> 143\n * contract supervisors rely on. The failure is surfaced by logging, not by the code.\n */\nimport { constants } from 'node:os'\nimport process from 'node:process'\n\nimport { killDescendantGroupsNow } from 'src/dev/managed-child'\n\n/**\n * The signals a resident dev process tears down on.\n *\n * SIGHUP is here for invariant 2: the terminal going away (window closed, ssh dropped) is delivered to\n * the foreground process group, so `dev` gets it \u2014 but its turbo/vite children DON'T, because they sit\n * in their own detached process groups. Without a handler, `dev` takes SIGHUP's default kill, never runs\n * `killDescendantGroupsNow`, and leaves the whole tree alive holding the dev ports; the next start then\n * 502s. Closing a terminal is the single most common way to walk away from a dev server, so this is the\n * likeliest orphan path, not an exotic one.\n */\nconst HANDLED_SIGNALS: readonly NodeJS.Signals[] = ['SIGINT', 'SIGTERM', 'SIGHUP']\n\n/**\n * `os.constants.signals` typed for lookup by an arbitrary {@link NodeJS.Signals}. The platform\n * table omits signals that do not exist on the host, so the index is genuinely partial.\n */\nconst SIGNAL_NUMBERS: Partial<Record<NodeJS.Signals, number>> = constants.signals\n\n/** Fallback signal number when the host table lacks the signal: SIGINT, yielding the usual 130. */\nconst FALLBACK_SIGNO = 2\n\n/**\n * The POSIX exit code for a process terminated by `signal`: `128 + signo`.\n *\n * A shell reports SIGINT as 130 and SIGTERM as 143. Exiting 0 after a signal tells a supervisor\n * the process stopped voluntarily, which is a lie.\n *\n * @example\n * exitCodeForSignal('SIGINT') // 130\n * exitCodeForSignal('SIGTERM') // 143\n */\nexport const exitCodeForSignal = (signal: NodeJS.Signals): number => {\n return 128 + (SIGNAL_NUMBERS[signal] ?? FALLBACK_SIGNO)\n}\n\n/**\n * Seams for {@link registerSignalShutdown}. Each exists because the real implementation is\n * untestable in-process: `process.exit` kills the test runner, and delivering a real POSIX\n * signal to the runner is unsafe and flaky.\n */\nexport interface SignalShutdownDeps {\n /** The context-specific teardown. May reject; the rejection is logged, never swallowed. */\n onSignal: (signal: NodeJS.Signals) => Promise<void>\n /**\n * Terminate the process. Typed as returning `void`, not `never`, so a test can pass a plain\n * recording fake: a `never` fake would have to throw a sentinel, and that throw would reject the\n * in-flight teardown promise. Nothing meaningful runs after a call, so a returning fake is safe.\n */\n exit?: (code: number) => void\n /** Subscribe `handler` to `signal`. Defaults to `process.on`. */\n register?: (signal: NodeJS.Signals, handler: () => void) => void\n /**\n * Unsubscribe `handler` from `signal`. Defaults to `process.off`. Called once the teardown this module\n * owns is over \u2014 graceful completion, the force-quit escape, or the deadline \u2014 so a process whose\n * `exit` seam does not actually terminate (a test, or a caller with its own reasons to keep running)\n * is not left with a dangling SIGINT/SIGTERM/SIGHUP handler from a session that already ended.\n */\n unregister?: (signal: NodeJS.Signals, handler: () => void) => void\n /**\n * Reap surviving descendant process groups on the force-quit path, where the normal teardown is\n * abandoned mid-flight. Synchronous by contract: the process exits on the next line, so anything\n * deferred to a later tick would never run. Defaults to {@link killDescendantGroupsNow}.\n */\n forceReap?: () => void\n /** How long a teardown may run before it is force-quit. See {@link TEARDOWN_DEADLINE_MS}. */\n teardownDeadlineMs?: number\n /**\n * Timer seam, returning its own cancel. Defaults to `setTimeout`/`clearTimeout`; a test injects a fake\n * so the deadline is fired deliberately rather than waited out.\n */\n setTimer?: (handler: () => void, ms: number) => () => void\n /**\n * Name the teardown step still in flight when the deadline trips \u2014 the CLI entry wires this to\n * `runner.shutdownStage`.\n *\n * This module holds NO reference to the runner and so cannot read a stage on its own. Without the seam\n * the deadline degrades into a plain force-quit and the incident's central question (\"`shutdown()`\n * demonstrably ran \u2014 so where did it wedge?\") survives the fix meant to answer it.\n */\n describeStall?: () => string\n /**\n * File the deadline report where it can still be read. The entry routes it into the per-service log sink:\n * on the path this deadline exists for, the terminal is exactly what is gone, so stderr is not a channel.\n * Defaults to a no-op \u2014 library code has no sink to file into.\n */\n fileReport?: (text: string) => void\n}\n\n/**\n * How long a teardown may run before the process force-quits: 20 s, well past a healthy reap (a turbo tree\n * escalates SIGTERM\u2192SIGKILL per child, seconds at worst) and far short of \"forever\".\n *\n * This module used to argue that NO deadline should be armed: *\"any value safe enough not to preempt a\n * legitimate teardown would fire later than a user's second Ctrl-C, and a supervisor sends its own SIGKILL\n * on its own grace.\"* That reasoning quietly ASSUMES A HUMAN IS PRESENT to press Ctrl-C twice \u2014 and the\n * incident that put this here is precisely the case where there is none: the terminal is gone, the operator\n * is not watching, and no supervisor sits above an interactive `ik dev`. Five processes took SIGHUP, ran\n * `shutdown()`, wedged, and spun for five hours writing 455 GB. The escape hatch the old design leaned on\n * cannot be pressed by anyone who has already walked away.\n */\nconst TEARDOWN_DEADLINE_MS = 20_000\n\nconst defaultExit = (code: number): void => {\n process.exit(code)\n}\n\nconst defaultRegister = (signal: NodeJS.Signals, handler: () => void): void => {\n process.on(signal, handler)\n}\n\nconst defaultUnregister = (signal: NodeJS.Signals, handler: () => void): void => {\n process.off(signal, handler)\n}\n\nconst defaultSetTimer = (handler: () => void, ms: number): (() => void) => {\n const timer = setTimeout(handler, ms)\n\n // `unref` so the deadline itself never HOLDS the process open: if the loop empties, the teardown is done\n // and Node exits on its own. A wedged teardown always has live handles (children, listening servers), so\n // the timer phase is still reached \u2014 the probe proved the fault storm does not starve it either.\n timer.unref()\n\n return (): void => {\n clearTimeout(timer)\n }\n}\n\n/**\n * Write to stderr, swallowing a failed write.\n *\n * `stderr.write` can throw (EPIPE, when the parent closed the pipe). Every caller here is on the\n * exit path, and failing to REPORT something must never break that path \u2014 the process still has to\n * die with the right code. A lost message is recoverable; a process that survives its own SIGINT\n * is not.\n */\nconst writeStderr = (message: string): void => {\n try {\n process.stderr.write(message)\n } catch {\n // stderr is gone; there is nowhere left to report to.\n }\n}\n\n/** Report a rejected teardown \u2014 message AND stack, so a wedge is never silent. */\nconst writeTeardownFailure = (signal: NodeJS.Signals, error: unknown): void => {\n const detail = error instanceof Error ? `${error.message}\\n${error.stack ?? '(no stack)'}` : String(error)\n\n writeStderr(`\\n\u2717 Teardown failed while handling ${signal}: ${detail}\\n`)\n}\n\n/**\n * Wire SIGINT/SIGTERM to `onSignal`, then exit `128 + signo`.\n *\n * The first signal runs the teardown. While it is in flight, a signal of a DIFFERENT type is a\n * relay from the process manager above us (`pnpm exec` turns one Ctrl-C into SIGINT + SIGTERM) and\n * is ignored, so the teardown survives to reap its children. A repeat of the FIRST signal is the\n * real escape hatch: it force-quits without touching `onSignal` again, SIGKILLing the descendant\n * groups on the way out so nothing is left holding a port.\n *\n * A {@link TEARDOWN_DEADLINE_MS} deadline IS armed \u2014 read its doc block before removing it.\n *\n * Returns a `detach` function that drops the SIGINT/SIGTERM/SIGHUP listeners this call installed. Every\n * exit path already calls it before exiting, so a real process never needs it.\n *\n * @example\n * registerSignalShutdown({\n * onSignal: async (signal) => {\n * process.stdout.write(`Received ${signal}, shutting down...`)\n * await runner.shutdown()\n * },\n * describeStall: () => runner.shutdownStage,\n * })\n */\n// On the deadline: this module used to explain at length why one should NOT be armed. That explanation\n// assumed a human at the keyboard to press Ctrl-C a second time; the incident it now guards against is the\n// case where nobody is there. Mechanically it depends on `terminal-liveness`: with a fault storm live,\n// teardown competes with ~100k blocking `writeSync` calls per second, and a deadline shipped into a\n// machine that cannot honour it is decoration. The storm is cut at its source first; this is what stops\n// the wedge that follows.\n//\n// On `detach`: it exists for a caller whose `exit` seam does NOT terminate the process (every test in this\n// suite), so a session that already ended does not leave dangling handlers behind for the next one in the\n// same process. In a real process `exit` terminates first.\nexport const registerSignalShutdown = ({\n onSignal,\n exit = defaultExit,\n register = defaultRegister,\n unregister = defaultUnregister,\n forceReap = killDescendantGroupsNow,\n teardownDeadlineMs = TEARDOWN_DEADLINE_MS,\n setTimer = defaultSetTimer,\n describeStall = () => {\n return 'unknown'\n },\n fileReport = () => {},\n}: SignalShutdownDeps): (() => void) => {\n /** The signal that opened the teardown \u2014 `null` until the first one lands. */\n let firstSignal: NodeJS.Signals | null = null\n /** Cancels the armed deadline \u2014 on a completed teardown, and on the second-signal escape. */\n let cancelDeadline: (() => void) | null = null\n /** Every handler this call installed, so `detach` can drop exactly those and nothing else. */\n const handlers = new Map<NodeJS.Signals, () => void>()\n\n /**\n * Drop every listener this call installed. Runs at the end of EVERY exit path below (graceful\n * completion, the second-signal escape, and the deadline) \u2014 not just the happy one \u2014 so a caller whose\n * `exit` seam does not actually terminate the process (a test, or a future non-terminating consumer)\n * is never left holding SIGINT/SIGTERM/SIGHUP handlers from a session that already ended. Idempotent:\n * a second call finds an empty map and does nothing.\n */\n const detach = (): void => {\n for (const [signal, handler] of handlers) {\n unregister(signal, handler)\n }\n handlers.clear()\n }\n\n const handle = (signal: NodeJS.Signals): void => {\n if (firstSignal !== null) {\n // A DIFFERENT signal while tearing down is the process manager in front of us relaying its\n // own (pnpm/npm turn one Ctrl-C into SIGINT + SIGTERM), never a second keypress. Ignoring it\n // is what lets the teardown finish and reap the children. Only a repeat of the signal that\n // STARTED the teardown is the user (or a supervisor) genuinely asking again.\n if (signal !== firstSignal) return\n\n writeStderr(`\\n\u26A0 Received ${signal} again \u2014 force-quitting.\\n`)\n cancelDeadline?.()\n // Abandoning the teardown must still not abandon the children: they hold the dev ports, and\n // the next start would 502 against their stale portless aliases.\n forceReap()\n detach()\n exit(exitCodeForSignal(signal))\n\n return\n }\n\n firstSignal = signal\n\n // The human's escape hatch, armed for the case where there is no human. Same destination as a second\n // Ctrl-C \u2014 reap the descendant groups, exit `128 + signo` \u2014 but it also NAMES the step that wedged, and\n // files that name where it can still be read once the terminal is gone.\n cancelDeadline = setTimer(() => {\n const report = `\\n\u26A0 Teardown deadline exceeded at stage=${describeStall()} after ${teardownDeadlineMs}ms \u2014 force-quitting.\\n`\n\n writeStderr(report)\n fileReport(report)\n forceReap()\n detach()\n exit(exitCodeForSignal(signal))\n }, teardownDeadlineMs)\n\n void (async (): Promise<void> => {\n try {\n await onSignal(signal)\n } catch (error) {\n writeTeardownFailure(signal, error)\n } finally {\n // `finally`, so the exit survives anything the catch body might later throw: a process must\n // never outlive its own signal. Not the old `finally { process.exit(0) }` bug \u2014 that one\n // exited zero without logging; this logs first, then exits the honest code.\n cancelDeadline?.()\n detach()\n exit(exitCodeForSignal(signal))\n }\n })()\n }\n\n for (const signal of HANDLED_SIGNALS) {\n const handler = (): void => {\n return handle(signal)\n }\n\n handlers.set(signal, handler)\n register(signal, handler)\n }\n\n return detach\n}\n", "/**\n * Names of the error classes thrown when an interactive prompt ends without a\n * value. From `@inquirer/core`: `ExitPromptError` (Ctrl-C \u2014 and ONLY Ctrl-C:\n * inquirer binds no escape key, so it raises this from readline's SIGINT) and\n * `AbortPromptError` (the prompt was aborted via an `AbortSignal`, which is how\n * Esc arrives \u2014 see lib/prompts/escapable-context). From our own Ink pickers:\n * `PromptCancelledError` (see ./prompt-cancelled-error), which is registered here\n * rather than impersonating an inquirer class name. All are intentional\n * cancellations, not failures.\n */\nconst CANCELLATION_ERROR_NAMES = new Set(['ExitPromptError', 'AbortPromptError', 'PromptCancelledError'])\n\nconst hasCancellationName = (value: unknown): boolean => {\n return value instanceof Error && CANCELLATION_ERROR_NAMES.has(value.name)\n}\n\n/**\n * True when `error` represents a user (or signal) cancellation of an interactive\n * prompt \u2014 Ctrl-C anywhere, or Esc, which reaches an `@inquirer` prompt as an\n * abort and an Ink picker as a `PromptCancelledError`. Matched by `name` rather\n * than `instanceof` so it stays correct\n * even when pnpm dedupes more than one copy of `@inquirer/core` into the tree\n * (an `instanceof` check fails across realms/duplicate classes).\n *\n * Also unwraps one level of `cause`, so a cancellation re-wrapped in an\n * {@link ./operation-error.OperationError} is still recognised at the top-level\n * error boundary.\n *\n * @example\n * try {\n * await checkbox({ message: 'Select release branches', choices })\n * } catch (err) {\n * if (isPromptCancellation(err)) process.exit(0) // clean back-out, not an error\n * throw err\n * }\n */\nexport const isPromptCancellation = (error: unknown): boolean => {\n if (hasCancellationName(error)) return true\n\n const cause = (error as { cause?: unknown } | null | undefined)?.cause\n\n return hasCancellationName(cause)\n}\n", "import { $ } from 'zx'\n\nimport { logger } from 'src/lib/logger'\n\n/**\n * The only place that spawns `orca`. Everything the driver knows about the CLI is\n * encoded here:\n *\n * - infra-kit owns git; Orca owns windows. The `orca worktree` subcommand's\n * `create` and `rm` verbs are forbidden in this codebase \u2014 `rm` also deletes the\n * local branch, and `create` picks paths/branch names infra-kit's worktree\n * layout does not control (docs/orca-migration-plan.md \u00A71, principle 1).\n * - The JSON envelope is the contract, never the exit code: `ok:false` came back\n * with exit 0 on 1.4.203 and exit 1 on 1.4.204, and the app auto-updates\n * unattended (docs/orca-cli-findings.md, \"Additional facts\").\n */\n\nexport interface OrcaErrorPayload {\n code: string\n message: string\n data?: unknown\n}\n\ninterface OrcaEnvelope<T> {\n ok: boolean\n result?: T\n error?: OrcaErrorPayload\n}\n\n/** Orca answered with `{ ok: false, error }`. */\nexport class OrcaError extends Error {\n readonly code: string\n readonly data: unknown\n\n constructor(payload: OrcaErrorPayload) {\n super(payload.message)\n this.name = 'OrcaError'\n this.code = payload.code\n this.data = payload.data\n }\n}\n\n/** Orca ran but stdout was not an envelope \u2014 a crash, a `--help` fallback, or a CLI too old for `--json`. */\nexport class OrcaMalformedError extends Error {\n readonly exitCode: number | null\n readonly stderrTail: string\n\n constructor(exitCode: number | null, stderrTail: string) {\n super(`orca: stdout is not a JSON envelope (exit ${exitCode ?? 'signal'})`)\n this.name = 'OrcaMalformedError'\n this.exitCode = exitCode\n this.stderrTail = stderrTail\n }\n}\n\n/** The `orca` binary is not on PATH (spawn ENOENT, or the shell's exit 127). */\nexport class OrcaAbsentError extends Error {\n constructor() {\n super('orca: command not found \u2014 the CLI is registered from Orca \u2192 Settings \u2192 Experimental \u2192 CLI')\n this.name = 'OrcaAbsentError'\n }\n}\n\nconst STDERR_TAIL_CHARS = 400\nconst SHELL_COMMAND_NOT_FOUND = 127\n\ninterface SpawnOutput {\n exitCode: number | null\n stdout: string\n stderr: string\n}\n\nconst isEnoent = (error: unknown): boolean => {\n return typeof error === 'object' && error !== null && (error as { code?: unknown }).code === 'ENOENT'\n}\n\nconst spawnOrca = async (argv: string[]): Promise<SpawnOutput> => {\n try {\n // zx runs through a shell, so a missing binary normally surfaces as exit 127 rather than a\n // rejection; the catch covers the shell-less spawn failure shape.\n return await $({ nothrow: true, quiet: true })`orca ${argv} --json`\n } catch (error) {\n if (isEnoent(error)) {\n throw new OrcaAbsentError()\n }\n\n throw error\n }\n}\n\nconst parseEnvelope = <T>(stdout: string): OrcaEnvelope<T> | null => {\n try {\n const parsed: unknown = JSON.parse(stdout)\n\n if (typeof parsed === 'object' && parsed !== null && typeof (parsed as { ok?: unknown }).ok === 'boolean') {\n return parsed as OrcaEnvelope<T>\n }\n\n return null\n } catch {\n return null\n }\n}\n\n/**\n * Run one `orca` command and return its `result`. Throws {@link OrcaError} on an\n * `ok:false` envelope, {@link OrcaMalformedError} when stdout is not an envelope,\n * and {@link OrcaAbsentError} when the binary is missing \u2014 regardless of exit code.\n */\nexport const runOrca = async <T>(argv: string[]): Promise<T> => {\n const output = await spawnOrca(argv)\n const envelope = parseEnvelope<T>(output.stdout)\n\n logger.debug({ argv, ok: envelope?.ok, code: envelope?.error?.code, exitCode: output.exitCode }, 'orca')\n\n if (!envelope) {\n if (output.exitCode === SHELL_COMMAND_NOT_FOUND) {\n throw new OrcaAbsentError()\n }\n\n throw new OrcaMalformedError(output.exitCode, output.stderr.slice(-STDERR_TAIL_CHARS))\n }\n\n if (!envelope.ok) {\n throw new OrcaError(envelope.error ?? { code: 'unknown', message: 'orca: ok:false without an error payload' })\n }\n\n return envelope.result as T\n}\n", "import { logger } from 'src/lib/logger'\n\nimport { OrcaAbsentError, runOrca } from './run-orca'\n\nexport type OrcaProbe = 'absent' | 'unreachable' | 'ready'\n\ninterface OrcaStatus {\n app?: { running?: boolean }\n runtime?: { reachable?: boolean; appVersion?: string }\n}\n\n/**\n * Three-state readiness probe behind every fallback branch and doctor's Orca row.\n * `ready` needs both halves: a headless `orca serve` runtime is reachable but has\n * no UI to adopt a tab, so it reads as `unreachable`. `runtime.appVersion` is the\n * only trustworthy version source (`ORCA_APP_VERSION` is spawn-time stale).\n */\nexport const probeOrca = async (): Promise<OrcaProbe> => {\n try {\n const status = await runOrca<OrcaStatus>(['status'])\n\n logger.debug({ appVersion: status.runtime?.appVersion }, 'orca: status')\n\n return status.app?.running === true && status.runtime?.reachable === true ? 'ready' : 'unreachable'\n } catch (error) {\n if (error instanceof OrcaAbsentError) {\n return 'absent'\n }\n\n logger.debug({ error }, 'orca: status probe failed')\n\n return 'unreachable'\n }\n}\n", "import { realpath } from 'node:fs/promises'\n\n/**\n * Resolve `path` through the filesystem, falling back to the input when it can't\n * be resolved (e.g. the directory was already removed). Both Orca-reported paths\n * and the caller's target are normalized the same way so a macOS\n * `/var`\u2192`/private/var` symlink can't make two equal paths compare unequal.\n *\n * Selectors are NOT built from this \u2014 they take the path as `git worktree list`\n * reports it; realpath is for equality only.\n */\nexport const realpathForOrcaCwd = async (path: string): Promise<string> => {\n try {\n return await realpath(path)\n } catch {\n return path\n }\n}\n", "import { realpathForOrcaCwd } from './realpath-for-orca-cwd'\nimport { runOrca } from './run-orca'\n\nexport type OrcaRepoVisibility = 'show' | 'hide'\n\ninterface OrcaRepoRow {\n path: string\n externalWorktreeVisibility?: OrcaRepoVisibility\n}\n\ninterface OrcaRepoListResult {\n repos?: OrcaRepoRow[]\n}\n\ninterface OrcaRepoAddResult {\n repo?: OrcaRepoRow\n}\n\nexport interface OrcaRepoLookup {\n registered: boolean\n visibility?: OrcaRepoVisibility\n}\n\n/**\n * Whether Orca knows the repo at `mainRepoRoot`, and whether its external\n * worktrees (infra-kit's `<repo>-worktrees/*`) are shown in the sidebar. Split\n * from {@link addOrcaRepo} so a caller can put \"will register <repo> in Orca\" in\n * its confirm preview before mutating \u2014 `repo add` carries hook policy defaults.\n */\nexport const findOrcaRepo = async (mainRepoRoot: string): Promise<OrcaRepoLookup> => {\n const [{ repos = [] }, wanted] = await Promise.all([\n runOrca<OrcaRepoListResult>(['repo', 'list']),\n realpathForOrcaCwd(mainRepoRoot),\n ])\n\n for (const repo of repos) {\n if ((await realpathForOrcaCwd(repo.path)) === wanted) {\n return { registered: true, visibility: repo.externalWorktreeVisibility }\n }\n }\n\n return { registered: false }\n}\n\n/** Registers the repo; the returned visibility is `hide` for a fresh row (measured). */\nexport const addOrcaRepo = async (mainRepoRoot: string): Promise<{ visibility?: OrcaRepoVisibility }> => {\n const { repo } = await runOrca<OrcaRepoAddResult>(['repo', 'add', '--path', mainRepoRoot])\n\n return { visibility: repo?.externalWorktreeVisibility }\n}\n", "import { realpathForOrcaCwd } from './realpath-for-orca-cwd'\nimport { runOrca } from './run-orca'\n\ninterface OrcaWorktreeListResult {\n worktrees?: Array<{ path: string }>\n}\n\nconst WORKTREE_LIST_LIMIT = '500'\n\n/**\n * Whether the sidebar shows the worktree at `cwd`. Distinct from \"the selector\n * resolves it\": a hidden external worktree still answers `terminal create` with a\n * tab nobody can see (docs/orca-cli-findings.md, gate outcome), so callers use\n * this to report `orcaHidden` instead of a silent success.\n */\nexport const isOrcaWorktreeListed = async (mainRepoRoot: string, cwd: string): Promise<boolean> => {\n const [{ worktrees = [] }, wanted] = await Promise.all([\n runOrca<OrcaWorktreeListResult>([\n 'worktree',\n 'list',\n '--repo',\n `path:${mainRepoRoot}`,\n '--limit',\n WORKTREE_LIST_LIMIT,\n ]),\n realpathForOrcaCwd(cwd),\n ])\n\n for (const worktree of worktrees) {\n if ((await realpathForOrcaCwd(worktree.path)) === wanted) {\n return true\n }\n }\n\n return false\n}\n", "import process from 'node:process'\n\nimport { realpathForOrcaCwd } from './realpath-for-orca-cwd'\n\nconst WORKTREE_ID_SEPARATOR = '::'\n\n/**\n * The path half of `ORCA_WORKTREE_ID` (`<repoId>::<abs path>`), or `null` when the\n * caller is not inside an Orca terminal. The id is per-terminal env \u2014 `cd` and\n * `-C` never change it \u2014 which is exactly why it identifies the terminal a\n * removal would destroy, and why \"re-run from elsewhere\" is the only remediation.\n */\nconst orcaCallerWorktreePath = (env: NodeJS.ProcessEnv): string | null => {\n const id = env.ORCA_WORKTREE_ID\n\n if (!id) {\n return null\n }\n\n const separatorAt = id.indexOf(WORKTREE_ID_SEPARATOR)\n\n if (separatorAt < 0) {\n return null\n }\n\n const path = id.slice(separatorAt + WORKTREE_ID_SEPARATOR.length)\n\n return path.length > 0 ? path : null\n}\n\n/**\n * The first of `paths` that is the caller's own Orca worktree, or `null`. Runs in\n * the command preflight of `worktrees remove` / `release-remove` \u2014 before the\n * confirm prompt and before any git call \u2014 because closing that worktree's\n * terminals would kill the shell running infra-kit.\n */\nexport const orcaCallerInsideTargets = async (\n paths: string[],\n env: NodeJS.ProcessEnv = process.env,\n): Promise<string | null> => {\n const callerPath = orcaCallerWorktreePath(env)\n\n if (callerPath === null) {\n return null\n }\n\n const caller = await realpathForOrcaCwd(callerPath)\n\n for (const path of paths) {\n if ((await realpathForOrcaCwd(path)) === caller) {\n return path\n }\n }\n\n return null\n}\n", "import { logger } from 'src/lib/logger'\n\nimport { OrcaError, runOrca } from './run-orca'\n\nexport interface OrcaTerminal {\n handle: string\n title: string\n tabId: string\n connected: boolean\n orphaned: boolean\n}\n\ninterface OrcaTerminalListResult {\n terminals?: OrcaTerminal[]\n truncated?: boolean\n}\n\nconst TERMINAL_LIST_LIMIT = '200'\n\n/**\n * The terminals Orca has open in the worktree at `cwd`. A `selector_not_found`\n * is \"nothing open\" (probe semantics \u2014 the row may not exist yet, or was pruned);\n * any other Orca failure propagates. `truncated` is surfaced, not resolved: a\n * truncated list still proves the worktree is open.\n */\nexport const listOrcaTerminals = async (cwd: string): Promise<{ terminals: OrcaTerminal[]; truncated: boolean }> => {\n try {\n const result = await runOrca<OrcaTerminalListResult>([\n 'terminal',\n 'list',\n '--worktree',\n `path:${cwd}`,\n '--limit',\n TERMINAL_LIST_LIMIT,\n ])\n\n const truncated = result.truncated === true\n\n if (truncated) {\n logger.debug({ cwd, limit: TERMINAL_LIST_LIMIT }, 'orca: terminal list truncated')\n }\n\n return { terminals: result.terminals ?? [], truncated }\n } catch (error) {\n if (error instanceof OrcaError && error.code === 'selector_not_found') {\n return { terminals: [], truncated: false }\n }\n\n throw error\n }\n}\n", "import { logger } from 'src/lib/logger'\n\nimport type { OrcaProbe } from './availability'\nimport { listOrcaTerminals } from './list-terminals-by-cwd'\nimport { OrcaAbsentError, OrcaError, OrcaMalformedError, runOrca } from './run-orca'\n\nexport type CloseOrcaWorktreeTerminalsOutcome =\n | { closed: true; count: number }\n | { closed: false; skipped: 'absent' | 'unreachable' | 'not_found' }\n | { closed: false; error: OrcaError }\n\ntype RetireOutcome = 'retired' | 'not_found'\n\n/**\n * `--all` refuses (`terminal_close_incomplete`) while any terminal is live and\n * on an empty row only retires the tab surfaces and resume records \u2014 so it runs\n * LAST, after every handle was closed one by one, and its refusal is not a\n * failure (docs/orca-cli-findings.md \u00A73.4).\n */\nconst retireSurfaces = async (cwd: string): Promise<RetireOutcome> => {\n try {\n await runOrca(['terminal', 'close', '--worktree', `path:${cwd}`, '--all'])\n\n return 'retired'\n } catch (error) {\n if (error instanceof OrcaError && error.code === 'selector_not_found') {\n return 'not_found'\n }\n\n if (error instanceof OrcaError && error.code === 'terminal_close_incomplete') {\n logger.debug({ cwd, error }, 'orca: surfaces not retired')\n\n return 'retired'\n }\n\n throw error\n }\n}\n\nconst closeEach = async (cwd: string): Promise<CloseOrcaWorktreeTerminalsOutcome> => {\n const { terminals } = await listOrcaTerminals(cwd)\n\n // Sequential on purpose: the tab is torn down pane by pane and Orca serializes these anyway.\n for (const terminal of terminals) {\n await runOrca(['terminal', 'close', '--terminal', terminal.handle])\n }\n\n const retired = await retireSurfaces(cwd)\n\n if (terminals.length === 0 && retired === 'not_found') {\n return { closed: false, skipped: 'not_found' }\n }\n\n return { closed: true, count: terminals.length }\n}\n\nconst asOutcome = (cwd: string, error: unknown): CloseOrcaWorktreeTerminalsOutcome => {\n if (error instanceof OrcaAbsentError) {\n return { closed: false, skipped: 'absent' }\n }\n\n if (error instanceof OrcaError) {\n return { closed: false, error }\n }\n\n if (error instanceof OrcaMalformedError) {\n return {\n closed: false,\n error: new OrcaError({\n code: 'orca_malformed_output',\n message: error.message,\n data: { exitCode: error.exitCode, stderrTail: error.stderrTail },\n }),\n }\n }\n\n logger.debug({ cwd, error }, 'orca: close failed outside the Orca contract')\n\n return {\n closed: false,\n error: new OrcaError({\n code: 'orca_close_failed',\n message: error instanceof Error ? error.message : String(error),\n }),\n }\n}\n\n/**\n * Tears down every Orca terminal of the worktree at `cwd` \u2014 called right before\n * `git worktree remove`, and nowhere else (a closed GUI is never a precondition\n * for anything that survives). Never throws: the caller decides whether an error\n * outcome blocks git, and a probe that is not `ready` skips without spawning.\n * The Orca-side row is pruned by Orca itself once the directory is gone.\n */\nexport const closeOrcaWorktreeTerminals = async (\n cwd: string,\n probe: OrcaProbe,\n): Promise<CloseOrcaWorktreeTerminalsOutcome> => {\n if (probe !== 'ready') {\n return { closed: false, skipped: probe }\n }\n\n try {\n return await closeEach(cwd)\n } catch (error) {\n return asOutcome(cwd, error)\n }\n}\n", "import { setTimeout as sleep } from 'node:timers/promises'\n\nimport { OrcaError, runOrca } from './run-orca'\n\nexport type OrcaPanes = 'two-columns' | 'three-pane'\nexport type OrcaOpenedLayout = 'full' | 'single-pane'\n\n/**\n * Shared per batch (one `worktrees add --all`): a fresh worktree\n * that `git worktree add` created milliseconds ago may not be selectable yet, so\n * `terminal create` is retried on `selector_not_found`. The cause of an\n * exhaustion (Orca not scanning the repo) is per-repo, so after the first one the\n * remaining targets attempt once \u2014 worst case `1 \u00D7 budget + (N\u22121) \u00D7 0`.\n */\nexport interface OrcaOpenPoll {\n attempts: number\n delayMs: number\n exhausted: boolean\n}\n\nconst DEFAULT_POLL_ATTEMPTS = 3\nconst DEFAULT_POLL_DELAY_MS = 300\n\nexport const createOrcaOpenPoll = (\n overrides: Partial<Pick<OrcaOpenPoll, 'attempts' | 'delayMs'>> = {},\n): OrcaOpenPoll => {\n return {\n attempts: overrides.attempts ?? DEFAULT_POLL_ATTEMPTS,\n delayMs: overrides.delayMs ?? DEFAULT_POLL_DELAY_MS,\n exhausted: false,\n }\n}\n\ninterface OpenOrcaWorktreeTerminalsArgs {\n cwd: string\n title: string\n /**\n * `--focus` steals the window; callers pass `false` on every multi-branch\n * `worktrees add` fan-out. It must also stay `false` on a row the sidebar hides \u2014\n * there `--focus` times out after ~10 s (docs/orca-cli-findings.md, axis 1).\n */\n focus: boolean\n layout: OrcaOpenedLayout\n /** The configured pane preset; only consulted when `layout` is `full`. */\n panes: OrcaPanes\n poll?: OrcaOpenPoll\n}\n\nexport interface OrcaTerminalCreateResult {\n terminal: { handle: string }\n}\n\nexport interface OrcaTerminalSplitResult {\n split: { handle: string }\n}\n\ntype CreateOutcome = { created: OrcaTerminalCreateResult } | { notSelectable: OrcaError }\n\nconst tryCreate = async (argv: string[]): Promise<CreateOutcome> => {\n try {\n return { created: await runOrca<OrcaTerminalCreateResult>(argv) }\n } catch (error) {\n if (error instanceof OrcaError && error.code === 'selector_not_found') {\n return { notSelectable: error }\n }\n\n throw error\n }\n}\n\nconst createWithPoll = async (argv: string[], cwd: string, poll: OrcaOpenPoll): Promise<string> => {\n const attempts = poll.exhausted ? 1 : poll.attempts\n const schedule = Array.from({ length: attempts }, (_, index) => {\n return index + 1\n })\n\n let lastRefusal: OrcaError | undefined\n\n for (const attempt of schedule) {\n const outcome = await tryCreate(argv)\n\n if ('created' in outcome) {\n return outcome.created.terminal.handle\n }\n\n lastRefusal = outcome.notSelectable\n\n if (attempt < attempts) {\n await sleep(poll.delayMs)\n }\n }\n\n poll.exhausted = true\n\n throw new OrcaError({\n code: 'orca_worktree_not_selectable',\n message: `orca: worktree ${cwd} is not selectable after ${attempts} attempt(s): ${lastRefusal?.message ?? ''}`,\n data: { cwd, attempts, cause: lastRefusal?.data },\n })\n}\n\nconst split = async (handle: string, direction: 'horizontal' | 'vertical'): Promise<string> => {\n const result = await runOrca<OrcaTerminalSplitResult>([\n 'terminal',\n 'split',\n '--terminal',\n handle,\n '--direction',\n direction,\n ])\n\n return result.split.handle\n}\n\n/**\n * Opens terminals for the worktree at `cwd` in one Orca tab and returns their\n * handles (`handles[0]` is the tab's first pane). `full` lays out the preset:\n * `horizontal` on `h0` puts a pane to the RIGHT, and `three-pane` adds a\n * `vertical` split on the same `h0` for the pane BELOW it (measured tree:\n * `horizontal(vertical(h0, h2), h1)`).\n *\n * A background (non-`--focus`) handle is splittable in the same tab, so no\n * layout depends on focus.\n */\nexport const openOrcaWorktreeTerminals = async (\n args: OpenOrcaWorktreeTerminalsArgs,\n): Promise<{ handles: string[]; layout: OrcaOpenedLayout }> => {\n const { cwd, title, focus, layout, panes, poll = createOrcaOpenPoll() } = args\n\n const createArgv = [\n 'terminal',\n 'create',\n '--worktree',\n `path:${cwd}`,\n '--title',\n title,\n ...(focus ? ['--focus'] : []),\n ]\n\n const first = await createWithPoll(createArgv, cwd, poll)\n const handles = [first]\n\n if (layout === 'full') {\n handles.push(await split(first, 'horizontal'))\n\n if (panes === 'three-pane') {\n handles.push(await split(first, 'vertical'))\n }\n }\n\n return { handles, layout }\n}\n", "import { displayLabel, parseBranchName } from 'src/lib/release-id'\n\ninterface BuildOrcaTerminalTitleArgs {\n branch: string\n}\n\n/**\n * The Orca TAB title for a worktree's terminals \u2014 release branches render via\n * their release-id label (`release/v1.48.0` \u2192 `1.48.0`), anything else is the raw\n * branch. It labels the tab only: the sidebar row keeps Orca's automatic branch\n * name, and titles are never an ownership key because Orca programs rewrite the\n * per-terminal title at runtime (`\u2733 Claude Code`).\n */\nexport const buildOrcaTerminalTitle = (args: BuildOrcaTerminalTitleArgs): string => {\n const { branch } = args\n\n const id = parseBranchName(branch)\n\n return id ? displayLabel(id) : branch\n}\n", "/**\n * @fileoverview\n * Loopback liveness probes for the apps `infra-kit dev` runs: a backend's `/__health`, a UI's vite ping.\n * Shared by the dev panel's health dots and by `infra-kit e2e`, which must agree on what \"up\" means.\n */\n\n/** What a probe is aimed at. `kind` picks the endpoint AND the verdict rules \u2014 the two are not separable. */\nexport interface ProbeTarget {\n /** Stream tag (`<app>/api`, `<app>/ui`) \u2014 the key the health map and every log line agree on. */\n tag: string\n /** The port to probe: a backend's ACTUAL bound port, or a UI's runner-assigned vite port. */\n port: number\n kind: 'api' | 'ui'\n}\n\n/**\n * Something answered, but not the thing we asked for \u2014 and WHAT it answered is the whole diagnostic.\n *\n * A UI's `foreign` never paints red (see {@link ProbeOutcome}), so this line is the only thing the user\n * gets, and \"not vite's ping\" alone cannot separate the three causes it exists to tell apart: a\n * `200 text/html` means vite dropped the ping and infra-kit must ship a fix; a `502` means portless or a\n * proxy is shadowing the port; a `404` means a squatter won the free-port race. One glance, three very\n * different next moves \u2014 so the status and content-type travel with the verdict.\n */\nexport interface ForeignAnswer {\n kind: 'foreign'\n status: number\n contentType: string | null\n}\n\n/**\n * Three outcomes, not a boolean:\n * - `ok` \u2014 the endpoint PROVED it is serving (a 2xx on `/__health`; a 204 on vite's ping).\n * - `refused` \u2014 nothing answered (ECONNREFUSED, timeout, transport error).\n * - {@link ForeignAnswer} \u2014 something answered, but not the thing we asked for. For a backend that is a\n * failure (our own fastify returning non-2xx). For a UI it is NOT: a non-204 is equally consistent with a\n * squatter, with a proxy that shadows the ping, and with a future vite that dropped it \u2014 so it can never\n * be allowed to paint red, and it is the reason this is not a boolean.\n */\nexport type ProbeOutcome = 'ok' | 'refused' | ForeignAnswer\n\n/** Narrow a {@link ProbeOutcome} to the one arm that carries data. */\nexport const isForeign = (outcome: ProbeOutcome): outcome is ForeignAnswer => {\n return typeof outcome === 'object'\n}\n\n/** Build the `foreign` verdict from the response that earned it, draining its body so the socket returns. */\nconst foreignFrom = async (res: Response): Promise<ForeignAnswer> => {\n await res.body?.cancel()\n\n return { kind: 'foreign', status: res.status, contentType: res.headers.get('content-type') }\n}\n\n/**\n * The `\u25CD ?` warn. Names what answered instead of vite, because that is the whole actionable content: an\n * `html` body means vite dropped the ping (ours to fix), a 5xx means something is shadowing the port, a 404\n * means a squatter took it. The content-type is trimmed of its `; charset=\u2026` tail \u2014 it is a hint, not a\n * header dump.\n */\nexport const describeForeign = (port: number, answer: ForeignAnswer): string => {\n const type = answer.contentType?.split(';')[0]?.trim()\n const what = type == null || type === '' ? `${answer.status}` : `${answer.status} ${type}`\n\n return `port ${port} answered ${what}, not vite's ping \u2014 liveness cannot be verified`\n}\n\n/** Health-probe seam: resolve one target's liveness. Injectable so the panel stays deterministic in tests. */\nexport type HealthProbe = (target: ProbeTarget) => Promise<ProbeOutcome>\n\n/** One budget for the whole probe \u2014 BOTH hops of a UI redirect share it (see {@link probeUi}). */\nconst PROBE_TIMEOUT_MS = 1500\n\n/**\n * Vite's HMR ping. It is installed unconditionally on every dev server (no plugin, no config) and it sits\n * AHEAD of vite's `htmlFallback` middleware \u2014 which is the whole point: `htmlFallback` answers a plain\n * `GET /` with `200 index.html` even for a vite whose entry module throws, so a naive GET would report a\n * broken app as healthy. The ping cannot be forged that way; a 204 means a vite dev server is listening.\n */\nconst VITE_PING_HEADERS = { accept: 'text/x-vite-ping' } as const\n\n/**\n * Where a target is probed. Always `http://127.0.0.1:<port>`, never the `https://<alias>` the panel prints:\n * the alias adds TLS, a private CA and the portless daemon to the path, so a probe through it would report\n * the PROXY's health, not the app's. And never `localhost` \u2014 ServerlessLocalRun binds v4 loopback only,\n * while `localhost` resolves `[::1]` first on modern Node, which renders a healthy backend `\u25CF down`.\n */\nconst probeUrl = (target: ProbeTarget): string => {\n return target.kind === 'api' ? `http://127.0.0.1:${target.port}/__health` : `http://127.0.0.1:${target.port}/`\n}\n\n/** `new URL(loc, base)`, or `null` when the header is unparseable even relative to the probe URL. */\nconst resolveHop = (location: string, base: string): URL | null => {\n try {\n return new URL(location, base)\n } catch {\n return null\n }\n}\n\n/** A backend: `/__health` must answer 2xx. Anything else IS our own fastify failing, so it is not `ok`. */\nconst probeApi = async (url: string, signal: AbortSignal): Promise<ProbeOutcome> => {\n const res = await fetch(url, { signal })\n\n if (!res.ok) return foreignFrom(res)\n // Drain: undici keeps the socket checked out until an unread body is GC'd, and this runs every 5s per app\n // for the life of the session.\n await res.body?.cancel()\n\n return 'ok'\n}\n\n/**\n * A frontend: vite's ping must answer 204.\n *\n * The redirect hop is the delicate part. Vite serves the ping from its `base`, so a UI configured with\n * `base: '/app/'` answers `/` with a 3xx whose `Location` is RELATIVE (`/app/`) \u2014 which is why the hop is\n * resolved against the probe URL rather than parsed on its own (`new URL('/app/')` throws outright). The\n * accept header is RE-SENT on the second hop: without it the redirected request falls through to vite's\n * html fallback and comes back `200 text/html` \u2014 the exact false green this probe exists to refuse.\n *\n * Exactly one hop, same-origin only. A cross-origin redirect is somebody else's server, and a second hop\n * is a loop we have no budget for; both report `foreign` \u2014 never followed, and never called alive.\n */\nconst probeUi = async (url: string, signal: AbortSignal): Promise<ProbeOutcome> => {\n const res = await fetch(url, { headers: VITE_PING_HEADERS, redirect: 'manual', signal })\n\n if (res.status === 204) return 'ok'\n if (res.status < 300 || res.status >= 400) return foreignFrom(res)\n\n const location = res.headers.get('location')\n const next = location == null ? null : resolveHop(location, url)\n\n if (next == null || next.origin !== new URL(url).origin) return foreignFrom(res)\n\n await res.body?.cancel()\n\n const hop = await fetch(next, { headers: VITE_PING_HEADERS, redirect: 'manual', signal })\n\n if (hop.status !== 204) return foreignFrom(hop)\n await hop.body?.cancel()\n\n return 'ok'\n}\n\n/** Default probe: per-kind, bounded, and loopback-only. A transport error or a timeout is `refused`. */\nexport const defaultHealthProbe: HealthProbe = async (target: ProbeTarget): Promise<ProbeOutcome> => {\n const url = probeUrl(target)\n // ONE signal, shared across both hops: a redirect must not double the budget a wedged server can spend.\n const signal = AbortSignal.timeout(PROBE_TIMEOUT_MS)\n\n try {\n return target.kind === 'api' ? await probeApi(url, signal) : await probeUi(url, signal)\n } catch {\n return 'refused'\n }\n}\n", "import { readRelease } from '@slip-stream-kit/config/internal'\n\n/**\n * Slugified `<release>` for the app's git branch (resolved from the app's own dir), falling back to\n * `DEFAULT_RELEASE_SLUG` outside a git repo / on an empty slug. Never throws.\n *\n * A release ALWAYS resolves because it is the first DNS label of every alias, and every app is reached\n * by hostname. The fallback cannot collide the way a branch can: worktrees are what make two checkouts\n * coexist, and a worktree is by definition inside a git repo. One implementation, in the config package,\n * because the vite and Playwright helpers must name the same host.\n */\nexport const readAppRelease = (cwd: string): string => {\n return readRelease(cwd)\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["execFile", "execFileSync", "process", "DEFAULT_GRACE_MS", "POLL_MS", "REAP_TIMEOUT_MS", "PS_BIN", "SAMPLE_INTERVAL_MS", "PS_ARGS", "PS_MAX_BUFFER", "parseProcRows", "raw", "line", "rawPid", "rawPpid", "rawPgid", "rest", "pid", "ppid", "pgid", "leaderStarts", "rows", "starts", "row", "snapshotGroups", "rootPid", "excludePgid", "collectDoomedGroups", "leaderStart", "validSnapshotPgids", "snapshot", "reapSnapshot", "childrenOf", "siblings", "groups", "seen", "queue", "i", "child", "snapshotProcRows", "snapshotProcRowsAsync", "resolve", "error", "stdout", "isChildOf", "parentPid", "doomedGroupsOf", "ownPgid", "groupAlive", "signalGroups", "pgids", "signal", "registry", "collectForceReapTargets", "registrySnapshots", "live", "fromRegistry", "killDescendantGroupsNow", "getGroups", "sleep", "ms", "waitForExit", "timeoutMs", "deadline", "alive", "superviseChild", "graceMs", "onUnexpectedExit", "snapshotRowsOverride", "killing", "exited", "sampler", "stopSampling", "deregisterAfter", "g", "applySample", "next", "sample", "reportUnexpected", "detail", "code", "doomed", "stragglers", "late", "forced", "constants", "process", "HANDLED_SIGNALS", "SIGNAL_NUMBERS", "constants", "FALLBACK_SIGNO", "exitCodeForSignal", "signal", "TEARDOWN_DEADLINE_MS", "defaultExit", "code", "process", "defaultRegister", "handler", "defaultUnregister", "defaultSetTimer", "ms", "timer", "writeStderr", "message", "writeTeardownFailure", "error", "detail", "registerSignalShutdown", "onSignal", "exit", "register", "unregister", "forceReap", "killDescendantGroupsNow", "teardownDeadlineMs", "setTimer", "describeStall", "fileReport", "firstSignal", "cancelDeadline", "handlers", "detach", "handle", "report", "CANCELLATION_ERROR_NAMES", "hasCancellationName", "value", "isPromptCancellation", "error", "cause", "$", "OrcaError", "payload", "OrcaMalformedError", "exitCode", "stderrTail", "OrcaAbsentError", "STDERR_TAIL_CHARS", "SHELL_COMMAND_NOT_FOUND", "isEnoent", "error", "spawnOrca", "argv", "$", "parseEnvelope", "stdout", "parsed", "runOrca", "output", "envelope", "logger", "probeOrca", "status", "runOrca", "logger", "error", "OrcaAbsentError", "realpath", "realpathForOrcaCwd", "path", "findOrcaRepo", "mainRepoRoot", "repos", "wanted", "runOrca", "realpathForOrcaCwd", "repo", "addOrcaRepo", "WORKTREE_LIST_LIMIT", "isOrcaWorktreeListed", "mainRepoRoot", "cwd", "worktrees", "wanted", "runOrca", "realpathForOrcaCwd", "worktree", "process", "WORKTREE_ID_SEPARATOR", "orcaCallerWorktreePath", "env", "id", "separatorAt", "path", "orcaCallerInsideTargets", "paths", "process", "callerPath", "caller", "realpathForOrcaCwd", "TERMINAL_LIST_LIMIT", "listOrcaTerminals", "cwd", "result", "runOrca", "truncated", "logger", "error", "OrcaError", "retireSurfaces", "cwd", "runOrca", "error", "OrcaError", "logger", "closeEach", "terminals", "listOrcaTerminals", "terminal", "retired", "asOutcome", "OrcaAbsentError", "OrcaMalformedError", "closeOrcaWorktreeTerminals", "probe", "sleep", "DEFAULT_POLL_ATTEMPTS", "DEFAULT_POLL_DELAY_MS", "createOrcaOpenPoll", "overrides", "tryCreate", "argv", "runOrca", "error", "OrcaError", "createWithPoll", "cwd", "poll", "attempts", "schedule", "_", "index", "lastRefusal", "attempt", "outcome", "sleep", "split", "handle", "direction", "openOrcaWorktreeTerminals", "args", "title", "focus", "layout", "panes", "createArgv", "first", "handles", "buildOrcaTerminalTitle", "args", "branch", "id", "parseBranchName", "displayLabel", "isForeign", "outcome", "foreignFrom", "res", "describeForeign", "port", "answer", "type", "what", "VITE_PING_HEADERS", "probeUrl", "target", "resolveHop", "location", "base", "probeApi", "url", "signal", "res", "foreignFrom", "probeUi", "next", "hop", "defaultHealthProbe", "readRelease", "readAppRelease", "cwd"]
|
|
7
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/lib/update-check/run-update-check.ts", "../src/lib/update-check/lock.ts", "../src/lib/update-check/registry.ts", "../src/lib/update-check/semver.ts", "../src/lib/update-check/start-auto-setup.ts", "../src/lib/auto-setup/child-env.ts", "../src/lib/update-check/update-cache.ts", "../src/lib/update-check/update-plugin.ts", "../src/lib/plugin-pointer/claude-cli.ts", "../src/lib/plugin-pointer/names.ts", "../src/lib/plugin-pointer/install-state.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * The CHILD half of the auto-update, spawned detached by `maybeAutoUpdate`. It outlives the CLI\n * invocation that started it, so it may take its time and it may replace the binary.\n *\n * WHY IT WAITS FOR THE PARENT TO EXIT \u2014 the load-bearing invariant of this whole feature:\n * `scripts/build.js` sets esbuild `splitting: true`, so `dist/cli.js` lazily imports sibling\n * `chunk-*.js` files at runtime (that is how the Ink TUI stays off the fast path). A package manager\n * installing over `dist/` mid-command deletes chunks the parent has not imported yet, and the parent\n * dies on a dynamic import of a file that no longer exists. Waiting for the parent to exit is what\n * makes a silent background install safe rather than a random crash under `infra-kit dev`.\n */\nimport { spawnSync } from 'node:child_process'\nimport { homedir } from 'node:os'\nimport process from 'node:process'\n\nimport { PACKAGE_NAME, defaultLazyNpmRoot, detectInstallManager, safeRealpath } from 'src/lib/install-manager'\nimport { packageManagerInstallEnv } from 'src/lib/pm-env'\n\nimport { acquireUpdateLock } from './lock'\nimport { fetchLatestVersion } from './registry'\nimport { isNewerVersion } from './semver'\nimport { startAutoSetup } from './start-auto-setup'\nimport type { StartAutoSetupDeps } from './start-auto-setup'\nimport { writeUpdateCache } from './update-cache'\nimport type { UpdateCache } from './update-cache'\n// Static, like every import in this file \u2014 see the module doc: on the `installed` path the plugin step\n// runs after `dist/` has been replaced, and a lazy `import()` of a chunk from a replaced dist is unsafe.\nimport { updatePlugin } from './update-plugin'\nimport type { UpdatePluginDeps } from './update-plugin'\n\n/** How often to re-check whether the parent is gone. */\nexport const PARENT_POLL_INTERVAL_MS = 200\n\n/**\n * Give up waiting after this long. A long-lived parent (`infra-kit dev` runs for hours) must not leave\n * an immortal child pinned to a stale version: we simply skip this cycle. `lastCheckMs` is already\n * persisted by then, so the next short-lived command re-checks after the normal throttle window.\n */\nexport const PARENT_WAIT_TIMEOUT_MS = 5 * 60 * 1000\n\nexport interface RunUpdateCheckDeps extends UpdatePluginDeps {\n /** The hand-off to `setup --auto`; tests inject it so no real child starts. */\n startAutoSetup?: (deps: StartAutoSetupDeps) => void\n env?: NodeJS.ProcessEnv\n nowMs?: number\n fetchLatest?: (env: NodeJS.ProcessEnv) => Promise<string | null>\n writeCache?: typeof writeUpdateCache\n isProcessAlive?: (pid: number) => boolean\n sleep?: (ms: number) => Promise<void>\n /** Monotonic-ish source for the parent-wait deadline. Injected so the timeout is testable without real time. */\n clock?: () => number\n /** `npm root -g` probe. Defaults to the real subprocess; this child is detached, so it can afford one. */\n lazyNpmRoot?: () => string | undefined\n /** Single-flight guard. Returns a release fn, or null when another worker already holds the lock. */\n acquireLock?: () => (() => void) | null\n spawnSync?: typeof spawnSync\n /**\n * The version the user now gets from the `infra-kit` on their PATH. Consulted once, after the install\n * command returns 0, because that exit code is not proof of anything \u2014 see {@link readInstalledVersion}.\n */\n installedVersion?: (env: NodeJS.ProcessEnv) => string | null\n /** Realpath of the installed `dist/cli.js`, used to identify the owning package manager. */\n selfRealPath: string\n parentPid?: number\n}\n\n/** Bounds the post-install probe; a hung shim must not pin this worker for the parent's 5-minute budget. */\nconst VERSION_PROBE_TIMEOUT_MS = 15_000\n\n/**\n * Ask the freshly installed CLI what it is, the way the user will: the `infra-kit` bin on PATH.\n *\n * Not `<selfRealPath>/../package.json`. pnpm keeps each version in its own directory\n * (`.pnpm/infra-kit@0.5.1/\u2026`, `global/v11/<hash>/\u2026`) and repoints the shim, so the path THIS worker was\n * launched from keeps reporting the old version forever, while the path the user gets has moved.\n *\n * `--json` doubles as the guard that stops the probe spawning a second updater from inside the first:\n * `autoUpdateSkipReason` returns `json` before anything else runs, and piped stdio fails the tty arm too.\n * Anything short of a parseable version is `null` \u2014 a shim that no longer resolves, a bin that crashes on\n * boot, output that is not JSON \u2014 and the caller treats every one of those as \"not verified\".\n */\nconst readInstalledVersion = (env: NodeJS.ProcessEnv): string | null => {\n const result = spawnSync(PACKAGE_NAME, ['version', '--json'], {\n stdio: ['ignore', 'pipe', 'ignore'],\n shell: process.platform === 'win32',\n cwd: homedir(),\n env,\n timeout: VERSION_PROBE_TIMEOUT_MS,\n windowsHide: true,\n encoding: 'utf8',\n })\n\n if (result.error || result.signal || result.status !== 0) return null\n\n try {\n const parsed: unknown = JSON.parse(result.stdout)\n const version = (parsed as { version?: unknown } | null)?.version\n\n return typeof version === 'string' && version !== '' ? version : null\n } catch {\n return null\n }\n}\n\n/** Signal 0 performs the permission/existence check without delivering anything. */\nconst defaultIsProcessAlive = (pid: number): boolean => {\n try {\n process.kill(pid, 0)\n\n return true\n } catch {\n return false\n }\n}\n\nconst defaultSleep = (ms: number): Promise<void> => {\n return new Promise((resolve) => {\n setTimeout(resolve, ms)\n })\n}\n\n/**\n * Block until `parentPid` exits or {@link PARENT_WAIT_TIMEOUT_MS} elapses. Returns whether the parent\n * is actually gone \u2014 the caller must not install on a timeout.\n */\nconst waitForParentExit = async (\n parentPid: number,\n deps: Required<Pick<RunUpdateCheckDeps, 'isProcessAlive' | 'sleep' | 'clock'>>,\n): Promise<boolean> => {\n const deadline = deps.clock() + PARENT_WAIT_TIMEOUT_MS\n\n while (deps.clock() < deadline) {\n if (!deps.isProcessAlive(parentPid)) return true\n\n await deps.sleep(PARENT_POLL_INTERVAL_MS)\n }\n\n return !deps.isProcessAlive(parentPid)\n}\n\nexport type UpdateCheckOutcome =\n | 'installed'\n | 'install-failed'\n /** The install command returned 0, yet the `infra-kit` on PATH still does not report `latestVersion`. */\n | 'install-stale'\n | 'up-to-date'\n | 'fetch-failed'\n | 'cannot-self-spawn'\n | 'parent-still-running'\n | 'parent-unknown'\n | 'already-running'\n\n/**\n * Fetch, decide, and (when safe) install \u2014 returning WHY it did what it did so tests can distinguish\n * \"no update\" from \"could not install\". Never throws.\n *\n * `lastCheckMs` is written from the ATTEMPT, before any early return. If it were written only on\n * success, an offline user would re-spawn a doomed child on every single command.\n *\n * @example\n * await runUpdateCheck('0.1.130', { selfRealPath: '/usr/local/lib/node_modules/infra-kit/dist/cli.js' })\n * // => 'installed'\n */\nexport const runUpdateCheck = async (currentVersion: string, deps: RunUpdateCheckDeps): Promise<UpdateCheckOutcome> => {\n const acquireLock = deps.acquireLock ?? acquireUpdateLock\n const writeCache = deps.writeCache ?? writeUpdateCache\n\n // Single-flight. N shells launched at once all read the same stale cache and each spawns a worker;\n // the cache throttle cannot stop them because it is only written after the fetch returns. Without\n // this, a pending update means N concurrent `npm install -g` over one global directory.\n const release = acquireLock()\n\n if (!release) return 'already-running'\n\n // Remember the locked function's final cache write so the plugin step can stamp its own outcome\n // onto it instead of reading the file back (a holder, not a `let`: TypeScript cannot see a closure\n // assignment and would narrow a `let` to its initial `null`).\n const written: { last: UpdateCache | null } = { last: null }\n const recordingWriteCache: typeof writeUpdateCache = (cache) => {\n written.last = cache\n writeCache(cache)\n }\n\n try {\n const outcome = await runUpdateCheckLocked(currentVersion, { ...deps, writeCache: recordingWriteCache })\n\n // The plugin step runs AFTER the locked function has returned, on EVERY outcome \u2014 `up-to-date`\n // included, which is what makes a plugin-only bump deliverable \u2014 and never inside its early-return\n // chain or ahead of the fetch. Placement is about the throttle: `lastCheckMs` is stamped only by\n // `finish()` after the fetch, and the lock is reaped as stale at `LOCK_STALE_MS` (30 min). A\n // `claude` probe (up to 120 s) plus an update ahead of the fetch would delay that stamp on every\n // run and eat into the stale window on a slow link. Here the stamp is written, the lock is still\n // held (single-flight), and the step has a bounded budget of its own. \"Every outcome\" has one\n // exception, decided by the cache the locked function wrote rather than by name: see\n // `pluginStepWithheld`.\n recordPluginUpdate(deps, written.last, currentVersion, writeCache)\n\n // On every outcome, last: an install that landed owes a pass for the new version, and one that\n // did not still owes the retry of a failed pass. `setup --auto` decides which, and returns at once\n // when neither \u2014 see `autoSetupDue`.\n const handOff = deps.startAutoSetup ?? startAutoSetup\n\n try {\n handOff({ env: deps.env })\n } catch {\n // The CLI outcome is already settled; a hand-off that did not start is retried next window.\n }\n\n return outcome\n } finally {\n release()\n }\n}\n\n/**\n * Must the plugin step stand down because the CLI is behind? Reads the `latestVersion` the locked\n * function wrote, not its outcome name: \"a newer CLI exists and this run did not install it\" is\n * exactly what a non-null value newer than `currentVersion` encodes.\n *\n * That covers `cannot-self-spawn` (`:300`), `parent-unknown` (`:305`), `parent-still-running` (`:326`),\n * `install-failed` (`:353`) and `install-stale` (`:364`) \u2014 each leaves the newer version in the cache \u2014\n * and any outcome added later that does the same, without this list needing to know. `installed` clears\n * `latestVersion` (`:367`) and `up-to-date` writes a non-newer one (`:282`), so both still advance the\n * plugin and a plugin-only bump keeps delivering; `fetch-failed` writes null (`:280`) and cannot be\n * gated \u2014 the plugin advances, as before; `already-running` never reaches the wrapper.\n *\n * @example\n * pluginStepWithheld('0.8.0', '0.7.2') // => true\n * pluginStepWithheld(null, '0.7.2') // => false\n */\n// Without this a Homebrew or unknown-location machine advances the plugin on every run while its CLI\n// waits for a hand-typed update, and once a repo serves infra-kit through the plugin, plugin N+1 spawns\n// CLI N \u2014 skills citing tool names that server does not serve, a `doctor` that fails on a key that is\n// meant to be absent, and a `setup` that re-adds it to a tracked `.mcp.json`.\nexport const pluginStepWithheld = (latestVersion: string | null, currentVersion: string): boolean => {\n return latestVersion !== null && isNewerVersion(latestVersion, currentVersion)\n}\n\n/**\n * Run the plugin step and write its outcome beside the CLI outcome. Never throws: `updatePlugin` is\n * total, and a cache write that fails here has nowhere left to record itself \u2014 the CLI outcome the\n * worker already produced is worth more than a plugin diagnostic.\n */\nconst recordPluginUpdate = (\n deps: UpdatePluginDeps,\n base: UpdateCache | null,\n currentVersion: string,\n writeCache: typeof writeUpdateCache,\n): void => {\n const outcome = pluginStepWithheld(base?.latestVersion ?? null, currentVersion)\n ? 'skipped-cli-stale'\n : updatePlugin(deps)\n const checkedMs = (deps.clock ?? Date.now)()\n\n // `finish()` writes on every path the locked function returns through, so `base` is only null if that\n // invariant breaks \u2014 and then there is no throttle stamp to attach the outcome to either.\n if (base === null) return\n\n try {\n writeCache({ ...base, plugin: { outcome, checkedMs } })\n } catch {\n // Recorded nowhere, deliberately: see the function doc.\n }\n}\n\nconst runUpdateCheckLocked = async (currentVersion: string, deps: RunUpdateCheckDeps): Promise<UpdateCheckOutcome> => {\n const env = deps.env ?? process.env\n const nowMs = deps.nowMs ?? Date.now()\n const fetchLatest = deps.fetchLatest ?? fetchLatestVersion\n const writeCache = deps.writeCache ?? writeUpdateCache\n const isProcessAlive = deps.isProcessAlive ?? defaultIsProcessAlive\n const sleep = deps.sleep ?? defaultSleep\n const clock = deps.clock ?? Date.now\n const lazyNpmRoot = deps.lazyNpmRoot ?? defaultLazyNpmRoot\n const spawn = deps.spawnSync ?? spawnSync\n const installedVersion = deps.installedVersion ?? readInstalledVersion\n\n const latestVersion = await fetchLatest(env)\n\n // Every path below writes the cache EXACTLY once, and always with `lastCheckMs: nowMs` \u2014 the throttle\n // burns on the ATTEMPT, never on success. If it burned only on success, an offline user would respawn\n // a doomed child on every command.\n const finish = (\n outcome: UpdateCheckOutcome,\n cache: Omit<UpdateCache, 'lastCheckMs' | 'outcome'>,\n ): UpdateCheckOutcome => {\n // `outcome` is recorded on EVERY write: it is what lets a reader (human or `isStale`) tell a settled\n // `installed`/`up-to-date` from a transient `fetch-failed`, all of which otherwise write an identical\n // `{latestVersion:null}`. See `UpdateCache.outcome`.\n writeCache({ lastCheckMs: nowMs, ...cache, outcome })\n\n return outcome\n }\n\n if (latestVersion === null) return finish('fetch-failed', { latestVersion: null, updateCommand: null })\n if (!isNewerVersion(latestVersion, currentVersion))\n return finish('up-to-date', { latestVersion, updateCommand: null })\n\n // `lazyNpmRoot` is what makes the COMMON case work: a plain `npm i -g infra-kit` leaves no\n // `npm_config_prefix` in the user's shell, so every cheap matcher misses and detection would report\n // `unknown` / `canSelfSpawn: false`. Without this probe the auto-update would silently degrade to a\n // notice for the majority of installs. The subprocess is affordable here and nowhere else.\n // `version` pins the spec to what we just fetched: a `@latest` command is resolved by the package\n // manager from ITS cache, which pnpm does not revalidate \u2014 see `pinSpec` in install-manager.\n const { canSelfSpawn, updateCommand } = detectInstallManager({\n selfRealPath: deps.selfRealPath,\n env,\n realpath: safeRealpath,\n lazyNpmRoot,\n version: latestVersion,\n })\n\n // Homebrew relinks its prefix and may prompt; an unknown location means the command is a guess.\n // Both stay the user's call, so record the command for `maybeAutoUpdate` to print next invocation.\n if (!canSelfSpawn) return finish('cannot-self-spawn', { latestVersion, updateCommand })\n\n // Fail SAFE. Without a parent to outlive we cannot know whether a live CLI is still lazily importing\n // `chunk-*.js` out of the `dist/` we are about to replace, so we skip the cycle rather than install\n // blind. The real spawn always passes `--parent-pid`; only a hand-run worker lands here.\n if (deps.parentPid == null) return finish('parent-unknown', { latestVersion, updateCommand: null })\n\n // Burn the throttle NOW, before the two unbounded-ish operations below (a 5-minute parent wait, then an\n // install of no fixed duration). Everything after this point can take many minutes, and until the cache\n // is written it still reads as STALE \u2014 so every new shell in that window spawns its own worker, and the\n // single-flight lock becomes the only thing standing between them and concurrent `npm install -g` runs\n // over one global directory. The lock is reaped by mtime (LOCK_STALE_MS), so a slow enough install has\n // its live lock stolen and we get exactly the pile-up the lock exists to prevent.\n //\n // Writing the stamp here makes the throttle \u2014 not the lock \u2014 the primary guard, which is what the cache\n // was always for. `finish()` overwrites this with the real outcome; this is the only path that writes\n // the cache twice, and deliberately so.\n //\n // `outcome: 'installing'` marks a mid-install checkpoint: if the worker is killed here (machine sleeps,\n // SIGKILL) and never reaches `finish()`, this stamp is what the next run reads, and the cache says so\n // rather than looking like a settled result. It does not shorten the next window \u2014 see CHECK_INTERVAL_MS.\n writeCache({ lastCheckMs: nowMs, latestVersion, updateCommand: null, outcome: 'installing' })\n\n const parentGone = await waitForParentExit(deps.parentPid, { isProcessAlive, sleep, clock })\n\n // A long-lived parent (`infra-kit dev`) outlasted the wait. Stay silent and retry next window.\n if (!parentGone) return finish('parent-still-running', { latestVersion, updateCommand: null })\n\n // `shell` on win32 so the `.cmd` shims npm/pnpm/yarn ship as global bins resolve.\n //\n // `packageManagerInstallEnv` strips the inherited npx/dlx markers \u2014 which otherwise make\n // pnpm/portless-style tools believe they were invoked via `npx`/`dlx` and refuse to run \u2014 while KEEPING\n // `npm_config_prefix` and `npm_config_registry`. Using the blunt `withoutPackageManagerEnv` here was a\n // bug: it also erased the prefix that `detectInstallManager` had just matched on, so we would detect a\n // global install at prefix P and then install into the default prefix instead \u2014 silently, forever. See\n // the module doc on `packageManagerInstallEnv`.\n //\n // `cwd` is pinned to the home directory, and this is a SECURITY control, not tidiness: npm resolves\n // `registry=` from an `.npmrc` on disk relative to the cwd. Inheriting the caller's cwd would let any\n // repo containing a hostile `.npmrc` silently redirect this unattended `install -g` to an attacker's\n // registry, executing its lifecycle scripts. Stripping `npm_config_registry` from the env does NOT\n // close that hole, because the redirect lives in a file, not the environment.\n const result = spawn(updateCommand[0] as string, updateCommand.slice(1), {\n stdio: 'ignore',\n shell: process.platform === 'win32',\n cwd: homedir(),\n env: packageManagerInstallEnv(env),\n windowsHide: true,\n })\n\n // Record the manual command so a persistently failing silent install (EACCES on a root-owned global\n // dir, say) still surfaces ONE actionable line, instead of failing invisibly forever.\n if (result.error || result.signal || result.status !== 0) {\n return finish('install-failed', { latestVersion, updateCommand })\n }\n\n // Exit 0 is the package manager saying it is content, not that anything changed. `pnpm add -g\n // infra-kit@latest` resolved the tag from its own stale metadata cache, reinstalled the version already\n // present, and returned 0 \u2014 and this function recorded `installed`, cleared `latestVersion`, and\n // repeated the same no-op every cycle. The spec is pinned now, but the check that would have SHOWN\n // that bug is this one: ask the binary the user actually runs. A mismatch keeps `latestVersion` and the\n // command in the cache, so the parent prints the manual line instead of staying silent.\n const nowInstalled = installedVersion(env)\n\n if (nowInstalled !== latestVersion) return finish('install-stale', { latestVersion, updateCommand })\n\n // Verified: clear `latestVersion` so the next run does not re-notify.\n return finish('installed', { latestVersion: null, updateCommand: null })\n}\n", "/**\n * Single-flight lock for the background update worker.\n *\n * Without it, N shells starting at once all read the same stale cache and each spawns a worker \u2014 and\n * once a newer version exists, each of those runs `npm install -g` concurrently, over the same global\n * directory. (Measured: five concurrent `ik` invocations spawn five workers.) The throttle in the cache\n * cannot prevent this: it is only written AFTER the fetch returns, long after the other workers launched.\n *\n * `openSync(path, 'wx')` is the primitive \u2014 an atomic create-if-absent, so exactly one worker wins even\n * if all of them call it in the same millisecond.\n */\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\n\nimport { getCacheRoot } from 'src/lib/constants'\n\nexport const LOCK_FILE_NAME = 'update-check.lock'\n\n/**\n * A lock older than this is assumed to belong to a worker that was killed before it could clean up.\n *\n * Must exceed the worker's ENTIRE critical section, and the install is part of it: the lock is held across\n * `fetch` (\u22642.5s) + the parent wait (\u22645min) + `npm install -g`, which has no bound at all \u2014 a cold cache on\n * a slow link can run for many minutes. The previous 10 minutes was budgeted from the wait and the fetch\n * alone, silently omitting the very operation the lock exists to serialise. A slow enough install therefore\n * had its own LIVE lock reaped as stale by the next worker, which then ran a second concurrent\n * `npm install -g` over the same global directory \u2014 precisely the pile-up described above.\n *\n * The throttle, not this timeout, is now the primary guard: `runUpdateCheck` stamps `lastCheckMs` BEFORE\n * the wait and the install, so concurrent shells stop spawning workers at all. This value only has to\n * outlive a plausible install, and is deliberately generous \u2014 the cost of being too large is that a worker\n * killed mid-install delays the next attempt, which the throttle would delay anyway.\n */\nexport const LOCK_STALE_MS = 30 * 60 * 1000\n\nexport const lockFilePath = (): string => {\n return path.join(getCacheRoot(), LOCK_FILE_NAME)\n}\n\nexport interface LockDeps {\n nowMs?: number\n lockPath?: string\n}\n\n/** Delete a lock whose mtime predates the staleness window. Best-effort: losing the race is harmless. */\nconst reapIfStale = (lockPath: string, nowMs: number): void => {\n try {\n if (nowMs - fs.statSync(lockPath).mtimeMs > LOCK_STALE_MS) fs.rmSync(lockPath, { force: true })\n } catch {\n // Vanished under us \u2014 another worker reaped it. Nothing to do.\n }\n}\n\n/**\n * Take the lock, or return null when another worker already holds it.\n *\n * Returns a release function rather than a boolean so the caller cannot forget which path releases:\n * there is exactly one handle, and it is the only thing that can unlink the file.\n *\n * @example\n * const release = acquireUpdateLock()\n * if (!release) return 'already-running'\n * try { ... } finally { release() }\n */\nexport const acquireUpdateLock = (deps: LockDeps = {}): (() => void) | null => {\n const nowMs = deps.nowMs ?? Date.now()\n const lockPath = deps.lockPath ?? lockFilePath()\n\n try {\n fs.mkdirSync(path.dirname(lockPath), { recursive: true })\n } catch {\n return null\n }\n\n const open = (): number | null => {\n try {\n // 'wx' fails with EEXIST if the file is already there: an atomic test-and-set.\n return fs.openSync(lockPath, 'wx', 0o600)\n } catch {\n return null\n }\n }\n\n let fd = open()\n\n if (fd === null) {\n reapIfStale(lockPath, nowMs)\n // One retry only. If a live worker holds the lock, we are the loser and must simply stand down.\n fd = open()\n }\n\n if (fd === null) return null\n\n try {\n fs.writeFileSync(fd, String(process.pid))\n } catch {\n // The pid is a debugging aid, not part of the protocol; the lock is held either way.\n }\n\n return () => {\n try {\n fs.closeSync(fd)\n } catch {\n // Already closed.\n }\n\n try {\n // Only our own lock: if a holder outlived LOCK_STALE_MS, the next one reaped it and took the path,\n // and unlinking THAT lock would let a third holder in beside it. Empty means our pid write failed.\n const holder = fs.readFileSync(lockPath, 'utf8')\n\n if (holder === String(process.pid) || holder === '') fs.rmSync(lockPath, { force: true })\n } catch {\n // Already gone; a later worker will reap it via LOCK_STALE_MS regardless.\n }\n }\n}\n", "/**\n * Ask the npm registry for the `latest` dist-tag. Node >= 24 (see `engines`), so global `fetch` is\n * available and this needs no dependency.\n */\nimport { PACKAGE_NAME } from 'src/lib/install-manager'\n\nexport const DEFAULT_REGISTRY = 'https://registry.npmjs.org'\n\n/**\n * Bounded so a hanging registry can never keep a detached child alive indefinitely. 5s, not a tighter\n * bound: the abbreviated `/pkg/latest` endpoint is usually ~20-900ms, but a cold TLS/DNS handshake in a\n * freshly-spawned worker can spike past a 2.5s cap \u2014 and a single trip used to cost a `fetch-failed` that\n * pushed the next check a full window out. A detached child that lingers a few extra seconds harms nothing.\n */\nexport const FETCH_TIMEOUT_MS = 5_000\n\n/**\n * The registry this install actually talks to. A user behind a corporate mirror has `npm_config_registry`\n * set; hitting registry.npmjs.org directly would either fail closed (firewall) or, worse, report a public\n * version they cannot install.\n *\n * @example\n * registryUrl({ npm_config_registry: 'https://nexus.corp/repo/npm/' }) // => 'https://nexus.corp/repo/npm'\n */\n/**\n * `npm_config_registry` in WHICHEVER case the environment spells it.\n *\n * A bare `env.npm_config_registry` reads only the lowercase form, but npm itself matches `/^npm_config_/i`,\n * and POSIX `process.env` is case-SENSITIVE \u2014 so a `NPM_CONFIG_REGISTRY` (the form Dockerfiles and CI\n * images export) was invisible here while npm honoured it. The check then queried the public registry that\n * the install would never touch: the two halves of this feature were exactly inverted.\n */\nconst configuredRegistry = (env: NodeJS.ProcessEnv): string | undefined => {\n const hit = Object.entries(env).find(([key, value]) => {\n return /^npm_config_registry$/i.test(key) && value != null && value !== ''\n })\n\n return hit?.[1]\n}\n\nexport const registryUrl = (env: NodeJS.ProcessEnv): string => {\n const configured = configuredRegistry(env)\n\n let base = configured != null && configured !== '' ? configured : DEFAULT_REGISTRY\n\n // A `/\\/+$/` regex would do this in one line, but its backtracking is super-linear on a hostile input.\n while (base.endsWith('/')) {\n base = base.slice(0, -1)\n }\n\n return base\n}\n\n/**\n * The published `latest` version, or null on ANY failure (offline, timeout, non-200, malformed body).\n * Never throws: a failed check must be indistinguishable from \"no update available\" to every caller.\n *\n * @example\n * await fetchLatestVersion(process.env) // => '0.1.131' | null\n */\nexport const fetchLatestVersion = async (\n env: NodeJS.ProcessEnv,\n fetchFn: typeof fetch = fetch,\n): Promise<string | null> => {\n const controller = new AbortController()\n const timer = setTimeout(() => {\n controller.abort()\n }, FETCH_TIMEOUT_MS)\n\n try {\n const response = await fetchFn(`${registryUrl(env)}/${PACKAGE_NAME}/latest`, {\n signal: controller.signal,\n headers: { accept: 'application/json' },\n })\n\n if (!response.ok) return null\n\n const body: unknown = await response.json()\n const version = (body as { version?: unknown } | null)?.version\n\n return typeof version === 'string' && version !== '' ? version : null\n } catch {\n return null\n } finally {\n clearTimeout(timer)\n }\n}\n", "/**\n * Registry-safe version comparison for the auto-update check.\n *\n * Deliberately NOT `src/lib/version-utils`: `parseVersion` there does `versionStr.slice(1)` because it\n * only ever sees `v`-prefixed release tags, so it turns a bare registry version (`0.1.130`) into\n * `[NaN, 1, 130]`. Feeding npm's `dist-tags.latest` through it silently compares NaNs and never fires.\n * These inputs are bare semver from `registry.npmjs.org` and from our own `package.json`.\n */\n\n/** `1.2.3-beta.4+build` \u2192 the `[1, 2, 3]` release triple and the `beta.4` prerelease, or null if unparsable. */\ninterface ParsedVersion {\n release: [number, number, number]\n /** Dot-separated prerelease identifiers, empty when this is a final release. */\n prerelease: string[]\n}\n\nconst RELEASE_PATTERN = /^(\\d+)\\.(\\d+)\\.(\\d+)$/\n\n/**\n * Parse bare semver. Returns null (never throws, never NaNs) for anything that is not\n * `major.minor.patch` with optional `-prerelease` and `+build` \u2014 a garbled registry body must read as\n * \"no update\", not as \"update to NaN\".\n *\n * @example\n * parseSemver('0.1.130') // => { release: [0, 1, 130], prerelease: [] }\n * parseSemver('1.0.0-rc.1+abc') // => { release: [1, 0, 0], prerelease: ['rc', '1'] }\n * parseSemver('v1.0.0') // => null\n */\nexport const parseSemver = (version: string): ParsedVersion | null => {\n // Build metadata is ignored entirely by semver precedence rules.\n const [withoutBuild = ''] = version.trim().split('+')\n const [core = '', ...prereleaseParts] = withoutBuild.split('-')\n const match = RELEASE_PATTERN.exec(core)\n\n if (!match) return null\n\n const [major, minor, patch] = [match[1], match[2], match[3]].map(Number) as [number, number, number]\n\n return {\n release: [major, minor, patch],\n // Re-join on '-' so `1.0.0-rc-1` keeps its hyphen inside a single identifier.\n prerelease: prereleaseParts.length > 0 ? prereleaseParts.join('-').split('.') : [],\n }\n}\n\nconst NUMERIC_IDENTIFIER = /^\\d+$/\n\n/** Compare two unequal prerelease identifiers per semver \u00A711. Numeric ones rank below alphanumeric ones. */\nconst compareIdentifier = (left: string, right: string): number => {\n const leftNumeric = NUMERIC_IDENTIFIER.test(left)\n const rightNumeric = NUMERIC_IDENTIFIER.test(right)\n\n if (leftNumeric && rightNumeric) return Number(left) - Number(right)\n if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1\n\n return left < right ? -1 : 1\n}\n\n/** Compare prerelease identifier lists. A shorter list has lower precedence: `1.0.0-rc` < `1.0.0-rc.1`. */\nconst comparePrerelease = (a: string[], b: string[]): number => {\n for (let index = 0; index < Math.max(a.length, b.length); index += 1) {\n const left = a[index]\n const right = b[index]\n\n if (left === undefined) return -1\n if (right === undefined) return 1\n if (left !== right) return compareIdentifier(left, right)\n }\n\n return 0\n}\n\n/**\n * Is `latest` strictly newer than `current`? False when either is unparsable, so a bad registry\n * response can never trigger an install.\n *\n * The load-bearing case is NUMERIC, not lexical, comparison: `0.1.9` must be older than `0.1.130`.\n * A string compare would order them the other way and pin every user to the older build forever.\n *\n * A final release outranks its own prereleases (`1.0.0` > `1.0.0-rc.1`), so a user on `rc` is offered\n * the release, and a user on the release is never \"downgraded\" to an rc.\n *\n * @example\n * isNewerVersion('0.1.130', '0.1.9') // => true (numeric, not lexical)\n * isNewerVersion('1.0.0', '1.0.0-rc.1') // => true\n * isNewerVersion('0.1.130', '0.1.130') // => false\n * isNewerVersion('garbage', '0.1.0') // => false\n */\nexport const isNewerVersion = (latest: string, current: string): boolean => {\n const parsedLatest = parseSemver(latest)\n const parsedCurrent = parseSemver(current)\n\n if (!parsedLatest || !parsedCurrent) return false\n\n for (let index = 0; index < 3; index += 1) {\n const left = parsedLatest.release[index] as number\n const right = parsedCurrent.release[index] as number\n\n if (left !== right) return left > right\n }\n\n const latestIsFinal = parsedLatest.prerelease.length === 0\n const currentIsFinal = parsedCurrent.prerelease.length === 0\n\n if (latestIsFinal !== currentIsFinal) return latestIsFinal\n\n return comparePrerelease(parsedLatest.prerelease, parsedCurrent.prerelease) > 0\n}\n", "/**\n * The update worker's last step: hand off to `infra-kit setup --auto` and return without waiting.\n *\n * The `infra-kit` on PATH, not this worker's own `dist/`: right after an install the worker is still the\n * OLD code, and the setup that has to run is the new version's. The child decides for itself whether a\n * pass is owed (`autoSetupDue`), so this side holds no version logic an old worker could get wrong.\n *\n * Detached and unref'd with its own lock, so the worker's update lock is released at once and a slow\n * pass never stretches it toward `LOCK_STALE_MS`.\n */\nimport { spawn } from 'node:child_process'\nimport { homedir } from 'node:os'\nimport process from 'node:process'\n\nimport { unattendedChildEnv } from 'src/lib/auto-setup/child-env'\nimport { PACKAGE_NAME } from 'src/lib/install-manager'\n\nexport const AUTO_SETUP_ARGV = ['setup', '--auto', '--json'] as const\n\nexport interface StartAutoSetupDeps {\n env?: NodeJS.ProcessEnv\n spawnDetached?: (command: string, args: readonly string[], env: NodeJS.ProcessEnv) => void\n}\n\nconst defaultSpawnDetached = (command: string, args: readonly string[], env: NodeJS.ProcessEnv): void => {\n const child = spawn(command, [...args], {\n cwd: homedir(),\n env,\n detached: true,\n stdio: 'ignore',\n shell: process.platform === 'win32',\n windowsHide: true,\n })\n\n // An `infra-kit` missing from PATH surfaces here asynchronously; there is no one to tell.\n child.on('error', () => {})\n child.unref()\n}\n\n/**\n * NEVER throws: a setup that does not start is a pass the next update check starts instead.\n *\n * @example\n * startAutoSetup() // spawns `infra-kit setup --auto --json` from $HOME, detached\n */\nexport const startAutoSetup = (deps: StartAutoSetupDeps = {}): void => {\n try {\n const env = unattendedChildEnv(deps.env ?? process.env)\n const spawnDetached = deps.spawnDetached ?? defaultSpawnDetached\n\n spawnDetached(PACKAGE_NAME, AUTO_SETUP_ARGV, env)\n } catch {\n // See the function doc.\n }\n}\n", "/**\n * The environment every unattended setup process gets: an allowlist, not the worker's snapshot.\n *\n * The worker inherits whichever shell ran the command that triggered it, and that shell can carry\n * anything \u2014 `env-load`'s Doppler secrets, another project's `INFRA_KIT_SESSION`, `GIT_DIR` from a git\n * hook, `CLAUDECODE` from an agent. Each would follow the setup into every project. A setup needs to\n * find its tools and its home, and nothing else.\n */\nconst ALLOWED_KEYS: ReadonlySet<string> = new Set([\n 'PATH',\n 'HOME',\n 'USER',\n 'LOGNAME',\n 'SHELL',\n 'LANG',\n 'TMPDIR',\n 'PNPM_HOME',\n 'NVM_DIR',\n 'VOLTA_HOME',\n 'FNM_DIR',\n])\n\nconst ALLOWED_PREFIXES = ['LC_', 'XDG_', 'HOMEBREW_'] as const\n\nconst isAllowed = (key: string): boolean => {\n return (\n ALLOWED_KEYS.has(key) ||\n ALLOWED_PREFIXES.some((prefix) => {\n return key.startsWith(prefix)\n })\n )\n}\n\n/**\n * @example\n * unattendedChildEnv({ PATH: '/bin', DOPPLER_TOKEN: 'x', GIT_DIR: '/r/.git' }) // => { PATH: '/bin' }\n */\nexport const unattendedChildEnv = (env: NodeJS.ProcessEnv): NodeJS.ProcessEnv => {\n return Object.fromEntries(\n Object.entries(env).filter(([key]) => {\n return isAllowed(key)\n }),\n )\n}\n", "/**\n * The throttle + notice state for the auto-update check, at `$cacheRoot/update-check.json`.\n *\n * `getCacheRoot()` \u2014 NOT `getSessionCacheDir()`, which throws unless `INFRA_KIT_SESSION` is set. A user\n * who installed this CLI globally and never ran `infra-kit setup` has no session, and the update check\n * must still work for exactly that person.\n */\nimport fs from 'node:fs'\nimport path from 'node:path'\n\nimport { atomicWriteFileSync, getCacheRoot } from 'src/lib/constants'\n\nexport const CACHE_FILE_NAME = 'update-check.json'\n\n/**\n * The ONE re-check window, whatever the last outcome was. A background check is cheap \u2014 one\n * detached child and a single registry fetch bounded by `FETCH_TIMEOUT_MS` \u2014 but not once per\n * shell command, which is the storm this throttle exists to stop.\n *\n * MUST stay above a full worker cycle: `runUpdateCheck` stamps `lastCheckMs` BEFORE waiting up to\n * `PARENT_WAIT_TIMEOUT_MS` (5min) for the parent to exit and THEN installing.\n */\n// There is deliberately no shorter \"retry after a transient failure\" window. One used to exist\n// because this interval was 24h, and burning a whole day on a single network blip locked out\n// exactly the publisher pushing a release mid-window. At 20 minutes that gap is gone: a blip costs\n// one window, so a second window buys nothing and cannot pay for what it costs.\n//\n// What it would cost: go below a full worker cycle and the cache reads stale while the first worker\n// is still legitimately working, so the next shell spawns a worker that dies on the live\n// single-flight lock and returns WITHOUT writing \u2014 leaving the cache stale for every command until\n// `LOCK_STALE_MS` (30min) reaps the lock. That is the per-command spawn storm this throttle exists\n// to prevent, and it is why a 5-minute retry window is not an option.\nexport const CHECK_INTERVAL_MS = 20 * 60 * 1000\n\n/**\n * What the worker's Claude Code plugin step did. `skipped` = no project-scope install record on this\n * machine; `updated` = every recorded project whose path still exists took `claude plugin update`\n * with exit 0, \"already at the latest version\" included.\n *\n * `skipped-cli-stale` = the step was withheld because a newer CLI exists that this run did not install\n * (`pluginStepWithheld` in `run-update-check.ts`) \u2014 advancing the plugin past the CLI would hand its\n * skills a server that does not serve the tool names they cite.\n */\nexport type PluginUpdateOutcome = 'updated' | 'failed' | 'claude-missing' | 'skipped' | 'skipped-cli-stale'\n\nexport interface PluginUpdateRecord {\n outcome: PluginUpdateOutcome\n /** When the plugin step ran \u2014 stamped separately from `lastCheckMs`, which it never moves. */\n checkedMs: number\n}\n\nexport interface UpdateCache {\n /**\n * When the last check was ATTEMPTED \u2014 never \"when it last succeeded\". An offline user whose fetch\n * throws must still burn their window, or every single command spawns another doomed child.\n */\n lastCheckMs: number\n /** Latest version seen on the registry, or null when the last attempt failed. */\n latestVersion: string | null\n /**\n * The command the USER must run, set only when the worker was not allowed to install for them\n * (Homebrew, or an install location it could not identify). Null means \"handled, say nothing\".\n *\n * The worker decides this, not the reader: identifying the owning package manager can cost an\n * `npm root -g` subprocess, and the CLI startup path must never pay for one. The background child\n * already pays it, so it writes the verdict down.\n */\n updateCommand: string[] | null\n /**\n * The last check's terminal outcome (`installed`, `fetch-failed`, `up-to-date`, \u2026), or absent on a\n * cache written before this field existed.\n *\n * Diagnostic ONLY \u2014 nothing branches on it, {@link isStale} included. It earns its keep by being the\n * one thing that makes this file legible: a successful `installed` and a failed `fetch-failed` both\n * write a byte-identical `{latestVersion:null, updateCommand:null}`, so without this the cache cannot\n * say which happened, and a silent auto-update leaves no other trace of what it did.\n */\n outcome?: string | null\n /**\n * The plugin step's outcome, written beside the CLI outcome by the same worker; absent on a cache\n * written before the step existed. Diagnostic like `outcome`: the plugin is delivered through the\n * marketplace, and this is the only trace on disk of whether the worker asked for it.\n */\n plugin?: PluginUpdateRecord\n}\n\nexport const cacheFilePath = (): string => {\n return path.join(getCacheRoot(), CACHE_FILE_NAME)\n}\n\nconst isStringArray = (value: unknown): value is string[] => {\n return (\n Array.isArray(value) &&\n value.every((entry) => {\n return typeof entry === 'string'\n })\n )\n}\n\nconst isPluginUpdateRecord = (value: unknown): value is PluginUpdateRecord => {\n if (typeof value !== 'object' || value === null) return false\n\n const { outcome, checkedMs } = value as Partial<PluginUpdateRecord>\n\n return typeof outcome === 'string' && typeof checkedMs === 'number' && Number.isFinite(checkedMs)\n}\n\nconst isUpdateCache = (value: unknown): value is UpdateCache => {\n if (typeof value !== 'object' || value === null) return false\n\n const { lastCheckMs, latestVersion, updateCommand, outcome, plugin } = value as Partial<UpdateCache>\n\n return (\n typeof lastCheckMs === 'number' &&\n Number.isFinite(lastCheckMs) &&\n (latestVersion === null || typeof latestVersion === 'string') &&\n (updateCommand === null || isStringArray(updateCommand)) &&\n (outcome === undefined || outcome === null || typeof outcome === 'string') &&\n (plugin === undefined || isPluginUpdateRecord(plugin))\n )\n}\n\n/**\n * Read the cache, or null when it is missing/unreadable/corrupt. A null read means \"stale\" to every\n * caller, so a first run and a hand-mangled file behave identically: check again.\n *\n * @example\n * readUpdateCache() // => { lastCheckMs: 1770000000000, latestVersion: '0.1.131' } | null\n */\nexport const readUpdateCache = (\n readFile: (p: string) => string = (p) => {\n return fs.readFileSync(p, 'utf8')\n },\n): UpdateCache | null => {\n try {\n const parsed: unknown = JSON.parse(readFile(cacheFilePath()))\n\n return isUpdateCache(parsed) ? parsed : null\n } catch {\n return null\n }\n}\n\n/**\n * Persist the check result. Creates `$cacheRoot` first: `atomicWriteFileSync` writes a temp file\n * beside the target and renames, so it throws ENOENT on a machine that has never had a `~/.cache/infra-kit`.\n * Without this mkdir the write fails, `lastCheckMs` never lands, and the throttle degrades into a\n * detached-child spawn on every invocation.\n *\n * @example\n * writeUpdateCache({ lastCheckMs: Date.now(), latestVersion: '0.1.131' })\n */\nexport const writeUpdateCache = (cache: UpdateCache): void => {\n const root = getCacheRoot()\n\n fs.mkdirSync(root, { recursive: true })\n atomicWriteFileSync(path.join(root, CACHE_FILE_NAME), JSON.stringify(cache), 0o600)\n}\n\n/**\n * Has the throttle window elapsed? A missing cache is stale by definition (first run).\n *\n * A `lastCheckMs` in the future (clock skew, or a restored backup) also reads as stale rather than\n * locking the user out of updates until their clock catches up.\n *\n * The window does NOT depend on `outcome`: every last outcome, transient or settled, is re-checked\n * after {@link CHECK_INTERVAL_MS}. See that constant for why a shorter transient-failure window is not\n * worth its cost here.\n *\n * @example\n * isStale(null, 1_000) // => true\n * isStale({ lastCheckMs: 0, latestVersion: null, updateCommand: null }, CHECK_INTERVAL_MS + 1) // => true\n */\nexport const isStale = (cache: UpdateCache | null, nowMs: number): boolean => {\n if (!cache) return true\n\n const elapsed = nowMs - cache.lastCheckMs\n\n if (elapsed < 0) return true\n\n return elapsed >= CHECK_INTERVAL_MS\n}\n", "/**\n * The Claude Code plugin half of the update worker: `claude plugin update infra-kit@infra-kit` for\n * every project this machine has the plugin installed in.\n *\n * WHY THE WORKER AND NOT CLAUDE CODE: Claude Code never advances an installed plugin on its own and\n * prints nothing for installed-but-old, so a plugin bump on `main` reached nobody until they typed\n * `claude plugin update` by hand \u2014 the CLI kept itself fresh while the skills it ships with rotted.\n * This step rides the same throttled child, so a plugin-only bump is delivered on the next check.\n *\n * Everything here is imported statically (see `run-update-check.ts`): on the `installed` path this runs\n * AFTER `npm install -g` has replaced `dist/`, where a lazy `import()` of a chunk is unsafe.\n */\nimport { spawnSync } from 'node:child_process'\nimport fs from 'node:fs'\nimport process from 'node:process'\n\nimport {\n CLAUDE_BIN,\n CLAUDE_VERSION_ARGV,\n PLUGIN_UPDATE_ARGV,\n defaultClaudeRunner,\n} from 'src/lib/plugin-pointer/claude-cli'\nimport type { ClaudeRunner } from 'src/lib/plugin-pointer/claude-cli'\nimport { listProjectPluginInstallations } from 'src/lib/plugin-pointer/install-state'\nimport type { PluginInstallation } from 'src/lib/plugin-pointer/install-state'\nimport { withoutPackageManagerEnv } from 'src/lib/pm-env'\n\nimport type { PluginUpdateOutcome } from './update-cache'\n\n/**\n * Probe plus every per-project run, end to end. Well inside `LOCK_STALE_MS` (30 min) even after a slow\n * CLI install has already spent minutes of the same lock \u2014 a plugin step that outlived the lock would\n * hand the next shell's worker a reaped lock and the concurrent-install pile-up the lock exists to stop.\n */\nexport const PLUGIN_UPDATE_BUDGET_MS = 5 * 60 * 1000\n\n/** One `claude plugin update` measured at \u22480.7 s; a minute is a wedged network, not a slow one. */\nexport const PLUGIN_UPDATE_RUN_TIMEOUT_MS = 60_000\n\nexport interface UpdatePluginDeps {\n env?: NodeJS.ProcessEnv\n /** Deadline source, injected so the budget is testable without real time. */\n clock?: () => number\n /** The `claude --version` probe. Output is captured and discarded; only `ok` is read. */\n runClaude?: ClaudeRunner\n /** The per-project update spawn \u2014 its own seam, so the CLI-install spawn's call count stays honest. */\n spawnPluginUpdate?: typeof spawnSync\n /** The project-scope install records; defaults to reading `~/.claude/plugins/installed_plugins.json`. */\n listPluginInstallations?: () => readonly PluginInstallation[]\n pathExists?: (target: string) => boolean\n}\n\ntype RecordResult = 'ok' | 'skipped' | 'failed'\n\ninterface UpdateContext {\n spawn: typeof spawnSync\n env: NodeJS.ProcessEnv\n clock: () => number\n deadlineMs: number\n pathExists: (target: string) => boolean\n}\n\n/**\n * The child env: npx/dlx markers stripped like every other child this CLI spawns, plus `CLAUDECODE`\n * \u2014 the marker a nested Claude Code refuses to start under. Whether `plugin update` shares that\n * refusal was not measured; the spike that proved this command ran without it, so it stays out.\n */\nconst pluginUpdateEnv = (env: NodeJS.ProcessEnv): NodeJS.ProcessEnv => {\n const child = withoutPackageManagerEnv(env)\n\n delete child.CLAUDECODE\n\n return child\n}\n\nconst isEnoent = (error: Error): boolean => {\n return (error as NodeJS.ErrnoException).code === 'ENOENT'\n}\n\n/**\n * One recorded project. `cwd` is the whole mechanism: `claude` resolves WHICH project-scope record an\n * update touches from the cwd, and from a non-project directory it silently picks some other record.\n *\n * A path that no longer exists is skipped, never a failure \u2014 `installed_plugins.json` is append-only,\n * so records for deleted checkouts outlive the checkouts. `ENOENT` from the spawn is the same case\n * caught a moment later (the directory vanished between the check and the spawn).\n */\nconst updateRecordedProject = (record: PluginInstallation, ctx: UpdateContext): RecordResult => {\n const projectPath = record.projectPath\n\n if (projectPath === null || !ctx.pathExists(projectPath)) return 'skipped'\n\n const remainingMs = ctx.deadlineMs - ctx.clock()\n\n if (remainingMs <= 0) return 'failed'\n\n const result = ctx.spawn(CLAUDE_BIN, [...PLUGIN_UPDATE_ARGV], {\n cwd: projectPath,\n // Silent, like the CLI install: this is a detached worker with nowhere to write.\n stdio: 'ignore',\n shell: process.platform === 'win32',\n env: ctx.env,\n timeout: Math.min(PLUGIN_UPDATE_RUN_TIMEOUT_MS, remainingMs),\n windowsHide: true,\n })\n\n if (result.error) return isEnoent(result.error) ? 'skipped' : 'failed'\n\n return result.signal === null && result.status === 0 ? 'ok' : 'failed'\n}\n\nconst updatePluginUnguarded = (deps: UpdatePluginDeps): PluginUpdateOutcome => {\n const clock = deps.clock ?? Date.now\n // The probe's own timeout (`CLAUDE_TIMEOUT_MS`) is inside this budget, not in addition to it.\n const deadlineMs = clock() + PLUGIN_UPDATE_BUDGET_MS\n const runClaude = deps.runClaude ?? defaultClaudeRunner\n\n // The only gate besides the lock. `canSelfSpawn` is a question about where npm put the CLI and says\n // nothing about `claude`; a Homebrew-owned CLI still gets its plugin updated.\n if (!runClaude({ args: CLAUDE_VERSION_ARGV }).ok) return 'claude-missing'\n\n const records = (deps.listPluginInstallations ?? listProjectPluginInstallations)()\n\n if (records.length === 0) return 'skipped'\n\n const ctx: UpdateContext = {\n spawn: deps.spawnPluginUpdate ?? spawnSync,\n env: pluginUpdateEnv(deps.env ?? process.env),\n clock,\n deadlineMs,\n pathExists: deps.pathExists ?? fs.existsSync,\n }\n\n // Every record gets its turn even after one fails: the records are independent projects, and a\n // failure in one is no reason to leave another stale. The verdict is still `failed` \u2014 the manual\n // command is what fixes the one that did not take.\n const results = records.map((record) => {\n return updateRecordedProject(record, ctx)\n })\n\n if (results.includes('failed')) return 'failed'\n\n return results.includes('ok') ? 'updated' : 'skipped'\n}\n\n/**\n * Update the installed plugin in every recorded project. NEVER throws: a plugin failure is a recorded\n * outcome, not a reason to lose the CLI outcome the worker has already produced.\n *\n * @example\n * updatePlugin({ listPluginInstallations: () => [] }) // => 'skipped'\n */\nexport const updatePlugin = (deps: UpdatePluginDeps = {}): PluginUpdateOutcome => {\n try {\n return updatePluginUnguarded(deps)\n } catch {\n return 'failed'\n }\n}\n", "import { spawnSync } from 'node:child_process'\n\nimport { MARKETPLACE_NAME, PLUGIN_KEY } from './names'\n\n/**\n * @fileoverview\n *\n * The `claude` binary as a dependency: the argv this CLI hands it and the one seam through which it is\n * spawned. Kept apart from `install-plugin.ts` (and its logger import) because the detached update\n * worker runs `claude plugin update` too, and that worker must stay a bundle of `node:child_process`\n * plus the cache writer \u2014 nothing that constructs a logger at import time.\n */\n\n/** Trust the `claude` binary resolved from `PATH`; there is no configured path and none is wanted. */\nexport const CLAUDE_BIN = 'claude'\n\n/**\n * Ceiling for any one `claude` invocation. `plugin install` clones a marketplace repo, so it is not\n * instant \u2014 but an `initCore` that hangs forever on a wedged network is worse than one that reports a\n * failed install step, and the user's remaining setup steps are behind this call.\n */\nexport const CLAUDE_TIMEOUT_MS = 120_000\n\n/** The PATH probe. Cheapest command that fails loudly when the binary is absent. */\nexport const CLAUDE_VERSION_ARGV: readonly string[] = ['--version']\n\n/**\n * `claude plugin update infra-kit@infra-kit --scope project -y`, as argv.\n *\n * Every flag is load-bearing (measured on Claude Code 2.1.270). Without `--scope project` the command\n * resolves USER scope from every cwd and fails \"not installed at scope user\" \u2014 while still refreshing\n * the marketplace clone as a side effect, so the failure looks like progress. `-y` is required when\n * stdio is not a TTY, which is every spawn from this CLI. WHICH project record the update advances is\n * decided by the command's cwd, not by any flag: callers pass `cwd: <recorded projectPath>`.\n */\nexport const PLUGIN_UPDATE_ARGV: readonly string[] = ['plugin', 'update', PLUGIN_KEY, '--scope', 'project', '-y']\n\n/**\n * `claude plugin marketplace update infra-kit`, as argv: run before `plugin update`, so the update\n * compares against what `main` serves now and not against the marketplace clone of the last refresh.\n */\nexport const MARKETPLACE_UPDATE_ARGV: readonly string[] = ['plugin', 'marketplace', 'update', MARKETPLACE_NAME]\n\n/** One `claude` invocation. `cwd` matters: `--scope project` records the directory it ran in. */\nexport interface ClaudeCommand {\n args: readonly string[]\n cwd?: string\n}\n\nexport interface ClaudeCommandResult {\n ok: boolean\n /** Diagnostic text from the command. Only read when `ok` is false. */\n output?: string\n}\n\n/** The injectable seam. Tests pass a fake so a suite never installs a plugin on the developer's machine. */\nexport type ClaudeRunner = (command: ClaudeCommand) => ClaudeCommandResult\n\n/**\n * Run one `claude` command, capturing its output instead of inheriting the terminal.\n *\n * Captured, not inherited, because `initCore` renders its own progress: a raw `plugin install` transcript\n * dumped between two `INFO:` lines reads as a crash. The captured text is not discarded \u2014 its first\n * line is what the failure warning quotes.\n *\n * @example\n * defaultClaudeRunner({ args: ['--version'] }) // => { ok: true, output: '2.0.0 (Claude Code)' }\n */\nexport const defaultClaudeRunner: ClaudeRunner = (command) => {\n // PATH lookup is the point: `claude` is installed by its own installer to a location this CLI does\n // not know and must not guess, and \"is it on PATH\" is exactly the question step (b) asks.\n // eslint-disable-next-line sonarjs/no-os-command-from-path\n const result = spawnSync(CLAUDE_BIN, [...command.args], {\n cwd: command.cwd,\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'pipe'],\n timeout: CLAUDE_TIMEOUT_MS,\n })\n\n if (result.error !== undefined) return { ok: false, output: result.error.message }\n\n const output = `${result.stderr ?? ''}\\n${result.stdout ?? ''}`.trim()\n\n return { ok: result.status === 0, output }\n}\n", "/**\n * The three names every plugin-pointer module agrees on, in a file that imports NOTHING.\n *\n * Split out of `plugin-pointer.ts` because that module constructs the pino logger at import time,\n * and the detached update worker (`dist/update-check.js`) needs `PLUGIN_KEY` to spawn\n * `claude plugin update` \u2014 a worker that logs to an ignored stderr has no business paying for pino.\n */\n\n/** The marketplace name, as it appears in `extraKnownMarketplaces` and in `<plugin>@<marketplace>`. */\nexport const MARKETPLACE_NAME = 'infra-kit'\n\n/** The `enabledPlugins` key: `<plugin>@<marketplace>`, both `infra-kit`. */\nexport const PLUGIN_KEY = 'infra-kit@infra-kit'\n\n/** The GitHub repo the marketplace is served from. */\nexport const MARKETPLACE_REPO = 'ArthurSaenz/infra-kit'\n", "import fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\n\nimport { MARKETPLACE_NAME, PLUGIN_KEY } from './names'\n\n/**\n * @fileoverview\n *\n * HOST-STATE readers: what Claude Code itself records on this machine about the `infra-kit`\n * marketplace and plugin. The consumer repo's own `.mcp.json` is read by `mcp-registration.ts`.\n *\n * All of it is read-only and total \u2014 every reader answers \"not registered / not installed\" for an\n * absent, unreadable or malformed file rather than throwing. `initCore` prints an install hint from\n * these answers and `doctor` reports them as rows, and neither may crash because a user hand-edited\n * a JSON file under `~/.claude/`.\n *\n * These files are Claude Code's, not ours. Nothing here writes to them: installing a plugin is the\n * `claude plugin install` command's job, and a CLI that edited another tool's state directory would\n * be repairing something it does not own.\n */\n\n/** Where Claude Code keeps its plugin bookkeeping, relative to `$HOME`. */\nconst PLUGINS_DIR = ['.claude', 'plugins']\n\n/** One entry of `installed_plugins.json`'s per-plugin array. Extra fields are ignored, not rejected. */\nexport interface PluginInstallation {\n scope: string | null\n projectPath: string | null\n installPath: string | null\n version: string | null\n}\n\nconst readJsonFile = (filePath: string): unknown => {\n try {\n return JSON.parse(fs.readFileSync(filePath, 'utf-8'))\n } catch {\n return undefined\n }\n}\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> => {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nconst readString = (source: Record<string, unknown>, key: string): string | null => {\n const value = source[key]\n\n return typeof value === 'string' ? value : null\n}\n\n/** `~/.claude/plugins/<file>`, with `$HOME` injectable so tests never read the developer's own state. */\nconst pluginsPath = (file: string, home: string = os.homedir()): string => {\n return path.join(home, ...PLUGINS_DIR, file)\n}\n\n/**\n * Is the `infra-kit` marketplace registered on this machine?\n *\n * Registration is a machine fact (`~/.claude/plugins/known_marketplaces.json`), distinct from the\n * per-project `extraKnownMarketplaces` pointer `initCore` writes: the pointer says where to look, this\n * says the lookup has actually happened.\n *\n * @example\n * isMarketplaceRegistered() // => true once `claude plugin marketplace add ArthurSaenz/infra-kit` has run\n */\nexport const isMarketplaceRegistered = (home?: string): boolean => {\n const parsed = readJsonFile(pluginsPath('known_marketplaces.json', home))\n\n return isPlainObject(parsed) && MARKETPLACE_NAME in parsed\n}\n\nconst toInstallation = (entry: unknown): PluginInstallation | null => {\n if (!isPlainObject(entry)) return null\n\n return {\n scope: readString(entry, 'scope'),\n projectPath: readString(entry, 'projectPath'),\n installPath: readString(entry, 'installPath'),\n version: readString(entry, 'version'),\n }\n}\n\nconst readInstallations = (home?: string): PluginInstallation[] => {\n const parsed = readJsonFile(pluginsPath('installed_plugins.json', home))\n\n if (!isPlainObject(parsed) || !isPlainObject(parsed.plugins)) return []\n\n const entries = parsed.plugins[PLUGIN_KEY]\n\n if (!Array.isArray(entries)) return []\n\n return entries.flatMap((entry): PluginInstallation[] => {\n const installation = toInstallation(entry)\n\n return installation === null ? [] : [installation]\n })\n}\n\nexport interface PluginInstallationQuery {\n /** Override `$HOME` (tests, and nothing else). */\n home?: string\n /** The project root being asked about. Omitted, only a user-scope entry can count as installed. */\n projectPath?: string\n}\n\n/**\n * Whether the plugin is installed FOR THIS PROJECT \u2014 not merely present somewhere on the machine.\n *\n * `elsewhere` is the distinction that matters: it carries the records that exist, so the caller can\n * say which other project they belong to instead of reporting a bare \"not installed\".\n */\nexport type PluginInstallState =\n | { kind: 'installed'; installation: PluginInstallation }\n | { kind: 'elsewhere'; installations: readonly PluginInstallation[] }\n | { kind: 'absent' }\n\n/**\n * A path in the form the two sides can be compared in. `realpath` on both sides or neither: on macOS\n * `/tmp` resolves to `/private/tmp`, so a resolved path compared against an unresolved one never\n * matches. Falls back to `path.resolve` for a path that no longer exists, which `realpath` rejects.\n */\nconst canonicalPath = (target: string): string => {\n try {\n return fs.realpathSync(target)\n } catch {\n return path.resolve(target)\n }\n}\n\n/**\n * Does one record make the plugin active in `root`?\n *\n * A `user`-scope install is machine-wide and covers every project. Otherwise the record has to NAME\n * this project: `claude plugin install --scope project` stores the directory the session was started\n * in, so an install done for another repo covers that repo and nothing else.\n *\n * A record whose `projectPath` is this root counts whatever scope string it carries \u2014 the path is the\n * stronger signal, and an older Claude Code wrote records with no `scope` field at all.\n */\nconst coversProject = (entry: PluginInstallation, root: string | null): boolean => {\n if (entry.scope === 'user') return true\n if (root === null || entry.projectPath === null) return false\n\n return canonicalPath(entry.projectPath) === root\n}\n\n/**\n * Resolve what `~/.claude/plugins/installed_plugins.json` says about THIS project.\n *\n * @example\n * resolvePluginInstall({ projectPath: '/repo' })\n * // => { kind: 'elsewhere', installations: [{ projectPath: '/other', \u2026 }] } // installed, but not here\n */\nexport const resolvePluginInstall = (query: PluginInstallationQuery = {}): PluginInstallState => {\n const installations = readInstallations(query.home)\n\n if (installations.length === 0) return { kind: 'absent' }\n\n const root = query.projectPath === undefined ? null : canonicalPath(query.projectPath)\n\n const covering = installations.find((entry) => {\n return coversProject(entry, root)\n })\n\n return covering === undefined ? { kind: 'elsewhere', installations } : { kind: 'installed', installation: covering }\n}\n\n/**\n * Every PROJECT-scope record, one per distinct project path \u2014 the set `claude plugin update` has to be\n * run against, because Claude Code resolves which record an update touches from the command's cwd\n * (measured: from a non-project cwd it silently picks some other record, so one run from `$HOME`\n * advances one project, not all).\n *\n * A record with no `scope` but a `projectPath` counts as project scope, the way `coversProject`\n * treats it. User-scope records are excluded: this CLI never installs at user scope, and an update\n * there needs no cwd at all. Duplicates collapse on the canonical path \u2014 the file is append-only, so\n * a reinstalled project may carry two records naming one directory.\n *\n * @example\n * listProjectPluginInstallations()\n * // => [{ scope: 'project', projectPath: '/Users/me/hulyo', installPath: '\u2026/0.7.0', version: '0.7.0' }]\n */\nexport const listProjectPluginInstallations = (home?: string): PluginInstallation[] => {\n const seen = new Set<string>()\n\n return readInstallations(home).filter((entry) => {\n if (entry.scope === 'user' || entry.projectPath === null) return false\n\n const key = canonicalPath(entry.projectPath)\n\n if (seen.has(key)) return false\n\n seen.add(key)\n\n return true\n })\n}\n\n/**\n * The installed plugin's version: the record's own `version`, falling back to the manifest at\n * `<installPath>/.claude-plugin/plugin.json`.\n *\n * The fallback is not redundant. `installed_plugins.json` records a git-commit sha as `version` for\n * marketplaces served from a repo, so the manifest is the only place a human-readable `0.4.0` lives\n * \u2014 and an entry written by an older Claude Code may carry no `version` at all.\n *\n * @example\n * readInstalledPluginVersion({ scope: 'project', projectPath: null, installPath: '/cache/infra-kit', version: null })\n * // => '0.4.0' (read from /cache/infra-kit/.claude-plugin/plugin.json)\n */\nexport const readInstalledPluginVersion = (installation: PluginInstallation): string | null => {\n if (installation.installPath === null) return installation.version\n\n const manifest = readJsonFile(path.join(installation.installPath, '.claude-plugin', 'plugin.json'))\n const declared = isPlainObject(manifest) ? readString(manifest, 'version') : null\n\n return declared ?? installation.version\n}\n\n/**\n * The plugin's directory inside the marketplace clone: `plugins/<plugin name>`, the left half of\n * `<plugin>@<marketplace>`. Derived, not spelled out again, so the two can never disagree.\n */\nconst [PLUGIN_NAME = MARKETPLACE_NAME] = PLUGIN_KEY.split('@')\n\n/**\n * The plugin version the marketplace CLONE carries \u2014 `~/.claude/plugins/marketplaces/<marketplace>/\n * plugins/<plugin>/.claude-plugin/plugin.json` \u2014 which is NOT what a session serves.\n *\n * Measured (plan \u00A76.1 S0-7(b)): sessions load the plugin from the install record's cache\n * `installPath`, and only a successful `claude plugin update --scope project` advances that record,\n * while any `claude plugin update` \u2014 a failing user-scope one included \u2014 refreshes the clone as a\n * side effect. So the clone can sit AHEAD of the served copy, and comparing the two is the one\n * freshness signal readable offline: \"fetched but not applied\". Reads as `null` for a clone that is\n * absent, unreadable or carries no version, so the caller drops the comparison instead of failing.\n *\n * @example\n * readMarketplacePluginVersion() // => '0.8.0' after a `claude plugin update` refreshed the clone\n * readMarketplacePluginVersion({ home: '/tmp/no-such-home' }) // => null\n */\nexport const readMarketplacePluginVersion = ({ home }: Pick<PluginInstallationQuery, 'home'> = {}): string | null => {\n const manifestPath = pluginsPath(\n path.join('marketplaces', MARKETPLACE_NAME, 'plugins', PLUGIN_NAME, '.claude-plugin', 'plugin.json'),\n home,\n )\n const manifest = readJsonFile(manifestPath)\n\n return isPlainObject(manifest) ? readString(manifest, 'version') : null\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["spawnSync", "homedir", "process", "fs", "path", "process", "LOCK_FILE_NAME", "LOCK_STALE_MS", "lockFilePath", "path", "getCacheRoot", "reapIfStale", "lockPath", "nowMs", "fs", "acquireUpdateLock", "deps", "open", "fd", "process", "holder", "DEFAULT_REGISTRY", "FETCH_TIMEOUT_MS", "configuredRegistry", "env", "key", "value", "registryUrl", "configured", "base", "fetchLatestVersion", "fetchFn", "controller", "timer", "response", "PACKAGE_NAME", "version", "RELEASE_PATTERN", "parseSemver", "version", "withoutBuild", "core", "prereleaseParts", "match", "major", "minor", "patch", "NUMERIC_IDENTIFIER", "compareIdentifier", "left", "right", "leftNumeric", "rightNumeric", "comparePrerelease", "a", "b", "index", "isNewerVersion", "latest", "current", "parsedLatest", "parsedCurrent", "latestIsFinal", "currentIsFinal", "spawn", "homedir", "process", "ALLOWED_KEYS", "ALLOWED_PREFIXES", "isAllowed", "key", "prefix", "unattendedChildEnv", "env", "AUTO_SETUP_ARGV", "defaultSpawnDetached", "command", "args", "env", "child", "spawn", "homedir", "process", "startAutoSetup", "deps", "unattendedChildEnv", "PACKAGE_NAME", "fs", "path", "CACHE_FILE_NAME", "CHECK_INTERVAL_MS", "cacheFilePath", "path", "getCacheRoot", "isStringArray", "value", "entry", "isPluginUpdateRecord", "outcome", "checkedMs", "isUpdateCache", "lastCheckMs", "latestVersion", "updateCommand", "plugin", "readUpdateCache", "readFile", "p", "fs", "parsed", "writeUpdateCache", "cache", "root", "atomicWriteFileSync", "isStale", "nowMs", "elapsed", "spawnSync", "fs", "process", "spawnSync", "MARKETPLACE_NAME", "PLUGIN_KEY", "MARKETPLACE_REPO", "CLAUDE_BIN", "CLAUDE_TIMEOUT_MS", "CLAUDE_VERSION_ARGV", "PLUGIN_UPDATE_ARGV", "PLUGIN_KEY", "MARKETPLACE_UPDATE_ARGV", "MARKETPLACE_NAME", "defaultClaudeRunner", "command", "result", "spawnSync", "output", "fs", "os", "path", "PLUGINS_DIR", "readJsonFile", "filePath", "fs", "isPlainObject", "value", "readString", "source", "key", "pluginsPath", "file", "home", "os", "path", "isMarketplaceRegistered", "parsed", "MARKETPLACE_NAME", "toInstallation", "entry", "readInstallations", "entries", "PLUGIN_KEY", "installation", "canonicalPath", "target", "coversProject", "root", "resolvePluginInstall", "query", "installations", "covering", "listProjectPluginInstallations", "seen", "readInstalledPluginVersion", "manifest", "PLUGIN_NAME", "readMarketplacePluginVersion", "manifestPath", "PLUGIN_UPDATE_BUDGET_MS", "PLUGIN_UPDATE_RUN_TIMEOUT_MS", "pluginUpdateEnv", "env", "child", "withoutPackageManagerEnv", "isEnoent", "error", "updateRecordedProject", "record", "ctx", "projectPath", "remainingMs", "result", "CLAUDE_BIN", "PLUGIN_UPDATE_ARGV", "process", "updatePluginUnguarded", "deps", "clock", "deadlineMs", "defaultClaudeRunner", "CLAUDE_VERSION_ARGV", "records", "listProjectPluginInstallations", "spawnSync", "fs", "results", "updatePlugin", "PARENT_POLL_INTERVAL_MS", "PARENT_WAIT_TIMEOUT_MS", "VERSION_PROBE_TIMEOUT_MS", "readInstalledVersion", "env", "result", "spawnSync", "PACKAGE_NAME", "process", "homedir", "version", "defaultIsProcessAlive", "pid", "defaultSleep", "ms", "resolve", "waitForParentExit", "parentPid", "deps", "deadline", "runUpdateCheck", "currentVersion", "acquireLock", "acquireUpdateLock", "writeCache", "writeUpdateCache", "release", "written", "recordingWriteCache", "cache", "outcome", "runUpdateCheckLocked", "recordPluginUpdate", "handOff", "startAutoSetup", "pluginStepWithheld", "latestVersion", "isNewerVersion", "base", "updatePlugin", "checkedMs", "nowMs", "fetchLatest", "fetchLatestVersion", "isProcessAlive", "sleep", "clock", "lazyNpmRoot", "defaultLazyNpmRoot", "spawn", "installedVersion", "finish", "canSelfSpawn", "updateCommand", "detectInstallManager", "safeRealpath", "packageManagerInstallEnv"]
|
|
7
|
-
}
|