infra-kit 0.1.131 → 0.1.133

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.
Files changed (56) hide show
  1. package/dist/boot-YCABHUPP.js +2 -0
  2. package/dist/boot-YCABHUPP.js.map +7 -0
  3. package/dist/chunk-3OMHS5RE.js +2 -0
  4. package/dist/chunk-3OMHS5RE.js.map +7 -0
  5. package/dist/chunk-6EPRH34O.js +2 -0
  6. package/dist/chunk-6EPRH34O.js.map +7 -0
  7. package/dist/{chunk-SR2GFEY7.js → chunk-6FDGBQGX.js} +2 -2
  8. package/dist/{chunk-SR2GFEY7.js.map → chunk-6FDGBQGX.js.map} +1 -1
  9. package/dist/chunk-FAR2L74S.js +2 -0
  10. package/dist/chunk-FAR2L74S.js.map +7 -0
  11. package/dist/{chunk-C2HRONYA.js → chunk-JD5T4VCB.js} +3 -3
  12. package/dist/chunk-JD5T4VCB.js.map +7 -0
  13. package/dist/chunk-RSF5XAQJ.js +4 -0
  14. package/dist/chunk-RSF5XAQJ.js.map +7 -0
  15. package/dist/{chunk-52WL2IQX.js → chunk-RXEMP3VI.js} +2 -2
  16. package/dist/{chunk-X2L4F2VM.js → chunk-V37MZDFR.js} +3 -3
  17. package/dist/{chunk-X2L4F2VM.js.map → chunk-V37MZDFR.js.map} +3 -3
  18. package/dist/chunk-WSO6DN6Q.js +220 -0
  19. package/dist/chunk-WSO6DN6Q.js.map +7 -0
  20. package/dist/cli.js +9 -3
  21. package/dist/cli.js.map +4 -4
  22. package/dist/dev-server.js +20 -15
  23. package/dist/dev-server.js.map +4 -4
  24. package/dist/{dev-wizard-run-7HEKSAS2.js → dev-wizard-run-TNA5YI7P.js} +2 -2
  25. package/dist/lib/vite/vite.d.ts +29 -10
  26. package/dist/mcp.js +1 -1
  27. package/dist/mcp.js.map +1 -1
  28. package/dist/persistent-ink-dev-ui-FUJQDFNY.js +8 -0
  29. package/dist/persistent-ink-dev-ui-FUJQDFNY.js.map +7 -0
  30. package/dist/update-check.js +1 -1
  31. package/dist/update-check.js.map +1 -1
  32. package/dist/vite.js +1 -1
  33. package/package.json +1 -1
  34. package/dist/boot-5GFZBDKL.js +0 -2
  35. package/dist/boot-5GFZBDKL.js.map +0 -7
  36. package/dist/chunk-4UUVKJYG.js +0 -2
  37. package/dist/chunk-4UUVKJYG.js.map +0 -7
  38. package/dist/chunk-6YAKVQZN.js +0 -4
  39. package/dist/chunk-6YAKVQZN.js.map +0 -7
  40. package/dist/chunk-C2HRONYA.js.map +0 -7
  41. package/dist/chunk-F6VCGS3D.js +0 -2
  42. package/dist/chunk-F6VCGS3D.js.map +0 -7
  43. package/dist/chunk-LENH44GY.js +0 -215
  44. package/dist/chunk-LENH44GY.js.map +0 -7
  45. package/dist/chunk-O3YJM6IR.js +0 -2
  46. package/dist/chunk-O3YJM6IR.js.map +0 -7
  47. package/dist/chunk-TKZINCST.js +0 -2
  48. package/dist/chunk-TKZINCST.js.map +0 -7
  49. package/dist/chunk-VJPLVBRA.js +0 -5
  50. package/dist/chunk-VJPLVBRA.js.map +0 -7
  51. package/dist/persistent-ink-dev-ui-4U4B2B7C.js +0 -8
  52. package/dist/persistent-ink-dev-ui-4U4B2B7C.js.map +0 -7
  53. package/dist/program.js +0 -2
  54. package/dist/program.js.map +0 -7
  55. /package/dist/{chunk-52WL2IQX.js.map → chunk-RXEMP3VI.js.map} +0 -0
  56. /package/dist/{dev-wizard-run-7HEKSAS2.js.map → dev-wizard-run-TNA5YI7P.js.map} +0 -0
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/dev/render.ts"],
4
+ "sourcesContent": ["/**\n * Terminal renderer for `infra-kit dev` \u2014 a calm-print layer (never a full-screen TUI) that owns\n * every line the dev-server writes to the terminal, plus the tee to the session log (`logs.txt`).\n *\n * Design (see `.omc/plans/dev-log-redesign.md`): the boot collapses into a single transient spinner;\n * the final screen leads with per-server endpoints + health; the live tail is tagged + timestamped.\n * Full detail always reaches the log file regardless of `--verbose`.\n *\n * All I/O is injected (`write` / `appendLog` / `isTTY` / `now`) so every frame is snapshot-testable\n * and the spinner is deterministically disabled in tests (`isTTY: false`). The renderer coexists with\n * a child that owns its own TTY (vite via `turbo run dev`), because that child starts only AFTER\n * {@link DevRenderer.ready} has cleared the spinner \u2014 the spinner is never live concurrently with\n * inherited child output.\n */\nimport process from 'node:process'\n\nimport type { DevUi } from './dev-ui.js'\n\nexport type LogLevel = 'info' | 'warn' | 'error' | 'debug'\n\n/** Injected I/O seams. Defaults wire to the real stdout + a caller-provided file appender. */\nexport interface DevRendererDeps {\n /** Terminal sink (default: `process.stdout.write`). */\n write: (text: string) => void\n /** File tee \u2014 always receives full detail (default: no-op; the runner passes its log appender). */\n appendLog: (text: string) => void\n /** Whether the terminal is a TTY. Gates the spinner + ANSI color; false in CI / when piped. */\n isTTY: boolean\n /** Clock seam (default: `() => new Date()`), so timestamps + `ready in Xs` are testable. */\n now: () => Date\n /** When true, boot narration reaches the terminal; otherwise it is file-only. */\n verbose: boolean\n}\n\n/** One resolved backend endpoint row in the ready header. */\nexport interface EndpointRow {\n /** Stream tag, e.g. `client/api`. */\n tag: string\n /** The app's `.localhost` alias URL \u2014 the only form, since an app that cannot be aliased never starts. */\n url: string\n /** Liveness: `true` \u2192 `\u25CF ok`, `false` \u2192 `\u25CF down`, `null` \u2192 no dot (not probed). */\n healthy: boolean | null\n}\n\n/**\n * A UI app whose URL the runner does not own, so it streams its own below \u2014 referenced, never given a\n * (fabricated) endpoint row. The fallback case only: a UI whose port the runner pre-assigned is an\n * {@link EndpointRow}, proxy or no proxy.\n */\nexport interface UiRef {\n /** Stream tag, e.g. `client/ui`. */\n tag: string\n}\n\n/**\n * An app that was asked for but never came up. It has no URL and no health \u2014 but it MUST still get a\n * row: the header is a report of what the user asked for, and an app that silently vanishes from it\n * reads as \"not requested\" rather than \"broken\".\n */\nexport interface FailedRow {\n /** Stream tag, e.g. `client/api`. */\n tag: string\n /** One-line reason, e.g. `config is missing field: 'connectionURL'`. */\n reason: string\n}\n\n/** Everything {@link DevRenderer.ready} needs to paint the final header in one shot. */\nexport interface ReadySummary {\n /** Resolved preset / target label (e.g. `client`, `*`). */\n target: string\n watch: boolean\n /**\n * True when this session launches a `turbo run dev` UI child that inherits the TTY. It is the\n * authoritative \"is this a UI session?\" signal \u2014 NOT `uiRefs.length > 0`. A UI whose port the runner\n * pre-assigned becomes an {@link EndpointRow}, not a {@link UiRef}, so `uiRefs` is normally empty even\n * for a UI session; any live region painted then would scribble into vite's inherited output. The\n * scroll-region footer (which tolerates the child) is chosen on this flag instead.\n */\n hasUiChild: boolean\n /** Slugified release for the header meta; omitted outside a git repo. */\n release?: string\n /** Backend readiness time in ms (UI is fire-and-forget, so this is BE-only \u2014 labeled honestly). */\n elapsedMs: number\n /** Real backend endpoint rows (owned ports + pre-probed health). */\n endpoints: EndpointRow[]\n /** UI apps that print their own URL below. */\n uiRefs: UiRef[]\n /**\n * Apps that failed to start. Optional only so the (many) existing summaries need not restate an\n * empty list; a non-empty one downgrades the title from a green `ready` to an honest `N failed`.\n */\n failed?: FailedRow[]\n /** Human watch summary, e.g. `1 app \u00B7 5 packages`; omitted when not watching. */\n watchSummary?: string\n /** Compact, human-readable log path shown as the `logs \u2192 \u2026` label (e.g. `~/.cache/infra-kit/<session>/logs.txt`). */\n logPath: string\n /** Absolute log path backing the clickable OSC-8 hyperlink (wrapped as `file://<logHref>`). */\n logHref: string\n}\n\nconst SPINNER_FRAMES = ['\u280B', '\u2819', '\u2839', '\u2838', '\u283C', '\u2834', '\u2826', '\u2827', '\u2807', '\u280F']\nconst SPINNER_INTERVAL_MS = 80\n\nconst ANSI = {\n reset: '\\x1B[0m',\n dim: '\\x1B[2m',\n bold: '\\x1B[1m',\n teal: '\\x1B[36m',\n green: '\\x1B[32m',\n blue: '\\x1B[34m',\n red: '\\x1B[31m',\n} as const\n\n/** Erase the current line and return the cursor to column 0 (used to clear the spinner). */\nconst CLEAR_LINE = '\\r\\x1B[2K'\n\n/**\n * SGR color codes (`ESC [ \u2026 m`) and OSC-8 hyperlink wrappers (`ESC ] 8 ; ; \u2026 ESC \\`), non-greedy.\n * Matching the ESC control character is the whole point here \u2014 this pattern exists to remove it.\n */\n// eslint-disable-next-line sonarjs/no-control-regex, no-control-regex\nconst ANSI_PATTERN = /\\x1B\\[[0-9;]*m|\\x1B\\]8;;.*?\\x1B\\\\/g\n\n/**\n * Strip SGR colors + OSC-8 hyperlink escapes, keeping the hyperlink's visible label. Applied at the\n * file-tee seam so `logs.txt` stays greppable plain text even though the terminal frames it renders\n * from are colored + hyperlinked (both renderers format once, for a TTY, and tee the same string).\n */\nexport const stripAnsi = (s: string): string => {\n return s.replace(ANSI_PATTERN, '')\n}\n\n/** Zero-pad to 2 digits for `HH:MM:SS`. */\nconst pad2 = (n: number): string => {\n return String(n).padStart(2, '0')\n}\n\n/** `HH:MM:SS` for a tail timestamp. */\nexport const formatClock = (d: Date): string => {\n return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`\n}\n\n/** `2.4s` for the `ready in \u2026` header. */\nexport const formatElapsed = (ms: number): string => {\n return `${(ms / 1000).toFixed(1)}s`\n}\n\n/**\n * Resolve the endpoint URL for an app: `https://<alias><prefix>`, where `alias` is the portless hostname\n * the runner registered (e.g. `feat-x.backend-api.localhost`). The runner refuses to start an app it could\n * not alias, so there is no port-form to fall back to.\n *\n * There is no port and no scheme choice, by construction. The proxy serves TLS on 443 \u2014 the implicit HTTPS\n * port \u2014 so the URL is byte-identical to the `dev.proxy` local template that `infra-kit/vite` proxies to,\n * and what the table prints cannot drift from what the frontend calls. Any port suffix here would mean the\n * proxy is not on 443, which `ensureProxy` has already refused to start.\n */\nexport const resolveEndpointUrl = (input: { prefixUrl: string; alias: string }): string => {\n return `https://${input.alias}${input.prefixUrl}`\n}\n\nexport class DevRenderer implements DevUi {\n private readonly deps: DevRendererDeps\n private spinnerTimer: ReturnType<typeof setInterval> | null = null\n private spinnerPhase = ''\n private spinnerFrame = 0\n\n constructor(deps: Partial<DevRendererDeps> = {}) {\n this.deps = {\n write:\n deps.write ??\n ((text: string): void => {\n process.stdout.write(text)\n }),\n appendLog: deps.appendLog ?? ((): void => {}),\n isTTY: deps.isTTY ?? Boolean(process.stdout.isTTY),\n now:\n deps.now ??\n ((): Date => {\n return new Date()\n }),\n verbose: deps.verbose ?? false,\n }\n }\n\n /** Wrap `s` in an ANSI color on a TTY; return it untouched when piped (deterministic snapshots). */\n private color(code: string, s: string): string {\n return this.deps.isTTY ? `${code}${s}${ANSI.reset}` : s\n }\n\n /**\n * Wrap `label` in an OSC-8 terminal hyperlink to `href` (a filesystem path \u2192 `file://` URI) so the\n * log path is cmd/ctrl-clickable. Returns the bare `label` when piped (deterministic snapshots) or\n * when `href` is empty \u2014 terminals without OSC-8 support silently ignore the escapes anyway.\n */\n private hyperlink(href: string, label: string): string {\n if (!this.deps.isTTY || href === '') return label\n\n // `encodeURI` escapes spaces / non-ASCII but leaves `#` and `?`, which a file URI reads as a\n // fragment / query \u2014 encode those too so a path containing them still resolves to the real file.\n const uri = encodeURI(href).replace(/[#?]/g, (c) => {\n return `%${c.charCodeAt(0).toString(16).toUpperCase()}`\n })\n\n return `\\x1B]8;;file://${uri}\\x1B\\\\${label}\\x1B]8;;\\x1B\\\\`\n }\n\n /**\n * Append one line to the log file ONLY \u2014 the canonical `[iso] [LEVEL] msg` shape, no terminal write.\n * The single source of the file-tee format: the private {@link tee} and the Ink boot UI both route\n * here, so the log file stays identical whichever renderer is live.\n */\n teeOnly(message: string, level: LogLevel): void {\n this.deps.appendLog(`[${this.deps.now().toISOString()}] [${level.toUpperCase()}] ${stripAnsi(message)}\\n`)\n }\n\n /** File tee for a line that is ALSO written to the terminal; delegates the format to {@link teeOnly}. */\n private tee(message: string, level: LogLevel): void {\n this.teeOnly(message, level)\n }\n\n /**\n * No-op for the plain renderer: it owns no Ink / raw-mode terminal state to release. Present to\n * satisfy {@link DevUi} so the runner can call `dispose()` uniformly. Idempotent by construction.\n */\n dispose(): void {\n // Nothing to release \u2014 the plain renderer never seizes the terminal.\n }\n\n /**\n * Write one terminal line spinner-safely: if the spinner is live on a TTY, erase it, print the\n * line, then repaint the spinner, so a mid-boot message never shreds the `\\r`-based spinner.\n */\n private emit(line: string): void {\n if (this.spinnerTimer != null && this.deps.isTTY) {\n this.deps.write(`${CLEAR_LINE}${line}\\n`)\n this.paintSpinner()\n\n return\n }\n\n this.deps.write(`${line}\\n`)\n }\n\n /** A general message routed by level (terminal + file tee). Debug is terminal-only in verbose. */\n log(message: string, level: LogLevel = 'info'): void {\n if (level !== 'debug' || this.deps.verbose) {\n this.emit(message)\n }\n this.tee(message, level)\n }\n\n /** A boot-narration step: terminal only when `--verbose`, but always tee'd to the log. */\n narrate(message: string): void {\n if (this.deps.verbose) {\n this.emit(message)\n }\n this.tee(message, 'info')\n }\n\n /** `LogFn`-shaped adapter for the build runner seam (which passes `(msg, level)`). */\n readonly logFn = (message: string, level: LogLevel = 'info'): void => {\n this.log(message, level)\n }\n\n // ---- boot spinner -------------------------------------------------------\n\n /** Paint the current spinner frame in place (TTY only). */\n private paintSpinner(): void {\n const frame = SPINNER_FRAMES[this.spinnerFrame % SPINNER_FRAMES.length]!\n\n this.deps.write(`${CLEAR_LINE}${this.color(ANSI.teal, frame)} ${this.color(ANSI.dim, this.spinnerPhase)}`)\n }\n\n /**\n * Update the boot phase. On a TTY this drives a single transient spinner line; when piped it\n * prints one plain phase line (no ANSI, deterministic). Always tee'd to the log.\n */\n bootStep(phase: string): void {\n this.tee(phase, 'info')\n this.spinnerPhase = phase\n\n if (!this.deps.isTTY) {\n this.deps.write(`${phase}\\n`)\n\n return\n }\n\n if (this.spinnerTimer == null) {\n this.spinnerTimer = setInterval(() => {\n this.spinnerFrame += 1\n this.paintSpinner()\n }, SPINNER_INTERVAL_MS)\n // Never keep the event loop alive for the spinner alone.\n this.spinnerTimer.unref?.()\n }\n this.paintSpinner()\n }\n\n /** Stop + erase the spinner line (idempotent). Called before the ready header prints. */\n stopSpinner(): void {\n if (this.spinnerTimer != null) {\n clearInterval(this.spinnerTimer)\n this.spinnerTimer = null\n }\n if (this.deps.isTTY) {\n this.deps.write(CLEAR_LINE)\n }\n }\n\n // ---- the ready header ---------------------------------------------------\n\n /** Format the health dot for an endpoint row (`\u25CF ok` / `\u25CF down` / '' when unprobed). */\n private healthDot(healthy: boolean | null): string {\n if (healthy === null) return ''\n if (healthy) return this.color(ANSI.green, '\u25CF ok')\n\n return this.color(ANSI.red, '\u25CF down')\n }\n\n /**\n * Collapse the boot and print the final header in one shot: title line, one endpoint row per\n * backend server (with health dot), a reference line per UI app, the watch line, the clickable\n * log path, and a separator rule. Synchronous \u2014 health is pre-probed by the caller.\n */\n ready(summary: ReadySummary): void {\n this.stopSpinner()\n\n const lines = this.formatReadyLines(summary)\n\n for (const line of lines) {\n this.emit(line)\n this.tee(line, 'info')\n }\n }\n\n /** Column width for the aligned `tag` gutter \u2014 the widest endpoint/UI/failed tag. */\n private tagWidth(summary: ReadySummary): number {\n const tags = [\n ...summary.endpoints.map((e) => {\n return e.tag\n }),\n ...summary.uiRefs.map((u) => {\n return u.tag\n }),\n ...(summary.failed ?? []).map((f) => {\n return f.tag\n }),\n ]\n\n return tags.reduce((w, t) => {\n return Math.max(w, t.length)\n }, 0)\n }\n\n /**\n * The `infra-kit dev \u00B7 <meta> ready in Xs` title line.\n *\n * A green `ready` is a claim about the whole session, so it is spent only when the whole session\n * is up. With anything in `failed` the status turns red and counts the casualties instead \u2014 the\n * boot time alone, next to a green word, is exactly how a half-dead session used to pass for a\n * healthy one.\n */\n private titleLine(summary: ReadySummary): string {\n const meta = [summary.target, summary.watch ? 'watch' : null, summary.release]\n .filter((s): s is string => {\n return Boolean(s)\n })\n .join(' \u00B7 ')\n\n const failedCount = summary.failed?.length ?? 0\n const elapsed = formatElapsed(summary.elapsedMs)\n const status =\n failedCount > 0\n ? this.color(ANSI.red, `${failedCount} failed \u00B7 started in ${elapsed}`)\n : this.color(ANSI.green, `ready in ${elapsed}`)\n\n return ` ${this.color(ANSI.bold, 'infra-kit dev')} ${this.color(ANSI.dim, meta)} ${status}`\n }\n\n /** The static legend tail: watch line + clickable log path, then a separator rule. */\n private legendLines(summary: ReadySummary): string[] {\n const watchText = summary.watch && summary.watchSummary ? `watching ${summary.watchSummary}` : 'watch off'\n const logLink = this.hyperlink(summary.logHref, summary.logPath)\n const watchLine = `${watchText} logs \u2192 ${logLink}`\n const rule = '\u2500'.repeat(60)\n\n return [` ${this.color(ANSI.dim, watchLine)}`, ` ${this.color(ANSI.dim, rule)}`]\n }\n\n /** A UI reference row (`client/ui \u2192 starting below \u2026`) \u2014 used only when infra-kit could not claim its port. */\n private uiRefLine(tag: string, tagWidth: number): string {\n return ` ${this.color(ANSI.teal, tag.padEnd(tagWidth))} ${this.color(\n ANSI.dim,\n '\u2192 starting below (vite prints its URL)',\n )}`\n }\n\n /** One endpoint row (`client/api http://\u2026`); appends a health dot only when `withHealthDot` and the endpoint is probed. */\n private endpointLine(endpoint: ReadySummary['endpoints'][number], tagWidth: number, withHealthDot: boolean): string {\n const dot = withHealthDot ? this.healthDot(endpoint.healthy) : ''\n const dotSuffix = dot ? ` ${dot}` : ''\n\n return ` ${this.color(ANSI.teal, endpoint.tag.padEnd(tagWidth))} ${this.color(ANSI.blue, endpoint.url)}${dotSuffix}`\n }\n\n /** One failed row (`client/api \u25CF failed <reason>`) \u2014 no URL, because there is nothing listening. */\n private failedLine(failed: FailedRow, tagWidth: number): string {\n return ` ${this.color(ANSI.teal, failed.tag.padEnd(tagWidth))} ${this.color(\n ANSI.red,\n '\u25CF failed',\n )} ${this.color(ANSI.dim, failed.reason)}`\n }\n\n /**\n * Shared header layout: blank, title line, blank, one endpoint row per backend, a reference row per\n * UI app, then the watch/log legend. `withHealthDot` is the ONLY difference between the static Ink\n * header (false \u2014 health is live, the footer owns it) and the ready/boot frame (true).\n */\n private formatLines(summary: ReadySummary, { withHealthDot }: { withHealthDot: boolean }): string[] {\n const tagWidth = this.tagWidth(summary)\n const lines: string[] = ['', this.titleLine(summary), '']\n\n for (const e of summary.endpoints) {\n lines.push(this.endpointLine(e, tagWidth, withHealthDot))\n }\n // Failures sit with the live rows, not in the scrollback above: the log line announcing the crash\n // is metres up the terminal by now, and the header is the only thing the user actually reads.\n for (const f of summary.failed ?? []) {\n lines.push(this.failedLine(f, tagWidth))\n }\n for (const u of summary.uiRefs) {\n lines.push(this.uiRefLine(u.tag, tagWidth))\n }\n\n lines.push('', ...this.legendLines(summary))\n\n return lines\n }\n\n /**\n * Static reference lines for the persistent Ink header, committed ONCE via `<Static>`: title,\n * endpoint URLs WITHOUT the health dot (health is live \u2192 the footer owns it), UI reference rows,\n * watch line, clickable log path, separator. Pure; colors follow `isTTY`.\n */\n formatHeaderLines(summary: ReadySummary): string[] {\n return this.formatLines(summary, { withHealthDot: false })\n }\n\n /**\n * Live status lines for the persistent Ink footer, re-rendered in place as health flips (never\n * committed to `<Static>`): one health row per PROBED backend endpoint (`client/api \u25CF ok`).\n * Unprobed endpoints (`healthy === null`) are omitted. Pure; colors follow `isTTY`.\n */\n formatFooterLines(summary: ReadySummary): string[] {\n const tagWidth = this.tagWidth(summary)\n\n return summary.endpoints\n .filter((e) => {\n return e.healthy !== null\n })\n .map((e) => {\n return ` ${this.color(ANSI.teal, e.tag.padEnd(tagWidth))} ${this.healthDot(e.healthy)}`\n })\n }\n\n /**\n * Build the ready-header lines with NO side effects (no terminal write, no tee), so both\n * {@link ready} (which writes + tees them) and the Ink boot UI (which commits them via `<Static>`\n * and tee-only's them) share one layout. Colors follow `isTTY` exactly like every other line.\n */\n formatReadyLines(summary: ReadySummary): string[] {\n return this.formatLines(summary, { withHealthDot: true })\n }\n\n // ---- the live tail ------------------------------------------------------\n\n /**\n * One tagged, timestamped tail line: `14:02:11 client/api GET /api/v1/ping 200 12ms`. Arrives\n * strictly after {@link ready} (a request implies a running server), so it never interleaves the\n * header. Timestamp comes from the injected clock.\n */\n event(input: { tag: string; text: string }): void {\n const ts = formatClock(this.deps.now())\n const line = ` ${this.color(ANSI.dim, ts)} ${this.color(ANSI.teal, input.tag)} ${input.text}`\n\n this.emit(line)\n this.tee(`${input.tag} ${input.text}`, 'info')\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["process", "SPINNER_FRAMES", "SPINNER_INTERVAL_MS", "ANSI", "CLEAR_LINE", "ANSI_PATTERN", "stripAnsi", "pad2", "n", "formatClock", "d", "formatElapsed", "ms", "resolveEndpointUrl", "input", "DevRenderer", "deps", "text", "code", "s", "href", "label", "c", "message", "level", "line", "frame", "phase", "healthy", "summary", "lines", "e", "u", "f", "w", "t", "meta", "failedCount", "elapsed", "status", "watchText", "logLink", "watchLine", "rule", "tag", "tagWidth", "endpoint", "withHealthDot", "dot", "dotSuffix", "failed", "ts"]
7
+ }
@@ -0,0 +1,4 @@
1
+ import{O as E,Q as C,c as b,i as v,v as u}from"./chunk-V37MZDFR.js";import{a as R}from"./chunk-6FU2TRU5.js";import L from"node:process";import{$ as g}from"zx";var W=async()=>{try{return await g`cmux --version`.quiet(),!0}catch{return!1}},O=async e=>{let{cwd:t,title:r,layout:s}=e,o=JSON.stringify(s),i=(await g({env:{...L.env,CMUX_QUIET:"1"}})`cmux new-workspace --name ${r} --cwd ${t} --focus false --layout ${o}`).stdout;return $(i)},N=async e=>{try{await g`cmux close-workspace --workspace ${e}`.quiet()}catch(t){u.debug({error:t,ref:e},"cmux: skipped closing dev workspace")}},$=e=>{let t=e.match(/workspace:\d+/);if(!t)throw new Error("cmux: could not locate workspace ref in new-workspace output");return t[0]};var _=/\bv(\d+\.\d+\.\d+)\b/g,l=e=>e.trim().replace(/\s+/g," ").replace(_,"$1");import{$ as T}from"zx";var D=async e=>{try{let t=(await T`cmux list-workspaces`.quiet()).stdout,r=I(t,e);if(!r)return;await T`cmux close-workspace --workspace ${r}`.quiet()}catch(t){u.debug({error:t,title:e},"cmux: skipped closing workspace")}},I=(e,t)=>{let r=l(t);for(let s of e.split(`
2
+ `)){let o=s.match(/^[* ]\s*(workspace:\d+)\s+(.+?)(?:\s+\[selected\])?\s*$/);if(!o)continue;let n=o[1],i=o[2]?.trim()??"";if(l(i)===r)return n}};import{$ as M}from"zx";var B=async()=>{try{let e=(await M`cmux list-workspaces`.quiet()).stdout,t=new Set;for(let r of e.split(`
3
+ `)){let s=r.match(/^[* ]\s*workspace:\d+\s+(.+?)(?:\s+\[selected\])?\s*$/);if(!s)continue;let o=s[1]?.trim();o&&t.add(l(o))}return t}catch(e){return u.debug({error:e},"cmux: skipped listing workspace titles"),new Set}};import{$ as p}from"zx";var U=async e=>{let{cwd:t,title:r}=e,s=E(await C()),o=(await p`cmux workspace create --cwd ${t}`).stdout,n=z(o),i=(await p`cmux list-pane-surfaces --workspace ${n}`).stdout,c=q(i);await p`cmux new-split right --workspace ${n} --surface ${c}`,s==="three-pane"&&await p`cmux new-split down --workspace ${n} --surface ${c}`,r&&await p`cmux workspace rename --workspace ${n} --title ${r}`},q=e=>{let t=e.match(/surface:\d+/);if(!t)throw new Error("cmux: could not locate initial surface in list-pane-surfaces output");return t[0]},z=e=>{let t=e.match(/workspace:\d+/);if(!t)throw new Error("cmux: could not locate workspace ref in workspace create output");return t[0]};var F=e=>{let{repoName:t,branch:r}=e,s=b(r),o=s?v(s):r;return`${t} ${o}`};var J=new Set(["ExitPromptError","AbortPromptError","PromptCancelledError"]),S=e=>e instanceof Error&&J.has(e.name),qe=e=>{if(S(e))return!0;let t=e?.cause;return S(t)};import{execFile as K}from"node:child_process";import{createHash as j}from"node:crypto";import{existsSync as H,readFileSync as f}from"node:fs";import V from"node:http";import Q from"node:https";import X from"node:net";import{homedir as G}from"node:os";import{dirname as x,join as m}from"node:path";import k from"node:process";import Y from"node:tls";import{fileURLToPath as Z}from"node:url";import{promisify as ee}from"node:util";var te=ee(K),re=e=>{let t=JSON.parse(f(e,"utf-8")),r=typeof t.bin=="string"?t.bin:t.bin?.portless;return r==null||r===""?null:r},oe=()=>{try{let e=x(Z(import.meta.url));for(;;){let t=m(e,"node_modules","portless","package.json");if(H(t)){let s=re(t);return s==null?null:m(x(t),s)}let r=x(e);if(r===e)return null;e=r}}catch{return null}},w,se=()=>(w===void 0&&(w=oe()),w),ne=1500,y=1500,ie="x-portless",h="127.0.0.1",ae="localhost",ce=e=>new Promise(t=>{let r=X.connect({host:h,port:e}),s=o=>{r.destroy(),t(o)};r.setTimeout(y),r.once("connect",()=>{s(!0)}),r.once("timeout",()=>{s(!1)}),r.once("error",()=>{s(!1)})}),ue=(e,t)=>new Promise(r=>{let o=(t?Q.request:V.request)({host:h,port:e,method:"HEAD",path:"/",timeout:y,...t?{rejectUnauthorized:!1,servername:ae}:{}},n=>{n.resume(),r(n.headers[ie]==="1")});o.on("error",()=>{r(!1)}),o.on("timeout",()=>{o.destroy(),r(!1)}),o.end()}),le=async(e,{timeoutMs:t})=>{let r=se();if(r==null)throw new Error("portless is not installed (not resolvable from node_modules)");await te(k.execPath,[r,...e],{signal:AbortSignal.timeout(t),encoding:"utf-8",env:R(k.env)})},P=()=>k.env.PORTLESS_STATE_DIR??m(G(),".portless"),me="ca.pem",pe="ca.trusted",A=()=>m(P(),me),rt=()=>{try{let e=f(m(P(),pe),"utf-8").trim();return e===""?!1:j("sha256").update(f(A())).digest("hex")===e.toLowerCase()}catch{return!1}},ot=(e,t)=>new Promise(r=>{let s;try{s=f(A())}catch{r({ok:!1,code:"ENOENT"});return}let o=Y.connect({host:h,port:e,servername:t,ca:[s],rejectUnauthorized:!0},()=>{let n=o.authorizationError,i=o.authorized;o.destroy(),r(i?{ok:!0}:{ok:!1,code:n?.code??n?.message??"UNKNOWN"})});o.setTimeout(y),o.once("timeout",()=>{o.destroy(),r({ok:!1,code:"ETIMEDOUT"})}),o.once("error",n=>{o.destroy(),r({ok:!1,code:n.code??"UNKNOWN"})})}),st=()=>{try{let e=JSON.parse(f(m(P(),"routes.json"),"utf-8"));return Array.isArray(e)?e.flatMap(t=>{let{hostname:r,port:s}=t??{};return typeof r!="string"||r===""||typeof s!="number"?[]:[{name:r,port:s}]}):[]}catch{return[]}},nt=(e={})=>{let t=e.run??le,r=e.isListening??ce,s=e.isProxyServing??ue,o=e.timeoutMs??ne,n=null,i=async a=>{try{return await t(a,{timeoutMs:o}),!0}catch{return!1}},c=async()=>(n??=await i(["--version"]),n);return{isAvailable:c,isProxyServing:async(a,d)=>!await c()||!await r(a)?!1:s(a,d),registerAlias:async(a,d)=>await c()?i(["alias",a,String(d)]):!1,removeAlias:async a=>{await c()&&await i(["alias","--remove",a])}}};export{oe as a,ce as b,ue as c,A as d,rt as e,ot as f,st as g,nt as h,l as i,D as j,B as k,W as l,O as m,N as n,U as o,F as p,qe as q};
4
+ //# sourceMappingURL=chunk-RSF5XAQJ.js.map
@@ -0,0 +1,7 @@
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\n * (`sudo portless service install`).\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/** 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 (`sudo portless service install` vs the sudo-free `portless trust`). Trust is a\n * separate, 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 /** 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 (`sudo portless service install`). A `false` here is turned\n * into a 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 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 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 { isAvailable, isProxyServing: serving, registerAlias, removeAlias }\n}\n"],
5
+ "mappings": "4GAAA,OAAOA,MAAa,eACpB,OAAS,KAAAC,MAAS,KAgBX,IAAMC,EAAkB,SAA8B,CAC3D,GAAI,CACF,aAAMC,kBAAkB,MAAM,EAEvB,EACT,MAAQ,CACN,MAAO,EACT,CACF,EAOaC,EAAuB,MAAOC,GAAoD,CAC7F,GAAM,CAAE,IAAAC,EAAK,MAAAC,EAAO,OAAAC,CAAO,EAAIH,EACzBI,EAAa,KAAK,UAAUD,CAAM,EAGlCE,GAAU,MADFP,EAAE,CAAE,IAAK,CAAE,GAAGQ,EAAQ,IAAK,WAAY,GAAI,CAAE,CAAC,8BACJJ,CAAK,UAAUD,CAAG,2BAA2BG,CAAU,IAC5G,OAEH,OAAOG,EAAkBF,CAAM,CACjC,EAOaG,EAAwB,MAAOC,GAA+B,CACzE,GAAI,CACF,MAAMX,qCAAqCW,CAAG,GAAG,MAAM,CACzD,OAASC,EAAO,CACdC,EAAO,MAAM,CAAE,MAAAD,EAAO,IAAAD,CAAI,EAAG,qCAAqC,CACpE,CACF,EASMF,EAAqBF,GAA2B,CACpD,IAAMO,EAAQP,EAAO,MAAM,eAAe,EAE1C,GAAI,CAACO,EACH,MAAM,IAAI,MAAM,8DAA8D,EAGhF,OAAOA,EAAM,CAAC,CAChB,ECtEA,IAAMC,EAAoB,wBA2BbC,EAAyBC,GAC7BA,EAAI,KAAK,EAAE,QAAQ,OAAQ,GAAG,EAAE,QAAQF,EAAmB,IAAI,EC7BxE,OAAS,KAAAG,MAAS,KAWX,IAAMC,EAA4B,MAAOC,GAAiC,CAC/E,GAAI,CACF,IAAMC,GAAc,MAAMC,wBAAwB,MAAM,GAAG,OAErDC,EAAMC,EAAwBH,EAAYD,CAAK,EAErD,GAAI,CAACG,EACH,OAGF,MAAMD,qCAAqCC,CAAG,GAAG,MAAM,CACzD,OAASE,EAAO,CACdC,EAAO,MAAM,CAAE,MAAAD,EAAO,MAAAL,CAAM,EAAG,iCAAiC,CAClE,CACF,EAaMI,EAA0B,CAACG,EAAgBP,IAAsC,CACrF,IAAMQ,EAASC,EAAsBT,CAAK,EAE1C,QAAWU,KAAWH,EAAO,MAAM;AAAA,CAAI,EAAG,CAExC,IAAMI,EAAQD,EAAQ,MAAM,yDAAyD,EAErF,GAAI,CAACC,EACH,SAGF,IAAMR,EAAMQ,EAAM,CAAC,EACbC,EAAYD,EAAM,CAAC,GAAG,KAAK,GAAK,GAEtC,GAAIF,EAAsBG,CAAS,IAAMJ,EACvC,OAAOL,CAEX,CAGF,EC1DA,OAAS,KAAAU,MAAS,KAmBX,IAAMC,EAA0B,SAAkC,CACvE,GAAI,CACF,IAAMC,GAAU,MAAMC,wBAAwB,MAAM,GAAG,OAEjDC,EAAS,IAAI,IAEnB,QAAWC,KAAWH,EAAO,MAAM;AAAA,CAAI,EAAG,CAExC,IAAMI,EAAQD,EAAQ,MAAM,uDAAuD,EAEnF,GAAI,CAACC,EACH,SAGF,IAAMC,EAAQD,EAAM,CAAC,GAAG,KAAK,EAEzBC,GACFH,EAAO,IAAII,EAAsBD,CAAK,CAAC,CAE3C,CAEA,OAAOH,CACT,OAASK,EAAO,CACd,OAAAC,EAAO,MAAM,CAAE,MAAAD,CAAM,EAAG,wCAAwC,EAEzD,IAAI,GACb,CACF,EC9CA,OAAS,KAAAE,MAAS,KAiBX,IAAMC,EAA8B,MAAOC,GAA+C,CAC/F,GAAM,CAAE,IAAAC,EAAK,MAAAC,CAAM,EAAIF,EAEjBG,EAASC,EAAkB,MAAMC,EAAkB,CAAC,EAEpDC,GAAsB,MAAMC,gCAAgCN,CAAG,IAAI,OAEnEO,EAAeC,EAAkBH,CAAkB,EAEnDI,GAAkB,MAAMH,wCAAwCC,CAAY,IAAI,OAEhFG,EAAaC,EAAqBF,CAAc,EAItD,MAAMH,qCAAqCC,CAAY,cAAcG,CAAU,GAE3ER,IAAW,cACb,MAAMI,oCAAoCC,CAAY,cAAcG,CAAU,GAG5ET,GACF,MAAMK,sCAAsCC,CAAY,YAAYN,CAAK,EAE7E,EAWMU,EAAwBC,GAA2B,CACvD,IAAMC,EAAQD,EAAO,MAAM,aAAa,EAExC,GAAI,CAACC,EACH,MAAM,IAAI,MAAM,qEAAqE,EAGvF,OAAOA,EAAM,CAAC,CAChB,EAWML,EAAqBI,GAA2B,CACpD,IAAMC,EAAQD,EAAO,MAAM,eAAe,EAE1C,GAAI,CAACC,EACH,MAAM,IAAI,MAAM,iEAAiE,EAGnF,OAAOA,EAAM,CAAC,CAChB,EChEO,IAAMC,EAA2BC,GAA8C,CACpF,GAAM,CAAE,SAAAC,EAAU,OAAAC,CAAO,EAAIF,EAEvBG,EAAKC,EAAgBF,CAAM,EAC3BG,EAAQF,EAAKG,EAAaH,CAAE,EAAID,EAEtC,MAAO,GAAGD,CAAQ,IAAII,CAAK,EAC7B,ECdA,IAAME,EAA2B,IAAI,IAAI,CAAC,kBAAmB,mBAAoB,sBAAsB,CAAC,EAElGC,EAAuBC,GACpBA,aAAiB,OAASF,EAAyB,IAAIE,EAAM,IAAI,EAsB7DC,GAAwBC,GAA4B,CAC/D,GAAIH,EAAoBG,CAAK,EAAG,MAAO,GAEvC,IAAMC,EAASD,GAAkD,MAEjE,OAAOH,EAAoBI,CAAK,CAClC,ECbA,OAAS,YAAAC,MAAgB,qBACzB,OAAS,cAAAC,MAAkB,cAC3B,OAAS,cAAAC,EAAY,gBAAAC,MAAoB,UACzC,OAAOC,MAAU,YACjB,OAAOC,MAAW,aAClB,OAAOC,MAAS,WAChB,OAAS,WAAAC,MAAe,UACxB,OAAS,WAAAC,EAAS,QAAAC,MAAY,YAC9B,OAAOC,MAAa,eACpB,OAAOC,MAAS,WAChB,OAAS,iBAAAC,MAAqB,WAC9B,OAAS,aAAAC,OAAiB,YAI1B,IAAMC,GAAgBC,GAAUC,CAAQ,EAGlCC,GAAcC,GAAuC,CACzD,IAAMC,EAAM,KAAK,MAAMC,EAAaF,EAAa,OAAO,CAAC,EACnDG,EAAM,OAAOF,EAAI,KAAQ,SAAWA,EAAI,IAAMA,EAAI,KAAK,SAE7D,OAAOE,GAAO,MAAQA,IAAQ,GAAK,KAAOA,CAC5C,EAYaC,GAAqB,IAAqB,CACrD,GAAI,CACF,IAAIC,EAAMC,EAAQC,EAAc,YAAY,GAAG,CAAC,EAEhD,OAAS,CACP,IAAMP,EAAcQ,EAAKH,EAAK,eAAgB,WAAY,cAAc,EAExE,GAAII,EAAWT,CAAW,EAAG,CAC3B,IAAMG,EAAMJ,GAAWC,CAAW,EAElC,OAAOG,GAAO,KAAO,KAAOK,EAAKF,EAAQN,CAAW,EAAGG,CAAG,CAC5D,CACA,IAAMO,EAASJ,EAAQD,CAAG,EAE1B,GAAIK,IAAWL,EAAK,OAAO,KAC3BA,EAAMK,CACR,CACF,MAAQ,CACN,OAAO,IACT,CACF,EAGIC,EACEC,GAAc,KACdD,IAAc,SAAWA,EAAYP,GAAmB,GAErDO,GAeHE,GAAqB,KACrBC,EAAmB,KAGnBC,GAAkB,aAGlBC,EAAW,YAQXC,GAAmB,YAEZC,GAAmCC,GACvC,IAAI,QAASC,GAAY,CAC9B,IAAMC,EAASC,EAAI,QAAQ,CAAE,KAAMN,EAAU,KAAAG,CAAK,CAAC,EAC7CI,EAAUC,GAA0B,CACxCH,EAAO,QAAQ,EACfD,EAAQI,CAAM,CAChB,EAEAH,EAAO,WAAWP,CAAgB,EAClCO,EAAO,KAAK,UAAW,IAAM,CAC3BE,EAAO,EAAI,CACb,CAAC,EACDF,EAAO,KAAK,UAAW,IAAM,CAC3BE,EAAO,EAAK,CACd,CAAC,EACDF,EAAO,KAAK,QAAS,IAAM,CACzBE,EAAO,EAAK,CACd,CAAC,CACH,CAAC,EAqBUE,GAAwC,CAACN,EAAMO,IACnD,IAAI,QAASN,GAAY,CAE9B,IAAMO,GADUD,EAAME,EAAM,QAAUC,EAAK,SAEzC,CACE,KAAMb,EACN,KAAAG,EACA,OAAQ,OACR,KAAM,IACN,QAASL,EACT,GAAIY,EAAM,CAAE,mBAAoB,GAAO,WAAYT,EAAiB,EAAI,CAAC,CAC3E,EACCa,GAAQ,CACPA,EAAI,OAAO,EACXV,EAAQU,EAAI,QAAQf,EAAe,IAAM,GAAG,CAC9C,CACF,EAEAY,EAAI,GAAG,QAAS,IAAM,CACpBP,EAAQ,EAAK,CACf,CAAC,EACDO,EAAI,GAAG,UAAW,IAAM,CACtBA,EAAI,QAAQ,EACZP,EAAQ,EAAK,CACf,CAAC,EACDO,EAAI,IAAI,CACV,CAAC,EAGGI,GAA0B,MAAOC,EAAM,CAAE,UAAAC,CAAU,IAAM,CAC7D,IAAMC,EAAMtB,GAAY,EAExB,GAAIsB,GAAO,KAAM,MAAM,IAAI,MAAM,8DAA8D,EAC/F,MAAMtC,GAAcuC,EAAQ,SAAU,CAACD,EAAK,GAAGF,CAAI,EAAG,CACpD,OAAQ,YAAY,QAAQC,CAAS,EACrC,SAAU,QACV,IAAKG,EAAyBD,EAAQ,GAAG,CAC3C,CAAC,CACH,EAUaE,EAAmB,IACvBF,EAAQ,IAAI,oBAAsB3B,EAAK8B,EAAQ,EAAG,WAAW,EAIhEC,GAAe,SAOfC,GAAuB,aAchBC,EAAa,IACjBjC,EAAK6B,EAAiB,EAAGE,EAAY,EAajCG,GAAuB,IAAe,CACjD,GAAI,CACF,IAAMC,EAAWzC,EAAaM,EAAK6B,EAAiB,EAAGG,EAAoB,EAAG,OAAO,EAAE,KAAK,EAE5F,OAAIG,IAAa,GAAW,GAEbC,EAAW,QAAQ,EAAE,OAAO1C,EAAauC,EAAW,CAAC,CAAC,EAAE,OAAO,KAAK,IAEjEE,EAAS,YAAY,CACzC,MAAQ,CACN,MAAO,EACT,CACF,EAkBaE,GAAsB,CAAC1B,EAAc2B,IACzC,IAAI,QAAS1B,GAAY,CAC9B,IAAI2B,EAEJ,GAAI,CACFA,EAAK7C,EAAauC,EAAW,CAAC,CAChC,MAAQ,CACNrB,EAAQ,CAAE,GAAI,GAAO,KAAM,QAAS,CAAC,EAErC,MACF,CAEA,IAAMC,EAASK,EAAI,QAAQ,CAAE,KAAMV,EAAU,KAAAG,EAAM,WAAA2B,EAAY,GAAI,CAACC,CAAE,EAAG,mBAAoB,EAAK,EAAG,IAAM,CAIzG,IAAMC,EAAY3B,EAAO,mBACnB4B,EAAa5B,EAAO,WAE1BA,EAAO,QAAQ,EACfD,EAAQ6B,EAAa,CAAE,GAAI,EAAK,EAAI,CAAE,GAAI,GAAO,KAAMD,GAAW,MAAQA,GAAW,SAAW,SAAU,CAAC,CAC7G,CAAC,EAED3B,EAAO,WAAWP,CAAgB,EAClCO,EAAO,KAAK,UAAW,IAAM,CAC3BA,EAAO,QAAQ,EACfD,EAAQ,CAAE,GAAI,GAAO,KAAM,WAAY,CAAC,CAC1C,CAAC,EACDC,EAAO,KAAK,QAAU6B,GAA+B,CACnD7B,EAAO,QAAQ,EACfD,EAAQ,CAAE,GAAI,GAAO,KAAM8B,EAAI,MAAQ,SAAU,CAAC,CACpD,CAAC,CACH,CAAC,EAQUC,GAAa,IAAuB,CAC/C,GAAI,CACF,IAAMC,EAAe,KAAK,MAAMlD,EAAaM,EAAK6B,EAAiB,EAAG,aAAa,EAAG,OAAO,CAAC,EAE9F,OAAK,MAAM,QAAQe,CAAG,EAEfA,EAAI,QAASC,GAA2B,CAC7C,GAAM,CAAE,SAAAC,EAAU,KAAAnC,CAAK,EAAKkC,GAAS,CAAC,EAEtC,OAAI,OAAOC,GAAa,UAAYA,IAAa,IAAM,OAAOnC,GAAS,SAAiB,CAAC,EAElF,CAAC,CAAE,KAAMmC,EAAU,KAAAnC,CAAK,CAAC,CAClC,CAAC,EAR+B,CAAC,CASnC,MAAQ,CACN,MAAO,CAAC,CACV,CACF,EAkCaoC,GAAuB,CAACC,EAA2B,CAAC,IAAsB,CACrF,IAAMC,EAAMD,EAAK,KAAOzB,GAClB2B,EAAcF,EAAK,aAAetC,GAClCyC,EAAiBH,EAAK,gBAAkB/B,GACxCQ,EAAYuB,EAAK,WAAa3C,GAChC+C,EAA+B,KAG7BC,EAAS,MAAO7B,GAAqC,CACzD,GAAI,CACF,aAAMyB,EAAIzB,EAAM,CAAE,UAAAC,CAAU,CAAC,EAEtB,EACT,MAAQ,CACN,MAAO,EACT,CACF,EAEM6B,EAAc,UAClBF,IAAiB,MAAMC,EAAO,CAAC,WAAW,CAAC,EAEpCD,GAsBT,MAAO,CAAE,YAAAE,EAAa,eAnBN,MAAO3C,EAAcO,IAC/B,CAAE,MAAMoC,EAAY,GAEpB,CAAE,MAAMJ,EAAYvC,CAAI,EAAW,GAEhCwC,EAAexC,EAAMO,CAAG,EAcc,cAXzB,MAAOqC,EAAc5C,IACnC,MAAM2C,EAAY,EAEjBD,EAAO,CAAC,QAASE,EAAM,OAAO5C,CAAI,CAAC,CAAC,EAFR,GAUyB,YAL1C,MAAO4C,GAAgC,CACnD,MAAMD,EAAY,GACxB,MAAMD,EAAO,CAAC,QAAS,WAAYE,CAAI,CAAC,CAC1C,CAE0E,CAC5E",
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", "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", "args", "timeoutMs", "bin", "process", "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,4 +1,4 @@
1
- import{v as e}from"./chunk-X2L4F2VM.js";var s=()=>{let n="",r=[],o=!1;return{start(t){n=t,r=[],o=!1},setInteractive(){o=!0},addOption(t,a){r.push({flag:t,value:a})},formatOptions(){return r.map(t=>typeof t.value=="boolean"?t.value?t.flag:"":Array.isArray(t.value)?`${t.flag} "${t.value.join(", ")}"`:`${t.flag} "${t.value}"`).filter(Boolean).join(" ")},snapshot(){return r.length===0?null:{formattedOptions:this.formatOptions()}},print(){!o||r.length===0||e.info(`\u{1F4DF} Equivalent command:
1
+ import{v as e}from"./chunk-V37MZDFR.js";var s=()=>{let n="",r=[],o=!1;return{start(t){n=t,r=[],o=!1},setInteractive(){o=!0},addOption(t,a){r.push({flag:t,value:a})},formatOptions(){return r.map(t=>typeof t.value=="boolean"?t.value?t.flag:"":Array.isArray(t.value)?`${t.flag} "${t.value.join(", ")}"`:`${t.flag} "${t.value}"`).filter(Boolean).join(" ")},snapshot(){return r.length===0?null:{formattedOptions:this.formatOptions()}},print(){!o||r.length===0||e.info(`\u{1F4DF} Equivalent command:
2
2
  pnpm exec infra-kit ${n} ${this.formatOptions()}
3
3
  `)},reset(){n="",r=[],o=!1}}},i=s();import m from"@inquirer/confirm";import f from"node:process";var l=async(n,r)=>{let o=n?!0:await m({message:r});n||i.setInteractive(),o||(e.info("Operation cancelled. Exiting..."),f.exit(0))};export{i as a,l as b};
4
- //# sourceMappingURL=chunk-52WL2IQX.js.map
4
+ //# sourceMappingURL=chunk-RXEMP3VI.js.map
@@ -1,3 +1,3 @@
1
- import T from"node:process";import C from"pino";import te from"pino-pretty";var _="/tmp/mcp-infra-kit.log",Rt=()=>{let e=T.argv.includes("--debug")?"debug":"info",t=C({level:e},C.destination({dest:_}));return t.info(`Logger initialized with level: ${e}. Logging to: ${_}`),t},re=()=>{let e=T.argv.includes("--debug")?"debug":"info",t=["time","pid","hostname"];return e==="debug"&&t.push("level"),C({level:e},te({destination:2,ignore:t.join(","),colorize:!0}))},wt=re();import*as p from"node:fs";import*as a from"node:path";function ne(e){let t=e;for(let r=0;r<10;r++){let n=a.join(t,"pnpm-workspace.yaml");if(p.existsSync(n))return t;t=a.dirname(t)}throw new Error("Could not find monorepo root (pnpm-workspace.yaml)")}function K(e,t){let r=a.join(e,"package.json");if(!p.existsSync(r))return t;try{let n=JSON.parse(p.readFileSync(r,"utf-8"));return typeof n.name=="string"?n.name:t}catch{return t}}function Ct(e){let t=a.join(e,"apps"),r=[];if(!p.existsSync(t))throw new Error(`Apps directory not found: ${t}`);let n=p.readdirSync(t,{withFileTypes:!0}).filter(s=>s.isDirectory()).map(s=>s.name);for(let s of n){let i=a.join(t,s,"api"),c=a.join(i,"serverless.yml");p.existsSync(c)&&r.push({name:s,packageName:K(i,s),path:i})}return r}var se=["vite.config.ts","vite.config.mts","vite.config.cts","vite.config.js","vite.config.mjs","vite.config.cjs"];function oe(e){for(let t of se){let r=a.join(e,t);if(p.existsSync(r))try{return p.readFileSync(r,"utf-8").includes("infra-kit/vite")}catch{return!1}}return!1}function ie(e){let t=a.join(e,"package.json");if(!p.existsSync(t))return!1;try{let r=JSON.parse(p.readFileSync(t,"utf-8"));return typeof r.scripts?.dev=="string"&&r.scripts.dev.length>0}catch{return!1}}function St(e){let t=a.join(e,"apps"),r=[];if(!p.existsSync(t))return r;let n=p.readdirSync(t,{withFileTypes:!0}).filter(s=>s.isDirectory()).map(s=>s.name);for(let s of n){let i=a.join(t,s,"ui");p.existsSync(i)&&ie(i)&&r.push({name:s,packageName:K(i,s),path:i,managedPort:oe(i)})}return r}function Dt(e){let t=e?.filter(Boolean)??[];return t.length>0?t:null}function It(e){let t=ne(e),r=a.relative(a.join(t,"apps"),e),n=r.split(a.sep)[0];if(r===""||r.startsWith("..")||a.isAbsolute(r)||!n)throw new Error(`--self: not inside an apps/<app> directory (cwd: ${e}). Run from an app folder or use --app=<name>.`);return n}var ae=new Set(["api","ui"]),O=e=>{let t=a.join(e,"dist");return p.existsSync(t)&&p.statSync(t).isDirectory()?t:void 0},S=e=>p.existsSync(e)?p.readdirSync(e,{withFileTypes:!0}).filter(t=>t.isDirectory()).map(t=>t.name):[];function jt(e){let t=[];for(let n of S(a.join(e,"packages"))){let s=O(a.join(e,"packages",n));s!==void 0&&t.push(s)}let r=a.join(e,"apps");for(let n of S(r))for(let s of S(a.join(r,n))){if(ae.has(s))continue;let i=a.join(r,n,s);if(!p.existsSync(a.join(i,"package.json")))continue;let c=O(i);c!==void 0&&t.push(c)}return t}function Et(e){return e.map(t=>a.join(t.path,"dist")).filter(t=>p.existsSync(t))}function At(e,t,r){let n=a.normalize(e),s=r.find(c=>n.startsWith(a.normalize(c)));return s?{kind:"package",packageDir:s}:{kind:"app",app:t.find(c=>n.startsWith(a.normalize(c)))}}var ce={"*/api":{},"*/ui":{}},F=e=>{let t=e.split("/"),[r,n]=t;return t.length!==2||!r?`devServersPresets: invalid target "${e}" (expected "<app>/api" or "<app>/ui")`:n!=="api"&&n!=="ui"?`devServersPresets: invalid target part "${n}" in "${e}" (expected "api" or "ui")`:null},pe=e=>{let t=F(e);if(t)throw new Error(t);let[r,n]=e.split("/");return{appGlob:r,part:n}},Lt=e=>{let t=[];for(let[r,n]of Object.entries(e))for(let s of Object.keys(n.apps??{})){let i=F(s);i&&t.push({preset:r,key:s,message:`preset "${r}": ${i}`})}return t},le=(e,t)=>t==="api"?e.api:e.ui,ue=e=>[...new Set([...e.api,...e.ui])].sort(),M=(e,t)=>`${e}/${t}`,fe=e=>e?0:1,ge=(e,t)=>t.watchDeps===void 0?e??{watchDeps:!0,rank:t.rank,explicitValue:!1}:!e||!e.explicitValue?{watchDeps:t.watchDeps,rank:t.rank,explicitValue:!0}:t.rank>e.rank?{watchDeps:t.watchDeps,rank:t.rank,explicitValue:!0}:e,de=(e,t,r)=>{let{app:n,part:s,watchDeps:i,isGlob:c}=r;if(!le(t,s).includes(n)){c||e.unmatched.push(M(n,s));return}let l=M(n,s),u=ge(e.targets.get(l),{watchDeps:i,rank:fe(c)});e.targets.set(l,{app:n,part:s,...u})},me=(e,t)=>{let r=Object.entries(e.apps??ce),n={targets:new Map,proxy:{},unmatched:[]};for(let[c,l]of r){let{appGlob:u,part:h}=pe(c),k=u==="*",R=k?ue(t):[u];for(let b of R)de(n,t,{app:b,part:h,watchDeps:l.watchDeps,isGlob:k}),l.proxy&&(n.proxy[b]={...n.proxy[b]??{},...l.proxy})}let s=[...n.targets.values()].map(({app:c,part:l,watchDeps:u})=>({app:c,part:l,watchDeps:u})),i=[...new Set(s.filter(c=>c.part==="api").map(c=>c.app))];return{targets:s,cmux:e.cmux??!1,proxy:n.proxy,localApps:i,unmatched:n.unmatched}},Nt=e=>{if(e.preset!=null)return e.preset;if(e.running.length===0)return"nothing";let t=new Set(e.running);return e.discovered.every(n=>t.has(n))?"*":[...t].sort().join(" + ")},he=(e,t)=>Object.keys(e).find(r=>e[r]===t),ve=({preset:e,app:t,route:r,launchedPkgs:n,ctx:s})=>{let i=s.routePkg(t,r);if(i===void 0)return{preset:e,app:t,route:r,kind:"unknown-route",message:`preset "${e}": proxy override "${r}" on "${t}" names a route not declared in ${t}'s infra-kit.config.ts dev.proxy.routes`};if(n.has(i))return null;let c=he(s.apiPkgByApp,i),l=c?`add "${c}/api" to the preset`:`launch the api whose package is "${i}"`;return{preset:e,app:t,route:r,pkg:i,kind:"backend-not-launched",message:`preset "${e}": proxy override "${r}" \u2192 "local" requires backend "${i}" to run locally, but the preset does not launch it \u2014 ${l}, or set "${r}" to "cloud"`}},_t=(e,t)=>{let r=[];for(let[n,s]of Object.entries(e)){let i=me(s,t.discovered),c=new Set(i.localApps.map(l=>t.apiPkgByApp[l]).filter(l=>l!==void 0));for(let[l,u]of Object.entries(i.proxy))for(let[h,k]of Object.entries(u)){if(k!=="local")continue;let R=ve({preset:n,app:l,route:h,launchedPkgs:c,ctx:t});R&&r.push(R)}}return r};import L from"node:fs/promises";import J from"node:os";import x from"node:path";import Oe from"node:process";import{z as o}from"zod";import d from"node:path";import{$ as f}from"zx";var ye=/^v?(\d+)\.(\d+)\.(\d+)$/,xe=/^(\d+)\.(\d+)\.(\d+)$/,Pe=/^[a-z0-9]+(?:-[a-z0-9]+)*$/,w="release/",D="release/v",B="refs/heads/",ke="next";var G=new Set(["dev","main","next","hotfix","regular","release"]),g=class extends Error{constructor(t){super(t),this.name="InvalidReleaseNameError"}},v=class extends Error{constructor(t){super(t),this.name="InvalidReleaseRefError"}},U=e=>e.startsWith(B)?e.slice(B.length):e,V=(e,t,r)=>({kind:"version",semver:{major:e,minor:t,patch:r},raw:`${e}.${t}.${r}`}),I=e=>{if(e.length===0)throw new g('Release name is empty. Provide a kebab-case name like "checkout-redesign".');if(e.length>50)throw new g(`Release name "${e}" is ${e.length} characters; the maximum is 50.`);if(!Pe.test(e))throw new g(`Release name "${e}" is not kebab-case. Use lowercase letters, digits, and single hyphens, e.g. "checkout-redesign".`);if(G.has(e))throw new g(`Release name "${e}" is reserved. Reserved names: ${[...G].join(", ")}.`)},j=e=>{let t=U(e.trim());if(!t.startsWith(w))return null;if(t.startsWith(D)){let n=t.slice(D.length),s=xe.exec(n);if(s)return V(Number(s[1]),Number(s[2]),Number(s[3]))}let r=t.slice(w.length);try{I(r)}catch{return null}return{kind:"name",name:r,raw:r}},Re=e=>{let t=e.trim();if(U(t).startsWith(w)){let s=j(t);if(!s)throw new v(`"${e}" looks like a release branch but is not a valid release/v<semver> or release/<name> ref.`);return s}let n=ye.exec(t);if(n)return V(Number(n[1]),Number(n[2]),Number(n[3]));if(t.toLowerCase()===ke)throw new v('The "next" token must be resolved to a concrete version (via computeNextVersion) before parsing a release ref.');try{I(t)}catch(s){let i=s instanceof Error?s.message:String(s);throw new v(`Cannot parse "${e}" as a release ref: ${i}`)}return{kind:"name",name:t,raw:t}},we=e=>e.kind==="version"?`${D}${e.raw}`:`${w}${e.name}`,be=(e,t)=>{let r=t==="hotfix"?"Hotfix":"Release";return e.kind==="version"?`${r} v${e.raw}`:`${r} ${e.name}`},Ce=e=>e.kind==="version"?`Release v${e.raw} (RC)`:`Release ${e.name} (RC)`,Se=e=>e.kind==="version"?`v${e.raw}`:e.name,De=e=>e.raw,E=e=>e==null?!1:j(e)!==null,z=e=>{if(e===void 0)return null;let t=e instanceof Date?e.getTime():new Date(e).getTime();return Number.isNaN(t)?null:t},Ie=(e,t,r)=>{if(e.kind==="version"&&t.kind==="version")return e.semver.major!==t.semver.major?e.semver.major-t.semver.major:e.semver.minor!==t.semver.minor?e.semver.minor-t.semver.minor:e.semver.patch-t.semver.patch;if(e.kind==="version")return-1;if(t.kind==="version")return 1;let n=z(r?.a),s=z(r?.b);return n!==null&&s!==null&&n!==s?n-s:e.name<t.name?-1:e.name>t.name?1:0};var je=async e=>{let r=(await f`git worktree list`).stdout.split(`
2
- `).filter(Boolean),n={release:Ee,feature:Ae};return r.map(n[e]).filter(s=>s!==null)},W=e=>{let t=e.trimEnd();if(!t.endsWith("]"))return null;let r=t.lastIndexOf("[");if(r===-1)return null;let n=t.slice(r+1,-1);return n.length>0?n:null},Ee=e=>{let t=W(e);return E(t)?t:null},Ae=e=>{let t=W(e);return t?.startsWith("feature/")?t:null},y=async()=>(await f`git rev-parse --show-toplevel`).stdout.trim(),A=async e=>{let t=e??await y(),r=(await f({cwd:t})`git rev-parse --git-common-dir`).stdout.trim(),n=d.resolve(t,r);return n.includes(`${d.sep}.git${d.sep}modules${d.sep}`)?t:d.dirname(n)},H=async()=>(await f`git rev-parse --abbrev-ref HEAD`).stdout.trim(),$e=async()=>(await f`git status --porcelain`).stdout.trim().length===0,Le=async()=>{let e=await y(),[t,r]=await Promise.all([f({cwd:e})`git rev-parse --absolute-git-dir`,f({cwd:e})`git rev-parse --git-common-dir`]),n=t.stdout.trim(),s=d.resolve(e,r.stdout.trim());return n!==s},Ne=async()=>{let e=await y();return d.basename(e)},_e=async e=>{(await f`git branch --list ${e}`).stdout.trim().length!==0&&await H()!==e&&await f`git branch -D ${e}`},Te=async e=>{(await f`git ls-remote --heads origin ${e}`).stdout.trim().length!==0&&await f`git push origin --delete ${e}`};var X="infra-kit.json",Y=".infra-kit",Ke="infra-kit.json",Me="projects",Fe=o.object({provider:o.literal("doppler"),config:o.object({name:o.string().min(1)})}),Be=o.discriminatedUnion("provider",[Fe]),Ge=o.object({workspaceConfigPath:o.string().min(1)}),ze=o.object({provider:o.literal("cursor"),config:Ge}),Ue=o.object({}),Ve=o.object({provider:o.literal("zed"),config:Ue}),q=o.discriminatedUnion("provider",[ze,Ve]),We=o.union([q,o.array(q).min(1)]),He=o.object({provider:o.literal("jira"),config:o.object({baseUrl:o.string().url(),projectId:o.number().int().positive()})}),Xe=o.discriminatedUnion("provider",[He]),qe=["two-columns","three-pane"],Je=o.object({layout:o.enum(qe).optional()}),Ye=o.object({openInGithubDesktop:o.boolean().optional(),openInCmux:o.boolean().optional(),cmux:Je.optional()}),Qe=o.object({port:o.number().int().positive().optional(),prefixUrl:o.string().min(1).optional()}).strict(),Ze=o.record(o.string().min(1),Qe),et=o.enum(["local","cloud"]),tt=o.object({watchDeps:o.boolean().optional(),proxy:o.record(o.string().min(1),et).optional()}).strict(),rt=o.object({apps:o.record(o.string().min(1),tt).optional(),cmux:o.boolean().optional()}).strict(),nt=o.record(o.string().min(1),rt),st=o.object({port:o.number().int().positive().optional()}).strict(),ot=o.object({trigger:o.enum(["shell-startup","cli-invocation"]),config:o.string().min(1)}).strict(),N=o.object({environments:o.array(o.string().min(1)).min(1),envManagement:Be,ide:We.optional(),taskManager:Xe.optional(),worktrees:Ye.optional(),envAutoLoad:ot.optional(),dev:Ze.optional(),devServersPresets:nt.optional(),devProxy:st.optional()}).strict(),it=80,at=1024,ct=1355,Q=N.superRefine((e,t)=>{if(!Array.isArray(e.ide))return;let r=new Set;for(let n of e.ide){if(r.has(n.provider)){t.addIssue({code:"custom",message:"each IDE provider may appear at most once",path:["ide"]});return}r.add(n.provider)}}),Z=N.partial(),pt=e=>{let t=e.ide;return t?Array.isArray(t)?t:[t]:[]},lt="two-columns",ut=e=>e.worktrees?.cmux?.layout??lt,m=null,P=null,ft=()=>`${Oe.cwd()} ${J.homedir()}`,ee=async()=>{let e=ft();if(P&&P.key===e)return P.value;let t=await y(),r=await A(t),n=x.basename(r),s=x.join(J.homedir(),Y),i={main:x.join(t,X),userGlobal:x.join(s,Ke),userProject:x.join(s,Me,n,X),projectName:n};return P={key:e,value:i},i},gt=async()=>{let e=await ee(),t;try{t=await L.stat(e.main)}catch{m=null;let u=e.main.replace(/\.json$/,".yml");throw await $(u)?new Error(`infra-kit.json not found at ${e.main}. A legacy infra-kit.yml exists \u2014 run \`infra-kit init\` to convert it.`):new Error(`infra-kit.json not found at ${e.main}`)}let[r,n]=await Promise.all([$(e.userGlobal),$(e.userProject)]),s={main:Number(t.mtimeMs),userGlobal:r?Number(r.mtimeMs):null,userProject:n?Number(n.mtimeMs):null};if(m&&vt(m.mtimes,s))return m.value;let i=[{label:"infra-kit.json",path:e.main,required:!0},{label:"~/.infra-kit/infra-kit.json",path:e.userGlobal,required:!1},{label:`~/.infra-kit/projects/${e.projectName}/infra-kit.json`,path:e.userProject,required:!1}],c={};for(let u of i){let h=await yt(u);h!==null&&(c={...c,...h})}let l=Q.safeParse(c);if(!l.success)throw new Error(`Invalid merged infra-kit config: ${o.prettifyError(l.error)}`);return m={mtimes:s,value:l.data},l.data},dt=()=>{m=null},mt=()=>{m=null,P=null},$=async e=>{try{return await L.stat(e)}catch{return null}},ht=async e=>{try{return await L.readFile(e,"utf-8")}catch{return null}},vt=(e,t)=>{let r=Object.keys(e);return r.length!==Object.keys(t).length?!1:r.every(n=>e[n]===t[n])},yt=async e=>{let t=await ht(e.path);if(t===null){if(e.required)throw new Error(`${e.label} not found at ${e.path}`);return null}let r;try{r=t.trim()===""?{}:JSON.parse(t)}catch(s){throw new Error(`Invalid JSON in ${e.label} at ${e.path}: ${s.message}`)}let n=Z.safeParse(r);if(!n.success)throw new Error(`Invalid ${e.label} at ${e.path}: ${o.prettifyError(n.error)}`);return n.data};export{g as a,I as b,j as c,Re as d,we as e,be as f,Ce as g,Se as h,De as i,E as j,Ie as k,je as l,y as m,H as n,$e as o,Le as p,Ne as q,_e as r,Te as s,_ as t,Rt as u,wt as v,ne as w,Ct as x,St as y,Dt as z,It as A,jt as B,Et as C,At as D,F as E,Lt as F,me as G,Nt as H,_t as I,Y as J,it as K,at as L,ct as M,Q as N,Z as O,pt as P,ut as Q,ee as R,gt as S,dt as T,mt as U};
3
- //# sourceMappingURL=chunk-X2L4F2VM.js.map
1
+ import _ from"node:process";import C from"pino";import te from"pino-pretty";var T="/tmp/mcp-infra-kit.log",Pt=()=>{let e=_.argv.includes("--debug")?"debug":"info",t=C({level:e},C.destination({dest:T}));return t.info(`Logger initialized with level: ${e}. Logging to: ${T}`),t},re=()=>{let e=_.argv.includes("--debug")?"debug":"info",t=["time","pid","hostname"];return e==="debug"&&t.push("level"),C({level:e},te({destination:2,ignore:t.join(","),colorize:!0}))},kt=re();import*as p from"node:fs";import*as a from"node:path";function ne(e){let t=e;for(let r=0;r<10;r++){let n=a.join(t,"pnpm-workspace.yaml");if(p.existsSync(n))return t;t=a.dirname(t)}throw new Error("Could not find monorepo root (pnpm-workspace.yaml)")}function O(e,t){let r=a.join(e,"package.json");if(!p.existsSync(r))return t;try{let n=JSON.parse(p.readFileSync(r,"utf-8"));return typeof n.name=="string"?n.name:t}catch{return t}}function Rt(e){let t=a.join(e,"apps"),r=[];if(!p.existsSync(t))throw new Error(`Apps directory not found: ${t}`);let n=p.readdirSync(t,{withFileTypes:!0}).filter(s=>s.isDirectory()).map(s=>s.name);for(let s of n){let i=a.join(t,s,"api"),c=a.join(i,"serverless.yml");p.existsSync(c)&&r.push({name:s,packageName:O(i,s),path:i})}return r}var se=["vite.config.ts","vite.config.mts","vite.config.cts","vite.config.js","vite.config.mjs","vite.config.cjs"];function oe(e){for(let t of se){let r=a.join(e,t);if(p.existsSync(r))try{return p.readFileSync(r,"utf-8").includes("infra-kit/vite")}catch{return!1}}return!1}function ie(e){let t=a.join(e,"package.json");if(!p.existsSync(t))return!1;try{let r=JSON.parse(p.readFileSync(t,"utf-8"));return typeof r.scripts?.dev=="string"&&r.scripts.dev.length>0}catch{return!1}}function bt(e){let t=a.join(e,"apps"),r=[];if(!p.existsSync(t))return r;let n=p.readdirSync(t,{withFileTypes:!0}).filter(s=>s.isDirectory()).map(s=>s.name);for(let s of n){let i=a.join(t,s,"ui");p.existsSync(i)&&ie(i)&&r.push({name:s,packageName:O(i,s),path:i,managedPort:oe(i)})}return r}function Ct(e){let t=e?.filter(Boolean)??[];return t.length>0?t:null}function St(e){let t=ne(e),r=a.relative(a.join(t,"apps"),e),n=r.split(a.sep)[0];if(r===""||r.startsWith("..")||a.isAbsolute(r)||!n)throw new Error(`--self: not inside an apps/<app> directory (cwd: ${e}). Run from an app folder or use --app=<name>.`);return n}var ae=new Set(["api","ui"]),K=e=>{let t=a.join(e,"dist");return p.existsSync(t)&&p.statSync(t).isDirectory()?t:void 0},S=e=>p.existsSync(e)?p.readdirSync(e,{withFileTypes:!0}).filter(t=>t.isDirectory()).map(t=>t.name):[];function It(e){let t=[];for(let n of S(a.join(e,"packages"))){let s=K(a.join(e,"packages",n));s!==void 0&&t.push(s)}let r=a.join(e,"apps");for(let n of S(r))for(let s of S(a.join(r,n))){if(ae.has(s))continue;let i=a.join(r,n,s);if(!p.existsSync(a.join(i,"package.json")))continue;let c=K(i);c!==void 0&&t.push(c)}return t}function Dt(e){return e.map(t=>a.join(t.path,"dist")).filter(t=>p.existsSync(t))}function jt(e,t,r){let n=a.normalize(e),s=r.find(c=>n.startsWith(a.normalize(c)));return s?{kind:"package",packageDir:s}:{kind:"app",app:t.find(c=>n.startsWith(a.normalize(c)))}}var ce={"*/api":{},"*/ui":{}},F=e=>{let t=e.split("/"),[r,n]=t;return t.length!==2||!r?`devServersPresets: invalid target "${e}" (expected "<app>/api" or "<app>/ui")`:n!=="api"&&n!=="ui"?`devServersPresets: invalid target part "${n}" in "${e}" (expected "api" or "ui")`:null},pe=e=>{let t=F(e);if(t)throw new Error(t);let[r,n]=e.split("/");return{appGlob:r,part:n}},Et=e=>{let t=[];for(let[r,n]of Object.entries(e))for(let s of Object.keys(n.apps??{})){let i=F(s);i&&t.push({preset:r,key:s,message:`preset "${r}": ${i}`})}return t},le=(e,t)=>t==="api"?e.api:e.ui,ue=e=>[...new Set([...e.api,...e.ui])].sort(),M=(e,t)=>`${e}/${t}`,fe=e=>e?0:1,ge=(e,t)=>t.watchDeps===void 0?e??{watchDeps:!0,rank:t.rank,explicitValue:!1}:!e||!e.explicitValue?{watchDeps:t.watchDeps,rank:t.rank,explicitValue:!0}:t.rank>e.rank?{watchDeps:t.watchDeps,rank:t.rank,explicitValue:!0}:e,de=(e,t,r)=>{let{app:n,part:s,watchDeps:i,isGlob:c}=r;if(!le(t,s).includes(n)){c||e.unmatched.push(M(n,s));return}let l=M(n,s),u=ge(e.targets.get(l),{watchDeps:i,rank:fe(c)});e.targets.set(l,{app:n,part:s,...u})},me=(e,t)=>{let r=Object.entries(e.apps??ce),n={targets:new Map,proxy:{},unmatched:[]};for(let[c,l]of r){let{appGlob:u,part:h}=pe(c),k=u==="*",w=k?ue(t):[u];for(let b of w)de(n,t,{app:b,part:h,watchDeps:l.watchDeps,isGlob:k}),l.proxy&&(n.proxy[b]={...n.proxy[b]??{},...l.proxy})}let s=[...n.targets.values()].map(({app:c,part:l,watchDeps:u})=>({app:c,part:l,watchDeps:u})),i=[...new Set(s.filter(c=>c.part==="api").map(c=>c.app))];return{targets:s,cmux:e.cmux??!1,proxy:n.proxy,localApps:i,unmatched:n.unmatched}},$t=e=>{if(e.preset!=null)return e.preset;if(e.running.length===0)return"nothing";let t=new Set(e.running);return e.discovered.every(n=>t.has(n))?"*":[...t].sort().join(" + ")},he=(e,t)=>Object.keys(e).find(r=>e[r]===t),ve=({preset:e,app:t,route:r,launchedPkgs:n,ctx:s})=>{let i=s.routePkg(t,r);if(i===void 0)return{preset:e,app:t,route:r,kind:"unknown-route",message:`preset "${e}": proxy override "${r}" on "${t}" names a route not declared in ${t}'s infra-kit.config.ts dev.proxy.routes`};if(n.has(i))return null;let c=he(s.apiPkgByApp,i),l=c?`add "${c}/api" to the preset`:`launch the api whose package is "${i}"`;return{preset:e,app:t,route:r,pkg:i,kind:"backend-not-launched",message:`preset "${e}": proxy override "${r}" \u2192 "local" requires backend "${i}" to run locally, but the preset does not launch it \u2014 ${l}, or set "${r}" to "cloud"`}},Nt=(e,t)=>{let r=[];for(let[n,s]of Object.entries(e)){let i=me(s,t.discovered),c=new Set(i.localApps.map(l=>t.apiPkgByApp[l]).filter(l=>l!==void 0));for(let[l,u]of Object.entries(i.proxy))for(let[h,k]of Object.entries(u)){if(k!=="local")continue;let w=ve({preset:n,app:l,route:h,launchedPkgs:c,ctx:t});w&&r.push(w)}}return r};import N from"node:fs/promises";import J from"node:os";import x from"node:path";import Ke from"node:process";import{z as o}from"zod";import d from"node:path";import{$ as f}from"zx";var ye=/^v?(\d+)\.(\d+)\.(\d+)$/,xe=/^(\d+)\.(\d+)\.(\d+)$/,Pe=/^[a-z0-9]+(?:-[a-z0-9]+)*$/,R="release/",I="release/v",B="refs/heads/",ke="next";var G=new Set(["dev","main","next","hotfix","regular","release"]),g=class extends Error{constructor(t){super(t),this.name="InvalidReleaseNameError"}},v=class extends Error{constructor(t){super(t),this.name="InvalidReleaseRefError"}},W=e=>e.startsWith(B)?e.slice(B.length):e,U=(e,t,r)=>({kind:"version",semver:{major:e,minor:t,patch:r},raw:`${e}.${t}.${r}`}),D=e=>{if(e.length===0)throw new g('Release name is empty. Provide a kebab-case name like "checkout-redesign".');if(e.length>50)throw new g(`Release name "${e}" is ${e.length} characters; the maximum is 50.`);if(!Pe.test(e))throw new g(`Release name "${e}" is not kebab-case. Use lowercase letters, digits, and single hyphens, e.g. "checkout-redesign".`);if(G.has(e))throw new g(`Release name "${e}" is reserved. Reserved names: ${[...G].join(", ")}.`)},j=e=>{let t=W(e.trim());if(!t.startsWith(R))return null;if(t.startsWith(I)){let n=t.slice(I.length),s=xe.exec(n);if(s)return U(Number(s[1]),Number(s[2]),Number(s[3]))}let r=t.slice(R.length);try{D(r)}catch{return null}return{kind:"name",name:r,raw:r}},we=e=>{let t=e.trim();if(W(t).startsWith(R)){let s=j(t);if(!s)throw new v(`"${e}" looks like a release branch but is not a valid release/v<semver> or release/<name> ref.`);return s}let n=ye.exec(t);if(n)return U(Number(n[1]),Number(n[2]),Number(n[3]));if(t.toLowerCase()===ke)throw new v('The "next" token must be resolved to a concrete version (via computeNextVersion) before parsing a release ref.');try{D(t)}catch(s){let i=s instanceof Error?s.message:String(s);throw new v(`Cannot parse "${e}" as a release ref: ${i}`)}return{kind:"name",name:t,raw:t}},Re=e=>e.kind==="version"?`${I}${e.raw}`:`${R}${e.name}`,be=(e,t)=>{let r=t==="hotfix"?"Hotfix":"Release";return e.kind==="version"?`${r} v${e.raw}`:`${r} ${e.name}`},Ce=e=>e.kind==="version"?`Release v${e.raw} (RC)`:`Release ${e.name} (RC)`,Se=e=>e.kind==="version"?`v${e.raw}`:e.name,Ie=e=>e.raw,A=e=>e==null?!1:j(e)!==null,z=e=>{if(e===void 0)return null;let t=e instanceof Date?e.getTime():new Date(e).getTime();return Number.isNaN(t)?null:t},De=(e,t,r)=>{if(e.kind==="version"&&t.kind==="version")return e.semver.major!==t.semver.major?e.semver.major-t.semver.major:e.semver.minor!==t.semver.minor?e.semver.minor-t.semver.minor:e.semver.patch-t.semver.patch;if(e.kind==="version")return-1;if(t.kind==="version")return 1;let n=z(r?.a),s=z(r?.b);return n!==null&&s!==null&&n!==s?n-s:e.name<t.name?-1:e.name>t.name?1:0};var je=async e=>{let r=(await f`git worktree list`).stdout.split(`
2
+ `).filter(Boolean),n={release:Ae,feature:Ee};return r.map(n[e]).filter(s=>s!==null)},V=e=>{let t=e.trimEnd();if(!t.endsWith("]"))return null;let r=t.lastIndexOf("[");if(r===-1)return null;let n=t.slice(r+1,-1);return n.length>0?n:null},Ae=e=>{let t=V(e);return A(t)?t:null},Ee=e=>{let t=V(e);return t?.startsWith("feature/")?t:null},y=async()=>(await f`git rev-parse --show-toplevel`).stdout.trim(),E=async e=>{let t=e??await y(),r=(await f({cwd:t})`git rev-parse --git-common-dir`).stdout.trim(),n=d.resolve(t,r);return n.includes(`${d.sep}.git${d.sep}modules${d.sep}`)?t:d.dirname(n)},H=async()=>(await f`git rev-parse --abbrev-ref HEAD`).stdout.trim(),$e=async()=>(await f`git status --porcelain`).stdout.trim().length===0,Ne=async()=>{let e=await y(),[t,r]=await Promise.all([f({cwd:e})`git rev-parse --absolute-git-dir`,f({cwd:e})`git rev-parse --git-common-dir`]),n=t.stdout.trim(),s=d.resolve(e,r.stdout.trim());return n!==s},Le=async()=>{let e=await y();return d.basename(e)},Te=async e=>{(await f`git branch --list ${e}`).stdout.trim().length!==0&&await H()!==e&&await f`git branch -D ${e}`},_e=async e=>{(await f`git ls-remote --heads origin ${e}`).stdout.trim().length!==0&&await f`git push origin --delete ${e}`};var X="infra-kit.json",Y=".infra-kit",Oe="infra-kit.json",Me="projects",Fe=o.object({provider:o.literal("doppler"),config:o.object({name:o.string().min(1)})}),Be=o.discriminatedUnion("provider",[Fe]),Ge=o.object({workspaceConfigPath:o.string().min(1)}),ze=o.object({provider:o.literal("cursor"),config:Ge}),We=o.object({}),Ue=o.object({provider:o.literal("zed"),config:We}),q=o.discriminatedUnion("provider",[ze,Ue]),Ve=o.union([q,o.array(q).min(1)]),He=o.object({provider:o.literal("jira"),config:o.object({baseUrl:o.string().url(),projectId:o.number().int().positive()})}),Xe=o.discriminatedUnion("provider",[He]),qe=["two-columns","three-pane"],Je=o.object({layout:o.enum(qe).optional()}),Ye=o.object({openInGithubDesktop:o.boolean().optional(),openInCmux:o.boolean().optional(),cmux:Je.optional()}),Qe=o.object({port:o.number().int().positive().optional(),prefixUrl:o.string().min(1).optional()}).strict(),Ze=o.record(o.string().min(1),Qe),et=o.enum(["local","cloud"]),tt=o.object({watchDeps:o.boolean().optional(),proxy:o.record(o.string().min(1),et).optional()}).strict(),rt=o.object({apps:o.record(o.string().min(1),tt).optional(),cmux:o.boolean().optional()}).strict(),nt=o.record(o.string().min(1),rt),st=o.object({port:o.number().int().positive().optional()}).strict(),ot=o.object({trigger:o.enum(["shell-startup","cli-invocation"]),config:o.string().min(1)}).strict(),L=o.object({environments:o.array(o.string().min(1)).min(1),envManagement:Be,ide:Ve.optional(),taskManager:Xe.optional(),worktrees:Ye.optional(),envAutoLoad:ot.optional(),dev:Ze.optional(),devServersPresets:nt.optional(),devProxy:st.optional()}).strict(),it=443,Q=L.superRefine((e,t)=>{if(!Array.isArray(e.ide))return;let r=new Set;for(let n of e.ide){if(r.has(n.provider)){t.addIssue({code:"custom",message:"each IDE provider may appear at most once",path:["ide"]});return}r.add(n.provider)}}),Z=L.partial(),at=e=>{let t=e.ide;return t?Array.isArray(t)?t:[t]:[]},ct="two-columns",pt=e=>e.worktrees?.cmux?.layout??ct,m=null,P=null,lt=()=>`${Ke.cwd()} ${J.homedir()}`,ee=async()=>{let e=lt();if(P&&P.key===e)return P.value;let t=await y(),r=await E(t),n=x.basename(r),s=x.join(J.homedir(),Y),i={main:x.join(t,X),userGlobal:x.join(s,Oe),userProject:x.join(s,Me,n,X),projectName:n};return P={key:e,value:i},i},ut=async()=>{let e=await ee(),t;try{t=await N.stat(e.main)}catch{m=null;let u=e.main.replace(/\.json$/,".yml");throw await $(u)?new Error(`infra-kit.json not found at ${e.main}. A legacy infra-kit.yml exists \u2014 run \`infra-kit init\` to convert it.`):new Error(`infra-kit.json not found at ${e.main}`)}let[r,n]=await Promise.all([$(e.userGlobal),$(e.userProject)]),s={main:Number(t.mtimeMs),userGlobal:r?Number(r.mtimeMs):null,userProject:n?Number(n.mtimeMs):null};if(m&&mt(m.mtimes,s))return m.value;let i=[{label:"infra-kit.json",path:e.main,required:!0},{label:"~/.infra-kit/infra-kit.json",path:e.userGlobal,required:!1},{label:`~/.infra-kit/projects/${e.projectName}/infra-kit.json`,path:e.userProject,required:!1}],c={};for(let u of i){let h=await ht(u);h!==null&&(c={...c,...h})}let l=Q.safeParse(c);if(!l.success)throw new Error(`Invalid merged infra-kit config: ${o.prettifyError(l.error)}`);return m={mtimes:s,value:l.data},l.data},ft=()=>{m=null},gt=()=>{m=null,P=null},$=async e=>{try{return await N.stat(e)}catch{return null}},dt=async e=>{try{return await N.readFile(e,"utf-8")}catch{return null}},mt=(e,t)=>{let r=Object.keys(e);return r.length!==Object.keys(t).length?!1:r.every(n=>e[n]===t[n])},ht=async e=>{let t=await dt(e.path);if(t===null){if(e.required)throw new Error(`${e.label} not found at ${e.path}`);return null}let r;try{r=t.trim()===""?{}:JSON.parse(t)}catch(s){throw new Error(`Invalid JSON in ${e.label} at ${e.path}: ${s.message}`)}let n=Z.safeParse(r);if(!n.success)throw new Error(`Invalid ${e.label} at ${e.path}: ${o.prettifyError(n.error)}`);return n.data};export{g as a,D as b,j as c,we as d,Re as e,be as f,Ce as g,Se as h,Ie as i,A as j,De as k,je as l,y as m,H as n,$e as o,Ne as p,Le as q,Te as r,_e as s,T as t,Pt as u,kt as v,ne as w,Rt as x,bt as y,Ct as z,St as A,It as B,Dt as C,jt as D,F as E,Et as F,me as G,$t as H,Nt as I,Y as J,it as K,Q as L,Z as M,at as N,pt as O,ee as P,ut as Q,ft as R,gt as S};
3
+ //# sourceMappingURL=chunk-V37MZDFR.js.map