infra-kit 0.5.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.
@@ -1,4 +1,4 @@
1
- import{a as Le,c as L,f as re,j as Oe,k as N,l as H,m as Ue,n as B,t as Fe,u as Me,v as He,y as Be}from"./chunk-DHYZ6MBU.js";import"./chunk-7POMR4QK.js";import{a as O,b as je}from"./chunk-VDBJ73LS.js";import{$ as Ne,L as T,M as C,N as F,O as Pe,P as De,Q as M,R as ke,S as $,T as xe,U as Ae,V as Re,W as Ee,X as Te,Y as Ce,Z as $e,fa as te}from"./chunk-Q4HKBLQG.js";import{d as Ie,e as ne,m as _e}from"./chunk-D4UYDBN5.js";import{Command as Un}from"commander";import P from"node:process";import{pathToFileURL as Fn}from"node:url";import*as Ge from"node:path";import Kt from"node:process";var Vt=r=>({pane:{surfaces:[{type:"terminal",command:r}]}}),ie=(r,e)=>{if(r.length===1)return Vt(r[0]);let t=Math.ceil(r.length/2),n=r.slice(0,t),i=r.slice(t);return{direction:e%2===0?"horizontal":"vertical",split:Math.round(t/r.length*100)/100,children:[ie(n,e+1),ie(i,e+1)]}},We=r=>{if(r.length===0)throw new Error("buildCmuxLayout: at least one command is required");return ie(r,0)};var Jt=(r,e)=>r.map(({app:t,targets:n})=>`pnpm exec infra-kit dev ${n&&n.length>0?`--target=${n.join(",")}`:`--app=${t}`}${e?" --watch":""}`),qt=r=>{let e=new Map;for(let t of Object.keys(r?.apps??{})){let n=t.split("/")[0];n===void 0||n==="*"||e.set(n,[...e.get(n)??[],t])}return e},zt=(r,e)=>{let t=F(r);return e?t.filter(n=>e.includes(n.name)):t},Yt=(r,e)=>{T.info(`\u{1F9E9} Opened cmux dev workspace ${e} with ${r.length} pane(s):`);for(let t of r)T.info(` \u2022 ${t.name} (infra-kit dev --app=${t.name})`)},Xt=r=>{B({onSignal:async e=>{T.info(`
1
+ import{a as Le,c as L,f as re,j as Oe,k as N,l as H,m as Ue,n as B,t as Fe,u as Me,v as He,y as Be}from"./chunk-Q23IJP6D.js";import"./chunk-XJOY7J3G.js";import{a as O,b as je}from"./chunk-VDBJ73LS.js";import{$ as Ne,L as T,M as C,N as F,O as Pe,P as De,Q as M,R as ke,S as $,T as xe,U as Ae,V as Re,W as Ee,X as Te,Y as Ce,Z as $e,fa as te}from"./chunk-Q4HKBLQG.js";import{d as Ie,e as ne,m as _e}from"./chunk-D4UYDBN5.js";import{Command as Un}from"commander";import P from"node:process";import{pathToFileURL as Fn}from"node:url";import*as Ge from"node:path";import Kt from"node:process";var Vt=r=>({pane:{surfaces:[{type:"terminal",command:r}]}}),ie=(r,e)=>{if(r.length===1)return Vt(r[0]);let t=Math.ceil(r.length/2),n=r.slice(0,t),i=r.slice(t);return{direction:e%2===0?"horizontal":"vertical",split:Math.round(t/r.length*100)/100,children:[ie(n,e+1),ie(i,e+1)]}},We=r=>{if(r.length===0)throw new Error("buildCmuxLayout: at least one command is required");return ie(r,0)};var Jt=(r,e)=>r.map(({app:t,targets:n})=>`pnpm exec infra-kit dev ${n&&n.length>0?`--target=${n.join(",")}`:`--app=${t}`}${e?" --watch":""}`),qt=r=>{let e=new Map;for(let t of Object.keys(r?.apps??{})){let n=t.split("/")[0];n===void 0||n==="*"||e.set(n,[...e.get(n)??[],t])}return e},zt=(r,e)=>{let t=F(r);return e?t.filter(n=>e.includes(n.name)):t},Yt=(r,e)=>{T.info(`\u{1F9E9} Opened cmux dev workspace ${e} with ${r.length} pane(s):`);for(let t of r)T.info(` \u2022 ${t.name} (infra-kit dev --app=${t.name})`)},Xt=r=>{B({onSignal:async e=>{T.info(`
2
2
  Received ${e}, closing cmux dev workspace ${r}...`),await He(r)}})},Ve=async r=>{let e=C(Kt.cwd()),t=zt(e,M(r.include));if(t.length===0){T.warn("No API apps found to run");return}let n=qt(r.presetDef),i=Jt(t.map(u=>({app:u.name,targets:n.get(u.name)})),r.watch??!1),s=We(i),o=`${Ge.basename(e)} dev`,a=await Me({cwd:e,title:o,layout:s});Yt(t,a),Xt(a);let l=setInterval(()=>{l.refresh()},2**30);await new Promise(()=>{})};import se from"node:process";var oe=r=>{try{se.stderr.write(r)}catch{}},I=(r,e,t=!0)=>{let n=e instanceof Error?`${e.message}
3
3
  ${e.stack??"(no stack)"}`:String(e);return`
4
4
  \u26A0\uFE0F ${r}: ${n}
package/dist/mcp.js CHANGED
@@ -1,2 +1,2 @@
1
- import{a as E,d as w,fa as O,ia as N,s as I}from"./chunk-LXBIHXUR.js";import"./chunk-DHYZ6MBU.js";import{a as D}from"./chunk-5OHTHRS7.js";import"./chunk-7POMR4QK.js";import{i as _}from"./chunk-HXBIE4JP.js";import{J as g,K as A,L as i,c as S,fa as P}from"./chunk-Q4HKBLQG.js";import"./chunk-D4UYDBN5.js";import"./chunk-QWVLAZ6N.js";import{serveStdio as je}from"@modelcontextprotocol/server/stdio";import f from"node:process";import{setTimeout as Le}from"node:timers/promises";import h from"node:process";var F=e=>{h.on("uncaughtException",t=>{e.fatal({err:t,msg:"Uncaught Exception"}),e.error(`Uncaught Exception! Check ${g}. Shutting down...`),e.flush(),h.exit(1)}),h.on("unhandledRejection",(t,r)=>{e.fatal({reason:t,promise:r,msg:"Unhandled Rejection"}),e.error(`Unhandled Rejection! Check ${g}. Shutting down...`),e.flush(),h.exit(1)})};import{McpServer as qe}from"@modelcontextprotocol/server";var M='# release-create \u2014 cutting a release through infra-kit\n\nThe tool is `mcp__infra-kit__release-create`. Everything below is about calling that tool.\n\nDo not shell out. A `Bash` call running `git switch`, `git push` or `gh pr create` reproduces none\nof the preconditions below and bypasses the confirm gate in section 2 \u2014 which is the only place a\nhuman approves the release.\n\n## 1. Preconditions\n\nCheck these before the first call; each one is a refusal the human has to clear, not something to\nwork around.\n\n- **The main repository checkout, not a linked worktree.** The tool refuses outright from inside a\n linked worktree.\n- **A clean working tree.** Uncommitted changes are refused; the human commits or stashes.\n- **No other worktree holding the base branch.** Regular releases branch off `dev`, hotfixes off\n `main`. If a linked worktree has that branch checked out, the tool refuses and names the path.\n- **Jira configured.** Every release gets a matching fix version, so `JIRA_BASE_URL`,\n `JIRA_EMAIL`, `JIRA_PROJECT_ID` and `JIRA_TOKEN` (or `JIRA_API_TOKEN`) must be in the\n environment \u2014 load them with `ik env-load` and source the file it returns. The check runs before\n anything is cut.\n\n**You do not have to already be on the base branch.** The tool runs `git fetch origin`,\n`git switch <base>` and `git pull --ff-only` itself. That is a real side effect on the human\'s\ncheckout: say so before call 2.\n\n## 2. The two-call confirm protocol\n\n`release-create` is gated. **The first call never executes anything.**\n\n**Call 1** \u2014 send the real arguments, with no `confirm` and no `confirmToken`. The result is a gate\npayload, `{"status": "confirmation_required", ...}`, and it carries `"isError": true`.\n\n**That `isError` does not mean the call failed.** It is set because the payload is a gate rather\nthan the tool\'s declared output. Nothing was created, nothing was pushed, nothing was switched. An\nagent that reads it as a failure \u2014 and gives up, or retries, or falls back to `Bash` \u2014 has skipped\nthe human approval this protocol exists for. Do none of those.\n\nThe gate payload carries two things you need:\n\n- `resolvedArgs` \u2014 exactly the arguments the server bound. **Show these to the human.** This is the\n approval moment; there is no other one.\n- `confirmToken` \u2014 an HMAC bound to the tool name and to those exact arguments. It expires 600\n seconds after it is minted.\n\n**Call 2** \u2014 repeat the **same arguments**, unchanged, plus `"confirm": true` and the\n`confirmToken` from call 1.\n\nChange any argument between the two calls and round 2 comes back as\n`{"status": "confirmation_refused", "reason": "mismatch"}`. That is terminal \u2014 it is not a second\ngate. The other reasons are `absent`, `malformed`, `mac`, `expired` and `bind`, and every one of\nthem recovers the same way: call again **without** `confirm` to mint a fresh gate, then re-call\nwith those arguments and the new token. Never retry call 2 with the old token.\n\nIf the human wants different arguments, go back to call 1 with the new arguments. Do not edit the\narguments and reuse the token \u2014 that is exactly what `mismatch` refuses.\n\n## 3. What goes in `releases`\n\n`releases` is an array with at least one entry. Every entry carries **exactly one** of:\n\n- `version` \u2014 a semver string such as `"1.64.0"`, or the literal token `"next"`.\n- `name` \u2014 a free-form kebab-case identifier such as `"checkout-redesign"`.\n\nThey are mutually exclusive and one is required. An entry with both, or with neither, is rejected\nby the schema before the tool runs.\n\nEach entry also carries `type` (`"regular"` or `"hotfix"`, default `"regular"`) and an optional\n`description`, which becomes the Jira fix version\'s description and feeds the PR body.\n\n### The `"next"` token\n\n`"next"` is version-only \u2014 a named release never auto-bumps. It resolves against the union of the\nremote `release/v*` branches on `origin` and the project\'s Jira fix versions:\n\n- `"regular"` bumps the minor and resets the patch: `1.63.2` becomes `1.64.0`.\n- `"hotfix"` bumps the patch on the highest minor: `1.63.2` becomes `1.63.3`.\n\nSeveral `"next"` entries in one call advance sequentially rather than all resolving to the same\nversion.\n\n**Be honest about what `"next"` could see.** The two sources are queried in parallel and a source\nthat fails is logged and dropped, not raised \u2014 if the Jira call fails, `"next"` is computed from\nthe remote branches alone and can land on a version Jira already knows about. If neither source\nyields a prior version the tool refuses and asks for an explicit one. When the exact number\nmatters, pass the semver instead of the token.\n\n### Reading `$ARGUMENTS`\n\nThe `/infra-kit:release-create` command hands you `$ARGUMENTS` verbatim, and its argument hint is\n`[--hotfix] [--desc <text>] [<version|name>]`. **Those two flags are conventions of this command, not\nCLI flags** \u2014 `infra-kit release create` accepts neither, and the tool takes neither. They exist so a\nhuman can type the whole request on one line, and it is your job to translate them:\n\n- `--hotfix` \u2192 `type: "hotfix"` on every entry you build. Its absence means `"regular"`.\n- `--desc <text>` \u2192 `description` on the entry. The text runs to the end of the argument string.\n- The bare token \u2192 `version` when it is a semver or the literal `next`, `name` when it is kebab-case.\n\nSo `--hotfix --desc "Card expiry fix" 1.63.3` is one entry:\n`{version: "1.63.3", type: "hotfix", description: "Card expiry fix"}`.\n\nIf `$ARGUMENTS` is empty, ask the human what to cut rather than guessing a version \u2014 and read the\n`"next"` caveats above before offering it.\n\n**Precedence, when a form is also involved.** If the server answers with an argument form and the\nhuman edits it, **the form wins field by field wherever the human supplied a value, and the values\nyou parsed from `$ARGUMENTS` win everywhere else.** A human who typed `--hotfix` and then picked\n`regular` in the form gets `regular` \u2014 they saw the field and changed it. A human who typed\n`--hotfix` and left `type` untouched gets `hotfix`. Never rebuild the entry from the form alone: that\nconverts every untouched field into a silent overwrite by a value the human never saw.\n\n### Batches\n\nOne call may create several releases, but **all entries must share the same `type`**. Regular and\nhotfix branch off different bases, so a mixed batch is rejected \u2014 cut them in separate\ninvocations.\n\nA batch does not stop at the first failure. Each entry is attempted and the result reports\n`successCount`, `failureCount`, `createdBranches` and `failedReleases`. Read all four before\ntelling the human the release was created: partial success is a normal outcome here.\n\n## 4. What one release does\n\nPer entry, in order: fetch and switch to the base branch, cut `release/v<semver>` (or\n`release/<name>`), open a GitHub release PR, and create or reuse the Jira fix version (`v<semver>`\nor `<name>`).\n\nTwo consequences worth stating before call 2:\n\n- An existing fix version is **reused**, and a `description` that differs is written through to it,\n so the PR and the fix version cannot disagree.\n- A fix version that is already released or archived is **refused**, not reused. The human either\n picks a different version or un-releases it in Jira.\n\n## 5. What not to do\n\n- Do not work around a refusal with `git` or `gh`. Every refusal in section 1 is a state only the\n human can clear.\n- Do not invent a list of candidate versions for the human. Pass `"next"` and let the server\n resolve it from the real branches and fix versions, or ask for the exact semver.\n- Do not read `isError: true` on a `confirmation_required` payload as a failure. See section 2.\n';var q='# setup \u2014 bringing a machine to a working infra-kit\n\nThe tool is `mcp__infra-kit__setup`. Everything below is about calling that tool.\n\nThe same code runs behind `infra-kit setup` in a terminal, so this body describes both spellings: the\nCLI flag first, the tool field it sets second. They are one implementation, not two.\n\nDo not shell out. A `Bash` call running `brew install`, `curl \u2026 | bash` or the writers in section 1\nreproduces none of the refusals in section 3 and bypasses the confirm gate in section 4 \u2014 which is the\nonly place a human approves an install.\n\n**If all you want is to know what this machine looks like, call `doctor` instead.** It reports the same\nfive tools plus the rest of the setup, mutates nothing, and raises no confirmation prompt. `infra-kit setup` is\nthe write path; `doctor` is the read path, and they are separate tools precisely so that asking a\nquestion does not cost an approval.\n\n## 1. What one call does, in order\n\nTwo halves, and **both always run**. Neither short-circuits the other: an init-half failure is recorded\nand the dependency half still runs.\n\n### Step 1 \u2014 the init half: local, offline, additive, near-instant\n\nIn this order:\n\n1. the managed block in `~/.zshrc` \u2014 the shell integration\n2. the four config migrations, in their recorded order\n3. the user-global config seed\n4. the repo\'s agent-instruction files (`CLAUDE.md` guidance blocks) \u2014 **non-fatal**; a repo it cannot\n resolve is warned about, not failed on\n5. the git-root resolution for writes, warning when the two root gates disagree\n6. the Claude Code plugin pointer \u2014 `.claude/settings.json`, `.mcp.json`, and the plugin install\n7. the per-project config reseed\n8. a warning when `$SHELL` is not zsh\n\nEvery writer here is additive and never overwrites. Nothing in this half installs software and nothing\nreaches the network.\n\n**It runs first deliberately.** Step 1.6 is what makes the MCP surface usable at all, so it must not sit\nbehind a network converge that can be slow or fail.\n\n### Step 2 \u2014 the dependency converge\n\nFive tools, serially, in registry order: **brew, aws, gh, doppler, portless**. Serial and ordered\nbecause the recipes have prerequisites \u2014 `gh` and `doppler` both need `brew`, and doppler\'s own two\nsteps (gnupg, then the tap) must not interleave with another tool\'s.\n\nPer tool: install it when it is absent, update it when it is present, skip it when its manager is not\none infra-kit manages. A recipe the risk predicate refuses is **printed, not run** \u2014 section 3.\n\n### Step 3 \u2014 one combined summary\n\nOne line per tool, then the exact argv for every refused recipe, then the `source ~/.zshrc` reminder\nlast of all.\n\n### How to read the result\n\n- `init` \u2014 one entry per step above, each with an `outcome` of `written`, `unchanged`, `skipped` or\n `warned`, and the same message a human would have read.\n- `tools` \u2014 one entry per requested dependency: `action` (`installed`, `updated`, `skipped`, `refused`\n or `failed`), the `before` state, the `commands` that were run or would have been, and a one-line\n `detail`.\n- `converged` \u2014 whether the dependency step could act at all. `false` under `skipTools`.\n- `changed` \u2014 whether anything was installed or updated.\n- `allSucceeded` \u2014 whether no tool **failed**. **A refusal is not a failure**, so this stays `true` when\n a recipe was printed instead of run.\n\nThe process exits non-zero when either half hard-failed. **Do not read a success as "everything is\ninstalled"** \u2014 read `tools[].action`, and tell the human about every `refused` entry.\n\n## 2. The flags, and what each one narrows\n\nThe default \u2014 no flag, no field \u2014 converges all five tools.\n\n- `--tools <ids...>` \u2192 `tools: ["gh", "doppler"]`. Converge **only those ids**. Same behaviour per\n tool, smaller set. The ids are `brew`, `aws`, `gh`, `doppler` and `portless`.\n- `--update [ids...]` \u2192 `mode: "update"`. **Never installs.** A tool that is present is updated; a tool\n that is absent is reported `skipped` with the reason, and its install recipe is not run. Given ids, it\n also narrows the set, so `--update gh` is "update gh, and nothing else, and only if it is there".\n- `--skip-tools` \u2192 `skipTools: true`. A **read-only probe**. The init half still runs \u2014 it is local and\n additive \u2014 and then each tool is reported with what it needs and the exact argv that would fix it.\n Nothing is installed and nothing is updated.\n\n**`--skip-tools` with `--tools` or `--update` is a usage error, not a precedence rule.** Every\nprecedence answer is wrong: honouring `--skip-tools` would ignore a set the caller chose, and honouring\nthe other would install software the caller asked not to install. The call is refused instead.\n\n## 3. Recipes that are printed rather than run\n\nWhether a recipe may run unattended is **computed**, not a per-recipe flag someone set. Two of the four\nconjuncts are static and are applied to every recipe unconditionally:\n\n- **`needs-sudo`** \u2014 the recipe escalates privilege.\n- **`fetches-network-script`** \u2014 the recipe pipes a script fetched at run time into a shell.\n\nTwo are detection-based: **`manager-absent`** (the package manager the recipe drives is not on this\nhost) and **`manager-mismatch`** (a different manager owns the binary \u2014 running `brew upgrade` against\nan npm install, or the vendor\'s self-updater against a Homebrew keg, is the split-brain infra-kit\nrefuses for itself).\n\n**Two recipes fail the static conjuncts, and they are the two bootstraps:**\n\n- **the Homebrew bootstrap** \u2014 `/bin/bash -c "$(curl -fsSL \u2026/install.sh)"` \u2014 fails **both**: it pipes a\n network-fetched script **and** needs sudo on macOS.\n- **the first AWS CLI install** \u2014 `curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash` \u2014\n needs no sudo (it installs under `$HOME`), but it is still a network-fetched script.\n\nBoth are the tools\' own documented installers, and neither is a bug to route around. They are refused\n**by computation, applied before any detection runs**, which is what makes the refusal trustworthy: it\ncannot be widened by a probe getting something wrong, only narrowed.\n\nOnce Homebrew exists, `gh` and `doppler` install through it and run unattended; once the AWS CLI exists,\n`aws update` is a plain no-sudo recipe and runs. The refusals are a first-install cost, not permanent.\n\nWhat to do with one: the entry\'s `commands` array is the exact argv, one string per step. **Show it to\nthe human and let them run it themselves.** Do not reconstruct it as a `Bash` call \u2014 that is the same\nunattended `sudo` and the same piped script, with the control removed.\n\n## 4. The confirm gate\n\n`mcp__infra-kit__setup` is gated, and **both gates fire on every call \u2014 `skipTools` included**.\n\n**Call 1** \u2014 send the real arguments, with no `confirm` and no `confirmToken`. The result is a gate\npayload, `{"status": "confirmation_required", \u2026}`, carrying `"isError": true`.\n\n**That `isError` does not mean the call failed.** Nothing was written and nothing was installed. An\nagent that reads it as a failure \u2014 and gives up, or retries, or falls back to `Bash` \u2014 has skipped the\nhuman approval this protocol exists for. Do none of those. Show the human `resolvedArgs`; that is the\napproval moment.\n\n**Call 2** \u2014 repeat the **same arguments**, unchanged, plus `"confirm": true` and the `confirmToken`\nfrom call 1. Change any argument between the two and round 2 comes back\n`{"status": "confirmation_refused", "reason": "mismatch"}`, which is terminal, not a second gate. Every\nrefusal reason recovers the same way: call again **without** `confirm` for a fresh gate, then re-call\nwith the new token. Never retry with the old one.\n\nThe tool also carries `anthropic/requiresUserInteraction`, so the host prompts a human even where an\nallow rule would otherwise skip it. One install therefore costs two prompts. That is intended: neither\ngate substitutes for the other, and neither substitutes for the computed refusals in section 3, which\nare the only control that ships inside the CLI itself.\n\n## 5. There is no `init` command\n\nIt was removed outright. The binary rejects the name \u2014 an instruction that still says to run it fails\nat the parser rather than quietly doing something else.\n\nRepos can still say it. A consumer\'s committed CLAUDE.md block is rewritten only from inside that repo,\nso a repo upgrades the global CLI without its own text changing and can sit arbitrarily far behind. So\nwhen a human asks for "init", or a repo\'s instructions still name it:\n\n- Run **`infra-kit setup`** if they want the tools installed or updated too.\n- Run **`infra-kit setup --skip-tools`** (`skipTools: true`) for the additive local writes with nothing\n installed. That is the whole reason the flag exists: without it, removing `init` would have deleted a\n capability rather than renamed one.\n\nSay which one you chose. Running `infra-kit audit --fix` inside that repo rewrites the stale block.\n\n## 6. What not to do\n\n- Do not work around a refusal in section 3 with `Bash`. The refusal is the control.\n- Do not read `isError: true` on a `confirmation_required` payload as a failure. See section 4.\n- Do not report success from the exit status alone. Read every `tools[].action` and name the refusals.\n- Do not call this tool to answer a question. Call `doctor` \u2014 it changes nothing and prompts no one.\n';var m={"release-create":M.trimEnd(),setup:q.trimEnd()};var j=async e=>{e.registerPrompt("release-create",{title:"Cut a release",description:'The procedure for cutting a release with infra-kit: preconditions, the two-call confirm protocol, and what "next" resolves against.'},()=>({messages:[{role:"user",content:{type:"text",text:m["release-create"]}}]}))};var ce="infra-kit://config",de="infra-kit://dev-context",ue="infra-kit://workflow/release-create",he="infra-kit://workflow/setup",me={loadConfig:P,readDevContext:()=>w()},y=(e,t)=>({contents:[{uri:e,mimeType:"application/json",text:JSON.stringify(t,null,2)}]}),L=(e,t)=>{e.registerResource(`infra-kit-workflow-${t.key}`,t.uri,{title:t.title,description:t.description,mimeType:"text/markdown"},r=>({contents:[{uri:r.toString(),mimeType:"text/markdown",text:m[t.key]}]}))},$=async(e,t=me)=>{e.registerResource("infra-kit-config",ce,{title:"infra-kit config",description:"The merged infra-kit.json configuration (all override layers applied). Read-only.",mimeType:"application/json"},async r=>{try{return y(r.toString(),await t.loadConfig())}catch(o){return y(r.toString(),{error:o instanceof Error?o.message:String(o)})}}),e.registerResource("infra-kit-dev-context",de,{title:"infra-kit dev context",description:"What `infra-kit dev` last wrote: backends currently up, their ports/origins, and fragment freshness. Read-only and advisory (no liveness probe). No active session resolves to an empty payload.",mimeType:"application/json"},r=>y(r.toString(),t.readDevContext())),L(e,{key:"release-create",uri:ue,title:"release-create procedure",description:'How to cut a release with the release-create tool: the preconditions, the two-call confirm protocol, and what the "next" token actually resolves against. Read this before calling mcp__infra-kit__release-create.'}),L(e,{key:"setup",uri:he,title:"setup procedure",description:"How to set a machine up with the setup tool: the ordered local writes, then the dependency converge; what tools/mode/skipTools each narrow; which recipes are printed instead of run and why; and what to run when a repo still tells you to set it up some older way. Read this before calling mcp__infra-kit__setup."})};import{z as T}from"zod";import{createRequestStateCodec as fe}from"@modelcontextprotocol/server";import{randomBytes as pe}from"node:crypto";var ge=new Set(["confirm","confirmToken"]),we=600,G=e=>({toolName:e}),v=e=>{if(Array.isArray(e))return e.map(v);if(e!==null&&typeof e=="object"){let t=e;return Object.fromEntries(Object.keys(t).sort().map(r=>[r,v(t[r])]))}return e},b=e=>JSON.stringify(v(e)),c=e=>typeof e!="object"||e===null?{}:Object.fromEntries(Object.entries(e).filter(([t])=>!ge.has(t))),U=(e={})=>{let t=fe({key:e.key??pe(32),ttlSeconds:e.ttlSeconds??we,bind:r=>r.toolName});return{mint:(r,o)=>t.mint(r,G(o)),verify:(r,o)=>t.verify(r,G(o))}},W,H=()=>(W??=U(),W),ye=e=>{if(typeof e!="object"||e===null)return;let{confirmToken:t}=e;return typeof t=="string"?t:void 0},ve=new Set(["malformed","mac","expired","bind"]),be=e=>{let t=e instanceof Error?e.message:"";return ve.has(t)?t:"malformed"},K=async(e,t,r)=>e.mint({args:b(c(r))},t),B=async(e,t,r)=>{let o=ye(r);if(o===void 0)return{ok:!1,reason:"absent"};let n;try{n=await e.verify(o,t)}catch(a){return{ok:!1,reason:be(a)}}return n.args!==b(c(r))?{ok:!1,reason:"mismatch"}:{ok:!0}};import{acceptedContent as ke,inputRequired as J,inputResponse as xe}from"@modelcontextprotocol/server";var k="args",Z=3e3,V=e=>{let t=xe(e,k);return t.kind==="elicit"?t.action:"missing"},Ce=async(e,t)=>new Promise(r=>{let o=setTimeout(()=>{r(null)},t);e.then(n=>{clearTimeout(o),r(n)},()=>{clearTimeout(o),r(null)})}),Y=async(e,t,r)=>{try{return await Ce(e.buildRequestedSchema(t),r)}catch{return null}},X=async(e,t,r)=>{let o=await Y(e,t,r);if(o===null)return null;try{return J({inputRequests:{[k]:J.elicit({message:e.message,requestedSchema:o})}})}catch{return null}},Q=async(e,t,r,o)=>{let n=await Y(e,t,o);if(n===null)return null;let a;try{a=ke(r,k,n)}catch{return null}if(a===void 0)return null;try{return e.toArgs(a,t)}catch{return null}},z=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),ee=(e,t)=>{if(!z(e))return!1;if(!z(t))return!0;for(let[r,o]of Object.entries(e)){if(!(r in t))return!0;let n=t[r];if(Array.isArray(o)&&(!Array.isArray(n)||n.length!==o.length))return!0}return!1};var Te=e=>typeof e=="object"&&e!==null&&e.confirm===!0,Re=e=>e.gated?e.gated&&!e.confirmed&&e.responses===void 0&&e.canForm&&e.hasProvider&&e.formable?"form":e.gated&&!e.confirmed&&e.responses!==void 0&&!e.accepted?"declined":e.gated&&!e.confirmed&&(e.responses===void 0||e.accepted)?"gate":"verify":"run",x=e=>({content:E(JSON.stringify(e,null,2)),structuredContent:e,isError:!0}),Se="The values you submitted in the form could NOT be applied and were DISCARDED \u2014 they failed validation or narrowed the arguments \u2014 so the arguments shown above are the ORIGINAL ones, not your selection.",Ae="This tool does not prompt for its arguments; you are being asked to approve the values shown above.",Ee=async(e,t,r,o)=>{let n=c(r),a=await K(e,t,r),s=[o.formDiscarded?Se:void 0,`${t} mutates external state and is gated. It was NOT executed. Re-call ${t} with the same arguments plus "confirm": true and this "confirmToken" to execute.`,o.hasProvider?void 0:Ae].filter(p=>p!==void 0).join(" ");return x({status:"confirmation_required",tool:t,resolvedArgs:n,confirmToken:a,formDiscarded:o.formDiscarded,message:s})},De=(e,t)=>x({status:"form_declined",tool:e,action:t,message:`${e} was NOT executed: the argument form came back as "${t}". No confirmation is pending \u2014 call ${e} again to start over.`}),Pe={absent:'no "confirmToken" was supplied',malformed:'the "confirmToken" is malformed',mac:'the "confirmToken" was not issued by this server',expired:'the "confirmToken" has expired',bind:'the "confirmToken" was issued for a different tool',mismatch:'the arguments differ from the ones the "confirmToken" was issued for'},_e=(e,t)=>x({status:"confirmation_refused",tool:e,reason:t,message:`${e} was NOT executed: ${Pe[t]}. Call ${e} again WITHOUT "confirm" to receive a fresh gate, then re-call with the same arguments plus "confirm": true and the returned "confirmToken".`}),te=e=>{try{return e()}catch{return!1}},Ie=async(e,t,r,o)=>{let n=e.formProvider;if(n===void 0||o!=="accept")return{params:t,formDiscarded:!1};let a=await Q(n,t,r,e.formDeadlineMs);return a===null?(i.info({msg:`Tool execution form discarded (validation): ${e.toolName}`}),{params:t,formDiscarded:!0}):ee(c(t),a)?(i.info({msg:`Tool execution form discarded (narrowed): ${e.toolName}`}),{params:t,formDiscarded:!0}):{params:a,formDiscarded:!1}},Oe=async(e,t,r,o,n)=>{if(o==="form"&&e.formProvider!==void 0){let s=await X(e.formProvider,t,e.formDeadlineMs);if(s!==null)return i.info({msg:`Tool execution form requested: ${e.toolName}`}),s;i.info({msg:`Tool execution form unavailable: ${e.toolName}`})}let a=await Ie(e,t,r,n);return i.info({msg:`Tool execution gated (awaiting confirm): ${e.toolName}`}),await Ee(e.codec,e.toolName,a.params,{formDiscarded:a.formDiscarded,hasProvider:e.formProvider!==void 0})},Ne=async(e,t,r)=>{let o=r?.mcpReq?.inputResponses,n=V(o),a=Re({gated:e.requiresHumanConfirm===!0,confirmed:Te(t),responses:o,canForm:te(()=>e.getClientCapabilities?.()?.elicitation?.form!==void 0),hasProvider:e.formProvider!==void 0,formable:te(()=>e.formProvider?.isFormable(t)===!0),accepted:n==="accept"});if(a==="run")return null;if(a==="declined")return i.info({msg:`Tool execution form declined (${n}): ${e.toolName}`}),De(e.toolName,n);if(a==="verify"){let s=await B(e.codec,e.toolName,t);return s.ok?null:(i.info({msg:`Tool execution refused (${s.reason}): ${e.toolName}`}),_e(e.toolName,s.reason))}return await Oe(e,t,o,a,n)},C=({toolName:e,handler:t,requiresHumanConfirm:r,formProvider:o,getClientCapabilities:n,confirmCodec:a,formDeadlineMs:s})=>{let p={toolName:e,codec:a??H(),requiresHumanConfirm:r,formProvider:o,getClientCapabilities:n,formDeadlineMs:s??Z};return async(u,R)=>{i.info({msg:`Tool execution started: ${e}`,params:u,sessionId:R?.sessionId});try{await I(),_.reset();let d=await Ne(p,u,R);if(d!==null)return d;let ie=await t({...u,confirmedCommand:!0});return i.info({msg:`Tool execution successful: ${e}`}),ie}catch(d){throw i.error({err:d,params:u,msg:`Tool execution failed: ${e}`}),d}}};var re=async e=>{for(let t of O())e.registerTool(t.name,{title:t.title,description:t.description,inputSchema:T.object(t.requiresHumanConfirm===!0?Fe(t.inputSchema):t.inputSchema),outputSchema:T.object(t.outputSchema),annotations:t.annotations,_meta:t.meta},Me(C({toolName:t.name,handler:t.handler,requiresHumanConfirm:t.requiresHumanConfirm,formProvider:t.formProvider,getClientCapabilities:()=>e.server.getClientCapabilities()})))},Fe=e=>({...e,confirmToken:T.string().optional().describe("Round-2 only: the token returned by the round-1 gate, proving the arguments are unchanged.")}),Me=e=>(t,r)=>e(t,r);async function oe(){S.enabled=!0;let e=new qe({name:"infra-kit",version:D.version},{capabilities:{resources:{listChanged:!0},tools:{},prompts:{}}});return await j(e),await $(e),await re(e),e}N();var l=A(),$e=async()=>{try{return await oe()}catch(e){l.error({err:e,msg:"Failed to create MCP server"}),l.flush(),f.exit(1)}};F(l);var Ge=je($e,{onerror:e=>{l.error({err:e,msg:"MCP stdio entry error"}),l.flush()}});l.info({msg:"MCP stdio entry started."});var ne=!1,ae=async e=>{if(!ne){ne=!0,l.info({msg:`Received ${e}. Shutting down...`});try{await Promise.race([Ge.close(),Le(1500)])}catch(t){l.error({err:t,msg:"MCP stdio close failed during shutdown"})}l.flush(),f.exit(0)}};f.on("SIGINT",()=>{ae("SIGINT")});f.on("SIGTERM",()=>{ae("SIGTERM")});
1
+ import{a as E,d as w,fa as O,ia as N,s as I}from"./chunk-X2POR5VH.js";import"./chunk-Q23IJP6D.js";import{a as D}from"./chunk-6GWRKIZ7.js";import"./chunk-XJOY7J3G.js";import{i as _}from"./chunk-HXBIE4JP.js";import{J as g,K as A,L as i,c as S,fa as P}from"./chunk-Q4HKBLQG.js";import"./chunk-D4UYDBN5.js";import"./chunk-QWVLAZ6N.js";import{serveStdio as je}from"@modelcontextprotocol/server/stdio";import f from"node:process";import{setTimeout as Le}from"node:timers/promises";import h from"node:process";var F=e=>{h.on("uncaughtException",t=>{e.fatal({err:t,msg:"Uncaught Exception"}),e.error(`Uncaught Exception! Check ${g}. Shutting down...`),e.flush(),h.exit(1)}),h.on("unhandledRejection",(t,r)=>{e.fatal({reason:t,promise:r,msg:"Unhandled Rejection"}),e.error(`Unhandled Rejection! Check ${g}. Shutting down...`),e.flush(),h.exit(1)})};import{McpServer as qe}from"@modelcontextprotocol/server";var M='# release-create \u2014 cutting a release through infra-kit\n\nThe tool is `mcp__infra-kit__release-create`. Everything below is about calling that tool.\n\nDo not shell out. A `Bash` call running `git switch`, `git push` or `gh pr create` reproduces none\nof the preconditions below and bypasses the confirm gate in section 2 \u2014 which is the only place a\nhuman approves the release.\n\n## 1. Preconditions\n\nCheck these before the first call; each one is a refusal the human has to clear, not something to\nwork around.\n\n- **The main repository checkout, not a linked worktree.** The tool refuses outright from inside a\n linked worktree.\n- **A clean working tree.** Uncommitted changes are refused; the human commits or stashes.\n- **No other worktree holding the base branch.** Regular releases branch off `dev`, hotfixes off\n `main`. If a linked worktree has that branch checked out, the tool refuses and names the path.\n- **Jira configured.** Every release gets a matching fix version, so `JIRA_BASE_URL`,\n `JIRA_EMAIL`, `JIRA_PROJECT_ID` and `JIRA_TOKEN` (or `JIRA_API_TOKEN`) must be in the\n environment \u2014 load them with `ik env-load` and source the file it returns. The check runs before\n anything is cut.\n\n**You do not have to already be on the base branch.** The tool runs `git fetch origin`,\n`git switch <base>` and `git pull --ff-only` itself. That is a real side effect on the human\'s\ncheckout: say so before call 2.\n\n## 2. The two-call confirm protocol\n\n`release-create` is gated. **The first call never executes anything.**\n\n**Call 1** \u2014 send the real arguments, with no `confirm` and no `confirmToken`. The result is a gate\npayload, `{"status": "confirmation_required", ...}`, and it carries `"isError": true`.\n\n**That `isError` does not mean the call failed.** It is set because the payload is a gate rather\nthan the tool\'s declared output. Nothing was created, nothing was pushed, nothing was switched. An\nagent that reads it as a failure \u2014 and gives up, or retries, or falls back to `Bash` \u2014 has skipped\nthe human approval this protocol exists for. Do none of those.\n\nThe gate payload carries two things you need:\n\n- `resolvedArgs` \u2014 exactly the arguments the server bound. **Show these to the human.** This is the\n approval moment; there is no other one.\n- `confirmToken` \u2014 an HMAC bound to the tool name and to those exact arguments. It expires 600\n seconds after it is minted.\n\n**Call 2** \u2014 repeat the **same arguments**, unchanged, plus `"confirm": true` and the\n`confirmToken` from call 1.\n\nChange any argument between the two calls and round 2 comes back as\n`{"status": "confirmation_refused", "reason": "mismatch"}`. That is terminal \u2014 it is not a second\ngate. The other reasons are `absent`, `malformed`, `mac`, `expired` and `bind`, and every one of\nthem recovers the same way: call again **without** `confirm` to mint a fresh gate, then re-call\nwith those arguments and the new token. Never retry call 2 with the old token.\n\nIf the human wants different arguments, go back to call 1 with the new arguments. Do not edit the\narguments and reuse the token \u2014 that is exactly what `mismatch` refuses.\n\n## 3. What goes in `releases`\n\n`releases` is an array with at least one entry. Every entry carries **exactly one** of:\n\n- `version` \u2014 a semver string such as `"1.64.0"`, or the literal token `"next"`.\n- `name` \u2014 a free-form kebab-case identifier such as `"checkout-redesign"`.\n\nThey are mutually exclusive and one is required. An entry with both, or with neither, is rejected\nby the schema before the tool runs.\n\nEach entry also carries `type` (`"regular"` or `"hotfix"`, default `"regular"`) and an optional\n`description`, which becomes the Jira fix version\'s description and feeds the PR body.\n\n### The `"next"` token\n\n`"next"` is version-only \u2014 a named release never auto-bumps. It resolves against the union of the\nremote `release/v*` branches on `origin` and the project\'s Jira fix versions:\n\n- `"regular"` bumps the minor and resets the patch: `1.63.2` becomes `1.64.0`.\n- `"hotfix"` bumps the patch on the highest minor: `1.63.2` becomes `1.63.3`.\n\nSeveral `"next"` entries in one call advance sequentially rather than all resolving to the same\nversion.\n\n**Be honest about what `"next"` could see.** The two sources are queried in parallel and a source\nthat fails is logged and dropped, not raised \u2014 if the Jira call fails, `"next"` is computed from\nthe remote branches alone and can land on a version Jira already knows about. If neither source\nyields a prior version the tool refuses and asks for an explicit one. When the exact number\nmatters, pass the semver instead of the token.\n\n### Reading `$ARGUMENTS`\n\nThe `/infra-kit:release-create` command hands you `$ARGUMENTS` verbatim, and its argument hint is\n`[--hotfix] [--desc <text>] [<version|name>]`. **Those two flags are conventions of this command, not\nCLI flags** \u2014 `infra-kit release create` accepts neither, and the tool takes neither. They exist so a\nhuman can type the whole request on one line, and it is your job to translate them:\n\n- `--hotfix` \u2192 `type: "hotfix"` on every entry you build. Its absence means `"regular"`.\n- `--desc <text>` \u2192 `description` on the entry. The text runs to the end of the argument string.\n- The bare token \u2192 `version` when it is a semver or the literal `next`, `name` when it is kebab-case.\n\nSo `--hotfix --desc "Card expiry fix" 1.63.3` is one entry:\n`{version: "1.63.3", type: "hotfix", description: "Card expiry fix"}`.\n\nIf `$ARGUMENTS` is empty, ask the human what to cut rather than guessing a version \u2014 and read the\n`"next"` caveats above before offering it.\n\n**Precedence, when a form is also involved.** If the server answers with an argument form and the\nhuman edits it, **the form wins field by field wherever the human supplied a value, and the values\nyou parsed from `$ARGUMENTS` win everywhere else.** A human who typed `--hotfix` and then picked\n`regular` in the form gets `regular` \u2014 they saw the field and changed it. A human who typed\n`--hotfix` and left `type` untouched gets `hotfix`. Never rebuild the entry from the form alone: that\nconverts every untouched field into a silent overwrite by a value the human never saw.\n\n### Batches\n\nOne call may create several releases, but **all entries must share the same `type`**. Regular and\nhotfix branch off different bases, so a mixed batch is rejected \u2014 cut them in separate\ninvocations.\n\nA batch does not stop at the first failure. Each entry is attempted and the result reports\n`successCount`, `failureCount`, `createdBranches` and `failedReleases`. Read all four before\ntelling the human the release was created: partial success is a normal outcome here.\n\n## 4. What one release does\n\nPer entry, in order: fetch and switch to the base branch, cut `release/v<semver>` (or\n`release/<name>`), open a GitHub release PR, and create or reuse the Jira fix version (`v<semver>`\nor `<name>`).\n\nTwo consequences worth stating before call 2:\n\n- An existing fix version is **reused**, and a `description` that differs is written through to it,\n so the PR and the fix version cannot disagree.\n- A fix version that is already released or archived is **refused**, not reused. The human either\n picks a different version or un-releases it in Jira.\n\n## 5. What not to do\n\n- Do not work around a refusal with `git` or `gh`. Every refusal in section 1 is a state only the\n human can clear.\n- Do not invent a list of candidate versions for the human. Pass `"next"` and let the server\n resolve it from the real branches and fix versions, or ask for the exact semver.\n- Do not read `isError: true` on a `confirmation_required` payload as a failure. See section 2.\n';var q='# setup \u2014 bringing a machine to a working infra-kit\n\nThe tool is `mcp__infra-kit__setup`. Everything below is about calling that tool.\n\nThe same code runs behind `infra-kit setup` in a terminal, so this body describes both spellings: the\nCLI flag first, the tool field it sets second. They are one implementation, not two.\n\nDo not shell out. A `Bash` call running `brew install`, `curl \u2026 | bash` or the writers in section 1\nreproduces none of the refusals in section 3 and bypasses the confirm gate in section 4 \u2014 which is the\nonly place a human approves an install.\n\n**If all you want is to know what this machine looks like, call `doctor` instead.** It reports the same\nfive tools plus the rest of the setup, mutates nothing, and raises no confirmation prompt. `infra-kit setup` is\nthe write path; `doctor` is the read path, and they are separate tools precisely so that asking a\nquestion does not cost an approval.\n\n## 1. What one call does, in order\n\nTwo halves, and **both always run**. Neither short-circuits the other: an init-half failure is recorded\nand the dependency half still runs.\n\n### Step 1 \u2014 the init half: local, offline, additive, near-instant\n\nIn this order:\n\n1. the managed block in `~/.zshrc` \u2014 the shell integration\n2. the four config migrations, in their recorded order\n3. the user-global config seed\n4. the repo\'s agent-instruction files (`CLAUDE.md` guidance blocks) \u2014 **non-fatal**; a repo it cannot\n resolve is warned about, not failed on\n5. the git-root resolution for writes, warning when the two root gates disagree\n6. the Claude Code plugin pointer \u2014 `.claude/settings.json`, `.mcp.json`, and the plugin install\n7. the per-project config reseed\n8. a warning when `$SHELL` is not zsh\n\nEvery writer here is additive and never overwrites. Nothing in this half installs software and nothing\nreaches the network.\n\n**It runs first deliberately.** Step 1.6 is what makes the MCP surface usable at all, so it must not sit\nbehind a network converge that can be slow or fail.\n\n### Step 2 \u2014 the dependency converge\n\nFive tools, serially, in registry order: **brew, aws, gh, doppler, portless**. Serial and ordered\nbecause the recipes have prerequisites \u2014 `gh` and `doppler` both need `brew`, and doppler\'s own two\nsteps (gnupg, then the tap) must not interleave with another tool\'s.\n\nPer tool: install it when it is absent, update it when it is present, skip it when its manager is not\none infra-kit manages. A recipe the risk predicate refuses is **printed, not run** \u2014 section 3.\n\n### Step 3 \u2014 one combined summary\n\nOne line per tool, then the exact argv for every refused recipe, then the `source ~/.zshrc` reminder\nlast of all.\n\n### How to read the result\n\n- `init` \u2014 one entry per step above, each with an `outcome` of `written`, `unchanged`, `skipped` or\n `warned`, and the same message a human would have read.\n- `tools` \u2014 one entry per requested dependency: `action` (`installed`, `updated`, `skipped`, `refused`\n or `failed`), the `before` state, the `commands` that were run or would have been, and a one-line\n `detail`.\n- `converged` \u2014 whether the dependency step could act at all. `false` under `skipTools`.\n- `changed` \u2014 whether anything was installed or updated.\n- `allSucceeded` \u2014 whether no tool **failed**. **A refusal is not a failure**, so this stays `true` when\n a recipe was printed instead of run.\n\nThe process exits non-zero when either half hard-failed. **Do not read a success as "everything is\ninstalled"** \u2014 read `tools[].action`, and tell the human about every `refused` entry.\n\n## 2. The flags, and what each one narrows\n\nThe default \u2014 no flag, no field \u2014 converges all five tools.\n\n- `--tools <ids...>` \u2192 `tools: ["gh", "doppler"]`. Converge **only those ids**. Same behaviour per\n tool, smaller set. The ids are `brew`, `aws`, `gh`, `doppler` and `portless`.\n- `--update [ids...]` \u2192 `mode: "update"`. **Never installs.** A tool that is present is updated; a tool\n that is absent is reported `skipped` with the reason, and its install recipe is not run. Given ids, it\n also narrows the set, so `--update gh` is "update gh, and nothing else, and only if it is there".\n- `--skip-tools` \u2192 `skipTools: true`. A **read-only probe**. The init half still runs \u2014 it is local and\n additive \u2014 and then each tool is reported with what it needs and the exact argv that would fix it.\n Nothing is installed and nothing is updated.\n\n**`--skip-tools` with `--tools` or `--update` is a usage error, not a precedence rule.** Every\nprecedence answer is wrong: honouring `--skip-tools` would ignore a set the caller chose, and honouring\nthe other would install software the caller asked not to install. The call is refused instead.\n\n## 3. Recipes that are printed rather than run\n\nWhether a recipe may run unattended is **computed**, not a per-recipe flag someone set. Two of the four\nconjuncts are static and are applied to every recipe unconditionally:\n\n- **`needs-sudo`** \u2014 the recipe escalates privilege.\n- **`fetches-network-script`** \u2014 the recipe pipes a script fetched at run time into a shell.\n\nTwo are detection-based: **`manager-absent`** (the package manager the recipe drives is not on this\nhost) and **`manager-mismatch`** (a different manager owns the binary \u2014 running `brew upgrade` against\nan npm install, or the vendor\'s self-updater against a Homebrew keg, is the split-brain infra-kit\nrefuses for itself).\n\n**Two recipes fail the static conjuncts, and they are the two bootstraps:**\n\n- **the Homebrew bootstrap** \u2014 `/bin/bash -c "$(curl -fsSL \u2026/install.sh)"` \u2014 fails **both**: it pipes a\n network-fetched script **and** needs sudo on macOS.\n- **the first AWS CLI install** \u2014 `curl -fsSL https://awscli.amazonaws.com/v2/install.sh | bash` \u2014\n needs no sudo (it installs under `$HOME`), but it is still a network-fetched script.\n\nBoth are the tools\' own documented installers, and neither is a bug to route around. They are refused\n**by computation, applied before any detection runs**, which is what makes the refusal trustworthy: it\ncannot be widened by a probe getting something wrong, only narrowed.\n\nOnce Homebrew exists, `gh` and `doppler` install through it and run unattended; once the AWS CLI exists,\n`aws update` is a plain no-sudo recipe and runs. The refusals are a first-install cost, not permanent.\n\nWhat to do with one: the entry\'s `commands` array is the exact argv, one string per step. **Show it to\nthe human and let them run it themselves.** Do not reconstruct it as a `Bash` call \u2014 that is the same\nunattended `sudo` and the same piped script, with the control removed.\n\n## 4. The confirm gate\n\n`mcp__infra-kit__setup` is gated, and **both gates fire on every call \u2014 `skipTools` included**.\n\n**Call 1** \u2014 send the real arguments, with no `confirm` and no `confirmToken`. The result is a gate\npayload, `{"status": "confirmation_required", \u2026}`, carrying `"isError": true`.\n\n**That `isError` does not mean the call failed.** Nothing was written and nothing was installed. An\nagent that reads it as a failure \u2014 and gives up, or retries, or falls back to `Bash` \u2014 has skipped the\nhuman approval this protocol exists for. Do none of those. Show the human `resolvedArgs`; that is the\napproval moment.\n\n**Call 2** \u2014 repeat the **same arguments**, unchanged, plus `"confirm": true` and the `confirmToken`\nfrom call 1. Change any argument between the two and round 2 comes back\n`{"status": "confirmation_refused", "reason": "mismatch"}`, which is terminal, not a second gate. Every\nrefusal reason recovers the same way: call again **without** `confirm` for a fresh gate, then re-call\nwith the new token. Never retry with the old one.\n\nThe tool also carries `anthropic/requiresUserInteraction`, so the host prompts a human even where an\nallow rule would otherwise skip it. One install therefore costs two prompts. That is intended: neither\ngate substitutes for the other, and neither substitutes for the computed refusals in section 3, which\nare the only control that ships inside the CLI itself.\n\n## 5. There is no `init` command\n\nIt was removed outright. The binary rejects the name \u2014 an instruction that still says to run it fails\nat the parser rather than quietly doing something else.\n\nRepos can still say it. A consumer\'s committed CLAUDE.md block is rewritten only from inside that repo,\nso a repo upgrades the global CLI without its own text changing and can sit arbitrarily far behind. So\nwhen a human asks for "init", or a repo\'s instructions still name it:\n\n- Run **`infra-kit setup`** if they want the tools installed or updated too.\n- Run **`infra-kit setup --skip-tools`** (`skipTools: true`) for the additive local writes with nothing\n installed. That is the whole reason the flag exists: without it, removing `init` would have deleted a\n capability rather than renamed one.\n\nSay which one you chose. Running `infra-kit audit --fix` inside that repo rewrites the stale block.\n\n## 6. What not to do\n\n- Do not work around a refusal in section 3 with `Bash`. The refusal is the control.\n- Do not read `isError: true` on a `confirmation_required` payload as a failure. See section 4.\n- Do not report success from the exit status alone. Read every `tools[].action` and name the refusals.\n- Do not call this tool to answer a question. Call `doctor` \u2014 it changes nothing and prompts no one.\n';var m={"release-create":M.trimEnd(),setup:q.trimEnd()};var j=async e=>{e.registerPrompt("release-create",{title:"Cut a release",description:'The procedure for cutting a release with infra-kit: preconditions, the two-call confirm protocol, and what "next" resolves against.'},()=>({messages:[{role:"user",content:{type:"text",text:m["release-create"]}}]}))};var ce="infra-kit://config",de="infra-kit://dev-context",ue="infra-kit://workflow/release-create",he="infra-kit://workflow/setup",me={loadConfig:P,readDevContext:()=>w()},y=(e,t)=>({contents:[{uri:e,mimeType:"application/json",text:JSON.stringify(t,null,2)}]}),L=(e,t)=>{e.registerResource(`infra-kit-workflow-${t.key}`,t.uri,{title:t.title,description:t.description,mimeType:"text/markdown"},r=>({contents:[{uri:r.toString(),mimeType:"text/markdown",text:m[t.key]}]}))},$=async(e,t=me)=>{e.registerResource("infra-kit-config",ce,{title:"infra-kit config",description:"The merged infra-kit.json configuration (all override layers applied). Read-only.",mimeType:"application/json"},async r=>{try{return y(r.toString(),await t.loadConfig())}catch(o){return y(r.toString(),{error:o instanceof Error?o.message:String(o)})}}),e.registerResource("infra-kit-dev-context",de,{title:"infra-kit dev context",description:"What `infra-kit dev` last wrote: backends currently up, their ports/origins, and fragment freshness. Read-only and advisory (no liveness probe). No active session resolves to an empty payload.",mimeType:"application/json"},r=>y(r.toString(),t.readDevContext())),L(e,{key:"release-create",uri:ue,title:"release-create procedure",description:'How to cut a release with the release-create tool: the preconditions, the two-call confirm protocol, and what the "next" token actually resolves against. Read this before calling mcp__infra-kit__release-create.'}),L(e,{key:"setup",uri:he,title:"setup procedure",description:"How to set a machine up with the setup tool: the ordered local writes, then the dependency converge; what tools/mode/skipTools each narrow; which recipes are printed instead of run and why; and what to run when a repo still tells you to set it up some older way. Read this before calling mcp__infra-kit__setup."})};import{z as T}from"zod";import{createRequestStateCodec as fe}from"@modelcontextprotocol/server";import{randomBytes as pe}from"node:crypto";var ge=new Set(["confirm","confirmToken"]),we=600,G=e=>({toolName:e}),v=e=>{if(Array.isArray(e))return e.map(v);if(e!==null&&typeof e=="object"){let t=e;return Object.fromEntries(Object.keys(t).sort().map(r=>[r,v(t[r])]))}return e},b=e=>JSON.stringify(v(e)),c=e=>typeof e!="object"||e===null?{}:Object.fromEntries(Object.entries(e).filter(([t])=>!ge.has(t))),U=(e={})=>{let t=fe({key:e.key??pe(32),ttlSeconds:e.ttlSeconds??we,bind:r=>r.toolName});return{mint:(r,o)=>t.mint(r,G(o)),verify:(r,o)=>t.verify(r,G(o))}},W,H=()=>(W??=U(),W),ye=e=>{if(typeof e!="object"||e===null)return;let{confirmToken:t}=e;return typeof t=="string"?t:void 0},ve=new Set(["malformed","mac","expired","bind"]),be=e=>{let t=e instanceof Error?e.message:"";return ve.has(t)?t:"malformed"},K=async(e,t,r)=>e.mint({args:b(c(r))},t),B=async(e,t,r)=>{let o=ye(r);if(o===void 0)return{ok:!1,reason:"absent"};let n;try{n=await e.verify(o,t)}catch(a){return{ok:!1,reason:be(a)}}return n.args!==b(c(r))?{ok:!1,reason:"mismatch"}:{ok:!0}};import{acceptedContent as ke,inputRequired as J,inputResponse as xe}from"@modelcontextprotocol/server";var k="args",Z=3e3,V=e=>{let t=xe(e,k);return t.kind==="elicit"?t.action:"missing"},Ce=async(e,t)=>new Promise(r=>{let o=setTimeout(()=>{r(null)},t);e.then(n=>{clearTimeout(o),r(n)},()=>{clearTimeout(o),r(null)})}),Y=async(e,t,r)=>{try{return await Ce(e.buildRequestedSchema(t),r)}catch{return null}},X=async(e,t,r)=>{let o=await Y(e,t,r);if(o===null)return null;try{return J({inputRequests:{[k]:J.elicit({message:e.message,requestedSchema:o})}})}catch{return null}},Q=async(e,t,r,o)=>{let n=await Y(e,t,o);if(n===null)return null;let a;try{a=ke(r,k,n)}catch{return null}if(a===void 0)return null;try{return e.toArgs(a,t)}catch{return null}},z=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),ee=(e,t)=>{if(!z(e))return!1;if(!z(t))return!0;for(let[r,o]of Object.entries(e)){if(!(r in t))return!0;let n=t[r];if(Array.isArray(o)&&(!Array.isArray(n)||n.length!==o.length))return!0}return!1};var Te=e=>typeof e=="object"&&e!==null&&e.confirm===!0,Re=e=>e.gated?e.gated&&!e.confirmed&&e.responses===void 0&&e.canForm&&e.hasProvider&&e.formable?"form":e.gated&&!e.confirmed&&e.responses!==void 0&&!e.accepted?"declined":e.gated&&!e.confirmed&&(e.responses===void 0||e.accepted)?"gate":"verify":"run",x=e=>({content:E(JSON.stringify(e,null,2)),structuredContent:e,isError:!0}),Se="The values you submitted in the form could NOT be applied and were DISCARDED \u2014 they failed validation or narrowed the arguments \u2014 so the arguments shown above are the ORIGINAL ones, not your selection.",Ae="This tool does not prompt for its arguments; you are being asked to approve the values shown above.",Ee=async(e,t,r,o)=>{let n=c(r),a=await K(e,t,r),s=[o.formDiscarded?Se:void 0,`${t} mutates external state and is gated. It was NOT executed. Re-call ${t} with the same arguments plus "confirm": true and this "confirmToken" to execute.`,o.hasProvider?void 0:Ae].filter(p=>p!==void 0).join(" ");return x({status:"confirmation_required",tool:t,resolvedArgs:n,confirmToken:a,formDiscarded:o.formDiscarded,message:s})},De=(e,t)=>x({status:"form_declined",tool:e,action:t,message:`${e} was NOT executed: the argument form came back as "${t}". No confirmation is pending \u2014 call ${e} again to start over.`}),Pe={absent:'no "confirmToken" was supplied',malformed:'the "confirmToken" is malformed',mac:'the "confirmToken" was not issued by this server',expired:'the "confirmToken" has expired',bind:'the "confirmToken" was issued for a different tool',mismatch:'the arguments differ from the ones the "confirmToken" was issued for'},_e=(e,t)=>x({status:"confirmation_refused",tool:e,reason:t,message:`${e} was NOT executed: ${Pe[t]}. Call ${e} again WITHOUT "confirm" to receive a fresh gate, then re-call with the same arguments plus "confirm": true and the returned "confirmToken".`}),te=e=>{try{return e()}catch{return!1}},Ie=async(e,t,r,o)=>{let n=e.formProvider;if(n===void 0||o!=="accept")return{params:t,formDiscarded:!1};let a=await Q(n,t,r,e.formDeadlineMs);return a===null?(i.info({msg:`Tool execution form discarded (validation): ${e.toolName}`}),{params:t,formDiscarded:!0}):ee(c(t),a)?(i.info({msg:`Tool execution form discarded (narrowed): ${e.toolName}`}),{params:t,formDiscarded:!0}):{params:a,formDiscarded:!1}},Oe=async(e,t,r,o,n)=>{if(o==="form"&&e.formProvider!==void 0){let s=await X(e.formProvider,t,e.formDeadlineMs);if(s!==null)return i.info({msg:`Tool execution form requested: ${e.toolName}`}),s;i.info({msg:`Tool execution form unavailable: ${e.toolName}`})}let a=await Ie(e,t,r,n);return i.info({msg:`Tool execution gated (awaiting confirm): ${e.toolName}`}),await Ee(e.codec,e.toolName,a.params,{formDiscarded:a.formDiscarded,hasProvider:e.formProvider!==void 0})},Ne=async(e,t,r)=>{let o=r?.mcpReq?.inputResponses,n=V(o),a=Re({gated:e.requiresHumanConfirm===!0,confirmed:Te(t),responses:o,canForm:te(()=>e.getClientCapabilities?.()?.elicitation?.form!==void 0),hasProvider:e.formProvider!==void 0,formable:te(()=>e.formProvider?.isFormable(t)===!0),accepted:n==="accept"});if(a==="run")return null;if(a==="declined")return i.info({msg:`Tool execution form declined (${n}): ${e.toolName}`}),De(e.toolName,n);if(a==="verify"){let s=await B(e.codec,e.toolName,t);return s.ok?null:(i.info({msg:`Tool execution refused (${s.reason}): ${e.toolName}`}),_e(e.toolName,s.reason))}return await Oe(e,t,o,a,n)},C=({toolName:e,handler:t,requiresHumanConfirm:r,formProvider:o,getClientCapabilities:n,confirmCodec:a,formDeadlineMs:s})=>{let p={toolName:e,codec:a??H(),requiresHumanConfirm:r,formProvider:o,getClientCapabilities:n,formDeadlineMs:s??Z};return async(u,R)=>{i.info({msg:`Tool execution started: ${e}`,params:u,sessionId:R?.sessionId});try{await I(),_.reset();let d=await Ne(p,u,R);if(d!==null)return d;let ie=await t({...u,confirmedCommand:!0});return i.info({msg:`Tool execution successful: ${e}`}),ie}catch(d){throw i.error({err:d,params:u,msg:`Tool execution failed: ${e}`}),d}}};var re=async e=>{for(let t of O())e.registerTool(t.name,{title:t.title,description:t.description,inputSchema:T.object(t.requiresHumanConfirm===!0?Fe(t.inputSchema):t.inputSchema),outputSchema:T.object(t.outputSchema),annotations:t.annotations,_meta:t.meta},Me(C({toolName:t.name,handler:t.handler,requiresHumanConfirm:t.requiresHumanConfirm,formProvider:t.formProvider,getClientCapabilities:()=>e.server.getClientCapabilities()})))},Fe=e=>({...e,confirmToken:T.string().optional().describe("Round-2 only: the token returned by the round-1 gate, proving the arguments are unchanged.")}),Me=e=>(t,r)=>e(t,r);async function oe(){S.enabled=!0;let e=new qe({name:"infra-kit",version:D.version},{capabilities:{resources:{listChanged:!0},tools:{},prompts:{}}});return await j(e),await $(e),await re(e),e}N();var l=A(),$e=async()=>{try{return await oe()}catch(e){l.error({err:e,msg:"Failed to create MCP server"}),l.flush(),f.exit(1)}};F(l);var Ge=je($e,{onerror:e=>{l.error({err:e,msg:"MCP stdio entry error"}),l.flush()}});l.info({msg:"MCP stdio entry started."});var ne=!1,ae=async e=>{if(!ne){ne=!0,l.info({msg:`Received ${e}. Shutting down...`});try{await Promise.race([Ge.close(),Le(1500)])}catch(t){l.error({err:t,msg:"MCP stdio close failed during shutdown"})}l.flush(),f.exit(0)}};f.on("SIGINT",()=>{ae("SIGINT")});f.on("SIGTERM",()=>{ae("SIGTERM")});
2
2
  //# sourceMappingURL=mcp.js.map
@@ -1,2 +1,2 @@
1
- import{d as i}from"./chunk-4ZE5XSVW.js";import{a as t}from"./chunk-5OHTHRS7.js";import"./chunk-7POMR4QK.js";import"./chunk-D4UYDBN5.js";import a from"node:fs";import o from"node:process";import{fileURLToPath as s}from"node:url";var m=r=>{let n=r.indexOf("--parent-pid");if(n===-1)return;let e=Number(r[n+1]);return Number.isInteger(e)&&e>0?e:void 0},p=()=>a.realpathSync(s(new URL("./cli.js",import.meta.url))),d=async()=>{await i(t.version,{selfRealPath:p(),parentPid:m(o.argv)})};d().catch(()=>{o.exitCode=0});
1
+ import{d as i}from"./chunk-Y3FV2BRI.js";import{a as t}from"./chunk-6GWRKIZ7.js";import"./chunk-XJOY7J3G.js";import"./chunk-D4UYDBN5.js";import a from"node:fs";import o from"node:process";import{fileURLToPath as s}from"node:url";var m=r=>{let n=r.indexOf("--parent-pid");if(n===-1)return;let e=Number(r[n+1]);return Number.isInteger(e)&&e>0?e:void 0},p=()=>a.realpathSync(s(new URL("./cli.js",import.meta.url))),d=async()=>{await i(t.version,{selfRealPath:p(),parentPid:m(o.argv)})};d().catch(()=>{o.exitCode=0});
2
2
  //# sourceMappingURL=update-check.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "infra-kit",
3
3
  "type": "module",
4
- "version": "0.5.0",
4
+ "version": "0.5.1",
5
5
  "files": [
6
6
  "dist"
7
7
  ],
@@ -43,8 +43,8 @@
43
43
  "esbuild": "^0.28.2",
44
44
  "ink-testing-library": "^4.0.0",
45
45
  "typescript": "^6.0.3",
46
- "@slip-stream-kit/eslint-plugin": "0.5.0",
47
46
  "@wl/eslint-config": "0.1.0",
47
+ "@slip-stream-kit/eslint-plugin": "0.5.0",
48
48
  "@wl/vitest-config": "0.1.0"
49
49
  },
50
50
  "scripts": {
package/readme.md CHANGED
@@ -110,7 +110,7 @@ When infra-kit runs as an MCP server (via `.mcp.json`), these commands are avail
110
110
 
111
111
  26 tools total.
112
112
 
113
- **Not exposed**: `dev` (long-running), `release deliver` (irreversible prod delivery), `config edit` / `config path` (interactive/CLI-only), `vendor config` (CLI-only), `self-update` and `mcp` (process-level).
113
+ **Not exposed**: `dev` (long-running), `release deliver` (irreversible prod delivery), `config edit` / `config path` (interactive/CLI-only), `vendor config` (CLI-only), and `mcp` (process-level).
114
114
 
115
115
  ---
116
116
 
@@ -1,7 +0,0 @@
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.0\",\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 a self-update advisory can name the one command\n * 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 * Shared by `self-update` (interactive) and the background update worker so the two can never disagree\n * 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,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/lib/pm-env/pm-env.ts"],
4
- "sourcesContent": ["/**\n * `env` minus every package-manager marker a spawned child could mistake for a throwaway install:\n * the whole `npm_*` block plus `PNPM_SCRIPT_SRC_DIR`. The result makes the child look exactly like\n * a direct `node <pkg>/dist/cli.js` run \u2014 which it is.\n */\n// Any child we launch through `node <bin>` (the portless driver, self-update, the mcp launcher)\n// inherits our env, and a tool that sniffs those markers will conclude it was run via `npx`/`pnpm\n// dlx` and refuse to work. portless is the motivating case; its guard (cli.js `main()`) is:\n//\n// isNpx = npm_command === 'exec' && !npm_lifecycle_event\n// isPnpmDlx = !!PNPM_SCRIPT_SRC_DIR && !npm_lifecycle_event\n// if ((isNpx || isPnpmDlx) && !isLocallyInstalled()) -> abort\n//\n// `isLocallyInstalled()` walks up from the CHILD's cwd, so in a consumer repo (where portless is a\n// transitive dep of infra-kit, absent from the root `node_modules`) it is false and the guard is\n// live. `pnpm exec infra-kit dev` exports `npm_command=exec` with no lifecycle event, so `isNpx`\n// holds, every portless call exits 1, `isAvailable()` is false, and `ensureProxy` aborts the whole\n// dev boot with \"portless is not installed\", on a machine where it plainly is.\n//\n// `PNPM_SCRIPT_SRC_DIR` must be dropped ALONGSIDE the `npm_*` block, not left behind: it is set by\n// `pnpm run <script>`, where `npm_lifecycle_event` is what suppresses `isPnpmDlx`. Stripping only\n// `npm_*` removes that suppressor and creates an abort for the script-style invocation that\n// previously worked. Both markers go, or neither is safe.\nexport const withoutPackageManagerEnv = (env: NodeJS.ProcessEnv): NodeJS.ProcessEnv => {\n return Object.fromEntries(\n Object.entries(env).filter(([key]) => {\n return !key.startsWith('npm_') && key !== 'PNPM_SCRIPT_SRC_DIR'\n }),\n )\n}\n\n/**\n * npm's own configuration \u2014 as opposed to the npx/dlx *markers* above. Case-insensitive because npm reads\n * `npm_config_*` case-insensitively (its parser matches `/^npm_config_/i`) and real environments export\n * both forms: shells write the lowercase name, Dockerfiles and CI images overwhelmingly write the\n * uppercase one.\n */\nconst NPM_CONFIG_TO_PRESERVE = /^npm_config_(?:prefix|registry)$/i\n\n/**\n * The env for spawning a package-manager INSTALL: npx/dlx markers stripped, but npm's own config\n * kept \u2014 specifically `npm_config_prefix` and `npm_config_registry`.\n *\n * @example\n * packageManagerInstallEnv({ npm_command: 'exec', npm_config_prefix: '/p', PATH: '/bin' })\n * // => { npm_config_prefix: '/p', PATH: '/bin' }\n */\n// `withoutPackageManagerEnv` is deliberately a blunt `npm_*` prefix filter, and it is the\n// right tool for the portless driver \u2014 but it is the WRONG tool for spawning an install, because it\n// also drops the two variables that tell the package manager where, and from where, to install:\n//\n// - `npm_config_prefix` is the npm matcher's ONLY signal (see `install-manager.ts`). Strip it and\n// we detect a global install rooted at prefix P, then run the install with P erased \u2014 so it\n// lands in the DEFAULT prefix instead. That is either a permanent EACCES, or a second global\n// copy that is not the one on PATH. Nothing notices: the install exits 0, we report\n// `installed`, the version on PATH never moves, and the whole cycle repeats every window\n// forever. `export npm_config_prefix=~/.npm-global` is npm's own documented EACCES workaround,\n// so this is a mainstream layout, not an exotic one.\n// - `npm_config_registry` is what the update CHECK already used to pick a registry. Dropping it\n// here means we decide against a corporate mirror and then install from somewhere else.\n//\n// Preserving these cannot re-arm the dlx guard: that guard reads `npm_command`,\n// `npm_lifecycle_event` and `PNPM_SCRIPT_SRC_DIR` \u2014 none of which are npm config, and all of which\n// remain stripped.\nexport const packageManagerInstallEnv = (env: NodeJS.ProcessEnv): NodeJS.ProcessEnv => {\n const sanitised = withoutPackageManagerEnv(env)\n\n for (const [key, value] of Object.entries(env)) {\n if (NPM_CONFIG_TO_PRESERVE.test(key)) sanitised[key] = value\n }\n\n return sanitised\n}\n"],
5
- "mappings": "AAuBO,IAAMA,EAA4BC,GAChC,OAAO,YACZ,OAAO,QAAQA,CAAG,EAAE,OAAO,CAAC,CAACC,CAAG,IACvB,CAACA,EAAI,WAAW,MAAM,GAAKA,IAAQ,qBAC3C,CACH,EASIC,EAAyB,oCA2BlBC,EAA4BH,GAA8C,CACrF,IAAMI,EAAYL,EAAyBC,CAAG,EAE9C,OAAW,CAACC,EAAKI,CAAK,IAAK,OAAO,QAAQL,CAAG,EACvCE,EAAuB,KAAKD,CAAG,IAAGG,EAAUH,CAAG,EAAII,GAGzD,OAAOD,CACT",
6
- "names": ["withoutPackageManagerEnv", "env", "key", "NPM_CONFIG_TO_PRESERVE", "packageManagerInstallEnv", "sanitised", "value"]
7
- }