infra-kit 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-4ZE5XSVW.js +2 -0
- package/dist/chunk-4ZE5XSVW.js.map +7 -0
- package/dist/chunk-5OHTHRS7.js +2 -0
- package/dist/chunk-5OHTHRS7.js.map +7 -0
- package/dist/{chunk-CHETBZ6M.js → chunk-D4UYDBN5.js} +2 -2
- package/dist/{chunk-CHETBZ6M.js.map → chunk-D4UYDBN5.js.map} +2 -2
- package/dist/{chunk-PVMF4FH7.js → chunk-DHYZ6MBU.js} +2 -2
- package/dist/chunk-HXBIE4JP.js +7 -0
- package/dist/{chunk-D7QNBU5A.js.map → chunk-HXBIE4JP.js.map} +3 -3
- package/dist/chunk-LXBIHXUR.js +422 -0
- package/dist/chunk-LXBIHXUR.js.map +7 -0
- package/dist/chunk-Q4HKBLQG.js +7 -0
- package/dist/chunk-Q4HKBLQG.js.map +7 -0
- package/dist/cli.js +11 -11
- package/dist/cli.js.map +4 -4
- package/dist/dev-server.js +2 -2
- package/dist/dev-server.js.map +1 -1
- package/dist/{dev-wizard-run-YMH3L7GE.js → dev-wizard-run-FNMMYTDX.js} +2 -2
- package/dist/mcp.js +1 -1
- package/dist/mcp.js.map +4 -4
- package/dist/update-check.js +1 -1
- package/dist/update-check.js.map +1 -1
- package/package.json +11 -11
- package/readme.md +36 -33
- package/dist/chunk-D7QNBU5A.js +0 -7
- package/dist/chunk-NKXTDKWV.js +0 -2
- package/dist/chunk-NKXTDKWV.js.map +0 -7
- package/dist/chunk-NS5KIBXS.js +0 -2
- package/dist/chunk-NS5KIBXS.js.map +0 -7
- package/dist/chunk-QRY6BS6K.js +0 -249
- package/dist/chunk-QRY6BS6K.js.map +0 -7
- package/dist/chunk-Z2SKAHP4.js +0 -6
- package/dist/chunk-Z2SKAHP4.js.map +0 -7
- /package/dist/{chunk-PVMF4FH7.js.map → chunk-DHYZ6MBU.js.map} +0 -0
- /package/dist/{dev-wizard-run-YMH3L7GE.js.map → dev-wizard-run-FNMMYTDX.js.map} +0 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/commands/audit/audit.ts", "../src/lib/agent-guidance/adoption.ts", "../src/lib/package-validator/loader/config-loader.ts", "../src/lib/agent-guidance/package-type.ts", "../src/lib/package-config/package-config.ts", "../src/lib/package-validator/fs-utils.ts", "../src/lib/package-validator/loader/package-discovery.ts", "../src/lib/package-validator/loader/package-root.ts", "../src/lib/managed-block/managed-block.ts", "../src/lib/agent-guidance/markers.ts", "../src/lib/agent-guidance/inspect.ts", "../src/lib/agent-guidance/read-guidance-file.ts", "../src/lib/agent-guidance/agent-guidance.ts", "../resources/design/skeleton.md", "../resources/package/backend.md", "../resources/package/e2e.md", "../resources/package/frontend.md", "../resources/package/lib.md", "../resources/package/mobile.md", "../resources/root/body.md", "../src/lib/agent-guidance/resources.ts", "../src/lib/agent-guidance/template.ts", "../src/lib/agent-guidance/bodies/design-skeleton.ts", "../src/lib/agent-guidance/bodies/package-body.ts", "../src/lib/agent-guidance/bodies/root-body.ts", "../src/lib/agent-guidance/write-managed-file.ts", "../src/lib/package-validator/package-validator.ts", "../src/lib/package-validator/checks/agent-guidance-check.ts", "../src/lib/package-validator/checks/config-check.ts", "../src/lib/package-validator/checks/files-check.ts", "../src/lib/package-validator/checks/scripts-check.ts", "../src/lib/package-validator/checks/turbo-check.ts", "../src/types.ts", "../src/commands/audit/fix.ts", "../src/commands/audit/preset-proxy-check.ts", "../src/commands/config-get/config-get.ts", "../src/commands/dev-status/dev-status.ts", "../src/lib/dev-context/dev-context.ts", "../src/commands/doctor/doctor.ts", "../src/commands/doctor/prune-routes.ts", "../src/commands/doctor/report.ts", "../src/commands/env-load/env-load.ts", "../src/integrations/doppler/doppler-errors.ts", "../src/integrations/doppler/doppler-project.ts", "../src/integrations/doppler/token-resolver.ts", "../src/lib/workflow-envs/protected-envs.ts", "../src/lib/workflow-envs/protected-env-access.ts", "../src/lib/workflow-envs/protected-env-warning.ts", "../src/lib/workflow-envs/workflow-envs.ts", "../src/lib/project-envs/project-envs.ts", "../src/lib/warm-cache/purge-repo.ts", "../src/lib/warm-cache/warm-cache.ts", "../src/commands/init/agent-files.ts", "../src/commands/init/init.ts", "../src/lib/config-bootstrap/config-bootstrap.ts", "../src/lib/config-templates/config-templates.ts", "../src/lib/plugin-pointer/install-plugin.ts", "../src/lib/plugin-pointer/install-state.ts", "../src/lib/plugin-pointer/plugin-pointer.ts", "../src/lib/plugin-pointer/mcp-registration.ts", "../src/commands/init/migrate-config.ts", "../src/lib/vendor/factory-config.ts", "../src/lib/vendor/factory-config-schema.ts", "../src/lib/config-overrides/config-overrides.ts", "../src/lib/dependency-probe/default-probe-deps.ts", "../src/lib/dependency-registry/dependency-registry.ts", "../src/lib/dependency-probe/dependency-probe.ts", "../src/lib/version-utils/load-existing-versions.ts", "../src/integrations/jira/api.ts", "../src/integrations/jira/jira-api-error.ts", "../src/integrations/jira/remove-version.ts", "../src/lib/version-utils/version-utils.ts", "../src/lib/version-utils/next-version.ts", "../src/commands/env-clear/env-clear.ts", "../src/commands/env-list/env-list.ts", "../src/commands/env-status/env-status.ts", "../src/commands/env-token-list/env-token-list.ts", "../src/lib/redact/redact.ts", "../src/commands/gh-merge-dev/gh-merge-dev.ts", "../src/integrations/gh/gh-cli-auth/gh-cli-auth.ts", "../src/integrations/gh/gh-release-prs/gh-release-prs.ts", "../src/lib/git-guard/git-guard.ts", "../src/lib/release-utils/release-utils.ts", "../src/integrations/gh/pr-status.ts", "../src/lib/prompts/release-picker.ts", "../src/lib/errors/prompt-cancelled-error.ts", "../src/lib/json-output/json-output.ts", "../src/commands/gh-merge-dev/merge-run.ts", "../src/commands/gh-merge-dev/run-cleanup.ts", "../src/commands/gh-release-deliver/gh-release-deliver.ts", "../src/lib/errors/format-zx-error.ts", "../src/lib/worktrees/log-removal-results.ts", "../src/lib/worktrees/removal-tool-result.ts", "../src/lib/worktrees/remove-worktrees.ts", "../src/lib/worktrees/remove-release-worktree.ts", "../src/commands/gh-release-deploy-all/gh-release-deploy-all.ts", "../src/lib/deploy-form/deploy-form.ts", "../src/lib/workflow-gates/workflow-gates.ts", "../src/lib/workflow-services/workflow-services.ts", "../src/lib/prompts/env-picker.ts", "../src/lib/release-deploy/confirm-deploy.ts", "../src/lib/release-deploy/source-picker.ts", "../src/lib/release-deploy/deploy-source.ts", "../src/lib/release-deploy/resolve-branch.ts", "../src/commands/gh-release-deploy-selected/gh-release-deploy-selected.ts", "../src/commands/gh-release-list/gh-release-list.ts", "../src/commands/local-deploy/deploy-env.ts", "../src/commands/local-deploy/local-deploy.ts", "../src/commands/local-deploy/preflight.ts", "../src/commands/local-deploy/service-discovery.ts", "../src/commands/release-create/release-create.ts", "../src/commands/release-desc-edit/release-desc-edit.ts", "../src/commands/release-remove/release-remove.ts", "../src/integrations/cursor/add-folders-to-workspace.ts", "../src/integrations/cursor/launch-cursor.ts", "../src/integrations/cursor/reconcile-workspace-folders.ts", "../src/integrations/cursor/remove-folders-from-workspace.ts", "../src/integrations/cursor/resolve-workspace-path.ts", "../src/integrations/cursor/open-cursor-workspace.ts", "../src/integrations/zed/add-folders-to-zed-workspace.ts", "../src/integrations/zed/open-zed-workspace.ts", "../src/integrations/zed/reuse-zed-workspace.ts", "../src/lib/assert-never/assert-never.ts", "../src/integrations/ide/add-ide-worktree-folders.ts", "../src/integrations/ide/open-ide-workspace.ts", "../src/integrations/ide/provider-label.ts", "../src/integrations/ide/remove-ide-worktree-folders.ts", "../src/integrations/ide/types.ts", "../src/commands/reopen/reopen.ts", "../src/lib/project-discovery/project-discovery.ts", "../src/lib/dependency-install/dependency-install.ts", "../src/lib/dependency-install/risk-predicate.ts", "../src/lib/dependency-plan/dependency-plan.ts", "../src/commands/setup/converge.ts", "../src/commands/setup/setup.ts", "../src/commands/vendor-check/vendor-check.ts", "../src/lib/vendor/hash.ts", "../src/lib/vendor/manifest.ts", "../src/lib/vendor/skip-sets.ts", "../src/lib/vendor/walk.ts", "../src/commands/version/version.ts", "../src/commands/worktrees-add/worktrees-add.ts", "../src/commands/worktrees-list/worktrees-list.ts", "../src/commands/worktrees-remove/worktrees-remove.ts", "../src/commands/worktrees-sync/worktrees-sync.ts", "../src/lib/command-catalog/command-catalog.ts", "../src/lib/node-warnings/node-warnings.ts"],
|
|
4
|
+
"sourcesContent": ["import process from 'node:process'\nimport { z } from 'zod'\n\nimport { findWorkspaceRoot, resolveAdoption } from 'src/lib/agent-guidance'\nimport { getProjectRoot } from 'src/lib/git-utils'\nimport { logger } from 'src/lib/logger'\nimport { ROOT_DEFAULT_RULES } from 'src/lib/package-config'\nimport type { ResolvedPackageRules } from 'src/lib/package-config'\nimport { discoverPackages, validatePackage } from 'src/lib/package-validator'\nimport type { PackageValidationResult } from 'src/lib/package-validator'\nimport { findPackageRoot } from 'src/lib/package-validator/loader'\nimport { defineMcpTool, textContent } from 'src/types'\n\nimport { runAuditFix } from './fix'\nimport type { FixedEntry } from './fix'\nimport { checkDevPresets } from './preset-proxy-check'\n\n// TODO [DO]: extract `audit` into its own standalone CLI tool, decoupled from infra-kit.\n\ninterface AuditOptions {\n /** Audit every non-vendor workspace package instead of just the current one. */\n all?: boolean\n /** Audit the monorepo root (turbo pipeline + root commands) instead of a package. */\n root?: boolean\n /** Directory to resolve the current package from. Defaults to `process.cwd()`. */\n cwd?: string\n /**\n * Write the guidance block for the audited scope BEFORE checking it. CLI-only \u2014 the MCP\n * input schema has no `fix` key, so an agent cannot reach this (see `auditMcpTool`).\n */\n fix?: boolean\n /** With `fix`: scaffold `DESIGN.md` for `frontend`/`mobile` packages that lack one. CLI-only. */\n design?: boolean\n}\n\n/** The payload `audit()` returns, and the CLI action reads its exit code out of. */\ninterface AuditStructuredContent {\n allPassed: boolean\n packages: { name: string; passed: boolean; checks: PackageValidationResult['checks'] }[]\n /**\n * Present only on a `--fix` run, which is why it is optional rather than conditionally\n * spread: `program.ts` destructures it, so the property has to exist on the type even\n * though it is absent from every response an MCP client can elicit.\n */\n fixed?: FixedEntry[]\n}\n\n/**\n * Printed when a single-package fix flips the workspace into adoption. From that moment every\n * OTHER package's `missing` block stops passing, so the run that caused it has to say so.\n */\nconst ADOPTION_FLIP_LINE =\n 'workspace adopted \u2014 every other package now needs a CLAUDE.md: run infra-kit audit --fix --all'\n\n/** A directory to audit plus the under-the-hood defaults that apply to it. */\ninterface AuditTarget {\n dir: string\n baseline?: Readonly<ResolvedPackageRules>\n /**\n * The monorepo root rather than a package. Carried so `validatePackage` can skip the\n * package-scoped `agent-guidance` check: the root `CLAUDE.md` holds the root marker pair.\n */\n isRoot?: boolean\n}\n\n/**\n * Resolve which directories to audit, and the baseline defaults each uses:\n * `root` \u2192 the monorepo root with {@link ROOT_DEFAULT_RULES}; `all` \u2192 every\n * discovered non-vendor package; otherwise the package walked up from cwd.\n */\nconst resolveTargets = async (options: AuditOptions): Promise<AuditTarget[]> => {\n if (options.root) {\n return [{ dir: await getProjectRoot(), baseline: ROOT_DEFAULT_RULES, isRoot: true }]\n }\n\n if (options.all) {\n const dirs = await discoverPackages(await getProjectRoot())\n\n return dirs.map((dir) => {\n return { dir }\n })\n }\n\n return [{ dir: await findPackageRoot(options.cwd ?? process.cwd()) }]\n}\n\n/**\n * Where the workspace-root walk starts. Repo-wide runs (`--root`, `--all`) already know the\n * project root; a per-package run \u2014 the `infra-kit-check` shape turbo runs once per package \u2014\n * starts at its own directory and walks up to `pnpm-workspace.yaml`. Adoption is a property\n * of the workspace, so both shapes must reach the same verdict.\n */\nconst resolveAdoptionStart = async (options: AuditOptions): Promise<string> => {\n return options.root || options.all ? await getProjectRoot() : (options.cwd ?? process.cwd())\n}\n\n/**\n * Print the audit outcome. Passing checks collapse into the summary line \u2014 only\n * failures print detail \u2014 so a green audit costs one line instead of one per\n * check (this output is read by CI logs and agent loops, where every line is\n * tokens). Returns whether every check passed.\n *\n * @example\n * // green: \u2705 audit passed \u2014 8 checks, 2 targets\n * // red: [FAIL] travelist-monorepo turbo:test: not defined in turbo.json\n * // \u274C audit failed \u2014 1/8 checks, 2 targets\n */\nconst logResults = (results: PackageValidationResult[]): boolean => {\n let total = 0\n let failed = 0\n\n for (const result of results) {\n for (const check of result.checks) {\n total += 1\n\n if (check.status !== 'pass') {\n failed += 1\n\n logger.info(`[FAIL] ${result.packageName} ${check.name}: ${check.message}`)\n }\n }\n }\n\n const allPassed = results.every((result) => {\n return result.passed\n })\n\n const plural = (count: number, noun: string): string => {\n return `${count} ${noun}${count === 1 ? '' : 's'}`\n }\n\n const scope = `${plural(total, 'check')}, ${plural(results.length, 'target')}`\n\n logger.info(allPassed ? `\u2705 audit passed \u2014 ${scope}` : `\u274C audit failed \u2014 ${failed}/${scope}`)\n\n return allPassed\n}\n\n/**\n * Run the CLI-only `--fix` branch and print the adoption-flip line when the run switched\n * enforcement on. Returns the entries `audit()` publishes as `structuredContent.fixed`.\n */\nconst applyFix = async (options: AuditOptions, workspaceRoot: string | null): Promise<FixedEntry[]> => {\n const { fixed, adoptionFlipped } = await runAuditFix(options, workspaceRoot)\n\n if (adoptionFlipped) logger.info(ADOPTION_FLIP_LINE)\n\n return fixed\n}\n\n/**\n * Audit the monorepo root (`root`), every non-vendor workspace package (`all`),\n * or the package resolved by walking up from the working directory (default \u2014\n * the shape used by a package's `\"infra-kit-check\": \"pnpm exec infra-kit audit\"` script). The\n * returned `structuredContent.allPassed` lets the CLI set a non-zero exit code so\n * the audit fails CI; this function never calls `process.exit` so the MCP tool\n * can reuse it.\n *\n * @example\n * // CLI inside packages/serverless-config: `infra-kit audit`\n * await audit() // audits the current package\n * await audit({ all: true }) // audits every non-vendor workspace package\n * await audit({ root: true }) // audits the monorepo root (turbo + root commands)\n */\nexport const audit = async (options: AuditOptions = {}) => {\n const workspaceRoot = findWorkspaceRoot(await resolveAdoptionStart(options))\n // Fix first, check second \u2014 the `eslint --fix` model. `runAuditFix` re-probes adoption after\n // writing, so the `resolveAdoption` below sees the files this run just created.\n const fixed = options.fix ? await applyFix(options, workspaceRoot) : undefined\n const targets = await resolveTargets(options)\n const adoption = await resolveAdoption(workspaceRoot)\n\n const results: PackageValidationResult[] = []\n\n for (const target of targets) {\n results.push(await validatePackage(target.dir, target.baseline, { adoption, isRoot: target.isRoot }))\n }\n\n // Root audit also validates project-level devServersPresets proxy locality (a `local`\n // route override must have its backend launched by the preset). Skipped when the\n // project declares no presets. Only meaningful at the root \u2014 presets live in the\n // project infra-kit.json, not in any single package.\n if (options.root) {\n const presetResult = await checkDevPresets(targets[0]!.dir)\n\n if (presetResult) {\n results.push(presetResult)\n }\n }\n\n const allPassed = logResults(results)\n\n const structuredContent: AuditStructuredContent = {\n allPassed,\n packages: results.map((result) => {\n return {\n name: result.packageName,\n passed: result.passed,\n checks: result.checks,\n }\n }),\n }\n\n // Assigned rather than spread so the key is ABSENT on a non-fix run: `outputSchema` does not\n // document `fixed`, and it stays accurate for every response an MCP client can elicit only\n // while no MCP-reachable call produces one.\n if (fixed) structuredContent.fixed = fixed\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n}\n\nconst auditInputSchema = {\n all: z.boolean().optional().describe('Audit every non-vendor workspace package'),\n root: z.boolean().optional().describe('Audit the monorepo root (turbo pipeline + root commands)'),\n}\n\nconst auditOutputSchema = {\n allPassed: z.boolean().describe('Whether every audited package passed all checks'),\n packages: z\n .array(\n z.object({\n name: z.string(),\n passed: z.boolean(),\n checks: z.array(\n z.object({\n name: z.string(),\n status: z.enum(['pass', 'fail']),\n message: z.string(),\n }),\n ),\n }),\n )\n .describe('Per-package check results'),\n}\n\n// MCP Tool Registration\nexport const auditMcpTool = defineMcpTool({\n name: 'audit',\n description:\n 'Audit packages against infra-kit.config.ts rules (config present and valid, required scripts, required files, and turbo tasks for the root). Defaults to the current package; all=true audits every non-vendor workspace package; root=true audits the monorepo root.',\n inputSchema: auditInputSchema,\n outputSchema: auditOutputSchema,\n // Read-only on purpose: `--fix` and `--design` are NOT reachable here. The MCP boundary\n // auto-confirms every tool call, so a flag that writes into the repo must never be one\n // `handler: audit` away from an agent invoking it.\n //\n // The catalog's `mutating: false` for `audit` stays accurate ONLY while this handler forwards\n // `params.all` / `params.root` BY FIELD and `auditInputSchema` carries no `fix` key. Either a\n // bare `handler: audit` or a `fix` key added to the schema would make the tool mutating while\n // `command-catalog.test.ts`'s fail-closed ungated-mutating gate stayed green \u2014 that gate reads\n // the catalog's `mutating` flag, which nothing recomputes from this handler.\n handler: (params) => {\n return audit({ all: params.all, root: params.root })\n },\n})\n", "import fs from 'node:fs'\nimport path from 'node:path'\n\n// Deep loader path, NOT the `src/lib/package-validator` barrel. That barrel pulls in\n// `package-validator.ts` \u2192 `checks/index` \u2192 `agent-guidance-check` \u2192 this module, a real\n// runtime cycle. The loader subtree imports nothing from `agent-guidance`, so this edge is safe.\nimport { discoverPackages } from 'src/lib/package-validator/loader'\n\nimport { inspectPackageGuidance } from './inspect'\nimport { readGuidanceFile } from './read-guidance-file'\n\n/** The pnpm workspace manifest whose presence defines the workspace root. */\nconst WORKSPACE_FILE = 'pnpm-workspace.yaml'\n\n/** The per-package guidance file the adoption probe looks for. */\nconst GUIDANCE_FILE = 'CLAUDE.md'\n\n/**\n * Whether this workspace has adopted per-package guidance blocks, and the evidence for it.\n *\n * Adoption is inferred from working-tree state: at least one discovered workspace package\n * already carries a well-formed package block. It is deliberately not a bare boolean \u2014\n * the post-adoption `missing` message has to name the package that switched enforcement on,\n * and `workspaceRoot` rides in both variants so that path can be relativized.\n */\nexport type AdoptionState =\n { adopted: true; workspaceRoot: string; evidencePath: string } | { adopted: false; workspaceRoot: string | null }\n\n/**\n * Walk upward from `start` to the nearest directory holding a `pnpm-workspace.yaml`.\n * Returns `null` when there is none \u2014 a per-package `audit` run outside a pnpm workspace\n * is simply never adopted rather than an error.\n *\n * @example\n * findWorkspaceRoot('/repo/apps/client/ui/src')\n * // => '/repo'\n * @example\n * findWorkspaceRoot('/tmp/loose-package')\n * // => null\n */\nexport const findWorkspaceRoot = (start: string): string | null => {\n let current = path.resolve(start)\n\n while (current !== path.dirname(current)) {\n if (fs.existsSync(path.join(current, WORKSPACE_FILE))) return current\n\n current = path.dirname(current)\n }\n\n return fs.existsSync(path.join(current, WORKSPACE_FILE)) ? current : null\n}\n\n/** Per-process memo, keyed by workspace root so a second root in the same process is probed afresh. */\nconst adoptionByRoot = new Map<string, AdoptionState>()\n\n/**\n * Drop the memoized adoption verdicts. Tests only \u2014 a real run resolves one workspace and\n * the memo is what keeps a 27-package `audit --all` from re-probing 27 times.\n *\n * @example\n * resetAdoptionCache()\n * // the next resolveAdoption() call re-reads the filesystem\n */\nexport const resetAdoptionCache = (): void => {\n adoptionByRoot.clear()\n}\n\n/**\n * The uncached probe: the first discovered package (packages come back sorted) whose\n * `CLAUDE.md` inspects as `ok` is the adoption evidence. A malformed or foreign block is\n * explicitly not adoption \u2014 a half-pasted marker pair must not switch enforcement on.\n */\nconst probeAdoption = async (workspaceRoot: string): Promise<AdoptionState> => {\n let packageDirs: string[]\n\n try {\n // `discoverPackages` reads the workspace file with an unguarded readFile + yaml.parse,\n // so proving the file exists does not prove it parses. Any throw degrades to unadopted.\n packageDirs = await discoverPackages(workspaceRoot)\n } catch {\n return { adopted: false, workspaceRoot }\n }\n\n for (const packageDir of packageDirs) {\n const guidancePath = path.join(packageDir, GUIDANCE_FILE)\n const content = readGuidanceFile(guidancePath)\n\n if (content === null) continue\n\n if (inspectPackageGuidance(content).state === 'ok') {\n return { adopted: true, workspaceRoot, evidencePath: guidancePath }\n }\n }\n\n return { adopted: false, workspaceRoot }\n}\n\n/**\n * Resolve whether `workspaceRoot`'s workspace has adopted package guidance blocks,\n * memoized per process. A `null` root (no `pnpm-workspace.yaml` above the target) is\n * never adopted, and needs no probe.\n *\n * @example\n * await resolveAdoption('/repo')\n * // => { adopted: true, workspaceRoot: '/repo', evidencePath: '/repo/apps/client/ui/CLAUDE.md' }\n * @example\n * await resolveAdoption(null)\n * // => { adopted: false, workspaceRoot: null }\n */\nexport const resolveAdoption = async (workspaceRoot: string | null): Promise<AdoptionState> => {\n if (workspaceRoot === null) return { adopted: false, workspaceRoot: null }\n\n const memoized = adoptionByRoot.get(workspaceRoot)\n\n if (memoized) return memoized\n\n const state = await probeAdoption(workspaceRoot)\n\n adoptionByRoot.set(workspaceRoot, state)\n\n return state\n}\n", "import { packageConfigSchema } from '@slip-stream-kit/config/internal'\nimport fs from 'node:fs/promises'\nimport path from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport { z } from 'zod'\n\n// Deep path, never the `agent-guidance` barrel: that barrel reaches `adoption.ts`, which\n// imports this loader subtree back. `package-type.ts` itself imports nothing but `node:path`.\nimport { PACKAGE_TYPES } from 'src/lib/agent-guidance/package-type'\nimport type { PackageType } from 'src/lib/agent-guidance/package-type'\nimport { DEFAULT_RULES, resolvePackageConfig } from 'src/lib/package-config'\nimport type { ResolvedPackageRules } from 'src/lib/package-config'\n\nimport { pathExists } from '../fs-utils'\n\n/** Per-package config filename every validated package must provide. */\nexport const PACKAGE_CONFIG_FILE = 'infra-kit.config.ts'\n\n/**\n * The `package.json` fields the audit and the guidance-block writer read. The three\n * dependency maps are here for `detectPackageType`, which reads them merged.\n */\ninterface PackageJsonShape {\n name?: string\n scripts?: Record<string, string>\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n peerDependencies?: Record<string, string>\n}\n\n/**\n * Read and JSON-parse a package.json, returning an empty object when it is\n * missing or unreadable so callers can degrade into a clear \"missing\" check.\n */\nexport const readPackageJson = async (packageDir: string): Promise<PackageJsonShape> => {\n try {\n const raw = await fs.readFile(path.join(packageDir, 'package.json'), 'utf-8')\n\n return JSON.parse(raw) as PackageJsonShape\n } catch {\n return {}\n }\n}\n\n/** Dynamic-import a config file and resolve its Vite-style factory-or-object default export. */\nconst importConfigExport = async (configPath: string): Promise<unknown> => {\n // Cache-bust with the file mtime so repeated loads (long-running MCP server)\n // pick up edits without a process restart. `.ts` configs load via Node's\n // native type stripping (the repo requires Node >= 24).\n const stat = await fs.stat(configPath)\n const moduleUrl = `${pathToFileURL(configPath).href}?mtime=${Number(stat.mtimeMs)}`\n\n const imported = (await import(moduleUrl)) as { default?: unknown }\n const rawExport = imported.default\n\n if (rawExport === undefined) {\n throw new Error(`${PACKAGE_CONFIG_FILE} at ${configPath} has no default export`)\n }\n\n return typeof rawExport === 'function' ? await (rawExport as () => unknown)() : rawExport\n}\n\n/**\n * Read the optional `type` a package declares in its `infra-kit.config.ts`, or `undefined`\n * when the file is absent, unloadable, or declares no recognised type.\n *\n * Deliberately does **not** go through {@link packageConfigSchema}. That schema ships in the\n * separately-published `@slip-stream-kit/config`, it is a `strictObject`, and the release\n * carrying `type` may not be the one a consumer has installed \u2014 so parsing a config that\n * declares `type` against an older schema throws on the unknown key. Reading the field\n * directly keeps type detection working across that version skew, and keeps this function\n * out of {@link loadPackageConfig}'s return type, which eight `validatePackage` call sites\n * depend on.\n *\n * @example\n * await readDeclaredPackageType('/repo/apps/client/ui')\n * // => 'frontend' (when the config exports `{ type: 'frontend' }`)\n * @example\n * await readDeclaredPackageType('/repo/packages/lib-a')\n * // => undefined (no config, or no `type` key)\n */\nexport const readDeclaredPackageType = async (packageDir: string): Promise<PackageType | undefined> => {\n const configPath = path.join(packageDir, PACKAGE_CONFIG_FILE)\n\n if (!(await pathExists(configPath))) return undefined\n\n try {\n const resolvedExport = await importConfigExport(configPath)\n const declared = (resolvedExport as { type?: unknown } | null)?.type\n\n return PACKAGE_TYPES.find((candidate) => {\n return candidate === declared\n })\n } catch {\n return undefined\n }\n}\n\nexport const loadPackageConfig = async (\n packageDir: string,\n baseline: Readonly<ResolvedPackageRules> = DEFAULT_RULES,\n): Promise<ResolvedPackageRules> => {\n const configPath = path.join(packageDir, PACKAGE_CONFIG_FILE)\n\n if (!(await pathExists(configPath))) {\n throw new Error(`${PACKAGE_CONFIG_FILE} not found at ${configPath}`)\n }\n\n const resolvedExport = await importConfigExport(configPath)\n\n const parsed = packageConfigSchema.safeParse(resolvedExport)\n\n if (!parsed.success) {\n throw new Error(`Invalid ${PACKAGE_CONFIG_FILE} at ${configPath}: ${z.prettifyError(parsed.error)}`)\n }\n\n return resolvePackageConfig(parsed.data, baseline)\n}\n", "import path from 'node:path'\n\n/**\n * The kind of workspace package a guidance block is written for. The repo root\n * has its own body and is deliberately not a `PackageType`.\n */\nexport type PackageType = 'frontend' | 'backend' | 'lib' | 'e2e' | 'mobile'\n\n/** Every package type, in registry order. Useful for exhaustive table-driven tests. */\nexport const PACKAGE_TYPES: readonly PackageType[] = ['frontend', 'backend', 'lib', 'e2e', 'mobile']\n\n/** The `package.json` fields package-type detection reads. */\nexport interface PackageTypeManifest {\n scripts?: Record<string, string>\n dependencies?: Record<string, string>\n devDependencies?: Record<string, string>\n peerDependencies?: Record<string, string>\n}\n\nexport interface DetectPackageTypeArgs {\n /** Absolute path to the package directory. */\n packageDir: string\n /** Absolute path to the repo root the directory convention is measured against. */\n repoRoot: string\n /** The package's parsed `package.json` (an empty object when it is missing or unreadable). */\n pkgJson: PackageTypeManifest\n /** `type` declared in the package's `infra-kit.config.ts`. Wins over every detected signal. */\n declaredType?: PackageType\n}\n\n/** Directory names under `apps/<app>/` that carry declared semantics inside infra-kit. */\nconst APP_DIRECTORY_TYPES: Readonly<Record<string, PackageType>> = {\n ui: 'frontend',\n api: 'backend',\n tests: 'e2e',\n}\n\n/** A directory with this basename is a Capacitor shell wherever it sits in the tree. */\nconst MOBILE_BASENAME = 'mobile-app'\n\n/**\n * Dependency signals, first match wins. `react` alone is deliberately absent:\n * component libraries carry it too, so `vite` + `react` + a `dev` script is what\n * separates an app from a library (handled separately below).\n */\nconst DEPENDENCY_SIGNALS: ReadonlyArray<{ type: PackageType; packages: readonly string[] }> = [\n { type: 'e2e', packages: ['@playwright/test'] },\n { type: 'mobile', packages: ['@capacitor/core', '@capacitor/cli'] },\n // `aws-lambda` / `@types/aws-lambda` are deliberately NOT signals: every consumer backend already\n // lives at `apps/<app>/api` (row 2), while the only packages those two deps would catch are\n // libraries (`packages/lib-core` in both consumers, the infra-kit CLI itself) \u2014 a lambda types\n // dependency alone never means backend, exactly as `react` alone never means frontend.\n { type: 'backend', packages: ['serverless'] },\n]\n\n/**\n * Type implied by where the package sits, or `null` when the path carries no\n * declared semantics. `apps/<app>/{ui,api,tests}` is already load-bearing inside\n * infra-kit (`devServersPresets` keys must be `<app>/api` or `<app>/ui`), so this\n * is the repo's own convention rather than a heuristic \u2014 which is why it beats\n * the dependency signals.\n *\n * @example\n * fromDirectory('/repo/apps/client/ui', '/repo') // => 'frontend'\n * fromDirectory('/repo/packages/mobile-app', '/repo') // => 'mobile'\n */\nconst fromDirectory = (packageDir: string, repoRoot: string): PackageType | null => {\n const segments = path\n .relative(repoRoot, packageDir)\n .split(path.sep)\n .filter((segment) => {\n return segment !== '' && segment !== '.'\n })\n\n const [first, , third] = segments\n\n if (segments.length === 3 && first === 'apps' && third !== undefined) {\n const appType = APP_DIRECTORY_TYPES[third]\n\n if (appType) return appType\n }\n\n return segments.at(-1) === MOBILE_BASENAME ? 'mobile' : null\n}\n\n/**\n * Type implied by the merged `dependencies` / `devDependencies` / `peerDependencies`\n * plus the `dev` script, or `null` when nothing matches.\n *\n * @example\n * fromDependencies({ dependencies: { '@playwright/test': '^1' } }) // => 'e2e'\n * fromDependencies({ dependencies: { react: '^19' } }) // => null (react alone is not a frontend)\n */\nconst fromDependencies = (pkgJson: PackageTypeManifest): PackageType | null => {\n const deps = new Set([\n ...Object.keys(pkgJson.dependencies ?? {}),\n ...Object.keys(pkgJson.devDependencies ?? {}),\n ...Object.keys(pkgJson.peerDependencies ?? {}),\n ])\n\n for (const signal of DEPENDENCY_SIGNALS) {\n if (\n signal.packages.some((name) => {\n return deps.has(name)\n })\n )\n return signal.type\n }\n\n const isViteReactApp = deps.has('vite') && deps.has('react') && Boolean(pkgJson.scripts?.dev)\n\n return isViteReactApp ? 'frontend' : null\n}\n\n/**\n * Resolve a package's type by the \u00A73.2 precedence: an explicit `type` in\n * `infra-kit.config.ts` beats the directory convention, which beats the\n * dependency signals, which fall back to `lib`. There is no `unknown` \u2014 the\n * fallback is the least opinionated body, so an unrecognised package still gets\n * a usable block rather than a failing check.\n *\n * @example\n * detectPackageType({\n * packageDir: '/repo/apps/client/ui',\n * repoRoot: '/repo',\n * pkgJson: { dependencies: { '@playwright/test': '^1' } },\n * })\n * // => 'frontend' (the directory convention beats the dependency signal)\n */\nexport const detectPackageType = ({\n packageDir,\n repoRoot,\n pkgJson,\n declaredType,\n}: DetectPackageTypeArgs): PackageType => {\n return declaredType ?? fromDirectory(packageDir, repoRoot) ?? fromDependencies(pkgJson) ?? 'lib'\n}\n", "import type { InfraKitPackageConfig } from '@slip-stream-kit/config'\n\n/**\n * Audit POLICY: the baselines a package's `infra-kit.config.ts` is resolved against, and the merge\n * that resolves it.\n *\n * The config-authoring surface (`defineConfig`, the `InfraKit*` types, `packageConfigSchema`) lives in\n * `@slip-stream-kit/config` \u2014 a separate npm package a consumer installs locally. The baselines\n * deliberately did NOT move with it: they are decisions this CLI enforces, not a contract a consumer\n * authors against. Shipping them to consumers would freeze audit policy into their lockfiles and make\n * every rule change a coordinated release.\n */\n\n/** Fully-resolved rules with every defaultable field present. */\nexport interface ResolvedPackageRules {\n requiredScripts: string[]\n requiredFiles: string[]\n turboTasks: string[]\n}\n\n/**\n * Baseline rules for a standard TypeScript workspace package, applied to any key\n * a package leaves unset. These are the \"under the hood\" defaults so a conforming\n * package's config can stay empty; non-standard packages override the relevant key.\n */\nexport const DEFAULT_RULES: Readonly<ResolvedPackageRules> = {\n requiredScripts: ['build', 'ts-check', 'eslint-check', 'prettier-check', 'test'],\n requiredFiles: ['tsconfig.json', 'eslint.config.js', 'readme.md'],\n turboTasks: [],\n}\n\n/**\n * Baseline rules for the monorepo root (`infra-kit audit --root`). Checks the\n * root commands, the workspace/turbo files, and that the turbo pipeline defines\n * the expected tasks \u2014 so the root's own config can also stay empty.\n */\nexport const ROOT_DEFAULT_RULES: Readonly<ResolvedPackageRules> = {\n requiredScripts: ['build', 'dev', 'test', 'qa', 'infra-kit-check', 'fix'],\n requiredFiles: ['turbo.json', 'pnpm-workspace.yaml'],\n turboTasks: ['build', 'test', 'ts-check', 'eslint-check', 'prettier-check', 'infra-kit-check'],\n}\n\n/**\n * Merge a parsed package config over a baseline. Each key is replaced wholesale\n * when the package provides it, otherwise the baseline value is used.\n *\n * @example\n * resolvePackageConfig({ requiredScripts: [] })\n * // => { requiredScripts: [], requiredFiles: [...DEFAULT_RULES.requiredFiles], turboTasks: [] }\n */\nexport const resolvePackageConfig = (\n config: InfraKitPackageConfig,\n baseline: Readonly<ResolvedPackageRules> = DEFAULT_RULES,\n): ResolvedPackageRules => {\n return {\n requiredScripts: config.requiredScripts ?? [...baseline.requiredScripts],\n requiredFiles: config.requiredFiles ?? [...baseline.requiredFiles],\n turboTasks: config.turbo?.requiredTasks ?? [...baseline.turboTasks],\n }\n}\n", "import fs from 'node:fs/promises'\n\n/**\n * Resolve whether a path is reachable, suppressing ENOENT into a boolean.\n *\n * @example\n * await pathExists('/etc/hosts') // => true\n * await pathExists('/nope') // => false\n */\nexport const pathExists = async (target: string): Promise<boolean> => {\n try {\n await fs.access(target)\n\n return true\n } catch {\n return false\n }\n}\n", "import fs from 'node:fs/promises'\nimport path from 'node:path'\nimport yaml from 'yaml'\n\nimport { pathExists } from '../fs-utils'\n\nconst WORKSPACE_FILE = 'pnpm-workspace.yaml'\n\n/**\n * List the immediate child directories of `dir`, returning `[]` when the path\n * can't be read (e.g. the parent glob segment matched a non-existent dir).\n */\nconst listChildDirs = async (dir: string): Promise<string[]> => {\n const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => {\n return []\n })\n\n return entries\n .filter((entry) => {\n return entry.isDirectory()\n })\n .map((entry) => {\n return path.join(dir, entry.name)\n })\n}\n\n/**\n * Apply one glob segment to a set of directories: `*` fans out to every child\n * directory, a literal segment keeps the dirs where that child path exists.\n */\nconst expandSegment = async (dirs: string[], segment: string): Promise<string[]> => {\n const next: string[] = []\n\n for (const dir of dirs) {\n if (segment === '*') {\n next.push(...(await listChildDirs(dir)))\n\n continue\n }\n\n const candidate = path.join(dir, segment)\n\n if (await pathExists(candidate)) {\n next.push(candidate)\n }\n }\n\n return next\n}\n\n/**\n * Expand a single pnpm-workspace glob (only the `*` segment wildcard is\n * supported, which covers every pattern this monorepo uses) into directories.\n */\nconst expandGlob = async (projectRoot: string, pattern: string): Promise<string[]> => {\n let dirs = [projectRoot]\n\n for (const segment of pattern.split('/')) {\n dirs = await expandSegment(dirs, segment)\n }\n\n return dirs\n}\n\n/**\n * Discover validatable workspace packages from `pnpm-workspace.yaml`.\n *\n * Negation patterns (`!\u2026`) and everything under `vendor/` are excluded \u2014\n * vendor is mirrored from `starter-workspace` and is checksum-enforced by\n * `pnpm vendor:check`, so its configs are owned upstream, not here. Only\n * directories that actually contain a package.json are returned.\n *\n * @example\n * await discoverPackages('/repo')\n * // => ['/repo/apps/infra-kit/cli', '/repo/packages/serverless-config']\n */\nexport const discoverPackages = async (projectRoot: string): Promise<string[]> => {\n const raw = await fs.readFile(path.join(projectRoot, WORKSPACE_FILE), 'utf-8')\n const parsed = (yaml.parse(raw) ?? {}) as { packages?: string[] }\n\n const patterns = (parsed.packages ?? []).filter((pattern) => {\n return !pattern.startsWith('!') && !pattern.startsWith('vendor')\n })\n\n const found = new Set<string>()\n\n for (const pattern of patterns) {\n const dirs = await expandGlob(projectRoot, pattern)\n\n for (const dir of dirs) {\n if (await pathExists(path.join(dir, 'package.json'))) {\n found.add(dir)\n }\n }\n }\n\n return [...found].sort()\n}\n", "import path from 'node:path'\n\nimport { pathExists } from '../fs-utils'\n\n/**\n * Walk upward from `start` to the nearest directory containing a package.json.\n * Used so `infra-kit audit` (no `--all`) targets the package whose package.json\n * script invoked it, regardless of the exact working directory.\n *\n * @example\n * await findPackageRoot('/repo/packages/serverless-config/src')\n * // => '/repo/packages/serverless-config'\n */\nexport const findPackageRoot = async (start: string): Promise<string> => {\n let current = path.resolve(start)\n\n while (current !== path.dirname(current)) {\n if (await pathExists(path.join(current, 'package.json'))) {\n return current\n }\n\n current = path.dirname(current)\n }\n\n if (await pathExists(path.join(current, 'package.json'))) {\n return current\n }\n\n throw new Error(\n `No package.json found in or above ${start} \u2014 cd into a package directory, or pass --root to audit a monorepo root`,\n )\n}\n", "/**\n * Generic \"managed block\" helpers \u2014 a marker-delimited region a tool owns and\n * rewrites idempotently while never touching content outside the markers.\n *\n * This is the same mechanism `infra-kit setup` already uses for the `~/.zshrc`\n * shell block (`# -- infra-kit:begin -- \u2026 # -- infra-kit:end --`), lifted here so\n * it can be reused for the repo agent-instruction guidance block in `CLAUDE.md`.\n * It mirrors the design of OMC's\n * `<!-- OMC:START --> \u2026 <!-- OMC:END -->` CLAUDE.md installer.\n */\n\n/**\n * Where a freshly upserted block lands relative to existing content.\n *\n * - `replace-in-place`: if the block already exists, rewrite it where it sits\n * (surrounding text untouched, verbatim); if absent, append it at end-of-file.\n * - `append-end`: strip any existing block from wherever it is, then append the\n * fresh block at end-of-file (i.e. always relocate to the end). This preserves\n * the historical `~/.zshrc` behavior of `removeExistingBlock` + append.\n */\nexport type BlockPlacement = 'replace-in-place' | 'append-end'\n\nexport interface UpsertManagedBlockArgs {\n /** Existing file content (`''` for a new file). */\n content: string\n /** Inner body to place between the markers (WITHOUT the markers). */\n body: string\n startMarker: string\n endMarker: string\n /** Defaults to `replace-in-place`. */\n placement?: BlockPlacement\n}\n\n/**\n * Whether `content` contains a well-formed `start \u2026 end` block. A reversed pair\n * (end before start) is treated as absent \u2014 the same guard `doctor.ts` applies\n * to the zshrc block, ported here so corrupted markers never match.\n *\n * @example\n * hasManagedBlock('a<!--s-->x<!--e-->b', '<!--s-->', '<!--e-->') // => true\n * hasManagedBlock('<!--e--><!--s-->', '<!--s-->', '<!--e-->') // => false (reversed)\n */\nexport const hasManagedBlock = (content: string, startMarker: string, endMarker: string): boolean => {\n const startIdx = content.indexOf(startMarker)\n const endIdx = content.indexOf(endMarker)\n\n return startIdx !== -1 && endIdx !== -1 && endIdx >= startIdx\n}\n\n/**\n * Remove the first complete `start \u2026 end` block, preserving surrounding text.\n * Returns `null` when no well-formed block is present (no markers, or reversed),\n * so callers can fall through to legacy-format handling \u2014 this matches the\n * `string | null` contract of `init.ts`'s original `removeBetween`, with the\n * added reversed-marker guard.\n *\n * @example\n * removeManagedBlock('top\\n<!--s-->mid<!--e-->\\nbot', '<!--s-->', '<!--e-->')\n * // => 'top\\nbot'\n */\nexport const removeManagedBlock = (content: string, startMarker: string, endMarker: string): string | null => {\n const startIdx = content.indexOf(startMarker)\n const endIdx = content.indexOf(endMarker)\n\n if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) return null\n\n // eslint-disable-next-line sonarjs/super-linear-regex\n const before = content.slice(0, startIdx).replace(/\\n+$/, '')\n const after = content.slice(endIdx + endMarker.length).replace(/^\\n+/, '')\n\n return before + (after ? `\\n${after}` : '')\n}\n\n/**\n * Read the version token that follows `versionPrefix` (e.g.\n * `'<!-- infra-kit:version '`). Returns the token up to the next whitespace or\n * `>`, or `null` if the prefix is absent. Mirrors OMC's `OMC:VERSION:` line.\n *\n * @example\n * extractVersion('<!-- infra-kit:version 0.1.105 -->', '<!-- infra-kit:version ')\n * // => '0.1.105'\n */\nexport const extractVersion = (content: string, versionPrefix: string): string | null => {\n const idx = content.indexOf(versionPrefix)\n\n if (idx === -1) return null\n\n const rest = content.slice(idx + versionPrefix.length)\n const match = rest.match(/^([^\\s>]+)/)\n\n return match ? match[1]! : null\n}\n\n/**\n * Compose a full block string: `start\\n{body}\\nend`. Kept identical in shape to\n * the historical `buildShellBlock()` output so doctor's exact-match comparison\n * stays valid.\n */\nexport const buildManagedBlock = (startMarker: string, body: string, endMarker: string): string => {\n return `${startMarker}\\n${body}\\n${endMarker}`\n}\n\n/**\n * Insert or update a managed block in `content`, preserving everything outside\n * the markers. Idempotent: re-running with the same body yields the same block\n * and never nests duplicates.\n *\n * @example\n * upsertManagedBlock({ content: '', body: 'hi', startMarker: '<!--s-->', endMarker: '<!--e-->' })\n * // => '<!--s-->\\nhi\\n<!--e-->\\n'\n * @example\n * upsertManagedBlock({ content: 'top\\n<!--s-->\\nold\\n<!--e-->\\nbot', body: 'new',\n * startMarker: '<!--s-->', endMarker: '<!--e-->' })\n * // => 'top\\n<!--s-->\\nnew\\n<!--e-->\\nbot' (surrounding text kept)\n */\nexport const upsertManagedBlock = ({\n content,\n body,\n startMarker,\n endMarker,\n placement = 'replace-in-place',\n}: UpsertManagedBlockArgs): string => {\n const block = buildManagedBlock(startMarker, body, endMarker)\n const present = hasManagedBlock(content, startMarker, endMarker)\n\n if (placement === 'replace-in-place' && present) {\n const startIdx = content.indexOf(startMarker)\n const endIdx = content.indexOf(endMarker) + endMarker.length\n\n return content.slice(0, startIdx) + block + content.slice(endIdx)\n }\n\n // append-end, or replace-in-place with no existing block: drop any stale block\n // then append the fresh one at end-of-file.\n const stripped = present ? (removeManagedBlock(content, startMarker, endMarker) ?? content) : content\n // eslint-disable-next-line sonarjs/super-linear-regex\n const base = stripped.replace(/\\n+$/, '')\n\n return base.length > 0 ? `${base}\\n${block}\\n` : `${block}\\n`\n}\n", "/**\n * Marker and version-line constants for the two managed guidance blocks.\n *\n * The repo root block lives in the repo's `CLAUDE.md`; every workspace package\n * gets its own block in its own `CLAUDE.md`. The two pairs are deliberately\n * distinct strings, and neither marker contains the other, so the `indexOf`\n * matching in `src/lib/managed-block` can never confuse them: a root block\n * pasted into a package directory is detectable as its own failure state\n * instead of silently counting as a package block.\n */\n\n/** Start marker of the repo-root guidance block (HTML comment \u2014 invisible in rendered markdown). */\nexport const ROOT_MARKER_START = '<!-- infra-kit:begin -->'\n/** End marker of the repo-root guidance block. */\nexport const ROOT_MARKER_END = '<!-- infra-kit:end -->'\n/**\n * Prefix of the root block's version line. The version rides on its own line\n * (OMC's `OMC:VERSION:` precedent) so the markers themselves stay constant-matchable.\n */\nexport const ROOT_VERSION_PREFIX = '<!-- infra-kit:version '\n\n/** Start marker of a per-package guidance block. */\nexport const PACKAGE_MARKER_START = '<!-- infra-kit:package:begin -->'\n/** End marker of a per-package guidance block. */\nexport const PACKAGE_MARKER_END = '<!-- infra-kit:package:end -->'\n/**\n * Prefix of a package block's version line. The line carries the CLI version\n * first and the resolved package type second \u2014 `<!-- infra-kit:package:version 0.4.0 frontend -->` \u2014\n * so `extractVersion` (which reads to the first whitespace or `>`) returns the\n * version unchanged and a separate one-line parse reads the type.\n */\nexport const PACKAGE_VERSION_PREFIX = '<!-- infra-kit:package:version '\n", "import { extractVersion, hasManagedBlock } from 'src/lib/managed-block'\n\nimport {\n PACKAGE_MARKER_END,\n PACKAGE_MARKER_START,\n PACKAGE_VERSION_PREFIX,\n ROOT_MARKER_END,\n ROOT_MARKER_START,\n} from './markers'\nimport { PACKAGE_TYPES } from './package-type'\nimport type { PackageType } from './package-type'\n\n/**\n * What a package's `CLAUDE.md` currently holds.\n *\n * - `ok` \u2014 a well-formed package block with a non-empty body.\n * - `missing` \u2014 the file itself does not exist.\n * - `no-block` \u2014 the file exists but carries no infra-kit markers at all.\n * - `malformed` \u2014 package markers are present but unusable: reversed, half a\n * pair, or an empty body.\n * - `foreign-block` \u2014 no package block, but the repo-root block is there, i.e.\n * the root `CLAUDE.md` was pasted into a package directory.\n */\nexport type GuidanceState = 'ok' | 'missing' | 'no-block' | 'malformed' | 'foreign-block'\n\nexport interface PackageGuidanceInspection {\n state: GuidanceState\n /** CLI version recorded in the block's version line. Only set when the state is `ok`. */\n version?: string\n /**\n * Package type recorded in the block's version line, validated against `PACKAGE_TYPES`.\n *\n * Absent when the line carries no type token, and equally when it names a type this CLI\n * does not know \u2014 the version line is read out of a file on disk, so an unvalidated token\n * is not necessarily a type at all. Callers render the absence as `unknown` rather than\n * repeating it as fact.\n */\n type?: PackageType\n}\n\n/** The text between the package markers, markers excluded. Assumes a well-formed pair. */\nconst readBlockBody = (content: string): string => {\n const start = content.indexOf(PACKAGE_MARKER_START) + PACKAGE_MARKER_START.length\n\n return content.slice(start, content.indexOf(PACKAGE_MARKER_END))\n}\n\n/**\n * The package type that follows the version on the version line, or `null`.\n *\n * The captured token is checked against `PACKAGE_TYPES` rather than trusted as written. The\n * version line ends in `-->`, so a block carrying no type token at all\n * (`<!-- infra-kit:package:version 0.4.0 -->`) otherwise captures `--` and reports the marker\n * terminator as the package's type.\n */\nconst readType = (body: string): PackageType | null => {\n const idx = body.indexOf(PACKAGE_VERSION_PREFIX)\n\n if (idx === -1) return null\n\n const token = body.slice(idx + PACKAGE_VERSION_PREFIX.length).match(/^\\S+\\s+([^\\s>]+)/)?.[1]\n\n return (\n PACKAGE_TYPES.find((candidate) => {\n return candidate === token\n }) ?? null\n )\n}\n\n/**\n * Classify a package `CLAUDE.md`. Pure \u2014 the caller reads the file (passing\n * `null` when it does not exist) so the audit check, the `--fix` writer and\n * `doctor` all share one state machine.\n *\n * @example\n * inspectPackageGuidance(null)\n * // => { state: 'missing' }\n * @example\n * inspectPackageGuidance(\n * '<!-- infra-kit:package:begin -->\\n<!-- infra-kit:package:version 0.4.0 lib -->\\nx\\n<!-- infra-kit:package:end -->',\n * )\n * // => { state: 'ok', version: '0.4.0', type: 'lib' }\n */\nexport const inspectPackageGuidance = (content: string | null): PackageGuidanceInspection => {\n if (content === null) return { state: 'missing' }\n\n if (!hasManagedBlock(content, PACKAGE_MARKER_START, PACKAGE_MARKER_END)) {\n const halfPair = content.includes(PACKAGE_MARKER_START) || content.includes(PACKAGE_MARKER_END)\n\n if (halfPair) return { state: 'malformed' }\n\n return hasManagedBlock(content, ROOT_MARKER_START, ROOT_MARKER_END)\n ? { state: 'foreign-block' }\n : { state: 'no-block' }\n }\n\n const body = readBlockBody(content)\n\n if (body.trim() === '') return { state: 'malformed' }\n\n const inspection: PackageGuidanceInspection = { state: 'ok' }\n const version = extractVersion(body, PACKAGE_VERSION_PREFIX)\n const type = readType(body)\n\n if (version !== null) inspection.version = version\n if (type !== null) inspection.type = type\n\n return inspection\n}\n", "import fs from 'node:fs'\n\n/**\n * A guidance file's content, or `null` when it does not exist or cannot be read.\n *\n * `null` is exactly the input `inspectPackageGuidance` reads as `missing`, so every read-only\n * reader of a `CLAUDE.md` \u2014 the adoption probe, the audit check and `doctor` \u2014 funnels an\n * unreadable file into that one state instead of throwing out of an inspection.\n *\n * Swallowing the error is deliberate here and wrong for a writer: `agent-guidance.ts` keeps its\n * own throwing read, because treating an existing-but-unreadable file as absent would let the\n * upsert overwrite hand-authored content it never managed to see.\n *\n * @example\n * readGuidanceFile('/repo/packages/lib-a/CLAUDE.md')\n * // => '# notes\\n<!-- infra-kit:package:begin -->\u2026'\n * @example\n * readGuidanceFile('/repo/packages/lib-a/absent.md')\n * // => null\n */\nexport const readGuidanceFile = (filePath: string): string | null => {\n try {\n return fs.readFileSync(filePath, 'utf-8')\n } catch {\n return null\n }\n}\n", "import fs from 'node:fs'\nimport path from 'node:path'\n\nimport { removeManagedBlock, upsertManagedBlock } from 'src/lib/managed-block'\n// Deep loader path, not the `package-validator` barrel \u2014 see the note in `adoption.ts`.\nimport { readPackageJson } from 'src/lib/package-validator/loader'\n\nimport { buildDesignSkeleton } from './bodies/design-skeleton'\nimport { buildPackageBody } from './bodies/package-body'\nimport { buildRootBody } from './bodies/root-body'\nimport { PACKAGE_MARKER_END, PACKAGE_MARKER_START, ROOT_MARKER_END, ROOT_MARKER_START } from './markers'\nimport type { PackageType } from './package-type'\nimport { detectPackageType } from './package-type'\nimport { assertBlockPresent, assertNotSymlink, assertOutsideMarkersUnchanged, writeManaged } from './write-managed-file'\nimport type { WriteAction } from './write-managed-file'\n\n/**\n * Marker pair for the legacy `@AGENTS.md` import region once injected into `CLAUDE.md`.\n * Migration-only: `syncRootGuidance` strips this region so the full guidance body can take\n * its place. TODO(remove after ~2 release cycles once repos have re-run init).\n */\nexport const LEGACY_IMPORT_START = '<!-- infra-kit:import:begin -->'\nexport const LEGACY_IMPORT_END = '<!-- infra-kit:import:end -->'\n\nconst AGENTS_FILE = 'AGENTS.md'\nconst CLAUDE_FILE = 'CLAUDE.md'\nconst README_FILE = 'README.md'\nconst DESIGN_FILE = 'DESIGN.md'\n\n/** Types that own a visual language, and so can be given a `DESIGN.md` skeleton. */\nconst DESIGN_TYPES: readonly PackageType[] = ['frontend', 'mobile']\n\n/** One file touched by a guidance sync. */\nexport interface GuidanceWrite {\n /** Absolute path of the file. */\n path: string\n action: WriteAction\n /** Resolved package type; absent for the repo-root files. */\n type?: PackageType\n /** Why the write failed. Only set when `action` is `failed`. */\n message?: string\n}\n\nexport interface SyncRootGuidanceOptions {\n /** CLI version recorded in the block's version line. */\n version: string\n}\n\nexport interface SyncPackageGuidanceOptions {\n /** Repo root the package's relative directory and type convention are measured against. */\n repoRoot: string\n /** CLI version recorded in the block's version line. */\n version: string\n /** Scaffold a `DESIGN.md` skeleton for `frontend`/`mobile` packages that lack one. */\n design?: boolean\n /** `type` declared in the package's `infra-kit.config.ts`. Wins over every detected signal. */\n declaredType?: PackageType\n}\n\n/**\n * File content, or `null` when the file does not exist.\n *\n * Deliberately NOT the swallowing `readGuidanceFile` the read-only inspectors share: this\n * result is the baseline an upsert preserves, so an existing-but-unreadable file must throw\n * (surfacing as `action: 'failed'`) rather than read as absent and be overwritten wholesale.\n */\nconst readExistingFile = (filePath: string): string | null => {\n return fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : null\n}\n\n/** Whether `dir` holds an exactly-cased `name`, decided from the directory listing. */\nconst hasExactFile = (dir: string, name: string): boolean => {\n try {\n return fs.readdirSync(dir).includes(name)\n } catch {\n return false\n }\n}\n\n/** The failure entry every catch in this module produces. */\nconst failedWrite = (filePath: string, error: unknown, type?: PackageType): GuidanceWrite => {\n const message = error instanceof Error ? error.message : String(error)\n const entry: GuidanceWrite = { path: filePath, action: 'failed', message }\n\n if (type) entry.type = type\n\n return entry\n}\n\ninterface UpsertFileArgs {\n filePath: string\n body: string\n startMarker: string\n endMarker: string\n /**\n * Rewrite the file's existing content before the block is upserted into it \u2014 the root file\n * strips its legacy `@AGENTS.md` import region this way. The unchanged-bytes assertion runs\n * against this result, so what it removes counts as removed by intent rather than as damage.\n */\n prepare?: (content: string) => string\n}\n\n/**\n * Upsert a managed block into one file, then assert what the write promised: the block is\n * present, and \u2014 on the replace-in-place path only \u2014 every byte outside the markers survived.\n */\nconst upsertGuidanceFile = ({ filePath, body, startMarker, endMarker, prepare }: UpsertFileArgs): WriteAction => {\n const before = readExistingFile(filePath)\n const baseline = before === null ? '' : (prepare?.(before) ?? before)\n\n const next = upsertManagedBlock({\n content: baseline,\n body,\n startMarker,\n endMarker,\n placement: 'replace-in-place',\n })\n\n const action = writeManaged(filePath, next)\n\n assertBlockPresent(filePath, startMarker, endMarker)\n\n // First insertion is exempt: `upsertManagedBlock` normalizes trailing newlines when no\n // block is there yet, which legitimately changes bytes outside the markers exactly once.\n if (before !== null) assertOutsideMarkersUnchanged(baseline, next, startMarker, endMarker)\n\n return action\n}\n\n/**\n * Migrate a legacy `AGENTS.md` now that the guidance lives solely in `CLAUDE.md`.\n * Strips the infra-kit managed block, then asymmetrically:\n * - no infra-kit block at all (hand-authored, not ours) \u2192 leave untouched (`unchanged`),\n * - file was purely generated (nothing but whitespace remains) \u2192 delete (`removed`),\n * - hand-authored content surrounds the block \u2192 write the block-free remainder (`updated`).\n */\nconst migrateLegacyAgentsFile = (agentsPath: string): WriteAction => {\n if (!fs.existsSync(agentsPath)) return 'unchanged'\n\n assertNotSymlink(agentsPath)\n\n const content = fs.readFileSync(agentsPath, 'utf-8')\n const stripped = removeManagedBlock(content, ROOT_MARKER_START, ROOT_MARKER_END)\n\n if (stripped === null) return 'unchanged'\n\n if (stripped.trim() === '') {\n fs.rmSync(agentsPath)\n\n return 'removed'\n }\n\n return writeManaged(agentsPath, stripped)\n}\n\n/** The root `CLAUDE.md` write: strip the legacy import region, then upsert the root block. */\nconst syncRootClaudeFile = (claudePath: string, version: string): WriteAction => {\n return upsertGuidanceFile({\n filePath: claudePath,\n body: buildRootBody(version),\n startMarker: ROOT_MARKER_START,\n endMarker: ROOT_MARKER_END,\n prepare: (content) => {\n return removeManagedBlock(content, LEGACY_IMPORT_START, LEGACY_IMPORT_END) ?? content\n },\n })\n}\n\n/**\n * Refresh the repo-root guidance block in `<root>/CLAUDE.md`, preserving every hand-authored\n * byte outside the markers, and migrate a legacy `AGENTS.md` away.\n *\n * Never throws. A per-file error comes back as an entry with `action: 'failed'` and a\n * `message`, so a caller syncing many files can continue and report.\n *\n * @example\n * await syncRootGuidance('/repo', { version: '0.4.0' })\n * // => [{ path: '/repo/CLAUDE.md', action: 'created' }, { path: '/repo/AGENTS.md', action: 'unchanged' }]\n */\nexport const syncRootGuidance = async (\n root: string,\n { version }: SyncRootGuidanceOptions,\n): Promise<GuidanceWrite[]> => {\n const claudePath = path.join(root, CLAUDE_FILE)\n const agentsPath = path.join(root, AGENTS_FILE)\n const written: GuidanceWrite[] = []\n\n try {\n written.push({ path: claudePath, action: syncRootClaudeFile(claudePath, version) })\n } catch (error) {\n written.push(failedWrite(claudePath, error))\n }\n\n try {\n written.push({ path: agentsPath, action: migrateLegacyAgentsFile(agentsPath) })\n } catch (error) {\n written.push(failedWrite(agentsPath, error))\n }\n\n return written\n}\n\n/** Scaffold `DESIGN.md` for a type that owns a visual language, never overwriting an existing file. */\nconst syncDesignFile = (packageDir: string, packageName: string, type: PackageType): GuidanceWrite | null => {\n if (!DESIGN_TYPES.includes(type) || hasExactFile(packageDir, DESIGN_FILE)) return null\n\n const designPath = path.join(packageDir, DESIGN_FILE)\n\n try {\n return {\n path: designPath,\n action: writeManaged(designPath, buildDesignSkeleton(packageName)),\n type,\n }\n } catch (error) {\n return failedWrite(designPath, error, type)\n }\n}\n\n/**\n * Write (or refresh) one workspace package's guidance block in its own `CLAUDE.md`, selecting\n * the body by detected package type and preserving every hand-authored byte outside the\n * markers.\n *\n * Never throws: a per-file error comes back as `action: 'failed'` with a `message`, because a\n * run that aborted halfway could leave one well-formed block behind, which adopts the whole\n * workspace and reddens every package the run never reached.\n *\n * @example\n * await syncPackageGuidance('/repo/apps/client/ui', { repoRoot: '/repo', version: '0.4.0' })\n * // => [{ path: '/repo/apps/client/ui/CLAUDE.md', action: 'created', type: 'frontend' }]\n */\nexport const syncPackageGuidance = async (\n packageDir: string,\n { repoRoot, version, design, declaredType }: SyncPackageGuidanceOptions,\n): Promise<GuidanceWrite[]> => {\n const claudePath = path.join(packageDir, CLAUDE_FILE)\n const pkgJson = await readPackageJson(packageDir)\n const packageName = pkgJson.name ?? path.basename(packageDir)\n const type = detectPackageType({ packageDir, repoRoot, pkgJson, declaredType })\n\n const body = buildPackageBody({\n version,\n type,\n packageName,\n relDir: path.relative(repoRoot, packageDir).split(path.sep).join('/'),\n hasReadme: hasExactFile(packageDir, README_FILE),\n hasDesign: hasExactFile(packageDir, DESIGN_FILE),\n })\n\n const written: GuidanceWrite[] = []\n\n try {\n written.push({\n path: claudePath,\n action: upsertGuidanceFile({\n filePath: claudePath,\n body,\n startMarker: PACKAGE_MARKER_START,\n endMarker: PACKAGE_MARKER_END,\n }),\n type,\n })\n } catch (error) {\n written.push(failedWrite(claudePath, error, type))\n }\n\n if (design) {\n const designWrite = syncDesignFile(packageDir, packageName, type)\n\n if (designWrite) written.push(designWrite)\n }\n\n return written\n}\n", "---\nname: TODO\ndescription: TODO \u2014 one sentence describing the product surface this package renders.\ncolors:\n primary: '#2563eb'\n secondary: '#64748b'\n background: '#ffffff'\n surface: '#f8fafc'\n text: '#0f172a'\n muted: '#64748b'\n border: '#e2e8f0'\n success: '#16a34a'\n warning: '#d97706'\n danger: '#dc2626'\ntypography:\n fontFamily: TODO \u2014 the UI typeface stack.\n scale: [12, 14, 16, 20, 24, 32, 40]\n weights: [400, 500, 600, 700]\nrounded: 8px\nspacing: [4, 8, 12, 16, 24, 32, 48]\ncomponents:\n - button\n - input\n - card\n - dialog\n---\n\n# Design \u2014 {{packageName}}\n\nReplace every TODO below with the real design language. This file is the source of truth\nfor UI decisions in this package; agents are instructed to read it before styling anything.\n\n## Overview\n\nTODO \u2014 what this product is, who uses it, and the feeling the interface should convey.\n\n## Colors\n\nTODO \u2014 what each token in the front matter means and when to reach for it. Name the\ncontrast pairs that are guaranteed accessible.\n\n## Typography\n\nTODO \u2014 the typeface, the scale, and which size and weight each role uses\n(page title, section title, body, caption, label).\n\n## Layout\n\nTODO \u2014 the grid, the page gutters, the maximum content width, and the breakpoints.\n\n## Elevation & Depth\n\nTODO \u2014 the shadow levels and what sits at each one (base, raised, overlay, modal).\n\n## Shapes\n\nTODO \u2014 corner radii per component size, border widths, and where a shape is square by intent.\n\n## Components\n\nTODO \u2014 for each component in the front matter list: its variants, its states\n(default, hover, active, focus, disabled, loading), and its sizes.\n\n## Do's and Don'ts\n\n- Do TODO.\n- Do TODO.\n- Don't TODO.\n- Don't TODO.\n", "# {{packageName}}\n\nWorkspace package at `{{relDir}}` \u2014 type: **{{type}}**.\nThis block is generated by `infra-kit audit --fix` \u2014 edit text _outside_ the markers, never inside.\n\n## Read first\n\n- {{readmeBullet}}\n- `infra-kit.config.ts` \u2014 the audit rules this package must satisfy.\n\n## Checks\n\n- `infra-kit audit` \u2014 validate this package against its infra-kit rules.\n- `pnpm run ts-check && pnpm run eslint-check && pnpm run test` \u2014 this package only.\n- `pnpm run qa` at the repo root \u2014 the full gate; run it before claiming completion.\n\n## Rules\n\n- Module-scope state belongs in the handler entry file \u2014 that is the file which re-evaluates on reload.\n- Env vars come from Doppler via `ik env-load`; never commit a secret.\n- The route prefix this service answers is declared in the consuming UI's `dev.proxy`, so changing a path is a two-package change.\n", "# {{packageName}}\n\nWorkspace package at `{{relDir}}` \u2014 type: **{{type}}**.\nThis block is generated by `infra-kit audit --fix` \u2014 edit text _outside_ the markers, never inside.\n\n## Read first\n\n- {{readmeBullet}}\n- `infra-kit.config.ts` \u2014 the audit rules this package must satisfy.\n\n## Checks\n\n- `infra-kit audit` \u2014 validate this package against its infra-kit rules.\n- `pnpm run ts-check && pnpm run eslint-check && pnpm run test` \u2014 this package only.\n- `pnpm run qa` at the repo root \u2014 the full gate; run it before claiming completion.\n\n## Rules\n\n- Specs run on Playwright.\n- Every spec cleans up what it creates, in teardown.\n- Selectors live in Page Objects, never inline in specs.\n- Never point a run at a production environment.\n", "# {{packageName}}\n\nWorkspace package at `{{relDir}}` \u2014 type: **{{type}}**.\nThis block is generated by `infra-kit audit --fix` \u2014 edit text _outside_ the markers, never inside.\n\n## Read first\n\n- {{readmeBullet}}\n- `DESIGN.md` \u2014 the visual language (colors, typography, spacing, components). It is the source of truth for UI decisions; if it is absent, ask before inventing one.\n- `infra-kit.config.ts` \u2014 the audit rules this package must satisfy.\n\n## Checks\n\n- `infra-kit audit` \u2014 validate this package against its infra-kit rules.\n- `pnpm run ts-check && pnpm run eslint-check && pnpm run test` \u2014 this package only.\n- `pnpm run qa` at the repo root \u2014 the full gate; run it before claiming completion.\n\n## Rules\n\n- Start the app with `infra-kit dev`, never a bare `vite` \u2014 dev URLs are proxied and port-free.\n- Backend calls go through the path prefixes declared in `dev.proxy` in `infra-kit.config.ts`. Never hardcode an API base URL or a port.\n- Use the tokens in `DESIGN.md` rather than ad-hoc colors, spacing, or type scales.\n", "# {{packageName}}\n\nWorkspace package at `{{relDir}}` \u2014 type: **{{type}}**.\nThis block is generated by `infra-kit audit --fix` \u2014 edit text _outside_ the markers, never inside.\n\n## Read first\n\n- {{readmeBullet}}\n- `infra-kit.config.ts` \u2014 the audit rules this package must satisfy.\n\n## Checks\n\n- `infra-kit audit` \u2014 validate this package against its infra-kit rules.\n- `pnpm run ts-check && pnpm run eslint-check && pnpm run test` \u2014 this package only.\n- `pnpm run qa` at the repo root \u2014 the full gate; run it before claiming completion.\n\n## Rules\n\n- The public surface is the `exports` map in `package.json`.\n- Changing that map is a breaking change for every dependent package.\n- Run `pnpm run build` before a dependent package can see a change.\n- No app-specific logic belongs here.\n", "# {{packageName}}\n\nWorkspace package at `{{relDir}}` \u2014 type: **{{type}}**.\nThis block is generated by `infra-kit audit --fix` \u2014 edit text _outside_ the markers, never inside.\n\n## Read first\n\n- {{readmeBullet}}\n- `DESIGN.md` \u2014 the visual language (colors, typography, spacing, components). It is the source of truth for UI decisions; if it is absent, ask before inventing one.\n- `infra-kit.config.ts` \u2014 the audit rules this package must satisfy.\n\n## Checks\n\n- `infra-kit audit` \u2014 validate this package against its infra-kit rules.\n- `pnpm run ts-check && pnpm run eslint-check && pnpm run test` \u2014 this package only.\n- `pnpm run qa` at the repo root \u2014 the full gate; run it before claiming completion.\n\n## Rules\n\n- The native shell is Capacitor; its settings live in `capacitor.config.*`.\n- The web build feeds the native shell, so a device run needs a rebuild first.\n- Use the tokens in `DESIGN.md` rather than ad-hoc colors, spacing, or type scales.\n", "# infra-kit\n\nThis repository uses the **infra-kit** CLI for environment, worktree, and release workflows.\nThis block is generated by `ik audit --fix --root` \u2014 edit text _outside_ the markers, never inside.\n\n## Commands (`ik` = `pnpm exec infra-kit`)\n\n- `ik env-load -c <config>` / `ik env-clear` / `ik env-status` \u2014 load, clear, or inspect Doppler env vars for a config (e.g. `dev`). Source the returned file to apply.\n- `ik worktrees add` / `worktrees list` / `reopen` / `worktrees remove` / `worktrees sync` \u2014 manage release and feature git worktrees.\n- `ik release create` / `release remove` / `release list` / `release deploy-all` / `release deploy-selected` / `release deliver` / `release desc-edit` \u2014 release-branch and deploy flow.\n- `ik release merge-dev` \u2014 merge the dev branch into every release branch.\n- `ik audit` \u2014 audit packages against `infra-kit.config.ts` rules.\n- `ik audit --fix` \u2014 regenerate this repo's infra-kit guidance blocks (`--all` for every package, `--root` for this file).\n- `ik doctor` \u2014 check gh / doppler / aws install + auth and repo setup.\n- `ik setup` \u2014 set up infra-kit on this machine: shell integration, the Claude Code plugin, the `.mcp.json` key, and the external CLIs (brew, aws, gh, doppler, portless).\n\n## Conventions\n\n- Tickets are prefixed by area: `[FE]` frontend, `[BE]` backend, `[DO]` DevOps/infra, `[APP]` mobile app, `[IDEA]` proposal, `[ROOT]` cross-cutting. Pick the single dominant area.\n- Environment variables are managed through Doppler via `ik env-load` \u2014 never commit secrets.\n- Every workspace package has its own CLAUDE.md with package-scoped rules \u2014 read it before editing that package.\n", "import designSkeleton from '../../../resources/design/skeleton.md?raw'\n// This import list must stay flat, static and explicit. Esbuild bundles the CLI\n// into a single file and only inlines specifiers it can see statically \u2014 a dynamic\n// `import()`, a glob or a computed path resolves to nothing it can follow, the\n// specifier survives into `dist`, and node throws `ERR_UNKNOWN_FILE_EXTENSION` on a\n// consumer's machine. Every test in this package runs from `src/`, where such a\n// refactor still works, so nothing but the bundle guard would catch it.\nimport backend from '../../../resources/package/backend.md?raw'\nimport e2e from '../../../resources/package/e2e.md?raw'\nimport frontend from '../../../resources/package/frontend.md?raw'\nimport lib from '../../../resources/package/lib.md?raw'\nimport mobile from '../../../resources/package/mobile.md?raw'\nimport rootBody from '../../../resources/root/body.md?raw'\n\n/** Path of a resource under `resources/`, without the `.md` extension. */\nexport type ResourceKey =\n | 'root/body'\n | 'package/frontend'\n | 'package/backend'\n | 'package/lib'\n | 'package/e2e'\n | 'package/mobile'\n | 'design/skeleton'\n\n/**\n * The guidance prose, inlined at build time. Trailing whitespace is stripped from\n * every entry, so a body never ends with a newline.\n *\n * The import list above must stay flat, static and explicit \u2014 see the note there.\n */\n// Why `trimEnd()`: prettier gives each `.md` a trailing newline, but\n// `buildManagedBlock` writes `${start}\\n${body}\\n${end}`, so an un-normalized body\n// would add a blank line before the end marker of every managed block in every\n// consumer file. `buildDesignSkeleton` re-adds the single trailing newline its own\n// contract has always had.\nexport const RESOURCES: Readonly<Record<ResourceKey, string>> = {\n 'root/body': rootBody.trimEnd(),\n 'package/frontend': frontend.trimEnd(),\n 'package/backend': backend.trimEnd(),\n 'package/lib': lib.trimEnd(),\n 'package/e2e': e2e.trimEnd(),\n 'package/mobile': mobile.trimEnd(),\n 'design/skeleton': designSkeleton.trimEnd(),\n}\n\n/**\n * The placeholders each resource is expected to carry, asserted still present\n * after formatting. Cheap and specific, and measurably insufficient on its own:\n * prettier's characteristic damage leaves every token in place and moves a line\n * beside it, which a presence check cannot see. The exact rendered line counts\n * in `bodies.test.ts` are the primary net for that.\n */\nexport const PLACEHOLDERS: Readonly<Record<ResourceKey, readonly string[]>> = {\n 'root/body': [],\n 'package/frontend': ['{{packageName}}', '{{relDir}}', '{{type}}', '{{readmeBullet}}'],\n 'package/backend': ['{{packageName}}', '{{relDir}}', '{{type}}', '{{readmeBullet}}'],\n 'package/lib': ['{{packageName}}', '{{relDir}}', '{{type}}', '{{readmeBullet}}'],\n 'package/e2e': ['{{packageName}}', '{{relDir}}', '{{type}}', '{{readmeBullet}}'],\n 'package/mobile': ['{{packageName}}', '{{relDir}}', '{{type}}', '{{readmeBullet}}'],\n 'design/skeleton': ['{{packageName}}'],\n}\n", "/**\n * Variables substituted into a guidance resource. Every value is a string; an\n * empty one is meaningful (it is what removes a conditional bullet).\n */\ntype TemplateVars = Readonly<Record<string, string>>\n\n/** Matches `{{name}}` anywhere in the template. */\nconst PLACEHOLDER = /\\{\\{(\\w+)\\}\\}/g\n\n/** Matches a list item whose entire content is one placeholder: `- {{readmeBullet}}`. */\nconst LONE_LIST_ITEM = /^- \\{\\{(\\w+)\\}\\}$/\n\nconst collectPlaceholders = (template: string): Set<string> => {\n return new Set(\n [...template.matchAll(PLACEHOLDER)].map((match) => {\n return match[1] as string\n }),\n )\n}\n\n/**\n * Reject a supplied variable the template never mentions. Without this a renamed\n * placeholder would silently drop its value rather than failing \u2014 the caller\n * keeps passing `readmeBullet`, the file now says `{{readmeText}}`, and the\n * bullet renders as its own literal placeholder.\n */\nconst assertNoUnusedVars = (used: Set<string>, vars: TemplateVars): void => {\n const unused = Object.keys(vars).filter((name) => {\n return !used.has(name)\n })\n\n if (unused.length > 0) {\n throw new Error(`renderTemplate: variable(s) never used by the template: ${unused.join(', ')}`)\n }\n}\n\nconst assertNoUnknownPlaceholders = (used: Set<string>, vars: TemplateVars): void => {\n const unknown = [...used].filter((name) => {\n return !(name in vars)\n })\n\n if (unknown.length > 0) {\n throw new Error(\n `renderTemplate: placeholder(s) with no variable: ${unknown\n .map((n) => {\n return `{{${n}}}`\n })\n .join(', ')}`,\n )\n }\n}\n\n/**\n * Drop a `- {{x}}` line whose value is empty, newline included.\n *\n * Evaluated on the **template**, never on the rendered result. On the result the\n * rule would also match the intentional blank-line-separated bullets under\n * `## Checks` once a value happened to render empty, and would eat structural\n * blank lines that carry no placeholder at all.\n *\n * The rule is deliberately marker-aware (`- ` required) rather than a general\n * \"lone placeholder line\" primitive. The general form \u2014 a bare `{{x}}` line \u2014 is\n * a markdown *paragraph* sitting next to a *list*, and prettier separates block\n * elements with a blank line, so it rewrites the resource on every format run.\n */\nconst dropEmptyListItems = (lines: readonly string[], vars: TemplateVars): string[] => {\n return lines.filter((line) => {\n const match = LONE_LIST_ITEM.exec(line)\n\n return match === null || vars[match[1] as string] !== ''\n })\n}\n\n/**\n * A multi-line value is only safe where the placeholder starts the line: anywhere\n * else its second and subsequent lines lose the prefix they were indented under,\n * which markdown reads as a different block. Fail rather than mis-indent.\n */\nconst assertMultilinePlaceholdersAtColumnZero = (lines: readonly string[], vars: TemplateVars): void => {\n for (const line of lines) {\n for (const match of line.matchAll(PLACEHOLDER)) {\n const value = vars[match[1] as string]\n\n if (value !== undefined && value.includes('\\n') && match.index !== 0) {\n throw new Error(\n `renderTemplate: multi-line value for {{${match[1]}}} must start its line (found at column ${match.index})`,\n )\n }\n }\n }\n}\n\n/**\n * Substitute `{{name}}` placeholders in a guidance resource.\n *\n * Substitution is **single-pass**: the whole template is scanned once and each\n * match replaced from `vars`, so a `{{\u2026}}` sequence appearing inside an injected\n * value is left alone. That matters because `packageName` comes from a\n * `package.json` on disk \u2014 successive `replaceAll` calls would re-substitute it.\n *\n * Throws in both directions (unknown placeholder, unused variable) so a resource\n * and its call site cannot drift apart silently.\n *\n * @example\n * renderTemplate('# {{packageName}}\\n- {{readmeBullet}}', { packageName: '@x/y', readmeBullet: '' })\n * // => '# @x/y'\n */\nexport const renderTemplate = (template: string, vars: TemplateVars): string => {\n const used = collectPlaceholders(template)\n\n assertNoUnknownPlaceholders(used, vars)\n assertNoUnusedVars(used, vars)\n\n const lines = dropEmptyListItems(template.split('\\n'), vars)\n\n assertMultilinePlaceholdersAtColumnZero(lines, vars)\n\n return lines.join('\\n').replace(PLACEHOLDER, (_match, name: string) => {\n return vars[name] as string\n })\n}\n", "import { RESOURCES } from '../resources'\nimport { renderTemplate } from '../template'\n\n/**\n * The static front-matter line `buildDesignSkeleton` rewrites. Front matter cannot\n * carry a `{{\u2026}}` placeholder \u2014 prettier reformats `name: {{packageName}}` into\n * `name: { { packageName } }`, which is still valid YAML with the expected key, so\n * every structural guard passes on the broken output. A literal replaced by exact\n * match avoids the whole class. Measured unique in the file: `fontFamily: TODO`\n * and `description: TODO` do not collide with it.\n */\nconst FRONT_MATTER_NAME = 'name: TODO'\n\n/**\n * Skeleton for a package's `DESIGN.md`, in the shape of the Google Labs\n * `design.md` spec: YAML front matter (`name`, `description`, `colors`,\n * `typography`, `rounded`, `spacing`, `components`) followed by the prose\n * sections in spec order. The text lives in `resources/design/skeleton.md`.\n *\n * Every value is a placeholder a human replaces \u2014 infra-kit cannot author brand\n * content, so the skeleton exists to give that content a home and a shape, not to\n * be correct on its own. `audit --fix --design` writes it only for `frontend` /\n * `mobile` packages and never overwrites an existing file.\n *\n * @example\n * buildDesignSkeleton('@hulyo/client-ui')\n * // => '---\\nname: @hulyo/client-ui\\n\u2026'\n */\nexport const buildDesignSkeleton = (packageName: string): string => {\n const rendered = renderTemplate(RESOURCES['design/skeleton'], { packageName })\n\n const nameLine = `name: ${packageName}`\n // Function replacement, not a string: a string second argument treats `$&`, `` $` ``\n // and `$'` in the value as replacement patterns, and `packageName` falls back to the\n // directory basename, which is not constrained to npm name syntax.\n const named = rendered.replace(FRONT_MATTER_NAME, () => {\n return nameLine\n })\n\n // Re-add the single trailing newline this builder has always emitted: `RESOURCES`\n // trims every entry, and unlike the two block bodies this one is written to a file\n // of its own rather than wrapped in markers.\n return `${named}\\n`\n}\n", "import { PACKAGE_VERSION_PREFIX } from '../markers'\nimport type { PackageType } from '../package-type'\nimport { RESOURCES } from '../resources'\nimport { renderTemplate } from '../template'\n\nexport interface BuildPackageBodyArgs {\n /** CLI version that generated the block; recorded in the version line. */\n version: string\n /** Resolved package type; selects the resource file and rides in the version line. */\n type: PackageType\n /** The package's `name` from `package.json`, rendered as the heading. */\n packageName: string\n /** Package directory relative to the repo root, posix-separated (e.g. `apps/client/ui`). */\n relDir: string\n /** Whether the package has a `README.md`. The bullet naming it is omitted when it does not. */\n hasReadme: boolean\n /**\n * Whether the package already has a `DESIGN.md`. Accepted for future use and\n * deliberately not rendered: the bullet appears for every `frontend` / `mobile`\n * package regardless, because it tells the agent what to do when the file is\n * absent (ask, do not invent one). Callers pass it so the body can react to it\n * later without a signature change.\n */\n hasDesign: boolean\n}\n\n/** The version line, e.g. `<!-- infra-kit:package:version 0.4.0 frontend -->`. */\nconst buildVersionLine = (version: string, type: PackageType): string => {\n return `${PACKAGE_VERSION_PREFIX}${version} ${type} -->`\n}\n\n/**\n * Text of the README bullet, without its `- ` marker \u2014 the marker lives in the\n * resource file so prettier sees a list item rather than a bare paragraph\n * placeholder. An empty string removes the whole line (see `renderTemplate`).\n */\nconst README_BULLET = '`README.md` \u2014 what this package is and how to run it.'\n\n/**\n * Render a package's guidance body \u2014 the text that goes *between* the package\n * markers, version line first. The body never contains a marker itself; the\n * caller wraps it via `upsertManagedBlock`.\n *\n * The prose lives in `resources/package/<type>.md`, one complete body per type.\n * Edit that file to change the text; nothing here needs to change with it.\n *\n * @example\n * buildPackageBody({ version: '0.4.0', type: 'frontend', packageName: '@hulyo/client-ui',\n * relDir: 'apps/client/ui', hasReadme: true, hasDesign: false })\n * // => '<!-- infra-kit:package:version 0.4.0 frontend -->\\n\\n# @hulyo/client-ui\\n\u2026'\n */\nexport const buildPackageBody = (args: BuildPackageBodyArgs): string => {\n const { version, type, packageName, relDir, hasReadme } = args\n\n const body = renderTemplate(RESOURCES[`package/${type}`], {\n packageName,\n relDir,\n type,\n readmeBullet: hasReadme ? README_BULLET : '',\n })\n\n return [buildVersionLine(version, type), '', body].join('\\n')\n}\n", "import { ROOT_VERSION_PREFIX } from '../markers'\nimport { RESOURCES } from '../resources'\nimport { renderTemplate } from '../template'\n\n/** The version comment line, e.g. `<!-- infra-kit:version 0.1.105 -->`. */\nconst buildVersionLine = (version: string): string => {\n return `${ROOT_VERSION_PREFIX}${version} -->`\n}\n\n/**\n * The repo-root guidance body (without the surrounding markers). Describes the\n * infra-kit CLI surface so any AI agent working in the repo learns it. The prose\n * lives in `resources/root/body.md`; the version line records which CLI version\n * generated it.\n *\n * @example\n * buildRootBody('0.4.0')\n * // => '<!-- infra-kit:version 0.4.0 -->\\n\\n# infra-kit\\n\u2026'\n */\nexport const buildRootBody = (version: string): string => {\n // Rendered with no variables on purpose: the root body has no placeholders, and\n // `renderTemplate` throws on an unknown one \u2014 so adding a `{{\u2026}}` to the resource\n // without wiring a value here fails loudly instead of shipping the literal token.\n const body = renderTemplate(RESOURCES['root/body'], {})\n\n return [buildVersionLine(version), '', body].join('\\n')\n}\n", "import fs from 'node:fs'\nimport path from 'node:path'\n\nimport { hasManagedBlock } from 'src/lib/managed-block'\n\n/**\n * What a managed write did to one file.\n *\n * `failed` carries the continue-and-report policy (\u00A73.6): a per-file error is recorded against\n * that path and the run proceeds, because aborting halfway can leave exactly one well-formed\n * block behind \u2014 which adopts the workspace and reddens every package the run never reached.\n */\nexport type WriteAction = 'created' | 'updated' | 'unchanged' | 'removed' | 'failed'\n\n/**\n * Refuse to write through a symlink. Gates on `lstatSync` inside a try/catch rather than\n * `existsSync`: `existsSync` *follows* links, so a **dangling** symlink reports `false`, the\n * symlink branch is never reached, and the write goes through the link \u2014 creating a file\n * outside the repo. Any stat failure (ENOENT for a plain absent file) is fine and returns.\n *\n * @example\n * assertNotSymlink('/repo/CLAUDE.md')\n * // returns silently when the path is absent or a regular file; throws when it is a symlink\n */\nexport const assertNotSymlink = (filePath: string): void => {\n let stats: fs.Stats\n\n try {\n stats = fs.lstatSync(filePath)\n } catch {\n return\n }\n\n if (stats.isSymbolicLink()) {\n throw new Error(`Refusing to write ${filePath} because the destination is a symlink`)\n }\n}\n\n/**\n * Write `next` to `filePath` with the OMC-style safety rails: refuse symlinks, and skip the\n * write entirely when the bytes are identical (so re-runs are churn-free). The prior file is\n * overwritten in place \u2014 guidance files are managed content under version control, so no\n * sibling copy of them is kept.\n *\n * @example\n * writeManaged('/repo/CLAUDE.md', body)\n * // => 'created' | 'updated' | 'unchanged'\n */\nexport const writeManaged = (filePath: string, next: string): WriteAction => {\n assertNotSymlink(filePath)\n\n const previous = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf-8') : null\n\n if (previous === next) return 'unchanged'\n\n fs.mkdirSync(path.dirname(filePath), { recursive: true })\n fs.writeFileSync(filePath, next, 'utf-8')\n\n return previous === null ? 'created' : 'updated'\n}\n\n/**\n * Post-write assertion: the managed block really is in the file we just wrote.\n *\n * @example\n * assertBlockPresent('/repo/CLAUDE.md', ROOT_MARKER_START, ROOT_MARKER_END)\n * // throws when the block is missing from the file on disk\n */\nexport const assertBlockPresent = (filePath: string, start: string, end: string): void => {\n const content = fs.readFileSync(filePath, 'utf-8')\n\n if (!hasManagedBlock(content, start, end)) {\n throw new Error(`Post-write validation failed: managed block missing from ${filePath}`)\n }\n}\n\n/** The text before the start marker and after the end marker, or `null` when there is no block. */\nconst outsideMarkers = (content: string, start: string, end: string): { before: string; after: string } | null => {\n if (!hasManagedBlock(content, start, end)) return null\n\n return {\n before: content.slice(0, content.indexOf(start)),\n after: content.slice(content.indexOf(end) + end.length),\n }\n}\n\n/**\n * Post-write assertion for the **replace-in-place** path only: every byte outside the markers\n * survived the update untouched.\n *\n * First insertion is deliberately exempt and must not be checked here. `upsertManagedBlock`'s\n * non-replace path collapses a trailing run of blank lines and appends a newline, so a file\n * that ends in blank lines legitimately changes outside the markers the first time a block\n * lands in it. After that, byte-identity is true and worth pinning.\n *\n * @example\n * assertOutsideMarkersUnchanged(before, after, ROOT_MARKER_START, ROOT_MARKER_END)\n * // throws when hand-authored text around the block was altered\n */\nexport const assertOutsideMarkersUnchanged = (before: string, after: string, start: string, end: string): void => {\n const previous = outsideMarkers(before, start, end)\n const next = outsideMarkers(after, start, end)\n\n if (previous === null || next === null) return\n\n if (previous.before !== next.before || previous.after !== next.after) {\n throw new Error('Post-write validation failed: content outside the managed markers changed')\n }\n}\n", "import path from 'node:path'\n\nimport type { AdoptionState } from 'src/lib/agent-guidance/adoption'\nimport { DEFAULT_RULES } from 'src/lib/package-config'\nimport type { ResolvedPackageRules } from 'src/lib/package-config'\n\nimport { checkAgentGuidance, checkConfig, checkFiles, checkScripts, checkTurbo } from './checks'\nimport { readPackageJson } from './loader'\nimport type { PackageCheck, PackageValidationResult } from './types'\n\n// Re-exported on the historical import path so consumers and tests that reach\n// for the loader through `package-validator` keep resolving after the split.\nexport { discoverPackages, loadPackageConfig } from './loader'\nexport type { PackageCheck, PackageValidationResult } from './types'\n\n/** Caller-supplied context for one {@link validatePackage} run. */\nexport interface ValidatePackageOptions {\n /**\n * Whether the enclosing workspace has adopted per-package guidance blocks, and the\n * evidence for it. Absent means \"not adopted\" \u2014 the eight existing call sites pass no\n * options, and an un-probed workspace must never enforce.\n */\n adoption?: AdoptionState\n /**\n * Set for the monorepo root. The root `CLAUDE.md` carries the *root* marker pair, so the\n * package-scoped `agent-guidance` check would resolve it to `foreign-block` and fail every\n * consumer's `infra-kit-check-root`. The check is skipped entirely instead.\n */\n isRoot?: boolean\n}\n\n/** What an absent `adoption` option means: never enforce on a workspace nobody probed. */\nconst UNADOPTED: AdoptionState = { adopted: false, workspaceRoot: null }\n\n/**\n * Validate a single directory against its `infra-kit.config.ts` rules: the config\n * must be present and valid, every required script must be declared, every\n * required file must exist, and (root only) every required turbo task must be\n * defined. When the config fails to load, the rule-based checks are skipped (the\n * rules are unknown) but the `agent-guidance` check still reports, since it reads\n * `CLAUDE.md` rather than the rules. `baseline` selects which under-the-hood\n * defaults apply \u2014 package defaults or {@link ROOT_DEFAULT_RULES}.\n *\n * @example\n * const result = await validatePackage('/repo/packages/serverless-config')\n * // result.passed reflects the package's conformance; result.checks lists each check\n * @example\n * // From `audit`, with the workspace adoption state threaded in:\n * await validatePackage(dir, DEFAULT_RULES, { adoption, isRoot: false })\n */\nexport const validatePackage = async (\n packageDir: string,\n baseline: Readonly<ResolvedPackageRules> = DEFAULT_RULES,\n options: ValidatePackageOptions = {},\n): Promise<PackageValidationResult> => {\n const pkgJson = await readPackageJson(packageDir)\n const packageName = pkgJson.name ?? path.basename(packageDir)\n\n const { check: configCheck, rules } = await checkConfig(packageDir, baseline)\n const checks: PackageCheck[] = [configCheck]\n\n // Outside the `if (rules)` branch on purpose: a package whose config fails to load still\n // gets its guidance reported. Deliberately not a `requiredFiles` entry either \u2014 both\n // consumers override `requiredFiles` wholesale, which would make the rule silently inert.\n if (!options.isRoot) {\n checks.push(checkAgentGuidance(packageDir, options.adoption ?? UNADOPTED))\n }\n\n if (rules) {\n checks.push(...checkScripts(pkgJson.scripts ?? {}, rules.requiredScripts))\n checks.push(...(await checkFiles(packageDir, rules.requiredFiles)))\n checks.push(...(await checkTurbo(packageDir, rules.turboTasks)))\n }\n\n const passed = checks.every((check) => {\n return check.status === 'pass'\n })\n\n return { packageDir, packageName, checks, passed }\n}\n", "import path from 'node:path'\n\n// Deep `agent-guidance` paths, never the `src/lib/agent-guidance` barrel: that barrel\n// re-exports `adoption.ts`, which imports the package-validator loader, and this module is\n// itself reached from the package-validator barrel. `inspect.ts`, `markers.ts` and\n// `read-guidance-file.ts` import nothing from package-validator, so these runtime edges\n// cannot close a cycle. The `AdoptionState` reference is `import type`, so it adds no edge at all.\nimport type { AdoptionState } from 'src/lib/agent-guidance/adoption'\nimport { inspectPackageGuidance } from 'src/lib/agent-guidance/inspect'\nimport type { GuidanceState } from 'src/lib/agent-guidance/inspect'\nimport { PACKAGE_MARKER_END, PACKAGE_MARKER_START, ROOT_MARKER_START } from 'src/lib/agent-guidance/markers'\nimport { readGuidanceFile } from 'src/lib/agent-guidance/read-guidance-file'\n\nimport type { PackageCheck } from '../types'\n\n/** Stable check name, identical in both adoption states so one grep finds every report. */\nconst CHECK_NAME = 'agent-guidance'\n\n/** The per-package guidance file this check validates. */\nconst GUIDANCE_FILE = 'CLAUDE.md'\n\n/** Tail of every non-`ok` message \u2014 one command repairs all four broken states. */\nconst FIX_HINT = '\u2014 run: infra-kit audit --fix'\n\n/**\n * The state-naming half of each message. Package-relative by design: this check gets an\n * absolute `packageDir` and has no repo-relative frame of its own, and the audit printer\n * already supplies the identity as `[FAIL] <packageName> <check>: <message>`.\n */\nconst STATE_MESSAGES: Record<Exclude<GuidanceState, 'ok'>, string> = {\n missing: `${GUIDANCE_FILE} missing`,\n 'no-block': `${GUIDANCE_FILE} has no infra-kit block (expected ${PACKAGE_MARKER_START} \u2026 ${PACKAGE_MARKER_END})`,\n malformed: `${GUIDANCE_FILE} block is malformed (end marker precedes start, or body is empty)`,\n 'foreign-block': `${GUIDANCE_FILE} carries the ROOT infra-kit block (${ROOT_MARKER_START}); a package needs the package block`,\n}\n\n/**\n * The adopting evidence, repo-relative, or `''` when the workspace has not adopted. This is\n * the one path in the message that must be repo-relative, because it names a *different*\n * package than the one being reported \u2014 hence `AdoptionState` carrying `workspaceRoot`.\n */\nconst evidenceSuffix = (adoption: AdoptionState): string => {\n if (!adoption.adopted) return ''\n\n return ` (workspace adopted: ${path.relative(adoption.workspaceRoot, adoption.evidencePath)} carries a package block)`\n}\n\n/**\n * Validate one package's `CLAUDE.md` guidance block.\n *\n * The structural predicate is the same in every workspace; what adoption changes is whether a\n * broken structure is allowed to fail. Before adoption \u2014 no package anywhere carries a\n * well-formed block \u2014 every non-`ok` state reports `pass` with the message prefixed\n * `not yet adopted \u2014 `, so a CLI upgrade alone can never redden a consumer's CI. After\n * adoption the same four states `fail`, and the `missing` message names the package whose\n * block switched enforcement on.\n *\n * @example\n * checkAgentGuidance('/repo/packages/lib-a', { adopted: false, workspaceRoot: '/repo' })\n * // => status 'pass', 'not yet adopted \u2014 CLAUDE.md missing \u2014 run: infra-kit audit --fix'\n * @example\n * checkAgentGuidance('/repo/packages/lib-b',\n * { adopted: true, workspaceRoot: '/repo', evidencePath: '/repo/packages/lib-a/CLAUDE.md' })\n * // => status 'fail', 'CLAUDE.md missing \u2014 run: infra-kit audit --fix\n * // (workspace adopted: packages/lib-a/CLAUDE.md carries a package block)'\n */\nexport const checkAgentGuidance = (packageDir: string, adoption: AdoptionState): PackageCheck => {\n const inspection = inspectPackageGuidance(readGuidanceFile(path.join(packageDir, GUIDANCE_FILE)))\n\n if (inspection.state === 'ok') {\n // Both fields come off the block on disk, not off the running CLI: a block written by a\n // newer release can name a version and a type this CLI does not know.\n const version = inspection.version ?? 'unknown'\n const type = inspection.type ?? 'unknown'\n\n return { name: CHECK_NAME, status: 'pass', message: `present (block from infra-kit ${version}, type ${type})` }\n }\n\n const detail = `${STATE_MESSAGES[inspection.state]} ${FIX_HINT}`\n const message = inspection.state === 'missing' ? `${detail}${evidenceSuffix(adoption)}` : detail\n\n if (adoption.adopted) {\n return { name: CHECK_NAME, status: 'fail', message }\n }\n\n return { name: CHECK_NAME, status: 'pass', message: `not yet adopted \u2014 ${message}` }\n}\n", "import { DEFAULT_RULES } from 'src/lib/package-config'\nimport type { ResolvedPackageRules } from 'src/lib/package-config'\n\nimport { PACKAGE_CONFIG_FILE, loadPackageConfig } from '../loader'\nimport type { PackageCheck } from '../types'\n\n/**\n * Build the \"config present and valid\" check, returning the resolved rules when\n * the load succeeds so the caller can run the rule-based checks against them.\n * When the config fails to load the rules are `null` and the caller skips the\n * rule-based checks (the expectations are unknown).\n */\nexport const checkConfig = async (\n packageDir: string,\n baseline: Readonly<ResolvedPackageRules> = DEFAULT_RULES,\n): Promise<{ check: PackageCheck; rules: ResolvedPackageRules | null }> => {\n try {\n const rules = await loadPackageConfig(packageDir, baseline)\n\n return {\n check: { name: PACKAGE_CONFIG_FILE, status: 'pass', message: 'present and valid' },\n rules,\n }\n } catch (err) {\n return {\n check: { name: PACKAGE_CONFIG_FILE, status: 'fail', message: (err as Error).message },\n rules: null,\n }\n }\n}\n", "import fs from 'node:fs/promises'\nimport path from 'node:path'\n\nimport type { PackageCheck } from '../types'\n\n/**\n * Check that every required file exists relative to the package root AND is a\n * regular file. A directory that happens to share the required name fails \u2014 a\n * required `readme.md` must be the file, not a folder.\n */\nexport const checkFiles = async (packageDir: string, requiredFiles: string[]): Promise<PackageCheck[]> => {\n return Promise.all(\n requiredFiles.map(async (file) => {\n const stat = await fs.stat(path.join(packageDir, file)).catch(() => {\n return null\n })\n\n if (!stat) {\n return { name: `file:${file}`, status: 'fail' as const, message: `missing file: ${file}` }\n }\n\n if (!stat.isFile()) {\n return { name: `file:${file}`, status: 'fail' as const, message: `not a file: ${file} (found a directory)` }\n }\n\n return { name: `file:${file}`, status: 'pass' as const, message: 'exists' }\n }),\n )\n}\n", "import type { PackageCheck } from '../types'\n\n/**\n * Check that every required script is present in the package.json `scripts` map\n * and carries a runnable command. A key declared with an empty or whitespace-only\n * value fails as well \u2014 an empty script silently no-ops in CI, so presence alone\n * is not enough.\n */\nexport const checkScripts = (scripts: Record<string, string>, requiredScripts: string[]): PackageCheck[] => {\n return requiredScripts.map((script) => {\n const value = scripts[script]\n\n if (typeof value !== 'string') {\n return { name: `script:${script}`, status: 'fail', message: `missing \"${script}\" in package.json scripts` }\n }\n\n if (value.trim().length === 0) {\n return { name: `script:${script}`, status: 'fail', message: `\"${script}\" is empty in package.json scripts` }\n }\n\n return { name: `script:${script}`, status: 'pass', message: 'defined' }\n })\n}\n", "import fs from 'node:fs/promises'\nimport path from 'node:path'\n\nimport type { PackageCheck } from '../types'\n\nconst TURBO_FILE = 'turbo.json'\n\n/**\n * Check that every required turbo task is defined in turbo.json `tasks`. A root\n * task may be keyed as either `name` or `//#name`, so both forms count as present.\n * Runs only when the resolved rules ask for turbo tasks (the monorepo root).\n *\n * Reports a single clear diagnostic when turbo.json is unreadable or carries no\n * `tasks` object, rather than emitting one identical \"missing task\" line per\n * required task.\n */\nexport const checkTurbo = async (packageDir: string, requiredTasks: string[]): Promise<PackageCheck[]> => {\n if (requiredTasks.length === 0) {\n return []\n }\n\n let parsed: { tasks?: Record<string, unknown> }\n\n try {\n const raw = await fs.readFile(path.join(packageDir, TURBO_FILE), 'utf-8')\n\n parsed = JSON.parse(raw) as { tasks?: Record<string, unknown> }\n } catch (err) {\n return [{ name: TURBO_FILE, status: 'fail', message: `cannot read/parse ${TURBO_FILE}: ${(err as Error).message}` }]\n }\n\n const tasks = parsed.tasks\n\n if (tasks === null || typeof tasks !== 'object') {\n return [{ name: TURBO_FILE, status: 'fail', message: `no \"tasks\" object defined in ${TURBO_FILE}` }]\n }\n\n return requiredTasks.map((task) => {\n const defined = task in tasks || `//#${task}` in tasks\n\n return {\n name: `turbo:${task}`,\n status: defined ? 'pass' : 'fail',\n message: defined ? 'defined' : `missing turbo task \"${task}\" in ${TURBO_FILE}`,\n }\n })\n}\n", "import type { z } from 'zod'\n\nexport interface ToolsExecutionResult<TStructured = Record<string, unknown>> {\n [x: string]: unknown\n content: {\n type: 'text'\n text: string\n }[]\n structuredContent?: TStructured\n}\n\nexport interface RequiredConfirmedOptionArg {\n confirmedCommand: boolean\n}\n\n/**\n * The per-tool seam that lets the domain-blind confirm chokepoint in `lib/tool-handler` collect a\n * tool's arguments from a HUMAN via an MCP elicitation form, without learning anything about the\n * tool's domain. The chokepoint asks only \"is there a provider?\" and \"can this client render a\n * form?\"; every domain-specific answer lives behind these three members.\n *\n * Nothing consumes this yet \u2014 it is declared here so the registration path can forward it while the\n * state machine that reads it lands separately.\n */\nexport interface ArgumentFormProvider {\n /** Human-facing prompt rendered above the form. */\n message: string\n /**\n * Whether a form is WORTH opening for THESE arguments. Pure, synchronous, never\n * throws; `false` sends the call straight to the gate with its arguments intact.\n * Defaults to `false` on anything it does not recognise.\n *\n * NOT load-bearing for TRUNCATION: the chokepoint's non-narrowing check\n * catches top-level key removal and any array length change \u2014 shortening OR\n * growth \u2014 which no provider can weaken. The check is load-bearing for NOTHING\n * ELSE: it does not look at nested keys or at values, so it does NOT cover a\n * form that drops `releases[0].type`. Do not read this as \"the chokepoint has\n * it covered\": for anything below the top level, `toArgs`'s merge is the only\n * control.\n */\n isFormable: (params: unknown) => boolean\n /**\n * Candidate values for THIS tool's arguments, as a zod object (a Standard\n * Schema \u2014 `inputRequired.elicit` accepts one). Returned rather than inlined so\n * the SAME schema validates the response on re-entry.\n *\n * Contract: never throws, never blocks past the caller's deadline, and returns\n * `null` when it cannot offer real values \u2014 which the handler reads as \"no\n * form\" and falls to the gate.\n *\n * Also: **validates SYNCHRONOUSLY \u2014 no `.refine(async \u2026)` anywhere in the\n * object.** `acceptedContent`'s schema-aware overload throws a `TypeError` on an\n * asynchronously-validating schema, and the declared return type\n * `z.ZodObject<z.ZodRawShape>` cannot express that constraint \u2014 so it is stated\n * here and backstopped by a wrap at the call site.\n */\n buildRequestedSchema: (params: unknown) => Promise<z.ZodObject<z.ZodRawShape> | null>\n /**\n * MERGES validated form content over the round-1 `params` \u2014 never constructs a\n * fresh argument object. The form overwrites field-by-field and only where the\n * human supplied a value; untouched fields keep their round-1 value.\n *\n * Contract: TOTAL over anything `buildRequestedSchema`'s own schema accepts,\n * and NEVER THROWS \u2014 it is the one member called on client-supplied input, and\n * a throw here escapes the chokepoint's catch as a tool error where a gate was\n * owed. It returns `null` if it cannot map, which the handler treats exactly as\n * a validation failure: gate, with the round-1 arguments.\n */\n toArgs: (content: Record<string, unknown>, params: unknown) => Record<string, unknown> | null\n}\n\nexport interface McpTool<TIn extends z.ZodRawShape = z.ZodRawShape, TOut extends z.ZodRawShape = z.ZodRawShape> {\n name: string\n description: string\n inputSchema: TIn\n outputSchema: TOut\n /**\n * When true, this tool is gated by the MCP destructive-op confirm gate (see `lib/tool-handler`):\n * a first call WITHOUT `confirm:true` returns a resolved-args gate response instead of running,\n * and only a second call carrying `confirm:true` executes. This is ORTHOGONAL to the\n * `confirmedCommand:true` the boundary always injects (a prompt-skip discriminator, not a gate).\n * Set only on genuinely destructive tools; the default-deny catalog test enforces coverage.\n */\n requiresHumanConfirm?: boolean\n /**\n * Optional per-tool {@link ArgumentFormProvider}. Present means \"this tool can offer the human a\n * form for its arguments\"; absent means the confirm gate behaves exactly as it always has. It is\n * ORTHOGONAL to `requiresHumanConfirm` as a declaration, though only a gated tool can reach a form\n * \u2014 the form is collected on the way INTO the gate, never instead of it.\n */\n formProvider?: ArgumentFormProvider\n /**\n * Host-facing `_meta` for this tool's `tools/list` entry.\n *\n * The one key used today is `anthropic/requiresUserInteraction: true`, which makes Claude Code prompt\n * a human on EVERY call \u2014 including in `acceptEdits`, `auto` and `bypassPermissions`, and with allow\n * rules unable to skip it. It is a Claude Code extension rather than MCP spec, so any other host, and\n * any Claude Code below v2.1.199, ignores it silently: it is a gate where honoured and a no-op\n * elsewhere, never a substitute for a refusal computed inside the CLI.\n */\n meta?: Record<string, unknown>\n handler: (\n params: z.infer<z.ZodObject<TIn>> & RequiredConfirmedOptionArg,\n ) => Promise<ToolsExecutionResult<z.infer<z.ZodObject<TOut>>>>\n}\n\n/**\n * Build the dual-channel content array shared by every MCP tool. Narrows the\n * literal `type: 'text'` so handlers can use inferred return types without TS\n * widening `type` to `string` \u2014 which would otherwise break assignability\n * against the MCP SDK's content union.\n *\n * @example\n * return {\n * content: textContent(JSON.stringify(structuredContent, null, 2)),\n * structuredContent,\n * }\n */\nexport const textContent = (text: string): ToolsExecutionResult['content'] => {\n return [{ type: 'text', text }]\n}\n\n/**\n * Factory that ties the handler's return type to the declared `outputSchema`\n * so `structuredContent` is checked against the schema at compile time. If a\n * handler accidentally drops or renames a field, TS errors at the registration\n * site rather than at runtime in an MCP client.\n *\n * @example\n * export const envLoadMcpTool = defineMcpTool({\n * name: 'env-load',\n * description: '...',\n * inputSchema: { config: z.string() },\n * outputSchema: { filePath: z.string(), variableCount: z.number() },\n * handler: envLoad,\n * })\n */\nexport const defineMcpTool = <TIn extends z.ZodRawShape, TOut extends z.ZodRawShape>(\n tool: McpTool<TIn, TOut>,\n): McpTool<TIn, TOut> => {\n return tool\n}\n", "import path from 'node:path'\nimport process from 'node:process'\n\nimport { resetAdoptionCache, resolveAdoption, syncPackageGuidance, syncRootGuidance } from 'src/lib/agent-guidance'\nimport type { GuidanceWrite, PackageType, WriteAction } from 'src/lib/agent-guidance'\nimport { getProjectRoot } from 'src/lib/git-utils'\nimport { logger } from 'src/lib/logger'\n// Deep loader path, NOT the `src/lib/package-validator` barrel: that barrel reaches\n// `checks/index` \u2192 `agent-guidance-check` \u2192 `lib/agent-guidance`, and this module already\n// imports that subtree. The loader imports nothing from `agent-guidance`, so this edge is safe.\nimport { discoverPackages, findPackageRoot, readDeclaredPackageType } from 'src/lib/package-validator/loader'\n\nimport packageJson from '../../../package.json' with { type: 'json' }\n\n/**\n * One file a `--fix` run touched, as it appears in `audit()`'s\n * `structuredContent.fixed`. Deliberately narrower than {@link GuidanceWrite}:\n * the failure `message` is logged rather than published, so the shape stays the\n * `{ path, action, type }` triple the plan documents.\n */\nexport interface FixedEntry {\n /** Absolute path of the file. */\n path: string\n action: WriteAction\n /** Resolved package type; absent for the repo-root files. */\n type?: PackageType\n}\n\n/** The CLI-only options that reach the `--fix` branch. Mirrors `audit`'s own scope flags. */\nexport interface AuditFixOptions {\n /** Fix every discovered workspace package \u2014 packages only, never the root. */\n all?: boolean\n /** Fix the repo-root block only. */\n root?: boolean\n /** Scaffold `DESIGN.md` for `frontend`/`mobile` packages that lack one. */\n design?: boolean\n /** Directory the cwd-scoped package is resolved from. Defaults to `process.cwd()`. */\n cwd?: string\n}\n\nexport interface AuditFixResult {\n fixed: FixedEntry[]\n /**\n * Whether this run switched the workspace from unadopted to adopted. Only ever true for a\n * single-package fix: an `--all` run leaves no package behind, so nothing starts failing.\n */\n adoptionFlipped: boolean\n}\n\n/** Action counts printed unconditionally, so a fully idempotent run still reports its scope. */\nconst ALWAYS_COUNTED: readonly WriteAction[] = ['unchanged', 'created']\n\n/** Every action, in the order the summary line names them. */\nconst COUNT_ORDER: readonly WriteAction[] = ['unchanged', 'created', 'updated', 'removed', 'failed']\n\n/**\n * Sync one workspace package, resolving its declared type first.\n *\n * `readDeclaredPackageType` is total by contract \u2014 an absent, unloadable or unrecognised\n * config resolves to `undefined` rather than rejecting \u2014 so a package whose config throws at\n * module scope degrades to detection instead of taking a whole `--all` run down with it.\n */\nconst syncOnePackage = async (\n packageDir: string,\n repoRoot: string,\n design: boolean | undefined,\n): Promise<GuidanceWrite[]> => {\n const declaredType = await readDeclaredPackageType(packageDir)\n\n return syncPackageGuidance(packageDir, { repoRoot, version: packageJson.version, design, declaredType })\n}\n\n/**\n * Run the sync over the resolved scope, which mirrors `audit`'s exactly: `--root` is the root\n * block only, `--all` is every discovered package and never the root, and no flag is the single\n * package walked up from cwd. Nothing here throws on a per-file error \u2014 a failure comes back as\n * an entry with `action: 'failed'`, because a run that aborted halfway could leave exactly one\n * well-formed block behind, adopting the workspace and reddening every package it never reached.\n */\nconst syncScope = async (options: AuditFixOptions, repoRoot: string): Promise<GuidanceWrite[]> => {\n if (options.root) {\n return syncRootGuidance(repoRoot, { version: packageJson.version })\n }\n\n if (options.all) {\n const written: GuidanceWrite[] = []\n\n for (const packageDir of await discoverPackages(repoRoot)) {\n written.push(...(await syncOnePackage(packageDir, repoRoot, options.design)))\n }\n\n return written\n }\n\n const packageDir = await findPackageRoot(options.cwd ?? process.cwd())\n\n return syncOnePackage(packageDir, repoRoot, options.design)\n}\n\n/** `unchanged` files are counted, not listed \u2014 a 63-package refresh must not cost 63 lines. */\nconst buildSummaryLine = (written: GuidanceWrite[]): string => {\n const counts = new Map<WriteAction, number>()\n\n for (const file of written) {\n counts.set(file.action, (counts.get(file.action) ?? 0) + 1)\n }\n\n const parts = COUNT_ORDER.filter((action) => {\n return ALWAYS_COUNTED.includes(action) || (counts.get(action) ?? 0) > 0\n }).map((action) => {\n return `${counts.get(action) ?? 0} ${action}`\n })\n\n return `Agent guidance synced \u2014 ${parts.join(', ')} (infra-kit ${packageJson.version})`\n}\n\n/**\n * Print one line per changed file, then the summary. Paths are repo-relative because the\n * absolute ones are noise in a CI log, and a failed write names its reason inline: the exit\n * code says a write failed, only this line says which one and why.\n */\nconst logWrites = (written: GuidanceWrite[], repoRoot: string): void => {\n for (const file of written) {\n if (file.action === 'unchanged') continue\n\n const relPath = path.relative(repoRoot, file.path)\n const type = file.type ? ` (${file.type})` : ''\n const reason = file.action === 'failed' && file.message ? ` \u2014 ${file.message}` : ''\n\n logger.info(` ${file.action.padEnd(9)} ${relPath}${type}${reason}`)\n }\n\n logger.info(buildSummaryLine(written))\n}\n\n/** Drop the failure `message`, which is logged rather than published in `structuredContent`. */\nconst toFixedEntry = (file: GuidanceWrite): FixedEntry => {\n const entry: FixedEntry = { path: file.path, action: file.action }\n\n if (file.type) entry.type = file.type\n\n return entry\n}\n\n/**\n * The `audit --fix` branch: write the guidance blocks for the resolved scope, log what changed,\n * and report whether the run flipped the workspace into adoption. Runs BEFORE the checks (the\n * `eslint --fix` model), so a block this call just created reports `ok` in the same invocation.\n *\n * Sets no exit code \u2014 `audit()`'s standing invariant is that it never touches `process.exitCode`,\n * so the MCP tool can reuse it. The `fixed` entries carrying `action: 'failed'` are the signal;\n * `program.ts` turns them into an exit code.\n *\n * The adoption cache is dropped on both sides of the sync: before, so the pre-fix verdict is the\n * real working-tree state rather than a memo from earlier in the process, and after, so the\n * post-fix verdict \u2014 the one `audit()` then threads into every check \u2014 sees the files just written.\n *\n * @example\n * await runAuditFix({ all: true }, '/repo')\n * // logs: created apps/demo/ui/CLAUDE.md (frontend)\n * // Agent guidance synced \u2014 3 unchanged, 1 created (infra-kit 0.4.0)\n * // => { fixed: [{ path: '/repo/apps/demo/ui/CLAUDE.md', action: 'created', type: 'frontend' }], adoptionFlipped: false }\n */\nexport const runAuditFix = async (options: AuditFixOptions, workspaceRoot: string | null): Promise<AuditFixResult> => {\n const repoRoot = await getProjectRoot()\n\n resetAdoptionCache()\n\n const before = await resolveAdoption(workspaceRoot)\n const written = await syncScope(options, repoRoot)\n\n logWrites(written, repoRoot)\n resetAdoptionCache()\n\n const after = await resolveAdoption(workspaceRoot)\n // An `--all` or `--root` run leaves no package behind, so it can never be the run that makes\n // every other package start failing. Only a scoped fix can.\n const singleScope = !options.all && !options.root\n\n return {\n fixed: written.map(toFixedEntry),\n adoptionFlipped: singleScope && !before.adopted && after.adopted,\n }\n}\n", "/**\n * Root-audit checks for `devServersPresets`: target-key grammar, then proxy locality.\n *\n * Bridges the impure world (project config + discovery + each frontend's loaded\n * `infra-kit.config.ts`) to the pure {@link validatePresetKeys} / {@link validatePresetProxy}\n * rules, then shapes the result as a {@link PackageValidationResult} so it slots into the\n * existing `infra-kit audit --root` output. Skipped (returns null) when the project declares\n * no `devServersPresets`.\n */\nimport { discoverApiApps, discoverUiApps } from 'src/dev/discovery'\nimport { buildPresetProxyContext } from 'src/dev/preset-proxy-context'\nimport type { LoadError } from 'src/dev/preset-proxy-context'\nimport { validatePresetKeys, validatePresetProxy } from 'src/dev/presets'\nimport type { DiscoveredParts } from 'src/dev/presets'\nimport { getInfraKitConfig } from 'src/lib/infra-kit-config'\nimport type { PackageCheck, PackageValidationResult } from 'src/lib/package-validator'\n\n/** Shape validator issues + config load errors into audit check lines (fail-only; caller adds the pass line). */\nconst toFailChecks = (issues: ReturnType<typeof validatePresetProxy>, loadErrors: LoadError[]): PackageCheck[] => {\n return [\n ...loadErrors.map((error): PackageCheck => {\n return {\n name: `devServersPresets:config:${error.app}`,\n status: 'fail',\n message: `could not load apps/${error.app}/ui/infra-kit.config.ts (referenced by a preset proxy override): ${error.message}`,\n }\n }),\n ...issues.map((issue): PackageCheck => {\n return { name: `devServersPresets:${issue.preset}`, status: 'fail', message: issue.message }\n }),\n ]\n}\n\n/**\n * Run the `devServersPresets` audit for the monorepo root: first the target-key grammar\n * (every key must name an `<app>/api` or `<app>/ui` package), then proxy locality. Returns\n * a synthetic `devServersPresets` validation result (one pass line, or one fail line per\n * bad key / violation / unloadable referenced config), or null when there are no presets\n * to check.\n *\n * @example\n * const result = await checkDevPresets('/repo')\n * // result?.checks[0] => { name: 'devServersPresets:proxy-locality', status: 'pass', message: '\u2026' }\n */\nexport const checkDevPresets = async (root: string): Promise<PackageValidationResult | null> => {\n const presets = (await getInfraKitConfig()).devServersPresets\n\n if (!presets || Object.keys(presets).length === 0) {\n return null\n }\n\n // Grammar first: an unparseable target key makes `resolvePreset` throw, so it has to\n // surface as an audit line here rather than crash every check below it.\n const keyIssues = validatePresetKeys(presets)\n\n if (keyIssues.length > 0) {\n const checks = keyIssues.map((issue): PackageCheck => {\n return { name: `devServersPresets:${issue.preset}`, status: 'fail', message: issue.message }\n })\n\n return { packageDir: root, packageName: 'devServersPresets', checks, passed: false }\n }\n\n const apiApps = discoverApiApps(root)\n const uiApps = discoverUiApps(root)\n const discovered: DiscoveredParts = {\n api: apiApps.map((a) => {\n return a.name\n }),\n ui: uiApps.map((a) => {\n return a.name\n }),\n }\n\n const { ctx, loadErrors } = await buildPresetProxyContext(root, presets, discovered, apiApps)\n\n const failChecks = toFailChecks(validatePresetProxy(presets, ctx), loadErrors)\n\n const checks: PackageCheck[] =\n failChecks.length === 0\n ? [\n {\n name: 'devServersPresets:proxy-locality',\n status: 'pass',\n message: 'every local proxy override resolves to a backend the preset launches',\n },\n ]\n : failChecks\n\n return { packageDir: root, packageName: 'devServersPresets', checks, passed: failChecks.length === 0 }\n}\n", "import { z } from 'zod'\n\nimport { getInfraKitConfig, getInfraKitConfigPaths } from 'src/lib/infra-kit-config'\nimport { logger } from 'src/lib/logger'\nimport { defineMcpTool, textContent } from 'src/types'\n\n/**\n * Return the fully MERGED infra-kit config \u2014 the exact object every other command sees at runtime, after\n * the three override layers (project `infra-kit.json` \u2192 `~/.infra-kit/infra-kit.json` \u2192\n * `~/.infra-kit/projects/<repo>/infra-kit.json`) are shallow-merged. Read-only introspection: it REUSES\n * {@link getInfraKitConfig} (never re-implements the loader), so an agent sees precisely the resolved\n * config, not a re-derived approximation, and can reason about dev presets / env management / proxy routes\n * without shelling out.\n *\n * Throws the same \"infra-kit.json not found\" error the loader throws when run outside a configured repo \u2014\n * that is an honest failure, not an empty success.\n */\nexport const configGet = async () => {\n const [config, paths] = await Promise.all([getInfraKitConfig(), getInfraKitConfigPaths()])\n\n const topLevelKeys = Object.keys(config).sort()\n\n logger.info(`Merged config for ${paths.projectName} \u2014 ${topLevelKeys.length} section(s): ${topLevelKeys.join(', ')}`)\n logger.info(` source: ${paths.main}`)\n\n const structuredContent = {\n // `config` is the merged object. A typed InfraKitConfig is assignable to Record<string, unknown>.\n config: config as Record<string, unknown>,\n configPath: paths.main,\n projectName: paths.projectName,\n }\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n}\n\nconst configGetOutputSchema = {\n config: z\n .record(z.string(), z.unknown())\n .describe(\n 'The fully resolved infra-kit configuration \u2014 the same object every command sees at runtime, after the project infra-kit.json, the user-global, and the user-scope per-project override layers are merged (later layers win). Top-level sections include envManagement, ide, worktrees, envAutoLoad, dev, devServersPresets, and devProxy.',\n ),\n configPath: z\n .string()\n .describe('Absolute path to the project-level infra-kit.json (the base layer / committed source of truth).'),\n projectName: z\n .string()\n .describe(\n 'Repository name the config is keyed to (the main-repo basename; shared across all worktrees of the repo).',\n ),\n}\n\n// MCP Tool Registration\nexport const configGetMcpTool = defineMcpTool({\n name: 'config-get',\n description:\n 'Return the fully merged infra-kit configuration (project + user-global + per-project override layers) as it is resolved at runtime. Read-only introspection \u2014 makes no changes; use `config edit` (CLI-only) to modify the override file. Fails with the loader error when run outside a configured infra-kit repo.',\n inputSchema: {},\n outputSchema: configGetOutputSchema,\n handler: () => {\n return configGet()\n },\n})\n", "import fs from 'node:fs'\nimport { z } from 'zod'\n\nimport { defaultIsListening } from 'src/dev/proxy/portless-driver'\nimport { readDevContext } from 'src/lib/dev-context'\nimport { logger } from 'src/lib/logger'\nimport { defineMcpTool, textContent } from 'src/types'\n\n/**\n * Injection seams for the handler. All three default to the real world; tests override them so the read is\n * hermetic (a seeded fragment dir, a fixed clock, a stub probe) and NO server is ever spawned.\n */\nexport interface DevStatusDeps {\n /** Clock for the freshness computation. Defaults to `Date.now`. */\n now?: () => number\n /**\n * Directory the fragment search STARTS from \u2014 it then walks UPWARD (see {@link readDevContext}).\n * Defaults to `process.cwd()`.\n */\n cwd?: string\n /**\n * TCP liveness probe. Defaults to the engine's OWN probe ({@link defaultIsListening}, a bounded\n * `net.connect`) \u2014 the same one `doctor` uses. `dev-status` NEVER asserts liveness it did not probe:\n * every `live` value below is a real connect result, not an inference from fragment presence.\n */\n isListening?: (port: number) => Promise<boolean>\n}\n\n/**\n * Report what `infra-kit dev` currently has running, by READING the dev-context fragments the dev server\n * writes to `.infra-kit/dev-context/`. Strictly advisory and strictly read-only: it starts no server and\n * only inspects on-disk state plus a bounded TCP probe.\n *\n * The fragment directory is located by {@link readDevContext}, the SAME reader the MCP\n * `infra-kit://dev-context` resource uses, which searches UPWARD from `cwd`.\n *\n * Three facts an agent must not conflate are surfaced separately: PRESENCE + FRESHNESS\n * (`fragmentMtime` / `ageSeconds`, from the file's mtime), LIVENESS (`live` \u2014 does the port answer\n * a TCP connect RIGHT NOW), and WHERE THIS LOOKED (`contextDir`, with `contextDirExists` as its\n * discriminator). When no fragments exist (no dev session, or `dev` cleaned up on exit) it returns\n * `{ active: false, apps: [] }` \u2014 a clean empty result, not an error.\n */\n// Sharing the reader is the fix for a shipped bug: this command used to carry its own reader that\n// did a plain `path.join(cwd, \u2026)`, so from any subdirectory the MCP resource reported a live\n// session while this tool reported none \u2014 one server, one question, two answers.\n//\n// Why the three facts stay separate:\n// - a STALE fragment is a strong hint a server died without cleaning up, which is why mtime is\n// the authoritative signal rather than mere presence;\n// - a FRESH fragment with `live:false` is a crashed or killed dev server whose fragment was\n// never removed;\n// - `contextDir` is the RESOLVED directory, not necessarily one that exists. The resolver falls\n// back to deriving `<up-tree .infra-kit>/dev-context`, so outside a repo that has one it can\n// name a `$HOME` path nothing ever wrote to. Without `contextDirExists` beside it, that path\n// would be actively misleading.\nexport const devStatus = async (deps: DevStatusDeps = {}) => {\n const now = deps.now ?? Date.now\n const isListening = deps.isListening ?? defaultIsListening\n\n const snapshot = readDevContext(deps.cwd)\n const at = now()\n\n // `readDevContext` stamps its own `Date.now()` into `readAt`/`ageMs`; both are deliberately ignored so\n // the injected clock \u2014 not wall time \u2014 governs freshness. Only `mtimeMs` (filesystem truth) is used.\n const apps = await Promise.all(\n snapshot.apps.map(async (record) => {\n const ageSeconds = Math.max(0, Math.round((at - record.mtimeMs) / 1000))\n const live = await isListening(record.port)\n\n return {\n app: record.app,\n package: record.package,\n port: record.port,\n pid: record.pid,\n alias: record.alias,\n origin: record.origin,\n release: record.release,\n writtenAt: record.writtenAt > 0 ? new Date(record.writtenAt).toISOString() : null,\n fragmentMtime: new Date(record.mtimeMs).toISOString(),\n ageSeconds,\n live,\n }\n }),\n )\n\n const active = apps.length > 0\n const contextDir = snapshot.fragmentDir ?? null\n // `isDirectory`, NOT a bare `existsSync`: a plain FILE at this path also \"exists\", but the reader's\n // `readdirSync` then throws ENOTDIR and is swallowed into an empty result \u2014 which would report\n // `contextDirExists: true` for a path that could not be read at all. This field's entire job is to\n // separate \"resolved\" from \"readable\", so it must not count a non-directory as the latter.\n const contextDirExists =\n contextDir !== null && (fs.statSync(contextDir, { throwIfNoEntry: false })?.isDirectory() ?? false)\n const where = contextDir === null ? 'nothing (no .infra-kit anchor exists up-tree)' : contextDir\n\n if (!active) {\n // Deliberately does not claim the directory was READ: the reader swallows any `readdirSync` failure\n // (a permissions error, for one) into the same empty result, so \"no fragment\" is an inference here,\n // not an observation.\n const why = contextDirExists ? 'no readable fragment found there' : 'that directory does not exist'\n\n logger.info(`No active infra-kit dev session \u2014 searched ${where} (${why}).`)\n } else {\n logger.info(`infra-kit dev: ${apps.length} app(s) with a dev-context fragment in ${where}:`)\n\n for (const app of apps) {\n const liveness = app.live ? 'live' : 'NOT responding'\n const freshness = `written ${app.ageSeconds}s ago`\n\n logger.info(` ${app.app} (${app.package}) \u2192 ${app.origin} on :${app.port} \u2014 ${liveness}, ${freshness}`)\n }\n }\n\n const structuredContent = { active, apps, contextDir, contextDirExists }\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n}\n\nconst devStatusOutputSchema = {\n active: z\n .boolean()\n .describe(\n 'True when at least one dev-context fragment exists on disk (an infra-kit dev session has apps registered). False means no active dev session was found \u2014 this is a clean result, NOT an error.',\n ),\n contextDir: z\n .string()\n .nullable()\n .describe(\n 'The RESOLVED dev-context fragment directory \u2014 found by searching UPWARD from the working directory. This is where the read looked, NOT proof anything is there: when no fragment directory exists the resolver derives one from an up-tree `.infra-kit`, so this can name a path (even under $HOME) that was never written to. Check contextDirExists before drawing any conclusion from it. Null when no `.infra-kit` anchor exists up-tree at all.',\n ),\n contextDirExists: z\n .boolean()\n .describe(\n 'Whether contextDir actually exists on disk. False with a non-null contextDir means the path was DERIVED, not read \u2014 the usual state in a repo where `infra-kit dev` has never run. Distinguishes \"looked and found nothing\" from \"had nowhere to look\".',\n ),\n apps: z\n .array(\n z.object({\n app: z.string().describe('App folder name (the fragment filename without .json).'),\n package: z.string().describe('The app package name the vite helper reads to decide a local route.'),\n port: z.number().describe('The actual bound port the dev server is listening on.'),\n pid: z\n .number()\n .describe('PID of the dev-server process that wrote the fragment (ownership / staleness metadata).'),\n alias: z.string().describe('Registered portless hostname for the app.'),\n origin: z.string().describe('Authoritative local origin the vite helper proxies to for this app.'),\n release: z.string().describe('Release slug the runner recorded for this worktree.'),\n writtenAt: z\n .string()\n .nullable()\n .describe(\n 'ISO 8601 time the runner self-reported writing the fragment (writer clock \u2014 may be wrong; prefer fragmentMtime). Null when the fragment did not record it.',\n ),\n fragmentMtime: z\n .string()\n .describe('ISO 8601 filesystem mtime of the fragment file \u2014 the AUTHORITATIVE freshness signal.'),\n ageSeconds: z\n .number()\n .describe(\n 'Seconds since the fragment was last written on disk. A large value with live:false means a stale/abandoned fragment.',\n ),\n live: z\n .boolean()\n .describe(\n 'Whether a TCP connection to the app port succeeds RIGHT NOW (a real engine liveness probe, not an inference). A fresh fragment with live:false is a crashed or killed dev server whose fragment was not cleaned up.',\n ),\n }),\n )\n .describe('One entry per dev-context fragment on disk, sorted by app name. Empty when no dev session is active.'),\n}\n\n// MCP Tool Registration\nexport const devStatusMcpTool = defineMcpTool({\n name: 'dev-status',\n description:\n 'Report what `infra-kit dev` currently has running by READING the on-disk dev-context fragments (never starts a server). Advisory + read-only: surfaces each app\u2019s package, bound port, portless alias, local origin, fragment freshness (mtime / ageSeconds), and a live TCP liveness probe of the port. The fragment directory is found by searching UPWARD from the working directory, so this agrees with the `infra-kit://dev-context` resource and with the vite helper\u2019s routing decision; `contextDir` / `contextDirExists` report where it looked and whether that directory is real. Returns { active: false, apps: [] } when no dev session is active.',\n inputSchema: {},\n outputSchema: devStatusOutputSchema,\n handler: () => {\n return devStatus()\n },\n})\n", "import fs from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\n\n/** Repo-relative dev-context fragment directory the `infra-kit dev` runner writes into. */\nconst DEV_CONTEXT_DIR = path.join('.infra-kit', 'dev-context')\n\n/** The state directory that holds {@link DEV_CONTEXT_DIR}, used as an up-tree fallback anchor. */\nconst STATE_DIR = '.infra-kit'\n\n/**\n * Search upward from `start` for `relative`, returning the first hit or `undefined`. Mirrors the\n * `infra-kit/vite` helper's own `findUp`, because the MCP server is long-lived and may be rooted at\n * any nested package dir \u2014 the fragments live at the repo root's `.infra-kit/dev-context`, not the cwd.\n */\nconst findUp = (start: string, relative: string): string | undefined => {\n let dir = path.resolve(start)\n\n for (;;) {\n const candidate = path.join(dir, relative)\n\n if (fs.existsSync(candidate)) return candidate\n\n const parent = path.dirname(dir)\n\n if (parent === dir) return undefined\n\n dir = parent\n }\n}\n\n/**\n * Locate the dev-context fragment directory for `cwd`: prefer an existing `.infra-kit/dev-context` found\n * up-tree, else derive it from an up-tree `.infra-kit` (which may exist before the runner has written any\n * fragment), else `undefined` when neither anchor is present.\n */\nconst resolveFragmentDir = (cwd: string): string | undefined => {\n const existing = findUp(cwd, DEV_CONTEXT_DIR)\n\n if (existing) return existing\n\n const state = findUp(cwd, STATE_DIR)\n\n return state ? path.join(state, 'dev-context') : undefined\n}\n\n/** One backend the `infra-kit dev` runner has recorded as up, plus advisory staleness metadata. */\nexport interface DevContextApp {\n /** App folder name the fragment is keyed by (`<app>.json`). */\n app: string\n /** The app's package name (what the vite helper's `readLocalSet` matches on). */\n package: string\n /** The ACTUAL bound port \u2014 the runner is the binder, so this is authoritative. */\n port: number\n /** The registered portless hostname the runner published for this backend. */\n alias: string\n /** The authoritative local origin the vite helper proxies to. */\n origin: string\n /** Release slug the runner recorded (empty when it derived none). */\n release: string\n /** Writer PID \u2014 advisory only; NOT probed for liveness here. */\n pid: number\n /** Epoch ms the runner stamped into the fragment. */\n writtenAt: number\n /** The fragment file's mtime in epoch ms \u2014 the freshness signal a consumer should judge staleness by. */\n mtimeMs: number\n /** How old the fragment is (now \u2212 mtime), in ms. Advisory: a live backend refreshes this on each write. */\n ageMs: number\n}\n\n/**\n * The dev-context resource payload. Read-only and side-effect free: this reflects what `infra-kit dev`\n * last wrote to disk, never a live probe. `session: 'none'` (with an empty `apps`) is the normal,\n * non-error state when no dev session has written any fragment \u2014 an absent session is not a failure.\n */\nexport interface DevContextSnapshot {\n /** `'active'` iff at least one readable fragment was found, else `'none'`. */\n session: 'active' | 'none'\n /** The resolved fragment directory, or `undefined` when no `.infra-kit` anchor exists up-tree. */\n fragmentDir: string | undefined\n /** Epoch ms this snapshot was read, so a consumer can compute freshness against {@link DevContextApp.mtimeMs}. */\n readAt: number\n /** Every backend the runner currently records as up, sorted by app name. Empty when the session is `none`. */\n apps: DevContextApp[]\n}\n\n/** Coerce an unknown parsed fragment field to a number, defaulting to `0` when absent/non-numeric. */\nconst num = (value: unknown): number => {\n return typeof value === 'number' && Number.isFinite(value) ? value : 0\n}\n\n/** Coerce an unknown parsed fragment field to a string, defaulting to `''`. */\nconst str = (value: unknown): string => {\n return typeof value === 'string' ? value : ''\n}\n\n/** Parse one `<app>.json` fragment into a {@link DevContextApp}, or `null` when it is unreadable/not JSON. */\nconst readFragment = (fragmentDir: string, file: string, now: number): DevContextApp | null => {\n const full = path.join(fragmentDir, file)\n\n try {\n const stat = fs.statSync(full)\n const parsed: unknown = JSON.parse(fs.readFileSync(full, 'utf-8'))\n\n if (typeof parsed !== 'object' || parsed === null) return null\n\n const fragment = parsed as Record<string, unknown>\n\n return {\n app: file.replace(/\\.json$/, ''),\n package: str(fragment.package),\n port: num(fragment.port),\n alias: str(fragment.alias),\n origin: str(fragment.origin),\n release: str(fragment.release),\n pid: num(fragment.pid),\n writtenAt: num(fragment.writtenAt),\n mtimeMs: Math.round(stat.mtimeMs),\n ageMs: Math.max(0, Math.round(now - stat.mtimeMs)),\n }\n } catch {\n // A half-written, deleted-mid-read, or non-JSON fragment is skipped rather than failing the whole\n // read \u2014 the resource stays advisory and never throws on a transient runner write.\n return null\n }\n}\n\n/**\n * Read the dev-context fragments `infra-kit dev` last wrote for the repo containing `cwd`.\n *\n * Strictly read-only and never throws: a missing anchor, a missing fragment dir, or an unreadable\n * fragment all resolve to a clean snapshot (`session: 'none'` when nothing readable was found). Freshness\n * is reported via each fragment's `mtimeMs`/`ageMs`; no liveness is asserted that was not observed on disk.\n */\nexport const readDevContext = (cwd: string = process.cwd()): DevContextSnapshot => {\n const now = Date.now()\n const fragmentDir = resolveFragmentDir(cwd)\n\n if (!fragmentDir || !fs.existsSync(fragmentDir)) {\n return { session: 'none', fragmentDir, readAt: now, apps: [] }\n }\n\n let files: string[]\n\n try {\n files = fs.readdirSync(fragmentDir).filter((file) => {\n return file.endsWith('.json')\n })\n } catch {\n return { session: 'none', fragmentDir, readAt: now, apps: [] }\n }\n\n const apps = files\n .map((file) => {\n return readFragment(fragmentDir, file, now)\n })\n .filter((app): app is DevContextApp => {\n return app !== null\n })\n .sort((a, b) => {\n return a.app.localeCompare(b.app)\n })\n\n return { session: apps.length > 0 ? 'active' : 'none', fragmentDir, readAt: now, apps }\n}\n", "import fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\nimport process from 'node:process'\nimport { z } from 'zod'\nimport { $ } from 'zx'\n\nimport { decidePrune, isDevSessionRunning } from 'src/commands/doctor/prune-routes'\n// The one source of truth for \"does `--fix` repair this row\". Imported rather than restated so the\n// `fixable` field on the payload cannot drift from the `--fix` code paths, exactly as `report.ts`'s\n// own comment requires of its hint.\nimport { FIXABLE_NAMES } from 'src/commands/doctor/report'\nimport { buildDopplerChildEnv } from 'src/commands/env-load/env-load'\n// `resolveGitRoot` is the WRITER's gate, imported rather than re-derived so the reader's row set and\n// the writer's reach cannot drift apart (see the gate comment beside the plugin rows below).\nimport { AGENTS_MARKER_END, AGENTS_MARKER_START, resolveGitRoot } from 'src/commands/init/agent-files'\nimport { MARKER_END, MARKER_START, buildShellBlock } from 'src/commands/init/init'\nimport {\n caFingerprintMatches,\n createPortlessDriver,\n defaultIsListening,\n defaultIsProxyServing,\n formatPortlessCommand,\n handshakeChainsToCa,\n listRoutes,\n readCaPath,\n resolvePortlessBin,\n} from 'src/dev/proxy/portless-driver'\nimport type { HandshakeResult, PortlessRoute } from 'src/dev/proxy/portless-driver'\nimport { INFRA_KIT_ENV_TOKEN_VAR, probeEnvToken, resolveEnvToken } from 'src/integrations/doppler'\nimport type { EnvTokenProbe, EnvTokenSource, ResolvedEnvToken } from 'src/integrations/doppler'\nimport { inspectPackageGuidance, readGuidanceFile } from 'src/lib/agent-guidance'\nimport { describeOverrides, readOverrideSummary } from 'src/lib/config-overrides'\nimport { DEFAULT_WARM_TTL_SECONDS, ENV_LOAD_FILE, getProjectWarmCacheDir } from 'src/lib/constants'\n// The probe reports FACTS about an install and can neither name nor run a fix \u2014 the same one-way seam\n// as the registry import below. `lib/dependency-plan` is the module that turns a probe into a recipe,\n// and `doctor` deliberately stops one module short of it.\nimport { defaultProbeDeps, probeAll } from 'src/lib/dependency-probe'\nimport type { DependencyState, ProbeDeps } from 'src/lib/dependency-probe'\n// Probe argv only. `doctor` must never import the install recipes: the registry owns \"how do I ask this\n// tool for its version\", `setup-dependency*` owns \"what would fix it\", and the direction is one-way.\nimport { specFor } from 'src/lib/dependency-registry'\nimport type { DependencyId } from 'src/lib/dependency-registry'\nimport { getTokenStorePath, readTokenStore } from 'src/lib/env-tokens'\nimport type { TokenStore } from 'src/lib/env-tokens'\nimport { getProjectRoot } from 'src/lib/git-utils/git-utils'\nimport {\n DEFAULT_DEV_PROXY_PORT,\n getInfraKitConfig,\n getInfraKitConfigPaths,\n resetInfraKitConfigCache,\n resolveConfiguredIdes,\n} from 'src/lib/infra-kit-config'\nimport type { InfraKitConfig } from 'src/lib/infra-kit-config'\nimport { hasManagedBlock } from 'src/lib/managed-block'\nimport { discoverPackages } from 'src/lib/package-validator/loader'\nimport { tildify } from 'src/lib/path-display'\nimport {\n MARKETPLACE_NAME,\n MARKETPLACE_REPO,\n PLUGIN_INSTALL_COMMAND,\n PLUGIN_KEY,\n inspectMcpRegistration,\n isMarketplaceRegistered,\n readInstalledPluginVersion,\n resolvePluginInstall,\n} from 'src/lib/plugin-pointer'\nimport type { McpRegistration, PluginInstallState } from 'src/lib/plugin-pointer'\nimport { listProjectEnvNames } from 'src/lib/project-envs'\nimport { sortVersions } from 'src/lib/version-utils'\nimport { canonicalizeProjectRoot } from 'src/lib/warm-cache'\nimport { defineMcpTool, textContent } from 'src/types'\n\nimport packageJson from '../../../package.json' with { type: 'json' }\n\n/**\n * What the probe knows about one external tool, riding along on the row that already answers \"is it\n * installed\" \u2014 so an agent asking that question reads ONE list, never a sibling array with its own\n * semantics. Deliberately probe facts ONLY: no `action`, no `commands`. Widening it to the install\n * report is how `doctor --fix` would arrive by another door, and `probe-argv-single-source.test.ts`\n * asserts the absence of those two keys at the type level for that reason.\n *\n * `present` and `onPath` are separate because they genuinely disagree: the AWS-documented installer\n * writes `$HOME/.local/bin`, so `aws` can be `status: 'fail'` (the row means \"resolves on PATH\") while\n * `detail.present` is `true`.\n */\nexport interface DependencyDetail {\n manager: DependencyState['manager']\n version: string | null\n present: boolean\n onPath: boolean\n}\n\n/**\n * One diagnosis. `name` is a stable public identifier \u2014 it is returned over MCP, keyed by the report's\n * section map (`report.ts`), and pasted into bug reports \u2014 so renaming one is a breaking change.\n *\n * `detail` is OPTIONAL and carried by the four dependency rows alone, which is what keeps the ~23 other\n * check functions untouched. `portless installed` deliberately has none: it resolves out of\n * `node_modules` rather than off `PATH`, so a probe payload would describe a different question than\n * the row asks (`probe-argv-single-source.test.ts`).\n */\nexport interface CheckResult {\n name: string\n status: 'pass' | 'fail'\n message: string\n detail?: DependencyDetail\n}\n\n/**\n * The rows that carry a {@link DependencyDetail} \u2014 every registry id except `portless`, whose\n * exclusion is argued in `probe-argv-single-source.test.ts`. Declared here rather than derived from\n * `DEPENDENCY_IDS` minus a filter: the exclusion is a decision, and a decision should be readable.\n */\nconst DETAILED_IDS: readonly DependencyId[] = ['brew', 'gh', 'doppler', 'aws']\n\n/**\n * Probe the detailed ids once, keyed by id. Runs alongside the `--version` rows rather than replacing\n * them: the rows keep their published meaning (\"resolves on PATH\"), and the probe answers the richer\n * question beside it.\n */\nconst probeDependencyDetails = async (deps: ProbeDeps): Promise<Map<DependencyId, DependencyDetail>> => {\n const states = await probeAll(DETAILED_IDS, deps)\n\n return new Map(\n states.map((state): [DependencyId, DependencyDetail] => {\n return [\n state.id,\n { manager: state.manager, version: state.version, present: state.present, onPath: state.onPath },\n ]\n }),\n )\n}\n\nconst checkCommand = async (\n name: string,\n command: string[],\n successMsg: string,\n failMsg: string,\n): Promise<CheckResult> => {\n try {\n await $`${command}`\n\n return { name, status: 'pass', message: successMsg }\n } catch {\n return { name, status: 'fail', message: failMsg }\n }\n}\n\nexport const checkZshrcInitialized = (): CheckResult => {\n const name = 'zshrc init block'\n const zshrcPath = path.join(os.homedir(), '.zshrc')\n\n if (!fs.existsSync(zshrcPath)) {\n return { name, status: 'fail', message: '~/.zshrc not found. Run: infra-kit setup --skip-tools' }\n }\n\n const content = fs.readFileSync(zshrcPath, 'utf-8')\n const startIdx = content.indexOf(MARKER_START)\n const endIdx = content.indexOf(MARKER_END)\n\n if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {\n return {\n name,\n status: 'fail',\n message: 'infra-kit shell block missing from ~/.zshrc. Run: infra-kit setup --skip-tools',\n }\n }\n\n const installedBlock = content.slice(startIdx, endIdx + MARKER_END.length).trim()\n const expectedBlock = buildShellBlock().trim()\n\n if (installedBlock !== expectedBlock) {\n return {\n name,\n status: 'fail',\n message: 'infra-kit shell block in ~/.zshrc is out of date. Run: infra-kit setup --skip-tools',\n }\n }\n\n return { name, status: 'pass', message: 'infra-kit shell block in ~/.zshrc is up to date' }\n}\n\n/**\n * Surface the state of the project-scoped warm env cache (populated by an\n * auto-loaded shell startup). Informational only \u2014 always passes \u2014 since a\n * missing or stale warm file just means the next auto-load will (re)populate\n * it rather than indicating a problem.\n */\nconst checkWarmCache = async (): Promise<CheckResult> => {\n const name = 'warm cache'\n\n let root: string\n\n try {\n root = await getProjectRoot()\n } catch {\n return { name, status: 'pass', message: 'warm cache: not a git project' }\n }\n\n const canon = canonicalizeProjectRoot(root)\n\n if (!canon) {\n return { name, status: 'pass', message: 'warm cache: unavailable' }\n }\n\n const warmFile = path.join(getProjectWarmCacheDir(canon), ENV_LOAD_FILE)\n\n if (!fs.existsSync(warmFile)) {\n return { name, status: 'pass', message: 'warm cache: none yet (populated on next auto-load)' }\n }\n\n const ageSeconds = Math.floor((Date.now() - fs.statSync(warmFile).mtimeMs) / 1000)\n const ageMinutes = Math.floor(ageSeconds / 60)\n const ttlMinutes = Math.floor(DEFAULT_WARM_TTL_SECONDS / 60)\n\n if (ageSeconds >= DEFAULT_WARM_TTL_SECONDS) {\n return {\n name,\n status: 'pass',\n message: `warm cache: present but stale (${ageMinutes}m old, TTL ${ttlMinutes}m) \u2014 will refresh`,\n }\n }\n\n return { name, status: 'pass', message: `warm cache: present, ${ageMinutes}m old` }\n}\n\nconst checkPnpmWorkspaceVirtualStore = async (): Promise<CheckResult> => {\n const name = 'pnpm enableGlobalVirtualStore'\n\n try {\n const root = await getProjectRoot()\n const yamlPath = path.join(root, 'pnpm-workspace.yaml')\n\n if (!fs.existsSync(yamlPath)) {\n return { name, status: 'fail', message: `pnpm-workspace.yaml not found at ${yamlPath}` }\n }\n\n const content = fs.readFileSync(yamlPath, 'utf-8')\n // eslint-disable-next-line sonarjs/super-linear-regex\n const enabled = /^\\s*enableGlobalVirtualStore\\s*:\\s*true\\s*$/m.test(content)\n\n if (!enabled) {\n return {\n name,\n status: 'fail',\n message: 'enableGlobalVirtualStore: true is missing in pnpm-workspace.yaml',\n }\n }\n\n return { name, status: 'pass', message: 'enableGlobalVirtualStore: true is set' }\n } catch (err) {\n return {\n name,\n status: 'fail',\n message: `Failed to read pnpm-workspace.yaml: ${(err as Error).message}`,\n }\n }\n}\n\n/**\n * The merged infra-kit config, read ONCE per `doctor()` run and threaded into every check that needs\n * it. Either half is populated, never both.\n *\n * Reading it once is not an optimization, it is the fix for a real race: `checkInfraKitConfigValid`\n * and `checkIdeInstalled` each called `resetInfraKitConfigCache()` and were dispatched together\n * inside one `Promise.all`, so they clobbered the same module-level cache slot \u2014 whichever reset\n * landed second could blow away a config the other had just populated. One read, no shared slot.\n */\nexport interface DoctorConfig {\n config: InfraKitConfig | null\n error: Error | null\n}\n\n/**\n * Read the merged config once for the whole run, resetting the module cache exactly once so `doctor`\n * always reports what is on disk NOW (it is the command a user runs right after editing the file).\n * Never throws: a broken config is the thing `doctor` exists to explain, so it is captured and\n * threaded rather than allowed to abort the run.\n *\n * @example\n * await readDoctorConfig() // => { config: { environments: ['dev'], \u2026 }, error: null }\n * // // broken file => { config: null, error: Error('\u2026unrecognized key\u2026') }\n */\nexport const readDoctorConfig = async (): Promise<DoctorConfig> => {\n try {\n resetInfraKitConfigCache()\n\n return { config: await getInfraKitConfig(), error: null }\n } catch (err) {\n return { config: null, error: err as Error }\n }\n}\n\nconst checkInfraKitConfigValid = (read: DoctorConfig): CheckResult => {\n const name = 'infra-kit config valid'\n\n if (read.error) return { name, status: 'fail', message: read.error.message }\n\n return {\n name,\n status: 'pass',\n message: 'infra-kit.json is valid (user overrides applied if present)',\n }\n}\n\n/** Every seam the three token checks touch \u2014 the store, the resolver, Doppler, and the file modes. */\nexport interface EnvTokenCheckDeps {\n /** Reads `tokens.json`. THROWS on a corrupt store (bad JSON / bad shape) \u2014 that throw is the diagnosis. */\n readStore?: () => Promise<TokenStore | null>\n /** Resolves one env's token (`INFRA_KIT_ENV_TOKEN` \u2192 store \u2192 throw). */\n resolveToken?: (env: string) => Promise<ResolvedEnvToken>\n /** Asks Doppler what a token actually is. Never throws; see {@link probeEnvToken}. */\n probe?: (args: { childEnv: NodeJS.ProcessEnv; project: string; config: string }) => Promise<EnvTokenProbe>\n storePath?: () => Promise<string>\n /** `null` = the path does not exist. */\n statPath?: (target: string) => fs.Stats | null\n chmodPath?: (target: string, mode: number) => void\n /**\n * Reads `INFRA_KIT_ENV_TOKEN` \u2014 the CI / agent channel. A SEAM rather than a direct `process.env`\n * read so the store checks are deterministic under test: a developer whose shell has already\n * auto-loaded an env would otherwise run the suite with the variable set and silently exercise the\n * skip branch instead of the one under assertion.\n */\n readEnvToken?: () => string | undefined\n}\n\n/** `tokens.json` holds live credentials: owner-only, and owner-only traversal to reach it. */\nconst TOKEN_FILE_MODE = 0o600\nconst TOKEN_DIR_MODE = 0o700\n\n/** One env's token as doctor sees it: WHERE it came from, never WHAT it is. */\ninterface EnvTokenEntry {\n env: string\n /** `null` = no token resolves for this env. */\n source: EnvTokenSource | null\n}\n\n/**\n * `env <name>: token (store)` / `env <name>: no token`, for every configured environment. The source\n * ONLY \u2014 a doctor line is printed to a terminal, pasted into a bug report, and returned over MCP, so\n * the one thing it can never carry is the token itself.\n *\n * @example\n * describeEntries([{ env: 'dev', source: 'store' }, { env: 'prod', source: null }])\n * // => 'dev: token (store), prod: no token'\n */\nconst describeEntries = (entries: EnvTokenEntry[]): string => {\n return entries\n .map((entry) => {\n const held = entry.source === null ? 'no token' : `token (${entry.source})`\n\n return `${entry.env}: ${held}`\n })\n .join(', ')\n}\n\n/** Resolve each env's token to a SOURCE, collapsing \"no token\" (a throw) into `source: null`. */\nconst resolveEntries = async (\n environments: string[],\n resolveToken: NonNullable<EnvTokenCheckDeps['resolveToken']>,\n): Promise<EnvTokenEntry[]> => {\n return Promise.all(\n environments.map(async (env): Promise<EnvTokenEntry> => {\n try {\n const { source } = await resolveToken(env)\n\n return { env, source }\n } catch {\n return { env, source: null }\n }\n }),\n )\n}\n\n/**\n * Which envs have a service token, and is the one that MATTERS among them?\n *\n * Under token-only auth the `envAutoLoad.config` env is load-bearing: no token there means the\n * developer's shell silently stops getting its environment, so that one is a FAIL. Every other env is\n * a listing line, not a verdict \u2014 a developer legitimately holds a `dev` token and no `prod` one, and\n * a doctor that failed on that would train everyone to ignore it.\n *\n * The gap it names is a store that holds tokens, just not the load-bearing one. A store that holds\n * NOTHING is {@link checkTokenStorePresent}'s failure and is deferred to it \u2014 see the branch below.\n *\n * @example\n * await checkEnvTokensConfigured({ config, error: null })\n * // => { name: 'env tokens configured', status: 'pass', message: 'auto-load env \"dev\": token (store). dev: token (store), prod: no token' }\n */\nexport const checkEnvTokensConfigured = async (\n read: DoctorConfig,\n deps: EnvTokenCheckDeps = {},\n): Promise<CheckResult> => {\n const name = 'env tokens configured'\n const readStore = deps.readStore ?? readTokenStore\n const resolveToken = deps.resolveToken ?? resolveEnvToken\n\n if (!read.config) {\n return { name, status: 'pass', message: 'Skipped \u2014 infra-kit config could not be read (see config check)' }\n }\n\n // A corrupt store throws for EVERY env, which would otherwise render as \"no token anywhere\" \u2014 the\n // same symptom as a fresh machine, with a completely different fix. Surface the store's own\n // (actionable) error instead, and let the remaining checks still run.\n //\n // There is no longer a \"store belongs to another repo\" case to report here: the store carries no\n // `repoRoot` (see `lib/env-tokens`), so a hand-written store loads and a basename collision between\n // two checkouts is an accepted risk, not a diagnosis. Do not add a branch back for it.\n let store: TokenStore | null\n\n try {\n store = await readStore()\n } catch (err) {\n return { name, status: 'fail', message: `Token store unreadable \u2014 ${(err as Error).message}` }\n }\n\n // The env universe is the union of what the workflows declare and what we hold tokens for \u2014 see\n // `lib/project-envs`. It replaces `config.environments`, which was a hand-maintained third copy.\n const envs = await listProjectEnvNames()\n const autoLoadEnv = read.config.envAutoLoad?.config\n\n // The auto-load env is load-bearing whether or not anything declares it: it is what the shell tries\n // to load. Probe it even when no workflow names it, so \"no token for it\" beats \"we never looked\".\n const universe = autoLoadEnv && !envs.includes(autoLoadEnv) ? [...envs, autoLoadEnv] : envs\n\n const entries = await resolveEntries(universe, resolveToken)\n const listing = describeEntries(entries)\n\n if (autoLoadEnv === undefined) {\n return { name, status: 'pass', message: `No envAutoLoad configured \u2014 no env is load-bearing. ${listing}` }\n }\n\n const autoLoadEntry = entries.find((entry) => {\n return entry.env === autoLoadEnv\n })\n\n if (!autoLoadEntry || autoLoadEntry.source === null) {\n // A store that holds NOTHING (absent, or hand-edited down to `{ envs: {} }`) is already the FAIL\n // of `tokens.json present`, with the same root cause and the same fix command \u2014 so this would be\n // the SECOND red line for one problem, on the most ordinary broken setup there is (a fresh\n // checkout that configures `envAutoLoad`). The gap this check exists to name is the narrower one:\n // a store that holds tokens, just not for the load-bearing env. Same deferral as `tokens.json\n // perms` below and `env token valid` above \u2014 one problem, one line.\n if (store === null || Object.keys(store.envs).length === 0) {\n return { name, status: 'pass', message: 'Skipped \u2014 there is no token store to read (see tokens.json present)' }\n }\n\n return {\n name,\n status: 'fail',\n message:\n `No Doppler service token for the auto-load env \"${autoLoadEnv}\" \u2014 your shell env will not load. ` +\n `Fix: run \\`infra-kit env-token-set ${autoLoadEnv}\\`. ${listing}`,\n }\n }\n\n return {\n name,\n status: 'pass',\n message: `auto-load env \"${autoLoadEnv}\": token (${autoLoadEntry.source}). ${listing}`,\n }\n}\n\n/** The verdict for each probe outcome. `unreachable` is a PASS \u2014 see {@link checkEnvTokenValid}. */\nconst PROBE_VERDICT: Record<EnvTokenProbe['outcome'], { status: CheckResult['status']; describe: string }> = {\n valid: { status: 'pass', describe: 'live and correctly scoped' },\n revoked: { status: 'fail', describe: 'REJECTED by Doppler (revoked or invalid)' },\n 'mis-scoped': { status: 'fail', describe: 'live but scoped to a DIFFERENT config' },\n unreachable: { status: 'pass', describe: 'could not be checked (Doppler unreachable)' },\n}\n\n/**\n * Is the auto-load env's token actually LIVE and correctly SCOPED? The only check here that leaves the\n * machine, and the only one that can tell a developer why their shell went quiet.\n *\n * An UNREACHABLE probe passes, deliberately: a network failure proves nothing about a token, and a\n * doctor that reported \"your token is revoked\" to a developer on a plane would be worse than one that\n * said nothing. The same `null`-means-couldn't-tell contract the Doppler listings always carried.\n *\n * @example\n * await checkEnvTokenValid({ config, error: null })\n * // => { name: 'env token valid', status: 'fail', message: 'The token for env \"dev\" is live but scoped to a DIFFERENT config \u2026' }\n */\nexport const checkEnvTokenValid = async (read: DoctorConfig, deps: EnvTokenCheckDeps = {}): Promise<CheckResult> => {\n const name = 'env token valid'\n const resolveToken = deps.resolveToken ?? resolveEnvToken\n const probe = deps.probe ?? probeEnvToken\n\n if (!read.config) {\n return { name, status: 'pass', message: 'Skipped \u2014 infra-kit config could not be read (see config check)' }\n }\n\n const env = read.config.envAutoLoad?.config\n\n if (env === undefined) {\n return { name, status: 'pass', message: 'Skipped \u2014 no envAutoLoad env to probe' }\n }\n\n let token: string\n\n try {\n token = (await resolveToken(env)).token\n } catch {\n // Already the FAIL of `env tokens configured`; repeating it here would double-count one problem.\n return { name, status: 'pass', message: `Skipped \u2014 no token for env \"${env}\" (see env tokens configured)` }\n }\n\n const project = read.config.envManagement.config.name\n const result = await probe({ childEnv: buildDopplerChildEnv(token), project, config: env })\n const verdict = PROBE_VERDICT[result.outcome]\n const suffix =\n result.outcome === 'revoked' || result.outcome === 'mis-scoped'\n ? ` Fix: run \\`infra-kit env-token-set ${env}\\` with a token scoped to \"${env}\".`\n : ''\n\n return {\n name,\n status: verdict.status,\n message: `The token for env \"${env}\" (project \"${project}\") is ${verdict.describe}.${suffix}`,\n }\n}\n\n/** The live `INFRA_KIT_ENV_TOKEN`, or `undefined`. An EMPTY value is a MISS \u2014 the resolver's rule. */\nconst defaultReadEnvToken = (): string | undefined => {\n return process.env[INFRA_KIT_ENV_TOKEN_VAR] || undefined\n}\n\n/**\n * Does this project HOLD any Doppler service token at all?\n *\n * The FIRST question in this section, and the only one that is repo-shaped rather than env-shaped.\n * Fails when `~/.infra-kit/projects/<repo>/tokens.json` is missing OR holds zero envs. Two states\n * are deliberately NOT failures: `INFRA_KIT_ENV_TOKEN` set (the CI / agent channel takes\n * precedence over the store and needs no home config \u2014 see {@link INFRA_KIT_ENV_TOKEN_VAR}), and a\n * store that THROWS (`env tokens configured` already reports that one).\n *\n * @example\n * await checkTokenStorePresent()\n * // => { name: 'tokens.json present', status: 'fail', message: 'No token store at ~/.infra-kit/projects/api/tokens.json \u2026' }\n */\n// Every project this CLI drives authenticates through that store, so a checkout without one cannot\n// load an env, deploy, or read a secret \u2014 whatever `envAutoLoad` happens to say. Before this check\n// existed that state was reported as THREE passes (`env tokens configured` passes outright when no\n// `envAutoLoad` is configured, and the other two skip), so a green report meant nothing on exactly\n// the machine that needed the report most.\n//\n// An EMPTY store fails alongside a missing one: `env-token-set` never writes `{ envs: {} }`, so a\n// store with no envs is a hand-edit or an `env-token-remove` of the last token \u2014 functionally the\n// same as no file, and it must not read as \"present, therefore fine\".\n//\n// Failing on `INFRA_KIT_ENV_TOKEN` would redden every CI run; repeating the throwing-store failure\n// here would double-count one problem.\nexport const checkTokenStorePresent = async (deps: EnvTokenCheckDeps = {}): Promise<CheckResult> => {\n const name = 'tokens.json present'\n const readStore = deps.readStore ?? readTokenStore\n const storePath = deps.storePath ?? getTokenStorePath\n const readEnvToken = deps.readEnvToken ?? defaultReadEnvToken\n\n if (readEnvToken()) {\n return {\n name,\n status: 'pass',\n message: `Skipped \u2014 ${INFRA_KIT_ENV_TOKEN_VAR} is set; that channel takes precedence over the store`,\n }\n }\n\n // Display only \u2014 every message below is read by a human, and nothing here touches the path again.\n let displayPath: string\n\n try {\n displayPath = tildify(await storePath())\n } catch {\n return { name, status: 'pass', message: 'Skipped \u2014 the token store path could not be resolved' }\n }\n\n let store: TokenStore | null\n\n try {\n store = await readStore()\n } catch {\n return { name, status: 'pass', message: `The store at ${displayPath} is unreadable (see env tokens configured)` }\n }\n\n const envs = store === null ? [] : Object.keys(store.envs)\n\n if (envs.length === 0) {\n const state = store === null ? `No token store at ${displayPath}` : `${displayPath} holds no tokens`\n\n return {\n name,\n status: 'fail',\n message:\n `${state} \u2014 every project needs one to authenticate. ` +\n `Fix: run \\`infra-kit env-token-set <env>\\`. CI / agents: set ${INFRA_KIT_ENV_TOKEN_VAR} instead.`,\n }\n }\n\n return { name, status: 'pass', message: `${displayPath} holds ${envs.length} token(s): ${envs.join(', ')}` }\n}\n\n/** `-rw-------` / `drwx------` \u2014 the permission bits of a path, or `null` when it does not exist. */\nconst modeOf = (target: string, statPath: NonNullable<EnvTokenCheckDeps['statPath']>): number | null => {\n const stats = statPath(target)\n\n return stats === null ? null : stats.mode & 0o777\n}\n\n/**\n * The token store is a credential file, so it must be 0600, reachable only through 0700 directories.\n * `writeTokenStore` sets all four every time it writes \u2014 so a violation here means something ELSE\n * touched them (an editor's save-and-rename, a `cp -r`, a synced dotfiles dir), which is exactly the\n * case a self-healing writer cannot fix on its own.\n *\n * Skipped when the store does not exist: a machine with no tokens has nothing to protect, and CI\n * (`INFRA_KIT_ENV_TOKEN`) never writes one.\n *\n * @example\n * await checkTokenStorePerms(false)\n * // => { name: 'tokens.json perms', status: 'fail', message: '~/.infra-kit/projects/api/tokens.json is 0644, expected 0600 \u2026' }\n */\nexport const checkTokenStorePerms = async (fix: boolean, deps: EnvTokenCheckDeps = {}): Promise<CheckResult> => {\n const name = 'tokens.json perms'\n const storePath = deps.storePath ?? getTokenStorePath\n const statPath = deps.statPath ?? defaultStatPath\n const chmodPath = deps.chmodPath ?? fs.chmodSync\n\n let target: string\n\n try {\n target = await storePath()\n } catch {\n return { name, status: 'pass', message: 'Skipped \u2014 the token store path could not be resolved' }\n }\n\n if (modeOf(target, statPath) === null) {\n return { name, status: 'pass', message: `No store to protect at ${tildify(target)} (see tokens.json present)` }\n }\n\n // Every directory on the way to the file, plus the file: a 0600 file inside a world-readable\n // directory chain is still a credential a `find` away.\n const projectDir = path.dirname(target)\n const projectsDir = path.dirname(projectDir)\n const userConfigDir = path.dirname(projectsDir)\n const audited: Array<{ target: string; expected: number; actual: number | null }> = [\n { target: userConfigDir, expected: TOKEN_DIR_MODE, actual: modeOf(userConfigDir, statPath) },\n { target: projectsDir, expected: TOKEN_DIR_MODE, actual: modeOf(projectsDir, statPath) },\n { target: projectDir, expected: TOKEN_DIR_MODE, actual: modeOf(projectDir, statPath) },\n { target, expected: TOKEN_FILE_MODE, actual: modeOf(target, statPath) },\n ]\n const loose = audited.filter((entry) => {\n return entry.actual !== null && entry.actual !== entry.expected\n })\n\n if (loose.length === 0) {\n return { name, status: 'pass', message: `${tildify(target)} is 0600 (0700 dirs)` }\n }\n\n const describe = loose\n .map((entry) => {\n return `${tildify(entry.target)} is ${formatMode(entry.actual!)}, expected ${formatMode(entry.expected)}`\n })\n .join('; ')\n\n if (!fix) {\n return {\n name,\n status: 'fail',\n message: `${describe}. Fix: run \\`infra-kit doctor --fix\\`, or chmod them by hand.`,\n }\n }\n\n for (const entry of loose) {\n chmodPath(entry.target, entry.expected)\n }\n\n return { name, status: 'pass', message: `Tightened ${loose.length} path(s): ${describe}` }\n}\n\n/** `0600`, `0755` \u2014 a mode's permission bits in the form a user types at `chmod`. */\nconst formatMode = (mode: number): string => {\n return `0${mode.toString(8).padStart(3, '0')}`\n}\n\n/** `fs.statSync` with \"does not exist\" as a value rather than a throw. */\nconst defaultStatPath = (target: string): fs.Stats | null => {\n try {\n return fs.statSync(target)\n } catch {\n return null\n }\n}\n\n/**\n * Surface where this developer's user-scope override file lives and what it CONTAINS. Always passes\n * (informational) inside a git repo; fails only when the config paths can't be resolved at all.\n *\n * Three-way on purpose. Every non-excluded command now seeds layer 3 in its preAction hook \u2014 and\n * `doctor` is one of them, so by the time this check runs the file should already exist. An ABSENT\n * file therefore no longer means \"not yet created\", it means the seed FAILED (read-only `$HOME`, a\n * sandbox, a locked-down CI image). That failure path is `logger.debug` only, so this line is the\n * ONLY place a permanently-failing seed becomes visible \u2014 it must not be confusable with a healthy\n * empty file. Content reporting is shared with `config path` via `readOverrideSummary` /\n * `describeOverrides` (one JSON-tolerance policy: raw keys, malformed degrades, never throws).\n *\n * @example\n * await checkUserOverridePath()\n * // { name: 'user override path', status: 'pass',\n * // message: '~/.infra-kit/projects/api/infra-kit.json (2 override(s): ide, dev) \u2014 project: api' }\n * // absent -> '\u2026 (not created \u2014 seed failed?) \u2014 project: api'\n * // empty -> '\u2026 (empty \u2014 no overrides) \u2014 project: api'\n * // corrupt -> '\u2026 (unreadable \u2014 invalid JSON) \u2014 project: api'\n */\nexport const checkUserOverridePath = async (): Promise<CheckResult> => {\n const name = 'user override path'\n\n try {\n const paths = await getInfraKitConfigPaths()\n const exists = fs.existsSync(paths.userProject)\n const summary = await readOverrideSummary(paths.userProject, exists)\n const suffix = exists ? describeOverrides(summary) : '(not created \u2014 seed failed?)'\n\n return {\n name,\n status: 'pass',\n message: `${tildify(paths.userProject)} ${suffix} \u2014 project: ${paths.projectName}`,\n }\n } catch (err) {\n return { name, status: 'fail', message: (err as Error).message }\n }\n}\n\n/**\n * Surface a lingering legacy `~/.infra-kit/config.json` left behind after the\n * user-global config was renamed to `infra-kit.json`. The on-`setup` auto-rename\n * is the migration path, but a user who edits the file and never re-runs `setup`\n * would silently have their overrides stop applying \u2014 this makes that visible.\n *\n * Branches on whether the canonical `infra-kit.json` already exists so the\n * message is accurate: only the \"no infra-kit.json yet\" case means overrides are\n * not being applied; when both exist the legacy file is merely stale.\n * Informational pass (no warning) when the paths can't resolve \u2014 mirrors\n * {@link checkIdeInstalled}: nothing to surface if there's no resolvable target.\n *\n * @example\n * await checkLegacyUserGlobalConfig()\n * // { name: 'legacy user-global config', status: 'fail', message: 'Legacy user-global config.json found \u2026' }\n */\nexport const checkLegacyUserGlobalConfig = async (): Promise<CheckResult> => {\n const name = 'legacy user-global config'\n\n let paths: Awaited<ReturnType<typeof getInfraKitConfigPaths>>\n\n try {\n paths = await getInfraKitConfigPaths()\n } catch {\n return { name, status: 'pass', message: 'Skipped \u2014 infra-kit config paths could not be resolved' }\n }\n\n const legacyPath = path.join(path.dirname(paths.userGlobal), 'config.json')\n\n if (!fs.existsSync(legacyPath)) {\n return { name, status: 'pass', message: 'No legacy user-global config.json' }\n }\n\n if (!fs.existsSync(paths.userGlobal)) {\n return {\n name,\n status: 'fail',\n message: `Legacy user-global config.json found at ${tildify(legacyPath)} \u2014 run \\`infra-kit setup --skip-tools\\` to migrate it (your overrides are not being applied).`,\n }\n }\n\n return {\n name,\n status: 'fail',\n message: `Stale legacy config.json found at ${tildify(legacyPath)} \u2014 ${tildify(paths.userGlobal)} is active; remove the old file.`,\n }\n}\n\ninterface IdeProbe {\n ok: boolean\n label: string\n failMsg: string\n}\n\nconst IDE_PROBE_META: Record<'cursor' | 'zed', { command: string[]; label: string; failMsg: string }> = {\n cursor: {\n command: ['cursor', '--version'],\n label: 'Cursor',\n failMsg: 'Cursor is not installed. Install from: https://cursor.com/',\n },\n zed: {\n command: ['zed', '--version'],\n label: 'Zed',\n failMsg: 'Zed is not installed. Install from: https://zed.dev/',\n },\n}\n\nconst probeIde = async (provider: 'cursor' | 'zed'): Promise<IdeProbe> => {\n const meta = IDE_PROBE_META[provider]\n\n try {\n await $`${meta.command}`\n\n return { ok: true, label: meta.label, failMsg: meta.failMsg }\n } catch {\n return { ok: false, label: meta.label, failMsg: meta.failMsg }\n }\n}\n\n/**\n * Check that every editor configured under `ide` is installed. Probes each binary (`cursor`/`zed`)\n * named by the ALREADY-READ config ({@link readDoctorConfig}). Passes only if all configured editors\n * are present; fails listing any that are missing. Informational pass when no IDE is configured or\n * the config can't be read \u2014 an unconfigured editor is a valid setup, and config validity is\n * reported separately by `checkInfraKitConfigValid`.\n *\n * @example\n * await checkIdeInstalled({ config: { ide: [{ provider: 'cursor' }] }, error: null })\n * // => { name: 'ide installed', status: 'pass', message: 'Installed: Cursor' }\n */\nexport const checkIdeInstalled = async (read: DoctorConfig): Promise<CheckResult> => {\n const name = 'ide installed'\n\n if (!read.config) {\n return { name, status: 'pass', message: 'Skipped \u2014 infra-kit config could not be read (see config check)' }\n }\n\n const providers = resolveConfiguredIdes(read.config).map((ide) => {\n return ide.provider\n })\n\n if (providers.length === 0) {\n return { name, status: 'pass', message: 'No IDE configured (ide unset)' }\n }\n\n const probes = await Promise.all(\n providers.map((provider) => {\n return probeIde(provider)\n }),\n )\n const missing = probes.filter((probe) => {\n return !probe.ok\n })\n\n if (missing.length === 0) {\n return {\n name,\n status: 'pass',\n message: `Installed: ${probes\n .map((probe) => {\n return probe.label\n })\n .join(', ')}`,\n }\n }\n\n return {\n name,\n status: 'fail',\n message: missing\n .map((probe) => {\n return probe.failMsg\n })\n .join('; '),\n }\n}\n\n/** A plain `major.minor.patch` version \u2014 the only shape the semver comparator below can order. */\nconst PLAIN_SEMVER = /^\\d+\\.\\d+\\.\\d+$/\n\n/**\n * True when `candidate` is a strictly lower version than `current`.\n *\n * Ordering is delegated to {@link sortVersions}, which is `v`-prefixed and SemVer-only, so both\n * sides are prefixed for the call and anything that is not a bare `major.minor.patch` is reported\n * as NOT older: a block written by a prerelease or otherwise unparseable build must never be\n * counted as stale on the strength of a `NaN` comparison.\n */\nconst isOlderVersion = (candidate: string, current: string): boolean => {\n if (candidate === current) return false\n if (!PLAIN_SEMVER.test(candidate) || !PLAIN_SEMVER.test(current)) return false\n\n return sortVersions([`v${candidate}`, `v${current}`])[0] === `v${candidate}`\n}\n\n/**\n * Versions recorded by well-formed package guidance blocks that predate the running CLI.\n *\n * `discoverPackages` is wrapped because it reads `pnpm-workspace.yaml` with an unguarded\n * `readFile` + `yaml.parse`: a single-package repo (and the doctor inventory fixture, a temp\n * directory holding only `infra-kit.json` and `CLAUDE.md`) has no such file, and an ENOENT here\n * would propagate out of `doctor()` and crash the command. Any throw degrades to \"no packages\".\n * Read-only by construction \u2014 nothing on this path writes.\n */\nconst collectStalePackageVersions = async (root: string, current: string): Promise<string[]> => {\n let packageDirs: string[]\n\n try {\n packageDirs = await discoverPackages(root)\n } catch {\n return []\n }\n\n const stale: string[] = []\n\n for (const dir of packageDirs) {\n const inspection = inspectPackageGuidance(readGuidanceFile(path.join(dir, 'CLAUDE.md')))\n\n if (inspection.state !== 'ok') continue\n if (inspection.version === undefined) continue\n if (isOlderVersion(inspection.version, current)) stale.push(inspection.version)\n }\n\n return stale\n}\n\n/**\n * The staleness sentence appended to the `CLAUDE.md block` message, or `''` when nothing is behind.\n *\n * Deliberately a message-only dimension: `CheckResult.status` is `'pass' | 'fail'` with no warn\n * state, and drift in per-package blocks is not a broken machine, so this never changes the\n * check's status.\n */\nconst packageGuidanceStaleness = async (root: string, current: string): Promise<string> => {\n const stale = await collectStalePackageVersions(root, current)\n\n if (stale.length === 0) return ''\n\n const oldest = sortVersions(\n stale.map((version) => {\n return `v${version}`\n }),\n )[0]!.slice(1)\n\n return ` \u2014 ${stale.length} package guidance blocks were generated by an older infra-kit (oldest ${oldest}, current ${current}) \u2014 run: infra-kit audit --fix --all`\n}\n\n/**\n * Check that the repo agent-instruction guidance managed by `infra-kit setup` exists:\n * the guidance block in `CLAUDE.md`. Presence only. Repo-gated: returns no checks\n * when run outside an infra-kit repo so doctor never crashes there. A repo that\n * predates the AGENTS.md\u2192CLAUDE.md migration will report this check as failing until\n * `infra-kit setup` is re-run.\n *\n * The message also carries a read-only per-package staleness report (see\n * {@link packageGuidanceStaleness}). It rides on THIS check rather than a new one on purpose:\n * a new check name would have to be added to `SECTION_MEMBERS` in `report.ts` and to the\n * inventory fixture's counts, and `'CLAUDE.md block'` is already sectioned.\n */\nexport const checkAgentFiles = async (): Promise<CheckResult[]> => {\n let mainConfigPath: string\n\n try {\n mainConfigPath = (await getInfraKitConfigPaths()).main\n } catch {\n return []\n }\n\n if (!fs.existsSync(mainConfigPath)) return []\n\n const root = path.dirname(mainConfigPath)\n const claudePath = path.join(root, 'CLAUDE.md')\n const content = fs.existsSync(claudePath) ? fs.readFileSync(claudePath, 'utf-8') : ''\n const present = hasManagedBlock(content, AGENTS_MARKER_START, AGENTS_MARKER_END)\n const message = present\n ? 'CLAUDE.md block present'\n : 'infra-kit block missing from CLAUDE.md. Run: infra-kit setup --skip-tools'\n const staleness = await packageGuidanceStaleness(root, packageJson.version)\n\n return [\n {\n name: 'CLAUDE.md block',\n status: present ? 'pass' : 'fail',\n message: `${message}${staleness}`,\n },\n ]\n}\n\n/**\n * The repo root the host-state checks below are asked about, or `null` outside an infra-kit repo.\n * Same gate as {@link checkAgentFiles}: the presence of `infra-kit.json` at the root.\n */\nconst resolveCheckedRepoRoot = async (): Promise<string | null> => {\n try {\n const mainConfigPath = (await getInfraKitConfigPaths()).main\n\n return fs.existsSync(mainConfigPath) ? path.dirname(mainConfigPath) : null\n } catch {\n return null\n }\n}\n\n/** The version row, reported only for an install that covers THIS project. */\nconst claudePluginVersionCheck = (state: PluginInstallState): CheckResult => {\n const version = state.kind === 'installed' ? readInstalledPluginVersion(state.installation) : null\n\n return {\n name: 'plugin version',\n status: version === null ? 'fail' : 'pass',\n message:\n version === null\n ? 'No infra-kit plugin installed for this project to read a version from'\n : `Plugin ${PLUGIN_KEY} version ${version}`,\n }\n}\n\n/** How many other projects the `elsewhere` message names before it summarises the rest as a count. */\nconst MAX_LISTED_PROJECTS = 3\n\n/** The install row. `elsewhere` names the projects the existing records DO cover, which is the fix. */\nconst claudePluginInstalledCheck = (state: PluginInstallState): CheckResult => {\n const name = 'plugin installed'\n\n if (state.kind === 'installed') {\n const { scope, projectPath } = state.installation\n const where = projectPath === null ? '' : `, ${tildify(projectPath)}`\n\n return { name, status: 'pass', message: `Plugin ${PLUGIN_KEY} installed (${scope ?? 'unknown'} scope${where})` }\n }\n\n if (state.kind === 'absent') {\n return { name, status: 'fail', message: `Plugin ${PLUGIN_KEY} is not installed. Run: ${PLUGIN_INSTALL_COMMAND}` }\n }\n\n // Capped at three: a machine that has rolled this out to every repo carries a dozen records, and a\n // row naming all of them buries the one thing the reader needs \u2014 that none of them is this project.\n const paths = state.installations.map((entry) => {\n return entry.projectPath === null ? '(unnamed project)' : tildify(entry.projectPath)\n })\n const extra = paths.length - MAX_LISTED_PROJECTS\n const suffix = extra > 0 ? ` and ${extra} more` : ''\n const others = `${paths.slice(0, MAX_LISTED_PROJECTS).join(', ')}${suffix}`\n\n return {\n name,\n status: 'fail',\n message: `Plugin ${PLUGIN_KEY} is installed for ${others} only, not this project. Run: ${PLUGIN_INSTALL_COMMAND}`,\n }\n}\n\n/**\n * Is the `claude` binary on PATH? The PREREQUISITE row for everything below it: `infra-kit setup`\n * installs the plugin by driving `claude plugin install`, so on a machine without that binary the\n * three rows that follow are failing for a reason none of their own messages names.\n *\n * Deliberately NOT wired into the exit code \u2014 only `plugin installed` is. A machine that has chosen\n * not to install Claude Code is reporting a fact, not a broken setup.\n *\n * @example\n * await checkClaudeCli() // => { name: 'claude CLI', status: 'pass', message: 'Installed: claude' }\n */\nexport const checkClaudeCli = (): Promise<CheckResult> => {\n return checkCommand(\n 'claude CLI',\n ['claude', '--version'],\n 'Installed: claude',\n 'claude CLI not found on PATH \u2014 `infra-kit setup` cannot install the plugin; install Claude Code first',\n )\n}\n\n/**\n * The four host-state rows about the `infra-kit` Claude Code plugin: is its marketplace registered\n * on this machine, is the plugin installed, which version, and which CLI is reporting it.\n *\n * All four are emitted whatever `root` is, so the row set never changes shape between directories.\n * `root` narrows the VERDICT only: it is the project a project-scope install has to name to count.\n *\n * @example\n * checkClaudePlugin('/repo')\n * // => [{ name: 'marketplace registered', \u2026 }, { name: 'plugin installed', \u2026 }, \u2026 ]\n */\nexport const checkClaudePlugin = (root: string | null): CheckResult[] => {\n const state = resolvePluginInstall(root === null ? {} : { projectPath: root })\n const registered = isMarketplaceRegistered()\n\n return [\n {\n name: 'marketplace registered',\n status: registered ? 'pass' : 'fail',\n message: registered\n ? `Marketplace ${MARKETPLACE_NAME} is known to Claude Code`\n : `Marketplace ${MARKETPLACE_NAME} is not registered. Run: claude plugin marketplace add ${MARKETPLACE_REPO}`,\n },\n claudePluginInstalledCheck(state),\n claudePluginVersionCheck(state),\n { name: 'CLI version', status: 'pass', message: `infra-kit CLI ${packageJson.version}` },\n ]\n}\n\n/** One message per `.mcp.json` verdict; `wrong-key` is built by the caller, which has the key. */\nconst MCP_MESSAGES: Record<Exclude<McpRegistration['kind'], 'wrong-key'>, string> = {\n ok: `.mcp.json registers the server as \"${MARKETPLACE_NAME}\" \u2014 plugin skills resolve mcp__${MARKETPLACE_NAME}__* tools`,\n 'missing-file':\n 'No .mcp.json at the repo root yet, so there is no server key to check. Run: infra-kit setup --skip-tools',\n unparseable: 'Could not read mcpServers from .mcp.json \u2014 fix the JSON and re-run',\n absent: `.mcp.json has no \"${MARKETPLACE_NAME}\" server. Plugin skills name mcp__${MARKETPLACE_NAME}__* tools and will resolve nothing without it`,\n}\n\n/**\n * The verdicts that do NOT fail the row: a correct registration, and a repo with no `.mcp.json` at\n * all.\n *\n * The second is no longer an abstention. `setup` now always writes the file, so \u2014 the row being gated\n * on the writer's own predicate \u2014 `missing-file` means one thing only: `setup` has never run here (or\n * ran on a CLI predating the writer). It stays a `pass` because two rows already fail in that exact\n * state with `infra-kit setup` as their fix (`zshrc init block`, `CLAUDE.md block`); a third would\n * repeat them and change nothing, since exit 1 is scoped to `plugin installed` alone.\n *\n * A file that IS present and does not name the server still fails: that is a misconfiguration.\n */\nconst MCP_NON_FAILING: ReadonlySet<McpRegistration['kind']> = new Set(['ok', 'missing-file'])\n\n/**\n * T4(b) \u2014 the live half of the tool-prefix guard, run IN the consumer repo against its own\n * `.mcp.json` rather than against a fixture copy of it committed elsewhere.\n *\n * @example\n * checkMcpServerKey('/repo') // => { name: 'MCP server key', status: 'pass', \u2026 }\n */\nexport const checkMcpServerKey = (root: string): CheckResult => {\n const name = 'MCP server key'\n const registration = inspectMcpRegistration(root)\n\n if (registration.kind === 'wrong-key') {\n return {\n name,\n status: 'fail',\n message: `.mcp.json registers the infra-kit server under \"${registration.key}\". Rename the key to \"${MARKETPLACE_NAME}\": tool names are mcp__<key>__<tool>, and every plugin skill expects the mcp__${MARKETPLACE_NAME}__ prefix`,\n }\n }\n\n return {\n name,\n status: MCP_NON_FAILING.has(registration.kind) ? 'pass' : 'fail',\n message: MCP_MESSAGES[registration.kind],\n }\n}\n\n/**\n * The one-time, out-of-band, ROOT command that installs the `:443` daemon. infra-kit never runs it \u2014 it\n * only ever prints it (principle 3: infra-kit probes and prints, it never elevates).\n *\n * Rendered from the resolved `dist/cli.js`, never as a bare `portless`: portless is an npm dependency in\n * `node_modules`, not a global bin, and `sudo` swaps `PATH` for `secure_path` \u2014 so the bare form cannot\n * resolve under sudo even in a shell where it otherwise would. See {@link formatPortlessCommand}.\n */\nconst installDaemonCmd = (bin: string): string => {\n return formatPortlessCommand(['service', 'install'], { sudo: true, bin })\n}\n\n/** The sudo-free \"trust the local CA\" command, rendered from the same resolved bin. */\nconst trustCmd = (bin: string): string => {\n return formatPortlessCommand(['trust'], { bin })\n}\n\n/** TLS chain failures that mean \"the daemon's cert does not chain to the `ca.pem` we hold\". */\nconst CA_MISMATCH_CODES = new Set(['SELF_SIGNED_CERT_IN_CHAIN', 'UNABLE_TO_VERIFY_LEAF_SIGNATURE'])\n\n/**\n * A validating probe of an IP literal sends no SNI and lands on portless's default cert, which has no IP\n * SAN \u2014 so this code means OUR probe forgot a `servername`, never that the user's CA went bad. It must\n * never reach a `portless trust` remediation: doing so would tell every correctly-configured user to\n * repair a trust store that is fine.\n */\nconst PROBE_BUG_CODE = 'ERR_TLS_CERT_ALTNAME_INVALID'\n\n/** Every process/fs seam the portless checks touch, injected so tests never reach the real daemon. */\nexport interface PortlessCheckDeps {\n resolveBin?: () => string | null\n isProxyServing?: (port: number, tls: boolean) => Promise<boolean>\n handshake?: (port: number, servername: string) => Promise<HandshakeResult>\n caTrusted?: () => boolean\n routes?: () => PortlessRoute[]\n isListening?: (port: number) => Promise<boolean>\n caPath?: () => string\n}\n\n/** Is the daemon serving portless-over-TLS on the dev proxy port? Proven on the wire; reads no state file. */\nconst checkPortlessServing = async (\n isProxyServing: NonNullable<PortlessCheckDeps['isProxyServing']>,\n bin: string,\n): Promise<CheckResult> => {\n const name = `portless serving TLS on :${DEFAULT_DEV_PROXY_PORT}`\n const serving = await isProxyServing(DEFAULT_DEV_PROXY_PORT, true)\n\n if (!serving) {\n return {\n name,\n status: 'fail',\n message: `No portless daemon is serving HTTPS on :${DEFAULT_DEV_PROXY_PORT}. Install it once (needs root): \\`${installDaemonCmd(bin)}\\`.`,\n }\n }\n\n return { name, status: 'pass', message: `portless is serving HTTPS on :${DEFAULT_DEV_PROXY_PORT}` }\n}\n\n/**\n * Does the daemon serve a certificate that chains to the `ca.pem` on disk? The only check that VALIDATES\n * the chain \u2014 the serving probe above deliberately does not, so that a down daemon and an untrusted CA stay\n * two distinguishable failures with two different fixes.\n *\n * Probes `localhost` (always in the default cert's SANs, so it works on a fresh machine with zero aliases)\n * and, when one is registered, a real alias \u2014 which validates the exact path the browser takes.\n */\nconst checkPortlessCaChain = async (\n handshake: NonNullable<PortlessCheckDeps['handshake']>,\n routes: PortlessRoute[],\n caPath: string,\n bin: string,\n): Promise<CheckResult> => {\n const name = 'portless CA chain valid'\n const servernames = [\n 'localhost',\n ...routes.slice(0, 1).map((route) => {\n return route.name\n }),\n ]\n const results = await Promise.all(\n servernames.map(async (servername) => {\n return { servername, result: await handshake(DEFAULT_DEV_PROXY_PORT, servername) }\n }),\n )\n const failed = results.filter((entry) => {\n return !entry.result.ok\n })\n\n if (failed.length === 0) {\n return {\n name,\n status: 'pass',\n message: `Served certificate chains to ${tildify(caPath)} (${servernames.join(', ')})`,\n }\n }\n\n const codes = failed.map((entry) => {\n return entry.result.ok ? '' : entry.result.code\n })\n\n if (codes.includes(PROBE_BUG_CODE)) {\n return {\n name,\n status: 'fail',\n message: `Internal check error: the TLS probe got ${PROBE_BUG_CODE} for servername(s) ${failed\n .map((entry) => {\n return entry.servername\n })\n .join(', ')} \u2014 doctor's probe is wrong, your trust store is not implicated. Please report this.`,\n }\n }\n\n if (\n codes.some((code) => {\n return CA_MISMATCH_CODES.has(code)\n })\n ) {\n return {\n name,\n status: 'fail',\n message: `The daemon is serving a certificate that does not chain to ${tildify(caPath)}. Its CA was regenerated \u2014 run \\`${trustCmd(bin)}\\`, or reinstall: \\`${installDaemonCmd(bin)}\\`.`,\n }\n }\n\n return { name, status: 'fail', message: `TLS handshake failed (${codes.join(', ')})` }\n}\n\n/**\n * Was `portless trust` run for the CA now on disk? Fingerprint marker only \u2014 it proves the marker was\n * written for THIS `ca.pem` and the CA has not been regenerated since. It does NOT prove the keychain still\n * trusts it: a user who deletes the certificate by hand in Keychain Access leaves the marker behind and\n * passes here. Accepted residual (see `caFingerprintMatches`); the chain check above is what proves what the\n * daemon actually serves.\n */\nconst checkPortlessCaTrusted = (trusted: boolean, caPath: string, bin: string): CheckResult => {\n const name = 'portless CA trusted'\n\n if (!trusted) {\n return {\n name,\n status: 'fail',\n message: `The portless CA is not trusted (or was regenerated). Run: \\`${trustCmd(bin)}\\` (no sudo needed).`,\n }\n }\n\n return { name, status: 'pass', message: `${tildify(caPath)} matches the recorded ca.trusted fingerprint` }\n}\n\n/**\n * Routes whose target port has nothing listening \u2014 a worktree whose runner was SIGKILLed. Newly worth\n * surfacing under HTTPS: a dead target is now an opaque 502 from the proxy rather than a connection refusal.\n */\nconst checkPortlessStaleRoutes = async (\n routes: PortlessRoute[],\n isListening: NonNullable<PortlessCheckDeps['isListening']>,\n bin: string,\n): Promise<CheckResult> => {\n const name = 'portless routes'\n\n if (routes.length === 0) return { name, status: 'pass', message: 'No portless routes registered' }\n\n const probed = await Promise.all(\n routes.map(async (route) => {\n return { route, live: await isListening(route.port) }\n }),\n )\n const stale = probed\n .filter((entry) => {\n return !entry.live\n })\n .map((entry) => {\n return entry.route.name\n })\n\n if (stale.length === 0) {\n return { name, status: 'pass', message: `${routes.length} portless route(s), all live` }\n }\n\n return {\n name,\n status: 'fail',\n message: `${stale.length} stale portless route(s): ${stale.join(', ')}. Remove with \\`${formatPortlessCommand(['alias', '--remove', '<name>'], { bin })}\\`.`,\n }\n}\n\n/**\n * The portless block of doctor: is the HTTPS dev proxy installed, serving, trusted, and free of dead routes?\n * This is the diagnostic surface for the port-free HTTPS dev URLs \u2014 it is what tells a developer to run\n * `trust` (sudo-free) or the one-time `service install` (root), and it never conflates the two. Both are\n * printed via {@link formatPortlessCommand} \u2014 as `<node> <cli.js> \u2026`, the only form that runs.\n *\n * Every check observes the daemon **on the wire or on our own disk** \u2014 never through portless's `proxy.port`\n * / `proxy.pid` / `proxy.tls` markers, which are process-global singletons that ANY daemon start rewrites and\n * ANY daemon stop deletes. Gating on them made doctor report a perfectly healthy `:443` daemon as dead.\n *\n * Reports only: nothing here is auto-run, and nothing requiring sudo ever could be.\n *\n * @example\n * await checkPortless()\n * // [{ name: 'portless installed', status: 'pass', message: '\u2026' }, \u2026 5 checks]\n */\nexport const checkPortless = async (deps: PortlessCheckDeps = {}): Promise<CheckResult[]> => {\n const resolveBin = deps.resolveBin ?? resolvePortlessBin\n const isProxyServing = deps.isProxyServing ?? defaultIsProxyServing\n const handshake = deps.handshake ?? handshakeChainsToCa\n const caTrusted = deps.caTrusted ?? caFingerprintMatches\n const readRoutes = deps.routes ?? listRoutes\n const isListening = deps.isListening ?? defaultIsListening\n const caPath = (deps.caPath ?? readCaPath)()\n // Resolved ONCE, then threaded into every remediation below: each one is rendered as `<node> <bin> \u2026`, so a\n // check that could not name the binary could not print a runnable fix either.\n const bin = resolveBin()\n\n if (bin === null) {\n return [\n {\n name: 'portless installed',\n status: 'fail',\n message: 'portless is not resolvable from node_modules \u2014 run `pnpm install`.',\n },\n ]\n }\n\n const installed: CheckResult = {\n name: 'portless installed',\n status: 'pass',\n message: 'portless is resolvable from node_modules',\n }\n const routes = readRoutes()\n const serving = await checkPortlessServing(isProxyServing, bin)\n // Nothing is answering on :443 \u2014 there is no certificate to validate, so the chain check would only add a\n // second, derivative failure to the one the user must fix first.\n const chain: CheckResult =\n serving.status === 'fail'\n ? { name: 'portless CA chain valid', status: 'pass', message: 'Skipped \u2014 no daemon to handshake with' }\n : await checkPortlessCaChain(handshake, routes, caPath, bin)\n\n return [\n installed,\n serving,\n chain,\n checkPortlessCaTrusted(caTrusted(), caPath, bin),\n await checkPortlessStaleRoutes(routes, isListening, bin),\n ]\n}\n\n/** Seams for {@link pruneStalePortlessRoutes}: the daemon, the wire and the process table, all injectable. */\nexport interface PruneRoutesDeps {\n routes?: () => PortlessRoute[]\n isListening?: (port: number) => Promise<boolean>\n /** Is a dev session alive? Defaults to a `ps` scan \u2014 see {@link isDevSessionRunning}. */\n devSessionRunning?: () => boolean\n removeAlias?: (name: string) => Promise<void>\n}\n\n/**\n * `doctor --fix`: remove the portless routes a dead dev-server left behind.\n *\n * The counterpart to {@link checkPortlessStaleRoutes}, which only ever REPORTS. Acting on the same\n * evidence needs one more guard, and it is not optional: \"nothing is listening\" cannot distinguish a dead\n * route from a UI whose vite has not bound its port yet (`assignUiPort` registers the alias, then\n * `getFreePort` RELEASES the port until vite boots \u2014 and a UI alias has no dev-context fragment to\n * corroborate it with). Every such false positive requires a dev session to be running, so a live dev\n * session withholds the prune entirely. See `prune-routes.ts` for the full argument.\n */\nexport const pruneStalePortlessRoutes = async (deps: PruneRoutesDeps = {}): Promise<CheckResult> => {\n const name = 'portless routes'\n const readRoutes = deps.routes ?? listRoutes\n const isListening = deps.isListening ?? defaultIsListening\n const devRunning = deps.devSessionRunning ?? isDevSessionRunning\n const routes = readRoutes()\n\n if (routes.length === 0) return { name, status: 'pass', message: 'No portless routes registered' }\n\n const probed = await Promise.all(\n routes.map(async (route) => {\n return { name: route.name, port: route.port, live: await isListening(route.port) }\n }),\n )\n const { prunable, withheld } = decidePrune(probed, devRunning())\n\n if (withheld.length > 0) {\n return {\n name,\n status: 'fail',\n message:\n `${withheld.length} route(s) look stale (${withheld.join(', ')}), but a dev session is running \u2014 ` +\n 'a UI that has not bound its port yet is indistinguishable from a dead one, so nothing was removed. ' +\n 'Stop dev and re-run `infra-kit doctor --fix`.',\n }\n }\n\n if (prunable.length === 0) {\n return { name, status: 'pass', message: `${routes.length} portless route(s), all live` }\n }\n\n const removeAlias = deps.removeAlias ?? createPortlessDriver().removeAlias\n\n // Serial, not `Promise.all`: each removal shells out to portless, which takes its own route lock.\n for (const alias of prunable) {\n await removeAlias(alias)\n }\n\n return {\n name,\n status: 'pass',\n message: `Removed ${prunable.length} stale portless route(s): ${prunable.join(', ')}`,\n }\n}\n\n/**\n * Check installation and authentication status of gh, doppler, and aws CLIs\n */\nexport const doctor = async (options: { fix?: boolean; probeDeps?: ProbeDeps } = {}) => {\n // ONE read, before anything is dispatched: the checks below used to reset the shared config cache\n // concurrently from inside the `Promise.all`. See `readDoctorConfig`.\n const read = await readDoctorConfig()\n // Started here and awaited inside `withDetail`, so the probe runs ALONGSIDE the ~95 checks rather\n // than in front of them.\n const details = probeDependencyDetails(options.probeDeps ?? defaultProbeDeps())\n const withDetail = async (id: DependencyId, check: Promise<CheckResult>): Promise<CheckResult> => {\n const [result, detail] = await Promise.all([check, details])\n const found = detail.get(id)\n\n return found === undefined ? result : { ...result, detail: found }\n }\n\n const baseChecks: CheckResult[] = await Promise.all([\n withDetail(\n 'brew',\n checkCommand(\n 'brew installed',\n specFor('brew').probeArgv,\n 'Homebrew is installed',\n 'Homebrew is not installed. Install from: https://brew.sh/',\n ),\n ),\n withDetail(\n 'gh',\n checkCommand(\n 'gh installed',\n specFor('gh').probeArgv,\n 'GitHub CLI is installed',\n 'GitHub CLI is not installed. Install from: https://cli.github.com/',\n ),\n ),\n checkCommand(\n 'gh authenticated',\n ['gh', 'auth', 'status'],\n 'GitHub CLI is authenticated',\n 'GitHub CLI is not authenticated. Run: gh auth login',\n ),\n withDetail(\n 'doppler',\n checkCommand(\n 'doppler installed',\n specFor('doppler').probeArgv,\n 'Doppler CLI is installed',\n 'Doppler CLI is not installed. Install from: https://docs.doppler.com/docs/install-cli',\n ),\n ),\n withDetail(\n 'aws',\n checkCommand(\n 'aws installed',\n specFor('aws').probeArgv,\n 'AWS CLI is installed',\n 'AWS CLI is not installed. Install from: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html',\n ),\n ),\n checkCommand(\n 'package manager installed',\n ['pnpm', '--version'],\n 'Installed: pnpm',\n 'pnpm is not installed. Install from: https://pnpm.io/installation',\n ),\n checkCommand(\n 'typescript-language-server installed',\n ['typescript-language-server', '--version'],\n 'typescript-language-server is installed',\n 'typescript-language-server is not installed. Install from: https://github.com/typescript-language-server/typescript-language-server#installing',\n ),\n checkCommand(\n 'terminal installed',\n ['cmux', '--version'],\n 'Installed: cmux',\n 'cmux is not installed. Install from: https://cmux.com/',\n ),\n Promise.resolve(checkZshrcInitialized()),\n checkWarmCache(),\n checkPnpmWorkspaceVirtualStore(),\n Promise.resolve(checkInfraKitConfigValid(read)),\n checkTokenStorePresent(),\n checkEnvTokensConfigured(read),\n checkEnvTokenValid(read),\n checkTokenStorePerms(options.fix ?? false),\n checkUserOverridePath(),\n checkLegacyUserGlobalConfig(),\n checkIdeInstalled(read),\n ])\n\n const portlessChecks = await checkPortless()\n\n // `--fix` SWAPS the read-only stale-route check for the one that actually removes them, so the report\n // never prints a \"stale route\" failure beside the line that just cleaned it up.\n if (options.fix) {\n const pruned = await pruneStalePortlessRoutes()\n const index = portlessChecks.findIndex((check) => {\n return check.name === pruned.name\n })\n\n if (index >= 0) portlessChecks[index] = pruned\n else portlessChecks.push(pruned)\n }\n\n // The Claude Code plugin rows read `~/.claude/` and answer from anywhere; the `.mcp.json` row is\n // about a PROJECT and so is gated \u2014 but NOT on the same predicate as the guidance check any more.\n // The row set follows the WRITER's gate, per writer: `setup` writes `.mcp.json` in any git toplevel\n // that is not `$HOME` (`resolveGitRoot`), while the guidance writer still requires an\n // `infra-kit.json` there. Gating this row on `infra-kit.json` too would leave the writer with no\n // monitoring row at all in exactly the repos that lack one.\n //\n // `resolveGitRoot` is also what keeps a blank `git rev-parse` from being answered: it returns\n // `null` rather than `''`, so the row is omitted instead of rendered against `process.cwd()`.\n const repoRoot = await resolveCheckedRepoRoot()\n const gitRoot = await resolveGitRoot()\n const pluginChecks = [\n // First in the section: the binary the install step drives. Read the prerequisite before the\n // three rows whose failure it explains.\n await checkClaudeCli(),\n ...checkClaudePlugin(repoRoot),\n ...(gitRoot === null ? [] : [checkMcpServerKey(gitRoot)]),\n ]\n\n const checks: CheckResult[] = [...baseChecks, ...portlessChecks, ...(await checkAgentFiles()), ...pluginChecks]\n\n // NO rendering here, deliberately. `doctor()` has two callers \u2014 the CLI action and the MCP tool \u2014\n // and only one of them has a terminal. Printing from inside would emit a human report into an MCP\n // server's stderr on every agent call; the CLI action owns presentation instead (see `report.ts`).\n //\n // `fixable` and `cliVersion` are here because this payload is what a non-terminal caller reads, and\n // neither is recoverable from it otherwise. The `--fix` hint lives in `report.ts`, which `--json`\n // skips entirely, and fixability is deliberately NOT derivable from a message \u2014 `portless routes`\n // names a MANUAL removal command in its failure text while `--fix` owns the row, so a caller\n // reading messages would send a user down the hand-removal path. `cliVersion` is a top-level field\n // rather than a parse of the `CLI version` row's message: a version floor is a comparison, and the\n // failure direction of a slipped parse is \"assume the floor is met\".\n const structuredContent = {\n checks: checks.map((c) => {\n // `detail` is carried unconditionally, `undefined` on the ~23 rows that have none: a conditional\n // spread would type the array as a UNION and make `row.detail` unreadable at every call site.\n // `JSON.stringify` drops an undefined value, so the rendered payload is identical either way.\n return {\n name: c.name,\n status: c.status,\n message: c.message,\n fixable: FIXABLE_NAMES.has(c.name),\n detail: c.detail,\n }\n }),\n allPassed: checks.every((c) => {\n return c.status === 'pass'\n }),\n cliVersion: packageJson.version,\n }\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n}\n\n// MCP Tool Registration\nexport const doctorMcpTool = defineMcpTool({\n name: 'doctor',\n description:\n 'Check installation and authentication status of gh, doppler, and aws CLIs. Read-only: a row marked fixable is one `infra-kit doctor --fix` repairs, and that flag is not reachable from this boundary \u2014 report such a row to the human instead.',\n inputSchema: {},\n outputSchema: {\n checks: z\n .array(\n z.object({\n name: z.string().describe('Name of the check'),\n status: z.enum(['pass', 'fail']).describe('Check result'),\n message: z.string().describe('Details about the check result'),\n fixable: z.boolean().describe('Whether `infra-kit doctor --fix` repairs this row'),\n // Present on the four dependency rows only. `status` answers \"resolves on PATH\"; this answers\n // the richer question beside it, which is why `present` and `onPath` are both here and can\n // disagree with each other.\n detail: z\n .object({\n manager: z.string().describe('Which package manager owns this install, read from its resolved path'),\n version: z.string().nullable().describe('The version the tool reported, or null'),\n present: z.boolean().describe('The tool is installed somewhere findable \u2014 on PATH or not'),\n onPath: z.boolean().describe('The binary resolves on PATH'),\n })\n .optional()\n .describe('Probe facts about an external dependency. Never a fix: no action, no commands'),\n }),\n )\n .describe('List of all check results'),\n allPassed: z.boolean().describe('Whether all checks passed'),\n cliVersion: z.string().describe('Version of the infra-kit CLI that produced this report'),\n },\n // Read-only on purpose: `--fix` is NOT reachable here. The MCP boundary auto-confirms every tool call,\n // so a state-mutating flag must never be one `handler: doctor` away from an agent invoking it.\n handler: () => {\n return doctor()\n },\n})\n", "/**\n * @fileoverview\n *\n * `infra-kit doctor --fix`: remove the portless routes a DEAD dev-server left behind.\n *\n * A runner deregisters its aliases in `shutdown()` \u2014 but `shutdown()` only runs when a signal handler\n * gets to run. `kill -9`, an OOM kill, a force-quit or a power loss all skip it, and alias removal is an\n * async subprocess, so neither the synchronous force-quit path nor a `process.on('exit')` hook could ever\n * do it either. The route then outlives its server: a name in portless's table pointing at a port nothing\n * serves. Hit that URL and you get an opaque 502. This command is the ONLY thing that can clean that up.\n *\n * \u2500\u2500\u2500 Why \"nothing is listening\" is NOT, on its own, a safe test \u2500\u2500\u2500\n *\n * It is the test {@link checkPortlessStaleRoutes} already uses, and it is unsound. It is only harmless\n * there because that check merely PRINTS. Making it ACT on the same evidence would delete live routes:\n *\n * - `assignUiPort` picks a UI port with `getFreePort()`, which binds a probe socket and RELEASES it.\n * The alias is registered immediately; `vite` only binds the port seconds later, when it boots. For\n * that entire window a perfectly healthy UI route has nothing listening on its port.\n * - A UI alias has no dev-context fragment either (only `startOneApp` writes those, and only for\n * backends), so there is no second source of evidence to fall back on.\n * - A watch-restart re-closes and re-binds a backend port, so even an API route has brief dead windows.\n *\n * \u2500\u2500\u2500 The invariant that makes it safe \u2500\u2500\u2500\n *\n * Every one of those false-positives requires a dev session to be RUNNING. So:\n *\n * prune only when no dev session is alive anywhere on this machine.\n *\n * With nothing running, nothing can be mid-boot or mid-restart, and a port with no listener is genuinely\n * dead. The check is deliberately OVER-eager (see {@link DEV_ACTIVITY_PATTERNS}): a false \"dev is running\"\n * costs the user one retry, while a false \"dev is stopped\" deletes a live route. The costs are not\n * symmetric, so neither is the default.\n *\n * Removal is also the mildest possible action: it kills no process, and the next `infra-kit dev` simply\n * re-registers the alias. This module never signals anything.\n */\nimport { execFileSync } from 'node:child_process'\n\n/** A portless route after its port has been probed on the wire. */\nexport interface ProbedRoute {\n name: string\n port: number\n /** Is something listening on `port` right now? */\n live: boolean\n}\n\n/** What {@link decidePrune} concluded, split by what may actually be touched. */\nexport interface PruneDecision {\n /** Dead on the wire AND no dev session is running: safe to remove. */\n prunable: string[]\n /**\n * Dead on the wire, but a dev session IS running \u2014 so \"dead\" is unprovable: this is exactly the shape\n * of a UI route whose vite has not bound its port yet. Reported, never removed.\n */\n withheld: string[]\n}\n\n/**\n * Decide which routes may be removed. Pure \u2014 the whole safety property lives here, so it is testable\n * without a daemon, a process table, or a network.\n *\n * @example\n * decidePrune([{ name: 'x.api', port: 1, live: false }], false) // => { prunable: ['x.api'], withheld: [] }\n * decidePrune([{ name: 'x.api', port: 1, live: false }], true) // => { prunable: [], withheld: ['x.api'] }\n */\nexport const decidePrune = (probed: readonly ProbedRoute[], devSessionRunning: boolean): PruneDecision => {\n const dead = probed\n .filter((route) => {\n return !route.live\n })\n .map((route) => {\n return route.name\n })\n\n // A live dev session makes \"nothing is listening\" meaningless \u2014 it is indistinguishable from a UI that\n // is still booting. Withhold ALL of them rather than guess which are which.\n if (devSessionRunning) return { prunable: [], withheld: dead }\n\n return { prunable: dead, withheld: [] }\n}\n\n/**\n * Command lines of a live `infra-kit dev` RUNNER. Matched against `ps -eo command=`.\n *\n * Deliberately the runner and NOTHING ELSE \u2014 not `turbo`, not `vite`.\n */\n// That looks too narrow, and the first draft of this file did include them, reasoning that\n// \"over-matching only withholds a prune\". Running it disproved that: a crashed session leaves\n// `turbo run dev` orphans alive (exactly the orphan class `managed-child` exists to prevent), so\n// matching turbo means the leftovers of a dead session permanently block the cleanup of that same\n// session's routes \u2014 `--fix` would be inert on precisely the machine that needs it. Observed:\n// three orphaned `turbo run dev` processes withholding six dead routes.\n//\n// The runner is also the CORRECT thing to gate on, not merely the workable one. It is the only\n// process that registers a portless alias, so only its presence can mean \"a route may be about to\n// become live\":\n// - a route can only appear at all while a runner is alive;\n// - the booting-UI window (alias registered, vite has not bound the port yet) is bounded by the\n// runner;\n// - an orphaned turbo/vite registers no new routes, and if one still HOLDS a route's port then\n// the wire probe already sees it as live and it is never a prune candidate in the first place.\nconst DEV_RUNNER_PATTERNS: readonly RegExp[] = [/\\binfra-kit\\s+dev\\b/, /cli\\.js\\s+dev\\b/, /dev-server\\.(?:js|ts)\\b/]\n\n/**\n * Does this `ps` command line belong to a live `infra-kit dev` runner?\n *\n * @example\n * looksLikeDevRunner('node /x/dist/cli.js dev --watch') // => true\n * looksLikeDevRunner('node /x/node_modules/.bin/turbo run dev') // => false \u2014 an orphan, not a runner\n */\nexport const looksLikeDevRunner = (command: string): boolean => {\n return DEV_RUNNER_PATTERNS.some((pattern) => {\n return pattern.test(command)\n })\n}\n\n/** Absolute path to `ps` \u2014 never resolved through `PATH`, which a writable entry could substitute. */\nconst PS_BIN = '/bin/ps'\n\n/** Every running process's command line, one per entry. */\nconst defaultReadCommands = (): string[] => {\n return execFileSync(PS_BIN, ['-eo', 'command='], { encoding: 'utf8', maxBuffer: 8 << 20 }).split('\\n')\n}\n\n/**\n * Is an `infra-kit dev` runner alive anywhere on this machine?\n *\n * Machine-wide, not repo-wide: portless routes are machine-global, and a runner in another worktree can be\n * mid-boot against a route this repo can see.\n *\n * Fails CLOSED: if `ps` cannot be read we report `true` \u2014 \"a runner might be up\" \u2014 so an unreadable process\n * table withholds the prune instead of authorising it.\n */\nexport const isDevSessionRunning = (readCommands: () => string[] = defaultReadCommands): boolean => {\n try {\n return readCommands().some(looksLikeDevRunner)\n } catch {\n // No process table, no proof of safety.\n return true\n }\n}\n", "import { Chalk } from 'chalk'\nimport process from 'node:process'\n\n// Imported from the leaf module, not the `src/lib/infra-kit-config` barrel, because this is read at\n// module scope (below) and 22 suites partially mock that barrel \u2014 a partial mock drops every export it\n// does not name, so a barrel import here makes an unrelated suite fail the moment anything imports\n// `report.ts`. Nothing mocks the leaf.\nimport { DEFAULT_DEV_PROXY_PORT } from 'src/lib/infra-kit-config/infra-kit-config'\n\nimport type { CheckResult } from './doctor'\n\n/**\n * @fileoverview\n *\n * Presentation layer for `infra-kit doctor`. Everything here is PURE: it takes the already-computed\n * `CheckResult[]` and returns lines. It never probes, never writes, and never reorders its input.\n *\n * Why this is not the logger: doctor emits ~24 checks, and pino-pretty rendered every one as an\n * identical `INFO:` line \u2014 so a single FAIL was invisible in a wall of PASS. This module groups the\n * checks into labelled sections with per-section rollups and a summary, which is the whole point.\n *\n * Why this is not Ink: this is a ONE-SHOT report, not a live region. Ink writes `ESC[3J` (erase saved\n * lines) whenever a frame overflows the viewport, which wipes the user's terminal SCROLLBACK \u2014 the\n * hazard `src/tui/safe-stderr.ts` exists to contain for the dev-ui. A report that is routinely taller\n * than the viewport is exactly the shape that triggers it, for no benefit a string[] does not already\n * give us.\n *\n * REDACTION INVARIANT \u2014 read before adding a check message: these lines are written straight to\n * stderr, which BYPASSES pino's `REDACT_PATHS` backstop (see `lib/logger`). Every doctor message is\n * pre-redacted by construction (`describeEntries` reports a token's SOURCE, never its value;\n * `checkEnvTokenValid` never interpolates the token). Any new check message must hold to that.\n */\n\n/** A named group of checks, in the order they are printed. */\nexport interface DoctorSection {\n label: string\n checks: CheckResult[]\n}\n\nconst SECTION_TOOLS = 'Tools & CLIs'\nconst SECTION_SHELL = 'Shell & integration'\nconst SECTION_CONFIG = 'Project config'\nconst SECTION_TOKENS = 'Doppler env tokens'\nconst SECTION_PROXY = 'Dev proxy (portless)'\nconst SECTION_PLUGIN = 'Claude Code plugin'\n\n/**\n * The `portless serving TLS on :443` check builds its name by interpolating the proxy port, so this\n * key is derived from the SAME constant. A frozen `':443'` literal would silently drop the check into\n * `Other` the day that port changes.\n */\nconst PORTLESS_SERVING_NAME = `portless serving TLS on :${DEFAULT_DEV_PROXY_PORT}`\n\n/**\n * Which section each check belongs to, in print order. Keyed by check `name` rather than carried as a\n * `category` field on `CheckResult` so the 24 check functions (and their tests) stay untouched \u2014 the\n * grouping knowledge lives in exactly one place instead of being smeared across `doctor.ts`.\n *\n * A name that is missing here is NOT dropped: it falls into `Other` at runtime (see {@link groupChecks}).\n * The drift is caught by tests instead \u2014 `report.test.ts` asserts every name in\n * {@link DOCTOR_CHECK_NAMES} maps to a non-`Other` section, and that the inventory itself still equals\n * what a real `doctor()` run produces.\n */\nconst SECTION_MEMBERS: ReadonlyArray<readonly [string, readonly string[]]> = [\n [\n SECTION_TOOLS,\n [\n // First in the section because it is first in prerequisite order: `gh` and `doppler` are both\n // installed THROUGH brew here, so a reader scanning a failing report reads the cause above the\n // effects.\n 'brew installed',\n 'gh installed',\n 'gh authenticated',\n 'doppler installed',\n 'aws installed',\n 'package manager installed',\n 'typescript-language-server installed',\n 'terminal installed',\n 'ide installed',\n ],\n ],\n [SECTION_SHELL, ['zshrc init block', 'warm cache', 'pnpm enableGlobalVirtualStore', 'CLAUDE.md block']],\n [SECTION_CONFIG, ['infra-kit config valid', 'user override path', 'legacy user-global config']],\n [SECTION_TOKENS, ['tokens.json present', 'env tokens configured', 'env token valid', 'tokens.json perms']],\n [\n SECTION_PROXY,\n ['portless installed', PORTLESS_SERVING_NAME, 'portless CA chain valid', 'portless CA trusted', 'portless routes'],\n ],\n [\n SECTION_PLUGIN,\n ['claude CLI', 'marketplace registered', 'plugin installed', 'plugin version', 'CLI version', 'MCP server key'],\n ],\n]\n\n/** The bucket for any check name not in {@link SECTION_MEMBERS} \u2014 a safety net, never a destination. */\nexport const OTHER_SECTION = 'Other'\n\n/** The section order used for printing. */\nconst SECTION_ORDER: readonly string[] = SECTION_MEMBERS.map(([label]) => {\n return label\n})\n\n/** name \u2192 section label. */\nconst SECTION_BY_NAME = new Map<string, string>(\n SECTION_MEMBERS.flatMap(([label, names]) => {\n return names.map((name): [string, string] => {\n return [name, label]\n })\n }),\n)\n\n/**\n * Every check name `doctor()` can produce. This is the inventory the coverage test asserts the section\n * map covers, and that a deterministic `doctor()` run is reconciled against \u2014 so a new check that\n * updates neither this list nor {@link SECTION_MEMBERS} fails CI instead of quietly landing in `Other`.\n */\nexport const DOCTOR_CHECK_NAMES: readonly string[] = SECTION_MEMBERS.flatMap(([, names]) => {\n return names\n})\n\n/**\n * The checks `infra-kit doctor --fix` can actually resolve \u2014 stale portless routes\n * ({@link import('./doctor').pruneStalePortlessRoutes}) and loose token-store modes\n * ({@link import('./doctor').checkTokenStorePerms}). The summary's remediation hint is driven by this\n * set rather than by matching message text, so it cannot drift from the real `--fix` code paths.\n */\nexport const FIXABLE_NAMES: ReadonlySet<string> = new Set(['portless routes', 'tokens.json perms'])\n\n/**\n * Group checks into printable sections. NON-MUTATING by contract: `structuredContent.checks` is built\n * from the same array in `doctor()`, and its order is part of that payload's compatibility surface, so\n * this must never sort or splice the input.\n *\n * Empty sections are omitted (a machine with no portless binary reports one portless check, not five),\n * and any unmapped name lands in a trailing {@link OTHER_SECTION} rather than disappearing.\n *\n * @example\n * groupChecks([{ name: 'gh installed', status: 'pass', message: '\u2026' }])\n * // => [{ label: 'Tools & CLIs', checks: [ \u2026 ] }]\n */\nexport const groupChecks = (checks: readonly CheckResult[]): DoctorSection[] => {\n const buckets = new Map<string, CheckResult[]>()\n\n for (const check of checks) {\n const label = SECTION_BY_NAME.get(check.name) ?? OTHER_SECTION\n const bucket = buckets.get(label)\n\n if (bucket) bucket.push(check)\n else buckets.set(label, [check])\n }\n\n const ordered = [...SECTION_ORDER, OTHER_SECTION]\n\n return ordered.flatMap((label): DoctorSection[] => {\n const bucket = buckets.get(label)\n\n return bucket && bucket.length > 0 ? [{ label, checks: bucket }] : []\n })\n}\n\nexport interface DoctorReportOptions {\n /** Emit ANSI colour. When false the output contains zero escape bytes. */\n color?: boolean\n /** Use `\u2713`/`\u2717` glyphs. When false, ASCII `ok`/`FAIL` markers are used instead. */\n unicode?: boolean\n /** Terminal width to wrap long messages against. */\n width?: number\n}\n\n/**\n * Every character that differs between unicode and ASCII mode, in ONE table. Each variant's `pass`\n * and `fail` are equal-width, and `separator` is a single column in both \u2014 which is what makes a\n * section header's measured width independent of the glyph mode.\n *\n * SCOPE: this governs the report's own chrome (markers, separator, rule). It does NOT sanitise check\n * MESSAGES, which come from `doctor.ts` and already contain non-ASCII punctuation (em-dashes, curly\n * quotes). `--ascii` therefore means \"don't require box-drawing/symbol glyphs\", not \"emit pure ASCII\".\n */\nconst GLYPHS = {\n unicode: { pass: '\u2713', fail: '\u2717', separator: '\u00B7', rule: '\u2500' },\n ascii: { pass: 'ok', fail: '!!', separator: '-', rule: '-' },\n} as const\n\ntype Glyphs = (typeof GLYPHS)[keyof typeof GLYPHS]\n\n/**\n * Never wrap a message into a column narrower than this, however small the terminal claims to be.\n *\n * This is a readability floor, NOT a fit guarantee: the message column starts after the widest check\n * name in the WHOLE report (`typescript-language-server installed` alone is 36 chars), so on a genuinely\n * narrow terminal a row can still exceed `width`. Wrapping messages into a 4-column ribbon would be\n * worse than overflowing, and the alternative \u2014 truncating \u2014 is ruled out because these messages carry\n * the commands the user has to copy.\n */\nconst MIN_MESSAGE_WIDTH = 24\n\n/**\n * Greedy word wrap. Words longer than `width` (a URL, a long `node \u2026/cli.js \u2026` command) are emitted\n * whole on their own line rather than being broken \u2014 a fix command split mid-token is not runnable,\n * and printing these commands runnable is the entire reason doctor renders them.\n */\nconst wrapText = (text: string, width: number): string[] => {\n const words = text.split(/\\s+/).filter((word) => {\n return word.length > 0\n })\n const lines: string[] = []\n let current = ''\n\n for (const word of words) {\n if (current.length === 0) current = word\n else if (`${current} ${word}`.length <= width) current = `${current} ${word}`\n else {\n lines.push(current)\n current = word\n }\n }\n if (current.length > 0) lines.push(current)\n\n return lines.length > 0 ? lines : ['']\n}\n\n/** Per-section pass/fail tallies. */\nconst tally = (checks: readonly CheckResult[]): { passed: number; failed: number } => {\n const failed = checks.filter((check) => {\n return check.status === 'fail'\n }).length\n\n return { passed: checks.length - failed, failed }\n}\n\ninterface Palette {\n header: (text: string) => string\n pass: (text: string) => string\n fail: (text: string) => string\n dim: (text: string) => string\n bold: (text: string) => string\n}\n\n/**\n * A palette seeded from an EXPLICIT colour decision rather than chalk's global auto-detection.\n * That default keys on `process.stdout`, but this report is written to `process.stderr` \u2014 so relying\n * on it would strip colour from `doctor | cat` (stdout piped, stderr still a terminal) and write raw\n * ANSI into `doctor 2>report.txt`. Both backwards.\n */\nconst createPalette = (color: boolean): Palette => {\n const chalk = new Chalk({ level: color ? 1 : 0 })\n\n return {\n header: (text): string => {\n return chalk.bold.cyan(text)\n },\n pass: (text): string => {\n return chalk.green(text)\n },\n fail: (text): string => {\n return chalk.red(text)\n },\n dim: (text): string => {\n return chalk.dim(text)\n },\n bold: (text): string => {\n return chalk.bold(text)\n },\n }\n}\n\n/**\n * `7/7 ok` / `4/5 \u00B7 1 failed` \u2014 a section's verdict at a glance.\n *\n * Returns the VISIBLE width alongside the (coloured) text. The caller needs that width to right-align\n * the rollup and cannot recover it from the string, because colour has already been baked in. An\n * earlier version reconstructed a plain stand-in template instead and got it wrong in both directions\n * \u2014 all-pass headers stopped 6 columns short, and failing headers overshot by 2 and WRAPPED, which\n * mangled the one line the grouping exists to make obvious. Never measure a coloured string.\n */\nconst formatRollup = (\n counts: { passed: number; failed: number },\n total: number,\n palette: Palette,\n glyphs: Glyphs,\n): { text: string; plainWidth: number } => {\n if (counts.failed === 0) {\n const plain = `${total}/${total} ok`\n\n return { text: palette.pass(plain), plainWidth: plain.length }\n }\n\n const ratio = `${counts.passed}/${total}`\n const failed = `${counts.failed} failed`\n\n return {\n text: `${palette.dim(ratio)} ${glyphs.separator} ${palette.fail(failed)}`,\n // +3 = the space, the single-column separator, and the space.\n plainWidth: ratio.length + failed.length + 3,\n }\n}\n\nconst INDENT = ' '\nconst ROW_INDENT = ' '\n\n/**\n * A section heading with its rollup pushed to the right margin (or one space away, when the label and\n * rollup together already fill the line). The gap is computed entirely from VISIBLE widths, so the\n * rendered header is exactly `width` columns and never wraps.\n */\nconst formatSectionHeader = (section: DoctorSection, palette: Palette, glyphs: Glyphs, width: number): string => {\n const counts = tally(section.checks)\n const rollup = formatRollup(counts, section.checks.length, palette, glyphs)\n const gap = Math.max(1, width - INDENT.length - section.label.length - rollup.plainWidth)\n\n return `${INDENT}${palette.header(section.label)}${' '.repeat(gap)}${rollup.text}`\n}\n\n/**\n * One check, as one or more lines: ` \u2713 name message`, with any wrapped continuation\n * hanging-indented to the message column so the two-column shape survives a long message.\n *\n * Colour is applied AFTER padding, never before: `padEnd` counts ANSI escape bytes as visible\n * characters, so colouring the glyph or name first would silently skew every column on the coloured\n * (i.e. the primary, interactive) path \u2014 the one a strip-and-assert test cannot see.\n */\nconst formatCheckRow = (\n check: CheckResult,\n nameWidth: number,\n palette: Palette,\n glyphs: { pass: string; fail: string },\n width: number,\n): string[] => {\n const failed = check.status === 'fail'\n // Both glyph pairs are internally equal-width (`\u2713`/`\u2717` = 1, `ok`/`!!` = 2), so the marker needs no\n // padding \u2014 but the column arithmetic below still has to account for its width.\n const glyphWidth = glyphs.pass.length\n const plainGlyph = failed ? glyphs.fail : glyphs.pass\n const plainName = check.name.padEnd(nameWidth)\n const glyph = failed ? palette.fail(plainGlyph) : palette.pass(plainGlyph)\n const name = failed ? palette.fail(plainName) : plainName\n const messageColumn = ROW_INDENT.length + glyphWidth + 1 + nameWidth + 2\n const messageWidth = Math.max(MIN_MESSAGE_WIDTH, width - messageColumn)\n const [first, ...rest] = wrapText(check.message, messageWidth)\n const paint = (text: string): string => {\n return failed ? text : palette.dim(text)\n }\n // `trimEnd` so a check with an empty message does not leave trailing whitespace on the line.\n const head = `${ROW_INDENT}${glyph} ${name} ${paint(first ?? '')}`.trimEnd()\n\n return [\n head,\n ...rest.map((line) => {\n return `${' '.repeat(messageColumn)}${paint(line)}`\n }),\n ]\n}\n\n/** The closing rule + totals, plus a `--fix` hint only when a FAILING check is actually fixable. */\nconst formatSummary = (checks: readonly CheckResult[], palette: Palette, glyphs: Glyphs, width: number): string[] => {\n const counts = tally(checks)\n const rule = glyphs.rule.repeat(Math.max(12, Math.min(width - INDENT.length, 56)))\n const fixable = checks.filter((check) => {\n return check.status === 'fail' && FIXABLE_NAMES.has(check.name)\n }).length\n const passed = palette.pass(`${counts.passed} passed`)\n const failed = palette.fail(`${counts.failed} failed`)\n const totals = counts.failed === 0 ? passed : `${passed} ${glyphs.separator} ${failed}`\n const hintText = palette.dim(`${fixable} fixable \u2014 run \\`infra-kit doctor --fix\\``)\n const hint = fixable > 0 ? ` ${hintText}` : ''\n\n return [`${INDENT}${palette.dim(rule)}`, `${INDENT}${totals}${hint}`]\n}\n\n/**\n * Render the whole report. Pure: same input, same lines, no I/O and no environment sniffing \u2014 every\n * capability is passed in (see {@link resolveReportCapabilities}), which is what makes the colour,\n * ASCII and narrow-width behaviours unit-testable rather than machine-dependent.\n *\n * @example\n * formatDoctorReport([{ name: 'gh installed', status: 'pass', message: 'installed' }], { color: false })\n * // => ['infra-kit doctor', '', ' Tools & CLIs \u2026', ' \u2713 gh installed installed', \u2026]\n */\nexport const formatDoctorReport = (checks: readonly CheckResult[], options: DoctorReportOptions = {}): string[] => {\n const color = options.color ?? false\n const unicode = options.unicode ?? true\n const width = options.width ?? 80\n const palette = createPalette(color)\n const glyphs = unicode ? GLYPHS.unicode : GLYPHS.ascii\n\n if (checks.length === 0) return [palette.bold('infra-kit doctor'), '', `${INDENT}${palette.dim('No checks ran.')}`]\n\n // ONE name column for the whole report, not one per section: the message column is the eye's\n // vertical guide down the page, and re-measuring it per section made it jump at every heading.\n // Costs a few spaces of padding in the sections whose names are short \u2014 worth it.\n const nameWidth = Math.max(\n ...checks.map((check) => {\n return check.name.length\n }),\n )\n\n const sections = groupChecks(checks).flatMap((section): string[] => {\n return [\n formatSectionHeader(section, palette, glyphs, width),\n ...section.checks.flatMap((check) => {\n return formatCheckRow(check, nameWidth, palette, glyphs, width)\n }),\n '',\n ]\n })\n\n return [palette.bold('infra-kit doctor'), '', ...sections, ...formatSummary(checks, palette, glyphs, width)]\n}\n\n/** Values of `FORCE_COLOR` that mean \"off\" \u2014 `'0'` is chalk's documented disable, and `Boolean('0')` is `true`. */\nconst FORCE_COLOR_OFF = new Set(['', '0', 'false', 'none'])\n\n/** The locale variables consulted for UTF-8 capability, in POSIX precedence order. */\nconst LOCALE_VARS = ['LC_ALL', 'LC_CTYPE', 'LANG'] as const\n\nexport interface ReportCapabilityInput {\n /** The `--ascii` flag: forces ASCII glyphs regardless of locale. */\n ascii?: boolean\n env?: NodeJS.ProcessEnv\n /** The stream the report is written to \u2014 `process.stderr`, NOT stdout. */\n stream?: { isTTY?: boolean; columns?: number }\n}\n\n/** `NO_COLOR` (any non-empty value) wins; then `FORCE_COLOR`; then whether the target stream is a TTY. */\nconst resolveColor = (env: NodeJS.ProcessEnv, stream: { isTTY?: boolean }): boolean => {\n if (env.NO_COLOR !== undefined && env.NO_COLOR !== '') return false\n\n if (env.FORCE_COLOR !== undefined) return !FORCE_COLOR_OFF.has(env.FORCE_COLOR.toLowerCase())\n\n return Boolean(stream.isTTY)\n}\n\n/**\n * UTF-8 unless a locale variable is SET and says otherwise. The \"is set\" guard is load-bearing: an\n * unset locale is the common case (launchd, CI, bare `sh`), and testing an empty string against\n * `/UTF-?8/` would force ASCII on every one of those terminals, all of which render the glyphs fine.\n */\nconst resolveUnicode = (env: NodeJS.ProcessEnv, ascii: boolean): boolean => {\n if (ascii) return false\n\n const locale = LOCALE_VARS.map((name) => {\n return env[name]\n }).find((value) => {\n return value !== undefined && value !== ''\n })\n\n return locale === undefined ? true : /UTF-?8/i.test(locale)\n}\n\n/**\n * Resolve colour / unicode / width from the environment and the TARGET stream. Split out from\n * {@link formatDoctorReport} so the formatter stays pure and these three rules stay testable without\n * mutating `process.env`.\n *\n * @example\n * resolveReportCapabilities({ env: { NO_COLOR: '1' }, stream: { isTTY: true, columns: 120 } })\n * // => { color: false, unicode: true, width: 120 }\n */\nexport const resolveReportCapabilities = (input: ReportCapabilityInput = {}): Required<DoctorReportOptions> => {\n const env = input.env ?? process.env\n const stream = input.stream ?? process.stderr\n\n return {\n color: resolveColor(env, stream),\n unicode: resolveUnicode(env, input.ascii ?? false),\n width: stream.columns ?? 80,\n }\n}\n\nexport interface PrintDoctorReportDeps extends ReportCapabilityInput {\n /** Where the report goes. Defaults to a single `process.stderr.write`. */\n write?: (text: string) => void\n}\n\n/**\n * Write the report to stderr \u2014 humans on stderr, machines on stdout, which is what keeps\n * `doctor --json | jq` and the MCP stdio transport clean.\n *\n * ONE write, deliberately: pino also targets fd 2, and emitting 30 separate writes would let any\n * concurrent `logger.*` call tear a line straight through the middle of the report.\n */\nexport const printDoctorReport = (checks: readonly CheckResult[], deps: PrintDoctorReportDeps = {}): void => {\n const write =\n deps.write ??\n ((text: string): void => {\n process.stderr.write(text)\n })\n\n write(`${formatDoctorReport(checks, resolveReportCapabilities(deps)).join('\\n')}\\n`)\n}\n", "import select from '@inquirer/select'\nimport { Buffer } from 'node:buffer'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\nimport { z } from 'zod'\nimport { $ } from 'zx'\n\nimport {\n DopplerAuthError,\n INFRA_KIT_ENV_TOKEN_VAR,\n buildDopplerNotFoundMessage,\n classifyDopplerAuthFailure,\n classifyDopplerFailure,\n getDopplerProject,\n resolveEnvToken,\n} from 'src/integrations/doppler'\nimport { commandEcho } from 'src/lib/command-echo'\nimport {\n ENV_LOAD_FILE,\n INFRA_KIT_ENV_AUTOLOADED_VAR,\n INFRA_KIT_ENV_CLEARED_VAR,\n INFRA_KIT_ENV_CONFIG_VAR,\n INFRA_KIT_ENV_LOADED_AT_VAR,\n INFRA_KIT_ENV_PROJECT_ROOT_VAR,\n INFRA_KIT_ENV_PROJECT_VAR,\n INFRA_KIT_ENV_VAR,\n atomicWriteFileSync,\n getSessionCacheDir,\n} from 'src/lib/constants'\nimport { extractStderr } from 'src/lib/errors/operation-error'\nimport { getProjectRoot } from 'src/lib/git-utils'\nimport { logger } from 'src/lib/logger'\nimport { listProjectEnvNames } from 'src/lib/project-envs'\nimport { withEscape } from 'src/lib/prompts/escapable-context'\nimport { canonicalizeProjectRoot, evictStaleWarmCaches, shouldWriteWarm, writeWarmCache } from 'src/lib/warm-cache'\nimport { defineMcpTool, textContent } from 'src/types'\n\ninterface EnvLoadArgs {\n config?: string\n}\n\ninterface WriteEnvLoadFileArgs {\n /** Resolved Doppler config / environment name (no interactive picker here). */\n config: string\n /**\n * Marks the produced file as auto-loaded. `true` writes the\n * INFRA_KIT_ENV_AUTOLOADED marker; `false` (a manual load) instead unsets the\n * marker and lifts any clear suppression so a deliberate load wins.\n */\n autoLoaded?: boolean\n /**\n * Auto-load only: re-checked AFTER the Doppler download, immediately before the\n * atomic write. Return false to abort (a clear or a manual load landed during the\n * slow download). Manual loads omit it and always write.\n */\n beforeWrite?: () => boolean\n /**\n * Canonical (realpath'd) project dir the SHELL passed via `--project-dir` on the\n * shell-startup auto-load spawn. Its presence (with `autoLoaded`) is what enables\n * the project-scoped WARM cache write; see {@link shouldWriteWarm}. Undefined for\n * manual loads and the cli-invocation trigger \u2014 those write no warm copy.\n */\n projectDir?: string\n}\n\nexport interface EnvLoadFileResult {\n filePath: string\n variableCount: number\n project: string\n config: string\n}\n\ninterface EnvLoadFileLinesArgs {\n pairs: Array<[string, string]>\n config: string\n project: string\n projectRoot: string\n loadedAt: string\n autoLoaded: boolean\n}\n\n/**\n * The auto-load marker lines appended to env-load.sh. Pure so the marker policy\n * is unit-testable without touching Doppler or the filesystem.\n *\n * @example\n * buildAutoLoadMarkerLines(true) // => [\"INFRA_KIT_ENV_AUTOLOADED='1'\"]\n * buildAutoLoadMarkerLines(false) // => ['unset INFRA_KIT_ENV_AUTOLOADED', 'unset INFRA_KIT_ENV_CLEARED']\n */\nconst buildAutoLoadMarkerLines = (autoLoaded: boolean): string[] => {\n if (autoLoaded) {\n return [`${INFRA_KIT_ENV_AUTOLOADED_VAR}=${shellSingleQuote('1')}`]\n }\n\n // Manual load: drop any auto marker so auto-load never re-clobbers a deliberate\n // choice, and lift a prior clear suppression so the manual load takes effect.\n return [`unset ${INFRA_KIT_ENV_AUTOLOADED_VAR}`, `unset ${INFRA_KIT_ENV_CLEARED_VAR}`]\n}\n\n/** The payload key that carries the token's scope \u2014 the one {@link assertTokenScope} compares. */\nconst DOPPLER_CONFIG_KEY = 'DOPPLER_CONFIG'\n\n/**\n * Secret names that must NEVER be written into the sourced file. A Doppler project can hold a secret\n * called `DOPPLER_TOKEN` (or `INFRA_KIT_ENV_TOKEN`), and `env-load.sh` is sourced into EVERY shell \u2014\n * exporting one would hand a live credential to every child process forever, and (per SPIKE-0 Q5) an\n * exported `DOPPLER_TOKEN` would then hijack the next download's auth.\n *\n * `DOPPLER_CONFIG` / `DOPPLER_PROJECT` are deliberately NOT filtered: they are not credentials, they\n * are the scope evidence {@link assertTokenScope} and `doctor` read back.\n */\nconst CREDENTIAL_SECRET_KEYS = new Set<string>(['DOPPLER_TOKEN', INFRA_KIT_ENV_TOKEN_VAR])\n\n/** Warn at most once per process: a project carrying a token secret would otherwise warn on every load. */\nlet credentialFilterWarned = false\n\n/**\n * Tell the user we dropped a credential-bearing secret from their shell env \u2014 silently swallowing a\n * key they can see in the Doppler dashboard would read as a bug. Names the KEY only, never the value.\n */\nconst warnFilteredCredentialKeys = (keys: string[]): void => {\n if (keys.length === 0 || credentialFilterWarned) return\n\n credentialFilterWarned = true\n\n logger.warn(\n `infra-kit: not exporting ${keys.join(', ')} from Doppler into your shell \u2014 ` +\n 'a service token must not be sourced into every process.',\n )\n}\n\n/**\n * Build the dotenv-format shell lines for env-load.sh. Pure apart from the one-shot credential\n * warning, so callers can assert the exact marker behavior. `set -a`/`set +a` auto-export every\n * assignment when the file is sourced.\n */\nexport const buildEnvLoadFileLines = ({\n pairs,\n config,\n project,\n projectRoot,\n loadedAt,\n autoLoaded,\n}: EnvLoadFileLinesArgs): string[] => {\n const emitted = pairs.filter(([key]) => {\n return !CREDENTIAL_SECRET_KEYS.has(key)\n })\n\n warnFilteredCredentialKeys(\n pairs\n .filter(([key]) => {\n return CREDENTIAL_SECRET_KEYS.has(key)\n })\n .map(([key]) => {\n return key\n }),\n )\n\n return [\n 'set -a',\n ...emitted.map(([key, value]) => {\n return `${key}=${shellSingleQuote(value)}`\n }),\n // Purpose-named env-name handle for non-shell tooling (e.g. infra-kit/vite's\n // `<env>` proxy interpolation). Single-quoted like every sourced value.\n `${INFRA_KIT_ENV_VAR}=${shellSingleQuote(config)}`,\n `${INFRA_KIT_ENV_CONFIG_VAR}=${shellSingleQuote(config)}`,\n `${INFRA_KIT_ENV_PROJECT_VAR}=${shellSingleQuote(project)}`,\n `${INFRA_KIT_ENV_PROJECT_ROOT_VAR}=${shellSingleQuote(projectRoot)}`,\n `${INFRA_KIT_ENV_LOADED_AT_VAR}=${shellSingleQuote(loadedAt)}`,\n ...buildAutoLoadMarkerLines(autoLoaded),\n 'set +a',\n ]\n}\n\n/**\n * Project root (git top-level) the loaded env belongs to, for the cross-project\n * shell gate. Returns '' when this isn't a git checkout so a non-git infra-kit\n * project still loads \u2014 the shell gate then fails open (spawns rather than skips)\n * instead of breaking the whole load on `git rev-parse` throwing.\n */\nconst resolveProjectRootSafe = async (): Promise<string> => {\n try {\n return await getProjectRoot()\n } catch {\n return ''\n }\n}\n\n/**\n * Download Doppler secrets for a resolved config and atomically write env-load.sh\n * to the session cache dir. Does NOT print to stdout \u2014 shared by the CLI/MCP\n * `envLoad` entry (which prints the path) and the auto-load path (which lets the\n * shell precmd hook source the file).\n */\nexport const writeEnvLoadFile = async ({\n config,\n autoLoaded = false,\n beforeWrite,\n projectDir,\n}: WriteEnvLoadFileArgs): Promise<EnvLoadFileResult | null> => {\n const project = await getDopplerProject()\n const projectRoot = await resolveProjectRootSafe()\n\n const pairs = await downloadDopplerSecrets(project, config)\n\n const loadedAt = new Date().toISOString()\n const envFileLines = buildEnvLoadFileLines({ pairs, config, project, projectRoot, loadedAt, autoLoaded })\n const fileContents = `${envFileLines.join('\\n')}\\n`\n\n const cacheDir = getSessionCacheDir()\n const envFilePath = path.resolve(cacheDir, ENV_LOAD_FILE)\n\n // Re-check suppression immediately before the atomic write: a clear or a manual\n // load may have landed during the slow Doppler download (auto-load path only).\n if (beforeWrite && !beforeWrite()) return null\n\n fs.mkdirSync(cacheDir, { recursive: true, mode: 0o700 })\n atomicWriteFileSync(envFilePath, fileContents, 0o600)\n\n // Project-scoped WARM copy: lets the NEXT shell source these vars instantly at\n // startup (pure zsh, no Doppler) before this session's fresh file lands. Gated so\n // it only fires for a shell-startup auto-load whose realpath'd git root matches the\n // dir the shell keyed on (see shouldWriteWarm). Each warm write also sweeps stale\n // warm dirs \u2014 we own the only cache reaper in the codebase.\n const canonicalProjectRoot = canonicalizeProjectRoot(projectRoot)\n\n if (shouldWriteWarm({ autoLoaded, projectDir, canonicalProjectRoot })) {\n writeWarmCache(projectDir!, fileContents)\n evictStaleWarmCaches()\n }\n\n return {\n filePath: envFilePath,\n variableCount: pairs.length,\n project,\n config,\n }\n}\n\n/**\n * Load environment variables from Doppler for the given config\n */\nexport const envLoad = async (args: EnvLoadArgs) => {\n const { config } = args\n\n let selectedConfig = ''\n\n if (config) {\n selectedConfig = config\n } else {\n // No auth pre-probe before the picker: under token-only auth there is no account\n // to validate, and the per-env token is only knowable AFTER a config is chosen.\n // A missing/rejected token surfaces from the download with an actionable message.\n //\n // The choices are every env this project has \u2014 workflow-declared and token-only alike (see\n // `lib/project-envs`). Deliberately NOT filtered to the envs we hold a token for: an env you cannot\n // yet load is exactly the one you need to SEE, so that picking it tells you to run\n // `infra-kit env-token-set <env>` rather than leaving you to wonder where it went.\n const envs = await listProjectEnvNames()\n\n commandEcho.setInteractive()\n selectedConfig = await withEscape(\n (context) => {\n return select(\n {\n message: 'Select environment config',\n choices: envs.map((env) => {\n return { name: env, value: env }\n }),\n },\n context,\n )\n },\n // Render to stderr so the prompt is visible when stdout is captured via $() in the shell function.\n // Only env-load and env-clear use the $() stdout-capture shell pattern.\n // MCP-unreachable: `config` is required on the env-load tool, so no agent call lands on this picker.\n { output: process.stderr, whenHeadless: 'unreachable' },\n )\n }\n\n commandEcho.addOption('--config', selectedConfig)\n\n // A manual load is authoritative: autoLoaded=false drops the auto marker.\n const result = await writeEnvLoadFile({ config: selectedConfig, autoLoaded: false })\n\n // A manual load passes no beforeWrite, so it always writes \u2014 this guards the\n // invariant (and narrows the nullable result) rather than handling a real path.\n if (!result) throw new Error('env-load: write was unexpectedly aborted')\n\n // REQUIRED\n process.stdout.write(`${result.filePath}\\n`)\n\n // Logs to stderr (pino \u2192 pretty-print), so it doesn't pollute the captured\n // file path that the shell wrapper reads from stdout.\n commandEcho.print()\n\n const structuredContent = {\n filePath: result.filePath,\n variableCount: result.variableCount,\n project: result.project,\n config: result.config,\n }\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n}\n\n/**\n * Cap the Doppler stdout we're willing to accept. A well-formed env bundle is\n * O(10 KB); megabytes would indicate a service regression or the wrong stream\n * being captured, and we don't want to write that to disk or source it.\n */\nexport const DOPPLER_MAX_OUTPUT_BYTES = 1024 * 1024\n\n/**\n * Hard upper bound for the Doppler subprocess. Well under zx's default so a\n * hung call surfaces quickly instead of blocking an interactive shell or an\n * MCP tool handler.\n */\nconst DOPPLER_DOWNLOAD_TIMEOUT_MS = 30_000\n\n/**\n * Doppler env vars we refuse to inherit into the download child. SPIKE-0 Q5: an inherited\n * `DOPPLER_TOKEN` OVERRIDES everything else the CLI would use \u2014 and `env-load` itself exports a whole\n * Doppler project into the shell, so a project that happens to hold a secret named `DOPPLER_TOKEN`\n * would silently hijack the NEXT download. `DOPPLER_PROJECT` / `DOPPLER_CONFIG` are scrubbed for the\n * same reason: they are the payload's own scope evidence, and an inherited copy could shadow the\n * `--project` / `--config` we pass in argv.\n */\nconst INHERITED_DOPPLER_VARS = ['DOPPLER_TOKEN', 'DOPPLER_PROJECT', 'DOPPLER_CONFIG'] as const\n\n/**\n * The child environment for `doppler secrets download`: the caller's env with the inherited Doppler\n * vars scrubbed and OUR resolved token set explicitly.\n *\n * The token travels by ENV, never by argv: `ps` shows argv to every user on the box, and\n * `commandEcho` prints option values back to the terminal. Pure (takes the base env) so the scrub is\n * unit-testable without touching `process.env`.\n *\n * @example\n * buildDopplerChildEnv('dp.st.x', { PATH: '/bin', DOPPLER_CONFIG: 'prod' })\n * // => { PATH: '/bin', DOPPLER_TOKEN: 'dp.st.x' } (DOPPLER_CONFIG dropped)\n */\nexport const buildDopplerChildEnv = (token: string, baseEnv: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv => {\n const childEnv: NodeJS.ProcessEnv = { ...baseEnv }\n\n for (const name of INHERITED_DOPPLER_VARS) {\n delete childEnv[name]\n }\n\n childEnv.DOPPLER_TOKEN = token\n\n return childEnv\n}\n\nconst downloadDopplerSecrets = async (project: string, config: string): Promise<Array<[string, string]>> => {\n const { token } = await resolveEnvToken(config)\n\n const childEnv = buildDopplerChildEnv(token)\n\n const prevQuiet = $.quiet\n\n $.quiet = true\n try {\n let result\n\n try {\n result = await $({\n env: childEnv,\n })`doppler secrets download --no-file --format json --project ${project} --config ${config}`.timeout(\n DOPPLER_DOWNLOAD_TIMEOUT_MS,\n )\n } catch (error: unknown) {\n throw translateDopplerDownloadError(error, project, config)\n }\n\n assertDopplerOutputSize(result.stdout)\n\n const pairs = parseDopplerSecretsJson(result.stdout)\n\n assertTokenScope(pairs, config)\n\n return pairs\n } finally {\n $.quiet = prevQuiet\n }\n}\n\n/**\n * Defense in depth against a MIS-SCOPED token: the payload always carries `DOPPLER_CONFIG` as an\n * ordinary secret, and it echoes the `--config` we asked for (SPIKE-0 Q2 \u2014 it is the CONFIG, not\n * `DOPPLER_ENVIRONMENT`, which can differ: config `dev_personal` lives in environment `dev`).\n *\n * ABSENT \u21D2 PROCEED, deliberately. The Doppler CLI already refuses a real mismatch on the wire\n * (SPIKE-0 Q1: exit 1, \"does not have access to requested config\"), so it \u2014 not this \u2014 is the\n * load-bearing guard. An absent `DOPPLER_CONFIG` therefore means Doppler changed what it injects,\n * and failing closed here would blank every developer's shell at once on the SILENT autoload path.\n *\n * @example\n * assertTokenScope([['DOPPLER_CONFIG', 'dev']], 'dev') // ok\n * assertTokenScope([['FOO', 'bar']], 'dev') // ok (absent \u21D2 proceed)\n * assertTokenScope([['DOPPLER_CONFIG', 'prod']], 'dev') // throws: token is scoped to \"prod\"\n */\nexport const assertTokenScope = (pairs: Array<[string, string]>, config: string): void => {\n const actual = pairs.find(([key]) => {\n return key === DOPPLER_CONFIG_KEY\n })?.[1]\n\n if (actual === undefined || actual === config) return\n\n // A DopplerAuthError, not a plain Error: this IS an auth failure, and only the auth CLASS reaches the\n // sticky marker that a shell-startup user ever sees. A plain Error here would be classified transient,\n // expire in 30s, and leave them silently loading another environment's secrets \u2014 the exact failure this\n // assert exists to catch. The message stays richer than the generic one: we know the token's real config.\n throw new DopplerAuthError(\n config,\n null,\n `Doppler returned secrets for config \"${actual}\" but \"${config}\" was requested \u2014 the service token for ` +\n `env \"${config}\" is scoped to the wrong config. Refusing to load another environment's secrets.\\n` +\n `Fix: run \\`infra-kit env-token-set ${config}\\` with a token scoped to \"${config}\".`,\n )\n}\n\n/**\n * Turn a raw `doppler secrets download` failure into an actionable error. An AUTH-class failure (the\n * token is invalid, revoked, or mis-scoped) becomes a {@link DopplerAuthError} carrying the\n * env-token-set message; a recognized project/config not-found points at the exact infra-kit.json\n * field. Anything else \u2014 network, timeout \u2014 is rethrown untouched so it degrades to the existing\n * behavior, never worse.\n *\n * This is THE boundary where Doppler's raw stderr is read: the markers exist only here, and the\n * classification leaves as a TYPE (`DopplerAuthError`), never as prose for a downstream module to\n * re-parse. Exported so a seam test can drive the REAL error a caller sees.\n *\n * @example\n * translateDopplerDownloadError({ stderr: 'Doppler Error: Invalid Auth token' }, 'p', 'dev')\n * // => DopplerAuthError { authKind: 'revoked', message: '\u2026 `infra-kit env-token-set <env>` \u2026' }\n * translateDopplerDownloadError(new Error('connect ETIMEDOUT'), 'p', 'dev')\n * // => the same Error, untouched\n */\n// Why a type and not prose: `lib/env-autoload` used to text-match the raw Doppler markers on the\n// error this function had already rewritten \u2014 they were long gone, so the sticky auth marker was\n// never written and a revoked token went silent.\n//\n// The not-found message no longer lists the AVAILABLE names: `listDopplerProjects` /\n// `listDopplerConfigs` require an ACCOUNT login, which token-only auth deleted, so that probe\n// could only ever return `null` now. `buildDopplerNotFoundMessage` already treats `null` as\n// \"couldn't tell\" and omits the suggestion line rather than misreporting \"none exist\".\nexport const translateDopplerDownloadError = (error: unknown, project: string, config: string): Error => {\n const stderr = extractStderr(error) ?? (error instanceof Error ? error.message : String(error))\n const kind = classifyDopplerFailure(stderr)\n\n if (kind === 'auth') return new DopplerAuthError(config, classifyDopplerAuthFailure(stderr))\n\n if (kind === 'unknown') return error instanceof Error ? error : new Error(String(error))\n\n return new Error(buildDopplerNotFoundMessage({ kind, project, config, available: null }))\n}\n\nexport const assertDopplerOutputSize = (stdout: string): void => {\n const bytes = Buffer.byteLength(stdout, 'utf-8')\n\n if (bytes > DOPPLER_MAX_OUTPUT_BYTES) {\n throw new Error(\n `doppler returned unexpectedly large output (${bytes} bytes > ${DOPPLER_MAX_OUTPUT_BYTES}) \u2014 refusing to write to disk`,\n )\n }\n}\n\nexport const shellSingleQuote = (value: string): string => {\n const escaped = value.replaceAll(\"'\", \"'\\\\''\")\n\n return `'${escaped}'`\n}\n\n/** A valid POSIX env-var name: a letter or underscore, then word characters. */\nconst ENV_VAR_NAME_PATTERN = /^[A-Z_]\\w*$/i\n\n/**\n * Parse the `doppler secrets download --format json` output (a flat\n * `{\"KEY\":\"value\"}` map) into ordered `[key, value]` pairs, validating the shape\n * and every name/value. Values are returned RAW \u2014 callers must single-quote them\n * before emitting shell (an unquoted `a$(cmd)b` would execute on `source`).\n *\n * @example\n * parseDopplerSecretsJson('{\"FOO\":\"bar\",\"BAZ\":\"a$(x)\"}')\n * // => [['FOO', 'bar'], ['BAZ', 'a$(x)']]\n */\nexport const parseDopplerSecretsJson = (stdout: string): Array<[string, string]> => {\n let parsed: unknown\n\n try {\n parsed = JSON.parse(stdout)\n } catch {\n throw new Error(`doppler returned non-JSON output for env-load (got: ${JSON.stringify(stdout.slice(0, 80))})`)\n }\n\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n throw new Error('doppler returned unexpected JSON shape for env-load (expected an object of KEY: value pairs)')\n }\n\n const pairs: Array<[string, string]> = []\n\n for (const [key, value] of Object.entries(parsed)) {\n if (!ENV_VAR_NAME_PATTERN.test(key)) {\n throw new Error(\n `doppler returned an invalid env var name for env-load (got: ${JSON.stringify(key.slice(0, 80))})`,\n )\n }\n\n if (typeof value !== 'string') {\n throw new TypeError(`doppler returned a non-string value for \"${key}\" (got ${typeof value})`)\n }\n\n pairs.push([key, value])\n }\n\n if (pairs.length === 0) {\n throw new Error('doppler returned empty output for env-load')\n }\n\n return pairs\n}\n\n// MCP Tool Registration\nexport const envLoadMcpTool = defineMcpTool({\n name: 'env-load',\n description:\n 'Download the env vars for a Doppler config and write them to a temporary shell script. Does NOT mutate the calling process \u2014 returns the path to a script that must be sourced (\"source <filePath>\") for the vars to take effect. The infra-kit shell wrapper auto-sources; direct MCP callers must handle sourcing themselves or surface filePath to the user. \"config\" is required when invoked via MCP (the CLI interactive picker is unreachable without a TTY).',\n inputSchema: {\n config: z\n .string()\n .describe('Doppler config / environment name to load (e.g. \"dev\", \"arthur\", \"renana\"). Required for MCP calls.'),\n },\n outputSchema: {\n filePath: z.string().describe('Path to the file that must be sourced to apply variables'),\n variableCount: z.number().describe('Number of variables loaded'),\n project: z.string().describe('Doppler project name'),\n config: z.string().describe('Doppler config name'),\n },\n handler: envLoad,\n})\n", "/**\n * @fileoverview\n *\n * Translate Doppler `secrets download` not-found failures into actionable,\n * point-at-the-fix guidance. Pure (no I/O) so the classification + message policy\n * is unit-testable without spawning the Doppler CLI \u2014 mirroring the pure-helper\n * pattern in `commands/env-load/env-load.ts`.\n *\n * Observed Doppler output on a not-found download (verified 2026-06-27, doppler\n * v3.76.0), exit 1, written to STDERR (stdout empty):\n * Unable to download secrets\n * Doppler Error: Could not find requested project 'name'\n * Doppler Error: Could not find requested config 'name'\n * Only the \"Doppler Error:\" prefix is ANSI-colored; the \"Could not find requested\n * ...\" text is plain, so substring matching is color-safe. zx's\n * `ProcessOutput.message` also concatenates stdout+stderr, so the same markers are\n * matchable off `.message` when `.stderr` is unavailable.\n */\nimport { EnvAuthError } from 'src/lib/errors/env-auth-error'\n\n/** Which kind of Doppler not-found a download failure represents (`unknown` = not a not-found error). */\nexport type DopplerNotFoundKind = 'project' | 'config' | 'unknown'\n\n/**\n * Every class of `secrets download` failure we act on. `'auth'` is the token-only addition: the\n * service token is absent, revoked, garbage, or scoped to a DIFFERENT config. It is deliberately\n * distinct from the not-found kinds (the name is wrong) and from `'unknown'` (network, timeout,\n * Doppler down) \u2014 only `'auth'` is a durable, user-fixable state, and only `'auth'` earns the sticky\n * shell-startup marker (see `lib/env-autoload/auth-failure.ts`).\n */\nexport type DopplerFailureKind = 'auth' | DopplerNotFoundKind\n\nconst PROJECT_NOT_FOUND_MARKER = 'Could not find requested project'\nconst CONFIG_NOT_FOUND_MARKER = 'Could not find requested config'\n\n/**\n * The REAL auth-class stderr markers, captured from a live Doppler CLI v3.76.0 (SPIKE-0 Q1/Q4):\n * - `Invalid Auth token` \u2014 revoked / garbage token\n * - `This token does not have access to requested config 'x'` \u2014 mis-scoped token\n * Both are plain (uncolored) text, so substring matching is ANSI-safe. The second one is the CLI\n * ENFORCING the token's scope for us \u2014 it is the primary mis-scope guard, not our payload assert.\n */\nconst INVALID_TOKEN_MARKER = 'Invalid Auth token'\nconst TOKEN_SCOPE_MARKER = 'does not have access to requested config'\n\n/**\n * Classify a Doppler `secrets download` failure from its stderr/message text. The full lattice:\n * auth-class first (a mis-scoped token's message names a config but is NOT a not-found), then the\n * two not-found kinds, then `'unknown'` for everything else so the caller can rethrow the original\n * untouched (degrades to status quo, never worse).\n *\n * @example\n * classifyDopplerFailure('Doppler Error: Invalid Auth token') // => 'auth'\n * classifyDopplerFailure(\"This token does not have access to requested config 'dev'\") // => 'auth'\n * classifyDopplerFailure(\"Doppler Error: Could not find requested project 'x'\") // => 'project'\n * classifyDopplerFailure('connect ETIMEDOUT') // => 'unknown'\n */\nexport const classifyDopplerFailure = (stderr: string): DopplerFailureKind => {\n if (stderr.includes(INVALID_TOKEN_MARKER) || stderr.includes(TOKEN_SCOPE_MARKER)) return 'auth'\n\n if (stderr.includes(PROJECT_NOT_FOUND_MARKER)) return 'project'\n\n if (stderr.includes(CONFIG_NOT_FOUND_MARKER)) return 'config'\n\n return 'unknown'\n}\n\n/**\n * The two auth-class failures, told apart. Both are `'auth'` to\n * {@link classifyDopplerFailure} \u2014 they share one fix (a new token) \u2014 but they are DIFFERENT\n * diagnoses, and `doctor` must not conflate them: \"your token was revoked\" sends a user to the\n * Doppler dashboard, \"your token is scoped to the wrong config\" sends them to re-issue it against\n * the right one.\n */\nexport type DopplerAuthKind = 'revoked' | 'mis-scoped'\n\n/**\n * Split an auth-class failure into its two diagnoses, or `null` when the text is not auth-class at\n * all (network, timeout, not-found) \u2014 the caller must not read `null` as \"the token is fine\", only\n * as \"this failure says nothing about the token\".\n *\n * @example\n * classifyDopplerAuthFailure(\"This token does not have access to requested config 'dev'\") // => 'mis-scoped'\n * classifyDopplerAuthFailure('Doppler Error: Invalid Auth token') // => 'revoked'\n * classifyDopplerAuthFailure('connect ETIMEDOUT') // => null\n */\nexport const classifyDopplerAuthFailure = (stderr: string): DopplerAuthKind | null => {\n if (stderr.includes(TOKEN_SCOPE_MARKER)) return 'mis-scoped'\n\n if (stderr.includes(INVALID_TOKEN_MARKER)) return 'revoked'\n\n return null\n}\n\n/**\n * Is this failure text an auth-class Doppler failure (bad / absent / mis-scoped service token)\n * rather than a transient one (network, timeout, Doppler down)? The single source of truth for that\n * question \u2014 `lib/env-autoload/auth-failure.ts` delegates its `isAuthFailure` default here.\n *\n * @example\n * isDopplerAuthFailure('Doppler Error: Invalid Auth token') // => true\n * isDopplerAuthFailure('network unreachable') // => false\n */\nexport const isDopplerAuthFailure = (stderr: string): boolean => {\n return classifyDopplerFailure(stderr) === 'auth'\n}\n\n/**\n * Classify a Doppler download failure into the NOT-FOUND lattice only. Auth-class failures collapse\n * to `'unknown'` here on purpose: this predicate's whole contract is \"is this a wrong-NAME error\",\n * and its callers enrich with project/config listings that an auth failure must never trigger. Use\n * {@link classifyDopplerFailure} when you need the full lattice.\n *\n * @example\n * classifyDopplerDownloadError(\"Doppler Error: Could not find requested project 'x'\") // => 'project'\n * classifyDopplerDownloadError(\"Doppler Error: Could not find requested config 'dev'\") // => 'config'\n * classifyDopplerDownloadError('Doppler Error: Invalid Auth token') // => 'unknown'\n */\nexport const classifyDopplerDownloadError = (stderr: string): DopplerNotFoundKind => {\n const kind = classifyDopplerFailure(stderr)\n\n return kind === 'auth' ? 'unknown' : kind\n}\n\n/**\n * The actionable body for an auth-class download failure: Doppler REFUSED the token we sent, so the\n * fix is always a new token for this env \u2014 never `doppler login` (account auth no longer exists).\n *\n * Deliberately does not interpolate the raw stderr: this message is user-facing, and the one rule a\n * credential path cannot bend is that nothing token-adjacent gets rendered by accident.\n *\n * @example\n * buildDopplerAuthFailureMessage('dev')\n * // => 'Doppler rejected the service token for env \"dev\" \u2026 run `infra-kit env-token-set <env>` \u2026'\n */\nexport const buildDopplerAuthFailureMessage = (env: string): string => {\n return [\n `Doppler rejected the service token for env \"${env}\" \u2014 it is invalid, revoked, or scoped to a different config.`,\n `Fix: run \\`infra-kit env-token-set ${env}\\` with a token scoped to that config.`,\n 'CI / agents: set INFRA_KIT_ENV_TOKEN to a token scoped to that config.',\n ].join('\\n')\n}\n\n/**\n * An auth-class `secrets download` failure, as a TYPE rather than a string. This is the error\n * `env-load`'s `translateDopplerDownloadError` throws for the auth class, and it is what every\n * DOWNSTREAM consumer classifies on.\n *\n * It is the DOPPLER-REFUSED-US subset of {@link EnvAuthError}, the durable/user-fixable env-auth\n * class. Downstream policy (the sticky auth marker) therefore classifies on `isEnvAuthFailure`;\n * `isDopplerAuthError` remains for the callers who genuinely mean \"the token was SENT and Doppler\n * rejected it\". The message is unchanged \u2014 {@link buildDopplerAuthFailureMessage} still renders\n * it \u2014 so a human who only ever sees `error.message` sees precisely what they saw before.\n *\n * @example\n * throw new DopplerAuthError('dev', 'revoked')\n * // message: 'Doppler rejected the service token for env \"dev\" \u2026 `infra-kit env-token-set <env>` \u2026'\n */\n// WHY a class and not \"match the message\": the moment the raw Doppler stderr is translated into a\n// human-facing message the markers it was classified by are GONE. A consumer that re-derives the\n// class by re-parsing that message is coupled to prose owned by another module \u2014 and the coupling\n// is SILENT: reword the message and the auth channel simply stops firing, with every unit test on\n// both sides still green (exactly the defect this type exists to make impossible). The\n// classification now travels WITH the error.\n//\n// Why the subset framing: a missing token never reaches Doppler, and a corrupt token store never\n// reaches the resolver, yet all three are equally durable and equally the user's to fix.\n//\n// The @example names the command as `infra-kit env-token-set <env>` rather than with a literal arg\n// for `program.test.ts`'s benefit: to that scan an `<env>` placeholder ENDS the command, but a\n// literal `dev` is indistinguishable from a subcommand and would be reported as a dead one.\nexport class DopplerAuthError extends EnvAuthError {\n /**\n * The env (= Doppler config) whose service token Doppler refused. Names the fix, carries no token.\n *\n * `declare` NARROWS the base's `string | null` (a Doppler refusal always names one env) without\n * re-declaring the field: under `useDefineForClassFields` a real re-declaration would `defineProperty`\n * it back to `undefined` AFTER `super()` had set it.\n */\n declare readonly env: string\n /**\n * Which of the two auth diagnoses the raw stderr indicated, or `null` when it was not stated\n * (e.g. the payload-scope assert, which never sees Doppler's stderr). Diagnostic only \u2014 every\n * auth kind shares one fix.\n */\n readonly authKind: DopplerAuthKind | null\n\n /**\n * `message` overrides the rendered text for a caller that knows MORE than the generic diagnosis \u2014\n * the payload-scope assert can name the config the token actually belongs to, which Doppler's own\n * refusal cannot. It never changes the CLASS: every auth failure must remain type-detectable, or it\n * degrades to the transient 30s marker and the user is never told (the defect this type exists to\n * make impossible).\n */\n constructor(env: string, authKind: DopplerAuthKind | null = null, message?: string) {\n super(message ?? buildDopplerAuthFailureMessage(env), env)\n this.name = 'DopplerAuthError'\n this.authKind = authKind\n }\n}\n\n/**\n * The NARROW type guard: \"we sent Doppler a token and Doppler refused it\". Not text-matching \u2014 that\n * is how a caller asks \"was this the token?\" without re-parsing a rendered message.\n *\n * Use {@link isEnvAuthFailure} instead when the question is the POLICY one \u2014 \"is this durable and the\n * user's to fix?\" \u2014 because a missing token and a corrupt token store are both of those and neither is\n * a Doppler refusal. Classifying auto-load policy on THIS guard is precisely how the missing-token\n * (migration) case shipped silent.\n *\n * @example\n * isDopplerAuthError(new DopplerAuthError('dev')) // => true\n * isDopplerAuthError(new Error('Doppler Error: Invalid Auth token')) // => false (untranslated: not ours to claim)\n */\nexport const isDopplerAuthError = (error: unknown): error is DopplerAuthError => {\n return error instanceof DopplerAuthError\n}\n\ninterface DopplerNotFoundMessageArgs {\n /** Whether the missing entity is the project or the config within it. */\n kind: 'project' | 'config'\n project: string\n config: string\n /**\n * Names to suggest. `null` = lookup failed/unavailable, `[]` = none exist; both\n * omit the suggestion line so a failed enrichment probe is never misreported as\n * a definitive \"none available\".\n */\n available?: string[] | null\n}\n\n/**\n * Build the actionable, multi-line error body for a Doppler not-found failure:\n * states what's missing, points at the exact infra-kit.json field, lists available\n * names when known, and gives a concrete fix.\n *\n * @example\n * buildDopplerNotFoundMessage({ kind: 'project', project: 'infra-kit', config: 'dev', available: ['nomadream'] })\n * // => 'Doppler project \"infra-kit\" not found (set in infra-kit.json \u2192 envManagement.config.name).\\nAvailable projects: nomadream.\\nFix: ...'\n */\nexport const buildDopplerNotFoundMessage = ({\n kind,\n project,\n config,\n available,\n}: DopplerNotFoundMessageArgs): string => {\n const lines: string[] =\n kind === 'project'\n ? [`Doppler project \"${project}\" not found (set in infra-kit.json \u2192 envManagement.config.name).`]\n : [`Doppler config \"${config}\" not found in project \"${project}\".`]\n\n if (available && available.length > 0) {\n const label = kind === 'project' ? 'Available projects' : `Available configs in \"${project}\"`\n\n lines.push(`${label}: ${available.join(', ')}.`)\n }\n\n // The config half no longer points at a config file: there IS no declared env list any more. The name\n // came from a workflow, a token, or the `--config` flag, and the only place to fix a genuinely missing\n // one is Doppler itself.\n lines.push(\n kind === 'project'\n ? 'Fix: update envManagement.config.name to an existing project, or create it in Doppler.'\n : 'Fix: pass an existing config, or create it in Doppler.',\n )\n\n return lines.join('\\n')\n}\n", "import { $ } from 'zx'\n\nimport { classifyDopplerAuthFailure, classifyDopplerFailure } from 'src/integrations/doppler/doppler-errors'\nimport { extractStderr } from 'src/lib/errors/operation-error'\nimport { getInfraKitConfig } from 'src/lib/infra-kit-config'\n\n/**\n * Hard upper bound for the Doppler probe. It runs only on the diagnostic path (`doctor`), but a hung\n * VPN/connection must not stall it.\n */\nconst DOPPLER_PROBE_TIMEOUT_MS = 5_000\n\n/**\n * The one secret every Doppler payload carries, whose value is the config the token is scoped to\n * (SPIKE-0 Q2 \u2014 the CONFIG, not `DOPPLER_ENVIRONMENT`, which can differ: config `dev_personal` lives\n * in environment `dev`). Reading it back is what turns a liveness probe into a SCOPE probe.\n */\nconst SCOPE_KEY = 'DOPPLER_CONFIG'\n\n/**\n * Resolve Doppler project name from infra-kit.json at the project root\n */\nexport const getDopplerProject = async (): Promise<string> => {\n const { envManagement } = await getInfraKitConfig()\n\n return envManagement.config.name\n}\n\n/**\n * What a service token turned out to be, once Doppler was asked.\n *\n * `'unreachable'` is the load-bearing one and it means \"COULDN'T TELL\", never \"bad\": network,\n * timeout, VPN, Doppler down, a CLI that isn't installed. A caller that renders it as a definitive\n * verdict would tell a developer on a plane that their token was revoked. (Same `null`-vs-`[]`\n * contract the deleted `listDopplerProjects` carried, in enum form.)\n */\nexport type EnvTokenProbeOutcome = 'valid' | 'revoked' | 'mis-scoped' | 'unreachable'\n\nexport interface EnvTokenProbe {\n outcome: EnvTokenProbeOutcome\n /** The config the token actually resolved to, when Doppler told us. Never a token value. */\n scopedTo?: string\n}\n\ninterface EnvTokenProbeArgs {\n /**\n * The child environment carrying the token \u2014 built by `buildDopplerChildEnv`, which scrubs the\n * inherited `DOPPLER_*` vars and sets ours. Passed in rather than built here so the token travels\n * by ENV and never by argv (`ps` shows argv to every user on the box), and so this module stays\n * free of a cycle back through `commands/env-load`.\n */\n childEnv: NodeJS.ProcessEnv\n project: string\n config: string\n}\n\n/**\n * The cheapest combined VALIDITY + SCOPE probe of a Doppler service token (SPIKE-0): read one secret\n * back \u2014 the payload's own `DOPPLER_CONFIG` \u2014 and compare it to the config we asked for.\n *\n * `doppler me` is NOT enough: it works under a service token but returns no project/config, so it can\n * only prove the token is alive, never that it points where we think. One `secrets get` answers both\n * questions in one round trip, and the CLI enforces the scope for us on the wire.\n *\n * Never throws. Every failure becomes an outcome, and an unrecognized one becomes `'unreachable'`\n * (\"couldn't tell\") rather than a guess \u2014 the caller is `doctor`, whose whole value is that its\n * verdicts are true.\n *\n * @example\n * await probeEnvToken({ childEnv, project: 'api', config: 'dev' })\n * // => { outcome: 'valid', scopedTo: 'dev' }\n * // revoked token => { outcome: 'revoked' }\n * // token for prod => { outcome: 'mis-scoped' }\n * // offline => { outcome: 'unreachable' }\n */\nexport const probeEnvToken = async ({ childEnv, project, config }: EnvTokenProbeArgs): Promise<EnvTokenProbe> => {\n const prevQuiet = $.quiet\n\n $.quiet = true\n try {\n const result = await $({\n env: childEnv,\n })`doppler secrets get ${SCOPE_KEY} --plain --project ${project} --config ${config}`.timeout(\n DOPPLER_PROBE_TIMEOUT_MS,\n )\n const scopedTo = result.stdout.trim()\n\n // Exit 0 with a DIFFERENT config would mean the CLI's own scope enforcement let a mis-scoped token\n // through \u2014 belt and braces, but this is the check that would catch it.\n if (scopedTo !== config) return { outcome: 'mis-scoped', scopedTo }\n\n return { outcome: 'valid', scopedTo }\n } catch (error: unknown) {\n return { outcome: classifyProbeFailure(error) }\n } finally {\n $.quiet = prevQuiet\n }\n}\n\n/**\n * Turn a failed probe into an outcome. Only the two auth-class markers are definitive; a\n * project/config not-found is a config bug rather than a token verdict, so it collapses to\n * `'unreachable'` too \u2014 the token check must not claim a token is bad because the PROJECT name is.\n */\nconst classifyProbeFailure = (error: unknown): EnvTokenProbeOutcome => {\n const stderr = extractStderr(error) ?? (error instanceof Error ? error.message : String(error))\n\n if (classifyDopplerFailure(stderr) !== 'auth') return 'unreachable'\n\n return classifyDopplerAuthFailure(stderr) ?? 'unreachable'\n}\n", "import process from 'node:process'\n\nimport { getTokenStorePath, readTokenStore } from 'src/lib/env-tokens'\nimport { EnvAuthError } from 'src/lib/errors/env-auth-error'\nimport { tildify } from 'src/lib/path-display'\n\n/**\n * The ONE environment variable that carries a Doppler service token into infra-kit (the CI / agent\n * path: no home directory, no token store, no interactive `env-token-set`).\n *\n * Deliberately NOT named `DOPPLER_TOKEN`. That name is contaminated on both ends: a Doppler project\n * may itself hold a secret called `DOPPLER_TOKEN`, and `env-load` exports the WHOLE project into the\n * shell \u2014 so a `DOPPLER_TOKEN` in the environment cannot be trusted to be the token WE were handed\n * (SPIKE-0 Q5: an inherited `DOPPLER_TOKEN` silently overrides everything else the CLI would use).\n * A private name keeps the input channel ours.\n */\nexport const INFRA_KIT_ENV_TOKEN_VAR = 'INFRA_KIT_ENV_TOKEN'\n\n/** Where a resolved token came from. Surfaced by `env-token-list`; never carries the token itself. */\nexport type EnvTokenSource = 'env' | 'store'\n\nexport interface ResolvedEnvToken {\n /** The Doppler service token. NEVER log, echo, or write this into a sourced file. */\n token: string\n source: EnvTokenSource\n}\n\n/**\n * Resolve the Doppler service token for one environment (= one Doppler config).\n *\n * Precedence, highest first:\n * 1. `INFRA_KIT_ENV_TOKEN` \u2014 the CI / agent channel. Checked BEFORE the store so a machine with no\n * home config (or a deliberately corrupted one) still loads, and so nothing on that path can\n * throw on a store it never needed.\n * 2. `envs[env]` from `~/.infra-kit/projects/<repo>/tokens.json` \u2014 the developer channel.\n * 3. Nothing \u21D2 THROW an {@link EnvAuthError}. Token-only auth has no fallback: there is no\n * `doppler login` account to degrade to, so a missing token is a hard, actionable stop rather than\n * a mysterious empty env. The CLASS matters as much as the message \u2014 see the throw below.\n *\n * An EMPTY `INFRA_KIT_ENV_TOKEN` is a MISS, not an empty token: `export INFRA_KIT_ENV_TOKEN=` (or an\n * unset var expanded by a CI template) must fall through to the store, not authenticate as \"\".\n *\n * @example\n * await resolveEnvToken('dev') // => { token: 'dp.st.dev.xxxx', source: 'store' }\n * // with INFRA_KIT_ENV_TOKEN set => { token: <that value>, source: 'env' }\n * // with neither => throws 'No Doppler service token for env \"dev\". \u2026'\n */\nexport const resolveEnvToken = async (env: string): Promise<ResolvedEnvToken> => {\n const fromEnv = process.env[INFRA_KIT_ENV_TOKEN_VAR]\n\n if (fromEnv) return { token: fromEnv, source: 'env' }\n\n const store = await readTokenStore()\n const fromStore = store?.envs[env]\n\n if (fromStore) return { token: fromStore, source: 'store' }\n\n // An EnvAuthError, not a plain Error: \"no token\" is the DURABLE, user-fixable auth failure \u2014 the\n // migration case, and the one case env auto-load's sticky marker exists to survive. A plain Error\n // here is classified TRANSIENT downstream, expires with the 30s backoff, and (on the backgrounded\n // shell-startup spawn, whose stderr is discarded) leaves the user with no channel at all: their env\n // silently stops loading and nothing ever says why. The class is what reaches them.\n throw new EnvAuthError(buildMissingTokenMessage(env, await getTokenStorePath()), env)\n}\n\n/**\n * Whether a token resolves for one env \u2014 the same precedence as {@link resolveEnvToken}, without\n * producing the token and without the refusal.\n *\n * For callers that must DECIDE rather than authenticate: env auto-load has to answer \"is this env\n * usable?\" at shell startup, and asking by catching {@link resolveEnvToken}'s throw would conflate two\n * very different failures \u2014 \"no token for this env\" (disable quietly, a typo'd `envAutoLoad.config`)\n * and \"the store itself is broken\" (surface loudly). This propagates the latter and reports the former\n * as a plain `false`.\n *\n * @example\n * await envTokenExists('dev') // => true\n * await envTokenExists('typo') // => false\n * // corrupt store => throws EnvAuthError (deliberately \u2014 the caller must not swallow that)\n */\nexport const envTokenExists = async (env: string): Promise<boolean> => {\n if (process.env[INFRA_KIT_ENV_TOKEN_VAR]) return true\n\n const store = await readTokenStore()\n\n return Boolean(store?.envs[env])\n}\n\n/**\n * The no-token refusal. It is the first thing an un-migrated user sees at update time, so it must be\n * fixable without reading any docs: it names the env, where to mint a token, the exact command, where\n * the token lands (and that it is NOT the repo), and the CI escape hatch.\n *\n * The fix command is a BACKTICK code span \u2014 the repo convention for naming a command to a human, and\n * the anchor `program.test.ts` scans to prove every command we print is a command we accept.\n *\n * (The @example names the command as `infra-kit env-token-set <env>` rather than with a literal arg\n * for that scan's benefit: a `<env>` placeholder ENDS the command, but a literal `dev` is\n * indistinguishable from a subcommand and would be reported as a dead one.)\n *\n * @example\n * buildMissingTokenMessage('dev', '/Users/x/.infra-kit/projects/api/tokens.json')\n * // => 'No Doppler service token for env \"dev\". Mint one at https://dashboard.doppler.com \u2026 run `infra-kit env-token-set <env>` \u2026'\n */\nconst buildMissingTokenMessage = (env: string, storePath: string): string => {\n return [\n `No Doppler service token for env \"${env}\".`,\n `Mint one at https://dashboard.doppler.com \u2014 this project \u2192 config \"${env}\" \u2192 Access tab \u2192 \"Generate Service Token\".`,\n `Fix: run \\`infra-kit env-token-set ${env}\\` and paste it.`,\n `It is stored in ${tildify(storePath)} (mode 0600, never in the repo).`,\n `CI / agents: set ${INFRA_KIT_ENV_TOKEN_VAR} instead \u2014 it takes precedence and needs no home config.`,\n ].join('\\n')\n}\n", "import { OperationError } from 'src/lib/errors/operation-error'\n\n/**\n * Environments that are NOT ordinary ad-hoc deploy targets by default.\n *\n * This is the DEFAULT, not an immovable policy: a project opts out with `protectedEnvs` in its\n * `infra-kit.json` (`'disallow' | 'allow' | 'cli-only'`, absent meaning `'disallow'`), resolved by\n * `protected-env-access.ts`. The LIST stays in code because it answers \"what is delivery-shaped in\n * this org\", which no single repo owns; the config answers \"may this repo reach it\", which each\n * repo does.\n */\n// `prod` is not a deploy target \u2014 it is a DELIVERY target. `gh-release-deliver` is the sanctioned\n// path to it, and it is a whole flow, not a dispatch: squash-merge the release branch to `main`,\n// open and merge the RC PR, dispatch `deploy-all.yml --ref main -f environment=prod` (see\n// `gh-release-deliver.ts`), then release the matching Jira fix version. A `gh-release-deploy-all\n// --env prod` straight from a release branch skips every one of those steps and ships production\n// from an unmerged ref.\n//\n// `stage` is deliberately NOT here. The old per-repo allowlists excluded it, but nothing delivers\n// to it either \u2014 so it was simply unreachable, which was an oversight rather than a policy. It is\n// an ad-hoc deploy target like any other.\n//\n// What is deliberately NOT rebuilt here is the deleted `environments` key \u2014 a hand-maintained\n// per-repo copy of each workflow's own `environment.options` that drifted from them and turned\n// that drift into a REFUSAL to deploy.\nexport const DEFAULT_PROTECTED_ENVS = ['prod']\n\n/**\n * A project's resolved access to {@link DEFAULT_PROTECTED_ENVS}, and WHY.\n *\n * The reason is not decoration: a refusal because the project disallows protected envs and a refusal\n * because the caller is an agent under `'cli-only'` need opposite remediation. Collapsed to a bare\n * boolean, the second case emits \"prod is delivered, not deployed\" and sends an agent off to run the\n * delivery flow, or to report that prod is unconfigured for the project.\n *\n * Declared here, in the pure leaf, so `protected-env-access.ts` depends on this module and never the\n * reverse.\n */\nexport interface ProtectedEnvAccess {\n allowed: boolean\n reason: 'disallow' | 'mcp-blocked' | 'allowed'\n}\n\n/**\n * No access \u2014 the DEFAULT for every function below.\n *\n * That default is the safety property of this module: a call site that forgets to thread the resolved\n * access refuses rather than permits, so a seventh caller is fail-closed until someone deliberately\n * opts it in.\n */\nexport const PROTECTED_ENV_DENIED: ProtectedEnvAccess = { allowed: false, reason: 'disallow' }\n\n/** Whether an environment is delivery-shaped by default, before any project opt-out. */\nexport const isProtectedEnv = (env: string): boolean => {\n return DEFAULT_PROTECTED_ENVS.includes(env)\n}\n\n/**\n * Environments the whole team shares. Everything else in a deploy picker is somebody's personal\n * account (`arthur`, `renana`, \u2026), where a dirty tree is the normal case rather than a hazard.\n */\nconst SHARED_ENVS = ['dev', 'stage']\n\n/**\n * Whether this environment belongs to other people \u2014 the input to `local-deploy`'s confirmation\n * prompt and, via `runPreflight`, its dirty-tree refusal, and to the argument form's `env` prose.\n *\n * A delivery-shaped env is shared by definition, more so than `dev`, yet it is deliberately NOT in\n * {@link SHARED_ENVS}: until a project could reach `prod` at all the question never arose, because\n * `assertDeployable` refused first. Now that a project can allow it, resolving from the list alone\n * would hand production the WEAKER treatment of a personal environment \u2014 a y/N prompt and no\n * clean-tree check, i.e. shipping an uncommitted working tree to prod.\n *\n * @example\n * isSharedEnv('dev') // => true\n * isSharedEnv('prod') // => true \u2014 protected, therefore shared\n * isSharedEnv('arthur') // => false\n */\n// Lives in this leaf rather than in `local-deploy.ts`, where it was written, because the deploy\n// argument form partitions its env dropdown's `.describe()` with it. The form provider is wired into\n// `local-deploy`'s own tool definitions, so importing it from there would close a cycle \u2014 and a\n// second hand-written list of shared envs in the provider is exactly the drift the plan requires the\n// prose be derived to avoid. Extracted rather than inlined so the resolution stays testable:\n// asserting `assertCleanTreeForSharedEnv({ isShared: true, \u2026 })` hand-passes the value under test and\n// passes whether or not the bug exists.\nexport const isSharedEnv = (env: string): boolean => {\n return SHARED_ENVS.includes(env) || isProtectedEnv(env)\n}\n\n/**\n * The ad-hoc deploy picker: a workflow's declared options, minus what this project may not reach.\n *\n * @example\n * deployableEnvs(['dev', 'prod']) // => ['dev']\n * deployableEnvs(['dev', 'prod'], { allowed: true, reason: 'allowed' }) // => ['dev', 'prod']\n */\nexport const deployableEnvs = (options: string[], access: ProtectedEnvAccess = PROTECTED_ENV_DENIED): string[] => {\n if (access.allowed) return options\n\n return options.filter((env) => {\n return !isProtectedEnv(env)\n })\n}\n\n/**\n * Refuse a protected environment this project may not reach, naming the fix for THIS refusal.\n *\n * GitHub cannot enforce either rule for us \u2014 from its side `-f environment=prod` is a perfectly valid\n * choice \u2014 so both are ours. Note what is NOT vetoed: an env absent from the local YAML still\n * dispatches, because that read is of the working tree while the run targets `--ref <branch>`, and\n * GitHub validates the choice itself.\n *\n * @example\n * assertDeployable('dev', 'launch deploy-all workflow') // ok\n * assertDeployable('prod', 'launch deploy-all workflow') // throws \u2014 use `infra-kit release deliver`\n */\nexport const assertDeployable = (\n env: string,\n operation: string,\n access: ProtectedEnvAccess = PROTECTED_ENV_DENIED,\n): void => {\n if (access.allowed) return\n if (!isProtectedEnv(env)) return\n\n if (access.reason === 'mcp-blocked') {\n throw new OperationError(undefined, {\n operation,\n remediation:\n `run it yourself in a terminal \u2014 this project sets \\`protectedEnvs: \"cli-only\"\\`, which ` +\n `deliberately withholds \"${env}\" from agents while allowing it on the CLI`,\n stderrExcerpt: `\"${env}\" is not reachable over MCP in this project`,\n })\n }\n\n throw new OperationError(undefined, {\n operation,\n remediation:\n `use \\`infra-kit release deliver\\` \u2014 it merges the release to main, opens the RC PR, ` +\n `deploys from main, and releases the Jira version. To allow ad-hoc deploys to \"${env}\" in this ` +\n `project, set \\`protectedEnvs\\` to \"allow\" or \"cli-only\" in infra-kit.json`,\n stderrExcerpt: `\"${env}\" is delivered, not deployed ad-hoc`,\n })\n}\n", "import { getInfraKitConfig } from 'src/lib/infra-kit-config'\nimport { isMcpMode } from 'src/lib/mcp-mode'\n\nimport { PROTECTED_ENV_DENIED } from './protected-envs'\nimport type { ProtectedEnvAccess } from './protected-envs'\n\n/**\n * Resolve whether THIS project may reach the delivery-shaped environments, and why not when it may\n * not. The single place the three-value `protectedEnvs` setting is interpreted \u2014 every call site\n * downstream sees the resolved {@link ProtectedEnvAccess} and nothing else.\n *\n * NEVER THROWS. A repo with no `infra-kit.json`, an unreadable one, or one the merge chain rejects\n * all resolve to denied.\n *\n * @example\n * await resolveProtectedEnvAccess() // no `protectedEnvs` key => { allowed: false, reason: 'disallow' }\n */\n// The only impure member of the protected-env pair: it reads the merged config and the MCP-mode\n// flag, so `protected-envs.ts` stays a dependency-free policy leaf with a mock-free unit test.\n//\n// Refusing a protected env on a config we cannot read is correct; turning every deploy into a\n// config-parse error is not \u2014 and `getInfraKitConfig` genuinely throws for a missing file.\n//\n// `isMcpMode()` is called INSIDE this function on purpose. `mcpMode` is a mutable object read at\n// call time, and the flag is assigned during MCP server bootstrap (`mcp/server.ts`), which happens\n// after this module is imported. Hoisting the read to module scope \u2014 `const inMcp =\n// mcpMode.enabled` at the top \u2014 would freeze `false` and silently degrade `'cli-only'` into\n// `'allow'` for every agent.\nexport const resolveProtectedEnvAccess = async (): Promise<ProtectedEnvAccess> => {\n let setting\n\n try {\n setting = (await getInfraKitConfig()).protectedEnvs ?? 'disallow'\n } catch {\n return PROTECTED_ENV_DENIED\n }\n\n if (setting === 'disallow') return PROTECTED_ENV_DENIED\n\n if (setting === 'cli-only' && isMcpMode()) {\n return { allowed: false, reason: 'mcp-blocked' }\n }\n\n return { allowed: true, reason: 'allowed' }\n}\n", "import { logger } from 'src/lib/logger'\n\nimport { isProtectedEnv } from './protected-envs'\n\n/**\n * Warn, at selection time, about what an ad-hoc dispatch to a delivery-shaped environment SKIPS.\n *\n * This is the knowledge the refusal in `assertDeployable` used to carry. A project that sets\n * `protectedEnvs` to `\"allow\"` removes the block, and the block was the only place in the codebase\n * recording WHY `prod` was special \u2014 so the sentence has to outlive it, or the next person learns the\n * hard way that `release deploy-all --env prod` is `release deliver` minus its bookkeeping.\n *\n * Deliberately a warning and not a second confirmation: the project already decided, and a prompt the\n * user cannot act on differently is just friction. The dispatch commands run their own `confirmDeploy`\n * immediately after this.\n *\n * Shared by both dispatch commands rather than copied: `gh-release-deploy-selected` already carries a\n * cognitive-complexity suppression, and this must not add to it.\n *\n * @example\n * warnProtectedEnvDispatch({ env: 'prod', branch: 'release/1.9.0' }) // logs the skipped steps\n * warnProtectedEnvDispatch({ env: 'dev', branch: 'release/1.9.0' }) // no-op\n */\nexport const warnProtectedEnvDispatch = (args: { env: string; branch: string }): void => {\n const { env, branch } = args\n\n if (!isProtectedEnv(env)) return\n\n logger.warn(\n [\n `\u26A0\uFE0F \"${env}\" is a delivery target \u2014 this project allows dispatching to it directly, but this is NOT the delivery flow.`,\n `Dispatching from \"${branch}\" ships ${env} from a ref that is not merged to main, and skips:`,\n ' \u2022 the squash-merge of the release branch into main',\n ' \u2022 the RC pull request',\n ' \u2022 the Jira fix-version release',\n 'Run `infra-kit release deliver` instead if you want those.',\n ].join('\\n'),\n )\n}\n", "import fs from 'node:fs/promises'\nimport path from 'node:path'\nimport yaml from 'yaml'\n\nimport { getProjectRoot } from 'src/lib/git-utils'\n\n/** Where GitHub keeps dispatchable workflows. The only place we look for environment declarations. */\nconst WORKFLOWS_DIR = '.github/workflows'\n\n/**\n * The environment list a workflow will accept, read from its own `workflow_dispatch` `choice`\n * input.\n *\n * ADVISORY ONLY: it sources the interactive picker, and callers must NOT veto against it. The value\n * comes from the WORKING TREE while the dispatch targets `--ref <branch>`, so the two can\n * legitimately differ and an explicit `--env` must always win.\n *\n * Never throws. A repo with no such workflow (bridge has none), a `type: string` input, an absent\n * `options:` key, or malformed YAML all yield `[]` \u2014 an empty picker, never a dead command.\n *\n * @example\n * await readWorkflowEnvOptions('deploy-all.yml') // => ['dev', 'arthur', 'stage', 'prod']\n * await readWorkflowEnvOptions('absent.yml') // => []\n */\n// This replaces `infra-kit.json \u2192 environments`, which was a THIRD copy of a fact GitHub already\n// owns and already enforces: a `choice` input is validated server-side on dispatch, so an\n// out-of-range value is rejected by GitHub whether or not we check it here. The copy had drifted \u2014\n// hulyo declared 6 envs while every hulyo workflow declared 8, and the CLI's client-side check\n// turned that drift into a REFUSAL to deploy to `stage` and `prod`. Vetoing on a local read of a\n// remote fact is what created the bug this function exists to remove.\nexport const readWorkflowEnvOptions = async (workflowFile: string): Promise<string[]> => {\n const projectRoot = await getProjectRoot()\n\n return parseEnvOptions(path.resolve(projectRoot, WORKFLOWS_DIR, workflowFile))\n}\n\n/**\n * Every environment this repo declares anywhere \u2014 the union across ALL workflows.\n *\n * Deliberately a UNION, and deliberately not `deploy-all.yml` alone. Two repos prove why: travelist's\n * `deploy-all` omits `eliran` and `roman` (who both hold live tokens and ARE deployable via\n * `deploy-selected-services`), and bridge has no `deploy-all.yml` at all, so a single-file read would\n * report that it has zero environments.\n *\n * Note the asymmetry with {@link readWorkflowEnvOptions}, and keep it: this answers the REPO-scoped\n * question (\"what environments exist here?\") and so unions; the deploy pickers answer the\n * WORKFLOW-scoped question (\"where can THIS workflow deploy?\") and so must not.\n *\n * Never throws \u2014 an unreadable workflows directory yields `[]`.\n *\n * @example\n * await listWorkflowEnvs() // => ['dev', 'arthur', 'eliran', 'renana', 'roman', 'oriana', 'stage', 'prod']\n */\nexport const listWorkflowEnvs = async (): Promise<string[]> => {\n const projectRoot = await getProjectRoot()\n const workflowsDir = path.resolve(projectRoot, WORKFLOWS_DIR)\n\n let entries: string[]\n\n try {\n entries = await fs.readdir(workflowsDir)\n } catch {\n return []\n }\n\n const workflows = entries\n .filter((entry) => {\n return entry.endsWith('.yml') || entry.endsWith('.yaml')\n })\n .sort()\n\n const perWorkflow = await Promise.all(\n workflows.map((file) => {\n return parseEnvOptions(path.resolve(workflowsDir, file))\n }),\n )\n\n // First-seen order, de-duped: the picker reads best when `dev` leads, as it does in every workflow.\n return [...new Set(perWorkflow.flat())]\n}\n\n/**\n * Pull `on.workflow_dispatch.inputs.environment.options` out of one workflow, or `[]`.\n *\n * `yaml.parse` is untyped, and every hop here is a place a real workflow legitimately differs (a\n * `push`-only workflow has no `workflow_dispatch`; a reusable `_jobs.yml` takes `environment` as a\n * `type: string`). Hence the guards rather than a schema: absence is normal, not an error.\n *\n * @example\n * await parseEnvOptions('/repo/.github/workflows/deploy-all.yml') // => ['dev', 'prod']\n * await parseEnvOptions('/repo/.github/workflows/code-quality.yml') // => []\n */\nconst parseEnvOptions = async (workflowPath: string): Promise<string[]> => {\n let parsed: unknown\n\n try {\n parsed = yaml.parse(await fs.readFile(workflowPath, 'utf-8'))\n } catch {\n return []\n }\n\n // `on` is the YAML 1.1 boolean `true` under some parsers; `yaml@2` keeps it a string, but read\n // defensively \u2014 a wrong key here would silently empty every picker.\n const on = pick(parsed, 'on') ?? pick(parsed, 'true')\n const options = pick(pick(pick(pick(on, 'workflow_dispatch'), 'inputs'), 'environment'), 'options')\n\n if (!Array.isArray(options)) return []\n\n return options.filter((option): option is string => {\n return typeof option === 'string' && option.length > 0\n })\n}\n\n/** One safe hop into an unknown object graph. Returns `undefined` for anything that is not a record. */\nconst pick = (value: unknown, key: string): unknown => {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined\n\n return (value as Record<string, unknown>)[key]\n}\n", "import { readTokenStore } from 'src/lib/env-tokens'\nimport { listWorkflowEnvs } from 'src/lib/workflow-envs'\n\n/** Where the knowledge that an environment EXISTS came from. Never says anything about credentials. */\nexport type ProjectEnvSource = 'gh-workflow' | 'token-only'\n\nexport interface ProjectEnv {\n env: string\n source: ProjectEnvSource\n}\n\n/**\n * Every environment this project has, as the union of the two authorities that actually own the\n * answer: `gh-workflow` (declared in a `workflow_dispatch` `environment.options` list) and\n * `token-only` (we hold a token for it, but no workflow mentions it \u2014 real and reachable, just not\n * deployable from here).\n *\n * @example\n * await listProjectEnvs()\n * // => [{ env: 'dev', source: 'gh-workflow' }, \u2026, { env: 'prod_observability', source: 'token-only' }]\n */\n// `gh-workflow` is what a repo says about itself, in git, reviewed, and enforced by GitHub on\n// dispatch. It is the ONLY source that works on a fresh clone with zero credentials, which is\n// exactly when a new developer needs to be told the env names they have to run\n// `infra-kit env-token-set <env>` for. hulyo's `prod_observability` is exactly a token-only env.\n//\n// The union is the point. Neither source alone is the truth: the token store is a per-DEVELOPER\n// cache (a teammate's holds one entry; a fresh clone's holds none), and the workflows cannot know\n// about an env that exists only to be read from. Reading them together is what replaced\n// `infra-kit.json \u2192 environments`, which was a hand-maintained third copy of both and had drifted\n// from both.\nexport const listProjectEnvs = async (): Promise<ProjectEnv[]> => {\n const workflowEnvs = await listWorkflowEnvs()\n const store = await readTokenStore()\n\n const declared: ProjectEnv[] = workflowEnvs.map((env) => {\n return { env, source: 'gh-workflow' }\n })\n\n const known = new Set(workflowEnvs)\n\n const tokenOnly: ProjectEnv[] = Object.keys(store?.envs ?? {})\n .filter((env) => {\n return !known.has(env)\n })\n .sort()\n .map((env) => {\n return { env, source: 'token-only' }\n })\n\n // Workflow envs first, in the order the workflows declare them (`dev` leads everywhere), then the\n // strays. A picker reads best when the common choice is at the top.\n return [...declared, ...tokenOnly]\n}\n\n/** The env names alone, for the callers that only need choices. Same order as {@link listProjectEnvs}. */\nexport const listProjectEnvNames = async (): Promise<string[]> => {\n const envs = await listProjectEnvs()\n\n return envs.map((entry) => {\n return entry.env\n })\n}\n", "import fs from 'node:fs'\nimport { $ } from 'zx'\n\nimport { getProjectWarmCacheDir } from 'src/lib/constants'\nimport { logger } from 'src/lib/logger'\n\nimport { canonicalizeProjectRoot } from './warm-cache'\n\n/**\n * Warm-cache invalidation for a CREDENTIAL change (`env-token-set` / `env-token-remove`), as opposed\n * to the per-worktree invalidation `env-clear` does.\n *\n * The two keys do not line up, and that mismatch is the whole reason this exists:\n * - the WARM CACHE is keyed per WORKTREE \u2014 a sha of the realpath'd project dir ({@link\n * canonicalizeProjectRoot} + `warmCacheKey`), so every worktree of a repo has its OWN warm dir;\n * - the TOKEN STORE is keyed to the MAIN repo root \u2014 one store shared by every worktree.\n *\n * So rotating a token in one worktree would leave every OTHER worktree's warm file \u2014 up to 2h of\n * plaintext secrets fetched with the OLD token \u2014 sitting on disk, ready to be sourced at the next\n * shell prompt. A fresh token would silently lose to a stale cache. Hence: enumerate the worktrees\n * from git and purge all of them.\n */\n\n/**\n * Absolute worktree paths from `git worktree list --porcelain`, whose stanzas begin with a\n * `worktree <path>` line (the main checkout first, then each linked worktree).\n *\n * Pure, so the parse is testable without a repo.\n *\n * @example\n * parseWorktreePaths('worktree /repo\\nHEAD abc\\n\\nworktree /repo-wt/feat\\nHEAD def\\n')\n * // => ['/repo', '/repo-wt/feat']\n */\nexport const parseWorktreePaths = (porcelain: string): string[] => {\n return porcelain\n .split('\\n')\n .filter((line) => {\n return line.startsWith('worktree ')\n })\n .map((line) => {\n return line.slice('worktree '.length).trim()\n })\n .filter(Boolean)\n}\n\n/**\n * Delete the warm cache of EVERY worktree of the current repo. Returns the dirs actually removed, so\n * the caller can report the count.\n *\n * Best-effort by design: outside a git checkout (or when git fails) there are no worktrees to\n * enumerate and no warm files to lose \u2014 a token write must not fail because the purge could not run.\n * A purge that DOES fail is far less bad than a token that never gets stored.\n *\n * @example\n * await purgeRepoWarmCaches() // => ['/Users/x/.cache/infra-kit/projects/<sha>', \u2026]\n */\nexport const purgeRepoWarmCaches = async (): Promise<string[]> => {\n let porcelain: string\n\n try {\n const result = await $({ quiet: true })`git worktree list --porcelain`\n\n porcelain = result.stdout\n } catch (error) {\n logger.debug(`warm-cache purge skipped (not a git repo?): ${(error as Error).message}`)\n\n return []\n }\n\n const removed: string[] = []\n\n for (const worktree of parseWorktreePaths(porcelain)) {\n const canonical = canonicalizeProjectRoot(worktree)\n\n if (!canonical) continue\n\n const dir = getProjectWarmCacheDir(canonical)\n\n try {\n if (!fs.existsSync(dir)) continue\n\n fs.rmSync(dir, { recursive: true, force: true })\n removed.push(dir)\n } catch (error) {\n logger.debug(`warm-cache purge skipped for ${dir}: ${(error as Error).message}`)\n }\n }\n\n return removed\n}\n", "import fs from 'node:fs'\nimport path from 'node:path'\n\nimport {\n DEFAULT_WARM_TTL_SECONDS,\n ENV_CLEAR_FILE,\n ENV_LOAD_FILE,\n atomicWriteFileSync,\n getProjectWarmCacheDir,\n getWarmCacheRoot,\n} from 'src/lib/constants'\nimport { logger } from 'src/lib/logger'\n\n/** Inputs for {@link shouldWriteWarm}. All derivable without I/O so the gate is pure. */\nexport interface ShouldWriteWarmInput {\n /** Was this an automatic load? Manual loads must never become the project default. */\n autoLoaded: boolean\n /** The canonical project dir the SHELL passed via `--project-dir` (undefined for cli-invocation / non-startup callers). */\n projectDir: string | undefined\n /** `realpath(git top-level)` for this load, or '' when non-git / unresolvable (see {@link canonicalizeProjectRoot}). */\n canonicalProjectRoot: string\n}\n\n/**\n * Decide whether an auto-load should ALSO write a project-scoped warm copy. Pure\n * (no I/O) so the full gate matrix is unit-testable. Warm requires ALL of:\n * - the load was automatic (a manual `env-load prod` must not warm the project);\n * - the shell passed a canonical `--project-dir` (warm is a shell-startup-only\n * optimization; the cli-invocation trigger passes none and gets no warm file);\n * - a non-empty git root (else every non-git project would collapse to one key\n * and cross-leak secrets);\n * - the realpath'd git root EQUALS the shell's canonical dir \u2014 the byte-identical\n * key contract. On any mismatch (symlink/nested-config monorepo) we skip warm\n * and degrade to the already-correct poll rather than write under a key the\n * shell will never look up.\n */\nexport const shouldWriteWarm = ({ autoLoaded, projectDir, canonicalProjectRoot }: ShouldWriteWarmInput): boolean => {\n if (!autoLoaded) return false\n if (!projectDir) return false\n if (!canonicalProjectRoot) return false\n\n return canonicalProjectRoot === projectDir\n}\n\n/** `realpath(projectRoot)`, or '' when projectRoot is empty or unresolvable. Never throws. */\nexport const canonicalizeProjectRoot = (projectRoot: string): string => {\n if (!projectRoot) return ''\n\n try {\n return fs.realpathSync(projectRoot)\n } catch {\n return ''\n }\n}\n\n/**\n * Write the warm copy of env-load.sh for a project (0o600). Best-effort: a warm\n * write must never break the real (session) load, so failures are swallowed to\n * debug. `contents` is the SAME bytes as the session file so a warm-source is\n * indistinguishable from a fresh one.\n */\nexport const writeWarmCache = (canonicalProjectDir: string, contents: string): void => {\n try {\n const dir = getProjectWarmCacheDir(canonicalProjectDir)\n\n fs.mkdirSync(dir, { recursive: true, mode: 0o700 })\n atomicWriteFileSync(path.join(dir, ENV_LOAD_FILE), contents, 0o600)\n } catch (error) {\n logger.debug(`warm-cache write skipped: ${(error as Error).message}`)\n }\n}\n\n/**\n * Delete every project's warm dir whose env-load.sh is older than the TTL. The\n * codebase has no cache reaper, so warm files (plaintext secrets) would otherwise\n * accumulate indefinitely at a stable path; each successful warm write sweeps the\n * whole `projects/` tree. The just-written project is fresh (mtime\u2248now) so it is\n * never swept. Best-effort, concurrency-tolerant (force), never throws.\n */\nexport const evictStaleWarmCaches = (\n nowMs: number = Date.now(),\n ttlSeconds: number = DEFAULT_WARM_TTL_SECONDS,\n): void => {\n const root = getWarmCacheRoot()\n\n let entries: fs.Dirent[]\n\n try {\n entries = fs.readdirSync(root, { withFileTypes: true })\n } catch {\n return // no warm root yet \u2014 nothing to sweep\n }\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue\n\n const projectDir = path.join(root, entry.name)\n\n try {\n const { mtimeMs } = fs.statSync(path.join(projectDir, ENV_LOAD_FILE))\n\n if (nowMs - mtimeMs >= ttlSeconds * 1000) {\n fs.rmSync(projectDir, { recursive: true, force: true })\n }\n } catch {\n // missing/unreadable warm file \u2014 leave the dir (a lone clear marker is harmless)\n }\n }\n}\n\n/**\n * Invalidate a project's warm cache on `env-clear`. `purge` removes the project's\n * warm dir outright (durable disable). Otherwise, when a warm file exists, write a\n * clear-marker (`env-clear.sh`) so the NEXT shell's startup gate skips warm-source\n * (it compares marker mtime vs warm mtime, clear wins). The marker is TRANSIENT: a\n * later successful auto-load writes a newer warm file and warm resumes \u2014 durable\n * disable requires `purge`. Best-effort, never throws.\n */\nexport const invalidateProjectWarmCache = (canonicalProjectDir: string, purge: boolean): void => {\n try {\n const dir = getProjectWarmCacheDir(canonicalProjectDir)\n\n if (purge) {\n fs.rmSync(dir, { recursive: true, force: true })\n\n return\n }\n\n // No warm file \u21D2 nothing to suppress; don't create a dir just to hold a marker.\n if (!fs.existsSync(path.join(dir, ENV_LOAD_FILE))) return\n\n atomicWriteFileSync(path.join(dir, ENV_CLEAR_FILE), '# infra-kit warm cache cleared\\n', 0o600)\n } catch (error) {\n logger.debug(`warm-cache invalidate skipped: ${(error as Error).message}`)\n }\n}\n", "import fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\n\nimport { syncPackageGuidance, syncRootGuidance } from 'src/lib/agent-guidance'\nimport type { GuidanceWrite, WriteAction } from 'src/lib/agent-guidance'\nimport { getProjectRoot } from 'src/lib/git-utils'\nimport { logger } from 'src/lib/logger'\nimport { discoverPackages, readDeclaredPackageType } from 'src/lib/package-validator/loader'\n\nimport packageJson from '../../../package.json' with { type: 'json' }\n\n// Re-exported on their historical import path so `doctor` and the existing tests keep\n// resolving after the markers and write rails moved into `src/lib/agent-guidance`.\nexport {\n LEGACY_IMPORT_END as AGENTS_IMPORT_END,\n LEGACY_IMPORT_START as AGENTS_IMPORT_START,\n} from 'src/lib/agent-guidance'\nexport { ROOT_MARKER_END as AGENTS_MARKER_END, ROOT_MARKER_START as AGENTS_MARKER_START } from 'src/lib/agent-guidance'\nexport type { WriteAction } from 'src/lib/agent-guidance'\n\nconst CLAUDE_FILE = 'CLAUDE.md'\n\nexport interface AgentFileWrite {\n path: string\n action: WriteAction\n}\n\nexport interface WriteAgentFilesResult {\n /** True when run outside an infra-kit repo (no `infra-kit.json` at the git root). */\n skipped: boolean\n /** Repo root the files were written under, or null when skipped. */\n root: string | null\n written: AgentFileWrite[]\n}\n\nconst INFRA_KIT_CONFIG_FILE = 'infra-kit.json'\n\n/**\n * The one skip announcement for every step gated on {@link resolveGitRootForWrites}.\n *\n * It names all four steps because one `null` skips all four: naming only the guidance\n * files (as it did while a single gate served everything) leaves a reader who lost the\n * plugin pointer, the plugin install and the `.mcp.json` write with no record of it.\n */\nconst SKIPPED_GIT_ROOT_STEPS =\n 'Skipped agent-instruction files, the plugin pointer, the plugin install and .mcp.json \u2014 no usable git repo root here (not a git repo, or the root is your home directory)'\n\n/**\n * The skip announcement for the guidance-only gate.\n *\n * Names the other three steps as UNAFFECTED: after the gate split this `null` stops the\n * guidance files alone, so a reader who saw the old text would wrongly assume the pointer,\n * the install and the `.mcp.json` write were skipped too.\n */\nconst SKIPPED_GUIDANCE_ONLY =\n 'Skipped agent-instruction files \u2014 no infra-kit.json at the repo root (the plugin pointer, the plugin install and .mcp.json are unaffected)'\n\n/**\n * The git toplevel, or `null` when there is no root safe to write into \u2014 a failed resolve,\n * a blank resolve, or `$HOME`.\n *\n * SILENT, and that is load-bearing: this answers a question, it does not narrate a decision.\n * `doctor` consults it read-only to decide whether the `MCP server key` row is answerable, and\n * a predicate that logs made a read-only command print `initCore`'s \"Skipped \u2026\" line \u2014 first, above\n * its own report header \u2014 in every non-git directory and in `$HOME`. Writers announce through\n * {@link resolveGitRootForWrites}; a new reader gets silence by default, which is the safe\n * direction for a default to fail in.\n *\n * Worktree-local by design: Claude Code reads `.mcp.json` and `.claude/settings.json` from\n * the session cwd, and sessions do run in worktrees, so `getMainRepoRoot` would write config\n * a worktree session never reads.\n *\n * @example\n * const root = await resolveGitRoot()\n * // => '/Users/me/projects/api' (or null, and nothing is logged)\n */\nexport const resolveGitRoot = async (): Promise<string | null> => {\n let resolved: string\n\n try {\n resolved = await getProjectRoot()\n } catch {\n return null\n }\n\n const root = resolved.trim()\n\n // Blank is a failed resolve by contract, not a lenient case. `getProjectRoot` is\n // `result.stdout.trim()` and rejects only when the shell-out itself fails, so empty\n // stdout resolves as `''` \u2014 and `''` is not merely an invalid root but a cwd-relative\n // one: every `path.join('', x)` silently targets `process.cwd()`, and `'' !== homedir()`\n // means the home check below would pass it.\n //\n // `$HOME` is checked here because \"inside a git repo\" does not protect the home\n // directory: anyone who git-manages their dotfiles has a repo there, and this is all\n // that stands between that and a `.claude/` directory written into it.\n if (root === '' || root === os.homedir()) return null\n\n return root\n}\n\n/**\n * {@link resolveGitRoot} for the callers that were about to WRITE \u2014 the same predicate, plus the\n * announcement.\n *\n * The line lives here because `initCore` is the only caller for whom the skip is otherwise invisible:\n * the steps it gates (the plugin pointer, the plugin install, the `.mcp.json` write) are silent\n * when they do not run, so this is the only evidence they did not. A decorator rather than a\n * `quiet` parameter: a boolean would default to loud, and the next reader who needs the predicate\n * would forget to pass it.\n *\n * @example\n * const root = await resolveGitRootForWrites()\n * // => '/Users/me/projects/api' (or null, with the four-step skip logged)\n */\nexport const resolveGitRootForWrites = async (): Promise<string | null> => {\n const root = await resolveGitRoot()\n\n if (root === null) logger.info(SKIPPED_GIT_ROOT_STEPS)\n\n return root\n}\n\n/**\n * The git toplevel of an infra-kit repo \u2014 {@link resolveGitRoot} plus an `infra-kit.json`\n * at that root \u2014 or `null`.\n *\n * Gates the guidance writers only. They render config-derived content, so the config file\n * is a real precondition for them; the plugin and MCP steps never read it.\n *\n * Announces its OWN predicate only. A refused git gate returns `null` silently here, because\n * `initCore` calls {@link resolveGitRootForWrites} for the same refusal \u2014 one gate, one line, rather\n * than the duplicate a delegating announcement produced.\n *\n * @example\n * const root = await resolveInfraKitRoot()\n * // => '/Users/me/projects/api' (null in a git repo with no infra-kit.json)\n */\nexport const resolveInfraKitRoot = async (): Promise<string | null> => {\n const root = await resolveGitRoot()\n\n if (root === null) return null\n\n if (!fs.existsSync(path.join(root, INFRA_KIT_CONFIG_FILE))) {\n logger.info(SKIPPED_GUIDANCE_ONLY)\n\n return null\n }\n\n return root\n}\n\n/**\n * Re-throw a root write failure.\n *\n * `syncRootGuidance` is continue-and-report \u2014 it never throws \u2014 because a multi-package fix\n * run must not abort halfway. `writeAgentFiles` is the opposite case: one file, written by a\n * command a human ran deliberately, whose failure has always propagated (a symlinked\n * `CLAUDE.md` still aborts `initCore`). Restoring that here keeps the CLI behaviour unchanged\n * while the library stays reusable by the multi-file writer.\n */\nconst rethrowFailures = (written: GuidanceWrite[]): void => {\n const failure = written.find((entry) => {\n return entry.action === 'failed'\n })\n\n if (failure) throw new Error(failure.message ?? `Failed to write ${failure.path}`)\n}\n\n/**\n * Generate (or refresh) the repo agent-instruction guidance, now hosted solely in\n * `CLAUDE.md` (preserving hand-authored content outside the markers). Also migrates\n * legacy setups \u2014 stripping the old `@AGENTS.md` import region from `CLAUDE.md` and\n * backing up/removing a now-redundant `AGENTS.md`. Repo-gated: a no-op outside an\n * infra-kit repo. Idempotent (within a CLI version) and non-destructive.\n *\n * A thin wrapper over {@link syncRootGuidance}: the repo gate, the logging and the result\n * shape live here, the write rails live in `src/lib/agent-guidance`.\n *\n * @example\n * await writeAgentFiles()\n * // INFO: Agent-instruction files synced (infra-kit 0.1.105)\n */\nexport const writeAgentFiles = async (): Promise<WriteAgentFilesResult> => {\n const root = await resolveInfraKitRoot()\n\n if (root === null) return { skipped: true, root: null, written: [] }\n\n const version = packageJson.version\n const written = await syncRootGuidance(root, { version })\n\n rethrowFailures(written)\n\n const claudePath = path.join(root, CLAUDE_FILE)\n\n for (const file of written) {\n // Always report CLAUDE.md; only mention a migrated file when it actually changed.\n if (file.action === 'unchanged' && file.path !== claudePath) continue\n\n logger.info(` ${file.action.padEnd(9)} ${path.relative(root, file.path)}`)\n }\n\n logger.info(`Agent-instruction files synced (infra-kit ${version})`)\n\n return {\n skipped: false,\n root,\n written: written.map((file) => {\n return { path: file.path, action: file.action }\n }),\n }\n}\n\nexport interface SyncRepoGuidanceResult {\n /** True when run outside an infra-kit repo (no `infra-kit.json` at the git root). */\n skipped: boolean\n /** Repo root every path was written under, or `null` when skipped. */\n root: string | null\n /** CLI version recorded in every block this run wrote. */\n version: string\n /**\n * Every write this run attempted \u2014 the two root files first, then one entry per\n * discovered package. Entries with `action: 'failed'` carry a `message`; the caller\n * decides how loud that is.\n */\n written: GuidanceWrite[]\n}\n\n/**\n * Discover the workspace packages under `root`, degrading to \"no packages\" when the\n * workspace cannot be read.\n *\n * `discoverPackages` reads `pnpm-workspace.yaml` and rejects with `ENOENT` when there\n * is none. A single-package infra-kit repo is a legitimate setup, so that rejection\n * must not abort `initCore` \u2014 it means \"the root block is the whole job\".\n */\nconst discoverWorkspacePackages = async (root: string): Promise<string[]> => {\n try {\n return await discoverPackages(root)\n } catch (err) {\n logger.debug({ err, msg: 'Skipped per-package agent guidance \u2014 no workspace packages could be discovered.' })\n\n return []\n }\n}\n\n/**\n * Refresh the agent-instruction guidance across the WHOLE repo: the root `CLAUDE.md`\n * (plus the legacy `AGENTS.md` migration) and one managed block per workspace package.\n * Repo-gated exactly like {@link writeAgentFiles} \u2014 a no-op outside an infra-kit repo.\n *\n * Continue-and-report throughout: nothing here throws, and a per-file error comes back as\n * an entry with `action: 'failed'`. A run that aborted halfway could leave exactly one\n * well-formed package block behind, which adopts the workspace and reddens every package\n * the run never reached.\n *\n * Writes unconditionally \u2014 it does not ask, and does not skip an unadopted workspace.\n * `initCore` is the repo-wide refresh path, so a consumer re-running it after a CLI upgrade\n * gets every block regenerated in one pass.\n *\n * @example\n * const result = await syncRepoGuidance()\n * // => { skipped: false, root: '/repo', version: '0.4.0', written: [ ...root, ...packages ] }\n */\nexport const syncRepoGuidance = async (): Promise<SyncRepoGuidanceResult> => {\n const root = await resolveInfraKitRoot()\n const version = packageJson.version\n\n if (root === null) return { skipped: true, root: null, version, written: [] }\n\n const written: GuidanceWrite[] = [...(await syncRootGuidance(root, { version }))]\n\n for (const packageDir of await discoverWorkspacePackages(root)) {\n // Total by contract: an absent, unloadable or unrecognised config resolves to `undefined`\n // rather than rejecting, so one broken package config cannot abort the repo-wide sync.\n const declaredType = await readDeclaredPackageType(packageDir)\n\n written.push(...(await syncPackageGuidance(packageDir, { repoRoot: root, version, declaredType })))\n }\n\n return { skipped: false, root, version, written }\n}\n", "import fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\nimport process from 'node:process'\n\nimport type { GuidanceWrite } from 'src/lib/agent-guidance'\nimport { seedCreatedMessage, seedUserProjectConfig } from 'src/lib/config-bootstrap'\nimport { CONFIG_STUB, buildUserGlobalExample, buildVendorExample } from 'src/lib/config-templates'\nimport { getInfraKitConfigPaths } from 'src/lib/infra-kit-config'\nimport { logger } from 'src/lib/logger'\nimport { removeManagedBlock, upsertManagedBlock } from 'src/lib/managed-block'\nimport {\n MARKETPLACE_ADD_COMMAND,\n MARKETPLACE_NAME,\n PLUGIN_INSTALL_COMMAND,\n PLUGIN_KEY,\n ensureMcpRegistration,\n ensurePluginPointer,\n installPluginForProject,\n} from 'src/lib/plugin-pointer'\nimport type { McpRegistrationResult, PluginInstallOutcome, PluginPointerResult } from 'src/lib/plugin-pointer'\n\nimport { resolveGitRootForWrites, syncRepoGuidance } from './agent-files'\nimport {\n migrateFactoryConfigToJson,\n migrateLegacyConfig,\n migrateUserGlobalConfigFilename,\n normalizeLegacyIdeStructures,\n} from './migrate-config'\n\nexport const MARKER_START = '# -- infra-kit:begin --'\nexport const MARKER_END = '# -- infra-kit:end --'\n\nconst LEGACY_PAIRED: [start: string, end: string][] = [['# region infra-kit', '# endregion infra-kit']]\nconst LEGACY_SINGLE = '# infra-kit shell functions'\n\n/** Which of `initCore`'s steps an {@link InitStep} reports on. */\nexport type InitStepName =\n 'guidance' | 'mcp-server' | 'migrations' | 'plugin-pointer' | 'project-config' | 'shell' | 'user-config' | 'zshrc'\n\n/**\n * What a step did.\n *\n * `written` changed something; `unchanged` ran and found nothing to change; `skipped` did not run, or\n * reports through another channel (the migrators and both `agent-files` gates print their own lines);\n * `warned` is a best-effort failure that did not stop the run.\n */\nexport type InitOutcome = 'skipped' | 'unchanged' | 'warned' | 'written'\n\n/**\n * One reportable thing `initCore` did, in the order it did it.\n *\n * Required rather than cosmetic: `logger` writes to `/tmp/mcp-infra-kit.log` and never to the caller, so\n * a `setup` tool reporting only through it performs ~8 local writes and tells the agent nothing about any\n * of them.\n */\nexport interface InitStep {\n step: InitStepName\n outcome: InitOutcome\n message: string\n}\n\n/**\n * How the CLI prints an entry \u2014 carried rather than derived from `outcome`, because the two disagree:\n * a guidance file whose write FAILED prints at `info` (it is one row of a per-file list) while the\n * \"N files could not be written\" summary prints at `warn`, and an already-installed plugin prints at\n * `debug`. Deriving the level from the outcome would move one of those lines to another stream, which\n * is exactly the byte difference this refactor may not introduce.\n *\n * `silent` reports to MCP and prints nothing, because the library that performed the step already\n * printed its own line \u2014 printing here would double every one of them.\n */\ntype InitLevel = 'debug' | 'info' | 'silent' | 'warn'\n\nexport interface InitEntry extends InitStep {\n level: InitLevel\n}\n\nexport type InitReport = InitEntry[]\n\n/** Notified as each entry happens, so a caller prints in step order instead of in a trailing block. */\nexport type InitStepSink = (entry: InitEntry) => void\n\n/** `initCore`'s internal accumulator, handed to the steps that must report as they go. */\ntype InitStepRecorder = (...entries: InitEntry[]) => void\n\n/**\n * Tags a throwing step with the step it threw in.\n *\n * The tag exists for `setup`, the only caller: it catches this, records the step as `warned`, and goes\n * on to its dependency half rather than losing the run to one failed local write. Nothing unwraps it\n * back to the bare reason any more \u2014 the standalone command that used to do that is gone \u2014 so a caller\n * that lets it escape surfaces the step name too, which is strictly more than the old CLI printed.\n */\nexport class InitStepError extends Error {\n constructor(\n readonly step: InitStepName,\n readonly reason: unknown,\n ) {\n super(reason instanceof Error ? reason.message : String(reason), { cause: reason })\n this.name = 'InitStepError'\n }\n}\n\n/**\n * `initCore`'s closing line.\n *\n * Exported so `setup` can hold this ONE entry back and print it after its dependency half, which is\n * where the reminder belongs when both halves run \u2014 without pattern-matching a message it does not own.\n */\nexport const SHELL_ACTIVATION_REMINDER = 'Run `source ~/.zshrc` or open a new terminal to activate.'\n\n/** The four migrations announce their own conversions, so this step has nothing of its own to print. */\nconst MIGRATIONS_CHECKED: InitEntry = {\n step: 'migrations',\n outcome: 'skipped',\n message: 'Config migrations checked (legacy yml layers, ide structure, user-global filename, factory config)',\n level: 'silent',\n}\n\n/** Run one step, tagging anything it throws with which step it was. */\nconst withStep = async <T>(step: InitStepName, body: () => Promise<T> | T): Promise<T> => {\n try {\n return await body()\n } catch (err) {\n throw new InitStepError(step, err)\n }\n}\n\n/**\n * Append infra-kit shell functions to .zshrc, migrate any legacy `infra-kit.yml`\n * config layers to JSON, normalize existing JSON configs from the old IDE structure\n * (strip the removed `ide.config.mode`), convert a legacy factory `vendor.config.ts`\n * to `vendor.json`, and seed the user-global config at ~/.infra-kit/infra-kit.json on\n * first run.\n *\n * Idempotent: a subsequent run replaces the existing zshrc block in place,\n * has nothing left to migrate or normalize, and leaves the REAL config files\n * (infra-kit.json / vendor.json) untouched \u2014 but the annotated `.example.jsonc`\n * reference files are refreshed on every run so they always reflect the current schema.\n *\n * Returns the report; `onStep` is notified as each entry happens.\n *\n * @example\n * // CLI: `infra-kit setup --skip-tools` (the additive half on its own; flagless `setup` runs it too)\n * // INFO: Added infra-kit shell functions to /Users/me/.zshrc\n * // INFO: Wrote user-global config to /Users/me/.infra-kit/infra-kit.json (see \u2026/infra-kit.example.jsonc \u2026)\n * // INFO: Run `source ~/.zshrc` or open a new terminal to activate.\n */\n// Why both a return value AND a sink: the return value is what an MCP caller reads, and the streaming\n// sink is what keeps the CLI's lines interleaved with the ones its libraries print (`\u2713 Migrated \u2026`, the\n// two skip announcements) exactly where they have always been. A trailing replay would reorder them.\nexport const initCore = async (onStep?: InitStepSink): Promise<InitReport> => {\n const report: InitReport = []\n const record = (...entries: InitEntry[]): void => {\n for (const entry of entries) {\n report.push(entry)\n onStep?.(entry)\n }\n }\n\n await withStep('zshrc', () => {\n record(writeShellBlock())\n })\n\n await withStep('migrations', async () => {\n await runConfigMigrations()\n record(MIGRATIONS_CHECKED)\n })\n\n await withStep('user-config', () => {\n record(seedUserGlobalConfig())\n })\n\n // Best-effort, non-fatal, repo-gated: keep the agent-instruction files in sync\n // with the CLI surface \u2014 root AND every workspace package. A no-op outside an\n // infra-kit repo (gated on `resolveInfraKitRoot`: the blocks render config-derived\n // content, so `infra-kit.json` is a real precondition for THEM).\n const guidanceRoot = await withStep('guidance', async () => {\n const { entries, root } = await syncAgentGuidance()\n\n record(...entries)\n\n return root\n })\n\n // A SEPARATE, weaker gate for the plugin steps: they write `.claude/settings.json`,\n // `.mcp.json` and drive `claude plugin install`, and none of the three reads\n // `infra-kit.json` \u2014 so gating them on it was incidental coupling that made a fresh\n // repo impossible to set up. One `resolveGitRootForWrites` for all three, because\n // `syncPluginPointer`'s contract binds pointer \u2194 installation \u2194 MCP entry to ONE\n // project: `--scope project` must record exactly what the pointer names.\n //\n // The announcing variant, and this is the ONLY call to it: the four-step skip is `initCore`'s line\n // to print, so the bare predicate stays silent for `doctor`, which only reads it.\n const gitRoot = await withStep('plugin-pointer', async () => {\n return resolveGitRootForWrites()\n })\n\n record(...gateDisagreementEntries(gitRoot, guidanceRoot))\n\n await withStep('plugin-pointer', () => {\n syncPluginPointer(gitRoot, record)\n })\n\n // Close the legacy-yml migration gap so a single `dx-init` leaves EVERY example current.\n await withStep('project-config', async () => {\n record(...(await reseedUserProjectConfig()))\n })\n\n record(...shellEntries())\n\n return report\n}\n\n/**\n * The one place an {@link InitEntry} becomes a log line.\n *\n * Exported for `setup`, which prints the same half with the same words \u2014 a second renderer there would\n * be a second set of strings to keep in step with this one.\n */\nexport const logInitEntry = (entry: InitEntry): void => {\n if (entry.level === 'silent') return\n\n if (entry.level === 'warn') {\n logger.warn(entry.message)\n\n return\n }\n\n if (entry.level === 'debug') {\n logger.debug({ msg: entry.message })\n\n return\n }\n\n logger.info(entry.message)\n}\n\n/**\n * Strip any prior block (current or legacy markers) anywhere in the file, then append a fresh block at\n * end-of-file via the shared managed-block utility \u2014 the historical `removeExistingBlock` + append\n * behavior, now centralized.\n */\nconst writeShellBlock = (): InitEntry => {\n const zshrcPath = path.join(os.homedir(), '.zshrc')\n const existing = fs.existsSync(zshrcPath) ? removeExistingBlock(fs.readFileSync(zshrcPath, 'utf-8')) : ''\n\n const updated = upsertManagedBlock({\n content: existing,\n body: buildShellBody(),\n startMarker: MARKER_START,\n endMarker: MARKER_END,\n placement: 'append-end',\n })\n\n fs.writeFileSync(zshrcPath, updated)\n\n return {\n step: 'zshrc',\n outcome: 'written',\n message: `Added infra-kit shell functions to ${zshrcPath}`,\n level: 'info',\n }\n}\n\n/** The four config migrations, in the one order that works. */\nconst runConfigMigrations = async (): Promise<void> => {\n // Convert any legacy infra-kit.yml config layers to JSON before seeding, so a\n // migrated infra-kit.json is not re-seeded as an empty stub.\n await migrateLegacyConfig()\n\n // Migrate existing JSON configs from the old IDE structure to the new one\n // (strip the removed `ide.config.mode` field). No-op for already-clean configs.\n await normalizeLegacyIdeStructures()\n\n // Rename a legacy user-global config.json \u2192 infra-kit.json (single canonical\n // filename). MUST run before seeding: otherwise the seeder checks the new name,\n // doesn't find it, and writes an empty stub that shadows the user's real config.\n await migrateUserGlobalConfigFilename()\n\n // Convert a legacy machine-local factory config from executable TS\n // (~/.infra-kit/vendor.config.ts) to static JSON (~/.infra-kit/vendor.json).\n // Independent of the infra-kit.json layers; grouped with the other migrations.\n await migrateFactoryConfigToJson()\n}\n\n/**\n * The shell integration is zsh-only (zmodload zsh/stat, add-zsh-hook, ${dir:h}), so a non-zsh login\n * shell is warned about non-fatally rather than left with a silent no-op block.\n */\nconst shellEntries = (): InitEntry[] => {\n const shell = process.env.SHELL ?? ''\n const entries: InitEntry[] = []\n\n if (!shell.includes('zsh')) {\n entries.push({\n step: 'shell',\n outcome: 'warned',\n message: `Your login shell ($SHELL=${shell || 'unset'}) is not zsh. The infra-kit shell integration (env-load/env-clear/auto-load) is zsh-only and won't activate in bash/fish.`,\n level: 'warn',\n })\n }\n\n // `unchanged` because that is what it reports: the running shell keeps its old environment until the\n // user sources the file this run just wrote.\n entries.push({ step: 'shell', outcome: 'unchanged', message: SHELL_ACTIVATION_REMINDER, level: 'info' })\n\n return entries\n}\n\n/**\n * Seed the user-global config on first run and (re)write the annotated reference\n * files. The real `~/.infra-kit/infra-kit.json` (empty `{}`) is written only when\n * absent so user edits are preserved. The non-loaded `.example.jsonc` reference\n * files (`infra-kit.example.jsonc` + `vendor.example.jsonc`) are rewritten on EVERY\n * run so existing users always get the current, complete schema documentation.\n *\n * Deliberately seeds NO real `vendor.json` \u2014 that is scaffolded by\n * `infra-kit vendor config --init` (a stub here would block `--init`, trip the\n * factory migration's no-overwrite guard, and fail the schema's `targets.min(1)`).\n *\n * @example\n * seedUserGlobalConfig()\n * // first call: writes ~/.infra-kit/infra-kit.json ({}) + both .example.jsonc files\n * // later calls: leaves infra-kit.json alone, refreshes both .example.jsonc files\n */\nexport const seedUserGlobalConfig = (): InitEntry => {\n const userConfigDir = path.join(os.homedir(), '.infra-kit')\n const userConfigPath = path.join(userConfigDir, 'infra-kit.json')\n\n fs.mkdirSync(userConfigDir, { recursive: true })\n\n // Reference examples are non-loaded docs \u2014 always refresh them so re-running init\n // delivers the current schema (and the newly-added vendor example) to existing users.\n fs.writeFileSync(path.join(userConfigDir, 'infra-kit.example.jsonc'), buildUserGlobalExample(), 'utf-8')\n fs.writeFileSync(path.join(userConfigDir, 'vendor.example.jsonc'), buildVendorExample(), 'utf-8')\n\n if (fs.existsSync(userConfigPath)) {\n return {\n step: 'user-config',\n outcome: 'unchanged',\n message: `User-global config already present at ${userConfigPath} (refreshed reference examples)`,\n level: 'info',\n }\n }\n\n fs.writeFileSync(userConfigPath, CONFIG_STUB, 'utf-8')\n\n return {\n step: 'user-config',\n outcome: 'written',\n message: `Wrote user-global config to ${userConfigPath} (see the sibling .example.jsonc files for reference)`,\n level: 'info',\n }\n}\n\n/**\n * Re-run the layer-3 seed at the END of `initCore`, closing the legacy-yml migration gap.\n *\n * Re-evaluates the preAction gate itself and drives the UNGATED `seedUserProjectConfig`, so it\n * honours `INFRA_KIT_NO_SEED` by hand. Any failure (no git repo, no project config, an unwritable\n * `$HOME`) degrades to a debug line \u2014 seeding a reference file must never fail `initCore`. Separate\n * from {@link seedUserGlobalConfig}, which keeps its layer-2-only duty and stays sync.\n *\n * @example\n * await reseedUserProjectConfig()\n * // after a legacy infra-kit.yml migration:\n * // INFO: Created ~/.infra-kit/projects/api/infra-kit.json \u2014 see \u2026/infra-kit.example.jsonc \u2026\n */\n// Why this exists at all: the per-command seed lives in the program's preAction hook, which\n// evaluates its \"is this an infra-kit repo?\" gate (does `<repo-root>/infra-kit.json` exist?)\n// BEFORE the action body runs. On the legacy-yml migration path `initCore` itself CREATES that file\n// (`migrateLegacyConfig`), so the gate had already declined and this very run would otherwise\n// leave the layer-3 example unwritten \u2014 self-healing only on the user's next command. Re-running\n// the gate here makes one `dx-init` enough.\n//\n// Why the ungated primitive: `ensureUserProjectConfig()` is an ENTRY-BOUNDARY primitive whose\n// once-per-process guard has ALREADY fired in preAction, so calling it here would be a silent\n// no-op \u2014 unusable as a re-seed hook. Its ungated replacement does not read `INFRA_KIT_NO_SEED`,\n// so without the check above the kill switch would be airtight on every path EXCEPT `initCore`.\nconst reseedUserProjectConfig = async (): Promise<InitEntry[]> => {\n if (process.env.INFRA_KIT_NO_SEED) return [projectConfigSkip('INFRA_KIT_NO_SEED is set')]\n\n try {\n const paths = await getInfraKitConfigPaths()\n\n // The same D2 gate the preAction seed applies \u2014 re-evaluated now that the migrations have run.\n if (!fs.existsSync(paths.main)) return [projectConfigSkip(`no project config at ${paths.main}`)]\n\n const result = await seedUserProjectConfig(paths)\n\n if (!result.createdConfig) return [projectConfigSkip('the user-project config was already present')]\n\n return [{ step: 'project-config', outcome: 'written', message: seedCreatedMessage(result), level: 'info' }]\n } catch (err) {\n // Kept as a raw logger call rather than routed through an entry: the `err` OBJECT is the whole point\n // of this line, and an entry carries a message string. The entry beside it reports the outcome.\n logger.debug({ err, msg: 'Skipped seeding the user-project config (init).' })\n\n return [projectConfigSkip(err instanceof Error ? err.message : String(err))]\n }\n}\n\n/** Every reason the layer-3 reseed does nothing is silent on the CLI, exactly as it has always been. */\nconst projectConfigSkip = (why: string): InitEntry => {\n return {\n step: 'project-config',\n outcome: 'skipped',\n message: `Skipped seeding the user-project config \u2014 ${why}`,\n level: 'silent',\n }\n}\n\n/**\n * Log one line per guidance file this run actually changed, then the step's closing line.\n *\n * Unchanged files are omitted: a repo-wide refresh touches every package, and a clean\n * re-run would otherwise print one no-op line per package on top of `initCore`'s other output.\n *\n * When any file failed, a distinct summary line names the count and the fix. `initCore` exits 0\n * regardless (its contract is shell setup, and the sync is a side effect that must not turn\n * a machine-setup command red), which makes this line the only signal a partial sync\n * happened \u2014 so it must not be a per-path line buried among the rest.\n */\nconst guidanceEntries = (root: string, version: string, written: GuidanceWrite[]): InitEntry[] => {\n const entries: InitEntry[] = []\n\n for (const file of written) {\n if (file.action === 'unchanged') continue\n\n const suffix = file.type === undefined ? '' : ` (${file.type})`\n\n entries.push({\n step: 'guidance',\n // A failed row is still one line of the per-file list, so it prints at `info` like its siblings \u2014\n // the level and the outcome part company here, which is why the entry carries both.\n outcome: file.action === 'failed' ? 'warned' : 'written',\n message: ` ${file.action.padEnd(9)} ${path.relative(root, file.path)}${suffix}`,\n level: 'info',\n })\n }\n\n entries.push({\n step: 'guidance',\n outcome: 'written',\n message: `Agent-instruction files synced (infra-kit ${version})`,\n level: 'info',\n })\n\n const failed = written.filter((file) => {\n return file.action === 'failed'\n })\n\n if (failed.length === 0) return entries\n\n entries.push({\n step: 'guidance',\n outcome: 'warned',\n message: `${failed.length} package guidance files could not be written \u2014 run: infra-kit audit --fix --all`,\n level: 'warn',\n })\n\n return entries\n}\n\n/**\n * `initCore`'s agent-guidance step: refresh the root block AND every workspace package's block in\n * one pass, then report. Continue-and-report \u2014 `syncRepoGuidance` never throws, a per-file\n * error arrives as `action: 'failed'`, and `process.exitCode` is deliberately left untouched\n * so `initCore` goes on to its remaining steps. A user who wants a failure to be an error runs\n * `infra-kit audit --fix --all`, which does exit non-zero.\n *\n * @example\n * await syncAgentGuidance()\n * // INFO: updated CLAUDE.md\n * // INFO: created apps/client/ui/CLAUDE.md (frontend)\n * // INFO: Agent-instruction files synced (infra-kit 0.4.0)\n */\nconst syncAgentGuidance = async (): Promise<{ root: string | null; entries: InitEntry[] }> => {\n const { skipped, root, version, written } = await syncRepoGuidance()\n\n if (skipped || root === null) {\n // Silent: `resolveInfraKitRoot` has already printed the reason it declined.\n return {\n root: null,\n entries: [\n {\n step: 'guidance',\n outcome: 'skipped',\n message: 'Agent-instruction files skipped \u2014 not an infra-kit repo',\n level: 'silent',\n },\n ],\n }\n }\n\n return { root, entries: guidanceEntries(root, version, written) }\n}\n\n/** One line per pointer outcome. `unparseable` already warned from inside the lib, so it says nothing here. */\nconst pointerEntry = (root: string, result: PluginPointerResult): InitEntry => {\n const relative = path.relative(root, result.path)\n\n if (result.status === 'created') {\n return {\n step: 'plugin-pointer',\n outcome: 'written',\n message: ` created ${relative} (Claude Code plugin pointer)`,\n level: 'info',\n }\n }\n\n if (result.status === 'added') {\n return {\n step: 'plugin-pointer',\n outcome: 'written',\n message: ` updated ${relative} \u2014 added ${result.added.join(', ')}`,\n level: 'info',\n }\n }\n\n return {\n step: 'plugin-pointer',\n outcome: result.status === 'unchanged' ? 'unchanged' : 'warned',\n message: `${relative} \u2014 ${result.status}`,\n level: 'silent',\n }\n}\n\n/**\n * One line per MCP-registration outcome, in {@link logPointerResult}'s style.\n *\n * Only the two writing outcomes speak here. Every refusal (`unparseable`, `misfiled`, a failed\n * write) already warned from inside the lib with the path and the fix, so repeating it would\n * double every real problem while adding nothing.\n */\nconst mcpEntry = (root: string, result: McpRegistrationResult): InitEntry => {\n const relative = path.relative(root, result.path)\n\n if (result.status === 'created') {\n return {\n step: 'mcp-server',\n outcome: 'written',\n message: ` created ${relative} (infra-kit MCP server)`,\n level: 'info',\n }\n }\n\n if (result.status === 'added') {\n return {\n step: 'mcp-server',\n outcome: 'written',\n message: ` updated ${relative} \u2014 added the ${MARKETPLACE_NAME} MCP server`,\n level: 'info',\n }\n }\n\n return {\n step: 'mcp-server',\n outcome: result.status === 'unchanged' ? 'unchanged' : 'warned',\n message: `${relative} \u2014 ${result.status}`,\n level: 'silent',\n }\n}\n\n/**\n * Announce the one state where the two gates disagree: a git root resolved, but no\n * `infra-kit.json` at it.\n *\n * This is the ONLY protection that exists there. `initCore` takes no arguments, and `setup`'s three\n * flags all narrow the DEPENDENCY half, so there is nothing here that could offer an\n * \"outside an infra-kit repo\" confirmation, and it must not prompt because non-TTY runs skip\n * prompts silently \u2014 so a run whose cwd is inside a stranger's project would otherwise write\n * three tracked files, print its success line and exit 0, surfacing later as an unexplained diff.\n *\n * It names the ABSOLUTE root rather than \"this repo\" because the whole failure mode is a caller\n * who is somewhere other than they think. That is truthful only because `resolveGitRoot` rejects a\n * blank resolve: `''` would reach this branch and announce the wrong path while the writers below\n * targeted `process.cwd()`.\n */\nconst gateDisagreementEntries = (gitRoot: string | null, guidanceRoot: string | null): InitEntry[] => {\n if (gitRoot === null || guidanceRoot !== null) return []\n\n return [\n {\n step: 'plugin-pointer',\n outcome: 'warned',\n message: `No infra-kit.json at ${gitRoot} \u2014 setting up Claude Code there anyway: writing .claude/settings.json and .mcp.json, and installing the ${PLUGIN_KEY} plugin for that project. If that is not the repo you meant to set up, revert those two files.`,\n level: 'warn',\n },\n ]\n}\n\n/** The two commands, in order, that a person runs by hand when `initCore` could not run them. */\nconst manualInstallEntries = (): InitEntry[] => {\n return [MARKETPLACE_ADD_COMMAND, PLUGIN_INSTALL_COMMAND].map((command) => {\n return { step: 'plugin-pointer', outcome: 'skipped', message: command, level: 'info' }\n })\n}\n\n/**\n * One line per install outcome.\n *\n * `already-installed` is DEBUG on purpose: it is the steady state of every configured machine, and a\n * setup command that reports its no-ops is a setup command people stop reading. The two failure\n * outcomes are WARN and carry the step, the tool's own first line, and the command to run instead \u2014\n * a warning a reader cannot act on is noise.\n */\nconst installEntries = (outcome: PluginInstallOutcome): InitEntry[] => {\n if (outcome.status === 'already-installed') {\n return [\n {\n step: 'plugin-pointer',\n outcome: 'unchanged',\n message: `Claude Code plugin ${PLUGIN_KEY} is already installed for this project.`,\n level: 'debug',\n },\n ]\n }\n\n if (outcome.status === 'installed') {\n return [\n {\n step: 'plugin-pointer',\n outcome: 'written',\n message: `installed Claude Code plugin ${PLUGIN_KEY} (project scope)`,\n level: 'info',\n },\n ]\n }\n\n if (outcome.status === 'claude-missing') {\n return [\n {\n step: 'plugin-pointer',\n outcome: 'skipped',\n message: 'No `claude` on PATH, so the Claude Code plugin was not installed. Install Claude Code, then run:',\n level: 'info',\n },\n ...manualInstallEntries(),\n ]\n }\n\n const reason =\n outcome.status === 'unverified'\n ? 'the install reported success but Claude Code recorded no installation'\n : `the ${outcome.step} step failed: ${outcome.error}`\n\n return [\n {\n step: 'plugin-pointer',\n outcome: 'warned',\n message: `Could not install the Claude Code plugin \u2014 ${reason}. Run by hand: ${PLUGIN_INSTALL_COMMAND}`,\n level: 'warn',\n },\n ]\n}\n\n/**\n * Point this repo's Claude Code at the infra-kit plugin marketplace, register the MCP server the\n * plugin's skills call, then INSTALL the plugin so a teammate's whole setup is one command.\n *\n * All three are driven from ONE root \u2014 `resolveGitRoot`'s, no longer the guidance sync's \u2014 so the\n * pointer keys, the `.mcp.json` entry and what `--scope project` records name a single project.\n * That binding is the contract here; which predicate produced the root is not.\n *\n * There is deliberately NO opt-out flag. The install is idempotent (an already-installed plugin runs\n * no command at all) and best-effort (every failure is a logged outcome, never a thrown error), so a\n * switch would only buy a way to end up with the pointer keys pointing at a plugin nobody has.\n */\nconst syncPluginPointer = (root: string | null, record: InitStepRecorder): void => {\n if (root === null) {\n // Silent: `resolveGitRootForWrites` has already printed the four-step skip.\n record({\n step: 'plugin-pointer',\n outcome: 'skipped',\n message: 'Claude Code plugin steps skipped \u2014 no git root safe to write into',\n level: 'silent',\n })\n\n return\n }\n\n try {\n // The MCP write goes AHEAD of the install: the install is the only step that spawns a process,\n // so it is the only one that can fail for reasons unrelated to this repo, and a `claude` binary\n // that throws on spawn must not cost the repo its server registration.\n //\n // Recorded one at a time rather than returned as an array: each of the three libraries logs its own\n // refusals, and a batched return would print all three of `initCore`'s lines after all three libraries'\n // \u2014 reordering output that a partial failure makes visible.\n record(pointerEntry(root, ensurePluginPointer(path.join(root, '.claude', 'settings.json'))))\n record(mcpEntry(root, ensureMcpRegistration(root)))\n record(...installEntries(installPluginForProject({ projectRoot: root })))\n } catch (err) {\n // Best-effort \u2014 neither an unwritable `.claude/settings.json`, a hand-broken\n // `~/.claude/plugins/installed_plugins.json`, nor a `claude` binary that throws on spawn may turn\n // a machine-setup command red. But WARN, not debug: `initCore` goes on to print its success line and\n // exit 0, so at `debug` a read-only settings file or a `.mcp.json` that is a directory failed with\n // no output at all. The error text carries the offending path; the root says which project.\n const message = err instanceof Error ? err.message : String(err)\n\n record({\n step: 'plugin-pointer',\n outcome: 'warned',\n message: `Could not finish the Claude Code plugin step for ${root} (${message}). Run by hand: ${PLUGIN_INSTALL_COMMAND}`,\n level: 'warn',\n })\n }\n}\n\nconst isBlockLine = (line: string): boolean => {\n return (\n line.startsWith('#') ||\n line.startsWith('alias ') ||\n line.startsWith('env-load') ||\n line.startsWith('env-clear') ||\n line.startsWith('env-status') ||\n line.startsWith('if ') ||\n line.startsWith(' export INFRA_KIT_SESSION') ||\n line.startsWith('export _INFRA_KIT_') ||\n line.startsWith(': ${_INFRA_KIT_') ||\n line.startsWith('fi') ||\n line.startsWith('zmodload ') ||\n line.startsWith('autoload ') ||\n line.startsWith('add-zsh-hook ') ||\n line.startsWith('_infra_kit_autoload')\n )\n}\n\nexport const removeExistingBlock = (content: string): string => {\n // 1. Current markers\n const result = removeManagedBlock(content, MARKER_START, MARKER_END)\n\n if (result !== null) return result\n\n // 2. Legacy paired markers (# region / # endregion)\n for (const [start, end] of LEGACY_PAIRED) {\n const legacyResult = removeManagedBlock(content, start, end)\n\n if (legacyResult !== null) return legacyResult\n }\n\n // 3. Oldest format: single marker + heuristic scan\n const legacyIdx = content.indexOf(LEGACY_SINGLE)\n\n if (legacyIdx === -1) return content\n\n // eslint-disable-next-line sonarjs/super-linear-regex\n const before = content.slice(0, legacyIdx).replace(/\\n+$/, '')\n const afterLines = content.slice(legacyIdx).split('\\n')\n\n let i = 0\n\n while (i < afterLines.length && isBlockLine(afterLines[i]!)) {\n i++\n }\n\n const remaining = afterLines.slice(i).join('\\n')\n\n return before + (remaining ? `\\n${remaining}` : '')\n}\n\n/**\n * The inner shell-function lines (no markers). Composed into the full marked\n * block by {@link buildShellBlock} and fed to `upsertManagedBlock` by `initCore`.\n */\nexport const buildShellBody = (): string => {\n const runCmd = 'pnpm exec infra-kit'\n\n return [\n 'zmodload zsh/stat 2>/dev/null',\n 'zmodload zsh/datetime 2>/dev/null',\n 'zmodload zsh/sched 2>/dev/null',\n // eslint-disable-next-line no-template-curly-in-string\n 'if [[ -z \"${INFRA_KIT_SESSION}\" ]]; then',\n ' export INFRA_KIT_SESSION=$(head -c 4 /dev/urandom | xxd -p)',\n 'fi',\n // eslint-disable-next-line no-template-curly-in-string\n ': ${_INFRA_KIT_LAST_LOAD_MTIME:=0}',\n // eslint-disable-next-line no-template-curly-in-string\n ': ${_INFRA_KIT_LAST_CLEAR_MTIME:=0}',\n // eslint-disable-next-line no-template-curly-in-string\n ': ${_INFRA_KIT_SHELL_STARTED:=${EPOCHSECONDS:-0}}',\n // Warm-cache source TTL (seconds); MUST equal DEFAULT_WARM_TTL_SECONDS (node\n // eviction) \u2014 both 2h. A warm file older than this is not sourced at startup.\n // eslint-disable-next-line no-template-curly-in-string\n ': ${_INFRA_KIT_WARM_TTL:=7200}',\n 'export _INFRA_KIT_LAST_LOAD_MTIME _INFRA_KIT_LAST_CLEAR_MTIME _INFRA_KIT_SHELL_STARTED _INFRA_KIT_WARM_TTL',\n `env-load() { local f m; f=$(${runCmd} env-load \"$@\") || return; m=$(zstat +mtime -- \"$f\" 2>/dev/null || echo 0); _INFRA_KIT_LAST_LOAD_MTIME=$m; source \"$f\"; ${runCmd} env-status; }`,\n `env-clear() { local f m; f=$(${runCmd} env-clear \"$@\") || return; m=$(zstat +mtime -- \"$f\" 2>/dev/null || echo 0); _INFRA_KIT_LAST_CLEAR_MTIME=$m; source \"$f\"; ${runCmd} env-status; }`,\n `env-status() { ${runCmd} env-status; }`,\n // No `alias ik=\u2026` here: a global install provides real `infra-kit` and `ik` bins, and an alias\n // would SHADOW the global `ik`, forcing the project-local `pnpm exec` even when a global CLI exists.\n // (`isBlockLine` still lists `alias ` so re-running `initCore` strips the stale alias from old configs.)\n // Print an async notice without corrupting an already-drawn prompt. The\n // startup poll fires via `sched` at an IDLE prompt (ZLE active) which does\n // NOT redraw the prompt, so a bare `print` lands appended to the visible\n // prompt line and looks like un-removable typed input. When ZLE is active we\n // erase the current line (CR + clear-to-EOL), print the message on its own\n // line, then `zle reset-prompt` to redraw the prompt below it. In precmd /\n // shell-startup (ZLE inactive) `zle` is false and this is a plain stderr\n // print above the about-to-be-drawn prompt \u2014 unchanged behavior.\n '_infra_kit_notify() {',\n // When ZLE is active, erase the current prompt line first and redraw after;\n // both are no-ops when ZLE is inactive so this stays a plain stderr print.\n \" zle && print -u2 -n $'\\\\r\\\\e[K'\",\n ' print -u2 -- \"$1\"',\n ' zle && zle reset-prompt',\n '}',\n '_infra_kit_autoload() {',\n ' [[ -z \"$INFRA_KIT_SESSION\" ]] && return',\n // eslint-disable-next-line no-template-curly-in-string\n ' local cache_root=\"${XDG_CACHE_HOME:-$HOME/.cache}/infra-kit\"',\n ' local dir=\"$cache_root/$INFRA_KIT_SESSION\"',\n ' local load_file=\"$dir/env-load.sh\"',\n ' local clear_file=\"$dir/env-clear.sh\"',\n ' local load_mtime=0 clear_mtime=0',\n ' [[ -f \"$load_file\" ]] && load_mtime=$(zstat +mtime -- \"$load_file\" 2>/dev/null || echo 0)',\n ' [[ -f \"$clear_file\" ]] && clear_mtime=$(zstat +mtime -- \"$clear_file\" 2>/dev/null || echo 0)',\n ' if (( load_mtime > _INFRA_KIT_LAST_LOAD_MTIME && load_mtime >= _INFRA_KIT_SHELL_STARTED && load_mtime >= clear_mtime )); then',\n ' source \"$load_file\"',\n ' _INFRA_KIT_LAST_LOAD_MTIME=$load_mtime',\n // eslint-disable-next-line no-template-curly-in-string\n ' _infra_kit_notify \"infra-kit: auto-loaded vars for ${INFRA_KIT_ENV_CONFIG:-?}\"',\n ' fi',\n ' if (( clear_mtime > _INFRA_KIT_LAST_CLEAR_MTIME && clear_mtime >= _INFRA_KIT_SHELL_STARTED && clear_mtime > load_mtime )); then',\n ' source \"$clear_file\"',\n ' _INFRA_KIT_LAST_CLEAR_MTIME=$clear_mtime',\n ' _infra_kit_notify \"infra-kit: auto-cleared env\"',\n ' fi',\n '}',\n 'autoload -Uz add-zsh-hook',\n 'if (( _INFRA_KIT_SHELL_STARTED > 0 )); then',\n ' add-zsh-hook precmd _infra_kit_autoload',\n 'fi',\n // Non-blocking startup poll: the shell-startup spawn (below) writes env-load.sh\n // asynchronously (node + Doppler fetch, ~0.5-2s), so the FIRST precmd \u2014 which\n // fires before the first prompt \u2014 runs before the file lands and sources nothing;\n // without this the vars only appear after the user's first command. This one-shot\n // re-arming poll re-runs the precmd sourcer once per second, at the idle prompt\n // with NO user command, until it sources the freshly written file (its mtime is\n // >= _INFRA_KIT_SHELL_STARTED so the existing gate accepts it) OR the startup\n // window closes. _infra_kit_autoload sets _INFRA_KIT_LAST_LOAD_MTIME on a\n // successful source, which lifts it to >= _INFRA_KIT_SHELL_STARTED and stops the\n // re-arm. Bounded to WINDOW seconds so an unconfigured project / down Doppler\n // that never writes a file only costs a handful of no-op ticks, then falls back\n // to the precmd hook. Sources the same freshly written file as precmd, so there\n // is no stale/last-known-good secrets window.\n '_INFRA_KIT_AUTOLOAD_WINDOW=6',\n 'export _INFRA_KIT_AUTOLOAD_WINDOW',\n '_infra_kit_poll_autoload() {',\n ' _infra_kit_autoload',\n // eslint-disable-next-line no-template-curly-in-string\n ' if (( _INFRA_KIT_LAST_LOAD_MTIME < _INFRA_KIT_SHELL_STARTED )) && (( ${EPOCHSECONDS:-0} - _INFRA_KIT_SHELL_STARTED < _INFRA_KIT_AUTOLOAD_WINDOW )); then',\n ' (( $+builtins[sched] )) && sched +1 _infra_kit_poll_autoload',\n ' fi',\n '}',\n // Warm cache: source the project-scoped last-known-good env-load.sh INSTANTLY at\n // startup (pure zsh, no node/Doppler) so vars are present at prompt-0, before the\n // backgrounded refresh lands. `key` is sha256 of the canonical project dir passed\n // in by the caller \u2014 byte-identical to node's warmCacheKey (printf %s | shasum,\n // full 64-hex). Skips when: no sha tool (degrade to poll), no warm file, a project\n // clear-marker is at least as new (clear wins ties), or the file is past TTL\n // (bounds a rotated secret served at prompt-0). Deliberately does NOT set\n // _INFRA_KIT_LAST_LOAD_MTIME (leaves it 0) so the poll/precmd still source the\n // fresh SESSION file (different path, newer mtime) and upgrade warm->fresh.\n '_infra_kit_warm_source() {',\n ' local canon=\"$1\" key',\n ' if (( $+commands[shasum] )); then',\n ' key=$(printf %s \"$canon\" | shasum -a 256 | cut -c1-64)',\n ' elif (( $+commands[sha256sum] )); then',\n ' key=$(printf %s \"$canon\" | sha256sum | cut -c1-64)',\n ' else',\n ' return',\n ' fi',\n // eslint-disable-next-line no-template-curly-in-string\n ' local cache_root=\"${XDG_CACHE_HOME:-$HOME/.cache}/infra-kit\"',\n ' local warm=\"$cache_root/projects/$key/env-load.sh\"',\n ' local wclear=\"$cache_root/projects/$key/env-clear.sh\"',\n ' [[ -f \"$warm\" ]] || return',\n ' local wm=0 cm=0',\n ' wm=$(zstat +mtime -- \"$warm\" 2>/dev/null || echo 0)',\n ' [[ -f \"$wclear\" ]] && cm=$(zstat +mtime -- \"$wclear\" 2>/dev/null || echo 0)',\n ' (( cm >= wm )) && return',\n // eslint-disable-next-line no-template-curly-in-string\n ' (( ${EPOCHSECONDS:-0} - wm >= _INFRA_KIT_WARM_TTL )) && return',\n ' source \"$warm\"',\n // eslint-disable-next-line no-template-curly-in-string\n ' _infra_kit_notify \"infra-kit: loaded cached vars for ${INFRA_KIT_ENV_CONFIG:-?} (refreshing\u2026)\"',\n '}',\n // One-shot env auto-load when a NEW shell opens inside an infra-kit project\n // or worktree (config: envAutoLoad.trigger \"shell-startup\"). Cheap pure-zsh\n // project gate (walk up for infra-kit.json) avoids spawning node in unrelated\n // shells; the spawn is skipped only when the already-loaded env belongs to THIS\n // project root (INFRA_KIT_ENV_PROJECT_ROOT), so cd'ing into a DIFFERENT project\n // re-loads instead of silently keeping the previous project's secrets. The spawn\n // is DETACHED+backgrounded ( ... & ) so it never blocks the prompt; it only\n // WRITES env-load.sh. The startup poll armed right after (and the precmd hook as\n // a later-prompt fallback) sources the file once it lands. Trade-off: the gate\n // can't read the merged JSON config, so a fresh shell in a project WITHOUT\n // envAutoLoad still spawns one background process that resolves config, finds\n // nothing, and exits.\n '_infra_kit_startup_autoload() {',\n ' [[ -z \"$INFRA_KIT_SESSION\" ]] && return',\n ' local dir=\"$PWD\" prev=\"\"',\n ' while [[ -n \"$dir\" && \"$dir\" != \"$prev\" ]]; do',\n ' if [[ -f \"$dir/infra-kit.json\" ]]; then',\n ' [[ -n \"$INFRA_KIT_ENV_CONFIG\" && \"$dir\" == \"$INFRA_KIT_ENV_PROJECT_ROOT\" ]] && return',\n // canon = realpath of the project dir; the SAME string node hashes for the warm\n // key (passed via --project-dir), so the two keys are byte-identical.\n // eslint-disable-next-line no-template-curly-in-string\n ' local canon=\"${dir:A}\"',\n ' _infra_kit_warm_source \"$canon\"',\n ` ( ${runCmd} env-autoload --project-dir \"$canon\" & ) >/dev/null 2>&1`,\n ' (( $+builtins[sched] )) && sched +1 _infra_kit_poll_autoload',\n ' return',\n ' fi',\n ' prev=\"$dir\"',\n // eslint-disable-next-line no-template-curly-in-string\n ' dir=\"${dir:h}\"',\n ' done',\n '}',\n 'if (( _INFRA_KIT_SHELL_STARTED > 0 )); then',\n ' _infra_kit_startup_autoload',\n 'fi',\n ].join('\\n')\n}\n\n/**\n * The full marker-delimited shell block (`MARKER_START \u2026 MARKER_END`). Kept as\n * a single composed string so `doctor`'s exact-match freshness check stays valid.\n */\nexport const buildShellBlock = (): string => {\n return `${MARKER_START}\\n${buildShellBody()}\\n${MARKER_END}`\n}\n", "// DEFAULT import, deliberately: the module is used as `fs.writeFile(...)` / `fs.mkdir(...)` so every\n// call is a property lookup on the live module object, which is what `vi.spyOn(fs, 'writeFile')`\n// patches. A named import (`import { writeFile }`) is bound at import time and the spy silently does\n// NOT intercept it \u2014 verified empirically (default form: spy sees 1 call; named form: spy sees 0\n// calls AND the real write still happens). The \"0 writes in steady state\" tests would then be false\n// greens while the seed churned the disk on every command. Do not \"modernize\" this import.\nimport fs from 'node:fs/promises'\nimport path from 'node:path'\nimport process from 'node:process'\n\nimport { CONFIG_STUB, buildUserProjectExample } from 'src/lib/config-templates'\nimport { getInfraKitConfigPaths, resetMergedConfigCache } from 'src/lib/infra-kit-config'\nimport type { InfraKitConfigPaths } from 'src/lib/infra-kit-config'\nimport { logger } from 'src/lib/logger'\nimport { fileExists, tildify } from 'src/lib/path-display'\n\nexport interface SeedUserProjectResult {\n configPath: string\n examplePath: string\n createdConfig: boolean\n refreshedExample: boolean\n}\n\n/**\n * Once-per-process guard for {@link ensureUserProjectConfig}. The MCP server is long-lived and\n * handles N tool calls in one process \u2014 without this it would re-stat the config on every call.\n */\nlet seeded = false\n\n/**\n * Derive the non-loaded `.example.jsonc` sibling for a config path. The config loader only globs the\n * three exact `infra-kit.json` filenames, so the sibling never enters the merge chain.\n *\n * @example\n * exampleSiblingPath('/u/.infra-kit/projects/api/infra-kit.json')\n * // => '/u/.infra-kit/projects/api/infra-kit.example.jsonc'\n */\nconst exampleSiblingPath = (jsonPath: string): string => {\n return jsonPath.replace(/\\.json$/, '.example.jsonc')\n}\n\n/**\n * Read a file, degrading a missing/unreadable file to `null` so callers can content-compare without\n * branching on ENOENT.\n *\n * @example\n * await readFileOrNull('/nope.jsonc') // => null\n */\nconst readFileOrNull = async (filePath: string): Promise<string | null> => {\n try {\n return await fs.readFile(filePath, 'utf-8')\n } catch {\n return null\n }\n}\n\n/**\n * Create the layer-3 user-project override file (`~/.infra-kit/projects/<repo>/infra-kit.json`) and\n * keep its annotated `.example.jsonc` sibling in sync with the current schema.\n *\n * UNGATED and MAY THROW \u2014 this is the primitive, shared by `config edit` and by the entry-boundary\n * wrapper {@link ensureUserProjectConfig}. It deliberately does NOT honour `INFRA_KIT_NO_SEED`\n * (that kill switch guards the wrapper only) and does NOT check for a committed project config.\n *\n * An existing `infra-kit.json` is NEVER touched \u2014 user bytes are sacred. The example is rewritten\n * only when its content differs (content compare, not mtime), so the steady state performs zero\n * writes. The MERGED config cache is reset only when the `.json` was actually created: the `.jsonc`\n * example is not part of the merge chain, so refreshing it invalidates nothing. The path memo is\n * left alone either way \u2014 see {@link resetMergedConfigCache}.\n *\n * @example\n * const paths = await getInfraKitConfigPaths()\n * await seedUserProjectConfig(paths)\n * // => { configPath: '/u/.infra-kit/projects/api/infra-kit.json',\n * // examplePath: '/u/.infra-kit/projects/api/infra-kit.example.jsonc',\n * // createdConfig: true, refreshedExample: true }\n */\nexport const seedUserProjectConfig = async (paths: InfraKitConfigPaths): Promise<SeedUserProjectResult> => {\n const configPath = paths.userProject\n const examplePath = exampleSiblingPath(configPath)\n\n await fs.mkdir(path.dirname(configPath), { recursive: true })\n\n const createdConfig = !(await fileExists(configPath))\n\n if (createdConfig) {\n // JSON can't carry comments, so seed an empty-but-valid config and drop the annotated guidance\n // next to it in a non-loaded .example.jsonc.\n await fs.writeFile(configPath, CONFIG_STUB, 'utf-8')\n }\n\n const example = buildUserProjectExample(paths.projectName)\n const refreshedExample = (await readFileOrNull(examplePath)) !== example\n\n if (refreshedExample) {\n await fs.writeFile(examplePath, example, 'utf-8')\n }\n\n if (createdConfig) {\n // Only the .json is a real merge layer \u2014 never invalidate the cache for a .jsonc-only refresh.\n // MERGED cache only: the new layer changes the merge result, but the resolved paths are a pure\n // function of cwd + homedir, so dropping the path memo here would re-spawn both `git rev-parse`\n // calls on the one run that creates the file.\n resetMergedConfigCache()\n }\n\n return { configPath, examplePath, createdConfig, refreshedExample }\n}\n\n/**\n * The one INFO line announcing a freshly created layer-3 config. Shared by all three seed call sites\n * (the entry-boundary bootstrap below, `config edit`, and `initCore`) \u2014 they announce the same event and\n * must not drift into three wordings.\n *\n * @example\n * seedCreatedMessage(result)\n * // => 'Created ~/.infra-kit/projects/api/infra-kit.json \u2014 see ~/.infra-kit/projects/api/infra-kit.example.jsonc for the annotated reference.'\n */\nexport const seedCreatedMessage = (result: SeedUserProjectResult): string => {\n return `Created ${tildify(result.configPath)} \u2014 see ${tildify(result.examplePath)} for the annotated reference.`\n}\n\n/**\n * Entry-boundary bootstrap: gate, seed, swallow. NEVER throws and NEVER changes the exit code, so it\n * is safe to await from a CLI preAction hook or an MCP tool-call boundary.\n *\n * Order: the `INFRA_KIT_NO_SEED` kill switch (truthiness, so `vi.stubEnv(\u2026, '')` disarms it), the\n * once-per-process guard, path resolution (throws outside a git repo \u2192 silent no-op), then the D2\n * gate \u2014 no committed `<repo-root>/infra-kit.json` means this is not an infra-kit project, so\n * nothing is ever created under `~/.infra-kit/projects/` for it.\n *\n * Any seed failure (EACCES / EROFS / ENOSPC / read-only HOME) is `logger.debug`-ed and swallowed.\n * Exactly one `logger.info` line is emitted, and only when the `.json` was actually created; an\n * example-only refresh is `logger.debug` (rewriting a non-loaded reference file is not news).\n * Steady state is completely silent.\n *\n * @example\n * // CLI preAction / MCP tool boundary\n * await ensureUserProjectConfig()\n * // first run in an infra-kit repo \u2014 INFO: Created ~/.infra-kit/projects/api/infra-kit.json \u2014 \u2026\n * // every later run \u2014 silent, zero writes\n */\nexport const ensureUserProjectConfig = async (): Promise<void> => {\n if (process.env.INFRA_KIT_NO_SEED) {\n return\n }\n\n if (seeded) {\n return\n }\n\n seeded = true\n\n let paths: InfraKitConfigPaths\n\n try {\n paths = await getInfraKitConfigPaths()\n } catch {\n // Outside a git repo `git rev-parse --show-toplevel` fails. Not an infra-kit project \u2014 no-op.\n return\n }\n\n if (!(await fileExists(paths.main))) {\n return\n }\n\n try {\n const result = await seedUserProjectConfig(paths)\n\n if (result.createdConfig) {\n logger.info(seedCreatedMessage(result))\n } else if (result.refreshedExample) {\n logger.debug(`Refreshed ${tildify(result.examplePath)} to match the current schema.`)\n }\n } catch (err) {\n logger.debug({ err, msg: `Skipped seeding ${tildify(paths.userProject)} (config bootstrap failed).` })\n }\n}\n\n/**\n * Clear the once-per-process seed guard. Tests only \u2014 production seeds exactly once per process.\n *\n * @example\n * resetUserProjectSeedGuard()\n * await ensureUserProjectConfig() // resolves paths again instead of short-circuiting\n */\nexport const resetUserProjectSeedGuard = (): void => {\n seeded = false\n}\n", "/**\n * The annotated JSONC reference templates for every infra-kit config file, in one\n * place. Extracted from `commands/init` (layers 2 + vendor) and `commands/config`\n * (layer 3), which each carried a private copy that drifted out of sync with the\n * schema.\n *\n * PURE by construction: strings only, zero I/O \u2014 no `node:fs`, `node:os` or\n * `node:path` here. Callers own the writing; this module owns the words. That is\n * what lets `config-templates.test.ts` diff the templates against\n * `infraKitConfigObject.shape` with no filesystem fakes.\n */\n\n/**\n * The real config files are strict JSON and cannot carry comments, so every seeded\n * `infra-kit.json` starts life as an empty-but-valid object; the guidance lives in\n * the non-loaded `.example.jsonc` sibling built below.\n */\nexport const CONFIG_STUB = '{}\\n'\n\n// The documented key set, shared verbatim by the layer-2 (user-global) and layer-3\n// (user-project) examples so the two can never disagree about what the schema is.\n// Covers ALL nine top-level keys of `infraKitConfigObject` \u2014 `config-templates.test.ts`\n// fails the build if a tenth is added to the schema and not documented here.\n//\n// Every line is a `//` comment: the surrounding braces are the only live JSON, so the\n// file always parses to `{}` once the comments are stripped.\nconst CONFIG_KEY_DOCS = ` // \"envManagement\": { // required in layer 1; provider-tagged\n // \"provider\": \"doppler\",\n // \"config\": { \"name\": \"my-doppler-project\" }\n // },\n //\n // \"ide\": {\n // \"provider\": \"cursor\",\n // \"config\": { \"workspaceConfigPath\": \"/path/to/your.code-workspace\" }\n // },\n // // Or, for Zed (no workspace file \u2014 one window with all worktrees via \"zed <root> <wt...>\"):\n // \"ide\": { \"provider\": \"zed\", \"config\": {} },\n // // Or drive BOTH editors at once with an array (at most one entry per provider):\n // \"ide\": [\n // { \"provider\": \"cursor\", \"config\": { \"workspaceConfigPath\": \"/path/to/your.code-workspace\" } },\n // { \"provider\": \"zed\", \"config\": {} }\n // ],\n //\n // \"taskManager\": {\n // \"provider\": \"jira\",\n // \"config\": { \"baseUrl\": \"https://acme.atlassian.net\", \"projectId\": 123 }\n // },\n // NOTE: taskManager is validated but NOT yet used \u2014 Jira credentials and the project id are read\n // from the JIRA_BASE_URL / JIRA_EMAIL / JIRA_TOKEN / JIRA_PROJECT_ID environment variables\n // (Doppler), never from this file. Editing projectId here changes nothing; change it in Doppler.\n //\n // \"worktrees\": {\n // \"openInGithubDesktop\": false,\n // \"openInCmux\": true,\n // // cmux pane layout for opened worktrees: \"two-columns\" (default, left | right)\n // // or \"three-pane\" (left split top/bottom + full-height right).\n // \"cmux\": { \"layout\": \"two-columns\" }\n // },\n //\n // // Auto-load Doppler env when working inside this project/worktree. Omit to\n // // disable. \"trigger\" (pick one): \"shell-startup\" (new shells) | \"cli-invocation\"\n // // (before each infra-kit command, primes subsequent commands). \"config\" is the\n // // environment to load \u2014 must have a service token (infra-kit env-token-set) or\n // // auto-load quietly disables. Requires the committed infra-kit.json at the git\n // // repo root, and the zsh shell integration (infra-kit init + a new shell). zsh only.\n // \"envAutoLoad\": { \"trigger\": \"shell-startup\", \"config\": \"dev\" },\n //\n // // Per-app local-dev overrides, keyed by APP FOLDER name (the directory under\n // // apps/). Both fields optional: \"port\" pins the local listen port (otherwise\n // // <APP>_PORT / PORT / a built-in default), \"prefixUrl\" pins the URL prefix the\n // // app is served under. An unknown app name is ignored at resolve time, never a\n // // parse error.\n // \"dev\": {\n // \"client\": { \"port\": 4001, \"prefixUrl\": \"/api\" }\n // },\n //\n // // Named \\`infra-kit dev <preset>\\` sessions. The keys under \"apps\" are PACKAGES \u2014\n // // \"<app>/api\" or \"<app>/ui\". A bare \"<app>\" is a FOLDER, not a package, and is\n // // REJECTED. Per target: \"watchDeps\" (api targets) rebuilds the shared-package\n // // dependency closure and restarts on change; \"proxy\" maps a route path to\n // // \"local\" | \"cloud\" for this session only. \"cmux\": true runs each launched target\n // // in its own cmux pane. Omit \"apps\" entirely to launch every discovered target.\n // \"devServersPresets\": {\n // \"backend\": {\n // \"apps\": {\n // \"client/api\": { \"watchDeps\": true, \"proxy\": { \"/api\": \"local\", \"/auth\": \"cloud\" } },\n // \"client/ui\": {}\n // },\n // \"cmux\": true\n // }\n // },\n //\n // // DEPRECATED \u2014 accepted but IGNORED; safe to delete. Remove it and this note in a\n // // future release.\n // //\n // // The proxy port is no longer configurable. Every dev URL is now\n // // https://<release>.<package>.localhost with NO port, and the only port that can\n // // serve a port-free HTTPS URL is 443 \u2014 so a setting here could only put the port\n // // back into the URL. Set up the daemon once (the install is the only step needing\n // // root; trusting its local CA does not). Run \\`infra-kit doctor\\`: it checks both and\n // // prints each fix as a command you can paste, with the paths filled in for your\n // // machine. Do not type a bare \\`portless\\` \u2014 it lives in node_modules, not on PATH.\n // // infra-kit itself never elevates.\n // \"devProxy\": { \"port\": 443 },\n //\n // // May this project deploy to the delivery-shaped environments (\"prod\")? The LIST lives in code\n // // (lib/workflow-envs/protected-envs); this decides only whether THIS project may reach it.\n // // \"disallow\" \u2014 the DEFAULT when the key is absent. Filtered out of every deploy picker and\n // // refused, on both \\`--from local\\` and \\`--from ci\\`. Use\n // // \\`infra-kit release deliver\\`, which also does the RC PR and the Jira version.\n // // \"allow\" \u2014 reachable from the CLI and from an MCP agent alike.\n // // \"cli-only\" \u2014 reachable from a terminal, but NOT over MCP: an agent's tool call carries no\n // // human keystroke, so agents keep the refusal.\n // // Allowing it does not remove the other gates: a local deploy still requires the AWS account to\n // // report that environment, and still refuses a dirty tree \u2014 unconditionally, with nothing to pass\n // // that waives it. A \\`--from ci\\` dispatch has no such second gate and prints a warning naming\n // // what it skips.\n // //\n // // It IS honoured from this layer \u2014 the merge treats it like any other key \u2014 but it belongs in the\n // // committed project infra-kit.json, where a reviewer and \\`git blame\\` can see it. Setting it here\n // // grants yourself production access with no trace in the repo.\n // \"protectedEnvs\": \"disallow\"\n //\n // // Doppler SERVICE TOKENS are not a config key and never belong in this file. They live in\n // // tokens.json \u2014 a SIBLING of this file, at ~/.infra-kit/projects/<repo>/tokens.json (mode 0600) \u2014\n // // shaped { \"envs\": { \"<env>\": \"dp.st\u2026\" } }. Write it with \\`infra-kit env-token-set <env>\\` (which\n // // also fixes its modes), or by hand: both are supported, which is why the store carries no\n // // repo-identity field and no mandatory version. Pasting a token into an\n // // infra-kit.json (any layer) is REJECTED: the loader refuses to parse it and tells you to revoke\n // // the token first, because a config file can be committed, backed up by your editor, or shared.`\n\n/**\n * Annotated JSONC reference for the user-global (layer 2) config, written next to the\n * real `~/.infra-kit/infra-kit.json` by `infra-kit setup`. Documents every top-level\n * key of the schema.\n *\n * @example\n * buildUserGlobalExample()\n * // => '// infra-kit user-global config \u2014 ~/.infra-kit/infra-kit.json\\n\u2026\\n{\\n // \"envManagement\": \u2026\\n}\\n'\n */\nexport const buildUserGlobalExample = (): string => {\n return `// infra-kit user-global config \u2014 ~/.infra-kit/infra-kit.json\n//\n// Merge chain (later layers override earlier ones at top-level keys):\n// 1. <repo>/infra-kit.json \u2014 committed project config (required)\n// 2. ~/.infra-kit/infra-kit.json \u2014 user-global (the sibling of this file)\n// 3. ~/.infra-kit/projects/<repo-name>/infra-kit.json \u2014 user-scope per-project override\n//\n// Merge is shallow: setting a top-level key replaces that whole section from\n// layer 1. Arrays do not concatenate. Top-level keys recognized:\n// envManagement, ide, taskManager, worktrees, envAutoLoad, dev,\n// devServersPresets, devProxy, protectedEnvs. The schema is strict \u2014 an\n// unrecognized top-level key is a parse error, not a silently ignored one.\n//\n// This .example.jsonc is reference only \u2014 it is NOT loaded. Put real global\n// overrides in the sibling infra-kit.json (strict JSON: no comments, double-quoted\n// keys). Per-project tweaks belong in layer 3 \u2014 run \\`infra-kit config edit\\`.\n//\n// Every recognized key is documented below. NOTE: \\`envManagement\\` is REQUIRED in\n// the committed project infra-kit.json (layer 1) and is usually NOT set in this\n// user-global layer \u2014 it is shown here only to document the full, valid config shape.\n{\n${CONFIG_KEY_DOCS}\n}\n`\n}\n\n/**\n * Annotated JSONC reference for the user-scope per-project override (layer 3), written\n * alongside the real `~/.infra-kit/projects/<projectName>/infra-kit.json`. Same key\n * documentation as layer 2 \u2014 only the header differs. Deterministic: the project name\n * is the only input, and no path or timestamp from the host leaks into the output.\n *\n * @example\n * buildUserProjectExample('hulyo-monorepo')\n * // => '// infra-kit user override for hulyo-monorepo \u2014 ~/.infra-kit/projects/hulyo-monorepo/infra-kit.json\\n\u2026'\n */\nexport const buildUserProjectExample = (projectName: string): string => {\n return `// infra-kit user override for ${projectName} \u2014 ~/.infra-kit/projects/${projectName}/infra-kit.json\n//\n// Layer 3 (highest precedence) of the config merge chain. Shallow-merged on top of\n// <repo>/infra-kit.json (layer 1) and ~/.infra-kit/infra-kit.json (layer 2) \u2014 a\n// top-level key set here replaces that whole section wholesale; arrays do not\n// concatenate. Top-level keys recognized: envManagement, ide,\n// taskManager, worktrees, envAutoLoad, dev, devServersPresets, devProxy,\n// protectedEnvs. The schema is strict \u2014 an unrecognized top-level key is a parse\n// error, not a silently ignored one.\n//\n// This .example.jsonc is reference only \u2014 it is NOT loaded. Put real overrides\n// in the sibling infra-kit.json (strict JSON: no comments, double-quoted keys).\n//\n// Every recognized key is documented below. NOTE: \\`envManagement\\` is REQUIRED in\n// the committed project infra-kit.json (layer 1) and is usually NOT set in this\n// override layer \u2014 it is shown here only to document the full, valid config shape.\n{\n${CONFIG_KEY_DOCS}\n}\n`\n}\n\n/**\n * Annotated JSONC reference for the machine-local factory registry\n * (`~/.infra-kit/vendor.json`). Independent of the infra-kit.json merge chain: the real\n * file is scaffolded by `infra-kit vendor config --init`, never seeded by `setup`.\n *\n * @example\n * buildVendorExample()\n * // => '// infra-kit factory registry \u2014 ~/.infra-kit/vendor.json\\n\u2026\\n{\\n // \"workspaceDir\": \u2026\\n}\\n'\n */\nexport const buildVendorExample = (): string => {\n return `// infra-kit factory registry \u2014 ~/.infra-kit/vendor.json\n//\n// Machine-local registry the vendor commands (sync/manifest/diff) read to know\n// where your project repos live and which ones to stamp. This .example.jsonc is\n// reference only \u2014 it is NOT loaded. The real file is the strict-JSON sibling\n// vendor.json (no comments, double-quoted keys); run \\`infra-kit vendor config --init\\`\n// to scaffold it.\n{\n // \"workspaceDir\": \"~/projects\", // string (absolute or ~-prefixed) \u2014 where target repos are cloned\n // \"targets\": [\"my-repo-a\", \"my-repo-b\"] // string[] (>=1) \u2014 repo dir names resolved under workspaceDir\n}\n`\n}\n", "import { spawnSync } from 'node:child_process'\n\nimport { logger } from 'src/lib/logger'\n\nimport { isMarketplaceRegistered, resolvePluginInstall } from './install-state'\nimport { MARKETPLACE_REPO, PLUGIN_KEY } from './plugin-pointer'\n\n/**\n * @fileoverview\n *\n * The step that makes `infra-kit setup` enough on its own: it DRIVES `claude plugin \u2026` so a teammate\n * does not have to copy two commands out of the terminal and run them by hand.\n *\n * The design constraint is that this shells out to somebody else's CLI. So every step is guarded by a\n * host-state READ first (`install-state.ts`), never by a second invocation: an already-installed\n * plugin runs nothing at all, an already-registered marketplace skips its `add`, and a machine with\n * no `claude` on PATH is a reported outcome rather than a spawn error. That is what makes re-running\n * `initCore` on a configured machine free, and what keeps this from turning a setup command red.\n *\n * TWO RULES THAT ARE NOT NEGOTIABLE.\n *\n * `--scope project`, always. The `claude` default is `user`, which would activate the plugin's skills\n * in every repository the person opens; every skill description costs context on every turn, and\n * these are written against one family's conventions. Project scope also writes `enabledPlugins` into\n * the same `.claude/settings.json` the pointer step manages, so the two agree by construction.\n *\n * Nothing here WRITES to `~/.claude/`. `installed_plugins.json` is Claude Code's bookkeeping; forging\n * a record there would make `doctor` green while the plugin was absent. When the command reports\n * success and the record still does not appear, that is `unverified` \u2014 a soft failure, reported as\n * one \u2014 precisely because the only honest source for \"is it installed\" is the file Claude Code owns.\n */\n\n/** Trust the `claude` binary resolved from `PATH`; there is no configured path and none is wanted. */\nconst CLAUDE_BIN = 'claude'\n\n/**\n * Ceiling for any one `claude` invocation. `plugin install` clones a marketplace repo, so it is not\n * instant \u2014 but an `initCore` that hangs forever on a wedged network is worse than one that reports a\n * failed install step, and the user's remaining setup steps are behind this call.\n */\nconst CLAUDE_TIMEOUT_MS = 120_000\n\n/** `claude plugin marketplace add ArthurSaenz/infra-kit`, as argv. */\nexport const MARKETPLACE_ADD_ARGV: readonly string[] = ['plugin', 'marketplace', 'add', MARKETPLACE_REPO]\n\n/** `claude plugin install infra-kit@infra-kit --scope project`, as argv. Never `--scope user`. */\nexport const PLUGIN_INSTALL_ARGV: readonly string[] = ['plugin', 'install', PLUGIN_KEY, '--scope', 'project']\n\n/** The PATH probe. Cheapest command that fails loudly when the binary is absent. */\nexport const CLAUDE_VERSION_ARGV: readonly string[] = ['--version']\n\n/** One `claude` invocation. `cwd` matters: `--scope project` records the directory it ran in. */\nexport interface ClaudeCommand {\n args: readonly string[]\n cwd?: string\n}\n\nexport interface ClaudeCommandResult {\n ok: boolean\n /** Diagnostic text from the command. Only read when `ok` is false. */\n output?: string\n}\n\n/** The injectable seam. Tests pass a fake so a suite never installs a plugin on the developer's machine. */\nexport type ClaudeRunner = (command: ClaudeCommand) => ClaudeCommandResult\n\n/**\n * Run one `claude` command, capturing its output instead of inheriting the terminal.\n *\n * Captured, not inherited, because `initCore` renders its own progress: a raw `plugin install` transcript\n * dumped between two `INFO:` lines reads as a crash. The captured text is not discarded \u2014 its first\n * line is what the failure warning quotes.\n *\n * @example\n * defaultClaudeRunner({ args: ['--version'] }) // => { ok: true, output: '2.0.0 (Claude Code)' }\n */\nexport const defaultClaudeRunner: ClaudeRunner = (command) => {\n // PATH lookup is the point: `claude` is installed by its own installer to a location this CLI does\n // not know and must not guess, and \"is it on PATH\" is exactly the question step (b) asks.\n // eslint-disable-next-line sonarjs/no-os-command-from-path\n const result = spawnSync(CLAUDE_BIN, [...command.args], {\n cwd: command.cwd,\n encoding: 'utf-8',\n stdio: ['ignore', 'pipe', 'pipe'],\n timeout: CLAUDE_TIMEOUT_MS,\n })\n\n if (result.error !== undefined) return { ok: false, output: result.error.message }\n\n const output = `${result.stderr ?? ''}\\n${result.stdout ?? ''}`.trim()\n\n return { ok: result.status === 0, output }\n}\n\n/** What `installPluginForProject` did. Every branch is a REPORTED outcome; none of them throws. */\nexport type PluginInstallOutcome =\n | { status: 'already-installed' }\n | { status: 'claude-missing' }\n | { status: 'installed' }\n | { status: 'unverified' }\n | { status: 'failed'; step: 'marketplace' | 'install'; error: string }\n\nexport interface InstallPluginOptions {\n /** The repo the plugin is installed FOR \u2014 both the `cwd` of the install and the verified root. */\n projectRoot: string\n /** Command runner. Defaults to spawning the `claude` binary from `PATH`. */\n run?: ClaudeRunner\n /** Override `$HOME` for the host-state reads (tests, and nothing else). */\n home?: string\n}\n\n/** The single question that decides \"already installed\" and \"verified installed\" alike. */\nconst isInstalledFor = (projectRoot: string, home?: string): boolean => {\n return resolvePluginInstall({ projectPath: projectRoot, home }).kind === 'installed'\n}\n\n/** First non-empty line of a command's output \u2014 a warning quotes one line, never a transcript. */\nconst firstLine = (text: string | undefined): string => {\n const line = (text ?? '').split('\\n').find((candidate) => {\n return candidate.trim().length > 0\n })\n\n return (line ?? '').trim()\n}\n\n/**\n * Register the marketplace when it is not already known to this machine.\n *\n * Returns the failure to report, or `null` when the marketplace is usable \u2014 including the common case\n * where it was already registered and nothing ran.\n */\nconst ensureMarketplace = (run: ClaudeRunner, home?: string): { step: 'marketplace'; error: string } | null => {\n if (isMarketplaceRegistered(home)) return null\n\n logger.debug({ msg: `Running: ${CLAUDE_BIN} ${MARKETPLACE_ADD_ARGV.join(' ')}` })\n\n const result = run({ args: MARKETPLACE_ADD_ARGV })\n\n return result.ok ? null : { step: 'marketplace', error: firstLine(result.output) }\n}\n\n/**\n * Install the `infra-kit` Claude Code plugin for `projectRoot`, at PROJECT scope, idempotently.\n *\n * Reads host state before each step, so a configured machine runs no commands at all; a `claude`\n * binary that is absent, a marketplace that will not register, an install that fails, and an install\n * that reports success without leaving a record are four DISTINCT outcomes, because the fix differs\n * for each and the caller prints them differently.\n *\n * @example\n * installPluginForProject({ projectRoot: '/repo' })\n * // first run: { status: 'installed' }\n * // second run: { status: 'already-installed' } \u2014 nothing spawned\n */\nexport const installPluginForProject = (options: InstallPluginOptions): PluginInstallOutcome => {\n const { projectRoot, home } = options\n const run = options.run ?? defaultClaudeRunner\n\n if (isInstalledFor(projectRoot, home)) return { status: 'already-installed' }\n\n if (!run({ args: CLAUDE_VERSION_ARGV }).ok) return { status: 'claude-missing' }\n\n const marketplaceFailure = ensureMarketplace(run, home)\n\n if (marketplaceFailure !== null) return { status: 'failed', ...marketplaceFailure }\n\n logger.debug({ msg: `Running: ${CLAUDE_BIN} ${PLUGIN_INSTALL_ARGV.join(' ')}` })\n\n const installed = run({ args: PLUGIN_INSTALL_ARGV, cwd: projectRoot })\n\n if (!installed.ok) return { status: 'failed', step: 'install', error: firstLine(installed.output) }\n\n // The command exited 0, which is NOT proof. Claude Code records the install in\n // `~/.claude/plugins/installed_plugins.json`; when that record is absent the plugin is not active\n // for this project, and reporting success would hand the user a green line and a broken session.\n return isInstalledFor(projectRoot, home) ? { status: 'installed' } : { status: 'unverified' }\n}\n", "import fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\n\nimport { MARKETPLACE_NAME, PLUGIN_KEY } from './plugin-pointer'\n\n/**\n * @fileoverview\n *\n * HOST-STATE readers: what Claude Code itself records on this machine about the `infra-kit`\n * marketplace and plugin, plus the consumer repo's own MCP server registration.\n *\n * All of it is read-only and total \u2014 every reader answers \"not registered / not installed\" for an\n * absent, unreadable or malformed file rather than throwing. `initCore` prints an install hint from\n * these answers and `doctor` reports them as rows, and neither may crash because a user hand-edited\n * a JSON file under `~/.claude/`.\n *\n * These files are Claude Code's, not ours. Nothing here writes to them: installing a plugin is the\n * `claude plugin install` command's job, and a CLI that edited another tool's state directory would\n * be repairing something it does not own.\n */\n\n/** Where Claude Code keeps its plugin bookkeeping, relative to `$HOME`. */\nconst PLUGINS_DIR = ['.claude', 'plugins']\n\n/** One entry of `installed_plugins.json`'s per-plugin array. Extra fields are ignored, not rejected. */\nexport interface PluginInstallation {\n scope: string | null\n projectPath: string | null\n installPath: string | null\n version: string | null\n}\n\nconst readJsonFile = (filePath: string): unknown => {\n try {\n return JSON.parse(fs.readFileSync(filePath, 'utf-8'))\n } catch {\n return undefined\n }\n}\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> => {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nconst readString = (source: Record<string, unknown>, key: string): string | null => {\n const value = source[key]\n\n return typeof value === 'string' ? value : null\n}\n\n/** `~/.claude/plugins/<file>`, with `$HOME` injectable so tests never read the developer's own state. */\nconst pluginsPath = (file: string, home: string = os.homedir()): string => {\n return path.join(home, ...PLUGINS_DIR, file)\n}\n\n/**\n * Is the `infra-kit` marketplace registered on this machine?\n *\n * Registration is a machine fact (`~/.claude/plugins/known_marketplaces.json`), distinct from the\n * per-project `extraKnownMarketplaces` pointer `initCore` writes: the pointer says where to look, this\n * says the lookup has actually happened.\n *\n * @example\n * isMarketplaceRegistered() // => true once `claude plugin marketplace add ArthurSaenz/infra-kit` has run\n */\nexport const isMarketplaceRegistered = (home?: string): boolean => {\n const parsed = readJsonFile(pluginsPath('known_marketplaces.json', home))\n\n return isPlainObject(parsed) && MARKETPLACE_NAME in parsed\n}\n\nconst toInstallation = (entry: unknown): PluginInstallation | null => {\n if (!isPlainObject(entry)) return null\n\n return {\n scope: readString(entry, 'scope'),\n projectPath: readString(entry, 'projectPath'),\n installPath: readString(entry, 'installPath'),\n version: readString(entry, 'version'),\n }\n}\n\nconst readInstallations = (home?: string): PluginInstallation[] => {\n const parsed = readJsonFile(pluginsPath('installed_plugins.json', home))\n\n if (!isPlainObject(parsed) || !isPlainObject(parsed.plugins)) return []\n\n const entries = parsed.plugins[PLUGIN_KEY]\n\n if (!Array.isArray(entries)) return []\n\n return entries.flatMap((entry): PluginInstallation[] => {\n const installation = toInstallation(entry)\n\n return installation === null ? [] : [installation]\n })\n}\n\nexport interface PluginInstallationQuery {\n /** Override `$HOME` (tests, and nothing else). */\n home?: string\n /** The project root being asked about. Omitted, only a user-scope entry can count as installed. */\n projectPath?: string\n}\n\n/**\n * Whether the plugin is installed FOR THIS PROJECT \u2014 not merely present somewhere on the machine.\n *\n * `elsewhere` is the distinction that matters: it carries the records that exist, so the caller can\n * say which other project they belong to instead of reporting a bare \"not installed\".\n */\nexport type PluginInstallState =\n | { kind: 'installed'; installation: PluginInstallation }\n | { kind: 'elsewhere'; installations: readonly PluginInstallation[] }\n | { kind: 'absent' }\n\n/**\n * A path in the form the two sides can be compared in. `realpath` on both sides or neither: on macOS\n * `/tmp` resolves to `/private/tmp`, so a resolved path compared against an unresolved one never\n * matches. Falls back to `path.resolve` for a path that no longer exists, which `realpath` rejects.\n */\nconst canonicalPath = (target: string): string => {\n try {\n return fs.realpathSync(target)\n } catch {\n return path.resolve(target)\n }\n}\n\n/**\n * Does one record make the plugin active in `root`?\n *\n * A `user`-scope install is machine-wide and covers every project. Otherwise the record has to NAME\n * this project: `claude plugin install --scope project` stores the directory the session was started\n * in, so an install done for another repo covers that repo and nothing else.\n *\n * A record whose `projectPath` is this root counts whatever scope string it carries \u2014 the path is the\n * stronger signal, and an older Claude Code wrote records with no `scope` field at all.\n */\nconst coversProject = (entry: PluginInstallation, root: string | null): boolean => {\n if (entry.scope === 'user') return true\n if (root === null || entry.projectPath === null) return false\n\n return canonicalPath(entry.projectPath) === root\n}\n\n/**\n * Resolve what `~/.claude/plugins/installed_plugins.json` says about THIS project.\n *\n * @example\n * resolvePluginInstall({ projectPath: '/repo' })\n * // => { kind: 'elsewhere', installations: [{ projectPath: '/other', \u2026 }] } // installed, but not here\n */\nexport const resolvePluginInstall = (query: PluginInstallationQuery = {}): PluginInstallState => {\n const installations = readInstallations(query.home)\n\n if (installations.length === 0) return { kind: 'absent' }\n\n const root = query.projectPath === undefined ? null : canonicalPath(query.projectPath)\n\n const covering = installations.find((entry) => {\n return coversProject(entry, root)\n })\n\n return covering === undefined ? { kind: 'elsewhere', installations } : { kind: 'installed', installation: covering }\n}\n\n/**\n * The installed plugin's version: the record's own `version`, falling back to the manifest at\n * `<installPath>/.claude-plugin/plugin.json`.\n *\n * The fallback is not redundant. `installed_plugins.json` records a git-commit sha as `version` for\n * marketplaces served from a repo, so the manifest is the only place a human-readable `0.4.0` lives\n * \u2014 and an entry written by an older Claude Code may carry no `version` at all.\n *\n * @example\n * readInstalledPluginVersion({ scope: 'project', projectPath: null, installPath: '/cache/infra-kit', version: null })\n * // => '0.4.0' (read from /cache/infra-kit/.claude-plugin/plugin.json)\n */\nexport const readInstalledPluginVersion = (installation: PluginInstallation): string | null => {\n if (installation.installPath === null) return installation.version\n\n const manifest = readJsonFile(path.join(installation.installPath, '.claude-plugin', 'plugin.json'))\n const declared = isPlainObject(manifest) ? readString(manifest, 'version') : null\n\n return declared ?? installation.version\n}\n\n/** How a repo's `.mcp.json` registers (or fails to register) the infra-kit MCP server. */\nexport type McpRegistration =\n | { kind: 'ok' }\n | { kind: 'missing-file' }\n | { kind: 'unparseable' }\n | { kind: 'wrong-key'; key: string }\n | { kind: 'absent' }\n\n/** A server entry that IS infra-kit's, whatever key it was filed under. */\nconst looksLikeInfraKitServer = (value: unknown): boolean => {\n if (!isPlainObject(value)) return false\n\n const command = readString(value, 'command') ?? ''\n const args = Array.isArray(value.args) ? value.args.join(' ') : ''\n\n return `${command} ${args}`.includes('infra-kit')\n}\n\n/**\n * Inspect a repo's `.mcp.json` for the `infra-kit` server key.\n *\n * Why the KEY and not merely the server: Claude Code namespaces MCP tools as\n * `mcp__<server key>__<tool>`, and every skill this plugin ships names its tools with the\n * `mcp__infra-kit__` prefix. Register the same server under `ik` and every one of those references\n * silently resolves to nothing \u2014 the skills do not fail loudly, they just never find the tool. So a\n * correctly-configured server under the wrong key is a FAILURE here, and it is reported as one.\n *\n * @example\n * inspectMcpRegistration('/repo') // => { kind: 'ok' }\n * inspectMcpRegistration('/other') // => { kind: 'wrong-key', key: 'ik' }\n */\nexport const inspectMcpRegistration = (projectRoot: string): McpRegistration => {\n const mcpPath = path.join(projectRoot, '.mcp.json')\n\n if (!fs.existsSync(mcpPath)) return { kind: 'missing-file' }\n\n const parsed = readJsonFile(mcpPath)\n\n if (!isPlainObject(parsed) || !isPlainObject(parsed.mcpServers)) return { kind: 'unparseable' }\n\n const servers = parsed.mcpServers\n\n if (MARKETPLACE_NAME in servers) return { kind: 'ok' }\n\n const misfiled = Object.keys(servers).find((key) => {\n return looksLikeInfraKitServer(servers[key])\n })\n\n return misfiled === undefined ? { kind: 'absent' } : { kind: 'wrong-key', key: misfiled }\n}\n", "import fs from 'node:fs'\nimport path from 'node:path'\n\nimport { logger } from 'src/lib/logger'\n\n/**\n * @fileoverview\n *\n * The two-key POINTER `infra-kit setup` writes into a consumer repo's `.claude/settings.json`\n * so Claude Code knows where the `infra-kit` plugin marketplace lives and that the plugin is\n * enabled for that project.\n *\n * Why a pointer and not a copy: the keys name a marketplace and a repo \u2014 nothing that can go\n * stale \u2014 whereas a copied skill body rots the moment the CLI surface moves.\n *\n * MERGE SAFETY IS THE WHOLE POINT. Every consumer's `.claude/settings.json` already carries\n * `permissions.deny`, several `hooks` events, nine `enabledPlugins` and three\n * `extraKnownMarketplaces` entries \u2014 all hand-maintained. So this module ADDS each key only when\n * it is absent, never overwrites an existing value (a `false` there is a deliberate per-machine\n * opt-out), touches no other key, preserves key order and the file's own indentation, and writes\n * nothing at all when both keys are already present. A file it cannot parse is reported and left\n * byte-for-byte alone: rewriting what we could not read would destroy hand-written config.\n */\n\n/** The marketplace name, as it appears in `extraKnownMarketplaces` and in `<plugin>@<marketplace>`. */\nexport const MARKETPLACE_NAME = 'infra-kit'\n\n/** The `enabledPlugins` key: `<plugin>@<marketplace>`, both `infra-kit`. */\nexport const PLUGIN_KEY = 'infra-kit@infra-kit'\n\n/** The GitHub repo the marketplace is served from. */\nexport const MARKETPLACE_REPO = 'ArthurSaenz/infra-kit'\n\n/**\n * The one command a teammate runs by hand. `--scope project` is not decoration: it bounds where the\n * plugin's skills are active and writes `enabledPlugins` into the same `.claude/settings.json` this\n * module writes, so the two agree by construction.\n */\nexport const PLUGIN_INSTALL_COMMAND = `claude plugin install ${PLUGIN_KEY} --scope project`\n\n/**\n * The command that makes the marketplace known to this machine, and the prerequisite of the one\n * above. Rendered from the same constants the automated install passes as argv, so the line a user\n * copies and the command `setup` runs can never drift apart.\n */\nexport const MARKETPLACE_ADD_COMMAND = `claude plugin marketplace add ${MARKETPLACE_REPO}`\n\n/** Indentation used for a file we create ourselves, and the fallback when detection finds none. */\nconst DEFAULT_INDENT = ' '\n\n/** What `ensurePluginPointer` did. `unparseable` is the only outcome that wrote nothing on purpose. */\nexport type PluginPointerStatus = 'added' | 'created' | 'unchanged' | 'unparseable'\n\nexport interface PluginPointerResult {\n status: PluginPointerStatus\n /** The settings file that was inspected (written only for `added` / `created`). */\n path: string\n /** Dotted paths of the keys this run added, in insertion order. Empty for every other status. */\n added: readonly string[]\n}\n\ntype JsonObject = Record<string, unknown>\n\n/** The `extraKnownMarketplaces` value, minted fresh per call so no caller can alias our constant. */\nconst buildMarketplaceEntry = (): JsonObject => {\n return { source: { source: 'github', repo: MARKETPLACE_REPO } }\n}\n\n/** The whole file, for the absent-file path: exactly the two keys and nothing else. */\nconst buildInitialSettings = (): JsonObject => {\n return {\n extraKnownMarketplaces: { [MARKETPLACE_NAME]: buildMarketplaceEntry() },\n enabledPlugins: { [PLUGIN_KEY]: true },\n }\n}\n\nconst isPlainObject = (value: unknown): value is JsonObject => {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * The file's own indent unit, taken from the first indented member line.\n *\n * Read rather than assumed so a re-serialized file differs from the original in the two added keys\n * and nothing else \u2014 a tab-indented settings file must not come back reindented to spaces, which\n * would land as a whole-file diff in the consumer's migration commit.\n */\nconst detectIndent = (raw: string): string => {\n const match = /\\n([ \\t]+)\"/.exec(raw)\n\n return match?.[1] ?? DEFAULT_INDENT\n}\n\n/**\n * Add `key` under `container`, creating the container when missing.\n *\n * Returns the dotted path when it added something and `null` when it did not \u2014 which covers both\n * \"already there\" (the value stays whatever the user set, `false` included) and \"the container\n * exists but is not an object\", where the only safe move is to leave the file alone.\n */\nconst addMissingKey = (settings: JsonObject, container: string, key: string, value: unknown): string | null => {\n const existing = settings[container]\n\n if (existing === undefined) {\n settings[container] = { [key]: value }\n\n return `${container}.${key}`\n }\n\n if (!isPlainObject(existing) || key in existing) return null\n\n existing[key] = value\n\n return `${container}.${key}`\n}\n\n/** Both pointer keys, added in place. Returns the dotted paths actually added. */\nconst addPointerKeys = (settings: JsonObject): string[] => {\n const added = [\n addMissingKey(settings, 'extraKnownMarketplaces', MARKETPLACE_NAME, buildMarketplaceEntry()),\n addMissingKey(settings, 'enabledPlugins', PLUGIN_KEY, true),\n ]\n\n return added.filter((entry): entry is string => {\n return entry !== null\n })\n}\n\nconst readSettingsFile = (settingsPath: string): string | null => {\n try {\n return fs.readFileSync(settingsPath, 'utf-8')\n } catch {\n return null\n }\n}\n\n/** Create the file (and its `.claude/` directory) with only the two keys. */\nconst createSettingsFile = (settingsPath: string): PluginPointerResult => {\n fs.mkdirSync(path.dirname(settingsPath), { recursive: true })\n fs.writeFileSync(settingsPath, `${JSON.stringify(buildInitialSettings(), null, DEFAULT_INDENT)}\\n`, 'utf-8')\n\n return {\n status: 'created',\n path: settingsPath,\n added: [`extraKnownMarketplaces.${MARKETPLACE_NAME}`, `enabledPlugins.${PLUGIN_KEY}`],\n }\n}\n\n/**\n * Ensure the two plugin-pointer keys exist in `.claude/settings.json`. Additive, idempotent, and\n * order-preserving; never overwrites an existing value and never touches another key.\n *\n * @example\n * ensurePluginPointer('/repo/.claude/settings.json')\n * // first run: { status: 'added', added: ['extraKnownMarketplaces.infra-kit', 'enabledPlugins.infra-kit@infra-kit'] }\n * // second run: { status: 'unchanged', added: [] } \u2014 no write, mtime untouched\n */\nexport const ensurePluginPointer = (settingsPath: string): PluginPointerResult => {\n const raw = readSettingsFile(settingsPath)\n\n if (raw === null) return createSettingsFile(settingsPath)\n\n let parsed: unknown\n\n try {\n parsed = JSON.parse(raw)\n } catch {\n parsed = undefined\n }\n\n if (!isPlainObject(parsed)) {\n logger.warn(\n `Could not parse ${settingsPath} \u2014 left it untouched. Add \"${PLUGIN_KEY}\" to enabledPlugins by hand, or fix the JSON and re-run: infra-kit setup --skip-tools`,\n )\n\n return { status: 'unparseable', path: settingsPath, added: [] }\n }\n\n const added = addPointerKeys(parsed)\n\n if (added.length === 0) return { status: 'unchanged', path: settingsPath, added: [] }\n\n const indent = detectIndent(raw)\n const trailingNewline = raw.endsWith('\\n') ? '\\n' : ''\n\n fs.writeFileSync(settingsPath, `${JSON.stringify(parsed, null, indent)}${trailingNewline}`, 'utf-8')\n\n return { status: 'added', path: settingsPath, added }\n}\n", "import fs from 'node:fs'\nimport path from 'node:path'\n\nimport { logger } from 'src/lib/logger'\n\nimport { inspectMcpRegistration } from './install-state'\nimport { MARKETPLACE_NAME } from './plugin-pointer'\n\n/**\n * @fileoverview\n *\n * The WRITER half of a consumer repo's `.mcp.json` registration, beside its reader\n * (`inspectMcpRegistration`). `initCore` calls it so a repo that has the plugin enabled also has the\n * MCP server the plugin's skills call.\n *\n * MERGE SAFETY IS THE WHOLE POINT, exactly as in `plugin-pointer.ts`: a consumer's `.mcp.json` is\n * hand-maintained and holds other people's servers. So this module ADDS one key when it is absent,\n * never overwrites an existing value, touches no sibling server, preserves key order, the file's own\n * indentation and its trailing newline, and writes nothing at all when the key is already there.\n *\n * It keys off ITS OWN `JSON.parse`, not off the reader's verdict. The reader answers `unparseable`\n * for a file whose `mcpServers` is merely missing (`install-state.ts:228`), so a perfectly valid\n * `{\"$schema\": \"\u2026\"}` reads as unparseable there \u2014 refusing on that verdict would strand a repairable\n * file forever while `doctor` told the user to fix JSON that is fine. The reader is consulted for one\n * question only, the one it answers precisely: is one of the SIBLING keys already infra-kit's server?\n */\n\n/** The project-scoped MCP manifest Claude Code reads, at the repo root. */\nconst MCP_FILE_NAME = '.mcp.json'\n\n/** The container key inside that file. */\nconst SERVERS_KEY = 'mcpServers'\n\n/**\n * The command, bare on purpose: consumer repos run the global install, so this is what `PATH`\n * resolves. It equals `MARKETPLACE_NAME` by coincidence of naming, not by definition \u2014 one is a\n * binary on this machine, the other is a marketplace \u2014 so it is written out rather than aliased.\n */\nconst SERVER_COMMAND = 'infra-kit'\n\n/** Indentation for a file we create ourselves, and the fallback when detection finds none. */\nconst DEFAULT_INDENT = ' '\n\n/** What `ensureMcpRegistration` did. Only `added` and `created` wrote; the rest are all deliberate. */\nexport type McpRegistrationStatus = 'added' | 'created' | 'unchanged' | 'unparseable' | 'misfiled' | 'failed'\n\nexport interface McpRegistrationResult {\n status: McpRegistrationStatus\n /** The `.mcp.json` that was inspected (written only for `added` / `created`). */\n path: string\n /** The sibling key a misfiled infra-kit server sits under. Set for `misfiled`, `null` otherwise. */\n misfiledKey: string | null\n}\n\ntype JsonObject = Record<string, unknown>\n\n/**\n * The server entry, minted fresh per call so no caller can mutate a shared constant.\n *\n * These exact three fields are what this repo's own committed `.mcp.json` carries.\n */\nconst buildServerEntry = (): JsonObject => {\n return { type: 'stdio', command: SERVER_COMMAND, args: ['mcp'] }\n}\n\nconst isPlainObject = (value: unknown): value is JsonObject => {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\nconst outcome = (\n status: McpRegistrationStatus,\n mcpPath: string,\n misfiledKey: string | null = null,\n): McpRegistrationResult => {\n return { status, path: mcpPath, misfiledKey }\n}\n\n/**\n * The file's own indent unit, taken from the first indented member line.\n *\n * Read rather than assumed so the re-serialized file differs from the original in the added key and\n * nothing else \u2014 a tab-indented `.mcp.json` must not come back reindented to spaces, which would land\n * as a whole-file diff in the consumer's commit.\n */\nconst detectIndent = (raw: string): string => {\n const match = /\\n([ \\t]+)\"/.exec(raw)\n\n return match?.[1] ?? DEFAULT_INDENT\n}\n\n/** Absent, readable, or present-but-unreadable \u2014 three states the caller must treat differently. */\ntype McpFileRead = { kind: 'absent' } | { kind: 'raw'; raw: string } | { kind: 'unreadable'; error: unknown }\n\n/**\n * Only `ENOENT` means \"no file here yet\".\n *\n * Every other read failure means the file EXISTS and could not be seen, and collapsing that into\n * \"absent\" hands it to {@link createMcpFile}, which replaces a file whose siblings were never read \u2014\n * pre-mortem 1's data loss reached through the error path instead of the merge path. A write-only\n * `.mcp.json` (mode `0o200`) is the reproducer: the read fails, the write succeeds, and an unrelated\n * server entry is gone with no warning.\n */\nconst readMcpFile = (mcpPath: string): McpFileRead => {\n try {\n return { kind: 'raw', raw: fs.readFileSync(mcpPath, 'utf-8') }\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { kind: 'absent' }\n\n return { kind: 'unreadable', error }\n }\n}\n\nconst parseJson = (raw: string): unknown => {\n try {\n return JSON.parse(raw)\n } catch {\n return undefined\n }\n}\n\nconst describeError = (error: unknown): string => {\n return error instanceof Error ? error.message : String(error)\n}\n\n/**\n * Write, or report the failure at `warn`.\n *\n * `warn` and not `debug`: `initCore`'s caller swallows what this module throws into a `debug` line and\n * still prints its success message, so a `.mcp.json` that is a directory or is read-only would\n * otherwise fail completely invisibly. The exit code stays 0 \u2014 a failed registration is not a reason\n * to abort the rest of `initCore`.\n */\nconst writeFileOrWarn = (mcpPath: string, contents: string): boolean => {\n try {\n fs.writeFileSync(mcpPath, contents, 'utf-8')\n\n return true\n } catch (error) {\n logger.warn(\n `Could not write ${mcpPath} (${describeError(error)}) \u2014 the infra-kit MCP server is not registered. Add a \"${MARKETPLACE_NAME}\" entry to ${SERVERS_KEY} by hand, then re-run: infra-kit setup --skip-tools`,\n )\n\n return false\n }\n}\n\n/** Re-serialize a mutated document, keeping the original's indent and trailing newline. */\nconst persist = (mcpPath: string, parsed: JsonObject, raw: string): McpRegistrationResult => {\n const trailingNewline = raw.endsWith('\\n') ? '\\n' : ''\n const contents = `${JSON.stringify(parsed, null, detectIndent(raw))}${trailingNewline}`\n\n return writeFileOrWarn(mcpPath, contents) ? outcome('added', mcpPath) : outcome('failed', mcpPath)\n}\n\n/** Create the file with only our server \u2014 the `initCore` decision that it lands in the repo either way. */\nconst createMcpFile = (mcpPath: string): McpRegistrationResult => {\n const document = { [SERVERS_KEY]: { [MARKETPLACE_NAME]: buildServerEntry() } }\n const contents = `${JSON.stringify(document, null, DEFAULT_INDENT)}\\n`\n\n fs.mkdirSync(path.dirname(mcpPath), { recursive: true })\n\n return writeFileOrWarn(mcpPath, contents) ? outcome('created', mcpPath) : outcome('failed', mcpPath)\n}\n\n/**\n * Is one of the sibling keys already infra-kit's server?\n *\n * Answered by the READER rather than by a second copy of its predicate, so the writer refuses in\n * exactly the cases `doctor` reports as `wrong-key` and the two can never drift apart. This is the\n * one question the reader answers precisely: `wrong-key` requires a parsed object, an absent\n * `infra-kit` key, and a sibling whose command matches \u2014 the same three facts asked for here.\n */\nconst findMisfiledKey = (projectRoot: string): string | null => {\n const registration = inspectMcpRegistration(projectRoot)\n\n return registration.kind === 'wrong-key' ? registration.key : null\n}\n\ninterface RegisterInput {\n mcpPath: string\n parsed: JsonObject\n projectRoot: string\n raw: string\n}\n\n/**\n * Add the key into a document that parsed, refusing on the two states a human has to resolve.\n *\n * Refusing on a misfiled sibling costs the user one read; adding alongside it would run TWO infra-kit\n * servers while `doctor` certified `ok`, because `install-state.ts:232` short-circuits on key presence\n * before it ever looks at the siblings. Renaming their key for them is a semantic edit of something\n * they deliberately typed, so it stays theirs to make.\n */\nconst registerServer = ({ mcpPath, parsed, projectRoot, raw }: RegisterInput): McpRegistrationResult => {\n const servers = parsed[SERVERS_KEY]\n\n if (servers === undefined) {\n parsed[SERVERS_KEY] = { [MARKETPLACE_NAME]: buildServerEntry() }\n\n return persist(mcpPath, parsed, raw)\n }\n\n if (!isPlainObject(servers)) {\n logger.warn(\n `${mcpPath} has a \"${SERVERS_KEY}\" that is not an object \u2014 left it untouched. Fix it and re-run: infra-kit setup --skip-tools`,\n )\n\n return outcome('unparseable', mcpPath)\n }\n\n if (MARKETPLACE_NAME in servers) {\n logger.debug(`${mcpPath} already registers the \"${MARKETPLACE_NAME}\" server`)\n\n return outcome('unchanged', mcpPath)\n }\n\n const misfiled = findMisfiledKey(projectRoot)\n\n if (misfiled !== null) {\n logger.warn(\n `${mcpPath} already registers the infra-kit server under \"${misfiled}\" \u2014 left it untouched. Rename that key to \"${MARKETPLACE_NAME}\" by hand: tool names are mcp__<key>__<tool>, and a second \"${MARKETPLACE_NAME}\" entry beside \"${misfiled}\" would run two servers`,\n )\n\n return outcome('misfiled', mcpPath, misfiled)\n }\n\n servers[MARKETPLACE_NAME] = buildServerEntry()\n\n return persist(mcpPath, parsed, raw)\n}\n\n/**\n * Ensure `<projectRoot>/.mcp.json` registers the infra-kit MCP server under the `infra-kit` key.\n *\n * The key is not negotiable: Claude Code namespaces tools as `mcp__<key>__<tool>` and every plugin\n * skill names `mcp__infra-kit__*`, so a correct server under another key resolves nothing, silently.\n *\n * @example\n * ensureMcpRegistration('/repo')\n * // first run: { status: 'created', path: '/repo/.mcp.json', misfiledKey: null }\n * // second run: { status: 'unchanged', \u2026 } \u2014 no write, mtime untouched\n */\nexport const ensureMcpRegistration = (projectRoot: string): McpRegistrationResult => {\n const mcpPath = path.join(projectRoot, MCP_FILE_NAME)\n const read = readMcpFile(mcpPath)\n\n if (read.kind === 'absent') return createMcpFile(mcpPath)\n\n if (read.kind === 'unreadable') {\n logger.warn(\n `Could not read ${mcpPath} (${describeError(read.error)}) \u2014 left it untouched. Fix its permissions and re-run \\`infra-kit setup --skip-tools\\`, or add a \"${MARKETPLACE_NAME}\" entry to ${SERVERS_KEY} by hand.`,\n )\n\n return outcome('failed', mcpPath)\n }\n\n const { raw } = read\n const parsed = parseJson(raw)\n\n if (!isPlainObject(parsed)) {\n logger.warn(\n `Could not parse ${mcpPath} \u2014 left it untouched. Add a \"${MARKETPLACE_NAME}\" entry to ${SERVERS_KEY} by hand, or fix the JSON and re-run: infra-kit setup --skip-tools`,\n )\n\n return outcome('unparseable', mcpPath)\n }\n\n return registerServer({ mcpPath, parsed, projectRoot, raw })\n}\n", "import fs from 'node:fs/promises'\nimport path from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport yaml from 'yaml'\nimport { z } from 'zod'\n\nimport {\n getInfraKitConfigPaths,\n infraKitConfigSchema,\n infraKitOverrideConfigSchema,\n resetInfraKitConfigCache,\n} from 'src/lib/infra-kit-config'\nimport { logger } from 'src/lib/logger'\nimport { fileExists, tildify } from 'src/lib/path-display'\nimport { getFactoryConfigPath } from 'src/lib/vendor/factory-config'\nimport { factoryConfigSchema } from 'src/lib/vendor/factory-config-schema'\n\ninterface MigrateLayer {\n label: string\n /** Legacy YAML source path. */\n yml: string\n /** Target JSON path. */\n json: string\n /** Validates the parsed object before any write (main vs override schema). */\n schema: z.ZodType\n}\n\n/**\n * Swap a resolved `.json` config path back to its legacy `.yml` sibling.\n *\n * @example\n * legacyYmlPath('/repo/infra-kit.json') // => '/repo/infra-kit.yml'\n */\nconst legacyYmlPath = (jsonPath: string): string => {\n return jsonPath.replace(/\\.json$/, '.yml')\n}\n\n/**\n * Convert any legacy `infra-kit.yml` config layers to `infra-kit.json` as part\n * of `infra-kit setup`. Best-effort and non-fatal: each merge-chain layer\n * (project, user-global, user-project) is migrated independently, and a\n * conflict (both `.yml` and `.json` present) or an invalid `.yml` warns and\n * skips that layer rather than aborting the run or touching the other layers.\n * Idempotent \u2014 already-JSON layers are left untouched.\n *\n * @example\n * await migrateLegacyConfig()\n * // INFO: \u2713 Migrated infra-kit.yml \u2192 infra-kit.json\n * // (no output when there is nothing legacy to convert)\n */\nexport const migrateLegacyConfig = async (): Promise<void> => {\n let paths: Awaited<ReturnType<typeof getInfraKitConfigPaths>>\n\n try {\n paths = await getInfraKitConfigPaths()\n } catch {\n // No resolvable project (e.g. init run outside a repo) \u2014 nothing to migrate.\n return\n }\n\n const layers: MigrateLayer[] = [\n { label: 'infra-kit.json', yml: legacyYmlPath(paths.main), json: paths.main, schema: infraKitConfigSchema },\n {\n label: '~/.infra-kit/infra-kit.json',\n // The user-global JSON filename changed (config.json \u2192 infra-kit.json), but\n // the legacy YAML it converts from kept the old `config.yml` name. Pin it\n // here so it does not drift to `infra-kit.yml` via legacyYmlPath(); the\n // main/userProject layers keep deriving their `.yml` sibling normally\n // because their `.json` filename never changed.\n yml: path.join(path.dirname(paths.userGlobal), 'config.yml'),\n json: paths.userGlobal,\n schema: infraKitOverrideConfigSchema,\n },\n {\n label: `~/.infra-kit/projects/${paths.projectName}/infra-kit.json`,\n yml: legacyYmlPath(paths.userProject),\n json: paths.userProject,\n schema: infraKitOverrideConfigSchema,\n },\n ]\n\n let migrated = 0\n\n for (const layer of layers) {\n const [ymlExists, jsonExists] = await Promise.all([fileExists(layer.yml), fileExists(layer.json)])\n\n if (!ymlExists) continue\n\n if (jsonExists) {\n logger.info(\n `\u26A0 Skipped ${tildify(layer.yml)} \u2014 ${tildify(layer.json)} already exists (remove the stale .yml manually)`,\n )\n\n continue\n }\n\n // Keep per-layer migration non-fatal even for malformed YAML or I/O errors\n // (TOCTOU after the existence probe, EACCES, read-only FS): warn and skip\n // so one bad layer never aborts `initCore` or the other layers.\n try {\n const raw = await fs.readFile(layer.yml, 'utf-8')\n const parsed = (yaml.parse(raw) ?? {}) as unknown\n const result = layer.schema.safeParse(parsed)\n\n if (!result.success) {\n logger.info(`\u26A0 Skipped ${tildify(layer.yml)} \u2014 invalid config: ${z.prettifyError(result.error)}`)\n\n continue\n }\n\n await fs.mkdir(path.dirname(layer.json), { recursive: true })\n await fs.writeFile(layer.json, `${JSON.stringify(result.data, null, 2)}\\n`, 'utf-8')\n await fs.rm(layer.yml, { force: true })\n\n logger.info(`\u2713 Migrated ${tildify(layer.yml)} \u2192 ${tildify(layer.json)}`)\n migrated++\n } catch (err) {\n logger.info(`\u26A0 Skipped ${tildify(layer.yml)} \u2014 ${(err as Error).message}`)\n }\n }\n\n if (migrated > 0) {\n resetInfraKitConfigCache()\n }\n}\n\n/**\n * Rename the user-global config from the legacy `config.json` filename to the\n * canonical `infra-kit.json` (and its `config.example.jsonc` reference to\n * `infra-kit.example.jsonc`), so every merge-chain layer shares one filename.\n * Run by `infra-kit setup` before the user-global config is seeded, so a user's\n * existing overrides are carried forward instead of being shadowed by a fresh\n * `{}` stub written under the new name.\n *\n * Best-effort and non-fatal per file: a missing source is skipped silently\n * (idempotent \u2014 nothing to rename on a second run), and an already-present\n * target is never overwritten (the stale `config.json` is left in place so no\n * bytes are lost \u2014 the user reconciles it manually). Resets the config cache\n * when anything was renamed.\n *\n * @example\n * await migrateUserGlobalConfigFilename()\n * // \u2713 Renamed ~/.infra-kit/config.json \u2192 ~/.infra-kit/infra-kit.json\n * // (no output when there is nothing legacy to rename)\n */\nexport const migrateUserGlobalConfigFilename = async (): Promise<void> => {\n let paths: Awaited<ReturnType<typeof getInfraKitConfigPaths>>\n\n try {\n paths = await getInfraKitConfigPaths()\n } catch {\n // No resolvable project (e.g. init run outside a repo) \u2014 nothing to rename.\n return\n }\n\n const dir = path.dirname(paths.userGlobal)\n // The config.json \u2192 infra-kit.json rename carries forward a user's REAL config and\n // is load-bearing. The example-file rename is now largely redundant: seedUserGlobalConfig\n // rewrites infra-kit.example.jsonc unconditionally moments later, so a renamed legacy\n // example is overwritten anyway (and if infra-kit.example.jsonc already exists, the\n // no-overwrite guard simply leaves an orphaned config.example.jsonc behind \u2014 harmless).\n const pairs: { from: string; to: string }[] = [\n { from: path.join(dir, 'config.json'), to: paths.userGlobal },\n { from: path.join(dir, 'config.example.jsonc'), to: path.join(dir, 'infra-kit.example.jsonc') },\n ]\n\n let renamed = 0\n\n for (const { from, to } of pairs) {\n const [fromExists, toExists] = await Promise.all([fileExists(from), fileExists(to)])\n\n if (!fromExists) continue\n\n if (toExists) {\n logger.info(`\u26A0 Skipped ${tildify(from)} \u2014 ${tildify(to)} already exists (remove the stale file manually)`)\n\n continue\n }\n\n // Same-directory rename is atomic; per-file try/catch keeps a TOCTOU race or\n // I/O error (EACCES, read-only FS) non-fatal so one bad file never aborts init.\n try {\n await fs.mkdir(path.dirname(to), { recursive: true })\n await fs.rename(from, to)\n\n logger.info(`\u2713 Renamed ${tildify(from)} \u2192 ${tildify(to)}`)\n renamed++\n } catch (err) {\n logger.info(`\u26A0 Skipped ${tildify(from)} \u2014 ${(err as Error).message}`)\n }\n }\n\n if (renamed > 0) {\n resetInfraKitConfigCache()\n }\n}\n\n/**\n * Convert a legacy machine-local factory config from executable TypeScript\n * (`~/.infra-kit/vendor.config.ts`) to static JSON (`~/.infra-kit/vendor.json`) as part of\n * `infra-kit setup`: dynamic-import the old `.ts`, resolve a factory-function or object default\n * export, validate it, write `vendor.json`, remove the old `.ts`.\n *\n * Best-effort, non-fatal, idempotent (no old `.ts` \u2192 no-op), and never overwrites an existing\n * `vendor.json`. A FUNCTION export emits a DISTINCT warning rather than a plain `\u2713 Migrated`.\n *\n * @example\n * await migrateFactoryConfigToJson()\n * // \u2713 Migrated ~/.infra-kit/vendor.config.ts \u2192 ~/.infra-kit/vendor.json\n * // (no output when there is nothing legacy to convert)\n */\n// The factory config used to be a `.ts` module loaded via dynamic `import()`; it is now strict\n// JSON read with `JSON.parse`, and this one-shot migration holds the ONLY remaining dynamic\n// import of the factory file.\n//\n// The function-export warning is not cosmetic: freezing a factory's resolved output into static\n// JSON means dynamic re-evaluation (env vars, directory globbing) no longer happens, and the user\n// has to be told. No `resetInfraKitConfigCache()` \u2014 the factory loader shares no cache with the\n// `infra-kit.json` layers.\nexport const migrateFactoryConfigToJson = async (): Promise<void> => {\n const newJson = getFactoryConfigPath() // ~/.infra-kit/vendor.json\n const dir = path.dirname(newJson)\n const oldTs = path.join(dir, 'vendor.config.ts')\n\n const [oldExists, newExists] = await Promise.all([fileExists(oldTs), fileExists(newJson)])\n\n if (!oldExists) return // idempotent: nothing to migrate (incl. after a prior successful run)\n\n if (newExists) {\n logger.info(\n `\u26A0 Skipped ${tildify(oldTs)} \u2014 ${tildify(newJson)} already exists (remove the stale vendor.config.ts manually)`,\n )\n\n return\n }\n\n // The factory `.ts` is executable; read it once via dynamic import (Node native\n // type stripping) with an mtime cache-bust, resolving a factory-function or object\n // default export. Per-file try/catch keeps a malformed module or I/O error\n // non-fatal so one bad file never aborts init.\n try {\n const stat = await fs.stat(oldTs)\n const moduleUrl = `${pathToFileURL(oldTs).href}?mtime=${Number(stat.mtimeMs)}`\n const imported = (await import(moduleUrl)) as { default?: unknown }\n const raw = imported.default\n const wasFunction = typeof raw === 'function'\n const resolved = wasFunction ? await (raw as () => unknown)() : raw\n\n const result = factoryConfigSchema.safeParse(resolved)\n\n if (!result.success) {\n logger.info(`\u26A0 Skipped ${tildify(oldTs)} \u2014 invalid factory config: ${z.prettifyError(result.error)}`)\n\n return // leave the old .ts in place so the user can fix and re-run; do not delete unconverted data\n }\n\n await fs.mkdir(dir, { recursive: true })\n await fs.writeFile(newJson, `${JSON.stringify(result.data, null, 2)}\\n`, 'utf-8')\n await fs.rm(oldTs, { force: true })\n\n if (wasFunction) {\n // The old export was a function: its values were resolved ONCE and frozen into\n // static JSON. Disclose the lost dynamism instead of reporting a clean success.\n logger.info(\n `\u26A0 Migrated ${tildify(oldTs)} \u2192 ${tildify(newJson)} as a STATIC SNAPSHOT \u2014 the old file was a ` +\n `function (export default () => ({...})); computed values (env vars, directory globbing) were ` +\n `frozen at their current values and no longer re-evaluate. Edit ${tildify(newJson)} directly to change them.`,\n )\n } else {\n logger.info(`\u2713 Migrated ${tildify(oldTs)} \u2192 ${tildify(newJson)}`)\n }\n } catch (err) {\n logger.info(`\u26A0 Skipped ${tildify(oldTs)} \u2014 ${(err as Error).message}`)\n }\n}\n\n/**\n * Surgically strip the removed `ide.config.mode` field from a parsed config\n * object (single-provider or array form). The windows-removal made `mode` a dead\n * key \u2014 the loader already ignores it, so this only matters for keeping the\n * on-disk file clean. Returns `changed: false` (and the input untouched) when\n * there is no `mode` to remove, so callers can skip rewriting clean files.\n *\n * Only `mode` is removed \u2014 every other key is preserved verbatim (no full-schema\n * re-validation), so a config that is otherwise invalid or carries forward-compat\n * keys is never altered beyond the dead field.\n */\nconst stripLegacyIdeMode = (parsed: Record<string, unknown>): { changed: boolean; result: Record<string, unknown> } => {\n const ide = parsed.ide\n\n if (ide === null || typeof ide !== 'object') {\n return { changed: false, result: parsed }\n }\n\n let changed = false\n\n const stripEntry = (entry: unknown): unknown => {\n if (entry === null || typeof entry !== 'object' || !('config' in entry)) {\n return entry\n }\n\n const config = (entry as { config: unknown }).config\n\n if (config === null || typeof config !== 'object' || !('mode' in config)) {\n return entry\n }\n\n changed = true\n\n const restConfig = Object.fromEntries(\n Object.entries(config as Record<string, unknown>).filter(([key]) => {\n return key !== 'mode'\n }),\n )\n\n return { ...(entry as Record<string, unknown>), config: restConfig }\n }\n\n const nextIde = Array.isArray(ide) ? ide.map(stripEntry) : stripEntry(ide)\n\n if (!changed) {\n return { changed: false, result: parsed }\n }\n\n return { changed: true, result: { ...parsed, ide: nextIde } }\n}\n\n/**\n * Normalize any existing `infra-kit.json` config layers (project, user-global,\n * user-project) from the old IDE structure to the new one by removing the\n * removed `ide.config.mode` field. Run by `infra-kit setup` after the YAML\u2192JSON\n * migration. Best-effort and non-fatal per layer; only rewrites a file when its\n * `ide` config actually carries the dead key, so clean configs are left byte-for-\n * byte untouched (idempotent). Resets the config cache when anything changed.\n *\n * @example\n * await normalizeLegacyIdeStructures()\n * // \u2713 Normalized ide config in infra-kit.json (removed legacy \"mode\")\n * // (no output when no config carries a legacy \"mode\")\n */\nexport const normalizeLegacyIdeStructures = async (): Promise<void> => {\n let paths: Awaited<ReturnType<typeof getInfraKitConfigPaths>>\n\n try {\n paths = await getInfraKitConfigPaths()\n } catch {\n return\n }\n\n const jsonPaths = [paths.main, paths.userGlobal, paths.userProject]\n\n let normalized = 0\n\n for (const jsonPath of jsonPaths) {\n if (!(await fileExists(jsonPath))) continue\n\n try {\n const raw = await fs.readFile(jsonPath, 'utf-8')\n\n if (raw.trim() === '') continue\n\n const parsed = JSON.parse(raw) as Record<string, unknown>\n const { changed, result } = stripLegacyIdeMode(parsed)\n\n if (!changed) continue\n\n await fs.writeFile(jsonPath, `${JSON.stringify(result, null, 2)}\\n`, 'utf-8')\n\n logger.info(`\u2713 Normalized ide config in ${tildify(jsonPath)} (removed legacy \"mode\")`)\n normalized++\n } catch (err) {\n logger.info(`\u26A0 Skipped normalizing ${tildify(jsonPath)} \u2014 ${(err as Error).message}`)\n }\n }\n\n if (normalized > 0) {\n resetInfraKitConfigCache()\n }\n}\n", "import fs from 'node:fs/promises'\nimport os from 'node:os'\nimport path from 'node:path'\nimport { z } from 'zod'\n\nimport { USER_CONFIG_DIR_NAME } from 'src/lib/infra-kit-config'\n\nimport { FACTORY_CONFIG_FILE, factoryConfigSchema } from './factory-config-schema'\nimport type { FactoryConfig } from './factory-config-schema'\n\n/**\n * Absolute path to the machine-local factory config (`~/.infra-kit/vendor.json`).\n * Always returned, even when the file does not exist, so callers can surface a\n * \"where would it go?\" prompt.\n *\n * @example\n * getFactoryConfigPath() // => '/Users/arthur/.infra-kit/vendor.json'\n */\nexport const getFactoryConfigPath = (): string => {\n return path.join(os.homedir(), USER_CONFIG_DIR_NAME, FACTORY_CONFIG_FILE)\n}\n\n/**\n * Expand a leading `~` to the home dir. Absolute paths pass through unchanged.\n * A non-absolute, non-`~` value throws \u2014 cwd-relative resolution is ambiguous\n * across CLI vs long-running MCP-server invocations, so we reject it loudly.\n *\n * @example\n * expandTilde('~') // => '/Users/arthur'\n * expandTilde('~/projects') // => '/Users/arthur/projects'\n * expandTilde('/abs/dir') // => '/abs/dir'\n * expandTilde('rel/dir') // throws\n */\nexport const expandTilde = (p: string): string => {\n if (p === '~') return os.homedir()\n if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2))\n if (path.isAbsolute(p)) return p\n\n throw new Error(`workspaceDir must be absolute or ~-prefixed, got: ${p}`)\n}\n\n/**\n * Load and validate the user-global factory config at `~/.infra-kit/vendor.json`.\n * Reads the static JSON with `JSON.parse` (an empty/whitespace-only file is treated\n * as `{}`), then validates against {@link factoryConfigSchema}.\n *\n * Reads the file on every call (no module cache to bust \u2014 unlike a `.ts`\n * `import()`, `JSON.parse` always sees the current bytes), so an absent \u2192 present\n * transition works within one process: a `--init` scaffold followed by a load\n * succeeds, and a long-running MCP server picks up edits without a restart.\n * Throws an actionable error pointing at `infra-kit vendor config --init` when the\n * file is absent.\n *\n * @example\n * const { workspaceDir, targets } = await loadFactoryConfig()\n */\nexport const loadFactoryConfig = async (): Promise<FactoryConfig> => {\n const configPath = getFactoryConfigPath()\n\n let raw: string\n\n try {\n raw = await fs.readFile(configPath, 'utf-8')\n } catch {\n throw new Error(\n `Factory config not found at ${configPath}. infra-kit needs a machine-local factory ` +\n `registry to know where your project repos live and which to stamp. Run ` +\n `\\`infra-kit vendor config --init\\` to scaffold it, or create it manually as JSON:\\n\\n` +\n ` {\\n` +\n ` \"workspaceDir\": \"~/projects\",\\n` +\n ` \"targets\": [\"my-repo\"]\\n` +\n ` }\\n`,\n )\n }\n\n let data: unknown\n\n try {\n data = raw.trim() === '' ? {} : JSON.parse(raw)\n } catch (err) {\n throw new Error(`Invalid JSON in vendor.json at ${configPath}: ${(err as Error).message}`)\n }\n\n const parsed = factoryConfigSchema.safeParse(data)\n\n if (!parsed.success) {\n throw new Error(`Invalid factory config at ${configPath}: ${z.prettifyError(parsed.error)}`)\n }\n\n return parsed.data\n}\n", "import { z } from 'zod'\n\n/**\n * Pure (node-free) factory config schema. Kept separate from `factory-config.ts`\n * (which imports node builtins for the runtime loader) so the public lib entry can\n * re-export the `FactoryConfig` type without dragging node types into the emitted\n * `.d.ts` \u2014 same split as `config-schema.ts` / `config.ts`.\n */\n\n/**\n * Basename of the machine-local factory config, which lives at\n * `~/.infra-kit/vendor.json`. It is static JSON (no longer an executable `.ts`\n * module): the loader reads it with `JSON.parse`. The committed SOURCE-repo config\n * keeps the `vendor.config.ts` basename \u2014 always refer to this one as \"the\n * user-global factory config\".\n */\nexport const FACTORY_CONFIG_FILE = 'vendor.json'\n\n/**\n * The machine-local factory registry. Answers \"where do my project repos live\"\n * (`workspaceDir`) and \"which ones does the factory stamp\" (`targets`). The\n * portable \"what to vendor\" definition (`copy[]`) lives in the committed SOURCE\n * `vendor.config.ts`, never here. `.strict()` rejects a stray `copy` key so a\n * misplaced source config produces a clear error.\n */\nexport const factoryConfigSchema = z\n .object({\n /** Absolute (or `~`-prefixed) dir where the target project repos are cloned. */\n workspaceDir: z.string().min(1),\n /** Target repo directory names, resolved under `workspaceDir`. */\n targets: z.array(z.string()).min(1),\n })\n .strict()\n\nexport type FactoryConfig = z.infer<typeof factoryConfigSchema>\n", "import fs from 'node:fs/promises'\n\nexport interface OverrideSummary {\n /** True only when the layer-3 file parses AND carries at least one top-level key. */\n hasOverrides: boolean\n /** Raw top-level keys, in file order. Empty when absent, empty-object, or unreadable. */\n overrideKeys: string[]\n /** True when the file exists but could not be read or parsed as a JSON object. */\n unreadable: boolean\n}\n\n/**\n * Summarize the layer-3 user-project override file for the read-only diagnostics (`config path`,\n * `doctor`). ONE JSON-tolerance policy, shared \u2014 the two diagnostics must never disagree about what\n * a given file on disk means.\n *\n * Any read/parse failure degrades to \"no overrides\" plus `unreadable: true`. An empty or\n * whitespace-only file is treated as `{}`. A file that parses but is NOT a plain object\n * (`[1,2,3]`, `\"x\"`, `null`, `7`) is also `unreadable`.\n *\n * @example\n * await readOverrideSummary('/u/.infra-kit/projects/api/infra-kit.json', true)\n * // => { hasOverrides: true, overrideKeys: ['ide', 'dev'], unreadable: false }\n */\n// RAW keys, deliberately \u2014 `Object.keys(JSON.parse(raw))`, never the config loader's `loadLayer`.\n// That one throws on malformed JSON and on schema failure, and the schema object is strict, so a\n// single typo'd top-level key would invalidate the whole layer and turn these diagnostic commands\n// into throwers \u2014 precisely when the user reaches for them. Raw keys also surface the typo (a\n// misspelled `devServersPreset` shows up in the list) instead of swallowing it.\n//\n// The empty-file tolerance mirrors `loadLayer`'s. A non-object cannot be a merge layer, and\n// reporting an array's indices as `(3 override(s): 0, 1, 2)` would be a lie.\nexport const readOverrideSummary = async (filePath: string, exists: boolean): Promise<OverrideSummary> => {\n if (!exists) {\n return { hasOverrides: false, overrideKeys: [], unreadable: false }\n }\n\n try {\n const raw = await fs.readFile(filePath, 'utf-8')\n const parsed: unknown = raw.trim() === '' ? {} : JSON.parse(raw)\n\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n return { hasOverrides: false, overrideKeys: [], unreadable: true }\n }\n\n const keys = Object.keys(parsed)\n\n return { hasOverrides: keys.length > 0, overrideKeys: keys, unreadable: false }\n } catch {\n return { hasOverrides: false, overrideKeys: [], unreadable: true }\n }\n}\n\n/**\n * The human-readable suffix for a layer-3 file that EXISTS. Callers own the absent case: `config\n * path` already prints a `[ ]` marker for it, while `doctor` must call it out as a possible seed\n * failure (every command seeds layer 3, so an absent file is no longer \"not yet created\").\n *\n * @example\n * describeOverrides({ hasOverrides: true, overrideKeys: ['ide', 'dev'], unreadable: false })\n * // => '(2 override(s): ide, dev)'\n */\nexport const describeOverrides = (summary: OverrideSummary): string => {\n if (summary.unreadable) {\n return '(unreadable \u2014 invalid JSON)'\n }\n\n if (summary.overrideKeys.length === 0) {\n return '(empty \u2014 no overrides)'\n }\n\n return `(${summary.overrideKeys.length} override(s): ${summary.overrideKeys.join(', ')})`\n}\n", "/**\n * The production wiring of {@link ProbeDeps} \u2014 the one implementation that actually shells out.\n *\n * It lives beside the probe rather than inside `dependency-probe.ts` because that module's whole\n * discipline is \"no `spawn`, no `zx`, no `fs`\", and beside the probe rather than inside\n * `lib/dependency-plan` because `doctor` needs it too: doctor may read the registry and the probe, and\n * must never reach the install recipes (`probe-argv-single-source.test.ts`). Housing the factory in the\n * planner would have forced doctor to import the module that owns `bootstrapInstall` / `updateFor` \u2014 or\n * to keep a second copy of the `$HOME/.local/bin` search, whose drift would make doctor and `setup`\n * disagree about whether a tool is installed.\n */\nimport fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\nimport process from 'node:process'\nimport { $ } from 'zx'\n\nimport { safeRealpath } from 'src/lib/install-manager'\n\nimport type { ProbeDeps } from './dependency-probe'\n\n/**\n * Where a tool can live besides `PATH`.\n *\n * The AWS-documented installer writes `$HOME/.local/bin`, which is frequently not on `PATH` \u2014 the whole\n * reason `present` and `onPath` are separate. Searching these reports a real install as\n * installed-but-unreachable, with the `PATH` fix, instead of \"not installed\" plus an install command\n * that would run the installer a second time.\n */\nconst extraBinDirs = (): string[] => {\n return [path.join(os.homedir(), '.local', 'bin'), '/opt/homebrew/bin', '/usr/local/bin']\n}\n\n/** `command -v`, so a shell-managed PATH (asdf, mise, nvm shims) is honoured rather than guessed at. */\nconst resolveOnPath = async (binName: string): Promise<string | null> => {\n try {\n const result = await $`command -v ${binName}`\n\n return result.stdout.trim() || null\n } catch {\n return null\n }\n}\n\nexport const defaultProbeDeps = (): ProbeDeps => {\n return {\n runCommand: async (argv) => {\n const result = await $`${argv}`\n\n // stderr, not stdout, is where several of these print their version \u2014 `aws --version` is the\n // everyday case. Concatenating is what keeps `versionFrom` from silently returning null.\n return { stdout: `${result.stdout}\\n${result.stderr}` }\n },\n resolveBinPath: async (binName) => {\n const onPath = await resolveOnPath(binName)\n\n if (onPath !== null) return onPath\n\n return (\n extraBinDirs()\n .map((dir) => {\n return path.join(dir, binName)\n })\n .find((candidate) => {\n return fs.existsSync(candidate)\n }) ?? null\n )\n },\n realpath: safeRealpath,\n platform: process.platform,\n }\n}\n", "/**\n * What the five external tools are, how to recognise an existing install, and the exact argv that\n * installs or updates each one. Pure data and pure predicates: no `fs`, no `spawn`, no `process.env`\n * read, matching `lib/install-manager`'s discipline, so the whole matrix is table-testable.\n *\n * Nothing here executes anything. `lib/dependency-install` is the only module that spawns, and it\n * decides what may run from {@link Recipe.needsSudo} and {@link Recipe.fetchesNetworkScript} \u2014 the two\n * literals declared below and derived from nothing.\n */\nimport { hasSegment, isBrewKegOf, npmPrefixOfPackage } from 'src/lib/install-manager'\nimport type { InstallManager } from 'src/lib/install-manager'\n\nexport type DependencyId = 'brew' | 'aws' | 'gh' | 'doppler' | 'portless'\n\n/**\n * `install-manager`'s classification plus `script`, which it has no reason to know about: the\n * AWS-documented installer unpacks into `~/.local/share/aws-cli` and is owned by no package manager at\n * all. Without this it would classify as `unknown`, and `unknown` means \"print, never execute\" \u2014 which\n * would make `aws update`, a safe no-sudo no-network-script recipe, permanently unreachable.\n */\nexport type DependencyManager = InstallManager | 'script'\n\n/**\n * One executable step list, plus the two static risk literals.\n *\n * `needsSudo` and `fetchesNetworkScript` are properties of the RECIPE, set here and never derived from\n * a probe. That is what makes the risk predicate non-circular: both dangerous recipes fail on these two\n * conjuncts, so refusing them never depends on detection having been right. See the plan's \u00A75.2.\n */\nexport interface Recipe {\n /** Run in order. Two steps only for doppler, whose signature verification needs `gnupg` first. */\n steps: string[][]\n needsSudo: boolean\n fetchesNetworkScript: boolean\n}\n\n/**\n * FIVE separate name fields, because for these tools the identifiers diverge and one `packageName`\n * would generate the very bug the registry exists to prevent: aws's binary is `aws` but its formula is\n * `awscli`, so a conflated name emits `brew upgrade aws` \u2014 a formula that does not exist.\n */\nexport interface DependencySpec {\n id: DependencyId\n /** The executable on `PATH`. */\n binName: string\n /** The Homebrew formula, when brew can own it. Absent for `brew` itself and for `portless`. */\n brewFormula?: string\n /** What `brew install` is given \u2014 differs from the formula when the formula lives in a tap. */\n brewInstallSpec?: string\n /** The directory under `Cellar/`. Equals the formula, NOT the tap: `Cellar/doppler`, not `dopplerhq`. */\n kegName?: string\n /** The npm package name, when npm can own it. */\n npmPackage?: string\n probeArgv: string[]\n versionFrom: (stdout: string) => string | null\n platforms: NodeJS.Platform[]\n /** Ids that must be present first. `gnupg` is NOT one \u2014 it has no spec of its own (see below). */\n prerequisites: DependencyId[]\n /** How to install when the tool is ABSENT. Distinct from `recipesFor`, which answers for an existing install. */\n bootstrapInstall: Recipe\n /** Which manager owns this binary, read from its resolved path alone. */\n identify: (binRealPath: string) => DependencyManager\n /** The update recipe for an existing install, or null when this manager must not be updated by us. */\n updateFor: (manager: DependencyManager) => Recipe | null\n}\n\n/** A recipe that runs a package manager already on the box: no privilege escalation, no piped script. */\nconst managed = (...steps: string[][]): Recipe => {\n return { steps, needsSudo: false, fetchesNetworkScript: false }\n}\n\n/**\n * First dotted version in the tool's `--version` output.\n *\n * Quantifiers are BOUNDED rather than `+`: this runs over whatever a third-party binary printed, and an\n * unbounded `\\d+\\.\\d+\\.\\d+` backtracks super-linearly on a long digit run.\n */\nconst firstVersion = (stdout: string): string | null => {\n return /\\d{1,10}\\.\\d{1,10}\\.\\d{1,10}/.exec(stdout)?.[0] ?? null\n}\n\nconst UNIX: NodeJS.Platform[] = ['darwin', 'linux']\n\n/**\n * Classify by keg, reading the spec's OWN `kegName`.\n *\n * A helper rather than a literal per row: with the names restated inline, editing `kegName` changed no\n * behaviour and the test pinning it guarded nothing.\n */\nconst brewKegIdentify = (kegName: string): DependencySpec['identify'] => {\n return (binRealPath) => {\n return isBrewKegOf(binRealPath, kegName) ? 'homebrew' : 'unknown'\n }\n}\n\n/** `brew upgrade <formula>` \u2014 the FORMULA, never the binary and never the tap-qualified install spec. */\nconst brewUpgradeFor = (brewFormula: string): DependencySpec['updateFor'] => {\n return (manager) => {\n return manager === 'homebrew' ? managed(['brew', 'upgrade', brewFormula]) : null\n }\n}\n\nconst BREW: DependencySpec = {\n id: 'brew',\n binName: 'brew',\n probeArgv: ['brew', '--version'],\n versionFrom: firstVersion,\n platforms: UNIX,\n prerequisites: [],\n // The one recipe that can never run from here. It pipes a script fetched over the network into bash\n // AND needs sudo on macOS, so it fails BOTH static conjuncts \u2014 refused by computation, not by prose.\n // Homebrew's own install.sh sets NONINTERACTIVE=1 when stdin is not a TTY and then probes sudo with\n // `sudo -n`, which never prompts: off a TTY it aborts or succeeds depending on whether an unrelated\n // earlier command left a sudo timestamp cached. We never set NONINTERACTIVE ourselves.\n bootstrapInstall: {\n steps: [['/bin/bash', '-c', '$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)']],\n needsSudo: true,\n fetchesNetworkScript: true,\n },\n identify: (binRealPath) => {\n return hasSegment(binRealPath, 'Homebrew') || hasSegment(binRealPath, 'homebrew') ? 'homebrew' : 'unknown'\n },\n updateFor: (manager) => {\n return manager === 'homebrew' ? managed(['brew', 'update']) : null\n },\n}\n\n// aws is the row that proves the fields must stay separate: the BINARY is `aws`, the FORMULA `awscli`.\n// One conflated name emits `brew upgrade aws`, a formula that does not exist.\nconst AWS_NAMES = { brewFormula: 'awscli', brewInstallSpec: 'awscli', kegName: 'awscli' } as const\n\nconst AWS: DependencySpec = {\n id: 'aws',\n binName: 'aws',\n ...AWS_NAMES,\n probeArgv: ['aws', '--version'],\n versionFrom: firstVersion,\n platforms: UNIX,\n prerequisites: [],\n // The AWS-documented path for macOS and Linux alike. It needs NO sudo (user scope, `$HOME/.local`),\n // but it is still a script fetched over the network, so it fails that conjunct and is printed.\n // We never pass `--system`, which is the sudo variant.\n bootstrapInstall: {\n steps: [['/bin/bash', '-c', 'curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash']],\n needsSudo: false,\n fetchesNetworkScript: true,\n },\n identify: (binRealPath) => {\n if (isBrewKegOf(binRealPath, AWS_NAMES.kegName)) return 'homebrew'\n // `~/.local/share/aws-cli/...` \u2014 the installer's own layout, owned by no package manager.\n if (hasSegment(binRealPath, 'aws-cli')) return 'script'\n\n return 'unknown'\n },\n updateFor: (manager) => {\n if (manager === 'homebrew') return managed(['brew', 'upgrade', AWS_NAMES.brewFormula])\n // The vendor's own updater, correct ONLY for a script install. Running it against a keg is the\n // split-brain `install-manager` already refuses for infra-kit itself.\n if (manager === 'script') return managed(['aws', 'update'])\n\n return null\n },\n}\n\nconst GH_NAMES = { brewFormula: 'gh', brewInstallSpec: 'gh', kegName: 'gh' } as const\n\nconst GH: DependencySpec = {\n id: 'gh',\n binName: 'gh',\n ...GH_NAMES,\n probeArgv: ['gh', '--version'],\n versionFrom: firstVersion,\n platforms: UNIX,\n prerequisites: ['brew'],\n bootstrapInstall: managed(['brew', 'install', GH_NAMES.brewInstallSpec]),\n identify: brewKegIdentify(GH_NAMES.kegName),\n updateFor: brewUpgradeFor(GH_NAMES.brewFormula),\n}\n\n// The tap-qualified spec `brew install` is given. The KEG it lands in is `Cellar/doppler` \u2014 asking\n// `isBrewKegOf(path, 'dopplerhq')` would miss every real install, which is why these are three fields\n// and not one, and why the helpers below read them rather than restating literals.\nconst DOPPLER_NAMES = { brewFormula: 'doppler', brewInstallSpec: 'dopplerhq/cli/doppler', kegName: 'doppler' } as const\n\nconst DOPPLER: DependencySpec = {\n id: 'doppler',\n binName: 'doppler',\n ...DOPPLER_NAMES,\n probeArgv: ['doppler', '--version'],\n versionFrom: firstVersion,\n platforms: UNIX,\n // `gnupg` is a real prerequisite (binary signature verification) and is ORDERED as the first step of\n // the recipe rather than assumed. It is deliberately not a `DependencyId`: it has no probe of its own\n // here, and its binary (`gpg`) and formula (`gnupg`) diverge, so a spec row for it would be a fifth\n // set of names carried for one `brew install` line.\n prerequisites: ['brew'],\n bootstrapInstall: managed(['brew', 'install', 'gnupg'], ['brew', 'install', DOPPLER_NAMES.brewInstallSpec]),\n identify: brewKegIdentify(DOPPLER_NAMES.kegName),\n // `brew upgrade doppler`, never `doppler update`, for a keg: the vendor self-updater against a\n // brew-managed binary is the same split-brain as running npm against a Homebrew install.\n updateFor: brewUpgradeFor(DOPPLER_NAMES.brewFormula),\n}\n\nconst PORTLESS_PACKAGE = 'portless'\n\nconst PORTLESS: DependencySpec = {\n id: 'portless',\n binName: PORTLESS_PACKAGE,\n npmPackage: PORTLESS_PACKAGE,\n probeArgv: ['portless', '--version'],\n versionFrom: firstVersion,\n platforms: [...UNIX, 'win32'],\n prerequisites: [],\n // Q1, recorded rather than assumed: a GLOBAL portless does not fix the case this repo actually hits.\n // portless is a `node_modules` dependency here and is never on `PATH` under `sudo` (secure_path), which\n // is why the codebase prints `<node> <abs cli.js>` via `formatPortlessCommand`. A global install helps\n // interactive use and repos that do not depend on infra-kit; it changes nothing about the sudo case.\n bootstrapInstall: managed(['npm', 'install', '-g', PORTLESS_PACKAGE]),\n identify: (binRealPath) => {\n return npmPrefixOfPackage(binRealPath, PORTLESS_PACKAGE) === null ? 'unknown' : 'npm'\n },\n updateFor: (manager) => {\n return manager === 'npm' ? managed(['npm', 'install', '-g', `${PORTLESS_PACKAGE}@latest`]) : null\n },\n}\n\n/** Every spec, keyed by id. The single source of the probe argv `doctor` also reads. */\nexport const DEPENDENCY_SPECS: Record<DependencyId, DependencySpec> = {\n brew: BREW,\n aws: AWS,\n gh: GH,\n doppler: DOPPLER,\n portless: PORTLESS,\n}\n\nexport const DEPENDENCY_IDS = Object.keys(DEPENDENCY_SPECS) as DependencyId[]\n\nexport const specFor = (id: DependencyId): DependencySpec => {\n return DEPENDENCY_SPECS[id]\n}\n\n/** Is this tool installable on `platform` at all? Every brew path is macOS/Linuxbrew only. */\nexport const supportsPlatform = (spec: DependencySpec, platform: NodeJS.Platform): boolean => {\n return spec.platforms.includes(platform)\n}\n\n/**\n * The ids in an order where every prerequisite precedes its dependents (depth-first, stable).\n * The graph is five nodes and acyclic by construction; a cycle would recurse, so `seen` is what makes\n * a hand-edit that introduces one fail loudly here rather than hang.\n */\nexport const installOrder = (ids: readonly DependencyId[]): DependencyId[] => {\n const ordered: DependencyId[] = []\n const seen = new Set<DependencyId>()\n\n const visit = (id: DependencyId, stack: readonly DependencyId[]): void => {\n if (stack.includes(id)) throw new Error(`dependency cycle: ${[...stack, id].join(' -> ')}`)\n if (seen.has(id)) return\n\n seen.add(id)\n for (const prerequisite of specFor(id).prerequisites) visit(prerequisite, [...stack, id])\n ordered.push(id)\n }\n\n for (const id of ids) visit(id, [])\n\n return ordered\n}\n", "/**\n * Turn one {@link DependencySpec} into the state doctor/install actually need: is the tool findable at\n * all, and separately, does `<binName>` resolve on `PATH`. Those two are NOT the same question \u2014 the\n * AWS-documented installer puts `aws` in `$HOME/.local/bin`, which is routinely absent from `PATH`, so a\n * successful install can still leave `aws --version` failing on `PATH` alone. Collapsing the two into one\n * boolean would report that install as \"not installed\", which is wrong and actively misleading.\n *\n * Every process interaction is an injected seam ({@link ProbeDeps}), same discipline as\n * `src/dev/proxy/portless-driver.ts` and `src/commands/self-update/self-update.ts`: nothing here calls\n * `spawn`/`zx`/`fs` directly, so the whole matrix \u2014 including the aws script-vs-homebrew split-brain case \u2014\n * is table-testable without shelling out.\n */\nimport type { DependencyId, DependencyManager, DependencySpec } from 'src/lib/dependency-registry'\nimport { specFor, supportsPlatform } from 'src/lib/dependency-registry'\n\nexport interface DependencyState {\n id: DependencyId\n /** The tool is installed somewhere we can find \u2014 on `PATH` or not. */\n present: boolean\n /** `<binName>` resolves on `PATH`. Can be `false` while `present` is `true`; see the file header. */\n onPath: boolean\n binRealPath: string | null\n version: string | null\n manager: DependencyManager\n}\n\nexport interface ProbeRunResult {\n stdout: string\n}\n\n/**\n * Seams for {@link probeDependency}. Every field is a real process/filesystem interaction elsewhere in\n * the codebase, injected here so tests assert exact inputs without touching the real machine.\n */\nexport interface ProbeDeps {\n /** Run `argv`, resolving with its stdout. Rejects on a non-zero exit or a missing binary \u2014 mirrors `execFile`. */\n runCommand: (argv: string[]) => Promise<ProbeRunResult>\n /**\n * Find `binName` however deps see fit \u2014 `PATH` plus whatever extra locations a real implementation\n * wants to search (e.g. the aws installer's `$HOME/.local/bin`). Null when not found anywhere. This is\n * NOT restricted to `PATH`: that is what lets `present` and `onPath` disagree.\n */\n resolveBinPath: (binName: string) => Promise<string | null>\n /** Canonicalise a resolved path (follow symlinks), same contract as install-manager's `RealpathFn`. */\n realpath: (p: string) => string\n platform: NodeJS.Platform\n}\n\nconst NOT_FOUND = (id: DependencyId): DependencyState => {\n return { id, present: false, onPath: false, binRealPath: null, version: null, manager: 'unknown' }\n}\n\n/** Run `argv`, swallowing any rejection into `null` \u2014 the non-throwing half of every seam call below. */\nconst tryRun = async (runCommand: ProbeDeps['runCommand'], argv: string[]): Promise<ProbeRunResult | null> => {\n try {\n return await runCommand(argv)\n } catch {\n return null\n }\n}\n\n/**\n * `resolveBinPath` and `realpath` are caller-supplied and may be backed by real filesystem calls, so\n * neither is trusted not to throw \u2014 requirement 4 (never a throw) has to hold regardless of what a\n * production implementation does on a permissions error or a dangling symlink.\n */\nconst tryResolveRealBin = async (deps: ProbeDeps, binName: string): Promise<string | null> => {\n try {\n const resolved = await deps.resolveBinPath(binName)\n\n return resolved == null ? null : deps.realpath(resolved)\n } catch {\n return null\n }\n}\n\n/**\n * Probe one dependency. Never throws: an unresolvable binary, a non-zero exit, or a misbehaving seam all\n * fold into `present: false, manager: 'unknown'`.\n *\n * A tool unsupported on `deps.platform` is not probed at all \u2014 no seam call is made \u2014 since asking \"is gh\n * on `PATH`\" is meaningless on a platform its recipe never targets.\n */\nexport const probeDependency = async (spec: DependencySpec, deps: ProbeDeps): Promise<DependencyState> => {\n if (!supportsPlatform(spec, deps.platform)) return NOT_FOUND(spec.id)\n\n const viaPath = await tryRun(deps.runCommand, spec.probeArgv)\n const binRealPath = await tryResolveRealBin(deps, spec.binName)\n\n if (viaPath == null && binRealPath == null) return NOT_FOUND(spec.id)\n\n // Version comes from whichever invocation actually succeeded: the `PATH` run when there was one,\n // otherwise a second attempt run BY absolute path \u2014 `present && !onPath` still deserves a version.\n const versionSource =\n viaPath ?? (binRealPath == null ? null : await tryRun(deps.runCommand, [binRealPath, ...spec.probeArgv.slice(1)]))\n\n return {\n id: spec.id,\n present: true,\n onPath: viaPath != null,\n binRealPath,\n version: versionSource == null ? null : spec.versionFrom(versionSource.stdout),\n manager: binRealPath == null ? 'unknown' : spec.identify(binRealPath),\n }\n}\n\n/** Probe every id in `ids`, independently and in parallel \u2014 order preserved, one {@link probeDependency} per id. */\nexport const probeAll = async (ids: readonly DependencyId[], deps: ProbeDeps): Promise<DependencyState[]> => {\n return Promise.all(\n ids.map((id) => {\n return probeDependency(specFor(id), deps)\n }),\n )\n}\n", "import { $ } from 'zx'\n\nimport { getProjectVersions, loadJiraConfigOptional } from 'src/integrations/jira'\nimport { logger } from 'src/lib/logger'\nimport { parseBranchName } from 'src/lib/release-id'\n\nimport { collectKnownVersions } from './next-version'\nimport type { SemVer } from './next-version'\n\n/**\n * Extract version-branch tokens from raw `git ls-remote` stdout. Each line is\n * `<sha>\\t<ref>`; refs are routed through release-id's lenient\n * {@link parseBranchName} and only `kind: 'version'` ids are kept (named\n * `release/<name>` branches are irrelevant to `next`-bump math and are dropped).\n * Returns the no-`v` semver tokens (e.g. `1.2.3`) that\n * {@link collectKnownVersions} parses as versions. Pure \u2014 no I/O \u2014 so it is\n * unit-testable without the network.\n */\nexport const extractVersionBranches = (lsRemoteStdout: string): string[] => {\n return lsRemoteStdout\n .split('\\n')\n .map((line) => {\n const tab = line.indexOf('\\t')\n\n if (tab === -1) return null\n\n return parseBranchName(line.slice(tab + 1))\n })\n .filter((id): id is NonNullable<typeof id> => {\n return id !== null && id.kind === 'version'\n })\n .map((id) => {\n return id.raw\n })\n}\n\n// Quiet is scoped to the one invocation instead of toggling the global `$`. The save/restore form\n// leaked nothing, but it still made `$.quiet` globally true for the duration of a network call \u2014 and\n// this process is a long-lived MCP server, so a concurrent tool call would be silenced by a flag it\n// never set. `release-create` reaches this through both the wizard and any \"next\" token.\nconst parseRemoteRefs = async (): Promise<string[]> => {\n const result = await $({ quiet: true })`git ls-remote --heads origin 'release/v*'`\n\n return extractVersionBranches(result.stdout)\n}\n\nconst fetchJiraVersionNames = async (): Promise<string[]> => {\n const config = await loadJiraConfigOptional()\n\n if (!config) return []\n\n const versions = await getProjectVersions(config)\n\n return versions.map((v) => {\n return v.name\n })\n}\n\n/**\n * Load known release versions from the union of:\n * - remote release branches (`release/v*` on origin)\n * - Jira fix versions (when configured)\n *\n * Each source is queried in parallel; if either fails, we log a warning\n * and continue with the other so a transient outage doesn't block release\n * creation.\n */\nexport const loadExistingVersions = async (): Promise<SemVer[]> => {\n const [branchesResult, jiraResult] = await Promise.allSettled([parseRemoteRefs(), fetchJiraVersionNames()])\n\n if (branchesResult.status === 'rejected') {\n logger.warn({ err: branchesResult.reason }, 'Failed to list remote release branches; continuing without them')\n }\n\n if (jiraResult.status === 'rejected') {\n // `err`: pino renders an Error under any other key as `{}`.\n logger.warn({ err: jiraResult.reason }, 'Failed to fetch Jira versions; continuing without them')\n }\n\n return collectKnownVersions({\n remoteBranches: branchesResult.status === 'fulfilled' ? branchesResult.value : [],\n jiraVersions: jiraResult.status === 'fulfilled' ? jiraResult.value : [],\n })\n}\n", "import process from 'node:process'\n\nimport { logger } from 'src/lib/logger'\n\nimport { JiraApiError, readSeraphLoginReason } from './jira-api-error.js'\nimport type {\n CreateJiraVersionParams,\n CreateJiraVersionResult,\n DeliverJiraReleaseParams,\n DeliverJiraReleaseResult,\n JiraConfig,\n JiraVersion,\n UpdateJiraVersionParams,\n UpdateJiraVersionResult,\n} from './types.js'\n\n/**\n * Throw a classified {@link JiraApiError} when a Jira API response is not OK.\n *\n * Emits at `debug`, not `error`. Severity is not the transport's to decide: the same failed call is\n * fatal to `release create` and merely cosmetic to `worktrees list`, and only the caller that\n * catches knows which. Callers that do not catch surface it through `entry/cli.ts`, which already\n * logs at ERROR and exits 1 \u2014 logging here as well is what printed one fault as two red blocks.\n * This line survives as the only record of the raw upstream body; reach it with `--debug`.\n *\n * @param response - The fetch Response to check\n * @param context - Describes the failed operation; becomes the error's prefix\n */\nexport const assertJiraOk = async (response: Response, context: string): Promise<void> => {\n if (response.ok) {\n return\n }\n\n const body = await response.text()\n const error = new JiraApiError({\n status: response.status,\n seraphLoginReason: readSeraphLoginReason(response),\n body,\n context,\n })\n\n logger.debug({ status: response.status, kind: error.kind, body }, context)\n\n throw error\n}\n\n/**\n * The human-facing Jira URL for a fix version's release report.\n *\n * Lives here rather than beside either caller because both of its inputs originate in this\n * module: `release create` needs it for a version it just created *or* reused, and\n * `release desc-edit` needs it for one it looked up. Hoisting it out of the command layer is\n * what lets `lib/release-utils` use it \u2014 importing the command's private copy would close a\n * cycle, since that command already imports `lib/release-utils`.\n */\nexport const buildJiraVersionUrl = (config: JiraConfig, version: Pick<JiraVersion, 'id' | 'projectId'>): string => {\n return `${config.baseUrl}/projects/${version.projectId}/versions/${version.id}/tab/release-report-all-issues`\n}\n\n/**\n * Creates a new version in Jira using the REST API\n *\n * @param params - Version creation parameters\n * @param config - Jira configuration (baseUrl, token, projectId)\n * @returns Result containing created version or error\n */\nexport const createJiraVersion = async (\n params: CreateJiraVersionParams,\n config: JiraConfig,\n): Promise<CreateJiraVersionResult> => {\n const { baseUrl, token, email, projectId } = config\n\n const requestBody = {\n name: params.name,\n projectId: params.projectId || projectId,\n description: params.description || '',\n released: params.released || false,\n archived: params.archived || false,\n }\n\n const url = `${baseUrl}/rest/api/3/version`\n\n // Create Basic auth credentials\n const credentials = btoa(`${email}:${token}`)\n\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Basic ${credentials}`,\n },\n body: JSON.stringify(requestBody),\n })\n\n await assertJiraOk(response, 'Failed to create Jira version')\n\n const version = (await response.json()) as JiraVersion\n\n return {\n success: true,\n version,\n }\n}\n\n/**\n * Gets all versions for a project from Jira\n *\n * @param config - Jira configuration\n * @returns Array of JiraVersion objects\n */\nexport const getProjectVersions = async (config: JiraConfig): Promise<JiraVersion[]> => {\n const { baseUrl, token, email, projectId } = config\n\n const url = `${baseUrl}/rest/api/3/project/${projectId}/versions`\n const credentials = btoa(`${email}:${token}`)\n\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n Accept: 'application/json',\n Authorization: `Basic ${credentials}`,\n },\n })\n\n await assertJiraOk(response, 'Failed to get Jira project versions')\n\n const versions = (await response.json()) as JiraVersion[]\n\n return versions\n}\n\n/**\n * Finds a Jira version by name in the project\n *\n * @param versionName - Name of the version to find (e.g., \"v1.33.10\")\n * @param config - Jira configuration\n * @returns JiraVersion if found, null otherwise\n */\nexport const findVersionByName = async (versionName: string, config: JiraConfig): Promise<JiraVersion | null> => {\n const versions = await getProjectVersions(config)\n const version = versions.find((v) => {\n return v.name === versionName\n })\n\n return version || null\n}\n\n/**\n * Updates an existing Jira version\n *\n * @param params - Update parameters\n * @param config - Jira configuration\n * @returns Result containing updated version or error\n */\nexport const updateJiraVersion = async (\n params: UpdateJiraVersionParams,\n config: JiraConfig,\n): Promise<UpdateJiraVersionResult> => {\n const { baseUrl, token, email } = config\n\n // Only include fields the caller explicitly passed.\n const requestBody: Record<string, any> = {}\n\n if (params.released !== undefined) requestBody.released = params.released\n if (params.archived !== undefined) requestBody.archived = params.archived\n if (params.releaseDate !== undefined) requestBody.releaseDate = params.releaseDate\n if (params.description !== undefined) requestBody.description = params.description\n\n const url = `${baseUrl}/rest/api/3/version/${params.versionId}`\n const credentials = btoa(`${email}:${token}`)\n\n const response = await fetch(url, {\n method: 'PUT',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Basic ${credentials}`,\n },\n body: JSON.stringify(requestBody),\n })\n\n await assertJiraOk(response, 'Failed to update Jira version')\n\n const version = (await response.json()) as JiraVersion\n\n return {\n success: true,\n version,\n }\n}\n\n/**\n * Delivers a Jira release by marking it as released with the current date\n *\n * @param params - Parameters containing the version name\n * @param config - Jira configuration\n * @returns Result containing updated version\n * @throws Error if version not found or update fails\n */\nexport const deliverJiraRelease = async (\n params: DeliverJiraReleaseParams,\n config: JiraConfig,\n): Promise<DeliverJiraReleaseResult> => {\n const { versionName } = params\n\n // Find the version by name\n const version = await findVersionByName(versionName, config)\n\n if (!version) {\n // No log: this sits inside the `try`, so it is not a tolerance decision, and its own throw\n // already names the version. Logging here made `release deliver` print one fault twice \u2014\n // once here and again at the tolerance site in gh-release-deliver.\n throw new Error(`Version \"${versionName}\" not found in Jira project`)\n }\n\n // Update the version to mark it as released\n const result = await updateJiraVersion(\n {\n versionId: version.id,\n released: true,\n releaseDate: new Date().toISOString().split('T')[0], // Current date in YYYY-MM-DD format\n },\n config,\n )\n\n return result\n}\n\n/**\n * Loads Jira configuration from environment variables\n *\n * @throws Error with detailed message if configuration is missing or invalid\n * @returns Promise<JiraConfig>\n */\nexport const loadJiraConfig = async (): Promise<JiraConfig> => {\n const baseUrl = process.env.JIRA_BASE_URL\n const token = process.env.JIRA_TOKEN || process.env.JIRA_API_TOKEN\n const projectIdStr = process.env.JIRA_PROJECT_ID\n const email = process.env.JIRA_EMAIL\n\n const missingVars: string[] = []\n\n if (!baseUrl) missingVars.push('JIRA_BASE_URL (e.g., https://your-domain.atlassian.net)')\n if (!token) missingVars.push('JIRA_TOKEN or JIRA_API_TOKEN (your Jira API token)')\n if (!projectIdStr) missingVars.push('JIRA_PROJECT_ID (numeric project ID)')\n if (!email) missingVars.push('JIRA_EMAIL (your Jira email address)')\n\n if (missingVars.length > 0) {\n const errorMessage = [\n 'Jira configuration is required but incomplete.',\n 'Please configure the following environment variables:',\n ...missingVars.map((v) => {\n return ` - ${v}`\n }),\n '',\n 'You can set these in your .env file or as environment variables.',\n ].join('\\n')\n\n throw new Error(errorMessage)\n }\n\n const projectId = Number.parseInt(projectIdStr!, 10)\n\n if (Number.isNaN(projectId)) {\n throw new TypeError(`Invalid JIRA_PROJECT_ID: \"${projectIdStr}\" must be a numeric value (e.g., 10001)`)\n }\n\n return {\n baseUrl: baseUrl!.replace(/\\/$/, ''), // Remove trailing slash\n token: token!,\n projectId,\n email: email!,\n }\n}\n\n/**\n * Attempts to load Jira configuration from environment variables\n * Returns null if configuration is missing or invalid (for optional Jira integration)\n *\n * @returns Promise<JiraConfig | null>\n */\nexport const loadJiraConfigOptional = async (): Promise<JiraConfig | null> => {\n try {\n const config = await loadJiraConfig()\n\n return config\n } catch (error) {\n // `err` \u2014 pino renders an Error under any other key as `{}`, which is what made the old\n // second ERROR line say literally `error: {}`.\n logger.warn({ err: error }, 'Jira configuration not available, skipping Jira integration')\n\n return null\n }\n}\n", "/**\n * @fileoverview\n * Classification of a failed Jira REST response, carried ON the error rather than re-derived.\n *\n * Why this exists: a dead API token makes Jira answer a project-scoped read with `404` and the body\n * \"no project could be found with id X\" \u2014 never `401`, never `403`, because Jira refuses to leak\n * whether a project the caller cannot see exists. Reporting that status verbatim told operators\n * their project ID was wrong when the real fault was an expired token, which is the bug this module\n * closes. The discriminator was on the response all along and simply was not read.\n *\n * Measured against a live Cloud instance (see docs/jira-401-misreported-as-404-plan.md \u00A71.1):\n * `x-seraph-loginreason: AUTHENTICATED_FAILED` is present on every request that supplied\n * credentials Jira rejected, and absent on every anonymous one, on every `200` \u2014 including 200s made\n * with the dead token \u2014 and on a malformed `Authorization` header. So its PRESENCE IS CONCLUSIVE and\n * its ABSENCE PROVES NOTHING, which is why {@link classifyJiraFailure} matches the header's value and\n * never merely tests that the header is there.\n */\n\n/** What a failed Jira response actually tells us about the cause. */\nexport type JiraFailureKind = 'auth' | 'forbidden' | 'not-found-or-forbidden' | 'other'\n\nconst SERAPH_HEADER = 'x-seraph-loginreason'\nconst SERAPH_AUTHENTICATION_FAILED = 'AUTHENTICATED_FAILED'\n\n/** First line of an upstream body, capped, so it can ride inside a one-line message. */\nconst firstLine = (body: string, max = 160): string => {\n const line = body.split('\\n', 1)[0]!.trim()\n\n return line.length > max ? `${line.slice(0, max)}\u2026` : line\n}\n\n/**\n * Decide what a non-OK Jira response means.\n *\n * Header first, by value: a rejected credential yields `404 + AUTHENTICATED_FAILED` on the very call\n * this CLI makes, so a status-first classifier would file the common case as \"project missing\".\n */\nexport const classifyJiraFailure = (status: number, seraphLoginReason: string | null): JiraFailureKind => {\n if (seraphLoginReason === SERAPH_AUTHENTICATION_FAILED || status === 401) return 'auth'\n if (status === 403) return 'forbidden'\n if (status === 404) return 'not-found-or-forbidden'\n\n return 'other'\n}\n\n/** The cause, stated plainly \u2014 including the ambiguity we cannot resolve. */\nconst explain = (kind: JiraFailureKind, status: number): string => {\n if (kind === 'auth') {\n return `Jira rejected the credentials (HTTP ${status}) \u2014 check JIRA_EMAIL and JIRA_TOKEN; Atlassian API tokens expire`\n }\n\n if (kind === 'forbidden') {\n return `Jira authenticated the account but refused the operation (HTTP ${status}) \u2014 the account lacks permission on this project`\n }\n\n if (kind === 'not-found-or-forbidden') {\n return `Jira returned HTTP ${status}: either JIRA_PROJECT_ID does not exist, or these credentials cannot browse it, or the Authorization header was malformed and the request went out anonymous`\n }\n\n return `Jira returned HTTP ${status}`\n}\n\n/**\n * A failed Jira REST call, classified.\n *\n * `message` is deliberately self-sufficient \u2014 `entry/cli.ts` prints only `error.message` for an\n * uncaught error, so anything omitted here never reaches the operator.\n */\nexport class JiraApiError extends Error {\n readonly status: number\n readonly kind: JiraFailureKind\n readonly body: string\n readonly context: string\n\n constructor(args: { status: number; seraphLoginReason: string | null; body: string; context: string }) {\n const kind = classifyJiraFailure(args.status, args.seraphLoginReason)\n const detail = firstLine(args.body)\n\n super(\n detail === ''\n ? `${args.context}: ${explain(kind, args.status)}`\n : `${args.context}: ${explain(kind, args.status)}. Jira said: ${detail}`,\n )\n\n this.name = 'JiraApiError'\n this.status = args.status\n this.kind = kind\n this.body = args.body\n this.context = args.context\n }\n}\n\n/** Type guard \u2014 classification travels with the error, so callers never grep a message. */\nexport const isJiraApiError = (error: unknown): error is JiraApiError => {\n return error instanceof JiraApiError\n}\n\n/** Read the discriminating header off a response. Exported so `assertJiraOk` stays declarative. */\nexport const readSeraphLoginReason = (response: Response): string | null => {\n return response.headers.get(SERAPH_HEADER)\n}\n", "import { assertJiraOk } from './api.js'\nimport type { JiraConfig } from './types.js'\n\n/**\n * @fileoverview\n * A separate leaf module from `api.ts` on purpose. Tests mock a single module without a barrel\n * partial-mock dropping siblings it does not name \u2014 the hazard commented at\n * `src/commands/gh-release-deliver/gh-release-deliver.ts:8-12`.\n *\n * UNVERIFIED \u2014 no live Jira instance was available while writing this module. Three claims this file\n * depends on, each with the measurement that would settle it. None of the below has been observed;\n * treat this block as a record of what is assumed, not what was measured.\n *\n * U1. `POST /version/{id}/removeAndSwap` deletes the version, and an empty body means \"delete it and\n * unset the field from every issue that carried it\". Settle by: against a throwaway version with\n * one issue carrying it as `fixVersion`, POST an empty body, then re-fetch that issue and check\n * its `fixVersions` no longer contains the version. The post-state of the issue is the thing to\n * check \u2014 a *semantic* surprise here (e.g. the swap being permissive in an unexpected direction)\n * is worse than an HTTP error, because it fails silently for the operator.\n * U2. `GET /version/{id}/relatedIssueCounts` exists and returns `issuesFixedCount` and\n * `issuesAffectedCount`. Settle by: GET it against a version with a known, differing number of\n * issues in each role and compare against a manual count. FALLBACK if false:\n * `GET /search?jql=fixVersion={id}` and read `total`. That fallback is a redesign, not a\n * swap \u2014 a different endpoint, a different permission scope, and one number instead of two \u2014 so\n * with it in play `moveAffectedIssuesTo` cannot be distinguished from `moveFixIssuesTo` and the\n * caller must refuse the move rather than guess which count it is reassigning.\n * U3. Plain `DELETE /version/{id}` is deprecated in favour of `removeAndSwap`. Settle by: reading the\n * endpoint's deprecation notice, or attempting the call and checking for a deprecation header on\n * the response.\n */\n\n/** Both fields returned by `GET /version/{id}/relatedIssueCounts` (U2, unverified). */\nexport interface JiraVersionIssueCounts {\n issuesFixedCount: number\n issuesAffectedCount: number\n}\n\nexport const getVersionRelatedIssueCounts = async (\n versionId: string,\n config: JiraConfig,\n): Promise<JiraVersionIssueCounts> => {\n const { baseUrl, token, email } = config\n const url = `${baseUrl}/rest/api/3/version/${versionId}/relatedIssueCounts`\n const credentials = btoa(`${email}:${token}`)\n\n const response = await fetch(url, {\n method: 'GET',\n headers: {\n Accept: 'application/json',\n Authorization: `Basic ${credentials}`,\n },\n })\n\n await assertJiraOk(response, 'Failed to get Jira version issue counts')\n\n const counts = (await response.json()) as JiraVersionIssueCounts\n\n return {\n issuesFixedCount: counts.issuesFixedCount,\n issuesAffectedCount: counts.issuesAffectedCount,\n }\n}\n\nexport interface RemoveJiraVersionParams {\n versionId: string\n moveFixIssuesTo?: string\n moveAffectedIssuesTo?: string\n}\n\nexport const removeJiraVersion = async (params: RemoveJiraVersionParams, config: JiraConfig): Promise<void> => {\n const { baseUrl, token, email } = config\n\n // Only the fields the caller explicitly passed \u2014 an unconditional `undefined` field changes what\n // Jira does with the swap (U1).\n const requestBody: Record<string, string> = {}\n\n if (params.moveFixIssuesTo !== undefined) requestBody.moveFixIssuesTo = params.moveFixIssuesTo\n if (params.moveAffectedIssuesTo !== undefined) requestBody.moveAffectedIssuesTo = params.moveAffectedIssuesTo\n\n const url = `${baseUrl}/rest/api/3/version/${params.versionId}/removeAndSwap`\n const credentials = btoa(`${email}:${token}`)\n\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n Authorization: `Basic ${credentials}`,\n },\n body: JSON.stringify(requestBody),\n })\n\n await assertJiraOk(response, 'Failed to remove Jira version')\n}\n", "/**\n * Parse a `v<semver>` token into major, minor, patch numbers.\n *\n * SemVer-ONLY: this assumes a `v<semver>` token and will produce `NaN`s for\n * anything else. Callers that may see branch names or named releases\n * (`release/<name>`) MUST use the `release-id` module (`parseBranchName` /\n * `parseReleaseRef` + `compareReleaseIds`) instead. Only provably names-free\n * callers (the `collectKnownVersions` / `next`-bump math path) may use this.\n */\nexport const parseVersion = (versionStr: string): [number, number, number] => {\n return versionStr.slice(1).split('.').map(Number) as [number, number, number]\n}\n\n/**\n * Sort version strings in ascending order.\n * Note: Returns a new sorted array without mutating the original.\n *\n * SemVer-ONLY: relies on {@link parseVersion} and will NaN-sort named releases.\n * Name-aware callers MUST use `compareReleaseIds` from the `release-id` module.\n */\nexport const sortVersions = (versions: string[]): string[] => {\n return [...versions].sort((a, b) => {\n const [majA, minA, patchA] = parseVersion(a)\n const [majB, minB, patchB] = parseVersion(b)\n\n if (majA !== majB) return (majA ?? 0) - (majB ?? 0)\n if (minA !== minB) return (minA ?? 0) - (minB ?? 0)\n\n return (patchA ?? 0) - (patchB ?? 0)\n })\n}\n", "import { parseReleaseRef, validateName } from 'src/lib/release-id'\nimport type { ReleaseId } from 'src/lib/release-id'\nimport type { ReleaseType } from 'src/lib/release-utils'\n\nimport { parseVersion, sortVersions } from './version-utils'\n\nexport const NEXT_TOKEN = 'next'\n\nexport type SemVer = readonly [number, number, number]\n\nexport interface ExistingVersionsSources {\n remoteBranches?: string[]\n jiraVersions?: string[]\n}\n\nconst VERSION_RE = /^v?(\\d+)\\.(\\d+)\\.(\\d+)$/\n\nconst stripBranchPrefix = (raw: string): string => {\n return raw.replace(/^.*release\\//, '')\n}\n\nconst tryParse = (raw: string): SemVer | null => {\n const cleaned = stripBranchPrefix(raw.trim())\n const match = VERSION_RE.exec(cleaned)\n\n if (!match) return null\n\n return [Number(match[1]), Number(match[2]), Number(match[3])]\n}\n\nconst semverKey = (v: SemVer): string => {\n return `${v[0]}.${v[1]}.${v[2]}`\n}\n\nexport const collectKnownVersions = (sources: ExistingVersionsSources): SemVer[] => {\n const all = [...(sources.remoteBranches ?? []), ...(sources.jiraVersions ?? [])]\n const parsed: SemVer[] = []\n const seen = new Set<string>()\n\n for (const raw of all) {\n const v = tryParse(raw)\n\n if (!v) continue\n\n const key = semverKey(v)\n\n if (seen.has(key)) continue\n\n seen.add(key)\n parsed.push(v)\n }\n\n return sortVersions(\n parsed.map((v) => {\n return semverKey(v)\n }),\n ).map((s) => {\n return parseVersion(`v${s}`)\n })\n}\n\nexport class NoPriorVersionsError extends Error {\n constructor() {\n super('No prior release versions found from git or Jira. Specify the version explicitly.')\n this.name = 'NoPriorVersionsError'\n }\n}\n\n/**\n * Compute the next semantic version based on release type.\n * - regular: bump minor, reset patch to 0\n * - hotfix: bump patch on the highest minor (any patch)\n *\n * Returns the version without the leading \"v\" (e.g. \"1.64.0\").\n */\nexport const computeNextVersion = (known: SemVer[], type: ReleaseType): string => {\n if (known.length === 0) throw new NoPriorVersionsError()\n\n const max = known[known.length - 1] as SemVer\n\n if (type === 'hotfix') {\n const [major, minor] = max\n\n const highestPatchOnMinor = known.reduce((acc, v) => {\n if (v[0] === major && v[1] === minor) return Math.max(acc, v[2])\n\n return acc\n }, 0)\n\n return `${major}.${minor}.${highestPatchOnMinor + 1}`\n }\n\n const [major, minor] = max\n\n return `${major}.${minor + 1}.0`\n}\n\nconst isNextToken = (token: string): boolean => {\n return token.trim().toLowerCase() === NEXT_TOKEN\n}\n\n/**\n * A release spec is the parsed form of a versioned release request: a raw\n * version token (`\"1.2.5\"` or `\"next\"`) plus its type and optional\n * description. This is the unchanged output of {@link parseReleaseSpec} and the\n * versioned-input shape consumed by {@link resolveReleaseEntries}.\n */\nexport interface ReleaseSpec {\n version: string\n type: ReleaseType\n description?: string\n}\n\n/**\n * A named release request: a bare kebab-case name plus its type and optional\n * description. Named releases never auto-bump; `\"next\"` is version-only.\n */\nexport interface NamedReleaseInput {\n name: string\n type: ReleaseType\n description?: string\n}\n\n/** Either a versioned spec or a named release request. */\nexport type ReleaseInput = ReleaseSpec | NamedReleaseInput\n\n/**\n * A fully resolved release entry. The {@link ReleaseId} carries the concrete\n * identity (version or name) and is the single source for branch/PR/Jira\n * formatting via the `release-id` module.\n */\nexport interface ReleaseEntry {\n id: ReleaseId\n type: ReleaseType\n description?: string\n}\n\nconst isReleaseType = (value: string): value is ReleaseType => {\n return value === 'regular' || value === 'hotfix'\n}\n\nconst isNamedReleaseInput = (input: ReleaseInput): input is NamedReleaseInput => {\n return 'name' in input\n}\n\n/**\n * Parse a CLI release spec of the form `<token>[:type[:description]]` into a\n * {@link ReleaseInput}. The token determines the kind: a semver (`\"1.2.5\"`) or\n * the literal `\"next\"` yields a versioned {@link ReleaseSpec}; anything else is\n * treated as a named release ({@link NamedReleaseInput}) \u2014 the name is not\n * validated here, {@link resolveReleaseEntries} runs `validateName` later. Type\n * defaults to \"regular\". Description is everything after the second colon, so\n * colons inside descriptions are preserved.\n */\nexport const parseReleaseSpec = (raw: string): ReleaseInput => {\n const spec = raw.trim()\n\n if (spec === '') throw new Error('Release spec is empty')\n\n const firstColon = spec.indexOf(':')\n let token = spec\n let type: ReleaseType = 'regular'\n let description = ''\n\n if (firstColon !== -1) {\n token = spec.slice(0, firstColon).trim()\n const rest = spec.slice(firstColon + 1)\n const secondColon = rest.indexOf(':')\n const typeRaw = secondColon === -1 ? rest.trim() : rest.slice(0, secondColon).trim()\n\n description = secondColon === -1 ? '' : rest.slice(secondColon + 1).trim()\n const typeLower = typeRaw.toLowerCase()\n\n if (!isReleaseType(typeLower)) {\n throw new Error(`Invalid release type \"${typeRaw}\". Expected \"regular\" or \"hotfix\".`)\n }\n\n type = typeLower\n }\n\n // A semver token or the \"next\" token is a versioned release; everything else\n // is a named release. parseReleaseRef applies the same precedence downstream.\n // Note: tryParse strips a leading \"release/\" prefix, so \"release/1.2.3\" reads\n // as the version 1.2.3 (a real name can never contain a slash anyway).\n if (isNextToken(token) || tryParse(token) !== null) {\n const entry: ReleaseSpec = { version: token, type }\n\n if (description !== '') entry.description = description\n\n return entry\n }\n\n const entry: NamedReleaseInput = { name: token, type }\n\n if (description !== '') entry.description = description\n\n return entry\n}\n\n/**\n * Render a resolved {@link ReleaseEntry} as the canonical `--release` spec that\n * reproduces it \u2014 the inverse of {@link parseReleaseSpec} composed with\n * {@link resolveReleaseEntries}. The form is minimal: the bare token when the\n * release is `regular` with no description, `token:hotfix` for a hotfix with no\n * description, and `token:type:description` whenever a description is present\n * (the type segment is required to reach the description segment). The token is\n * the resolved {@link ReleaseId.raw} \u2014 a concrete semver for versions (so a\n * resolved `\"next\"` pins its computed version) or the name for named releases.\n */\nexport const formatReleaseSpec = (entry: ReleaseEntry): string => {\n const token = entry.id.raw\n\n if (entry.description !== undefined && entry.description !== '') {\n return `${token}:${entry.type}:${entry.description}`\n }\n\n if (entry.type === 'hotfix') return `${token}:hotfix`\n\n return token\n}\n\nconst withDescription = (base: { id: ReleaseId; type: ReleaseType }, description?: string): ReleaseEntry => {\n return description !== undefined && description !== '' ? { ...base, description } : base\n}\n\nconst resolveNamedInput = (input: NamedReleaseInput): ReleaseEntry => {\n const name = input.name.trim()\n\n // validateName throws InvalidReleaseNameError with a specific message.\n validateName(name)\n\n return withDescription({ id: { kind: 'name', name, raw: name }, type: input.type }, input.description)\n}\n\n/**\n * Resolve a list of release inputs into entries carrying a concrete\n * {@link ReleaseId}. Versioned inputs use the existing spec format: the\n * `\"next\"` token advances based on the running max so successive `\"next\"`\n * tokens produce sequential versions (even across mixed types), then the\n * concrete version is wrapped in a {@link ReleaseId}. Named inputs are\n * validated via `validateName` and never auto-bump.\n */\nexport const resolveReleaseEntries = (entries: ReleaseInput[], known: SemVer[]): ReleaseEntry[] => {\n const running: SemVer[] = [...known]\n\n return entries.map((entry) => {\n if (isNamedReleaseInput(entry)) {\n return resolveNamedInput(entry)\n }\n\n const trimmed = entry.version.trim()\n\n if (trimmed === '') {\n throw new Error('Release entry has an empty version')\n }\n\n if (isNextToken(trimmed)) {\n const next = computeNextVersion(running, entry.type)\n\n running.push(parseVersion(`v${next}`))\n\n return withDescription({ id: parseReleaseRef(next), type: entry.type }, entry.description)\n }\n\n const parsed = tryParse(trimmed)\n\n if (!parsed) {\n throw new Error(`Invalid version \"${trimmed}\". Expected semver like \"1.2.5\" or the token \"next\".`)\n }\n\n const explicit = `${parsed[0]}.${parsed[1]}.${parsed[2]}`\n\n running.push(parsed)\n\n return withDescription({ id: parseReleaseRef(explicit), type: entry.type }, entry.description)\n })\n}\n\nexport const hasNextToken = (entries: ReleaseInput[]): boolean => {\n return entries.some((e) => {\n return !isNamedReleaseInput(e) && isNextToken(e.version)\n })\n}\n", "import fs from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\nimport { z } from 'zod'\n\nimport {\n ENV_CLEAR_FILE,\n ENV_LOAD_FILE,\n INFRA_KIT_ENV_AUTOLOADED_VAR,\n INFRA_KIT_ENV_CLEARED_VAR,\n INFRA_KIT_ENV_CONFIG_VAR,\n INFRA_KIT_ENV_LOADED_AT_VAR,\n INFRA_KIT_ENV_PROJECT_ROOT_VAR,\n INFRA_KIT_ENV_PROJECT_VAR,\n INFRA_KIT_ENV_VAR,\n atomicWriteFileSync,\n getSessionCacheDir,\n parseVarNamesFromEnvFile,\n} from 'src/lib/constants'\nimport { getProjectRoot } from 'src/lib/git-utils'\nimport { canonicalizeProjectRoot, invalidateProjectWarmCache } from 'src/lib/warm-cache'\nimport { defineMcpTool, textContent } from 'src/types'\n\nexport interface EnvClearArgs {\n /**\n * Also invalidate this project's WARM cache. `false` (default) writes a transient\n * clear-marker so the next shell skips warm-source once; `true` deletes the warm\n * dir outright (durable \u2014 warm won't resume until the next auto-load rewrites it).\n */\n purge?: boolean\n}\n\n/**\n * Invalidate the current project's warm cache alongside the session clear, so a\n * NEW shell doesn't warm-load what the user just cleared. Resolves the project root\n * itself (git top-level, realpath'd) to match the warm key; a non-git project never\n * had a warm file, so it is a no-op. Best-effort \u2014 never blocks the session clear.\n */\nconst clearProjectWarmCache = async (purge: boolean): Promise<void> => {\n let projectRoot = ''\n\n try {\n projectRoot = await getProjectRoot()\n } catch {\n return // non-git: no warm cache to invalidate\n }\n\n const canon = canonicalizeProjectRoot(projectRoot)\n\n if (canon) invalidateProjectWarmCache(canon, purge)\n}\n\n/**\n * Build the lines for env-clear.sh: `unset` every loaded var plus the session\n * metadata (config/project/loadedAt) and the auto-load marker, then EXPORT the\n * clear sentinel so cli-invocation auto-load stays suppressed in this shell until\n * an explicit `env-load` (which unsets it) or a new shell. Pure for testability.\n */\nexport const buildEnvClearLines = (varNames: string[]): string[] => {\n return [\n ...varNames.map((v) => {\n return `unset ${v}`\n }),\n `unset ${INFRA_KIT_ENV_VAR}`,\n `unset ${INFRA_KIT_ENV_CONFIG_VAR}`,\n `unset ${INFRA_KIT_ENV_PROJECT_VAR}`,\n `unset ${INFRA_KIT_ENV_PROJECT_ROOT_VAR}`,\n `unset ${INFRA_KIT_ENV_LOADED_AT_VAR}`,\n `unset ${INFRA_KIT_ENV_AUTOLOADED_VAR}`,\n `export ${INFRA_KIT_ENV_CLEARED_VAR}='1'`,\n ]\n}\n\n/**\n * Clear loaded env vars. Prints a file path to stdout that must be sourced to apply.\n * The env-clear shell alias does this automatically. Also invalidates the project's\n * WARM cache so a new shell doesn't re-load what was just cleared. Throws when no env\n * is loaded AND `--purge` was not passed (a bare clear needs something to clear; a\n * purge is a standalone warm-cache wipe that works regardless of session state).\n */\nexport const envClear = async ({ purge = false }: EnvClearArgs = {}) => {\n const cacheDir = getSessionCacheDir()\n const envLoadPath = path.join(cacheDir, ENV_LOAD_FILE)\n const hasLoadedEnv = fs.existsSync(envLoadPath)\n\n if (!hasLoadedEnv && !purge) {\n throw new Error('No loaded environment found. Run `env-load` first.')\n }\n\n // Warm invalidation is independent of the current session's load state.\n await clearProjectWarmCache(purge)\n\n const varNames = hasLoadedEnv ? parseVarNamesFromEnvFile(envLoadPath) : []\n\n const unsetLines = buildEnvClearLines(varNames)\n\n const clearFilePath = path.resolve(cacheDir, ENV_CLEAR_FILE)\n\n fs.mkdirSync(cacheDir, { recursive: true, mode: 0o700 })\n\n atomicWriteFileSync(clearFilePath, `${unsetLines.join('\\n')}\\n`, 0o600)\n\n // REQUIRED\n process.stdout.write(`${clearFilePath}\\n`)\n\n // Remove env load file so the next env-clear call correctly reports \"no env loaded\".\n // `force` so concurrent clears don't throw ENOENT when another already removed it.\n if (hasLoadedEnv) fs.rmSync(envLoadPath, { force: true })\n\n const structuredContent = {\n filePath: clearFilePath,\n variableCount: varNames.length,\n unsetStatements: unsetLines,\n purged: purge,\n }\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n}\n\n// MCP Tool Registration\nexport const envClearMcpTool = defineMcpTool({\n name: 'env-clear',\n description:\n 'Generate a shell script that unsets every env var previously loaded by env-load for this session, plus the infra-kit session metadata vars. Does NOT mutate the calling process. When `infra-kit setup` has installed the zsh shell integration, the user\\'s terminal auto-sources the unset script on its next prompt (precmd hook) \u2014 so calling this via MCP will clear the vars in the shell that launched Claude Code automatically. Other callers must source \"<filePath>\" themselves or surface it to the user. Errors if no env is currently loaded.',\n requiresHumanConfirm: true,\n inputSchema: {\n confirm: z\n .boolean()\n .optional()\n .describe('Set true to execute; omit for a dry-run gate that echoes the resolved action.'),\n },\n outputSchema: {\n filePath: z.string().describe('Path to the file that must be sourced to apply'),\n variableCount: z.number().describe('Number of variables cleared'),\n unsetStatements: z.array(z.string()).describe('Unset statements generated'),\n purged: z.boolean().describe('Whether the project warm cache was purged outright'),\n },\n handler: () => {\n return envClear()\n },\n})\n", "import process from 'node:process'\nimport { z } from 'zod'\n\nimport { getDopplerProject } from 'src/integrations/doppler/doppler-project'\nimport { INFRA_KIT_ENV_TOKEN_VAR } from 'src/integrations/doppler/token-resolver'\nimport { readTokenStore } from 'src/lib/env-tokens'\nimport { logger } from 'src/lib/logger'\nimport type { ProjectEnv, ProjectEnvSource } from 'src/lib/project-envs'\nimport { listProjectEnvs } from 'src/lib/project-envs'\nimport { defineMcpTool, textContent } from 'src/types'\n\n/** One row of the token column: an env, where we learned it exists, and whether a token resolves. */\nexport interface EnvTokenStatus {\n env: string\n /** `gh-workflow` \u2014 declared in a workflow's dispatch options. `token-only` \u2014 we hold a token, no workflow names it. */\n source: ProjectEnvSource\n hasToken: boolean\n}\n\n/**\n * Build the token column: one row per known environment, reporting only WHETHER a token resolves \u2014\n * never the token itself. Mirrors `resolveEnvToken`'s precedence (`INFRA_KIT_ENV_TOKEN` shadows the\n * store for every env) without importing it, so this stays a pure, no-I/O function the same way\n * `buildEnvTokenRows` in `env-token-list` is.\n *\n * @example\n * buildEnvTokenStatus([{ env: 'dev', source: 'gh-workflow' }], { dev: 'dp.st.dev.xxxx' }, undefined)\n * // => [{ env: 'dev', source: 'gh-workflow', hasToken: true }]\n */\nexport const buildEnvTokenStatus = (\n envs: ProjectEnv[],\n storeTokens: Record<string, string>,\n envToken: string | undefined,\n): EnvTokenStatus[] => {\n return envs.map(({ env, source }) => {\n return { env, source, hasToken: Boolean(envToken || storeTokens[env]) }\n })\n}\n\n/** Column headers for the environments table, in render order. */\nconst ENV_TABLE_HEADERS = ['Environment', 'Source', 'Token'] as const\n\n/**\n * Render the environments as an aligned, vertical-bar-separated table \u2014 the same columnar shape as\n * `gh-release-list`, with explicit `|` column rules. Display only: the structured/MCP payload is built\n * separately in {@link buildResult} from the same rows, so the machine contract never depends on this\n * formatting. Deliberately prints no fix command \u2014 setting a token is a manual `env-token-set` step,\n * not something this read-only listing spells out.\n *\n * @example\n * formatEnvTable([{ env: 'dev', source: 'gh-workflow', hasToken: true }])\n * // => ['Environments:', '', 'Environment | Source | Token', '\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2026', 'dev | gh-workflow | set']\n */\nconst formatEnvTable = (rows: EnvTokenStatus[]): string[] => {\n const cells = rows.map((row) => {\n return [row.env, row.source, row.hasToken ? 'set' : 'not set']\n })\n\n const widths = ENV_TABLE_HEADERS.map((header, col) => {\n return Math.max(\n header.length,\n ...cells.map((cell) => {\n return (cell[col] ?? '').length\n }),\n )\n })\n\n const renderRow = (values: readonly string[]): string => {\n return values\n .map((value, col) => {\n return value.padEnd(widths[col] ?? 0)\n })\n .join(' | ')\n .trimEnd()\n }\n\n const separator = widths\n .map((width) => {\n return '\u2500'.repeat(width)\n })\n .join('\u2500\u253C\u2500')\n\n return ['Environments:', '', renderRow(ENV_TABLE_HEADERS), separator, ...cells.map(renderRow)]\n}\n\n/**\n * List the environments this project has, alongside whether a service token resolves for each one.\n *\n * Purely local: reads the repo's workflows and the local token store, and does not call Doppler \u2014 so\n * it works before any service token exists, which is exactly when a user needs to see the env names\n * they have to run `infra-kit env-token-set <env>` for. (A Doppler SERVICE token could not answer this\n * anyway: it is config-scoped and enumerates only its own config.) The token PRESENCE check is a local\n * file read, not a network probe \u2014 validating that a stored token is still accepted stays\n * `env-token-list --check`'s job.\n *\n * @example\n * await envList()\n * // logs an aligned table: 'dev | gh-workflow | set', 'stage | gh-workflow | not set'\n */\nexport const envList = async () => {\n const project = await getDopplerProject()\n const envs = await listProjectEnvs()\n const store = await readTokenStore()\n const envToken = process.env[INFRA_KIT_ENV_TOKEN_VAR]\n\n const tokens = buildEnvTokenStatus(envs, store?.envs ?? {}, envToken)\n\n logger.info(`Doppler project: ${project}\\n`)\n\n if (tokens.length === 0) {\n // Not an error \u2014 a repo can legitimately declare no dispatchable environment. But an empty list with\n // no explanation is the one thing this command must never print: it is read precisely by the person\n // who does not yet know what the env names ARE.\n logger.info('No environments found.')\n logger.info(` Declare them in a workflow's \\`workflow_dispatch\\` \\`environment.options\\`,`)\n logger.info(' or add a token for one manually.')\n\n return buildResult({ project, envs: tokens })\n }\n\n for (const line of formatEnvTable(tokens)) {\n logger.info(line)\n }\n\n return buildResult({ project, envs: tokens })\n}\n\n/** Shared tail for both exits above \u2014 the empty state is a first-class result, not an error. */\nconst buildResult = ({ project, envs }: { project: string; envs: EnvTokenStatus[] }) => {\n const structuredContent = {\n project,\n configs: envs.map((row) => {\n return row.env\n }),\n tokens: envs,\n }\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n}\n\n// MCP Tool Registration\nexport const envListMcpTool = defineMcpTool({\n name: 'env-list',\n description:\n \"List the environments this project has. The list is the union of two local sources: every environment declared in a GitHub workflow's `workflow_dispatch` `environment.options` (source `gh-workflow`), and every environment the local token store holds a token for (source `token-only`). Not a live fetch from Doppler \u2014 a Doppler service token is config-scoped and cannot enumerate sibling configs. Also returns the Doppler project name, and reports per environment whether a service token resolves locally (from the token store or INFRA_KIT_ENV_TOKEN) \u2014 presence only, never the token value, and never a live Doppler probe. Read-only.\",\n inputSchema: {},\n outputSchema: {\n project: z.string().describe('Detected Doppler project name'),\n configs: z.array(z.string()).describe('Every environment name known to this project'),\n tokens: z\n .array(\n z.object({\n env: z.string().describe('Environment / Doppler config name'),\n source: z\n .enum(['gh-workflow', 'token-only'])\n .describe('Where we learned this environment exists \u2014 a workflow declaration, or a token we hold'),\n hasToken: z.boolean().describe('Whether a service token resolves locally for this environment'),\n }),\n )\n .describe('Per-environment token presence \u2014 never the token value'),\n },\n handler: envList,\n})\n", "import path from 'node:path'\nimport process from 'node:process'\nimport { z } from 'zod'\n\nimport {\n ENV_LOAD_FILE,\n INFRA_KIT_ENV_AUTOLOADED_VAR,\n INFRA_KIT_ENV_CLEARED_VAR,\n INFRA_KIT_ENV_CONFIG_VAR,\n INFRA_KIT_ENV_LOADED_AT_VAR,\n INFRA_KIT_ENV_PROJECT_VAR,\n INFRA_KIT_SESSION_VAR,\n getSessionCacheDir,\n parseVarNamesFromEnvFile,\n} from 'src/lib/constants'\nimport { logger } from 'src/lib/logger'\nimport { defineMcpTool, textContent } from 'src/types'\n\n/**\n * Report which env is currently loaded in the terminal session. Pure local\n * introspection: reads only `process.env` + the cached env-load.sh \u2014 it makes NO\n * Doppler call (use `doctor` for auth/CLI checks), so it works offline and never\n * hangs. Surfaces whether the env was auto-loaded vs manually loaded and whether a\n * clear is currently suppressing auto-load.\n */\nexport const envStatus = async () => {\n logger.info('Environment session status:')\n\n // Check session-loaded vars \u2014 getSessionCacheDir() throws if INFRA_KIT_SESSION is unset\n const cacheDir = getSessionCacheDir()\n\n const sessionId = process.env[INFRA_KIT_SESSION_VAR]!\n const envLoadPath = path.join(cacheDir, ENV_LOAD_FILE)\n\n let sessionLoadedCount = 0\n let sessionTotalCount = 0\n\n const sessionConfig = process.env[INFRA_KIT_ENV_CONFIG_VAR] ?? null\n const sessionProject = process.env[INFRA_KIT_ENV_PROJECT_VAR] ?? null\n const sessionLoadedAt = process.env[INFRA_KIT_ENV_LOADED_AT_VAR] ?? null\n const autoLoaded = process.env[INFRA_KIT_ENV_AUTOLOADED_VAR] === '1'\n const cleared = Boolean(process.env[INFRA_KIT_ENV_CLEARED_VAR])\n\n if (sessionConfig) {\n const varNames = parseVarNamesFromEnvFile(envLoadPath)\n\n if (varNames.length > 0) {\n sessionTotalCount = varNames.length\n sessionLoadedCount = varNames.filter((v) => {\n return v in process.env\n }).length\n }\n\n const loadedAtDisplay = sessionLoadedAt?.replace(/\\.\\d{3}Z$/, '') ?? null\n const origin = autoLoaded ? 'auto-loaded' : 'manually loaded'\n\n logger.info(\n ` ${sessionConfig}: ${sessionLoadedCount} of ${sessionTotalCount} vars loaded (${origin}, project: ${sessionProject}, loadedAt: ${loadedAtDisplay}, session: ${sessionId})\\n`,\n )\n\n if (sessionTotalCount > 0 && sessionLoadedCount < sessionTotalCount) {\n const missing = sessionTotalCount - sessionLoadedCount\n\n logger.warn(\n ` ${missing} cached var(s) are not present in the current process \u2014 env-load needs to be re-sourced, or vars were unset manually.`,\n )\n }\n } else {\n const clearedNote = cleared ? ' (cleared \u2014 auto-load suppressed until a new shell or explicit env-load)' : ''\n\n logger.info(` Session ${sessionId}: no env loaded${clearedNote}\\n`)\n }\n\n const structuredContent = {\n sessionId,\n sessionLoadedCount,\n sessionTotalCount,\n sessionConfig,\n sessionProject,\n sessionLoadedAt,\n autoLoaded,\n cleared,\n }\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n}\n\n// MCP Tool Registration\nexport const envStatusMcpTool = defineMcpTool({\n name: 'env-status',\n description:\n 'Report which Doppler project/config is currently loaded in the terminal session, when it was loaded, how many variables are cached, whether it was auto-loaded, and whether a clear is suppressing auto-load. Pure local introspection \u2014 makes NO Doppler call (use doctor for auth). Read-only \u2014 use env-load / env-clear to change the terminal session.',\n inputSchema: {},\n outputSchema: {\n sessionId: z.string().describe('Current terminal session ID'),\n sessionLoadedCount: z.number().describe('Number of cached vars active in the current session'),\n sessionTotalCount: z.number().describe('Total number of cached var names'),\n sessionConfig: z.string().nullable().describe('Doppler config name of the loaded session (environment name)'),\n sessionProject: z.string().nullable().describe('Doppler project name of the loaded session'),\n sessionLoadedAt: z.string().nullable().describe('ISO 8601 timestamp of when the env was loaded'),\n autoLoaded: z.boolean().describe('True when the loaded env was applied by env auto-load (not a manual env-load)'),\n cleared: z.boolean().describe('True when env-clear is currently suppressing auto-load in this shell'),\n },\n handler: envStatus,\n})\n", "import process from 'node:process'\nimport { z } from 'zod'\n\nimport { buildDopplerChildEnv } from 'src/commands/env-load'\nimport { INFRA_KIT_ENV_TOKEN_VAR, getDopplerProject, probeEnvToken } from 'src/integrations/doppler'\nimport type { EnvTokenProbeOutcome } from 'src/integrations/doppler'\nimport { readTokenStore } from 'src/lib/env-tokens'\nimport { logger } from 'src/lib/logger'\nimport { listProjectEnvNames } from 'src/lib/project-envs'\nimport { redactToken } from 'src/lib/redact'\nimport { defineMcpTool, textContent } from 'src/types'\n\nexport interface EnvTokenListArgs {\n /** Additionally ask Doppler whether each token is alive AND scoped where it claims. One call per env. */\n check?: boolean\n}\n\n/** Where an env's token came from, rendered for the table. `-` when there is none. */\ntype TokenSource = 'store' | `env:${typeof INFRA_KIT_ENV_TOKEN_VAR}` | '-'\n\ninterface EnvTokenRow {\n env: string\n source: TokenSource\n /** Redacted \u2014 at most the last 4 chars. The raw token NEVER leaves this module. */\n redactedToken: string\n present: boolean\n /** Only with `--check`. `unreachable` means COULDN'T TELL, never \"bad\". */\n status?: EnvTokenProbeOutcome\n}\n\n/**\n * Build one row per declared environment. Pure apart from the two reads it is handed, so the\n * precedence rule \u2014 `INFRA_KIT_ENV_TOKEN` shadows the store for EVERY env, exactly as `resolveEnvToken`\n * resolves it \u2014 is testable and provably the same rule the loader uses.\n *\n * The env var is reported against every env because that is what it does: it is a single value with no\n * env dimension, so a developer with it exported is using it everywhere, whatever their store says.\n *\n * @example\n * buildEnvTokenRows(['dev'], { dev: 'dp.st.dev.abcd1234' }, undefined)\n * // => [{ env: 'dev', source: 'store', redactedToken: '****1234', present: true }]\n */\nexport const buildEnvTokenRows = (\n environments: string[],\n storeTokens: Record<string, string>,\n envToken: string | undefined,\n): EnvTokenRow[] => {\n return environments.map((env) => {\n const token = envToken || storeTokens[env]\n\n if (!token) return { env, source: '-' as const, redactedToken: '-', present: false }\n\n return {\n env,\n source: envToken ? (`env:${INFRA_KIT_ENV_TOKEN_VAR}` as const) : ('store' as const),\n redactedToken: redactToken(token),\n present: true,\n }\n })\n}\n\n/** The rendered table's columns, in order. `CHECK` only appears when `--check` was asked for. */\nconst BASE_COLUMNS = ['ENV', 'SOURCE', 'TOKEN', 'PRESENT'] as const\n\n/**\n * Render the rows as an aligned table. Values are ALREADY redacted by {@link buildEnvTokenRows} \u2014\n * this function never sees a raw token, which is the point of doing the redaction upstream.\n *\n * @example\n * formatEnvTokenTable([{ env: 'dev', source: 'store', redactedToken: '****1234', present: true }], false)\n * // => ['ENV SOURCE TOKEN PRESENT', 'dev store ****1234 yes']\n */\nexport const formatEnvTokenTable = (rows: EnvTokenRow[], checked: boolean): string[] => {\n const header = [...BASE_COLUMNS, ...(checked ? ['CHECK'] : [])]\n\n const cells = rows.map((row) => {\n return [row.env, row.source, row.redactedToken, row.present ? 'yes' : 'no', ...(checked ? [row.status ?? '-'] : [])]\n })\n\n const widths = header.map((column, index) => {\n return Math.max(\n column.length,\n ...cells.map((cell) => {\n return (cell[index] ?? '').length\n }),\n )\n })\n\n return [header, ...cells].map((row) => {\n return row\n .map((cell, index) => {\n return cell.padEnd(widths[index]!)\n })\n .join(' ')\n .trimEnd()\n })\n}\n\n/**\n * Show which environments have a service token, where it came from, and (with `--check`) whether\n * Doppler still accepts it for that config.\n *\n * The ONLY env-token command exposed over MCP, and it can only ever emit redacted values \u2014 the raw\n * token is read here and never rendered, logged, or returned.\n */\nexport const envTokenList = async ({ check }: EnvTokenListArgs = {}) => {\n // Every env the project has, not just the ones with a token \u2014 a missing token is the row worth\n // printing. See `lib/project-envs` for why this is a union of the workflows and the store.\n const envs = await listProjectEnvNames()\n const store = await readTokenStore()\n const envToken = process.env[INFRA_KIT_ENV_TOKEN_VAR]\n\n const rows = buildEnvTokenRows(envs, store?.envs ?? {}, envToken)\n\n if (check) {\n const project = await getDopplerProject()\n\n for (const row of rows) {\n if (!row.present) continue\n\n // The RAW token is re-read here (never from the row, which only carries the redaction) and goes\n // straight into the child env \u2014 by ENV, never argv.\n const token = envToken || store!.envs[row.env]!\n\n const probe = await probeEnvToken({\n childEnv: buildDopplerChildEnv(token),\n project,\n config: row.env,\n })\n\n row.status = probe.outcome\n }\n }\n\n for (const line of formatEnvTokenTable(rows, Boolean(check))) {\n logger.info(line)\n }\n\n const missing = rows.filter((row) => {\n return !row.present\n })\n\n if (missing.length > 0) {\n logger.info(\n `\\nNo token for: ${missing\n .map((row) => {\n return row.env\n })\n .join(', ')}. Add one with \\`infra-kit env-token-set <env>\\`.`,\n )\n }\n\n const structuredContent = { tokens: rows }\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n}\n\n// MCP Tool Registration\n//\n// The ONLY env-token command that is an MCP tool, and the reason is the same one that keeps\n// `doctor --fix` CLI-only: `lib/tool-handler` injects `confirmedCommand: true` into EVERY tool call,\n// so the MCP boundary auto-confirms by construction. An agent must never be one call away from\n// writing a credential (`env-token-set`) or destroying one (`env-token-remove`). This command only\n// READS, and only ever emits redacted values. Do not \"fix\" this by exposing the other two.\nexport const envTokenListMcpTool = defineMcpTool({\n name: 'env-token-list',\n description:\n 'List the Doppler service tokens configured for this repo: one row per environment declared in infra-kit.json, with its source (the local token store, or the INFRA_KIT_ENV_TOKEN env var) and a REDACTED token (last 4 chars at most \u2014 the raw value is never returned). With check=true, also asks Doppler whether each token is valid and correctly scoped (valid | revoked | mis-scoped | unreachable; \"unreachable\" means the probe could not tell, NOT that the token is bad). Read-only.',\n inputSchema: {\n check: z\n .boolean()\n .optional()\n .describe('Probe Doppler for each token\u2019s validity and scope (one cheap call per environment).'),\n },\n outputSchema: {\n tokens: z\n .array(\n z.object({\n env: z.string().describe('Environment / Doppler config name'),\n source: z.string().describe('\"store\", \"env:INFRA_KIT_ENV_TOKEN\", or \"-\" when absent'),\n redactedToken: z.string().describe('Redacted token \u2014 last 4 chars at most, never the raw value'),\n present: z.boolean().describe('Whether a token is configured for this environment'),\n status: z.string().optional().describe('Only with check=true: valid | revoked | mis-scoped | unreachable'),\n }),\n )\n .describe('One row per declared environment'),\n },\n handler: envTokenList,\n})\n", "/**\n * The single redaction primitive for the CLI. Before this module there was none: every place that\n * wanted to show \"which token is this\" hand-rolled a slice, and a hand-rolled slice is exactly how a\n * credential ends up on a terminal (or in `/tmp/mcp-infra-kit.log`) in full.\n *\n * The rule it encodes: a rendered token shows AT MOST its last 4 characters, and never enough to\n * reconstruct it. Everything user-facing that names a token \u2014 `env-token-set`, `env-token-list`,\n * `doctor` \u2014 renders it through here, so the policy has ONE definition and one place to tighten.\n */\n\n/** How many trailing characters a redacted token may reveal. Never raise this. */\nconst VISIBLE_SUFFIX_LENGTH = 4\n\n/**\n * Below this length a token reveals NOTHING: showing the last 4 of a 6-char string hands over most of\n * it. Real Doppler service tokens are ~40+ chars, so this only ever fires for garbage input \u2014 which\n * is precisely when we must not be helpful.\n */\nconst MIN_LENGTH_FOR_SUFFIX = 8\n\n/** The fixed-width mask. Fixed on purpose: a mask whose length tracked the token would leak its length. */\nconst MASK = '****'\n\n/**\n * Render a token for human eyes: the mask, plus at most the last 4 characters.\n *\n * Total function \u2014 an empty, short, or malformed value returns the bare mask rather than throwing.\n * A redaction helper that can throw is a redaction helper that gets wrapped in a try/catch whose\n * fallback prints the raw value.\n *\n * @example\n * redactToken('dp.st.dev.abcdefgh1234') // => '****1234'\n * redactToken('short') // => '****' (too short to reveal anything)\n * redactToken('') // => '****'\n */\nexport const redactToken = (token: string): string => {\n if (typeof token !== 'string' || token.length < MIN_LENGTH_FOR_SUFFIX) return MASK\n\n return `${MASK}${token.slice(-VISIBLE_SUFFIX_LENGTH)}`\n}\n", "import { z } from 'zod'\nimport { $ } from 'zx'\n\nimport { getReleasePRsWithInfo } from 'src/integrations/gh'\nimport { commandEcho, confirmOrExit } from 'src/lib/command-echo'\nimport { isCommandDeclined } from 'src/lib/errors/command-declined-error'\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { assertRepoWithOrigin } from 'src/lib/git-guard'\nimport { getMainRepoRoot, listWorktrees, lsRemoteHead, pushAtomic, withScratchWorktree } from 'src/lib/git-utils'\nimport type { WorktreeEntry } from 'src/lib/git-utils'\nimport { logger } from 'src/lib/logger'\nimport { pickReleaseBranches } from 'src/lib/prompts/release-picker'\nimport { formatBranchName, formatJiraName, parseBranchName, parseReleaseRef } from 'src/lib/release-id'\nimport { detectReleaseType, formatBranchPickerItems } from 'src/lib/release-utils'\nimport { defineMcpTool, textContent } from 'src/types'\nimport type { RequiredConfirmedOptionArg } from 'src/types'\n\nimport { DEFAULT_VERIFY_COMMAND, planMergeRun, pushableRefs, reclassify, verifyMerges } from './merge-run'\nimport type { MergePlanEntry, MergeStatus } from './merge-run'\nimport { registerRunCleanup } from './run-cleanup'\n\ninterface GhMergeDevArgs extends RequiredConfirmedOptionArg {\n all?: boolean\n /**\n * Comma string (CLI) or array (MCP) of release selectors. Each entry may be a\n * version label (`1.2.5`) or a raw branch name (`release/v1.2.5`).\n */\n versions?: string | string[]\n /** Compute and print the plan, push nothing. */\n dryRun?: boolean\n /**\n * Verify each merge before pushing it. `true` runs the default install tier;\n * a string runs that command verbatim in the scratch worktree.\n */\n verify?: boolean | string\n}\n\ntype ResultStatus = MergeStatus | 'push-aborted' | 'verify-failed'\n\ninterface MergeDevResultEntry {\n branch: string\n status: ResultStatus\n mergeSha?: string\n conflictPaths?: string[]\n pushed: boolean\n reason?: string\n}\n\ninterface MergeDevResult {\n successfulMerges: number\n failedMerges: number\n failedBranches: string[]\n totalBranches: number\n dryRun: boolean\n atomicPush: { attempted: boolean; aborted: boolean; abortedBy?: string }\n results: MergeDevResultEntry[]\n}\n\nconst emptyResult = (dryRun: boolean): MergeDevResult => {\n return {\n successfulMerges: 0,\n failedMerges: 0,\n failedBranches: [],\n totalBranches: 0,\n dryRun,\n atomicPush: { attempted: false, aborted: false },\n results: [],\n }\n}\n\nconst toResponse = (structuredContent: MergeDevResult) => {\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n}\n\n/** Statuses that represent the branch reaching its desired end state. */\nconst SUCCESS_STATUSES = new Set<ResultStatus>(['up-to-date', 'fast-forward', 'merged'])\n\n/**\n * Resolve `--versions` selectors against the open **regular** release branches.\n *\n * Selects from `available` \u2014 the already-filtered list \u2014 rather than parsing tokens in\n * isolation, and never round-trips through `releaseBranchLabels`.\n */\n// Both properties above have already been shipped as bugs elsewhere in this CLI:\n//\n// 1. `worktrees-add` skips the PR lookup entirely when `versions` is given \u2014 correct there,\n// because it *wants* hotfix worktrees. Copying that shape here would bypass the\n// `detectReleaseType(pr.title) === 'regular'` filter, and since hotfix PRs carry ordinary\n// `release/v\u2026` head refs (only their *title* marks them), `--versions 1.2.5` would happily\n// merge `dev` into a branch that targets `main`.\n// 2. `releaseBranchLabels`'s `flatMap` drops any branch that fails `parseBranchName`, so routing\n// selectors through it is a silent selector loss.\nconst resolveRequestedBranches = (versions: string | string[], available: string[]): string[] => {\n const tokens = (Array.isArray(versions) ? versions : versions.split(','))\n .map((token) => {\n return token.trim()\n })\n .filter((token) => {\n return token.length > 0\n })\n\n const availableSet = new Set(available)\n const selected = new Set<string>()\n\n for (const token of tokens) {\n let branch: string\n\n try {\n branch = formatBranchName(parseReleaseRef(token))\n } catch (error) {\n throw new OperationError(error, {\n operation: `resolve release selector \"${token}\"`,\n remediation: `pass one of: ${available.join(', ')}`,\n })\n }\n\n if (!availableSet.has(branch)) {\n throw new OperationError(undefined, {\n operation: `select release branch for \"${token}\"`,\n stderrExcerpt: `${branch} is not an open regular release branch`,\n remediation: `pass one of: ${available.join(', ')}`,\n })\n }\n\n selected.add(branch)\n }\n\n // Preserve the caller-independent order the PR listing already locked in, so\n // two operators naming the same set get the same plan.\n return available.filter((branch) => {\n return selected.has(branch)\n })\n}\n\nconst PLAN_LABELS: Record<MergeStatus, string> = {\n 'up-to-date': 'already up to date',\n 'fast-forward': 'fast-forward',\n merged: 'clean merge',\n conflict: 'CONFLICT',\n 'hook-failed': 'HOOK FAILED',\n error: 'ERROR',\n 'not-attempted': 'not attempted',\n}\n\nconst describeEntry = (entry: MergePlanEntry): string => {\n const label = PLAN_LABELS[entry.status]\n\n if (entry.status === 'conflict') {\n const count = entry.conflictPaths?.length ?? 0\n\n return `${label}: ${count} file(s)`\n }\n\n return entry.reason ? `${label}: ${entry.reason}` : label\n}\n\n/**\n * Print the plan before the operator is asked to confirm it.\n *\n * This is the whole point of computing the merges first: the thing shown here is\n * literally the thing that will be pushed, because the plan and the apply are the\n * same operation rather than a prediction and an application that can disagree.\n */\nconst renderPlan = (entries: MergePlanEntry[]): void => {\n logger.info('\\n\uD83D\uDCCB Merge plan:')\n\n for (const entry of entries) {\n logger.info(` ${entry.branch} \u2014 ${describeEntry(entry)}`)\n }\n}\n\n/**\n * The exact command the operator can run to finish a branch by hand.\n *\n * It has to be worktree-aware, because the blanket `git switch <b> && \u2026` form is\n * wrong advice in precisely the common case: on this team `worktrees-add` keeps a\n * worktree per release branch, so `git switch` fails with `already used by\n * worktree at \u2026` \u2014 the very failure this whole change removed from the command.\n * Printing it back to the operator would hand them the broken workflow we just\n * fixed.\n */\nconst reproduceCommand = (branch: string, worktrees: WorktreeEntry[]): string => {\n const held = worktrees.find((entry) => {\n return entry.branch === branch\n })\n\n return held ? `git -C ${held.path} merge origin/dev` : `git switch ${branch} && git merge origin/dev`\n}\n\ninterface RunOutcome {\n entries: MergePlanEntry[]\n selected: string[]\n pushed: boolean\n pushAborted: boolean\n abortedBy?: string\n /** Branches origin turned out to already carry despite the push reporting failure. */\n observedLanded?: string[]\n /** branch \u2192 why verification refused it, when --verify ran. */\n verifyFailed?: Map<string, string>\n declined: boolean\n}\n\n/** Interactive selection, rendered with each branch's plan status inline. */\nconst selectBranches = async (args: {\n entries: MergePlanEntry[]\n toPlan: string[]\n all?: boolean\n explicit: boolean\n}): Promise<string[]> => {\n const { entries, toPlan, all, explicit } = args\n\n if (explicit || all) return toPlan\n\n commandEcho.setInteractive()\n\n // The picker runs AFTER the plan so the operator chooses with each branch's\n // conflict status visible. Choosing first and learning second defeats the\n // preflight the whole design exists to provide.\n //\n // Keyed by `formatJiraName`, NOT by branch name: `formatBranchPickerItems`\n // looks descriptions up by the Jira version name (`v1.2.3`), because its\n // original caller passed a Jira map. A branch-keyed map silently misses every\n // entry and renders no status at all \u2014 which is precisely the no-op this\n // feature exists to avoid.\n const descriptions = new Map(\n entries.flatMap((entry) => {\n const id = parseBranchName(entry.branch)\n\n return id ? [[formatJiraName(id), describeEntry(entry)] as const] : []\n }),\n )\n\n return pickReleaseBranches(formatBranchPickerItems({ branches: toPlan, descriptions }), { required: true })\n}\n\n/** Reclassify, optionally verify, reclassify again, and perform the single atomic push. */\nconst applyPush = async (args: {\n cwd: string\n worktreePath: string\n entries: MergePlanEntry[]\n selected: string[]\n verify?: string\n}): Promise<Pick<RunOutcome, 'pushed' | 'pushAborted' | 'abortedBy' | 'observedLanded' | 'verifyFailed'>> => {\n const { cwd, worktreePath, entries, selected, verify } = args\n\n const chosen = entries.filter((entry) => {\n return selected.includes(entry.branch)\n })\n\n const announceDropped = (branches: string[]): void => {\n for (const branch of branches) {\n logger.info(`\u2139\uFE0F ${branch} was merged by someone else while we planned \u2014 dropping it from the push`)\n }\n }\n\n const first = await reclassify(cwd, pushableRefs(chosen))\n\n announceDropped(first.nowUpToDate)\n\n let kept = first.kept\n let verifyFailed = new Map<string, string>()\n\n if (verify && kept.length > 0) {\n logger.info(`\\n\uD83D\uDD0E Verifying ${kept.length} merge(s) with: ${verify}`)\n\n const outcome = await verifyMerges({ worktreePath, refs: kept, command: verify })\n\n verifyFailed = outcome.failed\n\n for (const [branch, reason] of outcome.failed) {\n logger.info(`\u2717 ${branch} failed verification \u2014 not pushing it. ${reason.split('\\n')[0] ?? ''}`)\n }\n\n // Reclassify AGAIN. The verification pass is the only step here that can take\n // minutes, and a teammate merging during it makes the first reclassification\n // stale \u2014 under `--atomic` a single stale ref aborts the push for EVERY\n // branch, which is the exact failure reclassification exists to prevent.\n const second = await reclassify(cwd, outcome.kept)\n\n announceDropped(second.nowUpToDate)\n kept = second.kept\n }\n\n if (kept.length === 0) return { pushed: false, pushAborted: false, verifyFailed }\n\n // Printed BEFORE the push so the transcript names what was in flight: an atomic\n // push that lands server-side but whose response is lost would otherwise leave\n // origin advanced while the report says \"aborted\".\n logger.info(\n `\\n\u2B06\uFE0F Pushing atomically: ${kept\n .map((ref) => {\n return ref.branch\n })\n .join(', ')}`,\n )\n\n const result = await pushAtomic(cwd, kept)\n\n if (result.pushed) return { pushed: true, pushAborted: false, verifyFailed }\n\n // `--atomic` is all-or-nothing for REFS, not for our knowledge of them. A push\n // that lands server-side but whose response is lost \u2014 a dropped connection, or\n // a kill between the push returning and this line \u2014 leaves origin fully\n // advanced while we are about to report \"aborted\". And `--atomic` makes that\n // wrong report MORE credible, because the operator has been told all-or-nothing\n // and reads \"aborted\" as \"nothing happened\". So never infer: go and look.\n const observed = await Promise.all(\n kept.map(async (ref) => {\n return { branch: ref.branch, landed: (await lsRemoteHead(cwd, ref.branch)) === ref.sha }\n }),\n )\n\n const landed = observed.filter((entry) => {\n return entry.landed\n })\n\n if (landed.length > 0) {\n logger.warn(\n `\u26A0\uFE0F The push reported failure, but origin already carries ${landed\n .map((entry) => {\n return entry.branch\n })\n .join(', ')} \u2014 re-run to reconcile.`,\n )\n }\n\n return {\n pushed: false,\n pushAborted: true,\n abortedBy: result.stderr?.split('\\n')[0],\n observedLanded: landed.map((entry) => {\n return entry.branch\n }),\n }\n}\n\nexport const ghMergeDev = async (args: GhMergeDevArgs) => {\n const { all, versions, dryRun = false, verify, confirmedCommand } = args\n\n // `true` (bare `--verify`) selects the cheap default tier; a string is the\n // operator's own command. Configuring at the call site rather than in repo\n // config is deliberate \u2014 this CLI's config file is a strict schema, so a new\n // key could not reach consumer repos without a schema change and a release.\n const verifyCommand = verify === true ? DEFAULT_VERIFY_COMMAND : verify || undefined\n\n // Deliberately NOT `assertManagementContext`: this command never touches an\n // existing checkout, so requiring a clean tree and refusing to run from a\n // linked worktree would only refuse work that is about to succeed.\n await assertRepoWithOrigin({ operation: 'merge dev into release branches' })\n\n // Only merge dev into regular releases (not hotfixes, which target main)\n const allPRs = await getReleasePRsWithInfo()\n const candidates = allPRs\n .filter((pr) => {\n return detectReleaseType(pr.title) === 'regular'\n })\n .map((pr) => {\n return pr.branch\n })\n\n if (candidates.length === 0) {\n logger.info('\u2139\uFE0F No open release branches found')\n\n commandEcho.print()\n\n return toResponse(emptyResult(dryRun))\n }\n\n const cwd = await getMainRepoRoot()\n\n try {\n await $({ cwd, quiet: true })`git fetch origin --prune`\n } catch (error) {\n throw new OperationError(error, {\n operation: 'fetch origin before planning the merges',\n remediation: 'check network access and that `origin` is reachable',\n })\n }\n\n const toPlan = versions === undefined ? candidates : resolveRequestedBranches(versions, candidates)\n\n const outcome = await withScratchWorktree({ cwd }, async (worktree): Promise<RunOutcome> => {\n // The worktree exists from here until the push completes, which spans the\n // confirmation prompt. Registering its removal means a Ctrl-C in that window\n // still releases it \u2014 `finally` alone does not run when Node takes the\n // default action for SIGINT, and a leaked registration breaks every\n // subsequent run on this machine until someone runs `git worktree prune`.\n const unregister = registerRunCleanup(worktree.remove)\n\n try {\n return await planConfirmAndPush({\n cwd,\n worktree,\n toPlan,\n candidates,\n all,\n versions,\n dryRun,\n verify: verifyCommand,\n confirmedCommand,\n })\n } finally {\n unregister()\n }\n })\n\n const { entries, selected, pushed, pushAborted, abortedBy, declined, verifyFailed } = outcome\n\n // One cheap porcelain read, so a failed branch's printed recipe names the\n // worktree it actually lives in rather than a that would fail.\n const worktrees = await listWorktrees(cwd).catch(() => {\n return [] as WorktreeEntry[]\n })\n\n return report({ entries, selected, pushed, pushAborted, abortedBy, declined, verifyFailed, dryRun, worktrees })\n}\n\nconst planConfirmAndPush = async (args: {\n cwd: string\n worktree: { path: string }\n toPlan: string[]\n candidates: string[]\n all?: boolean\n versions?: string | string[]\n dryRun: boolean\n verify?: string\n confirmedCommand: boolean | undefined\n}): Promise<RunOutcome> => {\n const { cwd, worktree, toPlan, candidates, all, versions, dryRun, verify, confirmedCommand } = args\n\n const entries = await planMergeRun({ cwd, worktreePath: worktree.path, branches: toPlan })\n const idle = { entries, pushed: false, pushAborted: false, declined: false }\n\n renderPlan(entries)\n\n if (dryRun) return { ...idle, selected: toPlan }\n\n const selected = await selectBranches({ entries, toPlan, all, explicit: versions !== undefined })\n\n if (selected.length === candidates.length) {\n commandEcho.addOption('--all', true)\n } else {\n // Branch names, not `releaseBranchLabels` output: its `flatMap` drops any\n // branch that fails `parseBranchName`, so a label echo can print a NARROWER\n // selection than the one that just ran.\n commandEcho.addOption('--versions', selected)\n }\n\n const willPush = entries.filter((entry) => {\n return selected.includes(entry.branch) && entry.mergeSha\n }).length\n\n if (willPush > 0) {\n // Named before the operator commits, because it is the one consequence that\n // reaches beyond git: each push re-triggers CI on that branch's open PR, and\n // if the repo dismisses stale approvals on new commits, every one of those\n // PRs loses its approvals at once.\n logger.info(\n `\\n\u2139\uFE0F This will push ${willPush} branch(es), re-running CI on ${willPush} open PR(s).` +\n ' If the repo dismisses stale approvals on new commits, those PRs lose them.',\n )\n }\n\n try {\n // `throwOnDecline` is load-bearing here, not stylistic: the scratch worktree\n // is alive at this point, and `process.exit(0)` would skip the `finally`\n // that removes it \u2014 leaking one on the most ordinary non-happy path there is.\n await confirmOrExit(\n confirmedCommand,\n `Are you sure you want to merge dev into these branches: ${selected.join(', ')}?`,\n { throwOnDecline: true },\n )\n } catch (error) {\n if (!isCommandDeclined(error)) throw error\n\n return { ...idle, selected, declined: true }\n }\n\n if (!confirmedCommand) {\n commandEcho.addOption('--yes', true)\n }\n\n return {\n entries,\n selected,\n declined: false,\n ...(await applyPush({ cwd, worktreePath: worktree.path, entries, selected, verify })),\n }\n}\n\n/** Turn the run's outcome into the printed report and the structured result. */\nconst report = (args: RunOutcome & { dryRun: boolean; worktrees: WorktreeEntry[] }) => {\n const { entries, selected, pushed, pushAborted, abortedBy, declined, dryRun, worktrees, verifyFailed } = args\n\n const pushedBranches = new Set(\n pushed\n ? entries\n .filter((entry) => {\n // Having a merge sha is not the same as having been pushed: a branch\n // verification refused was dropped from the refspec set before the\n // push was assembled, so reporting it as pushed would be a lie about\n // an irreversible operation.\n return selected.includes(entry.branch) && entry.mergeSha && !verifyFailed?.has(entry.branch)\n })\n .map((entry) => {\n return entry.branch\n })\n : [],\n )\n\n const results: MergeDevResultEntry[] = entries\n .filter((entry) => {\n return selected.includes(entry.branch)\n })\n .map((entry) => {\n const wasPushable = Boolean(entry.mergeSha)\n const verifyReason = args.verifyFailed?.get(entry.branch)\n\n // Verification refusal outranks the push outcome: the branch never reached\n // the push, so reporting it as `push-aborted` would name the wrong cause.\n const pushOutcome: ResultStatus = pushAborted && wasPushable ? 'push-aborted' : entry.status\n const status: ResultStatus = verifyReason ? 'verify-failed' : pushOutcome\n\n return {\n branch: entry.branch,\n status,\n mergeSha: entry.mergeSha,\n conflictPaths: entry.conflictPaths,\n pushed: pushedBranches.has(entry.branch),\n reason: verifyReason ?? entry.reason,\n }\n })\n\n const failed = results.filter((entry) => {\n return !SUCCESS_STATUSES.has(entry.status)\n })\n\n // One structured line per branch, so a transcript can be read back mechanically\n // rather than scraped from the human summary below.\n for (const entry of results) {\n const planned = entries.find((candidate) => {\n return candidate.branch === entry.branch\n })\n\n logger.info({\n branch: entry.branch,\n status: entry.status,\n mergeSha: entry.mergeSha,\n conflictCount: entry.conflictPaths?.length ?? 0,\n durationMs: planned?.durationMs,\n pushed: entry.pushed,\n msg: 'merge-dev branch result',\n })\n }\n\n if (declined || dryRun) {\n logger.info(declined ? '\\nOperation cancelled \u2014 nothing was pushed.\\n' : '\\n\uD83D\uDD0D Dry run \u2014 nothing was pushed.\\n')\n } else if (failed.length > 0) {\n logger.info(`\\n\u26A0\uFE0F ${failed.length} branch(es) did not merge.\\n`)\n for (const entry of failed) {\n logger.info(`# ${entry.branch} \u2014 ${entry.status}\\n${reproduceCommand(entry.branch, worktrees)}\\n`)\n }\n } else {\n logger.info('\u2705 All merges completed successfully!\\n')\n }\n\n commandEcho.print()\n\n // A dry run must not claim merges: `--json` is wired globally, so this reaches\n // CLI scripts today, not only MCP agents.\n const reportable = dryRun || declined\n\n return toResponse({\n successfulMerges: reportable\n ? 0\n : results.filter((entry) => {\n return SUCCESS_STATUSES.has(entry.status)\n }).length,\n failedMerges: reportable ? 0 : failed.length,\n failedBranches: reportable\n ? []\n : failed.map((entry) => {\n return entry.branch\n }),\n totalBranches: results.length,\n dryRun,\n atomicPush: { attempted: !dryRun && !declined, aborted: pushAborted, abortedBy },\n results: results.map((entry) => {\n return reportable ? { ...entry, pushed: false } : entry\n }),\n })\n}\n\n// MCP Tool Registration\nexport const ghMergeDevMcpTool = defineMcpTool({\n name: 'gh-merge-dev',\n description:\n 'Merge origin/dev into open regular (non-hotfix) release branches and push them in one atomic push. Merges happen in a disposable scratch worktree, so no existing checkout is touched and the branches may be checked out elsewhere. When invoked via MCP, pass all=true or an explicit versions list \u2014 the branch picker is unreachable without a TTY, and the confirmation prompt is auto-skipped for MCP calls, so the caller is responsible for gating. Irreversible once pushed.',\n requiresHumanConfirm: true,\n inputSchema: {\n all: z\n .boolean()\n .optional()\n .describe(\n 'Target every open regular release branch. Must be true for MCP calls unless `versions` is supplied (the interactive picker is unavailable without a TTY).',\n ),\n versions: z\n .union([z.string(), z.array(z.string())])\n .optional()\n .describe(\n 'Target a subset of the open regular release branches. Each entry may be a version label (\"1.2.5\") or a raw branch name (\"release/v1.2.5\"). A selector that is not an open regular release is an error \u2014 hotfix branches cannot be selected. Takes precedence over `all`.',\n ),\n dryRun: z.boolean().optional().describe('Compute and print the merge plan without pushing anything.'),\n verify: z\n .union([z.boolean(), z.string()])\n .optional()\n .describe(\n 'Check each merge before pushing it. `true` runs `pnpm install --frozen-lockfile` \u2014 which catches the characteristic dev\u2192release failure, a pnpm-lock.yaml whose text merge is clean but whose result is broken. A string runs that command instead. A branch that fails verification is dropped from the push; nothing is ever rolled back, because verification runs pre-push.',\n ),\n confirm: z\n .boolean()\n .optional()\n .describe('Set true to execute; omit for a dry-run gate that echoes the resolved action.'),\n },\n outputSchema: {\n successfulMerges: z.number().describe('Number of branches that reached their desired state'),\n failedMerges: z.number().describe('Number of branches that did not'),\n failedBranches: z.array(z.string()).describe('List of branches that did not merge'),\n totalBranches: z.number().describe('Total number of branches processed'),\n dryRun: z.boolean().describe('True when nothing was pushed because this was a plan-only run'),\n atomicPush: z\n .object({\n attempted: z.boolean(),\n aborted: z.boolean(),\n abortedBy: z.string().optional(),\n })\n .describe('Outcome of the single all-or-nothing push'),\n results: z\n .array(\n z.object({\n branch: z.string(),\n status: z.string(),\n mergeSha: z.string().optional(),\n conflictPaths: z.array(z.string()).optional(),\n pushed: z.boolean(),\n reason: z.string().optional(),\n }),\n )\n .describe('Per-branch terminal state'),\n },\n handler: ghMergeDev,\n})\n", "import { $ } from 'zx'\n\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { logger } from 'src/lib/logger'\n\n/**\n * Validate GitHub CLI installation and authentication status and throw an error if not valid\n */\nexport const validateGitHubCliAndAuth = async () => {\n try {\n await $`gh --version`\n } catch (error: unknown) {\n logger.error({ error }, 'Error: GitHub CLI (gh) is not installed.')\n throw new OperationError(error, {\n operation: 'verify GitHub CLI is installed',\n remediation: 'install gh from https://cli.github.com/',\n })\n }\n\n try {\n await $`gh auth status`\n } catch (error: unknown) {\n logger.error({ error }, 'Error: GitHub CLI (gh) is not authenticated.')\n throw new OperationError(error, {\n operation: 'verify GitHub CLI authentication',\n remediation: 'run \"gh auth login\" (https://cli.github.com/manual/gh_auth_login)',\n })\n }\n}\n", "import { $ } from 'zx'\n\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { assertCleanCheckout } from 'src/lib/git-guard'\nimport { logger } from 'src/lib/logger'\nimport { compareReleaseIds, formatBranchName, formatPrTitle, parseBranchName } from 'src/lib/release-id'\nimport type { ReleaseId } from 'src/lib/release-id'\nimport { buildReleasePrBody, getBaseBranch } from 'src/lib/release-utils'\nimport type { ReleaseType } from 'src/lib/release-utils'\n\ninterface ReleasePR {\n headRefName: string\n number: number\n state: string\n title: string\n baseRefName: string\n createdAt: string\n}\n\nexport interface ReleasePRInfo {\n branch: string\n title: string\n createdAt: string\n}\n\n/**\n * Sort release head refs in the locked deterministic order (versions block\n * first by semver ascending, then names by PR creation date). Head refs that\n * are not valid release branches (parseBranchName \u2192 null) are filtered out\n * rather than throwing or NaN-sorting, so a stray junk branch can never break\n * discovery. Carries each PR's createdAt for name ordering.\n */\nconst sortReleasePRs = (prs: ReleasePR[]): ReleasePR[] => {\n return prs\n .map((pr) => {\n return { pr, id: parseBranchName(pr.headRefName) }\n })\n .filter((entry): entry is { pr: ReleasePR; id: NonNullable<typeof entry.id> } => {\n return entry.id !== null\n })\n .sort((a, b) => {\n return compareReleaseIds(a.id, b.id, { a: a.pr.createdAt, b: b.pr.createdAt })\n })\n .map((entry) => {\n return entry.pr\n })\n}\n\n/**\n * Page size for release-PR discovery. `gh pr list` defaults to 30, which\n * silently drops older open release PRs from every consumer of discovery\n * (list, merge-dev, deploy pickers). Explicit and high enough that hitting it\n * is a real anomaly, which `warnIfTruncated` then reports rather than hiding.\n */\nconst PR_DISCOVERY_LIMIT = 200\n\n/**\n * Surface a discovery page that came back full: at that point the result is\n * indistinguishable from a truncated one, and a silently short list reads as\n * \"there are no other releases\".\n */\nconst warnIfTruncated = (prs: ReleasePR[], source: string): void => {\n if (prs.length < PR_DISCOVERY_LIMIT) return\n\n logger.warn(\n { source, limit: PR_DISCOVERY_LIMIT },\n `\u26A0\uFE0F ${source} returned ${PR_DISCOVERY_LIMIT} PRs \u2014 the page is full, so older release PRs may be missing`,\n )\n}\n\n/**\n * Fetch all open release/hotfix PRs from GitHub.\n * Searches both dev (regular) and main (hotfix) base branches.\n * Returns deduplicated ReleasePR objects.\n */\nconst fetchAllReleasePRs = async (): Promise<ReleasePR[]> => {\n const releasePRs =\n await $`gh pr list --limit ${PR_DISCOVERY_LIMIT} --search \"Release in:title\" --base dev --json number,title,headRefName,state,baseRefName,createdAt`\n\n const hotfixPRs =\n await $`gh pr list --limit ${PR_DISCOVERY_LIMIT} --search \"Hotfix in:title\" --base main --json number,title,headRefName,state,baseRefName,createdAt`\n\n const releaseList: ReleasePR[] = JSON.parse(releasePRs.stdout)\n const hotfixList: ReleasePR[] = JSON.parse(hotfixPRs.stdout)\n\n warnIfTruncated(releaseList, 'release PR discovery (base dev)')\n warnIfTruncated(hotfixList, 'hotfix PR discovery (base main)')\n\n const all: ReleasePR[] = [...releaseList, ...hotfixList]\n\n // Deduplicate by headRefName\n const seen = new Set<string>()\n\n return all.filter((pr) => {\n if (seen.has(pr.headRefName)) return false\n\n seen.add(pr.headRefName)\n\n return true\n })\n}\n\n/**\n * Fetch, guard against empty, and sort all open release PRs in the locked\n * deterministic order. Shared core of the two public variants below; wraps the\n * gh calls so a fetch failure surfaces as an OperationError.\n */\nconst loadSortedReleasePRs = async (): Promise<ReleasePR[]> => {\n try {\n const prs = await fetchAllReleasePRs()\n\n if (prs.length === 0) {\n throw new OperationError(undefined, {\n operation: 'find open release PRs',\n remediation: 'open a release PR first, or check you are in the right repo',\n })\n }\n\n return sortReleasePRs(prs)\n } catch (error) {\n if (error instanceof OperationError) throw error\n\n // `debug`, not `error`: this rethrows as an OperationError, and `entry/cli.ts` logs any\n // uncaught error at ERROR and exits 1 \u2014 so logging here too printed one fault as two red\n // lines. Kept (demoted, not deleted) because the wrapped message renders only the operation\n // and remediation; the cause's stack survives here and is reachable with `--debug`.\n logger.debug({ err: error }, 'Error fetching release PRs')\n\n throw new OperationError(error, { operation: 'fetch release PRs' })\n }\n}\n\n/**\n * Fetch open release PRs from GitHub with 'Release' or 'Hotfix' in the title.\n * Returns an array of headRefName strings in the locked deterministic order\n * (version branches first by semver ascending, then named branches by PR\n * creation date). Unparseable head refs are filtered out.\n *\n * @returns [release/v1.18.22, release/v1.18.23, release/checkout-redesign]\n */\nexport const getReleasePRs = async (): Promise<string[]> => {\n const prs = await loadSortedReleasePRs()\n\n return prs.map((pr) => {\n return pr.headRefName\n })\n}\n\n/**\n * Fetch open release PRs with title info (for detecting release type).\n * Returns ReleasePRInfo objects in the locked deterministic order (version\n * branches first by semver ascending, then named branches by PR creation\n * date). Unparseable head refs are filtered out.\n */\nexport const getReleasePRsWithInfo = async (): Promise<ReleasePRInfo[]> => {\n const prs = await loadSortedReleasePRs()\n\n return prs.map((pr) => {\n return {\n branch: pr.headRefName,\n title: pr.title,\n createdAt: pr.createdAt,\n }\n })\n}\n\ninterface UpdateReleasePRBodyArgs {\n branch: string\n body: string\n}\n\n/**\n * Update the body of an open release PR identified by its head branch.\n */\nexport const updateReleasePRBody = async (args: UpdateReleasePRBodyArgs): Promise<void> => {\n const { branch, body } = args\n\n try {\n await $({ quiet: true })`gh pr edit ${branch} --body ${body}`\n } catch (error: unknown) {\n logger.error({ error, branch }, `Error updating release PR body for ${branch}`)\n throw error\n }\n}\n\ninterface CreateReleaseBranchArgs {\n id: ReleaseId\n jiraVersionUrl: string\n type: ReleaseType\n description?: string\n /**\n * The `origin/<base>` SHA `prepareGitForRelease` just landed on. The caller guarantees a\n * freshly fetched and fast-forwarded base checkout; this is how that guarantee is checked\n * at the moment it is consumed rather than taken on trust.\n */\n baseSha: string\n}\n\n/**\n * Roll back a release branch that was created locally but never reached the remote.\n *\n * Only the local-only window is undone, and the probe is what establishes that window: what\n * the code can observe is not \"the push has not happened\" but \"the push *call* rejected\", and\n * a push the remote accepted whose transport then died exits non-zero with the ref created.\n * So a ref that exists on origin \u2014 or a probe that cannot answer \u2014 means keep the branch and\n * report it. Deleting there would destroy the operator's only handle on a half-made release.\n */\n// Never throws. It runs on the failure path, and an exception here would replace the real\n// diagnosis with whatever the cleanup tripped over. `git switch` has to come first because\n// `deleteLocalBranch` silently no-ops on the current branch, so a rollback that skipped it\n// would report success while leaving the branch in place.\nconst cleanupUnpushedBranch = async (branchName: string, baseBranch: string): Promise<void> => {\n const git = $({ quiet: true, nothrow: true })\n\n const refs = await git`git ls-remote --heads origin ${branchName}`\n const probeFailed = refs.exitCode !== 0\n const onRemote = refs.stdout.trim().length > 0\n\n if (probeFailed || onRemote) {\n // Two different states, reported as two different sentences. Telling an operator a branch\n // \"may\" exist when we just listed it wastes their time re-checking; telling them it does\n // exist when the probe never answered would be a claim we cannot support.\n logger.error(\n { branchName },\n probeFailed\n ? `could not reach origin to check whether ${branchName} was pushed \u2014 the branch is left in place; check with \\`git ls-remote --heads origin ${branchName}\\` before deleting anything`\n : `release branch ${branchName} exists on origin without a PR \u2014 left in place; delete it with \\`git push origin --delete ${branchName}\\` once you are sure`,\n )\n\n return\n }\n\n // The switch has to land before the delete: git refuses to delete the branch you are standing\n // on, and under `nothrow` that refusal is silent. So the delete is checked rather than assumed \u2014\n // a cleanup that quietly did nothing is worse than one that says so, because the operator would\n // otherwise discover it only when the retry fails with \"branch already exists\".\n await git`git switch ${baseBranch}`\n\n const deleted = await git`git branch -D ${branchName}`\n\n if (deleted.exitCode !== 0) {\n logger.error(\n { branchName },\n `could not remove the local branch ${branchName} after a failed release \u2014 delete it with \\`git branch -D ${branchName}\\` before retrying`,\n )\n }\n}\n\n// Function to create a release branch\nexport const createReleaseBranch = async (\n args: CreateReleaseBranchArgs,\n): Promise<{ branchName: string; prUrl: string }> => {\n const { id, jiraVersionUrl, type, description, baseSha } = args\n const prTitle = formatPrTitle(id, type)\n const baseBranch = getBaseBranch(type)\n const git = $({ quiet: true })\n\n const branchName = formatBranchName(id)\n\n const body = buildReleasePrBody(jiraVersionUrl, description)\n\n try {\n // The base switch/pull the old code repeated here is gone: `prepareGitForRelease` already\n // did it, and doing it twice only widened the window in which the checkout could change\n // under us. What replaces it is a check rather than a repeat \u2014 the two assertions below\n // are the *last* thing that happens before the first destructive command, because the\n // caller's own checks are separated from this point by a Jira round trip.\n await assertCleanCheckout({ operation: `create release branch ${branchName}` })\n\n const head = (await git`git rev-parse HEAD`).stdout.trim()\n\n if (head !== baseSha) {\n throw new OperationError(undefined, {\n operation: `create release branch ${branchName}`,\n remediation: 'something moved the checkout mid-run \u2014 re-run the release',\n stderrExcerpt: `expected to be on ${baseSha.slice(0, 8)} (${baseBranch}) but HEAD is ${head.slice(0, 8)}`,\n })\n }\n\n await git`git checkout -b ${branchName}`\n\n // Everything from here on can leave a branch behind, so it runs under cleanup. The commit\n // precedes the push deliberately: pushing first published a branch with zero commits, so\n // any later failure left a remote ref that could not even take a PR (`gh pr create`\n // rejects \"no commits between base and head\"). One push, of a branch that is already\n // complete, makes the local-only window cover the ordinary failures instead of one command.\n try {\n await git`git commit --allow-empty-message --allow-empty --message ''`\n await git`git push -u origin ${branchName}`\n\n const prResult =\n await git`gh pr create --title ${prTitle} --body ${body} --base ${baseBranch} --head ${branchName}`\n\n return {\n branchName,\n prUrl: prResult.stdout.trim(),\n }\n } catch (error: unknown) {\n await cleanupUnpushedBranch(branchName, baseBranch)\n\n throw error\n }\n } catch (error: unknown) {\n logger.error({ error, branchName }, `Error creating release branch ${branchName}`)\n\n throw error\n } finally {\n // In a `finally`, and swallowing: the old code put this switch on the success path only,\n // so a failure left the operator standing on a half-made release branch \u2014 the one place\n // the \"you end on the base branch\" contract mattered most. Swallowing is required for the\n // same reason the cleanup swallows: a throw here would replace the original error.\n await $({ quiet: true, nothrow: true })`git switch ${baseBranch}`\n }\n}\n", "import { $ } from 'zx'\n\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport {\n getCurrentBranch,\n getProjectRoot,\n getWorkingTreeStatus,\n isInsideLinkedWorktree,\n listWorktrees,\n} from 'src/lib/git-utils'\nimport { logger } from 'src/lib/logger'\n\nexport interface AssertManagementContextArgs {\n /** Operation name surfaced in the failure message, e.g. 'create release'. */\n operation: string\n}\n\n/**\n * The preflight for a command that never touches an existing checkout: it needs a\n * repository and a resolvable `origin`, and nothing else. Deliberately narrower than\n * {@link assertManagementContext} \u2014 no linked-worktree check and no clean-tree check.\n */\n// The omissions are the point rather than an oversight:\n//\n// - No linked-worktree check. The command does its work in a scratch worktree of its own, so where\n// it was invoked from is irrelevant.\n// - No clean-tree check. It reads `refs/remotes/*` and writes only to its own scratch checkout, so\n// the operator's uncommitted work is never at stake. The old guard forced a stash before what is,\n// in effect, a remote-refs operation.\n//\n// Keeping this separate leaves `assertManagementContext` untouched for the commands that genuinely\n// do consume the operator's checkout.\nexport const assertRepoWithOrigin = async (args: AssertManagementContextArgs): Promise<void> => {\n const { operation } = args\n\n try {\n await $({ quiet: true })`git rev-parse --git-dir`\n } catch (error) {\n throw new OperationError(error, {\n operation,\n remediation: 'run this from inside a git repository',\n })\n }\n\n try {\n await $({ quiet: true })`git remote get-url origin`\n } catch (error) {\n throw new OperationError(error, {\n operation,\n remediation: 'this repository has no `origin` remote to fetch from or push to',\n })\n }\n}\n\n/** How many porcelain paths the single-line `OperationError` carries before it summarises. */\nconst MAX_LISTED_PATHS = 4\n\n/**\n * Refuse an operation that would consume the operator's checkout while it holds uncommitted\n * work, naming the paths that block it.\n *\n * Split out of {@link assertManagementContext} because tree cleanliness is the *volatile*\n * half of that guard: it is the only leg that can stop being true between the check and the\n * mutation it authorizes, so it has to be re-assertable on its own immediately before each\n * destructive step.\n */\n// Two channels, because one cannot carry the answer. `OperationError` renders a single line\n// and caps the excerpt at 200 bytes (`operation-error.ts`), which is three or four porcelain\n// entries \u2014 any tree dirtied by a build or an editor overflows it. So the full list goes to\n// the log and the error carries a bounded head plus an honest count. The count is computed\n// from the real total, never from the truncated slice.\nexport const assertCleanCheckout = async (args: AssertManagementContextArgs): Promise<void> => {\n const { operation } = args\n const status = await getWorkingTreeStatus()\n\n if (status.length === 0) return\n\n logger.error(`working tree has uncommitted changes:\\n ${status.join('\\n ')}`)\n\n const listed = status.slice(0, MAX_LISTED_PATHS)\n const remaining = status.length - listed.length\n const summary = remaining > 0 ? `${listed.join(', ')} (+${remaining} more)` : listed.join(', ')\n\n throw new OperationError(undefined, {\n operation,\n remediation: 'commit or stash your changes (`git stash -u`), then retry',\n stderrExcerpt: `working tree has uncommitted changes: ${summary}`,\n })\n}\n\nexport interface AssertBaseBranchSwitchableArgs extends AssertManagementContextArgs {\n /** The branch the command is about to `git switch` onto, e.g. `dev` or `main`. */\n base: string\n}\n\n/**\n * Refuse before the command tries to `git switch` onto a base branch that another linked\n * worktree already holds \u2014 the ordinary case on a team that keeps a worktree per release.\n *\n * git's own failure already names the holding path, so this exists for the *remediation*:\n * without it the refusal surfaces through a caller that advises \"verify the version or name\n * is unique and the base branch is clean\", which is the wrong action entirely. Refusing here\n * also moves the failure ahead of the confirmation prompt instead of into the middle of a\n * batch that has already created releases.\n */\n// The current checkout is tested by BRANCH, not by path. `listWorktrees` reports git's own\n// symlink-resolved paths while `getProjectRoot` reports `--show-toplevel`, and on macOS those\n// disagree (`/var` vs `/private/var`), so a path-equality predicate mis-fires. Branch identity\n// is exact instead of approximate: git forbids two worktrees checking out one branch, so\n// \"the current branch is the base\" is a complete proof that the holder is us.\nexport const assertBaseBranchSwitchable = async (args: AssertBaseBranchSwitchableArgs): Promise<void> => {\n const { operation, base } = args\n\n if ((await getCurrentBranch()) === base) return\n\n const holder = (await listWorktrees(await getProjectRoot())).find((entry) => {\n return entry.branch === base\n })\n\n if (!holder) return\n\n throw new OperationError(undefined, {\n operation,\n remediation: `close or remove that worktree, or run the release from it`,\n stderrExcerpt: `base branch \"${base}\" is checked out in the worktree at ${holder.path}`,\n })\n}\n\n/**\n * Guard release- and worktree-management commands so they run only from the main repository\n * checkout, with a clean working tree. Both refusals throw {@link OperationError}, which surfaces\n * uniformly to CLI users and MCP-connected agents.\n *\n * Deliberately says nothing about which branch you are on.\n */\n// The two states checked are the ones no command can recover from on the operator's behalf: a\n// linked worktree (the wrong checkout entirely) and a dirty tree (their uncommitted work is at\n// stake).\n//\n// No branch assertion, because the commands that need a canonical branch (`release-create`,\n// `gh-merge-dev`) switch onto it themselves, after their confirmation prompt and behind a\n// `git fetch` \u2014 neither of which a guard running before consent could do. The rest\n// (`worktrees-*`) address branches by name and never read `HEAD`. A branch assertion here would\n// only refuse work that is about to succeed anyway.\nexport const assertManagementContext = async (args: AssertManagementContextArgs): Promise<void> => {\n const { operation } = args\n\n if (await isInsideLinkedWorktree()) {\n throw new OperationError(undefined, {\n operation,\n remediation: 'run this from the main repository checkout, not a linked git worktree',\n stderrExcerpt: 'command run from inside a linked worktree',\n })\n }\n\n await assertCleanCheckout({ operation })\n}\n", "import { $ } from 'zx'\n\nimport { createReleaseBranch } from 'src/integrations/gh'\nimport {\n buildJiraVersionUrl,\n createJiraVersion,\n findVersionByName,\n getProjectVersions,\n loadJiraConfigOptional,\n updateJiraVersion,\n} from 'src/integrations/jira'\nimport type { JiraConfig, JiraVersion } from 'src/integrations/jira'\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { logger } from 'src/lib/logger'\n// Type-only cross-layer import: src/lib/prompts/types.ts is intentionally a\n// zero-import leaf so it can be imported from any layer (including this one)\n// without creating a cycle.\nimport type { BranchPickerItem } from 'src/lib/prompts/types'\nimport { displayLabel, formatBranchName, formatJiraName, parseBranchName, parseReleaseRef } from 'src/lib/release-id'\nimport type { ReleaseId } from 'src/lib/release-id'\n\n/** Sentinel ref for deploying from the `dev` branch instead of a release branch. */\nexport const DEV_REF = 'dev'\n\nexport type ReleaseType = 'regular' | 'hotfix'\n\n/**\n * Get the base branch for a release type.\n * Regular releases branch from/to dev, hotfixes branch from/to main.\n */\nexport const getBaseBranch = (type: ReleaseType): string => {\n return type === 'hotfix' ? 'main' : 'dev'\n}\n\n/**\n * The body of a release PR: the Jira fix-version link, then the description when there is one.\n *\n * Single home for a rule that was written out twice, byte for byte \u2014 once where the PR is opened\n * and once where its description is edited. Two copies of a format string drift silently, and the\n * drift only shows up as a PR whose body no longer matches what the other command would write.\n */\nexport const buildReleasePrBody = (jiraVersionUrl: string, description?: string): string => {\n return (description ?? '').trim() !== '' ? `${jiraVersionUrl}\\n\\n${description}` : `${jiraVersionUrl} \\n`\n}\n\nexport interface ReleaseCreationResult {\n version: string\n type: ReleaseType\n branchName: string\n prUrl: string\n jiraVersionUrl: string\n}\n\n/**\n * Put the operator's checkout on a freshly fetched base branch and report the SHA it landed\n * on, so the caller can prove that checkout is still the verified one when it finally mutates.\n *\n * `--ff-only` on the pull: without it a base branch that has drifted locally is *merged*\n * rather than refused, which writes a merge commit onto `dev`/`main` in the operator's own\n * checkout and cuts the release from a commit nobody reviewed. Refusing is the safe half of\n * that trade \u2014 the cost of a false refusal is one `git pull --rebase`, the cost of a false\n * proceed is a release branch rooted in an accidental merge.\n */\n// Quiet is scoped per invocation rather than set on the global `$`. The old form assigned\n// `$.quiet = true` and cleared it only after the last successful statement, so any throw in\n// between left the whole process silent \u2014 and this process is also a long-lived MCP server,\n// so the leak outlived the failing tool call and muted every command after it.\nexport const prepareGitForRelease = async (type: ReleaseType = 'regular'): Promise<string> => {\n const baseBranch = getBaseBranch(type)\n const git = $({ quiet: true })\n\n await git`git fetch origin`\n await git`git switch ${baseBranch}`\n\n try {\n await git`git pull --ff-only origin ${baseBranch}`\n } catch (error) {\n // `nothrow`, because this probe exists only to enrich the refusal. If it fails in turn \u2014\n // `origin/<base>` missing, a broken remote \u2014 throwing from here would replace an accurate\n // \"your base branch has diverged\" with an unrelated error about counting commits.\n const counts = await $({\n quiet: true,\n nothrow: true,\n })`git rev-list --left-right --count origin/${baseBranch}...${baseBranch}`\n const [behind = '?', ahead = '?'] = counts.exitCode === 0 ? counts.stdout.trim().split(/\\s+/) : []\n const diverged = ahead !== '0' || behind !== '0'\n\n // The excerpt is attached ONLY when the branches really have diverged. `buildMessage` gives an\n // explicit `stderrExcerpt` priority over the cause, so an unconditional one would overwrite\n // git's own text on every OTHER `--ff-only` failure \u2014 auth, network, a branch with no upstream \u2014\n // and print \"local dev is 0 commit(s) ahead and 0 behind\" next to advice about diverging. Left\n // off, `extractStderr` walks to the zx error and reports what git actually said.\n throw new OperationError(error, {\n operation: `fast-forward ${baseBranch} onto origin/${baseBranch}`,\n remediation: diverged\n ? `your local ${baseBranch} has diverged \u2014 rebase or reset it to origin/${baseBranch}, then retry`\n : `check your access to origin and that ${baseBranch} tracks it, then retry`,\n ...(diverged\n ? { stderrExcerpt: `local ${baseBranch} is ${ahead} commit(s) ahead and ${behind} behind origin/${baseBranch}` }\n : {}),\n })\n }\n\n return (await git`git rev-parse HEAD`).stdout.trim()\n}\n\ninterface CreateSingleReleaseArgs {\n id: ReleaseId\n jiraConfig: JiraConfig\n description?: string\n type?: ReleaseType\n /** The base-branch SHA from {@link prepareGitForRelease}, forwarded to the branch cut. */\n baseSha: string\n}\n\ninterface EnsureJiraVersionArgs {\n versionName: string\n jiraConfig: JiraConfig\n description?: string\n}\n\n/**\n * Return the project's fix version named `versionName`, creating it only if it is absent.\n *\n * Idempotent on purpose. `createJiraVersion` POSTs unconditionally and Jira answers a\n * duplicate name with a 4xx, so the unconditional create made a *retry* impossible: the\n * version outlives any later git failure, and the next attempt at the same release dies on\n * the leftover rather than on whatever actually went wrong. Reusing it costs one lookup \u2014 the\n * same `getProjectVersions` call this module already makes elsewhere \u2014 and keeps the older\n * invariant that a release PR never exists without its fix version.\n */\n// Reuse is deliberately not silent about two things:\n//\n// - A differing description is written through. The PR body is composed from the description\n// passed to THIS call, so a bare reuse would leave the PR and the fix version disagreeing\n// permanently, with nothing to indicate which one is current.\n// - A released or archived version is refused rather than reused. Re-cutting a release whose\n// version was already delivered is a plausible mistake, and quietly attaching a new branch\n// to a closed version would misreport the delivered scope.\nconst ensureJiraVersion = async (args: EnsureJiraVersionArgs): Promise<JiraVersion> => {\n const { versionName, jiraConfig, description } = args\n const existing = await findVersionByName(versionName, jiraConfig)\n\n if (!existing) {\n const created = await createJiraVersion(\n {\n name: versionName,\n projectId: jiraConfig.projectId,\n description: description || '',\n released: false,\n archived: false,\n },\n jiraConfig,\n )\n\n return created.version!\n }\n\n if (existing.released || existing.archived) {\n throw new OperationError(undefined, {\n operation: `reuse Jira fix version \"${versionName}\"`,\n remediation: 'pick a different version, or un-release it in Jira first',\n stderrExcerpt: `fix version \"${versionName}\" is already ${existing.released ? 'released' : 'archived'}`,\n })\n }\n\n const wanted = description || ''\n\n if (wanted !== '' && wanted !== (existing.description ?? '')) {\n await updateJiraVersion({ versionId: existing.id, description: wanted }, jiraConfig)\n\n return { ...existing, description: wanted }\n }\n\n return existing\n}\n\n/**\n * Create a single release by creating both Jira version and GitHub release branch\n */\nexport const createSingleRelease = async (args: CreateSingleReleaseArgs): Promise<ReleaseCreationResult> => {\n const { id, jiraConfig, description, type = 'regular', baseSha } = args\n // 1. Ensure the Jira version exists (mandatory). For versioned releases this is\n // \"v1.2.3\" (byte-identical to before); for named releases it is \"<name>\".\n const versionName = formatJiraName(id)\n const jiraVersion = await ensureJiraVersion({ versionName, jiraConfig, description })\n const jiraVersionUrl = buildJiraVersionUrl(jiraConfig, jiraVersion)\n\n // 2. Create GitHub release branch\n const releaseInfo = await createReleaseBranch({ id, jiraVersionUrl, type, description, baseSha })\n\n return {\n version: displayLabel(id),\n type,\n branchName: releaseInfo.branchName,\n prUrl: releaseInfo.prUrl,\n jiraVersionUrl,\n }\n}\n\n/**\n * Fetch Jira version descriptions mapped by version name (e.g., \"v1.2.5\" \u2192 \"Some description\")\n * Gracefully returns empty map if Jira is unavailable\n */\nexport const getJiraDescriptions = async (): Promise<Map<string, string>> => {\n const descriptions = new Map<string, string>()\n\n const jiraConfig = await loadJiraConfigOptional()\n\n if (!jiraConfig) return descriptions\n\n try {\n const versions = await getProjectVersions(jiraConfig)\n\n for (const version of versions) {\n if (version.description) {\n descriptions.set(version.name, version.description)\n }\n }\n } catch (error) {\n // WARN, not ERROR: the only residue is cosmetic \u2014 release rows render without their Jira\n // description. Loud enough to name the likely cause, quiet enough not to claim the command\n // failed when it did not. MUST NOT rethrow: `worktrees list` awaits this inside a\n // `Promise.all`, so throwing here would take the whole listing down over missing decoration.\n logger.warn(\n { err: error },\n 'Jira descriptions unavailable \u2014 the release list will render without them. If this persists, check JIRA_EMAIL / JIRA_TOKEN.',\n )\n }\n\n return descriptions\n}\n\n/**\n * Format a version string with its release type tag, e.g. \"1.2.5 [regular]\"\n * When maxVersionLength is provided, pads the version for alignment.\n */\nexport const formatVersionLabel = (version: string, type: ReleaseType, maxVersionLength?: number): string => {\n const padding = maxVersionLength ? ' '.repeat(maxVersionLength - version.length + 3) : ' '\n const tag = `[${type}]`.padEnd(11)\n\n return `${version}${padding}${tag}`\n}\n\n/**\n * Detect release type from PR title.\n * PRs titled \"Hotfix v...\" are hotfix, everything else is regular.\n */\nexport const detectReleaseType = (title: string): ReleaseType => {\n return title.toLowerCase().startsWith('hotfix') ? 'hotfix' : 'regular'\n}\n\ninterface FormatBranchChoicesArgs {\n branches: string[]\n descriptions: Map<string, string>\n types?: Map<string, ReleaseType>\n}\n\ninterface ParsedBranchChoice {\n branch: string\n id: ReleaseId\n /** Human display label: `1.2.3` | `<name>`. */\n label: string\n}\n\n/**\n * Parse branches into release ids, dropping any that do not parse (lenient\n * discovery source). Exported for unit testing the version/name/junk split.\n */\nexport const parseBranchChoices = (branches: string[]): ParsedBranchChoice[] => {\n return branches.flatMap((branch) => {\n const id = parseBranchName(branch)\n\n if (!id) return []\n\n return [{ branch, id, label: displayLabel(id) }]\n })\n}\n\n/**\n * Resolve an operator-supplied release ref (version `1.2.3` / `v1.2.3` or name\n * `checkout-redesign`) to its branch name (`release/v1.2.3` | `release/<name>`).\n * Strict: surfaces a parse failure as an OperationError with remediation text.\n */\nexport const resolveReleaseBranch = (versionArg: string): string => {\n try {\n return formatBranchName(parseReleaseRef(versionArg))\n } catch (error) {\n throw new OperationError(error, {\n operation: `resolve release ref \"${versionArg}\"`,\n remediation: 'pass a version (e.g. \"1.2.5\") or a release name (e.g. \"checkout-redesign\")',\n })\n }\n}\n\n/**\n * Render the human display label for a release branch. Returns the `dev`\n * sentinel unchanged; otherwise derives `1.2.3` | `<name>` from the branch.\n * Falls back to the raw branch when it does not parse as a release id.\n */\nexport const releaseLabelFromBranch = (branch: string): string => {\n if (branch === DEV_REF) return DEV_REF\n\n const id = parseBranchName(branch)\n\n return id ? displayLabel(id) : branch\n}\n\n/**\n * Render human display labels for a list of release branches, dropping any\n * branch that does not parse as a release id (lenient discovery contract).\n */\nexport const releaseBranchLabels = (branches: string[]): string[] => {\n return branches.flatMap((branch) => {\n const id = parseBranchName(branch)\n\n return id ? [displayLabel(id)] : []\n })\n}\n\nconst deriveBranchType = (branch: string, types?: Map<string, ReleaseType>): ReleaseType | undefined => {\n return types ? types.get(branch) || 'regular' : undefined\n}\n\n/**\n * Format release branch names as Ink picker items. Labels and descriptions\n * are returned as separate fields \u2014 the picker renders and pads them itself.\n */\nexport const formatBranchPickerItems = (args: FormatBranchChoicesArgs): BranchPickerItem[] => {\n const { branches, descriptions, types } = args\n\n return parseBranchChoices(branches).map(({ branch, id, label }) => {\n return {\n value: branch,\n label,\n // Jira-descriptions map is keyed by the Jira version NAME (`v1.2.3` | `<name>`).\n description: descriptions.get(formatJiraName(id)),\n type: deriveBranchType(branch, types),\n }\n })\n}\n", "import { $ } from 'zx'\n\ntype PRState = 'OPEN' | 'MERGED' | 'CLOSED'\n\nexport interface PRStatus {\n number: number\n state: PRState\n title: string\n}\n\n/**\n * Fetch the (most-recent) PR for the given head branch, across all states, so\n * we can resume a partially-completed delivery: a PR merged on a prior attempt\n * still appears here as `state: 'MERGED'`, letting the caller skip the merge.\n */\nexport const fetchPRByHead = async (head: string): Promise<PRStatus | null> => {\n const result = await $`gh pr list --head ${head} --state all --json number,state,title --limit 1`\n const prs = JSON.parse(result.stdout) as PRStatus[]\n\n return prs[0] ?? null\n}\n", "import process from 'node:process'\n\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { PromptCancelledError } from 'src/lib/errors/prompt-cancelled-error'\nimport { jsonOutput } from 'src/lib/json-output'\nimport { isMcpMode } from 'src/lib/mcp-mode'\n\nimport type { BranchPickerItem } from './types'\n\n/**\n * Gate the interactive Ink picker. Throws a clear `OperationError`, without ever loading React,\n * when there is no interactive input (`stdin.isTTY`), when running under MCP, or when structured\n * output is requested (`jsonOutput.enabled`).\n */\n// All three clauses are load-bearing.\n//\n// `--json` is registered on every command and sets `jsonOutput.enabled`, so `--json` on a TTY\n// without an explicit arg would otherwise pass a TTY-only gate and dynamically import the TUI,\n// leaking React onto the `--json` path.\n//\n// The TTY predicate is `stdin.isTTY` only \u2014 deliberately NOT the palette's `stdout && stdin`.\n// These prompts render to stderr, so an interactive run whose STDOUT is redirected (e.g.\n// `worktrees-remove > log.txt`) must still prompt; requiring `stdout.isTTY` would regress that.\n// Input-side interactivity is what the picker actually needs.\n//\n// The MCP clause is not defensive: `worktrees-add` and `gh-merge-dev` are `mcpExposed: true` with\n// ALL of their branch inputs `.optional()`, so an MCP call with those args omitted DOES enter the\n// interactive branch, and without a guard it would render an Ink picker into the JSON-RPC stream\n// (and load React there). It keys on `isMcpMode()`, NOT on `!process.stdin.isTTY`, because the TTY\n// check alone does not hold: `commands/mcp/mcp.ts` spawns the server with `stdio: 'inherit'`, so a\n// terminal-launched `infra-kit mcp` hands the child a real TTY stdin and an isTTY-keyed guard does\n// NOT fire. It only looks sufficient because Claude Code happens to spawn the server with piped\n// stdio \u2014 a property of one client, not a guarantee. The `!isTTY` clause stays as well, for\n// genuinely non-interactive runs (pipes, CI).\nconst assertInteractive = () => {\n if (isMcpMode() || !process.stdin.isTTY || jsonOutput.enabled) {\n throw new OperationError(undefined, {\n operation: 'interactive branch selection',\n remediation:\n 'pass the branch selection explicitly (CLI: `--version`/`--versions`/`--all`; MCP: the `version`/`versions`/`all` fields) for non-interactive, --json, or MCP runs',\n })\n }\n}\n\n/**\n * Prompt for a single release branch via the searchable Ink picker. Throws an\n * `OperationError` on non-interactive/`--json`/MCP runs (see `assertInteractive`)\n * and a `PromptCancelledError` if the user backs out with Esc/Ctrl-C. The Ink\n * TUI is reached only through a dynamic import so React never loads on the\n * bail paths.\n */\nexport const pickReleaseBranch = async (items: BranchPickerItem[]): Promise<string> => {\n assertInteractive()\n const { runBranchPicker } = await import('src/tui/boot')\n const value = await runBranchPicker(items)\n\n if (value === null) throw new PromptCancelledError()\n\n return value\n}\n\n/**\n * Prompt for one or more release branches via the searchable Ink multi-picker.\n * Same guard and cancellation semantics as {@link pickReleaseBranch}; returns\n * the selected values.\n */\nexport const pickReleaseBranches = async (\n items: BranchPickerItem[],\n opts?: { required?: boolean; allowSelectAll?: boolean },\n): Promise<string[]> => {\n assertInteractive()\n const { runBranchMultiPicker } = await import('src/tui/boot')\n const values = await runBranchMultiPicker(items, opts)\n\n if (values === null) throw new PromptCancelledError()\n\n return values\n}\n", "/**\n * Thrown by the Ink branch pickers (via the `release-picker` shim) when the user\n * presses Esc or Ctrl-C.\n *\n * The name is honest \u2014 `PromptCancelledError`, not a borrowed `@inquirer/core`\n * class name. It is registered in `CANCELLATION_ERROR_NAMES` so\n * {@link ./is-prompt-cancellation.isPromptCancellation} recognises it through the\n * same by-name mechanism it already uses for the `@inquirer` cancel errors. That\n * keeps the contract honest if `@inquirer` is ever dropped, and survives anyone\n * adding an `instanceof` fast-path for the real inquirer classes.\n *\n * @example\n * try {\n * await pickReleaseBranch(items)\n * } catch (err) {\n * if (isPromptCancellation(err)) process.exit(0) // user backed out\n * throw err\n * }\n */\nexport class PromptCancelledError extends Error {\n override name = 'PromptCancelledError'\n\n constructor() {\n super('Prompt cancelled')\n }\n}\n", "import type { Command } from 'commander'\nimport process from 'node:process'\n\n/**\n * `--json` output mode for the CLI. This is a presentation concern only: every\n * command handler already returns a `structuredContent` payload (the same one\n * the MCP surface consumes). Human/log output goes to stderr (see lib/logger),\n * so writing the structured payload to stdout never collides with it.\n */\n\nexport interface CommandResult {\n structuredContent?: unknown\n}\n\n/** Mutable holder (object so `prefer-const` holds while the flag toggles per run). */\nexport const jsonOutput = { enabled: false }\n\n/**\n * In `--json` mode, write a command result's `structuredContent` to stdout as\n * pretty JSON and return the result unchanged. Outside `--json` mode it is a\n * no-op pass-through. The writer is injectable for testing.\n */\nexport const emit = <T extends CommandResult | void>(\n result: T,\n write: (text: string) => void = (text) => {\n process.stdout.write(text)\n },\n): T => {\n if (jsonOutput.enabled && result && result.structuredContent != null) {\n write(`${JSON.stringify(result.structuredContent, null, 2)}\\n`)\n }\n\n return result\n}\n\n/** Register `--json` on a command and every nested subcommand (idempotent). */\nexport const addJsonOption = (cmd: Command): void => {\n const alreadyHasJson = cmd.options.some((option) => {\n return option.long === '--json'\n })\n\n if (!alreadyHasJson) {\n cmd.option('--json', 'Output the structured result as JSON on stdout (human logs stay on stderr)')\n }\n\n cmd.commands.forEach(addJsonOption)\n}\n", "import { $ } from 'zx'\n\nimport {\n assertPristineWorktree,\n hasMergeInProgress,\n isAncestor,\n resetScratchWorktree,\n revParseVerify,\n} from 'src/lib/git-utils'\n\n/**\n * Terminal state of one release branch in a merge-dev run.\n *\n * `up-to-date` and `fast-forward` are successes, not degenerate cases: an\n * already-merged branch is exactly what a re-run should report, and that is what\n * makes the whole command idempotent.\n */\nexport type MergeStatus =\n 'up-to-date' | 'fast-forward' | 'merged' | 'conflict' | 'hook-failed' | 'error' | 'not-attempted'\n\nexport interface MergePlanEntry {\n branch: string\n status: MergeStatus\n /** The merge commit to push. Absent for every non-success status. */\n mergeSha?: string\n /** Conflicted paths, for the report \u2014 never a diff, just names. */\n conflictPaths?: string[]\n /** git's own words, trimmed. The report quotes rather than paraphrases. */\n reason?: string\n /** Wall-clock for this branch, so a slow one is visible in the transcript. */\n durationMs?: number\n}\n\nconst MERGE_MESSAGE_PREFIX = \"Merge remote-tracking branch 'origin/dev' into \"\n\nconst stderrOf = (error: unknown): string => {\n return String((error as { stderr?: string }).stderr ?? (error as { message?: string }).message ?? error).trim()\n}\n\n/**\n * Conflicted paths, by name only. `--diff-filter=U` is the authoritative list;\n * parsing merge output would be a second, weaker source of the same truth.\n */\nconst conflictedPaths = async (cwd: string): Promise<string[]> => {\n try {\n const result = await $({ cwd, quiet: true })`git diff --name-only --diff-filter=U`\n\n return result.stdout\n .split('\\n')\n .map((line) => {\n return line.trim()\n })\n .filter((line) => {\n return line.length > 0\n })\n } catch {\n return []\n }\n}\n\n/**\n * Classify a failed `git merge` as `conflict`, `hook-failed` or `error`.\n *\n * Both discriminators are structural, never textual: unmerged paths mean `conflict`, and\n * `MERGE_HEAD` with zero unmerged paths means the merge resolved and was then refused at commit\n * time. git's stderr is carried as `reason` \u2014 reported to the operator, never branched on.\n */\n// Why not the exit code, and why not a `/hook/i` match on the message:\n//\n// A failing `commit-msg` / `pre-merge-commit` hook fails the merge without producing unmerged\n// paths, so the exit code cannot separate the two \u2014 and keeping them apart matters in practice,\n// because the scratch worktree starts cold and a hook shelling into `node_modules` can fail here\n// where the operator's own merge succeeds. That is a false failure produced *by* the fidelity of\n// using real `git merge`; filing it as `conflict` sends the operator hunting for a conflict that\n// does not exist.\n//\n// Measured: when a `commit-msg` hook rejects the merge, git exits 1 and writes the hook's own\n// stderr followed by `Not committing merge; use 'git commit' to complete the merge.` \u2014 the word\n// \"hook\" appears nowhere in git's own output. Matching on the text therefore only works when the\n// hook happens to mention itself, and a hook that says \"policy violation\" would land in `error`.\n// `reason` still carries that stderr because it holds the hook's actual complaint, which is the\n// only thing that tells the operator what to fix.\nconst classifyMergeFailure = async (\n cwd: string,\n error: unknown,\n): Promise<{ status: MergeStatus; conflictPaths?: string[]; reason: string }> => {\n const reason = stderrOf(error)\n const paths = await conflictedPaths(cwd)\n\n if (paths.length > 0) return { status: 'conflict', conflictPaths: paths, reason }\n\n // Merged cleanly, then refused at commit time \u2014 a hook. The scratch worktree\n // starts cold, so a hook shelling into `node_modules` fails here where the\n // operator's own merge succeeds; naming it precisely is what stops them\n // hunting for a conflict that does not exist.\n if (await hasMergeInProgress(cwd)) return { status: 'hook-failed', reason }\n\n return { status: 'error', reason }\n}\n\nexport interface PlanMergeRunArgs {\n /** Main repo root \u2014 where refs are resolved. */\n cwd: string\n /** Scratch detached worktree; merges happen here and nowhere else. */\n worktreePath: string\n branches: string[]\n}\n\n/**\n * Merge `origin/dev` into each branch inside the scratch worktree, collecting a\n * sha per success \u2014 and pushing nothing.\n *\n * Producing the plan and performing the merge are the SAME operation on purpose.\n * Any design where one engine predicts and another applies can ship a preflight\n * that disagrees with the artifact; here the thing shown to the operator is\n * literally the thing that will be pushed.\n *\n * Per-branch failures are values, never throws: one branch's conflict must not\n * change another branch's result. The `finally` around each iteration is what\n * makes that true \u2014 without it a conflicted merge left in place makes the *next*\n * `checkout --detach` fail with `error: you need to resolve your current index\n * first`, and branches k+1\u2026N report an error that branch k caused.\n */\nexport const planMergeRun = async (args: PlanMergeRunArgs): Promise<MergePlanEntry[]> => {\n const { cwd, worktreePath, branches } = args\n const entries: MergePlanEntry[] = []\n\n const devSha = await revParseVerify(cwd, 'origin/dev')\n\n if (!devSha) {\n return branches.map((branch) => {\n return { branch, status: 'error' as const, reason: 'origin/dev does not resolve' }\n })\n }\n\n for (const branch of branches) {\n const startedAt = Date.now()\n const entry = await planOne({ cwd, worktreePath, branch, devSha })\n\n entries.push({ ...entry, durationMs: Date.now() - startedAt })\n }\n\n return entries\n}\n\nconst planOne = async (args: {\n cwd: string\n worktreePath: string\n branch: string\n devSha: string\n}): Promise<MergePlanEntry> => {\n const { cwd, worktreePath, branch, devSha } = args\n const remoteRef = `origin/${branch}`\n\n // Must precede is-ancestor: `merge-base --is-ancestor` exits 128 on an unknown\n // ref, which would abort the whole run rather than failing this one branch.\n if (!(await revParseVerify(cwd, remoteRef))) {\n return { branch, status: 'error', reason: `${remoteRef} does not exist on origin` }\n }\n\n if (await isAncestor(cwd, 'origin/dev', remoteRef)) {\n return { branch, status: 'up-to-date' }\n }\n\n try {\n await assertPristineWorktree(worktreePath)\n\n await $({ cwd: worktreePath, quiet: true })`git checkout --detach ${remoteRef}`\n\n // `-m` rather than bare `--no-edit`: a merge on a detached HEAD writes\n // \"\u2026 into HEAD\", which would differ from the operator's own\n // `git switch <B> && git merge origin/dev` \u2014 and reproducibility by a command\n // they already know is the reason this engine was chosen.\n const message = `${MERGE_MESSAGE_PREFIX}${branch}`\n\n await $({ cwd: worktreePath, quiet: true })`git merge origin/dev --no-edit -m ${message}`\n\n const mergeSha = (await $({ cwd: worktreePath, quiet: true })`git rev-parse HEAD`).stdout.trim()\n\n // A branch strictly behind dev fast-forwards: no merge commit is created, and\n // HEAD lands exactly on dev. Reported distinctly because the operator sees a\n // different history shape, not because it is handled differently.\n return { branch, status: mergeSha === devSha ? 'fast-forward' : 'merged', mergeSha }\n } catch (error) {\n return { branch, ...(await classifyMergeFailure(worktreePath, error)) }\n } finally {\n await resetScratchWorktree(worktreePath)\n }\n}\n\n/** The branches a plan would actually push, in plan order. */\nexport const pushableRefs = (entries: MergePlanEntry[]): { branch: string; sha: string }[] => {\n return entries.flatMap((entry) => {\n return entry.mergeSha ? [{ branch: entry.branch, sha: entry.mergeSha }] : []\n })\n}\n\n/** Bare `--verify`: the cheap tier, and the one an operator will actually leave on. */\nexport const DEFAULT_VERIFY_COMMAND = 'pnpm install --frozen-lockfile'\n\nexport interface VerifyOutcome {\n /** Refs that passed and may still be pushed, in plan order. */\n kept: { branch: string; sha: string }[]\n /** branch \u2192 the verification failure, for the report. */\n failed: Map<string, string>\n}\n\n/**\n * Check out each collected merge and run the verification command against it,\n * dropping any branch that fails.\n *\n * **Pre-push, always**, and the command comes from the caller rather than from repo config.\n */\n// Pre-push is not an ordering preference: a failed verification must never lead to rolling back a\n// PUSHED merge, because rewinding a shared ref is the one thing this command refuses to do.\n// Verifying first makes the question moot rather than answered.\n//\n// Why the default tier is `pnpm install --frozen-lockfile` rather than a test suite: the\n// characteristic breakage of a `dev` \u2192 release merge in a pnpm monorepo is `pnpm-lock.yaml` \u2014\n// both sides touched it, the text merge is clean, and the result is semantically broken. No merge\n// engine reports anything, because it is not a conflict. That check is fast and cannot flake on\n// timing, which is what makes it a tier an operator leaves switched on.\n//\n// Caller-supplied, not config-read: this CLI's config file is a strict schema, so a new key could\n// not be adopted by consumer repos without a schema change and a published release. Configuring\n// at the call site also keeps the plan's rule \u2014 never invoke a repo's root `qa` by name \u2014 a\n// matter of what the operator types.\nexport const verifyMerges = async (args: {\n worktreePath: string\n refs: { branch: string; sha: string }[]\n command: string\n}): Promise<VerifyOutcome> => {\n const { worktreePath, refs, command } = args\n const kept: { branch: string; sha: string }[] = []\n const failed = new Map<string, string>()\n\n for (const ref of refs) {\n try {\n await assertPristineWorktree(worktreePath)\n await $({ cwd: worktreePath, quiet: true })`git checkout --detach ${ref.sha}`\n await $({ cwd: worktreePath, quiet: true })`sh -c ${command}`\n\n kept.push(ref)\n } catch (error) {\n failed.set(ref.branch, stderrOf(error))\n } finally {\n await resetScratchWorktree(worktreePath)\n }\n }\n\n return { kept, failed }\n}\n\nexport interface ReclassifyResult {\n /** Still genuinely needing a push, in plan order. */\n kept: { branch: string; sha: string }[]\n /** Branches someone else merged while we were planning. */\n nowUpToDate: string[]\n}\n\n/**\n * Re-check the plan against origin immediately before pushing, dropping branches\n * that no longer need it.\n *\n * Mandatory rather than defensive. Cheap by construction \u2014 one fetch plus one `is-ancestor` per\n * branch, no checkout \u2014 so it is safe to run twice when a long `--verify` pass sits between the\n * plan and the push.\n */\n// Two independent reasons it is mandatory:\n//\n// 1. Two operators planning against the same `origin/dev` produce the same tree but different\n// shas (the committer timestamp differs), so the second push is rejected on a branch that is\n// in fact correctly merged. Reporting that as a failure on a daily command manufactures\n// recurring false alarms.\n// 2. Under `--atomic` it is worse than a false alarm: one teammate's hand-merge would abort the\n// push for *every* branch in the run. `--atomic` without this step is a downgrade, not an\n// upgrade.\nexport const reclassify = async (cwd: string, refs: { branch: string; sha: string }[]): Promise<ReclassifyResult> => {\n await $({ cwd, quiet: true })`git fetch origin --prune`\n\n const kept: { branch: string; sha: string }[] = []\n const nowUpToDate: string[] = []\n\n for (const ref of refs) {\n if (await isAncestor(cwd, 'origin/dev', `origin/${ref.branch}`)) {\n nowUpToDate.push(ref.branch)\n } else {\n kept.push(ref)\n }\n }\n\n return { kept, nowUpToDate }\n}\n", "import process from 'node:process'\n\nimport { exitCodeForSignal } from 'src/dev/signal-shutdown'\nimport { logger } from 'src/lib/logger'\n\n/**\n * Resources a run has acquired that must be released even when the process is\n * terminated rather than unwound.\n *\n * The scratch worktree is the motivating case: it is created before the operator\n * is prompted and removed in a `finally`, and a `finally` does not run when Node\n * takes the default action for SIGINT. A leaked registration is not cosmetic \u2014\n * the next run's `worktree add` fails with `is a missing but already registered\n * worktree`, so the command stays broken on that machine until someone knows to\n * run `git worktree prune`.\n */\nconst pending = new Set<() => Promise<void>>()\n\n/** Register a release action for the duration of a run. Returns its unregister. */\nexport const registerRunCleanup = (release: () => Promise<void>): (() => void) => {\n pending.add(release)\n\n return (): void => {\n pending.delete(release)\n }\n}\n\n/** Run every pending release, swallowing failures \u2014 this executes on the way out. */\nconst releaseAll = async (): Promise<void> => {\n for (const release of [...pending]) {\n try {\n await release()\n } catch (error) {\n logger.warn({ error, msg: 'cleanup failed during shutdown' })\n }\n }\n\n pending.clear()\n}\n\nconst GUARDED_SIGNALS: NodeJS.Signals[] = ['SIGINT', 'SIGTERM']\n\nexport interface RunCleanupDeps {\n register?: (signal: NodeJS.Signals, handler: () => void) => void\n unregister?: (signal: NodeJS.Signals, handler: () => void) => void\n exit?: (code: number) => void\n}\n\n/**\n * Run `fn` with a SIGINT/SIGTERM guard that releases registered resources and\n * exits `128 + signo`, then always detaches the guard.\n *\n * **Install this on the CLI path ONLY**, never under MCP. `exit` is injectable purely as a test\n * seam, so a unit test can assert 130/143 without killing the runner; it is not a second\n * production mode.\n */\n// Why CLI-only: the precedent this copies (`installBootSignalGuard`) lives at an entry point,\n// which owns the process. `ghMergeDev` does not \u2014 it is also the MCP tool handler inside a\n// long-lived server, where a per-invocation handler calling `process.exit(130)` would preempt the\n// host's shutdown semantics and, on a stray signal, take the whole server down mid-session.\n//\n// Under MCP nothing is installed and ordinary unwinding suffices, because neither non-unwinding\n// exit exists there: the boundary always injects `confirmedCommand: true`, so the decline branch\n// is unreachable, and the server owns signal handling.\nexport const withRunCleanup = async <T>(fn: () => Promise<T>, deps: RunCleanupDeps = {}): Promise<T> => {\n const {\n register = (signal, handler): void => {\n process.on(signal, handler)\n },\n unregister = (signal, handler): void => {\n process.off(signal, handler)\n },\n exit = (code: number): void => {\n process.exit(code)\n },\n } = deps\n\n const handlers = new Map<NodeJS.Signals, () => void>()\n\n for (const signal of GUARDED_SIGNALS) {\n const handler = (): void => {\n void releaseAll().then(() => {\n exit(exitCodeForSignal(signal))\n })\n }\n\n handlers.set(signal, handler)\n register(signal, handler)\n }\n\n try {\n return await fn()\n } finally {\n for (const [signal, handler] of handlers) {\n unregister(signal, handler)\n }\n }\n}\n", "import { z } from 'zod'\nimport { $ } from 'zx'\n\nimport { fetchPRByHead, getReleasePRsWithInfo } from 'src/integrations/gh'\nimport type { PRStatus } from 'src/integrations/gh'\nimport { deliverJiraRelease, loadJiraConfigOptional } from 'src/integrations/jira'\n// Imported from the leaf module, not the `src/integrations/jira` barrel, and deliberately so:\n// `isJiraApiError` is a pure `instanceof` guard with no I/O. Tests mock that barrel to stub NETWORK\n// calls, and a partial mock drops every export it does not name \u2014 so a barrel import both broke\n// those tests and, worse, would have silently sent a mocked run down the wrong branch.\nimport { isJiraApiError } from 'src/integrations/jira/jira-api-error'\nimport { commandEcho, confirmOrExit } from 'src/lib/command-echo'\nimport { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'\nimport { formatZxError } from 'src/lib/errors/format-zx-error'\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { assertManagementContext } from 'src/lib/git-guard'\nimport { deleteLocalBranch, deleteRemoteBranch, getProjectRoot } from 'src/lib/git-utils'\nimport { logger } from 'src/lib/logger'\nimport { pickReleaseBranch } from 'src/lib/prompts/release-picker'\nimport { displayLabel, formatJiraName, formatRcTitle, parseBranchName } from 'src/lib/release-id'\nimport type { ReleaseId } from 'src/lib/release-id'\nimport {\n detectReleaseType,\n formatBranchPickerItems,\n getJiraDescriptions,\n resolveReleaseBranch,\n} from 'src/lib/release-utils'\nimport type { ReleaseType } from 'src/lib/release-utils'\nimport { removeReleaseWorktreeIfPresent } from 'src/lib/worktrees'\nimport { defineMcpTool, textContent } from 'src/types'\nimport type { RequiredConfirmedOptionArg } from 'src/types'\n\ninterface GhReleaseDeliverArgs extends RequiredConfirmedOptionArg {\n version: string\n}\n\n/**\n * Wrap a delivery step so its failure logs structured zx fields and surfaces\n * an `OperationError` whose message names the actual step that failed \u2014\n * instead of the previous blanket \"merging release branch into dev\" message.\n */\nconst runStep = async <T>(operation: string, remediation: string, fn: () => Promise<T>): Promise<T> => {\n try {\n return await fn()\n } catch (error) {\n // `debug`, not `error`: this rethrows as an OperationError, and `entry/cli.ts` logs any\n // uncaught error at ERROR and exits 1 \u2014 so logging here too printed one fault as two red\n // lines. Kept (demoted, not deleted) because the wrapped message renders only the operation\n // and remediation; the cause's stack survives here and is reachable with `--debug`.\n logger.debug({ err: formatZxError(error) }, `Failed to ${operation}`)\n throw new OperationError(error, { operation, remediation })\n }\n}\n\n/**\n * Find a MERGED RC PR (dev \u2192 main) whose title matches the given version, used\n * to detect that a prior delivery run already merged the RC and we should skip\n * the merge step on resume. Title-matched on purpose: an older MERGED RC PR\n * from a different release must not short-circuit this version's flow.\n */\nconst fetchMergedRcPRForVersion = async (id: ReleaseId): Promise<PRStatus | null> => {\n const expectedTitle = formatRcTitle(id)\n const result = await $`gh pr list --head dev --base main --state merged --json number,state,title --limit 20`\n const prs = JSON.parse(result.stdout) as PRStatus[]\n const match = prs.find((pr) => {\n return pr.title === expectedTitle\n })\n\n return match ?? null\n}\n\n/**\n * Find an open dev \u2192 main PR, if any. GitHub allows at most one open PR per\n * head/base pair, so an existing open PR here is the RC PR for this release \u2014\n * even if its title was set by a previous (failed) delivery run. Adopting it\n * is what makes the flow recoverable after a mid-run failure.\n */\nconst fetchOpenDevToMainPR = async (): Promise<PRStatus | null> => {\n const result = await $`gh pr list --head dev --base main --state open --json number,state,title --limit 5`\n const prs = JSON.parse(result.stdout) as PRStatus[]\n\n return prs[0] ?? null\n}\n\ninterface ResolvedTarget {\n selectedReleaseBranch: string\n releasePrTitle: string\n}\n\nconst resolveTargetFromVersion = async (version: string): Promise<ResolvedTarget> => {\n const selectedReleaseBranch = resolveReleaseBranch(version)\n const pr = await fetchPRByHead(selectedReleaseBranch)\n\n if (!pr) {\n logger.error(`\u274C No PR found for branch ${selectedReleaseBranch}.`)\n throw new OperationError(undefined, {\n operation: `deliver release ${selectedReleaseBranch}`,\n remediation: `confirm a PR exists ('gh pr list --head ${selectedReleaseBranch} --state all')`,\n })\n }\n\n return { selectedReleaseBranch, releasePrTitle: pr.title }\n}\n\nconst resolveTargetInteractively = async (): Promise<ResolvedTarget> => {\n const releasePRsInfo = await getReleasePRsWithInfo()\n\n const branches = releasePRsInfo.map((pr) => {\n return pr.branch\n })\n\n const releaseTypes = new Map<string, ReleaseType>(\n releasePRsInfo.map((pr) => {\n return [pr.branch, detectReleaseType(pr.title)]\n }),\n )\n\n commandEcho.setInteractive()\n\n const descriptions = await getJiraDescriptions()\n\n const selectedReleaseBranch = await pickReleaseBranch(\n formatBranchPickerItems({ branches, descriptions, types: releaseTypes }),\n )\n\n const prInfo = releasePRsInfo.find((pr) => {\n return pr.branch === selectedReleaseBranch\n })\n\n if (!prInfo) {\n logger.error(`\u274C Release branch ${selectedReleaseBranch} not found in open PRs.`)\n throw new OperationError(undefined, {\n operation: `deliver release ${selectedReleaseBranch}`,\n remediation: `confirm an open PR exists for ${selectedReleaseBranch} ('gh pr list')`,\n })\n }\n\n return { selectedReleaseBranch, releasePrTitle: prInfo.title }\n}\n\ninterface MergeReleasePRArgs {\n selectedReleaseBranch: string\n releaseType: ReleaseType\n}\n\nconst mergeReleasePR = async (args: MergeReleasePRArgs): Promise<void> => {\n const { selectedReleaseBranch, releaseType } = args\n\n const mergeTarget = releaseType === 'hotfix' ? 'main' : 'dev'\n\n const releasePr = await fetchPRByHead(selectedReleaseBranch)\n\n if (!releasePr) {\n throw new OperationError(undefined, {\n operation: `look up release PR for ${selectedReleaseBranch}`,\n remediation: `verify the PR exists in GitHub`,\n })\n }\n\n if (releasePr.state === 'MERGED') {\n logger.info(`\u2713 Release PR ${selectedReleaseBranch} already merged \u2014 skipping`)\n\n return\n }\n\n if (releasePr.state === 'CLOSED') {\n throw new OperationError(undefined, {\n operation: `merge release PR ${selectedReleaseBranch} into ${mergeTarget}`,\n remediation: `the PR is closed without merge; reopen it or create a new release`,\n })\n }\n\n await runStep(\n `merge release PR ${selectedReleaseBranch} into ${mergeTarget}`,\n `check 'gh pr view ${selectedReleaseBranch}' for mergeability and required reviews`,\n async () => {\n await $`gh pr merge ${selectedReleaseBranch} --squash --admin --delete-branch`\n },\n )\n}\n\nconst resolveRcPRNumber = async (id: ReleaseId): Promise<number> => {\n const selectedLabel = displayLabel(id)\n const expectedTitle = formatRcTitle(id)\n const existingOpen = await fetchOpenDevToMainPR()\n\n // Adopt any existing open dev\u2192main PR. GitHub permits only one open PR per\n // head/base pair, so a stale open RC PR (left behind by a prior failed run\n // \u2014 the single most common cause of \"Error merging release branch into\n // dev\") blocks `gh pr create`. Retitle it instead of fighting it.\n if (existingOpen) {\n const rcNumber = existingOpen.number\n\n if (existingOpen.title !== expectedTitle) {\n logger.info(\n `Adopting open dev \u2192 main PR #${rcNumber} (\"${existingOpen.title}\") and retitling for ${selectedLabel}`,\n )\n await runStep(\n `retitle dev \u2192 main PR #${rcNumber} to \"${expectedTitle}\"`,\n `update manually: gh pr edit ${rcNumber} --title \"${expectedTitle}\"`,\n async () => {\n await $`gh pr edit ${rcNumber} --title ${expectedTitle}`\n },\n )\n }\n\n return rcNumber\n }\n\n await runStep(\n `create RC PR (dev \u2192 main) for ${selectedLabel}`,\n `run 'gh pr create --base main --head dev' manually to surface the underlying error (e.g. no commits between dev and main)`,\n async () => {\n await $`gh pr create --base main --head dev --title ${expectedTitle} --body \"\"`\n },\n )\n\n const created = await fetchOpenDevToMainPR()\n\n if (!created) {\n throw new OperationError(undefined, {\n operation: `look up RC PR for ${selectedLabel}`,\n remediation: `verify the RC PR was created ('gh pr list --head dev --base main')`,\n })\n }\n\n return created.number\n}\n\nconst ensureRcPRMerged = async (id: ReleaseId): Promise<void> => {\n const selectedLabel = displayLabel(id)\n const alreadyMerged = await fetchMergedRcPRForVersion(id)\n\n if (alreadyMerged) {\n logger.info(`\u2713 RC PR for ${selectedLabel} already merged into main \u2014 skipping`)\n\n return\n }\n\n const rcNumber = await resolveRcPRNumber(id)\n\n await runStep(\n `merge RC PR #${rcNumber} (dev \u2192 main) for ${selectedLabel}`,\n `check 'gh pr view ${rcNumber}' for mergeability and required reviews`,\n async () => {\n await $`gh pr merge ${rcNumber} --squash --admin`\n },\n )\n}\n\nconst dispatchDeployWorkflow = async (): Promise<void> => {\n $.quiet = false\n\n await runStep(\n `dispatch deploy-all workflow on main`,\n `check 'gh workflow list' and that you have permission to dispatch deploy-all.yml`,\n async () => {\n await $`gh workflow run deploy-all.yml --ref main -f environment=prod`\n },\n )\n\n $.quiet = true\n}\n\nconst syncMainIntoDev = async (): Promise<void> => {\n await runStep(\n `sync main back into dev`,\n `run manually: git switch main && git pull && git switch dev && git pull && git merge main --no-edit && git push`,\n async () => {\n await $`git switch main && git pull && git switch dev && git pull && git merge main --no-edit && git push`\n },\n )\n}\n\n/**\n * Best-effort cleanup of the delivered release branch, locally and on `origin`.\n *\n * Idempotent backstop to `gh pr merge --delete-branch` (in `mergeReleasePR`):\n * that flag is skipped when the release PR was already merged on a prior\n * attempt, so an idempotent re-run would otherwise leave the branch behind.\n * Each delete is isolated \u2014 a failure (e.g. the branch checked out in a stray\n * worktree, or a remote hiccup) logs a warning naming the branch and never\n * aborts delivery, since the irreversible merge and deploy already happened.\n */\nconst removeDeliveredReleaseBranch = async (branch: string): Promise<void> => {\n try {\n await deleteLocalBranch(branch)\n } catch (error) {\n logger.warn({ err: formatZxError(error) }, `Failed to delete local branch ${branch} (non-blocking)`)\n }\n\n try {\n await deleteRemoteBranch(branch)\n } catch (error) {\n logger.warn({ err: formatZxError(error) }, `Failed to delete remote branch ${branch} (non-blocking)`)\n }\n}\n\nconst deliverJiraReleaseSafely = async (id: ReleaseId): Promise<void> => {\n const jiraConfig = await loadJiraConfigOptional()\n\n if (!jiraConfig) {\n logger.info('\uD83D\uDD14 Jira is not configured, skipping Jira release delivery')\n\n return\n }\n\n try {\n // Jira fix version name: `v1.2.3` | `<name>` \u2014 must match create-time formatJiraName.\n const versionName = formatJiraName(id)\n\n await deliverJiraRelease({ versionName }, jiraConfig)\n } catch (error) {\n // Stays ERROR while the command continues: the residue is durable and user-visible \u2014 the fix\n // version is left unreleased in Jira after the PR has already merged and deployed, so someone\n // must go finish it by hand. Naming the credentials matters here more than anywhere: this runs\n // late in `release deliver`, long past the point where the run can be retried from the top.\n const hint =\n isJiraApiError(error) && error.kind === 'auth' ? ' \u2014 reissue the Atlassian API token and update JIRA_TOKEN' : ''\n\n logger.error({ err: formatZxError(error) }, `Failed to deliver Jira release (non-blocking)${hint}`)\n }\n}\n\n/**\n * Deliver a release branch to production. Each network/git step is run inside\n * `runStep` so the surfaced error names the failing operation and includes the\n * subprocess stderr. PR-merge steps are idempotent: if the release PR or RC PR\n * is already MERGED, the step is skipped, so re-running after a mid-flight\n * failure picks up where it stopped.\n */\nexport const ghReleaseDeliver = async (args: GhReleaseDeliverArgs) => {\n const { version, confirmedCommand } = args\n\n // Branch-agnostic (operates on release/RC PRs via gh and self-switches), so\n // only the worktree + clean-tree legs apply.\n await assertManagementContext({ operation: 'deliver release' })\n\n const { selectedReleaseBranch, releasePrTitle } = version\n ? await resolveTargetFromVersion(version)\n : await resolveTargetInteractively()\n\n // selectedReleaseBranch is always a release branch (operator ref strictly\n // parsed, or picked from discovery-filtered choices) so this cannot be null.\n const releaseId = parseBranchName(selectedReleaseBranch)\n\n if (!releaseId) {\n throw new OperationError(undefined, {\n operation: `deliver release ${selectedReleaseBranch}`,\n remediation: 'pass a version (e.g. \"1.2.5\") or a release name (e.g. \"checkout-redesign\")',\n })\n }\n\n const selectedVersion = displayLabel(releaseId)\n\n commandEcho.addOption('--version', selectedVersion)\n logger.info(`Delivering ${releaseId.kind === 'name' ? 'named release' : 'version'} ${selectedReleaseBranch}`)\n\n const releaseType: ReleaseType = detectReleaseType(releasePrTitle)\n\n await confirmOrExit(\n confirmedCommand,\n `Are you sure you want to deliver version ${selectedReleaseBranch} to production?`,\n )\n\n // Track --yes flag if confirmation was interactive (user confirmed)\n commandEcho.addOption('--yes', true)\n\n $.quiet = true\n\n // `worktreeDir` is re-derived here (rather than exposed by the helper) so the messages below can\n // name the exact path, matching this command's original wording; `removeReleaseWorktreeIfPresent`\n // recomputes it internally for the same reason it needs it for the actual `removeWorktrees` call.\n const deliverProjectRoot = await getProjectRoot()\n const deliverWorktreeDir = `${deliverProjectRoot}${WORKTREES_DIR_SUFFIX}`\n\n await removeReleaseWorktreeIfPresent(selectedReleaseBranch, {\n operation: `remove worktree for ${selectedReleaseBranch} before merge`,\n remediation: `run manually: git worktree remove ${deliverWorktreeDir}/${selectedReleaseBranch} (use --force if uncommitted changes)`,\n })\n await mergeReleasePR({ selectedReleaseBranch, releaseType })\n\n if (releaseType !== 'hotfix') {\n await ensureRcPRMerged(releaseId)\n }\n\n await dispatchDeployWorkflow()\n await syncMainIntoDev()\n await removeDeliveredReleaseBranch(selectedReleaseBranch)\n\n $.quiet = false\n\n await deliverJiraReleaseSafely(releaseId)\n\n logger.info(`Successfully delivered ${selectedReleaseBranch} to production!`)\n\n commandEcho.print()\n\n const structuredContent = {\n releaseBranch: selectedReleaseBranch,\n version: selectedVersion,\n type: releaseType,\n success: true,\n }\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n}\n\n// MCP Tool Registration\nexport const ghReleaseDeliverMcpTool = defineMcpTool({\n name: 'gh-release-deliver',\n description:\n 'Deliver a release to production. For hotfixes: squash-merges the release branch to main and dispatches the deploy-all workflow. For regular releases: squash-merges to dev, opens an RC PR, merges dev into main, dispatches the deploy-all workflow, then syncs main back to dev. Also releases the matching Jira fix version if Jira is configured. Dispatches the deploy workflow fire-and-forget \u2014 the tool returns once the workflow is accepted by GitHub, not when the deployment finishes. PR-merge steps are idempotent: re-running after a partial failure skips PRs that are already merged. Irreversible production operation: the confirmation prompt is auto-skipped for MCP calls, so the caller is responsible for gating. \"version\" is required when invoked via MCP (the picker is unreachable without a TTY).',\n inputSchema: {\n version: z\n .string()\n .describe(\n 'Accepts a release version (e.g. \"1.2.5\") OR a release name (e.g. \"checkout-redesign\") to deliver to production. Required for MCP calls.',\n ),\n },\n outputSchema: {\n releaseBranch: z.string().describe('The release branch that was delivered'),\n version: z.string().describe('The version that was delivered'),\n type: z.enum(['regular', 'hotfix']).describe('Release type'),\n success: z.boolean().describe('Whether the delivery was successful'),\n },\n handler: ghReleaseDeliver,\n})\n", "const STDERR_EXCERPT_MAX_BYTES = 500\nconst STDOUT_EXCERPT_MAX_BYTES = 200\n\nexport interface ZxErrorFields {\n exitCode?: number | null\n stderr?: string\n stdout?: string\n message?: string\n name?: string\n}\n\nconst readTrimmedString = (value: unknown, max: number): string | undefined => {\n if (typeof value !== 'string' || value.length === 0) return undefined\n\n return value.slice(-max).trim()\n}\n\n/**\n * Extract loggable fields from an error for `logger.error({ err: formatZxError(e) }, ...)`.\n *\n * pino's default `err` serializer handles `Error` subclasses but renders zx's\n * `ProcessOutput` as `{}` because its informative fields (`stderr`, `stdout`,\n * `exitCode`) are non-enumerable / on the prototype. This helper duck-types\n * those fields so subprocess failures surface in logs instead of vanishing.\n */\nexport const formatZxError = (error: unknown): ZxErrorFields => {\n if (error === null || typeof error !== 'object') {\n return { message: String(error) }\n }\n\n const rec = error as Record<string, unknown>\n const fields: ZxErrorFields = {}\n\n if (error instanceof Error) {\n fields.name = error.name\n fields.message = error.message\n } else if (typeof rec.message === 'string') {\n fields.message = rec.message\n }\n\n const exitCode = rec.exitCode\n\n if (typeof exitCode === 'number' || exitCode === null) fields.exitCode = exitCode\n\n const stderr = readTrimmedString(rec.stderr, STDERR_EXCERPT_MAX_BYTES)\n\n if (stderr) fields.stderr = stderr\n\n const stdout = readTrimmedString(rec.stdout, STDOUT_EXCERPT_MAX_BYTES)\n\n if (stdout) fields.stdout = stdout\n\n return fields\n}\n", "import { logger } from 'src/lib/logger'\n\nimport type { RemoveWorktreesResult } from './remove-worktrees'\n\n/**\n * Human-readable summary of a batch removal. A branch that was not removed is listed with its\n * reason \u2014 never folded into a \"nothing to do\" line, which is what made a failed removal look like\n * a clean no-op before.\n */\nexport const logRemovalResults = (result: RemoveWorktreesResult): void => {\n const { removed, failed } = result\n\n if (removed.length > 0) {\n logger.info('\u274C Removed worktrees:')\n\n for (const branch of removed) {\n logger.info(branch)\n }\n\n logger.info('')\n }\n\n if (failed.length > 0) {\n logger.warn('\u26A0\uFE0F Not removed:')\n\n for (const { branch, reason } of failed) {\n logger.warn(`${branch} \u2014 ${reason}`)\n }\n\n logger.warn('')\n }\n\n if (removed.length === 0 && failed.length === 0) {\n logger.info('\u2139\uFE0F No worktrees to remove')\n }\n}\n", "import { OperationError } from 'src/lib/errors/operation-error'\nimport { isMcpMode } from 'src/lib/mcp-mode'\nimport { textContent } from 'src/types'\nimport type { ToolsExecutionResult } from 'src/types'\n\nimport type { RemoveWorktreesResult } from './remove-worktrees'\n\nexport interface RemovalStructuredContent {\n removedWorktrees: string[]\n failedWorktrees: string[]\n count: number\n}\n\ninterface ToRemovalToolResultArgs {\n result: RemoveWorktreesResult\n /** Used for the CLI-path OperationError, e.g. `remove worktrees`. */\n operation: string\n}\n\n/**\n * Turn a batch removal into the command's return value, surfacing failures on whichever surface the\n * command runs on:\n *\n * - **MCP**: a thrown error is flattened by the SDK to text-only `isError` content and loses\n * `structuredContent` entirely, so a failure is *returned* with `isError: true` and a\n * schema-valid `failedWorktrees` (same pattern as the confirm gate in `tool-handler.ts`).\n * - **CLI**: an `OperationError` so the process exits non-zero and names what was not removed.\n *\n * Callers run their IDE cleanup and `commandEcho.print()` BEFORE calling this, so the successful\n * part of a batch is still reported and echoed.\n */\nexport const toRemovalToolResult = (args: ToRemovalToolResultArgs): ToolsExecutionResult<RemovalStructuredContent> => {\n const { result, operation } = args\n\n const structuredContent: RemovalStructuredContent = {\n removedWorktrees: result.removed,\n failedWorktrees: result.failed.map((failure) => {\n return failure.branch\n }),\n count: result.removed.length,\n }\n\n const content = textContent(JSON.stringify(structuredContent, null, 2))\n\n if (result.failed.length === 0) return { content, structuredContent }\n\n if (isMcpMode()) return { content, structuredContent, isError: true }\n\n const failures = result.failed\n .map((failure) => {\n return `${failure.branch}: ${failure.reason}`\n })\n .join('; ')\n\n throw new OperationError(undefined, {\n operation,\n remediation: `not removed: ${structuredContent.failedWorktrees.join(', ')} \u2014 fix the cause above and re-run`,\n stderrExcerpt: failures,\n })\n}\n", "import fs from 'node:fs/promises'\nimport { $ } from 'zx'\n\nimport { closeCmuxWorkspaceByCwd } from 'src/integrations/cmux'\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { listWorktrees } from 'src/lib/git-utils'\nimport { logger } from 'src/lib/logger'\n\nexport interface RemoveWorktreesArgs {\n branches: string[]\n worktreeDir: string\n /** Main checkout of the repo \u2014 `git worktree list` must run inside a git-recognised cwd. */\n projectRoot: string\n pruneFolder?: boolean\n /** Injected so tests never wait for the real sweep-retry delay. */\n sleep?: (ms: number) => Promise<void>\n}\n\nexport interface WorktreeRemovalFailure {\n branch: string\n reason: string\n}\n\nexport interface RemoveWorktreesResult {\n removed: string[]\n failed: WorktreeRemovalFailure[]\n}\n\ntype RemovalOutcome = { branch: string; status: 'removed' } | { branch: string; status: 'failed'; reason: string }\n\ninterface LeftoverClassification {\n kind: 'gone' | 'sweepable' | 'not-sweepable'\n entries: string[]\n}\n\n/**\n * The only things the post-failure sweep may delete. Measured 2026-09-03: a Claude Code session's\n * oh-my-claudecode SessionEnd hook re-creates `<worktree>/.omc/{state,sessions}` ~2 s after the\n * session exits, racing `git worktree remove`; Finder leaves `.DS_Store`. Anything else (a\n * committable `.omc/skills/**`, user files) makes the leftover NOT sweepable.\n */\nconst SWEEPABLE_TOP_LEVEL = new Set(['.omc', '.DS_Store'])\nconst SWEEPABLE_OMC_ENTRIES = new Set(['state', 'sessions'])\n\n/** The measured writer burst is ~2 s; one bounded retry covers a sweep that lands mid-burst. */\nconst SWEEP_RETRY_DELAY_MS = 2000\n\nconst DIRTY_TREE_REMEDIATION = \"check 'git worktree list' for the path; uncommitted changes block removal\"\n\nconst defaultSleep = (ms: number): Promise<void> => {\n return new Promise((resolve) => {\n setTimeout(resolve, ms)\n })\n}\n\nconst errnoCode = (error: unknown): string | undefined => {\n return typeof error === 'object' && error !== null ? (error as { code?: string }).code : undefined\n}\n\nconst stripTrailingSlashes = (value: string): string => {\n let end = value.length\n\n while (end > 1 && value[end - 1] === '/') end -= 1\n\n return value.slice(0, end)\n}\n\n/**\n * Canonical form for path equality. `realpath` folds macOS's `/var` \u2192 `/private/var` asymmetry;\n * when the path no longer exists (ENOENT mid-race) fall back to the raw string.\n */\nconst canonicalPath = async (value: string): Promise<string> => {\n try {\n return stripTrailingSlashes(await fs.realpath(value))\n } catch {\n return stripTrailingSlashes(value)\n }\n}\n\ninterface RegistrationArgs {\n projectRoot: string\n worktreePath: string\n}\n\n/**\n * Whether git still lists `worktreePath` as a worktree. Unknown (git itself failed) is reported as\n * `true`: the sweep must never run when git cannot be asked.\n */\nconst isWorktreeRegisteredOrUnknown = async (args: RegistrationArgs): Promise<boolean> => {\n const { projectRoot, worktreePath } = args\n\n try {\n const entries = await listWorktrees(projectRoot)\n const target = await canonicalPath(worktreePath)\n\n for (const entry of entries) {\n if ((await canonicalPath(entry.path)) === target) return true\n }\n\n return false\n } catch {\n return true\n }\n}\n\n/**\n * Two-level, name-only classification of what git left behind. Any read error other than ENOENT is\n * treated as not sweepable \u2014 the CLI never deletes what it could not inspect.\n */\nconst classifyLeftover = async (worktreePath: string): Promise<LeftoverClassification> => {\n let entries: string[]\n\n try {\n entries = await fs.readdir(worktreePath)\n } catch (error) {\n return { kind: errnoCode(error) === 'ENOENT' ? 'gone' : 'not-sweepable', entries: [] }\n }\n\n const topLevelOk = entries.every((entry) => {\n return SWEEPABLE_TOP_LEVEL.has(entry)\n })\n\n if (!topLevelOk) return { kind: 'not-sweepable', entries }\n if (!entries.includes('.omc')) return { kind: 'sweepable', entries }\n\n try {\n const omcEntries = await fs.readdir(`${worktreePath}/.omc`)\n\n const omcOk = omcEntries.every((entry) => {\n return SWEEPABLE_OMC_ENTRIES.has(entry)\n })\n\n return { kind: omcOk ? 'sweepable' : 'not-sweepable', entries }\n } catch (error) {\n return { kind: errnoCode(error) === 'ENOENT' ? 'sweepable' : 'not-sweepable', entries }\n }\n}\n\ninterface SweepArgs {\n worktreePath: string\n sleep: (ms: number) => Promise<void>\n}\n\ntype RemoveTreeOutcome = 'removed' | 'busy' | 'failed'\n\n/** One recursive removal attempt; `busy` is ENOTEMPTY \u2014 a writer got in between readdir and rmdir. */\nconst removeTree = async (worktreePath: string): Promise<RemoveTreeOutcome> => {\n try {\n await fs.rm(worktreePath, { recursive: true, force: true })\n\n return 'removed'\n } catch (error) {\n return errnoCode(error) === 'ENOTEMPTY' ? 'busy' : 'failed'\n }\n}\n\n/** Recursive removal with exactly one retry when a concurrent writer makes it ENOTEMPTY again. */\nconst sweepLeftover = async (args: SweepArgs): Promise<boolean> => {\n const { worktreePath, sleep } = args\n\n const first = await removeTree(worktreePath)\n\n // Only a concurrent writer earns the single retry; anything else is final.\n if (first !== 'busy') return first === 'removed'\n\n await sleep(SWEEP_RETRY_DELAY_MS)\n\n return (await removeTree(worktreePath)) === 'removed'\n}\n\ninterface RecoverArgs {\n branch: string\n worktreePath: string\n projectRoot: string\n error: unknown\n sleep: (ms: number) => Promise<void>\n}\n\nconst failedOutcome = (branch: string, error: unknown, remediation: string): RemovalOutcome => {\n const err = new OperationError(error, { operation: `remove worktree for ${branch}`, remediation })\n\n logger.error({ error, branch, msg: err.message })\n\n return { branch, status: 'failed', reason: err.message }\n}\n\n/**\n * `git worktree remove` rejected. git deletes the tree first and unregisters the worktree LAST, so a\n * rejection with the path already unregistered means git finished its own work and only a\n * post-deletion write (the measured case: a session-end hook re-creating `.omc/`) kept `rmdir` from\n * succeeding. That leftover \u2014 and only an allowlisted one \u2014 is swept here. A path git still lists\n * is a real refusal (dirty tree) and is reported as such.\n */\nconst recoverFromRejectedRemove = async (args: RecoverArgs): Promise<RemovalOutcome> => {\n const { branch, worktreePath, projectRoot, error, sleep } = args\n\n if (await isWorktreeRegisteredOrUnknown({ projectRoot, worktreePath })) {\n return failedOutcome(branch, error, DIRTY_TREE_REMEDIATION)\n }\n\n const leftover = await classifyLeftover(worktreePath)\n\n if (leftover.kind === 'gone') return { branch, status: 'removed' }\n\n const manualRemediation = `git already unregistered this worktree; inspect and remove it manually: rm -r ${worktreePath}`\n\n if (leftover.kind === 'not-sweepable') return failedOutcome(branch, error, manualRemediation)\n\n if (!(await sweepLeftover({ worktreePath, sleep }))) return failedOutcome(branch, error, manualRemediation)\n\n logger.warn(\n `\u26A0\uFE0F swept leftover ${leftover.entries.join(', ')} from ${worktreePath}: a post-exit hook (e.g. oh-my-claudecode SessionEnd) wrote into the worktree while git was deleting it; set OMC_STATE_DIR to keep tool state out of worktrees`,\n )\n\n return { branch, status: 'removed' }\n}\n\ninterface RemoveOneArgs {\n branch: string\n worktreeDir: string\n projectRoot: string\n sleep: (ms: number) => Promise<void>\n}\n\nconst removeOne = async (args: RemoveOneArgs): Promise<RemovalOutcome> => {\n const { branch, worktreeDir, projectRoot, sleep } = args\n const worktreePath = `${worktreeDir}/${branch}`\n\n // Close the cmux workspace by its cwd (the worktree path still exists here \u2014\n // close runs before `git worktree remove`). Anchors are excluded, so a group\n // header is never closed. Kept OUTSIDE the git try: a cmux failure means git never\n // ran, so it must not enter the post-git recovery below.\n try {\n await closeCmuxWorkspaceByCwd(worktreePath)\n } catch (error) {\n return failedOutcome(branch, error, 'close the cmux workspace for this worktree manually and re-run')\n }\n\n try {\n await $`git worktree remove ${worktreePath}`\n\n return { branch, status: 'removed' }\n } catch (error) {\n return recoverFromRejectedRemove({ branch, worktreePath, projectRoot, error, sleep })\n }\n}\n\n/**\n * Close any cmux workspace for each branch and run `git worktree remove`, returning the branches\n * that were removed and, separately, the ones that were not (with the reason). Failures are\n * reported, never thrown, so a single bad worktree doesn't poison a batch removal \u2014 callers decide\n * how to surface `failed` (see `toRemovalToolResult`).\n *\n * When `pruneFolder` is true and every branch was removed, also run `git worktree prune` to clear\n * stale worktree metadata. The `<repo>-worktrees` container directory and its `release/`/`feature/`\n * subfolders are deliberately left in place so the per-repo worktree scaffold persists even when\n * empty.\n */\nexport const removeWorktrees = async (args: RemoveWorktreesArgs): Promise<RemoveWorktreesResult> => {\n const { branches, worktreeDir, projectRoot, pruneFolder = false, sleep = defaultSleep } = args\n\n // removeOne never rejects (both of its steps catch), so a plain Promise.all keeps the\n // one-bad-branch-never-poisons-the-batch contract.\n const outcomes = await Promise.all(\n branches.map((branch) => {\n return removeOne({ branch, worktreeDir, projectRoot, sleep })\n }),\n )\n\n const result: RemoveWorktreesResult = { removed: [], failed: [] }\n\n for (const outcome of outcomes) {\n if (outcome.status === 'removed') {\n result.removed.push(outcome.branch)\n } else {\n result.failed.push({ branch: outcome.branch, reason: outcome.reason })\n }\n }\n\n // `git worktree remove` deletes only the leaf worktree. We intentionally leave\n // the `<repo>-worktrees` container and its `release/`/`feature/` group folders\n // in place so the worktree scaffold survives an empty state (it is recreated\n // lazily by `worktrees-add` via `mkdir -p` regardless).\n if (pruneFolder && result.removed.length === branches.length) {\n await $`git worktree prune`\n }\n\n return result\n}\n", "import { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { getCurrentWorktrees, getProjectRoot } from 'src/lib/git-utils'\n\nimport { removeWorktrees } from './remove-worktrees'\n\nexport interface RemoveReleaseWorktreeMessages {\n /** Reported operation, e.g. \"remove worktree for X before merge\" \u2014 context differs per caller. */\n operation: string\n /**\n * Reported remediation. Required, with no shared default: this repo has deliberately refused\n * `--force` on `worktrees remove` (readme.md's Worktrees row; `removeOne` runs\n * `git worktree remove` bare), so a hoisted default string here could leak a `--force`\n * suggestion into a caller \u2014 such as a teardown flow \u2014 that must never offer it. Requiring both\n * fields forces every caller, present and future, to state its own.\n */\n remediation: string\n}\n\n/**\n * `gh pr merge --delete-branch` also deletes the local branch, which fails if a\n * worktree has it checked out (the actual root cause of the \"Failed to merge\n * release PR\" surface error). Pre-remove any worktree for the release branch\n * so the local delete can succeed.\n *\n * Returns the branches actually removed (empty when none were present), because\n * `removeIdeWorktreeFolders` treats an empty `removedWorktrees` list as \"nothing to clean up\"\n * and returns immediately \u2014 a `void`-returning helper would make that step a silent no-op for\n * a caller chaining IDE-folder cleanup off this result.\n */\nexport const removeReleaseWorktreeIfPresent = async (\n releaseBranch: string,\n messages: RemoveReleaseWorktreeMessages,\n): Promise<string[]> => {\n const worktreeBranches = await getCurrentWorktrees('release')\n\n if (!worktreeBranches.includes(releaseBranch)) return []\n\n const projectRoot = await getProjectRoot()\n const worktreeDir = `${projectRoot}${WORKTREES_DIR_SUFFIX}`\n\n const { removed, failed } = await removeWorktrees({ branches: [releaseBranch], worktreeDir, projectRoot })\n\n // Check membership, not emptiness: `removed` is one of two result lists now.\n if (!removed.includes(releaseBranch)) {\n const failure = failed.find((entry) => {\n return entry.branch === releaseBranch\n })\n\n throw new OperationError(undefined, { ...messages, stderrExcerpt: failure?.reason })\n }\n\n return removed\n}\n", "import { z } from 'zod'\nimport { $ } from 'zx'\n\nimport { commandEcho } from 'src/lib/command-echo'\nimport { createDeployFormProvider } from 'src/lib/deploy-form'\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { logger } from 'src/lib/logger'\nimport { pickEnv } from 'src/lib/prompts/env-picker'\nimport { confirmDeploy, resolveDeployBranch } from 'src/lib/release-deploy'\nimport { releaseLabelFromBranch } from 'src/lib/release-utils'\nimport {\n assertDeployable,\n deployableEnvs,\n readWorkflowEnvOptions,\n resolveProtectedEnvAccess,\n warnProtectedEnvDispatch,\n} from 'src/lib/workflow-envs'\nimport { defineMcpTool, textContent } from 'src/types'\n\n/** The workflow this command dispatches. Its own `environment` choices are the env list. */\nconst DEPLOY_ALL_WORKFLOW = 'deploy-all.yml'\n\ninterface GhReleaseDeployAllArgs {\n // Both used to be REQUIRED in the MCP schema, and these comments used to say so. PR-1 relaxed them\n // to `.optional()` so an argument form could offer real values \u2014 `narrowsArgs` only lets a form add\n // a key round 1 omitted. The guard that replaced the required field is `whenHeadless` at the picker.\n /** Omitted on the CLI offers the open release PRs; omitted over MCP offers them in a form. */\n version?: string\n /** Omitted on the CLI offers this workflow's own environments; over MCP the form offers them. */\n env?: string\n skipTerraform?: boolean\n confirmedCommand?: boolean\n}\n\n/**\n * Deploy a release branch to an environment\n */\nexport const ghReleaseDeployAll = async (args: GhReleaseDeployAllArgs) => {\n const { version, env, skipTerraform, confirmedCommand } = args\n\n const selectedReleaseBranch = await resolveDeployBranch(version)\n\n const selectedVersion = releaseLabelFromBranch(selectedReleaseBranch)\n\n commandEcho.addOption('--version', selectedVersion)\n\n // The workflow's own `environment` choices, minus the ones this project may not reach. This is\n // what `infra-kit.json \u2192 environments` used to hand-maintain in five copies: the same list, minus\n // `prod`, written out by hand and drifted (travelist's copy offered `roman` and `eliran`, whom its own\n // deploy-all.yml does not accept). Derived here from the three things that actually decide it \u2014 what\n // the workflow declares, what is delivery-shaped, and what this project's `protectedEnvs` says.\n const protectedEnvAccess = await resolveProtectedEnvAccess()\n const envOptions = deployableEnvs(await readWorkflowEnvOptions(DEPLOY_ALL_WORKFLOW), protectedEnvAccess)\n\n let selectedEnv = ''\n\n if (env) {\n selectedEnv = env\n } else {\n commandEcho.setInteractive()\n\n selectedEnv = await pickEnv(envOptions, 'launch deploy-all workflow')\n }\n\n commandEcho.addOption('--env', selectedEnv)\n\n // The one client-side veto: `prod` is delivered, not deployed ad-hoc \u2014 unless this project's\n // `protectedEnvs` says otherwise. GitHub cannot enforce either rule for us (from its side\n // `-f environment=prod` is a valid choice), so both are ours. Note what is NOT vetoed: an env absent\n // from the local YAML still dispatches, because this read is of the working tree while the run targets\n // `--ref <branch>`, and GitHub validates the choice itself.\n assertDeployable(selectedEnv, 'launch deploy-all workflow', protectedEnvAccess)\n\n // Allowed, but the reason it was ever blocked does not stop being true \u2014 and unlike the local path,\n // nothing downstream re-checks this one: GitHub holds the credentials and no job declares\n // `environment:`. So the guidance the refusal used to carry is emitted here instead.\n warnProtectedEnvDispatch({ env: selectedEnv, branch: selectedReleaseBranch })\n\n const shouldSkipTerraform = skipTerraform ?? false\n\n if (shouldSkipTerraform) {\n commandEcho.addOption('--skip-terraform', true)\n }\n\n if (!(await confirmDeploy({ confirmedCommand, branch: selectedReleaseBranch, env: selectedEnv }))) {\n logger.info('Deployment cancelled')\n\n const structuredContent = {\n releaseBranch: selectedReleaseBranch,\n version: selectedVersion,\n environment: selectedEnv,\n skipTerraformDeploy: shouldSkipTerraform,\n success: false,\n }\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n }\n\n try {\n $.quiet = true\n\n const skipTerraformFlag = shouldSkipTerraform ? ['-f', 'skip_terraform_deploy=true'] : []\n\n await $`gh workflow run deploy-all.yml --ref ${selectedReleaseBranch} -f environment=${selectedEnv} ${skipTerraformFlag}`\n\n $.quiet = false\n\n logger.info(\n `Successfully launched deploy-all workflow_dispatch for release branch: ${selectedReleaseBranch} and environment: ${selectedEnv}`,\n )\n\n commandEcho.print()\n\n const structuredContent = {\n releaseBranch: selectedReleaseBranch,\n version: selectedVersion,\n environment: selectedEnv,\n skipTerraformDeploy: shouldSkipTerraform,\n success: true,\n }\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n } catch (error: unknown) {\n logger.error({ error }, '\u274C Error launching workflow')\n throw new OperationError(error, {\n operation: 'launch deploy-all workflow',\n remediation: \"check 'gh workflow list' and that deploy-all.yml exists on the target ref\",\n })\n }\n}\n\n// MCP Tool Registration\nexport const ghReleaseDeployAllMcpTool = defineMcpTool({\n name: 'gh-release-deploy-all',\n // The first of the four live form providers. `fields` omits `services` because this tool has no\n // such argument \u2014 the two axes are the workflow FILE and the field set, and both differ across the\n // four tools (`gh-release-deploy-selected` reads a different workflow AND offers services; the\n // local pair offers no `version` at all).\n formProvider: createDeployFormProvider({\n workflowFile: DEPLOY_ALL_WORKFLOW,\n fields: ['version', 'env'],\n toolName: 'gh-release-deploy-all',\n }),\n description:\n 'Dispatch the deploy-all.yml GitHub Actions workflow to deploy every service from a release branch to the given environment. Fire-and-forget \u2014 returns once GitHub accepts the workflow_dispatch, NOT when the deployment finishes; watch the workflow run for completion status. Use gh-release-deploy-selected for a subset of services. Pass version=\"dev\" to deploy from the dev branch instead of a release branch. Omit \"version\" or \"env\" and this server offers the human a form listing the real releases and the environments this workflow declares; a client that cannot render one gets a refusal naming the missing field, never a guess.',\n requiresHumanConfirm: true,\n inputSchema: {\n // `.optional()` is what makes the form possible, not a loosening of intent. `narrowsArgs` only\n // lets the form ADD a key that round 1 omitted (`argument-form.ts`), so a required field can\n // never be form-filled. The sentence these descriptions used to carry \u2014 \"required for MCP\n // calls (interactive pickers are unavailable without a TTY)\" \u2014 was true, and the guard that\n // replaced it is `whenHeadless` at the picker itself, not a required field here.\n version: z\n .string()\n .optional()\n .describe(\n 'Accepts a release version (e.g. \"1.2.5\") OR a release name (e.g. \"checkout-redesign\") \u2014 resolves to the release/vX.Y.Z or release/<name> branch. Pass \"dev\" to deploy from the dev branch instead. Omit it to be offered the open releases.',\n ),\n env: z\n .string()\n .optional()\n .describe(\n 'Target environment name \u2014 must match an env this project may reach (e.g. \"dev\", \"renana\", \"oriana\"). Omit it to be offered the environments deploy-all.yml declares.',\n ),\n skipTerraform: z.boolean().optional().describe('Skip the terraform deployment stage.'),\n confirm: z\n .boolean()\n .optional()\n .describe('Set true to execute; omit for a dry-run gate that echoes the resolved action.'),\n },\n outputSchema: {\n releaseBranch: z.string().describe('The release branch that was deployed'),\n version: z.string().describe('The version that was deployed'),\n environment: z.string().describe('The environment deployed to'),\n skipTerraformDeploy: z.boolean().describe('Whether terraform deployment was skipped'),\n success: z.boolean().describe('Whether the deployment was successful'),\n },\n handler: ghReleaseDeployAll,\n})\n", "import { z } from 'zod'\n\nimport { getReleasePRsWithInfo } from 'src/integrations/gh'\nimport { getProjectRoot } from 'src/lib/git-utils'\nimport { logger } from 'src/lib/logger'\nimport { releaseBranchLabels } from 'src/lib/release-utils'\nimport { deployableEnvs, isSharedEnv, readWorkflowEnvOptions, resolveProtectedEnvAccess } from 'src/lib/workflow-envs'\nimport { readGatesFromWorkflow } from 'src/lib/workflow-gates'\nimport { parseServicesFromWorkflow } from 'src/lib/workflow-services'\nimport type { ArgumentFormProvider } from 'src/types'\n\n/**\n * @fileoverview\n *\n * The argument form the four deploy tools offer: pick a release, an environment, and \u2014 on\n * `gh-release-deploy-selected` \u2014 the services, from the values this repo actually declares.\n *\n * This is the FIRST live `formProvider` in the codebase. The seam (`types.ts`, `argument-form.ts`,\n * `tool-handler.ts`) was built before anything registered one, and every failure mode on this path\n * is SILENT: `buildArgumentForm` catches whatever `elicit()` throws and returns `null`, which the\n * handler reads as \"this client cannot render forms\" and gates instead. A provider that is simply\n * WRONG is therefore indistinguishable, from the outside, from a provider that correctly decided it\n * had nothing to offer \u2014 so the constraints below are load-bearing, and each is pinned by a test\n * that asserts `buildArgumentForm(...) !== null` rather than \"a gate came back\".\n */\n\n/** The literal `resolveDeployBranch` accepts for \"deploy from the dev branch, not a release\". */\nconst DEV_VERSION = 'dev'\n\n/** The arguments a deploy tool can have filled in by a form. */\nexport type DeployFormField = 'version' | 'env' | 'services'\n\ninterface DeployFormProviderArgs {\n /**\n * The workflow this tool dispatches, and the file BOTH the env list and the service list are read\n * from. Two tools, two files: `gh-release-deploy-all` reads `deploy-all.yml` and\n * `gh-release-deploy-selected` reads `deploy-selected-services.yml`, which genuinely declare\n * different environments in the consumer repos.\n */\n workflowFile: string\n /** Which arguments this tool has. The local pair offers no `version`; only `-selected` has services. */\n fields: readonly DeployFormField[]\n /** Named in the `form options empty` log line, which is how \"nothing to offer\" stays diagnosable. */\n toolName: string\n}\n\n/** A plain object \u2014 not an array, not `null`, not a primitive. */\nconst isRecord = (value: unknown): value is Record<string, unknown> => {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/**\n * Whether round 1 left this field for the human to supply.\n *\n * A JSON-RPC request cannot carry `undefined`, so an explicitly-`undefined` key can only come from\n * a hand-built call; treating it as absent is the reading that lets the form fill it, and it stays\n * safe because `narrowsArgs` only checks that the key is PRESENT afterwards.\n */\nconst isAbsent = (params: Record<string, unknown>, field: DeployFormField): boolean => {\n return params[field] === undefined\n}\n\n/** The round-1 value of a field, when it is a string worth quoting back in the prose. */\nconst roundOneString = (params: Record<string, unknown>, field: DeployFormField): string | undefined => {\n const value = params[field]\n\n return typeof value === 'string' && value.length > 0 ? value : undefined\n}\n\n/** Collapse a failed enumeration into an empty one \u2014 the provider's own contract is never-throws. */\nconst orEmpty = async (work: Promise<string[]>): Promise<string[]> => {\n try {\n return await work\n } catch {\n return []\n }\n}\n\n/** The open releases as the labels `resolveDeployBranch` accepts (`1.2.5`, `checkout-redesign`). */\nconst releaseLabels = async (): Promise<string[]> => {\n const prs = await getReleasePRsWithInfo()\n\n return releaseBranchLabels(\n prs.map((pr) => {\n return pr.branch\n }),\n )\n}\n\n/**\n * The gate map, as the one marking the wire permits.\n *\n * There are NO per-option labels in an elicitation enum \u2014 `.describe()` attaches at the field level\n * only (measured, `docs/reviews/elicit-schema-measurements.md`) \u2014 so this prose, read before the\n * list, is the whole of what the human is told about which services the environments exclude. It is\n * derived from the same workflow-scoped `readGatesFromWorkflow` the pre-dispatch refusal uses, so\n * the two cannot drift; a hand-written map here would go stale silently and mislead rather than warn.\n */\nconst gateMapProse = (services: string[], gates: Map<string, string[]>): string => {\n const gated = services.filter((service) => {\n return gates.get(service) !== undefined\n })\n\n if (gated.length === 0) return 'none declared.'\n\n const marks = gated\n .map((service) => {\n return `${service}: ${(gates.get(service) ?? []).join(', ')} only.`\n })\n .join(' ')\n\n return gated.length < services.length ? `${marks} Others: any environment.` : marks\n}\n\n/**\n * Create the `formProvider` for one deploy tool.\n *\n * @example\n * createDeployFormProvider({\n * workflowFile: 'deploy-selected-services.yml',\n * fields: ['version', 'env', 'services'],\n * toolName: 'gh-release-deploy-selected',\n * })\n */\nexport const createDeployFormProvider = (args: DeployFormProviderArgs): ArgumentFormProvider => {\n const { workflowFile, fields, toolName } = args\n\n // `null` with a REASON. `z.enum([])` is not an empty dropdown, it is a `TypeError` thrown inside\n // `inputRequired.elicit()` and flattened by `buildArgumentForm`'s catch into the same `null` a\n // non-elicitation client produces (measured). Returning early and logging is what keeps \"there is\n // nothing to offer\" a decision someone can read in the log rather than a swallowed error. This is\n // not hypothetical at this repo's own root, where neither deploy workflow exists.\n const noOptions = (list: string): null => {\n logger.info({ msg: `Tool execution form options empty (${workflowFile}): ${toolName}`, emptyList: list })\n\n return null\n }\n\n const versionField = async (params: Record<string, unknown>): Promise<z.ZodTypeAny | null> => {\n const labels = await orEmpty(releaseLabels())\n\n // Checked BEFORE `dev` is appended, deliberately: with it the list is never empty, so an empty\n // check afterwards could never fire and a `gh` outage would render a one-option dropdown that\n // looks like \"there are no open releases\".\n if (labels.length === 0) return null\n\n const current = roundOneString(params, 'version')\n\n return z\n .enum([...new Set([...labels, DEV_VERSION])])\n .optional()\n .describe(\n `Which release to deploy from. \"${DEV_VERSION}\" deploys from the dev branch instead of a release branch. ${\n current === undefined\n ? 'Leaving it blank sends no version, and the tool will refuse rather than guess one.'\n : `Leave it blank to keep the caller's \"${current}\".`\n }`,\n )\n }\n\n const envField = async (params: Record<string, unknown>): Promise<z.ZodTypeAny | null> => {\n const envs = deployableEnvs(await orEmpty(readWorkflowEnvOptions(workflowFile)), await resolveProtectedEnvAccess())\n\n if (envs.length === 0) return null\n\n const shared = envs.filter(isSharedEnv)\n const personal = envs.filter((env) => {\n return !isSharedEnv(env)\n })\n\n const current = roundOneString(params, 'env')\n\n return z\n .enum(envs)\n .optional()\n .describe(\n `Target environment. Shared with the whole team: ${shared.length > 0 ? shared.join(', ') : 'none'}. ` +\n `Personal accounts: ${personal.length > 0 ? personal.join(', ') : 'none'}. ` +\n `This list is read from .github/workflows/${workflowFile} in the WORKING TREE, not from the ` +\n `branch being deployed or from GitHub. ${\n current === undefined\n ? 'Leaving it blank sends no environment at all.'\n : `Leave it blank to keep the caller's \"${current}\" \u2014 including when this tree does not declare it.`\n }`,\n )\n }\n\n const servicesField = async (): Promise<z.ZodTypeAny | null> => {\n const projectRoot = await getProjectRoot()\n const services = await orEmpty(parseServicesFromWorkflow(projectRoot, workflowFile))\n\n if (services.length === 0) return null\n\n const gates = await readGatesFromWorkflow(projectRoot, workflowFile)\n\n // `z.array(z.enum(...))`, never `z.array(z.string())`. The second is the spelling this tool's own\n // `inputSchema` uses, so it is what an author copies across \u2014 and it THROWS inside `elicit()`\n // before a single byte is sent, degrading to a gate that looks exactly like a client which cannot\n // render forms. P10 is the guard, and it asserts on `buildArgumentForm`, not on this object.\n return z\n .array(z.enum(services))\n .optional()\n .describe(\n `Services to deploy. Environment gates declared by ${workflowFile} \u2014 ${gateMapProse(services, gates)} ` +\n `A service the chosen environment gates out is REFUSED before dispatch, not skipped in silence. ` +\n `Selecting none discards the form and falls back to the caller's arguments.`,\n )\n }\n\n return {\n message: `Choose the arguments for ${toolName}. Anything left blank keeps what the caller sent.`,\n\n isFormable: (params: unknown): boolean => {\n if (!isRecord(params)) return false\n\n return fields.some((field) => {\n return isAbsent(params, field)\n })\n },\n\n buildRequestedSchema: async (params: unknown): Promise<z.ZodObject<z.ZodRawShape> | null> => {\n const round1 = isRecord(params) ? params : {}\n // A MUTABLE shape: `z.ZodRawShape` is `Readonly` in zod 4, so it cannot be assembled in place.\n const shape: Record<string, z.ZodTypeAny> = {}\n\n if (fields.includes('version')) {\n const field = await versionField(round1)\n\n if (field === null) return noOptions('releases')\n\n shape.version = field\n }\n\n if (fields.includes('env')) {\n const field = await envField(round1)\n\n if (field === null) return noOptions('envs')\n\n shape.env = field\n }\n\n // Offered ONLY when round 1 omitted it. If the agent already supplied a list and the human\n // picks a different NUMBER of services, `narrowsArgs` sees an array whose length changed,\n // discards the whole merge, and gates on the agent's list \u2014 the human's selection vanishes with\n // the gate showing something they never chose. The conditional is what makes that unreachable.\n if (fields.includes('services') && isAbsent(round1, 'services')) {\n const field = await servicesField()\n\n if (field === null) return noOptions('services')\n\n shape.services = field\n }\n\n // Never `z.object({})` to mean \"nothing to offer\": an empty object renders cleanly and sends a\n // form with no fields in it, which the human can only accept or decline.\n if (Object.keys(shape).length === 0) return noOptions('fields')\n\n return z.object(shape)\n },\n\n // MERGES, field by field, over the round-1 arguments \u2014 never constructs a fresh object. Two\n // properties follow from that and are asserted by P11 and P12: a field the human left blank keeps\n // its round-1 value (which is also the drift escape hatch for an env this tree does not declare),\n // and no key outside round-1 keys \u222A `fields` can ever appear.\n toArgs: (content: Record<string, unknown>, params: unknown): Record<string, unknown> | null => {\n const merged: Record<string, unknown> = { ...(isRecord(params) ? params : {}) }\n\n for (const field of fields) {\n const value = content[field]\n\n if (value === undefined) continue\n\n // An empty selection is a NAMED discard, not an argument. `narrowsArgs` cannot catch it: the\n // key is absent from round 1, so `services: []` is a legal addition and would reach the tool\n // as \"deploy nothing\", which dispatches a run that reports success and ships nothing.\n // Returning `null` routes it through the existing validation-discard path instead, which sets\n // `formDiscarded` and says so in the gate's prose.\n if (field === 'services' && (!Array.isArray(value) || value.length === 0)) return null\n\n merged[field] = value\n }\n\n return merged\n },\n }\n}\n", "import fs from 'node:fs/promises'\nimport path from 'node:path'\n\nconst WORKFLOWS_DIR = '.github/workflows'\n\n/** `script_name: client-be` \u2014 ties a job block to the script it runs. */\nconst SCRIPT_NAME_PATTERN = /script_name:\\s*(?<service>[a-z0-9-]+)/\n\n/**\n * `uses: ./.github/workflows/_deploy-media-jobs.yml` \u2014 the other way a job names its script.\n *\n * `media` has no `script_name` in either repo because it has a dedicated reusable workflow rather\n * than going through the shared serverless one. Without this, travelist's prod-only media gate is\n * invisible and `--all` would offer it everywhere.\n *\n * This also matches `_deploy-serverless-jobs.yml` \u2192 `serverless`, which is not a service; callers\n * drop names that have no `deploy-<name>.sh` on disk, so it costs nothing.\n */\nconst REUSABLE_WORKFLOW_PATTERN = /uses:\\s*\\.\\/\\.github\\/workflows\\/_deploy-(?<service>[a-z0-9-]+)-jobs\\.ya?ml/\n\n/** `inputs.environment == 'prod'` \u2014 one arm of a job's `if:` gate. */\nconst ENV_EQUALS_PATTERN = /inputs\\.environment\\s*==\\s*'(?<env>[a-z0-9-]+)'/g\n\n/**\n * Every environment gate the repo declares for a service, unioned across ALL workflows.\n *\n * Answers \"can this service deploy here at all from this machine\". `local-deploy` uses it as a\n * fallback for services whose script carries no `skip_unless_env_enabled`: both sources encode the\n * same rule and normally agree, but they drift \u2014 travelist gates `mobile` to dev/prod and `media` to\n * prod alone in `deploy-all.yml`, while neither script has the guard hulyo's equivalents do. Reading\n * only the scripts would let `--all` include services CI refuses; reading only the YAML would miss\n * the guard that actually protects a manual run. So we read both and take the intersection.\n *\n * Best-effort by design: a gate we cannot parse yields no restriction, because inventing one would\n * silently shrink what the user can deploy.\n */\nexport const readWorkflowGates = async (projectRoot: string): Promise<Map<string, string[]>> => {\n const dir = path.resolve(projectRoot, WORKFLOWS_DIR)\n\n let files: string[]\n\n try {\n files = await fs.readdir(dir)\n } catch {\n return new Map()\n }\n\n return collectFrom(\n dir,\n files.filter((entry) => {\n return entry.endsWith('.yml') || entry.endsWith('.yaml')\n }),\n )\n}\n\n/**\n * The gates one named workflow declares \u2014 the file a dispatch is actually about to run.\n *\n * Answers a strictly different question from {@link readWorkflowGates}: \"will THIS dispatch's job\n * run\", not \"can this service deploy here at all\". Collapsing the two would be wrong in both\n * directions, and the union is the worse wrong one for a dispatch: travelist's `media` inherits\n * `['prod']` from `deploy-all.yml`, while `deploy-selected-services.yml` gates `media` not at all,\n * so a union-based refusal blocks `media` + `dev` on a workflow that would happily run it \u2014 a gate\n * borrowed from a file nobody dispatched.\n *\n * Same best-effort contract: an absent or unparseable file yields an empty map, i.e. no restriction.\n */\nexport const readGatesFromWorkflow = async (\n projectRoot: string,\n workflowFile: string,\n): Promise<Map<string, string[]>> => {\n return collectFrom(path.resolve(projectRoot, WORKFLOWS_DIR), [workflowFile])\n}\n\n/** Union the gates of the given workflow files. An unreadable file contributes nothing. */\nconst collectFrom = async (dir: string, files: string[]): Promise<Map<string, string[]>> => {\n const gates = new Map<string, string[]>()\n\n for (const file of files) {\n let source: string\n\n try {\n source = await fs.readFile(path.resolve(dir, file), 'utf-8')\n } catch {\n continue\n }\n\n collectGates(source, gates)\n }\n\n return gates\n}\n\n/**\n * Pull `<service> -> [envs]` out of one workflow.\n *\n * Job blocks are split on two-space-indented keys, which is how every deploy workflow in both repos\n * is written. A block contributes only when it names a script AND its `if:` tests the environment \u2014\n * an unconditional job means \"all environments\" and must not narrow anything.\n */\nconst collectGates = (source: string, gates: Map<string, string[]>): void => {\n for (const block of source.split(/\\n(?= {2}[\\w-]+:)/i)) {\n const service =\n SCRIPT_NAME_PATTERN.exec(block)?.groups?.service ?? REUSABLE_WORKFLOW_PATTERN.exec(block)?.groups?.service\n\n if (service === undefined) continue\n\n // A commented-out job (hulyo keeps `deploy-ai-ui` that way) declares nothing.\n if (block.trimStart().startsWith('#')) continue\n\n const envs = [...block.matchAll(ENV_EQUALS_PATTERN)].map((match) => {\n return match.groups?.env ?? ''\n })\n\n if (envs.length === 0) continue\n\n // A service appearing in several workflows is allowed wherever ANY of them allows it.\n gates.set(service, [...new Set([...(gates.get(service) ?? []), ...envs])])\n }\n}\n\n/**\n * Combine the script guard with the workflow gate.\n *\n * `null` on either side means \"unrestricted from that source\", so the answer is whatever the other\n * one says. When both restrict, the intersection is what actually deploys.\n */\nexport const intersectGates = (scriptEnvs: string[] | null, workflowEnvs: string[] | undefined): string[] | null => {\n if (scriptEnvs === null) return workflowEnvs ?? null\n if (workflowEnvs === undefined) return scriptEnvs\n\n return scriptEnvs.filter((env) => {\n return workflowEnvs.includes(env)\n })\n}\n", "import fs from 'node:fs/promises'\nimport path from 'node:path'\nimport yaml from 'yaml'\n\n/** Where GitHub keeps dispatchable workflows. The only place we look for service declarations. */\nconst WORKFLOWS_DIR = '.github/workflows'\n\n/**\n * A `workflow_dispatch` boolean that is a flag, not a service \u2014 the one exclusion from the list.\n *\n * Named here rather than at the call site because the exclusion is a property of the WORKFLOW's\n * input vocabulary, not of whoever is reading it: both readers (the command's validation and the\n * argument form's picker) must drop exactly the same key or the form offers a \"service\" the command\n * then refuses as invalid.\n */\nconst SKIP_TERRAFORM_INPUT = 'skip_terraform_deploy'\n\n/**\n * The services a workflow can deploy: its `workflow_dispatch` boolean inputs, minus the flags that\n * are not services.\n *\n * Returns `[]` rather than throwing when the workflow is missing or unreadable \u2014 callers turn that\n * into a sentence (the command) or into \"no form\" (the argument-form provider). Previously an absent\n * file (bridge has no `deploy-selected-services.yml`) escaped as a raw `ENOENT` from `fs.readFile`,\n * and a workflow with no `workflow_dispatch` as a `TypeError` on `parsed.on.workflow_dispatch`.\n *\n * @example\n * await parseServicesFromWorkflow('/repo', 'deploy-selected-services.yml') // => ['client-be', 'client-fe']\n * await parseServicesFromWorkflow('/repo', 'absent.yml') // => []\n */\n// Extracted out of `gh-release-deploy-selected.ts`, where it was module-private, for the same reason\n// `workflow-gates.ts` was extracted: the argument-form provider needs the identical list, and the\n// provider is wired INTO that command's tool definition. Importing it back from the command would\n// close a cycle (`command \u2192 lib/deploy-form \u2192 command`), and re-deriving it would put a second YAML\n// reader in the tree, free to drift from the validation the command performs on what the form\n// returns \u2014 the form would offer a name the handler then calls invalid.\nexport const parseServicesFromWorkflow = async (projectRoot: string, workflowFile: string): Promise<string[]> => {\n const workflowPath = path.resolve(projectRoot, WORKFLOWS_DIR, workflowFile)\n\n let parsed: unknown\n\n try {\n parsed = yaml.parse(await fs.readFile(workflowPath, 'utf-8'))\n } catch {\n return []\n }\n\n const on = (parsed as { on?: unknown } | null)?.on\n const inputs = (on as { workflow_dispatch?: { inputs?: unknown } } | undefined)?.workflow_dispatch?.inputs\n\n if (typeof inputs !== 'object' || inputs === null) return []\n\n return Object.entries(inputs)\n .filter(([key, value]) => {\n return (value as { type?: string } | null)?.type === 'boolean' && key !== SKIP_TERRAFORM_INPUT\n })\n .map(([key]) => {\n return key\n })\n}\n", "import select from '@inquirer/select'\n\nimport { OperationError } from 'src/lib/errors/operation-error'\n\nimport { withEscape } from './escapable-context'\n\n/**\n * Ask which environment to deploy to, given the options the target workflow declares.\n *\n * `options` may legitimately be empty: the repo may have no such workflow (bridge has none), the\n * `environment` input may be free-text (`type: string`) rather than a `choice`, or the YAML may not\n * parse. None of those is a broken repo, so none of them may kill the command \u2014 but there is also\n * nothing to put in a picker, so this asks for `--env` by name instead of showing an empty menu.\n *\n * Note what this does NOT do: it never rejects an env for being absent from `options`. The list is read\n * from the WORKING TREE while the dispatch targets `--ref <branch>`, so the two can legitimately differ,\n * and GitHub validates the `choice` server-side regardless. Vetoing on a local read of a remote fact is\n * what made `stage` and `prod` undeployable under the old `environments` array.\n *\n * @example\n * await pickEnv(['dev', 'stage'], 'launch deploy-all workflow') // => select prompt\n * await pickEnv([], 'launch deploy-all workflow') // => throws, naming --env\n */\nexport const pickEnv = async (options: string[], operation: string): Promise<string> => {\n if (options.length === 0) {\n throw new OperationError(undefined, {\n operation,\n remediation: 'pass --env <name> explicitly',\n stderrExcerpt: 'the workflow declares no `environment` choices to pick from',\n })\n }\n\n return withEscape(\n (context) => {\n return select(\n {\n message: '\uD83E\uDDEA Select environment',\n choices: options.map((option) => {\n return {\n name: option,\n value: option,\n }\n }),\n },\n context,\n )\n },\n // Refuse is the ANSWER, not an oversight. This was `'unreachable'` while `env` was required on all\n // four deploy tools; PR-1 made it optional so the form could offer the real environment list, and\n // G8 named this claim false the moment it did \u2014 which is the whole reason that guard exists.\n //\n // Now reachable, and refusing is the only honest outcome: there is no safe default for \"which\n // environment\", so answering would deploy somewhere nobody chose. An elicitation-capable client\n // never gets here (the form supplies `env`); one that cannot render a form gets a clean\n // `OperationError` before anything dispatches, which is \u00A72.4's stated residual.\n { whenHeadless: 'refuse' },\n )\n}\n", "import confirm from '@inquirer/confirm'\n\nimport { commandEcho } from 'src/lib/command-echo'\nimport { withEscape } from 'src/lib/prompts/escapable-context'\n\ninterface ConfirmDeployArgs {\n confirmedCommand?: boolean\n branch: string\n env: string\n}\n\n/**\n * Gate a workflow dispatch behind an interactive confirmation. Returns true to proceed;\n * `confirmedCommand` (CLI `--yes`) skips the prompt.\n *\n * The message names the RUNNER as well as the branch, because that is the one thing the command name\n * no longer carries: `release deploy-all` takes `--from ci|local`, so \"which machine is about to build\n * and upload this\" is a flag rather than a command name. Saying it here keeps the interactive path\n * honest about it. The local path has always named its runner (`local-deploy.ts`, \"from this machine\").\n *\n * SCOPE LIMIT \u2014 this is a courtesy for the interactive human, NOT a control. `--yes` returns early,\n * and every MCP call arrives with `confirmedCommand: true` already set by the tool handler, so no\n * agent-initiated deploy ever sees this string. Nor is `--from` the control on that path: agents call\n * the `local-deploy-*` / `gh-release-deploy-*` tools directly and never traverse the merged command at\n * all. The only control an agent meets is `requiresHumanConfirm`'s two-phase gate.\n */\nexport const confirmDeploy = async (args: ConfirmDeployArgs): Promise<boolean> => {\n const { confirmedCommand, branch, env } = args\n\n if (confirmedCommand) return true\n\n commandEcho.setInteractive()\n\n const answer = await withEscape(\n (context) => {\n return confirm({ message: `Deploy ${branch} \u2192 ${env} via GitHub Actions?`, default: false }, context)\n },\n // Refuse is the ANSWER, not an oversight: the early return above means an MCP call (which always\n // carries `confirmedCommand`) never arrives here, and there is no field that could make it a claim.\n { whenHeadless: 'refuse' },\n )\n\n if (answer) commandEcho.addOption('--yes', true)\n\n return answer\n}\n", "import select from '@inquirer/select'\nimport process from 'node:process'\n\nimport { jsonOutput } from 'src/lib/json-output'\nimport { isMcpMode } from 'src/lib/mcp-mode'\nimport { withEscape } from 'src/lib/prompts/escapable-context'\n\nimport { DEPLOY_SOURCES } from './deploy-source'\nimport type { DeploySource } from './deploy-source'\n\n/**\n * Whether `--from` may be asked for rather than demanded.\n *\n * Mirrors the gate in `prompts/release-picker`: MCP and `--json` runs must never render a prompt into\n * a structured stream, and a non-TTY run has nobody to answer it. In all three the missing `--from` has\n * to stay a hard error, which is what keeps the strict contract for scripts, `--yes` and CI.\n */\nexport const canPromptForDeploySource = (): boolean => {\n return !isMcpMode() && process.stdin.isTTY === true && !jsonOutput.enabled\n}\n\n/** What each runner actually does, shown where the choice is made rather than buried in `--help`. */\nconst SOURCE_DESCRIPTIONS: Record<DeploySource, string> = {\n ci: 'Dispatch the GitHub workflow for a release ref',\n local: 'Run devops/scripts/deploy-*.sh here, against your current checkout',\n}\n\n/**\n * Ask where the deploy should run.\n *\n * This exists because the palette and session shell dispatch a command by its group path and NOTHING\n * else (`session/run-session.ts`) \u2014 no flags, ever. Every other consequential input on these commands\n * (`--version`, `--env`, `--services`) is optional-with-a-picker for exactly that reason; `--from` has\n * to be too, or picking a deploy from the palette is a row that fails the moment it is chosen.\n *\n * A picker is NOT a default: the choice is still stated on every invocation, just answered instead of\n * typed. The no-default property that the whole design rests on is untouched.\n */\nexport const pickDeploySource = async (): Promise<DeploySource> => {\n return withEscape(\n (context) => {\n return select(\n {\n message: '\uD83D\uDE80 Where should this deploy run?',\n choices: DEPLOY_SOURCES.map((source) => {\n return { name: source, value: source, description: SOURCE_DESCRIPTIONS[source] }\n }),\n },\n context,\n )\n },\n // Refuse is the ANSWER, not an oversight: this is barrel-reachable from the exposed deploy tools,\n // but only the merged `release-deploy` CLI command calls it, and that command is not an MCP tool.\n { whenHeadless: 'refuse' },\n )\n}\n", "import { OperationError } from 'src/lib/errors/operation-error'\n\nimport { canPromptForDeploySource, pickDeploySource } from './source-picker'\n\n/**\n * Where a deploy runs \u2014 the one choice this command exists to make explicit.\n *\n * `ci` dispatches the GitHub workflow for a release ref; `local` runs the repo's own\n * devops/scripts/deploy-*.sh here, against the current checkout. The two also differ in WHAT ships\n * (a named ref vs your working tree), but that follows from the runner rather than being a separate\n * axis: there is no path that builds a release ref locally, and none that ships a working tree via\n * CI. The artifact difference is surfaced in the option help and the confirm prompt, where it is\n * useful, instead of being encoded in cryptic flag values.\n */\nexport const DEPLOY_SOURCES = ['ci', 'local'] as const\n\nexport type DeploySource = (typeof DEPLOY_SOURCES)[number]\n\n/** Flags that only mean something for one source, so the other must refuse them rather than ignore them. */\nconst SOURCE_ONLY_FLAGS: Record<DeploySource, readonly string[]> = {\n ci: ['--version', '--skip-terraform'],\n local: ['--dry-run', '--print-env'],\n}\n\n/**\n * Validate `--from`. Deliberately has NO default: the choice between a reviewed ref built in CI and\n * your working tree built here is the most consequential one in the command, so it is stated on every\n * invocation. A default here would silently re-create the ambiguity this argument was added to remove.\n */\nexport const parseDeploySource = (from: string | undefined): DeploySource => {\n if (!from) {\n throw new OperationError(undefined, {\n operation: 'resolve the deploy source',\n remediation: `pass --from with one of: ${DEPLOY_SOURCES.join(', ')}`,\n stderrExcerpt: '--from is required \u2014 it decides whether this deploy runs in CI or on this machine',\n })\n }\n\n const match = DEPLOY_SOURCES.find((source) => {\n return source === from\n })\n\n if (!match) {\n throw new OperationError(undefined, {\n operation: 'resolve the deploy source',\n remediation: `use one of: ${DEPLOY_SOURCES.join(', ')}`,\n stderrExcerpt: `unknown --from value \"${from}\"`,\n })\n }\n\n return match\n}\n\n/**\n * Refuse a flag that belongs to the other runner.\n *\n * These combinations used to be unrepresentable \u2014 `--skip-terraform` simply did not exist on the local\n * command \u2014 so merging the two surfaces has to keep them impossible. Ignoring an inert flag would be\n * worse than the old split: `--skip-terraform` silently dropped reads as \"terraform was skipped\".\n *\n * Commander gives `undefined` for an absent option and `false` for an absent boolean flag; both count\n * as absent. There is deliberately no array case: every flag in {@link SOURCE_ONLY_FLAGS} is a boolean\n * or a single string. `--skip-preflight` was the only variadic one, and an emptiness check kept for a\n * variadic flag that no longer exists would be exactly the unreachable-branch defect its removal\n * cleared. Add the check back with the next variadic source-only flag, not before it.\n */\nexport const assertFlagsMatchSource = (source: DeploySource, present: Record<string, unknown>) => {\n const foreign = SOURCE_ONLY_FLAGS[source === 'ci' ? 'local' : 'ci']\n\n const offenders = foreign.filter((flag) => {\n const value = present[flag]\n\n return value !== undefined && value !== false\n })\n\n if (offenders.length === 0) return\n\n throw new OperationError(undefined, {\n operation: `deploy --from ${source}`,\n remediation:\n source === 'ci'\n ? 'drop the flag, or use --from local to deploy from this machine'\n : 'drop the flag, or use --from ci to dispatch the release workflow',\n stderrExcerpt: `${offenders.join(', ')} ${offenders.length === 1 ? 'is' : 'are'} not valid with --from ${source}`,\n })\n}\n\n/**\n * Resolve `--from`, asking for it when the run can be asked.\n *\n * Still no default and still stated on every invocation \u2014 the interactive path answers the question\n * rather than assuming it. Non-TTY, `--json` and MCP runs fall through to {@link parseDeploySource}'s\n * hard error, so scripts, `--yes` and CI keep the strict contract where a wrong guess is unattended.\n */\nexport const resolveDeploySource = async (from: string | undefined): Promise<DeploySource> => {\n if (from || !canPromptForDeploySource()) return parseDeploySource(from)\n\n return pickDeploySource()\n}\n", "import { getReleasePRsWithInfo } from 'src/integrations/gh'\nimport { commandEcho } from 'src/lib/command-echo'\nimport { pickReleaseBranch } from 'src/lib/prompts/release-picker'\nimport {\n detectReleaseType,\n formatBranchPickerItems,\n getJiraDescriptions,\n resolveReleaseBranch,\n} from 'src/lib/release-utils'\nimport type { ReleaseType } from 'src/lib/release-utils'\n\n/**\n * Resolve the release branch a workflow dispatch should target.\n *\n * `version` accepts a version (`1.2.5`), a release name (`checkout-redesign`), or the literal `dev`.\n * When it is omitted the open release PRs are offered in the searchable picker, annotated with their\n * Jira descriptions and release types \u2014 so `--version` stays optional on the CLI even though the MCP\n * schemas require it (interactive pickers are unavailable without a TTY).\n *\n * Shared by `deploy-all` and `deploy-selected`, which carried byte-identical copies of this block.\n */\nexport const resolveDeployBranch = async (version?: string): Promise<string> => {\n if (version) return version === 'dev' ? 'dev' : resolveReleaseBranch(version)\n\n commandEcho.setInteractive()\n\n const releasePRsInfo = await getReleasePRsWithInfo()\n\n const branches = releasePRsInfo.map((pr) => {\n return pr.branch\n })\n\n const releaseTypes = new Map<string, ReleaseType>(\n releasePRsInfo.map((pr) => {\n return [pr.branch, detectReleaseType(pr.title)]\n }),\n )\n\n const descriptions = await getJiraDescriptions()\n\n return pickReleaseBranch([\n { value: 'dev', label: 'dev' },\n ...formatBranchPickerItems({ branches, descriptions, types: releaseTypes }),\n ])\n}\n", "import checkbox from '@inquirer/checkbox'\nimport { z } from 'zod'\nimport { $ } from 'zx'\n\nimport { commandEcho } from 'src/lib/command-echo'\nimport { createDeployFormProvider } from 'src/lib/deploy-form'\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { getProjectRoot } from 'src/lib/git-utils'\nimport { logger } from 'src/lib/logger'\nimport { pickEnv } from 'src/lib/prompts/env-picker'\nimport { withEscape } from 'src/lib/prompts/escapable-context'\nimport { confirmDeploy, resolveDeployBranch } from 'src/lib/release-deploy'\nimport { releaseLabelFromBranch } from 'src/lib/release-utils'\nimport {\n assertDeployable,\n deployableEnvs,\n readWorkflowEnvOptions,\n resolveProtectedEnvAccess,\n warnProtectedEnvDispatch,\n} from 'src/lib/workflow-envs'\nimport { readGatesFromWorkflow } from 'src/lib/workflow-gates'\nimport { parseServicesFromWorkflow } from 'src/lib/workflow-services'\nimport { defineMcpTool, textContent } from 'src/types'\n\n/** The workflow this command dispatches. Its own inputs are both the env list and the service list. */\nconst DEPLOY_SELECTED_WORKFLOW = 'deploy-selected-services.yml'\n\ninterface GhReleaseDeploySelectedArgs {\n // All three used to be REQUIRED in the MCP schema, and these comments used to say so. PR-1 relaxed\n // them to `.optional()` so an argument form could offer real values, because `narrowsArgs` only\n // lets a form add a key round 1 omitted. What replaced the required field as the guard is\n // `whenHeadless` at each picker below, not the schema.\n /** Omitted on the CLI offers the open release PRs; omitted over MCP offers them in a form. */\n version?: string\n /** Omitted on the CLI offers this workflow's own environments; over MCP the form offers them. */\n env?: string\n /** Omitted on the CLI opens the service checkbox; over MCP the form offers the declared services. */\n services?: string[]\n skipTerraform?: boolean\n confirmedCommand?: boolean\n}\n\n/**\n * Deploy selected services from a release branch to an environment\n */\nexport const ghReleaseDeploySelected = async (args: GhReleaseDeploySelectedArgs) => {\n const { version, env, services, skipTerraform, confirmedCommand } = args\n\n const selectedReleaseBranch = await resolveDeployBranch(version)\n\n const selectedVersion = releaseLabelFromBranch(selectedReleaseBranch)\n\n commandEcho.addOption('--version', selectedVersion)\n\n // This workflow's own `environment` choices, minus the delivery-only ones. Read PER WORKFLOW, not once\n // per repo: travelist's `deploy-all.yml` and `deploy-selected-services.yml` genuinely declare different\n // environments, so a single repo-level list could only ever be wrong for one of them \u2014 as the old\n // `environments` array was.\n const protectedEnvAccess = await resolveProtectedEnvAccess()\n const envOptions = deployableEnvs(await readWorkflowEnvOptions(DEPLOY_SELECTED_WORKFLOW), protectedEnvAccess)\n\n let selectedEnv = ''\n\n if (env) {\n selectedEnv = env\n } else {\n commandEcho.setInteractive()\n\n selectedEnv = await pickEnv(envOptions, 'launch deploy-selected workflow')\n }\n\n commandEcho.addOption('--env', selectedEnv)\n\n // `prod` is delivered, not deployed ad-hoc \u2014 see gh-release-deliver \u2014 unless this project's\n // `protectedEnvs` says otherwise. The one rule GitHub cannot enforce for us.\n assertDeployable(selectedEnv, 'launch deploy-selected workflow', protectedEnvAccess)\n\n // Allowed, but nothing downstream re-checks it on this path: GitHub holds the credentials and no job\n // declares `environment:`. The guidance the refusal used to carry is emitted here instead.\n warnProtectedEnvDispatch({ env: selectedEnv, branch: selectedReleaseBranch })\n\n // Available services, from the same workflow's boolean inputs. Unlike the env list this one is still\n // enforced below \u2014 a `choice` value is validated by GitHub, but an undeclared `-f <service>=true` is\n // NOT known to be rejected, and a typo that GitHub shrugs at would dispatch a run that deploys\n // nothing and reports success. Until that is proven otherwise, the local check stays.\n const projectRoot = await getProjectRoot()\n\n const availableServices = await parseServicesFromWorkflow(projectRoot, DEPLOY_SELECTED_WORKFLOW)\n\n // Genuinely fatal for THIS command \u2014 there is nothing to pick from. (The failure it replaces was an\n // uncaught ENOENT from `fs.readFile`, which is how a repo with no such workflow, like bridge, used to\n // die here with a raw stack trace instead of a sentence.)\n if (availableServices.length === 0) {\n throw new OperationError(undefined, {\n operation: 'launch deploy-selected workflow',\n remediation: `declare boolean service inputs in .github/workflows/${DEPLOY_SELECTED_WORKFLOW}`,\n stderrExcerpt: `no services found in .github/workflows/${DEPLOY_SELECTED_WORKFLOW}`,\n })\n }\n\n let selectedServices: string[] = []\n\n if (services && services.length > 0) {\n selectedServices = services\n } else {\n commandEcho.setInteractive()\n\n selectedServices = await withEscape(\n (context) => {\n return checkbox(\n {\n message: '\uD83D\uDE80 Select services to deploy (space to select, enter to confirm)',\n choices: availableServices.map((svc) => {\n return {\n name: svc,\n value: svc,\n }\n }),\n },\n context,\n )\n },\n // Refuse is the ANSWER, not an oversight. This was `'unreachable'` while `services` was required;\n // PR-1 made it optional so the form could offer the declared service list, and G8 named the claim\n // false the moment it did.\n //\n // Refusing rather than answering: an empty or guessed service list is not a safe default \u2014 it\n // either deploys nothing while reporting success, or deploys something nobody picked. The form\n // supplies `services` for a client that can render one; anything else gets a clean refusal.\n { whenHeadless: 'refuse' },\n )\n }\n\n commandEcho.addOption('--services', selectedServices)\n\n if (selectedServices.length === 0) {\n throw new OperationError(undefined, {\n operation: 'launch deploy-selected workflow',\n remediation: `pass at least one service from: ${availableServices.join(', ')}`,\n stderrExcerpt: 'no services selected',\n })\n }\n\n // Validate all selected services\n const invalidServices = selectedServices.filter((svc) => {\n return !availableServices.includes(svc)\n })\n\n if (invalidServices.length > 0) {\n throw new OperationError(undefined, {\n operation: 'launch deploy-selected workflow',\n remediation: `pass services from: ${availableServices.join(', ')}`,\n stderrExcerpt: `invalid services: ${invalidServices.join(', ')}`,\n })\n }\n\n // `workflow_dispatch.inputs` says a service CAN be asked for; the jobs' own `if:` says whether it\n // will RUN. Both consumer repos gate jobs by environment \u2014 `docs-fe` to dev plus the per-developer\n // envs, `mobile` to dev/prod \u2014 and dispatch is fire-and-forget, so without this check `mobile` to\n // `stage` is accepted by GitHub, skipped by the job, and reported here as `success: true`: a deploy\n // that shipped nothing. Refuse instead, after the env has resolved and before anything is sent.\n //\n // Scoped to THIS workflow, deliberately: the repo-wide union would import gates from files nobody\n // is dispatching (travelist's `media` is prod-only in `deploy-all.yml` and ungated here), turning a\n // legitimate `media` + `dev` run into a false refusal.\n //\n // Fail-open is also deliberate (`workflow-gates.ts`): a gate we cannot parse imposes no restriction,\n // so this closes the gates we measured and does not pretend to close every possible one.\n const gates = await readGatesFromWorkflow(projectRoot, DEPLOY_SELECTED_WORKFLOW)\n\n const gatedOut = selectedServices.filter((svc) => {\n const allowed = gates.get(svc)\n\n return allowed !== undefined && !allowed.includes(selectedEnv)\n })\n\n if (gatedOut.length > 0) {\n throw new OperationError(undefined, {\n operation: 'launch deploy-selected workflow',\n remediation: gatedOut\n .map((svc) => {\n return `${svc} deploys only to: ${(gates.get(svc) ?? []).join(', ')}`\n })\n .join('; '),\n stderrExcerpt: `${DEPLOY_SELECTED_WORKFLOW} gates these services out of ${selectedEnv}: ${gatedOut.join(', ')}`,\n })\n }\n\n const shouldSkipTerraform = skipTerraform ?? false\n\n if (shouldSkipTerraform) {\n commandEcho.addOption('--skip-terraform', true)\n }\n\n const buildResult = (success: boolean) => {\n const structuredContent = {\n releaseBranch: selectedReleaseBranch,\n version: selectedVersion,\n environment: selectedEnv,\n services: selectedServices,\n skipTerraformDeploy: shouldSkipTerraform,\n success,\n }\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n }\n\n if (!(await confirmDeploy({ confirmedCommand, branch: selectedReleaseBranch, env: selectedEnv }))) {\n logger.info('Deployment cancelled')\n\n return buildResult(false)\n }\n\n try {\n $.quiet = true\n\n // Build the workflow command with boolean flags for each selected service\n const serviceFlags = selectedServices.flatMap((svc) => {\n return ['-f', `${svc}=true`]\n })\n const skipTerraformFlag = shouldSkipTerraform ? ['-f', 'skip_terraform_deploy=true'] : []\n\n await $`gh workflow run deploy-selected-services.yml --ref ${selectedReleaseBranch} -f environment=${selectedEnv} ${serviceFlags} ${skipTerraformFlag}`\n\n $.quiet = false\n\n logger.info(\n `Successfully launched deploy-selected-services workflow_dispatch for release branch: ${selectedReleaseBranch}, environment: ${selectedEnv}, services: ${selectedServices.join(', ')}`,\n )\n\n commandEcho.print()\n\n return buildResult(true)\n } catch (error: unknown) {\n logger.error({ error }, '\u274C Error launching workflow')\n throw new OperationError(error, {\n operation: 'launch deploy-selected workflow',\n remediation: \"check 'gh workflow list' and that deploy-selected-services.yml exists on the target ref\",\n })\n }\n}\n\n// MCP Tool Registration\nexport const ghReleaseDeploySelectedMcpTool = defineMcpTool({\n name: 'gh-release-deploy-selected',\n requiresHumanConfirm: true,\n // The only one of the four with a `services` field, and it reads a DIFFERENT workflow file than\n // `gh-release-deploy-all` \u2014 the consumer repos declare different environments in the two. The\n // provider offers `services` only when round 1 omitted it, because a form that changes the LENGTH\n // of an array the caller supplied is discarded whole by `narrowsArgs`.\n formProvider: createDeployFormProvider({\n workflowFile: DEPLOY_SELECTED_WORKFLOW,\n fields: ['version', 'env', 'services'],\n toolName: 'gh-release-deploy-selected',\n }),\n description:\n 'Dispatch the deploy-selected-services.yml GitHub Actions workflow to deploy a chosen subset of services from a release branch to the given environment. Fire-and-forget \u2014 returns once GitHub accepts the workflow_dispatch, NOT when the deployment finishes; watch the workflow run for completion status. Service names are validated against the boolean inputs declared in the workflow, and a service the target environment gates out is refused BEFORE dispatch rather than dispatched and silently skipped. Use gh-release-deploy-all for every service. Omit any of \"version\", \"env\" or \"services\" and this server offers the human a form built from the real releases, environments and services; a client that cannot render one gets a refusal naming the missing field, never a guess.',\n inputSchema: {\n // All three `.optional()` for the same reason: `narrowsArgs` only lets a form ADD a key round 1\n // omitted, so a required field cannot be form-filled. `services` matters most \u2014 it is the one\n // the human actually wants to pick, and the one whose length the non-narrowing check would\n // otherwise discard in silence.\n version: z\n .string()\n .optional()\n .describe(\n 'Accepts a release version (e.g. \"1.2.5\") OR a release name (e.g. \"checkout-redesign\") \u2014 resolves to the release/vX.Y.Z or release/<name> branch. Pass \"dev\" to deploy from the dev branch instead. Omit it to be offered the open releases.',\n ),\n env: z\n .string()\n .optional()\n .describe(\n 'Target environment name \u2014 must match an env this project may reach (e.g. \"dev\", \"renana\", \"oriana\"). Omit it to be offered the environments deploy-selected-services.yml declares.',\n ),\n services: z\n .array(z.string())\n .optional()\n .describe(\n 'Service names to deploy. Each must match a boolean input declared in .github/workflows/deploy-selected-services.yml (e.g. \"client-be\", \"client-fe\"). Some services are gated to particular environments by that workflow and are refused here rather than skipped by CI. Omit it to be offered the declared services.',\n ),\n skipTerraform: z.boolean().optional().describe('Skip the terraform deployment stage.'),\n confirm: z\n .boolean()\n .optional()\n .describe('Set true to execute; omit for a dry-run gate that echoes the resolved action.'),\n },\n outputSchema: {\n releaseBranch: z.string().describe('The release branch that was deployed'),\n version: z.string().describe('The version that was deployed'),\n environment: z.string().describe('The environment deployed to'),\n services: z.array(z.string()).describe('The services that were deployed'),\n skipTerraformDeploy: z.boolean().describe('Whether terraform deployment was skipped'),\n success: z.boolean().describe('Whether the deployment was successful'),\n },\n handler: ghReleaseDeploySelected,\n})\n", "import { z } from 'zod'\n\nimport { getReleasePRsWithInfo } from 'src/integrations/gh'\nimport { logger } from 'src/lib/logger'\nimport { displayLabel, formatJiraName, parseBranchName } from 'src/lib/release-id'\nimport { detectReleaseType, formatVersionLabel, getJiraDescriptions } from 'src/lib/release-utils'\nimport { defineMcpTool, textContent } from 'src/types'\n\n/**\n * List all open release branches\n */\nexport const ghReleaseList = async () => {\n const releasePRs = await getReleasePRsWithInfo()\n\n // Skip branches that do not parse as release ids (lenient discovery source).\n const releases = releasePRs.flatMap((pr) => {\n const id = parseBranchName(pr.branch)\n\n if (!id) return []\n\n return [\n {\n // Human display label: `1.2.3` | `<name>`.\n version: displayLabel(id),\n // Jira-descriptions map is keyed by the Jira version NAME (`v1.2.3` | `<name>`).\n jiraKey: formatJiraName(id),\n type: detectReleaseType(pr.title),\n },\n ]\n })\n\n const jiraDescriptions = await getJiraDescriptions()\n\n const maxVersionLength = Math.max(\n ...releases.map((r) => {\n return r.version.length\n }),\n )\n\n const formattedLines = releases.map((release) => {\n const label = formatVersionLabel(release.version, release.type, maxVersionLength)\n const description = jiraDescriptions.get(release.jiraKey)\n\n if (description) {\n return `${label} ${description}`\n }\n\n return label\n })\n\n logger.info('All release branches: \\n')\n logger.info(`\\n${formattedLines.join('\\n')}\\n`)\n\n const structuredContent = {\n releases: releases.map((release) => {\n return {\n version: release.version,\n type: release.type,\n description: jiraDescriptions.get(release.jiraKey) || null,\n }\n }),\n count: releases.length,\n }\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n}\n\n// MCP Tool Registration\nexport const ghReleaseListMcpTool = defineMcpTool({\n name: 'gh-release-list',\n description:\n 'List every open release PR with its version, type (regular / hotfix), and associated Jira fix-version description. Read-only; sourced from GitHub and Jira.',\n inputSchema: {},\n outputSchema: {\n releases: z\n .array(\n z.object({\n version: z.string().describe('Release version'),\n type: z.enum(['regular', 'hotfix']).describe('Release type'),\n description: z.string().nullable().describe('Jira version description'),\n }),\n )\n .describe('List of all release branches'),\n count: z.number().describe('Number of release branches'),\n },\n handler: ghReleaseList,\n})\n", "import process from 'node:process'\n\n/** Vars the deploy build reads. Everything else in `VITE_*` is ambient noise and gets stripped. */\nexport interface DeployContract {\n VITE_DOMAIN_ENV: string\n VITE_BRANCH_NAME: string\n VITE_COMMIT_HASH: string\n}\n\n/**\n * The contract as a plain record, for the MCP `outputSchema`.\n *\n * An `interface` has no implicit index signature, so it is not assignable to `Record<string, string>`\n * \u2014 hence the explicit widening here rather than at each call site.\n */\nexport const contractRecord = (contract: DeployContract): Record<string, string> => {\n return { ...contract }\n}\n\nexport interface BuildEnvArgs {\n env: string\n branch: string\n sha: string\n /**\n * Whether the working tree matched HEAD when the deploy started.\n *\n * Required, not defaulted: an omitted value would silently produce the confident label, which is the\n * exact defect this argument exists to remove.\n */\n isClean: boolean\n /** Defaults to `process.env`; injectable so tests never depend on the ambient shell. */\n ambient?: NodeJS.ProcessEnv\n}\n\nexport interface BuildEnvResult {\n /** The full child environment to spawn the deploy script with. */\n childEnv: NodeJS.ProcessEnv\n /** The three contract values, for `--print-env` and the run record. */\n contract: DeployContract\n /** Names of ambient `VITE_*` vars that were dropped. Names only \u2014 values may be secrets. */\n stripped: string[]\n}\n\n/**\n * Build the environment a deploy script runs under \u2014 the whole point of this command.\n *\n * Sets the `VITE_*` contract the workflow YAML would otherwise supply, and STRIPS every ambient\n * `VITE_*` the developer's shell carries so nothing leaks into the artifact. `DEPLOY_*` are set\n * alongside for forward compatibility; nothing reads them today.\n *\n * @example\n * buildDeployEnv({ env: 'arthur', branch: 'dev', sha: 'abc123', ambient: { VITE_LEAK: 'x' } })\n * // => stripped: ['VITE_LEAK'], contract.VITE_DOMAIN_ENV: 'arthur'\n */\n// The deploy scripts are NOT self-contained. `_deploy-serverless-jobs.yml` sets `VITE_DOMAIN_ENV`,\n// `VITE_BRANCH_NAME` and `VITE_COMMIT_HASH` in the workflow YAML, and no script sets them. Run a\n// script straight from a laptop and they are simply unset \u2014 at which point\n// `apps/backoffice/ui/src/lib/constants/index.ts` falls through to `https://www.hulyo.co.il`, so a\n// bundle deployed to a dev environment links to PRODUCTION. Silently, with a green deploy. That\n// gap is why `infra-kit local deploy-all` exists rather than a README telling people which vars\n// to export.\n//\n// The strip is the other half. Builds run `turbo \u2026 --env-mode=loose`, which passes the entire\n// ambient environment through, and `turbo.json` declares `build.env: [\"VITE_*\"]`. After\n// `infra-kit env-load` a developer's shell carries Doppler values \u2014 so without stripping, a local\n// deploy would bake whatever `VITE_*` happens to be exported into an artifact shipped to a shared\n// environment.\nexport const buildDeployEnv = (args: BuildEnvArgs): BuildEnvResult => {\n const { env, branch, sha, isClean, ambient = process.env } = args\n\n const contract: DeployContract = {\n VITE_DOMAIN_ENV: env,\n VITE_BRANCH_NAME: branch,\n // `git describe --dirty` convention. A dirty tree ships content that is NOT in `sha` \u2014 the build\n // reads the filesystem, not git \u2014 so labelling it with the bare sha makes \"what is deployed here?\"\n // permanently unanswerable. That mislabelling is worst on PERSONAL environments, where a dirty\n // tree is normal and no check refuses it, so no amount of tightening the clean-tree gate reaches\n // it. Only the display/diagnostic value is suffixed; `DEPLOY_SHA` below stays the raw sha, because\n // the deploy scripts hand it to AWS.\n VITE_COMMIT_HASH: isClean ? sha : `${sha}-dirty`,\n }\n\n const childEnv: NodeJS.ProcessEnv = {}\n const stripped: string[] = []\n\n for (const [key, value] of Object.entries(ambient)) {\n if (key.startsWith('VITE_')) {\n // Recorded as stripped even when the contract re-sets it: the ambient value did not survive,\n // and saying so is what makes a wrong-looking build explainable after the fact.\n stripped.push(key)\n continue\n }\n\n childEnv[key] = value\n }\n\n stripped.sort()\n\n return {\n childEnv: {\n ...childEnv,\n ...contract,\n DEPLOY_ENV: env,\n DEPLOY_BRANCH: branch,\n DEPLOY_SHA: sha,\n // The dirty-aware label, carried in a NON-`VITE_` variable on purpose.\n //\n // The Phase-0 `devops/scripts/lib/deploy-env.sh` contract strips every ambient `VITE_*` and then\n // rebuilds `VITE_COMMIT_HASH` from `DEPLOY_SHA` \u2014 which is raw by design, because the scripts hand\n // it to AWS. So the moment that script lands in a consumer repo it would erase the `-dirty`\n // marker set above. This variable survives the strip, so the script's contract can prefer it and\n // the marker outlives Phase 0. See docs/local-deploy-design.md \u00A79.1.\n DEPLOY_SHA_LABEL: contract.VITE_COMMIT_HASH,\n // Deploy builds are not interactive and telemetry noise obscures the failure line we surface.\n TURBO_TELEMETRY_DISABLED: '1',\n },\n contract,\n stripped,\n }\n}\n\n/**\n * The contract as printed by `--print-env` and `--dry-run`.\n *\n * Prints the three contract keys and nothing else. Never dump the child environment: it inherits the\n * caller's shell, which is exactly where `AWS_SECRET_ACCESS_KEY` and `AWS_SESSION_TOKEN` live.\n */\nexport const formatContract = (result: BuildEnvResult): string => {\n const lines = Object.entries(result.contract).map(([key, value]) => {\n return ` ${key.padEnd(17)}= ${value}`\n })\n\n if (result.stripped.length > 0) {\n lines.push(` stripped from shell: ${result.stripped.join(', ')}`)\n }\n\n return lines.join('\\n')\n}\n", "import checkbox from '@inquirer/checkbox'\nimport confirm from '@inquirer/confirm'\nimport select from '@inquirer/select'\nimport { spawn } from 'node:child_process'\nimport process from 'node:process'\nimport { z } from 'zod'\nimport { $ } from 'zx'\n\nimport { commandEcho } from 'src/lib/command-echo'\nimport { createDeployFormProvider } from 'src/lib/deploy-form'\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { getCurrentBranch, getProjectRoot, getRepoName, isWorkingTreeClean } from 'src/lib/git-utils'\nimport { logger } from 'src/lib/logger'\nimport { pickEnv } from 'src/lib/prompts/env-picker'\nimport { withEscape } from 'src/lib/prompts/escapable-context'\nimport { deployableEnvs, isSharedEnv, readWorkflowEnvOptions, resolveProtectedEnvAccess } from 'src/lib/workflow-envs'\nimport { defineMcpTool, textContent } from 'src/types'\n\nimport { buildDeployEnv, contractRecord, formatContract } from './deploy-env'\nimport type { BuildEnvResult } from './deploy-env'\nimport { runPreflight } from './preflight'\nimport { discoverServices, eligibleServices, isEligible } from './service-discovery'\nimport type { DeployService } from './service-discovery'\n\n/** The workflow whose `environment` choices seed the picker. Advisory only \u2014 `--env` always wins. */\nconst DEPLOY_ALL_WORKFLOW = 'deploy-all.yml'\n\n/** Absolute, so the shell we run cannot be resolved from a caller-controlled PATH. */\nconst SHELL = '/bin/sh'\n\ninterface LocalDeployArgs {\n env?: string\n service?: string[]\n /**\n * Consent already given \u2014 skip {@link confirmTarget}.\n *\n * The name is not cosmetic and must match `confirmDeploy`'s (`confirm-deploy.ts:27`): the MCP\n * chokepoint injects `confirmedCommand: true` into every handler call it lets through\n * (`tool-handler.ts:466`), and nothing injects `yes`. This field was previously spelled `yes`, so\n * on an MCP call it was `undefined`, `!yes` was true, and `confirmTarget` at the guard below\n * rendered an inquirer prompt into the JSON-RPC stream \u2014 `@inquirer` writes to `process.stdout`\n * (its `create-prompt.js` pipes to `context.output ?? process.stdout`, and no call site here\n * passes `output`), which under MCP stdio IS the transport. That is stream corruption, not a hang.\n * The CLI's `--yes` reaches this field through `release-deploy.ts`, exactly as it does for the two\n * `gh-release-deploy-*` commands.\n */\n confirmedCommand?: boolean\n dryRun?: boolean\n printEnv?: boolean\n}\n\n/** Environments with a CI deploy currently in flight, or `[]` when `gh` cannot answer. */\nconst runningCiEnvs = async (): Promise<string[]> => {\n const previousQuiet = $.quiet\n\n $.quiet = true\n\n try {\n const result = await $`gh run list --workflow=${DEPLOY_ALL_WORKFLOW} --status=in_progress --json displayTitle`\n const runs = JSON.parse(result.stdout) as { displayTitle?: string }[]\n\n // The workflow's `run-name` renders as `01 \u2699\uFE0F Deploy all [dev]`, so the env sits in brackets.\n return runs.flatMap((run) => {\n return /\\[(?<env>[a-z0-9-]+)\\]/.exec(run.displayTitle ?? '')?.groups?.env ?? []\n })\n } catch {\n // `gh` missing, unauthenticated, or no such workflow. This is a race guard, not a security\n // control \u2014 refusing to deploy because an optional tool is absent would be worse than the race.\n return []\n } finally {\n $.quiet = previousQuiet\n }\n}\n\n/** Ask which services to deploy, showing only those the chosen environment actually accepts. */\nconst pickServices = async (services: DeployService[], env: string): Promise<string[]> => {\n const choices = services.map((service) => {\n return { name: service.name, value: service.name }\n })\n\n const selected = await withEscape(\n (context) => {\n return checkbox({ message: `Deploy which services to ${env}?`, choices, pageSize: 20 }, context)\n },\n // MCP-unreachable: `service` is required (min 1) on local-deploy-selected; local-deploy-all takes the \"all\" branch.\n { whenHeadless: 'unreachable' },\n )\n\n if (selected.length === 0) {\n throw new OperationError(undefined, {\n operation: 'select services',\n remediation: 'pick at least one service, or use `local deploy-all`',\n stderrExcerpt: 'no services selected',\n })\n }\n\n return selected\n}\n\n/**\n * Confirm before touching a shared environment.\n *\n * Shared targets get an explicit two-option prompt defaulting to cancel rather than a y/N whose\n * default is one keystroke away: `dev` is everyone's, and a reflexive Enter is how it gets clobbered.\n */\nconst confirmTarget = async (args: { env: string; isShared: boolean; count: number }): Promise<boolean> => {\n const { env, isShared, count } = args\n\n if (!isShared) {\n return withEscape(\n (context) => {\n return confirm({ message: `Deploy ${count} service(s) to ${env} from this machine?`, default: false }, context)\n },\n // Refuse is the ANSWER, not an oversight: the caller gates this on `!confirmedCommand`, which the\n // MCP chokepoint always injects. That is a gate, not a schema fact, so it is no `'unreachable'` claim.\n { whenHeadless: 'refuse' },\n )\n }\n\n return withEscape(\n (context) => {\n return select(\n {\n message: `\"${env}\" is a SHARED environment \u2014 deploying ${count} service(s) from this machine. Continue?`,\n choices: [\n { name: 'No, cancel', value: false },\n { name: `Yes, deploy to ${env}`, value: true },\n ],\n },\n context,\n )\n },\n // Refuse is the ANSWER, not an oversight: the caller gates this on `!confirmedCommand`, which the\n // MCP chokepoint always injects. That is a gate, not a schema fact, so it is no `'unreachable'` claim.\n { whenHeadless: 'refuse' },\n )\n}\n\n/**\n * Run one deploy script with the resolved contract, streaming its output.\n *\n * `stdio: 'inherit'` rather than captured: these scripts run for minutes and echo their own\n * `COMMAND:` lines, and someone watching a deploy needs to see it happen, not a transcript after.\n */\nconst runDeployScript = (service: DeployService, childEnv: NodeJS.ProcessEnv, cwd: string): Promise<number> => {\n return new Promise((resolve, reject) => {\n const child = spawn(SHELL, [service.scriptPath], { cwd, env: childEnv, stdio: 'inherit' })\n\n child.on('error', reject)\n child.on('close', (code) => {\n resolve(code ?? 1)\n })\n })\n}\n\n/**\n * Deploy each service in turn, stopping at the first failure.\n *\n * Sequential, not parallel: every script does `rm -rf ./out` in the repo root, so two at once would\n * delete each other's build. Stopping on failure avoids shipping a half-updated environment.\n */\nconst executeDeploys = async (args: {\n chosen: DeployService[]\n childEnv: NodeJS.ProcessEnv\n cwd: string\n env: string\n}): Promise<{ deployed: string[]; failed: string[] }> => {\n const { chosen, childEnv, cwd, env } = args\n\n const deployed: string[] = []\n const failed: string[] = []\n\n for (const entry of chosen) {\n logger.info(`\u2192 ${entry.name} \u2192 ${env}`)\n\n const code = await runDeployScript(entry, childEnv, cwd)\n\n if (code !== 0) {\n failed.push(entry.name)\n logger.error(`\u2717 ${entry.name} failed (exit ${code})`)\n break\n }\n\n deployed.push(entry.name)\n logger.info(`\u2713 ${entry.name}`)\n }\n\n return { deployed, failed }\n}\n\n/** Refuse names that do not exist, or that this environment will not accept. */\nconst assertServicesUsable = (args: { names: string[]; services: DeployService[]; env: string }): DeployService[] => {\n const { names, services, env } = args\n\n const known = new Map(\n services.map((entry) => {\n return [entry.name, entry]\n }),\n )\n\n const unknown = names.filter((name) => {\n return !known.has(name)\n })\n\n if (unknown.length > 0) {\n throw new OperationError(undefined, {\n operation: 'resolve the requested services',\n remediation: `known services: ${services\n .map((entry) => {\n return entry.name\n })\n .join(', ')}`,\n stderrExcerpt: `no deploy script for: ${unknown.join(', ')}`,\n })\n }\n\n // An explicitly named service the environment forbids is refused rather than silently skipped\n // inside the script \u2014 asking for it is a mistake worth surfacing, not absorbing.\n const forbidden = names.filter((name) => {\n const entry = known.get(name)\n\n return entry !== undefined && !isEligible(entry, env)\n })\n\n if (forbidden.length > 0) {\n throw new OperationError(undefined, {\n operation: `deploy to \"${env}\"`,\n remediation: 'drop those services, or deploy them to an environment they allow',\n stderrExcerpt: `not enabled for \"${env}\": ${forbidden.join(', ')} (the scripts would skip them)`,\n })\n }\n\n return names.map((name) => {\n return known.get(name) as DeployService\n })\n}\n\n/** Shape returned to both the CLI and MCP. */\nconst buildResult = (args: {\n env: string\n accountId: string\n names: string[]\n built: BuildEnvResult\n deployed: string[]\n failed: string[]\n dryRun: boolean\n success: boolean\n}) => {\n const { env, accountId, names, built, deployed, failed, dryRun, success } = args\n\n const structuredContent = {\n environment: env,\n accountId,\n services: names,\n deployed,\n failed,\n contract: contractRecord(built.contract),\n strippedVars: built.stripped,\n dryRun,\n success,\n }\n\n return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }\n}\n\n/** How the caller chose services \u2014 decides what the pickers ask, never what the gates allow. */\ntype Selection = 'all' | 'selected'\n\n/**\n * Which service names this run targets.\n *\n * `deploy-all` takes everything the environment accepts; `deploy-selected` takes the `--service`\n * names, or asks when none were given. The eligibility filter has already been applied to\n * `eligible`, so \"all\" here means \"all CI would deploy here\", not \"all on disk\".\n */\nconst resolveNames = async (args: {\n selection: Selection\n eligible: DeployService[]\n service?: string[]\n env: string\n}): Promise<string[]> => {\n const { selection, eligible, service, env } = args\n\n if (selection === 'all') {\n return eligible.map((entry) => {\n return entry.name\n })\n }\n\n if (service && service.length > 0) return service\n\n commandEcho.setInteractive()\n\n return pickServices(eligible, env)\n}\n\n/**\n * The shared body of `local deploy-all` and `local deploy-selected`.\n *\n * Both run the repo's own `devops/scripts/deploy-*.sh` \u2014 the same scripts CI runs \u2014 but supply the\n * build contract the workflow YAML normally provides, and refuse first when the target does not match\n * the AWS account the shell is pointed at. The only difference between the two commands is which\n * services they end up with; every gate applies identically.\n */\nconst runLocalDeploy = async (args: LocalDeployArgs, selection: Selection) => {\n const { env, service, confirmedCommand, dryRun, printEnv } = args\n\n const projectRoot = await getProjectRoot()\n const project = await getRepoName()\n const services = await discoverServices(projectRoot)\n\n if (services.length === 0) {\n throw new OperationError(undefined, {\n operation: 'discover deployable services',\n remediation: 'run this from a repo that has devops/scripts/deploy-*.sh',\n stderrExcerpt: `no deploy scripts found under ${projectRoot}/devops/scripts`,\n })\n }\n\n // Advisory only: `workflow-envs` reads the working tree while a dispatch targets a ref, and vetoing\n // against it once caused a real refuse-to-deploy bug. It seeds the picker; `--env` always wins.\n const protectedEnvAccess = await resolveProtectedEnvAccess()\n const envOptions = deployableEnvs(await readWorkflowEnvOptions(DEPLOY_ALL_WORKFLOW), protectedEnvAccess)\n\n let selectedEnv = env ?? ''\n\n if (!selectedEnv) {\n commandEcho.setInteractive()\n selectedEnv = await pickEnv(envOptions, 'deploy locally')\n }\n\n commandEcho.addOption('--env', selectedEnv)\n\n const eligible = eligibleServices(services, selectedEnv)\n\n if (eligible.length === 0) {\n throw new OperationError(undefined, {\n operation: `deploy to \"${selectedEnv}\"`,\n remediation: 'pick an environment these services allow',\n stderrExcerpt: `no service on disk is enabled for \"${selectedEnv}\"`,\n })\n }\n\n const names = await resolveNames({ selection, eligible, service, env: selectedEnv })\n const chosen = assertServicesUsable({ names, services, env: selectedEnv })\n\n if (selection === 'selected') {\n for (const name of names) {\n commandEcho.addOption('--service', name)\n }\n }\n\n const isShared = isSharedEnv(selectedEnv)\n const branch = await getCurrentBranch()\n const sha = (await $`git rev-parse HEAD`.quiet()).stdout.trim()\n // Resolved once and used twice \u2014 the contract label and the preflight gate must agree about the\n // tree, or a deploy could be refused as dirty while shipping a bundle labelled clean.\n const isClean = await isWorkingTreeClean()\n const built = buildDeployEnv({ env: selectedEnv, branch, sha, isClean })\n\n if (printEnv) {\n logger.info(`Deploy contract for ${selectedEnv}:\\n${formatContract(built)}`)\n }\n\n const preflight = await runPreflight({\n env: selectedEnv,\n project,\n isShared,\n isClean,\n runningEnvs: await runningCiEnvs(),\n protectedEnvAccess,\n })\n\n const accountId = preflight.identity.accountId\n\n if (dryRun) {\n // Deliberately narrow: this rehearses the CLI layer \u2014 target, contract, and the exact commands.\n // It says nothing about whether the deploy itself would succeed, because the scripts are the\n // executor and their AWS calls are not simulated.\n logger.info(\n [\n `Would deploy to \"${selectedEnv}\" (AWS account ${accountId}) from this machine`,\n formatContract(built),\n 'Commands:',\n ...chosen.map((entry) => {\n return ` ${SHELL} ${entry.scriptPath}`\n }),\n '',\n 'Dry run: preflight and contract only \u2014 says nothing about whether the deploy would succeed.',\n ].join('\\n'),\n )\n\n return buildResult({\n env: selectedEnv,\n accountId,\n names,\n built,\n deployed: [],\n failed: [],\n dryRun: true,\n success: true,\n })\n }\n\n if (!confirmedCommand && !(await confirmTarget({ env: selectedEnv, isShared, count: names.length }))) {\n logger.info('Deployment cancelled')\n\n return buildResult({\n env: selectedEnv,\n accountId,\n names,\n built,\n deployed: [],\n failed: [],\n dryRun: false,\n success: false,\n })\n }\n\n if (built.stripped.length > 0) {\n logger.info(`Stripped ambient VITE_* from the build env: ${built.stripped.join(', ')}`)\n }\n\n const { deployed, failed } = await executeDeploys({\n chosen,\n childEnv: built.childEnv,\n cwd: projectRoot,\n env: selectedEnv,\n })\n\n if (failed.length > 0) {\n // No rollback exists here, and none exists in CI either. The honest remedy is redeploying the\n // previous commit, so name it rather than implying the environment is back to a known state.\n logger.error(\n `Partial deploy to \"${selectedEnv}\": succeeded [${deployed.join(', ') || 'none'}], failed [${failed.join(', ')}]. ` +\n `There is no rollback \u2014 redeploy the previous commit to restore it.`,\n )\n\n process.exitCode = 1\n }\n\n commandEcho.print()\n\n return buildResult({\n env: selectedEnv,\n accountId,\n names,\n built,\n deployed,\n failed,\n dryRun: false,\n success: failed.length === 0,\n })\n}\n\n/** Deploy every service this environment accepts, from this machine. */\nexport const localDeployAll = async (args: LocalDeployArgs) => {\n return runLocalDeploy(args, 'all')\n}\n\n/** Deploy a chosen subset of services from this machine. */\nexport const localDeploySelected = async (args: LocalDeployArgs) => {\n return runLocalDeploy(args, 'selected')\n}\n\n/** Shared by both tools \u2014 the local counterpart of what `deploy-all.yml` sets in its `env:` block. */\nconst SHARED_TOOL_NOTE =\n 'Runs the repo\\'s own devops/scripts/deploy-*.sh on THIS machine instead of dispatching CI. Supplies the build contract (VITE_DOMAIN_ENV/BRANCH/COMMIT) that the workflow YAML normally provides and that the scripts do not set themselves, and refuses unless the requested environment matches the AWS account the shell is authenticated to. \"prod\" is refused by default \u2014 it is delivered, not deployed \u2014 and is reachable only if this project sets `protectedEnvs` in infra-kit.json; \"cli-only\" allows it in a terminal but still refuses it here. Services the environment forbids are refused, matching the workflow\\'s own per-service gates. Use dryRun first.'\n\nconst sharedInput = {\n // Shared by BOTH local tools, so this one `.optional()` relaxes `local-deploy-selected` too \u2014 which\n // is what lets an env form reach it. Its `service` stays required deliberately: a services picker\n // there needs an env-dependent domain (`eligibleServices`), which one round trip cannot supply.\n env: z\n .string()\n .optional()\n .describe(\n 'Target environment, e.g. \"dev\" or a personal env like \"arthur\". Omit it to be offered the environments this project may reach.',\n ),\n dryRun: z.boolean().optional().describe('Resolve target, contract and commands without deploying.'),\n confirm: z.boolean().optional().describe('Set true to execute; omit for a dry-run gate.'),\n}\n\nconst sharedOutput = {\n environment: z.string().describe('Environment deployed to'),\n accountId: z.string().describe('AWS account the deploy targeted'),\n services: z.array(z.string()).describe('Services requested'),\n deployed: z.array(z.string()).describe('Services that completed successfully'),\n failed: z.array(z.string()).describe('Services that failed'),\n contract: z.record(z.string(), z.string()).describe('Build contract passed to the scripts'),\n strippedVars: z.array(z.string()).describe('Ambient VITE_* names removed from the build env'),\n dryRun: z.boolean().describe('Whether this was a dry run'),\n success: z.boolean().describe('Whether every requested service deployed'),\n}\n\n/**\n * Both local tools get an `env`-only form, off the SAME `deploy-all.yml` the picker already reads.\n *\n * No `version`: neither tool has one \u2014 a local deploy builds the working tree. And no `services`\n * either, on `-selected` too: its `service` argument stays required because a services picker there\n * needs the env-dependent `eligibleServices` domain, which one round trip cannot supply \u2014 the env is\n * chosen in the same form. `toolName` differs so the `form options empty` log line names which tool\n * had nothing to offer.\n */\nconst localDeployForm = (toolName: string) => {\n return createDeployFormProvider({ workflowFile: DEPLOY_ALL_WORKFLOW, fields: ['env'], toolName })\n}\n\nexport const localDeployAllMcpTool = defineMcpTool({\n name: 'local-deploy-all',\n description: `Deploy EVERY service enabled for an environment, from this machine. ${SHARED_TOOL_NOTE}`,\n requiresHumanConfirm: true,\n formProvider: localDeployForm('local-deploy-all'),\n inputSchema: sharedInput,\n outputSchema: sharedOutput,\n handler: localDeployAll,\n})\n\nexport const localDeploySelectedMcpTool = defineMcpTool({\n name: 'local-deploy-selected',\n description: `Deploy a NAMED SUBSET of services from this machine. ${SHARED_TOOL_NOTE}`,\n requiresHumanConfirm: true,\n formProvider: localDeployForm('local-deploy-selected'),\n inputSchema: {\n ...sharedInput,\n service: z\n .array(z.string())\n .min(1)\n .describe('Service names as in devops/scripts/deploy-<name>.sh, e.g. [\"client-be\"]. Required for MCP.'),\n },\n outputSchema: sharedOutput,\n handler: localDeploySelected,\n})\n", "import { $ } from 'zx'\n\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { assertDeployable } from 'src/lib/workflow-envs'\nimport type { ProtectedEnvAccess } from 'src/lib/workflow-envs'\n\nexport interface AccountIdentity {\n accountId: string\n /** Value of `/{project}/environment` in the authenticated account. */\n stage: string\n}\n\n/**\n * Resolve who we are and which environment this account IS.\n *\n * Every deploy script already reads `/{project}/environment` and trusts it \u2014 `DEPLOY_STAGE` is\n * derived from the account, never passed in. So the account decides the environment, and nothing in\n * the pipeline has ever checked that it matches what the human meant.\n *\n * Throws on an unreadable parameter rather than returning a blank. An account with no such parameter\n * is precisely the \"you are pointed somewhere unexpected\" case this exists to catch, so treating it\n * as \"probably fine\" would inverts the check's purpose.\n */\nexport const resolveAccountIdentity = async (project: string): Promise<AccountIdentity> => {\n const previousQuiet = $.quiet\n\n $.quiet = true\n\n try {\n const parameterName = `/${project}/environment`\n\n const identity = await $`aws sts get-caller-identity --query Account --output text`\n const stage = await $`aws ssm get-parameter --name ${parameterName} --query Parameter.Value --output text`\n\n return { accountId: identity.stdout.trim(), stage: stage.stdout.trim() }\n } catch (error: unknown) {\n throw new OperationError(error, {\n operation: 'resolve the AWS account for this deploy',\n remediation:\n `check that credentials are loaded and that \\`/${project}/environment\\` exists in the ` +\n `account they point at \u2014 run \\`aws sts get-caller-identity\\` to see which account that is`,\n })\n } finally {\n $.quiet = previousQuiet\n }\n}\n\n/**\n * Refuse unless the environment the caller asked for is the one the account actually is.\n *\n * The single check that makes a laptop deploy safe. Credentials live in the shell, so the target is\n * whatever profile happens to be exported \u2014 a stale `AWS_PROFILE` is the ordinary way to deploy to the\n * wrong place, and every script downstream would read the account's own answer and proceed happily.\n *\n * Names both values, because \"wrong environment\" is not actionable and\n * \"you asked for arthur, account 1234 says prod\" is.\n */\nexport const assertEnvMatchesAccount = (intended: string, identity: AccountIdentity): void => {\n if (identity.stage === intended) return\n\n throw new OperationError(undefined, {\n operation: 'verify the target environment',\n remediation: 'switch AWS_PROFILE to the account for this environment, or pass the env this account is',\n stderrExcerpt:\n `you asked for \"${intended}\", but AWS account ${identity.accountId} reports \"${identity.stage}\" ` +\n `\u2014 refusing to deploy to an environment you did not name`,\n })\n}\n\n/**\n * Refuse to deploy a dirty tree to an environment other people use.\n *\n * Shared environments only. A personal env is a scratch target where deploying uncommitted work is\n * the entire point; `dev` is not, and there the cost of being wrong is everyone else's afternoon.\n */\nexport const assertCleanTreeForSharedEnv = (args: { env: string; isShared: boolean; isClean: boolean }): void => {\n const { env, isShared, isClean } = args\n\n if (!isShared || isClean) return\n\n throw new OperationError(undefined, {\n operation: `deploy to shared environment \"${env}\"`,\n // `git stash` alone is NOT enough and saying it was a real bug: `isWorkingTreeClean` is\n // `git status --porcelain`, which counts UNTRACKED files, and plain `stash` leaves those behind \u2014\n // so the refusal printed a fix that left the condition intact. Untracked files genuinely belong\n // in the check: the build reads the filesystem, not git, so an untracked source file does ship.\n remediation: 'commit, or `git stash -u` (plain stash leaves untracked files), or deploy to a personal environment',\n stderrExcerpt: 'the working tree has uncommitted or untracked changes and this environment is shared',\n })\n}\n\n/**\n * Refuse while CI is already deploying to the same environment.\n *\n * `deploy-all.yml` sets `concurrency: group: deploy-all-<env>, cancel-in-progress: true`, which makes\n * CI runs mutually exclusive \u2014 but a local deploy is invisible to that group, so nothing stops the two\n * from interleaving. Serverless would fail loudly on a CloudFormation conflict; an S3 sync racing a\n * CloudFront invalidation would not, and would leave a half-updated bundle.\n *\n * Advisory input: pass `runningEnvs` from `gh run list`. An empty list when `gh` is unavailable means\n * this check passes \u2014 it is a race guard, not a security control, and failing closed on a missing\n * optional tool would make the command unusable offline.\n */\nexport const assertNoCiDeployInFlight = (env: string, runningEnvs: string[]): void => {\n if (!runningEnvs.includes(env)) return\n\n throw new OperationError(undefined, {\n operation: `deploy to \"${env}\"`,\n remediation: 'wait for the CI run to finish, or watch it with `gh run watch`',\n stderrExcerpt: `a CI deploy to \"${env}\" is already in flight \u2014 running both would interleave`,\n })\n}\n\nexport interface PreflightArgs {\n env: string\n project: string\n isShared: boolean\n isClean: boolean\n runningEnvs: string[]\n /** This project's resolved access to delivery-shaped envs. Omitted means denied \u2014 see protected-envs. */\n protectedEnvAccess?: ProtectedEnvAccess\n}\n\nexport interface PreflightResult {\n identity: AccountIdentity\n}\n\n/**\n * Run every gate, in order, before anything is spawned.\n *\n * Ordered cheapest-and-most-absolute first: policy needs no network, the account lookup does. A\n * failure here must leave the world untouched, which is why this runs to completion before the first\n * child process exists.\n */\nexport const runPreflight = async (args: PreflightArgs): Promise<PreflightResult> => {\n const { env, project, isShared, isClean, runningEnvs, protectedEnvAccess } = args\n\n // `prod` is delivered, not deployed ad-hoc \u2014 the one client-side veto, shared with the\n // workflow-dispatch commands so both paths refuse it identically \u2014 unless this project's\n // `protectedEnvs` says otherwise.\n assertDeployable(env, `deploy to \"${env}\"`, protectedEnvAccess)\n\n assertNoCiDeployInFlight(env, runningEnvs)\n\n // Unconditional. `docs/local-deploy-design.md` check 5 says a shared env is NOT skippable, but the\n // implementation used to accept `--skip-preflight clean-tree` for `dev`/`stage` while refusing only\n // delivery-shaped envs \u2014 the one combination the design forbade, and it was reachable. With that\n // corrected the waiver had no remaining effect anywhere (a no-op on personal envs, where the check\n // never fires, and refused everywhere else), so the flag went with it.\n assertCleanTreeForSharedEnv({ env, isShared, isClean })\n\n const identity = await resolveAccountIdentity(project)\n\n assertEnvMatchesAccount(env, identity)\n\n return { identity }\n}\n", "import fs from 'node:fs/promises'\nimport path from 'node:path'\n\nimport { intersectGates, readWorkflowGates } from 'src/lib/workflow-gates'\n\n/** Where both consumer monorepos keep their deploy scripts. The only place we look. */\nconst SCRIPTS_DIR = 'devops/scripts'\n\n/** `deploy-<service>.sh` \u2014 the `lib/` subdir and `e2e-*.sh` are excluded by the shape itself. */\nconst SCRIPT_PATTERN = /^deploy-(?<service>[a-z0-9-]+)\\.sh$/\n\n/**\n * `skip_unless_env_enabled \"$DEPLOY_STAGE\" \"<label>\" \"<space separated envs>\"` \u2014 third arg is the\n * allow-list.\n *\n * Applied per line, to lines already known not to be comments (see {@link readAllowedEnvs}). Matching\n * the whole file at once needed a `[^#\\n]*` prefix to skip comment text, and that prefix next to `\\s+`\n * backtracks super-linearly \u2014 cheaper and clearer to filter the lines first.\n */\nconst GUARD_PATTERN = /skip_unless_env_enabled\\s+\"[^\"]*\"\\s+\"[^\"]*\"\\s+\"(?<envs>[^\"]*)\"/\n\nexport interface DeployService {\n /** `client-be`, `docs-fe`, `media` \u2014 the identity CI already uses as `script_name`. */\n name: string\n /** Absolute path to the script this service runs. */\n scriptPath: string\n /**\n * Environments this service may deploy to, or `null` when it declares no restriction.\n *\n * Read from the script's own `skip_unless_env_enabled` call rather than from the workflow's `if:`\n * conditions. Both encode the same rule, but only the script's copy travels with a local run \u2014 and\n * `deploy-utils.sh` says so explicitly: the guard is \"defense-in-depth alongside the workflow-level\n * env gate: protects manual runs and any other pipeline that wires the script in\".\n */\n allowedEnvs: string[] | null\n}\n\n/**\n * Every deployable service in the current repo, derived from disk.\n *\n * Deliberately NOT a hand-maintained list in `infra-kit.json`. Measured against both consumer repos,\n * globbing is strictly more accurate than the workflows' own `script_name` values in BOTH directions:\n * hulyo's `deploy-selected-services.yml` offers live `ai-ui` and `widgets-fe` checkboxes whose scripts\n * do not exist (ticking either fails the job), while `media` exists on disk with no `script_name` at\n * all because it has a dedicated workflow. Disk is the truth; the YAML has drifted from it.\n *\n * Sorted by name so the picker and `--dry-run` output are stable.\n *\n * @example\n * await discoverServices('/repo') // => [{ name: 'ai-be', \u2026 }, { name: 'backoffice-be', \u2026 }, \u2026]\n */\nexport const discoverServices = async (projectRoot: string): Promise<DeployService[]> => {\n const scriptsDir = path.resolve(projectRoot, SCRIPTS_DIR)\n\n let entries: string[]\n\n try {\n entries = await fs.readdir(scriptsDir)\n } catch {\n return []\n }\n\n // Both sources are read because they drift: travelist gates `mobile` and `media` in the workflow\n // but has no `skip_unless_env_enabled` in either script, so trusting the script alone would let\n // `--all` include services CI refuses.\n const gates = await readWorkflowGates(projectRoot)\n\n const services = await Promise.all(\n entries\n .map((entry) => {\n return { entry, match: SCRIPT_PATTERN.exec(entry) }\n })\n .filter((candidate) => {\n return candidate.match !== null\n })\n .map(async ({ entry, match }) => {\n const scriptPath = path.resolve(scriptsDir, entry)\n const name = match?.groups?.service ?? ''\n\n return {\n name,\n scriptPath,\n allowedEnvs: intersectGates(await readAllowedEnvs(scriptPath), gates.get(name)),\n }\n }),\n )\n\n return services.sort((left, right) => {\n return left.name.localeCompare(right.name)\n })\n}\n\n/**\n * The service's env allow-list, or `null` when it declares none.\n *\n * `null` and `[]` mean opposite things and must not be conflated: `null` is \"no restriction, deploys\n * anywhere\", `[]` would be \"deploys nowhere\". An unreadable script yields `null` \u2014 the same as\n * declaring no guard \u2014 because refusing to list a service on a read error would silently shrink the\n * picker.\n */\nconst readAllowedEnvs = async (scriptPath: string): Promise<string[] | null> => {\n let source: string\n\n try {\n source = await fs.readFile(scriptPath, 'utf-8')\n } catch {\n return null\n }\n\n // Comments are dropped before matching: `deploy-utils.sh` names the function several times in its\n // own docblock, and a consumer script may well explain the guard above the call.\n const envs = source\n .split('\\n')\n .filter((line) => {\n return !line.trimStart().startsWith('#')\n })\n .reduce<string | undefined>((found, line) => {\n return found ?? GUARD_PATTERN.exec(line)?.groups?.envs\n }, undefined)\n\n if (envs === undefined) return null\n\n const parsed = envs.split(/\\s+/).filter(Boolean)\n\n return parsed.length > 0 ? parsed : null\n}\n\n/**\n * Whether a service will actually deploy to an environment.\n *\n * Mirrors `skip_unless_env_enabled`: a service with no allow-list goes anywhere.\n *\n * @example\n * isEligible({ allowedEnvs: ['dev', 'prod'] } as DeployService, 'arthur') // => false\n * isEligible({ allowedEnvs: null } as DeployService, 'arthur') // => true\n */\nexport const isEligible = (service: DeployService, env: string): boolean => {\n return service.allowedEnvs === null || service.allowedEnvs.includes(env)\n}\n\n/**\n * The services `--all` should run for an environment \u2014 i.e. what CI's `deploy-all` would actually do\n * there, not everything on disk.\n *\n * Without this, `--all --env stage` would deploy `docs-fe` and `mobile`, which CI refuses: both repos\n * gate `docs-fe` to dev + the personal envs, `mobile` to dev/prod, and travelist gates `media` to prod\n * alone. Filtering here is what keeps the local run honest against the pipeline it is imitating.\n */\nexport const eligibleServices = (services: DeployService[], env: string): DeployService[] => {\n return services.filter((service) => {\n return isEligible(service, env)\n })\n}\n", "import confirm from '@inquirer/confirm'\nimport input from '@inquirer/input'\nimport select from '@inquirer/select'\nimport process from 'node:process'\nimport { z } from 'zod'\n\nimport { loadJiraConfig } from 'src/integrations/jira'\n// Imported from the leaf module, not the `src/integrations/jira` barrel, and deliberately so:\n// `isJiraApiError` is a pure `instanceof` guard with no I/O. Tests mock that barrel to stub NETWORK\n// calls, and a partial mock drops every export it does not name \u2014 so a barrel import both broke\n// those tests and, worse, would have silently sent a mocked run down the wrong branch.\nimport { isJiraApiError } from 'src/integrations/jira/jira-api-error'\nimport { commandEcho, confirmOrExit } from 'src/lib/command-echo'\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { assertBaseBranchSwitchable, assertCleanCheckout, assertManagementContext } from 'src/lib/git-guard'\nimport { logger } from 'src/lib/logger'\nimport { withEscape } from 'src/lib/prompts/escapable-context'\nimport { InvalidReleaseNameError, displayLabel, validateName } from 'src/lib/release-id'\nimport { createSingleRelease, getBaseBranch, prepareGitForRelease } from 'src/lib/release-utils'\nimport type { ReleaseCreationResult, ReleaseType } from 'src/lib/release-utils'\nimport {\n NoPriorVersionsError,\n computeNextVersion,\n formatReleaseSpec,\n hasNextToken,\n loadExistingVersions,\n parseVersion,\n resolveReleaseEntries,\n} from 'src/lib/version-utils'\nimport type { ReleaseEntry, ReleaseInput, SemVer } from 'src/lib/version-utils'\nimport { defineMcpTool, textContent } from 'src/types'\nimport type { RequiredConfirmedOptionArg } from 'src/types'\n\ninterface ReleaseCreateArgs extends RequiredConfirmedOptionArg {\n releases?: ReleaseInput[]\n}\n\nconst VERSION_PROMPT_HINT = '\"1.2.5\" or \"next\"'\n\nconst trySuggestNext = (known: SemVer[], type: ReleaseType): string | null => {\n try {\n return computeNextVersion(known, type)\n } catch (err) {\n if (err instanceof NoPriorVersionsError) return null\n\n throw err\n }\n}\n\nconst resolveOrExit = (entries: ReleaseInput[], known: SemVer[]): ReleaseEntry[] => {\n try {\n return resolveReleaseEntries(entries, known)\n } catch (err) {\n if (err instanceof NoPriorVersionsError) {\n throw new OperationError(err, {\n operation: 'resolve release version',\n remediation: 'pass an explicit version (e.g. \"1.2.5\") instead of \"next\" when there are no prior versions',\n })\n }\n\n if (err instanceof InvalidReleaseNameError) {\n throw new OperationError(err, {\n operation: 'validate release name',\n remediation:\n 'use a kebab-case name like \"checkout-redesign\" (lowercase, digits, single hyphens, not a reserved word)',\n })\n }\n\n throw err\n }\n}\n\n/** Exported for test only \u2014 the empty-answer fallback below is observable no other way. */\nexport const promptForVersionInput = async (running: SemVer[], type: ReleaseType): Promise<string> => {\n const suggestion = trySuggestNext(running, type)\n const defaultHint = suggestion ? ` [${suggestion}]` : ''\n // The suggestion stays in the MESSAGE and is applied by the empty-answer fallback below \u2014\n // deliberately not inquirer's `default:`, which prefills the editable buffer and would change\n // both what the user sees and what an empty submit returns.\n const versionAnswer = (\n await withEscape(\n (context) => {\n return input({ message: ` Version (e.g. ${VERSION_PROMPT_HINT})${defaultHint}: ` }, context)\n },\n // MCP-unreachable: `releases` is required (min 1) on the release-create tool, so this path is dead there.\n { whenHeadless: 'unreachable' },\n )\n ).trim()\n const versionInput = versionAnswer === '' ? (suggestion ?? '') : versionAnswer\n\n if (versionInput === '') {\n logger.error('No version provided. Exiting...')\n process.exit(1)\n }\n\n return versionInput\n}\n\nconst promptForNameInput = async (): Promise<string> => {\n const name = (\n await withEscape(\n (context) => {\n return input({ message: ' Name (kebab-case, e.g. \"checkout-redesign\"): ' }, context)\n },\n // MCP-unreachable: `releases` is required (min 1) on the release-create tool, so this path is dead there.\n { whenHeadless: 'unreachable' },\n )\n ).trim()\n\n if (name === '') {\n logger.error('No name provided. Exiting...')\n process.exit(1)\n }\n\n try {\n validateName(name)\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err)\n\n logger.error(`${reason} Exiting...`)\n process.exit(1)\n }\n\n return name\n}\n\nconst promptForReleasesInteractive = async (ensureKnown: () => Promise<SemVer[]>): Promise<ReleaseEntry[]> => {\n commandEcho.setInteractive()\n\n let baseKnown: SemVer[] | null = null\n const running: SemVer[] = []\n const ensureRunning = async (): Promise<SemVer[]> => {\n if (baseKnown === null) {\n baseKnown = await ensureKnown()\n running.push(...baseKnown)\n }\n\n return running\n }\n\n const entries: ReleaseEntry[] = []\n let addAnother = true\n\n while (addAnother) {\n const ordinal = entries.length + 1\n const kind = await withEscape(\n (context) => {\n return select<'version' | 'name'>(\n {\n message: `Release #${ordinal} \u2014 version or name?`,\n choices: [\n { name: 'version (semver / next)', value: 'version' },\n { name: 'name (free-form)', value: 'name' },\n ],\n default: 'version',\n },\n context,\n )\n },\n // MCP-unreachable: `releases` is required (min 1) on the release-create tool, so this path is dead there.\n { whenHeadless: 'unreachable' },\n )\n\n const type = await withEscape(\n (context) => {\n return select<ReleaseType>(\n {\n message: `Release #${ordinal} \u2014 select type:`,\n choices: [\n { name: 'regular', value: 'regular' },\n { name: 'hotfix', value: 'hotfix' },\n ],\n default: 'regular',\n },\n context,\n )\n },\n // MCP-unreachable: `releases` is required (min 1) on the release-create tool, so this path is dead there.\n { whenHeadless: 'unreachable' },\n )\n\n let resolved: ReleaseEntry\n\n if (kind === 'name') {\n const name = await promptForNameInput()\n\n resolved = resolveOrExit([{ name, type }], [])[0] as ReleaseEntry\n } else {\n // Versions may need prior versions for \"next\"; load lazily.\n const versionInput = await promptForVersionInput(await ensureRunning(), type)\n\n resolved = resolveOrExit([{ version: versionInput, type }], running)[0] as ReleaseEntry\n\n if (resolved.id.kind === 'version') {\n running.push(parseVersion(`v${resolved.id.raw}`))\n }\n }\n\n const description = (\n await withEscape(\n (context) => {\n return input({ message: ' Description (optional, press Enter to skip): ' }, context)\n },\n // MCP-unreachable: `releases` is required (min 1) on the release-create tool, so this path is dead there.\n { whenHeadless: 'unreachable' },\n )\n ).trim()\n\n entries.push({ ...resolved, ...(description !== '' ? { description } : {}) })\n\n addAnother = await withEscape(\n (context) => {\n return confirm({ message: 'Add another release?', default: false }, context)\n },\n // MCP-unreachable: `releases` is required (min 1) on the release-create tool, so this path is dead there.\n { whenHeadless: 'unreachable' },\n )\n }\n\n return entries\n}\n\nconst formatReleaseSummary = (entry: ReleaseEntry): string => {\n const label = entry.id.kind === 'version' ? `v${entry.id.raw}` : entry.id.name\n const parts = [label, entry.type]\n\n if (entry.description) parts.push(entry.description)\n\n return parts.join(' \u00B7 ')\n}\n\nconst echoReleases = (entries: ReleaseEntry[]): void => {\n // Every entry \u2014 versions, \"next\" (resolved to a concrete version), and named\n // releases alike \u2014 echoes through the single --release flag. formatReleaseSpec\n // produces a spec that parseReleaseSpec parses back into the same entry.\n for (const entry of entries) {\n commandEcho.addOption('--release', formatReleaseSpec(entry))\n }\n}\n\ninterface FailedRelease {\n version: string\n error: string\n}\n\nconst collectEntries = async (\n inputReleases: ReleaseInput[] | undefined,\n ensureKnown: () => Promise<SemVer[]>,\n): Promise<ReleaseEntry[]> => {\n if (inputReleases && inputReleases.length > 0) {\n const known = hasNextToken(inputReleases) ? await ensureKnown() : []\n const resolved = resolveOrExit(inputReleases, known)\n\n echoReleases(resolved)\n\n return resolved\n }\n\n const interactive = await promptForReleasesInteractive(ensureKnown)\n\n echoReleases(interactive)\n\n return interactive\n}\n\n/**\n * Reject a batch that mixes regular and hotfix releases.\n *\n * No longer a technical limit: `prepareGitForRelease` runs per entry, so a mixed\n * batch would switch to each release's own base and work. It is kept because the\n * confirmation prompt summarises the batch as one thing \u2014 an operator approving\n * \"these releases\" should not be silently agreeing to cut some off `dev` and\n * others off `main`. Splitting the invocation makes that choice explicit.\n *\n * Exported for unit testing without running the side-effecting handler.\n */\nexport const assertHomogeneousReleaseType = (entries: ReleaseEntry[]): void => {\n const types = new Set(\n entries.map((entry) => {\n return entry.type\n }),\n )\n\n if (types.size > 1) {\n throw new OperationError(undefined, {\n operation: 'create release',\n remediation: 'create regular and hotfix releases in separate invocations',\n stderrExcerpt: 'mixed regular and hotfix releases in one batch are not supported',\n })\n }\n}\n\nconst confirmReleases = async (entries: ReleaseEntry[], confirmedCommand: boolean): Promise<void> => {\n const summary = entries.map(formatReleaseSummary).join('\\n - ')\n\n await confirmOrExit(confirmedCommand, `Create the following ${entries.length} release(s)?\\n - ${summary}\\n`)\n\n commandEcho.addOption('--yes', true)\n}\n\ninterface ExecuteOneArgs {\n entry: ReleaseEntry\n jiraConfig: Awaited<ReturnType<typeof loadJiraConfig>>\n}\n\nconst executeOne = async (\n args: ExecuteOneArgs,\n): Promise<{ result?: ReleaseCreationResult; failure?: FailedRelease }> => {\n const { entry, jiraConfig } = args\n const label = displayLabel(entry.id)\n const prTitleLabel = entry.id.kind === 'version' ? `v${entry.id.raw}` : entry.id.name\n\n try {\n // Re-asserted per entry, and deliberately inside this `try`. The batch guard ran once,\n // before a confirmation prompt and before every preceding entry's Jira and GitHub round\n // trips, so by the time entry k>1 reaches here its evidence of a clean tree is minutes\n // old. Inside the `try` so a refusal is recorded as this entry's failure and the batch\n // still reports the releases that already succeeded.\n await assertCleanCheckout({ operation: `create release ${prTitleLabel}` })\n\n const baseSha = await prepareGitForRelease(entry.type)\n\n const result = await createSingleRelease({\n id: entry.id,\n jiraConfig,\n description: entry.description,\n type: entry.type,\n baseSha,\n })\n\n logger.info(`\u2705 Successfully created release: ${prTitleLabel} (${entry.type})`)\n logger.info(`\uD83D\uDD17 GitHub PR: ${result.prUrl}`)\n logger.info(`\uD83D\uDD17 Jira Version: ${result.jiraVersionUrl}\\n`)\n\n return { result }\n } catch (error) {\n // `stderrExcerpt` explicitly, because OperationError would otherwise drop the reason entirely:\n // `extractStderr` walks the cause chain for `.stderr`/`.stderrExcerpt` and never falls back to\n // `cause.message`, so a JiraApiError \u2014 which has neither \u2014 rendered as nothing but the generic\n // remediation below. A dead token used to advise \"verify the version or name is unique\", which\n // sent operators to check two things that were both fine.\n const jiraFailure = isJiraApiError(error) ? error : undefined\n\n const err = new OperationError(error, {\n operation: `create release ${prTitleLabel} (${entry.type})`,\n remediation:\n jiraFailure?.kind === 'auth'\n ? 'reissue the Atlassian API token and update JIRA_TOKEN'\n : 'verify the version or name is unique and the base branch is clean',\n stderrExcerpt: jiraFailure?.message,\n })\n\n logger.error(`\u274C ${err.message}\\n`)\n\n return { failure: { version: label, error: err.message } }\n }\n}\n\nconst logFinalSummary = (total: number, successCount: number, failureCount: number): void => {\n if (successCount === total) {\n logger.info(`\u2705 All ${total} release branch(es) were created successfully.`)\n } else if (successCount > 0) {\n logger.warn(`\u26A0\uFE0F ${successCount} of ${total} release branches were created successfully.`)\n logger.warn(`\u274C ${failureCount} release(s) failed.`)\n } else {\n logger.error(`\u274C All ${total} release branch(es) failed to create.`)\n }\n}\n\n/**\n * Create one or more release branches. Each release carries its own type\n * (regular/hotfix) and optional Jira description. All entries in a single\n * invocation must share the same type \u2014 regular and hotfix releases branch off\n * different bases (dev vs main), so mixed batches are rejected and must be\n * created separately.\n */\nexport const releaseCreate = async (args: ReleaseCreateArgs) => {\n const { releases: inputReleases, confirmedCommand } = args\n\n // First statement, matching `release deliver`. Both legs are base-agnostic, so neither\n // needs the entries the wizard is about to collect \u2014 and refusing here means an operator in\n // a linked worktree, or with a dirty tree, is told before they type a version, a type and a\n // description rather than after.\n await assertManagementContext({ operation: 'create release' })\n\n const jiraConfig = await loadJiraConfig()\n\n let known: SemVer[] | null = null\n const ensureKnown = async (): Promise<SemVer[]> => {\n if (known === null) known = await loadExistingVersions()\n\n return known\n }\n\n const entries = await collectEntries(inputReleases, ensureKnown)\n\n if (entries.length === 0) {\n throw new OperationError(undefined, {\n operation: 'create release',\n remediation: 'pass at least one entry in \"releases\" (e.g. [{ version: \"1.2.5\", type: \"regular\" }])',\n stderrExcerpt: 'no releases provided',\n })\n }\n\n assertHomogeneousReleaseType(entries)\n\n // The earliest point at which the base branch is knowable: it is derived from the entries,\n // and the entries are what the wizard above produces. Still ahead of the confirmation\n // prompt and of every mutation, which is the property that matters \u2014 a base branch held by\n // a linked worktree would otherwise surface as a raw `git switch` failure in the middle of\n // a batch, relabelled with advice about version uniqueness.\n await assertBaseBranchSwitchable({\n operation: 'create release',\n base: getBaseBranch((entries[0] as ReleaseEntry).type),\n })\n\n await confirmReleases(entries, Boolean(confirmedCommand))\n\n const created: ReleaseCreationResult[] = []\n const failed: FailedRelease[] = []\n\n for (const entry of entries) {\n const { result, failure } = await executeOne({ entry, jiraConfig })\n\n if (result) created.push(result)\n if (failure) failed.push(failure)\n }\n\n logFinalSummary(entries.length, created.length, failed.length)\n\n commandEcho.print()\n\n const structuredContent = {\n createdBranches: created.map((r) => {\n return r.branchName\n }),\n successCount: created.length,\n failureCount: failed.length,\n releases: created,\n failedReleases: failed,\n }\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n}\n\n// MCP Tool Registration\nexport const releaseCreateMcpTool = defineMcpTool({\n name: 'release-create',\n requiresHumanConfirm: true,\n description:\n 'Create one or more releases in a single call. Each entry in \"releases\" carries EITHER a \"version\" (semver or the literal token \"next\") OR a \"name\" (free-form kebab-case identifier) \u2014 exactly one is required and they are mutually exclusive. Each entry also has its own type (regular|hotfix, default regular) and optional description; all entries in one call must share the same type \u2014 mixed regular+hotfix batches are rejected (create them in separate invocations). For each release this tool switches to the appropriate base branch (dev for regular, main for hotfix), cuts the release branch (release/v<semver> for versions, release/<name> for names), opens a GitHub release PR, and creates the matching Jira fix version (v<semver> for versions, <name> for names). The literal token \"next\" auto-increments from the union of remote release branches and Jira fix versions (regular bumps minor + resets patch; hotfix bumps patch on the highest minor); multiple \"next\" tokens advance sequentially. Named releases never auto-bump and \"next\" is version-only. Must be run from the main repository checkout (not a linked worktree) on the matching base branch with a clean working tree. Confirmation is auto-skipped for MCP calls, so the caller is responsible for gating. Continues on per-release failure and reports successes/failures.',\n inputSchema: {\n releases: z\n .array(\n z\n .object({\n version: z\n .string()\n .optional()\n .describe(\n 'Version to create (e.g., \"1.2.5\") or the literal token \"next\" for auto-increment. Mutually exclusive with \"name\".',\n ),\n name: z\n .string()\n .optional()\n .describe(\n 'Free-form kebab-case release name (e.g., \"checkout-redesign\"). Mutually exclusive with \"version\". Named releases never auto-bump.',\n ),\n type: z\n .enum(['regular', 'hotfix'])\n .optional()\n .default('regular')\n .describe('Release type: \"regular\" (branches off dev) or \"hotfix\" (branches off main).'),\n description: z.string().optional().describe('Optional description for the Jira version.'),\n })\n .refine(\n (entry) => {\n return (entry.version === undefined) !== (entry.name === undefined)\n },\n {\n message: 'Each release entry must have exactly one of \"version\" or \"name\" (they are mutually exclusive).',\n },\n )\n .transform((entry): ReleaseInput => {\n return entry.name !== undefined\n ? { name: entry.name, type: entry.type, ...(entry.description ? { description: entry.description } : {}) }\n : {\n version: entry.version as string,\n type: entry.type,\n ...(entry.description ? { description: entry.description } : {}),\n }\n }),\n )\n .min(1)\n .describe(\n 'One or more releases to create. Each entry has exactly one of \"version\" or \"name\", plus its own type and optional description.',\n ),\n confirm: z\n .boolean()\n .optional()\n .describe('Set true to execute; omit for a dry-run gate that echoes the resolved action.'),\n },\n outputSchema: {\n createdBranches: z.array(z.string()).describe('List of created release branch names'),\n successCount: z.number().describe('Number of releases created successfully'),\n failureCount: z.number().describe('Number of releases that failed'),\n releases: z\n .array(\n z.object({\n version: z.string().describe('Version number'),\n type: z.enum(['regular', 'hotfix']).describe('Release type'),\n branchName: z.string().describe('Release branch name'),\n prUrl: z.string().describe('GitHub PR URL'),\n jiraVersionUrl: z.string().describe('Jira version URL'),\n }),\n )\n .describe('Detailed information for each created release with URLs'),\n failedReleases: z\n .array(\n z.object({\n version: z.string().describe('Version number that failed'),\n error: z.string().describe('Error message'),\n }),\n )\n .describe('List of releases that failed with error messages'),\n },\n handler: releaseCreate,\n})\n", "import input from '@inquirer/input'\nimport { z } from 'zod'\n\nimport { getReleasePRsWithInfo, updateReleasePRBody } from 'src/integrations/gh'\nimport { buildJiraVersionUrl, findVersionByName, loadJiraConfig, updateJiraVersion } from 'src/integrations/jira'\nimport { commandEcho, confirmOrExit } from 'src/lib/command-echo'\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { logger } from 'src/lib/logger'\nimport { withEscape } from 'src/lib/prompts/escapable-context'\nimport { pickReleaseBranch as pickReleaseBranchPrompt } from 'src/lib/prompts/release-picker'\nimport { displayLabel, formatJiraName, parseBranchName } from 'src/lib/release-id'\nimport {\n buildReleasePrBody,\n detectReleaseType,\n formatBranchPickerItems,\n getJiraDescriptions,\n resolveReleaseBranch,\n} from 'src/lib/release-utils'\nimport type { ReleaseType } from 'src/lib/release-utils'\nimport { defineMcpTool, textContent } from 'src/types'\nimport type { RequiredConfirmedOptionArg } from 'src/types'\n\ninterface ReleaseDescEditArgs extends RequiredConfirmedOptionArg {\n version?: string\n description?: string\n}\n\nconst pickReleaseBranch = async (): Promise<{ branch: string; type: ReleaseType }> => {\n const releasePRsInfo = await getReleasePRsWithInfo()\n const branches = releasePRsInfo.map((pr) => {\n return pr.branch\n })\n const types = new Map<string, ReleaseType>(\n releasePRsInfo.map((pr) => {\n return [pr.branch, detectReleaseType(pr.title)]\n }),\n )\n const descriptions = await getJiraDescriptions()\n\n const branch = await pickReleaseBranchPrompt(formatBranchPickerItems({ branches, descriptions, types }))\n\n return { branch, type: types.get(branch) || 'regular' }\n}\n\nconst verifyReleasePRExists = async (selectedBranch: string): Promise<ReleaseType> => {\n const releasePRsInfo = await getReleasePRsWithInfo()\n const prInfo = releasePRsInfo.find((pr) => {\n return pr.branch === selectedBranch\n })\n\n if (!prInfo) {\n throw new OperationError(undefined, {\n operation: `edit description for ${selectedBranch}`,\n remediation: `confirm an open PR exists for ${selectedBranch} ('gh pr list')`,\n })\n }\n\n return detectReleaseType(prInfo.title)\n}\n\n/**\n * Exported for test only. The keep-current semantics below turn on exactly how the answer is\n * whitespace-normalised, which is the detail that changed when this stopped being a zx `question`.\n */\nexport const promptDescription = async (current: string): Promise<string> => {\n const hint = current === '' ? '(no current description)' : `current: \"${current}\"`\n // SINGLE-LINE message on purpose. The hint used to sit on its own line via an embedded `\\n`,\n // which zx wrote raw and inquirer cannot: inquirer re-renders on every keystroke and derives\n // its ANSI erase-line count from the rendered message, so a manual newline miscounts and\n // leaves orphaned prompt fragments on screen. Inlining the hint removes the hazard outright\n // rather than testing around it.\n const answer = await withEscape(\n (context) => {\n return input({ message: ` New description ${hint} (press Enter to keep current): ` }, context)\n },\n // MCP-unreachable: `description` is required on the release-desc-edit tool, so this prompt is dead there.\n { whenHeadless: 'unreachable' },\n )\n // `.trim()`, where the zx version only stripped a trailing newline. Whitespace-only input now\n // means keep-current instead of overwriting the description with blanks.\n const trimmed = answer.trim()\n\n return trimmed === '' ? current : trimmed\n}\n\n/**\n * Edit a release's description in Jira (fix version) and in the matching\n * GitHub release PR body. The PR body is rewritten canonically to\n * `<jiraVersionUrl>\\n\\n<description>` (matching `release-create`).\n */\nexport const releaseDescEdit = async (args: ReleaseDescEditArgs) => {\n const { version: versionArg, description: descriptionArg, confirmedCommand } = args\n\n const jiraConfig = await loadJiraConfig()\n\n let selectedBranch: string\n\n if (versionArg) {\n selectedBranch = resolveReleaseBranch(versionArg)\n await verifyReleasePRExists(selectedBranch)\n } else {\n commandEcho.setInteractive()\n const picked = await pickReleaseBranch()\n\n selectedBranch = picked.branch\n }\n\n // selectedBranch is always a release branch here (operator ref strictly parsed,\n // or picked from discovery-filtered choices), so parseBranchName cannot be null.\n const releaseId = parseBranchName(selectedBranch)\n\n if (!releaseId) {\n throw new OperationError(undefined, {\n operation: `edit description for ${selectedBranch}`,\n remediation: 'pass a version (e.g. \"1.2.5\") or a release name (e.g. \"checkout-redesign\")',\n })\n }\n\n const selectedVersion = displayLabel(releaseId)\n\n commandEcho.addOption('--version', selectedVersion)\n\n // Jira fix version is named by the Jira convention: `v1.2.3` | `<name>`.\n const versionName = formatJiraName(releaseId)\n const jiraVersion = await findVersionByName(versionName, jiraConfig)\n\n if (!jiraVersion) {\n throw new OperationError(undefined, {\n operation: `edit description for ${versionName}`,\n remediation: `create the Jira fix version \"${versionName}\" first or pick a different release`,\n })\n }\n\n const previousDescription = jiraVersion.description ?? ''\n\n let newDescription: string\n\n if (descriptionArg !== undefined) {\n newDescription = descriptionArg\n commandEcho.addOption('--description', newDescription)\n } else {\n commandEcho.setInteractive()\n newDescription = await promptDescription(previousDescription)\n }\n\n const buildResult = (changed: boolean) => {\n const structuredContent = {\n version: selectedVersion,\n branch: selectedBranch,\n jiraVersionUrl: buildJiraVersionUrl(jiraConfig, jiraVersion),\n previousDescription,\n newDescription,\n changed,\n }\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n }\n\n if (newDescription === previousDescription) {\n logger.info(`No change \u2014 description for ${versionName} is already: \"${previousDescription}\"`)\n commandEcho.print()\n\n return buildResult(false)\n }\n\n await confirmOrExit(\n confirmedCommand,\n `Update description for ${versionName}?\\n from: \"${previousDescription}\"\\n to: \"${newDescription}\"\\n`,\n )\n\n commandEcho.addOption('--yes', true)\n\n await updateJiraVersion({ versionId: jiraVersion.id, description: newDescription }, jiraConfig)\n\n const jiraVersionUrl = buildJiraVersionUrl(jiraConfig, jiraVersion)\n const body = buildReleasePrBody(jiraVersionUrl, newDescription)\n\n await updateReleasePRBody({ branch: selectedBranch, body })\n\n logger.info(`\u2705 Updated description for ${versionName}`)\n logger.info(`\uD83D\uDD17 Jira Version: ${jiraVersionUrl}`)\n logger.info(`\uD83D\uDD17 PR branch: ${selectedBranch}\\n`)\n\n commandEcho.print()\n\n return buildResult(true)\n}\n\n// MCP Tool Registration\nexport const releaseDescEditMcpTool = defineMcpTool({\n name: 'release-desc-edit',\n description:\n \"Edit a release's description in Jira and in the matching GitHub release PR body. Accepts a release version or a release name: targets the Jira fix version named `v<version>` (versioned) or `<name>` (named) and the open PR on branch `release/v<version>` or `release/<name>`. The PR body is rewritten canonically to `<jiraVersionUrl>\\\\n\\\\n<description>` \u2014 any prior manual edits to the body are overwritten. Both `version` and `description` are required for MCP calls (the picker/prompt are unreachable without a TTY). Empty `description` clears the description on both sides. Confirmation is auto-skipped for MCP, so the caller is responsible for gating.\",\n inputSchema: {\n version: z\n .string()\n .describe('Accepts a release version (e.g. \"1.2.5\") OR a release name (e.g. \"checkout-redesign\").'),\n description: z.string().describe('New description. Empty string clears the description.'),\n },\n outputSchema: {\n version: z.string().describe('Release version'),\n branch: z.string().describe('Release branch name (e.g. \"release/v1.2.5\" or \"release/checkout-redesign\")'),\n jiraVersionUrl: z.string().describe('Jira fix version URL'),\n previousDescription: z.string().describe('The description before the update'),\n newDescription: z.string().describe('The description after the update'),\n changed: z.boolean().describe('Whether the description actually changed'),\n },\n handler: releaseDescEdit,\n})\n", "import { z } from 'zod'\nimport { $ } from 'zx'\n\nimport { listCmuxWorkspacesByCwd, realpathForCmuxCwd } from 'src/integrations/cmux'\nimport { getReleasePRsWithInfo } from 'src/integrations/gh'\n// Leaf module, not the `src/integrations/gh` barrel \u2014 and the same rule for the two freshly hoisted\n// collaborators below. A hoisted helper's barrel now re-exports a module that imports ITS siblings\n// from their leaves, so a partial `vi.mock` of the barrel intercepts nothing: the real\n// `gh`/`git`/Jira call runs inside what is supposed to be a mocked test. Precedent and the fuller\n// reasoning: `gh-release-deliver.ts:8-12`.\nimport { fetchPRByHead } from 'src/integrations/gh/pr-status'\nimport type { PRStatus } from 'src/integrations/gh/pr-status'\nimport { removeIdeWorktreeFolders } from 'src/integrations/ide'\nimport type { RemoveIdeWorktreeFoldersOutcome } from 'src/integrations/ide/types'\nimport { buildJiraVersionUrl, findVersionByName, loadJiraConfigOptional } from 'src/integrations/jira'\nimport type { JiraConfig, JiraVersion } from 'src/integrations/jira'\nimport { getVersionRelatedIssueCounts, removeJiraVersion } from 'src/integrations/jira/remove-version'\nimport { commandEcho, confirmOrExit } from 'src/lib/command-echo'\nimport { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'\nimport { isCommandDeclined } from 'src/lib/errors/command-declined-error'\nimport { formatZxError } from 'src/lib/errors/format-zx-error'\nimport { isPromptCancellation } from 'src/lib/errors/is-prompt-cancellation'\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport type { OperationErrorContext } from 'src/lib/errors/operation-error'\nimport { assertBaseBranchSwitchable, assertManagementContext } from 'src/lib/git-guard'\nimport {\n branchExists,\n deleteLocalBranch,\n deleteRemoteBranch,\n getCurrentBranch,\n getCurrentWorktrees,\n getProjectRoot,\n lsRemoteHead,\n revParseVerify,\n} from 'src/lib/git-utils'\nimport { getInfraKitConfig } from 'src/lib/infra-kit-config'\nimport { logger } from 'src/lib/logger'\nimport { isMcpMode } from 'src/lib/mcp-mode'\nimport { pickReleaseBranch } from 'src/lib/prompts/release-picker'\nimport { displayLabel, formatJiraName, parseBranchName } from 'src/lib/release-id'\nimport type { ReleaseId } from 'src/lib/release-id'\nimport {\n detectReleaseType,\n formatBranchPickerItems,\n getBaseBranch,\n getJiraDescriptions,\n resolveReleaseBranch,\n} from 'src/lib/release-utils'\nimport { removeReleaseWorktreeIfPresent } from 'src/lib/worktrees/remove-release-worktree'\nimport { defineMcpTool, textContent } from 'src/types'\nimport type { RequiredConfirmedOptionArg } from 'src/types'\n\nexport interface ReleaseRemoveArgs extends RequiredConfirmedOptionArg {\n version?: string\n moveIssuesTo?: string\n skipJira?: boolean\n}\n\n/** Step ids in execution order \u2014 also the vocabulary of the residue report. */\ntype RemovalStep = 'worktree' | 'ide-folders' | 'pr' | 'local-branch' | 'remote-branch' | 'jira'\n\n/**\n * Ordered by ASCENDING irreversibility, so an abort leaves the most repairable residue: a worktree is\n * recreated by `worktrees add`, a closed PR is reopened in the UI, a branch is restored from the tip\n * sha captured in preflight \u2014 while the Jira fix version, once removed, is gone with its issue links.\n */\nconst STEP_ORDER: RemovalStep[] = ['worktree', 'ide-folders', 'pr', 'local-branch', 'remote-branch', 'jira']\n\nconst STEP_LABELS: Record<RemovalStep, string> = {\n worktree: 'remove the worktree',\n 'ide-folders': 'strip the worktree from configured editors',\n pr: 'close the PR',\n 'local-branch': 'delete the local branch',\n 'remote-branch': 'delete the remote branch',\n jira: 'remove the Jira fix version',\n}\n\ninterface DoneStep {\n step: RemovalStep\n /** Rendered in parentheses after the step id in the residue report, when the step has one. */\n detail?: string\n}\n\ninterface JiraPlan {\n version: JiraVersion\n fixCount: number\n affectsCount: number\n url: string\n}\n\ninterface ReleaseRemovePlan {\n branch: string\n id: ReleaseId\n label: string\n baseBranch: string\n projectRoot: string\n worktreeDir: string\n worktreePath: string\n /** Every release worktree, needed by `removeIdeWorktreeFolders` to compute the remaining set. */\n currentWorktrees: string[]\n worktreePresent: boolean\n cmuxWorkspacePresent: boolean\n /** Porcelain paths inside the worktree, when one exists. */\n worktreeDirty: string[] | null\n /**\n * Captured HERE, not parsed from `git branch -D`'s `(was <sha>)` line: `deleteLocalBranch` discards\n * that output, it is a localizable porcelain string, and on a resume run the branch is already gone\n * so there is no delete output to read at all. After steps 4-5 this is the only handle that\n * restores the branch.\n */\n localTipSha: string | null\n remoteTipSha: string | null\n pr: PRStatus | null\n jira: JiraPlan | null\n jiraConfig: JiraConfig | null\n moveIssuesTo: JiraVersion | null\n}\n\ntype IdeStepOutcome = 'verified' | 'attempted' | 'skipped'\n\ninterface IdeProviderReport {\n provider: string\n outcome: IdeStepOutcome\n removed: string[]\n}\n\ninterface IdeFoldersReport {\n outcome: IdeStepOutcome\n providers: IdeProviderReport[]\n}\n\ninterface ReleaseRemoveStructured {\n branch: string\n version: string\n baseBranch: string\n worktree: 'removed' | 'absent'\n ideFolders: IdeFoldersReport\n pr: 'closed' | 'already-closed' | 'absent'\n prNumber: number | null\n localBranch: 'deleted' | 'absent'\n localTipSha: string | null\n remoteBranch: 'deleted' | 'absent'\n remoteTipSha: string | null\n jira: 'removed' | 'absent' | 'skipped' | 'manual'\n jiraVersion: { id: string; name: string; url: string } | null\n}\n\nconst OPERATION = 'remove a release'\n\n/**\n * Log-then-throw for every preflight refusal.\n *\n * `warn`, not `error`: a refused removal is the command doing its job, and the operator still needs\n * the reason in the log when the thrown message is later summarised by an agent. The DECLINE is\n * deliberately not routed through here \u2014 it is not a guard, and `entry/cli.ts` renders it at `error`\n * with exit 1, which is exactly what an operator scripting `release remove && \u2026` needs.\n */\n// The explicit annotation on the CONST, not merely on the arrow, is what makes TypeScript treat a\n// call to this as terminating control flow \u2014 without it every `refuse(...)` guard would leave the\n// value it just rejected still nullable at the call site.\nconst refuse: (context: OperationErrorContext) => never = (context) => {\n logger.warn({ operation: context.operation }, `\u26D4 ${context.stderrExcerpt ?? context.operation}`)\n\n throw new OperationError(undefined, context)\n}\n\n/**\n * MCP has no TTY and the boundary auto-confirms, so `version` cannot come from the picker. The two\n * Jira flags are refused rather than ignored: the Jira step is never attempted over MCP (D7), so\n * accepting `moveIssuesTo` would let an agent report a reassignment that never happened, and\n * `skipJira` would name an exception to a step that does not run. Applied to the shared handler, not\n * only the tool schema, so a direct call cannot slip past it. No-op on the CLI path.\n */\nconst assertMcpRemoveInput = (args: ReleaseRemoveArgs): void => {\n if (!isMcpMode()) return\n\n if (!args.version) {\n refuse({\n operation: OPERATION,\n remediation: 'pass \"version\" (a release version or name); the interactive picker needs a TTY',\n stderrExcerpt: 'release-remove over MCP requires \"version\"',\n })\n }\n\n if (args.moveIssuesTo) {\n refuse({\n operation: OPERATION,\n remediation: 'remove the Jira fix version yourself \u2014 this tool returns its id, name and URL as jira: \"manual\"',\n stderrExcerpt: 'moveIssuesTo is not permitted over MCP: the Jira step is never attempted here',\n })\n }\n\n if (args.skipJira) {\n refuse({\n operation: OPERATION,\n remediation: 'omit skipJira \u2014 over MCP the Jira fix version is always left in place',\n stderrExcerpt: 'skipJira is meaningless over MCP: the Jira step is never attempted here',\n })\n }\n}\n\n/** D3: a merged release has shipped, so its branch records the merge and its fix version is delivered work. */\nconst assertNotMerged = (plan: ReleaseRemovePlan): void => {\n if (plan.pr?.state !== 'MERGED') return\n\n refuse({\n operation: `remove release ${plan.label}`,\n remediation: `this release has shipped \u2014 removing it would delete the branch recording the merge and strip its Jira fix version from delivered work. To undo a delivery, revert the merge commit on ${plan.baseBranch} and cut a new release.`,\n stderrExcerpt: `PR #${plan.pr.number} for ${plan.branch} is MERGED`,\n })\n}\n\n/**\n * D2b/D2c/D2d. Three independent refusals, deliberately not collapsed into one `--force`: a hotfix\n * delivered while Jira was down has an unreleased version with a merged PR, and a version released by\n * hand in the UI has no merged PR, so either guard alone is dodgeable.\n */\nconst assertJiraRemovable = (plan: ReleaseRemovePlan, args: ReleaseRemoveArgs): void => {\n // `--skip-jira` queried Jira for nothing \u2014 no config load, no version lookup, no counts \u2014 so there\n // is no state here to check, and the delete it would guard cannot happen. Querying anyway would\n // break the flag's primary use, which D2d defines as the unconfigured environment where the query\n // itself fails. It follows that D2c is unenforceable on this path by construction, not by choice.\n if (args.skipJira) return\n\n if (!plan.jiraConfig) {\n // The remediation forks because `--skip-jira` is NOT an exit over MCP: `assertMcpRemoveInput`\n // refuses that flag a few lines above, so naming it here would send an agent round a loop \u2014\n // \"pass --skip-jira\" \u2192 \"skipJira is meaningless over MCP\" \u2014 with no way out. That is the same\n // refusal-with-no-exit shape the count guard was scoped to avoid. Jira config is read from\n // `process.env` only, and an MCP server spawned by a host does not inherit an `ik env-load`ed\n // shell, so an unconfigured Jira is an ordinary state there rather than an edge case.\n refuse({\n operation: `remove release ${plan.label}`,\n remediation: isMcpMode()\n ? 'set JIRA_BASE_URL / JIRA_TOKEN / JIRA_PROJECT_ID / JIRA_EMAIL in the environment the MCP server was launched with, or ask a human to run `infra-kit release remove --skip-jira` from a configured shell'\n : 'set JIRA_BASE_URL / JIRA_TOKEN / JIRA_PROJECT_ID / JIRA_EMAIL, or pass --skip-jira to tear down the branch and PR and leave any fix version alone',\n stderrExcerpt: 'Jira is not configured, so a fix version for this release cannot be checked or removed',\n })\n }\n\n if (!plan.jira) return\n\n const { version, fixCount, affectsCount } = plan.jira\n\n // D2c is NOT scoped to the paths that delete, and deliberately so: a released or archived version\n // means the release has shipped and is history rather than scaffolding \u2014 the same class of signal\n // as the MERGED PR guard \u2014 so it refuses the whole teardown on every path, MCP included.\n if (version.released || version.archived) {\n refuse({\n operation: `remove Jira fix version \"${version.name}\"`,\n remediation: 'un-release it in Jira first, or remove the release without it via --skip-jira',\n stderrExcerpt: `fix version \"${version.name}\" is already ${version.released ? 'released' : 'archived'}`,\n })\n }\n\n // The count guard protects the DELETE, so it is scoped to the paths that delete. Over MCP the fix\n // version is never removed (D7) \u2014 the counts guard a mutation that cannot occur, while\n // `--move-issues-to`, the escape hatch this refusal's own message points at, is itself refused on\n // that path. That is a refusal with no exit, protecting nothing. The counts are still reported.\n if (isMcpMode()) return\n\n if (fixCount === 0 && affectsCount === 0) return\n if (plan.moveIssuesTo) return\n\n // The two counts are never summed: they count two different fields and one issue can carry the\n // version in both, so their sum is an upper bound on distinct issues rather than a total.\n refuse({\n operation: `remove Jira fix version \"${version.name}\"`,\n remediation:\n 'deleting it would clear both fields on those issues with no way to restore them \u2014 pass --move-issues-to <version> to reassign both, or clear the version from the issues in Jira first',\n stderrExcerpt: `${version.name} is set as fixVersion on ${fixCount} issue(s) and as affectsVersion on ${affectsCount} issue(s)`,\n })\n}\n\n/**\n * D4's terminal predicate, and the reason this command applies NO target validation to `--version`.\n *\n * Validating the target against discovered worktrees or open PRs \u2014 what `worktrees-remove`'s\n * `assertTargetsExist` does \u2014 would refuse every resume run, because by the second run the worktree\n * is gone by design and the PR is closed. The typo is caught here instead, at the only point where\n * \"this release never existed\" and \"this release is already torn down\" are distinguishable: a\n * release THIS command removed keeps a CLOSED PR forever, so the PR conjunct never fires for it.\n */\nconst assertSomethingExists = (plan: ReleaseRemovePlan): void => {\n const found =\n plan.worktreePresent || plan.pr !== null || plan.localTipSha !== null || plan.remoteTipSha !== null || plan.jira\n\n if (found) return\n\n refuse({\n operation: `remove release ${plan.label}`,\n remediation: `nothing named \"${plan.label}\" exists to remove \u2014 check the spelling against \\`infra-kit release list\\`. (A release this command already removed keeps a CLOSED PR, so a genuine re-run is not affected.)`,\n stderrExcerpt: 'found no worktree, no PR (any state), no local or remote branch, and no Jira fix version',\n })\n}\n\n/**\n * The one detectable failure preflight would otherwise not precede: `removeOne` runs a bare\n * `git worktree remove` and git refuses on modified or untracked files, while `assertCleanCheckout`\n * reads the MAIN checkout's status and cannot see inside a linked worktree.\n */\nconst assertWorktreeClean = (plan: ReleaseRemovePlan): void => {\n if (!plan.worktreeDirty || plan.worktreeDirty.length === 0) return\n\n refuse({\n operation: `remove release ${plan.label}`,\n remediation: `commit or discard those changes (\\`git -C ${plan.worktreePath} stash -u\\`), then re-run`,\n stderrExcerpt: `the worktree at ${plan.worktreePath} has uncommitted changes: ${plan.worktreeDirty.slice(0, 4).join(', ')}`,\n })\n}\n\nconst probeWorktreeDirty = async (worktreePath: string): Promise<string[]> => {\n const result = await $`git -C ${worktreePath} status --porcelain`\n\n return result.stdout\n .split('\\n')\n .map((line) => {\n return line.trimEnd()\n })\n .filter((line) => {\n return line.length > 0\n })\n}\n\n/** Best-effort, and only informational: it exists so the confirm text can warn that a window will close. */\nconst probeCmuxWorkspace = async (worktreePath: string): Promise<boolean> => {\n const byCwd = await listCmuxWorkspacesByCwd()\n\n return byCwd.has(await realpathForCmuxCwd(worktreePath))\n}\n\ninterface JiraPreflight {\n jiraConfig: JiraConfig | null\n jira: JiraPlan | null\n moveIssuesTo: JiraVersion | null\n}\n\n/** `--move-issues-to` is resolved HERE so a typo'd target fails before any mutation, not at step 6. */\nconst buildJiraPreflight = async (id: ReleaseId, args: ReleaseRemoveArgs): Promise<JiraPreflight> => {\n const empty: JiraPreflight = { jiraConfig: null, jira: null, moveIssuesTo: null }\n\n if (args.skipJira) return empty\n\n const jiraConfig = await loadJiraConfigOptional()\n\n if (!jiraConfig) return empty\n\n const version = await findVersionByName(formatJiraName(id), jiraConfig)\n const moveIssuesTo = args.moveIssuesTo ? await resolveMoveTarget(args.moveIssuesTo, jiraConfig) : null\n\n if (!version) return { jiraConfig, jira: null, moveIssuesTo }\n\n const counts = await getVersionRelatedIssueCounts(version.id, jiraConfig)\n\n return {\n jiraConfig,\n moveIssuesTo,\n jira: {\n version,\n fixCount: counts.issuesFixedCount,\n affectsCount: counts.issuesAffectedCount,\n url: buildJiraVersionUrl(jiraConfig, version),\n },\n }\n}\n\nconst resolveMoveTarget = async (name: string, config: JiraConfig): Promise<JiraVersion> => {\n const target = await findVersionByName(name, config)\n\n if (target) return target\n\n return refuse({\n operation: `reassign issues to Jira fix version \"${name}\"`,\n remediation: 'name an existing fix version exactly as it appears in Jira (e.g. \"v1.2.6\")',\n stderrExcerpt: `no Jira fix version named \"${name}\"`,\n })\n}\n\nconst buildPlan = async (branch: string, args: ReleaseRemoveArgs): Promise<ReleaseRemovePlan> => {\n const id = parseBranchName(branch)\n\n if (!id) {\n refuse({\n operation: `remove release ${branch}`,\n remediation: 'pass a version (e.g. \"1.2.5\") or a release name (e.g. \"checkout-redesign\")',\n stderrExcerpt: `\"${branch}\" is not a release branch`,\n })\n }\n\n const projectRoot = await getProjectRoot()\n const worktreeDir = `${projectRoot}${WORKTREES_DIR_SUFFIX}`\n const worktreePath = `${worktreeDir}/${branch}`\n\n const [currentWorktrees, pr, localTipSha, remoteTipSha] = await Promise.all([\n getCurrentWorktrees('release'),\n fetchPRByHead(branch),\n revParseVerify(projectRoot, `refs/heads/${branch}`),\n lsRemoteHead(projectRoot, branch),\n ])\n\n const worktreePresent = currentWorktrees.includes(branch)\n const jiraPreflight = await buildJiraPreflight(id, args)\n\n return {\n ...jiraPreflight,\n branch,\n id,\n label: displayLabel(id),\n // A release with no PR falls to `dev`: `detectReleaseType` reads \"hotfix\" off the PR title and\n // returns 'regular' for everything else, which is the derivation `release create` used.\n baseBranch: getBaseBranch(detectReleaseType(pr?.title ?? '')),\n projectRoot,\n worktreeDir,\n worktreePath,\n currentWorktrees,\n worktreePresent,\n cmuxWorkspacePresent: worktreePresent ? await probeCmuxWorkspace(worktreePath) : false,\n worktreeDirty: worktreePresent ? await probeWorktreeDirty(worktreePath) : null,\n localTipSha,\n remoteTipSha,\n pr,\n }\n}\n\n/** Projection for `logger.debug`: the plan carries live Jira credentials, which must never reach a log. */\nconst describePlan = (plan: ReleaseRemovePlan): Record<string, unknown> => {\n return {\n branch: plan.branch,\n label: plan.label,\n baseBranch: plan.baseBranch,\n worktreePath: plan.worktreePath,\n worktreePresent: plan.worktreePresent,\n worktreeDirty: plan.worktreeDirty,\n cmuxWorkspacePresent: plan.cmuxWorkspacePresent,\n localTipSha: plan.localTipSha,\n remoteTipSha: plan.remoteTipSha,\n pr: plan.pr,\n jiraConfigured: plan.jiraConfig !== null,\n jiraVersion: plan.jira ? { id: plan.jira.version.id, name: plan.jira.version.name } : null,\n jiraFixCount: plan.jira?.fixCount ?? null,\n jiraAffectsCount: plan.jira?.affectsCount ?? null,\n moveIssuesTo: plan.moveIssuesTo ? { id: plan.moveIssuesTo.id, name: plan.moveIssuesTo.name } : null,\n }\n}\n\n// Keyed on the FLAG, not on `jiraConfig === null`. Those coincide only because `assertJiraRemovable`\n// refuses an unconfigured Jira before the confirm is ever rendered; derive the label from the absent\n// config and the day that refusal is relaxed the confirm text starts attributing a flag the operator\n// never passed \u2014 and no test can catch it, because the wrong-label case is unreachable until then.\nconst describeJiraLine = (plan: ReleaseRemovePlan, skipJira: boolean): string => {\n if (skipJira) return 'skipped (--skip-jira)'\n if (!plan.jiraConfig) return 'skipped (Jira not configured)'\n if (!plan.jira) return 'none found'\n\n const { version, fixCount, affectsCount } = plan.jira\n const attached = `fixVersion on ${fixCount} issue(s), affectsVersion on ${affectsCount} issue(s)`\n const move = plan.moveIssuesTo ? `, reassigned to ${plan.moveIssuesTo.name}` : ''\n\n return `${version.name} \u2014 ${attached}${move}`\n}\n\n/**\n * The full inventory, because this is where the operator consents to the one unrecoverable act. It is\n * also why the design needs no separate dry-run mode: everything a plan preview would print is here.\n */\nconst buildConfirmMessage = (plan: ReleaseRemovePlan, skipJira: boolean): string => {\n const prLine = plan.pr ? `#${plan.pr.number} (${plan.pr.state})` : 'none'\n\n const lines = [\n `Remove release ${plan.label} (${plan.branch})? This will:`,\n ` \u2022 worktree: ${plan.worktreePresent ? plan.worktreePath : 'none'}`,\n ` \u2022 PR: ${prLine}`,\n ` \u2022 local branch: ${plan.localTipSha ?? 'absent'} (switching to ${plan.baseBranch} first)`,\n ` \u2022 remote branch: ${plan.remoteTipSha ?? 'absent'}`,\n ` \u2022 Jira version: ${describeJiraLine(plan, skipJira)}`,\n ]\n\n if (plan.cmuxWorkspacePresent) {\n lines.push(` \u2022 the cmux window rooted at ${plan.worktreePath} will close`)\n }\n\n return lines.join('\\n')\n}\n\n/**\n * Tags a step failure with its step id so the handler \u2014 which holds the plan and the completed-step\n * list the residue report needs \u2014 can build the report without every step function knowing about it.\n */\nclass StepFailure extends Error {\n constructor(\n readonly step: RemovalStep,\n readonly reason: unknown,\n ) {\n super(`step ${step} failed`, { cause: reason })\n this.name = 'StepFailure'\n }\n}\n\n/**\n * The reportable outcome of a step result, for the log line.\n *\n * Steps return either a bare enum (`'deleted'`, `'absent'`, `'manual'`) or a record carrying one under\n * `outcome` (the PR step, which also reports its number), so both shapes are read here rather than\n * making every step return a uniform envelope for the sake of one log line.\n */\n/**\n * Every step reports either a bare enum or a record carrying one. Constraining `runStep` to this union\n * rather than reading `unknown` at runtime is what makes an unreportable step a COMPILE error: the\n * alternative had a `'done'` fallback that was unreachable today and could only ever fire on a future\n * step, quietly reintroducing the uninformative label this exists to remove.\n */\ntype StepResult = string | { outcome: string }\n\nconst outcomeOf = (result: StepResult): string => {\n return typeof result === 'string' ? result : result.outcome\n}\n\n/** Abort on failure. Continuing past an unseen failure can reach the unrecoverable step on a state nobody inspected. */\n// A `function` declaration, not the file's usual `const` arrow, and deliberately. The MCP\n// prompt-reachability sweep parses every source with `ScriptKind.TSX`\n// (lib/prompts/__tests__/mcp-reachable-prompt-sites.ts:61), where the `<T>` of a generic ARROW opens a\n// JSX tag: the file then fails to parse, contributes no modules to the graph, and is silently exempted\n// from the Esc contract. `<T,>` fixes the parse but prettier strips the comma straight back out, so the\n// declaration form is the only stable shape. The fail-open is invisible for a non-root module \u2014 ten\n// existing files hit it today \u2014 but an exposed tool is a graph ROOT, so here it surfaces as a failure.\nasync function runStep<T extends StepResult>(step: RemovalStep, done: DoneStep[], fn: () => Promise<T>): Promise<T> {\n try {\n const result = await fn()\n const outcome = outcomeOf(result)\n\n done.push({ step })\n\n // The OUTCOME, not merely the label. \u00A75.5 wants a partial run reconstructable from the log alone,\n // and most steps legitimately do nothing: on a resume every one of them is a no-op, and over MCP\n // the Jira step never removes anything. A bare `\u2705 remove the Jira fix version` in those cases\n // reports a mutation that did not happen \u2014 the same class of lie the post-delete `branchExists`\n // check exists to prevent, moved onto the log surface.\n logger.info({ step, outcome }, `\u2705 ${STEP_LABELS[step]} \u2014 ${outcome}`)\n\n return result\n } catch (error) {\n throw new StepFailure(step, error)\n }\n}\n\n/**\n * The single exemption from abort-on-first-error, and it is safe only because step 2 carries no state\n * into steps 3-6: a cosmetic editor edit must not strand a teardown with the worktree gone, the PR\n * open, the branch live and the fix version live.\n */\n// No success log here, deliberately: `tryStep`'s callback returns void, so this function cannot tell a\n// real editor edit from a no-op \u2014 and `ide-folders` is the step with the MOST to lie about, since an\n// empty `removed` array is evidence of nothing (`describeIdeProvider`). The caller owns the success\n// line because only it holds the computed outcome. The failure path stays here: `attempted` is exactly\n// what this level knows.\nconst tryStep = async (step: RemovalStep, done: DoneStep[], fn: () => Promise<void>): Promise<void> => {\n try {\n await fn()\n done.push({ step })\n } catch (error) {\n done.push({ step, detail: 'attempted' })\n logger.warn({ err: formatZxError(error), step }, `\u26A0\uFE0F ${STEP_LABELS[step]} failed (non-blocking)`)\n }\n}\n\nconst describeDoneStep = (plan: ReleaseRemovePlan, entry: DoneStep): string => {\n if (entry.step === 'local-branch' && plan.localTipSha) return `local-branch (was ${plan.localTipSha.slice(0, 7)})`\n\n return entry.detail ? `${entry.step} (${entry.detail})` : entry.step\n}\n\ninterface ResidueErrorArgs {\n plan: ReleaseRemovePlan\n step: RemovalStep\n done: DoneStep[]\n cause: unknown\n}\n\nconst buildResidueError = (args: ResidueErrorArgs): OperationError => {\n const { plan, step, done, cause } = args\n\n const completed =\n done\n .map((entry) => {\n return describeDoneStep(plan, entry)\n })\n .join(', ') || 'nothing'\n\n const jiraNote = plan.jira ? `The Jira fix version ${plan.jira.version.name} was NOT removed. ` : ''\n const restore = plan.localTipSha ? `Restore the branch with \\`git branch ${plan.branch} ${plan.localTipSha}\\`. ` : ''\n const residue = `completed: ${completed}. ${jiraNote}${restore}Re-run \\`infra-kit release remove --version ${plan.label}\\` to finish; completed steps are skipped.`\n\n // A step can end in a REFUSAL as well as a failure \u2014 the mid-flight PR and issue-count re-probes\n // both throw one \u2014 and a refusal's own remediation is the actionable half (it says what to do\n // about a merged PR, or about counts that moved). `buildMessage` renders only `remediation`, so\n // prepending it is what keeps that text from being swallowed by the residue report.\n const refusal = cause instanceof OperationError ? cause.remediation : undefined\n\n return new OperationError(cause, {\n operation: `remove release ${plan.label} \u2014 step ${STEP_ORDER.indexOf(step) + 1} of ${STEP_ORDER.length} (${STEP_LABELS[step]})`,\n remediation: refusal ? `${refusal} \u2014 ${residue}` : residue,\n })\n}\n\ninterface WorktreeStepResult {\n outcome: 'removed' | 'absent'\n removedBranches: string[]\n}\n\nconst removeWorktreeStep = async (plan: ReleaseRemovePlan): Promise<WorktreeStepResult> => {\n const removed = await removeReleaseWorktreeIfPresent(plan.branch, {\n operation: `remove the worktree for ${plan.branch}`,\n // Deliberately NOT a `--force` suggestion: `worktrees remove` runs bare on purpose, and handing a\n // teardown operator that escape would undo the policy through a shared string.\n remediation: `commit or discard the changes inside ${plan.worktreePath} (\\`git -C ${plan.worktreePath} stash -u\\`), then re-run`,\n })\n\n // Reported from what the helper actually removed rather than from the preflight flag: \"absent\" here\n // means the worktree was already gone, since a genuine refusal throws rather than returning.\n return { outcome: removed.includes(plan.branch) ? 'removed' : 'absent', removedBranches: removed }\n}\n\nconst summariseIdeOutcomes = (outcomes: RemoveIdeWorktreeFoldersOutcome[]): IdeFoldersReport => {\n const providers = outcomes.map((outcome) => {\n return { provider: outcome.provider, outcome: describeIdeProvider(outcome), removed: outcome.removed }\n })\n\n const verified = providers.some((entry) => {\n return entry.outcome === 'verified'\n })\n\n const attempted = providers.some((entry) => {\n return entry.outcome === 'attempted'\n })\n\n const outcome: IdeStepOutcome = attempted ? 'attempted' : 'skipped'\n\n return { outcome: verified ? 'verified' : outcome, providers }\n}\n\n/**\n * Zed is a declared skip on every path \u2014 `allowEditorRelaunch: false` means `removeFromZed` returns\n * before touching anything. Cursor is `verified` only with a non-empty `removed`, because an empty\n * array arrives from three different paths including a CAUGHT write failure, so it is evidence of\n * nothing.\n */\nconst describeIdeProvider = (outcome: RemoveIdeWorktreeFoldersOutcome): IdeStepOutcome => {\n if (outcome.provider === 'zed') return 'skipped'\n\n return outcome.removed.length > 0 ? 'verified' : 'attempted'\n}\n\nconst runIdeFoldersStep = async (\n plan: ReleaseRemovePlan,\n removedWorktrees: string[],\n done: DoneStep[],\n): Promise<IdeFoldersReport> => {\n let report: IdeFoldersReport = { outcome: 'attempted', providers: [] }\n\n await tryStep('ide-folders', done, async () => {\n const outcomes = await removeIdeWorktreeFolders({\n projectRoot: plan.projectRoot,\n worktreeDir: plan.worktreeDir,\n currentWorktrees: plan.currentWorktrees,\n removedWorktrees,\n // `false` ALWAYS, never `!confirmedCommand`. `zed --reuse` replaces the focused window's whole\n // folder set and silently drops folders opened for unrelated work, and it reports no diff \u2014 so\n // on the `--yes` path `!confirmedCommand` would make this leg a guaranteed no-op reported as\n // success, and on the interactive path it would destroy state nobody consented to losing.\n // Cursor never reads the flag, so it still does its surgical, verifiable work either way.\n allowEditorRelaunch: false,\n })\n\n report = summariseIdeOutcomes(outcomes)\n })\n\n // Logged HERE rather than inside `tryStep`, which cannot see this: `skipped` and `verified` are the\n // difference between \"nothing was open to change\" and \"a Cursor workspace was rewritten\", and the\n // bare label rendered both identically \u2014 the same lie as an unverified branch delete.\n logger.info({ outcome: report.outcome, step: 'ide-folders' }, `\u2705 ${STEP_LABELS['ide-folders']} \u2014 ${report.outcome}`)\n\n return report\n}\n\ninterface PrStepResult {\n outcome: 'closed' | 'already-closed' | 'absent'\n number: number | null\n}\n\nconst closePrStep = async (plan: ReleaseRemovePlan): Promise<PrStepResult> => {\n // Re-probe: preflight's answer is separated from this call by an interactive confirm of unbounded\n // duration. A PR merged in that window would otherwise fail here as an opaque `gh pr close` error,\n // after the worktree is already gone.\n const current = await fetchPRByHead(plan.branch)\n\n assertNotMerged({ ...plan, pr: current })\n\n if (!current) return { outcome: 'absent', number: null }\n if (current.state === 'CLOSED') return { outcome: 'already-closed', number: current.number }\n\n // Closed explicitly rather than as a side effect of deleting the head branch: an inferred close\n // carries no explanation, and `--delete-branch` would also delete the local branch, which fails\n // while a worktree holds it.\n const comment = `Closed by \\`infra-kit release remove\\`: release ${plan.label} was torn down.`\n\n await $`gh pr close ${String(current.number)} --comment ${comment}`\n\n return { outcome: 'closed', number: current.number }\n}\n\nconst deleteLocalBranchStep = async (plan: ReleaseRemovePlan): Promise<'deleted' | 'absent'> => {\n // `deleteLocalBranch` silently no-ops on the current branch, so the switch has to come first or the\n // command reports a deletion it never performed.\n if ((await getCurrentBranch()) === plan.branch) {\n await $`git switch ${plan.baseBranch}`\n }\n\n await deleteLocalBranch(plan.branch)\n\n // ...and the delete is checked rather than assumed, which converts that fail-open into a fail-closed.\n if (await branchExists(plan.branch)) {\n return refuse({\n operation: `delete local branch ${plan.branch}`,\n remediation: `delete it by hand with \\`git branch -D ${plan.branch}\\`, then re-run to finish the removal`,\n stderrExcerpt: `${plan.branch} still exists after the delete`,\n })\n }\n\n return plan.localTipSha ? 'deleted' : 'absent'\n}\n\nconst deleteRemoteBranchStep = async (plan: ReleaseRemovePlan): Promise<'deleted' | 'absent'> => {\n await deleteRemoteBranch(plan.branch)\n\n return plan.remoteTipSha ? 'deleted' : 'absent'\n}\n\n/**\n * The preflight guard passed before the confirm and five mutations across two remote systems. A\n * teammate attaching a ticket to this fix version in that window \u2014 the ordinary workflow of a team\n * filing against an open release \u2014 would otherwise be overwritten by a guard that is already stale.\n * Unchanged counts proceed silently; a change aborts BEFORE the one call that cannot be undone.\n */\nconst assertIssueCountsUnchanged = async (\n plan: ReleaseRemovePlan,\n jira: JiraPlan,\n config: JiraConfig,\n): Promise<void> => {\n const counts = await getVersionRelatedIssueCounts(jira.version.id, config)\n\n if (counts.issuesFixedCount === jira.fixCount && counts.issuesAffectedCount === jira.affectsCount) return\n\n refuse({\n operation: `remove Jira fix version \"${jira.version.name}\"`,\n remediation: `the fix version was NOT removed and every other step is complete \u2014 re-run \\`infra-kit release remove --version ${plan.label}\\` so the guard re-evaluates the new counts`,\n stderrExcerpt: `issue counts changed since preflight: fixVersion ${jira.fixCount} \u2192 ${counts.issuesFixedCount}, affectsVersion ${jira.affectsCount} \u2192 ${counts.issuesAffectedCount}`,\n })\n}\n\nconst removeJiraStep = async (\n plan: ReleaseRemovePlan,\n args: ReleaseRemoveArgs,\n): Promise<ReleaseRemoveStructured['jira']> => {\n if (args.skipJira) return 'skipped'\n if (!plan.jira || !plan.jiraConfig) return 'absent'\n\n // The one irreversible step stays human-only: an agent can undo every reversible artefact it\n // created and can never reach this call. The version's id, name and URL are returned instead, so a\n // human has one link to finish the job.\n if (isMcpMode()) {\n logger.info(\n { jiraVersionId: plan.jira.version.id, jiraVersionName: plan.jira.version.name, url: plan.jira.url },\n '\u2139\uFE0F Jira fix version left in place for a human to remove',\n )\n\n return 'manual'\n }\n\n await assertIssueCountsUnchanged(plan, plan.jira, plan.jiraConfig)\n\n // Logged BEFORE the delete: afterwards the id is the only handle that still exists, and it is what\n // an Atlassian support request needs.\n logger.info(\n { jiraVersionId: plan.jira.version.id, jiraVersionName: plan.jira.version.name },\n '\uD83D\uDDD1\uFE0F removing Jira fix version',\n )\n\n await removeJiraVersion(\n {\n versionId: plan.jira.version.id,\n // ONE flag, BOTH parameters: mapping it to `moveFixIssuesTo` alone would lose the affects links\n // through the very flag that exists to prevent data loss, and would leave a version with only\n // affects links permanently unremovable.\n moveFixIssuesTo: plan.moveIssuesTo?.id,\n moveAffectedIssuesTo: plan.moveIssuesTo?.id,\n },\n plan.jiraConfig,\n )\n\n return 'removed'\n}\n\ninterface StepResults {\n worktree: WorktreeStepResult\n ideFolders: IdeFoldersReport\n pr: PrStepResult\n localBranch: 'deleted' | 'absent'\n remoteBranch: 'deleted' | 'absent'\n jira: ReleaseRemoveStructured['jira']\n}\n\nconst buildResult = (plan: ReleaseRemovePlan, results: StepResults): ReleaseRemoveStructured => {\n return {\n branch: plan.branch,\n version: plan.label,\n baseBranch: plan.baseBranch,\n worktree: results.worktree.outcome,\n ideFolders: results.ideFolders,\n pr: results.pr.outcome,\n prNumber: results.pr.number,\n localBranch: results.localBranch,\n localTipSha: plan.localTipSha,\n remoteBranch: results.remoteBranch,\n remoteTipSha: plan.remoteTipSha,\n jira: results.jira,\n jiraVersion: plan.jira ? { id: plan.jira.version.id, name: plan.jira.version.name, url: plan.jira.url } : null,\n }\n}\n\nconst runSteps = async (plan: ReleaseRemovePlan, args: ReleaseRemoveArgs, done: DoneStep[]): Promise<StepResults> => {\n const worktree = await runStep('worktree', done, () => {\n return removeWorktreeStep(plan)\n })\n\n const ideFolders = await runIdeFoldersStep(plan, worktree.removedBranches, done)\n\n const pr = await runStep('pr', done, () => {\n return closePrStep(plan)\n })\n\n // Both shas, before the two steps that make them the only remaining handle on the branch.\n logger.info(\n { branch: plan.branch, localTipSha: plan.localTipSha, remoteTipSha: plan.remoteTipSha },\n '\uD83D\uDD16 branch tips before deletion',\n )\n\n const localBranch = await runStep('local-branch', done, () => {\n return deleteLocalBranchStep(plan)\n })\n\n const remoteBranch = await runStep('remote-branch', done, () => {\n return deleteRemoteBranchStep(plan)\n })\n\n const jira = await runStep('jira', done, () => {\n return removeJiraStep(plan, args)\n })\n\n return { worktree, ideFolders, pr, localBranch, remoteBranch, jira }\n}\n\nconst executeRemoval = async (plan: ReleaseRemovePlan, args: ReleaseRemoveArgs): Promise<ReleaseRemoveStructured> => {\n const done: DoneStep[] = []\n\n try {\n return buildResult(plan, await runSteps(plan, args, done))\n } catch (error) {\n if (error instanceof StepFailure) throw buildResidueError({ plan, step: error.step, done, cause: error.reason })\n\n throw error\n }\n}\n\n/**\n * `assertInteractive`'s remediation is shared across four commands and names `--versions` and\n * `--all`, both of which this single-target command rejects as unknown flags \u2014 so its refusal is\n * re-thrown with a `--version`-only remediation rather than editing the shared helper.\n */\nconst resolveTargetBranch = async (version?: string): Promise<string> => {\n if (version) return resolveReleaseBranch(version)\n\n commandEcho.setInteractive()\n\n const [descriptions, prInfo] = await Promise.all([getJiraDescriptions(), getReleasePRsWithInfo()])\n\n const branches = prInfo.map((pr) => {\n return pr.branch\n })\n\n if (branches.length === 0) {\n refuse({\n operation: 'select a release to remove',\n remediation: 'pass `--version <ref>` to remove a release that is already partially torn down',\n stderrExcerpt: 'no open release PRs to pick from',\n })\n }\n\n const types = new Map(\n prInfo.map((pr) => {\n return [pr.branch, detectReleaseType(pr.title)] as const\n }),\n )\n\n try {\n return await pickReleaseBranch(formatBranchPickerItems({ branches, descriptions, types }))\n } catch (error) {\n if (isPromptCancellation(error)) throw error\n\n throw new OperationError(error, {\n operation: 'select a release to remove',\n remediation: 'pass `--version <ref>` (e.g. `--version 1.2.5`); the picker needs an interactive terminal',\n })\n }\n}\n\n/**\n * Tear down one release: its worktree, editor entry, PR, both branches and its Jira fix version.\n *\n * Single-target by design (no `--versions`, no `--all`): nothing this removes is recreatable, and\n * multi-target would force continue-on-error semantics that destroy the residue report.\n */\nexport const releaseRemove = async (options: ReleaseRemoveArgs) => {\n const { confirmedCommand, version, moveIssuesTo, skipJira } = options\n\n // GUARD ORDER is load-bearing and mirrors `worktreesRemove`, whose own tests pin it.\n // First, so a caller standing inside the release's linked worktree gets worktree advice rather\n // than a config error.\n await assertManagementContext({ operation: OPERATION })\n\n // ABOVE the `try` whose catch rewraps: this throws a plain `Error` whose text `buildMessage` would\n // drop. BELOW the guard above: \"not an infra-kit project\" is the less fundamental failure.\n await getInfraKitConfig()\n\n assertMcpRemoveInput(options)\n\n try {\n const plan = await buildPlan(await resolveTargetBranch(version), options)\n\n logger.debug({ plan: describePlan(plan) }, 'release remove: resolved plan')\n\n assertNotMerged(plan)\n assertJiraRemovable(plan, options)\n assertSomethingExists(plan)\n await assertBaseBranchSwitchable({ operation: `remove release ${plan.label}`, base: plan.baseBranch })\n assertWorktreeClean(plan)\n\n commandEcho.addOption('--version', plan.label)\n if (moveIssuesTo) commandEcho.addOption('--move-issues-to', moveIssuesTo)\n if (skipJira) commandEcho.addOption('--skip-jira', true)\n\n // `throwOnDecline` because the default `process.exit(0)` would make a decline indistinguishable\n // from a legitimate \"already fully removed, every step skipped\" success \u2014 and an operator\n // scripting `release remove && \u2026` would proceed as though a release had been torn down.\n // It also makes the \"acquire nothing before the confirm\" ordering above load-bearing.\n await confirmOrExit(confirmedCommand, buildConfirmMessage(plan, Boolean(skipJira)), { throwOnDecline: true })\n\n if (!confirmedCommand) {\n commandEcho.addOption('--yes', true)\n }\n\n const structuredContent = await executeRemoval(plan, options)\n\n commandEcho.print()\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n } catch (error) {\n // A cancelled prompt (Esc / Ctrl-C) is \"I never answered\" and exits 0 at the boundary; a decline\n // is \"I answered, and the answer was no\" and must reach `entry/cli.ts` intact to exit 1. Neither\n // is a failure, so neither gets the generic wrap.\n if (isPromptCancellation(error) || isCommandDeclined(error)) throw error\n\n // Guard refusals and residue reports already carry an actionable remediation.\n if (error instanceof OperationError) throw error\n\n logger.error({ err: formatZxError(error) }, '\u274C Error removing release')\n throw new OperationError(error, {\n operation: OPERATION,\n remediation: 'check `gh auth status`, `git worktree list` and the Jira credentials, then re-run',\n })\n }\n}\n\nexport const releaseRemoveMcpTool = defineMcpTool({\n name: 'release-remove',\n description:\n 'Tear down ONE release created by release-create: removes its git worktree (and the cmux window rooted there), strips the worktree from the Cursor workspace, closes its pull request with a comment, and deletes the release branch locally and on origin. THE JIRA FIX VERSION IS DELIBERATELY LEFT IN PLACE \u2014 removing it is irreversible (new id, new URL, lost issue links), so this tool never attempts it and instead returns jira: \"manual\" with the version id, name and URL for a human to finish in Jira. Requires \"version\"; the moveIssuesTo/skipJira flags are CLI-only and are refused here because the Jira step does not run. Refuses before any mutation when the PR is MERGED (the release has shipped), when the fix version is released/archived or still carries issues, or when nothing named that version exists. Resumable: every step is a verified no-op when its artefact is already gone, so a re-run finishes a partial teardown. What is lost with the worktree directory: gitignored contents including a hydrated .env of Doppler secrets (re-fetch with env-load) and node_modules/dist.',\n requiresHumanConfirm: true,\n inputSchema: {\n version: z\n .string()\n .describe(\n 'The release version or name to remove (e.g. \"1.2.5\" or \"checkout-redesign\"). Required: the interactive picker is unavailable without a TTY.',\n ),\n confirm: z\n .boolean()\n .optional()\n .describe('Set true to execute; omit for a dry-run gate that echoes the resolved action.'),\n },\n outputSchema: {\n branch: z.string().describe('The release branch that was targeted'),\n version: z.string().describe('The release label (e.g. \"1.2.5\")'),\n baseBranch: z.string().describe('The branch HEAD was moved to before the local delete'),\n worktree: z.enum(['removed', 'absent']).describe('Whether a git worktree was removed or none existed'),\n ideFolders: z\n .object({\n outcome: z\n .enum(['verified', 'attempted', 'skipped'])\n .describe('\"verified\" only when an editor reported a real diff; Zed is always a declared skip'),\n providers: z.array(\n z.object({\n provider: z.string(),\n outcome: z.enum(['verified', 'attempted', 'skipped']),\n removed: z.array(z.string()).describe('Folder paths confirmed removed; always empty for Zed'),\n }),\n ),\n })\n .describe('Per-editor outcome of stripping the worktree folder'),\n pr: z.enum(['closed', 'already-closed', 'absent']).describe('What happened to the release pull request'),\n prNumber: z.number().nullable().describe('The pull request number, or null when none exists'),\n localBranch: z.enum(['deleted', 'absent']).describe('Whether a local branch was deleted or none existed'),\n localTipSha: z.string().nullable().describe('The local tip before deletion \u2014 the handle that restores the branch'),\n remoteBranch: z.enum(['deleted', 'absent']).describe('Whether the origin branch was deleted or none existed'),\n remoteTipSha: z.string().nullable().describe('The origin tip before deletion'),\n jira: z\n .enum(['removed', 'absent', 'skipped', 'manual'])\n .describe('Always \"manual\" over MCP: the fix version is left for a human. \"absent\" means none was found.'),\n jiraVersion: z\n .object({ id: z.string(), name: z.string(), url: z.string() })\n .nullable()\n .describe('The fix version this release owns \u2014 the link a human needs when jira is \"manual\"'),\n },\n handler: releaseRemove,\n})\n", "import fs from 'node:fs/promises'\nimport path from 'node:path'\n\ninterface AddFoldersToCursorWorkspaceArgs {\n workspacePath: string\n folderPaths: string[]\n}\n\ninterface AddFoldersToCursorWorkspaceResult {\n added: string[]\n skipped: string[]\n}\n\ninterface WorkspaceFolderEntry {\n path: string\n name?: string\n}\n\ninterface WorkspaceFile {\n folders?: WorkspaceFolderEntry[]\n [key: string]: unknown\n}\n\n/**\n * Adds folders to a Cursor (`.code-workspace`) file's `folders` array, skipping\n * entries that already point to the same absolute path. Folder paths are written\n * as relative to the workspace file's directory to match Cursor's default style.\n */\nexport const addFoldersToCursorWorkspace = async (\n args: AddFoldersToCursorWorkspaceArgs,\n): Promise<AddFoldersToCursorWorkspaceResult> => {\n const { workspacePath, folderPaths } = args\n\n const workspaceDir = path.dirname(workspacePath)\n\n let raw: string\n\n try {\n raw = await fs.readFile(workspacePath, 'utf-8')\n } catch (error) {\n throw new Error(`Cursor workspace file not found at ${workspacePath}: ${(error as Error).message}`)\n }\n\n let parsed: WorkspaceFile\n\n try {\n parsed = JSON.parse(raw) as WorkspaceFile\n } catch (error) {\n throw new Error(\n `Failed to parse ${workspacePath} as JSON. Comments (JSONC) are not supported. ${(error as Error).message}`,\n )\n }\n\n const existingFolders = parsed.folders ?? []\n const existingAbsolutePaths = new Set(\n existingFolders.map((entry) => {\n return path.resolve(workspaceDir, entry.path)\n }),\n )\n\n const added: string[] = []\n const skipped: string[] = []\n\n for (const folderPath of folderPaths) {\n const absolutePath = path.resolve(folderPath)\n\n if (existingAbsolutePaths.has(absolutePath)) {\n skipped.push(folderPath)\n continue\n }\n\n const relativePath = path.relative(workspaceDir, absolutePath)\n\n existingFolders.push({ path: relativePath })\n existingAbsolutePaths.add(absolutePath)\n added.push(folderPath)\n }\n\n parsed.folders = existingFolders\n\n await fs.writeFile(workspacePath, `${JSON.stringify(parsed, null, 2)}\\n`, 'utf-8')\n\n return { added, skipped }\n}\n", "import { $ } from 'zx'\n\n/**\n * Spawns Cursor against a `.code-workspace` file (or any path). Centralised here\n * \u2014 rather than inlined at each call site \u2014 so the editor spawn lives inside the\n * cursor provider module and is covered by a single `vi.mock('src/integrations/cursor')`.\n * Keeping it out of the IDE-orchestration layer is what stops tests that mock the\n * provider (but not `zx`) from shelling out to a real editor.\n */\nexport const launchCursor = async (workspacePath: string): Promise<void> => {\n await $`cursor ${workspacePath}`\n}\n", "import fs from 'node:fs/promises'\nimport path from 'node:path'\n\nimport { addFoldersToCursorWorkspace } from './add-folders-to-workspace'\nimport { removeFoldersFromCursorWorkspace } from './remove-folders-from-workspace'\n\ninterface ReconcileCursorWorkspaceFoldersArgs {\n workspacePath: string\n worktreeDir: string\n currentBranches: string[]\n}\n\ninterface ReconcileCursorWorkspaceFoldersResult {\n added: string[]\n removed: string[]\n}\n\ninterface WorkspaceFolderEntry {\n path: string\n name?: string\n}\n\ninterface WorkspaceFile {\n folders?: WorkspaceFolderEntry[]\n [key: string]: unknown\n}\n\n/**\n * Reconciles the configured Cursor workspace's `folders` array against the\n * actual set of release worktrees on disk:\n * - Adds any worktree folders that aren't already listed.\n * - Removes entries whose absolute path lives under `${worktreeDir}/release/`\n * but no longer corresponds to a current branch (drift from manual\n * `git worktree remove`, deleted branches, etc.).\n *\n * Non-worktree folder entries are left untouched.\n */\nexport const reconcileCursorWorkspaceFolders = async (\n args: ReconcileCursorWorkspaceFoldersArgs,\n): Promise<ReconcileCursorWorkspaceFoldersResult> => {\n const { workspacePath, worktreeDir, currentBranches } = args\n\n const workspaceDir = path.dirname(workspacePath)\n const releaseRoot = path.resolve(`${worktreeDir}/release`)\n\n const raw = await fs.readFile(workspacePath, 'utf-8')\n const parsed = JSON.parse(raw) as WorkspaceFile\n\n const existingFolders = parsed.folders ?? []\n\n const desiredAbsolutePaths = new Set(\n currentBranches.map((branch) => {\n return path.resolve(`${worktreeDir}/${branch}`)\n }),\n )\n\n const danglingFolderPaths: string[] = []\n\n for (const entry of existingFolders) {\n const entryAbsolutePath = path.resolve(workspaceDir, entry.path)\n\n const isReleaseShaped = entryAbsolutePath === releaseRoot || entryAbsolutePath.startsWith(`${releaseRoot}/`)\n\n if (isReleaseShaped && !desiredAbsolutePaths.has(entryAbsolutePath)) {\n danglingFolderPaths.push(entryAbsolutePath)\n }\n }\n\n let removed: string[] = []\n\n if (danglingFolderPaths.length > 0) {\n const result = await removeFoldersFromCursorWorkspace({\n workspacePath,\n folderPaths: danglingFolderPaths,\n })\n\n removed = result.removed\n }\n\n const desiredFolderPaths = currentBranches.map((branch) => {\n return `${worktreeDir}/${branch}`\n })\n\n const { added } =\n desiredFolderPaths.length > 0\n ? await addFoldersToCursorWorkspace({ workspacePath, folderPaths: desiredFolderPaths })\n : { added: [] as string[] }\n\n return { added, removed }\n}\n", "import fs from 'node:fs/promises'\nimport path from 'node:path'\n\ninterface RemoveFoldersFromCursorWorkspaceArgs {\n workspacePath: string\n folderPaths: string[]\n}\n\ninterface RemoveFoldersFromCursorWorkspaceResult {\n removed: string[]\n notFound: string[]\n}\n\ninterface WorkspaceFolderEntry {\n path: string\n name?: string\n}\n\ninterface WorkspaceFile {\n folders?: WorkspaceFolderEntry[]\n [key: string]: unknown\n}\n\n/**\n * Removes folders from a Cursor (`.code-workspace`) file's `folders` array. Entries\n * are matched by resolved absolute path, so relative and absolute entries pointing\n * at the same folder are both removed.\n */\nexport const removeFoldersFromCursorWorkspace = async (\n args: RemoveFoldersFromCursorWorkspaceArgs,\n): Promise<RemoveFoldersFromCursorWorkspaceResult> => {\n const { workspacePath, folderPaths } = args\n\n const workspaceDir = path.dirname(workspacePath)\n\n let raw: string\n\n try {\n raw = await fs.readFile(workspacePath, 'utf-8')\n } catch (error) {\n throw new Error(`Cursor workspace file not found at ${workspacePath}: ${(error as Error).message}`)\n }\n\n let parsed: WorkspaceFile\n\n try {\n parsed = JSON.parse(raw) as WorkspaceFile\n } catch (error) {\n throw new Error(\n `Failed to parse ${workspacePath} as JSON. Comments (JSONC) are not supported. ${(error as Error).message}`,\n )\n }\n\n const existingFolders = parsed.folders ?? []\n const targetAbsolutePaths = new Set(\n folderPaths.map((folderPath) => {\n return path.resolve(folderPath)\n }),\n )\n\n const removedAbsolutePaths = new Set<string>()\n\n const filteredFolders = existingFolders.filter((entry) => {\n const entryAbsolutePath = path.resolve(workspaceDir, entry.path)\n\n if (targetAbsolutePaths.has(entryAbsolutePath)) {\n removedAbsolutePaths.add(entryAbsolutePath)\n\n return false\n }\n\n return true\n })\n\n parsed.folders = filteredFolders\n\n await fs.writeFile(workspacePath, `${JSON.stringify(parsed, null, 2)}\\n`, 'utf-8')\n\n const removed: string[] = []\n const notFound: string[] = []\n\n for (const folderPath of folderPaths) {\n const absolutePath = path.resolve(folderPath)\n\n if (removedAbsolutePaths.has(absolutePath)) {\n removed.push(folderPath)\n } else {\n notFound.push(folderPath)\n }\n }\n\n return { removed, notFound }\n}\n", "import path from 'node:path'\n\n/**\n * Resolves the configured Cursor workspace path against the project root.\n * Absolute paths are returned unchanged.\n */\nexport const resolveCursorWorkspacePath = (configValue: string, projectRoot: string): string => {\n if (path.isAbsolute(configValue)) {\n return configValue\n }\n\n return path.resolve(projectRoot, configValue)\n}\n", "import { logger } from 'src/lib/logger'\n\nimport { launchCursor } from './launch-cursor'\nimport { reconcileCursorWorkspaceFolders } from './reconcile-workspace-folders'\nimport { resolveCursorWorkspacePath } from './resolve-workspace-path'\n\ninterface OpenCursorWorkspaceArgs {\n projectRoot: string\n worktreeDir: string\n currentBranches: string[]\n /**\n * The Cursor entry's config, selected by the facade from the (possibly\n * multi-IDE) infra-kit config. Passed in rather than re-read here so this\n * opener acts on the exact entry the caller chose \u2014 essential once `ide` can\n * be an array of providers.\n */\n cursorConfig: { workspaceConfigPath: string }\n}\n\ninterface OpenCursorWorkspaceOutcome {\n ran: boolean\n added: number\n removed: number\n}\n\n/**\n * Reconciles the configured Cursor `.code-workspace` `folders` array against the\n * set of release worktrees on disk, then launches Cursor against it (the folder\n * reconcile still runs when there are no worktrees, but the relaunch is skipped\n * so `reopen` never pops an empty Cursor window). Used by\n * `reopen`.\n *\n * No-ops (returns `ran: false`) when the Cursor entry has no `workspaceConfigPath`.\n * All failures are swallowed into a warning \u2014 opening Cursor is best-effort.\n */\nexport const openCursorWorkspace = async (args: OpenCursorWorkspaceArgs): Promise<OpenCursorWorkspaceOutcome> => {\n const { projectRoot, worktreeDir, currentBranches, cursorConfig } = args\n\n if (!cursorConfig.workspaceConfigPath) {\n logger.warn('\u26A0\uFE0F Skipping Cursor: workspaceConfigPath is not set.')\n\n return { ran: false, added: 0, removed: 0 }\n }\n\n const workspacePath = resolveCursorWorkspacePath(cursorConfig.workspaceConfigPath, projectRoot)\n\n try {\n const { added, removed } = await reconcileCursorWorkspaceFolders({\n workspacePath,\n worktreeDir,\n currentBranches,\n })\n\n if (currentBranches.length > 0) {\n await launchCursor(workspacePath)\n }\n\n return { ran: true, added: added.length, removed: removed.length }\n } catch (error) {\n logger.warn({ error }, `\u26A0\uFE0F Failed to reconcile/open Cursor workspace at ${workspacePath}`)\n\n return { ran: false, added: 0, removed: 0 }\n }\n}\n", "import { $ } from 'zx'\n\ninterface AddFoldersToZedWorkspaceArgs {\n folderPaths: string[]\n}\n\ninterface AddFoldersToZedWorkspaceResult {\n added: string[]\n}\n\n/**\n * Adds folders to Zed's current workspace via `zed --add <path>` per folder.\n * This is the only legitimate use of `--add` (it targets the most-recent\n * workspace, so it is reserved for the incremental add-only path right after\n * `worktrees-add` creates new worktrees \u2014 never for the full cold-start open).\n *\n * Zed has no folder-remove CLI and no skip-if-present concept, so every folder\n * is reported as added (no `skipped` set, unlike the Cursor `.code-workspace`).\n */\nexport const addFoldersToZedWorkspace = async (\n args: AddFoldersToZedWorkspaceArgs,\n): Promise<AddFoldersToZedWorkspaceResult> => {\n const { folderPaths } = args\n\n for (const folderPath of folderPaths) {\n await $`zed --add ${folderPath}`\n }\n\n return { added: folderPaths }\n}\n", "import { $ } from 'zx'\n\nimport { logger } from 'src/lib/logger'\n\ninterface OpenZedWorkspaceArgs {\n /**\n * The exact folder set to open, as absolute paths \u2014 the caller includes the\n * repo root (Zed realizes a multi-folder workspace from multiple path\n * arguments). Path-carrying rather than reconstructing `${worktreeDir}/${branch}`\n * so detached-HEAD and out-of-convention worktrees open too.\n */\n worktreePaths: string[]\n}\n\ninterface OpenZedWorkspaceOutcome {\n ran: boolean\n added: number\n removed: number\n}\n\n/**\n * Opens a single Zed workspace from the given folder paths, via one\n * `zed <path...>` invocation. Skips the launch when the set is empty so it never\n * pops a bare Zed window.\n *\n * Zed has no portable workspace file and no folder-remove CLI, so there is\n * nothing to reconcile: `added` is the number of folders opened and `removed`\n * is always 0. All failures are swallowed into a warning \u2014 opening Zed is\n * best-effort, mirroring the Cursor integration.\n */\nexport const openZedWorkspace = async (args: OpenZedWorkspaceArgs): Promise<OpenZedWorkspaceOutcome> => {\n const { worktreePaths } = args\n\n if (worktreePaths.length === 0) {\n return { ran: false, added: 0, removed: 0 }\n }\n\n try {\n await $`zed ${worktreePaths}`\n\n return { ran: true, added: worktreePaths.length, removed: 0 }\n } catch (error) {\n logger.warn({ error }, '\u26A0\uFE0F Failed to open Zed workspace')\n\n return { ran: false, added: 0, removed: 0 }\n }\n}\n", "import { $ } from 'zx'\n\nimport { logger } from 'src/lib/logger'\n\ninterface ReuseZedWorkspaceArgs {\n projectRoot: string\n worktreeDir: string\n remainingBranches: string[]\n}\n\ninterface ReuseZedWorkspaceOutcome {\n ran: boolean\n}\n\n/**\n * Reflects a worktree removal in the open Zed window by running\n * `zed --reuse <root> <remaining release worktrees...>`.\n *\n * IMPORTANT \u2014 this is destructive: `--reuse` REPLACES the focused window's ENTIRE folder set with\n * exactly these paths, so the caller MUST only invoke it on an interactive path where a human is\n * present and can re-add folders via `zed --add`. A disclosure log line says as much.\n *\n * Best-effort: any launch failure is swallowed into a warning, mirroring\n * `openZedWorkspace`. Root is always included so the path list is never empty.\n */\n// The remaining set is built from release worktrees only (`getCurrentWorktrees('release')`), so\n// any other folder open in that window \u2014 feature worktrees, scratch dirs, user-added dirs \u2014 is\n// DROPPED and is NOT recoverable by infra-kit: Zed offers no undo and we keep no record of the\n// prior set.\n//\n// Distinct from `openZedWorkspace`, which runs `zed <paths>` WITHOUT `--reuse` to realize a\n// multi-folder workspace; that does not replace a focused window.\nexport const reuseZedWorkspace = async (args: ReuseZedWorkspaceArgs): Promise<ReuseZedWorkspaceOutcome> => {\n const { projectRoot, worktreeDir, remainingBranches } = args\n\n const paths = [\n projectRoot,\n ...remainingBranches.map((branch) => {\n return `${worktreeDir}/${branch}`\n }),\n ]\n\n try {\n await $`zed --reuse ${paths}`\n\n logger.info(\n `\u21BB Refreshed Zed workspace to root + ${remainingBranches.length} release worktree(s) (other folders in the focused window are not preserved)`,\n )\n\n return { ran: true }\n } catch (error) {\n logger.warn({ error }, '\u26A0\uFE0F Failed to refresh Zed workspace')\n\n return { ran: false }\n }\n}\n", "/**\n * Exhaustiveness guard for discriminated unions. Place in the `default` branch of\n * a `switch` over a union: once every case is handled the argument narrows to\n * `never`, so adding a new variant later turns this into a compile error.\n *\n * @example\n * switch (provider) {\n * case 'cursor': return openCursor()\n * case 'zed': return openZed()\n * default: return assertNever(provider) // compile error if a provider is unhandled\n * }\n */\nexport const assertNever = (value: never): never => {\n throw new Error(`Unexpected value: ${JSON.stringify(value)}`)\n}\n", "import { addFoldersToCursorWorkspace, launchCursor, resolveCursorWorkspacePath } from 'src/integrations/cursor'\nimport { addFoldersToZedWorkspace } from 'src/integrations/zed'\nimport { assertNever } from 'src/lib/assert-never'\nimport { getInfraKitConfig, resolveConfiguredIdes } from 'src/lib/infra-kit-config'\nimport { logger } from 'src/lib/logger'\n\nimport type { AddIdeWorktreeFoldersOutcome } from './types'\n\ninterface AddIdeWorktreeFoldersArgs {\n projectRoot: string\n worktreeDir: string\n branches: string[]\n}\n\n/**\n * Workspace-mode add (used by `worktrees-add`): append the given worktree folders\n * to every configured editor's workspace and open it. Returns one outcome per\n * configured provider (empty array when no IDE is configured), iterating\n * sequentially.\n *\n * Cursor appends to the `.code-workspace` `folders` array (reporting `skipped`\n * for already-present entries) then runs `cursor <workspace>`. Zed has no\n * workspace file, so it `zed --add`s each folder (`skipped` is always 0).\n */\nexport const addIdeWorktreeFolders = async (\n args: AddIdeWorktreeFoldersArgs,\n): Promise<AddIdeWorktreeFoldersOutcome[]> => {\n const { projectRoot, worktreeDir, branches } = args\n\n const config = await getInfraKitConfig()\n const ides = resolveConfiguredIdes(config)\n\n const folderPaths = branches.map((branch) => {\n return `${worktreeDir}/${branch}`\n })\n\n const outcomes: AddIdeWorktreeFoldersOutcome[] = []\n\n for (const ide of ides) {\n switch (ide.provider) {\n case 'cursor': {\n if (!ide.config.workspaceConfigPath) {\n logger.warn('\u26A0\uFE0F Skipping Cursor: ide.config.workspaceConfigPath is not set in infra-kit config')\n\n outcomes.push({ ran: false, provider: 'cursor', added: 0, skipped: 0 })\n break\n }\n\n const workspacePath = resolveCursorWorkspacePath(ide.config.workspaceConfigPath, projectRoot)\n\n const { added, skipped } = await addFoldersToCursorWorkspace({ workspacePath, folderPaths })\n\n const skippedSuffix = skipped.length > 0 ? ` (${skipped.length} already present)` : ''\n\n logger.info(`\u2705 Added ${added.length} folder(s) to ${workspacePath}${skippedSuffix}`)\n\n await launchCursor(workspacePath)\n\n outcomes.push({ ran: true, provider: 'cursor', added: added.length, skipped: skipped.length })\n break\n }\n case 'zed': {\n const { added } = await addFoldersToZedWorkspace({ folderPaths })\n\n logger.info(`\u2705 Added ${added.length} folder(s) to Zed workspace`)\n\n outcomes.push({ ran: true, provider: 'zed', added: added.length, skipped: 0 })\n break\n }\n default: {\n assertNever(ide)\n }\n }\n }\n\n return outcomes\n}\n", "import { openCursorWorkspace } from 'src/integrations/cursor'\nimport { openZedWorkspace } from 'src/integrations/zed'\nimport { assertNever } from 'src/lib/assert-never'\nimport { getInfraKitConfig, resolveConfiguredIdes } from 'src/lib/infra-kit-config'\n\nimport type { OpenIdeWorkspaceOutcome } from './types'\n\ninterface OpenIdeWorkspaceArgs {\n projectRoot: string\n worktreeDir: string\n /**\n * The exact absolute folder set to open (root-first). Consumed by Zed, which\n * launches paths directly \u2014 this is what lets detached-HEAD and\n * out-of-convention worktrees open.\n */\n worktreePaths: string[]\n /**\n * Release branches, for Cursor's `.code-workspace` reconcile (its folder model\n * is release-branch-shaped, so it stays branch-based rather than path-carrying).\n */\n currentBranches: string[]\n}\n\n/**\n * Provider-agnostic entry point for the reload open: for every configured\n * editor, reconciles (or, for Zed, simply assembles) the workspace against the\n * release worktrees on disk and launches it \u2014 skipping the launch when there are\n * no worktrees, so `reopen` never pops a bare editor window. Returns\n * one outcome per configured provider (empty array when no IDE is configured).\n * Iterates sequentially \u2014 `reopen` already wraps this call in an outer\n * `Promise.all` with cmux, so a `Promise.all` here would compound editor-spawn\n * concurrency. Best-effort \u2014 every provider swallows failures into a warning.\n */\nexport const openIdeWorkspace = async (args: OpenIdeWorkspaceArgs): Promise<OpenIdeWorkspaceOutcome[]> => {\n const config = await getInfraKitConfig()\n const ides = resolveConfiguredIdes(config)\n\n const { projectRoot, worktreeDir, worktreePaths, currentBranches } = args\n\n const outcomes: OpenIdeWorkspaceOutcome[] = []\n\n for (const ide of ides) {\n switch (ide.provider) {\n case 'cursor': {\n // Cursor reconciles a release-branch-shaped `.code-workspace` file, so it\n // stays branch-based rather than consuming the harvested paths.\n const outcome = await openCursorWorkspace({\n projectRoot,\n worktreeDir,\n currentBranches,\n cursorConfig: ide.config,\n })\n\n outcomes.push({ ...outcome, provider: 'cursor' })\n break\n }\n case 'zed': {\n const outcome = await openZedWorkspace({ worktreePaths })\n\n outcomes.push({ ...outcome, provider: 'zed' })\n break\n }\n default: {\n assertNever(ide)\n }\n }\n }\n\n return outcomes\n}\n", "import { assertNever } from 'src/lib/assert-never'\n\nimport type { IdeProvider } from './types'\n\n/**\n * Human-facing display name for an IDE provider, used in log lines. Falls back to\n * a generic \"IDE\" when no provider is configured.\n */\nexport const ideProviderLabel = (provider: IdeProvider | undefined): string => {\n switch (provider) {\n case 'cursor': {\n return 'Cursor'\n }\n case 'zed': {\n return 'Zed'\n }\n case undefined: {\n return 'IDE'\n }\n default: {\n return assertNever(provider)\n }\n }\n}\n", "import { removeFoldersFromCursorWorkspace, resolveCursorWorkspacePath } from 'src/integrations/cursor'\nimport { reuseZedWorkspace } from 'src/integrations/zed'\nimport { assertNever } from 'src/lib/assert-never'\nimport { getInfraKitConfig, resolveConfiguredIdes } from 'src/lib/infra-kit-config'\nimport { logger } from 'src/lib/logger'\n\nimport type { RemoveIdeWorktreeFoldersOutcome } from './types'\n\ninterface RemoveIdeWorktreeFoldersArgs {\n projectRoot: string\n worktreeDir: string\n /** All release worktrees BEFORE removal (release-only \u2014 see Zed note below). */\n currentWorktrees: string[]\n /** The subset of `currentWorktrees` that was just removed. */\n removedWorktrees: string[]\n /**\n * Whether the caller is on an interactive path where firing Zed's destructive\n * `zed --reuse` is acceptable (a human is present and confirming). Interactive\n * `worktrees-remove` passes `!confirmedCommand`; `worktrees-sync` and every\n * non-interactive/MCP run pass `false`.\n */\n allowEditorRelaunch: boolean\n}\n\n/**\n * Strip removed worktrees from every configured editor's workspace (used by\n * `worktrees-remove` and `worktrees-sync`). Returns one outcome per configured\n * provider (empty array when no IDE is configured or no worktrees were removed),\n * iterating sequentially.\n *\n * Cursor surgically edits the `.code-workspace` `folders` array. Zed's path is DESTRUCTIVE, so it\n * fires ONLY when `allowEditorRelaunch` is true (interactive `worktrees-remove`); on the\n * non-interactive/MCP path it is a deliberate no-op (`supported: true`, `removed: []`) with an\n * info message. Zed's `removed` is ALWAYS `[]`.\n */\n// Zed has no surgical remove. The only mutation mechanism, `zed --reuse`, REPLACES the focused\n// window's entire folder set, so it can only re-state the release worktrees we know about and\n// silently drops any other open folder (`remaining` is built from release worktrees only). That is\n// why the no-op on the non-interactive path reports `supported: true`: the capability exists, and\n// skipping is a policy choice rather than a missing capability.\n//\n// `removed` stays empty because `--reuse` performs no diff \u2014 it confirms no specific removal, and\n// reporting intended-but-unverified paths would be a lie.\nexport const removeIdeWorktreeFolders = async (\n args: RemoveIdeWorktreeFoldersArgs,\n): Promise<RemoveIdeWorktreeFoldersOutcome[]> => {\n const { projectRoot, worktreeDir, currentWorktrees, removedWorktrees, allowEditorRelaunch } = args\n\n if (removedWorktrees.length === 0) {\n return []\n }\n\n const config = await getInfraKitConfig()\n const ides = resolveConfiguredIdes(config)\n\n const folderPaths = removedWorktrees.map((branch) => {\n return `${worktreeDir}/${branch}`\n })\n\n const remainingBranches = currentWorktrees.filter((branch) => {\n return !removedWorktrees.includes(branch)\n })\n\n const outcomes: RemoveIdeWorktreeFoldersOutcome[] = []\n\n for (const ide of ides) {\n switch (ide.provider) {\n case 'cursor': {\n outcomes.push(await removeFromCursor({ ide, projectRoot, folderPaths }))\n break\n }\n case 'zed': {\n outcomes.push(await removeFromZed({ projectRoot, worktreeDir, remainingBranches, allowEditorRelaunch }))\n break\n }\n default: {\n assertNever(ide)\n }\n }\n }\n\n return outcomes\n}\n\ninterface RemoveFromCursorArgs {\n ide: Extract<ReturnType<typeof resolveConfiguredIdes>[number], { provider: 'cursor' }>\n projectRoot: string\n folderPaths: string[]\n}\n\n/** Surgically strip the removed folder paths from the Cursor `.code-workspace`. */\nconst removeFromCursor = async (args: RemoveFromCursorArgs): Promise<RemoveIdeWorktreeFoldersOutcome> => {\n const { ide, projectRoot, folderPaths } = args\n\n if (!ide.config.workspaceConfigPath) {\n return { provider: 'cursor', supported: true, removed: [] }\n }\n\n const workspacePath = resolveCursorWorkspacePath(ide.config.workspaceConfigPath, projectRoot)\n\n try {\n const { removed } = await removeFoldersFromCursorWorkspace({ workspacePath, folderPaths })\n\n if (removed.length > 0) {\n logger.info(`\u2705 Removed ${removed.length} folder(s) from ${workspacePath}`)\n }\n\n return { provider: 'cursor', supported: true, removed }\n } catch (error) {\n logger.warn({ error }, `\u26A0\uFE0F Failed to update Cursor workspace at ${workspacePath}`)\n\n return { provider: 'cursor', supported: true, removed: [] }\n }\n}\n\ninterface RemoveFromZedArgs {\n projectRoot: string\n worktreeDir: string\n remainingBranches: string[]\n allowEditorRelaunch: boolean\n}\n\n/**\n * Reflect the removal in Zed by relaunching the focused window onto the\n * remaining set \u2014 but ONLY on an interactive path (see the module doc for why\n * `zed --reuse` is destructive). Otherwise a deliberate no-op. `removed` is\n * always `[]` because `--reuse` performs no diff.\n */\nconst removeFromZed = async (args: RemoveFromZedArgs): Promise<RemoveIdeWorktreeFoldersOutcome> => {\n const { projectRoot, worktreeDir, remainingBranches, allowEditorRelaunch } = args\n\n if (!allowEditorRelaunch) {\n logger.info(\n '\u2139\uFE0F Zed folder removal skipped (no interactive session); close removed worktree folders in Zed manually if needed.',\n )\n\n return { provider: 'zed', supported: true, removed: [] }\n }\n\n await reuseZedWorkspace({ projectRoot, worktreeDir, remainingBranches })\n\n return { provider: 'zed', supported: true, removed: [] }\n}\n", "export type IdeProvider = 'cursor' | 'zed'\n\n/**\n * CLI-level open modes for `worktrees-add` (`--ide`). `workspace` adds each\n * created worktree to the configured editor's workspace and opens it (the only\n * attach style); `none` means \"don't open any editor\".\n */\nexport const IDE_MODES = ['workspace', 'none'] as const\nexport type IdeMode = (typeof IDE_MODES)[number]\n\n// Each facade returns one outcome PER configured provider (an array), so every\n// outcome is tagged with its `provider`.\nexport interface OpenIdeWorkspaceOutcome {\n ran: boolean\n provider: IdeProvider\n added: number\n removed: number\n}\n\nexport interface AddIdeWorktreeFoldersOutcome {\n ran: boolean\n provider: IdeProvider\n added: number\n skipped: number\n}\n\nexport interface RemoveIdeWorktreeFoldersOutcome {\n provider: IdeProvider\n /**\n * True when the provider can reflect a removal. Both providers now report\n * `true`; `false` is reserved for a future provider with no remove capability.\n */\n supported: boolean\n /**\n * Folders confirmed removed. For Cursor this is the real diff of the\n * `.code-workspace` `folders` array. For Zed it is ALWAYS `[]`: the only\n * mechanism (`zed --reuse`) replaces the whole window without producing a\n * diff, so it confirms no specific removal even when it refreshed the window.\n */\n removed: string[]\n}\n", "import { execFile } from 'node:child_process'\nimport path from 'node:path'\nimport process from 'node:process'\nimport { z } from 'zod'\n\nimport {\n buildCmuxWorkspaceTitle,\n closeCmuxWorkspaceByCwd,\n createCmuxGroupFrom,\n findCmuxGroupRefByName,\n listCmuxWorkspacesByCwd,\n openCmuxWorkspaceWithLayout,\n realpathForCmuxCwd,\n} from 'src/integrations/cmux'\nimport { ideProviderLabel, openIdeWorkspace } from 'src/integrations/ide'\nimport { commandEcho } from 'src/lib/command-echo'\nimport { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { getMainRepoRoot, getProjectRoot, listWorktrees } from 'src/lib/git-utils'\nimport type { WorktreeEntry } from 'src/lib/git-utils'\nimport { getInfraKitConfig } from 'src/lib/infra-kit-config'\nimport { logger } from 'src/lib/logger'\nimport { discoverInfraKitProjects } from 'src/lib/project-discovery'\nimport type { DiscoveredProject } from 'src/lib/project-discovery'\nimport { isReleaseBranch } from 'src/lib/release-id'\nimport { defineMcpTool, textContent } from 'src/types'\nimport type { ToolsExecutionResult } from 'src/types'\n\ninterface ReopenArgs {\n /** Cross-project fan-out \u2014 dispatches to {@link reopenAll} instead of the single-project path. */\n all?: boolean\n /** Narrow `--all` to these project names. Ignored without `--all`. */\n project?: string[]\n /** Discovery roots for `--all` (falls back to the workspace dir when omitted). Ignored without `--all`. */\n root?: string[]\n /** Restrict to release worktrees (reproduces the legacy `worktrees reload` scope). */\n releaseOnly?: boolean\n /** Close each cmux workspace first, then reopen (the legacy `worktrees reload` behaviour). */\n force?: boolean\n /** Print the plan and spawn nothing. */\n dryRun?: boolean\n}\n\ninterface ReopenResult {\n repo: string\n dryRun: boolean\n releaseOnly: boolean\n /** Absolute folder set handed to the editor (root-first, exactly as opened). */\n worktreePaths: string[]\n ideProviders: string[]\n /** cmux titles opened this run (additive mode: only the ones not already open). */\n cmuxOpened: string[]\n /** cmux titles skipped because their workspace was already open (additive mode). */\n cmuxSkipped: string[]\n /** cmux titles closed first (only in `--force` mode). */\n cmuxClosed: string[]\n}\n\n/** One cmux workspace to (re)open: its canonical-friendly title and the cwd it runs in. */\ninterface CmuxTarget {\n title: string\n cwd: string\n}\n\n/**\n * Per-repo outcome of an `--all` sweep: either the child's parsed\n * {@link ReopenResult} (with `ok: true`), or an isolated failure record. `repo`\n * is the discovered project name (the child's own `repo` field is overwritten so\n * the aggregate keys off the name the parent discovered).\n */\nexport type ReopenAllRepoResult =\n (ReopenResult & { repo: string; ok: true }) | { repo: string; ok: false; error: string }\n\n/** Aggregated result of `infra-kit reopen --all` across every discovered project. */\nexport interface ReopenAllResult {\n all: true\n dryRun: boolean\n projects: DiscoveredProject[]\n results: ReopenAllRepoResult[]\n}\n\n/**\n * Entry point for `infra-kit reopen`. Dispatches on `--all`: without it, reopen\n * the current project (see {@link reopenCurrentProject}); with it, discover every\n * infra-kit project and fan out one child process per repo (see\n * {@link reopenAll}). Kept as a thin dispatcher so the single-project path and\n * its MCP tool return the narrow {@link ReopenResult} unchanged.\n */\nexport const reopen = async (\n options: ReopenArgs = {},\n): Promise<ToolsExecutionResult<ReopenResult | ReopenAllResult>> => {\n return options.all ? reopenAll(options) : reopenCurrentProject(options)\n}\n\n/**\n * Reopen editor + cmux windows for every *active* worktree in the current\n * project. \"Active\" = a `git worktree list` record that is neither bare nor\n * prunable \u2014 the main checkout plus every linked worktree (release, feature,\n * detached), regardless of branch. Additive and idempotent by default: cmux\n * workspaces already open are skipped, so running twice does not double the\n * windows. `--force` closes each cmux workspace first (the legacy reload\n * behaviour); `--release-only` narrows the set to release worktrees.\n *\n * Non-destructive to git \u2014 only cmux/editor view state is touched.\n */\nexport const reopenCurrentProject = async (options: ReopenArgs = {}): Promise<ToolsExecutionResult<ReopenResult>> => {\n const { releaseOnly = false, force = false, dryRun = false } = options\n\n // GUARD (placement is load-bearing): must stay ABOVE the `try` block below. Its catch rethrows only\n // OperationError; getInfraKitConfig's missing-config throw is a PLAIN Error, so a read inside the try\n // is rewrapped and its text is silently dropped by buildMessage \u2014 AC3b would fail green.\n await getInfraKitConfig()\n\n try {\n const projectRoot = await getProjectRoot()\n const worktreeDir = `${projectRoot}${WORKTREES_DIR_SUFFIX}`\n // cmux titles are keyed on the STABLE main-repo name (the basename of the main\n // checkout), not the worktree-local basename: running `reopen` from inside a\n // linked worktree must produce the same titles `worktrees add` created from the\n // main checkout, or dedup misses and every run duplicates the windows.\n const repoName = path.basename(await getMainRepoRoot(projectRoot))\n\n const entries = await listWorktrees(projectRoot)\n const active = selectActive(entries, releaseOnly)\n\n // The exact folder set to open (root-first, harvested from git \u2014 the main\n // checkout's own record is already in `entries`, so root is included here).\n const worktreePaths = active.map((entry) => {\n return entry.path\n })\n\n // Cursor's `.code-workspace` reconcile is release-branch-shaped, so it stays\n // branch-based on the release subset (Zed consumes the paths above).\n const currentBranches = active\n .map((entry) => {\n return entry.branch\n })\n .filter((branch): branch is string => {\n return isReleaseBranch(branch)\n })\n\n const targets: CmuxTarget[] = active.map((entry) => {\n return {\n // Detached / branch-less worktrees have no branch to title from; fall back\n // to the leaf directory so the title is still stable and meaningful.\n title: buildCmuxWorkspaceTitle({ branch: entry.branch ?? path.basename(entry.path) }),\n cwd: entry.path,\n }\n })\n\n if (dryRun) {\n const result = await planReopen({ repo: repoName, releaseOnly, force, worktreePaths, targets })\n\n logDryRun(result, worktreeDir)\n commandEcho.print()\n\n return { content: textContent(JSON.stringify(result, null, 2)), structuredContent: result }\n }\n\n const [ideOutcomes, cmux] = await Promise.all([\n openIdeWorkspace({ projectRoot, worktreeDir, worktreePaths, currentBranches }),\n runCmux({ targets, force, repoName }),\n ])\n\n const result: ReopenResult = {\n repo: repoName,\n dryRun: false,\n releaseOnly,\n worktreePaths,\n ideProviders: ideOutcomes\n .filter((outcome) => {\n return outcome.ran\n })\n .map((outcome) => {\n return outcome.provider\n }),\n cmuxOpened: cmux.opened,\n cmuxSkipped: cmux.skipped,\n cmuxClosed: cmux.closed,\n }\n\n logResults(result, {\n ideRan: ideOutcomes.some((outcome) => {\n return outcome.ran\n }),\n })\n\n commandEcho.print()\n\n return { content: textContent(JSON.stringify(result, null, 2)), structuredContent: result }\n } catch (error) {\n if (error instanceof OperationError) throw error\n\n // `debug`, not `error`: this rethrows as an OperationError, and `entry/cli.ts` logs any\n // uncaught error at ERROR and exits 1 \u2014 so logging here too printed one fault as two red\n // lines. Kept (demoted, not deleted) because the wrapped message renders only the operation\n // and remediation; the cause's stack survives here and is reachable with `--debug`.\n logger.debug({ err: error }, 'Error reopening worktree windows')\n throw new OperationError(error, {\n operation: 'reopen worktrees',\n remediation: \"run `infra-kit doctor` to check this project's setup\",\n })\n }\n}\n\n/**\n * Active = neither bare nor prunable. `--release-only` further restricts to\n * release worktrees (reproducing the legacy reload scope). This is a FILTER we\n * must write: `git worktree list` reports bare/prunable records with marker\n * lines, it does not omit them.\n */\nconst selectActive = (entries: WorktreeEntry[], releaseOnly: boolean): WorktreeEntry[] => {\n return entries.filter((entry) => {\n if (entry.bare || entry.prunable) return false\n\n return releaseOnly ? isReleaseBranch(entry.branch) : true\n })\n}\n\ninterface RunCmuxArgs {\n targets: CmuxTarget[]\n force: boolean\n /** Stable repo name (basename of the main-repo root) \u2192 the sidebar group name. */\n repoName: string\n}\n\ninterface RunCmuxOutcome {\n opened: string[]\n skipped: string[]\n closed: string[]\n}\n\n/**\n * Open a cmux workspace for every target, each inside the repo's sidebar group.\n * Default is ADDITIVE + DEDUPED by workspace cwd: a target whose worktree cwd is\n * already open is skipped, which makes `reopen` idempotent (running twice opens\n * nothing new \u2014 including the main checkout, whose cwd the group anchor shares but\n * which the cwd map excludes). `--force` closes each target first, then force-opens\n * with no dedup \u2014 the escape hatch that recovers the legacy reload behaviour and\n * doubles as the migration for pre-group ungrouped windows.\n */\nconst runCmux = async (args: RunCmuxArgs): Promise<RunCmuxOutcome> => {\n const { targets, force, repoName } = args\n\n // Resolve the group ONCE, before the close loop \u2014 closing the last member of a\n // group leaves it empty-but-pinned, and a later lookup must not race that.\n const groupRef = await findCmuxGroupRefByName(repoName)\n\n const closed = force ? await closeCmux({ targets }) : []\n\n const { opened, skipped } = await reopenCmux({ targets, force, repoName, groupRef })\n\n return { opened, skipped, closed }\n}\n\ninterface CloseCmuxArgs {\n targets: CmuxTarget[]\n}\n\n/**\n * Close the cmux workspace for each target that is currently open, matched BY CWD\n * (never by title \u2014 titles are no longer unique across repos, and a cwd match\n * excludes group anchors so a group header can never be closed). Snapshots the\n * open set up front so the returned list reflects which were actually open; the\n * close itself is best-effort per target.\n */\nexport const closeCmux = async (args: CloseCmuxArgs): Promise<string[]> => {\n const { targets } = args\n\n if (targets.length === 0) {\n return []\n }\n\n const openByCwd = await listCmuxWorkspacesByCwd()\n\n const closed: string[] = []\n\n for (const target of targets) {\n if (!openByCwd.has(await realpathForCmuxCwd(target.cwd))) {\n continue\n }\n\n await closeCmuxWorkspaceByCwd(target.cwd)\n closed.push(target.title)\n }\n\n return closed\n}\n\ninterface ReopenCmuxArgs {\n targets: CmuxTarget[]\n force: boolean\n repoName: string\n /** Existing group ref, or null/undefined to bootstrap one from the first opened workspace. */\n groupRef?: string | null\n}\n\ninterface ReopenCmuxOutcome {\n opened: string[]\n skipped: string[]\n}\n\n/**\n * Partition targets into those that need opening vs. those already open, against a\n * set of open workspace cwds (realpath'd, anchors excluded). `force` bypasses dedup\n * entirely \u2014 every target lands in `toOpen`. Shared by {@link reopenCmux} and\n * {@link planReopen}.\n */\nconst partitionCmuxTargets = async (\n targets: CmuxTarget[],\n openCwds: Set<string>,\n force: boolean,\n): Promise<{ toOpen: CmuxTarget[]; skipped: string[] }> => {\n const toOpen: CmuxTarget[] = []\n const skipped: string[] = []\n\n for (const target of targets) {\n if (!force && openCwds.has(await realpathForCmuxCwd(target.cwd))) {\n skipped.push(target.title)\n } else {\n toOpen.push(target)\n }\n }\n\n return { toOpen, skipped }\n}\n\n/**\n * (Re)open a cmux workspace per target, inside the repo's sidebar group. Additive\n * by default \u2014 skips any target whose worktree cwd is already open, so a second run\n * opens 0 new workspaces. If no group exists yet, the FIRST opened workspace seeds\n * it via `--from` (capture-free); the rest open straight into it. Under `force`,\n * dedup is bypassed and every target is force-opened (callers close first).\n */\nexport const reopenCmux = async (args: ReopenCmuxArgs): Promise<ReopenCmuxOutcome> => {\n const { targets, force, repoName, groupRef = null } = args\n\n // `listCmuxWorkspacesByCwd` returns `\u2205` when cmux is down or unreadable; its\n // contract says treat empty as \"unknown, proceed as if nothing is open\", which\n // is exactly force-open behaviour \u2014 acceptable here.\n const openCwds = force ? new Set<string>() : new Set((await listCmuxWorkspacesByCwd()).keys())\n const { toOpen, skipped } = await partitionCmuxTargets(targets, openCwds, force)\n\n const opened: string[] = []\n let group = groupRef\n let bootstrapAttempted = false\n\n for (const target of toOpen) {\n try {\n if (group) {\n await openCmuxWorkspaceWithLayout({ cwd: target.cwd, title: target.title, group })\n } else {\n const workspaceRef = await openCmuxWorkspaceWithLayout({ cwd: target.cwd, title: target.title })\n\n // Seed the group from the first workspace we open, once. `--from` makes\n // creation capture-free; if it fails, remaining targets open ungrouped\n // rather than spawning a second group per iteration.\n if (!bootstrapAttempted) {\n group = await createCmuxGroupFrom(repoName, [workspaceRef])\n bootstrapAttempted = true\n }\n }\n\n opened.push(target.title)\n } catch (error) {\n logger.warn({ error, title: target.title }, `\u26A0\uFE0F Failed to reopen cmux workspace ${target.title}`)\n }\n }\n\n return { opened, skipped }\n}\n\ninterface PlanReopenArgs {\n repo: string\n releaseOnly: boolean\n force: boolean\n worktreePaths: string[]\n targets: CmuxTarget[]\n}\n\n/**\n * Build the dry-run plan: which folders would open in the editor and which cmux\n * titles would open vs. are already open. Reads the live cmux workspace list but\n * spawns nothing.\n */\nconst planReopen = async (args: PlanReopenArgs): Promise<ReopenResult> => {\n const { repo, releaseOnly, force, worktreePaths, targets } = args\n\n const openCwds = force ? new Set<string>() : new Set((await listCmuxWorkspacesByCwd()).keys())\n const { toOpen, skipped } = await partitionCmuxTargets(targets, openCwds, force)\n\n return {\n repo,\n dryRun: true,\n releaseOnly,\n worktreePaths,\n ideProviders: [],\n cmuxOpened: toOpen.map((target) => {\n return target.title\n }),\n cmuxSkipped: skipped,\n cmuxClosed: [],\n }\n}\n\nconst logDryRun = (result: ReopenResult, worktreeDir: string): void => {\n logger.info(`\uD83D\uDD0E reopen --dry-run for ${result.repo} (worktrees dir: ${worktreeDir})`)\n logger.info(`\uD83D\uDCC2 Would open ${result.worktreePaths.length} folder(s) in the editor:`)\n for (const folder of result.worktreePaths) {\n logger.info(` ${folder}`)\n }\n\n if (result.cmuxOpened.length > 0) {\n logger.info(`\uD83E\uDE9F Would open ${result.cmuxOpened.length} cmux workspace(s):`)\n for (const title of result.cmuxOpened) {\n logger.info(` ${title}`)\n }\n }\n\n if (result.cmuxSkipped.length > 0) {\n logger.info(`\u21A9\uFE0F Already open (would skip) ${result.cmuxSkipped.length} cmux workspace(s):`)\n for (const title of result.cmuxSkipped) {\n logger.info(` ${title}`)\n }\n }\n}\n\ninterface LogResultsContext {\n ideRan: boolean\n}\n\nconst logResults = (result: ReopenResult, context: LogResultsContext): void => {\n if (result.cmuxClosed.length > 0) {\n logger.info(`\uD83E\uDDF9 Closed ${result.cmuxClosed.length} cmux workspace(s)`)\n }\n\n if (context.ideRan) {\n const ideLabels = result.ideProviders\n .map((provider) => {\n return ideProviderLabel(provider as Parameters<typeof ideProviderLabel>[0])\n })\n .join(', ')\n\n logger.info(`\u2705 Opened editor workspace(s): ${ideLabels}`)\n }\n\n if (result.cmuxOpened.length > 0) {\n logger.info('\u2705 Opened cmux workspaces:')\n for (const title of result.cmuxOpened) {\n logger.info(title)\n }\n }\n\n if (result.cmuxSkipped.length > 0) {\n logger.info(`\u21A9\uFE0F ${result.cmuxSkipped.length} cmux workspace(s) already open \u2014 skipped`)\n }\n\n if (!context.ideRan && result.cmuxOpened.length === 0 && result.cmuxClosed.length === 0) {\n logger.info('\u2139\uFE0F Nothing to reopen')\n }\n}\n\n/**\n * Cross-project fan-out for `--all`. Discovers every live infra-kit project\n * (scanning `--root` or `dirname(getMainRepoRoot())`), then spawns ONE child\n * `reopen --json [flags]` per repo, SERIALLY. Serial is deliberate: concurrent\n * node procs racing the same cmux GUI is the focus-stealing mess serial avoids.\n * Per-child failures are isolated \u2014 a repo that errors is recorded and the sweep\n * continues. Renders one aggregated summary and returns one combined result.\n */\nconst reopenAll = async (options: ReopenArgs): Promise<ToolsExecutionResult<ReopenAllResult>> => {\n const { project = [], root = [], releaseOnly = false, force = false, dryRun = false } = options\n\n const discovered = await discoverInfraKitProjects({ roots: root })\n const projects = filterProjects(discovered, project)\n const childFlags = buildChildFlags({ releaseOnly, force, dryRun })\n\n // SERIAL. Not parallel \u2014 N node procs racing the cmux GUI is the mess this avoids.\n const results: ReopenAllRepoResult[] = []\n\n for (const proj of projects) {\n results.push(await runRepoChild(proj, childFlags))\n }\n\n const result: ReopenAllResult = { all: true, dryRun, projects, results }\n\n logReopenAll(result)\n commandEcho.print()\n\n return { content: textContent(JSON.stringify(result, null, 2)), structuredContent: result }\n}\n\n/**\n * Narrow the discovered set to `--project` names. Empty \u2192 keep all. Names that\n * match nothing are warned and skipped (they never silently widen the sweep).\n */\nconst filterProjects = (discovered: DiscoveredProject[], names: string[]): DiscoveredProject[] => {\n if (names.length === 0) return discovered\n\n const discoveredNames = new Set(\n discovered.map((proj) => {\n return proj.name\n }),\n )\n\n for (const name of names) {\n if (!discoveredNames.has(name)) {\n logger.warn(`\u26A0\uFE0F --project ${name} matched no discovered project \u2014 skipping`)\n }\n }\n\n return discovered.filter((proj) => {\n return names.includes(proj.name)\n })\n}\n\n/**\n * The child passthrough flags. `--all` is NEVER passed (it would recurse\n * infinitely); `--project` / `--root` are parent-only and NEVER passed to\n * children. `--json` is added by the spawn itself, not here.\n */\nconst buildChildFlags = (args: { releaseOnly: boolean; force: boolean; dryRun: boolean }): string[] => {\n const flags: string[] = []\n\n if (args.dryRun) flags.push('--dry-run')\n if (args.releaseOnly) flags.push('--release-only')\n if (args.force) flags.push('--force')\n\n return flags\n}\n\n/**\n * Run one child `reopen --json` in `proj.repoRoot` and parse its single JSON\n * result off stdout. Any failure (non-zero exit, unparseable stdout, spawn\n * error) is isolated into an error record so the caller's sweep continues.\n */\nconst runRepoChild = async (proj: DiscoveredProject, childFlags: string[]): Promise<ReopenAllRepoResult> => {\n try {\n const stdout = await spawnReopenChild(proj.repoRoot, childFlags)\n const childResult = JSON.parse(stdout) as ReopenResult\n\n return { ...childResult, repo: proj.name, ok: true }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n\n logger.warn({ error }, `\u26A0\uFE0F reopen failed for ${proj.name} \u2014 continuing`)\n\n return { repo: proj.name, ok: false, error: message }\n }\n}\n\n/**\n * Spawn `<node> <this cli.js> reopen --json [flags]` in `cwd` and resolve its\n * stdout. Resolves the CLI via `process.execPath` + `process.argv[1]` \u2014 NEVER\n * PATH and NEVER `pnpm exec` (PATH/`pnpm exec` resolution of our own bin is a\n * repeatedly-relearned mistake). Rejects on spawn error or non-zero exit.\n */\nconst spawnReopenChild = (cwd: string, childFlags: string[]): Promise<string> => {\n const selfCli = process.argv[1]\n\n if (selfCli === undefined) {\n return Promise.reject(new Error('cannot resolve the infra-kit CLI entry \u2014 process.argv[1] is undefined'))\n }\n\n return new Promise((resolve, reject) => {\n const args = [selfCli, 'reopen', '--json', ...childFlags]\n\n execFile(process.execPath, args, { cwd, maxBuffer: 16 * 1024 * 1024 }, (error, stdout) => {\n if (error) {\n reject(error)\n\n return\n }\n\n resolve(stdout)\n })\n })\n}\n\n/** Render one aggregated summary for the whole `--all` sweep (parent owns all rendering). */\nconst logReopenAll = (result: ReopenAllResult): void => {\n const okCount = result.results.filter((repo) => {\n return repo.ok\n }).length\n const failedCount = result.results.length - okCount\n\n logger.info(`\uD83C\uDF10 reopen --all${result.dryRun ? ' --dry-run' : ''}: ${result.projects.length} project(s)`)\n\n let totalOpened = 0\n let totalSkipped = 0\n\n for (const repo of result.results) {\n if (repo.ok) {\n totalOpened += repo.cmuxOpened.length\n totalSkipped += repo.cmuxSkipped.length\n\n logger.info(\n ` \u2705 ${repo.repo}: ${repo.worktreePaths.length} folder(s), ${repo.cmuxOpened.length} cmux ${result.dryRun ? 'planned' : 'opened'}, ${repo.cmuxSkipped.length} skipped`,\n )\n } else {\n logger.info(` \u274C ${repo.repo}: ${repo.error}`)\n }\n }\n\n logger.info(\n `\uD83D\uDCCA ${okCount} ok, ${failedCount} failed \u2014 ${totalOpened} cmux ${result.dryRun ? 'planned' : 'opened'}, ${totalSkipped} already open`,\n )\n}\n\n// MCP Tool Registration\nexport const reopenMcpTool = defineMcpTool({\n name: 'reopen',\n description:\n 'Reopen editor + cmux windows for every active worktree in the current project \u2014 the main checkout plus every linked worktree (release, feature, detached). Additive and idempotent by default: cmux workspaces already open are skipped, so running twice does not double the windows. Zed opens the exact folder set; Cursor reconciles its release-branch-shaped workspace. Set releaseOnly to restrict to release worktrees (the legacy reload scope). Set force to close each cmux workspace first, then reopen (disruptive). Set dryRun to return the plan without spawning anything. Non-destructive to git \u2014 only cmux/editor view state is touched.',\n inputSchema: {\n releaseOnly: z\n .boolean()\n .optional()\n .describe('Restrict to release worktrees (reproduces the legacy worktrees-reload scope)'),\n force: z.boolean().optional().describe('Close each cmux workspace first, then reopen (disruptive)'),\n dryRun: z.boolean().optional().describe('Return the plan (paths + cmux titles) without spawning anything'),\n },\n outputSchema: {\n repo: z.string().describe('Repository name the reopen ran for'),\n dryRun: z.boolean().describe('Whether this was a dry run (nothing was spawned)'),\n releaseOnly: z.boolean().describe('Whether the set was restricted to release worktrees'),\n worktreePaths: z.array(z.string()).describe('Absolute folder set handed to the editor (root-first)'),\n ideProviders: z.array(z.string()).describe('Configured IDE providers that opened (cursor | zed); empty in dry run'),\n cmuxOpened: z.array(z.string()).describe('Titles of cmux workspaces opened this run'),\n cmuxSkipped: z.array(z.string()).describe('Titles of cmux workspaces skipped because already open (additive mode)'),\n cmuxClosed: z.array(z.string()).describe('Titles of cmux workspaces closed first (force mode only)'),\n },\n handler: reopenCurrentProject,\n})\n", "import type { Dirent } from 'node:fs'\nimport fs from 'node:fs/promises'\nimport path from 'node:path'\n\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { getMainRepoRoot } from 'src/lib/git-utils'\n\n/**\n * A live infra-kit project found by scanning a workspace root. `repoRoot` is\n * realpath'd so two entries that resolve to the same directory (a symlink, a\n * `/var`\u2192`/private/var` alias) collapse to one.\n */\nexport interface DiscoveredProject {\n name: string\n repoRoot: string\n}\n\nexport interface DiscoverInfraKitProjectsOptions {\n /**\n * Explicit discovery roots. When non-empty these win outright; otherwise the\n * scan falls back to `dirname(getMainRepoRoot())` (the workspace dir).\n */\n roots?: string[]\n}\n\n/** True iff `target` exists (file or dir); tolerates ENOENT and any other stat error. */\nconst pathExists = async (target: string): Promise<boolean> => {\n try {\n await fs.access(target)\n\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Resolve the scan roots per the discovery ladder: explicit `roots` win; else\n * `[dirname(await getMainRepoRoot())]`. Throws an `OperationError` with a\n * remediation hint when neither is available (not inside a git repo, no roots).\n */\nconst resolveRoots = async (roots?: string[]): Promise<string[]> => {\n if (roots && roots.length > 0) return roots\n\n try {\n const mainRepoRoot = await getMainRepoRoot()\n\n return [path.dirname(mainRepoRoot)]\n } catch (error) {\n throw new OperationError(error, {\n operation: 'discover infra-kit projects',\n remediation: 'run inside an infra-kit repo, or pass --root <dir>',\n })\n }\n}\n\n/** A candidate is worth stat-ing only if it is a directory or a symlink to one. */\nconst isScanCandidate = (child: Dirent): boolean => {\n return child.isDirectory() || child.isSymbolicLink()\n}\n\n/**\n * Resolve one direct child of a root into a {@link DiscoveredProject}, or `null`\n * when it is not a live infra-kit project. Live = BOTH `<candidate>/.git` (file\n * or dir \u2014 worktrees use a `.git` file) and `<candidate>/infra-kit.json` exist.\n * `repoRoot` is realpath'd; an unresolvable path yields `null`.\n */\nconst resolveCandidate = async (root: string, child: Dirent): Promise<DiscoveredProject | null> => {\n if (!isScanCandidate(child)) return null\n\n const candidate = path.join(root, child.name)\n\n const [hasGit, hasConfig] = await Promise.all([\n pathExists(path.join(candidate, '.git')),\n pathExists(path.join(candidate, 'infra-kit.json')),\n ])\n\n if (!hasGit || !hasConfig) return null\n\n try {\n const repoRoot = await fs.realpath(candidate)\n\n return { name: path.basename(candidate), repoRoot }\n } catch {\n return null\n }\n}\n\n/** Read a root's direct children; a missing or unreadable root yields none. */\nconst readRootChildren = async (root: string): Promise<Dirent[]> => {\n try {\n return await fs.readdir(root, { withFileTypes: true })\n } catch {\n return []\n }\n}\n\n/**\n * Scan the workspace for live infra-kit projects.\n *\n * SCAN-BASED \u2014 the registry (`~/.infra-kit/projects/`) is never read. For each\n * root (per {@link resolveRoots}), read its DIRECT children (depth 1 only \u2014 a\n * recursive home crawl is a `node_modules` latency bomb) and keep the ones that\n * {@link resolveCandidate} deems live. Entries are deduped by realpath'd\n * `repoRoot` and returned sorted by name.\n */\nexport const discoverInfraKitProjects = async (\n options: DiscoverInfraKitProjectsOptions = {},\n): Promise<DiscoveredProject[]> => {\n const roots = await resolveRoots(options.roots)\n\n const byRepoRoot = new Map<string, DiscoveredProject>()\n\n for (const root of roots) {\n const children = await readRootChildren(root)\n\n for (const child of children) {\n const project = await resolveCandidate(root, child)\n\n if (project && !byRepoRoot.has(project.repoRoot)) {\n byRepoRoot.set(project.repoRoot, project)\n }\n }\n }\n\n return [...byRepoRoot.values()].sort((a, b) => {\n return a.name.localeCompare(b.name)\n })\n}\n", "/**\n * The only module in the package that spawns an installer.\n *\n * Everything it may run has already been narrowed by {@link assessRecipe}; what is added here is the\n * execution environment those recipes get, and the two refusals that are about WHO is asking rather\n * than about the recipe itself.\n */\nimport { spawnSync } from 'node:child_process'\nimport { homedir } from 'node:os'\nimport process from 'node:process'\n\nimport { assessRecipe, formatRecipe } from 'src/lib/dependency-install/risk-predicate'\nimport type { RiskContext } from 'src/lib/dependency-install/risk-predicate'\nimport type { Recipe } from 'src/lib/dependency-registry'\nimport { isMcpMode } from 'src/lib/mcp-mode'\nimport { packageManagerInstallEnv } from 'src/lib/pm-env'\n\nexport type InstallOutcome =\n | { ran: true; ok: boolean; commands: string[]; failedStep?: string; detail?: string }\n | { ran: false; commands: string[]; refusedBecause: string[] }\n\nexport interface InstallDeps {\n spawnSync?: typeof spawnSync\n env?: NodeJS.ProcessEnv\n /** Reads whether this process is serving MCP. Injected so the guard can be exercised without a server. */\n mcpMode?: () => boolean\n}\n\n/**\n * Environment variables that make Homebrew's installer behave differently from the one a human runs.\n *\n * `install.sh` sets `NONINTERACTIVE=1` itself when stdin is not a TTY and then probes for sudo with\n * `sudo -n`, which never prompts \u2014 so off a TTY it aborts or proceeds depending on whether some\n * unrelated earlier command left a sudo timestamp cached. We never set any of these; scrubbing them\n * means an inherited value cannot silently choose that branch for us either.\n */\nconst BREW_ENV_SCRUB = ['NONINTERACTIVE', 'CI', 'INTERACTIVE', 'HAVE_SUDO_ACCESS', 'SUDO_ASKPASS']\n\nconst scrubbed = (env: NodeJS.ProcessEnv): NodeJS.ProcessEnv => {\n const copy = { ...packageManagerInstallEnv(env) }\n\n for (const key of BREW_ENV_SCRUB) delete copy[key]\n\n return copy\n}\n\n/**\n * Run one recipe, or refuse and hand back the argv.\n *\n * Two refusals, and they are about different things. {@link assessRecipe} answers \"is this recipe safe\n * to run unattended at all\"; the MCP guard answers \"may THIS caller run it\" \u2014 an MCP server has no\n * human watching and is the wrong authority regardless of how safe the recipe is.\n */\nexport const runRecipe = (recipe: Recipe, context: RiskContext, deps: InstallDeps = {}): InstallOutcome => {\n const spawn = deps.spawnSync ?? spawnSync\n const env = deps.env ?? process.env\n const inMcpMode = (deps.mcpMode ?? isMcpMode)()\n const commands = formatRecipe(recipe)\n const verdict = assessRecipe(recipe, context)\n\n if (!verdict.executable) return { ran: false, commands, refusedBecause: verdict.reasons }\n\n // Deliberately AFTER the recipe verdict and independent of it: a caller that is allowed to ask is\n // still refused an unsafe recipe, and a safe recipe is still refused to a caller with no human.\n if (inMcpMode) return { ran: false, commands, refusedBecause: ['mcp-mode'] }\n\n return execute(recipe, spawn, env, commands)\n}\n\n/**\n * `cwd` is pinned to the home directory as a security control, not for tidiness: npm resolves\n * `registry=` from an `.npmrc` relative to the cwd, so inheriting the caller's would let any repo you\n * happen to be standing in redirect an `install -g` to a registry of its choosing and run that\n * package's lifecycle scripts. Stripping env vars cannot close that \u2014 the redirect lives in a file.\n */\nconst execute = (\n recipe: Recipe,\n spawn: typeof spawnSync,\n env: NodeJS.ProcessEnv,\n commands: string[],\n): InstallOutcome => {\n const childEnv = scrubbed(env)\n\n for (const [index, step] of recipe.steps.entries()) {\n const result = spawn(step[0] as string, step.slice(1), {\n stdio: 'inherit',\n cwd: homedir(),\n env: childEnv,\n })\n\n const failure = describeFailure(result)\n\n // Stop at the first failure rather than pressing on: doppler's second step is meaningless without\n // gnupg, and a \"successful\" install that skipped signature verification is the worst outcome here.\n if (failure !== null) return { ran: true, ok: false, commands, failedStep: commands[index], detail: failure }\n }\n\n return { ran: true, ok: true, commands }\n}\n\n/**\n * Name the failure mode rather than collapsing it into an exit code.\n *\n * An `EACCES` from a global install is reported with the command, never retried under `sudo`: silently\n * escalating privileges to write a global directory is not something an installer may decide to do.\n */\nconst describeFailure = (result: ReturnType<typeof spawnSync>): string | null => {\n if (result.error) return `could not start: ${result.error.message}`\n if (result.signal) return `terminated by signal ${result.signal}`\n if (result.status !== 0) return `exited ${result.status ?? 'unknown'}`\n\n return null\n}\n", "/**\n * Which recipes may run unattended, decided by computation rather than by a hand-set flag per recipe.\n *\n * This is the control that ships INSIDE the CLI, so it is the one that still holds on a machine with no\n * `.claude/settings.json`, on a Claude Code older than v2.1.199, and on any host that is not Claude Code\n * at all \u2014 every place where the `anthropic/requiresUserInteraction` prompt is silently ignored.\n */\nimport type { DependencyManager, Recipe } from 'src/lib/dependency-registry'\n\n/** Why a recipe may not run. `printed` is the only outcome an agent ever sees for a refusal. */\nexport type RefusalReason = 'needs-sudo' | 'fetches-network-script' | 'manager-absent' | 'manager-mismatch'\n\nexport type RiskVerdict = { executable: true } | { executable: false; reasons: RefusalReason[] }\n\n/** What the probe already established about the tool this recipe targets. */\nexport interface RiskContext {\n /** The manager that currently owns the binary, or null when the tool is absent. */\n owner: DependencyManager | null\n /** Managers observed to be present on this host \u2014 `brew` for a keg recipe, `npm` for an npm one. */\n present: readonly DependencyManager[]\n}\n\n/**\n * Which manager a recipe drives, read from its own first token.\n *\n * Derived from the argv rather than declared beside it, so a recipe cannot claim to be an `npm` one\n * while invoking `brew`. `null` means the recipe drives no package manager at all \u2014 which is only ever\n * true of the two bootstrap recipes, and those are already refused on the static conjuncts below.\n */\nconst managerDriving = (recipe: Recipe): DependencyManager | null => {\n const commands = new Set(\n recipe.steps.map((step) => {\n return step[0]\n }),\n )\n\n if (commands.has('brew')) return 'homebrew'\n if (commands.has('npm')) return 'npm'\n // `aws update` is the vendor's own updater, which only a script install has.\n if (commands.has('aws')) return 'script'\n\n return null\n}\n\n/**\n * The two STATIC conjuncts. Applied UNCONDITIONALLY to whichever recipe was selected \u2014 no branch here\n * reads {@link RiskContext}, the manager, or any probe result.\n *\n * That unconditional application is the whole non-circularity argument. Both dangerous recipes fail on\n * these two, so refusing them never depends on detection having been right; detection can then only\n * ever NARROW what executes, never widen it. Making either of these contingent on `identify()` output\n * is the mutation that must redden (`\u00A78 #6`).\n */\nconst staticRefusals = (recipe: Recipe): RefusalReason[] => {\n const reasons: RefusalReason[] = []\n\n if (recipe.needsSudo) reasons.push('needs-sudo')\n if (recipe.fetchesNetworkScript) reasons.push('fetches-network-script')\n\n return reasons\n}\n\n/**\n * The two DETECTION conjuncts: the manager this recipe drives must already be present, and it must be\n * the one that currently owns the binary (or the binary must be absent, in which case the recipe's own\n * manager is by construction the canonical one for it).\n */\nconst detectionRefusals = (recipe: Recipe, context: RiskContext): RefusalReason[] => {\n const manager = managerDriving(recipe)\n\n if (manager === null) return []\n\n const reasons: RefusalReason[] = []\n\n if (!context.present.includes(manager)) reasons.push('manager-absent')\n if (context.owner !== null && context.owner !== manager) reasons.push('manager-mismatch')\n\n return reasons\n}\n\n/**\n * Is this recipe safe to run without a human watching?\n *\n * Every conjunct is derived \u2014 none is a per-recipe boolean saying \"this one is fine\" \u2014 so each gets its\n * own mutation test. A four-way conjunction with one dead term is otherwise indistinguishable from a\n * three-way one.\n */\nexport const assessRecipe = (recipe: Recipe, context: RiskContext): RiskVerdict => {\n const reasons = [...staticRefusals(recipe), ...detectionRefusals(recipe, context)]\n\n return reasons.length === 0 ? { executable: true } : { executable: false, reasons }\n}\n\n/** The argv a refusal prints, one line per step, so the human can run it themselves verbatim. */\nexport const formatRecipe = (recipe: Recipe): string[] => {\n return recipe.steps.map((step) => {\n return step.join(' ')\n })\n}\n", "/**\n * What each of the five tools needs, decided once and shared by every surface.\n *\n * `setup --skip-tools` renders these plans and stops; flagless `setup` runs them. Both reach the SAME\n * function, so the report is literally the dry run of the converge rather than a second implementation\n * that agrees with it until it does not.\n */\nimport { assessRecipe, formatRecipe } from 'src/lib/dependency-install/risk-predicate'\nimport type { RefusalReason, RiskContext } from 'src/lib/dependency-install/risk-predicate'\nimport { defaultProbeDeps, probeAll } from 'src/lib/dependency-probe'\nimport type { DependencyState, ProbeDeps } from 'src/lib/dependency-probe'\nimport { DEPENDENCY_IDS, installOrder, specFor } from 'src/lib/dependency-registry'\nimport type { DependencyId, DependencyManager, Recipe } from 'src/lib/dependency-registry'\n\n/** What would happen to one tool, and whether we are allowed to do it. */\nexport interface DependencyPlan {\n state: DependencyState\n /** `install` when absent, `update` when present, `none` when this manager must not be touched by us. */\n action: 'install' | 'update' | 'none'\n recipe: Recipe | null\n executable: boolean\n refusedBecause: RefusalReason[]\n /**\n * The EXACT context this plan's verdict was reached under, carried so the executor re-assesses against\n * the same thing rather than re-deriving `owner` itself.\n *\n * It was derived twice, and the two derivations disagreed: an absent tool has `manager: 'unknown'`, so\n * passing `state.manager` as the owner made every install refuse as `manager-mismatch` while the plan\n * said it was executable. Install was unreachable, and injecting a fake executor in the tests hid it.\n */\n context: RiskContext\n /** The argv, one runnable line per step \u2014 printed whether or not we may run it ourselves. */\n commands: string[]\n}\n\n/** The managers observed present, derived from the probe results rather than asked for separately. */\nexport const presentManagers = (states: readonly DependencyState[]): DependencyManager[] => {\n const present: DependencyManager[] = []\n const brewPresent = states.some((state) => {\n return state.id === 'brew' && state.present\n })\n const awsIsScriptInstalled = states.some((state) => {\n return state.id === 'aws' && state.manager === 'script'\n })\n\n // `brew` present at all means the homebrew manager is available, whoever owns the individual tools.\n if (brewPresent) present.push('homebrew')\n // npm ships with the node running this process, so it is present by construction.\n present.push('npm')\n if (awsIsScriptInstalled) present.push('script')\n\n return present\n}\n\n/** Install when absent, update when present. The single place that choice is made. */\nconst remediationFor = (state: DependencyState): Recipe | null => {\n const spec = specFor(state.id)\n\n return state.present ? spec.updateFor(state.manager) : spec.bootstrapInstall\n}\n\nconst planFor = (state: DependencyState, context: RiskContext): DependencyPlan => {\n const recipe = remediationFor(state)\n\n // `owner` is per-tool; the shared context carries only which managers exist on the box. An ABSENT tool\n // has no owner \u2014 `manager: 'unknown'` means \"no layout we recognise\", not \"owned by someone else\" \u2014\n // and conflating the two is what would refuse every install as a mismatch.\n const toolContext: RiskContext = { ...context, owner: state.present ? state.manager : null }\n\n if (recipe === null) {\n return {\n state,\n action: 'none',\n recipe: null,\n executable: false,\n refusedBecause: [],\n commands: [],\n context: toolContext,\n }\n }\n\n const verdict = assessRecipe(recipe, toolContext)\n\n return {\n state,\n action: state.present ? 'update' : 'install',\n recipe,\n executable: verdict.executable,\n refusedBecause: verdict.executable ? [] : verdict.reasons,\n commands: formatRecipe(recipe),\n context: toolContext,\n }\n}\n\n/** Probe every requested tool and decide what each needs. Reads only \u2014 nothing here spawns an installer. */\nexport const planDependencies = async (\n ids: readonly DependencyId[] = DEPENDENCY_IDS,\n deps: ProbeDeps = defaultProbeDeps(),\n): Promise<{ plans: DependencyPlan[]; context: RiskContext }> => {\n // Probing only the requested ids would hide `brew` from `presentManagers`, so a `--tools gh` run would\n // decide brew is absent and refuse a recipe it should have run. Probe all, plan the subset.\n const states = await probeAll(DEPENDENCY_IDS, deps)\n const context: RiskContext = { owner: null, present: presentManagers(states) }\n // Prerequisite order, not declaration order: `gh` and `doppler` both need `brew`, and today that\n // happens to hold only because `brew` is declared first in the registry. Reordering two rows there\n // would otherwise break installs silently.\n const ordered = installOrder(ids)\n const byId = new Map(\n states.map((state) => {\n return [state.id, state]\n }),\n )\n const plans = ordered\n .filter((id) => {\n return ids.includes(id)\n })\n .map((id) => {\n return planFor(byId.get(id) as DependencyState, context)\n })\n\n return { plans, context }\n}\n", "/**\n * The dependency half of `setup`: bring the five external tools to a working state, or \u2014 under\n * `--skip-tools` \u2014 report what that would take without running any of it.\n *\n * Both entry points reach the SAME {@link planDependencies}, so the probe is the converge's dry run by\n * construction rather than by agreement. The install-or-update choice is made in exactly one place, and\n * this is not it.\n *\n * This is the only copy of the loop. It began as `commands/setup-dependency`'s, which was deleted with\n * that command once `setup` took over its registrations \u2014 the fold happened before either name was ever\n * published, so no alias was owed and none was left behind.\n */\nimport { runRecipe } from 'src/lib/dependency-install'\nimport type { InstallOutcome } from 'src/lib/dependency-install'\nimport { planDependencies } from 'src/lib/dependency-plan'\nimport type { DependencyPlan } from 'src/lib/dependency-plan'\nimport type { ProbeDeps } from 'src/lib/dependency-probe'\nimport type { DependencyId, Recipe } from 'src/lib/dependency-registry'\n\n/** `update` skips a tool that is absent; `converge` installs it. The only difference between the two. */\nexport type SetupMode = 'converge' | 'update'\n\nexport interface ToolResult {\n id: DependencyId\n action: 'installed' | 'updated' | 'skipped' | 'refused' | 'failed'\n before: { present: boolean; onPath: boolean; version: string | null; manager: string }\n commands: string[]\n detail: string\n}\n\nexport interface ConvergeOptions {\n ids: readonly DependencyId[]\n mode: SetupMode\n /** Probe seams, threaded to {@link planDependencies}. Tests inject fixtures; production omits it. */\n probeDeps?: ProbeDeps\n /** The executor. Injected so a test can assert what WOULD have run without running it. */\n run?: typeof runRecipe\n}\n\nconst skipped = (plan: DependencyPlan, detail: string): ToolResult => {\n return { ...describeBefore(plan), action: 'skipped', commands: plan.commands, detail }\n}\n\nconst describeBefore = (plan: DependencyPlan): Pick<ToolResult, 'id' | 'before'> => {\n return {\n id: plan.state.id,\n before: {\n present: plan.state.present,\n onPath: plan.state.onPath,\n version: plan.state.version,\n manager: plan.state.manager,\n },\n }\n}\n\n/** What this plan means for THIS mode, before any execution is attempted. */\nconst preflight = (plan: DependencyPlan, mode: SetupMode): ToolResult | null => {\n if (plan.action === 'none') {\n return skipped(plan, `nothing to do: ${plan.state.manager} installs are not managed from here`)\n }\n\n // The one behavioural difference between the two modes, stated once.\n if (mode === 'update' && plan.action === 'install') {\n return skipped(plan, 'not installed, and --update only updates \u2014 drop the flag to install it')\n }\n\n if (!plan.executable) {\n return {\n ...describeBefore(plan),\n action: 'refused',\n commands: plan.commands,\n detail: `refused (${plan.refusedBecause.join(', ')}) \u2014 run the commands above yourself`,\n }\n }\n\n return null\n}\n\nconst toResult = (plan: DependencyPlan, outcome: InstallOutcome): ToolResult => {\n if (!outcome.ran) {\n return {\n ...describeBefore(plan),\n action: 'refused',\n commands: outcome.commands,\n detail: `refused (${outcome.refusedBecause.join(', ')}) \u2014 run the commands above yourself`,\n }\n }\n\n if (!outcome.ok) {\n return {\n ...describeBefore(plan),\n action: 'failed',\n commands: outcome.commands,\n detail: `${outcome.failedStep ?? 'a step'} ${outcome.detail ?? 'failed'}`,\n }\n }\n\n return {\n ...describeBefore(plan),\n action: plan.action === 'install' ? 'installed' : 'updated',\n commands: outcome.commands,\n detail: plan.action === 'install' ? 'installed' : 'updated',\n }\n}\n\n/** Install what is missing and update what is present, serial and in registry order. */\nexport const convergeDependencies = async (options: ConvergeOptions): Promise<ToolResult[]> => {\n const run = options.run ?? runRecipe\n const { plans } = await planDependencies(options.ids, options.probeDeps)\n const tools: ToolResult[] = []\n\n // Serial, in registry order, because the recipes have prerequisites: gh and doppler both need brew,\n // and doppler's own two steps must not interleave with another tool's.\n for (const plan of plans) {\n const stop = preflight(plan, options.mode)\n\n if (stop !== null) {\n tools.push(stop)\n continue\n }\n\n // `plan.recipe` is non-null here: `preflight` returns a stop for `action: 'none'`, which is the only\n // plan that carries a null recipe.\n // `plan.context`, never a fresh derivation: the executor re-assesses, and a second derivation of\n // `owner` here is exactly the bug that made every install refuse as `manager-mismatch`.\n tools.push(toResult(plan, run(plan.recipe as Recipe, plan.context)))\n }\n\n return tools\n}\n\n/**\n * `--skip-tools`: the same plans, rendered and not run.\n *\n * It never reaches an executor \u2014 not a stubbed one, not an injected one, none \u2014 which is the whole\n * property the flag exists to carry, so there is deliberately no `run` seam on this path to forget to\n * disable. A plan the risk predicate refuses still reports as `refused`, because its argv is exactly\n * what this mode exists to print.\n */\nexport const probeDependencies = async (ids: readonly DependencyId[], probeDeps?: ProbeDeps): Promise<ToolResult[]> => {\n const { plans } = await planDependencies(ids, probeDeps)\n\n return plans.map((plan) => {\n return preflight(plan, 'converge') ?? skipped(plan, `would ${plan.action} \u2014 nothing was run (--skip-tools)`)\n })\n}\n", "/**\n * One command that sets a machine up: the local, offline `initCore` half first, then the dependency\n * converge, then one combined summary.\n *\n * The order is not arbitrary. The init half is local, cheap, and one of its steps (`.mcp.json` plus the\n * plugin install) is what makes the MCP surface usable at all, so it must not sit behind a network\n * converge. The refused recipes' manual commands are the last thing the human reads, and the\n * `source ~/.zshrc` reminder is last of all.\n *\n * Both halves ALWAYS run: neither short-circuits the other, an init-half throw is recorded and the\n * dependency half still runs, and the run exits non-zero if either half hard-failed. A `risk-predicate`\n * refusal is not a failure \u2014 its argv is printed for a human, and the run still succeeds.\n */\nimport process from 'node:process'\nimport { z } from 'zod'\n\nimport { InitStepError, SHELL_ACTIVATION_REMINDER, initCore, logInitEntry } from 'src/commands/init'\nimport type { InitEntry, InitStep, InitStepName } from 'src/commands/init'\nimport type { runRecipe } from 'src/lib/dependency-install'\nimport type { ProbeDeps } from 'src/lib/dependency-probe'\nimport { DEPENDENCY_IDS } from 'src/lib/dependency-registry'\nimport type { DependencyId } from 'src/lib/dependency-registry'\nimport { logger } from 'src/lib/logger'\nimport { defineMcpTool, textContent } from 'src/types'\n\nimport { convergeDependencies, probeDependencies } from './converge'\nimport type { SetupMode, ToolResult } from './converge'\n\nexport interface SetupOptions {\n /** `--tools <ids...>`: converge only these. */\n tools?: DependencyId[]\n /** `--update [ids...]`: update what is present and never install; `true` means every tool. */\n update?: boolean | DependencyId[]\n /** `--skip-tools`: the read-only probe. Reports what is missing and the argv that would fix it. */\n skipTools?: boolean\n /** Probe seams for tests; production omits it. */\n probeDeps?: ProbeDeps\n /** The executor, injected so a test can assert what WOULD have run. Never reached under `skipTools`. */\n run?: typeof runRecipe\n}\n\n/**\n * A usage error rather than a precedence rule, deliberately.\n *\n * Every precedence answer is wrong here: silently ignoring `--skip-tools` installs software the caller\n * asked not to install, and silently ignoring `--tools` runs against a set they did not choose. There is\n * no reading of the combination that does what someone typing it meant.\n */\nexport const SKIP_TOOLS_CONFLICT =\n '--skip-tools cannot be combined with --tools or --update: --skip-tools installs and updates nothing at all, so narrowing what it would act on is a contradiction. Use one or the other.'\n\ninterface ResolvedRequest {\n ids: readonly DependencyId[]\n mode: SetupMode\n skipTools: boolean\n}\n\nconst resolveRequest = (options: SetupOptions): ResolvedRequest => {\n const updateIds = Array.isArray(options.update) ? options.update : undefined\n const updating = options.update === true || updateIds !== undefined\n const skipTools = options.skipTools === true\n\n if (skipTools && (options.tools !== undefined || updating)) throw new Error(SKIP_TOOLS_CONFLICT)\n\n return {\n ids: options.tools ?? updateIds ?? DEPENDENCY_IDS,\n mode: updating ? 'update' : 'converge',\n skipTools,\n }\n}\n\n/**\n * Which step an untagged throw belongs to.\n *\n * `initCore` tags every step it runs, so the fallback covers only a throw from outside one \u2014 the last\n * step that reported is the furthest the run is known to have got, and an empty report means it died in\n * the first step.\n */\nconst failedStep = (err: unknown, completed: InitEntry[]): InitStepName => {\n if (err instanceof InitStepError) return err.step\n\n return completed.at(-1)?.step ?? 'zshrc'\n}\n\n/** Run the init half, printing as it goes, and report a throw as an entry instead of propagating it. */\nconst runInitHalf = async (): Promise<{ entries: InitEntry[]; failed: boolean }> => {\n const entries: InitEntry[] = []\n const sink = (entry: InitEntry): void => {\n entries.push(entry)\n\n // Everything prints where it happens \u2014 except the activation reminder, which `setup` holds back so\n // it lands after the dependency summary rather than in the middle of the run.\n if (entry.message !== SHELL_ACTIVATION_REMINDER) logInitEntry(entry)\n }\n\n try {\n await initCore(sink)\n\n return { entries, failed: false }\n } catch (err) {\n const entry: InitEntry = {\n step: failedStep(err, entries),\n outcome: 'warned',\n message: `The ${failedStep(err, entries)} step failed: ${err instanceof Error ? err.message : String(err)}`,\n level: 'warn',\n }\n\n logInitEntry(entry)\n entries.push(entry)\n\n return { entries, failed: true }\n }\n}\n\n/** One line per tool, then the argv a human has to run themselves, then the activation reminder. */\nconst printSummary = (tools: ToolResult[], skipTools: boolean): void => {\n for (const tool of tools) {\n logger.info(` ${tool.action.padEnd(9)} ${tool.id} \u2014 ${tool.detail}`)\n }\n\n for (const tool of tools) {\n if (!needsManualRun(tool, skipTools)) continue\n\n logger.info(`Run these yourself to ${skipTools ? 'set up' : 'finish'} ${tool.id}:`)\n\n for (const command of tool.commands) {\n logger.info(` ${command}`)\n }\n }\n\n logger.info(SHELL_ACTIVATION_REMINDER)\n}\n\n/** A tool whose argv the human has to run: every refusal, and \u2014 under the probe \u2014 everything not run. */\nconst needsManualRun = (tool: ToolResult, skipTools: boolean): boolean => {\n if (tool.commands.length === 0) return false\n\n return tool.action === 'refused' || (skipTools && tool.action === 'skipped')\n}\n\n/** The MCP payload carries the three declared keys and not the CLI's rendering hint. */\nconst toInitStep = (entry: InitEntry): InitStep => {\n return { step: entry.step, outcome: entry.outcome, message: entry.message }\n}\n\nexport const setup = async (options: SetupOptions = {}) => {\n const request = resolveRequest(options)\n const init = await runInitHalf()\n const tools = request.skipTools\n ? await probeDependencies(request.ids, options.probeDeps)\n : await convergeDependencies({\n ids: request.ids,\n mode: request.mode,\n probeDeps: options.probeDeps,\n run: options.run,\n })\n\n printSummary(tools, request.skipTools)\n\n const structuredContent = {\n init: init.entries.map(toInitStep),\n tools,\n converged: !request.skipTools,\n changed: tools.some((tool) => {\n return tool.action === 'installed' || tool.action === 'updated'\n }),\n allSucceeded: tools.every((tool) => {\n return tool.action !== 'failed'\n }),\n }\n\n // The action owns the exit code, as `vendor-config` and `local-deploy` do: nothing downstream of here\n // reads the payload's booleans, so a hard failure in EITHER half has to be turned into one here or the\n // command exits 0 having failed.\n if (init.failed || !structuredContent.allSucceeded) process.exitCode = 1\n\n return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }\n}\n\nconst initStepSchema = z.object({\n step: z\n .enum(['zshrc', 'migrations', 'user-config', 'guidance', 'plugin-pointer', 'mcp-server', 'project-config', 'shell'])\n .describe('Which init step this reports on'),\n outcome: z\n .enum(['written', 'unchanged', 'skipped', 'warned'])\n .describe('What that step did: changed something, found nothing to change, did not run, or failed non-fatally'),\n message: z.string().describe('The same line a human running this would have read'),\n})\n\nconst resultSchema = z.object({\n id: z.string().describe('Dependency id'),\n action: z.enum(['installed', 'updated', 'skipped', 'refused', 'failed']).describe('What happened'),\n before: z\n .object({\n present: z.boolean(),\n onPath: z.boolean(),\n version: z.string().nullable(),\n manager: z.string(),\n })\n .describe('The tool\u2019s state before this run'),\n commands: z.array(z.string()).describe('The exact commands, one per step \u2014 printed even when refused'),\n detail: z.string().describe('Why, in one line'),\n})\n\nconst outputSchema = {\n init: z.array(initStepSchema).describe('One entry per init step, in the order they ran'),\n tools: z.array(resultSchema).describe('One result per requested dependency'),\n converged: z.boolean().describe('Whether the dependency step could act at all (false under skipTools)'),\n changed: z.boolean().describe('Whether anything was installed or updated'),\n allSucceeded: z.boolean().describe('Whether no tool failed (a refusal is not a failure)'),\n}\n\nconst inputSchema = {\n tools: z\n .array(z.enum(DEPENDENCY_IDS as [DependencyId, ...DependencyId[]]))\n .optional()\n .describe('Which tools to act on. Omit for all five.'),\n mode: z\n .enum(['converge', 'update'])\n .optional()\n .describe('converge (default): install what is missing and update the rest. update: never install.'),\n skipTools: z\n .boolean()\n .optional()\n .describe('Read-only: run the init half, then REPORT what each tool needs without installing anything.'),\n}\n\n/**\n * Two gates fire in series on an MCP call, and that is intended rather than redundant.\n *\n * `_meta[\"anthropic/requiresUserInteraction\"]` makes the HOST prompt a human on every call \u2014 in\n * `acceptEdits`, `auto` and `bypassPermissions` alike, with allow rules unable to skip it. The confirm\n * gate in `lib/tool-handler` then makes the AGENT re-call with a token bound to these arguments, which\n * raises the host prompt a second time. One successful install therefore costs the human two prompts.\n *\n * Neither substitutes for the other: `requiresHumanConfirm` is required for every exposed mutating tool\n * (`command-catalog.test.ts`) and is the only gate left on a host that ignores the annotation \u2014 anything\n * below Claude Code v2.1.199, and anything that is not Claude Code. Neither substitutes for the computed\n * refusal in `lib/dependency-install/risk-predicate`, which is the only control shipping inside the CLI.\n */\n// Both fire UNCONDITIONALLY, `skipTools` included. The read path that raises no prompt is `doctor` \u2014 a\n// separate tool name, therefore a separate permission identity, which is why the two are not one tool.\nconst REQUIRES_USER_INTERACTION = { 'anthropic/requiresUserInteraction': true } as const\n\nexport const setupMcpTool = defineMcpTool({\n name: 'setup',\n description:\n 'Set this machine up in one call: inject the shell integration into .zshrc, run the config migrations, seed the user-global config, refresh the agent-instruction files, register the Claude Code plugin pointer and the infra-kit MCP server, then bring brew, aws, gh, doppler and portless to a working state \u2014 installing what is missing and updating what is present. Pass mode:\"update\" to update only and never install, or skipTools:true to do the local setup and then REPORT what each tool needs without installing anything. Recipes that need sudo or pipe a script fetched over the network \u2014 the Homebrew bootstrap and the first AWS CLI install \u2014 are never run; they are reported with the exact commands for you to run yourself. Use `doctor` first to see the state of this machine without changing it.',\n inputSchema,\n outputSchema,\n requiresHumanConfirm: true,\n meta: REQUIRES_USER_INTERACTION,\n handler: (params: { tools?: DependencyId[]; mode?: SetupMode; skipTools?: boolean }) => {\n return setup({ tools: params.tools, update: params.mode === 'update', skipTools: params.skipTools })\n },\n})\n", "import { existsSync } from 'node:fs'\nimport path from 'node:path'\nimport process from 'node:process'\nimport { z } from 'zod'\n\nimport { getProjectRoot } from 'src/lib/git-utils'\nimport { logger } from 'src/lib/logger'\nimport { MANIFEST_FILE, VENDOR_DIR, compareToManifest, isUnknownSchema, readManifest } from 'src/lib/vendor'\nimport { defineMcpTool, textContent } from 'src/types'\n\n/** Max drifted paths to print per category before truncating. */\nconst MAX_REPORTED = 30\n\ntype VendorCheckStatus = 'clean' | 'drift' | 'skipped' | 'missing-manifest' | 'unknown-schema'\n\ninterface VendorCheckOptions {\n /** Repo root to check. Defaults to the git toplevel. */\n cwd?: string\n}\n\nconst report = (label: string, list: string[]): void => {\n if (list.length === 0) {\n return\n }\n\n logger.info(`\\n${label} (${list.length}):`)\n\n for (const rel of list.slice(0, MAX_REPORTED)) {\n logger.info(` ${rel}`)\n }\n\n if (list.length > MAX_REPORTED) {\n logger.info(` \u2026and ${list.length - MAX_REPORTED} more`)\n }\n}\n\n/**\n * Verify that files under `vendor/` still match the checksums recorded in\n * `vendor/.sync-manifest.json`. Config-free and self-contained (no source repo,\n * no `vendor.config.ts`, no rsync) so it runs in any consumer's CI. Read-only \u2014\n * never calls `process.exit`; the CLI action maps `structuredContent.ok` to the\n * exit code, and the MCP tool reuses the same handler.\n *\n * Exit-code contract (preserved from the legacy `vendor-check.mjs`):\n * clean \u2192 ok; drift \u2192 not ok; missing `vendor/` \u2192 ok (skip);\n * missing manifest \u2192 not ok; unknown (newer) schemaVersion \u2192 not ok (fail-closed).\n *\n * @example\n * await vendorCheck() // checks the current repo's vendor/\n * await vendorCheck({ cwd: '/repo' }) // checks a specific root (tests)\n */\nexport const vendorCheck = async (options: VendorCheckOptions = {}) => {\n const root = options.cwd ?? (await getProjectRoot())\n const vendorRoot = path.join(root, VENDOR_DIR)\n\n const base = { modified: [] as string[], added: [] as string[], removed: [] as string[], fileCount: 0 }\n\n if (!existsSync(vendorRoot)) {\n logger.info(`\u2139\uFE0F No ${VENDOR_DIR}/ folder found \u2014 nothing to check.`)\n const structuredContent = { status: 'skipped' as VendorCheckStatus, ok: true, ...base }\n\n return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }\n }\n\n if (!existsSync(path.join(vendorRoot, MANIFEST_FILE))) {\n logger.error(`\u274C Missing ${VENDOR_DIR}/${MANIFEST_FILE}. Re-run the vendor sync to generate it.`)\n const structuredContent = { status: 'missing-manifest' as VendorCheckStatus, ok: false, ...base }\n\n return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }\n }\n\n const manifest = readManifest(vendorRoot)\n\n if (isUnknownSchema(manifest)) {\n logger.error(\n `\u274C ${VENDOR_DIR}/${MANIFEST_FILE} has a newer schemaVersion (${String(manifest.schemaVersion)}) than this CLI supports. Upgrade infra-kit.`,\n )\n const structuredContent = { status: 'unknown-schema' as VendorCheckStatus, ok: false, ...base }\n\n return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }\n }\n\n const { modified, added, removed } = compareToManifest(vendorRoot, manifest)\n const drifted = modified.length > 0 || added.length > 0 || removed.length > 0\n\n if (drifted) {\n logger.error(`\u274C ${VENDOR_DIR}/ has drifted from ${VENDOR_DIR}/${MANIFEST_FILE}.`)\n logger.error(' These files are mirrored from the source repo \u2014 edit them upstream, not here.')\n report('Modified', modified)\n report('Added (not in manifest)', added)\n report('Removed (in manifest, now missing)', removed)\n } else {\n logger.info(`\u2705 ${VENDOR_DIR}/ matches manifest (${manifest.fileCount} files).`)\n }\n\n const structuredContent = {\n status: (drifted ? 'drift' : 'clean') as VendorCheckStatus,\n ok: !drifted,\n fileCount: manifest.fileCount,\n modified,\n added,\n removed,\n }\n\n return { content: textContent(JSON.stringify(structuredContent, null, 2)), structuredContent }\n}\n\nconst vendorCheckOutputSchema = {\n status: z.enum(['clean', 'drift', 'skipped', 'missing-manifest', 'unknown-schema']),\n ok: z.boolean().describe('Whether the vendor tree matches its manifest (false \u2192 CI should fail)'),\n fileCount: z.number(),\n modified: z.array(z.string()),\n added: z.array(z.string()),\n removed: z.array(z.string()),\n}\n\n// MCP Tool Registration (read-only; safe to expose).\nexport const vendorCheckMcpTool = defineMcpTool({\n name: 'vendor-check',\n description:\n 'Verify that files under vendor/ match the checksums in vendor/.sync-manifest.json. Self-contained (no source repo or config needed). Returns ok=false on drift, missing manifest, or an unknown future schemaVersion; ok=true when clean or when no vendor/ folder exists.',\n inputSchema: {},\n outputSchema: vendorCheckOutputSchema,\n handler: () => {\n return vendorCheck({ cwd: process.cwd() })\n },\n})\n", "import { createHash } from 'node:crypto'\nimport { readFileSync } from 'node:fs'\n\n/**\n * SHA-256 of a file's contents, hex-encoded. Matches the legacy scripts'\n * `createHash('sha256').update(readFileSync(path)).digest('hex')`.\n *\n * `readFileSync` follows symlinks, so a symlinked file is hashed by its target\n * content \u2014 the behavior both legacy scripts already relied on.\n *\n * @example\n * sha256('/repo/vendor/configs/eslint-config/index.js') // => 'a1b2c3...'\n */\nexport const sha256 = (filePath: string): string => {\n return createHash('sha256').update(readFileSync(filePath)).digest('hex')\n}\n", "import { readFileSync, writeFileSync } from 'node:fs'\nimport path from 'node:path'\nimport { z } from 'zod'\n\nimport { sha256 } from './hash'\nimport { isSkippedPath } from './skip-sets'\nimport { walkVendorTree } from './walk'\n\n/** Filename of the integrity manifest inside a `vendor/` tree. */\nexport const MANIFEST_FILE = '.sync-manifest.json'\n\n/**\n * Manifest schema version the CLI understands. A manifest with a HIGHER version\n * than this was written by a newer CLI and is treated as drift (fail-closed) \u2014\n * see {@link isUnknownSchema}. Legacy manifests carry no `schemaVersion` field\n * and read as compatible.\n */\nexport const CURRENT_SCHEMA_VERSION = 1\n\n/**\n * `schemaVersion` is OPTIONAL with no default: legacy manifests written by\n * `writeVendorMeta` have no such field, and a required field would make\n * `vendor check` fail-closed on every existing manifest. Absent is read as\n * \"compatible\" (current). `writeManifest` always emits the current version.\n */\nconst manifestSchema = z.object({\n schemaVersion: z.number().optional(),\n source: z.string(),\n commit: z.string(),\n syncedAt: z.string(),\n fileCount: z.number(),\n files: z.record(z.string(), z.string()),\n})\n\nexport type VendorManifest = z.infer<typeof manifestSchema>\n\nexport interface ManifestDiff {\n modified: string[]\n added: string[]\n removed: string[]\n}\n\n/**\n * Whether a manifest's `schemaVersion` is newer than this CLI supports. Absent\n * (legacy) reads as compatible; only a strictly-greater version is unknown.\n */\nexport const isUnknownSchema = (manifest: VendorManifest): boolean => {\n return (manifest.schemaVersion ?? CURRENT_SCHEMA_VERSION) > CURRENT_SCHEMA_VERSION\n}\n\n/**\n * Walk the vendor tree and hash every file into a `{ relativePath: sha256 }`\n * map (sorted by walk order \u2192 stable). A file that cannot be read (e.g. a\n * dangling symlink) surfaces an explicit error rather than crashing with a raw\n * fs stack.\n */\nexport const buildFilesMap = (vendorRoot: string): Record<string, string> => {\n const files: Record<string, string> = {}\n\n for (const rel of walkVendorTree(vendorRoot)) {\n try {\n files[rel] = sha256(path.join(vendorRoot, rel))\n } catch (cause) {\n const reason = cause instanceof Error ? cause.message : String(cause)\n\n throw new Error(`Cannot hash vendor file \"${rel}\" (broken symlink or unreadable file): ${reason}`)\n }\n }\n\n return files\n}\n\n/**\n * Read and validate the manifest at `<vendorRoot>/.sync-manifest.json`. Throws a\n * descriptive error if the file is missing or malformed.\n */\nexport const readManifest = (vendorRoot: string): VendorManifest => {\n const manifestPath = path.join(vendorRoot, MANIFEST_FILE)\n\n let raw: string\n\n try {\n raw = readFileSync(manifestPath, 'utf8')\n } catch {\n throw new Error(`Missing ${MANIFEST_FILE} at ${manifestPath}`)\n }\n\n const parsed = manifestSchema.safeParse(JSON.parse(raw))\n\n if (!parsed.success) {\n throw new Error(`Invalid ${MANIFEST_FILE} at ${manifestPath}: ${z.prettifyError(parsed.error)}`)\n }\n\n return parsed.data\n}\n\n/**\n * Build and write a fresh manifest for the current content of `vendorRoot`.\n * Emits the CURRENT schema version, the supplied `source`/`commit`, an ISO\n * `syncedAt`, and the per-file checksum map. Returns the written manifest.\n *\n * The `files` map and field set (minus `schemaVersion`) match legacy\n * `writeVendorMeta`, so a regenerated manifest differs from a legacy one only by\n * the added `schemaVersion` and the `syncedAt` timestamp.\n */\nexport const writeManifest = (vendorRoot: string, meta: { source: string; commit: string }): VendorManifest => {\n const files = buildFilesMap(vendorRoot)\n\n const manifest: VendorManifest = {\n schemaVersion: CURRENT_SCHEMA_VERSION,\n source: meta.source,\n commit: meta.commit,\n syncedAt: new Date().toISOString(),\n fileCount: Object.keys(files).length,\n files,\n }\n\n writeFileSync(path.join(vendorRoot, MANIFEST_FILE), `${JSON.stringify(manifest, null, 2)}\\n`)\n\n return manifest\n}\n\n/**\n * Compare the live `vendorRoot` tree against a manifest's recorded checksums.\n * Returns the modified / added (present, not in manifest) / removed (in\n * manifest, now missing) relative paths.\n *\n * Entries the current skip rules exclude are dropped from the manifest side\n * first: a manifest issued before a skip rule existed still records those\n * paths, and comparing them against a walk that no longer emits them would\n * report drift on files the rule says to ignore.\n */\nexport const compareToManifest = (vendorRoot: string, manifest: VendorManifest): ManifestDiff => {\n const expected = Object.fromEntries(\n Object.entries(manifest.files).filter(([rel]) => {\n return !isSkippedPath(rel)\n }),\n )\n const actualFiles = walkVendorTree(vendorRoot)\n\n const modified: string[] = []\n const added: string[] = []\n\n for (const rel of actualFiles) {\n if (!(rel in expected)) {\n added.push(rel)\n continue\n }\n\n if (sha256(path.join(vendorRoot, rel)) !== expected[rel]) {\n modified.push(rel)\n }\n }\n\n const actualSet = new Set(actualFiles)\n const removed = Object.keys(expected).filter((rel) => {\n return !actualSet.has(rel)\n })\n\n return { modified, added, removed }\n}\n", "/**\n * Skip rules for walking a `vendor/` tree to build or verify its integrity\n * manifest. This is the SINGLE source of truth consumed by both the read path\n * (`vendor check`) and the write path (`vendor sync`/`manifest`).\n *\n * The set below is a strict superset of the legacy read script's\n * `MANIFEST_SKIP_DIRS` (`scripts/vendor-check.mjs`). It additionally skips\n * `.vitest-attachments` \u2014 a git-ignored directory (see repo `.gitignore`) that\n * therefore never appears in a committed vendor tree. Widening the read path's\n * skip set this way is deliberate and behavior-preserving: no existing manifest\n * changes, and the read and write paths share this single set so they cannot\n * diverge.\n */\n/** Root of the mirrored tree inside each repo. Single source for read + write paths. */\nexport const VENDOR_DIR = 'vendor'\n\nexport const MANIFEST_SKIP_DIRS: ReadonlySet<string> = new Set([\n 'node_modules',\n 'dist',\n '.turbo',\n '.omc',\n '__screenshots__',\n '.vitest-attachments',\n '.output',\n '.source',\n '.nitro',\n '.tanstack',\n])\n\n/**\n * `routeTree.gen.ts` is written by the TanStack Router generator on every dev\n * run and build. Its route order is not stable across machines, so a consumer\n * that merely opens the docs app rewrites the file into an equivalent-but-\n * different form and trips the integrity check on a file nobody edited. It is\n * still mirrored by the sync (a fresh consumer needs it for `tsc --noEmit`);\n * only its checksum is untracked.\n */\nexport const MANIFEST_SKIP_FILES: ReadonlySet<string> = new Set([\n '.sync-manifest.json',\n '.eslintcache',\n 'log.txt',\n 'routeTree.gen.ts',\n])\n\nexport const MANIFEST_SKIP_SUFFIXES: readonly string[] = ['.tsbuildinfo']\n\n/** Whether a directory name should be skipped when walking the vendor tree. */\nexport const isSkippedDir = (name: string): boolean => {\n return MANIFEST_SKIP_DIRS.has(name)\n}\n\n/** Whether a file name should be skipped when walking the vendor tree. */\nexport const isSkippedFile = (name: string): boolean => {\n if (MANIFEST_SKIP_FILES.has(name)) {\n return true\n }\n\n return MANIFEST_SKIP_SUFFIXES.some((suffix) => {\n return name.endsWith(suffix)\n })\n}\n\n/**\n * Whether a POSIX-relative path inside the vendor tree is skipped \u2014 the same\n * rules `walkVendorTree` applies, but expressed over a recorded path instead of\n * a live directory entry.\n *\n * This is what lets a skip rule be ADDED without reissuing every manifest: a\n * manifest written before the rule still lists the path, the walk no longer\n * emits it, and without this filter the comparison would report a phantom\n * \"removed\" for a file that is sitting right there.\n */\nexport const isSkippedPath = (relativePath: string): boolean => {\n const segments = relativePath.split('/')\n const name = segments.pop() ?? ''\n\n const inSkippedDir = segments.some((segment) => {\n return isSkippedDir(segment)\n })\n\n return inSkippedDir || isSkippedFile(name)\n}\n", "import { readdirSync } from 'node:fs'\nimport path from 'node:path'\n\nimport { isSkippedDir, isSkippedFile } from './skip-sets'\n\n/**\n * Convert a (possibly Windows) relative path to a POSIX-normalized form so the\n * manifest is byte-identical across operating systems.\n */\nconst toPosix = (relativePath: string): string => {\n return relativePath.split(path.sep).join('/')\n}\n\n/**\n * Walk a `vendor/` tree and return the relative paths of every non-skipped file,\n * POSIX-normalized and sorted byte-wise. The sort makes the output independent\n * of `readdirSync` order, which keeps generated manifests deterministic.\n *\n * Symlink handling matches the legacy scripts: only REAL directories\n * (`entry.isDirectory()`) are recursed; a symlink is treated as a leaf and its\n * relative path is emitted, to be hashed later by following it to target\n * content (`sha256`/`readFileSync`). This preserves legacy behavior exactly \u2014\n * the manifest checksum reflects target content, never the link itself.\n *\n * @example\n * walkVendorTree('/repo/vendor')\n * // => ['README.md', 'configs/eslint-config/index.js', ...]\n */\nexport const walkVendorTree = (vendorRoot: string): string[] => {\n const acc: string[] = []\n\n const walk = (dir: string): void => {\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n const full = path.join(dir, entry.name)\n\n if (entry.isDirectory()) {\n if (isSkippedDir(entry.name)) {\n continue\n }\n\n walk(full)\n continue\n }\n\n if (isSkippedFile(entry.name)) {\n continue\n }\n\n acc.push(toPosix(path.relative(vendorRoot, full)))\n }\n }\n\n walk(vendorRoot)\n\n return acc.sort()\n}\n", "import { z } from 'zod'\n\nimport { logger } from 'src/lib/logger'\nimport { defineMcpTool, textContent } from 'src/types'\n\nimport packageJson from '../../../package.json' with { type: 'json' }\n\n/**\n * Print the infra-kit CLI version\n */\nexport const version = async () => {\n const cliVersion = packageJson.version\n\n logger.info(cliVersion)\n\n const structuredContent = { version: cliVersion }\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n}\n\n// MCP Tool Registration\nexport const versionMcpTool = defineMcpTool({\n name: 'version',\n description: 'Print the installed infra-kit CLI version',\n inputSchema: {},\n outputSchema: {\n version: z.string().describe('Installed infra-kit CLI version (from package.json)'),\n },\n handler: version,\n})\n", "/* eslint-disable sonarjs/cognitive-complexity */\nimport confirm from '@inquirer/confirm'\nimport path from 'node:path'\nimport { z } from 'zod'\nimport { $ } from 'zx'\n\nimport {\n buildCmuxWorkspaceTitle,\n createCmuxGroupFrom,\n findCmuxGroupRefByName,\n listCmuxWorkspacesByCwd,\n openCmuxWorkspaceWithLayout,\n realpathForCmuxCwd,\n} from 'src/integrations/cmux'\nimport { getReleasePRsWithInfo } from 'src/integrations/gh'\nimport { IDE_MODES, addIdeWorktreeFolders } from 'src/integrations/ide'\nimport type { IdeMode } from 'src/integrations/ide'\nimport { commandEcho, confirmOrExit } from 'src/lib/command-echo'\nimport { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'\nimport { isPromptCancellation } from 'src/lib/errors/is-prompt-cancellation'\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { assertManagementContext } from 'src/lib/git-guard'\nimport { getCurrentWorktrees, getMainRepoRoot, getProjectRoot } from 'src/lib/git-utils'\nimport { getInfraKitConfig, resolveConfiguredIdes } from 'src/lib/infra-kit-config'\nimport { logger } from 'src/lib/logger'\nimport { withEscape } from 'src/lib/prompts/escapable-context'\nimport { pickReleaseBranches } from 'src/lib/prompts/release-picker'\nimport { formatBranchName, isReleaseBranch, parseReleaseRef } from 'src/lib/release-id'\nimport {\n detectReleaseType,\n formatBranchPickerItems,\n getJiraDescriptions,\n releaseBranchLabels,\n} from 'src/lib/release-utils'\nimport type { ReleaseType } from 'src/lib/release-utils'\nimport { defineMcpTool, textContent } from 'src/types'\nimport type { RequiredConfirmedOptionArg } from 'src/types'\n\n// Constants\nconst FEATURE_DIR = 'feature'\nconst RELEASE_DIR = 'release'\n\n// The two optional follow-ups below declare `whenHeadless: { value: false }`, and that value is not\n// a convenience \u2014 it is what `githubDesktop`/`cmux`'s own `.describe()` text already promises:\n// \"interactive prompt (CLI) / false (MCP, no TTY)\". It was documented and never implemented. With\n// neither the flag nor the config key set, an MCP call fell through to a real `@inquirer/confirm`,\n// which writes to `process.stdout` \u2014 the JSON-RPC transport under MCP \u2014 corrupting the stream rather\n// than hanging. This tool is ungated (`requiresHumanConfirm` is unset), so one call carrying\n// `versions` or `all` reached both prompts: the happy path was the defect path.\n//\n// `{ value: false }` rather than `withEscape`'s `'refuse'` default, and the difference is the whole\n// reason the policy is declared per site. Refusing here would be right-outcome-by-accident at best:\n// it keeps the bytes out of the stream, but turns a documented harmless default into a hard failure\n// and makes the tool unusable over MCP unless a caller passes both booleans explicitly.\n//\n// The guard lives in `withEscape` keyed on `isMcpMode()`, never `process.stdin.isTTY` \u2014\n// `commands/mcp/mcp.ts` spawns the server with `stdio: 'inherit'`, so a terminal-launched\n// `infra-kit mcp` has a real TTY stdin and an isTTY-keyed guard would not fire. And never on\n// `confirmedCommand`, which carries the CLI's `--yes` (`program.ts:109`): keying on that would stop\n// `worktrees add --yes` prompting on a terminal, breaking the documented order in the CLI direction\n// in order to fix it in the MCP one.\n\ninterface WorktreeManagementArgs extends RequiredConfirmedOptionArg {\n all?: boolean\n versions?: string\n ide?: IdeMode\n /** @deprecated Alias for `ide`, kept for back-compat. Ignored when `ide` is set. */\n cursor?: IdeMode\n githubDesktop?: boolean\n cmux?: boolean\n}\n\n/**\n * Manage git worktrees for release branches\n * Creates worktrees for active release branches and removes unused ones\n */\nexport const worktreesAdd = async (options: WorktreeManagementArgs) => {\n const { confirmedCommand, all, versions, githubDesktop, cmux } = options\n // `cursor` is the deprecated alias for `ide`; `ide` wins when both are present.\n const ide = options.ide ?? options.cursor\n\n // Branch-agnostic: `git worktree add` addresses branches by name and never\n // reads HEAD, so only the worktree + clean-tree legs apply.\n await assertManagementContext({ operation: 'create worktrees' })\n\n try {\n const currentWorktrees = await getCurrentWorktrees('release')\n const projectRoot = await getProjectRoot()\n\n const worktreeDir = `${projectRoot}${WORKTREES_DIR_SUFFIX}`\n\n await ensureWorktreeDirectory(`${worktreeDir}/${RELEASE_DIR}`)\n await ensureWorktreeDirectory(`${worktreeDir}/${FEATURE_DIR}`)\n\n let selectedReleaseBranches: string[] = []\n\n if (versions) {\n selectedReleaseBranches = versions.split(',').map((v) => {\n return formatBranchName(parseReleaseRef(v.trim()))\n })\n } else {\n const releasePRsInfo = await getReleasePRsWithInfo()\n\n const releasePRsList = releasePRsInfo.map((pr) => {\n return pr.branch\n })\n\n if (releasePRsList.length === 0) {\n logger.info('\u2139\uFE0F No open release branches found')\n\n commandEcho.print()\n\n return {\n content: textContent(JSON.stringify({ createdWorktrees: [], count: 0 }, null, 2)),\n structuredContent: { createdWorktrees: [], count: 0 },\n }\n }\n\n if (all) {\n selectedReleaseBranches = releasePRsList\n } else {\n commandEcho.setInteractive()\n\n const releaseTypes = new Map<string, ReleaseType>(\n releasePRsInfo.map((pr) => {\n return [pr.branch, detectReleaseType(pr.title)]\n }),\n )\n\n const descriptions = await getJiraDescriptions()\n\n selectedReleaseBranches = await pickReleaseBranches(\n formatBranchPickerItems({ branches: releasePRsList, descriptions, types: releaseTypes }),\n { required: true },\n )\n }\n }\n\n // Track --all flag if all branches were selected (either via flag or interactively)\n if (all) {\n commandEcho.addOption('--all', true)\n } else {\n commandEcho.addOption('--versions', releaseBranchLabels(selectedReleaseBranches))\n }\n\n // Ask for confirmation\n await confirmOrExit(confirmedCommand, 'Are you sure you want to proceed with these worktree changes?')\n\n // Track --yes flag if confirmation was interactive (user confirmed)\n if (!confirmedCommand) {\n commandEcho.addOption('--yes', true)\n }\n\n const config = await getInfraKitConfig()\n\n // One attach style: every configured editor gets the worktrees added to its\n // workspace. Per-run skip is `--ide none` / `--no-ide`; no editor configured\n // means there's nothing to open.\n const ideMode: IdeMode = ide ?? (resolveConfiguredIdes(config).length > 0 ? 'workspace' : 'none')\n\n commandEcho.addOption('--ide', ideMode)\n\n const openInGithubDesktop =\n githubDesktop ??\n config.worktrees?.openInGithubDesktop ??\n (await withEscape(\n (context) => {\n return confirm({ message: 'Open created worktrees in GitHub Desktop?' }, context)\n },\n { whenHeadless: { value: false } },\n ))\n\n if (typeof githubDesktop === 'undefined' && config.worktrees?.openInGithubDesktop === undefined) {\n commandEcho.setInteractive()\n }\n\n if (openInGithubDesktop) {\n commandEcho.addOption('--github-desktop', true)\n } else {\n commandEcho.addOption('--no-github-desktop', true)\n }\n\n const openInCmux =\n cmux ??\n config.worktrees?.openInCmux ??\n (await withEscape(\n (context) => {\n return confirm({ message: 'Open created worktrees in cmux?' }, context)\n },\n { whenHeadless: { value: false } },\n ))\n\n if (typeof cmux === 'undefined' && config.worktrees?.openInCmux === undefined) {\n commandEcho.setInteractive()\n }\n\n if (openInCmux) {\n commandEcho.addOption('--cmux', true)\n } else {\n commandEcho.addOption('--no-cmux', true)\n }\n\n const { branchesToCreate } = categorizeWorktrees({\n selectedReleaseBranches,\n currentWorktrees,\n })\n\n const createdWorktrees = await createWorktrees(branchesToCreate, worktreeDir)\n\n logResults(createdWorktrees)\n\n if (ideMode === 'workspace') {\n await addIdeWorktreeFolders({ projectRoot, worktreeDir, branches: createdWorktrees })\n }\n\n if (openInGithubDesktop) {\n for (const branch of createdWorktrees) {\n await $`github ${worktreeDir}/${branch}`\n await $`sleep 5`\n }\n }\n\n if (openInCmux) {\n // Group name keys on the STABLE main-repo basename (not the worktree-local\n // `getRepoName()`), so every worktree of a repo lands in the same sidebar\n // group regardless of which checkout this runs from.\n const repoName = path.basename(await getMainRepoRoot(projectRoot))\n const openByCwd = await listCmuxWorkspacesByCwd()\n\n let groupRef = await findCmuxGroupRefByName(repoName)\n let bootstrapAttempted = false\n\n for (const branch of createdWorktrees) {\n const cwd = `${worktreeDir}/${branch}`\n\n // Skip branches whose cmux workspace is already open (matched by cwd, not\n // title), so re-running worktrees-add never duplicates an existing workspace.\n if (openByCwd.has(await realpathForCmuxCwd(cwd))) {\n continue\n }\n\n const title = buildCmuxWorkspaceTitle({ branch })\n\n try {\n if (groupRef) {\n await openCmuxWorkspaceWithLayout({ cwd, title, group: groupRef })\n } else {\n // No group yet: open ungrouped, then seed the group from this first\n // workspace via `--from` (capture-free). Attempt the seed once, so a\n // failed create doesn't spawn a group per iteration.\n const workspaceRef = await openCmuxWorkspaceWithLayout({ cwd, title })\n\n if (!bootstrapAttempted) {\n groupRef = await createCmuxGroupFrom(repoName, [workspaceRef])\n bootstrapAttempted = true\n }\n }\n } catch (error) {\n logger.warn({ error, branch }, `\u26A0\uFE0F Failed to open cmux workspace for ${branch}`)\n }\n }\n }\n\n commandEcho.print()\n\n const structuredContent = {\n createdWorktrees,\n count: createdWorktrees.length,\n }\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n } catch (error) {\n // A cancelled prompt (Ctrl-C / Esc) is a user back-out, not a failure: let it\n // reach the top-level boundary untouched so it exits cleanly, instead of being\n // logged as an error with a misleading \"branches already exist\" remediation.\n if (isPromptCancellation(error)) throw error\n\n // `debug`, not `error`: this rethrows as an OperationError, and `entry/cli.ts` logs any\n // uncaught error at ERROR and exits 1 \u2014 so logging here too printed one fault as two red\n // lines. Kept (demoted, not deleted) because the wrapped message renders only the operation\n // and remediation; the cause's stack survives here and is reachable with `--debug`.\n logger.debug({ err: error }, 'Error managing worktrees')\n throw new OperationError(error, {\n operation: 'create worktrees',\n remediation: \"verify branches don't already exist as worktrees: 'git worktree list'\",\n })\n }\n}\n\n/**\n * Ensure the worktree directory exists\n */\nconst ensureWorktreeDirectory = async (worktreeDir: string): Promise<void> => {\n await $`mkdir -p ${worktreeDir}`\n}\n\ninterface CategorizeWorktreesArgs {\n selectedReleaseBranches: string[]\n currentWorktrees: string[]\n}\n\n/**\n * Categorize release worktrees into those that need to be created or removed\n */\nconst categorizeWorktrees = (args: CategorizeWorktreesArgs): { branchesToCreate: string[] } => {\n const { selectedReleaseBranches, currentWorktrees } = args\n\n const currentBranchNames = currentWorktrees.filter((branch) => {\n return isReleaseBranch(branch)\n })\n\n const branchesToCreate = selectedReleaseBranches.filter((branch) => {\n return !currentBranchNames.includes(branch)\n })\n\n return { branchesToCreate }\n}\n\n/**\n * Create worktrees for the specified branches\n */\nconst createWorktrees = async (branches: string[], worktreeDir: string): Promise<string[]> => {\n const results = await Promise.allSettled(\n branches.map(async (branch) => {\n const worktreePath = `${worktreeDir}/${branch}`\n\n await $`git worktree add ${worktreePath} ${branch}`\n await $({ cwd: worktreePath })`pnpm install`\n\n return branch\n }),\n )\n\n const created: string[] = []\n\n for (const [index, result] of results.entries()) {\n if (result.status === 'fulfilled') {\n created.push(result.value)\n } else {\n const branch = branches[index]\n const err = new OperationError(result.reason, {\n operation: `git worktree add for ${branch}`,\n remediation: 'check the branch name and that the parent dir is writable',\n })\n\n logger.error({ error: result.reason, msg: err.message })\n }\n }\n\n return created\n}\n\n/**\n * Log the results of worktree management\n */\nconst logResults = (created: string[]): void => {\n if (created.length > 0) {\n logger.info('\u2705 Created git worktrees:')\n for (const branch of created) {\n logger.info(branch)\n }\n logger.info('')\n } else {\n logger.info('\u2139\uFE0F No new git worktrees to create')\n }\n}\n\n// MCP Tool Registration\nexport const worktreesAddMcpTool = defineMcpTool({\n name: 'worktrees-add',\n description:\n 'Create local git worktrees for release branches under the worktrees directory and run \"pnpm install\" in each. Mutates the local filesystem. When invoked via MCP, pass either \"versions\" (comma-separated) or all=true \u2014 the branch picker and \"open in Cursor / GitHub Desktop / cmux\" follow-up prompts are unreachable without a TTY, and the CLI confirmation is auto-skipped for MCP calls.',\n inputSchema: {\n all: z\n .boolean()\n .optional()\n .describe(\n 'Add worktrees for every open release branch. Either \"all\" or \"versions\" must be provided for MCP calls (the interactive picker is unavailable without a TTY). Ignored if \"versions\" is provided.',\n ),\n versions: z\n .string()\n .optional()\n .describe(\n 'Comma-separated release versions or names to target (e.g. \"1.2.5, 1.2.6\" or \"checkout-redesign, 1.2.5\"). Either \"versions\" or all=true must be provided for MCP calls. Overrides \"all\" when set.',\n ),\n ide: z\n .enum(IDE_MODES)\n .optional()\n .describe(\n 'Editor open mode for created worktrees, applied to all configured editors (Cursor and/or Zed, per the \"ide\" config). \"workspace\" (the only attach style) adds each worktree to every configured editor workspace and opens it. \"none\" skips the editor. Resolution order: this flag \u2192 \"workspace\" when at least one \"ide\" is configured \u2192 \"none\" otherwise.',\n ),\n cursor: z\n .enum(IDE_MODES)\n .optional()\n .describe('Deprecated alias for \"ide\". Prefer \"ide\". Ignored when \"ide\" is provided.'),\n githubDesktop: z\n .boolean()\n .optional()\n .describe(\n 'Open each created worktree in GitHub Desktop. Resolution order: this flag \u2192 \"worktrees.openInGithubDesktop\" from infra-kit config \u2192 interactive prompt (CLI) / false (MCP, no TTY).',\n ),\n cmux: z\n .boolean()\n .optional()\n .describe(\n 'Open each created worktree in a new cmux workspace, all rooted at the worktree directory. Pane layout follows \"worktrees.cmux.layout\" (default \"two-columns\": left | right; or \"three-pane\": left split top/bottom + full-height right). Resolution order: this flag \u2192 \"worktrees.openInCmux\" from infra-kit config \u2192 interactive prompt (CLI) / false (MCP, no TTY).',\n ),\n },\n outputSchema: {\n createdWorktrees: z.array(z.string()).describe('List of created git worktree branches'),\n count: z.number().describe('Number of git worktrees created'),\n },\n handler: worktreesAdd,\n})\n", "import { z } from 'zod'\n\nimport { getReleasePRsWithInfo } from 'src/integrations/gh'\nimport { getCurrentWorktrees } from 'src/lib/git-utils'\nimport { getInfraKitConfig } from 'src/lib/infra-kit-config'\nimport { logger } from 'src/lib/logger'\nimport { displayLabel, formatJiraName, parseBranchName } from 'src/lib/release-id'\nimport { detectReleaseType, formatVersionLabel, getJiraDescriptions } from 'src/lib/release-utils'\nimport type { ReleaseType } from 'src/lib/release-utils'\nimport { defineMcpTool, textContent } from 'src/types'\n\ninterface WorktreeInfo {\n version: string\n type: ReleaseType\n description: string | null\n}\n\n/**\n * List all release git worktrees with version, type, and Jira description\n */\nexport const worktreesList = async () => {\n // GUARD (placement is load-bearing): `git worktree list` answers in ANY repo, so without this the\n // release-branch filter renders a generic-git empty set as the infra-kit fact \"No active worktrees\n // found\" \u2014 a false statement in a stranger's repo. Must stay ABOVE `getCurrentWorktrees`.\n await getInfraKitConfig()\n\n const currentWorktrees = await getCurrentWorktrees('release')\n\n if (currentWorktrees.length === 0) {\n logger.info('\u2139\uFE0F No active worktrees found')\n\n return {\n content: textContent(JSON.stringify({ worktrees: [], count: 0 }, null, 2)),\n structuredContent: { worktrees: [], count: 0 },\n }\n }\n\n const [releasePRsInfo, jiraDescriptions] = await Promise.all([getReleasePRsWithInfo(), getJiraDescriptions()])\n\n const releaseTypes = new Map<string, ReleaseType>(\n releasePRsInfo.map((pr) => {\n return [pr.branch, detectReleaseType(pr.title)]\n }),\n )\n\n // Skip worktrees whose branch does not parse as a release id (lenient source).\n const worktrees: WorktreeInfo[] = currentWorktrees.flatMap((branch) => {\n const id = parseBranchName(branch)\n\n if (!id) return []\n\n // Human label `1.2.3` | `<name>`; Jira-descriptions map is keyed by the\n // Jira version NAME (`v1.2.3` | `<name>`) \u2014 same split as formatBranchChoices.\n const version = displayLabel(id)\n const type = releaseTypes.get(branch) || 'regular'\n const description = jiraDescriptions.get(formatJiraName(id)) || null\n\n return [{ version, type, description }]\n })\n\n // Log formatted output\n const maxVersionLength = Math.max(\n ...worktrees.map((w) => {\n return w.version.length\n }),\n )\n\n const formattedLines = worktrees.map((worktree) => {\n const label = formatVersionLabel(worktree.version, worktree.type, maxVersionLength)\n\n if (worktree.description) {\n return `${label} ${worktree.description}`\n }\n\n return label\n })\n\n // One record, no trailing newline. Two calls made pino-pretty emit a bare `INFO:` line for the\n // leading `\\n`, and the trailing one stacked a second blank line under the list \u2014 which, in the\n // session shell, collided with the blank line the transcript footer already writes above itself.\n logger.info(`\uD83C\uDF3F Active worktrees:\\n\\n${formattedLines.join('\\n')}`)\n\n const structuredContent = {\n worktrees,\n count: worktrees.length,\n }\n\n return {\n content: textContent(JSON.stringify(structuredContent, null, 2)),\n structuredContent,\n }\n}\n\n// MCP Tool Registration\nexport const worktreesListMcpTool = defineMcpTool({\n name: 'worktrees-list',\n description:\n 'List existing release-branch worktrees with version, release type (regular / hotfix), and Jira fix-version description. Read-only.',\n inputSchema: {},\n outputSchema: {\n worktrees: z\n .array(\n z.object({\n version: z.string().describe('Release version'),\n type: z.enum(['regular', 'hotfix']).describe('Release type'),\n description: z.string().nullable().describe('Jira version description'),\n }),\n )\n .describe('List of all worktrees with details'),\n count: z.number().describe('Number of worktrees'),\n },\n handler: worktreesList,\n})\n", "import { z } from 'zod'\n\nimport { getReleasePRsWithInfo } from 'src/integrations/gh'\nimport { removeIdeWorktreeFolders } from 'src/integrations/ide'\nimport { commandEcho, confirmOrExit } from 'src/lib/command-echo'\nimport { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'\nimport { isPromptCancellation } from 'src/lib/errors/is-prompt-cancellation'\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { assertManagementContext } from 'src/lib/git-guard'\nimport { getCurrentWorktrees, getProjectRoot } from 'src/lib/git-utils'\nimport { getInfraKitConfig } from 'src/lib/infra-kit-config'\nimport { logger } from 'src/lib/logger'\nimport { isMcpMode } from 'src/lib/mcp-mode'\nimport { pickReleaseBranches } from 'src/lib/prompts/release-picker'\nimport { formatBranchName, parseReleaseRef } from 'src/lib/release-id'\nimport {\n detectReleaseType,\n formatBranchPickerItems,\n getJiraDescriptions,\n releaseBranchLabels,\n} from 'src/lib/release-utils'\nimport type { ReleaseType } from 'src/lib/release-utils'\nimport { logRemovalResults, removeWorktrees, toRemovalToolResult } from 'src/lib/worktrees'\nimport { defineMcpTool, textContent } from 'src/types'\nimport type { RequiredConfirmedOptionArg } from 'src/types'\n\n// Constants\ninterface WorktreeManagementArgs extends RequiredConfirmedOptionArg {\n all?: boolean\n versions?: string\n}\n\n/**\n * MCP has no TTY (JSON-RPC owns stdin) and the boundary auto-confirms, so the branch picker is\n * unreachable and a bulk removal would run without any human gate. `all` is intentionally absent from\n * this tool's MCP inputSchema; guard the shared handler too so a direct/agent call can never fan out to\n * every worktree. Require MCP callers to name targets explicitly via `versions`. No-op on the CLI path.\n */\nconst assertMcpRemovalInput = (input: { all?: boolean; versions?: string }): void => {\n if (!isMcpMode()) return\n\n if (input.all) {\n throw new OperationError(undefined, {\n operation: 'remove worktrees',\n remediation: 'name targets explicitly via \"versions\"; bulk all=true removal is disabled over MCP',\n stderrExcerpt: 'all=true is not permitted for worktrees-remove over MCP',\n })\n }\n\n if (!input.versions) {\n throw new OperationError(undefined, {\n operation: 'remove worktrees',\n remediation: 'pass \"versions\" (comma-separated release versions/names); the interactive picker needs a TTY',\n stderrExcerpt: 'worktrees-remove over MCP requires \"versions\"',\n })\n }\n}\n\n/**\n * Every named target must be an active release worktree. Without this an unmatched version builds a\n * path that does not exist; `removeWorktrees` (Promise.allSettled) swallows the failure and reports a\n * no-op as success. Validate all-or-nothing BEFORE removing anything so a typo or a feature-worktree\n * name fails loudly on both the CLI `--versions` and the MCP path. The `all` and interactive-picker\n * paths select from `currentWorktrees`, so this is a no-op for them.\n */\nconst assertTargetsExist = (selected: string[], currentWorktrees: string[]): void => {\n const unmatched = selected.filter((branch) => {\n return !currentWorktrees.includes(branch)\n })\n\n if (unmatched.length > 0) {\n throw new OperationError(undefined, {\n operation: 'remove worktrees',\n remediation: `no active worktree for: ${unmatched.join(', ')}. Run 'infra-kit worktrees list' to see current worktrees`,\n stderrExcerpt: `unmatched worktree target(s): ${unmatched.join(', ')}`,\n })\n }\n}\n\n/**\n * Manage git worktrees for release branches\n * Creates worktrees for active release branches and removes unused ones\n */\nexport const worktreesRemove = async (options: WorktreeManagementArgs) => {\n const { confirmedCommand, all, versions } = options\n\n // Branch-agnostic: `git worktree remove` addresses worktrees by path and never\n // reads HEAD, so only the worktree + clean-tree legs apply.\n await assertManagementContext({ operation: 'remove worktrees' })\n\n // GUARD (placement is load-bearing): must stay ABOVE the `try` block below \u2014 its catch rewraps, and\n // getInfraKitConfig's missing-config throw is a PLAIN Error whose text buildMessage would drop.\n // Must stay BELOW assertManagementContext so a linked-worktree caller still gets the worktree advice.\n // Kept ABOVE assertMcpRemovalInput: \"not an infra-kit project\" is the more fundamental failure.\n await getInfraKitConfig()\n\n assertMcpRemovalInput({ all, versions })\n\n try {\n const currentWorktrees = await getCurrentWorktrees('release')\n\n if (currentWorktrees.length === 0) {\n logger.info('\u2139\uFE0F No active worktrees to remove')\n\n commandEcho.print()\n\n const empty = { removedWorktrees: [], failedWorktrees: [], count: 0 }\n\n return {\n content: textContent(JSON.stringify(empty, null, 2)),\n structuredContent: empty,\n }\n }\n\n const projectRoot = await getProjectRoot()\n\n const worktreeDir = `${projectRoot}${WORKTREES_DIR_SUFFIX}`\n\n let selectedReleaseBranches: string[] = []\n\n if (all) {\n selectedReleaseBranches = currentWorktrees\n } else if (versions) {\n selectedReleaseBranches = versions.split(',').map((v) => {\n return formatBranchName(parseReleaseRef(v.trim()))\n })\n } else {\n commandEcho.setInteractive()\n\n const [descriptions, prInfo] = await Promise.all([getJiraDescriptions(), getReleasePRsWithInfo()])\n\n const releaseTypes = new Map<string, ReleaseType>(\n prInfo.map((pr) => {\n return [pr.branch, detectReleaseType(pr.title)]\n }),\n )\n\n selectedReleaseBranches = await pickReleaseBranches(\n formatBranchPickerItems({ branches: currentWorktrees, descriptions, types: releaseTypes }),\n { required: true },\n )\n }\n\n assertTargetsExist(selectedReleaseBranches, currentWorktrees)\n\n // Track --all flag if all branches were selected (either via flag or interactively)\n const allSelected = selectedReleaseBranches.length === currentWorktrees.length\n\n if (allSelected) {\n commandEcho.addOption('--all', true)\n } else {\n commandEcho.addOption('--versions', releaseBranchLabels(selectedReleaseBranches))\n }\n\n // Ask for confirmation\n await confirmOrExit(confirmedCommand, 'Are you sure you want to proceed with these worktree changes?')\n\n // Track --yes flag if confirmation was interactive (user confirmed)\n if (!confirmedCommand) {\n commandEcho.addOption('--yes', true)\n }\n\n const removal = await removeWorktrees({\n branches: selectedReleaseBranches,\n worktreeDir,\n projectRoot,\n pruneFolder: allSelected,\n })\n\n // `!confirmedCommand` is the interactive path (a human used the picker/confirm). Only there\n // do we let Zed's destructive `--reuse` relaunch fire; MCP/--yes runs (confirmedCommand=true)\n // and worktrees-sync never relaunch an editor window.\n await removeIdeWorktreeFolders({\n projectRoot,\n worktreeDir,\n currentWorktrees,\n removedWorktrees: removal.removed,\n allowEditorRelaunch: !confirmedCommand,\n })\n\n logRemovalResults(removal)\n\n commandEcho.print()\n\n // Ordering is load-bearing: the echo line and the IDE cleanup for the branches that DID succeed\n // run first; only then does a failed branch throw (CLI) or become an isError result (MCP).\n return toRemovalToolResult({ result: removal, operation: 'remove worktrees' })\n } catch (error) {\n // A cancelled prompt (Ctrl-C / Esc) is a user back-out, not a failure: let it\n // reach the top-level boundary untouched so it exits cleanly, instead of being\n // logged as an error with a misleading remediation.\n if (isPromptCancellation(error)) throw error\n\n // Our own guard failures (unmatched target, MCP misuse) already carry an actionable remediation;\n // surface them as-is instead of re-wrapping them with the generic message below.\n if (error instanceof OperationError) throw error\n\n logger.error({ error }, '\u274C Error managing worktrees')\n throw new OperationError(error, {\n operation: 'remove worktrees',\n remediation: \"check 'git worktree list' for the path; uncommitted changes block removal\",\n })\n }\n}\n\n// MCP Tool Registration\nexport const worktreesRemoveMcpTool = defineMcpTool({\n name: 'worktrees-remove',\n description:\n 'Remove local git worktrees for the named release branches. Over MCP you MUST pass \"versions\" (comma-separated); bulk all=true removal is disabled here (it is a one-shot, unconfirmed wipe of every worktree) \u2014 the branch picker and confirmation are unavailable without a TTY. Every named version must be an active worktree, or the call errors without removing anything. What survives: the release branches/commits themselves are never deleted, and the worktrees directory plus its release/feature subfolders are left in place (recreate a worktree with worktrees-add). What is lost: git refuses to remove a worktree with modified tracked files or untracked files, BUT it DOES delete the worktree directory including gitignored contents \u2014 a hydrated .env of Doppler secrets (re-fetch with env-load) and build output such as node_modules/dist (needs reinstall/rebuild). When every worktree is removed it also runs \"git worktree prune\". A branch git refuses to remove is listed in failedWorktrees and the result carries isError; a leftover that git already unregistered and that holds only tool state (.omc/state, .omc/sessions, .DS_Store) is swept automatically.',\n requiresHumanConfirm: true,\n inputSchema: {\n versions: z\n .string()\n .describe(\n 'Comma-separated release versions or names to remove (e.g. \"1.2.5, 1.2.6\" or \"checkout-redesign, 1.2.5\"). Required: each must name an active worktree, or the call errors before removing anything.',\n ),\n confirm: z\n .boolean()\n .optional()\n .describe('Set true to execute; omit for a dry-run gate that echoes the resolved action.'),\n },\n outputSchema: {\n removedWorktrees: z.array(z.string()).describe('List of removed git worktree branches'),\n failedWorktrees: z\n .array(z.string())\n .describe('Branches whose worktree could NOT be removed (git refused, or an unsweepable leftover remained)'),\n count: z.number().describe('Number of git worktrees removed'),\n },\n handler: worktreesRemove,\n})\n", "import { z } from 'zod'\n\nimport { getReleasePRs } from 'src/integrations/gh'\nimport { removeIdeWorktreeFolders } from 'src/integrations/ide'\nimport { commandEcho, confirmOrExit } from 'src/lib/command-echo'\nimport { WORKTREES_DIR_SUFFIX } from 'src/lib/constants'\nimport { isPromptCancellation } from 'src/lib/errors/is-prompt-cancellation'\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { assertManagementContext } from 'src/lib/git-guard'\nimport { getCurrentWorktrees, getProjectRoot } from 'src/lib/git-utils'\nimport { getInfraKitConfig } from 'src/lib/infra-kit-config'\nimport { logger } from 'src/lib/logger'\nimport { isReleaseBranch } from 'src/lib/release-id'\nimport { logRemovalResults, removeWorktrees, toRemovalToolResult } from 'src/lib/worktrees'\nimport { defineMcpTool } from 'src/types'\nimport type { RequiredConfirmedOptionArg } from 'src/types'\n\ninterface WorktreeSyncArgs extends RequiredConfirmedOptionArg {}\n\n/**\n * Manage git worktrees for release branches.\n *\n * Creates worktrees for active release branches and removes unused ones\n */\nexport const worktreesSync = async (options: WorktreeSyncArgs) => {\n const { confirmedCommand } = options\n\n // Branch-agnostic: reconciles worktrees by name/path and never reads HEAD, so\n // only the worktree + clean-tree legs apply. Load-bearing on the MCP path,\n // which runs this unattended \u2014 a branch requirement there would refuse a\n // cleanup that has no quarrel with the caller's checkout.\n await assertManagementContext({ operation: 'sync worktrees' })\n\n // GUARD (placement is load-bearing): must stay ABOVE the `try` block below \u2014 its catch rewraps, and\n // getInfraKitConfig's missing-config throw is a PLAIN Error whose text buildMessage would drop.\n // Must stay BELOW assertManagementContext so a linked-worktree caller still gets the worktree advice.\n await getInfraKitConfig()\n\n try {\n const currentWorktrees = await getCurrentWorktrees('release')\n const projectRoot = await getProjectRoot()\n\n const worktreeDir = `${projectRoot}${WORKTREES_DIR_SUFFIX}`\n\n const releasePRsList = await getReleasePRs()\n\n // Ask for confirmation\n await confirmOrExit(confirmedCommand, 'Are you sure you want to proceed with these worktree changes?')\n\n // Track --yes flag if confirmation was interactive (user confirmed)\n if (!confirmedCommand) {\n commandEcho.addOption('--yes', true)\n }\n\n const { branchesToRemove } = categorizeWorktrees({\n releasePRsList,\n currentWorktrees,\n })\n\n // Shared with worktrees-remove: same verify-then-sweep recovery for a leftover a post-exit hook\n // re-created, same {removed, failed} reporting. Branches are removed concurrently, exactly as\n // `worktrees remove --all` does.\n const removal = await removeWorktrees({\n branches: branchesToRemove,\n worktreeDir,\n projectRoot,\n })\n\n // Hard `false`: sync is background/stale-cleanup (predominantly the MCP path) \u2014 it must never\n // relaunch and overwrite a focused Zed window. Zed removal stays a no-op here.\n await removeIdeWorktreeFolders({\n projectRoot,\n worktreeDir,\n currentWorktrees,\n removedWorktrees: removal.removed,\n allowEditorRelaunch: false,\n })\n\n logRemovalResults(removal)\n\n commandEcho.print()\n\n // Ordering is load-bearing: IDE cleanup and the echo line run first; only then does a failed\n // branch throw (CLI) or become an isError result (MCP).\n return toRemovalToolResult({ result: removal, operation: 'sync worktrees' })\n } catch (error) {\n // A cancelled prompt (Ctrl-C / Esc) is a user back-out, not a failure: let it\n // reach the top-level boundary untouched so it exits cleanly, instead of being\n // logged as an error with a misleading remediation.\n if (isPromptCancellation(error)) throw error\n\n // A failed-removal report already names the branch and the fix; re-wrapping it with the\n // remote-connectivity remediation below would point the user at the wrong problem.\n if (error instanceof OperationError) throw error\n\n logger.error({ error }, '\u274C Error managing worktrees')\n throw new OperationError(error, {\n operation: 'sync worktrees with remote',\n remediation: \"ensure 'gh auth status' is ok and you can reach origin\",\n })\n }\n}\n\ninterface CategorizeWorktreesArgs {\n releasePRsList: string[]\n currentWorktrees: string[]\n}\n\n/**\n * Categorize worktrees into those that need to be created or removed\n */\nconst categorizeWorktrees = (args: CategorizeWorktreesArgs): { branchesToRemove: string[] } => {\n const { releasePRsList, currentWorktrees } = args\n\n const currentBranchNames = currentWorktrees.filter((branch) => {\n return isReleaseBranch(branch)\n })\n\n const branchesToRemove = currentBranchNames.filter((branch) => {\n return !releasePRsList.includes(branch)\n })\n\n return { branchesToRemove }\n}\n\n// MCP Tool Registration\nexport const worktreesSyncMcpTool = defineMcpTool({\n name: 'worktrees-sync',\n description:\n 'Remove worktrees whose release PR is no longer open (stale cleanup). Only removes \u2014 never creates; use worktrees-add to create worktrees for new releases. The CLI confirmation is auto-skipped for MCP calls, so the caller is responsible for gating. A branch git refuses to remove is listed in failedWorktrees and the result carries isError; a leftover that git already unregistered and that holds only tool state (.omc/state, .omc/sessions, .DS_Store) is swept automatically.',\n inputSchema: {},\n outputSchema: {\n removedWorktrees: z.array(z.string()).describe('List of removed worktree branches'),\n failedWorktrees: z\n .array(z.string())\n .describe('Branches whose worktree could NOT be removed (git refused, or an unsweepable leftover remained)'),\n count: z.number().describe('Number of worktrees removed during sync'),\n },\n handler: worktreesSync,\n})\n", "import type { z } from 'zod'\n\nimport { auditMcpTool } from 'src/commands/audit'\nimport { configGetMcpTool } from 'src/commands/config-get'\nimport { devStatusMcpTool } from 'src/commands/dev-status'\nimport { doctorMcpTool } from 'src/commands/doctor'\nimport { envClearMcpTool } from 'src/commands/env-clear'\nimport { envListMcpTool } from 'src/commands/env-list'\nimport { envLoadMcpTool } from 'src/commands/env-load'\nimport { envStatusMcpTool } from 'src/commands/env-status'\nimport { envTokenListMcpTool } from 'src/commands/env-token-list'\nimport { ghMergeDevMcpTool } from 'src/commands/gh-merge-dev'\nimport { ghReleaseDeliverMcpTool } from 'src/commands/gh-release-deliver'\nimport { ghReleaseDeployAllMcpTool } from 'src/commands/gh-release-deploy-all'\nimport { ghReleaseDeploySelectedMcpTool } from 'src/commands/gh-release-deploy-selected'\nimport { ghReleaseListMcpTool } from 'src/commands/gh-release-list'\nimport { localDeployAllMcpTool, localDeploySelectedMcpTool } from 'src/commands/local-deploy'\nimport { releaseCreateMcpTool } from 'src/commands/release-create'\nimport { releaseDescEditMcpTool } from 'src/commands/release-desc-edit'\nimport { releaseRemoveMcpTool } from 'src/commands/release-remove'\nimport { reopenMcpTool } from 'src/commands/reopen'\nimport { setupMcpTool } from 'src/commands/setup'\nimport { vendorCheckMcpTool } from 'src/commands/vendor-check'\nimport { versionMcpTool } from 'src/commands/version'\nimport { worktreesAddMcpTool } from 'src/commands/worktrees-add'\nimport { worktreesListMcpTool } from 'src/commands/worktrees-list'\nimport { worktreesRemoveMcpTool } from 'src/commands/worktrees-remove'\nimport { worktreesSyncMcpTool } from 'src/commands/worktrees-sync'\nimport type { ArgumentFormProvider, ToolsExecutionResult } from 'src/types'\n\n/**\n * Registration-facing shape of an MCP tool. The concrete `*McpTool` definitions\n * are generic over their Zod input/output shapes (and invariant), so they cannot\n * share one precise `McpTool<...>` element type in an array. This widened, non-\n * generic view exposes exactly what registration needs and every concrete tool\n * assigns to it. Matches the loose handler typing already used in tool-handler.\n */\nexport interface CatalogMcpTool {\n name: string\n description: string\n inputSchema: z.ZodRawShape\n outputSchema: z.ZodRawShape\n /**\n * Marks a destructive tool for the orthogonal MCP confirm gate in `lib/tool-handler`. Widened,\n * non-generic mirror of {@link McpTool.requiresHumanConfirm} so registration (`mcp/tools/index.ts`)\n * can forward it to `createToolHandler` without reaching into the concrete generic tool type.\n */\n requiresHumanConfirm?: boolean\n /**\n * Optional per-tool argument-form seam consumed by `lib/tool-handler`. Widened, non-generic mirror\n * of {@link McpTool.formProvider} for the same reason `requiresHumanConfirm` is mirrored above:\n * registration (`mcp/tools/index.ts`) forwards it to `createToolHandler` without reaching into the\n * concrete generic tool type.\n *\n * {@link ArgumentFormProvider} is itself non-generic and lives in `src/types`, which this module\n * already imports from \u2014 so mirroring it costs no new dependency and, in particular, no MCP SDK\n * import.\n */\n formProvider?: ArgumentFormProvider\n /**\n * Host-facing `_meta` for the tool's `tools/list` entry. Widened, non-generic mirror of\n * {@link McpTool.meta}, mirrored here for the same reason `requiresHumanConfirm` is: registration\n * forwards it without reaching into the concrete generic tool type.\n */\n meta?: Record<string, unknown>\n // Heterogeneous tool params; loose `any` mirrors the existing tool-handler typing.\n handler: (params: any) => Promise<ToolsExecutionResult>\n}\n\n/**\n * Structural mirror of the SDK's `ToolAnnotations`. Deliberately NOT imported from\n * `@modelcontextprotocol/server`: this module is on every CLI command path, and the `u6`/`u7` bundle\n * guards depend on the catalog staying free of MCP SDK imports.\n *\n * These are ADVISORY hints \u2014 the MCP spec states a client must not make security decisions from them.\n * The authority for destructive operations remains `requiresHumanConfirm` plus the confirm gate in\n * `lib/tool-handler`; nothing in that gate reads these fields.\n *\n * `idempotentHint` is deliberately absent: with no tool claiming `true`, an explicit `false` is\n * indistinguishable from omission under both host-reading conventions, and the hint licenses retry \u2014\n * the one place where over-claiming is dangerous rather than merely noisy.\n */\nexport interface McpToolAnnotations {\n readOnlyHint: boolean\n /** Present iff `readOnlyHint` is false \u2014 the spec makes it meaningless on a read-only tool. */\n destructiveHint?: boolean\n openWorldHint: boolean\n}\n\n/** A catalog tool plus the registration-facing metadata {@link getExposedMcpTools} derives for it. */\nexport interface RegistrableMcpTool extends CatalogMcpTool {\n title: string\n annotations: McpToolAnnotations\n}\n\n/**\n * Single source of truth for the CLI command surface. It consolidates what used\n * to live in three hand-maintained places (the MCP `tools[]` array and the\n * three no-arg-menu name arrays) into one list, so they can no longer drift.\n *\n * It does NOT replace Commander's `.command().option()` wiring in entry/cli.ts \u2014\n * that stays the source of truth for argument parsing. This catalog only carries\n * cross-surface metadata: the canonical names, which menu group a command shows\n * in, and whether the command is exposed as an MCP tool.\n */\n\n/**\n * The menu groups, in display order \u2014 the SINGLE source of both membership and presentation. `cli.ts`\n * renders the palette by mapping over this list, so adding, renaming, or reordering a group is a\n * one-line edit here and nowhere else. (The group list used to be restated three times in `cli.ts`, and\n * the labels a fourth time in the Inquirer separators.)\n *\n * One group per CLI noun, ordered by how often it is reached for. `Develop` leads because `dev` is the\n * daily driver. `Environment` means the Doppler env commands and nothing else \u2014 it used to be a\n * 13-entry drawer holding config, vendor, and setup commands too, which made its name a lie.\n */\nexport const MENU_GROUPS = [\n { key: 'develop', label: 'Develop' },\n { key: 'release', label: 'Release Management' },\n { key: 'worktrees', label: 'Worktrees' },\n { key: 'environment', label: 'Environment' },\n { key: 'configuration', label: 'Configuration' },\n { key: 'vendor', label: 'Vendor' },\n { key: 'setup', label: 'Setup & Diagnostics' },\n] as const\n\n/** Top-level menu group for the no-arg interactive picker (null = not shown). */\nexport type MenuGroup = (typeof MENU_GROUPS)[number]['key']\n\nexport interface CommandCatalogEntry {\n /**\n * Stable flat id for the command (`release-create`), and the name its MCP tool registers under \u2014 MCP\n * tool names cannot contain a space, so the grouped path cannot serve as one.\n *\n * NOT a CLI surface: the CLI registers only the grouped form ({@link groupPath}). Flat Commander\n * commands were removed once every menu surface learned to address a command by its group path.\n */\n cliName: string\n /** Menu group, or null for subcommands not shown at the top level. */\n menuGroup: MenuGroup | null\n /** The co-located MCP tool, or null for CLI-only commands (init/config/vendor group). */\n mcpTool: CatalogMcpTool | null\n /**\n * Whether the command is registered as an MCP tool. Explicit allowlist:\n * `doctor` is deliberately UNEXPOSED (host-inspecting), so it must never become\n * agent-callable by accident.\n */\n mcpExposed: boolean\n /**\n * Whether running this command writes git/remote/consumer-repo/Doppler-env/fs-outside-cache state.\n * Drives the MCP destructive-op gate (Phase 4).\n */\n mutating: boolean\n /**\n * Canonical Commander argv for this command (grouped form, e.g. `['vendor','check']`) \u2014 the ONLY form\n * the CLI registers. Every menu surface renders `groupPath.join(' ')` and spawns/parses\n * `infra-kit <...groupPath>`, so what the palette shows is exactly what it runs and what the user can\n * retype.\n */\n groupPath: string[]\n /**\n * The command hands the terminal to a full-screen child (an `$EDITOR`) that enters the alternate\n * screen and restores it itself. The session never enters it for them \u2014 but a child killed before it\n * restores the primary buffer would leave every later frame drawn into an abandoned buffer, so the\n * session's post-child hygiene reset leaves the alternate screen defensively. Only `config-edit`.\n */\n entersAltScreen?: boolean\n /**\n * Running this from inside the session cannot mutate the parent shell's env (a child can't). The\n * transcript entry appends a notice that the change applies only after the session exits. `env-load`\n * / `env-clear`.\n */\n sessionEnvNotice?: boolean\n /**\n * A foreground process that runs until the user stops it, rather than doing a unit of work and\n * exiting. Only `dev`. It breaks BOTH of the session shell's assumptions about a child, so the two\n * consequences are deliberately carried by ONE flag \u2014 they must never be set apart:\n *\n * 1. **It produces no verdict.** Its Commander action RESOLVES seconds after boot (the process stays\n * alive on fastify/chokidar handles, not on a pending promise), so `postAction` would write a\n * session report while the server is merely *starting*. Report-presence means \"the command produced\n * a verdict\" (see `session/report.ts`), so the report is suppressed for it entirely.\n * 2. **It exits `128 + signo`, not 0.** It installs its own SIGINT handler and exits 130 on a Ctrl-C\n * \u2014 deliberately, so a supervisor sees an honest signal death. Without this flag the session would\n * read that as a failure and stamp every normal dev exit `\u2717 failed`.\n */\n longRunning?: boolean\n}\n\n/**\n * Authored in no-arg-menu display order so the interactive picker derives\n * directly from this list (see entry/cli.ts). MCP registration filters this\n * list by `mcpExposed`; MCP tool order is not contractual (clients address\n * tools by name), so the registration order need not match the array order.\n */\nexport const commandCatalog: CommandCatalogEntry[] = [\n // --- Develop (menu group) ---\n // The local dev server, and the reason most people open this menu at all \u2014 so it leads.\n //\n // It is a long-running foreground child, which the one-shot Inquirer menu could not host: that menu\n // ran the pick IN-PROCESS, so a dev server would have blocked the menu process forever. The session\n // shell spawns every pick as a separate child on the primary screen and swallows SIGINT while one\n // runs, so Ctrl-C stops only the dev server and the palette comes back. See `longRunning` for the two\n // session assumptions it still breaks (no verdict; exits 128+signo).\n //\n // Spawning the BARE `['dev']` path is deliberate: `shouldRunWizard` fires on exactly bare + TTY +\n // non-json, so a menu pick lands in the interactive wizard \u2014 the wizard IS the flag picker.\n //\n // Not an MCP tool: it never returns a result, so it cannot fit the request/response tool contract.\n {\n cliName: 'dev',\n menuGroup: 'develop',\n mcpTool: null,\n mcpExposed: false,\n mutating: true,\n groupPath: ['dev'],\n longRunning: true,\n },\n // Read-only companion to `dev`: reports what `infra-kit dev` currently has running by reading the\n // on-disk dev-context fragments (never starts a server). Top-level (`infra-kit dev-status`) per the\n // house rule \u2014 new commands go top-level with related names, not nested under a group \u2014 but carries\n // menuGroup 'develop' so it renders beside `dev` in the palette (same shape as reopen/env-status).\n {\n cliName: 'dev-status',\n menuGroup: 'develop',\n mcpTool: devStatusMcpTool,\n mcpExposed: true,\n mutating: false,\n groupPath: ['dev-status'],\n },\n\n // --- Release Management (menu group) ---\n {\n cliName: 'merge-dev',\n menuGroup: 'release',\n mcpTool: ghMergeDevMcpTool,\n mcpExposed: true,\n mutating: true,\n groupPath: ['release', 'merge-dev'],\n },\n {\n cliName: 'release-list',\n menuGroup: 'release',\n mcpTool: ghReleaseListMcpTool,\n mcpExposed: true,\n mutating: false,\n groupPath: ['release', 'list'],\n },\n {\n cliName: 'release-create',\n menuGroup: 'release',\n mcpTool: releaseCreateMcpTool,\n mcpExposed: true,\n mutating: true,\n groupPath: ['release', 'create'],\n },\n {\n cliName: 'release-desc-edit',\n menuGroup: 'release',\n mcpTool: releaseDescEditMcpTool,\n mcpExposed: true,\n mutating: true,\n groupPath: ['release', 'desc-edit'],\n },\n {\n cliName: 'release-deploy-all',\n menuGroup: 'release',\n mcpTool: ghReleaseDeployAllMcpTool,\n mcpExposed: true,\n mutating: true,\n groupPath: ['release', 'deploy-all'],\n },\n {\n cliName: 'release-deploy-selected',\n menuGroup: 'release',\n mcpTool: ghReleaseDeploySelectedMcpTool,\n mcpExposed: true,\n mutating: true,\n groupPath: ['release', 'deploy-selected'],\n },\n // release-deliver does prod delivery + admin-merge \u2014 genuinely irreversible,\n // so it is CLI-only by design.\n {\n cliName: 'release-deliver',\n menuGroup: 'release',\n mcpTool: ghReleaseDeliverMcpTool,\n mcpExposed: false,\n mutating: true,\n groupPath: ['release', 'deliver'],\n },\n // Exposed, unlike its neighbour `release-deliver`. Exposure here is governed by the allowlist-or-gate\n // rule, not by whether the verb sounds destructive: `requiresHumanConfirm` puts it behind the\n // two-phase gate, and the tool narrows itself under MCP \u2014 `version` is required, `moveIssuesTo` /\n // `skipJira` are refused, and the one irreversible step (the Jira fix version) is never attempted,\n // returning `jira: 'manual'` instead. The asymmetry that would otherwise exist is the argument for\n // exposing it: `release-create` IS exposed, so an agent can create a release it could never clean up.\n {\n cliName: 'release-remove',\n menuGroup: 'release',\n mcpTool: releaseRemoveMcpTool,\n mcpExposed: true,\n mutating: true,\n groupPath: ['release', 'remove'],\n },\n // The CLI commands behind these two are DEPRECATED aliases of `release deploy-* --from local`,\n // so `menuGroup: null` keeps them out of the palette while they still resolve for anyone who types\n // them. The MCP tools are deliberately NOT folded into the release pair: the MCP boundary\n // auto-confirms every call (`tool-handler`), and `local-deploy-selected` requires `service` with\n // `.min(1)` \u2014 a merged tool would have to relax that, and an agent omitting it would deploy\n // everything. Agent-reachable while human-unbrowsable is the intended end state here.\n {\n cliName: 'local-deploy-all',\n menuGroup: null,\n mcpTool: localDeployAllMcpTool,\n mcpExposed: true,\n // Writes to real cloud infrastructure. Both carry `requiresHumanConfirm`, so an agent gets the\n // two-phase gate \u2014 unlike the release-deploy-* pair these leave no CI run to inspect afterwards.\n mutating: true,\n groupPath: ['local', 'deploy-all'],\n },\n {\n cliName: 'local-deploy-selected',\n menuGroup: null,\n mcpTool: localDeploySelectedMcpTool,\n mcpExposed: true,\n mutating: true,\n groupPath: ['local', 'deploy-selected'],\n },\n\n // --- Worktrees (menu group) ---\n {\n cliName: 'worktrees-add',\n menuGroup: 'worktrees',\n mcpTool: worktreesAddMcpTool,\n mcpExposed: true,\n mutating: true,\n groupPath: ['worktrees', 'add'],\n },\n {\n cliName: 'worktrees-list',\n menuGroup: 'worktrees',\n mcpTool: worktreesListMcpTool,\n mcpExposed: true,\n mutating: false,\n groupPath: ['worktrees', 'list'],\n },\n // `reopen` is a top-level command (`infra-kit reopen`, groupPath ['reopen']) but carries\n // menuGroup 'worktrees' so it renders alongside its kin in the palette \u2014 the same single-segment\n // top-level + non-matching-menuGroup shape as env-status/env-load (['env-status'] + 'environment').\n {\n cliName: 'reopen',\n menuGroup: 'worktrees',\n mcpTool: reopenMcpTool,\n mcpExposed: true,\n mutating: false,\n groupPath: ['reopen'],\n },\n // worktrees-remove runs `git worktree remove` (no --force) on the named leaf worktrees. It is NOT\n // irreversible in the way release-deliver is: the release branches/commits survive, the\n // worktrees scaffold is left in place, and worktrees-add recreates a removed worktree. git also\n // refuses to remove a worktree with modified-tracked or untracked files. The residual risk \u2014\n // deletion of gitignored local state (a hydrated `.env` of Doppler secrets, node_modules/dist) \u2014 is\n // contained by the tool's own invariants rather than by withholding it: over MCP it rejects\n // all=true (no one-shot wipe) and errors on any unmatched target before removing anything. So it is\n // exposed, unlike the genuinely-irreversible release-deliver. After a refused removal it sweeps\n // ONLY a leftover git has already unregistered and that holds nothing but `.omc/{state,sessions}`\n // and `.DS_Store` (a post-exit hook re-creating tool state mid-deletion); everything else is\n // reported in failedWorktrees with isError.\n {\n cliName: 'worktrees-remove',\n menuGroup: 'worktrees',\n mcpTool: worktreesRemoveMcpTool,\n mcpExposed: true,\n mutating: true,\n groupPath: ['worktrees', 'remove'],\n },\n {\n cliName: 'worktrees-sync',\n menuGroup: 'worktrees',\n mcpTool: worktreesSyncMcpTool,\n mcpExposed: true,\n mutating: true,\n groupPath: ['worktrees', 'sync'],\n },\n\n // --- Environment (menu group): the Doppler env commands, and ONLY those ---\n // Every menu-eligible entry must be a Commander LEAF (an action, no subcommands) so the session\n // shell's report discriminator holds. The bare `vendor`/`config` GROUPS are menuGroup:null \u2014 a bare\n // group prints help and exits non-zero, which the shell would misreport. Their useful leaves\n // (vendor-check/diff/config, config-path/edit) are surfaced in the Vendor/Configuration groups below,\n // each with a hidden flat Commander alias (see src/lib/program/program.ts) so the palette can\n // introspect + single-token dispatch.\n {\n cliName: 'env-status',\n menuGroup: 'environment',\n mcpTool: envStatusMcpTool,\n mcpExposed: true,\n mutating: false,\n groupPath: ['env-status'],\n },\n {\n cliName: 'env-list',\n menuGroup: 'environment',\n mcpTool: envListMcpTool,\n mcpExposed: true,\n mutating: false,\n groupPath: ['env-list'],\n },\n {\n cliName: 'env-load',\n menuGroup: 'environment',\n mcpTool: envLoadMcpTool,\n mcpExposed: true,\n mutating: true,\n groupPath: ['env-load'],\n sessionEnvNotice: true,\n },\n {\n cliName: 'env-clear',\n menuGroup: 'environment',\n mcpTool: envClearMcpTool,\n mcpExposed: true,\n mutating: true,\n groupPath: ['env-clear'],\n sessionEnvNotice: true,\n },\n // The only env-token command that is agent-callable. It READS, and only ever emits redacted values.\n {\n cliName: 'env-token-list',\n menuGroup: 'environment',\n mcpTool: envTokenListMcpTool,\n mcpExposed: true,\n mutating: false,\n groupPath: ['env-token-list'],\n },\n\n // --- Configuration (menu group) ---\n // Read-only merged-config introspection. Top-level (`infra-kit config-get`) per the house rule \u2014 new\n // commands go top-level with related names \u2014 but menuGroup 'configuration' renders it beside\n // `config path`/`config edit`. Exposed to agents (mutating:false); `config edit` stays CLI-only for writes.\n {\n cliName: 'config-get',\n menuGroup: 'configuration',\n mcpTool: configGetMcpTool,\n mcpExposed: true,\n mutating: false,\n groupPath: ['config-get'],\n },\n {\n cliName: 'config-path',\n menuGroup: 'configuration',\n mcpTool: null,\n mcpExposed: false,\n mutating: false,\n groupPath: ['config', 'path'],\n },\n {\n cliName: 'config-edit',\n menuGroup: 'configuration',\n mcpTool: null,\n mcpExposed: false,\n mutating: true,\n groupPath: ['config', 'edit'],\n entersAltScreen: true,\n },\n\n // --- Vendor (menu group) ---\n {\n cliName: 'vendor-check',\n menuGroup: 'vendor',\n mcpTool: vendorCheckMcpTool,\n mcpExposed: true,\n mutating: false,\n groupPath: ['vendor', 'check'],\n },\n {\n cliName: 'vendor-config',\n menuGroup: 'vendor',\n mcpTool: null,\n mcpExposed: false,\n mutating: true,\n groupPath: ['vendor', 'config'],\n },\n\n // --- Setup & Diagnostics (menu group) ---\n // `doctor` checks the machine; `audit` checks the REPO against its config rules. Both answer \"is this\n // in a good state?\", which is why they sit together. The command that ACTS on those answers \u2014 `setup` \u2014\n // is deliberately not here; see its entry below for the two reasons.\n {\n cliName: 'doctor',\n menuGroup: 'setup',\n mcpTool: doctorMcpTool,\n mcpExposed: true,\n // `mutating: false` is a statement about the EXPOSED TOOL, not about the CLI command. `--fix`\n // chmods the token store and prunes stale portless routes, and it is unreachable from here:\n // `doctorMcpTool.inputSchema` is `{}` and its handler is parameterless, so an agent has no way to\n // ask for it. Exactly the `audit` precedent (`mutating: false` beside a CLI-only `--fix`), and\n // pinned the same way \u2014 plus U-D2(b), which drives the real `doctor()` and asserts the handler\n // reaches zero `chmodSync` calls on a fixture the CLI path demonstrably chmods.\n mutating: false,\n groupPath: ['doctor'],\n },\n {\n cliName: 'audit',\n menuGroup: 'setup',\n mcpTool: auditMcpTool,\n mcpExposed: true,\n mutating: false,\n groupPath: ['audit'],\n },\n {\n cliName: 'version',\n menuGroup: 'setup',\n mcpTool: versionMcpTool,\n mcpExposed: true,\n mutating: false,\n groupPath: ['version'],\n },\n\n // --- Not menu items (groups, long-running, or internal) ---\n // Bare groups: help + non-zero exit, so they never belong in the leaf-only menu.\n { cliName: 'vendor', menuGroup: null, mcpTool: null, mcpExposed: false, mutating: false, groupPath: ['vendor'] },\n { cliName: 'config', menuGroup: null, mcpTool: null, mcpExposed: false, mutating: false, groupPath: ['config'] },\n // Internal shell-startup trigger; hidden from the menu and never an MCP tool\n // (it can't apply env to a shell \u2014 only the zsh integration sources the file).\n {\n cliName: 'env-autoload',\n menuGroup: null,\n mcpTool: null,\n mcpExposed: false,\n mutating: true,\n groupPath: ['env-autoload'],\n },\n // env-token-set / env-token-remove carry NO MCP tool at all \u2014 not merely `mcpExposed: false`, so\n // there is nothing to accidentally flip on. `lib/tool-handler` injects `confirmedCommand: true` into\n // EVERY tool call, so the MCP boundary auto-confirms by construction: an agent must never be one call\n // away from WRITING a credential or DESTROYING one. Same argument that keeps `doctor --fix` CLI-only.\n // menuGroup is null because both take a required `<env>` argument the no-arg menu cannot supply.\n {\n cliName: 'env-token-set',\n menuGroup: null,\n mcpTool: null,\n mcpExposed: false,\n mutating: true,\n groupPath: ['env-token-set'],\n },\n {\n cliName: 'env-token-remove',\n menuGroup: null,\n mcpTool: null,\n mcpExposed: false,\n mutating: true,\n groupPath: ['env-token-remove'],\n },\n // Sets the machine up in one pass: the local `initCore` writes, then install-or-update for the five\n // external tools. Exposed despite mutating, on the rule the catalog already enforces: exposure is\n // bounded by a tool's own invariants, not by the verb (`worktrees-add` is exposed, ungated, and runs\n // `pnpm install`). Here the invariant is computed \u2014 `lib/dependency-install/risk-predicate` refuses any\n // recipe needing sudo or piping a network-fetched script, which is both bootstrap recipes, on every\n // host regardless of configuration.\n //\n // `requiresHumanConfirm` rather than LOW_RISK_MUTATING_ALLOWLIST membership: the allowlist ASSERTS low\n // risk, and that would be a false claim for a command that installs software. It also carries\n // `_meta['anthropic/requiresUserInteraction']` (setup.ts), the only gate that puts a human on the MCP\n // path. Both fire unconditionally \u2014 `skipTools` included \u2014 because the read path that raises no prompt\n // is `doctor`, a separate tool name and therefore a separate permission identity.\n //\n // `menuGroup: null` IS THE DECISION, not an oversight \u2014 do not \"fix\" it by giving it a group. Two\n // independent reasons, either sufficient:\n // 1. It would be a one-keystroke installer. `run-session.ts:199` spawns `[deps.cliPath,\n // ...command.groupPath]` with ZERO flags, and flags are not Commander leaves, so the palette has\n // no way to offer the narrowed `--skip-tools` form \u2014 only flagless `setup`, which installs.\n // Installing software is a deliberate act rather than something to land on by arrowing a menu.\n // 2. `command-palette.tsx:285` renders `item.name` \u2014 `groupPath.join(' ')`, with NO binary\n // qualifier \u2014 so the row would read a bare `setup`, colliding with the `pnpm run setup` script\n // that exists in this repo and in every consumer. Every GENERATED instruction is bound to write\n // `ik setup`; a palette row is a render, not generated text, and is the one place that\n // mitigation cannot reach.\n // A human who wants the additive local writes without the installer types `infra-kit setup --skip-tools`.\n {\n cliName: 'setup',\n menuGroup: null,\n mcpTool: setupMcpTool,\n mcpExposed: true,\n mutating: true,\n groupPath: ['setup'],\n },\n // The MCP boundary auto-confirms every tool, so an agent-triggered unattended global package install\n // must never be reachable there. menuGroup null keeps it off the no-arg picker too \u2014 updating the CLI\n // is a deliberate act, not something to land on by arrowing through a menu.\n {\n cliName: 'self-update',\n menuGroup: null,\n mcpTool: null,\n mcpExposed: false,\n mutating: true,\n groupPath: ['self-update'],\n },\n // Launcher for the MCP server itself; it blocks on a stdio transport, so it is neither a one-shot menu\n // command nor expressible as a request/response tool.\n { cliName: 'mcp', menuGroup: null, mcpTool: null, mcpExposed: false, mutating: false, groupPath: ['mcp'] },\n]\n\n/**\n * Mutating, MCP-exposed tools that are DELIBERATELY exempt from the destructive-op confirm gate\n * (`mcpTool.requiresHumanConfirm`) because each is low-risk / reversible. Hand-maintained: adding a\n * name here is the ONLY sanctioned way to opt a mutating exposed tool out of the gate, and the\n * default-deny test (`command-catalog.test.ts`) reds CI for any mutating+exposed tool that is neither\n * gated nor listed here. Every member carries a one-line justification so an audit reads at a glance.\n */\nexport const LOW_RISK_MUTATING_ALLOWLIST: readonly string[] = [\n // Overwrites the Jira fix-version description and the release PR body \u2014 reversible by re-editing;\n // no git/branch/deploy side effect.\n 'release-desc-edit',\n // Purely additive: creates worktrees for existing release branches; removing them is a separate op.\n 'worktrees-add',\n // Reconciles the worktree set to the live release branches; recreatable via worktrees-add.\n 'worktrees-sync',\n // Writes a load file under the infra-kit cache dir; cleared by env-clear, no remote/git effect.\n 'env-load',\n]\n\n// `openWorld` is DECLARED rather than derived because neither candidate derivation is sound. An\n// import-graph check produces false NEGATIVES on the four deploy tools (they reach GitHub/AWS through\n// `zx` with no `src/integrations/*` import at all \u2014 an error in the dangerous direction on the most\n// dangerous tools in the catalog) and false POSITIVES on `env-list` and `worktrees-remove` (which\n// import an integration module whose MCP-reachable path never calls out). The per-row call-site\n// citation IS the control: nobody can write \"never calls Doppler\" next to `env-token-list.ts:115-131`.\n//\n// Allowlist membership and `destructiveHint` are independent \u2014 all four LOW_RISK_MUTATING_ALLOWLIST\n// members read destructive. The allowlist answers \"is a confirm prompt warranted?\"; the hint answers\n// \"does this perform destructive updates?\".\n/** Display title and the one annotation nothing derives (`openWorld`), per exposed tool. */\nexport const MCP_TOOL_PRESENTATION: Record<string, { title: string; openWorld: boolean }> = {\n // --- Read-only ---\n // Reads on-disk dev-context fragments; the TCP liveness probe is loopback-only (dev-status.ts:178).\n 'dev-status': { title: 'Dev server status', openWorld: false },\n // gh-release-list.ts:13 getReleasePRsWithInfo(); getJiraDescriptions imported :6.\n 'gh-release-list': { title: 'Open releases', openWorld: true },\n // worktrees-list.ts:38 Promise.all([getReleasePRsWithInfo(), getJiraDescriptions()]).\n 'worktrees-list': { title: 'Release worktrees', openWorld: true },\n // env-status.ts:94 \u2014 \"Pure local introspection \u2014 makes NO Doppler call\".\n 'env-status': { title: 'Loaded environment', openWorld: false },\n // env-list.ts:147 \u2014 \"never a live Doppler probe\". The :4 doppler import is getDopplerProject, which\n // reads getInfraKitConfig() and returns a name (doppler-project.ts:23-27) \u2014 no network.\n 'env-list': { title: 'Available environments', openWorld: false },\n // env-token-list.ts:115-131 probeEnvToken per environment, and `check` is in the MCP inputSchema\n // (:173-176), so the probe is agent-reachable.\n 'env-token-list': { title: 'Doppler service tokens', openWorld: true },\n // config-get.ts:58 \u2014 \"Read-only introspection \u2026 use `config edit` (CLI-only) to modify\".\n 'config-get': { title: 'Merged infra-kit config', openWorld: false },\n // vendor-check.ts:120 \u2014 \"Self-contained (no source repo or config needed)\".\n 'vendor-check': { title: 'Vendor checksum check', openWorld: false },\n // audit.ts:242-255 reads workspace manifests; --fix/--design are unreachable through the MCP schema.\n audit: { title: 'Package audit', openWorld: false },\n // Reads packageJson.version; no registry check.\n version: { title: 'CLI version', openWorld: false },\n // doctor.ts spawns `gh auth status`, and the `gh` / `doppler` / `aws` rows shell out to binaries that\n // reach their own services. openWorld is TRUE for that reason. DECLARED, never derived: the network\n // is reached through third-party binaries, so no import-graph rule could have spotted it.\n doctor: { title: 'Diagnose this machine', openWorld: true },\n\n // --- Mutating, gated ---\n // setup.ts \u2014 the init half writes locally, then `converge.ts` reaches `runRecipe`, which spawns\n // `brew install` / `npm install -g`; both fetch from a remote registry. openWorld is TRUE for that\n // reason, even though the argv are registry literals. It stays TRUE under `skipTools:true` as well:\n // the annotation describes the tool, not the arguments of one call.\n setup: { title: 'Set up this machine', openWorld: true },\n // gh-merge-dev.ts:355 getReleasePRsWithInfo(); pushes to remote release branches.\n 'gh-merge-dev': { title: 'Merge dev into release branches', openWorld: true },\n // release-create.ts:7 loadJiraConfig from src/integrations/jira; creates branches and PRs.\n 'release-create': { title: 'Create releases', openWorld: true },\n // release-remove.ts:5 fetchPRByHead from src/integrations/gh and :9-15 the Jira barrel; closes the\n // PR via the gh CLI and deletes the remote branch. Reaches Jira read-only under MCP \u2014 the delete is\n // the one step it never attempts there \u2014 but a read is still an outbound call, so openWorld holds.\n 'release-remove': { title: 'Remove a release', openWorld: true },\n // gh-release-deploy-all.ts:2 `import { $ } from 'zx'` \u2014 dispatches deploy-all.yml via the gh CLI.\n // Imports NO src/integrations/*, which is why the import-graph derivation was rejected.\n 'gh-release-deploy-all': { title: 'Deploy all services (CI)', openWorld: true },\n // gh-release-deploy-selected.ts:6 `import { $ } from 'zx'` \u2014 dispatches deploy-selected-services.yml\n // via the gh CLI. Same import shape as deploy-all: no src/integrations/* to spot.\n 'gh-release-deploy-selected': { title: 'Deploy selected services (CI)', openWorld: true },\n // local-deploy.ts runs devops/scripts/deploy-*.sh against AWS via zx; no src/integrations/* import.\n 'local-deploy-all': { title: 'Deploy all services from this machine', openWorld: true },\n // local-deploy.ts:503 shares the same body as local-deploy-all (:304-321), running\n // devops/scripts/deploy-<name>.sh against the authenticated AWS account via zx.\n 'local-deploy-selected': { title: 'Deploy selected services from this machine', openWorld: true },\n // The only network path is worktrees-remove.ts:130, inside the picker `else` branch at :129-141.\n // That branch is unreachable via MCP twice over: the inputSchema declares `versions` REQUIRED and\n // omits `all` entirely, and assertMcpRemovalInput (:39-57, called at :97) throws under isMcpMode()\n // on `all` and on missing `versions`.\n 'worktrees-remove': { title: 'Remove release worktrees', openWorld: false },\n // Emits `unset` statements into a local shell script and returns its path; the whole output surface\n // (filePath, unsetStatements, variableCount, purged) is local-file shaped, with no remote to contact.\n 'env-clear': { title: 'Clear environment variables', openWorld: false },\n\n // --- Mutating, ungated (LOW_RISK_MUTATING_ALLOWLIST members \u2014 still destructive) ---\n // release-desc-edit.ts:180 updateJiraVersion, :185 updateReleasePRBody. Overwrites existing text in\n // two systems.\n 'release-desc-edit': { title: 'Edit release description', openWorld: true },\n // worktrees-add.ts:82 getReleasePRsWithInfo(); :301 `pnpm install`. Additive in fact (existing\n // worktrees are skipped, not errored) \u2014 reads destructive under the uniform derivation, the single\n // accepted over-claim.\n 'worktrees-add': { title: 'Add release worktrees', openWorld: true },\n // worktrees-sync.ts:45 getReleasePRs(). \"Only removes \u2014 never creates\".\n 'worktrees-sync': { title: 'Prune stale worktrees', openWorld: true },\n // env-load.ts:17 imports src/integrations/doppler and downloads secrets. Destructive: :192\n // \"atomically write env-load.sh\" into getSessionCacheDir() (:212) \u2014 a path deterministic per\n // terminal session, so loading `dev` after `prod` OVERWRITES.\n 'env-load': { title: 'Load environment variables', openWorld: true },\n\n // --- Not-read-only exception ---\n // Spawns local editor and cmux processes on this machine; every output field (worktreePaths,\n // ideProviders, cmuxOpened/Skipped/Closed) describes local window state and contacts no remote.\n reopen: { title: 'Reopen project windows', openWorld: false },\n}\n\n/**\n * Tools that modify machine state the {@link CommandCatalogEntry.mutating} predicate deliberately\n * excludes \u2014 it covers only git/remote/consumer-repo/Doppler-env/fs-outside-cache state.\n *\n * This array may ONLY ever TIGHTEN a hint toward the spec default of `readOnlyHint: false`; it can\n * never loosen one, because it is applied as a negation. `reopen` is `mutating: false` yet its\n * MCP-reachable `force` flag closes cmux workspaces (\"Close each cmux workspace first, then reopen\n * (disruptive)\") and reports them in `cmuxClosed`. Pinned by the T7 catalog test.\n */\nexport const NOT_READ_ONLY: readonly string[] = ['reopen']\n\n/**\n * The MCP tools to register: catalog entries that are exposed and carry a tool, each widened with the\n * display title and the protocol annotations derived from the catalog's own fields.\n *\n * `readOnlyHint` is DERIVED from `mutating` rather than hand-typed, so it cannot silently disagree\n * with the field the confirm gate keys off. `destructiveHint` follows from it: present as `true` on\n * every write tool, absent on every read tool (the spec makes it meaningless there). Only `title` and\n * `openWorldHint` are declared, in {@link MCP_TOOL_PRESENTATION}.\n *\n * The returned objects are SPREAD COPIES \u2014 `{ ...entry.mcpTool, title, annotations }` \u2014 not the\n * catalog's `mcpTool` object identities. `handler` survives as a reference, so dispatch is unaffected,\n * but never write an identity comparison against `entry.mcpTool` on the strength of this return value.\n */\nexport const getExposedMcpTools = (): RegistrableMcpTool[] => {\n return commandCatalog.flatMap((entry) => {\n if (!entry.mcpExposed || !entry.mcpTool) {\n return []\n }\n\n const presentation = MCP_TOOL_PRESENTATION[entry.mcpTool.name]\n\n // Scoped HERE rather than at module scope on purpose: this module is imported by every CLI command\n // path, so a module-scope throw on a table typo would take down every command instead of only MCP\n // registration. The T1/T3 catalog tests already red CI for a missing row.\n if (!presentation) {\n throw new Error(\n `No MCP_TOOL_PRESENTATION entry for exposed tool \"${entry.mcpTool.name}\" \u2014 add a title and an evidence-cited openWorld value in command-catalog.ts.`,\n )\n }\n\n const readOnlyHint = !entry.mutating && !NOT_READ_ONLY.includes(entry.mcpTool.name)\n\n return [\n {\n ...entry.mcpTool,\n title: presentation.title,\n annotations: {\n readOnlyHint,\n ...(readOnlyHint ? {} : { destructiveHint: true }),\n openWorldHint: presentation.openWorld,\n },\n },\n ]\n })\n}\n\n/**\n * Catalog entries for a menu group, in catalog (display) order.\n *\n * Yields ENTRIES, not names: every menu surface addresses a command by its {@link\n * CommandCatalogEntry.groupPath} (`['release','create']`), which is both what it renders and what it\n * spawns. `cliName` is an id \u2014 the MCP tool name, which cannot hold a space \u2014 and is not a CLI surface.\n *\n * @example\n * getMenuGroupEntries('vendor').map((entry) => entry.groupPath.join(' ')) // => ['vendor check', \u2026]\n */\nexport const getMenuGroupEntries = (group: MenuGroup): CommandCatalogEntry[] => {\n return commandCatalog.filter((entry) => {\n return entry.menuGroup === group\n })\n}\n\n/**\n * True when the invoked command runs until the user stops it (see {@link CommandCatalogEntry.longRunning}).\n * Keyed by the SPACE-JOINED Commander path (`'dev'`, `'vendor check'`) so it matches what `commandPath()`\n * hands the `postAction` hook, not the flat alias.\n *\n * @example\n * isLongRunningCommand('dev') // => true (no verdict to report; exits 128+signo)\n * isLongRunningCommand('vendor check') // => false\n */\nexport const isLongRunningCommand = (commandPath: string): boolean => {\n return commandCatalog.some((entry) => {\n return entry.longRunning === true && entry.groupPath.join(' ') === commandPath\n })\n}\n", "import process from 'node:process'\n\n/**\n * Node's warning code for \"I had to guess this file's module type\".\n *\n * Emitted when Node loads a `.ts` (or extensionless-ambiguous) file whose nearest\n * `package.json` declares no `\"type\"`: it first tries to parse the source as\n * CommonJS, fails on the ESM syntax, then reparses as an ES module and warns\n * about the double parse.\n */\nconst TYPELESS_PACKAGE_JSON = 'MODULE_TYPELESS_PACKAGE_JSON'\n\n/** Read the warning `code` out of either `emitWarning` overload. */\nconst readWarningCode = (typeOrOptions: unknown, codeOrCtor: unknown): unknown => {\n // emitWarning(warning, options) \u2014 the code lives on the options object.\n if (typeof typeOrOptions === 'object' && typeOrOptions !== null) {\n return (typeOrOptions as { code?: unknown }).code\n }\n\n // emitWarning(warning, type, code, ctor) \u2014 the code is the third argument.\n return codeOrCtor\n}\n\nlet installed = false\n\n/**\n * Silence Node's `MODULE_TYPELESS_PACKAGE_JSON` warning for the rest of this\n * process, leaving every other warning untouched. Idempotent, and safe to call\n * before any config import.\n *\n * Filters at `process.emitWarning` rather than replacing the `'warning'` listener,\n * so unrelated warnings keep Node's native formatting and `--trace-warnings` hint.\n * Installed once and never restored.\n *\n * @example\n * suppressTypelessPackageJsonWarning()\n * // no MODULE_TYPELESS_PACKAGE_JSON banner, even though apps/ui/package.json has no \"type\"\n * const config = await import(pathToFileURL('apps/ui/infra-kit.config.ts').href)\n */\n// The CLI imports config files it does not own \u2014 `infra-kit.config.ts` inside a consumer's\n// `apps/<app>/ui` \u2014 via `await import()`, relying on Node's native type stripping. When that\n// package declares no `\"type\"`, Node reparses the config and prints a four-line warning naming a\n// `package.json` that belongs to the consumer, not to us.\n//\n// We swallow it rather than push `\"type\": \"module\"` onto consumers, because we are the ones who\n// chose the `.ts` config format and a published tool cannot assume every package it reads a config\n// from is ESM. (In our own repos this is currently rare \u2014 79 of 80 packages carrying an\n// `infra-kit.config.ts` already declare `\"type\": \"module\"`. The lone holdout is a Docusaurus app\n// whose `babel.config.js` still needs CommonJS resolution, which is exactly the class of consumer\n// we cannot dictate to.) The double parse of a small config is irrelevant; the noise is not.\n//\n// Never restored on purpose: `loadDev` is called concurrently (`Promise.all` over apps), so a\n// scoped save/restore around each `await import()` would race \u2014 one import's restore would fire\n// while another was still linking, and the warning is emitted during linkage, inside that await\n// window.\nexport const suppressTypelessPackageJsonWarning = (): void => {\n if (installed) return\n\n installed = true\n\n const emitWarning = process.emitWarning.bind(process) as (...args: unknown[]) => void\n\n process.emitWarning = ((warning: string | Error, ...rest: unknown[]): void => {\n if (readWarningCode(rest[0], rest[1]) === TYPELESS_PACKAGE_JSON) return\n\n emitWarning(warning, ...rest)\n }) as typeof process.emitWarning\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["process", "z", "fs", "path", "packageConfigSchema", "fs", "path", "pathToFileURL", "z", "path", "PACKAGE_TYPES", "APP_DIRECTORY_TYPES", "MOBILE_BASENAME", "DEPENDENCY_SIGNALS", "fromDirectory", "packageDir", "repoRoot", "segments", "segment", "first", "third", "appType", "fromDependencies", "pkgJson", "deps", "signal", "name", "detectPackageType", "declaredType", "DEFAULT_RULES", "ROOT_DEFAULT_RULES", "resolvePackageConfig", "config", "baseline", "fs", "pathExists", "target", "PACKAGE_CONFIG_FILE", "readPackageJson", "packageDir", "raw", "fs", "path", "importConfigExport", "configPath", "stat", "rawExport", "pathToFileURL", "readDeclaredPackageType", "pathExists", "declared", "PACKAGE_TYPES", "candidate", "loadPackageConfig", "baseline", "DEFAULT_RULES", "resolvedExport", "parsed", "packageConfigSchema", "z", "resolvePackageConfig", "fs", "path", "yaml", "WORKSPACE_FILE", "listChildDirs", "dir", "fs", "entry", "path", "expandSegment", "dirs", "segment", "next", "candidate", "pathExists", "expandGlob", "projectRoot", "pattern", "discoverPackages", "raw", "patterns", "yaml", "found", "path", "findPackageRoot", "start", "current", "path", "pathExists", "hasManagedBlock", "content", "startMarker", "endMarker", "startIdx", "endIdx", "removeManagedBlock", "before", "after", "extractVersion", "versionPrefix", "idx", "match", "buildManagedBlock", "body", "upsertManagedBlock", "placement", "block", "present", "base", "ROOT_MARKER_START", "ROOT_MARKER_END", "ROOT_VERSION_PREFIX", "PACKAGE_MARKER_START", "PACKAGE_MARKER_END", "PACKAGE_VERSION_PREFIX", "readBlockBody", "content", "start", "PACKAGE_MARKER_START", "PACKAGE_MARKER_END", "readType", "body", "idx", "PACKAGE_VERSION_PREFIX", "token", "PACKAGE_TYPES", "candidate", "inspectPackageGuidance", "hasManagedBlock", "ROOT_MARKER_START", "ROOT_MARKER_END", "inspection", "version", "extractVersion", "type", "fs", "readGuidanceFile", "filePath", "WORKSPACE_FILE", "GUIDANCE_FILE", "findWorkspaceRoot", "start", "current", "path", "fs", "adoptionByRoot", "resetAdoptionCache", "probeAdoption", "workspaceRoot", "packageDirs", "discoverPackages", "packageDir", "guidancePath", "content", "readGuidanceFile", "inspectPackageGuidance", "resolveAdoption", "memoized", "state", "fs", "path", "skeleton_default", "backend_default", "e2e_default", "frontend_default", "lib_default", "mobile_default", "body_default", "RESOURCES", "body_default", "frontend_default", "backend_default", "lib_default", "e2e_default", "mobile_default", "skeleton_default", "PLACEHOLDER", "LONE_LIST_ITEM", "collectPlaceholders", "template", "match", "assertNoUnusedVars", "used", "vars", "unused", "name", "assertNoUnknownPlaceholders", "unknown", "n", "dropEmptyListItems", "lines", "line", "assertMultilinePlaceholdersAtColumnZero", "value", "renderTemplate", "_match", "FRONT_MATTER_NAME", "buildDesignSkeleton", "packageName", "rendered", "renderTemplate", "RESOURCES", "nameLine", "buildVersionLine", "version", "type", "PACKAGE_VERSION_PREFIX", "README_BULLET", "buildPackageBody", "args", "packageName", "relDir", "hasReadme", "body", "renderTemplate", "RESOURCES", "buildVersionLine", "version", "ROOT_VERSION_PREFIX", "buildRootBody", "body", "renderTemplate", "RESOURCES", "fs", "path", "assertNotSymlink", "filePath", "stats", "fs", "writeManaged", "next", "previous", "path", "assertBlockPresent", "start", "end", "content", "hasManagedBlock", "outsideMarkers", "assertOutsideMarkersUnchanged", "before", "after", "LEGACY_IMPORT_START", "LEGACY_IMPORT_END", "AGENTS_FILE", "CLAUDE_FILE", "README_FILE", "DESIGN_FILE", "DESIGN_TYPES", "readExistingFile", "filePath", "fs", "hasExactFile", "dir", "name", "failedWrite", "error", "type", "message", "entry", "upsertGuidanceFile", "body", "startMarker", "endMarker", "prepare", "before", "baseline", "next", "upsertManagedBlock", "action", "writeManaged", "assertBlockPresent", "assertOutsideMarkersUnchanged", "migrateLegacyAgentsFile", "agentsPath", "assertNotSymlink", "content", "stripped", "removeManagedBlock", "ROOT_MARKER_START", "ROOT_MARKER_END", "syncRootClaudeFile", "claudePath", "version", "buildRootBody", "syncRootGuidance", "root", "path", "written", "syncDesignFile", "packageDir", "packageName", "designPath", "buildDesignSkeleton", "syncPackageGuidance", "repoRoot", "design", "declaredType", "pkgJson", "readPackageJson", "detectPackageType", "buildPackageBody", "PACKAGE_MARKER_START", "PACKAGE_MARKER_END", "designWrite", "path", "path", "CHECK_NAME", "GUIDANCE_FILE", "FIX_HINT", "STATE_MESSAGES", "PACKAGE_MARKER_START", "PACKAGE_MARKER_END", "ROOT_MARKER_START", "evidenceSuffix", "adoption", "path", "checkAgentGuidance", "packageDir", "inspection", "inspectPackageGuidance", "readGuidanceFile", "version", "type", "detail", "message", "checkConfig", "packageDir", "baseline", "DEFAULT_RULES", "rules", "loadPackageConfig", "PACKAGE_CONFIG_FILE", "err", "fs", "path", "checkFiles", "packageDir", "requiredFiles", "file", "stat", "checkScripts", "scripts", "requiredScripts", "script", "value", "fs", "path", "TURBO_FILE", "checkTurbo", "packageDir", "requiredTasks", "parsed", "raw", "err", "tasks", "task", "defined", "UNADOPTED", "validatePackage", "packageDir", "baseline", "DEFAULT_RULES", "options", "pkgJson", "readPackageJson", "packageName", "path", "configCheck", "rules", "checkConfig", "checks", "checkAgentGuidance", "checkScripts", "checkFiles", "checkTurbo", "passed", "check", "textContent", "text", "defineMcpTool", "tool", "path", "process", "ALWAYS_COUNTED", "COUNT_ORDER", "syncOnePackage", "packageDir", "repoRoot", "design", "declaredType", "readDeclaredPackageType", "syncPackageGuidance", "package_default", "syncScope", "options", "syncRootGuidance", "written", "discoverPackages", "findPackageRoot", "process", "buildSummaryLine", "counts", "file", "action", "logWrites", "relPath", "path", "type", "reason", "logger", "toFixedEntry", "entry", "runAuditFix", "workspaceRoot", "getProjectRoot", "resetAdoptionCache", "before", "resolveAdoption", "after", "singleScope", "toFailChecks", "issues", "loadErrors", "error", "issue", "checkDevPresets", "root", "presets", "getInfraKitConfig", "keyIssues", "validatePresetKeys", "checks", "apiApps", "discoverApiApps", "uiApps", "discoverUiApps", "discovered", "a", "ctx", "buildPresetProxyContext", "failChecks", "validatePresetProxy", "ADOPTION_FLIP_LINE", "resolveTargets", "options", "getProjectRoot", "ROOT_DEFAULT_RULES", "discoverPackages", "dir", "findPackageRoot", "process", "resolveAdoptionStart", "logResults", "results", "total", "failed", "result", "check", "logger", "allPassed", "plural", "count", "noun", "scope", "applyFix", "workspaceRoot", "fixed", "adoptionFlipped", "runAuditFix", "audit", "findWorkspaceRoot", "targets", "adoption", "resolveAdoption", "target", "validatePackage", "presetResult", "checkDevPresets", "structuredContent", "textContent", "auditInputSchema", "z", "auditOutputSchema", "auditMcpTool", "defineMcpTool", "params", "z", "configGet", "config", "paths", "getInfraKitConfig", "getInfraKitConfigPaths", "topLevelKeys", "logger", "structuredContent", "textContent", "configGetOutputSchema", "z", "configGetMcpTool", "defineMcpTool", "fs", "z", "fs", "path", "process", "DEV_CONTEXT_DIR", "STATE_DIR", "findUp", "start", "relative", "dir", "candidate", "parent", "resolveFragmentDir", "cwd", "existing", "state", "num", "value", "str", "readFragment", "fragmentDir", "file", "now", "full", "stat", "parsed", "fragment", "readDevContext", "files", "apps", "app", "a", "b", "devStatus", "deps", "now", "isListening", "defaultIsListening", "snapshot", "readDevContext", "at", "apps", "record", "ageSeconds", "live", "active", "contextDir", "contextDirExists", "fs", "where", "logger", "app", "liveness", "freshness", "why", "structuredContent", "textContent", "devStatusOutputSchema", "z", "devStatusMcpTool", "defineMcpTool", "fs", "os", "path", "process", "z", "$", "execFileSync", "decidePrune", "probed", "devSessionRunning", "dead", "route", "DEV_RUNNER_PATTERNS", "looksLikeDevRunner", "command", "pattern", "PS_BIN", "defaultReadCommands", "isDevSessionRunning", "readCommands", "Chalk", "process", "SECTION_TOOLS", "SECTION_SHELL", "SECTION_CONFIG", "SECTION_TOKENS", "SECTION_PROXY", "SECTION_PLUGIN", "PORTLESS_SERVING_NAME", "DEFAULT_DEV_PROXY_PORT", "SECTION_MEMBERS", "OTHER_SECTION", "SECTION_ORDER", "label", "SECTION_BY_NAME", "names", "name", "DOCTOR_CHECK_NAMES", "FIXABLE_NAMES", "groupChecks", "checks", "buckets", "check", "bucket", "GLYPHS", "MIN_MESSAGE_WIDTH", "wrapText", "text", "width", "words", "word", "lines", "current", "tally", "failed", "createPalette", "color", "chalk", "Chalk", "formatRollup", "counts", "total", "palette", "glyphs", "plain", "ratio", "INDENT", "ROW_INDENT", "formatSectionHeader", "section", "rollup", "gap", "formatCheckRow", "nameWidth", "glyphWidth", "plainGlyph", "plainName", "glyph", "messageColumn", "messageWidth", "first", "rest", "paint", "line", "formatSummary", "rule", "fixable", "passed", "totals", "hintText", "hint", "formatDoctorReport", "options", "unicode", "sections", "FORCE_COLOR_OFF", "LOCALE_VARS", "resolveColor", "env", "stream", "resolveUnicode", "ascii", "locale", "value", "resolveReportCapabilities", "input", "process", "printDoctorReport", "deps", "select", "Buffer", "fs", "path", "process", "z", "$", "PROJECT_NOT_FOUND_MARKER", "CONFIG_NOT_FOUND_MARKER", "INVALID_TOKEN_MARKER", "TOKEN_SCOPE_MARKER", "classifyDopplerFailure", "stderr", "classifyDopplerAuthFailure", "buildDopplerAuthFailureMessage", "env", "DopplerAuthError", "EnvAuthError", "authKind", "message", "buildDopplerNotFoundMessage", "kind", "project", "config", "available", "lines", "label", "$", "DOPPLER_PROBE_TIMEOUT_MS", "SCOPE_KEY", "getDopplerProject", "envManagement", "getInfraKitConfig", "probeEnvToken", "childEnv", "project", "config", "prevQuiet", "$", "scopedTo", "error", "classifyProbeFailure", "stderr", "extractStderr", "classifyDopplerFailure", "classifyDopplerAuthFailure", "process", "INFRA_KIT_ENV_TOKEN_VAR", "resolveEnvToken", "env", "fromEnv", "process", "fromStore", "readTokenStore", "EnvAuthError", "buildMissingTokenMessage", "getTokenStorePath", "envTokenExists", "storePath", "tildify", "DEFAULT_PROTECTED_ENVS", "PROTECTED_ENV_DENIED", "isProtectedEnv", "env", "SHARED_ENVS", "isSharedEnv", "deployableEnvs", "options", "access", "assertDeployable", "operation", "OperationError", "resolveProtectedEnvAccess", "setting", "getInfraKitConfig", "PROTECTED_ENV_DENIED", "isMcpMode", "warnProtectedEnvDispatch", "args", "env", "branch", "isProtectedEnv", "logger", "fs", "path", "yaml", "WORKFLOWS_DIR", "readWorkflowEnvOptions", "workflowFile", "projectRoot", "getProjectRoot", "parseEnvOptions", "path", "listWorkflowEnvs", "workflowsDir", "entries", "fs", "workflows", "entry", "perWorkflow", "file", "workflowPath", "parsed", "yaml", "on", "pick", "options", "option", "value", "key", "listProjectEnvs", "workflowEnvs", "listWorkflowEnvs", "store", "readTokenStore", "declared", "env", "known", "tokenOnly", "listProjectEnvNames", "entry", "fs", "$", "fs", "path", "shouldWriteWarm", "autoLoaded", "projectDir", "canonicalProjectRoot", "canonicalizeProjectRoot", "projectRoot", "fs", "writeWarmCache", "canonicalProjectDir", "contents", "dir", "getProjectWarmCacheDir", "atomicWriteFileSync", "path", "ENV_LOAD_FILE", "error", "logger", "evictStaleWarmCaches", "nowMs", "ttlSeconds", "DEFAULT_WARM_TTL_SECONDS", "root", "getWarmCacheRoot", "entries", "entry", "mtimeMs", "invalidateProjectWarmCache", "purge", "ENV_CLEAR_FILE", "parseWorktreePaths", "porcelain", "line", "purgeRepoWarmCaches", "$", "error", "logger", "removed", "worktree", "canonical", "canonicalizeProjectRoot", "dir", "getProjectWarmCacheDir", "fs", "buildAutoLoadMarkerLines", "autoLoaded", "INFRA_KIT_ENV_AUTOLOADED_VAR", "shellSingleQuote", "INFRA_KIT_ENV_CLEARED_VAR", "DOPPLER_CONFIG_KEY", "CREDENTIAL_SECRET_KEYS", "INFRA_KIT_ENV_TOKEN_VAR", "credentialFilterWarned", "warnFilteredCredentialKeys", "keys", "logger", "buildEnvLoadFileLines", "pairs", "config", "project", "projectRoot", "loadedAt", "emitted", "key", "value", "INFRA_KIT_ENV_VAR", "INFRA_KIT_ENV_CONFIG_VAR", "INFRA_KIT_ENV_PROJECT_VAR", "INFRA_KIT_ENV_PROJECT_ROOT_VAR", "INFRA_KIT_ENV_LOADED_AT_VAR", "resolveProjectRootSafe", "getProjectRoot", "writeEnvLoadFile", "beforeWrite", "projectDir", "getDopplerProject", "downloadDopplerSecrets", "fileContents", "cacheDir", "getSessionCacheDir", "envFilePath", "path", "ENV_LOAD_FILE", "fs", "atomicWriteFileSync", "canonicalProjectRoot", "canonicalizeProjectRoot", "shouldWriteWarm", "writeWarmCache", "evictStaleWarmCaches", "envLoad", "args", "selectedConfig", "envs", "listProjectEnvNames", "commandEcho", "withEscape", "context", "select", "env", "process", "result", "structuredContent", "textContent", "DOPPLER_MAX_OUTPUT_BYTES", "DOPPLER_DOWNLOAD_TIMEOUT_MS", "INHERITED_DOPPLER_VARS", "buildDopplerChildEnv", "token", "baseEnv", "childEnv", "name", "resolveEnvToken", "prevQuiet", "$", "error", "translateDopplerDownloadError", "assertDopplerOutputSize", "parseDopplerSecretsJson", "assertTokenScope", "actual", "DopplerAuthError", "stderr", "extractStderr", "kind", "classifyDopplerFailure", "classifyDopplerAuthFailure", "buildDopplerNotFoundMessage", "stdout", "bytes", "Buffer", "ENV_VAR_NAME_PATTERN", "parsed", "envLoadMcpTool", "defineMcpTool", "z", "fs", "os", "path", "INFRA_KIT_CONFIG_FILE", "SKIPPED_GIT_ROOT_STEPS", "SKIPPED_GUIDANCE_ONLY", "resolveGitRoot", "resolved", "getProjectRoot", "root", "os", "resolveGitRootForWrites", "logger", "resolveInfraKitRoot", "fs", "path", "discoverWorkspacePackages", "root", "discoverPackages", "err", "logger", "syncRepoGuidance", "resolveInfraKitRoot", "version", "package_default", "written", "syncRootGuidance", "packageDir", "declaredType", "readDeclaredPackageType", "syncPackageGuidance", "fs", "os", "path", "process", "fs", "path", "process", "CONFIG_KEY_DOCS", "buildUserGlobalExample", "buildUserProjectExample", "projectName", "buildVendorExample", "seeded", "exampleSiblingPath", "jsonPath", "readFileOrNull", "filePath", "fs", "seedUserProjectConfig", "paths", "configPath", "examplePath", "path", "createdConfig", "fileExists", "example", "buildUserProjectExample", "refreshedExample", "resetMergedConfigCache", "seedCreatedMessage", "result", "tildify", "ensureUserProjectConfig", "process", "getInfraKitConfigPaths", "logger", "err", "spawnSync", "fs", "os", "path", "fs", "path", "MARKETPLACE_NAME", "PLUGIN_KEY", "MARKETPLACE_REPO", "PLUGIN_INSTALL_COMMAND", "MARKETPLACE_ADD_COMMAND", "DEFAULT_INDENT", "buildMarketplaceEntry", "buildInitialSettings", "isPlainObject", "value", "detectIndent", "raw", "addMissingKey", "settings", "container", "key", "existing", "addPointerKeys", "entry", "readSettingsFile", "settingsPath", "fs", "createSettingsFile", "path", "ensurePluginPointer", "parsed", "logger", "added", "indent", "trailingNewline", "PLUGINS_DIR", "readJsonFile", "filePath", "fs", "isPlainObject", "value", "readString", "source", "key", "pluginsPath", "file", "home", "os", "path", "isMarketplaceRegistered", "parsed", "MARKETPLACE_NAME", "toInstallation", "entry", "readInstallations", "entries", "PLUGIN_KEY", "installation", "canonicalPath", "target", "coversProject", "root", "resolvePluginInstall", "query", "installations", "covering", "readInstalledPluginVersion", "manifest", "looksLikeInfraKitServer", "command", "args", "inspectMcpRegistration", "projectRoot", "mcpPath", "servers", "misfiled", "CLAUDE_BIN", "CLAUDE_TIMEOUT_MS", "MARKETPLACE_ADD_ARGV", "MARKETPLACE_REPO", "PLUGIN_INSTALL_ARGV", "PLUGIN_KEY", "CLAUDE_VERSION_ARGV", "defaultClaudeRunner", "command", "result", "spawnSync", "output", "isInstalledFor", "projectRoot", "home", "resolvePluginInstall", "firstLine", "text", "candidate", "ensureMarketplace", "run", "isMarketplaceRegistered", "logger", "installPluginForProject", "options", "marketplaceFailure", "installed", "fs", "path", "MCP_FILE_NAME", "SERVERS_KEY", "SERVER_COMMAND", "DEFAULT_INDENT", "buildServerEntry", "isPlainObject", "value", "outcome", "status", "mcpPath", "misfiledKey", "detectIndent", "raw", "readMcpFile", "fs", "error", "parseJson", "describeError", "writeFileOrWarn", "contents", "logger", "MARKETPLACE_NAME", "persist", "parsed", "trailingNewline", "createMcpFile", "document", "path", "findMisfiledKey", "projectRoot", "registration", "inspectMcpRegistration", "registerServer", "servers", "misfiled", "ensureMcpRegistration", "read", "fs", "path", "pathToFileURL", "yaml", "z", "fs", "os", "path", "z", "z", "FACTORY_CONFIG_FILE", "factoryConfigSchema", "getFactoryConfigPath", "path", "os", "USER_CONFIG_DIR_NAME", "FACTORY_CONFIG_FILE", "expandTilde", "p", "loadFactoryConfig", "configPath", "raw", "fs", "data", "err", "parsed", "factoryConfigSchema", "z", "legacyYmlPath", "jsonPath", "migrateLegacyConfig", "paths", "getInfraKitConfigPaths", "layers", "infraKitConfigSchema", "path", "infraKitOverrideConfigSchema", "migrated", "layer", "ymlExists", "jsonExists", "fileExists", "logger", "tildify", "raw", "fs", "parsed", "yaml", "result", "z", "err", "resetInfraKitConfigCache", "migrateUserGlobalConfigFilename", "dir", "pairs", "renamed", "from", "to", "fromExists", "toExists", "migrateFactoryConfigToJson", "newJson", "getFactoryConfigPath", "oldTs", "oldExists", "newExists", "stat", "pathToFileURL", "wasFunction", "resolved", "factoryConfigSchema", "stripLegacyIdeMode", "ide", "changed", "stripEntry", "entry", "config", "restConfig", "key", "nextIde", "normalizeLegacyIdeStructures", "jsonPaths", "normalized", "MARKER_START", "MARKER_END", "LEGACY_PAIRED", "LEGACY_SINGLE", "InitStepError", "step", "reason", "SHELL_ACTIVATION_REMINDER", "MIGRATIONS_CHECKED", "withStep", "body", "err", "initCore", "onStep", "report", "record", "entries", "entry", "writeShellBlock", "runConfigMigrations", "seedUserGlobalConfig", "guidanceRoot", "root", "syncAgentGuidance", "gitRoot", "resolveGitRootForWrites", "gateDisagreementEntries", "syncPluginPointer", "reseedUserProjectConfig", "shellEntries", "logInitEntry", "logger", "zshrcPath", "path", "os", "existing", "fs", "removeExistingBlock", "updated", "upsertManagedBlock", "buildShellBody", "migrateLegacyConfig", "normalizeLegacyIdeStructures", "migrateUserGlobalConfigFilename", "migrateFactoryConfigToJson", "shell", "process", "userConfigDir", "userConfigPath", "buildUserGlobalExample", "buildVendorExample", "projectConfigSkip", "paths", "getInfraKitConfigPaths", "result", "seedUserProjectConfig", "seedCreatedMessage", "why", "guidanceEntries", "version", "written", "file", "suffix", "failed", "skipped", "syncRepoGuidance", "pointerEntry", "relative", "mcpEntry", "MARKETPLACE_NAME", "PLUGIN_KEY", "manualInstallEntries", "MARKETPLACE_ADD_COMMAND", "PLUGIN_INSTALL_COMMAND", "command", "installEntries", "outcome", "ensurePluginPointer", "ensureMcpRegistration", "installPluginForProject", "message", "isBlockLine", "line", "content", "removeManagedBlock", "start", "end", "legacyResult", "legacyIdx", "before", "afterLines", "i", "remaining", "runCmd", "buildShellBlock", "fs", "readOverrideSummary", "filePath", "exists", "raw", "parsed", "keys", "describeOverrides", "summary", "fs", "os", "path", "process", "$", "extraBinDirs", "path", "os", "resolveOnPath", "binName", "$", "defaultProbeDeps", "argv", "result", "onPath", "dir", "candidate", "fs", "safeRealpath", "process", "managed", "steps", "firstVersion", "stdout", "UNIX", "brewKegIdentify", "kegName", "binRealPath", "isBrewKegOf", "brewUpgradeFor", "brewFormula", "manager", "BREW", "hasSegment", "AWS_NAMES", "AWS", "GH_NAMES", "GH", "DOPPLER_NAMES", "DOPPLER", "PORTLESS_PACKAGE", "PORTLESS", "npmPrefixOfPackage", "DEPENDENCY_SPECS", "DEPENDENCY_IDS", "specFor", "id", "supportsPlatform", "spec", "platform", "installOrder", "ids", "ordered", "seen", "visit", "stack", "prerequisite", "NOT_FOUND", "id", "tryRun", "runCommand", "argv", "tryResolveRealBin", "deps", "binName", "resolved", "probeDependency", "spec", "supportsPlatform", "viaPath", "binRealPath", "versionSource", "probeAll", "ids", "specFor", "$", "process", "SERAPH_HEADER", "SERAPH_AUTHENTICATION_FAILED", "firstLine", "body", "max", "line", "classifyJiraFailure", "status", "seraphLoginReason", "explain", "kind", "JiraApiError", "args", "detail", "isJiraApiError", "error", "readSeraphLoginReason", "response", "assertJiraOk", "response", "context", "body", "error", "JiraApiError", "readSeraphLoginReason", "logger", "buildJiraVersionUrl", "config", "version", "createJiraVersion", "params", "baseUrl", "token", "email", "projectId", "requestBody", "url", "credentials", "getProjectVersions", "findVersionByName", "versionName", "v", "updateJiraVersion", "deliverJiraRelease", "loadJiraConfig", "process", "projectIdStr", "missingVars", "errorMessage", "loadJiraConfigOptional", "getVersionRelatedIssueCounts", "versionId", "config", "baseUrl", "token", "email", "url", "credentials", "response", "assertJiraOk", "counts", "removeJiraVersion", "params", "requestBody", "parseVersion", "versionStr", "sortVersions", "versions", "a", "b", "majA", "minA", "patchA", "majB", "minB", "patchB", "NEXT_TOKEN", "VERSION_RE", "stripBranchPrefix", "raw", "tryParse", "cleaned", "match", "semverKey", "v", "collectKnownVersions", "sources", "all", "parsed", "seen", "key", "sortVersions", "s", "parseVersion", "NoPriorVersionsError", "computeNextVersion", "known", "type", "max", "major", "minor", "highestPatchOnMinor", "acc", "isNextToken", "token", "isReleaseType", "value", "isNamedReleaseInput", "input", "parseReleaseSpec", "spec", "firstColon", "description", "rest", "secondColon", "typeRaw", "typeLower", "entry", "formatReleaseSpec", "withDescription", "base", "resolveNamedInput", "name", "validateName", "resolveReleaseEntries", "entries", "running", "trimmed", "next", "parseReleaseRef", "explicit", "hasNextToken", "e", "extractVersionBranches", "lsRemoteStdout", "line", "tab", "parseBranchName", "id", "parseRemoteRefs", "result", "$", "fetchJiraVersionNames", "config", "loadJiraConfigOptional", "getProjectVersions", "v", "loadExistingVersions", "branchesResult", "jiraResult", "logger", "collectKnownVersions", "DETAILED_IDS", "probeDependencyDetails", "deps", "states", "probeAll", "state", "checkCommand", "name", "command", "successMsg", "failMsg", "$", "checkZshrcInitialized", "zshrcPath", "path", "os", "fs", "content", "startIdx", "MARKER_START", "endIdx", "MARKER_END", "installedBlock", "expectedBlock", "buildShellBlock", "checkWarmCache", "root", "getProjectRoot", "canon", "canonicalizeProjectRoot", "warmFile", "getProjectWarmCacheDir", "ENV_LOAD_FILE", "ageSeconds", "ageMinutes", "ttlMinutes", "DEFAULT_WARM_TTL_SECONDS", "checkPnpmWorkspaceVirtualStore", "yamlPath", "err", "readDoctorConfig", "resetInfraKitConfigCache", "getInfraKitConfig", "checkInfraKitConfigValid", "read", "TOKEN_FILE_MODE", "TOKEN_DIR_MODE", "describeEntries", "entries", "entry", "held", "resolveEntries", "environments", "resolveToken", "env", "source", "checkEnvTokensConfigured", "readStore", "readTokenStore", "resolveEnvToken", "store", "envs", "listProjectEnvNames", "autoLoadEnv", "universe", "listing", "autoLoadEntry", "PROBE_VERDICT", "checkEnvTokenValid", "probe", "probeEnvToken", "token", "project", "result", "buildDopplerChildEnv", "verdict", "suffix", "defaultReadEnvToken", "process", "INFRA_KIT_ENV_TOKEN_VAR", "checkTokenStorePresent", "storePath", "getTokenStorePath", "displayPath", "tildify", "modeOf", "target", "statPath", "stats", "checkTokenStorePerms", "fix", "defaultStatPath", "chmodPath", "projectDir", "projectsDir", "userConfigDir", "loose", "describe", "formatMode", "mode", "checkUserOverridePath", "paths", "getInfraKitConfigPaths", "exists", "summary", "readOverrideSummary", "describeOverrides", "checkLegacyUserGlobalConfig", "legacyPath", "IDE_PROBE_META", "probeIde", "provider", "meta", "checkIdeInstalled", "providers", "resolveConfiguredIdes", "ide", "probes", "missing", "PLAIN_SEMVER", "isOlderVersion", "candidate", "current", "sortVersions", "collectStalePackageVersions", "packageDirs", "discoverPackages", "stale", "dir", "inspection", "inspectPackageGuidance", "readGuidanceFile", "packageGuidanceStaleness", "oldest", "version", "checkAgentFiles", "mainConfigPath", "claudePath", "present", "hasManagedBlock", "ROOT_MARKER_START", "ROOT_MARKER_END", "message", "staleness", "package_default", "resolveCheckedRepoRoot", "claudePluginVersionCheck", "readInstalledPluginVersion", "PLUGIN_KEY", "MAX_LISTED_PROJECTS", "claudePluginInstalledCheck", "scope", "projectPath", "where", "PLUGIN_INSTALL_COMMAND", "extra", "others", "checkClaudeCli", "checkClaudePlugin", "resolvePluginInstall", "registered", "isMarketplaceRegistered", "MARKETPLACE_NAME", "MARKETPLACE_REPO", "MCP_MESSAGES", "MCP_NON_FAILING", "checkMcpServerKey", "registration", "inspectMcpRegistration", "installDaemonCmd", "bin", "formatPortlessCommand", "trustCmd", "CA_MISMATCH_CODES", "PROBE_BUG_CODE", "checkPortlessServing", "isProxyServing", "DEFAULT_DEV_PROXY_PORT", "checkPortlessCaChain", "handshake", "routes", "caPath", "servernames", "route", "failed", "servername", "codes", "code", "checkPortlessCaTrusted", "trusted", "checkPortlessStaleRoutes", "isListening", "checkPortless", "resolveBin", "resolvePortlessBin", "defaultIsProxyServing", "handshakeChainsToCa", "caTrusted", "caFingerprintMatches", "readRoutes", "listRoutes", "defaultIsListening", "readCaPath", "installed", "serving", "chain", "pruneStalePortlessRoutes", "devRunning", "isDevSessionRunning", "probed", "prunable", "withheld", "decidePrune", "removeAlias", "createPortlessDriver", "alias", "doctor", "options", "details", "defaultProbeDeps", "withDetail", "id", "check", "detail", "found", "baseChecks", "specFor", "portlessChecks", "pruned", "index", "repoRoot", "gitRoot", "resolveGitRoot", "pluginChecks", "checks", "structuredContent", "c", "FIXABLE_NAMES", "textContent", "doctorMcpTool", "defineMcpTool", "z", "fs", "path", "process", "z", "clearProjectWarmCache", "purge", "projectRoot", "getProjectRoot", "canon", "canonicalizeProjectRoot", "invalidateProjectWarmCache", "buildEnvClearLines", "varNames", "v", "INFRA_KIT_ENV_VAR", "INFRA_KIT_ENV_CONFIG_VAR", "INFRA_KIT_ENV_PROJECT_VAR", "INFRA_KIT_ENV_PROJECT_ROOT_VAR", "INFRA_KIT_ENV_LOADED_AT_VAR", "INFRA_KIT_ENV_AUTOLOADED_VAR", "INFRA_KIT_ENV_CLEARED_VAR", "envClear", "cacheDir", "getSessionCacheDir", "envLoadPath", "path", "ENV_LOAD_FILE", "hasLoadedEnv", "fs", "parseVarNamesFromEnvFile", "unsetLines", "clearFilePath", "ENV_CLEAR_FILE", "atomicWriteFileSync", "process", "structuredContent", "textContent", "envClearMcpTool", "defineMcpTool", "z", "process", "z", "buildEnvTokenStatus", "envs", "storeTokens", "envToken", "env", "source", "ENV_TABLE_HEADERS", "formatEnvTable", "rows", "cells", "row", "widths", "header", "col", "cell", "renderRow", "values", "value", "separator", "width", "envList", "project", "getDopplerProject", "listProjectEnvs", "store", "readTokenStore", "process", "INFRA_KIT_ENV_TOKEN_VAR", "tokens", "logger", "buildResult", "line", "structuredContent", "textContent", "envListMcpTool", "defineMcpTool", "z", "path", "process", "z", "envStatus", "logger", "cacheDir", "getSessionCacheDir", "sessionId", "process", "INFRA_KIT_SESSION_VAR", "envLoadPath", "path", "ENV_LOAD_FILE", "sessionLoadedCount", "sessionTotalCount", "sessionConfig", "INFRA_KIT_ENV_CONFIG_VAR", "sessionProject", "INFRA_KIT_ENV_PROJECT_VAR", "sessionLoadedAt", "INFRA_KIT_ENV_LOADED_AT_VAR", "autoLoaded", "INFRA_KIT_ENV_AUTOLOADED_VAR", "cleared", "INFRA_KIT_ENV_CLEARED_VAR", "varNames", "parseVarNamesFromEnvFile", "v", "loadedAtDisplay", "origin", "missing", "clearedNote", "structuredContent", "textContent", "envStatusMcpTool", "defineMcpTool", "z", "process", "z", "MASK", "redactToken", "token", "buildEnvTokenRows", "environments", "storeTokens", "envToken", "env", "token", "INFRA_KIT_ENV_TOKEN_VAR", "redactToken", "BASE_COLUMNS", "formatEnvTokenTable", "rows", "checked", "header", "cells", "row", "widths", "column", "index", "cell", "envTokenList", "check", "envs", "listProjectEnvNames", "store", "readTokenStore", "process", "project", "getDopplerProject", "probe", "probeEnvToken", "buildDopplerChildEnv", "line", "logger", "missing", "structuredContent", "textContent", "envTokenListMcpTool", "defineMcpTool", "z", "z", "$", "$", "$", "$", "assertRepoWithOrigin", "args", "operation", "$", "error", "OperationError", "MAX_LISTED_PATHS", "assertCleanCheckout", "status", "getWorkingTreeStatus", "logger", "listed", "remaining", "summary", "assertBaseBranchSwitchable", "base", "getCurrentBranch", "holder", "listWorktrees", "getProjectRoot", "entry", "assertManagementContext", "isInsideLinkedWorktree", "$", "DEV_REF", "getBaseBranch", "type", "buildReleasePrBody", "jiraVersionUrl", "description", "prepareGitForRelease", "baseBranch", "git", "$", "error", "counts", "behind", "ahead", "diverged", "OperationError", "ensureJiraVersion", "args", "versionName", "jiraConfig", "existing", "findVersionByName", "createJiraVersion", "wanted", "updateJiraVersion", "createSingleRelease", "id", "baseSha", "formatJiraName", "jiraVersion", "buildJiraVersionUrl", "releaseInfo", "createReleaseBranch", "displayLabel", "getJiraDescriptions", "descriptions", "loadJiraConfigOptional", "versions", "getProjectVersions", "version", "logger", "formatVersionLabel", "maxVersionLength", "padding", "tag", "detectReleaseType", "title", "parseBranchChoices", "branches", "branch", "parseBranchName", "resolveReleaseBranch", "versionArg", "formatBranchName", "parseReleaseRef", "releaseLabelFromBranch", "releaseBranchLabels", "deriveBranchType", "types", "formatBranchPickerItems", "label", "sortReleasePRs", "prs", "pr", "parseBranchName", "entry", "a", "b", "compareReleaseIds", "PR_DISCOVERY_LIMIT", "warnIfTruncated", "source", "logger", "fetchAllReleasePRs", "releasePRs", "$", "hotfixPRs", "releaseList", "hotfixList", "all", "seen", "loadSortedReleasePRs", "OperationError", "error", "getReleasePRs", "getReleasePRsWithInfo", "updateReleasePRBody", "args", "branch", "body", "cleanupUnpushedBranch", "branchName", "baseBranch", "git", "refs", "probeFailed", "onRemote", "createReleaseBranch", "id", "jiraVersionUrl", "type", "description", "baseSha", "prTitle", "formatPrTitle", "getBaseBranch", "formatBranchName", "buildReleasePrBody", "assertCleanCheckout", "head", "prResult", "$", "fetchPRByHead", "head", "result", "process", "PromptCancelledError", "process", "jsonOutput", "emit", "result", "write", "text", "addJsonOption", "cmd", "option", "assertInteractive", "isMcpMode", "process", "jsonOutput", "OperationError", "pickReleaseBranch", "items", "runBranchPicker", "value", "PromptCancelledError", "pickReleaseBranches", "opts", "runBranchMultiPicker", "values", "$", "MERGE_MESSAGE_PREFIX", "stderrOf", "error", "conflictedPaths", "cwd", "$", "line", "classifyMergeFailure", "reason", "paths", "hasMergeInProgress", "planMergeRun", "args", "worktreePath", "branches", "entries", "devSha", "revParseVerify", "branch", "startedAt", "entry", "planOne", "remoteRef", "isAncestor", "assertPristineWorktree", "message", "mergeSha", "resetScratchWorktree", "pushableRefs", "DEFAULT_VERIFY_COMMAND", "verifyMerges", "refs", "command", "kept", "failed", "ref", "reclassify", "nowUpToDate", "process", "pending", "registerRunCleanup", "release", "releaseAll", "error", "logger", "GUARDED_SIGNALS", "withRunCleanup", "fn", "deps", "register", "signal", "handler", "process", "unregister", "exit", "code", "handlers", "exitCodeForSignal", "emptyResult", "dryRun", "toResponse", "structuredContent", "textContent", "SUCCESS_STATUSES", "resolveRequestedBranches", "versions", "available", "tokens", "token", "availableSet", "selected", "branch", "formatBranchName", "parseReleaseRef", "error", "OperationError", "PLAN_LABELS", "describeEntry", "entry", "label", "count", "renderPlan", "entries", "logger", "reproduceCommand", "worktrees", "held", "selectBranches", "args", "toPlan", "all", "explicit", "commandEcho", "descriptions", "id", "parseBranchName", "formatJiraName", "pickReleaseBranches", "formatBranchPickerItems", "applyPush", "cwd", "worktreePath", "verify", "chosen", "announceDropped", "branches", "first", "reclassify", "pushableRefs", "kept", "verifyFailed", "outcome", "verifyMerges", "reason", "second", "ref", "result", "pushAtomic", "landed", "lsRemoteHead", "ghMergeDev", "confirmedCommand", "verifyCommand", "DEFAULT_VERIFY_COMMAND", "assertRepoWithOrigin", "candidates", "getReleasePRsWithInfo", "pr", "detectReleaseType", "getMainRepoRoot", "$", "withScratchWorktree", "worktree", "unregister", "registerRunCleanup", "planConfirmAndPush", "pushed", "pushAborted", "abortedBy", "declined", "listWorktrees", "report", "planMergeRun", "idle", "willPush", "confirmOrExit", "isCommandDeclined", "pushedBranches", "results", "wasPushable", "verifyReason", "pushOutcome", "status", "failed", "planned", "candidate", "reportable", "ghMergeDevMcpTool", "defineMcpTool", "z", "z", "$", "readTrimmedString", "value", "max", "formatZxError", "error", "rec", "fields", "exitCode", "stderr", "stdout", "logRemovalResults", "result", "removed", "failed", "logger", "branch", "reason", "toRemovalToolResult", "args", "result", "operation", "structuredContent", "failure", "content", "textContent", "isMcpMode", "failures", "OperationError", "fs", "$", "SWEEPABLE_TOP_LEVEL", "SWEEPABLE_OMC_ENTRIES", "SWEEP_RETRY_DELAY_MS", "DIRTY_TREE_REMEDIATION", "defaultSleep", "ms", "resolve", "errnoCode", "error", "stripTrailingSlashes", "value", "end", "canonicalPath", "fs", "isWorktreeRegisteredOrUnknown", "args", "projectRoot", "worktreePath", "entries", "listWorktrees", "target", "entry", "classifyLeftover", "removeTree", "sweepLeftover", "sleep", "first", "failedOutcome", "branch", "remediation", "err", "OperationError", "logger", "recoverFromRejectedRemove", "leftover", "manualRemediation", "removeOne", "worktreeDir", "closeCmuxWorkspaceByCwd", "$", "removeWorktrees", "branches", "pruneFolder", "outcomes", "result", "outcome", "removeReleaseWorktreeIfPresent", "releaseBranch", "messages", "getCurrentWorktrees", "projectRoot", "getProjectRoot", "worktreeDir", "WORKTREES_DIR_SUFFIX", "removed", "failed", "removeWorktrees", "failure", "entry", "OperationError", "runStep", "operation", "remediation", "fn", "error", "logger", "formatZxError", "OperationError", "fetchMergedRcPRForVersion", "id", "expectedTitle", "formatRcTitle", "result", "$", "pr", "fetchOpenDevToMainPR", "resolveTargetFromVersion", "version", "selectedReleaseBranch", "resolveReleaseBranch", "fetchPRByHead", "resolveTargetInteractively", "releasePRsInfo", "getReleasePRsWithInfo", "branches", "releaseTypes", "detectReleaseType", "commandEcho", "descriptions", "getJiraDescriptions", "pickReleaseBranch", "formatBranchPickerItems", "prInfo", "mergeReleasePR", "args", "releaseType", "mergeTarget", "releasePr", "resolveRcPRNumber", "selectedLabel", "displayLabel", "existingOpen", "rcNumber", "created", "ensureRcPRMerged", "dispatchDeployWorkflow", "syncMainIntoDev", "removeDeliveredReleaseBranch", "branch", "deleteLocalBranch", "deleteRemoteBranch", "deliverJiraReleaseSafely", "jiraConfig", "loadJiraConfigOptional", "versionName", "formatJiraName", "deliverJiraRelease", "hint", "isJiraApiError", "ghReleaseDeliver", "confirmedCommand", "assertManagementContext", "releasePrTitle", "releaseId", "parseBranchName", "selectedVersion", "confirmOrExit", "deliverWorktreeDir", "getProjectRoot", "WORKTREES_DIR_SUFFIX", "removeReleaseWorktreeIfPresent", "structuredContent", "textContent", "ghReleaseDeliverMcpTool", "defineMcpTool", "z", "z", "$", "z", "fs", "path", "WORKFLOWS_DIR", "SCRIPT_NAME_PATTERN", "REUSABLE_WORKFLOW_PATTERN", "ENV_EQUALS_PATTERN", "readWorkflowGates", "projectRoot", "dir", "files", "collectFrom", "entry", "readGatesFromWorkflow", "workflowFile", "gates", "file", "source", "collectGates", "block", "service", "envs", "match", "intersectGates", "scriptEnvs", "workflowEnvs", "env", "fs", "path", "yaml", "WORKFLOWS_DIR", "SKIP_TERRAFORM_INPUT", "parseServicesFromWorkflow", "projectRoot", "workflowFile", "workflowPath", "parsed", "inputs", "key", "value", "DEV_VERSION", "isRecord", "value", "isAbsent", "params", "field", "roundOneString", "orEmpty", "work", "releaseLabels", "prs", "getReleasePRsWithInfo", "releaseBranchLabels", "pr", "gateMapProse", "services", "gates", "gated", "service", "marks", "createDeployFormProvider", "args", "workflowFile", "fields", "toolName", "noOptions", "list", "logger", "versionField", "labels", "current", "z", "envField", "envs", "deployableEnvs", "readWorkflowEnvOptions", "resolveProtectedEnvAccess", "shared", "isSharedEnv", "personal", "env", "servicesField", "projectRoot", "getProjectRoot", "parseServicesFromWorkflow", "readGatesFromWorkflow", "round1", "shape", "content", "merged", "select", "pickEnv", "options", "operation", "OperationError", "withEscape", "context", "select", "option", "confirm", "confirmDeploy", "args", "confirmedCommand", "branch", "env", "commandEcho", "answer", "withEscape", "context", "confirm", "select", "process", "canPromptForDeploySource", "isMcpMode", "process", "jsonOutput", "SOURCE_DESCRIPTIONS", "pickDeploySource", "withEscape", "context", "select", "DEPLOY_SOURCES", "source", "DEPLOY_SOURCES", "SOURCE_ONLY_FLAGS", "parseDeploySource", "from", "OperationError", "match", "source", "assertFlagsMatchSource", "present", "offenders", "flag", "value", "resolveDeploySource", "canPromptForDeploySource", "pickDeploySource", "resolveDeployBranch", "version", "resolveReleaseBranch", "commandEcho", "releasePRsInfo", "getReleasePRsWithInfo", "branches", "pr", "releaseTypes", "detectReleaseType", "descriptions", "getJiraDescriptions", "pickReleaseBranch", "formatBranchPickerItems", "DEPLOY_ALL_WORKFLOW", "ghReleaseDeployAll", "args", "version", "env", "skipTerraform", "confirmedCommand", "selectedReleaseBranch", "resolveDeployBranch", "selectedVersion", "releaseLabelFromBranch", "commandEcho", "protectedEnvAccess", "resolveProtectedEnvAccess", "envOptions", "deployableEnvs", "readWorkflowEnvOptions", "selectedEnv", "pickEnv", "assertDeployable", "warnProtectedEnvDispatch", "shouldSkipTerraform", "confirmDeploy", "logger", "structuredContent", "textContent", "$", "error", "OperationError", "ghReleaseDeployAllMcpTool", "defineMcpTool", "createDeployFormProvider", "z", "checkbox", "z", "$", "DEPLOY_SELECTED_WORKFLOW", "ghReleaseDeploySelected", "args", "version", "env", "services", "skipTerraform", "confirmedCommand", "selectedReleaseBranch", "resolveDeployBranch", "selectedVersion", "releaseLabelFromBranch", "commandEcho", "protectedEnvAccess", "resolveProtectedEnvAccess", "envOptions", "deployableEnvs", "readWorkflowEnvOptions", "selectedEnv", "pickEnv", "assertDeployable", "warnProtectedEnvDispatch", "projectRoot", "getProjectRoot", "availableServices", "parseServicesFromWorkflow", "OperationError", "selectedServices", "withEscape", "context", "checkbox", "svc", "invalidServices", "gates", "readGatesFromWorkflow", "gatedOut", "allowed", "shouldSkipTerraform", "buildResult", "success", "structuredContent", "textContent", "confirmDeploy", "logger", "$", "serviceFlags", "error", "ghReleaseDeploySelectedMcpTool", "defineMcpTool", "createDeployFormProvider", "z", "z", "ghReleaseList", "releases", "getReleasePRsWithInfo", "pr", "id", "parseBranchName", "displayLabel", "formatJiraName", "detectReleaseType", "jiraDescriptions", "getJiraDescriptions", "maxVersionLength", "r", "formattedLines", "release", "label", "formatVersionLabel", "description", "logger", "structuredContent", "textContent", "ghReleaseListMcpTool", "defineMcpTool", "z", "process", "contractRecord", "contract", "buildDeployEnv", "args", "env", "branch", "sha", "isClean", "ambient", "childEnv", "stripped", "key", "value", "formatContract", "result", "lines", "checkbox", "confirm", "select", "spawn", "process", "z", "$", "$", "resolveAccountIdentity", "project", "previousQuiet", "$", "parameterName", "identity", "stage", "error", "OperationError", "assertEnvMatchesAccount", "intended", "assertCleanTreeForSharedEnv", "args", "env", "isShared", "isClean", "assertNoCiDeployInFlight", "runningEnvs", "runPreflight", "protectedEnvAccess", "assertDeployable", "fs", "path", "SCRIPTS_DIR", "SCRIPT_PATTERN", "GUARD_PATTERN", "discoverServices", "projectRoot", "scriptsDir", "path", "entries", "fs", "gates", "readWorkflowGates", "entry", "candidate", "match", "scriptPath", "name", "intersectGates", "readAllowedEnvs", "left", "right", "source", "envs", "line", "found", "parsed", "isEligible", "service", "env", "eligibleServices", "services", "DEPLOY_ALL_WORKFLOW", "SHELL", "runningCiEnvs", "previousQuiet", "$", "result", "run", "pickServices", "services", "env", "choices", "service", "selected", "withEscape", "context", "checkbox", "OperationError", "confirmTarget", "args", "isShared", "count", "select", "confirm", "runDeployScript", "childEnv", "cwd", "resolve", "reject", "child", "spawn", "code", "executeDeploys", "chosen", "deployed", "failed", "entry", "logger", "assertServicesUsable", "names", "known", "unknown", "name", "forbidden", "isEligible", "buildResult", "accountId", "built", "dryRun", "success", "structuredContent", "contractRecord", "textContent", "resolveNames", "selection", "eligible", "commandEcho", "runLocalDeploy", "confirmedCommand", "printEnv", "projectRoot", "getProjectRoot", "project", "getRepoName", "discoverServices", "protectedEnvAccess", "resolveProtectedEnvAccess", "envOptions", "deployableEnvs", "readWorkflowEnvOptions", "selectedEnv", "pickEnv", "eligibleServices", "isSharedEnv", "branch", "getCurrentBranch", "sha", "isClean", "isWorkingTreeClean", "buildDeployEnv", "formatContract", "runPreflight", "process", "localDeployAll", "localDeploySelected", "SHARED_TOOL_NOTE", "sharedInput", "z", "sharedOutput", "localDeployForm", "toolName", "createDeployFormProvider", "localDeployAllMcpTool", "defineMcpTool", "localDeploySelectedMcpTool", "confirm", "input", "select", "process", "z", "VERSION_PROMPT_HINT", "trySuggestNext", "known", "type", "computeNextVersion", "err", "NoPriorVersionsError", "resolveOrExit", "entries", "resolveReleaseEntries", "OperationError", "InvalidReleaseNameError", "promptForVersionInput", "running", "suggestion", "defaultHint", "versionAnswer", "withEscape", "context", "input", "versionInput", "logger", "process", "promptForNameInput", "name", "validateName", "reason", "promptForReleasesInteractive", "ensureKnown", "commandEcho", "baseKnown", "ensureRunning", "addAnother", "ordinal", "kind", "select", "resolved", "parseVersion", "description", "confirm", "formatReleaseSummary", "entry", "parts", "echoReleases", "formatReleaseSpec", "collectEntries", "inputReleases", "hasNextToken", "interactive", "assertHomogeneousReleaseType", "confirmReleases", "confirmedCommand", "summary", "confirmOrExit", "executeOne", "args", "jiraConfig", "label", "displayLabel", "prTitleLabel", "assertCleanCheckout", "baseSha", "prepareGitForRelease", "result", "createSingleRelease", "error", "jiraFailure", "isJiraApiError", "logFinalSummary", "total", "successCount", "failureCount", "releaseCreate", "assertManagementContext", "loadJiraConfig", "loadExistingVersions", "assertBaseBranchSwitchable", "getBaseBranch", "created", "failed", "failure", "structuredContent", "r", "textContent", "releaseCreateMcpTool", "defineMcpTool", "z", "input", "z", "pickReleaseBranch", "releasePRsInfo", "getReleasePRsWithInfo", "branches", "pr", "types", "detectReleaseType", "descriptions", "getJiraDescriptions", "branch", "formatBranchPickerItems", "verifyReleasePRExists", "selectedBranch", "prInfo", "OperationError", "promptDescription", "current", "hint", "trimmed", "withEscape", "context", "input", "releaseDescEdit", "args", "versionArg", "descriptionArg", "confirmedCommand", "jiraConfig", "loadJiraConfig", "resolveReleaseBranch", "commandEcho", "releaseId", "parseBranchName", "selectedVersion", "displayLabel", "versionName", "formatJiraName", "jiraVersion", "findVersionByName", "previousDescription", "newDescription", "buildResult", "changed", "structuredContent", "buildJiraVersionUrl", "textContent", "logger", "confirmOrExit", "updateJiraVersion", "jiraVersionUrl", "body", "buildReleasePrBody", "updateReleasePRBody", "releaseDescEditMcpTool", "defineMcpTool", "z", "z", "$", "fs", "path", "addFoldersToCursorWorkspace", "args", "workspacePath", "folderPaths", "workspaceDir", "raw", "error", "parsed", "existingFolders", "existingAbsolutePaths", "entry", "added", "skipped", "folderPath", "absolutePath", "relativePath", "$", "launchCursor", "workspacePath", "fs", "path", "fs", "path", "removeFoldersFromCursorWorkspace", "args", "workspacePath", "folderPaths", "workspaceDir", "raw", "error", "parsed", "existingFolders", "targetAbsolutePaths", "folderPath", "removedAbsolutePaths", "filteredFolders", "entry", "entryAbsolutePath", "removed", "notFound", "absolutePath", "reconcileCursorWorkspaceFolders", "args", "workspacePath", "worktreeDir", "currentBranches", "workspaceDir", "path", "releaseRoot", "raw", "fs", "existingFolders", "desiredAbsolutePaths", "branch", "danglingFolderPaths", "entry", "entryAbsolutePath", "removed", "removeFoldersFromCursorWorkspace", "desiredFolderPaths", "added", "addFoldersToCursorWorkspace", "path", "resolveCursorWorkspacePath", "configValue", "projectRoot", "openCursorWorkspace", "args", "projectRoot", "worktreeDir", "currentBranches", "cursorConfig", "logger", "workspacePath", "resolveCursorWorkspacePath", "added", "removed", "reconcileCursorWorkspaceFolders", "launchCursor", "error", "$", "addFoldersToZedWorkspace", "args", "folderPaths", "folderPath", "$", "openZedWorkspace", "args", "worktreePaths", "$", "error", "logger", "$", "reuseZedWorkspace", "args", "projectRoot", "worktreeDir", "remainingBranches", "paths", "branch", "$", "logger", "error", "assertNever", "value", "addIdeWorktreeFolders", "args", "projectRoot", "worktreeDir", "branches", "config", "getInfraKitConfig", "ides", "resolveConfiguredIdes", "folderPaths", "branch", "outcomes", "ide", "logger", "workspacePath", "resolveCursorWorkspacePath", "added", "skipped", "addFoldersToCursorWorkspace", "skippedSuffix", "launchCursor", "addFoldersToZedWorkspace", "assertNever", "openIdeWorkspace", "args", "config", "getInfraKitConfig", "ides", "resolveConfiguredIdes", "projectRoot", "worktreeDir", "worktreePaths", "currentBranches", "outcomes", "ide", "outcome", "openCursorWorkspace", "openZedWorkspace", "assertNever", "ideProviderLabel", "provider", "assertNever", "removeIdeWorktreeFolders", "args", "projectRoot", "worktreeDir", "currentWorktrees", "removedWorktrees", "allowEditorRelaunch", "config", "getInfraKitConfig", "ides", "resolveConfiguredIdes", "folderPaths", "branch", "remainingBranches", "outcomes", "ide", "removeFromCursor", "removeFromZed", "assertNever", "workspacePath", "resolveCursorWorkspacePath", "removed", "removeFoldersFromCursorWorkspace", "logger", "error", "reuseZedWorkspace", "IDE_MODES", "STEP_ORDER", "STEP_LABELS", "OPERATION", "refuse", "context", "logger", "OperationError", "assertMcpRemoveInput", "args", "isMcpMode", "assertNotMerged", "plan", "assertJiraRemovable", "version", "fixCount", "affectsCount", "assertSomethingExists", "assertWorktreeClean", "probeWorktreeDirty", "worktreePath", "$", "line", "probeCmuxWorkspace", "listCmuxWorkspacesByCwd", "safeRealpath", "buildJiraPreflight", "id", "empty", "jiraConfig", "loadJiraConfigOptional", "findVersionByName", "formatJiraName", "moveIssuesTo", "resolveMoveTarget", "counts", "getVersionRelatedIssueCounts", "buildJiraVersionUrl", "name", "config", "target", "buildPlan", "branch", "parseBranchName", "projectRoot", "getProjectRoot", "worktreeDir", "WORKTREES_DIR_SUFFIX", "currentWorktrees", "pr", "localTipSha", "remoteTipSha", "getCurrentWorktrees", "fetchPRByHead", "revParseVerify", "lsRemoteHead", "worktreePresent", "displayLabel", "getBaseBranch", "detectReleaseType", "describePlan", "describeJiraLine", "skipJira", "attached", "move", "buildConfirmMessage", "prLine", "lines", "StepFailure", "step", "reason", "outcomeOf", "result", "runStep", "done", "fn", "outcome", "error", "tryStep", "formatZxError", "describeDoneStep", "entry", "buildResidueError", "cause", "completed", "jiraNote", "restore", "residue", "refusal", "removeWorktreeStep", "removed", "removeReleaseWorktreeIfPresent", "summariseIdeOutcomes", "outcomes", "providers", "describeIdeProvider", "verified", "runIdeFoldersStep", "removedWorktrees", "report", "removeIdeWorktreeFolders", "closePrStep", "current", "comment", "deleteLocalBranchStep", "getCurrentBranch", "deleteLocalBranch", "branchExists", "deleteRemoteBranchStep", "deleteRemoteBranch", "assertIssueCountsUnchanged", "jira", "removeJiraStep", "removeJiraVersion", "buildResult", "results", "runSteps", "worktree", "ideFolders", "localBranch", "remoteBranch", "executeRemoval", "resolveTargetBranch", "resolveReleaseBranch", "commandEcho", "descriptions", "prInfo", "getJiraDescriptions", "getReleasePRsWithInfo", "branches", "types", "pickReleaseBranch", "formatBranchPickerItems", "isPromptCancellation", "releaseRemove", "options", "confirmedCommand", "assertManagementContext", "getInfraKitConfig", "assertBaseBranchSwitchable", "confirmOrExit", "structuredContent", "textContent", "isCommandDeclined", "releaseRemoveMcpTool", "defineMcpTool", "z", "execFile", "path", "process", "z", "fs", "path", "pathExists", "target", "fs", "resolveRoots", "roots", "mainRepoRoot", "getMainRepoRoot", "path", "error", "OperationError", "isScanCandidate", "child", "resolveCandidate", "root", "candidate", "hasGit", "hasConfig", "repoRoot", "readRootChildren", "discoverInfraKitProjects", "options", "byRepoRoot", "children", "project", "a", "b", "reopen", "options", "reopenAll", "reopenCurrentProject", "releaseOnly", "force", "dryRun", "getInfraKitConfig", "projectRoot", "getProjectRoot", "worktreeDir", "WORKTREES_DIR_SUFFIX", "repoName", "path", "getMainRepoRoot", "entries", "listWorktrees", "active", "selectActive", "worktreePaths", "entry", "currentBranches", "branch", "isReleaseBranch", "targets", "buildCmuxWorkspaceTitle", "result", "planReopen", "logDryRun", "commandEcho", "textContent", "ideOutcomes", "cmux", "openIdeWorkspace", "runCmux", "outcome", "logResults", "error", "OperationError", "logger", "args", "groupRef", "findCmuxGroupRefByName", "closed", "closeCmux", "opened", "skipped", "reopenCmux", "openByCwd", "listCmuxWorkspacesByCwd", "target", "safeRealpath", "closeCmuxWorkspaceByCwd", "partitionCmuxTargets", "openCwds", "toOpen", "group", "bootstrapAttempted", "openCmuxWorkspaceWithLayout", "workspaceRef", "createCmuxGroupFrom", "repo", "folder", "title", "context", "ideLabels", "provider", "ideProviderLabel", "project", "root", "discovered", "discoverInfraKitProjects", "projects", "filterProjects", "childFlags", "buildChildFlags", "results", "proj", "runRepoChild", "logReopenAll", "names", "discoveredNames", "name", "flags", "stdout", "spawnReopenChild", "message", "cwd", "selfCli", "process", "resolve", "reject", "execFile", "okCount", "failedCount", "totalOpened", "totalSkipped", "reopenMcpTool", "defineMcpTool", "z", "spawnSync", "homedir", "process", "managerDriving", "recipe", "commands", "step", "staticRefusals", "reasons", "detectionRefusals", "context", "manager", "assessRecipe", "formatRecipe", "BREW_ENV_SCRUB", "scrubbed", "env", "copy", "packageManagerInstallEnv", "key", "runRecipe", "recipe", "context", "deps", "spawn", "spawnSync", "process", "inMcpMode", "isMcpMode", "commands", "formatRecipe", "verdict", "assessRecipe", "execute", "childEnv", "index", "step", "result", "homedir", "failure", "describeFailure", "presentManagers", "states", "present", "brewPresent", "state", "awsIsScriptInstalled", "remediationFor", "spec", "specFor", "planFor", "context", "recipe", "toolContext", "verdict", "assessRecipe", "formatRecipe", "planDependencies", "ids", "DEPENDENCY_IDS", "deps", "defaultProbeDeps", "probeAll", "ordered", "installOrder", "byId", "id", "skipped", "plan", "detail", "describeBefore", "preflight", "mode", "toResult", "outcome", "convergeDependencies", "options", "run", "runRecipe", "plans", "planDependencies", "tools", "stop", "probeDependencies", "ids", "probeDeps", "process", "z", "SKIP_TOOLS_CONFLICT", "resolveRequest", "options", "updateIds", "updating", "skipTools", "DEPENDENCY_IDS", "failedStep", "err", "completed", "InitStepError", "runInitHalf", "entries", "sink", "entry", "SHELL_ACTIVATION_REMINDER", "logInitEntry", "initCore", "printSummary", "tools", "tool", "logger", "needsManualRun", "command", "toInitStep", "setup", "request", "init", "probeDependencies", "convergeDependencies", "structuredContent", "process", "textContent", "initStepSchema", "z", "resultSchema", "outputSchema", "inputSchema", "REQUIRES_USER_INTERACTION", "setupMcpTool", "defineMcpTool", "params", "existsSync", "path", "process", "z", "createHash", "readFileSync", "sha256", "filePath", "readFileSync", "writeFileSync", "path", "z", "VENDOR_DIR", "MANIFEST_SKIP_DIRS", "MANIFEST_SKIP_FILES", "MANIFEST_SKIP_SUFFIXES", "isSkippedDir", "name", "isSkippedFile", "suffix", "isSkippedPath", "relativePath", "segments", "segment", "readdirSync", "path", "toPosix", "relativePath", "path", "walkVendorTree", "vendorRoot", "acc", "walk", "dir", "entry", "readdirSync", "full", "isSkippedDir", "isSkippedFile", "MANIFEST_FILE", "CURRENT_SCHEMA_VERSION", "manifestSchema", "z", "isUnknownSchema", "manifest", "readManifest", "vendorRoot", "manifestPath", "path", "MANIFEST_FILE", "raw", "readFileSync", "parsed", "manifestSchema", "z", "compareToManifest", "vendorRoot", "manifest", "expected", "rel", "isSkippedPath", "actualFiles", "walkVendorTree", "modified", "added", "sha256", "path", "actualSet", "removed", "MAX_REPORTED", "report", "label", "list", "logger", "rel", "vendorCheck", "options", "root", "getProjectRoot", "vendorRoot", "path", "VENDOR_DIR", "base", "existsSync", "structuredContent", "textContent", "MANIFEST_FILE", "manifest", "readManifest", "isUnknownSchema", "modified", "added", "removed", "compareToManifest", "drifted", "vendorCheckOutputSchema", "z", "vendorCheckMcpTool", "defineMcpTool", "process", "z", "version", "cliVersion", "package_default", "logger", "structuredContent", "textContent", "versionMcpTool", "defineMcpTool", "z", "confirm", "path", "z", "$", "FEATURE_DIR", "RELEASE_DIR", "worktreesAdd", "options", "confirmedCommand", "all", "versions", "githubDesktop", "cmux", "ide", "assertManagementContext", "currentWorktrees", "getCurrentWorktrees", "projectRoot", "getProjectRoot", "worktreeDir", "WORKTREES_DIR_SUFFIX", "ensureWorktreeDirectory", "selectedReleaseBranches", "v", "formatBranchName", "parseReleaseRef", "releasePRsInfo", "getReleasePRsWithInfo", "releasePRsList", "pr", "logger", "commandEcho", "textContent", "releaseTypes", "detectReleaseType", "descriptions", "getJiraDescriptions", "pickReleaseBranches", "formatBranchPickerItems", "releaseBranchLabels", "confirmOrExit", "config", "getInfraKitConfig", "ideMode", "resolveConfiguredIdes", "openInGithubDesktop", "withEscape", "context", "confirm", "openInCmux", "branchesToCreate", "categorizeWorktrees", "createdWorktrees", "createWorktrees", "logResults", "addIdeWorktreeFolders", "branch", "$", "repoName", "path", "getMainRepoRoot", "openByCwd", "listCmuxWorkspacesByCwd", "groupRef", "findCmuxGroupRefByName", "bootstrapAttempted", "cwd", "safeRealpath", "title", "buildCmuxWorkspaceTitle", "openCmuxWorkspaceWithLayout", "workspaceRef", "createCmuxGroupFrom", "error", "structuredContent", "isPromptCancellation", "OperationError", "args", "currentBranchNames", "isReleaseBranch", "branches", "results", "worktreePath", "created", "index", "result", "err", "worktreesAddMcpTool", "defineMcpTool", "z", "IDE_MODES", "z", "worktreesList", "getInfraKitConfig", "currentWorktrees", "getCurrentWorktrees", "logger", "textContent", "releasePRsInfo", "jiraDescriptions", "getReleasePRsWithInfo", "getJiraDescriptions", "releaseTypes", "pr", "detectReleaseType", "worktrees", "branch", "id", "parseBranchName", "version", "displayLabel", "type", "description", "formatJiraName", "maxVersionLength", "w", "formattedLines", "worktree", "label", "formatVersionLabel", "structuredContent", "worktreesListMcpTool", "defineMcpTool", "z", "z", "assertMcpRemovalInput", "input", "isMcpMode", "OperationError", "assertTargetsExist", "selected", "currentWorktrees", "unmatched", "branch", "worktreesRemove", "options", "confirmedCommand", "all", "versions", "assertManagementContext", "getInfraKitConfig", "getCurrentWorktrees", "logger", "commandEcho", "empty", "textContent", "projectRoot", "getProjectRoot", "worktreeDir", "WORKTREES_DIR_SUFFIX", "selectedReleaseBranches", "v", "formatBranchName", "parseReleaseRef", "descriptions", "prInfo", "getJiraDescriptions", "getReleasePRsWithInfo", "releaseTypes", "pr", "detectReleaseType", "pickReleaseBranches", "formatBranchPickerItems", "allSelected", "releaseBranchLabels", "confirmOrExit", "removal", "removeWorktrees", "removeIdeWorktreeFolders", "logRemovalResults", "toRemovalToolResult", "error", "isPromptCancellation", "worktreesRemoveMcpTool", "defineMcpTool", "z", "z", "worktreesSync", "options", "confirmedCommand", "assertManagementContext", "getInfraKitConfig", "currentWorktrees", "getCurrentWorktrees", "projectRoot", "getProjectRoot", "worktreeDir", "WORKTREES_DIR_SUFFIX", "releasePRsList", "getReleasePRs", "confirmOrExit", "commandEcho", "branchesToRemove", "categorizeWorktrees", "removal", "removeWorktrees", "removeIdeWorktreeFolders", "logRemovalResults", "toRemovalToolResult", "error", "isPromptCancellation", "OperationError", "logger", "args", "branch", "isReleaseBranch", "worktreesSyncMcpTool", "defineMcpTool", "z", "MENU_GROUPS", "commandCatalog", "devStatusMcpTool", "ghMergeDevMcpTool", "ghReleaseListMcpTool", "releaseCreateMcpTool", "releaseDescEditMcpTool", "ghReleaseDeployAllMcpTool", "ghReleaseDeploySelectedMcpTool", "ghReleaseDeliverMcpTool", "releaseRemoveMcpTool", "localDeployAllMcpTool", "localDeploySelectedMcpTool", "worktreesAddMcpTool", "worktreesListMcpTool", "reopenMcpTool", "worktreesRemoveMcpTool", "worktreesSyncMcpTool", "envStatusMcpTool", "envListMcpTool", "envLoadMcpTool", "envClearMcpTool", "envTokenListMcpTool", "configGetMcpTool", "vendorCheckMcpTool", "doctorMcpTool", "auditMcpTool", "versionMcpTool", "setupMcpTool", "MCP_TOOL_PRESENTATION", "NOT_READ_ONLY", "getExposedMcpTools", "commandCatalog", "entry", "presentation", "readOnlyHint", "getMenuGroupEntries", "group", "isLongRunningCommand", "commandPath", "process", "TYPELESS_PACKAGE_JSON", "readWarningCode", "typeOrOptions", "codeOrCtor", "installed", "suppressTypelessPackageJsonWarning", "emitWarning", "warning", "rest"]
|
|
7
|
+
}
|