@lunora/cli 1.0.0-alpha.469 → 1.0.0-alpha.470

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/index.d.mts +12 -0
  2. package/dist/index.d.ts +12 -0
  3. package/dist/index.mjs +1 -1
  4. package/dist/packem_chunks/cli.mjs +2 -2
  5. package/dist/packem_chunks/dispatch.mjs +1 -1
  6. package/dist/packem_chunks/handler.mjs +2 -2
  7. package/dist/packem_chunks/handler10.mjs +2 -1
  8. package/dist/packem_chunks/handler11.mjs +1 -1
  9. package/dist/packem_chunks/handler12.mjs +1 -1
  10. package/dist/packem_chunks/handler13.mjs +1 -3
  11. package/dist/packem_chunks/handler14.mjs +3 -1
  12. package/dist/packem_chunks/handler15.mjs +1 -1
  13. package/dist/packem_chunks/handler16.mjs +1 -1
  14. package/dist/packem_chunks/handler17.mjs +1 -1
  15. package/dist/packem_chunks/handler18.mjs +1 -1
  16. package/dist/packem_chunks/handler19.mjs +1 -1
  17. package/dist/packem_chunks/handler20.mjs +1 -7
  18. package/dist/packem_chunks/handler21.mjs +7 -2
  19. package/dist/packem_chunks/handler24.mjs +1 -1
  20. package/dist/packem_chunks/handler25.mjs +2 -1
  21. package/dist/packem_chunks/handler26.mjs +1 -2
  22. package/dist/packem_chunks/handler27.mjs +2 -99
  23. package/dist/packem_chunks/handler28.mjs +99 -3
  24. package/dist/packem_chunks/handler29.mjs +3 -15
  25. package/dist/packem_chunks/handler3.mjs +1 -1
  26. package/dist/packem_chunks/handler30.mjs +15 -1
  27. package/dist/packem_chunks/handler31.mjs +1 -1
  28. package/dist/packem_chunks/handler32.mjs +1 -0
  29. package/dist/packem_chunks/handler4.mjs +1 -1
  30. package/dist/packem_chunks/handler5.mjs +1 -3
  31. package/dist/packem_chunks/handler6.mjs +3 -1
  32. package/dist/packem_chunks/handler7.mjs +1 -1
  33. package/dist/packem_chunks/handler8.mjs +1 -1
  34. package/dist/packem_chunks/handler9.mjs +1 -2
  35. package/dist/packem_chunks/runDevCommand.mjs +1 -1
  36. package/dist/packem_chunks/runInitCommand.mjs +37 -27
  37. package/dist/packem_chunks/runRpcCommand.mjs +1 -1
  38. package/dist/packem_shared/createRecordingSpawner-DchLi0gR.mjs +1 -0
  39. package/dist/packem_shared/{plan-KTxWYvt_.mjs → plan-pog5AvMk.mjs} +1 -1
  40. package/dist/packem_shared/{planDevCommand-CEDLRkjW.mjs → planDevCommand-Bl6OrwXm.mjs} +1 -1
  41. package/dist/packem_shared/{resolve-target-C_ZloTvd.mjs → resolve-target-BRvw8Pf7.mjs} +1 -1
  42. package/dist/packem_shared/{runDeployCommand-5ng8LjBN.mjs → runDeployCommand-CNgOUx91.mjs} +1 -1
  43. package/dist/packem_shared/{vectorize-metadata-BVSF8z2h.mjs → vectorize-metadata-DK61NmVx.mjs} +1 -1
  44. package/dist/packem_shared/{wrangler-secrets-DlSlWvRH.mjs → wrangler-secrets-C9siMBwN.mjs} +1 -1
  45. package/package.json +8 -8
  46. package/dist/packem_shared/createRecordingSpawner-01ZngIwK.mjs +0 -1
@@ -1,2 +1,7 @@
1
- import{randomUUID as $}from"node:crypto";import{readdirSync as D,readFileSync as b,existsSync as d,rmSync as l,mkdirSync as S,copyFileSync as z,writeFileSync as A,lstatSync as j}from"node:fs";import{join as f,resolve as m,dirname as v}from"node:path";import{resolveLazyFunctions as F,DEFAULT_BUNDLE_META_PATH as E}from"@lunora/config";import{p as M}from"../packem_shared/api-spec-lYyq4vL6.mjs";import{d as T}from"../packem_shared/command-BpMv2n0o.mjs";import{p as _,v as R,r as U}from"./handler.mjs";import{defaultSpawner as x}from"../packem_shared/createRecordingSpawner-01ZngIwK.mjs";import{gzipSync as I}from"node:zlib";const g=64*1024*1024,P=e=>!e.endsWith(".map")&&e!=="bundle-meta.json"&&e!=="README.md",C=e=>{let i;try{i=D(e,{recursive:!0,withFileTypes:!0})}catch{return}let o=0,r=0,n=0;for(const t of i){if(!t.isFile()||!P(t.name))continue;const c=b(f(t.parentPath,t.name));o+=1,n+=c.byteLength,r+=I(c).byteLength}return o===0?void 0:{files:o,gzipBytes:r,rawBytes:n}},k=".lunora/build",O=async e=>x({...e,stdoutToStderr:!0}),B=e=>`${(e/1024).toFixed(1)} KiB`,h=e=>d(e)?D(e,{encoding:"utf8",recursive:!0}).map(i=>{try{const o=j(f(e,i));return`${i}:${String(o.size)}:${String(o.mtimeMs)}`}catch{return`${i}:gone`}}).join(`
2
- `):"",W=async e=>{const i=e.outDir??k,o=e.format==="json",{logger:r}=e,n=e.cwd??process.cwd(),t=m(n,i,"bundle-meta.json"),c=R(n,$()),y=d(t)?b(t):void 0;l(t,{force:!0}),l(c,{force:!0});const w=m(n,i),L=h(w);let s;try{s=await U({allowSchemaDrift:e.allowSchemaDrift,apiSpec:e.apiSpec,bundleMetaPath:c,frameworkBuild:e.frameworkBuild,lazyFunctions:F(e.lazyFunctions),commandName:"build",clientBuildOutputJson:o,cwd:e.cwd,dryRun:!0,emitBindings:e.emitBindings,format:void 0,interactive:o?!1:void 0,logger:r,outDir:i,smokeLauncher:e.smokeLauncher,smokeLoad:e.smokeLoad,spawner:e.spawner??(o?O:void 0),strictAdvisories:e.strictAdvisories,target:e.target});const u=e.frameworkBuild===!1?f(n,E):c;s.code===0&&!d(t)&&d(u)&&(S(v(t),{recursive:!0}),z(u,t))}finally{l(c,{force:!0}),y!==void 0&&!d(t)&&h(w)===L&&s?.code!==0&&(S(v(t),{recursive:!0}),A(t,y))}if(s.code!==0)return s;r.success(`build complete — bundle written to ${i}`);const a=C(m(e.cwd??process.cwd(),i));if(a===void 0)r.warn(`could not weigh the bundle — nothing uploadable was found in ${i}`);else{const u=(a.rawBytes/g*100).toFixed(1),p=`bundle: ${B(a.rawBytes)} raw across ${String(a.files)} file(s) — ${u}% of Cloudflare's 64 MiB uncompressed Worker size limit (${B(a.gzipBytes)} gzipped, for reference; there is no compressed limit)`;a.rawBytes>g?r.warn(`${p} — Cloudflare will reject this upload`):r.info(p)}return{...s,bundle:a}},V=T(async({cwd:e,format:i,logger:o,options:r})=>{const n=_(e,r.root),t=await W({allowSchemaDrift:r.allowSchemaDrift===!0,apiSpec:M(r.apiSpec),lazyFunctions:F(r.lazyFunctions),cwd:n,emitBindings:r.emitBindings,format:i,frameworkBuild:r.frameworkBuild,logger:o,outDir:r.outDir,smokeLoad:r.smokeLoad,strictAdvisories:r.strictAdvisories,target:r.target});return{code:t.code,data:{bundle:t.bundle,deployment:t.deployment,outDir:r.outDir??k,validation:t.validation},error:t.error}});export{V as execute,W as runBuildCommand};
1
+ import{writeFileSync as h,existsSync as S,readFileSync as p}from"node:fs";import{isAbsolute as v,join as w}from"node:path";import{byCodepoint as b,scoreAdvisor as y,parseAdvisorMap as x,compareToBaseline as j}from"@lunora/advisor";import{runCodegen as A}from"@lunora/codegen";import{resolveLazyFunctions as M}from"@lunora/config";import{wranglerCodegenInputs as F}from"@lunora/config/cloudflare";import{d as D}from"../packem_shared/command-BpMv2n0o.mjs";import{EXIT_CODE as E}from"../packem_shared/EXIT_CODE-DQVhSE-1.mjs";import{D as u}from"./cli.mjs";const l={clean:"·",exempt:"–",failing:"✗",warned:"!"},d=(e,r)=>e.length>=r?e:e+" ".repeat(r-e.length),R=e=>`${e.id} (${e.visibility} ${e.kind})`,f=e=>{let r=0;for(const n of e)r=Math.max(r,n.id.length);return r},g=(e,r)=>` ${d(String(e.score),3)} ${l[e.coverage]} ${d(e.id,r)} ${e.visibility} ${e.kind}`,k=e=>{if(!e.comparable)return[` baseline not comparable (${e.reason}) — regenerate it; this run cannot verify anything`];if(!e.regressed)return[` no regression against baseline (score ${e.scoreDelta>=0?"+":""}${String(e.scoreDelta)})`];const r=[` REGRESSED against baseline (score ${e.scoreDelta>=0?"+":""}${String(e.scoreDelta)})`];for(const n of e.dropped)r.push(` ${n.id}: ${String(n.before)} → ${String(n.after)}`);for(const n of e.newFailing)r.push(` ${n}: now failing`);for(const n of e.worsened)r.push(` ${n}: more findings than the baseline`);return e.projectRegressed&&r.push(" project: new schema/config rules fired"),r},C=(e,r)=>{const n=e.procedures.filter(i=>i.coverage!=="clean"&&i.coverage!=="exempt"),t=f(n),o=[`advisor health ${String(e.score)}/100 — ${e.grade}`,` ${String(e.summary.clean)} clean · ${String(e.summary.warned)} warned · ${String(e.summary.failing)} failing · ${String(e.summary.exempt)} exempt (${String(e.summary.procedures)} procedures, ${String(e.summary.rulesFired)} rules fired)`];return e.project.checks.length>0&&o.push("",` project ${String(e.project.score)}/100 — ${e.project.checks.map(i=>i.name).join(", ")}`),n.length>0&&o.push("",...n.map(i=>g(i,t)),""," run with --all for every procedure, or --entry <file#export> for one"),r!==void 0&&o.push("",...k(r)),o.join(`
2
+ `)},T=e=>e.coverage==="exempt"?[` exempt: ${e.exemptReason===void 0||e.exemptReason===""?"no reason given":e.exemptReason}`]:e.checks.map(r=>{const n=r.occurrences>1?`, ×${String(r.occurrences)}`:"";return` ${r.name} (−${String(r.weight)}${n})`}),B=e=>{const r=new Map;for(const n of e){const t=r.get(n.file);t===void 0?r.set(n.file,[n]):t.push(n)}return[...r].toSorted(([n],[t])=>b(n,t))},I=e=>{const r=f(e.procedures),n=[`advisor health ${String(e.score)}/100 — ${e.grade}`,"",` legend: ${l.clean} clean ${l.warned} warned ${l.failing} failing ${l.exempt} exempt`];for(const[t,o]of B(e.procedures)){n.push("",` ${t}`);for(const i of o)n.push(g(i,r),...T(i))}return n.join(`
3
+ `)},L=(e,r)=>{const n=e.procedures.find(o=>o.id===r);if(n===void 0){const o=e.procedures.slice(0,10).map(i=>` ${i.id}`);return[`no procedure ${r} in the map. Known ids:`,...o,e.procedures.length>10?" …":""].filter(Boolean).join(`
4
+ `)}const t=[`${R(n)} — ${String(n.score)}/100, ${n.coverage}`,` weight in the global mean: ${String(n.weight)}`];if(n.checks.length===0)return t.push(""," no rule fired"),t.join(`
5
+ `);t.push(""," rules fired:");for(const o of n.checks)t.push(` [${o.level}] ${o.name} −${String(o.weight)}${o.occurrences>1?` (×${String(o.occurrences)})`:""}`);return t.join(`
6
+ `)},N=new Date(0).toISOString(),m=(e,r)=>v(r)?r:w(e,r),P=e=>{if(e===void 0)return{value:void 0};if(e===null||e==="")return{error:"--min-score needs a value between 0 and 100"};const r=Number(e);return Number.isFinite(r)&&r>=0&&r<=100?{value:r}:{error:"--min-score must be a number between 0 and 100"}},_=(e,r,n)=>{const t=m(r,n===null||n===""?u:n);if(!S(t))return{error:`baseline not found at ${t} — generate one with \`lunora advisor\` and commit it`};const o=x(JSON.parse(p(t,"utf8")));return o===void 0?{error:`baseline at ${t} is malformed — regenerate it with \`lunora advisor\``}:{comparison:j(e,o)}},z=e=>`baseline is not comparable (${e}) — regenerate it; this run verified nothing`,G=(e,r,n)=>r.entry!==void 0?L(e,r.entry):r.all===!0?I(e):C(e,n),H=e=>{const r=e.cwd??process.cwd(),n=e.format==="json",{logger:t}=e,o=P(e.minScore);if("error"in o)return e.logger.error(o.error),{code:E.USAGE,error:o.error};const{advisorContext:i,advisories:$}=A({dryRun:!0,lazyFunctions:M(),projectRoot:r,...F(r)}),a=y(i?.procedureProtections??[],$,{generatedAt:e.generatedAt??N}),s={map:a};if(e.baseline!==void 0){const c=_(a,r,e.baseline);if("error"in c)return e.logger.error(c.error),{...s,error:c.error};s.comparison=c.comparison,c.comparison.comparable||e.logger.error(z(c.comparison.reason))}if(e.write!==!1){const c=m(r,e.out??u);h(c,`${JSON.stringify(a,void 0,4)}
7
+ `,"utf8"),s.written=c}return s.belowMinScore=o.value!==void 0&&a.score<o.value,s.belowMinScore&&t.error(`advisor score ${String(a.score)} is below the required ${String(o.value)}`),n||(t.info(G(a,e,s.comparison)),s.written!==void 0&&t.success(`wrote ${s.written}`)),s},J=e=>e.error!==void 0||e.belowMinScore===!0?!0:e.comparison!==void 0&&(!e.comparison.comparable||e.comparison.regressed),Z=D(({cwd:e,format:r,logger:n,options:t})=>{const o=H({all:t.all,baseline:t.baseline,cwd:e,entry:t.entry,format:r,logger:n,minScore:t.minScore,out:t.out,write:t.write});return{code:o.code??(J(o)?1:0),data:{belowMinScore:o.belowMinScore,comparison:o.comparison,map:o.map,written:o.written},error:o.error}});export{Z as execute,H as runAdvisorCommand};
@@ -1,3 +1,3 @@
1
- import z,{join as m,resolve as U}from"node:path";import{isInteractive as J,promptYesNo as W}from"@lunora/config";import{backupObjectKey as S,isBackupManifestEntry as G,normalizeBackupPrefix as Y,BACKUP_KEY_PREFIX as X,backupManifestKey as Q,isBackupManifestKey as V,backupObjectKeyOfManifest as j}from"@lunora/runtime";import{r as A,t as Z}from"../packem_shared/admin-token-VdUnvnKW.mjs";import{r as T,a as I}from"../packem_shared/admin-url-DJB8P6Jh.mjs";import{d as ee}from"../packem_shared/command-BpMv2n0o.mjs";import{EXIT_CODE as u,isRefusal as $,exitCodeForStatus as y}from"../packem_shared/EXIT_CODE-DQVhSE-1.mjs";import{a as re}from"../packem_shared/resolve-target-C_ZloTvd.mjs";import{runExportCommand as te}from"../packem_shared/runExportCommand-_PtkJJDT.mjs";import{b as ae,l as oe,M as _,u as ne,p as ie,a as se,r as ce}from"../packem_shared/import-3Y59Ry9F.mjs";import{readAndLogBody as p}from"./runRpcCommand.mjs";import{randomUUID as q,createHash as de}from"node:crypto";import{existsSync as N,createReadStream as ue,createWriteStream as le}from"node:fs";import{mkdir as fe,rm as k,rename as M,readFile as C,writeFile as me,realpath as R,mkdtemp as x}from"node:fs/promises";import{LunoraError as b}from"@lunora/errors";import{i as O}from"../packem_shared/path-containment-CgxYZggb.mjs";import{tmpdir as P}from"node:os";import{Readable as ge}from"node:stream";import{pipeline as he}from"node:stream/promises";const E="manifest.json",L=async e=>{const t=de("sha256");let r=0;for await(const a of ue(e)){const o=a;r+=o.byteLength,t.update(o)}return{bytes:r,sha256:t.digest("hex")}},B=async e=>{const t=m(e,E);if(!N(t))return[];let r;try{r=JSON.parse(await C(t,"utf8"))}catch(a){const o=a instanceof Error?a.message:String(a);throw new b("INTERNAL",`backup: ${t} exists but is not valid JSON (${o}) — refusing to overwrite it; fix or remove it manually`,{cause:a})}if(!Array.isArray(r))throw new TypeError(`backup: ${t} exists but is not a JSON array — refusing to overwrite it; fix or remove it manually`);return r.filter(a=>G(a))},we=async(e,t)=>{const r=m(e,`${E}.${q()}.tmp`);try{await me(r,`${JSON.stringify(t,void 0,2)}
1
+ import z,{join as m,resolve as U}from"node:path";import{isInteractive as J,promptYesNo as W}from"@lunora/config";import{backupObjectKey as S,isBackupManifestEntry as G,normalizeBackupPrefix as Y,BACKUP_KEY_PREFIX as X,backupManifestKey as Q,isBackupManifestKey as V,backupObjectKeyOfManifest as j}from"@lunora/runtime";import{r as A,t as Z}from"../packem_shared/admin-token-VdUnvnKW.mjs";import{r as T,a as I}from"../packem_shared/admin-url-DJB8P6Jh.mjs";import{d as ee}from"../packem_shared/command-BpMv2n0o.mjs";import{EXIT_CODE as u,isRefusal as $,exitCodeForStatus as y}from"../packem_shared/EXIT_CODE-DQVhSE-1.mjs";import{r as re}from"../packem_shared/resolve-target-BRvw8Pf7.mjs";import{runExportCommand as te}from"../packem_shared/runExportCommand-_PtkJJDT.mjs";import{b as ae,l as oe,M as _,u as ne,p as ie,a as se,r as ce}from"../packem_shared/import-3Y59Ry9F.mjs";import{readAndLogBody as p}from"./runRpcCommand.mjs";import{randomUUID as q,createHash as de}from"node:crypto";import{existsSync as N,createReadStream as ue,createWriteStream as le}from"node:fs";import{mkdir as fe,rm as k,rename as M,readFile as C,writeFile as me,realpath as R,mkdtemp as x}from"node:fs/promises";import{LunoraError as b}from"@lunora/errors";import{i as O}from"../packem_shared/path-containment-CgxYZggb.mjs";import{tmpdir as P}from"node:os";import{Readable as ge}from"node:stream";import{pipeline as he}from"node:stream/promises";const E="manifest.json",L=async e=>{const t=de("sha256");let r=0;for await(const a of ue(e)){const o=a;r+=o.byteLength,t.update(o)}return{bytes:r,sha256:t.digest("hex")}},B=async e=>{const t=m(e,E);if(!N(t))return[];let r;try{r=JSON.parse(await C(t,"utf8"))}catch(a){const o=a instanceof Error?a.message:String(a);throw new b("INTERNAL",`backup: ${t} exists but is not valid JSON (${o}) — refusing to overwrite it; fix or remove it manually`,{cause:a})}if(!Array.isArray(r))throw new TypeError(`backup: ${t} exists but is not a JSON array — refusing to overwrite it; fix or remove it manually`);return r.filter(a=>G(a))},we=async(e,t)=>{const r=m(e,`${E}.${q()}.tmp`);try{await me(r,`${JSON.stringify(t,void 0,2)}
2
2
  `,"utf8"),await M(r,m(e,E))}catch(a){throw await k(r,{force:!0}),a}},be=async(e,t)=>{const r=U(e),a=U(r,t.file),o=()=>{throw new b("INTERNAL",`backup: entry ${t.id} points outside ${r} (${t.file}) — refusing to read it`)};if(O(r,a)||o(),!!N(a))return O(await R(r),await R(a))||o(),a},ke=e=>{const t=r=>S("",r);return{commit:async(r,a)=>{await M(a,m(e,r))},label:e,list:async()=>B(e),locate:t,materialize:async(r,a)=>{const o=i=>i===void 0?void 0:{path:i,release:()=>Promise.resolve()};return o(r===void 0?N(a)?a:void 0:await be(e,r))},record:async r=>{const a=await B(e);a.push(r),await we(e,a)},stage:async r=>{await fe(e,{recursive:!0});const a=m(e,`${t(r)}.${q()}.partial`);return{path:a,release:()=>k(a,{force:!0})}}}},ye="/_lunora/admin/storage/object",pe="/_lunora/admin/storage",D=8,$e=(e,t=z)=>{const r=t.basename(e);return r===""||r==="."||r===".."?"snapshot.ndjson":r},ve=e=>{const{context:t,logger:r}=e,a=Y(e.prefix??X),o=`bucket ${t.bucket??"(default)"} under ${a}`,i=ae(t),n=c=>`${t.baseUrl}${ye}?key=${encodeURIComponent(c)}${i}`,f=async c=>{const s=await t.fetchImpl(n(c),{headers:{authorization:`Bearer ${t.token}`},method:"GET"});if(!s.ok){const l=await s.text().catch(()=>"<no body>");throw new b("INTERNAL",`backup: could not read ${c} (HTTP ${String(s.status)}): ${l}`)}return s.text()},g=async c=>{try{const s=JSON.parse(await f(c));if(G(s))return s;r.warn(`backup: ignoring ${c} — not a backup manifest`)}catch(s){r.warn(`backup: ignoring ${c} — ${s instanceof Error?s.message:String(s)}`)}};return{commit:async(c,s,l)=>{if(l.bytes>_)throw new b("INTERNAL",`backup: the snapshot is ${String(l.bytes)} bytes, above the ${String(_)}-byte limit for a checksum-verified upload — nothing was written. Narrow it with \`--tables\`, or write it to a directory with \`--dir\` and move the file yourself (\`wrangler r2 object put\`).`);const d=await C(s);await ne(t,c,d,{contentType:"application/x-ndjson",id:c,sha256:l.sha256,size:l.bytes},r)},label:o,list:async()=>{const s=(await oe(t,a)).map(d=>d.key).filter(d=>V(d)),l=[];for(let d=0;d<s.length;d+=D){const h=await Promise.all(s.slice(d,d+D).map(async w=>g(w)));l.push(...h.filter(w=>w!==void 0))}return l.toSorted((d,h)=>d.id.localeCompare(h.id))},locate:c=>S(a,c),materialize:async(c,s)=>{const l=c?.file??s,d=await t.fetchImpl(n(l),{headers:{authorization:`Bearer ${t.token}`},method:"GET"});if(d.status===404)return;if(!d.ok||d.body===null){const v=await d.text().catch(()=>"<no body>");throw new b("INTERNAL",`backup: could not download ${l} (HTTP ${String(d.status)}): ${v}`)}const h=await x(m(P(),"lunora-backup-")),w=m(h,$e(l));try{await he(ge.from(d.body),le(w))}catch(v){throw await k(h,{force:!0,recursive:!0}),v}return{path:w,release:async()=>k(h,{force:!0,recursive:!0})}},record:async c=>{const s=Q(c.file),l=await t.fetchImpl(`${t.baseUrl}${pe}?key=${encodeURIComponent(s)}${i}`,{body:`${JSON.stringify(c,void 0,2)}
3
3
  `,headers:{authorization:`Bearer ${t.token}`,"content-type":"application/json"},method:"PUT"});if(!l.ok){const d=await l.text().catch(()=>"<no body>");throw new b("INTERNAL",`backup: snapshot ${c.file} was written but its manifest was not (HTTP ${String(l.status)}): ${d}`)}},stage:async c=>{const s=await x(m(P(),"lunora-backup-"));return{path:m(s,S("",c)),release:async()=>k(s,{force:!0,recursive:!0})}}}},Se=".lunora-backups",F="/_lunora/admin/backup/retention",Ee="/_lunora/admin/backup/prune",Ae="/_lunora/admin/pitr",Te="__lunora_admin__:getPitrBookmark",Ie="__lunora_admin__:pitrRestore",Ne=async(e,t)=>{const r=(e.now??(()=>new Date))().toISOString(),a=await t.stage(r);try{const o=await te({cwd:e.cwd,fetchImpl:e.fetchImpl,logger:e.logger,out:a.path,prod:e.prod,tables:e.tables,token:e.token,url:e.url});if(o.code!==0)return{code:o.code};const i=t.locate(r),n=await L(a.path);await t.commit(i,a.path,n);const f={bytes:n.bytes,createdAt:r,file:i,id:r,rows:o.rows,sha256:n.sha256,tables:e.tables};return await t.record(f),e.logger.success(`backup created: ${i} (${o.rows.toString()} rows, ${n.bytes.toString()} bytes)`),{code:0,entry:f}}finally{await a.release().catch(o=>{e.logger.warn(`backup: could not clean up staging (${o instanceof Error?o.message:String(o)})`)})}},Ue=async(e,t,r)=>{if(t?.sha256===void 0)return r.error(t===void 0?"--verify needs a recorded checksum, and a snapshot restored by path/key has no manifest entry — restore it by its backup id instead":`--verify: backup ${t.id} carries no recorded checksum — it was taken by a release before checksums existed`),{code:u.USAGE};const a=await L(e);if(a.sha256!==t.sha256)return r.error(`--verify: ${t.file} does not match its recorded checksum (expected ${t.sha256}, got ${a.sha256}) — nothing was imported`),{code:1};r.success(`verified ${t.file} against its recorded sha256`)},_e=async(e,t)=>{const r=await t.list();if(r.length===0)return e.logger.info(`no backups found in ${t.label}`),{code:0,entries:[]};for(const a of r)e.logger.info(`${a.id} ${a.rows.toString()} rows ${a.bytes.toString()} bytes ${a.file}`);return{code:0,entries:r}},Re=async(e,t)=>{const{target:r}=e;if(r===void 0||r.length===0)return e.logger.error("restore requires a backup id or file path. Usage: lunora backup restore <id|file>"),{code:u.USAGE};const o=(await t.list()).find(n=>n.id===r),i=await t.materialize(o,r);if(i===void 0)return e.logger.error(`backup not found: ${r}`),{code:u.NOT_FOUND};try{if(e.verify===!0){const f=await Ue(i.path,o,e.logger);if(f!==void 0)return f}const n=await ce({cwd:e.cwd,fetchImpl:e.fetchImpl,file:i.path,format:e.format,logger:e.logger,dryRun:e.dryRun,mode:e.mode??"require-empty",prod:e.prod,token:e.token,url:e.url,yes:e.yes});return{code:n.code,entry:o,...n.body?.plan===void 0?{}:{plan:n.body.plan},restored:n.inserted+(n.replaced??0)}}finally{await i.release().catch(n=>{e.logger.warn(`backup: could not clean up the downloaded snapshot (${n instanceof Error?n.message:String(n)})`)})}},xe=e=>{if(e.prod&&e.url===void 0)return e.logger.error("--prod requires an explicit --url (refusing to target the implicit localhost worker)"),{refused:u.USAGE};if(e.restore===!0&&e.at===void 0&&e.bookmark===void 0)return e.logger.error("pitr --restore requires --at <time> or --bookmark <bookmark>"),{refused:u.USAGE};const t=T(e.url,e.logger,e.cwd);if(t===void 0)return{refused:u.USAGE};if(e.restore===!0&&Z({prod:e.prod,url:t})&&e.yes!==!0)return e.logger.error(`pitr --restore restores data in place at ${t}, which is not local. Re-run with --yes to confirm.`),{refused:u.USAGE};const{token:r}=A({cwd:e.cwd??process.cwd(),token:e.token,url:t});return r?{fetchImpl:e.adminFetch??I,requestUrl:`${t}${Ae}`,token:r}:(e.logger.error("admin token required — pass --token, set LUNORA_ADMIN_TOKEN, or add it to .dev.vars (local targets only)"),{refused:u.AUTH})},Oe=(e,t)=>{const r={};return e.at!==void 0&&(r.time=e.at),t&&e.bookmark!==void 0&&(r.bookmark=e.bookmark),t&&e.restart===!0&&(r.restart=!0),r},Pe=async e=>{const t=xe(e);if($(t))return{code:t.refused};const r=e.restore===!0,a=r?Ie:Te,o=Oe(e,r),i=r?"restore":"bookmark";e.logger.info(`POST ${t.requestUrl} -> pitr ${i}${e.shard===void 0?"":` (shard "${e.shard}")`}`);const n=await t.fetchImpl(t.requestUrl,{body:JSON.stringify({args:o,functionPath:a,shardKey:e.shard}),headers:{authorization:`Bearer ${t.token}`,"content-type":"application/json"},method:"POST"});return{body:await p(n,e.logger),code:n.ok?0:y(n.status)}},Be=(e,t)=>{if(e.bucket===void 0)return e.prefix!==void 0?(e.logger.error("--prefix applies only to an R2 destination — pass --bucket alongside it, or use --dir to point at a local directory."),{refused:u.USAGE}):ke(m(t,e.dir??Se));if(e.dir!==void 0)return e.logger.error("--dir applies only to a local destination and does not apply alongside --bucket — drop one."),{refused:u.USAGE};if(e.prod===!0&&e.url===void 0)return e.logger.error("--prod requires an explicit --url (refusing to target the implicit localhost worker)"),{refused:u.USAGE};const r=T(e.url,e.logger,e.cwd);if(r===void 0)return{refused:u.USAGE};const{token:a}=A({cwd:t,token:e.token,url:r});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:u.AUTH};const o=e.fetchImpl??I;return ve({context:{baseUrl:r,bucket:e.bucket==="default"?void 0:e.bucket,fetchImpl:o,token:a},logger:e.logger,prefix:e.prefix})},K=e=>{const t=[e.bucket===void 0?void 0:"--bucket",e.prefix===void 0?void 0:"--prefix",e.dir===void 0?void 0:"--dir"].filter(i=>i!==void 0);if(t.length>0)return e.logger.error(`${t.join(" / ")} ${t.length===1?"does":"do"} not apply here — retention and prune act on the store the worker itself is configured with (\`backupStore\`), not a destination you name.`),{refused:u.USAGE};if(e.prod===!0&&e.url===void 0)return e.logger.error("--prod requires an explicit --url (refusing to target the implicit localhost worker)"),{refused:u.USAGE};const r=T(e.url,e.logger,e.cwd);if(r===void 0)return{refused:u.USAGE};const{token:a}=A({cwd:e.cwd??process.cwd(),token:e.token,url:r});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:u.AUTH};const o=e.adminFetch??I;return{baseUrl:r,fetchImpl:o,token:a}},H=(e,t)=>{e.logger.info(`backup retention would keep the newest ${t.keep.toString()} of ${t.eligible.toString()} under ${t.prefix} and delete ${t.wouldDelete.length.toString()}`);for(const r of t.wouldDelete)e.logger.info(` ${j(r)}`)},De=async e=>{const t=K(e);if($(t))return{code:t.refused};const r=await t.fetchImpl(`${t.baseUrl}${F}`,{headers:{authorization:`Bearer ${t.token}`},method:"GET"});if(!r.ok)return await p(r,e.logger),{code:y(r.status)};const a=await r.json();return a.cron===void 0?(e.logger.info("no scheduled backup is configured (`backupCron`), so retention would delete nothing"),{code:0,preview:a}):a.keep<=0?(e.logger.info(`retention is off for cron "${a.cron}" (\`backupRetain\` unset) — every snapshot under ${a.prefix} is kept`),{code:0,preview:a}):(H(e,a),a.eligible===0&&e.logger.info(`no snapshot under ${a.prefix} was written by this cron, so retention has nothing of its own to remove`),{code:0,preview:a})},Ge=(e,t)=>{e.logger.success(`deleted ${t.deleted.length.toString()} backup(s)`);for(const r of t.deleted)e.logger.info(` ${j(r)}`);t.failed.length>0&&e.logger.warn(`${t.failed.length.toString()} could not be removed — the snapshot may be gone with its manifest left behind; run again to retry`),t.ignored>0&&e.logger.info(`${t.ignored.toString()} were already gone by the time the delete ran`),t.remaining>0&&e.logger.info(`${t.remaining.toString()} more are past the window — run \`lunora backup prune\` again`)},je=async e=>{const t=K(e);if($(t))return{code:t.refused};const r=await t.fetchImpl(`${t.baseUrl}${F}`,{headers:{authorization:`Bearer ${t.token}`},method:"GET"});if(!r.ok)return await p(r,e.logger),{code:y(r.status)};const a=await r.json();if(a.keep<=0||a.cron===void 0)return e.logger.error("backup prune needs a retention window: set `backupRetain` (and `backupCron`) on the worker. Without one there is nothing past the window to remove."),{code:u.USAGE,preview:a};if(a.wouldDelete.length===0)return e.logger.info(`nothing to prune — ${a.eligible.toString()} snapshot(s) under ${a.prefix} and retention keeps the newest ${a.keep.toString()}`),{code:0,preview:a};if(H(e,a),e.yes!==!0){const n=e.confirm??(J()?W:void 0);if(n===void 0)return e.logger.error("refusing to delete backups without confirmation — re-run with --yes (there is no undo for an object-store delete)"),{code:u.USAGE,preview:a};if(!await n(`delete ${a.wouldDelete.length.toString()} backup(s)? This cannot be undone. [y/N] `))return e.logger.info("nothing was deleted"),{code:0,preview:a}}const o=await t.fetchImpl(`${t.baseUrl}${Ee}`,{body:JSON.stringify({confirm:a.wouldDelete}),headers:{authorization:`Bearer ${t.token}`,"content-type":"application/json"},method:"POST"});if(!o.ok)return await p(o,e.logger),{code:y(o.status),preview:a};const i=await o.json();return Ge(e,i),{code:i.failed.length>0?1:0,deleted:i.deleted,preview:a}},qe=async(e,t)=>{try{if(e.subcommand==="pitr")return await Pe(e);if(e.subcommand==="retention")return await De(e);if(e.subcommand==="prune")return await je(e);const r=Be(e,t);return $(r)?{code:r.refused}:e.subcommand==="create"?await Ne(e,r):e.subcommand==="list"?await _e(e,r):await Re(e,r)}catch(r){return e.logger.error(r instanceof Error?r.message:String(r)),{code:1}}},Me=async e=>{const t=e.cwd??process.cwd(),r=await qe(e,t);return{...r,data:{subcommand:e.subcommand,...r.entries===void 0?{}:{entries:r.entries},...r.entry===void 0?{}:{entry:r.entry},...r.plan===void 0?{}:{plan:r.plan},...r.restored===void 0?{}:{restored:r.restored},...r.preview===void 0?{}:{preview:r.preview},...r.deleted===void 0?{}:{deleted:r.deleted},...r.body===void 0?{}:{result:r.body}}}},Ce=e=>e==="create"||e==="list"||e==="pitr"||e==="prune"||e==="restore"||e==="retention",sr=ee(async({argument:e,cwd:t,format:r,logger:a,options:o})=>{const i=e[0];if(!Ce(i)){const g=`backup: unknown subcommand "${i??""}" — expected create | list | restore | retention | prune | pitr`;return a.error(g),{code:u.USAGE,error:g}}const n=ie(o.mode,"require-empty");if(n===void 0){const g=`--mode ${String(o.mode)} is not a known mode. Expected one of: ${se.join(", ")}.`;return a.error(g),{code:u.USAGE,error:g}}const f=await Me({at:o.at,bookmark:o.bookmark,bucket:o.bucket,cwd:t,dir:o.dir,format:r,logger:a,prefix:o.prefix,dryRun:o.dryRun===!0,mode:n,prod:o.prod===!0,restart:o.restart===!0,restore:o.restore===!0,shard:o.shard,subcommand:i,tables:o.tables,target:e[1],token:o.token,url:re({cwd:t,prod:o.prod===!0,url:o.url}),verify:o.verify===!0,yes:o.yes===!0});return{code:f.code,data:f.data,error:f.error}});export{sr as execute,Me as runBackupCommand};
