@lunora/cli
Version:
The Lunora CLI: init, dev, deploy, codegen, migrate, seed, doctor, insights, logs, registry, and the rest of the project commands
20 lines (18 loc) • 15.7 kB
JavaScript
import{existsSync as m,readFileSync as h,writeFileSync as $,mkdirSync as S}from"node:fs";import{join as l,relative as L,dirname as w}from"@visulima/path";import{i as q,d as U}from"./detect-package-manager-DSaAqRof.mjs";import{DEV_VARS_FILE as G,parseDevVariableEntries as H,writeDevVariablesFileAtomically as Y}from"@lunora/config";import{modify as J,applyEdits as F,parse as z}from"jsonc-parser";import{d as X,s as x,e as Q,f as Z,c as K,g as ee}from"./resolve-DZAGzg67.mjs";import{b as ne}from"./tui-prompts-Dd0pDuNr.mjs";import{collectCatalog as te,buildRegistryIndex as re}from"./buildRegistryIndex-Zi3bBqpc.mjs";import{findWranglerFile as se,readWranglerJsonc as ie}from"@lunora/config/cloudflare";import{LunoraError as j}from"@lunora/errors";import{insertSchemaExtension as oe}from"./insertSchemaExtension-DY4pA8wE.mjs";import{createHash as ae}from"node:crypto";const T=e=>{if(!e.startsWith("workspace:"))return e;const n=e.slice(10);return n===""||n==="*"||n==="^"||n==="~"?X():n},ce=new Set(["@lunora/client","@lunora/do","@lunora/ratelimit","@lunora/runtime","@lunora/server","@lunora/values"]),de=/(['"])@lunora\/(client|do|ratelimit|runtime|server|values)(\/[^'"]*)?\1/gu,ue=e=>{const n=l(e,"package.json");if(!m(n))return!1;try{const t=JSON.parse(h(n,"utf8"));return t.dependencies?.lunorash!==void 0||t.devDependencies?.lunorash!==void 0}catch{return!1}},fe=e=>e.replaceAll(de,(n,t,i,r)=>`${t}lunorash/${i}${r??""}${t}`),E=(e,n,t,i="dependencies",r=!1)=>{const s=Object.entries(e);if(s.length===0)return[];const o=l(n,"package.json");if(!m(o))return t.warn(`package.json not found at ${o} — skipping dependency updates`),[];let a=h(o,"utf8");const c=JSON.parse(a),d=[];for(const[u,f]of s){if(r&&ce.has(u)){t.info(`dep provided by the lunorash umbrella, skipping: ${u}`);continue}if(c.dependencies?.[u]!==void 0||c.devDependencies?.[u]!==void 0){t.info(`dep already present: ${u}`);continue}const p=J(a,[i,u],T(f),{formattingOptions:{insertSpaces:!0,tabSize:4}});a=F(a,p),d.push(u)}return d.length>0&&($(o,a,"utf8"),t.success(`added ${String(d.length)} ${i==="devDependencies"?"devDependency(ies)":"dependency(ies)"} to package.json: ${d.join(", ")}`)),d},le=(e,n,t)=>{if(e.length===0)return[];const i=l(n,G),r=m(i)?h(i,"utf8"):"",s=new Set(H(r).map(d=>d.key)),o=[],a=[],c=[];for(const d of e)d.secret&&a.push(d.name),!s.has(d.name)&&(d.description&&c.push(`# ${d.description}`),c.push(`${d.name}=${d.secret?"":d.value??""}`),o.push(d.name));if(o.length>0){const d=r===""||r.endsWith(`
`)?r:`${r}
`;Y(i,`${d}${c.join(`
`)}
`),t.success(`scaffolded ${String(o.length)} env var(s) into .dev.vars: ${o.join(", ")}`)}return a.length>0&&t.info(`set secret value(s) locally in .dev.vars, then for production: ${a.map(d=>`wrangler secret put ${d}`).join("; ")}`),o},O=new Set(["ai","analytics_engine_datasets","browser","d1_databases","durable_objects","hyperdrive","kv_namespaces","mtls_certificates","queues","r2_buckets","send_email","services","vars","vectorize","version_metadata","workflows"]),pe=["binding","name","queue","pattern"],R=e=>{if(!(typeof e!="object"||e===null))for(const n of pe){const t=e[n];if(typeof t=="string"&&t.length>0)return`${n}:${t}`}},me=(e,n,t,i)=>{const r=new Set(e.map(a=>JSON.stringify(a))),s=new Set(e.map(a=>R(a)).filter(a=>a!==void 0)),o=[];for(const a of n){if(r.has(JSON.stringify(a)))continue;const c=R(a);if(c!==void 0&&s.has(c)){i.warn(`${c.replace(":"," ")} already exists in ${t} — keeping the project's entry and skipping the registry item's. Reconcile by hand if the item needs different settings.`);continue}c!==void 0&&s.add(c),o.push(a)}return o},D=e=>Array.isArray(e),he=(e,n)=>{let t=z(e);for(const i of n){if(typeof t!="object"||t===null)return;t=t[i]}return t},ge=(e,n)=>{const t=e[0];return t!==void 0&&O.has(t)?!0:(n.warn(`skipping binding "${e.join(".")}" — only resource bindings (${[...O].join(", ")}) may be written, not exec/entrypoint keys`),!1)},C=Symbol("skip-binding"),$e=(e,n,t)=>{const{value:i}=n;if(!D(i))return i;const r=he(e,n.path);if(!D(r))return i;const s=me(r,i,n.path.join("."),t);return s.length===0?C:[...r,...s]},ve=(e,n,t)=>{if(e.length===0)return[];const r=["wrangler.jsonc","wrangler.json"].map(a=>l(n,a)).find(a=>m(a));if(!r)return t.warn("wrangler.jsonc not found — skipping binding updates"),[];let s=h(r,"utf8");const o=[];for(const a of e){if(!ge(a.path,t))continue;const c=$e(s,a,t);if(c===C)continue;const d=J(s,[...a.path],c,{formattingOptions:{insertSpaces:!0,tabSize:4}});d.length!==0&&(s=F(s,d),o.push(a.path.join(".")))}return o.length>0&&($(r,s,"utf8"),t.success(`applied ${String(o.length)} binding(s) to ${r}: ${o.join(", ")}`)),o},ye=(e,n,t,i=!1)=>{const r=[],s=[];return e.deps&&r.push(...E(e.deps,n,t,"dependencies",i)),e.devDependencies&&r.push(...E(e.devDependencies,n,t,"devDependencies",i)),e.bindings&&s.push(...ve(e.bindings,n,t)),e.envVars&&le(e.envVars,n,t),{bindings:s,deps:r}},we=async(e,n)=>{const t=e.some(({manifest:d})=>Object.keys(d.deps??{}).length>0||Object.keys(d.devDependencies??{}).length>0),i=e.some(({manifest:d})=>(d.bindings??[]).length>0),r=n.source!==void 0&&n.source.length>0;if(!t&&!i&&!r||n.yes)return!0;const s=[];t&&s.push("add dependencies to package.json"),i&&s.push("write wrangler.jsonc bindings"),r&&s.push(`come from a non-default source (${String(n.source)})`);const o=s.join(", ");if(!process.stdin.isTTY&&n.confirm===void 0)return n.logger.error(`add: stdin is not a TTY and the requested items ${o} — re-run with --yes to confirm`),!1;const c=await(n.confirm??ne)(`The requested items ${o}. Continue?`);return c||n.logger.info("add: aborted"),c},b=1,ke=".lunora-registry.json",B=e=>l(e,"lunora",ke),be=e=>typeof e!="object"||e===null||!("items"in e)?!1:typeof e.items=="object"&&e.items!==null,y=e=>ae("sha256").update(e).digest("hex"),Se=e=>{const n=B(e);if(!m(n))return{items:{},version:b};try{const t=JSON.parse(h(n,"utf8"));if(be(t))return{items:t.items,version:b}}catch{}return{items:{},version:b}},xe=(e,n)=>{$(B(e),`${JSON.stringify(n,void 0,2)}
`,"utf8")},A=(e,n,t,i)=>{const r=e.items[n]??{files:{}};e.items[n]=r,r.files[t]=y(i)},je=(e,n,t)=>e.items[n]?.files[t],N=3,_=e=>e===""?[]:e.split(`
`),Ee=(e,n)=>{const t=_(e),i=_(n);let r=0;for(;r<t.length&&r<i.length&&t[r]===i[r];)r+=1;let s=t.length,o=i.length;for(;s>r&&o>r&&t[s-1]===i[o-1];)s-=1,o-=1;if(r===s&&r===o)return[];const a=[];for(let c=Math.max(0,r-N);c<r;c+=1)a.push(` ${t[c]??""}`);for(let c=r;c<s;c+=1)a.push(`- ${t[c]??""}`);for(let c=r;c<o;c+=1)a.push(`+ ${i[c]??""}`);for(let c=o;c<Math.min(i.length,o+N);c+=1)a.push(` ${i[c]??""}`);return a},Oe=/\.[cm]?[jt]sx?$/u,V=(e,n,t)=>{const i=h(l(e,n.from),"utf8");return t&&Oe.test(n.to)?fe(i):i},Re=(e,n,t,i,r,s,o)=>{const a=l(i,"lunora","schema.ts");if(s)return r.info(`~ would merge .extend(${n}.extension) into lunora/schema.ts (and create ${e.to} if absent)`),{kind:"skipped",path:a};const c=l(i,e.to);m(c)||(S(w(c),{recursive:!0}),$(c,V(t,e,o),"utf8"));const d=o?"lunorash/server":"@lunora/server",u=m(a)?h(a,"utf8"):`import { defineSchema } from "${d}";
export const schema = defineSchema({});
`,f=oe(u,n);if(f.ok)return S(w(a),{recursive:!0}),$(a,f.text,"utf8"),r.success(`merged .extend(${n}.extension) into lunora/schema.ts`),{kind:"written",path:a};if(f.reason==="already-applied")return r.warn(`lunora/schema.ts already extends "${n}" — skipping`),{kind:"skipped",path:a};throw f.reason==="invalid-identifier"?new j("INTERNAL",`schema-extension item "${n}" is not a valid JS identifier — it is spliced into lunora/schema.ts as \`import { ${n} }\` / \`.extend(${n}.extension)\`. Rename the item to a valid identifier (no leading digit, no "-").`):new j("INTERNAL",`schema-extension merge failed for "${n}": ${f.reason}`)},De=(e,n,t,i,r)=>{const s=Ee(n,t);if(s.length===0){r.info(`= ${e.to} (unchanged)`);return}r.info(`${i?"~":"+"} ${e.to}`);for(const o of s)r.info(` ${o}`)},Ae=(e,n,t,i,r,s,o,a)=>{const c=l(i,e.to),d=V(t,e,a),u=m(c),f=u?h(c,"utf8"):"",p=M=>(S(w(c),{recursive:!0}),$(c,d,"utf8"),A(s,n,e.to,d),r.success(`${M}: ${e.to}`),{kind:"written",path:c});if(o.diff)return De(e,f,d,u,r),{kind:"skipped",path:c};if(!u)return p("write");const g=y(f);if(g===y(d))return A(s,n,e.to,d),r.warn(`skip (exists): ${e.to}`),{kind:"skipped",path:c};if(o.overwrite)return p("overwrite");const v=je(s,n,e.to);return v===void 0?(r.warn(`skip (exists, untracked): ${e.to} — refusing to overwrite a file lunora didn't add (use --overwrite to force)`),{kind:"skipped",path:c}):v===g?p("update"):v===y(d)?(r.warn(`skip (locally edited): ${e.to}`),{kind:"skipped",path:c}):($(`${c}.new`,d,"utf8"),r.warn(`conflict: ${e.to} has local edits and an upstream update — wrote ${e.to}.new (use --overwrite to take theirs)`),{kind:"skipped",path:c})},Ne=(e,n,t,i,r,s,o={},a=!1)=>e.merge==="schema-extension"?Re(e,n,t,i,r,o.diff===!0,a):Ae(e,n,t,i,r,s,o,a),P=["src/server.ts","src/server/index.ts","src/server/index.tsx","src/index.ts","src/worker.ts"],_e=e=>{const n=se(e);if(n===void 0)return;const{parsed:t}=ie(n);return typeof t?.main=="string"?t.main:void 0},Pe=e=>{const n=_e(e),t=n===void 0?P:[n,...P];for(const i of t){const r=l(e,i);if(!m(r))continue;const s=h(r,"utf8");if(!s.includes("createShardDO(")){if(i===n)break;continue}return{entryPath:r,main:i,source:s}}},Ie=(e,n,t)=>{const i=L(w(e),l(n,"lunora",t)).replaceAll("\\","/");return i.startsWith(".")?i:`./${i}`},Le=(e,n)=>{for(const t of e){const r=`Add \`export * from "${`./lunora/${t.module}.js`}"\` to your worker entry`,s=t.comment?` (${t.comment})`:"";n.warn(`${r}${s}`)}return 0},Je=(e,n,t,i)=>{const r=[];for(const s of e){const o=Ie(n,t,s.module),a=o.replaceAll(/[.*+?^${}()|[\]\\]/g,String.raw`\$&`);new RegExp(String.raw`export\s+\*\s+from\s+["']${a}\.js["']`,"u").test(i)||(s.comment&&r.push(`
// ${s.comment}`),r.push(`export * from "${o}.js";`))}return r},Fe=(e,n,t,i)=>{if(e.length===0)return 0;const r=Pe(n);if(r===void 0)return Le(e,t);const s=Je(e,r.entryPath,n,r.source);if(s.length===0)return 0;if(i){for(const a of s)a!==""&&t.info(`~ entrypoint: ${a}`);return s.length}const o=r.source.endsWith(`
`)?"":`
`;return $(r.entryPath,`${r.source}${o}${s.join(`
`)}
`,"utf8"),t.success(`wrote ${String(s.length)} entrypoint re-export(s) to ${L(n,r.entryPath)}`),s.length},I=(e,n,t,i={})=>{const r=[],s=[],o=[],a=[],c=Se(n),d=ue(n);for(const{directory:u,manifest:f}of e){for(const g of f.files){const v=Ne(g,f.name,u,n,t,c,i,d);(v.kind==="written"?r:s).push(v.path)}if(f.entrypointReexports!==void 0&&Fe(f.entrypointReexports,n,t,i.diff===!0),i.diff)continue;const p=ye(f,n,t,d);o.push(...p.deps),a.push(...p.bindings)}return!i.diff&&Object.keys(c.items).length>0&&xe(n,c),{bindings:a,deps:o,skipped:s,written:r}},k=()=>({bindings:[],code:0,deps:[],skipped:[],written:[]}),en=(e,n,t,i,r)=>e.bindings?{...e,bindings:e.bindings.map(s=>{if(s.path[0]!==n||!Array.isArray(s.value))return s;const o=s.value;return{...s,value:o.map(a=>typeof a=="object"&&a!==null&&a[t.key]===t.value?{...a,[i]:r}:a)}})}:e,W=(e,n)=>{const t=n.title??n.description;e.info(`plan: ${n.name}${t?` — ${t}`:""}`);for(const s of n.files)e.info(` file ${s.to} (${s.merge})`);const i=new Map,r=s=>{const o=i.get(s);if(o!==void 0)return o;const a=T(s);return i.set(s,a),a};for(const[s,o]of Object.entries(n.deps??{}))e.info(` dep ${s}@${r(o)}`);for(const[s,o]of Object.entries(n.devDependencies??{}))e.info(` dev ${s}@${r(o)}`);for(const s of n.bindings??[])e.info(` bind ${s.path.join(".")} = ${JSON.stringify(s.value)}`);for(const s of n.envVars??[]){const o=s.secret?" (secret)":` = ${JSON.stringify(s.value??"")}`;e.info(` env ${s.name}${o}`)}for(const s of n.entrypointReexports??[]){const o=`./lunora/${s.module}`,a=s.comment?` // ${s.comment}`:"";e.info(` entry ${o}${a}`)}},Te=e=>{const n=e.map(({manifest:t})=>({bindings:(t.bindings??[]).map(i=>({path:i.path.join("."),value:i.value})),deps:Object.keys(t.deps??{}),devDependencies:Object.keys(t.devDependencies??{}),entrypointReexports:(t.entrypointReexports??[]).map(i=>({module:i.module,...i.comment?{comment:i.comment}:{}})),envVars:(t.envVars??[]).map(i=>({name:i.name,...i.secret?{secret:!0}:{value:i.value??""}})),files:t.files.map(i=>({merge:i.merge,to:i.to})),name:t.name,requires:t.requires??[],title:t.title}));process.stdout.write(`${JSON.stringify({items:n},void 0,2)}
`)},Ce=e=>{try{const{args:n,command:t}=q(U(e));return`${t} ${n.join(" ")}`}catch{return"<your-package-manager> install"}},Be=(e,n,t,i,r,s)=>{r.success(`add complete: ${String(t)} written, ${String(i)} skipped`),r.info("next steps:"),r.info(" lunora codegen # regenerate _generated/ so the new tables/functions appear"),n.length>0&&r.info(` ${Ce(s)} # install newly-added dependencies`);for(const{manifest:o}of e)o.docs&&r.info(`${o.name}: ${o.docs}`)},Ve=async e=>{const n=k(),t=x("list",e);if(t)return e.logger.error(t),{...n,code:1};let i=()=>{};try{const r=await Z(e);i=r.cleanup;const s=te(r.root);if(e.json)return process.stdout.write(`${JSON.stringify(s,void 0,2)}
`),n;e.logger.info(`available registry items (${String(s.length)}):`);for(const o of s)e.logger.info(` ${o.name}${o.description?` — ${o.description}`:""}`);return n}catch(r){return e.logger.error(`list failed: ${r instanceof Error?r.message:String(r)}`),{...n,code:1}}finally{i()}},nn=async e=>{const n=e.cwd??process.cwd(),t=k();if(e.list)return Ve(e);if(e.names.length===0)return e.logger.error("add requires at least one item name. Usage: lunora registry add <name> [...names]"),{...t,code:1};const i=x("add",e);if(i)return e.logger.error(i),{...t,code:1};let r=[];try{const{cleanups:s,items:o}=await Q(e.names,e);r=s;const{transformManifest:a}=e,c=a?o.map(g=>({...g,manifest:a(g.manifest)})):o;for(const{manifest:g}of c)W(e.logger,g);if(e.json&&Te(c),e.dryRun)return e.logger.info("dry-run: stopping before any files are written"),t;if(e.diff)return I(c,n,e.logger,{diff:!0}),e.logger.info("diff: preview only — re-run without --diff to apply"),t;if(!await we(c,e))return{...t,code:1};const{bindings:d,deps:u,skipped:f,written:p}=I(c,n,e.logger,{overwrite:e.overwrite});return Be(c,u,p.length,f.length,e.logger,n),{bindings:d,code:0,deps:u,skipped:f,written:p}}catch(s){return e.logger.error(`add failed: ${s instanceof Error?s.message:String(s)}`),{...t,code:1}}finally{for(const s of r)s()}},tn=async e=>{const n=k();if(e.names.length===0)return e.logger.error("view requires an item name. Usage: lunora registry view <name>"),{...n,code:1};const t=x("view",e);if(t)return e.logger.error(t),{...n,code:1};const i=[];try{for(const r of e.names){const{cleanup:s,directory:o}=await K(r,e);i.push(s);const a=ee(o,r);W(e.logger,a);for(const c of a.files){e.logger.info(`--- ${c.to} (${c.merge}) ---`);const d=h(l(o,c.from),"utf8");for(const u of d.split(`
`))e.logger.info(u)}}return n}catch(r){return e.logger.error(`view failed: ${r instanceof Error?r.message:String(r)}`),{...n,code:1}}finally{for(const r of i)r()}},rn=async e=>{const n=k(),t=e.from;if(t===void 0)return e.logger.error("registry build requires --from <registry root>"),{...n,code:1};if(!m(t))return e.logger.error(`registry root not found: ${t}`),{...n,code:1};const i=re(t),r=e.out??l(t,"index.json");if(e.check){const s=m(r)?JSON.parse(h(r,"utf8")):{items:[]};return JSON.stringify(s.items??[])!==JSON.stringify(i.items)?(e.logger.error(`registry: ${r} is stale — run \`lunora registry build\` to regenerate it`),{...n,code:1}):(e.logger.success(`registry: ${r} is up to date (${String(i.items.length)} items)`),n)}return $(r,`${JSON.stringify({$schema:"./schema/registry.schema.json",...i},void 0,4)}
`,"utf8"),e.logger.success(`registry: wrote ${r} (${String(i.items.length)} items)`),n};export{rn as a,tn as b,nn as r,en as s};