UNPKG

@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) • 11.1 kB
import{existsSync as p,mkdirSync as _,writeFileSync as w,mkdtempSync as C,rmSync as F,readFileSync as U}from"node:fs";import{tmpdir as O}from"node:os";import{discoverMigrations as k,discoverSchema as x,readPackageDependencies as D}from"@lunora/codegen";import{isInteractive as G,promptSelect as P,promptText as j}from"@lunora/config";import{LunoraError as T}from"@lunora/errors";import{join as m}from"@visulima/path";import{Project as v}from"ts-morph";import{r as z,t as L}from"./admin-token-VdUnvnKW.mjs";import{r as B,a as q,n as H}from"./admin-url-DJB8P6Jh.mjs";import{isRefusal as W,exitCodeForStatus as J,EXIT_CODE as d}from"./EXIT_CODE-DVjUgIaG.mjs";import{diffSnapshots as K,renderMigrationFile as V}from"./diffSnapshots-htqNZT8Q.mjs";import X from"./schemaIrToSnapshot-BHuNOfgs.mjs";import{runExportCommand as Z}from"./runExportCommand-DiMCQ-Il.mjs";import{r as Q}from"./import-DoAUdyfU.mjs";import{readAndLogBody as Y}from"../packem_chunks/runRpcCommand.mjs";const h="__lunora_reproject__",ee=e=>{if(!e.startsWith(h))return;const r=e.slice(h.length);return r===""?void 0:r},re=".snapshot.json",N=/[^\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)},te=e=>{const r=M(e.toLowerCase().replaceAll(N,"_"),"_");return r===""?"auto":r},oe=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())}`},ae=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})}},Pe=e=>{const r=e.cwd??process.cwd(),t=m(r,"lunora","schema.ts");if(!p(t)){const f=`schema not found: ${t} — run \`vis generate lunora-table --name=<name>\` to create one`;return e.logger.error(f),{code:d.NOT_FOUND,empty:!0,error:f,migrationFile:""}}const o=new v({skipAddingFilesFromTsConfig:!0}),n=x(o,t),l=X(n),c=m(r,"lunora","migrations"),s=m(c,re);let a;try{a=ae(s)}catch(f){const S=f instanceof Error?f.message:String(f);return e.logger.error(S),{code:d.USAGE,empty:!0,error:S,migrationFile:""}}const i=K(a,l);if(i.empty)return e.logger.info("no schema changes detected — snapshot is already up to date"),{code:0,empty:!0,migrationFile:""};const g=(e.now??(()=>new Date))(),$=te(e.name??"auto"),y=`${oe(g)}_${$}.sql`,b=m(c,y);_(c,{recursive:!0});const R=V($,i,g.toISOString());return w(b,R,"utf8"),w(s,`${JSON.stringify(l,void 0,4)} `,"utf8"),e.logger.success(`wrote ${b}`),i.unsupported.length>0&&e.logger.warn(`${String(i.unsupported.length)} unsupported diff(s) — see the comment block in ${y} and write the SQL manually`),{code:0,empty:!1,migrationFile:b}},ne="migrations.ts",A=/^[A-Za-z_]\w*$/u,ie=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"]),E=/^import\s*\{\s*defineMigration\s*\}\s*from\s*["'](?:@lunora\/server|lunorash\/server)["'][^\n]*$/mu,se=e=>`import { defineMigration } from "${D(e)?.has("lunorash")??!1?"lunorash/server":"@lunora/server"}";`,ce="__lunora_admin__:runMigration",de="__lunora_admin__:migrationStatus",le="/_lunora/migrate",ue=e=>M(e.trim().toLowerCase().replaceAll(N,"-"),"-"),me=e=>e.split("-").filter(r=>r.length>0).map((r,t)=>t===0?r:r.charAt(0).toUpperCase()+r.slice(1)).join(""),ge=e=>{const r=m(e,"lunora","schema.ts");if(!p(r))return[];try{const t=new v({skipAddingFilesFromTsConfig:!0});return x(t,r).tables.map(o=>o.name)}catch{return[]}},fe=async e=>e.length>0?P("Which table does this migration iterate?",e.map(r=>({label:r,value:r}))):j("Target table for the migration: "),he=async(e,r)=>{if(r.table!==void 0)return r.table;const t=r.promptTable??(G()?fe: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 n=(await t(ge(e)))?.trim();if(n===void 0||n===""){r.logger.error("no table selected — re-run with --table <table>");return}return n},je=async e=>{const r=e.cwd??process.cwd(),t=ue(e.name);if(t==="")return e.logger.error(`invalid migration name: "${e.name}" — must contain at least one alphanumeric character`),{code:d.USAGE,file:""};const o=me(t);if(!A.test(o)||ie.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:d.USAGE,file:""};const n=await he(r,e);if(n===void 0)return{code:d.USAGE,file:""};if(!A.test(n))return e.logger.error(`invalid table: "${n}" — must be a valid identifier ([A-Za-z_][A-Za-z0-9_]*)`),{code:d.USAGE,file:""};const l=m(r,"lunora"),c=m(l,ne);let s=p(c)?U(c,"utf8"):"";if(s.includes(`id: "${t}"`)||new RegExp(String.raw`\bexport const ${o}\b`,"u").test(s))return e.logger.error(`a migration with id "${t}" (export \`${o}\`) already exists in ${c}`),{code:d.CONFLICT,file:""};const a=se(r);s.trim()===""?s=`${a} `:E.test(s)?s=s.replace(E,a):s=`${a} ${s}`;const i=`export const ${o} = defineMigration({ id: "${t}", table: "${n}", up: (document) => document, });`;return _(l,{recursive:!0}),w(c,`${s.trimEnd()} ${i} `,"utf8"),e.logger.success(`scaffolded migration "${t}" in ${c}`),{code:0,file:c}},pe=(e,r)=>{const t=new v({skipAddingFilesFromTsConfig:!0});return k(t,m(e,"lunora")).find(n=>n.id===r)?.table},be=(e,r)=>{const t=ee(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=pe(e,r.id)}catch(n){r.logger.error(n instanceof Error?n.message:String(n));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},we=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:d.USAGE};const t=be(r,e);if(t===void 0)return{refused:d.USAGE};const o=B(e.url,e.logger,e.cwd);if(o===void 0)return{refused:d.USAGE};const{token:n}=z({cwd:r,token:e.token,url:o});return n?(e.subcommand==="up"||e.subcommand==="down")&&L({prod:e.prod,url:o})&&!e.yes?(e.logger.error(`migrate ${e.subcommand} runs the migration against ${o}, which is not local. Re-run with --yes to confirm.`),{refused:d.USAGE}):{fetchImpl:e.fetchImpl??q,requestUrl:`${o}${le}`,table:t,token:n}:(e.logger.error("admin token required — pass --token, set LUNORA_ADMIN_TOKEN, or add it to .dev.vars (local targets only)"),{refused:d.AUTH})},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"},$e=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},ze=async e=>{const r=we(e);if(W(r))return{body:void 0,code:r.refused,requestUrl:""};const{fetchImpl:t,requestUrl:o,table:n,token:l}=r,c=e.subcommand==="status"?de:ce,s=$e(e);e.logger.info(`POST ${o} -> ${e.subcommand} ${e.id} (table "${n}")`);const a=await t(o,{body:JSON.stringify({args:s,functionPath:c,table:n}),headers:{authorization:`Bearer ${l}`,"content-type":"application/json"},method:"POST"}),i=await Y(a,e.logger),u=ve(i);u!==void 0&&e.logger.error(`migrate ${e.subcommand} "${e.id}": ${u}`);const g=u===void 0?0:1;return{body:i,code:a.ok?g:J(a.status),requestUrl:o}},I=e=>e===void 0?void 0:H(e),ye={"d1-to-hyperdrive":{source:"D1",target:"Hyperdrive"},"hyperdrive-to-d1":{source:"Hyperdrive",target:"D1"}},Se=(e,r,t,o)=>e?`Imported ${String(r.inserted)} and replaced ${String(r.replaced)} of ${String(r.exported)} exported row(s). The rest were not written; see the errors reported above, then fix them and re-run.`:`Imported ${String(r.inserted)} of ${String(r.exported)} exported row(s). The rest already exist in the ${o} target and were left unchanged (reported as conflicts above), so this copied rows missing from ${o}, not later edits to rows it already holds. Re-run with --replace to overwrite them with the ${t} values, or keep the dump with --out and reconcile by hand.`,Le=async e=>{const{logger:r}=e,{source:t,target:o}=ye[e.direction],n=I(e.fromUrl??e.toUrl),l=I(e.toUrl??e.fromUrl);if(n===l){const a=`source and target are the same deployment — pass distinct --from-url and --to-url so the ${t} export and ${o} import don't run against one database`;return r.error(a),{bytes:0,code:d.USAGE,error:a,exported:0,imported:0,replaced:0}}if(e.replace===!0&&(e.tables===void 0||e.tables.trim()==="")){const a="--replace needs --tables naming the .global() tables to move: an export without a table list also carries shard-local rows, and --replace would overwrite them in the target";return r.error(a),{bytes:0,code:d.USAGE,error:a,exported:0,imported:0,replaced:0}}const c=e.out===void 0?C(m(O(),"lunora-d1ps-")):void 0,s=e.out??m(c,"dump.ndjson");try{r.info(`Exporting .global() data from the ${t} source (${n??"http://localhost:8787"}) …`);const a=await Z({fetchImpl:e.fetchImpl,logger:r,out:s,prod:e.prod,tables:e.tables,token:e.fromToken,url:n});if(a.code!==0)return{bytes:a.bytes,code:a.code,exported:a.rows,imported:0,replaced:0,...a.error===void 0?{}:{error:a.error}};r.info(`Exported ${String(a.rows)} row(s) (${String(a.bytes)} bytes).`),r.info(`Importing into the ${o} target (${l??"http://localhost:8787"}) …`);const i=await Q({batchSize:e.batchSize,fetchImpl:e.fetchImpl,file:s,logger:r,prod:e.prod,replace:e.replace,token:e.toToken,url:l,yes:e.yes}),u=i.replaced??0;if(i.code!==0)return{bytes:a.bytes,code:i.code,exported:a.rows,imported:i.inserted,replaced:u,...i.error===void 0?{}:{error:i.error}};if(i.inserted+u===a.rows){const g=u>0?` (${String(u)} existing row(s) overwritten; rows deleted on ${t} since cutover are not removed)`:"";r.info(`✓ Migrated ${String(a.rows)} row(s)${g} — counts match. Verify your app reads from ${o}, then decommission the ${t} binding.`)}else r.warn(Se(e.replace===!0,{exported:a.rows,inserted:i.inserted,replaced:u},t,o));return{bytes:a.bytes,code:0,exported:a.rows,imported:i.inserted,replaced:u}}finally{c!==void 0&&F(c,{force:!0,recursive:!0})}};export{Le as runMigrateBackendCommand,je as runMigrateCreateCommand,ze as runMigrateDataCommand,Pe as runMigrateGenerateCommand};