infra-kit 0.3.15 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/boot-AJLBGRED.js +2 -0
  2. package/dist/boot-AJLBGRED.js.map +7 -0
  3. package/dist/chunk-4ZE5XSVW.js +2 -0
  4. package/dist/chunk-4ZE5XSVW.js.map +7 -0
  5. package/dist/chunk-5OHTHRS7.js +2 -0
  6. package/dist/chunk-5OHTHRS7.js.map +7 -0
  7. package/dist/{chunk-LKKKDB6V.js → chunk-7POMR4QK.js} +1 -1
  8. package/dist/chunk-7POMR4QK.js.map +7 -0
  9. package/dist/{chunk-CHETBZ6M.js → chunk-D4UYDBN5.js} +2 -2
  10. package/dist/{chunk-CHETBZ6M.js.map → chunk-D4UYDBN5.js.map} +2 -2
  11. package/dist/{chunk-MKMT6KQT.js → chunk-DHYZ6MBU.js} +2 -2
  12. package/dist/chunk-DHYZ6MBU.js.map +7 -0
  13. package/dist/chunk-HXBIE4JP.js +7 -0
  14. package/dist/chunk-HXBIE4JP.js.map +7 -0
  15. package/dist/chunk-LXBIHXUR.js +422 -0
  16. package/dist/chunk-LXBIHXUR.js.map +7 -0
  17. package/dist/chunk-Q4HKBLQG.js +7 -0
  18. package/dist/chunk-Q4HKBLQG.js.map +7 -0
  19. package/dist/{chunk-X46DP23W.js → chunk-QWVLAZ6N.js} +1 -1
  20. package/dist/chunk-QWVLAZ6N.js.map +7 -0
  21. package/dist/chunk-TVG47UHG.js +2 -0
  22. package/dist/chunk-TVG47UHG.js.map +7 -0
  23. package/dist/{chunk-TPVXMJ7N.js → chunk-WIZJQ57Q.js} +1 -1
  24. package/dist/chunk-WIZJQ57Q.js.map +7 -0
  25. package/dist/cli.js +12 -11
  26. package/dist/cli.js.map +4 -4
  27. package/dist/dev-server.js +3 -3
  28. package/dist/dev-server.js.map +2 -2
  29. package/dist/{dev-wizard-run-AU7SZJV7.js → dev-wizard-run-FNMMYTDX.js} +2 -2
  30. package/dist/{dev-wizard-run-AU7SZJV7.js.map → dev-wizard-run-FNMMYTDX.js.map} +2 -2
  31. package/dist/mcp.js +1 -1
  32. package/dist/mcp.js.map +4 -4
  33. package/dist/{persistent-ink-dev-ui-QNFTO2B2.js → persistent-ink-dev-ui-P65EJCC6.js} +2 -2
  34. package/dist/persistent-ink-dev-ui-P65EJCC6.js.map +7 -0
  35. package/dist/safe-stderr-ECYA6DRR.js +2 -0
  36. package/dist/update-check.js +1 -1
  37. package/dist/update-check.js.map +1 -1
  38. package/package.json +16 -15
  39. package/readme.md +45 -34
  40. package/dist/boot-OA6HGGO6.js +0 -2
  41. package/dist/boot-OA6HGGO6.js.map +0 -7
  42. package/dist/chunk-KATIE633.js +0 -247
  43. package/dist/chunk-KATIE633.js.map +0 -7
  44. package/dist/chunk-LKKKDB6V.js.map +0 -7
  45. package/dist/chunk-MKMT6KQT.js.map +0 -7
  46. package/dist/chunk-OKO3DKIM.js +0 -7
  47. package/dist/chunk-OKO3DKIM.js.map +0 -7
  48. package/dist/chunk-TPVXMJ7N.js.map +0 -7
  49. package/dist/chunk-VFU2USJG.js +0 -2
  50. package/dist/chunk-VFU2USJG.js.map +0 -7
  51. package/dist/chunk-X46DP23W.js.map +0 -7
  52. package/dist/chunk-XR6HR4LL.js +0 -6
  53. package/dist/chunk-XR6HR4LL.js.map +0 -7
  54. package/dist/chunk-YDW6324A.js +0 -2
  55. package/dist/chunk-YDW6324A.js.map +0 -7
  56. package/dist/persistent-ink-dev-ui-QNFTO2B2.js.map +0 -7
  57. package/dist/safe-stderr-HXDR2DNJ.js +0 -2
  58. /package/dist/{safe-stderr-HXDR2DNJ.js.map → safe-stderr-ECYA6DRR.js.map} +0 -0
