@lunora/cli 1.0.0-alpha.442 → 1.0.0-alpha.444
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.
- package/dist/cli.d.mts +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/index.d.mts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.mjs +1 -1
- package/dist/packem_chunks/cli.mjs +2 -2
- package/dist/packem_chunks/dispatch.mjs +1 -1
- package/dist/packem_chunks/handler.mjs +1 -1
- package/dist/packem_chunks/handler14.mjs +1 -1
- package/dist/packem_chunks/handler18.mjs +1 -7
- package/dist/packem_chunks/handler19.mjs +7 -2
- package/dist/packem_chunks/handler20.mjs +2 -1
- package/dist/packem_chunks/handler21.mjs +1 -3
- package/dist/packem_chunks/handler22.mjs +3 -3
- package/dist/packem_chunks/handler23.mjs +3 -1
- package/dist/packem_chunks/handler24.mjs +1 -2
- package/dist/packem_chunks/handler25.mjs +2 -1
- package/dist/packem_chunks/handler26.mjs +1 -99
- package/dist/packem_chunks/handler27.mjs +99 -15
- package/dist/packem_chunks/handler28.mjs +15 -1
- package/dist/packem_chunks/handler29.mjs +1 -0
- package/dist/packem_chunks/handler7.mjs +1 -1
- package/dist/packem_chunks/runInitCommand.mjs +40 -17
- package/dist/packem_shared/{DEFAULT_IMPORT_BATCH_SIZE-DoFxIuJF.mjs → DEFAULT_IMPORT_BATCH_SIZE-5xxKz-zB.mjs} +1 -1
- package/dist/packem_shared/import-DoAUdyfU.mjs +13 -0
- package/dist/packem_shared/runMigrateGenerateCommand-Bd2W9t1k.mjs +11 -0
- package/package.json +10 -10
- package/skills/lunora-migration-helper/SKILL.md +8 -2
- package/skills/lunora-setup-hyperdrive-global/SKILL.md +13 -0
- package/dist/packem_shared/import-U8wA5qmm.mjs +0 -13
- package/dist/packem_shared/runMigrateGenerateCommand-D5azbBCr.mjs +0 -11
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{existsSync as m,mkdirSync as M,writeFileSync as v,readFileSync as x,appendFileSync as it,readdirSync as Y,rmSync as R,lstatSync as ge,mkdtempSync as we,copyFileSync as st,renameSync as lt}from"node:fs";import{tmpdir as ye}from"node:os";import{detectFramework as ct,DEV_VARS_EXAMPLE_FILE as be,DEV_VARS_FILE as dt,isInteractive as b,LUNA_ART as ut,LUNA_NAME as pt,LUNA_SIGNOFF as ht,BADGES as g,paintAnswer as ft,detectLintTools as mt,readProjectDependencies as ke,applyLintIgnores as vt,readProjectManifest as gt}from"@lunora/config";import{readJsonSync as wt,walkSync as yt}from"@visulima/fs";import{relative as D,join as u,dirname as C,basename as T,resolve as xe}from"@visulima/path";import{downloadTemplate as Se}from"giget";import{modify as bt,applyEdits as kt}from"jsonc-parser";import{join as J,dirname as xt}from"node:path";import{installArgsFor as Ce,execArgsFor as St,detectPackageManager as
|
|
1
|
+
import{existsSync as m,mkdirSync as M,writeFileSync as v,readFileSync as x,appendFileSync as it,readdirSync as Y,rmSync as R,lstatSync as ge,mkdtempSync as we,copyFileSync as st,renameSync as lt}from"node:fs";import{tmpdir as ye}from"node:os";import{detectFramework as ct,DEV_VARS_EXAMPLE_FILE as be,DEV_VARS_FILE as dt,isInteractive as b,LUNA_ART as ut,LUNA_NAME as pt,LUNA_SIGNOFF as ht,BADGES as g,paintAnswer as ft,detectLintTools as mt,readProjectDependencies as ke,applyLintIgnores as vt,readProjectManifest as gt}from"@lunora/config";import{readJsonSync as wt,walkSync as yt}from"@visulima/fs";import{relative as D,join as u,dirname as C,basename as T,resolve as xe}from"@visulima/path";import{downloadTemplate as Se}from"giget";import{modify as bt,applyEdits as kt}from"jsonc-parser";import{join as J,dirname as xt}from"node:path";import{installArgsFor as Ce,execArgsFor as St,detectPackageManager as B,detectInstalledManagers as Ct,runScriptCommand as Lt,addArgsFor as At}from"@lunora/config/cli";import{d as Et}from"../packem_shared/command-B6vu47iR.mjs";import{EXIT_CODE as w}from"../packem_shared/EXIT_CODE-DVjUgIaG.mjs";import Mt from"magic-string";import{Project as It,SyntaxKind as y}from"ts-morph";import{P as X}from"../packem_shared/prompt-cancelled-BvsNxg_Q.mjs";import{l as Le,m as _t,n as Ae,o as Ee,a as Tt}from"../packem_shared/resolve-Bdc9KEUv.mjs";import{defaultSpawner as Me}from"../packem_shared/createRecordingSpawner-01ZngIwK.mjs";import{d as $t,e as Nt,f as Rt,a as Ie,t as O,g as _e,h as Ot,b as Te,i as $e,w as Pt,j as Dt,k as Ne,l as Bt}from"../packem_shared/tui-prompts-B3YwUhGw.mjs";import{A as Re,p as Ut,w as jt,M as Vt,r as Ft,E as Oe,k as Wt,e as Ht,c as Pe,l as De,i as zt,f as Kt}from"../packem_shared/storage-BlIF1T0Y.mjs";import{r as qt}from"../packem_shared/commands-phAlD1KR.mjs";import{findWranglerFile as Gt,readWranglerJsonc as Yt,DEFAULT_OBSERVABILITY as Jt,CLOUDFLARE_TOOLCHAIN_VERSIONS as re}from"@lunora/config/cloudflare";import{logStep as ae}from"../packem_shared/createLogger-4TujevyB.mjs";import{r as Xt}from"../packem_shared/lint-ignore-report-DKZagpqk.mjs";import Qt from"node:dns/promises";const Q={bun:{githubCacheKey:void 0,githubSetupStep:` - uses: oven-sh/setup-bun@v2
|
|
2
2
|
`,gitlabImage:"oven/bun:1",gitlabProvisionScript:[],installCmd:"bun install --frozen-lockfile",lockfile:"bun.lockb"},npm:{githubCacheKey:"npm",githubSetupStep:"",gitlabImage:"node:22",gitlabProvisionScript:[],installCmd:"npm ci",lockfile:"package-lock.json"},pnpm:{githubCacheKey:"pnpm",githubSetupStep:` - uses: pnpm/action-setup@v4
|
|
3
3
|
with:
|
|
4
4
|
version: latest
|
|
@@ -84,10 +84,10 @@ preview:
|
|
|
84
84
|
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
85
85
|
script:
|
|
86
86
|
- ${E(e,["deploy","--preview"])}
|
|
87
|
-
`},
|
|
87
|
+
`},Be={github:{file:J(".github","workflows","deploy.yml"),secretsHint:"repository secrets (Settings → Secrets and variables → Actions)"},gitlab:{file:".gitlab-ci.yml",secretsHint:"masked CI/CD variables (Settings → CI/CD → Variables)"}},tr=(e,t)=>e==="github"?Zt(t):er(t),rr=e=>e==="github"||e==="gitlab",ar=(e,t,r,a={})=>{const n=J(e,Be[t].file);return m(n)&&a.overwrite!==!0?{path:n,skipped:!0,written:!1}:(M(xt(n),{recursive:!0}),v(n,tr(t,r),"utf8"),{path:n,skipped:!1,written:!0})},nr=(e,t,r,a,n={})=>{const o=Be[t];try{const s=ar(e,t,r,n);if(s.skipped)a.info(`--ci ${t}: ${o.file} already exists — left unchanged (re-run with overwrite to replace).`);else{const i=Ce(r),l=[i.command,...i.args].join(" "),{installCmd:c,lockfile:d}=Q[r];a.success(`--ci ${t}: wrote ${o.file}`),a.info(`--ci ${t}: set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID as ${o.secretsHint} to enable deploys.`),a.info(`--ci ${t}: run \`${l}\` and commit ${d} before pushing — the pipeline runs \`${c}\`.`)}return s}catch(s){const i=s instanceof Error?s.message:String(s);return a.warn(`--ci ${t}: could not write ${o.file} (${i})`),{path:J(e,o.file),skipped:!1,written:!1}}},or={astro:"@lunora/react",none:"@lunora/react",nuxt:"@lunora/vue","react-router":"@lunora/react","solid-start":"@lunora/solid",sveltekit:"@lunora/svelte","tanstack-start":"@lunora/react","tanstack-start-solid":"@lunora/solid",vinext:"@lunora/react"},ir=e=>{const t=ct(e);return{...t,adapter:or[t.framework]}},N="lunora()",oe='import { lunora } from "@lunora/vite";',sr="@lunora/vite",lr="lunora",cr=e=>{for(const t of e.getStatements()){if(t.getKind()!==y.ExportAssignment)continue;const r=t.asKindOrThrow(y.ExportAssignment).getExpression();if(r.getKind()!==y.CallExpression)continue;const a=r.asKindOrThrow(y.CallExpression);if(a.getExpression().getText()!=="defineConfig")continue;const n=a.getArguments()[0];if(n?.getKind()===y.ObjectLiteralExpression)return n.asKindOrThrow(y.ObjectLiteralExpression)}},dr=e=>{for(const t of e.getStatements()){if(t.getKind()!==y.ExportAssignment)continue;const r=t.asKindOrThrow(y.ExportAssignment).getExpression();if(r.getKind()===y.ObjectLiteralExpression)return r.asKindOrThrow(y.ObjectLiteralExpression)}},ur=e=>e.getDescendantsOfKind(y.CallExpression).some(t=>t.getExpression().getText()===lr),pr=e=>e.getImportDeclarations().some(t=>t.getModuleSpecifierValue()===sr),hr=e=>new It({compilerOptions:{allowJs:!0},useInMemoryFileSystem:!0}).createSourceFile("vite.config.ts",e,{overwrite:!0}),fr=(e,t)=>{const r=t.getImportDeclarations().at(-1)?.getEnd()??0;r===0?e.prepend(`${oe}
|
|
88
88
|
`):e.appendLeft(r,`
|
|
89
89
|
${oe}`)},mr=(e,t)=>{const r=t.getProperty("plugins");if(r===void 0){const s=t.getProperties(),i=t.getStart()+1;if(s.length===0){e.appendLeft(i,` plugins: [${N}] `);return}const l=s[0];if(l===void 0)return"could not locate the start of the Vite config object's first property";e.appendLeft(l.getStart(),`plugins: [${N}],
|
|
90
|
-
`);return}const a=r.getDescendantsOfKind(y.ArrayLiteralExpression)[0];if(a===void 0)return"the Vite config's `plugins` is not an array literal — add `lunora()` to it by hand";const n=a.getElements();if(n.length===0){e.appendLeft(a.getStart()+1,N);return}const o=n[0];if(o===void 0)return"could not locate the first entry of the Vite config's `plugins` array";e.appendLeft(o.getStart(),`${N}, `)},
|
|
90
|
+
`);return}const a=r.getDescendantsOfKind(y.ArrayLiteralExpression)[0];if(a===void 0)return"the Vite config's `plugins` is not an array literal — add `lunora()` to it by hand";const n=a.getElements();if(n.length===0){e.appendLeft(a.getStart()+1,N);return}const o=n[0];if(o===void 0)return"could not locate the first entry of the Vite config's `plugins` array";e.appendLeft(o.getStart(),`${N}, `)},Ue=e=>{const t=hr(e);if(ur(t))return{changed:!1,code:e,reason:"lunora plugin already present"};const r=cr(t)??dr(t);if(r===void 0)return{changed:!1,code:e,reason:"could not locate a Vite config plugins array to patch"};const a=new Mt(e);pr(t)||fr(a,t);const n=mr(a,r);return n!==void 0?{changed:!1,code:e,reason:n}:{changed:!0,code:a.toString()}},vr=/enotfound|eai_again|econnrefused|etimedout|network|fetch failed|getaddrinfo/u,gr=/404|not found|could not find|no such/u,wr=(e,t)=>{const r=e instanceof Error?e.message:String(e),a=r.toLowerCase(),n=`failed to download template "${t.templateType}" from ${t.remote}: ${r}`;return gr.test(a)?{hints:[`Check the template name "${t.templateType}" and the ref "${t.ref}".`,"List/inspect available templates, or target a branch/tag with `--ref <branch>`."],message:`template "${t.templateType}" not found at ${t.remote}: ${r}`}:vr.test(a)?{hints:["You appear to be offline or unable to reach GitHub.","To scaffold without a network, point at a local template root: `lunora init --from <dir>`."],message:n}:{hints:["If this is a network/offline issue, scaffold from a local root with `lunora init --from <dir>`."],message:n}},je=[{description:"No sign-in yet; add it later with `lunora add auth`",label:"None",value:"none"},{description:"Users and sessions in your own D1, via better-auth (@lunora/auth)",label:"Lunora auth",value:"lunora"},{description:"Clerk owns the session; the Worker verifies its session token",label:"Clerk",value:"clerk"},{description:"WorkOS AuthKit owns the session; the Worker verifies its access token",label:"WorkOS AuthKit",value:"workos"},{description:"Auth0 owns the session; the Worker verifies its access token",label:"Auth0",value:"auth0"}],ie=je.map(e=>e.value),Z={auth0:{clientSnippet:["// @auth0/auth0-react: <Auth0Provider authorizationParams={{ audience: AUTH0_AUDIENCE }} useRefreshTokens>","// const { getAccessTokenSilently, isLoading, user } = useAuth0();","// In a useEffect over [client, getAccessTokenSilently, isLoading, user?.sub]:","if (isLoading) return;","if (!user) return client.setAuthToken(null);","const fetchToken = () => getAccessTokenSilently().catch((error) => {",' if (["login_required", "missing_refresh_token"].includes(error?.error)) return null; // signed out'," throw error;","});","return setAuthTokenProvider(client, fetchToken, { subject: user.sub });"].join(`
|
|
91
91
|
`),envVars:["AUTH0_DOMAIN","AUTH0_AUDIENCE"],identity:"auth0Identity",identityFile:"auth0",item:"auth-auth0-jwt"},clerk:{clientSnippet:["// @clerk/react: const { getToken, isLoaded, isSignedIn, userId } = useAuth();","// In a useEffect over [client, getToken, isLoaded, isSignedIn, userId]:","if (!isLoaded) return;","if (!isSignedIn) return client.setAuthToken(null);","return setAuthTokenProvider(client, getToken, { subject: userId });"].join(`
|
|
92
92
|
`),envVars:["CLERK_PUBLISHABLE_KEY","CLERK_AUTHORIZED_PARTIES"],identity:"clerkIdentity",identityFile:"clerk",item:"auth-clerk-jwt"},workos:{clientSnippet:["// @workos-inc/authkit-react: const { getAccessToken, isLoading, user } = useAuth();","// In a useEffect over [client, getAccessToken, isLoading, user?.id]:","if (isLoading) return;","if (!user) return client.setAuthToken(null);","const fetchToken = () => getAccessToken().catch((error) => {"," if (error instanceof LoginRequiredError) return null; // signed out"," throw error;","});","return setAuthTokenProvider(client, fetchToken, { subject: user.id });"].join(`
|
|
93
93
|
`),envVars:["WORKOS_CLIENT_ID"],identity:"workosIdentity",identityFile:"workos",item:"auth-workos-jwt"}},yr=e=>Object.hasOwn(Z,e),br=(e,t)=>{const r=e.trim().toLowerCase();return ie.includes(r)?r:(t(`init: unknown --auth "${e}" — expected ${ie.join(" | ")}; skipping auth.`),"none")},kr=["lunora/app.ts","src/app.ts","lunora/server.ts","src/server.ts","src/server/index.ts","src/server/index.tsx","src/index.ts","src/worker.ts"],xr="virtual:lunora/worker",Sr=["lunora.config.ts","lunora.config.mts","lunora.config.js","lunora.config.mjs"],Cr=["src","app","pages","plugins"],Lr=/\.(?:[jt]sx?|svelte|vue)$/u,H="new LunoraClient({",se=/\n([\t ]*)\.build(?:FrameworkWorker)?\(/uy,Ar=new Set(["(","[","{"]),Er=new Set([")","]","}"]),Mr=(e,t)=>{const r=e[t];if(r==="/"&&e[t+1]==="/"){const n=e.indexOf(`
|
|
@@ -102,14 +102,14 @@ export default {
|
|
|
102
102
|
// Verify the provider's token on every request and feed it into ctx.auth (memoized for 5 s).
|
|
103
103
|
app: (app: AppBuilder<object>): AppBuilder<object> => app.extend((env) => ({ resolveIdentity: ${t.identity}(env) })),
|
|
104
104
|
} satisfies LunoraConfig<object>;
|
|
105
|
-
`),"lunora.config.ts"},Ve=(e,t=0)=>t>3||!m(e)?[]:Y(e,{withFileTypes:!0}).flatMap(r=>{const a=u(e,r.name);return r.isDirectory()?r.name==="node_modules"?[]:Ve(a,t+1):Lr.test(r.name)?[a]:[]}),Dr=e=>Cr.flatMap(t=>Ve(u(e,t))).filter(t=>{const r=x(t,"utf8");return!r.includes(H)||r.includes("authTokenOnUpgrade")?!1:(v(t,r.replaceAll(H,`${H} authTokenOnUpgrade: true,`)),!0)}),
|
|
105
|
+
`),"lunora.config.ts"},Ve=(e,t=0)=>t>3||!m(e)?[]:Y(e,{withFileTypes:!0}).flatMap(r=>{const a=u(e,r.name);return r.isDirectory()?r.name==="node_modules"?[]:Ve(a,t+1):Lr.test(r.name)?[a]:[]}),Dr=e=>Cr.flatMap(t=>Ve(u(e,t))).filter(t=>{const r=x(t,"utf8");return!r.includes(H)||r.includes("authTokenOnUpgrade")?!1:(v(t,r.replaceAll(H,`${H} authTokenOnUpgrade: true,`)),!0)}),Br=(e,t)=>{const r=u(e,be),a=u(e,dt),n=m(r)?x(r,"utf8"):"",o=m(a)?x(a,"utf8").split(`
|
|
106
106
|
`):[],s=t.envVars.filter(c=>!n.split(`
|
|
107
107
|
`).some(d=>d.startsWith(`${c}=`)));if(s.length===0)return!1;const i=s.map(c=>o.find(d=>d.startsWith(`${c}=`))??`${c}=`),l=n.length===0||n.endsWith(`
|
|
108
108
|
`)?"":`
|
|
109
109
|
`;return it(r,`${l}# ${t.item}: the identity provider the Worker verifies tokens from.
|
|
110
110
|
${i.join(`
|
|
111
111
|
`)}
|
|
112
|
-
`),!0},
|
|
112
|
+
`),!0},Ur=(e,t,r)=>{const a=Z[t],n=()=>{const s=Pr(e,a);return s===void 0?void 0:{file:s,outcome:"wired"}},o=Rr(e,a)??n();o===void 0?r.warn(`auth: add \`.extend((env) => ({ resolveIdentity: ${a.identity}(env) }))\` to your app builder (lunora.config.ts \`app\` hook or the Worker entry) — ${a.identity} is in lunora/identity/${a.identityFile}.ts.`):o.outcome==="already-wired"?r.info(`auth: ${o.file} already wires ${a.identity}; left as is.`):o.outcome==="has-resolver"?r.warn(`auth: ${o.file} already sets resolveIdentity, so it was left as is. Compose the two: \`resolveIdentity: composeIdentityResolvers([${a.identity}(env), <your resolver>])\` — ${a.identity} is in lunora/identity/${a.identityFile}.ts.`):r.success(`auth: wired ${a.identity} as resolveIdentity in ${o.file}`),Br(e,a)&&r.success(`auth: listed ${a.envVars.join(", ")} in ${be}`);for(const s of Dr(e))r.success(`auth: turned on authTokenOnUpgrade in ${D(e,s)}`);r.info(`auth: hand the provider's token to Lunora once signed in (setAuthTokenProvider from lunorash/client or @lunora/client):
|
|
113
113
|
${a.clientSnippet}
|
|
114
114
|
Fill in the env vars the item wrote to .dev.vars.`)},P=async(e,t,r)=>{if(b()){await Nt(g[e],t,r);return}if(process.stdout.write(`
|
|
115
115
|
`),r===void 0||r===""){ae(e,t);return}const a=r.split(`
|
|
@@ -117,7 +117,7 @@ Fill in the env vars the item wrote to .dev.vars.`)},P=async(e,t,r)=>{if(b()){aw
|
|
|
117
117
|
`);ae(e,`${t}
|
|
118
118
|
${a}`)},jr=async e=>{if(b()){await $t();return}e.info(`
|
|
119
119
|
${ut}
|
|
120
|
-
${pt}: ${ht}`)},Fe=[{description:"LLMs via Workers AI (summarize, generate, stream)",label:"AI",value:"ai"},{description:"Sign-up / sign-in with @lunora/auth (asks which provider)",label:"Authentication",value:"auth"},{description:"Copy-in auth screens for your framework (sign in/up, reset, 2FA)",label:"Auth UI",value:"auth-ui"},{description:"Snapshot + restore your Durable Object data",label:"Backups",value:"backup"},{description:"Headless browser screenshots + PDFs",label:"Browser rendering",value:"browser"},{description:"Zero Trust identity via Cloudflare Access",label:"Cloudflare Access",value:"cloudflare-access"},{description:"Scheduled jobs via Cron Triggers (@lunora/scheduler)",label:"Cron jobs",value:"crons"},{description:"Cloudflare Email Workers + a dev mail catcher",label:"Transactional email",value:"email"},{description:"OpenFeature feature flags (ctx.flags)",label:"Feature flags",value:"flags"},{description:"External Postgres/MySQL via Hyperdrive",label:"Hyperdrive",value:"hyperdrive"},{description:"Stripe-first payments (checkout, subscription, webhooks)",label:"Payments",value:"payment"},{description:"Live presence / who's-online over hibernated WebSockets",label:"Presence",value:"presence"},{description:"Async message queues (push/pull consumers)",label:"Queues",value:"queue"},{description:"Typed R2 buckets + signed URLs (@lunora/storage)",label:"File storage",value:"storage"},{description:"Durable long-running workflows (step.do, sleep, branch)",label:"Workflows",value:"workflow"}],le=Fe.map(e=>e.value),ce=Fe.filter(e=>e.value!=="auth"),We=new Set(["auth","auth-ui"]),Vr=e=>e==="email"?Oe:e,Fr=(e,t)=>{const r=[];for(const a of e.split(",").map(n=>n.trim()).filter(Boolean))le.includes(a)?r.includes(a)||r.push(a):t(`init: unknown --add feature "${a}" — expected ${le.join(" | ")}; skipping.`);return r},He=e=>e.resolveAuthUiItem===void 0?"auth-ui-react":e.resolveAuthUiItem(),Wr=async e=>{const t=await Ht(e.select),r=await Pe(e.text,e.projectName);return{label:"auth",names:[t],transformManifest:a=>De(a,r)}},Hr=async e=>{const t=await e.text(Vt,{placeholder:"you@yourdomain.com"}),r=Ft(t,a=>{e.logger.warn(a)});return{label:"email",names:[Oe],transformManifest:r===void 0?void 0:a=>Wt(a,r)}},zr=async(e,t)=>{const r=He(e);if(r===void 0){e.logger.error(`init: ${Re}`);return}if(t)return{label:"auth-ui",names:[r]};const a=await Pe(e.text,e.projectName);return{label:"auth-ui",names:[r],transformManifest:n=>De(n,a)}},Kr=async e=>{const t=await
|
|
120
|
+
${pt}: ${ht}`)},Fe=[{description:"LLMs via Workers AI (summarize, generate, stream)",label:"AI",value:"ai"},{description:"Sign-up / sign-in with @lunora/auth (asks which provider)",label:"Authentication",value:"auth"},{description:"Copy-in auth screens for your framework (sign in/up, reset, 2FA)",label:"Auth UI",value:"auth-ui"},{description:"Snapshot + restore your Durable Object data",label:"Backups",value:"backup"},{description:"Headless browser screenshots + PDFs",label:"Browser rendering",value:"browser"},{description:"Zero Trust identity via Cloudflare Access",label:"Cloudflare Access",value:"cloudflare-access"},{description:"Scheduled jobs via Cron Triggers (@lunora/scheduler)",label:"Cron jobs",value:"crons"},{description:"Cloudflare Email Workers + a dev mail catcher",label:"Transactional email",value:"email"},{description:"OpenFeature feature flags (ctx.flags)",label:"Feature flags",value:"flags"},{description:"External Postgres/MySQL via Hyperdrive",label:"Hyperdrive",value:"hyperdrive"},{description:"Stripe-first payments (checkout, subscription, webhooks)",label:"Payments",value:"payment"},{description:"Live presence / who's-online over hibernated WebSockets",label:"Presence",value:"presence"},{description:"Async message queues (push/pull consumers)",label:"Queues",value:"queue"},{description:"Typed R2 buckets + signed URLs (@lunora/storage)",label:"File storage",value:"storage"},{description:"Durable long-running workflows (step.do, sleep, branch)",label:"Workflows",value:"workflow"}],le=Fe.map(e=>e.value),ce=Fe.filter(e=>e.value!=="auth"),We=new Set(["auth","auth-ui"]),Vr=e=>e==="email"?Oe:e,Fr=(e,t)=>{const r=[];for(const a of e.split(",").map(n=>n.trim()).filter(Boolean))le.includes(a)?r.includes(a)||r.push(a):t(`init: unknown --add feature "${a}" — expected ${le.join(" | ")}; skipping.`);return r},He=e=>e.resolveAuthUiItem===void 0?"auth-ui-react":e.resolveAuthUiItem(),Wr=async e=>{const t=await Ht(e.select),r=await Pe(e.text,e.projectName);return{label:"auth",names:[t],transformManifest:a=>De(a,r)}},Hr=async e=>{const t=await e.text(Vt,{placeholder:"you@yourdomain.com"}),r=Ft(t,a=>{e.logger.warn(a)});return{label:"email",names:[Oe],transformManifest:r===void 0?void 0:a=>Wt(a,r)}},zr=async(e,t)=>{const r=He(e);if(r===void 0){e.logger.error(`init: ${Re}`);return}if(t)return{label:"auth-ui",names:[r]};const a=await Pe(e.text,e.projectName);return{label:"auth-ui",names:[r],transformManifest:n=>De(n,a)}},Kr=async e=>{const t=await Ut(e.text,e.projectName);return{label:"storage",names:["storage"],transformManifest:r=>jt(r,t)}},qr=(e,t)=>{const r=[];let a=!1;for(const n of t){if(n!=="auth-ui"){r.push({label:n,names:[Vr(n)]});continue}const o=He(e);if(o===void 0){e.logger.error(`init: ${Re}`),a=!0;continue}r.push({label:n,names:[o]})}return{plans:r,refused:a}},ze=e=>e!==void 0&&yr(e)?{externalAuth:e,label:e,names:[Z[e].item]}:void 0,Gr={auth:Wr,"auth-ui":async(e,t)=>zr(e,t.includes("auth")),email:Hr,storage:Kr},Yr=async e=>{const t=ze(e.auth),r=e.auth!==void 0&&e.auth!=="lunora",a=new Set(r?(e.preselected??[]).filter(l=>We.has(l)):[]),n=(e.preselected??[]).filter(l=>!a.has(l));for(const l of a)e.logger.warn(`--auth ${String(e.auth)} rules out Lunora auth; ignoring \`${l}\` from --add.`);e.auth==="lunora"&&!n.includes("auth")&&n.unshift("auth");const{plans:o,refused:s}=qr(e,n);return t!==void 0&&o.unshift(t),await e.applyAll(o)&&!s&&a.size===0},de=async e=>{if(e.preselected!==void 0&&e.preselected.length>0||e.auth!==void 0&&!e.interactive)return Yr(e);if(!e.interactive)return e.logger.info("tip: add features later with `lunora add <ai|auth|backup|browser|cloudflare-access|crons|email|flags|hyperdrive|payment|presence|queue|storage|workflow>`."),!0;const t=e.auth??await e.selectAuth("Which auth do you want?",je,{default:"none"})??"none",r=ze(t),a=t==="lunora"?ce:ce.filter(i=>!We.has(i.value)),n=await e.multiSelect("Which features do you want to add?",a,{defaults:[]}),o=t==="lunora"&&!n.includes("auth")?["auth",...n]:n;if(o.length===0&&r===void 0)return!0;const s=r===void 0?[]:[r];for(const i of o){const l=Gr[i],c=l?await l(e,o):{label:i,names:[i]};c!==void 0&&s.push(c)}return await e.applyAll(s),!0},Jr=[{description:"Adds an ignores entry to eslint.config.js",label:"ESLint",value:"eslint"},{description:"Writes .prettierignore",label:"Prettier",value:"prettier"},{description:"Adds negated patterns to biome.json",label:"Biome",value:"biome"},{description:"Writes ignorePatterns to .oxlintrc.json",label:"oxlint (oxc)",value:"oxlint"}],Xr="Which linter/formatter do you use? We'll tell it to skip Lunora's generated files.",Qr=async e=>{const t=e.interactive?await e.multiSelect(Xr,Jr,{defaults:e.detected}):[...e.detected];if(t.length===0)return[];const r=e.apply(t);return Xt(r,e.logger),r},$="M 259.500 10.552 C 220.080 15.859, 182.424 32.566, 152.500 58.025 C 110.179 94.031, 85.380 137.183, 77.518 188.500 C 75.410 202.255, 74.569 225.677, 75.796 236.466 C 76.757 244.917, 76.683 245.692, 74.518 249.966 C 63.118 272.466, 53.141 303.876, 51.382 322.799 L 50.718 329.943 71.960 320.471 C 83.643 315.262, 93.326 311, 93.478 311 C 93.630 311, 96.547 316.063, 99.959 322.250 C 103.371 328.438, 107.249 334.850, 108.577 336.500 L 110.990 339.500 110.981 336 C 110.977 334.075, 111.499 324.991, 112.143 315.813 L 113.312 299.127 121.406 293.336 C 132.495 285.403, 149.593 271.554, 161 261.268 C 171.556 251.748, 189.116 235, 188.540 235 C 188.337 235, 183.069 238.648, 176.835 243.106 C 142.318 267.789, 68.537 314, 63.646 314 C 61.843 314, 72.791 281.179, 80.905 262.259 C 92.233 235.845, 107.473 212.389, 132.106 183.453 L 138.451 176 148.268 176 C 176.192 176, 197.512 187.154, 212.868 209.797 C 216.470 215.108, 217.035 216.595, 216.477 219.297 C 211.386 243.968, 202.359 274.496, 193.797 296 C 183.898 320.861, 167.147 352.101, 152.395 373.215 L 147.004 380.930 152.891 385.830 C 161.400 392.911, 165.563 396, 166.594 395.998 C 167.092 395.998, 168.772 391.641, 170.327 386.317 C 176.279 365.934, 188.422 338.749, 200.942 317.778 C 223.060 280.731, 256.432 244.369, 294.500 215.836 C 309.956 204.252, 313.937 201.603, 314.719 202.385 C 315.116 202.783, 315.449 213.096, 315.460 225.304 C 315.474 241.855, 315.021 250.405, 313.680 258.924 C 307.009 301.272, 291.175 336.677, 263.112 372 C 255.259 381.883, 227.182 410.673, 218.516 417.727 L 213.532 421.783 223.439 424.880 C 281.705 443.093, 349.165 436.018, 398.616 406.508 C 446.728 377.797, 483.322 331.466, 497.366 281.481 C 503.381 260.075, 504.480 250.741, 504.491 221 C 504.501 191.997, 503.598 184.047, 497.987 163.732 C 484.768 115.871, 452.505 72.708, 407.718 42.964 C 381.051 25.254, 352.818 14.828, 319.695 10.460 C 305.932 8.645, 273.298 8.695, 259.500 10.552",_=`/* The welcome page is a full-bleed starter — reset the browser's default
|
|
121
121
|
body margin/padding so it sits flush; everything else is scoped under
|
|
122
122
|
.lunora-welcome (collision-safe). */
|
|
123
123
|
body {
|
|
@@ -1023,7 +1023,7 @@ const count = document.querySelector<HTMLSpanElement>("#lw-count")!;
|
|
|
1023
1023
|
client.subscribe(api.messages.list, { channelId: "channel:demo" }, (result) => {
|
|
1024
1024
|
count.textContent = String(result.messages.length);
|
|
1025
1025
|
});
|
|
1026
|
-
`,
|
|
1026
|
+
`,U="const url = (import.meta.env.VITE_LUNORA_URL as string | undefined) ?? globalThis.location.origin;",na=`import "./index.css";
|
|
1027
1027
|
|
|
1028
1028
|
import { LunoraProvider } from "@lunora/react";
|
|
1029
1029
|
import { LunoraClient } from "lunorash/client";
|
|
@@ -1035,7 +1035,7 @@ import App from "./App.tsx";
|
|
|
1035
1035
|
// \`@lunora/vite\` runs the Worker on the same origin as Vite, so default to
|
|
1036
1036
|
// \`location.origin\`. Point \`VITE_LUNORA_URL\` at a deployed Worker to develop
|
|
1037
1037
|
// the client against production data.
|
|
1038
|
-
${
|
|
1038
|
+
${U}
|
|
1039
1039
|
const client = new LunoraClient({ url });
|
|
1040
1040
|
|
|
1041
1041
|
const root = document.getElementById("root");
|
|
@@ -1060,7 +1060,7 @@ import { createApp } from "vue";
|
|
|
1060
1060
|
import App from "./App.vue";
|
|
1061
1061
|
|
|
1062
1062
|
// Provide one LunoraClient at the app root via the Vue plugin form.
|
|
1063
|
-
${
|
|
1063
|
+
${U}
|
|
1064
1064
|
createApp(App).use(createLunora(new LunoraClient({ url }))).mount("#app");
|
|
1065
1065
|
`,ia=`import "./index.css";
|
|
1066
1066
|
|
|
@@ -1070,7 +1070,7 @@ import { render } from "solid-js/web";
|
|
|
1070
1070
|
|
|
1071
1071
|
import App from "./App";
|
|
1072
1072
|
|
|
1073
|
-
${
|
|
1073
|
+
${U}
|
|
1074
1074
|
const client = new LunoraClient({ url });
|
|
1075
1075
|
const root = document.getElementById("root");
|
|
1076
1076
|
|
|
@@ -1088,7 +1088,7 @@ render(
|
|
|
1088
1088
|
|
|
1089
1089
|
import App from "./App.svelte";
|
|
1090
1090
|
|
|
1091
|
-
${
|
|
1091
|
+
${U}
|
|
1092
1092
|
setLunoraClient(new LunoraClient({ url }));
|
|
1093
1093
|
<\/script>
|
|
1094
1094
|
|
|
@@ -1109,10 +1109,10 @@ export default app;
|
|
|
1109
1109
|
* Defines the \`ratelimit_buckets\` table used by \`createDbStore\` for durable,
|
|
1110
1110
|
* DO-backed rate limiting. Included automatically in every Lunora project.
|
|
1111
1111
|
*/
|
|
1112
|
-
import type { Middleware } from "lunorash/server";
|
|
1113
|
-
import { defineSchemaExtension, defineTable, definePlugin, v } from "lunorash/server";
|
|
1114
|
-
import { createDbStore, RateLimiter } from "lunorash/ratelimit";
|
|
1115
1112
|
import type { RateLimitConfigMap } from "lunorash/ratelimit";
|
|
1113
|
+
import { createDbStore, RateLimiter } from "lunorash/ratelimit";
|
|
1114
|
+
import type { Middleware } from "lunorash/server";
|
|
1115
|
+
import { definePlugin, defineSchemaExtension, defineTable, v } from "lunorash/server";
|
|
1116
1116
|
|
|
1117
1117
|
/**
|
|
1118
1118
|
* Named limits this app enforces. This is the one place they live — add your own
|
|
@@ -1124,14 +1124,27 @@ export const limits = {
|
|
|
1124
1124
|
send: { kind: "token bucket", period: 60_000, rate: 10 },
|
|
1125
1125
|
} as const satisfies RateLimitConfigMap;
|
|
1126
1126
|
|
|
1127
|
+
/** The limit names you've configured above. */
|
|
1127
1128
|
export type LimitName = keyof typeof limits;
|
|
1128
1129
|
|
|
1130
|
+
/**
|
|
1131
|
+
* Build a durable {@link RateLimiter} bound to this request's \`ctx.db\`. State is
|
|
1132
|
+
* persisted in the \`ratelimit_buckets\` table, so limits survive Durable Object
|
|
1133
|
+
* hibernation/eviction and are shared across every call into the same DO. Each
|
|
1134
|
+
* \`get\`→\`set\` runs under the DO input gate, so the read-modify-write is atomic.
|
|
1135
|
+
*/
|
|
1129
1136
|
export const makeRateLimiter = (ctx: { db: unknown }): RateLimiter<LimitName> =>
|
|
1130
1137
|
new RateLimiter<LimitName>({
|
|
1131
1138
|
config: limits,
|
|
1139
|
+
// The merged table name carries the plugin prefix.
|
|
1132
1140
|
store: createDbStore({ db: ctx.db as never, table: "ratelimit_buckets" }),
|
|
1133
1141
|
});
|
|
1134
1142
|
|
|
1143
|
+
/**
|
|
1144
|
+
* Middleware that injects \`ctx.api.ratelimit\` — a per-request limiter helper —
|
|
1145
|
+
* for any procedure that opts in with \`.use(ratelimit.middleware)\`. Convention
|
|
1146
|
+
* (see \`definePlugin\`): helpers hang off \`ctx.api.<key>\`.
|
|
1147
|
+
*/
|
|
1135
1148
|
const middleware: Middleware<{ api?: Record<string, unknown>; db: unknown }, { api: Record<string, unknown>; db: unknown }> = ({ ctx, next }) =>
|
|
1136
1149
|
next({
|
|
1137
1150
|
ctx: {
|
|
@@ -1140,9 +1153,19 @@ const middleware: Middleware<{ api?: Record<string, unknown>; db: unknown }, { a
|
|
|
1140
1153
|
},
|
|
1141
1154
|
});
|
|
1142
1155
|
|
|
1156
|
+
/**
|
|
1157
|
+
* The rate-limit plugin: a schema extension (\`ratelimit_buckets\`) + middleware
|
|
1158
|
+
* (\`ctx.api.ratelimit\`). \`lunora/schema.ts\` wires the extension in with
|
|
1159
|
+
* \`.extend(ratelimit.extension)\`; attach the middleware yourself with
|
|
1160
|
+
* \`c.mutation.use(ratelimit.middleware)\`.
|
|
1161
|
+
*/
|
|
1143
1162
|
export const ratelimit = definePlugin("ratelimit", {
|
|
1144
1163
|
extension: defineSchemaExtension("ratelimit", {
|
|
1145
1164
|
tables: {
|
|
1165
|
+
// Bare name — auto-prefixes to \`ratelimit_buckets\` at merge time.
|
|
1166
|
+
// \`externallyManaged\`: rows are written by \`createDbStore\` (via
|
|
1167
|
+
// \`makeRateLimiter\`), not a discoverable \`ctx.db.insert(...)\`, so the
|
|
1168
|
+
// \`table_without_insert\` advisor correctly skips it.
|
|
1146
1169
|
buckets: defineTable({
|
|
1147
1170
|
key: v.string(),
|
|
1148
1171
|
value: v.number(),
|
|
@@ -1254,7 +1277,7 @@ export default app;
|
|
|
1254
1277
|
${o.join(`
|
|
1255
1278
|
`)}
|
|
1256
1279
|
`,"utf8")},F=e=>e==="lunorash"||e.startsWith("@lunora/"),ya=(e,t,r,a)=>F(e)?a.get(e)??r:t,A=(e,t,r)=>({...e,[t]:r}),pe=(e,t,r)=>Object.fromEntries(Object.entries(e).map(([a,n])=>[a,ya(a,n,t,r)])),ba=async(e,t,r,a)=>{const n=u(e,"package.json"),o=wt(n);let s=A(o.dependencies??{},"lunorash",a);s=A(s,"@lunora/ratelimit",a),r.adapter.startsWith("@lunora/")&&(s=A(s,r.adapter,a));for(const[d,h]of Object.entries(r.extraDependencies??{}))s=A(s,d,h);let i=A(o.devDependencies??{},"@lunora/vite",a);i=A(i,"@lunora/studio",a);for(const[d,h]of Object.entries(va))i=A(i,d,h);const l=[...Object.keys(s),...Object.keys(i)].filter(d=>F(d)),c=await Le(l,a);o.name=t,o.imports={...o.imports,"#lunora/*":"./lunora/*"},o.dependencies=pe(s,a,c),o.devDependencies=pe(i,a,c),o.scripts={...o.scripts,codegen:"lunora codegen",deploy:"lunora deploy"},v(n,`${JSON.stringify(o,void 0,4)}
|
|
1257
|
-
`,"utf8")},ka=["vite.config.ts","vite.config.mts","vite.config.js","vite.config.mjs"],Ke=e=>ka.map(t=>u(e,t)).find(t=>m(t)),xa=(e,t)=>{const r=Ke(e);if(r===void 0){t.warn("overlay: no vite.config found in the create-vite base — add `lunora()` to your Vite plugins manually.");return}const a=
|
|
1280
|
+
`,"utf8")},ka=["vite.config.ts","vite.config.mts","vite.config.js","vite.config.mjs"],Ke=e=>ka.map(t=>u(e,t)).find(t=>m(t)),xa=(e,t)=>{const r=Ke(e);if(r===void 0){t.warn("overlay: no vite.config found in the create-vite base — add `lunora()` to your Vite plugins manually.");return}const a=Ue(x(r,"utf8"));a.changed&&v(r,a.code,"utf8")},Sa=async e=>{const{adapter:t,distTag:r,logger:a,name:n,target:o}=e,s=[];L(o,u("lunora","ratelimit","schema.ts"),ca,s),L(o,u("lunora","schema.ts"),da,s),L(o,u("lunora","messages.ts"),ua,s),L(o,u("src","server.ts"),pa,s),L(o,"wrangler.jsonc",fa.replaceAll("__NAME__",n),s),L(o,".env.example",ma,s);for(const i of t.files)L(o,i.path,i.contents,s);return xa(o,a),await ba(o,n,t,r),wa(o),s},Ca=["lunar","silver","silent","waning","waxing","crescent","cosmic","stellar","orbital","gibbous","twilight","midnight","shimmering","drifting","weightless"],La=["moon","tide","crater","comet","eclipse","halo","orbit","nebula","voyager","lander","rover","beacon","harbor","meadow","fox"],he=e=>e[Math.floor(Math.random()*e.length)],Aa=()=>`${he(Ca)}-${he(La)}`,Ea=async()=>Qt.lookup("github.com").then(()=>!0,()=>!1),Ma=/^(?:gh|github):([^/]+)\/([^#/]+)(?:\/[^#]*)?(?:#(.+))?$/,qe=e=>{const t=Ma.exec(e);if(t===null)return;const[,r,a,n]=t;if(!(r===void 0||a===void 0))return{owner:r,ref:n??"HEAD",repo:a}},Ia=async e=>{const t=qe(e);if(t===void 0)return;const r=`https://codeload.github.com/${t.owner}/${t.repo}/tar.gz/${t.ref}`;try{const a=await fetch(r,{method:"HEAD"});return a.status===404?!1:a.ok?!0:void 0}catch{return}},Ge=async e=>e.isLocal||!(e.source===void 0||qe(e.source)!==void 0)?!0:await Ea()?e.source!==void 0&&await Ia(e.source)===!1?(e.logger.error(`template source not found: ${e.source} — double-check --ref / --source, or browse the templates at https://lunora.sh/docs.`),!1):!0:(e.logger.error("you appear to be offline — connect to the internet and try again, or scaffold from a local template with `--from <dir>`."),!1),S={extras:"Let's finish setting up your app.",framework:"Which framework should we launch?",git:"Initialize a new git repository? (optional)",install:"Install dependencies now?",name:"Where should we land your project?",nextHeader:"Liftoff confirmed — explore your project!",packageManager:"Which package manager?"},_a=new Set([".gitignore",".html",".js",".json",".jsonc",".md",".mjs",".ts",".tsx"]),Ta=`import { defineConfig } from "vite";
|
|
1258
1281
|
import { lunora } from "@lunora/vite";
|
|
1259
1282
|
|
|
1260
1283
|
export default defineConfig({ plugins: [lunora()] });
|
|
@@ -1281,8 +1304,8 @@ export const send = mutation
|
|
|
1281
1304
|
.mutation(async ({ args }) => {
|
|
1282
1305
|
return { channelId: args.channelId, text: args.text };
|
|
1283
1306
|
});
|
|
1284
|
-
`,Ra="gh:anolilab/lunora/templates",Oa=e=>{const t=e.lastIndexOf(".");return t===-1?!1:_a.has(e.slice(t))},Pa=/^[a-z0-9_][a-z0-9\-_]*$/u,Da=/([a-z0-9])([A-Z])/gu,
|
|
1307
|
+
`,Ra="gh:anolilab/lunora/templates",Oa=e=>{const t=e.lastIndexOf(".");return t===-1?!1:_a.has(e.slice(t))},Pa=/^[a-z0-9_][a-z0-9\-_]*$/u,Da=/([a-z0-9])([A-Z])/gu,Ba=/[^a-z0-9_]/gu,Ua=e=>{const t=e.replaceAll(Da,"$1-$2").toLowerCase().replaceAll(Ba,"-").split("-").filter(r=>r.length>0);return t.length>0?t.join("-"):"my-app"},ja=(e,t)=>e.replaceAll("{{name}}",t),Va=async(e,t)=>{const r=new Set;for(const a of e)if(T(a)==="package.json")for(const n of Object.keys(ke(C(a))))F(n)&&r.add(n);return Le(r,t)},Fa=(e,t,r)=>{let a;try{a=JSON.parse(e)}catch{return e}let n=e;for(const o of["dependencies","devDependencies"])for(const s of Object.keys(a[o]??{})){if(!F(s))continue;const i=r.get(s)??t,l=bt(n,[o,s],i,{formattingOptions:{insertSpaces:!0,tabSize:4}});n=kt(n,l)}return n},Wa=["@parcel/watcher","esbuild","lmdb","msgpackr-extract","rs-module-lexer","sharp","unrs-resolver","vue-demi","workerd"],Ha=["cpu-features","protobufjs","ssh2"],Ye="pnpm-workspace.yaml",za=()=>["# pnpm reads its settings from here (the package.json `pnpm` field is no longer read).","# Pre-approve the toolchain's native build scripts so `pnpm install` runs them","# without the interactive `pnpm approve-builds` step; deny the optional native","# builds a scaffold doesn't need (so no C/C++ toolchain is required).","allowBuilds:",...Wa.map(e=>` '${e}': true`),...Ha.map(e=>` '${e}': false`),""].join(`
|
|
1285
1308
|
`),Je=e=>{const t=u(e,Ye);m(t)||v(t,za(),"utf8")},W=e=>{const t=[];for(const r of yt(e,{includeDirs:!1,includeFiles:!0}))ge(r.path).isSymbolicLink()||t.push(r.path);return t},fe="vitejs/vite",me=(e,t)=>{for(const r of W(e)){const a=u(t,D(e,r));M(C(a),{recursive:!0}),st(r,a)}},Xe=async(e,t,r)=>{const a=W(e),n=[],o=Ae(),s=await Va(a,o);for(const i of a){const l=D(e,i),c=u(t,l);M(C(c),{recursive:!0});const d=x(i);let h=Oa(i)?ja(d.toString("utf8"),r):void 0;h!==void 0&&T(i)==="package.json"&&(h=Fa(h,o,s)),h===void 0?v(c,d):v(c,h,"utf8"),n.push(c)}return n},q=(e,t,r)=>t!==void 0&&t.length>0?t:`${Ra}/${e}#${Ee(r)}`,Ka=e=>e.includes("..")?!1:e.startsWith("gh:")||e.startsWith("github:")||e.startsWith("https://"),I=(e,t)=>{e.info(`[dry-run] would ${t}`)},Qe=(e,t,r)=>(e.error(`init: the template at ${r} contains no files — nothing was scaffolded. Check the template name, \`--source\` layout and \`--ref\`.`),{code:w.NOT_FOUND,files:[],target:t}),Ze="Template files copied.",ee=(e,t,r)=>{b()&&process.stdout.write(`
|
|
1286
1309
|
`),e.success(`scaffolded ${String(t.length)} files into ${r}`)},qa=(e,t)=>{const{args:r,command:a}=At(e,t);return`${a} ${r.join(" ")}`},et=(e,t)=>{let r=xe(e);for(;;){if(t(r))return!0;const a=C(r);if(a===r)return!1;r=a}},Ga=e=>m(u(e,Ye))?!0:gt(e)?.workspaces!==void 0,te=e=>et(e,Ga),Ya=e=>et(e,t=>m(u(t,".git"))),Ja=async(e,t)=>{if(e.yes===!0||!b()||Ya(C(t)))return;if(!await Te(S.git,{badge:g.git,defaultYes:!1})){await $e("Sounds good! You can always run git init manually.");return}if(e.dryRun===!0){I(e.logger,"initialize a git repository");return}const r=e.spawner??Me;(await Pt("Initializing a git repository…",()=>r({args:["init"],command:"git",cwd:t}))).code===0?await P("git","Initialized an empty git repository."):e.logger.warn("`git init` failed — initialize it yourself later with `git init`.")},Xa=async(e,t,r,a)=>{const n=[{code:`cd ./${e}`,lead:"Enter your project directory using"}];t===void 0&&n.push({code:`${r} install`,lead:"Install dependencies with",tail:a?" from the workspace root":void 0}),n.push({code:Lt(r,"dev"),lead:"Run",tail:" to start the dev server."},{code:"lunora add",lead:"Add features like auth or storage using"});const o=[{code:"https://lunora.sh/docs",lead:"Read the docs at"},{code:"https://lunora.sh/chat",lead:"Stuck? Join the chat at"}];if(b()){await Dt(g.next,S.nextHeader,n,o);return}const s=n.map(i=>`${i.lead} ${i.code}${i.tail??""}`);s.push("",...o.map(i=>`${i.lead} ${i.code}${i.tail??""}`)),await P("next",S.nextHeader,s.join(`
|
|
1287
1310
|
`))},Qa=e=>e.yes!==!0&&(e.installPrompt!==void 0||b()),Za=async(e,t)=>{if(!Qa(e)||te(C(t)))return;const r=Ct(e.packageManagerProbe),[a]=r;if(a===void 0)return;if(!await(e.installPrompt?.confirmInstall??(async()=>Te(S.install,{badge:g.deps,defaultYes:!0})))()){await $e("No problem! Remember to install dependencies after setup.");return}let o=a;if(r.length>1&&(o=e.installPrompt?await e.installPrompt.selectManager(r):await O(S.packageManager,r.map(d=>({label:d,value:d})),{badge:g.deps,default:a})??a),e.dryRun===!0){I(e.logger,`install dependencies with ${o}`);return}o==="pnpm"&&Je(t);const s=e.spawner??Me,{args:i,command:l}=Ce(o);await P("deps",`Installing dependencies with ${o}…`);const c=await s({args:i,command:l,cwd:t});if(c.code!==0){e.logger.warn(`\`${l} install\` exited with code ${String(c.code)} — run it yourself in ${T(t)}/.`);return}return await P("deps",`Dependencies installed with ${o}.`),o},en=async(e,t,r,a,n,o)=>{const s=u(e,t);if(!m(s))return n.error(`template not found in local source: ${s}`),{code:w.NOT_FOUND,files:[],target:r};const i=await Xe(s,r,a);return i.length===0?Qe(n,r,s):(o(),ee(n,i,r),{code:0,files:i,target:r})},tn=async e=>{const{logger:t,markComplete:r,name:a,ref:n,source:o,target:s,templateType:i}=e,l=we(u(ye(),"lunora-init-fetch-")),c=u(l,"template");try{const d=o!==void 0&&o.length>0?n:await Tt(n,t),h=q(i,o,d);let f,p=[];if(await Ne([{label:`${i} template fetched`,run:async()=>{f=await Se(h,{cwd:l,dir:c,force:!0,install:!1,silent:!0})}},{label:`files copied into ${a}/`,run:async()=>{p=await Xe(c,s,a)}}],{end:Ze,start:"Project initializing…"}),p.length===0)return Qe(t,s,f?.source??h);r();const k=W(c);return b()&&process.stdout.write(`
|
|
1288
|
-
`),t.info(f?.commit?`template: ${f.source} @ ${f.commit} (${String(k.length)} files)`:`template: ${f?.source??h} (${String(k.length)} files)`),ee(t,p,s),{code:0,files:p,target:s}}catch(d){if(d instanceof X)throw d;const{hints:h,message:f}=wr(d,{ref:Ee(n),remote:q(i,o,n),templateType:i});t.error(f);for(const p of h)t.warn(p);return{code:1,files:[],target:s}}finally{R(l,{force:!0,recursive:!0})}},rn=e=>{for(const t of["_gitignore","_npmrc","_gitattributes"]){const r=u(e,t);m(r)&<(r,u(e,`.${t.slice(1)}`))}},an=async e=>{const{framework:t,logger:r,markComplete:a,name:n,overlayBaseFrom:o,target:s}=e,i=j[t],l=we(u(ye(),"lunora-vite-base-"));try{let c;if(o!==void 0&&(c=u(o,`template-${i.createViteTemplate}`),!m(c)))return r.error(`create-vite base not found on disk: ${c}`),{code:w.NOT_FOUND,files:[],target:s};const d=async()=>{if(c!==void 0){me(c,s);return}const f=u(l,"base"),p=await _t(fe,"main",r),k=`github:${fe}/packages/create-vite/template-${i.createViteTemplate}#${p}`;await Se(k,{cwd:l,dir:f,force:!0,install:!1,silent:!0}),rn(f),me(f,s)};let h=[];return await Ne([{label:`create-vite (${i.label}) base ready`,run:d},{label:`Lunora overlay applied (${i.label})`,run:async()=>{h=await Sa({adapter:i,distTag:Ae(),logger:r,name:n,target:s})}}],{end:Ze,start:"Project initializing…"}),a(),ee(r,h,s),{code:0,files:[...W(s)],target:s}}catch(c){if(c instanceof X)throw c;const d=c instanceof Error?c.message:String(c);return r.error(`failed to scaffold the ${i.label} base: ${d}`),{code:1,files:[],target:s}}finally{R(l,{force:!0,recursive:!0})}},G=(e,t,r,a,n)=>(n.error(`init --in-place: could not ${e} ${t}: ${r instanceof Error?r.message:String(r)}`),{code:1,files:[],target:a}),nn=(e,t)=>{const r=u(e,"vite.config.ts");try{v(r,Ta,"utf8")}catch(a){return G("write",r,a,e,t)}return t.success(`created ${r} with lunora() plugin`),{code:0,files:[r],target:e}},on=(e,t,r)=>{let a;try{a=x(e,"utf8")}catch(o){return G("read",e,o,t,r)}const n=Be(a);if(!n.changed)return r.info(`${e}: ${n.reason??"no changes needed"}`),{code:0,files:[],target:t};try{v(e,n.code,"utf8")}catch(o){return G("write",e,o,t,r)}return r.success(`patched ${e} — added lunora() plugin`),{code:0,files:[e],target:t}},sn=(e,t)=>{const r=u(e,"lunora"),a=u(r,"schema.ts");if(m(a))return t.info(`lunora/ already present — left ${a} untouched`),[];const n=[];try{M(r,{recursive:!0}),v(a,$a,"utf8"),n.push(a);const o=u(r,"messages.ts");m(o)||(v(o,Na,"utf8"),n.push(o)),t.success(`scaffolded lunora/ (${String(n.length)} file(s))`)}catch(o){const s=o instanceof Error?o.message:String(o);t.error(`init --here: could not scaffold lunora/: ${s}`)}return n},tt=e=>e==="sveltekit"||e==="nuxt"||e==="astro",ln=(e,t,r)=>{const{adapter:a,class:n,framework:o}=e;r.info(""),r.info(`detected framework: ${o} (class ${n})`),r.info("next steps:");const s=[a,"@lunora/client","@lunora/runtime","@lunora/server"];tt(o)||s.push("@lunora/vite"),r.info(` 1. install the adapter: ${qa(t,s)}`),r.info(" 2. run codegen: lunora codegen"),n==="A"?(r.info(" 3. compose one worker: wrap your worker entry with"),r.info(" createWorker({ httpRouter: <your framework SSR handler>, shardDO: ShardDO, ... })"),r.info(` 4. add the provider: mount the ${a} provider in your root layout/route`),r.info(" 5. make a loader live: preloadQuery() in a loader, usePreloadedQuery() in the component"),r.info(" see https://lunora.sh/docs/frameworks/reactive-loaders")):n==="B"?(r.info(" 3. inject Lunora: mount Lunora realtime under /_lunora/* in your server hook"),r.info(` (${o} owns its Cloudflare adapter — Lunora composes into its server entry)`),r.info(` 4. add the provider: mount the ${a} provider in your root layout`),r.info(" 5. read the guide: https://lunora.sh/docs/frameworks/deploy")):(r.info(" 3. add the provider: wrap your app with the LunoraProvider from @lunora/react"),r.info(" 4. read the guide: https://lunora.sh/docs/frameworks/bring-your-framework")),r.info("")},cn=(e,t,r)=>{if(tt(t))return r.info(`${t} wires Lunora through its server entry, not a lunora() Vite plugin — leaving the Vite config alone (see next steps)`),{code:0,files:[],target:e};const a=Ke(e);return a===void 0?nn(e,r):on(a,e,r)},dn=(e,t)=>{const r=ir(e),a=cn(e,r.framework,t);if(a.code!==0)return a;const n=sn(e,t);return ln(r,U(e),t),{code:0,files:[...a.files,...n],target:e}},un=e=>e.yes!==!0&&(e.prompt!==void 0||(e.interactive??b())),ve=async(e,t,r)=>{r.dryRun!==!0&&await Qr({apply:a=>vt(e,a),detected:mt(e),interactive:t,logger:r.logger,multiSelect:r.lintPrompt??((a,n,o)=>_e(a,n,{...o,badge:g.add}))})},pn=async(e,t)=>{const r=un(e),a=e.add===void 0?[]:Fr(e.add,i=>{e.logger.warn(i)}),n=async i=>{if(i.length===0)return!0;if(e.dryRun===!0)return I(e.logger,`add ${i.map(p=>p.externalAuth===void 0?p.label:`${p.label} (and wire resolveIdentity)`).join(", ")}`),!0;const l=[],c=b()?{error:p=>{l.push({level:"error",message:p})},info:()=>{},success:()=>{},warn:p=>{l.push({level:"warn",message:p})}}:e.logger,d=i.map(p=>({running:`adding ${p.label}…`,task:()=>qt({allowUnsafeSource:e.allowUnsafeSource,cwd:t,from:e.registryFrom,logger:c,names:[...p.names],ref:e.ref,source:e.registrySource,transformManifest:p.transformManifest,yes:!0})})),h=`added ${i.map(p=>p.label).join(", ")}`,f=await Ut(g.add,d,h);for(const{level:p,message:k}of l)e.logger[p](k);for(const[p,k]of i.entries())k.externalAuth!==void 0&&f[p]?.code===0&&Br(t,k.externalAuth,e.logger);return f.every(p=>p.code===0)},o=e.auth===void 0?void 0:br(e.auth,i=>{e.logger.warn(i)}),s={applyAll:n,auth:o,interactive:r,logger:e.logger,multiSelect:e.prompt?.multiSelect??((i,l,c)=>_e(i,l,{...c,badge:g.add})),preselected:a.length>0?a:void 0,projectName:T(t),resolveAuthUiItem:()=>{const i=ke(t);if(!zt(i))return Kt(i)??"auth-ui-react"},select:e.prompt?.select??((i,l,c)=>O(i,l,{...c,badge:g.add})),selectAuth:e.prompt===void 0?(i,l,c)=>O(i,l,{...c,badge:g.add}):e.prompt.selectAuth??(()=>Promise.resolve("none")),text:e.prompt?.text??((i,l)=>Ie(i,{...l,badge:g.add}))};if(a.length>0||o!==void 0&&!r){const i=await de(s);return await ve(t,r,e),i}return b()&&await Ot(S.extras),await de(s),await ve(t,r,e),!0},z="react",rt=[{description:"React SPA — official create-vite base + the Lunora layer (the default)",label:"React",value:"react"},{description:"Vue SPA — create-vite base + Lunora",label:"Vue",value:"vue"},{description:"Solid SPA — create-vite base + Lunora",label:"Solid",value:"solid"},{description:"Solid 2.0 SPA — the Solid 2 line (@solidjs/web, vite-plugin-solid 3)",label:"Solid 2",value:"solid-v2"},{description:"Svelte SPA — create-vite base + Lunora",label:"Svelte",value:"svelte"},{description:"React SPA on Rsbuild — the Worker runs under wrangler dev behind the dev server",label:"React · Rsbuild",value:"rspack-react"},{description:"Next.js (App Router) — OpenNext on Cloudflare + a standalone Lunora worker",label:"Next.js",value:"next"},{description:"TanStack Start (React) — SSR with live-loader routes",label:"TanStack Start · React",value:"tanstack-start-react"},{description:"TanStack Start (React) on Rsbuild — SSR composed into the Lunora worker",label:"TanStack Start · React · Rsbuild",value:"tanstack-start-react-rspack"},{description:"TanStack Start (Solid)",label:"TanStack Start · Solid",value:"tanstack-start-solid"},{description:"Next.js App Router on Vite (vinext) — composed into the Lunora worker (experimental)",label:"vinext · App Router",value:"vinext"},{description:"Next.js Pages Router on Vite (vinext) — composed into one worker (experimental)",label:"vinext · Pages Router",value:"vinext-pages"},{description:"Astro (islands) — single-worker, Lunora composed into the adapter worker",label:"Astro",value:"astro"},{description:"AnalogJS (Angular) — single-worker, Lunora mounted in Nitro",label:"Analog",value:"analog"},{description:"Nuxt (Vue) — single-worker, Lunora mounted in Nitro",label:"Nuxt",value:"nuxt"},{description:"SvelteKit — single-worker, Lunora composed into the adapter worker",label:"SvelteKit",value:"sveltekit"},{description:"React Native (Expo) — an iOS/Android/web app + a Lunora worker backend",label:"React Native · Expo",value:"expo"},{description:"Worker only — no frontend",label:"Standalone",value:"standalone"}],at=Object.keys(j).join("|"),nt=rt.filter(e=>!V(e.value)).map(e=>e.value),ot=nt.join("|"),hn=e=>V(e)?{framework:e,kind:"overlay"}:{kind:"template",templateType:e},fn=async e=>e.vite!==void 0?{framework:e.vite,kind:"overlay"}:e.templateType!==void 0?{kind:"template",templateType:e.templateType}:!b()||e.yes===!0?{framework:z,kind:"overlay"}:hn(await O(S.framework,rt,{badge:g.tmpl,default:z})??z),mn=e=>{if(b()||e.yes===!0)return;const t=[];if(e.name===void 0&&t.push("a project name (`lunora init <name>`)"),e.templateType===void 0&&e.vite===void 0&&t.push(`a framework — \`--vite <${at}>\` for an SPA, or \`-t <${ot}>\` for a bespoke template`),t.length!==0)return`lunora init can't prompt in a non-interactive terminal — provide ${t.join(" and ")}, or pass --yes to accept the defaults.`},vn=async(e,t,r,a,n)=>V(t)?await Ge({isLocal:e.overlayBaseFrom!==void 0,logger:e.logger})?(M(a,{recursive:!0}),an({framework:t,logger:e.logger,markComplete:n,name:r,overlayBaseFrom:e.overlayBaseFrom,target:a})):{code:1,files:[],target:a}:(e.logger.error(`init: unknown framework "${t}". Supported overlays: ${Object.keys(j).join(", ")}.`),{code:w.USAGE,files:[],target:a}),gn=async(e,t,r,a,n)=>e.from!==void 0?await en(e.from,t,a,r,e.logger,n):e.source!==void 0&&e.source.length>0&&!e.allowUnsafeSource&&!Ka(e.source)?(e.logger.error(`init: refusing --source ${e.source} — only gh:, github:, or https:// sources are allowed (and may not contain ".."). Re-run with --allow-unsafe-source if you really want this.`),{code:w.USAGE,files:[],target:a}):await Ge({isLocal:!1,logger:e.logger,source:q(t,e.source,e.ref)})?tn({logger:e.logger,markComplete:n,name:r,ref:e.ref,source:e.source,target:a,templateType:t}):{code:1,files:[],target:a},wn=(e,t)=>{if(te(t))return;let r;try{r=U(e)}catch{return}r==="pnpm"&&Je(e)},yn=async(e,t,r)=>{await Rt("realtime backend on Cloudflare Workers + Durable Objects");const a=mn(e);if(a!==void 0)return e.logger.error(a),{code:w.USAGE,files:[],target:""};const n=Aa(),o=e.name??await Ie(S.name,{badge:g.dir,default:n,placeholder:n}),s=await fn(e),i=o.trim();if(i.length===0)return e.logger.error("init: refusing an empty project name — pass a directory name (e.g. `lunora init my-app`)."),{code:w.USAGE,files:[],target:""};if(i.includes("/")||i.includes("\\")||i===".."||i===".")return e.logger.error(`init: refusing project name "${i}" — must not contain path separators or be "." / "..".`),{code:w.USAGE,files:[],target:""};if(!Pa.test(i))return e.logger.error(`init: refusing project name "${i}" — a Cloudflare Worker name must be lowercase letters, digits, "-" or "_" (starting with a letter, digit or "_"), and wrangler rejects anything else. Try \`${Ba(i)}\`.`),{code:w.USAGE,files:[],target:""};const l=xe(t,i),c=ge(l,{throwIfNoEntry:!1});if(c?.isSymbolicLink()===!0)return e.logger.error(`init: refusing to scaffold into "${l}" — it is a symlink. Remove it, or scaffold into a different directory name.`),{code:w.CONFLICT,files:[],target:l};const d=c!==void 0;if(d&&Y(l).length>0)return e.logger.error(`target directory not empty: ${l}`),{code:w.CONFLICT,files:[],target:l};if(e.dryRun===!0){const f=s.kind==="overlay"?`the ${s.framework} create-vite overlay`:`the ${s.templateType} template`;return I(e.logger,`scaffold ${f} into ${l}`),{code:0,files:[],target:l}}r.record(l,d);const h=s.kind==="overlay"?await vn(e,s.framework,i,l,r.complete):await gn(e,s.templateType,i,l,r.complete);return h.code===0&&wn(l,t),h},K=(e,t)=>{const{target:r,targetPreExisted:a}=e;if(!(r===void 0||!m(r))){if(a===!0)for(const n of Y(r))R(u(r,n),{force:!0,recursive:!0});else R(r,{force:!0,recursive:!0});t.info(`removed the partially-created project at ${r}`)}},bn=async(e,t,r)=>e.inPlace!==!0?yn(e,t,r):e.dryRun===!0?(I(e.logger,`configure Lunora into ${t}`),{code:0,files:[],target:t}):dn(t,e.logger),kn=async(e,t,r)=>{const a=await pn(e,t.target),n=e.inPlace===!0?void 0:await Za(e,t.target);if(e.inPlace!==!0){await Ja(e,t.target);const o=n??U(t.target);await Xa(T(t.target),n,o,te(r)),await jr(e.logger)}return a},xn=(e,t,r)=>{if(t.code!==0||e.ci===void 0)return;if(e.dryRun===!0){I(e.logger,`scaffold a ${e.ci} CI deploy pipeline`);return}const a=e.inPlace===!0?r:t.target;try{const n=U(a);nr(a,e.ci,n,e.logger)}catch{e.logger.warn(`--ci ${e.ci}: could not detect a package manager for this project — skipped the CI pipeline.`)}},Sn=async e=>{const t=e.cwd??process.cwd(),r={};let a;try{a=await bn(e,t,{complete:()=>{r.target=void 0},record:(n,o)=>{r.target=n,r.targetPreExisted=o}}),a.code===0&&a.target!==""?(r.target=void 0,await kn(e,a,t)||(a={...a,code:1})):a.code!==0&&K(r,e.logger)}catch(n){if(n instanceof X)return K(r,e.logger),process.stdout.write("\n ✖ Setup cancelled — run `lunora init` again whenever you're ready. 🌙\n"),{code:130,files:[],target:""};throw K(r,e.logger),n}return xn(e,a,t),a},Cn=e=>typeof e=="string"&&nt.includes(e),Ln=(e,t)=>{if(e!==void 0){if(rr(e))return e;t.warn(`init: unknown --ci "${e}" — expected github | gitlab; skipping CI scaffold.`)}},An=e=>e===void 0?{}:Cn(e)?{templateType:e}:V(e)?{vite:e}:{error:`init: unknown --template "${e}" — expected a bespoke template (${ot}) or an overlay framework (${at}).`},Jn=Et(({argument:e,cwd:t,logger:r,options:a})=>{const n=An(a.template);return"error"in n?(r.error(n.error),{code:w.USAGE}):Sn({add:a.add,auth:a.auth,allowUnsafeSource:a.allowUnsafeSource===!0,cwd:t,ci:Ln(a.ci,r),dryRun:a.dryRun===!0,from:a.from,inPlace:a.here===!0,interactive:a.interactive===!0?!0:void 0,logger:r,name:e[0],ref:a.ref,source:a.source,templateType:n.templateType,vite:a.vite??n.vite,yes:a.yes===!0})});export{rt as FRAMEWORK_CHOICES,Jn as execute,Cn as isTemplate,An as resolveTemplateFlag,q as resolveTemplateSource,Sn as runInitCommand};
|
|
1311
|
+
`),t.info(f?.commit?`template: ${f.source} @ ${f.commit} (${String(k.length)} files)`:`template: ${f?.source??h} (${String(k.length)} files)`),ee(t,p,s),{code:0,files:p,target:s}}catch(d){if(d instanceof X)throw d;const{hints:h,message:f}=wr(d,{ref:Ee(n),remote:q(i,o,n),templateType:i});t.error(f);for(const p of h)t.warn(p);return{code:1,files:[],target:s}}finally{R(l,{force:!0,recursive:!0})}},rn=e=>{for(const t of["_gitignore","_npmrc","_gitattributes"]){const r=u(e,t);m(r)&<(r,u(e,`.${t.slice(1)}`))}},an=async e=>{const{framework:t,logger:r,markComplete:a,name:n,overlayBaseFrom:o,target:s}=e,i=j[t],l=we(u(ye(),"lunora-vite-base-"));try{let c;if(o!==void 0&&(c=u(o,`template-${i.createViteTemplate}`),!m(c)))return r.error(`create-vite base not found on disk: ${c}`),{code:w.NOT_FOUND,files:[],target:s};const d=async()=>{if(c!==void 0){me(c,s);return}const f=u(l,"base"),p=await _t(fe,"main",r),k=`github:${fe}/packages/create-vite/template-${i.createViteTemplate}#${p}`;await Se(k,{cwd:l,dir:f,force:!0,install:!1,silent:!0}),rn(f),me(f,s)};let h=[];return await Ne([{label:`create-vite (${i.label}) base ready`,run:d},{label:`Lunora overlay applied (${i.label})`,run:async()=>{h=await Sa({adapter:i,distTag:Ae(),logger:r,name:n,target:s})}}],{end:Ze,start:"Project initializing…"}),a(),ee(r,h,s),{code:0,files:[...W(s)],target:s}}catch(c){if(c instanceof X)throw c;const d=c instanceof Error?c.message:String(c);return r.error(`failed to scaffold the ${i.label} base: ${d}`),{code:1,files:[],target:s}}finally{R(l,{force:!0,recursive:!0})}},G=(e,t,r,a,n)=>(n.error(`init --in-place: could not ${e} ${t}: ${r instanceof Error?r.message:String(r)}`),{code:1,files:[],target:a}),nn=(e,t)=>{const r=u(e,"vite.config.ts");try{v(r,Ta,"utf8")}catch(a){return G("write",r,a,e,t)}return t.success(`created ${r} with lunora() plugin`),{code:0,files:[r],target:e}},on=(e,t,r)=>{let a;try{a=x(e,"utf8")}catch(o){return G("read",e,o,t,r)}const n=Ue(a);if(!n.changed)return r.info(`${e}: ${n.reason??"no changes needed"}`),{code:0,files:[],target:t};try{v(e,n.code,"utf8")}catch(o){return G("write",e,o,t,r)}return r.success(`patched ${e} — added lunora() plugin`),{code:0,files:[e],target:t}},sn=(e,t)=>{const r=u(e,"lunora"),a=u(r,"schema.ts");if(m(a))return t.info(`lunora/ already present — left ${a} untouched`),[];const n=[];try{M(r,{recursive:!0}),v(a,$a,"utf8"),n.push(a);const o=u(r,"messages.ts");m(o)||(v(o,Na,"utf8"),n.push(o)),t.success(`scaffolded lunora/ (${String(n.length)} file(s))`)}catch(o){const s=o instanceof Error?o.message:String(o);t.error(`init --here: could not scaffold lunora/: ${s}`)}return n},tt=e=>e==="sveltekit"||e==="nuxt"||e==="astro",ln=(e,t,r)=>{const{adapter:a,class:n,framework:o}=e;r.info(""),r.info(`detected framework: ${o} (class ${n})`),r.info("next steps:");const s=[a,"@lunora/client","@lunora/runtime","@lunora/server"];tt(o)||s.push("@lunora/vite"),r.info(` 1. install the adapter: ${qa(t,s)}`),r.info(" 2. run codegen: lunora codegen"),n==="A"?(r.info(" 3. compose one worker: wrap your worker entry with"),r.info(" createWorker({ httpRouter: <your framework SSR handler>, shardDO: ShardDO, ... })"),r.info(` 4. add the provider: mount the ${a} provider in your root layout/route`),r.info(" 5. make a loader live: preloadQuery() in a loader, usePreloadedQuery() in the component"),r.info(" see https://lunora.sh/docs/frameworks/reactive-loaders")):n==="B"?(r.info(" 3. inject Lunora: mount Lunora realtime under /_lunora/* in your server hook"),r.info(` (${o} owns its Cloudflare adapter — Lunora composes into its server entry)`),r.info(` 4. add the provider: mount the ${a} provider in your root layout`),r.info(" 5. read the guide: https://lunora.sh/docs/frameworks/deploy")):(r.info(" 3. add the provider: wrap your app with the LunoraProvider from @lunora/react"),r.info(" 4. read the guide: https://lunora.sh/docs/frameworks/bring-your-framework")),r.info("")},cn=(e,t,r)=>{if(tt(t))return r.info(`${t} wires Lunora through its server entry, not a lunora() Vite plugin — leaving the Vite config alone (see next steps)`),{code:0,files:[],target:e};const a=Ke(e);return a===void 0?nn(e,r):on(a,e,r)},dn=(e,t)=>{const r=ir(e),a=cn(e,r.framework,t);if(a.code!==0)return a;const n=sn(e,t);return ln(r,B(e),t),{code:0,files:[...a.files,...n],target:e}},un=e=>e.yes!==!0&&(e.prompt!==void 0||(e.interactive??b())),ve=async(e,t,r)=>{r.dryRun!==!0&&await Qr({apply:a=>vt(e,a),detected:mt(e),interactive:t,logger:r.logger,multiSelect:r.lintPrompt??((a,n,o)=>_e(a,n,{...o,badge:g.add}))})},pn=async(e,t)=>{const r=un(e),a=e.add===void 0?[]:Fr(e.add,i=>{e.logger.warn(i)}),n=async i=>{if(i.length===0)return!0;if(e.dryRun===!0)return I(e.logger,`add ${i.map(p=>p.externalAuth===void 0?p.label:`${p.label} (and wire resolveIdentity)`).join(", ")}`),!0;const l=[],c=b()?{error:p=>{l.push({level:"error",message:p})},info:()=>{},success:()=>{},warn:p=>{l.push({level:"warn",message:p})}}:e.logger,d=i.map(p=>({running:`adding ${p.label}…`,task:()=>qt({allowUnsafeSource:e.allowUnsafeSource,cwd:t,from:e.registryFrom,logger:c,names:[...p.names],ref:e.ref,source:e.registrySource,transformManifest:p.transformManifest,yes:!0})})),h=`added ${i.map(p=>p.label).join(", ")}`,f=await Bt(g.add,d,h);for(const{level:p,message:k}of l)e.logger[p](k);for(const[p,k]of i.entries())k.externalAuth!==void 0&&f[p]?.code===0&&Ur(t,k.externalAuth,e.logger);return f.every(p=>p.code===0)},o=e.auth===void 0?void 0:br(e.auth,i=>{e.logger.warn(i)}),s={applyAll:n,auth:o,interactive:r,logger:e.logger,multiSelect:e.prompt?.multiSelect??((i,l,c)=>_e(i,l,{...c,badge:g.add})),preselected:a.length>0?a:void 0,projectName:T(t),resolveAuthUiItem:()=>{const i=ke(t);if(!zt(i))return Kt(i)??"auth-ui-react"},select:e.prompt?.select??((i,l,c)=>O(i,l,{...c,badge:g.add})),selectAuth:e.prompt===void 0?(i,l,c)=>O(i,l,{...c,badge:g.add}):e.prompt.selectAuth??(()=>Promise.resolve("none")),text:e.prompt?.text??((i,l)=>Ie(i,{...l,badge:g.add}))};if(a.length>0||o!==void 0&&!r){const i=await de(s);return await ve(t,r,e),i}return b()&&await Ot(S.extras),await de(s),await ve(t,r,e),!0},z="react",rt=[{description:"React SPA — official create-vite base + the Lunora layer (the default)",label:"React",value:"react"},{description:"Vue SPA — create-vite base + Lunora",label:"Vue",value:"vue"},{description:"Solid SPA — create-vite base + Lunora",label:"Solid",value:"solid"},{description:"Solid 2.0 SPA — the Solid 2 line (@solidjs/web, vite-plugin-solid 3)",label:"Solid 2",value:"solid-v2"},{description:"Svelte SPA — create-vite base + Lunora",label:"Svelte",value:"svelte"},{description:"React SPA on Rsbuild — the Worker runs under wrangler dev behind the dev server",label:"React · Rsbuild",value:"rspack-react"},{description:"Next.js (App Router) — OpenNext on Cloudflare + a standalone Lunora worker",label:"Next.js",value:"next"},{description:"TanStack Start (React) — SSR with live-loader routes",label:"TanStack Start · React",value:"tanstack-start-react"},{description:"TanStack Start (React) on Rsbuild — SSR composed into the Lunora worker",label:"TanStack Start · React · Rsbuild",value:"tanstack-start-react-rspack"},{description:"TanStack Start (Solid)",label:"TanStack Start · Solid",value:"tanstack-start-solid"},{description:"Next.js App Router on Vite (vinext) — composed into the Lunora worker (experimental)",label:"vinext · App Router",value:"vinext"},{description:"Next.js Pages Router on Vite (vinext) — composed into one worker (experimental)",label:"vinext · Pages Router",value:"vinext-pages"},{description:"Astro (islands) — single-worker, Lunora composed into the adapter worker",label:"Astro",value:"astro"},{description:"AnalogJS (Angular) — single-worker, Lunora mounted in Nitro",label:"Analog",value:"analog"},{description:"Nuxt (Vue) — single-worker, Lunora mounted in Nitro",label:"Nuxt",value:"nuxt"},{description:"SvelteKit — single-worker, Lunora composed into the adapter worker",label:"SvelteKit",value:"sveltekit"},{description:"React Native (Expo) — an iOS/Android/web app + a Lunora worker backend",label:"React Native · Expo",value:"expo"},{description:"Worker only — no frontend",label:"Standalone",value:"standalone"}],at=Object.keys(j).join("|"),nt=rt.filter(e=>!V(e.value)).map(e=>e.value),ot=nt.join("|"),hn=e=>V(e)?{framework:e,kind:"overlay"}:{kind:"template",templateType:e},fn=async e=>e.vite!==void 0?{framework:e.vite,kind:"overlay"}:e.templateType!==void 0?{kind:"template",templateType:e.templateType}:!b()||e.yes===!0?{framework:z,kind:"overlay"}:hn(await O(S.framework,rt,{badge:g.tmpl,default:z})??z),mn=e=>{if(b()||e.yes===!0)return;const t=[];if(e.name===void 0&&t.push("a project name (`lunora init <name>`)"),e.templateType===void 0&&e.vite===void 0&&t.push(`a framework — \`--vite <${at}>\` for an SPA, or \`-t <${ot}>\` for a bespoke template`),t.length!==0)return`lunora init can't prompt in a non-interactive terminal — provide ${t.join(" and ")}, or pass --yes to accept the defaults.`},vn=async(e,t,r,a,n)=>V(t)?await Ge({isLocal:e.overlayBaseFrom!==void 0,logger:e.logger})?(M(a,{recursive:!0}),an({framework:t,logger:e.logger,markComplete:n,name:r,overlayBaseFrom:e.overlayBaseFrom,target:a})):{code:1,files:[],target:a}:(e.logger.error(`init: unknown framework "${t}". Supported overlays: ${Object.keys(j).join(", ")}.`),{code:w.USAGE,files:[],target:a}),gn=async(e,t,r,a,n)=>e.from!==void 0?await en(e.from,t,a,r,e.logger,n):e.source!==void 0&&e.source.length>0&&!e.allowUnsafeSource&&!Ka(e.source)?(e.logger.error(`init: refusing --source ${e.source} — only gh:, github:, or https:// sources are allowed (and may not contain ".."). Re-run with --allow-unsafe-source if you really want this.`),{code:w.USAGE,files:[],target:a}):await Ge({isLocal:!1,logger:e.logger,source:q(t,e.source,e.ref)})?tn({logger:e.logger,markComplete:n,name:r,ref:e.ref,source:e.source,target:a,templateType:t}):{code:1,files:[],target:a},wn=(e,t)=>{if(te(t))return;let r;try{r=B(e)}catch{return}r==="pnpm"&&Je(e)},yn=async(e,t,r)=>{await Rt("realtime backend on Cloudflare Workers + Durable Objects");const a=mn(e);if(a!==void 0)return e.logger.error(a),{code:w.USAGE,files:[],target:""};const n=Aa(),o=e.name??await Ie(S.name,{badge:g.dir,default:n,placeholder:n}),s=await fn(e),i=o.trim();if(i.length===0)return e.logger.error("init: refusing an empty project name — pass a directory name (e.g. `lunora init my-app`)."),{code:w.USAGE,files:[],target:""};if(i.includes("/")||i.includes("\\")||i===".."||i===".")return e.logger.error(`init: refusing project name "${i}" — must not contain path separators or be "." / "..".`),{code:w.USAGE,files:[],target:""};if(!Pa.test(i))return e.logger.error(`init: refusing project name "${i}" — a Cloudflare Worker name must be lowercase letters, digits, "-" or "_" (starting with a letter, digit or "_"), and wrangler rejects anything else. Try \`${Ua(i)}\`.`),{code:w.USAGE,files:[],target:""};const l=xe(t,i),c=ge(l,{throwIfNoEntry:!1});if(c?.isSymbolicLink()===!0)return e.logger.error(`init: refusing to scaffold into "${l}" — it is a symlink. Remove it, or scaffold into a different directory name.`),{code:w.CONFLICT,files:[],target:l};const d=c!==void 0;if(d&&Y(l).length>0)return e.logger.error(`target directory not empty: ${l}`),{code:w.CONFLICT,files:[],target:l};if(e.dryRun===!0){const f=s.kind==="overlay"?`the ${s.framework} create-vite overlay`:`the ${s.templateType} template`;return I(e.logger,`scaffold ${f} into ${l}`),{code:0,files:[],target:l}}r.record(l,d);const h=s.kind==="overlay"?await vn(e,s.framework,i,l,r.complete):await gn(e,s.templateType,i,l,r.complete);return h.code===0&&wn(l,t),h},K=(e,t)=>{const{target:r,targetPreExisted:a}=e;if(!(r===void 0||!m(r))){if(a===!0)for(const n of Y(r))R(u(r,n),{force:!0,recursive:!0});else R(r,{force:!0,recursive:!0});t.info(`removed the partially-created project at ${r}`)}},bn=async(e,t,r)=>e.inPlace!==!0?yn(e,t,r):e.dryRun===!0?(I(e.logger,`configure Lunora into ${t}`),{code:0,files:[],target:t}):dn(t,e.logger),kn=async(e,t,r)=>{const a=await pn(e,t.target),n=e.inPlace===!0?void 0:await Za(e,t.target);if(e.inPlace!==!0){await Ja(e,t.target);const o=n??B(t.target);await Xa(T(t.target),n,o,te(r)),await jr(e.logger)}return a},xn=(e,t,r)=>{if(t.code!==0||e.ci===void 0)return;if(e.dryRun===!0){I(e.logger,`scaffold a ${e.ci} CI deploy pipeline`);return}const a=e.inPlace===!0?r:t.target;try{const n=B(a);nr(a,e.ci,n,e.logger)}catch{e.logger.warn(`--ci ${e.ci}: could not detect a package manager for this project — skipped the CI pipeline.`)}},Sn=async e=>{const t=e.cwd??process.cwd(),r={};let a;try{a=await bn(e,t,{complete:()=>{r.target=void 0},record:(n,o)=>{r.target=n,r.targetPreExisted=o}}),a.code===0&&a.target!==""?(r.target=void 0,await kn(e,a,t)||(a={...a,code:1})):a.code!==0&&K(r,e.logger)}catch(n){if(n instanceof X)return K(r,e.logger),process.stdout.write("\n ✖ Setup cancelled — run `lunora init` again whenever you're ready. 🌙\n"),{code:130,files:[],target:""};throw K(r,e.logger),n}return xn(e,a,t),a},Cn=e=>typeof e=="string"&&nt.includes(e),Ln=(e,t)=>{if(e!==void 0){if(rr(e))return e;t.warn(`init: unknown --ci "${e}" — expected github | gitlab; skipping CI scaffold.`)}},An=e=>e===void 0?{}:Cn(e)?{templateType:e}:V(e)?{vite:e}:{error:`init: unknown --template "${e}" — expected a bespoke template (${ot}) or an overlay framework (${at}).`},Jn=Et(({argument:e,cwd:t,logger:r,options:a})=>{const n=An(a.template);return"error"in n?(r.error(n.error),{code:w.USAGE}):Sn({add:a.add,auth:a.auth,allowUnsafeSource:a.allowUnsafeSource===!0,cwd:t,ci:Ln(a.ci,r),dryRun:a.dryRun===!0,from:a.from,inPlace:a.here===!0,interactive:a.interactive===!0?!0:void 0,logger:r,name:e[0],ref:a.ref,source:a.source,templateType:n.templateType,vite:a.vite??n.vite,yes:a.yes===!0})});export{rt as FRAMEWORK_CHOICES,Jn as execute,Cn as isTemplate,An as resolveTemplateFlag,q as resolveTemplateSource,Sn as runInitCommand};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./admin-token-VdUnvnKW.mjs";import"./admin-url-DJB8P6Jh.mjs";import"./EXIT_CODE-DVjUgIaG.mjs";import{D as a,r as I}from"./import-
|
|
1
|
+
import"./admin-token-VdUnvnKW.mjs";import"./admin-url-DJB8P6Jh.mjs";import"./EXIT_CODE-DVjUgIaG.mjs";import{D as a,r as I}from"./import-DoAUdyfU.mjs";import"./shared-D-zCOmgY.mjs";export{a as DEFAULT_IMPORT_BATCH_SIZE,I as runImportCommand};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import{createReadStream as E}from"node:fs";import{open as st,stat as L,readdir as x,realpath as ae,readFile as v,mkdir as J,writeFile as $e,appendFile as ot}from"node:fs/promises";import{t as it,r as at}from"./admin-token-VdUnvnKW.mjs";import{r as ct,a as dt}from"./admin-url-DJB8P6Jh.mjs";import{EXIT_CODE as $,isRefusal as lt}from"./EXIT_CODE-DVjUgIaG.mjs";import{join as w,resolve as F,basename as p,dirname as Se,relative as ut,sep as ft}from"node:path";import{createInterface as M}from"node:readline";import ht from"adm-zip";import{i as ve}from"./path-containment-CgxYZggb.mjs";import{Transform as gt}from"node:stream";import{createInflateRaw as mt,crc32 as pt}from"node:zlib";import{LunoraError as f}from"@lunora/errors";import{parse as yt}from"csv-parse/sync";import{parse as Ne}from"csv-parse";import{STORAGE_UPLOAD_MAX_BODY_BYTES as wt}from"@lunora/runtime";import{S as V,a as bt,I as $t}from"./shared-D-zCOmgY.mjs";import{createHash as St}from"node:crypto";const vt=67324752,O=30,Nt=26,Et=28,ce=0,Tt=8,It=async(e,t)=>{const r=await st(e,"r");try{const n=Buffer.alloc(O),{bytesRead:s}=await r.read(n,0,O,t);if(s<O||n.readUInt32LE(0)!==vt)throw new Error(`${e}: no local file header at offset ${String(t)} — the archive is truncated or corrupt`);return t+O+n.readUInt16LE(Nt)+n.readUInt16LE(Et)}finally{await r.close()}},de=e=>{let t=0;return new gt({flush(r){if(t===e.header.crc){r();return}r(new Error(`${e.entryName} failed its CRC check — the archive is corrupt or truncated (expected ${String(e.header.crc)}, read ${String(t)})`))},transform(r,n,s){t=pt(r,t),s(void 0,r)}})},At=async(e,t)=>{const{compressedSize:r,encrypted:n,method:s,offset:o}=t.header;if(n)throw new Error(`${t.entryName} is encrypted — decrypt the archive before importing`);if(s!==ce&&s!==Tt)throw new Error(`${t.entryName} uses unsupported compression method ${String(s)} — re-create the archive with standard deflate`);if(r===0){if(t.header.size>0||t.header.crc!==0)throw new Error(`${t.entryName} declares 0 compressed bytes but ${String(t.header.size)} uncompressed with CRC ${String(t.header.crc)} — the archive is corrupt`);return}const i=await It(e,o),a=E(e,{end:i+r-1,start:i});if(s===ce){const l=de(t);return a.on("error",u=>l.destroy(u)),a.pipe(l)}const c=mt(),d=de(t);return a.on("error",l=>c.destroy(l)),c.on("error",l=>d.destroy(l)),a.pipe(c).pipe(d)},N="_storage",Ee=e=>e.startsWith("_"),Rt=e=>{for(const t of e.getEntries()){const n=t.entryName.replaceAll("\\","/").split("/");if(n.length>=2&&n[n.length-2]==="_storage")return n.slice(0,-1).join("/")}return"_storage"},_t=async e=>{const t=await L(e).catch(()=>{});if(t?.isDirectory())return{kind:"directory",root:e};if(t?.isFile()&&e.toLowerCase().endsWith(".zip")){const r=new ht(e);return{kind:"zip",storagePrefix:Rt(r),zip:r,zipPath:e}}},Ot=async e=>{const t=[];for(const r of await x(e,{withFileTypes:!0})){if(!r.isDirectory())continue;const n=w(e,r.name,"documents.jsonl");(await L(n).catch(()=>{}))?.isFile()&&t.push({file:n,table:r.name})}return t},kt=e=>{const t=[];for(const r of e.getEntries()){if(r.isDirectory)continue;const n=r.entryName.replaceAll("\\","/"),s=n.split("/");s.length>=2&&s[s.length-1]==="documents.jsonl"&&t.push({file:n,table:s[s.length-2]})}return t},Lt=async e=>{const t=e.kind==="directory"?await Ot(e.root):kt(e.zip);return t.length>0?t.toSorted((r,n)=>r.table.localeCompare(n.table)):void 0},H=async function*(e,t){if(e.kind==="directory"){for await(const o of M({crlfDelay:Number.POSITIVE_INFINITY,input:E(t.file,{encoding:"utf8"})}))yield o;return}const r=e.zip.getEntry(t.file);if(r===null)throw new Error(`missing ${t.file} in archive`);const n=await At(e.zipPath,r);if(n===void 0)return;const s=M({crlfDelay:Number.POSITIVE_INFINITY,input:n});try{for await(const o of s)yield o}finally{s.close(),n.destroy()}},xt=async(e,t)=>{if(e.kind==="directory"){const n=await ae(w(e.root,"_storage")),s=await ae(F(n,t)).catch(()=>{});if(s===void 0||!ve(n,s))throw new Error(`blob ${t} resolves outside the snapshot's _storage directory`);return v(s)}const r=e.zip.readFile(`${e.storagePrefix}/${t}`);if(r===null)throw new Error(`missing blob ${t} in archive`);return Buffer.from(r)},j=207,Ct=e=>{const t={conflicts:0,errors:[],failed:[],inserted:{},received:0,replaced:0,warnings:[]};let r=[],n=0;const s=a=>{for(const[c,d]of Object.entries(a.inserted??{}))t.inserted[c]=(t.inserted[c]??0)+d;t.errors.push(...a.errors??[]),t.failed.push(...a.failed??[]),t.conflicts+=a.conflicts??0,t.received+=a.received??0,t.replaced+=a.replaced??0;for(const c of a.warnings??[])t.warnings.includes(c)||t.warnings.push(c)},o=async()=>{if(r.length===0)return;const a=r.join(`
|
|
2
|
+
`);r=[],n=0;const c=await e.fetchImpl(e.requestUrl,{body:a,headers:{authorization:`Bearer ${e.token}`,"content-type":"application/x-ndjson"},method:"POST"});if(!c.ok&&c.status!==j){const l=await c.text().catch(()=>"<no body>");throw new f("INTERNAL",`import batch failed (HTTP ${String(c.status)}): ${l}`)}const d=await c.json();s(d),c.status===j&&(d.failed??[]).length===0&&t.failed.push({message:`the endpoint answered ${String(j)} without naming the failed shards`,shardKey:"<unknown>",timedOut:!1})};return{flush:o,push:async a=>{const c=Buffer.byteLength(a)+1;r.length>0&&n+c>e.maxBatchBytes&&await o(),r.push(a),n+=c,r.length>=e.batchSize&&await o()},totals:t}},Te=(e,t,r,n)=>{const s=[],o=[];let i=0;const a=l=>n?.[r]?.includes(l)===!0,c=(l,u,h=!1)=>{if(Array.isArray(l))return l.map(g=>c(g,u,h));if(l!==null&&typeof l=="object"){const g=l;if(typeof g.$storage=="string"){const m=g.$storage,y=t.get(m);return y===void 0?(o.push({column:u,storageId:m,table:r}),l):(i+=1,y)}return Object.fromEntries(Object.entries(g).map(([m,y])=>[m,c(y,u)]))}return typeof l=="string"&&t.has(l)?a(u)?(i+=1,t.get(l)??l):(s.push({column:u,storageId:l,table:r}),l):(h&&typeof l=="string"&&l.length>0&&a(u)&&n!==void 0&&o.push({column:u,storageId:l,table:r}),l)},d=Object.fromEntries(Object.entries(e).map(([l,u])=>[l,c(u,l,!0)]));return{ambiguous:s,document:d,rewritten:i,unmigrated:o}},le=(e,t,r)=>{for(const n of r){const s=`${n.table}\0${n.column}\0${n.storageId}`;t.has(s)||(t.add(s),e.push(n))}},Ut=e=>{const{remapDocument:t,report:r,storageColumns:n,storageIdMap:s,table:o}=e,i=new Set,a=new Set,c=(u,h)=>{let g;try{g=JSON.parse(u)}catch(m){const y=m instanceof Error?m.message:String(m);throw new f("INTERNAL",`invalid JSON on line ${String(h)}: ${y}`,{cause:m})}return JSON.stringify({doc:g,table:o})},d=(u,h)=>{let g;try{g=JSON.parse(u)}catch(y){throw new f("INTERNAL",`line ${String(h)}: import envelope is not valid JSON — ${y instanceof Error?y.message:String(y)}`,{cause:y})}const m=g===null||typeof g!="object"||Array.isArray(g)?void 0:g;if(m===void 0||typeof m.table!="string")throw new f("INTERNAL",`line ${String(h)}: import envelope is missing a string \`table\``);return m},l=u=>{if(u.doc!==null&&typeof u.doc=="object"&&!Array.isArray(u.doc)){let h=u.doc;if(s!==void 0){const g=Te(h,s,u.table,n);h=g.document,r.rewritten+=g.rewritten,le(r.ambiguous,i,g.ambiguous),le(r.unmigrated,a,g.unmigrated)}return JSON.stringify({...u,doc:t===void 0?h:t(h,u.table)})}return JSON.stringify(u)};return(u,h)=>{const g=u.trim();if(g.length===0)return;if(o!==void 0)return c(g,h);const m=d(g,h);return s===void 0&&t===void 0?g:l(m)}},Ie=async(e,t,r,n)=>{const s=await n(e).catch(()=>{});if(s===void 0)throw new f("INTERNAL",`${e} is not a readable directory`);const o=new Map;for(const[d,l]of Object.entries(t?.tables??{}))l.file!==void 0&&o.set(p(l.file),d);const i=s.filter(d=>d.isFile()&&r.matches(d.name)&&!r.authFiles.has(d.name)).map(d=>({file:w(e,d.name),table:o.get(d.name)??r.tableNameOf(d.name)}));if(i.length===0)throw new f("INTERNAL",`${e} ${r.emptyMessage}`);const a=new Set(i.map(d=>p(d.file))),c=[...o].filter(([d])=>!a.has(d));if(c.length>0)throw new f("INTERNAL",`${e}: the mapping names ${String(c.length)} file(s) that are not importable from this directory — ${c.map(([d,l])=>`\`${d}\` (table \`${l}\`)`).join(", ")}. Check the name and that the file is one this source reads.`);return i.toSorted((d,l)=>d.table.localeCompare(l.table))},Ae=async function*(e,t,r,n){for(const s of e){r.has(s.table)||r.set(s.table,0),t.info(`reading ${p(s.file)} → ${s.table}`);for await(const o of n(s))r.set(s.table,(r.get(s.table)??0)+1),yield`${JSON.stringify({doc:o,table:s.table})}
|
|
3
|
+
`}},Re=["timestamp-ms","timestamp-iso","json","bytea-base64","int8-string","number","boolean","text-array"],Pt=e=>Re.includes(e),jt=/^\\x([\dA-Fa-f]*)$/,ue=/^[+-]?\d+$/,_e=/[+-]\d{2}$/,Oe=/(?:Z|[+-]\d{2}:\d{2})$/i,ke=/^\+/,Ft=/e/i,Mt=/e/i,Dt=e=>{const[t="0",r="0"]=e.split(Mt),n=Number(r),s=t.startsWith("-"),[o="0",i=""]=(s?t.slice(1):t).replace(ke,"").split("."),a=`${o}${i}`,c=o.length+n;let d;return c<=0?d=`0.${"0".repeat(-c)}${a}`:c>=a.length?d=`${a}${"0".repeat(c-a.length)}`:d=`${a.slice(0,c)}.${a.slice(c)}`,s?`-${d}`:d},zt=/^0+(?=\d)/,fe=e=>{const r=(Ft.test(e.trim())?Dt(e.trim()):e.trim()).replace(ke,""),n=r.startsWith("-"),[s="0",o=""]=(n?r.slice(1):r).split("."),i=s.replace(zt,"");let a=o.length;for(;a>0&&o[a-1]==="0";)a-=1;const c=o.slice(0,a),d=c.length>0?`${i}.${c}`:i;return n&&Number(d)!==0?`-${d}`:d},b=(e,t,r,n)=>{throw new f("INTERNAL",`column \`${e}\`: cannot reshape ${JSON.stringify(r)} as \`${t}\` — ${n}`)},he=(e,t,r)=>{const n=r.includes("T")?r:r.replace(" ","T"),s=_e.test(n)?`${n}:00`:n,o=Oe.test(s)?s:`${s}Z`,i=Date.parse(o);return Number.isNaN(i)&&b(e,t,r,"not a date Postgres or ISO-8601 syntax can express"),i},Bt=(e,t)=>{(!t.startsWith("{")||!t.endsWith("}"))&&b(e,"text-array",t,"not a Postgres array literal (expected `{…}`)");const r=t.slice(1,-1);if(r.length===0)return[];r.includes("{")&&b(e,"text-array",t,"nested arrays are not supported — map the column to `json` instead");const n=[];let s="",o=!1,i=!1,a=!1;const c=()=>{n.push(!i&&s==="NULL"?null:s),s="",i=!1};for(const d of r)a?(s+=d,a=!1):d==="\\"?a=!0:d==='"'?(o=!o,i=!0):d===","&&!o?c():s+=d;return c(),n},Le=(e,t,r)=>{if(r===null)return null;switch(t){case"boolean":return["1","t","TRUE","true"].includes(r)?!0:["0","f","FALSE","false"].includes(r)?!1:b(e,t,r,"not a Postgres boolean literal");case"bytea-base64":{const n=jt.exec(r);if(n===null)return b(e,t,r,"not `bytea` hex output (expected a leading `\\x`) — set `bytea_output = 'hex'` before dumping");const s=n[1];return s.length%2!==0?b(e,t,r,"has an odd number of hex digits, so the dump is truncated — re-export the column"):Buffer.from(s,"hex").toString("base64")}case"int8-string":return ue.test(r)?r:b(e,t,r,"not an integer");case"json":try{return JSON.parse(r)}catch(n){return b(e,t,r,`invalid JSON — ${n instanceof Error?n.message:String(n)}`)}case"number":{const n=Number(r);if(r.trim().length===0||!Number.isFinite(n))return b(e,t,r,"not a finite number");if(ue.test(r)){if(!Number.isSafeInteger(n))return b(e,t,r,"exceeds Number.MAX_SAFE_INTEGER — map this column to `int8-string` to keep it lossless")}else if(fe(String(n))!==fe(r))return b(e,t,r,"has more precision than a JS number holds — map this column to `int8-string` or `json` to keep it lossless");return n}case"text-array":return Bt(e,r);case"timestamp-iso":{he(e,t,r);const n=r.includes("T")?r:r.replace(" ","T"),s=_e.test(n)?`${n}:00`:n;return Oe.test(s)?s:`${s}Z`}case"timestamp-ms":return he(e,t,r);default:return r}},Jt="auth.",Vt=new Set(["confirmation_token","email_change_token_current","email_change_token_new","encrypted_password","password","password_hash","passwordhash","reauthentication_token","recovery_token","salt"]),Ht=String.raw`\N`,xe=(e,t)=>t.header?e:!t.quoting&&e.length===0||e===Ht?null:e,Gt=e=>new Set([e?.auth?.file,e?.auth?.identitiesFile].filter(t=>t!==void 0).map(t=>p(t))),qt=async(e,t)=>Ie(e,t,{authFiles:Gt(t),emptyMessage:"holds no .csv files — export each table with `COPY <table> TO STDOUT WITH CSV HEADER` first",matches:r=>r.toLowerCase().endsWith(".csv")&&!r.toLowerCase().startsWith(Jt),tableNameOf:r=>p(r,".csv")},async r=>x(r,{withFileTypes:!0})),Wt=(e,t,r)=>{const n=t?.idColumn??"id",s=t?.types??{},o={};let i=!1;for(const[a,c]of Object.entries(e)){if(Vt.has(a.toLowerCase()))throw new f("INTERNAL",`${r}.${a} is credential material — this looks like an auth dump being imported as a table. Name it under \`auth\` in the mapping instead; passwords are never migrated.`);const d=s[a],l=d===void 0?c:Le(a,d,c);if(a===n){if(c===null)throw new f("INTERNAL",`id column \`${n}\` is NULL — every row needs an id to preserve`);o._id=c,i=!0}else{if(a==="_id")throw new f("INTERNAL",`${r}: source column \`_id\` collides with the reserved id field. Rename it in the source, or map it via \`tables.${r}.idColumn\` if it IS the id.`);o[a]=l}}if(!i)throw new f("INTERNAL",`${r}: no \`${n}\` column to preserve as the id (columns present: ${Object.keys(e).join(", ")}). Set \`tables.${r}.idColumn\` in the mapping.`);return o},Kt=async function*(e,t){const r=t?.tables?.[e.table],n=E(e.file).pipe(Ne({cast:xe,columns:!0,relaxColumnCountLess:!1,skipEmptyLines:!0}));let s=0;const o=n[Symbol.asyncIterator]();for(;;){let i,a;try{if(i=await o.next(),i.done===!0)return;s+=1,a=Wt(i.value,r,e.table)}catch(c){throw new f("INTERNAL",`${p(e.file)} row ${String(s+1)}: ${c instanceof Error?c.message:String(c)}`,{cause:c})}yield a}},Yt=async function*(e,t,r,n,s){yield*Pe("supabase",s,t,r,n),yield*Ae(e,r,n,o=>Kt(o,t))},Xt=/^\d+$/,Zt=/[+-]\d{2}$/,D=e=>{if(e==null)return;if(typeof e=="number")return e;const t=Number(e);if(Number.isFinite(t)&&Xt.test(e))return t;const r=e.includes("T")?e:e.replace(" ","T"),n=Date.parse(Zt.test(r)?`${r}:00`:r);return Number.isNaN(n)?void 0:n},Ce=e=>{if(typeof e=="string")try{const t=JSON.parse(e);return t!==null&&typeof t=="object"?t:void 0}catch{return}return e!==null&&typeof e=="object"?e:void 0},Qt=e=>{const t=Ce(e);if(t!==void 0){for(const r of["name","full_name","user_name","preferred_username"])if(typeof t[r]=="string"&&t[r].length>0)return t[r]}},er=e=>{const t=Ce(e);if(t!==void 0){for(const r of["avatar_url","picture"])if(typeof t[r]=="string"&&t[r].length>0)return t[r]}},tr=(e,t)=>{const{id:r}=e;if(typeof r!="string"||r.length===0)throw new f("INTERNAL","auth row is missing `id` — every user needs an id to preserve");const n=e.raw_user_meta_data,s={_id:r,email:e.email??null,emailVerified:typeof e.email_confirmed_at=="string"&&e.email_confirmed_at.length>0,id:r},o=Qt(n),i=er(n),a=D(e.created_at),c=D(e.updated_at);return o!==void 0&&(s.name=o),i!==void 0&&(s.image=i),a!==void 0&&(s.createdAt=a),c!==void 0&&(s.updatedAt=c),{accounts:t.map(l=>{const u=typeof l.provider=="string"?l.provider:"unknown",h=typeof l.provider_id=="string"?l.provider_id:r,g=`${r}:${u}:${h}`;return{_id:g,accountId:h,id:g,providerId:u,userId:r}}),user:s}},rr=e=>{const t=e.localId;if(typeof t!="string"||t.length===0)throw new f("INTERNAL","auth row is missing `localId` — every user needs an id to preserve");const r={_id:t,email:e.email??null,emailVerified:e.emailVerified===!0,id:t},n=D(e.createdAt);return typeof e.displayName=="string"&&(r.name=e.displayName),typeof e.photoUrl=="string"&&(r.image=e.photoUrl),n!==void 0&&(r.createdAt=n),{accounts:(e.providerUserInfo??[]).filter(o=>typeof o.providerId=="string"&&o.providerId!=="password").map(o=>{const i=o.providerId,a=o.rawId??o.federatedId??t,c=`${t}:${i}:${a}`;return{_id:c,accountId:a,id:c,providerId:i,userId:t}}),user:r}},nr=(e,t)=>{const r=new Map,n=[],s=[];for(const{accounts:o,user:i}of e){const a=typeof i.email=="string"?i.email.toLowerCase():void 0;if(a!==void 0&&a.length>0){const c=r.get(a);c===void 0?r.set(a,String(i._id)):n.push(`${a} (ids ${c} and ${String(i._id)})`)}s.push(`${JSON.stringify({doc:i,table:"user"})}
|
|
4
|
+
`),t.set("user",(t.get("user")??0)+1);for(const c of o)s.push(`${JSON.stringify({doc:c,table:"account"})}
|
|
5
|
+
`),t.set("account",(t.get("account")??0)+1)}if(n.length>0)throw new f("INTERNAL",`auth import found ${String(n.length)} duplicate email(s), which would merge distinct users: ${n.slice(0,10).join("; ")}${n.length>10?" …":""}`);return s},G=(e,t)=>w(e,p(t)),Ue=async e=>{const t=await v(e,"utf8");return yt(t,{cast:xe,columns:!0,skipEmptyLines:!0})},sr=async(e,t)=>{const r=new Map;if(t===void 0)return r;for(const n of await Ue(G(e,t))){const s=n.user_id;typeof s=="string"&&r.set(s,[...r.get(s)??[],n])}return r},or=async(e,t)=>{const r=t.auth?.file;if(r===void 0)return[];const n=await Ue(G(e,r)),s=await sr(e,t.auth?.identitiesFile);return n.map(o=>tr(o,s.get(o.id??"")??[]))},ir=async(e,t)=>{const r=t.auth?.file;if(r===void 0)return[];const n=G(e,r);let s;try{s=JSON.parse(await v(n,"utf8"))}catch(i){throw new f("INTERNAL",`${p(n)}: invalid JSON — ${i instanceof Error?i.message:String(i)}`,{cause:i})}const o=Array.isArray(s)?s:s.users??[];if(!Array.isArray(o))throw new f("INTERNAL",`${p(n)}: expected \`{ users: [...] }\` from \`firebase auth:export\`, or a bare array`);return o.map(i=>rr(i))},Pe=async function*(e,t,r,n,s){if(r?.auth?.file===void 0)return;const o=e==="supabase"?await or(t,r):await ir(t,r);n.info(`auth: ${String(o.length)} user(s) — passwords are never migrated; users reset via "forgot password"`);for(const i of nr(o,s))yield i},ge=/\.(?:nd)?json$/i,je=e=>{const t=e.split("/").filter(r=>r.length>0);return t[t.length-1]??e},ar=e=>{const t=Number(e.seconds??0),r=e.nanos??0;return Number.isFinite(t)&&Number.isFinite(r)?t*1e3+Math.floor(r/1e6):Number.NaN},cr=(e,t)=>{const r=Array.isArray(e)?e:e.data;if(!Array.isArray(r))throw new f("INTERNAL",`${t}: \`bytesValue\` ${JSON.stringify(e)} is neither base64 nor a byte array`);return Buffer.from(r).toString("base64")},dr=(e,t)=>{const r=typeof e=="string"?Date.parse(e):ar(e);if(Number.isNaN(r))throw new f("INTERNAL",`${t}: \`timestampValue\` ${JSON.stringify(e)} is neither an RFC-3339 string nor a \`{ seconds, nanos }\` protobuf timestamp`);return r},lr=(e,t)=>typeof e=="string"?e:cr(e,t),Fe=(e,t)=>{if("nullValue"in e)return null;if(e.stringValue!==void 0)return e.stringValue;if(e.booleanValue!==void 0)return e.booleanValue;if(e.integerValue!==void 0){const r=String(e.integerValue),n=Number(r);return Number.isSafeInteger(n)?n:r}if(e.doubleValue!==void 0)return Number(e.doubleValue);if(e.timestampValue!==void 0)return dr(e.timestampValue,t);if(e.bytesValue!==void 0)return lr(e.bytesValue,t);if(e.geoPointValue!==void 0)return{latitude:e.geoPointValue.latitude??0,longitude:e.geoPointValue.longitude??0};if(e.referenceValue!==void 0)return je(e.referenceValue);if(e.arrayValue!==void 0)return(e.arrayValue.values??[]).map((r,n)=>Fe(r,`${t}[${String(n)}]`));if(e.mapValue!==void 0)return z(e.mapValue.fields??{},t);throw new f("INTERNAL",`${t}: unrecognised Firestore value ${JSON.stringify(e).slice(0,80)}`)},ur=new Set(["createTime","fields","name","readTime","updateTime"]),fr=e=>{if(typeof e!="object"||e===null||Array.isArray(e))return!1;const t=Object.keys(e);return t.length===1&&t[0]?.endsWith("Value")===!0},hr=e=>{if(typeof e.name=="string")return!0;const{fields:t}=e;return typeof t!="object"||t===null?!1:Object.keys(e).every(r=>ur.has(r))&&!fr(t)},z=(e,t)=>Object.fromEntries(Object.entries(e).map(([r,n])=>[r,Fe(n,`${t}.${r}`)])),gr=(e,t,r,n)=>{const s=hr(e),o=z(s?e.fields??{}:e,n),i=e.name??e.__name__,a=typeof i=="string"?je(i):t;if(a===void 0)throw new f("INTERNAL",`${n}: no document id — expected a \`name\`/\`__name__\` resource path, or a document keyed by its id`);const c=r?.types??{};for(const[d,l]of Object.entries(c)){const u=o[d];if(u!==void 0){if(u!==null&&typeof u!="boolean"&&typeof u!="number"&&typeof u!="string")throw new f("INTERNAL",`${n}.${d}: a \`${l}\` reshape needs a scalar, but this field decoded to an object or array`);o[d]=Le(d,l,u===null?null:String(u))}}return{...o,_id:a}},mr=async(e,t)=>Ie(e,t,{authFiles:new Set(t?.auth?.file===void 0?[]:[p(t.auth.file)]),emptyMessage:"holds no .json/.ndjson collection files",matches:r=>ge.test(r),tableNameOf:r=>r.replace(ge,"")},async r=>x(r,{withFileTypes:!0})),pr=async function*(e){const t=M({crlfDelay:Number.POSITIVE_INFINITY,input:E(e,"utf8")});let r=0;try{for await(const n of t){r+=1;const s=n.trim();if(s.length!==0)try{yield{raw:JSON.parse(s)}}catch(o){throw new f("INTERNAL",`${p(e)} line ${String(r)}: invalid JSON — ${o instanceof Error?o.message:String(o)}`,{cause:o})}}}finally{t.close()}},yr=async e=>{const t=await v(e,"utf8");let r;try{r=JSON.parse(t)}catch(n){throw new f("INTERNAL",`${p(e)}: invalid JSON — ${n instanceof Error?n.message:String(n)}`,{cause:n})}if(r!==null&&typeof r=="object"&&Array.isArray(r.documents))return r.documents.map(n=>({raw:n}));if(Array.isArray(r))return r.map(n=>({raw:n}));if(r!==null&&typeof r=="object")return Object.entries(r).map(([n,s])=>({fallbackId:n,raw:s}));throw new f("INTERNAL",`${p(e)}: expected an object, an array, or \`{ documents: [...] }\``)},wr=async function*(e,t){const r=t?.tables?.[e.table],n=e.file.toLowerCase().endsWith(".ndjson")?pr(e.file):await yr(e.file);let s=0;for await(const o of n)yield gr(o.raw,o.fallbackId,r,`${e.table}[${String(s)}]`),s+=1},br=async function*(e,t,r,n,s){yield*Pe("firebase",s,t,r,n),yield*Ae(e,r,n,o=>wr(o,t))},I=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),q=(e,t)=>{if(!I(e))throw new f("INTERNAL",`${t}: expected a JSON object`);return e},T=(e,t,r)=>{const n=e[t];if(n!==void 0&&typeof n!="string")throw new f("INTERNAL",`${r}: \`${t}\` must be a string`);return n},Me=(e,t,r)=>{const n=e[t];if(n!==void 0&&(!Array.isArray(n)||n.some(s=>typeof s!="string")))throw new f("INTERNAL",`${r}: \`${t}\` must be an array of column names`);return n},$r=(e,t)=>{const r=q(e,t),n=T(r,"file",t),s=T(r,"idColumn",t),o=Me(r,"storageColumns",t),{types:i}=r;if(i!==void 0){if(!I(i))throw new f("INTERNAL",`${t}.types must be an object of column → reshape`);for(const[a,c]of Object.entries(i))if(!Pt(c))throw new f("INTERNAL",`${t}.types.${a}: unknown reshape ${JSON.stringify(c)} — expected one of ${Re.join(", ")}`)}return{file:n,idColumn:s,storageColumns:o,types:i}},Sr=(e,t)=>{if(e!==void 0){if(!I(e))throw new f("INTERNAL",`${t}: \`auth\` must be an object`);for(const r of["file","identitiesFile"])T(e,r,`${t}: auth`)}},vr=(e,t)=>{if(e!==void 0){if(!I(e))throw new f("INTERNAL",`${t}: \`tables\` must be an object of table → mapping`);return Object.fromEntries(Object.entries(e).map(([r,n])=>[r,$r(n,`${t}: tables.${r}`)]))}},Nr=(e,t)=>{const r=q(e,t),n=T(r,"keyPrefix",t),{auth:s,tables:o}=r;return Sr(s,t),{auth:s,keyPrefix:n,tables:vr(o,t)}},De=e=>w("lunora",`import-${e}.json`),Er=async(e,t,r)=>{const n=De(t),s=w(e,n);let o;try{o=await v(s,"utf8")}catch(a){if(a.code==="ENOENT"){r.info(`no ${n} found — every column is copied through untouched (run with --scan to generate one)`);return}throw a}let i;try{i=JSON.parse(o)}catch(a){throw new f("INTERNAL",`${s}: invalid JSON — ${a instanceof Error?a.message:String(a)}`,{cause:a})}return Nr(i,s)},Tr=async e=>{const t=[];let r;for(;;){const n=await e(r);if(t.push(...Array.isArray(n.records)?n.records:[]),n.truncated!==!0||typeof n.cursor!="string"||n.cursor.length===0)return t;if(n.cursor===r)throw new Error("collectPages: the list did not advance its cursor — refusing to page forever");r=n.cursor}},Ir=8,Ar=24*1048576,Rr=1e3,_r=/^[\dA-F]{64}$/i,Or=e=>{for(let t=0;t<e.length;t+=1){const r=e.codePointAt(t)??0;if(r<32||r===127)return!1}return!0},kr=/^[\d+/A-Z]{43}=$/i,Lr=e=>_r.test(e)?e.toLowerCase():kr.test(e)?Buffer.from(e,"base64").toString("hex"):void 0,xr=(e,t)=>{const r=JSON.parse(e),n=r._id;if(typeof n!="string"||n.length===0||n.includes("/")||n.includes("\\"))throw new f("INTERNAL",`${t}: \`_id\` must be a path-free non-empty string`);if(typeof r.sha256!="string")throw new f("INTERNAL",`${t}: \`sha256\` is missing — re-export with \`npx convex export --include-file-storage\``);const s=Lr(r.sha256);if(s===void 0)throw new f("INTERNAL",`${t}: \`sha256\` is neither base16 nor base64 SHA-256 (${r.sha256})`);if(typeof r.size!="number"||!Number.isInteger(r.size)||r.size<0)throw new f("INTERNAL",`${t}: \`size\` must be a non-negative integer`);if(r.contentType!==void 0&&(typeof r.contentType!="string"||!Or(r.contentType)))throw new f("INTERNAL",`${t}: \`contentType\` must be a string with no control characters`);return{contentType:typeof r.contentType=="string"?r.contentType:void 0,id:n,sha256:s,size:r.size}},C=async(e,t,r)=>{const n=[];try{let s=0;for await(const o of H(e,t)){const i=o.trim();s+=1,i.length>0&&n.push(xr(i,`_storage/documents.jsonl line ${String(s)}`))}}catch(s){const o=s instanceof Error?s.message:String(s);throw r.error(`failed to read _storage metadata: ${o}`),s}return n},ze=wt,U=e=>e.bucket===void 0?"":`&bucket=${encodeURIComponent(e.bucket)}`,W=async(e,t)=>await Tr(async r=>{const n=`${e.baseUrl}${V}?prefix=${encodeURIComponent(t)}&limit=${String(Rr)}${r===void 0?"":`&cursor=${encodeURIComponent(r)}`}${U(e)}`,s=await e.fetchImpl(n,{headers:{authorization:`Bearer ${e.token}`},method:"GET"});if(!s.ok){const i=await s.text().catch(()=>"<no body>");throw new f("INTERNAL",`storage list failed (HTTP ${String(s.status)}): ${i}`)}const o=await s.json();return{cursor:o.cursor,records:o.objects,truncated:o.truncated}}),Cr=async(e,t,r,n)=>{const s=`${e.baseUrl}${V}?key=${encodeURIComponent(t)}&expectedSha256=${n.sha256}&expectedSize=${String(n.size)}${U(e)}`,o=await e.fetchImpl(s,{body:new Uint8Array(r),headers:{authorization:`Bearer ${e.token}`,"content-type":n.contentType??"application/octet-stream"},method:"PUT"});if(!o.ok){const a=await o.text().catch(()=>"<no body>");throw new f("INTERNAL",`blob upload failed (HTTP ${String(o.status)}): ${a}`)}const i=await o.json();if(i.sha256!==n.sha256)throw new f("INTERNAL",`blob upload verification failed: expected ${n.sha256}, got ${i.sha256??"none"}`);return t},Ur=async(e,t)=>(await e.fetchImpl(`${e.baseUrl}${V}?key=${encodeURIComponent(t)}${U(e)}`,{headers:{authorization:`Bearer ${e.token}`},method:"DELETE"}).catch(()=>{}))?.ok===!0,Pr=async(e,t,r,n,s)=>{const o=`${e.baseUrl}${bt}?key=${encodeURIComponent(t)}&method=PUT&contentType=${encodeURIComponent(n.contentType??"application/octet-stream")}${U(e)}`,i=await e.fetchImpl(o,{headers:{authorization:`Bearer ${e.token}`},method:"GET"});if(!i.ok){const m=await i.text().catch(()=>"<no body>");throw new f("INTERNAL",`blob ${t} is ${String(n.size)} bytes, above the ${String(ze)}-byte verified-upload cap, and no signed PUT URL could be minted (HTTP ${String(i.status)}): ${m}`)}const{url:a}=await i.json(),c=await e.fetchImpl(a,{body:new Uint8Array(r),headers:{"content-type":n.contentType??"application/octet-stream"},method:"PUT"});if(!c.ok){const m=await c.text().catch(()=>"<no body>");throw new f("INTERNAL",`signed PUT failed (HTTP ${String(c.status)}): ${m}`)}const l=(await W(e,t)).find(m=>m.key===t);if(l===void 0)throw new f("INTERNAL",`post-upload verification failed: blob not found at key ${t}`);const u=l.size!==void 0&&l.size!==n.size,h=l.sha256!==void 0&&l.sha256.toLowerCase()!==n.sha256;if(u||h){const m=await Ur(e,t);throw new f("INTERNAL",`post-upload verification failed: expected sha256=${n.sha256} size=${String(n.size)}, got sha256=${l.sha256??"none"} size=${String(l.size??"none")}${m?" (the object was removed)":` — AND the object could not be removed: delete ${t} by hand before re-running, or the next run will treat it as already migrated`}`)}const g=[l.size===void 0?"size":void 0,l.sha256===void 0?"sha256":void 0].filter(Boolean);return g.length>0&&s.warn(`blob ${t} went through the signed-PUT path and the host reports no ${g.join(" or ")} for it — that much of the write is unverified`),t},Be=async(e,t,r,n,s)=>{if(r.length!==n.size)throw new f("INTERNAL",`blob ${n.id} is ${String(r.length)} bytes on disk but the export declares ${String(n.size)}`);return r.length<=ze?Cr(e,t,r,n):Pr(e,t,r,n,s)},jr=e=>{const t=[];let r=[],n=0;for(const s of e)r.length>0&&(r.length>=Ir||n+s.size>Ar)&&(t.push(r),r=[],n=0),r.push(s),n+=s.size;return r.length>0&&t.push(r),t},B=(e,t)=>`${e}${t.sha256}`,Fr=async(e,t,r,n)=>{const s=await C(e,t,n);return new Map(s.map(o=>[o.id,B(r,o)]))},Mr=async(e,t,r,n,s)=>{const o=await C(t,r,s),i=new Map,a=await W(e,n),c=new Map(a.map(u=>[u.key,u])),d=[];for(const u of o){const h=B(n,u);c.get(h)?.size===u.size?i.set(u.id,h):d.push(u)}s.info(`migrating ${String(d.length)} storage blobs${i.size>0?` (${String(i.size)} already present)`:""}...`);const l=async u=>{try{const h=await xt(t,u.id);i.set(u.id,await Be(e,B(n,u),h,u,s))}catch(h){const g=h instanceof Error?h.message:String(h);throw s.error(`failed to upload blob ${u.id}: ${g}`),h}};for(const u of jr(d)){const g=(await Promise.allSettled(u.map(m=>l(m)))).find(m=>m.status==="rejected");if(g!==void 0)throw g.reason}return s.success(`migrated ${String(d.length)} storage blobs`),i};async function*Dr(e,t,r){let n=0;for await(const s of H(e,t)){const o=s.trim();if(n+=1,o.length===0)continue;let i;try{i=JSON.parse(o)}catch(a){throw new f("INTERNAL",`${t.table}/documents.jsonl line ${String(n)}: invalid JSON — ${a instanceof Error?a.message:String(a)}`,{cause:a})}r.set(t.table,(r.get(t.table)??0)+1),yield`${JSON.stringify({doc:i,table:t.table})}
|
|
6
|
+
`}}async function*zr(e,t,r,n,s){for(const o of t){if(Ee(o.table)){o.table===N&&!n&&r.warn(`skipping "${N}" — those rows describe stored files, and their blobs were not migrated. Re-run with --with-storage to upload them and rewrite the references.`);continue}s.has(o.table)||s.set(o.table,0),yield*Dr(e,o,s)}}const fs=["firebase","supabase"],Br=async(e,t,r)=>{if(e.withStorage===!0&&t==="firebase"&&e.storageDir===void 0)return e.logger.error("--with-storage on Firebase needs --storage-dir — download the bucket first with `gcloud storage cp -r gs://<bucket> <dir>`."),{kind:"invalid"};if(e.table!==void 0)return e.logger.error(`--table cannot be combined with --from ${t} — each row's table comes from its source file.`),{kind:"invalid"};const n=await Er(r,t,e.logger),s=Object.values(n?.tables??{}).some(o=>(o.storageColumns??[]).length>0);return e.verify===!0&&e.withStorage!==!0&&s?(e.logger.error("--verify with `storageColumns` declared requires --with-storage — otherwise every storage path stays unmigrated and only row counts would be checked."),{kind:"invalid"}):t==="supabase"?{kind:"supabase",mapping:n,tables:await qt(e.file,n)}:{collections:await mr(e.file,n),kind:"firebase",mapping:n}},Jr=async e=>{if(!await L(e.file).then(()=>!0,()=>!1))return!1;for(const[r,n]of[["--scan",e.scan],["--verify",e.verify],["--with-storage",e.withStorage]])if(n===!0)return e.logger.error(`${r} requires a Convex export directory or .zip snapshot — ${e.file} is not one.`),!0;return!1},Vr=async(e,t,r)=>{const n=t.find(o=>o.table===N);if(n===void 0)return r.logger.error("--verify cannot check file references: this export has no `_storage` table, so it was taken without `--include-file-storage`. Re-export with that flag and pass --with-storage, or drop --verify."),!0;const s=await C(e,n,r.logger);return s.length>0?(r.logger.error(`--verify on an export carrying ${String(s.length)} stored file(s) requires --with-storage — otherwise every file reference stays unmigrated and only row counts would be checked.`),!0):!1},Hr=async(e,t)=>{if(e.from==="supabase"||e.from==="firebase")return Br(e,e.from,t);const r=await _t(e.file),n=r===void 0?void 0:await Lt(r);return r!==void 0&&n===void 0?(e.logger.error(`${e.file} is a ${r.kind==="zip"?".zip":"directory"} but holds no <table>/documents.jsonl — expected a \`npx convex export --path\` snapshot, or pass an NDJSON file.`),{kind:"invalid"}):r===void 0||n===void 0?await Jr(e)?{kind:"invalid"}:{kind:"ndjson"}:e.table!==void 0?(e.logger.error("--table cannot be combined with a Convex export directory — each row's table comes from its source directory."),{kind:"invalid"}):e.verify===!0&&e.withStorage!==!0&&await Vr(r,n,e)?{kind:"invalid"}:{kind:"convex",snapshot:r,tables:n}},k=w("lunora","import-convex.json"),Gr=(e,t)=>{const r=q(e,t),n=T(r,"keyPrefix",t),s=r.storageColumns;if(s!==void 0){if(!I(s))throw new f("INTERNAL",`${t}: \`storageColumns\` must be an object of table → column names`);for(const o of Object.keys(s))Me(s,o,`${t}: storageColumns`)}return{keyPrefix:n,storageColumns:s}},qr=async(e,t)=>{const r=w(e,k);let n;try{n=await v(r,"utf8")}catch(o){if(o.code==="ENOENT"){t.info(`no ${k} found — rewriting only self-describing { $storage } refs (run with --scan to generate one)`);return}throw o}let s;try{s=JSON.parse(n)}catch(o){throw new f("INTERNAL",`${r}: invalid JSON — ${o instanceof Error?o.message:String(o)}`,{cause:o})}return Gr(s,r)},Wr=(e,t,r)=>{try{return JSON.parse(e)}catch(n){throw new f("INTERNAL",`${t}/documents.jsonl line ${String(r)}: invalid JSON — ${n instanceof Error?n.message:String(n)}`,{cause:n})}},Kr=async(e,t,r)=>{const n=new Map([...r].map(i=>[i,i])),s=[];let o=0;for await(const i of H(e,t)){const a=i.trim();if(o+=1,a.length===0)continue;const{ambiguous:c}=Te(Wr(a,t.table,o),n,t.table);for(const{column:d}of c)s.includes(d)||s.push(d)}return s},Yr=async(e,t,r)=>{const n={};for(const s of t){if(Ee(s.table))continue;const o=await Kr(e,s,r);o.length>0&&(n[s.table]=o)}return n},Xr=async(e,t,r)=>{const n=w(t,k),s=`${JSON.stringify(e,void 0,4)}
|
|
7
|
+
`;await J(w(t,"lunora"),{recursive:!0});try{await $e(n,s,{encoding:"utf8",flag:"wx"}),r.success(`wrote candidate mapping to ${n} — review it, then re-run without --scan`)}catch(o){if(o.code!=="EEXIST")throw o;r.warn(`${n} already exists — leaving it untouched. Candidate mapping:`),r.info(s)}},Zr=async(e,t,r,n)=>{const s=t.find(c=>c.table===N);if(s===void 0){n.error("no `_storage` table in this export — re-export with `npx convex export --include-file-storage`");return}const o=await C(e,s,n),i=new Set(o.map(c=>c.id));n.info(`found ${String(i.size)} storage ids`);const a={keyPrefix:"",storageColumns:await Yr(e,t,i)};return await Xr(a,r,n),a},S=20,Qr=(e,t,r)=>{let n=0;if(r.conflicts===0)for(const[s,o]of t){const i=r.inserted[s]??0;i<o&&(n+=1,e.error(`verify: ${s} inserted ${String(i)} of ${String(o)} source rows (${String(o-i)} missing)`))}else{const s=[...t.values()].reduce((i,a)=>i+a,0),o=Object.values(r.inserted).reduce((i,a)=>i+a,0)+r.conflicts;o<s&&(n+=1,e.error(`verify: ${String(o)} of ${String(s)} source rows accounted for across all tables (${String(s-o)} missing; ${String(r.conflicts)} already present)`))}return n>0?e.error(`verify: ${String(n)} row-parity check(s) failed`):e.success("verify: all source rows accounted for"),n},me=(e,t,r)=>{for(const n of t.slice(0,S))e.warn(r(n));t.length>S&&e.warn(`… and ${String(t.length-S)} more`)},Je=(e,t,r)=>(e.info(`storage refs: ${String(t.rewritten)} rewritten, ${String(t.unmigrated.length)} unmigrated, ${String(t.ambiguous.length)} ambiguous`),me(e,t.unmigrated,n=>`unmigrated storage reference ${n.table}.${n.column}: ${n.storageId} has no exported blob — re-export with \`npx convex export --include-file-storage\``),me(e,t.ambiguous,n=>`unrewritten storage id in ${n.table}.${n.column}: ${n.storageId} — if that column holds storage references, add it to ${k} and re-import`),r&&t.unmigrated.length>0?(e.error(`verify: ${String(t.unmigrated.length)} storage reference(s) resolved to no migrated blob`),!0):!1),en=(e,t,r)=>{for(const n of[...t].slice(0,S))e.warn(`storage path never transferred: ${n} — left as-is`);return t.size>S&&e.warn(`… and ${String(t.size-S)} more untransferred storage paths`),r&&t.size>0},pe=200,tn=/^\\x[\dA-Fa-f]*$/,rn=/^[+-]?\d+$/,nn=/^[+-]?\d+\.\d+$/,sn=/^\{.*\}$/,on=/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}/,an=[["bytea-base64",e=>tn.test(e)],["timestamp-ms",e=>on.test(e)],[e=>e.some(t=>!Number.isSafeInteger(Number(t)))?"int8-string":void 0,e=>rn.test(e)],["boolean",e=>e==="f"||e==="t"],["number",e=>nn.test(e)],[e=>e.every(t=>sn.test(t))?"text-array":"json",e=>e.startsWith("{")||e.startsWith("[")]],cn=e=>{if(e.length!==0){for(const[t,r]of an)if(e.every(n=>r(n)))return typeof t=="function"?t(e):t}},dn=(e,t)=>{for(const[r,n]of Object.entries(e)){if(n.length===0)continue;const s=t.get(r);s===void 0?t.set(r,[n]):s.push(n)}},ln=async e=>{const t=new Map,r=E(e).pipe(Ne({columns:!0,skipEmptyLines:!0,toLine:pe+1}));let n=0;for await(const o of r)if(dn(o,t),n+=1,n>=pe)break;r.destroy();const s={};for(const[o,i]of t){const a=cn(i);a!==void 0&&(s[o]=a)}return s},Ve=async(e,t,r,n)=>{const s=De(r),o=w(t,s),i=`${JSON.stringify(e,void 0,4)}
|
|
8
|
+
`;await J(Se(o),{recursive:!0});try{await $e(o,i,{encoding:"utf8",flag:"wx"}),n.success(`wrote candidate mapping to ${o} — review the inferred types, then re-run without --scan`)}catch(a){if(a.code!=="EEXIST")throw a;n.warn(`${o} already exists — leaving it untouched. Candidate mapping:`),n.info(i)}},un=async(e,t,r)=>{const n={};for(const o of e){const i=await ln(o.file);r.info(`${p(o.file)} → ${o.table}: ${String(Object.keys(i).length)} column(s) need a reshape`),n[o.table]={file:p(o.file),idColumn:"id",...Object.keys(i).length>0?{types:i}:{}}}const s={keyPrefix:"",tables:n};return await Ve(s,t,"supabase",r),s},fn=async(e,t,r)=>{const n={};for(const o of e)n[o.table]={file:p(o.file),storageColumns:[]};r.info(`found ${String(e.length)} collection(s) — Firestore values are self-describing, so only storage columns need declaring`);const s={keyPrefix:"",tables:n};return await Ve(s,t,"firebase",r),s},K=e=>w("lunora",`.import-storage-${e}.ndjson`),hn=async(e,t,r)=>{const n=new Map;let s;try{s=await v(w(e,K(t)),"utf8")}catch(i){if(i.code==="ENOENT")return n;throw i}let o=0;for(const i of s.split(`
|
|
9
|
+
`)){const a=i.trim();if(a.length!==0)try{const c=JSON.parse(a);typeof c.path=="string"&&typeof c.key=="string"?n.set(c.path,c):o+=1}catch{o+=1}}return n.size>0&&r.info(`resuming: ${String(n.size)} object(s) already transferred${o>0?` (${String(o)} unreadable checkpoint line(s) ignored)`:""}`),n},gn=async(e,t,r)=>{const n=w(e,K(t));await J(Se(n),{recursive:!0}),await ot(n,`${JSON.stringify(r)}
|
|
10
|
+
`,"utf8")},He=100,mn=e=>e.split("/").map(t=>encodeURIComponent(t)).join("/"),pn=async(e,t,r,n,s)=>{const o=await r(`${e.url}/storage/v1/object/list/${encodeURIComponent(t)}`,{body:JSON.stringify({limit:He,offset:s,prefix:n}),headers:{authorization:`Bearer ${e.serviceKey}`,"content-type":"application/json"},method:"POST"});if(!o.ok){const i=await o.text().catch(()=>"<no body>");throw new f("INTERNAL",`Supabase storage list failed for bucket ${t} (HTTP ${String(o.status)}): ${i}`)}return await o.json()},Ge=async(e,t,r,n="")=>{const s=[];let o=0;for(;;){const i=await pn(e,t,r,n,o);for(const a of i){const c=n===""?a.name:`${n}/${a.name}`;a.id===null||a.id===void 0?s.push(...await Ge(e,t,r,c)):s.push({contentType:a.metadata?.mimetype,name:c})}if(i.length<He)return s;o+=i.length}},yn=async(e,t,r)=>{const n=await t(`${e.url}/storage/v1/bucket`,{headers:{authorization:`Bearer ${e.serviceKey}`},method:"GET"});if(!n.ok){const i=await n.text().catch(()=>"<no body>");throw new f("INTERNAL",`Supabase bucket list failed (HTTP ${String(n.status)}): ${i} — check the project URL and that the key is the service-role key, not the anon key`)}const s=await n.json(),o=[];for(const i of s){const a=await Ge(e,i.name,t);r.info(`supabase bucket ${i.name}: ${String(a.length)} object(s)`);for(const c of a){const d=`${i.name}/${c.name}`;o.push({contentType:c.contentType,bytes:async()=>{const l=await t(`${e.url}/storage/v1/object/${encodeURIComponent(i.name)}/${mn(c.name)}`,{headers:{authorization:`Bearer ${e.serviceKey}`},method:"GET"});if(!l.ok){const u=await l.text().catch(()=>"<no body>");throw new f("INTERNAL",`Supabase download failed for ${d} (HTTP ${String(l.status)}): ${u}`)}if(l.arrayBuffer===void 0)throw new f("INTERNAL","the fetch implementation cannot read response bytes, which the storage transfer requires");return Buffer.from(await l.arrayBuffer())},path:d})}}return o},wn=async e=>{const t=F(e),r=[],n=async s=>{const o=await x(s,{withFileTypes:!0}).catch(()=>{});if(o===void 0)throw new f("INTERNAL",`${e} is not a readable directory — download the bucket first with \`gcloud storage cp -r gs://<bucket> <dir>\``);for(const i of o){const a=F(s,i.name);if(!ve(t,a))throw new f("INTERNAL",`${i.name} resolves outside ${e} — refusing to upload it`);i.isDirectory()?await n(a):i.isFile()&&r.push({bytes:async()=>v(a),path:ut(t,a).split(ft).join("/")})}};return await n(t),r},bn=async(e,t,r,n,s)=>{const o=await t.bytes(),i=St("sha256").update(o).digest("hex"),a=`${r.keyPrefix}${i}`;if(n.get(a)?.size!==o.length){const c={contentType:t.contentType,id:t.path,sha256:i,size:o.length};await Be(e,a,o,c,s)}return await gn(r.cwd,r.source,{key:a,path:t.path,size:o.length}),a},$n=(e,t)=>{const r=e>500?100:25;return n=>{if(n===e||n%r===0){const s=e===0?100:Math.round(n/e*100);t.info(`transferred ${String(n)}/${String(e)} object(s) (${String(s)}%)`)}}},ye=async(e,t,r,n)=>{const s=new Map,o=await hn(r.cwd,r.source,n),i=await W(e,r.keyPrefix),a=new Map(i.map(l=>[l.key,l]));o.size>0&&i.length===0&&n.warn(`the checkpoint records ${String(o.size)} transferred object(s) but the target holds none under \`${r.keyPrefix}\` — re-transferring (a different deployment, a wiped bucket, or a changed keyPrefix)`);const c=$n(t.length,n);let d=0;n.info(`transferring ${String(t.length)} object(s) to R2...`);for(const l of t){const u=o.get(l.path);if(u!==void 0&&a.has(u.key)){s.set(l.path,u.key),d+=1,c(d);continue}try{const h=await bn(e,l,r,a,n);s.set(l.path,h)}catch(h){const g=h instanceof Error?h.message:String(h);throw n.error(`failed transferring ${l.path} after ${String(d)} object(s): ${g}`),n.error(`progress is saved — re-run the same command to continue from here (delete ${K(r.source)} to start over)`),h}d+=1,c(d)}return n.success(`transferred ${String(t.length)} object(s) to R2`),s},we=/^\/+/,Sn=/\/storage\/v1\/object\/(?:public\/|sign\/|authenticated\/)?/,vn=/[#?]/u,Nn=e=>{const t=new Map(e),r=new Map;for(const[n,s]of e){const o=n.indexOf("/");if(o===-1)continue;const i=n.slice(o+1);i.length===0||e.has(i)||r.set(i,r.has(i)?void 0:s)}for(const[n,s]of r)s!==void 0&&t.set(n,s);return t},En=e=>{const t=Sn.exec(e);if(t===null)return;const r=e.slice(t.index+t[0].length).split(vn)[0]??"";if(r.length!==0)try{return decodeURIComponent(r)}catch{return r}},Tn=(e,t)=>{const r=t.get(e);if(r!==void 0)return r;const n=e.replace(we,""),s=t.get(n);if(s!==void 0)return s;const o=En(e);return o===void 0?void 0:t.get(o)??t.get(o.replace(we,""))},In=500,An=9e5,Rn=async e=>{if(e.prod&&e.url===void 0)return e.logger.error("--prod requires an explicit --url (refusing to import to the implicit localhost worker)"),{refused:$.USAGE};const t=ct(e.url,e.logger,e.cwd);if(t===void 0)return{refused:$.USAGE};if(it({prod:e.prod,url:t})&&e.yes!==!0)return e.logger.error(`import bulk-writes ${t}, which is not local. Re-run with --yes to confirm.`),{refused:$.USAGE};const{token:r}=at({cwd:e.cwd??process.cwd(),token:e.token,url:t});if(!r)return e.logger.error("admin token required — pass --token, set LUNORA_ADMIN_TOKEN, or add it to .dev.vars (local targets only)"),{refused:$.AUTH};try{const s=await L(e.file);if(!s.isFile()&&!s.isDirectory())return e.logger.error(`not a file or directory: ${e.file}`),{refused:$.USAGE}}catch(s){const o=s instanceof Error?s.message:String(s);return e.logger.error(`failed to stat ${e.file}: ${o}`),{refused:$.NOT_FOUND}}const n=e.fetchImpl??dt;return{baseUrl:t,fetchImpl:n,requestUrl:`${t}${$t}`,token:r}},qe=(e,t,r)=>({conflicts:e.conflicts,errors:e.errors,...e.failed.length>0?{failed:e.failed}:{},inserted:e.inserted,received:e.received,replaced:e.replaced,...t===void 0?{}:{storage:{ambiguous:r.ambiguous.slice(0,S),ambiguousTotal:r.ambiguous.length,blobs:t.size,rewritten:r.rewritten,unmigrated:r.unmigrated.slice(0,S),unmigratedTotal:r.unmigrated.length}},...e.warnings.length>0?{warnings:e.warnings}:{}}),_n=(e,t,r,n)=>{switch(e.kind){case"convex":return zr(e.snapshot,e.tables,t.logger,r,n);case"firebase":return br(e.collections,e.mapping,t.logger,n,t.file);case"supabase":return Yt(e.tables,e.mapping,t.logger,n,t.file);default:return E(t.file,{encoding:"utf8"})}},On=async(e,t,r)=>{switch(e.kind){case"convex":return Zr(e.snapshot,e.tables,t,r);case"firebase":return fn(e.collections,t,r);case"supabase":return un(e.tables,t,r);default:{r.error("--scan needs a Convex, Supabase, or Firebase source.");return}}},kn=async(e,t,r,n)=>{const s=t.mapping?.keyPrefix??"";if(r.storageDir!==void 0)return ye(e,await wn(r.storageDir),{cwd:n,keyPrefix:s,source:t.kind},r.logger);if(t.kind==="firebase"){r.logger.error("--with-storage needs --storage-dir for a Firebase source: download the bucket first (`gcloud storage cp -r gs://<bucket> ./storage`), then point --storage-dir at it.");return}const o=process.env.SUPABASE_URL,i=process.env.SUPABASE_SERVICE_ROLE_KEY;if(o===void 0||i===void 0){r.logger.error("--with-storage needs SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in the environment (the service-role key, not the anon key), or --storage-dir pointing at an already-downloaded bucket.");return}let a=o.length;for(;a>0&&o[a-1]==="/";)a-=1;const c=o.slice(0,a);if(!c.startsWith("https://")){r.logger.error(`SUPABASE_URL must be https:// — refusing to send the service-role key over ${c.split(":")[0]??"an unknown scheme"}.`);return}const d=await yn({serviceKey:i,url:c},e.fetchImpl,r.logger);return ye(e,d,{cwd:n,keyPrefix:s,source:t.kind},r.logger)},Ln=(e,t,r,n,s)=>{const o=n.mapping?.tables?.[t]?.storageColumns??[];if(o.length===0)return e;const i={...e};for(const a of o){const c=i[a];if(typeof c!="string"||c.length===0)continue;const d=Tn(c,r);d===void 0?s.add(`${t}.${a}: ${c}`):i[a]=d}return i},xn=e=>{const t=e.source.kind==="supabase"||e.source.kind==="firebase"?e.source:void 0,{transferredPaths:r}=e,n=r===void 0||t===void 0?void 0:Nn(r);return Ut({remapDocument:n===void 0||t===void 0?void 0:(s,o)=>Ln(s,o,n,t,e.unresolvedPaths),report:e.report,storageColumns:e.storageColumns,storageIdMap:e.storageIdMap,table:e.table})},Cn=async(e,t,r,n)=>{let s="",o=0;const i=async a=>{o+=1;const c=t(a,o);c!==void 0&&await r.push(c)};try{for await(const a of e){s+=typeof a=="string"?a:a.toString("utf8");let c=s.indexOf(`
|
|
11
|
+
`);for(;c!==-1;)await i(s.slice(0,c)),s=s.slice(c+1),c=s.indexOf(`
|
|
12
|
+
`)}s.length>0&&await i(s),await r.flush();return}catch(a){const c=a instanceof Error?a.message:String(a);return r.totals.received===0?(n.error(`import failed before any batch was acknowledged: ${c}`),n.error("re-run the same command once the worker is reachable (existing rows conflict rather than duplicate)")):(n.error(`import failed part-way through: ${c}`),n.error("the rows below had already been written — re-run the same command to resume (existing rows conflict rather than duplicate)")),a}},Un=async(e,t,r)=>{if(t.withStorage!==!0||e.kind!=="convex")return{};const n=await qr(r,t.logger),s=e.tables.find(o=>o.table===N);if(s===void 0){t.logger.error("--with-storage requires a Convex export with a `_storage` metadata table — re-export with `npx convex export --include-file-storage`.");return}return{mapping:n,storageIdMap:await Fr(e.snapshot,s,n?.keyPrefix??"",t.logger)}},Pn=async(e,t,r,n)=>{const s=t.tables.find(i=>i.table===N);if(s===void 0)return;const o=await Mr(e,t.snapshot,s,r.mapping?.keyPrefix??"",n);return n.info(`storage map: ${String(o.size)} blobs mapped`),{mapping:r.mapping,storageIdMap:o}},jn=async(e,t,r,n,s)=>{if(r.withStorage!==!0)return{};if(t.kind==="convex")return Pn(e,t,s,r.logger);if(t.kind!=="supabase"&&t.kind!=="firebase")return{};try{const o=await kn(e,t,r,n);return o===void 0?void 0:{transferredPaths:o}}catch(o){r.logger.error(`storage transfer failed: ${o instanceof Error?o.message:String(o)}`),r.logger.error("no rows were imported — fix the error above and re-run; objects that did transfer are checkpointed and will be skipped");return}},Fn=(e,t)=>{for(const o of t.warnings)e.warn(o);const r=t.received-t.insertedTotal-t.replaced-t.conflicts-t.errorCount;r>0&&e.warn(`${String(r)} of ${String(t.received)} rows were neither inserted, replaced, conflicted, nor reported as errors`);const n=t.replaced>0?`${String(t.replaced)} replaced, `:"",s=`imported ${String(t.insertedTotal+t.replaced)} of ${String(t.received)} rows (${n}${String(t.conflicts)} conflicts, ${String(t.errorCount)} errors)`;t.failed?e.error(s):e.success(s)},Mn=async(e,t,r)=>await On(e,t,r.logger)===void 0?{body:void 0,code:$.USAGE,error:"import --scan: the export could not be scanned",inserted:0}:{body:void 0,code:0,inserted:0},Dn="GLOBAL_DRY_RUN_UNSUPPORTED",zn={error:()=>{},info:()=>{},success:()=>{},warn:()=>{}},Bn=e=>({...e,logger:zn}),be=async(e,t,r,n)=>{const s=new Set,o={ambiguous:[],rewritten:0,unmigrated:[]},i=new Map,a=_n(t,e,r.storageIdMap!==void 0,i),c=Ct({batchSize:n.batchSize,fetchImpl:n.fetchImpl,maxBatchBytes:An,requestUrl:n.requestUrl,token:n.token}),d=xn({report:o,source:t,storageColumns:r.mapping?.storageColumns,storageIdMap:r.storageIdMap,table:e.table,transferredPaths:r.transferredPaths,unresolvedPaths:s}),l=await Cn(a,d,c,e.logger);return{batcher:c,remapReport:o,sourceRows:i,streamFailure:l,unresolvedPaths:s}},Y=e=>e instanceof Error?e.message:typeof e=="string"?e:"an unknown failure",We=e=>{const{conflicts:t,inserted:r}=e.batcher.totals;return t>0||Object.values(r).some(n=>n>0)},Jn=(e,t,r,n)=>{const s=qe(t.batcher.totals,n,t.remapReport),o=We(t);if(e.format!=="json"&&e.logger.info(JSON.stringify(s,void 0,2)),t.streamFailure!==void 0){const c=Y(t.streamFailure);e.logger.error(t.batcher.totals.received===0?`import failed before any batch was acknowledged: ${c}`:`import failed while checking the rows: ${c}`),e.logger.error(o?"rows may already be written by the check; check the shard before running the command again":"no rows were written; re-run the same command once the worker is reachable")}for(const c of t.batcher.totals.failed)e.logger.error(`import: shard "${c.shardKey}" was never reached${c.timedOut?" (timed out)":""} — no rows were written: ${c.message}`);const i=Object.values(t.batcher.totals.inserted).reduce((c,d)=>c+d,0),a=o?`import: ${e.file} was refused, and rows may already be written: ${r}`:`import: ${e.file} was not written: ${r}`;return{body:s,code:1,data:{file:e.file,inserted:i,summary:s},error:a,inserted:i}},Vn=(e,t)=>{if(e.streamFailure!==void 0)return`the check stopped early: ${Y(e.streamFailure)}`;const{errors:r,failed:n}=e.batcher.totals;if(We(e))return"this worker wrote rows during its check, so it cannot check an import before writing, and some rows may already be written";if(n.length>0)return`${String(n.length)} shard(s) could not be reached to check`;const s=r.filter(i=>i.code!==Dn);if(s.length>0)return`${String(s.length)} row(s) would be refused`;const o=e.remapReport.unmigrated.length;return t&&o>0?`${String(o)} storage reference(s) resolve to no exported blob`:void 0},Hn=(e,t)=>{e.verify===!0&&t.remapReport.unmigrated.length>0&&Je(e.logger,t.remapReport,!0)},Gn=(e,t)=>{for(const r of t)e.error(`import: shard "${r.shardKey}" was never reached${r.timedOut?" (timed out)":""} — its rows were NOT written: ${r.message}`)},qn=async(e,t)=>{try{const r=await e.fetchImpl(`${e.requestUrl}?dryRun=true${t?"&onConflict=replace":""}`,{body:`
|
|
13
|
+
`,headers:{authorization:`Bearer ${e.token}`,"content-type":"application/x-ndjson"},method:"POST"});if(!r.ok)return`the worker at ${e.requestUrl} answered HTTP ${String(r.status)} to the check`;const n=await r.json();return n.dryRun!==!0?`the worker at ${e.requestUrl} predates ?dryRun=true, so it would write the file while checking it; deploy the current runtime and import again`:t&&n.onConflict!=="replace"?`the worker at ${e.requestUrl} predates ?onConflict=replace, so --replace would overwrite nothing; deploy the current runtime and import again`:void 0}catch(r){return`the worker at ${e.requestUrl} did not answer the check: ${Y(r)}`}},Wn=(e,t)=>{const r=t?"onConflict=replace":void 0;return{check:`${e}?dryRun=true${r===void 0?"":`&${r}`}`,write:r===void 0?e:`${e}?${r}`}},hs=async e=>{const t=e.cwd??process.cwd(),r=await Hr(e,t);if(r.kind==="invalid")return{body:void 0,code:$.USAGE,error:`import: could not read ${e.file}`,inserted:0};if(e.scan===!0)return Mn(r,t,e);const n=await Rn(e);if(lt(n))return{body:void 0,code:n.refused,error:"import: could not resolve the worker URL and admin token",inserted:0};const{baseUrl:s,fetchImpl:o,token:i}=n,a=e.replace===!0,c=Wn(n.requestUrl,a),{write:d}=c,u={batchSize:e.batchSize??In,fetchImpl:o,requestUrl:n.requestUrl,token:i},h=await qn(u,a);if(h!==void 0)return{body:void 0,code:1,error:`import: ${e.file} was not written: ${h}`,inserted:0};const g=await Un(r,e,t);if(g===void 0)return{body:void 0,code:1,error:"import: the storage phase failed — no rows were written",inserted:0};const m=await be(Bn(e),r,g,{...u,requestUrl:c.check}),y=Vn(m,e.verify===!0);if(y!==void 0)return Hn(e,m),Jn(e,m,y,void 0);const P=await jn({baseUrl:s,fetchImpl:o,token:i},r,e,t,g);if(P===void 0)return{body:void 0,code:1,error:"import: the storage phase failed — no rows were written",inserted:0};const{storageIdMap:X}=P;e.logger.info(r.kind==="convex"?`POST ${d} -> import Convex export ${e.file} (${String(r.tables.length)} tables)`:`POST ${d} -> import ${e.file}`);const{batcher:Z,remapReport:Q,sourceRows:Ke,streamFailure:ee,unresolvedPaths:Ye}=await be(e,r,P,{...u,requestUrl:d}),{conflicts:te,errors:re,failed:ne,inserted:se,received:Xe,replaced:A,warnings:Ze}=Z.totals,Qe=e.verify===!0&&ee===void 0?Qr(e.logger,Ke,{conflicts:te+A,inserted:se}):0,et=X!==void 0&&Je(e.logger,Q,e.verify===!0),tt=en(e.logger,Ye,e.verify===!0),R=Object.values(se).reduce((rt,nt)=>rt+nt,0),_=qe(Z.totals,X,Q);Gn(e.logger,ne);const oe=ee!==void 0||re.length>0||ne.length>0||Qe>0||et||tt;e.format!=="json"&&e.logger.info(JSON.stringify(_,void 0,2)),Fn(e.logger,{conflicts:te,errorCount:re.length,failed:oe,insertedTotal:R,received:Xe,replaced:A,warnings:Ze});const ie={file:e.file,inserted:R,summary:_};return oe?{body:_,code:1,data:ie,error:`import: ${e.file} did not import cleanly`,inserted:R,replaced:A}:{body:_,code:0,data:ie,inserted:R,replaced:A}};export{In as D,fs as I,ze as M,U as b,W as l,hs as r,Be as u};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import{existsSync as p,mkdirSync as _,writeFileSync as w,mkdtempSync as C,rmSync as F,readFileSync as U}from"node:fs";import{tmpdir as O}from"node:os";import{discoverMigrations as k,discoverSchema as x,readPackageDependencies as D}from"@lunora/codegen";import{isInteractive as G,promptSelect as P,promptText as j}from"@lunora/config";import{LunoraError as T}from"@lunora/errors";import{join as m}from"@visulima/path";import{Project as v}from"ts-morph";import{r as z,t as L}from"./admin-token-VdUnvnKW.mjs";import{r as B,a as q,n as H}from"./admin-url-DJB8P6Jh.mjs";import{isRefusal as W,exitCodeForStatus as J,EXIT_CODE as d}from"./EXIT_CODE-DVjUgIaG.mjs";import{diffSnapshots as K,renderMigrationFile as V}from"./diffSnapshots-htqNZT8Q.mjs";import X from"./schemaIrToSnapshot-BHuNOfgs.mjs";import{runExportCommand as Z}from"./runExportCommand-DiMCQ-Il.mjs";import{r as Q}from"./import-DoAUdyfU.mjs";import{readAndLogBody as Y}from"../packem_chunks/runRpcCommand.mjs";const h="__lunora_reproject__",ee=e=>{if(!e.startsWith(h))return;const r=e.slice(h.length);return r===""?void 0:r},re=".snapshot.json",N=/[^\da-z]+/gu,M=(e,r)=>{let t=0,o=e.length;for(;t<o&&e[t]===r;)t+=1;for(;o>t&&e[o-1]===r;)o-=1;return e.slice(t,o)},te=e=>{const r=M(e.toLowerCase().replaceAll(N,"_"),"_");return r===""?"auto":r},oe=e=>{const r=(t,o=2)=>t.toString().padStart(o,"0");return`${String(e.getUTCFullYear())}${r(e.getUTCMonth()+1)}${r(e.getUTCDate())}${r(e.getUTCHours())}${r(e.getUTCMinutes())}${r(e.getUTCSeconds())}`},ae=e=>{if(p(e))try{const r=U(e,"utf8"),t=JSON.parse(r);if(t.version!==1)throw new T("INTERNAL",`unsupported snapshot version: ${t.version}`);return t}catch(r){const t=r instanceof Error?r.message:String(r);throw new T("INTERNAL",`failed to read ${e}: ${t}`,{cause:r})}},Pe=e=>{const r=e.cwd??process.cwd(),t=m(r,"lunora","schema.ts");if(!p(t)){const f=`schema not found: ${t} — run \`vis generate lunora-table --name=<name>\` to create one`;return e.logger.error(f),{code:d.NOT_FOUND,empty:!0,error:f,migrationFile:""}}const o=new v({skipAddingFilesFromTsConfig:!0}),n=x(o,t),l=X(n),c=m(r,"lunora","migrations"),s=m(c,re);let a;try{a=ae(s)}catch(f){const S=f instanceof Error?f.message:String(f);return e.logger.error(S),{code:d.USAGE,empty:!0,error:S,migrationFile:""}}const i=K(a,l);if(i.empty)return e.logger.info("no schema changes detected — snapshot is already up to date"),{code:0,empty:!0,migrationFile:""};const g=(e.now??(()=>new Date))(),$=te(e.name??"auto"),y=`${oe(g)}_${$}.sql`,b=m(c,y);_(c,{recursive:!0});const R=V($,i,g.toISOString());return w(b,R,"utf8"),w(s,`${JSON.stringify(l,void 0,4)}
|
|
2
|
+
`,"utf8"),e.logger.success(`wrote ${b}`),i.unsupported.length>0&&e.logger.warn(`${String(i.unsupported.length)} unsupported diff(s) — see the comment block in ${y} and write the SQL manually`),{code:0,empty:!1,migrationFile:b}},ne="migrations.ts",A=/^[A-Za-z_]\w*$/u,ie=new Set(["await","break","case","catch","class","const","continue","debugger","default","delete","do","else","enum","export","extends","false","finally","for","function","if","implements","import","in","instanceof","interface","let","new","null","package","private","protected","public","return","static","super","switch","this","throw","true","try","typeof","var","void","while","with","yield"]),E=/^import\s*\{\s*defineMigration\s*\}\s*from\s*["'](?:@lunora\/server|lunorash\/server)["'][^\n]*$/mu,se=e=>`import { defineMigration } from "${D(e)?.has("lunorash")??!1?"lunorash/server":"@lunora/server"}";`,ce="__lunora_admin__:runMigration",de="__lunora_admin__:migrationStatus",le="/_lunora/migrate",ue=e=>M(e.trim().toLowerCase().replaceAll(N,"-"),"-"),me=e=>e.split("-").filter(r=>r.length>0).map((r,t)=>t===0?r:r.charAt(0).toUpperCase()+r.slice(1)).join(""),ge=e=>{const r=m(e,"lunora","schema.ts");if(!p(r))return[];try{const t=new v({skipAddingFilesFromTsConfig:!0});return x(t,r).tables.map(o=>o.name)}catch{return[]}},fe=async e=>e.length>0?P("Which table does this migration iterate?",e.map(r=>({label:r,value:r}))):j("Target table for the migration: "),he=async(e,r)=>{if(r.table!==void 0)return r.table;const t=r.promptTable??(G()?fe:void 0);if(t===void 0){r.logger.error("migrate create requires a target table when not running interactively — re-run with --table <table>");return}const n=(await t(ge(e)))?.trim();if(n===void 0||n===""){r.logger.error("no table selected — re-run with --table <table>");return}return n},je=async e=>{const r=e.cwd??process.cwd(),t=ue(e.name);if(t==="")return e.logger.error(`invalid migration name: "${e.name}" — must contain at least one alphanumeric character`),{code:d.USAGE,file:""};const o=me(t);if(!A.test(o)||ie.has(o))return e.logger.error(`invalid migration name: "${e.name}" derives the export \`${o}\`, which is not a valid identifier — pick a name that starts with a letter and isn't a reserved word`),{code:d.USAGE,file:""};const n=await he(r,e);if(n===void 0)return{code:d.USAGE,file:""};if(!A.test(n))return e.logger.error(`invalid table: "${n}" — must be a valid identifier ([A-Za-z_][A-Za-z0-9_]*)`),{code:d.USAGE,file:""};const l=m(r,"lunora"),c=m(l,ne);let s=p(c)?U(c,"utf8"):"";if(s.includes(`id: "${t}"`)||new RegExp(String.raw`\bexport const ${o}\b`,"u").test(s))return e.logger.error(`a migration with id "${t}" (export \`${o}\`) already exists in ${c}`),{code:d.CONFLICT,file:""};const a=se(r);s.trim()===""?s=`${a}
|
|
3
|
+
`:E.test(s)?s=s.replace(E,a):s=`${a}
|
|
4
|
+
${s}`;const i=`export const ${o} = defineMigration({
|
|
5
|
+
id: "${t}",
|
|
6
|
+
table: "${n}",
|
|
7
|
+
up: (document) => document,
|
|
8
|
+
});`;return _(l,{recursive:!0}),w(c,`${s.trimEnd()}
|
|
9
|
+
|
|
10
|
+
${i}
|
|
11
|
+
`,"utf8"),e.logger.success(`scaffolded migration "${t}" in ${c}`),{code:0,file:c}},pe=(e,r)=>{const t=new v({skipAddingFilesFromTsConfig:!0});return k(t,m(e,"lunora")).find(n=>n.id===r)?.table},be=(e,r)=>{const t=ee(r.id);if(t!==void 0)return t;if(r.id.startsWith(h)){r.logger.error(`"${r.id}" names no table — use ${h}<table>`);return}let o;try{o=pe(e,r.id)}catch(n){r.logger.error(n instanceof Error?n.message:String(n));return}if(o===void 0){r.logger.error(`migration "${r.id}" not found under lunora/ — declare it with defineMigration({ id: "${r.id}", ... })`);return}if(o===""){r.logger.error(`migration "${r.id}" must declare \`table\` as a static string literal`);return}return o},we=e=>{const r=e.cwd??process.cwd();if(e.prod&&e.url===void 0)return e.logger.error("--prod requires an explicit --url (refusing to migrate the implicit localhost worker)"),{refused:d.USAGE};const t=be(r,e);if(t===void 0)return{refused:d.USAGE};const o=B(e.url,e.logger,e.cwd);if(o===void 0)return{refused:d.USAGE};const{token:n}=z({cwd:r,token:e.token,url:o});return n?(e.subcommand==="up"||e.subcommand==="down")&&L({prod:e.prod,url:o})&&!e.yes?(e.logger.error(`migrate ${e.subcommand} runs the migration against ${o}, which is not local. Re-run with --yes to confirm.`),{refused:d.USAGE}):{fetchImpl:e.fetchImpl??q,requestUrl:`${o}${le}`,table:t,token:n}:(e.logger.error("admin token required — pass --token, set LUNORA_ADMIN_TOKEN, or add it to .dev.vars (local targets only)"),{refused:d.AUTH})},ve=e=>{if(e===null||typeof e!="object")return;const r=e,t=typeof r.failed=="number"&&r.failed>0?r.failed:0;if(t>0)return`${String(t)} shard(s) could not be reached — their rows were NOT migrated (see the per-shard errors above)`;if(r.status==="failed")return"the migration reported `failed` on at least one shard — see the per-shard results above"},$e=e=>{const r={id:e.id};return e.subcommand==="status"||(r.direction=e.subcommand,e.dryRun&&(r.dryRun=!0),e.batchSize!==void 0&&(r.batchSize=e.batchSize),e.maxBatches!==void 0&&(r.maxBatches=e.maxBatches)),r},ze=async e=>{const r=we(e);if(W(r))return{body:void 0,code:r.refused,requestUrl:""};const{fetchImpl:t,requestUrl:o,table:n,token:l}=r,c=e.subcommand==="status"?de:ce,s=$e(e);e.logger.info(`POST ${o} -> ${e.subcommand} ${e.id} (table "${n}")`);const a=await t(o,{body:JSON.stringify({args:s,functionPath:c,table:n}),headers:{authorization:`Bearer ${l}`,"content-type":"application/json"},method:"POST"}),i=await Y(a,e.logger),u=ve(i);u!==void 0&&e.logger.error(`migrate ${e.subcommand} "${e.id}": ${u}`);const g=u===void 0?0:1;return{body:i,code:a.ok?g:J(a.status),requestUrl:o}},I=e=>e===void 0?void 0:H(e),ye={"d1-to-hyperdrive":{source:"D1",target:"Hyperdrive"},"hyperdrive-to-d1":{source:"Hyperdrive",target:"D1"}},Se=(e,r,t,o)=>e?`Imported ${String(r.inserted)} and replaced ${String(r.replaced)} of ${String(r.exported)} exported row(s). The rest were not written; see the errors reported above, then fix them and re-run.`:`Imported ${String(r.inserted)} of ${String(r.exported)} exported row(s). The rest already exist in the ${o} target and were left unchanged (reported as conflicts above), so this copied rows missing from ${o}, not later edits to rows it already holds. Re-run with --replace to overwrite them with the ${t} values, or keep the dump with --out and reconcile by hand.`,Le=async e=>{const{logger:r}=e,{source:t,target:o}=ye[e.direction],n=I(e.fromUrl??e.toUrl),l=I(e.toUrl??e.fromUrl);if(n===l){const a=`source and target are the same deployment — pass distinct --from-url and --to-url so the ${t} export and ${o} import don't run against one database`;return r.error(a),{bytes:0,code:d.USAGE,error:a,exported:0,imported:0,replaced:0}}if(e.replace===!0&&(e.tables===void 0||e.tables.trim()==="")){const a="--replace needs --tables naming the .global() tables to move: an export without a table list also carries shard-local rows, and --replace would overwrite them in the target";return r.error(a),{bytes:0,code:d.USAGE,error:a,exported:0,imported:0,replaced:0}}const c=e.out===void 0?C(m(O(),"lunora-d1ps-")):void 0,s=e.out??m(c,"dump.ndjson");try{r.info(`Exporting .global() data from the ${t} source (${n??"http://localhost:8787"}) …`);const a=await Z({fetchImpl:e.fetchImpl,logger:r,out:s,prod:e.prod,tables:e.tables,token:e.fromToken,url:n});if(a.code!==0)return{bytes:a.bytes,code:a.code,exported:a.rows,imported:0,replaced:0,...a.error===void 0?{}:{error:a.error}};r.info(`Exported ${String(a.rows)} row(s) (${String(a.bytes)} bytes).`),r.info(`Importing into the ${o} target (${l??"http://localhost:8787"}) …`);const i=await Q({batchSize:e.batchSize,fetchImpl:e.fetchImpl,file:s,logger:r,prod:e.prod,replace:e.replace,token:e.toToken,url:l,yes:e.yes}),u=i.replaced??0;if(i.code!==0)return{bytes:a.bytes,code:i.code,exported:a.rows,imported:i.inserted,replaced:u,...i.error===void 0?{}:{error:i.error}};if(i.inserted+u===a.rows){const g=u>0?` (${String(u)} existing row(s) overwritten; rows deleted on ${t} since cutover are not removed)`:"";r.info(`✓ Migrated ${String(a.rows)} row(s)${g} — counts match. Verify your app reads from ${o}, then decommission the ${t} binding.`)}else r.warn(Se(e.replace===!0,{exported:a.rows,inserted:i.inserted,replaced:u},t,o));return{bytes:a.bytes,code:0,exported:a.rows,imported:i.inserted,replaced:u}}finally{c!==void 0&&F(c,{force:!0,recursive:!0})}};export{Le as runMigrateBackendCommand,je as runMigrateCreateCommand,ze as runMigrateDataCommand,Pe as runMigrateGenerateCommand};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/cli",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.444",
|
|
4
4
|
"description": "The Lunora CLI: init, dev, deploy, codegen, migrate, seed, doctor, insights, logs, registry, and the rest of the project commands",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skills",
|
|
@@ -56,16 +56,16 @@
|
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@bomb.sh/tab": "0.0.22",
|
|
59
|
-
"@lunora/advisor": "1.0.0-alpha.
|
|
60
|
-
"@lunora/bindings": "1.0.0-alpha.
|
|
61
|
-
"@lunora/codegen": "1.0.0-alpha.
|
|
62
|
-
"@lunora/config": "1.0.0-alpha.
|
|
63
|
-
"@lunora/container": "1.0.0-alpha.
|
|
64
|
-
"@lunora/d1": "1.0.0-alpha.
|
|
59
|
+
"@lunora/advisor": "1.0.0-alpha.265",
|
|
60
|
+
"@lunora/bindings": "1.0.0-alpha.139",
|
|
61
|
+
"@lunora/codegen": "1.0.0-alpha.365",
|
|
62
|
+
"@lunora/config": "1.0.0-alpha.420",
|
|
63
|
+
"@lunora/container": "1.0.0-alpha.96",
|
|
64
|
+
"@lunora/d1": "1.0.0-alpha.214",
|
|
65
65
|
"@lunora/errors": ">=1.0.0-alpha.51 <2.0.0-0",
|
|
66
|
-
"@lunora/mcp": "1.0.0-alpha.
|
|
67
|
-
"@lunora/runtime": "1.0.0-alpha.
|
|
68
|
-
"@lunora/seed": "1.0.0-alpha.
|
|
66
|
+
"@lunora/mcp": "1.0.0-alpha.268",
|
|
67
|
+
"@lunora/runtime": "1.0.0-alpha.248",
|
|
68
|
+
"@lunora/seed": "1.0.0-alpha.262",
|
|
69
69
|
"@visulima/cerebro": "4.0.1",
|
|
70
70
|
"@visulima/error": "6.1.0",
|
|
71
71
|
"@visulima/fs": "7.0.0",
|