@@ -1 +1,2 @@
1
- import{readdirSync as E,lstatSync as S,existsSync as y}from"node:fs";import{join as m,basename as $}from"node:path";import{d as w}from"../packem_shared/command-BpMv2n0o.mjs";import{EXIT_CODE as h}from"../packem_shared/EXIT_CODE-DQVhSE-1.mjs";const l=".eval.ts",F=new Set([".git","_generated","dist","node_modules"]),p=(e,s=[])=>{let r;try{r=E(e)}catch{return s}for(const t of r){const n=m(e,t);let o;try{o=S(n)}catch{continue}if(o.isDirectory()){if(F.has(t))continue;p(n,s)}else o.isFile()&&t.endsWith(l)&&s.push(n)}return s.toSorted((t,n)=>t.localeCompare(n))},b=e=>typeof e=="object"&&e!==null&&typeof e.run=="function",g=e=>typeof e=="number"&&Number.isFinite(e)&&e>=0&&e<=1,R="evals";let u;const T=async()=>(u??=import("jiti").then(({createJiti:e})=>e(import.meta.url,{interopDefault:!1})),u),f=e=>$(e,l),x=e=>({average:e.result?.average,error:e.error,items:e.result?.items,name:e.name,passed:e.passed,path:e.path,threshold:e.threshold}),A=e=>({evals:e.evals.map(s=>x(s))}),v=(e,s,r)=>(e.error(s),{code:r,error:s,evals:[]}),L=(e,s,r)=>{if(!y(e))return r.info(`eval: no "${s}" directory — nothing to run`),[];const t=p(e);return t.length===0&&r.info(`eval: no ${l} files found under "${s}"`),t},I=async e=>{const t=(await(await T()).import(e)).default;if(!b(t))throw new Error("does not default-export an eval module (expected `{ run(): Promise<EvalResult>, name?, threshold? }`)");return t},O=async(e,s)=>{let r;try{r=await I(e)}catch(o){return{error:o instanceof Error?o.message:String(o),name:f(e),passed:!1,path:e}}const t=r.name??f(e);if(r.threshold!==void 0&&!g(r.threshold))return{error:`invalid \`threshold\` export: must be a number in [0, 1] — received ${JSON.stringify(r.threshold)}`,name:t,passed:!1,path:e};const n=r.threshold??s;try{const o=await r.run(),a=n===void 0||o.average>=n;return{name:t,passed:a,path:e,result:o,threshold:n}}catch(o){return{error:o instanceof Error?o.message:String(o),name:t,passed:!1,path:e,threshold:n}}},_=async(e,s)=>{const r=[];for(const t of e)r.push(await O(t,s));return r},D=e=>{if(e.error!==void 0)return`ERROR: ${e.error}`;const s=e.passed?"pass":"FAIL",r=e.result?.items.length??0;return`${s} (${String(r)} cases)`},C=e=>{if(e.length===0)return[];const s=Math.max(4,...e.map(t=>t.name.length)),r=[`${"NAME".padEnd(s)} SCORE THRESHOLD STATUS`];for(const t of e){const n=t.result===void 0?"—":t.result.average.toFixed(3),o=t.threshold===void 0?"—":t.threshold.toFixed(3);r.push(`${t.name.padEnd(s)} ${n.padEnd(6)} ${o.padEnd(9)} ${D(t)}`)}return r},M=async e=>{const s=e.cwd??process.cwd(),{logger:r}=e;if(e.threshold!==void 0&&!g(e.threshold))return v(r,`eval: --threshold must be a number in [0, 1] — received "${String(e.threshold)}"`,h.USAGE);const t=e.dir??R,n=m(s,t),o=L(n,t,r),a=await _(o,e.threshold);if(e.threshold!==void 0&&a.length===0){const d=`eval: --threshold ${String(e.threshold)} was set but 0 eval files were discovered under "${t}" — nothing was gated`;return v(r,d,h.USAGE)}for(const d of C(a))r.info(d);const i=a.filter(d=>!d.passed),c=i.length>0?1:0;return c===0?r.success(`eval: ${String(a.length)} eval(s) passed`):r.error(`eval: ${String(i.length)}/${String(a.length)} eval(s) failed`),{code:c,evals:a}},N=w(async({cwd:e,format:s,logger:r,options:t})=>{const n=await M({cwd:e,dir:t.dir,format:s,logger:r,threshold:t.threshold});return{code:n.code,data:A(n),error:n.error}});export{N as execute,M as runEvalCommand};
1
+ import{randomUUID as L}from"node:crypto";import{readdirSync as $,readFileSync as g,existsSync as d,rmSync as y,mkdtempSync as U,mkdirSync as k,copyFileSync as M,writeFileSync as R,lstatSync as C}from"node:fs";import{tmpdir as E}from"node:os";import{join as l,resolve as w,dirname as h}from"node:path";import{resolveSchemaDirectory as P,resolveLazyFunctions as A,DEFAULT_BUNDLE_META_PATH as z}from"@lunora/config";import{p as I}from"../packem_shared/api-spec-lYyq4vL6.mjs";import{d as j}from"../packem_shared/command-BpMv2n0o.mjs";import{p as x,v as N,r as O}from"./handler.mjs";import{defaultSpawner as F}from"../packem_shared/createRecordingSpawner-DchLi0gR.mjs";import{gzipSync as W}from"node:zlib";import{toolchainExecArgs as H,detectPackageManager as K}from"@lunora/config/cli";const v=64*1024*1024,Y=e=>!e.endsWith(".map")&&e!=="bundle-meta.json"&&e!=="README.md",Z=e=>{let r;try{r=$(e,{recursive:!0,withFileTypes:!0})}catch{return}let n=0,t=0,i=0;for(const o of r){if(!o.isFile()||!Y(o.name))continue;const c=g(l(o.parentPath,o.name));n+=1,i+=c.byteLength,t+=W(c).byteLength}return n===0?void 0:{files:n,gzipBytes:t,rawBytes:i}},J=1e3,b=200,D=3,V="https://lunora.sh/docs/startup-cpu-limit",q=/Active:\s+([\d.]+) ms/u,G=12e4,Q=e=>{const r=l(e,P(e),"_generated","functions.ts");return d(r)&&g(r,"utf8").includes("export const loadAllFunctions")},X=async({bundleFile:e,cwd:r,spawner:n})=>{const t=[],i=l(h(e),"startup.cpuprofile"),{args:o,command:c}=H(K(r),{args:["check","startup","--worker",e,"--outfile",i],tool:"wrangler"});for(let a=0;a<D;a+=1){let u;try{const s=await n({args:o,captureStderrSilently:!0,captureStdoutSilently:!0,command:c,cwd:r,timeoutMs:G});if(s.code!==0)return{skip:`\`wrangler check startup\` exited with code ${String(s.code)}`};u=s.stdout}catch(s){return{skip:`\`wrangler check startup\` could not run (${s instanceof Error?s.message:String(s)})`}}const f=q.exec(u??"");if(f?.[1]===void 0)return{skip:"`wrangler check startup` printed no Active time"};t.push(Number(f[1]))}return t.sort((a,u)=>a-u),{median:t[Math.floor(t.length/2)]}},ee=async e=>{const{bundleFile:r,cwd:n,logger:t}=e;if(!d(w(n,r))){t.debug?.(`startup check skipped: no upload form at ${r}`);return}const i=await X(e);if("skip"in i){t.debug?.(`startup check skipped: ${i.skip}`);return}const{median:o}=i,c=`startup: ${o.toFixed(0)} ms local CPU (median of ${String(D)} \`wrangler check startup\` runs)`;if(o<=b)return t.info(c),o;const a=Q(n)?"":" This build is eager: `--lazy-functions` (or LUNORA_LAZY_FUNCTIONS=1) defers function modules to first use.";return t.warn(`${c} is above the ${String(b)} ms warning line. Cloudflare rejects a Worker whose startup exceeds ${String(J)} ms of ITS CPU (error 10021), and how local CPU maps to Cloudflare's is unknown: a 0.4–0.7 s local profile has been rejected, so the line is a heuristic, not a prediction.${a} See ${V}`),o},_=".lunora/build",te=async e=>F({...e,stdoutToStderr:!0}),B=e=>`${(e/1024).toFixed(1)} KiB`,T=e=>d(e)?$(e,{encoding:"utf8",recursive:!0}).map(r=>{try{const n=C(l(e,r));return`${r}:${String(n.size)}:${String(n.mtimeMs)}`}catch{return`${r}:gone`}}).join(`
2
+ `):"",re=(e,r,n)=>{const t=Z(e);if(t===void 0)return n.warn(`could not weigh the bundle — nothing uploadable was found in ${r}`),t;const i=(t.rawBytes/v*100).toFixed(1),o=`bundle: ${B(t.rawBytes)} raw across ${String(t.files)} file(s) — ${i}% of Cloudflare's 64 MiB uncompressed Worker size limit (${B(t.gzipBytes)} gzipped, for reference; there is no compressed limit)`;return t.rawBytes>v?n.warn(`${o} — Cloudflare will reject this upload`):n.info(o),t},S=e=>{if(e!==void 0)try{y(h(e),{force:!0,recursive:!0})}catch{}},oe=async(e,r,n,t)=>{if(r!==void 0)try{await ee({bundleFile:r,cwd:e,logger:n,spawner:t})}finally{S(r)}},ne=async e=>{const r=e.outDir??_,n=e.format==="json",{logger:t}=e,i=e.cwd??process.cwd(),o=w(i,r,"bundle-meta.json"),c=N(i,L()),a=d(o)?g(o):void 0;y(o,{force:!0}),y(c,{force:!0});const u=w(i,r),f=T(u);let s;const m=e.startupCheck===!1?void 0:l(U(l(E(),"lunora-startup-")),"worker.bundle");try{s=await O({allowSchemaDrift:e.allowSchemaDrift,apiSpec:e.apiSpec,bundleMetaPath:c,frameworkBuild:e.frameworkBuild,lazyFunctions:A(e.lazyFunctions),commandName:"build",clientBuildOutputJson:n,cwd:e.cwd,dryRun:!0,emitBindings:e.emitBindings,format:void 0,interactive:n?!1:void 0,logger:t,outDir:r,outFile:m,smokeLauncher:e.smokeLauncher,smokeLoad:e.smokeLoad,spawner:e.spawner??(n?te:void 0),strictAdvisories:e.strictAdvisories,target:e.target});const p=e.frameworkBuild===!1?l(i,z):c;s.code===0&&!d(o)&&d(p)&&(k(h(o),{recursive:!0}),M(p,o))}finally{s===void 0&&S(m),y(c,{force:!0}),a!==void 0&&!d(o)&&T(u)===f&&s?.code!==0&&(k(h(o),{recursive:!0}),R(o,a))}if(s.code!==0)return S(m),s;try{t.success(`build complete — bundle written to ${r}`);const p=re(w(e.cwd??process.cwd(),r),r,t);return await oe(i,m,t,e.spawner??F),{...s,bundle:p}}finally{S(m)}},we=j(async({cwd:e,format:r,logger:n,options:t})=>{const i=x(e,t.root),o=await ne({allowSchemaDrift:t.allowSchemaDrift===!0,apiSpec:I(t.apiSpec),lazyFunctions:A(t.lazyFunctions),cwd:i,emitBindings:t.emitBindings,format:r,frameworkBuild:t.frameworkBuild,logger:n,outDir:t.outDir,smokeLoad:t.smokeLoad,startupCheck:t.startupCheck,strictAdvisories:t.strictAdvisories,target:t.target});return{code:o.code,data:{bundle:o.bundle,deployment:o.deployment,outDir:t.outDir??_,validation:o.validation},error:o.error}});export{we as execute,ne as runBuildCommand};
@@ -1,2 +1 @@
1
- import{readLinkedProject as h,resolveDeployDriver as b}from"@lunora/config";import{d as y}from"../packem_shared/command-BpMv2n0o.mjs";import{r as w}from"../packem_shared/deploy-target-resolve-BTCW1Iv4.mjs";import{EXIT_CODE as m}from"../packem_shared/EXIT_CODE-DQVhSE-1.mjs";import{defaultSpawner as E}from"../packem_shared/createRecordingSpawner-01ZngIwK.mjs";import{createR2Sql as L}from"@lunora/bindings/r2sql";import{createPipelineLogReader as S}from"@lunora/runtime";import{toolchainExecArgs as _,detectPackageManager as T}from"@lunora/config/cli";const p=["trace","debug","log","info","warn","error","fatal"],C=new Set(p),v=/^\d+$/,f=(e,r)=>{if(e===void 0)return;const a=e.trim();if(v.test(a))return Number(a);const n=Date.parse(a);if(Number.isNaN(n))throw new TypeError(`logs: invalid ${r} "${e}" — pass epoch-millis or an ISO 8601 date`);return n},I=e=>Buffer.from(JSON.stringify(e),"utf8").toString("base64url"),$=e=>{const r=e.trim();if(v.test(r))return{ts:Number(r)};let a;try{a=JSON.parse(Buffer.from(r,"base64url").toString("utf8"))}catch{throw new TypeError(`logs: invalid --cursor "${e}" — expected the token printed by the previous page (or a bare epoch-millis ts)`)}if(typeof a!="object"||a===null||typeof a.ts!="number"||!Number.isFinite(a.ts))throw new TypeError(`logs: invalid --cursor "${e}" — expected the token printed by the previous page (or a bare epoch-millis ts)`);const{ts:n}=a,t=a.seen,s=Array.isArray(t)?t.filter(o=>typeof o=="string"):void 0;return s!==void 0&&s.length>0?{seen:s,ts:n}:{ts:n}},g=(e,r)=>{if(e!==void 0){if(!C.has(e))throw new TypeError(`logs: invalid ${r} "${e}" — expected one of trace, debug, log, info, warn, error, fatal`);return e}},R=e=>{const r={},a=f(e.since,"--since");a!==void 0&&(r.sinceTs=a);const n=f(e.until,"--until");n!==void 0&&(r.untilTs=n);const t=g(e.level,"--level");t!==void 0&&(r.level=t);const s=g(e.minLevel,"--min-level");if(s!==void 0&&(r.minLevel=s),e.functionPrefix!==void 0&&(r.functionPathPrefix=e.functionPrefix),e.traceId!==void 0&&(r.traceId=e.traceId),e.shardKey!==void 0&&(r.shardKey=e.shardKey),e.userId!==void 0&&(r.userId=e.userId),e.limit!==void 0){const o=Number(e.limit);if(!Number.isFinite(o))throw new TypeError(`logs: invalid --limit "${e.limit}" — expected a number`);r.limit=o}return e.cursor!==void 0&&(r.cursor=$(e.cursor)),r},N=e=>{const r=new Date(e.ts).toISOString(),a=e.level.toUpperCase().padEnd(5);return`${r} ${a} ${e.functionPath} ${e.message}`},O=async e=>{const r=e.environment??process.env,a=r.R2_SQL_ACCOUNT_ID,n=r.R2_SQL_TOKEN,t=r.R2_SQL_BUCKET,s=[];if((a===void 0||a.length===0)&&s.push("R2_SQL_ACCOUNT_ID"),(n===void 0||n.length===0)&&s.push("R2_SQL_TOKEN"),(t===void 0||t.length===0)&&s.push("R2_SQL_BUCKET"),(e.table===void 0||e.table.length===0)&&s.push("--table"),s.length>0)return e.logger.error(`logs --durable: R2 SQL not configured (missing ${s.join(", ")}). The Pipeline must write to an R2 Data Catalog (Iceberg) table, and you must supply R2_SQL_ACCOUNT_ID / R2_SQL_TOKEN / R2_SQL_BUCKET plus --table — see the observability docs.`),{code:m.USAGE,error:"not configured"};let o;try{o=R(e)}catch(i){return e.logger.error(i instanceof Error?i.message:String(i)),{code:m.USAGE,error:"invalid option"}}const u=L({accountId:a,apiToken:n,bucket:t,fetch:e.fetch}),c=await S(u,{namespace:e.namespace,table:e.table}).query(o);for(const i of c.rows)process.stdout.write(`${e.ndjson===!0?JSON.stringify(i):N(i)}
2
- `);return c.rows.length===0?e.logger.info("logs --durable: no matching log records"):c.nextCursor!==void 0&&e.logger.info(`logs --durable: more rows available — pass --cursor ${I(c.nextCursor)} for the next page`),{code:0,rows:c.rows}},x=async e=>{const r=e.cwd??process.cwd(),a=e.env??h(r)?.env,n=w(r,e.target);if(n.target===void 0){const d=`logs: ${n.error??"no deploy target"}`;return e.logger.error(d),{code:m.USAGE,descriptor:void 0,error:d}}const t=b(n.target),s=t.toolchain?.tail;if(s===void 0){const d=`logs: deploy target "${t.id}" has no log tail`;return e.logger.error(d),{code:m.USAGE,descriptor:void 0,error:d}}const o=s({environment:a,format:e.format,search:e.search,status:e.status,temporary:e.temporary,worker:e.worker}),u=_(T(r),o),l={args:u.args,command:u.command,cwd:r};return e.logger.info(`tailing logs via ${l.command} ${l.args.join(" ")}`),{code:(await(e.spawner??E)(l)).code,delegated:!0,descriptor:l}},G=y(async({argument:e,cwd:r,format:a,logger:n,options:t})=>{if(t.durable===!0){const s=await O({cursor:t.cursor,functionPrefix:t.functionPrefix,level:t.level,limit:t.limit,logger:n,minLevel:t.minLevel,namespace:t.namespace,ndjson:t.ndjson===!0,shardKey:t.shardKey,since:t.since,table:t.table,traceId:t.traceId,until:t.until,userId:t.userId});return{...s,...s.rows===void 0?{}:{delegated:!0}}}return x({cwd:r,env:t.env,format:a,logger:n,search:t.search,status:t.status,target:t.target,temporary:t.temporary===!0,worker:e[0]})});export{G as execute,x as runLogsCommand};
1
+ import{readdirSync as E,lstatSync as S,existsSync as y}from"node:fs";import{join as m,basename as $}from"node:path";import{d as w}from"../packem_shared/command-BpMv2n0o.mjs";import{EXIT_CODE as h}from"../packem_shared/EXIT_CODE-DQVhSE-1.mjs";const l=".eval.ts",F=new Set([".git","_generated","dist","node_modules"]),p=(e,s=[])=>{let r;try{r=E(e)}catch{return s}for(const t of r){const n=m(e,t);let o;try{o=S(n)}catch{continue}if(o.isDirectory()){if(F.has(t))continue;p(n,s)}else o.isFile()&&t.endsWith(l)&&s.push(n)}return s.toSorted((t,n)=>t.localeCompare(n))},b=e=>typeof e=="object"&&e!==null&&typeof e.run=="function",g=e=>typeof e=="number"&&Number.isFinite(e)&&e>=0&&e<=1,R="evals";let u;const T=async()=>(u??=import("jiti").then(({createJiti:e})=>e(import.meta.url,{interopDefault:!1})),u),f=e=>$(e,l),x=e=>({average:e.result?.average,error:e.error,items:e.result?.items,name:e.name,passed:e.passed,path:e.path,threshold:e.threshold}),A=e=>({evals:e.evals.map(s=>x(s))}),v=(e,s,r)=>(e.error(s),{code:r,error:s,evals:[]}),L=(e,s,r)=>{if(!y(e))return r.info(`eval: no "${s}" directory — nothing to run`),[];const t=p(e);return t.length===0&&r.info(`eval: no ${l} files found under "${s}"`),t},I=async e=>{const t=(await(await T()).import(e)).default;if(!b(t))throw new Error("does not default-export an eval module (expected `{ run(): Promise<EvalResult>, name?, threshold? }`)");return t},O=async(e,s)=>{let r;try{r=await I(e)}catch(o){return{error:o instanceof Error?o.message:String(o),name:f(e),passed:!1,path:e}}const t=r.name??f(e);if(r.threshold!==void 0&&!g(r.threshold))return{error:`invalid \`threshold\` export: must be a number in [0, 1] — received ${JSON.stringify(r.threshold)}`,name:t,passed:!1,path:e};const n=r.threshold??s;try{const o=await r.run(),a=n===void 0||o.average>=n;return{name:t,passed:a,path:e,result:o,threshold:n}}catch(o){return{error:o instanceof Error?o.message:String(o),name:t,passed:!1,path:e,threshold:n}}},_=async(e,s)=>{const r=[];for(const t of e)r.push(await O(t,s));return r},D=e=>{if(e.error!==void 0)return`ERROR: ${e.error}`;const s=e.passed?"pass":"FAIL",r=e.result?.items.length??0;return`${s} (${String(r)} cases)`},C=e=>{if(e.length===0)return[];const s=Math.max(4,...e.map(t=>t.name.length)),r=[`${"NAME".padEnd(s)} SCORE THRESHOLD STATUS`];for(const t of e){const n=t.result===void 0?"—":t.result.average.toFixed(3),o=t.threshold===void 0?"—":t.threshold.toFixed(3);r.push(`${t.name.padEnd(s)} ${n.padEnd(6)} ${o.padEnd(9)} ${D(t)}`)}return r},M=async e=>{const s=e.cwd??process.cwd(),{logger:r}=e;if(e.threshold!==void 0&&!g(e.threshold))return v(r,`eval: --threshold must be a number in [0, 1] — received "${String(e.threshold)}"`,h.USAGE);const t=e.dir??R,n=m(s,t),o=L(n,t,r),a=await _(o,e.threshold);if(e.threshold!==void 0&&a.length===0){const d=`eval: --threshold ${String(e.threshold)} was set but 0 eval files were discovered under "${t}" — nothing was gated`;return v(r,d,h.USAGE)}for(const d of C(a))r.info(d);const i=a.filter(d=>!d.passed),c=i.length>0?1:0;return c===0?r.success(`eval: ${String(a.length)} eval(s) passed`):r.error(`eval: ${String(i.length)}/${String(a.length)} eval(s) failed`),{code:c,evals:a}},N=w(async({cwd:e,format:s,logger:r,options:t})=>{const n=await M({cwd:e,dir:t.dir,format:s,logger:r,threshold:t.threshold});return{code:n.code,data:A(n),error:n.error}});export{N as execute,M as runEvalCommand};
@@ -1,99 +1,2 @@
1
- import{existsSync as O,readFileSync as k}from"node:fs";import{writeFile as I,mkdir as U}from"node:fs/promises";import{parseSchema as B,applyAdditiveEdit as Y,projectUsesUmbrella as q}from"@lunora/config";import{join as E}from"@visulima/path";import{d as F}from"../packem_shared/command-BpMv2n0o.mjs";import{EXIT_CODE as T}from"../packem_shared/EXIT_CODE-DQVhSE-1.mjs";import{createRequire as P}from"node:module";import{LunoraError as S}from"@lunora/errors";import{addArgsFor as H,detectPackageManager as K}from"@lunora/config/cli";import{l as f}from"../packem_shared/js-literal-FWLi94Lh.mjs";const j=/^\//,G=(e,r)=>{try{const{args:n,command:t}=H(K(e),[r],{dev:!0});return`${t} ${n.join(" ")}`}catch{return`<your-package-manager> add -D ${r}`}},L=(e,r,n)=>{const t=P(E(n,"noop.cjs"));try{t.resolve(e)}catch{throw new S("INTERNAL",`\`lunora introspect\` needs the \`${r}\` driver to read this database, and it isn't installed.
2
-
3
- Install it in your project:
4
-
5
- ${G(n,r)}`)}return t(e)},Q=e=>{const r=e.slice(0,Math.max(0,e.indexOf(":"))).toLowerCase();if(r==="postgres"||r==="postgresql")return"postgres";if(r==="mysql"||r==="mariadb")return"mysql";throw new S("INTERNAL",`Unrecognised database URL scheme \`${r}\`. Expected a \`postgres://\`, \`postgresql://\`, \`mysql://\`, or \`mariadb://\` connection string.`)},W=e=>{try{const r=new URL(e).pathname.replace(j,"");return r===""?void 0:decodeURIComponent(r)}catch{return}},J=async(e,r,n,t=process.cwd())=>{if(r==="postgres"){const m=L("pg","pg",t).Client,u=new m({connectionString:e});return await u.connect(),{close:async()=>{await u.end()},execute:async(l,p)=>(await u.query(l,[...p])).rows,schema:n??"public"}}const a=L("mysql2/promise","mysql2",t).createConnection,o=await a(e),i=n??W(e);if(i===void 0)throw new S("INTERNAL","A MySQL connection string must name a database (`mysql://host/<database>`), or pass `--schema`.");return{close:async()=>{await o.end()},execute:async(c,m)=>{const[u]=await o.execute(c,[...m]);return Array.isArray(u)?u:[]},schema:i}},h=new Set(["_creationTime","_id"]),X={bigint:"v.bigint()",bigserial:"v.bigint()",bit:"v.string()",bool:"v.boolean()",boolean:"v.boolean()",box:"v.string()",bpchar:"v.string()",bytea:"v.bytes()",char:"v.string()",character:"v.string()","character varying":"v.string()",cidr:"v.string()",circle:"v.string()",citext:"v.string()",date:"v.date()","double precision":"v.number()",float4:"v.number()",float8:"v.number()",inet:"v.string()",int:"v.number()",int2:"v.number()",int4:"v.number()",int8:"v.bigint()",integer:"v.number()",interval:"v.string()",json:"v.any()",jsonb:"v.any()",line:"v.string()",lseg:"v.string()",macaddr:"v.string()",money:"v.number()",name:"v.string()",numeric:"v.number()",path:"v.string()",point:"v.string()",polygon:"v.string()",real:"v.number()",serial:"v.number()",smallint:"v.number()",smallserial:"v.number()",text:"v.string()",time:"v.string()","time with time zone":"v.string()","time without time zone":"v.string()",timestamp:"v.timestamp()","timestamp with time zone":"v.timestamp()","timestamp without time zone":"v.timestamp()",timestamptz:"v.timestamp()",tsquery:"v.string()",tsvector:"v.string()",uuid:"v.string()",varbit:"v.string()",varchar:"v.string()",xml:"v.string()"},z={bigint:"v.bigint()",binary:"v.bytes()",bit:"v.number()",blob:"v.bytes()",char:"v.string()",date:"v.date()",datetime:"v.timestamp()",decimal:"v.number()",double:"v.number()",enum:"v.string()",float:"v.number()",int:"v.number()",integer:"v.number()",json:"v.any()",longblob:"v.bytes()",longtext:"v.string()",mediumblob:"v.bytes()",mediumint:"v.number()",mediumtext:"v.string()",numeric:"v.number()",set:"v.string()",smallint:"v.number()",text:"v.string()",time:"v.string()",timestamp:"v.timestamp()",tinyblob:"v.bytes()",tinyint:"v.number()",tinytext:"v.string()",varbinary:"v.bytes()",varchar:"v.string()",year:"v.number()"},w=(e,r)=>{const t=(r==="postgres"?X:z)[e.dataType],s=e.references===void 0?t:`v.id(${JSON.stringify(e.references.table)})`;let a=s??"v.any()";for(let o=0;o<e.arrayDepth;o+=1)a=`v.array(${a})`;return e.nullable&&(a=`v.optional(${a})`),{expression:a,known:s!==void 0}},M=/^[A-Z_$][\w$]*$/i,Z=/[^\dA-Z]+(.)?/gi,V=/^\d+/,ee=/[^\w-]/g,N=e=>M.test(e)?e:f(e),ne=e=>M.test(e)?`.${e}`:`[${f(e)}]`,x=e=>e.replaceAll("*/",String.raw`*\/`),te=e=>{const r=e.replaceAll(ee,"_");return r===""?"table":r},re=e=>{const n=e.replaceAll(Z,(t,s)=>s?.toUpperCase()??"").replace(V,"");return n===""?"table":`${n.charAt(0).toLowerCase()}${n.slice(1)}`},D=e=>e.columns.filter(r=>!h.has(r.name)),ae=e=>{const r=new Set(e.primaryKey);for(const n of e.indexes)for(const t of n.columns)r.add(t);for(const n of e.columns)n.references!==void 0&&r.add(n.name);return[...r].filter(n=>!h.has(n))},C=(e,r,n,t)=>e.references===void 0||r.has(e.references.table)?e:(t.push(`${n}.${e.name}: references \`${e.references.table}\`, which isn't in the generated schema — emitted as a plain column instead of \`v.id(...)\`.`),{arrayDepth:e.arrayDepth,dataType:e.dataType,name:e.name,nullable:e.nullable}),se=(e,r,n,t)=>{const s=[];for(const o of e.columns){if(h.has(o.name)){t.push(`${e.name}.${o.name}: skipped — \`${o.name}\` is a Lunora system column. Rename it in the source database or map it by hand.`);continue}const i=C(o,n,e.name,t),{expression:c,known:m}=w(i,r);!m&&i.references===void 0&&(t.push(`${e.name}.${o.name}: no mapping for SQL type \`${o.dataType}\` — emitted as \`v.any()\`.`),s.push(` // TODO: \`${x(o.dataType)}\` has no direct validator; narrow this.`)),s.push(` ${N(o.name)}: ${c},`)}const a=[' .global({ backend: "hyperdrive" })'];if(e.primaryKey.length>0&&e.primaryKey.every(o=>!h.has(o))){const o=e.primaryKey.map(i=>f(i)).join(", ");a.push(` .index(${f(`by_${e.primaryKey.join("_")}`)}, [${o}], { unique: true })`)}for(const o of e.indexes){const i=o.columns.filter(u=>!h.has(u));if(i.length===0)continue;const c=i.map(u=>f(u)).join(", "),m=o.unique?", { unique: true }":"";a.push(` .index(${f(o.name)}, [${c}]${m})`)}return` ${N(e.name)}: defineTable({
6
- ${s.join(`
7
- `)}
8
- })
9
- ${a.join(`
10
- `)},`},oe=(e,r,n)=>{const t=new Set(e.tables.map(a=>a.name)),s=e.tables.map(a=>se(a,e.dialect,t,n)).join(`
11
- `);return`/**
12
- * Generated by \`lunora introspect\` from an existing ${e.dialect==="postgres"?"Postgres":"MySQL"} database.
13
- *
14
- * This file is a STARTING POINT, not a build artifact — it is written once and is
15
- * yours to edit. Review it before shipping: column types are mapped
16
- * conservatively, and every table is \`.global({ backend: "hyperdrive" })\` because
17
- * its rows live in the external database. Re-running \`lunora introspect\` will not
18
- * overwrite it unless you pass \`--force\`.
19
- */
20
- import { defineSchema, defineTable, v } from "${r.serverImport}";
21
-
22
- export default defineSchema({
23
- ${s}
24
- });
25
- `},ie=(e,r,n,t=new Set([e.name]),s=[])=>{const a=re(e.name),o=ae(e),i=new Map(D(e).map(l=>[l.name,l])),c=o.map(l=>{const p=i.get(l);if(p===void 0)return;const v=C({...p,nullable:!1},t,e.name,[]),{expression:d,known:A}=w(v,r);return!A&&v.references===void 0&&s.push(`${e.name}.${l}: filter falls back to \`v.any()\` (no mapping for SQL type \`${p.dataType}\`).`),` ${N(l)}: ${d},`}).filter(l=>l!==void 0),m=[f("_creationTime"),...o.map(l=>f(l))].join(", "),u=`ctx.db${ne(e.name)}`;return`/**
26
- * Generated by \`lunora introspect\` for the \`${x(e.name)}\` table — a
27
- * starting point you own and edit.
28
- *
29
- * Both procedures are RPC-only. Add \`.expose({ rest: true })\` once you've decided
30
- * this data should be public, and gate them with your auth/RLS middleware first —
31
- * \`introspect\` cannot know who is allowed to read this table.
32
- */
33
- import { defineListArgs, v } from "${n.serverImport}";
34
-
35
- import type { Doc } from "./_generated/dataModel";
36
- import { c } from "./_generated/server";
37
-
38
- const ${a}List = defineListArgs<Doc<${f(e.name)}>>()({
39
- // Only index-backed columns are published as filterable, so a caller cannot
40
- // reach a column you did not choose to expose. Note that \`contains\` and the
41
- // negative operators still scan — narrow this list, and review it, before
42
- // adding \`.expose({ rest: true })\`.
43
- filter: {
44
- ${c.join(`
45
- `)}
46
- },
47
- orderBy: [${m}],
48
- });
49
-
50
- export const list = c.query.input(${a}List.args).query(({ args, ctx }) => ${u}.findMany(${a}List.toQueryArgs(args)));
51
-
52
- export const get = c.query.input({ id: v.id(${f(e.name)}) }).query(({ args, ctx }) => ${u}.get(args.id));
53
- `},ce=(e,r)=>{const n=[],t=new Set(e.tables.map(a=>a.name)),s=[{contents:oe(e,r,n),path:"schema.ts"}];if(r.procedures){const a=new Map([["schema","the generated schema module"]]);for(const o of e.tables){if(D(o).length===0){n.push(`${o.name}: no usable columns — procedure module skipped.`);continue}const i=te(o.name);i!==o.name&&n.push(`${o.name}: written as \`${i}.ts\` — the table name isn't usable as a filename.`);const c=a.get(i);if(c!==void 0){n.push(`${o.name}: procedure module skipped — its filename \`${i}.ts\` collides with table \`${c}\`.`);continue}a.set(i,o.name),s.push({contents:ie(o,e.dialect,r,t,n),path:`${i}.ts`})}}return{files:s,warnings:n}},y=/^[A-Z_$][\w$]*$/i,me=e=>e.startsWith("v.optional(")&&e.endsWith(")")?e.slice(11,-1):e,le=(e,r,n)=>{const t=[],s=new Set(r?.columns.map(a=>a.name));for(const a of e.columns){if(h.has(a.name)||s.has(a.name))continue;if(!y.test(a.name)){n.warnings.push(`${e.name}.${a.name}: skipped — column names must be bare identifiers to merge.`);continue}const o=a.references!==void 0&&!n.present.has(a.references.table),{expression:i}=w(o?{...a,references:void 0}:a,n.dialect);t.push({column:a.name,kind:"addOptionalColumn",table:e.name,validator:me(i)}),!a.nullable&&r!==void 0&&n.warnings.push(`${e.name}.${a.name}: added as \`v.optional(...)\` — a required column on an existing table needs a backfill migration.`)}return t},ue=(e,r,n)=>{const t=[],s=new Set(r?.indexes.map(a=>a.name));for(const a of e.indexes){const o=a.columns.filter(i=>!h.has(i));if(!(s.has(a.name)||o.length===0)){if(!y.test(a.name)||!o.every(i=>y.test(i))){n.push(`${e.name}: index \`${a.name}\` skipped — index and column names must be bare identifiers to merge.`);continue}t.push({fields:o,kind:"addIndex",name:a.name,table:e.name,...a.unique?{unique:!0}:{}})}}return t},de=(e,r,n)=>{const t=[],s=[],a=new Map(r.map(i=>[i.name,i])),o=new Set([...a.keys(),...e.tables.map(i=>i.name)]);for(const i of e.tables){if(!y.test(i.name)){s.push(`${i.name}: skipped — merging into an existing schema needs a table name that is a bare identifier.`);continue}const c=a.get(i.name);c===void 0&&t.push({global:{backend:"hyperdrive"},kind:"addTable",table:i.name}),t.push(...le(i,c,{dialect:n,present:o,warnings:s}),...ue(i,c,s))}return{edits:t,warnings:s}},pe=(e,r,n)=>{const t=B(e);if(!t.ok)return{applied:0,warnings:[`lunora/schema.ts could not be parsed (${t.reason}) — leave it alone and merge by hand, or pass --force.`]};const s=de(r,t.tables,n),a=[...s.warnings];let o=e,i=0;for(const c of s.edits){const m=Y(o,c);m.ok?(o=m.text,i+=1):a.push(`${c.table}: skipped one edit (${m.reason}).`)}return{applied:i,warnings:a,...i===0?{}:{text:o}}},g=(e,r)=>{const n=e[r]??e[r.toLowerCase()]??e[r.toUpperCase()];return typeof n=="string"?n:typeof n=="number"||typeof n=="bigint"||typeof n=="boolean"?String(n):""},ge=`
54
- SELECT c.table_name, c.column_name, c.is_nullable, c.data_type, c.udt_name
55
- FROM information_schema.columns c
56
- JOIN information_schema.tables t
57
- ON t.table_schema = c.table_schema AND t.table_name = c.table_name
58
- WHERE c.table_schema = $1 AND t.table_type = 'BASE TABLE'
59
- ORDER BY c.table_name, c.ordinal_position`,fe=`
60
- SELECT tc.table_name, kcu.column_name
61
- FROM information_schema.table_constraints tc
62
- JOIN information_schema.key_column_usage kcu
63
- ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema
64
- WHERE tc.table_schema = $1 AND tc.constraint_type = 'PRIMARY KEY'
65
- ORDER BY tc.table_name, kcu.ordinal_position`,ve=`
66
- SELECT tc.table_name, kcu.column_name, ccu.table_name AS foreign_table, ccu.column_name AS foreign_column
67
- FROM information_schema.table_constraints tc
68
- JOIN information_schema.key_column_usage kcu
69
- ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema
70
- JOIN information_schema.constraint_column_usage ccu
71
- ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
72
- WHERE tc.table_schema = $1 AND tc.constraint_type = 'FOREIGN KEY'`,be=`
73
- SELECT t.relname AS table_name, i.relname AS index_name, ix.indisunique AS is_unique, a.attname AS column_name
74
- FROM pg_class t
75
- JOIN pg_index ix ON t.oid = ix.indrelid
76
- JOIN pg_class i ON i.oid = ix.indexrelid
77
- JOIN pg_namespace n ON n.oid = t.relnamespace
78
- CROSS JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, ord)
79
- JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
80
- WHERE n.nspname = $1 AND t.relkind = 'r' AND NOT ix.indisprimary
81
- ORDER BY t.relname, i.relname, k.ord`,he=`
82
- SELECT c.TABLE_NAME, c.COLUMN_NAME, c.IS_NULLABLE, c.DATA_TYPE
83
- FROM information_schema.COLUMNS c
84
- JOIN information_schema.TABLES t
85
- ON t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME
86
- WHERE c.TABLE_SCHEMA = ? AND t.TABLE_TYPE = 'BASE TABLE'
87
- ORDER BY c.TABLE_NAME, c.ORDINAL_POSITION`,_e=`
88
- SELECT TABLE_NAME, COLUMN_NAME
89
- FROM information_schema.KEY_COLUMN_USAGE
90
- WHERE TABLE_SCHEMA = ? AND CONSTRAINT_NAME = 'PRIMARY'
91
- ORDER BY TABLE_NAME, ORDINAL_POSITION`,Ee=`
92
- SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME AS foreign_table, REFERENCED_COLUMN_NAME AS foreign_column
93
- FROM information_schema.KEY_COLUMN_USAGE
94
- WHERE TABLE_SCHEMA = ? AND REFERENCED_TABLE_NAME IS NOT NULL`,ye=`
95
- SELECT TABLE_NAME, INDEX_NAME AS index_name, NON_UNIQUE, COLUMN_NAME
96
- FROM information_schema.STATISTICS
97
- WHERE TABLE_SCHEMA = ? AND INDEX_NAME <> 'PRIMARY'
98
- ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX`,_=e=>{const r=new Map;for(const n of e){const t=g(n,"table_name"),s=r.get(t);s===void 0?r.set(t,[n]):s.push(n)}return r},Ae=(e,r)=>{const n=g(e,"data_type").toLowerCase();if(r!=="postgres")return{arrayDepth:0,dataType:n};const t=g(e,"udt_name").toLowerCase();return n==="array"&&t.startsWith("_")?{arrayDepth:1,dataType:t.slice(1)}:{arrayDepth:0,dataType:t===""?n:t}},Ne=(e,r)=>{const n=new Map;for(const t of e){const s=g(t,"index_name"),a=r==="postgres"?g(t,"is_unique")==="true":g(t,"NON_UNIQUE")==="0",o=n.get(s);o===void 0?n.set(s,{columns:[g(t,"column_name")],unique:a}):o.columns.push(g(t,"column_name"))}return[...n.entries()].map(([t,s])=>({columns:s.columns,name:t,unique:s.unique}))},Se=async(e,r,n)=>{const t=r==="postgres",[s,a,o,i]=await Promise.all([e(t?ge:he,[n]),e(t?fe:_e,[n]),e(t?ve:Ee,[n]),e(t?be:ye,[n])]),c=_(a),m=_(o),u=_(i),l=[];for(const[p,v]of _(s)){const d=new Map((m.get(p)??[]).map(b=>[g(b,"column_name"),{column:g(b,"foreign_column"),table:g(b,"foreign_table")}])),A=v.map(b=>{const R=g(b,"column_name"),$=d.get(R);return{...Ae(b,r),name:R,nullable:g(b,"is_nullable").toUpperCase()==="YES",...$===void 0?{}:{references:$}}});l.push({columns:A,indexes:Ne(u.get(p)??[],r),name:p,primaryKey:(c.get(p)??[]).map(b=>g(b,"column_name"))})}return l.sort((p,v)=>p.name.localeCompare(v.name)),{dialect:r,tables:l}},we=e=>q(e)?"lunorash/server":"@lunora/server",Re=async(e,r,n)=>{const t=E(r,e.path);return O(t)&&n.force!==!0?(n.logger.warn(`skipped lunora/${e.path} — it already exists (pass --force to overwrite)`),!1):n.dryRun===!0?(n.logger.info(`would write lunora/${e.path} (${String(e.contents.split(`
99
- `).length)} lines)`),!1):(await U(r,{recursive:!0}),await I(t,e.contents,"utf8"),n.logger.info(`wrote lunora/${e.path}`),!0)},$e=(e,r)=>{const n=r.tables;if(n===void 0||n.length===0)return e.tables;for(const t of n)e.tables.some(s=>s.name===t)||r.logger.warn(`--tables: no table named "${t}" exists in this schema — skipped.`);return e.tables.filter(t=>n.includes(t.name))},Te=async(e,r,n,t)=>{const s=pe(k(e,"utf8"),r,n);for(const a of s.warnings)t.logger.warn(a);return s.text===void 0?(t.logger.info("lunora/schema.ts is already up to date with the database."),!1):t.dryRun===!0?(t.logger.info(`would merge ${String(s.applied)} addition(s) into lunora/schema.ts`),!1):(await I(e,s.text,"utf8"),t.logger.info(`merged ${String(s.applied)} addition(s) into lunora/schema.ts`),!0)},Le=async e=>{const r=e.cwd??process.cwd();if(e.connection===void 0&&(e.url===void 0||e.url==="")){const d="`lunora introspect` needs a database URL: pass --url, or set DATABASE_URL.";return e.logger.error(d),{code:T.USAGE,error:d,written:[]}}const n=e.connection===void 0?Q(e.url):e.dialect??"postgres",t=e.connection??await J(e.url,n,e.schema,r);let s;try{s=await Se(t.execute,n,t.schema)}finally{e.connection===void 0&&await t.close()}const a=$e(s,e);if(a.length===0){const d="no tables found to introspect — check --schema and --tables.";return e.logger.error(d),{code:T.NOT_FOUND,error:d,written:[]}}const{files:o,warnings:i}=ce({...s,tables:a},{procedures:e.procedures!==!1,serverImport:we(r)}),c=E(r,"lunora"),m=[],u=E(c,"schema.ts"),l=O(u)&&e.force!==!0,p=l?o.filter(d=>d.path!=="schema.ts"):o;l&&await Te(u,{...s,tables:a},n,e)&&m.push("schema.ts");for(const d of p)await Re(d,c,e)&&m.push(d.path);for(const d of i)e.logger.warn(d);e.logger.info(`introspected ${String(a.length)} table(s) from ${n}. Review the generated files before running \`lunora dev\`.`);const v=a.map(d=>d.name);return{code:0,dialect:n,tables:v,written:m}},qe=F(async({cwd:e,format:r,logger:n,options:t})=>{const s=await Le({cwd:e,dryRun:t.dryRun===!0,force:t.force===!0,format:r,logger:n,procedures:t.procedures!==!1,schema:t.schema,tables:t.tables===void 0?void 0:t.tables.split(",").map(a=>a.trim()),url:t.url??process.env.DATABASE_URL});return{code:s.code,data:s.dialect===void 0?void 0:{dialect:s.dialect,dryRun:t.dryRun===!0,tables:s.tables??[],written:s.written},error:s.error}});export{qe as execute,we as resolveServerImport,Le as runIntrospectCommand};
1
+ import{readLinkedProject as h,resolveDeployDriver as b}from"@lunora/config";import{d as y}from"../packem_shared/command-BpMv2n0o.mjs";import{r as w}from"../packem_shared/deploy-target-resolve-BTCW1Iv4.mjs";import{EXIT_CODE as m}from"../packem_shared/EXIT_CODE-DQVhSE-1.mjs";import{defaultSpawner as E}from"../packem_shared/createRecordingSpawner-DchLi0gR.mjs";import{createR2Sql as L}from"@lunora/bindings/r2sql";import{createPipelineLogReader as S}from"@lunora/runtime";import{toolchainExecArgs as _,detectPackageManager as T}from"@lunora/config/cli";const p=["trace","debug","log","info","warn","error","fatal"],C=new Set(p),v=/^\d+$/,f=(e,r)=>{if(e===void 0)return;const a=e.trim();if(v.test(a))return Number(a);const n=Date.parse(a);if(Number.isNaN(n))throw new TypeError(`logs: invalid ${r} "${e}" — pass epoch-millis or an ISO 8601 date`);return n},I=e=>Buffer.from(JSON.stringify(e),"utf8").toString("base64url"),$=e=>{const r=e.trim();if(v.test(r))return{ts:Number(r)};let a;try{a=JSON.parse(Buffer.from(r,"base64url").toString("utf8"))}catch{throw new TypeError(`logs: invalid --cursor "${e}" — expected the token printed by the previous page (or a bare epoch-millis ts)`)}if(typeof a!="object"||a===null||typeof a.ts!="number"||!Number.isFinite(a.ts))throw new TypeError(`logs: invalid --cursor "${e}" — expected the token printed by the previous page (or a bare epoch-millis ts)`);const{ts:n}=a,t=a.seen,s=Array.isArray(t)?t.filter(o=>typeof o=="string"):void 0;return s!==void 0&&s.length>0?{seen:s,ts:n}:{ts:n}},g=(e,r)=>{if(e!==void 0){if(!C.has(e))throw new TypeError(`logs: invalid ${r} "${e}" — expected one of trace, debug, log, info, warn, error, fatal`);return e}},R=e=>{const r={},a=f(e.since,"--since");a!==void 0&&(r.sinceTs=a);const n=f(e.until,"--until");n!==void 0&&(r.untilTs=n);const t=g(e.level,"--level");t!==void 0&&(r.level=t);const s=g(e.minLevel,"--min-level");if(s!==void 0&&(r.minLevel=s),e.functionPrefix!==void 0&&(r.functionPathPrefix=e.functionPrefix),e.traceId!==void 0&&(r.traceId=e.traceId),e.shardKey!==void 0&&(r.shardKey=e.shardKey),e.userId!==void 0&&(r.userId=e.userId),e.limit!==void 0){const o=Number(e.limit);if(!Number.isFinite(o))throw new TypeError(`logs: invalid --limit "${e.limit}" — expected a number`);r.limit=o}return e.cursor!==void 0&&(r.cursor=$(e.cursor)),r},N=e=>{const r=new Date(e.ts).toISOString(),a=e.level.toUpperCase().padEnd(5);return`${r} ${a} ${e.functionPath} ${e.message}`},O=async e=>{const r=e.environment??process.env,a=r.R2_SQL_ACCOUNT_ID,n=r.R2_SQL_TOKEN,t=r.R2_SQL_BUCKET,s=[];if((a===void 0||a.length===0)&&s.push("R2_SQL_ACCOUNT_ID"),(n===void 0||n.length===0)&&s.push("R2_SQL_TOKEN"),(t===void 0||t.length===0)&&s.push("R2_SQL_BUCKET"),(e.table===void 0||e.table.length===0)&&s.push("--table"),s.length>0)return e.logger.error(`logs --durable: R2 SQL not configured (missing ${s.join(", ")}). The Pipeline must write to an R2 Data Catalog (Iceberg) table, and you must supply R2_SQL_ACCOUNT_ID / R2_SQL_TOKEN / R2_SQL_BUCKET plus --table — see the observability docs.`),{code:m.USAGE,error:"not configured"};let o;try{o=R(e)}catch(i){return e.logger.error(i instanceof Error?i.message:String(i)),{code:m.USAGE,error:"invalid option"}}const u=L({accountId:a,apiToken:n,bucket:t,fetch:e.fetch}),c=await S(u,{namespace:e.namespace,table:e.table}).query(o);for(const i of c.rows)process.stdout.write(`${e.ndjson===!0?JSON.stringify(i):N(i)}
2
+ `);return c.rows.length===0?e.logger.info("logs --durable: no matching log records"):c.nextCursor!==void 0&&e.logger.info(`logs --durable: more rows available — pass --cursor ${I(c.nextCursor)} for the next page`),{code:0,rows:c.rows}},x=async e=>{const r=e.cwd??process.cwd(),a=e.env??h(r)?.env,n=w(r,e.target);if(n.target===void 0){const d=`logs: ${n.error??"no deploy target"}`;return e.logger.error(d),{code:m.USAGE,descriptor:void 0,error:d}}const t=b(n.target),s=t.toolchain?.tail;if(s===void 0){const d=`logs: deploy target "${t.id}" has no log tail`;return e.logger.error(d),{code:m.USAGE,descriptor:void 0,error:d}}const o=s({environment:a,format:e.format,search:e.search,status:e.status,temporary:e.temporary,worker:e.worker}),u=_(T(r),o),l={args:u.args,command:u.command,cwd:r};return e.logger.info(`tailing logs via ${l.command} ${l.args.join(" ")}`),{code:(await(e.spawner??E)(l)).code,delegated:!0,descriptor:l}},G=y(async({argument:e,cwd:r,format:a,logger:n,options:t})=>{if(t.durable===!0){const s=await O({cursor:t.cursor,functionPrefix:t.functionPrefix,level:t.level,limit:t.limit,logger:n,minLevel:t.minLevel,namespace:t.namespace,ndjson:t.ndjson===!0,shardKey:t.shardKey,since:t.since,table:t.table,traceId:t.traceId,until:t.until,userId:t.userId});return{...s,...s.rows===void 0?{}:{delegated:!0}}}return x({cwd:r,env:t.env,format:a,logger:n,search:t.search,status:t.status,target:t.target,temporary:t.temporary===!0,worker:e[0]})});export{G as execute,x as runLogsCommand};
@@ -1,3 +1,99 @@
1
- import{randomUUID as V,createHash as gr,createHmac as yr}from"node:crypto";import{mkdirSync as $r,writeFileSync as ne,linkSync as xe,rmSync as F,statSync as Ce,unlinkSync as kr,readFileSync as L,existsSync as Q,mkdtempSync as Er}from"node:fs";import{tmpdir as br}from"node:os";import{join as ee,dirname as _r}from"node:path";import{SCHEMA_SNAPSHOT_FILENAME as Sr,readServiceBindings as Rr}from"@lunora/codegen";import{generateSecretValue as Ir}from"@lunora/config";import{readManifest as Ke,recordManifestKey as Or,findWranglerFile as He,readWranglerJsonc as $e,applyModify as Tr}from"@lunora/config/cloudflare";import{n as pe,r as Ar,c as Cr}from"../packem_shared/cloudflare-credentials-BVmZnDZk.mjs";import{d as jr}from"../packem_shared/command-BpMv2n0o.mjs";import{r as Ur}from"../packem_shared/deploy-target-resolve-BTCW1Iv4.mjs";import{EXIT_CODE as w}from"../packem_shared/EXIT_CODE-DQVhSE-1.mjs";import{c as oe,d as Nr,b as Lr,e as Dr,r as Pr}from"./handler.mjs";import{runSeedCommand as Fr}from"./handler13.mjs";const Be=r=>{if(!Number.isInteger(r)||r<=0)return!1;try{return process.kill(r,0),!0}catch(e){return e.code==="EPERM"}},ve=r=>Number(L(r,"utf8")),je=r=>{try{return ve(r)}catch{return}},Mr=(r,e)=>{const t=`${r}.retire-${String(e)}`;try{xe(r,t)}catch{return!1}try{return ve(t)!==e||Be(ve(t))?!1:(Ce(r).ino===Ce(t).ino&&kr(r),!0)}finally{F(t,{force:!0})}},Wr=(r,e={})=>{const t=ee(r,".lunora","preview.lock"),n=`${t}.${String(process.pid)}.${V()}`;$r(_r(t),{recursive:!0}),ne(n,String(process.pid));try{for(let o=0;o<3;o+=1){try{return xe(n,t),{release:()=>{je(t)===process.pid&&F(t,{force:!0})}}}catch(i){if(i.code!=="EEXIST")throw i}const s=je(t);if(s!==void 0){if(Be(s))return{error:`another preview command is running in this checkout (pid ${String(s)}); wait for it, or remove ${t} if it is not`};if(e.afterReadingOwner?.(),!Mr(t,s))return{error:`another preview command is taking over the lock at ${t}; run this again in a moment. If none is running, remove ${t}.retire-${String(s)}`}}}return{error:`could not take the preview lock ${t}`}}finally{F(n,{force:!0})}},xr=[["SIGINT",130],["SIGTERM",143]],Kr=5e3,H=new Set,Ge=r=>process.exit(r);let Ve=Ge,Z=[],he;const Hr=async r=>{const e=[...H].map(async n=>{try{await n()}catch{}});H.clear();const t=new Promise(n=>{setTimeout(n,Kr).unref()});await Promise.race([Promise.all(e),t]),Ve(r)},Br=r=>(he??=Hr(r),he),ie=(r,e)=>(H.add(r),Z.length===0&&(Z=xr.map(([t,n])=>{const o=()=>{Br(n).catch(()=>{})};return process.on(t,o),[t,o]})),()=>{if(H.delete(r),H.size===0){for(const[t,n]of Z)process.off(t,n);Z=[],Ve=Ge,he=void 0}}),Gr=/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu,ke=r=>{const e=Ke(r)?.lunora?.appId;return typeof e=="string"&&Gr.test(e)?e.toLowerCase():void 0},Vr=r=>{const e=Ke(r);if(e===void 0||e.lunoraIsForeign||e.lunora?.appId!==void 0)return;const t=V();return Or(e,"appId",t),ke(r)===t?t:void 0},se=63,Ee=new Set(["$schema","account_id","alias","assets","base_dir","build","cache","compatibility_date","compatibility_flags","dev","env","find_additional_modules","jsx_factory","jsx_fragment","keep_names","keep_vars","limits","logpush","main","migrations","minify","name","no_bundle","observability","placement","preview_urls","routes","rules","send_metrics","triggers","tsconfig","upload_source_maps","workers_dev"]),Ye=["ai","browser","define","images","media","secrets","vars","version_metadata","worker_loaders"],Yr=new Set(["env","name","routes","triggers","workers_dev"]),Jr=new Set(["tail_consumers"]),Y={d1_databases:"d1",kv_namespaces:"kv",r2_buckets:"r2"},R=r=>r==null?!1:Array.isArray(r)?r.length>0:typeof r!="object"||Object.keys(r).length>0,I=r=>(Array.isArray(r)?r:[]).filter(e=>typeof e=="object"&&e!==null),Je=(r,e)=>{if(!(Ee.has(r)||Jr.has(r)||Ye.includes(r)||r in Y)){if(r==="route")return`"route": write it as "routes" so the preview can clear it — a preview must never claim production's hostname`;if(r==="durable_objects"){const t=I(e.bindings).find(n=>n.script_name!==void 0);return t===void 0?void 0:`"durable_objects": the "${String(t.name)}" binding reaches another Worker's Durable Objects (script_name "${String(t.script_name)}"), which the preview would share`}if(r==="exports"){const t=Object.entries(e).find(([,n])=>n?.type==="workflow");return t===void 0?void 0:`"exports": workflow "${t[0]}" has an account-wide name, so the preview would take over production's workflow`}return`"${r}": a preview cannot isolate this binding yet, and copying it would point the preview at production's resource`}},Ue="LUNORA_ORIGIN_URL",U="LUNORA_PREVIEW",M=r=>`${r.production}/${r.slug}`,Xr=/[^a-z0-9]+/u,Xe=r=>{const e=r.toLowerCase().split(Xr).filter(t=>t!=="").join("-");return e===""||e===r?e:`${e}-${gr("sha256").update(r).digest("hex").slice(0,6)}`},ae=(r,e)=>oe(`${r}-preview-${e}`,se),be=(r,e)=>{const t=typeof r.name=="string"?r.name:"",n=Xe(e);return t===""?{error:`wrangler.jsonc has no "name", which the preview Worker's name is derived from`}:n===""?{error:`"${e}" has no letter or digit to name a preview by`}:{names:{environment:oe(`preview-${n}`,se),production:t,slug:n,worker:ae(t,n)}}},J=(r,e,t)=>{const n=t?.vars?.[U],o=typeof n=="string"?n.lastIndexOf("/"):-1;if(typeof n!="string"||o<=0)return!1;const s=n.slice(0,o),i=n.slice(o+1);return Xe(i)===i&&M({production:s,slug:i})===n&&e===oe(`preview-${i}`,se)&&t?.name===ae(s,i)},ce=(r,e)=>{const t=Object.entries(r.env??{}).filter(([i,a])=>!J(r,i,a)),n=t.find(([i])=>i===e)?.[1];if(n===void 0)return{error:`wrangler.jsonc declares no environment "${e}"${t.length===0?", and no environments at all":`; the environments are: ${t.map(([i])=>i).join(", ")}`}`};const o=typeof r.name=="string"?r.name:"";return{config:{...Object.fromEntries(Object.entries(r).filter(([i])=>Ee.has(i)||i==="exports")),...n,env:r.env,name:typeof n.name=="string"?n.name:`${o}-${e}`}}},zr=r=>Object.entries(r.env??{}).filter(([e,t])=>!J(r,e,t)).filter(([,e])=>typeof e.account_id=="string"&&e.account_id!==""&&e.account_id!==r.account_id).map(([e])=>e),ze=(r,e)=>oe(`${r}-${e}`,se),Ne=(r,e)=>{const t=I(r==="durable_objects"?e?.bindings:e);return new Set(t.map(n=>String(r==="durable_objects"?n.name:n.binding)))},Qr=r=>Object.entries(r.env??{}).flatMap(([e,t])=>J(r,e,t)?[]:Object.entries(t).flatMap(([n,o])=>{const s=r[n];if(n in Y||n==="durable_objects"){const i=Ne(n,s),a=[...Ne(n,o)].filter(c=>!i.has(c));return a.length===0?[]:[`"env.${e}.${n}": binding ${a.map(c=>`"${c}"`).join(", ")} is declared only in this environment, and a preview is planned from the top-level config, which lacks it; pass --env ${e} to preview that environment`]}return Je(n,o)!==void 0&&R(o)&&!R(s)?[`"env.${e}.${n}": declared only in this environment, and a preview is planned from the top-level config, which has none; pass --env ${e} to preview that environment`]:[]})),Qe=(r,e)=>Object.entries(Y).flatMap(([t,n])=>I(r[t]).map(o=>{const s=String(o.binding);return{binding:s,kind:n,name:ze(e,s)}})),Ze=(r,e,t)=>{const n=be(r,e);if(n.error!==void 0)return{error:n.error};const{environment:o,worker:s}=n.names;if(r.env?.[o]!==void 0&&!J(r,o,r.env[o]))return{error:`wrangler.jsonc already declares "env.${o}"; a preview writes that block itself, so name the preview differently`};const i=Object.entries(r).flatMap(([f,l])=>{const m=R(l)?Je(f,l):void 0;return m===void 0?[]:[m]});i.push(...t===void 0?Qr(r):[]);const a=I(r.r2_buckets).find(f=>f.jurisdiction!==void 0);if(a!==void 0&&i.push(`"r2_buckets": the "${String(a.binding)}" bucket has a jurisdiction, which a preview bucket is not created in yet`),i.length>0)return{error:`this app cannot be previewed in isolation yet:
2
- - ${i.join(`
3
- - `)}`};const c=Object.keys(Y).find(f=>I(r[f]).some(l=>typeof l.binding!="string"||l.binding===""));if(c!==void 0)return{error:`"${c}": every entry needs a non-empty "binding"`};const d=Qe(r,s),u=d.find((f,l)=>d.findIndex(m=>m.kind===f.kind&&m.name===f.name)!==l);return u!==void 0?{error:`two bindings would share the preview resource name "${u.name}"; rename one of them (binding names differ only by case or punctuation)`}:{plan:{...n.names,...t===void 0?{}:{sourceEnv:t},resources:d}}},_e=(r,e)=>`${r}:${e}`,le=(r,e)=>Object.fromEntries(Object.entries(r).filter(([t])=>t!==e)),Zr=(r,e,t,n)=>{const o=String(e.binding),s=t.resources.find(a=>a.kind===r&&a.binding===o),i=n.get(_e(r,o));if(s===void 0||i===void 0)throw new Error(`no preview ${r} resource for the "${o}" binding`);return r==="d1"?{...le(e,"preview_database_id"),database_id:i,database_name:s.name}:r==="kv"?{...le(e,"preview_id"),id:i}:{...le(e,"preview_bucket_name"),bucket_name:i}},qr=(r,e,t)=>{if(t.sourceEnv!==void 0)for(const[n,o]of Object.entries(r))Ee.has(n)&&!(n in e)&&!Yr.has(n)&&R(o)&&(e[n]=o)},qe=(r,e,t,n={},o)=>{const s=r,i={name:e.worker,routes:[],triggers:{crons:[]},workers_dev:!0},a={[U]:M(e)};for(const d of Ye)R(s[d])&&(i[d]=s[d]);const c=R(r.vars)?{...r.vars}:{};Ue in c&&(c[Ue]=o??e.worker),i.vars={...c,...n,...a},qr(s,i,e),R(r.durable_objects)&&(i.durable_objects=r.durable_objects),R(s.exports)&&(i.exports=s.exports);for(const[d,u]of Object.entries(Y)){const f=I(s[d]);f.length>0&&(i[d]=f.map(l=>Zr(u,l,e,t)))}return i},et={analytics_engine_datasets:["dataset"],d1_databases:["database_id","database_name","preview_database_id"],hyperdrive:["id"],kv_namespaces:["id","preview_id"],r2_buckets:["bucket_name","preview_bucket_name"],vectorize:["index_name"]},rt={d1_databases:"d1",kv_namespaces:"kv",r2_buckets:"r2"},er=(r,e)=>`${r}:${e}`,tt=r=>{const e=Object.entries(et).flatMap(([t,n])=>I(r[t]).flatMap(o=>n.map(s=>[rt[t]??t,o[s]])));return[["worker",r.name],...e].flatMap(([t,n])=>typeof n=="string"&&n!==""?[er(t,n)]:[])},nt=r=>new Set([r,...Object.entries(r.env??{}).flatMap(([e,t])=>J(r,e,t)?[]:[t])].flatMap(e=>tt(e))),Se=(r,e)=>[{kind:"worker",value:r},...e.flatMap(t=>[...t.id===void 0?[]:[{kind:t.kind,value:t.id}],{kind:t.kind,value:t.name}])],B=(r,e)=>{const t=nt(r);return e.filter(n=>t.has(er(n.kind,n.value))).map(n=>n.value)},fe="lunora-preview-registry",re=50,de=r=>{try{const{errors:e}=JSON.parse(r.text),t=(e??[]).flatMap(n=>typeof n.message=="string"?[n.message]:[]).join("; ");return t===""?`HTTP ${String(r.status)}`:`HTTP ${String(r.status)}: ${t}`}catch{return`HTTP ${String(r.status)}`}},k=(r,e)=>{if(!r.ok)throw new Error(`${e} failed (${de(r)})`);return r.body},X=r=>typeof r.result=="object"&&r.result!==null?r.result:{},rr=async(r,e,t,n)=>{const o=e.includes("?")?"&":"?";for(let s=1;s<=re;s+=1){const i=k(await r(`${e}${o}per_page=100&page=${String(s)}`),`listing ${t}`);if(!Array.isArray(i.result))throw new TypeError(`the listing of ${t} had no result list`);const a=i.result.find(d=>n(d)),c=typeof i.result_info=="object"&&i.result_info!==null?i.result_info:{};if(a!==void 0||typeof c.total_pages!="number"||s>=c.total_pages)return a}throw new Error(`the listing of ${t} has more than ${String(re)} pages`)},me=async(r,e)=>{const t=await rr(r,"/storage/kv/namespaces","KV namespaces",n=>n.title===e);return typeof t?.id=="string"?t.id:void 0},tr=async(r,e)=>{const{id:t}=X(k(await r("/storage/kv/namespaces",{body:JSON.stringify({title:e}),method:"POST"}),`creating KV namespace "${e}"`));if(typeof t!="string")throw new TypeError(`creating KV namespace "${e}" returned no id`);return t},Re=async(r,e)=>{const t=await me(r,fe);if(t!==void 0||!e)return t;try{return await tr(r,fe)}catch(n){const o=await me(r,fe);if(o===void 0)throw n;return o}},D=(r,e)=>`${r}/${e}`,Ie=(r,e)=>`/storage/kv/namespaces/${encodeURIComponent(r)}/values/${encodeURIComponent(e)}`,ot=new Set(["d1","kv","r2"]),it=(r,e)=>{const t=e;return typeof t=="object"&&t!==null&&ot.has(t.kind)&&typeof t.binding=="string"&&typeof t.id=="string"&&(t.pending===void 0||t.pending===!0)&&t.name===ze(r,t.binding)&&(t.kind!=="r2"||t.id===t.name)},st=r=>{const e=r;return typeof e=="object"&&e!==null&&typeof e.repository=="string"&&typeof e.pullRequest=="string"},at=r=>{const e=r;return typeof e=="object"&&e!==null&&e.version===1&&typeof e.worker=="string"&&typeof e.production=="string"&&(e.appId===void 0||typeof e.appId=="string")&&(e.sourceEnv===void 0||typeof e.sourceEnv=="string")&&(e.origin===void 0||st(e.origin))&&Array.isArray(e.resources)&&e.resources.every(t=>it(e.worker,t))},ge=async(r,e,t)=>{const n=await r(Ie(e,t));if(!n.ok&&n.status===404)return;const o=k(n,`reading the preview record "${t}"`);if(!at(o))throw new TypeError(`the preview record "${t}" is not one this CLI wrote; refusing to act on it`);return o},j=async(r,e,t)=>{k(await r(Ie(e,D(t.appId??t.production,t.name)),{body:JSON.stringify(t),method:"PUT"}),"writing the preview record")},nr=async(r,e,t)=>{k(await r(Ie(e,D(t.appId??t.production,t.name)),{method:"DELETE"}),"removing the preview record")},Le=async(r,e,t)=>{const n=`${t}/`,o=[];let s="";for(let a=1;;a+=1){if(a>re)throw new Error(`the preview records of "${t}" span more than ${String(re)} pages`);const c=`prefix=${encodeURIComponent(n)}${s===""?"":`&cursor=${encodeURIComponent(s)}`}`,d=k(await r(`/storage/kv/namespaces/${encodeURIComponent(e)}/keys?${c}`),"listing preview records");o.push(...(Array.isArray(d.result)?d.result:[]).flatMap(f=>typeof f.name=="string"?[f.name]:[]));const u=typeof d.result_info=="object"&&d.result_info!==null?d.result_info:{};if(typeof u.cursor!="string"||u.cursor==="")break;s=u.cursor}const i=await Promise.all(o.map(async a=>{try{return{key:a,record:await ge(r,e,a)}}catch(c){if(c instanceof TypeError)return{key:a,unreadable:!0};throw c}}));return{records:i.flatMap(a=>a.record===void 0?[]:[a.record]),unreadable:i.flatMap(a=>a.unreadable===!0?[a.key]:[])}},W=async(r,e,t)=>{if(e==="d1"){const o=await rr(r,`/d1/database?name=${encodeURIComponent(t)}`,"D1 databases",s=>s.name===t);return typeof o?.uuid=="string"?o.uuid:void 0}if(e==="kv")return me(r,t);const n=await r(`/r2/buckets/${encodeURIComponent(t)}`);if(!(!n.ok&&n.status===404))return k(n,`looking up R2 bucket "${t}"`),t},or=async(r,e,t)=>{if(e==="kv")return tr(r,t);if(e==="r2")return k(await r("/r2/buckets",{body:JSON.stringify({name:t}),method:"POST"}),`creating R2 bucket "${t}"`),t;const{uuid:n}=X(k(await r("/d1/database",{body:JSON.stringify({name:t}),method:"POST"}),`creating D1 database "${t}"`));if(typeof n!="string")throw new TypeError(`creating D1 database "${t}" returned no id`);return n},q="lunora-preview-locks",G=1800,ct=/UNIQUE constraint failed|SQLITE_CONSTRAINT/iu,dt="CREATE TABLE IF NOT EXISTS lunora_preview_locks (app_id TEXT NOT NULL, name TEXT NOT NULL, owner_token TEXT NOT NULL, expires_at INTEGER NOT NULL, PRIMARY KEY (app_id, name))",ut="INSERT INTO lunora_preview_locks (app_id, name, owner_token, expires_at) VALUES (?, ?, ?, unixepoch() + ?)",lt="SELECT owner_token, expires_at, unixepoch() AS now FROM lunora_preview_locks WHERE app_id = ? AND name = ?",ft="UPDATE lunora_preview_locks SET owner_token = ?, expires_at = unixepoch() + ? WHERE app_id = ? AND name = ? AND owner_token = ? AND expires_at <= unixepoch()",wt="DELETE FROM lunora_preview_locks WHERE app_id = ? AND name = ? AND owner_token = ?",E=async(r,e,t,n)=>{const o=await r(`/d1/database/${encodeURIComponent(e)}/query`,{body:JSON.stringify({params:n,sql:t}),method:"POST"});if(!o.ok&&ct.test(o.text))return;const s=k(o,"using the preview lock database").result?.[0];return{changes:typeof s?.meta?.changes=="number"?s.meta.changes:0,rows:Array.isArray(s?.results)?s.results:[]}},pt=4,vt=50,ht=async r=>new Promise(e=>{setTimeout(e,r)}),z="lunora_preview_lock_owner",ir="lunora-preview-locks/v1",mt=`CREATE TABLE IF NOT EXISTS ${z} (marker TEXT NOT NULL)`,gt=`INSERT INTO ${z} (marker) VALUES (?)`,yt="SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",$t=`SELECT marker FROM ${z}`,De=async(r,e)=>((await E(r,e,yt,[z]))?.rows.length??0)===0?!1:(await E(r,e,$t,[]))?.rows.some(o=>o.marker===ir)===!0,sr=async r=>{let e=await W(r,"d1",q),t=!1;if(e===void 0)try{e=await or(r,"d1",q),t=!0}catch(n){if(e=await W(r,"d1",q),e===void 0)throw n}if(t)await E(r,e,mt,[]),await E(r,e,gt,[ir]);else{let n=await De(r,e);for(let o=1;o<=pt&&!n;o+=1)await ht(vt*o),n=await De(r,e);if(!n)throw new Error(`a D1 database named "${q}" exists but was not made by lunora preview (it has no ${z} marker), so it is not used or written to. Rename it, or delete it if it is an empty leftover, then run this again`)}return await E(r,e,dt,[]),e},ar=async(r,e,t,n)=>{const o=V();for(let s=0;s<2;s+=1){if(await E(r,e,ut,[t,n,o,G])!==void 0)return{token:o};const a=(await E(r,e,lt,[t,n]))?.rows[0];if(a===void 0)continue;const c=Number(a.expires_at),d=Number.isFinite(c)&&c<=Number(a.now);return typeof a.owner_token!="string"||!d?{held:new Date(c*1e3).toISOString()}:(await E(r,e,ft,[o,G,t,n,a.owner_token]))?.changes===1?{token:o}:{held:"just now"}}return{held:"just now"}},cr="UPDATE lunora_preview_locks SET expires_at = unixepoch() + ? WHERE app_id = ? AND name = ? AND owner_token = ?",dr=(r,e,t,n,o,s,i=G*1e3/3)=>{let a=Date.now();const c=async()=>{try{(await E(r,e,cr,[G,t,n,o]))?.changes===1?a=Date.now():s()}catch{Date.now()-a>=2*i&&s()}},d=setInterval(()=>{c().catch(()=>{})},i);return d.unref(),()=>{clearInterval(d)}},ur=async(r,e,t,n,o)=>(await E(r,e,cr,[G,t,n,o]))?.changes===1,te=async(r,e,t,n,o)=>{try{await E(r,e,wt,[t,n,o])}catch{}},lr={d1:r=>`/d1/database/${encodeURIComponent(r)}`,kv:r=>`/storage/kv/namespaces/${encodeURIComponent(r)}`,r2:r=>`/r2/buckets/${encodeURIComponent(r)}`},Oe=async(r,e)=>{if(e.pending===!0)return"present";const t=await r(lr[e.kind](e.id));if(!t.ok&&t.status===404)return"missing";const n=X(k(t,`looking up ${e.kind} "${e.name}"`));return{d1:n.name,kv:n.title,r2:n.name}[e.kind]===e.name?"present":"changed"},Pe=10,kt=(r,e)=>{const t=`/r2/buckets/${encodeURIComponent(r)}/objects/`,n=`${t}${e.split("/").map(o=>encodeURIComponent(o)).join("/")}`;return e.split("/").some(o=>o==="."||o==="..")?{error:`the object key ${JSON.stringify(e)} has a "." or ".." segment, which cannot be addressed without leaving the bucket`}:new URL(`https://api.invalid/accounts/a${n}`).pathname.startsWith(`/accounts/a${t}`)?{path:n}:{error:`the object key ${JSON.stringify(e)} resolves outside bucket "${r}"`}},Et=async(r,e,t)=>{const n=kt(e,t);if(n.error!==void 0)throw new Error(`${n.error}; remove the object by hand, then delete the preview again`);const o=await r(n.path,{method:"DELETE"});if(!o.ok&&o.status!==404)throw new Error(`deleting object "${t}" failed (${de(o)})`)},bt=async(r,e)=>{let t="";for(;;){const n=await r(`/r2/buckets/${encodeURIComponent(e)}/objects?per_page=1000`);if(!n.ok&&n.status===404)return;const o=k(n,`listing the objects of R2 bucket "${e}"`),s=(Array.isArray(o.result)?o.result:[]).flatMap(a=>typeof a.key=="string"?[a.key]:[]);if(s.length===0)return;const i=`${String(s.length)}:${s[0]??""}:${s.at(-1)??""}`;if(i===t)throw new Error(`R2 bucket "${e}" still holds the same ${String(s.length)} objects after deleting them`);t=i;for(let a=0;a<s.length;a+=Pe)await Promise.all(s.slice(a,a+Pe).map(async c=>Et(r,e,c)))}},fr=async(r,e)=>{e.kind==="r2"&&await bt(r,e.id);const t=await r(lr[e.kind](e.id),{method:"DELETE"});if(!(t.ok||t.status===404))throw new Error(de(t))},_t=async(r,e)=>{const t=await r(`/workers/scripts/${encodeURIComponent(e)}?force=true`,{method:"DELETE"});if(!t.ok&&t.status!==404)throw new Error(de(t))},ue=async(r,e,t)=>{const n=await r(`/workers/scripts/${encodeURIComponent(e)}/settings`);if(!n.ok&&n.status===404)return{exists:!1};const{bindings:o}=X(k(n,`reading Worker "${e}"`)),s=(Array.isArray(o)?o:[]).find(i=>i.type==="plain_text"&&i.name===t)?.text;return typeof s=="string"?{exists:!0,marker:s}:{exists:!0}},St=async r=>{const{subdomain:e}=X(k(await r("/workers/subdomain"),"reading the workers.dev subdomain"));if(typeof e!="string"||e==="")throw new TypeError("the account has no workers.dev subdomain; register one in the Cloudflare dashboard (Workers & Pages) first");return e},Fe=["deploy","list","delete"],Rt=15e3,It=5,Ot=1e3,we="LUNORA_ADMIN_TOKEN",Tt="LUNORA_PREVIEW_SECRET_SEED",At={celld:"celld has no isolated previews: it has no Wrangler environments, and an app's Durable Objects and storage live in its fleet bucket. Deploy the copy as its own config, with CELLD_BUCKET naming a bucket of its own, and delete that bucket to remove it.",node:"the Node target has no `lunora deploy`, so there is nothing to preview: run a second process with its own database for a copy."},p=(r,e,t=w.USAGE)=>(r.error(e),{code:t,error:e}),C=r=>({environment:r.environment,name:r.name,resources:r.resources,url:r.url,worker:r.worker}),Ct=async(r,e)=>{if(r===void 0)return e();r.begin();try{return await e()}finally{r.end()}},wr=(r,e)=>{const t=pe(r.GITHUB_REPOSITORY)??pe(r.CI_PROJECT_PATH);return t===void 0?void 0:{pullRequest:e,repository:t}},jt=(r,e)=>e!==void 0&&r.origin?.repository===e.repository&&r.origin.pullRequest===e.pullRequest,Ut=r=>`the record for preview "${r}" was not made by a deploy of this pull request in this repository, so the identity the pull request's files name cannot be used to delete it; nothing was deleted`,Nt=(r,e)=>{const t=Se(e.worker,e.resources);return[...B(r.config,t),...r.alsoProduction===void 0?[]:B(r.alsoProduction,t)]},Lt=(r,e,t)=>{Object.assign(r,{origin:wr(e.environment??process.env,t)??r.origin})},Dt=r=>{const e=r.cwd??process.cwd(),t=Ur(e,r.target);if(t.target===void 0)return{error:t.error??"unknown deploy target"};if(t.target!=="cloudflare")return{error:`lunora preview: ${At[t.target]??`target "${t.target}" has no isolated previews.`}`};const n=He(e),o=n===void 0?void 0:$e(n).parsed;return n===void 0||o===void 0?{error:"lunora preview needs a readable wrangler.jsonc (or wrangler.json) in the project root"}:{context:{config:o,cwd:e,appId:ke(e),wranglerPath:n}}},pr=(r,e)=>{if(e.env!==void 0)return ce(r,e.env);const t=zr(r);return t.length>0&&pe((e.environment??process.env).CLOUDFLARE_ACCOUNT_ID)===void 0?{error:`environment(s) ${t.join(", ")} name an account_id other than the top level's, so the account to look in is not clear: pass --env <name> to use that environment's account, or set CLOUDFLARE_ACCOUNT_ID`}:{config:r}},Te=(r,e)=>{const{accountId:t,token:n}=Ar(r.environment??process.env,e.account_id);if(t===void 0||n===void 0)return{error:"lunora preview needs CLOUDFLARE_API_TOKEN (Workers Scripts, D1, Workers KV Storage and Workers R2 Storage: Edit) and CLOUDFLARE_ACCOUNT_ID (or `account_id` in wrangler.jsonc)"};const o=r.sleep??(async s=>new Promise(i=>{setTimeout(i,s)}));return{request:async(s,i)=>{for(let a=0;;a+=1){const c=await Cr({accountId:t,apiToken:n,...r.fetch===void 0?{}:{fetch:r.fetch},init:{...i,signal:AbortSignal.timeout(Rt)},path:s});if(c.ok||c.status!==429||a===It)return c;await o(Ot*2**a)}}}},Pt=r=>{const e={};for(const t of r){const n=t.indexOf("="),o=n===-1?"":t.slice(0,n);if(o==="")return{error:`--var ${t}: expected KEY=VALUE`};if(o===U)return{error:`--var ${o}: the preview sets ${o} itself; \`delete\` relies on it`};e[o]=t.slice(n+1)}return{variables:e}},$=r=>r instanceof Error?r.message:String(r),Ft=async(r,e,t,n)=>{const o=n.environment??process.env,s={},i=o[Tt],a=h=>i===void 0||i===""?Ir():yr("sha256",i).update(`${t}/${h}`).digest("hex");for(const h of n.secrets??[]){const y=o[h];if(y===void 0||y==="")return{error:`--secret ${h}: ${h} is not set in the environment, so there is no value to give the preview`};s[h]=y}const{fromLocal:c,fromPackages:d,mintableKeys:u}=await Dr(r,void 0);for(const h of[...d,...c,we])s[h]===void 0&&(u.has(h)||h===we)&&(s[h]=a(h));const f=[...new Set([...d,...c])].filter(h=>s[h]===void 0),m=(Array.isArray(e.secrets?.required)?e.secrets.required.filter(h=>typeof h=="string"):[]).filter(h=>s[h]===void 0);return m.length>0?{error:`the config requires ${m.join(", ")}, which a preview only gets when you pass ${m.map(h=>`--secret ${h}`).join(" ")}`}:{adminToken:s[we]??"",missing:f,values:s}},Mt=async(r,e,t,n)=>{const o=await Oe(r,t);if(o!=="present")throw new Error(`the record lists ${t.kind} "${t.name}" (${t.id}), which is ${o==="missing"?"gone":"now a different resource"}; run \`lunora preview delete ${e.slug}\`, then deploy again`);return n.info(`reusing preview ${t.kind} "${t.name}" for ${t.binding}`),t.id},Wt=3,xt=async(r,e,t,n)=>{let o;for(let s=1;s<=Wt;s+=1)try{await j(r,e,t);return}catch(i){o=i}try{await fr(r,n),t.resources.splice(t.resources.indexOf(n),1),await j(r,e,t).catch(()=>{})}catch{n.pending=!0}throw new Error(`${$(o)}; the ${n.kind} "${n.name}" just made could not be recorded, so it was removed again`)},Me=async(r,e,t,n,o)=>{let s=t.resources.find(i=>i.kind===n.kind&&i.binding===n.binding);if(s===void 0){if(await W(r,n.kind,n.name)!==void 0)throw new Error(`a ${n.kind} resource named "${n.name}" already exists but no preview record says this preview created it; refusing to bind or later delete it. Rename or remove it, then deploy again.`);s={binding:n.binding,id:n.kind==="r2"?n.name:"",kind:n.kind,name:n.name,pending:!0},t.resources.push(s),await j(r,e,t)}return s.id=await or(r,n.kind,n.name),delete s.pending,await xt(r,e,t,s),o.success(`created preview ${n.kind} "${n.name}" for ${n.binding}`),s.id},Kt=async(r,e,t,n,o,s,i)=>{const a=n.resources.find(c=>c.kind===o.kind&&c.binding===o.binding);if(i(),a?.pending===!0){if(await W(r,o.kind,o.name)!==void 0)throw new Error(`a ${o.kind} resource named "${o.name}" exists, but the record only claims it (a deploy was interrupted between creating it and confirming it), so nothing shows it is this preview's and it is left alone. If it is, delete it by hand, then deploy again`);return Me(r,e,n,o,s)}return a===void 0?Me(r,e,n,o,s):Mt(r,t,a,s)},Ht=async(r,e,t,n,o,s)=>{const i=new Map;for(const a of t.resources)i.set(_e(a.kind,a.binding),await Kt(r,e,t,n,a,o,s));return i},Ae=async(r,e,t)=>{const n=new Set((t?.resources??[]).map(i=>`${i.kind}:${i.name}`)),o=e.filter(i=>!n.has(`${i.kind}:${i.name}`));return(await Promise.all(o.map(async i=>await W(r,i.kind,i.name)===void 0?[]:[i]))).flat().map(i=>`${i.kind} "${i.name}"`)},vr=async(r,e,t,n,o,s,i=!0)=>{if(t!==void 0){const d=await ge(r,e,D(t,o));if(d!==void 0){if(d.appId!==t||d.name!==o)throw new Error(`the record at "${D(t,o)}" does not name this project and preview; refusing to act on it`);return d}}const a=await ge(r,e,D(n,o));if(a===void 0)return;if(a.appId!==void 0||a.production!==n||a.name!==o||a.worker!==ae(n,o))throw new Error(`the record at "${D(n,o)}" is not the one this CLI wrote for that Worker and preview; refusing to act on it`);if(t===void 0)throw new Error(`preview "${o}" is recorded under the Worker name, from before projects had an id. Add an app id to package.json (\`"lunora": { "appId": "${V()}" }\`), commit it, and run this again: it re-keys the record`);const c={...a,appId:t};return i&&(await j(r,e,c),await nr(r,e,a),s.warn(`preview "${o}" was recorded under the Worker name "${n}"; it is now recorded under project ${t}`)),c},Bt=async(r,e,t,n)=>{if(t===void 0&&n)return{code:w.USAGE,error:`a Worker named "${e.worker}" exists but no preview record says this preview deployed it; refusing to deploy over it`};if(t!==void 0&&t.worker!==e.worker)return{code:w.USAGE,error:`the record for preview "${e.slug}" names Worker "${t.worker}", not "${e.worker}" (the app was renamed since it was deployed); run \`lunora preview delete ${e.slug}\` first, then deploy it again`};const o=await Ae(r,e.resources,t);return o.length===0?void 0:{code:w.FAILURE,error:`${o.join(", ")} already exist(s) but no preview record read says this preview created it, so nothing was written or deployed. If a deploy of this preview just ran, its record may not have reached this location yet: run the deploy again. Otherwise rename or remove it, then deploy again.`}},Gt=r=>{const e=Er(ee(br(),"lunora-preview-")),t=ee(e,"secrets.json");return ne(t,JSON.stringify(r),{encoding:"utf8",mode:384}),{directory:e,path:t}},Vt=r=>({environment:r.environment,name:r.slug,resources:r.resources,worker:r.worker}),Yt=(r,e,t)=>{r.info(`dry run: preview "${e.slug}" would deploy Worker "${e.worker}" (env.${e.environment}, its own Durable Object namespace)`);for(const s of e.resources)r.info(` ${s.kind} "${s.name}" for ${s.binding} (created, or reused when this preview's record lists it)`);const{vars:n}=t,o=n===void 0?t:{...t,vars:Object.fromEntries(Object.keys(n).map(s=>[s,"<redacted>"]))};r.info(` env.${e.environment}: ${JSON.stringify(o)}`)},hr=(r,e)=>`another run is deploying or deleting preview "${r}" (its hold lapses at ${e}); wait for it. A run that crashed lets go by itself`,mr=(r,e,t)=>{const n=`the hold on preview "${e}" was lost while this run worked, so another run may have changed it too; check it with \`lunora preview list\` and run \`lunora preview delete ${e}\` if it looks wrong`;return r.error(n),{...t,code:t.code===w.SUCCESS?w.FAILURE:t.code,error:n}},Jt=async(r,e,t)=>{try{const n=await ue(r,e,U);return n.exists&&n.marker===t?"present":"absent"}catch{return"unknown"}},ye=(r,e)=>{if(r.lost)throw new Error(`the hold on preview "${e}" can no longer be vouched for, so this run stopped before changing anything more; check it with \`lunora preview list\`, then run again`)},Xt=async(r,e,t,n,o,s)=>{const{config:i,cwd:a,wranglerPath:c}=r,{logger:d}=n,{appId:u,hold:f,namespace:l,secrets:m,subdomain:h}=s;let y,_;const v=`https://${e.worker}.${h}.workers.dev`;try{const g=await ue(o,e.worker,U);if(g.exists&&g.marker!==M(e))return p(d,`a Worker named "${e.worker}" already exists and was not deployed as preview "${e.slug}"; refusing to deploy over it`);const b=new Date().toISOString(),T=await vr(o,l,u,e.production,e.slug,d),A=await Bt(o,e,T,g.exists);if(A!==void 0)return p(d,A.error,A.code);const N=B(r.committed??i,Se(e.worker,T?.resources??[]));if(N.length>0)return p(d,`preview "${e.slug}" would bind production's ${N.join(", ")}; refusing to deploy, nothing was created`);y=T??{createdAt:b,environment:e.environment,name:e.slug,production:e.production,appId:u,...e.sourceEnv===void 0?{}:{sourceEnv:e.sourceEnv},resources:[],updatedAt:b,version:1,worker:e.worker},Lt(y,n,e.slug),await f.verify(),await j(o,l,y),_=await Ht(o,l,e,y,d,()=>{ye(f,e.slug)})}catch(g){return p(d,`preview "${e.slug}": ${$(g)}`,w.FAILURE)}const O=B(r.committed??i,[..._].map(([g,b])=>({kind:g.slice(0,g.indexOf(":")),value:b})));if(O.length>0)return p(d,`preview "${e.slug}" would bind production's ${O.join(", ")}; refusing to deploy`);const x=L(c,"utf8");ne(c,Tr(x,["env",e.environment],qe(i,e,_,t,v)),"utf8");const S=Gt(m.values),K=ie(()=>{F(S.directory,{force:!0,recursive:!0})});try{ye(f,e.slug);const g=await Ct(r.baseline,async()=>Pr({allowSchemaDrift:!0,cwd:a,env:e.environment,fetchImpl:n.adminFetch,format:n.format,healthCheck:!0,healthFetch:n.healthFetch,healthSleep:n.healthSleep,interactive:!1,isolatedPreview:!0,logger:d,migrate:!0,migrateToken:m.adminToken,migrateUrl:v,migrateYes:!0,secretsFile:S.path,spawner:n.spawner,target:"cloudflare"})),b=g.code===0?"present":await Jt(o,e.worker,M(e));if(b==="present"?y.url=v:b==="absent"&&delete y.url,y.updatedAt=new Date().toISOString(),await f.verify(),await j(o,l,y),g.code!==0)return{code:g.code,data:{preview:C(y)},error:g.error??`preview "${e.slug}" did not deploy`}}catch(g){return p(d,`preview "${e.slug}": ${$(g)}`,w.FAILURE)}finally{F(S.directory,{force:!0,recursive:!0}),K()}if(n.seed===!0){const g=await Fr({cwd:a,fetchImpl:n.adminFetch,format:n.format,logger:d,prod:!0,token:m.adminToken,url:y.url,yes:!0});if(g.code!==0)return{code:g.code,data:{preview:C(y)},error:g.error??`seeding preview "${e.slug}" failed`}}return d.success(`preview "${e.slug}" is live at ${y.url??v} — remove it with \`lunora preview delete ${e.slug}\``),{code:w.SUCCESS,data:{preview:C(y)}}},zt=async(r,e,t,n,o)=>{const{appId:s,config:i,cwd:a}=r,{logger:c}=n;if(s===void 0)return p(c,"the project has no id to key the preview record by");const d=await Ft(a,i,e.worker,n);if(d.error!==void 0)return p(c,d.error);for(const v of d.missing)c.warn(`the preview has no ${v}: production's is never copied. Pass --secret ${v} with ${v} set in the environment to give it one.`);let u,f,l,m;try{f=await St(o),u=await Re(o,!0)??"",l=await sr(o);const v=await ar(o,l,s,e.slug);if(v.held!==void 0)return p(c,hr(e.slug,v.held));({token:m}=v)}catch(v){return p(c,`preview "${e.slug}": ${$(v)}`,w.FAILURE)}const h={lost:!1,verify:async()=>{if(!await ur(o,l,s,e.slug,m))throw h.lost=!0,new Error(`the hold on preview "${e.slug}" is no longer this run's, so its record was left as the other run has it`)}},y=dr(o,l,s,e.slug,m,()=>{h.lost=!0}),_=ie(async()=>{y(),await te(o,l,s,e.slug,m)});try{const v=await Xt(r,e,t,n,o,{appId:s,hold:h,namespace:u,secrets:d,subdomain:f});return h.lost?mr(c,e.slug,v):v}finally{y(),await te(o,l,s,e.slug,m),_()}},Qt=(r,e)=>{if(r.appId!==void 0)return{appId:r.appId};const t=e.environment??process.env,n='package.json has a "lunora.appId" that is not a UUID, or no `lunora` object this CLI can add one to';if(e.dryRun===!0)return e.logger.info(" app id: none yet; the first deploy writes one to package.json for you to commit"),{appId:void 0};if(t.CI!==void 0&&t.CI!==""&&t.CI!=="false")return{error:`package.json has no "lunora.appId", and a CI run cannot mint one (its checkout is discarded, so the preview could never be found again to delete). Commit \`"lunora": { "appId": "${V()}" }\` to package.json`};const o=Vr(r.cwd);return o===void 0?{error:n}:(e.logger.warn(`wrote an app id to package.json ("lunora.appId"); commit it so CI finds this project's previews`),{appId:o})},Zt=async(r,e,t)=>{const{cwd:n}=r,{logger:o}=e,s=Pt(e.vars??[]);if(s.error!==void 0)return p(o,s.error);const{variables:i}=s,a=Wr(n);if(a.error!==void 0)return p(o,a.error);const c=Qt(r,e);if(c.error!==void 0)return a.release(),p(o,c.error);let d=()=>{};const u=ee(n,"lunora",Sr);let f;try{f=Q(u)?L(u):void 0}catch(v){return a.release(),p(o,`could not read ${u}: ${$(v)}`)}let l=!1,m;const h={begin:()=>{l=!0},end:()=>{m=Q(u)?L(u):void 0,l=!1}},y=()=>{try{if(d(),f===void 0){const v=Q(u)?L(u):void 0;v!==void 0&&(l||m!==void 0&&v.equals(m))&&F(u,{force:!0})}else f.equals(Q(u)?L(u):Buffer.alloc(0))||ne(u,f)}finally{a.release()}},_=ie(y);try{d=Nr(n),await Lr(n,o,void 0,"cloudflare",void 0);const{services:v}=Rr(n);if(v.length>0)return p(o,`this app cannot be previewed in isolation yet: it declares lunora.config services (${v.map(A=>A.name).join(", ")}), which a preview would share with production`);const O=$e(r.wranglerPath).parsed??r.config,x=e.env===void 0?{config:O}:ce(O,e.env);if(x.error!==void 0)return p(o,x.error);const{config:S}=x,K=Ze(S,t,e.env);if(K.error!==void 0)return p(o,K.error);const{plan:g}=K,b=B(O,Se(g.worker,g.resources));if(b.length>0)return p(o,`preview "${g.slug}" names collide with production's ${b.join(", ")}; name the preview differently`);if(e.dryRun===!0){const A=new Map(g.resources.map(N=>[_e(N.kind,N.binding),`<${N.name}>`]));return Yt(o,g,qe(S,g,A,i,`https://${g.worker}.<your-subdomain>.workers.dev`)),o.info(` secrets: every one Lunora can mint, fresh${(e.secrets??[]).length>0?`; from the environment: ${(e.secrets??[]).join(", ")}`:""}`),{code:w.SUCCESS,data:{plan:Vt(g)}}}const T=Te(e,S);return T.error!==void 0?p(o,T.error,w.AUTH):await zt({...r,baseline:h,committed:O,config:S,appId:c.appId},g,i,e,T.request)}finally{y(),_()}},qt=async(r,e,t)=>{try{if(e.pending===!0)return await W(r,e.kind,e.name)===void 0?!0:(t.error(`${e.kind} "${e.name}" exists, but the record only claims it (a deploy was interrupted before confirming it), so nothing shows it is this preview's: it was left alone. If it is, delete it by hand, then run \`lunora preview delete\` again`),!1);const n=await Oe(r,e);return n==="changed"?(t.error(`${e.kind} "${e.name}" (${e.id}): the recorded id now names a different resource, so it was left alone`),!1):(n==="present"&&(await fr(r,e),t.success(`deleted ${e.kind} "${e.name}"`)),!0)}catch(n){return t.error(`deleting ${e.kind} "${e.name}" failed (${$(n)}); it stays on record for the next \`lunora preview delete\``),!1}},en=async(r,e,t,n,o,s,i)=>{try{s();const u=await ue(r,t.worker,U);if(u.exists&&u.marker!==M({production:t.production,slug:t.name}))return p(o,`Worker "${t.worker}" was not deployed as preview "${t.name}"; refusing to delete anything`);u.exists&&(await _t(r,t.worker),o.success(`deleted Worker "${t.worker}"`))}catch(u){return p(o,`deleting Worker "${t.worker}" failed (${$(u)}); its resources were left in place`,w.FAILURE)}const a=[];for(const u of t.resources)s(),await qt(r,u,o)||a.push(u);const c={...t,resources:a,updatedAt:new Date().toISOString()};let d;try{d=await Ae(r,n?.resources??[],t),d.length===0&&(await i(),await(a.length===0?nr(r,e,t):j(r,e,c)))}catch(u){return p(o,`preview "${t.name}": ${$(u)}`,w.FAILURE)}return d.length>0?p(o,`${d.join(", ")} exist(s) under this preview's names but not in the record read (a deploy may have just recorded it); the record was left as it is — run \`lunora preview delete ${t.name}\` again. If that keeps failing, the resource was never recorded (a deploy stopped between creating and recording it): delete it by hand, then run delete once more`,w.FAILURE):a.length>0?{code:w.FAILURE,data:{preview:C(c)},error:`${String(a.length)} preview resource(s) could not be deleted`}:{code:w.SUCCESS,data:{deleted:C(t)}}},rn=async(r,e,t)=>{const n=[{config:e}];for(const s of Object.keys(e.env??{})){const i=ce(e,s);i.config!==void 0&&n.push({config:i.config,env:s})}const o=await Promise.all(n.map(async s=>{const{names:i}=be(s.config,t);if(i===void 0)return[];const a=Qe(s.config,i.worker),c=await ue(r,i.worker,U),d=await Ae(r,a,void 0);return[...c.exists&&c.marker===M(i)?[`Worker "${i.worker}" exists`]:[],...d.map(u=>`${u} exists`)]}));return[...new Set(o.flat())]},tn=(r,e)=>{const t=e.sourceEnv===void 0?{config:r}:ce(r,e.sourceEnv);return t.config===void 0?void 0:Ze({...t.config,name:e.production},e.name,e.sourceEnv).plan},nn=async(r,e,t,n)=>{const o=await rn(r,e,t);return o.length===0?void 0:p(n,`no record of preview "${t}" could be read, but ${o.join(" and ")}; nothing was deleted. If a deploy just ran, its record may not have reached this location yet: run delete again. Otherwise delete from a checkout whose package.json has the lunora.appId that deployed it`,w.FAILURE)},on=(r,e)=>{e.info(`dry run: would delete Worker "${r.worker}" and its Durable Objects`);for(const t of r.resources)e.info(` ${t.kind} "${t.name}" (${t.binding})`);return{code:w.SUCCESS,data:{plan:C(r)}}},sn=(r,e,t)=>e.worker===ae(e.production,e.name)&&e.appId===r.appId&&e.name===t,We=async(r,e,{hold:t,names:n,namespace:o,request:s})=>{const{config:i}=r,{logger:a}=e;let c;try{if(c=o===void 0?void 0:await vr(s,o,r.appId,n.production,n.slug,a,e.dryRun!==!0),o===void 0||c===void 0){const l=await nn(s,i,n.slug,a);if(l!==void 0)return l}}catch(l){return p(a,`preview "${n.slug}": ${$(l)}`,w.FAILURE)}if(o===void 0||c===void 0)return p(a,`no preview "${n.slug}" of "${n.production}" is on record; nothing was deleted`,w.NOT_FOUND);const d=Nt(r,c);if(!sn(r,c,n.slug)||d.length>0)return p(a,`the record for preview "${n.slug}" names ${d.length>0?`production's ${d.join(", ")}`:`Worker "${c.worker}", which is not the Worker of this preview`}; refusing to delete anything`);if(r.requireOrigin===!0&&!jt(c,wr(e.environment??process.env,n.slug)))return p(a,Ut(n.slug),w.FAILURE);let u;try{u=await Promise.all(c.resources.map(async l=>Oe(s,l)))}catch(l){return p(a,`preview "${n.slug}": ${$(l)}`,w.FAILURE)}const f=c.resources.filter((l,m)=>u[m]==="changed");return f.length>0?p(a,`the record for preview "${n.slug}" lists ${f.map(l=>`${l.kind} ${l.id}`).join(", ")}, which no longer name(s) the recorded resource; refusing to delete anything`):e.dryRun===!0?on(c,a):en(s,o,c,tn(i,c),a,()=>{ye(t,c.name)},t.verify)},P=async(r,e,t)=>{const{config:n}=r,{logger:o}=e,s=be(n,t);if(s.error!==void 0)return p(o,s.error);const{names:i}=s,a=pr(n,e);if(a.error!==void 0)return p(o,a.error,e.env===void 0?w.AUTH:w.USAGE);const c=Te(e,a.config);if(c.error!==void 0)return p(o,c.error,w.AUTH);const{request:d}=c;let u;try{u=await Re(d,!1)}catch(v){return p(o,`preview "${i.slug}": ${$(v)}`,w.FAILURE)}if(r.appId===void 0||u===void 0||e.dryRun===!0)return We(r,e,{hold:{lost:!1,verify:()=>Promise.resolve()},names:i,namespace:u,request:d});const{appId:f}=r;let l,m;try{l=await sr(d);const v=await ar(d,l,f,i.slug);if(v.held!==void 0)return p(o,hr(i.slug,v.held));({token:m}=v)}catch(v){return p(o,`preview "${i.slug}": ${$(v)}`,w.FAILURE)}const h={lost:!1,verify:async()=>{if(!await ur(d,l,f,i.slug,m))throw h.lost=!0,new Error(`the hold on preview "${i.slug}" is no longer this run's, so its record was left as the other run has it`)}},y=dr(d,l,f,i.slug,m,()=>{h.lost=!0}),_=ie(async()=>{y(),await te(d,l,f,i.slug,m)});try{const v=await We(r,e,{hold:h,names:i,namespace:u,request:d});return h.lost?mr(o,i.slug,v):v}catch(v){return p(o,`preview "${i.slug}": ${$(v)}`,w.FAILURE)}finally{y(),await te(d,l,f,i.slug,m),_()}},an=async(r,e)=>{const{config:t}=r,{logger:n}=e,o=typeof t.name=="string"?t.name:"",s=pr(t,e);if(s.error!==void 0)return p(n,s.error,e.env===void 0?w.AUTH:w.USAGE);const i=Te(e,s.config);if(i.error!==void 0)return p(n,i.error,w.AUTH);let a;try{const c=await Re(i.request,!1),d={records:[],unreadable:[]},u=c===void 0||r.appId===void 0?d:await Le(i.request,c,r.appId),f=c===void 0?d:await Le(i.request,c,o);a=[...u.records,...f.records],f.records.length>0&&n.warn(`${String(f.records.length)} preview(s) are recorded under the Worker name, from before projects had an id: ${f.records.map(l=>l.name).join(", ")}. ${r.appId===void 0?"Add an app id to package.json, then run":"Run"} \`lunora preview delete <name>\` (or deploy it again) to re-key each`);for(const l of[...u.unreadable,...f.unreadable])n.warn(`the registry key "${l}" is not a preview record this CLI wrote; it is not listed, and \`delete\` will refuse it`)}catch(c){return p(n,$(c),w.FAILURE)}a.length===0&&n.info(`no previews of "${o}" are on record`);for(const c of a)n.info(`${c.name} ${c.url??"(not deployed)"} Worker ${c.worker}, ${String(c.resources.length)} resource(s), updated ${c.updatedAt}`);return{code:w.SUCCESS,data:{previews:a.map(c=>C(c))}}},cn=/^[\dA-Za-z][\w-]{0,62}$/u,dn=r=>{const e=ke(r);if(e===void 0)return{error:'its package.json has no readable "lunora.appId"'};const t=He(r);let n;try{n=t===void 0?void 0:$e(t).parsed}catch{n=void 0}const o=n?.name;return n===void 0||typeof o!="string"||!cn.test(o)?{error:"its wrangler config has no readable, valid Worker name"}:{identity:{appId:e,config:n,production:o}}},un=async(r,e,t,n)=>{const o=[...e];if(o.some(i=>i.code===w.SUCCESS)){for(const[i,a]of o.entries())if(a.code!==w.SUCCESS&&a.code!==w.NOT_FOUND){const c=await P(r[i],t,n);o[i]=c.code===w.NOT_FOUND?{code:w.SUCCESS}:c}}const s=o.find(i=>i.code!==w.SUCCESS&&i.code!==w.NOT_FOUND);return s!==void 0?s:o.find(i=>i.code===w.SUCCESS&&i.data!==void 0)??o.find(i=>i.code===w.SUCCESS)??p(t.logger,`no preview "${n}" is on record under this checkout's project or under the pull request head's, whose id or Worker name differs; nothing was deleted`,w.FAILURE)},ln=async(r,e,t,n)=>{const o=dn(n);if(o.error!==void 0){e.logger.warn(`the pull request head's identity could not be read (${o.error}); looking for the preview under this checkout's only`);const l=await P(r,e,t);return l.code===w.NOT_FOUND?p(e.logger,`no preview "${t}" is on record under this checkout's project, and the pull request head's identity could not be read (${o.error}), so it cannot be ruled out under that; nothing was deleted`,w.FAILURE):l}const{appId:s,config:i,production:a}=o.identity,c={...r,alsoProduction:i};if(s===r.appId&&a===r.config.name)return P(c,e,t);const d={...c,appId:s,config:{...r.config,...typeof i.account_id=="string"?{account_id:i.account_id}:{},name:a},requireOrigin:!0},u=[c,d],f=[await P(c,e,t),await P(d,e,t)];return un(u,f,e,t)},fn=async r=>{const{logger:e,subcommand:t}=r;if(!Fe.includes(t))return p(e,`usage: lunora preview <${Fe.join(" | ")}> [name]`);const n=Dt(r);return n.error!==void 0?p(e,n.error):r.headDir!==void 0&&t!=="delete"?p(e,"--head-dir applies to `lunora preview delete` only"):t==="list"?an(n.context,r):r.name===void 0||r.name===""?p(e,`usage: lunora preview ${t??""} <name> — e.g. the pull request number`):t==="deploy"?Zt(n.context,r,r.name):r.headDir===void 0?P(n.context,r,r.name):ln(n.context,r,r.name,r.headDir)},Rn=jr(async({argument:r,cwd:e,format:t,logger:n,options:o})=>{const s=await fn({cwd:e,dryRun:o.dryRun===!0,env:o.env,format:t,headDir:o.headDir,logger:n,name:r[1],secrets:o.secret??[],seed:o.seed===!0,subcommand:r[0],target:o.target,vars:o.var});return{code:s.code,data:s.data,error:s.error}});export{Rn as execute,fn as runPreviewCommand};
1
+ import{existsSync as O,readFileSync as k}from"node:fs";import{writeFile as I,mkdir as U}from"node:fs/promises";import{parseSchema as B,applyAdditiveEdit as Y,projectUsesUmbrella as q}from"@lunora/config";import{join as E}from"@visulima/path";import{d as F}from"../packem_shared/command-BpMv2n0o.mjs";import{EXIT_CODE as T}from"../packem_shared/EXIT_CODE-DQVhSE-1.mjs";import{createRequire as P}from"node:module";import{LunoraError as S}from"@lunora/errors";import{addArgsFor as H,detectPackageManager as K}from"@lunora/config/cli";import{l as f}from"../packem_shared/js-literal-FWLi94Lh.mjs";const j=/^\//,G=(e,r)=>{try{const{args:n,command:t}=H(K(e),[r],{dev:!0});return`${t} ${n.join(" ")}`}catch{return`<your-package-manager> add -D ${r}`}},L=(e,r,n)=>{const t=P(E(n,"noop.cjs"));try{t.resolve(e)}catch{throw new S("INTERNAL",`\`lunora introspect\` needs the \`${r}\` driver to read this database, and it isn't installed.
2
+
3
+ Install it in your project:
4
+
5
+ ${G(n,r)}`)}return t(e)},Q=e=>{const r=e.slice(0,Math.max(0,e.indexOf(":"))).toLowerCase();if(r==="postgres"||r==="postgresql")return"postgres";if(r==="mysql"||r==="mariadb")return"mysql";throw new S("INTERNAL",`Unrecognised database URL scheme \`${r}\`. Expected a \`postgres://\`, \`postgresql://\`, \`mysql://\`, or \`mariadb://\` connection string.`)},W=e=>{try{const r=new URL(e).pathname.replace(j,"");return r===""?void 0:decodeURIComponent(r)}catch{return}},J=async(e,r,n,t=process.cwd())=>{if(r==="postgres"){const m=L("pg","pg",t).Client,u=new m({connectionString:e});return await u.connect(),{close:async()=>{await u.end()},execute:async(l,p)=>(await u.query(l,[...p])).rows,schema:n??"public"}}const a=L("mysql2/promise","mysql2",t).createConnection,o=await a(e),i=n??W(e);if(i===void 0)throw new S("INTERNAL","A MySQL connection string must name a database (`mysql://host/<database>`), or pass `--schema`.");return{close:async()=>{await o.end()},execute:async(c,m)=>{const[u]=await o.execute(c,[...m]);return Array.isArray(u)?u:[]},schema:i}},h=new Set(["_creationTime","_id"]),X={bigint:"v.bigint()",bigserial:"v.bigint()",bit:"v.string()",bool:"v.boolean()",boolean:"v.boolean()",box:"v.string()",bpchar:"v.string()",bytea:"v.bytes()",char:"v.string()",character:"v.string()","character varying":"v.string()",cidr:"v.string()",circle:"v.string()",citext:"v.string()",date:"v.date()","double precision":"v.number()",float4:"v.number()",float8:"v.number()",inet:"v.string()",int:"v.number()",int2:"v.number()",int4:"v.number()",int8:"v.bigint()",integer:"v.number()",interval:"v.string()",json:"v.any()",jsonb:"v.any()",line:"v.string()",lseg:"v.string()",macaddr:"v.string()",money:"v.number()",name:"v.string()",numeric:"v.number()",path:"v.string()",point:"v.string()",polygon:"v.string()",real:"v.number()",serial:"v.number()",smallint:"v.number()",smallserial:"v.number()",text:"v.string()",time:"v.string()","time with time zone":"v.string()","time without time zone":"v.string()",timestamp:"v.timestamp()","timestamp with time zone":"v.timestamp()","timestamp without time zone":"v.timestamp()",timestamptz:"v.timestamp()",tsquery:"v.string()",tsvector:"v.string()",uuid:"v.string()",varbit:"v.string()",varchar:"v.string()",xml:"v.string()"},z={bigint:"v.bigint()",binary:"v.bytes()",bit:"v.number()",blob:"v.bytes()",char:"v.string()",date:"v.date()",datetime:"v.timestamp()",decimal:"v.number()",double:"v.number()",enum:"v.string()",float:"v.number()",int:"v.number()",integer:"v.number()",json:"v.any()",longblob:"v.bytes()",longtext:"v.string()",mediumblob:"v.bytes()",mediumint:"v.number()",mediumtext:"v.string()",numeric:"v.number()",set:"v.string()",smallint:"v.number()",text:"v.string()",time:"v.string()",timestamp:"v.timestamp()",tinyblob:"v.bytes()",tinyint:"v.number()",tinytext:"v.string()",varbinary:"v.bytes()",varchar:"v.string()",year:"v.number()"},w=(e,r)=>{const t=(r==="postgres"?X:z)[e.dataType],s=e.references===void 0?t:`v.id(${JSON.stringify(e.references.table)})`;let a=s??"v.any()";for(let o=0;o<e.arrayDepth;o+=1)a=`v.array(${a})`;return e.nullable&&(a=`v.optional(${a})`),{expression:a,known:s!==void 0}},M=/^[A-Z_$][\w$]*$/i,Z=/[^\dA-Z]+(.)?/gi,V=/^\d+/,ee=/[^\w-]/g,N=e=>M.test(e)?e:f(e),ne=e=>M.test(e)?`.${e}`:`[${f(e)}]`,x=e=>e.replaceAll("*/",String.raw`*\/`),te=e=>{const r=e.replaceAll(ee,"_");return r===""?"table":r},re=e=>{const n=e.replaceAll(Z,(t,s)=>s?.toUpperCase()??"").replace(V,"");return n===""?"table":`${n.charAt(0).toLowerCase()}${n.slice(1)}`},D=e=>e.columns.filter(r=>!h.has(r.name)),ae=e=>{const r=new Set(e.primaryKey);for(const n of e.indexes)for(const t of n.columns)r.add(t);for(const n of e.columns)n.references!==void 0&&r.add(n.name);return[...r].filter(n=>!h.has(n))},C=(e,r,n,t)=>e.references===void 0||r.has(e.references.table)?e:(t.push(`${n}.${e.name}: references \`${e.references.table}\`, which isn't in the generated schema — emitted as a plain column instead of \`v.id(...)\`.`),{arrayDepth:e.arrayDepth,dataType:e.dataType,name:e.name,nullable:e.nullable}),se=(e,r,n,t)=>{const s=[];for(const o of e.columns){if(h.has(o.name)){t.push(`${e.name}.${o.name}: skipped — \`${o.name}\` is a Lunora system column. Rename it in the source database or map it by hand.`);continue}const i=C(o,n,e.name,t),{expression:c,known:m}=w(i,r);!m&&i.references===void 0&&(t.push(`${e.name}.${o.name}: no mapping for SQL type \`${o.dataType}\` — emitted as \`v.any()\`.`),s.push(` // TODO: \`${x(o.dataType)}\` has no direct validator; narrow this.`)),s.push(` ${N(o.name)}: ${c},`)}const a=[' .global({ backend: "hyperdrive" })'];if(e.primaryKey.length>0&&e.primaryKey.every(o=>!h.has(o))){const o=e.primaryKey.map(i=>f(i)).join(", ");a.push(` .index(${f(`by_${e.primaryKey.join("_")}`)}, [${o}], { unique: true })`)}for(const o of e.indexes){const i=o.columns.filter(u=>!h.has(u));if(i.length===0)continue;const c=i.map(u=>f(u)).join(", "),m=o.unique?", { unique: true }":"";a.push(` .index(${f(o.name)}, [${c}]${m})`)}return` ${N(e.name)}: defineTable({
6
+ ${s.join(`
7
+ `)}
8
+ })
9
+ ${a.join(`
10
+ `)},`},oe=(e,r,n)=>{const t=new Set(e.tables.map(a=>a.name)),s=e.tables.map(a=>se(a,e.dialect,t,n)).join(`
11
+ `);return`/**
12
+ * Generated by \`lunora introspect\` from an existing ${e.dialect==="postgres"?"Postgres":"MySQL"} database.
13
+ *
14
+ * This file is a STARTING POINT, not a build artifact — it is written once and is
15
+ * yours to edit. Review it before shipping: column types are mapped
16
+ * conservatively, and every table is \`.global({ backend: "hyperdrive" })\` because
17
+ * its rows live in the external database. Re-running \`lunora introspect\` will not
18
+ * overwrite it unless you pass \`--force\`.
19
+ */
20
+ import { defineSchema, defineTable, v } from "${r.serverImport}";
21
+
22
+ export default defineSchema({
23
+ ${s}
24
+ });
25
+ `},ie=(e,r,n,t=new Set([e.name]),s=[])=>{const a=re(e.name),o=ae(e),i=new Map(D(e).map(l=>[l.name,l])),c=o.map(l=>{const p=i.get(l);if(p===void 0)return;const v=C({...p,nullable:!1},t,e.name,[]),{expression:d,known:A}=w(v,r);return!A&&v.references===void 0&&s.push(`${e.name}.${l}: filter falls back to \`v.any()\` (no mapping for SQL type \`${p.dataType}\`).`),` ${N(l)}: ${d},`}).filter(l=>l!==void 0),m=[f("_creationTime"),...o.map(l=>f(l))].join(", "),u=`ctx.db${ne(e.name)}`;return`/**
26
+ * Generated by \`lunora introspect\` for the \`${x(e.name)}\` table — a
27
+ * starting point you own and edit.
28
+ *
29
+ * Both procedures are RPC-only. Add \`.expose({ rest: true })\` once you've decided
30
+ * this data should be public, and gate them with your auth/RLS middleware first —
31
+ * \`introspect\` cannot know who is allowed to read this table.
32
+ */
33
+ import { defineListArgs, v } from "${n.serverImport}";
34
+
35
+ import type { Doc } from "./_generated/dataModel";
36
+ import { c } from "./_generated/server";
37
+
38
+ const ${a}List = defineListArgs<Doc<${f(e.name)}>>()({
39
+ // Only index-backed columns are published as filterable, so a caller cannot
40
+ // reach a column you did not choose to expose. Note that \`contains\` and the
41
+ // negative operators still scan — narrow this list, and review it, before
42
+ // adding \`.expose({ rest: true })\`.
43
+ filter: {
44
+ ${c.join(`
45
+ `)}
46
+ },
47
+ orderBy: [${m}],
48
+ });
49
+
50
+ export const list = c.query.input(${a}List.args).query(({ args, ctx }) => ${u}.findMany(${a}List.toQueryArgs(args)));
51
+
52
+ export const get = c.query.input({ id: v.id(${f(e.name)}) }).query(({ args, ctx }) => ${u}.get(args.id));
53
+ `},ce=(e,r)=>{const n=[],t=new Set(e.tables.map(a=>a.name)),s=[{contents:oe(e,r,n),path:"schema.ts"}];if(r.procedures){const a=new Map([["schema","the generated schema module"]]);for(const o of e.tables){if(D(o).length===0){n.push(`${o.name}: no usable columns — procedure module skipped.`);continue}const i=te(o.name);i!==o.name&&n.push(`${o.name}: written as \`${i}.ts\` — the table name isn't usable as a filename.`);const c=a.get(i);if(c!==void 0){n.push(`${o.name}: procedure module skipped — its filename \`${i}.ts\` collides with table \`${c}\`.`);continue}a.set(i,o.name),s.push({contents:ie(o,e.dialect,r,t,n),path:`${i}.ts`})}}return{files:s,warnings:n}},y=/^[A-Z_$][\w$]*$/i,me=e=>e.startsWith("v.optional(")&&e.endsWith(")")?e.slice(11,-1):e,le=(e,r,n)=>{const t=[],s=new Set(r?.columns.map(a=>a.name));for(const a of e.columns){if(h.has(a.name)||s.has(a.name))continue;if(!y.test(a.name)){n.warnings.push(`${e.name}.${a.name}: skipped — column names must be bare identifiers to merge.`);continue}const o=a.references!==void 0&&!n.present.has(a.references.table),{expression:i}=w(o?{...a,references:void 0}:a,n.dialect);t.push({column:a.name,kind:"addOptionalColumn",table:e.name,validator:me(i)}),!a.nullable&&r!==void 0&&n.warnings.push(`${e.name}.${a.name}: added as \`v.optional(...)\` — a required column on an existing table needs a backfill migration.`)}return t},ue=(e,r,n)=>{const t=[],s=new Set(r?.indexes.map(a=>a.name));for(const a of e.indexes){const o=a.columns.filter(i=>!h.has(i));if(!(s.has(a.name)||o.length===0)){if(!y.test(a.name)||!o.every(i=>y.test(i))){n.push(`${e.name}: index \`${a.name}\` skipped — index and column names must be bare identifiers to merge.`);continue}t.push({fields:o,kind:"addIndex",name:a.name,table:e.name,...a.unique?{unique:!0}:{}})}}return t},de=(e,r,n)=>{const t=[],s=[],a=new Map(r.map(i=>[i.name,i])),o=new Set([...a.keys(),...e.tables.map(i=>i.name)]);for(const i of e.tables){if(!y.test(i.name)){s.push(`${i.name}: skipped — merging into an existing schema needs a table name that is a bare identifier.`);continue}const c=a.get(i.name);c===void 0&&t.push({global:{backend:"hyperdrive"},kind:"addTable",table:i.name}),t.push(...le(i,c,{dialect:n,present:o,warnings:s}),...ue(i,c,s))}return{edits:t,warnings:s}},pe=(e,r,n)=>{const t=B(e);if(!t.ok)return{applied:0,warnings:[`lunora/schema.ts could not be parsed (${t.reason}) — leave it alone and merge by hand, or pass --force.`]};const s=de(r,t.tables,n),a=[...s.warnings];let o=e,i=0;for(const c of s.edits){const m=Y(o,c);m.ok?(o=m.text,i+=1):a.push(`${c.table}: skipped one edit (${m.reason}).`)}return{applied:i,warnings:a,...i===0?{}:{text:o}}},g=(e,r)=>{const n=e[r]??e[r.toLowerCase()]??e[r.toUpperCase()];return typeof n=="string"?n:typeof n=="number"||typeof n=="bigint"||typeof n=="boolean"?String(n):""},ge=`
54
+ SELECT c.table_name, c.column_name, c.is_nullable, c.data_type, c.udt_name
55
+ FROM information_schema.columns c
56
+ JOIN information_schema.tables t
57
+ ON t.table_schema = c.table_schema AND t.table_name = c.table_name
58
+ WHERE c.table_schema = $1 AND t.table_type = 'BASE TABLE'
59
+ ORDER BY c.table_name, c.ordinal_position`,fe=`
60
+ SELECT tc.table_name, kcu.column_name
61
+ FROM information_schema.table_constraints tc
62
+ JOIN information_schema.key_column_usage kcu
63
+ ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema
64
+ WHERE tc.table_schema = $1 AND tc.constraint_type = 'PRIMARY KEY'
65
+ ORDER BY tc.table_name, kcu.ordinal_position`,ve=`
66
+ SELECT tc.table_name, kcu.column_name, ccu.table_name AS foreign_table, ccu.column_name AS foreign_column
67
+ FROM information_schema.table_constraints tc
68
+ JOIN information_schema.key_column_usage kcu
69
+ ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema
70
+ JOIN information_schema.constraint_column_usage ccu
71
+ ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
72
+ WHERE tc.table_schema = $1 AND tc.constraint_type = 'FOREIGN KEY'`,be=`
73
+ SELECT t.relname AS table_name, i.relname AS index_name, ix.indisunique AS is_unique, a.attname AS column_name
74
+ FROM pg_class t
75
+ JOIN pg_index ix ON t.oid = ix.indrelid
76
+ JOIN pg_class i ON i.oid = ix.indexrelid
77
+ JOIN pg_namespace n ON n.oid = t.relnamespace
78
+ CROSS JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, ord)
79
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
80
+ WHERE n.nspname = $1 AND t.relkind = 'r' AND NOT ix.indisprimary
81
+ ORDER BY t.relname, i.relname, k.ord`,he=`
82
+ SELECT c.TABLE_NAME, c.COLUMN_NAME, c.IS_NULLABLE, c.DATA_TYPE
83
+ FROM information_schema.COLUMNS c
84
+ JOIN information_schema.TABLES t
85
+ ON t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME
86
+ WHERE c.TABLE_SCHEMA = ? AND t.TABLE_TYPE = 'BASE TABLE'
87
+ ORDER BY c.TABLE_NAME, c.ORDINAL_POSITION`,_e=`
88
+ SELECT TABLE_NAME, COLUMN_NAME
89
+ FROM information_schema.KEY_COLUMN_USAGE
90
+ WHERE TABLE_SCHEMA = ? AND CONSTRAINT_NAME = 'PRIMARY'
91
+ ORDER BY TABLE_NAME, ORDINAL_POSITION`,Ee=`
92
+ SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME AS foreign_table, REFERENCED_COLUMN_NAME AS foreign_column
93
+ FROM information_schema.KEY_COLUMN_USAGE
94
+ WHERE TABLE_SCHEMA = ? AND REFERENCED_TABLE_NAME IS NOT NULL`,ye=`
95
+ SELECT TABLE_NAME, INDEX_NAME AS index_name, NON_UNIQUE, COLUMN_NAME
96
+ FROM information_schema.STATISTICS
97
+ WHERE TABLE_SCHEMA = ? AND INDEX_NAME <> 'PRIMARY'
98
+ ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX`,_=e=>{const r=new Map;for(const n of e){const t=g(n,"table_name"),s=r.get(t);s===void 0?r.set(t,[n]):s.push(n)}return r},Ae=(e,r)=>{const n=g(e,"data_type").toLowerCase();if(r!=="postgres")return{arrayDepth:0,dataType:n};const t=g(e,"udt_name").toLowerCase();return n==="array"&&t.startsWith("_")?{arrayDepth:1,dataType:t.slice(1)}:{arrayDepth:0,dataType:t===""?n:t}},Ne=(e,r)=>{const n=new Map;for(const t of e){const s=g(t,"index_name"),a=r==="postgres"?g(t,"is_unique")==="true":g(t,"NON_UNIQUE")==="0",o=n.get(s);o===void 0?n.set(s,{columns:[g(t,"column_name")],unique:a}):o.columns.push(g(t,"column_name"))}return[...n.entries()].map(([t,s])=>({columns:s.columns,name:t,unique:s.unique}))},Se=async(e,r,n)=>{const t=r==="postgres",[s,a,o,i]=await Promise.all([e(t?ge:he,[n]),e(t?fe:_e,[n]),e(t?ve:Ee,[n]),e(t?be:ye,[n])]),c=_(a),m=_(o),u=_(i),l=[];for(const[p,v]of _(s)){const d=new Map((m.get(p)??[]).map(b=>[g(b,"column_name"),{column:g(b,"foreign_column"),table:g(b,"foreign_table")}])),A=v.map(b=>{const R=g(b,"column_name"),$=d.get(R);return{...Ae(b,r),name:R,nullable:g(b,"is_nullable").toUpperCase()==="YES",...$===void 0?{}:{references:$}}});l.push({columns:A,indexes:Ne(u.get(p)??[],r),name:p,primaryKey:(c.get(p)??[]).map(b=>g(b,"column_name"))})}return l.sort((p,v)=>p.name.localeCompare(v.name)),{dialect:r,tables:l}},we=e=>q(e)?"lunorash/server":"@lunora/server",Re=async(e,r,n)=>{const t=E(r,e.path);return O(t)&&n.force!==!0?(n.logger.warn(`skipped lunora/${e.path} — it already exists (pass --force to overwrite)`),!1):n.dryRun===!0?(n.logger.info(`would write lunora/${e.path} (${String(e.contents.split(`
99
+ `).length)} lines)`),!1):(await U(r,{recursive:!0}),await I(t,e.contents,"utf8"),n.logger.info(`wrote lunora/${e.path}`),!0)},$e=(e,r)=>{const n=r.tables;if(n===void 0||n.length===0)return e.tables;for(const t of n)e.tables.some(s=>s.name===t)||r.logger.warn(`--tables: no table named "${t}" exists in this schema — skipped.`);return e.tables.filter(t=>n.includes(t.name))},Te=async(e,r,n,t)=>{const s=pe(k(e,"utf8"),r,n);for(const a of s.warnings)t.logger.warn(a);return s.text===void 0?(t.logger.info("lunora/schema.ts is already up to date with the database."),!1):t.dryRun===!0?(t.logger.info(`would merge ${String(s.applied)} addition(s) into lunora/schema.ts`),!1):(await I(e,s.text,"utf8"),t.logger.info(`merged ${String(s.applied)} addition(s) into lunora/schema.ts`),!0)},Le=async e=>{const r=e.cwd??process.cwd();if(e.connection===void 0&&(e.url===void 0||e.url==="")){const d="`lunora introspect` needs a database URL: pass --url, or set DATABASE_URL.";return e.logger.error(d),{code:T.USAGE,error:d,written:[]}}const n=e.connection===void 0?Q(e.url):e.dialect??"postgres",t=e.connection??await J(e.url,n,e.schema,r);let s;try{s=await Se(t.execute,n,t.schema)}finally{e.connection===void 0&&await t.close()}const a=$e(s,e);if(a.length===0){const d="no tables found to introspect — check --schema and --tables.";return e.logger.error(d),{code:T.NOT_FOUND,error:d,written:[]}}const{files:o,warnings:i}=ce({...s,tables:a},{procedures:e.procedures!==!1,serverImport:we(r)}),c=E(r,"lunora"),m=[],u=E(c,"schema.ts"),l=O(u)&&e.force!==!0,p=l?o.filter(d=>d.path!=="schema.ts"):o;l&&await Te(u,{...s,tables:a},n,e)&&m.push("schema.ts");for(const d of p)await Re(d,c,e)&&m.push(d.path);for(const d of i)e.logger.warn(d);e.logger.info(`introspected ${String(a.length)} table(s) from ${n}. Review the generated files before running \`lunora dev\`.`);const v=a.map(d=>d.name);return{code:0,dialect:n,tables:v,written:m}},qe=F(async({cwd:e,format:r,logger:n,options:t})=>{const s=await Le({cwd:e,dryRun:t.dryRun===!0,force:t.force===!0,format:r,logger:n,procedures:t.procedures!==!1,schema:t.schema,tables:t.tables===void 0?void 0:t.tables.split(",").map(a=>a.trim()),url:t.url??process.env.DATABASE_URL});return{code:s.code,data:s.dialect===void 0?void 0:{dialect:s.dialect,dryRun:t.dryRun===!0,tables:s.tables??[],written:s.written},error:s.error}});export{qe as execute,we as resolveServerImport,Le as runIntrospectCommand};