UNPKG

@lunora/cli

Version:

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

2 lines (1 loc) 15.9 kB
import{existsSync as m,readFileSync as D}from"node:fs";import{runCodegen as U,discoverMigrations as C}from"@lunora/codegen";import{readLinkedProject as k,writeLinkedProject as L,validateWranglerProject as I,inferLunoraBindings as x,reconcileWranglerBindings as _,reconcileWranglerCompatibilityDate as E,DEV_VARS_FILE as b,parseDevVariableEntries as N,isMintableSecretKey as R,reconcileWranglerCrons as O,findWranglerFile as $,readWranglerJsonc as S,discoverContainerInfo as j,packageNamesFromBindings as P,requiredSecrets as W,generateSecretValue as B,discoverSchemaInfo as K}from"@lunora/config";import{join as p}from"@visulima/path";import{Spinner as M}from"@visulima/spinner";import{Project as q}from"ts-morph";import{r as Y}from"../packem_shared/api-spec-BENwiyUa.mjs";import{d as T}from"../packem_shared/wrangler-name-HIl0Z6yx.mjs";import{i as F}from"../packem_shared/command-Bjv4VmYA.mjs";import{M as h,P as w}from"../packem_shared/detect-package-manager-B2eIuAiP.mjs";import{o as V,r as G}from"../packem_shared/docker-CKvYE9YF.mjs";import{i as z,r as f,d as H,s as J}from"../packem_shared/output-format-D7cKB-O5.mjs";import{containerBuildTag as Q}from"@lunora/container";import{defaultSpawner as v}from"../packem_shared/createRecordingSpawner-ByIqBepG.mjs";import{t as X}from"../packem_shared/resolve-target-vxaXHmvg.mjs";import{k as Z}from"../packem_shared/schema-drift-gate-CpqG-xP4.mjs";import{$ as ee}from"../packem_shared/tui-prompts-CCt3jm1k.mjs";import{m as re,p as te}from"../packem_shared/vectorize-metadata-BvZbbqUD.mjs";import{f as oe}from"../packem_shared/wrangler-secrets-BMRJwKQU.mjs";import{runMigrateDataCommand as ne}from"./runMigrateGenerateCommand.mjs";const ie=/https?:\/\/[^\s"'<>]+\.workers\.dev[^\s"'<>]*/u,ae=/https:\/\/[^\s"'<>]+/u,se=e=>{const r=ie.exec(e);return r?r[0]:ae.exec(e)?.[0]},ce=({cwd:e,env:r,logger:o,now:t,output:n})=>{if(n===void 0||k(e)!==void 0)return;const i=se(n);if(i!==void 0)try{const a=(t??(()=>new Date().toISOString()))();L(e,{env:r,linkedAt:a,workerName:T(e),workerUrl:i}),o.success(`link: recorded ${i} in .lunora/project.json (run \`lunora link\` to change)`)}catch{}},le=e=>{const{cwd:r,env:o,logger:t,migrated:n}=e;try{const i=k(r),a=i?.workerName??T(r);t.success("deploy complete"),t.info(` worker: ${a??"(see wrangler output above)"}`),o!==void 0&&t.info(` env: ${o}`),i?.workerUrl===void 0?t.info(" url: run `lunora link --url <https://your-worker>` to record it"):t.info(` url: ${i.workerUrl}`),n&&t.info(" migrations: applied"),t.info(" studio: lunora view --remote"),t.info(" logs: lunora logs")}catch{}},de=async e=>{if(e.targets.length===0)return{builtTags:[],code:0};if(!(e.railpackAvailable??V)()){const t="deploy blocked: a container uses `image: { build }` (Railpack), but Railpack isn't ready. Install the `railpack` CLI and start a BuildKit instance, e.g. `docker run --rm --privileged -d --name buildkit moby/buildkit` then `export BUILDKIT_HOST=docker-container://buildkit`. Alternatively switch the container's `image` to a Dockerfile path or a pre-built registry reference.";return e.logger.error(t),{builtTags:[],code:1,error:t}}const r=e.spawner??v,o=[];for(const t of e.targets){const n=Q(t.exportName),i={args:["build",t.buildDir,"--name",n],command:"railpack",cwd:e.cwd},a=h(w(e.cwd),"wrangler",["containers","push",n]),s={args:a.args,command:a.command,cwd:e.cwd};e.logger.info(`railpack: building "${t.exportName}" → ${n} from ${t.buildDir}`);const c=await r(i);if(c.code!==0){const d=`railpack build failed for container "${t.exportName}" (${t.buildDir})`;return e.logger.error(d),{builtTags:o,code:c.code,error:d}}e.logger.info(`railpack: pushing ${n} to the Cloudflare Registry`);const l=await r(s);if(l.code!==0){const d=`wrangler containers push failed for "${t.exportName}" (${n})`;return e.logger.error(d),{builtTags:o,code:l.code,error:d}}o.push(n)}return{builtTags:o,code:0}},A="<replace-with-d1-create-id>",ge=["LUNORA_ORIGIN_URL","LUNORA_WORKER_ORIGIN","AUTH_URL"],ue=e=>{try{const{hostname:r}=new URL(e);return r==="localhost"||r==="127.0.0.1"||r==="::1"||r==="[::1]"}catch{return!1}},pe=e=>e.startsWith("./")||e.startsWith("../")||e.startsWith("/")||e.includes("Dockerfile"),me=(e,r,o)=>{const t=$(e);if(!t)return;const{parsed:n}=S(t),i=(n?.containers??[]).filter(s=>typeof s?.image=="string"&&pe(s.image));if(i.length===0||o())return;const a=`deploy blocked: wrangler.jsonc declares ${String(i.length)} container(s) built from a local Dockerfile, but no Docker-compatible engine is available. Start Docker (or Colima), or point the container's \`image\` at a pre-built registry reference. Note: container images must target linux/amd64.`;return r.error(a),a},fe=e=>m(p(e,"src","worker.ts"))?"src/worker.ts":void 0,he=(e,r)=>{for(const o of j(e,"lunora").containers){const{image:t}=o;if(t.kind==="dockerfile"&&!m(p(e,t.dockerfilePath))){const n=`deploy blocked: container "${o.exportName}" references a Dockerfile at "${t.dockerfilePath}" that does not exist. Create it or fix the \`image\` path in lunora/containers.ts.`;return r.error(n),n}if(t.kind==="build"&&!m(p(e,t.buildDir))){const n=`deploy blocked: container "${o.exportName}" references a Railpack build directory "${t.buildDir}" that does not exist. Create it or fix the \`image.build\` path in lunora/containers.ts.`;return r.error(n),n}}},we=e=>f(e.format)?!1:e.interactive!==void 0?e.interactive:process.stdout.isTTY&&!process.env.CI,ve=e=>{const r=$(e);if(!r)return;const{parsed:o}=S(r);return o?(o.d1_databases??[]).find(t=>t.database_id===A)?.binding:void 0},ye=async(e,r)=>{const o=j(e,"lunora").containers.filter(n=>n.image.kind==="build").map(n=>({buildDir:n.image.buildDir,exportName:n.exportName}));if(o.length===0)return;const t=await de({cwd:e,logger:r.logger,railpackAvailable:r.railpackAvailable,spawner:r.spawner,targets:o});return t.code===0?void 0:t.error??"railpack build failed"},be=(e,r,o)=>{if(o!==void 0)try{const t=O(e,o);t.changed&&r.success(`synced ${String(o.length)} cron trigger(s) → ${t.wranglerPath??"wrangler.jsonc"}`)}catch(t){const n=t instanceof Error?t.message:String(t);r.warn(`cron trigger sync skipped: ${n}`)}},ke=async(e,r,o)=>{try{const t=await x({projectRoot:e}),n=_(e,t);n.changed&&r.success(`provisioned bindings: ${n.added.join(", ")}${n.wranglerPath??"wrangler.jsonc"}`);for(const i of n.warnings)r.warn(i)}catch(t){const n=t instanceof Error?t.message:String(t);r.warn(`binding inference skipped: ${n}`)}try{const t=E(e);t.changed&&r.success(`bumped compatibility_date to ${t.date??"unknown"} (Workers Cache enabled) → ${t.wranglerPath??"wrangler.jsonc"}`)}catch(t){const n=t instanceof Error?t.message:String(t);r.warn(`compatibility date sync skipped: ${n}`)}be(e,r,o)},$e=(e,r)=>{const o=p(e,b);if(!m(o))return;let t;try{t=N(D(o,"utf8")).length}catch{return}t!==0&&r.warn(`Note: \`lunora deploy\` does not push secrets. ${b} has ${String(t)} key(s); if you changed them, run \`lunora env push --yes\` to update the deployed secrets.`)},Se=/(?:KEY|PASSWORD|SECRET|TOKEN)$/u,Re=async e=>{let r=[];try{r=P(await x({projectRoot:e}))}catch{}const o=W(r).map(n=>n.key);let t=[];try{const n=p(e,b);m(n)&&(t=N(D(n,"utf8")).map(i=>i.key).filter(i=>Se.test(i)))}catch{}return[...new Set([...o,...t])]},De=async(e,r,o)=>{const{logger:t}=r,n=r.spawner??v,i=w(e),a=r.env===void 0?"":` --env ${r.env}`;for(const s of o){const c=["secret","put",s];r.env!==void 0&&c.push("--env",r.env),r.temporary===!0&&c.push("--temporary");const l=h(i,"wrangler",c),d=await n({args:l.args,command:l.command,cwd:e,input:B()});if(d.code!==0)return t.error(`failed to push secret ${s} (exit ${String(d.code)}); set it manually with \`wrangler secret put ${s}${a}\`.`),!1;t.success(`generated + pushed ${s}`)}return!0},xe=async(e,r,o)=>{if(r.dryRun===!0||r.preview===!0)return;const{logger:t}=r,n=r.env===void 0?"":` --env ${r.env}`;let i;try{i=await(r.secretLister??oe)({cwd:e,env:r.env,temporary:r.temporary})}catch{return}if(!i.ok)return;const a=new Set(i.names),s=(await Re(e)).filter(l=>!a.has(l));if(s.length===0)return;if(!o)return`missing required secret(s) on the deploy target: ${s.join(", ")}. Set them with \`wrangler secret put <KEY>${n}\` (or \`lunora env generate --set\` then \`lunora env push --yes${r.env===void 0?"":" --prod"}\`), then re-deploy.`;for(const l of s.filter(d=>!R(d)))t.warn(`required secret ${l} is not set on the target — set it with: wrangler secret put ${l}${n}`);const c=s.filter(l=>R(l));if(c.length!==0){if(await(r.secretConfirm??ee())(`${String(c.length)} required secret(s) not set on the target (${c.join(", ")}). Generate strong values and push them now?`)){await De(e,r,c);return}t.warn(`${String(c.length)} required secret(s) not set on the target: ${c.join(", ")}. Generate + push with \`lunora env generate --set\` then \`lunora env push --yes${r.env===void 0?"":" --prod"}\`.`)}},Ne=async(e,r)=>{const o=new q({skipAddingFilesFromTsConfig:!0}),t=p(r,"lunora");let n;try{n=C(o,t)}catch(i){const a=i instanceof Error?i.message:String(i);return e.logger.warn(`--migrate: could not discover migrations (${a}); skipping`),0}if(n.length===0)return e.logger.info("--migrate: no data migrations declared in lunora/"),0;e.logger.info(`--migrate: running ${String(n.length)} migration(s) against deployed worker`);for(const i of n){e.logger.info(`--migrate: up "${i.id}" (table "${i.table}")`);const a={cwd:r,fetchImpl:e.fetchImpl,id:i.id,logger:e.logger,prod:!0,subcommand:"up",token:e.migrateToken,url:e.migrateUrl,yes:e.migrateYes===!0},s=await ne(a);if(s.code!==0)return e.logger.error(`--migrate: migration "${i.id}" failed — see output above`),s.code;e.logger.success(`--migrate: "${i.id}" applied`)}return 0},je=e=>{if(!(!e.migrate||e.dryRun||e.preview)){if(e.migrateUrl===void 0){const r="--migrate requires --migrate-url <https://your-worker> — the deploy target URL is not captured automatically, refusing to default to localhost";return e.logger.error(r),r}if(e.migrateYes!==!0){const r="--migrate runs production data migrations after deploy. Re-run with --migrate-yes to confirm.";return e.logger.error(r),r}if((e.migrateToken??process.env.LUNORA_ADMIN_TOKEN)===void 0||(e.migrateToken??process.env.LUNORA_ADMIN_TOKEN)===""){const r="admin token required for --migrate — pass --migrate-token or set LUNORA_ADMIN_TOKEN";return e.logger.error(r),r}}},Te=(e,r,o,t)=>{let n;r?(n=new M({name:"dots"}),n.start("running codegen")):o.info("running codegen");try{const i=U({apiSpec:t,projectRoot:e});return n?.succeed("codegen complete"),n||o.success("codegen complete"),{result:i}}catch(i){n?.failed("codegen failed");const a=i instanceof Error?i.message:String(i);return o.error(`codegen failed: ${a}`),{error:`codegen failed: ${a}`}}},Ae=(e,r)=>{const o=ve(e);if(o===void 0)return;const t=`deploy blocked: the "${o}" D1 binding has a placeholder database_id ("${A}"). Run \`wrangler d1 create <name>\` to create the database, then replace the placeholder in wrangler.jsonc with the real id before deploying.`;return r.error(t),t},Ue=(e,r)=>{const o=$(e);if(!o)return;const{parsed:t}=S(o),n=t?.vars;if(!n)return;const i=ge.filter(s=>typeof n[s]=="string"&&ue(n[s]));if(i.length===0)return;const a=`deploy blocked: ${i.join(", ")} in wrangler.jsonc point at localhost. A deployed Worker can't reach a loopback address, so this silently breaks scheduled-job dispatch / auth callbacks. Set each to the deployed worker's public URL (or move it to a secret with \`wrangler secret put\`) before deploying.`;return r.error(a),a},Ce=async(e,r)=>{const{info:o}=K(r,"lunora"),t=o?.vectorMetadata??[];if(t.length===0)return;const n=[];for(const a of t){const s=te(a.kind);if(s===void 0){e.logger.warn(`vector index "${a.index}" declares metadata "${a.property}", whose column type cannot be filtered on in Vectorize — it is stored with each vector but no filter will match it.`);continue}n.push({index:a.index,property:a.property,type:s})}if(n.length===0)return;const i=(await re({cwd:r,entries:n,exec:h(w(r),"wrangler",[]),logger:e.logger,spawner:e.spawner??v})).filter(a=>a.status!=="failed").length;i>0&&e.logger.success(`vectorize metadata indexes ready: ${String(i)}/${String(n.length)}`)},Le=async(e,r,o,t,n)=>{if(await Ce(e,r),e.migrate){const i=await Ne(e,r);return i===0&&n?.(),{code:i,descriptor:o,validation:t}}return n?.(),{code:0,descriptor:o,validation:t}},Ie=async(e,r)=>{const o=Ae(e,r.logger);if(o!==void 0)return o;const t=Ue(e,r.logger);if(t!==void 0)return t;const n=he(e,r.logger);if(n!==void 0)return n;const i=me(e,r.logger,r.dockerAvailable??G);return i!==void 0?i:ye(e,r)},_e=(e,r)=>{const o=r.preview?["versions","upload"]:["deploy"],t=fe(e);return t!==void 0&&(o.push(t),r.logger.info(`class-B composition: deploying ${t} (overrides wrangler main)`)),r.env!==void 0&&o.push("--env",r.env),r.temporary&&(o.push("--temporary"),r.logger.info("temporary account: deploying to a short-lived Cloudflare account (~60min); wrangler will print a claim URL")),r.dryRun&&(o.push("--dry-run"),r.logger.info("dry run: validating + bundling without publishing")),r.outDir!==void 0&&(o.push("--outdir",r.outDir,"--metafile"),r.logger.info(`build artifact: emitting bundle to ${r.outDir}`)),o},Ee=(e,r)=>{if(e.problems.length===0)return!1;r.error("wrangler.jsonc validation failed:");for(const o of e.problems)r.error(` - ${o}`);return!0},Oe=async e=>{const r=e.cwd??process.cwd(),o=we(e);let t;if(!e.skipCodegen){const u=Te(r,o,e.logger,e.apiSpec);if(u.error!==void 0)return{code:1,descriptor:void 0,error:u.error,validation:{problems:[],wranglerPath:void 0}};t=u.result}let n;if(t!==void 0){const u=Z({allowDrift:e.allowSchemaDrift===!0,codegen:t,logger:e.logger,updateBaseline:e.updateSchemaBaseline===!0});if(u.blocked)return{code:1,descriptor:void 0,error:"schema drift gate blocked deploy",schemaDrift:{blocked:!0,reason:u.reason},validation:{problems:[],wranglerPath:void 0}};n=u.rebless}await ke(r,e.logger,t?.cronTriggers);const i=je(e);if(i!==void 0)return{code:1,descriptor:void 0,error:i,validation:{problems:[],wranglerPath:void 0}};const a=await Ie(r,e);if(a!==void 0)return{code:1,descriptor:void 0,error:a,validation:{problems:[],wranglerPath:void 0}};const s=I({projectRoot:r});if(Ee(s,e.logger))return{code:1,descriptor:void 0,error:"wrangler validation failed",validation:s};$e(r,e.logger);const c=await xe(r,e,o);if(c!==void 0)return e.logger.error(c),{code:1,descriptor:void 0,error:c,validation:s};const l=!f(e.format)&&e.dryRun!==!0&&e.preview!==!0&&k(r)===void 0,d=h(w(r),"wrangler",_e(r,e)),g={args:d.args,captureStdout:l,command:d.command,cwd:r,stdoutToStderr:f(e.format)};e.logger.info(`deploying via ${g.command} ${g.args.join(" ")}`);const y=await(e.spawner??v)(g);return y.code!==0?{code:y.code,descriptor:g,validation:s}:e.dryRun?{code:0,descriptor:g,validation:s}:e.preview?{code:0,descriptor:g,validation:s}:(ce({cwd:r,env:e.env,logger:e.logger,output:y.stdout}),Le(e,r,g,s,n))},Pe=async e=>{const r=z("deploy",e.format);if(r!==void 0)return e.logger.error(r),{code:1,descriptor:void 0,error:r,validation:{problems:[],wranglerPath:void 0}};const o=await Oe({...e,logger:J(e.format,e.logger)});return f(e.format)?(H(o),o):(o.code===0&&e.dryRun!==!0&&e.preview!==!0?le({cwd:e.cwd??process.cwd(),env:e.env,logger:e.logger,migrated:e.migrate===!0}):o.code===0&&e.preview===!0&&e.logger.success("preview version uploaded — see the preview URL in the wrangler output above"),o)},ir=F(async({cwd:e,logger:r,options:o})=>({code:(await Pe({allowSchemaDrift:o.allowSchemaDrift===!0,apiSpec:Y(o.apiSpec),cwd:e,dryRun:o.dryRun===!0,env:o.env,format:o.format,logger:r,migrate:o.migrate===!0,migrateToken:o.migrateToken,migrateUrl:X({cwd:e,url:o.migrateUrl}),migrateYes:o.migrateYes===!0,preview:o.preview===!0,skipCodegen:o.prebuilt===!0,temporary:o.temporary===!0,updateSchemaBaseline:o.updateSchemaBaseline===!0})).code}));export{ir as execute,Pe as runDeployCommand};