@lunora/cli
Version:
The Lunora CLI: init, dev, deploy, codegen, migrate, seed, doctor, insights, logs, registry, and the rest of the project commands
16 lines (14 loc) • 16.4 kB
JavaScript
import{V as te}from"./cli.mjs";import{d as re}from"../packem_shared/command-B6vu47iR.mjs";import{EXIT_CODE as f}from"../packem_shared/EXIT_CODE-DVjUgIaG.mjs";import{existsSync as m,readFileSync as F,statSync as ne,watch as oe}from"node:fs";import{homedir as C}from"node:os";import{findWranglerFile as le}from"@lunora/config/cloudflare";import{join as i,relative as ae}from"@visulima/path";import{stringify as ce}from"smol-toml";import{h as se,u as ie,i as de,r as ue}from"../packem_shared/mcp-config-file-GR1Y_CVw.mjs";import{detectPackageManager as me,execArgsFor as fe}from"@lunora/config/cli";import{resolve as $,join as pe,dirname as L}from"node:path";import{readLiveDevServerState as b,DEV_STATE_DIR as ge}from"@lunora/config";import{resolveAdminToken as he}from"@lunora/config/studio-host";import{connectLocalStdio as ve,connectFunctionStdio as ye}from"@lunora/mcp";import{i as we}from"../packem_shared/path-containment-CgxYZggb.mjs";const g=e=>({args:[...e.args],command:e.command,...e.env===void 0?{}:{env:e.env}}),j=e=>e.transport==="http"?{type:"http",url:e.url}:g(e),be=e=>e.transport==="http"?{httpUrl:e.url}:g(e),Se=e=>e.transport==="http"?{serverUrl:e.url}:g(e),ke=e=>({args:["-y","mcp-remote",e],command:"npx"}),Ee=e=>e.transport==="http"?ke(e.url):g(e),$e=e=>e.transport==="http"?{url:e.url}:g(e),je=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}},Oe=e=>e.transport==="http"?{disabled:!1,type:"streamableHttp",url:e.url}:{disabled:!1,...g(e)},Ce=({home:e,platform:t})=>{if(t==="darwin")return{global:i(e,"Library","Application Support","Claude","claude_desktop_config.json")};if(t==="win32"){const r=process.env.APPDATA;return r===void 0||r===""?{}:{global:i(r,"Claude","claude_desktop_config.json")}}return t==="linux"?{global:i(e,".config","Claude","claude_desktop_config.json")}:{}},_e=(e,t)=>ce({mcp_servers:{[e]:t.transport==="http"?{url:t.url}:{args:[...t.args],command:t.command,...t.env===void 0?{}:{env:t.env}}}}),v=[{buildEntry:j,paths:({projectRoot:e})=>({project:i(e,".mcp.json")}),format:"json",id:"claude-code",key:"mcpServers",label:"Claude Code"},{buildEntry:j,paths:({home:e,projectRoot:t})=>({global:i(e,".cursor","mcp.json"),project:i(t,".cursor","mcp.json")}),format:"json",id:"cursor",key:"mcpServers",label:"Cursor"},{buildEntry:j,paths:({projectRoot:e})=>({project:i(e,".vscode","mcp.json")}),format:"json",id:"vscode",key:"servers",label:"VS Code (GitHub Copilot)"},{buildEntry:be,paths:({home:e,projectRoot:t})=>({global:i(e,".gemini","settings.json"),project:i(t,".gemini","settings.json")}),format:"json",id:"gemini",key:"mcpServers",label:"Gemini CLI"},{buildEntry:Ee,paths:Ce,format:"json",id:"claude-desktop",key:"mcpServers",label:"Claude Desktop"},{buildEntry:Se,paths:({home:e})=>({global:i(e,".codeium","windsurf","mcp_config.json")}),format:"json",id:"windsurf",key:"mcpServers",label:"Windsurf"},{buildEntry:je,paths:({home:e,projectRoot:t})=>({global:i(e,".config","opencode","opencode.json"),project:i(t,"opencode.json")}),format:"json",id:"opencode",key:"mcp",label:"OpenCode"},{buildEntry:Oe,paths:({home:e})=>({global:i(e,".cline","mcp.json")}),format:"json",id:"cline",key:"mcpServers",label:"Cline"},{buildEntry:$e,paths:({home:e})=>({global:i(e,".config","zed","settings.json")}),format:"json",id:"zed",key:"context_servers",label:"Zed"},{paths:({home:e,projectRoot:t})=>({global:i(e,".codex","config.toml"),project:i(t,".codex","config.toml")}),format:"manual",id:"codex",label:"Codex CLI",renderSnippet:_e}],P=v.map(e=>e.id),Me=e=>v.find(t=>t.id===e.toLowerCase()),Ue=["global","project"],p=(e,t)=>{const r=ae(t,e);return r.length>0&&!r.startsWith("..")?r:e},xe=(e,t)=>{const r=e.paths(t);return Ue.flatMap(n=>{const o=r[n];return o===void 0?[]:[{client:e,path:o,scope:n}]})},Le=(e,t,r,n)=>{const o=e.paths(t),l=r==="global"?"project":"global",a=o[r]??(n?void 0:o[l]);if(a!==void 0)return{client:e,path:a,scope:o[r]===void 0?l:r}},Ne=e=>v.filter(t=>Object.values(t.paths(e)).some(r=>m(r))),I=(e,t,r,n)=>{if(e.length===0)return t();const o=[];for(const l of e){const a=Me(l);if(a===void 0){r.error(`mcp ${n}: unknown client "${l}". Known clients: ${P.join(", ")}.`);return}o.push(a)}return o},Ae=e=>e.client.format==="json",De="https://lunora.sh/mcp",R="lunora-docs",H="lunora",Te=e=>m(i(e,"lunora"))&&le(e)!==void 0,Fe=e=>{const{args:t,command:r}=fe(e,"lunora",["mcp","serve"]);return{args:t,command:r,transport:"stdio"}},Pe=e=>({transport:"http",url:e}),Ie=e=>({home:e.home,platform:e.platform,projectRoot:e.cwd}),W=(e,t,r)=>Le(e,Ie(r),r.scope??t.preferredScope,r.scope!==void 0||!t.allowScopeFallback),Re=(e,t,r)=>{const n=W(e,t,r);if(n===void 0){r.logger.warn(`${e.label}: has no ${r.scope??t.preferredScope}-scoped config — skipped "${t.name}".`);return}const{path:o}=n,l=e.buildEntry(t.spec);if(r.print===!0){const d=p(o,r.cwd);return r.force!==!0&&se({key:e.key,name:t.name,path:o})?(r.logger.info(`${e.label}: "${t.name}" already configured in ${d} — a real install would skip it (re-run with --force to replace).`),{action:"printed",path:o}):(r.logger.info(`${e.label} → ${d}
${JSON.stringify({[e.key]:{[t.name]:l}},void 0,4)}`),{action:"printed",path:o})}const a=ie({entry:l,force:r.force===!0,key:e.key,name:t.name,path:o}),c=p(o,r.cwd);return a.action==="invalid"?r.logger.error(`${e.label}: ${c} is not valid JSON (${a.error??"unknown error"}) — left untouched.`):a.action==="skipped"?r.logger.info(`${e.label}: "${t.name}" already configured in ${c} — re-run with --force to replace it.`):r.logger.success(`${e.label}: ${a.action==="created"?"created":"updated"} ${c} with "${t.name}".`),{action:a.action,path:o}},He=(e,t,r)=>{const n=W(e,t,r)?.path;if(n===void 0){r.logger.warn(`${e.label}: no known config location on ${r.platform} — skipped.`);return}return r.logger.info(`${e.label}: add this to ${p(n,r.cwd)}
${e.renderSnippet(t.name,t.spec)}`),{action:"printed",path:n}},We=e=>{const t=[];return e.localOnly!==!0&&t.push({allowScopeFallback:!0,name:R,preferredScope:"global",spec:Pe(e.docsUrl??De)}),e.docsOnly!==!0&&Te(e.cwd)&&t.push({allowScopeFallback:!1,name:H,preferredScope:"project",spec:Fe(me(e.cwd))}),t},Ge=e=>{const t=e.home??C(),r=e.platform??process.platform;e.logger.info("Supported MCP clients:");for(const n of v){const o=n.paths({home:t,platform:r,projectRoot:e.cwd}),l=a=>{const c=o[a];return c===void 0?"—":p(c,e.cwd)};e.logger.info(` ${n.id.padEnd(15)} ${n.label.padEnd(24)} project: ${l("project").padEnd(30)} global: ${l("global")}`)}return{code:0,written:[]}},Je=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).",Ve=(e,t,r)=>{const n=[];for(const o of e)for(const l of t){const a=o.format==="json"?Re(o,l,r):He(o,l,r);a!==void 0&&n.push({action:a.action,client:o.id,path:a.path,server:l.name})}return n},ze=e=>{const t=e.home??C(),r=e.platform??process.platform,n=I(e.clients,()=>Ne({home:t,platform:r,projectRoot:e.cwd}),e.logger,"install");if(n===void 0)return{code:f.USAGE,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: ${P.join(", ")}.`),{code:f.NOT_FOUND,written:[]};e.clients.length===0&&e.logger.info(`Detected ${String(n.length)} configured client(s): ${n.map(a=>a.label).join(", ")}.`);const o=We(e);if(o.length===0)return e.logger.error(`mcp install: nothing to install — ${Je(e)}`),{code:f.USAGE,written:[]};const l=Ve(n,o,{...e,home:t,platform:r});return l.some(a=>a.action==="created"||a.action==="updated")&&e.logger.info("Restart your editor (or reload its MCP servers) to pick up the change."),{code:l.some(a=>a.action==="invalid")?1:0,written:l}},G=100,J=1e3,N=e=>({content:[{text:JSON.stringify(e,void 0,2),type:"text"}]}),Ke=e=>({content:[{text:e,type:"text"}]}),qe=e=>{const t=typeof e=="string"?Number(e):e;return typeof t!="number"||!Number.isFinite(t)?G:Math.min(J,Math.max(1,Math.floor(t)))},A=(e,t)=>{if(t===void 0)return;const r=$(e,ge);return we(r,$(e,t))?$(e,t):void 0},Be=(e,t)=>{const r=F(e,"utf8").split(`
`);return r.at(-1)===""&&r.pop(),r.slice(-t).join(`
`)},Xe=e=>[{definition:{annotations:{destructiveHint:!1,idempotentHint:!0,openWorldHint:!1,readOnlyHint:!0,title:"Check the local dev server"},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 t=b(e);return N(t===void 0?{hint:"Start it with `lunora dev` in the project directory.",running:!1}:{background:t.background===!0,logFile:A(e,t.logFile),mode:t.mode,pid:t.pid,running:!0,startedAt:t.startedAt,studioUrl:t.studioUrl,url:t.url})}},{definition:{annotations:{destructiveHint:!1,idempotentHint:!0,openWorldHint:!1,readOnlyHint:!0,title:"Read the local dev server log"},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(G)}, max ${String(J)})`,type:"number"}},type:"object"},name:"lunora_dev_logs"},handle:async t=>{const r=b(e);if(r===void 0)return{content:[{text:"no dev server is running — start one with `lunora dev`",type:"text"}],isError:!0};const n=A(e,r.logFile);if(n===void 0||!m(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 o=qe(t.lines),l=Be(n,o);return Ke(l.length===0?"(the dev server log is empty)":l)}}],O={methods:[],openrpc:"1.3.2"},Ze=e=>e instanceof Error&&e.code==="ENOENT",Qe=new Set(["127.0.0.1","::1","localhost"]),V=e=>{try{const{hostname:t}=new URL(e);return Qe.has(t.startsWith("[")?t.slice(1,-1):t)}catch{return!1}},D=e=>{const t=e.url??b(e.cwd)?.url;if(t===void 0||t.length===0)return;if(e.token!==void 0&&e.token.length>0)return{token:e.token,url:t};if(!V(t))return{url:t};const r=he(e.cwd);return r===void 0||r.length===0?{url:t}:{token:r,url:t}},z=async e=>{await new Promise(t=>{const r=()=>{t()};e.onclose=r,process.stdin.once("close",r),process.stdin.once("end",r)})},Ye=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 r=!V(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} (${r})
`},K=e=>{process.stderr.write(e)},et=async(e,t)=>{const r=e.writeError??K,n=pe(e.cwd,"lunora","_generated","openrpc.json"),o=()=>e.url??b(e.cwd)?.url;let l,a;const c=s=>{r(`lunora mcp serve --functions: kept the last tool list — the rewritten document did not load: ${String(s)}
`)};let d={mtimeMs:-1,spec:O};const y=()=>{let s;try{s=ne(n).mtimeMs,a=void 0}catch(u){if(Ze(u))d.spec!==O&&(d={mtimeMs:-1,spec:O});else{const E=u.code??String(u);a!==E&&(a=E,c(u))}return d.spec}if(s!==d.mtimeMs)try{d={mtimeMs:s,spec:JSON.parse(F(n,"utf8"))}}catch(u){l!==s&&(l=s,c(u))}return d.spec};let w;try{w=await t({allowWrites:e.allowWrites===!0,onReloadError:c,openRpcSpec:y,url:o,version:e.version,...e.token===void 0||e.token.length===0?{}:{token:e.token}})}catch(s){const u=s instanceof Error?s.message:String(s);return r(`lunora mcp serve --functions: failed to start — ${u}
`),{code:1,deployment:void 0}}const q=e.token===void 0||e.token.length===0,S=o(),B=S===void 0||S.length===0?"no deployment yet (start `lunora dev` or pass --url; calls fail until one runs)":S,X=m(n)?"":" (no openrpc.json yet: run `lunora codegen --api-spec openrpc`; the functions appear once it exists)";r(`lunora mcp serve --functions: ready — ${B}${q?" (no --token: only public functions answer, and writes are refused)":""}${X}
`);const k=L(n),_=s=>{r(`lunora mcp serve --functions: the tool list will not be announced on a change — ${String(s)}
`)},Z=()=>{if(m(k))return k;const s=L(k);return m(s)?s:e.cwd};let M=m(n),h;const U=()=>{const s=Z();if(h?.directory!==s){h?.watcher.close(),h=void 0;try{const u=oe(s,(E,Q)=>{const x=m(n),Y=Q==="openrpc.json"||x!==M;M=x,U(),Y&&w.sendToolListChanged?.().catch(ee=>{r(`lunora mcp serve --functions: could not announce the tool list change — ${String(ee)}
`)})});u.on("error",_),h={directory:s,watcher:u}}catch(u){_(u)}}};U();try{await z(w)}finally{h?.watcher.close();try{await w.close?.()}catch(s){r(`lunora mcp serve --functions: could not close the server — ${String(s)}
`)}}return{code:0,deployment:void 0}},tt=async(e,t=ve,r=ye)=>{if(e.functions===!0)return et(e,r);const n=e.writeError??K;let o;try{o=await t({allowWrites:e.allowWrites===!0,deployment:()=>D(e),docs:e.noDocs===!0?!1:{...e.docsUrl===void 0?{}:{baseUrl:e.docsUrl}},extraTools:Xe(e.cwd),version:e.version})}catch(a){const c=a instanceof Error?a.message:String(a);return n(`lunora mcp serve: failed to start — ${c}
`),{code:1,deployment:void 0}}const l=D(e);return n(Ye(l)),await z(o),{code:0,deployment:l}},rt=e=>{const t=[];return e.localOnly!==!0&&t.push(R),e.docsOnly!==!0&&t.push(H),t},nt=(e,t,r,n)=>{const o=ue({key:e.key,name:t,path:r}),l=p(r,n.cwd);return o.action==="removed"?n.logger.success(`${e.label}: removed "${t}" from ${l}.`):o.action==="invalid"&&n.logger.error(`${e.label}: ${l} is not valid JSON (${o.error??"unknown error"}) — left untouched.`),{action:o.action,path:r}},ot=(e,t,r,n)=>{const o=de({key:e.key,name:t,path:r}),l=p(r,n.cwd);return o==="invalid"?(n.logger.error(`${e.label}: ${l} is not valid JSON — a real run would report it and exit non-zero.`),{action:"invalid",path:r}):o==="absent"?{action:"absent",path:r}:(n.logger.info(`${e.label}: would remove "${t}" from ${l}.`),{action:"removed",path:r})},lt=(e,t,r)=>e.flatMap(n=>xe(n,t)).filter(Ae).filter(n=>r===void 0||n.scope===r),at=e=>{const t=e.home??C(),r=e.platform??process.platform,n=I(e.clients,()=>v,e.logger,"uninstall");if(n===void 0)return{code:f.USAGE,removed:[]};const o=rt(e);if(o.length===0)return e.logger.error("mcp uninstall: nothing to remove — `--docs-only` and `--local-only` are mutually exclusive, pass at most one."),{code:f.USAGE,removed:[]};const l=[],a={home:t,platform:r,projectRoot:e.cwd};for(const c of lt(n,a,e.scope))for(const d of o){const y=e.print===!0?ot(c.client,d,c.path,e):nt(c.client,d,c.path,e);y.action!=="absent"&&l.push({...y,client:c.client.id,server:d})}return l.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:l.some(c=>c.action==="invalid")?1:0,removed:l}},T=e=>e.global===!0?"global":e.project===!0?"project":void 0,ct=async(e,t)=>{const{code:r}=await tt({allowWrites:t.allowWrites===!0,cwd:e,functions:t.functions===!0,noDocs:t.docs===!1,version:te,...t.docsUrl===void 0?{}:{docsUrl:t.docsUrl},...t.token===void 0?{}:{token:t.token},...t.url===void 0?{}:{url:t.url}});return{code:r}},Et=re(async({argument:e,cwd:t,logger:r,options:n})=>{const[o,...l]=e;if(o==="serve")return ct(t,n);if(o==="uninstall"){const a=T(n),{code:c}=at({clients:l,cwd:t,docsOnly:n.docsOnly===!0,localOnly:n.localOnly===!0,logger:r,print:n.print===!0,...a===void 0?{}:{scope:a}});return{code:c}}if(o==="install"){if(n.list===!0)return Ge({cwd:t,logger:r});const a=T(n),{code:c}=ze({clients:l,cwd:t,docsOnly:n.docsOnly===!0,force:n.force===!0,localOnly:n.localOnly===!0,logger:r,print:n.print===!0,...a===void 0?{}:{scope:a},...n.docsUrl===void 0?{}:{docsUrl:n.docsUrl}});return{code:c}}return r.error("mcp: unknown subcommand. Usage: lunora mcp <install|uninstall|serve>"),{code:f.USAGE}});export{Et as execute};