@lunora/cli
Version:
The Lunora CLI: init, dev, deploy, codegen, migrate, seed, doctor, insights, logs, registry, and the rest of the project commands
11 lines (9 loc) • 15.9 kB
JavaScript
import{V as I}from"../packem_shared/cli-DIb5Ay4K.mjs";import{d as R}from"../packem_shared/command-DwC11Cwe.mjs";import{existsSync as u,readFileSync as v,mkdirSync as J,realpathSync as W,statSync as V,writeFileSync as z,chmodSync as G,renameSync as H,unlinkSync as K}from"node:fs";import{homedir as w}from"node:os";import{findWranglerFile as q}from"@lunora/config/cloudflare";import{join as s,dirname as B,relative as X}from"@visulima/path";import{stringify as Z}from"smol-toml";import{parse as Q,modify as Y,applyEdits as ee}from"jsonc-parser";import{detectPackageManager as re,execArgsFor as te,readLiveDevServerState as b,DEV_STATE_DIR as ne}from"@lunora/config";import{resolveAdminToken as oe}from"@lunora/config/studio-host";import{connectLocalStdio as ae}from"@lunora/mcp";import{resolve as h}from"node:path";import{i as le}from"../packem_shared/path-containment-CgxYZggb.mjs";const f=e=>({args:[...e.args],command:e.command,...e.env===void 0?{}:{env:e.env}}),y=e=>e.transport==="http"?{type:"http",url:e.url}:f(e),ce=e=>e.transport==="http"?{httpUrl:e.url}:f(e),se=e=>e.transport==="http"?{serverUrl:e.url}:f(e),ie=e=>({args:["-y","mcp-remote",e],command:"npx"}),de=e=>e.transport==="http"?ie(e.url):f(e),ue=e=>e.transport==="http"?{url:e.url}:f(e),fe=e=>e.transport==="http"?{enabled:!0,type:"remote",url:e.url}:{command:[e.command,...e.args],enabled:!0,type:"local",...e.env===void 0?{}:{environment:e.env}},me=e=>e.transport==="http"?{disabled:!1,type:"streamableHttp",url:e.url}:{disabled:!1,...f(e)},pe=({home:e,platform:r})=>{if(r==="darwin")return{global:s(e,"Library","Application Support","Claude","claude_desktop_config.json")};if(r==="win32"){const t=process.env.APPDATA;return t===void 0||t===""?{}:{global:s(t,"Claude","claude_desktop_config.json")}}return r==="linux"?{global:s(e,".config","Claude","claude_desktop_config.json")}:{}},ge=(e,r)=>Z({mcp_servers:{[e]:r.transport==="http"?{url:r.url}:{args:[...r.args],command:r.command,...r.env===void 0?{}:{env:r.env}}}}),g=[{buildEntry:y,paths:({projectRoot:e})=>({project:s(e,".mcp.json")}),format:"json",id:"claude-code",key:"mcpServers",label:"Claude Code"},{buildEntry:y,paths:({home:e,projectRoot:r})=>({global:s(e,".cursor","mcp.json"),project:s(r,".cursor","mcp.json")}),format:"json",id:"cursor",key:"mcpServers",label:"Cursor"},{buildEntry:y,paths:({projectRoot:e})=>({project:s(e,".vscode","mcp.json")}),format:"json",id:"vscode",key:"servers",label:"VS Code (GitHub Copilot)"},{buildEntry:ce,paths:({home:e,projectRoot:r})=>({global:s(e,".gemini","settings.json"),project:s(r,".gemini","settings.json")}),format:"json",id:"gemini",key:"mcpServers",label:"Gemini CLI"},{buildEntry:de,paths:pe,format:"json",id:"claude-desktop",key:"mcpServers",label:"Claude Desktop"},{buildEntry:se,paths:({home:e})=>({global:s(e,".codeium","windsurf","mcp_config.json")}),format:"json",id:"windsurf",key:"mcpServers",label:"Windsurf"},{buildEntry:fe,paths:({home:e,projectRoot:r})=>({global:s(e,".config","opencode","opencode.json"),project:s(r,"opencode.json")}),format:"json",id:"opencode",key:"mcp",label:"OpenCode"},{buildEntry:me,paths:({home:e})=>({global:s(e,".cline","mcp.json")}),format:"json",id:"cline",key:"mcpServers",label:"Cline"},{buildEntry:ue,paths:({home:e})=>({global:s(e,".config","zed","settings.json")}),format:"json",id:"zed",key:"context_servers",label:"Zed"},{paths:({home:e,projectRoot:r})=>({global:s(e,".codex","config.toml"),project:s(r,".codex","config.toml")}),format:"manual",id:"codex",label:"Codex CLI",renderSnippet:ge}],x=g.map(e=>e.id),ve=e=>g.find(r=>r.id===e.toLowerCase()),_=(e,r)=>{let t=e,n;try{t=W(e),n=V(t).mode}catch{}const a=`${t}.lunora-${Math.random().toString(36).slice(2,10)}.tmp`;try{z(a,r,"utf8"),n!==void 0&&G(a,n),H(a,t)}catch(o){try{K(a)}catch{}throw o}},he={formattingOptions:{insertSpaces:!0,tabSize:4}},S=e=>{const r=[],t=Q(e,r,{allowTrailingComma:!0});return{errors:r,value:t}},p=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),k=(e,r,t)=>{if(!p(e))return;const n=e[r];return p(n)?n[t]:void 0},M=e=>{if(!u(e.path))return"absent";let r;try{r=v(e.path,"utf8")}catch{return"invalid"}if(r.trim().length===0)return"absent";const{errors:t,value:n}=S(r);return t.length>0||!p(n)?"invalid":k(n,e.key,e.name)===void 0?"absent":"present"},ye=e=>M(e)==="present",L=(e,r,t,n,a)=>{try{const o=Y(e,[t,n],a,he);o.length>0&&_(r,ee(e,o));return}catch(o){return o instanceof Error?o.message:String(o)}},be=e=>{const{entry:r,force:t=!1,key:n,name:a,path:o}=e,l=u(o)?v(o,"utf8"):"";if(l.trim().length===0)return J(B(o),{recursive:!0}),_(o,`${JSON.stringify({[n]:{[a]:r}},void 0,4)}
`),{action:"created",path:o};const{errors:c,value:i}=S(l);if(c.length>0)return{action:"invalid",error:`parse error at offset ${String(c[0]?.offset??0)}`,path:o};if(!p(i))return{action:"invalid",error:"the file's root is not a JSON object",path:o};const m=i[n];if(m!==void 0&&!p(m))return{action:"invalid",error:`"${n}" is not an object`,path:o};if(k(i,n,a)!==void 0&&!t)return{action:"skipped",path:o};const $=L(l,o,n,a,r);return $===void 0?{action:"updated",path:o}:{action:"invalid",error:$,path:o}},we=e=>{const{key:r,name:t,path:n}=e;if(!u(n))return{action:"absent",path:n};let a;try{a=v(n,"utf8")}catch(i){return{action:"invalid",error:i instanceof Error?i.message:String(i),path:n}}const{errors:o,value:l}=S(a);if(o.length>0)return{action:"invalid",error:`parse error at offset ${String(o[0]?.offset??0)}`,path:n};if(k(l,r,t)===void 0)return{action:"absent",path:n};const c=L(a,n,r,t,void 0);return c===void 0?{action:"removed",path:n}:{action:"invalid",error:c,path:n}},Se=["global","project"],d=(e,r)=>{const t=X(r,e);return t.length>0&&!t.startsWith("..")?t:e},ke=(e,r)=>{const t=e.paths(r);return Se.flatMap(n=>{const a=t[n];return a===void 0?[]:[{client:e,path:a,scope:n}]})},$e=(e,r,t,n)=>{const a=e.paths(r),o=t==="global"?"project":"global",l=a[t]??(n?void 0:a[o]);if(l!==void 0)return{client:e,path:l,scope:a[t]===void 0?o:t}},Ee=e=>g.filter(r=>Object.values(r.paths(e)).some(t=>u(t))),N=(e,r,t,n)=>{if(e.length===0)return r();const a=[];for(const o of e){const l=ve(o);if(l===void 0){t.error(`mcp ${n}: unknown client "${o}". Known clients: ${x.join(", ")}.`);return}a.push(l)}return a},je=e=>e.client.format==="json",Oe="https://lunora.sh/mcp",A="lunora-docs",U="lunora",Ce=e=>u(s(e,"lunora"))&&q(e)!==void 0,xe=e=>{const{args:r,command:t}=te(e,"lunora",["mcp","serve"]);return{args:r,command:t,transport:"stdio"}},_e=e=>({transport:"http",url:e}),Me=e=>({home:e.home,platform:e.platform,projectRoot:e.cwd}),D=(e,r,t)=>$e(e,Me(t),t.scope??r.preferredScope,t.scope!==void 0||!r.allowScopeFallback),Le=(e,r,t)=>{const n=D(e,r,t);if(n===void 0){t.logger.warn(`${e.label}: has no ${t.scope??r.preferredScope}-scoped config — skipped "${r.name}".`);return}const{path:a}=n,o=e.buildEntry(r.spec);if(t.print===!0){const i=d(a,t.cwd);return t.force!==!0&&ye({key:e.key,name:r.name,path:a})?(t.logger.info(`${e.label}: "${r.name}" already configured in ${i} — a real install would skip it (re-run with --force to replace).`),{action:"printed",path:a}):(t.logger.info(`${e.label} → ${i}
${JSON.stringify({[e.key]:{[r.name]:o}},void 0,4)}`),{action:"printed",path:a})}const l=be({entry:o,force:t.force===!0,key:e.key,name:r.name,path:a}),c=d(a,t.cwd);return l.action==="invalid"?t.logger.error(`${e.label}: ${c} is not valid JSON (${l.error??"unknown error"}) — left untouched.`):l.action==="skipped"?t.logger.info(`${e.label}: "${r.name}" already configured in ${c} — re-run with --force to replace it.`):t.logger.success(`${e.label}: ${l.action==="created"?"created":"updated"} ${c} with "${r.name}".`),{action:l.action,path:a}},Ne=(e,r,t)=>{const n=D(e,r,t)?.path;if(n===void 0){t.logger.warn(`${e.label}: no known config location on ${t.platform} — skipped.`);return}return t.logger.info(`${e.label}: add this to ${d(n,t.cwd)}
${e.renderSnippet(r.name,r.spec)}`),{action:"printed",path:n}},Ae=e=>{const r=[];return e.localOnly!==!0&&r.push({allowScopeFallback:!0,name:A,preferredScope:"global",spec:_e(e.docsUrl??Oe)}),e.docsOnly!==!0&&Ce(e.cwd)&&r.push({allowScopeFallback:!1,name:U,preferredScope:"project",spec:xe(re(e.cwd))}),r},Ue=e=>{const r=e.home??w(),t=e.platform??process.platform;e.logger.info("Supported MCP clients:");for(const n of g){const a=n.paths({home:r,platform:t,projectRoot:e.cwd}),o=l=>{const c=a[l];return c===void 0?"—":d(c,e.cwd)};e.logger.info(` ${n.id.padEnd(15)} ${n.label.padEnd(24)} project: ${o("project").padEnd(30)} global: ${o("global")}`)}return{code:0,written:[]}},De=e=>e.docsOnly===!0&&e.localOnly===!0?"`--docs-only` and `--local-only` are mutually exclusive — pass at most one.":"`--local-only` was set but this directory is not a Lunora project (no `lunora/` directory and wrangler config).",Pe=(e,r,t)=>{const n=[];for(const a of e)for(const o of r){const l=a.format==="json"?Le(a,o,t):Ne(a,o,t);l!==void 0&&n.push({action:l.action,client:a.id,path:l.path,server:o.name})}return n},Te=e=>{const r=e.home??w(),t=e.platform??process.platform,n=N(e.clients,()=>Ee({home:r,platform:t,projectRoot:e.cwd}),e.logger,"install");if(n===void 0)return{code:1,written:[]};if(n.length===0)return e.logger.error(`mcp install: no MCP client config found — name one explicitly, e.g. \`lunora mcp install claude-code\`. Known clients: ${x.join(", ")}.`),{code:1,written:[]};e.clients.length===0&&e.logger.info(`Detected ${String(n.length)} configured client(s): ${n.map(l=>l.label).join(", ")}.`);const a=Ae(e);if(a.length===0)return e.logger.error(`mcp install: nothing to install — ${De(e)}`),{code:1,written:[]};const o=Pe(n,a,{...e,home:r,platform:t});return o.some(l=>l.action==="created"||l.action==="updated")&&e.logger.info("Restart your editor (or reload its MCP servers) to pick up the change."),{code:o.some(l=>l.action==="invalid")?1:0,written:o}},P=100,T=1e3,E=e=>({content:[{text:JSON.stringify(e,void 0,2),type:"text"}]}),Fe=e=>({content:[{text:e,type:"text"}]}),Ie=e=>{const r=typeof e=="string"?Number(e):e;return typeof r!="number"||!Number.isFinite(r)?P:Math.min(T,Math.max(1,Math.floor(r)))},j=(e,r)=>{if(r===void 0)return;const t=h(e,ne);return le(t,h(e,r))?h(e,r):void 0},Re=(e,r)=>{const t=v(e,"utf8").split(`
`);return t.at(-1)===""&&t.pop(),t.slice(-r).join(`
`)},Je=e=>[{definition:{description:"Report whether a `lunora dev` server is running for this project, and its URL, studio URL, mode and uptime. Call this first when a query or mutation tool fails to connect.",inputSchema:{properties:{},type:"object"},name:"lunora_dev_status"},handle:async()=>{const r=b(e);return E(r===void 0?{hint:"Start it with `lunora dev` in the project directory.",running:!1}:{background:r.background===!0,logFile:j(e,r.logFile),mode:r.mode,pid:r.pid,running:!0,startedAt:r.startedAt,studioUrl:r.studioUrl,url:r.url})}},{definition:{description:"Return the tail of the running dev server's captured log — worker output, request errors, and stack traces. Only available when the server was started in the background (`lunora dev --background`); a foreground server prints to its own terminal.",inputSchema:{properties:{lines:{description:`How many trailing lines to return (default ${String(P)}, max ${String(T)})`,type:"number"}},type:"object"},name:"lunora_dev_logs"},handle:async r=>{const t=b(e);if(t===void 0)return{content:[{text:"no dev server is running — start one with `lunora dev`",type:"text"}],isError:!0};const n=j(e,t.logFile);if(n===void 0||!u(n))return{content:[{text:"this dev server is not capturing its output to a file (it runs in the foreground) — restart it with `lunora dev --background` to make its logs readable here",type:"text"}],isError:!0};const a=Ie(r.lines),o=Re(n,a);return Fe(o.length===0?"(the dev server log is empty)":o)}}],We=new Set(["127.0.0.1","::1","localhost"]),F=e=>{try{const{hostname:r}=new URL(e);return We.has(r.startsWith("[")?r.slice(1,-1):r)}catch{return!1}},O=e=>{const r=e.url??b(e.cwd)?.url;if(r===void 0||r.length===0)return;if(e.token!==void 0&&e.token.length>0)return{token:e.token,url:r};if(!F(r))return{url:r};const t=oe(e.cwd);return t===void 0||t.length===0?{url:r}:{token:t,url:r}},Ve=async e=>{await new Promise(r=>{const t=()=>{r()};e.onclose=t,process.stdin.once("close",t),process.stdin.once("end",t)})},ze=e=>{if(e===void 0)return"lunora mcp serve: ready (documentation tools). No dev server running yet — start `lunora dev` and the deployment tools start working, no restart needed.\n";if(e.token!==void 0)return`lunora mcp serve: ready — deployment at ${e.url}
`;const t=!F(e.url)?"the project's admin token was NOT sent to this non-local url; pass --token to authenticate deliberately":"no LUNORA_ADMIN_TOKEN in the environment or .dev.vars; the deployment tools will be refused until one is set";return`lunora mcp serve: ready — deployment at ${e.url} (${t})
`},Ge=async(e,r=ae)=>{const t=e.writeError??(o=>{process.stderr.write(o)});let n;try{n=await r({allowWrites:e.allowWrites===!0,deployment:()=>O(e),docs:e.noDocs===!0?!1:{...e.docsUrl===void 0?{}:{baseUrl:e.docsUrl}},extraTools:Je(e.cwd),version:e.version})}catch(o){const l=o instanceof Error?o.message:String(o);return t(`lunora mcp serve: failed to start — ${l}
`),{code:1,deployment:void 0}}const a=O(e);return t(ze(a)),await Ve(n),{code:0,deployment:a}},He=e=>{const r=[];return e.localOnly!==!0&&r.push(A),e.docsOnly!==!0&&r.push(U),r},Ke=(e,r,t,n)=>{const a=we({key:e.key,name:r,path:t}),o=d(t,n.cwd);return a.action==="removed"?n.logger.success(`${e.label}: removed "${r}" from ${o}.`):a.action==="invalid"&&n.logger.error(`${e.label}: ${o} is not valid JSON (${a.error??"unknown error"}) — left untouched.`),{action:a.action,path:t}},qe=(e,r,t,n)=>{const a=M({key:e.key,name:r,path:t}),o=d(t,n.cwd);return a==="invalid"?(n.logger.error(`${e.label}: ${o} is not valid JSON — a real run would report it and exit non-zero.`),{action:"invalid",path:t}):a==="absent"?{action:"absent",path:t}:(n.logger.info(`${e.label}: would remove "${r}" from ${o}.`),{action:"removed",path:t})},Be=(e,r,t)=>e.flatMap(n=>ke(n,r)).filter(je).filter(n=>t===void 0||n.scope===t),Xe=e=>{const r=e.home??w(),t=e.platform??process.platform,n=N(e.clients,()=>g,e.logger,"uninstall");if(n===void 0)return{code:1,removed:[]};const a=He(e);if(a.length===0)return e.logger.error("mcp uninstall: nothing to remove — `--docs-only` and `--local-only` are mutually exclusive, pass at most one."),{code:1,removed:[]};const o=[],l={home:r,platform:t,projectRoot:e.cwd};for(const c of Be(n,l,e.scope))for(const i of a){const m=e.print===!0?qe(c.client,i,c.path,e):Ke(c.client,i,c.path,e);m.action!=="absent"&&o.push({...m,client:c.client.id,server:i})}return o.length===0?e.logger.info("Nothing to remove — no Lunora MCP servers were configured in the clients checked."):e.print===!0&&e.logger.info("Nothing was changed — re-run without --print to remove these."),{code:o.some(c=>c.action==="invalid")?1:0,removed:o}},C=e=>e.global===!0?"global":e.project===!0?"project":void 0,Ze=async(e,r)=>{const{code:t}=await Ge({allowWrites:r.allowWrites===!0,cwd:e,noDocs:r.docs===!1,version:I,...r.docsUrl===void 0?{}:{docsUrl:r.docsUrl},...r.token===void 0?{}:{token:r.token},...r.url===void 0?{}:{url:r.url}});return{code:t}},ur=R(async({argument:e,cwd:r,logger:t,options:n})=>{const[a,...o]=e;if(a==="serve")return Ze(r,n);if(a==="uninstall"){const l=C(n),{code:c}=Xe({clients:o,cwd:r,docsOnly:n.docsOnly===!0,localOnly:n.localOnly===!0,logger:t,print:n.print===!0,...l===void 0?{}:{scope:l}});return{code:c}}if(a==="install"){if(n.list===!0)return Ue({cwd:r,logger:t});const l=C(n),{code:c}=Te({clients:o,cwd:r,docsOnly:n.docsOnly===!0,force:n.force===!0,localOnly:n.localOnly===!0,logger:t,print:n.print===!0,...l===void 0?{}:{scope:l},...n.docsUrl===void 0?{}:{docsUrl:n.docsUrl}});return{code:c}}return t.error("mcp: unknown subcommand. Usage: lunora mcp <install|uninstall|serve>"),{code:1}});export{ur as execute};