UNPKG

@lunora/cli

Version:

The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands

9 lines (8 loc) 7.46 kB
import{existsSync as f,readFileSync as m,writeFileSync as $}from"node:fs";import{join as S}from"node:path";import{DEV_VARS_FILE as d,DEV_VARS_KEY_PATTERN as b,generateSecretValue as _,DEV_VARS_EXAMPLE_FILE as v,parseDevVariableEntries as E,isPlaceholderValue as x,packageNamesFromBindings as R,inferLunoraBindings as z,requiredSecrets as L,isMintableSecretKey as k}from"@lunora/config";import{i as K}from"../packem_shared/command-Bjv4VmYA.mjs";import{P as Y,M as q}from"../packem_shared/detect-package-manager-B2eIuAiP.mjs";import{defaultSpawner as U}from"../packem_shared/createRecordingSpawner-C8_7nffs.mjs";import{f as j}from"../packem_shared/wrangler-secrets-BMRJwKQU.mjs";const C=/[\r\n]/u,F=/["\\]/u,V=e=>{const o=new Map;for(const t of E(e))o.set(t.key,t);return o},M=e=>e.replaceAll(/[.*+?^${}()|[\]\\]/gu,String.raw`\$&`),w=(e,o)=>new RegExp(String.raw`^[ \t]*${M(e)}[ \t]*=.*(\r?\n|$)`,o?"gmu":"mu"),P=(e,o,t)=>{const r=`${o}="${t}"`;if(!w(o,!1).test(e))return e===""?`${r} `:e.endsWith(` `)?`${e}${r} `:`${e} ${r} `;let s=!1;return e.replace(w(o,!0),(i,n)=>s?"":(s=!0,`${r}${n}`))},Z=(e,o)=>e.replaceAll(new RegExp(String.raw`^[ \t]*${o}[ \t]*=.*(?:\r?\n|$)`,"gmu"),""),A=e=>e.length<=4?"****":`${e.slice(0,4)}${"*".repeat(Math.min(8,e.length-4))}`,g=e=>f(e)?V(m(e,"utf8")):new Map,D=e=>{const o=g(e.devVariablesPath);if(o.size===0)return e.logger.info(`${d}: (empty)`),{code:0,descriptors:[]};for(const t of o.values())e.logger.info(`${t.key}=${A(t.value)}`);return{code:0,descriptors:[]}},B=e=>{const{devVariablesPath:o,logger:t,options:r}=e;if(!r.key)return t.error("env get requires a key. Usage: lunora env get <KEY>"),{code:1,descriptors:[]};const s=g(o).get(r.key);return s?(process.stdout.write(`${s.value} `),{code:0,descriptors:[]}):(t.error(`env: ${r.key} is not set in ${d}`),{code:1,descriptors:[]})},I=e=>{const{devVariablesPath:o,logger:t,options:r}=e;if(!r.key)return t.error("env set requires a key. Usage: lunora env set <KEY> <VALUE>"),{code:1,descriptors:[]};if(!b.test(r.key))return t.error(`env: invalid key "${r.key}" — must match [A-Za-z_][A-Za-z0-9_]*`),{code:1,descriptors:[]};if(r.value===void 0)return t.error("env set requires a value. Usage: lunora env set <KEY> <VALUE>"),{code:1,descriptors:[]};if(C.test(r.value))return t.error(`env: value for "${r.key}" contains a newline, which .dev.vars cannot represent`),{code:1,descriptors:[]};if(F.test(r.value))return t.error(`env: value for "${r.key}" contains a double-quote or backslash, which .dev.vars cannot round-trip`),{code:1,descriptors:[]};const s=f(o)?m(o,"utf8"):"";return $(o,P(s,r.key,r.value),"utf8"),t.success(`env: set ${r.key} (${A(r.value)}) in ${d}`),{code:0,descriptors:[]}},N=e=>{const{devVariablesPath:o,logger:t,options:r}=e;if(!r.key)return t.error("env unset requires a key. Usage: lunora env unset <KEY>"),{code:1,descriptors:[]};const s=f(o)?m(o,"utf8"):"";return V(s).has(r.key)?($(o,Z(s,r.key),"utf8"),t.success(`env: unset ${r.key} in ${d}`),{code:0,descriptors:[]}):(t.warn(`env: ${r.key} was not set in ${d}`),{code:0,descriptors:[]})},T=async e=>{const{devVariablesPath:o,logger:t,options:r}=e;if(!r.yes)return t.error("env push uploads secrets to Cloudflare. Re-run with --yes to confirm."),{code:1,descriptors:[]};const s=g(o);if(s.size===0)return t.warn(`${d}: nothing to push (empty)`),{code:0,descriptors:[]};const i=r.spawner??U,n=r.cwd??process.cwd(),u=Y(n),p=[];for(const a of s.values()){const l=["secret","put",a.key];r.prod&&l.push("--env","production"),r.temporary&&l.push("--temporary");const c=q(u,"wrangler",l),h={args:c.args,command:c.command,cwd:n,input:a.value};p.push(h),t.info(`pushing ${a.key} -> wrangler secret${r.prod?" (production)":""}`);const y=await i(h);if(y.code!==0)return t.error(`env push: failed at ${a.key} (exit ${String(y.code)})`),{code:y.code,descriptors:p}}return t.success(`env: pushed ${String(s.size)} secret(s)`),{code:0,descriptors:p}},W=e=>(e.options.secretLister??j)({cwd:e.cwd,env:e.options.prod?"production":void 0,temporary:e.options.temporary}),X=async e=>{const{devVariablesPath:o,logger:t}=e,r=await W(e);if(!r.ok)return t.error(`env diff: ${r.error??"failed to list remote secrets"}`),{code:1,descriptors:[]};const s=new Set(g(o).keys()),i=new Set(r.names),n=[...s].filter(a=>!i.has(a)).toSorted((a,l)=>a.localeCompare(l)),u=r.names.filter(a=>!s.has(a)),p=[...s].filter(a=>i.has(a)).toSorted((a,l)=>a.localeCompare(l));for(const a of n)t.info(`local only (run \`lunora env push\`): ${a}`);for(const a of u)t.info(`remote only (in Cloudflare, not ${d}): ${a}`);return t.info(`in both: ${String(p.length)} secret(s)`),n.length===0&&u.length===0&&t.success("env diff: local and remote secret names match"),{code:0,descriptors:[]}},G=e=>{const{cwd:o,devVariablesPath:t,logger:r}=e,s=S(o,v);if(!f(s))return r.info(`env doctor: no ${v} to check against — nothing to validate.`),{code:0,descriptors:[]};const i=E(m(s,"utf8")).map(c=>c.key),n=g(t);if(!f(t))return r.error(`env doctor: ${d} is missing. Run \`lunora dev\` to scaffold it, or \`lunora env set <KEY> <VALUE>\`.`),r.info(`expected (from ${v}): ${i.join(", ")}`),{code:1,descriptors:[]};const u=i.filter(c=>!n.has(c)),p=[...n.values()].filter(c=>x(c.value)).map(c=>c.key),a=new Set(i),l=[...n.keys()].filter(c=>!a.has(c));for(const c of u)r.error(`missing: ${c} is in ${v} but not ${d}`);for(const c of p)r.error(`unset: ${c} still has a placeholder value`);for(const c of l)r.info(`extra: ${c} is set locally but not listed in ${v}`);return u.length===0&&p.length===0?(r.success(`env doctor: ${d} looks good (${String(n.size)} var(s)).`),{code:0,descriptors:[]}):{code:1,descriptors:[]}},H=async e=>{let o=[];try{o=R(await z({projectRoot:e.cwd}))}catch{}const t=L(o).map(s=>s.key).filter(s=>k(s)),r=[...g(e.devVariablesPath).keys()].filter(s=>k(s));return[...new Set([...t,...r])]},J=async e=>{const{devVariablesPath:o,logger:t,options:r}=e;let s;if(r.key===void 0){if(s=await H(e),s.length===0)return t.info("env generate: no locally-generatable secrets for this project. Name one explicitly: lunora env generate <KEY>"),{code:0,descriptors:[]}}else{if(!b.test(r.key))return t.error(`env: invalid key "${r.key}" — must match [A-Za-z_][A-Za-z0-9_]*`),{code:1,descriptors:[]};s=[r.key]}const i=s.map(n=>({key:n,value:_()}));if(r.set===!0){let n=f(o)?m(o,"utf8"):"";for(const u of i)n=P(n,u.key,u.value);return $(o,n,"utf8"),t.success(`env: generated ${String(i.length)} secret(s) into ${d}: ${i.map(u=>u.key).join(", ")}`),{code:0,descriptors:[]}}for(const n of i)process.stdout.write(`${n.key}=${n.value} `);return{code:0,descriptors:[]}},Q=async e=>{const o=e.cwd??process.cwd(),t={cwd:o,devVariablesPath:S(o,d),logger:e.logger,options:e};switch(e.subcommand){case"diff":return X(t);case"doctor":return G(t);case"generate":return J(t);case"get":return B(t);case"list":return D(t);case"push":return T(t);case"set":return I(t);case"unset":return N(t);default:return e.logger.error(`env: unknown subcommand "${e.subcommand}"`),{code:1,descriptors:[]}}},O=e=>e==="list"||e==="get"||e==="set"||e==="unset"||e==="push"||e==="diff"||e==="doctor"||e==="generate",ce=K(({argument:e,cwd:o,logger:t,options:r})=>{const s=e[0];return O(s)?Q({cwd:o,key:e[1],logger:t,prod:r.prod===!0,set:r.set===!0,subcommand:s,temporary:r.temporary===!0,value:e[2],yes:r.yes===!0}):(t.error(`env: unknown subcommand "${s??""}" — expected list | get | set | unset | push | diff | doctor | generate`),{code:1})});export{ce as execute,Q as runEnvCommand};