@lunora/cli
Version:
The Lunora CLI: init, dev, deploy, codegen, migrate, seed, doctor, insights, logs, registry, and the rest of the project commands
12 lines (10 loc) • 10.8 kB
JavaScript
import{existsSync as f,mkdirSync as _,writeFileSync as b,mkdtempSync as N,rmSync as M,readFileSync as k}from"node:fs";import{tmpdir as x}from"node:os";import{discoverSchema as U,readPackageDependencies as C,discoverMigrations as F}from"@lunora/codegen";import{isInteractive as O,promptSelect as P,promptText as D}from"@lunora/config";import{LunoraError as $}from"@lunora/errors";import{join as c}from"@visulima/path";import{Project as v}from"ts-morph";import{r as z,t as j}from"../packem_shared/admin-token-VdUnvnKW.mjs";import{n as q,r as L}from"../packem_shared/admin-url-BhF5ufg1.mjs";import{d as B}from"../packem_shared/command-DwC11Cwe.mjs";import{diffSnapshots as H,renderMigrationFile as G}from"../packem_shared/diffSnapshots-DtC5Cs4Z.mjs";import{r as W}from"../packem_shared/resolve-target-BRvw8Pf7.mjs";import J from"../packem_shared/schemaIrToSnapshot-e5fO6GRg.mjs";import{readAndLogBody as V}from"./runRpcCommand.mjs";import{runExportCommand as Z}from"../packem_shared/runExportCommand-Dq-SpjAe.mjs";import{r as K}from"../packem_shared/import-Dqd1MOOv.mjs";const m="__lunora_reproject__",X=e=>{if(!e.startsWith(m))return;const r=e.slice(m.length);return r===""?void 0:r},Q=".snapshot.json",A=/[^\da-z]+/gu,E=(e,r)=>{let a=0,t=e.length;for(;a<t&&e[a]===r;)a+=1;for(;t>a&&e[t-1]===r;)t-=1;return e.slice(a,t)},Y=e=>{const r=E(e.toLowerCase().replaceAll(A,"_"),"_");return r===""?"auto":r},ee=e=>{const r=(a,t=2)=>a.toString().padStart(t,"0");return`${String(e.getUTCFullYear())}${r(e.getUTCMonth()+1)}${r(e.getUTCDate())}${r(e.getUTCHours())}${r(e.getUTCMinutes())}${r(e.getUTCSeconds())}`},re=e=>{if(f(e))try{const r=k(e,"utf8"),a=JSON.parse(r);if(a.version!==1)throw new $("INTERNAL",`unsupported snapshot version: ${a.version}`);return a}catch(r){const a=r instanceof Error?r.message:String(r);throw new $("INTERNAL",`failed to read ${e}: ${a}`,{cause:r})}},te=e=>{const r=e.cwd??process.cwd(),a=c(r,"lunora","schema.ts");if(!f(a))return e.logger.error(`schema not found: ${a} — run \`vis generate lunora-table --name=<name>\` to create one`),{code:1,empty:!0,migrationFile:""};const t=new v({skipAddingFilesFromTsConfig:!0}),n=U(t,a),s=J(n),o=c(r,"lunora","migrations"),i=c(o,Q);let l;try{l=re(i)}catch(h){return e.logger.error(h instanceof Error?h.message:String(h)),{code:1,empty:!0,migrationFile:""}}const d=H(l,s);if(d.empty)return e.logger.info("no schema changes detected — snapshot is already up to date"),{code:0,empty:!0,migrationFile:""};const p=(e.now??(()=>new Date))(),w=Y(e.name??"auto"),y=`${ee(p)}_${w}.sql`,g=c(o,y);_(o,{recursive:!0});const R=G(w,d,p.toISOString());return b(g,R,"utf8"),b(i,`${JSON.stringify(s,void 0,4)}
`,"utf8"),e.logger.success(`wrote ${g}`),d.unsupported.length>0&&e.logger.warn(`${String(d.unsupported.length)} unsupported diff(s) — see the comment block in ${y} and write the SQL manually`),{code:0,empty:!1,migrationFile:g}},ae="migrations.ts",S=/^[A-Za-z_]\w*$/u,ne=new Set(["await","break","case","catch","class","const","continue","debugger","default","delete","do","else","enum","export","extends","false","finally","for","function","if","implements","import","in","instanceof","interface","let","new","null","package","private","protected","public","return","static","super","switch","this","throw","true","try","typeof","var","void","while","with","yield"]),T=/^import\s*\{\s*defineMigration\s*\}\s*from\s*["'](?:@lunora\/server|lunorash\/server)["'][^\n]*$/mu,oe=e=>`import { defineMigration } from "${C(e)?.has("lunorash")??!1?"lunorash/server":"@lunora/server"}";`,ie="__lunora_admin__:runMigration",se="__lunora_admin__:migrationStatus",ce="/_lunora/migrate",le=e=>E(e.trim().toLowerCase().replaceAll(A,"-"),"-"),de=e=>e.split("-").filter(r=>r.length>0).map((r,a)=>a===0?r:r.charAt(0).toUpperCase()+r.slice(1)).join(""),ue=e=>{const r=c(e,"lunora","schema.ts");if(!f(r))return[];try{const a=new v({skipAddingFilesFromTsConfig:!0});return U(a,r).tables.map(t=>t.name)}catch{return[]}},me=async e=>e.length>0?P("Which table does this migration iterate?",e.map(r=>({label:r,value:r}))):D("Target table for the migration: "),fe=async(e,r)=>{if(r.table!==void 0)return r.table;const a=r.promptTable??(O()?me:void 0);if(a===void 0){r.logger.error("migrate create requires a target table when not running interactively — re-run with --table <table>");return}const n=(await a(ue(e)))?.trim();if(n===void 0||n===""){r.logger.error("no table selected — re-run with --table <table>");return}return n},ge=async e=>{const r=e.cwd??process.cwd(),a=le(e.name);if(a==="")return e.logger.error(`invalid migration name: "${e.name}" — must contain at least one alphanumeric character`),{code:1,file:""};const t=de(a);if(!S.test(t)||ne.has(t))return e.logger.error(`invalid migration name: "${e.name}" derives the export \`${t}\`, which is not a valid identifier — pick a name that starts with a letter and isn't a reserved word`),{code:1,file:""};const n=await fe(r,e);if(n===void 0)return{code:1,file:""};if(!S.test(n))return e.logger.error(`invalid table: "${n}" — must be a valid identifier ([A-Za-z_][A-Za-z0-9_]*)`),{code:1,file:""};const s=c(r,"lunora"),o=c(s,ae);let i=f(o)?k(o,"utf8"):"";if(i.includes(`id: "${a}"`)||new RegExp(String.raw`\bexport const ${t}\b`,"u").test(i))return e.logger.error(`a migration with id "${a}" (export \`${t}\`) already exists in ${o}`),{code:1,file:""};const l=oe(r);i.trim()===""?i=`${l}
`:T.test(i)?i=i.replace(T,l):i=`${l}
${i}`;const d=`export const ${t} = defineMigration({
id: "${a}",
table: "${n}",
up: (document) => document,
});`;return _(s,{recursive:!0}),b(o,`${i.trimEnd()}
${d}
`,"utf8"),e.logger.success(`scaffolded migration "${a}" in ${o}`),{code:0,file:o}},he=(e,r)=>{const a=new v({skipAddingFilesFromTsConfig:!0});return F(a,c(e,"lunora")).find(n=>n.id===r)?.table},be=(e,r)=>{const a=X(r.id);if(a!==void 0)return a;if(r.id.startsWith(m)){r.logger.error(`"${r.id}" names no table — use ${m}<table>`);return}let t;try{t=he(e,r.id)}catch(n){r.logger.error(n instanceof Error?n.message:String(n));return}if(t===void 0){r.logger.error(`migration "${r.id}" not found under lunora/ — declare it with defineMigration({ id: "${r.id}", ... })`);return}if(t===""){r.logger.error(`migration "${r.id}" must declare \`table\` as a static string literal`);return}return t},ve=e=>{const r=e.cwd??process.cwd();if(e.prod&&e.url===void 0){e.logger.error("--prod requires an explicit --url (refusing to migrate the implicit localhost worker)");return}const a=be(r,e);if(a===void 0)return;const t=L(e.url,e.logger,e.cwd);if(t===void 0)return;const{token:n}=z({cwd:r,token:e.token,url:t});if(!n){e.logger.error("admin token required — pass --token, set LUNORA_ADMIN_TOKEN, or add it to .dev.vars (local targets only)");return}if((e.subcommand==="up"||e.subcommand==="down")&&j({prod:e.prod,url:t})&&!e.yes){e.logger.error(`migrate ${e.subcommand} runs the migration against ${t}, which is not local. Re-run with --yes to confirm.`);return}const s=e.fetchImpl??globalThis.fetch;if(typeof s!="function")throw new TypeError("no fetch implementation available — pass fetchImpl or run on Node >= 18");return{fetchImpl:s,requestUrl:`${t}${ce}`,table:a,token:n}},pe=e=>{if(e===null||typeof e!="object")return;const r=e,a=typeof r.failed=="number"&&r.failed>0?r.failed:0;if(a>0)return`${String(a)} shard(s) could not be reached — their rows were NOT migrated (see the per-shard errors above)`;if(r.status==="failed")return"the migration reported `failed` on at least one shard — see the per-shard results above"},we=e=>{const r={id:e.id};return e.subcommand==="status"||(r.direction=e.subcommand,e.dryRun&&(r.dryRun=!0),e.batchSize!==void 0&&(r.batchSize=e.batchSize),e.maxBatches!==void 0&&(r.maxBatches=e.maxBatches)),r},ye=async e=>{const r=ve(e);if(r===void 0)return{body:void 0,code:1,requestUrl:""};const{fetchImpl:a,requestUrl:t,table:n,token:s}=r,o=e.subcommand==="status"?se:ie,i=we(e);e.logger.info(`POST ${t} -> ${e.subcommand} ${e.id} (table "${n}")`);const l=await a(t,{body:JSON.stringify({args:i,functionPath:o,table:n}),headers:{authorization:`Bearer ${s}`,"content-type":"application/json"},method:"POST"}),d=await V(l,e.logger),u=pe(d);return u!==void 0&&e.logger.error(`migrate ${e.subcommand} "${e.id}": ${u}`),{body:d,code:l.ok&&u===void 0?0:1,requestUrl:t}},I=e=>e===void 0?void 0:q(e),$e=async e=>{const{logger:r}=e,a=I(e.fromUrl??e.toUrl),t=I(e.toUrl??e.fromUrl);if(a===t)return r.error("source and target are the same deployment — pass distinct --from-url and --to-url so the D1 export and Hyperdrive import don't run against one database"),{code:1};const n=e.out===void 0?N(c(x(),"lunora-d1ps-")):void 0,s=e.out??c(n,"dump.ndjson");try{r.info(`Exporting .global() data from the D1 source (${a??"http://localhost:8787"}) …`);const o=await Z({fetchImpl:e.fetchImpl,logger:r,out:s,prod:e.prod,tables:e.tables,token:e.fromToken,url:a});if(o.code!==0)return{code:o.code};r.info(`Exported ${String(o.rows)} row(s) (${String(o.bytes)} bytes).`),r.info(`Importing into the Hyperdrive target (${t??"http://localhost:8787"}) …`);const i=await K({batchSize:e.batchSize,fetchImpl:e.fetchImpl,file:s,logger:r,prod:e.prod,token:e.toToken,url:t,yes:e.yes});return i.code!==0?{code:i.code}:(i.inserted===o.rows?r.info(`✓ Migrated ${String(o.rows)} row(s) — counts match. Verify your app reads from Hyperdrive, then decommission the D1 binding.`):r.warn(`Imported ${String(i.inserted)} of ${String(o.rows)} exported row(s) — the remainder likely already existed in the target (see conflicts above). Re-run after resolving, or inspect the dump with --out.`),{code:0})}finally{n!==void 0&&M(n,{force:!0,recursive:!0})}},ze=B(({argument:e,cwd:r,logger:a,options:t})=>{const n=e[0];if(n==="generate")return te({cwd:r,logger:a,name:e[1]??t.name});if(n==="d1-to-hyperdrive")return $e({batchSize:t.batchSize,fromToken:t.fromToken??t.token,fromUrl:t.fromUrl??t.url,logger:a,out:t.out,prod:t.prod===!0,tables:t.tables,toToken:t.toToken??t.token,toUrl:t.toUrl??t.url,yes:t.yes===!0});if(n==="create"){const s=e[1]??t.name;return s?ge({cwd:r,logger:a,name:s,table:t.table}):(a.error("migrate create requires a name. Usage: lunora migrate create <name> [--table <table>]"),{code:1})}if(n==="up"||n==="down"||n==="status"){const s=e[1]??t.name;return s?ye({batchSize:t.batchSize,cwd:r,dryRun:t.dryRun===!0,id:s,logger:a,maxBatches:t.steps,prod:t.prod===!0,subcommand:n,token:t.token,url:W({cwd:r,prod:t.prod===!0,url:t.url}),yes:t.yes===!0}):(a.error(`migrate ${n} requires a migration id. Usage: lunora migrate ${n} <id>`),{code:1})}return a.error(`unknown migrate subcommand: "${n??""}" — expected generate | create | up | down | status`),{code:1}});export{ze as execute,ge as runMigrateCreateCommand,ye as runMigrateDataCommand,te as runMigrateGenerateCommand,$e as runMigrateToHyperdriveCommand};