@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.8 kB
JavaScript
import{i as L}from"../packem_shared/cli-DiEQDidI.mjs";import{i as J}from"../packem_shared/command-0l-ZPhIX.mjs";import{existsSync as p,readFileSync as h,mkdirSync as P,realpathSync as W,statSync as D,writeFileSync as T,chmodSync as z,renameSync as H,unlinkSync as I}from"node:fs";import{homedir as $}from"node:os";import{findWranglerFile as K}from"@lunora/config/cloudflare";import{join as s,dirname as q,relative as G}from"@visulima/path";import{l as V,M as Z}from"../packem_shared/detect-package-manager-DXDstphE.mjs";import{stringify as B}from"smol-toml";import{parse as Q,modify as X,applyEdits as Y}from"jsonc-parser";import{readLiveDevServerState as y}from"@lunora/config";import{resolveAdminToken as oo}from"@lunora/config/studio-host";import{connectLocalStdio as to}from"@lunora/mcp";const u=o=>({args:[...o.args],command:o.command,...o.env===void 0?{}:{env:o.env}}),v=o=>o.transport==="http"?{type:"http",url:o.url}:u(o),eo=o=>o.transport==="http"?{httpUrl:o.url}:u(o),ro=o=>o.transport==="http"?{serverUrl:o.url}:u(o),no=o=>({args:["-y","mcp-remote",o],command:"npx"}),ao=o=>o.transport==="http"?no(o.url):u(o),io=o=>o.transport==="http"?{url:o.url}:u(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,...u(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)=>B({mcp_servers:{[o]:t.transport==="http"?{url:t.url}:{args:[...t.args],command:t.command,...t.env===void 0?{}:{env:t.env}}}}),g=[{buildEntry:v,paths:({projectRoot:o})=>({project:s(o,".mcp.json")}),format:"json",id:"claude-code",key:"mcpServers",label:"Claude Code"},{buildEntry:v,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:v,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}],O=g.map(o=>o.id),uo=o=>g.find(t=>t.id===o.toLowerCase()),x=(o,t)=>{let e=o,r;try{e=W(o),r=D(e).mode}catch{}const a=`${e}.lunora-${Math.random().toString(36).slice(2,10)}.tmp`;try{T(a,t,"utf8"),r!==void 0&&z(a,r),H(a,e)}catch(n){try{I(a)}catch{}throw n}},mo={formattingOptions:{insertSpaces:!0,tabSize:4}},b=o=>{const t=[],e=Q(o,t,{allowTrailingComma:!0});return{errors:t,value:e}},f=o=>typeof o=="object"&&o!==null&&!Array.isArray(o),w=(o,t,e)=>{if(!f(o))return;const r=o[t];return f(r)?r[e]:void 0},C=o=>{if(!p(o.path))return"absent";let t;try{t=h(o.path,"utf8")}catch{return"invalid"}if(t.trim().length===0)return"absent";const{errors:e,value:r}=b(t);return e.length>0||!f(r)?"invalid":w(r,o.key,o.name)===void 0?"absent":"present"},fo=o=>C(o)==="present",E=(o,t,e,r,a)=>{try{const n=X(o,[e,r],a,mo);n.length>0&&x(t,Y(o,n));return}catch(n){return n instanceof Error?n.message:String(n)}},go=o=>{const{entry:t,force:e=!1,key:r,name:a,path:n}=o,i=p(n)?h(n,"utf8"):"";if(i.trim().length===0)return P(q(n),{recursive:!0}),x(n,`${JSON.stringify({[r]:{[a]:t}},void 0,4)}
`),{action:"created",path:n};const{errors:l,value:c}=b(i);if(l.length>0)return{action:"invalid",error:`parse error at offset ${String(l[0]?.offset??0)}`,path:n};if(!f(c))return{action:"invalid",error:"the file's root is not a JSON object",path:n};const m=c[r];if(m!==void 0&&!f(m))return{action:"invalid",error:`"${r}" is not an object`,path:n};if(w(c,r,a)!==void 0&&!e)return{action:"skipped",path:n};const j=E(i,n,r,a,t);return j===void 0?{action:"updated",path:n}:{action:"invalid",error:j,path:n}},ho=o=>{const{key:t,name:e,path:r}=o;if(!p(r))return{action:"absent",path:r};let a;try{a=h(r,"utf8")}catch(c){return{action:"invalid",error:c instanceof Error?c.message:String(c),path:r}}const{errors:n,value:i}=b(a);if(n.length>0)return{action:"invalid",error:`parse error at offset ${String(n[0]?.offset??0)}`,path:r};if(w(i,t,e)===void 0)return{action:"absent",path:r};const l=E(a,r,t,e,void 0);return l===void 0?{action:"removed",path:r}:{action:"invalid",error:l,path:r}},vo=["global","project"],d=(o,t)=>{const e=G(t,o);return e.length>0&&!e.startsWith("..")?e:o},yo=(o,t)=>{const e=o.paths(t);return vo.flatMap(r=>{const a=e[r];return a===void 0?[]:[{client:o,path:a,scope:r}]})},$o=(o,t,e,r)=>{const a=o.paths(t),n=e==="global"?"project":"global",i=a[e]??(r?void 0:a[n]);if(i!==void 0)return{client:o,path:i,scope:a[e]===void 0?n:e}},bo=o=>g.filter(t=>Object.values(t.paths(o)).some(e=>p(e))),R=(o,t,e,r)=>{if(o.length===0)return t();const a=[];for(const n of o){const i=uo(n);if(i===void 0){e.error(`mcp ${r}: unknown client "${n}". Known clients: ${O.join(", ")}.`);return}a.push(i)}return a},wo=o=>o.client.format==="json",jo="https://lunora.sh/mcp",U="lunora-docs",N="lunora",ko=o=>p(s(o,"lunora"))&&K(o)!==void 0,So=o=>{const{args:t,command:e}=Z(o,"lunora",["mcp","serve"]);return{args:t,command:e,transport:"stdio"}},Oo=o=>({transport:"http",url:o}),xo=o=>({home:o.home,platform:o.platform,projectRoot:o.cwd}),_=(o,t,e)=>$o(o,xo(e),e.scope??t.preferredScope,e.scope!==void 0||!t.allowScopeFallback),Co=(o,t,e)=>{const r=_(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:a}=r,n=o.buildEntry(t.spec);if(e.print===!0){const c=d(a,e.cwd);return e.force!==!0&&fo({key:o.key,name:t.name,path:a})?(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:a}):(e.logger.info(`${o.label} → ${c}
${JSON.stringify({[o.key]:{[t.name]:n}},void 0,4)}`),{action:"printed",path:a})}const i=go({entry:n,force:e.force===!0,key:o.key,name:t.name,path:a}),l=d(a,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:a}},Eo=(o,t,e)=>{const r=_(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 ${d(r,e.cwd)}
${o.renderSnippet(t.name,t.spec)}`),{action:"printed",path:r}},Ro=o=>{const t=[];return o.localOnly!==!0&&t.push({allowScopeFallback:!0,name:U,preferredScope:"global",spec:Oo(o.docsUrl??jo)}),o.docsOnly!==!0&&ko(o.cwd)&&t.push({allowScopeFallback:!1,name:N,preferredScope:"project",spec:So(V(o.cwd))}),t},Uo=o=>{const t=o.home??$(),e=o.platform??process.platform;o.logger.info("Supported MCP clients:");for(const r of g){const a=r.paths({home:t,platform:e,projectRoot:o.cwd}),n=i=>{const l=a[i];return l===void 0?"—":d(l,o.cwd)};o.logger.info(` ${r.id.padEnd(15)} ${r.label.padEnd(24)} project: ${n("project").padEnd(30)} global: ${n("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).",_o=(o,t,e)=>{const r=[];for(const a of o)for(const n of t){const i=a.format==="json"?Co(a,n,e):Eo(a,n,e);i!==void 0&&r.push({action:i.action,client:a.id,path:i.path,server:n.name})}return r},Mo=o=>{const t=o.home??$(),e=o.platform??process.platform,r=R(o.clients,()=>bo({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: ${O.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 a=Ro(o);if(a.length===0)return o.logger.error(`mcp install: nothing to install — ${No(o)}`),{code:1,written:[]};const n=_o(r,a,{...o,home:t,platform:e});return n.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:n.some(i=>i.action==="invalid")?1:0,written:n}},M=100,A=1e3,Ao=o=>({content:[{text:JSON.stringify(o,void 0,2),type:"text"}]}),Fo=o=>({content:[{text:o,type:"text"}]}),Lo=o=>{const t=typeof o=="string"?Number(o):o;return typeof t!="number"||!Number.isFinite(t)?M:Math.min(A,Math.max(1,Math.floor(t)))},Jo=(o,t)=>{const e=h(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=y(o);return Ao(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(M)}, max ${String(A)})`,type:"number"}},type:"object"},name:"lunora_dev_logs"},handle:async t=>{const e=y(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||!p(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),a=Jo(e.logFile,r);return Fo(a.length===0?"(the dev server log is empty)":a)}}],Wo=new Set(["127.0.0.1","::1","localhost"]),F=o=>{try{const{hostname:t}=new URL(o);return Wo.has(t.startsWith("[")?t.slice(1,-1):t)}catch{return!1}},k=o=>{const t=o.url??y(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&&!F(o.url))return{url:t};const e=oo(o.cwd);return e===void 0||e.length===0?{url:t}:{token:e,url:t}},Do=async o=>{await new Promise(t=>{const e=()=>{t()};o.onclose=e,process.stdin.once("close",e),process.stdin.once("end",e)})},To=(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&&!F(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})
`},zo=async(o,t=to)=>{const e=o.writeError??(n=>{process.stderr.write(n)});let r;try{r=await t({allowWrites:o.allowWrites===!0,deployment:()=>k(o),docs:o.noDocs===!0?!1:{...o.docsUrl===void 0?{}:{baseUrl:o.docsUrl}},extraTools:Po(o.cwd),version:o.version})}catch(n){const i=n instanceof Error?n.message:String(n);return e(`lunora mcp serve: failed to start — ${i}
`),{code:1,deployment:void 0}}const a=k(o);return e(To(a,o)),await Do(r),{code:0,deployment:a}},Ho=o=>{const t=[];return o.localOnly!==!0&&t.push(U),o.docsOnly!==!0&&t.push(N),t},Io=(o,t,e,r)=>{const a=ho({key:o.key,name:t,path:e}),n=d(e,r.cwd);return a.action==="removed"?r.logger.success(`${o.label}: removed "${t}" from ${n}.`):a.action==="invalid"&&r.logger.error(`${o.label}: ${n} is not valid JSON (${a.error??"unknown error"}) — left untouched.`),{action:a.action,path:e}},Ko=(o,t,e,r)=>{const a=C({key:o.key,name:t,path:e}),n=d(e,r.cwd);return a==="invalid"?(r.logger.error(`${o.label}: ${n} is not valid JSON — a real run would report it and exit non-zero.`),{action:"invalid",path:e}):a==="absent"?{action:"absent",path:e}:(r.logger.info(`${o.label}: would remove "${t}" from ${n}.`),{action:"removed",path:e})},qo=(o,t,e)=>o.flatMap(r=>yo(r,t)).filter(wo).filter(r=>e===void 0||r.scope===e),Go=o=>{const t=o.home??$(),e=o.platform??process.platform,r=R(o.clients,()=>g,o.logger,"uninstall");if(r===void 0)return{code:1,removed:[]};const a=Ho(o);if(a.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 n=[],i={home:t,platform:e,projectRoot:o.cwd};for(const l of qo(r,i,o.scope))for(const c of a){const m=o.print===!0?Ko(l.client,c,l.path,o):Io(l.client,c,l.path,o);m.action!=="absent"&&n.push({...m,client:l.client.id,server:c})}return n.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:n.some(l=>l.action==="invalid")?1:0,removed:n}},S=o=>o.global===!0?"global":o.project===!0?"project":void 0,Vo=async(o,t)=>{const{code:e}=await zo({allowWrites:t.allowWrites===!0,cwd:o,noDocs:t.docs===!1,version:L,...t.docsUrl===void 0?{}:{docsUrl:t.docsUrl},...t.token===void 0?{}:{token:t.token},...t.url===void 0?{}:{url:t.url}});return{code:e}},lt=J(async({argument:o,cwd:t,logger:e,options:r})=>{const[a,...n]=o;if(a==="serve")return Vo(t,r);if(a==="uninstall"){const i=S(r),{code:l}=Go({clients:n,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(a==="install"){if(r.list===!0)return Uo({cwd:t,logger:e});const i=S(r),{code:l}=Mo({clients:n,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{lt as execute};