@lunora/cli
Version:
The Lunora CLI: init, dev, deploy, codegen, migrate, seed, doctor, insights, logs, registry, and the rest of the project commands
4 lines (3 loc) • 9.35 kB
JavaScript
import{existsSync as E,readFileSync as P}from"node:fs";import{join as D}from"node:path";import{DEV_VARS_FILE as m,DEV_VARS_KEY_PATTERN as b,writeDevVariablesFileAtomically as w,removeDevVariableLine as K,upsertDevVariableLine as _,isPlaceholderValue as S,detectPackageManager as j,resolveDeployDriver as z,resolveProjectTarget as M,toolchainExecArgs as T,generateSecretValue as C,DEV_VARS_EXAMPLE_FILE as g,parseDevVariableEntries as G,packageNamesFromBindings as O,inferLunoraBindings as Y,resolveSchemaDirectory as F,requiredSecrets as q,isMintableSecretKey as U}from"@lunora/config";import{d as Z}from"../packem_shared/command-CAXl2A1S.mjs";import{EXIT_CODE as d}from"../packem_shared/EXIT_CODE-D_XCixv8.mjs";import{defaultSpawner as B}from"../packem_shared/createRecordingSpawner-CYhdPBD5.mjs";import{l as I}from"../packem_shared/wrangler-secrets-Bqi6kLuq.mjs";const X=/[\r\n]/u,H=/["\\]/u,L=e=>new Map(G(e).map(o=>[o.key,o])),h=e=>E(e)?P(e,"utf8"):"",N=e=>e.length<=4?"****":`${e.slice(0,4)}${"*".repeat(Math.min(8,e.length-4))}`,p=e=>L(h(e)),$=e=>e.env??(e.prod===!0?"production":void 0),W=e=>{const o=p(e.devVariablesPath),t={file:m,keys:[...o.keys()],subcommand:"list"};if(o.size===0)return e.logger.info(`${m}: (empty)`),{code:0,data:t,descriptors:[]};for(const r of o.values())e.logger.info(`${r.key}=${N(r.value)}`);return{code:0,data:t,descriptors:[]}},J=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:d.USAGE,descriptors:[]};const s=p(o).get(r.key);return s?(r.format!=="json"&&process.stdout.write(`${s.value}
`),{code:0,data:{key:r.key,subcommand:"get",value:s.value},descriptors:[]}):(t.error(`env: ${r.key} is not set in ${m}`),{code:d.NOT_FOUND,descriptors:[]})},Q=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:d.USAGE,descriptors:[]};if(!b.test(r.key))return t.error(`env: invalid key "${r.key}" — must match [A-Za-z_][A-Za-z0-9_]*`),{code:d.USAGE,descriptors:[]};if(r.value===void 0)return t.error("env set requires a value. Usage: lunora env set <KEY> <VALUE>"),{code:d.USAGE,descriptors:[]};if(X.test(r.value))return t.error(`env: value for "${r.key}" contains a newline, which .dev.vars cannot represent`),{code:d.USAGE,descriptors:[]};if(H.test(r.value))return t.error(`env: value for "${r.key}" contains a double-quote or backslash, which .dev.vars cannot round-trip`),{code:d.USAGE,descriptors:[]};const s=h(o);return w(o,_(s,r.key,r.value)),t.success(`env: set ${r.key} (${N(r.value)}) in ${m}`),{code:0,data:{key:r.key,subcommand:"set"},descriptors:[]}},x=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:d.USAGE,descriptors:[]};if(!b.test(r.key))return t.error(`env: invalid key "${r.key}" — must match [A-Za-z_][A-Za-z0-9_]*`),{code:d.USAGE,descriptors:[]};const s=h(o);return L(s).has(r.key)?(w(o,K(s,r.key)),t.success(`env: unset ${r.key} in ${m}`),{code:0,data:{key:r.key,removed:!0,subcommand:"unset"},descriptors:[]}):(t.warn(`env: ${r.key} was not set in ${m}`),{code:0,data:{key:r.key,removed:!1,subcommand:"unset"},descriptors:[]})},ee=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:d.USAGE,descriptors:[]};const s=p(o);if(s.size===0)return t.warn(`${m}: nothing to push (empty)`),{code:0,data:{pushed:[],subcommand:"push",target:$(r)},descriptors:[]};const n=[...s.values()].filter(a=>S(a.value)).map(a=>a.key);if(n.length>0)return t.error(`env push: refusing to upload placeholder value(s) for ${n.join(", ")} — run \`lunora env doctor\` to review, and \`lunora env generate --set\` (or \`lunora env set <KEY> <VALUE>\`) to fill them, then re-run.`),{code:d.USAGE,descriptors:[]};const c=r.spawner??B,u=r.cwd??process.cwd(),l=j(u),i=[],v=$(r),f=z(M(u)),y=f.toolchain?.secretPut;if(y===void 0)return t.error(`deploy target "${f.id}" has no secret store; cannot push secrets`),{code:d.USAGE,descriptors:[]};for(const a of s.values()){const R=y({environment:v,key:a.key,temporary:r.temporary}),A=T(l,R),V={args:A.args,command:A.command,cwd:u,input:a.value};i.push(V),t.info(`pushing ${a.key} -> wrangler secret${v===void 0?"":` (${v})`}`);const k=await c(V);if(k.code!==0)return t.error(`env push: failed at ${a.key} (exit ${String(k.code)})`),{code:k.code,descriptors:i}}return t.success(`env: pushed ${String(s.size)} secret(s)`),{code:0,data:{pushed:[...s.keys()],subcommand:"push",target:v},descriptors:i}},re=e=>(e.options.secretLister??I)({cwd:e.cwd,env:$(e.options),temporary:e.options.temporary}),te=async e=>{const{devVariablesPath:o,logger:t}=e,r=await re(e);if(!r.ok)return t.error(`env diff: ${r.error??"failed to list remote secrets"}`),{code:1,descriptors:[]};const s=new Set(p(o).keys()),n=new Set(r.names),c=[...s].filter(i=>!n.has(i)).toSorted((i,v)=>i.localeCompare(v)),u=r.names.filter(i=>!s.has(i)),l=[...s].filter(i=>n.has(i)).toSorted((i,v)=>i.localeCompare(v));for(const i of c)t.info(`local only (run \`lunora env push\`): ${i}`);for(const i of u)t.info(`remote only (in Cloudflare, not ${m}): ${i}`);return t.info(`in both: ${String(l.length)} secret(s)`),c.length===0&&u.length===0&&t.success("env diff: local and remote secret names match"),{code:0,data:{both:l,localOnly:c,remoteOnly:u,subcommand:"diff"},descriptors:[]}},se=e=>{const{cwd:o,devVariablesPath:t,logger:r}=e,s=D(o,g);if(!E(s))return r.info(`env doctor: no ${g} to check against — nothing to validate.`),{code:0,data:{extra:[],missing:[],ok:!0,placeholders:[],subcommand:"doctor"},descriptors:[]};const n=G(P(s,"utf8")).map(a=>a.key),c=p(t);if(!E(t))return r.error(`env doctor: ${m} is missing. Run \`lunora dev\` to scaffold it, or \`lunora env set <KEY> <VALUE>\`.`),r.info(`expected (from ${g}): ${n.join(", ")}`),{code:d.NOT_FOUND,descriptors:[]};const u=n.filter(a=>!c.has(a)),l=[...c.values()].filter(a=>S(a.value)).map(a=>a.key),i=new Set(n),v=[...c.keys()].filter(a=>!i.has(a));for(const a of u)r.error(`missing: ${a} is in ${g} but not ${m}`);for(const a of l)r.error(`unset: ${a} still has a placeholder value`);for(const a of v)r.info(`extra: ${a} is set locally but not listed in ${g}`);const f=u.length===0&&l.length===0,y={extra:v,missing:u,ok:f,placeholders:l,subcommand:"doctor"};return f?(r.success(`env doctor: ${m} looks good (${String(c.size)} var(s)).`),{code:0,data:y,descriptors:[]}):{code:1,data:y,descriptors:[]}},oe=async e=>{let o=[];try{o=O(await Y({projectRoot:e.cwd,schemaDir:F(e.cwd)}))}catch{}const t=q(o).map(s=>s.key).filter(s=>U(s)),r=[...p(e.devVariablesPath).keys()].filter(s=>U(s));return[...new Set([...t,...r])]},ne=(e,o,t)=>{const r=p(e),s=n=>{const c=r.get(n);return c!==void 0&&c.value!==""&&!S(c.value)};return{live:o.filter(n=>s(n.key)).map(n=>n.key),writable:t?o:o.filter(n=>!s(n.key))}},ae=(e,o)=>{const{devVariablesPath:t,logger:r,options:s}=e,{live:n,writable:c}=ne(t,o,s.yes===!0);if(c.length===0)return r.error(`env generate --set: ${n.join(", ")} already hold(s) a live secret and rotating it is irreversible (outstanding signed URLs and bearers stop verifying). Re-run with --yes to rotate anyway.`),{code:d.USAGE,descriptors:[]};n.length>0&&r.warn(`env generate --set: left ${n.join(", ")} untouched (already set) — re-run with --yes to rotate them too`);let u=h(t);for(const l of c)u=_(u,l.key,l.value);return w(t,u),r.success(`env: generated ${String(c.length)} secret(s) into ${m}: ${c.map(l=>l.key).join(", ")}`),{code:0,data:{subcommand:"generate",written:c.map(l=>l.key)},descriptors:[]}},ce=async e=>{const{logger:o,options:t}=e;let r;if(t.key===void 0){if(r=await oe(e),r.length===0)return o.info("env generate: no locally-generatable secrets for this project. Name one explicitly: lunora env generate <KEY>"),{code:0,data:{secrets:[],subcommand:"generate"},descriptors:[]}}else{if(!b.test(t.key))return o.error(`env: invalid key "${t.key}" — must match [A-Za-z_][A-Za-z0-9_]*`),{code:d.USAGE,descriptors:[]};r=[t.key]}const s=r.map(n=>({key:n,value:C()}));if(t.set===!0)return ae(e,s);if(t.format!=="json")for(const n of s)process.stdout.write(`${n.key}=${n.value}
`);return{code:0,data:{secrets:s,subcommand:"generate"},descriptors:[]}},ie=async e=>{const{options:o}=e;switch(o.subcommand){case"diff":return te(e);case"doctor":return se(e);case"generate":return ce(e);case"get":return J(e);case"list":return W(e);case"push":return ee(e);case"set":return Q(e);case"unset":return x(e);default:return e.logger.error(`env: unknown subcommand "${o.subcommand}"`),{code:d.USAGE,descriptors:[]}}},de=async e=>{const o=e.cwd??process.cwd(),t={cwd:o,devVariablesPath:D(o,m),logger:e.logger,options:e};return ie(t)},ue=new Set(["diff","doctor","generate","get","list","push","set","unset"]),le=e=>typeof e=="string"&&ue.has(e),ke=Z(({argument:e,cwd:o,format:t,logger:r,options:s})=>{const n=e[0];if(!le(n)){const c=`env: unknown subcommand "${n??""}" — expected list | get | set | unset | push | diff | doctor | generate`;return r.error(c),{code:d.USAGE,error:c}}return de({cwd:o,env:s.env,format:t,key:e[1],logger:r,prod:s.prod===!0,set:s.set===!0,subcommand:n,temporary:s.temporary===!0,value:e[2],yes:s.yes===!0})});export{ke as execute,de as runEnvCommand};