@lunora/cli
Version:
The Lunora CLI: init, dev, deploy, codegen, migrate, seed, doctor, insights, logs, registry, and the rest of the project commands
2 lines (1 loc) • 8.69 kB
JavaScript
import{existsSync as p,readFileSync as v,realpathSync as h}from"node:fs";import{join as u}from"node:path";import{isPlaceholderValue as w,DEV_VARS_FILE as g,parseDevVariableEntries as k,discoverSchemaInfo as x,inferLunoraBindings as b}from"@lunora/config";import{findWranglerFile as y,readWranglerJsonc as $,validateWranglerConfig as _,collectExportGaps as R}from"@lunora/config/cloudflare";import{i as S,m as N,c as L}from"../packem_shared/vectorize-metadata-dXpl2_Ar.mjs";import{r as A,d as I}from"../packem_shared/admin-token-VdUnvnKW.mjs";import{d as E}from"../packem_shared/command-DwC11Cwe.mjs";import{v as O,i as j,p as D,l as V}from"../packem_shared/output-format-DfogXx0p.mjs";import{i as C}from"../packem_shared/path-containment-CgxYZggb.mjs";const F=o=>{const e=o.trim();if(e==="")return!0;const n=e.toLowerCase();return n.includes("replace")||n.startsWith("<")&&n.endsWith(">")},P=o=>{const e=y(o);if(e===void 0)return{parsed:void 0,path:void 0};const{parsed:n}=$(e);return{parsed:n,path:e}},U=(o,e,n)=>{if(e===void 0){n.push({code:"wrangler-missing",fix:"Run `lunora init` (or `lunora dev`) to scaffold and reconcile wrangler.jsonc.",level:"fail",message:"wrangler.jsonc not found."});return}if(o===void 0){n.push({code:"wrangler-unparseable",fix:`Check ${e} is valid JSONC.`,level:"fail",message:`Could not parse ${e}.`});return}const a=_(o);for(const r of a.warnings.filter(i=>i.includes("LUNORA_ORIGIN_URL")))n.push({code:"scheduler-origin-missing",fix:"Set LUNORA_ORIGIN_URL in wrangler vars to the worker's public URL (or `wrangler secret put LUNORA_ORIGIN_URL`).",level:"warn",message:r});const s=a.errors.find(r=>r.includes("SHARD"));s===void 0?n.push({code:"wrangler-shard-binding-ok",level:"pass",message:"wrangler.jsonc present with a SHARD durable-object binding."}):n.push({code:"wrangler-shard-binding-missing",fix:"Run `lunora dev` to auto-reconcile, or add the binding manually.",level:"fail",message:s})},M=(o,e)=>{const{error:n,info:a}=x(o,"lunora");if(n!==void 0){e.push({code:"schema-unreadable",fix:"Fix the parse error, then re-run `lunora doctor`. `lunora codegen` reports the same failure with the offending source.",level:"fail",message:`lunora/schema.ts could not be parsed, so every schema-derived check was skipped: ${n}`});return}for(const s of a?.vectorMetadata??[]){const r=N(s.kind);if(r===void 0){e.push({code:"vector-metadata-unfilterable",fix:"Filter on a string, number or boolean column, or drop it from `metadata`.",level:"warn",message:`vector index "${s.index}" declares metadata "${s.property}", whose type Vectorize cannot filter on.`});continue}e.push({code:"vector-metadata-index-required",fix:`wrangler ${L({index:s.index,property:s.property,type:r}).join(" ")}`,level:"info",message:`vector index "${s.index}" filters on metadata "${s.property}" — that needs a Vectorize metadata index (\`lunora deploy\` creates it).`})}},W=(o,e)=>{if(o===void 0)return;const n=(o.d1_databases??[]).filter(Boolean);for(const a of n){const s=typeof a.database_id=="string"?a.database_id:"";if(F(s)){const r=typeof a.binding=="string"&&a.binding.length>0?a.binding:"<unnamed>";e.push({code:"d1-placeholder-id",fix:"Run `wrangler d1 create <name>` and paste the returned database_id into wrangler.jsonc.",level:"fail",message:`D1 binding "${r}" has a placeholder database_id ("${s||"<empty>"}").`})}}},T=(o,e)=>{if(o===void 0)return;const n=(o.send_email??[]).filter(Boolean);for(const a of n){const s=typeof a.destination_address=="string"?a.destination_address:"";if(s!==""&&w(s)){const r=typeof a.name=="string"&&a.name.length>0?a.name:"send_email";e.push({code:"email-destination-placeholder",fix:"Set destination_address to a verified Cloudflare Email Routing address.",level:"warn",message:`send_email binding "${r}" has a placeholder destination_address ("${s}").`})}}},z=(o,e)=>{const n=u(o,g);if(!p(n))return;let a;try{a=v(n,"utf8")}catch{return}const s=k(a).filter(r=>S(r.key)&&w(r.value)).map(r=>r.key);s.length>0&&e.push({code:"dev-vars-missing-secret",fix:"Run `lunora dev` to auto-generate secrets, or fill them in by hand.",level:"warn",message:`${g} has unfilled secret value(s): ${s.join(", ")}.`})},G=(o,e)=>{const{source:n}=A({cwd:o});n===void 0?e.push({code:"admin-token-missing",fix:"Set LUNORA_ADMIN_TOKEN (env or `.dev.vars`) to enable admin RPCs / studio.",level:"info",message:"LUNORA_ADMIN_TOKEN is not set."}):e.push({code:"admin-token-set",level:"pass",message:`LUNORA_ADMIN_TOKEN is set (${I(n)}).`})},B=async(o,e)=>{let n;try{n=await b({projectRoot:o})}catch(r){e.push({code:"declared-export-unchecked",fix:"Make sure the worker entry resolves (see `lunora codegen`), then re-run.",level:"warn",message:`could not check whether declared containers/workflows/agents are re-exported by the worker entry: ${r instanceof Error?r.message:String(r)}`});return}const a=R(n),s=[...n.containers.map(r=>({...r,kind:"container"})),...n.workflows.map(r=>({...r,kind:"workflow"})),...n.agents.map(r=>({...r,kind:"agent"}))];for(const r of s.filter(i=>i.exported))e.push({code:"declared-export-ok",level:"pass",message:`${r.kind} "${r.exportName}" is exported by the worker entry.`});for(const r of a)e.push({code:"declared-export-missing",fix:`Add \`export * from "./lunora/_generated/${r.module}"\` to your worker entry (or re-run \`vis generate lunora-${r.kind}\`).`,level:"fail",message:`${r.kind} "${r.exportName}" is declared but ${r.className} is not exported by the worker entry.`})},J=/^[\^~><= ]+/u,K=/^(?<core>\d+\.\d+\.\d+)(?:-(?<channel>[a-z]+)\.(?<counter>\d+))?$/u,H=o=>{const e=o.replace(J,"").trim(),n=K.exec(e);if(!n?.groups)return;const{channel:a,core:s,counter:r}=n.groups;return{channel:a??"",core:s??"",counter:r===void 0?void 0:Number.parseInt(r,10),raw:e}},q=(o,e)=>{const n=u(o,"package.json");if(!p(n))return;let a;try{a=JSON.parse(v(n,"utf8"))}catch{return}const s={...a.devDependencies,...a.dependencies},r=[];for(const[t,c]of Object.entries(s)){if(t!=="lunorash"&&!t.startsWith("@lunora/"))continue;const m=H(c);m&&r.push({name:t,version:m})}if(r.length<2)return;const i=t=>t.map(c=>`${c.name}@${c.version.raw}`).join(", "),l=new Set(r.map(t=>t.version.core)),f=new Set(r.map(t=>t.version.channel));if(l.size>1){e.push({code:"version-skew-cores",fix:'Run `pnpm update "@lunora/*" lunorash` (or `npm`/`yarn` equivalent) so every Lunora package moves to the same release.',level:"warn",message:`Lunora packages span ${String(l.size)} different versions: ${i(r)}.`});return}if(f.size>1){e.push({code:"version-skew-channels",fix:"Pick one channel (all stable, or all alpha/beta) and update the odd package out.",level:"warn",message:`Lunora packages mix release channels (${[...f].map(t=>t===""?"stable":t).join(" + ")}): ${i(r)}.`});return}const d=r.map(t=>t.version.counter).filter(t=>t!==void 0);if(d.length>1){const t=Math.min(...d),c=Math.max(...d);e.push({code:"version-counter-spread",level:"info",message:d.length===r.length&&t===c?`Lunora packages are all at ${[...f][0]??""}.${String(c)}.`:`Lunora pre-release counters span ${String(t)}–${String(c)}: ${i(r)}.`})}},X=[u("@lunora","cli"),"lunorash"],Q=(o,e,n)=>{if(e===void 0)return;const a=[];for(const r of X){const i=u(o,"node_modules",r);if(p(i))try{a.push(h(i))}catch{}}if(a.length===0)return;let s;try{s=h(e)}catch{return}a.some(r=>C(r,s))||n.push({code:"cli-shadowed",fix:"Run the project's own CLI: `pnpm exec lunora …` (or `npx lunora …`).",level:"warn",message:`the running lunora (${s}) is not the project's own install (${a.join(", ")}).`})},Y=async o=>{const e=o.cwd??process.cwd(),n=[],{parsed:a,path:s}=P(e);U(a,s,n),W(a,n),T(a,n),z(e,n),G(e,n),q(e,n),M(e,n),Q(e,o.executablePath??process.argv[1],n),await B(e,n);const r={fail:0,info:0,pass:0,warn:0};for(const l of n)r[l.level]+=1;const i=r.fail>0?1:0;return{code:i,findings:n,ok:i===0,summary:r}},Z={fail:"FAIL",info:"INFO",pass:"PASS",warn:"WARN"},ee=(o,e)=>{e.info("lunora doctor — project preflight");for(const s of o.findings){const r=`[${Z[s.level]}] ${s.message}`;s.level==="fail"?e.error(r):s.level==="warn"?e.warn(r):e.info(r),s.fix!==void 0&&s.level!=="pass"&&e.info(` fix: ${s.fix}`)}const{fail:n,warn:a}=o.summary;n>0?e.error(`${String(n)} failure(s), ${String(a)} warning(s).`):a>0?e.warn(`0 failures, ${String(a)} warning(s).`):e.success("all checks passed.")},re=async o=>{const e=O("doctor",o.format);if(e!==void 0)return o.logger.error(e),{code:1,findings:[],ok:!1,summary:{fail:0,info:0,pass:0,warn:0}};const n=V(o.format,o.logger),a=await Y({...o});return ee(a,n),j(o.format)&&D(a),a},ue=E(async({cwd:o,logger:e,options:n})=>({code:(await re({cwd:o,format:n.format,logger:e})).code}));export{ue as execute,Y as runDoctor,re as runDoctorCommand};