infra-kit 0.4.0 → 0.5.1
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-6GWRKIZ7.js +2 -0
- package/dist/chunk-6GWRKIZ7.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-HXBIE4JP.js +7 -0
- package/dist/{chunk-D7QNBU5A.js.map → chunk-HXBIE4JP.js.map} +3 -3
- package/dist/{chunk-PVMF4FH7.js → chunk-Q23IJP6D.js} +2 -2
- package/dist/chunk-Q4HKBLQG.js +7 -0
- package/dist/chunk-Q4HKBLQG.js.map +7 -0
- package/dist/chunk-X2POR5VH.js +422 -0
- package/dist/chunk-X2POR5VH.js.map +7 -0
- package/dist/{chunk-7POMR4QK.js → chunk-XJOY7J3G.js} +1 -1
- package/dist/chunk-XJOY7J3G.js.map +7 -0
- package/dist/chunk-Y3FV2BRI.js +2 -0
- package/dist/chunk-Y3FV2BRI.js.map +7 -0
- package/dist/cli.js +12 -12
- 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-7POMR4QK.js.map +0 -7
- 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-Q23IJP6D.js.map} +0 -0
- /package/dist/{dev-wizard-run-YMH3L7GE.js.map → dev-wizard-run-FNMMYTDX.js.map} +0 -0
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var D={name:"infra-kit",type:"module",version:"0.5.1",files:["dist"],description:"infra-kit",bin:{"infra-kit":"dist/cli.js",ik:"dist/cli.js"},engines:{node:">=24.x"},scripts:{inspector:"npx @modelcontextprotocol/inspector node ./dist/mcp.js --debug",build:"pnpm run clean-artifacts && node ./scripts/build.js",prepack:"pnpm run build",prepublishOnly:"pnpm run build","infra-kit-check":"pnpm exec infra-kit audit","clean-artifacts":"rm -rf dist","clean-cache":"rm -rf node_modules/.cache .eslintcache tsconfig.tsbuildinfo .turbo .swc","prettier-fix":"pnpm exec prettier **/* --write --no-error-on-unmatched-pattern --log-level silent --ignore-path ../../../.prettierignore","prettier-check":"pnpm exec prettier **/* --check --no-error-on-unmatched-pattern --log-level silent --ignore-path ../../../.prettierignore","eslint-check":"pnpm exec eslint --cache --report-unused-disable-directives ./src","eslint-fix":"pnpm exec eslint --cache --quiet --report-unused-disable-directives ./src --fix","ts-check":"tsc --noEmit",test:"pnpm exec vitest run --reporter=minimal","test-watch":"pnpm exec vitest --watch --silent passed-only","test-ui":"pnpm exec vitest --ui --silent passed-only","test-report":"pnpm exec vitest run --coverage --silent passed-only","qa:pty":"INFRA_KIT_REQUIRE_PTY=1 pnpm exec vitest run --reporter=minimal src/tui/__tests__/stdin-pause-pty.test.ts src/entry/__tests__/quit-keys-pty.test.ts src/entry/__tests__/post-run-pause-pty.test.ts",qa:"pnpm run prettier-check && pnpm run eslint-check && pnpm run ts-check && pnpm run test && echo \u2705 Success",fix:"pnpm run prettier-fix && pnpm run eslint-fix && pnpm run qa"},dependencies:{"@aws-lambda-powertools/logger":"^2.35.0","@inquirer/checkbox":"^5.2.5","@inquirer/confirm":"^6.3.2","@inquirer/input":"^5.1.6","@inquirer/password":"^5.2.2","@inquirer/select":"^5.2.5","@modelcontextprotocol/server":"^2.0.0","@slip-stream-kit/config":"^0.4.0",chalk:"^6.0.0",chokidar:"^5.0.0",commander:"^15.0.0",fastify:"^5.12.3",ink:"^7.1.1",pino:"^10.3.1","pino-pretty":"^13.1.3",portless:"^0.15.6",react:"19.2.8",yaml:"^2.9.0",zod:"^4.5.4",zx:"^8.8.5"},devDependencies:{"@modelcontextprotocol/client":"^2.0.0","@modelcontextprotocol/sdk":"^1.30.0","@slip-stream-kit/eslint-plugin":"workspace:*","@types/aws-lambda":"catalog:","@types/react":"catalog:","@wl/eslint-config":"workspace:*","@wl/vitest-config":"workspace:*",esbuild:"^0.28.2","ink-testing-library":"^4.0.0",typescript:"^6.0.3"}};import a from"node:path";var i="infra-kit",o=`${i}@latest`,m=(e,t,n)=>{let r=a.relative(n(a.resolve(e)),a.resolve(t));return r!==""&&!r.startsWith("..")&&!a.isAbsolute(r)},s=(e,t)=>a.resolve(e).split(a.sep).includes(t),b=(e,t)=>{let n=a.resolve(e).split(a.sep),r=n.indexOf("Cellar");return r!==-1&&n[r+1]===t},M=(e,t)=>{let n=a.resolve(e).split(a.sep),r=n.lastIndexOf("node_modules");return r<2||n[r-1]!=="lib"||n[r+1]!==t?null:n.slice(0,r-1).join(a.sep)||a.sep},w=e=>M(e,i),I=e=>["npm","install","-g","--prefix",e,o],v=e=>e.map(t=>t.includes(" ")?`'${t}'`:t).join(" "),c=(e,t,n,r)=>{let p=e[t];return p!=null&&p!==""&&m(p,n,r)},_=[{manager:"volta",test:({selfRealPath:e,env:t,realpath:n})=>c(t,"VOLTA_HOME",e,n)||s(e,".volta")||s(e,"volta"),updateCommand:["volta","install",i],canSelfSpawn:!0},{manager:"homebrew",test:({selfRealPath:e})=>b(e,i),updateCommand:["brew","upgrade",i],canSelfSpawn:!1}],k=[{manager:"pnpm",test:({selfRealPath:e,env:t,realpath:n})=>c(t,"PNPM_HOME",e,n)||s(e,"pnpm")&&s(e,"global"),updateCommand:["pnpm","add","-g",o],canSelfSpawn:!0},{manager:"bun",test:({selfRealPath:e,env:t,realpath:n})=>c(t,"BUN_INSTALL",e,n)||s(e,".bun"),updateCommand:["bun","add","-g",o],canSelfSpawn:!0},{manager:"yarn",test:({selfRealPath:e})=>s(e,".yarn")||s(e,"yarn")&&s(e,"global"),updateCommand:["yarn","global","add",o],canSelfSpawn:!0},{manager:"npm",test:({selfRealPath:e,env:t,realpath:n})=>c(t,"npm_config_prefix",e,n),updateCommand:["npm","install","-g",o],canSelfSpawn:!0}],S=["npm","install","-g",o],C=e=>{let{selfRealPath:t,env:n,realpath:r,lazyNpmRoot:p}=e,d=l=>({manager:l.manager,updateCommand:l.updateCommand,canSelfSpawn:l.canSelfSpawn}),g=({test:l})=>l({selfRealPath:t,env:n,realpath:r}),f=_.find(g);if(f)return d(f);let x=w(t);if(x!==null)return{manager:"npm",updateCommand:I(x),canSelfSpawn:!0};let h=k.find(g);if(h)return d(h);let u=p?.();return u!=null&&u!==""&&m(u,t,r)?{manager:"npm",updateCommand:S,canSelfSpawn:!0}:{manager:"unknown",updateCommand:S,canSelfSpawn:!1}},E=(e,t,n)=>s(e,"node_modules")&&m(t,e,n);import{realpathSync as R}from"node:fs";import N from"node:path";var A=e=>{try{return R(e)}catch{return N.resolve(e)}};import{execFileSync as P}from"node:child_process";import O from"node:process";var T=3e3,q=()=>{try{let t=P("npm",["root","-g"],{encoding:"utf-8",timeout:T,stdio:["ignore","pipe","ignore"],shell:O.platform==="win32"}).trim();return t===""?void 0:t}catch{return}};export{D as a,i as b,s as c,b as d,M as e,v as f,C as g,E as h,q as i,A as j};
|
|
2
|
+
//# sourceMappingURL=chunk-6GWRKIZ7.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../package.json", "../src/lib/install-manager/install-manager.ts", "../src/lib/install-manager/safe-realpath.ts", "../src/lib/install-manager/npm-root.ts"],
|
|
4
|
+
"sourcesContent": ["{\n \"name\": \"infra-kit\",\n \"type\": \"module\",\n \"version\": \"0.5.1\",\n \"files\": [\n \"dist\"\n ],\n \"description\": \"infra-kit\",\n \"bin\": {\n \"infra-kit\": \"dist/cli.js\",\n \"ik\": \"dist/cli.js\"\n },\n \"engines\": {\n \"node\": \">=24.x\"\n },\n \"scripts\": {\n \"inspector\": \"npx @modelcontextprotocol/inspector node ./dist/mcp.js --debug\",\n \"build\": \"pnpm run clean-artifacts && node ./scripts/build.js\",\n \"prepack\": \"pnpm run build\",\n \"prepublishOnly\": \"pnpm run build\",\n \"infra-kit-check\": \"pnpm exec infra-kit audit\",\n \"clean-artifacts\": \"rm -rf dist\",\n \"clean-cache\": \"rm -rf node_modules/.cache .eslintcache tsconfig.tsbuildinfo .turbo .swc\",\n \"prettier-fix\": \"pnpm exec prettier **/* --write --no-error-on-unmatched-pattern --log-level silent --ignore-path ../../../.prettierignore\",\n \"prettier-check\": \"pnpm exec prettier **/* --check --no-error-on-unmatched-pattern --log-level silent --ignore-path ../../../.prettierignore\",\n \"eslint-check\": \"pnpm exec eslint --cache --report-unused-disable-directives ./src\",\n \"eslint-fix\": \"pnpm exec eslint --cache --quiet --report-unused-disable-directives ./src --fix\",\n \"ts-check\": \"tsc --noEmit\",\n \"test\": \"pnpm exec vitest run --reporter=minimal\",\n \"test-watch\": \"pnpm exec vitest --watch --silent passed-only\",\n \"test-ui\": \"pnpm exec vitest --ui --silent passed-only\",\n \"test-report\": \"pnpm exec vitest run --coverage --silent passed-only\",\n \"qa:pty\": \"INFRA_KIT_REQUIRE_PTY=1 pnpm exec vitest run --reporter=minimal src/tui/__tests__/stdin-pause-pty.test.ts src/entry/__tests__/quit-keys-pty.test.ts src/entry/__tests__/post-run-pause-pty.test.ts\",\n \"qa\": \"pnpm run prettier-check && pnpm run eslint-check && pnpm run ts-check && pnpm run test && echo \u2705 Success\",\n \"fix\": \"pnpm run prettier-fix && pnpm run eslint-fix && pnpm run qa\"\n },\n \"dependencies\": {\n \"@aws-lambda-powertools/logger\": \"^2.35.0\",\n \"@inquirer/checkbox\": \"^5.2.5\",\n \"@inquirer/confirm\": \"^6.3.2\",\n \"@inquirer/input\": \"^5.1.6\",\n \"@inquirer/password\": \"^5.2.2\",\n \"@inquirer/select\": \"^5.2.5\",\n \"@modelcontextprotocol/server\": \"^2.0.0\",\n \"@slip-stream-kit/config\": \"^0.4.0\",\n \"chalk\": \"^6.0.0\",\n \"chokidar\": \"^5.0.0\",\n \"commander\": \"^15.0.0\",\n \"fastify\": \"^5.12.3\",\n \"ink\": \"^7.1.1\",\n \"pino\": \"^10.3.1\",\n \"pino-pretty\": \"^13.1.3\",\n \"portless\": \"^0.15.6\",\n \"react\": \"19.2.8\",\n \"yaml\": \"^2.9.0\",\n \"zod\": \"^4.5.4\",\n \"zx\": \"^8.8.5\"\n },\n \"devDependencies\": {\n \"@modelcontextprotocol/client\": \"^2.0.0\",\n \"@modelcontextprotocol/sdk\": \"^1.30.0\",\n \"@slip-stream-kit/eslint-plugin\": \"workspace:*\",\n \"@types/aws-lambda\": \"catalog:\",\n \"@types/react\": \"catalog:\",\n \"@wl/eslint-config\": \"workspace:*\",\n \"@wl/vitest-config\": \"workspace:*\",\n \"esbuild\": \"^0.28.2\",\n \"ink-testing-library\": \"^4.0.0\",\n \"typescript\": \"^6.0.3\"\n }\n}\n", "/**\n * Pure detection of *how* this CLI was installed, so the background updater knows whether it may act at\n * all, and the advisory it prints can name the one command that will actually work. No fs, no spawn, no `process.env` read \u2014 every input is injected, which is\n * what lets the whole matrix be table-tested.\n *\n * Detection is env-first and LAZY: the only signal that costs a subprocess (`npm root -g`) is passed in\n * as `lazyNpmRoot` and consulted solely when every cheaper matcher has missed.\n */\nimport path from 'node:path'\n\n/** The published package this CLI updates itself to. Single source for every suggested argv. */\nexport const PACKAGE_NAME = 'infra-kit'\n\nconst LATEST = `${PACKAGE_NAME}@latest`\n\nexport type InstallManager = 'npm' | 'pnpm' | 'yarn' | 'bun' | 'volta' | 'homebrew' | 'unknown'\n\nexport interface InstallManagerInfo {\n manager: InstallManager\n /** The command to run. Only safe to spawn ourselves when `canSelfSpawn`; otherwise print it. */\n updateCommand: string[]\n /** False when we must not run the command for the user \u2014 either it needs sudo/a tap, or we guessed. */\n canSelfSpawn: boolean\n}\n\n/**\n * Canonicalises a candidate *root* directory. Required, not optional: `selfRealPath` has already had its\n * symlinks followed, so comparing it against a raw `PNPM_HOME`/`npm root -g`/`cwd` compares a resolved\n * path to an unresolved one and silently never matches. macOS is the everyday proof \u2014 `/var` is a symlink\n * to `/private/var`, so a global install under `/var/...` reports `unknown` unless both sides are resolved.\n * Callers inject the real thing; tests inject a stub.\n */\nexport type RealpathFn = (dir: string) => string\n\nexport interface DetectInstallManagerInput {\n /** Fully-resolved (symlinks followed) path to this CLI's entry file. */\n selfRealPath: string\n env: NodeJS.ProcessEnv\n /** Canonicalises each candidate root before comparison. See {@link RealpathFn}. */\n realpath: RealpathFn\n /** `npm root -g`, deferred: invoked at most once, and only when no cheaper matcher hit. */\n lazyNpmRoot?: () => string | undefined\n}\n\n/**\n * Is `child` inside the `parent` subtree? Boundary-aware: a naive `startsWith` would call\n * `/Users/x/pnpm-ish` a child of `/Users/x/pnpm`. `parent` is canonicalised first because it arrives raw\n * from the environment while `child` is already a realpath.\n */\nexport const isWithin = (parent: string, child: string, realpath: RealpathFn): boolean => {\n const rel = path.relative(realpath(path.resolve(parent)), path.resolve(child))\n\n return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel)\n}\n\n/** Does `p` contain `name` as a whole path SEGMENT? `/a/node_modules_backup/b` must not match `node_modules`. */\nexport const hasSegment = (p: string, name: string): boolean => {\n return path.resolve(p).split(path.sep).includes(name)\n}\n\n/**\n * Is `p` inside a Homebrew keg belonging to the `name` FORMULA \u2014 i.e. `.../Cellar/<name>/<version>/...`?\n *\n * The formula name is what makes this sound. A bare `Cellar` segment says only \"somewhere under a keg\",\n * and the keg it lands in is routinely someone else's: when npm's global prefix is a node keg\n * (`npm config set prefix \"$(brew --prefix node)\"`, or any keg-only `node@X`), an `npm i -g` package\n * installs to `Cellar/node/<v>/lib/node_modules/<pkg>/...` \u2014 brew has never heard of it, yet the segment\n * is right there. Requiring `<name>` to sit directly after `Cellar` is what separates \"brew installed\n * THIS\" from \"brew installed the runtime that installed this\".\n *\n * Works for both keg layouts, since each is rooted at the formula: a plain formula\n * (`Cellar/infra-kit/1.2.3/bin/infra-kit`) and brew's node-CLI layout\n * (`Cellar/infra-kit/1.2.3/libexec/lib/node_modules/infra-kit/dist/cli.js`). Do NOT try to tell them\n * apart by excluding `node_modules` \u2014 the second one contains it.\n */\nexport const isBrewKegOf = (p: string, name: string): boolean => {\n const segments = path.resolve(p).split(path.sep)\n const cellar = segments.indexOf('Cellar')\n\n return cellar !== -1 && segments[cellar + 1] === name\n}\n\n/**\n * The npm global prefix that owns `binRealPath`, derived from the path ITSELF, or null when the\n * layout is not an npm global one.\n *\n * Matches `<prefix>/lib/node_modules/<packageName>` \u2014 npm's global layout and no other manager's\n * \u2014 requiring BOTH the `lib` parent and our package as the immediate child. Windows global npm has\n * no `lib` segment (`%APPDATA%\\npm\\node_modules`), so this returns null there and detection falls\n * through to `npm root -g`, which is correct on Windows, where there is normally one npm.\n */\n// This is the one signal that cannot point at the wrong tree. Everything else in this module\n// answers \"is this file somewhere inside tool X's directory?\", and containment is not ownership:\n// - `npm root -g` answers for whichever `npm` is first on PATH, which is routinely a DIFFERENT\n// node than the one that installed us (a pnpm/nvm/fnm-managed node, or a node that has since\n// been removed). It then reports a root that does not contain us, every matcher misses, and\n// detection degrades to `unknown` \u2014 a notice printed forever, on the single most common install\n// method. That is the bug this exists to close.\n// - `PNPM_HOME` containment is satisfied by `<PNPM_HOME>/nodejs/<v>/lib/node_modules/<pkg>` (a\n// plain `npm i -g` under a pnpm-managed node), yet `pnpm add -g` installs to\n// `<PNPM_HOME>/global/<v>/...` \u2014 a different directory. That one is worse than a missed update:\n// the install \"succeeds\" while the binary on PATH stays old, silently, with no notice to show\n// for it.\n//\n// Requiring both segments is what makes the match proof rather than a hint. The `lib` requirement\n// is also what keeps a project-local `<repo>/node_modules/<pkg>` out (it has no `lib`), and nesting\n// cannot spoof it: `.../node_modules/foo/node_modules/<pkg>` fails the `lib` test.\nexport const npmPrefixOfPackage = (binRealPath: string, packageName: string): string | null => {\n const segments = path.resolve(binRealPath).split(path.sep)\n const nodeModules = segments.lastIndexOf('node_modules')\n\n if (nodeModules < 2) return null\n if (segments[nodeModules - 1] !== 'lib') return null\n if (segments[nodeModules + 1] !== packageName) return null\n\n // Drop `lib/node_modules/...`; the empty result of a root-level prefix (`/lib/node_modules/...`) is `/`.\n return segments.slice(0, nodeModules - 1).join(path.sep) || path.sep\n}\n\n// The self-scoped reading, kept as the name every call site below already uses. `dependency-registry`\n// asks the same question about OTHER packages (`portless`), which is why the body moved up one level\n// rather than gaining a defaulted parameter \u2014 a default is how the wrong package silently gets probed.\nconst npmPrefixFromSelfPath = (selfRealPath: string): string | null => {\n return npmPrefixOfPackage(selfRealPath, PACKAGE_NAME)\n}\n\n/**\n * `--prefix` is passed EXPLICITLY rather than trusting the ambient one: the whole point of deriving it is\n * that the `npm` we are about to run may default to a different prefix than the one we are installed in.\n * A command-line flag outranks both `npm_config_prefix` and any `.npmrc`, so this targets the tree we\n * actually run from. With `-g`, npm writes `<prefix>/lib/node_modules` and links bins into `<prefix>/bin`.\n */\nconst npmPrefixInstallCommand = (prefix: string): string[] => {\n return ['npm', 'install', '-g', '--prefix', prefix, LATEST]\n}\n\n/**\n * Render argv as a line the user can paste into a shell.\n *\n * A plain `join(' ')` was fine while every token came from a static table; `--prefix <path>` puts a\n * filesystem path in argv, and `/Users/Ada Lovelace/.nvm/...` pasted unquoted is two arguments and a\n * failed install. Single quotes are sound here without escaping because the callers' validation rejects a\n * token containing a quote \u2014 see `SAFE_COMMAND_TOKEN` in src/lib/update-check/auto-update.ts.\n *\n * @example\n * formatUpdateCommand(['npm', 'install', '-g', '--prefix', '/opt/x y', 'infra-kit@latest'])\n * // => \"npm install -g --prefix '/opt/x y' infra-kit@latest\"\n */\nexport const formatUpdateCommand = (command: string[]): string => {\n return command\n .map((token) => {\n return token.includes(' ') ? `'${token}'` : token\n })\n .join(' ')\n}\n\n/** `env[key]` names a directory that contains `selfRealPath`. Absent/empty env var \u2192 no match. */\nconst underEnvDir = (env: NodeJS.ProcessEnv, key: string, selfRealPath: string, realpath: RealpathFn): boolean => {\n const dir = env[key]\n\n return dir != null && dir !== '' && isWithin(dir, selfRealPath, realpath)\n}\n\ninterface Matcher extends Omit<InstallManagerInfo, 'manager'> {\n manager: InstallManager\n test: (input: Required<Pick<DetectInstallManagerInput, 'selfRealPath' | 'env' | 'realpath'>>) => boolean\n}\n\n/**\n * Consulted FIRST, before {@link npmPrefixFromSelfPath}. Both of these legitimately wrap an npm-shaped\n * `lib/node_modules` tree \u2014 volta at `~/.volta/tools/image/node/<v>/lib/node_modules`, brew at\n * `Cellar/<pkg>/<v>/libexec/lib/node_modules` \u2014 so deriving an npm prefix from the path would hijack\n * installs that a wrapper owns and must keep owning.\n */\nconst WRAPPER_MATCHERS: Matcher[] = [\n {\n manager: 'volta',\n // volta owns the shim; `volta install` is the correct tool, not a workaround \u2014 so we may run it.\n test: ({ selfRealPath, env, realpath }) => {\n return (\n underEnvDir(env, 'VOLTA_HOME', selfRealPath, realpath) ||\n hasSegment(selfRealPath, '.volta') ||\n hasSegment(selfRealPath, 'volta')\n )\n },\n updateCommand: ['volta', 'install', PACKAGE_NAME],\n canSelfSpawn: true,\n },\n {\n manager: 'homebrew',\n // `brew upgrade` can touch the prefix, relink, and prompt \u2014 never ours to run unattended.\n //\n // Our own keg is the ONLY sound signal \u2014 see `isBrewKegOf`. Two broader tests look plausible\n // and are both wrong, because each answers \"did brew put something here?\" when the question is \"does\n // brew own THIS package?\":\n // - `HOMEBREW_PREFIX` containment: when node comes from brew, npm's global prefix IS\n // $HOMEBREW_PREFIX, so EVERY `npm i -g` package lands under it, unknown to brew.\n // - a bare `Cellar` segment: an npm prefix pointed at a node keg puts packages inside that keg.\n // Either misfire tells macOS users to run `brew upgrade infra-kit` \u2014 a formula that does not exist \u2014\n // and, far worse, makes the background auto-updater bail with `cannot-self-spawn` forever, so the\n // very fix for it can never reach them. Detection runs on the realpath, and a linked brew bin always\n // resolves into its own keg, so the narrow test has no false negatives to trade for this.\n test: ({ selfRealPath }) => {\n return isBrewKegOf(selfRealPath, PACKAGE_NAME)\n },\n updateCommand: ['brew', 'upgrade', PACKAGE_NAME],\n canSelfSpawn: false,\n },\n]\n\n/**\n * Consulted LAST, after {@link npmPrefixFromSelfPath} has had its say. Each of these proves only that we\n * live somewhere inside a tool's directory tree, which is weaker than knowing the layout npm itself\n * created \u2014 see the `PNPM_HOME`/`nodejs` case in that helper's doc for a containment hit whose install\n * command targets the wrong directory.\n */\nconst TREE_MATCHERS: Matcher[] = [\n {\n manager: 'pnpm',\n test: ({ selfRealPath, env, realpath }) => {\n return (\n underEnvDir(env, 'PNPM_HOME', selfRealPath, realpath) ||\n (hasSegment(selfRealPath, 'pnpm') && hasSegment(selfRealPath, 'global'))\n )\n },\n updateCommand: ['pnpm', 'add', '-g', LATEST],\n canSelfSpawn: true,\n },\n {\n manager: 'bun',\n test: ({ selfRealPath, env, realpath }) => {\n return underEnvDir(env, 'BUN_INSTALL', selfRealPath, realpath) || hasSegment(selfRealPath, '.bun')\n },\n updateCommand: ['bun', 'add', '-g', LATEST],\n canSelfSpawn: true,\n },\n {\n manager: 'yarn',\n test: ({ selfRealPath }) => {\n return (\n hasSegment(selfRealPath, '.yarn') || (hasSegment(selfRealPath, 'yarn') && hasSegment(selfRealPath, 'global'))\n )\n },\n updateCommand: ['yarn', 'global', 'add', LATEST],\n canSelfSpawn: true,\n },\n {\n manager: 'npm',\n test: ({ selfRealPath, env, realpath }) => {\n return underEnvDir(env, 'npm_config_prefix', selfRealPath, realpath)\n },\n updateCommand: ['npm', 'install', '-g', LATEST],\n canSelfSpawn: true,\n },\n]\n\nconst NPM_UPDATE_COMMAND = ['npm', 'install', '-g', LATEST]\n\n/**\n * Identify the package manager that owns `selfRealPath`.\n *\n * Order, and why it is this one: wrappers that own an npm-shaped tree (volta, brew) first, then the npm\n * prefix DERIVED from our own path, then the env/path containment matchers, then the `npm root -g` probe.\n * The derived prefix sits above containment because it is the only signal that cannot name a directory we\n * do not live in \u2014 see {@link npmPrefixFromSelfPath}.\n *\n * `lazyNpmRoot` is the last resort and is invoked at most once, only after every matcher has missed \u2014 a\n * pnpm or volta install never pays for the subprocess, and neither does any npm global install in the\n * standard layout, which the derived prefix now settles for free. When nothing matches we report `unknown`\n * with an npm command that is a *suggestion only* (`canSelfSpawn: false`): running a guessed global\n * install is worse than printing one.\n *\n * @example\n * detectInstallManager({ selfRealPath: '/Users/x/Library/pnpm/global/5/node_modules/infra-kit/dist/cli.js', env: {} })\n * // => { manager: 'pnpm', updateCommand: ['pnpm', 'add', '-g', 'infra-kit@latest'], canSelfSpawn: true }\n */\nexport const detectInstallManager = (input: DetectInstallManagerInput): InstallManagerInfo => {\n const { selfRealPath, env, realpath, lazyNpmRoot } = input\n const toInfo = (hit: Matcher): InstallManagerInfo => {\n return { manager: hit.manager, updateCommand: hit.updateCommand, canSelfSpawn: hit.canSelfSpawn }\n }\n const matches = ({ test }: Matcher): boolean => {\n return test({ selfRealPath, env, realpath })\n }\n\n const wrapper = WRAPPER_MATCHERS.find(matches)\n\n if (wrapper) return toInfo(wrapper)\n\n const derivedPrefix = npmPrefixFromSelfPath(selfRealPath)\n\n if (derivedPrefix !== null) {\n return { manager: 'npm', updateCommand: npmPrefixInstallCommand(derivedPrefix), canSelfSpawn: true }\n }\n\n const tree = TREE_MATCHERS.find(matches)\n\n if (tree) return toInfo(tree)\n\n const npmRoot = lazyNpmRoot?.()\n\n if (npmRoot != null && npmRoot !== '' && isWithin(npmRoot, selfRealPath, realpath)) {\n return { manager: 'npm', updateCommand: NPM_UPDATE_COMMAND, canSelfSpawn: true }\n }\n\n return { manager: 'unknown', updateCommand: NPM_UPDATE_COMMAND, canSelfSpawn: false }\n}\n\n/**\n * Is this CLI running from a *project-local* `node_modules` (as opposed to a global root)?\n *\n * The cwd clause is what does the distinguishing \u2014 BOTH a project install and a global root contain a\n * `/node_modules/` segment (`pnpm root -g` is `~/Library/pnpm/global/5/node_modules`). Without it we\n * would nag every global pnpm user to stop using a local install they do not have.\n *\n * Accepted false-negative: invoked from a subdirectory (cwd `project/apps/x`, deps at\n * `project/node_modules`) this returns false and the advisory stays silent. Under-warning is the safe\n * direction for a best-effort, never-throwing advisory \u2014 do not \"fix\" it by dropping the cwd clause.\n *\n * `cwd` is canonicalised before comparison for the same reason `detectInstallManager` canonicalises its\n * roots: `selfRealPath` is a realpath, and comparing it to an unresolved cwd never matches.\n *\n * @example\n * isLocalNodeModulesInstall('/repo/node_modules/infra-kit/dist/cli.js', '/repo', (p) => p) // => true\n */\nexport const isLocalNodeModulesInstall = (selfRealPath: string, cwd: string, realpath: RealpathFn): boolean => {\n return hasSegment(selfRealPath, 'node_modules') && isWithin(cwd, selfRealPath, realpath)\n}\n", "import { realpathSync } from 'node:fs'\nimport path from 'node:path'\n\n/**\n * The impure companion to the pure detection module: canonicalise a directory that may not exist.\n *\n * `realpathSync` throws on a missing path, and candidate roots routinely miss \u2014 `PNPM_HOME` can point at a\n * directory the user never created, `npm root -g` can name a prefix that was removed. A throw there would\n * turn \"we could not identify your install\" into a crash, so an unresolvable path falls back to\n * `path.resolve`, which still normalises `.`/`..` and yields a usable, non-matching absolute path.\n */\nexport const safeRealpath = (dir: string): string => {\n try {\n return realpathSync(dir)\n } catch {\n return path.resolve(dir)\n }\n}\n", "/* eslint-disable sonarjs/no-os-command-from-path */\n/**\n * `npm root -g`, the LAZY fallback for {@link detectInstallManager}.\n *\n * Why it is indispensable: the `npm` matcher keys off `npm_config_prefix`, which npm sets only while npm\n * itself is running. A user who ran `npm i -g infra-kit` months ago and now types `ik` has no such var,\n * so every cheap matcher misses and detection reports `unknown` / `canSelfSpawn: false`. Without this\n * probe the single most common install method can never update itself.\n *\n * Read by the background update worker, and by the `doctor` advisory that names the command a human\n * would run by hand \u2014 one probe so the two can never disagree about who owns the install.\n */\nimport { execFileSync } from 'node:child_process'\nimport process from 'node:process'\n\nconst NPM_ROOT_TIMEOUT_MS = 3_000\n\n/**\n * The global npm root, or undefined when npm is absent, slow, or errors. Costs a subprocess, so\n * `detectInstallManager` invokes it at most once and only after every env/path matcher has missed \u2014 a\n * pnpm or volta install never pays for it.\n *\n * Resolving the user's `npm` from PATH is inherent here: we must ask the very package manager that owns\n * this install where its global root is. Pinning an absolute path would defeat the detection.\n *\n * @example\n * defaultLazyNpmRoot() // => '/usr/local/lib/node_modules' | undefined\n */\nexport const defaultLazyNpmRoot = (): string | undefined => {\n try {\n const out = execFileSync('npm', ['root', '-g'], {\n encoding: 'utf-8',\n timeout: NPM_ROOT_TIMEOUT_MS,\n stdio: ['ignore', 'pipe', 'ignore'],\n shell: process.platform === 'win32',\n })\n const trimmed = out.trim()\n\n return trimmed === '' ? undefined : trimmed\n } catch {\n return undefined\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["package_default", "path", "PACKAGE_NAME", "LATEST", "isWithin", "parent", "child", "realpath", "rel", "hasSegment", "p", "name", "isBrewKegOf", "segments", "cellar", "npmPrefixOfPackage", "binRealPath", "packageName", "nodeModules", "npmPrefixFromSelfPath", "selfRealPath", "npmPrefixInstallCommand", "prefix", "formatUpdateCommand", "command", "token", "underEnvDir", "env", "key", "dir", "WRAPPER_MATCHERS", "TREE_MATCHERS", "NPM_UPDATE_COMMAND", "detectInstallManager", "input", "lazyNpmRoot", "toInfo", "hit", "matches", "test", "wrapper", "derivedPrefix", "tree", "npmRoot", "isLocalNodeModulesInstall", "cwd", "realpathSync", "path", "safeRealpath", "dir", "execFileSync", "process", "NPM_ROOT_TIMEOUT_MS", "defaultLazyNpmRoot", "trimmed"]
|
|
7
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import T from"node:crypto";import r from"node:fs";import F from"node:os";import n from"node:path";import A from"node:process";var V="env-load.sh",a="env-clear.sh",p="projects",O=7200,E="INFRA_KIT_SESSION",m="INFRA_KIT_ENV",C="INFRA_KIT_ENV_CONFIG",D="INFRA_KIT_ENV_PROJECT",S="INFRA_KIT_ENV_PROJECT_ROOT",K="INFRA_KIT_ENV_LOADED_AT",g="INFRA_KIT_ENV_AUTOLOADED",x="INFRA_KIT_ENV_CLEARED",I=/^([A-Z_]\w*)=/i,L=(t,e)=>{let o=e;for(let _=0;_<t.length;_++){if(!o&&t[_]==="\\"){_++;continue}t[_]==="'"&&(o=!o)}return o},h=t=>{if(!r.existsSync(t))return[];let e=r.readFileSync(t,"utf-8"),o=[],_=!1;for(let s of e.split(`
|
|
2
|
-
`)){if(!_){let c=I.exec(s);c&&o.push(c[1])}_=L(s,_)}return o},R=()=>{let t=A.env.XDG_CACHE_HOME,e=t&&t.length>0?t:n.join(F.homedir(),".cache");return n.join(e,"infra-kit")},f=()=>{let t=A.env[E];if(!t)throw new Error(`${E} is not set. Run \`infra-kit
|
|
3
|
-
//# sourceMappingURL=chunk-
|
|
2
|
+
`)){if(!_){let c=I.exec(s);c&&o.push(c[1])}_=L(s,_)}return o},R=()=>{let t=A.env.XDG_CACHE_HOME,e=t&&t.length>0?t:n.join(F.homedir(),".cache");return n.join(e,"infra-kit")},f=()=>{let t=A.env[E];if(!t)throw new Error(`${E} is not set. Run \`infra-kit setup --skip-tools\` then \`source ~/.zshrc\`.`);return n.join(R(),t)},N=t=>T.createHash("sha256").update(t,"utf8").digest("hex"),i=()=>n.join(R(),p),u=t=>n.join(i(),N(t)),l=(t,e,o)=>{let _=`${t}.tmp.${A.pid}`;r.writeFileSync(_,e,{mode:o});try{r.renameSync(_,t)}catch(s){throw r.rmSync(_,{force:!0}),s}},y="-worktrees";export{V as a,a as b,O as c,E as d,m as e,C as f,D as g,S as h,K as i,g as j,x as k,h as l,R as m,f as n,i as o,u as p,l as q,y as r};
|
|
3
|
+
//# sourceMappingURL=chunk-D4UYDBN5.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/lib/constants/constants.ts"],
|
|
4
|
-
"sourcesContent": ["import crypto from 'node:crypto'\nimport fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\nimport process from 'node:process'\n\nexport const ENV_LOAD_FILE = 'env-load.sh'\nexport const ENV_CLEAR_FILE = 'env-clear.sh'\n\n/** Subdir of the cache root holding the project-scoped WARM caches (one dir per\n * project, keyed by {@link warmCacheKey}). Kept in sync with the zsh block in\n * init.ts, which reads `$cache_root/projects/$key/env-load.sh`. */\nexport const WARM_CACHE_SUBDIR = 'projects'\n\n/**\n * Default warm-cache TTL in seconds (2h). Governs BOTH the node-side eviction\n * sweep and the zsh-side source gate (`_INFRA_KIT_WARM_TTL` in the shell block);\n * the two MUST stay equal, so this constant is the single source of truth and the\n * shell default is emitted from it. Short by design: a warm file older than this\n * is never sourced (bounds a revoked/rotated secret served at prompt-0) and is\n * deleted on the next write. The background refresh lands in ~1-2s regardless.\n */\nexport const DEFAULT_WARM_TTL_SECONDS = 2 * 60 * 60\n\nexport const INFRA_KIT_SESSION_VAR = 'INFRA_KIT_SESSION'\n/**\n * The active environment/config NAME (e.g. `dev`, `arthur`), exported plainly so\n * non-shell consumers can read it from `process.env`. Notably `infra-kit/vite`'s\n * `infraKitDev()` reads this to interpolate the `<env>` placeholder in cloud proxy\n * targets. Mirrors {@link INFRA_KIT_ENV_CONFIG_VAR} (same value) but is the stable,\n * purpose-named handle for tooling rather than the shell session-metadata var.\n */\nexport const INFRA_KIT_ENV_VAR = 'INFRA_KIT_ENV'\nexport const INFRA_KIT_ENV_CONFIG_VAR = 'INFRA_KIT_ENV_CONFIG'\nexport const INFRA_KIT_ENV_PROJECT_VAR = 'INFRA_KIT_ENV_PROJECT'\n/**\n * Absolute project root (git top-level) the loaded env belongs to. Lets the shell\n * startup gate tell a same-project subshell (skip) from a NEW project (load), so a\n * different project's secrets are never silently kept after `cd`/new shell.\n */\nexport const INFRA_KIT_ENV_PROJECT_ROOT_VAR = 'INFRA_KIT_ENV_PROJECT_ROOT'\nexport const INFRA_KIT_ENV_LOADED_AT_VAR = 'INFRA_KIT_ENV_LOADED_AT'\n/**\n * Marker exported into env-load.sh ONLY when the load was triggered automatically\n * (see lib/env-autoload). Its presence is the sole signal distinguishing an\n * auto-loaded env from a deliberate manual `env-load`, so auto-load never\n * clobbers a manual choice. A manual load unsets it.\n */\nexport const INFRA_KIT_ENV_AUTOLOADED_VAR = 'INFRA_KIT_ENV_AUTOLOADED'\n/**\n * Suppression sentinel exported by env-clear. While set in a shell, cli-invocation\n * auto-load stays silent (a deliberate clear must not be immediately re-loaded).\n * Lifted by a manual `env-load` or a new shell.\n */\nexport const INFRA_KIT_ENV_CLEARED_VAR = 'INFRA_KIT_ENV_CLEARED'\n\n/**\n * Matches a line of the form `KEY=...` where KEY is an env-var identifier\n * (letter or underscore, then word chars). Capture group 1 is the name. Shared\n * between env-load (validation, var counting) and parseVarNamesFromEnvFile.\n */\nexport const ENV_VAR_LINE_PATTERN = /^([A-Z_]\\w*)=/i\n\n/**\n * Track whether a physical line leaves us inside an open single-quoted value,\n * mirroring how `shellSingleQuote` emits values (`'\u2026'`, with literal quotes as\n * `'\\''`). Outside a quote a backslash escapes the next char; inside a quote a\n * `'` closes it. Lets the parser skip the continuation lines of a multiline value.\n */\nconst advanceSingleQuoteState = (line: string, startInQuote: boolean): boolean => {\n let inQuote = startInQuote\n\n for (let i = 0; i < line.length; i++) {\n if (!inQuote && line[i] === '\\\\') {\n i++\n continue\n }\n\n if (line[i] === \"'\") {\n inQuote = !inQuote\n }\n }\n\n return inQuote\n}\n\nexport const parseVarNamesFromEnvFile = (filePath: string): string[] => {\n if (!fs.existsSync(filePath)) return []\n\n const content = fs.readFileSync(filePath, 'utf-8')\n const names: string[] = []\n let inQuote = false\n\n for (const line of content.split('\\n')) {\n // Only a line that starts OUTSIDE a quoted value can be a real assignment;\n // continuation lines of a multiline secret value are skipped.\n if (!inQuote) {\n const match = ENV_VAR_LINE_PATTERN.exec(line)\n\n if (match) {\n names.push(match[1]!)\n }\n }\n\n inQuote = advanceSingleQuoteState(line, inQuote)\n }\n\n return names\n}\n\n/**\n * Root cache dir for infra-kit across all sessions. Resolved from\n * $XDG_CACHE_HOME when set, falling back to ~/.cache/infra-kit. Keep in sync\n * with the shell block emitted by `infra-kit
|
|
5
|
-
"mappings": "AAAA,OAAOA,MAAY,cACnB,OAAOC,MAAQ,UACf,OAAOC,MAAQ,UACf,OAAOC,MAAU,YACjB,OAAOC,MAAa,eAEb,IAAMC,EAAgB,cAChBC,EAAiB,eAKjBC,EAAoB,WAUpBC,EAA2B,KAE3BC,EAAwB,oBAQxBC,EAAoB,gBACpBC,EAA2B,uBAC3BC,EAA4B,wBAM5BC,EAAiC,6BACjCC,EAA8B,0BAO9BC,EAA+B,2BAM/BC,EAA4B,wBAO5BC,EAAuB,iBAQ9BC,EAA0B,CAACC,EAAcC,IAAmC,CAChF,IAAIC,EAAUD,EAEd,QAASE,EAAI,EAAGA,EAAIH,EAAK,OAAQG,IAAK,CACpC,GAAI,CAACD,GAAWF,EAAKG,CAAC,IAAM,KAAM,CAChCA,IACA,QACF,CAEIH,EAAKG,CAAC,IAAM,MACdD,EAAU,CAACA,EAEf,CAEA,OAAOA,CACT,EAEaE,EAA4BC,GAA+B,CACtE,GAAI,CAACvB,EAAG,WAAWuB,CAAQ,EAAG,MAAO,CAAC,EAEtC,IAAMC,EAAUxB,EAAG,aAAauB,EAAU,OAAO,EAC3CE,EAAkB,CAAC,EACrBL,EAAU,GAEd,QAAWF,KAAQM,EAAQ,MAAM;AAAA,CAAI,EAAG,CAGtC,GAAI,CAACJ,EAAS,CACZ,IAAMM,EAAQV,EAAqB,KAAKE,CAAI,EAExCQ,GACFD,EAAM,KAAKC,EAAM,CAAC,CAAE,CAExB,CAEAN,EAAUH,EAAwBC,EAAME,CAAO,CACjD,CAEA,OAAOK,CACT,EAOaE,EAAe,IAAc,CACxC,IAAMC,EAAMzB,EAAQ,IAAI,eAClB0B,EAAOD,GAAOA,EAAI,OAAS,EAAIA,EAAM1B,EAAK,KAAKD,EAAG,QAAQ,EAAG,QAAQ,EAE3E,OAAOC,EAAK,KAAK2B,EAAM,WAAW,CACpC,EAEaC,EAAqB,IAAc,CAC9C,IAAMC,EAAU5B,EAAQ,IAAIK,CAAqB,EAEjD,GAAI,CAACuB,EACH,MAAM,IAAI,
|
|
4
|
+
"sourcesContent": ["import crypto from 'node:crypto'\nimport fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\nimport process from 'node:process'\n\nexport const ENV_LOAD_FILE = 'env-load.sh'\nexport const ENV_CLEAR_FILE = 'env-clear.sh'\n\n/** Subdir of the cache root holding the project-scoped WARM caches (one dir per\n * project, keyed by {@link warmCacheKey}). Kept in sync with the zsh block in\n * init.ts, which reads `$cache_root/projects/$key/env-load.sh`. */\nexport const WARM_CACHE_SUBDIR = 'projects'\n\n/**\n * Default warm-cache TTL in seconds (2h). Governs BOTH the node-side eviction\n * sweep and the zsh-side source gate (`_INFRA_KIT_WARM_TTL` in the shell block);\n * the two MUST stay equal, so this constant is the single source of truth and the\n * shell default is emitted from it. Short by design: a warm file older than this\n * is never sourced (bounds a revoked/rotated secret served at prompt-0) and is\n * deleted on the next write. The background refresh lands in ~1-2s regardless.\n */\nexport const DEFAULT_WARM_TTL_SECONDS = 2 * 60 * 60\n\nexport const INFRA_KIT_SESSION_VAR = 'INFRA_KIT_SESSION'\n/**\n * The active environment/config NAME (e.g. `dev`, `arthur`), exported plainly so\n * non-shell consumers can read it from `process.env`. Notably `infra-kit/vite`'s\n * `infraKitDev()` reads this to interpolate the `<env>` placeholder in cloud proxy\n * targets. Mirrors {@link INFRA_KIT_ENV_CONFIG_VAR} (same value) but is the stable,\n * purpose-named handle for tooling rather than the shell session-metadata var.\n */\nexport const INFRA_KIT_ENV_VAR = 'INFRA_KIT_ENV'\nexport const INFRA_KIT_ENV_CONFIG_VAR = 'INFRA_KIT_ENV_CONFIG'\nexport const INFRA_KIT_ENV_PROJECT_VAR = 'INFRA_KIT_ENV_PROJECT'\n/**\n * Absolute project root (git top-level) the loaded env belongs to. Lets the shell\n * startup gate tell a same-project subshell (skip) from a NEW project (load), so a\n * different project's secrets are never silently kept after `cd`/new shell.\n */\nexport const INFRA_KIT_ENV_PROJECT_ROOT_VAR = 'INFRA_KIT_ENV_PROJECT_ROOT'\nexport const INFRA_KIT_ENV_LOADED_AT_VAR = 'INFRA_KIT_ENV_LOADED_AT'\n/**\n * Marker exported into env-load.sh ONLY when the load was triggered automatically\n * (see lib/env-autoload). Its presence is the sole signal distinguishing an\n * auto-loaded env from a deliberate manual `env-load`, so auto-load never\n * clobbers a manual choice. A manual load unsets it.\n */\nexport const INFRA_KIT_ENV_AUTOLOADED_VAR = 'INFRA_KIT_ENV_AUTOLOADED'\n/**\n * Suppression sentinel exported by env-clear. While set in a shell, cli-invocation\n * auto-load stays silent (a deliberate clear must not be immediately re-loaded).\n * Lifted by a manual `env-load` or a new shell.\n */\nexport const INFRA_KIT_ENV_CLEARED_VAR = 'INFRA_KIT_ENV_CLEARED'\n\n/**\n * Matches a line of the form `KEY=...` where KEY is an env-var identifier\n * (letter or underscore, then word chars). Capture group 1 is the name. Shared\n * between env-load (validation, var counting) and parseVarNamesFromEnvFile.\n */\nexport const ENV_VAR_LINE_PATTERN = /^([A-Z_]\\w*)=/i\n\n/**\n * Track whether a physical line leaves us inside an open single-quoted value,\n * mirroring how `shellSingleQuote` emits values (`'\u2026'`, with literal quotes as\n * `'\\''`). Outside a quote a backslash escapes the next char; inside a quote a\n * `'` closes it. Lets the parser skip the continuation lines of a multiline value.\n */\nconst advanceSingleQuoteState = (line: string, startInQuote: boolean): boolean => {\n let inQuote = startInQuote\n\n for (let i = 0; i < line.length; i++) {\n if (!inQuote && line[i] === '\\\\') {\n i++\n continue\n }\n\n if (line[i] === \"'\") {\n inQuote = !inQuote\n }\n }\n\n return inQuote\n}\n\nexport const parseVarNamesFromEnvFile = (filePath: string): string[] => {\n if (!fs.existsSync(filePath)) return []\n\n const content = fs.readFileSync(filePath, 'utf-8')\n const names: string[] = []\n let inQuote = false\n\n for (const line of content.split('\\n')) {\n // Only a line that starts OUTSIDE a quoted value can be a real assignment;\n // continuation lines of a multiline secret value are skipped.\n if (!inQuote) {\n const match = ENV_VAR_LINE_PATTERN.exec(line)\n\n if (match) {\n names.push(match[1]!)\n }\n }\n\n inQuote = advanceSingleQuoteState(line, inQuote)\n }\n\n return names\n}\n\n/**\n * Root cache dir for infra-kit across all sessions. Resolved from\n * $XDG_CACHE_HOME when set, falling back to ~/.cache/infra-kit. Keep in sync\n * with the shell block emitted by `infra-kit setup` (src/commands/init/init.ts).\n */\nexport const getCacheRoot = (): string => {\n const xdg = process.env.XDG_CACHE_HOME\n const base = xdg && xdg.length > 0 ? xdg : path.join(os.homedir(), '.cache')\n\n return path.join(base, 'infra-kit')\n}\n\nexport const getSessionCacheDir = (): string => {\n const session = process.env[INFRA_KIT_SESSION_VAR]\n\n if (!session) {\n throw new Error(\n `${INFRA_KIT_SESSION_VAR} is not set. Run \\`infra-kit setup --skip-tools\\` then \\`source ~/.zshrc\\`.`,\n )\n }\n\n return path.join(getCacheRoot(), session)\n}\n\n/**\n * The warm-cache key for a project: the hex SHA-256 of its CANONICAL (realpath'd)\n * directory. This MUST be byte-identical to the zsh block's\n * `printf %s \"$canon\" | shasum -a 256 | cut -c1-64` \u2014 same input string (no\n * trailing newline), same digest, full 64-hex output \u2014 or a warm file written by\n * node is never found by the shell. The shell passes the already-canonicalized\n * dir via `--project-dir`, so node hashes that VERBATIM (it does not re-resolve).\n */\nexport const warmCacheKey = (canonicalProjectDir: string): string => {\n return crypto.createHash('sha256').update(canonicalProjectDir, 'utf8').digest('hex')\n}\n\n/** Root holding all project-scoped warm caches: `$cacheRoot/projects`. */\nexport const getWarmCacheRoot = (): string => {\n return path.join(getCacheRoot(), WARM_CACHE_SUBDIR)\n}\n\n/** This project's warm-cache dir: `$cacheRoot/projects/<warmCacheKey>`. */\nexport const getProjectWarmCacheDir = (canonicalProjectDir: string): string => {\n return path.join(getWarmCacheRoot(), warmCacheKey(canonicalProjectDir))\n}\n\n/**\n * Write content atomically: write to a pid-suffixed temp file in the same\n * directory, then rename. fs.renameSync is atomic on a single filesystem, so\n * concurrent writers can't produce a half-written secret file.\n */\nexport const atomicWriteFileSync = (filePath: string, content: string, mode: number): void => {\n const tmpPath = `${filePath}.tmp.${process.pid}`\n\n fs.writeFileSync(tmpPath, content, { mode })\n\n try {\n fs.renameSync(tmpPath, filePath)\n } catch (error) {\n fs.rmSync(tmpPath, { force: true })\n throw error\n }\n}\n\nexport const WORKTREES_DIR_SUFFIX = '-worktrees'\n// eslint-disable-next-line sonarjs/publicly-writable-directories\nexport const LOG_FILE_PATH = '/tmp/mcp-infra-kit.log'\n"],
|
|
5
|
+
"mappings": "AAAA,OAAOA,MAAY,cACnB,OAAOC,MAAQ,UACf,OAAOC,MAAQ,UACf,OAAOC,MAAU,YACjB,OAAOC,MAAa,eAEb,IAAMC,EAAgB,cAChBC,EAAiB,eAKjBC,EAAoB,WAUpBC,EAA2B,KAE3BC,EAAwB,oBAQxBC,EAAoB,gBACpBC,EAA2B,uBAC3BC,EAA4B,wBAM5BC,EAAiC,6BACjCC,EAA8B,0BAO9BC,EAA+B,2BAM/BC,EAA4B,wBAO5BC,EAAuB,iBAQ9BC,EAA0B,CAACC,EAAcC,IAAmC,CAChF,IAAIC,EAAUD,EAEd,QAASE,EAAI,EAAGA,EAAIH,EAAK,OAAQG,IAAK,CACpC,GAAI,CAACD,GAAWF,EAAKG,CAAC,IAAM,KAAM,CAChCA,IACA,QACF,CAEIH,EAAKG,CAAC,IAAM,MACdD,EAAU,CAACA,EAEf,CAEA,OAAOA,CACT,EAEaE,EAA4BC,GAA+B,CACtE,GAAI,CAACvB,EAAG,WAAWuB,CAAQ,EAAG,MAAO,CAAC,EAEtC,IAAMC,EAAUxB,EAAG,aAAauB,EAAU,OAAO,EAC3CE,EAAkB,CAAC,EACrBL,EAAU,GAEd,QAAWF,KAAQM,EAAQ,MAAM;AAAA,CAAI,EAAG,CAGtC,GAAI,CAACJ,EAAS,CACZ,IAAMM,EAAQV,EAAqB,KAAKE,CAAI,EAExCQ,GACFD,EAAM,KAAKC,EAAM,CAAC,CAAE,CAExB,CAEAN,EAAUH,EAAwBC,EAAME,CAAO,CACjD,CAEA,OAAOK,CACT,EAOaE,EAAe,IAAc,CACxC,IAAMC,EAAMzB,EAAQ,IAAI,eAClB0B,EAAOD,GAAOA,EAAI,OAAS,EAAIA,EAAM1B,EAAK,KAAKD,EAAG,QAAQ,EAAG,QAAQ,EAE3E,OAAOC,EAAK,KAAK2B,EAAM,WAAW,CACpC,EAEaC,EAAqB,IAAc,CAC9C,IAAMC,EAAU5B,EAAQ,IAAIK,CAAqB,EAEjD,GAAI,CAACuB,EACH,MAAM,IAAI,MACR,GAAGvB,CAAqB,6EAC1B,EAGF,OAAON,EAAK,KAAKyB,EAAa,EAAGI,CAAO,CAC1C,EAUaC,EAAgBC,GACpBlC,EAAO,WAAW,QAAQ,EAAE,OAAOkC,EAAqB,MAAM,EAAE,OAAO,KAAK,EAIxEC,EAAmB,IACvBhC,EAAK,KAAKyB,EAAa,EAAGrB,CAAiB,EAIvC6B,EAA0BF,GAC9B/B,EAAK,KAAKgC,EAAiB,EAAGF,EAAaC,CAAmB,CAAC,EAQ3DG,EAAsB,CAACb,EAAkBC,EAAiBa,IAAuB,CAC5F,IAAMC,EAAU,GAAGf,CAAQ,QAAQpB,EAAQ,GAAG,GAE9CH,EAAG,cAAcsC,EAASd,EAAS,CAAE,KAAAa,CAAK,CAAC,EAE3C,GAAI,CACFrC,EAAG,WAAWsC,EAASf,CAAQ,CACjC,OAASgB,EAAO,CACd,MAAAvC,EAAG,OAAOsC,EAAS,CAAE,MAAO,EAAK,CAAC,EAC5BC,CACR,CACF,EAEaC,EAAuB",
|
|
6
6
|
"names": ["crypto", "fs", "os", "path", "process", "ENV_LOAD_FILE", "ENV_CLEAR_FILE", "WARM_CACHE_SUBDIR", "DEFAULT_WARM_TTL_SECONDS", "INFRA_KIT_SESSION_VAR", "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", "ENV_VAR_LINE_PATTERN", "advanceSingleQuoteState", "line", "startInQuote", "inQuote", "i", "parseVarNamesFromEnvFile", "filePath", "content", "names", "match", "getCacheRoot", "xdg", "base", "getSessionCacheDir", "session", "warmCacheKey", "canonicalProjectDir", "getWarmCacheRoot", "getProjectWarmCacheDir", "atomicWriteFileSync", "mode", "tmpPath", "error", "WORKTREES_DIR_SUFFIX"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import{L as f,b as O,d as y,ea as T}from"./chunk-Q4HKBLQG.js";import{q as k}from"./chunk-D4UYDBN5.js";import{a as S,b as P}from"./chunk-QWVLAZ6N.js";import u from"node:fs/promises";import c from"node:path";import{z as s}from"zod";var i=class extends Error{env;constructor(t,o=null){super(t),this.name="EnvAuthError",this.env=o}},L=e=>e instanceof i;import N from"node:fs/promises";import I from"node:os";var j=async e=>{try{return await N.access(e),!0}catch{return!1}},d=e=>{let t=I.homedir();return e.startsWith(t)?`~${e.slice(t.length)}`:e};var _="tokens.json",h=1,b=384,C=448,A=s.object({version:s.literal(h).default(h),envs:s.record(s.string().min(1),s.string().min(1))}).strict(),g=async()=>{let e=await T();return c.join(c.dirname(e.userProject),_)},v=async()=>{let e=await g(),t;try{t=await u.readFile(e,"utf-8")}catch{return null}let o;try{o=JSON.parse(t)}catch(n){throw new i(`Invalid JSON in the token store at ${d(e)}: ${n.message}
|
|
2
|
+
Fix or delete the file, then re-add the token with \`infra-kit env-token-set <env>\`.`)}let r=A.safeParse(o);if(!r.success)throw new i(`Invalid token store at ${d(e)}: ${s.prettifyError(r.error)}
|
|
3
|
+
Fix or delete the file, then re-add the token with \`infra-kit env-token-set <env>\`.`);return r.data},x=async e=>{let t=await g(),o=c.dirname(t),r=c.dirname(o),n=c.dirname(r);for(let a of[n,r,o])await u.mkdir(a,{recursive:!0,mode:C}),await u.chmod(a,C);k(t,`${JSON.stringify(e,null,2)}
|
|
4
|
+
`,b),await u.chmod(t,b)},F=async(e,t)=>{let o=await v();await x({version:h,envs:{...o?.envs,[e]:t}})},H=async e=>{let t=await v();if(!t||!(e in t.envs))return;let o={...t.envs};delete o[e],await x({version:h,envs:o})};var M=()=>{let e="",t=[],o=!1;return{start(r){e=r,t=[],o=!1},setInteractive(){o=!0},addOption(r,n){t.push({flag:r,value:n})},formatOptions(){return t.map(r=>typeof r.value=="boolean"?r.value?r.flag:"":Array.isArray(r.value)?`${r.flag} "${r.value.join(", ")}"`:`${r.flag} "${r.value}"`).filter(Boolean).join(" ")},snapshot(){return t.length===0?null:{formattedOptions:this.formatOptions()}},print(){!o||t.length===0||!e||f.info(`\u{1F4DF} Equivalent command:
|
|
5
|
+
pnpm exec infra-kit ${e} ${this.formatOptions()}
|
|
6
|
+
`)},reset(){e="",t=[],o=!1}}},w=M();import D from"node:process";var R=27,J=e=>{if(typeof e=="object")return e.value;throw new O(void 0,{operation:"interactive prompt",remediation:e==="unreachable"?"this prompt is declared unreachable under MCP because a required field should block the path \u2014 reaching it means that field was relaxed without updating the call site":"pass the value explicitly instead of relying on the prompt \u2014 MCP runs have no human to answer it",stderrExcerpt:"an interactive prompt was reached under MCP, where stdin carries JSON-RPC"})},$=async(e,t)=>{let o=new AbortController,{whenHeadless:r="refuse",...n}=t??{},a={...n,signal:o.signal};if(y())return J(r);if(!D.stdin.isTTY)return e(a);let m=D.stdin,E=p=>{p.length===1&&p[0]===R&&o.abort()};S();try{let p=e(a);return m.isPaused()&&m.resume(),m.on("data",E),await p}finally{m.removeListener("data",E),P()}};import B from"@inquirer/confirm";import K from"node:process";var l=class extends Error{constructor(t="Operation cancelled by the operator"){super(t),this.name="CommandDeclinedError"}},ue=e=>e instanceof l;var q=async(e,t,o={})=>{let r=e?!0:await $(n=>B({message:t},n),{whenHeadless:"refuse"});if(e||w.setInteractive(),!r){if(o.throwOnDecline)throw new l;f.info("Operation cancelled. Exiting..."),K.exit(0)}};export{i as a,L as b,j as c,d,g as e,v as f,F as g,H as h,w as i,ue as j,$ as k,q as l};
|
|
7
|
+
//# sourceMappingURL=chunk-HXBIE4JP.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/lib/env-tokens/env-tokens.ts", "../src/lib/errors/env-auth-error.ts", "../src/lib/path-display/path-display.ts", "../src/lib/command-echo/command-echo.ts", "../src/lib/prompts/escapable-context.ts", "../src/lib/command-echo/confirm-or-exit.ts", "../src/lib/errors/command-declined-error.ts"],
|
|
4
|
-
"sourcesContent": ["// DEFAULT import, deliberately \u2014 the same reason config-bootstrap.ts documents: the module is used as\n// `fs.readFile(...)` / `fs.mkdir(...)` so every call is a property lookup on the live module object,\n// which is what `vi.spyOn(fs, \u2026)` patches. A named import (`import { readFile }`) is bound at import\n// time and the spy silently does NOT intercept it \u2014 the assertion goes green while the real call\n// still hits disk. Do not \"modernize\" this import.\nimport fs from 'node:fs/promises'\nimport path from 'node:path'\nimport { z } from 'zod'\n\nimport { atomicWriteFileSync } from 'src/lib/constants'\nimport { EnvAuthError } from 'src/lib/errors/env-auth-error'\nimport { getInfraKitConfigPaths } from 'src/lib/infra-kit-config'\nimport { tildify } from 'src/lib/path-display'\n\n/**\n * Basename of the token store. A SIBLING of the layer-3 `infra-kit.json`, never a key inside it:\n * `getInfraKitConfig` only ever globs the three hardcoded `infra-kit.json` paths and its schema is\n * `.strict()`, so an unknown key throws at the LAYER parse and bricks every command on every pinned\n * version. A sibling file is structurally invisible to that loader \u2014 `config-bootstrap.ts` already\n * relies on exactly this for its `.example.jsonc`. That invisibility is the blast-radius guarantee:\n * a corrupt `tokens.json` can only break the readers below, never `worktrees-list`.\n */\nconst TOKEN_STORE_FILE = 'tokens.json'\n\n/** Schema version of the on-disk store. Bump only alongside a migration. */\nconst TOKEN_STORE_VERSION = 1\n\n/** Mode of `tokens.json` itself \u2014 owner read/write only. It holds live credentials. */\nconst TOKEN_FILE_MODE = 0o600\n\n/** Mode of every directory on the way to the store \u2014 owner-only traversal. */\nconst TOKEN_DIR_MODE = 0o700\n\n/**\n * The store's OWN schema: `{ version?: 1, envs: { <env>: <token> } }`. Deliberately NOT derived from\n * `infraKitConfigObject`: this file is not a merge layer, so `.strict()` here is free (a typo is a\n * loud error in three readers, not a bricked CLI).\n */\n// HAND-AUTHORABILITY IS THE CONSTRAINT THIS SHAPE IS CHOSEN FOR, and two earlier fields were\n// dropped for it \u2014 do not re-add either without re-deciding that:\n//\n// - `version` is OPTIONAL (defaulted to 1) because a store typed by a human carries no version\n// key, and a store the CLI refuses to read is a store the human cannot write.\n// - `repoRoot` is GONE, and with it the refusal that compared it against the current checkout.\n// That guard was real: the store directory is keyed on `path.basename(mainRepoRoot)`, so\n// `~/work/api` and `~/oss/api` share ONE token directory and repo B could load (and then\n// overwrite) repo A's credentials. It was removed anyway, knowingly: it also made the CLI refuse\n// every hand-written store, which carries no such field. Hand-authorability wins; the\n// basename-collision risk is ACCEPTED. A weakened version of the guard (warn-and-continue, or\n// \"only check when present\") is NOT the compromise here \u2014 it would re-introduce the same refusal\n// for anyone who did write the field. If you want the guard back, key the store directory on\n// something collision-free instead.\nconst tokenStoreSchema = z\n .object({\n version: z.literal(TOKEN_STORE_VERSION).default(TOKEN_STORE_VERSION),\n envs: z.record(z.string().min(1), z.string().min(1)),\n })\n .strict()\n\n/** The parsed `tokens.json` shape: `{ version, envs }` \u2014 `version` defaulted to 1 when absent on disk. */\nexport type TokenStore = z.infer<typeof tokenStoreSchema>\n\n/**\n * Absolute path to the per-project token store: `~/.infra-kit/projects/<repo>/tokens.json`.\n *\n * Derived from {@link getInfraKitConfigPaths} rather than re-deriving the home/projects/repo chain \u2014\n * that resolver already keys the directory off the MAIN repo root (so every worktree of a repo shares\n * one store) and memoizes the two `git rev-parse` spawns.\n *\n * @example\n * await getTokenStorePath() // => '/Users/arthur/.infra-kit/projects/infra-kit/tokens.json'\n */\nexport const getTokenStorePath = async (): Promise<string> => {\n const paths = await getInfraKitConfigPaths()\n\n return path.join(path.dirname(paths.userProject), TOKEN_STORE_FILE)\n}\n\n/**\n * Read the token store, or `null` when it does not exist.\n *\n * ABSENCE IS NOT AN ERROR \u2014 a fresh machine has no store, and every caller must be able to say \"no\n * tokens yet\" without a try/catch. The two things that DO throw are malformed JSON and a schema\n * mismatch: both are states only a human can fix, so both are loud.\n *\n * @example\n * const store = await readTokenStore()\n * store?.envs.dev // => 'dp.st.dev.xxxx' | undefined\n * // no file on disk => null\n * // garbage on disk => throws 'Invalid token store at ~/.infra-kit/projects/api/tokens.json: \u2026'\n */\nexport const readTokenStore = async (): Promise<TokenStore | null> => {\n const storePath = await getTokenStorePath()\n\n let raw: string\n\n try {\n raw = await fs.readFile(storePath, 'utf-8')\n } catch {\n return null\n }\n\n // Every refusal below is an EnvAuthError, not a plain Error. A broken store is the MOST durable\n // env-auth failure there is \u2014 it cannot heal on a 30s retry, only a human can fix it \u2014 and env\n // auto-load's sticky marker is keyed on that CLASS. Thrown as a plain Error these were classified\n // TRANSIENT, so a corrupt store went silent on the backgrounded shell-startup spawn and the user's\n // env simply stopped loading with nothing ever said. `env` is null: no ONE environment is at fault.\n let parsedRaw: unknown\n\n try {\n parsedRaw = JSON.parse(raw)\n } catch (err) {\n throw new EnvAuthError(\n `Invalid JSON in the token store at ${tildify(storePath)}: ${(err as Error).message}\\n` +\n `Fix or delete the file, then re-add the token with \\`infra-kit env-token-set <env>\\`.`,\n )\n }\n\n const result = tokenStoreSchema.safeParse(parsedRaw)\n\n if (!result.success) {\n throw new EnvAuthError(\n `Invalid token store at ${tildify(storePath)}: ${z.prettifyError(result.error)}\\n` +\n `Fix or delete the file, then re-add the token with \\`infra-kit env-token-set <env>\\`.`,\n )\n }\n\n return result.data\n}\n\n/**\n * Persist the store atomically at 0600, tightening every directory on the way to it to 0700.\n *\n * The chmods are not belt-and-braces: the config bootstrap already seeds\n * `~/.infra-kit/projects/<repo>/` at the default umask (0755), so an EXISTING directory is\n * world-readable and `mkdir`'s mode argument is a no-op for it. The same holds for the file \u2014 a\n * hand-written `tokens.json` is 0644 \u2014 so the modes of the whole credential path are (re)asserted on\n * EVERY write, not only on the writes that created it. Self-healing by construction: a plain\n * `env-token-set` is enough to fix a store a human typed.\n *\n * @example\n * await writeTokenStore({ version: 1, envs: { dev: 'dp.st.dev.x' } })\n * // ~/.infra-kit/projects/api/tokens.json -rw-------\n * // ~/.infra-kit/projects/api/ drwx------\n */\nexport const writeTokenStore = async (store: TokenStore): Promise<void> => {\n const storePath = await getTokenStorePath()\n\n const projectDir = path.dirname(storePath)\n const projectsDir = path.dirname(projectDir)\n const userConfigDir = path.dirname(projectsDir)\n\n for (const dir of [userConfigDir, projectsDir, projectDir]) {\n await fs.mkdir(dir, { recursive: true, mode: TOKEN_DIR_MODE })\n await fs.chmod(dir, TOKEN_DIR_MODE)\n }\n\n atomicWriteFileSync(storePath, `${JSON.stringify(store, null, 2)}\\n`, TOKEN_FILE_MODE)\n\n // The rename above already lands a fresh 0600 inode, but the mode of a credential file is not a\n // thing to leave to the umask that happened to be in effect \u2014 assert it outright.\n await fs.chmod(storePath, TOKEN_FILE_MODE)\n}\n\n/**\n * Add or replace one environment's token, read-modify-write, stamping the current `version`. Inherits\n * {@link readTokenStore}'s refusals: a corrupt store is never silently overwritten.\n *\n * @example\n * await setToken('dev', 'dp.st.dev.xxxxxxxx')\n * // tokens.json => { version: 1, envs: { dev: 'dp.st.dev.xxxxxxxx' } }\n */\nexport const setToken = async (env: string, token: string): Promise<void> => {\n const existing = await readTokenStore()\n\n await writeTokenStore({\n version: TOKEN_STORE_VERSION,\n envs: { ...existing?.envs, [env]: token },\n })\n}\n\n/**\n * Drop one environment's token. A no-op when the store (or the key) is absent \u2014 removing something\n * that is not there is the state the caller asked for.\n *\n * Removing a token here does NOT revoke it in Doppler; callers say so.\n *\n * @example\n * await removeToken('dev')\n * // tokens.json => { version: 1, envs: {} }\n */\nexport const removeToken = async (env: string): Promise<void> => {\n const existing = await readTokenStore()\n\n if (!existing || !(env in existing.envs)) return\n\n const envs = { ...existing.envs }\n\n delete envs[env]\n\n await writeTokenStore({\n version: TOKEN_STORE_VERSION,\n envs,\n })\n}\n", "/**\n * The DURABLE, USER-FIXABLE env-auth failure \u2014 as a TYPE rather than a string.\n *\n * Throw this (or a subclass) \u2014 never a plain `Error` \u2014 for anything a user must fix before\n * env-load can work: no token minted, a corrupt token store, a foreign token store. Only this\n * class earns the STICKY marker that survives the backgrounded shell-startup spawn and is replayed\n * onto the next interactive command. `DopplerAuthError extends EnvAuthError` for the subset that\n * is specifically \"Doppler refused the token we sent\".\n *\n * @example\n * // no token minted for this env (integrations/doppler/token-resolver.ts)\n * throw new EnvAuthError(buildMissingTokenMessage('dev', storePath), 'dev')\n * @example\n * // the token store itself is unusable \u2014 no single env is at fault, so `env` stays null\n * throw new EnvAuthError(`Invalid JSON in the token store at ${storePath} \u2026`)\n */\n// WHY THIS EXISTS (and why it is deliberately NOT a Doppler type): env auto-load must tell a\n// TRANSIENT failure (network, timeout, Doppler down \u2014 heals itself, 30s backoff, stay quiet) apart\n// from a DURABLE one (never heals, and only a human can fix it). A durable failure misclassified as\n// transient is TOTAL SILENCE, forever, on the one path the whole feature exists to survive.\n//\n// That classification therefore has to travel WITH the error, not be re-derived by grepping a\n// message some other module owns \u2014 and it has to be throwable from EVERY module that can produce\n// the class. `lib/env-tokens` (the store) is a layer BELOW `integrations/doppler`, so a\n// Doppler-owned base type could not be thrown there without a layering inversion and an import\n// cycle. Hence a neutral base here in `lib/errors`.\n//\n// The defect class this closes is \"a durable failure that is not type-detectable\". Adding a new\n// durable failure and forgetting to type it is exactly how this shipped silent twice.\nexport class EnvAuthError extends Error {\n /**\n * The env (= Doppler config) whose token is at fault, or `null` when the failure is not attributable\n * to ONE env (a corrupt or foreign token store breaks every env at once). `null` is honest: do NOT\n * invent an env to fill it \u2014 the consumer names the env it was TRYING to load, which it already\n * knows.\n */\n readonly env: string | null\n\n constructor(message: string, env: string | null = null) {\n super(message)\n this.name = 'EnvAuthError'\n this.env = env\n }\n}\n\n/**\n * The TYPE GUARD every consumer of the auth channel classifies on \u2014 the single question \"is this a\n * durable, user-fixable env-auth failure?\", answered by the error's CLASS and never by its text.\n *\n * This is the default `isAuthFailure` of `lib/env-autoload`'s `runEnvAutoLoad`. It is deliberately\n * WIDER than `isDopplerAuthError`: that one means \"Doppler refused us\", which is only one of the\n * durable failures \u2014 a missing token never reaches Doppler at all, and a corrupt store never reaches\n * the resolver.\n *\n * @example\n * isEnvAuthFailure(new EnvAuthError('No Doppler service token for env \"dev\".', 'dev')) // => true\n * isEnvAuthFailure(new DopplerAuthError('dev', 'revoked')) // => true (subclass)\n * isEnvAuthFailure(new Error('connect ETIMEDOUT')) // => false (transient)\n */\nexport const isEnvAuthFailure = (error: unknown): error is EnvAuthError => {\n return error instanceof EnvAuthError\n}\n", "import fs from 'node:fs/promises'\nimport os from 'node:os'\n\n/**\n * Shared path-display helpers for CLI output. Extracted from `commands/config`\n * and `commands/init/migrate-config` (which each had private copies) so config\n * surfaces print paths consistently. Keep this dependency-light (node builtins\n * only) \u2014 it is imported by several commands.\n */\n\n/**\n * Resolve whether a file is reachable, suppressing ENOENT (and any access error)\n * into a boolean. Used to render `[\u2713]`/`[ ]` existence markers.\n *\n * @example\n * await fileExists('/etc/hosts') // => true\n * await fileExists('/nope.txt') // => false\n */\nexport const fileExists = async (filePath: string): Promise<boolean> => {\n try {\n await fs.access(filePath)\n\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Replace the user's home prefix with `~` so logged paths stay short and portable\n * across machines. Leaves non-home paths untouched.\n *\n * @example\n * // os.homedir() === '/Users/arthur'\n * tildify('/Users/arthur/.infra-kit/infra-kit.json') // => '~/.infra-kit/infra-kit.json'\n * tildify('/etc/hosts') // => '/etc/hosts'\n */\nexport const tildify = (filePath: string): string => {\n const home = os.homedir()\n\n return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath\n}\n", "import { logger } from 'src/lib/logger'\n\ninterface CommandOption {\n flag: string\n value: string | string[] | boolean\n}\n\nconst createCommandEcho = () => {\n let cliPath = ''\n let options: CommandOption[] = []\n let isInteractive = false\n\n return {\n /**\n * Bind the echo to the command about to run, and clear the previous one's recording.\n *\n * `cliPath` must be the argv Commander actually parsed (`release create`), which is why the ONLY\n * caller is program.ts's `preAction` hook, feeding it `commandPath(actionCommand)`. Commands used to\n * name themselves here, and that is precisely how the printed line rotted: they passed the flat\n * `release-create`, which stopped being a command the day the flat aliases were dropped, so the\n * \"equivalent command\" we told the user to retype no longer parsed. Commander is the only thing that\n * knows the real path, so it is the only thing allowed to say it.\n */\n start(path: string): void {\n cliPath = path\n options = []\n isInteractive = false\n },\n\n /**\n * Mark that the command had interactive input (prompts)\n * Call this once when ANY prompt happens\n */\n setInteractive(): void {\n isInteractive = true\n },\n\n /**\n * Track an option selection\n *\n * @param flag The CLI flag (e.g., \"--versions\")\n * @param value The selected value\n */\n addOption(flag: string, value: string | string[] | boolean): void {\n options.push({ flag, value })\n },\n\n /**\n * Format the tracked options into a replayable flag string (empty when none).\n */\n formatOptions(): string {\n return options\n .map((opt) => {\n if (typeof opt.value === 'boolean') {\n return opt.value ? opt.flag : ''\n }\n\n if (Array.isArray(opt.value)) {\n return `${opt.flag} \"${opt.value.join(', ')}\"`\n }\n\n return `${opt.flag} \"${opt.value}\"`\n })\n .filter(Boolean)\n .join(' ')\n },\n\n /**\n * The recorded interactive flags for the session shell's report side channel, so the transcript's\n * equivalent line carries the resolved options (`--versions \"1.2.5\"`), not just the bare command.\n * `null` when nothing was recorded \u2014 the caller then falls back to the bare spawned argv.\n */\n snapshot(): { formattedOptions: string } | null {\n if (options.length === 0) {\n return null\n }\n\n return { formattedOptions: this.formatOptions() }\n },\n\n /**\n * Print the equivalent CLI command if there was interactive input.\n *\n * Silent without a bound `cliPath`: the only caller that binds one is Commander's `preAction`, so an\n * unbound echo means the command ran off the CLI (an MCP tool), where a `pnpm exec` line would be\n * nonsense. Printing a path-less `pnpm exec infra-kit --yes` would be worse than printing nothing.\n */\n print(): void {\n if (!isInteractive || options.length === 0 || !cliPath) {\n return\n }\n\n logger.info(`\uD83D\uDCDF Equivalent command: \\npnpm exec infra-kit ${cliPath} ${this.formatOptions()}\\n`)\n },\n\n /**\n * Reset state (useful for testing)\n */\n reset(): void {\n cliPath = ''\n options = []\n isInteractive = false\n },\n }\n}\n\n// Singleton instance (same pattern as logger)\nexport const commandEcho = createCommandEcho()\n", "import type { Buffer } from 'node:buffer'\nimport process from 'node:process'\n\nimport { isMcpMode } from 'src/lib/mcp-mode'\n\nimport { acquireStdin, releaseStdin } from './stdin-ref'\n\n/**\n * Structural mirror of `@inquirer/type`'s `Context` \u2014 the second argument every\n * `@inquirer/*` prompt takes. Declared here rather than imported because\n * `@inquirer/core` and `@inquirer/type` are transitive dependencies of this\n * package, not direct ones (only the four prompt packages are).\n */\nexport interface PromptContext {\n input?: NodeJS.ReadableStream\n output?: NodeJS.WritableStream\n clearPromptOnDone?: boolean\n signal?: AbortSignal\n}\n\nconst ESC_BYTE = 0x1b\n\n/**\n * Run an `@inquirer/*` prompt with Esc bound to cancellation. On Esc the prompt rejects with an\n * `AbortPromptError`, which `entry/cli.ts` already catches at the top level -> \"Operation\n * cancelled.\" -> exit 0.\n *\n * The listener attaches only on a real interactive terminal. `isTTY` alone is NOT a sufficient\n * guard \u2014 see lib/mcp-mode for why `stdio: 'inherit'` defeats it.\n */\n// The cancel path is entirely pre-existing plumbing: an `AbortController`'s signal goes into the\n// prompt context, `@inquirer/core` rejects with an `AbortPromptError` when it fires, and that name\n// is already in `CANCELLATION_ERROR_NAMES` (lib/errors/is-prompt-cancellation). All this adds is\n// the thing that calls `abort()`.\n//\n// DETECTION \u2014 a raw `data` listener that aborts iff the chunk is exactly one `0x1b` byte.\n// Deliberately NOT readline `keypress`: that delivers a lone Esc only after its escape-sequence\n// timeout (~523ms measured), and the `escapeCodeTimeout` dial that would shorten it is\n// process-global and first-armer-wins \u2014 a second `emitKeypressEvents` call on `process.stdin` is\n// SILENTLY IGNORED, so the dial degrades to a no-op with no error and no failing test. The raw\n// listener costs ~24ms and touches no global state. Arrow keys (`\\x1b[B`, 3 bytes) and Alt combos\n// (`\\x1bb`, 2 bytes) are ignored because they are LONGER \u2014 no parsed key field is involved. Node\n// `data` listeners broadcast, so this one does not steal bytes from readline: arrows and Enter keep\n// working while it is attached.\n//\n// FALSE POSITIVES are a non-issue: there is no routine source of a lone-`0x1b` chunk. Focus events\n// (`\\x1b[I`/`\\x1b[O`), bracketed paste (`\\x1b[200~`) and mouse reports (`\\x1b[M\u2026`) are all\n// multi-byte, and inquirer enables none of them.\n//\n// FALSE NEGATIVE \u2014 measured and accepted: a COALESCED Esc (`\"\\x1bx\"` arriving in ONE chunk, i.e.\n// Esc immediately followed by another key) is silently dropped and the prompt stays open. The fix\n// would be Ink's 20ms deferred abort \u2014 hold the lone Esc, cancel it if a follow-up chunk lands.\n// That is a logged NON-GOAL; do not pre-build it.\nexport const withEscape = async <T>(run: (context: PromptContext) => Promise<T>, base?: PromptContext): Promise<T> => {\n const controller = new AbortController()\n const context: PromptContext = { ...base, signal: controller.signal }\n\n if (isMcpMode() || !process.stdin.isTTY) return run(context)\n\n const input = process.stdin\n\n const onData = (chunk: Buffer) => {\n if (chunk.length === 1 && chunk[0] === ESC_BYTE) controller.abort()\n }\n\n // STDIN OWNERSHIP \u2014 this is the seam that keeps stdin alive for inquirer, which refs\n // nothing itself. It must sit BELOW the early return above: a non-TTY run never reads\n // stdin, so acquiring there would ref a handle nobody reads and hang the process. See\n // lib/prompts/stdin-ref for why an unref'd stdin kills a prompt with exit 13.\n acquireStdin()\n\n try {\n // Attach after the prompt has built its readline interface. Cheap ordering defence\n // only \u2014 there is no race today: readline attaches its own consumer synchronously\n // and `data` events are async.\n //\n // INSIDE the `try`, both of them: `run` is caller-supplied and `@inquirer/core` runs\n // synchronously up to readline construction, so a SYNCHRONOUS throw here would skip a\n // `finally` placed any lower \u2014 leaking the ref and hanging the CLI, which is the exact\n // failure this module exists to prevent.\n const pending = run(context)\n\n // Explicit, because `on('data')` alone is NOT enough to start the flow: `Readable` resumes on a\n // new `data` listener only when `flowing !== false`, and the palette's teardown now pauses stdin\n // (tui/boot.tsx) so a prompt opened after an Ink screen inherits `flowing === false`. Esc would be\n // silently dead. It happens to work anyway today \u2014 `run()` above builds inquirer's readline, whose\n // constructor resumes the stream \u2014 but that is an undocumented side effect of a TRANSITIVE\n // dependency, and the ordering it relies on is the very thing the comment below reserves the right\n // to change. One idempotent call is cheaper than that coupling.\n if (input.isPaused()) input.resume()\n\n input.on('data', onData)\n\n return await pending\n } finally {\n // Non-negotiable: the dev wizard runs ~5 prompts back to back, so a leaked\n // listener would abort the next one.\n input.removeListener('data', onData)\n // Same `finally` as the listener teardown, and for the same reason: every exit path\n // (answer, Esc abort, throw) must give the ref back, or node never exits.\n releaseStdin()\n }\n}\n", "/**\n * @fileoverview\n * Shared interactive-confirmation gate for mutating commands.\n *\n * When `confirmedCommand` is truthy (CLI `--yes` or an MCP call, which always\n * injects `confirmedCommand: true`) the prompt is skipped and execution\n * proceeds. Otherwise it prompts the user, marks the echo as interactive, and\n * exits cleanly if the user declines.\n *\n * Esc aborts the prompt (via `withEscape`) rather than answering it \u2014 the caller\n * sees an `AbortPromptError`, which every command's boundary already treats as a\n * clean back-out through `isPromptCancellation`. That is deliberately NOT the same\n * as declining: a decline is an answer of \"no\" and takes the `process.exit(0)` path\n * below, whereas Esc means \"I never answered\".\n *\n * Unlike every other prompt in the CLI this one renders to STDOUT, not stderr \u2014\n * left as-is, since changing it would move confirmation text out of the stream\n * callers have always read it from.\n *\n * The `process.exit(0)` only runs on the interactive-decline path, which is\n * unreachable under MCP \u2014 so this stays safe for the long-lived MCP server.\n *\n * Callers remain responsible for their own `commandEcho.addOption('--yes', \u2026)`\n * bookkeeping, which varies between commands.\n */\nimport confirm from '@inquirer/confirm'\nimport process from 'node:process'\n\nimport { CommandDeclinedError } from 'src/lib/errors/command-declined-error'\nimport { logger } from 'src/lib/logger'\nimport { withEscape } from 'src/lib/prompts/escapable-context'\n\nimport { commandEcho } from './command-echo'\n\nexport interface ConfirmOrExitOptions {\n /**\n * Throw {@link CommandDeclinedError} on a decline instead of `process.exit(0)`.\n *\n * **Defaults to false, and must stay that way.** Seven commands call this\n * helper; flipping the default would change the exit semantics of all of them\n * at once, inside a change whose tests cover none of them.\n *\n * Opt in when the command has already acquired something that needs releasing\n * by the time it prompts \u2014 `process.exit` skips every `finally`, so for those\n * commands the decline path is precisely where a resource leaks. The caller\n * owns catching it and exiting 0: a decline is a successful outcome.\n */\n throwOnDecline?: boolean\n}\n\nexport const confirmOrExit = async (\n confirmedCommand: boolean | undefined,\n message: string,\n options: ConfirmOrExitOptions = {},\n): Promise<void> => {\n const answer = confirmedCommand\n ? true\n : await withEscape((context) => {\n return confirm({ message }, context)\n })\n\n if (!confirmedCommand) {\n commandEcho.setInteractive()\n }\n\n if (!answer) {\n if (options.throwOnDecline) {\n throw new CommandDeclinedError()\n }\n\n logger.info('Operation cancelled. Exiting...')\n process.exit(0)\n }\n}\n", "/**\n * The operator answered \"no\" at a confirmation prompt.\n *\n * Deliberately NOT an {@link OperationError}: nothing failed. It is also not a prompt\n * cancellation \u2014 Esc means \"I never answered\" and has its own handling, while this means\n * \"I answered, and the answer was no.\" Callers that opt in via `throwOnDecline` are\n * responsible for catching it and exiting 0.\n */\n// It exists so a command that acquired a resource before prompting \u2014 a scratch worktree, a temp\n// directory, a lock \u2014 can unwind through its own `finally` blocks and release it, instead of\n// `confirmOrExit` calling `process.exit(0)` and skipping every one of them.\n//\n// That distinction is load-bearing rather than stylistic. In a plan -> confirm -> apply design,\n// declining is a first-class expected outcome, not an edge case, so the most ordinary non-happy\n// path is exactly the one that would leak.\nexport class CommandDeclinedError extends Error {\n constructor(message = 'Operation cancelled by the operator') {\n super(message)\n this.name = 'CommandDeclinedError'\n }\n}\n\n/** Narrow an unknown catch binding to a decline. */\nexport const isCommandDeclined = (error: unknown): error is CommandDeclinedError => {\n return error instanceof CommandDeclinedError\n}\n"],
|
|
5
|
-
"mappings": "
|
|
6
|
-
"names": ["fs", "path", "z", "EnvAuthError", "message", "env", "isEnvAuthFailure", "error", "fs", "os", "fileExists", "filePath", "tildify", "home", "TOKEN_STORE_FILE", "TOKEN_STORE_VERSION", "TOKEN_FILE_MODE", "TOKEN_DIR_MODE", "tokenStoreSchema", "z", "getTokenStorePath", "paths", "getInfraKitConfigPaths", "path", "readTokenStore", "storePath", "raw", "fs", "parsedRaw", "err", "EnvAuthError", "tildify", "result", "writeTokenStore", "store", "projectDir", "projectsDir", "userConfigDir", "dir", "atomicWriteFileSync", "setToken", "env", "token", "existing", "removeToken", "envs", "createCommandEcho", "cliPath", "options", "isInteractive", "path", "flag", "value", "opt", "logger", "commandEcho", "process", "ESC_BYTE", "withEscape", "run", "base", "controller", "context", "isMcpMode", "process", "input", "onData", "chunk", "acquireStdin", "pending", "releaseStdin", "confirm", "process", "CommandDeclinedError", "message", "isCommandDeclined", "error", "confirmOrExit", "confirmedCommand", "message", "options", "answer", "withEscape", "context", "confirm", "commandEcho", "CommandDeclinedError", "logger", "process"]
|
|
4
|
+
"sourcesContent": ["// DEFAULT import, deliberately \u2014 the same reason config-bootstrap.ts documents: the module is used as\n// `fs.readFile(...)` / `fs.mkdir(...)` so every call is a property lookup on the live module object,\n// which is what `vi.spyOn(fs, \u2026)` patches. A named import (`import { readFile }`) is bound at import\n// time and the spy silently does NOT intercept it \u2014 the assertion goes green while the real call\n// still hits disk. Do not \"modernize\" this import.\nimport fs from 'node:fs/promises'\nimport path from 'node:path'\nimport { z } from 'zod'\n\nimport { atomicWriteFileSync } from 'src/lib/constants'\nimport { EnvAuthError } from 'src/lib/errors/env-auth-error'\nimport { getInfraKitConfigPaths } from 'src/lib/infra-kit-config'\nimport { tildify } from 'src/lib/path-display'\n\n/**\n * Basename of the token store. A SIBLING of the layer-3 `infra-kit.json`, never a key inside it:\n * `getInfraKitConfig` only ever globs the three hardcoded `infra-kit.json` paths and its schema is\n * `.strict()`, so an unknown key throws at the LAYER parse and bricks every command on every pinned\n * version. A sibling file is structurally invisible to that loader \u2014 `config-bootstrap.ts` already\n * relies on exactly this for its `.example.jsonc`. That invisibility is the blast-radius guarantee:\n * a corrupt `tokens.json` can only break the readers below, never `worktrees-list`.\n */\nconst TOKEN_STORE_FILE = 'tokens.json'\n\n/** Schema version of the on-disk store. Bump only alongside a migration. */\nconst TOKEN_STORE_VERSION = 1\n\n/** Mode of `tokens.json` itself \u2014 owner read/write only. It holds live credentials. */\nconst TOKEN_FILE_MODE = 0o600\n\n/** Mode of every directory on the way to the store \u2014 owner-only traversal. */\nconst TOKEN_DIR_MODE = 0o700\n\n/**\n * The store's OWN schema: `{ version?: 1, envs: { <env>: <token> } }`. Deliberately NOT derived from\n * `infraKitConfigObject`: this file is not a merge layer, so `.strict()` here is free (a typo is a\n * loud error in three readers, not a bricked CLI).\n */\n// HAND-AUTHORABILITY IS THE CONSTRAINT THIS SHAPE IS CHOSEN FOR, and two earlier fields were\n// dropped for it \u2014 do not re-add either without re-deciding that:\n//\n// - `version` is OPTIONAL (defaulted to 1) because a store typed by a human carries no version\n// key, and a store the CLI refuses to read is a store the human cannot write.\n// - `repoRoot` is GONE, and with it the refusal that compared it against the current checkout.\n// That guard was real: the store directory is keyed on `path.basename(mainRepoRoot)`, so\n// `~/work/api` and `~/oss/api` share ONE token directory and repo B could load (and then\n// overwrite) repo A's credentials. It was removed anyway, knowingly: it also made the CLI refuse\n// every hand-written store, which carries no such field. Hand-authorability wins; the\n// basename-collision risk is ACCEPTED. A weakened version of the guard (warn-and-continue, or\n// \"only check when present\") is NOT the compromise here \u2014 it would re-introduce the same refusal\n// for anyone who did write the field. If you want the guard back, key the store directory on\n// something collision-free instead.\nconst tokenStoreSchema = z\n .object({\n version: z.literal(TOKEN_STORE_VERSION).default(TOKEN_STORE_VERSION),\n envs: z.record(z.string().min(1), z.string().min(1)),\n })\n .strict()\n\n/** The parsed `tokens.json` shape: `{ version, envs }` \u2014 `version` defaulted to 1 when absent on disk. */\nexport type TokenStore = z.infer<typeof tokenStoreSchema>\n\n/**\n * Absolute path to the per-project token store: `~/.infra-kit/projects/<repo>/tokens.json`.\n *\n * Derived from {@link getInfraKitConfigPaths} rather than re-deriving the home/projects/repo chain \u2014\n * that resolver already keys the directory off the MAIN repo root (so every worktree of a repo shares\n * one store) and memoizes the two `git rev-parse` spawns.\n *\n * @example\n * await getTokenStorePath() // => '/Users/arthur/.infra-kit/projects/infra-kit/tokens.json'\n */\nexport const getTokenStorePath = async (): Promise<string> => {\n const paths = await getInfraKitConfigPaths()\n\n return path.join(path.dirname(paths.userProject), TOKEN_STORE_FILE)\n}\n\n/**\n * Read the token store, or `null` when it does not exist.\n *\n * ABSENCE IS NOT AN ERROR \u2014 a fresh machine has no store, and every caller must be able to say \"no\n * tokens yet\" without a try/catch. The two things that DO throw are malformed JSON and a schema\n * mismatch: both are states only a human can fix, so both are loud.\n *\n * @example\n * const store = await readTokenStore()\n * store?.envs.dev // => 'dp.st.dev.xxxx' | undefined\n * // no file on disk => null\n * // garbage on disk => throws 'Invalid token store at ~/.infra-kit/projects/api/tokens.json: \u2026'\n */\nexport const readTokenStore = async (): Promise<TokenStore | null> => {\n const storePath = await getTokenStorePath()\n\n let raw: string\n\n try {\n raw = await fs.readFile(storePath, 'utf-8')\n } catch {\n return null\n }\n\n // Every refusal below is an EnvAuthError, not a plain Error. A broken store is the MOST durable\n // env-auth failure there is \u2014 it cannot heal on a 30s retry, only a human can fix it \u2014 and env\n // auto-load's sticky marker is keyed on that CLASS. Thrown as a plain Error these were classified\n // TRANSIENT, so a corrupt store went silent on the backgrounded shell-startup spawn and the user's\n // env simply stopped loading with nothing ever said. `env` is null: no ONE environment is at fault.\n let parsedRaw: unknown\n\n try {\n parsedRaw = JSON.parse(raw)\n } catch (err) {\n throw new EnvAuthError(\n `Invalid JSON in the token store at ${tildify(storePath)}: ${(err as Error).message}\\n` +\n `Fix or delete the file, then re-add the token with \\`infra-kit env-token-set <env>\\`.`,\n )\n }\n\n const result = tokenStoreSchema.safeParse(parsedRaw)\n\n if (!result.success) {\n throw new EnvAuthError(\n `Invalid token store at ${tildify(storePath)}: ${z.prettifyError(result.error)}\\n` +\n `Fix or delete the file, then re-add the token with \\`infra-kit env-token-set <env>\\`.`,\n )\n }\n\n return result.data\n}\n\n/**\n * Persist the store atomically at 0600, tightening every directory on the way to it to 0700.\n *\n * The chmods are not belt-and-braces: the config bootstrap already seeds\n * `~/.infra-kit/projects/<repo>/` at the default umask (0755), so an EXISTING directory is\n * world-readable and `mkdir`'s mode argument is a no-op for it. The same holds for the file \u2014 a\n * hand-written `tokens.json` is 0644 \u2014 so the modes of the whole credential path are (re)asserted on\n * EVERY write, not only on the writes that created it. Self-healing by construction: a plain\n * `env-token-set` is enough to fix a store a human typed.\n *\n * @example\n * await writeTokenStore({ version: 1, envs: { dev: 'dp.st.dev.x' } })\n * // ~/.infra-kit/projects/api/tokens.json -rw-------\n * // ~/.infra-kit/projects/api/ drwx------\n */\nexport const writeTokenStore = async (store: TokenStore): Promise<void> => {\n const storePath = await getTokenStorePath()\n\n const projectDir = path.dirname(storePath)\n const projectsDir = path.dirname(projectDir)\n const userConfigDir = path.dirname(projectsDir)\n\n for (const dir of [userConfigDir, projectsDir, projectDir]) {\n await fs.mkdir(dir, { recursive: true, mode: TOKEN_DIR_MODE })\n await fs.chmod(dir, TOKEN_DIR_MODE)\n }\n\n atomicWriteFileSync(storePath, `${JSON.stringify(store, null, 2)}\\n`, TOKEN_FILE_MODE)\n\n // The rename above already lands a fresh 0600 inode, but the mode of a credential file is not a\n // thing to leave to the umask that happened to be in effect \u2014 assert it outright.\n await fs.chmod(storePath, TOKEN_FILE_MODE)\n}\n\n/**\n * Add or replace one environment's token, read-modify-write, stamping the current `version`. Inherits\n * {@link readTokenStore}'s refusals: a corrupt store is never silently overwritten.\n *\n * @example\n * await setToken('dev', 'dp.st.dev.xxxxxxxx')\n * // tokens.json => { version: 1, envs: { dev: 'dp.st.dev.xxxxxxxx' } }\n */\nexport const setToken = async (env: string, token: string): Promise<void> => {\n const existing = await readTokenStore()\n\n await writeTokenStore({\n version: TOKEN_STORE_VERSION,\n envs: { ...existing?.envs, [env]: token },\n })\n}\n\n/**\n * Drop one environment's token. A no-op when the store (or the key) is absent \u2014 removing something\n * that is not there is the state the caller asked for.\n *\n * Removing a token here does NOT revoke it in Doppler; callers say so.\n *\n * @example\n * await removeToken('dev')\n * // tokens.json => { version: 1, envs: {} }\n */\nexport const removeToken = async (env: string): Promise<void> => {\n const existing = await readTokenStore()\n\n if (!existing || !(env in existing.envs)) return\n\n const envs = { ...existing.envs }\n\n delete envs[env]\n\n await writeTokenStore({\n version: TOKEN_STORE_VERSION,\n envs,\n })\n}\n", "/**\n * The DURABLE, USER-FIXABLE env-auth failure \u2014 as a TYPE rather than a string.\n *\n * Throw this (or a subclass) \u2014 never a plain `Error` \u2014 for anything a user must fix before\n * env-load can work: no token minted, a corrupt token store, a foreign token store. Only this\n * class earns the STICKY marker that survives the backgrounded shell-startup spawn and is replayed\n * onto the next interactive command. `DopplerAuthError extends EnvAuthError` for the subset that\n * is specifically \"Doppler refused the token we sent\".\n *\n * @example\n * // no token minted for this env (integrations/doppler/token-resolver.ts)\n * throw new EnvAuthError(buildMissingTokenMessage('dev', storePath), 'dev')\n * @example\n * // the token store itself is unusable \u2014 no single env is at fault, so `env` stays null\n * throw new EnvAuthError(`Invalid JSON in the token store at ${storePath} \u2026`)\n */\n// WHY THIS EXISTS (and why it is deliberately NOT a Doppler type): env auto-load must tell a\n// TRANSIENT failure (network, timeout, Doppler down \u2014 heals itself, 30s backoff, stay quiet) apart\n// from a DURABLE one (never heals, and only a human can fix it). A durable failure misclassified as\n// transient is TOTAL SILENCE, forever, on the one path the whole feature exists to survive.\n//\n// That classification therefore has to travel WITH the error, not be re-derived by grepping a\n// message some other module owns \u2014 and it has to be throwable from EVERY module that can produce\n// the class. `lib/env-tokens` (the store) is a layer BELOW `integrations/doppler`, so a\n// Doppler-owned base type could not be thrown there without a layering inversion and an import\n// cycle. Hence a neutral base here in `lib/errors`.\n//\n// The defect class this closes is \"a durable failure that is not type-detectable\". Adding a new\n// durable failure and forgetting to type it is exactly how this shipped silent twice.\nexport class EnvAuthError extends Error {\n /**\n * The env (= Doppler config) whose token is at fault, or `null` when the failure is not attributable\n * to ONE env (a corrupt or foreign token store breaks every env at once). `null` is honest: do NOT\n * invent an env to fill it \u2014 the consumer names the env it was TRYING to load, which it already\n * knows.\n */\n readonly env: string | null\n\n constructor(message: string, env: string | null = null) {\n super(message)\n this.name = 'EnvAuthError'\n this.env = env\n }\n}\n\n/**\n * The TYPE GUARD every consumer of the auth channel classifies on \u2014 the single question \"is this a\n * durable, user-fixable env-auth failure?\", answered by the error's CLASS and never by its text.\n *\n * This is the default `isAuthFailure` of `lib/env-autoload`'s `runEnvAutoLoad`. It is deliberately\n * WIDER than `isDopplerAuthError`: that one means \"Doppler refused us\", which is only one of the\n * durable failures \u2014 a missing token never reaches Doppler at all, and a corrupt store never reaches\n * the resolver.\n *\n * @example\n * isEnvAuthFailure(new EnvAuthError('No Doppler service token for env \"dev\".', 'dev')) // => true\n * isEnvAuthFailure(new DopplerAuthError('dev', 'revoked')) // => true (subclass)\n * isEnvAuthFailure(new Error('connect ETIMEDOUT')) // => false (transient)\n */\nexport const isEnvAuthFailure = (error: unknown): error is EnvAuthError => {\n return error instanceof EnvAuthError\n}\n", "import fs from 'node:fs/promises'\nimport os from 'node:os'\n\n/**\n * Shared path-display helpers for CLI output. Extracted from `commands/config`\n * and `commands/init/migrate-config` (which each had private copies) so config\n * surfaces print paths consistently. Keep this dependency-light (node builtins\n * only) \u2014 it is imported by several commands.\n */\n\n/**\n * Resolve whether a file is reachable, suppressing ENOENT (and any access error)\n * into a boolean. Used to render `[\u2713]`/`[ ]` existence markers.\n *\n * @example\n * await fileExists('/etc/hosts') // => true\n * await fileExists('/nope.txt') // => false\n */\nexport const fileExists = async (filePath: string): Promise<boolean> => {\n try {\n await fs.access(filePath)\n\n return true\n } catch {\n return false\n }\n}\n\n/**\n * Replace the user's home prefix with `~` so logged paths stay short and portable\n * across machines. Leaves non-home paths untouched.\n *\n * @example\n * // os.homedir() === '/Users/arthur'\n * tildify('/Users/arthur/.infra-kit/infra-kit.json') // => '~/.infra-kit/infra-kit.json'\n * tildify('/etc/hosts') // => '/etc/hosts'\n */\nexport const tildify = (filePath: string): string => {\n const home = os.homedir()\n\n return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath\n}\n", "import { logger } from 'src/lib/logger'\n\ninterface CommandOption {\n flag: string\n value: string | string[] | boolean\n}\n\nconst createCommandEcho = () => {\n let cliPath = ''\n let options: CommandOption[] = []\n let isInteractive = false\n\n return {\n /**\n * Bind the echo to the command about to run, and clear the previous one's recording.\n *\n * `cliPath` must be the argv Commander actually parsed (`release create`), which is why the ONLY\n * caller is program.ts's `preAction` hook, feeding it `commandPath(actionCommand)`. Commands used to\n * name themselves here, and that is precisely how the printed line rotted: they passed the flat\n * `release-create`, which stopped being a command the day the flat aliases were dropped, so the\n * \"equivalent command\" we told the user to retype no longer parsed. Commander is the only thing that\n * knows the real path, so it is the only thing allowed to say it.\n */\n start(path: string): void {\n cliPath = path\n options = []\n isInteractive = false\n },\n\n /**\n * Mark that the command had interactive input (prompts)\n * Call this once when ANY prompt happens\n */\n setInteractive(): void {\n isInteractive = true\n },\n\n /**\n * Track an option selection\n *\n * @param flag The CLI flag (e.g., \"--versions\")\n * @param value The selected value\n */\n addOption(flag: string, value: string | string[] | boolean): void {\n options.push({ flag, value })\n },\n\n /**\n * Format the tracked options into a replayable flag string (empty when none).\n */\n formatOptions(): string {\n return options\n .map((opt) => {\n if (typeof opt.value === 'boolean') {\n return opt.value ? opt.flag : ''\n }\n\n if (Array.isArray(opt.value)) {\n return `${opt.flag} \"${opt.value.join(', ')}\"`\n }\n\n return `${opt.flag} \"${opt.value}\"`\n })\n .filter(Boolean)\n .join(' ')\n },\n\n /**\n * The recorded interactive flags for the session shell's report side channel, so the transcript's\n * equivalent line carries the resolved options (`--versions \"1.2.5\"`), not just the bare command.\n * `null` when nothing was recorded \u2014 the caller then falls back to the bare spawned argv.\n */\n snapshot(): { formattedOptions: string } | null {\n if (options.length === 0) {\n return null\n }\n\n return { formattedOptions: this.formatOptions() }\n },\n\n /**\n * Print the equivalent CLI command if there was interactive input.\n *\n * Silent without a bound `cliPath`: the only caller that binds one is Commander's `preAction`, so an\n * unbound echo means the command ran off the CLI (an MCP tool), where a `pnpm exec` line would be\n * nonsense. Printing a path-less `pnpm exec infra-kit --yes` would be worse than printing nothing.\n */\n print(): void {\n if (!isInteractive || options.length === 0 || !cliPath) {\n return\n }\n\n logger.info(`\uD83D\uDCDF Equivalent command: \\npnpm exec infra-kit ${cliPath} ${this.formatOptions()}\\n`)\n },\n\n /**\n * Reset state (useful for testing)\n */\n reset(): void {\n cliPath = ''\n options = []\n isInteractive = false\n },\n }\n}\n\n// Singleton instance (same pattern as logger)\nexport const commandEcho = createCommandEcho()\n", "import type { Buffer } from 'node:buffer'\nimport process from 'node:process'\n\nimport { OperationError } from 'src/lib/errors/operation-error'\nimport { isMcpMode } from 'src/lib/mcp-mode'\n\nimport { acquireStdin, releaseStdin } from './stdin-ref'\n\n/**\n * Structural mirror of `@inquirer/type`'s `Context` \u2014 the second argument every\n * `@inquirer/*` prompt takes. Declared here rather than imported because\n * `@inquirer/core` and `@inquirer/type` are transitive dependencies of this\n * package, not direct ones (only the four prompt packages are).\n */\nexport interface PromptContext {\n input?: NodeJS.ReadableStream\n output?: NodeJS.WritableStream\n clearPromptOnDone?: boolean\n signal?: AbortSignal\n}\n\n/**\n * What this call site does when there is no human: refuse, assert the path is blocked upstream, or\n * answer with a fixed value.\n *\n * `'unreachable'` is a CLAIM about the owning tool's MCP schema \u2014 \"a required field stops an agent\n * reaching this prompt at all\" \u2014 and it is checkable, which `'refuse'` is not. Reaching it at\n * runtime means the claim is false, so it throws rather than degrading quietly.\n */\nexport type HeadlessPolicy<T> = 'refuse' | 'unreachable' | { value: T }\n\n/** {@link withEscape}'s options: the prompt context, plus this site's headless policy. */\nexport type EscapeOptions<T> = PromptContext & { whenHeadless?: HeadlessPolicy<T> }\n\nconst ESC_BYTE = 0x1b\n\n// Widening the existing options object rather than adding a third parameter: six call sites already\n// pass a `PromptContext` (`entry/cli.ts`, `env-load`, `env-token-set`, and the wizard's shared\n// context used by three prompts), all of them `{ output: process.stderr }` for documented reasons \u2014\n// `env-token-set.ts:64-68` notes that some env commands run inside `$(\u2026)`, where a prompt on stdout\n// would leave the user typing a credential into a captured stream. A third parameter would have\n// forced every one of them to grow a positional `undefined`.\n//\n// Optional, and the default is `'refuse'`. That default is safe for the same reason the guard exists\n// at all: under `isMcpMode()` `process.stdin` IS the JSON-RPC transport, so no prompt at any site can\n// return a usable answer, and refusing removes no capability that exists. It is what CLI-only sites\n// (the palette, the dev wizard, `env-token-set`) keep, because they face a real human.\n//\n// It is NOT what an MCP-reachable site may keep. There, omitting `whenHeadless` is indistinguishable\n// from never having considered the question, so G7 (`every-inquirer-site-is-escapable`) requires the\n// answer to be written out \u2014 `'refuse'` included.\n//\n// What the default is NOT is *correct* everywhere \u2014 that distinction is the whole point of the\n// parameter. `worktrees-add` documents a `false` fallback for MCP in its own schema, so a refusal\n// there is right-outcome-by-accident at best and a regression at worst. The type cannot tell those\n// apart; G6 (description-to-policy) and G8 (schema-to-`'unreachable'`) are what check the answers,\n// because a wrong answer here compiles and can never fail a test on its own.\nconst resolveHeadless = <T>(policy: HeadlessPolicy<T>): T => {\n if (typeof policy === 'object') return policy.value\n\n throw new OperationError(undefined, {\n operation: 'interactive prompt',\n remediation:\n policy === 'unreachable'\n ? 'this prompt is declared unreachable under MCP because a required field should block the path \u2014 ' +\n 'reaching it means that field was relaxed without updating the call site'\n : 'pass the value explicitly instead of relying on the prompt \u2014 MCP runs have no human to answer it',\n stderrExcerpt: 'an interactive prompt was reached under MCP, where stdin carries JSON-RPC',\n })\n}\n\n/**\n * Run an `@inquirer/*` prompt with Esc bound to cancellation. On Esc the prompt rejects with an\n * `AbortPromptError`, which `entry/cli.ts` already catches at the top level -> \"Operation\n * cancelled.\" -> exit 0.\n *\n * The listener attaches only on a real interactive terminal. `isTTY` alone is NOT a sufficient\n * guard \u2014 see lib/mcp-mode for why `stdio: 'inherit'` defeats it.\n */\n// The cancel path is entirely pre-existing plumbing: an `AbortController`'s signal goes into the\n// prompt context, `@inquirer/core` rejects with an `AbortPromptError` when it fires, and that name\n// is already in `CANCELLATION_ERROR_NAMES` (lib/errors/is-prompt-cancellation). All this adds is\n// the thing that calls `abort()`.\n//\n// DETECTION \u2014 a raw `data` listener that aborts iff the chunk is exactly one `0x1b` byte.\n// Deliberately NOT readline `keypress`: that delivers a lone Esc only after its escape-sequence\n// timeout (~523ms measured), and the `escapeCodeTimeout` dial that would shorten it is\n// process-global and first-armer-wins \u2014 a second `emitKeypressEvents` call on `process.stdin` is\n// SILENTLY IGNORED, so the dial degrades to a no-op with no error and no failing test. The raw\n// listener costs ~24ms and touches no global state. Arrow keys (`\\x1b[B`, 3 bytes) and Alt combos\n// (`\\x1bb`, 2 bytes) are ignored because they are LONGER \u2014 no parsed key field is involved. Node\n// `data` listeners broadcast, so this one does not steal bytes from readline: arrows and Enter keep\n// working while it is attached.\n//\n// FALSE POSITIVES are a non-issue: there is no routine source of a lone-`0x1b` chunk. Focus events\n// (`\\x1b[I`/`\\x1b[O`), bracketed paste (`\\x1b[200~`) and mouse reports (`\\x1b[M\u2026`) are all\n// multi-byte, and inquirer enables none of them.\n//\n// FALSE NEGATIVE \u2014 measured and accepted: a COALESCED Esc (`\"\\x1bx\"` arriving in ONE chunk, i.e.\n// Esc immediately followed by another key) is silently dropped and the prompt stays open. The fix\n// would be Ink's 20ms deferred abort \u2014 hold the lone Esc, cancel it if a follow-up chunk lands.\n// That is a logged NON-GOAL; do not pre-build it.\nexport const withEscape = async <T>(\n run: (context: PromptContext) => Promise<T>,\n base?: EscapeOptions<T>,\n): Promise<T> => {\n const controller = new AbortController()\n // `whenHeadless` is ours, not inquirer's \u2014 split it off so it never reaches a prompt's context.\n const { whenHeadless = 'refuse', ...promptBase } = base ?? {}\n const context: PromptContext = { ...promptBase, signal: controller.signal }\n\n // Keyed on `isMcpMode()` alone. `!isTTY` deliberately does NOT ride along: a piped-but-human run\n // (`infra-kit worktrees add > log.txt`) has no TTY and still deserves its prompt, and the two\n // conditions were only ever collapsed here because both merely skipped the Esc listener. Now that\n // this branch decides an ANSWER rather than just a listener, conflating them would refuse prompts\n // no MCP server is waiting on. `release-picker.ts` keeps its own `!isTTY` clause for its own reason.\n if (isMcpMode()) return resolveHeadless(whenHeadless)\n\n if (!process.stdin.isTTY) return run(context)\n\n const input = process.stdin\n\n const onData = (chunk: Buffer) => {\n if (chunk.length === 1 && chunk[0] === ESC_BYTE) controller.abort()\n }\n\n // STDIN OWNERSHIP \u2014 this is the seam that keeps stdin alive for inquirer, which refs\n // nothing itself. It must sit BELOW the early return above: a non-TTY run never reads\n // stdin, so acquiring there would ref a handle nobody reads and hang the process. See\n // lib/prompts/stdin-ref for why an unref'd stdin kills a prompt with exit 13.\n acquireStdin()\n\n try {\n // Attach after the prompt has built its readline interface. Cheap ordering defence\n // only \u2014 there is no race today: readline attaches its own consumer synchronously\n // and `data` events are async.\n //\n // INSIDE the `try`, both of them: `run` is caller-supplied and `@inquirer/core` runs\n // synchronously up to readline construction, so a SYNCHRONOUS throw here would skip a\n // `finally` placed any lower \u2014 leaking the ref and hanging the CLI, which is the exact\n // failure this module exists to prevent.\n const pending = run(context)\n\n // Explicit, because `on('data')` alone is NOT enough to start the flow: `Readable` resumes on a\n // new `data` listener only when `flowing !== false`, and the palette's teardown now pauses stdin\n // (tui/boot.tsx) so a prompt opened after an Ink screen inherits `flowing === false`. Esc would be\n // silently dead. It happens to work anyway today \u2014 `run()` above builds inquirer's readline, whose\n // constructor resumes the stream \u2014 but that is an undocumented side effect of a TRANSITIVE\n // dependency, and the ordering it relies on is the very thing the comment below reserves the right\n // to change. One idempotent call is cheaper than that coupling.\n if (input.isPaused()) input.resume()\n\n input.on('data', onData)\n\n return await pending\n } finally {\n // Non-negotiable: the dev wizard runs ~5 prompts back to back, so a leaked\n // listener would abort the next one.\n input.removeListener('data', onData)\n // Same `finally` as the listener teardown, and for the same reason: every exit path\n // (answer, Esc abort, throw) must give the ref back, or node never exits.\n releaseStdin()\n }\n}\n", "/**\n * @fileoverview\n * Shared interactive-confirmation gate for mutating commands.\n *\n * When `confirmedCommand` is truthy (CLI `--yes` or an MCP call, which always\n * injects `confirmedCommand: true`) the prompt is skipped and execution\n * proceeds. Otherwise it prompts the user, marks the echo as interactive, and\n * exits cleanly if the user declines.\n *\n * Esc aborts the prompt (via `withEscape`) rather than answering it \u2014 the caller\n * sees an `AbortPromptError`, which every command's boundary already treats as a\n * clean back-out through `isPromptCancellation`. That is deliberately NOT the same\n * as declining: a decline is an answer of \"no\" and takes the `process.exit(0)` path\n * below, whereas Esc means \"I never answered\".\n *\n * Unlike every other prompt in the CLI this one renders to STDOUT, not stderr \u2014\n * left as-is, since changing it would move confirmation text out of the stream\n * callers have always read it from.\n *\n * The `process.exit(0)` only runs on the interactive-decline path, which is\n * unreachable under MCP \u2014 so this stays safe for the long-lived MCP server.\n *\n * Callers remain responsible for their own `commandEcho.addOption('--yes', \u2026)`\n * bookkeeping, which varies between commands.\n */\nimport confirm from '@inquirer/confirm'\nimport process from 'node:process'\n\nimport { CommandDeclinedError } from 'src/lib/errors/command-declined-error'\nimport { logger } from 'src/lib/logger'\nimport { withEscape } from 'src/lib/prompts/escapable-context'\n\nimport { commandEcho } from './command-echo'\n\nexport interface ConfirmOrExitOptions {\n /**\n * Throw {@link CommandDeclinedError} on a decline instead of `process.exit(0)`.\n *\n * **Defaults to false, and must stay that way.** Seven commands call this\n * helper; flipping the default would change the exit semantics of all of them\n * at once, inside a change whose tests cover none of them.\n *\n * Opt in when the command has already acquired something that needs releasing\n * by the time it prompts \u2014 `process.exit` skips every `finally`, so for those\n * commands the decline path is precisely where a resource leaks. The caller\n * owns catching it and exiting 0: a decline is a successful outcome.\n */\n throwOnDecline?: boolean\n}\n\nexport const confirmOrExit = async (\n confirmedCommand: boolean | undefined,\n message: string,\n options: ConfirmOrExitOptions = {},\n): Promise<void> => {\n const answer = confirmedCommand\n ? true\n : await withEscape(\n (context) => {\n return confirm({ message }, context)\n },\n // Refuse is the ANSWER, not an oversight: the ternary above short-circuits on `confirmedCommand`,\n // which the MCP chokepoint injects into every call it lets through, and all seven callers pass it.\n { whenHeadless: 'refuse' },\n )\n\n if (!confirmedCommand) {\n commandEcho.setInteractive()\n }\n\n if (!answer) {\n if (options.throwOnDecline) {\n throw new CommandDeclinedError()\n }\n\n logger.info('Operation cancelled. Exiting...')\n process.exit(0)\n }\n}\n", "/**\n * The operator answered \"no\" at a confirmation prompt.\n *\n * Deliberately NOT an {@link OperationError}: nothing failed. It is also not a prompt\n * cancellation \u2014 Esc means \"I never answered\" and has its own handling, while this means\n * \"I answered, and the answer was no.\" Callers that opt in via `throwOnDecline` are\n * responsible for catching it and exiting 0.\n */\n// It exists so a command that acquired a resource before prompting \u2014 a scratch worktree, a temp\n// directory, a lock \u2014 can unwind through its own `finally` blocks and release it, instead of\n// `confirmOrExit` calling `process.exit(0)` and skipping every one of them.\n//\n// That distinction is load-bearing rather than stylistic. In a plan -> confirm -> apply design,\n// declining is a first-class expected outcome, not an edge case, so the most ordinary non-happy\n// path is exactly the one that would leak.\nexport class CommandDeclinedError extends Error {\n constructor(message = 'Operation cancelled by the operator') {\n super(message)\n this.name = 'CommandDeclinedError'\n }\n}\n\n/** Narrow an unknown catch binding to a decline. */\nexport const isCommandDeclined = (error: unknown): error is CommandDeclinedError => {\n return error instanceof CommandDeclinedError\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["fs", "path", "z", "EnvAuthError", "message", "env", "isEnvAuthFailure", "error", "fs", "os", "fileExists", "filePath", "tildify", "home", "TOKEN_STORE_FILE", "TOKEN_STORE_VERSION", "TOKEN_FILE_MODE", "TOKEN_DIR_MODE", "tokenStoreSchema", "z", "getTokenStorePath", "paths", "getInfraKitConfigPaths", "path", "readTokenStore", "storePath", "raw", "fs", "parsedRaw", "err", "EnvAuthError", "tildify", "result", "writeTokenStore", "store", "projectDir", "projectsDir", "userConfigDir", "dir", "atomicWriteFileSync", "setToken", "env", "token", "existing", "removeToken", "envs", "createCommandEcho", "cliPath", "options", "isInteractive", "path", "flag", "value", "opt", "logger", "commandEcho", "process", "ESC_BYTE", "resolveHeadless", "policy", "OperationError", "withEscape", "run", "base", "controller", "whenHeadless", "promptBase", "context", "isMcpMode", "process", "input", "onData", "chunk", "acquireStdin", "pending", "releaseStdin", "confirm", "process", "CommandDeclinedError", "message", "isCommandDeclined", "error", "confirmOrExit", "confirmedCommand", "message", "options", "answer", "withEscape", "context", "confirm", "commandEcho", "CommandDeclinedError", "logger", "process"]
|
|
7
7
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{a as z}from"./chunk-
|
|
1
|
+
import{a as z}from"./chunk-XJOY7J3G.js";import{L as m,X as q,da as H,fa as K,g as U,m as j}from"./chunk-Q4HKBLQG.js";import{execFile as me,execFileSync as de}from"node:child_process";import x from"node:process";var fe=5e3,ge=100,V=2e3,X="/bin/ps",xe=1e3,Y=["-eo","pid=,ppid=,pgid=,lstart="],Z=8<<20,ee=e=>e.split(`
|
|
2
2
|
`).flatMap(r=>{let[t,o,n,...i]=r.trim().split(/\s+/);if(t==null||o==null||n==null)return[];let a=Number(t),c=Number(o),u=Number(n);return!Number.isInteger(a)||!Number.isInteger(c)||!Number.isInteger(u)?[]:[{pid:a,ppid:c,pgid:u,lstart:i.join(" ")}]}),re=e=>{let r=new Map;for(let t of e)t.pid===t.pgid&&r.set(t.pgid,t.lstart);return r},we=(e,r,t)=>{let o=re(r);return oe(e,r,t).flatMap(n=>{let i=o.get(n);return i==null||i===""?[]:[{pgid:n,leaderStart:i}]})},te=(e,r)=>{let t=re(r);return e.flatMap(({pgid:o,leaderStart:n})=>t.get(o)===n?[o]:[])},Q=(e,r)=>te(e,r).flatMap(t=>{try{x.kill(-t,"SIGKILL")}catch{return[]}return[t]}),oe=(e,r,t)=>{let o=new Map;for(let c of r){let u=o.get(c.ppid);u?u.push(c):o.set(c.ppid,[c])}let n=new Set([e]),i=new Set([e]),a=[e];for(let c=0;c<a.length;c+=1)for(let u of o.get(a[c]??-1)??[])i.has(u.pid)||(i.add(u.pid),n.add(u.pgid),a.push(u.pid));return[...n].filter(c=>c>1&&c!==t)},L=()=>{try{return ee(de(X,[...Y],{encoding:"utf8",maxBuffer:Z}))}catch{return[]}},ye=async()=>new Promise(e=>{me(X,[...Y],{encoding:"utf8",maxBuffer:Z},(r,t)=>{e(r?[]:ee(t))})}),Pe=(e,r,t)=>t.some(o=>o.pid===e&&o.ppid===r),A=(e,r)=>{let t=r.find(o=>o.pid===x.pid)?.pgid;return oe(e,r,t)},ne=e=>{try{return x.kill(-e,0),!0}catch{return!1}},I=(e,r)=>{for(let t of e)try{x.kill(-t,r)}catch{}},w=new Set,Se=(e,r,t)=>{let o=A(e,r),n=t.flatMap(i=>te(i,r));return[...new Set([...o,...n])]},se=()=>{let e=L(),r=[...w].map(t=>t());I(Se(x.pid,e,r),"SIGKILL")},be=async e=>new Promise(r=>{setTimeout(r,e)}),N=async(e,r)=>{let t=Date.now()+r,o=e;for(;o.length>0&&Date.now()<t&&(o=o.filter(ne),o.length!==0);)await be(ge);return o};function vr(e,r=fe,t,o=L){e.unref();let n=!1,i=!1,a=[],c=null,u=()=>a;w.add(u);let p=()=>{c&&clearInterval(c),c=null},d=async()=>{await N(a.map(s=>s.pgid),V),w.delete(u)},f=s=>{let l=e.pid;if(l==null||i||n||s.length===0)return;let v=s.find(R=>R.pid===x.pid)?.pgid,h=we(l,s,v);h.length>0&&(a=h)},g=async()=>{e.pid==null||i||n||f(await ye())};f(L()),c=setInterval(()=>{g()},xe),c.unref();let b=s=>{n||t?.(s)};return e.on("exit",(s,l)=>{i=!0,p(),n||(Q(a,o()),d()),b(`exited unexpectedly (code=${s??"null"}, signal=${l??"null"})`)}),e.on("error",s=>{b(`failed to spawn: ${s.message}`)}),{kill:async()=>{n=!0,p();let s=e.pid;if(s==null){w.delete(u);return}if(e.exitCode!==null||e.signalCode!==null){Q(a,o()),await d();return}let l=o();if(l.length>0&&!Pe(s,x.pid,l))return;let v=A(s,l);I(v,"SIGTERM");let h=await N(v,r);if(h.length===0){w.delete(u);return}let R=A(s,o()),J=[...new Set([...h,...R])].filter(ne);I(J,"SIGKILL"),await N(J,V),w.delete(u)}}}import{constants as he}from"node:os";import E from"node:process";var ke=["SIGINT","SIGTERM","SIGHUP"],ve=he.signals,Ee=2,T=e=>128+(ve[e]??Ee),Ce=2e4,Re=e=>{E.exit(e)},Ne=(e,r)=>{E.on(e,r)},Le=(e,r)=>{E.off(e,r)},Ae=(e,r)=>{let t=setTimeout(e,r);return t.unref(),()=>{clearTimeout(t)}},_=e=>{try{E.stderr.write(e)}catch{}},Ie=(e,r)=>{let t=r instanceof Error?`${r.message}
|
|
3
3
|
${r.stack??"(no stack)"}`:String(r);_(`
|
|
4
4
|
\u2717 Teardown failed while handling ${e}: ${t}
|
|
@@ -7,4 +7,4 @@ ${r.stack??"(no stack)"}`:String(r);_(`
|
|
|
7
7
|
`),d?.(),n(),g(),r(T(s));return}p=s,d=a(()=>{let l=`
|
|
8
8
|
\u26A0 Teardown deadline exceeded at stage=${c()} after ${i}ms \u2014 force-quitting.
|
|
9
9
|
`;_(l),u(l),n(),g(),r(T(s))},i),(async()=>{try{await e(s)}catch(l){Ie(s,l)}finally{d?.(),g(),r(T(s))}})()};for(let s of ke){let l=()=>b(s);f.set(s,l),t(s,l)}return g};import Te from"node:process";import{$ as O}from"zx";var _e=async()=>{try{return await O`cmux --version`.quiet(),!0}catch{return!1}},Oe=async e=>{let{cwd:r,title:t,layout:o}=e,n=JSON.stringify(o),a=(await O({env:{...Te.env,CMUX_QUIET:"1"}})`cmux new-workspace --name ${t} --cwd ${r} --focus false --layout ${n}`).stdout;return Me(a)},De=async e=>{try{await O`cmux close-workspace --workspace ${e}`.quiet()}catch(r){m.debug({error:r,ref:e},"cmux: skipped closing dev workspace")}},Me=e=>{let r=e.match(/workspace:\d+/);if(!r)throw new Error("cmux: could not locate workspace ref in new-workspace output");return r[0]};import{$ as Ge}from"zx";import{realpath as $e}from"node:fs/promises";import{$ as ie}from"zx";var C=async e=>{try{return await $e(e)}catch{return e}},D=async()=>{try{let[e,r]=await Promise.all([ie`cmux workspace list --json`.quiet(),ie`cmux workspace-group list --json`.quiet()]),t=JSON.parse(e.stdout).workspaces??[],o=JSON.parse(r.stdout).groups??[],n=new Set(o.map(a=>a.anchor_workspace_ref).filter(a=>typeof a=="string")),i=new Map;for(let a of t)!a.current_directory||n.has(a.ref)||i.set(await C(a.current_directory),a.ref);return i}catch(e){return m.debug({error:e},"cmux: skipped listing workspaces by cwd"),new Map}};var We=async e=>{try{let t=(await D()).get(await C(e));if(!t)return;await Ge`cmux workspace close ${t}`.quiet()}catch(r){m.debug({error:r,cwd:e},"cmux: skipped closing workspace by cwd")}};import{$ as M}from"zx";var ae=async e=>{try{let r=(await M`cmux workspace-group list --json`.quiet()).stdout;return(JSON.parse(r).groups??[]).find(n=>n.name===e)?.ref??null}catch(r){return m.debug({error:r,name:e},"cmux: skipped group lookup"),null}},Be=async(e,r)=>{try{let o=(await M`cmux workspace-group create --name ${e} --from ${r.join(",")}`).stdout.match(/workspace_group:\d+/)?.[0]??await ae(e);return o&&await M`cmux workspace-group pin ${o}`.quiet(),o??null}catch(t){return m.warn({error:t,name:e},"\u26A0\uFE0F cmux: failed to create workspace group"),null}};import{$ as y}from"zx";var Fe=async e=>{let{cwd:r,title:t,group:o}=e,n=H(await K()),i=o?(await y`cmux workspace create --cwd ${r} --group ${o} --group-placement end`).stdout:(await y`cmux workspace create --cwd ${r}`).stdout,a=Ue(i),c=(await y`cmux list-pane-surfaces --workspace ${a}`).stdout,u=Je(c);return await y`cmux new-split right --workspace ${a} --surface ${u}`,n==="three-pane"&&await y`cmux new-split down --workspace ${a} --surface ${u}`,t&&await y`cmux workspace rename --workspace ${a} --title ${t}`,a},Je=e=>{let r=e.match(/surface:\d+/);if(!r)throw new Error("cmux: could not locate initial surface in list-pane-surfaces output");return r[0]},Ue=e=>{let r=e.match(/workspace:\d+/);if(!r)throw new Error("cmux: could not locate workspace ref in workspace create output");return r[0]};var je=e=>{let{branch:r}=e,t=U(r);return t?j(t):r};var qe=new Set(["ExitPromptError","AbortPromptError","PromptCancelledError"]),ce=e=>e instanceof Error&&qe.has(e.name),nt=e=>{if(ce(e))return!0;let r=e?.cause;return ce(r)};import{loadDev as He}from"@slip-stream-kit/config/internal";import Ke from"node:path";var ze=(e,r)=>{let t=new Set;for(let o of Object.values(e))for(let n of Object.keys(q(o,r).proxy))t.add(n);return[...t]},Ve=async(e,r)=>{let t={},o=[];return await Promise.all(r.map(async n=>{try{let a=(await He(Ke.join(e,"apps",n,"ui")))?.proxy?.routes;if(!a)return;t[n]=Object.fromEntries(Object.entries(a).map(([c,u])=>[c,u.packageName]))}catch(i){o.push({app:n,message:i instanceof Error?i.message:String(i)})}})),{routePkg:(n,i)=>t[n]?.[i],loadErrors:o}},ut=async(e,r,t,o)=>{let{routePkg:n,loadErrors:i}=await Ve(e,ze(r,t));return{ctx:{discovered:t,apiPkgByApp:Object.fromEntries(o.map(a=>[a.name,a.packageName])),routePkg:n},loadErrors:i}};import{execFile as Qe}from"node:child_process";import{createHash as Xe}from"node:crypto";import{existsSync as ue,readFileSync as k}from"node:fs";import Ye from"node:http";import Ze from"node:https";import er from"node:net";import{homedir as rr}from"node:os";import{dirname as $,join as P}from"node:path";import S from"node:process";import tr from"node:tls";import{fileURLToPath as or}from"node:url";import{promisify as nr}from"node:util";var sr=nr(Qe),ir=e=>{let r=JSON.parse(k(e,"utf-8")),t=typeof r.bin=="string"?r.bin:r.bin?.portless;return t==null||t===""?null:t},ar=()=>{try{let e=$(or(import.meta.url));for(;;){let r=P(e,"node_modules","portless","package.json");if(ue(r)){let o=ir(r);return o==null?null:P($(r),o)}let t=$(e);if(t===e)return null;e=t}}catch{return null}},G,le=()=>(G===void 0&&(G=ar()),G),cr=/^[\w@%+=:,./-]+$/,ur="'\\''",lr=e=>cr.test(e)?e:`'${e.replaceAll("'",ur)}'`,vt=(e,r)=>{let t=[r.execPath??S.execPath,r.bin,...e];return(r.sudo===!0?"sudo ":"")+t.map(lr).join(" ")},pr=1500,W=1500,mr="x-portless",B="127.0.0.1",dr="localhost",fr="/Library/LaunchDaemons/sh.portless.proxy.plist",gr="/etc/systemd/system/portless.service",xr=(e=ue)=>{try{return S.platform==="darwin"?e(fr)?"yes":"no":S.platform==="linux"?e(gr)?"yes":"no":"unknown"}catch{return"unknown"}},wr=e=>new Promise(r=>{let t=er.connect({host:B,port:e}),o=n=>{t.destroy(),r(n)};t.setTimeout(W),t.once("connect",()=>{o(!0)}),t.once("timeout",()=>{o(!1)}),t.once("error",()=>{o(!1)})}),yr=(e,r)=>new Promise(t=>{let n=(r?Ze.request:Ye.request)({host:B,port:e,method:"HEAD",path:"/",timeout:W,...r?{rejectUnauthorized:!1,servername:dr}:{}},i=>{i.resume(),t(i.headers[mr]==="1")});n.on("error",()=>{t(!1)}),n.on("timeout",()=>{n.destroy(),t(!1)}),n.end()}),Pr=async(e,{timeoutMs:r})=>{let t=le();if(t==null)throw new Error("portless is not installed (not resolvable from node_modules)");await sr(S.execPath,[t,...e],{signal:AbortSignal.timeout(r),encoding:"utf-8",env:z(S.env)})},F=()=>S.env.PORTLESS_STATE_DIR??P(rr(),".portless"),Sr="ca.pem",br="ca.trusted",pe=()=>P(F(),Sr),Et=()=>{try{let e=k(P(F(),br),"utf-8").trim();return e===""?!1:Xe("sha256").update(k(pe())).digest("hex")===e.toLowerCase()}catch{return!1}},Ct=(e,r)=>new Promise(t=>{let o;try{o=k(pe())}catch{t({ok:!1,code:"ENOENT"});return}let n=tr.connect({host:B,port:e,servername:r,ca:[o],rejectUnauthorized:!0},()=>{let i=n.authorizationError,a=n.authorized;n.destroy(),t(a?{ok:!0}:{ok:!1,code:i?.code??i?.message??"UNKNOWN"})});n.setTimeout(W),n.once("timeout",()=>{n.destroy(),t({ok:!1,code:"ETIMEDOUT"})}),n.once("error",i=>{n.destroy(),t({ok:!1,code:i.code??"UNKNOWN"})})}),Rt=()=>{try{let e=JSON.parse(k(P(F(),"routes.json"),"utf-8"));return Array.isArray(e)?e.flatMap(r=>{let{hostname:t,port:o}=r??{};return typeof t!="string"||t===""||typeof o!="number"?[]:[{name:t,port:o}]}):[]}catch{return[]}},Nt=(e={})=>{let r=e.bin===void 0?le():e.bin,t=e.run??Pr,o=e.isListening??wr,n=e.isProxyServing??yr,i=e.isServiceInstalled??xr,a=e.timeoutMs??pr,c=null,u=async s=>{try{return await t(s,{timeoutMs:a}),!0}catch{return!1}},p=async()=>(c??=await u(["--version"]),c);return{binPath:()=>r,isAvailable:p,isProxyServing:async(s,l)=>!await p()||!await o(s)?!1:n(s,l),probeProxy:async(s,l)=>await o(s)?await n(s,l)?"serving":"not-portless":"daemon-down",isServiceInstalled:i,registerAlias:async(s,l)=>await p()?u(["alias",s,String(l)]):!1,removeAlias:async s=>{await p()&&await u(["alias","--remove",s])}}};export{ut as a,ar as b,vt as c,wr as d,yr as e,pe as f,Et as g,Ct as h,Rt as i,Nt as j,se as k,vr as l,T as m,Lr as n,C as o,D as p,We as q,ae as r,Be as s,_e as t,Oe as u,De as v,Fe as w,je as x,nt as y};
|
|
10
|
-
//# sourceMappingURL=chunk-
|
|
10
|
+
//# sourceMappingURL=chunk-Q23IJP6D.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import B from"node:process";import A from"pino";import he from"pino-pretty";var G="/tmp/mcp-infra-kit.log",q=["token","*.token","serviceToken","*.serviceToken","DOPPLER_TOKEN","*.DOPPLER_TOKEN","INFRA_KIT_ENV_TOKEN","*.INFRA_KIT_ENV_TOKEN"],Vt=()=>{let e=B.argv.includes("--debug")?"debug":"info",t=A({level:e,redact:q},A.destination({dest:G}));return t.info(`Logger initialized with level: ${e}. Logging to: ${G}`),t},ye=()=>{let e=B.argv.includes("--debug")?"debug":"info",t=["time","pid","hostname"];return e==="debug"&&t.push("level"),A({level:e,redact:q},he({destination:2,ignore:t.join(","),colorize:!0}))},Ut=ye();import*as p from"node:fs";import*as a from"node:path";function ve(e){let t=e;for(let r=0;r<10;r++){let n=a.join(t,"pnpm-workspace.yaml");if(p.existsSync(n))return t;t=a.dirname(t)}throw new Error("Could not find monorepo root (pnpm-workspace.yaml)")}function V(e,t){let r=a.join(e,"package.json");if(!p.existsSync(r))return t;try{let n=JSON.parse(p.readFileSync(r,"utf-8"));return typeof n.name=="string"?n.name:t}catch{return t}}function Xt(e){let t=a.join(e,"apps"),r=[];if(!p.existsSync(t))throw new Error(`Apps directory not found: ${t}`);let n=p.readdirSync(t,{withFileTypes:!0}).filter(o=>o.isDirectory()).map(o=>o.name);for(let o of n){let i=a.join(t,o,"api"),c=a.join(i,"serverless.yml");p.existsSync(c)&&r.push({name:o,packageName:V(i,o),path:i})}return r}var U=["vite.config.ts","vite.config.mts","vite.config.cts","vite.config.js","vite.config.mjs","vite.config.cjs"],ke=["@slip-stream-kit/vite","@slip-stream-kit/config/vite","infra-kit/vite"];function xe(e){for(let t of U){let r=a.join(e,t);if(p.existsSync(r))try{let n=p.readFileSync(r,"utf-8");return ke.some(o=>n.includes(o))}catch{return!1}}return!1}function Jt(e){for(let t of U){let r=a.join(e,t);if(p.existsSync(r))try{return p.readFileSync(r,"utf-8").includes("infraKit(")}catch{return!1}}return!1}function we(e){let t=a.join(e,"package.json");if(!p.existsSync(t))return!1;try{let r=JSON.parse(p.readFileSync(t,"utf-8"));return typeof r.scripts?.dev=="string"&&r.scripts.dev.length>0}catch{return!1}}function Yt(e){let t=a.join(e,"apps"),r=[];if(!p.existsSync(t))return r;let n=p.readdirSync(t,{withFileTypes:!0}).filter(o=>o.isDirectory()).map(o=>o.name);for(let o of n){let i=a.join(t,o,"ui");p.existsSync(i)&&we(i)&&r.push({name:o,packageName:V(i,o),path:i,managedPort:xe(i)})}return r}function Qt(e){let t=e?.filter(Boolean)??[];return t.length>0?t:null}function Zt(e){let t=ve(e),r=a.relative(a.join(t,"apps"),e),n=r.split(a.sep)[0];if(r===""||r.startsWith("..")||a.isAbsolute(r)||!n)throw new Error(`--self: not inside an apps/<app> directory (cwd: ${e}). Run from an app folder or use --app=<name>.`);return n}var Pe=new Set(["api","ui"]),z=e=>{let t=a.join(e,"dist");return p.existsSync(t)&&p.statSync(t).isDirectory()?t:void 0},D=e=>p.existsSync(e)?p.readdirSync(e,{withFileTypes:!0}).filter(t=>t.isDirectory()).map(t=>t.name):[];function er(e){let t=[];for(let n of D(a.join(e,"packages"))){let o=z(a.join(e,"packages",n));o!==void 0&&t.push(o)}let r=a.join(e,"apps");for(let n of D(r))for(let o of D(a.join(r,n))){if(Pe.has(o))continue;let i=a.join(r,n,o);if(!p.existsSync(a.join(i,"package.json")))continue;let c=z(i);c!==void 0&&t.push(c)}return t}function tr(e){return e.map(t=>a.join(t.path,"dist")).filter(t=>p.existsSync(t))}function rr(e,t,r){let n=a.normalize(e),o=r.find(c=>n.startsWith(a.normalize(c)));return o?{kind:"package",packageDir:o}:{kind:"app",app:t.find(c=>n.startsWith(a.normalize(c)))}}var be={"*/api":{},"*/ui":{}},X=e=>{let t=e.split("/"),[r,n]=t;return t.length!==2||!r?`devServersPresets: invalid target "${e}" (expected "<app>/api" or "<app>/ui")`:n!=="api"&&n!=="ui"?`devServersPresets: invalid target part "${n}" in "${e}" (expected "api" or "ui")`:null},Re=e=>{let t=X(e);if(t)throw new Error(t);let[r,n]=e.split("/");return{appGlob:r,part:n}},or=e=>{let t=[];for(let[r,n]of Object.entries(e))for(let o of Object.keys(n.apps??{})){let i=X(o);i&&t.push({preset:r,key:o,message:`preset "${r}": ${i}`})}return t},Ee=(e,t)=>t==="api"?e.api:e.ui,Se=e=>[...new Set([...e.api,...e.ui])].sort(),H=(e,t)=>`${e}/${t}`,Ce=e=>e?0:1,je=(e,t)=>t.watchDeps===void 0?e??{watchDeps:!0,rank:t.rank,explicitValue:!1}:!e||!e.explicitValue?{watchDeps:t.watchDeps,rank:t.rank,explicitValue:!0}:t.rank>e.rank?{watchDeps:t.watchDeps,rank:t.rank,explicitValue:!0}:e,Ie=(e,t,r)=>{let{app:n,part:o,watchDeps:i,isGlob:c}=r;if(!Ee(t,o).includes(n)){c||e.unmatched.push(H(n,o));return}let u=H(n,o),l=je(e.targets.get(u),{watchDeps:i,rank:Ce(c)});e.targets.set(u,{app:n,part:o,...l})},Ae=(e,t)=>{let r=Object.entries(e.apps??be),n={targets:new Map,proxy:{},unmatched:[]};for(let[c,u]of r){let{appGlob:l,part:h}=Re(c),y=l==="*",S=y?Se(t):[l];for(let I of S)Ie(n,t,{app:I,part:h,watchDeps:u.watchDeps,isGlob:y}),u.proxy&&(n.proxy[I]={...n.proxy[I]??{},...u.proxy})}let o=[...n.targets.values()].map(({app:c,part:u,watchDeps:l})=>({app:c,part:u,watchDeps:l})),i=[...new Set(o.filter(c=>c.part==="api").map(c=>c.app))];return{targets:o,cmux:e.cmux??!1,proxy:n.proxy,localApps:i,unmatched:n.unmatched}},sr=e=>{if(e.preset!=null)return e.preset;if(e.running.length===0)return"nothing";let t=new Set(e.running);return e.discovered.every(n=>t.has(n))?"*":[...t].sort().join(" + ")},De=(e,t)=>Object.keys(e).find(r=>e[r]===t),$e=({preset:e,app:t,route:r,launchedPkgs:n,ctx:o})=>{let i=o.routePkg(t,r);if(i===void 0)return{preset:e,app:t,route:r,kind:"unknown-route",message:`preset "${e}": proxy override "${r}" on "${t}" names a route not declared in ${t}'s infra-kit.config.ts dev.proxy.routes`};if(n.has(i))return null;let c=De(o.apiPkgByApp,i),u=c?`add "${c}/api" to the preset`:`launch the api whose package is "${i}"`;return{preset:e,app:t,route:r,pkg:i,kind:"backend-not-launched",message:`preset "${e}": proxy override "${r}" \u2192 "local" requires backend "${i}" to run locally, but the preset does not launch it \u2014 ${u}, or set "${r}" to "cloud"`}},ir=(e,t)=>{let r=[];for(let[n,o]of Object.entries(e)){let i=Ae(o,t.discovered),c=new Set(i.localApps.map(u=>t.apiPkgByApp[u]).filter(u=>u!==void 0));for(let[u,l]of Object.entries(i.proxy))for(let[h,y]of Object.entries(l)){if(y!=="local")continue;let S=$e({preset:n,app:u,route:h,launchedPkgs:c,ctx:t});S&&r.push(S)}}return r};import W from"node:fs/promises";import ue from"node:os";import R from"node:path";import at from"node:process";import{z as s}from"zod";import k from"node:path";import re from"node:process";import{$ as f}from"zx";var P=e=>{let t=new Set,r=e;for(;r!==null&&typeof r=="object"&&!t.has(r);){t.add(r);let{stderr:n,stderrExcerpt:o}=r;if(typeof n=="string"&&n.length>0)return n;if(typeof o=="string"&&o.length>0)return o;r=r.cause}},Te=(e,t)=>{let r=t.stderrExcerpt??P(e),n=[`failed to ${t.operation}`];return r&&n.push(`stderr: ${r.slice(0,200).trim()}`),t.remediation&&n.push(`try: ${t.remediation}`),n.join(" \u2014 ")},d=class extends Error{operation;remediation;stderrExcerpt;constructor(t,r){super(Te(t,r),{cause:t}),this.name="OperationError",this.operation=r.operation,this.remediation=r.remediation,this.stderrExcerpt=r.stderrExcerpt??P(t)}};var J={enabled:!1},b=()=>J.enabled;var Ne=/^v?(\d+)\.(\d+)\.(\d+)$/,_e=/^(\d+)\.(\d+)\.(\d+)$/,Le=/^[a-z0-9]+(?:-[a-z0-9]+)*$/,C="release/",$="release/v",Y="refs/heads/",Ke="next";var Q=new Set(["dev","main","next","hotfix","regular","release"]),v=class extends Error{constructor(t){super(t),this.name="InvalidReleaseNameError"}},x=class extends Error{constructor(t){super(t),this.name="InvalidReleaseRefError"}},ee=e=>e.startsWith(Y)?e.slice(Y.length):e,te=(e,t,r)=>({kind:"version",semver:{major:e,minor:t,patch:r},raw:`${e}.${t}.${r}`}),T=e=>{if(e.length===0)throw new v('Release name is empty. Provide a kebab-case name like "checkout-redesign".');if(e.length>50)throw new v(`Release name "${e}" is ${e.length} characters; the maximum is 50.`);if(!Le.test(e))throw new v(`Release name "${e}" is not kebab-case. Use lowercase letters, digits, and single hyphens, e.g. "checkout-redesign".`);if(Q.has(e))throw new v(`Release name "${e}" is reserved. Reserved names: ${[...Q].join(", ")}.`)},N=e=>{let t=ee(e.trim());if(!t.startsWith(C))return null;if(t.startsWith($)){let n=t.slice($.length),o=_e.exec(n);if(o)return te(Number(o[1]),Number(o[2]),Number(o[3]))}let r=t.slice(C.length);try{T(r)}catch{return null}return{kind:"name",name:r,raw:r}},Oe=e=>{let t=e.trim();if(ee(t).startsWith(C)){let o=N(t);if(!o)throw new x(`"${e}" looks like a release branch but is not a valid release/v<semver> or release/<name> ref.`);return o}let n=Ne.exec(t);if(n)return te(Number(n[1]),Number(n[2]),Number(n[3]));if(t.toLowerCase()===Ke)throw new x('The "next" token must be resolved to a concrete version (via computeNextVersion) before parsing a release ref.');try{T(t)}catch(o){let i=o instanceof Error?o.message:String(o);throw new x(`Cannot parse "${e}" as a release ref: ${i}`)}return{kind:"name",name:t,raw:t}},Me=e=>e.kind==="version"?`${$}${e.raw}`:`${C}${e.name}`,We=(e,t)=>{let r=t==="hotfix"?"Hotfix":"Release";return e.kind==="version"?`${r} v${e.raw}`:`${r} ${e.name}`},Fe=e=>e.kind==="version"?`Release v${e.raw} (RC)`:`Release ${e.name} (RC)`,Ge=e=>e.kind==="version"?`v${e.raw}`:e.name,Be=e=>e.raw,_=e=>e==null?!1:N(e)!==null,Z=e=>{if(e===void 0)return null;let t=e instanceof Date?e.getTime():new Date(e).getTime();return Number.isNaN(t)?null:t},qe=(e,t,r)=>{if(e.kind==="version"&&t.kind==="version")return e.semver.major!==t.semver.major?e.semver.major-t.semver.major:e.semver.minor!==t.semver.minor?e.semver.minor-t.semver.minor:e.semver.patch-t.semver.patch;if(e.kind==="version")return-1;if(t.kind==="version")return 1;let n=Z(r?.a),o=Z(r?.b);return n!==null&&o!==null&&n!==o?n-o:e.name<t.name?-1:e.name>t.name?1:0};var ne=async e=>{let t=await f({cwd:e})`git worktree list --porcelain`,r=[],n=null;for(let o of t.stdout.split(`
|
|
2
|
+
`)){if(o.startsWith("worktree ")){n&&r.push(n),n={path:o.slice(9),branch:null,detached:!1,bare:!1,prunable:!1,locked:!1};continue}n&&(o.startsWith("branch ")?n.branch=o.slice(7).replace(/^refs\/heads\//,""):o==="detached"?n.detached=!0:o==="bare"?n.bare=!0:o==="locked"||o.startsWith("locked ")?n.locked=!0:(o==="prunable"||o.startsWith("prunable "))&&(n.prunable=!0))}return n&&r.push(n),r},ze=async e=>{let t=await ne(re.cwd()),r=n=>e==="release"?_(n):n.startsWith("feature/");return t.map(n=>n.branch).filter(n=>n!==null&&r(n))},Ve=e=>{let t=P(e);return t&&/not a git repository/i.test(t)?"not a git repository":void 0},Ue=({hasStderr:e})=>{if(b())return"the infra-kit MCP server resolves its project from the working directory it was launched in; the operator must relaunch the server with its working directory set to an infra-kit project repo";let t="run infra-kit from inside an infra-kit project repo (or one of its git worktrees)";return e?`${t}; if you are already in one, the stderr in this message is the real cause`:t},w=async()=>{try{return(await f({quiet:!0})`git rev-parse --show-toplevel`).stdout.trim()}catch(e){let t=Ve(e)??P(e);throw new d(e,{operation:`resolve the project root from ${re.cwd()}`,stderrExcerpt:t,remediation:Ue({hasStderr:t!==void 0})})}},L=async e=>{let t=e??await w(),r=(await f({cwd:t,quiet:!0})`git rev-parse --git-common-dir`).stdout.trim(),n=k.resolve(t,r);return n.includes(`${k.sep}.git${k.sep}modules${k.sep}`)?t:k.dirname(n)},oe=async()=>(await f`git rev-parse --abbrev-ref HEAD`).stdout.trim(),se=async()=>(await f`git status --porcelain`).stdout.split(`
|
|
3
|
+
`).map(t=>t.trimEnd()).filter(t=>t.trim().length>0),He=async()=>(await se()).length===0,Xe=async()=>{let e=await w(),[t,r]=await Promise.all([f({cwd:e})`git rev-parse --absolute-git-dir`,f({cwd:e})`git rev-parse --git-common-dir`]),n=t.stdout.trim(),o=k.resolve(e,r.stdout.trim());return n!==o},Je=async()=>{let e=await w();return k.basename(e)},Ye=async e=>(await f`git branch --list ${e}`).stdout.trim().length>0,Qe=async e=>{(await f`git branch --list ${e}`).stdout.trim().length!==0&&await oe()!==e&&await f`git branch -D ${e}`},Ze=async e=>{(await f`git ls-remote --heads origin ${e}`).stdout.trim().length!==0&&await f`git push origin --delete ${e}`};import{$ as j}from"zx";var et=async(e,t)=>{let r=`${t}^{commit}`;try{return(await j({cwd:e,quiet:!0})`git rev-parse --verify --quiet ${r}`).stdout.trim()||null}catch{return null}},tt=async(e,t,r)=>{try{return await j({cwd:e,quiet:!0})`git merge-base --is-ancestor ${t} ${r}`,!0}catch(n){if(n.exitCode===1)return!1;throw new d(n,{operation:`test whether ${t} is contained in ${r}`,remediation:"both refs must exist \u2014 verify them with rev-parse first"})}},rt=async(e,t)=>{let n=(await j({cwd:e,quiet:!0})`git ls-remote --heads origin ${t}`).stdout.trim().split(`
|
|
4
|
+
`)[0];return n?n.split(/\s+/)[0]??null:null},nt=async(e,t)=>{let r=t.map(n=>`${n.sha}:refs/heads/${n.branch}`);if(r.length===0)return{refspecs:r,pushed:!1};try{return await j({cwd:e,quiet:!0})`git push --atomic origin ${r}`,{refspecs:r,pushed:!0}}catch(n){return{refspecs:r,pushed:!1,stderr:n.stderr}}};import ie from"node:path";import K from"node:process";import{$ as g}from"zx";var ae=async e=>{let t=e??K.cwd(),r=(await g({cwd:t,quiet:!0})`git rev-parse --git-common-dir`).stdout.trim(),n=K.env.INFRA_KIT_SESSION||String(K.pid);return ie.join(ie.resolve(t,r),"infra-kit",`merge-dev-${n}`)},O=async e=>{try{return await g({cwd:e,quiet:!0})`git rev-parse -q --verify MERGE_HEAD`,!0}catch{return!1}},ot=async e=>{if(await O(e))throw new d(void 0,{operation:"reuse the scratch worktree",stderrExcerpt:"a merge is still in progress in the scratch worktree",remediation:"this is an infra-kit bug \u2014 the previous branch iteration did not clean up"});let t=(await g({cwd:e,quiet:!0})`git status --porcelain`).stdout.trim();if(t.length>0)throw new d(void 0,{operation:"reuse the scratch worktree",stderrExcerpt:`scratch worktree is dirty: ${t.split(`
|
|
5
|
+
`)[0]}`,remediation:"this is an infra-kit bug \u2014 the previous branch iteration did not clean up"})},st=async e=>{let t=async r=>{try{await r()}catch{}};await O(e)&&await t(()=>g({cwd:e,quiet:!0})`git merge --abort`),await t(()=>g({cwd:e,quiet:!0})`git reset --hard`),await t(()=>g({cwd:e,quiet:!0})`git clean -fd`)},it=async(e,t)=>{let{cwd:r,anchor:n="origin/dev"}=e,o=await ae(r),i=async()=>{await g({cwd:r,quiet:!0})`git worktree add --detach ${o} ${n}`};try{await i()}catch(u){try{await g({cwd:r,quiet:!0})`git worktree prune`,await i()}catch{throw new d(u,{operation:"create the scratch worktree for the merge",remediation:`remove any stale checkout at ${o}, then retry`})}}let c={path:o,remove:async()=>{try{await g({cwd:r,quiet:!0})`git worktree remove --force ${o}`}catch{}}};try{return await t(c)}finally{await c.remove()}};var ce="infra-kit.json",le=".infra-kit",ct="infra-kit.json",pt="projects",ut=s.object({provider:s.literal("doppler"),config:s.object({name:s.string().min(1)})}),lt=s.discriminatedUnion("provider",[ut]),ft=s.object({workspaceConfigPath:s.string().min(1)}),dt=s.object({provider:s.literal("cursor"),config:ft}),gt=s.object({}),mt=s.object({provider:s.literal("zed"),config:gt}),pe=s.discriminatedUnion("provider",[dt,mt]),ht=s.union([pe,s.array(pe).min(1)]),yt=s.object({provider:s.literal("jira"),config:s.object({baseUrl:s.string().url(),projectId:s.number().int().positive()})}),vt=s.discriminatedUnion("provider",[yt]),kt=["two-columns","three-pane"],xt=s.object({layout:s.enum(kt).optional()}),wt=s.object({openInGithubDesktop:s.boolean().optional(),openInCmux:s.boolean().optional(),cmux:xt.optional()}),Pt=s.object({port:s.number().int().positive().optional(),prefixUrl:s.string().min(1).optional()}).strict(),bt=s.record(s.string().min(1),Pt),Rt=s.enum(["local","cloud"]),Et=s.object({watchDeps:s.boolean().optional(),proxy:s.record(s.string().min(1),Rt).optional()}).strict(),St=s.object({apps:s.record(s.string().min(1),Et).optional(),cmux:s.boolean().optional()}).strict(),Ct=s.record(s.string().min(1),St),jt=s.object({port:s.number().int().positive().optional()}).strict(),It=s.object({trigger:s.enum(["shell-startup","cli-invocation"]),config:s.string().min(1)}).strict(),At=s.enum(["disallow","allow","cli-only"]),F=s.object({envManagement:lt,ide:ht.optional(),taskManager:vt.optional(),worktrees:wt.optional(),envAutoLoad:It.optional(),dev:bt.optional(),devServersPresets:Ct.optional(),devProxy:jt.optional(),protectedEnvs:At.optional()}).strict(),Dt=443,fe=F.superRefine((e,t)=>{if(!Array.isArray(e.ide))return;let r=new Set;for(let n of e.ide){if(r.has(n.provider)){t.addIssue({code:"custom",message:"each IDE provider may appear at most once",path:["ide"]});return}r.add(n.provider)}}),de=F.partial(),$t=e=>{let t=e.ide;return t?Array.isArray(t)?t:[t]:[]},Tt="two-columns",Nt=e=>e.worktrees?.cmux?.layout??Tt,m=null,E=null,ge=()=>`${at.cwd()} ${ue.homedir()}`,me=async()=>{let e=ge();if(E&&E.key===e)return E.value;let t=await w(),r=await L(t),n=R.basename(r),o=R.join(ue.homedir(),le),i={main:R.join(t,ce),userGlobal:R.join(o,ct),userProject:R.join(o,pt,n,ce),projectName:n};return E={key:e,value:i},i},_t=async()=>{let e=ge(),t=await me(),r;try{r=await W.stat(t.main)}catch{m=null;let h=t.main.replace(/\.json$/,".yml");throw await M(h)?new Error(`infra-kit.json not found at ${t.main}. A legacy infra-kit.yml exists \u2014 run \`infra-kit setup --skip-tools\` to convert it.`):new Error(b()?`infra-kit.json not found at ${t.main} \u2014 the directory the infra-kit MCP server was launched in is not an infra-kit project. The operator must relaunch the server with its working directory set to an infra-kit project repo.`:`infra-kit.json not found at ${t.main} \u2014 this git repo is not an infra-kit project. Run \`infra-kit setup --skip-tools\` here to create it, cd into an infra-kit project repo, or check out a branch that has infra-kit.json.`)}let[n,o]=await Promise.all([M(t.userGlobal),M(t.userProject)]),i={main:Number(r.mtimeMs),userGlobal:n?Number(n.mtimeMs):null,userProject:o?Number(o.mtimeMs):null};if(m&&m.key===e&&Mt(m.mtimes,i))return m.value;let c=[{label:"infra-kit.json",path:t.main,required:!0},{label:"~/.infra-kit/infra-kit.json",path:t.userGlobal,required:!1},{label:`~/.infra-kit/projects/${t.projectName}/infra-kit.json`,path:t.userProject,required:!1}],u={};for(let h of c){let y=await Wt(h);y!==null&&(u={...u,...y})}let l=fe.safeParse(u);if(!l.success)throw new Error(`Invalid merged infra-kit config: ${s.prettifyError(l.error)}`);return m={key:e,mtimes:i,value:l.data},l.data},Lt=()=>{m=null},Kt=()=>{m=null,E=null},M=async e=>{try{return await W.stat(e)}catch{return null}},Ot=async e=>{try{return await W.readFile(e,"utf-8")}catch{return null}},Mt=(e,t)=>{let r=Object.keys(e);return r.length!==Object.keys(t).length?!1:r.every(n=>e[n]===t[n])},Wt=async e=>{let t=await Ot(e.path);if(t===null){if(e.required)throw new Error(`${e.label} not found at ${e.path}`);return null}let r;try{r=t.trim()===""?{}:JSON.parse(t)}catch(o){throw new Error(`Invalid JSON in ${e.label} at ${e.path}: ${o.message}`)}if(Ft(r)&&"envTokens"in r)throw new Error(Gt(e));let n=de.safeParse(r);if(!n.success)throw new Error(`Invalid ${e.label} at ${e.path}: ${s.prettifyError(n.error)}`);return n.data},Ft=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),Gt=e=>[`Refusing to load ${e.label} \u2014 \`envTokens\` is not a config key.`,"A service token in a config file can be committed, backed up by your editor, or shared."," 1. REVOKE the token in Doppler now \u2014 treat it as compromised.",` 2. Remove the \`envTokens\` key from ${e.path}.`," 3. Re-add it privately: `infra-kit env-token-set <env>`"," (it is written to ~/.infra-kit/projects/<repo>/tokens.json, mode 0600, never to the repo)."].join(`
|
|
6
|
+
`);export{P as a,d as b,J as c,b as d,v as e,T as f,N as g,Oe as h,Me as i,We as j,Fe as k,Ge as l,Be as m,_ as n,qe as o,ne as p,ze as q,w as r,L as s,oe as t,se as u,He as v,Xe as w,Je as x,Ye as y,Qe as z,Ze as A,et as B,tt as C,rt as D,nt as E,O as F,ot as G,st as H,it as I,G as J,Vt as K,Ut as L,ve as M,Xt as N,Jt as O,Yt as P,Qt as Q,Zt as R,er as S,tr as T,rr as U,X as V,or as W,Ae as X,sr as Y,ir as Z,le as _,Dt as $,fe as aa,de as ba,$t as ca,Nt as da,me as ea,_t as fa,Lt as ga,Kt as ha};
|
|
7
|
+
//# sourceMappingURL=chunk-Q4HKBLQG.js.map
|