infra-kit 0.3.2 → 0.3.4

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/entry/dev-server.ts", "../src/dev/cmux-dev.ts", "../src/dev/cmux-layout.ts", "../src/dev/signal-shutdown.ts", "../src/dev/managed-child.ts", "../src/dev/crash-barrier.ts", "../src/dev/dev-server.ts", "../src/dev/dep-closure.ts", "../src/dev/local-pairing.ts", "../src/dev/log-attribution.ts", "../src/dev/log-sink.ts", "../src/dev/terminal-liveness.ts", "../src/dev/output-intercept.ts", "../src/dev/ports.ts", "../src/dev/serverless-local-run.ts", "../src/dev/turbo-watch.ts", "../src/dev/ui-dev.ts"],
4
- "sourcesContent": ["/**\n * Long-running CLI entry for the local dev-server. Owns flag parsing and wires signal\n * handling to `src/dev/signal-shutdown` \u2014 the orchestrator (`src/dev/dev-server`) stays\n * signal- and exit-agnostic. Kept off the eager cli.js graph: entry/cli.ts reaches this\n * module (and the fastify/chokidar it pulls in) only via `await import(...)`, so\n * those heavy deps never load on the machine command paths.\n */\nimport { Command } from 'commander'\nimport process from 'node:process'\nimport { pathToFileURL } from 'node:url'\n\nimport { runCmuxDevServer } from 'src/dev/cmux-dev'\nimport { formatFault, registerCrashBarrier } from 'src/dev/crash-barrier'\nimport { run } from 'src/dev/dev-server'\nimport type { DevServerOptions, DevServerRunner } from 'src/dev/dev-server'\nimport type { WizardResult } from 'src/dev/dev-wizard-run'\nimport { resolveSelfAppName } from 'src/dev/discovery'\nimport { rawStdoutWrite } from 'src/dev/log-sink'\nimport { killDescendantGroupsNow } from 'src/dev/managed-child'\nimport { explainTargetKey } from 'src/dev/presets'\nimport { registerSignalShutdown } from 'src/dev/signal-shutdown'\nimport { installTerminalLiveness } from 'src/dev/terminal-liveness'\nimport { isCmuxAvailable } from 'src/integrations/cmux'\nimport { isPromptCancellation } from 'src/lib/errors/is-prompt-cancellation'\nimport type { DevPreset } from 'src/lib/infra-kit-config'\n\n/**\n * Exit code when the session dies because its own stdio is unwritable. Not 0 \u2014 the process did not stop\n * voluntarily \u2014 and not `128 + signo`, because no signal was necessarily involved: a full disk under\n * `nohup ik dev > out.log` reaches this path with `ENOSPC` and nobody sent anything.\n */\nconst FATAL_EXIT_CODE = 1\n\n/**\n * How long the fatal path waits for a graceful teardown before SIGKILLing the descendant groups and\n * leaving. Mirrors `signal-shutdown`'s deadline, and for the same reason: on this path there is by\n * definition no operator watching, so \"wait forever\" means \"spin forever\".\n */\nconst FATAL_TEARDOWN_DEADLINE_MS = 20_000\n\n/** Raw option object as produced by Commander (comma-joined strings). */\nexport interface DevCliOptions {\n watch?: boolean\n app?: string\n /**\n * Comma-separated `<app>/<part>` target keys (`--target=client/api,client/ui`). The part-level\n * selector `--app` cannot express: `--app=client` expands to every part `client` has. Same grammar as\n * a `devServersPresets` key, so a wizard selection round-trips into a command you can paste.\n */\n target?: string\n /** Named preset positional (`infra-kit dev <preset>`); selects launch targets from `devServersPresets`. */\n preset?: string\n cmux?: boolean\n self?: boolean\n verbose?: boolean\n /** Print each app's registered routes at startup (opt-in; off keeps the calm default screen). */\n routes?: boolean\n /**\n * Commander's negated flag: `true` by default, `false` only when `--no-ui-health` was passed. Off means\n * the frontends are never probed and their rows carry no health dot.\n */\n uiHealth?: boolean\n}\n\n/**\n * Turn `--target=<app>/<part>,\u2026` into the in-memory preset the runner already understands, or\n * `undefined` when the flag is absent (leaving `preset`/`*` resolution untouched). Validated against the\n * same grammar as a `devServersPresets` key, but with a message that names the FLAG \u2014 a user typing\n * `--target=client` must not be told about `devServersPresets`.\n */\nconst toPresetDef = (targets: string[] | null): DevPreset | undefined => {\n if (targets == null) return undefined\n\n for (const key of targets) {\n if (explainTargetKey(key) !== null) {\n throw new Error(`infra-kit dev: invalid --target \"${key}\" (expected \"<app>/api\" or \"<app>/ui\").`)\n }\n }\n\n return {\n apps: Object.fromEntries(\n targets.map((key) => {\n return [key, {}]\n }),\n ),\n }\n}\n\n/** Split a comma-separated flag value into a trimmed, non-empty list (`null` when unset/empty). */\nconst splitList = (value: string | undefined): string[] | null => {\n if (value == null) return null\n\n const parts = value\n .split(',')\n .map((s) => {\n return s.trim()\n })\n .filter(Boolean)\n\n return parts.length > 0 ? parts : null\n}\n\n/**\n * Map raw Commander flags to the orchestrator's typed options. Shared by the\n * `infra-kit dev` subcommand and the standalone entry so the two never diverge.\n * `self` is passed through as-is here \u2014 it's resolved to `include` later, in\n * `runDevServer`, where `--app` (if also given) is allowed to win.\n */\nexport const toDevServerOptions = (raw: DevCliOptions): DevServerOptions => {\n return {\n watch: raw.watch ?? false,\n include: splitList(raw.app),\n preset: raw.preset,\n presetDef: toPresetDef(splitList(raw.target)),\n cmux: raw.cmux ?? false,\n self: raw.self ?? false,\n verbose: raw.verbose ?? false,\n routes: raw.routes ?? false,\n uiHealth: raw.uiHealth ?? true,\n }\n}\n\n/**\n * Resolve `--self` into an `include` list, without mutating `options`. `--app` (an\n * explicit `include`) wins over `--self` when both are given \u2014 self is only a\n * convenience default for scripts that don't want to hardcode their app name.\n * Lets `resolveSelfAppName`'s error (not inside `apps/<app>/...`) propagate as-is;\n * the caller's top-level catch turns it into a clean message + non-zero exit.\n */\nconst resolveSelfOptions = (options: DevServerOptions): DevServerOptions => {\n if (!options.self || options.include) {\n return options\n }\n\n return { ...options, include: [resolveSelfAppName(process.cwd())] }\n}\n\n/**\n * Seams for {@link createFatalHandler}. `exit` and `forceReap` mirror `signal-shutdown`'s, and for the same\n * reason: the real ones kill the test runner and SIGKILL its children.\n */\nexport interface FatalHandlerDeps {\n /**\n * The runner \u2014 read LATE, on every call, never captured. It is `null` for the whole of boot (all of it\n * happens inside `run()`), and that is the branch the handler exists to get right.\n */\n getRunner: () => { shutdown: () => Promise<void>; fileFault: (detail: string) => void } | null\n /** SIGKILL the descendant process groups. Synchronous by contract \u2014 the process exits on the next line. */\n forceReap?: () => void\n exit?: (code: number) => void\n /** Timer seam returning its cancel; defaults to an `unref`'d `setTimeout`. */\n setTimer?: (handler: () => void, ms: number) => () => void\n deadlineMs?: number\n}\n\nconst defaultFatalTimer = (handler: () => void, ms: number): (() => void) => {\n const timer = setTimeout(handler, ms)\n\n timer.unref()\n\n return (): void => {\n clearTimeout(timer)\n }\n}\n\n/**\n * The one handler for \"our own stdio is unwritable\" \u2014 from the liveness listener (with the errno) and, as a\n * backstop, from the crash barrier.\n *\n * Once-only. Terminal death is ONE event arriving through as many as three channels (the stdio `'error'`\n * event, the kernel's SIGHUP, a fault raised while the streams are already dead). `shutdown()` is memoized\n * so they may all call it; the reap-and-exit here must still happen exactly once.\n *\n * It never PRINTS: printing onto a dead stream is what produced the fault, so the report is FILED into the\n * sink \u2014 the only channel a post-mortem can still read.\n *\n * The `runner == null` branch is not a corner case, it is a REGRESSION GUARD. Today an EIO during boot\n * correctly kills the process (the barrier is not installed yet). A liveness listener that merely swallowed\n * the `'error'` would make boot silently survive a dead terminal \u2014 strictly worse. So boot reaps and exits,\n * synchronously: anything deferred to a later tick does not survive `process.exit`.\n */\nexport const createFatalHandler = ({\n getRunner,\n forceReap = killDescendantGroupsNow,\n exit = (code: number): void => {\n process.exit(code)\n },\n setTimer = defaultFatalTimer,\n deadlineMs = FATAL_TEARDOWN_DEADLINE_MS,\n}: FatalHandlerDeps): ((reason: string) => void) => {\n let fired = false\n\n return (reason: string): void => {\n if (fired) return\n fired = true\n\n const runner = getRunner()\n\n if (runner == null) {\n forceReap()\n exit(FATAL_EXIT_CODE)\n\n return\n }\n\n runner.fileFault(`\\n\u2717 dev-server exiting: ${reason}\\n`)\n\n // Bounded, for the same reason `signal-shutdown` is: on this path nobody is watching, so a teardown\n // that wedges spins forever.\n const cancel = setTimer(() => {\n forceReap()\n exit(FATAL_EXIT_CODE)\n }, deadlineMs)\n\n // The trailing `.catch` is NOT redundant with `shutdown()`'s own. `shutdown()` attaches its handler to\n // the memoized `this.teardown`; `.finally()` returns a NEW, derived promise, and a derived promise\n // carries its own rejection state. `doShutdown()` can reject (`watcher.close`, `turboWatch.kill` and\n // `uiDev.kill` are unguarded), so without this the derived promise rejects unhandled \u2014 on the one path\n // where an `unhandledRejection` feeds straight back into the barrier that called us.\n runner\n .shutdown()\n .finally(() => {\n cancel()\n exit(FATAL_EXIT_CODE)\n })\n .catch(() => {})\n }\n}\n\n/**\n * Start the dev-server, then wait for an OS signal and shut every server down\n * cleanly before exiting. Signal handling and exit are delegated to\n * {@link registerSignalShutdown}: a second signal force-quits, a rejected teardown\n * is logged, and the process exits `128 + signo` rather than a dishonest `0`.\n */\nexport const runDevServer = async (rawOptions: DevServerOptions): Promise<void> => {\n const options = resolveSelfOptions(rawOptions)\n\n // `--cmux`: one workspace, one pane per app. `runCmuxDevServer` owns its own\n // signal handling and never returns, so return before wiring the in-process\n // handlers below. Fall through to single-process dev when cmux isn't installed.\n if (options.cmux) {\n if (await isCmuxAvailable()) {\n await runCmuxDevServer(options)\n\n return\n }\n\n process.stdout.write('cmux not available; falling back to single-terminal dev\\n')\n }\n\n // A settable target, not a `const`: liveness must be armed BEFORE `run()`, because all of boot happens\n // inside it (apps built, turbo and vite spawned, Ink mounted) \u2014 minutes of build during which a developer\n // walks away and closes the window. But `onDeath` can then fire with no runner to tear down, so the fatal\n // handler branches on it (and reads it LATE, through the getter).\n //\n // Installed HERE \u2014 below the `--cmux` early return above, alongside `registerCrashBarrier`, which is\n // deliberately on the same side of it (each pane is its own process). At the top of `runDevServer` the\n // cmux PARENT would get a listener whose `runner` stays `null` forever, and its `onFatal` would take the\n // boot branch: `killDescendantGroupsNow()` + exit, reaping the entire workspace.\n let runner: DevServerRunner | null = null\n\n const onFatal = createFatalHandler({\n getRunner: () => {\n return runner\n },\n })\n\n const liveness = installTerminalLiveness({\n // The errno, never a story. A file-backed stdout emits `'error'` on `ENOSPC` too, so \"the terminal is\n // gone\" would be a lie in the exact scenario (a disk-fill) this exists to fix.\n onDeath: (stream, error) => {\n onFatal(`stdio unwritable: ${stream} ${error.code}`)\n },\n })\n\n runner = await run(options)\n\n // In-process backends share this event loop; a handler's escaped async path would otherwise terminate\n // the whole session. Installed only on the single-process path (the cmux path returned above, each pane\n // being its own process) and only after `run()` succeeds, so a boot failure still exits honestly.\n //\n // `onFault` is not optional decoration. The dev-server owns `process.stderr` for the life of a TTY\n // session (every log line goes to a per-service file, nothing prints), and the barrier's default\n // reporter is a plain stderr write \u2014 so a crash would be silently FILED while the panel kept showing\n // `\u25CF ok` and `\u26A0 0`. Routing it through the runner both counts it (the row turns red) and punches it\n // onto the terminal through the panel's bypass.\n //\n // `isTerminalDead` is the one thing that may turn a fault fatal, and it is a STREAM-IDENTITY question,\n // never an error-code one: a handler writing to a client socket that hung up throws `EPIPE` too, and\n // sniffing for that would let one closed browser tab kill the whole session.\n const boundRunner = runner\n\n registerCrashBarrier({\n onFault: (event, error) => {\n boundRunner.reportFault(formatFault(event, error))\n },\n isTerminalDead: liveness.isDead,\n fileFault: (event, error) => {\n boundRunner.fileFault(formatFault(event, error, false))\n },\n onFatal,\n })\n\n registerSignalShutdown({\n onSignal: async (signal) => {\n // Bypass, not `process.stdout.write`: the interceptor is still installed and suppressing at this\n // point, so a plain write would file this into a log and the user would see nothing after Ctrl-C.\n // Gated on the liveness latch, so a SIGHUP from a terminal that is already gone does not re-arm the\n // very write that killed it.\n rawStdoutWrite(`\\nReceived ${signal}, shutting down dev-server...\\n`)\n await boundRunner.shutdown()\n },\n // The seam that turns the deadline from a blunt force-quit into the instrument that says WHERE the\n // teardown wedged \u2014 the question the incident leaves open.\n describeStall: () => {\n return boundRunner.shutdownStage\n },\n fileReport: (text) => {\n boundRunner.fileFault(text)\n },\n })\n}\n\n/**\n * True when `infra-kit dev` was invoked BARE \u2014 no preset and no selection/mode flag \u2014 in an interactive\n * TTY (both stdin and stdout) and not `--json`. This is the ONLY condition that launches the wizard;\n * every flagged, piped, non-TTY, `--json`, or MCP invocation runs directly from the parsed flags, so no\n * existing script path changes behaviour.\n */\nexport const shouldRunWizard = (raw: DevCliOptions, tty: boolean, json: boolean): boolean => {\n // `--no-ui-health` is deliberately NOT in this list. It selects a diagnostic, not a run plan, and a flag\n // that quietly turns the picker into \"run the entire repo\" is a far bigger surprise than the one it would\n // avoid. The wizard carries it through instead (see `wizardToOptions`), so the user gets both.\n const bare = !raw.preset && !raw.app && !raw.self && !raw.cmux && !raw.watch && !raw.verbose && !raw.routes\n\n return bare && tty && !json\n}\n\n/** Map a wizard result to runner options: the cmux path uses `include`; otherwise the in-memory `presetDef`. */\nconst wizardToOptions = (result: WizardResult, raw: DevCliOptions): DevServerOptions => {\n return {\n watch: result.watch,\n cmux: result.cmux,\n include: result.include ?? null,\n preset: result.preset,\n presetDef: result.presetDef,\n self: false,\n verbose: false,\n routes: false,\n // The wizard asks about the run plan, never about health \u2014 so this rides through from the command line.\n // Dropping it here is what would make `--no-ui-health` silently probe anyway on the one path that\n // reaches the wizard.\n uiHealth: raw.uiHealth ?? true,\n }\n}\n\n/**\n * Entry for the `infra-kit dev` subcommand. On a bare TTY invocation it launches the interactive wizard\n * (lazily imported so its inquirer/config graph stays off every other path), then starts the server with\n * the assembled options; otherwise it runs directly from the parsed flags. A cancelled prompt (Ctrl-C /\n * Esc) or an empty selection exits cleanly without starting a server.\n */\nexport const runDevServerCli = async (raw: DevCliOptions, tty: boolean, json: boolean): Promise<void> => {\n if (shouldRunWizard(raw, tty, json)) {\n const { runDevWizard } = await import('src/dev/dev-wizard-run')\n\n // Warm the Ink dev-UI chunks BEFORE the wizard blocks on human input.\n //\n // `splitting: true` emits these as CONTENT-HASHED sibling chunks, and `selectDevUi` imports them only\n // AFTER the wizard returns (src/dev/dev-server.ts). That leaves a window as long as the user takes to\n // answer \u2014 minutes, realistically \u2014 in which those files are still un-imported. If the background\n // auto-updater installs a new version during it, npm UNLINKS the old hashes (measured: 7 chunk files\n // vanished across a real 0.1.133 \u2192 0.1.134 install), and the deferred import dies on\n // ERR_MODULE_NOT_FOUND the instant the user presses enter.\n //\n // Importing them up-front collapses that window to nothing: ESM caches by URL, so `selectDevUi`'s\n // later import resolves from memory even if the file is gone by then. This is a warm, not a use \u2014\n // failure is ignored, because `selectDevUi` will import them again and surface any real error there.\n // Costs nothing on any other path: it runs only on the bare interactive TTY that is about to load\n // this exact UI anyway.\n const warmed = Promise.all([import('src/tui/dev-ui/persistent-ink-dev-ui'), import('src/tui/safe-stderr')]).catch(\n () => {\n return undefined\n },\n )\n\n let result: WizardResult | null\n\n try {\n result = await runDevWizard()\n } catch (error) {\n if (isPromptCancellation(error)) return\n\n throw error\n }\n\n if (result == null) return\n\n // Settle the warm before starting: from here on the chunks are resident, so a `dist/` swap mid-session\n // can no longer strand the deferred import in `selectDevUi`.\n await warmed\n\n // The wizard only runs on a bare interactive TTY, so tty=true, json=false \u2014 thread them so `run()`\n // selects the Ink boot UI.\n await runDevServer({ ...wizardToOptions(result, raw), tty, json })\n\n return\n }\n\n // Thread the TTY / json signal so `run()` can pick the Ink boot UI (TTY, non-json) vs the plain renderer.\n await runDevServer({ ...toDevServerOptions(raw), tty, json })\n}\n\n/** Parse `node dist/dev-server.js ...` flags and start the server. */\nconst parseAndRun = async (argv: string[]): Promise<void> => {\n const program = new Command()\n\n program\n .name('infra-kit-dev-server')\n .description('Run local dev servers for the apps in a named devServersPresets preset (or all apps)')\n .argument('[preset]', 'Named preset from devServersPresets (omit to run every app)')\n .option('-w, --watch', 'Rebuild and restart on file save')\n .option('--app <names>', 'Further narrow to these app folder names (comma-separated)')\n .option(\n '--cmux',\n 'Run each app in its own cmux pane (one workspace, N panes; falls back to single terminal if cmux is unavailable)',\n )\n .option('--self', 'Run only the app of the current directory (infer from cwd; use inside apps/<app>/\u2026)')\n .option('-V, --verbose', 'Print full boot narration (default: quiet; full detail always in the session log)')\n .option('--routes', 'Print each app\u2019s registered METHOD /path routes at startup (default: off)')\n\n program.parse(argv)\n\n await runDevServer(toDevServerOptions({ ...program.opts<DevCliOptions>(), preset: program.args[0] }))\n}\n\n// Self-execute only when run directly (`node dist/dev-server.js`), not when the\n// `infra-kit dev` subcommand imports this module for its exported helpers.\nif (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {\n parseAndRun(process.argv).catch((error: unknown) => {\n // Message only, matching the `infra-kit dev` path: a bad preset name or target key is a\n // config mistake, and its stack frames are noise.\n console.error(error instanceof Error ? error.message : String(error))\n process.exit(1)\n })\n}\n", "/**\n * Resident `infra-kit dev --cmux` supervisor.\n *\n * Opens ONE cmux workspace with one pane per discovered backend API app, each\n * pane running the primitive `pnpm exec infra-kit dev --app=<name>` (single-app,\n * single-process). Stays resident: on SIGINT/SIGTERM it closes the workspace\n * (reaping every pane) and exits. The pure command/layout construction lives here\n * and in {@link file://./cmux-layout.ts} so it is unit-testable without a shell.\n */\nimport * as path from 'node:path'\nimport process from 'node:process'\n\nimport { closeCmuxDevWorkspace, openCmuxDevWorkspace } from 'src/integrations/cmux'\nimport { logger } from 'src/lib/logger'\n\nimport { buildCmuxLayout } from './cmux-layout.js'\nimport type { DevServerOptions } from './dev-server.js'\nimport { discoverApiApps, findMonorepoRoot, normalizeAppInclude } from './discovery.js'\nimport type { DiscoveredApiApp } from './discovery.js'\nimport { registerSignalShutdown } from './signal-shutdown.js'\n\n/** One cmux pane: the app it runs, and (optionally) the exact `<app>/<part>` targets selected for it. */\nexport interface PaneSpec {\n app: string\n /**\n * Exact target keys for this pane. When absent the pane falls back to `--app=<name>`, which expands to\n * EVERY part the app has (both the api and the ui glob targets) \u2014 right for a plain `--cmux`, wrong for a wizard selection\n * that deliberately unticked a part. `--app` has app-name granularity and cannot express `<app>/api`\n * alone; that is exactly what `--target` exists for.\n */\n targets?: string[]\n}\n\n/** Build the per-pane primitive command for each pane (append `--watch` when requested). */\nexport const buildPaneCommands = (panes: PaneSpec[], watch: boolean): string[] => {\n return panes.map(({ app, targets }) => {\n const selector = targets && targets.length > 0 ? `--target=${targets.join(',')}` : `--app=${app}`\n\n return `pnpm exec infra-kit dev ${selector}${watch ? ' --watch' : ''}`\n })\n}\n\n/** The concrete `<app>/<part>` keys of an in-memory preset, grouped by app. Glob keys (those whose app segment is a bare star) are skipped. */\nexport const paneTargetsByApp = (presetDef: DevServerOptions['presetDef']): Map<string, string[]> => {\n const byApp = new Map<string, string[]>()\n\n for (const key of Object.keys(presetDef?.apps ?? {})) {\n const app = key.split('/')[0]\n\n if (app === undefined || app === '*') continue\n\n byApp.set(app, [...(byApp.get(app) ?? []), key])\n }\n\n return byApp\n}\n\n/** Discover API apps under `root`, applying the optional `--app` include filter. */\nconst selectApiApps = (root: string, include: string[] | null): DiscoveredApiApp[] => {\n const apps = discoverApiApps(root)\n\n if (!include) {\n return apps\n }\n\n return apps.filter((app) => {\n return include.includes(app.name)\n })\n}\n\n/**\n * Log each app pane and the opened workspace ref. Ports are NOT shown here: each\n * pane binds a dynamic (ephemeral) port at runtime and prints its own real port \u2014\n * the supervisor cannot know it at spawn time, so a static resolved port would lie.\n */\nconst logDevWorkspace = (apps: DiscoveredApiApp[], ref: string): void => {\n logger.info(`\uD83E\uDDE9 Opened cmux dev workspace ${ref} with ${apps.length} pane(s):`)\n\n for (const app of apps) {\n logger.info(` \u2022 ${app.name} (infra-kit dev --app=${app.name})`)\n }\n}\n\n/**\n * Register SIGINT/SIGTERM handlers that close the workspace `ref` \u2014 the supervisor's whole\n * teardown. No force-deadline: closing a workspace is a single call, not a child reap, and a\n * second signal is already an unconditional escape (see {@link registerSignalShutdown}).\n */\nconst registerShutdown = (ref: string): void => {\n registerSignalShutdown({\n onSignal: async (signal) => {\n logger.info(`\\nReceived ${signal}, closing cmux dev workspace ${ref}...`)\n await closeCmuxDevWorkspace(ref)\n },\n })\n}\n\n/**\n * Open a cmux workspace with one pane per API app, then stay resident as a\n * supervisor until a signal tears the workspace down. Falls out early (no\n * workspace) when no apps are discovered. Never returns on the happy path \u2014 it\n * owns its own SIGINT/SIGTERM handling and blocks forever otherwise.\n *\n * @example\n * // Runs until Ctrl-C; opens `pnpm exec infra-kit dev --app=<name>` per app.\n * await runCmuxDevServer({ include: null, watch: false })\n */\nexport const runCmuxDevServer = async (options: DevServerOptions): Promise<void> => {\n const root = findMonorepoRoot(process.cwd())\n const apps = selectApiApps(root, normalizeAppInclude(options.include))\n\n if (apps.length === 0) {\n logger.warn('No API apps found to run')\n\n return\n }\n\n // A wizard run hands down its in-memory preset, so each pane reproduces the exact parts that were\n // ticked. A plain `--cmux` has no presetDef and falls back to `--app=<name>` (every part), unchanged.\n const targetsByApp = paneTargetsByApp(options.presetDef)\n const commands = buildPaneCommands(\n apps.map((app) => {\n return { app: app.name, targets: targetsByApp.get(app.name) }\n }),\n options.watch ?? false,\n )\n const layout = buildCmuxLayout(commands)\n const title = `${path.basename(root)} dev`\n const ref = await openCmuxDevWorkspace({ cwd: root, title, layout })\n\n logDevWorkspace(apps, ref)\n registerShutdown(ref)\n\n // Stay resident until a signal fires. A never-resolving promise alone does NOT\n // keep Node's event loop alive (nothing pending \u2192 the process exits with code\n // 13, \"unsettled top-level await\"); a ref'd heartbeat timer holds it open. The\n // SIGINT/SIGTERM handler in registerShutdown owns the actual exit.\n const heartbeat = setInterval(() => {\n heartbeat.refresh()\n }, 2 ** 30)\n\n await new Promise<never>(() => {})\n}\n", "/**\n * Pure cmux layout construction for `infra-kit dev --cmux`.\n *\n * Turns a flat list of per-pane shell commands into the recursive binary-tree\n * layout JSON cmux's `new-workspace --layout` consumes. Side-effect free (no fs,\n * no cwd, no shell) so the tiling math stays unit-testable in isolation.\n */\n\n/** A single terminal surface inside a pane, running one shell command. */\nexport interface CmuxTerminalSurface {\n type: 'terminal'\n command: string\n}\n\n/** A leaf node: one pane holding one or more terminal surfaces. */\nexport interface CmuxPaneLeaf {\n pane: {\n surfaces: CmuxTerminalSurface[]\n }\n}\n\n/** An internal node: a horizontal/vertical split into exactly two children. */\nexport interface CmuxSplitNode {\n direction: 'horizontal' | 'vertical'\n split: number\n children: [CmuxLayoutNode, CmuxLayoutNode]\n}\n\n/** Either a pane leaf or a two-way split \u2014 the recursive layout tree. */\nexport type CmuxLayoutNode = CmuxPaneLeaf | CmuxSplitNode\n\n/** Wrap a single command in a bare pane leaf. */\nconst makeLeaf = (command: string): CmuxPaneLeaf => {\n return { pane: { surfaces: [{ type: 'terminal', command }] } }\n}\n\n/**\n * Recursively split `commands` into a balanced binary tree: the left half takes\n * `Math.ceil(n / 2)` commands, the split ratio is that count over the total\n * (2-decimal), and the direction alternates by depth (even \u2192 horizontal,\n * odd \u2192 vertical) so panes tile into a grid rather than one axis.\n */\nconst buildNode = (commands: string[], depth: number): CmuxLayoutNode => {\n if (commands.length === 1) {\n return makeLeaf(commands[0]!)\n }\n\n const leftCount = Math.ceil(commands.length / 2)\n const left = commands.slice(0, leftCount)\n const right = commands.slice(leftCount)\n\n return {\n direction: depth % 2 === 0 ? 'horizontal' : 'vertical',\n split: Math.round((leftCount / commands.length) * 100) / 100,\n children: [buildNode(left, depth + 1), buildNode(right, depth + 1)],\n }\n}\n\n/**\n * Build a balanced cmux layout tree with one pane leaf per command. A single\n * command yields a bare leaf; N commands tile into a grid via alternating\n * horizontal/vertical splits. Throws when `commands` is empty.\n *\n * @example\n * buildCmuxLayout(['a', 'b'])\n * // => {\n * // direction: 'horizontal',\n * // split: 0.5,\n * // children: [\n * // { pane: { surfaces: [{ type: 'terminal', command: 'a' }] } },\n * // { pane: { surfaces: [{ type: 'terminal', command: 'b' }] } },\n * // ],\n * // }\n */\nexport const buildCmuxLayout = (commands: string[]): CmuxLayoutNode => {\n if (commands.length === 0) {\n throw new Error('buildCmuxLayout: at least one command is required')\n }\n\n return buildNode(commands, 0)\n}\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 * 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 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, because this\n * module used to explain at length why one should not be. That explanation assumed a human at the keyboard\n * to press Ctrl-C a second time; the incident it now guards against is the case where nobody is there.\n *\n * Mechanically the deadline depends on `terminal-liveness`: with a fault storm live, teardown competes with\n * ~100k blocking `writeSync` calls per second, and a deadline shipped into a machine that cannot honour it\n * is decoration. The storm is cut at its source first; this is what stops the wedge that follows.\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 */\nexport const registerSignalShutdown = ({\n onSignal,\n exit = defaultExit,\n register = defaultRegister,\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\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 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 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 exit(exitCodeForSignal(signal))\n }\n })()\n }\n\n for (const signal of HANDLED_SIGNALS) {\n register(signal, () => {\n return handle(signal)\n })\n }\n}\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 */\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 * 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 const starts = leaderStarts(rows)\n\n return snapshot.flatMap(({ pgid, leaderStart }) => {\n if (starts.get(pgid) !== leaderStart) return []\n\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 * SIGKILL every process group descended from this process, right now.\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 signalGroups(doomedGroupsOf(process.pid, snapshotProcRows()), '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 * That covers the ORDERLY death, where we choose the moment and can walk the tree first. It does not\n * cover turbo CRASHING, because reparenting happens at termination \u2014 strictly before Node hands us the\n * `exit` event \u2014 so by then the walk is already blind. A rolling snapshot (see `sample`) is therefore\n * kept for the child's whole life, and both the crash path and a `kill()` on an already-dead child reap\n * from it, validating each group against its leader's start time so a recycled pgid is never killed.\n *\n * `onUnexpectedExit` (optional) fires when the child dies WITHOUT `kill()` having been called \u2014\n * the \"engine died silently\" case. A `killing` latch, set at the top of `kill()` before any signal\n * goes out, suppresses the callback for the exit our own teardown causes, so it reports only genuine\n * crashes. The `error` listener is load-bearing beyond reporting: a {@link ChildProcess} that emits\n * `error` (e.g. `pnpm` ENOENT) with no listener throws as an uncaught exception and would take the\n * whole dev session down.\n */\nexport function superviseChild(\n child: ChildProcess,\n graceMs: number = DEFAULT_GRACE_MS,\n onUnexpectedExit?: UnexpectedExitHandler,\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 const stopSampling = (): void => {\n if (sampler) clearInterval(sampler)\n sampler = null\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 * An empty result is discarded rather than stored: `ps` failing, or racing the child's own death,\n * must degrade to the previous snapshot, never erase it.\n */\n const sample = async (): Promise<void> => {\n const pid = child.pid\n\n if (pid == null || exited || killing) return\n\n const rows = await snapshotProcRowsAsync()\n\n if (rows.length === 0 || exited || killing) 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 void sample()\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 if (!killing) reapSnapshot(groups, snapshotProcRows())\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) return\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, snapshotProcRows())\n\n return\n }\n\n const rows = snapshotProcRows()\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.\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) return\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, snapshotProcRows())\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 },\n }\n}\n", "/**\n * Process-level crash barrier for the resident single-process dev-server.\n *\n * The backends run IN-PROCESS (`ServerlessLocalRun` is an in-process fastify that `import()`s the\n * compiled handler), so they share the runner's event loop. Fastify catches errors thrown INSIDE a\n * route handler and turns them into a 500 \u2014 but a stray async path a handler forgot to await\n * (`unhandledRejection`) or a throw from a timer/emitter (`uncaughtException`) escapes to the process\n * and, by Node's defaults, terminates it. That kills the WHOLE dev session \u2014 every backend, the watch\n * engine, the UI child \u2014 over one bad path in one handler.\n *\n * This barrier keeps the session alive and reports the fault loudly instead, the same resilience\n * `nodemon`/`vite dev`/`next dev` give. The tradeoff is deliberate and dev-only: it is installed by the\n * CLI entry point, never by library code, so it can never mask a fault in a production consumer.\n *\n * Node's caveat stands: after an `uncaughtException` the process may be in an undefined state. For a\n * local dev tool a possibly-degraded session the developer can see and restart beats a hard exit that\n * looks like the tool itself crashed. The fault is surfaced with a full stack so it is never silent.\n */\nimport process from 'node:process'\n\nimport { killDescendantGroupsNow } from 'src/dev/managed-child'\n\n/** The two process-level fault channels that terminate Node by default. */\ntype FaultEvent = 'uncaughtException' | 'unhandledRejection'\n\n/**\n * Seams for {@link registerCrashBarrier}. Each exists because the real implementation is untestable\n * in-process: attaching real `process.on('uncaughtException')` handlers in the test runner would swallow\n * the runner's own faults, and there is nothing to assert against a handler that only logs to stderr.\n */\nexport interface CrashBarrierDeps {\n /** Report the fault. Defaults to a guarded stderr write. Must not throw. */\n onFault?: (event: FaultEvent, error: unknown) => void\n /** Subscribe `handler` to `event`. Defaults to `process.on`. */\n register?: (event: FaultEvent, handler: (error: unknown) => void) => void\n /**\n * Is our own stdio unwritable? The ONE discriminator between \"survive\" and \"die\" \u2014 see\n * `terminal-liveness.ts`. Defaults to `() => false`, so the LIBRARY's behaviour is unchanged: the exit\n * path is opt-in and wired only by the CLI entry, exactly as this whole barrier is.\n *\n * It must never be re-implemented as an error-code sniff (`EIO`/`EPIPE`). A handler writing to a client\n * socket that hung up throws `EPIPE` too, so a code-sniff would let ONE user closing a browser tab\n * mid-request kill the entire dev session \u2014 the very fragility the barrier exists to remove, re-armed.\n */\n isTerminalDead?: () => boolean\n /**\n * File the fault where it can still be READ once the terminal cannot be written to. The dev entry routes\n * this into the log sink; the default falls back to the guarded stderr write, which may itself be lost \u2014\n * a lost report is recoverable, an unbounded loop is not.\n */\n fileFault?: (event: FaultEvent, error: unknown) => void\n /**\n * Terminate the session because stdio is gone. Reached ONLY when {@link isTerminalDead} is true: a fault\n * reported onto a dead stream is what produced the fault, and surviving it means spinning forever.\n * Defaults to a force-reap of the descendant groups (they hold the dev ports) plus `process.exit`.\n */\n onFatal?: (reason: string) => void\n}\n\n/**\n * Write to stderr, swallowing a failed write. Mirrors `signal-shutdown`'s `writeStderr`: reporting must\n * never itself throw on the fault path (EPIPE when the parent closed the pipe), and a lost message is\n * recoverable where a barrier that throws 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/**\n * The fault report, as one string: message AND stack, plus a note that the session was kept alive.\n *\n * Exported because the dev-server must route this into the panel's error counter and onto the terminal\n * through the interceptor's bypass. `infra-kit dev` no longer prints logs, and the interceptor owns\n * `process.stderr` \u2014 so the default stderr write below would file a crash into a log file and leave the\n * panel showing a healthy, silent session. A fault is the one thing that may never be quiet.\n */\nexport const formatFault = (event: FaultEvent, error: unknown, kept = true): string => {\n const detail = error instanceof Error ? `${error.message}\\n${error.stack ?? '(no stack)'}` : String(error)\n // `kept` is not cosmetic. On the fatal path the session is NOT kept alive, and a trailing \"kept alive\"\n // there is a lie filed into the one log a post-mortem will actually read. Defaulted to `true` so the\n // survive path \u2014 the barrier's entire reason for existing \u2014 is byte-identical to before.\n const outcome = kept\n ? ` dev-server kept alive (likely a bug in a handler's async path); restart if it misbehaves.\\n`\n : ` dev-server is shutting down: its stdio is unwritable, so reporting this fault would loop forever.\\n`\n\n return `\\n\u26A0\uFE0F ${event}: ${detail}\\n${outcome}`\n}\n\n/** Default fault reporter: straight to stderr. Replaced by the dev entry, which also files + counts it. */\nconst defaultOnFault = (event: FaultEvent, error: unknown): void => {\n writeStderr(formatFault(event, error))\n}\n\n/**\n * Default filer for the fatal path. Stderr is very likely the stream that just died \u2014 this write is\n * expected to be dropped, and that is acceptable: the CLI entry overrides it with the log sink, which is\n * the only channel left when stdio is gone.\n */\nconst defaultFileFault = (event: FaultEvent, error: unknown): void => {\n writeStderr(formatFault(event, error, false))\n}\n\n/**\n * Default fatal action: SIGKILL the descendant process GROUPS before exiting. Not `process.exit` alone \u2014\n * turbo and vite sit in their own detached groups, so an exit that skips the reap leaves them holding the\n * dev ports and the next `infra-kit dev` 502s against their stale aliases. That is the orphan half of the\n * incident this exists for (five processes, alive for five hours).\n */\nconst defaultOnFatal = (reason: string): void => {\n writeStderr(`\\n\u2717 dev-server exiting: ${reason}\\n`)\n killDescendantGroupsNow()\n process.exit(1)\n}\n\nconst defaultRegister = (event: FaultEvent, handler: (error: unknown) => void): void => {\n process.on(event, handler)\n}\n\n/**\n * Install the crash barrier: wire `uncaughtException` and `unhandledRejection` to `onFault` and DO NOT\n * exit, so a single escaped async path in an in-process backend handler no longer tears the whole dev\n * session down. The handler never rethrows \u2014 rethrowing would re-arm the very termination this prevents.\n *\n * The ONE exception is {@link CrashBarrierDeps.isTerminalDead}: when our own stdio can no longer be\n * written to, \"report it loudly and survive\" is not resilience, it is the loop \u2014 the report goes to the\n * dead stream, fails, and comes straight back here. So on that path the fault is FILED (never printed) and\n * the session exits. This is a backstop, not the fix: `terminal-liveness` already initiates the same\n * bounded teardown from the stream's own `'error'` event, and this covers a fault arriving from a stream\n * that listener does not own.\n *\n * @example\n * registerCrashBarrier() // resident dev process survives a handler's stray rejection, logs it loudly\n */\nexport const registerCrashBarrier = ({\n onFault = defaultOnFault,\n register = defaultRegister,\n isTerminalDead = () => {\n return false\n },\n fileFault = defaultFileFault,\n onFatal = defaultOnFatal,\n}: CrashBarrierDeps = {}): void => {\n const handle = (event: FaultEvent) => {\n return (error: unknown): void => {\n if (isTerminalDead()) {\n fileFault(event, error)\n // The reason names the CHANNEL, not a story: the errno-bearing reason comes from the liveness\n // listener; here all we honestly know is that a fault arrived while stdio was already unwritable.\n onFatal(`${event} while stdio is unwritable`)\n\n return\n }\n\n onFault(event, error)\n }\n }\n\n register('uncaughtException', handle('uncaughtException'))\n register('unhandledRejection', handle('unhandledRejection'))\n}\n", "/**\n * Unified Development Server Runner\n *\n * Discovers and runs API apps under each `apps/<app>/api` folder that contains `serverless.yml`.\n *\n * Ports: `{APP}_PORT`, then `process.env.PORT`, then `dev.<app>.port` from infra-kit.json,\n * else 3010. URL prefix: `dev.<app>.prefixUrl`, else `/api/v1`.\n * Env vars should be provided via secrets manager (e.g. `doppler run -- pnpm dev-server`) or shell.\n *\n * This module is side-effect free on import: call `run()` (or construct `DevServerRunner`\n * directly) to start. Signal handling and process exit are the entry point's responsibility.\n *\n * Logs are written PER SERVICE under `<cacheRoot>/<INFRA_KIT_SESSION>/dev/<pid>/` \u2014 `runner.log` for the\n * runner's own narration, `<app>/api` and `<app>/ui` for each app, plus `turbo.log` (the UI engine's raw\n * chunk tee) and `watch.log`. Lambda / Powertools logs from handlers still go to stdout.\n */\nimport {\n DEFAULT_RELEASE_SLUG,\n DEV_CONTEXT_WIRE_VERSION,\n loadDev,\n slugifyHostLabel,\n slugifyRelease,\n} from '@slip-stream-kit/config/internal'\nimport chokidar from 'chokidar'\nimport type { FSWatcher } from 'chokidar'\nimport { exec, execFileSync } from 'node:child_process'\nimport * as fs from 'node:fs'\nimport net from 'node:net'\nimport os from 'node:os'\nimport * as path from 'node:path'\nimport process from 'node:process'\nimport util from 'node:util'\n\nimport { INFRA_KIT_ENV_VAR } from 'src/lib/constants'\nimport type { DevConfig, DevPreset, DevPresets, ProxySource } from 'src/lib/infra-kit-config'\nimport { DEFAULT_DEV_PROXY_PORT, getInfraKitConfig } from 'src/lib/infra-kit-config'\n\nimport { buildClosureMap, packageDebounceKey, selectPackageRestartTargets } from './dep-closure.js'\nimport type { ClosureMap, DryRunner } from './dep-closure.js'\nimport type { DevUi } from './dev-ui.js'\nimport {\n classifyDistChange,\n discoverApiApps as discoverApiAppsBare,\n discoverUiApps as discoverUiAppsBare,\n findMonorepoRoot,\n getAppDistDirs,\n getPackageDistDirs,\n normalizeAppInclude as normalizeAppIncludePure,\n} from './discovery.js'\nimport type { DiscoveredUiApp } from './discovery.js'\nimport { findDegradedRoutes, formatPairingRefusal } from './local-pairing.js'\nimport type { DegradedRoute, LaunchedUi } from './local-pairing.js'\nimport { currentService } from './log-attribution.js'\nimport { DevLogSink, panelStream } from './log-sink.js'\nimport { installOutputIntercept } from './output-intercept.js'\nimport type { OutputIntercept } from './output-intercept.js'\nimport {\n findPortConflicts,\n resolvePreferredPort as resolvePreferredPortPure,\n resolvePrefixUrl as resolvePrefixUrlPure,\n} from './ports.js'\nimport { deriveTargetLabel, resolvePreset } from './presets.js'\nimport { createPortlessDriver, formatPortlessCommand, readCaPath } from './proxy/portless-driver.js'\nimport type { PortlessDriver } from './proxy/portless-driver.js'\nimport { DevRenderer, resolveEndpointUrl } from './render.js'\nimport type { DegradedRow, EndpointRow, HealthState, ReadySummary, UiRef } from './render.js'\nimport { ServerlessLocalRun } from './serverless-local-run.js'\nimport { defaultTurboWatchFactory } from './turbo-watch.js'\nimport type { TurboWatchFactory, TurboWatchHandle } from './turbo-watch.js'\nimport { defaultUiDevFactory } from './ui-dev.js'\nimport type { UiDevFactory, UiDevHandle } from './ui-dev.js'\n\n/**\n * The service tag every runner-authored line is filed under. Framework and request lines carry their own\n * app tag (`<app>/ui`, `<app>/api`); anything the runner itself says belongs here.\n */\nconst RUNNER_SERVICE = 'runner'\n\n/**\n * The turbo child's RAW chunk tee \u2014 turbo's own run chrome plus every framework line, ANSI intact and\n * un-de-multiplexed. Kept as its own file rather than smeared across the per-app UI logs: a raw chunk\n * arrives before `parseTurboDevLine` has attributed it to a package, so there is no honest app to file\n * it under. The per-app `<app>/ui` files get the parsed, attributed lines.\n */\nconst TURBO_SERVICE = 'turbo'\n\n/**\n * The `turbo watch build` engine's file. It is spawned with the log fd as its INHERITED stdio\n * (`turbo-watch.ts`), so it writes raw bytes straight into the file \u2014 which is exactly why it needs one\n * of its own rather than a share of any service's.\n */\nconst WATCH_SERVICE = 'watch'\n\n/** Replace a leading home dir with `~` for a compact, human-readable path label (the on-screen log link). */\nexport function homeShorten(p: string): string {\n const home = os.homedir()\n\n return p === home || p.startsWith(`${home}${path.sep}`) ? `~${p.slice(home.length)}` : p\n}\n\n/**\n * Comma-joined package names \u2014 not folder names, so a boot line names the exact `turbo --filter`\n * targets the build runs on and matches turbo's own output.\n */\nfunction packageList(apps: { packageName: string }[]): string {\n return apps\n .map((a) => {\n return a.packageName\n })\n .join(', ')\n}\n\nconst execFn = util.promisify(exec)\n\ntype LogFn = (msg: string, level?: 'info' | 'warn' | 'error' | 'debug') => void\n\n/**\n * Build seam: shells out to turbo by default ({@link launchScript}); injectable so\n * tests can run the orchestrator without a real build. `logFn` is optional so both\n * the verbose initial build and the terse restart builds keep their current output.\n */\nexport type BuildRunner = (cmd: string, logFn?: LogFn) => Promise<void>\n\nconst launchScript = async (script: string, logFn?: LogFn): Promise<void> => {\n try {\n const { stderr } = await execFn(script)\n\n if (stderr && logFn) logFn(` (build) ${stderr.trim()}`, 'debug')\n if (stderr && !logFn) console.error('stderr:', stderr)\n } catch (error) {\n const err = error as { stdout?: string; stderr?: string; message?: string }\n\n if (logFn && (err.stdout || err.stderr)) {\n if (err.stdout) logFn(` stdout: ${err.stdout.trim()}`, 'error')\n if (err.stderr) logFn(` stderr: ${err.stderr.trim()}`, 'error')\n }\n\n throw error\n }\n}\n\n/**\n * The process-wide per-service log sink. Created once at construction ({@link DevServerRunner}) and\n * closed in {@link DevServerRunner.shutdown}; module-scoped so the renderer's `appendLog` seam \u2014 which\n * is wired before `this` is fully initialised \u2014 can reach it.\n */\nlet logSink: DevLogSink | null = null\n\n/** Tee a runner-authored line to `runner.log`. The seam {@link DevRenderer} wraps. */\nfunction appendRunnerLog(text: string): void {\n logSink?.write(RUNNER_SERVICE, text)\n}\n\n/**\n * An OS-assigned free TCP port on 127.0.0.1 \u2014 used to pre-assign each UI's Vite port so the runner can\n * print its URL (and alias it, when a proxy is up) before Vite binds. Probes then releases (small TOCTOU\n * window; `strictPort` on the Vite side turns a rare lost race into a loud failure rather than a silent drift).\n */\nconst getFreePort = (): Promise<number> => {\n return new Promise((resolve, reject) => {\n const srv = net.createServer()\n\n srv.unref()\n srv.on('error', reject)\n srv.listen(0, '127.0.0.1', () => {\n const address = srv.address()\n const port = typeof address === 'object' && address !== null ? address.port : 0\n\n srv.close(() => {\n return resolve(port)\n })\n })\n })\n}\n\ninterface IApiAppConfig {\n /** App folder name (e.g. backoffice, client) */\n name: string\n /** Package name from package.json (e.g. sls-trvl-client) */\n packageName: string\n path: string\n /**\n * EXPLICITLY-configured preferred port (`{APP}_PORT`/`PORT`/`dev.<app>.port`), or\n * `undefined` when unconfigured. Under dynamic allocation this is only a bind hint \u2014 the\n * ACTUAL port is the ephemeral one bound at start time (see {@link IAppServer.boundPort}).\n */\n preferredPort: number | undefined\n prefixUrl: string\n /**\n * Whether this backend participates in dependency-closure watching (plan Phase 1): in\n * `--watch`, a rebuild of a package in this app's closure restarts it. Default `true`;\n * a preset `watchDeps: false` opts out (sticky). Resolved from the preset in {@link run}.\n */\n watchDeps: boolean\n}\n\n/**\n * Runner options, parsed by the CLI entry point (`--watch`, `--app`) and threaded\n * through `run()`. The entry owns flag parsing; the runner never reads `process.argv`\n * itself. App selection is `--app` only; ports come from env/config (see `resolvePort`).\n */\nexport interface DevServerOptions {\n /**\n * Watch mode: start a long-lived `turbo watch build` engine (incremental rebuilds\n * + dependency fan-out) and restart the affected server(s) when compiled `dist/`\n * changes. Without it, `dev` builds and serves once and exits on signal.\n */\n watch?: boolean\n /** Only run these app folder names (null/empty = all discovered). Filters BOTH api and ui apps. */\n include?: string[] | null\n /**\n * Named dev preset (`infra-kit dev <preset>`) from `devServersPresets` in the project's infra-kit config.\n * It selects the launch targets (`apps/<app>/{api,ui}`); resolved by {@link file://./presets.ts}.\n * Unset \u2192 run everything (`*`). `include` (`--app`/`--self`) further narrows the resolved set.\n */\n preset?: string\n /**\n * In-memory preset definition, produced by the interactive wizard (`infra-kit dev` with no args in a\n * TTY). When set it is used verbatim as the run plan \u2014 it WINS over `preset` (the named lookup) and\n * the default `*`. This is how the wizard expresses part-level selection (`<app>/ui` without\n * `<app>/api`), which `include` (app-name-only) cannot. Unset on every non-wizard invocation.\n */\n presetDef?: DevPreset\n /**\n * Run each discovered API app in its own cmux pane (one workspace, N panes), supervised by a\n * resident process that closes the workspace on signal. Falls back to single-process dev when\n * cmux is absent. Handled by `runCmuxDevServer`, not the in-process `DevServerRunner`.\n */\n cmux?: boolean\n /**\n * Infer the single app to run from the current working directory (equivalent to\n * `--app=<that app>`), so every app can share the identical script\n * `pnpm exec infra-kit dev --self` instead of hardcoding its own folder name.\n * Resolved by the entry point (`resolveSelfAppName`) into `include` before this\n * runner ever sees it; the runner itself does not read `self`.\n */\n self?: boolean\n /**\n * Print the full boot narration (build/discovery/watch steps) to the terminal. Default false:\n * the terminal shows only the server panel, warnings, errors, and restart lines. The FULL detail\n * is written to the per-service logs under `<cacheRoot>/<INFRA_KIT_SESSION>/dev/<pid>/` regardless of\n * this flag.\n */\n verbose?: boolean\n /**\n * Print each app's registered `METHOD /path` route table at startup. Default false \u2014 the route\n * dump is opt-in so the calm default screen stays glanceable; the routes are always in the log.\n */\n routes?: boolean\n /**\n * Interactive TTY (both stdin+stdout). Set by the entry point; gates the Ink boot UI in {@link run}.\n * When unset, {@link run} falls back to `process.stdout.isTTY`. The runner itself never reads it.\n */\n tty?: boolean\n /**\n * Structured `--json` / MCP mode. When true, {@link run} forces the plain {@link DevRenderer} \u2014 Ink\n * must never seize a machine-readable stream. The runner itself never reads it.\n */\n json?: boolean\n /**\n * Background liveness-probe interval in ms (default {@link DevServerRunner.LIVENESS_INTERVAL_MS}, 5000).\n * A test seam only \u2014 lets a test drive the monitor loop fast; production never sets it.\n */\n livenessIntervalMs?: number\n /**\n * Probe the frontends' liveness (vite's HMR ping) and give their rows a health dot. Default `true`;\n * `--no-ui-health` / `INFRA_KIT_NO_UI_HEALTH=1` turns it off, which drops every UI row back to no dot\n * at all and issues zero UI probes. The escape hatch exists because the ping is an undocumented vite\n * internal: if a future vite drops it, the dot has to be switchable off without a CLI downgrade.\n */\n uiHealth?: boolean\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 */\nconst 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`. */\nconst 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\n/**\n * One row's probe history \u2014 the state the 5-arm {@link HealthState} is derived from, keyed by TAG (never\n * by app name: `foo/api` and `foo/ui` are different rows on the same app, and a name-keyed counter\n * collides them).\n */\ninterface HealthEntry {\n kind: ProbeTarget['kind']\n /** Consecutive `refused` probes. `>= LIVENESS_FAILURE_THRESHOLD` is what \"down\" MEANS. */\n failures: number\n /** Consecutive `foreign` probes \u2014 a separate counter, because a foreign answer must never go red. */\n foreignStreak: number\n /** Has this row EVER proved it was serving? Until it has, a UI is `starting`, not `down`. */\n everUp: boolean\n /** A CERTAINTY of death from outside the probe loop: a thrown restart, a dead UI engine. */\n dead: boolean\n /** The port answers, but not with what we asked for \u2014 liveness cannot be established either way. */\n unverified: boolean\n}\n\n/** What a successful {@link DevServerRunner.startOneApp} hands back: a bound server and the alias it took. */\ninterface StartedApp {\n server: ServerlessLocalRun\n /** The ACTUAL port bound at start (ephemeral or preferred), reported by `server.start()`. */\n boundPort: number\n /** Layer-B alias host (`<release>.<package>.localhost`) \u2014 the app's only address. */\n alias: string\n}\n\ninterface IAppServer {\n app: IApiAppConfig\n server: ServerlessLocalRun\n /** The ACTUAL port bound at start (ephemeral or preferred), reported by `server.start()`. */\n boundPort: number\n /** Layer-B alias host (`<release>.<package>.localhost`) \u2014 the app's only address. */\n alias: string\n /** Epoch ms of this server's last (re)start \u2014 the source of the panel's `up Xs` field. */\n startedAt: number\n /** Watch-triggered restarts so far. */\n restarts: number\n}\n\n/**\n * One `.infra-kit/dev-context/<app>.json` fragment. `package` (feeds the vite helper's\n * `readLocalSet`) is the app's package name; `port` is the ACTUAL bound port (the writer\n * IS the binder). `release` lets the helper prefer the runner-recorded slug over its own\n * git derivation; `pid`/`writtenAt` are staleness metadata.\n *\n * `alias` is the registered portless hostname, and `origin` is **the authoritative local target** \u2014 the\n * exact origin the helper must proxy to, published by the runner rather than re-derived by the helper from\n * a template in a separately-versioned repo. The runner is the only party that knows what it actually\n * registered, so it says so; the helper obeys.\n *\n * `proxyPort` is deliberately NOT written any more. The CLI self-updates while `infra-kit/vite` stays\n * pinned per consumer, so a new CLI routinely meets an OLD helper \u2014 and that helper's `withProxyPort`\n * grafts any port other than 80 onto its target. Writing `proxyPort: 443` would therefore have produced\n * `http://<alias>:443`: **plain HTTP into a TLS listener**, silently. Omitting the field leaves the old\n * helper's target ungrafted instead. (That is not by itself a loud failure \u2014 `:80` is bound by portless's\n * redirect server, so it 302s rather than refusing \u2014 which is why the CLI-side version floor, not this\n * omission, is the load-bearing skew guard.)\n */\ninterface DevContextFragment {\n /** Wire version. See {@link DEV_CONTEXT_WIRE_VERSION} \u2014 a promise that `origin` is present. */\n v: number\n package: string\n port: number\n pid: number\n writtenAt: number\n release: string\n alias: string\n origin: string\n}\n\n/**\n * Every package that can supply the `infraKitDev` helper, with the lowest version of THAT package whose\n * helper understands the dev-context fragment's `origin` field.\n *\n * This is the load-bearing guard against version skew, and skew here is GUARANTEED rather than\n * hypothetical: the CLI is installed globally and **self-updates silently**, while the helper is PINNED\n * in each consumer's `node_modules`. So a new CLI routinely meets an old helper. An old helper ignores\n * `origin` and rebuilds the target from the consumer's `templates.local` \u2014 which still says `http://` \u2014\n * and then proxies plain HTTP at a TLS listener. That failure is silent (portless answers :80 with a 302\n * rather than refusing), so nothing downstream would catch it. Refuse at start instead.\n *\n * A LIST, and each floor is a point on ITS OWN package's version line. This is the whole subtlety of the\n * `infra-kit` \u2192 `@slip-stream-kit/config` split, and getting it wrong fails silently in both directions:\n * - Comparing the new package's version against the OLD package's floor (`0.1.132`) is meaningless. The\n * two are unrelated version lines; a new package seeded low would throw for every consumer, and one\n * seeded high would pass vacuously \u2014 a dead guard that still looks alive.\n * - Simply RE-KEYING the guard to the new package (rather than adding to it) drops the old entry, and\n * then a not-yet-migrated consumer \u2014 the exact population still running an old helper \u2014 silently stops\n * being checked at all.\n *\n * So: keep them all, check whichever the repo actually resolves, and only drop the `infra-kit` entry once\n * no consumer imports `infra-kit/vite` any more. The packages release in LOCKSTEP, which is what keeps\n * each floor comparable to the CLI's own version as the wire evolves.\n *\n * `@slip-stream-kit/vite` (the plugin) needs its OWN entry even though it only wraps\n * `@slip-stream-kit/config`, and the reason is pnpm's layout rather than style: a consumer on the plugin\n * declares only the plugin, so `config` is a TRANSITIVE dep living in the virtual store \u2014 it resolves\n * from neither the app dir nor the repo root, and {@link assertHelperVersionFloor} would find nothing\n * to check and skip the repo entirely. The plugin's own `dependencies` pin the config version exactly\n * (`workspace:*` publishes as the released version), so checking the plugin checks the pair.\n */\nexport const HELPER_PACKAGES = [\n { name: '@slip-stream-kit/vite', floor: '0.1.134' },\n { name: '@slip-stream-kit/config', floor: '0.1.134' },\n { name: 'infra-kit', floor: '0.1.132' },\n] as const\n\n/** `true` when `version` sorts strictly below `floor` (numeric, dot-separated; missing parts are 0). */\nexport const isBelowVersion = (version: string, floor: string): boolean => {\n const parse = (v: string): number[] => {\n return v.split('.').map((part) => {\n return Number.parseInt(part, 10) || 0\n })\n }\n const a = parse(version)\n const b = parse(floor)\n\n for (let i = 0; i < Math.max(a.length, b.length); i += 1) {\n const left = a[i] ?? 0\n const right = b[i] ?? 0\n\n if (left !== right) return left < right\n }\n\n return false\n}\n\n/** Package dirs whose manifest may declare a helper: the repo root, plus every `apps/<app>/{api,ui}`. */\nconst manifestDirs = (repoRoot: string): string[] => {\n const dirs = [repoRoot]\n\n try {\n for (const app of fs.readdirSync(path.join(repoRoot, 'apps'), { withFileTypes: true })) {\n if (!app.isDirectory()) continue\n for (const part of ['api', 'ui']) dirs.push(path.join(repoRoot, 'apps', app.name, part))\n }\n } catch {\n // No `apps/` dir \u2014 the root manifest alone decides.\n }\n\n return dirs\n}\n\n/** Does `<dir>/package.json` declare `name` in dependencies or devDependencies? */\nconst declaresPackage = (dir: string, name: string): boolean => {\n try {\n const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8')) as Record<\n string,\n Record<string, string> | undefined\n >\n\n return ['dependencies', 'devDependencies'].some((field) => {\n return pkg[field]?.[name] != null\n })\n } catch {\n return false\n }\n}\n\n/**\n * Find the installed `name` by walking `node_modules` UPWARD from `fromDir`, exactly as Node resolves it.\n *\n * Probing `<repoRoot>/node_modules/<name>` alone \u2014 which is what this used to do \u2014 is wrong under pnpm,\n * and wrong in the direction that hurts. pnpm does NOT hoist a workspace package's dependency to the root:\n * a dep declared in `apps/client/ui/package.json` lands in `apps/client/ui/node_modules/`, and the root has\n * no trace of it. So the root-only probe would (a) fail to find \u2014 and therefore never version-check \u2014 a\n * helper declared where it is actually USED (next to the `vite.config.ts` that imports it), and (b) once\n * the consumer drops root `infra-kit` for the global CLI, report the correctly-installed helper as\n * \"missing\" and tell the user to run `pnpm install`, which can never fix it. Resolve it the way Node does.\n */\nconst findHelperDir = (repoRoot: string, fromDir: string, name: string): string | undefined => {\n const segments = name.split('/')\n let dir = fromDir\n\n for (;;) {\n const candidate = path.join(dir, 'node_modules', ...segments)\n\n if (fs.existsSync(candidate)) return candidate\n\n const parent = path.dirname(dir)\n\n if (dir === repoRoot || parent === dir) return undefined\n dir = parent\n }\n}\n\n/**\n * `true` when `<repoRoot>/node_modules/<name>` is a workspace link back into this repo rather than a\n * published install.\n *\n * Resolves BOTH sides before comparing: on macOS a temp path realpaths from `/var` to `/private/var`, so\n * an unresolved root would never contain a resolved child and a workspace link would be misread as a\n * published install.\n */\nconst isWorkspaceLinked = (repoRoot: string, helperDir: string): boolean => {\n try {\n const real = fs.realpathSync(helperDir)\n const root = fs.realpathSync(repoRoot)\n\n return real.startsWith(root + path.sep) && !real.includes(`${path.sep}node_modules${path.sep}`)\n } catch {\n return false\n }\n}\n\n/** `realpath`, falling back to the input when it cannot be resolved (used only as a dedupe key). */\nconst safeRealpath = (target: string): string => {\n try {\n return fs.realpathSync(target)\n } catch {\n return target\n }\n}\n\n/** Enforce one helper package's floor against ONE resolved install directory. */\nconst assertFloorAt = (repoRoot: string, name: string, floor: string, helperDir: string): void => {\n // In this repo `node_modules/@slip-stream-kit/config` symlinks to `apps/infra-kit/config`, whose\n // version is the unreleased working tree. Enforcing a floor there would brick `infra-kit dev` on the\n // very repo that develops it.\n if (isWorkspaceLinked(repoRoot, helperDir)) return\n\n let version: string\n\n try {\n const pkg = JSON.parse(fs.readFileSync(path.join(helperDir, 'package.json'), 'utf-8')) as { version?: string }\n\n if (typeof pkg.version !== 'string') throw new Error('no version field')\n version = pkg.version\n } catch {\n throw new Error(\n `infra-kit dev: could not read the version of the pinned \\`${name}\\` helper (${helperDir}). Refusing ` +\n `to start rather than risk proxying plain HTTP at a TLS listener. Run \\`pnpm install\\`.`,\n )\n }\n\n if (isBelowVersion(version, floor)) {\n throw new Error(\n `infra-kit dev: this repo pins ${name} ${version}, but dev URLs are now HTTPS and the ` +\n `\\`infraKitDev\\` helper only understands them from ${floor}. An older helper would proxy plain ` +\n `HTTP at a TLS listener \u2014 silently. Bump the dependency:\\n` +\n ` pnpm add -D ${name}@^${floor}`,\n )\n }\n}\n\n/**\n * Refuse to start against a consumer-pinned `infraKitDev` helper too old to understand the dev-context\n * fragment's `origin` field \u2014 whichever package that helper comes from (see {@link HELPER_PACKAGES}).\n *\n * Every helper package is checked INDEPENDENTLY, and every place it resolves from is checked. Two\n * migration shapes make that necessary rather than fussy:\n * - A repo mid-migration has BOTH (`@slip-stream-kit/config` added, `infra-kit` not yet dropped), and its\n * vite configs may still import from either. Stopping at the first helper that passes would hand exactly\n * that repo a silent HTTP-into-TLS proxy from the other one.\n * - Under pnpm the helper usually resolves NOT from the repo root but from the package that declares it \u2014\n * `apps/client/ui/node_modules/` sits right next to the `vite.config.ts` that imports it. See\n * {@link findHelperDir}.\n *\n * Three outcomes per package, and the reasoning for each matters:\n * - **Workspace-linked \u2192 SKIP** (that install only; every other one is still checked).\n * - **Declared but unresolvable \u2192 THROW (fail closed).** The consumer says it uses a helper and we\n * cannot prove which version; guessing is how the silent case ships.\n * - **Neither installed nor declared \u2192 SKIP.** Nothing to be skewed against. (This is also what keeps\n * bare test fixtures runnable.)\n */\nexport const assertHelperVersionFloor = (repoRoot: string): void => {\n const dirs = manifestDirs(repoRoot)\n\n for (const { name, floor } of HELPER_PACKAGES) {\n const declaredIn = dirs.filter((dir) => {\n return declaresPackage(dir, name)\n })\n\n // Always probe the root too: a helper can be present without being declared (hoisted, or a transitive\n // of something else), and it would still be the one vite resolves.\n const resolved = new Map<string, string>()\n\n for (const dir of [...declaredIn, repoRoot]) {\n const found = findHelperDir(repoRoot, dir, name)\n\n // Key by realpath: several packages symlinking into the same pnpm store entry are ONE install, and\n // re-reading it per app would just multiply identical work (and identical error messages).\n if (found) resolved.set(safeRealpath(found), found)\n }\n\n if (resolved.size === 0) {\n if (declaredIn.length === 0) continue\n\n throw new Error(\n `infra-kit dev: ${declaredIn[0]}/package.json depends on ${name} but it does not resolve from there, ` +\n `so the \\`infraKitDev\\` helper version cannot be verified. Run \\`pnpm install\\`.`,\n )\n }\n\n for (const helperDir of resolved.values()) assertFloorAt(repoRoot, name, floor, helperDir)\n }\n}\n\n/**\n * Slugified `<release>` for the app's git branch (resolved from the app's own dir), falling back to\n * {@link 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.\n */\nconst readAppRelease = (cwd: string): string => {\n try {\n // eslint-disable-next-line sonarjs/no-os-command-from-path\n const branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, encoding: 'utf-8' }).trim()\n const slug = slugifyRelease(branch)\n\n return slug === '' ? DEFAULT_RELEASE_SLUG : slug\n } catch {\n return DEFAULT_RELEASE_SLUG\n }\n}\n\n/**\n * The one-line `reason` for a `\u25CF failed` row. Endpoint rows are a single terminal line, so a stack\n * trace cannot go there \u2014 it is already in the log tail and the session log. Take the message only,\n * and its first line at that: validation errors like to append their own multi-line dumps.\n *\n * @example\n * errorReason(new Error(\"config is missing field: 'connectionURL'\\n at \u2026\")) // \"config is missing field: 'connectionURL'\"\n */\nconst errorReason = (error: unknown): string => {\n const message = error instanceof Error ? error.message : String(error)\n const firstLine = message.split('\\n')[0]?.trim() ?? ''\n\n return firstLine === '' ? 'unknown error (see the log)' : firstLine\n}\n\nexport class DevServerRunner {\n private readonly monorepoRoot: string\n /**\n * `<cwd>/.infra-kit/dev-context` \u2014 the fragment directory this runner writes its own\n * per-app `<app>.json` into (mirrors {@link LOG_FILE_PATH}'s cwd-relative resolution). The\n * `infra-kit/vite` helper searches up-tree for this dir and merges the fragments.\n */\n private readonly devContextDir: string\n private readonly appServers: IAppServer[] = []\n /** Per-app request timestamps, pruned to a 60s window \u2014 the panel's `18/min` field. */\n private readonly reqTimes = new Map<string, number[]>()\n /**\n * The last {@link ReadySummary} painted. Kept so {@link refreshStatus} can repaint the panel with\n * fresh live fields without re-deriving the static half (URLs, aliases, watch summary) every tick.\n */\n private lastSummary: ReadySummary | null = null\n /** Epoch ms at which the session went ready \u2014 the source of the panel's heartbeat. */\n private readyAt = 0\n private watchDebounceTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()\n /** Active chokidar watcher in `--watch` mode; closed on {@link shutdown}. */\n private watcher: FSWatcher | null = null\n private static readonly WATCH_DEBOUNCE_MS = 400\n /** Serialized restarts so rapid saves never bind :port while the previous server is still shutting down. */\n private restartWorkChain: Promise<void> = Promise.resolve()\n private static readonly PORT_RELEASE_DELAY_MS = 200\n /** Self-rescheduling backend liveness probe; cleared on {@link shutdown}. Null when unarmed (UI-only). */\n private livenessTimer: ReturnType<typeof setTimeout> | null = null\n /**\n * Probe history per row, keyed by TAG (`<app>/api`, `<app>/ui`) \u2014 never by app name, which would\n * collide an app's two halves into one counter the moment the frontends joined the tick. It survives an\n * ephemeral-port rebind on restart, because the tag does.\n */\n private readonly health = new Map<string, HealthEntry>()\n /** Tags whose first error has already been announced \u2014 the 0 \u2192 >0 edge fires once per row, not per tick. */\n private readonly firstErrorLogged = new Set<string>()\n /** `<ui-package>` \u2192 `<app>/ui` tag, so {@link uiTargets} can name the rows {@link uiPortMap} keys by package. */\n private readonly uiTagByPackage = new Map<string, string>()\n /** Liveness re-probe cadence. A wedged-but-not-crashed backend is caught within THRESHOLD ticks. */\n private static readonly LIVENESS_INTERVAL_MS = 5000\n /** Consecutive failures before a row is declared down \u2014 the anti-flap / restart-window filter. */\n private static readonly LIVENESS_FAILURE_THRESHOLD = 2\n /**\n * Refused probes a UI that has NEVER been up may take before its `\u25CC starting` turns `\u25CF down` (~30s at\n * the 5s cadence). Far above the backend threshold on purpose: a cold vite on a big frontend genuinely\n * takes tens of seconds to bind, and a red dot over a UI that is merely still booting is the one false\n * red this whole design refuses to ship. A UI that has been up once is not covered by it \u2014 its death is\n * probe-established at the ordinary threshold.\n */\n private static readonly NEVER_UP_DOWN_THRESHOLD = 6\n private readonly options: DevServerOptions\n /** Build runner seam \u2014 real turbo shell-out by default, injectable for tests. */\n private readonly runBuild: BuildRunner\n /** `turbo watch` spawn seam \u2014 real detached child by default, injectable for tests. */\n private readonly turboWatchFactory: TurboWatchFactory\n /** Live `turbo watch` engine in `--watch` mode; reaped on {@link shutdown}. */\n private turboWatch: TurboWatchHandle | null = null\n /** `turbo run dev` (frontends) spawn seam \u2014 real detached child by default, injectable for tests. */\n private readonly uiDevFactory: UiDevFactory\n /** Live `turbo run dev` frontend engine in `--ui` mode; reaped on {@link shutdown}. */\n private uiDev: UiDevHandle | null = null\n /**\n * Per-service log files. One held fd per service \u2014 so a chunk tee during an HMR burst is a single\n * `writeSync` on an already-open fd, not the open/write/close that the old `appendFileSync` paid per\n * call, and there is no buffered stream whose tail a hard exit could drop.\n */\n private readonly sink: DevLogSink\n /**\n * Owns `console` + the raw stream writes for the life of a TTY session, routing every line into its\n * service's file. `null` on a `--json` / MCP / piped run, where stdout must stay byte-clean.\n */\n private readonly intercept: OutputIntercept | null\n /** `turbo --dry` closure-source seam \u2014 real turbo (via {@link buildClosureMap}'s default) unless injected for tests. */\n private readonly dryRunner: DryRunner | undefined\n /** Apps that threw during {@link startAllApps} \u2014 rendered as `\u25CF failed` rows by {@link printReady}. */\n private readonly failedApps: { app: IApiAppConfig; reason: string }[] = []\n\n /**\n * Routes that wanted a local backend and lost it to a start failure (see `local-pairing.ts`). Computed\n * once, right after {@link startAllApps} \u2014 it is a fact about the boot, and the panel re-derives which\n * of them are STILL degraded on every repaint from the running set.\n */\n private degradedRoutes: DegradedRoute[] = []\n /**\n * The terminal UI \u2014 owns every stdout line + the log-file tee. Either the plain {@link DevRenderer}\n * or the Ink boot UI, selected by {@link run} and injected here; the runner drives it through {@link DevUi}.\n */\n private readonly renderer: DevUi\n /** Health-probe seam (real bounded `/__health` GET by default; injectable for tests). */\n private readonly healthProbe: HealthProbe\n /** Portless driver (Layer B) \u2014 best-effort alias registration; injectable for tests. */\n private readonly proxy: PortlessDriver\n /**\n * The port the portless daemon must be serving TLS on. Fixed at {@link DEFAULT_DEV_PROXY_PORT} (443):\n * it is not configurable, because a port-free `https://` URL can only be served from the implicit HTTPS\n * port. A configurable port would put the port straight back into the URL.\n */\n private readonly proxyPort: number = DEFAULT_DEV_PROXY_PORT\n /** Every `<release>.<package>` alias this runner registered; removed one-by-one in {@link shutdown}. */\n private readonly registeredAliases = new Set<string>()\n /**\n * `{ \"<ui-package>\": { port, alias } }` handed to the turbo child via `INFRA_KIT_UI_PORTS` (computed in\n * printReady).\n *\n * The `alias` is load-bearing, not provenance: the page is served from `https://<alias>` on :443, so\n * vite's HMR client must dial `wss://<alias>` \u2014 left to itself it derives the socket from vite's own\n * bound port, which the browser then blocks as mixed content. Only the runner knows which alias it\n * actually registered, so it publishes it rather than letting the helper re-derive it (a helper that\n * merely *computes* an alias would point HMR at an unregistered host on a bare `vite dev`).\n */\n private uiPortMap: Record<string, { port: number; alias: string }> = {}\n /**\n * Latched by {@link shutdown} before it touches any state. Teardown deregisters aliases and then\n * spends seconds reaping children, so without this latch a watch event \u2014 or a restart already in\n * flight \u2014 re-enters {@link startOneApp} and re-registers an alias that nothing will ever remove.\n */\n private shuttingDown = false\n /**\n * Dependency closure used to scope a shared-package restart, or `null` for \"restart every app\"\n * (the fail-safe {@link selectPackageRestartTargets} already honours). Read at EVENT time, not\n * captured when the watcher is armed: building it shells out to turbo (~1s on a 7-backend repo),\n * and blocking the watcher on it left a window right after `ready` in which a save was silently\n * dropped. The watcher arms immediately on `null` and this is swapped in when turbo answers.\n */\n private closureMap: ClosureMap | null = null\n /** In-flight {@link closureMap} build, awaited by {@link shutdown} so no turbo child outlives the runner. */\n private closureBuild: Promise<void> = Promise.resolve()\n /**\n * The single in-flight teardown, memoized by {@link shutdown} \u2014 `null` until the first caller arrives.\n *\n * Terminal death is ONE event observed through TWO channels: the kernel SIGHUPs the foreground process\n * group (`signal-shutdown.ts` handles it) and the next write trips the stdio `'error'` listener\n * (`terminal-liveness.ts`). Both fire, in either order. Without this memo the two callers would\n * `turboWatch.kill()`, `uiDev.kill()` and `server.close()` TWICE \u2014 a second SIGTERM\u2192grace\u2192SIGKILL cycle\n * aimed at a child group that is already mid-reap, which is the one way this fix could strand children\n * worse than the bug it repairs.\n */\n private teardown: Promise<void> | null = null\n /**\n * The teardown step currently in flight, for {@link shutdownStage}. Assigned before EVERY `await` in\n * {@link doShutdown}, because the whole value of the deadline in `signal-shutdown` is that it can NAME\n * the step that wedged \u2014 that is the instrument that answers why five orphaned processes never exited.\n */\n private stage = 'idle'\n\n constructor(\n options: DevServerOptions = {},\n runBuild: BuildRunner = launchScript,\n turboWatchFactory: TurboWatchFactory = defaultTurboWatchFactory,\n uiDevFactory: UiDevFactory = defaultUiDevFactory,\n dryRunner?: DryRunner,\n renderer?: DevUi,\n healthProbe: HealthProbe = defaultHealthProbe,\n proxy: PortlessDriver = createPortlessDriver(),\n sink?: DevLogSink,\n ) {\n this.options = options\n this.runBuild = runBuild\n this.turboWatchFactory = turboWatchFactory\n this.uiDevFactory = uiDevFactory\n this.dryRunner = dryRunner\n this.proxy = proxy\n // Injectable so a test can drive the REAL `reportFault` against a temp-dir sink and assert on the bytes\n // it actually files. Without the seam the fault-loop regression test can only hand-model the loop it is\n // supposed to be proving \u2014 and a hand-modelled loop goes green against the unfixed code.\n this.sink = sink ?? new DevLogSink()\n logSink = this.sink\n\n // Install EARLY and FILE-ONLY: an app's log line never reaches the terminal, at any point. Early,\n // because handler modules are imported during boot \u2014 long before the panel exists \u2014 so a late\n // install would let their import-time output escape onto the screen, which is the leak this whole\n // mechanism exists to prevent (a Powertools `Server listening` banner printed above the panel).\n //\n // No tee window, and no boot crash is lost to it: Node writes a fatal stack STRAIGHT TO FD 2 (never\n // through the patched stream), a rejected `start()` surfaces after `shutdown()` has already called\n // `uninstall()`, and a post-`ready()` fault goes through `reportFault` onto the panel.\n this.intercept = ownsTerminal(this.options)\n ? installOutputIntercept({ sink: this.sink, fallbackService: RUNNER_SERVICE, currentService })\n : null\n // The renderer owns all terminal output + the log tee; construct it before the first narrate below.\n this.renderer = renderer ?? new DevRenderer({ appendLog: appendRunnerLog, verbose: this.options.verbose ?? false })\n this.healthProbe = healthProbe\n this.devContextDir = path.join(process.cwd(), '.infra-kit', 'dev-context')\n\n // Walk up from the consumer repo cwd to the monorepo root.\n this.monorepoRoot = findMonorepoRoot(process.cwd())\n\n if (process.env.DOPPLER_PROJECT != null || process.env.DOPPLER_ENVIRONMENT != null) {\n this.renderer.log('\uD83D\uDD10 Doppler env detected (DOPPLER_PROJECT / DOPPLER_ENVIRONMENT)', 'debug')\n }\n }\n\n /**\n * Discover API apps and resolve each app's port + URL prefix. Delegates bare\n * filesystem discovery to {@link discoverApiAppsBare} and per-app resolution to\n * the pure `resolvePort` / `resolvePrefixUrl`, preserving the original behavior.\n */\n private discoverApiApps(devConfig: DevConfig): IApiAppConfig[] {\n return discoverApiAppsBare(this.monorepoRoot).map((app) => {\n return {\n ...app,\n preferredPort: this.resolvePreferredPort(app.name, devConfig),\n prefixUrl: this.resolvePrefixUrl(app.name, devConfig),\n // Default participate; the resolved preset value overrides this in `run()`.\n watchDeps: true,\n }\n })\n }\n\n /**\n * Read the `dev` section from the resolved infra-kit config. Defensive: any\n * failure (not in an infra-kit project, missing/invalid config) resolves to an\n * empty map so the dev-server always falls back to env vars + built-in defaults\n * rather than refusing to start.\n */\n private async loadDevConfig(): Promise<DevConfig> {\n try {\n const config = await getInfraKitConfig()\n\n return config.dev ?? {}\n } catch {\n return {}\n }\n }\n\n /** Thin delegator to the pure {@link normalizeAppIncludePure} over the runner's `--app` list. */\n private normalizeAppInclude(): string[] | null {\n return normalizeAppIncludePure(this.options.include)\n }\n\n /** Thin delegator to the pure {@link resolvePreferredPortPure}, threading env + config. */\n private resolvePreferredPort(appName: string, devConfig: DevConfig): number | undefined {\n return resolvePreferredPortPure(appName, process.env, devConfig)\n }\n\n /** Thin delegator to the pure {@link resolvePrefixUrlPure}. */\n private resolvePrefixUrl(appName: string, devConfig: DevConfig): string {\n return resolvePrefixUrlPure(appName, devConfig)\n }\n\n /** Load the top-level `devServersPresets` map from the resolved infra-kit config (defensive: `{}` on failure). */\n private async loadDevPresets(): Promise<DevPresets> {\n try {\n return (await getInfraKitConfig()).devServersPresets ?? {}\n } catch {\n return {}\n }\n }\n\n /**\n * The preset definition to run: the named preset (`infra-kit dev <preset>`; throws with the\n * available names when unknown), or an `apps`-less preset when no preset was given \u2014 which\n * `resolvePreset` expands to every discovered app + part.\n */\n private resolvePresetDef(devPresets: DevPresets): DevPreset {\n // A wizard-built in-memory preset wins over the named lookup: it already IS the resolved run plan.\n if (this.options.presetDef != null) {\n return this.options.presetDef\n }\n\n const name = this.options.preset\n\n if (name == null) {\n return {}\n }\n\n const def = devPresets[name]\n\n if (!def) {\n const available = Object.keys(devPresets)\n\n throw new Error(\n `Unknown dev preset \"${name}\". Available: ${available.length > 0 ? available.join(', ') : '(none defined in devServersPresets)'}`,\n )\n }\n\n return def\n }\n\n public async start(): Promise<void> {\n // Backend readiness clock (UI is fire-and-forget, so `ready in Xs` is BE-only \u2014 labeled honestly).\n const bootStart = Date.now()\n const include = this.normalizeAppInclude()\n const watch = this.options.watch ?? false\n const devConfig = await this.loadDevConfig()\n\n process.env.POWERTOOLS_DEV ??= 'true'\n process.env.LOG_LEVEL ??= 'DEBUG'\n\n this.renderer.narrate('\uD83D\uDE80 Starting Development Server Runner')\n\n if (watch) {\n this.renderer.narrate('\uD83D\uDC40 Watch mode: will rebuild and restart on file save')\n }\n\n const { apps, uiApps, apiAppsAll, uiAppsAll, wantedLocalPkgs, presetProxy } = await this.resolveRunPlan(\n devConfig,\n include,\n )\n\n if (apps.length === 0 && uiApps.length === 0) {\n this.renderer.log('\u26A0\uFE0F No API or UI apps to run for this preset', 'warn')\n\n return\n }\n\n await this.bringUpProxy(apps)\n await this.buildAll(apps, uiApps, watch)\n\n if (apps.length > 0) {\n this.renderer.bootStep('starting servers')\n await this.startAllApps(apps)\n this.renderer.narrate(\n this.failedApps.length === 0\n ? '\uD83C\uDF89 All servers started!'\n : `\u26A0\uFE0F ${this.appServers.length}/${apps.length} servers started \u2014 ${this.failedApps.length} failed`,\n )\n this.renderer.narrate(`\uD83D\uDCDD Logs \u2192 ${homeShorten(this.sink.dir)} (one file per service)`)\n }\n\n // The label for what the user asked to run. Derived from the post-`--app` sets \u2014 not from\n // `options.preset` (unset for the wizard's in-memory preset) nor from `include` (app names only).\n // Hoisted above the refusal below so both it and the ready header name the run the same way.\n const target = deriveTargetLabel({\n preset: this.options.preset,\n running: [\n ...apps.map((a) => {\n return `${a.name}/api`\n }),\n ...uiApps.map((a) => {\n return `${a.name}/ui`\n }),\n ],\n discovered: [\n ...apiAppsAll.map((a) => {\n return `${a.name}/api`\n }),\n ...uiAppsAll.map((a) => {\n return `${a.name}/ui`\n }),\n ],\n })\n\n // A backend that was asked for and died takes its frontend's `local` routes down with it \u2014 silently,\n // to cloud. Refuse BEFORE printReady: a refusal must not register portless aliases or spawn a vite it\n // is about to abandon, and the message carries the backend's real error, so there is nothing left in\n // the header the user still needs.\n //\n // `--watch` is the one exception, and only because it can genuinely fix this: a boot-failed app is a\n // restart target now (see {@link resolveRestartTargets}), so the next save can bring the backend up\n // and the route back to local. It stays resident with a loud, self-clearing `\u26A0 \u2026 \u25CF cloud` row instead.\n //\n // That healing is real, and it is the `infraKit()` vite PLUGIN that makes it real: its `configureServer`\n // hook watches the dev-context fragment dir, re-resolves the proxy, and restarts vite when the resolved\n // map changes (verified live \u2014 `[vite] server restarted.` the moment a fragment appears). A UI that\n // instead wires the bare `infraKitDev()` helper directly in its vite config has NO such watcher: its\n // proxy is baked at config load, so a backend recovering mid-session will not flip its route back to\n // `local` until that UI is restarted. Discovery treats both shapes as managed, so on such a UI this row\n // can clear while the traffic still goes to cloud. The plugin is the supported wiring and every\n // consumer uses it today; stated here so the assumption is on the record rather than merely held.\n this.degradedRoutes = await this.collectDegradedRoutes(uiApps, wantedLocalPkgs, presetProxy)\n if (this.degradedRoutes.length > 0 && !watch) {\n throw new Error(formatPairingRefusal(this.degradedRoutes, target))\n }\n\n // Collapse the boot spinner into the calm ready header (BE endpoints + UI reference lines).\n // Runs for a UI-only session too, so it never leaves a blank screen. The route dump is opt-in.\n await this.printReady(apps, uiApps, bootStart, target)\n if (this.options.routes) {\n this.printRouteDump()\n }\n\n // Everything the user asked for is dead and there is no UI to fall back on: there is nothing left\n // to serve, watch, or proxy. Resident-but-empty is the worst of both worlds \u2014 it looks like a\n // running dev server and exits 0 when finally interrupted, so a CI step or a script would call it\n // a success. Fail loudly instead. A PARTIAL failure stays resident: the survivors are still useful,\n // and under `--watch` a boot-failed app IS retried on the next save ({@link resolveRestartTargets}) \u2014\n // without `--watch` it is gone for the session, which is why a broken local pairing refuses above.\n if (this.appServers.length === 0 && uiApps.length === 0 && this.failedApps.length > 0) {\n throw new Error(\n `infra-kit dev: no app started (${this.failedApps.length} of ${apps.length} failed). ` +\n `First failure: ${this.failedApps[0]?.reason ?? 'unknown'}`,\n )\n }\n\n this.armWatch(apps, uiApps, watch)\n\n // Frontends last: their delegated `turbo run dev` feeds the live tail below the BE table.\n if (uiApps.length > 0) {\n this.startUiDev(uiApps)\n }\n\n // Armed UNCONDITIONALLY, including for a UI-only session with nothing to probe.\n //\n // The tick does two jobs now: it probes backend health, and it repaints the status panel. The probe\n // half is a backend concern; the repaint half is not \u2014 and gating the whole timer on\n // `appServers.length > 0` gated the PANEL on a backend existing. A UI-only session painted once at\n // boot and then froze: a `\u26A0 0` row held forever while vite piped compile errors into its log file\n // and the counter behind the row climbed unread. A green row over a broken UI is precisely the lie\n // this design exists to prevent, and it is worse here than the tail it replaced, because there is no\n // longer anything else on screen to contradict it.\n //\n // Over an empty `appServers` the probe half is `Promise.all([])` \u2014 free. The timer is `unref`'d, so\n // it cannot hold the loop open on its own.\n this.startLivenessMonitor()\n }\n\n /**\n * Resolve WHAT to run: discover the api/ui app parts, resolve the active preset against them, then\n * narrow to the `--app`/`--self` include set. Returns both the filtered run sets (`apps`/`uiApps`)\n * and the full discovered sets (`apiAppsAll`/`uiAppsAll`), which the ready-header target label needs.\n */\n private async resolveRunPlan(\n devConfig: DevConfig,\n include: string[] | null,\n ): Promise<{\n apps: IApiAppConfig[]\n uiApps: DiscoveredUiApp[]\n apiAppsAll: IApiAppConfig[]\n uiAppsAll: DiscoveredUiApp[]\n /**\n * Backend packages the PRESET promised to serve locally, captured BEFORE `--app`/`--self` narrowing.\n * Narrowing changes what runs; it does not change what the preset promised, and a backend dropped by\n * a narrowing flag is never attempted \u2014 so it never lands in `failedApps` and a crash-keyed check\n * would say nothing while its frontend quietly proxied to cloud. See `local-pairing.ts`.\n */\n wantedLocalPkgs: Set<string>\n /** The preset's declared per-route proxy overrides (`app \u2192 route \u2192 source`), for `pinnedLocal`. */\n presetProxy: Record<string, Record<string, ProxySource>>\n }> {\n // What to run is a named preset (`infra-kit dev <preset>`), resolved against the discovered app\n // parts (api/ui). No preset \u2192 run everything (`*`). `--app`/`--self` (`include`) further narrow it.\n const apiAppsAll = this.discoverApiApps(devConfig)\n const uiAppsAll = discoverUiAppsBare(this.monorepoRoot)\n const resolved = resolvePreset(this.resolvePresetDef(await this.loadDevPresets()), {\n api: apiAppsAll.map((a) => {\n return a.name\n }),\n ui: uiAppsAll.map((a) => {\n return a.name\n }),\n })\n\n if (resolved.unmatched.length > 0) {\n this.renderer.log(`\u26A0\uFE0F Preset targets not found (skipped): ${resolved.unmatched.join(', ')}`, 'warn')\n }\n\n const apiNames = new Set(\n resolved.targets\n .filter((t) => {\n return t.part === 'api'\n })\n .map((t) => {\n return t.app\n }),\n )\n const uiNames = new Set(\n resolved.targets\n .filter((t) => {\n return t.part === 'ui'\n })\n .map((t) => {\n return t.app\n }),\n )\n const passesInclude = (name: string): boolean => {\n return !include || include.includes(name)\n }\n const watchDepsByApp = new Map(\n resolved.targets\n .filter((t) => {\n return t.part === 'api'\n })\n .map((t) => {\n return [t.app, t.watchDeps] as const\n }),\n )\n const apps = apiAppsAll\n .filter((a) => {\n return apiNames.has(a.name) && passesInclude(a.name)\n })\n .map((a) => {\n return { ...a, watchDeps: watchDepsByApp.get(a.name) ?? a.watchDeps }\n })\n const uiApps = uiAppsAll.filter((a) => {\n return uiNames.has(a.name) && passesInclude(a.name)\n })\n\n // Every api the PRESET names, pre-`passesInclude` \u2014 the promise, not the survivors.\n const wantedLocalPkgs = new Set(\n apiAppsAll\n .filter((a) => {\n return apiNames.has(a.name)\n })\n .map((a) => {\n return a.packageName\n }),\n )\n\n return { apps, uiApps, apiAppsAll, uiAppsAll, wantedLocalPkgs, presetProxy: resolved.proxy }\n }\n\n /**\n * Which of this run's routes are about to be silently misrouted to cloud, because the local backend\n * they name failed to start (see `local-pairing.ts` for why this is the dangerous half).\n *\n * Reads each launched frontend's own `infra-kit.config.ts` \u2014 the SAME file, through the same loader,\n * that the vite helper will read when it resolves the proxy. That is deliberate: any second source of\n * truth here could disagree with the proxy the frontend actually ends up serving, and a disagreement\n * would either cry wolf or (far worse) stay quiet on a real one. A frontend with no `dev.proxy` block\n * declares no routes and can degrade nothing.\n */\n private async collectDegradedRoutes(\n uiApps: DiscoveredUiApp[],\n wanted: ReadonlySet<string>,\n presetProxy: Record<string, Record<string, ProxySource>>,\n ): Promise<DegradedRoute[]> {\n if (uiApps.length === 0) return []\n\n const uis: LaunchedUi[] = []\n\n for (const ui of uiApps) {\n // A config that throws must not take the run down here: the frontend is about to load the very same\n // file itself and will report it far better than this check can. Treat it as \"declares no routes\"\n // and let vite own the error.\n const dev = await loadDev(ui.path).catch(() => {\n return undefined\n })\n\n if (!dev?.proxy) continue\n\n const overrides = presetProxy[ui.name] ?? {}\n const routes = Object.fromEntries(\n Object.entries(dev.proxy.routes).map(([route, spec]) => {\n return [route, { ...spec, pinnedLocal: overrides[route] === 'local' }]\n }),\n )\n\n uis.push({ app: ui.name, routes, cloudTemplate: dev.proxy.templates.cloud })\n }\n\n return findDegradedRoutes({\n uis,\n wanted,\n // REALITY: the packages actually serving. `appServers` holds exactly the apps that bound a port and\n // wrote a dev-context fragment \u2014 which is the same fact the vite helper reads to decide `local`.\n running: new Set(\n this.appServers.map(({ app }) => {\n return app.packageName\n }),\n ),\n reasons: new Map(\n this.failedApps.map(({ app, reason }) => {\n return [app.packageName, { app: app.name, reason }]\n }),\n ),\n env: process.env[INFRA_KIT_ENV_VAR],\n })\n }\n\n /**\n * Layer B: confirm the portless proxy is serving BEFORE any alias registration \u2014 backends register in\n * startOneApp, UIs in printReady, and both need a live daemon. Runs ahead of startAllApps AND startUiDev\n * so a UI-only session checks it too; a proxy that is not up throws here, before anything is spawned.\n *\n * The port is no longer negotiable: every dev URL is `https://<alias>` with no port, which means the one\n * port that can serve them is {@link DEFAULT_DEV_PROXY_PORT} (443, the implicit HTTPS port).\n */\n private async bringUpProxy(apps: IApiAppConfig[]): Promise<void> {\n // Before anything is spawned or aliased: refuse if the consumer's PINNED `infra-kit/vite` predates the\n // HTTPS contract. It would ignore the fragment's `origin` and proxy plain HTTP at a TLS listener.\n assertHelperVersionFloor(findMonorepoRoot(process.cwd()))\n await this.ensureProxy()\n\n if (apps.length > 0) this.assertNoPortConflicts(apps)\n }\n\n /**\n * Build both build phases back to back under one boot line: the API boot closure (dist must exist\n * before servers import handlers) then the UI dependency closure (warmed BEFORE any persistent child\n * so both see a warm cache and the cold-cache double-build race can't corrupt shared dist).\n */\n private async buildAll(apps: IApiAppConfig[], uiApps: DiscoveredUiApp[], watch: boolean): Promise<void> {\n // One boot line covers both build phases: they run back to back, so two boot steps only make\n // the spinner flip between near-identical lines. Never empty \u2014 the no-apps case returned above.\n const bootLabel = [\n apps.length > 0 ? `building ${packageList(apps)}` : '',\n uiApps.length > 0 ? `warming ${packageList(uiApps)}` : '',\n ]\n .filter(Boolean)\n .join(' \u00B7 ')\n\n this.renderer.bootStep(bootLabel)\n\n if (apps.length > 0) await this.buildApps(apps, watch)\n if (uiApps.length > 0) await this.buildUiApps(uiApps)\n }\n\n /**\n * Start the watch engine when there's anything to rebuild \u2014 backends OR frontends. A UI-only session\n * (no API app) still needs the engine so a shared-lib edit rebuilds its dist and vite reloads.\n */\n private armWatch(apps: IApiAppConfig[], uiApps: DiscoveredUiApp[], watch: boolean): void {\n if (!(watch && (this.appServers.length > 0 || uiApps.length > 0))) {\n return\n }\n\n // Arm the watcher FIRST, on the fail-safe `null` map (a shared-package change restarts every app).\n // Awaiting the closure map here instead cost ~1s on a 7-backend repo \u2014 a full second after \"ready\"\n // in which a save was watched by nobody. Scoping is an optimisation; never buy it with a blind window.\n this.setupWatch(apps, uiApps)\n\n this.closureBuild = this.buildClosureMapSafe(apps).then((map) => {\n // A teardown may have overtaken us; assigning is harmless either way (nothing reads it after).\n this.closureMap = map\n })\n }\n\n /**\n * Warm ONLY each UI's dependency closure (`<pkg>^...` \u2014 deps, excluding the UI itself, so no full\n * production `vite build`) with a cache-friendly (non-`--force`) turbo build.\n *\n * This is the ONLY build of that closure: the `turbo run dev` child runs with `--only`, so it no\n * longer re-walks `^build` as a fallback (that walk was pure noise \u2014 see {@link file://./ui-dev.ts}).\n * Still non-fatal, because a failure here is loud rather than silent: vite fails to resolve the\n * missing dep and reports it in the live tail. Warn and continue instead of refusing to start.\n */\n private async buildUiApps(uiApps: DiscoveredUiApp[]): Promise<void> {\n const filters = uiApps\n .map((a) => {\n return `--filter=${a.packageName}^...`\n })\n .join(' ')\n\n try {\n await this.runBuild(\n `pnpm exec turbo run build ${filters} --env-mode=loose --output-logs=errors-only --no-update-notifier`,\n this.renderer.logFn,\n )\n this.renderer.narrate('\u2705 UI deps built')\n } catch (error) {\n this.renderer.log(\n `\u26A0\uFE0F UI dep build failed (continuing; vite will report unresolved deps): ${String(error)}`,\n 'warn',\n )\n }\n }\n\n /**\n * Start the frontends via ONE delegated `turbo run dev` child. Its stdio is PIPED, so the raw output\n * is tee'd verbatim to the runner log while each framework line lands in the renderer's tagged tail \u2014\n * the terminal stays owned by `infra-kit dev`. Reaped on {@link shutdown}. Concurrency \u2265 the persistent\n * UI `dev` task count (turbo hard-errors otherwise).\n */\n private startUiDev(uiApps: DiscoveredUiApp[]): void {\n const names = uiApps\n .map((a) => {\n return a.name\n })\n .join(', ')\n\n this.renderer.narrate(`\uD83C\uDFA8 Starting ${uiApps.length} UI dev server(s) via \\`turbo run dev\\`: ${names}`)\n this.renderer.narrate(' (framework output is routed into the live tail; full detail in the runner log)')\n\n // Hand the runner-assigned UI ports to the vite child so each UI binds exactly the port the ready\n // header already advertised (`strictPort` on the vite side) \u2014 proxy or not, the printed URL and the\n // bound port cannot drift. An empty map (every assignment failed) passes no env, so vite falls back\n // to picking its own free port and printing it.\n const uiPortEnv =\n Object.keys(this.uiPortMap).length > 0 ? { INFRA_KIT_UI_PORTS: JSON.stringify(this.uiPortMap) } : undefined\n\n // Every dev URL is now HTTPS behind portless's PRIVATE CA, which Node's bundled trust store knows\n // nothing about (and Node does not read the macOS keychain, so `portless trust` alone does not help a\n // Node process). Vite's own proxy is covered by the scoped `secure: false`, but anything ELSE the dev\n // loop runs \u2014 a backend calling a sibling's hero URL, a node `fetch`, an e2e runner \u2014 would fail with\n // `SELF_SIGNED_CERT_IN_CHAIN`. Hand the CA down so those clients validate instead of breaking.\n // A pre-set NODE_EXTRA_CA_CERTS wins: it is the user's own trust decision, not ours to overwrite.\n const caEnv =\n process.env.NODE_EXTRA_CA_CERTS == null && fs.existsSync(readCaPath())\n ? { NODE_EXTRA_CA_CERTS: readCaPath() }\n : undefined\n\n // turbo tags each line with the PACKAGE name; the endpoint rows are keyed by APP name (`client/ui`),\n // so map back onto the exact label the ready header already shows. An unrecognized package degrades\n // to its own name rather than being dropped \u2014 a line is never silently lost.\n const tagByPackage = new Map(\n uiApps.map((a) => {\n return [a.packageName, `${a.name}/ui`] as const\n }),\n )\n\n this.uiDev = this.uiDevFactory({\n packageNames: uiApps.map((a) => {\n return a.packageName\n }),\n cwd: process.cwd(),\n concurrency: Math.max(uiApps.length + 4, 12),\n env: uiPortEnv || caEnv ? { ...uiPortEnv, ...caEnv } : undefined,\n // The RAW chunk tee: turbo's chrome plus every framework line, un-de-multiplexed. It arrives\n // before `parseTurboDevLine` has attributed it to a package, so there is no honest app to file it\n // under \u2014 it gets its own `turbo.log`. The attributed lines land in `<app>/ui` below.\n appendLog: (text) => {\n this.sink.write(TURBO_SERVICE, text)\n },\n onLine: ({ pkg, text, level }) => {\n const tag = tagByPackage.get(pkg) ?? `${pkg}/ui`\n\n this.sink.write(tag, text, { level })\n },\n // Surface a silently-dead frontend engine: once `turbo run dev` exits, every UI's live reload\n // stops and no framework line ever reaches the tail again.\n onUnexpectedExit: (detail) => {\n this.markUiEngineDead()\n this.reportEngineDeath('UI dev engine (`turbo run dev`)', 'frontends stopped reloading', detail)\n },\n })\n }\n\n /** Render an app list as `name:port, name:port` for log lines. */\n private formatAppList(apps: Array<{ name: string; port: number }>): string {\n return apps\n .map((a) => {\n return `${a.name}:${a.port}`\n })\n .join(', ')\n }\n\n /**\n * Throw (after logging remediation tips) when two apps are EXPLICITLY pinned to the same\n * port. Apps with no explicit port bind an ephemeral `listen(0)` port each (collision-free\n * by construction), so they are excluded from the gate \u2014 otherwise the default multi-app\n * run would false-throw on the shared `DEFAULT_PORT` before dynamic allocation de-conflicts.\n */\n private assertNoPortConflicts(apps: IApiAppConfig[]): void {\n const explicitApps = apps\n .filter((app) => {\n return app.preferredPort != null\n })\n .map((app) => {\n return { name: app.name, port: app.preferredPort! }\n })\n const { duplicatePorts, conflictingApps } = findPortConflicts(explicitApps)\n\n if (duplicatePorts.length === 0) {\n return\n }\n\n this.renderer.log(`\u26A0\uFE0F Port conflict detected! ${duplicatePorts.join(', ')}`, 'error')\n this.renderer.log(`Conflicting apps: ${this.formatAppList(conflictingApps)}`, 'error')\n this.renderer.log('\\n\uD83D\uDCA1 Tip: give each app a distinct port via `{APP}_PORT` env (e.g. `CLIENT_PORT=`,', 'error')\n this.renderer.log(' `SEARCH_ENGINE_PORT=`) or `dev.<app>.port` in infra-kit.json; or run a subset with', 'error')\n this.renderer.log(' `--app=<name>,<name>`.\\n', 'error')\n throw new Error(`Port conflict detected: ${duplicatePorts.join(', ')}`)\n }\n\n /** Build every app via turbo; rethrows the build error after logging stdout/stderr. */\n private async buildApps(apps: IApiAppConfig[], watch: boolean): Promise<void> {\n const filters = apps\n .map((a) => {\n return `--filter=${a.packageName}`\n })\n .join(' ')\n // With `--watch`, always bypass Turbo cache so `tsc` runs and `dist/` matches disk (otherwise watch restarts can be no-ops).\n const buildCmd = `pnpm exec turbo run build ${filters} --env-mode=loose --output-logs=errors-only --no-update-notifier${watch ? ' --force' : ''}`\n\n // No narration here: the `building <pkgs>` boot step already names these exact targets.\n try {\n await this.runBuild(buildCmd, this.renderer.logFn)\n this.renderer.narrate('\u2705 Build complete')\n } catch (buildError) {\n this.renderer.log(`\u274C Build failed: ${String(buildError)}`, 'error')\n if (buildError instanceof Error && buildError.message) {\n this.renderer.log(` ${buildError.message}`, 'error')\n }\n const err = buildError as { stdout?: string; stderr?: string }\n\n if (err.stdout) this.renderer.log(` stdout: ${err.stdout.trim()}`, 'error')\n if (err.stderr) this.renderer.log(` stderr: ${err.stderr.trim()}`, 'error')\n throw buildError\n }\n }\n\n /**\n * Start every app concurrently, collecting the ones that boot; per-app failures are logged,\n * not fatal. Safe to parallelize because each app has a distinct port (guarded up-front) and a\n * distinct `ServerlessLocalRun`, and `startOneApp` no longer mutates cwd. Push order into\n * `appServers` is non-deterministic but nothing depends on it (the table renders from `apps`).\n */\n private async startAllApps(apps: IApiAppConfig[]): Promise<void> {\n await Promise.all(\n apps.map(async (app) => {\n try {\n const started = await this.startOneApp(app)\n\n if (started) {\n this.appServers.push({ app, ...started, startedAt: Date.now(), restarts: 0 })\n }\n } catch (error) {\n this.renderer.log(`\u274C Failed to start ${app.name}: ${String(error)}`, 'error')\n // Recorded, not just logged: `printReady` renders the table from `appServers`, so an app\n // that only ever appears in a log line vanishes from the header entirely \u2014 indistinguishable\n // from one that was never requested. `failedApps` is what puts the `\u25CF failed` row back.\n this.failedApps.push({ app, reason: errorReason(error) })\n }\n }),\n )\n }\n\n /**\n * Confirm a portless daemon is serving TLS on {@link DEFAULT_DEV_PROXY_PORT} before any alias is\n * registered. Every dev URL is a hostname served by this daemon, so a proxy that is not up is a FATAL\n * start error, not a degraded mode: there is no second way to reach an app, and a half-started dev loop\n * that silently routes nowhere is worse than a refusal that names the fix.\n *\n * **Probe only \u2014 never start, never elevate.** `:443` is privileged, and portless binds it by re-execing\n * through `sudo` with an inherited stdio, which a detached child cannot answer. The daemon is installed\n * once, out-of-band. There is deliberately no unprivileged fallback: a fallback puts the port back in the\n * URL, which is the whole thing this design removes.\n *\n * Identity is proven **on the wire** (`X-Portless`), never from portless's state files \u2014 those are\n * process-global singletons that any other daemon's start rewrites and any stop deletes, which would make\n * a perfectly healthy `:443` daemon look dead. See {@link defaultIsProxyServing}.\n *\n * @throws When portless is missing, or no portless daemon is serving TLS on the proxy port.\n */\n private async ensureProxy(): Promise<void> {\n // The bin comes from the driver we were handed, never from a fresh resolution behind its back: the fix we\n // print must name the binary THIS driver would run. `null` is not a command to render \u2014 it is a different\n // report, and it is made here rather than passed downstream.\n const bin = this.proxy.binPath()\n\n if (bin == null || !(await this.proxy.isAvailable())) {\n throw new Error(\n 'infra-kit dev: portless is not installed, so no dev URL can resolve. It ships as a dependency of infra-kit \u2014 reinstall with `pnpm install`.',\n )\n }\n\n if (await this.proxy.isProxyServing(this.proxyPort, true)) return\n\n throw new Error(\n `infra-kit dev: no portless daemon is serving HTTPS on :${this.proxyPort}, so no dev URL can resolve. ` +\n 'Install it once (this is the only step that needs root):\\n' +\n ` ${formatPortlessCommand(['service', 'install'], { sudo: true, bin })}\\n` +\n 'Then trust its local CA (no sudo needed):\\n' +\n ` ${formatPortlessCommand(['trust'], { bin })}\\n` +\n '`infra-kit doctor` checks both.',\n )\n }\n\n /**\n * Register `<release>.<package>` \u2192 `port` with portless and return the alias HOST\n * (`<release>.<package>.localhost`). The alias IS the app's only address, so a failure here is fatal\n * rather than a silent downgrade \u2014 an app nobody can reach is not a running app.\n *\n * @throws When the package name yields no legal DNS label, or portless rejects the registration.\n */\n private async registerAppAlias(packageName: string, appDir: string, port: number): Promise<string> {\n const release = readAppRelease(appDir)\n // An npm name is not a DNS label \u2014 see {@link slugifyHostLabel}. `infra-kit/vite` slugifies its\n // own `<packageName>` template token identically, so the proxy target and this alias cannot drift.\n const label = slugifyHostLabel(packageName)\n\n if (label === '') {\n throw new Error(`infra-kit dev: package name \"${packageName}\" has no letters or digits to build a hostname from.`)\n }\n const name = `${release}.${label}`\n\n if (!(await this.proxy.registerAlias(name, port))) {\n throw new Error(`infra-kit dev: portless refused the alias \"${name}\" \u2192 127.0.0.1:${port}.`)\n }\n this.registeredAliases.add(name)\n\n return `${name}.localhost`\n }\n\n private async startOneApp(app: IApiAppConfig): Promise<StartedApp | null> {\n this.renderer.narrate(`\uD83D\uDD04 Starting ${app.name}...`)\n\n // No `process.chdir` here: `ServerlessLocalRun` reads `serverless.yml` and imports the\n // compiled handler from `controllersPath` (absolute), so the runner never mutates cwd \u2014\n // which is what makes concurrent boot/restart safe.\n const server = new ServerlessLocalRun({\n controllersPath: app.path,\n prefixUrl: app.prefixUrl,\n port: app.preferredPort,\n appName: app.name,\n // Claims every line this app's handlers emit \u2014 `console.log`, Powertools, a dependency's banner \u2014\n // for `<app>/api`, via an AsyncLocalStorage context entered in the request's `onRequest` hook and\n // around the handler module's import.\n //\n // Without it NOTHING attributes: the backend is in-process and multi-app, so a raw stdout write\n // says nothing about which app produced it, and every handler line falls into the runner's\n // fallback bucket. The app's row then counts zero errors no matter how loudly its handler fails \u2014\n // which, with no log tail on screen, means the panel reports a healthy app that is broken.\n serviceTag: `${app.name}/api`,\n // Route live request traffic into the renderer's tagged, timestamped tail (`<app>/api \u2026`).\n // This is the structured seam \u2014 independent of the legacy `DEV_SERVER_REQUEST_LOG` raw line \u2014\n // so the app name is threaded in-process and never leaked to spawned turbo/vite children.\n onRequestLog: ({ method, path: reqPath, status, ms }) => {\n // Keep the runner's own `/__health` liveness probes out of the live tail \u2014 they are internal\n // noise, not app traffic. Real handler routes still stream.\n if (reqPath === '/__health') return\n\n const tag = `${app.name}/api`\n const text = `${method} ${reqPath} ${status} ${ms}ms`\n\n // The level is DECLARED, not sniffed: fastify already knows the status it returned. A 5xx is an\n // error because the server said so \u2014 no regex ever reads this line's bytes to decide.\n this.sink.write(tag, text, { level: status >= 500 ? 'error' : 'info' })\n // Mutate in place: spreading into a fresh array copied the whole window on every single request.\n const window = this.reqTimes.get(tag)\n\n if (window) window.push(Date.now())\n else this.reqTimes.set(tag, [Date.now()])\n },\n })\n\n // `start()` binds an ephemeral (or preferred-then-ephemeral) port and RETURNS the actual\n // one \u2014 consume THAT, never the static preferred hint, so the log/table/health agree.\n const boundPort = await server.start()\n\n // Layer B: (re)point the portless alias to the freshly-bound port (also fires on watch-restart,\n // since startOneApp is the shared start+restart path). Throws if the alias cannot be registered.\n //\n // Unwind the bind on failure: the caller only records `server` in `appServers` once this method\n // RETURNS, so a throw here would otherwise leave a listening fastify that `shutdown()` never sees\n // and never closes \u2014 a port held for the rest of the session.\n let alias: string\n\n try {\n alias = await this.registerAppAlias(app.packageName, app.path, boundPort)\n } catch (error) {\n await server.close().catch(() => {})\n throw error\n }\n\n // Record the ACTUAL bound port + the alias that was actually registered in this runner's own\n // dev-context fragment. Ordered AFTER registerAppAlias: a fragment written before it could only\n // ever claim `alias: undefined`, and the vite helper would fall back to a direct target for an\n // app that IS reachable by name. Inside startOneApp (the shared start+restart path) so a\n // watch-restart refreshes both instead of orphaning a stale fragment (M2). Non-fatal: a\n // fragment-write failure must not down the server.\n try {\n this.writeDevContextFragment(app, boundPort, alias)\n } catch (error) {\n this.renderer.log(`\u26A0\uFE0F Failed to write dev-context fragment for ${app.name}: ${String(error)}`, 'warn')\n }\n\n this.renderer.narrate(`\u2705 ${app.name} started on port ${boundPort}`)\n\n return { server, boundPort, alias }\n }\n\n /**\n * Atomically write this runner's `.infra-kit/dev-context/<app>.json` fragment recording the\n * ACTUAL bound port (REV-5: serialize to a same-dir temp file, then `renameSync` into place, so a\n * concurrent reader \u2014 the vite helper's directory merge \u2014 never observes torn JSON). Each runner\n * writes ONLY its own app's fragment, so cmux panes never clobber each other.\n */\n private writeDevContextFragment(app: IApiAppConfig, boundPort: number, alias: string): void {\n const fragment: DevContextFragment = {\n // Declares the wire contract this fragment honours, so the helper never has to INFER it from a\n // package version \u2014 which stopped being inferable the moment the helper moved to its own npm\n // package with its own version line. `v` promises `origin` below is present and authoritative; the\n // helper refuses rather than guessing a target if that promise is ever broken.\n v: DEV_CONTEXT_WIRE_VERSION,\n package: app.packageName,\n port: boundPort,\n pid: process.pid,\n writtenAt: Date.now(),\n release: readAppRelease(app.path),\n alias,\n // The ORIGIN of the hero URL the ready screen prints (`resolveEndpointUrl` appends the app's\n // `prefixUrl` on top of this same alias). Prefix-free on purpose: this is a proxy target, and the\n // frontend supplies its own path.\n origin: `https://${alias}`,\n }\n const target = path.join(this.devContextDir, `${app.name}.json`)\n const tmp = path.join(this.devContextDir, `${app.name}.json.${process.pid}.tmp`)\n\n fs.mkdirSync(this.devContextDir, { recursive: true })\n fs.writeFileSync(tmp, JSON.stringify(fragment, null, 2))\n fs.renameSync(tmp, target)\n }\n\n /** Remove THIS runner's own `<app>.json` fragment on shutdown (never another runner's). */\n private removeDevContextFragment(app: IApiAppConfig): void {\n fs.rmSync(path.join(this.devContextDir, `${app.name}.json`), { force: true })\n }\n\n /** Run restart jobs one after another (watch can fire faster than close + listen). */\n private scheduleRestartWork(work: () => Promise<void>): Promise<void> {\n const run = this.restartWorkChain.then(\n () => {\n return work()\n },\n () => {\n return work()\n },\n )\n\n this.restartWorkChain = run.catch(() => {})\n\n return run\n }\n\n private async delayPortRelease(): Promise<void> {\n await new Promise((r) => {\n return setTimeout(r, DevServerRunner.PORT_RELEASE_DELAY_MS)\n })\n }\n\n /**\n * Schedule a restart of the given apps (1 or N), serialized against other restarts via\n * {@link scheduleRestartWork}. A single-app dist change passes `[app]`; a dependency-package\n * dist change passes every running app. The `turbo watch` engine has already rebuilt `dist/`,\n * so the runner only bounces the fastify server(s) \u2014 no build here.\n */\n private restart(apps: IApiAppConfig[]): Promise<void> {\n if (this.shuttingDown) return Promise.resolve()\n\n return this.scheduleRestartWork(() => {\n // Re-checked inside the chain: this job may have queued behind a restart that was still\n // running when shutdown() latched, so the flag can flip between scheduling and execution.\n if (this.shuttingDown) return Promise.resolve()\n\n return this.runRestart(apps)\n })\n }\n\n /** Resolve the requested apps to their live server slots (dropping any not running). */\n private resolveRestartTargets(apps: IApiAppConfig[]): Array<{ idx: number; app: IApiAppConfig }> {\n return apps\n .map((app) => {\n return {\n idx: this.appServers.findIndex((e) => {\n return e.app.name === app.name\n }),\n app,\n }\n })\n .filter((t) => {\n // `idx >= 0` is a RUNNING app \u2014 the ordinary restart. `idx === -1` plus a `failedApps` entry is an\n // app that never came up at all, and it is a restart target precisely because it isn't running:\n // watch used to filter it out, which meant a backend that died on boot stayed dead for the whole\n // session no matter how many times you fixed and saved the file that broke it. Its frontend sat\n // there proxying to cloud the entire time. Everything else \u2014 an app the run never launched \u2014 is\n // neither, and is correctly skipped.\n return t.idx >= 0 || this.isFailedApp(t.app.name)\n })\n }\n\n /** Did this app fail to start (and is therefore still absent from {@link appServers})? */\n private isFailedApp(name: string): boolean {\n return this.failedApps.some((f) => {\n return f.app.name === name\n })\n }\n\n /**\n * Promote an app that just came back from the dead: it has no {@link appServers} slot to overwrite, so\n * it is appended, cleared from `failedApps`, and given the endpoint row the boot header never made for\n * it. Without that last step the panel \u2014 which maps over `lastSummary.endpoints` \u2014 would keep the\n * app invisible even though it is now serving.\n */\n private promoteRecoveredApp(app: IApiAppConfig, started: StartedApp): IAppServer {\n const entry: IAppServer = { app, ...started, startedAt: Date.now(), restarts: 0 }\n const tag = `${app.name}/api`\n\n // Seed the health entry BEFORE the app is visible in `appServers`, not after the probe the caller takes\n // a few lines later. The liveness tick can fire in that window, and {@link healthOf} treats an ABSENT\n // entry as `unknown` \u2014 a row with no dot at all for a server that is already serving. A fresh entry is\n // the honest prior instead: never-up, which for a backend reads `\u25CF down` until a probe says otherwise.\n this.healthEntry(tag, 'api')\n this.appServers.push(entry)\n\n const failedIdx = this.failedApps.findIndex((f) => {\n return f.app.name === app.name\n })\n\n if (failedIdx >= 0) this.failedApps.splice(failedIdx, 1)\n\n if (this.lastSummary) {\n this.lastSummary = {\n ...this.lastSummary,\n endpoints: [\n ...this.lastSummary.endpoints,\n {\n tag,\n url: resolveEndpointUrl({ prefixUrl: app.prefixUrl, alias: started.alias }),\n // NOT `ok`. This app has bound a port; nothing has probed it. A server that binds and then 500s\n // on `/__health` would be painted green here on the strength of having started \u2014 the same\n // unearned claim the `\u25CF failed` row exists to prevent. The caller probes right after and\n // {@link refreshStatus} paints the answer.\n health: this.healthOf(tag),\n },\n ],\n failed: (this.lastSummary.failed ?? []).filter((f) => {\n return f.tag !== tag\n }),\n }\n }\n\n return entry\n }\n\n private async runRestart(apps: IApiAppConfig[]): Promise<void> {\n const targets = this.resolveRestartTargets(apps)\n\n if (targets.length === 0) return\n\n const label = targets.length === 1 ? targets[0]!.app.name : `${targets.length} apps`\n\n this.renderer.log(`\uD83D\uDD04 Restarting ${label}...`)\n await Promise.all(\n targets.map(async ({ idx }) => {\n // A boot-failed target (`idx === -1`) has no server to close \u2014 it never bound one. Only a running\n // app is torn down before its replacement starts.\n if (idx < 0) return\n\n try {\n await this.appServers[idx]!.server.close()\n } catch (err) {\n this.renderer.log(` Close warning: ${String(err)}`, 'debug')\n }\n }),\n )\n\n await this.delayPortRelease()\n\n // Thread each restart's OUTCOME (the fresh entry, or `null` on failure) straight into the probe below\n // instead of re-reading `appServers[idx]`: a failed `startOneApp` leaves the stale, now-closed entry in\n // that slot, so re-reading it would probe a freed port \u2014 wasting the full probe timeout and, if the old\n // `close()` also failed, reporting the dead-but-still-listening server as `\u25CF up`. The outcome list makes\n // the no-probe \"down\" branch actually reachable on failure.\n const outcomes = await Promise.all(\n targets.map(async ({ idx, app }) => {\n try {\n const restarted = await this.startOneApp(app)\n\n if (restarted) {\n // A boot-failed app has no slot to overwrite: it is APPENDED and cleared from `failedApps`,\n // which is also what takes its frontend's `\u26A0 \u2026 \u25CF cloud` row down (see {@link degradedRows}).\n if (idx < 0) {\n const recovered = this.promoteRecoveredApp(app, restarted)\n\n this.renderer.log(`\u2705 ${app.name} recovered \u2014 its routes are served locally again`, 'info')\n\n return { app, entry: recovered }\n }\n\n // Carry the restart count forward across the replacement and reset the clock: `up Xs` must\n // measure THIS process, not the one watch just killed, or the panel would claim an uptime\n // for a server that has been alive for two seconds.\n const previous = this.appServers[idx]\n const entry: IAppServer = {\n app,\n ...restarted,\n startedAt: Date.now(),\n restarts: (previous?.restarts ?? 0) + 1,\n }\n\n this.appServers[idx] = entry\n\n return { app, entry }\n }\n\n return { app, entry: null }\n } catch (error) {\n // A target that was ALREADY failed stays failed \u2014 it keeps its `failedApps` entry and its\n // frontend keeps the degraded row. Only report the reason; a retry that fails again is the\n // expected case while the user is still fixing the bug that broke it.\n this.renderer.log(`\u274C Failed to ${idx < 0 ? 'start' : 'restart'} ${app.name}: ${String(error)}`, 'error')\n\n return { app, entry: null }\n }\n }),\n )\n // Show each restarted app's CURRENT bound port: an ephemeral-port app rebinds a fresh port on\n // restart, and the server table is printed only once at boot \u2014 so this line is the only place the\n // new port surfaces in the default (quiet) terminal.\n //\n // Re-probe `/__health` so the summary reports HONEST liveness, not a bare \"restarted\" for a server\n // that binds its port but 500s on the first request. Mirrors printReady's probe (127.0.0.1, never\n // `localhost` \u2014 ServerlessLocalRun binds v4 loopback only). Probes run concurrently; a target whose\n // restart failed (`entry === null`) is reported down without a probe. A single down server downgrades\n // the leading \u2705 to \u26A0\uFE0F so the line reads consistently.\n const probed = await Promise.all(\n outcomes.map(async ({ app, entry }) => {\n const tag = `${app.name}/api`\n\n // A restart that THREW never bound a port \u2014 there is nothing to probe, and nothing to be flap-shy\n // about. Routed through {@link markDown}, not the probe path, because the soft path would leave the\n // row one failure short of the threshold, i.e. GREEN, for a server that does not exist.\n if (entry == null) {\n this.markDown(tag, 'api')\n this.refreshStatus()\n\n return { label: `${app.name} \u25CF down`, healthy: false }\n }\n\n // Through the SAME state machine as every tick, so the panel's dot and this line report the same\n // probe. They used to be two verdicts computed from one result, and they could disagree.\n const target: ProbeTarget = { tag, port: entry.boundPort, kind: 'api' }\n const outcome = await this.healthProbe(target)\n\n this.recordProbe(target, outcome)\n this.refreshStatus()\n\n const healthy = outcome === 'ok'\n\n return { label: `${app.name}:${entry.boundPort} ${healthy ? '\u25CF up' : '\u25CF down'}`, healthy }\n }),\n )\n\n const allHealthy = probed.every((p) => {\n return p.healthy\n })\n\n this.renderer.log(\n `${allHealthy ? '\u2705' : '\u26A0\uFE0F '} Restarted ${probed\n .map((p) => {\n return p.label\n })\n .join(', ')}`,\n )\n }\n\n /**\n * Build the dependency-closure map ({@link buildClosureMap}) for scoped restarts, or `null` on\n * failure (a `turbo --dry` spawn/parse error). `null` is the fail-safe signal: {@link setupWatch}\n * then restarts every launched app on a package change, exactly as it did before scoping \u2014 never\n * a silent dropped restart.\n */\n private async buildClosureMapSafe(apps: IApiAppConfig[]): Promise<ClosureMap | null> {\n try {\n return await buildClosureMap(this.monorepoRoot, apps, this.dryRunner)\n } catch (err) {\n this.renderer.log(\n `\u26A0\uFE0F Dependency-closure map unavailable (${String(err)}); package changes restart all apps`,\n 'warn',\n )\n\n return null\n }\n }\n\n /**\n * Start the long-lived `turbo watch build` engine, then watch compiled `dist/` output to\n * trigger restarts. A change under an app's `dist` restarts that app; a change under a\n * `packages/<pkg>/dist` restarts only the participating backends whose dependency closure\n * includes that package ({@link selectPackageRestartTargets}), keyed per package dir so unrelated\n * packages don't collapse into one debounce bucket. When the closure map is unavailable\n * (`null`) it falls back to restarting every launched app (fail-safe superset). Restarts are\n * build-less \u2014 the engine already rebuilt `dist/`.\n */\n /**\n * Report a persistent engine (`turbo watch build` / `turbo run dev`) that died on its OWN as a single\n * warn line, unless teardown is already underway. Centralises the `shuttingDown` guard and the message\n * shape shared by both engine callbacks \u2014 `superviseChild`'s `killing` latch already suppresses the\n * exit our own `kill()` causes, so this only ever fires on a genuine crash.\n */\n private reportEngineDeath(engine: string, consequence: string, detail: string): void {\n if (this.shuttingDown) return\n this.renderer.log(`\u26A0\uFE0F ${engine} ${detail} \u2014 ${consequence}. Restart \\`infra-kit dev\\`.`, 'warn')\n }\n\n /**\n * The `turbo run dev` engine died on its own, so a UI that was NEVER up is never coming up \u2014 its vite\n * either never bound or went down with the engine, and no further probe is going to tell us anything the\n * engine's corpse has not already said. Recorded as `dead`, which {@link neverUpState} reads ONLY in the\n * never-up branch: a UI that HAS been up keeps its ordinary probe-established death, because turbo does\n * not kill its task process groups and our reap is best-effort \u2014 an orphaned vite may well still be\n * serving, and it would be a live, hot-reloading UI we had just painted red.\n */\n private markUiEngineDead(): void {\n if (this.shuttingDown) return\n\n for (const { tag, kind } of this.uiTargets()) {\n this.healthEntry(tag, kind).dead = true\n }\n this.refreshStatus()\n }\n\n private setupWatch(apps: IApiAppConfig[], uiApps: DiscoveredUiApp[]): void {\n this.turboWatch = this.turboWatchFactory({\n // API apps: dep-inclusive (`...<pkg>`) \u2014 rebuild the backend + its shared-lib closure and restart it.\n depInclusive: apps.map((a) => {\n return a.packageName\n }),\n // UI apps: dep-closure-only (`<pkg>^...`) \u2014 rebuild the frontend's shared libs (so vite reloads on a\n // FE-only lib edit) WITHOUT production-building the UI; vite owns the UI's own live reload.\n depClosure: uiApps.map((a) => {\n return a.packageName\n }),\n cwd: process.cwd(),\n // `turbo watch build` opens this path itself and inherits it as its stdio, so the watch engine\n // gets its own file rather than interleaving raw child bytes into any service's log.\n logFile: this.sink.pathFor(WATCH_SERVICE),\n // Surface a silently-dead engine: once `turbo watch build` exits, saves no longer rebuild `dist/`,\n // so no restart ever fires and the session looks healthy while being frozen.\n onUnexpectedExit: (detail) => {\n this.reportEngineDeath('Watch engine (`turbo watch build`)', 'file saves no longer rebuild', detail)\n },\n })\n this.renderer.narrate('\uD83D\uDC40 Watch mode: started `turbo watch build` engine; watching dist output')\n\n const appDistDirs = getAppDistDirs(apps)\n const packageDistDirs = getPackageDistDirs(this.monorepoRoot)\n const allDistDirs = [...appDistDirs, ...packageDistDirs]\n\n if (allDistDirs.length === 0) {\n this.renderer.log('\u26A0\uFE0F No app or package dist directories found to watch (were they built?)', 'warn')\n\n return\n }\n\n const usePoll = process.env.DEV_SERVER_CHOKIDAR_POLL === '1'\n\n const watcher = chokidar.watch(allDistDirs, {\n ignoreInitial: true,\n awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 100 },\n // Ignore tsc's incremental bookkeeping + sourcemaps: they rewrite on every build\n // (even content-identical ones) and would bounce fastify onto no real change.\n ignored: (p: string): boolean => {\n return p.endsWith('.tsbuildinfo') || p.endsWith('.map')\n },\n ...(usePoll ? { usePolling: true, interval: 400 } : {}),\n })\n\n this.watcher = watcher\n\n if (usePoll) {\n this.renderer.log('\uD83D\uDC40 chokidar: usePolling enabled (DEV_SERVER_CHOKIDAR_POLL=1)', 'debug')\n }\n\n watcher.on('change', (filePath: string) => {\n this.handleDistChange(filePath, apps, appDistDirs, packageDistDirs)\n })\n\n this.renderer.narrate(\n `\uD83D\uDC40 Watching ${appDistDirs.length} app dist + ${packageDistDirs.length} package dist dir(s) for changes...`,\n )\n }\n\n /**\n * Dispatch a single dist-file change to the right restart. A package (shared-lib) change restarts the\n * dependent apps selected from the closure map (or, fail-safe, every app when the map is missing); an\n * app's own dist change restarts just that app. All restarts are debounced.\n */\n private handleDistChange(\n filePath: string,\n apps: IApiAppConfig[],\n appDistDirs: string[],\n packageDistDirs: string[],\n ): void {\n this.renderer.log(`\uD83D\uDC40 dist change detected: ${filePath}`, 'debug')\n\n const change = classifyDistChange(filePath, appDistDirs, packageDistDirs)\n\n if (change.kind === 'package') {\n // Read late: the map may still be building (\u2192 `null` \u2192 restart all), which is correct, just unscoped.\n const targets = selectPackageRestartTargets(apps, this.closureMap, change.packageDir)\n\n if (targets === null) {\n // Fail-safe: no closure map / no package identity \u2192 restart every launched app. Note this\n // ignores per-app `watchDeps: false` opt-outs \u2014 opt-out is best-effort and yields to the\n // fail-safe superset, so a `turbo --dry` failure never silently drops a needed restart.\n this.scheduleDebounced('__packages__', () => {\n return this.restart(apps)\n })\n\n return\n }\n\n // Empty target set = the package is UI-only or every dependent opted out \u2192 no restart.\n // The `packageDir !== undefined` check narrows it to `string` for `packageDebounceKey`;\n // it is always defined here (a non-null `targets` implies a matched package identity).\n if (targets.length > 0 && change.packageDir !== undefined) {\n this.scheduleDebounced(packageDebounceKey(change.packageDir), () => {\n return this.restart(targets)\n })\n }\n\n return\n }\n\n const app = apps.find((a) => {\n return path.join(a.path, 'dist') === change.app\n })\n\n if (!app) return\n\n this.scheduleDebounced(app.name, () => {\n return this.restart([app])\n })\n }\n\n /**\n * Debounce a restart under `key`: cancel any pending timer for the same key and start\n * a fresh {@link DevServerRunner.WATCH_DEBOUNCE_MS} timer, so a burst of saves collapses\n * into one restart. Errors from the scheduled work are logged, never thrown.\n */\n private scheduleDebounced(key: string, work: () => Promise<void>): void {\n if (this.shuttingDown) return\n\n const existing = this.watchDebounceTimers.get(key)\n\n if (existing) clearTimeout(existing)\n\n const timer = setTimeout(() => {\n this.watchDebounceTimers.delete(key)\n if (this.shuttingDown) return\n work().catch((err) => {\n this.renderer.log(`Restart error (${key}): ${String(err)}`, 'error')\n })\n }, DevServerRunner.WATCH_DEBOUNCE_MS)\n\n this.watchDebounceTimers.set(key, timer)\n }\n\n /** Is the frontend health probe on? `--no-ui-health` / `INFRA_KIT_NO_UI_HEALTH=1` turn it off. */\n private uiHealthEnabled(): boolean {\n return (this.options.uiHealth ?? true) && process.env.INFRA_KIT_NO_UI_HEALTH !== '1'\n }\n\n /** Every running backend, as probe targets. The CURRENT `boundPort` is read fresh \u2014 a restart rebinds it. */\n private apiTargets(): ProbeTarget[] {\n return this.appServers.map(({ app, boundPort }) => {\n return { tag: `${app.name}/api`, port: boundPort, kind: 'api' as const }\n })\n }\n\n /**\n * Every MANAGED frontend, as probe targets \u2014 derived from {@link uiPortMap}, which is exactly the set of\n * UIs whose port this runner assigned. A UI that fell back to a reference line has no port we own, so\n * there is nothing to probe and its row stays `unknown` (no dot), which is the honest answer. Empty when\n * UI health is off, which is the whole implementation of `--no-ui-health`: no targets, no probes, no dots.\n */\n private uiTargets(): ProbeTarget[] {\n if (!this.uiHealthEnabled()) return []\n\n return Object.entries(this.uiPortMap).map(([pkg, { port }]) => {\n return { tag: this.uiTagByPackage.get(pkg) ?? `${pkg}/ui`, port, kind: 'ui' as const }\n })\n }\n\n /** This tag's entry, created on first sight. */\n private healthEntry(tag: string, kind: ProbeTarget['kind']): HealthEntry {\n const existing = this.health.get(tag)\n\n if (existing) return existing\n\n const fresh: HealthEntry = { kind, failures: 0, foreignStreak: 0, everUp: false, dead: false, unverified: false }\n\n this.health.set(tag, fresh)\n\n return fresh\n }\n\n /**\n * Fold one probe outcome into a row's history and fire the edge logs \u2014 each exactly once, so a steady\n * state (healthy OR wedged) says nothing.\n *\n * The `foreign` arms are where the two kinds part company. A backend answering non-2xx is OUR fastify\n * failing, so it counts as a failure like any other. A UI answering something that is not vite's ping is\n * unverifiable \u2014 a squatter, a proxy shadowing the ping, a future vite that dropped it \u2014 so it counts\n * toward nothing and only raises `unverified`, which renders `\u25CD ?` and never red.\n */\n private recordProbe(target: ProbeTarget, outcome: ProbeOutcome): void {\n const entry = this.healthEntry(target.tag, target.kind)\n const threshold = DevServerRunner.LIVENESS_FAILURE_THRESHOLD\n\n if (outcome === 'ok') {\n const wasDown = entry.failures >= threshold\n\n entry.everUp = true\n entry.failures = 0\n entry.foreignStreak = 0\n entry.unverified = false\n if (wasDown) this.renderer.log(`\u2705 ${target.tag} recovered`)\n\n return\n }\n\n if (isForeign(outcome) && target.kind === 'ui') {\n entry.foreignStreak += 1\n entry.unverified = entry.foreignStreak >= threshold\n if (entry.foreignStreak === threshold) {\n this.renderer.log(`\u26A0\uFE0F ${target.tag}: ${describeForeign(target.port, outcome)}`, 'warn')\n }\n\n return\n }\n\n entry.foreignStreak = 0\n entry.failures += 1\n // A port that answers SOMETHING is not provably dead \u2014 so a UI drops `unverified` only once its refusals\n // have earned a real `down`, and never on the way there.\n if (target.kind === 'ui' && entry.failures >= threshold) entry.unverified = false\n if (entry.failures === threshold) {\n const why = target.kind === 'api' ? '/__health not responding' : \"not answering vite's ping\"\n\n this.renderer.log(`\u26A0\uFE0F ${target.tag} unhealthy (${why})`, 'warn')\n }\n }\n\n /**\n * Declare a row down WITHOUT a probe \u2014 for the two facts the probe loop cannot establish: a restart whose\n * `startOneApp` threw (there is no server left to probe), and a UI whose engine died before it was ever\n * up. Routing those through the soft probe path would leave the row one failure short of the threshold,\n * i.e. GREEN, for something that demonstrably does not exist.\n */\n private markDown(tag: string, kind: ProbeTarget['kind']): void {\n const entry = this.healthEntry(tag, kind)\n\n entry.dead = true\n entry.failures = DevServerRunner.LIVENESS_FAILURE_THRESHOLD\n entry.foreignStreak = 0\n entry.unverified = false\n }\n\n /** The 5-arm row state for a tag. No entry at all \u2192 `unknown` (a `UiRef`, or `--no-ui-health`). */\n private healthOf(tag: string): HealthState {\n const entry = this.health.get(tag)\n\n if (entry == null) return 'unknown'\n if (entry.unverified) return 'unverified'\n if (!entry.everUp) return this.neverUpState(entry)\n\n return entry.failures >= DevServerRunner.LIVENESS_FAILURE_THRESHOLD ? 'down' : 'ok'\n }\n\n /**\n * A row that has never proved it was serving. A backend is `down` on sight \u2014 it is started in-process and\n * probed the moment it binds, so \"up but never answered\" is already a failure. A UI is `starting` until it\n * has burned the whole never-up budget: vite is spawned AFTER the ready frame and takes real seconds to\n * bind, and a red dot over a UI that is merely still booting is the one false red this design refuses.\n *\n * `dead` is read HERE and nowhere else, and the narrowness is the point: an `everUp` UI's death must stay\n * probe-established. Turbo puts each task in its OWN process group and reaps none of them on death\n * (`managed-child.ts`), and our own reap is best-effort \u2014 so \"the engine exited \u21D2 every vite is dead\" is\n * simply false, and believing it would red-dot a live, hot-reloading UI.\n */\n private neverUpState(entry: HealthEntry): HealthState {\n if (entry.kind === 'api' || entry.dead) return 'down'\n\n return entry.failures >= DevServerRunner.NEVER_UP_DOWN_THRESHOLD ? 'down' : 'starting'\n }\n\n /**\n * Start the always-on backend liveness monitor: a background probe loop that catches a backend which\n * goes unhealthy WITHOUT crashing the process (a blocked event loop, a wedged dependency, a fastify that\n * stopped serving) \u2014 invisible to both the one-shot {@link printReady} probe and the crash barrier, which\n * only sees thrown faults. Edge-triggered and flap-resistant; see {@link livenessTick}.\n */\n private startLivenessMonitor(): void {\n this.scheduleLivenessTick()\n }\n\n /**\n * Schedule the next liveness tick. A self-rescheduling `setTimeout` (never `setInterval`): a slow tick \u2014\n * every backend timing out its probe \u2014 can never pile up on the next one, and teardown clears exactly one\n * timer. `unref()` so the monitor never keeps the process alive on its own; the fastify servers do that,\n * and lifecycle is owned by {@link shutdown}.\n */\n private scheduleLivenessTick(): void {\n const intervalMs = this.options.livenessIntervalMs ?? DevServerRunner.LIVENESS_INTERVAL_MS\n\n this.livenessTimer = setTimeout(() => {\n // `finally` always reschedules; the trailing `catch` swallows a rejecting probe seam so one bad tick\n // is skipped rather than crashing the loop, and leaves no floating promise.\n this.livenessTick()\n .finally(() => {\n if (!this.shuttingDown) this.scheduleLivenessTick()\n })\n .catch(() => {})\n }, intervalMs)\n this.livenessTimer.unref()\n }\n\n /**\n * One liveness sweep over every running backend AND every managed frontend. Edge-triggered per ROW,\n * keyed by tag (so the counter survives an ephemeral-port rebind on restart) with the CURRENT port read\n * fresh each tick. The verdict rules live in {@link recordProbe}; the threshold is what makes a normal\n * watch-restart (server down <1s, a single interval) invisible \u2014 only a genuinely wedged app stays down\n * across two sweeps.\n *\n * `/__health` probes are already filtered out of the live request tail ({@link startOneApp}'s\n * `onRequestLog`), and vite's ping is not a line vite logs, so this never spams. Bails once teardown has\n * latched so a closing server is not misread as down (the timer is also cleared in {@link shutdown}\n * before the servers close).\n */\n /**\n * Repaint the status panel with fresh live fields (health, uptime, req/min, restarts, errors).\n *\n * This is the first caller `DevUi.refresh()` has ever had: it was declared, implemented twice, and\n * invoked from nowhere, so the \"live\" footer has always painted boot-time values that never changed.\n * That was survivable while a log tail scrolled beside it. It is not survivable now \u2014 with nothing\n * else on screen, a panel that never moves cannot be told apart from a hung process.\n */\n private refreshStatus(): void {\n const summary = this.lastSummary\n\n if (summary == null) return\n\n const now = Date.now()\n const byTag = new Map<string, IAppServer>(\n this.appServers.map((server) => {\n return [`${server.app.name}/api`, server]\n }),\n )\n\n // Prune FIRST, unconditionally, and never inside the argument to `refresh?.()`: an optional call\n // does not evaluate its argument at all when the method is absent, and `DevRenderer` \u2014 the renderer\n // on every `--json` / MCP / piped run \u2014 has no `refresh`. Pruning in there meant the window was never\n // trimmed off the TTY path, so `reqTimes` grew without bound while `onRequestLog` copied the whole\n // array on every request. Unbounded memory and O(n\u00B2) CPU, on the long-lived MCP path specifically.\n this.pruneRequestWindow(now)\n\n this.renderer.refresh?.({\n ...summary,\n sessionUptimeMs: now - this.readyAt,\n // Re-derived, never carried over from the boot summary: a `--watch` restart that brings the backend\n // back must take this row down with it (see {@link degradedRows}).\n degraded: this.degradedRows(),\n // A UI with no managed port has no endpoint row \u2014 only a reference line. It still gets its error\n // count, or its breakage would be counted into a file with nothing on screen pointing at it.\n uiRefs: summary.uiRefs.map((ref) => {\n return { ...ref, errors: this.announceFirstError(ref.tag) }\n }),\n endpoints: summary.endpoints.map((endpoint) => {\n const server = byTag.get(endpoint.tag)\n\n return {\n ...endpoint,\n // Every row's health comes from the ONE probe state machine now \u2014 a UI row's included. A row\n // nothing probes (a `UiRef`, or `--no-ui-health`) has no entry and resolves to `unknown`, which\n // renders no dot at all: exactly what it used to hardcode.\n health: this.healthOf(endpoint.tag),\n // A UI row has no backend server, so it has no uptime, no restarts and no request rate \u2014 but it\n // DOES have an error count, which is the whole reason the panel can report a broken frontend.\n uptimeMs: server ? now - server.startedAt : undefined,\n restarts: server?.restarts,\n rpm: (this.reqTimes.get(endpoint.tag) ?? []).length,\n errors: this.announceFirstError(endpoint.tag),\n }\n }),\n })\n }\n\n /**\n * This row's error count, announcing the 0 \u2192 >0 EDGE once with a terminal line.\n *\n * The counter alone is not enough, and the reason is the honest cost of a liveness dot: a frontend that\n * fails to compile still serves and still answers vite's ping, so its dot stays a truthful green while\n * the app is unusable. `\u26A0 N` is then the only thing on screen that disagrees \u2014 and a number quietly\n * ticking up in a panel corner is not a thing anyone notices. The edge log is; it also reaches a non-TTY\n * run, which has no panel at all.\n */\n private announceFirstError(tag: string): number {\n const { errors } = this.sink.statsFor(tag)\n\n if (errors > 0 && !this.firstErrorLogged.has(tag)) {\n this.firstErrorLogged.add(tag)\n this.renderer.log(`\u26A0\uFE0F ${tag} reported its first error \u2192 ${homeShorten(this.sink.pathFor(tag))}`, 'warn')\n }\n\n return errors\n }\n\n /**\n * The degraded routes that are STILL degraded \u2014 i.e. whose backend is not (yet) running.\n *\n * Re-derived from the live `appServers` set rather than cached, because under `--watch` this is the\n * one row on the panel that is supposed to disappear: the whole point of retrying a boot-failed app is\n * that the route it broke goes back to local. A row that outlived its cause would be a permanent\n * warning about a fixed condition, and a warning that is always on is a warning nobody reads.\n *\n * Membership in `appServers` is the right test: {@link startAllApps} pushes only apps that booted, and\n * {@link runRestart} pushes a recovered app in at the moment it does \u2014 which is also the moment its\n * dev-context fragment lands, i.e. exactly when the vite helper flips the route back to `local`.\n */\n private degradedRows(): DegradedRow[] {\n if (this.degradedRoutes.length === 0) return []\n\n // Keyed by PACKAGE, not by app folder: a route degraded because the run never launched its backend\n // has no owning app name at all (`apiApp` is undefined), and the package is the identity the route,\n // the fragment, and the vite helper's local set all agree on.\n const running = new Set(\n this.appServers.map(({ app }) => {\n return app.packageName\n }),\n )\n\n return this.degradedRoutes\n .filter((d) => {\n return !running.has(d.packageName)\n })\n .map((d) => {\n return { route: d.route, tag: `${d.uiApp}/ui`, fallback: d.fallback, target: d.cloudTarget }\n })\n }\n\n /** Drop request timestamps older than the 60s rpm window. Runs on every tick, painting or not. */\n private pruneRequestWindow(now: number): void {\n const cutoff = now - 60_000\n\n for (const [tag, times] of this.reqTimes) {\n this.reqTimes.set(\n tag,\n times.filter((at) => {\n return at > cutoff\n }),\n )\n }\n }\n\n private async livenessTick(): Promise<void> {\n if (this.shuttingDown) return\n\n await Promise.all(\n [...this.apiTargets(), ...this.uiTargets()].map((target) => {\n return this.probeOne(target)\n }),\n )\n\n // The panel's heartbeat. It rides the probe tick that already exists rather than adding a timer of\n // its own, so the numbers on screen are exactly as fresh as the health behind them.\n this.refreshStatus()\n }\n\n /** Probe one target and fold the outcome in. Split out of {@link livenessTick} to keep both simple. */\n private async probeOne(target: ProbeTarget): Promise<void> {\n const outcome = await this.healthProbe(target)\n\n // Re-check AFTER the await: shutdown() may have latched and begun closing servers while this probe was\n // in flight, resolving it `refused` against a closing socket. Without this a tick that passed the\n // top-of-method bail could log a false `unhealthy` during teardown.\n if (this.shuttingDown) return\n\n this.recordProbe(target, outcome)\n }\n\n /**\n * Collapse the boot spinner into the calm ready header: one endpoint row per running backend\n * (pre-probed health dot + a resolving URL), one reference line per UI app (vite prints its own\n * URL in the stream below), the watch line, the clickable log path, and a separator rule.\n * Handles a UI-only session (no backend rows) so it never leaves a blank screen. `ready()` itself\n * is synchronous \u2014 health is probed here and passed in resolved.\n */\n private async printReady(\n apps: IApiAppConfig[],\n uiApps: DiscoveredUiApp[],\n bootStart: number,\n target: string,\n ): Promise<void> {\n // Snapshot BE readiness BEFORE probing \u2014 a `\u25CF down` server's probe timeout must not inflate\n // `ready in Xs` (nor is the boot time itself the probe latency).\n const elapsedMs = Date.now() - bootStart\n\n // The boot probe goes through the SAME state machine as every tick \u2014 it is not a separate verdict that\n // the panel then forgets. It used to be: `printReady` probed, painted the dot from the local result,\n // and left the failure counter empty \u2014 so the `refreshStatus()` at the bottom of this method, reading\n // that empty map, painted a backend probed DOWN one line ago a confident green, and kept it green until\n // the first tick. The boot frame and the panel under it disagreed about a probe taken once.\n await Promise.all(\n this.apiTargets().map((target) => {\n return this.probeOne(target)\n }),\n )\n\n const endpoints: EndpointRow[] = this.appServers.map(({ app, alias }) => {\n return {\n tag: `${app.name}/api`,\n url: resolveEndpointUrl({ prefixUrl: app.prefixUrl, alias }),\n health: this.healthOf(`${app.name}/api`),\n }\n })\n\n // Pre-assign each UI a free port + its portless alias and stash the map for startUiDev's env. Owning\n // the port is what makes the UI's URL knowable before vite prints it, so every UI gets a real endpoint\n // row. Only a UI whose port could not be assigned falls back to a reference line (\"vite prints its URL\n // below\") \u2014 and, having no port we own, no health dot either.\n this.uiPortMap = {}\n const uiEndpoints: EndpointRow[] = []\n const uiRefs: UiRef[] = []\n\n for (const ui of uiApps) {\n const assigned = await this.assignUiPort(ui)\n const tag = `${ui.name}/ui`\n\n if (assigned != null) {\n uiEndpoints.push({\n tag,\n url: resolveEndpointUrl({ prefixUrl: '', alias: assigned.alias }),\n health: this.seedUiHealth(tag),\n })\n } else {\n uiRefs.push({ tag })\n }\n }\n const watch = this.options.watch ?? false\n const appCount = apps.length + uiApps.length\n const pkgCount = getPackageDistDirs(this.monorepoRoot).length\n\n this.lastSummary = {\n target,\n watch,\n release: readAppRelease(process.cwd()),\n elapsedMs,\n endpoints: [...endpoints, ...uiEndpoints],\n uiRefs,\n failed: this.failedApps.map(({ app, reason }) => {\n return { tag: `${app.name}/api`, reason }\n }),\n degraded: this.degradedRows(),\n watchSummary: `${appCount} app${appCount === 1 ? '' : 's'} \u00B7 ${pkgCount} package${pkgCount === 1 ? '' : 's'}`,\n // The DIRECTORY, not a file: there is one log per service now, so a single path would have to\n // pick a favourite. `tail -f <dir>/<service>.log` is the workflow.\n logPath: homeShorten(this.sink.dir),\n logHref: this.sink.dir,\n }\n\n // Paint the boot frame from the same summary the panel will keep repainting, so the header and the\n // live rows can never disagree about what is running.\n this.readyAt = Date.now()\n this.renderer.ready(this.lastSummary)\n this.refreshStatus()\n }\n\n /**\n * Pre-assign a UI a free port, alias it, and record the port in {@link uiPortMap} (handed to the vite\n * child via `INFRA_KIT_UI_PORTS`, which it binds with `strictPort`). Owning the port ahead of vite's own\n * announcement is what makes the URL knowable in time to print it.\n *\n * `null` degrades this UI to a reference line: either its vite config does not wire `infraKitDev()`\n * ({@link DiscoveredUiApp.managedPort}), so it would ignore the assignment and bind its own port \u2014\n * printing the assigned one would be a lie and aliasing it would 502 \u2014 or the free-port probe failed\n * (extremely rare). An alias that portless REFUSES is not degraded here: {@link registerAppAlias} throws,\n * because a UI advertised at a hostname nothing serves is worse than a UI with no row.\n */\n private async assignUiPort(ui: DiscoveredUiApp): Promise<{ port: number; alias: string } | null> {\n if (!ui.managedPort) return null\n\n let port: number\n\n try {\n port = await getFreePort()\n } catch {\n return null\n }\n const alias = await this.registerAppAlias(ui.packageName, ui.path, port)\n\n this.uiPortMap[ui.packageName] = { port, alias }\n // `uiPortMap` is keyed by PACKAGE because that is the key the vite child reads it back by\n // (`INFRA_KIT_UI_PORTS`) \u2014 the rows are keyed by app. Record the mapping rather than widening the\n // fragment: it is a published wire contract with a separately-versioned helper.\n this.uiTagByPackage.set(ui.packageName, `${ui.name}/ui`)\n\n return { port, alias }\n }\n\n /**\n * Seed a managed UI's row so it reads `\u25CC starting` from the boot frame onward.\n *\n * Vite is spawned AFTER `printReady` \u2014 the first probe is a whole tick away \u2014 so an unseeded row would\n * carry no dot at all until then, and a row that is `\u25CF down` at boot would be a lie about a server that\n * has not been asked a single question yet. `unknown` when UI health is off: no entry, no dot, ever.\n */\n private seedUiHealth(tag: string): HealthState {\n if (!this.uiHealthEnabled()) return 'unknown'\n\n this.healthEntry(tag, 'ui')\n\n return this.healthOf(tag)\n }\n\n /**\n * Dump each running app's registered `METHOD /path` routes (opt-in via `--routes`) so the\n * emulator is self-describing. Prints to the terminal (not verbose-gated) \u2014 invoking it means\n * the user explicitly asked for the routes. Reads the live set via `getRegisteredRoutes`.\n */\n private printRouteDump(): void {\n if (this.appServers.length === 0) return\n\n this.renderer.log('\uD83D\uDDFA\uFE0F Registered routes:')\n for (const { app, server } of this.appServers) {\n const routes = server.getRegisteredRoutes()\n\n this.renderer.log(` ${app.name} (${routes.length}): ${routes.length > 0 ? routes.join(', ') : '(none)'}`)\n }\n }\n\n /**\n * Stop watching, cancel any pending debounced restart, and close all running servers.\n * Does not exit the process \u2014 the entry point owns exit.\n */\n /**\n * Report a process-level fault \u2014 an `uncaughtException` / `unhandledRejection` the crash barrier caught\n * and deliberately survived.\n *\n * This exists because the interceptor owns `process.stderr` for the life of a TTY session. The crash\n * barrier's own reporter is a plain `process.stderr.write`, so after `ready()` a crash would be FILED\n * into a log and never printed \u2014 leaving a panel that still says `\u25CF ok` and `\u26A0 0` over a session that\n * has just faulted. Silence is the one thing a fault may never produce, and the panel is now the only\n * signal there is.\n *\n * So a fault takes both channels, deliberately: it is filed at `error` level (which turns the row's\n * counter red) AND punched onto the terminal through the panel's bypass, which steps over the very\n * patch that would otherwise swallow it. Attributed to the app whose async context faulted, when\n * there is one.\n */\n public reportFault(detail: string): void {\n // File it against the app whose async context faulted \u2014 that is what turns its row's counter red.\n this.sink.write(currentService() ?? RUNNER_SERVICE, detail, { level: 'error' })\n\n // Print it through the UI, NOT through the bypass. `rawStdoutWrite` would push N raw lines onto a\n // terminal whose live region Ink believes it owns: Ink erases by counting rows back from where it\n // thinks the cursor is, so the very next repaint (\u22645s away, on the liveness tick) would erase the\n // tail of this stack and leave a ghost of the old panel above it. The method exists to make a fault\n // impossible to miss; writing it somewhere the next frame deletes it is worse than not writing it.\n //\n // `renderer.log` commits through Ink's `<Static>` region, which survives every repaint \u2014 and falls\n // through to a plain stdout write on the non-TTY renderer, where there is no region to respect.\n //\n // `tee: false` because the line above ALREADY filed it. Left teeing, the renderer files a second copy\n // through `appendRunnerLog` \u2014 every fault landing twice in `runner.log`, a literal 2\u00D7 on the 185 GB the\n // storm wrote. The direct `sink.write` is the copy that must survive: it is the one that carries the\n // `error` level, and the level is what turns the panel row red.\n this.renderer.log(detail, 'error', { tee: false })\n }\n\n /**\n * File a fault into the log WITHOUT touching the terminal.\n *\n * The channel of last resort: when stdio is unwritable, printing is what produces the fault, so the sink\n * is the only surface a post-mortem can still read. Used by the entry's fatal path.\n */\n public fileFault(detail: string): void {\n this.sink.write(currentService() ?? RUNNER_SERVICE, detail, { level: 'error' })\n }\n\n /**\n * The teardown step currently in flight (`'idle'` before {@link shutdown}, `'done'` after it completes).\n *\n * Read by the entry's `describeStall` seam when the teardown deadline trips, so the force-quit line names\n * WHICH step wedged rather than shrugging. Without it the deadline is just a force-quit, and the question\n * the incident actually poses \u2014 why five processes that demonstrably ran `shutdown()` never exited \u2014 stays\n * open after shipping the thing meant to answer it.\n */\n public get shutdownStage(): string {\n return this.stage\n }\n\n /**\n * Stop watching, cancel any pending debounced restart, and close all running servers. Idempotent: every\n * caller after the first gets the SAME in-flight promise, never a second teardown.\n *\n * The `.catch` is attached HERE, at assignment, in the same tick \u2014 not by the caller. `doShutdown` can\n * reject (`watcher.close()`, `turboWatch.kill()`, `uiDev.kill()` are unguarded), and the fatal path calls\n * this fire-and-forget while a deadline races it. A rejection with no handler attached in the assigning\n * tick fires `unhandledRejection` \u2192 the crash barrier \u2192 and, since stdio is dead on that path, straight\n * back into the fatal handler: the exact loop this whole change exists to remove, re-created inside its\n * own fix. Callers that DO await still see the rejection \u2014 this handler only disarms the process-level\n * channel.\n */\n public shutdown(): Promise<void> {\n if (this.teardown != null) return this.teardown\n\n this.teardown = this.doShutdown()\n this.teardown.catch(() => {})\n\n return this.teardown\n }\n\n private async doShutdown(): Promise<void> {\n // Latch BEFORE anything else. Everything below assumes no new alias can be registered once\n // teardown begins; `scheduleDebounced` and `restart` read this flag to honour that.\n this.shuttingDown = true\n this.stage = 'starting'\n\n // Release the terminal FIRST: if the Ink boot UI is still mounted (e.g. SIGINT mid-boot), unmount it\n // before any plain write below, so the shutdown lines never clobber a live region. No-op for the\n // plain renderer and idempotent when Ink already unmounted at ready().\n this.renderer.dispose()\n\n // Hand `console` and the raw streams back HERE \u2014 before the first shutdown line, not after the last.\n // `dispose()` drops the panel, so `renderer.log` below falls through to a plain `process.stdout`\n // write; while the interceptor still owned it and was suppressing, that line went to a log file. The\n // user pressed Ctrl-C and then watched a dead terminal for the seconds teardown takes (killing the\n // turbo tree escalates SIGTERM\u2192SIGKILL per child) \u2014 which is exactly how a second Ctrl-C gets\n // pressed, taking the force-quit path and orphaning the children.\n this.intercept?.uninstall()\n\n this.renderer.log('\uD83D\uDED1 Shutting down all servers...')\n\n // Silence every restart SOURCE before deregistering aliases below. Ordered first because alias\n // removal is not idempotent against a concurrent `startOneApp`: a chokidar event or an armed\n // debounce timer firing after the removal would re-register an alias into a set nothing drains\n // again, stranding it exactly like the force-quit case the removal is there to prevent.\n for (const timer of this.watchDebounceTimers.values()) {\n clearTimeout(timer)\n }\n this.watchDebounceTimers.clear()\n\n // Stop the liveness monitor BEFORE the servers close, so a final tick can't probe a closing backend and\n // log a false `unhealthy`. The `shuttingDown` latch above also makes an in-flight tick bail and never\n // reschedule; clearing the pending timer here closes the window between ticks.\n if (this.livenessTimer) {\n clearTimeout(this.livenessTimer)\n this.livenessTimer = null\n }\n\n if (this.watcher) {\n this.stage = 'watcher.close'\n await this.watcher.close()\n this.watcher = null\n }\n\n // Drain a restart already in flight. The latch makes every QUEUED job a no-op, but a job that\n // began before the latch is mid `close() \u2192 listen() \u2192 registerAlias()` and must finish, or its\n // alias lands after the removal below. Never rejects (the chain self-catches).\n this.stage = 'restartWorkChain'\n await this.restartWorkChain\n\n // And the background closure build, so its `turbo --dry` child never outlives the runner.\n this.stage = 'closureBuild'\n await this.closureBuild\n\n // Layer B: deregister every portless alias BEFORE the child reap below. The reap can take\n // seconds (grace + SIGKILL escalation, per child), and a force-quit during it would otherwise\n // strand an alias pointing at a dead backend \u2014 the 502 you hit on the next start.\n //\n // Concurrent and best-effort: `removeAlias` never throws (the driver swallows failures) and each\n // call is self-bounded by the driver's own subprocess timeout, so this caps the pre-reap delay at\n // roughly one timeout regardless of alias count, and `Promise.all` cannot reject. That bound holds\n // only because `availability` is already warm here: an alias can exist only after `registerAlias`,\n // which awaits `isAvailable()`, and `ensureProxy` awaits it during `start()`. Register an alias\n // without a prior availability check and this silently becomes two subprocesses per alias.\n this.stage = 'removeAlias'\n await Promise.all(\n [...this.registeredAliases].map((name) => {\n return this.proxy.removeAlias(name)\n }),\n )\n this.registeredAliases.clear()\n\n // Reap the long-lived engines (group SIGTERM\u2192SIGKILL) so neither writes fresh dist nor\n // holds a port mid-teardown. Reaped here (not only in the entry signal handler) because tests\n // and any non-signal caller invoke shutdown() directly. Awaited so the SIGKILL escalation\n // completes before the entry point's `process.exit`.\n if (this.turboWatch) {\n this.stage = 'turboWatch.kill'\n await this.turboWatch.kill()\n this.turboWatch = null\n }\n\n if (this.uiDev) {\n this.stage = 'uiDev.kill'\n await this.uiDev.kill()\n this.uiDev = null\n }\n\n for (const { app, server } of this.appServers) {\n this.stage = `server.close(${app.name})`\n try {\n await server.close()\n } catch {\n // ignore\n }\n // Remove this runner's own dev-context fragment so a stopped app drops out of the\n // helper's localSet (only its own \u2014 the directory model keeps runners independent).\n this.removeDevContextFragment(app)\n }\n\n // Final terminal-visible confirmation, so Ctrl-C never ends on a bare cursor. `log` (not the\n // verbose-only `narrate`) so the last line the user sees is always infra-kit's.\n //\n // Our own detached `turbo run dev` child writes its teardown to the log, not the TTY. Any pnpm\n // `ELIFECYCLE` still visible after Ctrl-C therefore comes from the `pnpm run` wrapper processes\n // ABOVE us in the shell's foreground process group (nested consumer scripts), which we cannot\n // redirect: the terminal signals the whole group, pnpm dies at once, and the shell redraws its\n // prompt while this teardown is still running. Fixing that means `exec`ing into the binary from\n // the consumer's dev script so no wrapper survives to report a failed child.\n // Hand `console` and the raw streams back BEFORE the final line prints, so the goodbye actually\n // reaches the terminal instead of being filed into a log the user is no longer watching.\n this.intercept?.uninstall()\n\n this.renderer.log(`\u2713 dev stopped \u00B7 logs \u2192 ${homeShorten(this.sink.dir)}`)\n\n // Strictly last: every line above still has to reach a file. Closing holds no buffered data (the\n // sink writes through a held fd), so this only releases the fds.\n this.sink.close()\n this.stage = 'done'\n }\n}\n\n/**\n * Select the terminal UI for this run: the persistent Ink UI on an interactive TTY (dynamically imported\n * so React never loads on the non-TTY / `--json` / MCP chunks), else the plain {@link DevRenderer}\n * (returned as `undefined` so the runner constructs its own default). `--json`/MCP always forces plain \u2014\n * Ink must never seize a machine-readable stream.\n *\n * {@link PersistentInkDevUi} covers both shapes of session, branching at {@link DevUi.ready} on whether a\n * UI child owns the TTY, so there is nothing left to gate on here.\n */\n/**\n * Whether this run owns the terminal \u2014 the single gate for BOTH the live UI and the output interception.\n *\n * Derived once and shared, never re-derived: a `--json` / MCP / piped run must keep a byte-clean stdout,\n * and interception there would file the machine-readable stream into a log and hand the caller nothing.\n */\nexport const ownsTerminal = (options: DevServerOptions): boolean => {\n return (options.tty ?? Boolean(process.stdout.isTTY)) && !options.json\n}\n\nconst selectDevUi = async (options: DevServerOptions): Promise<DevUi | undefined> => {\n if (!ownsTerminal(options)) {\n return undefined\n }\n\n const { PersistentInkDevUi } = await import('src/tui/dev-ui/persistent-ink-dev-ui')\n const { createSafeStream } = await import('src/tui/safe-stderr')\n\n return new PersistentInkDevUi({\n appendLog: appendRunnerLog,\n verbose: options.verbose ?? false,\n // Composition order is load-bearing. The BYPASS proxy is inside (its `write` reaches the real\n // terminal, stepping over the interceptor's patch); the SCRUB proxy is outside (it strips the\n // `ESC[3J` that an overflowing Ink frame emits, which would wipe the user's scrollback).\n //\n // Inverting them breaks silently: `createSafeStream` resolves `target.write` at CALL time, so\n // wrapping the raw `process.stdout` would route every frame through the patch and into a log file \u2014\n // a blank screen with no error anywhere.\n stdout: createSafeStream(panelStream()),\n })\n}\n\n/**\n * Construct a {@link DevServerRunner}, start it, and return the instance so the caller\n * (the CLI entry point) can wire signal handlers to `shutdown()` and own process exit.\n * Selects the boot UI (Ink on a TTY, plain otherwise) before constructing the runner.\n */\nexport async function run(options: DevServerOptions = {}): Promise<DevServerRunner> {\n const renderer = await selectDevUi(options)\n // Positions: options, runBuild, turboWatchFactory, uiDevFactory, dryRunner, renderer \u2014 pass `undefined`\n // for the injectable seams so their ctor defaults apply; only the renderer is chosen here.\n const runner = new DevServerRunner(options, undefined, undefined, undefined, undefined, renderer)\n\n // Unwind a partial boot. By the time `start()` can reject (e.g. a UI alias portless refuses), the\n // backends are already listening, aliased in the portless daemon, and recorded as dev-context\n // fragments on disk. The caller wires `shutdown()` to signals only AFTER this resolves, so without\n // this the process exits leaving external daemon state and on-disk fragments behind \u2014 the next\n // `vite dev` then proxies at an alias nothing serves. Teardown failures must not mask the original\n // boot error, so they are swallowed.\n try {\n await runner.start()\n } catch (error) {\n await runner.shutdown().catch(() => {})\n throw error\n }\n\n return runner\n}\n", "/**\n * Dependency-closure map for scoped `infra-kit dev --watch` restarts (plan Phase 1, Option B).\n *\n * `pnpm` is resolved from PATH \u2014 the same trust posture as the rest of the dev-server, which\n * already shells out to `pnpm exec turbo \u2026`; the args are fixed literals plus discovered package\n * names, never shell-interpolated.\n *\n * Rather than re-derive the workspace graph in-process, infra-kit asks turbo what it would\n * rebuild: `turbo run build --dry=json --filter=...<pkg>` lists every package in an app's build\n * closure (`tasks[].package`). That set is authoritative \u2014 it is exactly what the running\n * `turbo watch build` engine rebuilds \u2014 so there is no drift and no glob-parser/BFS/cycle-guard\n * to maintain here. The runner inverts these per-app closures into `dependentsByPackageDir`\n * (which app folders depend on each `packages/<x>/dist`) and, on a package-dist change, restarts\n * only the dependent backends instead of every one.\n */\nimport { execFile } from 'node:child_process'\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport { promisify } from 'node:util'\n\nimport { getPackageDistDirs } from './discovery.js'\n\nconst execFileAsync = promisify(execFile)\n\n/** The subset of an app's identity the closure map needs. */\nexport interface ClosureApp {\n /** App folder name (e.g. `client`) \u2014 the restart key in the dependents map. */\n name: string\n /** package.json `name` (e.g. `sls-trvl-client`) \u2014 the turbo `--filter` target. */\n packageName: string\n}\n\n/**\n * Resolve one app's build closure: the set of workspace package **names** turbo would rebuild\n * for it. Injectable so tests drive `buildClosureMap` without spawning a real turbo. A rejection\n * propagates through `Promise.all` so the caller can apply its restart-all fail-safe.\n */\nexport type DryRunner = (packageName: string) => Promise<string[]>\n\n/** The inverted closure: `packages/<x>/dist` \u2192 the app folders that depend on it, plus the dir\u2192name bridge. */\nexport interface ClosureMap {\n /** `packages/<x>/dist` absolute dir \u2192 set of app folder names whose closure includes it. */\n dependentsByPackageDir: Map<string, Set<string>>\n /** `packages/<x>/dist` absolute dir \u2192 that package's package.json `name` (dir \u2260 name for scoped pkgs). */\n packageNameByDir: Map<string, string>\n}\n\n/** Read `<dir>/package.json` `name`, or `undefined` when absent/unreadable (never throws). */\nconst readPackageName = (dir: string): string | undefined => {\n try {\n const parsed = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8')) as { name?: unknown }\n\n return typeof parsed.name === 'string' ? parsed.name : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * Default {@link DryRunner}: `turbo run build --dry=json --filter=...<pkg>` in `root`, returning the\n * unique `tasks[].package` set (the packages turbo would rebuild for `<pkg>`). `...` includes the\n * app's own dependency closure, mirroring the `turbo watch build` filter.\n */\nexport const defaultDryRunner = (root: string): DryRunner => {\n return async (packageName: string): Promise<string[]> => {\n const { stdout } = await execFileAsync(\n 'pnpm',\n ['exec', 'turbo', 'run', 'build', '--dry=json', `--filter=...${packageName}`],\n { cwd: root, maxBuffer: 32 * 1024 * 1024 },\n )\n const parsed = JSON.parse(stdout) as { tasks?: Array<{ package?: unknown }> }\n const names = (parsed.tasks ?? [])\n .map((t) => {\n return t.package\n })\n .filter((p): p is string => {\n return typeof p === 'string'\n })\n\n return [...new Set(names)]\n }\n}\n\n/** Build `packageNameByDir` (and its reverse) for every existing `packages/<x>/dist` under `root`. */\nconst readPackageDirs = (root: string): { packageNameByDir: Map<string, string>; dirByName: Map<string, string> } => {\n const packageNameByDir = new Map<string, string>()\n const dirByName = new Map<string, string>()\n\n for (const distDir of getPackageDistDirs(root)) {\n const name = readPackageName(path.dirname(distDir))\n\n if (name !== undefined) {\n packageNameByDir.set(distDir, name)\n dirByName.set(name, distDir)\n }\n }\n\n return { packageNameByDir, dirByName }\n}\n\n/**\n * Ask turbo for each app's build closure (in parallel) and invert it into {@link ClosureMap}:\n * `packages/<x>/dist` \u2192 the app folders whose closure includes that package. Apps' own packages\n * (not under `packages/`) simply don't appear in the map, so they never scope a package restart.\n *\n * A {@link DryRunner} rejection propagates (via `Promise.all`) so the caller falls back to\n * restart-all \u2014 the map is never silently partial.\n *\n * @example\n * // appX deps pkgA+pkgC; appY deps pkgC only \u2192\n * // dependentsByPackageDir: { '\u2026/pkgA/dist': {appX}, '\u2026/pkgC/dist': {appX, appY} }\n */\nexport const buildClosureMap = async (\n root: string,\n apps: ClosureApp[],\n dryRunner: DryRunner = defaultDryRunner(root),\n): Promise<ClosureMap> => {\n const { packageNameByDir, dirByName } = readPackageDirs(root)\n const dependentsByPackageDir = new Map<string, Set<string>>()\n\n const closures = await Promise.all(\n apps.map(async (app) => {\n return { app, closure: await dryRunner(app.packageName) }\n }),\n )\n\n for (const { app, closure } of closures) {\n for (const pkgName of closure) {\n const distDir = dirByName.get(pkgName)\n\n if (distDir === undefined) continue\n\n const dependents = dependentsByPackageDir.get(distDir) ?? new Set<string>()\n\n dependents.add(app.name)\n dependentsByPackageDir.set(distDir, dependents)\n }\n }\n\n return { dependentsByPackageDir, packageNameByDir }\n}\n\n/** Debounce key for a package-dist change \u2014 distinct per package dir so unrelated packages never collapse into one bucket. */\nexport const packageDebounceKey = (packageDir: string): string => {\n return `__pkg__:${packageDir}`\n}\n\n/**\n * Decide which launched apps a `packages/<pkg>/dist` change should restart. `null` means\n * \"fail-safe: restart all\" \u2014 the closure map is missing (a `--dry` failure) or the change carried\n * no package identity. Otherwise the participating (`watchDeps`) apps whose closure includes that\n * package dir \u2014 possibly an empty array (the package is UI-only, or every dependent opted out),\n * which the caller treats as \"restart nothing\".\n *\n * @example\n * // closure: pkgA \u2192 {appX}; pkgC \u2192 {appX, appY}\n * selectPackageRestartTargets([appX, appY], map, '\u2026/pkgA/dist') // => [appX] (not appY)\n * selectPackageRestartTargets([appX, appY], map, '\u2026/uiOnly/dist') // => [] (restart nothing)\n * selectPackageRestartTargets([appX, appY], null, '\u2026/pkgA/dist') // => null (fail-safe: restart all)\n */\nexport const selectPackageRestartTargets = <T extends { name: string; watchDeps: boolean }>(\n apps: T[],\n closureMap: ClosureMap | null,\n packageDir: string | undefined,\n): T[] | null => {\n if (closureMap === null || packageDir === undefined) return null\n\n const dependents = closureMap.dependentsByPackageDir.get(packageDir) ?? new Set<string>()\n\n return apps.filter((a) => {\n return a.watchDeps && dependents.has(a.name)\n })\n}\n", "/**\n * Detect a BROKEN LOCAL PAIRING: a frontend route this run meant to serve from a local backend, whose\n * backend is not actually up.\n *\n * A backend that is not running writes no `.infra-kit/dev-context/<app>.json` fragment, so the vite\n * helper's `pickSource` finds nothing in its local set and falls through to the route's FALLBACK. For the\n * common route (`from: ['local','cloud'], default: 'cloud'`) that fallback is the shared CLOUD backend \u2014\n * and the frontend comes up looking perfectly healthy while every request on that route leaves your\n * machine. Nothing in the resolved proxy map records that the route was ever meant to be local. You can be\n * POSTing at the cloud dev database believing you are pointed at localhost.\n *\n * The rule is INTENT vs REALITY, deliberately not \"did a backend crash\":\n * degraded \u27FA the route can be local \u2227 this run intended that package to be local \u2227 it is not up\n *\n * Keying off crashes alone (the obvious formulation) misses two reachable paths that produce the exact\n * same silent cloud proxy:\n * - `--app` / `--self` narrows the launch set AFTER preset resolution, so a preset's backend can be\n * dropped and never even attempted \u2014 no crash, nothing to key off;\n * - a preset can pin a route `local` while launching no backend for it at all. (`validatePresetProxy`\n * catches THAT statically, but it runs only under `infra-kit audit` and the bare-invocation wizard \u2014\n * never on the `infra-kit dev <preset>` path, which is the one people actually type.)\n * \"Intended local, isn't up\" covers all three with one predicate, crash included.\n *\n * Side-effect free: every input is passed in (nothing is read from disk here), so the rule is fully\n * unit-testable.\n */\n\n/** The two places a frontend route can be served from \u2014 mirrors `InfraKitDevProxySource`. */\nexport type PairingSource = 'local' | 'cloud'\n\n/**\n * One route from a frontend's `dev.proxy.routes`, as the pairing check reads it. Deliberately shaped like\n * the real config route so the caller hands its loaded config straight in with no translation.\n */\nexport interface PairingRoute {\n /** The backend package this route proxies to. */\n packageName: string\n /** The sources this route can be served from. Only a route that lists `local` can be degraded. */\n from: readonly PairingSource[]\n /**\n * The declared fallback. MUST be carried, and the reason is subtle: the helper resolves an unserved\n * route to `route.default ?? route.from[0]` (see `pickSource`). So a `from: ['local']` route with no\n * `default` \u2014 which the schema allows, since `default` is only required for a multi-source route \u2014\n * falls back to **`local`**, at an alias nothing registered. That is a loud 502, NOT a silent cloud\n * proxy, and a message that says \"would proxy to cloud\" would be naming a destination the traffic\n * never reaches. Still degraded, still worth refusing; just a different failure to describe honestly.\n */\n default?: PairingSource\n /** The preset pinned this route `local` via a `devServersPresets` proxy override \u2014 an explicit intent. */\n pinnedLocal?: boolean\n}\n\n/** A frontend this run launched, plus the `dev.proxy` block it declared. */\nexport interface LaunchedUi {\n /** App folder name (e.g. `client`). */\n app: string\n /** Route path \u2192 its declared backend, capable sources, and fallback. */\n routes: Record<string, PairingRoute>\n /**\n * The frontend's `dev.proxy.templates.cloud`, so a cloud-falling route can name the origin it is about\n * to use. Omitted \u2192 the finding carries no `cloudTarget`.\n */\n cloudTemplate?: string\n}\n\n/** Everything the rule needs to decide intent vs reality. */\nexport interface PairingInputs {\n /** The frontends this run actually launched. */\n uis: readonly LaunchedUi[]\n /**\n * Backend packages this run INTENDED to serve locally: every `<app>/api` the preset names, taken BEFORE\n * `--app`/`--self` narrowing \u2014 narrowing changes what runs, never what the preset promised.\n */\n wanted: ReadonlySet<string>\n /** Backend packages that are actually up (i.e. have written a dev-context fragment). */\n running: ReadonlySet<string>\n /** package \u2192 its start-failure reason. Absent for a package that was never attempted at all. */\n reasons: ReadonlyMap<string, { app: string; reason: string }>\n /** `INFRA_KIT_ENV`, for the `<env>` placeholder in a cloud template. */\n env?: string\n}\n\n/** A route this run meant to serve locally, whose backend is not up. */\nexport interface DegradedRoute {\n /** Frontend app folder the route belongs to. */\n uiApp: string\n /** Route path (e.g. `/api`). */\n route: string\n /** The backend package the route wanted. */\n packageName: string\n /** Where the route ACTUALLY resolves now \u2014 what the helper's `pickSource` will return. */\n fallback: PairingSource\n /** App folder of the backend, when this run attempted it (absent when it was never launched). */\n apiApp?: string\n /** Why it is not up: the start-failure reason, or a statement that the run never launched it. */\n reason: string\n /** The cloud origin the route now resolves to. Set ONLY when `fallback` is `cloud` AND it is knowable. */\n cloudTarget?: string\n}\n\n/** Reason text for a package the run never even attempted to start. */\nconst NOT_LAUNCHED = 'this run never launched it'\n\n/**\n * Fill the `<env>`/`<packageName>` placeholders in a cloud template (`<release>` is local-only), or\n * `undefined` when the template needs an `<env>` and none is sourced.\n *\n * Refusing to interpolate an empty `<env>` matters: `https://<env>.hulyo.co.il` with nothing to put in it\n * renders `https://.hulyo.co.il`, a host that resolves nowhere. Naming a made-up origin in a message whose\n * entire job is to tell the user WHERE their traffic was about to go is worse than naming none \u2014 the\n * caller then says \"the cloud backend\" and stays true.\n */\nconst interpolateCloud = (template: string, packageName: string, env: string | undefined): string | undefined => {\n if (!env && template.includes('<env>')) return undefined\n\n return template.replaceAll('<packageName>', packageName).replaceAll('<env>', env ?? '')\n}\n\n/** Where an unserved route actually lands, per the helper's `pickSource`: `default`, else the sole source. */\nconst resolveFallback = (route: PairingRoute): PairingSource => {\n return route.default ?? route.from[0] ?? 'cloud'\n}\n\n/**\n * Every route across the launched frontends that this run meant to serve locally and cannot.\n *\n * Three non-cases matter and are load-bearing:\n * - a CLOUD-ONLY route (`from: ['cloud']`) is never degraded \u2014 it was always going to cloud, by design\n * (hulyo's `/dynamic` and `/media` are exactly this), and flagging it would cry wolf on every run;\n * - a route whose backend is UP is not degraded, obviously \u2014 that is the happy path;\n * - a route naming a package this run never intended to serve locally (not in `wanted`, not pinned) is\n * not degraded: a frontend developing against cloud on purpose is a supported, common workflow.\n *\n * @example\n * findDegradedRoutes({\n * uis: [{ app: 'client', cloudTemplate: 'https://<env>.hulyo.co.il',\n * routes: { '/api': { packageName: 'backend-api', from: ['local', 'cloud'], default: 'cloud' } } }],\n * wanted: new Set(['backend-api']),\n * running: new Set(),\n * reasons: new Map([['backend-api', { app: 'client', reason: \"config is missing field: 'connectionURL'\" }]]),\n * env: 'dev',\n * })\n * // => [{ uiApp: 'client', route: '/api', packageName: 'backend-api', fallback: 'cloud', apiApp: 'client',\n * // reason: \"config is missing field: 'connectionURL'\", cloudTarget: 'https://dev.hulyo.co.il' }]\n */\nexport const findDegradedRoutes = (input: PairingInputs): DegradedRoute[] => {\n return input.uis.flatMap((ui) => {\n return Object.entries(ui.routes).flatMap(([route, spec]) => {\n const finding = judgeRoute(ui, route, spec, input)\n\n return finding ? [finding] : []\n })\n })\n}\n\n/** The whole rule, for one route: intended local \u2227 not up \u21D2 degraded. Null when the route is fine. */\nconst judgeRoute = (\n ui: LaunchedUi,\n route: string,\n spec: PairingRoute,\n { wanted, running, reasons, env }: PairingInputs,\n): DegradedRoute | null => {\n const { packageName } = spec\n const intendedLocal = wanted.has(packageName) || spec.pinnedLocal === true\n\n if (!spec.from.includes('local') || !intendedLocal || running.has(packageName)) return null\n\n const fallback = resolveFallback(spec)\n const failure = reasons.get(packageName)\n const cloudTarget =\n fallback === 'cloud' && ui.cloudTemplate != null ? interpolateCloud(ui.cloudTemplate, packageName, env) : undefined\n\n return {\n uiApp: ui.app,\n route,\n packageName,\n fallback,\n apiApp: failure?.app,\n reason: failure?.reason ?? NOT_LAUNCHED,\n cloudTarget,\n }\n}\n\n/** Where this route's traffic is really about to go \u2014 the half of the message that must never lie. */\nconst describeDestination = (d: DegradedRoute): string => {\n if (d.fallback === 'local') {\n // `pickSource` returns `local` for an unserved single-source route, so the proxy dials the alias the\n // backend would have registered \u2014 and nothing did. Every request 502s. Loud, but still not what was\n // asked for, and saying \"cloud\" here would name a destination the traffic never reaches.\n return `would proxy ${d.route} at the local alias for \"${d.packageName}\", which nothing is serving \u2014 every request will fail`\n }\n\n const to = d.cloudTarget ?? 'the cloud backend'\n\n return `would proxy ${d.route} to ${to} instead of your local backend`\n}\n\n/** Was this backend actually attempted (and it crashed), or did the run never launch it at all? */\nconst wasAttempted = (d: DegradedRoute): boolean => {\n return d.apiApp != null\n}\n\n/**\n * What to actually DO about one finding. The two cases have genuinely different remedies, and conflating\n * them produces advice that cannot work:\n *\n * A CRASHED backend is retryable \u2014 `resolveRestartTargets` treats a boot-failed app as a restart target,\n * so `--watch` really can bring it back on the next save (and the vite plugin re-resolves the proxy off\n * the fragment it then writes, flipping the route back to `local` on its own).\n *\n * A backend the run NEVER LAUNCHED is not retryable by any amount of saving: `runRestart` only ever sees\n * the post-`--app`/`--self` app list, so a backend a narrowing flag dropped \u2014 or one a preset pinned\n * `local` without ever naming an api target for \u2014 is not in it and never will be. Telling that user to\n * \"run with --watch\" would be sending them to wait on a restart that cannot happen.\n */\nconst remedy = (d: DegradedRoute): string => {\n if (wasAttempted(d)) return `fix ${d.apiApp}/api and re-run (or use --watch to retry it on the next save)`\n\n return (\n `this run never launched \"${d.packageName}\" \u2014 drop the --app/--self narrowing, add its api to the ` +\n `preset, or set \"${d.route}\" to \"cloud\" if you meant to develop against cloud`\n )\n}\n\n/**\n * The refusal message for a run that cannot honour its local pairings. Spelled out rather than summarised:\n * the whole failure mode is that the fallback is invisible, so the refusal names the route, where the\n * traffic would really have gone, why the backend isn't there, and what will actually fix it.\n *\n * `target` is the run's preset/target label, purely so the message can blame the thing the user typed.\n */\nexport const formatPairingRefusal = (degraded: readonly DegradedRoute[], target: string): string => {\n const lines = degraded.map((d) => {\n const owner = wasAttempted(d) ? `${d.apiApp}/api` : `\"${d.packageName}\"`\n\n return (\n ` ${d.uiApp}/ui ${d.route} \u2192 ${d.packageName}: ${d.uiApp}/ui ${describeDestination(d)}.\\n` +\n ` ${owner}: ${d.reason}\\n` +\n ` \u2192 ${remedy(d)}`\n )\n })\n\n return (\n `infra-kit dev: \"${target}\" is supposed to serve a backend locally, but it is not running \u2014 refusing ` +\n `to bring the frontend up silently pointed somewhere else.\\n\\n${lines.join('\\n\\n')}`\n )\n}\n", "/**\n * Which service is \"speaking\" right now, for output that arrives through a channel carrying no tag of\n * its own \u2014 a handler's `console.log`, a Powertools line, a dependency's import-time banner.\n *\n * The backend is IN-PROCESS and multi-app: `DevServerRunner` holds an `appServers` array, so several\n * apps share one node process and one global `console`. Nothing in a raw stdout write says which app\n * wrote it. An `AsyncLocalStorage` carries the tag down the async call chain from the fastify hook that\n * opened the request, so a line emitted anywhere under that request attributes correctly.\n *\n * This is INFERENCE, not declaration \u2014 the honest limit is worth stating: a module imported by two apps\n * is ONE module instance, so a timer it registers at import time keeps whichever app imported it first.\n * Anything with no context at all falls back to a NAMED bucket (`runner.log`), never to a guess.\n */\nimport { AsyncLocalStorage } from 'node:async_hooks'\n\nconst storage = new AsyncLocalStorage<string>()\n\n/** Run `fn` with every line it emits attributed to `service`, however deep the async chain goes. */\nexport const runAttributed = <T>(service: string, fn: () => T): T => {\n return storage.run(service, fn)\n}\n\n/**\n * Enter `service`'s context for the REST of the current async chain, without wrapping a callback.\n *\n * This is the fastify shape: an `onRequest` hook must not wrap the handler (fastify owns that call),\n * but `enterWith` makes the whole remaining hook chain \u2014 handler, `onResponse`, `setErrorHandler` \u2014 a\n * continuation of this context.\n */\nexport const enterAttribution = (service: string): void => {\n storage.enterWith(service)\n}\n\n/** The service owning the current async context, or `undefined` outside any request / import. */\nexport const currentService = (): string | undefined => {\n return storage.getStore()\n}\n", "/**\n * Per-service log files for `infra-kit dev`.\n *\n * Replaces the single shared `<cacheRoot>/<session>/logs.txt`, which was written through TWO handles \u2014\n * a buffered `fs.createWriteStream` (the turbo tee) and a sync `fs.appendFileSync` (everything else) \u2014\n * so line order was never deterministic. Worse, `--cmux` spawns N `infra-kit dev` processes that all\n * INHERIT the same `INFRA_KIT_SESSION`, so N processes appended to one file through 2N handles. The\n * `<pid>` path segment makes that collision structurally impossible; one fd per service kills the rest.\n *\n * Writes are `fs.writeSync` on a long-lived fd, NOT a `WriteStream`. That is deliberate: a stream's\n * pending buffer is lost on `process.exit()` (an exit handler can only run sync code), which would drop\n * the tail of the log \u2014 precisely the lines anyone reads a crash log for. A held fd + `writeSync` is\n * durable by construction, so there is no flush to forget and no second handle to race.\n */\nimport { Buffer } from 'node:buffer'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\n\nimport { INFRA_KIT_SESSION_VAR, getCacheRoot } from 'src/lib/constants'\n\nimport type { LogLevel } from './render.js'\nimport { isTerminalDead } from './terminal-liveness.js'\n\n/**\n * The un-patched `process.stdout.write`, reached through the PROTOTYPE rather than captured off the\n * instance at module load.\n *\n * A module-load capture is only order-immune if nothing patched stdout before this file was evaluated \u2014\n * an assumption no import graph can guarantee. A monkeypatch installs an OWN property on the stream\n * instance, so resolving `write` from the prototype chain steps over it regardless of import order.\n * This is the panel's bypass: it must reach the terminal even while the interceptor owns `process.stdout`.\n *\n * Resolved at CALL time (like {@link rawStderrWrite}'s), not captured into a const: identical semantics \u2014\n * the prototype is what it is \u2014 and it keeps the real terminal write observable to a test, which is the\n * only way to prove the liveness gate below actually drops a chunk instead of merely claiming to.\n */\nconst protoWrite = (): NodeJS.WriteStream['write'] => {\n return (Object.getPrototypeOf(process.stdout) as { write: NodeJS.WriteStream['write'] }).write\n}\n\n/**\n * Write straight to the real stdout, bypassing any patch installed on the stream instance.\n *\n * Gated on {@link isTerminalDead}: once a stdio stream has emitted `'error'`, every further write to it is\n * DROPPED. This is the choke point every terminal-bound line already funnels through \u2014 Ink via\n * `createSafeStream(panelStream())`, the embedded `DevRenderer`, and the entry's own shutdown line \u2014 so\n * gating it here is what stops `reportFault` from feeding the write that produced the fault it is reporting.\n *\n * It returns `true` (a lie the caller can act on) and never `false`: the return value propagates through\n * `panelStream`'s Proxy into Ink, and `false` reads as BACKPRESSURE \u2014 Ink would stall or buffer, hanging the\n * very teardown this exists to reach. It never throws, for the same reason.\n *\n * Gate-completeness here is hygiene, not correctness: a handful of writes stay ungated\n * (`output-intercept.ts`'s non-string passthrough, the direct `process.stderr.write` in `crash-barrier` and\n * `signal-shutdown`). That is survivable \u2014 with `terminal-liveness` owning the `'error'` event, an ungated\n * write costs one extra tick, not a loop. The correctness rests entirely on owning that event.\n */\nexport const rawStdoutWrite = (chunk: string): boolean => {\n if (isTerminalDead()) return true\n\n return protoWrite().call(process.stdout, chunk)\n}\n\n/**\n * The same bypass \u2014 and the same liveness gate \u2014 for stderr. Separate from {@link rawStdoutWrite} because\n * they are different fds: echoing stderr through stdout would send it to the terminal under\n * `dev 2>boot.log` and leave the redirect empty \u2014 the caller asked for stderr on a file, and we would have\n * quietly moved it.\n */\nexport const rawStderrWrite = (chunk: string): boolean => {\n if (isTerminalDead()) return true\n\n return (Object.getPrototypeOf(process.stderr) as { write: NodeJS.WriteStream['write'] }).write.call(\n process.stderr,\n chunk,\n )\n}\n\n/** True when something has already monkeypatched `process.stdout.write` as an own property. */\nexport const hasForeignStdoutPatch = (): boolean => {\n return Object.getOwnPropertyDescriptor(process.stdout, 'write') != null\n}\n\n/**\n * A `process.stdout` stand-in whose `write` goes straight to the real terminal, stepping over any patch\n * the output interceptor has installed. THIS is what the status panel must paint through.\n *\n * Hand the panel the raw `process.stdout` instead and its frames are quietly filed into a log file\n * rather than drawn \u2014 the screen simply stays blank, with no error and no failing test. That black hole,\n * not a stack overflow, is the failure mode this exists to prevent.\n *\n * Every other property (`columns`, `rows`, `isTTY`, `on('resize')`) is forwarded LIVE to the real\n * stream, so a resize still reaches the renderer.\n */\nexport const panelStream = (): NodeJS.WriteStream => {\n return new Proxy(process.stdout, {\n get(target, property, receiver) {\n if (property === 'write') {\n return (chunk: unknown): boolean => {\n return rawStdoutWrite(String(chunk))\n }\n }\n\n const value = Reflect.get(target, property, receiver) as unknown\n\n return typeof value === 'function' ? (value as (...args: unknown[]) => unknown).bind(target) : value\n },\n })\n}\n\n/** Running totals for one service row on the status panel. Bumped from DECLARED level, never from content. */\nexport interface ServiceStats {\n errors: number\n warns: number\n /** Epoch ms of the most recent error, or `null` when the service has never errored. */\n lastErrorAt: number | null\n}\n\nconst emptyStats = (): ServiceStats => {\n return { errors: 0, warns: 0, lastErrorAt: null }\n}\n\n/**\n * Collapse an untrusted string into ONE safe path segment, or `null` when nothing survives.\n *\n * Split-and-rejoin rather than replace-then-trim: it collapses runs, drops leading and trailing\n * separators by construction, and has no backtracking quantifier to be super-linear about. Dots are NOT\n * in the allow-set, so the result is structurally incapable of being `..` or of holding a separator \u2014\n * `path.join(root, safeSegment(x))` cannot leave `root` for any input.\n *\n * Used for BOTH halves of the path. Hardening the file name while leaving the directory unhardened was\n * the wrong half to protect: the directory is the root `gcOldLogDirs` walks with a recursive delete.\n */\nconst safeSegment = (raw: string): string | null => {\n const safe = raw\n .split(/\\W+/)\n .filter((part) => {\n return part !== ''\n })\n .join('-')\n\n return safe === '' ? null : safe\n}\n\n/**\n * Session log root: `<cacheRoot>/<INFRA_KIT_SESSION>/dev/<pid>/`.\n *\n * The `<pid>` segment is load-bearing, not cosmetic. `--cmux` spawns one `infra-kit dev` per pane and\n * every pane inherits the SAME `INFRA_KIT_SESSION`, so without it N panes interleave into one file.\n * Falls back to a literal `no-session` folder when the shell exported no id, so dev logging never\n * depends on `infra-kit init` having run.\n */\nexport const resolveLogDir = (): string => {\n // The session id is collapsed to a single safe segment, exactly as a service name is. It is normally\n // an 8-hex id the init rc exports \u2014 but it is an env var, and this path is the ROOT that `gcOldLogDirs`\n // walks with `rmSync(recursive, force)`. Hardening the file name while leaving the directory that the\n // recursive delete is anchored to unhardened is the wrong half to protect.\n const raw = process.env[INFRA_KIT_SESSION_VAR] ?? ''\n const session = safeSegment(raw) ?? 'no-session'\n\n return path.join(getCacheRoot(), session, 'dev', String(process.pid))\n}\n\n/** `client/api` \u2192 `client-api.log`. */\nexport const logFileName = (service: string): string => {\n return `${safeSegment(service) ?? 'unnamed'}.log`\n}\n\n/**\n * Drop dev log dirs older than {@link GC_MAX_AGE_MS}. A `<pid>` segment means a fresh dir per run, and\n * nothing else ever prunes them \u2014 so the sink prunes its own. Best-effort: a GC failure must never\n * take down a dev session, so every error is swallowed.\n */\nconst GC_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000\n\n/**\n * Is `name` a pid that is still running? `kill(pid, 0)` sends no signal \u2014 it only asks the kernel whether\n * the process exists (and whether we may signal it; `EPERM` means it exists but is not ours, which still\n * answers \"alive\"). A name that is not a pid at all is treated as dead, so a stray directory is still\n * collectable.\n */\nconst isPidAlive = (name: string): boolean => {\n const pid = Number(name)\n\n if (!Number.isInteger(pid) || pid <= 0) return false\n\n try {\n process.kill(pid, 0)\n\n return true\n } catch (error) {\n return (error as { code?: string }).code === 'EPERM'\n }\n}\n\nconst gcOldLogDirs = (devRoot: string, now: number): void => {\n let entries: fs.Dirent[]\n\n try {\n entries = fs.readdirSync(devRoot, { withFileTypes: true })\n } catch {\n return\n }\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n // NEVER touch a dir whose pid is still alive. Age alone is not enough to call one dead: a directory's\n // mtime is not bumped by writes INTO the files it holds, only by create/unlink, and every service\n // file is created at boot. So a dev session left running over a long break looks a week old from the\n // moment it starts \u2014 and deleting it would unlink the very inodes it still holds open, whose writes\n // then succeed silently into nothing. Liveness answers the question age only guesses at.\n if (isPidAlive(entry.name)) continue\n\n const dir = path.join(devRoot, entry.name)\n\n try {\n if (now - fs.statSync(dir).mtimeMs > GC_MAX_AGE_MS) {\n fs.rmSync(dir, { recursive: true, force: true })\n }\n } catch {\n // A dir we cannot stat or remove is a dir we leave alone.\n }\n }\n}\n\n/**\n * Hard per-service byte cap. A logging LOOP \u2014 the crash barrier reporting a fault whose report is itself\n * the fault \u2014 wrote 455 GB of `runner.log` at 50k\u2013127k lines/sec and filled a 926 GB disk. The loop itself\n * is fixed at its source (`terminal-liveness.ts`); this is the net under ANY future one, whatever its cause.\n *\n * A byte cap and not dedupe: this sink's stated principle is that it reads DECLARED provenance and never\n * inspects the bytes, and a \"same line N times\" filter would violate it. Not rotation either \u2014 rotating a\n * fault storm yields two enormous files and still fills the disk.\n *\n * Worst case on disk is `services \u00D7 MAX_LOG_BYTES \u00D7 retained_dirs`; in practice one file storms at a time,\n * so the incident's own shape (5 orphaned processes) caps at \u22481.3 GB against the 455 GB that happened.\n */\nconst DEFAULT_MAX_LOG_BYTES = 256 * 1024 * 1024\n\n/** Escape hatch for a session that legitimately needs a bigger (or smaller) budget. */\nconst MAX_LOG_BYTES_VAR = 'INFRA_KIT_DEV_LOG_MAX_BYTES'\n\n/** `INFRA_KIT_DEV_LOG_MAX_BYTES` when it parses to a positive integer, else {@link DEFAULT_MAX_LOG_BYTES}. */\nexport const resolveMaxLogBytes = (): number => {\n const raw = Number(process.env[MAX_LOG_BYTES_VAR])\n\n return Number.isInteger(raw) && raw > 0 ? raw : DEFAULT_MAX_LOG_BYTES\n}\n\n/**\n * The per-service log sink. One held fd per service, opened lazily on first write.\n *\n * Every routing and counting decision here reads DECLARED provenance \u2014 the service the caller named and\n * the level it chose \u2014 never the bytes of the line. Content is written, never inspected.\n */\nexport class DevLogSink {\n readonly dir: string\n private readonly fds = new Map<string, number>()\n private readonly counters = new Map<string, ServiceStats>()\n /** Bytes this process has written per service, SEEDED from the file's existing size on lazy open. */\n private readonly bytes = new Map<string, number>()\n /** Services whose cap has been hit; every further line is dropped for them, and only for them. */\n private readonly capped = new Set<string>()\n private readonly maxBytes: number\n private closed = false\n\n constructor(dir: string = resolveLogDir(), maxBytes: number = resolveMaxLogBytes()) {\n this.dir = dir\n this.maxBytes = maxBytes\n fs.mkdirSync(this.dir, { recursive: true })\n gcOldLogDirs(path.dirname(this.dir), Date.now())\n this.linkLatest()\n }\n\n /**\n * Point `<session>/dev/latest` at this run's dir, so `tail -f` has a stable path across runs even\n * though the real dir is pid-scoped. Best-effort \u2014 a filesystem without symlink support just loses\n * the shortcut, not the logs.\n */\n private linkLatest(): void {\n const link = path.join(path.dirname(this.dir), 'latest')\n\n try {\n fs.rmSync(link, { force: true })\n fs.symlinkSync(this.dir, link, 'dir')\n } catch {\n // No symlink \u2192 the panel still prints the absolute dir.\n }\n }\n\n /** Absolute path of a service's log file (whether or not it has been written to yet). */\n pathFor(service: string): string {\n return path.join(this.dir, logFileName(service))\n }\n\n /** Lazily open (and cache) the append fd for a service, seeding its byte count from the file on disk. */\n private fdFor(service: string): number | null {\n const cached = this.fds.get(service)\n\n if (cached != null) return cached\n\n try {\n const fd = fs.openSync(this.pathFor(service), 'a')\n\n this.fds.set(service, fd)\n // Seed from the file's CURRENT size, not zero: the fd is opened `'a'`, so a re-attached (or\n // pre-existing) file's bytes are real bytes on the disk we are trying to bound. Counting only our own\n // appends would let an already-huge file grow by another whole cap.\n try {\n this.bytes.set(service, fs.fstatSync(fd).size)\n } catch {\n this.bytes.set(service, 0)\n }\n\n return fd\n } catch {\n // A log file we cannot open must never down the dev session \u2014 the line is dropped, not thrown.\n return null\n }\n }\n\n /**\n * Append `text` to `<service>.log` and fold its DECLARED level into that service's counters.\n *\n * `text` is written verbatim (a trailing newline is added when absent) \u2014 the sink never parses it.\n * `level` comes from the emitter (the `console` method it chose, the Powertools level it set, the\n * HTTP status it returned), so the counter reports what the caller declared rather than guessing.\n */\n write(service: string, text: string, meta: { level?: LogLevel } = {}): void {\n if (this.closed) return\n\n const stats = this.counters.get(service) ?? emptyStats()\n\n if (meta.level === 'error') {\n stats.errors += 1\n stats.lastErrorAt = Date.now()\n } else if (meta.level === 'warn') {\n stats.warns += 1\n }\n this.counters.set(service, stats)\n\n // The cap latch lives HERE \u2014 after the counter bump, before the fd. A top-of-function early return (the\n // obvious implementation) would freeze the panel's red error counter for the whole storm, and a cap that\n // silences the only remaining signal is worse than no cap: it makes the next incident harder to diagnose,\n // which is precisely the objection to capping alone.\n if (this.capped.has(service)) return\n\n const fd = this.fdFor(service)\n\n if (fd == null) return\n\n const line = text.endsWith('\\n') ? text : `${text}\\n`\n const size = Buffer.byteLength(line)\n const written = this.bytes.get(service) ?? 0\n\n if (written + size > this.maxBytes) {\n this.capped.add(service)\n // One loud final line, naming the likely cause, into the service's OWN file (and counted against its\n // own budget). Per-service, so a chatty `client-ui.log` can never silence `runner.log`.\n this.writeRaw(\n fd,\n service,\n `[capped] ${logFileName(service)} hit ${this.maxBytes} bytes; further lines dropped (likely a logging loop)\\n`,\n )\n\n return\n }\n\n this.writeRaw(fd, service, line)\n }\n\n /** The one place bytes reach the disk: write, count what landed, and never throw. */\n private writeRaw(fd: number, service: string, line: string): void {\n try {\n fs.writeSync(fd, line)\n this.bytes.set(service, (this.bytes.get(service) ?? 0) + Buffer.byteLength(line))\n } catch {\n // A failed write is a dropped line, never a crashed dev server.\n }\n }\n\n /** Counters for one service (a service that has never written returns zeroes, not `undefined`). */\n statsFor(service: string): ServiceStats {\n return this.counters.get(service) ?? emptyStats()\n }\n\n /** Every service that has written at least one line. */\n services(): string[] {\n return [...this.fds.keys()]\n }\n\n /**\n * Close every fd. Idempotent, and synchronous by design so it is safe from a `process.on('exit')`\n * handler \u2014 there is no buffered data to flush, because there was never a buffer.\n */\n close(): void {\n if (this.closed) return\n this.closed = true\n for (const fd of this.fds.values()) {\n try {\n fs.closeSync(fd)\n } catch {\n // Already closed / bad fd \u2014 nothing left to do at teardown.\n }\n }\n this.fds.clear()\n }\n}\n", "/**\n * Owns the `'error'` channel of `process.stdout` / `process.stderr` \u2014 the ONLY point at which a failed\n * stdio write can be observed, and therefore the only place the fault loop can be cut.\n *\n * Why an `'error'` listener and nothing else (each alternative was probed, and each one silently fails):\n *\n * - A stdio write NEVER throws synchronously. Node builds the error in `afterWriteDispatched`, routes it\n * through `destroy(err, cb)`, and `internal/streams/destroy.js` schedules `process.nextTick(emitErrorCloseNT)`.\n * It surfaces a TICK LATER as an unhandled `'error'` event, which Node then turns into an\n * `uncaughtException`. So a `try/catch` around the writer catches nothing, and a synchronous\n * re-entrancy latch sees an empty stack.\n * - `stdout.destroyed` / `stdout.writable` are dead discriminators: Node's stdio streams carry a\n * `dummyDestroy` that `_undestroy()`s them, so after the failure they still read `false` / `true` and\n * keep accepting writes (and keep emitting `'error'`). Anything keyed on them never fires.\n *\n * With one listener attached, the `'error'` event has a handler, so it is no longer an `uncaughtException` \u2014\n * the crash barrier is never re-entered by our own writes, and the loop (write \u2192 EIO \u2192 uncaughtException \u2192\n * reportFault \u2192 write) is broken at its only edge.\n *\n * **Do NOT implement liveness by patching `write`.** `output-intercept.uninstall()`\n * (`output-intercept.ts:187`) does `Reflect.deleteProperty(stream, 'write')` \u2014 it would silently delete a\n * `write` patch installed here, and the liveness gate would vanish at exactly the moment (`shutdown()`)\n * the incident begins. The `'error'` event is a third, orthogonal channel that no `deleteProperty` can\n * touch:\n *\n * | Claimant | Channel | Removed by |\n * |--------------------------|-------------------------------------------------------------|-----------------------------|\n * | `output-intercept` | OWN-property `write` on the stream instance (`:149`) | `Reflect.deleteProperty` (`:187`) |\n * | `log-sink` / Ink | PROTOTYPE `write` via Proxy, stepping over the own-property | n/a (a Proxy, not a patch) |\n * | `terminal-liveness` | the **`'error'` event** | `removeListener` |\n *\n * ## Fatality is decided by STREAM IDENTITY, never by error code\n *\n * The listener is bound to the `process.stdout` / `process.stderr` OBJECTS. A handler writing to its own\n * client socket that hung up produces an `EPIPE` too \u2014 under a code-sniff, one user closing a browser tab\n * mid-request would kill the whole dev session, a worse fragility than the one the crash barrier removes.\n * Stream identity IS the proof that cannot happen.\n *\n * The one code-based rule permitted here is an EXCLUSION, not a selection: {@link TRANSIENT_CODES}\n * (`EAGAIN`, `EINTR`) are the only errnos that can mean \"still alive\" (libuv retries `EINTR` internally and\n * defers `EAGAIN` on pipes via `POLLOUT`; a TTY stdout is set blocking anyway). Everything else latches.\n *\n * ## The latch is right even when the terminal is fine\n *\n * With stdout redirected to a file, `process.stdout` is a `SyncWriteStream` and a failed `writeSync`\n * (`ENOSPC`, `EDQUOT`, `EFBIG`, `EBADF`) takes the SAME async `'error'` path. The latch is still correct \u2014\n * *stop writing to a stream that cannot be written to* holds whatever the cause \u2014 but the REPORT must name\n * the errno, never a story: `nohup ik dev > out.log` on a full disk is `ENOSPC`, and saying \"the terminal is\n * gone\" there would be a lie in the exact scenario (a disk-fill) this module exists to fix.\n */\nimport process from 'node:process'\n\n/** Which of the two stdio streams died \u2014 carried into the reason string, never guessed at. */\nexport type StdioStreamName = 'stdout' | 'stderr'\n\n/**\n * Errnos that do NOT mean the stream is gone. `EINTR` is retried inside libuv and `EAGAIN` is deferred via\n * `POLLOUT`, so neither should reach us at all \u2014 they are excluded as cheap insurance, and this exclusion is\n * the ONLY code-based logic in the module (see the doc block: fatality is stream identity, not error shape).\n */\nconst TRANSIENT_CODES = new Set(['EAGAIN', 'EINTR'])\n\nexport interface TerminalLivenessDeps {\n /**\n * The streams to own. Defaults to the real stdio pair. Injectable so a test can drive the real listener\n * with a fake stream instead of breaking the runner's own terminal.\n */\n streams?: NodeJS.WriteStream[]\n /**\n * Called at most ONCE, after the latch is already set. The reason a caller builds from it must name the\n * errno (`stdio unwritable: stdout ENOSPC`), never \"the terminal is gone\".\n */\n onDeath?: (stream: StdioStreamName, error: NodeJS.ErrnoException) => void\n}\n\nexport interface TerminalLiveness {\n /** True once one of the owned streams has emitted a non-transient `'error'`. */\n isDead: () => boolean\n /** Detach the listeners and clear the latch. Production never calls it; it keeps tests hermetic. */\n uninstall: () => void\n}\n\n/**\n * Module-scoped so the write gates in `log-sink.ts` can read it without threading a handle through every\n * writer. Precedent: `dev-server.ts`'s module-scoped `logSink`, wired for exactly the same reason.\n */\nlet dead = false\n\n/**\n * Has an owned stdio stream failed? The gate `rawStdoutWrite` / `rawStderrWrite` consult before every raw\n * terminal write \u2014 and the discriminator the crash barrier uses to decide fatal-vs-survive.\n */\nexport const isTerminalDead = (): boolean => {\n return dead\n}\n\n/** Identity first (the real streams), position as the fallback for injected fakes (`[stdout, stderr]`). */\nconst streamName = (stream: NodeJS.WriteStream, index: number): StdioStreamName => {\n if (stream === process.stdout) return 'stdout'\n if (stream === process.stderr) return 'stderr'\n\n return index === 1 ? 'stderr' : 'stdout'\n}\n\n/**\n * Install the one and only observer of stdio write failures.\n *\n * @example\n * const liveness = installTerminalLiveness({\n * onDeath: (stream, error) => onFatal(`stdio unwritable: ${stream} ${error.code}`),\n * })\n */\nexport const installTerminalLiveness = ({\n streams = [process.stdout, process.stderr],\n onDeath,\n}: TerminalLivenessDeps = {}): TerminalLiveness => {\n /** `onDeath` fires once, not once per stream: closing a terminal kills both fds at the same instant. */\n let fired = false\n const attached: { stream: NodeJS.WriteStream; listener: (error: unknown) => void }[] = []\n\n for (const [index, stream] of streams.entries()) {\n const name = streamName(stream, index)\n const listener = (raw: unknown): void => {\n const error = (raw ?? new Error('stdio error')) as NodeJS.ErrnoException\n\n if (error.code != null && TRANSIENT_CODES.has(error.code)) return\n\n // ORDER IS LOAD-BEARING: the latch closes BEFORE `onDeath` runs. `onDeath` leads to\n // `runner.shutdown()`, whose FIRST act is `renderer.dispose()` \u2192 `rerenderPersistent()`\n // (`persistent-ink-dev-ui.tsx:276-283`) \u2014 a write to this very stream. Latch second and teardown\n // begins by re-arming the thing it is tearing down.\n dead = true\n\n if (fired) return\n fired = true\n\n onDeath?.(name, error)\n }\n\n stream.on('error', listener)\n attached.push({ stream, listener })\n }\n\n return {\n isDead: () => {\n return dead\n },\n uninstall: () => {\n for (const { stream, listener } of attached) {\n stream.removeListener('error', listener)\n }\n attached.length = 0\n // Clearing the latch is what makes a test hermetic \u2014 a leaked `dead` would gate every raw write for\n // the rest of the worker. Nothing in production uninstalls: the dev process owns stdio until it exits.\n dead = false\n fired = false\n },\n }\n}\n", "/*\n * This module's entire job is to take ownership of the global `console`, so it necessarily names every\n * console method \u2014 including the ones `no-console` exists to keep out of application code. The rule is\n * disabled here and NOWHERE else: any other file reaching for `console.log` should still be stopped.\n */\n/* eslint-disable no-console */\n/**\n * Routes every line the dev process emits into the per-service log files, so the terminal can be given\n * over entirely to the status panel.\n *\n * ## Why this has to exist\n *\n * The backend runs IN-PROCESS: `ServerlessLocalRun` is fastify + `@aws-lambda-powertools/logger` in the\n * same node process, and the runner sets `POWERTOOLS_DEV=true`, which makes Powertools bind the GLOBAL\n * `console`. So a handler's `console.log` AND every Powertools line reach `process.stdout` through\n * channels no seam of ours owns. There is no other place to catch them. (Reassigning the Powertools\n * `Logger`'s console does NOT work: handlers are given a `createChild()` logger, and `createChild`\n * re-derives its console from the global \u2014 the parent's is never consulted.)\n *\n * ## Why this is not the classifier that was rejected\n *\n * An earlier design tried to decide, per line, whether to PRINT it \u2014 inferring \"whatever is left on\n * stdout must be human, therefore promote it\". That was unsound: the residual bucket also holds the\n * env-gated raw request line, dependency import banners, and Node's own warnings, so the rule promoted\n * exactly the noise it meant to hide. There is no such decision here. **Every line goes to a file** \u2014\n * one rule, no residue, nothing to be wrong about. The only thing read is DECLARED provenance: which\n * `console` method the caller chose, and which fd it wrote to. The line's bytes are never inspected.\n *\n * ## One switch, not two: NOTHING is ever echoed\n *\n * The sink installs EARLY (at process start) and is file-only from the first byte. There is no tee\n * window and no \"print until `ready()`\" phase: an app's log line NEVER reaches the terminal, at any\n * point in the process's life. That is the whole product decision \u2014 the terminal belongs to the panel.\n *\n * The tee that used to exist was justified by one fear: that a boot crash would vanish into a log file\n * and leave the user staring at a blank screen. That fear is unfounded, and each of the three ways a\n * dev session can die is covered WITHOUT echoing a single log line:\n *\n * 1. An uncaught exception / rejection during boot. Node's fatal report is written STRAIGHT TO FD 2 by\n * the runtime \u2014 it never goes through `process.stderr.write`, so the patch below cannot swallow it.\n * (Verified, not assumed: patch `stderr.write`, throw, and the stack still prints while the patch\n * counts zero chunks.)\n * 2. A boot failure that rejects `run()`. `shutdown()` calls {@link OutputIntercept.uninstall} BEFORE\n * anything prints, so the error surfaces from the entry point's top-level catch on a clean stderr.\n * 3. A fault after `ready()`. The crash barrier routes it to `DevServerRunner.reportFault`, which files\n * it at `error` (turning the row red) AND paints it through the panel's bypass.\n *\n * So a crash is never silent, and a LOG is never printed. Those were always two separate jobs; the tee\n * conflated them, and the price was a Powertools `Server listening` banner printed above the panel.\n *\n * ## Anti-recursion\n *\n * The panel writes to the terminal through {@link rawStdoutWrite}, which resolves `write` off the\n * PROTOTYPE and so steps over the own-property patch installed here. The interceptor's only sink is a\n * file. The failure mode to guard against is not a stack overflow \u2014 it is a BLACK HOLE: hand the panel\n * a patched stream and its frames are quietly filed into a log instead of drawn.\n */\nimport process from 'node:process'\nimport util from 'node:util'\n\nimport type { DevLogSink } from './log-sink.js'\nimport type { LogLevel } from './render.js'\n\n/** The `console` methods that reach a terminal, each with the level its NAME declares. */\nconst CONSOLE_LEVELS = {\n log: 'info',\n info: 'info',\n debug: 'debug',\n trace: 'debug',\n warn: 'warn',\n error: 'error',\n} as const satisfies Record<string, LogLevel>\n\ntype ConsoleMethod = keyof typeof CONSOLE_LEVELS\n\nexport interface OutputInterceptOptions {\n /** Where every captured line is filed. */\n sink: DevLogSink\n /** The service to file a line under when nothing else claims it. A NAMED bucket, never a guess. */\n fallbackService: string\n /** The service owning the current async context, if any (the `AsyncLocalStorage` lookup). */\n currentService: () => string | undefined\n}\n\nexport interface OutputIntercept {\n /** Restore `console` and the raw stream writes to exactly what they were. Idempotent. */\n uninstall: () => void\n}\n\n/**\n * Install the interception. Every captured line goes to a FILE and only to a file \u2014 there is no\n * terminal echo at any point. Call {@link OutputIntercept.uninstall} on every exit path, BEFORE\n * anything else prints.\n *\n * Never install this on a non-TTY / `--json` / MCP run: it would file the machine-readable stream into\n * a log and hand the caller an empty stdout. The caller owns that gate.\n */\nexport const installOutputIntercept = ({\n sink,\n fallbackService,\n currentService,\n}: OutputInterceptOptions): OutputIntercept => {\n let live = true\n\n const serviceFor = (): string => {\n return currentService() ?? fallbackService\n }\n\n /** File a line. There is no second destination: the terminal belongs to the panel. */\n const capture = (text: string, level: LogLevel): void => {\n sink.write(serviceFor(), text, { level })\n }\n\n // ---- console ------------------------------------------------------------\n //\n // The console patches do NOT delegate to the original methods. They must not: `console.log` writes\n // through `process.stdout.write`, which is patched below, so delegating would file every console line\n // TWICE and double every counter. Routing them straight to the sink also preserves the one thing the\n // raw-stream patch cannot see \u2014 the level the caller DECLARED by picking `warn` over `log`.\n const originalConsole = {} as Record<ConsoleMethod, (...args: unknown[]) => void>\n\n for (const method of Object.keys(CONSOLE_LEVELS) as ConsoleMethod[]) {\n originalConsole[method] = console[method] as (...args: unknown[]) => void\n console[method] = (...args: unknown[]): void => {\n if (!live) {\n originalConsole[method](...args)\n\n return\n }\n capture(util.format(...args), CONSOLE_LEVELS[method])\n }\n }\n\n // ---- raw stream writes --------------------------------------------------\n //\n // What is left after the console patch: writes that bypass `console` entirely \u2014 the env-gated raw\n // request line, a dependency's import-time banner, and `process.emitWarning` (which goes to stderr,\n // NOT through `console.warn`). Chunk-oriented, so they are line-buffered before being filed.\n const patchStream = (stream: NodeJS.WriteStream, level: LogLevel): (() => void) => {\n const original = stream.write.bind(stream)\n let pending = ''\n\n const flush = (): void => {\n if (pending === '') return\n sink.write(serviceFor(), pending, { level })\n pending = ''\n }\n\n stream.write = ((chunk: unknown, ...rest: unknown[]): boolean => {\n if (!live || typeof chunk !== 'string') {\n return (original as (...args: unknown[]) => boolean)(chunk, ...rest)\n }\n\n pending += chunk\n\n const lines = pending.split('\\n')\n\n pending = lines.pop() ?? ''\n for (const line of lines) {\n if (line !== '') sink.write(serviceFor(), line, { level })\n }\n\n // A chunk that never ends in a newline (a progress line, a prompt) would otherwise be held\n // forever; flush it once it is clearly not a partial line.\n if (pending.length > 8192) flush()\n\n // Honour the stream contract's completion callback \u2014 `write(chunk, cb)` and `write(chunk, enc, cb)`\n // both promise to call it. Swallowing it hangs any caller that awaits the write before proceeding\n // (a logger flushing before exit, a promisified write), and it would hang inside a dev session\n // whose terminal shows nothing but a frozen panel. Deferred, exactly as a real stream defers it.\n const callback = rest.find((arg) => {\n return typeof arg === 'function'\n })\n\n if (typeof callback === 'function') {\n process.nextTick(callback as () => void)\n }\n\n return true\n }) as NodeJS.WriteStream['write']\n\n return (): void => {\n flush()\n // Delete the own-property patch rather than reassigning the original: reassigning would leave a\n // second own property in place, and `hasForeignStdoutPatch` (and the next installer) would still\n // see a patched stream.\n Reflect.deleteProperty(stream, 'write')\n }\n }\n\n // stderr is levelled `warn`, not `error`: `process.emitWarning` is far and away its highest-volume\n // user, and a genuine fault does not rely on this level \u2014 the crash barrier reports through\n // `DevServerRunner.reportFault`, which files at `error` AND punches the stack onto the terminal.\n // Neither stream is echoed anywhere: a level here decides which FILE bucket and which counter a line\n // lands in, nothing more.\n const restoreStdout = patchStream(process.stdout, 'info')\n const restoreStderr = patchStream(process.stderr, 'warn')\n\n return {\n uninstall: (): void => {\n if (!live) return\n live = false\n restoreStdout()\n restoreStderr()\n for (const method of Object.keys(CONSOLE_LEVELS) as ConsoleMethod[]) {\n console[method] = originalConsole[method] as typeof console.log\n }\n },\n }\n}\n", "/**\n * Pure port / URL-prefix resolution for the dev-server.\n *\n * These functions are intentionally side-effect free: the environment is passed\n * in (never read from `process.env` here) and no cwd / fs access happens. That\n * keeps port precedence and conflict detection unit-testable in isolation.\n */\nimport type { DevConfig } from '../lib/infra-kit-config/index.js'\n\n/** Fallback port when no PORT / {APP}_PORT env var and no config port is set. */\nexport const DEFAULT_PORT = 3010\n\n/**\n * URL prefix applied to each app's routes when nothing is configured. Overridable\n * per app via `dev.<app>.prefixUrl` in infra-kit.json \u2014 this is only the fallback.\n */\nexport const DEFAULT_PREFIX_URL = '/api/v1'\n\n/**\n * Parse a raw port string (env var or config), stripping a single pair of\n * surrounding quotes and treating blank / non-numeric input as \"unset\".\n */\nexport function parsePortString(raw: string | undefined): number | undefined {\n if (raw == null || raw === '') {\n return undefined\n }\n\n const n = parseInt(raw.trim().replace(/^[\"']|[\"']$/g, ''), 10)\n\n return Number.isNaN(n) ? undefined : n\n}\n\n/**\n * Resolve the PORT for an API app (highest priority first):\n *\n * 1. **`{APP}_PORT`** \u2014 e.g. `CLIENT_PORT`, `SEARCH_ENGINE_PORT` (secrets manager or shell)\n * 2. **`PORT`** \u2014 shared fallback (multi-app: use distinct `{APP}_PORT` in env)\n * 3. **`dev.<app>.port`** from infra-kit.json\n * 4. Default {@link DEFAULT_PORT}\n *\n * Per-app env keys use the app folder name in **UPPER_SNAKE_CASE** (hyphens \u2192 underscores).\n */\nexport function resolvePort(appName: string, env: NodeJS.ProcessEnv, devConfig: DevConfig): number {\n return resolvePreferredPort(appName, env, devConfig) ?? DEFAULT_PORT\n}\n\n/**\n * Resolve the EXPLICITLY-configured port for an API app \u2014 `{APP}_PORT`, then `PORT`, then\n * `dev.<app>.port` \u2014 or `undefined` when none is set. Unlike {@link resolvePort} this does\n * NOT fall back to {@link DEFAULT_PORT}: it distinguishes an app the developer pinned to a\n * port (a preferred bind target) from an unconfigured app (which binds ephemeral straight\n * away under dynamic allocation). Used by the dev-server to (a) pick the preferred bind\n * port and (b) relax the conflict gate to explicit ports only.\n */\nexport function resolvePreferredPort(\n appName: string,\n env: NodeJS.ProcessEnv,\n devConfig: DevConfig,\n): number | undefined {\n const prefix = appName.replace(/-/g, '_').toUpperCase()\n const prefixedKey = `${prefix}_PORT`\n\n const fromPrefixed = parsePortString(env[prefixedKey])\n\n if (fromPrefixed != null) {\n return fromPrefixed\n }\n\n const fromPort = parsePortString(env.PORT)\n\n if (fromPort != null) {\n return fromPort\n }\n\n return devConfig[appName]?.port ?? undefined\n}\n\n/**\n * Resolve the URL prefix for an API app: `dev.<app>.prefixUrl` from\n * infra-kit.json, falling back to {@link DEFAULT_PREFIX_URL} (`/api/v1`).\n */\nexport function resolvePrefixUrl(appName: string, devConfig: DevConfig): string {\n return devConfig[appName]?.prefixUrl ?? DEFAULT_PREFIX_URL\n}\n\n/** The apps that collide on a port, plus the raw duplicate-port list for messaging. */\nexport interface PortConflicts {\n /** Ports that appear more than once (one entry per extra occurrence, in scan order). */\n duplicatePorts: number[]\n /** Every app whose resolved port is one of the duplicates. */\n conflictingApps: Array<{ name: string; port: number }>\n}\n\n/**\n * Find apps that resolve to the same port. Returns empty `duplicatePorts` when\n * there is no conflict; the caller decides how to surface / throw.\n */\nexport function findPortConflicts(apps: Array<{ name: string; port: number }>): PortConflicts {\n const ports = apps.map((a) => {\n return a.port\n })\n const duplicatePorts = ports.filter((port, index) => {\n return ports.indexOf(port) !== index\n })\n const conflictingApps = apps.filter((a) => {\n return duplicatePorts.includes(a.port)\n })\n\n return { duplicatePorts, conflictingApps }\n}\n", "import { Logger } from '@aws-lambda-powertools/logger'\nimport type { APIGatewayProxyEvent, APIGatewayProxyEventQueryStringParameters, Context } from 'aws-lambda'\nimport fastify from 'fastify'\nimport * as fs from 'node:fs'\nimport type { Server } from 'node:http'\nimport * as path from 'node:path'\nimport process from 'node:process'\nimport { pathToFileURL } from 'node:url'\nimport { parse as parseYaml } from 'yaml'\n\nimport type { ILogger } from './interfaces.js'\nimport { enterAttribution, runAttributed } from './log-attribution.js'\n\nexport interface IServerConfig {\n controllersPath: string\n prefixUrl?: string\n /**\n * PREFERRED bind port. A number is tried first and, on `EADDRINUSE`, the runner falls\n * back to an ephemeral `listen(0)` port. `undefined` (no explicit config) binds an\n * ephemeral port straight away. After {@link ServerlessLocalRun.start} the field is\n * mutated in place to the ACTUAL bound port so `/__health` reports the real port.\n */\n port?: number\n /** App folder name, surfaced by the `/__health` endpoint. Optional. */\n appName?: string\n /**\n * Structured per-request sink. When provided, every response emits `{ method, path, status, ms }`\n * (path already trimmed of any query string) so the caller can tag + timestamp it \u2014 the dev-server\n * routes this into its renderer's live tail. Independent of the env-gated raw stdout line below.\n */\n onRequestLog?: (event: { method: string; path: string; status: number; ms: number }) => void\n /**\n * The service tag (`<app>/api`) every line this app emits is filed under \u2014 a handler's `console.log`,\n * a Powertools line, a dependency's banner. The backend is IN-PROCESS and multi-app, so nothing in a\n * raw stdout write says which app wrote it; entering an `AsyncLocalStorage` context per request is\n * what makes the attribution possible at all. Omit \u2192 those lines land in the runner's fallback bucket.\n */\n serviceTag?: string\n}\n\n/** True when a listen error is a port-already-in-use (`EADDRINUSE`) failure. */\nconst isAddressInUse = (error: unknown): boolean => {\n return (error as { code?: string } | null)?.code === 'EADDRINUSE'\n}\n\ntype HandlerResult = Promise<{ body: string; headers: Record<string, string>; statusCode: number }>\n\n/** Default simulated Lambda timeout; overridable via `DEV_SERVER_TIMEOUT_MS`. */\nconst DEFAULT_LAMBDA_TIMEOUT_MS = 30_000\n\n/** Resolve the simulated Lambda timeout (ms), honoring `DEV_SERVER_TIMEOUT_MS` when it parses. */\nconst resolveLambdaTimeoutMs = (): number => {\n const raw = Number.parseInt(process.env.DEV_SERVER_TIMEOUT_MS ?? '', 10)\n\n return Number.isNaN(raw) ? DEFAULT_LAMBDA_TIMEOUT_MS : raw\n}\n\n/**\n * Whether to emit a one-line `<method> <url> \u2192 <status> <ms>ms` log per request.\n * Off by default; opt in with `DEV_SERVER_REQUEST_LOG=1` (kept out of the Powertools\n * JSON logger so the line stays terminal-readable). Mirrors the env-reader precedent\n * of {@link resolveLambdaTimeoutMs}.\n */\nconst isRequestLogEnabled = (): boolean => {\n return process.env.DEV_SERVER_REQUEST_LOG === '1'\n}\n\nexport class ServerlessLocalRun {\n /** Busts Node ESM `import()` cache on each new server instance (watch restart). */\n private readonly importCacheBust: string\n private readonly logger: Logger\n private readonly server: ReturnType<typeof fastify>\n private readonly controllers: Record<\n string,\n {\n action: Record<string, (event: APIGatewayProxyEvent, ctx: Context, log: ILogger) => HandlerResult>\n handler: string\n }\n > = {}\n\n /** `method urlAction` keys reserved synchronously, so duplicates are caught before any async import. */\n private readonly registeredRouteKeys = new Set<string>()\n\n constructor(private readonly serverConfig: IServerConfig) {\n this.importCacheBust = `${Date.now()}`\n this.logger = new Logger({ serviceName: 'LocalServer', logLevel: 'DEBUG' })\n this.serverConfig.prefixUrl = this.serverConfig.prefixUrl ?? ''\n this.server = fastify({ logger: false })\n\n // Add CORS support for local development\n this.server.addHook(\n 'onRequest',\n async (\n request: { method: string },\n reply: { header: (k: string, v: string) => unknown; status: (n: number) => { send: () => void } },\n ) => {\n // Claim every line the rest of this request emits for this app. `enterWith` (not `run`) because\n // fastify owns the call into the handler \u2014 we cannot wrap it. Entering here makes the WHOLE\n // remaining hook chain a continuation of this context: the handler, `onResponse` (where the raw\n // request line is written), and the error handler all attribute to the same app.\n const serviceTag = this.serverConfig.serviceTag\n\n if (serviceTag != null) enterAttribution(serviceTag)\n\n reply.header('Access-Control-Allow-Origin', '*')\n reply.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS')\n reply.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With')\n\n // Handle preflight OPTIONS requests\n if (request.method === 'OPTIONS') {\n reply.status(204).send()\n }\n },\n )\n\n // Per-request visibility for live dev traffic. Two independent sinks: a structured\n // `onRequestLog` callback (the dev-server tags + timestamps it in its tail) and/or the\n // legacy env-gated raw stdout line (standalone use). Registered only when at least one is on.\n const onRequestLog = this.serverConfig.onRequestLog\n\n if (isRequestLogEnabled() || onRequestLog) {\n this.server.addHook(\n 'onResponse',\n async (request: { method: string; url: string }, reply: { statusCode: number; elapsedTime: number }) => {\n const ms = Math.round(reply.elapsedTime)\n // Trim the query string so the tail shows a clean route, not `?a=b` noise.\n const requestPath = request.url.split('?')[0] ?? request.url\n\n onRequestLog?.({ method: request.method, path: requestPath, status: reply.statusCode, ms })\n if (isRequestLogEnabled()) {\n process.stdout.write(`${request.method} ${request.url} \u2192 ${reply.statusCode} ${ms}ms\\n`)\n }\n },\n )\n }\n }\n\n /**\n * The registered `METHOD /path` route keys (sorted). Excludes the internal\n * `/__health` liveness route, which is registered outside {@link defineRoute} and\n * never added to {@link registeredRouteKeys}. Used by the runner's startup route dump.\n */\n public getRegisteredRoutes(): string[] {\n return [...this.registeredRouteKeys].sort()\n }\n\n /**\n * Boot the server and RETURN the actual bound port. Under dynamic allocation the port is\n * a runtime fact known only after `listen`, so the caller must consume this return value\n * (never the static config port). {@link serverConfig.port} is mutated in place to the\n * bound port so `/__health` reports the real value.\n */\n public async start(): Promise<number> {\n this.registerHealthRoute()\n\n await Promise.all(this.loadRoutes())\n\n const boundPort = await this.listenWithFallback()\n\n this.serverConfig.port = boundPort\n\n this.logger.info(`Server listening on http://127.0.0.1:${boundPort}`, {\n address: `http://127.0.0.1:${boundPort}`,\n })\n\n return boundPort\n }\n\n /**\n * Bind the server and return the ACTUAL bound port. With an explicitly-configured\n * preferred port, try it first and fall back to an ephemeral `listen(0)` on `EADDRINUSE`\n * so extra worktrees never collide; with no preferred port (`undefined`), bind ephemeral\n * straight away. Non-`EADDRINUSE` errors propagate.\n */\n private async listenWithFallback(): Promise<number> {\n const preferred = this.serverConfig.port\n\n if (preferred != null) {\n try {\n await this.server.listen({ port: preferred, host: '127.0.0.1' })\n\n return this.readBoundPort()\n } catch (error) {\n if (!isAddressInUse(error)) {\n throw error\n }\n }\n }\n\n await this.server.listen({ port: 0, host: '127.0.0.1' })\n\n return this.readBoundPort()\n }\n\n /** Read the concrete bound port from the underlying HTTP server after `listen`. */\n private readBoundPort(): number {\n const address = (this.server.server as Server).address()\n\n if (address == null || typeof address === 'string') {\n throw new Error('Server address unavailable after listen()')\n }\n\n return address.port\n }\n\n /** Close the server (for watch/restart). */\n public async close(): Promise<void> {\n const raw = this.server.server as Server\n\n if (typeof raw.closeAllConnections === 'function') {\n raw.closeAllConnections()\n }\n await this.server.close()\n }\n\n /**\n * Register a fixed `GET /__health` liveness route returning 200. The path is\n * unprefixed (serverless.yml routes carry the `prefixUrl`, e.g. `/api/v1/...`),\n * so it never collides with a handler route.\n */\n private registerHealthRoute(): void {\n this.server.route({\n method: 'GET',\n url: '/__health',\n handler: (_request: unknown, reply: { code: (n: number) => { send: (body: unknown) => void } }) => {\n return reply.code(200).send({\n status: 'ok',\n app: this.serverConfig.appName ?? null,\n port: this.serverConfig.port,\n })\n },\n })\n }\n\n private loadRoutes(): Promise<void>[] {\n const serverlessYmlPath = path.join(this.serverConfig.controllersPath, 'serverless.yml')\n const fileContents = fs.readFileSync(serverlessYmlPath, 'utf8')\n const data = parseYaml(fileContents) as {\n functions: Record<string, { events?: Array<{ http?: { method: string; path: string } }>; handler?: string }>\n }\n const p: Promise<void>[] = []\n\n if (!data?.functions) return p\n\n for (const funcDef of Object.values(data.functions)) {\n if (!funcDef?.events?.length) continue\n for (const element of funcDef.events) {\n const http = element?.http\n\n if (!http) continue\n p.push(this.defineRoute(http, funcDef))\n }\n }\n\n return p\n }\n\n private async defineRoute(http: { method: string; path: string }, funcDef: { handler?: string }): Promise<void> {\n let url = http.path.toString()\n\n url = url.replaceAll('{', ':').replaceAll('}', '')\n\n let urlAction = path.posix.join(this.serverConfig.prefixUrl ?? '', url)\n\n urlAction = urlAction[0] === '/' ? urlAction : `/${urlAction}`\n\n const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']\n const method = String(http.method).toUpperCase()\n\n if (!validMethods.includes(method)) {\n throw new Error(`Invalid HTTP method: \"${http.method}\" for URL: ${urlAction}`)\n }\n\n // Key on method + path: two events can share a path but differ by method (e.g. GET/POST /users),\n // so keying on the path alone would let one handler overwrite the other. Reserve the key\n // synchronously (before the first `await`) so concurrent route loads detect a true duplicate.\n const routeKey = `${method} ${urlAction}`\n\n if (this.registeredRouteKeys.has(routeKey)) {\n throw new Error(`Duplicate route: ${routeKey}`)\n }\n this.registeredRouteKeys.add(routeKey)\n\n const handlerStr = funcDef.handler ?? ''\n const parts = handlerStr.split('.')\n const filepath = parts[0] ?? ''\n const handler = parts[1] ?? ''\n\n const controllerPath = path.join(this.serverConfig.controllersPath, `${filepath}.js`)\n const fileUrl = pathToFileURL(controllerPath)\n\n // Search params bust Node's ESM import cache so watch rebuilds load new `dist` output.\n fileUrl.searchParams.set('v', this.importCacheBust)\n\n // Attribute the handler module's IMPORT-TIME output \u2014 a banner from one of its deps, a top-level\n // log \u2014 to this app rather than the runner's fallback bucket. Honest limit: a library shared by two\n // apps is ONE module instance, so anything it registers at import keeps whichever app loaded it\n // first. That is inference, not declaration; the fallback bucket is named precisely so the wrong\n // guess is never made silently.\n const serviceTag = this.serverConfig.serviceTag\n const importHandler = async (): Promise<\n Record<string, (event: APIGatewayProxyEvent, ctx: Context, log: ILogger) => HandlerResult>\n > => {\n return (await import(fileUrl.href)) as Record<\n string,\n (event: APIGatewayProxyEvent, ctx: Context, log: ILogger) => HandlerResult\n >\n }\n const action = serviceTag == null ? await importHandler() : await runAttributed(serviceTag, importHandler)\n\n this.controllers[routeKey] = { action, handler }\n\n const traceLogger = this.logger.createChild({ serviceName: 'RequestLogger' })\n\n this.server.route({\n method: method as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS',\n url: urlAction,\n handler: async (\n request: { body?: unknown; query?: unknown; params?: unknown; headers?: unknown },\n reply: {\n headers: (h: Record<string, string>) => unknown\n code: (n: number) => { send: (body: unknown) => void }\n },\n ) => {\n const controller = this.controllers[routeKey]\n\n if (!controller) throw new Error(`No controller for ${routeKey}`)\n const handlerFn = controller.action[controller.handler]\n\n if (!handlerFn) throw new Error(`No handler ${controller.handler} for ${urlAction}`)\n const retVal = await handlerFn(\n this.getEventObj(request.body, request.query, request.params, request.headers, method, urlAction),\n this.getContext(),\n traceLogger,\n )\n const responseBody = JSON.parse(retVal.body)\n\n reply.headers(retVal.headers ?? {})\n\n return reply.code(retVal?.statusCode ?? 500).send(responseBody)\n },\n })\n }\n\n private getEventObj(\n requestBody?: unknown,\n queryParams?: unknown,\n pathParameters?: unknown,\n headers?: unknown,\n httpMethod = '',\n path = '',\n ): APIGatewayProxyEvent {\n const retVal = {\n body: requestBody ? JSON.stringify(requestBody) : null,\n headers: (headers ?? {}) as APIGatewayProxyEvent['headers'],\n multiValueHeaders: {},\n httpMethod,\n isBase64Encoded: false,\n path,\n pathParameters: pathParameters ?? null,\n queryStringParameters: (queryParams as APIGatewayProxyEventQueryStringParameters) ?? null,\n multiValueQueryStringParameters: null,\n stageVariables: null,\n requestContext: {\n accountId: '',\n apiId: '',\n authorizer: undefined,\n protocol: '',\n httpMethod,\n identity: {\n accessKey: null,\n accountId: null,\n apiKey: null,\n apiKeyId: null,\n caller: null,\n clientCert: null,\n cognitoAuthenticationProvider: null,\n cognitoAuthenticationType: null,\n cognitoIdentityId: null,\n cognitoIdentityPoolId: null,\n principalOrgId: null,\n sourceIp: 'devIp',\n user: null,\n userAgent: null,\n userArn: null,\n },\n path,\n stage: '',\n requestId: '',\n requestTimeEpoch: 0,\n resourceId: '',\n resourcePath: path,\n },\n resource: path,\n }\n\n ;(retVal as APIGatewayProxyEvent & { source: string }).source = 'aws.events'\n\n return retVal as APIGatewayProxyEvent\n }\n\n private getContext(): Context {\n const startTime = Date.now()\n const timeoutMs = resolveLambdaTimeoutMs()\n const datePart = new Date().toISOString().split('T')[0] ?? ''\n\n return {\n callbackWaitsForEmptyEventLoop: false,\n functionName: 'local-dev',\n functionVersion: '1.0.0',\n invokedFunctionArn: 'arn:aws:lambda:local:000000000000:function:local-dev',\n memoryLimitInMB: '1024',\n awsRequestId: `local-${Date.now()}`,\n logGroupName: '/aws/lambda/local-dev',\n logStreamName: `${datePart}/local`,\n getRemainingTimeInMillis: (): number => {\n return Math.max(0, timeoutMs - (Date.now() - startTime))\n },\n done: (_error?: Error, _result?: unknown): void => {},\n fail: (_error: string | Error): void => {},\n succeed: (_messageOrObject: unknown): void => {},\n }\n }\n}\n", "/*\n * `pnpm` is resolved from PATH \u2014 the same trust posture as the rest of the dev-server, which\n * already shells out to `pnpm exec turbo \u2026`; the args are fixed literals plus discovered package\n * names, never shell-interpolated. Matches the file-level disable in `scripts/build.js`.\n */\n/* eslint-disable sonarjs/no-os-command-from-path */\n/**\n * `turbo watch build` engine for `infra-kit dev --watch`.\n *\n * Spawns ONE long-lived `turbo watch build` child that owns incremental rebuilds and\n * dependency-graph fan-out. Its stdout is teed to the runner log but NEVER parsed for\n * control flow: piped `turbo watch` block-buffers stdout and emits no per-task completion\n * marker for `tsc -b` tasks, so the dev-server derives \"a build finished\" from watching\n * `dist/` output instead (see {@link file://./dev-server.ts} `setupWatch`).\n *\n * The child is a 5-deep tree (`sh \u2192 pnpm \u2192 node \u2192 turbo \u2192 native binary`); killing the\n * wrapper PID orphans the rest. So it is spawned `detached` (its own process group) and\n * torn down with a process-GROUP signal \u2014 verified to fully reap the tree.\n */\nimport { spawn } from 'node:child_process'\nimport * as fs from 'node:fs'\n\nimport { superviseChild } from './managed-child.js'\nimport type { ManagedChild, UnexpectedExitHandler } from './managed-child.js'\n\n/** Handle to the running `turbo watch` child; `kill()` reaps the whole process group. */\nexport type TurboWatchHandle = ManagedChild\n\n/** Injectable spawn seam so tests run the orchestrator without a real turbo child. */\nexport type TurboWatchFactory = (opts: TurboWatchOptions) => TurboWatchHandle\n\nexport interface TurboWatchOptions {\n /**\n * API app package names, watched DEPENDENCY-INCLUSIVE (`--filter=...<pkg>`): rebuild the app AND its\n * dependency closure, so editing a shared lib the backend uses triggers a rebuild + restart.\n */\n depInclusive: string[]\n /**\n * UI app package names, watched DEP-CLOSURE-ONLY (`--filter=<pkg>^...`): rebuild the frontend's shared-lib\n * dependencies but NEVER the UI's own `build` (that would run a production `vite build` \u2014 the UI's live\n * dev is owned by the separate `turbo run dev`/vite child). Covers FE-only libs + UI-only sessions.\n */\n depClosure: string[]\n /** Consumer repo cwd the child runs in (turbo resolves the consumer's own pin here). */\n cwd: string\n /** Runner log file; the child's stdout+stderr are appended to it. */\n logFile: string\n /**\n * Called if the engine dies on its own (not via `kill()`): incremental rebuilds silently stop, so\n * the runner surfaces it. Optional so the injected test factory can ignore it.\n */\n onUnexpectedExit?: UnexpectedExitHandler\n}\n\n/**\n * Build the `--filter=` arg vector: `...<pkg>` (dependency-inclusive) for each API package, `<pkg>^...`\n * (dependencies only, excluding the package itself) for each UI package. Pure/order-stable so the exact\n * emitted args are unit-testable; API-only input reproduces the historical `--filter=...<pkg>` vector.\n *\n * @example\n * buildTurboWatchFilters(['api-a'], ['ui-a']) // => ['--filter=...api-a', '--filter=ui-a^...']\n */\nexport const buildTurboWatchFilters = (depInclusive: string[], depClosure: string[]): string[] => {\n return [\n ...depInclusive.map((name) => {\n return `--filter=...${name}`\n }),\n ...depClosure.map((name) => {\n return `--filter=${name}^...`\n }),\n ]\n}\n\n/**\n * Default factory: spawn `pnpm exec turbo watch build <filters> --continue=dependencies-successful\n * --env-mode=loose` detached, tee output to `logFile`, and reap the process group on `kill()`.\n *\n * Filters come from {@link buildTurboWatchFilters}: `...<api>` (dep-inclusive) rebuilds a backend + its\n * closure; `<ui>^...` (dep-closure-only) rebuilds the frontend's shared libs without production-building\n * the UI. `--continue=dependencies-successful` keeps the watcher alive when one package fails to compile,\n * so a shared-lib type error never tears down the whole engine and the last-good `dist/` keeps serving.\n */\nexport const defaultTurboWatchFactory: TurboWatchFactory = ({\n depInclusive,\n depClosure,\n cwd,\n logFile,\n onUnexpectedExit,\n}) => {\n const filters = buildTurboWatchFilters(depInclusive, depClosure)\n const out = fs.openSync(logFile, 'a')\n const child = spawn(\n 'pnpm',\n ['exec', 'turbo', 'watch', 'build', ...filters, '--continue=dependencies-successful', '--env-mode=loose'],\n { cwd, detached: true, stdio: ['ignore', out, out] },\n )\n\n return superviseChild(child, undefined, onUnexpectedExit)\n}\n", "/*\n * `pnpm` is resolved from PATH \u2014 same trust posture as the rest of the dev-server (which already\n * shells out to `pnpm exec turbo \u2026`); args are fixed literals plus discovered package names, never\n * shell-interpolated. Matches the file-level disable in `scripts/build.js`.\n */\n/* eslint-disable sonarjs/no-os-command-from-path */\n/**\n * Frontend dev engine for `infra-kit dev --ui`.\n *\n * Delegates FE to ONE `turbo run dev` child (turbo owns the `dev` fan-out and concurrency) rather than\n * infra-kit spawning each framework itself \u2014 the same delegation choice already made for builds via\n * `turbo watch`. infra-kit treats UIs opaquely: it runs their `dev` script (vite/vike/astro/\u2026) and\n * never encodes per-framework knowledge.\n *\n * This child's stdio is PIPED, never inherited. With `inherit`, turbo and the framework write straight\n * to the TTY, bypassing the renderer entirely: turbo's run chrome interleaves with the pinned footer,\n * and vite's `Local:` URL contradicts the proxy hero URL the ready header already shows. Piping makes\n * `infra-kit dev` the single owner of the terminal \u2014 every line is tee'd verbatim to the runner log,\n * and the framework's own lines are routed through `onLine` into the renderer's tagged tail, so vite's\n * URLs, HMR notices and compile errors surface INSIDE the UI instead of fighting it.\n *\n * `--only` keeps that tail signal-dense. A `dev` task `dependsOn: [\"^build\"]`, so turbo would otherwise\n * re-walk the whole dependency closure and emit one `cache hit` line per dep \u2014 work the runner already\n * did in `buildUiApps` (`turbo run build <pkg>^...`) before spawning this child. `--only` drops those\n * `^build` tasks from the graph, so the redundant walk never happens. `--output-logs=new-only` collapses\n * any stray cache-hit replay, `--no-update-notifier` drops turbo's \"Update available\" banner, and\n * `--ui=stream` pins line-oriented output (turbo picks it anyway off a pipe, but it is cheap to be\n * explicit, and it is what `parseTurboDevLine`'s `<pkg>:dev:` prefix contract depends on).\n *\n * Detached \u2192 its own process group; reaped as a group (SIGTERM\u2192SIGKILL) via {@link superviseChild}.\n */\nimport { spawn } from 'node:child_process'\nimport process from 'node:process'\nimport type { Readable } from 'node:stream'\n\nimport { superviseChild } from './managed-child.js'\nimport type { ManagedChild, UnexpectedExitHandler } from './managed-child.js'\n\n/** Handle to the running `turbo run dev` child; `kill()` reaps the whole process group. */\nexport type UiDevHandle = ManagedChild\n\n/** Injectable spawn seam so tests run the orchestrator without a real turbo child. */\nexport type UiDevFactory = (opts: UiDevOptions) => UiDevHandle\n\n/** One framework output line, already stripped of turbo's `<pkg>:dev:` prefix. */\nexport interface TurboDevLine {\n /** The turbo package name that emitted the line (e.g. `website-ui`). */\n pkg: string\n /** The framework's own text, ANSI-stripped. */\n text: string\n /** Severity, read out of turbo's own line format \u2014 see {@link turboLineLevel}. */\n level: 'info' | 'error'\n}\n\n/**\n * Vocabulary a framework uses to announce a failure on turbo's stream.\n *\n * **Why this reads the line's text, when nothing else in the design does.** Under `--ui=stream` turbo\n * relays each task's stdout AND stderr onto its OWN stdout \u2014 measured, not assumed: a task writing one\n * line to each fd yields both lines on turbo's fd 1, and fd 2 carries only turbo's chrome. So the fd\n * that would otherwise DECLARE severity does not survive the relay: `child.stderr` never sees a single\n * framework line, and a level counter built on it would be structurally, permanently zero. The panel\n * would then show a green `client/ui` row over a UI that fails to compile \u2014 the one failure mode this\n * whole design exists to prevent.\n *\n * This is not the residual-bucket guess that was rejected. That one asked \"what IS this line?\" of an\n * unknown channel and promoted whatever it could not identify. This asks a narrower question of a KNOWN,\n * declared format: turbo's `<pkg>:dev:` prefix contract is the same one already relied on to route the\n * line to its package. The rule: classify only within a format you know; never guess about one you\n * don't.\n *\n * Deliberately small and anchored. A miss costs an uncounted error (the line is still in the log); a\n * false positive costs a red row over a healthy app, which is worse \u2014 so patterns must be specific, and\n * every addition needs a real line that motivates it.\n */\nconst ERROR_VOCABULARY = [\n /^error\\b/i,\n // The glyphs get no `\\b`: they are not word characters, so there is NO word boundary between `\u2718` and\n // the space that follows it \u2014 `\u2718\\b` never matches the esbuild/vite lines it was aimed at. Anchoring\n // them alone is both correct and safe, since a line can only start with one by way of announcing a\n // failure.\n /^[\u2718\u2716\u00D7]/,\n /^\\[vite\\][^\\n]*\\berror\\b/i,\n // A thrown JS error's first line (`TypeError: x is not a function`). ANCHORED, unlike a bare\n // `/\\berror:/i`, which fires on any line merely containing `error:` \u2014 inside a URL, a JSON blob, or a\n // dev script's own echo.\n /^\\w*Error: /,\n // `tsc`/`vue-tsc --watch` diagnostics: `src/foo.ts(3,5): error TS2322: \u2026`. Matched explicitly because\n // NONE of the other patterns reach it \u2014 the line starts with a path, and `error` is followed by a\n // space, not a colon. Without this a `--watch` type-check task can fail to compile while its row on\n // the panel stays green, which is the exact lie this counter exists to prevent.\n /\\berror TS\\d+\\b/,\n // Specific vite/esbuild failures, NOT a bare `^failed to`: that also matches the entirely benign\n // `Failed to load source map for \u2026`, and a red row over a healthy app costs more trust than an\n // uncounted error costs information (the line is still in the log either way).\n /^Failed to (?:resolve|load url|parse|compile)\\b/i,\n /^(?:ENOENT|EADDRINUSE|ECONNREFUSED)\\b/,\n /pre-transform error/i,\n /\\b(?:build|transform|compilation) failed\\b/i,\n // NOTE: stack frames (` at Module._compile (\u2026)`) are deliberately NOT here. Matching them would\n // turn one thrown exception into one error per FRAME \u2014 a 20-frame stack reading as `\u26A0 21`, at which\n // point the number stops being a count of anything. The throw's first line is already matched above.\n] as const\n\n/**\n * Severity of one framework line, from turbo's own relayed format. `error` when the framework announced\n * a failure in its output; `info` otherwise. See {@link ERROR_VOCABULARY} for why this cannot come from\n * the file descriptor.\n */\nexport const turboLineLevel = (text: string): 'info' | 'error' => {\n return ERROR_VOCABULARY.some((pattern) => {\n return pattern.test(text)\n })\n ? 'error'\n : 'info'\n}\n\nexport interface UiDevOptions {\n /** UI app package names; each becomes an exact `--filter=<pkg>` (turbo runs its `dev` task). */\n packageNames: string[]\n /** Consumer repo cwd the child runs in. */\n cwd: string\n /**\n * Concurrency cap. Must be \u2265 the number of selected UI `dev` tasks (they're persistent) \u2014\n * turbo hard-errors when persistent tasks exceed concurrency (default 10).\n */\n concurrency: number\n /**\n * Extra env merged over `process.env` for the turbo child (Layer B passes `INFRA_KIT_UI_PORTS`).\n * `turbo \u2026 --env-mode=loose` passes the full env through to each vite `dev` task. Omit \u2192 inherit only.\n */\n env?: Record<string, string>\n /** Raw child output (turbo chrome included, ANSI intact) appended verbatim to the runner log. */\n appendLog?: (text: string) => void\n /** One call per framework output line; turbo's own chrome is filtered out first. */\n onLine?: (line: TurboDevLine) => void\n /**\n * Called if the child dies on its own (not via `kill()`): every UI's live reload silently stops, so\n * the runner surfaces it. Optional so the injected test factory can ignore it.\n */\n onUnexpectedExit?: UnexpectedExitHandler\n}\n\n/* eslint-disable no-control-regex, sonarjs/no-control-regex -- terminal escapes and control chars are, by definition, control chars. */\n/** An OSC sequence: ESC `]` ... terminated by BEL or ST. Frameworks emit these for terminal hyperlinks. */\nconst OSC_ESCAPE = /\\u001B\\][\\s\\S]*?(?:\\u0007|\\u001B\\\\)/g\n/** A CSI escape: ESC `[`, parameter bytes, intermediate bytes, final byte. Covers every SGR colour. */\nconst CSI_ESCAPE = /\\u001B\\[[0-9;?]*[\\u0020-\\u002F]*[\\u0040-\\u007E]/g\n/**\n * Every remaining C0 control char except TAB. CR is the dangerous one: written straight to a TTY it\n * snaps the cursor to column 0, smearing the row and the pinned footer below it - exactly the corruption\n * that piping the child was meant to end. LF cannot appear here: `pumpLines` already split on it.\n */\nconst CONTROL_CHARS = /[\\u0000-\\u0008\\u000B-\\u001F\\u007F]/g\n/* eslint-enable no-control-regex, sonarjs/no-control-regex */\n\n/**\n * Make a raw child line safe for the renderer's tail: drop terminal escapes (which would fight the\n * scroll region) and stray control chars (which would move the cursor), leaving plain text the renderer\n * styles itself. Order matters - OSC is matched before the bare-control sweep can eat its leading ESC.\n */\nexport const stripAnsi = (text: string): string => {\n return text.replace(OSC_ESCAPE, '').replace(CSI_ESCAPE, '').replace(CONTROL_CHARS, '')\n}\n\n/** Turbo's per-task line prefix under `--ui=stream`: `<pkg>:dev:`. */\nconst TASK_PREFIX = /^([^\\s:]+):dev:[ \\t]?/\n\n/**\n * Per-task bookkeeping turbo emits before the framework speaks: the cache verdict\n * (`cache bypass, force executing <hash>`) and the echoed command (`$ pnpm exec vike dev`).\n * Neither is dev signal \u2014 both are already implied by the app appearing in the ready header.\n */\nconst isTaskChrome = (text: string): boolean => {\n return /^cache (?:bypass|hit|miss)/.test(text) || text.startsWith('$ ')\n}\n\n/**\n * One raw turbo line \u2192 the framework line to surface, or `null` to drop it.\n *\n * Turbo's run chrome (`\u2022 Packages in scope: \u2026`, `\u2022 Running dev in 1 packages`, `\u2022 Remote caching\n * disabled`, the closing task summary, pnpm's `ELIFECYCLE` teardown) carries no `<pkg>:dev:` prefix.\n * Requiring that prefix drops all of it under one rule instead of chasing a brittle denylist, and what\n * survives is exactly the framework's own stdout/stderr \u2014 including its errors.\n *\n * @example\n * parseTurboDevLine('website-ui:dev: ready in 384 ms') // => { pkg: 'website-ui', text: 'ready in 384 ms' }\n * parseTurboDevLine('\u2022 Remote caching disabled') // => null\n */\nexport const parseTurboDevLine = (raw: string): TurboDevLine | null => {\n const line = stripAnsi(raw).trimEnd()\n const match = TASK_PREFIX.exec(line)\n\n if (match == null) {\n return null\n }\n\n const text = line.slice(match[0].length)\n\n if (text.trim() === '' || isTaskChrome(text.trim())) {\n return null\n }\n\n return { pkg: match[1]!, text, level: turboLineLevel(text) }\n}\n\n/**\n * Cap for the newline-less carry buffer. A framework that renders progress with bare CR and never a LF\n * would otherwise grow `pending` without bound for the life of the dev session.\n */\nconst MAX_PENDING_CHARS = 64 * 1024\n\n/**\n * Split a piped stream into lines: tee every chunk verbatim to the log, and route each complete\n * framework line to `onLine`. A trailing partial line is flushed on `end`, so a framework that exits\n * without a final newline never swallows its last (often the most interesting) line.\n *\n * The `data` listener is attached unconditionally \u2014 NOT gated on `onLine`/`appendLog` being set. A piped\n * child whose stdout is never read blocks once the OS pipe buffer fills, so draining is the contract\n * here; the sinks are merely optional consumers of what we drain.\n */\nconst pumpLines = (stream: Readable | null, opts: Pick<UiDevOptions, 'appendLog' | 'onLine'>): void => {\n if (stream == null) {\n return\n }\n\n let pending = ''\n\n const emit = (raw: string): void => {\n const parsed = parseTurboDevLine(raw)\n\n if (parsed != null) opts.onLine?.(parsed)\n }\n\n stream.setEncoding('utf-8')\n stream.on('data', (chunk: string) => {\n opts.appendLog?.(chunk)\n pending += chunk\n\n const lines = pending.split('\\n')\n\n pending = lines.pop() ?? ''\n for (const raw of lines) {\n emit(raw)\n }\n\n if (pending.length > MAX_PENDING_CHARS) {\n emit(pending)\n pending = ''\n }\n })\n stream.on('end', () => {\n if (pending === '') return\n emit(pending)\n pending = ''\n })\n // A readable that emits `error` with no listener THROWS, taking the whole dev session down. The child's\n // lifecycle is already owned by `superviseChild`, so a read error (pty EIO, a pipe torn down mid-SIGKILL)\n // only needs recording, never escalation.\n stream.on('error', (err: Error) => {\n opts.appendLog?.(`[infra-kit] ui dev stream error: ${err.message}\\n`)\n })\n}\n\n/**\n * Default factory: spawn `pnpm exec turbo run dev --filter=<pkg> \u2026 --only` detached with piped stdio,\n * fan its output into the runner log + the renderer's tail, and reap the process group on `kill()`.\n *\n * Exact `--filter=<pkg>` (no `...`) selects only the UI packages, so an API app that also defines a\n * `dev` task is never picked up.\n */\nexport const defaultUiDevFactory: UiDevFactory = ({\n packageNames,\n cwd,\n concurrency,\n env,\n appendLog,\n onLine,\n onUnexpectedExit,\n}) => {\n const filters = packageNames.map((name) => {\n return `--filter=${name}`\n })\n const child = spawn(\n 'pnpm',\n [\n 'exec',\n 'turbo',\n 'run',\n 'dev',\n ...filters,\n `--concurrency=${concurrency}`,\n '--env-mode=loose',\n '--only',\n '--output-logs=new-only',\n '--no-update-notifier',\n '--ui=stream',\n ],\n { cwd, detached: true, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, ...env } },\n )\n\n pumpLines(child.stdout, { appendLog, onLine })\n pumpLines(child.stderr, { appendLog, onLine })\n\n return superviseChild(child, undefined, onUnexpectedExit)\n}\n"],
5
- "mappings": "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",
6
- "names": ["Command", "process", "pathToFileURL", "path", "process", "makeLeaf", "command", "buildNode", "commands", "depth", "leftCount", "left", "right", "buildCmuxLayout", "constants", "process", "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", "reapSnapshot", "snapshot", "childrenOf", "siblings", "groups", "seen", "queue", "i", "child", "snapshotProcRows", "snapshotProcRowsAsync", "resolve", "error", "stdout", "isChildOf", "parentPid", "doomedGroupsOf", "ownPgid", "groupAlive", "signalGroups", "pgids", "signal", "killDescendantGroupsNow", "sleep", "ms", "waitForExit", "timeoutMs", "deadline", "alive", "superviseChild", "graceMs", "onUnexpectedExit", "killing", "exited", "sampler", "stopSampling", "sample", "next", "reportUnexpected", "detail", "code", "doomed", "stragglers", "late", "forced", "HANDLED_SIGNALS", "SIGNAL_NUMBERS", "constants", "FALLBACK_SIGNO", "exitCodeForSignal", "signal", "TEARDOWN_DEADLINE_MS", "defaultExit", "code", "process", "defaultRegister", "handler", "defaultSetTimer", "ms", "timer", "writeStderr", "message", "writeTeardownFailure", "error", "detail", "registerSignalShutdown", "onSignal", "exit", "register", "forceReap", "killDescendantGroupsNow", "teardownDeadlineMs", "setTimer", "describeStall", "fileReport", "firstSignal", "cancelDeadline", "handle", "report", "buildPaneCommands", "panes", "watch", "app", "targets", "paneTargetsByApp", "presetDef", "byApp", "key", "selectApiApps", "root", "include", "apps", "discoverApiApps", "logDevWorkspace", "ref", "logger", "registerShutdown", "registerSignalShutdown", "signal", "closeCmuxDevWorkspace", "runCmuxDevServer", "options", "findMonorepoRoot", "process", "normalizeAppInclude", "targetsByApp", "commands", "layout", "buildCmuxLayout", "title", "openCmuxDevWorkspace", "heartbeat", "process", "writeStderr", "message", "process", "formatFault", "event", "error", "kept", "detail", "defaultOnFault", "defaultFileFault", "defaultOnFatal", "reason", "killDescendantGroupsNow", "defaultRegister", "handler", "registerCrashBarrier", "onFault", "register", "isTerminalDead", "fileFault", "onFatal", "handle", "DEFAULT_RELEASE_SLUG", "DEV_CONTEXT_WIRE_VERSION", "loadDev", "slugifyHostLabel", "slugifyRelease", "chokidar", "exec", "execFileSync", "fs", "net", "os", "path", "process", "util", "execFile", "fs", "path", "promisify", "execFileAsync", "promisify", "execFile", "readPackageName", "dir", "parsed", "defaultDryRunner", "root", "packageName", "stdout", "names", "t", "p", "readPackageDirs", "packageNameByDir", "dirByName", "distDir", "getPackageDistDirs", "name", "buildClosureMap", "apps", "dryRunner", "dependentsByPackageDir", "closures", "app", "closure", "pkgName", "dependents", "packageDebounceKey", "packageDir", "selectPackageRestartTargets", "closureMap", "a", "NOT_LAUNCHED", "interpolateCloud", "template", "packageName", "env", "resolveFallback", "route", "findDegradedRoutes", "input", "ui", "spec", "finding", "judgeRoute", "wanted", "running", "reasons", "intendedLocal", "fallback", "failure", "cloudTarget", "describeDestination", "d", "to", "wasAttempted", "remedy", "formatPairingRefusal", "degraded", "target", "lines", "owner", "AsyncLocalStorage", "storage", "runAttributed", "service", "fn", "enterAttribution", "currentService", "Buffer", "fs", "path", "process", "process", "TRANSIENT_CODES", "dead", "isTerminalDead", "streamName", "stream", "index", "installTerminalLiveness", "streams", "onDeath", "fired", "attached", "name", "listener", "raw", "error", "protoWrite", "process", "rawStdoutWrite", "chunk", "isTerminalDead", "panelStream", "process", "target", "property", "receiver", "chunk", "rawStdoutWrite", "value", "emptyStats", "safeSegment", "raw", "safe", "part", "resolveLogDir", "INFRA_KIT_SESSION_VAR", "session", "path", "getCacheRoot", "logFileName", "service", "GC_MAX_AGE_MS", "isPidAlive", "name", "pid", "error", "gcOldLogDirs", "devRoot", "now", "entries", "fs", "entry", "dir", "DEFAULT_MAX_LOG_BYTES", "MAX_LOG_BYTES_VAR", "resolveMaxLogBytes", "DevLogSink", "maxBytes", "link", "cached", "fd", "text", "meta", "stats", "line", "size", "Buffer", "process", "util", "CONSOLE_LEVELS", "installOutputIntercept", "sink", "fallbackService", "currentService", "live", "serviceFor", "capture", "text", "level", "originalConsole", "method", "args", "patchStream", "stream", "original", "pending", "flush", "chunk", "rest", "lines", "line", "callback", "arg", "restoreStdout", "restoreStderr", "DEFAULT_PREFIX_URL", "parsePortString", "raw", "n", "resolvePreferredPort", "appName", "env", "devConfig", "prefixedKey", "fromPrefixed", "parsePortString", "fromPort", "resolvePrefixUrl", "DEFAULT_PREFIX_URL", "findPortConflicts", "apps", "ports", "a", "duplicatePorts", "port", "index", "conflictingApps", "Logger", "fastify", "fs", "path", "process", "pathToFileURL", "parseYaml", "isAddressInUse", "error", "DEFAULT_LAMBDA_TIMEOUT_MS", "resolveLambdaTimeoutMs", "raw", "process", "isRequestLogEnabled", "ServerlessLocalRun", "serverConfig", "Logger", "fastify", "request", "reply", "serviceTag", "enterAttribution", "onRequestLog", "ms", "requestPath", "boundPort", "preferred", "address", "_request", "serverlessYmlPath", "fileContents", "data", "parseYaml", "p", "funcDef", "element", "http", "url", "urlAction", "validMethods", "method", "routeKey", "parts", "filepath", "handler", "controllerPath", "fileUrl", "pathToFileURL", "importHandler", "action", "runAttributed", "traceLogger", "controller", "handlerFn", "retVal", "responseBody", "requestBody", "queryParams", "pathParameters", "headers", "httpMethod", "path", "startTime", "timeoutMs", "datePart", "_error", "_result", "_messageOrObject", "spawn", "fs", "buildTurboWatchFilters", "depInclusive", "depClosure", "name", "defaultTurboWatchFactory", "cwd", "logFile", "onUnexpectedExit", "filters", "out", "child", "spawn", "superviseChild", "spawn", "process", "ERROR_VOCABULARY", "turboLineLevel", "text", "pattern", "OSC_ESCAPE", "CSI_ESCAPE", "CONTROL_CHARS", "stripAnsi", "TASK_PREFIX", "isTaskChrome", "parseTurboDevLine", "raw", "line", "match", "MAX_PENDING_CHARS", "pumpLines", "stream", "opts", "pending", "emit", "parsed", "chunk", "lines", "err", "defaultUiDevFactory", "packageNames", "cwd", "concurrency", "env", "appendLog", "onLine", "onUnexpectedExit", "filters", "name", "child", "spawn", "process", "superviseChild", "RUNNER_SERVICE", "TURBO_SERVICE", "WATCH_SERVICE", "homeShorten", "p", "home", "os", "packageList", "apps", "a", "execFn", "util", "exec", "launchScript", "script", "logFn", "stderr", "error", "err", "logSink", "appendRunnerLog", "text", "getFreePort", "resolve", "reject", "srv", "net", "address", "port", "isForeign", "outcome", "foreignFrom", "res", "describeForeign", "answer", "type", "what", "PROBE_TIMEOUT_MS", "VITE_PING_HEADERS", "probeUrl", "target", "resolveHop", "location", "base", "probeApi", "url", "signal", "probeUi", "next", "hop", "defaultHealthProbe", "HELPER_PACKAGES", "isBelowVersion", "version", "floor", "parse", "v", "part", "b", "i", "left", "right", "manifestDirs", "repoRoot", "dirs", "app", "declaresPackage", "dir", "name", "pkg", "field", "findHelperDir", "fromDir", "segments", "candidate", "parent", "isWorkspaceLinked", "helperDir", "real", "root", "safeRealpath", "assertFloorAt", "assertHelperVersionFloor", "declaredIn", "resolved", "found", "readAppRelease", "cwd", "branch", "execFileSync", "slug", "slugifyRelease", "DEFAULT_RELEASE_SLUG", "errorReason", "firstLine", "DevServerRunner", "_DevServerRunner", "DEFAULT_DEV_PROXY_PORT", "options", "runBuild", "turboWatchFactory", "defaultTurboWatchFactory", "uiDevFactory", "defaultUiDevFactory", "dryRunner", "renderer", "healthProbe", "proxy", "createPortlessDriver", "sink", "DevLogSink", "ownsTerminal", "installOutputIntercept", "currentService", "DevRenderer", "process", "findMonorepoRoot", "devConfig", "discoverApiApps", "getInfraKitConfig", "normalizeAppInclude", "appName", "resolvePreferredPort", "resolvePrefixUrl", "devPresets", "def", "available", "bootStart", "include", "watch", "uiApps", "apiAppsAll", "uiAppsAll", "wantedLocalPkgs", "presetProxy", "deriveTargetLabel", "formatPairingRefusal", "discoverUiApps", "resolvePreset", "apiNames", "t", "uiNames", "passesInclude", "watchDepsByApp", "wanted", "uis", "ui", "dev", "loadDev", "overrides", "routes", "route", "spec", "findDegradedRoutes", "reason", "INFRA_KIT_ENV_VAR", "bootLabel", "map", "filters", "names", "uiPortEnv", "caEnv", "readCaPath", "tagByPackage", "level", "tag", "detail", "explicitApps", "duplicatePorts", "conflictingApps", "findPortConflicts", "buildCmd", "buildError", "started", "bin", "formatPortlessCommand", "packageName", "appDir", "release", "label", "slugifyHostLabel", "server", "ServerlessLocalRun", "method", "reqPath", "status", "ms", "window", "boundPort", "alias", "fragment", "DEV_CONTEXT_WIRE_VERSION", "tmp", "work", "run", "r", "e", "f", "entry", "failedIdx", "resolveEndpointUrl", "targets", "idx", "outcomes", "restarted", "recovered", "previous", "probed", "healthy", "allHealthy", "buildClosureMap", "engine", "consequence", "kind", "appDistDirs", "getAppDistDirs", "packageDistDirs", "getPackageDistDirs", "allDistDirs", "usePoll", "watcher", "chokidar", "filePath", "change", "classifyDistChange", "selectPackageRestartTargets", "packageDebounceKey", "key", "existing", "timer", "fresh", "threshold", "wasDown", "why", "intervalMs", "summary", "now", "byTag", "ref", "endpoint", "errors", "running", "d", "cutoff", "times", "at", "elapsedMs", "endpoints", "uiEndpoints", "uiRefs", "assigned", "appCount", "pkgCount", "selectDevUi", "PersistentInkDevUi", "createSafeStream", "panelStream", "runner", "FATAL_EXIT_CODE", "FATAL_TEARDOWN_DEADLINE_MS", "toPresetDef", "targets", "key", "explainTargetKey", "splitList", "value", "parts", "s", "toDevServerOptions", "raw", "resolveSelfOptions", "options", "resolveSelfAppName", "process", "defaultFatalTimer", "handler", "ms", "timer", "createFatalHandler", "getRunner", "forceReap", "killDescendantGroupsNow", "exit", "code", "setTimer", "deadlineMs", "fired", "reason", "runner", "cancel", "runDevServer", "rawOptions", "isCmuxAvailable", "runCmuxDevServer", "onFatal", "liveness", "installTerminalLiveness", "stream", "error", "run", "boundRunner", "registerCrashBarrier", "event", "formatFault", "registerSignalShutdown", "signal", "rawStdoutWrite", "text", "shouldRunWizard", "tty", "json", "wizardToOptions", "result", "runDevServerCli", "runDevWizard", "warmed", "isPromptCancellation", "parseAndRun", "argv", "program", "Command", "pathToFileURL"]
4
+ "sourcesContent": ["/**\n * Long-running CLI entry for the local dev-server. Owns flag parsing and wires signal\n * handling to `src/dev/signal-shutdown` \u2014 the orchestrator (`src/dev/dev-server`) stays\n * signal- and exit-agnostic. Kept off the eager cli.js graph: entry/cli.ts reaches this\n * module (and the fastify/chokidar it pulls in) only via `await import(...)`, so\n * those heavy deps never load on the machine command paths.\n */\nimport { Command } from 'commander'\nimport process from 'node:process'\nimport { pathToFileURL } from 'node:url'\n\nimport { runCmuxDevServer } from 'src/dev/cmux-dev'\nimport { formatFault, registerCrashBarrier } from 'src/dev/crash-barrier'\nimport { run } from 'src/dev/dev-server'\nimport type { DevServerOptions, DevServerRunner } from 'src/dev/dev-server'\nimport type { WizardResult } from 'src/dev/dev-wizard-run'\nimport { resolveSelfAppName } from 'src/dev/discovery'\nimport { rawStdoutWrite } from 'src/dev/log-sink'\nimport { killDescendantGroupsNow } from 'src/dev/managed-child'\nimport { explainTargetKey } from 'src/dev/presets'\nimport { registerSignalShutdown } from 'src/dev/signal-shutdown'\nimport { installTerminalLiveness } from 'src/dev/terminal-liveness'\nimport { isCmuxAvailable } from 'src/integrations/cmux'\nimport { isPromptCancellation } from 'src/lib/errors/is-prompt-cancellation'\nimport type { DevPreset } from 'src/lib/infra-kit-config'\n\n/**\n * Exit code when the session dies because its own stdio is unwritable. Not 0 \u2014 the process did not stop\n * voluntarily \u2014 and not `128 + signo`, because no signal was necessarily involved: a full disk under\n * `nohup ik dev > out.log` reaches this path with `ENOSPC` and nobody sent anything.\n */\nconst FATAL_EXIT_CODE = 1\n\n/**\n * How long the fatal path waits for a graceful teardown before SIGKILLing the descendant groups and\n * leaving. Mirrors `signal-shutdown`'s deadline, and for the same reason: on this path there is by\n * definition no operator watching, so \"wait forever\" means \"spin forever\".\n */\nconst FATAL_TEARDOWN_DEADLINE_MS = 20_000\n\n/** Raw option object as produced by Commander (comma-joined strings). */\nexport interface DevCliOptions {\n watch?: boolean\n app?: string\n /**\n * Comma-separated `<app>/<part>` target keys (`--target=client/api,client/ui`). The part-level\n * selector `--app` cannot express: `--app=client` expands to every part `client` has. Same grammar as\n * a `devServersPresets` key, so a wizard selection round-trips into a command you can paste.\n */\n target?: string\n /** Named preset positional (`infra-kit dev <preset>`); selects launch targets from `devServersPresets`. */\n preset?: string\n cmux?: boolean\n self?: boolean\n verbose?: boolean\n /** Print each app's registered routes at startup (opt-in; off keeps the calm default screen). */\n routes?: boolean\n /**\n * Commander's negated flag: `true` by default, `false` only when `--no-ui-health` was passed. Off means\n * the frontends are never probed and their rows carry no health dot.\n */\n uiHealth?: boolean\n}\n\n/**\n * Turn `--target=<app>/<part>,\u2026` into the in-memory preset the runner already understands, or\n * `undefined` when the flag is absent (leaving `preset`/`*` resolution untouched). Validated against the\n * same grammar as a `devServersPresets` key, but with a message that names the FLAG \u2014 a user typing\n * `--target=client` must not be told about `devServersPresets`.\n */\nconst toPresetDef = (targets: string[] | null): DevPreset | undefined => {\n if (targets == null) return undefined\n\n for (const key of targets) {\n if (explainTargetKey(key) !== null) {\n throw new Error(`infra-kit dev: invalid --target \"${key}\" (expected \"<app>/api\" or \"<app>/ui\").`)\n }\n }\n\n return {\n apps: Object.fromEntries(\n targets.map((key) => {\n return [key, {}]\n }),\n ),\n }\n}\n\n/** Split a comma-separated flag value into a trimmed, non-empty list (`null` when unset/empty). */\nconst splitList = (value: string | undefined): string[] | null => {\n if (value == null) return null\n\n const parts = value\n .split(',')\n .map((s) => {\n return s.trim()\n })\n .filter(Boolean)\n\n return parts.length > 0 ? parts : null\n}\n\n/**\n * Map raw Commander flags to the orchestrator's typed options. Shared by the\n * `infra-kit dev` subcommand and the standalone entry so the two never diverge.\n * `self` is passed through as-is here \u2014 it's resolved to `include` later, in\n * `runDevServer`, where `--app` (if also given) is allowed to win.\n */\nexport const toDevServerOptions = (raw: DevCliOptions): DevServerOptions => {\n return {\n watch: raw.watch ?? false,\n include: splitList(raw.app),\n preset: raw.preset,\n presetDef: toPresetDef(splitList(raw.target)),\n cmux: raw.cmux ?? false,\n self: raw.self ?? false,\n verbose: raw.verbose ?? false,\n routes: raw.routes ?? false,\n uiHealth: raw.uiHealth ?? true,\n }\n}\n\n/**\n * Resolve `--self` into an `include` list, without mutating `options`. `--app` (an\n * explicit `include`) wins over `--self` when both are given \u2014 self is only a\n * convenience default for scripts that don't want to hardcode their app name.\n * Lets `resolveSelfAppName`'s error (not inside `apps/<app>/...`) propagate as-is;\n * the caller's top-level catch turns it into a clean message + non-zero exit.\n */\nconst resolveSelfOptions = (options: DevServerOptions): DevServerOptions => {\n if (!options.self || options.include) {\n return options\n }\n\n return { ...options, include: [resolveSelfAppName(process.cwd())] }\n}\n\n/**\n * Seams for {@link createFatalHandler}. `exit` and `forceReap` mirror `signal-shutdown`'s, and for the same\n * reason: the real ones kill the test runner and SIGKILL its children.\n */\nexport interface FatalHandlerDeps {\n /**\n * The runner \u2014 read LATE, on every call, never captured. It is `null` for the whole of boot (all of it\n * happens inside `run()`), and that is the branch the handler exists to get right.\n */\n getRunner: () => { shutdown: () => Promise<void>; fileFault: (detail: string) => void } | null\n /** SIGKILL the descendant process groups. Synchronous by contract \u2014 the process exits on the next line. */\n forceReap?: () => void\n exit?: (code: number) => void\n /** Timer seam returning its cancel; defaults to an `unref`'d `setTimeout`. */\n setTimer?: (handler: () => void, ms: number) => () => void\n deadlineMs?: number\n}\n\nconst defaultFatalTimer = (handler: () => void, ms: number): (() => void) => {\n const timer = setTimeout(handler, ms)\n\n timer.unref()\n\n return (): void => {\n clearTimeout(timer)\n }\n}\n\n/**\n * The one handler for \"our own stdio is unwritable\" \u2014 from the liveness listener (with the errno) and, as a\n * backstop, from the crash barrier.\n *\n * Once-only. Terminal death is ONE event arriving through as many as three channels (the stdio `'error'`\n * event, the kernel's SIGHUP, a fault raised while the streams are already dead). `shutdown()` is memoized\n * so they may all call it; the reap-and-exit here must still happen exactly once.\n *\n * It never PRINTS: printing onto a dead stream is what produced the fault, so the report is FILED into the\n * sink \u2014 the only channel a post-mortem can still read.\n *\n * The `runner == null` branch is not a corner case, it is a REGRESSION GUARD. Today an EIO during boot\n * correctly kills the process (the barrier is not installed yet). A liveness listener that merely swallowed\n * the `'error'` would make boot silently survive a dead terminal \u2014 strictly worse. So boot reaps and exits,\n * synchronously: anything deferred to a later tick does not survive `process.exit`.\n */\nexport const createFatalHandler = ({\n getRunner,\n forceReap = killDescendantGroupsNow,\n exit = (code: number): void => {\n process.exit(code)\n },\n setTimer = defaultFatalTimer,\n deadlineMs = FATAL_TEARDOWN_DEADLINE_MS,\n}: FatalHandlerDeps): ((reason: string) => void) => {\n let fired = false\n\n return (reason: string): void => {\n if (fired) return\n fired = true\n\n const runner = getRunner()\n\n if (runner == null) {\n forceReap()\n exit(FATAL_EXIT_CODE)\n\n return\n }\n\n runner.fileFault(`\\n\u2717 dev-server exiting: ${reason}\\n`)\n\n // Bounded, for the same reason `signal-shutdown` is: on this path nobody is watching, so a teardown\n // that wedges spins forever.\n const cancel = setTimer(() => {\n forceReap()\n exit(FATAL_EXIT_CODE)\n }, deadlineMs)\n\n // The trailing `.catch` is NOT redundant with `shutdown()`'s own. `shutdown()` attaches its handler to\n // the memoized `this.teardown`; `.finally()` returns a NEW, derived promise, and a derived promise\n // carries its own rejection state. `doShutdown()` can reject (`watcher.close`, `turboWatch.kill` and\n // `uiDev.kill` are unguarded), so without this the derived promise rejects unhandled \u2014 on the one path\n // where an `unhandledRejection` feeds straight back into the barrier that called us.\n runner\n .shutdown()\n .finally(() => {\n cancel()\n exit(FATAL_EXIT_CODE)\n })\n .catch(() => {})\n }\n}\n\n/**\n * Start the dev-server, then wait for an OS signal and shut every server down\n * cleanly before exiting. Signal handling and exit are delegated to\n * {@link registerSignalShutdown}: a second signal force-quits, a rejected teardown\n * is logged, and the process exits `128 + signo` rather than a dishonest `0`.\n */\nexport const runDevServer = async (rawOptions: DevServerOptions): Promise<void> => {\n const options = resolveSelfOptions(rawOptions)\n\n // `--cmux`: one workspace, one pane per app. `runCmuxDevServer` owns its own\n // signal handling and never returns, so return before wiring the in-process\n // handlers below. Fall through to single-process dev when cmux isn't installed.\n if (options.cmux) {\n if (await isCmuxAvailable()) {\n await runCmuxDevServer(options)\n\n return\n }\n\n process.stdout.write('cmux not available; falling back to single-terminal dev\\n')\n }\n\n // A settable target, not a `const`: liveness must be armed BEFORE `run()`, because all of boot happens\n // inside it (apps built, turbo and vite spawned, Ink mounted) \u2014 minutes of build during which a developer\n // walks away and closes the window. But `onDeath` can then fire with no runner to tear down, so the fatal\n // handler branches on it (and reads it LATE, through the getter).\n //\n // Installed HERE \u2014 below the `--cmux` early return above, alongside `registerCrashBarrier`, which is\n // deliberately on the same side of it (each pane is its own process). At the top of `runDevServer` the\n // cmux PARENT would get a listener whose `runner` stays `null` forever, and its `onFatal` would take the\n // boot branch: `killDescendantGroupsNow()` + exit, reaping the entire workspace.\n let runner: DevServerRunner | null = null\n\n const onFatal = createFatalHandler({\n getRunner: () => {\n return runner\n },\n })\n\n const liveness = installTerminalLiveness({\n // The errno, never a story. A file-backed stdout emits `'error'` on `ENOSPC` too, so \"the terminal is\n // gone\" would be a lie in the exact scenario (a disk-fill) this exists to fix.\n onDeath: (stream, error) => {\n onFatal(`stdio unwritable: ${stream} ${error.code}`)\n },\n })\n\n runner = await run(options)\n\n // In-process backends share this event loop; a handler's escaped async path would otherwise terminate\n // the whole session. Installed only on the single-process path (the cmux path returned above, each pane\n // being its own process) and only after `run()` succeeds, so a boot failure still exits honestly.\n //\n // `onFault` is not optional decoration. The dev-server owns `process.stderr` for the life of a TTY\n // session (every log line goes to a per-service file, nothing prints), and the barrier's default\n // reporter is a plain stderr write \u2014 so a crash would be silently FILED while the panel kept showing\n // `\u25CF ok` and `\u26A0 0`. Routing it through the runner both counts it (the row turns red) and punches it\n // onto the terminal through the panel's bypass.\n //\n // `isTerminalDead` is the one thing that may turn a fault fatal, and it is a STREAM-IDENTITY question,\n // never an error-code one: a handler writing to a client socket that hung up throws `EPIPE` too, and\n // sniffing for that would let one closed browser tab kill the whole session.\n const boundRunner = runner\n\n registerCrashBarrier({\n onFault: (event, error) => {\n boundRunner.reportFault(formatFault(event, error))\n },\n isTerminalDead: liveness.isDead,\n fileFault: (event, error) => {\n boundRunner.fileFault(formatFault(event, error, false))\n },\n onFatal,\n })\n\n registerSignalShutdown({\n onSignal: async (signal) => {\n // Bypass, not `process.stdout.write`: the interceptor is still installed and suppressing at this\n // point, so a plain write would file this into a log and the user would see nothing after Ctrl-C.\n // Gated on the liveness latch, so a SIGHUP from a terminal that is already gone does not re-arm the\n // very write that killed it.\n rawStdoutWrite(`\\nReceived ${signal}, shutting down dev-server...\\n`)\n await boundRunner.shutdown()\n },\n // The seam that turns the deadline from a blunt force-quit into the instrument that says WHERE the\n // teardown wedged \u2014 the question the incident leaves open.\n describeStall: () => {\n return boundRunner.shutdownStage\n },\n fileReport: (text) => {\n boundRunner.fileFault(text)\n },\n })\n}\n\n/**\n * True when `infra-kit dev` was invoked BARE \u2014 no preset and no selection/mode flag \u2014 in an interactive\n * TTY (both stdin and stdout) and not `--json`. This is the ONLY condition that launches the wizard;\n * every flagged, piped, non-TTY, `--json`, or MCP invocation runs directly from the parsed flags, so no\n * existing script path changes behaviour.\n */\nexport const shouldRunWizard = (raw: DevCliOptions, tty: boolean, json: boolean): boolean => {\n // `--no-ui-health` is deliberately NOT in this list. It selects a diagnostic, not a run plan, and a flag\n // that quietly turns the picker into \"run the entire repo\" is a far bigger surprise than the one it would\n // avoid. The wizard carries it through instead (see `wizardToOptions`), so the user gets both.\n const bare = !raw.preset && !raw.app && !raw.self && !raw.cmux && !raw.watch && !raw.verbose && !raw.routes\n\n return bare && tty && !json\n}\n\n/** Map a wizard result to runner options: the cmux path uses `include`; otherwise the in-memory `presetDef`. */\nconst wizardToOptions = (result: WizardResult, raw: DevCliOptions): DevServerOptions => {\n return {\n watch: result.watch,\n cmux: result.cmux,\n include: result.include ?? null,\n preset: result.preset,\n presetDef: result.presetDef,\n self: false,\n verbose: false,\n routes: false,\n // The wizard asks about the run plan, never about health \u2014 so this rides through from the command line.\n // Dropping it here is what would make `--no-ui-health` silently probe anyway on the one path that\n // reaches the wizard.\n uiHealth: raw.uiHealth ?? true,\n }\n}\n\n/**\n * Entry for the `infra-kit dev` subcommand. On a bare TTY invocation it launches the interactive wizard\n * (lazily imported so its inquirer/config graph stays off every other path), then starts the server with\n * the assembled options; otherwise it runs directly from the parsed flags. A cancelled prompt (Ctrl-C /\n * Esc) or an empty selection exits cleanly without starting a server.\n */\nexport const runDevServerCli = async (raw: DevCliOptions, tty: boolean, json: boolean): Promise<void> => {\n if (shouldRunWizard(raw, tty, json)) {\n const { runDevWizard } = await import('src/dev/dev-wizard-run')\n\n // Warm the Ink dev-UI chunks BEFORE the wizard blocks on human input.\n //\n // `splitting: true` emits these as CONTENT-HASHED sibling chunks, and `selectDevUi` imports them only\n // AFTER the wizard returns (src/dev/dev-server.ts). That leaves a window as long as the user takes to\n // answer \u2014 minutes, realistically \u2014 in which those files are still un-imported. If the background\n // auto-updater installs a new version during it, npm UNLINKS the old hashes (measured: 7 chunk files\n // vanished across a real 0.1.133 \u2192 0.1.134 install), and the deferred import dies on\n // ERR_MODULE_NOT_FOUND the instant the user presses enter.\n //\n // Importing them up-front collapses that window to nothing: ESM caches by URL, so `selectDevUi`'s\n // later import resolves from memory even if the file is gone by then. This is a warm, not a use \u2014\n // failure is ignored, because `selectDevUi` will import them again and surface any real error there.\n // Costs nothing on any other path: it runs only on the bare interactive TTY that is about to load\n // this exact UI anyway.\n const warmed = Promise.all([import('src/tui/dev-ui/persistent-ink-dev-ui'), import('src/tui/safe-stderr')]).catch(\n () => {\n return undefined\n },\n )\n\n let result: WizardResult | null\n\n try {\n result = await runDevWizard()\n } catch (error) {\n if (isPromptCancellation(error)) return\n\n throw error\n }\n\n if (result == null) return\n\n // Settle the warm before starting: from here on the chunks are resident, so a `dist/` swap mid-session\n // can no longer strand the deferred import in `selectDevUi`.\n await warmed\n\n // The wizard only runs on a bare interactive TTY, so tty=true, json=false \u2014 thread them so `run()`\n // selects the Ink boot UI.\n await runDevServer({ ...wizardToOptions(result, raw), tty, json })\n\n return\n }\n\n // Thread the TTY / json signal so `run()` can pick the Ink boot UI (TTY, non-json) vs the plain renderer.\n await runDevServer({ ...toDevServerOptions(raw), tty, json })\n}\n\n/** Parse `node dist/dev-server.js ...` flags and start the server. */\nconst parseAndRun = async (argv: string[]): Promise<void> => {\n const program = new Command()\n\n program\n .name('infra-kit-dev-server')\n .description('Run local dev servers for the apps in a named devServersPresets preset (or all apps)')\n .argument('[preset]', 'Named preset from devServersPresets (omit to run every app)')\n .option('-w, --watch', 'Rebuild and restart on file save')\n .option('--app <names>', 'Further narrow to these app folder names (comma-separated)')\n .option(\n '--cmux',\n 'Run each app in its own cmux pane (one workspace, N panes; falls back to single terminal if cmux is unavailable)',\n )\n .option('--self', 'Run only the app of the current directory (infer from cwd; use inside apps/<app>/\u2026)')\n .option('-V, --verbose', 'Print full boot narration (default: quiet; full detail always in the session log)')\n .option('--routes', 'Print each app\u2019s registered METHOD /path routes at startup (default: off)')\n\n program.parse(argv)\n\n await runDevServer(toDevServerOptions({ ...program.opts<DevCliOptions>(), preset: program.args[0] }))\n}\n\n// Self-execute only when run directly (`node dist/dev-server.js`), not when the\n// `infra-kit dev` subcommand imports this module for its exported helpers.\nif (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {\n parseAndRun(process.argv).catch((error: unknown) => {\n // Message only, matching the `infra-kit dev` path: a bad preset name or target key is a\n // config mistake, and its stack frames are noise.\n console.error(error instanceof Error ? error.message : String(error))\n process.exit(1)\n })\n}\n", "/**\n * Resident `infra-kit dev --cmux` supervisor.\n *\n * Opens ONE cmux workspace with one pane per discovered backend API app, each\n * pane running the primitive `pnpm exec infra-kit dev --app=<name>` (single-app,\n * single-process). Stays resident: on SIGINT/SIGTERM it closes the workspace\n * (reaping every pane) and exits. The pure command/layout construction lives here\n * and in {@link file://./cmux-layout.ts} so it is unit-testable without a shell.\n */\nimport * as path from 'node:path'\nimport process from 'node:process'\n\nimport { closeCmuxDevWorkspace, openCmuxDevWorkspace } from 'src/integrations/cmux'\nimport { logger } from 'src/lib/logger'\n\nimport { buildCmuxLayout } from './cmux-layout.js'\nimport type { DevServerOptions } from './dev-server.js'\nimport { discoverApiApps, findMonorepoRoot, normalizeAppInclude } from './discovery.js'\nimport type { DiscoveredApiApp } from './discovery.js'\nimport { registerSignalShutdown } from './signal-shutdown.js'\n\n/** One cmux pane: the app it runs, and (optionally) the exact `<app>/<part>` targets selected for it. */\nexport interface PaneSpec {\n app: string\n /**\n * Exact target keys for this pane. When absent the pane falls back to `--app=<name>`, which expands to\n * EVERY part the app has (both the api and the ui glob targets) \u2014 right for a plain `--cmux`, wrong for a wizard selection\n * that deliberately unticked a part. `--app` has app-name granularity and cannot express `<app>/api`\n * alone; that is exactly what `--target` exists for.\n */\n targets?: string[]\n}\n\n/** Build the per-pane primitive command for each pane (append `--watch` when requested). */\nexport const buildPaneCommands = (panes: PaneSpec[], watch: boolean): string[] => {\n return panes.map(({ app, targets }) => {\n const selector = targets && targets.length > 0 ? `--target=${targets.join(',')}` : `--app=${app}`\n\n return `pnpm exec infra-kit dev ${selector}${watch ? ' --watch' : ''}`\n })\n}\n\n/** The concrete `<app>/<part>` keys of an in-memory preset, grouped by app. Glob keys (those whose app segment is a bare star) are skipped. */\nexport const paneTargetsByApp = (presetDef: DevServerOptions['presetDef']): Map<string, string[]> => {\n const byApp = new Map<string, string[]>()\n\n for (const key of Object.keys(presetDef?.apps ?? {})) {\n const app = key.split('/')[0]\n\n if (app === undefined || app === '*') continue\n\n byApp.set(app, [...(byApp.get(app) ?? []), key])\n }\n\n return byApp\n}\n\n/** Discover API apps under `root`, applying the optional `--app` include filter. */\nconst selectApiApps = (root: string, include: string[] | null): DiscoveredApiApp[] => {\n const apps = discoverApiApps(root)\n\n if (!include) {\n return apps\n }\n\n return apps.filter((app) => {\n return include.includes(app.name)\n })\n}\n\n/**\n * Log each app pane and the opened workspace ref. Ports are NOT shown here: each\n * pane binds a dynamic (ephemeral) port at runtime and prints its own real port \u2014\n * the supervisor cannot know it at spawn time, so a static resolved port would lie.\n */\nconst logDevWorkspace = (apps: DiscoveredApiApp[], ref: string): void => {\n logger.info(`\uD83E\uDDE9 Opened cmux dev workspace ${ref} with ${apps.length} pane(s):`)\n\n for (const app of apps) {\n logger.info(` \u2022 ${app.name} (infra-kit dev --app=${app.name})`)\n }\n}\n\n/**\n * Register SIGINT/SIGTERM handlers that close the workspace `ref` \u2014 the supervisor's whole\n * teardown. No force-deadline: closing a workspace is a single call, not a child reap, and a\n * second signal is already an unconditional escape (see {@link registerSignalShutdown}).\n */\nconst registerShutdown = (ref: string): void => {\n registerSignalShutdown({\n onSignal: async (signal) => {\n logger.info(`\\nReceived ${signal}, closing cmux dev workspace ${ref}...`)\n await closeCmuxDevWorkspace(ref)\n },\n })\n}\n\n/**\n * Open a cmux workspace with one pane per API app, then stay resident as a\n * supervisor until a signal tears the workspace down. Falls out early (no\n * workspace) when no apps are discovered. Never returns on the happy path \u2014 it\n * owns its own SIGINT/SIGTERM handling and blocks forever otherwise.\n *\n * @example\n * // Runs until Ctrl-C; opens `pnpm exec infra-kit dev --app=<name>` per app.\n * await runCmuxDevServer({ include: null, watch: false })\n */\nexport const runCmuxDevServer = async (options: DevServerOptions): Promise<void> => {\n const root = findMonorepoRoot(process.cwd())\n const apps = selectApiApps(root, normalizeAppInclude(options.include))\n\n if (apps.length === 0) {\n logger.warn('No API apps found to run')\n\n return\n }\n\n // A wizard run hands down its in-memory preset, so each pane reproduces the exact parts that were\n // ticked. A plain `--cmux` has no presetDef and falls back to `--app=<name>` (every part), unchanged.\n const targetsByApp = paneTargetsByApp(options.presetDef)\n const commands = buildPaneCommands(\n apps.map((app) => {\n return { app: app.name, targets: targetsByApp.get(app.name) }\n }),\n options.watch ?? false,\n )\n const layout = buildCmuxLayout(commands)\n const title = `${path.basename(root)} dev`\n const ref = await openCmuxDevWorkspace({ cwd: root, title, layout })\n\n logDevWorkspace(apps, ref)\n registerShutdown(ref)\n\n // Stay resident until a signal fires. A never-resolving promise alone does NOT\n // keep Node's event loop alive (nothing pending \u2192 the process exits with code\n // 13, \"unsettled top-level await\"); a ref'd heartbeat timer holds it open. The\n // SIGINT/SIGTERM handler in registerShutdown owns the actual exit.\n const heartbeat = setInterval(() => {\n heartbeat.refresh()\n }, 2 ** 30)\n\n await new Promise<never>(() => {})\n}\n", "/**\n * Pure cmux layout construction for `infra-kit dev --cmux`.\n *\n * Turns a flat list of per-pane shell commands into the recursive binary-tree\n * layout JSON cmux's `new-workspace --layout` consumes. Side-effect free (no fs,\n * no cwd, no shell) so the tiling math stays unit-testable in isolation.\n */\n\n/** A single terminal surface inside a pane, running one shell command. */\nexport interface CmuxTerminalSurface {\n type: 'terminal'\n command: string\n}\n\n/** A leaf node: one pane holding one or more terminal surfaces. */\nexport interface CmuxPaneLeaf {\n pane: {\n surfaces: CmuxTerminalSurface[]\n }\n}\n\n/** An internal node: a horizontal/vertical split into exactly two children. */\nexport interface CmuxSplitNode {\n direction: 'horizontal' | 'vertical'\n split: number\n children: [CmuxLayoutNode, CmuxLayoutNode]\n}\n\n/** Either a pane leaf or a two-way split \u2014 the recursive layout tree. */\nexport type CmuxLayoutNode = CmuxPaneLeaf | CmuxSplitNode\n\n/** Wrap a single command in a bare pane leaf. */\nconst makeLeaf = (command: string): CmuxPaneLeaf => {\n return { pane: { surfaces: [{ type: 'terminal', command }] } }\n}\n\n/**\n * Recursively split `commands` into a balanced binary tree: the left half takes\n * `Math.ceil(n / 2)` commands, the split ratio is that count over the total\n * (2-decimal), and the direction alternates by depth (even \u2192 horizontal,\n * odd \u2192 vertical) so panes tile into a grid rather than one axis.\n */\nconst buildNode = (commands: string[], depth: number): CmuxLayoutNode => {\n if (commands.length === 1) {\n return makeLeaf(commands[0]!)\n }\n\n const leftCount = Math.ceil(commands.length / 2)\n const left = commands.slice(0, leftCount)\n const right = commands.slice(leftCount)\n\n return {\n direction: depth % 2 === 0 ? 'horizontal' : 'vertical',\n split: Math.round((leftCount / commands.length) * 100) / 100,\n children: [buildNode(left, depth + 1), buildNode(right, depth + 1)],\n }\n}\n\n/**\n * Build a balanced cmux layout tree with one pane leaf per command. A single\n * command yields a bare leaf; N commands tile into a grid via alternating\n * horizontal/vertical splits. Throws when `commands` is empty.\n *\n * @example\n * buildCmuxLayout(['a', 'b'])\n * // => {\n * // direction: 'horizontal',\n * // split: 0.5,\n * // children: [\n * // { pane: { surfaces: [{ type: 'terminal', command: 'a' }] } },\n * // { pane: { surfaces: [{ type: 'terminal', command: 'b' }] } },\n * // ],\n * // }\n */\nexport const buildCmuxLayout = (commands: string[]): CmuxLayoutNode => {\n if (commands.length === 0) {\n throw new Error('buildCmuxLayout: at least one command is required')\n }\n\n return buildNode(commands, 0)\n}\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 * 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 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, because this\n * module used to explain at length why one should not be. That explanation assumed a human at the keyboard\n * to press Ctrl-C a second time; the incident it now guards against is the case where nobody is there.\n *\n * Mechanically the deadline depends on `terminal-liveness`: with a fault storm live, teardown competes with\n * ~100k blocking `writeSync` calls per second, and a deadline shipped into a machine that cannot honour it\n * is decoration. The storm is cut at its source first; this is what stops the wedge that follows.\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 */\nexport const registerSignalShutdown = ({\n onSignal,\n exit = defaultExit,\n register = defaultRegister,\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\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 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 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 exit(exitCodeForSignal(signal))\n }\n })()\n }\n\n for (const signal of HANDLED_SIGNALS) {\n register(signal, () => {\n return handle(signal)\n })\n }\n}\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 */\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 * 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 const starts = leaderStarts(rows)\n\n return snapshot.flatMap(({ pgid, leaderStart }) => {\n if (starts.get(pgid) !== leaderStart) return []\n\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 * SIGKILL every process group descended from this process, right now.\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 signalGroups(doomedGroupsOf(process.pid, snapshotProcRows()), '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 * That covers the ORDERLY death, where we choose the moment and can walk the tree first. It does not\n * cover turbo CRASHING, because reparenting happens at termination \u2014 strictly before Node hands us the\n * `exit` event \u2014 so by then the walk is already blind. A rolling snapshot (see `sample`) is therefore\n * kept for the child's whole life, and both the crash path and a `kill()` on an already-dead child reap\n * from it, validating each group against its leader's start time so a recycled pgid is never killed.\n *\n * `onUnexpectedExit` (optional) fires when the child dies WITHOUT `kill()` having been called \u2014\n * the \"engine died silently\" case. A `killing` latch, set at the top of `kill()` before any signal\n * goes out, suppresses the callback for the exit our own teardown causes, so it reports only genuine\n * crashes. The `error` listener is load-bearing beyond reporting: a {@link ChildProcess} that emits\n * `error` (e.g. `pnpm` ENOENT) with no listener throws as an uncaught exception and would take the\n * whole dev session down.\n */\nexport function superviseChild(\n child: ChildProcess,\n graceMs: number = DEFAULT_GRACE_MS,\n onUnexpectedExit?: UnexpectedExitHandler,\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 const stopSampling = (): void => {\n if (sampler) clearInterval(sampler)\n sampler = null\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 * An empty result is discarded rather than stored: `ps` failing, or racing the child's own death,\n * must degrade to the previous snapshot, never erase it.\n */\n const sample = async (): Promise<void> => {\n const pid = child.pid\n\n if (pid == null || exited || killing) return\n\n const rows = await snapshotProcRowsAsync()\n\n if (rows.length === 0 || exited || killing) 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 void sample()\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 if (!killing) reapSnapshot(groups, snapshotProcRows())\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) return\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, snapshotProcRows())\n\n return\n }\n\n const rows = snapshotProcRows()\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.\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) return\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, snapshotProcRows())\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 },\n }\n}\n", "/**\n * Process-level crash barrier for the resident single-process dev-server.\n *\n * The backends run IN-PROCESS (`ServerlessLocalRun` is an in-process fastify that `import()`s the\n * compiled handler), so they share the runner's event loop. Fastify catches errors thrown INSIDE a\n * route handler and turns them into a 500 \u2014 but a stray async path a handler forgot to await\n * (`unhandledRejection`) or a throw from a timer/emitter (`uncaughtException`) escapes to the process\n * and, by Node's defaults, terminates it. That kills the WHOLE dev session \u2014 every backend, the watch\n * engine, the UI child \u2014 over one bad path in one handler.\n *\n * This barrier keeps the session alive and reports the fault loudly instead, the same resilience\n * `nodemon`/`vite dev`/`next dev` give. The tradeoff is deliberate and dev-only: it is installed by the\n * CLI entry point, never by library code, so it can never mask a fault in a production consumer.\n *\n * Node's caveat stands: after an `uncaughtException` the process may be in an undefined state. For a\n * local dev tool a possibly-degraded session the developer can see and restart beats a hard exit that\n * looks like the tool itself crashed. The fault is surfaced with a full stack so it is never silent.\n */\nimport process from 'node:process'\n\nimport { killDescendantGroupsNow } from 'src/dev/managed-child'\n\n/** The two process-level fault channels that terminate Node by default. */\ntype FaultEvent = 'uncaughtException' | 'unhandledRejection'\n\n/**\n * Seams for {@link registerCrashBarrier}. Each exists because the real implementation is untestable\n * in-process: attaching real `process.on('uncaughtException')` handlers in the test runner would swallow\n * the runner's own faults, and there is nothing to assert against a handler that only logs to stderr.\n */\nexport interface CrashBarrierDeps {\n /** Report the fault. Defaults to a guarded stderr write. Must not throw. */\n onFault?: (event: FaultEvent, error: unknown) => void\n /** Subscribe `handler` to `event`. Defaults to `process.on`. */\n register?: (event: FaultEvent, handler: (error: unknown) => void) => void\n /**\n * Is our own stdio unwritable? The ONE discriminator between \"survive\" and \"die\" \u2014 see\n * `terminal-liveness.ts`. Defaults to `() => false`, so the LIBRARY's behaviour is unchanged: the exit\n * path is opt-in and wired only by the CLI entry, exactly as this whole barrier is.\n *\n * It must never be re-implemented as an error-code sniff (`EIO`/`EPIPE`). A handler writing to a client\n * socket that hung up throws `EPIPE` too, so a code-sniff would let ONE user closing a browser tab\n * mid-request kill the entire dev session \u2014 the very fragility the barrier exists to remove, re-armed.\n */\n isTerminalDead?: () => boolean\n /**\n * File the fault where it can still be READ once the terminal cannot be written to. The dev entry routes\n * this into the log sink; the default falls back to the guarded stderr write, which may itself be lost \u2014\n * a lost report is recoverable, an unbounded loop is not.\n */\n fileFault?: (event: FaultEvent, error: unknown) => void\n /**\n * Terminate the session because stdio is gone. Reached ONLY when {@link isTerminalDead} is true: a fault\n * reported onto a dead stream is what produced the fault, and surviving it means spinning forever.\n * Defaults to a force-reap of the descendant groups (they hold the dev ports) plus `process.exit`.\n */\n onFatal?: (reason: string) => void\n}\n\n/**\n * Write to stderr, swallowing a failed write. Mirrors `signal-shutdown`'s `writeStderr`: reporting must\n * never itself throw on the fault path (EPIPE when the parent closed the pipe), and a lost message is\n * recoverable where a barrier that throws 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/**\n * The fault report, as one string: message AND stack, plus a note that the session was kept alive.\n *\n * Exported because the dev-server must route this into the panel's error counter and onto the terminal\n * through the interceptor's bypass. `infra-kit dev` no longer prints logs, and the interceptor owns\n * `process.stderr` \u2014 so the default stderr write below would file a crash into a log file and leave the\n * panel showing a healthy, silent session. A fault is the one thing that may never be quiet.\n */\nexport const formatFault = (event: FaultEvent, error: unknown, kept = true): string => {\n const detail = error instanceof Error ? `${error.message}\\n${error.stack ?? '(no stack)'}` : String(error)\n // `kept` is not cosmetic. On the fatal path the session is NOT kept alive, and a trailing \"kept alive\"\n // there is a lie filed into the one log a post-mortem will actually read. Defaulted to `true` so the\n // survive path \u2014 the barrier's entire reason for existing \u2014 is byte-identical to before.\n const outcome = kept\n ? ` dev-server kept alive (likely a bug in a handler's async path); restart if it misbehaves.\\n`\n : ` dev-server is shutting down: its stdio is unwritable, so reporting this fault would loop forever.\\n`\n\n return `\\n\u26A0\uFE0F ${event}: ${detail}\\n${outcome}`\n}\n\n/** Default fault reporter: straight to stderr. Replaced by the dev entry, which also files + counts it. */\nconst defaultOnFault = (event: FaultEvent, error: unknown): void => {\n writeStderr(formatFault(event, error))\n}\n\n/**\n * Default filer for the fatal path. Stderr is very likely the stream that just died \u2014 this write is\n * expected to be dropped, and that is acceptable: the CLI entry overrides it with the log sink, which is\n * the only channel left when stdio is gone.\n */\nconst defaultFileFault = (event: FaultEvent, error: unknown): void => {\n writeStderr(formatFault(event, error, false))\n}\n\n/**\n * Default fatal action: SIGKILL the descendant process GROUPS before exiting. Not `process.exit` alone \u2014\n * turbo and vite sit in their own detached groups, so an exit that skips the reap leaves them holding the\n * dev ports and the next `infra-kit dev` 502s against their stale aliases. That is the orphan half of the\n * incident this exists for (five processes, alive for five hours).\n */\nconst defaultOnFatal = (reason: string): void => {\n writeStderr(`\\n\u2717 dev-server exiting: ${reason}\\n`)\n killDescendantGroupsNow()\n process.exit(1)\n}\n\nconst defaultRegister = (event: FaultEvent, handler: (error: unknown) => void): void => {\n process.on(event, handler)\n}\n\n/**\n * Install the crash barrier: wire `uncaughtException` and `unhandledRejection` to `onFault` and DO NOT\n * exit, so a single escaped async path in an in-process backend handler no longer tears the whole dev\n * session down. The handler never rethrows \u2014 rethrowing would re-arm the very termination this prevents.\n *\n * The ONE exception is {@link CrashBarrierDeps.isTerminalDead}: when our own stdio can no longer be\n * written to, \"report it loudly and survive\" is not resilience, it is the loop \u2014 the report goes to the\n * dead stream, fails, and comes straight back here. So on that path the fault is FILED (never printed) and\n * the session exits. This is a backstop, not the fix: `terminal-liveness` already initiates the same\n * bounded teardown from the stream's own `'error'` event, and this covers a fault arriving from a stream\n * that listener does not own.\n *\n * @example\n * registerCrashBarrier() // resident dev process survives a handler's stray rejection, logs it loudly\n */\nexport const registerCrashBarrier = ({\n onFault = defaultOnFault,\n register = defaultRegister,\n isTerminalDead = () => {\n return false\n },\n fileFault = defaultFileFault,\n onFatal = defaultOnFatal,\n}: CrashBarrierDeps = {}): void => {\n const handle = (event: FaultEvent) => {\n return (error: unknown): void => {\n if (isTerminalDead()) {\n fileFault(event, error)\n // The reason names the CHANNEL, not a story: the errno-bearing reason comes from the liveness\n // listener; here all we honestly know is that a fault arrived while stdio was already unwritable.\n onFatal(`${event} while stdio is unwritable`)\n\n return\n }\n\n onFault(event, error)\n }\n }\n\n register('uncaughtException', handle('uncaughtException'))\n register('unhandledRejection', handle('unhandledRejection'))\n}\n", "/**\n * Unified Development Server Runner\n *\n * Discovers and runs API apps under each `apps/<app>/api` folder that contains `serverless.yml`.\n *\n * Ports: `{APP}_PORT`, then `process.env.PORT`, then `dev.<app>.port` from infra-kit.json,\n * else 3010. URL prefix: `dev.<app>.prefixUrl`, else `/api/v1`.\n * Env vars should be provided via secrets manager (e.g. `doppler run -- pnpm dev-server`) or shell.\n *\n * This module is side-effect free on import: call `run()` (or construct `DevServerRunner`\n * directly) to start. Signal handling and process exit are the entry point's responsibility.\n *\n * Logs are written PER SERVICE under `<cacheRoot>/<INFRA_KIT_SESSION>/dev/<pid>/` \u2014 `runner.log` for the\n * runner's own narration, `<app>/api` and `<app>/ui` for each app, plus `turbo.log` (the UI engine's raw\n * chunk tee) and `watch.log`. Lambda / Powertools logs from handlers still go to stdout.\n */\nimport {\n DEFAULT_RELEASE_SLUG,\n DEV_CONTEXT_WIRE_VERSION,\n loadDev,\n slugifyHostLabel,\n slugifyRelease,\n} from '@slip-stream-kit/config/internal'\nimport chokidar from 'chokidar'\nimport type { FSWatcher } from 'chokidar'\nimport { exec, execFileSync } from 'node:child_process'\nimport * as fs from 'node:fs'\nimport net from 'node:net'\nimport os from 'node:os'\nimport * as path from 'node:path'\nimport process from 'node:process'\nimport util from 'node:util'\n\nimport { INFRA_KIT_ENV_VAR } from 'src/lib/constants'\nimport type { DevConfig, DevPreset, DevPresets, ProxySource } from 'src/lib/infra-kit-config'\nimport { DEFAULT_DEV_PROXY_PORT, getInfraKitConfig } from 'src/lib/infra-kit-config'\n\nimport { buildClosureMap, packageDebounceKey, selectPackageRestartTargets } from './dep-closure.js'\nimport type { ClosureMap, DryRunner } from './dep-closure.js'\nimport type { DevUi } from './dev-ui.js'\nimport {\n classifyDistChange,\n discoverApiApps as discoverApiAppsBare,\n discoverUiApps as discoverUiAppsBare,\n findMonorepoRoot,\n getAppDistDirs,\n getPackageDistDirs,\n normalizeAppInclude as normalizeAppIncludePure,\n} from './discovery.js'\nimport type { DiscoveredUiApp } from './discovery.js'\nimport { findDegradedRoutes, formatPairingRefusal, resolveProxyRoutes } from './local-pairing.js'\nimport type { DegradedRoute, LaunchedUi, ResolvedProxyRoute } from './local-pairing.js'\nimport { currentService } from './log-attribution.js'\nimport { DevLogSink, panelStream } from './log-sink.js'\nimport { installOutputIntercept } from './output-intercept.js'\nimport type { OutputIntercept } from './output-intercept.js'\nimport {\n findPortConflicts,\n resolvePreferredPort as resolvePreferredPortPure,\n resolvePrefixUrl as resolvePrefixUrlPure,\n} from './ports.js'\nimport { deriveTargetLabel, resolvePreset } from './presets.js'\nimport { createPortlessDriver, formatPortlessCommand, readCaPath } from './proxy/portless-driver.js'\nimport type { PortlessDriver } from './proxy/portless-driver.js'\nimport { DevRenderer, resolveEndpointUrl } from './render.js'\nimport type { DegradedRow, EndpointRow, HealthState, ProxyRouteRow, ReadySummary, UiRef } from './render.js'\nimport { ServerlessLocalRun } from './serverless-local-run.js'\nimport { defaultTurboWatchFactory } from './turbo-watch.js'\nimport type { TurboWatchFactory, TurboWatchHandle } from './turbo-watch.js'\nimport { defaultUiDevFactory } from './ui-dev.js'\nimport type { UiDevFactory, UiDevHandle } from './ui-dev.js'\n\n/**\n * The service tag every runner-authored line is filed under. Framework and request lines carry their own\n * app tag (`<app>/ui`, `<app>/api`); anything the runner itself says belongs here.\n */\nconst RUNNER_SERVICE = 'runner'\n\n/**\n * The turbo child's RAW chunk tee \u2014 turbo's own run chrome plus every framework line, ANSI intact and\n * un-de-multiplexed. Kept as its own file rather than smeared across the per-app UI logs: a raw chunk\n * arrives before `parseTurboDevLine` has attributed it to a package, so there is no honest app to file\n * it under. The per-app `<app>/ui` files get the parsed, attributed lines.\n */\nconst TURBO_SERVICE = 'turbo'\n\n/**\n * The `turbo watch build` engine's file. It is spawned with the log fd as its INHERITED stdio\n * (`turbo-watch.ts`), so it writes raw bytes straight into the file \u2014 which is exactly why it needs one\n * of its own rather than a share of any service's.\n */\nconst WATCH_SERVICE = 'watch'\n\n/** Replace a leading home dir with `~` for a compact, human-readable path label (the on-screen log link). */\nexport function homeShorten(p: string): string {\n const home = os.homedir()\n\n return p === home || p.startsWith(`${home}${path.sep}`) ? `~${p.slice(home.length)}` : p\n}\n\n/**\n * Comma-joined package names \u2014 not folder names, so a boot line names the exact `turbo --filter`\n * targets the build runs on and matches turbo's own output.\n */\nfunction packageList(apps: { packageName: string }[]): string {\n return apps\n .map((a) => {\n return a.packageName\n })\n .join(', ')\n}\n\nconst execFn = util.promisify(exec)\n\ntype LogFn = (msg: string, level?: 'info' | 'warn' | 'error' | 'debug') => void\n\n/**\n * Build seam: shells out to turbo by default ({@link launchScript}); injectable so\n * tests can run the orchestrator without a real build. `logFn` is optional so both\n * the verbose initial build and the terse restart builds keep their current output.\n */\nexport type BuildRunner = (cmd: string, logFn?: LogFn) => Promise<void>\n\nconst launchScript = async (script: string, logFn?: LogFn): Promise<void> => {\n try {\n const { stderr } = await execFn(script)\n\n if (stderr && logFn) logFn(` (build) ${stderr.trim()}`, 'debug')\n if (stderr && !logFn) console.error('stderr:', stderr)\n } catch (error) {\n const err = error as { stdout?: string; stderr?: string; message?: string }\n\n if (logFn && (err.stdout || err.stderr)) {\n if (err.stdout) logFn(` stdout: ${err.stdout.trim()}`, 'error')\n if (err.stderr) logFn(` stderr: ${err.stderr.trim()}`, 'error')\n }\n\n throw error\n }\n}\n\n/**\n * The process-wide per-service log sink. Created once at construction ({@link DevServerRunner}) and\n * closed in {@link DevServerRunner.shutdown}; module-scoped so the renderer's `appendLog` seam \u2014 which\n * is wired before `this` is fully initialised \u2014 can reach it.\n */\nlet logSink: DevLogSink | null = null\n\n/** Tee a runner-authored line to `runner.log`. The seam {@link DevRenderer} wraps. */\nfunction appendRunnerLog(text: string): void {\n logSink?.write(RUNNER_SERVICE, text)\n}\n\n/**\n * An OS-assigned free TCP port on 127.0.0.1 \u2014 used to pre-assign each UI's Vite port so the runner can\n * print its URL (and alias it, when a proxy is up) before Vite binds. Probes then releases (small TOCTOU\n * window; `strictPort` on the Vite side turns a rare lost race into a loud failure rather than a silent drift).\n */\nconst getFreePort = (): Promise<number> => {\n return new Promise((resolve, reject) => {\n const srv = net.createServer()\n\n srv.unref()\n srv.on('error', reject)\n srv.listen(0, '127.0.0.1', () => {\n const address = srv.address()\n const port = typeof address === 'object' && address !== null ? address.port : 0\n\n srv.close(() => {\n return resolve(port)\n })\n })\n })\n}\n\ninterface IApiAppConfig {\n /** App folder name (e.g. backoffice, client) */\n name: string\n /** Package name from package.json (e.g. sls-trvl-client) */\n packageName: string\n path: string\n /**\n * EXPLICITLY-configured preferred port (`{APP}_PORT`/`PORT`/`dev.<app>.port`), or\n * `undefined` when unconfigured. Under dynamic allocation this is only a bind hint \u2014 the\n * ACTUAL port is the ephemeral one bound at start time (see {@link IAppServer.boundPort}).\n */\n preferredPort: number | undefined\n prefixUrl: string\n /**\n * Whether this backend participates in dependency-closure watching (plan Phase 1): in\n * `--watch`, a rebuild of a package in this app's closure restarts it. Default `true`;\n * a preset `watchDeps: false` opts out (sticky). Resolved from the preset in {@link run}.\n */\n watchDeps: boolean\n}\n\n/**\n * Runner options, parsed by the CLI entry point (`--watch`, `--app`) and threaded\n * through `run()`. The entry owns flag parsing; the runner never reads `process.argv`\n * itself. App selection is `--app` only; ports come from env/config (see `resolvePort`).\n */\nexport interface DevServerOptions {\n /**\n * Watch mode: start a long-lived `turbo watch build` engine (incremental rebuilds\n * + dependency fan-out) and restart the affected server(s) when compiled `dist/`\n * changes. Without it, `dev` builds and serves once and exits on signal.\n */\n watch?: boolean\n /** Only run these app folder names (null/empty = all discovered). Filters BOTH api and ui apps. */\n include?: string[] | null\n /**\n * Named dev preset (`infra-kit dev <preset>`) from `devServersPresets` in the project's infra-kit config.\n * It selects the launch targets (`apps/<app>/{api,ui}`); resolved by {@link file://./presets.ts}.\n * Unset \u2192 run everything (`*`). `include` (`--app`/`--self`) further narrows the resolved set.\n */\n preset?: string\n /**\n * In-memory preset definition, produced by the interactive wizard (`infra-kit dev` with no args in a\n * TTY). When set it is used verbatim as the run plan \u2014 it WINS over `preset` (the named lookup) and\n * the default `*`. This is how the wizard expresses part-level selection (`<app>/ui` without\n * `<app>/api`), which `include` (app-name-only) cannot. Unset on every non-wizard invocation.\n */\n presetDef?: DevPreset\n /**\n * Run each discovered API app in its own cmux pane (one workspace, N panes), supervised by a\n * resident process that closes the workspace on signal. Falls back to single-process dev when\n * cmux is absent. Handled by `runCmuxDevServer`, not the in-process `DevServerRunner`.\n */\n cmux?: boolean\n /**\n * Infer the single app to run from the current working directory (equivalent to\n * `--app=<that app>`), so every app can share the identical script\n * `pnpm exec infra-kit dev --self` instead of hardcoding its own folder name.\n * Resolved by the entry point (`resolveSelfAppName`) into `include` before this\n * runner ever sees it; the runner itself does not read `self`.\n */\n self?: boolean\n /**\n * Print the full boot narration (build/discovery/watch steps) to the terminal. Default false:\n * the terminal shows only the server panel, warnings, errors, and restart lines. The FULL detail\n * is written to the per-service logs under `<cacheRoot>/<INFRA_KIT_SESSION>/dev/<pid>/` regardless of\n * this flag.\n */\n verbose?: boolean\n /**\n * Print each app's registered `METHOD /path` route table at startup. Default false \u2014 the route\n * dump is opt-in so the calm default screen stays glanceable; the routes are always in the log.\n */\n routes?: boolean\n /**\n * Interactive TTY (both stdin+stdout). Set by the entry point; gates the Ink boot UI in {@link run}.\n * When unset, {@link run} falls back to `process.stdout.isTTY`. The runner itself never reads it.\n */\n tty?: boolean\n /**\n * Structured `--json` / MCP mode. When true, {@link run} forces the plain {@link DevRenderer} \u2014 Ink\n * must never seize a machine-readable stream. The runner itself never reads it.\n */\n json?: boolean\n /**\n * Background liveness-probe interval in ms (default {@link DevServerRunner.LIVENESS_INTERVAL_MS}, 5000).\n * A test seam only \u2014 lets a test drive the monitor loop fast; production never sets it.\n */\n livenessIntervalMs?: number\n /**\n * Probe the frontends' liveness (vite's HMR ping) and give their rows a health dot. Default `true`;\n * `--no-ui-health` / `INFRA_KIT_NO_UI_HEALTH=1` turns it off, which drops every UI row back to no dot\n * at all and issues zero UI probes. The escape hatch exists because the ping is an undocumented vite\n * internal: if a future vite drops it, the dot has to be switchable off without a CLI downgrade.\n */\n uiHealth?: boolean\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 */\nconst 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`. */\nconst 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\n/**\n * One row's probe history \u2014 the state the 5-arm {@link HealthState} is derived from, keyed by TAG (never\n * by app name: `foo/api` and `foo/ui` are different rows on the same app, and a name-keyed counter\n * collides them).\n */\ninterface HealthEntry {\n kind: ProbeTarget['kind']\n /** Consecutive `refused` probes. `>= LIVENESS_FAILURE_THRESHOLD` is what \"down\" MEANS. */\n failures: number\n /** Consecutive `foreign` probes \u2014 a separate counter, because a foreign answer must never go red. */\n foreignStreak: number\n /** Has this row EVER proved it was serving? Until it has, a UI is `starting`, not `down`. */\n everUp: boolean\n /** A CERTAINTY of death from outside the probe loop: a thrown restart, a dead UI engine. */\n dead: boolean\n /** The port answers, but not with what we asked for \u2014 liveness cannot be established either way. */\n unverified: boolean\n}\n\n/** What a successful {@link DevServerRunner.startOneApp} hands back: a bound server and the alias it took. */\ninterface StartedApp {\n server: ServerlessLocalRun\n /** The ACTUAL port bound at start (ephemeral or preferred), reported by `server.start()`. */\n boundPort: number\n /** Layer-B alias host (`<release>.<package>.localhost`) \u2014 the app's only address. */\n alias: string\n}\n\ninterface IAppServer {\n app: IApiAppConfig\n server: ServerlessLocalRun\n /** The ACTUAL port bound at start (ephemeral or preferred), reported by `server.start()`. */\n boundPort: number\n /** Layer-B alias host (`<release>.<package>.localhost`) \u2014 the app's only address. */\n alias: string\n /** Epoch ms of this server's last (re)start \u2014 the source of the panel's `up Xs` field. */\n startedAt: number\n /** Watch-triggered restarts so far. */\n restarts: number\n}\n\n/**\n * One `.infra-kit/dev-context/<app>.json` fragment. `package` (feeds the vite helper's\n * `readLocalSet`) is the app's package name; `port` is the ACTUAL bound port (the writer\n * IS the binder). `release` lets the helper prefer the runner-recorded slug over its own\n * git derivation; `pid`/`writtenAt` are staleness metadata.\n *\n * `alias` is the registered portless hostname, and `origin` is **the authoritative local target** \u2014 the\n * exact origin the helper must proxy to, published by the runner rather than re-derived by the helper from\n * a template in a separately-versioned repo. The runner is the only party that knows what it actually\n * registered, so it says so; the helper obeys.\n *\n * `proxyPort` is deliberately NOT written any more. The CLI self-updates while `infra-kit/vite` stays\n * pinned per consumer, so a new CLI routinely meets an OLD helper \u2014 and that helper's `withProxyPort`\n * grafts any port other than 80 onto its target. Writing `proxyPort: 443` would therefore have produced\n * `http://<alias>:443`: **plain HTTP into a TLS listener**, silently. Omitting the field leaves the old\n * helper's target ungrafted instead. (That is not by itself a loud failure \u2014 `:80` is bound by portless's\n * redirect server, so it 302s rather than refusing \u2014 which is why the CLI-side version floor, not this\n * omission, is the load-bearing skew guard.)\n */\ninterface DevContextFragment {\n /** Wire version. See {@link DEV_CONTEXT_WIRE_VERSION} \u2014 a promise that `origin` is present. */\n v: number\n package: string\n port: number\n pid: number\n writtenAt: number\n release: string\n alias: string\n origin: string\n}\n\n/**\n * Every package that can supply the `infraKitDev` helper, with the lowest version of THAT package whose\n * helper understands the dev-context fragment's `origin` field.\n *\n * This is the load-bearing guard against version skew, and skew here is GUARANTEED rather than\n * hypothetical: the CLI is installed globally and **self-updates silently**, while the helper is PINNED\n * in each consumer's `node_modules`. So a new CLI routinely meets an old helper. An old helper ignores\n * `origin` and rebuilds the target from the consumer's `templates.local` \u2014 which still says `http://` \u2014\n * and then proxies plain HTTP at a TLS listener. That failure is silent (portless answers :80 with a 302\n * rather than refusing), so nothing downstream would catch it. Refuse at start instead.\n *\n * A LIST, and each floor is a point on ITS OWN package's version line. This is the whole subtlety of the\n * `infra-kit` \u2192 `@slip-stream-kit/config` split, and getting it wrong fails silently in both directions:\n * - Comparing the new package's version against the OLD package's floor (`0.1.132`) is meaningless. The\n * two are unrelated version lines; a new package seeded low would throw for every consumer, and one\n * seeded high would pass vacuously \u2014 a dead guard that still looks alive.\n * - Simply RE-KEYING the guard to the new package (rather than adding to it) drops the old entry, and\n * then a not-yet-migrated consumer \u2014 the exact population still running an old helper \u2014 silently stops\n * being checked at all.\n *\n * So: keep them all, check whichever the repo actually resolves, and only drop the `infra-kit` entry once\n * no consumer imports `infra-kit/vite` any more. The packages release in LOCKSTEP, which is what keeps\n * each floor comparable to the CLI's own version as the wire evolves.\n *\n * `@slip-stream-kit/vite` (the plugin) needs its OWN entry even though it only wraps\n * `@slip-stream-kit/config`, and the reason is pnpm's layout rather than style: a consumer on the plugin\n * declares only the plugin, so `config` is a TRANSITIVE dep living in the virtual store \u2014 it resolves\n * from neither the app dir nor the repo root, and {@link assertHelperVersionFloor} would find nothing\n * to check and skip the repo entirely. The plugin's own `dependencies` pin the config version exactly\n * (`workspace:*` publishes as the released version), so checking the plugin checks the pair.\n */\nexport const HELPER_PACKAGES = [\n { name: '@slip-stream-kit/vite', floor: '0.1.134' },\n { name: '@slip-stream-kit/config', floor: '0.1.134' },\n { name: 'infra-kit', floor: '0.1.132' },\n] as const\n\n/** `true` when `version` sorts strictly below `floor` (numeric, dot-separated; missing parts are 0). */\nexport const isBelowVersion = (version: string, floor: string): boolean => {\n const parse = (v: string): number[] => {\n return v.split('.').map((part) => {\n return Number.parseInt(part, 10) || 0\n })\n }\n const a = parse(version)\n const b = parse(floor)\n\n for (let i = 0; i < Math.max(a.length, b.length); i += 1) {\n const left = a[i] ?? 0\n const right = b[i] ?? 0\n\n if (left !== right) return left < right\n }\n\n return false\n}\n\n/** Package dirs whose manifest may declare a helper: the repo root, plus every `apps/<app>/{api,ui}`. */\nconst manifestDirs = (repoRoot: string): string[] => {\n const dirs = [repoRoot]\n\n try {\n for (const app of fs.readdirSync(path.join(repoRoot, 'apps'), { withFileTypes: true })) {\n if (!app.isDirectory()) continue\n for (const part of ['api', 'ui']) dirs.push(path.join(repoRoot, 'apps', app.name, part))\n }\n } catch {\n // No `apps/` dir \u2014 the root manifest alone decides.\n }\n\n return dirs\n}\n\n/** Does `<dir>/package.json` declare `name` in dependencies or devDependencies? */\nconst declaresPackage = (dir: string, name: string): boolean => {\n try {\n const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8')) as Record<\n string,\n Record<string, string> | undefined\n >\n\n return ['dependencies', 'devDependencies'].some((field) => {\n return pkg[field]?.[name] != null\n })\n } catch {\n return false\n }\n}\n\n/**\n * Find the installed `name` by walking `node_modules` UPWARD from `fromDir`, exactly as Node resolves it.\n *\n * Probing `<repoRoot>/node_modules/<name>` alone \u2014 which is what this used to do \u2014 is wrong under pnpm,\n * and wrong in the direction that hurts. pnpm does NOT hoist a workspace package's dependency to the root:\n * a dep declared in `apps/client/ui/package.json` lands in `apps/client/ui/node_modules/`, and the root has\n * no trace of it. So the root-only probe would (a) fail to find \u2014 and therefore never version-check \u2014 a\n * helper declared where it is actually USED (next to the `vite.config.ts` that imports it), and (b) once\n * the consumer drops root `infra-kit` for the global CLI, report the correctly-installed helper as\n * \"missing\" and tell the user to run `pnpm install`, which can never fix it. Resolve it the way Node does.\n */\nconst findHelperDir = (repoRoot: string, fromDir: string, name: string): string | undefined => {\n const segments = name.split('/')\n let dir = fromDir\n\n for (;;) {\n const candidate = path.join(dir, 'node_modules', ...segments)\n\n if (fs.existsSync(candidate)) return candidate\n\n const parent = path.dirname(dir)\n\n if (dir === repoRoot || parent === dir) return undefined\n dir = parent\n }\n}\n\n/**\n * `true` when `<repoRoot>/node_modules/<name>` is a workspace link back into this repo rather than a\n * published install.\n *\n * Resolves BOTH sides before comparing: on macOS a temp path realpaths from `/var` to `/private/var`, so\n * an unresolved root would never contain a resolved child and a workspace link would be misread as a\n * published install.\n */\nconst isWorkspaceLinked = (repoRoot: string, helperDir: string): boolean => {\n try {\n const real = fs.realpathSync(helperDir)\n const root = fs.realpathSync(repoRoot)\n\n return real.startsWith(root + path.sep) && !real.includes(`${path.sep}node_modules${path.sep}`)\n } catch {\n return false\n }\n}\n\n/** `realpath`, falling back to the input when it cannot be resolved (used only as a dedupe key). */\nconst safeRealpath = (target: string): string => {\n try {\n return fs.realpathSync(target)\n } catch {\n return target\n }\n}\n\n/** Enforce one helper package's floor against ONE resolved install directory. */\nconst assertFloorAt = (repoRoot: string, name: string, floor: string, helperDir: string): void => {\n // In this repo `node_modules/@slip-stream-kit/config` symlinks to `apps/infra-kit/config`, whose\n // version is the unreleased working tree. Enforcing a floor there would brick `infra-kit dev` on the\n // very repo that develops it.\n if (isWorkspaceLinked(repoRoot, helperDir)) return\n\n let version: string\n\n try {\n const pkg = JSON.parse(fs.readFileSync(path.join(helperDir, 'package.json'), 'utf-8')) as { version?: string }\n\n if (typeof pkg.version !== 'string') throw new Error('no version field')\n version = pkg.version\n } catch {\n throw new Error(\n `infra-kit dev: could not read the version of the pinned \\`${name}\\` helper (${helperDir}). Refusing ` +\n `to start rather than risk proxying plain HTTP at a TLS listener. Run \\`pnpm install\\`.`,\n )\n }\n\n if (isBelowVersion(version, floor)) {\n throw new Error(\n `infra-kit dev: this repo pins ${name} ${version}, but dev URLs are now HTTPS and the ` +\n `\\`infraKitDev\\` helper only understands them from ${floor}. An older helper would proxy plain ` +\n `HTTP at a TLS listener \u2014 silently. Bump the dependency:\\n` +\n ` pnpm add -D ${name}@^${floor}`,\n )\n }\n}\n\n/**\n * Refuse to start against a consumer-pinned `infraKitDev` helper too old to understand the dev-context\n * fragment's `origin` field \u2014 whichever package that helper comes from (see {@link HELPER_PACKAGES}).\n *\n * Every helper package is checked INDEPENDENTLY, and every place it resolves from is checked. Two\n * migration shapes make that necessary rather than fussy:\n * - A repo mid-migration has BOTH (`@slip-stream-kit/config` added, `infra-kit` not yet dropped), and its\n * vite configs may still import from either. Stopping at the first helper that passes would hand exactly\n * that repo a silent HTTP-into-TLS proxy from the other one.\n * - Under pnpm the helper usually resolves NOT from the repo root but from the package that declares it \u2014\n * `apps/client/ui/node_modules/` sits right next to the `vite.config.ts` that imports it. See\n * {@link findHelperDir}.\n *\n * Three outcomes per package, and the reasoning for each matters:\n * - **Workspace-linked \u2192 SKIP** (that install only; every other one is still checked).\n * - **Declared but unresolvable \u2192 THROW (fail closed).** The consumer says it uses a helper and we\n * cannot prove which version; guessing is how the silent case ships.\n * - **Neither installed nor declared \u2192 SKIP.** Nothing to be skewed against. (This is also what keeps\n * bare test fixtures runnable.)\n */\nexport const assertHelperVersionFloor = (repoRoot: string): void => {\n const dirs = manifestDirs(repoRoot)\n\n for (const { name, floor } of HELPER_PACKAGES) {\n const declaredIn = dirs.filter((dir) => {\n return declaresPackage(dir, name)\n })\n\n // Always probe the root too: a helper can be present without being declared (hoisted, or a transitive\n // of something else), and it would still be the one vite resolves.\n const resolved = new Map<string, string>()\n\n for (const dir of [...declaredIn, repoRoot]) {\n const found = findHelperDir(repoRoot, dir, name)\n\n // Key by realpath: several packages symlinking into the same pnpm store entry are ONE install, and\n // re-reading it per app would just multiply identical work (and identical error messages).\n if (found) resolved.set(safeRealpath(found), found)\n }\n\n if (resolved.size === 0) {\n if (declaredIn.length === 0) continue\n\n throw new Error(\n `infra-kit dev: ${declaredIn[0]}/package.json depends on ${name} but it does not resolve from there, ` +\n `so the \\`infraKitDev\\` helper version cannot be verified. Run \\`pnpm install\\`.`,\n )\n }\n\n for (const helperDir of resolved.values()) assertFloorAt(repoRoot, name, floor, helperDir)\n }\n}\n\n/**\n * Slugified `<release>` for the app's git branch (resolved from the app's own dir), falling back to\n * {@link 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.\n */\nconst readAppRelease = (cwd: string): string => {\n try {\n // eslint-disable-next-line sonarjs/no-os-command-from-path\n const branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, encoding: 'utf-8' }).trim()\n const slug = slugifyRelease(branch)\n\n return slug === '' ? DEFAULT_RELEASE_SLUG : slug\n } catch {\n return DEFAULT_RELEASE_SLUG\n }\n}\n\n/**\n * The one-line `reason` for a `\u25CF failed` row. Endpoint rows are a single terminal line, so a stack\n * trace cannot go there \u2014 it is already in the log tail and the session log. Take the message only,\n * and its first line at that: validation errors like to append their own multi-line dumps.\n *\n * @example\n * errorReason(new Error(\"config is missing field: 'connectionURL'\\n at \u2026\")) // \"config is missing field: 'connectionURL'\"\n */\nconst errorReason = (error: unknown): string => {\n const message = error instanceof Error ? error.message : String(error)\n const firstLine = message.split('\\n')[0]?.trim() ?? ''\n\n return firstLine === '' ? 'unknown error (see the log)' : firstLine\n}\n\nexport class DevServerRunner {\n private readonly monorepoRoot: string\n /**\n * `<cwd>/.infra-kit/dev-context` \u2014 the fragment directory this runner writes its own\n * per-app `<app>.json` into (mirrors {@link LOG_FILE_PATH}'s cwd-relative resolution). The\n * `infra-kit/vite` helper searches up-tree for this dir and merges the fragments.\n */\n private readonly devContextDir: string\n private readonly appServers: IAppServer[] = []\n /** Per-app request timestamps, pruned to a 60s window \u2014 the panel's `18/min` field. */\n private readonly reqTimes = new Map<string, number[]>()\n /**\n * The last {@link ReadySummary} painted. Kept so {@link refreshStatus} can repaint the panel with\n * fresh live fields without re-deriving the static half (URLs, aliases, watch summary) every tick.\n */\n private lastSummary: ReadySummary | null = null\n /** Epoch ms at which the session went ready \u2014 the source of the panel's heartbeat. */\n private readyAt = 0\n private watchDebounceTimers: Map<string, ReturnType<typeof setTimeout>> = new Map()\n /** Active chokidar watcher in `--watch` mode; closed on {@link shutdown}. */\n private watcher: FSWatcher | null = null\n private static readonly WATCH_DEBOUNCE_MS = 400\n /** Serialized restarts so rapid saves never bind :port while the previous server is still shutting down. */\n private restartWorkChain: Promise<void> = Promise.resolve()\n private static readonly PORT_RELEASE_DELAY_MS = 200\n /** Self-rescheduling backend liveness probe; cleared on {@link shutdown}. Null when unarmed (UI-only). */\n private livenessTimer: ReturnType<typeof setTimeout> | null = null\n /**\n * Probe history per row, keyed by TAG (`<app>/api`, `<app>/ui`) \u2014 never by app name, which would\n * collide an app's two halves into one counter the moment the frontends joined the tick. It survives an\n * ephemeral-port rebind on restart, because the tag does.\n */\n private readonly health = new Map<string, HealthEntry>()\n /** Tags whose first error has already been announced \u2014 the 0 \u2192 >0 edge fires once per row, not per tick. */\n private readonly firstErrorLogged = new Set<string>()\n /** `<ui-package>` \u2192 `<app>/ui` tag, so {@link uiTargets} can name the rows {@link uiPortMap} keys by package. */\n private readonly uiTagByPackage = new Map<string, string>()\n /** Liveness re-probe cadence. A wedged-but-not-crashed backend is caught within THRESHOLD ticks. */\n private static readonly LIVENESS_INTERVAL_MS = 5000\n /** Consecutive failures before a row is declared down \u2014 the anti-flap / restart-window filter. */\n private static readonly LIVENESS_FAILURE_THRESHOLD = 2\n /**\n * Refused probes a UI that has NEVER been up may take before its `\u25CC starting` turns `\u25CF down` (~30s at\n * the 5s cadence). Far above the backend threshold on purpose: a cold vite on a big frontend genuinely\n * takes tens of seconds to bind, and a red dot over a UI that is merely still booting is the one false\n * red this whole design refuses to ship. A UI that has been up once is not covered by it \u2014 its death is\n * probe-established at the ordinary threshold.\n */\n private static readonly NEVER_UP_DOWN_THRESHOLD = 6\n private readonly options: DevServerOptions\n /** Build runner seam \u2014 real turbo shell-out by default, injectable for tests. */\n private readonly runBuild: BuildRunner\n /** `turbo watch` spawn seam \u2014 real detached child by default, injectable for tests. */\n private readonly turboWatchFactory: TurboWatchFactory\n /** Live `turbo watch` engine in `--watch` mode; reaped on {@link shutdown}. */\n private turboWatch: TurboWatchHandle | null = null\n /** `turbo run dev` (frontends) spawn seam \u2014 real detached child by default, injectable for tests. */\n private readonly uiDevFactory: UiDevFactory\n /** Live `turbo run dev` frontend engine in `--ui` mode; reaped on {@link shutdown}. */\n private uiDev: UiDevHandle | null = null\n /**\n * Per-service log files. One held fd per service \u2014 so a chunk tee during an HMR burst is a single\n * `writeSync` on an already-open fd, not the open/write/close that the old `appendFileSync` paid per\n * call, and there is no buffered stream whose tail a hard exit could drop.\n */\n private readonly sink: DevLogSink\n /**\n * Owns `console` + the raw stream writes for the life of a TTY session, routing every line into its\n * service's file. `null` on a `--json` / MCP / piped run, where stdout must stay byte-clean.\n */\n private readonly intercept: OutputIntercept | null\n /** `turbo --dry` closure-source seam \u2014 real turbo (via {@link buildClosureMap}'s default) unless injected for tests. */\n private readonly dryRunner: DryRunner | undefined\n /** Apps that threw during {@link startAllApps} \u2014 rendered as `\u25CF failed` rows by {@link printReady}. */\n private readonly failedApps: { app: IApiAppConfig; reason: string }[] = []\n\n /**\n * Routes that wanted a local backend and lost it to a start failure (see `local-pairing.ts`). Computed\n * once, right after {@link startAllApps} \u2014 it is a fact about the boot, and the panel re-derives which\n * of them are STILL degraded on every repaint from the running set.\n */\n private degradedRoutes: DegradedRoute[] = []\n /**\n * The launched frontends and their declared `dev.proxy` routes, built once alongside {@link degradedRoutes}\n * from the SAME `loadDev` read the vite helper uses. Shared so both the degraded check and the ready\n * header's per-app proxy listing resolve off one source of truth.\n */\n private launchedUis: LaunchedUi[] = []\n /**\n * Every launched frontend route resolved to where it actually lands this run ({@link resolveProxyRoutes}),\n * for the nested \"all proxies \u2014 which one and where\" list under each UI row.\n *\n * A BOOT-TIME SNAPSHOT: computed once in {@link printReady} and only ever filtered (never re-derived) by\n * {@link proxiesFor}. It is committed into the static header, so under `--watch` a backend that boot-fails\n * then recovers keeps its `cloud` line here even after vite has flipped the live proxy back to `local`.\n * That live surface is deliberately owned by the self-clearing {@link degradedRows}, not this list.\n */\n private proxyRoutes: ResolvedProxyRoute[] = []\n /**\n * The terminal UI \u2014 owns every stdout line + the log-file tee. Either the plain {@link DevRenderer}\n * or the Ink boot UI, selected by {@link run} and injected here; the runner drives it through {@link DevUi}.\n */\n private readonly renderer: DevUi\n /** Health-probe seam (real bounded `/__health` GET by default; injectable for tests). */\n private readonly healthProbe: HealthProbe\n /** Portless driver (Layer B) \u2014 best-effort alias registration; injectable for tests. */\n private readonly proxy: PortlessDriver\n /**\n * The port the portless daemon must be serving TLS on. Fixed at {@link DEFAULT_DEV_PROXY_PORT} (443):\n * it is not configurable, because a port-free `https://` URL can only be served from the implicit HTTPS\n * port. A configurable port would put the port straight back into the URL.\n */\n private readonly proxyPort: number = DEFAULT_DEV_PROXY_PORT\n /** Every `<release>.<package>` alias this runner registered; removed one-by-one in {@link shutdown}. */\n private readonly registeredAliases = new Set<string>()\n /**\n * `{ \"<ui-package>\": { port, alias } }` handed to the turbo child via `INFRA_KIT_UI_PORTS` (computed in\n * printReady).\n *\n * The `alias` is load-bearing, not provenance: the page is served from `https://<alias>` on :443, so\n * vite's HMR client must dial `wss://<alias>` \u2014 left to itself it derives the socket from vite's own\n * bound port, which the browser then blocks as mixed content. Only the runner knows which alias it\n * actually registered, so it publishes it rather than letting the helper re-derive it (a helper that\n * merely *computes* an alias would point HMR at an unregistered host on a bare `vite dev`).\n */\n private uiPortMap: Record<string, { port: number; alias: string }> = {}\n /**\n * Latched by {@link shutdown} before it touches any state. Teardown deregisters aliases and then\n * spends seconds reaping children, so without this latch a watch event \u2014 or a restart already in\n * flight \u2014 re-enters {@link startOneApp} and re-registers an alias that nothing will ever remove.\n */\n private shuttingDown = false\n /**\n * Dependency closure used to scope a shared-package restart, or `null` for \"restart every app\"\n * (the fail-safe {@link selectPackageRestartTargets} already honours). Read at EVENT time, not\n * captured when the watcher is armed: building it shells out to turbo (~1s on a 7-backend repo),\n * and blocking the watcher on it left a window right after `ready` in which a save was silently\n * dropped. The watcher arms immediately on `null` and this is swapped in when turbo answers.\n */\n private closureMap: ClosureMap | null = null\n /** In-flight {@link closureMap} build, awaited by {@link shutdown} so no turbo child outlives the runner. */\n private closureBuild: Promise<void> = Promise.resolve()\n /**\n * The single in-flight teardown, memoized by {@link shutdown} \u2014 `null` until the first caller arrives.\n *\n * Terminal death is ONE event observed through TWO channels: the kernel SIGHUPs the foreground process\n * group (`signal-shutdown.ts` handles it) and the next write trips the stdio `'error'` listener\n * (`terminal-liveness.ts`). Both fire, in either order. Without this memo the two callers would\n * `turboWatch.kill()`, `uiDev.kill()` and `server.close()` TWICE \u2014 a second SIGTERM\u2192grace\u2192SIGKILL cycle\n * aimed at a child group that is already mid-reap, which is the one way this fix could strand children\n * worse than the bug it repairs.\n */\n private teardown: Promise<void> | null = null\n /**\n * The teardown step currently in flight, for {@link shutdownStage}. Assigned before EVERY `await` in\n * {@link doShutdown}, because the whole value of the deadline in `signal-shutdown` is that it can NAME\n * the step that wedged \u2014 that is the instrument that answers why five orphaned processes never exited.\n */\n private stage = 'idle'\n\n constructor(\n options: DevServerOptions = {},\n runBuild: BuildRunner = launchScript,\n turboWatchFactory: TurboWatchFactory = defaultTurboWatchFactory,\n uiDevFactory: UiDevFactory = defaultUiDevFactory,\n dryRunner?: DryRunner,\n renderer?: DevUi,\n healthProbe: HealthProbe = defaultHealthProbe,\n proxy: PortlessDriver = createPortlessDriver(),\n sink?: DevLogSink,\n ) {\n this.options = options\n this.runBuild = runBuild\n this.turboWatchFactory = turboWatchFactory\n this.uiDevFactory = uiDevFactory\n this.dryRunner = dryRunner\n this.proxy = proxy\n // Injectable so a test can drive the REAL `reportFault` against a temp-dir sink and assert on the bytes\n // it actually files. Without the seam the fault-loop regression test can only hand-model the loop it is\n // supposed to be proving \u2014 and a hand-modelled loop goes green against the unfixed code.\n this.sink = sink ?? new DevLogSink()\n logSink = this.sink\n\n // Install EARLY and FILE-ONLY: an app's log line never reaches the terminal, at any point. Early,\n // because handler modules are imported during boot \u2014 long before the panel exists \u2014 so a late\n // install would let their import-time output escape onto the screen, which is the leak this whole\n // mechanism exists to prevent (a Powertools `Server listening` banner printed above the panel).\n //\n // No tee window, and no boot crash is lost to it: Node writes a fatal stack STRAIGHT TO FD 2 (never\n // through the patched stream), a rejected `start()` surfaces after `shutdown()` has already called\n // `uninstall()`, and a post-`ready()` fault goes through `reportFault` onto the panel.\n this.intercept = ownsTerminal(this.options)\n ? installOutputIntercept({ sink: this.sink, fallbackService: RUNNER_SERVICE, currentService })\n : null\n // The renderer owns all terminal output + the log tee; construct it before the first narrate below.\n this.renderer = renderer ?? new DevRenderer({ appendLog: appendRunnerLog, verbose: this.options.verbose ?? false })\n this.healthProbe = healthProbe\n this.devContextDir = path.join(process.cwd(), '.infra-kit', 'dev-context')\n\n // Walk up from the consumer repo cwd to the monorepo root.\n this.monorepoRoot = findMonorepoRoot(process.cwd())\n\n if (process.env.DOPPLER_PROJECT != null || process.env.DOPPLER_ENVIRONMENT != null) {\n this.renderer.log('\uD83D\uDD10 Doppler env detected (DOPPLER_PROJECT / DOPPLER_ENVIRONMENT)', 'debug')\n }\n }\n\n /**\n * Discover API apps and resolve each app's port + URL prefix. Delegates bare\n * filesystem discovery to {@link discoverApiAppsBare} and per-app resolution to\n * the pure `resolvePort` / `resolvePrefixUrl`, preserving the original behavior.\n */\n private discoverApiApps(devConfig: DevConfig): IApiAppConfig[] {\n return discoverApiAppsBare(this.monorepoRoot).map((app) => {\n return {\n ...app,\n preferredPort: this.resolvePreferredPort(app.name, devConfig),\n prefixUrl: this.resolvePrefixUrl(app.name, devConfig),\n // Default participate; the resolved preset value overrides this in `run()`.\n watchDeps: true,\n }\n })\n }\n\n /**\n * Read the `dev` section from the resolved infra-kit config. Defensive: any\n * failure (not in an infra-kit project, missing/invalid config) resolves to an\n * empty map so the dev-server always falls back to env vars + built-in defaults\n * rather than refusing to start.\n */\n private async loadDevConfig(): Promise<DevConfig> {\n try {\n const config = await getInfraKitConfig()\n\n return config.dev ?? {}\n } catch {\n return {}\n }\n }\n\n /** Thin delegator to the pure {@link normalizeAppIncludePure} over the runner's `--app` list. */\n private normalizeAppInclude(): string[] | null {\n return normalizeAppIncludePure(this.options.include)\n }\n\n /** Thin delegator to the pure {@link resolvePreferredPortPure}, threading env + config. */\n private resolvePreferredPort(appName: string, devConfig: DevConfig): number | undefined {\n return resolvePreferredPortPure(appName, process.env, devConfig)\n }\n\n /** Thin delegator to the pure {@link resolvePrefixUrlPure}. */\n private resolvePrefixUrl(appName: string, devConfig: DevConfig): string {\n return resolvePrefixUrlPure(appName, devConfig)\n }\n\n /** Load the top-level `devServersPresets` map from the resolved infra-kit config (defensive: `{}` on failure). */\n private async loadDevPresets(): Promise<DevPresets> {\n try {\n return (await getInfraKitConfig()).devServersPresets ?? {}\n } catch {\n return {}\n }\n }\n\n /**\n * The preset definition to run: the named preset (`infra-kit dev <preset>`; throws with the\n * available names when unknown), or an `apps`-less preset when no preset was given \u2014 which\n * `resolvePreset` expands to every discovered app + part.\n */\n private resolvePresetDef(devPresets: DevPresets): DevPreset {\n // A wizard-built in-memory preset wins over the named lookup: it already IS the resolved run plan.\n if (this.options.presetDef != null) {\n return this.options.presetDef\n }\n\n const name = this.options.preset\n\n if (name == null) {\n return {}\n }\n\n const def = devPresets[name]\n\n if (!def) {\n const available = Object.keys(devPresets)\n\n throw new Error(\n `Unknown dev preset \"${name}\". Available: ${available.length > 0 ? available.join(', ') : '(none defined in devServersPresets)'}`,\n )\n }\n\n return def\n }\n\n public async start(): Promise<void> {\n // Backend readiness clock (UI is fire-and-forget, so `ready in Xs` is BE-only \u2014 labeled honestly).\n const bootStart = Date.now()\n const include = this.normalizeAppInclude()\n const watch = this.options.watch ?? false\n const devConfig = await this.loadDevConfig()\n\n process.env.POWERTOOLS_DEV ??= 'true'\n process.env.LOG_LEVEL ??= 'DEBUG'\n\n this.renderer.narrate('\uD83D\uDE80 Starting Development Server Runner')\n\n if (watch) {\n this.renderer.narrate('\uD83D\uDC40 Watch mode: will rebuild and restart on file save')\n }\n\n const { apps, uiApps, apiAppsAll, uiAppsAll, wantedLocalPkgs, presetProxy } = await this.resolveRunPlan(\n devConfig,\n include,\n )\n\n if (apps.length === 0 && uiApps.length === 0) {\n this.renderer.log('\u26A0\uFE0F No API or UI apps to run for this preset', 'warn')\n\n return\n }\n\n await this.bringUpProxy(apps)\n await this.buildAll(apps, uiApps, watch)\n\n if (apps.length > 0) {\n this.renderer.bootStep('starting servers')\n await this.startAllApps(apps)\n this.renderer.narrate(\n this.failedApps.length === 0\n ? '\uD83C\uDF89 All servers started!'\n : `\u26A0\uFE0F ${this.appServers.length}/${apps.length} servers started \u2014 ${this.failedApps.length} failed`,\n )\n this.renderer.narrate(`\uD83D\uDCDD Logs \u2192 ${homeShorten(this.sink.dir)} (one file per service)`)\n }\n\n // The label for what the user asked to run. Derived from the post-`--app` sets \u2014 not from\n // `options.preset` (unset for the wizard's in-memory preset) nor from `include` (app names only).\n // Hoisted above the refusal below so both it and the ready header name the run the same way.\n const target = deriveTargetLabel({\n preset: this.options.preset,\n running: [\n ...apps.map((a) => {\n return `${a.name}/api`\n }),\n ...uiApps.map((a) => {\n return `${a.name}/ui`\n }),\n ],\n discovered: [\n ...apiAppsAll.map((a) => {\n return `${a.name}/api`\n }),\n ...uiAppsAll.map((a) => {\n return `${a.name}/ui`\n }),\n ],\n })\n\n // A backend that was asked for and died takes its frontend's `local` routes down with it \u2014 silently,\n // to cloud. Refuse BEFORE printReady: a refusal must not register portless aliases or spawn a vite it\n // is about to abandon, and the message carries the backend's real error, so there is nothing left in\n // the header the user still needs.\n //\n // `--watch` is the one exception, and only because it can genuinely fix this: a boot-failed app is a\n // restart target now (see {@link resolveRestartTargets}), so the next save can bring the backend up\n // and the route back to local. It stays resident with a loud, self-clearing `\u26A0 \u2026 \u25CF cloud` row instead.\n //\n // That healing is real, and it is the `infraKit()` vite PLUGIN that makes it real: its `configureServer`\n // hook watches the dev-context fragment dir, re-resolves the proxy, and restarts vite when the resolved\n // map changes (verified live \u2014 `[vite] server restarted.` the moment a fragment appears). A UI that\n // instead wires the bare `infraKitDev()` helper directly in its vite config has NO such watcher: its\n // proxy is baked at config load, so a backend recovering mid-session will not flip its route back to\n // `local` until that UI is restarted. Discovery treats both shapes as managed, so on such a UI this row\n // can clear while the traffic still goes to cloud. The plugin is the supported wiring and every\n // consumer uses it today; stated here so the assumption is on the record rather than merely held.\n this.degradedRoutes = await this.collectDegradedRoutes(uiApps, wantedLocalPkgs, presetProxy)\n if (this.degradedRoutes.length > 0 && !watch) {\n throw new Error(formatPairingRefusal(this.degradedRoutes, target))\n }\n\n // Collapse the boot spinner into the calm ready header (BE endpoints + UI reference lines).\n // Runs for a UI-only session too, so it never leaves a blank screen. The route dump is opt-in.\n await this.printReady(apps, uiApps, bootStart, target)\n if (this.options.routes) {\n this.printRouteDump()\n }\n\n // Everything the user asked for is dead and there is no UI to fall back on: there is nothing left\n // to serve, watch, or proxy. Resident-but-empty is the worst of both worlds \u2014 it looks like a\n // running dev server and exits 0 when finally interrupted, so a CI step or a script would call it\n // a success. Fail loudly instead. A PARTIAL failure stays resident: the survivors are still useful,\n // and under `--watch` a boot-failed app IS retried on the next save ({@link resolveRestartTargets}) \u2014\n // without `--watch` it is gone for the session, which is why a broken local pairing refuses above.\n if (this.appServers.length === 0 && uiApps.length === 0 && this.failedApps.length > 0) {\n throw new Error(\n `infra-kit dev: no app started (${this.failedApps.length} of ${apps.length} failed). ` +\n `First failure: ${this.failedApps[0]?.reason ?? 'unknown'}`,\n )\n }\n\n this.armWatch(apps, uiApps, watch)\n\n // Frontends last: their delegated `turbo run dev` feeds the live tail below the BE table.\n if (uiApps.length > 0) {\n this.startUiDev(uiApps)\n }\n\n // Armed UNCONDITIONALLY, including for a UI-only session with nothing to probe.\n //\n // The tick does two jobs now: it probes backend health, and it repaints the status panel. The probe\n // half is a backend concern; the repaint half is not \u2014 and gating the whole timer on\n // `appServers.length > 0` gated the PANEL on a backend existing. A UI-only session painted once at\n // boot and then froze: a `\u26A0 0` row held forever while vite piped compile errors into its log file\n // and the counter behind the row climbed unread. A green row over a broken UI is precisely the lie\n // this design exists to prevent, and it is worse here than the tail it replaced, because there is no\n // longer anything else on screen to contradict it.\n //\n // Over an empty `appServers` the probe half is `Promise.all([])` \u2014 free. The timer is `unref`'d, so\n // it cannot hold the loop open on its own.\n this.startLivenessMonitor()\n }\n\n /**\n * Resolve WHAT to run: discover the api/ui app parts, resolve the active preset against them, then\n * narrow to the `--app`/`--self` include set. Returns both the filtered run sets (`apps`/`uiApps`)\n * and the full discovered sets (`apiAppsAll`/`uiAppsAll`), which the ready-header target label needs.\n */\n private async resolveRunPlan(\n devConfig: DevConfig,\n include: string[] | null,\n ): Promise<{\n apps: IApiAppConfig[]\n uiApps: DiscoveredUiApp[]\n apiAppsAll: IApiAppConfig[]\n uiAppsAll: DiscoveredUiApp[]\n /**\n * Backend packages the PRESET promised to serve locally, captured BEFORE `--app`/`--self` narrowing.\n * Narrowing changes what runs; it does not change what the preset promised, and a backend dropped by\n * a narrowing flag is never attempted \u2014 so it never lands in `failedApps` and a crash-keyed check\n * would say nothing while its frontend quietly proxied to cloud. See `local-pairing.ts`.\n */\n wantedLocalPkgs: Set<string>\n /** The preset's declared per-route proxy overrides (`app \u2192 route \u2192 source`), for `pinnedLocal`. */\n presetProxy: Record<string, Record<string, ProxySource>>\n }> {\n // What to run is a named preset (`infra-kit dev <preset>`), resolved against the discovered app\n // parts (api/ui). No preset \u2192 run everything (`*`). `--app`/`--self` (`include`) further narrow it.\n const apiAppsAll = this.discoverApiApps(devConfig)\n const uiAppsAll = discoverUiAppsBare(this.monorepoRoot)\n const resolved = resolvePreset(this.resolvePresetDef(await this.loadDevPresets()), {\n api: apiAppsAll.map((a) => {\n return a.name\n }),\n ui: uiAppsAll.map((a) => {\n return a.name\n }),\n })\n\n if (resolved.unmatched.length > 0) {\n this.renderer.log(`\u26A0\uFE0F Preset targets not found (skipped): ${resolved.unmatched.join(', ')}`, 'warn')\n }\n\n const apiNames = new Set(\n resolved.targets\n .filter((t) => {\n return t.part === 'api'\n })\n .map((t) => {\n return t.app\n }),\n )\n const uiNames = new Set(\n resolved.targets\n .filter((t) => {\n return t.part === 'ui'\n })\n .map((t) => {\n return t.app\n }),\n )\n const passesInclude = (name: string): boolean => {\n return !include || include.includes(name)\n }\n const watchDepsByApp = new Map(\n resolved.targets\n .filter((t) => {\n return t.part === 'api'\n })\n .map((t) => {\n return [t.app, t.watchDeps] as const\n }),\n )\n const apps = apiAppsAll\n .filter((a) => {\n return apiNames.has(a.name) && passesInclude(a.name)\n })\n .map((a) => {\n return { ...a, watchDeps: watchDepsByApp.get(a.name) ?? a.watchDeps }\n })\n const uiApps = uiAppsAll.filter((a) => {\n return uiNames.has(a.name) && passesInclude(a.name)\n })\n\n // Every api the PRESET names, pre-`passesInclude` \u2014 the promise, not the survivors.\n const wantedLocalPkgs = new Set(\n apiAppsAll\n .filter((a) => {\n return apiNames.has(a.name)\n })\n .map((a) => {\n return a.packageName\n }),\n )\n\n return { apps, uiApps, apiAppsAll, uiAppsAll, wantedLocalPkgs, presetProxy: resolved.proxy }\n }\n\n /**\n * Which of this run's routes are about to be silently misrouted to cloud, because the local backend\n * they name failed to start (see `local-pairing.ts` for why this is the dangerous half).\n *\n * Reads each launched frontend's own `infra-kit.config.ts` \u2014 the SAME file, through the same loader,\n * that the vite helper will read when it resolves the proxy. That is deliberate: any second source of\n * truth here could disagree with the proxy the frontend actually ends up serving, and a disagreement\n * would either cry wolf or (far worse) stay quiet on a real one. A frontend with no `dev.proxy` block\n * declares no routes and can degrade nothing.\n */\n private async collectDegradedRoutes(\n uiApps: DiscoveredUiApp[],\n wanted: ReadonlySet<string>,\n presetProxy: Record<string, Record<string, ProxySource>>,\n ): Promise<DegradedRoute[]> {\n if (uiApps.length === 0) return []\n\n const uis: LaunchedUi[] = []\n\n for (const ui of uiApps) {\n // A config that throws must not take the run down here: the frontend is about to load the very same\n // file itself and will report it far better than this check can. Treat it as \"declares no routes\"\n // and let vite own the error.\n const dev = await loadDev(ui.path).catch(() => {\n return undefined\n })\n\n if (!dev?.proxy) continue\n\n const overrides = presetProxy[ui.name] ?? {}\n const routes = Object.fromEntries(\n Object.entries(dev.proxy.routes).map(([route, spec]) => {\n return [route, { ...spec, pinnedLocal: overrides[route] === 'local' }]\n }),\n )\n\n uis.push({ app: ui.name, routes, cloudTemplate: dev.proxy.templates.cloud })\n }\n\n // Stashed so the ready header's per-app proxy listing resolves off the exact same loaded routes the\n // degraded check does \u2014 one read, one source of truth, no chance of the two disagreeing.\n this.launchedUis = uis\n\n return findDegradedRoutes({\n uis,\n wanted,\n // REALITY: the packages actually serving. `appServers` holds exactly the apps that bound a port and\n // wrote a dev-context fragment \u2014 which is the same fact the vite helper reads to decide `local`.\n running: new Set(\n this.appServers.map(({ app }) => {\n return app.packageName\n }),\n ),\n reasons: new Map(\n this.failedApps.map(({ app, reason }) => {\n return [app.packageName, { app: app.name, reason }]\n }),\n ),\n env: process.env[INFRA_KIT_ENV_VAR],\n })\n }\n\n /**\n * The resolved proxy rows for one frontend, for its nested \"which proxy, and where\" list \u2014 or undefined\n * when it declares none, so a frontend with no `dev.proxy` block adds no empty group to the header.\n */\n private proxiesFor(app: string): ProxyRouteRow[] | undefined {\n const rows = this.proxyRoutes\n .filter((p) => {\n return p.uiApp === app\n })\n .map((p): ProxyRouteRow => {\n return { route: p.route, source: p.source, target: p.target }\n })\n\n return rows.length > 0 ? rows : undefined\n }\n\n /**\n * Layer B: confirm the portless proxy is serving BEFORE any alias registration \u2014 backends register in\n * startOneApp, UIs in printReady, and both need a live daemon. Runs ahead of startAllApps AND startUiDev\n * so a UI-only session checks it too; a proxy that is not up throws here, before anything is spawned.\n *\n * The port is no longer negotiable: every dev URL is `https://<alias>` with no port, which means the one\n * port that can serve them is {@link DEFAULT_DEV_PROXY_PORT} (443, the implicit HTTPS port).\n */\n private async bringUpProxy(apps: IApiAppConfig[]): Promise<void> {\n // Before anything is spawned or aliased: refuse if the consumer's PINNED `infra-kit/vite` predates the\n // HTTPS contract. It would ignore the fragment's `origin` and proxy plain HTTP at a TLS listener.\n assertHelperVersionFloor(findMonorepoRoot(process.cwd()))\n await this.ensureProxy()\n\n if (apps.length > 0) this.assertNoPortConflicts(apps)\n }\n\n /**\n * Build both build phases back to back under one boot line: the API boot closure (dist must exist\n * before servers import handlers) then the UI dependency closure (warmed BEFORE any persistent child\n * so both see a warm cache and the cold-cache double-build race can't corrupt shared dist).\n */\n private async buildAll(apps: IApiAppConfig[], uiApps: DiscoveredUiApp[], watch: boolean): Promise<void> {\n // One boot line covers both build phases: they run back to back, so two boot steps only make\n // the spinner flip between near-identical lines. Never empty \u2014 the no-apps case returned above.\n const bootLabel = [\n apps.length > 0 ? `building ${packageList(apps)}` : '',\n uiApps.length > 0 ? `warming ${packageList(uiApps)}` : '',\n ]\n .filter(Boolean)\n .join(' \u00B7 ')\n\n this.renderer.bootStep(bootLabel)\n\n if (apps.length > 0) await this.buildApps(apps, watch)\n if (uiApps.length > 0) await this.buildUiApps(uiApps)\n }\n\n /**\n * Start the watch engine when there's anything to rebuild \u2014 backends OR frontends. A UI-only session\n * (no API app) still needs the engine so a shared-lib edit rebuilds its dist and vite reloads.\n */\n private armWatch(apps: IApiAppConfig[], uiApps: DiscoveredUiApp[], watch: boolean): void {\n if (!(watch && (this.appServers.length > 0 || uiApps.length > 0))) {\n return\n }\n\n // Arm the watcher FIRST, on the fail-safe `null` map (a shared-package change restarts every app).\n // Awaiting the closure map here instead cost ~1s on a 7-backend repo \u2014 a full second after \"ready\"\n // in which a save was watched by nobody. Scoping is an optimisation; never buy it with a blind window.\n this.setupWatch(apps, uiApps)\n\n this.closureBuild = this.buildClosureMapSafe(apps).then((map) => {\n // A teardown may have overtaken us; assigning is harmless either way (nothing reads it after).\n this.closureMap = map\n })\n }\n\n /**\n * Warm ONLY each UI's dependency closure (`<pkg>^...` \u2014 deps, excluding the UI itself, so no full\n * production `vite build`) with a cache-friendly (non-`--force`) turbo build.\n *\n * This is the ONLY build of that closure: the `turbo run dev` child runs with `--only`, so it no\n * longer re-walks `^build` as a fallback (that walk was pure noise \u2014 see {@link file://./ui-dev.ts}).\n * Still non-fatal, because a failure here is loud rather than silent: vite fails to resolve the\n * missing dep and reports it in the live tail. Warn and continue instead of refusing to start.\n */\n private async buildUiApps(uiApps: DiscoveredUiApp[]): Promise<void> {\n const filters = uiApps\n .map((a) => {\n return `--filter=${a.packageName}^...`\n })\n .join(' ')\n\n try {\n await this.runBuild(\n `pnpm exec turbo run build ${filters} --env-mode=loose --output-logs=errors-only --no-update-notifier`,\n this.renderer.logFn,\n )\n this.renderer.narrate('\u2705 UI deps built')\n } catch (error) {\n this.renderer.log(\n `\u26A0\uFE0F UI dep build failed (continuing; vite will report unresolved deps): ${String(error)}`,\n 'warn',\n )\n }\n }\n\n /**\n * Start the frontends via ONE delegated `turbo run dev` child. Its stdio is PIPED, so the raw output\n * is tee'd verbatim to the runner log while each framework line lands in the renderer's tagged tail \u2014\n * the terminal stays owned by `infra-kit dev`. Reaped on {@link shutdown}. Concurrency \u2265 the persistent\n * UI `dev` task count (turbo hard-errors otherwise).\n */\n private startUiDev(uiApps: DiscoveredUiApp[]): void {\n const names = uiApps\n .map((a) => {\n return a.name\n })\n .join(', ')\n\n this.renderer.narrate(`\uD83C\uDFA8 Starting ${uiApps.length} UI dev server(s) via \\`turbo run dev\\`: ${names}`)\n this.renderer.narrate(' (framework output is routed into the live tail; full detail in the runner log)')\n\n // Hand the runner-assigned UI ports to the vite child so each UI binds exactly the port the ready\n // header already advertised (`strictPort` on the vite side) \u2014 proxy or not, the printed URL and the\n // bound port cannot drift. An empty map (every assignment failed) passes no env, so vite falls back\n // to picking its own free port and printing it.\n const uiPortEnv =\n Object.keys(this.uiPortMap).length > 0 ? { INFRA_KIT_UI_PORTS: JSON.stringify(this.uiPortMap) } : undefined\n\n // Every dev URL is now HTTPS behind portless's PRIVATE CA, which Node's bundled trust store knows\n // nothing about (and Node does not read the macOS keychain, so `portless trust` alone does not help a\n // Node process). Vite's own proxy is covered by the scoped `secure: false`, but anything ELSE the dev\n // loop runs \u2014 a backend calling a sibling's hero URL, a node `fetch`, an e2e runner \u2014 would fail with\n // `SELF_SIGNED_CERT_IN_CHAIN`. Hand the CA down so those clients validate instead of breaking.\n // A pre-set NODE_EXTRA_CA_CERTS wins: it is the user's own trust decision, not ours to overwrite.\n const caEnv =\n process.env.NODE_EXTRA_CA_CERTS == null && fs.existsSync(readCaPath())\n ? { NODE_EXTRA_CA_CERTS: readCaPath() }\n : undefined\n\n // turbo tags each line with the PACKAGE name; the endpoint rows are keyed by APP name (`client/ui`),\n // so map back onto the exact label the ready header already shows. An unrecognized package degrades\n // to its own name rather than being dropped \u2014 a line is never silently lost.\n const tagByPackage = new Map(\n uiApps.map((a) => {\n return [a.packageName, `${a.name}/ui`] as const\n }),\n )\n\n this.uiDev = this.uiDevFactory({\n packageNames: uiApps.map((a) => {\n return a.packageName\n }),\n cwd: process.cwd(),\n concurrency: Math.max(uiApps.length + 4, 12),\n env: uiPortEnv || caEnv ? { ...uiPortEnv, ...caEnv } : undefined,\n // The RAW chunk tee: turbo's chrome plus every framework line, un-de-multiplexed. It arrives\n // before `parseTurboDevLine` has attributed it to a package, so there is no honest app to file it\n // under \u2014 it gets its own `turbo.log`. The attributed lines land in `<app>/ui` below.\n appendLog: (text) => {\n this.sink.write(TURBO_SERVICE, text)\n },\n onLine: ({ pkg, text, level }) => {\n const tag = tagByPackage.get(pkg) ?? `${pkg}/ui`\n\n this.sink.write(tag, text, { level })\n },\n // Surface a silently-dead frontend engine: once `turbo run dev` exits, every UI's live reload\n // stops and no framework line ever reaches the tail again.\n onUnexpectedExit: (detail) => {\n this.markUiEngineDead()\n this.reportEngineDeath('UI dev engine (`turbo run dev`)', 'frontends stopped reloading', detail)\n },\n })\n }\n\n /** Render an app list as `name:port, name:port` for log lines. */\n private formatAppList(apps: Array<{ name: string; port: number }>): string {\n return apps\n .map((a) => {\n return `${a.name}:${a.port}`\n })\n .join(', ')\n }\n\n /**\n * Throw (after logging remediation tips) when two apps are EXPLICITLY pinned to the same\n * port. Apps with no explicit port bind an ephemeral `listen(0)` port each (collision-free\n * by construction), so they are excluded from the gate \u2014 otherwise the default multi-app\n * run would false-throw on the shared `DEFAULT_PORT` before dynamic allocation de-conflicts.\n */\n private assertNoPortConflicts(apps: IApiAppConfig[]): void {\n const explicitApps = apps\n .filter((app) => {\n return app.preferredPort != null\n })\n .map((app) => {\n return { name: app.name, port: app.preferredPort! }\n })\n const { duplicatePorts, conflictingApps } = findPortConflicts(explicitApps)\n\n if (duplicatePorts.length === 0) {\n return\n }\n\n this.renderer.log(`\u26A0\uFE0F Port conflict detected! ${duplicatePorts.join(', ')}`, 'error')\n this.renderer.log(`Conflicting apps: ${this.formatAppList(conflictingApps)}`, 'error')\n this.renderer.log('\\n\uD83D\uDCA1 Tip: give each app a distinct port via `{APP}_PORT` env (e.g. `CLIENT_PORT=`,', 'error')\n this.renderer.log(' `SEARCH_ENGINE_PORT=`) or `dev.<app>.port` in infra-kit.json; or run a subset with', 'error')\n this.renderer.log(' `--app=<name>,<name>`.\\n', 'error')\n throw new Error(`Port conflict detected: ${duplicatePorts.join(', ')}`)\n }\n\n /** Build every app via turbo; rethrows the build error after logging stdout/stderr. */\n private async buildApps(apps: IApiAppConfig[], watch: boolean): Promise<void> {\n const filters = apps\n .map((a) => {\n return `--filter=${a.packageName}`\n })\n .join(' ')\n // With `--watch`, always bypass Turbo cache so `tsc` runs and `dist/` matches disk (otherwise watch restarts can be no-ops).\n const buildCmd = `pnpm exec turbo run build ${filters} --env-mode=loose --output-logs=errors-only --no-update-notifier${watch ? ' --force' : ''}`\n\n // No narration here: the `building <pkgs>` boot step already names these exact targets.\n try {\n await this.runBuild(buildCmd, this.renderer.logFn)\n this.renderer.narrate('\u2705 Build complete')\n } catch (buildError) {\n this.renderer.log(`\u274C Build failed: ${String(buildError)}`, 'error')\n if (buildError instanceof Error && buildError.message) {\n this.renderer.log(` ${buildError.message}`, 'error')\n }\n const err = buildError as { stdout?: string; stderr?: string }\n\n if (err.stdout) this.renderer.log(` stdout: ${err.stdout.trim()}`, 'error')\n if (err.stderr) this.renderer.log(` stderr: ${err.stderr.trim()}`, 'error')\n throw buildError\n }\n }\n\n /**\n * Start every app concurrently, collecting the ones that boot; per-app failures are logged,\n * not fatal. Safe to parallelize because each app has a distinct port (guarded up-front) and a\n * distinct `ServerlessLocalRun`, and `startOneApp` no longer mutates cwd. Push order into\n * `appServers` is non-deterministic but nothing depends on it (the table renders from `apps`).\n */\n private async startAllApps(apps: IApiAppConfig[]): Promise<void> {\n await Promise.all(\n apps.map(async (app) => {\n try {\n const started = await this.startOneApp(app)\n\n if (started) {\n this.appServers.push({ app, ...started, startedAt: Date.now(), restarts: 0 })\n }\n } catch (error) {\n this.renderer.log(`\u274C Failed to start ${app.name}: ${String(error)}`, 'error')\n // Recorded, not just logged: `printReady` renders the table from `appServers`, so an app\n // that only ever appears in a log line vanishes from the header entirely \u2014 indistinguishable\n // from one that was never requested. `failedApps` is what puts the `\u25CF failed` row back.\n this.failedApps.push({ app, reason: errorReason(error) })\n }\n }),\n )\n }\n\n /**\n * Confirm a portless daemon is serving TLS on {@link DEFAULT_DEV_PROXY_PORT} before any alias is\n * registered. Every dev URL is a hostname served by this daemon, so a proxy that is not up is a FATAL\n * start error, not a degraded mode: there is no second way to reach an app, and a half-started dev loop\n * that silently routes nowhere is worse than a refusal that names the fix.\n *\n * **Probe only \u2014 never start, never elevate.** `:443` is privileged, and portless binds it by re-execing\n * through `sudo` with an inherited stdio, which a detached child cannot answer. The daemon is installed\n * once, out-of-band. There is deliberately no unprivileged fallback: a fallback puts the port back in the\n * URL, which is the whole thing this design removes.\n *\n * Identity is proven **on the wire** (`X-Portless`), never from portless's state files \u2014 those are\n * process-global singletons that any other daemon's start rewrites and any stop deletes, which would make\n * a perfectly healthy `:443` daemon look dead. See {@link defaultIsProxyServing}.\n *\n * @throws When portless is missing, or no portless daemon is serving TLS on the proxy port.\n */\n private async ensureProxy(): Promise<void> {\n // The bin comes from the driver we were handed, never from a fresh resolution behind its back: the fix we\n // print must name the binary THIS driver would run. `null` is not a command to render \u2014 it is a different\n // report, and it is made here rather than passed downstream.\n const bin = this.proxy.binPath()\n\n if (bin == null || !(await this.proxy.isAvailable())) {\n throw new Error(\n 'infra-kit dev: portless is not installed, so no dev URL can resolve. It ships as a dependency of infra-kit \u2014 reinstall with `pnpm install`.',\n )\n }\n\n if (await this.proxy.isProxyServing(this.proxyPort, true)) return\n\n throw new Error(\n `infra-kit dev: no portless daemon is serving HTTPS on :${this.proxyPort}, so no dev URL can resolve. ` +\n 'Install it once (this is the only step that needs root):\\n' +\n ` ${formatPortlessCommand(['service', 'install'], { sudo: true, bin })}\\n` +\n 'Then trust its local CA (no sudo needed):\\n' +\n ` ${formatPortlessCommand(['trust'], { bin })}\\n` +\n '`infra-kit doctor` checks both.',\n )\n }\n\n /**\n * Register `<release>.<package>` \u2192 `port` with portless and return the alias HOST\n * (`<release>.<package>.localhost`). The alias IS the app's only address, so a failure here is fatal\n * rather than a silent downgrade \u2014 an app nobody can reach is not a running app.\n *\n * @throws When the package name yields no legal DNS label, or portless rejects the registration.\n */\n private async registerAppAlias(packageName: string, appDir: string, port: number): Promise<string> {\n const release = readAppRelease(appDir)\n // An npm name is not a DNS label \u2014 see {@link slugifyHostLabel}. `infra-kit/vite` slugifies its\n // own `<packageName>` template token identically, so the proxy target and this alias cannot drift.\n const label = slugifyHostLabel(packageName)\n\n if (label === '') {\n throw new Error(`infra-kit dev: package name \"${packageName}\" has no letters or digits to build a hostname from.`)\n }\n const name = `${release}.${label}`\n\n if (!(await this.proxy.registerAlias(name, port))) {\n throw new Error(`infra-kit dev: portless refused the alias \"${name}\" \u2192 127.0.0.1:${port}.`)\n }\n this.registeredAliases.add(name)\n\n return `${name}.localhost`\n }\n\n private async startOneApp(app: IApiAppConfig): Promise<StartedApp | null> {\n this.renderer.narrate(`\uD83D\uDD04 Starting ${app.name}...`)\n\n // No `process.chdir` here: `ServerlessLocalRun` reads `serverless.yml` and imports the\n // compiled handler from `controllersPath` (absolute), so the runner never mutates cwd \u2014\n // which is what makes concurrent boot/restart safe.\n const server = new ServerlessLocalRun({\n controllersPath: app.path,\n prefixUrl: app.prefixUrl,\n port: app.preferredPort,\n appName: app.name,\n // Claims every line this app's handlers emit \u2014 `console.log`, Powertools, a dependency's banner \u2014\n // for `<app>/api`, via an AsyncLocalStorage context entered in the request's `onRequest` hook and\n // around the handler module's import.\n //\n // Without it NOTHING attributes: the backend is in-process and multi-app, so a raw stdout write\n // says nothing about which app produced it, and every handler line falls into the runner's\n // fallback bucket. The app's row then counts zero errors no matter how loudly its handler fails \u2014\n // which, with no log tail on screen, means the panel reports a healthy app that is broken.\n serviceTag: `${app.name}/api`,\n // Route live request traffic into the renderer's tagged, timestamped tail (`<app>/api \u2026`).\n // This is the structured seam \u2014 independent of the legacy `DEV_SERVER_REQUEST_LOG` raw line \u2014\n // so the app name is threaded in-process and never leaked to spawned turbo/vite children.\n onRequestLog: ({ method, path: reqPath, status, ms }) => {\n // Keep the runner's own `/__health` liveness probes out of the live tail \u2014 they are internal\n // noise, not app traffic. Real handler routes still stream.\n if (reqPath === '/__health') return\n\n const tag = `${app.name}/api`\n const text = `${method} ${reqPath} ${status} ${ms}ms`\n\n // The level is DECLARED, not sniffed: fastify already knows the status it returned. A 5xx is an\n // error because the server said so \u2014 no regex ever reads this line's bytes to decide.\n this.sink.write(tag, text, { level: status >= 500 ? 'error' : 'info' })\n // Mutate in place: spreading into a fresh array copied the whole window on every single request.\n const window = this.reqTimes.get(tag)\n\n if (window) window.push(Date.now())\n else this.reqTimes.set(tag, [Date.now()])\n },\n })\n\n // `start()` binds an ephemeral (or preferred-then-ephemeral) port and RETURNS the actual\n // one \u2014 consume THAT, never the static preferred hint, so the log/table/health agree.\n const boundPort = await server.start()\n\n // Layer B: (re)point the portless alias to the freshly-bound port (also fires on watch-restart,\n // since startOneApp is the shared start+restart path). Throws if the alias cannot be registered.\n //\n // Unwind the bind on failure: the caller only records `server` in `appServers` once this method\n // RETURNS, so a throw here would otherwise leave a listening fastify that `shutdown()` never sees\n // and never closes \u2014 a port held for the rest of the session.\n let alias: string\n\n try {\n alias = await this.registerAppAlias(app.packageName, app.path, boundPort)\n } catch (error) {\n await server.close().catch(() => {})\n throw error\n }\n\n // Record the ACTUAL bound port + the alias that was actually registered in this runner's own\n // dev-context fragment. Ordered AFTER registerAppAlias: a fragment written before it could only\n // ever claim `alias: undefined`, and the vite helper would fall back to a direct target for an\n // app that IS reachable by name. Inside startOneApp (the shared start+restart path) so a\n // watch-restart refreshes both instead of orphaning a stale fragment (M2). Non-fatal: a\n // fragment-write failure must not down the server.\n try {\n this.writeDevContextFragment(app, boundPort, alias)\n } catch (error) {\n this.renderer.log(`\u26A0\uFE0F Failed to write dev-context fragment for ${app.name}: ${String(error)}`, 'warn')\n }\n\n this.renderer.narrate(`\u2705 ${app.name} started on port ${boundPort}`)\n\n return { server, boundPort, alias }\n }\n\n /**\n * Atomically write this runner's `.infra-kit/dev-context/<app>.json` fragment recording the\n * ACTUAL bound port (REV-5: serialize to a same-dir temp file, then `renameSync` into place, so a\n * concurrent reader \u2014 the vite helper's directory merge \u2014 never observes torn JSON). Each runner\n * writes ONLY its own app's fragment, so cmux panes never clobber each other.\n */\n private writeDevContextFragment(app: IApiAppConfig, boundPort: number, alias: string): void {\n const fragment: DevContextFragment = {\n // Declares the wire contract this fragment honours, so the helper never has to INFER it from a\n // package version \u2014 which stopped being inferable the moment the helper moved to its own npm\n // package with its own version line. `v` promises `origin` below is present and authoritative; the\n // helper refuses rather than guessing a target if that promise is ever broken.\n v: DEV_CONTEXT_WIRE_VERSION,\n package: app.packageName,\n port: boundPort,\n pid: process.pid,\n writtenAt: Date.now(),\n release: readAppRelease(app.path),\n alias,\n // The ORIGIN of the hero URL the ready screen prints (`resolveEndpointUrl` appends the app's\n // `prefixUrl` on top of this same alias). Prefix-free on purpose: this is a proxy target, and the\n // frontend supplies its own path.\n origin: `https://${alias}`,\n }\n const target = path.join(this.devContextDir, `${app.name}.json`)\n const tmp = path.join(this.devContextDir, `${app.name}.json.${process.pid}.tmp`)\n\n fs.mkdirSync(this.devContextDir, { recursive: true })\n fs.writeFileSync(tmp, JSON.stringify(fragment, null, 2))\n fs.renameSync(tmp, target)\n }\n\n /** Remove THIS runner's own `<app>.json` fragment on shutdown (never another runner's). */\n private removeDevContextFragment(app: IApiAppConfig): void {\n fs.rmSync(path.join(this.devContextDir, `${app.name}.json`), { force: true })\n }\n\n /** Run restart jobs one after another (watch can fire faster than close + listen). */\n private scheduleRestartWork(work: () => Promise<void>): Promise<void> {\n const run = this.restartWorkChain.then(\n () => {\n return work()\n },\n () => {\n return work()\n },\n )\n\n this.restartWorkChain = run.catch(() => {})\n\n return run\n }\n\n private async delayPortRelease(): Promise<void> {\n await new Promise((r) => {\n return setTimeout(r, DevServerRunner.PORT_RELEASE_DELAY_MS)\n })\n }\n\n /**\n * Schedule a restart of the given apps (1 or N), serialized against other restarts via\n * {@link scheduleRestartWork}. A single-app dist change passes `[app]`; a dependency-package\n * dist change passes every running app. The `turbo watch` engine has already rebuilt `dist/`,\n * so the runner only bounces the fastify server(s) \u2014 no build here.\n */\n private restart(apps: IApiAppConfig[]): Promise<void> {\n if (this.shuttingDown) return Promise.resolve()\n\n return this.scheduleRestartWork(() => {\n // Re-checked inside the chain: this job may have queued behind a restart that was still\n // running when shutdown() latched, so the flag can flip between scheduling and execution.\n if (this.shuttingDown) return Promise.resolve()\n\n return this.runRestart(apps)\n })\n }\n\n /** Resolve the requested apps to their live server slots (dropping any not running). */\n private resolveRestartTargets(apps: IApiAppConfig[]): Array<{ idx: number; app: IApiAppConfig }> {\n return apps\n .map((app) => {\n return {\n idx: this.appServers.findIndex((e) => {\n return e.app.name === app.name\n }),\n app,\n }\n })\n .filter((t) => {\n // `idx >= 0` is a RUNNING app \u2014 the ordinary restart. `idx === -1` plus a `failedApps` entry is an\n // app that never came up at all, and it is a restart target precisely because it isn't running:\n // watch used to filter it out, which meant a backend that died on boot stayed dead for the whole\n // session no matter how many times you fixed and saved the file that broke it. Its frontend sat\n // there proxying to cloud the entire time. Everything else \u2014 an app the run never launched \u2014 is\n // neither, and is correctly skipped.\n return t.idx >= 0 || this.isFailedApp(t.app.name)\n })\n }\n\n /** Did this app fail to start (and is therefore still absent from {@link appServers})? */\n private isFailedApp(name: string): boolean {\n return this.failedApps.some((f) => {\n return f.app.name === name\n })\n }\n\n /**\n * Promote an app that just came back from the dead: it has no {@link appServers} slot to overwrite, so\n * it is appended, cleared from `failedApps`, and given the endpoint row the boot header never made for\n * it. Without that last step the panel \u2014 which maps over `lastSummary.endpoints` \u2014 would keep the\n * app invisible even though it is now serving.\n */\n private promoteRecoveredApp(app: IApiAppConfig, started: StartedApp): IAppServer {\n const entry: IAppServer = { app, ...started, startedAt: Date.now(), restarts: 0 }\n const tag = `${app.name}/api`\n\n // Seed the health entry BEFORE the app is visible in `appServers`, not after the probe the caller takes\n // a few lines later. The liveness tick can fire in that window, and {@link healthOf} treats an ABSENT\n // entry as `unknown` \u2014 a row with no dot at all for a server that is already serving. A fresh entry is\n // the honest prior instead: never-up, which for a backend reads `\u25CF down` until a probe says otherwise.\n this.healthEntry(tag, 'api')\n this.appServers.push(entry)\n\n const failedIdx = this.failedApps.findIndex((f) => {\n return f.app.name === app.name\n })\n\n if (failedIdx >= 0) this.failedApps.splice(failedIdx, 1)\n\n if (this.lastSummary) {\n this.lastSummary = {\n ...this.lastSummary,\n endpoints: [\n ...this.lastSummary.endpoints,\n {\n tag,\n url: resolveEndpointUrl({ prefixUrl: app.prefixUrl, alias: started.alias }),\n // NOT `ok`. This app has bound a port; nothing has probed it. A server that binds and then 500s\n // on `/__health` would be painted green here on the strength of having started \u2014 the same\n // unearned claim the `\u25CF failed` row exists to prevent. The caller probes right after and\n // {@link refreshStatus} paints the answer.\n health: this.healthOf(tag),\n },\n ],\n failed: (this.lastSummary.failed ?? []).filter((f) => {\n return f.tag !== tag\n }),\n }\n }\n\n return entry\n }\n\n private async runRestart(apps: IApiAppConfig[]): Promise<void> {\n const targets = this.resolveRestartTargets(apps)\n\n if (targets.length === 0) return\n\n const label = targets.length === 1 ? targets[0]!.app.name : `${targets.length} apps`\n\n this.renderer.log(`\uD83D\uDD04 Restarting ${label}...`)\n await Promise.all(\n targets.map(async ({ idx }) => {\n // A boot-failed target (`idx === -1`) has no server to close \u2014 it never bound one. Only a running\n // app is torn down before its replacement starts.\n if (idx < 0) return\n\n try {\n await this.appServers[idx]!.server.close()\n } catch (err) {\n this.renderer.log(` Close warning: ${String(err)}`, 'debug')\n }\n }),\n )\n\n await this.delayPortRelease()\n\n // Thread each restart's OUTCOME (the fresh entry, or `null` on failure) straight into the probe below\n // instead of re-reading `appServers[idx]`: a failed `startOneApp` leaves the stale, now-closed entry in\n // that slot, so re-reading it would probe a freed port \u2014 wasting the full probe timeout and, if the old\n // `close()` also failed, reporting the dead-but-still-listening server as `\u25CF up`. The outcome list makes\n // the no-probe \"down\" branch actually reachable on failure.\n const outcomes = await Promise.all(\n targets.map(async ({ idx, app }) => {\n try {\n const restarted = await this.startOneApp(app)\n\n if (restarted) {\n // A boot-failed app has no slot to overwrite: it is APPENDED and cleared from `failedApps`,\n // which is also what takes its frontend's `\u26A0 \u2026 \u25CF cloud` row down (see {@link degradedRows}).\n if (idx < 0) {\n const recovered = this.promoteRecoveredApp(app, restarted)\n\n this.renderer.log(`\u2705 ${app.name} recovered \u2014 its routes are served locally again`, 'info')\n\n return { app, entry: recovered }\n }\n\n // Carry the restart count forward across the replacement and reset the clock: `up Xs` must\n // measure THIS process, not the one watch just killed, or the panel would claim an uptime\n // for a server that has been alive for two seconds.\n const previous = this.appServers[idx]\n const entry: IAppServer = {\n app,\n ...restarted,\n startedAt: Date.now(),\n restarts: (previous?.restarts ?? 0) + 1,\n }\n\n this.appServers[idx] = entry\n\n return { app, entry }\n }\n\n return { app, entry: null }\n } catch (error) {\n // A target that was ALREADY failed stays failed \u2014 it keeps its `failedApps` entry and its\n // frontend keeps the degraded row. Only report the reason; a retry that fails again is the\n // expected case while the user is still fixing the bug that broke it.\n this.renderer.log(`\u274C Failed to ${idx < 0 ? 'start' : 'restart'} ${app.name}: ${String(error)}`, 'error')\n\n return { app, entry: null }\n }\n }),\n )\n // Show each restarted app's CURRENT bound port: an ephemeral-port app rebinds a fresh port on\n // restart, and the server table is printed only once at boot \u2014 so this line is the only place the\n // new port surfaces in the default (quiet) terminal.\n //\n // Re-probe `/__health` so the summary reports HONEST liveness, not a bare \"restarted\" for a server\n // that binds its port but 500s on the first request. Mirrors printReady's probe (127.0.0.1, never\n // `localhost` \u2014 ServerlessLocalRun binds v4 loopback only). Probes run concurrently; a target whose\n // restart failed (`entry === null`) is reported down without a probe. A single down server downgrades\n // the leading \u2705 to \u26A0\uFE0F so the line reads consistently.\n const probed = await Promise.all(\n outcomes.map(async ({ app, entry }) => {\n const tag = `${app.name}/api`\n\n // A restart that THREW never bound a port \u2014 there is nothing to probe, and nothing to be flap-shy\n // about. Routed through {@link markDown}, not the probe path, because the soft path would leave the\n // row one failure short of the threshold, i.e. GREEN, for a server that does not exist.\n if (entry == null) {\n this.markDown(tag, 'api')\n this.refreshStatus()\n\n return { label: `${app.name} \u25CF down`, healthy: false }\n }\n\n // Through the SAME state machine as every tick, so the panel's dot and this line report the same\n // probe. They used to be two verdicts computed from one result, and they could disagree.\n const target: ProbeTarget = { tag, port: entry.boundPort, kind: 'api' }\n const outcome = await this.healthProbe(target)\n\n this.recordProbe(target, outcome)\n this.refreshStatus()\n\n const healthy = outcome === 'ok'\n\n return { label: `${app.name}:${entry.boundPort} ${healthy ? '\u25CF up' : '\u25CF down'}`, healthy }\n }),\n )\n\n const allHealthy = probed.every((p) => {\n return p.healthy\n })\n\n this.renderer.log(\n `${allHealthy ? '\u2705' : '\u26A0\uFE0F '} Restarted ${probed\n .map((p) => {\n return p.label\n })\n .join(', ')}`,\n )\n }\n\n /**\n * Build the dependency-closure map ({@link buildClosureMap}) for scoped restarts, or `null` on\n * failure (a `turbo --dry` spawn/parse error). `null` is the fail-safe signal: {@link setupWatch}\n * then restarts every launched app on a package change, exactly as it did before scoping \u2014 never\n * a silent dropped restart.\n */\n private async buildClosureMapSafe(apps: IApiAppConfig[]): Promise<ClosureMap | null> {\n try {\n return await buildClosureMap(this.monorepoRoot, apps, this.dryRunner)\n } catch (err) {\n this.renderer.log(\n `\u26A0\uFE0F Dependency-closure map unavailable (${String(err)}); package changes restart all apps`,\n 'warn',\n )\n\n return null\n }\n }\n\n /**\n * Start the long-lived `turbo watch build` engine, then watch compiled `dist/` output to\n * trigger restarts. A change under an app's `dist` restarts that app; a change under a\n * `packages/<pkg>/dist` restarts only the participating backends whose dependency closure\n * includes that package ({@link selectPackageRestartTargets}), keyed per package dir so unrelated\n * packages don't collapse into one debounce bucket. When the closure map is unavailable\n * (`null`) it falls back to restarting every launched app (fail-safe superset). Restarts are\n * build-less \u2014 the engine already rebuilt `dist/`.\n */\n /**\n * Report a persistent engine (`turbo watch build` / `turbo run dev`) that died on its OWN as a single\n * warn line, unless teardown is already underway. Centralises the `shuttingDown` guard and the message\n * shape shared by both engine callbacks \u2014 `superviseChild`'s `killing` latch already suppresses the\n * exit our own `kill()` causes, so this only ever fires on a genuine crash.\n */\n private reportEngineDeath(engine: string, consequence: string, detail: string): void {\n if (this.shuttingDown) return\n this.renderer.log(`\u26A0\uFE0F ${engine} ${detail} \u2014 ${consequence}. Restart \\`infra-kit dev\\`.`, 'warn')\n }\n\n /**\n * The `turbo run dev` engine died on its own, so a UI that was NEVER up is never coming up \u2014 its vite\n * either never bound or went down with the engine, and no further probe is going to tell us anything the\n * engine's corpse has not already said. Recorded as `dead`, which {@link neverUpState} reads ONLY in the\n * never-up branch: a UI that HAS been up keeps its ordinary probe-established death, because turbo does\n * not kill its task process groups and our reap is best-effort \u2014 an orphaned vite may well still be\n * serving, and it would be a live, hot-reloading UI we had just painted red.\n */\n private markUiEngineDead(): void {\n if (this.shuttingDown) return\n\n for (const { tag, kind } of this.uiTargets()) {\n this.healthEntry(tag, kind).dead = true\n }\n this.refreshStatus()\n }\n\n private setupWatch(apps: IApiAppConfig[], uiApps: DiscoveredUiApp[]): void {\n this.turboWatch = this.turboWatchFactory({\n // API apps: dep-inclusive (`...<pkg>`) \u2014 rebuild the backend + its shared-lib closure and restart it.\n depInclusive: apps.map((a) => {\n return a.packageName\n }),\n // UI apps: dep-closure-only (`<pkg>^...`) \u2014 rebuild the frontend's shared libs (so vite reloads on a\n // FE-only lib edit) WITHOUT production-building the UI; vite owns the UI's own live reload.\n depClosure: uiApps.map((a) => {\n return a.packageName\n }),\n cwd: process.cwd(),\n // `turbo watch build` opens this path itself and inherits it as its stdio, so the watch engine\n // gets its own file rather than interleaving raw child bytes into any service's log.\n logFile: this.sink.pathFor(WATCH_SERVICE),\n // Surface a silently-dead engine: once `turbo watch build` exits, saves no longer rebuild `dist/`,\n // so no restart ever fires and the session looks healthy while being frozen.\n onUnexpectedExit: (detail) => {\n this.reportEngineDeath('Watch engine (`turbo watch build`)', 'file saves no longer rebuild', detail)\n },\n })\n this.renderer.narrate('\uD83D\uDC40 Watch mode: started `turbo watch build` engine; watching dist output')\n\n const appDistDirs = getAppDistDirs(apps)\n const packageDistDirs = getPackageDistDirs(this.monorepoRoot)\n const allDistDirs = [...appDistDirs, ...packageDistDirs]\n\n if (allDistDirs.length === 0) {\n this.renderer.log('\u26A0\uFE0F No app or package dist directories found to watch (were they built?)', 'warn')\n\n return\n }\n\n const usePoll = process.env.DEV_SERVER_CHOKIDAR_POLL === '1'\n\n const watcher = chokidar.watch(allDistDirs, {\n ignoreInitial: true,\n awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 100 },\n // Ignore tsc's incremental bookkeeping + sourcemaps: they rewrite on every build\n // (even content-identical ones) and would bounce fastify onto no real change.\n ignored: (p: string): boolean => {\n return p.endsWith('.tsbuildinfo') || p.endsWith('.map')\n },\n ...(usePoll ? { usePolling: true, interval: 400 } : {}),\n })\n\n this.watcher = watcher\n\n if (usePoll) {\n this.renderer.log('\uD83D\uDC40 chokidar: usePolling enabled (DEV_SERVER_CHOKIDAR_POLL=1)', 'debug')\n }\n\n watcher.on('change', (filePath: string) => {\n this.handleDistChange(filePath, apps, appDistDirs, packageDistDirs)\n })\n\n this.renderer.narrate(\n `\uD83D\uDC40 Watching ${appDistDirs.length} app dist + ${packageDistDirs.length} package dist dir(s) for changes...`,\n )\n }\n\n /**\n * Dispatch a single dist-file change to the right restart. A package (shared-lib) change restarts the\n * dependent apps selected from the closure map (or, fail-safe, every app when the map is missing); an\n * app's own dist change restarts just that app. All restarts are debounced.\n */\n private handleDistChange(\n filePath: string,\n apps: IApiAppConfig[],\n appDistDirs: string[],\n packageDistDirs: string[],\n ): void {\n this.renderer.log(`\uD83D\uDC40 dist change detected: ${filePath}`, 'debug')\n\n const change = classifyDistChange(filePath, appDistDirs, packageDistDirs)\n\n if (change.kind === 'package') {\n // Read late: the map may still be building (\u2192 `null` \u2192 restart all), which is correct, just unscoped.\n const targets = selectPackageRestartTargets(apps, this.closureMap, change.packageDir)\n\n if (targets === null) {\n // Fail-safe: no closure map / no package identity \u2192 restart every launched app. Note this\n // ignores per-app `watchDeps: false` opt-outs \u2014 opt-out is best-effort and yields to the\n // fail-safe superset, so a `turbo --dry` failure never silently drops a needed restart.\n this.scheduleDebounced('__packages__', () => {\n return this.restart(apps)\n })\n\n return\n }\n\n // Empty target set = the package is UI-only or every dependent opted out \u2192 no restart.\n // The `packageDir !== undefined` check narrows it to `string` for `packageDebounceKey`;\n // it is always defined here (a non-null `targets` implies a matched package identity).\n if (targets.length > 0 && change.packageDir !== undefined) {\n this.scheduleDebounced(packageDebounceKey(change.packageDir), () => {\n return this.restart(targets)\n })\n }\n\n return\n }\n\n const app = apps.find((a) => {\n return path.join(a.path, 'dist') === change.app\n })\n\n if (!app) return\n\n this.scheduleDebounced(app.name, () => {\n return this.restart([app])\n })\n }\n\n /**\n * Debounce a restart under `key`: cancel any pending timer for the same key and start\n * a fresh {@link DevServerRunner.WATCH_DEBOUNCE_MS} timer, so a burst of saves collapses\n * into one restart. Errors from the scheduled work are logged, never thrown.\n */\n private scheduleDebounced(key: string, work: () => Promise<void>): void {\n if (this.shuttingDown) return\n\n const existing = this.watchDebounceTimers.get(key)\n\n if (existing) clearTimeout(existing)\n\n const timer = setTimeout(() => {\n this.watchDebounceTimers.delete(key)\n if (this.shuttingDown) return\n work().catch((err) => {\n this.renderer.log(`Restart error (${key}): ${String(err)}`, 'error')\n })\n }, DevServerRunner.WATCH_DEBOUNCE_MS)\n\n this.watchDebounceTimers.set(key, timer)\n }\n\n /** Is the frontend health probe on? `--no-ui-health` / `INFRA_KIT_NO_UI_HEALTH=1` turn it off. */\n private uiHealthEnabled(): boolean {\n return (this.options.uiHealth ?? true) && process.env.INFRA_KIT_NO_UI_HEALTH !== '1'\n }\n\n /** Every running backend, as probe targets. The CURRENT `boundPort` is read fresh \u2014 a restart rebinds it. */\n private apiTargets(): ProbeTarget[] {\n return this.appServers.map(({ app, boundPort }) => {\n return { tag: `${app.name}/api`, port: boundPort, kind: 'api' as const }\n })\n }\n\n /**\n * Every MANAGED frontend, as probe targets \u2014 derived from {@link uiPortMap}, which is exactly the set of\n * UIs whose port this runner assigned. A UI that fell back to a reference line has no port we own, so\n * there is nothing to probe and its row stays `unknown` (no dot), which is the honest answer. Empty when\n * UI health is off, which is the whole implementation of `--no-ui-health`: no targets, no probes, no dots.\n */\n private uiTargets(): ProbeTarget[] {\n if (!this.uiHealthEnabled()) return []\n\n return Object.entries(this.uiPortMap).map(([pkg, { port }]) => {\n return { tag: this.uiTagByPackage.get(pkg) ?? `${pkg}/ui`, port, kind: 'ui' as const }\n })\n }\n\n /** This tag's entry, created on first sight. */\n private healthEntry(tag: string, kind: ProbeTarget['kind']): HealthEntry {\n const existing = this.health.get(tag)\n\n if (existing) return existing\n\n const fresh: HealthEntry = { kind, failures: 0, foreignStreak: 0, everUp: false, dead: false, unverified: false }\n\n this.health.set(tag, fresh)\n\n return fresh\n }\n\n /**\n * Fold one probe outcome into a row's history and fire the edge logs \u2014 each exactly once, so a steady\n * state (healthy OR wedged) says nothing.\n *\n * The `foreign` arms are where the two kinds part company. A backend answering non-2xx is OUR fastify\n * failing, so it counts as a failure like any other. A UI answering something that is not vite's ping is\n * unverifiable \u2014 a squatter, a proxy shadowing the ping, a future vite that dropped it \u2014 so it counts\n * toward nothing and only raises `unverified`, which renders `\u25CD ?` and never red.\n */\n private recordProbe(target: ProbeTarget, outcome: ProbeOutcome): void {\n const entry = this.healthEntry(target.tag, target.kind)\n const threshold = DevServerRunner.LIVENESS_FAILURE_THRESHOLD\n\n if (outcome === 'ok') {\n const wasDown = entry.failures >= threshold\n\n entry.everUp = true\n entry.failures = 0\n entry.foreignStreak = 0\n entry.unverified = false\n if (wasDown) this.renderer.log(`\u2705 ${target.tag} recovered`)\n\n return\n }\n\n if (isForeign(outcome) && target.kind === 'ui') {\n entry.foreignStreak += 1\n entry.unverified = entry.foreignStreak >= threshold\n if (entry.foreignStreak === threshold) {\n this.renderer.log(`\u26A0\uFE0F ${target.tag}: ${describeForeign(target.port, outcome)}`, 'warn')\n }\n\n return\n }\n\n entry.foreignStreak = 0\n entry.failures += 1\n // A port that answers SOMETHING is not provably dead \u2014 so a UI drops `unverified` only once its refusals\n // have earned a real `down`, and never on the way there.\n if (target.kind === 'ui' && entry.failures >= threshold) entry.unverified = false\n if (entry.failures === threshold) {\n const why = target.kind === 'api' ? '/__health not responding' : \"not answering vite's ping\"\n\n this.renderer.log(`\u26A0\uFE0F ${target.tag} unhealthy (${why})`, 'warn')\n }\n }\n\n /**\n * Declare a row down WITHOUT a probe \u2014 for the two facts the probe loop cannot establish: a restart whose\n * `startOneApp` threw (there is no server left to probe), and a UI whose engine died before it was ever\n * up. Routing those through the soft probe path would leave the row one failure short of the threshold,\n * i.e. GREEN, for something that demonstrably does not exist.\n */\n private markDown(tag: string, kind: ProbeTarget['kind']): void {\n const entry = this.healthEntry(tag, kind)\n\n entry.dead = true\n entry.failures = DevServerRunner.LIVENESS_FAILURE_THRESHOLD\n entry.foreignStreak = 0\n entry.unverified = false\n }\n\n /** The 5-arm row state for a tag. No entry at all \u2192 `unknown` (a `UiRef`, or `--no-ui-health`). */\n private healthOf(tag: string): HealthState {\n const entry = this.health.get(tag)\n\n if (entry == null) return 'unknown'\n if (entry.unverified) return 'unverified'\n if (!entry.everUp) return this.neverUpState(entry)\n\n return entry.failures >= DevServerRunner.LIVENESS_FAILURE_THRESHOLD ? 'down' : 'ok'\n }\n\n /**\n * A row that has never proved it was serving. A backend is `down` on sight \u2014 it is started in-process and\n * probed the moment it binds, so \"up but never answered\" is already a failure. A UI is `starting` until it\n * has burned the whole never-up budget: vite is spawned AFTER the ready frame and takes real seconds to\n * bind, and a red dot over a UI that is merely still booting is the one false red this design refuses.\n *\n * `dead` is read HERE and nowhere else, and the narrowness is the point: an `everUp` UI's death must stay\n * probe-established. Turbo puts each task in its OWN process group and reaps none of them on death\n * (`managed-child.ts`), and our own reap is best-effort \u2014 so \"the engine exited \u21D2 every vite is dead\" is\n * simply false, and believing it would red-dot a live, hot-reloading UI.\n */\n private neverUpState(entry: HealthEntry): HealthState {\n if (entry.kind === 'api' || entry.dead) return 'down'\n\n return entry.failures >= DevServerRunner.NEVER_UP_DOWN_THRESHOLD ? 'down' : 'starting'\n }\n\n /**\n * Start the always-on backend liveness monitor: a background probe loop that catches a backend which\n * goes unhealthy WITHOUT crashing the process (a blocked event loop, a wedged dependency, a fastify that\n * stopped serving) \u2014 invisible to both the one-shot {@link printReady} probe and the crash barrier, which\n * only sees thrown faults. Edge-triggered and flap-resistant; see {@link livenessTick}.\n */\n private startLivenessMonitor(): void {\n this.scheduleLivenessTick()\n }\n\n /**\n * Schedule the next liveness tick. A self-rescheduling `setTimeout` (never `setInterval`): a slow tick \u2014\n * every backend timing out its probe \u2014 can never pile up on the next one, and teardown clears exactly one\n * timer. `unref()` so the monitor never keeps the process alive on its own; the fastify servers do that,\n * and lifecycle is owned by {@link shutdown}.\n */\n private scheduleLivenessTick(): void {\n const intervalMs = this.options.livenessIntervalMs ?? DevServerRunner.LIVENESS_INTERVAL_MS\n\n this.livenessTimer = setTimeout(() => {\n // `finally` always reschedules; the trailing `catch` swallows a rejecting probe seam so one bad tick\n // is skipped rather than crashing the loop, and leaves no floating promise.\n this.livenessTick()\n .finally(() => {\n if (!this.shuttingDown) this.scheduleLivenessTick()\n })\n .catch(() => {})\n }, intervalMs)\n this.livenessTimer.unref()\n }\n\n /**\n * One liveness sweep over every running backend AND every managed frontend. Edge-triggered per ROW,\n * keyed by tag (so the counter survives an ephemeral-port rebind on restart) with the CURRENT port read\n * fresh each tick. The verdict rules live in {@link recordProbe}; the threshold is what makes a normal\n * watch-restart (server down <1s, a single interval) invisible \u2014 only a genuinely wedged app stays down\n * across two sweeps.\n *\n * `/__health` probes are already filtered out of the live request tail ({@link startOneApp}'s\n * `onRequestLog`), and vite's ping is not a line vite logs, so this never spams. Bails once teardown has\n * latched so a closing server is not misread as down (the timer is also cleared in {@link shutdown}\n * before the servers close).\n */\n /**\n * Repaint the status panel with fresh live fields (health, uptime, req/min, restarts, errors).\n *\n * This is the first caller `DevUi.refresh()` has ever had: it was declared, implemented twice, and\n * invoked from nowhere, so the \"live\" footer has always painted boot-time values that never changed.\n * That was survivable while a log tail scrolled beside it. It is not survivable now \u2014 with nothing\n * else on screen, a panel that never moves cannot be told apart from a hung process.\n */\n private refreshStatus(): void {\n const summary = this.lastSummary\n\n if (summary == null) return\n\n const now = Date.now()\n const byTag = new Map<string, IAppServer>(\n this.appServers.map((server) => {\n return [`${server.app.name}/api`, server]\n }),\n )\n\n // Prune FIRST, unconditionally, and never inside the argument to `refresh?.()`: an optional call\n // does not evaluate its argument at all when the method is absent, and `DevRenderer` \u2014 the renderer\n // on every `--json` / MCP / piped run \u2014 has no `refresh`. Pruning in there meant the window was never\n // trimmed off the TTY path, so `reqTimes` grew without bound while `onRequestLog` copied the whole\n // array on every request. Unbounded memory and O(n\u00B2) CPU, on the long-lived MCP path specifically.\n this.pruneRequestWindow(now)\n\n this.renderer.refresh?.({\n ...summary,\n sessionUptimeMs: now - this.readyAt,\n // Re-derived, never carried over from the boot summary: a `--watch` restart that brings the backend\n // back must take this row down with it (see {@link degradedRows}).\n degraded: this.degradedRows(),\n // A UI with no managed port has no endpoint row \u2014 only a reference line. It still gets its error\n // count, or its breakage would be counted into a file with nothing on screen pointing at it.\n uiRefs: summary.uiRefs.map((ref) => {\n return { ...ref, errors: this.announceFirstError(ref.tag) }\n }),\n endpoints: summary.endpoints.map((endpoint) => {\n const server = byTag.get(endpoint.tag)\n\n return {\n ...endpoint,\n // Every row's health comes from the ONE probe state machine now \u2014 a UI row's included. A row\n // nothing probes (a `UiRef`, or `--no-ui-health`) has no entry and resolves to `unknown`, which\n // renders no dot at all: exactly what it used to hardcode.\n health: this.healthOf(endpoint.tag),\n // A UI row has no backend server, so it has no uptime, no restarts and no request rate \u2014 but it\n // DOES have an error count, which is the whole reason the panel can report a broken frontend.\n uptimeMs: server ? now - server.startedAt : undefined,\n restarts: server?.restarts,\n rpm: (this.reqTimes.get(endpoint.tag) ?? []).length,\n errors: this.announceFirstError(endpoint.tag),\n }\n }),\n })\n }\n\n /**\n * This row's error count, announcing the 0 \u2192 >0 EDGE once with a terminal line.\n *\n * The counter alone is not enough, and the reason is the honest cost of a liveness dot: a frontend that\n * fails to compile still serves and still answers vite's ping, so its dot stays a truthful green while\n * the app is unusable. `\u26A0 N` is then the only thing on screen that disagrees \u2014 and a number quietly\n * ticking up in a panel corner is not a thing anyone notices. The edge log is; it also reaches a non-TTY\n * run, which has no panel at all.\n */\n private announceFirstError(tag: string): number {\n const { errors } = this.sink.statsFor(tag)\n\n if (errors > 0 && !this.firstErrorLogged.has(tag)) {\n this.firstErrorLogged.add(tag)\n this.renderer.log(`\u26A0\uFE0F ${tag} reported its first error \u2192 ${homeShorten(this.sink.pathFor(tag))}`, 'warn')\n }\n\n return errors\n }\n\n /**\n * The degraded routes that are STILL degraded \u2014 i.e. whose backend is not (yet) running.\n *\n * Re-derived from the live `appServers` set rather than cached, because under `--watch` this is the\n * one row on the panel that is supposed to disappear: the whole point of retrying a boot-failed app is\n * that the route it broke goes back to local. A row that outlived its cause would be a permanent\n * warning about a fixed condition, and a warning that is always on is a warning nobody reads.\n *\n * Membership in `appServers` is the right test: {@link startAllApps} pushes only apps that booted, and\n * {@link runRestart} pushes a recovered app in at the moment it does \u2014 which is also the moment its\n * dev-context fragment lands, i.e. exactly when the vite helper flips the route back to `local`.\n */\n private degradedRows(): DegradedRow[] {\n if (this.degradedRoutes.length === 0) return []\n\n // Keyed by PACKAGE, not by app folder: a route degraded because the run never launched its backend\n // has no owning app name at all (`apiApp` is undefined), and the package is the identity the route,\n // the fragment, and the vite helper's local set all agree on.\n const running = new Set(\n this.appServers.map(({ app }) => {\n return app.packageName\n }),\n )\n\n return this.degradedRoutes\n .filter((d) => {\n return !running.has(d.packageName)\n })\n .map((d) => {\n return { route: d.route, tag: `${d.uiApp}/ui`, fallback: d.fallback, target: d.cloudTarget }\n })\n }\n\n /** Drop request timestamps older than the 60s rpm window. Runs on every tick, painting or not. */\n private pruneRequestWindow(now: number): void {\n const cutoff = now - 60_000\n\n for (const [tag, times] of this.reqTimes) {\n this.reqTimes.set(\n tag,\n times.filter((at) => {\n return at > cutoff\n }),\n )\n }\n }\n\n private async livenessTick(): Promise<void> {\n if (this.shuttingDown) return\n\n await Promise.all(\n [...this.apiTargets(), ...this.uiTargets()].map((target) => {\n return this.probeOne(target)\n }),\n )\n\n // The panel's heartbeat. It rides the probe tick that already exists rather than adding a timer of\n // its own, so the numbers on screen are exactly as fresh as the health behind them.\n this.refreshStatus()\n }\n\n /** Probe one target and fold the outcome in. Split out of {@link livenessTick} to keep both simple. */\n private async probeOne(target: ProbeTarget): Promise<void> {\n const outcome = await this.healthProbe(target)\n\n // Re-check AFTER the await: shutdown() may have latched and begun closing servers while this probe was\n // in flight, resolving it `refused` against a closing socket. Without this a tick that passed the\n // top-of-method bail could log a false `unhealthy` during teardown.\n if (this.shuttingDown) return\n\n this.recordProbe(target, outcome)\n }\n\n /**\n * Collapse the boot spinner into the calm ready header: one endpoint row per running backend\n * (pre-probed health dot + a resolving URL), one reference line per UI app (vite prints its own\n * URL in the stream below), the watch line, the clickable log path, and a separator rule.\n * Handles a UI-only session (no backend rows) so it never leaves a blank screen. `ready()` itself\n * is synchronous \u2014 health is probed here and passed in resolved.\n */\n private async printReady(\n apps: IApiAppConfig[],\n uiApps: DiscoveredUiApp[],\n bootStart: number,\n target: string,\n ): Promise<void> {\n // Snapshot BE readiness BEFORE probing \u2014 a `\u25CF down` server's probe timeout must not inflate\n // `ready in Xs` (nor is the boot time itself the probe latency).\n const elapsedMs = Date.now() - bootStart\n\n // The boot probe goes through the SAME state machine as every tick \u2014 it is not a separate verdict that\n // the panel then forgets. It used to be: `printReady` probed, painted the dot from the local result,\n // and left the failure counter empty \u2014 so the `refreshStatus()` at the bottom of this method, reading\n // that empty map, painted a backend probed DOWN one line ago a confident green, and kept it green until\n // the first tick. The boot frame and the panel under it disagreed about a probe taken once.\n await Promise.all(\n this.apiTargets().map((target) => {\n return this.probeOne(target)\n }),\n )\n\n const endpoints: EndpointRow[] = this.appServers.map(({ app, alias }) => {\n return {\n tag: `${app.name}/api`,\n url: resolveEndpointUrl({ prefixUrl: app.prefixUrl, alias }),\n health: this.healthOf(`${app.name}/api`),\n }\n })\n\n // Pre-assign each UI a free port + its portless alias and stash the map for startUiDev's env. Owning\n // the port is what makes the UI's URL knowable before vite prints it, so every UI gets a real endpoint\n // row. Only a UI whose port could not be assigned falls back to a reference line (\"vite prints its URL\n // below\") \u2014 and, having no port we own, no health dot either.\n // Resolve every launched frontend's routes to where they actually land, so each UI row can carry its\n // own \"which proxy, and where\" list. Built from the SAME loaded routes the degraded check used plus the\n // live running set + backend origins here, so the listing matches the proxy vite really serves.\n const originByPkg = new Map(\n this.appServers.map(({ app, alias }) => {\n return [app.packageName, resolveEndpointUrl({ prefixUrl: app.prefixUrl, alias })] as const\n }),\n )\n\n this.proxyRoutes = resolveProxyRoutes({\n uis: this.launchedUis,\n running: new Set(\n this.appServers.map(({ app }) => {\n return app.packageName\n }),\n ),\n localOrigin: (pkg) => {\n return originByPkg.get(pkg)\n },\n env: process.env[INFRA_KIT_ENV_VAR],\n })\n\n this.uiPortMap = {}\n const uiEndpoints: EndpointRow[] = []\n const uiRefs: UiRef[] = []\n\n for (const ui of uiApps) {\n const assigned = await this.assignUiPort(ui)\n const tag = `${ui.name}/ui`\n\n if (assigned != null) {\n uiEndpoints.push({\n tag,\n url: resolveEndpointUrl({ prefixUrl: '', alias: assigned.alias }),\n health: this.seedUiHealth(tag),\n proxies: this.proxiesFor(ui.name),\n })\n } else {\n uiRefs.push({ tag, proxies: this.proxiesFor(ui.name) })\n }\n }\n const watch = this.options.watch ?? false\n const appCount = apps.length + uiApps.length\n const pkgCount = getPackageDistDirs(this.monorepoRoot).length\n\n this.lastSummary = {\n target,\n watch,\n release: readAppRelease(process.cwd()),\n elapsedMs,\n endpoints: [...endpoints, ...uiEndpoints],\n uiRefs,\n failed: this.failedApps.map(({ app, reason }) => {\n return { tag: `${app.name}/api`, reason }\n }),\n degraded: this.degradedRows(),\n watchSummary: `${appCount} app${appCount === 1 ? '' : 's'} \u00B7 ${pkgCount} package${pkgCount === 1 ? '' : 's'}`,\n // The DIRECTORY, not a file: there is one log per service now, so a single path would have to\n // pick a favourite. `tail -f <dir>/<service>.log` is the workflow.\n logPath: homeShorten(this.sink.dir),\n logHref: this.sink.dir,\n }\n\n // Paint the boot frame from the same summary the panel will keep repainting, so the header and the\n // live rows can never disagree about what is running.\n this.readyAt = Date.now()\n this.renderer.ready(this.lastSummary)\n this.refreshStatus()\n }\n\n /**\n * Pre-assign a UI a free port, alias it, and record the port in {@link uiPortMap} (handed to the vite\n * child via `INFRA_KIT_UI_PORTS`, which it binds with `strictPort`). Owning the port ahead of vite's own\n * announcement is what makes the URL knowable in time to print it.\n *\n * `null` degrades this UI to a reference line: either its vite config does not wire `infraKitDev()`\n * ({@link DiscoveredUiApp.managedPort}), so it would ignore the assignment and bind its own port \u2014\n * printing the assigned one would be a lie and aliasing it would 502 \u2014 or the free-port probe failed\n * (extremely rare). An alias that portless REFUSES is not degraded here: {@link registerAppAlias} throws,\n * because a UI advertised at a hostname nothing serves is worse than a UI with no row.\n */\n private async assignUiPort(ui: DiscoveredUiApp): Promise<{ port: number; alias: string } | null> {\n if (!ui.managedPort) return null\n\n let port: number\n\n try {\n port = await getFreePort()\n } catch {\n return null\n }\n const alias = await this.registerAppAlias(ui.packageName, ui.path, port)\n\n this.uiPortMap[ui.packageName] = { port, alias }\n // `uiPortMap` is keyed by PACKAGE because that is the key the vite child reads it back by\n // (`INFRA_KIT_UI_PORTS`) \u2014 the rows are keyed by app. Record the mapping rather than widening the\n // fragment: it is a published wire contract with a separately-versioned helper.\n this.uiTagByPackage.set(ui.packageName, `${ui.name}/ui`)\n\n return { port, alias }\n }\n\n /**\n * Seed a managed UI's row so it reads `\u25CC starting` from the boot frame onward.\n *\n * Vite is spawned AFTER `printReady` \u2014 the first probe is a whole tick away \u2014 so an unseeded row would\n * carry no dot at all until then, and a row that is `\u25CF down` at boot would be a lie about a server that\n * has not been asked a single question yet. `unknown` when UI health is off: no entry, no dot, ever.\n */\n private seedUiHealth(tag: string): HealthState {\n if (!this.uiHealthEnabled()) return 'unknown'\n\n this.healthEntry(tag, 'ui')\n\n return this.healthOf(tag)\n }\n\n /**\n * Dump each running app's registered `METHOD /path` routes (opt-in via `--routes`) so the\n * emulator is self-describing. Prints to the terminal (not verbose-gated) \u2014 invoking it means\n * the user explicitly asked for the routes. Reads the live set via `getRegisteredRoutes`.\n */\n private printRouteDump(): void {\n if (this.appServers.length === 0) return\n\n this.renderer.log('\uD83D\uDDFA\uFE0F Registered routes:')\n for (const { app, server } of this.appServers) {\n const routes = server.getRegisteredRoutes()\n\n this.renderer.log(` ${app.name} (${routes.length}): ${routes.length > 0 ? routes.join(', ') : '(none)'}`)\n }\n }\n\n /**\n * Stop watching, cancel any pending debounced restart, and close all running servers.\n * Does not exit the process \u2014 the entry point owns exit.\n */\n /**\n * Report a process-level fault \u2014 an `uncaughtException` / `unhandledRejection` the crash barrier caught\n * and deliberately survived.\n *\n * This exists because the interceptor owns `process.stderr` for the life of a TTY session. The crash\n * barrier's own reporter is a plain `process.stderr.write`, so after `ready()` a crash would be FILED\n * into a log and never printed \u2014 leaving a panel that still says `\u25CF ok` and `\u26A0 0` over a session that\n * has just faulted. Silence is the one thing a fault may never produce, and the panel is now the only\n * signal there is.\n *\n * So a fault takes both channels, deliberately: it is filed at `error` level (which turns the row's\n * counter red) AND punched onto the terminal through the panel's bypass, which steps over the very\n * patch that would otherwise swallow it. Attributed to the app whose async context faulted, when\n * there is one.\n */\n public reportFault(detail: string): void {\n // File it against the app whose async context faulted \u2014 that is what turns its row's counter red.\n this.sink.write(currentService() ?? RUNNER_SERVICE, detail, { level: 'error' })\n\n // Print it through the UI, NOT through the bypass. `rawStdoutWrite` would push N raw lines onto a\n // terminal whose live region Ink believes it owns: Ink erases by counting rows back from where it\n // thinks the cursor is, so the very next repaint (\u22645s away, on the liveness tick) would erase the\n // tail of this stack and leave a ghost of the old panel above it. The method exists to make a fault\n // impossible to miss; writing it somewhere the next frame deletes it is worse than not writing it.\n //\n // `renderer.log` commits through Ink's `<Static>` region, which survives every repaint \u2014 and falls\n // through to a plain stdout write on the non-TTY renderer, where there is no region to respect.\n //\n // `tee: false` because the line above ALREADY filed it. Left teeing, the renderer files a second copy\n // through `appendRunnerLog` \u2014 every fault landing twice in `runner.log`, a literal 2\u00D7 on the 185 GB the\n // storm wrote. The direct `sink.write` is the copy that must survive: it is the one that carries the\n // `error` level, and the level is what turns the panel row red.\n this.renderer.log(detail, 'error', { tee: false })\n }\n\n /**\n * File a fault into the log WITHOUT touching the terminal.\n *\n * The channel of last resort: when stdio is unwritable, printing is what produces the fault, so the sink\n * is the only surface a post-mortem can still read. Used by the entry's fatal path.\n */\n public fileFault(detail: string): void {\n this.sink.write(currentService() ?? RUNNER_SERVICE, detail, { level: 'error' })\n }\n\n /**\n * The teardown step currently in flight (`'idle'` before {@link shutdown}, `'done'` after it completes).\n *\n * Read by the entry's `describeStall` seam when the teardown deadline trips, so the force-quit line names\n * WHICH step wedged rather than shrugging. Without it the deadline is just a force-quit, and the question\n * the incident actually poses \u2014 why five processes that demonstrably ran `shutdown()` never exited \u2014 stays\n * open after shipping the thing meant to answer it.\n */\n public get shutdownStage(): string {\n return this.stage\n }\n\n /**\n * Stop watching, cancel any pending debounced restart, and close all running servers. Idempotent: every\n * caller after the first gets the SAME in-flight promise, never a second teardown.\n *\n * The `.catch` is attached HERE, at assignment, in the same tick \u2014 not by the caller. `doShutdown` can\n * reject (`watcher.close()`, `turboWatch.kill()`, `uiDev.kill()` are unguarded), and the fatal path calls\n * this fire-and-forget while a deadline races it. A rejection with no handler attached in the assigning\n * tick fires `unhandledRejection` \u2192 the crash barrier \u2192 and, since stdio is dead on that path, straight\n * back into the fatal handler: the exact loop this whole change exists to remove, re-created inside its\n * own fix. Callers that DO await still see the rejection \u2014 this handler only disarms the process-level\n * channel.\n */\n public shutdown(): Promise<void> {\n if (this.teardown != null) return this.teardown\n\n this.teardown = this.doShutdown()\n this.teardown.catch(() => {})\n\n return this.teardown\n }\n\n private async doShutdown(): Promise<void> {\n // Latch BEFORE anything else. Everything below assumes no new alias can be registered once\n // teardown begins; `scheduleDebounced` and `restart` read this flag to honour that.\n this.shuttingDown = true\n this.stage = 'starting'\n\n // Release the terminal FIRST: if the Ink boot UI is still mounted (e.g. SIGINT mid-boot), unmount it\n // before any plain write below, so the shutdown lines never clobber a live region. No-op for the\n // plain renderer and idempotent when Ink already unmounted at ready().\n this.renderer.dispose()\n\n // Hand `console` and the raw streams back HERE \u2014 before the first shutdown line, not after the last.\n // `dispose()` drops the panel, so `renderer.log` below falls through to a plain `process.stdout`\n // write; while the interceptor still owned it and was suppressing, that line went to a log file. The\n // user pressed Ctrl-C and then watched a dead terminal for the seconds teardown takes (killing the\n // turbo tree escalates SIGTERM\u2192SIGKILL per child) \u2014 which is exactly how a second Ctrl-C gets\n // pressed, taking the force-quit path and orphaning the children.\n this.intercept?.uninstall()\n\n this.renderer.log('\uD83D\uDED1 Shutting down all servers...')\n\n // Silence every restart SOURCE before deregistering aliases below. Ordered first because alias\n // removal is not idempotent against a concurrent `startOneApp`: a chokidar event or an armed\n // debounce timer firing after the removal would re-register an alias into a set nothing drains\n // again, stranding it exactly like the force-quit case the removal is there to prevent.\n for (const timer of this.watchDebounceTimers.values()) {\n clearTimeout(timer)\n }\n this.watchDebounceTimers.clear()\n\n // Stop the liveness monitor BEFORE the servers close, so a final tick can't probe a closing backend and\n // log a false `unhealthy`. The `shuttingDown` latch above also makes an in-flight tick bail and never\n // reschedule; clearing the pending timer here closes the window between ticks.\n if (this.livenessTimer) {\n clearTimeout(this.livenessTimer)\n this.livenessTimer = null\n }\n\n if (this.watcher) {\n this.stage = 'watcher.close'\n await this.watcher.close()\n this.watcher = null\n }\n\n // Drain a restart already in flight. The latch makes every QUEUED job a no-op, but a job that\n // began before the latch is mid `close() \u2192 listen() \u2192 registerAlias()` and must finish, or its\n // alias lands after the removal below. Never rejects (the chain self-catches).\n this.stage = 'restartWorkChain'\n await this.restartWorkChain\n\n // And the background closure build, so its `turbo --dry` child never outlives the runner.\n this.stage = 'closureBuild'\n await this.closureBuild\n\n // Layer B: deregister every portless alias BEFORE the child reap below. The reap can take\n // seconds (grace + SIGKILL escalation, per child), and a force-quit during it would otherwise\n // strand an alias pointing at a dead backend \u2014 the 502 you hit on the next start.\n //\n // Concurrent and best-effort: `removeAlias` never throws (the driver swallows failures) and each\n // call is self-bounded by the driver's own subprocess timeout, so this caps the pre-reap delay at\n // roughly one timeout regardless of alias count, and `Promise.all` cannot reject. That bound holds\n // only because `availability` is already warm here: an alias can exist only after `registerAlias`,\n // which awaits `isAvailable()`, and `ensureProxy` awaits it during `start()`. Register an alias\n // without a prior availability check and this silently becomes two subprocesses per alias.\n this.stage = 'removeAlias'\n await Promise.all(\n [...this.registeredAliases].map((name) => {\n return this.proxy.removeAlias(name)\n }),\n )\n this.registeredAliases.clear()\n\n // Reap the long-lived engines (group SIGTERM\u2192SIGKILL) so neither writes fresh dist nor\n // holds a port mid-teardown. Reaped here (not only in the entry signal handler) because tests\n // and any non-signal caller invoke shutdown() directly. Awaited so the SIGKILL escalation\n // completes before the entry point's `process.exit`.\n if (this.turboWatch) {\n this.stage = 'turboWatch.kill'\n await this.turboWatch.kill()\n this.turboWatch = null\n }\n\n if (this.uiDev) {\n this.stage = 'uiDev.kill'\n await this.uiDev.kill()\n this.uiDev = null\n }\n\n for (const { app, server } of this.appServers) {\n this.stage = `server.close(${app.name})`\n try {\n await server.close()\n } catch {\n // ignore\n }\n // Remove this runner's own dev-context fragment so a stopped app drops out of the\n // helper's localSet (only its own \u2014 the directory model keeps runners independent).\n this.removeDevContextFragment(app)\n }\n\n // Final terminal-visible confirmation, so Ctrl-C never ends on a bare cursor. `log` (not the\n // verbose-only `narrate`) so the last line the user sees is always infra-kit's.\n //\n // Our own detached `turbo run dev` child writes its teardown to the log, not the TTY. Any pnpm\n // `ELIFECYCLE` still visible after Ctrl-C therefore comes from the `pnpm run` wrapper processes\n // ABOVE us in the shell's foreground process group (nested consumer scripts), which we cannot\n // redirect: the terminal signals the whole group, pnpm dies at once, and the shell redraws its\n // prompt while this teardown is still running. Fixing that means `exec`ing into the binary from\n // the consumer's dev script so no wrapper survives to report a failed child.\n // Hand `console` and the raw streams back BEFORE the final line prints, so the goodbye actually\n // reaches the terminal instead of being filed into a log the user is no longer watching.\n this.intercept?.uninstall()\n\n this.renderer.log(`\u2713 dev stopped \u00B7 logs \u2192 ${homeShorten(this.sink.dir)}`)\n\n // Strictly last: every line above still has to reach a file. Closing holds no buffered data (the\n // sink writes through a held fd), so this only releases the fds.\n this.sink.close()\n this.stage = 'done'\n }\n}\n\n/**\n * Select the terminal UI for this run: the persistent Ink UI on an interactive TTY (dynamically imported\n * so React never loads on the non-TTY / `--json` / MCP chunks), else the plain {@link DevRenderer}\n * (returned as `undefined` so the runner constructs its own default). `--json`/MCP always forces plain \u2014\n * Ink must never seize a machine-readable stream.\n *\n * {@link PersistentInkDevUi} covers both shapes of session, branching at {@link DevUi.ready} on whether a\n * UI child owns the TTY, so there is nothing left to gate on here.\n */\n/**\n * Whether this run owns the terminal \u2014 the single gate for BOTH the live UI and the output interception.\n *\n * Derived once and shared, never re-derived: a `--json` / MCP / piped run must keep a byte-clean stdout,\n * and interception there would file the machine-readable stream into a log and hand the caller nothing.\n */\nexport const ownsTerminal = (options: DevServerOptions): boolean => {\n return (options.tty ?? Boolean(process.stdout.isTTY)) && !options.json\n}\n\nconst selectDevUi = async (options: DevServerOptions): Promise<DevUi | undefined> => {\n if (!ownsTerminal(options)) {\n return undefined\n }\n\n const { PersistentInkDevUi } = await import('src/tui/dev-ui/persistent-ink-dev-ui')\n const { createSafeStream } = await import('src/tui/safe-stderr')\n\n return new PersistentInkDevUi({\n appendLog: appendRunnerLog,\n verbose: options.verbose ?? false,\n // Composition order is load-bearing. The BYPASS proxy is inside (its `write` reaches the real\n // terminal, stepping over the interceptor's patch); the SCRUB proxy is outside (it strips the\n // `ESC[3J` that an overflowing Ink frame emits, which would wipe the user's scrollback).\n //\n // Inverting them breaks silently: `createSafeStream` resolves `target.write` at CALL time, so\n // wrapping the raw `process.stdout` would route every frame through the patch and into a log file \u2014\n // a blank screen with no error anywhere.\n stdout: createSafeStream(panelStream()),\n })\n}\n\n/**\n * Construct a {@link DevServerRunner}, start it, and return the instance so the caller\n * (the CLI entry point) can wire signal handlers to `shutdown()` and own process exit.\n * Selects the boot UI (Ink on a TTY, plain otherwise) before constructing the runner.\n */\nexport async function run(options: DevServerOptions = {}): Promise<DevServerRunner> {\n const renderer = await selectDevUi(options)\n // Positions: options, runBuild, turboWatchFactory, uiDevFactory, dryRunner, renderer \u2014 pass `undefined`\n // for the injectable seams so their ctor defaults apply; only the renderer is chosen here.\n const runner = new DevServerRunner(options, undefined, undefined, undefined, undefined, renderer)\n\n // Unwind a partial boot. By the time `start()` can reject (e.g. a UI alias portless refuses), the\n // backends are already listening, aliased in the portless daemon, and recorded as dev-context\n // fragments on disk. The caller wires `shutdown()` to signals only AFTER this resolves, so without\n // this the process exits leaving external daemon state and on-disk fragments behind \u2014 the next\n // `vite dev` then proxies at an alias nothing serves. Teardown failures must not mask the original\n // boot error, so they are swallowed.\n try {\n await runner.start()\n } catch (error) {\n await runner.shutdown().catch(() => {})\n throw error\n }\n\n return runner\n}\n", "/**\n * Dependency-closure map for scoped `infra-kit dev --watch` restarts (plan Phase 1, Option B).\n *\n * `pnpm` is resolved from PATH \u2014 the same trust posture as the rest of the dev-server, which\n * already shells out to `pnpm exec turbo \u2026`; the args are fixed literals plus discovered package\n * names, never shell-interpolated.\n *\n * Rather than re-derive the workspace graph in-process, infra-kit asks turbo what it would\n * rebuild: `turbo run build --dry=json --filter=...<pkg>` lists every package in an app's build\n * closure (`tasks[].package`). That set is authoritative \u2014 it is exactly what the running\n * `turbo watch build` engine rebuilds \u2014 so there is no drift and no glob-parser/BFS/cycle-guard\n * to maintain here. The runner inverts these per-app closures into `dependentsByPackageDir`\n * (which app folders depend on each `packages/<x>/dist`) and, on a package-dist change, restarts\n * only the dependent backends instead of every one.\n */\nimport { execFile } from 'node:child_process'\nimport * as fs from 'node:fs'\nimport * as path from 'node:path'\nimport { promisify } from 'node:util'\n\nimport { getPackageDistDirs } from './discovery.js'\n\nconst execFileAsync = promisify(execFile)\n\n/** The subset of an app's identity the closure map needs. */\nexport interface ClosureApp {\n /** App folder name (e.g. `client`) \u2014 the restart key in the dependents map. */\n name: string\n /** package.json `name` (e.g. `sls-trvl-client`) \u2014 the turbo `--filter` target. */\n packageName: string\n}\n\n/**\n * Resolve one app's build closure: the set of workspace package **names** turbo would rebuild\n * for it. Injectable so tests drive `buildClosureMap` without spawning a real turbo. A rejection\n * propagates through `Promise.all` so the caller can apply its restart-all fail-safe.\n */\nexport type DryRunner = (packageName: string) => Promise<string[]>\n\n/** The inverted closure: `packages/<x>/dist` \u2192 the app folders that depend on it, plus the dir\u2192name bridge. */\nexport interface ClosureMap {\n /** `packages/<x>/dist` absolute dir \u2192 set of app folder names whose closure includes it. */\n dependentsByPackageDir: Map<string, Set<string>>\n /** `packages/<x>/dist` absolute dir \u2192 that package's package.json `name` (dir \u2260 name for scoped pkgs). */\n packageNameByDir: Map<string, string>\n}\n\n/** Read `<dir>/package.json` `name`, or `undefined` when absent/unreadable (never throws). */\nconst readPackageName = (dir: string): string | undefined => {\n try {\n const parsed = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8')) as { name?: unknown }\n\n return typeof parsed.name === 'string' ? parsed.name : undefined\n } catch {\n return undefined\n }\n}\n\n/**\n * Default {@link DryRunner}: `turbo run build --dry=json --filter=...<pkg>` in `root`, returning the\n * unique `tasks[].package` set (the packages turbo would rebuild for `<pkg>`). `...` includes the\n * app's own dependency closure, mirroring the `turbo watch build` filter.\n */\nexport const defaultDryRunner = (root: string): DryRunner => {\n return async (packageName: string): Promise<string[]> => {\n const { stdout } = await execFileAsync(\n 'pnpm',\n ['exec', 'turbo', 'run', 'build', '--dry=json', `--filter=...${packageName}`],\n { cwd: root, maxBuffer: 32 * 1024 * 1024 },\n )\n const parsed = JSON.parse(stdout) as { tasks?: Array<{ package?: unknown }> }\n const names = (parsed.tasks ?? [])\n .map((t) => {\n return t.package\n })\n .filter((p): p is string => {\n return typeof p === 'string'\n })\n\n return [...new Set(names)]\n }\n}\n\n/** Build `packageNameByDir` (and its reverse) for every existing `packages/<x>/dist` under `root`. */\nconst readPackageDirs = (root: string): { packageNameByDir: Map<string, string>; dirByName: Map<string, string> } => {\n const packageNameByDir = new Map<string, string>()\n const dirByName = new Map<string, string>()\n\n for (const distDir of getPackageDistDirs(root)) {\n const name = readPackageName(path.dirname(distDir))\n\n if (name !== undefined) {\n packageNameByDir.set(distDir, name)\n dirByName.set(name, distDir)\n }\n }\n\n return { packageNameByDir, dirByName }\n}\n\n/**\n * Ask turbo for each app's build closure (in parallel) and invert it into {@link ClosureMap}:\n * `packages/<x>/dist` \u2192 the app folders whose closure includes that package. Apps' own packages\n * (not under `packages/`) simply don't appear in the map, so they never scope a package restart.\n *\n * A {@link DryRunner} rejection propagates (via `Promise.all`) so the caller falls back to\n * restart-all \u2014 the map is never silently partial.\n *\n * @example\n * // appX deps pkgA+pkgC; appY deps pkgC only \u2192\n * // dependentsByPackageDir: { '\u2026/pkgA/dist': {appX}, '\u2026/pkgC/dist': {appX, appY} }\n */\nexport const buildClosureMap = async (\n root: string,\n apps: ClosureApp[],\n dryRunner: DryRunner = defaultDryRunner(root),\n): Promise<ClosureMap> => {\n const { packageNameByDir, dirByName } = readPackageDirs(root)\n const dependentsByPackageDir = new Map<string, Set<string>>()\n\n const closures = await Promise.all(\n apps.map(async (app) => {\n return { app, closure: await dryRunner(app.packageName) }\n }),\n )\n\n for (const { app, closure } of closures) {\n for (const pkgName of closure) {\n const distDir = dirByName.get(pkgName)\n\n if (distDir === undefined) continue\n\n const dependents = dependentsByPackageDir.get(distDir) ?? new Set<string>()\n\n dependents.add(app.name)\n dependentsByPackageDir.set(distDir, dependents)\n }\n }\n\n return { dependentsByPackageDir, packageNameByDir }\n}\n\n/** Debounce key for a package-dist change \u2014 distinct per package dir so unrelated packages never collapse into one bucket. */\nexport const packageDebounceKey = (packageDir: string): string => {\n return `__pkg__:${packageDir}`\n}\n\n/**\n * Decide which launched apps a `packages/<pkg>/dist` change should restart. `null` means\n * \"fail-safe: restart all\" \u2014 the closure map is missing (a `--dry` failure) or the change carried\n * no package identity. Otherwise the participating (`watchDeps`) apps whose closure includes that\n * package dir \u2014 possibly an empty array (the package is UI-only, or every dependent opted out),\n * which the caller treats as \"restart nothing\".\n *\n * @example\n * // closure: pkgA \u2192 {appX}; pkgC \u2192 {appX, appY}\n * selectPackageRestartTargets([appX, appY], map, '\u2026/pkgA/dist') // => [appX] (not appY)\n * selectPackageRestartTargets([appX, appY], map, '\u2026/uiOnly/dist') // => [] (restart nothing)\n * selectPackageRestartTargets([appX, appY], null, '\u2026/pkgA/dist') // => null (fail-safe: restart all)\n */\nexport const selectPackageRestartTargets = <T extends { name: string; watchDeps: boolean }>(\n apps: T[],\n closureMap: ClosureMap | null,\n packageDir: string | undefined,\n): T[] | null => {\n if (closureMap === null || packageDir === undefined) return null\n\n const dependents = closureMap.dependentsByPackageDir.get(packageDir) ?? new Set<string>()\n\n return apps.filter((a) => {\n return a.watchDeps && dependents.has(a.name)\n })\n}\n", "/**\n * Detect a BROKEN LOCAL PAIRING: a frontend route this run meant to serve from a local backend, whose\n * backend is not actually up.\n *\n * A backend that is not running writes no `.infra-kit/dev-context/<app>.json` fragment, so the vite\n * helper's `pickSource` finds nothing in its local set and falls through to the route's FALLBACK. For the\n * common route (`from: ['local','cloud'], default: 'cloud'`) that fallback is the shared CLOUD backend \u2014\n * and the frontend comes up looking perfectly healthy while every request on that route leaves your\n * machine. Nothing in the resolved proxy map records that the route was ever meant to be local. You can be\n * POSTing at the cloud dev database believing you are pointed at localhost.\n *\n * The rule is INTENT vs REALITY, deliberately not \"did a backend crash\":\n * degraded \u27FA the route can be local \u2227 this run intended that package to be local \u2227 it is not up\n *\n * Keying off crashes alone (the obvious formulation) misses two reachable paths that produce the exact\n * same silent cloud proxy:\n * - `--app` / `--self` narrows the launch set AFTER preset resolution, so a preset's backend can be\n * dropped and never even attempted \u2014 no crash, nothing to key off;\n * - a preset can pin a route `local` while launching no backend for it at all. (`validatePresetProxy`\n * catches THAT statically, but it runs only under `infra-kit audit` and the bare-invocation wizard \u2014\n * never on the `infra-kit dev <preset>` path, which is the one people actually type.)\n * \"Intended local, isn't up\" covers all three with one predicate, crash included.\n *\n * Side-effect free: every input is passed in (nothing is read from disk here), so the rule is fully\n * unit-testable.\n */\n\n/** The two places a frontend route can be served from \u2014 mirrors `InfraKitDevProxySource`. */\nexport type PairingSource = 'local' | 'cloud'\n\n/**\n * One route from a frontend's `dev.proxy.routes`, as the pairing check reads it. Deliberately shaped like\n * the real config route so the caller hands its loaded config straight in with no translation.\n */\nexport interface PairingRoute {\n /** The backend package this route proxies to. */\n packageName: string\n /** The sources this route can be served from. Only a route that lists `local` can be degraded. */\n from: readonly PairingSource[]\n /**\n * The declared fallback. MUST be carried, and the reason is subtle: the helper resolves an unserved\n * route to `route.default ?? route.from[0]` (see `pickSource`). So a `from: ['local']` route with no\n * `default` \u2014 which the schema allows, since `default` is only required for a multi-source route \u2014\n * falls back to **`local`**, at an alias nothing registered. That is a loud 502, NOT a silent cloud\n * proxy, and a message that says \"would proxy to cloud\" would be naming a destination the traffic\n * never reaches. Still degraded, still worth refusing; just a different failure to describe honestly.\n */\n default?: PairingSource\n /** The preset pinned this route `local` via a `devServersPresets` proxy override \u2014 an explicit intent. */\n pinnedLocal?: boolean\n}\n\n/** A frontend this run launched, plus the `dev.proxy` block it declared. */\nexport interface LaunchedUi {\n /** App folder name (e.g. `client`). */\n app: string\n /** Route path \u2192 its declared backend, capable sources, and fallback. */\n routes: Record<string, PairingRoute>\n /**\n * The frontend's `dev.proxy.templates.cloud`, so a cloud-falling route can name the origin it is about\n * to use. Omitted \u2192 the finding carries no `cloudTarget`.\n */\n cloudTemplate?: string\n}\n\n/** Everything the rule needs to decide intent vs reality. */\nexport interface PairingInputs {\n /** The frontends this run actually launched. */\n uis: readonly LaunchedUi[]\n /**\n * Backend packages this run INTENDED to serve locally: every `<app>/api` the preset names, taken BEFORE\n * `--app`/`--self` narrowing \u2014 narrowing changes what runs, never what the preset promised.\n */\n wanted: ReadonlySet<string>\n /** Backend packages that are actually up (i.e. have written a dev-context fragment). */\n running: ReadonlySet<string>\n /** package \u2192 its start-failure reason. Absent for a package that was never attempted at all. */\n reasons: ReadonlyMap<string, { app: string; reason: string }>\n /** `INFRA_KIT_ENV`, for the `<env>` placeholder in a cloud template. */\n env?: string\n}\n\n/** A route this run meant to serve locally, whose backend is not up. */\nexport interface DegradedRoute {\n /** Frontend app folder the route belongs to. */\n uiApp: string\n /** Route path (e.g. `/api`). */\n route: string\n /** The backend package the route wanted. */\n packageName: string\n /** Where the route ACTUALLY resolves now \u2014 what the helper's `pickSource` will return. */\n fallback: PairingSource\n /** App folder of the backend, when this run attempted it (absent when it was never launched). */\n apiApp?: string\n /** Why it is not up: the start-failure reason, or a statement that the run never launched it. */\n reason: string\n /** The cloud origin the route now resolves to. Set ONLY when `fallback` is `cloud` AND it is knowable. */\n cloudTarget?: string\n}\n\n/** Reason text for a package the run never even attempted to start. */\nconst NOT_LAUNCHED = 'this run never launched it'\n\n/**\n * Fill the `<env>`/`<packageName>` placeholders in a cloud template (`<release>` is local-only), or\n * `undefined` when the template needs an `<env>` and none is sourced.\n *\n * Refusing to interpolate an empty `<env>` matters: `https://<env>.hulyo.co.il` with nothing to put in it\n * renders `https://.hulyo.co.il`, a host that resolves nowhere. Naming a made-up origin in a message whose\n * entire job is to tell the user WHERE their traffic was about to go is worse than naming none \u2014 the\n * caller then says \"the cloud backend\" and stays true.\n */\nconst interpolateCloud = (template: string, packageName: string, env: string | undefined): string | undefined => {\n if (!env && template.includes('<env>')) return undefined\n\n return template.replaceAll('<packageName>', packageName).replaceAll('<env>', env ?? '')\n}\n\n/** Where an unserved route actually lands, per the helper's `pickSource`: `default`, else the sole source. */\nconst resolveFallback = (route: PairingRoute): PairingSource => {\n return route.default ?? route.from[0] ?? 'cloud'\n}\n\n/** The cloud origin a route lands on, or undefined when there is no template (or no `<env>` for one). */\nconst cloudTargetOf = (\n cloudTemplate: string | undefined,\n packageName: string,\n env: string | undefined,\n): string | undefined => {\n return cloudTemplate == null ? undefined : interpolateCloud(cloudTemplate, packageName, env)\n}\n\n/**\n * Every route across the launched frontends that this run meant to serve locally and cannot.\n *\n * Three non-cases matter and are load-bearing:\n * - a CLOUD-ONLY route (`from: ['cloud']`) is never degraded \u2014 it was always going to cloud, by design\n * (hulyo's `/dynamic` and `/media` are exactly this), and flagging it would cry wolf on every run;\n * - a route whose backend is UP is not degraded, obviously \u2014 that is the happy path;\n * - a route naming a package this run never intended to serve locally (not in `wanted`, not pinned) is\n * not degraded: a frontend developing against cloud on purpose is a supported, common workflow.\n *\n * @example\n * findDegradedRoutes({\n * uis: [{ app: 'client', cloudTemplate: 'https://<env>.hulyo.co.il',\n * routes: { '/api': { packageName: 'backend-api', from: ['local', 'cloud'], default: 'cloud' } } }],\n * wanted: new Set(['backend-api']),\n * running: new Set(),\n * reasons: new Map([['backend-api', { app: 'client', reason: \"config is missing field: 'connectionURL'\" }]]),\n * env: 'dev',\n * })\n * // => [{ uiApp: 'client', route: '/api', packageName: 'backend-api', fallback: 'cloud', apiApp: 'client',\n * // reason: \"config is missing field: 'connectionURL'\", cloudTarget: 'https://dev.hulyo.co.il' }]\n */\nexport const findDegradedRoutes = (input: PairingInputs): DegradedRoute[] => {\n return input.uis.flatMap((ui) => {\n return Object.entries(ui.routes).flatMap(([route, spec]) => {\n const finding = judgeRoute(ui, route, spec, input)\n\n return finding ? [finding] : []\n })\n })\n}\n\n/** The whole rule, for one route: intended local \u2227 not up \u21D2 degraded. Null when the route is fine. */\nconst judgeRoute = (\n ui: LaunchedUi,\n route: string,\n spec: PairingRoute,\n { wanted, running, reasons, env }: PairingInputs,\n): DegradedRoute | null => {\n const { packageName } = spec\n const intendedLocal = wanted.has(packageName) || spec.pinnedLocal === true\n\n if (!spec.from.includes('local') || !intendedLocal || running.has(packageName)) return null\n\n const fallback = resolveFallback(spec)\n const failure = reasons.get(packageName)\n const cloudTarget =\n fallback === 'cloud' && ui.cloudTemplate != null ? interpolateCloud(ui.cloudTemplate, packageName, env) : undefined\n\n return {\n uiApp: ui.app,\n route,\n packageName,\n fallback,\n apiApp: failure?.app,\n reason: failure?.reason ?? NOT_LAUNCHED,\n cloudTarget,\n }\n}\n\n/** One route's resolved proxy destination, for the per-app listing the ready header paints under each UI. */\nexport interface ResolvedProxyRoute {\n /** Frontend app folder the route belongs to (e.g. `client`). */\n uiApp: string\n /** Route path (e.g. `/api`). */\n route: string\n /** The backend package this route proxies to. */\n packageName: string\n /** Where the route actually resolves \u2014 what the vite helper's `pickSource` will return for this run. */\n source: PairingSource\n /**\n * The origin the route resolves to: the running backend's local origin (`local`) or the interpolated\n * cloud origin (`cloud`). Omitted when it is not knowable \u2014 a `local` route whose backend is not up (a\n * dead alias, already surfaced as a degraded row) or a `cloud` route with no `<env>` sourced.\n */\n target?: string\n}\n\n/** Everything {@link resolveProxyRoutes} needs; every input is passed in, so the mapping stays testable. */\nexport interface ProxyResolutionInputs {\n /** The frontends this run launched, with their declared `dev.proxy` routes. */\n uis: readonly LaunchedUi[]\n /**\n * Backend packages up FROM THIS RUN. Mirrors \u2014 but is not identical to \u2014 the vite helper's on-disk\n * `localSet`: this is only what this process launched, so a backend started by another terminal or\n * worktree is invisible here (the same approximation {@link findDegradedRoutes} already makes). Kept in\n * lockstep with the degraded check on purpose, so the two never disagree with each other.\n */\n running: ReadonlySet<string>\n /** A running backend package \u2192 its local origin URL, for a `local` route's `target`. */\n localOrigin: (packageName: string) => string | undefined\n /** `INFRA_KIT_ENV`, for the `<env>` placeholder in a cloud template. */\n env?: string\n}\n\n/**\n * Resolve EVERY route across the launched frontends to where it actually lands this run \u2014 the data the\n * ready header paints as a nested list under each UI app.\n *\n * Deliberately mirrors the vite helper's `pickSource` (a route is `local` iff it lists `local` AND its\n * package is in the running set, else it falls back to `route.default ?? route.from[0]`), so the listing\n * can never disagree with the proxy the frontend actually serves. Unlike {@link findDegradedRoutes} this\n * reports the happy path too: a healthy `local` route and an intentional `cloud` route both get a row.\n *\n * Routes are emitted sorted by path within each UI, so the listing is stable regardless of config order.\n */\nexport const resolveProxyRoutes = (input: ProxyResolutionInputs): ResolvedProxyRoute[] => {\n const { uis, running, localOrigin, env } = input\n\n return uis.flatMap((ui) => {\n return Object.entries(ui.routes)\n .sort(([a], [b]) => {\n return a.localeCompare(b)\n })\n .map(([route, spec]): ResolvedProxyRoute => {\n const source: PairingSource =\n spec.from.includes('local') && running.has(spec.packageName) ? 'local' : resolveFallback(spec)\n const target =\n source === 'local' ? localOrigin(spec.packageName) : cloudTargetOf(ui.cloudTemplate, spec.packageName, env)\n\n return { uiApp: ui.app, route, packageName: spec.packageName, source, target }\n })\n })\n}\n\n/** Where this route's traffic is really about to go \u2014 the half of the message that must never lie. */\nconst describeDestination = (d: DegradedRoute): string => {\n if (d.fallback === 'local') {\n // `pickSource` returns `local` for an unserved single-source route, so the proxy dials the alias the\n // backend would have registered \u2014 and nothing did. Every request 502s. Loud, but still not what was\n // asked for, and saying \"cloud\" here would name a destination the traffic never reaches.\n return `would proxy ${d.route} at the local alias for \"${d.packageName}\", which nothing is serving \u2014 every request will fail`\n }\n\n const to = d.cloudTarget ?? 'the cloud backend'\n\n return `would proxy ${d.route} to ${to} instead of your local backend`\n}\n\n/** Was this backend actually attempted (and it crashed), or did the run never launch it at all? */\nconst wasAttempted = (d: DegradedRoute): boolean => {\n return d.apiApp != null\n}\n\n/**\n * What to actually DO about one finding. The two cases have genuinely different remedies, and conflating\n * them produces advice that cannot work:\n *\n * A CRASHED backend is retryable \u2014 `resolveRestartTargets` treats a boot-failed app as a restart target,\n * so `--watch` really can bring it back on the next save (and the vite plugin re-resolves the proxy off\n * the fragment it then writes, flipping the route back to `local` on its own).\n *\n * A backend the run NEVER LAUNCHED is not retryable by any amount of saving: `runRestart` only ever sees\n * the post-`--app`/`--self` app list, so a backend a narrowing flag dropped \u2014 or one a preset pinned\n * `local` without ever naming an api target for \u2014 is not in it and never will be. Telling that user to\n * \"run with --watch\" would be sending them to wait on a restart that cannot happen.\n */\nconst remedy = (d: DegradedRoute): string => {\n if (wasAttempted(d)) return `fix ${d.apiApp}/api and re-run (or use --watch to retry it on the next save)`\n\n return (\n `this run never launched \"${d.packageName}\" \u2014 drop the --app/--self narrowing, add its api to the ` +\n `preset, or set \"${d.route}\" to \"cloud\" if you meant to develop against cloud`\n )\n}\n\n/**\n * The refusal message for a run that cannot honour its local pairings. Spelled out rather than summarised:\n * the whole failure mode is that the fallback is invisible, so the refusal names the route, where the\n * traffic would really have gone, why the backend isn't there, and what will actually fix it.\n *\n * `target` is the run's preset/target label, purely so the message can blame the thing the user typed.\n */\nexport const formatPairingRefusal = (degraded: readonly DegradedRoute[], target: string): string => {\n const lines = degraded.map((d) => {\n const owner = wasAttempted(d) ? `${d.apiApp}/api` : `\"${d.packageName}\"`\n\n return (\n ` ${d.uiApp}/ui ${d.route} \u2192 ${d.packageName}: ${d.uiApp}/ui ${describeDestination(d)}.\\n` +\n ` ${owner}: ${d.reason}\\n` +\n ` \u2192 ${remedy(d)}`\n )\n })\n\n return (\n `infra-kit dev: \"${target}\" is supposed to serve a backend locally, but it is not running \u2014 refusing ` +\n `to bring the frontend up silently pointed somewhere else.\\n\\n${lines.join('\\n\\n')}`\n )\n}\n", "/**\n * Which service is \"speaking\" right now, for output that arrives through a channel carrying no tag of\n * its own \u2014 a handler's `console.log`, a Powertools line, a dependency's import-time banner.\n *\n * The backend is IN-PROCESS and multi-app: `DevServerRunner` holds an `appServers` array, so several\n * apps share one node process and one global `console`. Nothing in a raw stdout write says which app\n * wrote it. An `AsyncLocalStorage` carries the tag down the async call chain from the fastify hook that\n * opened the request, so a line emitted anywhere under that request attributes correctly.\n *\n * This is INFERENCE, not declaration \u2014 the honest limit is worth stating: a module imported by two apps\n * is ONE module instance, so a timer it registers at import time keeps whichever app imported it first.\n * Anything with no context at all falls back to a NAMED bucket (`runner.log`), never to a guess.\n */\nimport { AsyncLocalStorage } from 'node:async_hooks'\n\nconst storage = new AsyncLocalStorage<string>()\n\n/** Run `fn` with every line it emits attributed to `service`, however deep the async chain goes. */\nexport const runAttributed = <T>(service: string, fn: () => T): T => {\n return storage.run(service, fn)\n}\n\n/**\n * Enter `service`'s context for the REST of the current async chain, without wrapping a callback.\n *\n * This is the fastify shape: an `onRequest` hook must not wrap the handler (fastify owns that call),\n * but `enterWith` makes the whole remaining hook chain \u2014 handler, `onResponse`, `setErrorHandler` \u2014 a\n * continuation of this context.\n */\nexport const enterAttribution = (service: string): void => {\n storage.enterWith(service)\n}\n\n/** The service owning the current async context, or `undefined` outside any request / import. */\nexport const currentService = (): string | undefined => {\n return storage.getStore()\n}\n", "/**\n * Per-service log files for `infra-kit dev`.\n *\n * Replaces the single shared `<cacheRoot>/<session>/logs.txt`, which was written through TWO handles \u2014\n * a buffered `fs.createWriteStream` (the turbo tee) and a sync `fs.appendFileSync` (everything else) \u2014\n * so line order was never deterministic. Worse, `--cmux` spawns N `infra-kit dev` processes that all\n * INHERIT the same `INFRA_KIT_SESSION`, so N processes appended to one file through 2N handles. The\n * `<pid>` path segment makes that collision structurally impossible; one fd per service kills the rest.\n *\n * Writes are `fs.writeSync` on a long-lived fd, NOT a `WriteStream`. That is deliberate: a stream's\n * pending buffer is lost on `process.exit()` (an exit handler can only run sync code), which would drop\n * the tail of the log \u2014 precisely the lines anyone reads a crash log for. A held fd + `writeSync` is\n * durable by construction, so there is no flush to forget and no second handle to race.\n */\nimport { Buffer } from 'node:buffer'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\n\nimport { INFRA_KIT_SESSION_VAR, getCacheRoot } from 'src/lib/constants'\n\nimport type { LogLevel } from './render.js'\nimport { isTerminalDead } from './terminal-liveness.js'\n\n/**\n * The un-patched `process.stdout.write`, reached through the PROTOTYPE rather than captured off the\n * instance at module load.\n *\n * A module-load capture is only order-immune if nothing patched stdout before this file was evaluated \u2014\n * an assumption no import graph can guarantee. A monkeypatch installs an OWN property on the stream\n * instance, so resolving `write` from the prototype chain steps over it regardless of import order.\n * This is the panel's bypass: it must reach the terminal even while the interceptor owns `process.stdout`.\n *\n * Resolved at CALL time (like {@link rawStderrWrite}'s), not captured into a const: identical semantics \u2014\n * the prototype is what it is \u2014 and it keeps the real terminal write observable to a test, which is the\n * only way to prove the liveness gate below actually drops a chunk instead of merely claiming to.\n */\nconst protoWrite = (): NodeJS.WriteStream['write'] => {\n return (Object.getPrototypeOf(process.stdout) as { write: NodeJS.WriteStream['write'] }).write\n}\n\n/**\n * Write straight to the real stdout, bypassing any patch installed on the stream instance.\n *\n * Gated on {@link isTerminalDead}: once a stdio stream has emitted `'error'`, every further write to it is\n * DROPPED. This is the choke point every terminal-bound line already funnels through \u2014 Ink via\n * `createSafeStream(panelStream())`, the embedded `DevRenderer`, and the entry's own shutdown line \u2014 so\n * gating it here is what stops `reportFault` from feeding the write that produced the fault it is reporting.\n *\n * It returns `true` (a lie the caller can act on) and never `false`: the return value propagates through\n * `panelStream`'s Proxy into Ink, and `false` reads as BACKPRESSURE \u2014 Ink would stall or buffer, hanging the\n * very teardown this exists to reach. It never throws, for the same reason.\n *\n * Gate-completeness here is hygiene, not correctness: a handful of writes stay ungated\n * (`output-intercept.ts`'s non-string passthrough, the direct `process.stderr.write` in `crash-barrier` and\n * `signal-shutdown`). That is survivable \u2014 with `terminal-liveness` owning the `'error'` event, an ungated\n * write costs one extra tick, not a loop. The correctness rests entirely on owning that event.\n */\nexport const rawStdoutWrite = (chunk: string): boolean => {\n if (isTerminalDead()) return true\n\n return protoWrite().call(process.stdout, chunk)\n}\n\n/**\n * The same bypass \u2014 and the same liveness gate \u2014 for stderr. Separate from {@link rawStdoutWrite} because\n * they are different fds: echoing stderr through stdout would send it to the terminal under\n * `dev 2>boot.log` and leave the redirect empty \u2014 the caller asked for stderr on a file, and we would have\n * quietly moved it.\n */\nexport const rawStderrWrite = (chunk: string): boolean => {\n if (isTerminalDead()) return true\n\n return (Object.getPrototypeOf(process.stderr) as { write: NodeJS.WriteStream['write'] }).write.call(\n process.stderr,\n chunk,\n )\n}\n\n/** True when something has already monkeypatched `process.stdout.write` as an own property. */\nexport const hasForeignStdoutPatch = (): boolean => {\n return Object.getOwnPropertyDescriptor(process.stdout, 'write') != null\n}\n\n/**\n * A `process.stdout` stand-in whose `write` goes straight to the real terminal, stepping over any patch\n * the output interceptor has installed. THIS is what the status panel must paint through.\n *\n * Hand the panel the raw `process.stdout` instead and its frames are quietly filed into a log file\n * rather than drawn \u2014 the screen simply stays blank, with no error and no failing test. That black hole,\n * not a stack overflow, is the failure mode this exists to prevent.\n *\n * Every other property (`columns`, `rows`, `isTTY`, `on('resize')`) is forwarded LIVE to the real\n * stream, so a resize still reaches the renderer.\n */\nexport const panelStream = (): NodeJS.WriteStream => {\n return new Proxy(process.stdout, {\n get(target, property, receiver) {\n if (property === 'write') {\n return (chunk: unknown): boolean => {\n return rawStdoutWrite(String(chunk))\n }\n }\n\n const value = Reflect.get(target, property, receiver) as unknown\n\n return typeof value === 'function' ? (value as (...args: unknown[]) => unknown).bind(target) : value\n },\n })\n}\n\n/** Running totals for one service row on the status panel. Bumped from DECLARED level, never from content. */\nexport interface ServiceStats {\n errors: number\n warns: number\n /** Epoch ms of the most recent error, or `null` when the service has never errored. */\n lastErrorAt: number | null\n}\n\nconst emptyStats = (): ServiceStats => {\n return { errors: 0, warns: 0, lastErrorAt: null }\n}\n\n/**\n * Collapse an untrusted string into ONE safe path segment, or `null` when nothing survives.\n *\n * Split-and-rejoin rather than replace-then-trim: it collapses runs, drops leading and trailing\n * separators by construction, and has no backtracking quantifier to be super-linear about. Dots are NOT\n * in the allow-set, so the result is structurally incapable of being `..` or of holding a separator \u2014\n * `path.join(root, safeSegment(x))` cannot leave `root` for any input.\n *\n * Used for BOTH halves of the path. Hardening the file name while leaving the directory unhardened was\n * the wrong half to protect: the directory is the root `gcOldLogDirs` walks with a recursive delete.\n */\nconst safeSegment = (raw: string): string | null => {\n const safe = raw\n .split(/\\W+/)\n .filter((part) => {\n return part !== ''\n })\n .join('-')\n\n return safe === '' ? null : safe\n}\n\n/**\n * Session log root: `<cacheRoot>/<INFRA_KIT_SESSION>/dev/<pid>/`.\n *\n * The `<pid>` segment is load-bearing, not cosmetic. `--cmux` spawns one `infra-kit dev` per pane and\n * every pane inherits the SAME `INFRA_KIT_SESSION`, so without it N panes interleave into one file.\n * Falls back to a literal `no-session` folder when the shell exported no id, so dev logging never\n * depends on `infra-kit init` having run.\n */\nexport const resolveLogDir = (): string => {\n // The session id is collapsed to a single safe segment, exactly as a service name is. It is normally\n // an 8-hex id the init rc exports \u2014 but it is an env var, and this path is the ROOT that `gcOldLogDirs`\n // walks with `rmSync(recursive, force)`. Hardening the file name while leaving the directory that the\n // recursive delete is anchored to unhardened is the wrong half to protect.\n const raw = process.env[INFRA_KIT_SESSION_VAR] ?? ''\n const session = safeSegment(raw) ?? 'no-session'\n\n return path.join(getCacheRoot(), session, 'dev', String(process.pid))\n}\n\n/** `client/api` \u2192 `client-api.log`. */\nexport const logFileName = (service: string): string => {\n return `${safeSegment(service) ?? 'unnamed'}.log`\n}\n\n/**\n * Drop dev log dirs older than {@link GC_MAX_AGE_MS}. A `<pid>` segment means a fresh dir per run, and\n * nothing else ever prunes them \u2014 so the sink prunes its own. Best-effort: a GC failure must never\n * take down a dev session, so every error is swallowed.\n */\nconst GC_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000\n\n/**\n * Is `name` a pid that is still running? `kill(pid, 0)` sends no signal \u2014 it only asks the kernel whether\n * the process exists (and whether we may signal it; `EPERM` means it exists but is not ours, which still\n * answers \"alive\"). A name that is not a pid at all is treated as dead, so a stray directory is still\n * collectable.\n */\nconst isPidAlive = (name: string): boolean => {\n const pid = Number(name)\n\n if (!Number.isInteger(pid) || pid <= 0) return false\n\n try {\n process.kill(pid, 0)\n\n return true\n } catch (error) {\n return (error as { code?: string }).code === 'EPERM'\n }\n}\n\nconst gcOldLogDirs = (devRoot: string, now: number): void => {\n let entries: fs.Dirent[]\n\n try {\n entries = fs.readdirSync(devRoot, { withFileTypes: true })\n } catch {\n return\n }\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n // NEVER touch a dir whose pid is still alive. Age alone is not enough to call one dead: a directory's\n // mtime is not bumped by writes INTO the files it holds, only by create/unlink, and every service\n // file is created at boot. So a dev session left running over a long break looks a week old from the\n // moment it starts \u2014 and deleting it would unlink the very inodes it still holds open, whose writes\n // then succeed silently into nothing. Liveness answers the question age only guesses at.\n if (isPidAlive(entry.name)) continue\n\n const dir = path.join(devRoot, entry.name)\n\n try {\n if (now - fs.statSync(dir).mtimeMs > GC_MAX_AGE_MS) {\n fs.rmSync(dir, { recursive: true, force: true })\n }\n } catch {\n // A dir we cannot stat or remove is a dir we leave alone.\n }\n }\n}\n\n/**\n * Hard per-service byte cap. A logging LOOP \u2014 the crash barrier reporting a fault whose report is itself\n * the fault \u2014 wrote 455 GB of `runner.log` at 50k\u2013127k lines/sec and filled a 926 GB disk. The loop itself\n * is fixed at its source (`terminal-liveness.ts`); this is the net under ANY future one, whatever its cause.\n *\n * A byte cap and not dedupe: this sink's stated principle is that it reads DECLARED provenance and never\n * inspects the bytes, and a \"same line N times\" filter would violate it. Not rotation either \u2014 rotating a\n * fault storm yields two enormous files and still fills the disk.\n *\n * Worst case on disk is `services \u00D7 MAX_LOG_BYTES \u00D7 retained_dirs`; in practice one file storms at a time,\n * so the incident's own shape (5 orphaned processes) caps at \u22481.3 GB against the 455 GB that happened.\n */\nconst DEFAULT_MAX_LOG_BYTES = 256 * 1024 * 1024\n\n/** Escape hatch for a session that legitimately needs a bigger (or smaller) budget. */\nconst MAX_LOG_BYTES_VAR = 'INFRA_KIT_DEV_LOG_MAX_BYTES'\n\n/** `INFRA_KIT_DEV_LOG_MAX_BYTES` when it parses to a positive integer, else {@link DEFAULT_MAX_LOG_BYTES}. */\nexport const resolveMaxLogBytes = (): number => {\n const raw = Number(process.env[MAX_LOG_BYTES_VAR])\n\n return Number.isInteger(raw) && raw > 0 ? raw : DEFAULT_MAX_LOG_BYTES\n}\n\n/**\n * The per-service log sink. One held fd per service, opened lazily on first write.\n *\n * Every routing and counting decision here reads DECLARED provenance \u2014 the service the caller named and\n * the level it chose \u2014 never the bytes of the line. Content is written, never inspected.\n */\nexport class DevLogSink {\n readonly dir: string\n private readonly fds = new Map<string, number>()\n private readonly counters = new Map<string, ServiceStats>()\n /** Bytes this process has written per service, SEEDED from the file's existing size on lazy open. */\n private readonly bytes = new Map<string, number>()\n /** Services whose cap has been hit; every further line is dropped for them, and only for them. */\n private readonly capped = new Set<string>()\n private readonly maxBytes: number\n private closed = false\n\n constructor(dir: string = resolveLogDir(), maxBytes: number = resolveMaxLogBytes()) {\n this.dir = dir\n this.maxBytes = maxBytes\n fs.mkdirSync(this.dir, { recursive: true })\n gcOldLogDirs(path.dirname(this.dir), Date.now())\n this.linkLatest()\n }\n\n /**\n * Point `<session>/dev/latest` at this run's dir, so `tail -f` has a stable path across runs even\n * though the real dir is pid-scoped. Best-effort \u2014 a filesystem without symlink support just loses\n * the shortcut, not the logs.\n */\n private linkLatest(): void {\n const link = path.join(path.dirname(this.dir), 'latest')\n\n try {\n fs.rmSync(link, { force: true })\n fs.symlinkSync(this.dir, link, 'dir')\n } catch {\n // No symlink \u2192 the panel still prints the absolute dir.\n }\n }\n\n /** Absolute path of a service's log file (whether or not it has been written to yet). */\n pathFor(service: string): string {\n return path.join(this.dir, logFileName(service))\n }\n\n /** Lazily open (and cache) the append fd for a service, seeding its byte count from the file on disk. */\n private fdFor(service: string): number | null {\n const cached = this.fds.get(service)\n\n if (cached != null) return cached\n\n try {\n const fd = fs.openSync(this.pathFor(service), 'a')\n\n this.fds.set(service, fd)\n // Seed from the file's CURRENT size, not zero: the fd is opened `'a'`, so a re-attached (or\n // pre-existing) file's bytes are real bytes on the disk we are trying to bound. Counting only our own\n // appends would let an already-huge file grow by another whole cap.\n try {\n this.bytes.set(service, fs.fstatSync(fd).size)\n } catch {\n this.bytes.set(service, 0)\n }\n\n return fd\n } catch {\n // A log file we cannot open must never down the dev session \u2014 the line is dropped, not thrown.\n return null\n }\n }\n\n /**\n * Append `text` to `<service>.log` and fold its DECLARED level into that service's counters.\n *\n * `text` is written verbatim (a trailing newline is added when absent) \u2014 the sink never parses it.\n * `level` comes from the emitter (the `console` method it chose, the Powertools level it set, the\n * HTTP status it returned), so the counter reports what the caller declared rather than guessing.\n */\n write(service: string, text: string, meta: { level?: LogLevel } = {}): void {\n if (this.closed) return\n\n const stats = this.counters.get(service) ?? emptyStats()\n\n if (meta.level === 'error') {\n stats.errors += 1\n stats.lastErrorAt = Date.now()\n } else if (meta.level === 'warn') {\n stats.warns += 1\n }\n this.counters.set(service, stats)\n\n // The cap latch lives HERE \u2014 after the counter bump, before the fd. A top-of-function early return (the\n // obvious implementation) would freeze the panel's red error counter for the whole storm, and a cap that\n // silences the only remaining signal is worse than no cap: it makes the next incident harder to diagnose,\n // which is precisely the objection to capping alone.\n if (this.capped.has(service)) return\n\n const fd = this.fdFor(service)\n\n if (fd == null) return\n\n const line = text.endsWith('\\n') ? text : `${text}\\n`\n const size = Buffer.byteLength(line)\n const written = this.bytes.get(service) ?? 0\n\n if (written + size > this.maxBytes) {\n this.capped.add(service)\n // One loud final line, naming the likely cause, into the service's OWN file (and counted against its\n // own budget). Per-service, so a chatty `client-ui.log` can never silence `runner.log`.\n this.writeRaw(\n fd,\n service,\n `[capped] ${logFileName(service)} hit ${this.maxBytes} bytes; further lines dropped (likely a logging loop)\\n`,\n )\n\n return\n }\n\n this.writeRaw(fd, service, line)\n }\n\n /** The one place bytes reach the disk: write, count what landed, and never throw. */\n private writeRaw(fd: number, service: string, line: string): void {\n try {\n fs.writeSync(fd, line)\n this.bytes.set(service, (this.bytes.get(service) ?? 0) + Buffer.byteLength(line))\n } catch {\n // A failed write is a dropped line, never a crashed dev server.\n }\n }\n\n /** Counters for one service (a service that has never written returns zeroes, not `undefined`). */\n statsFor(service: string): ServiceStats {\n return this.counters.get(service) ?? emptyStats()\n }\n\n /** Every service that has written at least one line. */\n services(): string[] {\n return [...this.fds.keys()]\n }\n\n /**\n * Close every fd. Idempotent, and synchronous by design so it is safe from a `process.on('exit')`\n * handler \u2014 there is no buffered data to flush, because there was never a buffer.\n */\n close(): void {\n if (this.closed) return\n this.closed = true\n for (const fd of this.fds.values()) {\n try {\n fs.closeSync(fd)\n } catch {\n // Already closed / bad fd \u2014 nothing left to do at teardown.\n }\n }\n this.fds.clear()\n }\n}\n", "/**\n * Owns the `'error'` channel of `process.stdout` / `process.stderr` \u2014 the ONLY point at which a failed\n * stdio write can be observed, and therefore the only place the fault loop can be cut.\n *\n * Why an `'error'` listener and nothing else (each alternative was probed, and each one silently fails):\n *\n * - A stdio write NEVER throws synchronously. Node builds the error in `afterWriteDispatched`, routes it\n * through `destroy(err, cb)`, and `internal/streams/destroy.js` schedules `process.nextTick(emitErrorCloseNT)`.\n * It surfaces a TICK LATER as an unhandled `'error'` event, which Node then turns into an\n * `uncaughtException`. So a `try/catch` around the writer catches nothing, and a synchronous\n * re-entrancy latch sees an empty stack.\n * - `stdout.destroyed` / `stdout.writable` are dead discriminators: Node's stdio streams carry a\n * `dummyDestroy` that `_undestroy()`s them, so after the failure they still read `false` / `true` and\n * keep accepting writes (and keep emitting `'error'`). Anything keyed on them never fires.\n *\n * With one listener attached, the `'error'` event has a handler, so it is no longer an `uncaughtException` \u2014\n * the crash barrier is never re-entered by our own writes, and the loop (write \u2192 EIO \u2192 uncaughtException \u2192\n * reportFault \u2192 write) is broken at its only edge.\n *\n * **Do NOT implement liveness by patching `write`.** `output-intercept.uninstall()`\n * (`output-intercept.ts:187`) does `Reflect.deleteProperty(stream, 'write')` \u2014 it would silently delete a\n * `write` patch installed here, and the liveness gate would vanish at exactly the moment (`shutdown()`)\n * the incident begins. The `'error'` event is a third, orthogonal channel that no `deleteProperty` can\n * touch:\n *\n * | Claimant | Channel | Removed by |\n * |--------------------------|-------------------------------------------------------------|-----------------------------|\n * | `output-intercept` | OWN-property `write` on the stream instance (`:149`) | `Reflect.deleteProperty` (`:187`) |\n * | `log-sink` / Ink | PROTOTYPE `write` via Proxy, stepping over the own-property | n/a (a Proxy, not a patch) |\n * | `terminal-liveness` | the **`'error'` event** | `removeListener` |\n *\n * ## Fatality is decided by STREAM IDENTITY, never by error code\n *\n * The listener is bound to the `process.stdout` / `process.stderr` OBJECTS. A handler writing to its own\n * client socket that hung up produces an `EPIPE` too \u2014 under a code-sniff, one user closing a browser tab\n * mid-request would kill the whole dev session, a worse fragility than the one the crash barrier removes.\n * Stream identity IS the proof that cannot happen.\n *\n * The one code-based rule permitted here is an EXCLUSION, not a selection: {@link TRANSIENT_CODES}\n * (`EAGAIN`, `EINTR`) are the only errnos that can mean \"still alive\" (libuv retries `EINTR` internally and\n * defers `EAGAIN` on pipes via `POLLOUT`; a TTY stdout is set blocking anyway). Everything else latches.\n *\n * ## The latch is right even when the terminal is fine\n *\n * With stdout redirected to a file, `process.stdout` is a `SyncWriteStream` and a failed `writeSync`\n * (`ENOSPC`, `EDQUOT`, `EFBIG`, `EBADF`) takes the SAME async `'error'` path. The latch is still correct \u2014\n * *stop writing to a stream that cannot be written to* holds whatever the cause \u2014 but the REPORT must name\n * the errno, never a story: `nohup ik dev > out.log` on a full disk is `ENOSPC`, and saying \"the terminal is\n * gone\" there would be a lie in the exact scenario (a disk-fill) this module exists to fix.\n */\nimport process from 'node:process'\n\n/** Which of the two stdio streams died \u2014 carried into the reason string, never guessed at. */\nexport type StdioStreamName = 'stdout' | 'stderr'\n\n/**\n * Errnos that do NOT mean the stream is gone. `EINTR` is retried inside libuv and `EAGAIN` is deferred via\n * `POLLOUT`, so neither should reach us at all \u2014 they are excluded as cheap insurance, and this exclusion is\n * the ONLY code-based logic in the module (see the doc block: fatality is stream identity, not error shape).\n */\nconst TRANSIENT_CODES = new Set(['EAGAIN', 'EINTR'])\n\nexport interface TerminalLivenessDeps {\n /**\n * The streams to own. Defaults to the real stdio pair. Injectable so a test can drive the real listener\n * with a fake stream instead of breaking the runner's own terminal.\n */\n streams?: NodeJS.WriteStream[]\n /**\n * Called at most ONCE, after the latch is already set. The reason a caller builds from it must name the\n * errno (`stdio unwritable: stdout ENOSPC`), never \"the terminal is gone\".\n */\n onDeath?: (stream: StdioStreamName, error: NodeJS.ErrnoException) => void\n}\n\nexport interface TerminalLiveness {\n /** True once one of the owned streams has emitted a non-transient `'error'`. */\n isDead: () => boolean\n /** Detach the listeners and clear the latch. Production never calls it; it keeps tests hermetic. */\n uninstall: () => void\n}\n\n/**\n * Module-scoped so the write gates in `log-sink.ts` can read it without threading a handle through every\n * writer. Precedent: `dev-server.ts`'s module-scoped `logSink`, wired for exactly the same reason.\n */\nlet dead = false\n\n/**\n * Has an owned stdio stream failed? The gate `rawStdoutWrite` / `rawStderrWrite` consult before every raw\n * terminal write \u2014 and the discriminator the crash barrier uses to decide fatal-vs-survive.\n */\nexport const isTerminalDead = (): boolean => {\n return dead\n}\n\n/** Identity first (the real streams), position as the fallback for injected fakes (`[stdout, stderr]`). */\nconst streamName = (stream: NodeJS.WriteStream, index: number): StdioStreamName => {\n if (stream === process.stdout) return 'stdout'\n if (stream === process.stderr) return 'stderr'\n\n return index === 1 ? 'stderr' : 'stdout'\n}\n\n/**\n * Install the one and only observer of stdio write failures.\n *\n * @example\n * const liveness = installTerminalLiveness({\n * onDeath: (stream, error) => onFatal(`stdio unwritable: ${stream} ${error.code}`),\n * })\n */\nexport const installTerminalLiveness = ({\n streams = [process.stdout, process.stderr],\n onDeath,\n}: TerminalLivenessDeps = {}): TerminalLiveness => {\n /** `onDeath` fires once, not once per stream: closing a terminal kills both fds at the same instant. */\n let fired = false\n const attached: { stream: NodeJS.WriteStream; listener: (error: unknown) => void }[] = []\n\n for (const [index, stream] of streams.entries()) {\n const name = streamName(stream, index)\n const listener = (raw: unknown): void => {\n const error = (raw ?? new Error('stdio error')) as NodeJS.ErrnoException\n\n if (error.code != null && TRANSIENT_CODES.has(error.code)) return\n\n // ORDER IS LOAD-BEARING: the latch closes BEFORE `onDeath` runs. `onDeath` leads to\n // `runner.shutdown()`, whose FIRST act is `renderer.dispose()` \u2192 `rerenderPersistent()`\n // (`persistent-ink-dev-ui.tsx:276-283`) \u2014 a write to this very stream. Latch second and teardown\n // begins by re-arming the thing it is tearing down.\n dead = true\n\n if (fired) return\n fired = true\n\n onDeath?.(name, error)\n }\n\n stream.on('error', listener)\n attached.push({ stream, listener })\n }\n\n return {\n isDead: () => {\n return dead\n },\n uninstall: () => {\n for (const { stream, listener } of attached) {\n stream.removeListener('error', listener)\n }\n attached.length = 0\n // Clearing the latch is what makes a test hermetic \u2014 a leaked `dead` would gate every raw write for\n // the rest of the worker. Nothing in production uninstalls: the dev process owns stdio until it exits.\n dead = false\n fired = false\n },\n }\n}\n", "/*\n * This module's entire job is to take ownership of the global `console`, so it necessarily names every\n * console method \u2014 including the ones `no-console` exists to keep out of application code. The rule is\n * disabled here and NOWHERE else: any other file reaching for `console.log` should still be stopped.\n */\n/* eslint-disable no-console */\n/**\n * Routes every line the dev process emits into the per-service log files, so the terminal can be given\n * over entirely to the status panel.\n *\n * ## Why this has to exist\n *\n * The backend runs IN-PROCESS: `ServerlessLocalRun` is fastify + `@aws-lambda-powertools/logger` in the\n * same node process, and the runner sets `POWERTOOLS_DEV=true`, which makes Powertools bind the GLOBAL\n * `console`. So a handler's `console.log` AND every Powertools line reach `process.stdout` through\n * channels no seam of ours owns. There is no other place to catch them. (Reassigning the Powertools\n * `Logger`'s console does NOT work: handlers are given a `createChild()` logger, and `createChild`\n * re-derives its console from the global \u2014 the parent's is never consulted.)\n *\n * ## Why this is not the classifier that was rejected\n *\n * An earlier design tried to decide, per line, whether to PRINT it \u2014 inferring \"whatever is left on\n * stdout must be human, therefore promote it\". That was unsound: the residual bucket also holds the\n * env-gated raw request line, dependency import banners, and Node's own warnings, so the rule promoted\n * exactly the noise it meant to hide. There is no such decision here. **Every line goes to a file** \u2014\n * one rule, no residue, nothing to be wrong about. The only thing read is DECLARED provenance: which\n * `console` method the caller chose, and which fd it wrote to. The line's bytes are never inspected.\n *\n * ## One switch, not two: NOTHING is ever echoed\n *\n * The sink installs EARLY (at process start) and is file-only from the first byte. There is no tee\n * window and no \"print until `ready()`\" phase: an app's log line NEVER reaches the terminal, at any\n * point in the process's life. That is the whole product decision \u2014 the terminal belongs to the panel.\n *\n * The tee that used to exist was justified by one fear: that a boot crash would vanish into a log file\n * and leave the user staring at a blank screen. That fear is unfounded, and each of the three ways a\n * dev session can die is covered WITHOUT echoing a single log line:\n *\n * 1. An uncaught exception / rejection during boot. Node's fatal report is written STRAIGHT TO FD 2 by\n * the runtime \u2014 it never goes through `process.stderr.write`, so the patch below cannot swallow it.\n * (Verified, not assumed: patch `stderr.write`, throw, and the stack still prints while the patch\n * counts zero chunks.)\n * 2. A boot failure that rejects `run()`. `shutdown()` calls {@link OutputIntercept.uninstall} BEFORE\n * anything prints, so the error surfaces from the entry point's top-level catch on a clean stderr.\n * 3. A fault after `ready()`. The crash barrier routes it to `DevServerRunner.reportFault`, which files\n * it at `error` (turning the row red) AND paints it through the panel's bypass.\n *\n * So a crash is never silent, and a LOG is never printed. Those were always two separate jobs; the tee\n * conflated them, and the price was a Powertools `Server listening` banner printed above the panel.\n *\n * ## Anti-recursion\n *\n * The panel writes to the terminal through {@link rawStdoutWrite}, which resolves `write` off the\n * PROTOTYPE and so steps over the own-property patch installed here. The interceptor's only sink is a\n * file. The failure mode to guard against is not a stack overflow \u2014 it is a BLACK HOLE: hand the panel\n * a patched stream and its frames are quietly filed into a log instead of drawn.\n */\nimport process from 'node:process'\nimport util from 'node:util'\n\nimport type { DevLogSink } from './log-sink.js'\nimport type { LogLevel } from './render.js'\n\n/** The `console` methods that reach a terminal, each with the level its NAME declares. */\nconst CONSOLE_LEVELS = {\n log: 'info',\n info: 'info',\n debug: 'debug',\n trace: 'debug',\n warn: 'warn',\n error: 'error',\n} as const satisfies Record<string, LogLevel>\n\ntype ConsoleMethod = keyof typeof CONSOLE_LEVELS\n\nexport interface OutputInterceptOptions {\n /** Where every captured line is filed. */\n sink: DevLogSink\n /** The service to file a line under when nothing else claims it. A NAMED bucket, never a guess. */\n fallbackService: string\n /** The service owning the current async context, if any (the `AsyncLocalStorage` lookup). */\n currentService: () => string | undefined\n}\n\nexport interface OutputIntercept {\n /** Restore `console` and the raw stream writes to exactly what they were. Idempotent. */\n uninstall: () => void\n}\n\n/**\n * Install the interception. Every captured line goes to a FILE and only to a file \u2014 there is no\n * terminal echo at any point. Call {@link OutputIntercept.uninstall} on every exit path, BEFORE\n * anything else prints.\n *\n * Never install this on a non-TTY / `--json` / MCP run: it would file the machine-readable stream into\n * a log and hand the caller an empty stdout. The caller owns that gate.\n */\nexport const installOutputIntercept = ({\n sink,\n fallbackService,\n currentService,\n}: OutputInterceptOptions): OutputIntercept => {\n let live = true\n\n const serviceFor = (): string => {\n return currentService() ?? fallbackService\n }\n\n /** File a line. There is no second destination: the terminal belongs to the panel. */\n const capture = (text: string, level: LogLevel): void => {\n sink.write(serviceFor(), text, { level })\n }\n\n // ---- console ------------------------------------------------------------\n //\n // The console patches do NOT delegate to the original methods. They must not: `console.log` writes\n // through `process.stdout.write`, which is patched below, so delegating would file every console line\n // TWICE and double every counter. Routing them straight to the sink also preserves the one thing the\n // raw-stream patch cannot see \u2014 the level the caller DECLARED by picking `warn` over `log`.\n const originalConsole = {} as Record<ConsoleMethod, (...args: unknown[]) => void>\n\n for (const method of Object.keys(CONSOLE_LEVELS) as ConsoleMethod[]) {\n originalConsole[method] = console[method] as (...args: unknown[]) => void\n console[method] = (...args: unknown[]): void => {\n if (!live) {\n originalConsole[method](...args)\n\n return\n }\n capture(util.format(...args), CONSOLE_LEVELS[method])\n }\n }\n\n // ---- raw stream writes --------------------------------------------------\n //\n // What is left after the console patch: writes that bypass `console` entirely \u2014 the env-gated raw\n // request line, a dependency's import-time banner, and `process.emitWarning` (which goes to stderr,\n // NOT through `console.warn`). Chunk-oriented, so they are line-buffered before being filed.\n const patchStream = (stream: NodeJS.WriteStream, level: LogLevel): (() => void) => {\n const original = stream.write.bind(stream)\n let pending = ''\n\n const flush = (): void => {\n if (pending === '') return\n sink.write(serviceFor(), pending, { level })\n pending = ''\n }\n\n stream.write = ((chunk: unknown, ...rest: unknown[]): boolean => {\n if (!live || typeof chunk !== 'string') {\n return (original as (...args: unknown[]) => boolean)(chunk, ...rest)\n }\n\n pending += chunk\n\n const lines = pending.split('\\n')\n\n pending = lines.pop() ?? ''\n for (const line of lines) {\n if (line !== '') sink.write(serviceFor(), line, { level })\n }\n\n // A chunk that never ends in a newline (a progress line, a prompt) would otherwise be held\n // forever; flush it once it is clearly not a partial line.\n if (pending.length > 8192) flush()\n\n // Honour the stream contract's completion callback \u2014 `write(chunk, cb)` and `write(chunk, enc, cb)`\n // both promise to call it. Swallowing it hangs any caller that awaits the write before proceeding\n // (a logger flushing before exit, a promisified write), and it would hang inside a dev session\n // whose terminal shows nothing but a frozen panel. Deferred, exactly as a real stream defers it.\n const callback = rest.find((arg) => {\n return typeof arg === 'function'\n })\n\n if (typeof callback === 'function') {\n process.nextTick(callback as () => void)\n }\n\n return true\n }) as NodeJS.WriteStream['write']\n\n return (): void => {\n flush()\n // Delete the own-property patch rather than reassigning the original: reassigning would leave a\n // second own property in place, and `hasForeignStdoutPatch` (and the next installer) would still\n // see a patched stream.\n Reflect.deleteProperty(stream, 'write')\n }\n }\n\n // stderr is levelled `warn`, not `error`: `process.emitWarning` is far and away its highest-volume\n // user, and a genuine fault does not rely on this level \u2014 the crash barrier reports through\n // `DevServerRunner.reportFault`, which files at `error` AND punches the stack onto the terminal.\n // Neither stream is echoed anywhere: a level here decides which FILE bucket and which counter a line\n // lands in, nothing more.\n const restoreStdout = patchStream(process.stdout, 'info')\n const restoreStderr = patchStream(process.stderr, 'warn')\n\n return {\n uninstall: (): void => {\n if (!live) return\n live = false\n restoreStdout()\n restoreStderr()\n for (const method of Object.keys(CONSOLE_LEVELS) as ConsoleMethod[]) {\n console[method] = originalConsole[method] as typeof console.log\n }\n },\n }\n}\n", "/**\n * Pure port / URL-prefix resolution for the dev-server.\n *\n * These functions are intentionally side-effect free: the environment is passed\n * in (never read from `process.env` here) and no cwd / fs access happens. That\n * keeps port precedence and conflict detection unit-testable in isolation.\n */\nimport type { DevConfig } from '../lib/infra-kit-config/index.js'\n\n/** Fallback port when no PORT / {APP}_PORT env var and no config port is set. */\nexport const DEFAULT_PORT = 3010\n\n/**\n * URL prefix applied to each app's routes when nothing is configured. Overridable\n * per app via `dev.<app>.prefixUrl` in infra-kit.json \u2014 this is only the fallback.\n */\nexport const DEFAULT_PREFIX_URL = '/api/v1'\n\n/**\n * Parse a raw port string (env var or config), stripping a single pair of\n * surrounding quotes and treating blank / non-numeric input as \"unset\".\n */\nexport function parsePortString(raw: string | undefined): number | undefined {\n if (raw == null || raw === '') {\n return undefined\n }\n\n const n = parseInt(raw.trim().replace(/^[\"']|[\"']$/g, ''), 10)\n\n return Number.isNaN(n) ? undefined : n\n}\n\n/**\n * Resolve the PORT for an API app (highest priority first):\n *\n * 1. **`{APP}_PORT`** \u2014 e.g. `CLIENT_PORT`, `SEARCH_ENGINE_PORT` (secrets manager or shell)\n * 2. **`PORT`** \u2014 shared fallback (multi-app: use distinct `{APP}_PORT` in env)\n * 3. **`dev.<app>.port`** from infra-kit.json\n * 4. Default {@link DEFAULT_PORT}\n *\n * Per-app env keys use the app folder name in **UPPER_SNAKE_CASE** (hyphens \u2192 underscores).\n */\nexport function resolvePort(appName: string, env: NodeJS.ProcessEnv, devConfig: DevConfig): number {\n return resolvePreferredPort(appName, env, devConfig) ?? DEFAULT_PORT\n}\n\n/**\n * Resolve the EXPLICITLY-configured port for an API app \u2014 `{APP}_PORT`, then `PORT`, then\n * `dev.<app>.port` \u2014 or `undefined` when none is set. Unlike {@link resolvePort} this does\n * NOT fall back to {@link DEFAULT_PORT}: it distinguishes an app the developer pinned to a\n * port (a preferred bind target) from an unconfigured app (which binds ephemeral straight\n * away under dynamic allocation). Used by the dev-server to (a) pick the preferred bind\n * port and (b) relax the conflict gate to explicit ports only.\n */\nexport function resolvePreferredPort(\n appName: string,\n env: NodeJS.ProcessEnv,\n devConfig: DevConfig,\n): number | undefined {\n const prefix = appName.replace(/-/g, '_').toUpperCase()\n const prefixedKey = `${prefix}_PORT`\n\n const fromPrefixed = parsePortString(env[prefixedKey])\n\n if (fromPrefixed != null) {\n return fromPrefixed\n }\n\n const fromPort = parsePortString(env.PORT)\n\n if (fromPort != null) {\n return fromPort\n }\n\n return devConfig[appName]?.port ?? undefined\n}\n\n/**\n * Resolve the URL prefix for an API app: `dev.<app>.prefixUrl` from\n * infra-kit.json, falling back to {@link DEFAULT_PREFIX_URL} (`/api/v1`).\n */\nexport function resolvePrefixUrl(appName: string, devConfig: DevConfig): string {\n return devConfig[appName]?.prefixUrl ?? DEFAULT_PREFIX_URL\n}\n\n/** The apps that collide on a port, plus the raw duplicate-port list for messaging. */\nexport interface PortConflicts {\n /** Ports that appear more than once (one entry per extra occurrence, in scan order). */\n duplicatePorts: number[]\n /** Every app whose resolved port is one of the duplicates. */\n conflictingApps: Array<{ name: string; port: number }>\n}\n\n/**\n * Find apps that resolve to the same port. Returns empty `duplicatePorts` when\n * there is no conflict; the caller decides how to surface / throw.\n */\nexport function findPortConflicts(apps: Array<{ name: string; port: number }>): PortConflicts {\n const ports = apps.map((a) => {\n return a.port\n })\n const duplicatePorts = ports.filter((port, index) => {\n return ports.indexOf(port) !== index\n })\n const conflictingApps = apps.filter((a) => {\n return duplicatePorts.includes(a.port)\n })\n\n return { duplicatePorts, conflictingApps }\n}\n", "import { Logger } from '@aws-lambda-powertools/logger'\nimport type { APIGatewayProxyEvent, APIGatewayProxyEventQueryStringParameters, Context } from 'aws-lambda'\nimport fastify from 'fastify'\nimport * as fs from 'node:fs'\nimport type { Server } from 'node:http'\nimport * as path from 'node:path'\nimport process from 'node:process'\nimport { pathToFileURL } from 'node:url'\nimport { parse as parseYaml } from 'yaml'\n\nimport type { ILogger } from './interfaces.js'\nimport { enterAttribution, runAttributed } from './log-attribution.js'\n\nexport interface IServerConfig {\n controllersPath: string\n prefixUrl?: string\n /**\n * PREFERRED bind port. A number is tried first and, on `EADDRINUSE`, the runner falls\n * back to an ephemeral `listen(0)` port. `undefined` (no explicit config) binds an\n * ephemeral port straight away. After {@link ServerlessLocalRun.start} the field is\n * mutated in place to the ACTUAL bound port so `/__health` reports the real port.\n */\n port?: number\n /** App folder name, surfaced by the `/__health` endpoint. Optional. */\n appName?: string\n /**\n * Structured per-request sink. When provided, every response emits `{ method, path, status, ms }`\n * (path already trimmed of any query string) so the caller can tag + timestamp it \u2014 the dev-server\n * routes this into its renderer's live tail. Independent of the env-gated raw stdout line below.\n */\n onRequestLog?: (event: { method: string; path: string; status: number; ms: number }) => void\n /**\n * The service tag (`<app>/api`) every line this app emits is filed under \u2014 a handler's `console.log`,\n * a Powertools line, a dependency's banner. The backend is IN-PROCESS and multi-app, so nothing in a\n * raw stdout write says which app wrote it; entering an `AsyncLocalStorage` context per request is\n * what makes the attribution possible at all. Omit \u2192 those lines land in the runner's fallback bucket.\n */\n serviceTag?: string\n}\n\n/** True when a listen error is a port-already-in-use (`EADDRINUSE`) failure. */\nconst isAddressInUse = (error: unknown): boolean => {\n return (error as { code?: string } | null)?.code === 'EADDRINUSE'\n}\n\ntype HandlerResult = Promise<{ body: string; headers: Record<string, string>; statusCode: number }>\n\n/** Default simulated Lambda timeout; overridable via `DEV_SERVER_TIMEOUT_MS`. */\nconst DEFAULT_LAMBDA_TIMEOUT_MS = 30_000\n\n/** Resolve the simulated Lambda timeout (ms), honoring `DEV_SERVER_TIMEOUT_MS` when it parses. */\nconst resolveLambdaTimeoutMs = (): number => {\n const raw = Number.parseInt(process.env.DEV_SERVER_TIMEOUT_MS ?? '', 10)\n\n return Number.isNaN(raw) ? DEFAULT_LAMBDA_TIMEOUT_MS : raw\n}\n\n/**\n * Whether to emit a one-line `<method> <url> \u2192 <status> <ms>ms` log per request.\n * Off by default; opt in with `DEV_SERVER_REQUEST_LOG=1` (kept out of the Powertools\n * JSON logger so the line stays terminal-readable). Mirrors the env-reader precedent\n * of {@link resolveLambdaTimeoutMs}.\n */\nconst isRequestLogEnabled = (): boolean => {\n return process.env.DEV_SERVER_REQUEST_LOG === '1'\n}\n\nexport class ServerlessLocalRun {\n /** Busts Node ESM `import()` cache on each new server instance (watch restart). */\n private readonly importCacheBust: string\n private readonly logger: Logger\n private readonly server: ReturnType<typeof fastify>\n private readonly controllers: Record<\n string,\n {\n action: Record<string, (event: APIGatewayProxyEvent, ctx: Context, log: ILogger) => HandlerResult>\n handler: string\n }\n > = {}\n\n /** `method urlAction` keys reserved synchronously, so duplicates are caught before any async import. */\n private readonly registeredRouteKeys = new Set<string>()\n\n constructor(private readonly serverConfig: IServerConfig) {\n this.importCacheBust = `${Date.now()}`\n this.logger = new Logger({ serviceName: 'LocalServer', logLevel: 'DEBUG' })\n this.serverConfig.prefixUrl = this.serverConfig.prefixUrl ?? ''\n this.server = fastify({ logger: false })\n\n // Add CORS support for local development\n this.server.addHook(\n 'onRequest',\n async (\n request: { method: string },\n reply: { header: (k: string, v: string) => unknown; status: (n: number) => { send: () => void } },\n ) => {\n // Claim every line the rest of this request emits for this app. `enterWith` (not `run`) because\n // fastify owns the call into the handler \u2014 we cannot wrap it. Entering here makes the WHOLE\n // remaining hook chain a continuation of this context: the handler, `onResponse` (where the raw\n // request line is written), and the error handler all attribute to the same app.\n const serviceTag = this.serverConfig.serviceTag\n\n if (serviceTag != null) enterAttribution(serviceTag)\n\n reply.header('Access-Control-Allow-Origin', '*')\n reply.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS')\n reply.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With')\n\n // Handle preflight OPTIONS requests\n if (request.method === 'OPTIONS') {\n reply.status(204).send()\n }\n },\n )\n\n // Per-request visibility for live dev traffic. Two independent sinks: a structured\n // `onRequestLog` callback (the dev-server tags + timestamps it in its tail) and/or the\n // legacy env-gated raw stdout line (standalone use). Registered only when at least one is on.\n const onRequestLog = this.serverConfig.onRequestLog\n\n if (isRequestLogEnabled() || onRequestLog) {\n this.server.addHook(\n 'onResponse',\n async (request: { method: string; url: string }, reply: { statusCode: number; elapsedTime: number }) => {\n const ms = Math.round(reply.elapsedTime)\n // Trim the query string so the tail shows a clean route, not `?a=b` noise.\n const requestPath = request.url.split('?')[0] ?? request.url\n\n onRequestLog?.({ method: request.method, path: requestPath, status: reply.statusCode, ms })\n if (isRequestLogEnabled()) {\n process.stdout.write(`${request.method} ${request.url} \u2192 ${reply.statusCode} ${ms}ms\\n`)\n }\n },\n )\n }\n }\n\n /**\n * The registered `METHOD /path` route keys (sorted). Excludes the internal\n * `/__health` liveness route, which is registered outside {@link defineRoute} and\n * never added to {@link registeredRouteKeys}. Used by the runner's startup route dump.\n */\n public getRegisteredRoutes(): string[] {\n return [...this.registeredRouteKeys].sort()\n }\n\n /**\n * Boot the server and RETURN the actual bound port. Under dynamic allocation the port is\n * a runtime fact known only after `listen`, so the caller must consume this return value\n * (never the static config port). {@link serverConfig.port} is mutated in place to the\n * bound port so `/__health` reports the real value.\n */\n public async start(): Promise<number> {\n this.registerHealthRoute()\n\n await Promise.all(this.loadRoutes())\n\n const boundPort = await this.listenWithFallback()\n\n this.serverConfig.port = boundPort\n\n this.logger.info(`Server listening on http://127.0.0.1:${boundPort}`, {\n address: `http://127.0.0.1:${boundPort}`,\n })\n\n return boundPort\n }\n\n /**\n * Bind the server and return the ACTUAL bound port. With an explicitly-configured\n * preferred port, try it first and fall back to an ephemeral `listen(0)` on `EADDRINUSE`\n * so extra worktrees never collide; with no preferred port (`undefined`), bind ephemeral\n * straight away. Non-`EADDRINUSE` errors propagate.\n */\n private async listenWithFallback(): Promise<number> {\n const preferred = this.serverConfig.port\n\n if (preferred != null) {\n try {\n await this.server.listen({ port: preferred, host: '127.0.0.1' })\n\n return this.readBoundPort()\n } catch (error) {\n if (!isAddressInUse(error)) {\n throw error\n }\n }\n }\n\n await this.server.listen({ port: 0, host: '127.0.0.1' })\n\n return this.readBoundPort()\n }\n\n /** Read the concrete bound port from the underlying HTTP server after `listen`. */\n private readBoundPort(): number {\n const address = (this.server.server as Server).address()\n\n if (address == null || typeof address === 'string') {\n throw new Error('Server address unavailable after listen()')\n }\n\n return address.port\n }\n\n /** Close the server (for watch/restart). */\n public async close(): Promise<void> {\n const raw = this.server.server as Server\n\n if (typeof raw.closeAllConnections === 'function') {\n raw.closeAllConnections()\n }\n await this.server.close()\n }\n\n /**\n * Register a fixed `GET /__health` liveness route returning 200. The path is\n * unprefixed (serverless.yml routes carry the `prefixUrl`, e.g. `/api/v1/...`),\n * so it never collides with a handler route.\n */\n private registerHealthRoute(): void {\n this.server.route({\n method: 'GET',\n url: '/__health',\n handler: (_request: unknown, reply: { code: (n: number) => { send: (body: unknown) => void } }) => {\n return reply.code(200).send({\n status: 'ok',\n app: this.serverConfig.appName ?? null,\n port: this.serverConfig.port,\n })\n },\n })\n }\n\n private loadRoutes(): Promise<void>[] {\n const serverlessYmlPath = path.join(this.serverConfig.controllersPath, 'serverless.yml')\n const fileContents = fs.readFileSync(serverlessYmlPath, 'utf8')\n const data = parseYaml(fileContents) as {\n functions: Record<string, { events?: Array<{ http?: { method: string; path: string } }>; handler?: string }>\n }\n const p: Promise<void>[] = []\n\n if (!data?.functions) return p\n\n for (const funcDef of Object.values(data.functions)) {\n if (!funcDef?.events?.length) continue\n for (const element of funcDef.events) {\n const http = element?.http\n\n if (!http) continue\n p.push(this.defineRoute(http, funcDef))\n }\n }\n\n return p\n }\n\n private async defineRoute(http: { method: string; path: string }, funcDef: { handler?: string }): Promise<void> {\n let url = http.path.toString()\n\n url = url.replaceAll('{', ':').replaceAll('}', '')\n\n let urlAction = path.posix.join(this.serverConfig.prefixUrl ?? '', url)\n\n urlAction = urlAction[0] === '/' ? urlAction : `/${urlAction}`\n\n const validMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']\n const method = String(http.method).toUpperCase()\n\n if (!validMethods.includes(method)) {\n throw new Error(`Invalid HTTP method: \"${http.method}\" for URL: ${urlAction}`)\n }\n\n // Key on method + path: two events can share a path but differ by method (e.g. GET/POST /users),\n // so keying on the path alone would let one handler overwrite the other. Reserve the key\n // synchronously (before the first `await`) so concurrent route loads detect a true duplicate.\n const routeKey = `${method} ${urlAction}`\n\n if (this.registeredRouteKeys.has(routeKey)) {\n throw new Error(`Duplicate route: ${routeKey}`)\n }\n this.registeredRouteKeys.add(routeKey)\n\n const handlerStr = funcDef.handler ?? ''\n const parts = handlerStr.split('.')\n const filepath = parts[0] ?? ''\n const handler = parts[1] ?? ''\n\n const controllerPath = path.join(this.serverConfig.controllersPath, `${filepath}.js`)\n const fileUrl = pathToFileURL(controllerPath)\n\n // Search params bust Node's ESM import cache so watch rebuilds load new `dist` output.\n fileUrl.searchParams.set('v', this.importCacheBust)\n\n // Attribute the handler module's IMPORT-TIME output \u2014 a banner from one of its deps, a top-level\n // log \u2014 to this app rather than the runner's fallback bucket. Honest limit: a library shared by two\n // apps is ONE module instance, so anything it registers at import keeps whichever app loaded it\n // first. That is inference, not declaration; the fallback bucket is named precisely so the wrong\n // guess is never made silently.\n const serviceTag = this.serverConfig.serviceTag\n const importHandler = async (): Promise<\n Record<string, (event: APIGatewayProxyEvent, ctx: Context, log: ILogger) => HandlerResult>\n > => {\n return (await import(fileUrl.href)) as Record<\n string,\n (event: APIGatewayProxyEvent, ctx: Context, log: ILogger) => HandlerResult\n >\n }\n const action = serviceTag == null ? await importHandler() : await runAttributed(serviceTag, importHandler)\n\n this.controllers[routeKey] = { action, handler }\n\n const traceLogger = this.logger.createChild({ serviceName: 'RequestLogger' })\n\n this.server.route({\n method: method as 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS',\n url: urlAction,\n handler: async (\n request: { body?: unknown; query?: unknown; params?: unknown; headers?: unknown },\n reply: {\n headers: (h: Record<string, string>) => unknown\n code: (n: number) => { send: (body: unknown) => void }\n },\n ) => {\n const controller = this.controllers[routeKey]\n\n if (!controller) throw new Error(`No controller for ${routeKey}`)\n const handlerFn = controller.action[controller.handler]\n\n if (!handlerFn) throw new Error(`No handler ${controller.handler} for ${urlAction}`)\n const retVal = await handlerFn(\n this.getEventObj(request.body, request.query, request.params, request.headers, method, urlAction),\n this.getContext(),\n traceLogger,\n )\n const responseBody = JSON.parse(retVal.body)\n\n reply.headers(retVal.headers ?? {})\n\n return reply.code(retVal?.statusCode ?? 500).send(responseBody)\n },\n })\n }\n\n private getEventObj(\n requestBody?: unknown,\n queryParams?: unknown,\n pathParameters?: unknown,\n headers?: unknown,\n httpMethod = '',\n path = '',\n ): APIGatewayProxyEvent {\n const retVal = {\n body: requestBody ? JSON.stringify(requestBody) : null,\n headers: (headers ?? {}) as APIGatewayProxyEvent['headers'],\n multiValueHeaders: {},\n httpMethod,\n isBase64Encoded: false,\n path,\n pathParameters: pathParameters ?? null,\n queryStringParameters: (queryParams as APIGatewayProxyEventQueryStringParameters) ?? null,\n multiValueQueryStringParameters: null,\n stageVariables: null,\n requestContext: {\n accountId: '',\n apiId: '',\n authorizer: undefined,\n protocol: '',\n httpMethod,\n identity: {\n accessKey: null,\n accountId: null,\n apiKey: null,\n apiKeyId: null,\n caller: null,\n clientCert: null,\n cognitoAuthenticationProvider: null,\n cognitoAuthenticationType: null,\n cognitoIdentityId: null,\n cognitoIdentityPoolId: null,\n principalOrgId: null,\n sourceIp: 'devIp',\n user: null,\n userAgent: null,\n userArn: null,\n },\n path,\n stage: '',\n requestId: '',\n requestTimeEpoch: 0,\n resourceId: '',\n resourcePath: path,\n },\n resource: path,\n }\n\n ;(retVal as APIGatewayProxyEvent & { source: string }).source = 'aws.events'\n\n return retVal as APIGatewayProxyEvent\n }\n\n private getContext(): Context {\n const startTime = Date.now()\n const timeoutMs = resolveLambdaTimeoutMs()\n const datePart = new Date().toISOString().split('T')[0] ?? ''\n\n return {\n callbackWaitsForEmptyEventLoop: false,\n functionName: 'local-dev',\n functionVersion: '1.0.0',\n invokedFunctionArn: 'arn:aws:lambda:local:000000000000:function:local-dev',\n memoryLimitInMB: '1024',\n awsRequestId: `local-${Date.now()}`,\n logGroupName: '/aws/lambda/local-dev',\n logStreamName: `${datePart}/local`,\n getRemainingTimeInMillis: (): number => {\n return Math.max(0, timeoutMs - (Date.now() - startTime))\n },\n done: (_error?: Error, _result?: unknown): void => {},\n fail: (_error: string | Error): void => {},\n succeed: (_messageOrObject: unknown): void => {},\n }\n }\n}\n", "/*\n * `pnpm` is resolved from PATH \u2014 the same trust posture as the rest of the dev-server, which\n * already shells out to `pnpm exec turbo \u2026`; the args are fixed literals plus discovered package\n * names, never shell-interpolated. Matches the file-level disable in `scripts/build.js`.\n */\n/* eslint-disable sonarjs/no-os-command-from-path */\n/**\n * `turbo watch build` engine for `infra-kit dev --watch`.\n *\n * Spawns ONE long-lived `turbo watch build` child that owns incremental rebuilds and\n * dependency-graph fan-out. Its stdout is teed to the runner log but NEVER parsed for\n * control flow: piped `turbo watch` block-buffers stdout and emits no per-task completion\n * marker for `tsc -b` tasks, so the dev-server derives \"a build finished\" from watching\n * `dist/` output instead (see {@link file://./dev-server.ts} `setupWatch`).\n *\n * The child is a 5-deep tree (`sh \u2192 pnpm \u2192 node \u2192 turbo \u2192 native binary`); killing the\n * wrapper PID orphans the rest. So it is spawned `detached` (its own process group) and\n * torn down with a process-GROUP signal \u2014 verified to fully reap the tree.\n */\nimport { spawn } from 'node:child_process'\nimport * as fs from 'node:fs'\n\nimport { superviseChild } from './managed-child.js'\nimport type { ManagedChild, UnexpectedExitHandler } from './managed-child.js'\n\n/** Handle to the running `turbo watch` child; `kill()` reaps the whole process group. */\nexport type TurboWatchHandle = ManagedChild\n\n/** Injectable spawn seam so tests run the orchestrator without a real turbo child. */\nexport type TurboWatchFactory = (opts: TurboWatchOptions) => TurboWatchHandle\n\nexport interface TurboWatchOptions {\n /**\n * API app package names, watched DEPENDENCY-INCLUSIVE (`--filter=...<pkg>`): rebuild the app AND its\n * dependency closure, so editing a shared lib the backend uses triggers a rebuild + restart.\n */\n depInclusive: string[]\n /**\n * UI app package names, watched DEP-CLOSURE-ONLY (`--filter=<pkg>^...`): rebuild the frontend's shared-lib\n * dependencies but NEVER the UI's own `build` (that would run a production `vite build` \u2014 the UI's live\n * dev is owned by the separate `turbo run dev`/vite child). Covers FE-only libs + UI-only sessions.\n */\n depClosure: string[]\n /** Consumer repo cwd the child runs in (turbo resolves the consumer's own pin here). */\n cwd: string\n /** Runner log file; the child's stdout+stderr are appended to it. */\n logFile: string\n /**\n * Called if the engine dies on its own (not via `kill()`): incremental rebuilds silently stop, so\n * the runner surfaces it. Optional so the injected test factory can ignore it.\n */\n onUnexpectedExit?: UnexpectedExitHandler\n}\n\n/**\n * Build the `--filter=` arg vector: `...<pkg>` (dependency-inclusive) for each API package, `<pkg>^...`\n * (dependencies only, excluding the package itself) for each UI package. Pure/order-stable so the exact\n * emitted args are unit-testable; API-only input reproduces the historical `--filter=...<pkg>` vector.\n *\n * @example\n * buildTurboWatchFilters(['api-a'], ['ui-a']) // => ['--filter=...api-a', '--filter=ui-a^...']\n */\nexport const buildTurboWatchFilters = (depInclusive: string[], depClosure: string[]): string[] => {\n return [\n ...depInclusive.map((name) => {\n return `--filter=...${name}`\n }),\n ...depClosure.map((name) => {\n return `--filter=${name}^...`\n }),\n ]\n}\n\n/**\n * Default factory: spawn `pnpm exec turbo watch build <filters> --continue=dependencies-successful\n * --env-mode=loose` detached, tee output to `logFile`, and reap the process group on `kill()`.\n *\n * Filters come from {@link buildTurboWatchFilters}: `...<api>` (dep-inclusive) rebuilds a backend + its\n * closure; `<ui>^...` (dep-closure-only) rebuilds the frontend's shared libs without production-building\n * the UI. `--continue=dependencies-successful` keeps the watcher alive when one package fails to compile,\n * so a shared-lib type error never tears down the whole engine and the last-good `dist/` keeps serving.\n */\nexport const defaultTurboWatchFactory: TurboWatchFactory = ({\n depInclusive,\n depClosure,\n cwd,\n logFile,\n onUnexpectedExit,\n}) => {\n const filters = buildTurboWatchFilters(depInclusive, depClosure)\n const out = fs.openSync(logFile, 'a')\n const child = spawn(\n 'pnpm',\n ['exec', 'turbo', 'watch', 'build', ...filters, '--continue=dependencies-successful', '--env-mode=loose'],\n { cwd, detached: true, stdio: ['ignore', out, out] },\n )\n\n return superviseChild(child, undefined, onUnexpectedExit)\n}\n", "/*\n * `pnpm` is resolved from PATH \u2014 same trust posture as the rest of the dev-server (which already\n * shells out to `pnpm exec turbo \u2026`); args are fixed literals plus discovered package names, never\n * shell-interpolated. Matches the file-level disable in `scripts/build.js`.\n */\n/* eslint-disable sonarjs/no-os-command-from-path */\n/**\n * Frontend dev engine for `infra-kit dev --ui`.\n *\n * Delegates FE to ONE `turbo run dev` child (turbo owns the `dev` fan-out and concurrency) rather than\n * infra-kit spawning each framework itself \u2014 the same delegation choice already made for builds via\n * `turbo watch`. infra-kit treats UIs opaquely: it runs their `dev` script (vite/vike/astro/\u2026) and\n * never encodes per-framework knowledge.\n *\n * This child's stdio is PIPED, never inherited. With `inherit`, turbo and the framework write straight\n * to the TTY, bypassing the renderer entirely: turbo's run chrome interleaves with the pinned footer,\n * and vite's `Local:` URL contradicts the proxy hero URL the ready header already shows. Piping makes\n * `infra-kit dev` the single owner of the terminal \u2014 every line is tee'd verbatim to the runner log,\n * and the framework's own lines are routed through `onLine` into the renderer's tagged tail, so vite's\n * URLs, HMR notices and compile errors surface INSIDE the UI instead of fighting it.\n *\n * `--only` keeps that tail signal-dense. A `dev` task `dependsOn: [\"^build\"]`, so turbo would otherwise\n * re-walk the whole dependency closure and emit one `cache hit` line per dep \u2014 work the runner already\n * did in `buildUiApps` (`turbo run build <pkg>^...`) before spawning this child. `--only` drops those\n * `^build` tasks from the graph, so the redundant walk never happens. `--output-logs=new-only` collapses\n * any stray cache-hit replay, `--no-update-notifier` drops turbo's \"Update available\" banner, and\n * `--ui=stream` pins line-oriented output (turbo picks it anyway off a pipe, but it is cheap to be\n * explicit, and it is what `parseTurboDevLine`'s `<pkg>:dev:` prefix contract depends on).\n *\n * Detached \u2192 its own process group; reaped as a group (SIGTERM\u2192SIGKILL) via {@link superviseChild}.\n */\nimport { spawn } from 'node:child_process'\nimport process from 'node:process'\nimport type { Readable } from 'node:stream'\n\nimport { superviseChild } from './managed-child.js'\nimport type { ManagedChild, UnexpectedExitHandler } from './managed-child.js'\n\n/** Handle to the running `turbo run dev` child; `kill()` reaps the whole process group. */\nexport type UiDevHandle = ManagedChild\n\n/** Injectable spawn seam so tests run the orchestrator without a real turbo child. */\nexport type UiDevFactory = (opts: UiDevOptions) => UiDevHandle\n\n/** One framework output line, already stripped of turbo's `<pkg>:dev:` prefix. */\nexport interface TurboDevLine {\n /** The turbo package name that emitted the line (e.g. `website-ui`). */\n pkg: string\n /** The framework's own text, ANSI-stripped. */\n text: string\n /** Severity, read out of turbo's own line format \u2014 see {@link turboLineLevel}. */\n level: 'info' | 'error'\n}\n\n/**\n * Vocabulary a framework uses to announce a failure on turbo's stream.\n *\n * **Why this reads the line's text, when nothing else in the design does.** Under `--ui=stream` turbo\n * relays each task's stdout AND stderr onto its OWN stdout \u2014 measured, not assumed: a task writing one\n * line to each fd yields both lines on turbo's fd 1, and fd 2 carries only turbo's chrome. So the fd\n * that would otherwise DECLARE severity does not survive the relay: `child.stderr` never sees a single\n * framework line, and a level counter built on it would be structurally, permanently zero. The panel\n * would then show a green `client/ui` row over a UI that fails to compile \u2014 the one failure mode this\n * whole design exists to prevent.\n *\n * This is not the residual-bucket guess that was rejected. That one asked \"what IS this line?\" of an\n * unknown channel and promoted whatever it could not identify. This asks a narrower question of a KNOWN,\n * declared format: turbo's `<pkg>:dev:` prefix contract is the same one already relied on to route the\n * line to its package. The rule: classify only within a format you know; never guess about one you\n * don't.\n *\n * Deliberately small and anchored. A miss costs an uncounted error (the line is still in the log); a\n * false positive costs a red row over a healthy app, which is worse \u2014 so patterns must be specific, and\n * every addition needs a real line that motivates it.\n */\nconst ERROR_VOCABULARY = [\n /^error\\b/i,\n // The glyphs get no `\\b`: they are not word characters, so there is NO word boundary between `\u2718` and\n // the space that follows it \u2014 `\u2718\\b` never matches the esbuild/vite lines it was aimed at. Anchoring\n // them alone is both correct and safe, since a line can only start with one by way of announcing a\n // failure.\n /^[\u2718\u2716\u00D7]/,\n /^\\[vite\\][^\\n]*\\berror\\b/i,\n // A thrown JS error's first line (`TypeError: x is not a function`). ANCHORED, unlike a bare\n // `/\\berror:/i`, which fires on any line merely containing `error:` \u2014 inside a URL, a JSON blob, or a\n // dev script's own echo.\n /^\\w*Error: /,\n // `tsc`/`vue-tsc --watch` diagnostics: `src/foo.ts(3,5): error TS2322: \u2026`. Matched explicitly because\n // NONE of the other patterns reach it \u2014 the line starts with a path, and `error` is followed by a\n // space, not a colon. Without this a `--watch` type-check task can fail to compile while its row on\n // the panel stays green, which is the exact lie this counter exists to prevent.\n /\\berror TS\\d+\\b/,\n // Specific vite/esbuild failures, NOT a bare `^failed to`: that also matches the entirely benign\n // `Failed to load source map for \u2026`, and a red row over a healthy app costs more trust than an\n // uncounted error costs information (the line is still in the log either way).\n /^Failed to (?:resolve|load url|parse|compile)\\b/i,\n /^(?:ENOENT|EADDRINUSE|ECONNREFUSED)\\b/,\n /pre-transform error/i,\n /\\b(?:build|transform|compilation) failed\\b/i,\n // NOTE: stack frames (` at Module._compile (\u2026)`) are deliberately NOT here. Matching them would\n // turn one thrown exception into one error per FRAME \u2014 a 20-frame stack reading as `\u26A0 21`, at which\n // point the number stops being a count of anything. The throw's first line is already matched above.\n] as const\n\n/**\n * Severity of one framework line, from turbo's own relayed format. `error` when the framework announced\n * a failure in its output; `info` otherwise. See {@link ERROR_VOCABULARY} for why this cannot come from\n * the file descriptor.\n */\nexport const turboLineLevel = (text: string): 'info' | 'error' => {\n return ERROR_VOCABULARY.some((pattern) => {\n return pattern.test(text)\n })\n ? 'error'\n : 'info'\n}\n\nexport interface UiDevOptions {\n /** UI app package names; each becomes an exact `--filter=<pkg>` (turbo runs its `dev` task). */\n packageNames: string[]\n /** Consumer repo cwd the child runs in. */\n cwd: string\n /**\n * Concurrency cap. Must be \u2265 the number of selected UI `dev` tasks (they're persistent) \u2014\n * turbo hard-errors when persistent tasks exceed concurrency (default 10).\n */\n concurrency: number\n /**\n * Extra env merged over `process.env` for the turbo child (Layer B passes `INFRA_KIT_UI_PORTS`).\n * `turbo \u2026 --env-mode=loose` passes the full env through to each vite `dev` task. Omit \u2192 inherit only.\n */\n env?: Record<string, string>\n /** Raw child output (turbo chrome included, ANSI intact) appended verbatim to the runner log. */\n appendLog?: (text: string) => void\n /** One call per framework output line; turbo's own chrome is filtered out first. */\n onLine?: (line: TurboDevLine) => void\n /**\n * Called if the child dies on its own (not via `kill()`): every UI's live reload silently stops, so\n * the runner surfaces it. Optional so the injected test factory can ignore it.\n */\n onUnexpectedExit?: UnexpectedExitHandler\n}\n\n/* eslint-disable no-control-regex, sonarjs/no-control-regex -- terminal escapes and control chars are, by definition, control chars. */\n/** An OSC sequence: ESC `]` ... terminated by BEL or ST. Frameworks emit these for terminal hyperlinks. */\nconst OSC_ESCAPE = /\\u001B\\][\\s\\S]*?(?:\\u0007|\\u001B\\\\)/g\n/** A CSI escape: ESC `[`, parameter bytes, intermediate bytes, final byte. Covers every SGR colour. */\nconst CSI_ESCAPE = /\\u001B\\[[0-9;?]*[\\u0020-\\u002F]*[\\u0040-\\u007E]/g\n/**\n * Every remaining C0 control char except TAB. CR is the dangerous one: written straight to a TTY it\n * snaps the cursor to column 0, smearing the row and the pinned footer below it - exactly the corruption\n * that piping the child was meant to end. LF cannot appear here: `pumpLines` already split on it.\n */\nconst CONTROL_CHARS = /[\\u0000-\\u0008\\u000B-\\u001F\\u007F]/g\n/* eslint-enable no-control-regex, sonarjs/no-control-regex */\n\n/**\n * Make a raw child line safe for the renderer's tail: drop terminal escapes (which would fight the\n * scroll region) and stray control chars (which would move the cursor), leaving plain text the renderer\n * styles itself. Order matters - OSC is matched before the bare-control sweep can eat its leading ESC.\n */\nexport const stripAnsi = (text: string): string => {\n return text.replace(OSC_ESCAPE, '').replace(CSI_ESCAPE, '').replace(CONTROL_CHARS, '')\n}\n\n/** Turbo's per-task line prefix under `--ui=stream`: `<pkg>:dev:`. */\nconst TASK_PREFIX = /^([^\\s:]+):dev:[ \\t]?/\n\n/**\n * Per-task bookkeeping turbo emits before the framework speaks: the cache verdict\n * (`cache bypass, force executing <hash>`) and the echoed command (`$ pnpm exec vike dev`).\n * Neither is dev signal \u2014 both are already implied by the app appearing in the ready header.\n */\nconst isTaskChrome = (text: string): boolean => {\n return /^cache (?:bypass|hit|miss)/.test(text) || text.startsWith('$ ')\n}\n\n/**\n * One raw turbo line \u2192 the framework line to surface, or `null` to drop it.\n *\n * Turbo's run chrome (`\u2022 Packages in scope: \u2026`, `\u2022 Running dev in 1 packages`, `\u2022 Remote caching\n * disabled`, the closing task summary, pnpm's `ELIFECYCLE` teardown) carries no `<pkg>:dev:` prefix.\n * Requiring that prefix drops all of it under one rule instead of chasing a brittle denylist, and what\n * survives is exactly the framework's own stdout/stderr \u2014 including its errors.\n *\n * @example\n * parseTurboDevLine('website-ui:dev: ready in 384 ms') // => { pkg: 'website-ui', text: 'ready in 384 ms' }\n * parseTurboDevLine('\u2022 Remote caching disabled') // => null\n */\nexport const parseTurboDevLine = (raw: string): TurboDevLine | null => {\n const line = stripAnsi(raw).trimEnd()\n const match = TASK_PREFIX.exec(line)\n\n if (match == null) {\n return null\n }\n\n const text = line.slice(match[0].length)\n\n if (text.trim() === '' || isTaskChrome(text.trim())) {\n return null\n }\n\n return { pkg: match[1]!, text, level: turboLineLevel(text) }\n}\n\n/**\n * Cap for the newline-less carry buffer. A framework that renders progress with bare CR and never a LF\n * would otherwise grow `pending` without bound for the life of the dev session.\n */\nconst MAX_PENDING_CHARS = 64 * 1024\n\n/**\n * Split a piped stream into lines: tee every chunk verbatim to the log, and route each complete\n * framework line to `onLine`. A trailing partial line is flushed on `end`, so a framework that exits\n * without a final newline never swallows its last (often the most interesting) line.\n *\n * The `data` listener is attached unconditionally \u2014 NOT gated on `onLine`/`appendLog` being set. A piped\n * child whose stdout is never read blocks once the OS pipe buffer fills, so draining is the contract\n * here; the sinks are merely optional consumers of what we drain.\n */\nconst pumpLines = (stream: Readable | null, opts: Pick<UiDevOptions, 'appendLog' | 'onLine'>): void => {\n if (stream == null) {\n return\n }\n\n let pending = ''\n\n const emit = (raw: string): void => {\n const parsed = parseTurboDevLine(raw)\n\n if (parsed != null) opts.onLine?.(parsed)\n }\n\n stream.setEncoding('utf-8')\n stream.on('data', (chunk: string) => {\n opts.appendLog?.(chunk)\n pending += chunk\n\n const lines = pending.split('\\n')\n\n pending = lines.pop() ?? ''\n for (const raw of lines) {\n emit(raw)\n }\n\n if (pending.length > MAX_PENDING_CHARS) {\n emit(pending)\n pending = ''\n }\n })\n stream.on('end', () => {\n if (pending === '') return\n emit(pending)\n pending = ''\n })\n // A readable that emits `error` with no listener THROWS, taking the whole dev session down. The child's\n // lifecycle is already owned by `superviseChild`, so a read error (pty EIO, a pipe torn down mid-SIGKILL)\n // only needs recording, never escalation.\n stream.on('error', (err: Error) => {\n opts.appendLog?.(`[infra-kit] ui dev stream error: ${err.message}\\n`)\n })\n}\n\n/**\n * Default factory: spawn `pnpm exec turbo run dev --filter=<pkg> \u2026 --only` detached with piped stdio,\n * fan its output into the runner log + the renderer's tail, and reap the process group on `kill()`.\n *\n * Exact `--filter=<pkg>` (no `...`) selects only the UI packages, so an API app that also defines a\n * `dev` task is never picked up.\n */\nexport const defaultUiDevFactory: UiDevFactory = ({\n packageNames,\n cwd,\n concurrency,\n env,\n appendLog,\n onLine,\n onUnexpectedExit,\n}) => {\n const filters = packageNames.map((name) => {\n return `--filter=${name}`\n })\n const child = spawn(\n 'pnpm',\n [\n 'exec',\n 'turbo',\n 'run',\n 'dev',\n ...filters,\n `--concurrency=${concurrency}`,\n '--env-mode=loose',\n '--only',\n '--output-logs=new-only',\n '--no-update-notifier',\n '--ui=stream',\n ],\n { cwd, detached: true, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, ...env } },\n )\n\n pumpLines(child.stdout, { appendLog, onLine })\n pumpLines(child.stderr, { appendLog, onLine })\n\n return superviseChild(child, undefined, onUnexpectedExit)\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["Command", "process", "pathToFileURL", "path", "process", "makeLeaf", "command", "buildNode", "commands", "depth", "leftCount", "left", "right", "buildCmuxLayout", "constants", "process", "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", "reapSnapshot", "snapshot", "childrenOf", "siblings", "groups", "seen", "queue", "i", "child", "snapshotProcRows", "snapshotProcRowsAsync", "resolve", "error", "stdout", "isChildOf", "parentPid", "doomedGroupsOf", "ownPgid", "groupAlive", "signalGroups", "pgids", "signal", "killDescendantGroupsNow", "sleep", "ms", "waitForExit", "timeoutMs", "deadline", "alive", "superviseChild", "graceMs", "onUnexpectedExit", "killing", "exited", "sampler", "stopSampling", "sample", "next", "reportUnexpected", "detail", "code", "doomed", "stragglers", "late", "forced", "HANDLED_SIGNALS", "SIGNAL_NUMBERS", "constants", "FALLBACK_SIGNO", "exitCodeForSignal", "signal", "TEARDOWN_DEADLINE_MS", "defaultExit", "code", "process", "defaultRegister", "handler", "defaultSetTimer", "ms", "timer", "writeStderr", "message", "writeTeardownFailure", "error", "detail", "registerSignalShutdown", "onSignal", "exit", "register", "forceReap", "killDescendantGroupsNow", "teardownDeadlineMs", "setTimer", "describeStall", "fileReport", "firstSignal", "cancelDeadline", "handle", "report", "buildPaneCommands", "panes", "watch", "app", "targets", "paneTargetsByApp", "presetDef", "byApp", "key", "selectApiApps", "root", "include", "apps", "discoverApiApps", "logDevWorkspace", "ref", "logger", "registerShutdown", "registerSignalShutdown", "signal", "closeCmuxDevWorkspace", "runCmuxDevServer", "options", "findMonorepoRoot", "process", "normalizeAppInclude", "targetsByApp", "commands", "layout", "buildCmuxLayout", "title", "openCmuxDevWorkspace", "heartbeat", "process", "writeStderr", "message", "process", "formatFault", "event", "error", "kept", "detail", "defaultOnFault", "defaultFileFault", "defaultOnFatal", "reason", "killDescendantGroupsNow", "defaultRegister", "handler", "registerCrashBarrier", "onFault", "register", "isTerminalDead", "fileFault", "onFatal", "handle", "DEFAULT_RELEASE_SLUG", "DEV_CONTEXT_WIRE_VERSION", "loadDev", "slugifyHostLabel", "slugifyRelease", "chokidar", "exec", "execFileSync", "fs", "net", "os", "path", "process", "util", "execFile", "fs", "path", "promisify", "execFileAsync", "promisify", "execFile", "readPackageName", "dir", "parsed", "defaultDryRunner", "root", "packageName", "stdout", "names", "t", "p", "readPackageDirs", "packageNameByDir", "dirByName", "distDir", "getPackageDistDirs", "name", "buildClosureMap", "apps", "dryRunner", "dependentsByPackageDir", "closures", "app", "closure", "pkgName", "dependents", "packageDebounceKey", "packageDir", "selectPackageRestartTargets", "closureMap", "a", "NOT_LAUNCHED", "interpolateCloud", "template", "packageName", "env", "resolveFallback", "route", "cloudTargetOf", "cloudTemplate", "findDegradedRoutes", "input", "ui", "spec", "finding", "judgeRoute", "wanted", "running", "reasons", "intendedLocal", "fallback", "failure", "cloudTarget", "resolveProxyRoutes", "uis", "localOrigin", "a", "b", "source", "target", "describeDestination", "d", "to", "wasAttempted", "remedy", "formatPairingRefusal", "degraded", "lines", "owner", "AsyncLocalStorage", "storage", "runAttributed", "service", "fn", "enterAttribution", "currentService", "Buffer", "fs", "path", "process", "process", "TRANSIENT_CODES", "dead", "isTerminalDead", "streamName", "stream", "index", "installTerminalLiveness", "streams", "onDeath", "fired", "attached", "name", "listener", "raw", "error", "protoWrite", "process", "rawStdoutWrite", "chunk", "isTerminalDead", "panelStream", "process", "target", "property", "receiver", "chunk", "rawStdoutWrite", "value", "emptyStats", "safeSegment", "raw", "safe", "part", "resolveLogDir", "INFRA_KIT_SESSION_VAR", "session", "path", "getCacheRoot", "logFileName", "service", "GC_MAX_AGE_MS", "isPidAlive", "name", "pid", "error", "gcOldLogDirs", "devRoot", "now", "entries", "fs", "entry", "dir", "DEFAULT_MAX_LOG_BYTES", "MAX_LOG_BYTES_VAR", "resolveMaxLogBytes", "DevLogSink", "maxBytes", "link", "cached", "fd", "text", "meta", "stats", "line", "size", "Buffer", "process", "util", "CONSOLE_LEVELS", "installOutputIntercept", "sink", "fallbackService", "currentService", "live", "serviceFor", "capture", "text", "level", "originalConsole", "method", "args", "patchStream", "stream", "original", "pending", "flush", "chunk", "rest", "lines", "line", "callback", "arg", "restoreStdout", "restoreStderr", "DEFAULT_PREFIX_URL", "parsePortString", "raw", "n", "resolvePreferredPort", "appName", "env", "devConfig", "prefixedKey", "fromPrefixed", "parsePortString", "fromPort", "resolvePrefixUrl", "DEFAULT_PREFIX_URL", "findPortConflicts", "apps", "ports", "a", "duplicatePorts", "port", "index", "conflictingApps", "Logger", "fastify", "fs", "path", "process", "pathToFileURL", "parseYaml", "isAddressInUse", "error", "DEFAULT_LAMBDA_TIMEOUT_MS", "resolveLambdaTimeoutMs", "raw", "process", "isRequestLogEnabled", "ServerlessLocalRun", "serverConfig", "Logger", "fastify", "request", "reply", "serviceTag", "enterAttribution", "onRequestLog", "ms", "requestPath", "boundPort", "preferred", "address", "_request", "serverlessYmlPath", "fileContents", "data", "parseYaml", "p", "funcDef", "element", "http", "url", "urlAction", "validMethods", "method", "routeKey", "parts", "filepath", "handler", "controllerPath", "fileUrl", "pathToFileURL", "importHandler", "action", "runAttributed", "traceLogger", "controller", "handlerFn", "retVal", "responseBody", "requestBody", "queryParams", "pathParameters", "headers", "httpMethod", "path", "startTime", "timeoutMs", "datePart", "_error", "_result", "_messageOrObject", "spawn", "fs", "buildTurboWatchFilters", "depInclusive", "depClosure", "name", "defaultTurboWatchFactory", "cwd", "logFile", "onUnexpectedExit", "filters", "out", "child", "spawn", "superviseChild", "spawn", "process", "ERROR_VOCABULARY", "turboLineLevel", "text", "pattern", "OSC_ESCAPE", "CSI_ESCAPE", "CONTROL_CHARS", "stripAnsi", "TASK_PREFIX", "isTaskChrome", "parseTurboDevLine", "raw", "line", "match", "MAX_PENDING_CHARS", "pumpLines", "stream", "opts", "pending", "emit", "parsed", "chunk", "lines", "err", "defaultUiDevFactory", "packageNames", "cwd", "concurrency", "env", "appendLog", "onLine", "onUnexpectedExit", "filters", "name", "child", "spawn", "process", "superviseChild", "RUNNER_SERVICE", "TURBO_SERVICE", "WATCH_SERVICE", "homeShorten", "p", "home", "os", "packageList", "apps", "a", "execFn", "util", "exec", "launchScript", "script", "logFn", "stderr", "error", "err", "logSink", "appendRunnerLog", "text", "getFreePort", "resolve", "reject", "srv", "net", "address", "port", "isForeign", "outcome", "foreignFrom", "res", "describeForeign", "answer", "type", "what", "PROBE_TIMEOUT_MS", "VITE_PING_HEADERS", "probeUrl", "target", "resolveHop", "location", "base", "probeApi", "url", "signal", "probeUi", "next", "hop", "defaultHealthProbe", "HELPER_PACKAGES", "isBelowVersion", "version", "floor", "parse", "v", "part", "b", "i", "left", "right", "manifestDirs", "repoRoot", "dirs", "app", "declaresPackage", "dir", "name", "pkg", "field", "findHelperDir", "fromDir", "segments", "candidate", "parent", "isWorkspaceLinked", "helperDir", "real", "root", "safeRealpath", "assertFloorAt", "assertHelperVersionFloor", "declaredIn", "resolved", "found", "readAppRelease", "cwd", "branch", "execFileSync", "slug", "slugifyRelease", "DEFAULT_RELEASE_SLUG", "errorReason", "firstLine", "DevServerRunner", "_DevServerRunner", "DEFAULT_DEV_PROXY_PORT", "options", "runBuild", "turboWatchFactory", "defaultTurboWatchFactory", "uiDevFactory", "defaultUiDevFactory", "dryRunner", "renderer", "healthProbe", "proxy", "createPortlessDriver", "sink", "DevLogSink", "ownsTerminal", "installOutputIntercept", "currentService", "DevRenderer", "process", "findMonorepoRoot", "devConfig", "discoverApiApps", "getInfraKitConfig", "normalizeAppInclude", "appName", "resolvePreferredPort", "resolvePrefixUrl", "devPresets", "def", "available", "bootStart", "include", "watch", "uiApps", "apiAppsAll", "uiAppsAll", "wantedLocalPkgs", "presetProxy", "deriveTargetLabel", "formatPairingRefusal", "discoverUiApps", "resolvePreset", "apiNames", "t", "uiNames", "passesInclude", "watchDepsByApp", "wanted", "uis", "ui", "dev", "loadDev", "overrides", "routes", "route", "spec", "findDegradedRoutes", "reason", "INFRA_KIT_ENV_VAR", "rows", "bootLabel", "map", "filters", "names", "uiPortEnv", "caEnv", "readCaPath", "tagByPackage", "level", "tag", "detail", "explicitApps", "duplicatePorts", "conflictingApps", "findPortConflicts", "buildCmd", "buildError", "started", "bin", "formatPortlessCommand", "packageName", "appDir", "release", "label", "slugifyHostLabel", "server", "ServerlessLocalRun", "method", "reqPath", "status", "ms", "window", "boundPort", "alias", "fragment", "DEV_CONTEXT_WIRE_VERSION", "tmp", "work", "run", "r", "e", "f", "entry", "failedIdx", "resolveEndpointUrl", "targets", "idx", "outcomes", "restarted", "recovered", "previous", "probed", "healthy", "allHealthy", "buildClosureMap", "engine", "consequence", "kind", "appDistDirs", "getAppDistDirs", "packageDistDirs", "getPackageDistDirs", "allDistDirs", "usePoll", "watcher", "chokidar", "filePath", "change", "classifyDistChange", "selectPackageRestartTargets", "packageDebounceKey", "key", "existing", "timer", "fresh", "threshold", "wasDown", "why", "intervalMs", "summary", "now", "byTag", "ref", "endpoint", "errors", "running", "d", "cutoff", "times", "at", "elapsedMs", "endpoints", "originByPkg", "resolveProxyRoutes", "uiEndpoints", "uiRefs", "assigned", "appCount", "pkgCount", "selectDevUi", "PersistentInkDevUi", "createSafeStream", "panelStream", "runner", "FATAL_EXIT_CODE", "FATAL_TEARDOWN_DEADLINE_MS", "toPresetDef", "targets", "key", "explainTargetKey", "splitList", "value", "parts", "s", "toDevServerOptions", "raw", "resolveSelfOptions", "options", "resolveSelfAppName", "process", "defaultFatalTimer", "handler", "ms", "timer", "createFatalHandler", "getRunner", "forceReap", "killDescendantGroupsNow", "exit", "code", "setTimer", "deadlineMs", "fired", "reason", "runner", "cancel", "runDevServer", "rawOptions", "isCmuxAvailable", "runCmuxDevServer", "onFatal", "liveness", "installTerminalLiveness", "stream", "error", "run", "boundRunner", "registerCrashBarrier", "event", "formatFault", "registerSignalShutdown", "signal", "rawStdoutWrite", "text", "shouldRunWizard", "tty", "json", "wizardToOptions", "result", "runDevServerCli", "runDevWizard", "warmed", "isPromptCancellation", "parseAndRun", "argv", "program", "Command", "pathToFileURL"]
7
7
  }