@lunora/cli
Version:
The Lunora CLI: init, dev, deploy, codegen, migrate, seed, doctor, insights, logs, registry, and the rest of the project commands
6 lines (5 loc) • 10.9 kB
JavaScript
import{existsSync as p,readFileSync as h,writeFileSync as j,mkdtempSync as G,rmSync as w}from"node:fs";import{tmpdir as W}from"node:os";import{LunoraError as g}from"@lunora/errors";import{dirname as S,join as f}from"@visulima/path";import{downloadTemplate as J}from"giget";import{fileURLToPath as z}from"node:url";import{DEV_VARS_FILE as H,parseDevVariableEntries as K,writeDevVariablesFileAtomically as q}from"@lunora/config";import{modify as N,applyEdits as O,parse as Y}from"jsonc-parser";import{b as Z}from"./tui-prompts-B3YwUhGw.mjs";import X from"./parseManifest-CwPTKdtS.mjs";const Q="alpha",T="main",ee=new Set(["alpha","beta","next"]),te=/^[\w./@-]+$/,ne=e=>!e.includes("..")&&te.test(e),v=()=>{try{let e=S(z(import.meta.url));for(let t=0;t<6;t+=1){const n=f(e,"package.json");if(p(n)){const r=JSON.parse(h(n,"utf8"));if(r.name==="@lunora/cli"&&typeof r.version=="string")return r.version}const s=S(e);if(s===e)break;e=s}}catch{}return"0.0.0"},D=e=>{if(e==="0.0.0")return Q;const t=e.split("+")[0]??e,n=t.indexOf("-");if(n!==-1){const[s]=t.slice(n+1).split(".");if(s!==void 0&&ee.has(s))return s}return T},I=e=>{if(e!==void 0&&e.length>0){if(!ne(e))throw new g("INTERNAL",`invalid --ref "${e}" — a ref may contain letters, digits, ".", "_", "-", "/", "@" and must not contain "..".`);return e}return D(v())},L="anolilab/lunora",B=/^[0-9a-f]{40}$/iu,P=String.raw`\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)+)?(?:\+[0-9A-Za-z.-]+)?`,se=new RegExp(String.raw`^v?${P}$`,"u"),re=new RegExp(String.raw`^(?:@[\w.-]+\/)?[\w.-]+@${P}$`,"u"),oe=e=>B.test(e)||se.test(e)||re.test(e),ie=()=>{const e=process.env.GITHUB_TOKEN??process.env.GH_TOKEN;return e!==void 0&&e.length>0?{authorization:`Bearer ${e}`}:{}},ae=async(e,t=L)=>{try{const n=await fetch(`https://api.github.com/repos/${t}/commits/${encodeURIComponent(e)}`,{headers:{accept:"application/vnd.github+json","user-agent":"lunora-cli",...ie()},signal:AbortSignal.timeout(1e4)});if(!n.ok)return;const s=await n.json();return typeof s.sha=="string"&&B.test(s.sha)?s.sha:void 0}catch{return}},ce=async(e,t,n)=>{if(oe(t))return t;const s=await ae(t,e);return s===void 0?(n.warn(`could not pin ${e}#${t} to a commit — fetching the UNPINNED branch (set GITHUB_TOKEN if rate-limited).`),t):(n.info(`pinned ${e}#${t} → ${s}`),s)},de=async(e,t)=>ce(L,I(e),t),Fe=(e=v())=>`@lunora/cli@${e}`,ue="latest",V=(e=v())=>{const t=D(e);return t===T?ue:t},fe="https://registry.npmjs.org",le=()=>{const e=process.env.npm_config_registry,t=e!==void 0&&e.length>0?e:fe;return t.endsWith("/")?t.slice(0,-1):t},pe=async(e,t)=>{try{const n=await fetch(`${le()}/${e.replaceAll("/","%2F")}`,{headers:{accept:"application/vnd.npm.install-v1+json"},signal:AbortSignal.timeout(1e4)});return n.ok?(await n.json())["dist-tags"]?.[t]:void 0}catch{return}},he=async(e,t)=>{const n=new Map;return await Promise.all([...new Set(e)].map(async s=>{const r=await pe(s,t);r!==void 0&&n.set(s,r)})),n},ge=e=>{if(!e.startsWith("workspace:"))return!1;const t=e.slice(10);return t===""||t==="*"||t==="^"||t==="~"},me=(e,t,n)=>{if(!e.startsWith("workspace:"))return e;const s=e.slice(10);return s===""||s==="*"||s==="^"||s==="~"?(n===void 0?void 0:t?.get(n))??V():s},Ge=async e=>{const t=new Set;for(const n of e)for(const s of[n.deps,n.devDependencies])for(const[r,o]of Object.entries(s??{}))ge(o)&&t.add(r);return t.size===0?new Map:he([...t],V())},ve=new Set(["@lunora/client","@lunora/do","@lunora/ratelimit","@lunora/runtime","@lunora/server","@lunora/values"]),ye=/(['"])@lunora\/(client|do|ratelimit|runtime|server|values)(\/[^'"]*)?\1/gu,We=e=>{const t=f(e,"package.json");if(!p(t))return!1;try{const n=JSON.parse(h(t,"utf8"));return n.dependencies?.lunorash!==void 0||n.devDependencies?.lunorash!==void 0}catch{return!1}},Je=e=>e.replaceAll(ye,(t,n,s,r)=>`${n}lunorash/${s}${r??""}${n}`),$=(e,t,n,s="dependencies",r=!1,o)=>{const a=Object.entries(e);if(a.length===0)return[];const i=f(t,"package.json");if(!p(i))return n.warn(`package.json not found at ${i} — skipping dependency updates`),[];let d=h(i,"utf8");const c=JSON.parse(d),l=[];for(const[u,M]of a){if(r&&ve.has(u)){n.info(`dep provided by the lunorash umbrella, skipping: ${u}`);continue}if(c.dependencies?.[u]!==void 0||c.devDependencies?.[u]!==void 0){n.info(`dep already present: ${u}`);continue}const F=N(d,[s,u],me(M,o,u),{formattingOptions:{insertSpaces:!0,tabSize:4}});d=O(d,F),l.push(u)}return l.length>0&&(j(i,d,"utf8"),n.success(`added ${String(l.length)} ${s==="devDependencies"?"devDependency(ies)":"dependency(ies)"} to package.json: ${l.join(", ")}`)),l},we=(e,t,n)=>{if(e.length===0)return[];const s=f(t,H),r=p(s)?h(s,"utf8"):"",o=new Set(K(r).map(c=>c.key)),a=[],i=[],d=[];for(const c of e)c.secret&&i.push(c.name),!o.has(c.name)&&(c.description&&d.push(`# ${c.description}`),d.push(`${c.name}=${c.secret?"":c.value??""}`),a.push(c.name));if(a.length>0){const c=r===""||r.endsWith(`
`)?r:`${r}
`;q(s,`${c}${d.join(`
`)}
`),n.success(`scaffolded ${String(a.length)} env var(s) into .dev.vars: ${a.join(", ")}`)}return i.length>0&&n.info(`set secret value(s) locally in .dev.vars, then for production: ${i.map(c=>`wrangler secret put ${c}`).join("; ")}`),a},E=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"]),Se=["binding","name","queue","pattern"],b=e=>{if(!(typeof e!="object"||e===null))for(const t of Se){const n=e[t];if(typeof n=="string"&&n.length>0)return`${t}:${n}`}},$e=(e,t,n,s)=>{const r=new Set(e.map(i=>JSON.stringify(i))),o=new Set(e.map(i=>b(i)).filter(i=>i!==void 0)),a=[];for(const i of t){if(r.has(JSON.stringify(i)))continue;const d=b(i);if(d!==void 0&&o.has(d)){s.warn(`${d.replace(":"," ")} already exists in ${n} — keeping the project's entry and skipping the registry item's. Reconcile by hand if the item needs different settings.`);continue}d!==void 0&&o.add(d),a.push(i)}return a},R=e=>Array.isArray(e),_=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),Ee=(e,t,n,s)=>{const r={};for(const[o,a]of Object.entries(t)){if(!Object.hasOwn(e,o)){r[o]=a;continue}JSON.stringify(e[o])!==JSON.stringify(a)&&s.warn(`${o} already exists in ${n} — keeping the project's value and skipping the registry item's. Reconcile by hand if the item needs different settings.`)}return r},be=(e,t)=>{let n=Y(e);for(const s of t){if(typeof n!="object"||n===null)return;n=n[s]}return n},Re=(e,t)=>{const n=e[0];return n!==void 0&&E.has(n)?!0:(t.warn(`skipping binding "${e.join(".")}" — only resource bindings (${[...E].join(", ")}) may be written, not exec/entrypoint keys`),!1)},m=Symbol("skip-binding"),_e=(e,t,n)=>{const{value:s}=t,r=t.path.join("."),o=be(e,t.path);if(R(s)){if(!R(o))return s;const a=$e(o,s,r,n);return a.length===0?m:[...o,...a]}if(_(s)){if(!_(o))return s;const a=Ee(o,s,r,n);return Object.keys(a).length===0?m:{...o,...a}}return s},Ae=(e,t,n)=>{if(e.length===0)return[];const r=["wrangler.jsonc","wrangler.json"].map(i=>f(t,i)).find(i=>p(i));if(!r)return n.warn("wrangler.jsonc not found — skipping binding updates"),[];let o=h(r,"utf8");const a=[];for(const i of e){if(!Re(i.path,n))continue;const d=_e(o,i,n);if(d===m)continue;const c=N(o,[...i.path],d,{formattingOptions:{insertSpaces:!0,tabSize:4}});c.length!==0&&(o=O(o,c),a.push(i.path.join(".")))}return a.length>0&&(j(r,o,"utf8"),n.success(`applied ${String(a.length)} binding(s) to ${r}: ${a.join(", ")}`)),a},ze=(e,t,n,s=!1,r)=>{const o=[],a=[];return e.deps&&o.push(...$(e.deps,t,n,"dependencies",s,r)),e.devDependencies&&o.push(...$(e.devDependencies,t,n,"devDependencies",s,r)),e.bindings&&a.push(...Ae(e.bindings,t,n)),e.envVars&&we(e.envVars,t,n),{bindings:a,deps:o}},y=e=>e.source!==void 0&&e.source.length>0||e.from!==void 0&&e.from.length>0,He=async(e,t)=>{const n=e.some(({manifest:c})=>Object.keys(c.deps??{}).length>0||Object.keys(c.devDependencies??{}).length>0),s=e.some(({manifest:c})=>(c.bindings??[]).length>0),r=y(t);if(!n&&!s&&!r||t.yes)return{ok:!0};const o=[];n&&o.push("add dependencies to package.json"),s&&o.push("write wrangler.jsonc bindings"),r&&o.push(`come from a custom registry source (${String(t.from??t.source)})`);const a=o.join(", ");return!process.stdin.isTTY&&t.confirm===void 0?(t.logger.error(`add: stdin is not a TTY and the requested items ${a} — re-run with --yes to confirm`),{ok:!1,reason:"non-interactive"}):await(t.confirm??Z)(`The requested items ${a}. Continue?`)?{ok:!0}:(t.logger.info("add: aborted"),{ok:!1,reason:"declined"})},U="gh:anolilab/lunora/registry",A=/^[A-Za-z0-9][\w-]*$/u,ke=e=>{if(!A.test(e))throw new g("INTERNAL",`invalid registry item name "${e}" — names must match ${A.source} (letters, digits, "-", "_"; no path separators or "..")`)},je=e=>e.includes("..")?!1:e.startsWith("gh:")||e.startsWith("github:")||e.startsWith("https://"),Ne=e=>y(e)&&e.from===void 0&&!e.allowUnsafeSource&&!je(e.source??""),Ke=(e,t)=>Ne(t)?`${e}: refusing --source ${String(t.source)} — only gh:, github:, or https:// sources are allowed (and may not contain ".."). Re-run with --allow-unsafe-source if you really want this.`:void 0,C=async(e,t,n)=>{const s=G(f(W(),`lunora-${t}-fetch-`)),r=f(s,t);n.info(`fetching ${e}`);try{const o=await J(e,{cwd:s,dir:r,force:!0,install:!1,silent:!0});return n.info(o.commit?`resolved ${o.source} @ ${o.commit}`:`resolved ${o.source}`),{cleanup:()=>{w(s,{force:!0,recursive:!0})},directory:r}}catch(o){throw w(s,{force:!0,recursive:!0}),o}},k=new WeakMap,x=async e=>{const t=k.get(e);if(t!==void 0)return t;const n=y(e)?Promise.resolve(I(e.ref)):de(e.ref,e.logger);return k.set(e,n),n},Oe=async(e,t,n=U)=>{if(ke(e),t.from!==void 0){const r=f(t.from,e);if(!p(r))throw new g("INTERNAL",`registry item not found in local source: ${r}`);return{cleanup:()=>{},directory:r}}const s=t.source??n;return C(`${s}/${e}#${await x(t)}`,"item",t.logger)},qe=async e=>{if(e.from!==void 0){if(!p(e.from))throw new g("INTERNAL",`registry root not found: ${e.from}`);return{cleanup:()=>{},root:e.from}}const t=e.source??U,{cleanup:n,directory:s}=await C(`${t}#${await x(e)}`,"registry",e.logger);return{cleanup:n,root:s}},Te=(e,t)=>{const n=JSON.parse(h(f(e,"registry.json"),"utf8"));return X(n,t)},Ye=async(e,t)=>{const n=[],s=[],r=new Set,o=new Set,a=async i=>{if(r.has(i))return;if(o.has(i))throw new g("INTERNAL",`cyclic registry dependency detected at "${i}"`);o.add(i);const{cleanup:d,directory:c}=await Oe(i,t);s.push(d);const l=Te(c,i);for(const u of l.requires??[])await a(u);o.delete(i),r.add(i),n.push({directory:c,manifest:l})};try{for(const i of e)await a(i)}catch(i){for(const d of s)d();throw i}return{cleanups:s,items:n}};export{v as a,de as b,Oe as c,ze as d,Je as e,Ye as f,Ge as g,He as h,y as i,qe as j,me as k,Te as l,he as m,ce as n,V as o,We as p,I as q,Fe as r,Ke as s};