lensmcp 1.25.21 → 1.25.22

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.
@@ -58,7 +58,7 @@ export interface InstanceResult {
58
58
  exitCode: number;
59
59
  instance?: StartedInstance;
60
60
  }
61
- export declare const INSTANCE_USAGE = "Usage:\n lensmcp instance create <name> [--branch <branch> | --detach] [--from <commit>] [--frozen] [--no-prepare] [--share-stores] [--json] [--cwd <dir>]\n Make a git worktree of this checkout and start it as an instance (what a coding agent's\n MCP worktree_create runs): on <branch> (default lensmcp/<name>, made from --from, default\n HEAD), in nx.json \u2192 lensmcp.instances.worktrees (default .lensmcp/worktrees/<name>).\n lensmcp instance start [--name <name>] [--apex <apex>] [--frozen | --live] [--prepare | --no-prepare] [--share-stores] [--cwd <dir>]\n Serve this checkout beside the main one, under hosts named after it, with its own\n data (nx.json \u2192 lensmcp.instances). In a git worktree the name defaults to the\n worktree folder's. --frozen: serve the commit as it is \u2014 each service compiles\n once, no watcher, no type checker (--live undoes it; the default is live).\n --share-stores: start a project that declares no resources.\n lensmcp instance list [--json]\n lensmcp instance status [<name>] [--json] [--cwd <dir>]\n lensmcp instance exec [<name>] [--cwd <dir>] -- <command> [args\u2026]\n Run a command with the instance's variables (LENSMCP_HOST_<PROJECT>, \u2026).\n lensmcp instance stop [<name>] [--cwd <dir>]\n Unregister it from the gateway (its services stop); `start` brings it back.\n lensmcp instance rm [<name>] [--keep-data] [--worktree [--delete-branch] [--force]] [--json] [--cwd <dir>]\n Stop it, drop the data the lens made for it, and clear its lens state. --worktree also\n removes the git worktree \u2014 refused while it has uncommitted changes, unless --force;\n --delete-branch deletes a branch the lens made, once merged (--force: even unmerged).\n lensmcp instance prune [--dry-run]\n Remove every instance whose folder is gone, data included.";
61
+ export declare const INSTANCE_USAGE = "Usage:\n lensmcp instance create <name> [--branch <branch> | --detach] [--from <commit>] [--frozen] [--no-prepare] [--share-stores] [--json] [--cwd <dir>]\n Make a git worktree of this checkout and start it as an instance (what a coding agent's\n MCP worktree_create runs): on <branch> (default lensmcp/<name>, made from --from, default\n HEAD), in nx.json \u2192 lensmcp.instances.worktrees (default .lensmcp/worktrees/<name>).\n lensmcp instance start [--name <name>] [--apex <apex>] [--frozen | --live] [--prepare | --no-prepare] [--share-stores] [--cwd <dir>]\n Serve this checkout beside the main one, under hosts named after it, with its own\n data (nx.json \u2192 lensmcp.instances). In a git worktree the name defaults to the\n worktree folder's. --frozen: serve the commit as it is \u2014 each service compiles\n once, no watcher, no type checker (--live undoes it; the default is live).\n --share-stores: start a project that declares no resources.\n Preparation must succeed before services start; failed or unfinished preparation retries\n on the next start. --prepare runs it again; --no-prepare explicitly skips it for this start.\n lensmcp instance list [--json]\n lensmcp instance status [<name>] [--json] [--cwd <dir>]\n lensmcp instance exec [<name>] [--cwd <dir>] -- <command> [args\u2026]\n Run a command with the instance's variables (LENSMCP_HOST_<PROJECT>, \u2026).\n lensmcp instance stop [<name>] [--cwd <dir>]\n Unregister it from the gateway (its services stop); `start` brings it back.\n lensmcp instance rm [<name>] [--keep-data] [--worktree [--delete-branch] [--force]] [--json] [--cwd <dir>]\n Stop it, drop the data the lens made for it, and clear its lens state. --worktree also\n removes the git worktree \u2014 refused while it has uncommitted changes, unless --force;\n --delete-branch deletes a branch the lens made, once merged (--force: even unmerged).\n lensmcp instance prune [--dry-run]\n Remove every instance whose folder is gone, data included.";
62
62
  /** A linked worktree's facts, from git. */
