@lunora/cli
Version:
The Lunora CLI: init, dev, deploy, codegen, migrate, seed, doctor, insights, logs, registry, and the rest of the project commands
3 lines (2 loc) • 12.1 kB
JavaScript
import{r as _,c as g}from"./cloudflare-credentials-BVmZnDZk.mjs";import{spawnSync as I}from"node:child_process";import{readFileSync as p,writeFileSync as x,existsSync as E}from"node:fs";import{join as w}from"node:path";import{readProjectTarget as P,DEFAULT_TARGET as N}from"@lunora/codegen";import{DEV_VARS_FILE as u,parseDevVariableEntries as k,upsertDevVariableLine as L,generateSecretValue as F,writeDevVariablesFileAtomically as U,isPlaceholderValue as y,isMintableSecretKey as K,mintableSecretKeysFor as M,secretSourcesFromBindings as W,inferLunoraBindings as V,resolveSchemaDirectory as B}from"@lunora/config";import{modify as Y,applyEdits as z,parse as H}from"jsonc-parser";import{r as G}from"./admin-token-VdUnvnKW.mjs";import{defaultSpawner as J}from"./createRecordingSpawner-01ZngIwK.mjs";import{toolchainExecArgs as X,detectPackageManager as q}from"@lunora/config/cli";const b=5e3,D=(e,t,s)=>`POST https://api.cloudflare.com/client/v4/accounts/${e}/artifacts/namespaces with body { "namespace": "${t}", "jurisdiction": "${s}" }`,Q=e=>{let t="";try{const{errors:s}=JSON.parse(e.text);t=(s??[]).flatMap(n=>typeof n.message=="string"?[n.message]:[]).slice(0,2).join("; ")}catch{}return t===""?`HTTP ${String(e.status)}`:`HTTP ${String(e.status)}: ${t}`},Z=async(e,t,s)=>{const{jurisdiction:n,namespace:r}=e;try{const a=await g({accountId:t,apiToken:s,...e.fetch===void 0?{}:{fetch:e.fetch},init:{body:JSON.stringify({jurisdiction:n,namespace:r}),method:"POST",signal:AbortSignal.timeout(b)},path:"/artifacts/namespaces"});return a.ok?{}:{reason:Q(a)}}catch(a){return{reason:a instanceof Error?a.message:String(a)}}},ee=async(e,t,s)=>{const n=await g({accountId:t,apiToken:s,...e.fetch===void 0?{}:{fetch:e.fetch},init:{signal:AbortSignal.timeout(b)},path:`/artifacts/namespaces/${encodeURIComponent(e.namespace)}`}).catch(()=>{}),r=n?.ok===!0?n.body.result:void 0;return typeof r=="object"&&r!==null&&"jurisdiction"in r?r.jurisdiction:void 0},j=(e,t,s,n)=>({fix:`A namespace's jurisdiction cannot change: create one with ${D(e,"<name>",s)}, point the binding's "namespace" at it, and move the repos over.`,message:`${t} is in the "${n}" jurisdiction, but the schema pins data to "${s}".`,verdict:"mismatch"}),te=async(e,t,s,n)=>{const{jurisdiction:r,namespace:a}=e;let i;if(e.create===!0){if(i=(await Z(e,t,s)).reason,i===void 0)return{changesAccount:!0,message:`created ${n} in the "${r}" jurisdiction, as the schema pins.`,verdict:"ok"};const l=await ee(e,t,s);if(l===r)return{message:`${n} is in the "${r}" jurisdiction, as the schema pins.`,verdict:"ok"};if(typeof l=="string")return j(t,n,r,l)}if(e.missingOk===!0)return{changesAccount:!0,message:`${n} does not exist yet; it will be created in the "${r}" jurisdiction just before the deploy.`,verdict:"ok"};const c=i===void 0?"":` (${i})`,o=e.create===!0?` and could not be created${c}`:"";return{fix:`Create it before the first repo: ${D(t,a,r)}.`,message:`${n} does not exist${o}, and the first repo create() would make it unrestricted — the schema pins data to "${r}".`,verdict:"missing"}},Ce=async e=>{const{binding:t,jurisdiction:s,namespace:n}=e,r=`Artifacts namespace "${n}" (binding ${t})`,{accountId:a,token:i}=_(e.environment??process.env,e.accountId);if(i===void 0||a===void 0)return{fix:"Set CLOUDFLARE_API_TOKEN (with Artifacts edit access, which creating a missing namespace needs) and CLOUDFLARE_ACCOUNT_ID (or `account_id` in wrangler.jsonc) to check it.",message:`the jurisdiction of ${r} was not checked against the schema's "${s}".`,verdict:"unchecked"};let c;try{c=await g({accountId:a,apiToken:i,...e.fetch===void 0?{}:{fetch:e.fetch},init:{signal:AbortSignal.timeout(b)},path:`/artifacts/namespaces/${encodeURIComponent(n)}`})}catch(l){return{message:`the jurisdiction of ${r} could not be read (${l instanceof Error?l.message:String(l)}).`,verdict:"unchecked"}}if(!c.ok&&c.status===404)return te(e,a,i,r);if(!c.ok)return{...c.status===401||c.status===403?{fix:`Give CLOUDFLARE_API_TOKEN Artifacts edit access on account ${a}.`}:{},message:`the jurisdiction of ${r} could not be read (HTTP ${String(c.status)}).`,verdict:"unchecked"};const{result:o}=c.body,d=typeof o=="object"&&o!==null&&"jurisdiction"in o?o.jurisdiction:void 0;return typeof d!="string"?{message:`the jurisdiction of ${r} could not be read (the response carried none).`,verdict:"unchecked"}:d===s?{message:`${r} is in the "${d}" jurisdiction, as the schema pins.`,verdict:"ok"}:j(a,r,s,d)},m=100,f=500,S=10,ne=5e3,h="Delete the Workers you no longer need (old preview / staging deploys free their classes), or set `durableObjects: { merge: true }` in lunora.config before an app's first deploy so it spends one class instead of up to three.",re=e=>e>=f?{fix:h,message:`the Cloudflare account has ${String(e)} Durable Object classes — the Workers Paid cap is ${String(f)}, so a deploy that adds one will fail.`,verdict:"full"}:e>=f-S?{fix:h,message:`the Cloudflare account has ${String(e)} of ${String(f)} Durable Object classes (Workers Paid cap).`,verdict:"near"}:e>=m-S&&e<=m?{fix:h,message:`the Cloudflare account has ${String(e)} Durable Object classes — on Workers Free the cap is ${String(m)} (Paid: ${String(f)}).`,verdict:"near"}:{message:`the Cloudflare account has ${String(e)} Durable Object classes (caps: ${String(m)} Free, ${String(f)} Paid).`,verdict:"ok"},Ie=async(e={})=>{const{accountId:t,token:s}=_(e.environment??process.env,e.accountId);if(s===void 0||t===void 0)return{fix:"Set CLOUDFLARE_API_TOKEN (Workers Scripts Read) and CLOUDFLARE_ACCOUNT_ID (or `account_id` in wrangler.jsonc) to check it.",message:"the account's Durable Object class count was not checked.",verdict:"unchecked"};try{const n=await g({accountId:t,apiToken:s,...e.fetch===void 0?{}:{fetch:e.fetch},init:{signal:AbortSignal.timeout(ne)},path:"/workers/durable_objects/namespaces?per_page=1"}),r=n.ok?n.body.result_info:void 0,a=typeof r=="object"&&r!==null&&"total_count"in r?r.total_count:void 0;return typeof a!="number"?{message:`the account's Durable Object class count could not be read${n.ok?"":` (HTTP ${String(n.status)})`}.`,verdict:"unchecked"}:re(a)}catch(n){return{message:`the account's Durable Object class count could not be read (${n instanceof Error?n.message:String(n)}).`,verdict:"unchecked"}}},$=e=>e.endsWith("S")?e.slice(0,-1):e,T=e=>e.replaceAll(/([a-z\d])([A-Z])/gu,"$1_$2").toUpperCase().split(/[_-]+/u).filter(t=>t.length>0),O=new Set(["CREDENTIAL","DSN","PASSPHRASE","PASSWD","PASSWORD","SECRET","TOKEN"]),R=["ACCESS_KEY","API_KEY","ENCRYPTION_KEY","PRIVATE_KEY","SIGNING_KEY"],se=new RegExp(`(?:${[...O,...R.map(e=>e.replaceAll("_",""))].join("|")})S?$`,"iu"),ae=e=>T(e).some(t=>t==="PUBLIC"||t==="PUBLISHABLE"),ie=e=>{if(ae(e))return!1;const t=T(e);if(t.some(n=>O.has($(n))))return!0;const s=$(t.join("_"));return R.some(n=>s===n||s.endsWith(`_${n}`))||se.test(e)},ce=e=>{const t=e.trim();return t===""||t.startsWith("<")&&t.endsWith(">")},oe=/replace[-_\s]?(?:with|me)(?![a-z])/iu,de=e=>ce(e)||oe.test(e),ue=e=>de(e),C=/\r?\n/u,le=async(e,t,s)=>{const{args:n,command:r}=X(q(t),{args:e,tool:"wrangler"});try{const a=await J({args:n,captureStderr:!0,captureStdoutSilently:!0,command:r,cwd:t,...s===void 0?{}:{env:{CLOUDFLARE_ENV:s}}});return{code:a.code,stderr:a.stderr??"",stdout:a.stdout??""}}catch(a){return{code:1,stderr:a instanceof Error?a.message:String(a),stdout:""}}},v=async(e,t,s)=>{const n=await e(["d1","list","--json"],t,s);if(n.code!==0)return;let r;try{r=JSON.parse(n.stdout)}catch{return}if(!Array.isArray(r))return;const a=new Map;for(const i of r)typeof i.name=="string"&&typeof i.uuid=="string"&&a.set(i.name,i.uuid);return a},fe=(e,t,s)=>(Array.isArray(e)?e:[]).flatMap((r,a)=>{if(r===null||typeof r!="object")return[];const i=r,c=typeof i.database_id=="string"?i.database_id:"";return ue(c)?[{binding:typeof i.binding=="string"&&i.binding!==""?i.binding:"<unnamed>",environment:s,path:[...t,"d1_databases",a],name:typeof i.database_name=="string"&&i.database_name!==""?i.database_name:void 0}]:[]}),me=(e,t)=>{const s=H(e);return[{config:s,environment:void 0,prefix:[]},...Object.entries(t?{}:s?.env??{}).map(([r,a])=>({config:a,environment:r,prefix:["env",r]}))].flatMap(({config:r,environment:a,prefix:i})=>fe(r?.d1_databases,i,a))},ge=async(e,t,s,n)=>{const r=await v(s,t.cwd,e.environment);if(r===void 0){n.skipped.push(`D1 "${e.binding}": could not run \`wrangler d1 list --json\`. Check that wrangler is installed in this project and logged in (\`wrangler login\`), then retry.`);return}const a=r.get(e.name);if(a!==void 0)return a;if(t.dryRun){n.applied.push(`would run \`wrangler d1 create ${e.name}\` and write its id into ${e.binding}`);return}const i=await s(["d1","create",e.name],t.cwd,e.environment);if(i.code!==0){const d=i.stderr.trim()||`exit ${String(i.code)}`;n.skipped.push(`D1 "${e.binding}": \`wrangler d1 create ${e.name}\` failed: ${d}`);return}const o=(await v(s,t.cwd,e.environment))?.get(e.name);return o===void 0&&n.skipped.push(`D1 "${e.binding}": created ${e.name} but could not read back its id.`),o},he=async(e,t)=>{if(e.configPath===void 0)return;const s=e.runner??le,n=p(e.configPath,"utf8"),r=me(n,(P(e.cwd)??N)==="celld");let a=n;for(const i of r){if(i.name===void 0){t.skipped.push(`D1 "${i.binding}" has no database_name, so there is no database to create or reuse.`);continue}const c=await ge({...i,name:i.name},e,s,t);if(c!==void 0){const o=Y(a,[...i.path,"database_id"],c,{formattingOptions:{eol:`
`,insertSpaces:!0,tabSize:4}});a=z(a,o),t.applied.push(`${i.binding}: database_id set to the id of D1 "${i.name}"`)}}!e.dryRun&&a!==n&&x(e.configPath,a)},pe=e=>{const t=I("git",["check-ignore","-q",u],{cwd:e,stdio:"ignore"});if(t.status===0)return!0;if(t.status===1)return!1;const s=w(e,".gitignore");return E(s)&&p(s,"utf8").split(C).some(n=>n.trim()===u)},be=async(e,t)=>{try{return M(W(await V({projectRoot:e,schemaDir:B(e)})))}catch{t.skipped.push(`${u}: could not read the project's bindings, so the relay key was not checked against its role.`);return}},ye=(e,t,s,n,r)=>{const a=[],i=new Set(t.split(C).flatMap(c=>{const[o]=k(c);return o!==void 0&&!y(o.value)?[o.key]:[]}));G({cwd:e}).source===void 0&&!i.has("LUNORA_ADMIN_TOKEN")&&a.push("LUNORA_ADMIN_TOKEN");for(const c of s)!ie(c.key)||!y(c.value)||i.has(c.key)||(K(c.key,c.value,r)?a.push(c.key):n.skipped.push(`${u}: ${c.key} is a provider key and cannot be minted here; fill it in by hand.`));return a},Se=async(e,t)=>{const s=w(e.cwd,u),n=E(s)?p(s,"utf8"):"",r=ye(e.cwd,n,k(n),t,await be(e.cwd,t));if(r.length===0)return;if(!pe(e.cwd)){t.skipped.push(`${u} is not in .gitignore, so no secret is written there. Add \`${u}\` to .gitignore first.`);return}if(e.dryRun){t.applied.push(`would mint ${r.join(", ")} into ${u}`);return}let a=n;for(const i of r)a=L(a,i,F());U(s,a),t.applied.push(`minted ${r.join(", ")} into ${u}`)},xe=async e=>{const t={applied:[],skipped:[]};return await he(e,t),await Se(e,t),t},Pe=e=>{switch(e){case"boolean":return"boolean";case"date":case"number":case"timestamp":return"number";case"id":case"literal":case"string":return"string";default:return}},$e=/already (?:exists|indexed)|duplicate/iu,A=e=>["vectorize","create-metadata-index",e.index,`--property-name=${e.property}`,`--type=${e.type}`],ve=async(e,t)=>{try{return await e(t)}catch(s){return{code:1,stderr:s instanceof Error?s.message:String(s)}}},Ne=async e=>{const{cwd:t,entries:s,exec:n,logger:r,spawner:a}=e,i=[];for(const c of s){const o=await ve(a,{args:[...n.args,...A(c)],captureStderr:!0,command:n.command,cwd:t,stdoutToStderr:!0});if(o.code===0){i.push({entry:c,status:"created"});continue}const d=`${o.stderr??""}${o.stdout??""}`;if($e.test(d)){i.push({entry:c,status:"exists"});continue}i.push({entry:c,error:d.trim()||`exit code ${String(o.code)}`,status:"failed"}),r.warn(`could not create the Vectorize metadata index for "${c.property}" on "${c.index}" — filters on that property will match nothing until it exists. Run: wrangler ${A(c).join(" ")}`)}return i};export{xe as a,Ie as b,A as c,Ce as d,de as e,ue as f,Ne as g,ie as i,Pe as m};