UNPKG

@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) 16 kB
import{V as R}from"../packem_shared/cli-DOpChqUe.mjs";import{d as G}from"../packem_shared/command-Mmxzxyr5.mjs";import{EXIT_CODE as d}from"../packem_shared/EXIT_CODE-08cwt3MK.mjs";import{existsSync as f,readFileSync as h,mkdirSync as J,realpathSync as W,statSync as V,writeFileSync as z,chmodSync as H,renameSync as K,unlinkSync as q}from"node:fs";import{homedir as w}from"node:os";import{findWranglerFile as X}from"@lunora/config/cloudflare";import{join as s,dirname as B,relative as Z}from"@visulima/path";import{stringify as Q}from"smol-toml";import{parse as Y,modify as ee,applyEdits as re}from"jsonc-parser";import{detectPackageManager as te,execArgsFor as ne,readLiveDevServerState as S,DEV_STATE_DIR as oe}from"@lunora/config";import{resolveAdminToken as ae}from"@lunora/config/studio-host";import{connectLocalStdio as le}from"@lunora/mcp";import{resolve as y}from"node:path";import{i as ce}from"../packem_shared/path-containment-CgxYZggb.mjs";const m=e=>({args:[...e.args],command:e.command,...e.env===void 0?{}:{env:e.env}}),b=e=>e.transport==="http"?{type:"http",url:e.url}:m(e),se=e=>e.transport==="http"?{httpUrl:e.url}:m(e),ie=e=>e.transport==="http"?{serverUrl:e.url}:m(e),de=e=>({args:["-y","mcp-remote",e],command:"npx"}),ue=e=>e.transport==="http"?de(e.url):m(e),fe=e=>e.transport==="http"?{url:e.url}:m(e),me=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}},pe=e=>e.transport==="http"?{disabled:!1,type:"streamableHttp",url:e.url}:{disabled:!1,...m(e)},ge=({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")}:{}},ve=(e,r)=>Q({mcp_servers:{[e]:r.transport==="http"?{url:r.url}:{args:[...r.args],command:r.command,...r.env===void 0?{}:{env:r.env}}}}),v=[{buildEntry:b,paths:({projectRoot:e})=>({project:s(e,".mcp.json")}),format:"json",id:"claude-code",key:"mcpServers",label:"Claude Code"},{buildEntry:b,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:b,paths:({projectRoot:e})=>({project:s(e,".vscode","mcp.json")}),format:"json",id:"vscode",key:"servers",label:"VS Code (GitHub Copilot)"},{buildEntry:se,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:ue,paths:ge,format:"json",id:"claude-desktop",key:"mcpServers",label:"Claude Desktop"},{buildEntry:ie,paths:({home:e})=>({global:s(e,".codeium","windsurf","mcp_config.json")}),format:"json",id:"windsurf",key:"mcpServers",label:"Windsurf"},{buildEntry:me,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:pe,paths:({home:e})=>({global:s(e,".cline","mcp.json")}),format:"json",id:"cline",key:"mcpServers",label:"Cline"},{buildEntry:fe,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:ve}],x=v.map(e=>e.id),he=e=>v.find(r=>r.id===e.toLowerCase()),M=(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&&H(a,n),K(a,t)}catch(o){try{q(a)}catch{}throw o}},ye={formattingOptions:{insertSpaces:!0,tabSize:4}},k=e=>{const r=[],t=Y(e,r,{allowTrailingComma:!0});return{errors:r,value:t}},g=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),E=(e,r,t)=>{if(!g(e))return;const n=e[r];return g(n)?n[t]:void 0},L=e=>{if(!f(e.path))return"absent";let r;try{r=h(e.path,"utf8")}catch{return"invalid"}if(r.trim().length===0)return"absent";const{errors:t,value:n}=k(r);return t.length>0||!g(n)?"invalid":E(n,e.key,e.name)===void 0?"absent":"present"},be=e=>L(e)==="present",U=(e,r,t,n,a)=>{try{const o=ee(e,[t,n],a,ye);o.length>0&&M(r,re(e,o));return}catch(o){return o instanceof Error?o.message:String(o)}},Se=e=>{const{entry:r,force:t=!1,key:n,name:a,path:o}=e,l=f(o)?h(o,"utf8"):"";if(l.trim().length===0)return J(B(o),{recursive:!0}),M(o,`${JSON.stringify({[n]:{[a]:r}},void 0,4)} `),{action:"created",path:o};const{errors:c,value:i}=k(l);if(c.length>0)return{action:"invalid",error:`parse error at offset ${String(c[0]?.offset??0)}`,path:o};if(!g(i))return{action:"invalid",error:"the file's root is not a JSON object",path:o};const p=i[n];if(p!==void 0&&!g(p))return{action:"invalid",error:`"${n}" is not an object`,path:o};if(E(i,n,a)!==void 0&&!t)return{action:"skipped",path:o};const $=U(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(!f(n))return{action:"absent",path:n};let a;try{a=h(n,"utf8")}catch(i){return{action:"invalid",error:i instanceof Error?i.message:String(i),path:n}}const{errors:o,value:l}=k(a);if(o.length>0)return{action:"invalid",error:`parse error at offset ${String(o[0]?.offset??0)}`,path:n};if(E(l,r,t)===void 0)return{action:"absent",path:n};const c=U(a,n,r,t,void 0);return c===void 0?{action:"removed",path:n}:{action:"invalid",error:c,path:n}},ke=["global","project"],u=(e,r)=>{const t=Z(r,e);return t.length>0&&!t.startsWith("..")?t:e},Ee=(e,r)=>{const t=e.paths(r);return ke.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}},je=e=>v.filter(r=>Object.values(r.paths(e)).some(t=>f(t))),A=(e,r,t,n)=>{if(e.length===0)return r();const a=[];for(const o of e){const l=he(o);if(l===void 0){t.error(`mcp ${n}: unknown client "${o}". Known clients: ${x.join(", ")}.`);return}a.push(l)}return a},Oe=e=>e.client.format==="json",Ce="https://lunora.sh/mcp",N="lunora-docs",D="lunora",_e=e=>f(s(e,"lunora"))&&X(e)!==void 0,xe=e=>{const{args:r,command:t}=ne(e,"lunora",["mcp","serve"]);return{args:r,command:t,transport:"stdio"}},Me=e=>({transport:"http",url:e}),Le=e=>({home:e.home,platform:e.platform,projectRoot:e.cwd}),T=(e,r,t)=>$e(e,Le(t),t.scope??r.preferredScope,t.scope!==void 0||!r.allowScopeFallback),Ue=(e,r,t)=>{const n=T(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=u(a,t.cwd);return t.force!==!0&&be({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=Se({entry:o,force:t.force===!0,key:e.key,name:r.name,path:a}),c=u(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}},Ae=(e,r,t)=>{const n=T(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 ${u(n,t.cwd)} ${e.renderSnippet(r.name,r.spec)}`),{action:"printed",path:n}},Ne=e=>{const r=[];return e.localOnly!==!0&&r.push({allowScopeFallback:!0,name:N,preferredScope:"global",spec:Me(e.docsUrl??Ce)}),e.docsOnly!==!0&&_e(e.cwd)&&r.push({allowScopeFallback:!1,name:D,preferredScope:"project",spec:xe(te(e.cwd))}),r},De=e=>{const r=e.home??w(),t=e.platform??process.platform;e.logger.info("Supported MCP clients:");for(const n of v){const a=n.paths({home:r,platform:t,projectRoot:e.cwd}),o=l=>{const c=a[l];return c===void 0?"—":u(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:[]}},Te=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"?Ue(a,o,t):Ae(a,o,t);l!==void 0&&n.push({action:l.action,client:a.id,path:l.path,server:o.name})}return n},Fe=e=>{const r=e.home??w(),t=e.platform??process.platform,n=A(e.clients,()=>je({home:r,platform:t,projectRoot:e.cwd}),e.logger,"install");if(n===void 0)return{code:d.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: ${x.join(", ")}.`),{code:d.NOT_FOUND,written:[]};e.clients.length===0&&e.logger.info(`Detected ${String(n.length)} configured client(s): ${n.map(l=>l.label).join(", ")}.`);const a=Ne(e);if(a.length===0)return e.logger.error(`mcp install: nothing to install — ${Te(e)}`),{code:d.USAGE,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,F=1e3,j=e=>({content:[{text:JSON.stringify(e,void 0,2),type:"text"}]}),Ie=e=>({content:[{text:e,type:"text"}]}),Re=e=>{const r=typeof e=="string"?Number(e):e;return typeof r!="number"||!Number.isFinite(r)?P:Math.min(F,Math.max(1,Math.floor(r)))},O=(e,r)=>{if(r===void 0)return;const t=y(e,oe);return ce(t,y(e,r))?y(e,r):void 0},Ge=(e,r)=>{const t=h(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=S(e);return j(r===void 0?{hint:"Start it with `lunora dev` in the project directory.",running:!1}:{background:r.background===!0,logFile:O(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(F)})`,type:"number"}},type:"object"},name:"lunora_dev_logs"},handle:async r=>{const t=S(e);if(t===void 0)return{content:[{text:"no dev server is running — start one with `lunora dev`",type:"text"}],isError:!0};const n=O(e,t.logFile);if(n===void 0||!f(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=Re(r.lines),o=Ge(n,a);return Ie(o.length===0?"(the dev server log is empty)":o)}}],We=new Set(["127.0.0.1","::1","localhost"]),I=e=>{try{const{hostname:r}=new URL(e);return We.has(r.startsWith("[")?r.slice(1,-1):r)}catch{return!1}},C=e=>{const r=e.url??S(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(!I(r))return{url:r};const t=ae(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=!I(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}) `},He=async(e,r=le)=>{const t=e.writeError??(o=>{process.stderr.write(o)});let n;try{n=await r({allowWrites:e.allowWrites===!0,deployment:()=>C(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=C(e);return t(ze(a)),await Ve(n),{code:0,deployment:a}},Ke=e=>{const r=[];return e.localOnly!==!0&&r.push(N),e.docsOnly!==!0&&r.push(D),r},qe=(e,r,t,n)=>{const a=we({key:e.key,name:r,path:t}),o=u(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}},Xe=(e,r,t,n)=>{const a=L({key:e.key,name:r,path:t}),o=u(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=>Ee(n,r)).filter(Oe).filter(n=>t===void 0||n.scope===t),Ze=e=>{const r=e.home??w(),t=e.platform??process.platform,n=A(e.clients,()=>v,e.logger,"uninstall");if(n===void 0)return{code:d.USAGE,removed:[]};const a=Ke(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:d.USAGE,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 p=e.print===!0?Xe(c.client,i,c.path,e):qe(c.client,i,c.path,e);p.action!=="absent"&&o.push({...p,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}},_=e=>e.global===!0?"global":e.project===!0?"project":void 0,Qe=async(e,r)=>{const{code:t}=await He({allowWrites:r.allowWrites===!0,cwd:e,noDocs:r.docs===!1,version:R,...r.docsUrl===void 0?{}:{docsUrl:r.docsUrl},...r.token===void 0?{}:{token:r.token},...r.url===void 0?{}:{url:r.url}});return{code:t}},mr=G(async({argument:e,cwd:r,logger:t,options:n})=>{const[a,...o]=e;if(a==="serve")return Qe(r,n);if(a==="uninstall"){const l=_(n),{code:c}=Ze({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 De({cwd:r,logger:t});const l=_(n),{code:c}=Fe({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:d.USAGE}});export{mr as execute};