63
63
  export interface WorktreeInfo {
64
64
  /** The checkout's top-level directory. */
@@ -1,4 +1,4 @@
1
- "use strict";var De=Object.defineProperty;var w=(c,l)=>De(c,"name",{value:l,configurable:!0});var Be=Object.defineProperty,m=w((c,l)=>Be(c,"name",{value:l,configurable:!0}),"h");import{spawn as ze,spawnSync as qe}from"node:child_process";import{existsSync as N,mkdirSync as Ce,readFileSync as Fe,realpathSync as Ge,rmSync as te,writeFileSync as de}from"node:fs";import{basename as je,dirname as Je,join as R,relative as Se,resolve as X}from"node:path";import{registrationState as We,refusalMessage as Oe}from"./gateway-registration.js";import{readRegistry as ne,redactUrl as He,registryPath as Ue,withRegistry as se}from"./instance-registry.js";import{bootstrapCheckout as Xe,defaultRuntime as Ye,dropAll as Qe,provisionAll as Ve,readInstancesDecl as Ee,resourceEnvFile as Ze,writeResourceEnv as et}from"./instance-resources.js";import{addWorktree as tt,branchNameProblem as nt,claimCreate as st,currentBranch as rt,deleteBranch as ot,ensureIgnored as at,gitExec as it,progressFile as Y,readProgress as Ne,removeWorktree as ct,resolveCommit as dt,settledProgress as ut,uncommittedChanges as Pe,worktreesHome as lt,writeProgress as Re}from"./instance-worktree.js";import{derivePorts as pt,INSTANCE_NAME_PATTERN as Ke,instanceSockNs as mt,readLensConfig as Q,slugify as ft}from"./workspace-scope.js";export const INSTANCE_USAGE=`Usage:
1
+ "use strict";var Je=Object.defineProperty;var g=(i,c)=>Je(i,"name",{value:c,configurable:!0});var Fe=Object.defineProperty,f=g((i,c)=>Fe(i,"name",{value:c,configurable:!0}),"p");import{spawn as Ge,spawnSync as He}from"node:child_process";import{createHash as We}from"node:crypto";import{existsSync as N,mkdirSync as Pe,readFileSync as Re,realpathSync as Ue,rmSync as Y,writeFileSync as ie}from"node:fs";import{basename as Ke,dirname as Xe,join as S,relative as Ae,resolve as ee}from"node:path";import{registrationState as Ve,refusalMessage as ce}from"./gateway-registration.js";import{readRegistry as de,redactUrl as Ye,registryPath as Qe,withRegistry as ue}from"./instance-registry.js";import{bootstrapCheckout as Ze,defaultRuntime as et,dropAll as tt,provisionAll as nt,readInstancesDecl as Te,resourceEnvFile as st,writeResourceEnv as rt}from"./instance-resources.js";import{addWorktree as ot,branchNameProblem as at,claimCreate as it,currentBranch as ct,deleteBranch as dt,ensureIgnored as ut,gitExec as lt,progressFile as te,readProgress as _e,removeWorktree as pt,resolveCommit as ft,settledProgress as mt,uncommittedChanges as Me,worktreesHome as ht,writeProgress as Le}from"./instance-worktree.js";import{derivePorts as yt,INSTANCE_NAME_PATTERN as Be,instanceSockNs as kt,readLensConfig as ne,slugify as gt}from"./workspace-scope.js";export const INSTANCE_USAGE=`Usage:
2
2
  lensmcp instance create <name> [--branch <branch> | --detach] [--from <commit>] [--frozen] [--no-prepare] [--share-stores] [--json] [--cwd <dir>]
3
3
  Make a git worktree of this checkout and start it as an instance (what a coding agent's
4
4
  MCP worktree_create runs): on <branch> (default lensmcp/<name>, made from --from, default
@@ -9,6 +9,8 @@
9
9
  worktree folder's. --frozen: serve the commit as it is \u2014 each service compiles
10
10
  once, no watcher, no type checker (--live undoes it; the default is live).
11
11
  --share-stores: start a project that declares no resources.
12
+ Preparation must succeed before services start; failed or unfinished preparation retries
13
+ on the next start. --prepare runs it again; --no-prepare explicitly skips it for this start.
12
14
  lensmcp instance list [--json]
13
15
  lensmcp instance status [<name>] [--json] [--cwd <dir>]
14
16
  lensmcp instance exec [<name>] [--cwd <dir>] -- <command> [args\u2026]
@@ -20,11 +22,12 @@
20
22
  removes the git worktree \u2014 refused while it has uncommitted changes, unless --force;
21
23
  --delete-branch deletes a branch the lens made, once merged (--force: even unmerged).
22
24
  lensmcp instance prune [--dry-run]
23
- Remove every instance whose folder is gone, data included.`;export function worktreeInfo(c,l){const o=l(c,["rev-parse","--show-toplevel"]);if(!o)return;const r=l(c,["rev-parse","--path-format=absolute","--git-dir"]),e=l(c,["rev-parse","--path-format=absolute","--git-common-dir"]),O=!!r&&!!e&&X(r)!==X(e),h=O&&e&&je(e)===".git"?Je(X(e)):void 0;return{root:o,linked:O,...h?{mainRoot:h}:{}}}w(worktreeInfo,"worktreeInfo"),m(worktreeInfo,"worktreeInfo");export function nameFromFolder(c){return ft(je(c)).slice(0,20).replace(/-+$/,"")}w(nameFromFolder,"nameFromFolder"),m(nameFromFolder,"nameFromFolder");export function instanceConfig(c,l){const o=`${l.baseKey}-${l.name}`;return{...c,key:o,ports:pt(o),dashboardBasePath:"/"+o,sockNs:mt(o),instance:l}}w(instanceConfig,"instanceConfig"),m(instanceConfig,"instanceConfig");export function baseKeyOf(c,l){return c.instance?.baseKey?c.instance.baseKey:l&&N(R(l,".lensmcp","config.json"))?Q(l).key:c.key}w(baseKeyOf,"baseKeyOf"),m(baseKeyOf,"baseKeyOf");function G(c){if(!c||c.status!==200||!c.json||typeof c.json!="object")return[];const l=c.json.workspaces;return Array.isArray(l)?l:[]}w(G,"J"),m(G,"listed");export function resolveInstance(c,l,o){const r=c.filter(e=>typeof e.instance=="string");if(l){const e=r.find(P=>P.key===l);if(e)return e;const O=r.filter(P=>P.instance===l),h=O.find(P=>P.baseKey===(o.instance?.baseKey??o.key));if(h)return h;const[W,...re]=O;return W&&re.length===0?W:O.length>1?`'${l}' is an instance of ${O.map(P=>`'${P.baseKey}'`).join(" and ")} \u2014 name it by key: ${O.map(P=>P.key).join(", ")}.`:`no instance '${l}' is registered with the gateway${r.length?` (running: ${r.map(P=>P.key).join(", ")})`:""}.`}return o.instance?r.find(e=>e.key===o.key)??`this checkout's instance '${o.key}' is not registered with the gateway \u2014 \`lensmcp instance start\` here.`:"this checkout is not an instance \u2014 name one, or run `lensmcp instance list`."}w(resolveInstance,"resolveInstance"),m(resolveInstance,"resolveInstance");function B(c,l){for(const[o,r]of c.entries()){if(r===l)return c[o+1];if(r.startsWith(`${l}=`))return r.slice(l.length+1)}}w(B,"z"),m(B,"flag");function Ae(c,l,o){const r=[];let e=!1;for(const O of c){if(e){e=!1;continue}if(l.includes(O)){e=!0;continue}l.some(h=>O.startsWith(`${h}=`))||o.includes(O)||r.push(O)}return r}w(Ae,"ft"),m(Ae,"positionals");export async function runInstance(c,l,o,r,e){const O=c.indexOf("--"),h=O>=0?c.slice(0,O):c,W=O>=0?c.slice(O+1):[],re=["--cwd","--name","--apex","--branch","--from","--base"],[P,...H]=Ae(h,re,["--json","--prepare","--no-prepare","--share-stores","--keep-data","--dry-run","--frozen","--live","--worktree","--delete-branch","--force","--detach"]),_={...process.env,...o.env??{}},q=o.registryFile??Ue(_),pe=m(n=>(o.resources??Ye)(n.filter(t=>!!t&&N(t))),"runtimeFor"),F=h.includes("--json"),K=X(B(h,"--cwd")??l),M=o.gitExec??it,oe=m((n,t)=>o.onStep?.(n,t),"step");if(O>=0&&(P??"").toLowerCase()!=="exec")return e(`\`--\` belongs to \`lensmcp instance exec\` only.
24
- ${INSTANCE_USAGE}`),{exitCode:2};switch((P??"").toLowerCase()){case"create":case"new":return fe(H[0]??B(h,"--name"));case"start":return me();case"list":case"ls":return ke();case"status":return ge(H[0]);case"exec":return we(H[0]);case"stop":return $e(H[0]);case"rm":case"remove":return be(H[0]);case"prune":return ve();default:return(P?e:r)(P?`Unknown instance command: ${P}
25
- ${INSTANCE_USAGE}`:INSTANCE_USAGE),{exitCode:P?2:0}}async function me(){const n=K,t=worktreeInfo(K,o.git),i=t?Se(t.root,n):"",d=Q(n),s=B(h,"--name")??d.instance?.name??(t?.linked?nameFromFolder(t.root):void 0);if(!s)return e(`${n} is not a linked git worktree, so its instance has no name \u2014 pass \`--name <name>\`.`),{exitCode:2};if(!Ke.test(s))return e(`'${s}' is not an instance name: one DNS label of at most 20 characters \u2014 lowercase letters, digits and inner hyphens.`),{exitCode:2};const p=B(h,"--apex")??d.instance?.apex,a=(t?.mainRoot?R(t.mainRoot,i):void 0)??d.instance?.mainRoot,y=h.includes("--frozen")?!0:h.includes("--live")?!1:d.instance?.frozen===!0,j={name:s,baseKey:baseKeyOf(d,a),...p?{apex:p.toLowerCase()}:{},...a?{mainRoot:a}:{},...y?{frozen:!0}:{}},k=instanceConfig(d,j),u=o.daemonRequest("GET","/list");if(u===null)return e("No gateway daemon is running. An instance is served by the main checkout's gateway \u2014"),e(`start it there first (\`lensmcp gateway start\`${a?` in ${a}`:""}), then run this again.`),{exitCode:1};const x=G(u),g=x.find(v=>v.key!==k.key&&v.root!==void 0&&le(v.root,n));if(g?.instance)return e(`${n} is already served as instance '${g.instance}' (${g.key}). \`lensmcp instance stop\` first to rename it.`),{exitCode:1};if(g)return e(`${n} is already served by the gateway as '${g.key}'. An instance is a SECOND checkout \u2014 run this in a`),e("git worktree of it (`git worktree add <dir>`), or `lensmcp gateway stop` here first."),{exitCode:1};const E=We(x,k.key,n);if(E.kind==="other-checkout")return e(`'${k.key}' is already served from ${E.root}. Give this one another name: \`lensmcp instance start --name <name>\`.`),{exitCode:1};const $=a&&N(R(a,"nx.json"))?a:n,{decl:T,errors:f}=Ee($);if(f.length>0){e(`${R($,"nx.json")} \u2192 lensmcp.instances is not usable:`);for(const v of f)e(` ${v}`);return{exitCode:2}}if(!T&&E.kind==="free"&&!h.includes("--share-stores"))return e(`${R($,"nx.json")} declares no \`lensmcp.instances\`, so the instance would run on the main checkout's database and`),e("queues \u2014 whatever its .env points at: its workers would take main's jobs, its migrations would run on main's data."),e("Declare its resources (`lensmcp.instances.resources` \u2014 see `lensmcp instance --help`), or pass --share-stores."),{exitCode:1};let b;if(T){if(oe("provisioning",Object.keys(T.resources).length?`its data: ${Object.keys(T.resources).join(", ")}`:"bootstrapping the checkout"),a){const S=Xe(n,a,T.bootstrap);S.copied.length&&r(` copied \u2190 ${S.copied.join(", ")} (from ${a})`),S.cloned.length&&r(` cloned \u2190 ${S.cloned.join(", ")} (copy-on-write)`);for(const z of S.notes)r(` note: ${z}`)}const v={key:k.key,name:s,baseKey:j.baseKey,root:n,...a?{mainRoot:a}:{}};try{b=await se(q,S=>Ve(T,v,S,pe([n,a]),_))}catch(S){return e(`could not give instance '${k.key}' its data: ${S.message}`),e("Whatever was already created is on record \u2014 `lensmcp instance rm` drops it."),{exitCode:1}}et(n,b)}if(Te(n,k),oe("registering","with the gateway"),E.kind==="free"){const v=o.daemonRequest("POST","/register",o.registrationBody(n,k.key));if(!v||v.status!==200)return e(`The gateway refused instance '${k.key}': ${Oe(v)??(v?JSON.stringify(v.json):"no response from the control socket")}`),{exitCode:1}}_e(n,k.key);const C=Z(k.key),I={};for(const[v,S]of Object.entries(C?.env??{}))v.startsWith("LENSMCP_HOST_")&&(I[v.slice(13).toLowerCase()]=`https://${S}`);const J={name:s,key:k.key,baseKey:j.baseKey,root:n,...a?{mainRoot:a}:{},frozen:y,state:E.kind==="mine"?"already-running":"started",...C?.env.LENSMCP_APEX?{apex:C.env.LENSMCP_APEX}:{},hosts:I,dashboard:`https://lensmcp.local${k.dashboardBasePath}/`,resources:Object.keys(T?.resources??{}),created:b?.created??[],prepared:!1};if(r(`${E.kind==="mine"?"instance already running":"instance started"}: '${s}' of '${j.baseKey}' \u2014 key ${k.key}${y?" (frozen: compiled once, no watcher)":""}`),E.kind==="mine"&&d.instance?.frozen===!0!==y&&r(` note: it was already running ${d.instance?.frozen?"frozen":"live"} \u2014 \`lensmcp instance stop\` then \`start\` applies the change.`),C){r(` apex \u2192 ${C.env.LENSMCP_APEX??"?"}`);for(const[v,S]of Object.entries(C.env).filter(([z])=>z.startsWith("LENSMCP_HOST_")).sort())r(` ${v.slice(13).toLowerCase().padEnd(9)} \u2192 https://${S}`)}else r(" (the daemon did not report its hosts \u2014 it may predate instances, and serves this checkout under main's hosts)");if(r(` dashboard \u2192 https://lensmcp.local${k.dashboardBasePath}/`),b){for(const[v,S]of Object.entries(b.resolved))r(` ${v.padEnd(9)} = ${He(S)}`);b.created.length&&r(` created : ${b.created.join(", ")}`),E.kind==="mine"&&b.created.length&&r(" note: it was already running \u2014 `lensmcp instance stop` then `start` hands the new resources to its services.")}const D=T?.prepare;if(D&&!h.includes("--no-prepare")&&(h.includes("--prepare")||(b?.created.length??0)>0)){r(` prepare \u2192 ${D}`),oe("preparing",D);const v=(o.runPrepare??ue)(D,{cwd:n,env:{..._,...b?.resolved??{},...C?.env??{}}});if(v!==0)return e(`\`${D}\` exited ${v??"on a signal"}. The instance is up, but its data may not be ready \u2014 fix it and run \`lensmcp instance start --prepare\`.`),{exitCode:1,instance:J};J.prepared=!0}return r(" its always-warm services start now, one at a time while the machine has memory to spare (LENSMCP_WARM_MIN_FREE_MB);"),r(" the rest on their first request, and each stops after 30 min unused. `lensmcp instance exec -- <cmd>` runs a command with its variables;"),r(" `lensmcp instance stop` unregisters it, `lensmcp instance rm` also drops its data."),he(k.key,J.prepared?"started again, prepared":"started again"),{exitCode:0,instance:J}}w(me,"xe"),m(me,"start");async function fe(n){const t=F?e:r;if(!n)return e(`Name the instance: \`lensmcp instance create <name>\`.
26
- ${INSTANCE_USAGE}`),{exitCode:2};if(!Ke.test(n))return e(`'${n}' is not an instance name: one DNS label of at most 20 characters \u2014 lowercase letters, digits and inner hyphens.`),{exitCode:2};const i=worktreeInfo(K,o.git);if(!i)return e(`${K} is not in a git checkout \u2014 \`lensmcp instance create\` makes a git worktree of one.`),{exitCode:2};const d=i.linked?i.mainRoot:i.root;if(!d)return e(`${i.root} is a worktree whose main checkout git does not name (a bare repository?) \u2014 make the worktree yourself and \`lensmcp instance start\` in it.`),{exitCode:2};const s=Se(i.root,K),p=R(d,s),a=baseKeyOf(Q(p),p),y=`${a}-${n}`,j=st(Y(y,_));if("heldBy"in j)return e(`'${y}' is being created right now by another lensmcp (pid ${j.heldBy}). Wait for it \u2014 \`lensmcp instance list\` shows it \u2014 or pick another name.`),{exitCode:1};try{const{decl:k,errors:u}=Ee(N(R(p,"nx.json"))?p:K);if(u.length>0){e(`${R(p,"nx.json")} \u2192 lensmcp.instances is not usable:`);for(const A of u)e(` ${A}`);return{exitCode:2}}if(!k&&!h.includes("--share-stores"))return e(`${R(p,"nx.json")} declares no \`lensmcp.instances\`, so a new instance would run on the main checkout's database and`),e("queues. Declare its resources (`lensmcp.instances.resources`), or pass --share-stores."),{exitCode:1};const x=lt(p,k?.worktrees,_),g=R(x,n),E=R(g,s);if(N(g))return e(`${g} already exists. \`lensmcp instance start\` in it serves it; or pick another name.`),{exitCode:1};const $=ne(q).instances[y];if($)return e(`'${y}' is already on record for ${$.root}${N($.root)?"":" (its folder is gone \u2014 `lensmcp instance prune` clears it)"}. Pick another name.`),{exitCode:1};const T=o.daemonRequest("GET","/list");if(T===null)return e("No gateway daemon is running. An instance is served by the main checkout's gateway \u2014"),e(`start it there first (\`lensmcp gateway start\` in ${p}), then run this again.`),{exitCode:1};if(G(T).some(A=>A.key===y))return e(`'${y}' is already served by the gateway. Pick another name.`),{exitCode:1};const f=h.includes("--detach")?void 0:B(h,"--branch")??`lensmcp/${n}`;if(f){const A=nt(d,f,M);if(A)return e(A),{exitCode:2}}const b=B(h,"--from");if(f&&b&&ye(d,["show-ref","--verify","--quiet",`refs/heads/${f}`]))return e(`Branch '${f}' exists already, so it is checked out as it is \u2014 --from applies to a new branch. Drop --from, or name a new --branch.`),{exitCode:2};const C=dt(K,b??"HEAD",M);if("error"in C)return e(C.error),{exitCode:2};const I=at(d,x,M);I&&t(` ignored : ${I} (added to .git/info/exclude \u2014 the worktrees stay out of main's git status)`);const J=Y(y,_),D=new Date().toISOString();let v={key:y,name:n,baseKey:a,root:E,state:"creating",detail:`git worktree add ${g}`,pid:process.pid,startedAt:D,updatedAt:D,..._.LENSMCP_CREATE_LOG?{log:_.LENSMCP_CREATE_LOG}:{}};const S=m(A=>{v={...v,...A,updatedAt:new Date().toISOString()};try{Re(J,v)}catch{}},"record");S({});const z=tt(d,g,{...f?{branch:f}:{},from:C.sha},M);if(!z.ok)return S({state:"failed",detail:z.error}),e(`git could not make the worktree: ${z.error}`),{exitCode:1};const ce={path:g,...f?{branch:f}:{},createdBranch:z.createdBranch,from:C.sha,createdAt:D};S({worktree:ce,detail:"worktree made"}),t(`worktree : ${g} \u2014 ${f?`branch ${f}${z.createdBranch?" (new)":""}`:"detached"} at ${C.sha.slice(0,12)}`);const Ie={key:y,name:n,baseKey:a,root:E,...p!==E?{mainRoot:p}:{},createdAt:D,resources:{},worktree:ce};await se(q,A=>{A.instances[y]=Ie});const Le=["--frozen","--live","--prepare","--no-prepare","--share-stores"].filter(A=>h.includes(A)),xe=B(h,"--apex"),Me=["start","--name",n,...xe?["--apex",xe]:[],...Le],U=await runInstance(Me,E,{...o,...F&&!o.runPrepare?{runPrepare:m((A,ee)=>ue(A,ee,!0),"runPrepare")}:{},onStep:m((A,ee)=>S({state:A,...ee?{detail:ee}:{}}),"onStep")},t,e),L=U.instance?{...U.instance,worktree:ce}:void 0;return U.exitCode!==0?(S({state:"failed",detail:`${U.instance?"started, but `prepare` failed":"could not start"} \u2014 \`lensmcp instance rm ${n} --worktree\` takes it away`,...L?{result:L}:{}}),e(`The worktree is at ${g}. \`lensmcp instance start --prepare\` there retries; \`lensmcp instance rm ${n} --worktree\` takes it all away.`),F&&L&&r(JSON.stringify(L,null,2)),{exitCode:U.exitCode,...L?{instance:L}:{}}):(S({state:"ready",detail:"ready",...L?{result:L}:{}}),F&&r(JSON.stringify(L,null,2)),{exitCode:0,...L?{instance:L}:{}})}finally{j.release()}}w(fe,"Ce"),m(fe,"create");function he(n,t){const i=Y(n,_),d=Ne(i);if(!(!d||d.state==="ready"||d.pid===process.pid))try{Re(i,{...d,state:"ready",detail:t,updatedAt:new Date().toISOString()})}catch{}}w(he,"je"),m(he,"settleCreate");function ye(n,t){return M(n,t).ok}w(ye,"Se"),m(ye,"gitExecOk");function ke(){const n=o.daemonRequest("GET","/list"),t=B(h,"--base"),i=m(a=>!t||a===t,"ofBase"),d=G(n).filter(a=>typeof a.instance=="string"&&i(a.baseKey)),s=ne(q),p=Object.values(s.instances).filter(a=>!d.some(y=>y.key===a.key)&&i(a.baseKey));if(F){const a=m(u=>{if(!u||!N(u))return{};const x=Pe(u,M),g=rt(u,M);return{...g?{branch:g}:{},...x?{changes:x.length}:{}}},"gitFacts"),y=m(u=>{const x=Ne(Y(u,_));if(!x||x.state==="ready")return{};const g=ut(x);return{progress:{state:g.state,...g.detail?{detail:g.detail}:{},updatedAt:g.updatedAt,...g.log?{log:g.log}:{}}}},"unfinished"),j=m(u=>{const x={};for(const[g,E]of Object.entries(Z(u)?.env??{}))g.startsWith("LENSMCP_HOST_")&&(x[g.slice(13).toLowerCase()]=`https://${E}`);return Object.keys(x).length?{hosts:x}:{}},"hostsOf"),k=d.map(u=>({...u,...a(u.root),...j(u.key),...s.instances[u.key]?.worktree?{worktree:s.instances[u.key]?.worktree}:{},...y(u.key)}));return r(JSON.stringify({checkout:{root:K,...a(K)},instances:k,stopped:p.map(u=>({key:u.key,name:u.name,baseKey:u.baseKey,root:u.root,resources:Object.keys(u.resources),folderGone:!N(u.root),...a(u.root),...u.worktree?{worktree:u.worktree}:{},...y(u.key)}))},null,2)),{exitCode:0}}if(n===null&&r("(no gateway daemon is running)"),d.length===0&&p.length===0)return r("No instances. `lensmcp instance start` in a worktree serves one."),{exitCode:0};for(const a of d){const y=Object.keys(s.instances[a.key]?.resources??{});r(`${a.instance} (${a.key}, instance of ${a.baseKey}) ${a.apex??""}${a.frozen?" frozen":""} ${a.services??0} service(s)${y.length?` data: ${y.join(", ")}`:""} ${a.root??""}`)}for(const a of p){const y=!N(a.root);r(`${a.name} (${a.key}, instance of ${a.baseKey}) ${y?"FOLDER GONE \u2014 `lensmcp instance prune`":"stopped"} data: ${Object.keys(a.resources).join(", ")||"none"} ${a.root}`)}return{exitCode:0}}w(ke,"Oe"),m(ke,"list");function V(n){const t=o.daemonRequest("GET","/list");if(t===null){e("No gateway daemon is running.");return}const i=resolveInstance(G(t),n,Q(K));if(typeof i=="string"){e(i);return}return i}w(V,"re"),m(V,"target");function ge(n){const t=V(n);if(!t)return{exitCode:1};const i=o.daemonRequest("GET","/status"),d=i?.status===200&&i.json&&typeof i.json=="object"?(i.json.services??[]).filter(s=>s.wsKey===t.key):[];if(F)return r(JSON.stringify({instance:t,services:d},null,2)),{exitCode:0};r(`${t.instance} (${t.key}, instance of ${t.baseKey}) \u2014 ${t.apex??"?"} ${t.root??""}`),d.length===0&&r(" no pod services");for(const s of d)r(` ${s.project.padEnd(24)} ${s.state.padEnd(9)} ${s.host?`https://${s.host}`:""}`);return{exitCode:0}}w(ge,"Ee"),m(ge,"status");async function we(n){if(W.length===0)return e("Nothing to run: `lensmcp instance exec [<name>] -- <command> [args\u2026]`."),{exitCode:2};const t=V(n);if(!t)return{exitCode:1};const i=Z(t.key);if(!i)return e(`The gateway did not give the variables of '${t.key}'.`),{exitCode:1};const[d,...s]=W;return new Promise(p=>{const a=ze(d,s,{cwd:t.root??K,stdio:"inherit",env:{...process.env,...o.env??{},...i.env}});a.on("error",y=>{e(`could not run ${d}: ${y.message}`),p({exitCode:127})}),a.on("exit",(y,j)=>p({exitCode:y??(j?128:1)}))})}w(we,"Ne"),m(we,"exec");function $e(n){const t=V(n);if(!t)return{exitCode:1};const i=o.daemonRequest("POST","/unregister",{wsKey:t.key,...t.root?{root:t.root}:{}});return!i||i.status!==200?(e(`could not stop '${t.key}': ${Oe(i)??(i?JSON.stringify(i.json):"no response from the control socket")}`),{exitCode:1}):(t.root&&te(R(t.root,".lensmcp","registered.json"),{force:!0}),r(`stopped '${t.key}': its hosts are gone and its services are stopping. \`lensmcp instance start\` in ${t.root??"it"} brings it back.`),{exitCode:0})}w($e,"Re"),m($e,"stop");function ae(n,t,i){const d=i.find(p=>p.key===n);if(!d)return!1;const s=o.daemonRequest("POST","/unregister",{wsKey:n,...d.root??t?{root:d.root??t}:{}});return!!s&&s.status===200}w(ae,"de"),m(ae,"unregisterIfServed");async function ie(n,t,i=r,d){const s=await se(q,p=>Qe(n,p,pe(t),_));s.dropped.length&&i(` dropped : ${s.dropped.join(", ")}`);for(const p of s.failed)e(` could not drop ${p.name}: ${p.error} \u2014 still on record; run this again once it can be reached`);return d&&Object.assign(d,{dropped:s.dropped,...s.failed.length?{dropFailed:s.failed}:{}}),s.failed.length===0}w(ie,"le"),m(ie,"dropData");async function be(n){const t=ne(q),i=G(o.daemonRequest("GET","/list")),d=[...i.filter(f=>typeof f.instance=="string"),...Object.values(t.instances).filter(f=>!i.some(b=>b.key===f.key)).map(f=>({key:f.key,root:f.root,instance:f.name,baseKey:f.baseKey}))],s=(n?void 0:d.find(f=>f.root!==void 0&&le(f.root,K)))??resolveInstance(d,n,Q(K)),p={},a=m(f=>(F&&r(JSON.stringify({...p,exitCode:f},null,2)),{exitCode:f}),"finish");if(typeof s=="string")return e(s.replace("is registered with the gateway","is served or on record")),a(1);const y=B(h,"--base");if(y&&s.baseKey!==y)return e(`'${s.key}' is an instance of '${s.baseKey}', not of '${y}'.`),a(1);const j=s.key,k=t.instances[j],u=s.root??k?.root,x=F?e:r;Object.assign(p,{key:j,name:s.instance,...u?{root:u}:{}});const g=h.includes("--worktree"),E=h.includes("--force"),$=k?.worktree?.path??(u&&N(u)?worktreeInfo(u,o.git)?.root:void 0);if(g){const f=$&&N($)?worktreeInfo($,o.git):void 0;if($&&N($)&&!f?.linked)return e(`${$} is not a linked git worktree \u2014 not removing it (the lens removes only worktrees).`),a(1);if($&&N($)&&!E){const b=Pe($,M);if(b===void 0||b.length>0){p.uncommitted=b??["(git could not read its status)"],e(`${$} has uncommitted work \u2014 commit it (or stash it) first; --force throws it away:`);for(const C of(b??["(git could not read its status)"]).slice(0,20))e(` ${C}`);return(b?.length??0)>20&&e(` \u2026 and ${(b?.length??0)-20} more`),a(1)}}}x(`removing instance '${s.instance}' (${j})${u?` \u2014 ${u}`:""}`),ae(j,u,i)&&(x(" stopped : its routes are gone, its services are stopping"),p.stopped=!0);let T=!0;if(h.includes("--keep-data")?(k&&Object.keys(k.resources).length&&x(` kept : ${Object.keys(k.resources).join(", ")} (on record \u2014 \`start\` reuses it, \`rm\` drops it)`),p.keptData=Object.keys(k?.resources??{})):T=await ie(j,[u,k?.mainRoot,K],x,p),u&&N(u)){const f=o.stopMcpServer?.(u,j);f&&x(` mcp : ${f}`),clearInstanceState(u,j)}if(te(Y(j,_),{force:!0}),g&&$&&N($)){const f=k?.mainRoot??worktreeInfo($,o.git)?.mainRoot??K,b=ct(f,$,E,M);b.ok?(x(` removed : the worktree ${$}`),p.worktreeRemoved=$,k?.worktree&&await se(q,I=>{delete I.instances[j]?.worktree})):(e(` could not remove the worktree ${$}: ${b.err}`),p.worktreeError=b.err,T=!1);const C=k?.worktree?.branch;if(b.ok&&C&&h.includes("--delete-branch"))if(!k?.worktree?.createdBranch)x(` kept : branch ${C} (it existed before the lens checked it out)`),p.branchKept={branch:C,why:"not made by the lens"};else{const I=ot(f,C,E,M);I.ok?(x(` deleted : branch ${C}`),p.branchDeleted=C):(x(` kept : branch ${C} \u2014 ${I.err.split(`
27
- `)[0]}`),p.branchKept={branch:C,why:I.err.split(`
28
- `)[0]})}else b.ok&&C&&(p.branchKept={branch:C,why:"not asked to delete it"})}else $&&N($)&&(p.worktreeKept=$);return a(T?0:1)}w(be,"Pe"),m(be,"rm");async function ve(){const n=ne(q),t=G(o.daemonRequest("GET","/list")),i=new Map;for(const s of Object.values(n.instances))N(s.root)||i.set(s.key,s.root);for(const s of t)s.instance&&s.root&&!N(s.root)&&i.set(s.key,s.root);if(i.size===0)return r("Nothing to prune: every instance on record still has its folder."),{exitCode:0};let d=!0;for(const[s,p]of i)r(`${h.includes("--dry-run")?"would remove":"removing"} '${s}' \u2014 ${p} is gone${n.instances[s]?`; data: ${Object.keys(n.instances[s].resources).join(", ")||"none"}`:""}`),!h.includes("--dry-run")&&(ae(s,p,t)&&r(" stopped : its routes are gone"),n.instances[s]&&(d=await ie(s,[n.instances[s].mainRoot,K])&&d));return{exitCode:d?0:1}}w(ve,"Ke"),m(ve,"prune");function Z(n){const t=o.daemonRequest("GET",`/instance-env?wsKey=${encodeURIComponent(n)}`);if(!t||t.status!==200||!t.json||typeof t.json!="object")return;const i=t.json.env;return i&&typeof i=="object"?{env:i}:void 0}w(Z,"oe"),m(Z,"instanceEnvOf")}w(runInstance,"runInstance"),m(runInstance,"runInstance");function ue(c,l,o=!1){return qe(c,{cwd:l.cwd,env:l.env,shell:!0,stdio:o?["inherit",2,2]:"inherit"}).status}w(ue,"be"),m(ue,"runShell");export function clearInstanceState(c,l){te(R(c,".lensmcp","registered.json"),{force:!0}),te(Ze(c),{force:!0});const o=R(c,".lensmcp","config.json");let r;try{r=JSON.parse(Fe(o,"utf8"))}catch{return}if(r.key===l){for(const e of["key","ports","dashboardBasePath","sockNs","instance"])delete r[e];de(o,JSON.stringify(r,null,2)+`
29
- `)}}w(clearInstanceState,"clearInstanceState"),m(clearInstanceState,"clearInstanceState");function le(c,l){const o=m(r=>{try{return Ge(r)}catch{return X(r)}},"canonical");return o(c)===o(l)}w(le,"ve"),m(le,"sameDir");function Te(c,l){const o=R(c,".lensmcp");N(o)||Ce(o,{recursive:!0}),de(R(o,"config.json"),JSON.stringify(l,null,2)+`
30
- `)}w(Te,"ut"),m(Te,"writeConfig");function _e(c,l){Ce(R(c,".lensmcp"),{recursive:!0}),de(R(c,".lensmcp","registered.json"),JSON.stringify({wsKey:l}))}w(_e,"pt"),m(_e,"writeMarker");
25
+ Remove every instance whose folder is gone, data included.`;export function worktreeInfo(i,c){const r=c(i,["rev-parse","--show-toplevel"]);if(!r)return;const o=c(i,["rev-parse","--path-format=absolute","--git-dir"]),e=c(i,["rev-parse","--path-format=absolute","--git-common-dir"]),E=!!o&&!!e&&ee(o)!==ee(e),y=E&&e&&Ke(e)===".git"?Xe(ee(e)):void 0;return{root:r,linked:E,...y?{mainRoot:y}:{}}}g(worktreeInfo,"worktreeInfo"),f(worktreeInfo,"worktreeInfo");export function nameFromFolder(i){return gt(Ke(i)).slice(0,20).replace(/-+$/,"")}g(nameFromFolder,"nameFromFolder"),f(nameFromFolder,"nameFromFolder");export function instanceConfig(i,c){const r=`${c.baseKey}-${c.name}`;return{...i,key:r,ports:yt(r),dashboardBasePath:"/"+r,sockNs:kt(r),instance:c}}g(instanceConfig,"instanceConfig"),f(instanceConfig,"instanceConfig");export function baseKeyOf(i,c){return i.instance?.baseKey?i.instance.baseKey:c&&N(S(c,".lensmcp","config.json"))?ne(c).key:i.key}g(baseKeyOf,"baseKeyOf"),f(baseKeyOf,"baseKeyOf");function W(i){if(!i||i.status!==200||!i.json||typeof i.json!="object")return[];const c=i.json.workspaces;return Array.isArray(c)?c:[]}g(W,"Q"),f(W,"listed");export function resolveInstance(i,c,r){const o=i.filter(e=>typeof e.instance=="string");if(c){const e=o.find(P=>P.key===c);if(e)return e;const E=o.filter(P=>P.instance===c),y=E.find(P=>P.baseKey===(r.instance?.baseKey??r.key));if(y)return y;const[Q,...pe]=E;return Q&&pe.length===0?Q:E.length>1?`'${c}' is an instance of ${E.map(P=>`'${P.baseKey}'`).join(" and ")} \u2014 name it by key: ${E.map(P=>P.key).join(", ")}.`:`no instance '${c}' is registered with the gateway${o.length?` (running: ${o.map(P=>P.key).join(", ")})`:""}.`}return r.instance?o.find(e=>e.key===r.key)??`this checkout's instance '${r.key}' is not registered with the gateway \u2014 \`lensmcp instance start\` here.`:"this checkout is not an instance \u2014 name one, or run `lensmcp instance list`."}g(resolveInstance,"resolveInstance"),f(resolveInstance,"resolveInstance");function D(i,c){for(const[r,o]of i.entries()){if(o===c)return i[r+1];if(o.startsWith(`${c}=`))return o.slice(c.length+1)}}g(D,"z"),f(D,"flag");function Ie(i,c,r){const o=[];let e=!1;for(const E of i){if(e){e=!1;continue}if(c.includes(E)){e=!0;continue}c.some(y=>E.startsWith(`${y}=`))||r.includes(E)||o.push(E)}return o}g(Ie,"ht"),f(Ie,"positionals");export async function runInstance(i,c,r,o,e){const E=i.indexOf("--"),y=E>=0?i.slice(0,E):i,Q=E>=0?i.slice(E+1):[],pe=["--cwd","--name","--apex","--branch","--from","--base"],[P,...Z]=Ie(y,pe,["--json","--prepare","--no-prepare","--share-stores","--keep-data","--dry-run","--frozen","--live","--worktree","--delete-branch","--force","--detach"]),M={...process.env,...r.env??{}},F=r.registryFile??Qe(M),we=f(t=>(r.resources??et)(t.filter(n=>!!n&&N(n))),"runtimeFor"),G=y.includes("--json"),R=ee(D(y,"--cwd")??c),q=r.gitExec??lt,fe=f((t,n)=>r.onStep?.(t,n),"step");if(E>=0&&(P??"").toLowerCase()!=="exec")return e(`\`--\` belongs to \`lensmcp instance exec\` only.
26
+ ${INSTANCE_USAGE}`),{exitCode:2};switch((P??"").toLowerCase()){case"create":case"new":return be(Z[0]??D(y,"--name"));case"start":return $e();case"list":case"ls":return Ce();case"status":return je(Z[0]);case"exec":return Se(Z[0]);case"stop":return Oe(Z[0]);case"rm":case"remove":return Ee(Z[0]);case"prune":return Ne();default:return(P?e:o)(P?`Unknown instance command: ${P}
27
+ ${INSTANCE_USAGE}`:INSTANCE_USAGE),{exitCode:P?2:0}}async function $e(){const t=R,n=worktreeInfo(R,r.git),l=n?Ae(n.root,t):"",d=ne(t),s=D(y,"--name")??d.instance?.name??(n?.linked?nameFromFolder(n.root):void 0);if(!s)return e(`${t} is not a linked git worktree, so its instance has no name \u2014 pass \`--name <name>\`.`),{exitCode:2};if(!Be.test(s))return e(`'${s}' is not an instance name: one DNS label of at most 20 characters \u2014 lowercase letters, digits and inner hyphens.`),{exitCode:2};const p=D(y,"--apex")??d.instance?.apex,a=(n?.mainRoot?S(n.mainRoot,l):void 0)??d.instance?.mainRoot,k=y.includes("--frozen")?!0:y.includes("--live")?!1:d.instance?.frozen===!0,j={name:s,baseKey:baseKeyOf(d,a),...p?{apex:p.toLowerCase()}:{},...a?{mainRoot:a}:{},...k?{frozen:!0}:{}},m=instanceConfig(d,j),u=r.daemonRequest("GET","/list");if(u===null)return e("No gateway daemon is running. An instance is served by the main checkout's gateway \u2014"),e(`start it there first (\`lensmcp gateway start\`${a?` in ${a}`:""}), then run this again.`),{exitCode:1};const x=W(u),w=x.find(C=>C.key!==m.key&&C.root!==void 0&&ge(C.root,t));if(w?.instance)return e(`${t} is already served as instance '${w.instance}' (${w.key}). \`lensmcp instance stop\` first to rename it.`),{exitCode:1};if(w)return e(`${t} is already served by the gateway as '${w.key}'. An instance is a SECOND checkout \u2014 run this in a`),e("git worktree of it (`git worktree add <dir>`), or `lensmcp gateway stop` here first."),{exitCode:1};const T=Ve(x,m.key,t);if(T.kind==="other-checkout")return e(`'${m.key}' is already served from ${T.root}. Give this one another name: \`lensmcp instance start --name <name>\`.`),{exitCode:1};const v=a&&N(S(a,"nx.json"))?a:t,{decl:K,errors:h}=Te(v);if(h.length>0){e(`${S(v,"nx.json")} \u2192 lensmcp.instances is not usable:`);for(const C of h)e(` ${C}`);return{exitCode:2}}if(!K&&T.kind==="free"&&!y.includes("--share-stores"))return e(`${S(v,"nx.json")} declares no \`lensmcp.instances\`, so the instance would run on the main checkout's database and`),e("queues \u2014 whatever its .env points at: its workers would take main's jobs, its migrations would run on main's data."),e("Declare its resources (`lensmcp.instances.resources` \u2014 see `lensmcp instance --help`), or pass --share-stores."),{exitCode:1};let $;if(K){if(fe("provisioning",Object.keys(K.resources).length?`its data: ${Object.keys(K.resources).join(", ")}`:"bootstrapping the checkout"),a){const O=Ze(t,a,K.bootstrap);O.copied.length&&o(` copied \u2190 ${O.copied.join(", ")} (from ${a})`),O.cloned.length&&o(` cloned \u2190 ${O.cloned.join(", ")} (copy-on-write)`);for(const H of O.notes)o(` note: ${H}`)}const C={key:m.key,name:s,baseKey:j.baseKey,root:t,...a?{mainRoot:a}:{}};try{$=await ue(F,O=>nt(K,C,O,we([t,a]),M))}catch(O){return e(`could not give instance '${m.key}' its data: ${O.message}`),e("Whatever was already created is on record \u2014 `lensmcp instance rm` drops it."),{exitCode:1}}rt(t,$)}De(t,m);const b=K?.prepare,_=qe(t),U=We("sha256").update(JSON.stringify({resources:K?.resources,env:$?.resolved})).digest("hex"),X=b&&!y.includes("--no-prepare")&&(y.includes("--prepare")||($?.created.length??0)>0||_?.key!==m.key||_.command!==b||_.resourcesHash!==U||_.state!=="ready");let B=T.kind==="mine",z=!1;if(X){le(t,{key:m.key,command:b,resourcesHash:U,state:"pending"}),fe("preparing",b);const C=r.daemonRequest("POST","/instance-env",r.registrationBody(t,m.key)),O=C?.status===200?ye(C.json):void 0;if(!O)return e(`Could not resolve the preparation environment for '${m.key}' before registration: ${ce(C)??"the gateway does not support environment preview"}. Update and restart the main gateway, then retry \`lensmcp instance start\`.`),{exitCode:1};if(B){const J=r.daemonRequest("POST","/unregister",{wsKey:m.key,root:t});if(!J||J.status!==200)return e(`Could not stop '${m.key}' before preparation: ${ce(J)??"no response from the control socket"}`),{exitCode:1};B=!1,Y(S(t,".lensmcp","registered.json"),{force:!0})}o(` prepare \u2192 ${b}`);const H=(r.runPrepare??ke)(b,{cwd:t,env:{...M,...$?.resolved??{},...O.env}});if(H!==0)return e(`\`${b}\` exited ${H??"on a signal"}. The instance remains stopped \u2014 fix it and retry \`lensmcp instance start\`; preparation will run again.`),{exitCode:1};le(t,{key:m.key,command:b,resourcesHash:U,state:"ready"}),z=!0}else b&&($?.created.length??0)>0&&le(t,{key:m.key,command:b,resourcesHash:U,state:"pending"});if(fe("registering","with the gateway"),!B){const C=r.daemonRequest("POST","/register",r.registrationBody(t,m.key));if(!C||C.status!==200)return e(`The gateway refused instance '${m.key}': ${ce(C)??(C?JSON.stringify(C.json):"no response from the control socket")}`),{exitCode:1}}ze(t,m.key);const L=re(m.key),V={};for(const[C,O]of Object.entries(L?.env??{}))C.startsWith("LENSMCP_HOST_")&&(V[C.slice(13).toLowerCase()]=`https://${O}`);const oe={name:s,key:m.key,baseKey:j.baseKey,root:t,...a?{mainRoot:a}:{},frozen:k,state:B?"already-running":"started",...L?.env.LENSMCP_APEX?{apex:L.env.LENSMCP_APEX}:{},hosts:V,dashboard:`https://lensmcp.local${m.dashboardBasePath}/`,resources:Object.keys(K?.resources??{}),created:$?.created??[],prepared:z};if(o(`${B?"instance already running":"instance started"}: '${s}' of '${j.baseKey}' \u2014 key ${m.key}${k?" (frozen: compiled once, no watcher)":""}`),B&&d.instance?.frozen===!0!==k&&o(` note: it was already running ${d.instance?.frozen?"frozen":"live"} \u2014 \`lensmcp instance stop\` then \`start\` applies the change.`),L){o(` apex \u2192 ${L.env.LENSMCP_APEX??"?"}`);for(const[C,O]of Object.entries(L.env).filter(([H])=>H.startsWith("LENSMCP_HOST_")).sort())o(` ${C.slice(13).toLowerCase().padEnd(9)} \u2192 https://${O}`)}else o(" (the daemon did not report its hosts \u2014 it may predate instances, and serves this checkout under main's hosts)");if(o(` dashboard \u2192 https://lensmcp.local${m.dashboardBasePath}/`),$){for(const[C,O]of Object.entries($.resolved))o(` ${C.padEnd(9)} = ${Ye(O)}`);$.created.length&&o(` created : ${$.created.join(", ")}`),B&&$.created.length&&o(" note: it was already running \u2014 `lensmcp instance stop` then `start` hands the new resources to its services.")}return o(" its always-warm services start now, one at a time while the machine has memory to spare (LENSMCP_WARM_MIN_FREE_MB);"),o(" the rest on their first request, and each stops after 30 min unused. `lensmcp instance exec -- <cmd>` runs a command with its variables;"),o(" `lensmcp instance stop` unregisters it, `lensmcp instance rm` also drops its data."),ve(m.key,oe.prepared?"started again, prepared":"started again"),{exitCode:0,instance:oe}}g($e,"Ne"),f($e,"start");async function be(t){const n=G?e:o;if(!t)return e(`Name the instance: \`lensmcp instance create <name>\`.
28
+ ${INSTANCE_USAGE}`),{exitCode:2};if(!Be.test(t))return e(`'${t}' is not an instance name: one DNS label of at most 20 characters \u2014 lowercase letters, digits and inner hyphens.`),{exitCode:2};const l=worktreeInfo(R,r.git);if(!l)return e(`${R} is not in a git checkout \u2014 \`lensmcp instance create\` makes a git worktree of one.`),{exitCode:2};const d=l.linked?l.mainRoot:l.root;if(!d)return e(`${l.root} is a worktree whose main checkout git does not name (a bare repository?) \u2014 make the worktree yourself and \`lensmcp instance start\` in it.`),{exitCode:2};const s=Ae(l.root,R),p=S(d,s),a=baseKeyOf(ne(p),p),k=`${a}-${t}`,j=it(te(k,M));if("heldBy"in j)return e(`'${k}' is being created right now by another lensmcp (pid ${j.heldBy}). Wait for it \u2014 \`lensmcp instance list\` shows it \u2014 or pick another name.`),{exitCode:1};try{const{decl:m,errors:u}=Te(N(S(p,"nx.json"))?p:R);if(u.length>0){e(`${S(p,"nx.json")} \u2192 lensmcp.instances is not usable:`);for(const A of u)e(` ${A}`);return{exitCode:2}}if(!m&&!y.includes("--share-stores"))return e(`${S(p,"nx.json")} declares no \`lensmcp.instances\`, so a new instance would run on the main checkout's database and`),e("queues. Declare its resources (`lensmcp.instances.resources`), or pass --share-stores."),{exitCode:1};const x=ht(p,m?.worktrees,M),w=S(x,t),T=S(w,s);if(N(w))return e(`${w} already exists. \`lensmcp instance start\` in it serves it; or pick another name.`),{exitCode:1};const v=de(F).instances[k];if(v)return e(`'${k}' is already on record for ${v.root}${N(v.root)?"":" (its folder is gone \u2014 `lensmcp instance prune` clears it)"}. Pick another name.`),{exitCode:1};const K=r.daemonRequest("GET","/list");if(K===null)return e("No gateway daemon is running. An instance is served by the main checkout's gateway \u2014"),e(`start it there first (\`lensmcp gateway start\` in ${p}), then run this again.`),{exitCode:1};if(W(K).some(A=>A.key===k))return e(`'${k}' is already served by the gateway. Pick another name.`),{exitCode:1};const h=y.includes("--detach")?void 0:D(y,"--branch")??`lensmcp/${t}`;if(h){const A=at(d,h,q);if(A)return e(A),{exitCode:2}}const $=D(y,"--from");if(h&&$&&xe(d,["show-ref","--verify","--quiet",`refs/heads/${h}`]))return e(`Branch '${h}' exists already, so it is checked out as it is \u2014 --from applies to a new branch. Drop --from, or name a new --branch.`),{exitCode:2};const b=ft(R,$??"HEAD",q);if("error"in b)return e(b.error),{exitCode:2};const _=ut(d,x,q);_&&n(` ignored : ${_} (added to .git/info/exclude \u2014 the worktrees stay out of main's git status)`);const U=te(k,M),X=new Date().toISOString();let B={key:k,name:t,baseKey:a,root:T,state:"creating",detail:`git worktree add ${w}`,pid:process.pid,startedAt:X,updatedAt:X,...M.LENSMCP_CREATE_LOG?{log:M.LENSMCP_CREATE_LOG}:{}};const z=f(A=>{B={...B,...A,updatedAt:new Date().toISOString()};try{Le(U,B)}catch{}},"record");z({});const L=ot(d,w,{...h?{branch:h}:{},from:b.sha},q);if(!L.ok)return z({state:"failed",detail:L.error}),e(`git could not make the worktree: ${L.error}`),{exitCode:1};const V={path:w,...h?{branch:h}:{},createdBranch:L.createdBranch,from:b.sha,createdAt:X};z({worktree:V,detail:"worktree made"}),n(`worktree : ${w} \u2014 ${h?`branch ${h}${L.createdBranch?" (new)":""}`:"detached"} at ${b.sha.slice(0,12)}`);const oe={key:k,name:t,baseKey:a,root:T,...p!==T?{mainRoot:p}:{},createdAt:X,resources:{},worktree:V};await ue(F,A=>{A.instances[k]=oe});const C=["--frozen","--live","--prepare","--no-prepare","--share-stores"].filter(A=>y.includes(A)),O=D(y,"--apex"),H=["start","--name",t,...O?["--apex",O]:[],...C],J=await runInstance(H,T,{...r,...G&&!r.runPrepare?{runPrepare:f((A,ae)=>ke(A,ae,!0),"runPrepare")}:{},onStep:f((A,ae)=>z({state:A,...ae?{detail:ae}:{}}),"onStep")},n,e),I=J.instance?{...J.instance,worktree:V}:void 0;return J.exitCode!==0?(z({state:"failed",detail:`${J.instance?"started, but `prepare` failed":"could not start"} \u2014 \`lensmcp instance rm ${t} --worktree\` takes it away`,...I?{result:I}:{}}),e(`The worktree is at ${w}. \`lensmcp instance start --prepare\` there retries; \`lensmcp instance rm ${t} --worktree\` takes it all away.`),G&&I&&o(JSON.stringify(I,null,2)),{exitCode:J.exitCode,...I?{instance:I}:{}}):(z({state:"ready",detail:"ready",...I?{result:I}:{}}),G&&o(JSON.stringify(I,null,2)),{exitCode:0,...I?{instance:I}:{}})}finally{j.release()}}g(be,"Pe"),f(be,"create");function ve(t,n){const l=te(t,M),d=_e(l);if(!(!d||d.state==="ready"||d.pid===process.pid))try{Le(l,{...d,state:"ready",detail:n,updatedAt:new Date().toISOString()})}catch{}}g(ve,"Re"),f(ve,"settleCreate");function xe(t,n){return q(t,n).ok}g(xe,"Ke"),f(xe,"gitExecOk");function Ce(){const t=r.daemonRequest("GET","/list"),n=D(y,"--base"),l=f(a=>!n||a===n,"ofBase"),d=W(t).filter(a=>typeof a.instance=="string"&&l(a.baseKey)),s=de(F),p=Object.values(s.instances).filter(a=>!d.some(k=>k.key===a.key)&&l(a.baseKey));if(G){const a=f(u=>{if(!u||!N(u))return{};const x=Me(u,q),w=ct(u,q);return{...w?{branch:w}:{},...x?{changes:x.length}:{}}},"gitFacts"),k=f(u=>{const x=_e(te(u,M));if(!x||x.state==="ready")return{};const w=mt(x);return{progress:{state:w.state,...w.detail?{detail:w.detail}:{},updatedAt:w.updatedAt,...w.log?{log:w.log}:{}}}},"unfinished"),j=f(u=>{const x={};for(const[w,T]of Object.entries(re(u)?.env??{}))w.startsWith("LENSMCP_HOST_")&&(x[w.slice(13).toLowerCase()]=`https://${T}`);return Object.keys(x).length?{hosts:x}:{}},"hostsOf"),m=d.map(u=>({...u,...a(u.root),...j(u.key),...s.instances[u.key]?.worktree?{worktree:s.instances[u.key]?.worktree}:{},...k(u.key)}));return o(JSON.stringify({checkout:{root:R,...a(R)},instances:m,stopped:p.map(u=>({key:u.key,name:u.name,baseKey:u.baseKey,root:u.root,resources:Object.keys(u.resources),folderGone:!N(u.root),...a(u.root),...u.worktree?{worktree:u.worktree}:{},...k(u.key)}))},null,2)),{exitCode:0}}if(t===null&&o("(no gateway daemon is running)"),d.length===0&&p.length===0)return o("No instances. `lensmcp instance start` in a worktree serves one."),{exitCode:0};for(const a of d){const k=Object.keys(s.instances[a.key]?.resources??{});o(`${a.instance} (${a.key}, instance of ${a.baseKey}) ${a.apex??""}${a.frozen?" frozen":""} ${a.services??0} service(s)${k.length?` data: ${k.join(", ")}`:""} ${a.root??""}`)}for(const a of p){const k=!N(a.root);o(`${a.name} (${a.key}, instance of ${a.baseKey}) ${k?"FOLDER GONE \u2014 `lensmcp instance prune`":"stopped"} data: ${Object.keys(a.resources).join(", ")||"none"} ${a.root}`)}return{exitCode:0}}g(Ce,"Ae"),f(Ce,"list");function se(t){const n=r.daemonRequest("GET","/list");if(n===null){e("No gateway daemon is running.");return}const l=resolveInstance(W(n),t,ne(R));if(typeof l=="string"){e(l);return}return l}g(se,"le"),f(se,"target");function je(t){const n=se(t);if(!n)return{exitCode:1};const l=r.daemonRequest("GET","/status"),d=l?.status===200&&l.json&&typeof l.json=="object"?(l.json.services??[]).filter(s=>s.wsKey===n.key):[];if(G)return o(JSON.stringify({instance:n,services:d},null,2)),{exitCode:0};o(`${n.instance} (${n.key}, instance of ${n.baseKey}) \u2014 ${n.apex??"?"} ${n.root??""}`),d.length===0&&o(" no pod services");for(const s of d)o(` ${s.project.padEnd(24)} ${s.state.padEnd(9)} ${s.host?`https://${s.host}`:""}`);return{exitCode:0}}g(je,"Te"),f(je,"status");async function Se(t){if(Q.length===0)return e("Nothing to run: `lensmcp instance exec [<name>] -- <command> [args\u2026]`."),{exitCode:2};const n=se(t);if(!n)return{exitCode:1};const l=re(n.key);if(!l)return e(`The gateway did not give the variables of '${n.key}'.`),{exitCode:1};const[d,...s]=Q;return new Promise(p=>{const a=Ge(d,s,{cwd:n.root??R,stdio:"inherit",env:{...process.env,...r.env??{},...l.env}});a.on("error",k=>{e(`could not run ${d}: ${k.message}`),p({exitCode:127})}),a.on("exit",(k,j)=>p({exitCode:k??(j?128:1)}))})}g(Se,"_e"),f(Se,"exec");function Oe(t){const n=se(t);if(!n)return{exitCode:1};const l=r.daemonRequest("POST","/unregister",{wsKey:n.key,...n.root?{root:n.root}:{}});return!l||l.status!==200?(e(`could not stop '${n.key}': ${ce(l)??(l?JSON.stringify(l.json):"no response from the control socket")}`),{exitCode:1}):(n.root&&Y(S(n.root,".lensmcp","registered.json"),{force:!0}),o(`stopped '${n.key}': its hosts are gone and its services are stopping. \`lensmcp instance start\` in ${n.root??"it"} brings it back.`),{exitCode:0})}g(Oe,"Le"),f(Oe,"stop");function me(t,n,l){const d=l.find(p=>p.key===t);if(!d)return!1;const s=r.daemonRequest("POST","/unregister",{wsKey:t,...d.root??n?{root:d.root??n}:{}});return!!s&&s.status===200}g(me,"me"),f(me,"unregisterIfServed");async function he(t,n,l=o,d){const s=await ue(F,p=>tt(t,p,we(n),M));s.dropped.length&&l(` dropped : ${s.dropped.join(", ")}`);for(const p of s.failed)e(` could not drop ${p.name}: ${p.error} \u2014 still on record; run this again once it can be reached`);return d&&Object.assign(d,{dropped:s.dropped,...s.failed.length?{dropFailed:s.failed}:{}}),s.failed.length===0}g(he,"ye"),f(he,"dropData");async function Ee(t){const n=de(F),l=W(r.daemonRequest("GET","/list")),d=[...l.filter(h=>typeof h.instance=="string"),...Object.values(n.instances).filter(h=>!l.some($=>$.key===h.key)).map(h=>({key:h.key,root:h.root,instance:h.name,baseKey:h.baseKey}))],s=(t?void 0:d.find(h=>h.root!==void 0&&ge(h.root,R)))??resolveInstance(d,t,ne(R)),p={},a=f(h=>(G&&o(JSON.stringify({...p,exitCode:h},null,2)),{exitCode:h}),"finish");if(typeof s=="string")return e(s.replace("is registered with the gateway","is served or on record")),a(1);const k=D(y,"--base");if(k&&s.baseKey!==k)return e(`'${s.key}' is an instance of '${s.baseKey}', not of '${k}'.`),a(1);const j=s.key,m=n.instances[j],u=s.root??m?.root,x=G?e:o;Object.assign(p,{key:j,name:s.instance,...u?{root:u}:{}});const w=y.includes("--worktree"),T=y.includes("--force"),v=m?.worktree?.path??(u&&N(u)?worktreeInfo(u,r.git)?.root:void 0);if(w){const h=v&&N(v)?worktreeInfo(v,r.git):void 0;if(v&&N(v)&&!h?.linked)return e(`${v} is not a linked git worktree \u2014 not removing it (the lens removes only worktrees).`),a(1);if(v&&N(v)&&!T){const $=Me(v,q);if($===void 0||$.length>0){p.uncommitted=$??["(git could not read its status)"],e(`${v} has uncommitted work \u2014 commit it (or stash it) first; --force throws it away:`);for(const b of($??["(git could not read its status)"]).slice(0,20))e(` ${b}`);return($?.length??0)>20&&e(` \u2026 and ${($?.length??0)-20} more`),a(1)}}}x(`removing instance '${s.instance}' (${j})${u?` \u2014 ${u}`:""}`),me(j,u,l)&&(x(" stopped : its routes are gone, its services are stopping"),p.stopped=!0);let K=!0;if(y.includes("--keep-data")?(m&&Object.keys(m.resources).length&&x(` kept : ${Object.keys(m.resources).join(", ")} (on record \u2014 \`start\` reuses it, \`rm\` drops it)`),p.keptData=Object.keys(m?.resources??{})):K=await he(j,[u,m?.mainRoot,R],x,p),u&&N(u)){const h=r.stopMcpServer?.(u,j);h&&x(` mcp : ${h}`),clearInstanceState(u,j)}if(Y(te(j,M),{force:!0}),w&&v&&N(v)){const h=m?.mainRoot??worktreeInfo(v,r.git)?.mainRoot??R,$=pt(h,v,T,q);$.ok?(x(` removed : the worktree ${v}`),p.worktreeRemoved=v,m?.worktree&&await ue(F,_=>{delete _.instances[j]?.worktree})):(e(` could not remove the worktree ${v}: ${$.err}`),p.worktreeError=$.err,K=!1);const b=m?.worktree?.branch;if($.ok&&b&&y.includes("--delete-branch"))if(!m?.worktree?.createdBranch)x(` kept : branch ${b} (it existed before the lens checked it out)`),p.branchKept={branch:b,why:"not made by the lens"};else{const _=dt(h,b,T,q);_.ok?(x(` deleted : branch ${b}`),p.branchDeleted=b):(x(` kept : branch ${b} \u2014 ${_.err.split(`
29
+ `)[0]}`),p.branchKept={branch:b,why:_.err.split(`
30
+ `)[0]})}else $.ok&&b&&(p.branchKept={branch:b,why:"not asked to delete it"})}else v&&N(v)&&(p.worktreeKept=v);return a(K?0:1)}g(Ee,"Me"),f(Ee,"rm");async function Ne(){const t=de(F),n=W(r.daemonRequest("GET","/list")),l=new Map;for(const s of Object.values(t.instances))N(s.root)||l.set(s.key,s.root);for(const s of n)s.instance&&s.root&&!N(s.root)&&l.set(s.key,s.root);if(l.size===0)return o("Nothing to prune: every instance on record still has its folder."),{exitCode:0};let d=!0;for(const[s,p]of l)o(`${y.includes("--dry-run")?"would remove":"removing"} '${s}' \u2014 ${p} is gone${t.instances[s]?`; data: ${Object.keys(t.instances[s].resources).join(", ")||"none"}`:""}`),!y.includes("--dry-run")&&(me(s,p,n)&&o(" stopped : its routes are gone"),t.instances[s]&&(d=await he(s,[t.instances[s].mainRoot,R])&&d));return{exitCode:d?0:1}}g(Ne,"De"),f(Ne,"prune");function re(t){const n=r.daemonRequest("GET",`/instance-env?wsKey=${encodeURIComponent(t)}`);if(!(!n||n.status!==200||!n.json||typeof n.json!="object"))return ye(n.json)}g(re,"fe"),f(re,"instanceEnvOf")}g(runInstance,"runInstance"),f(runInstance,"runInstance");function ye(i){if(!i||typeof i!="object")return;const c=i.env;return c&&typeof c=="object"&&!Array.isArray(c)&&Object.values(c).every(r=>typeof r=="string")?{env:c}:void 0}g(ye,"Se"),f(ye,"environmentOf");function qe(i){try{return JSON.parse(Re(S(i,".lensmcp","prepare.json"),"utf8"))}catch{return}}g(qe,"mt"),f(qe,"readPreparation");function le(i,c){ie(S(i,".lensmcp","prepare.json"),JSON.stringify(c)+`
31
+ `,{mode:384})}g(le,"pe"),f(le,"writePreparation");function ke(i,c,r=!1){return He(i,{cwd:c.cwd,env:c.env,shell:!0,stdio:r?["inherit",2,2]:"inherit"}).status}g(ke,"Oe"),f(ke,"runShell");export function clearInstanceState(i,c){Y(S(i,".lensmcp","registered.json"),{force:!0}),Y(st(i),{force:!0}),Y(S(i,".lensmcp","prepare.json"),{force:!0});const r=S(i,".lensmcp","config.json");let o;try{o=JSON.parse(Re(r,"utf8"))}catch{return}if(o.key===c){for(const e of["key","ports","dashboardBasePath","sockNs","instance"])delete o[e];ie(r,JSON.stringify(o,null,2)+`
32
+ `)}}g(clearInstanceState,"clearInstanceState"),f(clearInstanceState,"clearInstanceState");function ge(i,c){const r=f(o=>{try{return Ue(o)}catch{return ee(o)}},"canonical");return r(i)===r(c)}g(ge,"Ee"),f(ge,"sameDir");function De(i,c){const r=S(i,".lensmcp");N(r)||Pe(r,{recursive:!0}),ie(S(r,"config.json"),JSON.stringify(c,null,2)+`
33
+ `)}g(De,"yt"),f(De,"writeConfig");function ze(i,c){Pe(S(i,".lensmcp"),{recursive:!0}),ie(S(i,".lensmcp","registered.json"),JSON.stringify({wsKey:c}))}g(ze,"kt"),f(ze,"writeMarker");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lensmcp",
3
- "version": "1.25.21",
3
+ "version": "1.25.22",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "module": "./index.js",
@@ -18,8 +18,8 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@frontmcp/sdk": "^1.10.10",
21
- "@lensmcp/cluster": "1.25.21",
22
- "@lensmcp/nx-plugin": "1.25.21",
21
+ "@lensmcp/cluster": "1.25.22",
22
+ "@lensmcp/nx-plugin": "1.25.22",
23
23
  "reflect-metadata": "^0.2.2",
24
24
  "tslib": "^2.3.0",
25
25
  "vectoriadb": "^2.2.0"
@@ -2,7 +2,7 @@
2
2
  "name": "lensmcp",
3
3
  "displayName": "LensMCP",
4
4
  "description": "The observability lens for coding agents. One command brings up the dev cluster gateway (every project.json `cluster` decl → its host on :443), the per-project lens dashboard at https://lensmcp.local/<project>/, and the MCP server your agent connects to — scoped automatically to whatever project you opened Claude Code in.",
5
- "version": "1.25.21",
5
+ "version": "1.25.22",
6
6
  "author": {
7
7
  "name": "David Antoon",
8
8
  "email": "davidmantoon@gmail.com"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lensmcp",
3
- "version": "1.25.21",
3
+ "version": "1.25.22",
4
4
  "description": "Observe a running Nx workspace from Codex with runtime flows, build health, cluster failures, and a live dashboard.",
5
5
  "author": {
6
6
  "name": "David Antoon",