infra-kit 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boot-EEEVHAMO.js +2 -0
- package/dist/boot-EEEVHAMO.js.map +7 -0
- package/dist/{chunk-V4XK2YEH.js → chunk-7F5YKMWK.js} +2 -2
- package/dist/chunk-7F5YKMWK.js.map +7 -0
- package/dist/chunk-AJW5M44V.js +231 -0
- package/dist/chunk-AJW5M44V.js.map +7 -0
- package/dist/{chunk-JPTSUMWI.js → chunk-FTCM2766.js} +2 -2
- package/dist/{chunk-JPTSUMWI.js.map → chunk-FTCM2766.js.map} +1 -1
- package/dist/chunk-IPF7ILCB.js +7 -0
- package/dist/{chunk-KAKTWDWJ.js.map → chunk-IPF7ILCB.js.map} +4 -4
- package/dist/{chunk-AVLZBOOA.js → chunk-KHEUED4B.js} +1 -1
- package/dist/chunk-KHEUED4B.js.map +7 -0
- package/dist/chunk-NLOLELZ5.js +6 -0
- package/dist/chunk-NLOLELZ5.js.map +7 -0
- package/dist/cli.js +9 -9
- package/dist/cli.js.map +3 -3
- package/dist/dev-server.js +39 -25
- package/dist/dev-server.js.map +4 -4
- package/dist/dev-wizard-run-5NOQIMR5.js +2 -0
- package/dist/dev-wizard-run-5NOQIMR5.js.map +7 -0
- package/dist/mcp.js +1 -1
- package/dist/mcp.js.map +3 -3
- package/dist/persistent-ink-dev-ui-TLUJJC2D.js +2 -0
- package/dist/persistent-ink-dev-ui-TLUJJC2D.js.map +7 -0
- package/dist/update-check.js +1 -1
- package/package.json +1 -1
- package/dist/boot-YQAPYAKT.js +0 -2
- package/dist/boot-YQAPYAKT.js.map +0 -7
- package/dist/chunk-AVLZBOOA.js.map +0 -7
- package/dist/chunk-KAKTWDWJ.js +0 -7
- package/dist/chunk-TI33V5G2.js +0 -6
- package/dist/chunk-TI33V5G2.js.map +0 -7
- package/dist/chunk-TL7LO2FF.js +0 -231
- package/dist/chunk-TL7LO2FF.js.map +0 -7
- package/dist/chunk-V4XK2YEH.js.map +0 -7
- package/dist/dev-wizard-run-2ISRQPGA.js +0 -2
- package/dist/dev-wizard-run-2ISRQPGA.js.map +0 -7
- package/dist/persistent-ink-dev-ui-KOI44ZVT.js +0 -2
- package/dist/persistent-ink-dev-ui-KOI44ZVT.js.map +0 -7
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/integrations/cmux/open-dev-workspace.ts", "../src/integrations/cmux/canonicalize-cmux-title.ts", "../src/integrations/cmux/close-workspace-by-title.ts", "../src/integrations/cmux/list-workspace-titles.ts", "../src/integrations/cmux/open-workspace-with-layout.ts", "../src/integrations/cmux/workspace-title.ts", "../src/lib/errors/is-prompt-cancellation.ts", "../src/dev/proxy/portless-driver.ts"],
|
|
4
|
-
"sourcesContent": ["import process from 'node:process'\nimport { $ } from 'zx'\n\nimport type { CmuxLayoutNode } from 'src/dev/cmux-layout'\nimport { logger } from 'src/lib/logger'\n\n/** Args for {@link openCmuxDevWorkspace}: the workspace root, title, and pane layout tree. */\ninterface OpenCmuxDevWorkspaceArgs {\n cwd: string\n title: string\n layout: CmuxLayoutNode\n}\n\n/**\n * True iff the `cmux` CLI is invokable (i.e. `cmux --version` resolves). Used to\n * gate `--cmux` mode and fall back to single-process dev when cmux is absent.\n */\nexport const isCmuxAvailable = async (): Promise<boolean> => {\n try {\n await $`cmux --version`.quiet()\n\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Open ONE cmux workspace rooted at `cwd`, laid out per `layout` (one pane per\n * command). Runs in a `CMUX_QUIET=1` scoped env to suppress cmux's one-time compat\n * notice, then parses and returns the `workspace:<id>` ref from stdout.\n */\nexport const openCmuxDevWorkspace = async (args: OpenCmuxDevWorkspaceArgs): Promise<string> => {\n const { cwd, title, layout } = args\n const layoutJson = JSON.stringify(layout)\n\n const $cmux = $({ env: { ...process.env, CMUX_QUIET: '1' } })\n const output = (await $cmux`cmux new-workspace --name ${title} --cwd ${cwd} --focus false --layout ${layoutJson}`)\n .stdout\n\n return parseWorkspaceRef(output)\n}\n\n/**\n * Best-effort close of the cmux workspace `ref`, tearing down the workspace and\n * every pane process. Silently no-ops (debug-logged) if cmux isn't running or the\n * close fails, mirroring {@link file://./close-workspace-by-title.ts}.\n */\nexport const closeCmuxDevWorkspace = async (ref: string): Promise<void> => {\n try {\n await $`cmux close-workspace --workspace ${ref}`.quiet()\n } catch (error) {\n logger.debug({ error, ref }, 'cmux: skipped closing dev workspace')\n }\n}\n\n/**\n * Extract the `workspace:<id>` ref from `cmux new-workspace` output (e.g.\n * `OK workspace:5`). Throws a clear error when no ref is present.\n *\n * @example\n * parseWorkspaceRef('OK workspace:5\\n') // => 'workspace:5'\n */\nconst parseWorkspaceRef = (output: string): string => {\n const match = output.match(/workspace:\\d+/)\n\n if (!match) {\n throw new Error('cmux: could not locate workspace ref in new-workspace output')\n }\n\n return match[0]\n}\n", "/** Matches a `v`-prefixed semver token (e.g. `v1.48.0`) anchored on shape. */\nconst V_SEMVER_TOKEN_RE = /\\bv(\\d+\\.\\d+\\.\\d+)\\b/g\n\n/**\n * Canonicalizes a cmux workspace title into a stable dedup/close key.\n *\n * cmux workspace titles are human display strings built by\n * `buildCmuxWorkspaceTitle`, so the value stored when a workspace is created can\n * drift from the value rebuilt later \u2014 across whitespace and across CLI versions\n * (an older build titled version releases `v1.48.0`; the current build titles\n * them `1.48.0`). Keying dedup or close on the raw title silently creates\n * duplicate / unclosable workspaces whenever that drift occurs.\n *\n * Canonicalization collapses the known drift axes so both sides round-trip to an\n * equal key:\n * - trims and collapses internal whitespace to single spaces;\n * - normalizes a `v`-prefixed semver token to its bare form\n * (`v1.48.0` \u2192 `1.48.0`), anchored on semver shape so named releases that\n * merely start with `v` (e.g. `vega-redesign`) are left untouched.\n *\n * Non-release fallback titles (which may contain `/`, e.g. `feature/foo`) are\n * preserved as-is apart from whitespace normalization.\n *\n * @example\n * canonicalizeCmuxTitle('hulyo-monorepo v1.48.0') // => 'hulyo-monorepo 1.48.0'\n * canonicalizeCmuxTitle('hulyo-monorepo 1.48.0') // => 'hulyo-monorepo 1.48.0'\n * canonicalizeCmuxTitle('hulyo-monorepo vega-redesign') // => 'hulyo-monorepo vega-redesign'\n */\nexport const canonicalizeCmuxTitle = (raw: string): string => {\n return raw.trim().replace(/\\s+/g, ' ').replace(V_SEMVER_TOKEN_RE, '$1')\n}\n", "import { $ } from 'zx'\n\nimport { logger } from 'src/lib/logger'\n\nimport { canonicalizeCmuxTitle } from './canonicalize-cmux-title'\n\n/**\n * Best-effort close of the cmux workspace whose title matches `title` (compared\n * via {@link canonicalizeCmuxTitle}, so a drifted stored title still resolves).\n * Silently no-ops if cmux isn't running, the workspace isn't found, or close fails.\n */\nexport const closeCmuxWorkspaceByTitle = async (title: string): Promise<void> => {\n try {\n const listOutput = (await $`cmux list-workspaces`.quiet()).stdout\n\n const ref = findWorkspaceRefByTitle(listOutput, title)\n\n if (!ref) {\n return\n }\n\n await $`cmux close-workspace --workspace ${ref}`.quiet()\n } catch (error) {\n logger.debug({ error, title }, 'cmux: skipped closing workspace')\n }\n}\n\n/**\n * Parses `cmux list-workspaces` output and returns the workspace ref whose\n * title matches `title`, or undefined if no match. Both sides are compared via\n * {@link canonicalizeCmuxTitle} so a workspace stored under a drifted title\n * (whitespace, or an older CLI's `v`-prefixed semver) is still found \u2014 keeping\n * close symmetric with the cmux open dedup in `worktrees-reload`.\n *\n * Each line looks like:\n * \" workspace:8 hulyo-monorepo 1.48.0\"\n * \"* workspace:6 obsidian-workspace [selected]\"\n */\nconst findWorkspaceRefByTitle = (output: string, title: string): string | undefined => {\n const target = canonicalizeCmuxTitle(title)\n\n for (const rawLine of output.split('\\n')) {\n // eslint-disable-next-line sonarjs/super-linear-regex, regexp/no-super-linear-backtracking\n const match = rawLine.match(/^[* ]\\s*(workspace:\\d+)\\s+(.+?)(?:\\s+\\[selected\\])?\\s*$/)\n\n if (!match) {\n continue\n }\n\n const ref = match[1]\n const lineTitle = match[2]?.trim() ?? ''\n\n if (canonicalizeCmuxTitle(lineTitle) === target) {\n return ref\n }\n }\n\n return undefined\n}\n", "import { $ } from 'zx'\n\nimport { logger } from 'src/lib/logger'\n\nimport { canonicalizeCmuxTitle } from './canonicalize-cmux-title'\n\n/**\n * Returns the set of **canonical** titles for all currently-open cmux\n * workspaces (see {@link canonicalizeCmuxTitle}). Keying on the canonical form\n * lets callers match a workspace even when its stored title drifted from the\n * title they rebuild (whitespace, or an older CLI's `v`-prefixed semver).\n * Returns an empty set if cmux isn't running, the call fails, or the output\n * can't be parsed \u2014 callers should treat \"empty\" as \"unknown, proceed as if\n * nothing is open\".\n *\n * Each line of `cmux list-workspaces` looks like:\n * \" workspace:8 hulyo-monorepo 1.48.0\"\n * \"* workspace:6 obsidian-workspace [selected]\"\n */\nexport const listCmuxWorkspaceTitles = async (): Promise<Set<string>> => {\n try {\n const output = (await $`cmux list-workspaces`.quiet()).stdout\n\n const titles = new Set<string>()\n\n for (const rawLine of output.split('\\n')) {\n // eslint-disable-next-line sonarjs/super-linear-regex, regexp/no-super-linear-backtracking\n const match = rawLine.match(/^[* ]\\s*workspace:\\d+\\s+(.+?)(?:\\s+\\[selected\\])?\\s*$/)\n\n if (!match) {\n continue\n }\n\n const title = match[1]?.trim()\n\n if (title) {\n titles.add(canonicalizeCmuxTitle(title))\n }\n }\n\n return titles\n } catch (error) {\n logger.debug({ error }, 'cmux: skipped listing workspace titles')\n\n return new Set()\n }\n}\n", "import { $ } from 'zx'\n\nimport { getInfraKitConfig, resolveCmuxLayout } from 'src/lib/infra-kit-config'\n\ninterface OpenCmuxWorkspaceArgs {\n cwd: string\n title?: string\n}\n\n/**\n * Opens a new cmux workspace rooted at `cwd`, with panes arranged per the\n * configured `worktrees.cmux.layout` (resolved via {@link resolveCmuxLayout},\n * default `two-columns`):\n * two-columns \u2014 left | right, both full-height (two panes)\n * three-pane \u2014 left-top / left-bottom | full-height right (three panes)\n * All panes inherit `cwd` from the workspace.\n */\nexport const openCmuxWorkspaceWithLayout = async (args: OpenCmuxWorkspaceArgs): Promise<void> => {\n const { cwd, title } = args\n\n const layout = resolveCmuxLayout(await getInfraKitConfig())\n\n const newWorkspaceOutput = (await $`cmux workspace create --cwd ${cwd}`).stdout\n\n const workspaceRef = parseWorkspaceRef(newWorkspaceOutput)\n\n const surfacesOutput = (await $`cmux list-pane-surfaces --workspace ${workspaceRef}`).stdout\n\n const leftTopRef = parseFirstSurfaceRef(surfacesOutput)\n\n // Both layouts share the vertical split into left | right columns; only the\n // legacy three-pane layout additionally splits the left column top/bottom.\n await $`cmux new-split right --workspace ${workspaceRef} --surface ${leftTopRef}`\n\n if (layout === 'three-pane') {\n await $`cmux new-split down --workspace ${workspaceRef} --surface ${leftTopRef}`\n }\n\n if (title) {\n await $`cmux workspace rename --workspace ${workspaceRef} --title ${title}`\n }\n}\n\n/**\n * Extracts the first `surface:<id>` reference from the output of\n * `cmux list-pane-surfaces`. Used to locate the initial (primary) pane\n * surface so subsequent splits can be anchored relative to it.\n *\n * @example\n * const output = 'surface:12 (active)\\nsurface:13\\n'\n * parseFirstSurfaceRef(output) // => 'surface:12'\n */\nconst parseFirstSurfaceRef = (output: string): string => {\n const match = output.match(/surface:\\d+/)\n\n if (!match) {\n throw new Error('cmux: could not locate initial surface in list-pane-surfaces output')\n }\n\n return match[0]\n}\n\n/**\n * Extracts the `workspace:<id>` reference from the output of\n * `cmux workspace create`. The returned ref is used to target the newly\n * created workspace in follow-up `cmux` commands (splits, rename, etc.).\n *\n * @example\n * const output = 'created workspace:7\\n'\n * parseWorkspaceRef(output) // => 'workspace:7'\n */\nconst parseWorkspaceRef = (output: string): string => {\n const match = output.match(/workspace:\\d+/)\n\n if (!match) {\n throw new Error('cmux: could not locate workspace ref in workspace create output')\n }\n\n return match[0]\n}\n", "import { displayLabel, parseBranchName } from 'src/lib/release-id'\n\ninterface BuildCmuxWorkspaceTitleArgs {\n repoName: string\n branch: string\n}\n\n/**\n * Builds the cmux workspace title used by `worktrees-add` and looked up by\n * `worktrees-remove`. Release branches are rendered via their release-id\n * display label so the title reads e.g. `\"hulyo-monorepo 1.48.0\"` for\n * `\"release/v1.48.0\"` and `\"hulyo-monorepo checkout-redesign\"` for\n * `\"release/checkout-redesign\"`. Non-release branches (cmux titles them too)\n * fall back to the raw branch string.\n */\nexport const buildCmuxWorkspaceTitle = (args: BuildCmuxWorkspaceTitleArgs): string => {\n const { repoName, branch } = args\n\n const id = parseBranchName(branch)\n const label = id ? displayLabel(id) : branch\n\n return `${repoName} ${label}`\n}\n", "/**\n * Names of the error classes thrown when an interactive prompt ends without a\n * value. From `@inquirer/core`: `ExitPromptError` (user pressed Ctrl-C / Esc) and\n * `AbortPromptError` (the prompt was aborted via an `AbortSignal`). From our own\n * Ink pickers: `PromptCancelledError` (see ./prompt-cancelled-error), which is\n * registered here rather than impersonating an inquirer class name. All are\n * intentional cancellations, not failures.\n */\nconst CANCELLATION_ERROR_NAMES = new Set(['ExitPromptError', 'AbortPromptError', 'PromptCancelledError'])\n\nconst hasCancellationName = (value: unknown): boolean => {\n return value instanceof Error && CANCELLATION_ERROR_NAMES.has(value.name)\n}\n\n/**\n * True when `error` represents a user (or signal) cancellation of an\n * `@inquirer/*` prompt \u2014 i.e. pressing Ctrl-C / Esc in the branch picker or a\n * confirm step. Matched by `name` rather than `instanceof` so it stays correct\n * even when pnpm dedupes more than one copy of `@inquirer/core` into the tree\n * (an `instanceof` check fails across realms/duplicate classes).\n *\n * Also unwraps one level of `cause`, so a cancellation re-wrapped in an\n * {@link ./operation-error.OperationError} is still recognised at the top-level\n * error boundary.\n *\n * @example\n * try {\n * await checkbox({ message: 'Select release branches', choices })\n * } catch (err) {\n * if (isPromptCancellation(err)) process.exit(0) // clean back-out, not an error\n * throw err\n * }\n */\nexport const isPromptCancellation = (error: unknown): boolean => {\n if (hasCancellationName(error)) return true\n\n const cause = (error as { cause?: unknown } | null | undefined)?.cause\n\n return hasCancellationName(cause)\n}\n", "/**\n * Thin, injectable driver for the `portless` daemon (Layer B \u2014 see `.omc/plans/dev-https-portless.md`).\n *\n * `infra-kit dev` uses it to register `<release>.<package>.localhost \u2192 127.0.0.1:<port>` routes so the\n * hero URLs resolve over **HTTPS on :443, with no port in the URL**. Every call here is **time-bounded and\n * never throws**: a missing binary, a non-zero exit, or a wedged process resolves to `false`/no-op. That is\n * a reporting contract, not a tolerance one \u2014 portless IS a hard dependency of the dev loop, and\n * `DevServerRunner.ensureProxy` turns a `false` from this driver into a fatal, actionable start error.\n *\n * The binary is NOT resolved from `PATH`: `portless` is a normal npm dependency living in\n * `node_modules/.bin`, which is on `PATH` only when the process was launched via pnpm/npm. Since\n * `infra-kit dev` is often launched otherwise (a global bin, a cmux runner, a foreign cwd), we resolve\n * portless's own `dist/cli.js` by walking `node_modules` from this file (see {@link resolvePortlessBin})\n * and run it with the current `node` (`process.execPath`) \u2014 so it works regardless of how `dev` was\n * invoked. Args are fixed literals plus discovered release/package names + a numeric port, never\n * shell-interpolated.\n *\n * **The daemon is PROBED, never started.** `:443` is privileged, and portless binds it by re-execing\n * itself through `sudo` with an inherited stdio \u2014 which a detached `stdio:'ignore'` child can never\n * satisfy: the password prompt has nowhere to go. Setup is one-time and out-of-band: a root\n * `portless service install`, printed for the user by {@link formatPortlessCommand} (never as a bare\n * `portless`, which no shell can resolve \u2014 see there).\n *\n * All process I/O is injected (`run` for awaited commands, `isProxyServing` for the wire probe) so tests\n * never shell out and can assert the exact portless argv.\n */\nimport type { Buffer } from 'node:buffer'\nimport { execFile } from 'node:child_process'\nimport { createHash } from 'node:crypto'\nimport { existsSync, readFileSync } from 'node:fs'\nimport http from 'node:http'\nimport https from 'node:https'\nimport net from 'node:net'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\nimport process from 'node:process'\nimport tls from 'node:tls'\nimport { fileURLToPath } from 'node:url'\nimport { promisify } from 'node:util'\n\nimport { withoutPackageManagerEnv } from 'src/lib/pm-env'\n\nconst execFileAsync = promisify(execFile)\n\n/** Read `bin.portless` (the `dist/cli.js` relative path) from a portless `package.json` on disk. */\nconst readBinRel = (pkgJsonPath: string): string | null => {\n const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8')) as { bin?: string | Record<string, string> }\n const rel = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.portless\n\n return rel == null || rel === '' ? null : rel\n}\n\n/**\n * Resolve the absolute path to portless's CLI entry (`portless/dist/cli.js`) from node_modules,\n * independent of `PATH`. Returns `null` when portless is not installed, degrading the whole driver to\n * a no-op.\n *\n * portless is ESM-only (its `.` export exposes only `import`/`types`, no `require`), so\n * `createRequire().resolve` can't see it. We instead walk `node_modules` upward from this file \u2014 the\n * standard resolution path \u2014 and read the package's `package.json` straight off disk, which bypasses the\n * exports map that would otherwise hide both `package.json` and the main entry.\n */\nexport const resolvePortlessBin = (): string | null => {\n try {\n let dir = dirname(fileURLToPath(import.meta.url))\n\n for (;;) {\n const pkgJsonPath = join(dir, 'node_modules', 'portless', 'package.json')\n\n if (existsSync(pkgJsonPath)) {\n const rel = readBinRel(pkgJsonPath)\n\n return rel == null ? null : join(dirname(pkgJsonPath), rel)\n }\n const parent = dirname(dir)\n\n if (parent === dir) return null\n dir = parent\n }\n } catch {\n return null\n }\n}\n\n/** Resolve portless's CLI once per process \u2014 the on-disk location never changes within a run. */\nlet cachedBin: string | null | undefined\nconst portlessBin = (): string | null => {\n if (cachedBin === undefined) cachedBin = resolvePortlessBin()\n\n return cachedBin\n}\n\n/**\n * Characters that survive a POSIX shell unquoted. Anything outside this set (a space, a paren \u2014 both of\n * which appear in real install paths like `/Applications/My Editor.app`) gets single-quoted.\n */\nconst SHELL_SAFE = /^[\\w@%+=:,./-]+$/\n\n/** A literal `'` inside single quotes: close, emit an escaped quote, reopen \u2014 the only way a shell allows it. */\nconst SINGLE_QUOTE_ESCAPE = \"'\\\\''\"\n\n/** Single-quote `value` for a POSIX shell unless it is already inert. */\nconst shellQuote = (value: string): string => {\n return SHELL_SAFE.test(value) ? value : `'${value.replaceAll(\"'\", SINGLE_QUOTE_ESCAPE)}'`\n}\n\n/** Seams for {@link formatPortlessCommand}, injected so tests never depend on the real node_modules layout. */\nexport interface FormatPortlessCommandOptions {\n /**\n * Absolute path to portless's `dist/cli.js`. **Required and non-nullable on purpose.** The caller must\n * have resolved portless before it can describe how to run it, so \"I could not find the binary\" cannot be\n * silently rendered as a plausible-looking command \u2014 the type makes that unwritable rather than merely\n * discouraged. A `null` bin is a different report (\"run `pnpm install`\"), which every caller makes first.\n */\n bin: string\n /** Prefix with `sudo` \u2014 only `service install`, which binds the privileged `:443`, needs it. */\n sudo?: boolean\n execPath?: string\n}\n\n/**\n * Render a portless command the user can actually paste into a shell.\n *\n * This exists because the obvious string is a lie. `portless` is a plain npm dependency living in\n * `node_modules/.bin`, which is on `PATH` only inside a pnpm/npm script \u2014 so printing `sudo portless service\n * install` hands the user a command that dies with `sudo: portless: command not found`. `sudo` makes it\n * strictly worse: it replaces `PATH` with `secure_path`, so even a shell that *could* resolve `portless`\n * loses it the moment the command is elevated.\n *\n * We therefore print what the driver itself runs (see {@link defaultRun}): the current interpreter, by\n * absolute path, invoking portless's `dist/cli.js`, by absolute path. Nothing is resolved from `PATH`, so the\n * command works under `sudo`, from any cwd, and however `infra-kit` was launched.\n *\n * This is not merely cosmetic. portless's `service install` writes **the interpreter and script path it was\n * invoked with** straight into the launchd plist's `ProgramArguments` (`nodePath: process.execPath` plus\n * `process.argv[1]`), so the command printed here is the command that gets installed as a **root system\n * daemon**. Printing a name for the shell to resolve would not just fail \u2014 it would decide what runs as root.\n *\n * @example\n * formatPortlessCommand(['service', 'install'], { sudo: true, bin })\n * // 'sudo /usr/local/bin/node /repo/node_modules/portless/dist/cli.js service install'\n */\nexport const formatPortlessCommand = (args: string[], options: FormatPortlessCommandOptions): string => {\n const words = [options.execPath ?? process.execPath, options.bin, ...args]\n const prefix = options.sudo === true ? 'sudo ' : ''\n\n return prefix + words.map(shellQuote).join(' ')\n}\n\n/** Awaited portless invocation. Rejects on non-zero exit / timeout; the driver swallows that into a no-op. */\nexport type PortlessRun = (args: string[], opts: { timeoutMs: number }) => Promise<void>\n\n/** Cheap \"is anything at all accepting TCP here?\" pre-filter in front of the wire probe. */\nexport type IsListening = (port: number) => Promise<boolean>\n\n/**\n * Ground-truth identity: is the process serving `port` actually **portless**, and (when `tls`) is it\n * serving **TLS**? See {@link defaultIsProxyServing} for why this cannot be answered from state files.\n */\nexport type IsProxyServing = (port: number, tls: boolean) => Promise<boolean>\n\nconst DEFAULT_TIMEOUT_MS = 1500\nconst PROBE_TIMEOUT_MS = 1500\n\n/** Response header portless sets on every response it serves. Node lower-cases response header names. */\nconst PORTLESS_HEADER = 'x-portless'\n\n/** IPv4 loopback: portless binds and dials `127.0.0.1`. */\nconst LOOPBACK = '127.0.0.1'\n\n/**\n * SNI for the probe. Node sends **no SNI to an IP literal** (RFC 6066), which would drop portless onto its\n * default certificate \u2014 whose SANs are `localhost`, `*.localhost`, `*.local` and contain **no IP entry**.\n * `localhost` is always in that set, so it is the one name guaranteed to work even on a machine with zero\n * aliases registered.\n */\nconst PROBE_SERVERNAME = 'localhost'\n\nexport const defaultIsListening: IsListening = (port) => {\n return new Promise((resolve) => {\n const socket = net.connect({ host: LOOPBACK, port })\n const finish = (result: boolean): void => {\n socket.destroy()\n resolve(result)\n }\n\n socket.setTimeout(PROBE_TIMEOUT_MS)\n socket.once('connect', () => {\n finish(true)\n })\n socket.once('timeout', () => {\n finish(false)\n })\n socket.once('error', () => {\n finish(false)\n })\n })\n}\n\n/**\n * Is the listener on `port` portless itself, serving `tls`? Proven **on the wire**, by asking it: portless\n * sets `X-Portless: 1` on every response, before route lookup \u2014 so an unrouted host still answers the probe\n * (a 404 with the header is a pass). This mirrors portless's own `isProxyRunning`.\n *\n * This replaces the old state-file check (`proxy.port` + `proxy.pid`), which was **unsound**: portless's\n * `resolveStateDir(_port)` ignores its port argument, so `proxy.port` / `proxy.pid` / `proxy.tls` are\n * process-global singletons shared by every daemon on every port. Starting ANY daemon rewrites them, and\n * stopping ANY daemon DELETES them \u2014 so a second, unrelated daemon (or a stale sibling repo still on the\n * old CLI, falling back to an unprivileged port) makes a perfectly healthy `:443` daemon look dead. Both\n * were reproduced against portless 0.15.1; see `.omc/research/portless-https-spike.md`.\n *\n * `rejectUnauthorized: false` is deliberate and load-bearing: this probe answers *\"is portless serving\n * here?\"*, **never** *\"is its CA trusted?\"*. Validating the chain here would collapse two different\n * failures \u2014 a daemon that is down, and a CA that was never trusted \u2014 into one indistinguishable error,\n * with two different fixes (a root `service install` vs the sudo-free `trust`). Trust is a separate,\n * explicitly-validating probe (doctor's CA check).\n */\nexport const defaultIsProxyServing: IsProxyServing = (port, tls) => {\n return new Promise((resolve) => {\n const request = tls ? https.request : http.request\n const req = request(\n {\n host: LOOPBACK,\n port,\n method: 'HEAD',\n path: '/',\n timeout: PROBE_TIMEOUT_MS,\n ...(tls ? { rejectUnauthorized: false, servername: PROBE_SERVERNAME } : {}),\n },\n (res) => {\n res.resume()\n resolve(res.headers[PORTLESS_HEADER] === '1')\n },\n )\n\n req.on('error', () => {\n resolve(false)\n })\n req.on('timeout', () => {\n req.destroy()\n resolve(false)\n })\n req.end()\n })\n}\n\nconst defaultRun: PortlessRun = async (args, { timeoutMs }) => {\n const bin = portlessBin()\n\n if (bin == null) throw new Error('portless is not installed (not resolvable from node_modules)')\n await execFileAsync(process.execPath, [bin, ...args], {\n signal: AbortSignal.timeout(timeoutMs),\n encoding: 'utf-8',\n env: withoutPackageManagerEnv(process.env),\n })\n}\n\n/**\n * portless's state directory. Exported so `doctor` reports on the same directory the driver reads.\n *\n * The default is deliberately **unchanged** (`~/.portless`): portless's `service install` bakes\n * `PORTLESS_STATE_DIR`, resolved from `SUDO_USER`, into the launchd plist \u2014 so the root daemon reads the\n * *invoking user's* home. Pointing this anywhere else by default would manufacture the very split it looks\n * like it prevents.\n */\nexport const portlessStateDir = (): string => {\n return process.env.PORTLESS_STATE_DIR ?? join(homedir(), '.portless')\n}\n\n/** portless's local CA certificate \u2014 the root every host cert it mints is signed by. */\nconst CA_CERT_FILE = 'ca.pem'\n\n/**\n * Marker portless's `trust` writes: the **hex sha256 of `ca.pem`'s bytes** that was added to the login\n * keychain (`writeTrustMarker` \u2192 `caFingerprint`, `cli.js:78-101`). It records WHICH CA was trusted, so a\n * regenerated CA leaves a marker that no longer matches.\n */\nconst CA_TRUST_MARKER_FILE = 'ca.trusted'\n\n/** A route portless is serving: `<name> \u2192 127.0.0.1:<port>`. */\nexport interface PortlessRoute {\n /**\n * The registered hostname (e.g. `2-4.client-api.localhost`). Usable verbatim as a\n * `portless alias --remove <name>` argument \u2014 portless strips a trailing TLD off the name it is handed\n * (`parseHostnames`, `chunk-SD2PIWJU.js:68-79`) \u2014 and as a TLS `servername`.\n */\n name: string\n port: number\n}\n\n/** Absolute path to portless's local CA certificate, in whichever state dir {@link portlessStateDir} names. */\nexport const readCaPath = (): string => {\n return join(portlessStateDir(), CA_CERT_FILE)\n}\n\n/**\n * Was `portless trust` run for the CA that is on disk right now? Compares `sha256(ca.pem)` against the\n * fingerprint recorded in `ca.trusted`. `false` when either file is missing; never throws.\n *\n * **This proves the marker was written for THIS fingerprint \u2014 not that the keychain still trusts it.** A\n * user who deletes the certificate from Keychain Access by hand leaves the marker behind and gets a false\n * pass here. That residual is accepted (reading the keychain would mean shelling out to `security` on a\n * check that must stay cheap); it is why this is a *separate* check from the chain handshake\n * ({@link handshakeChainsToCa}), which proves what the daemon actually serves.\n */\nexport const caFingerprintMatches = (): boolean => {\n try {\n const recorded = readFileSync(join(portlessStateDir(), CA_TRUST_MARKER_FILE), 'utf-8').trim()\n\n if (recorded === '') return false\n\n const actual = createHash('sha256').update(readFileSync(readCaPath())).digest('hex')\n\n return actual === recorded.toLowerCase()\n } catch {\n return false\n }\n}\n\n/** Outcome of {@link handshakeChainsToCa}: `code` is the Node TLS error code, which the caller discriminates on. */\nexport type HandshakeResult = { ok: true } | { ok: false; code: string }\n\n/**\n * Does the certificate served on `port` chain to the CA in `ca.pem`? A **validating** TLS handshake \u2014 the\n * complement of {@link defaultIsProxyServing}, which deliberately does not validate.\n *\n * `servername` is **mandatory and load-bearing**, never optional: Node sends no SNI to an IP literal\n * (RFC 6066), which drops portless onto its default certificate, whose SANs (`localhost`, `*.localhost`,\n * `*.local`) contain **no IP entry** \u2014 so a validating probe of `127.0.0.1` with no `servername` fails with\n * `ERR_TLS_CERT_ALTNAME_INVALID` against a perfectly healthy daemon. Any such code coming back from here is\n * therefore a bug in the CALLER's probe, never a finding about the user's trust store. Passing an\n * unregistered name is safe: portless's SNI callback mints a cert on demand for any servername.\n *\n * Time-bounded; never throws.\n */\nexport const handshakeChainsToCa = (port: number, servername: string): Promise<HandshakeResult> => {\n return new Promise((resolve) => {\n let ca: Buffer<ArrayBufferLike>\n\n try {\n ca = readFileSync(readCaPath())\n } catch {\n resolve({ ok: false, code: 'ENOENT' })\n\n return\n }\n\n const socket = tls.connect({ host: LOOPBACK, port, servername, ca: [ca], rejectUnauthorized: true }, () => {\n // With `rejectUnauthorized: true` a chain failure normally surfaces as an 'error' event and this\n // callback never runs; the check is here so a future Node that connects-then-reports can't slip a\n // rejected chain through as a pass.\n const authError = socket.authorizationError as NodeJS.ErrnoException | undefined\n const authorized = socket.authorized\n\n socket.destroy()\n resolve(authorized ? { ok: true } : { ok: false, code: authError?.code ?? authError?.message ?? 'UNKNOWN' })\n })\n\n socket.setTimeout(PROBE_TIMEOUT_MS)\n socket.once('timeout', () => {\n socket.destroy()\n resolve({ ok: false, code: 'ETIMEDOUT' })\n })\n socket.once('error', (err: NodeJS.ErrnoException) => {\n socket.destroy()\n resolve({ ok: false, code: err.code ?? 'UNKNOWN' })\n })\n })\n}\n\n/**\n * Routes portless currently has registered, read from `routes.json` in {@link portlessStateDir}. `[]` on any\n * failure (absent file, malformed JSON, unexpected shape) \u2014 an unreadable route list is reported as \"no\n * routes\", never as an error, because every caller uses this for diagnostics only.\n */\nexport const listRoutes = (): PortlessRoute[] => {\n try {\n const raw: unknown = JSON.parse(readFileSync(join(portlessStateDir(), 'routes.json'), 'utf-8'))\n\n if (!Array.isArray(raw)) return []\n\n return raw.flatMap((entry): PortlessRoute[] => {\n const { hostname, port } = (entry ?? {}) as { hostname?: unknown; port?: unknown }\n\n if (typeof hostname !== 'string' || hostname === '' || typeof port !== 'number') return []\n\n return [{ name: hostname, port }]\n })\n } catch {\n return []\n }\n}\n\nexport interface PortlessDriver {\n /**\n * Absolute path to the `dist/cli.js` this driver executes, or `null` when portless is not installed.\n *\n * Exposed so a caller rendering a remediation ({@link formatPortlessCommand}) names the binary THIS driver\n * would run, rather than re-resolving one behind its back \u2014 which, under an injected driver, would print a\n * fix derived from the real machine instead of the one under test.\n */\n binPath: () => string | null\n /** Resolve (and memoize) whether the `portless` binary is usable. Absent \u2192 every other call no-ops. */\n isAvailable: () => Promise<boolean>\n /**\n * Is a portless daemon serving `port` over `tls`? **Probe only \u2014 this never starts anything.** Binding\n * the privileged `:443` needs root, and portless's sudo re-exec cannot prompt from a detached child, so\n * the daemon is installed once, out-of-band (a root `service install`). A `false` here is turned into a\n * fatal, actionable start error by the caller.\n */\n isProxyServing: (port: number, tls: boolean) => Promise<boolean>\n /**\n * Register `<name> \u2192 127.0.0.1:<port>` (`name` = `<release>.<package>`). Returns `true` on success so\n * the caller shows the hero URL only for an alias that actually resolves (best-effort otherwise).\n */\n registerAlias: (name: string, port: number) => Promise<boolean>\n /** Deregister `<name>`. Best-effort. */\n removeAlias: (name: string) => Promise<void>\n}\n\nexport interface PortlessDriverDeps {\n /** Override the resolved `dist/cli.js` path (default: the real node_modules walk). Injected in tests. */\n bin?: string | null\n run?: PortlessRun\n /** TCP liveness pre-filter (default: real `net` connect). Injected in tests. */\n isListening?: IsListening\n /** Wire-probe identity check (default: real `HEAD /` + `X-Portless`). Injected in tests. */\n isProxyServing?: IsProxyServing\n timeoutMs?: number\n}\n\n/**\n * Build a {@link PortlessDriver}. Inject `run` in tests to assert argv without shelling out.\n * `isAvailable` memoizes so the binary is probed at most once per runner.\n */\nexport const createPortlessDriver = (deps: PortlessDriverDeps = {}): PortlessDriver => {\n const bin = deps.bin === undefined ? portlessBin() : deps.bin\n const run = deps.run ?? defaultRun\n const isListening = deps.isListening ?? defaultIsListening\n const isProxyServing = deps.isProxyServing ?? defaultIsProxyServing\n const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS\n let availability: boolean | null = null\n\n /** Run a portless subcommand, swallowing any failure into `false` (best-effort contract). */\n const tryRun = async (args: string[]): Promise<boolean> => {\n try {\n await run(args, { timeoutMs })\n\n return true\n } catch {\n return false\n }\n }\n\n const isAvailable = async (): Promise<boolean> => {\n availability ??= await tryRun(['--version'])\n\n return availability\n }\n\n const serving = async (port: number, tls: boolean): Promise<boolean> => {\n if (!(await isAvailable())) return false\n // Nothing is even accepting TCP \u2192 skip the (more expensive) wire probe entirely.\n if (!(await isListening(port))) return false\n\n return isProxyServing(port, tls)\n }\n\n const registerAlias = async (name: string, port: number): Promise<boolean> => {\n if (!(await isAvailable())) return false\n\n return tryRun(['alias', name, String(port)])\n }\n\n const removeAlias = async (name: string): Promise<void> => {\n if (!(await isAvailable())) return\n await tryRun(['alias', '--remove', name])\n }\n\n return {\n binPath: () => {\n return bin\n },\n isAvailable,\n isProxyServing: serving,\n registerAlias,\n removeAlias,\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["process", "$", "isCmuxAvailable", "$", "openCmuxDevWorkspace", "args", "cwd", "title", "layout", "layoutJson", "output", "process", "parseWorkspaceRef", "closeCmuxDevWorkspace", "ref", "error", "logger", "match", "V_SEMVER_TOKEN_RE", "canonicalizeCmuxTitle", "raw", "$", "closeCmuxWorkspaceByTitle", "title", "listOutput", "$", "ref", "findWorkspaceRefByTitle", "error", "logger", "output", "target", "canonicalizeCmuxTitle", "rawLine", "match", "lineTitle", "$", "listCmuxWorkspaceTitles", "output", "$", "titles", "rawLine", "match", "title", "canonicalizeCmuxTitle", "error", "logger", "$", "openCmuxWorkspaceWithLayout", "args", "cwd", "title", "layout", "resolveCmuxLayout", "getInfraKitConfig", "newWorkspaceOutput", "$", "workspaceRef", "parseWorkspaceRef", "surfacesOutput", "leftTopRef", "parseFirstSurfaceRef", "output", "match", "buildCmuxWorkspaceTitle", "args", "repoName", "branch", "id", "parseBranchName", "label", "displayLabel", "CANCELLATION_ERROR_NAMES", "hasCancellationName", "value", "isPromptCancellation", "error", "cause", "execFile", "createHash", "existsSync", "readFileSync", "http", "https", "net", "homedir", "dirname", "join", "process", "tls", "fileURLToPath", "promisify", "execFileAsync", "promisify", "execFile", "readBinRel", "pkgJsonPath", "pkg", "readFileSync", "rel", "resolvePortlessBin", "dir", "dirname", "fileURLToPath", "join", "existsSync", "parent", "cachedBin", "portlessBin", "SHELL_SAFE", "SINGLE_QUOTE_ESCAPE", "shellQuote", "value", "formatPortlessCommand", "args", "options", "words", "process", "DEFAULT_TIMEOUT_MS", "PROBE_TIMEOUT_MS", "PORTLESS_HEADER", "LOOPBACK", "PROBE_SERVERNAME", "defaultIsListening", "port", "resolve", "socket", "net", "finish", "result", "defaultIsProxyServing", "tls", "req", "https", "http", "res", "defaultRun", "timeoutMs", "bin", "withoutPackageManagerEnv", "portlessStateDir", "homedir", "CA_CERT_FILE", "CA_TRUST_MARKER_FILE", "readCaPath", "caFingerprintMatches", "recorded", "createHash", "handshakeChainsToCa", "servername", "ca", "authError", "authorized", "err", "listRoutes", "raw", "entry", "hostname", "createPortlessDriver", "deps", "run", "isListening", "isProxyServing", "availability", "tryRun", "isAvailable", "name"]
|
|
7
|
-
}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{f as z,i as l}from"./chunk-KAKTWDWJ.js";import{I as w,Q as b,v as d,w as x,x as y,y as k}from"./chunk-AVLZBOOA.js";import{e as W}from"./chunk-CHETBZ6M.js";import N from"@inquirer/checkbox";import $ from"@inquirer/confirm";import O,{Separator as j}from"@inquirer/select";import{loadDev as B}from"@slip-stream-kit/config/internal";import q from"node:path";import h from"node:process";var f=e=>e.split("/")[0],M=e=>new Map(e.apps.map(r=>[r.name,r])),R=(e,r)=>{let a={},s=!1;for(let t of e.backends){let n=t.localCapable&&t.ownerApp!=null&&r.has(t.ownerApp);n||(s=!0);for(let o of t.routes)a[o]=n?"local":"cloud"}return{overrides:a,anyCloud:s}},C=(e,r)=>{let a=M(r),s=e.targets.filter(i=>i.endsWith("/ui")),t=new Set(e.targets.filter(i=>i.endsWith("/api")).map(f)),n={},o=!1;for(let i of s){let u=a.get(f(i)),{overrides:c,anyCloud:m}=u?R(u,t):{overrides:{},anyCloud:!1};m&&(o=!0),n[i]=Object.keys(c).length>0?{proxy:c}:{}}for(let i of t)n[`${i}/api`]??={};return{presetDef:{apps:n,cmux:e.cmux},anyCloudRoute:o,targetKeys:Object.keys(n).sort()}},A=(e,r,a)=>{let s=M(a),t=[...new Set(e.targetKeys.map(f))].sort(),n=new Set(e.targetKeys),o=t.every(u=>{let c=s.get(u);if(!c)return!1;let m=!c.hasUi||n.has(`${u}/ui`),p=!c.hasApi||n.has(`${u}/api`);return m&&p}),i=[`--app=${t.join(",")}`];return r.watch&&i.push("--watch"),r.cmux&&i.push("--cmux"),{flags:i.join(" "),exact:o}};var g={output:h.stderr,clearPromptOnDone:!0},K={select:e=>O({message:e.message,choices:e.choices,default:e.default},g),checkbox:e=>N({message:e.message,choices:e.choices},g),confirm:e=>$({message:e.message,default:e.default},g)},U=(e,r)=>{let a=new Map;for(let[s,t]of Object.entries(e)){let n=a.get(t.packageName),o=t.from.includes("local");n?(n.routes.push(s),n.localCapable=n.localCapable||o):a.set(t.packageName,{packageName:t.packageName,routes:[s],localCapable:o,ownerApp:r.get(t.packageName)})}return[...a.values()]},_=async e=>{let r=y(e),a=k(e),s=await b(),t=new Map(r.map(p=>[p.packageName,p.name])),n=new Map(r.map(p=>[p.name,p.packageName])),o=new Set(a.map(p=>p.name)),i=new Set(r.map(p=>p.name)),u=[...new Set([...i,...o])].sort(),c=await Promise.all(u.map(async p=>{let P=o.has(p),v=P?(await B(q.join(e,"apps",p,"ui")))?.proxy?.routes:void 0;return{name:p,hasApi:i.has(p),hasUi:P,apiPackage:n.get(p),backends:v?U(v,t):[]}})),m=await z();return{apps:c,presets:Object.keys(s.devServersPresets??{}),environments:Object.keys(m?.envs??{}).sort()}},I=e=>{let r=[];for(let a of e.apps){if(a.hasUi){let s=a.backends.flatMap(n=>n.routes),t=s.length>0?`frontend \u2014 proxies ${[...s].sort().join(", ")}`:"frontend";r.push({name:`${a.name}/ui`,value:`${a.name}/ui`,description:t})}a.hasApi&&r.push({name:`${a.name}/api`,value:`${a.name}/api`,description:"backend"})}return r},E=e=>{let r={api:e.apps.filter(t=>t.hasApi).map(t=>t.name),ui:e.apps.filter(t=>t.hasUi).map(t=>t.name)},a={},s=new Map;for(let t of e.apps){t.apiPackage!=null&&(a[t.name]=t.apiPackage);for(let n of t.backends)for(let o of n.routes)s.set(`${t.name} ${o}`,n.packageName)}return{discovered:r,apiPkgByApp:a,routePkg:(t,n)=>s.get(`${t} ${n}`)}},T=(e,r)=>w({__wizard__:e},E(r)).map(a=>a.message),D=async(e,r)=>{if(r.apps.length===0)return d.warn("No apps discovered to run."),null;let a=await e.checkbox({message:"\u{1F4E6} Which packages?",choices:I(r)});if(a.length===0)return d.warn("No packages selected."),null;let s=await e.confirm({message:"\u{1F440} Rebuild & restart on save (watch)?",default:!1}),t=await e.confirm({message:"\u{1F9E9} Run each app in its own cmux pane?",default:!1}),n={targets:a,watch:s,cmux:t},o=C(n,r);o.anyCloudRoute&&(n.env=await e.select({message:"\u2601\uFE0F Point cloud routes at which environment?",choices:r.environments.map(c=>({name:c,value:c}))}),h.env[W]=n.env);let i=T(o.presetDef,r);if(i.length>0){d.warn("\u26A0\uFE0F Proxy audit found issues with this selection:");for(let c of i)d.warn(` \u2022 ${c}`);return null}F(o,n,r);let u=o.targetKeys.filter(c=>c.endsWith("/api")).map(c=>c.split("/")[0]);return t&&u.length>0?{include:u,presetDef:o.presetDef,watch:s,cmux:!0}:(t&&d.info("\u2139\uFE0F cmux needs at least one local backend (panes are backend-only) \u2014 running in-process instead."),{presetDef:o.presetDef,watch:s,cmux:!1})},F=(e,r,a)=>{l.setInteractive();let s=A(e,r,a);l.addOption(s.flags,!0),l.print(),s.exact||d.info("\u2139\uFE0F This part-level selection has no exact single-flag form \u2014 save it as a devPreset to reproduce it.")},L=async(e,r)=>{let a=await e.confirm({message:"\u{1F440} Rebuild & restart on save (watch)?",default:!1});return l.setInteractive(),l.addOption(r,!0),a&&l.addOption("--watch",!0),l.print(),{preset:r,watch:a,cmux:!1}},S=" manual",V=async(e,r)=>{if(r.presets.length===0)return D(e,r);let a=await e.select({message:"\u{1F680} Start from a preset, or configure manually?",choices:[...r.presets.map(s=>({name:s,value:s})),new j(" "),{name:"Manual (custom)\u2026",value:S}]});return a===S?D(e,r):L(e,a)},ce=async(e=K,r=x(h.cwd()))=>V(e,await _(r));export{S as MANUAL_CHOICE,T as auditManualPlan,K as defaultPrompts,_ as gatherWizardModel,ce as runDevWizard,V as runWizardFlow};
|
|
2
|
-
//# sourceMappingURL=dev-wizard-run-2ISRQPGA.js.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/dev/dev-wizard-run.ts", "../src/dev/dev-wizard.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Impure driver for the interactive `infra-kit dev` wizard: filesystem/config discovery, the inquirer\n * prompt flow (step-0 preset-or-manual, then the manual matrix), the pre-flight proxy audit, and the\n * `commandEcho` teaching line. The pure answer\u2192plan mapping lives in {@link file://./dev-wizard.ts}.\n *\n * Prompts are behind the injectable {@link WizardPrompts} seam so the flow is testable without a TTY.\n * This module is reached ONLY from the bare-invocation TTY branch of the entry point; every flagged /\n * non-TTY / `--json` / MCP invocation bypasses it entirely.\n */\nimport inquirerCheckbox from '@inquirer/checkbox'\nimport inquirerConfirm from '@inquirer/confirm'\nimport inquirerSelect, { Separator } from '@inquirer/select'\nimport { loadDev } from '@slip-stream-kit/config/internal'\nimport path from 'node:path'\nimport process from 'node:process'\n\nimport { discoverApiApps, discoverUiApps, findMonorepoRoot } from 'src/dev/discovery'\nimport { commandEcho } from 'src/lib/command-echo'\nimport { INFRA_KIT_ENV_VAR } from 'src/lib/constants'\nimport { readTokenStore } from 'src/lib/env-tokens'\nimport { getInfraKitConfig } from 'src/lib/infra-kit-config'\nimport type { DevPreset } from 'src/lib/infra-kit-config'\nimport { logger } from 'src/lib/logger'\n\nimport { deriveManualPlan, equivalentCommand } from './dev-wizard.js'\nimport type { DerivedPlan, ManualSelection, ProxyBackend, WizardApp, WizardModel } from './dev-wizard.js'\nimport { validatePresetProxy } from './presets.js'\nimport type { DiscoveredParts, PresetProxyContext } from './presets.js'\n\n/** A wizard prompt choice \u2014 all values are strings, so the seam needs no generics. */\nexport interface WizardChoice {\n name: string\n value: string\n description?: string\n checked?: boolean\n disabled?: boolean | string\n}\n\n/**\n * Injectable prompt seam (a string-valued subset of `@inquirer/*`). The default implementation\n * ({@link defaultPrompts}) delegates to the real prompts, rendering to stderr so the dev-server's\n * stdout stays clean. Tests pass a scripted object.\n */\nexport interface WizardPrompts {\n select: (cfg: { message: string; choices: (WizardChoice | Separator)[]; default?: string }) => Promise<string>\n checkbox: (cfg: { message: string; choices: WizardChoice[] }) => Promise<string[]>\n confirm: (cfg: { message: string; default?: boolean }) => Promise<boolean>\n}\n\n/**\n * Shared `@inquirer/*` context. Rendered to stderr (mirrors the bare-`infra-kit` command palette).\n *\n * `clearPromptOnDone` erases each answered prompt instead of leaving a `\u2714 <question> <answer>` line\n * behind, so the wizard collapses to nothing once it finishes and the dev-server's ready header lands\n * at the top of a clean screen. The choices are not lost: {@link echoManual} / {@link runPresetBranch}\n * print the equivalent flag command, which is the durable record of what was picked.\n */\nconst promptContext = { output: process.stderr, clearPromptOnDone: true }\n\n/** Real `@inquirer/*` prompts, rendered to stderr and erased once answered. */\nexport const defaultPrompts: WizardPrompts = {\n select: (cfg) => {\n return inquirerSelect({ message: cfg.message, choices: cfg.choices, default: cfg.default }, promptContext)\n },\n checkbox: (cfg) => {\n return inquirerCheckbox({ message: cfg.message, choices: cfg.choices }, promptContext)\n },\n confirm: (cfg) => {\n return inquirerConfirm({ message: cfg.message, default: cfg.default }, promptContext)\n },\n}\n\n/** What the wizard hands back to the entry point (merged into `DevServerOptions`), or null on cancel. */\nexport interface WizardResult {\n /** Named preset (preset branch) \u2014 passed through as `options.preset`. */\n preset?: string\n /** In-memory preset (manual branch, non-cmux) \u2014 passed through as `options.presetDef`. */\n presetDef?: DevPreset\n /**\n * App-name include list for the cmux path only: which discovered API apps get a pane. Paired with\n * `presetDef`, which tells each pane the exact parts to run. An app selected UI-only gets no pane\n * (cmux panes are opened per API app). Unset for the non-cmux manual path.\n */\n include?: string[]\n watch: boolean\n cmux: boolean\n}\n\n/** Group one frontend's `dev.proxy.routes` into {@link ProxyBackend}s keyed by backend package. */\nconst groupBackends = (\n routes: Record<string, { packageName: string; from: readonly string[] }>,\n ownerByPkg: Map<string, string>,\n): ProxyBackend[] => {\n const byPkg = new Map<string, ProxyBackend>()\n\n for (const [route, def] of Object.entries(routes)) {\n const existing = byPkg.get(def.packageName)\n const localCapable = def.from.includes('local')\n\n if (existing) {\n existing.routes.push(route)\n existing.localCapable = existing.localCapable || localCapable\n } else {\n byPkg.set(def.packageName, {\n packageName: def.packageName,\n routes: [route],\n localCapable,\n ownerApp: ownerByPkg.get(def.packageName),\n })\n }\n }\n\n return [...byPkg.values()]\n}\n\n/** Discover apps + configs and assemble the {@link WizardModel} (backends resolved per frontend). */\nexport const gatherWizardModel = async (root: string): Promise<WizardModel> => {\n const apiApps = discoverApiApps(root)\n const uiApps = discoverUiApps(root)\n const config = await getInfraKitConfig()\n\n const ownerByPkg = new Map(\n apiApps.map((a) => {\n return [a.packageName, a.name] as const\n }),\n )\n const apiPkgByName = new Map(\n apiApps.map((a) => {\n return [a.name, a.packageName] as const\n }),\n )\n const uiNames = new Set(\n uiApps.map((a) => {\n return a.name\n }),\n )\n const apiNames = new Set(\n apiApps.map((a) => {\n return a.name\n }),\n )\n const allNames = [...new Set([...apiNames, ...uiNames])].sort()\n\n const apps: WizardApp[] = await Promise.all(\n allNames.map(async (name): Promise<WizardApp> => {\n const hasUi = uiNames.has(name)\n const routes = hasUi ? (await loadDev(path.join(root, 'apps', name, 'ui')))?.proxy?.routes : undefined\n\n return {\n name,\n hasApi: apiNames.has(name),\n hasUi,\n apiPackage: apiPkgByName.get(name),\n backends: routes ? groupBackends(routes, ownerByPkg) : [],\n }\n }),\n )\n\n // The cloud-env choices are the envs we hold a token for \u2014 the same authority as `env-load`, because\n // it is the same question. This picker writes INFRA_KIT_ENV, which `@slip-stream-kit/config/vite`\n // reads to build the cloud backend's URL, and whose only other writer is `env-load` (which cannot run\n // without a Doppler token). Sourcing it from the workflow options instead would put `prod` in the list\n // for everyone \u2014 including a developer holding no prod credential \u2014 and point a local UI at\n // production. No token, no entry.\n const store = await readTokenStore()\n\n return {\n apps,\n presets: Object.keys(config.devServersPresets ?? {}),\n environments: Object.keys(store?.envs ?? {}).sort(),\n }\n}\n\n/**\n * Build the flat `<app>/<part>` checkbox choices, grouped per app as `ui` then `api`. A frontend choice\n * carries a description of the routes it proxies, so ticking the matching api part reads as \"run that\n * route locally\". This is the checkbox the user directly picks their targets from.\n *\n * Nothing starts checked: the manual branch is opt-in, so a run only ever launches what was explicitly\n * ticked. Pre-checking every part would make the fast path (accept the defaults) boot the whole monorepo\n * \u2014 the exact opposite of why someone reached for \"Manual\" over a preset.\n */\nconst buildPartChoices = (model: WizardModel): WizardChoice[] => {\n const choices: WizardChoice[] = []\n\n for (const app of model.apps) {\n if (app.hasUi) {\n const routes = app.backends.flatMap((b) => {\n return b.routes\n })\n const description = routes.length > 0 ? `frontend \u2014 proxies ${[...routes].sort().join(', ')}` : 'frontend'\n\n choices.push({ name: `${app.name}/ui`, value: `${app.name}/ui`, description })\n }\n if (app.hasApi) {\n choices.push({ name: `${app.name}/api`, value: `${app.name}/api`, description: 'backend' })\n }\n }\n\n return choices\n}\n\n/** Build the audit context from the gathered model (no re-reading of configs). */\nconst buildAuditContext = (model: WizardModel): PresetProxyContext => {\n const discovered: DiscoveredParts = {\n api: model.apps\n .filter((a) => {\n return a.hasApi\n })\n .map((a) => {\n return a.name\n }),\n ui: model.apps\n .filter((a) => {\n return a.hasUi\n })\n .map((a) => {\n return a.name\n }),\n }\n const apiPkgByApp: Record<string, string> = {}\n const routeToPkg = new Map<string, string>()\n\n for (const app of model.apps) {\n // Map EVERY discovered api app to its own api package \u2014 including api-only apps a frontend proxies\n // to cross-app \u2014 so `launchedPkgs` in validatePresetProxy is faithful (mirrors preset-proxy-check.ts).\n // Deriving this from proxy backends alone would miss api-only owners and false-positive the audit.\n if (app.apiPackage != null) apiPkgByApp[app.name] = app.apiPackage\n for (const b of app.backends) {\n for (const route of b.routes) {\n routeToPkg.set(`${app.name} ${route}`, b.packageName)\n }\n }\n }\n\n return {\n discovered,\n apiPkgByApp,\n routePkg: (app, route) => {\n return routeToPkg.get(`${app} ${route}`)\n },\n }\n}\n\n/**\n * Run the assembled plan through the SAME proxy-locality rule the root audit uses. Returns issue\n * messages (empty when clean). Catches a `local` override whose backend won't launch \u2014 the wizard's\n * derivation launches owners, so a hit means the backend has no discoverable owning app.\n */\nexport const auditManualPlan = (presetDef: DevPreset, model: WizardModel): string[] => {\n return validatePresetProxy({ __wizard__: presetDef }, buildAuditContext(model)).map((i) => {\n return i.message\n })\n}\n\n/** The manual-branch flow: app + per-app proxy + env + watch + cmux \u2192 audited plan \u2192 echo. */\nconst runManualBranch = async (prompts: WizardPrompts, model: WizardModel): Promise<WizardResult | null> => {\n if (model.apps.length === 0) {\n logger.warn('No apps discovered to run.')\n\n return null\n }\n\n const selectedTargets = await prompts.checkbox({ message: '\uD83D\uDCE6 Which packages?', choices: buildPartChoices(model) })\n\n if (selectedTargets.length === 0) {\n logger.warn('No packages selected.')\n\n return null\n }\n\n const watch = await prompts.confirm({ message: '\uD83D\uDC40 Rebuild & restart on save (watch)?', default: false })\n const cmux = await prompts.confirm({ message: '\uD83E\uDDE9 Run each app in its own cmux pane?', default: false })\n\n const selection: ManualSelection = { targets: selectedTargets, watch, cmux }\n const plan = deriveManualPlan(selection, model)\n\n if (plan.anyCloudRoute) {\n selection.env = await prompts.select({\n message: '\u2601\uFE0F Point cloud routes at which environment?',\n choices: model.environments.map((e) => {\n return { name: e, value: e }\n }),\n })\n process.env[INFRA_KIT_ENV_VAR] = selection.env\n }\n\n const issues = auditManualPlan(plan.presetDef, model)\n\n if (issues.length > 0) {\n logger.warn('\u26A0\uFE0F Proxy audit found issues with this selection:')\n for (const issue of issues) {\n logger.warn(` \u2022 ${issue}`)\n }\n\n return null\n }\n\n echoManual(plan, selection, model)\n\n // cmux opens one pane per selected API app. `include` picks WHICH apps get a pane; `presetDef` tells\n // each pane exactly which of its parts to run. Without the latter a pane runs `--app=<name>`, which\n // expands to every part the app has \u2014 silently starting a UI the user just unticked.\n const apiApps = plan.targetKeys\n .filter((k) => {\n return k.endsWith('/api')\n })\n .map((k) => {\n return k.split('/')[0]!\n })\n\n // An empty include would collapse to `null` in `normalizeAppInclude` and make cmux run EVERY api app,\n // so a cmux run with no selected backends (e.g. an all-frontend selection) falls back to in-process.\n if (cmux && apiApps.length > 0) {\n return { include: apiApps, presetDef: plan.presetDef, watch, cmux: true }\n }\n\n if (cmux) {\n logger.info('\u2139\uFE0F cmux needs at least one local backend (panes are backend-only) \u2014 running in-process instead.')\n }\n\n return { presetDef: plan.presetDef, watch, cmux: false }\n}\n\n/** Print the equivalent flag command (and, for a part-level selection, the save-as-preset hint). */\nconst echoManual = (plan: DerivedPlan, selection: ManualSelection, model: WizardModel): void => {\n commandEcho.setInteractive()\n\n const eq = equivalentCommand(plan, selection, model)\n\n commandEcho.addOption(eq.flags, true)\n commandEcho.print()\n\n if (!eq.exact) {\n logger.info('\u2139\uFE0F This part-level selection has no exact single-flag form \u2014 save it as a devPreset to reproduce it.')\n }\n}\n\n/** The preset-branch flow: run a named preset, asking only whether to watch (presets can't encode it). */\nconst runPresetBranch = async (prompts: WizardPrompts, preset: string): Promise<WizardResult> => {\n const watch = await prompts.confirm({ message: '\uD83D\uDC40 Rebuild & restart on save (watch)?', default: false })\n\n commandEcho.setInteractive()\n commandEcho.addOption(preset, true)\n if (watch) commandEcho.addOption('--watch', true)\n commandEcho.print()\n\n return { preset, watch, cmux: false }\n}\n\n/** Sentinel value for the \"Manual (custom)\" step-0 choice (a preset name can never be empty). */\nexport const MANUAL_CHOICE = ' manual'\n\n/**\n * Drive the wizard's branch flow over an ALREADY-gathered model (no disk/config access). Split from\n * {@link runDevWizard} so the flow is unit-testable with scripted prompts + a fixture model.\n */\nexport const runWizardFlow = async (prompts: WizardPrompts, model: WizardModel): Promise<WizardResult | null> => {\n if (model.presets.length === 0) {\n return runManualBranch(prompts, model)\n }\n\n const choice = await prompts.select({\n message: '\uD83D\uDE80 Start from a preset, or configure manually?',\n choices: [\n ...model.presets.map((p) => {\n return { name: p, value: p }\n }),\n new Separator(' '),\n { name: 'Manual (custom)\u2026', value: MANUAL_CHOICE },\n ],\n })\n\n return choice === MANUAL_CHOICE ? runManualBranch(prompts, model) : runPresetBranch(prompts, choice)\n}\n\n/**\n * Gather the model from disk/config, then drive the wizard flow, returning the resolved run options (or\n * null on cancel / empty selection). The entry point calls this on a bare TTY `infra-kit dev`.\n */\nexport const runDevWizard = async (\n prompts: WizardPrompts = defaultPrompts,\n root: string = findMonorepoRoot(process.cwd()),\n): Promise<WizardResult | null> => {\n return runWizardFlow(prompts, await gatherWizardModel(root))\n}\n", "/**\n * Pure core of the interactive `infra-kit dev` wizard (the no-args, TTY path).\n *\n * The wizard is FRONTEND-CENTRIC: you pick apps to run, and for each frontend you choose, per backend\n * it proxies to, whether that backend runs LOCALLY (its `/api` is launched \u2192 the route resolves local)\n * or stays CLOUD (not launched \u2192 the route proxies to the `<env>` cloud target). A backend runs iff a\n * frontend points at it locally, or its app is selected and not demoted to cloud. This module turns\n * those answers into an in-memory {@link DevPreset} the runner consumes verbatim (part-level targets\n * that the app-name-only `--app` include cannot express) \u2014 and is side-effect-free so the mapping is\n * fully unit-testable. The impure I/O (discovery, inquirer prompts, audit, launch) lives in\n * {@link file://./dev-wizard-run.ts}.\n */\nimport type { DevPreset, ProxySource } from 'src/lib/infra-kit-config'\n\n/**\n * One backend a frontend's proxy routes can be pointed at, grouped by the backend package. A frontend\n * that maps `/api` and `/media` to the same package yields ONE {@link ProxyBackend} with both routes \u2014\n * launching the package flips every one of its `local`-capable routes together (see `pickSource`).\n */\nexport interface ProxyBackend {\n /** Backend package name (a route's `packageName`). */\n packageName: string\n /** Route paths that resolve to this package (for display + the audit's per-route overrides). */\n routes: string[]\n /** True when at least one of `routes` lists `local` in its `from` capabilities \u2014 i.e. it is toggleable. A `false` backend is fixed-cloud, shown as info. */\n localCapable: boolean\n /** App folder whose `api` package is {@link packageName}, or undefined when no discovered app owns it (a `local` choice then has nothing to launch \u2014 the audit flags it). */\n ownerApp?: string\n}\n\n/** A discovered app as the wizard sees it: which parts exist + which backends its frontend proxies to. */\nexport interface WizardApp {\n /** App folder name (e.g. `client`). */\n name: string\n /** Has `apps/<app>/api` (a launchable backend). */\n hasApi: boolean\n /** Has `apps/<app>/ui` (a launchable frontend). */\n hasUi: boolean\n /**\n * This app's OWN `api` package name (from `apps/<app>/api`), or undefined when it has no api. Carried\n * so the audit can build a faithful `app \u2192 apiPackage` map for EVERY discovered api app \u2014 including\n * api-only apps a frontend proxies to cross-app \u2014 mirroring the root audit (`preset-proxy-check.ts`).\n */\n apiPackage?: string\n /** Backends this app's frontend proxies to (empty for api-only apps or a frontend with no `dev.proxy`). */\n backends: ProxyBackend[]\n}\n\n/** Everything the wizard needs, gathered impurely by {@link file://./dev-wizard-run.ts}. */\nexport interface WizardModel {\n /** All discovered apps (api and/or ui), sorted by name. */\n apps: WizardApp[]\n /** `devServersPresets` names (drives the preset-or-manual step-0). */\n presets: string[]\n /** Configured `environments` (the cloud-env `select` options). */\n environments: string[]\n}\n\n/** The manual-branch answers collected from the prompts, fed to {@link deriveManualPlan}. */\nexport interface ManualSelection {\n /**\n * The `<app>/<part>` target keys the user ticked directly (e.g. `client/ui`, `client/api`). A frontend's\n * proxy route resolves LOCAL iff the backend's owner-api part is in this set \u2014 otherwise CLOUD \u2014 so the\n * part checkbox IS the local/cloud decision; there is no separate per-backend question.\n */\n targets: string[]\n /** Chosen cloud env for cloud routes (undefined when nothing resolves cloud). */\n env?: string\n /** Rebuild + restart on save. */\n watch: boolean\n /** One cmux pane per app. */\n cmux: boolean\n}\n\n/** The resolved run plan derived from a {@link ManualSelection}. */\nexport interface DerivedPlan {\n /** In-memory preset handed to the runner (`options.presetDef`). */\n presetDef: DevPreset\n /** At least one route resolves to cloud \u2192 an env must be chosen (pre-flight gate). */\n anyCloudRoute: boolean\n /** Resolved `<app>/<part>` target keys, sorted \u2014 for the echo + audit display. */\n targetKeys: string[]\n}\n\n/** App folder name from an `<app>/<part>` target key. */\nconst appOf = (key: string): string => {\n return key.split('/')[0]!\n}\n\n/** Index a model's apps by folder name for O(1) lookup. */\nconst byName = (model: WizardModel): Map<string, WizardApp> => {\n return new Map(\n model.apps.map((a) => {\n return [a.name, a]\n }),\n )\n}\n\n/**\n * Resolve ONE selected frontend's proxy routes against the set of api parts the user ticked: a route is\n * `local` iff its backend is `localCapable` AND that backend's owner-api part was selected \u2014 otherwise\n * `cloud`. Kept separate from {@link deriveManualPlan} so the per-app override loop stays flat.\n */\nconst resolveUiProxy = (\n app: WizardApp,\n selectedApiApps: ReadonlySet<string>,\n): { overrides: Record<string, ProxySource>; anyCloud: boolean } => {\n const overrides: Record<string, ProxySource> = {}\n let anyCloud = false\n\n for (const backend of app.backends) {\n const isLocal = backend.localCapable && backend.ownerApp != null && selectedApiApps.has(backend.ownerApp)\n\n if (!isLocal) anyCloud = true\n for (const route of backend.routes) {\n overrides[route] = isLocal ? 'local' : 'cloud'\n }\n }\n\n return { overrides, anyCloud }\n}\n\n/**\n * Turn a part-level {@link ManualSelection} into an in-memory {@link DevPreset}. Because the user picks\n * `<app>/<part>` targets directly, the mapping is straightforward:\n * - every selected `<app>/ui` runs that frontend;\n * - every selected `<app>/api` runs that backend locally;\n * - each frontend's proxy route resolves `local` iff the backend's owner-api part was also selected\n * (even cross-app), else `cloud` \u2014 i.e. ticking the api part IS the local choice.\n *\n * @example\n * // client frontend only (its /api left unticked \u2192 its proxy route resolves to cloud):\n * deriveManualPlan(\n * { targets: ['client/ui'], watch: false, cmux: false },\n * { apps: [{ name: 'client', hasApi: true, hasUi: true,\n * backends: [{ packageName: 'client-api', routes: ['/api'], localCapable: true, ownerApp: 'client' }] }],\n * presets: [], environments: ['dev'] },\n * ).targetKeys // => ['client/ui']\n */\nexport const deriveManualPlan = (selection: ManualSelection, model: WizardModel): DerivedPlan => {\n const apps = byName(model)\n const uiKeys = selection.targets.filter((t) => {\n return t.endsWith('/ui')\n })\n const apiApps = new Set(\n selection.targets\n .filter((t) => {\n return t.endsWith('/api')\n })\n .map(appOf),\n )\n\n const presetApps: Record<string, { proxy?: Record<string, ProxySource> }> = {}\n let anyCloud = false\n\n for (const key of uiKeys) {\n const app = apps.get(appOf(key))\n const { overrides, anyCloud: cloud } = app ? resolveUiProxy(app, apiApps) : { overrides: {}, anyCloud: false }\n\n if (cloud) anyCloud = true\n presetApps[key] = Object.keys(overrides).length > 0 ? { proxy: overrides } : {}\n }\n for (const name of apiApps) {\n presetApps[`${name}/api`] ??= {}\n }\n\n return {\n presetDef: { apps: presetApps, cmux: selection.cmux },\n anyCloudRoute: anyCloud,\n targetKeys: Object.keys(presetApps).sort(),\n }\n}\n\n/** The equivalent non-interactive command, plus whether it reproduces the selection exactly. */\nexport interface EquivalentCommand {\n /** The `--app=\u2026`-form flag string (no leading `infra-kit dev`). */\n flags: string\n /**\n * True when `--app` reproduces the plan exactly \u2014 i.e. every involved app runs ALL the parts it has.\n * False for a part-level selection (e.g. frontend-only), which `--app` (app-name granularity) cannot\n * express; the caller then hints \"save as a preset\" for exact reproduction.\n */\n exact: boolean\n}\n\n/**\n * Build the equivalent `infra-kit dev --app=\u2026 [--watch] [--cmux]` flag string for a derived plan.\n * `exact` is true only when the target set covers every part each involved app HAS (a whole-app\n * selection), since `--app` filters by app name, not part.\n */\nexport const equivalentCommand = (\n plan: DerivedPlan,\n selection: ManualSelection,\n model: WizardModel,\n): EquivalentCommand => {\n const apps = byName(model)\n const involved = [...new Set(plan.targetKeys.map(appOf))].sort()\n const present = new Set(plan.targetKeys)\n\n const exact = involved.every((name) => {\n const app = apps.get(name)\n\n if (!app) return false\n\n const uiOk = !app.hasUi || present.has(`${name}/ui`)\n const apiOk = !app.hasApi || present.has(`${name}/api`)\n\n return uiOk && apiOk\n })\n\n const parts = [`--app=${involved.join(',')}`]\n\n if (selection.watch) parts.push('--watch')\n if (selection.cmux) parts.push('--cmux')\n\n return { flags: parts.join(' '), exact }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["inquirerCheckbox", "inquirerConfirm", "inquirerSelect", "Separator", "loadDev", "path", "process", "appOf", "key", "byName", "model", "a", "resolveUiProxy", "app", "selectedApiApps", "overrides", "anyCloud", "backend", "isLocal", "route", "deriveManualPlan", "selection", "apps", "uiKeys", "t", "apiApps", "presetApps", "cloud", "name", "equivalentCommand", "plan", "involved", "present", "exact", "uiOk", "apiOk", "parts", "promptContext", "process", "defaultPrompts", "cfg", "inquirerSelect", "inquirerCheckbox", "inquirerConfirm", "groupBackends", "routes", "ownerByPkg", "byPkg", "route", "def", "existing", "localCapable", "gatherWizardModel", "root", "apiApps", "discoverApiApps", "uiApps", "discoverUiApps", "config", "getInfraKitConfig", "a", "apiPkgByName", "uiNames", "apiNames", "allNames", "apps", "name", "hasUi", "loadDev", "path", "store", "readTokenStore", "buildPartChoices", "model", "choices", "app", "b", "description", "buildAuditContext", "discovered", "apiPkgByApp", "routeToPkg", "auditManualPlan", "presetDef", "validatePresetProxy", "i", "runManualBranch", "prompts", "logger", "selectedTargets", "watch", "cmux", "selection", "plan", "deriveManualPlan", "e", "INFRA_KIT_ENV_VAR", "issues", "issue", "echoManual", "k", "commandEcho", "eq", "equivalentCommand", "runPresetBranch", "preset", "MANUAL_CHOICE", "runWizardFlow", "choice", "p", "Separator", "runDevWizard", "findMonorepoRoot"]
|
|
7
|
-
}
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{a as p,c as u}from"./chunk-TI33V5G2.js";import{a as l}from"./chunk-TPVXMJ7N.js";import{render as C}from"ink";import E from"node:process";import{Box as c,Static as S,Text as a}from"ink";import{useEffect as F,useState as b}from"react";import{jsx as o,jsxs as v}from"react/jsx-runtime";var g=r=>{let{phase:e,narration:t,readyLines:i}=r,[h,f]=b(0);return F(()=>{if(i)return;let n=setInterval(()=>{f(s=>s+1)},w);return()=>{clearInterval(n)}},[i]),i?o(S,{items:i,children:(n,s)=>o(a,{children:n},s)}):v(c,{flexDirection:"column",children:[v(c,{children:[o(a,{color:"cyan",children:m[h%m.length]}),o(a,{dimColor:!0,children:` ${e}`})]}),t?o(a,{dimColor:!0,children:t}):null]})},m=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"],w=80;import{Box as P,Static as D,Text as L}from"ink";import{Fragment as T,jsx as d,jsxs as z}from"react/jsx-runtime";var R=r=>{let{headerLines:e,logLines:t,footerLines:i}=r,h=[...e,...t],f=B(i,r.maxFooterLines);return z(T,{children:[d(D,{items:h,children:(n,s)=>d(L,{children:n},s)}),d(P,{flexDirection:"column",children:f.map((n,s)=>d(L,{children:n},s))})]})},B=(r,e)=>{if(e==null||e<1||r.length<=e)return r;let t=r.slice(0,e-1);return[...t,` \u2026 +${r.length-t.length} more`]};import{jsx as x}from"react/jsx-runtime";var y=class{renderer;renderFn;stdout;verbose;now;instance=null;finalized=!1;persistent=!1;phase="";narration="";headerLines=[];logLines=[];footerLines=[];constructor(e){this.verbose=e.verbose??!1,this.renderFn=e.render??C,this.stdout=e.stdout??l(E.stdout),this.now=e.now??(()=>new Date),this.renderer=new u({appendLog:e.appendLog,verbose:this.verbose,isTTY:!0,write:t=>{this.stdout.write(t)},now:this.now})}logFn=(e,t="info")=>{this.log(e,t)};renderBoot(e){let t=x(g,{phase:this.phase,narration:this.narration,readyLines:e});if(this.instance){this.instance.rerender(t);return}this.instance=this.renderFn(t,{stdout:this.stdout,exitOnCtrlC:!1,patchConsole:!1})}unmount(){this.instance&&(this.instance.unmount(),this.instance=null)}narrate(e){if(this.renderer.teeOnly(e,"info"),this.persistent){this.verbose&&(this.logLines.push(e),this.rerenderPersistent());return}this.finalized||(this.narration=e,this.instance&&this.renderBoot())}log(e,t="info"){let i=t!=="debug"||this.verbose;if(this.persistent){this.renderer.teeOnly(e,t),i&&(this.logLines.push(e),this.rerenderPersistent());return}i&&this.unmount(),this.renderer.log(e,t)}bootStep(e){this.renderer.teeOnly(e,"info"),!this.finalized&&(this.phase=e,this.renderBoot())}stopSpinner(){this.persistent||this.unmount()}ready(e){this.headerLines=this.renderer.formatHeaderLines(e),this.footerLines=this.renderer.formatFooterLines(e),this.logLines=[];for(let t of this.headerLines)this.renderer.teeOnly(t,"info");this.finalized=!0,this.persistent=!0,typeof this.stdout.on=="function"&&this.stdout.on("resize",this.onResize),this.rerenderPersistent()}maxFooterLines(){return Math.max(3,(this.stdout.rows??24)-6)}rerenderPersistent(){let e=x(R,{headerLines:this.headerLines,logLines:this.logLines,footerLines:this.footerLines,maxFooterLines:this.maxFooterLines()});if(this.instance){this.instance.rerender(e);return}this.instance=this.renderFn(e,{stdout:this.stdout,exitOnCtrlC:!1,patchConsole:!1})}onResize=()=>{this.persistent&&this.rerenderPersistent()};event(e){if(this.persistent){let t=` ${p(this.now())} ${e.tag} ${e.text}`;this.renderer.teeOnly(`${e.tag} ${e.text}`,"info"),this.logLines.push(t),this.rerenderPersistent();return}this.unmount(),this.renderer.event(e)}refresh(e){this.persistent&&(this.footerLines=this.renderer.formatFooterLines(e),this.rerenderPersistent())}dispose(){this.persistent&&(this.rerenderPersistent(),typeof this.stdout.off=="function"&&this.stdout.off("resize",this.onResize)),this.finalized=!0,this.persistent=!1,this.unmount()}};export{y as PersistentInkDevUi};
|
|
2
|
-
//# sourceMappingURL=persistent-ink-dev-ui-KOI44ZVT.js.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../src/tui/dev-ui/persistent-ink-dev-ui.tsx", "../src/tui/dev-ui/boot-region.tsx", "../src/tui/dev-ui/live-region.tsx"],
|
|
4
|
-
"sourcesContent": ["import { render as inkRender } from 'ink'\nimport process from 'node:process'\nimport type { ReactElement } from 'react'\n\nimport type { DevUi, LogLevel, ReadySummary } from 'src/dev/dev-ui'\nimport { DevRenderer, formatClock } from 'src/dev/render'\nimport { createSafeStream } from 'src/tui/safe-stderr'\n\nimport { BootRegion } from './boot-region'\nimport { LiveRegion } from './live-region'\n\n/**\n * Ink-backed {@link DevUi} for `infra-kit dev`: a boot region that collapses into a PERSISTENT status\n * panel. It is the UI on any interactive TTY.\n *\n * There is one shape of session now, not two. Until recently `ready` branched on `summary.hasUiChild`\n * and, for a UI session, unmounted Ink and handed off to a DECSTBM scroll-region UI \u2014 because the\n * `turbo run dev` child's piped output produced an unbounded tail, Ink repaints its live region on every\n * appended line, and a pinned footer can only survive a scrolling stream if the TERMINAL is told to\n * confine it.\n *\n * That tail no longer exists. Framework lines and request lines go to their per-service log files and\n * are never printed, so there is nothing to scroll past the panel and nothing for a scroll region to\n * defend it from. The branch, the handoff, and the whole DECSTBM machinery were deleted with the stream\n * that justified them.\n *\n * What still prints above the panel is only what the RUNNER says \u2014 a restart, an unhealthy app, a dead\n * engine. Those are rare and they are events, not logs.\n *\n * The embedded {@link DevRenderer} owns the file-tee format (`teeOnly`); in persistent mode it is used\n * ONLY for teeing, never its direct stdout write, which would collide with the live region. Every\n * component here is OUTPUT-ONLY (no `useInput`) \u2014 see live-region.tsx.\n */\n\n/** The subset of an Ink render handle this UI drives \u2014 shared by `ink` and `ink-testing-library`. */\ninterface RenderHandle {\n rerender: (tree: ReactElement) => void\n unmount: () => void\n}\n\n/** Render seam: `ink`'s `render` by default; `ink-testing-library`'s `render` is injected in tests. */\ntype RenderFn = (tree: ReactElement, options?: unknown) => RenderHandle\n\nexport interface PersistentInkDevUiDeps {\n /** File tee \u2014 the same appender the runner hands the plain renderer. */\n appendLog: (text: string) => void\n /** Mirrors the renderer's verbose gate: only in verbose does a `debug` log reach the terminal. */\n verbose?: boolean\n /** Clock seam (for deterministic tee + event timestamps in tests). */\n now?: () => Date\n /** Render seam (default `ink`'s `render`); tests inject `ink-testing-library`'s `render`. */\n render?: RenderFn\n /**\n * Target stream (default: `process.stdout` behind the scrollback-safe filter).\n *\n * The runner injects a stream whose `write` BYPASSES the output interceptor. That is not optional:\n * the interceptor owns `process.stdout` for the life of the session, so a panel painted through the\n * raw stream would have its frames filed into a log file instead of drawn \u2014 a blank screen, with no\n * error and no failing test.\n */\n stdout?: NodeJS.WriteStream\n}\n\nexport class PersistentInkDevUi implements DevUi {\n private readonly renderer: DevRenderer\n private readonly renderFn: RenderFn\n private readonly stdout: NodeJS.WriteStream\n private readonly verbose: boolean\n private readonly now: () => Date\n /** Live Ink handle, or `null` when not mounted. */\n private instance: RenderHandle | null = null\n /** True once boot is done: never mount the boot region again. */\n private finalized = false\n /** True while the persistent panel owns the terminal (after ready). */\n private persistent = false\n private phase = ''\n private narration = ''\n private headerLines: string[] = []\n private logLines: string[] = []\n private footerLines: string[] = []\n\n constructor(deps: PersistentInkDevUiDeps) {\n this.verbose = deps.verbose ?? false\n this.renderFn = deps.render ?? (inkRender as RenderFn)\n // NOT raw `process.stdout`: Ink writes `ESC[3J` (erase saved lines) whenever a frame overflows the\n // viewport, which wipes the terminal's SCROLLBACK \u2014 the user's history, and every previous command's\n // output when `dev` runs as a session-shell child. `createSafeStream` drops that one escape and\n // forwards everything else live.\n this.stdout = deps.stdout ?? createSafeStream(process.stdout as NodeJS.WriteStream)\n this.now =\n deps.now ??\n ((): Date => {\n return new Date()\n })\n this.renderer = new DevRenderer({\n appendLog: deps.appendLog,\n verbose: this.verbose,\n isTTY: true,\n write: (text) => {\n this.stdout.write(text)\n },\n now: this.now,\n })\n }\n\n /** `LogFn`-shaped adapter for the build runner seam. */\n readonly logFn = (message: string, level: LogLevel = 'info'): void => {\n this.log(message, level)\n }\n\n // ---- boot ---------------------------------------------------------------\n\n /** Mount the Ink boot region (or rerender it in place) with the current phase + narration. */\n private renderBoot(readyLines?: string[]): void {\n const tree = <BootRegion phase={this.phase} narration={this.narration} readyLines={readyLines} />\n\n if (this.instance) {\n this.instance.rerender(tree)\n\n return\n }\n this.instance = this.renderFn(tree, { stdout: this.stdout, exitOnCtrlC: false, patchConsole: false })\n }\n\n /** Unmount Ink if mounted (idempotent). */\n private unmount(): void {\n if (this.instance) {\n this.instance.unmount()\n this.instance = null\n }\n }\n\n narrate(message: string): void {\n this.renderer.teeOnly(message, 'info')\n if (this.persistent) {\n // Visible only in verbose (matches the plain renderer); tee already happened above.\n if (this.verbose) {\n this.logLines.push(message)\n this.rerenderPersistent()\n }\n\n return\n }\n if (this.finalized) {\n return\n }\n this.narration = message\n if (this.instance) {\n this.renderBoot()\n }\n }\n\n log(message: string, level: LogLevel = 'info'): void {\n const willWrite = level !== 'debug' || this.verbose\n\n if (this.persistent) {\n // Never the embedded renderer's direct stdout write here \u2014 append to the live region instead.\n this.renderer.teeOnly(message, level)\n if (willWrite) {\n this.logLines.push(message)\n this.rerenderPersistent()\n }\n\n return\n }\n if (willWrite) {\n this.unmount()\n }\n this.renderer.log(message, level)\n }\n\n bootStep(phase: string): void {\n this.renderer.teeOnly(phase, 'info')\n if (this.finalized) {\n return\n }\n this.phase = phase\n this.renderBoot()\n }\n\n stopSpinner(): void {\n if (!this.persistent) {\n this.unmount()\n }\n }\n\n // ---- the persistent panel ----------------------------------------------\n\n /** Commit the header, tee it, mount the live panel, and STAY mounted for the rest of the session. */\n ready(summary: ReadySummary): void {\n this.headerLines = this.renderer.formatHeaderLines(summary)\n this.footerLines = this.renderer.formatFooterLines(summary)\n this.logLines = []\n for (const line of this.headerLines) {\n this.renderer.teeOnly(line, 'info')\n }\n this.finalized = true\n this.persistent = true\n // Re-clamp the panel when the terminal resizes. Guarded: an injected (test/piped) stream may not\n // be an EventEmitter TTY.\n if (typeof this.stdout.on === 'function') {\n this.stdout.on('resize', this.onResize)\n }\n this.rerenderPersistent()\n }\n\n /**\n * Cap for the pinned panel: it is a live region and cannot scroll, so it must never exceed the\n * viewport. Reserve rows for the runner's occasional lines above it; fall back to an 80\u00D724 assumption\n * when the stream reports no row count (piped / test stdout).\n */\n private maxFooterLines(): number {\n return Math.max(3, (this.stdout.rows ?? 24) - 6)\n }\n\n /** Mount or repaint the live region from the current header / logs / panel snapshot. */\n private rerenderPersistent(): void {\n const tree = (\n <LiveRegion\n headerLines={this.headerLines}\n logLines={this.logLines}\n footerLines={this.footerLines}\n maxFooterLines={this.maxFooterLines()}\n />\n )\n\n if (this.instance) {\n this.instance.rerender(tree)\n\n return\n }\n this.instance = this.renderFn(tree, { stdout: this.stdout, exitOnCtrlC: false, patchConsole: false })\n }\n\n /** Re-clamp + repaint on terminal resize (SIGWINCH), so a shrink never overflows the viewport. */\n private readonly onResize = (): void => {\n if (this.persistent) {\n this.rerenderPersistent()\n }\n }\n\n /**\n * A tagged, timestamped line. The runner no longer emits any \u2014 framework and request lines go to their\n * per-service files instead of the terminal \u2014 but the seam stays on the {@link DevUi} contract for the\n * plain/non-TTY renderer, which still streams.\n */\n event(input: { tag: string; text: string }): void {\n if (this.persistent) {\n const line = ` ${formatClock(this.now())} ${input.tag} ${input.text}`\n\n this.renderer.teeOnly(`${input.tag} ${input.text}`, 'info')\n this.logLines.push(line)\n this.rerenderPersistent()\n\n return\n }\n this.unmount()\n this.renderer.event(input)\n }\n\n /**\n * Repaint the live status rows in place from a fresh summary (header + runner lines untouched).\n *\n * This is the panel's heartbeat. With no log tail on screen, a panel whose numbers never move is\n * indistinguishable from a hung process \u2014 so this is what proves the session is alive.\n */\n refresh(summary: ReadySummary): void {\n // A session still in boot has no panel to refresh.\n if (!this.persistent) {\n return\n }\n this.footerLines = this.renderer.formatFooterLines(summary)\n this.rerenderPersistent()\n }\n\n dispose(): void {\n if (this.persistent) {\n // Commit the last panel state, then release the terminal.\n this.rerenderPersistent()\n if (typeof this.stdout.off === 'function') {\n this.stdout.off('resize', this.onResize)\n }\n }\n this.finalized = true\n this.persistent = false\n this.unmount()\n }\n}\n", "import { Box, Static, Text } from 'ink'\nimport { useEffect, useState } from 'react'\n\n/**\n * The Ink boot region for `infra-kit dev`: an animated spinner + phase line while the server boots,\n * then the ready header committed to scrollback via `<Static>`.\n *\n * HARD INVARIANT \u2014 this component (and every boot component under `src/tui/dev-ui/`) is OUTPUT-ONLY.\n * It MUST NEVER call `useInput`. `useInput` arms Ink's raw mode + Ctrl-C interception, which would\n * steal SIGINT from the process signal handler that owns dev-server shutdown. The spinner animates via\n * a self-contained interval only; there is no keyboard input during boot. Do not add `useInput` here.\n */\n\nexport interface BootRegionProps {\n /** Current boot phase, shown beside the spinner. */\n phase: string\n /** Latest narration detail (dim subtitle under the spinner); empty hides the line. */\n narration: string\n /**\n * When set, boot is done: the pre-formatted ready-header lines are committed to scrollback via\n * `<Static>` and the transient spinner region is gone. The renderer unmounts right after.\n */\n readyLines?: string[]\n}\n\n/** Animated boot spinner + phase/narration, or the committed ready header once `readyLines` is set. */\nexport const BootRegion = (props: BootRegionProps) => {\n const { phase, narration, readyLines } = props\n\n const [frame, setFrame] = useState(0)\n\n useEffect(() => {\n // No spinner once the ready header is committed.\n if (readyLines) {\n return\n }\n\n const timer = setInterval(() => {\n setFrame((f) => {\n return f + 1\n })\n }, SPINNER_INTERVAL_MS)\n\n return () => {\n clearInterval(timer)\n }\n }, [readyLines])\n\n if (readyLines) {\n // `<Static>` writes each line once, above the (now empty) live region, so the header persists in\n // scrollback after unmount. Lines are pre-formatted by DevRenderer.formatReadyLines.\n return (\n <Static items={readyLines}>\n {(line, index) => {\n return <Text key={index}>{line}</Text>\n }}\n </Static>\n )\n }\n\n return (\n <Box flexDirection=\"column\">\n <Box>\n <Text color=\"cyan\">{SPINNER_FRAMES[frame % SPINNER_FRAMES.length]}</Text>\n <Text dimColor>{` ${phase}`}</Text>\n </Box>\n {narration ? <Text dimColor>{narration}</Text> : null}\n </Box>\n )\n}\n\nconst SPINNER_FRAMES = ['\u280B', '\u2819', '\u2839', '\u2838', '\u283C', '\u2834', '\u2826', '\u2827', '\u2807', '\u280F']\nconst SPINNER_INTERVAL_MS = 80\n", "import { Box, Static, Text } from 'ink'\n\n/**\n * The persistent Ink live region for `infra-kit dev` (backend-only sessions): the committed header +\n * growing log scrollback above, and a live health footer re-rendered in place below.\n *\n * HARD INVARIANT \u2014 this component (and every component under `src/tui/dev-ui/`) is OUTPUT-ONLY. It\n * MUST NEVER call `useInput`. `useInput` arms Ink's raw mode + Ctrl-C interception, which would steal\n * SIGINT from the process signal handler that owns dev-server shutdown. There is no keyboard input in\n * the live region \u2014 it is props only, with no state, no effects, no input. Do not add `useInput` here.\n */\n\nexport interface LiveRegionProps {\n /** Reference header, committed once to scrollback via `<Static>` (from `formatHeaderLines`). */\n headerLines: string[]\n /** Log lines committed above the footer as they arrive (also via the SAME `<Static>`). */\n logLines: string[]\n /** Live footer, re-rendered in place on every health change (from `formatFooterLines`). */\n footerLines: string[]\n /**\n * Cap on rendered footer rows so a many-backend session never eats a short viewport (the footer is a\n * live region \u2014 it cannot scroll). Overflow collapses into a trailing dim `\u2026 +N more` line. Omit for\n * no cap. The pinned footer must stay short; the full health picture is always in the log file.\n */\n maxFooterLines?: number\n}\n\n/**\n * A SINGLE `<Static>` streams the header then the growing logs (Ink writes only newly-appended items\n * each commit, so the header lands first and logs accrete beneath it \u2014 one block, never two). The\n * footer is an ordinary `<Box>`, so it repaints in place on each rerender without touching scrollback.\n */\nexport const LiveRegion = (props: LiveRegionProps) => {\n const { headerLines, logLines, footerLines } = props\n\n const staticItems = [...headerLines, ...logLines]\n const footer = clampFooter(footerLines, props.maxFooterLines)\n\n return (\n <>\n <Static items={staticItems}>\n {(line, index) => {\n return <Text key={index}>{line}</Text>\n }}\n </Static>\n <Box flexDirection=\"column\">\n {footer.map((line, index) => {\n return <Text key={index}>{line}</Text>\n })}\n </Box>\n </>\n )\n}\n\n/** Cap the footer to `max` rows, collapsing the overflow into one trailing ` \u2026 +N more` line. */\nexport const clampFooter = (lines: string[], max?: number): string[] => {\n if (max == null || max < 1 || lines.length <= max) {\n return lines\n }\n\n const shown = lines.slice(0, max - 1)\n\n return [...shown, ` \u2026 +${lines.length - shown.length} more`]\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": ["inkRender", "process", "Box", "Static", "Text", "useEffect", "useState", "jsx", "jsxs", "BootRegion", "props", "phase", "narration", "readyLines", "frame", "setFrame", "timer", "f", "SPINNER_INTERVAL_MS", "line", "index", "SPINNER_FRAMES", "Box", "Static", "Text", "Fragment", "jsx", "jsxs", "LiveRegion", "props", "headerLines", "logLines", "footerLines", "staticItems", "footer", "clampFooter", "line", "index", "lines", "max", "shown", "jsx", "PersistentInkDevUi", "deps", "inkRender", "createSafeStream", "process", "DevRenderer", "text", "message", "level", "readyLines", "tree", "BootRegion", "willWrite", "phase", "summary", "line", "LiveRegion", "input", "formatClock"]
|
|
7
|
-
}
|