@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.2 kB
JavaScript
import{existsSync as m,mkdirSync as T,writeFileSync as h,mkdtempSync as N,rmSync as M,readFileSync as I}from"node:fs";import{tmpdir as R}from"node:os";import{discoverSchema as _,readPackageDependencies as U,discoverMigrations as x}from"@lunora/codegen";import{isInteractive as C,promptSelect as O,promptText as F}from"@lunora/config";import{LunoraError as y}from"@lunora/errors";import{join as c}from"@visulima/path";import{Project as b}from"ts-morph";import{r as P}from"../packem_shared/admin-url-Cil3Or6N.mjs";import{d as D}from"../packem_shared/command-DwC11Cwe.mjs";import{diffSnapshots as z,renderMigrationFile as L}from"../packem_shared/diffSnapshots-CIOFQN_a.mjs";import{a as j}from"../packem_shared/resolve-target-C_ZloTvd.mjs";import q from"../packem_shared/schemaIrToSnapshot-qpRoV7HY.mjs";import{readAndLogBody as H}from"./runRpcCommand.mjs";import{runExportCommand as G}from"../packem_shared/runExportCommand-DmXKEWSq.mjs";import{r as B}from"../packem_shared/import-Db3Gk4mx.mjs";const u="__lunora_reproject__",W=e=>{if(!e.startsWith(u))return;const r=e.slice(u.length);return r===""?void 0:r},J=".snapshot.json",k=/[^\da-z]+/gu,A=(e,r)=>{let n=0,t=e.length;for(;n<t&&e[n]===r;)n+=1;for(;t>n&&e[t-1]===r;)t-=1;return e.slice(n,t)},K=e=>{const r=A(e.toLowerCase().replaceAll(k,"_"),"_");return r===""?"auto":r},V=e=>{const r=(n,t=2)=>n.toString().padStart(t,"0");return`${String(e.getUTCFullYear())}${r(e.getUTCMonth()+1)}${r(e.getUTCDate())}${r(e.getUTCHours())}${r(e.getUTCMinutes())}${r(e.getUTCSeconds())}`},Z=e=>{if(m(e))try{const r=I(e,"utf8"),n=JSON.parse(r);if(n.version!==1)throw new y("INTERNAL",`unsupported snapshot version: ${n.version}`);return n}catch(r){const n=r instanceof Error?r.message:String(r);throw new y("INTERNAL",`failed to read ${e}: ${n}`,{cause:r})}},X=e=>{const r=e.cwd??process.cwd(),n=c(r,"lunora","schema.ts");if(!m(n))return e.logger.error(`schema not found: ${n} — run \`vis generate lunora-table --name=<name>\` to create one`),{code:1,empty:!0,migrationFile:""};const t=new b({skipAddingFilesFromTsConfig:!0}),a=_(t,n),s=q(a),o=c(r,"lunora","migrations"),i=c(o,J);let d;try{d=Z(i)}catch(g){return e.logger.error(g instanceof Error?g.message:String(g)),{code:1,empty:!0,migrationFile:""}}const l=z(d,s);if(l.empty)return e.logger.info("no schema changes detected — snapshot is already up to date"),{code:0,empty:!0,migrationFile:""};const v=(e.now??(()=>new Date))(),p=K(e.name??"auto"),w=`${V(v)}_${p}.sql`,f=c(o,w);T(o,{recursive:!0});const E=L(p,l,v.toISOString());return h(f,E,"utf8"),h(i,`${JSON.stringify(s,void 0,4)}
`,"utf8"),e.logger.success(`wrote ${f}`),l.unsupported.length>0&&e.logger.warn(`${String(l.unsupported.length)} unsupported diff(s) — see the comment block in ${w} and write the SQL manually`),{code:0,empty:!1,migrationFile:f}},Q="migrations.ts",$=/^[A-Za-z_]\w*$/u,Y=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"]),S=/^import\s*\{\s*defineMigration\s*\}\s*from\s*["'](?:@lunora\/server|lunorash\/server)["'][^\n]*$/mu,ee=e=>`import { defineMigration } from "${U(e)?.has("lunorash")??!1?"lunorash/server":"@lunora/server"}";`,re="__lunora_admin__:runMigration",te="__lunora_admin__:migrationStatus",ne="/_lunora/migrate",ae=e=>A(e.trim().toLowerCase().replaceAll(k,"-"),"-"),oe=e=>e.split("-").filter(r=>r.length>0).map((r,n)=>n===0?r:r.charAt(0).toUpperCase()+r.slice(1)).join(""),ie=e=>{const r=c(e,"lunora","schema.ts");if(!m(r))return[];try{const n=new b({skipAddingFilesFromTsConfig:!0});return _(n,r).tables.map(t=>t.name)}catch{return[]}},se=async e=>e.length>0?O("Which table does this migration iterate?",e.map(r=>({label:r,value:r}))):F("Target table for the migration: "),ce=async(e,r)=>{if(r.table!==void 0)return r.table;const n=r.promptTable??(C()?se:void 0);if(n===void 0){r.logger.error("migrate create requires a target table when not running interactively — re-run with --table <table>");return}const a=(await n(ie(e)))?.trim();if(a===void 0||a===""){r.logger.error("no table selected — re-run with --table <table>");return}return a},de=async e=>{const r=e.cwd??process.cwd(),n=ae(e.name);if(n==="")return e.logger.error(`invalid migration name: "${e.name}" — must contain at least one alphanumeric character`),{code:1,file:""};const t=oe(n);if(!$.test(t)||Y.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 a=await ce(r,e);if(a===void 0)return{code:1,file:""};if(!$.test(a))return e.logger.error(`invalid table: "${a}" — must be a valid identifier ([A-Za-z_][A-Za-z0-9_]*)`),{code:1,file:""};const s=c(r,"lunora"),o=c(s,Q);let i=m(o)?I(o,"utf8"):"";if(i.includes(`id: "${n}"`)||new RegExp(String.raw`\bexport const ${t}\b`,"u").test(i))return e.logger.error(`a migration with id "${n}" (export \`${t}\`) already exists in ${o}`),{code:1,file:""};const d=ee(r);i.trim()===""?i=`${d}
`:S.test(i)?i=i.replace(S,d):i=`${d}
${i}`;const l=`export const ${t} = defineMigration({
id: "${n}",
table: "${a}",
up: (document) => document,
});`;return T(s,{recursive:!0}),h(o,`${i.trimEnd()}
${l}
`,"utf8"),e.logger.success(`scaffolded migration "${n}" in ${o}`),{code:0,file:o}},le=(e,r)=>{const n=new b({skipAddingFilesFromTsConfig:!0});return x(n,c(e,"lunora")).find(a=>a.id===r)?.table},ue=(e,r)=>{const n=W(r.id);if(n!==void 0)return n;if(r.id.startsWith(u)){r.logger.error(`"${r.id}" names no table — use ${u}<table>`);return}let t;try{t=le(e,r.id)}catch(a){r.logger.error(a instanceof Error?a.message:String(a));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},me=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}if(e.prod&&(e.subcommand==="up"||e.subcommand==="down")&&!e.yes){e.logger.error(`migrate ${e.subcommand} --prod runs the migration against production. Re-run with --yes to confirm.`);return}const n=e.token??process.env.LUNORA_ADMIN_TOKEN;if(!n){e.logger.error("admin token required — pass --token or set LUNORA_ADMIN_TOKEN");return}const t=ue(r,e);if(t===void 0)return;const a=P(e.url,e.logger,e.cwd);if(a===void 0)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:`${a}${ne}`,table:t,token:n}},fe=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},ge=async e=>{const r=me(e);if(r===void 0)return{body:void 0,code:1,requestUrl:""};const{fetchImpl:n,requestUrl:t,table:a,token:s}=r,o=e.subcommand==="status"?te:re,i=fe(e);e.logger.info(`POST ${t} -> ${e.subcommand} ${e.id} (table "${a}")`);const d=await n(t,{body:JSON.stringify({args:i,functionPath:o,table:a}),headers:{authorization:`Bearer ${s}`,"content-type":"application/json"},method:"POST"});return{body:await H(d,e.logger),code:d.ok?0:1,requestUrl:t}},he=async e=>{const{logger:r}=e,n=e.fromUrl??e.toUrl,t=e.toUrl??e.fromUrl;if(n!==void 0&&n===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 a=e.out===void 0?N(c(R(),"lunora-d1ps-")):void 0,s=e.out??c(a,"dump.ndjson");try{r.info(`Exporting .global() data from the D1 source (${n??"http://localhost:8787"}) …`);const o=await G({fetchImpl:e.fetchImpl,logger:r,out:s,prod:e.prod,tables:e.tables,token:e.fromToken,url:n});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 B({batchSize:e.batchSize,fetchImpl:e.fetchImpl,file:s,logger:r,prod:e.prod,token:e.toToken,url:t});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{a!==void 0&&M(a,{force:!0,recursive:!0})}},xe=D(({argument:e,cwd:r,logger:n,options:t})=>{const a=e[0];if(a==="generate")return X({cwd:r,logger:n,name:e[1]??t.name});if(a==="d1-to-hyperdrive")return he({batchSize:t.batchSize,fromToken:t.fromToken??t.token,fromUrl:t.fromUrl??t.url,logger:n,out:t.out,prod:t.prod===!0,tables:t.tables,toToken:t.toToken??t.token,toUrl:t.toUrl??t.url});if(a==="create"){const s=e[1]??t.name;return s?de({cwd:r,logger:n,name:s,table:t.table}):(n.error("migrate create requires a name. Usage: lunora migrate create <name> [--table <table>]"),{code:1})}if(a==="up"||a==="down"||a==="status"){const s=e[1]??t.name;return s?ge({batchSize:t.batchSize,cwd:r,dryRun:t.dryRun===!0,id:s,logger:n,maxBatches:t.steps,prod:t.prod===!0,subcommand:a,token:t.token,url:j({cwd:r,prod:t.prod===!0,url:t.url}),yes:t.yes===!0}):(n.error(`migrate ${a} requires a migration id. Usage: lunora migrate ${a} <id>`),{code:1})}return n.error(`unknown migrate subcommand: "${a??""}" — expected generate | create | up | down | status`),{code:1}});export{xe as execute,de as runMigrateCreateCommand,ge as runMigrateDataCommand,X as runMigrateGenerateCommand,he as runMigrateToHyperdriveCommand};