@@ -1,2 +1,2 @@
1
- import{f as C,i as f,k as g}from"./chunk-OKO3DKIM.js";import{J as u,K as b,L as y,N as k,X as x,da as v}from"./chunk-XR6HR4LL.js";import{e as w}from"./chunk-CHETBZ6M.js";import"./chunk-X46DP23W.js";import O from"@inquirer/checkbox";import _ from"@inquirer/confirm";import j,{Separator as I}from"@inquirer/select";import{loadDev as K}from"@slip-stream-kit/config/internal";import U from"node:path";import m from"node:process";var W=e=>e.split("/")[0],A=e=>new Map(e.apps.map(r=>[r.name,r])),D=e=>e.localCapable&&e.cloudCapable,N=(e,r,t)=>D(r)?t[`${e} ${r.path}`]??(r.cloudCapable?"cloud":"local"):r.localCapable?"local":"cloud",$=(e,r,t)=>{let a=new Set,o=new Map;for(let n of e){let s=r.get(W(n));for(let c of s?.backends??[]){o.set(c.packageName,c.ownerApp);for(let l of c.routes)N(n,l,t)==="local"&&a.add(c.packageName)}}return{localPkgs:a,ownerByPkg:o}},B=(e,r)=>{let t={},a=!1;for(let o of e?.backends??[]){let n=r.has(o.packageName);for(let s of o.routes){let c=s.localCapable&&n?"local":"cloud";t[s.path]=c,c==="cloud"&&(a=!0)}}return{overrides:t,anyCloud:a}},z=(e,r)=>{let t=A(r),a=e.targets.filter(p=>p.endsWith("/ui")),o=e.targets.filter(p=>p.endsWith("/api")),{localPkgs:n,ownerByPkg:s}=$(a,t,e.sources),c={},l=!1;for(let p of a){let{overrides:d,anyCloud:i}=B(t.get(W(p)),n);i&&(l=!0),c[p]=Object.keys(d).length>0?{proxy:d}:{}}for(let p of o)c[p]??={};for(let p of n){let d=s.get(p);d!=null&&(c[`${d}/api`]??={})}return{presetDef:{apps:c,cmux:e.cmux},anyCloudRoute:l,targetKeys:Object.keys(c).sort()}},M=(e,r)=>{let t=[`--target=${e.targetKeys.join(",")}`];return r.watch&&t.push("--watch"),r.cmux&&t.push("--cmux"),{flags:t.join(" ")}};var h={output:m.stderr,clearPromptOnDone:!0},q={select:e=>g(r=>j({message:e.message,choices:e.choices,default:e.default},r),h),checkbox:e=>g(r=>O({message:e.message,choices:e.choices},r),h),confirm:e=>g(r=>_({message:e.message,default:e.default},r),h)},E=(e,r)=>{let t=new Map;for(let[a,o]of Object.entries(e)){let n={path:a,localCapable:o.from.includes("local"),cloudCapable:o.from.includes("cloud"),default:o.default},s=t.get(o.packageName);s?(s.routes.push(n),s.localCapable=s.localCapable||n.localCapable,s.cloudCapable=s.cloudCapable||n.cloudCapable):t.set(o.packageName,{packageName:o.packageName,routes:[n],localCapable:n.localCapable,cloudCapable:n.cloudCapable,ownerApp:r.get(o.packageName)})}return[...t.values()]},T=async(e,r,t)=>{try{let a=(await K(e))?.proxy?.routes;return a?E(a,t):[]}catch(a){return u.warn(`\u26A0\uFE0F Skipping ${r}: its dev config failed to load \u2014 ${a instanceof Error?a.message:String(a)}`),[]}},V=async e=>{let r=y(e),t=k(e),a=await v(),o=new Map(r.map(i=>[i.packageName,i.name])),n=new Map(r.map(i=>[i.name,i.packageName])),s=new Set(t.map(i=>i.name)),c=new Set(r.map(i=>i.name)),l=[...new Set([...c,...s])].sort(),p=await Promise.all(l.map(async i=>{let P=s.has(i);return{name:i,hasApi:c.has(i),hasUi:P,apiPackage:n.get(i),backends:P?await T(U.join(e,"apps",i,"ui"),i,o):[]}})),d=await C();return{apps:p,presets:Object.keys(a.devServersPresets??{}),environments:Object.keys(d?.envs??{}).sort()}},F=e=>{let r=[];for(let t of e.apps){if(t.hasUi){let a=t.backends.flatMap(n=>n.routes.map(s=>s.path)),o=a.length>0?`frontend \u2014 proxies ${[...a].sort().join(", ")}`:"frontend";r.push({name:`${t.name}/ui`,value:`${t.name}/ui`,description:o})}t.hasApi&&!t.hasUi&&r.push({name:`${t.name}/api`,value:`${t.name}/api`,description:"backend"})}return r},L=e=>{let r={api:e.apps.filter(o=>o.hasApi).map(o=>o.name),ui:e.apps.filter(o=>o.hasUi).map(o=>o.name)},t={},a=new Map;for(let o of e.apps){o.apiPackage!=null&&(t[o.name]=o.apiPackage);for(let n of o.backends)for(let s of n.routes)a.set(`${o.name} ${s.path}`,n.packageName)}return{discovered:r,apiPkgByApp:t,routePkg:(o,n)=>a.get(`${o} ${n}`)}},X=(e,r)=>x({__wizard__:e},L(r)).map(t=>t.message),Z="INFRA_KIT_DEV_WIZARD_CMUX",H=()=>m.env[Z]==="1",G=async(e,r,t)=>{let a=new Map(r.apps.map(n=>[n.name,n])),o={};for(let n of t){let s=a.get(n.split("/")[0]);for(let c of s?.backends??[])for(let l of c.routes){if(!l.localCapable||!l.cloudCapable)continue;let p=await e.select({message:`\u{1F500} ${n} ${l.path} \u2192 local or cloud?`,choices:[{name:"local (this machine)",value:"local"},{name:"cloud",value:"cloud"}],default:l.default??"local"});o[`${n} ${l.path}`]=p}}return o},J=(e,r,t)=>e.apps.find(a=>a.name===r)?.backends.flatMap(a=>a.routes).find(a=>a.path===t),Q=(e,r)=>{let t=[];for(let[a,o]of Object.entries(e.presetDef.apps??{})){if(!a.endsWith("/ui")||!o.proxy)continue;let n=a.split("/")[0];for(let[s,c]of Object.entries(o.proxy)){let l=J(r,n,s),p=l&&l.localCapable&&l.cloudCapable?"":" (only option)";t.push(` ${a} ${s} \u2192 ${c}${p}`)}}if(t.length!==0){u.info("Resolved routes:");for(let a of[...t].sort())u.info(a)}},R=async(e,r)=>{if(r.apps.length===0)return u.warn("No apps discovered to run."),null;let t=await e.checkbox({message:"\u{1F4E6} Which packages?",choices:F(r)});if(t.length===0)return u.warn("No packages selected."),null;let a=t.filter(i=>i.endsWith("/ui")),o=await G(e,r,a),n=await e.confirm({message:"\u{1F440} Rebuild & restart on save (watch)?",default:!1}),s=H()?await e.confirm({message:"\u{1F9E9} Run each app in its own cmux pane?",default:!1}):!1,c={targets:t,sources:o,watch:n,cmux:s},l=z(c,r);if(l.anyCloudRoute){if(r.environments.length===0)return u.warn("\u2601\uFE0F This selection routes to a cloud environment, but no environment token is loaded."),u.warn(" Run `infra-kit env-load` to add one, then re-run the wizard."),null;c.env=await e.select({message:"\u2601\uFE0F Point cloud routes at which environment?",choices:r.environments.map(i=>({name:i,value:i}))}),m.env[w]=c.env}let p=X(l.presetDef,r);if(p.length>0){u.warn("\u26A0\uFE0F Proxy audit found issues with this selection:");for(let i of p)u.warn(` \u2022 ${i}`);return null}Q(l,r),Y(l,c);let d=l.targetKeys.filter(i=>i.endsWith("/api")).map(i=>i.split("/")[0]);return s&&d.length>0?{include:d,presetDef:l.presetDef,watch:n,cmux:!0}:(s&&u.info("\u2139\uFE0F cmux needs at least one local backend (panes are backend-only) \u2014 running in-process instead."),{presetDef:l.presetDef,watch:n,cmux:!1})},Y=(e,r)=>{f.setInteractive();let t=M(e,r);f.addOption(t.flags,!0),f.print(),u.info("\u2139\uFE0F Prefer a short name? Save this selection as a devPreset and run `infra-kit dev <name>`.")},ee=async(e,r)=>{let t=await e.confirm({message:"\u{1F440} Rebuild & restart on save (watch)?",default:!1});return f.setInteractive(),f.addOption(r,!0),t&&f.addOption("--watch",!0),f.print(),{preset:r,watch:t,cmux:!1}},S=" manual",re=async(e,r)=>{if(r.presets.length===0)return R(e,r);let t=await e.select({message:"\u{1F680} Start from a preset, or configure manually?",choices:[...r.presets.map(a=>({name:a,value:a})),new I(" "),{name:"Manual (custom)\u2026",value:S}]});return t===S?R(e,r):ee(e,t)},be=async(e=q,r=b(m.cwd()))=>re(e,await V(r));export{S as MANUAL_CHOICE,Z as WIZARD_CMUX_VAR,X as auditManualPlan,q as defaultPrompts,V as gatherWizardModel,T as loadBackends,be as runDevWizard,re as runWizardFlow};
2
- //# sourceMappingURL=dev-wizard-run-AU7SZJV7.js.map
1
+ import{f as C,i as f,k as g}from"./chunk-HXBIE4JP.js";import{L as u,M as b,N as y,P as k,Z as x,fa as v}from"./chunk-Q4HKBLQG.js";import{e as w}from"./chunk-D4UYDBN5.js";import"./chunk-QWVLAZ6N.js";import O from"@inquirer/checkbox";import _ from"@inquirer/confirm";import j,{Separator as I}from"@inquirer/select";import{loadDev as K}from"@slip-stream-kit/config/internal";import U from"node:path";import m from"node:process";var W=e=>e.split("/")[0],A=e=>new Map(e.apps.map(r=>[r.name,r])),D=e=>e.localCapable&&e.cloudCapable,N=(e,r,t)=>D(r)?t[`${e} ${r.path}`]??(r.cloudCapable?"cloud":"local"):r.localCapable?"local":"cloud",$=(e,r,t)=>{let a=new Set,o=new Map;for(let n of e){let s=r.get(W(n));for(let c of s?.backends??[]){o.set(c.packageName,c.ownerApp);for(let l of c.routes)N(n,l,t)==="local"&&a.add(c.packageName)}}return{localPkgs:a,ownerByPkg:o}},B=(e,r)=>{let t={},a=!1;for(let o of e?.backends??[]){let n=r.has(o.packageName);for(let s of o.routes){let c=s.localCapable&&n?"local":"cloud";t[s.path]=c,c==="cloud"&&(a=!0)}}return{overrides:t,anyCloud:a}},z=(e,r)=>{let t=A(r),a=e.targets.filter(p=>p.endsWith("/ui")),o=e.targets.filter(p=>p.endsWith("/api")),{localPkgs:n,ownerByPkg:s}=$(a,t,e.sources),c={},l=!1;for(let p of a){let{overrides:d,anyCloud:i}=B(t.get(W(p)),n);i&&(l=!0),c[p]=Object.keys(d).length>0?{proxy:d}:{}}for(let p of o)c[p]??={};for(let p of n){let d=s.get(p);d!=null&&(c[`${d}/api`]??={})}return{presetDef:{apps:c,cmux:e.cmux},anyCloudRoute:l,targetKeys:Object.keys(c).sort()}},M=(e,r)=>{let t=[`--target=${e.targetKeys.join(",")}`];return r.watch&&t.push("--watch"),r.cmux&&t.push("--cmux"),{flags:t.join(" ")}};var h={output:m.stderr,clearPromptOnDone:!0},q={select:e=>g(r=>j({message:e.message,choices:e.choices,default:e.default},r),h),checkbox:e=>g(r=>O({message:e.message,choices:e.choices},r),h),confirm:e=>g(r=>_({message:e.message,default:e.default},r),h)},E=(e,r)=>{let t=new Map;for(let[a,o]of Object.entries(e)){let n={path:a,localCapable:o.from.includes("local"),cloudCapable:o.from.includes("cloud"),default:o.default},s=t.get(o.packageName);s?(s.routes.push(n),s.localCapable=s.localCapable||n.localCapable,s.cloudCapable=s.cloudCapable||n.cloudCapable):t.set(o.packageName,{packageName:o.packageName,routes:[n],localCapable:n.localCapable,cloudCapable:n.cloudCapable,ownerApp:r.get(o.packageName)})}return[...t.values()]},T=async(e,r,t)=>{try{let a=(await K(e))?.proxy?.routes;return a?E(a,t):[]}catch(a){return u.warn(`\u26A0\uFE0F Skipping ${r}: its dev config failed to load \u2014 ${a instanceof Error?a.message:String(a)}`),[]}},V=async e=>{let r=y(e),t=k(e),a=await v(),o=new Map(r.map(i=>[i.packageName,i.name])),n=new Map(r.map(i=>[i.name,i.packageName])),s=new Set(t.map(i=>i.name)),c=new Set(r.map(i=>i.name)),l=[...new Set([...c,...s])].sort(),p=await Promise.all(l.map(async i=>{let P=s.has(i);return{name:i,hasApi:c.has(i),hasUi:P,apiPackage:n.get(i),backends:P?await T(U.join(e,"apps",i,"ui"),i,o):[]}})),d=await C();return{apps:p,presets:Object.keys(a.devServersPresets??{}),environments:Object.keys(d?.envs??{}).sort()}},F=e=>{let r=[];for(let t of e.apps){if(t.hasUi){let a=t.backends.flatMap(n=>n.routes.map(s=>s.path)),o=a.length>0?`frontend \u2014 proxies ${[...a].sort().join(", ")}`:"frontend";r.push({name:`${t.name}/ui`,value:`${t.name}/ui`,description:o})}t.hasApi&&!t.hasUi&&r.push({name:`${t.name}/api`,value:`${t.name}/api`,description:"backend"})}return r},L=e=>{let r={api:e.apps.filter(o=>o.hasApi).map(o=>o.name),ui:e.apps.filter(o=>o.hasUi).map(o=>o.name)},t={},a=new Map;for(let o of e.apps){o.apiPackage!=null&&(t[o.name]=o.apiPackage);for(let n of o.backends)for(let s of n.routes)a.set(`${o.name} ${s.path}`,n.packageName)}return{discovered:r,apiPkgByApp:t,routePkg:(o,n)=>a.get(`${o} ${n}`)}},X=(e,r)=>x({__wizard__:e},L(r)).map(t=>t.message),Z="INFRA_KIT_DEV_WIZARD_CMUX",H=()=>m.env[Z]==="1",G=async(e,r,t)=>{let a=new Map(r.apps.map(n=>[n.name,n])),o={};for(let n of t){let s=a.get(n.split("/")[0]);for(let c of s?.backends??[])for(let l of c.routes){if(!l.localCapable||!l.cloudCapable)continue;let p=await e.select({message:`\u{1F500} ${n} ${l.path} \u2192 local or cloud?`,choices:[{name:"local (this machine)",value:"local"},{name:"cloud",value:"cloud"}],default:l.default??"local"});o[`${n} ${l.path}`]=p}}return o},J=(e,r,t)=>e.apps.find(a=>a.name===r)?.backends.flatMap(a=>a.routes).find(a=>a.path===t),Q=(e,r)=>{let t=[];for(let[a,o]of Object.entries(e.presetDef.apps??{})){if(!a.endsWith("/ui")||!o.proxy)continue;let n=a.split("/")[0];for(let[s,c]of Object.entries(o.proxy)){let l=J(r,n,s),p=l&&l.localCapable&&l.cloudCapable?"":" (only option)";t.push(` ${a} ${s} \u2192 ${c}${p}`)}}if(t.length!==0){u.info("Resolved routes:");for(let a of[...t].sort())u.info(a)}},R=async(e,r)=>{if(r.apps.length===0)return u.warn("No apps discovered to run."),null;let t=await e.checkbox({message:"\u{1F4E6} Which packages?",choices:F(r)});if(t.length===0)return u.warn("No packages selected."),null;let a=t.filter(i=>i.endsWith("/ui")),o=await G(e,r,a),n=await e.confirm({message:"\u{1F440} Rebuild & restart on save (watch)?",default:!1}),s=H()?await e.confirm({message:"\u{1F9E9} Run each app in its own cmux pane?",default:!1}):!1,c={targets:t,sources:o,watch:n,cmux:s},l=z(c,r);if(l.anyCloudRoute){if(r.environments.length===0)return u.warn("\u2601\uFE0F This selection routes to a cloud environment, but no environment token is loaded."),u.warn(" Run `infra-kit env-load` to add one, then re-run the wizard."),null;c.env=await e.select({message:"\u2601\uFE0F Point cloud routes at which environment?",choices:r.environments.map(i=>({name:i,value:i}))}),m.env[w]=c.env}let p=X(l.presetDef,r);if(p.length>0){u.warn("\u26A0\uFE0F Proxy audit found issues with this selection:");for(let i of p)u.warn(` \u2022 ${i}`);return null}Q(l,r),Y(l,c);let d=l.targetKeys.filter(i=>i.endsWith("/api")).map(i=>i.split("/")[0]);return s&&d.length>0?{include:d,presetDef:l.presetDef,watch:n,cmux:!0}:(s&&u.info("\u2139\uFE0F cmux needs at least one local backend (panes are backend-only) \u2014 running in-process instead."),{presetDef:l.presetDef,watch:n,cmux:!1})},Y=(e,r)=>{f.setInteractive();let t=M(e,r);f.addOption(t.flags,!0),f.print(),u.info("\u2139\uFE0F Prefer a short name? Save this selection as a devPreset and run `infra-kit dev <name>`.")},ee=async(e,r)=>{let t=await e.confirm({message:"\u{1F440} Rebuild & restart on save (watch)?",default:!1});return f.setInteractive(),f.addOption(r,!0),t&&f.addOption("--watch",!0),f.print(),{preset:r,watch:t,cmux:!1}},S=" manual",re=async(e,r)=>{if(r.presets.length===0)return R(e,r);let t=await e.select({message:"\u{1F680} Start from a preset, or configure manually?",choices:[...r.presets.map(a=>({name:a,value:a})),new I(" "),{name:"Manual (custom)\u2026",value:S}]});return t===S?R(e,r):ee(e,t)},be=async(e=q,r=b(m.cwd()))=>re(e,await V(r));export{S as MANUAL_CHOICE,Z as WIZARD_CMUX_VAR,X as auditManualPlan,q as defaultPrompts,V as gatherWizardModel,T as loadBackends,be as runDevWizard,re as runWizardFlow};
2
+ //# sourceMappingURL=dev-wizard-run-FNMMYTDX.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/dev/dev-wizard-run.ts", "../src/dev/dev-wizard.ts"],
4
- "sourcesContent": ["/**\n * Impure driver for the interactive `infra-kit dev` wizard: filesystem/config discovery, the inquirer\n * prompt flow (step-0 preset-or-manual, then the manual matrix), the pre-flight proxy audit, and the\n * `commandEcho` teaching line. The pure answer\u2192plan mapping lives in {@link file://./dev-wizard.ts}.\n *\n * Prompts are behind the injectable {@link WizardPrompts} seam so the flow is testable without a TTY.\n * This module is reached ONLY from the bare-invocation TTY branch of the entry point; every flagged /\n * non-TTY / `--json` / MCP invocation bypasses it entirely.\n */\nimport inquirerCheckbox from '@inquirer/checkbox'\nimport inquirerConfirm from '@inquirer/confirm'\nimport inquirerSelect, { Separator } from '@inquirer/select'\nimport { loadDev } from '@slip-stream-kit/config/internal'\nimport path from 'node:path'\nimport process from 'node:process'\n\nimport { discoverApiApps, discoverUiApps, findMonorepoRoot } from 'src/dev/discovery'\nimport { commandEcho } from 'src/lib/command-echo'\nimport { INFRA_KIT_ENV_VAR } from 'src/lib/constants'\nimport { readTokenStore } from 'src/lib/env-tokens'\nimport { getInfraKitConfig } from 'src/lib/infra-kit-config'\nimport type { DevPreset, ProxySource } from 'src/lib/infra-kit-config'\nimport { logger } from 'src/lib/logger'\nimport { withEscape } from 'src/lib/prompts/escapable-context'\n\nimport { deriveManualPlan, equivalentCommand } from './dev-wizard.js'\nimport type { DerivedPlan, ManualSelection, ProxyBackend, RouteCap, WizardApp, WizardModel } from './dev-wizard.js'\nimport { validatePresetProxy } from './presets.js'\nimport type { DiscoveredParts, PresetProxyContext } from './presets.js'\n\n/** A wizard prompt choice \u2014 all values are strings, so the seam needs no generics. */\nexport interface WizardChoice {\n name: string\n value: string\n description?: string\n checked?: boolean\n disabled?: boolean | string\n}\n\n/**\n * Injectable prompt seam (a string-valued subset of `@inquirer/*`). The default implementation\n * ({@link defaultPrompts}) delegates to the real prompts, rendering to stderr so the dev-server's\n * stdout stays clean. Tests pass a scripted object.\n */\nexport interface WizardPrompts {\n select: (cfg: { message: string; choices: (WizardChoice | Separator)[]; default?: string }) => Promise<string>\n checkbox: (cfg: { message: string; choices: WizardChoice[] }) => Promise<string[]>\n confirm: (cfg: { message: string; default?: boolean }) => Promise<boolean>\n}\n\n/**\n * Shared `@inquirer/*` context. Rendered to stderr (mirrors the bare-`infra-kit` command palette).\n *\n * `clearPromptOnDone` erases each answered prompt instead of leaving a `\u2714 <question> <answer>` line\n * behind, so the wizard collapses to nothing once it finishes and the dev-server's ready header lands\n * at the top of a clean screen. The choices are not lost: {@link echoManual} / {@link runPresetBranch}\n * print the equivalent flag command, which is the durable record of what was picked.\n */\nconst promptContext = { output: process.stderr, clearPromptOnDone: true }\n\n/**\n * Real `@inquirer/*` prompts, rendered to stderr and erased once answered.\n *\n * Every one goes through {@link withEscape}, which binds Esc to cancellation. The wrap is here rather\n * than around the {@link WizardPrompts} seam so the scripted prompts the tests inject stay untouched:\n * the seam is the test boundary, `defaultPrompts` is the only implementation that talks to a terminal.\n *\n * Esc rejects with an `AbortPromptError`, which `entry/cli.ts` catches as a cancellation \u2192 exit 0. The\n * wizard therefore needs no cancel branch of its own: a cancelled prompt never returns here at all, so\n * no server is started.\n */\nexport const defaultPrompts: WizardPrompts = {\n select: (cfg) => {\n return withEscape((context) => {\n return inquirerSelect({ message: cfg.message, choices: cfg.choices, default: cfg.default }, context)\n }, promptContext)\n },\n checkbox: (cfg) => {\n return withEscape((context) => {\n return inquirerCheckbox({ message: cfg.message, choices: cfg.choices }, context)\n }, promptContext)\n },\n confirm: (cfg) => {\n return withEscape((context) => {\n return inquirerConfirm({ message: cfg.message, default: cfg.default }, context)\n }, promptContext)\n },\n}\n\n/** What the wizard hands back to the entry point (merged into `DevServerOptions`), or null on cancel. */\nexport interface WizardResult {\n /** Named preset (preset branch) \u2014 passed through as `options.preset`. */\n preset?: string\n /** In-memory preset (manual branch, non-cmux) \u2014 passed through as `options.presetDef`. */\n presetDef?: DevPreset\n /**\n * App-name include list for the cmux path only: which discovered API apps get a pane. Paired with\n * `presetDef`, which tells each pane the exact parts to run. An app selected UI-only gets no pane\n * (cmux panes are opened per API app). Unset for the non-cmux manual path.\n */\n include?: string[]\n watch: boolean\n cmux: boolean\n}\n\n/** Group one frontend's `dev.proxy.routes` into {@link ProxyBackend}s keyed by backend package. */\nconst groupBackends = (\n routes: Record<string, { packageName: string; from: readonly string[]; default?: 'local' | 'cloud' }>,\n ownerByPkg: Map<string, string>,\n): ProxyBackend[] => {\n const byPkg = new Map<string, ProxyBackend>()\n\n for (const [route, def] of Object.entries(routes)) {\n const cap: RouteCap = {\n path: route,\n localCapable: def.from.includes('local'),\n cloudCapable: def.from.includes('cloud'),\n default: def.default,\n }\n const existing = byPkg.get(def.packageName)\n\n if (existing) {\n existing.routes.push(cap)\n existing.localCapable = existing.localCapable || cap.localCapable\n existing.cloudCapable = existing.cloudCapable || cap.cloudCapable\n } else {\n byPkg.set(def.packageName, {\n packageName: def.packageName,\n routes: [cap],\n localCapable: cap.localCapable,\n cloudCapable: cap.cloudCapable,\n ownerApp: ownerByPkg.get(def.packageName),\n })\n }\n }\n\n return [...byPkg.values()]\n}\n\n/**\n * Load one frontend's proxy routes and group them into backends. A config that throws \u2014 schema-invalid\n * or an import-time error \u2014 degrades THAT app to zero backends with a named warning instead of rejecting.\n * Without this, a single broken `apps/<x>/ui/infra-kit.config.ts` fails the whole `Promise.all` in\n * {@link gatherWizardModel} and takes the entire wizard down, so `infra-kit dev` becomes unusable for every\n * other app in the monorepo. Mirrors the audit path's per-app guard ({@link file://../commands/audit/preset-proxy-check.ts}).\n */\nexport const loadBackends = async (\n uiDir: string,\n name: string,\n ownerByPkg: Map<string, string>,\n): Promise<ProxyBackend[]> => {\n try {\n const routes = (await loadDev(uiDir))?.proxy?.routes\n\n return routes ? groupBackends(routes, ownerByPkg) : []\n } catch (error) {\n logger.warn(\n `\u26A0\uFE0F Skipping ${name}: its dev config failed to load \u2014 ${error instanceof Error ? error.message : String(error)}`,\n )\n\n return []\n }\n}\n\n/** Discover apps + configs and assemble the {@link WizardModel} (backends resolved per frontend). */\nexport const gatherWizardModel = async (root: string): Promise<WizardModel> => {\n const apiApps = discoverApiApps(root)\n const uiApps = discoverUiApps(root)\n const config = await getInfraKitConfig()\n\n const ownerByPkg = new Map(\n apiApps.map((a) => {\n return [a.packageName, a.name] as const\n }),\n )\n const apiPkgByName = new Map(\n apiApps.map((a) => {\n return [a.name, a.packageName] as const\n }),\n )\n const uiNames = new Set(\n uiApps.map((a) => {\n return a.name\n }),\n )\n const apiNames = new Set(\n apiApps.map((a) => {\n return a.name\n }),\n )\n const allNames = [...new Set([...apiNames, ...uiNames])].sort()\n\n const apps: WizardApp[] = await Promise.all(\n allNames.map(async (name): Promise<WizardApp> => {\n const hasUi = uiNames.has(name)\n\n return {\n name,\n hasApi: apiNames.has(name),\n hasUi,\n apiPackage: apiPkgByName.get(name),\n backends: hasUi ? await loadBackends(path.join(root, 'apps', name, 'ui'), name, ownerByPkg) : [],\n }\n }),\n )\n\n // The cloud-env choices are the envs we hold a token for \u2014 the same authority as `env-load`, because\n // it is the same question. This picker writes INFRA_KIT_ENV, which `@slip-stream-kit/config/vite`\n // reads to build the cloud backend's URL, and whose only other writer is `env-load` (which cannot run\n // without a Doppler token). Sourcing it from the workflow options instead would put `prod` in the list\n // for everyone \u2014 including a developer holding no prod credential \u2014 and point a local UI at\n // production. No token, no entry.\n const store = await readTokenStore()\n\n return {\n apps,\n presets: Object.keys(config.devServersPresets ?? {}),\n environments: Object.keys(store?.envs ?? {}).sort(),\n }\n}\n\n/**\n * Build the checkbox choices: one `<app>/ui` per frontend, plus `<app>/api` ONLY for api-only apps\n * (an api with no ui). A full-stack `<app>/api` is deliberately NOT offered \u2014 a full-stack backend is\n * launched by choosing `local` for one of its frontend's routes (asked per-route afterwards), so the\n * checkbox stays a pick of frontends + standalone backends.\n *\n * Nothing starts checked: the manual branch is opt-in, so a run only ever launches what was explicitly\n * ticked. Pre-checking every part would make the fast path (accept the defaults) boot the whole monorepo\n * \u2014 the exact opposite of why someone reached for \"Manual\" over a preset.\n */\nconst buildPartChoices = (model: WizardModel): WizardChoice[] => {\n const choices: WizardChoice[] = []\n\n for (const app of model.apps) {\n if (app.hasUi) {\n const paths = app.backends.flatMap((b) => {\n return b.routes.map((r) => {\n return r.path\n })\n })\n const description = paths.length > 0 ? `frontend \u2014 proxies ${[...paths].sort().join(', ')}` : 'frontend'\n\n choices.push({ name: `${app.name}/ui`, value: `${app.name}/ui`, description })\n }\n if (app.hasApi && !app.hasUi) {\n choices.push({ name: `${app.name}/api`, value: `${app.name}/api`, description: 'backend' })\n }\n }\n\n return choices\n}\n\n/** Build the audit context from the gathered model (no re-reading of configs). */\nconst buildAuditContext = (model: WizardModel): PresetProxyContext => {\n const discovered: DiscoveredParts = {\n api: model.apps\n .filter((a) => {\n return a.hasApi\n })\n .map((a) => {\n return a.name\n }),\n ui: model.apps\n .filter((a) => {\n return a.hasUi\n })\n .map((a) => {\n return a.name\n }),\n }\n const apiPkgByApp: Record<string, string> = {}\n const routeToPkg = new Map<string, string>()\n\n for (const app of model.apps) {\n // Map EVERY discovered api app to its own api package \u2014 including api-only apps a frontend proxies\n // to cross-app \u2014 so `launchedPkgs` in validatePresetProxy is faithful (mirrors preset-proxy-check.ts).\n // Deriving this from proxy backends alone would miss api-only owners and false-positive the audit.\n if (app.apiPackage != null) apiPkgByApp[app.name] = app.apiPackage\n for (const b of app.backends) {\n for (const route of b.routes) {\n routeToPkg.set(`${app.name} ${route.path}`, b.packageName)\n }\n }\n }\n\n return {\n discovered,\n apiPkgByApp,\n routePkg: (app, route) => {\n return routeToPkg.get(`${app} ${route}`)\n },\n }\n}\n\n/**\n * Run the assembled plan through the SAME proxy-locality rule the root audit uses. Returns issue\n * messages (empty when clean). Catches a `local` override whose backend won't launch \u2014 the wizard's\n * derivation launches owners, so a hit means the backend has no discoverable owning app.\n */\nexport const auditManualPlan = (presetDef: DevPreset, model: WizardModel): string[] => {\n return validatePresetProxy({ __wizard__: presetDef }, buildAuditContext(model)).map((i) => {\n return i.message\n })\n}\n\n/**\n * Opt back into the cmux question: set to exactly `'1'`. The prompt is temporarily off \u2014 the manual\n * branch always runs in-process \u2014 but the derivation below is kept whole for when it returns. `--cmux`\n * on the command line is a separate path and stays live either way.\n */\nexport const WIZARD_CMUX_VAR = 'INFRA_KIT_DEV_WIZARD_CMUX'\n\nconst asksCmux = (): boolean => {\n return process.env[WIZARD_CMUX_VAR] === '1'\n}\n\n/**\n * Ask, per selected frontend, an EXPLICIT local/cloud choice for every TWO-OPTION route (its `from`\n * lists both). Single-option routes are auto-picked in the pure mapping, so they are never prompted.\n * The answers are keyed `${app}/ui ${routePath}` to match {@link ManualSelection.sources}; the select's\n * default seeds from the route's declared `default` when it has one.\n */\nconst promptRouteSources = async (\n prompts: WizardPrompts,\n model: WizardModel,\n uiKeys: string[],\n): Promise<Record<string, ProxySource>> => {\n const apps = new Map(\n model.apps.map((a) => {\n return [a.name, a] as const\n }),\n )\n const sources: Record<string, ProxySource> = {}\n\n for (const key of uiKeys) {\n const app = apps.get(key.split('/')[0]!)\n\n for (const backend of app?.backends ?? []) {\n for (const route of backend.routes) {\n if (!route.localCapable || !route.cloudCapable) continue\n\n const answer = await prompts.select({\n message: `\uD83D\uDD00 ${key} ${route.path} \u2192 local or cloud?`,\n choices: [\n { name: 'local (this machine)', value: 'local' },\n { name: 'cloud', value: 'cloud' },\n ],\n default: route.default ?? 'local',\n })\n\n sources[`${key} ${route.path}`] = answer as ProxySource\n }\n }\n }\n\n return sources\n}\n\n/** Look up a frontend route's capability in the model (for the recap's \"only option\" annotation). */\nconst findRouteCap = (model: WizardModel, app: string, routePath: string): RouteCap | undefined => {\n return model.apps\n .find((a) => {\n return a.name === app\n })\n ?.backends.flatMap((b) => {\n return b.routes\n })\n .find((r) => {\n return r.path === routePath\n })\n}\n\n/**\n * Print a one-line-per-route recap of what each frontend route resolved to, so the chosen sources are\n * visible before launch. A single-option route is annotated `(only option)` \u2014 it was auto-picked, not\n * asked. This is in addition to {@link echoManual}'s pasteable `--target` command.\n */\nconst recapSources = (plan: DerivedPlan, model: WizardModel): void => {\n const lines: string[] = []\n\n for (const [key, cfg] of Object.entries(plan.presetDef.apps ?? {})) {\n if (!key.endsWith('/ui') || !cfg.proxy) continue\n\n const app = key.split('/')[0]!\n\n for (const [routePath, source] of Object.entries(cfg.proxy)) {\n const cap = findRouteCap(model, app, routePath)\n const suffix = cap && cap.localCapable && cap.cloudCapable ? '' : ' (only option)'\n\n lines.push(` ${key} ${routePath} \u2192 ${source}${suffix}`)\n }\n }\n\n if (lines.length === 0) return\n logger.info('Resolved routes:')\n for (const line of [...lines].sort()) {\n logger.info(line)\n }\n}\n\n/** The manual-branch flow: frontends + per-route source + env + watch + cmux \u2192 audited plan \u2192 recap \u2192 echo. */\nconst runManualBranch = async (prompts: WizardPrompts, model: WizardModel): Promise<WizardResult | null> => {\n if (model.apps.length === 0) {\n logger.warn('No apps discovered to run.')\n\n return null\n }\n\n const selectedTargets = await prompts.checkbox({ message: '\uD83D\uDCE6 Which packages?', choices: buildPartChoices(model) })\n\n if (selectedTargets.length === 0) {\n logger.warn('No packages selected.')\n\n return null\n }\n\n const uiKeys = selectedTargets.filter((t) => {\n return t.endsWith('/ui')\n })\n const sources = await promptRouteSources(prompts, model, uiKeys)\n\n const watch = await prompts.confirm({ message: '\uD83D\uDC40 Rebuild & restart on save (watch)?', default: false })\n const cmux = asksCmux()\n ? await prompts.confirm({ message: '\uD83E\uDDE9 Run each app in its own cmux pane?', default: false })\n : false\n\n const selection: ManualSelection = { targets: selectedTargets, sources, watch, cmux }\n const plan = deriveManualPlan(selection, model)\n\n if (plan.anyCloudRoute) {\n // A cloud route needs an environment to point at, and the only environments we can offer are those we\n // hold a token for. With none loaded, the `select` below would be handed zero choices and `@inquirer/select`\n // throws `No selectable choices` \u2014 a raw stack on a realistic first-run path (fresh clone, no `env-load`).\n // Back out cleanly with guidance instead, and BEFORE mutating `process.env`, so a refused run leaks nothing.\n if (model.environments.length === 0) {\n logger.warn('\u2601\uFE0F This selection routes to a cloud environment, but no environment token is loaded.')\n logger.warn(' Run `infra-kit env-load` to add one, then re-run the wizard.')\n\n return null\n }\n\n selection.env = await prompts.select({\n message: '\u2601\uFE0F Point cloud routes at which environment?',\n choices: model.environments.map((e) => {\n return { name: e, value: e }\n }),\n })\n process.env[INFRA_KIT_ENV_VAR] = selection.env\n }\n\n const issues = auditManualPlan(plan.presetDef, model)\n\n if (issues.length > 0) {\n logger.warn('\u26A0\uFE0F Proxy audit found issues with this selection:')\n for (const issue of issues) {\n logger.warn(` \u2022 ${issue}`)\n }\n\n return null\n }\n\n recapSources(plan, model)\n echoManual(plan, selection)\n\n // cmux opens one pane per selected API app. `include` picks WHICH apps get a pane; `presetDef` tells\n // each pane exactly which of its parts to run. Without the latter a pane runs `--app=<name>`, which\n // expands to every part the app has \u2014 silently starting a UI the user just unticked.\n const apiApps = plan.targetKeys\n .filter((k) => {\n return k.endsWith('/api')\n })\n .map((k) => {\n return k.split('/')[0]!\n })\n\n // An empty include would collapse to `null` in `normalizeAppInclude` and make cmux run EVERY api app,\n // so a cmux run with no selected backends (e.g. an all-frontend selection) falls back to in-process.\n if (cmux && apiApps.length > 0) {\n return { include: apiApps, presetDef: plan.presetDef, watch, cmux: true }\n }\n\n if (cmux) {\n logger.info('\u2139\uFE0F cmux needs at least one local backend (panes are backend-only) \u2014 running in-process instead.')\n }\n\n return { presetDef: plan.presetDef, watch, cmux: false }\n}\n\n/** Print the equivalent (`--target=\u2026`) flag command, plus a save-as-preset ergonomics hint. */\nconst echoManual = (plan: DerivedPlan, selection: ManualSelection): void => {\n commandEcho.setInteractive()\n\n const eq = equivalentCommand(plan, selection)\n\n commandEcho.addOption(eq.flags, true)\n commandEcho.print()\n\n // The `--target=\u2026` line reproduces the selection exactly; the preset is pure convenience \u2014 a short,\n // memorable name instead of retyping the target keys.\n logger.info('\u2139\uFE0F Prefer a short name? Save this selection as a devPreset and run `infra-kit dev <name>`.')\n}\n\n/** The preset-branch flow: run a named preset, asking only whether to watch (presets can't encode it). */\nconst runPresetBranch = async (prompts: WizardPrompts, preset: string): Promise<WizardResult> => {\n const watch = await prompts.confirm({ message: '\uD83D\uDC40 Rebuild & restart on save (watch)?', default: false })\n\n commandEcho.setInteractive()\n commandEcho.addOption(preset, true)\n if (watch) commandEcho.addOption('--watch', true)\n commandEcho.print()\n\n return { preset, watch, cmux: false }\n}\n\n/** Sentinel value for the \"Manual (custom)\" step-0 choice (a preset name can never be empty). */\nexport const MANUAL_CHOICE = ' manual'\n\n/**\n * Drive the wizard's branch flow over an ALREADY-gathered model (no disk/config access). Split from\n * {@link runDevWizard} so the flow is unit-testable with scripted prompts + a fixture model.\n */\nexport const runWizardFlow = async (prompts: WizardPrompts, model: WizardModel): Promise<WizardResult | null> => {\n if (model.presets.length === 0) {\n return runManualBranch(prompts, model)\n }\n\n const choice = await prompts.select({\n message: '\uD83D\uDE80 Start from a preset, or configure manually?',\n choices: [\n ...model.presets.map((p) => {\n return { name: p, value: p }\n }),\n new Separator(' '),\n { name: 'Manual (custom)\u2026', value: MANUAL_CHOICE },\n ],\n })\n\n return choice === MANUAL_CHOICE ? runManualBranch(prompts, model) : runPresetBranch(prompts, choice)\n}\n\n/**\n * Gather the model from disk/config, then drive the wizard flow, returning the resolved run options (or\n * null on cancel / empty selection). The entry point calls this on a bare TTY `infra-kit dev`.\n */\nexport const runDevWizard = async (\n prompts: WizardPrompts = defaultPrompts,\n root: string = findMonorepoRoot(process.cwd()),\n): Promise<WizardResult | null> => {\n return runWizardFlow(prompts, await gatherWizardModel(root))\n}\n", "/**\n * Pure core of the interactive `infra-kit dev` wizard (the no-args, TTY path).\n *\n * The wizard is FRONTEND-CENTRIC: you pick which frontends to run, and for each frontend you choose,\n * every proxy route that has TWO real options (its `from` lists both `local` and `cloud`) is decided\n * EXPLICITLY \u2014 local (its backend is launched \u2192 the route resolves local) or cloud (the route proxies\n * to the `<env>` cloud target). Single-option routes are auto-picked. Because a backend runs local OR\n * cloud as a whole, routes that share one backend package RECONCILE: if any of them resolved local, the\n * backend runs local and all its local-capable routes follow. This module turns those answers into an\n * in-memory {@link DevPreset} the runner consumes verbatim (part-level targets the app-name-only `--app`\n * include cannot express) \u2014 and is side-effect-free so the mapping is fully unit-testable. The impure\n * I/O (discovery, inquirer prompts, audit, launch) lives in {@link file://./dev-wizard-run.ts}.\n */\nimport type { DevPreset, ProxySource } from 'src/lib/infra-kit-config'\n\n/**\n * One proxy route's per-route capability, carried through grouping so the wizard can decide whether it\n * even needs to ask about the route (only a `local`+`cloud` route is a real choice).\n */\nexport interface RouteCap {\n /** Route path (e.g. `/api`). */\n path: string\n /** `from` includes `local` \u2014 the route can resolve to a locally-launched backend. */\n localCapable: boolean\n /** `from` includes `cloud` \u2014 the route can resolve to the cloud target. */\n cloudCapable: boolean\n /** The route's declared `default` source when it has one (present iff `from` lists >1 source) \u2014 seeds the prompt's default. */\n default?: ProxySource\n}\n\n/**\n * One backend a frontend's proxy routes can be pointed at, grouped by the backend package. A frontend\n * that maps `/api` and `/media` to the same package yields ONE {@link ProxyBackend} with both routes \u2014\n * launching the package flips every one of its `local`-capable routes together (the reconcile in\n * {@link deriveManualPlan}).\n */\nexport interface ProxyBackend {\n /** Backend package name (a route's `packageName`). */\n packageName: string\n /** The routes that resolve to this package, each with its own local/cloud capability. */\n routes: RouteCap[]\n /** True when at least one of `routes` lists `local` in its `from` capabilities \u2014 i.e. it is launchable-local. */\n localCapable: boolean\n /** True when at least one of `routes` lists `cloud` \u2014 i.e. it can stay on the cloud target. */\n cloudCapable: boolean\n /** App folder whose `api` package is {@link packageName}, or undefined when no discovered app owns it (a `local` choice then has nothing to launch \u2014 the audit flags it). */\n ownerApp?: string\n}\n\n/** A discovered app as the wizard sees it: which parts exist + which backends its frontend proxies to. */\nexport interface WizardApp {\n /** App folder name (e.g. `client`). */\n name: string\n /** Has `apps/<app>/api` (a launchable backend). */\n hasApi: boolean\n /** Has `apps/<app>/ui` (a launchable frontend). */\n hasUi: boolean\n /**\n * This app's OWN `api` package name (from `apps/<app>/api`), or undefined when it has no api. Carried\n * so the audit can build a faithful `app \u2192 apiPackage` map for EVERY discovered api app \u2014 including\n * api-only apps a frontend proxies to cross-app \u2014 mirroring the root audit (`preset-proxy-check.ts`).\n */\n apiPackage?: string\n /** Backends this app's frontend proxies to (empty for api-only apps or a frontend with no `dev.proxy`). */\n backends: ProxyBackend[]\n}\n\n/** Everything the wizard needs, gathered impurely by {@link file://./dev-wizard-run.ts}. */\nexport interface WizardModel {\n /** All discovered apps (api and/or ui), sorted by name. */\n apps: WizardApp[]\n /** `devServersPresets` names (drives the preset-or-manual step-0). */\n presets: string[]\n /** Configured `environments` (the cloud-env `select` options). */\n environments: string[]\n}\n\n/** The manual-branch answers collected from the prompts, fed to {@link deriveManualPlan}. */\nexport interface ManualSelection {\n /**\n * The target keys the user ticked directly: every selected `<app>/ui` frontend, plus any api-only\n * `<app>/api` backend (apps with an api but no ui). A full-stack backend is NOT ticked here \u2014 it is\n * launched by choosing `local` for one of its frontend's routes (recorded in {@link sources}).\n */\n targets: string[]\n /**\n * Explicit per-route choice for every TWO-OPTION route, keyed `${app}/ui ${routePath}` (e.g.\n * `client/ui /api`). Single-option routes are absent (auto-picked). Missing two-option answers fall\n * back to the route's cloud/local capability in {@link deriveManualPlan}.\n */\n sources: Record<string, ProxySource>\n /** Chosen cloud env for cloud routes (undefined when nothing resolves cloud). */\n env?: string\n /** Rebuild + restart on save. */\n watch: boolean\n /** One cmux pane per app. */\n cmux: boolean\n}\n\n/** The resolved run plan derived from a {@link ManualSelection}. */\nexport interface DerivedPlan {\n /** In-memory preset handed to the runner (`options.presetDef`). */\n presetDef: DevPreset\n /** At least one route resolves to cloud \u2192 an env must be chosen (pre-flight gate). */\n anyCloudRoute: boolean\n /** Resolved `<app>/<part>` target keys, sorted \u2014 for the echo + audit display. */\n targetKeys: string[]\n}\n\n/** App folder name from an `<app>/<part>` target key. */\nconst appOf = (key: string): string => {\n return key.split('/')[0]!\n}\n\n/** Index a model's apps by folder name for O(1) lookup. */\nconst byName = (model: WizardModel): Map<string, WizardApp> => {\n return new Map(\n model.apps.map((a) => {\n return [a.name, a]\n }),\n )\n}\n\n/** Is this route a real choice (both local and cloud reachable)? */\nconst isTwoOption = (route: RouteCap): boolean => {\n return route.localCapable && route.cloudCapable\n}\n\n/**\n * The source a single route resolves to BEFORE per-backend reconciliation:\n * - two-option: the user's recorded answer, else 'cloud' when cloud-capable else 'local';\n * - single local-only: always 'local';\n * - single cloud-only: always 'cloud'.\n */\nconst rawSource = (uiKey: string, route: RouteCap, sources: Record<string, ProxySource>): ProxySource => {\n if (isTwoOption(route)) {\n return sources[`${uiKey} ${route.path}`] ?? (route.cloudCapable ? 'cloud' : 'local')\n }\n\n return route.localCapable ? 'local' : 'cloud'\n}\n\n/**\n * Pass 1: which backend packages resolve local (any of their routes did), plus each package's owner app.\n * A package's local verdict is shared across every frontend that proxies to it \u2014 that is the reconcile.\n */\nconst collectLocalPackages = (\n uiKeys: string[],\n apps: Map<string, WizardApp>,\n sources: Record<string, ProxySource>,\n): { localPkgs: Set<string>; ownerByPkg: Map<string, string | undefined> } => {\n const localPkgs = new Set<string>()\n const ownerByPkg = new Map<string, string | undefined>()\n\n for (const key of uiKeys) {\n const app = apps.get(appOf(key))\n\n for (const backend of app?.backends ?? []) {\n ownerByPkg.set(backend.packageName, backend.ownerApp)\n for (const route of backend.routes) {\n if (rawSource(key, route, sources) === 'local') localPkgs.add(backend.packageName)\n }\n }\n }\n\n return { localPkgs, ownerByPkg }\n}\n\n/**\n * Pass 2 for ONE frontend: its per-route overrides after the per-package verdict, and whether any route\n * went cloud. A local-capable route on a local package is `local` (reconciled up); everything else `cloud`.\n */\nconst frontendOverrides = (\n app: WizardApp | undefined,\n localPkgs: ReadonlySet<string>,\n): { overrides: Record<string, ProxySource>; anyCloud: boolean } => {\n const overrides: Record<string, ProxySource> = {}\n let anyCloud = false\n\n for (const backend of app?.backends ?? []) {\n const backendLocal = localPkgs.has(backend.packageName)\n\n for (const route of backend.routes) {\n const source: ProxySource = route.localCapable && backendLocal ? 'local' : 'cloud'\n\n overrides[route.path] = source\n if (source === 'cloud') anyCloud = true\n }\n }\n\n return { overrides, anyCloud }\n}\n\n/**\n * Turn the manual {@link ManualSelection} into an in-memory {@link DevPreset}. The frontend checkbox picks\n * which UIs (and standalone api-only backends) to run; the per-route answers in {@link ManualSelection.sources}\n * decide each two-option route. Resolution is two passes so same-backend routes stay coherent:\n *\n * 1. Resolve every route of every selected frontend to its raw source; a package is LOCAL iff any of its\n * routes resolved local.\n * 2. Re-resolve each route against that per-package verdict: a local-capable route on a local package is\n * `local` (reconciled up even if it was individually answered cloud); a cloud-only route stays `cloud`.\n *\n * Launched backends are the ticked api-only parts \u222A every package that resolved local (added as\n * `${ownerApp}/api` when the owner is discoverable \u2014 a local override on an owner-less package is still\n * emitted, and the audit flags it).\n *\n * @example\n * // client frontend, its /api answered local \u2192 launches client/api + client/ui, /api resolves local:\n * deriveManualPlan(\n * { targets: ['client/ui'], sources: { 'client/ui /api': 'local' }, watch: false, cmux: false },\n * { apps: [{ name: 'client', hasApi: true, hasUi: true,\n * backends: [{ packageName: 'client-api', cloudCapable: true, localCapable: true,\n * routes: [{ path: '/api', localCapable: true, cloudCapable: true }],\n * ownerApp: 'client' }] }],\n * presets: [], environments: ['dev'] },\n * ).targetKeys // => ['client/api', 'client/ui']\n */\nexport const deriveManualPlan = (selection: ManualSelection, model: WizardModel): DerivedPlan => {\n const apps = byName(model)\n const uiKeys = selection.targets.filter((t) => {\n return t.endsWith('/ui')\n })\n const apiOnlyKeys = selection.targets.filter((t) => {\n return t.endsWith('/api')\n })\n\n // Pass 1: a package is local iff any of its routes (across every selected frontend) resolved local.\n const { localPkgs, ownerByPkg } = collectLocalPackages(uiKeys, apps, selection.sources)\n\n // Pass 2: re-resolve each route against the per-package verdict + build per-frontend overrides.\n const presetApps: Record<string, { proxy?: Record<string, ProxySource> }> = {}\n let anyCloud = false\n\n for (const key of uiKeys) {\n const { overrides, anyCloud: cloud } = frontendOverrides(apps.get(appOf(key)), localPkgs)\n\n if (cloud) anyCloud = true\n presetApps[key] = Object.keys(overrides).length > 0 ? { proxy: overrides } : {}\n }\n\n // Launch: the ticked api-only backends, plus every package that resolved local (via its owner app).\n for (const key of apiOnlyKeys) {\n presetApps[key] ??= {}\n }\n for (const pkg of localPkgs) {\n const owner = ownerByPkg.get(pkg)\n\n if (owner != null) presetApps[`${owner}/api`] ??= {}\n }\n\n return {\n presetDef: { apps: presetApps, cmux: selection.cmux },\n anyCloudRoute: anyCloud,\n targetKeys: Object.keys(presetApps).sort(),\n }\n}\n\n/** The equivalent non-interactive command for a derived plan. */\nexport interface EquivalentCommand {\n /** The `--target=<app>/<part>,\u2026`-form flag string (no leading `infra-kit dev`). */\n flags: string\n}\n\n/**\n * Build the equivalent `infra-kit dev --target=\u2026 [--watch] [--cmux]` flag string for a derived plan.\n * Always exact: `--target` names packages at `<app>/<part>` granularity \u2014 the same grammar the plan's\n * `targetKeys` already use \u2014 so every wizard selection, whole-app or part-level, round-trips into a\n * pasteable command. (`--app` is deliberately NOT used here: its app-name granularity over-launches a\n * part-level pick \u2014 `--app=client` starts `client/api` even when only `client/ui` was ticked.)\n */\nexport const equivalentCommand = (plan: DerivedPlan, selection: ManualSelection): EquivalentCommand => {\n const parts = [`--target=${plan.targetKeys.join(',')}`]\n\n if (selection.watch) parts.push('--watch')\n if (selection.cmux) parts.push('--cmux')\n\n return { flags: parts.join(' ') }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Impure driver for the interactive `infra-kit dev` wizard: filesystem/config discovery, the inquirer\n * prompt flow (step-0 preset-or-manual, then the manual matrix), the pre-flight proxy audit, and the\n * `commandEcho` teaching line. The pure answer\u2192plan mapping lives in {@link file://./dev-wizard.ts}.\n *\n * Prompts are behind the injectable {@link WizardPrompts} seam so the flow is testable without a TTY.\n * This module is reached ONLY from the bare-invocation TTY branch of the entry point; every flagged /\n * non-TTY / `--json` / MCP invocation bypasses it entirely.\n */\nimport inquirerCheckbox from '@inquirer/checkbox'\nimport inquirerConfirm from '@inquirer/confirm'\nimport inquirerSelect, { Separator } from '@inquirer/select'\nimport { loadDev } from '@slip-stream-kit/config/internal'\nimport path from 'node:path'\nimport process from 'node:process'\n\nimport { discoverApiApps, discoverUiApps, findMonorepoRoot } from 'src/dev/discovery'\nimport { commandEcho } from 'src/lib/command-echo'\nimport { INFRA_KIT_ENV_VAR } from 'src/lib/constants'\nimport { readTokenStore } from 'src/lib/env-tokens'\nimport { getInfraKitConfig } from 'src/lib/infra-kit-config'\nimport type { DevPreset, ProxySource } from 'src/lib/infra-kit-config'\nimport { logger } from 'src/lib/logger'\nimport { withEscape } from 'src/lib/prompts/escapable-context'\n\nimport { deriveManualPlan, equivalentCommand } from './dev-wizard.js'\nimport type { DerivedPlan, ManualSelection, ProxyBackend, RouteCap, WizardApp, WizardModel } from './dev-wizard.js'\nimport { validatePresetProxy } from './presets.js'\nimport type { DiscoveredParts, PresetProxyContext } from './presets.js'\n\n/** A wizard prompt choice \u2014 all values are strings, so the seam needs no generics. */\nexport interface WizardChoice {\n name: string\n value: string\n description?: string\n checked?: boolean\n disabled?: boolean | string\n}\n\n/**\n * Injectable prompt seam (a string-valued subset of `@inquirer/*`). The default implementation\n * ({@link defaultPrompts}) delegates to the real prompts, rendering to stderr so the dev-server's\n * stdout stays clean. Tests pass a scripted object.\n */\nexport interface WizardPrompts {\n select: (cfg: { message: string; choices: (WizardChoice | Separator)[]; default?: string }) => Promise<string>\n checkbox: (cfg: { message: string; choices: WizardChoice[] }) => Promise<string[]>\n confirm: (cfg: { message: string; default?: boolean }) => Promise<boolean>\n}\n\n/**\n * Shared `@inquirer/*` context. Rendered to stderr (mirrors the bare-`infra-kit` command palette).\n *\n * `clearPromptOnDone` erases each answered prompt instead of leaving a `\u2714 <question> <answer>` line\n * behind, so the wizard collapses to nothing once it finishes and the dev-server's ready header lands\n * at the top of a clean screen. The choices are not lost: {@link echoManual} / {@link runPresetBranch}\n * print the equivalent flag command, which is the durable record of what was picked.\n */\nconst promptContext = { output: process.stderr, clearPromptOnDone: true }\n\n/**\n * Real `@inquirer/*` prompts, rendered to stderr and erased once answered.\n *\n * Every one goes through {@link withEscape}, which binds Esc to cancellation. The wrap is here rather\n * than around the {@link WizardPrompts} seam so the scripted prompts the tests inject stay untouched:\n * the seam is the test boundary, `defaultPrompts` is the only implementation that talks to a terminal.\n *\n * Esc rejects with an `AbortPromptError`, which `entry/cli.ts` catches as a cancellation \u2192 exit 0. The\n * wizard therefore needs no cancel branch of its own: a cancelled prompt never returns here at all, so\n * no server is started.\n */\nexport const defaultPrompts: WizardPrompts = {\n select: (cfg) => {\n return withEscape((context) => {\n return inquirerSelect({ message: cfg.message, choices: cfg.choices, default: cfg.default }, context)\n }, promptContext)\n },\n checkbox: (cfg) => {\n return withEscape((context) => {\n return inquirerCheckbox({ message: cfg.message, choices: cfg.choices }, context)\n }, promptContext)\n },\n confirm: (cfg) => {\n return withEscape((context) => {\n return inquirerConfirm({ message: cfg.message, default: cfg.default }, context)\n }, promptContext)\n },\n}\n\n/** What the wizard hands back to the entry point (merged into `DevServerOptions`), or null on cancel. */\nexport interface WizardResult {\n /** Named preset (preset branch) \u2014 passed through as `options.preset`. */\n preset?: string\n /** In-memory preset (manual branch, non-cmux) \u2014 passed through as `options.presetDef`. */\n presetDef?: DevPreset\n /**\n * App-name include list for the cmux path only: which discovered API apps get a pane. Paired with\n * `presetDef`, which tells each pane the exact parts to run. An app selected UI-only gets no pane\n * (cmux panes are opened per API app). Unset for the non-cmux manual path.\n */\n include?: string[]\n watch: boolean\n cmux: boolean\n}\n\n/** Group one frontend's `dev.proxy.routes` into {@link ProxyBackend}s keyed by backend package. */\nconst groupBackends = (\n routes: Record<string, { packageName: string; from: readonly string[]; default?: 'local' | 'cloud' }>,\n ownerByPkg: Map<string, string>,\n): ProxyBackend[] => {\n const byPkg = new Map<string, ProxyBackend>()\n\n for (const [route, def] of Object.entries(routes)) {\n const cap: RouteCap = {\n path: route,\n localCapable: def.from.includes('local'),\n cloudCapable: def.from.includes('cloud'),\n default: def.default,\n }\n const existing = byPkg.get(def.packageName)\n\n if (existing) {\n existing.routes.push(cap)\n existing.localCapable = existing.localCapable || cap.localCapable\n existing.cloudCapable = existing.cloudCapable || cap.cloudCapable\n } else {\n byPkg.set(def.packageName, {\n packageName: def.packageName,\n routes: [cap],\n localCapable: cap.localCapable,\n cloudCapable: cap.cloudCapable,\n ownerApp: ownerByPkg.get(def.packageName),\n })\n }\n }\n\n return [...byPkg.values()]\n}\n\n/**\n * Load one frontend's proxy routes and group them into backends. A config that throws \u2014 schema-invalid\n * or an import-time error \u2014 degrades THAT app to zero backends with a named warning instead of rejecting.\n * Without this, a single broken `apps/<x>/ui/infra-kit.config.ts` fails the whole `Promise.all` in\n * {@link gatherWizardModel} and takes the entire wizard down, so `infra-kit dev` becomes unusable for every\n * other app in the monorepo. Mirrors the audit path's per-app guard ({@link file://../commands/audit/preset-proxy-check.ts}).\n */\nexport const loadBackends = async (\n uiDir: string,\n name: string,\n ownerByPkg: Map<string, string>,\n): Promise<ProxyBackend[]> => {\n try {\n const routes = (await loadDev(uiDir))?.proxy?.routes\n\n return routes ? groupBackends(routes, ownerByPkg) : []\n } catch (error) {\n logger.warn(\n `\u26A0\uFE0F Skipping ${name}: its dev config failed to load \u2014 ${error instanceof Error ? error.message : String(error)}`,\n )\n\n return []\n }\n}\n\n/** Discover apps + configs and assemble the {@link WizardModel} (backends resolved per frontend). */\nexport const gatherWizardModel = async (root: string): Promise<WizardModel> => {\n const apiApps = discoverApiApps(root)\n const uiApps = discoverUiApps(root)\n const config = await getInfraKitConfig()\n\n const ownerByPkg = new Map(\n apiApps.map((a) => {\n return [a.packageName, a.name] as const\n }),\n )\n const apiPkgByName = new Map(\n apiApps.map((a) => {\n return [a.name, a.packageName] as const\n }),\n )\n const uiNames = new Set(\n uiApps.map((a) => {\n return a.name\n }),\n )\n const apiNames = new Set(\n apiApps.map((a) => {\n return a.name\n }),\n )\n const allNames = [...new Set([...apiNames, ...uiNames])].sort()\n\n const apps: WizardApp[] = await Promise.all(\n allNames.map(async (name): Promise<WizardApp> => {\n const hasUi = uiNames.has(name)\n\n return {\n name,\n hasApi: apiNames.has(name),\n hasUi,\n apiPackage: apiPkgByName.get(name),\n backends: hasUi ? await loadBackends(path.join(root, 'apps', name, 'ui'), name, ownerByPkg) : [],\n }\n }),\n )\n\n // The cloud-env choices are the envs we hold a token for \u2014 the same authority as `env-load`, because\n // it is the same question. This picker writes INFRA_KIT_ENV, which `@slip-stream-kit/config/vite`\n // reads to build the cloud backend's URL, and whose only other writer is `env-load` (which cannot run\n // without a Doppler token). Sourcing it from the workflow options instead would put `prod` in the list\n // for everyone \u2014 including a developer holding no prod credential \u2014 and point a local UI at\n // production. No token, no entry.\n const store = await readTokenStore()\n\n return {\n apps,\n presets: Object.keys(config.devServersPresets ?? {}),\n environments: Object.keys(store?.envs ?? {}).sort(),\n }\n}\n\n/**\n * Build the checkbox choices: one `<app>/ui` per frontend, plus `<app>/api` ONLY for api-only apps\n * (an api with no ui). A full-stack `<app>/api` is deliberately NOT offered \u2014 a full-stack backend is\n * launched by choosing `local` for one of its frontend's routes (asked per-route afterwards), so the\n * checkbox stays a pick of frontends + standalone backends.\n *\n * Nothing starts checked: the manual branch is opt-in, so a run only ever launches what was explicitly\n * ticked. Pre-checking every part would make the fast path (accept the defaults) boot the whole monorepo\n * \u2014 the exact opposite of why someone reached for \"Manual\" over a preset.\n */\nconst buildPartChoices = (model: WizardModel): WizardChoice[] => {\n const choices: WizardChoice[] = []\n\n for (const app of model.apps) {\n if (app.hasUi) {\n const paths = app.backends.flatMap((b) => {\n return b.routes.map((r) => {\n return r.path\n })\n })\n const description = paths.length > 0 ? `frontend \u2014 proxies ${[...paths].sort().join(', ')}` : 'frontend'\n\n choices.push({ name: `${app.name}/ui`, value: `${app.name}/ui`, description })\n }\n if (app.hasApi && !app.hasUi) {\n choices.push({ name: `${app.name}/api`, value: `${app.name}/api`, description: 'backend' })\n }\n }\n\n return choices\n}\n\n/** Build the audit context from the gathered model (no re-reading of configs). */\nconst buildAuditContext = (model: WizardModel): PresetProxyContext => {\n const discovered: DiscoveredParts = {\n api: model.apps\n .filter((a) => {\n return a.hasApi\n })\n .map((a) => {\n return a.name\n }),\n ui: model.apps\n .filter((a) => {\n return a.hasUi\n })\n .map((a) => {\n return a.name\n }),\n }\n const apiPkgByApp: Record<string, string> = {}\n const routeToPkg = new Map<string, string>()\n\n for (const app of model.apps) {\n // Map EVERY discovered api app to its own api package \u2014 including api-only apps a frontend proxies\n // to cross-app \u2014 so `launchedPkgs` in validatePresetProxy is faithful (mirrors preset-proxy-check.ts).\n // Deriving this from proxy backends alone would miss api-only owners and false-positive the audit.\n if (app.apiPackage != null) apiPkgByApp[app.name] = app.apiPackage\n for (const b of app.backends) {\n for (const route of b.routes) {\n routeToPkg.set(`${app.name} ${route.path}`, b.packageName)\n }\n }\n }\n\n return {\n discovered,\n apiPkgByApp,\n routePkg: (app, route) => {\n return routeToPkg.get(`${app} ${route}`)\n },\n }\n}\n\n/**\n * Run the assembled plan through the SAME proxy-locality rule the root audit uses. Returns issue\n * messages (empty when clean). Catches a `local` override whose backend won't launch \u2014 the wizard's\n * derivation launches owners, so a hit means the backend has no discoverable owning app.\n */\nexport const auditManualPlan = (presetDef: DevPreset, model: WizardModel): string[] => {\n return validatePresetProxy({ __wizard__: presetDef }, buildAuditContext(model)).map((i) => {\n return i.message\n })\n}\n\n/**\n * Opt back into the cmux question: set to exactly `'1'`. The prompt is temporarily off \u2014 the manual\n * branch always runs in-process \u2014 but the derivation below is kept whole for when it returns. `--cmux`\n * on the command line is a separate path and stays live either way.\n */\nexport const WIZARD_CMUX_VAR = 'INFRA_KIT_DEV_WIZARD_CMUX'\n\nconst asksCmux = (): boolean => {\n return process.env[WIZARD_CMUX_VAR] === '1'\n}\n\n/**\n * Ask, per selected frontend, an EXPLICIT local/cloud choice for every TWO-OPTION route (its `from`\n * lists both). Single-option routes are auto-picked in the pure mapping, so they are never prompted.\n * The answers are keyed `${app}/ui ${routePath}` to match {@link ManualSelection.sources}; the select's\n * default seeds from the route's declared `default` when it has one.\n */\nconst promptRouteSources = async (\n prompts: WizardPrompts,\n model: WizardModel,\n uiKeys: string[],\n): Promise<Record<string, ProxySource>> => {\n const apps = new Map(\n model.apps.map((a) => {\n return [a.name, a] as const\n }),\n )\n const sources: Record<string, ProxySource> = {}\n\n for (const key of uiKeys) {\n const app = apps.get(key.split('/')[0]!)\n\n for (const backend of app?.backends ?? []) {\n for (const route of backend.routes) {\n if (!route.localCapable || !route.cloudCapable) continue\n\n const answer = await prompts.select({\n message: `\uD83D\uDD00 ${key} ${route.path} \u2192 local or cloud?`,\n choices: [\n { name: 'local (this machine)', value: 'local' },\n { name: 'cloud', value: 'cloud' },\n ],\n default: route.default ?? 'local',\n })\n\n sources[`${key} ${route.path}`] = answer as ProxySource\n }\n }\n }\n\n return sources\n}\n\n/** Look up a frontend route's capability in the model (for the recap's \"only option\" annotation). */\nconst findRouteCap = (model: WizardModel, app: string, routePath: string): RouteCap | undefined => {\n return model.apps\n .find((a) => {\n return a.name === app\n })\n ?.backends.flatMap((b) => {\n return b.routes\n })\n .find((r) => {\n return r.path === routePath\n })\n}\n\n/**\n * Print a one-line-per-route recap of what each frontend route resolved to, so the chosen sources are\n * visible before launch. A single-option route is annotated `(only option)` \u2014 it was auto-picked, not\n * asked. This is in addition to {@link echoManual}'s pasteable `--target` command.\n */\nconst recapSources = (plan: DerivedPlan, model: WizardModel): void => {\n const lines: string[] = []\n\n for (const [key, cfg] of Object.entries(plan.presetDef.apps ?? {})) {\n if (!key.endsWith('/ui') || !cfg.proxy) continue\n\n const app = key.split('/')[0]!\n\n for (const [routePath, source] of Object.entries(cfg.proxy)) {\n const cap = findRouteCap(model, app, routePath)\n const suffix = cap && cap.localCapable && cap.cloudCapable ? '' : ' (only option)'\n\n lines.push(` ${key} ${routePath} \u2192 ${source}${suffix}`)\n }\n }\n\n if (lines.length === 0) return\n logger.info('Resolved routes:')\n for (const line of [...lines].sort()) {\n logger.info(line)\n }\n}\n\n/** The manual-branch flow: frontends + per-route source + env + watch + cmux \u2192 audited plan \u2192 recap \u2192 echo. */\nconst runManualBranch = async (prompts: WizardPrompts, model: WizardModel): Promise<WizardResult | null> => {\n if (model.apps.length === 0) {\n logger.warn('No apps discovered to run.')\n\n return null\n }\n\n const selectedTargets = await prompts.checkbox({ message: '\uD83D\uDCE6 Which packages?', choices: buildPartChoices(model) })\n\n if (selectedTargets.length === 0) {\n logger.warn('No packages selected.')\n\n return null\n }\n\n const uiKeys = selectedTargets.filter((t) => {\n return t.endsWith('/ui')\n })\n const sources = await promptRouteSources(prompts, model, uiKeys)\n\n const watch = await prompts.confirm({ message: '\uD83D\uDC40 Rebuild & restart on save (watch)?', default: false })\n const cmux = asksCmux()\n ? await prompts.confirm({ message: '\uD83E\uDDE9 Run each app in its own cmux pane?', default: false })\n : false\n\n const selection: ManualSelection = { targets: selectedTargets, sources, watch, cmux }\n const plan = deriveManualPlan(selection, model)\n\n if (plan.anyCloudRoute) {\n // A cloud route needs an environment to point at, and the only environments we can offer are those we\n // hold a token for. With none loaded, the `select` below would be handed zero choices and `@inquirer/select`\n // throws `No selectable choices` \u2014 a raw stack on a realistic first-run path (fresh clone, no `env-load`).\n // Back out cleanly with guidance instead, and BEFORE mutating `process.env`, so a refused run leaks nothing.\n if (model.environments.length === 0) {\n logger.warn('\u2601\uFE0F This selection routes to a cloud environment, but no environment token is loaded.')\n logger.warn(' Run `infra-kit env-load` to add one, then re-run the wizard.')\n\n return null\n }\n\n selection.env = await prompts.select({\n message: '\u2601\uFE0F Point cloud routes at which environment?',\n choices: model.environments.map((e) => {\n return { name: e, value: e }\n }),\n })\n process.env[INFRA_KIT_ENV_VAR] = selection.env\n }\n\n const issues = auditManualPlan(plan.presetDef, model)\n\n if (issues.length > 0) {\n logger.warn('\u26A0\uFE0F Proxy audit found issues with this selection:')\n for (const issue of issues) {\n logger.warn(` \u2022 ${issue}`)\n }\n\n return null\n }\n\n recapSources(plan, model)\n echoManual(plan, selection)\n\n // cmux opens one pane per selected API app. `include` picks WHICH apps get a pane; `presetDef` tells\n // each pane exactly which of its parts to run. Without the latter a pane runs `--app=<name>`, which\n // expands to every part the app has \u2014 silently starting a UI the user just unticked.\n const apiApps = plan.targetKeys\n .filter((k) => {\n return k.endsWith('/api')\n })\n .map((k) => {\n return k.split('/')[0]!\n })\n\n // An empty include would collapse to `null` in `normalizeAppInclude` and make cmux run EVERY api app,\n // so a cmux run with no selected backends (e.g. an all-frontend selection) falls back to in-process.\n if (cmux && apiApps.length > 0) {\n return { include: apiApps, presetDef: plan.presetDef, watch, cmux: true }\n }\n\n if (cmux) {\n logger.info('\u2139\uFE0F cmux needs at least one local backend (panes are backend-only) \u2014 running in-process instead.')\n }\n\n return { presetDef: plan.presetDef, watch, cmux: false }\n}\n\n/** Print the equivalent (`--target=\u2026`) flag command, plus a save-as-preset ergonomics hint. */\nconst echoManual = (plan: DerivedPlan, selection: ManualSelection): void => {\n commandEcho.setInteractive()\n\n const eq = equivalentCommand(plan, selection)\n\n commandEcho.addOption(eq.flags, true)\n commandEcho.print()\n\n // The `--target=\u2026` line reproduces the selection exactly; the preset is pure convenience \u2014 a short,\n // memorable name instead of retyping the target keys.\n logger.info('\u2139\uFE0F Prefer a short name? Save this selection as a devPreset and run `infra-kit dev <name>`.')\n}\n\n/** The preset-branch flow: run a named preset, asking only whether to watch (presets can't encode it). */\nconst runPresetBranch = async (prompts: WizardPrompts, preset: string): Promise<WizardResult> => {\n const watch = await prompts.confirm({ message: '\uD83D\uDC40 Rebuild & restart on save (watch)?', default: false })\n\n commandEcho.setInteractive()\n commandEcho.addOption(preset, true)\n if (watch) commandEcho.addOption('--watch', true)\n commandEcho.print()\n\n return { preset, watch, cmux: false }\n}\n\n/** Sentinel value for the \"Manual (custom)\" step-0 choice (a preset name can never be empty). */\nexport const MANUAL_CHOICE = ' manual'\n\n/**\n * Drive the wizard's branch flow over an ALREADY-gathered model (no disk/config access). Split from\n * {@link runDevWizard} so the flow is unit-testable with scripted prompts + a fixture model.\n */\nexport const runWizardFlow = async (prompts: WizardPrompts, model: WizardModel): Promise<WizardResult | null> => {\n if (model.presets.length === 0) {\n return runManualBranch(prompts, model)\n }\n\n const choice = await prompts.select({\n message: '\uD83D\uDE80 Start from a preset, or configure manually?',\n choices: [\n ...model.presets.map((p) => {\n return { name: p, value: p }\n }),\n new Separator(' '),\n { name: 'Manual (custom)\u2026', value: MANUAL_CHOICE },\n ],\n })\n\n return choice === MANUAL_CHOICE ? runManualBranch(prompts, model) : runPresetBranch(prompts, choice)\n}\n\n/**\n * Gather the model from disk/config, then drive the wizard flow, returning the resolved run options (or\n * null on cancel / empty selection). The entry point calls this on a bare TTY `infra-kit dev`.\n */\nexport const runDevWizard = async (\n prompts: WizardPrompts = defaultPrompts,\n root: string = findMonorepoRoot(process.cwd()),\n): Promise<WizardResult | null> => {\n return runWizardFlow(prompts, await gatherWizardModel(root))\n}\n", "/**\n * Pure core of the interactive `infra-kit dev` wizard (the no-args, TTY path).\n *\n * The wizard is FRONTEND-CENTRIC: you pick which frontends to run, and for each frontend you choose,\n * every proxy route that has TWO real options (its `from` lists both `local` and `cloud`) is decided\n * EXPLICITLY \u2014 local (its backend is launched \u2192 the route resolves local) or cloud (the route proxies\n * to the `<env>` cloud target). Single-option routes are auto-picked. Because a backend runs local OR\n * cloud as a whole, routes that share one backend package RECONCILE: if any of them resolved local, the\n * backend runs local and all its local-capable routes follow. This module turns those answers into an\n * in-memory {@link DevPreset} the runner consumes verbatim (part-level targets the app-name-only `--app`\n * include cannot express) \u2014 and is side-effect-free so the mapping is fully unit-testable. The impure\n * I/O (discovery, inquirer prompts, audit, launch) lives in {@link file://./dev-wizard-run.ts}.\n */\nimport type { DevPreset, ProxySource } from 'src/lib/infra-kit-config'\n\n/**\n * One proxy route's per-route capability, carried through grouping so the wizard can decide whether it\n * even needs to ask about the route (only a `local`+`cloud` route is a real choice).\n */\nexport interface RouteCap {\n /** Route path (e.g. `/api`). */\n path: string\n /** `from` includes `local` \u2014 the route can resolve to a locally-launched backend. */\n localCapable: boolean\n /** `from` includes `cloud` \u2014 the route can resolve to the cloud target. */\n cloudCapable: boolean\n /** The route's declared `default` source when it has one (present iff `from` lists >1 source) \u2014 seeds the prompt's default. */\n default?: ProxySource\n}\n\n/**\n * One backend a frontend's proxy routes can be pointed at, grouped by the backend package. A frontend\n * that maps `/api` and `/media` to the same package yields ONE {@link ProxyBackend} with both routes \u2014\n * launching the package flips every one of its `local`-capable routes together (the reconcile in\n * {@link deriveManualPlan}).\n */\nexport interface ProxyBackend {\n /** Backend package name (a route's `packageName`). */\n packageName: string\n /** The routes that resolve to this package, each with its own local/cloud capability. */\n routes: RouteCap[]\n /** True when at least one of `routes` lists `local` in its `from` capabilities \u2014 i.e. it is launchable-local. */\n localCapable: boolean\n /** True when at least one of `routes` lists `cloud` \u2014 i.e. it can stay on the cloud target. */\n cloudCapable: boolean\n /** App folder whose `api` package is {@link packageName}, or undefined when no discovered app owns it (a `local` choice then has nothing to launch \u2014 the audit flags it). */\n ownerApp?: string\n}\n\n/** A discovered app as the wizard sees it: which parts exist + which backends its frontend proxies to. */\nexport interface WizardApp {\n /** App folder name (e.g. `client`). */\n name: string\n /** Has `apps/<app>/api` (a launchable backend). */\n hasApi: boolean\n /** Has `apps/<app>/ui` (a launchable frontend). */\n hasUi: boolean\n /**\n * This app's OWN `api` package name (from `apps/<app>/api`), or undefined when it has no api. Carried\n * so the audit can build a faithful `app \u2192 apiPackage` map for EVERY discovered api app \u2014 including\n * api-only apps a frontend proxies to cross-app \u2014 mirroring the root audit (`preset-proxy-check.ts`).\n */\n apiPackage?: string\n /** Backends this app's frontend proxies to (empty for api-only apps or a frontend with no `dev.proxy`). */\n backends: ProxyBackend[]\n}\n\n/** Everything the wizard needs, gathered impurely by {@link file://./dev-wizard-run.ts}. */\nexport interface WizardModel {\n /** All discovered apps (api and/or ui), sorted by name. */\n apps: WizardApp[]\n /** `devServersPresets` names (drives the preset-or-manual step-0). */\n presets: string[]\n /** Configured `environments` (the cloud-env `select` options). */\n environments: string[]\n}\n\n/** The manual-branch answers collected from the prompts, fed to {@link deriveManualPlan}. */\nexport interface ManualSelection {\n /**\n * The target keys the user ticked directly: every selected `<app>/ui` frontend, plus any api-only\n * `<app>/api` backend (apps with an api but no ui). A full-stack backend is NOT ticked here \u2014 it is\n * launched by choosing `local` for one of its frontend's routes (recorded in {@link sources}).\n */\n targets: string[]\n /**\n * Explicit per-route choice for every TWO-OPTION route, keyed `${app}/ui ${routePath}` (e.g.\n * `client/ui /api`). Single-option routes are absent (auto-picked). Missing two-option answers fall\n * back to the route's cloud/local capability in {@link deriveManualPlan}.\n */\n sources: Record<string, ProxySource>\n /** Chosen cloud env for cloud routes (undefined when nothing resolves cloud). */\n env?: string\n /** Rebuild + restart on save. */\n watch: boolean\n /** One cmux pane per app. */\n cmux: boolean\n}\n\n/** The resolved run plan derived from a {@link ManualSelection}. */\nexport interface DerivedPlan {\n /** In-memory preset handed to the runner (`options.presetDef`). */\n presetDef: DevPreset\n /** At least one route resolves to cloud \u2192 an env must be chosen (pre-flight gate). */\n anyCloudRoute: boolean\n /** Resolved `<app>/<part>` target keys, sorted \u2014 for the echo + audit display. */\n targetKeys: string[]\n}\n\n/** App folder name from an `<app>/<part>` target key. */\nconst appOf = (key: string): string => {\n return key.split('/')[0]!\n}\n\n/** Index a model's apps by folder name for O(1) lookup. */\nconst byName = (model: WizardModel): Map<string, WizardApp> => {\n return new Map(\n model.apps.map((a) => {\n return [a.name, a]\n }),\n )\n}\n\n/** Is this route a real choice (both local and cloud reachable)? */\nconst isTwoOption = (route: RouteCap): boolean => {\n return route.localCapable && route.cloudCapable\n}\n\n/**\n * The source a single route resolves to BEFORE per-backend reconciliation:\n * - two-option: the user's recorded answer, else 'cloud' when cloud-capable else 'local';\n * - single local-only: always 'local';\n * - single cloud-only: always 'cloud'.\n */\nconst rawSource = (uiKey: string, route: RouteCap, sources: Record<string, ProxySource>): ProxySource => {\n if (isTwoOption(route)) {\n return sources[`${uiKey} ${route.path}`] ?? (route.cloudCapable ? 'cloud' : 'local')\n }\n\n return route.localCapable ? 'local' : 'cloud'\n}\n\n/**\n * Pass 1: which backend packages resolve local (any of their routes did), plus each package's owner app.\n * A package's local verdict is shared across every frontend that proxies to it \u2014 that is the reconcile.\n */\nconst collectLocalPackages = (\n uiKeys: string[],\n apps: Map<string, WizardApp>,\n sources: Record<string, ProxySource>,\n): { localPkgs: Set<string>; ownerByPkg: Map<string, string | undefined> } => {\n const localPkgs = new Set<string>()\n const ownerByPkg = new Map<string, string | undefined>()\n\n for (const key of uiKeys) {\n const app = apps.get(appOf(key))\n\n for (const backend of app?.backends ?? []) {\n ownerByPkg.set(backend.packageName, backend.ownerApp)\n for (const route of backend.routes) {\n if (rawSource(key, route, sources) === 'local') localPkgs.add(backend.packageName)\n }\n }\n }\n\n return { localPkgs, ownerByPkg }\n}\n\n/**\n * Pass 2 for ONE frontend: its per-route overrides after the per-package verdict, and whether any route\n * went cloud. A local-capable route on a local package is `local` (reconciled up); everything else `cloud`.\n */\nconst frontendOverrides = (\n app: WizardApp | undefined,\n localPkgs: ReadonlySet<string>,\n): { overrides: Record<string, ProxySource>; anyCloud: boolean } => {\n const overrides: Record<string, ProxySource> = {}\n let anyCloud = false\n\n for (const backend of app?.backends ?? []) {\n const backendLocal = localPkgs.has(backend.packageName)\n\n for (const route of backend.routes) {\n const source: ProxySource = route.localCapable && backendLocal ? 'local' : 'cloud'\n\n overrides[route.path] = source\n if (source === 'cloud') anyCloud = true\n }\n }\n\n return { overrides, anyCloud }\n}\n\n/**\n * Turn the manual {@link ManualSelection} into an in-memory {@link DevPreset}. The frontend checkbox picks\n * which UIs (and standalone api-only backends) to run; the per-route answers in {@link ManualSelection.sources}\n * decide each two-option route. Resolution is two passes so same-backend routes stay coherent:\n *\n * 1. Resolve every route of every selected frontend to its raw source; a package is LOCAL iff any of its\n * routes resolved local.\n * 2. Re-resolve each route against that per-package verdict: a local-capable route on a local package is\n * `local` (reconciled up even if it was individually answered cloud); a cloud-only route stays `cloud`.\n *\n * Launched backends are the ticked api-only parts \u222A every package that resolved local (added as\n * `${ownerApp}/api` when the owner is discoverable \u2014 a local override on an owner-less package is still\n * emitted, and the audit flags it).\n *\n * @example\n * // client frontend, its /api answered local \u2192 launches client/api + client/ui, /api resolves local:\n * deriveManualPlan(\n * { targets: ['client/ui'], sources: { 'client/ui /api': 'local' }, watch: false, cmux: false },\n * { apps: [{ name: 'client', hasApi: true, hasUi: true, backends: [{ packageName: 'client-api',\n * cloudCapable: true, localCapable: true, ownerApp: 'client',\n * routes: [{ path: '/api', localCapable: true, cloudCapable: true }] }] }],\n * presets: [], environments: ['dev'] },\n * ).targetKeys // => ['client/api', 'client/ui']\n */\nexport const deriveManualPlan = (selection: ManualSelection, model: WizardModel): DerivedPlan => {\n const apps = byName(model)\n const uiKeys = selection.targets.filter((t) => {\n return t.endsWith('/ui')\n })\n const apiOnlyKeys = selection.targets.filter((t) => {\n return t.endsWith('/api')\n })\n\n // Pass 1: a package is local iff any of its routes (across every selected frontend) resolved local.\n const { localPkgs, ownerByPkg } = collectLocalPackages(uiKeys, apps, selection.sources)\n\n // Pass 2: re-resolve each route against the per-package verdict + build per-frontend overrides.\n const presetApps: Record<string, { proxy?: Record<string, ProxySource> }> = {}\n let anyCloud = false\n\n for (const key of uiKeys) {\n const { overrides, anyCloud: cloud } = frontendOverrides(apps.get(appOf(key)), localPkgs)\n\n if (cloud) anyCloud = true\n presetApps[key] = Object.keys(overrides).length > 0 ? { proxy: overrides } : {}\n }\n\n // Launch: the ticked api-only backends, plus every package that resolved local (via its owner app).\n for (const key of apiOnlyKeys) {\n presetApps[key] ??= {}\n }\n for (const pkg of localPkgs) {\n const owner = ownerByPkg.get(pkg)\n\n if (owner != null) presetApps[`${owner}/api`] ??= {}\n }\n\n return {\n presetDef: { apps: presetApps, cmux: selection.cmux },\n anyCloudRoute: anyCloud,\n targetKeys: Object.keys(presetApps).sort(),\n }\n}\n\n/** The equivalent non-interactive command for a derived plan. */\nexport interface EquivalentCommand {\n /** The `--target=<app>/<part>,\u2026`-form flag string (no leading `infra-kit dev`). */\n flags: string\n}\n\n/**\n * Build the equivalent `infra-kit dev --target=\u2026 [--watch] [--cmux]` flag string for a derived plan.\n * Always exact: `--target` names packages at `<app>/<part>` granularity \u2014 the same grammar the plan's\n * `targetKeys` already use \u2014 so every wizard selection, whole-app or part-level, round-trips into a\n * pasteable command. (`--app` is deliberately NOT used here: its app-name granularity over-launches a\n * part-level pick \u2014 `--app=client` starts `client/api` even when only `client/ui` was ticked.)\n */\nexport const equivalentCommand = (plan: DerivedPlan, selection: ManualSelection): EquivalentCommand => {\n const parts = [`--target=${plan.targetKeys.join(',')}`]\n\n if (selection.watch) parts.push('--watch')\n if (selection.cmux) parts.push('--cmux')\n\n return { flags: parts.join(' ') }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["inquirerCheckbox", "inquirerConfirm", "inquirerSelect", "Separator", "loadDev", "path", "process", "appOf", "key", "byName", "model", "a", "isTwoOption", "route", "rawSource", "uiKey", "sources", "collectLocalPackages", "uiKeys", "apps", "localPkgs", "ownerByPkg", "app", "backend", "frontendOverrides", "overrides", "anyCloud", "backendLocal", "source", "deriveManualPlan", "selection", "t", "apiOnlyKeys", "presetApps", "cloud", "pkg", "owner", "equivalentCommand", "plan", "parts", "promptContext", "process", "defaultPrompts", "cfg", "withEscape", "context", "inquirerSelect", "inquirerCheckbox", "inquirerConfirm", "groupBackends", "routes", "ownerByPkg", "byPkg", "route", "def", "cap", "existing", "loadBackends", "uiDir", "name", "loadDev", "error", "logger", "gatherWizardModel", "root", "apiApps", "discoverApiApps", "uiApps", "discoverUiApps", "config", "getInfraKitConfig", "a", "apiPkgByName", "uiNames", "apiNames", "allNames", "apps", "hasUi", "path", "store", "readTokenStore", "buildPartChoices", "model", "choices", "app", "paths", "b", "r", "description", "buildAuditContext", "discovered", "apiPkgByApp", "routeToPkg", "auditManualPlan", "presetDef", "validatePresetProxy", "i", "WIZARD_CMUX_VAR", "asksCmux", "promptRouteSources", "prompts", "uiKeys", "sources", "key", "backend", "answer", "findRouteCap", "routePath", "recapSources", "plan", "lines", "source", "suffix", "line", "runManualBranch", "selectedTargets", "t", "watch", "cmux", "selection", "deriveManualPlan", "e", "INFRA_KIT_ENV_VAR", "issues", "issue", "echoManual", "k", "commandEcho", "eq", "equivalentCommand", "runPresetBranch", "preset", "MANUAL_CHOICE", "runWizardFlow", "choice", "p", "Separator", "runDevWizard", "findMonorepoRoot"]
7
7
  }
