UNPKG

@lunora/cli

Version:

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

2 lines (1 loc) 15.1 kB
import{existsSync as m,readFileSync as D}from"node:fs";import{runCodegen as A,discoverMigrations as C}from"@lunora/codegen";import{readLinkedProject as v,writeLinkedProject as L,validateWranglerProject as _,inferLunoraBindings as N,reconcileWranglerBindings as E,reconcileWranglerCompatibilityDate as I,reconcileWranglerCrons as P,DEV_VARS_FILE as w,parseDevVariableEntries as j,isMintableSecretKey as R,findWranglerFile as y,readWranglerJsonc as b,discoverContainerInfo as T,packageNamesFromBindings as O,requiredSecrets as W,generateSecretValue as B}from"@lunora/config";import{join as p}from"@visulima/path";import{Spinner as K}from"@visulima/spinner";import{Project as q}from"ts-morph";import{r as M}from"../packem_shared/api-spec-BENwiyUa.mjs";import{d as U}from"../packem_shared/wrangler-name-HIl0Z6yx.mjs";import{i as Y}from"../packem_shared/command-Bjv4VmYA.mjs";import{M as k,P as $}from"../packem_shared/detect-package-manager-B2eIuAiP.mjs";import{o as F,r as G}from"../packem_shared/docker-CKvYE9YF.mjs";import{i as V,r as f,d as H,s as J}from"../packem_shared/output-format-D7cKB-O5.mjs";import{containerBuildTag as z}from"@lunora/container";import{defaultSpawner as S}from"../packem_shared/createRecordingSpawner-C8_7nffs.mjs";import{t as Q}from"../packem_shared/resolve-target-vxaXHmvg.mjs";import{k as X}from"../packem_shared/schema-drift-gate-CpqG-xP4.mjs";import{$ as Z}from"../packem_shared/tui-prompts-CCt3jm1k.mjs";import{f as ee}from"../packem_shared/wrangler-secrets-BMRJwKQU.mjs";import{runMigrateDataCommand as re}from"./runMigrateGenerateCommand.mjs";const te=/https?:\/\/[^\s"'<>]+\.workers\.dev[^\s"'<>]*/u,oe=/https:\/\/[^\s"'<>]+/u,ne=e=>{const r=te.exec(e);return r?r[0]:oe.exec(e)?.[0]},ie=({cwd:e,env:r,logger:o,now:t,output:n})=>{if(n===void 0||v(e)!==void 0)return;const i=ne(n);if(i!==void 0)try{const s=(t??(()=>new Date().toISOString()))();L(e,{env:r,linkedAt:s,workerName:U(e),workerUrl:i}),o.success(`link: recorded ${i} in .lunora/project.json (run \`lunora link\` to change)`)}catch{}},ae=e=>{const{cwd:r,env:o,logger:t,migrated:n}=e;try{const i=v(r),s=i?.workerName??U(r);t.success("deploy complete"),t.info(` worker: ${s??"(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{}},se=async e=>{if(e.targets.length===0)return{builtTags:[],code:0};if(!(e.railpackAvailable??F)()){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??S,o=[];for(const t of e.targets){const n=z(t.exportName),i={args:["build",t.buildDir,"--name",n],command:"railpack",cwd:e.cwd},s=k($(e.cwd),"wrangler",["containers","push",n]),a={args:s.args,command:s.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(a);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}},x="<replace-with-d1-create-id>",ce=["LUNORA_ORIGIN_URL","LUNORA_WORKER_ORIGIN","AUTH_URL"],le=e=>{try{const{hostname:r}=new URL(e);return r==="localhost"||r==="127.0.0.1"||r==="::1"||r==="[::1]"}catch{return!1}},de=e=>e.startsWith("./")||e.startsWith("../")||e.startsWith("/")||e.includes("Dockerfile"),ge=(e,r,o)=>{const t=y(e);if(!t)return;const{parsed:n}=b(t),i=(n?.containers??[]).filter(a=>typeof a?.image=="string"&&de(a.image));if(i.length===0||o())return;const s=`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(s),s},ue=e=>m(p(e,"src","worker.ts"))?"src/worker.ts":void 0,pe=(e,r)=>{for(const o of T(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}}},me=e=>f(e.format)?!1:e.interactive!==void 0?e.interactive:process.stdout.isTTY&&!process.env.CI,fe=e=>{const r=y(e);if(!r)return;const{parsed:o}=b(r);return o?(o.d1_databases??[]).find(t=>t.database_id===x)?.binding:void 0},he=async(e,r)=>{const o=T(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 se({cwd:e,logger:r.logger,railpackAvailable:r.railpackAvailable,spawner:r.spawner,targets:o});return t.code===0?void 0:t.error??"railpack build failed"},we=async(e,r,o=[])=>{try{const t=await N({projectRoot:e}),n=E(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=I(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}`)}try{const t=P(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}`)}},ve=(e,r)=>{const o=p(e,w);if(!m(o))return;let t;try{t=j(D(o,"utf8")).length}catch{return}t!==0&&r.warn(`Note: \`lunora deploy\` does not push secrets. ${w} has ${String(t)} key(s); if you changed them, run \`lunora env push --yes\` to update the deployed secrets.`)},ye=/(?:KEY|PASSWORD|SECRET|TOKEN)$/u,be=async e=>{let r=[];try{r=O(await N({projectRoot:e}))}catch{}const o=W(r).map(n=>n.key);let t=[];try{const n=p(e,w);m(n)&&(t=j(D(n,"utf8")).map(i=>i.key).filter(i=>ye.test(i)))}catch{}return[...new Set([...o,...t])]},ke=async(e,r,o)=>{const{logger:t}=r,n=r.spawner??S,i=$(e),s=r.env===void 0?"":` --env ${r.env}`;for(const a of o){const c=["secret","put",a];r.env!==void 0&&c.push("--env",r.env),r.temporary===!0&&c.push("--temporary");const l=k(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 ${a} (exit ${String(d.code)}); set it manually with \`wrangler secret put ${a}${s}\`.`),!1;t.success(`generated + pushed ${a}`)}return!0},$e=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??ee)({cwd:e,env:r.env,temporary:r.temporary})}catch{return}if(!i.ok)return;const s=new Set(i.names),a=(await be(e)).filter(l=>!s.has(l));if(a.length===0)return;if(!o)return`missing required secret(s) on the deploy target: ${a.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 a.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=a.filter(l=>R(l));if(c.length!==0){if(await(r.secretConfirm??Z())(`${String(c.length)} required secret(s) not set on the target (${c.join(", ")}). Generate strong values and push them now?`)){await ke(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"}\`.`)}},Se=async(e,r)=>{const o=new q({skipAddingFilesFromTsConfig:!0}),t=p(r,"lunora");let n;try{n=C(o,t)}catch(i){const s=i instanceof Error?i.message:String(i);return e.logger.warn(`--migrate: could not discover migrations (${s}); 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 s={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},a=await re(s);if(a.code!==0)return e.logger.error(`--migrate: migration "${i.id}" failed — see output above`),a.code;e.logger.success(`--migrate: "${i.id}" applied`)}return 0},Re=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}}},De=(e,r,o,t)=>{let n;r?(n=new K({name:"dots"}),n.start("running codegen")):o.info("running codegen");try{const i=A({apiSpec:t,projectRoot:e});return n?.succeed("codegen complete"),n||o.success("codegen complete"),{result:i}}catch(i){n?.failed("codegen failed");const s=i instanceof Error?i.message:String(i);return o.error(`codegen failed: ${s}`),{error:`codegen failed: ${s}`}}},Ne=(e,r)=>{const o=fe(e);if(o===void 0)return;const t=`deploy blocked: the "${o}" D1 binding has a placeholder database_id ("${x}"). 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},je=(e,r)=>{const o=y(e);if(!o)return;const{parsed:t}=b(o),n=t?.vars;if(!n)return;const i=ce.filter(a=>typeof n[a]=="string"&&le(n[a]));if(i.length===0)return;const s=`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(s),s},Te=async(e,r,o,t,n)=>{if(e.migrate){const i=await Se(e,r);return i===0&&n?.(),{code:i,descriptor:o,validation:t}}return n?.(),{code:0,descriptor:o,validation:t}},Ue=async(e,r)=>{const o=Ne(e,r.logger);if(o!==void 0)return o;const t=je(e,r.logger);if(t!==void 0)return t;const n=pe(e,r.logger);if(n!==void 0)return n;const i=ge(e,r.logger,r.dockerAvailable??G);return i!==void 0?i:he(e,r)},xe=(e,r)=>{const o=r.preview?["versions","upload"]:["deploy"],t=ue(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},Ae=(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},Ce=async e=>{const r=e.cwd??process.cwd(),o=me(e);let t;if(!e.skipCodegen){const u=De(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=X({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 we(r,e.logger,t?.cronTriggers??[]);const i=Re(e);if(i!==void 0)return{code:1,descriptor:void 0,error:i,validation:{problems:[],wranglerPath:void 0}};const s=await Ue(r,e);if(s!==void 0)return{code:1,descriptor:void 0,error:s,validation:{problems:[],wranglerPath:void 0}};const a=_({projectRoot:r});if(Ae(a,e.logger))return{code:1,descriptor:void 0,error:"wrangler validation failed",validation:a};ve(r,e.logger);const c=await $e(r,e,o);if(c!==void 0)return e.logger.error(c),{code:1,descriptor:void 0,error:c,validation:a};const l=!f(e.format)&&e.dryRun!==!0&&e.preview!==!0&&v(r)===void 0,d=k($(r),"wrangler",xe(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 h=await(e.spawner??S)(g);return h.code!==0?{code:h.code,descriptor:g,validation:a}:e.dryRun?{code:0,descriptor:g,validation:a}:e.preview?{code:0,descriptor:g,validation:a}:(ie({cwd:r,env:e.env,logger:e.logger,output:h.stdout}),Te(e,r,g,a,n))},Le=async e=>{const r=V("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 Ce({...e,logger:J(e.format,e.logger)});return f(e.format)?(H(o),o):(o.code===0&&e.dryRun!==!0&&e.preview!==!0?ae({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)},Ze=Y(async({cwd:e,logger:r,options:o})=>({code:(await Le({allowSchemaDrift:o.allowSchemaDrift===!0,apiSpec:M(o.apiSpec),cwd:e,dryRun:o.dryRun===!0,env:o.env,format:o.format,logger:r,migrate:o.migrate===!0,migrateToken:o.migrateToken,migrateUrl:Q({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{Ze as execute,Le as runDeployCommand};