@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.5 kB
JavaScript
import{existsSync as l,readFileSync as p,writeFileSync as N,mkdtempSync as F,rmSync as w}from"node:fs";import{tmpdir as G}from"node:os";import{LunoraError as h}from"@lunora/errors";import{dirname as S,join as f}from"@visulima/path";import{downloadTemplate as M}from"giget";import{fileURLToPath as J}from"node:url";import{DEV_VARS_FILE as W,parseDevVariableEntries as z,writeDevVariablesFileAtomically as H}from"@lunora/config";import{modify as k,applyEdits as T,parse as K}from"jsonc-parser";import{b as q}from"./tui-prompts-DzYd2fL3.mjs";import Y from"./parseManifest-CwPTKdtS.mjs";const Z="alpha",O="main",X=new Set(["alpha","beta","next"]),Q=/^[\w./@-]+$/,ee=e=>!e.includes("..")&&Q.test(e),y=()=>{try{let e=S(J(import.meta.url));for(let t=0;t<6;t+=1){const n=f(e,"package.json");if(l(n)){const r=JSON.parse(p(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 Z;const t=e.split("+")[0]??e,n=t.indexOf("-");if(n!==-1){const[s]=t.slice(n+1).split(".");if(s!==void 0&&X.has(s))return s}return O},I=e=>{if(e!==void 0&&e.length>0){if(!ee(e))throw new h("INTERNAL",`invalid --ref "${e}" — a ref may contain letters, digits, ".", "_", "-", "/", "@" and must not contain "..".`);return e}return D(y())},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.-]+)?`,te=new RegExp(String.raw`^v?${P}$`,"u"),ne=new RegExp(String.raw`^(?:@[\w.-]+\/)?[\w.-]+@${P}$`,"u"),se=e=>B.test(e)||te.test(e)||ne.test(e),re=()=>{const e=process.env.GITHUB_TOKEN??process.env.GH_TOKEN;return e!==void 0&&e.length>0?{authorization:`Bearer ${e}`}:{}},oe=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",...re()},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}},ie=async(e,t,n)=>{if(se(t))return t;const s=await oe(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)},ae=async(e,t)=>ie(L,I(e),t),Ce=(e=y())=>`@lunora/cli@${e}`,ce="latest",ue=(e=y())=>{const t=D(e);return t===O?ce:t},de="https://registry.npmjs.org",fe=()=>{const e=process.env.npm_config_registry,t=e!==void 0&&e.length>0?e:de;return t.endsWith("/")?t.slice(0,-1):t},le=async(e,t)=>{try{const n=await fetch(`${fe()}/${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}},xe=async(e,t)=>{const n=new Map;return await Promise.all([...new Set(e)].map(async s=>{const r=await le(s,t);r!==void 0&&n.set(s,r)})),n},pe=e=>{if(!e.startsWith("workspace:"))return e;const t=e.slice(10);return t===""||t==="*"||t==="^"||t==="~"?ue():t},he=new Set(["@lunora/client","@lunora/do","@lunora/ratelimit","@lunora/runtime","@lunora/server","@lunora/values"]),ge=/(['"])@lunora\/(client|do|ratelimit|runtime|server|values)(\/[^'"]*)?\1/gu,Fe=e=>{const t=f(e,"package.json");if(!l(t))return!1;try{const n=JSON.parse(p(t,"utf8"));return n.dependencies?.lunorash!==void 0||n.devDependencies?.lunorash!==void 0}catch{return!1}},Ge=e=>e.replaceAll(ge,(t,n,s,r)=>`${n}lunorash/${s}${r??""}${n}`),$=(e,t,n,s="dependencies",r=!1)=>{const o=Object.entries(e);if(o.length===0)return[];const a=f(t,"package.json");if(!l(a))return n.warn(`package.json not found at ${a} — skipping dependency updates`),[];let i=p(a,"utf8");const u=JSON.parse(i),c=[];for(const[d,g]of o){if(r&&he.has(d)){n.info(`dep provided by the lunorash umbrella, skipping: ${d}`);continue}if(u.dependencies?.[d]!==void 0||u.devDependencies?.[d]!==void 0){n.info(`dep already present: ${d}`);continue}const x=k(i,[s,d],pe(g),{formattingOptions:{insertSpaces:!0,tabSize:4}});i=T(i,x),c.push(d)}return c.length>0&&(N(a,i,"utf8"),n.success(`added ${String(c.length)} ${s==="devDependencies"?"devDependency(ies)":"dependency(ies)"} to package.json: ${c.join(", ")}`)),c},me=(e,t,n)=>{if(e.length===0)return[];const s=f(t,W),r=l(s)?p(s,"utf8"):"",o=new Set(z(r).map(c=>c.key)),a=[],i=[],u=[];for(const c of e)c.secret&&i.push(c.name),!o.has(c.name)&&(c.description&&u.push(`# ${c.description}`),u.push(`${c.name}=${c.secret?"":c.value??""}`),a.push(c.name));if(a.length>0){const c=r===""||r.endsWith(`
`)?r:`${r}
`;H(s,`${c}${u.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"]),ye=["binding","name","queue","pattern"],b=e=>{if(!(typeof e!="object"||e===null))for(const t of ye){const n=e[t];if(typeof n=="string"&&n.length>0)return`${t}:${n}`}},ve=(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 u=b(i);if(u!==void 0&&o.has(u)){s.warn(`${u.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}u!==void 0&&o.add(u),a.push(i)}return a},R=e=>Array.isArray(e),_=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),we=(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},Se=(e,t)=>{let n=K(e);for(const s of t){if(typeof n!="object"||n===null)return;n=n[s]}return n},$e=(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"),Ee=(e,t,n)=>{const{value:s}=t,r=t.path.join("."),o=Se(e,t.path);if(R(s)){if(!R(o))return s;const a=ve(o,s,r,n);return a.length===0?m:[...o,...a]}if(_(s)){if(!_(o))return s;const a=we(o,s,r,n);return Object.keys(a).length===0?m:{...o,...a}}return s},be=(e,t,n)=>{if(e.length===0)return[];const r=["wrangler.jsonc","wrangler.json"].map(i=>f(t,i)).find(i=>l(i));if(!r)return n.warn("wrangler.jsonc not found — skipping binding updates"),[];let o=p(r,"utf8");const a=[];for(const i of e){if(!$e(i.path,n))continue;const u=Ee(o,i,n);if(u===m)continue;const c=k(o,[...i.path],u,{formattingOptions:{insertSpaces:!0,tabSize:4}});c.length!==0&&(o=T(o,c),a.push(i.path.join(".")))}return a.length>0&&(N(r,o,"utf8"),n.success(`applied ${String(a.length)} binding(s) to ${r}: ${a.join(", ")}`)),a},Me=(e,t,n,s=!1)=>{const r=[],o=[];return e.deps&&r.push(...$(e.deps,t,n,"dependencies",s)),e.devDependencies&&r.push(...$(e.devDependencies,t,n,"devDependencies",s)),e.bindings&&o.push(...be(e.bindings,t,n)),e.envVars&&me(e.envVars,t,n),{bindings:o,deps:r}},v=e=>e.source!==void 0&&e.source.length>0||e.from!==void 0&&e.from.length>0,Je=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=v(t);if(!n&&!s&&!r||t.yes)return!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(", ");if(!process.stdin.isTTY&&t.confirm===void 0)return t.logger.error(`add: stdin is not a TTY and the requested items ${a} — re-run with --yes to confirm`),!1;const u=await(t.confirm??q)(`The requested items ${a}. Continue?`);return u||t.logger.info("add: aborted"),u},U="gh:anolilab/lunora/registry",A=/^[A-Za-z0-9][\w-]*$/u,Re=e=>{if(!A.test(e))throw new h("INTERNAL",`invalid registry item name "${e}" — names must match ${A.source} (letters, digits, "-", "_"; no path separators or "..")`)},_e=e=>e.includes("..")?!1:e.startsWith("gh:")||e.startsWith("github:")||e.startsWith("https://"),Ae=e=>v(e)&&e.from===void 0&&!e.allowUnsafeSource&&!_e(e.source??""),We=(e,t)=>Ae(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,V=async(e,t,n)=>{const s=F(f(G(),`lunora-${t}-fetch-`)),r=f(s,t);n.info(`fetching ${e}`);try{const o=await M(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}},j=new WeakMap,C=async e=>{const t=j.get(e);if(t!==void 0)return t;const n=v(e)?Promise.resolve(I(e.ref)):ae(e.ref,e.logger);return j.set(e,n),n},je=async(e,t,n=U)=>{if(Re(e),t.from!==void 0){const r=f(t.from,e);if(!l(r))throw new h("INTERNAL",`registry item not found in local source: ${r}`);return{cleanup:()=>{},directory:r}}const s=t.source??n;return V(`${s}/${e}#${await C(t)}`,"item",t.logger)},ze=async e=>{if(e.from!==void 0){if(!l(e.from))throw new h("INTERNAL",`registry root not found: ${e.from}`);return{cleanup:()=>{},root:e.from}}const t=e.source??U,{cleanup:n,directory:s}=await V(`${t}#${await C(e)}`,"registry",e.logger);return{cleanup:n,root:s}},Ne=(e,t)=>{const n=JSON.parse(p(f(e,"registry.json"),"utf8"));return Y(n,t)},He=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 h("INTERNAL",`cyclic registry dependency detected at "${i}"`);o.add(i);const{cleanup:u,directory:c}=await je(i,t);s.push(u);const d=Ne(c,i);for(const g of d.requires??[])await a(g);o.delete(i),r.add(i),n.push({directory:c,manifest:d})};try{for(const i of e)await a(i)}catch(i){for(const u of s)u();throw i}return{cleanups:s,items:n}};export{y as a,ae as b,je as c,Me as d,Ge as e,He as f,Je as g,ze as h,v as i,pe as j,Ne as k,xe as l,ie as m,ue as n,I as o,Fe as p,Ce as r,We as s};