@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 p,mkdirSync as _,writeFileSync as v,mkdtempSync as x,rmSync as F,readFileSync as U}from"node:fs";import{tmpdir as O}from"node:os";import{discoverMigrations as k,discoverSchema as N,readPackageDependencies as D}from"@lunora/codegen";import{isInteractive as P,promptSelect as G,promptText as j}from"@lunora/config";import{LunoraError as T}from"@lunora/errors";import{join as l}from"@visulima/path";import{Project as w}from"ts-morph";import{r as z,t as L}from"./admin-token-VdUnvnKW.mjs";import{r as q,n as H}from"./admin-url-BhF5ufg1.mjs";import{isRefusal as B,exitCodeForStatus as W,EXIT_CODE as c}from"./EXIT_CODE-08cwt3MK.mjs";import{diffSnapshots as J,renderMigrationFile as V}from"./diffSnapshots-DtC5Cs4Z.mjs";import X from"./schemaIrToSnapshot-irmd5g0Y.mjs";import{runExportCommand as Z}from"./runExportCommand-LEwEr8p0.mjs";import{r as K}from"./import-D7qdpSmJ.mjs";import{readAndLogBody as Q}from"../packem_chunks/runRpcCommand.mjs";const h="__lunora_reproject__",Y=e=>{if(!e.startsWith(h))return;const r=e.slice(h.length);return r===""?void 0:r},ee=".snapshot.json",R=/[^\da-z]+/gu,M=(e,r)=>{let t=0,o=e.length;for(;t<o&&e[t]===r;)t+=1;for(;o>t&&e[o-1]===r;)o-=1;return e.slice(t,o)},re=e=>{const r=M(e.toLowerCase().replaceAll(R,"_"),"_");return r===""?"auto":r},te=e=>{const r=(t,o=2)=>t.toString().padStart(o,"0");return`${String(e.getUTCFullYear())}${r(e.getUTCMonth()+1)}${r(e.getUTCDate())}${r(e.getUTCHours())}${r(e.getUTCMinutes())}${r(e.getUTCSeconds())}`},oe=e=>{if(p(e))try{const r=U(e,"utf8"),t=JSON.parse(r);if(t.version!==1)throw new T("INTERNAL",`unsupported snapshot version: ${t.version}`);return t}catch(r){const t=r instanceof Error?r.message:String(r);throw new T("INTERNAL",`failed to read ${e}: ${t}`,{cause:r})}},ke=e=>{const r=e.cwd??process.cwd(),t=l(r,"lunora","schema.ts");if(!p(t)){const m=`schema not found: ${t} — run \`vis generate lunora-table --name=<name>\` to create one`;return e.logger.error(m),{code:c.NOT_FOUND,empty:!0,error:m,migrationFile:""}}const o=new w({skipAddingFilesFromTsConfig:!0}),a=N(o,t),s=X(a),n=l(r,"lunora","migrations"),i=l(n,ee);let d;try{d=oe(i)}catch(m){const S=m instanceof Error?m.message:String(m);return e.logger.error(S),{code:c.USAGE,empty:!0,error:S,migrationFile:""}}const u=J(d,s);if(u.empty)return e.logger.info("no schema changes detected — snapshot is already up to date"),{code:0,empty:!0,migrationFile:""};const f=(e.now??(()=>new Date))(),y=re(e.name??"auto"),$=`${te(f)}_${y}.sql`,b=l(n,$);_(n,{recursive:!0});const C=V(y,u,f.toISOString());return v(b,C,"utf8"),v(i,`${JSON.stringify(s,void 0,4)}
`,"utf8"),e.logger.success(`wrote ${b}`),u.unsupported.length>0&&e.logger.warn(`${String(u.unsupported.length)} unsupported diff(s) — see the comment block in ${$} and write the SQL manually`),{code:0,empty:!1,migrationFile:b}},ne="migrations.ts",I=/^[A-Za-z_]\w*$/u,ae=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"]),A=/^import\s*\{\s*defineMigration\s*\}\s*from\s*["'](?:@lunora\/server|lunorash\/server)["'][^\n]*$/mu,ie=e=>`import { defineMigration } from "${D(e)?.has("lunorash")??!1?"lunorash/server":"@lunora/server"}";`,se="__lunora_admin__:runMigration",ce="__lunora_admin__:migrationStatus",de="/_lunora/migrate",le=e=>M(e.trim().toLowerCase().replaceAll(R,"-"),"-"),ue=e=>e.split("-").filter(r=>r.length>0).map((r,t)=>t===0?r:r.charAt(0).toUpperCase()+r.slice(1)).join(""),me=e=>{const r=l(e,"lunora","schema.ts");if(!p(r))return[];try{const t=new w({skipAddingFilesFromTsConfig:!0});return N(t,r).tables.map(o=>o.name)}catch{return[]}},ge=async e=>e.length>0?G("Which table does this migration iterate?",e.map(r=>({label:r,value:r}))):j("Target table for the migration: "),fe=async(e,r)=>{if(r.table!==void 0)return r.table;const t=r.promptTable??(P()?ge:void 0);if(t===void 0){r.logger.error("migrate create requires a target table when not running interactively — re-run with --table <table>");return}const a=(await t(me(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(),t=le(e.name);if(t==="")return e.logger.error(`invalid migration name: "${e.name}" — must contain at least one alphanumeric character`),{code:c.USAGE,file:""};const o=ue(t);if(!I.test(o)||ae.has(o))return e.logger.error(`invalid migration name: "${e.name}" derives the export \`${o}\`, which is not a valid identifier — pick a name that starts with a letter and isn't a reserved word`),{code:c.USAGE,file:""};const a=await fe(r,e);if(a===void 0)return{code:c.USAGE,file:""};if(!I.test(a))return e.logger.error(`invalid table: "${a}" — must be a valid identifier ([A-Za-z_][A-Za-z0-9_]*)`),{code:c.USAGE,file:""};const s=l(r,"lunora"),n=l(s,ne);let i=p(n)?U(n,"utf8"):"";if(i.includes(`id: "${t}"`)||new RegExp(String.raw`\bexport const ${o}\b`,"u").test(i))return e.logger.error(`a migration with id "${t}" (export \`${o}\`) already exists in ${n}`),{code:c.CONFLICT,file:""};const d=ie(r);i.trim()===""?i=`${d}
`:A.test(i)?i=i.replace(A,d):i=`${d}
${i}`;const u=`export const ${o} = defineMigration({
id: "${t}",
table: "${a}",
up: (document) => document,
});`;return _(s,{recursive:!0}),v(n,`${i.trimEnd()}
${u}
`,"utf8"),e.logger.success(`scaffolded migration "${t}" in ${n}`),{code:0,file:n}},he=(e,r)=>{const t=new w({skipAddingFilesFromTsConfig:!0});return k(t,l(e,"lunora")).find(a=>a.id===r)?.table},pe=(e,r)=>{const t=Y(r.id);if(t!==void 0)return t;if(r.id.startsWith(h)){r.logger.error(`"${r.id}" names no table — use ${h}<table>`);return}let o;try{o=he(e,r.id)}catch(a){r.logger.error(a instanceof Error?a.message:String(a));return}if(o===void 0){r.logger.error(`migration "${r.id}" not found under lunora/ — declare it with defineMigration({ id: "${r.id}", ... })`);return}if(o===""){r.logger.error(`migration "${r.id}" must declare \`table\` as a static string literal`);return}return o},be=e=>{const r=e.cwd??process.cwd();if(e.prod&&e.url===void 0)return e.logger.error("--prod requires an explicit --url (refusing to migrate the implicit localhost worker)"),{refused:c.USAGE};const t=pe(r,e);if(t===void 0)return{refused:c.USAGE};const o=q(e.url,e.logger,e.cwd);if(o===void 0)return{refused:c.USAGE};const{token:a}=z({cwd:r,token:e.token,url:o});if(!a)return e.logger.error("admin token required — pass --token, set LUNORA_ADMIN_TOKEN, or add it to .dev.vars (local targets only)"),{refused:c.AUTH};if((e.subcommand==="up"||e.subcommand==="down")&&L({prod:e.prod,url:o})&&!e.yes)return e.logger.error(`migrate ${e.subcommand} runs the migration against ${o}, which is not local. Re-run with --yes to confirm.`),{refused:c.USAGE};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:`${o}${de}`,table:t,token:a}},ve=e=>{if(e===null||typeof e!="object")return;const r=e,t=typeof r.failed=="number"&&r.failed>0?r.failed:0;if(t>0)return`${String(t)} 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},Pe=async e=>{const r=be(e);if(B(r))return{body:void 0,code:r.refused,requestUrl:""};const{fetchImpl:t,requestUrl:o,table:a,token:s}=r,n=e.subcommand==="status"?ce:se,i=we(e);e.logger.info(`POST ${o} -> ${e.subcommand} ${e.id} (table "${a}")`);const d=await t(o,{body:JSON.stringify({args:i,functionPath:n,table:a}),headers:{authorization:`Bearer ${s}`,"content-type":"application/json"},method:"POST"}),u=await Q(d,e.logger),g=ve(u);g!==void 0&&e.logger.error(`migrate ${e.subcommand} "${e.id}": ${g}`);const f=g===void 0?0:1;return{body:u,code:d.ok?f:W(d.status),requestUrl:o}},E=e=>e===void 0?void 0:H(e),Ge=async e=>{const{logger:r}=e,t=E(e.fromUrl??e.toUrl),o=E(e.toUrl??e.fromUrl);if(t===o){const n="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";return r.error(n),{bytes:0,code:c.USAGE,error:n,exported:0,imported:0}}const a=e.out===void 0?x(l(O(),"lunora-d1ps-")):void 0,s=e.out??l(a,"dump.ndjson");try{r.info(`Exporting .global() data from the D1 source (${t??"http://localhost:8787"}) …`);const n=await Z({fetchImpl:e.fetchImpl,logger:r,out:s,prod:e.prod,tables:e.tables,token:e.fromToken,url:t});if(n.code!==0)return{bytes:n.bytes,code:n.code,exported:n.rows,imported:0,...n.error===void 0?{}:{error:n.error}};r.info(`Exported ${String(n.rows)} row(s) (${String(n.bytes)} bytes).`),r.info(`Importing into the Hyperdrive target (${o??"http://localhost:8787"}) …`);const i=await K({batchSize:e.batchSize,fetchImpl:e.fetchImpl,file:s,logger:r,prod:e.prod,token:e.toToken,url:o,yes:e.yes});return i.code!==0?{bytes:n.bytes,code:i.code,exported:n.rows,imported:i.inserted,...i.error===void 0?{}:{error:i.error}}:(i.inserted===n.rows?r.info(`✓ Migrated ${String(n.rows)} row(s) — counts match. Verify your app reads from Hyperdrive, then decommission the D1 binding.`):r.warn(`Imported ${String(i.inserted)} of ${String(n.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.`),{bytes:n.bytes,code:0,exported:n.rows,imported:i.inserted})}finally{a!==void 0&&F(a,{force:!0,recursive:!0})}};export{De as runMigrateCreateCommand,Pe as runMigrateDataCommand,ke as runMigrateGenerateCommand,Ge as runMigrateToHyperdriveCommand};