package/dist/mcp.js CHANGED
@@ -1,2 +1,2 @@
1
- import{a as d,ba as h,d as p,ea as S,r as y}from"./chunk-KATIE633.js";import"./chunk-MKMT6KQT.js";import{a as v}from"./chunk-VFU2USJG.js";import"./chunk-LKKKDB6V.js";import{i as x}from"./chunk-OKO3DKIM.js";import{H as c,I as l,J as a,c as u,da as g}from"./chunk-XR6HR4LL.js";import"./chunk-CHETBZ6M.js";import"./chunk-X46DP23W.js";import{StdioServerTransport as P}from"@modelcontextprotocol/server/stdio";import j from"node:process";import n from"node:process";var C=e=>{n.on("SIGINT",()=>{e.info({msg:"Received SIGINT. Shutting down..."}),n.exit(0)}),n.on("SIGTERM",()=>{e.info({msg:"Received SIGTERM. Shutting down..."}),n.exit(0)}),n.on("uncaughtException",r=>{e.fatal({err:r,msg:"Uncaught Exception"}),e.error(`Uncaught Exception! Check ${c}. Shutting down...`),e.flush(),n.exit(1)}),n.on("unhandledRejection",(r,o)=>{e.fatal({reason:r,promise:o,msg:"Unhandled Rejection"}),e.error(`Unhandled Rejection! Check ${c}. Shutting down...`),e.flush(),n.exit(1)})};import{McpServer as O}from"@modelcontextprotocol/server";var T=async e=>{};var b="infra-kit://config",M="infra-kit://dev-context",D={loadConfig:g,readDevContext:()=>p()},m=(e,r)=>({contents:[{uri:e,mimeType:"application/json",text:JSON.stringify(r,null,2)}]}),R=async(e,r=D)=>{e.registerResource("infra-kit-config",b,{title:"infra-kit config",description:"The merged infra-kit.json configuration (all override layers applied). Read-only.",mimeType:"application/json"},async o=>{try{return m(o.toString(),await r.loadConfig())}catch(t){return m(o.toString(),{error:t instanceof Error?t.message:String(t)})}}),e.registerResource("infra-kit-dev-context",M,{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"},o=>m(o.toString(),r.readDevContext()))};import{z as w}from"zod";var I=e=>typeof e=="object"&&e!==null&&e.confirm===!0,H=(e,r)=>{let o=typeof r=="object"&&r!==null?Object.fromEntries(Object.entries(r).filter(([s])=>s!=="confirm")):{},t={status:"confirmation_required",tool:e,resolvedArgs:o,message:`${e} mutates external state and is gated. It was NOT executed. Re-call ${e} with the same arguments plus "confirm": true to execute.`};return{content:d(JSON.stringify(t,null,2)),structuredContent:t,isError:!0}},f=({toolName:e,handler:r,requiresHumanConfirm:o})=>async t=>{a.info({msg:`Tool execution started: ${e}`,params:t});try{if(await y(),x.reset(),o===!0&&!I(t))return a.info({msg:`Tool execution gated (awaiting confirm): ${e}`}),H(e,t);let s=await r({...t,confirmedCommand:!0});return a.info({msg:`Tool execution successful: ${e}`}),s}catch(s){throw a.error({err:s,params:t,msg:`Tool execution failed: ${e}`}),s}};var E=async e=>{for(let r of h())e.registerTool(r.name,{description:r.description,inputSchema:w.object(r.inputSchema),outputSchema:w.object(r.outputSchema)},f({toolName:r.name,handler:r.handler,requiresHumanConfirm:r.requiresHumanConfirm}))};async function k(){u.enabled=!0;let e=new O({name:"infra-kit",version:v.version},{capabilities:{resources:{listChanged:!0},tools:{},prompts:{}}});return await T(e),await R(e),await E(e),e}S();var i=l(),U=async()=>{let e;try{e=await k(),i.info("MCP Server instance created")}catch(r){i.error({err:r,msg:"Failed to create MCP server"}),i.error("Fatal error during server creation."),j.exit(1)}try{let r=new P;await e.connect(r),i.info({msg:"Server connected to transport. Ready."})}catch(r){i.error({err:r,msg:"Failed to initialize server"}),i.error("Fatal error during server transport init."),j.exit(1)}};C(i);U();
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")});
2
2
  //# sourceMappingURL=mcp.js.map