@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.4 kB
JavaScript
import{existsSync as E,readFileSync as U}from"node:fs";import{join as P}from"node:path";import{DEV_VARS_FILE as m,DEV_VARS_KEY_PATTERN as b,writeDevVariablesFileAtomically as w,removeDevVariableLine as N,upsertDevVariableLine as D,isPlaceholderValue as S,resolveDeployDriver as K,resolveProjectTarget as j,generateSecretValue as z,DEV_VARS_EXAMPLE_FILE as g,parseDevVariableEntries as _,secretSourcesFromBindings as T,inferLunoraBindings as C,resolveSchemaDirectory as F,mintableSecretKeysFor as M,requiredSecrets as O}from"@lunora/config";import{d as Y}from"../packem_shared/command-B6vu47iR.mjs";import{EXIT_CODE as d}from"../packem_shared/EXIT_CODE-DVjUgIaG.mjs";import{defaultSpawner as q}from"../packem_shared/createRecordingSpawner-01ZngIwK.mjs";import{l as Z}from"../packem_shared/wrangler-secrets-DlSlWvRH.mjs";import{detectPackageManager as B,toolchainExecArgs as I}from"@lunora/config/cli";const X=/[\r\n]/u,H=/["\\]/u,G=e=>new Map(_(e).map(s=>[s.key,s])),h=e=>E(e)?U(e,"utf8"):"",L=e=>e.length<=4?"****":`${e.slice(0,4)}${"*".repeat(Math.min(8,e.length-4))}`,p=e=>G(h(e)),$=e=>e.env??(e.prod===!0?"production":void 0),W=e=>{const s=p(e.devVariablesPath),t={file:m,keys:[...s.keys()],subcommand:"list"};if(s.size===0)return e.logger.info(`${m}: (empty)`),{code:0,data:t,descriptors:[]};for(const r of s.values())e.logger.info(`${r.key}=${L(r.value)}`);return{code:0,data:t,descriptors:[]}},J=e=>{const{devVariablesPath:s,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 o=p(s).get(r.key);return o?(r.format!=="json"&&process.stdout.write(`${o.value}
`),{code:0,data:{key:r.key,subcommand:"get",value:o.value},descriptors:[]}):(t.error(`env: ${r.key} is not set in ${m}`),{code:d.NOT_FOUND,descriptors:[]})},Q=e=>{const{devVariablesPath:s,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 o=h(s);return w(s,D(o,r.key,r.value)),t.success(`env: set ${r.key} (${L(r.value)}) in ${m}`),{code:0,data:{key:r.key,subcommand:"set"},descriptors:[]}},x=e=>{const{devVariablesPath:s,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 o=h(s);return G(o).has(r.key)?(w(s,N(o,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:s,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 o=p(s);if(o.size===0)return t.warn(`${m}: nothing to push (empty)`),{code:0,data:{pushed:[],subcommand:"push",target:$(r)},descriptors:[]};const n=[...o.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??q,u=r.cwd??process.cwd(),l=B(u),i=[],v=$(r),f=K(j(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 o.values()){const R=y({environment:v,key:a.key,temporary:r.temporary}),A=I(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(o.size)} secret(s)`),{code:0,data:{pushed:[...o.keys()],subcommand:"push",target:v},descriptors:i}},re=e=>(e.options.secretLister??Z)({cwd:e.cwd,env:$(e.options),temporary:e.options.temporary}),te=async e=>{const{devVariablesPath:s,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 o=new Set(p(s).keys()),n=new Set(r.names),c=[...o].filter(i=>!n.has(i)).toSorted((i,v)=>i.localeCompare(v)),u=r.names.filter(i=>!o.has(i)),l=[...o].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:s,devVariablesPath:t,logger:r}=e,o=P(s,g);if(!E(o))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=_(U(o,"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 s=[];try{s=T(await C({projectRoot:e.cwd,schemaDir:F(e.cwd)}))}catch{}const t=M(s),r=O(s).map(n=>n.key).filter(n=>t.has(n)),o=[...p(e.devVariablesPath).keys()].filter(n=>t.has(n));return[...new Set([...r,...o])]},ne=(e,s,t)=>{const r=p(e),o=n=>{const c=r.get(n);return c!==void 0&&c.value!==""&&!S(c.value)};return{live:s.filter(n=>o(n.key)).map(n=>n.key),writable:t?s:s.filter(n=>!o(n.key))}},ae=(e,s)=>{const{devVariablesPath:t,logger:r,options:o}=e,{live:n,writable:c}=ne(t,s,o.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=D(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:s,options:t}=e;let r;if(t.key===void 0){if(r=await oe(e),r.length===0)return s.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 s.error(`env: invalid key "${t.key}" — must match [A-Za-z_][A-Za-z0-9_]*`),{code:d.USAGE,descriptors:[]};r=[t.key]}const o=r.map(n=>({key:n,value:z()}));if(t.set===!0)return ae(e,o);if(t.format!=="json")for(const n of o)process.stdout.write(`${n.key}=${n.value}
`);return{code:0,data:{secrets:o,subcommand:"generate"},descriptors:[]}},ie=async e=>{const{options:s}=e;switch(s.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 "${s.subcommand}"`),{code:d.USAGE,descriptors:[]}}},de=async e=>{const s=e.cwd??process.cwd(),t={cwd:s,devVariablesPath:P(s,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),Ee=Y(({argument:e,cwd:s,format:t,logger:r,options:o})=>{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:s,env:o.env,format:t,key:e[1],logger:r,prod:o.prod===!0,set:o.set===!0,subcommand:n,temporary:o.temporary===!0,value:e[2],yes:o.yes===!0})});export{Ee as execute,de as runEnvCommand};