@lunora/cli
Version:
The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands
12 lines (10 loc) • 15.8 kB
JavaScript
import{VERSION as J}from"../packem_shared/COMMANDS-DmB3_936.mjs";import{i as W}from"../packem_shared/command-Bjv4VmYA.mjs";import{existsSync as u,readFileSync as y,realpathSync as D,statSync as T,writeFileSync as z,chmodSync as I,renameSync as H,unlinkSync as K,mkdirSync as q}from"node:fs";import{homedir as w}from"node:os";import{findWranglerFile as G,readLiveDevServerState as b}from"@lunora/config";import{join as s,dirname as V,relative as Z}from"@visulima/path";import{P as B,M as Q}from"../packem_shared/detect-package-manager-B2eIuAiP.mjs";import{stringify as X}from"smol-toml";import{modify as x,applyEdits as E,parse as Y}from"jsonc-parser";import{resolveAdminToken as oo}from"@lunora/config/studio-host";import{connectLocalStdio as to}from"@lunora/mcp";const m=o=>({args:[...o.args],command:o.command,...o.env===void 0?{}:{env:o.env}}),$=o=>o.transport==="http"?{type:"http",url:o.url}:m(o),eo=o=>o.transport==="http"?{httpUrl:o.url}:m(o),ro=o=>o.transport==="http"?{serverUrl:o.url}:m(o),no=o=>({args:["-y","mcp-remote",o],command:"npx"}),ao=o=>o.transport==="http"?no(o.url):m(o),io=o=>o.transport==="http"?{url:o.url}:m(o),lo=o=>o.transport==="http"?{enabled:!0,type:"remote",url:o.url}:{command:[o.command,...o.args],enabled:!0,type:"local",...o.env===void 0?{}:{environment:o.env}},so=o=>o.transport==="http"?{disabled:!1,type:"streamableHttp",url:o.url}:{disabled:!1,...m(o)},co=({home:o,platform:t})=>{if(t==="darwin")return{global:s(o,"Library","Application Support","Claude","claude_desktop_config.json")};if(t==="win32"){const e=process.env.APPDATA;return e===void 0||e===""?{}:{global:s(e,"Claude","claude_desktop_config.json")}}return t==="linux"?{global:s(o,".config","Claude","claude_desktop_config.json")}:{}},po=(o,t)=>X({mcp_servers:{[o]:t.transport==="http"?{url:t.url}:{args:[...t.args],command:t.command,...t.env===void 0?{}:{env:t.env}}}}),h=[{buildEntry:$,paths:({projectRoot:o})=>({project:s(o,".mcp.json")}),format:"json",id:"claude-code",key:"mcpServers",label:"Claude Code"},{buildEntry:$,paths:({home:o,projectRoot:t})=>({global:s(o,".cursor","mcp.json"),project:s(t,".cursor","mcp.json")}),format:"json",id:"cursor",key:"mcpServers",label:"Cursor"},{buildEntry:$,paths:({projectRoot:o})=>({project:s(o,".vscode","mcp.json")}),format:"json",id:"vscode",key:"servers",label:"VS Code (GitHub Copilot)"},{buildEntry:eo,paths:({home:o,projectRoot:t})=>({global:s(o,".gemini","settings.json"),project:s(t,".gemini","settings.json")}),format:"json",id:"gemini",key:"mcpServers",label:"Gemini CLI"},{buildEntry:ao,paths:co,format:"json",id:"claude-desktop",key:"mcpServers",label:"Claude Desktop"},{buildEntry:ro,paths:({home:o})=>({global:s(o,".codeium","windsurf","mcp_config.json")}),format:"json",id:"windsurf",key:"mcpServers",label:"Windsurf"},{buildEntry:lo,paths:({home:o,projectRoot:t})=>({global:s(o,".config","opencode","opencode.json"),project:s(t,"opencode.json")}),format:"json",id:"opencode",key:"mcp",label:"OpenCode"},{buildEntry:so,paths:({home:o})=>({global:s(o,".cline","mcp.json")}),format:"json",id:"cline",key:"mcpServers",label:"Cline"},{buildEntry:io,paths:({home:o})=>({global:s(o,".config","zed","settings.json")}),format:"json",id:"zed",key:"context_servers",label:"Zed"},{paths:({home:o,projectRoot:t})=>({global:s(o,".codex","config.toml"),project:s(t,".codex","config.toml")}),format:"manual",id:"codex",label:"Codex CLI",renderSnippet:po}],C=h.map(o=>o.id),uo=o=>h.find(t=>t.id===o.toLowerCase()),v=(o,t)=>{let e=o,r;try{e=D(o),r=T(e).mode}catch{}const n=`${e}.lunora-${Math.random().toString(36).slice(2,10)}.tmp`;try{z(n,t,"utf8"),r!==void 0&&I(n,r),H(n,e)}catch(a){try{K(n)}catch{}throw a}},R={formattingOptions:{insertSpaces:!0,tabSize:4}},k=o=>{const t=[],e=Y(o,t,{allowTrailingComma:!0});return{errors:t,value:e}},g=o=>typeof o=="object"&&o!==null&&!Array.isArray(o),j=(o,t,e)=>{if(!g(o))return;const r=o[t];return g(r)?r[e]:void 0},N=o=>{if(!u(o.path))return"absent";let t;try{t=y(o.path,"utf8")}catch{return"invalid"}if(t.trim().length===0)return"absent";const{errors:e,value:r}=k(t);return e.length>0||!g(r)?"invalid":j(r,o.key,o.name)===void 0?"absent":"present"},mo=o=>N(o)==="present",fo=o=>{const{entry:t,force:e=!1,key:r,name:n,path:a}=o;if(!u(a))return q(V(a),{recursive:!0}),v(a,`${JSON.stringify({[r]:{[n]:t}},void 0,4)}
`),{action:"created",path:a};const i=y(a,"utf8");if(i.trim().length===0)return v(a,`${JSON.stringify({[r]:{[n]:t}},void 0,4)}
`),{action:"created",path:a};const{errors:l,value:c}=k(i);if(l.length>0)return{action:"invalid",error:`parse error at offset ${String(l[0]?.offset??0)}`,path:a};if(!g(c))return{action:"invalid",error:"the file's root is not a JSON object",path:a};const f=c[r];if(f!==void 0&&!g(f))return{action:"invalid",error:`"${r}" is not an object`,path:a};if(j(c,r,n)!==void 0&&!e)return{action:"skipped",path:a};try{const d=x(i,[r,n],t,R);d.length>0&&v(a,E(i,d))}catch(d){return{action:"invalid",error:d instanceof Error?d.message:String(d),path:a}}return{action:"updated",path:a}},go=o=>{const{key:t,name:e,path:r}=o;if(!u(r))return{action:"absent",path:r};let n;try{n=y(r,"utf8")}catch(l){return{action:"invalid",error:l instanceof Error?l.message:String(l),path:r}}const{errors:a,value:i}=k(n);if(a.length>0)return{action:"invalid",error:`parse error at offset ${String(a[0]?.offset??0)}`,path:r};if(j(i,t,e)===void 0)return{action:"absent",path:r};try{const l=x(n,[t,e],void 0,R);l.length>0&&v(r,E(n,l))}catch(l){return{action:"invalid",error:l instanceof Error?l.message:String(l),path:r}}return{action:"removed",path:r}},ho=["global","project"],p=(o,t)=>{const e=Z(t,o);return e.length>0&&!e.startsWith("..")?e:o},vo=(o,t)=>{const e=o.paths(t);return ho.flatMap(r=>{const n=e[r];return n===void 0?[]:[{client:o,path:n,scope:r}]})},yo=(o,t,e,r)=>{const n=o.paths(t),a=e==="global"?"project":"global",i=n[e]??(r?void 0:n[a]);if(i!==void 0)return{client:o,path:i,scope:n[e]===void 0?a:e}},$o=o=>h.filter(t=>Object.values(t.paths(o)).some(e=>u(e))),U=(o,t,e,r)=>{if(o.length===0)return t();const n=[];for(const a of o){const i=uo(a);if(i===void 0){e.error(`mcp ${r}: unknown client "${a}". Known clients: ${C.join(", ")}.`);return}n.push(i)}return n},bo=o=>o.client.format==="json",wo="https://lunora.sh/mcp",_="lunora-docs",M="lunora",ko=o=>u(s(o,"lunora"))&&G(o)!==void 0,jo=o=>{const{args:t,command:e}=Q(o,"lunora",["mcp","serve"]);return{args:t,command:e,transport:"stdio"}},So=o=>({transport:"http",url:o}),Oo=o=>({home:o.home,platform:o.platform,projectRoot:o.cwd}),A=(o,t,e)=>yo(o,Oo(e),e.scope??t.preferredScope,e.scope!==void 0||!t.allowScopeFallback),xo=(o,t,e)=>{const r=A(o,t,e);if(r===void 0){e.logger.warn(`${o.label}: has no ${e.scope??t.preferredScope}-scoped config — skipped "${t.name}".`);return}const{path:n}=r,a=o.buildEntry(t.spec);if(e.print===!0){const c=p(n,e.cwd);return e.force!==!0&&mo({key:o.key,name:t.name,path:n})?(e.logger.info(`${o.label}: "${t.name}" already configured in ${c} — a real install would skip it (re-run with --force to replace).`),{action:"printed",path:n}):(e.logger.info(`${o.label} → ${c}
${JSON.stringify({[o.key]:{[t.name]:a}},void 0,4)}`),{action:"printed",path:n})}const i=fo({entry:a,force:e.force===!0,key:o.key,name:t.name,path:n}),l=p(n,e.cwd);return i.action==="invalid"?e.logger.error(`${o.label}: ${l} is not valid JSON (${i.error??"unknown error"}) — left untouched.`):i.action==="skipped"?e.logger.info(`${o.label}: "${t.name}" already configured in ${l} — re-run with --force to replace it.`):e.logger.success(`${o.label}: ${i.action==="created"?"created":"updated"} ${l} with "${t.name}".`),{action:i.action,path:n}},Eo=(o,t,e)=>{const r=A(o,t,e)?.path;if(r===void 0){e.logger.warn(`${o.label}: no known config location on ${e.platform} — skipped.`);return}return e.logger.info(`${o.label}: add this to ${p(r,e.cwd)}
${o.renderSnippet(t.name,t.spec)}`),{action:"printed",path:r}},Co=o=>{const t=[];return o.localOnly!==!0&&t.push({allowScopeFallback:!0,name:_,preferredScope:"global",spec:So(o.docsUrl??wo)}),o.docsOnly!==!0&&ko(o.cwd)&&t.push({allowScopeFallback:!1,name:M,preferredScope:"project",spec:jo(B(o.cwd))}),t},Ro=o=>{const t=o.home??w(),e=o.platform??process.platform;o.logger.info("Supported MCP clients:");for(const r of h){const n=r.paths({home:t,platform:e,projectRoot:o.cwd}),a=i=>{const l=n[i];return l===void 0?"—":p(l,o.cwd)};o.logger.info(` ${r.id.padEnd(15)} ${r.label.padEnd(24)} project: ${a("project").padEnd(30)} global: ${a("global")}`)}return{code:0,written:[]}},No=o=>o.docsOnly===!0&&o.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).",Uo=(o,t,e)=>{const r=[];for(const n of o)for(const a of t){const i=n.format==="json"?xo(n,a,e):Eo(n,a,e);i!==void 0&&r.push({action:i.action,client:n.id,path:i.path,server:a.name})}return r},_o=o=>{const t=o.home??w(),e=o.platform??process.platform,r=U(o.clients,()=>$o({home:t,platform:e,projectRoot:o.cwd}),o.logger,"install");if(r===void 0)return{code:1,written:[]};if(r.length===0)return o.logger.error(`mcp install: no MCP client config found — name one explicitly, e.g. \`lunora mcp install claude-code\`. Known clients: ${C.join(", ")}.`),{code:1,written:[]};o.clients.length===0&&o.logger.info(`Detected ${String(r.length)} configured client(s): ${r.map(i=>i.label).join(", ")}.`);const n=Co(o);if(n.length===0)return o.logger.error(`mcp install: nothing to install — ${No(o)}`),{code:1,written:[]};const a=Uo(r,n,{...o,home:t,platform:e});return a.some(i=>i.action==="created"||i.action==="updated")&&o.logger.info("Restart your editor (or reload its MCP servers) to pick up the change."),{code:a.some(i=>i.action==="invalid")?1:0,written:a}},L=100,F=1e3,Mo=o=>({content:[{text:JSON.stringify(o,void 0,2),type:"text"}]}),Ao=o=>({content:[{text:o,type:"text"}]}),Lo=o=>{const t=typeof o=="string"?Number(o):o;return typeof t!="number"||!Number.isFinite(t)?L:Math.min(F,Math.max(1,Math.floor(t)))},Fo=(o,t)=>{const e=y(o,"utf8").split(`
`);return e.at(-1)===""&&e.pop(),e.slice(-t).join(`
`)},Po=o=>[{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 t=b(o);return Mo(t===void 0?{hint:"Start it with `lunora dev` in the project directory.",running:!1}:{background:t.background===!0,logFile:t.logFile,mode:t.mode,pid:t.pid,running:!0,startedAt:t.startedAt,studioUrl:t.studioUrl,url:t.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(L)}, max ${String(F)})`,type:"number"}},type:"object"},name:"lunora_dev_logs"},handle:async t=>{const e=b(o);if(e===void 0)return{content:[{text:"no dev server is running — start one with `lunora dev`",type:"text"}],isError:!0};if(e.logFile===void 0||!u(e.logFile))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 r=Lo(t.lines),n=Fo(e.logFile,r);return Ao(n.length===0?"(the dev server log is empty)":n)}}],Jo=new Set(["127.0.0.1","::1","localhost"]),P=o=>{try{const{hostname:t}=new URL(o);return Jo.has(t.startsWith("[")?t.slice(1,-1):t)}catch{return!1}},S=o=>{const t=o.url??b(o.cwd)?.url;if(t===void 0||t.length===0)return;if(o.token!==void 0&&o.token.length>0)return{token:o.token,url:t};if(o.url!==void 0&&!P(o.url))return{url:t};const e=oo(o.cwd);return e===void 0||e.length===0?{url:t}:{token:e,url:t}},Wo=async o=>{await new Promise(t=>{const e=()=>{t()};o.onclose=e,process.stdin.once("close",e),process.stdin.once("end",e)})},Do=(o,t)=>{if(o===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(o.token!==void 0)return`lunora mcp serve: ready — deployment at ${o.url}
`;const e=t.url!==void 0&&!P(t.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 ${o.url} (${e})
`},To=async(o,t=to)=>{const e=o.writeError??(a=>{process.stderr.write(a)});let r;try{r=await t({allowWrites:o.allowWrites===!0,deployment:()=>S(o),docs:o.noDocs===!0?!1:{...o.docsUrl===void 0?{}:{baseUrl:o.docsUrl}},extraTools:Po(o.cwd),version:o.version})}catch(a){const i=a instanceof Error?a.message:String(a);return e(`lunora mcp serve: failed to start — ${i}
`),{code:1,deployment:void 0}}const n=S(o);return e(Do(n,o)),await Wo(r),{code:0,deployment:n}},zo=o=>{const t=[];return o.localOnly!==!0&&t.push(_),o.docsOnly!==!0&&t.push(M),t},Io=(o,t,e,r)=>{const n=go({key:o.key,name:t,path:e}),a=p(e,r.cwd);return n.action==="removed"?r.logger.success(`${o.label}: removed "${t}" from ${a}.`):n.action==="invalid"&&r.logger.error(`${o.label}: ${a} is not valid JSON (${n.error??"unknown error"}) — left untouched.`),{action:n.action,path:e}},Ho=(o,t,e,r)=>{const n=N({key:o.key,name:t,path:e}),a=p(e,r.cwd);return n==="invalid"?(r.logger.error(`${o.label}: ${a} is not valid JSON — a real run would report it and exit non-zero.`),{action:"invalid",path:e}):n==="absent"?{action:"absent",path:e}:(r.logger.info(`${o.label}: would remove "${t}" from ${a}.`),{action:"removed",path:e})},Ko=(o,t,e)=>o.flatMap(r=>vo(r,t)).filter(bo).filter(r=>e===void 0||r.scope===e),qo=o=>{const t=o.home??w(),e=o.platform??process.platform,r=U(o.clients,()=>h,o.logger,"uninstall");if(r===void 0)return{code:1,removed:[]};const n=zo(o);if(n.length===0)return o.logger.error("mcp uninstall: nothing to remove — `--docs-only` and `--local-only` are mutually exclusive, pass at most one."),{code:1,removed:[]};const a=[],i={home:t,platform:e,projectRoot:o.cwd};for(const l of Ko(r,i,o.scope))for(const c of n){const f=o.print===!0?Ho(l.client,c,l.path,o):Io(l.client,c,l.path,o);f.action!=="absent"&&a.push({...f,client:l.client.id,server:c})}return a.length===0?o.logger.info("Nothing to remove — no Lunora MCP servers were configured in the clients checked."):o.print===!0&&o.logger.info("Nothing was changed — re-run without --print to remove these."),{code:a.some(l=>l.action==="invalid")?1:0,removed:a}},O=o=>o.global===!0?"global":o.project===!0?"project":void 0,Go=async(o,t)=>{const{code:e}=await To({allowWrites:t.allowWrites===!0,cwd:o,noDocs:t.docs===!1,version:J,...t.docsUrl===void 0?{}:{docsUrl:t.docsUrl},...t.token===void 0?{}:{token:t.token},...t.url===void 0?{}:{url:t.url}});return{code:e}},at=W(async({argument:o,cwd:t,logger:e,options:r})=>{const[n,...a]=o;if(n==="serve")return Go(t,r);if(n==="uninstall"){const i=O(r),{code:l}=qo({clients:a,cwd:t,docsOnly:r.docsOnly===!0,localOnly:r.localOnly===!0,logger:e,print:r.print===!0,...i===void 0?{}:{scope:i}});return{code:l}}if(n==="install"){if(r.list===!0)return Ro({cwd:t,logger:e});const i=O(r),{code:l}=_o({clients:a,cwd:t,docsOnly:r.docsOnly===!0,force:r.force===!0,localOnly:r.localOnly===!0,logger:e,print:r.print===!0,...i===void 0?{}:{scope:i},...r.docsUrl===void 0?{}:{docsUrl:r.docsUrl}});return{code:l}}return e.error("mcp: unknown subcommand. Usage: lunora mcp <install|uninstall|serve>"),{code:1}});export{at as execute};