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