generatesaas 3.6.0 → 3.7.0

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/index.js CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import{Command as Ml}from"commander";import*as hs from"@clack/prompts";import{existsSync as Xc,readdirSync as Zc,rmSync as Qc}from"fs";import{join as el,resolve as tl}from"path";import{Option as re}from"commander";import*as k from"@clack/prompts";import*as Pt from"@clack/prompts";import tr from"picocolors";function Kr(e){let t=e?` GenerateSaaS v${e} `:" GenerateSaaS ";Pt.intro(tr.bgYellow(tr.black(t)))}function Gr(){Pt.outro(tr.yellow("Happy building!"))}import*as m from"@clack/prompts";import h from"picocolors";var pt={fullstack:{label:"Fullstack",hint:"Frontend hosts the API - works on serverless or long-running"},separate:{label:"Separate",hint:"Standalone Hono backend - long-running runtimes only (Docker, Render, Fly.io, Railway, Coolify, Dokploy)"}},Rt={stripe:{label:"Stripe"},polar:{label:"Polar"},none:{label:"None",hint:"disable payments"}},It={smtp:{label:"SMTP",hint:"Mailpit for local dev"},ses:{label:"Amazon SES"},resend:{label:"Resend"}},Tt={sentry:{label:"Sentry",hint:"web, server and desktop - release health, tracing, alerting"},posthog:{label:"PostHog",hint:"web and server, plus LLM analytics - no Electron SDK, so a desktop build reports nothing"}},Hr={postgres:{label:"PostgreSQL",hint:"port 5432",port:5432},redis:{label:"Redis",hint:"port 6379",port:6379},inngest:{label:"Inngest",hint:"port 8288",port:8288},mailpit:{label:"Mailpit",hint:"port 1025",port:1025}};var Te={USD:{symbol:"$",name:"US Dollar",place:"left",space:!1},EUR:{symbol:"\u20AC",name:"Euro",place:"right",space:!1},GBP:{symbol:"\xA3",name:"British Pound",place:"left",space:!1},CAD:{symbol:"CA$",name:"Canadian Dollar",place:"left",space:!1},AUD:{symbol:"A$",name:"Australian Dollar",place:"left",space:!1},BRL:{symbol:"R$",name:"Brazilian Real",place:"left",space:!1},JPY:{symbol:"\xA5",name:"Japanese Yen",place:"left",space:!1}};var W={node:{label:"Node.js / Docker",hint:"long-running runtime - Render, Fly.io, Railway, Coolify, Dokploy, VPS",edgeRuntime:!1},vercel:{label:"Vercel",hint:"serverless functions",edgeRuntime:!0}},_e={postgres:{label:"PostgreSQL (self-hosted)",hint:"local Docker, drizzle-orm/node-postgres",managed:!1,envVars:[{key:"DATABASE_URL",defaultValue:"postgres://postgres:postgres@localhost:5432/saas"}]},neon:{label:"Neon",hint:"serverless Postgres",managed:!0,envVars:[{key:"DATABASE_URL",comment:"# TODO: Add your Neon connection string"}]},supabase:{label:"Supabase",hint:"managed Postgres",managed:!0,envVars:[{key:"DATABASE_URL",comment:"# TODO: Add your Supabase connection string"}]}},Oe={redis:{label:"Redis (self-hosted)",hint:"local Docker, ioredis",managed:!1,envVars:[{key:"REDIS_URL",defaultValue:"redis://localhost:6379"}]},upstash:{label:"Upstash",hint:"serverless Redis",managed:!0,envVars:[{key:"UPSTASH_REDIS_REST_URL",comment:"# TODO: Add your Upstash REST URL"},{key:"UPSTASH_REDIS_REST_TOKEN",comment:"# TODO: Add your Upstash REST token"}]}},We=[{target:"vercel",provider:"redis",reason:"Vercel serverless cannot maintain persistent Redis connections. Consider Upstash."},{target:"vercel",provider:"postgres",reason:"Vercel serverless cannot pool connections to a self-hosted Postgres. Use Neon or Supabase - both are managed, connection-pooled Postgres."}],qe=[{architecture:"separate",target:"vercel",reason:"Standalone backends need a long-running runtime. Vercel's per-function TypeScript check conflicts with pnpm's isolated install. Use --architecture fullstack for serverless, or deploy the standalone backend to a long-running runtime (Render, Fly.io, Railway, Coolify, Dokploy, or your own VPS via Docker)."}],ys=[{feature:"runner",target:"vercel",reason:"Serverless (Vercel) cannot hold a connection open, and the runner holds one open per paired device. Deploy to a long-running runtime instead (Node.js / Docker on Render, Fly.io, Railway, Coolify, Dokploy, or your own VPS)."}];function ne(e){return ys.find(t=>e[t.feature]===!0&&e.deploymentTarget===t.target)}var ie={serverless:{label:"Serverless",hint:"Vercel + Neon + Upstash",values:{deploymentTarget:"vercel",databaseProvider:"neon",cacheProvider:"upstash"}},selfhost:{label:"Self-host",hint:"Docker or Node + Postgres + Redis",values:{deploymentTarget:"node",databaseProvider:"postgres",cacheProvider:"redis"}}},xe=Object.keys(ie);function rr(e){let t=[];for(let r of We)if(e.deploymentTarget===r.target&&(e.databaseProvider===r.provider||e.cacheProvider===r.provider)){let n=e.databaseProvider===r.provider?"databaseProvider":"cacheProvider";t.push(`\`${n}: "${r.provider}"\` with \`deploymentTarget: "${r.target}"\` - ${r.reason}`)}for(let r of qe)e.deploymentTarget===r.target&&e.architecture===r.architecture&&t.push(`\`architecture: "${r.architecture}"\` with \`deploymentTarget: "${r.target}"\` - ${r.reason}`);return t}var Yr=["Local file storage (sharp, geoip-lite)","SMTP email (use Resend or SES instead)","Content API git integration"];function ze(e){let t=[];return e.databaseProvider==="postgres"&&t.push("postgres"),e.cacheProvider==="redis"&&t.push("redis"),e.emailProvider==="smtp"&&t.push("mailpit"),t.push("inngest"),t}var Je=["fullstack","separate"],Xe=["stripe","polar","none"],Ze=["smtp","ses","resend"];var Wr=["claude-code","cursor","codex","gemini-cli","windsurf"],se=["USD","EUR","GBP","CAD","AUD","BRL","JPY"],oe=["node","vercel"],ae=["postgres","neon","supabase"],ce=["redis","upstash"],K=["sentry","posthog"];function qr(e,t){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||t}function _t(e){return e.split("-").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function Ot(e){return/^[a-z][a-z0-9-]*$/.test(e)}function N(e){return e instanceof Error?e.message:String(e)}var zr=["runner"];function nr(e){if(!zr.some(r=>e[r]===!0))return e;if(e.ai===!1)throw new Error("--runner requires AI features; remove --no-ai.");return e.ai===void 0&&(e.ai=!0),e}function dt(e){if(e.webAi!==!1)return e;if(e.ai===!1)throw new Error("--no-web-ai requires --ai (the desktop app is where AI lives when the web surface is off).");if(e.desktop===!1)throw new Error("--no-web-ai requires --desktop - with the web AI surface off, the desktop app is the only place your AI features exist.");if(e.runner===!0)throw new Error("--runner needs the web AI surface; drop --runner or keep web AI.");return e}function Jr(e){return zr.some(r=>e[r]===!0)&&(e.ai=!0),e}function Xr(e){if(e.webAi!==!1)return[];let t=[];return e.desktop!==!0&&t.push("`desktop` is not true, so this project has no desktop app - and with the web AI surface off, that app is the only place its AI features would exist."),e.runner===!0&&t.push("`runner` is true, and the runner's pairing, install and Runners screens are part of the web AI surface."),t}function Zr(e){let t=vs(e,e.desktopAi);return delete e.desktopAi,t}function vs(e,t){return e.desktop!==!0?{kind:"none"}:t===!0&&e.ai!==!0?{kind:"legacy-desktop-ai"}:t===!1&&e.ai===!0?{kind:"desktop-gains-ai"}:{kind:"none"}}function Qr(e){let t={};if(e.name!==void 0){if(!Ot(e.name))throw new Error(`Invalid project name "${e.name}". Use lowercase letters, numbers, and hyphens only. Must start with a letter.`);t.projectName=e.name}if(e.appName!==void 0){if(!e.appName.trim())throw new Error("App name cannot be empty.");t.appName=e.appName}if(e.location!==void 0?t.projectDir=e.location==="."?process.cwd():e.location:t.projectName!==void 0&&(t.projectDir=`./${t.projectName}`),e.architecture!==void 0&&(t.architecture=e.architecture),e.payment!==void 0&&(t.paymentProvider=e.payment),e.email!==void 0&&(t.emailProvider=e.email),e.org!==void 0&&(t.multiTenancy=e.org),e.projects!==void 0&&(t.projects=e.projects),e.docs!==void 0&&(t.docs=e.docs),e.desktop!==void 0&&(t.desktop=e.desktop),e.ai!==void 0&&(t.ai=e.ai),e.webAi!==void 0&&(t.webAi=e.webAi),e.runner!==void 0&&(t.runner=e.runner),nr(t),dt(t),e.revenueSharing!==void 0&&(t.revenueSharing=e.revenueSharing),e.credits!==void 0){if(e.credits===!0&&e.payment==="none")throw new Error("--credits requires a payment provider (got --payment none).");t.credits=e.credits}if(e.currency!==void 0){if(!se.includes(e.currency))throw new Error(`Invalid currency "${e.currency}". Valid values: ${se.join(", ")}`);t.defaultCurrency=e.currency}if(e.deploy!==void 0){if(!oe.includes(e.deploy))throw new Error(`Invalid deployment target "${e.deploy}". Valid values: ${oe.join(", ")}`);t.deploymentTarget=e.deploy}if(e.database!==void 0){if(!ae.includes(e.database))throw new Error(`Invalid database provider "${e.database}". Valid values: ${ae.join(", ")}`);t.databaseProvider=e.database}if(e.cache!==void 0){if(!ce.includes(e.cache))throw new Error(`Invalid cache provider "${e.cache}". Valid values: ${ce.join(", ")}`);t.cacheProvider=e.cache}if(e.observability!==void 0){let r=e.observability.split(",").map(i=>i.trim()).filter(Boolean),n=r.filter(i=>!K.includes(i));if(n.length>0)throw new Error(`Invalid observability provider ${JSON.stringify(n.join(", "))}. Valid values: ${K.join(", ")}`);t.observability=r}if(e.preset!==void 0){if(!xe.includes(e.preset))throw new Error(`Invalid preset "${e.preset}". Valid values: ${xe.join(", ")}`);let r=ie[e.preset].values;t.deploymentTarget??=r.deploymentTarget,t.databaseProvider??=r.databaseProvider,t.cacheProvider??=r.cacheProvider}if(e.demo===!0&&(t.demo=!0),e.baseUrl!==void 0){let r=e.baseUrl.trim();if(r==="")throw new Error("--base-url cannot be empty. Provide an absolute URL like https://example.com.");let n;try{n=new URL(r)}catch{throw new Error(`Invalid --base-url "${e.baseUrl}". Must be an absolute URL like https://example.com.`)}if(n.protocol!=="http:"&&n.protocol!=="https:")throw new Error(`Invalid --base-url "${e.baseUrl}". Must use http or https.`);t.baseUrl=`${n.protocol}//${n.host}`}return t}var Qe={projectName:"my-saas",architecture:"fullstack",paymentProvider:"stripe",emailProvider:"smtp",multiTenancy:!1,projects:!1,docs:!1,desktop:!1,ai:!1,webAi:!0,runner:!1,revenueSharing:!1,credits:!0,defaultCurrency:"USD",deploymentTarget:"node",databaseProvider:"postgres",cacheProvider:"redis",observability:[]};function en(e){let t=e.projectName??Qe.projectName,r=e.projectDir??`./${t}`,n=e.appName??_t(t),i=e.deploymentTarget??Qe.deploymentTarget,o=W[i]?.edgeRuntime??!1,s=e.databaseProvider??(o?"neon":Qe.databaseProvider),a=e.cacheProvider??(o?"upstash":Qe.cacheProvider),p=e.emailProvider??(o?"resend":Qe.emailProvider),u={...Qe,...e,projectName:t,appName:n,projectDir:r,deploymentTarget:i,databaseProvider:s,cacheProvider:a,emailProvider:p};u.paymentProvider==="none"&&(u.credits=!1);for(let g of We){if(u.deploymentTarget!==g.target)continue;let w=u.databaseProvider===g.provider?"database":"cache";if(u.databaseProvider===g.provider||u.cacheProvider===g.provider)throw new Error(`Incompatible: --deploy ${g.target} + --${w} ${g.provider}. ${g.reason}`)}for(let g of qe)if(u.architecture===g.architecture&&u.deploymentTarget===g.target)throw new Error(`Incompatible: --architecture ${g.architecture} + --deploy ${g.target}. ${g.reason}`);let v=ne(u);if(v)throw new Error(`Incompatible: --${v.feature} + --deploy ${v.target}. ${v.reason}`);return dt(u),u}function R(e){m.isCancel(e)&&(m.cancel("Setup cancelled."),process.exit(0))}function ir(e){let t=[];return e.databaseProvider==="neon"&&t.push({key:"DATABASE_URL",message:"Neon connection string (optional):",placeholder:"postgres://...",secret:!0}),e.databaseProvider==="supabase"&&t.push({key:"DATABASE_URL",message:"Supabase connection string (optional):",placeholder:"postgres://...",secret:!0}),e.cacheProvider==="upstash"&&(t.push({key:"UPSTASH_REDIS_REST_URL",message:"Upstash REST URL (optional):",placeholder:"https://...",secret:!1}),t.push({key:"UPSTASH_REDIS_REST_TOKEN",message:"Upstash REST token (optional):",secret:!0})),e.paymentProvider==="stripe"&&(t.push({key:"STRIPE_SECRET_KEY",message:"Stripe secret key (optional):",placeholder:"sk_test_...",secret:!0}),t.push({key:"STRIPE_WEBHOOK_SECRET",message:"Stripe webhook secret (optional):",placeholder:"whsec_...",secret:!0})),e.paymentProvider==="polar"&&(t.push({key:"POLAR_ACCESS_TOKEN",message:"Polar access token (optional):",secret:!0}),t.push({key:"POLAR_WEBHOOK_SECRET",message:"Polar webhook secret (optional):",secret:!0})),e.emailProvider==="resend"&&t.push({key:"RESEND_API_KEY",message:"Resend API key (optional):",placeholder:"re_...",secret:!0}),e.emailProvider==="ses"&&(t.push({key:"AMAZON_SES_REGION",message:"Amazon SES region (optional):",placeholder:"us-east-1",secret:!1}),t.push({key:"AMAZON_SES_KEY",message:"Amazon SES access key (optional):",secret:!0}),t.push({key:"AMAZON_SES_SECRET",message:"Amazon SES secret (optional):",secret:!0})),e.demo&&t.push({key:"TURNSTILE_SECRET_KEY",message:"Cloudflare Turnstile secret key (optional):",secret:!0}),t}async function tn(e,t){let r=!1;e&&nr(e),e&&dt(e),m.log.info(h.bold("Project"));let n=e?.projectName??await(async()=>{r=!0;let c=await m.text({message:"Project name:",placeholder:"my-saas",validate:d=>{if(!d?.trim())return"Project name is required.";if(!Ot(d))return"Use lowercase letters, numbers, and hyphens only. Must start with a letter."}});return R(c),c})(),i=e?.appName??await(async()=>{r=!0;let c=await m.text({message:"App name:",initialValue:_t(n),validate:d=>{if(!d?.trim())return"App name is required."}});return R(c),c})(),o=e?.projectDir??await(async()=>{r=!0;let c=await m.text({message:"Project location:",initialValue:`./${n}`});return R(c),c==="."?process.cwd():c})();m.log.info(h.bold("Infrastructure"));let s={};if(e?.deploymentTarget===void 0&&e?.databaseProvider===void 0&&e?.cacheProvider===void 0){r=!0;let c=await m.select({message:"Infrastructure:",options:[...xe.map(d=>({value:d,label:ie[d].label,hint:ie[d].hint})),{value:"custom",label:"Customize",hint:"choose each piece yourself"}]});if(R(c),c!=="custom"){let d=ie[c].values;s.deploymentTarget=d.deploymentTarget,s.databaseProvider=d.databaseProvider,s.cacheProvider=d.cacheProvider,m.log.info(`${ie[c].label}: ${ie[c].hint}.`)}}let a=e?.deploymentTarget??s.deploymentTarget??"node";if(e?.deploymentTarget===void 0&&s.deploymentTarget===void 0){r=!0;let c=await m.select({message:"Deployment target:",options:oe.map(d=>({value:d,label:W[d].label,hint:W[d].hint}))});R(c),a=c}let p=e?.architecture?qe.find(c=>c.architecture===e.architecture&&c.target===a):void 0;if(p)throw new Error(`Incompatible: --architecture ${p.architecture} + --deploy ${p.target}. ${p.reason}`);let u=ne({...e,deploymentTarget:a});if(u)throw new Error(`Incompatible: --${u.feature} + --deploy ${u.target}. ${u.reason}`);let v=ne({runner:!0,deploymentTarget:a})!==void 0,g=new Set(qe.filter(c=>c.target===a).map(c=>c.architecture)),w=Je.filter(c=>!g.has(c)),P=e?.architecture??await(async()=>{if(w.length===1){let d=w[0];return m.log.info(`Auto-selected ${pt[d].label} architecture (only compatible option for ${W[a].label}).`),d}r=!0;let c=await m.select({message:"Architecture:",options:w.map(d=>({value:d,label:pt[d].label,hint:pt[d].hint}))});return R(c),c})(),f=e?.databaseProvider??s.databaseProvider??await(async()=>{r=!0;let c=ae.filter(_=>!We.some(ye=>ye.target===a&&ye.provider===_));if(c.length===1){let _=c[0];return m.log.info(`Auto-selected ${_e[_].label} (only compatible option for ${W[a].label}).`),_}let d=await m.select({message:"Database provider:",options:c.map(_=>({value:_,label:_e[_].label,hint:_e[_].hint}))});return R(d),d})(),L=e?.cacheProvider??s.cacheProvider??await(async()=>{r=!0;let c=ce.filter(_=>!We.some(ye=>ye.target===a&&ye.provider===_));if(c.length===1){let _=c[0];return m.log.info(`Auto-selected ${Oe[_].label} (only compatible option for ${W[a].label}).`),_}let d=await m.select({message:"Cache provider:",options:c.map(_=>({value:_,label:Oe[_].label,hint:Oe[_].hint}))});return R(d),d})();if(W[a]?.edgeRuntime){let c=Yr.map(d=>` - ${d}`).join(`
3
- `);m.note(c,"Unavailable on edge runtime")}m.log.info(h.bold("Features"));let M=e?.paymentProvider??await(async()=>{r=!0;let c=await m.select({message:"Payment provider:",options:Xe.map(d=>({value:d,label:Rt[d].label,hint:Rt[d].hint}))});return R(c),c})(),fe=e?.defaultCurrency??await(async()=>{if(M==="none")return"USD";r=!0;let c=await m.select({message:"Default currency:",options:se.map(d=>({value:d,label:d,hint:Te[d].name}))});return R(c),c})(),Z=e?.emailProvider??await(async()=>{r=!0;let c=await m.select({message:"Email provider:",options:Ze.map(d=>({value:d,label:It[d].label,hint:It[d].hint}))});return R(c),c})(),he=e?.observability??await(async()=>{r=!0;let c=await m.multiselect({message:"Observability (space to select, enter to confirm):",required:!1,initialValues:[],options:K.map(d=>({value:d,label:Tt[d].label,hint:Tt[d].hint}))});return R(c),c})(),Se=e?.multiTenancy??await(async()=>{r=!0;let c=await m.confirm({message:"Enable multi-tenancy (organizations)?",initialValue:!1});return R(c),c})(),Ae=e?.projects??await(async()=>{r=!0;let c=await m.confirm({message:"Enable projects (a switcher that scopes a user's work)?",initialValue:!1});return R(c),c})(),Pe=e?.docs??await(async()=>{r=!0;let c=await m.confirm({message:"Include docs app? (self-hosted Fumadocs documentation site)",initialValue:!1});return R(c),c})(),b=t?.desktopAllowed!==!1,D=e?.desktop??(b?await(async()=>{r=!0;let c=await m.confirm({message:"Include the desktop app? (apps/desktop - cross-platform, built with Electron, device-auth). With AI on, it also runs a local AI stack.",initialValue:!1});return R(c),c})():(m.log.info(h.dim("Desktop app: available on the Pro plan and up - skipped.")),!1)),$=e?.ai??await(async()=>{r=!0;let c=await m.confirm({message:"Add AI features? (chat, models, automations, integrations - and unlocks the runner)",initialValue:!1});return R(c),c})(),ge=t?.runnerAllowed!==!1,F=e?.webAi===!1,V=!$||v||F?!1:e?.runner??(ge?await(async()=>{r=!0;let c=await m.confirm({message:"Add the runner daemon for your WEB users? A small program they install on their own machine or VPS to run their Claude Code / Codex subscription for this app 24/7 - the AI runs on their hardware, not yours. Off means no install UI and no pairing endpoint on your site; you can adopt it later.",initialValue:!1});return R(c),c})():(m.log.info(h.dim("Runner: available on the Pro plan and up - skipped.")),!1));$&&v?m.log.info(h.dim(`Runner: ${W[a].label} cannot hold a connection open - skipped.`)):$&&F&&m.log.info(h.dim("Runner: its pairing and device screens live in the web AI surface, which is off - skipped."));let B=e?.webAi??(D&&$&&!V?await(async()=>{r=!0;let c=await m.confirm({message:"Show the AI surfaces in the web app? (No: desktop-first - the web keeps account, billing, and licensing only)",initialValue:!0});return R(c),c})():!0);dt({webAi:B,ai:$,desktop:D,runner:V});let Re=e?.revenueSharing??await(async()=>{r=!0;let c=await m.confirm({message:"Enable revenue sharing? (opt-in MRR leaderboard with dofollow backlinks)",initialValue:!1});return R(c),c})(),Ye=M==="none"?!1:e?.credits??await(async()=>{r=!0;let c=await m.confirm({message:"Enable credits? (metered usage on top of subscription plans)",initialValue:!0});return R(c),c})(),I=e?.demo,Ie=ir({databaseProvider:f,cacheProvider:L,paymentProvider:M,emailProvider:Z,demo:I}),lt={};if(Ie.length>0&&r){m.log.info(h.bold("Credentials")+h.dim(" all optional - press Enter to skip, fill in .env later"));for(let c of Ie)if(r=!0,c.secret){let d=await m.password({message:c.message,mask:"*"});R(d),typeof d=="string"&&d.trim()&&(lt[c.key]=d.trim())}else{let d=await m.text({message:c.message,placeholder:c.placeholder});R(d),typeof d=="string"&&d.trim()&&(lt[c.key]=d.trim())}}if(r){let c=[` Name: ${h.cyan(n)}`,` App name: ${h.cyan(i)}`,` Location: ${h.cyan(o)}`,` Architecture: ${h.cyan(pt[P].label)}`].join(`
4
- `),d=[` Deploy target: ${h.cyan(W[a]?.label??"Node.js / Docker")}`,` Database: ${h.cyan(_e[f].label)}`,` Cache: ${h.cyan(Oe[L].label)}`].filter(Boolean).join(`
5
- `),_=[M!=="none"?` Payment: ${h.cyan(Rt[M].label)} (${fe})`:` Payment: ${h.dim("none")}`,` Credits: ${Ye?h.cyan("Yes"):h.dim("No")}`,` AI features: ${$?h.cyan("Yes"):h.dim("No")}`,...$?[` Runner: ${V?h.cyan("Yes"):h.dim("No")}`]:[],...B?[]:[` Web AI: ${h.dim("No")} (desktop-first)`],` Email: ${h.cyan(It[Z].label)}`,` Observability: ${he.length===0?h.dim("none"):h.cyan(he.map(er=>Tt[er].label).join(", "))}`,` Multi-tenancy: ${Se?h.cyan("Yes"):h.dim("No")}`,` Projects: ${Ae?h.cyan("Yes"):h.dim("No")}`,` Docs app: ${Pe?h.cyan("Yes"):h.dim("No")}`,` Desktop app: ${D?h.cyan("Yes"):h.dim("No")}`,` Rev. sharing: ${Re?h.cyan("Yes"):h.dim("No")}`].join(`
6
- `),ye=[h.bold("Project"),c,"",h.bold("Infrastructure"),d,"",h.bold("Features"),_];if(Ie.length>0){let er=Ie.map(Br=>{let gs=lt[Br.key]?h.green("provided"):h.dim("skipped");return` ${Br.key}: ${gs}`}).join(`
7
- `);ye.push("",h.bold("Credentials"),er)}m.note(ye.join(`
8
- `),"Summary");let Vr=await m.confirm({message:"Proceed with these settings?"});(m.isCancel(Vr)||!Vr)&&(m.cancel("Setup cancelled."),process.exit(0))}return{projectName:n,appName:i,projectDir:o,architecture:P,deploymentTarget:a,databaseProvider:f,cacheProvider:L,paymentProvider:M,emailProvider:Z,observability:he,multiTenancy:Se,projects:Ae,docs:Pe,desktop:D,ai:$,webAi:B,runner:V,revenueSharing:Re,credits:Ye,defaultCurrency:fe,...Object.keys(lt).length>0?{credentials:lt}:{},...e?.baseUrl!==void 0?{baseUrl:e.baseUrl}:{},...I!==void 0?{demo:I}:{}}}import{createReadStream as mn,existsSync as Ts}from"fs";import{mkdir as _s}from"fs/promises";import{join as Os}from"path";import{Readable as xs}from"stream";import{pipeline as mr}from"stream/promises";import{extract as Ds}from"tar";import{join as De}from"path";import{homedir as ws}from"os";var ut=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",q=".generatesaas",ve=De(q,"manifest.json"),rn="nextjs",sr="nuxt",or="2.4.1",nn=De(q,"hashes.json"),xt=De(q,"template-hashes.json"),Dt=De(q,"template"),sn=De(q,"staging"),on=De(q,"staging.json"),le=De(ws(),".generatesaas");var j=class extends Error{constructor(r,n,i){super(n);this.status=r;this.body=i}status;body;name="ApiError"};function pe(e){return{apiKey:e,baseUrl:ut}}async function we(e,t,r){let n=`${e.baseUrl}${t}`,i=await fetch(n,{...r,headers:{...r?.headers,Authorization:`Bearer ${e.apiKey}`,"User-Agent":"generatesaas-cli/3.6.0"}});if(!i.ok){let o,s;try{s=await i.json(),o=s.error??`API ${i.status}: ${t}`}catch{o=`API ${i.status}: ${t}`}throw new j(i.status,o,s)}return i}import{existsSync as bs,readFileSync as ks,writeFileSync as Es,mkdirSync as Ss}from"fs";import{dirname as As}from"path";import*as be from"@clack/prompts";function mt(){if(!bs(le))return null;try{let e=JSON.parse(ks(le,"utf-8"));return e.apiKey?e.apiKey:(e.token&&!e.apiKey&&be.log.warning(`Found old GitHub token in ${le}. Run 'generatesaas init' to set up your API key.`),null)}catch{return null}}function Ne(e){Ss(As(le),{recursive:!0}),Es(le,JSON.stringify({apiKey:e},null," ")+`
9
- `,{mode:384})}async function et(e){if(e?.apiKey)return e.apiKey;let t=process.env.GENERATESAAS_API_KEY;if(t)return t;let r=mt();if(r)return r;if(!e?.prompt)throw new Error("API key not found. Set GENERATESAAS_API_KEY or run 'generatesaas init' to configure.");return ft()}async function ft(){let e=await be.text({message:"Enter your GenerateSaaS API key:",placeholder:"gs_live_...",validate:t=>{if(!t?.trim())return"API key is required."}});return be.isCancel(e)&&(be.cancel("Setup cancelled."),process.exit(0)),e.trim()}import{readFile as Ps}from"fs/promises";async function ke(e){let t=process.env.GENERATESAAS_VERSION_INDEX;return t?JSON.parse(await Ps(t,"utf-8")):process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{latest:"0.0.0-ci",versions:[{version:"0.0.0-ci",date:new Date().toISOString(),breaking:!1}],entitlements:null}:await(await we(e,"/versions")).json()}async function an(e,t){if(process.env.GENERATESAAS_OFFLINE_LICENSE==="1")return null;try{return await(await we(e,`/changelog/${encodeURIComponent(t)}`)).text()}catch(r){if(r instanceof j&&r.status===404)return null;throw r}}async function cn(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{skills:{"generatesaas-update":{skillMd:`# Offline skill stub
10
- `,scripts:{},references:{}}}}:await(await we(e,`/skills/${encodeURIComponent(t)}`)).json()}function Nt(e){if(!(e instanceof j)||e.status!==403)return null;let t=e.body;return!t||t.code!=="update_window_expired"?null:{message:e.message,lastAllowedVersion:t.lastAllowedVersion??null}}var ar="nextjs";function ln(e){return{frontend:ar,architecture:e.architecture,deployTarget:e.deploymentTarget,database:e.databaseProvider,cache:e.cacheProvider,payment:e.paymentProvider,email:e.emailProvider,multiTenancy:e.multiTenancy,blog:!0,docs:e.docs,desktop:e.desktop,runner:e.runner,credits:e.credits,revenueSharing:e.revenueSharing,currency:e.defaultCurrency}}async function cr(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:"offline-test-token",licenseId:"offline-test-license-id",installId:t.installId}:await(await we(e,"/license/sign",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function pn(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:t.currentToken}:await(await we(e,"/license/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function lr(e,t){let r=await fetch(`${e}/license/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok)throw new Error(`Verification service returned ${r.status}`);return await r.json()}async function dn(e,t,r){let n=await fetch(`${e}/license/inspect`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify(r)});if(!n.ok){let i=await n.json().catch(()=>null);throw new Error(i?.error??`Inspect endpoint returned ${n.status}`)}return await n.json()}var pr=new Set([".git","node_modules",".pnpm-store",".env",".env.test",".turbo",".data","dist",".next",".next-e2e-web",".next-e2e-desktop",".svelte-kit",".wrangler",".devcontainer","playwright-report","test-results",".drive-out",".mastra"]),dr=new Set(["pnpm-lock.yaml"]);function tt(e){if(ur(e))return!0;for(let t of e.split("/"))if(dr.has(t))return!0;return!1}var Rs=new Set(["data","mksaas","references","scripts",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","vitest.config.ts","README.md","TODO.md","OVERVIEW.md","HANDOFF.md"]),un=["scripts/docs-lint.ts"],Is=["docs/superpowers","packages/cli","packages/cli-api","packages/config/tests/runner-brand-lockstep.test.ts","packages/database/tests/no-migration-history.test.ts","apps/web/tests/content/competitor-claims.test.ts","apps/runner","docs/agentrunner","infra/docker-compose.yml",".claude/commands",".claude/skills/web-port",".claude/skills/web-port-workspace",".claude/settings.local.json",".claude/worktrees"];function ur(e){let t=e.split("/");for(let r of t)if(pr.has(r))return!0;if(un.includes(e)||un.some(r=>r.startsWith(e+"/")))return!1;if(Rs.has(t[0]))return!0;for(let r of Is)if(e===r||e.startsWith(r+"/"))return!0;return!1}async function Ct(e,t,r){await _s(r,{recursive:!0});let n=process.env.GENERATESAAS_TEMPLATE_TARBALL_DIR;if(n){let a=Os(n,`v${t}.tar.gz`);if(!Ts(a))throw new Error(`GENERATESAAS_TEMPLATE_TARBALL_DIR is set but ${a} does not exist.`);await mr(mn(a),fr(r));return}let i=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(i){await mr(mn(i),fr(r));return}let o=await we(e,`/template/${encodeURIComponent(t)}`);if(!o.body)throw new Error("Empty response body");let s=xs.fromWeb(o.body);await mr(s,fr(r))}function fr(e){return Ds({cwd:e,strip:1,filter:t=>{let r=t.replace(/^[^/]+\//,"");return r?!ur(r):!0},sync:!1})}import{readdir as Lt,readFile as rt,rm as hn,rmdir as Ns,writeFile as ht}from"fs/promises";import{join as G}from"path";async function gn(e){let t;try{t=await Lt(e,{withFileTypes:!0})}catch{return!1}let r=!1;for(let n of t){let i=G(e,n.name);if(n.isDirectory()){await gn(i)&&(r=!0);continue}if(n.name.startsWith("_")){r=!0;continue}await hn(i,{force:!0})}return r||await Ns(e).catch(()=>{}),r}async function yn(e){let t=G(e,"packages/content"),r=[];try{r=(await Lt(t,{withFileTypes:!0})).filter(s=>s.isDirectory()).map(s=>s.name)}catch{return}let n=new Set;for(let s of r){let a=G(t,s),p;try{p=(await Lt(a,{withFileTypes:!0})).filter(u=>u.isDirectory())}catch{continue}for(let u of p)n.add(u.name),await gn(G(a,u.name))}let i=await js(e),o=new Set([...n].filter(s=>i===void 0||i.has(s)));await Promise.all([...o].map(s=>hn(G(e,"apps/web/public/images",s),{recursive:!0,force:!0}))),await Ms(e,o)}var Cs="packages/config/src/index.ts",Ls=/\bcontent:\s*\{[\s\S]*?\bsections:\s*\[([\s\S]*?)\]/,$s=/\bslug:\s*"([^"]+)"/g;async function js(e){let t;try{t=await rt(G(e,Cs),"utf-8")}catch{return}let r=Ls.exec(t)?.[1];if(r===void 0)return;let n=[...r.matchAll($s)].map(i=>i[1]).filter(i=>i!==void 0);return n.length>0?new Set(n):void 0}var Us="packages/translate/.meta/mdx-translation-meta.json";async function Ms(e,t){if(t.size===0)return;let r=G(e,Us),n;try{n=JSON.parse(await rt(r,"utf-8"))}catch{return}if(typeof n!="object"||n===null)return;let i=n;if(typeof i.files!="object"||i.files===null)return;let o=i.files,s={};for(let[a,p]of Object.entries(o)){let u=a.split("/"),v=u.length>1?u[0]:void 0,g=u[u.length-1]?.startsWith("_")===!0;v!==void 0&&t.has(v)&&!g||(s[a]=p)}await ht(r,JSON.stringify({...i,files:s},null," ")+`
11
- `)}var Fs="packages/config/src/blog.ts";async function vn(e){let t=G(e,Fs),r=await rt(t,"utf-8"),n=fn(fn(r,"blogCategories",t),"blogAuthors",t);await ht(t,n)}function fn(e,t,r){let n=new RegExp(`(export const ${t}\\b[^=]*=\\s*)\\[[\\s\\S]*?\\];`);if(!n.test(e))throw new Error(`emptyBlogConfig: could not find the \`export const ${t} = [...]\` declaration in ${r}. The boilerplate blog config may have been renamed or restructured; update the CLI strip in cleanup.ts.`);return e.replace(n,"$1[];")}async function wn(e){let t=G(e,"packages/i18n/translations"),r;try{r=await Lt(t)}catch{return}for(let n of r){let i=G(t,n,"web.json"),o;try{o=await rt(i,"utf-8")}catch{continue}let s=JSON.parse(o);!s.content?.categories||s.content.categories.blog===void 0||(s.content.categories.blog={},await ht(i,JSON.stringify(s,null," ")+`
12
- `))}}var Vs="packages/config/src/ai/tasks.ts",Bs="The array carries a SINGLE INERT DEMO task: the CLI strips it for buyer builds (leaving an empty\n * `aiTasks`, which hides the Tasks tab) and keeps it for `--demo` builds.",Ks="The array starts EMPTY, which hides the Tasks tab until you declare your first task.";async function bn(e){let t=G(e,Vs),r=await rt(t,"utf-8"),n=r.replace(/\n?[\t ]*\/\/ gsaas:demo-ai-task-start[\s\S]*?\/\/ gsaas:demo-ai-task-end/,"");if(n===r)throw new Error(`removeDemoAiTasks: could not find the \`gsaas:demo-ai-task\` markers in ${t}. The boilerplate AI tasks config may have been renamed or restructured; update the CLI strip in cleanup.ts.`);let i=n.replace(Bs,Ks);if(i===n)throw new Error(`removeDemoAiTasks: the demo-task paragraph in ${t} did not match, so the shipped file would describe a demo task it no longer carries (boilerplate drift).`);await ht(t,i)}async function kn(e){let t=G(e,".claude","settings.json"),r;try{r=await rt(t,"utf8")}catch{return}let n=JSON.parse(r);delete n.alwaysThinkingEnabled,delete n.enableAllProjectMcpServers,n.env&&(delete n.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS,Object.keys(n.env).length===0&&delete n.env),n.permissions?.allow&&(n.permissions.allow=n.permissions.allow.filter(Gs)),await ht(t,JSON.stringify(n,null," ")+`
13
- `)}function Gs(e){return!(e.startsWith("mcp__")||e.includes("nuxt"))}import{readFileSync as eo}from"fs";import{dirname as to,join as gr}from"path";import{fileURLToPath as ro}from"url";import{existsSync as Hs}from"fs";import{join as En}from"path";function $t(e,t,r){let n=En(e,...t);return Hs(n)?n:En(e,r)}import{mkdir as Ys,readdir as Ws,rm as qs,rmdir as zs,writeFile as Js}from"fs/promises";import{dirname as jt,join as Xs,relative as Zs,sep as Qs}from"path";function Ce(e){return e.split(Qs).join("/")}async function Ut(e){await Ys(e,{recursive:!0})}async function l(e,t){await Ut(jt(e)),await Js(e,t,"utf-8")}async function Mt(e,t){await qs(e,{force:!0});let r=jt(e);for(;r!==t&&r!==jt(r);){try{await zs(r)}catch{return}r=jt(r)}}async function de(e,t,r,n){let i=[],o=await Ws(e,{withFileTypes:!0});for(let s of o){let a=Xs(e,s.name),p=Ce(Zs(t,a));r(p)||(s.isDirectory()?i.push(...await de(a,t,r,n)):s.isFile()&&(n===void 0||n(p))&&i.push(a))}return i}var no={postgres:"Postgres",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"},io={resend:"Resend",ses:"Amazon SES",smtp:"SMTP"},so={redis:"Redis",upstash:"Upstash Redis"},oo={node:"Node.js / Docker",vercel:"Vercel"},hr="apps/web",ao="Next.js 16",co="`app/` routes, `components/`, `lib/`",lo={stripe:"Stripe",polar:"Polar"};function po(e){let t=e.architecture==="fullstack",r=t?"(fullstack - Hono API mounted inside the app)":"(separate - standalone Hono backend)",n=t?`Mounted inside \`${hr}\`. A standalone \`apps/backend\` is also kept (inert) so you can switch to separate later.`:"Runs from `apps/backend`; the frontend reaches it over HTTP.",i=so[e.cacheProvider],o=oo[e.deploymentTarget],s=e.paymentProvider==="none"?"":`
14
- - **Payments:** ${lo[e.paymentProvider]}`,a=e.desktop?", and the desktop app (built with Electron) under `docs/desktop/`":"",p=e.runner?"\n- **Runner daemon** - your users install AgentRunner on a machine they control and pair it with their account. Everything you own lives in this repo: the `/runner/*` routes, the relay, the Runners page, and the capability layer runs get. The daemon itself is published separately and auto-updates - there is nothing to build or release here.":"",u=`\`useTranslations()\` from \`next-intl\` in components; messages are loaded in \`${hr}/i18n/request.ts\`.`;return mo({archSuffix:r,apiLocation:n,databaseName:no[e.databaseProvider],cacheName:i,paymentLine:s,emailName:io[e.emailProvider],deployName:o,runnerBullet:p,desktopDocsRef:a,i18nRule:u,webAppDir:hr,webAppName:ao,webAppLayout:co})}function uo(){return $t(to(ro(import.meta.url)),["generators","content"],"content")}function mo(e){let t=eo(gr(uo(),"AGENTS.template.md"),"utf-8"),r=new Map(Object.entries(e)),n=t.replace(/\{\{(\w+)\}\}/g,(o,s)=>{let a=r.get(s);if(a===void 0)throw new Error(`AGENTS.template.md uses unknown token {{${s}}}`);return a}),i=/\{\{[^}]*\}\}/.exec(n);if(i)throw new Error(`AGENTS.template.md carries an unrendered token: ${i[0]}`);return n}async function Sn(e){await l(gr(e.projectDir,"AGENTS.md"),po(e)),await l(gr(e.projectDir,"CLAUDE.md"),`@AGENTS.md
15
- `)}import{join as fo}from"path";var An="apps/web",yr="Next.js 16",ho={postgres:"Postgres (self-hosted)",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"};function go(e){let t=e.appName.trim()||e.projectName,r=ho[e.databaseProvider],n=e.architecture==="fullstack"?`${yr} app at \`${An}/\` with the Hono API mounted inside it. A standalone \`apps/backend/\` is also included (inert in this fullstack setup) so you can split the API into a separate service later without re-scaffolding.`:`${yr} app at \`${An}/\` and a separate Hono backend at \`apps/backend/\`.`,i=e.architecture==="fullstack"?`- App + API: http://localhost:3000
2
+ import{Command as Cp}from"commander";import*as Ns from"@clack/prompts";import{existsSync as ql,readdirSync as Jl,rmSync as Xl}from"fs";import{join as Zl,resolve as Ql}from"path";import{Option as ne}from"commander";import*as k from"@clack/prompts";import*as Nt from"@clack/prompts";import ur from"picocolors";function rn(e){let t=e?` GenerateSaaS v${e} `:" GenerateSaaS ";Nt.intro(ur.bgYellow(ur.black(t)))}function nn(){Nt.outro(ur.yellow("Happy building!"))}import*as h from"@clack/prompts";import g from"picocolors";var gt={fullstack:{label:"Fullstack",hint:"Frontend hosts the API - works on serverless or long-running"},separate:{label:"Separate",hint:"Standalone Hono backend - long-running runtimes only (Docker, Render, Fly.io, Railway, Coolify, Dokploy)"}},Ct={stripe:{label:"Stripe"},polar:{label:"Polar"},none:{label:"None",hint:"disable payments"}},Lt={smtp:{label:"SMTP",hint:"Mailpit for local dev"},ses:{label:"Amazon SES"},resend:{label:"Resend"}},$t={sentry:{label:"Sentry",hint:"web, server and desktop - release health, tracing, alerting"},posthog:{label:"PostHog",hint:"web and server, plus LLM analytics - no Electron SDK, so a desktop build reports nothing"}},sn={postgres:{label:"PostgreSQL",hint:"port 5432",port:5432},redis:{label:"Redis",hint:"port 6379",port:6379},inngest:{label:"Inngest",hint:"port 8288",port:8288},mailpit:{label:"Mailpit",hint:"port 1025",port:1025}};var Ce={USD:{symbol:"$",name:"US Dollar",place:"left",space:!1},EUR:{symbol:"\u20AC",name:"Euro",place:"right",space:!1},GBP:{symbol:"\xA3",name:"British Pound",place:"left",space:!1},CAD:{symbol:"CA$",name:"Canadian Dollar",place:"left",space:!1},AUD:{symbol:"A$",name:"Australian Dollar",place:"left",space:!1},BRL:{symbol:"R$",name:"Brazilian Real",place:"left",space:!1},JPY:{symbol:"\xA5",name:"Japanese Yen",place:"left",space:!1}};var Z={node:{label:"Node.js / Docker",hint:"long-running runtime - Render, Fly.io, Railway, Coolify, Dokploy, VPS",edgeRuntime:!1},vercel:{label:"Vercel",hint:"serverless functions",edgeRuntime:!0}},Le={postgres:{label:"PostgreSQL (self-hosted)",hint:"local Docker, drizzle-orm/node-postgres",managed:!1,envVars:[{key:"DATABASE_URL",defaultValue:"postgres://postgres:postgres@localhost:5432/saas"}]},neon:{label:"Neon",hint:"serverless Postgres",managed:!0,envVars:[{key:"DATABASE_URL",comment:"# TODO: Add your Neon connection string"}]},supabase:{label:"Supabase",hint:"managed Postgres",managed:!0,envVars:[{key:"DATABASE_URL",comment:"# TODO: Add your Supabase connection string"}]}},$e={redis:{label:"Redis (self-hosted)",hint:"local Docker, ioredis",managed:!1,envVars:[{key:"REDIS_URL",defaultValue:"redis://localhost:6379"}]},upstash:{label:"Upstash",hint:"serverless Redis",managed:!0,envVars:[{key:"UPSTASH_REDIS_REST_URL",comment:"# TODO: Add your Upstash REST URL"},{key:"UPSTASH_REDIS_REST_TOKEN",comment:"# TODO: Add your Upstash REST token"}]}},Xe=[{target:"vercel",provider:"redis",reason:"Vercel serverless cannot maintain persistent Redis connections. Consider Upstash."},{target:"vercel",provider:"postgres",reason:"Vercel serverless cannot pool connections to a self-hosted Postgres. Use Neon or Supabase - both are managed, connection-pooled Postgres."}],Ze=[{architecture:"separate",target:"vercel",reason:"Standalone backends need a long-running runtime. Vercel's per-function TypeScript check conflicts with pnpm's isolated install. Use --architecture fullstack for serverless, or deploy the standalone backend to a long-running runtime (Render, Fly.io, Railway, Coolify, Dokploy, or your own VPS via Docker)."}],Ls=[{feature:"runner",target:"vercel",reason:"Serverless (Vercel) cannot hold a connection open, and the runner holds one open per paired device. Deploy to a long-running runtime instead (Node.js / Docker on Render, Fly.io, Railway, Coolify, Dokploy, or your own VPS)."}];function me(e){return Ls.find(t=>e[t.feature]===!0&&e.deploymentTarget===t.target)}var he={serverless:{label:"Serverless",hint:"Vercel + Neon + Upstash",values:{deploymentTarget:"vercel",databaseProvider:"neon",cacheProvider:"upstash"}},selfhost:{label:"Self-host",hint:"Docker or Node + Postgres + Redis",values:{deploymentTarget:"node",databaseProvider:"postgres",cacheProvider:"redis"}}},je=Object.keys(he);function mr(e){let t=[];for(let r of Xe)if(e.deploymentTarget===r.target&&(e.databaseProvider===r.provider||e.cacheProvider===r.provider)){let n=e.databaseProvider===r.provider?"databaseProvider":"cacheProvider";t.push(`\`${n}: "${r.provider}"\` with \`deploymentTarget: "${r.target}"\` - ${r.reason}`)}for(let r of Ze)e.deploymentTarget===r.target&&e.architecture===r.architecture&&t.push(`\`architecture: "${r.architecture}"\` with \`deploymentTarget: "${r.target}"\` - ${r.reason}`);return t}var on=["Local file storage (sharp, geoip-lite)","SMTP email (use Resend or SES instead)","Content API git integration"];function Qe(e){let t=[];return e.databaseProvider==="postgres"&&t.push("postgres"),e.cacheProvider==="redis"&&t.push("redis"),e.emailProvider==="smtp"&&t.push("mailpit"),t.push("inngest"),t}var et=["fullstack","separate"],tt=["stripe","polar","none"],rt=["smtp","ses","resend"];var an=["claude-code","cursor","codex","gemini-cli","windsurf"],fe=["USD","EUR","GBP","CAD","AUD","BRL","JPY"],ge=["node","vercel"],ye=["postgres","neon","supabase"],be=["redis","upstash"],K=["sentry","posthog"],Me=["revenuecat","none"];function oe(e){return e.paymentProvider!==void 0&&e.paymentProvider!=="none"}var hr=[{key:"architecture",label:"Architecture",hint:"Fullstack (frontend hosts the API) or a standalone Hono backend.",category:"Infrastructure",kind:"enum",default:"fullstack",choices:et,adoptable:!1,impact:"structural",shapesTree:!0},{key:"deploymentTarget",label:"Deployment target",hint:"Node.js / Docker or Vercel serverless.",category:"Infrastructure",kind:"enum",default:"node",choices:ge,adoptable:!1,impact:"config",shapesTree:!0},{key:"databaseProvider",label:"Database provider",hint:"Self-hosted PostgreSQL, Neon, or Supabase.",category:"Infrastructure",kind:"enum",default:"postgres",choices:ye,adoptable:!1,impact:"config",shapesTree:!0},{key:"cacheProvider",label:"Cache provider",hint:"Self-hosted Redis or Upstash.",category:"Infrastructure",kind:"enum",default:"redis",choices:be,adoptable:!1,impact:"config",shapesTree:!0},{key:"paymentProvider",label:"Payment provider",hint:"Stripe, Polar, or none.",category:"Features",kind:"enum",default:"none",choices:tt,adoptable:!1,impact:"structural",shapesTree:!0},{key:"defaultCurrency",label:"Default currency",hint:"Billing and pricing currency.",category:"Features",kind:"enum",default:"USD",choices:fe,adoptable:!1,impact:"config",shapesTree:!1},{key:"emailProvider",label:"Email provider",hint:"SMTP, Amazon SES, or Resend.",category:"Features",kind:"enum",default:"smtp",choices:rt,adoptable:!1,impact:"config",shapesTree:!1},{key:"observability",label:"Observability",hint:"Which services receive your app's errors, and on PostHog its LLM analytics. Pick any combination - anything you do not pick costs nothing, since its SDK and its adapter are never generated.",category:"Features",kind:"multiselect",default:[],choices:K,adoptable:!0,impact:"config",shapesTree:!0},{key:"multiTenancy",label:"Multi-tenancy (organizations)",hint:"Adds organizations - teams, members, and shared resources.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural",shapesTree:!1},{key:"projects",label:"Projects",hint:"Adds projects - a switcher that scopes a user's work inside their account or organization.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"config",shapesTree:!1},{key:"docs",label:"Docs app",hint:"Adds the self-hosted Fumadocs documentation site (apps/docs).",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural",shapesTree:!0},{key:"desktop",label:"Desktop app",hint:"Adds the cross-platform desktop app (apps/desktop, built with Electron).",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural",shapesTree:!0},{key:"mobile",label:"Mobile app",hint:"Adds the cross-platform mobile app (apps/mobile, built with Expo).",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural",shapesTree:!0},{key:"mobilePurchases",label:"Mobile store purchases",hint:"In-app purchases in the mobile app, through RevenueCat. Store purchases sit ON TOP of your web payment provider - a subscriber who buys in the app is the same account, billed by Apple or Google instead of by you. Off keeps the app on web billing, where the plan card says the subscription is managed on your website.",category:"Features",kind:"enum",default:()=>"none",choices:Me,adoptable:!0,impact:"structural",shapesTree:!0,requires:e=>e.mobile===!0&&oe(e),requiresLabel:"requires the mobile app and a web payment provider - store purchases are additive to the web lane, never a replacement"},{key:"ai",label:"AI features",hint:"Adds the AI surface (chat, models, automations, integrations) and unlocks the runner option. A desktop project ALWAYS ships the app's local AI stack: this option sets config.desktop.agents.enabled (the runtime gate on those screens). Off keeps config.ai.enabled false so the AI nav stays hidden - the AI packages still ship, because packages/translate depends on @repo/ai for the i18n pipeline every project uses.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural",shapesTree:!0},{key:"webAi",label:"AI in the web app",hint:"Whether the WEB app shows the AI surfaces - chat, automations, model and integration settings, and their nav entries. Off is the app-first shape: your desktop or mobile app is where your AI product lives and the website keeps account, billing and licensing only. The AI SERVICE is untouched either way (config.ai.enabled, the /api/ai routes and the desktop agents all stay exactly as your AI option set them), so both apps keep talking to the same backend. Only meaningful with AI features and at least one app - the desktop app or the mobile app - and never with the runner: its pairing and device screens are part of the web AI surface.",category:"Features",kind:"boolean",default:!0,adoptable:!0,impact:"config",shapesTree:!1,requires:e=>e.ai===!0&&(e.desktop===!0||e.mobile===!0)&&e.runner!==!0,requiresLabel:"requires AI features and an app to host them (the desktop app or the mobile app), and is unavailable with the runner (its pairing and device screens live in the web AI surface)"},{key:"runner",label:"Runner daemon",hint:"Lets your users connect a machine that runs their own AI subscriptions (and any device-side work you add) on their hardware. They install AgentRunner, the daemon we publish and keep updated - your project ships the pairing, dispatch and Runners surface, never the daemon. Needs an always-on backend: the transport holds one connection open per paired device, which serverless (Vercel) cannot do.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural",shapesTree:!0,requires:e=>e.ai===!0&&e.webAi!==!1&&me({...e,runner:!0})===void 0,requiresLabel:"requires AI features, the web AI surface, and an always-on deployment target (not Vercel)"},{key:"credits",label:"Metered credits",hint:"Adds metered usage credits on top of subscription plans.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"config",shapesTree:!1,requires:e=>oe(e),requiresLabel:"requires a payment provider"},{key:"revenueSharing",label:"Revenue sharing",hint:"Opt-in MRR leaderboard with dofollow backlinks.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"config",shapesTree:!1}];function $s(e,t){return e[t]!==void 0}function cn(e){return hr.filter(t=>t.adoptable&&!$s(e,t.key)&&(t.requires?t.requires(e):!0)).map(t=>({key:t.key,label:t.label,hint:t.hint,kind:t.kind,default:typeof t.default=="function"?t.default(e):t.default,...t.choices?{choices:t.choices}:{},impact:t.impact,...t.requiresLabel?{requiresLabel:t.requiresLabel}:{}}))}function js(e,t){switch(e.kind){case"boolean":return typeof t=="boolean";case"enum":return typeof t=="string"&&(e.choices??[]).includes(t);case"multiselect":return Array.isArray(t)&&t.every(r=>typeof r=="string"&&(e.choices??[]).includes(r))}}function Ms(e){let t=(e.choices??[]).map(r=>JSON.stringify(r)).join(", ");switch(e.kind){case"boolean":return"true or false";case"enum":return`one of ${t}`;case"multiselect":return`an array of ${t}`}}function ln(e){let t=[];for(let r of hr){let n=e[r.key];n!==void 0&&(js(r,n)||t.push(`\`${r.key}\` is ${JSON.stringify(n)}, but it must be ${Ms(r)}.`))}return t}function pn(e){let t={};for(let r of hr)if(typeof r.default=="function"){if(r.requires&&!r.requires(e))continue;t[r.key]=r.default(e)}else t[r.key]=Array.isArray(r.default)?[...r.default]:r.default;return{...t,...e}}function fr(e,t){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||t}function jt(e){return e.split("-").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function Mt(e){return/^[a-z][a-z0-9-]*$/.test(e)}function j(e){return e instanceof Error?e.message:String(e)}var dn=["runner"];function gr(e){if(!dn.some(r=>e[r]===!0))return e;if(e.ai===!1)throw new Error("--runner requires AI features; remove --no-ai.");return e.ai===void 0&&(e.ai=!0),e}function yt(e){if(e.mobilePurchases!=="revenuecat")return e;if(e.mobile===!1)throw new Error("--mobile-purchases revenuecat requires the mobile app; remove --no-mobile.");if(e.paymentProvider==="none")throw new Error("--mobile-purchases revenuecat requires a payment provider (got --payment none): store purchases are additive to your web billing, never a replacement.");return e.mobile===void 0&&(e.mobile=!0),e}function bt(e){if(e.webAi!==!1)return e;if(e.ai===!1)throw new Error("--no-web-ai requires --ai (the desktop and mobile apps are where AI lives when the web surface is off).");if(e.desktop===!1&&e.mobile===!1)throw new Error("--no-web-ai requires --desktop or --mobile - with the web AI surface off, the desktop and mobile apps are the only places your AI features exist.");if(e.runner===!0)throw new Error("--runner needs the web AI surface; drop --runner or keep web AI.");return e}function un(e){return dn.some(r=>e[r]===!0)&&(e.ai=!0),e}function mn(e){if(e.webAi!==!1)return[];let t=[];return e.desktop!==!0&&e.mobile!==!0&&t.push("neither `desktop` nor `mobile` is true, so this project has no desktop or mobile app - and with the web AI surface off, those apps are the only places its AI features would exist."),e.runner===!0&&t.push("`runner` is true, and the runner's pairing, install and Runners screens are part of the web AI surface."),t}function hn(e){if(e.mobilePurchases!=="revenuecat")return[];let t=[];return e.mobile!==!0&&t.push("`mobile` is not true, so this project has no app to sell in - the store lane's paywall, purchase and restore surfaces all live in `apps/mobile`."),oe(e)||t.push('`paymentProvider` is absent or `"none"`, and store purchases are additive to your web billing - there is nothing here for them to sit on top of.'),t}function fn(e){let t=Us(e,e.desktopAi);return delete e.desktopAi,t}function Us(e,t){return e.desktop!==!0?{kind:"none"}:t===!0&&e.ai!==!0?{kind:"legacy-desktop-ai"}:t===!1&&e.ai===!0?{kind:"desktop-gains-ai"}:{kind:"none"}}function gn(e){let t={};if(e.name!==void 0){if(!Mt(e.name))throw new Error(`Invalid project name "${e.name}". Use lowercase letters, numbers, and hyphens only. Must start with a letter.`);t.projectName=e.name}if(e.appName!==void 0){if(!e.appName.trim())throw new Error("App name cannot be empty.");t.appName=e.appName}if(e.location!==void 0?t.projectDir=e.location==="."?process.cwd():e.location:t.projectName!==void 0&&(t.projectDir=`./${t.projectName}`),e.architecture!==void 0&&(t.architecture=e.architecture),e.payment!==void 0&&(t.paymentProvider=e.payment),e.email!==void 0&&(t.emailProvider=e.email),e.org!==void 0&&(t.multiTenancy=e.org),e.projects!==void 0&&(t.projects=e.projects),e.docs!==void 0&&(t.docs=e.docs),e.desktop!==void 0&&(t.desktop=e.desktop),e.mobile!==void 0&&(t.mobile=e.mobile),e.mobilePurchases!==void 0){if(!Me.includes(e.mobilePurchases))throw new Error(`Invalid mobile purchases "${e.mobilePurchases}". Valid values: ${Me.join(", ")}`);t.mobilePurchases=e.mobilePurchases}if(e.ai!==void 0&&(t.ai=e.ai),e.webAi!==void 0&&(t.webAi=e.webAi),e.runner!==void 0&&(t.runner=e.runner),gr(t),yt(t),bt(t),e.revenueSharing!==void 0&&(t.revenueSharing=e.revenueSharing),e.credits!==void 0){if(e.credits===!0&&e.payment==="none")throw new Error("--credits requires a payment provider (got --payment none).");t.credits=e.credits}if(e.currency!==void 0){if(!fe.includes(e.currency))throw new Error(`Invalid currency "${e.currency}". Valid values: ${fe.join(", ")}`);t.defaultCurrency=e.currency}if(e.deploy!==void 0){if(!ge.includes(e.deploy))throw new Error(`Invalid deployment target "${e.deploy}". Valid values: ${ge.join(", ")}`);t.deploymentTarget=e.deploy}if(e.database!==void 0){if(!ye.includes(e.database))throw new Error(`Invalid database provider "${e.database}". Valid values: ${ye.join(", ")}`);t.databaseProvider=e.database}if(e.cache!==void 0){if(!be.includes(e.cache))throw new Error(`Invalid cache provider "${e.cache}". Valid values: ${be.join(", ")}`);t.cacheProvider=e.cache}if(e.observability!==void 0){let r=e.observability.split(",").map(i=>i.trim()).filter(Boolean),n=r.filter(i=>!K.includes(i));if(n.length>0)throw new Error(`Invalid observability provider ${JSON.stringify(n.join(", "))}. Valid values: ${K.join(", ")}`);t.observability=r}if(e.preset!==void 0){if(!je.includes(e.preset))throw new Error(`Invalid preset "${e.preset}". Valid values: ${je.join(", ")}`);let r=he[e.preset].values;t.deploymentTarget??=r.deploymentTarget,t.databaseProvider??=r.databaseProvider,t.cacheProvider??=r.cacheProvider}if(e.demo===!0&&(t.demo=!0),e.baseUrl!==void 0){let r=e.baseUrl.trim();if(r==="")throw new Error("--base-url cannot be empty. Provide an absolute URL like https://example.com.");let n;try{n=new URL(r)}catch{throw new Error(`Invalid --base-url "${e.baseUrl}". Must be an absolute URL like https://example.com.`)}if(n.protocol!=="http:"&&n.protocol!=="https:")throw new Error(`Invalid --base-url "${e.baseUrl}". Must use http or https.`);t.baseUrl=`${n.protocol}//${n.host}`}return t}var nt={projectName:"my-saas",architecture:"fullstack",paymentProvider:"stripe",emailProvider:"smtp",multiTenancy:!1,projects:!1,docs:!1,desktop:!1,mobile:!1,mobilePurchases:"none",ai:!1,webAi:!0,runner:!1,revenueSharing:!1,credits:!0,defaultCurrency:"USD",deploymentTarget:"node",databaseProvider:"postgres",cacheProvider:"redis",observability:[]};function yn(e){let t=e.projectName??nt.projectName,r=e.projectDir??`./${t}`,n=e.appName??jt(t),i=e.deploymentTarget??nt.deploymentTarget,o=Z[i]?.edgeRuntime??!1,s=e.databaseProvider??(o?"neon":nt.databaseProvider),a=e.cacheProvider??(o?"upstash":nt.cacheProvider),d=e.emailProvider??(o?"resend":nt.emailProvider),u={...nt,...e,projectName:t,appName:n,projectDir:r,deploymentTarget:i,databaseProvider:s,cacheProvider:a,emailProvider:d};u.paymentProvider==="none"&&(u.credits=!1),(!u.mobile||u.paymentProvider==="none")&&(u.mobilePurchases="none"),yt(u);for(let y of Xe){if(u.deploymentTarget!==y.target)continue;let P=u.databaseProvider===y.provider?"database":"cache";if(u.databaseProvider===y.provider||u.cacheProvider===y.provider)throw new Error(`Incompatible: --deploy ${y.target} + --${P} ${y.provider}. ${y.reason}`)}for(let y of Ze)if(u.architecture===y.architecture&&u.deploymentTarget===y.target)throw new Error(`Incompatible: --architecture ${y.architecture} + --deploy ${y.target}. ${y.reason}`);let w=me(u);if(w)throw new Error(`Incompatible: --${w.feature} + --deploy ${w.target}. ${w.reason}`);return bt(u),u}function T(e){h.isCancel(e)&&(h.cancel("Setup cancelled."),process.exit(0))}function yr(e){let t=[];return e.databaseProvider==="neon"&&t.push({key:"DATABASE_URL",message:"Neon connection string (optional):",placeholder:"postgres://...",secret:!0}),e.databaseProvider==="supabase"&&t.push({key:"DATABASE_URL",message:"Supabase connection string (optional):",placeholder:"postgres://...",secret:!0}),e.cacheProvider==="upstash"&&(t.push({key:"UPSTASH_REDIS_REST_URL",message:"Upstash REST URL (optional):",placeholder:"https://...",secret:!1}),t.push({key:"UPSTASH_REDIS_REST_TOKEN",message:"Upstash REST token (optional):",secret:!0})),e.paymentProvider==="stripe"&&(t.push({key:"STRIPE_SECRET_KEY",message:"Stripe secret key (optional):",placeholder:"sk_test_...",secret:!0}),t.push({key:"STRIPE_WEBHOOK_SECRET",message:"Stripe webhook secret (optional):",placeholder:"whsec_...",secret:!0})),e.paymentProvider==="polar"&&(t.push({key:"POLAR_ACCESS_TOKEN",message:"Polar access token (optional):",secret:!0}),t.push({key:"POLAR_WEBHOOK_SECRET",message:"Polar webhook secret (optional):",secret:!0})),e.emailProvider==="resend"&&t.push({key:"RESEND_API_KEY",message:"Resend API key (optional):",placeholder:"re_...",secret:!0}),e.emailProvider==="ses"&&(t.push({key:"AMAZON_SES_REGION",message:"Amazon SES region (optional):",placeholder:"us-east-1",secret:!1}),t.push({key:"AMAZON_SES_KEY",message:"Amazon SES access key (optional):",secret:!0}),t.push({key:"AMAZON_SES_SECRET",message:"Amazon SES secret (optional):",secret:!0})),e.demo&&t.push({key:"TURNSTILE_SECRET_KEY",message:"Cloudflare Turnstile secret key (optional):",secret:!0}),t}async function bn(e,t){let r=!1;e&&gr(e),e&&yt(e),e&&bt(e),h.log.info(g.bold("Project"));let n=e?.projectName??await(async()=>{r=!0;let l=await h.text({message:"Project name:",placeholder:"my-saas",validate:m=>{if(!m?.trim())return"Project name is required.";if(!Mt(m))return"Use lowercase letters, numbers, and hyphens only. Must start with a letter."}});return T(l),l})(),i=e?.appName??await(async()=>{r=!0;let l=await h.text({message:"App name:",initialValue:jt(n),validate:m=>{if(!m?.trim())return"App name is required."}});return T(l),l})(),o=e?.projectDir??await(async()=>{r=!0;let l=await h.text({message:"Project location:",initialValue:`./${n}`});return T(l),l==="."?process.cwd():l})();h.log.info(g.bold("Infrastructure"));let s={};if(e?.deploymentTarget===void 0&&e?.databaseProvider===void 0&&e?.cacheProvider===void 0){r=!0;let l=await h.select({message:"Infrastructure:",options:[...je.map(m=>({value:m,label:he[m].label,hint:he[m].hint})),{value:"custom",label:"Customize",hint:"choose each piece yourself"}]});if(T(l),l!=="custom"){let m=he[l].values;s.deploymentTarget=m.deploymentTarget,s.databaseProvider=m.databaseProvider,s.cacheProvider=m.cacheProvider,h.log.info(`${he[l].label}: ${he[l].hint}.`)}}let a=e?.deploymentTarget??s.deploymentTarget??"node";if(e?.deploymentTarget===void 0&&s.deploymentTarget===void 0){r=!0;let l=await h.select({message:"Deployment target:",options:ge.map(m=>({value:m,label:Z[m].label,hint:Z[m].hint}))});T(l),a=l}let d=e?.architecture?Ze.find(l=>l.architecture===e.architecture&&l.target===a):void 0;if(d)throw new Error(`Incompatible: --architecture ${d.architecture} + --deploy ${d.target}. ${d.reason}`);let u=me({...e,deploymentTarget:a});if(u)throw new Error(`Incompatible: --${u.feature} + --deploy ${u.target}. ${u.reason}`);let w=me({runner:!0,deploymentTarget:a})!==void 0,y=new Set(Ze.filter(l=>l.target===a).map(l=>l.architecture)),P=et.filter(l=>!y.has(l)),I=e?.architecture??await(async()=>{if(P.length===1){let m=P[0];return h.log.info(`Auto-selected ${gt[m].label} architecture (only compatible option for ${Z[a].label}).`),m}r=!0;let l=await h.select({message:"Architecture:",options:P.map(m=>({value:m,label:gt[m].label,hint:gt[m].hint}))});return T(l),l})(),R=e?.databaseProvider??s.databaseProvider??await(async()=>{r=!0;let l=ye.filter(x=>!Xe.some(Pe=>Pe.target===a&&Pe.provider===x));if(l.length===1){let x=l[0];return h.log.info(`Auto-selected ${Le[x].label} (only compatible option for ${Z[a].label}).`),x}let m=await h.select({message:"Database provider:",options:l.map(x=>({value:x,label:Le[x].label,hint:Le[x].hint}))});return T(m),m})(),f=e?.cacheProvider??s.cacheProvider??await(async()=>{r=!0;let l=be.filter(x=>!Xe.some(Pe=>Pe.target===a&&Pe.provider===x));if(l.length===1){let x=l[0];return h.log.info(`Auto-selected ${$e[x].label} (only compatible option for ${Z[a].label}).`),x}let m=await h.select({message:"Cache provider:",options:l.map(x=>({value:x,label:$e[x].label,hint:$e[x].hint}))});return T(m),m})();if(Z[a]?.edgeRuntime){let l=on.map(m=>` - ${m}`).join(`
3
+ `);h.note(l,"Unavailable on edge runtime")}h.log.info(g.bold("Features"));let O=e?.paymentProvider??await(async()=>{r=!0;let l=await h.select({message:"Payment provider:",options:tt.map(m=>({value:m,label:Ct[m].label,hint:Ct[m].hint}))});return T(l),l})(),_e=e?.defaultCurrency??await(async()=>{if(O==="none")return"USD";r=!0;let l=await h.select({message:"Default currency:",options:fe.map(m=>({value:m,label:m,hint:Ce[m].name}))});return T(l),l})(),q=e?.emailProvider??await(async()=>{r=!0;let l=await h.select({message:"Email provider:",options:rt.map(m=>({value:m,label:Lt[m].label,hint:Lt[m].hint}))});return T(l),l})(),J=e?.observability??await(async()=>{r=!0;let l=await h.multiselect({message:"Observability (space to select, enter to confirm):",required:!1,initialValues:[],options:K.map(m=>({value:m,label:$t[m].label,hint:$t[m].hint}))});return T(l),l})(),Oe=e?.multiTenancy??await(async()=>{r=!0;let l=await h.confirm({message:"Enable multi-tenancy (organizations)?",initialValue:!1});return T(l),l})(),xe=e?.projects??await(async()=>{r=!0;let l=await h.confirm({message:"Enable projects (a switcher that scopes a user's work)?",initialValue:!1});return T(l),l})(),De=e?.docs??await(async()=>{r=!0;let l=await h.confirm({message:"Include docs app? (self-hosted Fumadocs documentation site)",initialValue:!1});return T(l),l})(),Je=t?.desktopAllowed!==!1,v=e?.desktop??(Je?await(async()=>{r=!0;let l=await h.confirm({message:"Include the desktop app? (apps/desktop - cross-platform, built with Electron, device-auth). With AI on, it also runs a local AI stack.",initialValue:!1});return T(l),l})():(h.log.info(g.dim("Desktop app: available on the Pro plan and up - skipped.")),!1)),U=t?.mobileAllowed!==!1,G=e?.mobile??(U?await(async()=>{r=!0;let l=await h.confirm({message:"Include the mobile app? (apps/mobile - iOS and Android, built with Expo, with push notifications and universal links)",initialValue:!1});return T(l),l})():(h.log.info(g.dim("Mobile app: available on the Pro plan and up - skipped.")),!1)),se=!G||O==="none"?"none":e?.mobilePurchases??await(async()=>{r=!0;let x=await h.select({message:"Mobile in-app purchases?",options:[{value:"revenuecat",label:"RevenueCat",hint:"store billing on top of your web provider - Apple and Google charge, your app grants"},{value:"none",label:"None",hint:"web billing only; the app shows the plan, never a paywall"}],initialValue:"none"});return T(x),x})();yt({mobile:G,mobilePurchases:se,paymentProvider:O});let C=e?.ai??await(async()=>{r=!0;let l=await h.confirm({message:"Add AI features? (chat, models, automations, integrations - and unlocks the runner)",initialValue:!1});return T(l),l})(),de=t?.runnerAllowed!==!1,X=e?.webAi===!1,ue=!C||w||X?!1:e?.runner??(de?await(async()=>{r=!0;let l=await h.confirm({message:"Add the runner daemon for your WEB users? A small program they install on their own machine or VPS to run their Claude Code / Codex subscription for this app 24/7 - the AI runs on their hardware, not yours. Off means no install UI and no pairing endpoint on your site; you can adopt it later.",initialValue:!1});return T(l),l})():(h.log.info(g.dim("Runner: available on the Pro plan and up - skipped.")),!1));C&&w?h.log.info(g.dim(`Runner: ${Z[a].label} cannot hold a connection open - skipped.`)):C&&X&&h.log.info(g.dim("Runner: its pairing and device screens live in the web AI surface, which is off - skipped."));let Ne=e?.webAi??((v||G)&&C&&!ue?await(async()=>{r=!0;let l=await h.confirm({message:"Show the AI surfaces in the web app? (No: app-first - your desktop or mobile app is where your AI product lives, and the web keeps account, billing, and licensing only)",initialValue:!0});return T(l),l})():!0);bt({webAi:Ne,ai:C,desktop:v,mobile:G,runner:ue});let N=e?.revenueSharing??await(async()=>{r=!0;let l=await h.confirm({message:"Enable revenue sharing? (opt-in MRR leaderboard with dofollow backlinks)",initialValue:!1});return T(l),l})(),ht=O==="none"?!1:e?.credits??await(async()=>{r=!0;let l=await h.confirm({message:"Enable credits? (metered usage on top of subscription plans)",initialValue:!0});return T(l),l})(),pr=e?.demo,Dt=yr({databaseProvider:R,cacheProvider:f,paymentProvider:O,emailProvider:q,demo:pr}),ft={};if(Dt.length>0&&r){h.log.info(g.bold("Credentials")+g.dim(" all optional - press Enter to skip, fill in .env later"));for(let l of Dt)if(r=!0,l.secret){let m=await h.password({message:l.message,mask:"*"});T(m),typeof m=="string"&&m.trim()&&(ft[l.key]=m.trim())}else{let m=await h.text({message:l.message,placeholder:l.placeholder});T(m),typeof m=="string"&&m.trim()&&(ft[l.key]=m.trim())}}if(r){let l=[` Name: ${g.cyan(n)}`,` App name: ${g.cyan(i)}`,` Location: ${g.cyan(o)}`,` Architecture: ${g.cyan(gt[I].label)}`].join(`
4
+ `),m=[` Deploy target: ${g.cyan(Z[a]?.label??"Node.js / Docker")}`,` Database: ${g.cyan(Le[R].label)}`,` Cache: ${g.cyan($e[f].label)}`].filter(Boolean).join(`
5
+ `),x=[O!=="none"?` Payment: ${g.cyan(Ct[O].label)} (${_e})`:` Payment: ${g.dim("none")}`,` Credits: ${ht?g.cyan("Yes"):g.dim("No")}`,` AI features: ${C?g.cyan("Yes"):g.dim("No")}`,...C?[` Runner: ${ue?g.cyan("Yes"):g.dim("No")}`]:[],...Ne?[]:[` Web AI: ${g.dim("No")} (app-first)`],` Email: ${g.cyan(Lt[q].label)}`,` Observability: ${J.length===0?g.dim("none"):g.cyan(J.map(dr=>$t[dr].label).join(", "))}`,` Multi-tenancy: ${Oe?g.cyan("Yes"):g.dim("No")}`,` Projects: ${xe?g.cyan("Yes"):g.dim("No")}`,` Docs app: ${De?g.cyan("Yes"):g.dim("No")}`,` Desktop app: ${v?g.cyan("Yes"):g.dim("No")}`,` Mobile app: ${G?g.cyan("Yes"):g.dim("No")}`,...se==="none"?[]:[` In-app purch.: ${g.cyan("RevenueCat")}`],` Rev. sharing: ${N?g.cyan("Yes"):g.dim("No")}`].join(`
6
+ `),Pe=[g.bold("Project"),l,"",g.bold("Infrastructure"),m,"",g.bold("Features"),x];if(Dt.length>0){let dr=Dt.map(tn=>{let Cs=ft[tn.key]?g.green("provided"):g.dim("skipped");return` ${tn.key}: ${Cs}`}).join(`
7
+ `);Pe.push("",g.bold("Credentials"),dr)}h.note(Pe.join(`
8
+ `),"Summary");let en=await h.confirm({message:"Proceed with these settings?"});(h.isCancel(en)||!en)&&(h.cancel("Setup cancelled."),process.exit(0))}return{projectName:n,appName:i,projectDir:o,architecture:I,deploymentTarget:a,databaseProvider:R,cacheProvider:f,paymentProvider:O,emailProvider:q,observability:J,multiTenancy:Oe,projects:xe,docs:De,desktop:v,mobile:G,mobilePurchases:se,ai:C,webAi:Ne,runner:ue,revenueSharing:N,credits:ht,defaultCurrency:_e,...Object.keys(ft).length>0?{credentials:ft}:{},...e?.baseUrl!==void 0?{baseUrl:e.baseUrl}:{},...pr!==void 0?{demo:pr}:{}}}import{createReadStream as _n,existsSync as qs}from"fs";import{mkdir as Js}from"fs/promises";import{join as Xs}from"path";import{Readable as Zs}from"stream";import{pipeline as Ir}from"stream/promises";import{extract as Qs}from"tar";import{join as Ue}from"path";import{homedir as Fs}from"os";var wt=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",Q=".generatesaas",Ae=Ue(Q,"manifest.json"),wn="nextjs",br="nuxt",wr="2.4.1",vn=Ue(Q,"hashes.json"),Ut=Ue(Q,"template-hashes.json"),Ft=Ue(Q,"template"),kn=Ue(Q,"staging"),En=Ue(Q,"staging.json"),we=Ue(Fs(),".generatesaas");var F=class extends Error{constructor(r,n,i){super(n);this.status=r;this.body=i}status;body;name="ApiError"};function ve(e){return{apiKey:e,baseUrl:wt}}async function Ie(e,t,r){let n=`${e.baseUrl}${t}`,i=await fetch(n,{...r,headers:{...r?.headers,Authorization:`Bearer ${e.apiKey}`,"User-Agent":"generatesaas-cli/3.7.0"}});if(!i.ok){let o,s;try{s=await i.json(),o=s.error??`API ${i.status}: ${t}`}catch{o=`API ${i.status}: ${t}`}throw new F(i.status,o,s)}return i}import{existsSync as Vs,readFileSync as Bs,writeFileSync as Gs,mkdirSync as Ks}from"fs";import{dirname as Hs}from"path";import*as Re from"@clack/prompts";function vt(){if(!Vs(we))return null;try{let e=JSON.parse(Bs(we,"utf-8"));return e.apiKey?e.apiKey:(e.token&&!e.apiKey&&Re.log.warning(`Found old GitHub token in ${we}. Run 'generatesaas init' to set up your API key.`),null)}catch{return null}}function Fe(e){Ks(Hs(we),{recursive:!0}),Gs(we,JSON.stringify({apiKey:e},null," ")+`
9
+ `,{mode:384})}async function it(e){if(e?.apiKey)return e.apiKey;let t=process.env.GENERATESAAS_API_KEY;if(t)return t;let r=vt();if(r)return r;if(!e?.prompt)throw new Error("API key not found. Set GENERATESAAS_API_KEY or run 'generatesaas init' to configure.");return kt()}async function kt(){let e=await Re.text({message:"Enter your GenerateSaaS API key:",placeholder:"gs_live_...",validate:t=>{if(!t?.trim())return"API key is required."}});return Re.isCancel(e)&&(Re.cancel("Setup cancelled."),process.exit(0)),e.trim()}import{readFile as Ys}from"fs/promises";async function Te(e){let t=process.env.GENERATESAAS_VERSION_INDEX;return t?JSON.parse(await Ys(t,"utf-8")):process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{latest:"0.0.0-ci",versions:[{version:"0.0.0-ci",date:new Date().toISOString(),breaking:!1}],entitlements:null}:await(await Ie(e,"/versions")).json()}async function Sn(e,t){if(process.env.GENERATESAAS_OFFLINE_LICENSE==="1")return null;try{return await(await Ie(e,`/changelog/${encodeURIComponent(t)}`)).text()}catch(r){if(r instanceof F&&r.status===404)return null;throw r}}async function Pn(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{skills:{"generatesaas-update":{skillMd:`# Offline skill stub
10
+ `,scripts:{},references:{}}}}:await(await Ie(e,`/skills/${encodeURIComponent(t)}`)).json()}function Vt(e){if(!(e instanceof F)||e.status!==403)return null;let t=e.body;return!t||t.code!=="update_window_expired"?null:{message:e.message,lastAllowedVersion:t.lastAllowedVersion??null}}var vr="nextjs";function An(e){return{frontend:vr,architecture:e.architecture,deployTarget:e.deploymentTarget,database:e.databaseProvider,cache:e.cacheProvider,payment:e.paymentProvider,email:e.emailProvider,multiTenancy:e.multiTenancy,blog:!0,docs:e.docs,desktop:e.desktop,mobile:e.mobile,runner:e.runner,credits:e.credits,revenueSharing:e.revenueSharing,currency:e.defaultCurrency}}async function kr(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:"offline-test-token",licenseId:"offline-test-license-id",installId:t.installId}:await(await Ie(e,"/license/sign",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function In(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:t.currentToken}:await(await Ie(e,"/license/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function Er(e,t){let r=await fetch(`${e}/license/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok)throw new Error(`Verification service returned ${r.status}`);return await r.json()}async function Rn(e,t,r){let n=await fetch(`${e}/license/inspect`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify(r)});if(!n.ok){let i=await n.json().catch(()=>null);throw new Error(i?.error??`Inspect endpoint returned ${n.status}`)}return await n.json()}var Sr=new Set([".git","node_modules",".pnpm-store",".env",".env.test",".turbo",".data","dist",".next",".next-e2e-web",".next-e2e-desktop",".svelte-kit",".wrangler",".devcontainer","playwright-report","test-results",".drive-out",".mastra"]),Pr=new Set(["pnpm-lock.yaml"]);function st(e){if(Ar(e))return!0;for(let t of e.split("/"))if(Pr.has(t))return!0;return!1}var Ws=new Set(["data","mksaas","references","scripts",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","vitest.config.ts","README.md","TODO.md","OVERVIEW.md","HANDOFF.md"]),Tn=["scripts/docs-lint.ts"],zs=["docs/superpowers","packages/cli","packages/cli-api","packages/config/tests/runner-brand-lockstep.test.ts","packages/database/tests/no-migration-history.test.ts","apps/web/tests/content/competitor-claims.test.ts","apps/runner","docs/agentrunner","infra/docker-compose.yml",".claude/commands",".claude/skills/web-port",".claude/skills/web-port-workspace",".claude/settings.local.json",".claude/worktrees"];function Ar(e){let t=e.split("/");for(let r of t)if(Sr.has(r))return!0;if(Tn.includes(e)||Tn.some(r=>r.startsWith(e+"/")))return!1;if(Ws.has(t[0]))return!0;for(let r of zs)if(e===r||e.startsWith(r+"/"))return!0;return!1}async function Bt(e,t,r){await Js(r,{recursive:!0});let n=process.env.GENERATESAAS_TEMPLATE_TARBALL_DIR;if(n){let a=Xs(n,`v${t}.tar.gz`);if(!qs(a))throw new Error(`GENERATESAAS_TEMPLATE_TARBALL_DIR is set but ${a} does not exist.`);await Ir(_n(a),Rr(r));return}let i=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(i){await Ir(_n(i),Rr(r));return}let o=await Ie(e,`/template/${encodeURIComponent(t)}`);if(!o.body)throw new Error("Empty response body");let s=Zs.fromWeb(o.body);await Ir(s,Rr(r))}function Rr(e){return Qs({cwd:e,strip:1,filter:t=>{let r=t.replace(/^[^/]+\//,"");return r?!Ar(r):!0},sync:!1})}import{readdir as Gt,readFile as ot,rm as xn,rmdir as eo,writeFile as Et}from"fs/promises";import{join as H}from"path";async function Dn(e){let t;try{t=await Gt(e,{withFileTypes:!0})}catch{return!1}let r=!1;for(let n of t){let i=H(e,n.name);if(n.isDirectory()){await Dn(i)&&(r=!0);continue}if(n.name.startsWith("_")){r=!0;continue}await xn(i,{force:!0})}return r||await eo(e).catch(()=>{}),r}async function Nn(e){let t=H(e,"packages/content"),r=[];try{r=(await Gt(t,{withFileTypes:!0})).filter(s=>s.isDirectory()).map(s=>s.name)}catch{return}let n=new Set;for(let s of r){let a=H(t,s),d;try{d=(await Gt(a,{withFileTypes:!0})).filter(u=>u.isDirectory())}catch{continue}for(let u of d)n.add(u.name),await Dn(H(a,u.name))}let i=await io(e),o=new Set([...n].filter(s=>i===void 0||i.has(s)));await Promise.all([...o].map(s=>xn(H(e,"apps/web/public/images",s),{recursive:!0,force:!0}))),await oo(e,o)}var to="packages/config/src/index.ts",ro=/\bcontent:\s*\{[\s\S]*?\bsections:\s*\[([\s\S]*?)\]/,no=/\bslug:\s*"([^"]+)"/g;async function io(e){let t;try{t=await ot(H(e,to),"utf-8")}catch{return}let r=ro.exec(t)?.[1];if(r===void 0)return;let n=[...r.matchAll(no)].map(i=>i[1]).filter(i=>i!==void 0);return n.length>0?new Set(n):void 0}var so="packages/translate/.meta/mdx-translation-meta.json";async function oo(e,t){if(t.size===0)return;let r=H(e,so),n;try{n=JSON.parse(await ot(r,"utf-8"))}catch{return}if(typeof n!="object"||n===null)return;let i=n;if(typeof i.files!="object"||i.files===null)return;let o=i.files,s={};for(let[a,d]of Object.entries(o)){let u=a.split("/"),w=u.length>1?u[0]:void 0,y=u[u.length-1]?.startsWith("_")===!0;w!==void 0&&t.has(w)&&!y||(s[a]=d)}await Et(r,JSON.stringify({...i,files:s},null," ")+`
11
+ `)}var ao="packages/config/src/blog.ts";async function Cn(e){let t=H(e,ao),r=await ot(t,"utf-8"),n=On(On(r,"blogCategories",t),"blogAuthors",t);await Et(t,n)}function On(e,t,r){let n=new RegExp(`(export const ${t}\\b[^=]*=\\s*)\\[[\\s\\S]*?\\];`);if(!n.test(e))throw new Error(`emptyBlogConfig: could not find the \`export const ${t} = [...]\` declaration in ${r}. The boilerplate blog config may have been renamed or restructured; update the CLI strip in cleanup.ts.`);return e.replace(n,"$1[];")}async function Ln(e){let t=H(e,"packages/i18n/translations"),r;try{r=await Gt(t)}catch{return}for(let n of r){let i=H(t,n,"web.json"),o;try{o=await ot(i,"utf-8")}catch{continue}let s=JSON.parse(o);!s.content?.categories||s.content.categories.blog===void 0||(s.content.categories.blog={},await Et(i,JSON.stringify(s,null," ")+`
12
+ `))}}var co="packages/config/src/ai/tasks.ts",lo="The array carries a SINGLE INERT DEMO task: the CLI strips it for buyer builds (leaving an empty\n * `aiTasks`, which hides the Tasks tab) and keeps it for `--demo` builds.",po="The array starts EMPTY, which hides the Tasks tab until you declare your first task.";async function $n(e){let t=H(e,co),r=await ot(t,"utf-8"),n=r.replace(/\n?[\t ]*\/\/ gsaas:demo-ai-task-start[\s\S]*?\/\/ gsaas:demo-ai-task-end/,"");if(n===r)throw new Error(`removeDemoAiTasks: could not find the \`gsaas:demo-ai-task\` markers in ${t}. The boilerplate AI tasks config may have been renamed or restructured; update the CLI strip in cleanup.ts.`);let i=n.replace(lo,po);if(i===n)throw new Error(`removeDemoAiTasks: the demo-task paragraph in ${t} did not match, so the shipped file would describe a demo task it no longer carries (boilerplate drift).`);await Et(t,i)}async function jn(e){let t=H(e,".claude","settings.json"),r;try{r=await ot(t,"utf8")}catch{return}let n=JSON.parse(r);delete n.alwaysThinkingEnabled,delete n.enableAllProjectMcpServers,n.env&&(delete n.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS,Object.keys(n.env).length===0&&delete n.env),n.permissions?.allow&&(n.permissions.allow=n.permissions.allow.filter(uo)),await Et(t,JSON.stringify(n,null," ")+`
13
+ `)}function uo(e){return!(e.startsWith("mcp__")||e.includes("nuxt"))}import{readFileSync as Eo}from"fs";import{dirname as So,join as _r}from"path";import{fileURLToPath as Po}from"url";import{existsSync as mo}from"fs";import{join as Mn}from"path";function Kt(e,t,r){let n=Mn(e,...t);return mo(n)?n:Mn(e,r)}import{mkdir as ho,readdir as fo,rm as go,rmdir as yo,writeFile as bo}from"fs/promises";import{dirname as Ht,join as wo,relative as vo,sep as ko}from"path";function Ve(e){return e.split(ko).join("/")}async function Yt(e){await ho(e,{recursive:!0})}async function c(e,t){await Yt(Ht(e)),await bo(e,t,"utf-8")}async function at(e,t){await go(e,{force:!0});let r=Ht(e);for(;r!==t&&r!==Ht(r);){try{await yo(r)}catch{return}r=Ht(r)}}async function ke(e,t,r,n){let i=[],o=await fo(e,{withFileTypes:!0});for(let s of o){let a=wo(e,s.name),d=Ve(vo(t,a));r(d)||(s.isDirectory()?i.push(...await ke(a,t,r,n)):s.isFile()&&(n===void 0||n(d))&&i.push(a))}return i}var Ao={postgres:"Postgres",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"},Io={resend:"Resend",ses:"Amazon SES",smtp:"SMTP"},Ro={redis:"Redis",upstash:"Upstash Redis"},To={node:"Node.js / Docker",vercel:"Vercel"},Tr="apps/web",_o="Next.js 16",Oo="`app/` routes, `components/`, `lib/`",xo={stripe:"Stripe",polar:"Polar"};function Do(e){let t=e.architecture==="fullstack",r=t?"(fullstack - Hono API mounted inside the app)":"(separate - standalone Hono backend)",n=t?`Mounted inside \`${Tr}\`. A standalone \`apps/backend\` is also kept (inert) so you can switch to separate later.`:"Runs from `apps/backend`; the frontend reaches it over HTTP.",i=Ro[e.cacheProvider],o=To[e.deploymentTarget],s=e.paymentProvider==="none"?"":`
14
+ - **Payments:** ${xo[e.paymentProvider]}`,a=e.desktop?e.mobile?", the desktop app (built with Electron) under `docs/desktop/` and the mobile app (built with Expo) under `docs/mobile/`":", and the desktop app (built with Electron) under `docs/desktop/`":e.mobile?", and the mobile app (built with Expo) under `docs/mobile/`":"",d=e.runner?"\n- **Runner daemon** - your users install AgentRunner on a machine they control and pair it with their account. Everything you own lives in this repo: the `/runner/*` routes, the relay, the Runners page, and the capability layer runs get. The daemon itself is published separately and auto-updates - there is nothing to build or release here.":"",u=`\`useTranslations()\` from \`next-intl\` in components; messages are loaded in \`${Tr}/i18n/request.ts\`.`;return Co({archSuffix:r,apiLocation:n,databaseName:Ao[e.databaseProvider],cacheName:i,paymentLine:s,emailName:Io[e.emailProvider],deployName:o,runnerBullet:d,appDocsRef:a,i18nRule:u,webAppDir:Tr,webAppName:_o,webAppLayout:Oo})}function No(){return Kt(So(Po(import.meta.url)),["generators","content"],"content")}function Co(e){let t=Eo(_r(No(),"AGENTS.template.md"),"utf-8"),r=new Map(Object.entries(e)),n=t.replace(/\{\{(\w+)\}\}/g,(o,s)=>{let a=r.get(s);if(a===void 0)throw new Error(`AGENTS.template.md uses unknown token {{${s}}}`);return a}),i=/\{\{[^}]*\}\}/.exec(n);if(i)throw new Error(`AGENTS.template.md carries an unrendered token: ${i[0]}`);return n}async function Un(e){await c(_r(e.projectDir,"AGENTS.md"),Do(e)),await c(_r(e.projectDir,"CLAUDE.md"),`@AGENTS.md
15
+ `)}import{join as Lo}from"path";var Fn="apps/web",Or="Next.js 16",$o={postgres:"Postgres (self-hosted)",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"};function jo(e){let t=e.appName.trim()||e.projectName,r=$o[e.databaseProvider],n=e.architecture==="fullstack"?`${Or} app at \`${Fn}/\` with the Hono API mounted inside it. A standalone \`apps/backend/\` is also included (inert in this fullstack setup) so you can split the API into a separate service later without re-scaffolding.`:`${Or} app at \`${Fn}/\` and a separate Hono backend at \`apps/backend/\`.`,i=e.architecture==="fullstack"?`- App + API: http://localhost:3000
16
16
  - Inngest dev server: http://127.0.0.1:8288`:`- App: http://localhost:3000
17
17
  - API: http://localhost:3010
18
18
  - Inngest dev server: http://127.0.0.1:8288`,o=e.ai?`pnpm mastra:studio # optional: inspect the AI assistant and its memory (localhost:4111)
@@ -23,7 +23,7 @@ ${n}
23
23
 
24
24
  ## Stack
25
25
 
26
- - **Frontend:** ${yr}
26
+ - **Frontend:** ${Or}
27
27
  - **Backend:** Hono (TypeScript, RPC-typed)
28
28
  - **Auth:** Better Auth
29
29
  - **ORM / DB:** Drizzle + ${r}
@@ -75,30 +75,41 @@ pnpm dlx generatesaas eject
75
75
  ## Updates
76
76
 
77
77
  To pull the latest boilerplate changes into this project, open it in your AI coding agent and ask: \`update my GenerateSaaS project\`.
78
- `}async function Pn(e){await l(fo(e.projectDir,"README.md"),go(e))}import{access as yo}from"fs/promises";import{join as Ft}from"path";async function vo(e,t,r){let n=Ft(e,"apps/desktop/dev-app-update.yml");try{await yo(n)}catch{throw new Error(`Cannot neutralize the desktop dev update feed: ${n} is missing (boilerplate drift).`)}await l(n,`provider: generic
78
+ `}async function Vn(e){await c(Lo(e.projectDir,"README.md"),jo(e))}import{access as Mo}from"fs/promises";import{join as St}from"path";async function Uo(e,t,r){let n=St(e,"apps/desktop/dev-app-update.yml");try{await Mo(n)}catch{throw new Error(`Cannot neutralize the desktop dev update feed: ${n} is missing (boilerplate drift).`)}await c(n,`provider: generic
79
79
  url: ${t}
80
80
  updaterCacheDirName: ${r}
81
- `)}function wo(e){let t=e.split(".");return t.length>=3?t.slice(1).join("."):e}function bo(e,t){let r=qr(e,"app");return{appId:`${t.split(".").reverse().join(".")}.${r.replace(/-/g,"")}`,productName:e,protocol:r}}var nt={name:"AgentRunner",binary:"agentrunner",envPrefix:"AGENTRUNNER",repoUrl:"https://github.com/AlexChelan/AgentRunner",installBase:"https://github.com/AlexChelan/AgentRunner/releases/latest/download",image:"ghcr.io/alexchelan/agentrunner"};function ko(e){return e.runner?["{"," enabled: true,",' clientId: "runner",'," daemon: {",` name: ${JSON.stringify(nt.name)},`,` binary: ${JSON.stringify(nt.binary)},`,` envPrefix: ${JSON.stringify(nt.envPrefix)},`,` repoUrl: ${JSON.stringify(nt.repoUrl)},`,` installBase: ${JSON.stringify(nt.installBase)},`,` image: ${JSON.stringify(nt.image)}`," }"," }"].join(`
82
- `):'{ enabled: false, clientId: "runner" }'}function Eo(e){let t=[" // PRESENCE IS THE SWITCH: reporting runs as soon as the credential below is non-empty. There"," // is no separate `enabled` flag to remember - pasting the key IS turning it on, and clearing it"," // is turning it off. These are PUBLISHABLE credentials: they only authorize SENDING events,"," // never reading them, which is why they live here rather than in env and why a packaged desktop"," // app can carry them. Server-side, SENTRY_DSN and POSTHOG_API_KEY override them."].join(`
81
+ `)}function Fo(e){let t=e.split(".");return t.length>=3?t.slice(1).join("."):e}function Vo(e,t){let r=fr(e,"app");return{appId:`${t.split(".").reverse().join(".")}.${r.replace(/-/g,"")}`,productName:e,protocol:r}}function Bo(e){let t=e.toLowerCase().replace(/[^a-z0-9_]/g,"");return/^[a-z]/.test(t)?t:`app${t}`}function Go(e,t){let r=fr(e,"app");return{appName:e,slug:r,bundleId:[...t.split(".").reverse(),r.replace(/-/g,"")].map(Bo).join("."),scheme:/^[a-z]/.test(r)?r:`app-${r}`}}var ct={name:"AgentRunner",binary:"agentrunner",envPrefix:"AGENTRUNNER",repoUrl:"https://github.com/AlexChelan/AgentRunner",installBase:"https://github.com/AlexChelan/AgentRunner/releases/latest/download",image:"ghcr.io/alexchelan/agentrunner"};function Ko(e){return e.runner?["{"," enabled: true,",' clientId: "runner",'," daemon: {",` name: ${JSON.stringify(ct.name)},`,` binary: ${JSON.stringify(ct.binary)},`,` envPrefix: ${JSON.stringify(ct.envPrefix)},`,` repoUrl: ${JSON.stringify(ct.repoUrl)},`,` installBase: ${JSON.stringify(ct.installBase)},`,` image: ${JSON.stringify(ct.image)}`," }"," }"].join(`
82
+ `):'{ enabled: false, clientId: "runner" }'}function Ho(e){let t=[" // PRESENCE IS THE SWITCH: reporting runs as soon as the credential below is non-empty. There"," // is no separate `enabled` flag to remember - pasting the key IS turning it on, and clearing it"," // is turning it off. These are PUBLISHABLE credentials: they only authorize SENDING events,"," // never reading them, which is why they live here rather than in env and why a packaged desktop"," // app can carry them. Server-side, SENTRY_DSN and POSTHOG_API_KEY override them."].join(`
83
83
  `),r=[];return e.includes("sentry")&&r.push(' sentry: { dsn: "" }'),e.includes("posthog")&&r.push([" // llmContent sends your users' prompts and completions to PostHog. Every number (tokens,"," // cost, latency, errors) is captured either way - turn it on only if your privacy policy"," // covers sending their message text to a third party.",' posthog: { publicKey: "", host: "https://us.i.posthog.com", llmContent: false }'].join(`
84
84
  `)),r.length===0?[" // Error and product reporting is OFF: no provider was selected at init, so neither adapter",' // nor SDK was generated. To turn one on, add `"sentry"`, `"posthog"` or both to the'," // `observability` array in `.generatesaas/manifest.json`, then run `generatesaas update` -"," // which re-shapes the project and installs what it needs. Note that update stages nothing"," // while you are already on the latest version, so take the upgrade first if it reports no"," // changes."," observability: {},"].join(`
85
85
  `):`${t}
86
86
  observability: {
87
87
  ${r.join(`,
88
88
  `)}
89
- },`}async function Rn(e){let t=e.appName.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),r=e.paymentProvider!=="none",n=e.baseUrl??"http://localhost:3000",i=e.baseUrl?new URL(e.baseUrl).hostname:"example.com",o=wo(i),s=e.demo?"false":"true",a=e.multiTenancy?`{
89
+ },`}async function Bn(e){let t=e.appName.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),r=e.paymentProvider!=="none",n=e.baseUrl??"http://localhost:3000",i=e.baseUrl?new URL(e.baseUrl).hostname:"example.com",o=Fo(i),s=e.demo?"false":"true",a=e.multiTenancy?`{
90
90
  multiTenant: true,
91
91
  organizationLimit: 5,
92
92
  defaultOrganizationName: "Personal"
93
- }`:"{ multiTenant: false }",p=e.projects?"{ enabled: true }":"{ enabled: false }",u=Eo(e.observability),v=e.demo?`{
93
+ }`:"{ multiTenant: false }",d=e.projects?"{ enabled: true }":"{ enabled: false }",u=Ho(e.observability),w=e.demo?`{
94
94
  enableInDev: true,
95
95
  chatwoot: {
96
96
  baseUrl: "https://support.alexchelan.com",
97
97
  websiteToken: "1uWvJLHkhWLihkK2bxLaj2Fg"
98
98
  }
99
- }`:"{}",g=e.architecture==="fullstack"?"false":"true",w=bo(t,o),P=`https://cdn.${o}/desktop`,f=ko(e),L=`import type { AgentsConfig, AppConfig } from "@repo/config/types";
99
+ }`:"{}",y=e.architecture==="fullstack"?"false":"true",P=Vo(t,o),I=`https://cdn.${o}/desktop`,R=Go(t,o),f=Ko(e),O=` // Store purchases (RevenueCat) for the mobile app - ADDITIVE to the web provider, never a
100
+ // replacement: store rules require digital goods to be sold through in-app purchase.
101
+ mobile: {
102
+ enabled: ${e.mobilePurchases==="revenuecat"},
103
+ provider: "revenuecat",
104
+ allowSandboxEvents: false,
105
+ grantCreditsToFamilyShare: false,
106
+ externalPurchaseLinks: "off",
107
+ customerCenter: true
108
+ }`,_e=`import type { AgentsConfig, AppConfig } from "@repo/config/types";
100
109
  import { resolveAgentsConfig } from "./agents";
101
110
  import { desktopConfig } from "./desktop.mjs";
111
+ import { mobileConfig } from "./mobile.mjs";
112
+ import { NOTIFICATIONS_ENABLED } from "./notifications-flag.mjs";
102
113
  import { supportConfig } from "./support";
103
114
 
104
115
  /**
@@ -131,7 +142,7 @@ export const config: AppConfig = {
131
142
  origins: ["http://localhost:3000", "http://localhost:5173"],
132
143
  waitlist: false,
133
144
  tenancy: ${a},
134
- projects: ${p},
145
+ projects: ${d},
135
146
  theme: {
136
147
  default: "system",
137
148
  switcher: true,
@@ -177,8 +188,12 @@ export const config: AppConfig = {
177
188
  payment: ${r?`{
178
189
  enabled: true,
179
190
  provider: "${e.paymentProvider}",
180
- bannedCountries: ["BY", "CU", "IR", "KP", "RU", "SY"]
181
- }`:"{ enabled: false }"},
191
+ bannedCountries: ["BY", "CU", "IR", "KP", "RU", "SY"],
192
+ ${O}
193
+ }`:`{
194
+ enabled: false,
195
+ ${O}
196
+ }`},
182
197
  sms: { enabled: false },
183
198
  newsletter: { enabled: false },
184
199
  apiKeys: {
@@ -186,10 +201,10 @@ export const config: AppConfig = {
186
201
  prefix: "key_"
187
202
  },
188
203
  apiDocs: true,
189
- performanceMonitor: { enabled: ${g} },
204
+ performanceMonitor: { enabled: ${y} },
190
205
  ${u}
191
206
  notifications: {
192
- enabled: true,
207
+ enabled: NOTIFICATIONS_ENABLED,
193
208
  maxPerUser: 100,
194
209
  cleanupDays: 30,
195
210
  dropdownCharLimit: 100,
@@ -231,7 +246,7 @@ ${u}
231
246
  }`:`{
232
247
  base: "${e.defaultCurrency}",
233
248
  list: [
234
- { symbol: "${Te[e.defaultCurrency].symbol}", name: "${Te[e.defaultCurrency].name}", code: "${e.defaultCurrency}", place: "${Te[e.defaultCurrency].place}", space: ${Te[e.defaultCurrency].space} }
249
+ { symbol: "${Ce[e.defaultCurrency].symbol}", name: "${Ce[e.defaultCurrency].name}", code: "${e.defaultCurrency}", place: "${Ce[e.defaultCurrency].place}", space: ${Ce[e.defaultCurrency].space} }
235
250
  ],
236
251
  countryMap: {
237
252
  default: "${e.defaultCurrency}"
@@ -363,6 +378,7 @@ ${u}
363
378
  }
364
379
  },
365
380
  desktop: desktopConfig,
381
+ mobile: mobileConfig,
366
382
  // Runner: the AgentRunner daemon your users install to run their own subscription CLIs for this app. Off by default.
367
383
  runner: ${f},
368
384
  // Outward MCP server exposing the capability layer to external agents. Its code ships with every
@@ -429,6 +445,7 @@ export * from "./footer";
429
445
  export * from "./i18n";
430
446
  export * from "./landing";
431
447
  export * from "./nav-active";
448
+ export * from "./notification-types";
432
449
  export * from "./password";
433
450
  export * from "./pricing";
434
451
  export * from "./projects";
@@ -436,7 +453,7 @@ export * from "./robots";
436
453
  export * from "./roles";
437
454
  export * from "./section-tabs";
438
455
  export * from "./tenancy";
439
- `,M=Ft(e.projectDir,"packages/config/src/index.ts");await l(M,L),await l(Ft(e.projectDir,"packages/config/src/support.ts"),`import type { SupportConfig } from "@repo/config/types";
456
+ `,q=St(e.projectDir,"packages/config/src/index.ts");await c(q,_e),await c(St(e.projectDir,"packages/config/src/support.ts"),`import type { SupportConfig } from "@repo/config/types";
440
457
 
441
458
  /**
442
459
  * Live-chat (support widget) configuration, re-exported by the config index as
@@ -448,8 +465,8 @@ export * from "./tenancy";
448
465
  * Node, which cannot resolve the index's extensionless relative imports. Keep any relative
449
466
  * import here type-only.
450
467
  */
451
- export const supportConfig: SupportConfig = ${v};
452
- `),await l(Ft(e.projectDir,"packages/config/src/desktop.mjs"),`// Plain-JS desktop app identity. NO env reads, so tooling that cannot import the
468
+ export const supportConfig: SupportConfig = ${w};
469
+ `),await c(St(e.projectDir,"packages/config/src/desktop.mjs"),`// Plain-JS desktop app identity. NO env reads, so tooling that cannot import the
453
470
  // TypeScript config index reads it directly: the Electron main + renderer bundles,
454
471
  // the CSP policy builder, and the electron-builder packaging config. Edit these
455
472
  // values once to rebrand - they are the same in dev and production, so they live in
@@ -457,14 +474,14 @@ export const supportConfig: SupportConfig = ${v};
457
474
 
458
475
  export const desktopConfig = {
459
476
  enabled: ${e.desktop},
460
- appId: "${w.appId}",
461
- productName: "${w.productName}",
462
- protocol: "${w.protocol}",
477
+ appId: "${P.appId}",
478
+ productName: "${P.productName}",
479
+ protocol: "${P.protocol}",
463
480
  baseUrl: "${n}",
464
481
  // Public base URL the release workflow uploads to; electron-updater polls
465
482
  // \`<url>/latest-mac.yml\` (\`latest.yml\` on Windows, \`latest-linux.yml\` on Linux) plus
466
483
  // the per-installer blockmaps, and the stable installer download links live under it.
467
- autoUpdate: { url: "${P}" },
484
+ autoUpdate: { url: "${I}" },
468
485
  // Whether the support live-chat widget you configured in config.support (Crisp,
469
486
  // Chatwoot) also mounts in the DESKTOP app. On by default, so one provider reaches
470
487
  // every surface. Set false to keep live chat on your website only - the app then
@@ -500,8 +517,122 @@ export const desktopConfig = {
500
517
  background: false
501
518
  }
502
519
  };
503
- `),e.desktop&&await vo(e.projectDir,P,`${w.protocol}-updater`)}import{join as So}from"path";function Ao(e){return e==="stripe"?' stripePriceId: "",':' polarProductId: "",'}function vr(e){let t=[];return e.withCredits&&(t.push(` credits: ${e.credits},`),t.push(" creditInterval: 30,")),e.maxConcurrentRuns!==void 0?(t.push(` apiRateLimit: { maxRequests: ${e.rateLimit} },`),t.push(` maxConcurrentRuns: ${e.maxConcurrentRuns}`)):t.push(` apiRateLimit: { maxRequests: ${e.rateLimit} }`),t.join(`
504
- `)}async function In(e){let t=So(e.projectDir,"packages/config/src/pricing.ts"),r=e.defaultCurrency;if(e.paymentProvider==="none"){let P=`import type { PricingConfig } from "@repo/config/types";
520
+ `),await c(St(e.projectDir,"packages/config/src/mobile.mjs"),`// Plain-JS mobile app identity. NO env reads, so tooling that cannot import the
521
+ // TypeScript config index reads it directly: app.config.ts, the EAS build config,
522
+ // Metro, and the app bundle itself (\`+native-intent\` reads UNIVERSAL_LINK_PATHS).
523
+ // The CLI wrote these values from your app name + domain at init; edit them here
524
+ // once to rebrand - they are the same in dev and production, so they live in
525
+ // committed config, not env. The config index re-exports this as config.mobile.
526
+
527
+ import { NON_DEFAULT_LOCALES } from "./i18n-locales.mjs";
528
+ import { NOTIFICATIONS_ENABLED } from "./notifications-flag.mjs";
529
+
530
+ export const mobileConfig = {
531
+ enabled: ${e.mobile},
532
+ // Home-screen and store display name.
533
+ appName: "${R.appName}",
534
+ // Expo slug, EAS project name, and the prefix the auth client namespaces its
535
+ // stored session under.
536
+ slug: "${R.slug}",
537
+ // iOS bundle id and Android application id.
538
+ bundleId: "${R.bundleId}",
539
+ // Deep-link scheme, with NO \`://\` - the code appends the separator it needs.
540
+ scheme: "${R.scheme}",
541
+ // The web host whose /.well-known files claim this app's universal links. Your
542
+ // PROJECT domain, subdomain included: it must match the host your links carry.
543
+ universalLinks: { host: "${i}" },
544
+ // Public store listing URLs. Empty hides the badge on the landing page, which is
545
+ // what you want until the listing exists.
546
+ stores: { ios: "", android: "" },
547
+ // Push notifications. The mobile answer AND the notifications flag the config index
548
+ // reads: push is a channel of in-app notifications, so turning those off turns push
549
+ // off with them. The left side stays your --mobile answer, so a project without the
550
+ // app reads false whatever the flag says.
551
+ push: { enabled: ${e.mobile} && NOTIFICATIONS_ENABLED },
552
+ // Biometric app lock: locks on cold start and after a configurable foreground delay.
553
+ appLock: { enabled: true }
554
+ };
555
+
556
+ // The web page an emailed organization invitation points at. A FIXED constant with no
557
+ // config.routes entry, deliberately: the association file, the Android intent filters,
558
+ // the app's own link handler and the invitation email all read this one value, and a
559
+ // route you could edit independently would move the page and break all four at once.
560
+ export const ACCEPT_INVITATION_PATH = "/accept-invitation";
561
+
562
+ // The web page an emailed account-deletion link points at. Fixed for the same reason,
563
+ // and read by packages/auth in EVERY project - the deletion mail link ships whether or
564
+ // not the mobile app does.
565
+ export const DELETE_ACCOUNT_PATH = "/delete-account";
566
+
567
+ // The web paths an installed app claims. PREFIXES, never globs: each consumer expands a
568
+ // prefix its own way, because one path carries its payload in a path segment
569
+ // (/accept-invitation/<id>) and the other in the query (/delete-account?token=).
570
+ // The locale-prefixed forms exist because a user browsing in another language can share
571
+ // that URL; the emails themselves always use the default-locale path.
572
+ export const UNIVERSAL_LINK_PATHS = [
573
+ ACCEPT_INVITATION_PATH,
574
+ DELETE_ACCOUNT_PATH,
575
+ ...NON_DEFAULT_LOCALES.flatMap((locale) => [
576
+ \`/\${locale}\${ACCEPT_INVITATION_PATH}\`,
577
+ \`/\${locale}\${DELETE_ACCOUNT_PATH}\`
578
+ ])
579
+ ];
580
+
581
+ // The app's OWN navigation targets. config.routes.* is the WEB app's route table -
582
+ // its /auth and /dashboard name Next.js pages, and Expo Router has no screen at
583
+ // either, so a mobile redirect to one lands on the unmatched-route screen. These are
584
+ // the paths apps/mobile/app/** creates, with the (group) segments elided the way the
585
+ // router elides them. Renaming a screen file means renaming its constant here.
586
+ //
587
+ // They are ALSO the allowlist a tapped push is matched against: a notification's
588
+ // stored link is a WEB path, and the app follows it only when it names a screen this
589
+ // table declares, falling back to the notification's own detail screen otherwise. A
590
+ // route with no constant is therefore not a deep-link target - which is why the
591
+ // purchases paywall deliberately has none: the paywall belongs to store purchases,
592
+ // which are optional, so a constant naming it would be a dead target in any project
593
+ // that turns them off.
594
+ export const HOME_PATH = "/";
595
+ export const SIGN_IN_PATH = "/sign-in";
596
+ export const SIGN_UP_PATH = "/sign-up";
597
+ export const VERIFY_EMAIL_PATH = "/verify-email";
598
+ export const MAGIC_LINK_SENT_PATH = "/magic-link-sent";
599
+ export const FORGOT_PASSWORD_PATH = "/forgot-password";
600
+ export const RESET_PASSWORD_PATH = "/reset-password";
601
+ export const TWO_FACTOR_PATH = "/two-factor";
602
+ export const PASSKEY_PATH = "/passkey";
603
+ /** app/onboarding.tsx - the post-sign-up walkthrough, outside the (app) group. */
604
+ export const ONBOARDING_PATH = "/onboarding";
605
+ /** app/(app)/notifications/index.tsx - the in-app notification list. */
606
+ export const NOTIFICATIONS_PATH = "/notifications";
607
+ /** app/(app)/chat/index.tsx - the AI chat conversation list. */
608
+ export const CHAT_PATH = "/chat";
609
+ /** app/(app)/automations/index.tsx - the automation list. */
610
+ export const AUTOMATIONS_PATH = "/automations";
611
+ /** app/(app)/settings/index.tsx - the settings hub every row below opens from. */
612
+ export const SETTINGS_PATH = "/settings";
613
+ /** app/(app)/settings/profile.tsx - name, avatar and email. */
614
+ export const SETTINGS_PROFILE_PATH = "/settings/profile";
615
+ /** app/(app)/settings/security.tsx - password, two-factor, passkeys and sessions. */
616
+ export const SETTINGS_SECURITY_PATH = "/settings/security";
617
+ /** app/(app)/settings/organization.tsx - the active organization and its members. */
618
+ export const SETTINGS_ORGANIZATION_PATH = "/settings/organization";
619
+ /** app/(app)/settings/projects.tsx - the project switcher. */
620
+ export const SETTINGS_PROJECTS_PATH = "/settings/projects";
621
+ /** app/(app)/settings/billing/index.tsx - plan, credits and invoices. */
622
+ export const SETTINGS_BILLING_PATH = "/settings/billing";
623
+ /** app/(app)/settings/notifications.tsx - push permission and per-type preferences. */
624
+ export const SETTINGS_NOTIFICATIONS_PATH = "/settings/notifications";
625
+ /** app/(app)/settings/appearance.tsx - colour scheme and language. */
626
+ export const SETTINGS_APPEARANCE_PATH = "/settings/appearance";
627
+ /** app/(app)/settings/privacy.tsx - analytics and crash-report choices. */
628
+ export const SETTINGS_PRIVACY_PATH = "/settings/privacy";
629
+ /** app/(app)/settings/app-lock.tsx - the biometric lock and its foreground delay. */
630
+ export const SETTINGS_APP_LOCK_PATH = "/settings/app-lock";
631
+ /** app/(app)/settings/about.tsx - version, legal links and support. */
632
+ export const SETTINGS_ABOUT_PATH = "/settings/about";
633
+ `),e.desktop&&await Uo(e.projectDir,I,`${P.protocol}-updater`)}import{join as Yo}from"path";function Wo(e,t){let r=[e==="stripe"?' stripePriceId: "",':' polarProductId: "",'];return t&&r.push(' appleProductId: "",',' googleProductId: "",'," storeAmounts: {},"),r.join(`
634
+ `)}function xr(e){let t=[];return e.withCredits&&(t.push(` credits: ${e.credits},`),t.push(" creditInterval: 30,")),e.maxConcurrentRuns!==void 0?(t.push(` apiRateLimit: { maxRequests: ${e.rateLimit} },`),t.push(` maxConcurrentRuns: ${e.maxConcurrentRuns}`)):t.push(` apiRateLimit: { maxRequests: ${e.rateLimit} }`),t.join(`
635
+ `)}async function Gn(e){let t=Yo(e.projectDir,"packages/config/src/pricing.ts"),r=e.defaultCurrency;if(e.paymentProvider==="none"){let I=`import type { PricingConfig } from "@repo/config/types";
505
636
 
506
637
  export const pricingConfig: PricingConfig = {
507
638
  defaultPlan: "free",
@@ -530,7 +661,7 @@ export const pricingConfig: PricingConfig = {
530
661
  credits: { enabled: false },
531
662
  products: { enabled: false, items: [] }
532
663
  };
533
- `;await l(t,P);return}let n=e.paymentProvider,i=Ao(n),o=e.credits,s=!!e.ai,a=vr({credits:5,rateLimit:100,withCredits:o,...s&&{maxConcurrentRuns:2}}),p=vr({credits:10,rateLimit:1e3,withCredits:o,...s&&{maxConcurrentRuns:5}}),u=vr({credits:40,rateLimit:5e3,withCredits:o,...s&&{maxConcurrentRuns:10}}),v=r==="USD"?"":` // !! ${r} pricing WARNING: creditsPerUsd anchors credit cost to USD (AI providers bill in
664
+ `;await c(t,I);return}let n=e.paymentProvider,i=Wo(n,e.mobilePurchases==="revenuecat"),o=e.credits,s=!!e.ai,a=xr({credits:5,rateLimit:100,withCredits:o,...s&&{maxConcurrentRuns:2}}),d=xr({credits:10,rateLimit:1e3,withCredits:o,...s&&{maxConcurrentRuns:5}}),u=xr({credits:40,rateLimit:5e3,withCredits:o,...s&&{maxConcurrentRuns:10}}),w=r==="USD"?"":` // !! ${r} pricing WARNING: creditsPerUsd anchors credit cost to USD (AI providers bill in
534
665
  // !! USD), but your plan prices and credit purchases are denominated in ${r}. Verify that the
535
666
  // !! ${r} amounts above cover the USD cost per credit (~0.50 USD at creditsPerUsd 1 with the
536
667
  // !! 100% markup) - and raise prices or creditsPerUsd if they do not.
@@ -539,7 +670,7 @@ export const pricingConfig: PricingConfig = {
539
670
  !! plan prices and credit purchases are denominated in ${r}. At the shipped economics 1 credit
540
671
  !! carries ~0.50 USD of AI cost - verify every ${r} price still covers it (see the comment on the
541
672
  !! credits block in packages/config/src/pricing.ts), or raise prices / creditsPerUsd.
542
- `);let g=o?` // creditsPerUsd scales USD cost -> credits (1 = 1 credit carries ~0.50 USD of cost after the
673
+ `);let y=o?` // creditsPerUsd scales USD cost -> credits (1 = 1 credit carries ~0.50 USD of cost after the
543
674
  // 100% markup); markupPercent is the DEFAULT margin for everything that debits credits (AI,
544
675
  // external APIs, your own features), and credits.markupByCategory overrides it per metered
545
676
  // service ("ai", "webSearch", "webExtract"). Two optional knobs shape each debit's size:
@@ -550,7 +681,7 @@ export const pricingConfig: PricingConfig = {
550
681
  // you change plan credits, prices, or any markup: each plan's credits / (creditsPerUsd *
551
682
  // (1 + your LOWEST markup)) must stay BELOW what that plan earns per credit interval, or full
552
683
  // utilization runs you at a loss.
553
- ${v} credits: { enabled: true, creditsPerUsd: 1, markupPercent: 100 }`:" credits: { enabled: false }",w=`import type { PricingConfig } from "@repo/config/types";
684
+ ${w} credits: { enabled: true, creditsPerUsd: 1, markupPercent: 100 }`:" credits: { enabled: false }",P=`import type { PricingConfig } from "@repo/config/types";
554
685
 
555
686
  export const pricingConfig: PricingConfig = {
556
687
  defaultPlan: "free",
@@ -600,7 +731,7 @@ ${i}
600
731
  anchorAmounts: { ${r}: 109 }
601
732
  }
602
733
  ],
603
- ${p}
734
+ ${d}
604
735
  },
605
736
  {
606
737
  id: "pro",
@@ -634,17 +765,18 @@ ${i}
634
765
  ${u}
635
766
  }
636
767
  ],
637
- ${g},
768
+ ${y},
638
769
  products: {
639
770
  enabled: false,
640
771
  items: []
641
772
  }
642
773
  };
643
- `;await l(t,w)}var Po={smtp:[{key:"SMTP_HOST",defaultValue:"localhost"},{key:"SMTP_PORT",defaultValue:"1025"},{key:"SMTP_USER"},{key:"SMTP_PASSWORD"}],ses:[{key:"AMAZON_SES_REGION",comment:"# TODO: Configure Amazon SES credentials (e.g. us-east-1)"},{key:"AMAZON_SES_KEY"},{key:"AMAZON_SES_SECRET"}],resend:[{key:"RESEND_API_KEY",comment:"# TODO: Add your Resend API key"}]},Ro={stripe:[{key:"STRIPE_SECRET_KEY",comment:"# TODO: Add your Stripe keys"},{key:"STRIPE_WEBHOOK_SECRET"},{key:"STRIPE_API_BASE",comment:"# Point the Stripe SDK at another host - a local mock, or a proxy of your own. Unset means api.stripe.com."}],polar:[{key:"POLAR_ACCESS_TOKEN",comment:"# TODO: Add your Polar keys"},{key:"POLAR_WEBHOOK_SECRET"}]};function Vt(e,t){return t?e.map(r=>{let n=t[r.key];return n?{...r,defaultValue:n,comment:void 0}:r}):e}function gt(e,t){for(let r of e)r.comment&&t.push(r.comment),r.defaultValue!==void 0?t.push(`${r.key}=${r.defaultValue}`):t.push(`#${r.key}=`)}function wr(e){return Array.from(crypto.getRandomValues(new Uint8Array(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}function On(e){return e.architecture==="fullstack"?{apiUrl:"http://localhost:3000/api",baseUrl:"http://localhost:3000"}:{apiUrl:"http://localhost:3010",baseUrl:"http://localhost:3000"}}var Io="https://your-app.example.com/api";function Bt(e,t,r,n){e.push(n==="example"?`${t}=`:`${t}=${r}`)}function Tn(e,t){let r=t==="example"?void 0:e.credentials,{apiUrl:n,baseUrl:i}=On(e),o=[];e.architecture==="separate"?o.push("# API Configuration","# Standalone backend's own URL (the frontend reaches it via the public var above).",`API_URL=${n}`,`BASE_URL=${i}`):o.push("# App","# (API_URL is derived from the frontend's *_PUBLIC_API_URL by the runtime env schema.)",`BASE_URL=${i}`),o.push("","# Client IP (production)","# The backend rate-limits by client IP and stamps it on audit entries. YOU ALMOST","# CERTAINLY DO NOT NEED THIS: it detects Vercel, Fly.io, Render, Railway, Netlify and","# Coolify from their own env vars, and treats a private connection peer (Docker, compose,","# Kubernetes, Dokploy, an nginx/Traefik/Caddy on the same host) as proof that something is","# forwarding to it. A forwarded header is trusted only in those cases, never on a server","# exposed directly - there, a caller could have sent it themselves."),e.architecture==="fullstack"?o.push("# This build runs the API in the frontend's route handler, which has no connection","# address, so on a host the backend does not recognise there is nothing to fall back on","# and every caller shares ONE rate-limit bucket: the 10/min sign-in limit applies to","# your whole app rather than per person, and audit entries record no address."):o.push("# This build reads the connection peer, so a server exposed directly identifies its","# callers correctly with nothing set here."),o.push("# SET IT when nothing above can see your topology - the common case is a CDN in front of a","# directly exposed server, where connections arrive from a public CDN address:","# Cloudflare -> TRUSTED_PROXY=cf-connecting-ip","# another proxy in front -> TRUSTED_PROXY=1 (the number of proxies)","# Whatever you set wins over detection; `false` turns detection off and trusts nothing.","# TRUSTED_PROXY=1"),o.push("","# Auth rate limits","# Raises every Better Auth rate limit by a whole number (1-50). Unset = 1 = the","# shipped limits. No value disables them: 0, empty or unparseable fails at boot.","# You almost certainly want the default. The reason to raise it is an automated suite,","# which drives ONE address and so shares one bucket - and the e2e config already sets","# its own value, so it needs nothing here.","# AUTH_RATE_LIMIT_MULTIPLIER=1"),o.push("","# Database"),gt(Vt(_e[e.databaseProvider].envVars,r),o),o.push("","# Cache"),gt(Vt(Oe[e.cacheProvider].envVars,r),o),e.cacheProvider==="redis"&&o.push("# Optional prefix on every Redis key the app writes. Set it when several deployments share","# one Redis instance, or to keep a load test out of the live keyspace.","# Set it once, before first boot. Changing it on a running deployment orphans in-flight","# auto-top-up claims and distributed locks (autotopup:pending:*, withMutex) - drain or use","# a maintenance window first.","#REDIS_KEY_PREFIX="),o.push("","# Authentication"),t==="example"&&o.push("# Generate a strong secret, e.g. `openssl rand -hex 32`"),Bt(o,"BETTER_AUTH_SECRET",crypto.randomUUID(),t),o.push("","# Content API (random secret; required when contentApi feature is enabled in @repo/config)"),Bt(o,"CONTENT_API_KEY",wr(32),t),o.push("","# Job Queue - Inngest","INNGEST_APP_ID=api"),Bt(o,"INNGEST_EVENT_KEY",wr(32),t),Bt(o,"INNGEST_SIGNING_KEY",wr(32),t),o.push("INNGEST_BASE_URL=http://127.0.0.1:8288"),e.architecture==="separate"&&(o.push("","# API Port (standalone backend)","API_PORT=3010"),o.push("","# CORS + cross-subdomain cookies (production only - leave unset for local dev)","# TRUSTED_ORIGINS=https://your-app.example.com","# AUTH_COOKIE_DOMAIN=.example.com"));let s=Po[e.emailProvider];if(s&&(o.push("","# Email"),gt(Vt(s,r),o)),e.paymentProvider!=="none"){let p=Ro[e.paymentProvider];p&&(o.push("","# Payment"),gt(Vt(p,r),o))}o.push("","# Storage - S3 Compatible (required for uploads: avatars, files)","# Works with AWS S3, Cloudflare R2, MinIO, and other S3-compatible services.","# Uploads fail until these are set - storage is enabled in @repo/config by default.","#STORAGE_REGION=us-east-1","#STORAGE_ENDPOINT=https://your-account.r2.cloudflarestorage.com","#STORAGE_ACCESS_KEY_ID=","#STORAGE_SECRET_ACCESS_KEY=","# Two buckets: one publicly readable (avatars, assets), one credentials-only (private","# uploads served through the app). The bucket boundary is the privacy guarantee.","#STORAGE_PUBLIC_BUCKET=","#STORAGE_PRIVATE_BUCKET=",'# Public base URL for the public bucket. Public file URLs are STORAGE_PUBLIC_URL + "/" + key.',"#STORAGE_PUBLIC_URL=https://cdn.example.com","","# Storage - Local Filesystem (optional overrides)","# Defaults work out of the box; override for Docker volume mounts.","#STORAGE_LOCAL_PATH=/data","#STORAGE_LOCAL_PUBLIC_URL=https://cdn.example.com/files"),o.push("","# Admin Notifications (optional)","# Real-time notifications when business events occur (signups, purchases, etc.).","# Each provider is auto-enabled when its env vars are set.","#DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...","#SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...","#TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz","#TELEGRAM_CHAT_ID=-1001234567890","#TELEGRAM_TOPIC_ID=123"),o.push("","# Social sign-in (optional)","# Enable the matching provider in @repo/config, then set its pair here.","#GOOGLE_CLIENT_ID=","#GOOGLE_CLIENT_SECRET=","#GITHUB_CLIENT_ID=","#GITHUB_CLIENT_SECRET="),o.push("","# SMS - Twilio (optional)","#TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","#TWILIO_AUTH_TOKEN=","#TWILIO_PHONE_NUMBER=+1234567890"),o.push("","# Newsletter (optional)","#RESEND_AUDIENCE_ID=","#LISTMONK_URL=https://listmonk.yourdomain.com","#LISTMONK_API_USER=admin","#LISTMONK_API_TOKEN=","#LISTMONK_LIST_ID=1"),o.push("","# Translations - OpenRouter (optional)","# Set to auto-translate en/* into your other locales with `pnpm translate`.","# The pre-commit hook runs it on commit; without a key it skips and untranslated","# locales fall back to the default locale. Get a key: https://openrouter.ai/keys","#OPENROUTER_API_KEY=");let a=[];return e.observability.includes("sentry")&&a.push({key:"SENTRY_DSN",comment:"# Overrides config.observability.sentry.dsn for the server only."}),e.observability.includes("posthog")&&a.push({key:"POSTHOG_API_KEY",comment:"# Overrides config.observability.posthog.publicKey for the server only (host stays in config)."}),a.length>0&&(o.push("","# Error and product reporting (optional)"),gt(a,o)),o.push("","# AI base (optional)","# Enable config.ai in packages/config. Built-in AI resolves through OpenRouter only,","# so OPENROUTER_API_KEY above is the only MODEL provider key the backend reads.","# BYOK is per-user and never an env var: users paste their own key in the app and it","# is stored encrypted in the database.","# Override the OpenRouter API base - a gateway or a proxy of your own. It must carry the","# /api/v1 suffix. Unset uses the hosted API.","#OPENROUTER_BASE_URL=https://openrouter.ai/api/v1","","# AI web tools (optional)","# Give the assistant its own web access. A key's presence is the whole switch: set one","# and the model gets its tools, set neither and chat is unchanged. Users still opt in","# per chat with the composer's web-search toggle, and config.ai.webSearch: false","# disables both. Calls bill to user credits - set the price and engine in","# config.ai.webTools.","# ANY one key alone covers both capabilities. Firecrawl is the recommended one, so a","# single signup is the whole setup, and with several keys set Firecrawl serves search","# and extract.","# Firecrawl (recommended) - `web_search` AND `web_extract` (reads one page).","# https://firecrawl.dev/","#FIRECRAWL_API_KEY=","# Parallel (optional) - both tools, cheaper on each, at high quality; extract is slower","# on a page it has not already indexed. Note its customer terms include a license to","# train on submitted queries. Set config.ai.webTools.searchEngine or .extractEngine to",'# "parallel" to use it alongside a Firecrawl key.',"# https://platform.parallel.ai/","#PARALLEL_API_KEY=","# TinyFish (optional) - both tools at a $0 list price, rate-limited instead (30 searches","# and 150 pages a minute per API key by default, shared by every user), so a call bills","# the user nothing unless you set a price in config.ai.webTools. Note its terms include","# a license to train on submitted content. Set config.ai.webTools.searchEngine or",'# .extractEngine to "tinyfish" to use it alongside another key.',"# https://agent.tinyfish.ai/","#TINYFISH_API_KEY="),e.desktop&&o.push("","# Desktop app (optional)","# Base URL the marketing download page reads the release feed from.","# Unset uses config.desktop.autoUpdate.url.","#DESKTOP_FEED_BASE_URL="),o.push("","# Build output (optional)","# Next's distDir for apps/web. Unset uses .next; the e2e harness sets its own so a test","# build never overwrites a dev server's.","#NEXT_DIST_DIR="),o.push(""),o.join(`
644
- `)}var _n="NEXT_PUBLIC_API_URL";function To(e){let{apiUrl:t}=On(e),r=["# API Configuration",`${_n}=${t}`];return e.architecture==="separate"&&r.push("","# Production (uncomment and replace with your deployed hostnames):",`# ${_n}=${Io}`),r.push(""),r.join(`
645
- `)}async function xn(e){let t=To(e);await l(`${e.projectDir}/.env`,t+`
646
- `+Tn(e,"env")),await l(`${e.projectDir}/.env.example`,t+`
647
- `+Tn(e,"example"))}import{join as _o}from"path";async function Dn(e){let t=ze(e),r=[],n=[];if(t.includes("postgres")&&(r.push(` postgres:
774
+ `;await c(t,P)}var zo={smtp:[{key:"SMTP_HOST",defaultValue:"localhost"},{key:"SMTP_PORT",defaultValue:"1025"},{key:"SMTP_USER"},{key:"SMTP_PASSWORD"}],ses:[{key:"AMAZON_SES_REGION",comment:"# TODO: Configure Amazon SES credentials (e.g. us-east-1)"},{key:"AMAZON_SES_KEY"},{key:"AMAZON_SES_SECRET"}],resend:[{key:"RESEND_API_KEY",comment:"# TODO: Add your Resend API key"}]},qo={stripe:[{key:"STRIPE_SECRET_KEY",comment:"# TODO: Add your Stripe keys"},{key:"STRIPE_WEBHOOK_SECRET"},{key:"STRIPE_API_BASE",comment:"# Point the Stripe SDK at another host - a local mock, or a proxy of your own. Unset means api.stripe.com."}],polar:[{key:"POLAR_ACCESS_TOKEN",comment:"# TODO: Add your Polar keys"},{key:"POLAR_WEBHOOK_SECRET"}]};function Wt(e,t){return t?e.map(r=>{let n=t[r.key];return n?{...r,defaultValue:n,comment:void 0}:r}):e}function Pt(e,t){for(let r of e)r.comment&&t.push(r.comment),r.defaultValue!==void 0?t.push(`${r.key}=${r.defaultValue}`):t.push(`#${r.key}=`)}function Dr(e){return Array.from(crypto.getRandomValues(new Uint8Array(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}function Nr(e){return e.architecture==="fullstack"?{apiUrl:"http://localhost:3000/api",baseUrl:"http://localhost:3000"}:{apiUrl:"http://localhost:3010",baseUrl:"http://localhost:3000"}}var Jo="https://your-app.example.com/api";function zt(e,t,r,n){e.push(n==="example"?`${t}=`:`${t}=${r}`)}function Kn(e,t){let r=t==="example"?void 0:e.credentials,{apiUrl:n,baseUrl:i}=Nr(e),o=[];e.architecture==="separate"?o.push("# API Configuration","# Standalone backend's own URL (the frontend reaches it via the public var above).",`API_URL=${n}`,`BASE_URL=${i}`):o.push("# App","# (API_URL is derived from the frontend's *_PUBLIC_API_URL by the runtime env schema.)",`BASE_URL=${i}`),o.push("","# Client IP (production)","# The backend rate-limits by client IP and stamps it on audit entries. YOU ALMOST","# CERTAINLY DO NOT NEED THIS: it detects Vercel, Fly.io, Render, Railway, Netlify and","# Coolify from their own env vars, and treats a private connection peer (Docker, compose,","# Kubernetes, Dokploy, an nginx/Traefik/Caddy on the same host) as proof that something is","# forwarding to it. A forwarded header is trusted only in those cases, never on a server","# exposed directly - there, a caller could have sent it themselves."),e.architecture==="fullstack"?o.push("# This build runs the API in the frontend's route handler, which has no connection","# address, so on a host the backend does not recognise there is nothing to fall back on","# and every caller shares ONE rate-limit bucket: the 10/min sign-in limit applies to","# your whole app rather than per person, and audit entries record no address."):o.push("# This build reads the connection peer, so a server exposed directly identifies its","# callers correctly with nothing set here."),o.push("# SET IT when nothing above can see your topology - the common case is a CDN in front of a","# directly exposed server, where connections arrive from a public CDN address:","# Cloudflare -> TRUSTED_PROXY=cf-connecting-ip","# another proxy in front -> TRUSTED_PROXY=1 (the number of proxies)","# Whatever you set wins over detection; `false` turns detection off and trusts nothing.","# TRUSTED_PROXY=1"),o.push("","# Auth rate limits","# Raises every Better Auth rate limit by a whole number (1-50). Unset = 1 = the","# shipped limits. No value disables them: 0, empty or unparseable fails at boot.","# You almost certainly want the default. The reason to raise it is an automated suite,","# which drives ONE address and so shares one bucket - and the e2e config already sets","# its own value, so it needs nothing here.","# AUTH_RATE_LIMIT_MULTIPLIER=1"),o.push("","# Database"),Pt(Wt(Le[e.databaseProvider].envVars,r),o),o.push("","# Cache"),Pt(Wt($e[e.cacheProvider].envVars,r),o),e.cacheProvider==="redis"&&o.push("# Optional prefix on every Redis key the app writes. Set it when several deployments share","# one Redis instance, or to keep a load test out of the live keyspace.","# Set it once, before first boot. Changing it on a running deployment orphans in-flight","# auto-top-up claims and distributed locks (autotopup:pending:*, withMutex) - drain or use","# a maintenance window first.","#REDIS_KEY_PREFIX="),o.push("","# Authentication"),t==="example"&&o.push("# Generate a strong secret, e.g. `openssl rand -hex 32`"),zt(o,"BETTER_AUTH_SECRET",crypto.randomUUID(),t),o.push("","# Content API (random secret; required when contentApi feature is enabled in @repo/config)"),zt(o,"CONTENT_API_KEY",Dr(32),t),o.push("","# Job Queue - Inngest","INNGEST_APP_ID=api"),zt(o,"INNGEST_EVENT_KEY",Dr(32),t),zt(o,"INNGEST_SIGNING_KEY",Dr(32),t),o.push("INNGEST_BASE_URL=http://127.0.0.1:8288"),e.architecture==="separate"&&(o.push("","# API Port (standalone backend)","API_PORT=3010"),o.push("","# CORS + cross-subdomain cookies (production only - leave unset for local dev)","# TRUSTED_ORIGINS=https://your-app.example.com","# AUTH_COOKIE_DOMAIN=.example.com"));let s=zo[e.emailProvider];if(s&&(o.push("","# Email"),Pt(Wt(s,r),o)),e.paymentProvider!=="none"){let d=qo[e.paymentProvider];d&&(o.push("","# Payment"),Pt(Wt(d,r),o))}o.push("","# Storage - S3 Compatible (required for uploads: avatars, files)","# Works with AWS S3, Cloudflare R2, MinIO, and other S3-compatible services.","# Uploads fail until these are set - storage is enabled in @repo/config by default.","#STORAGE_REGION=us-east-1","#STORAGE_ENDPOINT=https://your-account.r2.cloudflarestorage.com","#STORAGE_ACCESS_KEY_ID=","#STORAGE_SECRET_ACCESS_KEY=","# Two buckets: one publicly readable (avatars, assets), one credentials-only (private","# uploads served through the app). The bucket boundary is the privacy guarantee.","#STORAGE_PUBLIC_BUCKET=","#STORAGE_PRIVATE_BUCKET=",'# Public base URL for the public bucket. Public file URLs are STORAGE_PUBLIC_URL + "/" + key.',"#STORAGE_PUBLIC_URL=https://cdn.example.com","","# Storage - Local Filesystem (optional overrides)","# Defaults work out of the box; override for Docker volume mounts.","#STORAGE_LOCAL_PATH=/data","#STORAGE_LOCAL_PUBLIC_URL=https://cdn.example.com/files"),o.push("","# Admin Notifications (optional)","# Real-time notifications when business events occur (signups, purchases, etc.).","# Each provider is auto-enabled when its env vars are set.","#DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...","#SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...","#TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz","#TELEGRAM_CHAT_ID=-1001234567890","#TELEGRAM_TOPIC_ID=123"),o.push("","# Social sign-in (optional)","# Enable the matching provider in @repo/config, then set its pair here.","#GOOGLE_CLIENT_ID=","#GOOGLE_CLIENT_SECRET=","#GITHUB_CLIENT_ID=","#GITHUB_CLIENT_SECRET=","# Apple needs four values rather than a pair, and they also back the mobile app's native","# Sign in with Apple - the same client is what the backend accepts a native token from.","#APPLE_CLIENT_ID=","#APPLE_TEAM_ID=","#APPLE_KEY_ID=","#APPLE_PRIVATE_KEY="),o.push("","# SMS - Twilio (optional)","#TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","#TWILIO_AUTH_TOKEN=","#TWILIO_PHONE_NUMBER=+1234567890"),o.push("","# Newsletter (optional)","#RESEND_AUDIENCE_ID=","#LISTMONK_URL=https://listmonk.yourdomain.com","#LISTMONK_API_USER=admin","#LISTMONK_API_TOKEN=","#LISTMONK_LIST_ID=1"),o.push("","# Translations - OpenRouter (optional)","# Set to auto-translate en/* into your other locales with `pnpm translate`.","# The pre-commit hook runs it on commit; without a key it skips and untranslated","# locales fall back to the default locale. Get a key: https://openrouter.ai/keys","#OPENROUTER_API_KEY=");let a=[];return e.observability.includes("sentry")&&a.push({key:"SENTRY_DSN",comment:"# Overrides config.observability.sentry.dsn for the server only."}),e.observability.includes("posthog")&&a.push({key:"POSTHOG_API_KEY",comment:"# Overrides config.observability.posthog.publicKey for the server only (host stays in config)."}),a.length>0&&(o.push("","# Error and product reporting (optional)"),Pt(a,o)),o.push("","# AI base (optional)","# Enable config.ai in packages/config. Built-in AI resolves through OpenRouter only,","# so OPENROUTER_API_KEY above is the only MODEL provider key the backend reads.","# BYOK is per-user and never an env var: users paste their own key in the app and it","# is stored encrypted in the database.","# Override the OpenRouter API base - a gateway or a proxy of your own. It must carry the","# /api/v1 suffix. Unset uses the hosted API.","#OPENROUTER_BASE_URL=https://openrouter.ai/api/v1","","# AI web tools (optional)","# Give the assistant its own web access. A key's presence is the whole switch: set one","# and the model gets its tools, set neither and chat is unchanged. Users still opt in","# per chat with the composer's web-search toggle, and config.ai.webSearch: false","# disables both. Calls bill to user credits - set the price and engine in","# config.ai.webTools.","# ANY one key alone covers both capabilities. Firecrawl is the recommended one, so a","# single signup is the whole setup, and with several keys set Firecrawl serves search","# and extract.","# Firecrawl (recommended) - `web_search` AND `web_extract` (reads one page).","# https://firecrawl.dev/","#FIRECRAWL_API_KEY=","# Parallel (optional) - both tools, cheaper on each, at high quality; extract is slower","# on a page it has not already indexed. Note its customer terms include a license to","# train on submitted queries. Set config.ai.webTools.searchEngine or .extractEngine to",'# "parallel" to use it alongside a Firecrawl key.',"# https://platform.parallel.ai/","#PARALLEL_API_KEY=","# TinyFish (optional) - both tools at a $0 list price, rate-limited instead (30 searches","# and 150 pages a minute per API key by default, shared by every user), so a call bills","# the user nothing unless you set a price in config.ai.webTools. Note its terms include","# a license to train on submitted content. Set config.ai.webTools.searchEngine or",'# .extractEngine to "tinyfish" to use it alongside another key.',"# https://agent.tinyfish.ai/","#TINYFISH_API_KEY="),e.desktop&&o.push("","# Desktop app (optional)","# Base URL the marketing download page reads the release feed from.","# Unset uses config.desktop.autoUpdate.url.","#DESKTOP_FEED_BASE_URL="),e.mobile&&(o.push("","# Mobile app","# The bundle identifier the native Sign in with Apple token is issued to - it joins","# APPLE_CLIENT_ID in the audience the backend accepts.","#APPLE_APP_BUNDLE_IDENTIFIER=","# Comma-separated Android signing certificate SHA-256 fingerprints. They feed","# .well-known/assetlinks.json (which verifies your universal links) and the passkey","# Android origins.","#MOBILE_ANDROID_SHA256_FINGERPRINTS=","# Optional. An Expo access token turns on enhanced security for push sends.","#EXPO_ACCESS_TOKEN="),e.mobilePurchases==="revenuecat"&&o.push("","# Mobile store purchases - RevenueCat (server)","# The first two verify incoming webhooks; the third is read by the sync route and the","# nightly reconcile pass.","#REVENUECAT_WEBHOOK_SECRET=","#REVENUECAT_AUTH_HEADER=","#REVENUECAT_API_KEY=")),o.push("","# Build output (optional)","# Next's distDir for apps/web. Unset uses .next; the e2e harness sets its own so a test","# build never overwrites a dev server's.","#NEXT_DIST_DIR="),o.push(""),o.join(`
775
+ `)}var Hn="NEXT_PUBLIC_API_URL";function Xo(e){let{apiUrl:t}=Nr(e),r=["# API Configuration",`${Hn}=${t}`];return e.architecture==="separate"&&r.push("","# Production (uncomment and replace with your deployed hostnames):",`# ${Hn}=${Jo}`),r.push(""),r.join(`
776
+ `)}function Zo(e){let{apiUrl:t,baseUrl:r}=Nr(e),n=["# Expo reads env from THIS directory, never the monorepo root.","# Every EXPO_PUBLIC_* value is inlined into the app bundle - never put a secret here.","# Android emulator: replace localhost with 10.0.2.2; a physical device needs your machine's LAN IP","# EXPO_PUBLIC_API_URL is the value to fill first - without it the app throws at launch and names it.",`EXPO_PUBLIC_API_URL=${t}`,`EXPO_PUBLIC_WEB_URL=${r}`,"","# Native Google sign-in (optional)","#EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID=","#EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID="];e.mobilePurchases==="revenuecat"&&n.push("","# RevenueCat public SDK keys (optional until you create your products)","#EXPO_PUBLIC_REVENUECAT_IOS_KEY=","#EXPO_PUBLIC_REVENUECAT_ANDROID_KEY=");let i=[];return e.observability.includes("sentry")&&i.push("#EXPO_PUBLIC_SENTRY_DSN="),e.observability.includes("posthog")&&i.push("#EXPO_PUBLIC_POSTHOG_KEY="),i.length>0&&n.push("","# Observability (optional)",...i),n.push(""),n.join(`
777
+ `)}async function Yn(e){let t=Xo(e);if(await c(`${e.projectDir}/.env`,t+`
778
+ `+Kn(e,"env")),await c(`${e.projectDir}/.env.example`,t+`
779
+ `+Kn(e,"example")),e.mobile){let r=Zo(e);await c(`${e.projectDir}/apps/mobile/.env`,r),await c(`${e.projectDir}/apps/mobile/.env.example`,r)}}import{join as Qo}from"path";async function Wn(e){let t=Qe(e),r=[],n=[];if(t.includes("postgres")&&(r.push(` postgres:
648
780
  image: postgres:18-alpine
649
781
  restart: unless-stopped
650
782
  ports:
@@ -706,22 +838,22 @@ ${r.join(`
706
838
  volumes:
707
839
  ${n.join(`
708
840
  `)}
709
- `),await l(_o(e.projectDir,"infra/docker-compose.yml"),o)}import{join as Oo}from"path";var yt="Next.js";function xo(e){let t=[];if(t.push({type:"node-terminal",request:"launch",name:"Dev",command:"pnpm dev",cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),e.architecture==="separate"&&t.push({type:"node-terminal",request:"launch",name:"Backend",command:"pnpm dev",cwd:"${workspaceFolder}/apps/backend",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),t.push({type:"node-terminal",request:"launch",name:yt,command:"pnpm dev",cwd:"${workspaceFolder}/apps/web",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),e.docs&&t.push({type:"node-terminal",request:"launch",name:"Docs",command:"pnpm dev",cwd:"${workspaceFolder}/apps/docs",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),e.desktop){let r=e.architecture==="separate"?"http://localhost:3010":"http://localhost:3000/api";t.push({type:"node-terminal",request:"launch",name:"Desktop",command:"pnpm dev",cwd:"${workspaceFolder}/apps/desktop",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development",BASE_URL:"http://localhost:3000",PUBLIC_API_URL:r}})}if(t.push({type:"node-terminal",request:"launch",name:"Inngest",command:"pnpm dev:inngest",cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),t.push({type:"node-terminal",request:"launch",name:"Mail",command:"pnpm dev:mail",cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),e.paymentProvider==="stripe"){let r=e.architecture==="separate"?"localhost:3010/auth/stripe/webhook":"localhost:3000/api/auth/stripe/webhook";t.push({type:"node-terminal",request:"launch",name:"Stripe",command:`stripe listen --forward-to ${r}`,cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"]})}return t}function Do(e){let t=[];return e.architecture==="separate"?t.push({name:`Dev (${yt} + Backend + Inngest)`,configurations:["Backend",yt,"Inngest"]}):t.push({name:`Dev (${yt} + Inngest)`,configurations:[yt,"Inngest"]}),t}async function Nn(e){let t={version:"0.2.0",configurations:xo(e),compounds:Do(e)};await l(Oo(e.projectDir,".vscode/launch.json"),JSON.stringify(t,null," ")+`
710
- `)}import{readFile as Ln}from"fs/promises";import{join as $n}from"path";var br='import { drizzle } from "drizzle-orm/node-postgres";',kr=`export const db = drizzle(parsed.data.DATABASE_URL, { schema });
711
- export const pool = db.$client;`,No={postgres:{importLine:br,instantiation:kr},neon:{importLine:`import { Pool } from "@neondatabase/serverless";
841
+ `),await c(Qo(e.projectDir,"infra/docker-compose.yml"),o)}import{join as ea}from"path";var At="Next.js";function ta(e){let t=[];if(t.push({type:"node-terminal",request:"launch",name:"Dev",command:"pnpm dev",cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),e.architecture==="separate"&&t.push({type:"node-terminal",request:"launch",name:"Backend",command:"pnpm dev",cwd:"${workspaceFolder}/apps/backend",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),t.push({type:"node-terminal",request:"launch",name:At,command:"pnpm dev",cwd:"${workspaceFolder}/apps/web",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),e.docs&&t.push({type:"node-terminal",request:"launch",name:"Docs",command:"pnpm dev",cwd:"${workspaceFolder}/apps/docs",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),e.desktop){let r=e.architecture==="separate"?"http://localhost:3010":"http://localhost:3000/api";t.push({type:"node-terminal",request:"launch",name:"Desktop",command:"pnpm dev",cwd:"${workspaceFolder}/apps/desktop",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development",BASE_URL:"http://localhost:3000",PUBLIC_API_URL:r}})}if(t.push({type:"node-terminal",request:"launch",name:"Inngest",command:"pnpm dev:inngest",cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),t.push({type:"node-terminal",request:"launch",name:"Mail",command:"pnpm dev:mail",cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),e.paymentProvider==="stripe"){let r=e.architecture==="separate"?"localhost:3010/auth/stripe/webhook":"localhost:3000/api/auth/stripe/webhook";t.push({type:"node-terminal",request:"launch",name:"Stripe",command:`stripe listen --forward-to ${r}`,cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"]})}return t}function ra(e){let t=[];return e.architecture==="separate"?t.push({name:`Dev (${At} + Backend + Inngest)`,configurations:["Backend",At,"Inngest"]}):t.push({name:`Dev (${At} + Inngest)`,configurations:[At,"Inngest"]}),t}async function zn(e){let t={version:"0.2.0",configurations:ta(e),compounds:ra(e)};await c(ea(e.projectDir,".vscode/launch.json"),JSON.stringify(t,null," ")+`
842
+ `)}import{readFile as Jn}from"fs/promises";import{join as Xn}from"path";var Cr='import { drizzle } from "drizzle-orm/node-postgres";',Lr=`export const db = drizzle(parsed.data.DATABASE_URL, { schema });
843
+ export const pool = db.$client;`,na={postgres:{importLine:Cr,instantiation:Lr},neon:{importLine:`import { Pool } from "@neondatabase/serverless";
712
844
  import { drizzle } from "drizzle-orm/neon-serverless";`,instantiation:`const pool = new Pool({ connectionString: parsed.data.DATABASE_URL });
713
845
  export const db = drizzle(pool, { schema });`},supabase:{importLine:`import { drizzle } from "drizzle-orm/postgres-js";
714
846
  import postgres from "postgres";`,instantiation:`const client = postgres(parsed.data.DATABASE_URL);
715
- export const db = drizzle(client, { schema });`}};async function jn(e){let t=$n(e.projectDir,"packages/database/src/index.ts"),r=await Ln(t,"utf-8");if(!r.includes(br)||!r.includes(kr))throw new Error("generateDbDriver: boilerplate packages/database/src/index.ts is missing the node-postgres driver anchors (the import line or the `db` instantiation block); the boilerplate drifted.");let n=No[e.databaseProvider],i=r.replace(br,n.importLine).replace(kr,n.instantiation);await l(t,i),await Lo(e)}var Cn='import { Client } from "pg";',Er=` const client = new Client({ connectionString });
847
+ export const db = drizzle(client, { schema });`}};async function Zn(e){let t=Xn(e.projectDir,"packages/database/src/index.ts"),r=await Jn(t,"utf-8");if(!r.includes(Cr)||!r.includes(Lr))throw new Error("generateDbDriver: boilerplate packages/database/src/index.ts is missing the node-postgres driver anchors (the import line or the `db` instantiation block); the boilerplate drifted.");let n=na[e.databaseProvider],i=r.replace(Cr,n.importLine).replace(Lr,n.instantiation);await c(t,i),await sa(e)}var qn='import { Client } from "pg";',$r=` const client = new Client({ connectionString });
716
848
  await client.connect();
717
849
  return {
718
850
  query: async (text) => ({ rows: (await client.query<LiveColumnRow>(text)).rows }),
719
851
  end: () => client.end()
720
- };`,Co={postgres:null,neon:{importLine:'import { Client } from "@neondatabase/serverless";',open:Er},supabase:{importLine:'import postgres from "postgres";',open:` const sql = postgres(connectionString, { prepare: false });
852
+ };`,ia={postgres:null,neon:{importLine:'import { Client } from "@neondatabase/serverless";',open:$r},supabase:{importLine:'import postgres from "postgres";',open:` const sql = postgres(connectionString, { prepare: false });
721
853
  return {
722
854
  query: async (text) => ({ rows: await sql.unsafe(text) }),
723
855
  end: () => sql.end()
724
- };`}};async function Lo(e){let t=Co[e.databaseProvider];if(!t)return;let r=$n(e.projectDir,"packages/database/src/db-verify.ts"),n=await Ln(r,"utf-8");if(!n.includes(Cn)||!n.includes(Er))throw new Error("generateDbDriver: boilerplate packages/database/src/db-verify.ts is missing the node-postgres verifier anchors (the import line or the openVerifyClient body); the boilerplate drifted.");await l(r,n.replace(Cn,t.importLine).replace(Er,t.open))}import{join as Un}from"path";async function Mn(e){e.cacheProvider==="upstash"&&(await $o(e),await jo(e))}async function $o(e){await l(Un(e.projectDir,"packages/runtime/src/redis.ts"),`import { Redis } from "@upstash/redis";
856
+ };`}};async function sa(e){let t=ia[e.databaseProvider];if(!t)return;let r=Xn(e.projectDir,"packages/database/src/db-verify.ts"),n=await Jn(r,"utf-8");if(!n.includes(qn)||!n.includes($r))throw new Error("generateDbDriver: boilerplate packages/database/src/db-verify.ts is missing the node-postgres verifier anchors (the import line or the openVerifyClient body); the boilerplate drifted.");await c(r,n.replace(qn,t.importLine).replace($r,t.open))}import{join as Qn}from"path";async function ei(e){e.cacheProvider==="upstash"&&(await oa(e),await aa(e))}async function oa(e){await c(Qn(e.projectDir,"packages/runtime/src/redis.ts"),`import { Redis } from "@upstash/redis";
725
857
  import type { Store } from "hono-rate-limiter";
726
858
  import { env } from "./env";
727
859
 
@@ -891,7 +1023,7 @@ export function createLimiterStore(): Store {
891
1023
 
892
1024
  /** No persistent connection to close with Upstash REST. */
893
1025
  export async function closeRedis(): Promise<void> {}
894
- `)}async function jo(e){await l(Un(e.projectDir,"packages/runtime/src/mutex.ts"),`import { Lock } from "@upstash/lock";
1026
+ `)}async function aa(e){await c(Qn(e.projectDir,"packages/runtime/src/mutex.ts"),`import { Lock } from "@upstash/lock";
895
1027
  import { redis } from "./redis";
896
1028
 
897
1029
  export class MutexTimeoutError extends Error {
@@ -969,10 +1101,44 @@ export async function withMutex<T>(
969
1101
  });
970
1102
  }
971
1103
  }
972
- `)}import{readFile as Uo}from"fs/promises";import{join as Mo}from"path";var Fn=" REDIS_URL: z.string(),",Fo=` UPSTASH_REDIS_REST_URL: z.string(),
973
- UPSTASH_REDIS_REST_TOKEN: z.string(),`;async function Vn(e){let t=Mo(e.projectDir,"packages/runtime/src/env.ts"),r=await Uo(t,"utf-8");if(!r.includes(Fn))throw new Error("generateEnvSchema: boilerplate packages/runtime/src/env.ts is missing the REDIS_URL cache binding anchor; the boilerplate drifted.");let n=e.cacheProvider==="upstash"?r.replace(Fn,Fo):r;await l(t,n)}import{readFile as Bn}from"fs/promises";import{join as Q}from"path";var Kt={"@upstash/redis":"^1.37.0","@upstash/lock":"^0.2.1","@neondatabase/serverless":"^1.0.1",postgres:"^3.4.7"};async function it(e){let t=await Bn(e,"utf-8");return JSON.parse(t)}async function st(e,t){let r=/\n([\t ]+)"/.exec(await Bn(e,"utf-8"))?.[1]??" ";await l(e,JSON.stringify(t,null,r)+`
974
- `)}function Le(e,t){for(let r of t)delete e.dependencies?.[r],delete e.devDependencies?.[r]}function vt(e,t,r,n=!1){let i=n?"devDependencies":"dependencies";e[i]||(e[i]={}),e[i][t]=r}async function Kn(e){await Vo(e),await Bo(e),await Ko(e),await Yo(e),await Wo(e),await Xo(e)}async function Vo(e){let t=Q(e.projectDir,"packages/api/package.json"),r=await it(t);Le(r,["sharp","@types/sharp"]),e.cacheProvider==="upstash"&&Le(r,["ioredis"]),await st(t,r)}async function Bo(e){let t=Q(e.projectDir,"apps/web/package.json"),r=await it(t);Le(r,["@upstash/ratelimit","@upstash/redis"]),await st(t,r)}async function Ko(e){let t=Q(e.projectDir,"packages/runtime/package.json"),r=await it(t);e.cacheProvider==="upstash"&&(Le(r,["ioredis","rate-limit-redis","redis-semaphore"]),vt(r,"@upstash/redis",Kt["@upstash/redis"]),vt(r,"@upstash/lock",Kt["@upstash/lock"])),await st(t,r)}var Sr={sentry:["@sentry/node","@sentry/browser","@sentry/electron"],posthog:["posthog-node","posthog-js"]},Go=["@sentry/electron"],Ho=[...Sr.sentry,...Sr.posthog];async function Yo(e){let t=Q(e.projectDir,"packages/observability/package.json"),r=await it(t),n=new Set(e.observability.flatMap(i=>Sr[i]));if(!e.desktop)for(let i of Go)n.delete(i);Le(r,Ho.filter(i=>!n.has(i))),await st(t,r)}async function Wo(e){let t=Q(e.projectDir,"packages/database/package.json"),r=await it(t);e.databaseProvider==="neon"?(Le(r,["pg","@types/pg"]),vt(r,"@neondatabase/serverless",Kt["@neondatabase/serverless"])):e.databaseProvider==="supabase"&&(Le(r,["pg","@types/pg"]),vt(r,"postgres",Kt.postgres)),await st(t,r)}var qo=["app/.well-known/oauth-authorization-server/[[...path]]/route.ts","app/.well-known/oauth-protected-resource/[[...path]]/route.ts"];async function zo(e){let t=await it(e),r=t.dependencies?.["@repo/api"];r&&(delete t.dependencies?.["@repo/api"],vt(t,"@repo/api",r,!0),await st(e,t))}var Jo=["instrumentation.ts","tests/instrumentation.test.ts"];async function Xo(e){if(e.architecture!=="separate")return;let t=Q(e.projectDir,"apps/web");await Mt(Q(t,"app/api/[[...rest]]/route.ts"),t);for(let r of qo)await Mt(Q(t,r),t);for(let r of Jo)await Mt(Q(t,r),t);await zo(Q(t,"package.json"))}import{readFile as Zo}from"fs/promises";import{join as Qo}from"path";var ea=["base.json","node.json","next.json"],Gn="GenerateSaaS ";async function Hn(e){if(!e.demo)for(let t of ea){let r=Qo(e.projectDir,"tooling/typescript",t),n;try{n=await Zo(r,"utf-8")}catch{continue}let i=JSON.parse(n);typeof i.display!="string"||!i.display.startsWith(Gn)||(i.display=i.display.slice(Gn.length),await l(r,JSON.stringify(i,null," ")+`
975
- `))}}import{readFile as Wn,rm as ta}from"fs/promises";import{join as wt}from"path";async function qn(e){e.cacheProvider==="upstash"&&(await Promise.all([Yn(wt(e.projectDir,"packages/runtime/tests/setup.ts")),Yn(wt(e.projectDir,"packages/mail/tests/setup.ts")),na(e.projectDir),ta(wt(e.projectDir,"packages/runtime/tests/redis.test.ts"),{force:!0})]),await ra(e.projectDir))}async function ra(e){await l(wt(e,"packages/runtime/tests/redis.test.ts"),`import { beforeEach, describe, expect, it, vi } from "vitest";
1104
+ `)}import{readFile as ca}from"fs/promises";import{join as la}from"path";var ti=" REDIS_URL: z.string(),",pa=` UPSTASH_REDIS_REST_URL: z.string(),
1105
+ UPSTASH_REDIS_REST_TOKEN: z.string(),`;async function ri(e){let t=la(e.projectDir,"packages/runtime/src/env.ts"),r=await ca(t,"utf-8");if(!r.includes(ti))throw new Error("generateEnvSchema: boilerplate packages/runtime/src/env.ts is missing the REDIS_URL cache binding anchor; the boilerplate drifted.");let n=e.cacheProvider==="upstash"?r.replace(ti,pa):r;await c(t,n)}import{readFile as ci}from"fs/promises";import{join as V}from"path";import{readFile as ii,rm as ga}from"fs/promises";import{join as lt}from"path";import{readFile as Rt,rm as da}from"fs/promises";import{join as It}from"path";function ua(e){return new RegExp(`^[ \\t]*(?:\\/\\/|\\{\\/\\*|\\*) gsaas:${e}-start(?: \\*\\/\\})?[ \\t]*\\r?\\n[\\s\\S]*?^[ \\t]*(?:\\/\\/|\\{\\/\\*|\\*) gsaas:${e}-end(?: \\*\\/\\})?[ \\t]*\\r?\\n`,"gm")}function Y(e,t,r,n){let i=ua(r);if(e.match(i)===null)throw new Error(`${n}: expected gsaas ${r} markers in ${t}, but found none (boilerplate drift).`);let o=e.replace(i,"");if(o.includes(`gsaas:${r}-start`)||o.includes(`gsaas:${r}-end`))throw new Error(`${n}: an unbalanced gsaas ${r} marker is left in ${t} (boilerplate drift).`);return o.replace(/\n{3,}/g,`
1106
+
1107
+ `).replace(/,(\r?\n[ \t]*[\]})])/g,"$1")}async function E(e,t,r,n,i){let o=await Rt(e,"utf-8"),s=o.indexOf(t);if(s===-1)throw new Error(`${i}: expected to find the ${n} seam, but it was missing (boilerplate drift).`);if(o.indexOf(t,s+t.length)!==-1)throw new Error(`${i}: the ${n} seam appears more than once (boilerplate drift).`);await c(e,o.slice(0,s)+r+o.slice(s+t.length))}async function Ee(e,t,r,n){let i=/\n([\t ]+)"/.exec(await Rt(e,"utf-8"))?.[1];if(i!==r){let o=s=>s===" "?"tab":`${s.length}-space`;throw new Error(`${n}: expected ${e} to be ${o(r)}-indented, but it is ${i===void 0?"not indented":o(i)}-indented (boilerplate drift). Re-serializing it with the wrong indent would reformat the whole file.`)}await c(e,`${JSON.stringify(t,null,r)}
1108
+ `)}async function ma(e,t,r){let n=It(e,"ai.mdx"),o=(await Rt(n,"utf-8")).split(`
1109
+ `).find(s=>s.includes(`href="${t}"`));if(o===void 0)throw new Error(`${r}: expected the ai.mdx card linking to ${t} (boilerplate drift).`);await E(n,`${o}
1110
+ `,"",`ai.mdx ${t} card`,r)}async function ha(e,t,r){let n=It(e,"ai.mdx"),i=`| **${t}** |`,s=(await Rt(n,"utf-8")).split(`
1111
+ `).find(a=>a.startsWith(i));if(s===void 0)throw new Error(`${r}: expected the ai.mdx "${t}" hub-table row (boilerplate drift).`);await E(n,`${s}
1112
+ `,"",`ai.mdx ${t} hub-table row`,r)}async function ni(e,t,r,n,i){let o=It(e,"docs/(platform)");await da(It(o,t),{recursive:!0}),await E(It(o,"meta.json"),` "${t}",
1113
+ `,"",`docs meta.json ${t} nav entry`,n);for(let s of r)await ma(o,s,n);i!==void 0&&await ha(o,i,n)}function fa(e){let t="",r=0;for(;r<e.length;){let n=e[r],i=e[r+1];if(n==="/"&&i==="/"){for(;r<e.length&&e[r]!==`
1114
+ `;)r++;continue}if(n==="/"&&i==="*"){for(r+=2;r<e.length&&!(e[r]==="*"&&e[r+1]==="/");)e[r]===`
1115
+ `&&(t+=`
1116
+ `),r++;r+=2;continue}if(n==='"'||n==="'"||n==="`"){let o=n;for(r++;r<e.length&&e[r]!==o;)e[r]==="\\"?r++:e[r]===`
1117
+ `&&(t+=`
1118
+ `),r++;r++;continue}t+=n,r++}return t}async function qt(e){let t=`import { config } from "@repo/config";
1119
+ `,r=await Rt(e,"utf-8");if(!r.includes(t))return;let n=r.replace(t,""),i=fa(n);/\bconfig\b/.test(i)||await c(e,n)}var pt="stripMobileObservability",si=["apps/mobile/lib/observability.ts","apps/mobile/types/env.d.ts","apps/mobile/tests/settings-privacy-screen.test.tsx"],oi="apps/mobile/tests/observability.test.ts",ya="mobile-obs-any",ba=["apps/mobile/tests/settings-privacy-screen.test.tsx"],Jt={sentry:["@sentry/react-native"],posthog:["posthog-react-native"]},wa=`,
1120
+ "@sentry/react-native/expo"`,va="|@sentry/react-native",ka=` * DUAL-PROVIDER, the way \`packages/observability\` is. This repo carries the Sentry and the PostHog
1121
+ * halves at once so dev and the test suite exercise both. A generated project carries only the
1122
+ * providers its \`observability\` manifest option names: the CLI removes the other one's marked region
1123
+ * from this file, its SDK from \`apps/mobile/package.json\` and, for Sentry, its Expo config plugin
1124
+ * from \`app.config.ts\` - the same "an unselected provider costs nothing" contract the server, the
1125
+ * browser and the desktop app already keep.
1126
+ *
1127
+ * THE RULE THAT KEEPS THAT WORKING: a surviving region may never reference a stripped one. Every
1128
+ * cross-provider \`{@link}\` and every paragraph about one provider's credentials sits INSIDE that
1129
+ * provider's own region, and what is left between them is provider-neutral - the {@link PROVIDERS}
1130
+ * list and the two exported entry points that walk it, which is what a project generated with
1131
+ * \`observability: []\` keeps and starts nothing with.`,Ea='import { readPrivacySettings, type PrivacySettings } from "@/lib/privacy";',Sa='import type { PrivacySettings } from "@/lib/privacy";';function Pa(e){let t=e.map(n=>n==="sentry"?"Sentry":"PostHog").join(" and ");return`${e.length===0?` * Your project was generated with observability OFF, so this app received no reporting adapter
1132
+ * and no reporting SDK: {@link PROVIDERS} is empty, and both functions below are no-ops that load
1133
+ * nothing and send nothing.`:` * This app reports to ${t}. No other provider's start, SDK or Expo config plugin was
1134
+ * generated, so there is nothing here for Metro to fold away and nothing for pnpm to install.`}
1135
+ *
1136
+ * To change providers, edit \`observability\` in .generatesaas/manifest.json and run
1137
+ * \`generatesaas update\`: re-shaping brings that provider's half of this file, its SDK and its env
1138
+ * key back. Pasting the other provider's credential into \`config.observability\` on its own cannot -
1139
+ * the start it would need is not here.`}async function ai(e){if(!e.mobile)return;let t=e.projectDir,r=["sentry","posthog"].filter(o=>!e.observability.includes(o));e.observability.length===0&&await ga(lt(t,oi),{force:!0});let n=e.observability.length===0?si:[...si,oi];for(let o of r)for(let s of n){let a=lt(t,s);await c(a,Y(await ii(a,"utf-8"),s,`mobile-obs-${o}`,pt))}if(e.observability.length===0)for(let o of ba){let s=lt(t,o);await c(s,Y(await ii(s,"utf-8"),o,ya,pt))}r.includes("sentry")&&(await E(lt(t,"apps/mobile/app.config.ts"),wa,"","app.config.ts Sentry Expo plugin",pt),await E(lt(t,"apps/mobile/jest.config.js"),va,"","jest.config.js Sentry transform allowance",pt));let i=lt(t,"apps/mobile/lib/observability.ts");await E(i,ka,Pa(e.observability),"lib/observability.ts dual-provider note",pt),e.observability.length===0&&(await E(i,Ea,Sa,"lib/observability.ts privacy import",pt),await qt(i))}var Xt={"@upstash/redis":"^1.37.0","@upstash/lock":"^0.2.1","@neondatabase/serverless":"^1.0.1",postgres:"^3.4.7"};async function ae(e){let t=await ci(e,"utf-8");return JSON.parse(t)}async function ce(e,t){let r=/\n([\t ]+)"/.exec(await ci(e,"utf-8"))?.[1]??" ";await c(e,JSON.stringify(t,null,r)+`
1140
+ `)}function ee(e,t){for(let r of t)delete e.dependencies?.[r],delete e.devDependencies?.[r]}function Aa(e){e.dependencies&&Object.keys(e.dependencies).length===0&&delete e.dependencies,e.devDependencies&&Object.keys(e.devDependencies).length===0&&delete e.devDependencies}function Tt(e,t,r,n=!1){let i=n?"devDependencies":"dependencies";e[i]||(e[i]={}),e[i][t]=r}async function li(e){await Ra(e),await Ta(e),await _a(e),await Na(e),await Ca(e),await Ma(e),await Ia(e)}async function Ia(e){if(!e.mobile){let t=V(e.projectDir,"packages/auth/package.json"),r=await ae(t);ee(r,["@better-auth/expo"]),await ce(t,r);let n=V(e.projectDir,"tooling/styles/package.json"),i=await ae(n);ee(i,["tsx"]),Aa(i),await ce(n,i)}if(e.mobile&&e.mobilePurchases!=="revenuecat"){let t=V(e.projectDir,"apps/mobile/package.json"),r=await ae(t);ee(r,["react-native-purchases","react-native-purchases-ui"]),await ce(t,r)}}async function Ra(e){let t=V(e.projectDir,"packages/api/package.json"),r=await ae(t);ee(r,["sharp","@types/sharp"]),e.cacheProvider==="upstash"&&ee(r,["ioredis"]),await ce(t,r)}async function Ta(e){let t=V(e.projectDir,"apps/web/package.json"),r=await ae(t);ee(r,["@upstash/ratelimit","@upstash/redis"]),await ce(t,r)}async function _a(e){let t=V(e.projectDir,"packages/runtime/package.json"),r=await ae(t);e.cacheProvider==="upstash"&&(ee(r,["ioredis","rate-limit-redis","redis-semaphore"]),Tt(r,"@upstash/redis",Xt["@upstash/redis"]),Tt(r,"@upstash/lock",Xt["@upstash/lock"])),await ce(t,r)}var jr={sentry:["@sentry/node","@sentry/browser","@sentry/electron"],posthog:["posthog-node","posthog-js"]},Oa=["@sentry/electron"],xa=[...jr.sentry,...jr.posthog],Da=[...Jt.sentry,...Jt.posthog];async function Na(e){let t=V(e.projectDir,"packages/observability/package.json"),r=await ae(t),n=new Set(e.observability.flatMap(a=>jr[a]));if(!e.desktop)for(let a of Oa)n.delete(a);if(ee(r,xa.filter(a=>!n.has(a))),await ce(t,r),!e.mobile)return;let i=V(e.projectDir,"apps/mobile/package.json"),o=await ae(i),s=new Set(e.observability.flatMap(a=>Jt[a]));ee(o,Da.filter(a=>!s.has(a))),await ce(i,o)}async function Ca(e){let t=V(e.projectDir,"packages/database/package.json"),r=await ae(t);e.databaseProvider==="neon"?(ee(r,["pg","@types/pg"]),Tt(r,"@neondatabase/serverless",Xt["@neondatabase/serverless"])):e.databaseProvider==="supabase"&&(ee(r,["pg","@types/pg"]),Tt(r,"postgres",Xt.postgres)),await ce(t,r)}var La=["app/.well-known/oauth-authorization-server/[[...path]]/route.ts","app/.well-known/oauth-protected-resource/[[...path]]/route.ts"];async function $a(e){let t=await ae(e),r=t.dependencies?.["@repo/api"];r&&(delete t.dependencies?.["@repo/api"],Tt(t,"@repo/api",r,!0),await ce(e,t))}var ja=["instrumentation.ts","tests/instrumentation.test.ts"];async function Ma(e){if(e.architecture!=="separate")return;let t=V(e.projectDir,"apps/web");await at(V(t,"app/api/[[...rest]]/route.ts"),t);for(let r of La)await at(V(t,r),t);for(let r of ja)await at(V(t,r),t);await $a(V(t,"package.json"))}import{readFile as Ua}from"fs/promises";import{join as Fa}from"path";var Va=["base.json","node.json","next.json"],pi="GenerateSaaS ";async function di(e){if(!e.demo)for(let t of Va){let r=Fa(e.projectDir,"tooling/typescript",t),n;try{n=await Ua(r,"utf-8")}catch{continue}let i=JSON.parse(n);typeof i.display!="string"||!i.display.startsWith(pi)||(i.display=i.display.slice(pi.length),await c(r,JSON.stringify(i,null," ")+`
1141
+ `))}}import{readFile as mi,rm as Ba}from"fs/promises";import{join as _t}from"path";async function hi(e){e.cacheProvider==="upstash"&&(await Promise.all([ui(_t(e.projectDir,"packages/runtime/tests/setup.ts")),ui(_t(e.projectDir,"packages/mail/tests/setup.ts")),Ka(e.projectDir),Ba(_t(e.projectDir,"packages/runtime/tests/redis.test.ts"),{force:!0})]),await Ga(e.projectDir))}async function Ga(e){await c(_t(e,"packages/runtime/tests/redis.test.ts"),`import { beforeEach, describe, expect, it, vi } from "vitest";
976
1142
 
977
1143
  /**
978
1144
  * Regression guards for the Upstash-backed cache layer in src/redis.ts.
@@ -1094,9 +1260,9 @@ describe("cacheScan", () => {
1094
1260
  expect(seen.sort()).toEqual(["scan:1", "scan:2", "scan:3"]);
1095
1261
  });
1096
1262
  });
1097
- `)}async function Yn(e){let t;try{t=await Wn(e,"utf-8")}catch{throw new Error(`pruneCacheTests: expected ${e} for the Upstash cache swap (boilerplate drift).`)}let r=t.replace(/\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1263
+ `)}async function ui(e){let t;try{t=await mi(e,"utf-8")}catch{throw new Error(`pruneCacheTests: expected ${e} for the Upstash cache swap (boilerplate drift).`)}let r=t.replace(/\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1098
1264
  UPSTASH_REDIS_REST_TOKEN: "test-token",
1099
- `);if(r===t){if(t.includes("UPSTASH_REDIS_REST_URL"))return;throw new Error(`pruneCacheTests: found no REDIS_URL default to swap in ${e} (boilerplate drift).`)}await l(e,r)}async function na(e){let t=wt(e,"packages/api/tests/setup.ts"),r;try{r=await Wn(t,"utf-8")}catch{throw new Error(`pruneCacheTests: expected ${t} for the Upstash cache swap (boilerplate drift).`)}let n=r;if(n=n.replace(/\tREDIS_URL:\s*"[^"]*",?\n/g,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1265
+ `);if(r===t){if(t.includes("UPSTASH_REDIS_REST_URL"))return;throw new Error(`pruneCacheTests: found no REDIS_URL default to swap in ${e} (boilerplate drift).`)}await c(e,r)}async function Ka(e){let t=_t(e,"packages/api/tests/setup.ts"),r;try{r=await mi(t,"utf-8")}catch{throw new Error(`pruneCacheTests: expected ${t} for the Upstash cache swap (boilerplate drift).`)}let n=r;if(n=n.replace(/\tREDIS_URL:\s*"[^"]*",?\n/g,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1100
1266
  UPSTASH_REDIS_REST_TOKEN: "test-token",
1101
1267
  `),n=n.replace(/vi\.mock\("ioredis"[\s\S]*?\n\}\);\n\n?/,""),n=n.replace(/vi\.mock\("rate-limit-redis"[\s\S]*?\n\}\);\n\n?/,""),n=n.replace(/\t\t\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1102
1268
  UPSTASH_REDIS_REST_TOKEN: "test-token",
@@ -1125,33 +1291,34 @@ vi.mock("@upstash/lock", () => {
1125
1291
  return { Lock };
1126
1292
  });
1127
1293
 
1128
- $1`),n===r){if(r.includes("UPSTASH_REDIS_REST_URL"))return;throw new Error(`pruneCacheTests: nothing to rewrite in ${t} for the Upstash swap (boilerplate drift).`)}for(let o of['vi.mock("ioredis"','vi.mock("rate-limit-redis"',"REDIS_URL:"])if(n.includes(o))throw new Error(`pruneCacheTests: ${t} still contains ${o} after the Upstash swap (boilerplate drift).`);await l(t,n)}import{readFile as ia}from"fs/promises";var sa=/^[ \t]*(?:\/\/|\{\/\*|\*) gsaas:[a-z0-9-]+-(?:start|end)(?: \*\/\})?[ \t]*\r?\n/gm,oa=new Set([".ts",".tsx",".vue",".mjs",".js",".jsx"]),zn=32;function aa(e){let t=e.lastIndexOf(".");return t>=0&&oa.has(e.slice(t))}async function ca(e){let t=await ia(e,"utf-8");if(!t.includes("gsaas:"))return;let r=t.replace(sa,"");r!==t&&await l(e,r)}async function Jn(e){let t=await de(e.projectDir,e.projectDir,r=>r==="node_modules"||r.startsWith("node_modules/")||r===".git"||r.startsWith(".git/"),aa);for(let r=0;r<t.length;r+=zn)await Promise.all(t.slice(r,r+zn).map(ca))}import{readdir as ti,readFile as je,rm as ee}from"fs/promises";import{join as S}from"path";import{readFile as kt,rm as la}from"fs/promises";import{join as bt}from"path";function pa(e){return new RegExp(`^[ \\t]*(?:\\/\\/|\\{\\/\\*|\\*) gsaas:${e}-start(?: \\*\\/\\})?[ \\t]*\\r?\\n[\\s\\S]*?^[ \\t]*(?:\\/\\/|\\{\\/\\*|\\*) gsaas:${e}-end(?: \\*\\/\\})?[ \\t]*\\r?\\n`,"gm")}function Ee(e,t,r,n){let i=pa(r);if(e.match(i)===null)throw new Error(`${n}: expected gsaas ${r} markers in ${t}, but found none (boilerplate drift).`);let o=e.replace(i,"");if(o.includes(`gsaas:${r}-start`)||o.includes(`gsaas:${r}-end`))throw new Error(`${n}: an unbalanced gsaas ${r} marker is left in ${t} (boilerplate drift).`);return o.replace(/\n{3,}/g,`
1129
-
1130
- `).replace(/,(\r?\n[ \t]*[\]})])/g,"$1")}async function H(e,t,r,n,i){let o=await kt(e,"utf-8"),s=o.indexOf(t);if(s===-1)throw new Error(`${i}: expected to find the ${n} seam, but it was missing (boilerplate drift).`);if(o.indexOf(t,s+t.length)!==-1)throw new Error(`${i}: the ${n} seam appears more than once (boilerplate drift).`);await l(e,o.slice(0,s)+r+o.slice(s+t.length))}async function $e(e,t,r,n){let i=/\n([\t ]+)"/.exec(await kt(e,"utf-8"))?.[1];if(i!==r){let o=s=>s===" "?"tab":`${s.length}-space`;throw new Error(`${n}: expected ${e} to be ${o(r)}-indented, but it is ${i===void 0?"not indented":o(i)}-indented (boilerplate drift). Re-serializing it with the wrong indent would reformat the whole file.`)}await l(e,`${JSON.stringify(t,null,r)}
1131
- `)}async function da(e,t,r){let n=bt(e,"ai.mdx"),o=(await kt(n,"utf-8")).split(`
1132
- `).find(s=>s.includes(`href="${t}"`));if(o===void 0)throw new Error(`${r}: expected the ai.mdx card linking to ${t} (boilerplate drift).`);await H(n,`${o}
1133
- `,"",`ai.mdx ${t} card`,r)}async function ua(e,t,r){let n=bt(e,"ai.mdx"),i=`| **${t}** |`,s=(await kt(n,"utf-8")).split(`
1134
- `).find(a=>a.startsWith(i));if(s===void 0)throw new Error(`${r}: expected the ai.mdx "${t}" hub-table row (boilerplate drift).`);await H(n,`${s}
1135
- `,"",`ai.mdx ${t} hub-table row`,r)}async function Xn(e,t,r,n,i){let o=bt(e,"docs/(platform)");await la(bt(o,t),{recursive:!0}),await H(bt(o,"meta.json"),` "${t}",
1136
- `,"",`docs meta.json ${t} nav entry`,n);for(let s of r)await da(o,s,n);i!==void 0&&await ua(o,i,n)}function ma(e){let t="",r=0;for(;r<e.length;){let n=e[r],i=e[r+1];if(n==="/"&&i==="/"){for(;r<e.length&&e[r]!==`
1137
- `;)r++;continue}if(n==="/"&&i==="*"){for(r+=2;r<e.length&&!(e[r]==="*"&&e[r+1]==="/");)e[r]===`
1138
- `&&(t+=`
1139
- `),r++;r+=2;continue}if(n==='"'||n==="'"||n==="`"){let o=n;for(r++;r<e.length&&e[r]!==o;)e[r]==="\\"?r++:e[r]===`
1140
- `&&(t+=`
1141
- `),r++;r++;continue}t+=n,r++}return t}async function Zn(e){let t=`import { config } from "@repo/config";
1142
- `,r=await kt(e,"utf-8");if(!r.includes(t))return;let n=r.replace(t,""),i=ma(n);/\bconfig\b/.test(i)||await l(e,n)}import{readFile as ei,rm as A}from"fs/promises";import{join as E}from"path";import{readFile as Et}from"fs/promises";import{join as St}from"path";async function Qn(e,t){let r=St(e,"packages/api/src/routes/internal/index.ts");await l(r,Ee(await Et(r,"utf-8"),"internal router","runner",t)),await Zn(r);let n=St(e,"packages/api/src/ai/automation-runner.ts");await l(n,Ee(await Et(n,"utf-8"),"automation-runner.ts","runner",t));for(let s of["packages/api/src/index.ts","packages/api/src/routes/internal/ai/chat-router.ts","packages/api/src/routes/internal/ai/automations-router.ts","packages/api/src/ai/byok.ts","packages/api/tests/ai/automations.test.ts","packages/api/tests/ai/automations-runs-route.test.ts","packages/api/tests/routes/ai.test.ts","packages/api/tests/ai/summarize-cap.test.ts"]){let a=St(e,s);await l(a,Ee(await Et(a,"utf-8"),s,"runner",t))}let i=St(e,"packages/api/src/ai/settings-store.ts");await l(i,Ee(await Et(i,"utf-8"),"settings-store.ts","runner",t));let o=St(e,"packages/api/tests/ai/settings-store.test.ts");await l(o,Ee(await Et(o,"utf-8"),"settings-store.test.ts","runner",t))}async function Ar(e){if(e.runner)return;let t=e.projectDir,r="stripRunner";await A(E(t,"packages/api/src/relay"),{recursive:!0}),await A(E(t,"packages/api/src/runner"),{recursive:!0}),await A(E(t,"packages/api/src/ai/runner-model.ts")),await A(E(t,"packages/api/src/ai/runner-routing.ts")),await A(E(t,"packages/api/tests/relay"),{recursive:!0}),await A(E(t,"packages/api/tests/runner"),{recursive:!0}),await A(E(t,"packages/api/src/routes/internal/runner.ts")),await A(E(t,"packages/api/src/routes/internal/runner-transport.ts")),await A(E(t,"packages/api/src/routes/internal/runner-limits.ts")),await A(E(t,"packages/api/tests/routes/runner.test.ts")),await A(E(t,"packages/api/tests/routes/runner-transport-harness.ts")),await A(E(t,"packages/api/tests/routes/runner-transport-connect.test.ts")),await A(E(t,"packages/api/tests/routes/runner-transport-events.test.ts")),await A(E(t,"packages/api/tests/ai/automation-coalesce.test.ts")),await A(E(t,"packages/api/tests/ai/automations-lazy-expiry.test.ts")),await A(E(t,"packages/api/tests/ai/runner-model.test.ts")),await A(E(t,"packages/api/tests/unit/runner-offline-routing.test.ts")),await A(E(t,"packages/api/tests/unit/runner-offline-interactive.test.ts")),await A(E(t,"packages/api/tests/unit/runner-unavailable-routing.test.ts")),await A(E(t,"packages/api/tests/unit/runner-unavailable-interactive.test.ts")),await A(E(t,"packages/api/tests/unit/runner-arm-harness.ts")),await A(E(t,"packages/api/tests/ai/runner-chat.test.ts")),await A(E(t,"packages/api/tests/helpers/runner-protocol.ts")),await A(E(t,"packages/api/tests/helpers/runner-frames.ts")),await A(E(t,"packages/api/tests/helpers/relay-runtime-mock.ts"));let n=E(t,"packages/api/package.json"),i=JSON.parse(await ei(n,"utf-8"));if(!i.exports?.["./runner"])throw new Error(`${r}: expected packages/api/package.json to carry the ./runner export`);delete i.exports["./runner"],await $e(n,i," ",r),await Qn(t,r),await Xn(t,"runner",["/runner"],r,"Runners"),await H(E(t,"docs/(platform)/ai.mdx"),"Runs on a paired [runner](/runner) CLI are never metered either - the user's own subscription executes them. ","","ai.mdx runner metering sentence",r),await H(E(t,"docs/(platform)/caching.mdx")," - see [per-runner request budgets](/runner/install#per-runner-request-budgets)","","caching.mdx runner budgets link",r);let o=E(t,"docs/(platform)/configuration.mdx"),s="| `runner.enabled` |",a=(await ei(o,"utf-8")).split(`
1143
- `).find(p=>p.startsWith(s));if(a===void 0)throw new Error(`${r}: expected the configuration.mdx \`runner.enabled\` row (boilerplate drift).`);await H(o,`${a}
1144
- `,"","configuration.mdx runner config-table row",r)}var fa=new Set(["ci.yml","desktop-release.yml"]),ha=["apps/web/app/[locale]/(marketing)/download","apps/web/tests/routes/download-page.test.tsx"],ga=["cli","cli:clean","demo:bench","playground:regen","playground:test","playground:test:repo","playground:test:audit","playground:test:units","playground:test:e2e","playground:test:build","check-types:scripts","ai:parity","runner:export","runner:verify","runner:publish"];async function ri(e){let t=S(e.projectDir,".github/workflows"),r=await ti(t).catch(()=>[]);for(let n of r)fa.has(n)||await ee(S(t,n),{recursive:!0,force:!0})}async function ya(e,t){let r=S(e,"package.json"),n=JSON.parse(await je(r,"utf-8"));if(!n.scripts)return;let i=!1;for(let s of t)s in n.scripts&&(delete n.scripts[s],i=!0);if(!i)return;let o=/\n([\t ]+)"/.exec(await je(r,"utf-8"))?.[1]??" ";await l(r,JSON.stringify(n,null,o)+`
1145
- `)}async function ni(e){await ya(e.projectDir,[...ga])}async function ii(e){let t=S(e.projectDir,"package.json"),r=await je(t,"utf-8"),n=JSON.parse(r);!n.scripts||!("dev"in n.scripts)||(n.scripts.dev=e.architecture==="separate"?"turbo run dev --continue":"turbo run dev --continue --filter=!backend",await l(t,JSON.stringify(n,null," ")+`
1146
- `))}async function si(e){let t=S(e.projectDir,"package.json"),r=JSON.parse(await je(t,"utf-8")),n=r.scripts?.test;if(n!=="turbo run test --concurrency=4 && vitest run")throw new Error(`rewriteRootTestScript: expected the root test script to be "turbo run test --concurrency=4 && vitest run", got ${JSON.stringify(n)} (boilerplate drift).`);r.scripts.test="turbo run test --concurrency=4",await l(t,JSON.stringify(r,null," ")+`
1147
- `)}async function oi(e){e.docs||await ee(S(e.projectDir,"apps/docs"),{recursive:!0})}async function ai(e){let t=S(e.projectDir,"docs/(platform)/meta.json"),r=JSON.parse(await je(t,"utf-8"));if(r.title!=="GenerateSaaS")throw new Error(`stripUnusedDocsSilo: expected the feature-page group to be titled "GenerateSaaS", found ${JSON.stringify(r.title)} (boilerplate drift).`);r.title=e.appName,await $e(t,r," ","stripUnusedDocsSilo"),e.desktop||await ee(S(e.projectDir,"docs/desktop"),{recursive:!0,force:!0}),await va(e)}async function ue(e,t,r,n){await l(e,Ee(await je(e,"utf-8"),t,r,n))}async function va(e){let t=e.projectDir,r="stripUnusedDocsSilo",n=S(t,"docs/(platform)/configuration.mdx"),i=e.desktop,o=e.desktop&&e.ai;e.desktop||(await H(n,"| Desktop app + device sign-in - see [desktop app](/desktop) |","| Desktop app + device sign-in |","configuration.mdx desktop config-table link",r),await ue(n,"configuration.mdx desktop TRUSTED_ORIGINS callout","desktop-origins-docs",r),await H(S(t,"docs/cli/init.mdx"),"the [desktop app](/desktop), AI features","the desktop app, AI features","cli init.mdx desktop app link",r),await ue(S(t,"docs/(platform)/payments/organizations.mdx"),"payments organizations.mdx desktop tenancy callout","desktop-tenancy-docs",r),await ue(S(t,"docs/(platform)/projects.mdx"),"projects.mdx desktop scoping callout","desktop-projects-docs",r),await ue(S(t,"docs/(platform)/client-architecture.mdx"),"client-architecture.mdx desktop card","desktop-architecture-docs",r),await ue(S(t,"docs/(platform)/testing/e2e.mdx"),"testing/e2e.mdx desktop e2e callout","desktop-testing-docs",r),await H(S(t,"docs/(platform)/index.mdx"),` <Card href="/desktop" title="Desktop app" description="Cross-platform desktop app with device sign-in and AI agents." />
1148
- `,"","index.mdx desktop app card",r),await ue(S(t,"docs/(platform)/ai.mdx"),"ai.mdx automation lanes row + section","desktop-lanes-docs",r)),i||(await ue(n,"configuration.mdx desktop-AI paragraph","desktop-ai-docs",r),await H(S(t,"docs/(platform)/navigation.mdx"),"gates the desktop app's Chat and Automations, see [desktop AI](/desktop/ai) |","gates the desktop app's Chat and Automations |","navigation.mdx sidebar-flag agents-row link",r)),!o&&e.runner&&(await ue(S(t,"docs/(platform)/runner/index.mdx"),"runner index.mdx desktop-setup section","desktop-setup-docs",r),await ue(S(t,"docs/(platform)/runner/terminal.mdx"),"runner terminal.mdx desktop-panel section","desktop-terminal-docs",r))}async function ci(e){if(e.desktop)return;await ee(S(e.projectDir,"apps/desktop"),{recursive:!0});let t=S(e.projectDir,"packages/i18n/translations");for(let i of await ti(t,{withFileTypes:!0}))i.isDirectory()&&await ee(S(t,i.name,"desktop.json"),{force:!0});await ee(S(e.projectDir,".github/workflows/desktop-release.yml"));for(let i of ha)await ee(S(e.projectDir,i),{recursive:!0});await ee(S(e.projectDir,"packages/utils/src/desktop-download.ts")),await ee(S(e.projectDir,"packages/utils/tests/desktop-download.test.ts"));let r=S(e.projectDir,"packages/utils/package.json"),n=JSON.parse(await je(r,"utf-8"));if(!n.exports?.["./desktop-download"])throw new Error("stripDesktopApp: expected packages/utils/package.json to carry the ./desktop-download export");delete n.exports["./desktop-download"],await $e(r,n," ","stripDesktopApp")}function wa(e){return e.desktop}async function li(e){wa(e)||await ee(S(e.projectDir,"packages/agent-core"),{recursive:!0})}import{readFile as ba}from"fs/promises";import{join as ka}from"path";var pi=`on:
1294
+ $1`),n===r){if(r.includes("UPSTASH_REDIS_REST_URL"))return;throw new Error(`pruneCacheTests: nothing to rewrite in ${t} for the Upstash swap (boilerplate drift).`)}for(let o of['vi.mock("ioredis"','vi.mock("rate-limit-redis"',"REDIS_URL:"])if(n.includes(o))throw new Error(`pruneCacheTests: ${t} still contains ${o} after the Upstash swap (boilerplate drift).`);await c(t,n)}import{readFile as Ha}from"fs/promises";var Ya=/^[ \t]*(?:\/\/|\{\/\*|\*) gsaas:[a-z0-9-]+-(?:start|end)(?: \*\/\})?[ \t]*\r?\n/gm,Wa=new Set([".ts",".tsx",".vue",".mjs",".js",".jsx",".md",".mdx"]),fi=32;function za(e){let t=e.lastIndexOf(".");return t>=0&&Wa.has(e.slice(t))}async function qa(e){let t=await Ha(e,"utf-8");if(!t.includes("gsaas:"))return;let r=t.replace(Ya,"");r!==t&&await c(e,r)}async function gi(e){let t=await ke(e.projectDir,e.projectDir,r=>r==="node_modules"||r.startsWith("node_modules/")||r===".git"||r.startsWith(".git/"),za);for(let r=0;r<t.length;r+=fi)await Promise.all(t.slice(r,r+fi).map(qa))}import{readdir as Qt,readFile as le,rm as S}from"fs/promises";import{join as p}from"path";import{readFile as bi,rm as _}from"fs/promises";import{join as A}from"path";import{readFile as Ot}from"fs/promises";import{join as xt}from"path";async function yi(e,t){let r=xt(e,"packages/api/src/routes/internal/index.ts");await c(r,Y(await Ot(r,"utf-8"),"internal router","runner",t)),await qt(r);let n=xt(e,"packages/api/src/ai/automation-runner.ts");await c(n,Y(await Ot(n,"utf-8"),"automation-runner.ts","runner",t));for(let s of["packages/api/src/index.ts","packages/api/src/routes/internal/ai/chat-router.ts","packages/api/src/routes/internal/ai/automations-router.ts","packages/api/src/ai/byok.ts","packages/api/tests/ai/automations.test.ts","packages/api/tests/ai/automations-runs-route.test.ts","packages/api/tests/routes/ai.test.ts","packages/api/tests/ai/summarize-cap.test.ts"]){let a=xt(e,s);await c(a,Y(await Ot(a,"utf-8"),s,"runner",t))}let i=xt(e,"packages/api/src/ai/settings-store.ts");await c(i,Y(await Ot(i,"utf-8"),"settings-store.ts","runner",t));let o=xt(e,"packages/api/tests/ai/settings-store.test.ts");await c(o,Y(await Ot(o,"utf-8"),"settings-store.test.ts","runner",t))}async function Mr(e){if(e.runner)return;let t=e.projectDir,r="stripRunner";await _(A(t,"packages/api/src/relay"),{recursive:!0}),await _(A(t,"packages/api/src/runner"),{recursive:!0}),await _(A(t,"packages/api/src/ai/runner-model.ts")),await _(A(t,"packages/api/src/ai/runner-routing.ts")),await _(A(t,"packages/api/tests/relay"),{recursive:!0}),await _(A(t,"packages/api/tests/runner"),{recursive:!0}),await _(A(t,"packages/api/src/routes/internal/runner.ts")),await _(A(t,"packages/api/src/routes/internal/runner-transport.ts")),await _(A(t,"packages/api/src/routes/internal/runner-limits.ts")),await _(A(t,"packages/api/tests/routes/runner.test.ts")),await _(A(t,"packages/api/tests/routes/runner-transport-harness.ts")),await _(A(t,"packages/api/tests/routes/runner-transport-connect.test.ts")),await _(A(t,"packages/api/tests/routes/runner-transport-events.test.ts")),await _(A(t,"packages/api/tests/ai/automation-coalesce.test.ts")),await _(A(t,"packages/api/tests/ai/automations-lazy-expiry.test.ts")),await _(A(t,"packages/api/tests/ai/runner-model.test.ts")),await _(A(t,"packages/api/tests/unit/runner-offline-routing.test.ts")),await _(A(t,"packages/api/tests/unit/runner-offline-interactive.test.ts")),await _(A(t,"packages/api/tests/unit/runner-unavailable-routing.test.ts")),await _(A(t,"packages/api/tests/unit/runner-unavailable-interactive.test.ts")),await _(A(t,"packages/api/tests/unit/runner-arm-harness.ts")),await _(A(t,"packages/api/tests/ai/runner-chat.test.ts")),await _(A(t,"packages/api/tests/helpers/runner-protocol.ts")),await _(A(t,"packages/api/tests/helpers/runner-frames.ts")),await _(A(t,"packages/api/tests/helpers/relay-runtime-mock.ts"));let n=A(t,"packages/api/package.json"),i=JSON.parse(await bi(n,"utf-8"));if(!i.exports?.["./runner"])throw new Error(`${r}: expected packages/api/package.json to carry the ./runner export`);delete i.exports["./runner"],await Ee(n,i," ",r),await yi(t,r),await ni(t,"runner",["/runner"],r,"Runners"),await E(A(t,"docs/(platform)/ai.mdx"),"Runs on a paired [runner](/runner) CLI are never metered either - the user's own subscription executes them. ","","ai.mdx runner metering sentence",r),await E(A(t,"docs/(platform)/caching.mdx")," - see [per-runner request budgets](/runner/install#per-runner-request-budgets)","","caching.mdx runner budgets link",r);let o=A(t,"docs/(platform)/configuration.mdx"),s="| `runner.enabled` |",a=(await bi(o,"utf-8")).split(`
1295
+ `).find(d=>d.startsWith(s));if(a===void 0)throw new Error(`${r}: expected the configuration.mdx \`runner.enabled\` row (boilerplate drift).`);await E(o,`${a}
1296
+ `,"","configuration.mdx runner config-table row",r)}var Ja=new Set(["ci.yml","desktop-release.yml","mobile-release.yml"]),Xa=["apps/web/app/.well-known/apple-app-site-association/route.ts","apps/web/app/.well-known/assetlinks.json/route.ts","apps/web/tests/routes/apple-app-site-association.test.ts","apps/web/tests/routes/assetlinks.test.ts"],Za=["packages/auth/src/config.ts","packages/api/src/routes/internal/index.ts","packages/api/src/index.ts"],Qa=["packages/api/src/routes/internal/index.ts","packages/api/src/routes/inngest.ts","packages/notifications/src/index.ts","packages/config/src/cache.ts"],ec=["packages/notifications/src/push","packages/notifications/tests/push","packages/api/src/routes/internal/push.ts","packages/api/tests/routes/push.test.ts","packages/api/src/functions/notifications/push-dispatch.ts","packages/api/src/functions/notifications/push-receipts.ts","packages/api/src/functions/notifications/push-sink.ts","packages/api/tests/functions/push-dispatch.test.ts","packages/api/tests/functions/push-receipts.test.ts","packages/api/tests/functions/push-sink.test.ts","packages/api/tests/unit/push-store.test.ts","packages/api/tests/routes/push-registry.test.ts","packages/config/tests/push-cache-keys.test.ts"],tc=["packages/payments/src/store","packages/payments/tests/store","packages/payments/tests/helpers/clock.ts","packages/payments/tests/lib/store-claim-ids.test.ts","packages/api/src/routes/internal/webhooks/revenuecat.ts","packages/api/tests/routes/revenuecat-webhook.test.ts","packages/api/tests/routes/revenuecat-webhook-limiter.test.ts","packages/api/tests/routes/revenuecat-interleavings.test.ts","packages/api/src/routes/internal/billing-mobile.ts","packages/api/tests/routes/billing-mobile-sync.test.ts","packages/api/tests/routes/billing-mobile-mount.test.ts","packages/api/src/functions/billing/revenuecat-reconcile.ts","packages/api/src/functions/billing/revenuecat-reconcile-entity.ts","packages/api/tests/functions/revenuecat-reconcile.test.ts"],rc=["packages/api/src/routes/internal/index.ts","packages/api/src/routes/inngest.ts","packages/api/src/index.ts","packages/payments/src/index.ts","packages/config/src/cache.ts"],nc=["apps/mobile/lib/purchases","apps/mobile/app/(app)/settings/billing/paywall.tsx","apps/mobile/e2e/flows/billing-paywall.yaml","apps/mobile/e2e/flows/billing-purchase.yaml","apps/mobile/e2e/flows/billing-restore.yaml"],ic=[{dir:"apps/mobile/components/billing",prefixes:["paywall","credit-packs","store-"]},{dir:"apps/mobile/tests",prefixes:["purchases-"]}],sc=["apps/mobile/app.config.ts","apps/mobile/app/_layout.tsx","apps/mobile/app/delete-account.tsx","apps/mobile/app/(app)/settings/billing/index.tsx","apps/mobile/components/home/plan-cards.tsx","apps/mobile/lib/api-client.ts","apps/mobile/lib/auth/sign-out.ts","apps/mobile/types/env.d.ts","apps/mobile/tests/api-client.test.ts","apps/mobile/tests/feature-seams.test.ts","apps/mobile/tests/root-layout.test.tsx","apps/mobile/tests/route-manifest.test.ts","apps/mobile/tests/sign-out.test.ts"],oc=[","," {",' file: "billing-paywall.yaml",',' tags: ["purchases"],',' title: "The paywall renders sandbox offerings"'," },"," {",' file: "billing-purchase.yaml",',' tags: ["purchases"],',' title: "A sandbox purchase updates the plan card through the sync route"'," },",' { file: "billing-restore.yaml", tags: ["purchases"], title: "Restore purchases" }',""].join(`
1297
+ `),ac=[["## Billing (store purchases)","","#### Paywall (`billing-paywall.yaml`)","","| `testID` | Element | Flow |","| --- | --- | --- |","| `settings-paywall-screen` | The paywall route's screen root | `billing-paywall.yaml` |","| `billing-paywall-loading` | The offering read's skeleton | `billing-paywall.yaml` |","| `billing-paywall-status-error` | The offering read failed | `billing-paywall.yaml` |","| `billing-paywall-status-retry` | Retry the offering read | `billing-paywall.yaml` |","| `billing-paywall-list` | The list of purchasable packages | `billing-paywall.yaml` |","| `billing-paywall-row-<productId>` | One package row, keyed by store product id | `billing-paywall.yaml` |","| `billing-paywall-row-<productId>-action` | That row's buy control | `billing-purchase.yaml` |","| `billing-paywall-restore` | Restore purchases, from the paywall | `billing-restore.yaml` |","",""].join(`
1298
+ `),["#### Purchase and restore controls (`billing-purchase.yaml`, `billing-restore.yaml`)","","| `testID` | Element | Flow |","| --- | --- | --- |",'| `billing-upgrade` | The plan card\'s "See plans" link into the paywall | `billing-purchase.yaml` |',"| `billing-restore` | Restore purchases, on the billing page | `billing-restore.yaml` |",'| `billing-restore-lead` | The "Already subscribed?" lead beside it | `billing-restore.yaml` |',"| `billing-manage` | Manage subscription (Customer Center or the store page) | `billing-purchase.yaml` |","| `billing-external-link` | The external purchase link, when the disclosure sheet ships | `billing-purchase.yaml` |","| `billing-external-link-disclosure` | That link's disclosure sheet | `billing-purchase.yaml` |","",""].join(`
1299
+ `),["#### Credit packs (`billing-purchase.yaml`)","","| `testID` | Element | Flow |","| --- | --- | --- |","| `billing-credit-packs` | The credit packs card | `billing-purchase.yaml` |","| `billing-credit-packs-list` | The pack list | `billing-purchase.yaml` |","| `billing-credit-packs-row-<storeProductId>` | One pack row | `billing-purchase.yaml` |","| `billing-credit-packs-row-<storeProductId>-action` | That pack's buy control | `billing-purchase.yaml` |","| `billing-credit-packs-empty` | No pack is available on this plan | `billing-purchase.yaml` |","| `billing-credit-packs-unavailable` | The store listed none of the configured ids | `billing-purchase.yaml` |","",""].join(`
1300
+ `)],cc=["apps/web/app/[locale]/(marketing)/download","apps/web/tests/routes/download-page.test.tsx"],lc=["cli","cli:clean","demo:bench","playground:regen","playground:test","playground:test:repo","playground:test:audit","playground:test:units","playground:test:e2e","playground:test:build","playground:test:mobile","check-types:scripts","ai:parity","runner:export","runner:verify","runner:publish"];async function wi(e){let t=p(e.projectDir,".github/workflows"),r=await Qt(t).catch(()=>[]);for(let n of r)Ja.has(n)||await S(p(t,n),{recursive:!0,force:!0})}async function pc(e,t){let r=p(e,"package.json"),n=JSON.parse(await le(r,"utf-8"));if(!n.scripts)return;let i=!1;for(let s of t)s in n.scripts&&(delete n.scripts[s],i=!0);if(!i)return;let o=/\n([\t ]+)"/.exec(await le(r,"utf-8"))?.[1]??" ";await c(r,JSON.stringify(n,null,o)+`
1301
+ `)}async function vi(e){await pc(e.projectDir,[...lc])}async function ki(e){let t=p(e.projectDir,"package.json"),r=await le(t,"utf-8"),n=JSON.parse(r);!n.scripts||!("dev"in n.scripts)||(n.scripts.dev=e.architecture==="separate"?"turbo run dev --continue":"turbo run dev --continue --filter=!backend",await c(t,JSON.stringify(n,null," ")+`
1302
+ `))}async function Ei(e){let t=p(e.projectDir,"package.json"),r=JSON.parse(await le(t,"utf-8")),n=r.scripts?.test;if(n!=="turbo run test --concurrency=4 && vitest run")throw new Error(`rewriteRootTestScript: expected the root test script to be "turbo run test --concurrency=4 && vitest run", got ${JSON.stringify(n)} (boilerplate drift).`);r.scripts.test="turbo run test --concurrency=4",await c(t,JSON.stringify(r,null," ")+`
1303
+ `)}async function Si(e){e.docs||await S(p(e.projectDir,"apps/docs"),{recursive:!0})}async function Pi(e){let t=p(e.projectDir,"docs/(platform)/meta.json"),r=JSON.parse(await le(t,"utf-8"));if(r.title!=="GenerateSaaS")throw new Error(`stripUnusedDocsSilo: expected the feature-page group to be titled "GenerateSaaS", found ${JSON.stringify(r.title)} (boilerplate drift).`);r.title=e.appName,await Ee(t,r," ","stripUnusedDocsSilo"),e.desktop||await S(p(e.projectDir,"docs/desktop"),{recursive:!0,force:!0}),e.mobile||await S(p(e.projectDir,"docs/mobile"),{recursive:!0,force:!0}),await dc(e),await hc(e)}async function W(e,t,r,n){await c(e,Y(await le(e,"utf-8"),t,r,n))}async function dc(e){let t=e.projectDir,r="stripUnusedDocsSilo",n=p(t,"docs/(platform)/configuration.mdx"),i=e.desktop,o=e.desktop&&e.ai;e.desktop||(await E(n,"| Desktop app + device sign-in - see [desktop app](/desktop) |","| Desktop app + device sign-in |","configuration.mdx desktop config-table link",r),await W(n,"configuration.mdx desktop TRUSTED_ORIGINS callout","desktop-origins-docs",r),await E(p(t,"docs/cli/init.mdx"),"the [desktop app](/desktop), AI features","the desktop app, AI features","cli init.mdx desktop app link",r),await W(p(t,"docs/(platform)/payments/organizations.mdx"),"payments organizations.mdx desktop tenancy callout","desktop-tenancy-docs",r),await W(p(t,"docs/(platform)/projects.mdx"),"projects.mdx desktop scoping callout","desktop-projects-docs",r),await W(p(t,"docs/(platform)/client-architecture.mdx"),"client-architecture.mdx desktop card","desktop-architecture-docs",r),await W(p(t,"docs/(platform)/testing/e2e.mdx"),"testing/e2e.mdx desktop e2e callout","desktop-testing-docs",r),await E(p(t,"docs/(platform)/index.mdx"),` <Card href="/desktop" title="Desktop app" description="Cross-platform desktop app with device sign-in and AI agents." />
1304
+ `,"","index.mdx desktop app card",r),await W(p(t,"docs/(platform)/ai.mdx"),"ai.mdx automation lanes row + section","desktop-lanes-docs",r)),i||(await W(n,"configuration.mdx desktop-AI paragraph","desktop-ai-docs",r),await E(p(t,"docs/(platform)/navigation.mdx"),"gates the desktop app's Chat and Automations, see [desktop AI](/desktop/ai) |","gates the desktop app's Chat and Automations |","navigation.mdx sidebar-flag agents-row link",r)),!o&&e.runner&&(await W(p(t,"docs/(platform)/runner/index.mdx"),"runner index.mdx desktop-setup section","desktop-setup-docs",r),await W(p(t,"docs/(platform)/runner/terminal.mdx"),"runner terminal.mdx desktop-panel section","desktop-terminal-docs",r))}async function Ai(e){if(e.desktop)return;await S(p(e.projectDir,"apps/desktop"),{recursive:!0});let t=p(e.projectDir,"packages/i18n/translations");for(let i of await Qt(t,{withFileTypes:!0}))i.isDirectory()&&await S(p(t,i.name,"desktop.json"),{force:!0});await S(p(e.projectDir,".github/workflows/desktop-release.yml"));for(let i of cc)await S(p(e.projectDir,i),{recursive:!0});await S(p(e.projectDir,"packages/utils/src/desktop-download.ts")),await S(p(e.projectDir,"packages/utils/tests/desktop-download.test.ts"));let r=p(e.projectDir,"packages/utils/package.json"),n=JSON.parse(await le(r,"utf-8"));if(!n.exports?.["./desktop-download"])throw new Error("stripDesktopApp: expected packages/utils/package.json to carry the ./desktop-download export");delete n.exports["./desktop-download"],await Ee(r,n," ","stripDesktopApp")}function uc(e){return e.desktop}async function Ii(e){uc(e)||await S(p(e.projectDir,"packages/agent-core"),{recursive:!0})}async function mc(e,t,r,n){let i=await Qt(e);for(let o of t){let s=i.filter(a=>a.startsWith(o));if(s.length===0)throw new Error(`${n}: expected ${r} to hold at least one ${o}* entry`);for(let a of s)await S(p(e,a),{recursive:!0})}}async function Zt(e,t,r,n){await c(e,Y(await le(e,"utf-8"),t,r,n))}async function hc(e){let t=e.projectDir,r="stripUnusedDocsSilo",n=e.mobile&&e.mobilePurchases==="revenuecat";e.mobile||(await E(p(t,"docs/(platform)/index.mdx"),` <Card href="/mobile" title="Mobile app" description="Cross-platform iOS and Android app with push, universal links, and store billing." />
1305
+ `,"","index.mdx mobile app card",r),await E(p(t,"docs/(platform)/configuration.mdx")," - see [mobile app](/mobile)","","configuration.mdx mobile config-table link",r),await E(p(t,"docs/(platform)/client-architecture.mdx"),` <Card href="/mobile" title="Mobile app">The Expo app - the third surface this client core serves.</Card>
1306
+ `,"","client-architecture.mdx mobile app card",r),await E(p(t,"docs/cli/init.mdx"),"The [mobile app](/mobile) (`apps/mobile`, built with Expo)","The mobile app (`apps/mobile`, built with Expo)","cli init.mdx mobile app link",r),await E(p(t,"docs/(platform)/notifications.mdx"),"- **On a phone too.** With the [mobile app](/mobile) shipped and `config.mobile.push.enabled` on, the same notification is delivered to the user's registered devices as a push - see [mobile notifications](/mobile/notifications).\n\n","","notifications.mdx mobile push bullet",r),await E(p(t,"docs/(platform)/authentication/social-oauth.mdx")," - the native flow adds `APPLE_APP_BUNDLE_IDENTIFIER`, see [mobile auth](/mobile/auth) |"," |","social-oauth.mdx Apple native-flow tail",r),await E(p(t,"docs/(platform)/authentication/two-factor-and-passkeys.mdx"),"- **On the [mobile app](/mobile/auth)**, passkeys use the platform's own biometrics and are claimed by the same `.well-known` association files. The app's biometric **App lock** is a separate device gate, never a second factor.\n\n","","two-factor-and-passkeys.mdx mobile passkeys bullet",r),await E(p(t,"docs/(platform)/project-structure.mdx"),"the web, desktop and mobile apps","the web and desktop apps","project-structure.mdx surface list",r),await W(p(t,"docs/(platform)/testing/e2e.mdx"),"testing e2e.mdx mobile Maestro section","mobile-testing-docs",r),await W(p(t,"docs/(platform)/environment-variables/index.mdx"),"environment-variables mobile section","mobile-env-docs",r)),n||(await E(p(t,"docs/(platform)/payments/meta.json"),', "mobile-billing"',"","payments meta.json mobile-billing entry",r),await E(p(t,"docs/(platform)/payments/index.mdx"),` <Card href="/payments/mobile-billing" title="Mobile store billing" description="In-app purchases through RevenueCat, additive to your web provider." />
1307
+ `,"","payments index.mdx mobile store billing card",r),await S(p(t,"docs/(platform)/payments/mobile-billing.mdx"))),e.mobile&&!n&&(await E(p(t,"docs/mobile/meta.json"),', "billing"',"","mobile meta.json billing entry",r),await E(p(t,"docs/mobile/index.mdx"),` <Card href="/mobile/billing" title="Billing" description="Store purchases through RevenueCat, and the web-billing shape." />
1308
+ `,"","mobile index.mdx billing card",r),await W(p(t,"docs/mobile/index.mdx"),"mobile index.mdx store-purchases block","mobile-purchases-docs",r),await S(p(t,"docs/mobile/billing"),{recursive:!0,force:!0}))}async function Ri(e){if(e.mobile)return;await S(p(e.projectDir,"apps/mobile"),{recursive:!0});let t=p(e.projectDir,"packages/i18n/translations");for(let i of await Qt(t,{withFileTypes:!0}))i.isDirectory()&&await S(p(t,i.name,"mobile.json"),{force:!0});await S(p(e.projectDir,"packages/i18n/tests/mobile-notification-labels.test.ts")),await S(p(e.projectDir,"packages/i18n/tests/mobile-brand-literals.test.ts")),await S(p(e.projectDir,"packages/auth/tests/expo-plugin.test.ts")),await S(p(e.projectDir,"packages/auth/tests/expo-authorization-allowlist.test.ts")),await S(p(e.projectDir,"packages/auth/src/expo-authorization-allowlist.ts")),await S(p(e.projectDir,".github/workflows/mobile-release.yml")),await S(p(e.projectDir,"tooling/styles/scripts/build-mobile-css.ts"));let r=p(e.projectDir,"tooling/styles/package.json"),n=JSON.parse(await le(r,"utf-8"));if(!n.scripts?.["mobile-css"])throw new Error("stripMobileApp: expected tooling/styles/package.json to carry the mobile-css script");delete n.scripts["mobile-css"],Object.keys(n.scripts).length===0&&delete n.scripts,await Ee(r,n," ","stripMobileApp");for(let i of Xa)await at(p(e.projectDir,i),p(e.projectDir,"apps/web"));await S(p(e.projectDir,"packages/api/src/routes/internal/well-known.ts")),await S(p(e.projectDir,"packages/api/tests/routes/well-known-mobile.test.ts")),await S(p(e.projectDir,"packages/api/src/routes/internal/mobile-auth.ts")),await S(p(e.projectDir,"packages/api/tests/routes/mobile-auth.test.ts")),await S(p(e.projectDir,"apps/web/public/images/stores"),{recursive:!0});for(let i of Za)await Zt(p(e.projectDir,i),i,"mobile-auth","stripMobileApp");for(let i of Qa)await Zt(p(e.projectDir,i),i,"mobile-push","stripMobileApp");for(let i of ec)await S(p(e.projectDir,i),{recursive:!0})}async function Ti(e){if(e.mobile&&e.mobilePurchases==="revenuecat")return;for(let n of tc)await S(p(e.projectDir,n),{recursive:!0});let t=p(e.projectDir,"packages/payments/package.json"),r=JSON.parse(await le(t,"utf-8"));if(!r.sideEffects?.includes("./src/store/revenuecat/index.ts"))throw new Error("stripMobilePurchases: expected packages/payments/package.json to list the store lane in sideEffects");r.sideEffects=r.sideEffects.filter(n=>n!=="./src/store/revenuecat/index.ts"),await Ee(t,r," ","stripMobilePurchases");for(let n of rc)await Zt(p(e.projectDir,n),n,"mobile-purchases","stripMobilePurchases");if(e.mobile){for(let n of nc)await S(p(e.projectDir,n),{recursive:!0});for(let{dir:n,prefixes:i}of ic)await mc(p(e.projectDir,n),i,n,"stripMobilePurchases");for(let n of sc)await Zt(p(e.projectDir,n),n,"mobile-purchases","stripMobilePurchases");for(let n of ac)await E(p(e.projectDir,"apps/mobile/e2e/README.md"),n,"","e2e README purchases row block","stripMobilePurchases");await E(p(e.projectDir,"apps/mobile/e2e/tags.ts"),oc,`
1309
+ `,"e2e tags.ts purchases inventory entries","stripMobilePurchases")}}import{readFile as fc}from"fs/promises";import{join as gc}from"path";var _i=`on:
1310
+ workflow_run:
1311
+ workflows: ["Tag Release"]
1312
+ branches: [main]
1313
+ types: [completed]
1314
+ workflow_dispatch:`,yc=`on:
1315
+ workflow_dispatch:`;async function Oi(e){if(!e.desktop)return;let t=gc(e.projectDir,".github/workflows/desktop-release.yml"),r=await fc(t,"utf8");if(!r.includes(_i))throw new Error(`Cannot configure the desktop release trigger: the expected workflow_run block was not found in ${t}. The boilerplate workflow may have drifted.`);await c(t,r.replace(_i,yc))}import{readFile as bc}from"fs/promises";import{join as wc}from"path";var xi=`on:
1149
1316
  workflow_run:
1150
1317
  workflows: ["Tag Release"]
1151
1318
  branches: [main]
1152
1319
  types: [completed]
1153
- workflow_dispatch:`,Ea=`on:
1154
- workflow_dispatch:`;async function di(e){if(!e.desktop)return;let t=ka(e.projectDir,".github/workflows/desktop-release.yml"),r=await ba(t,"utf8");if(!r.includes(pi))throw new Error(`Cannot configure the desktop release trigger: the expected workflow_run block was not found in ${t}. The boilerplate workflow may have drifted.`);await l(t,r.replace(pi,Ea))}import{join as Sa}from"path";async function fi(e){let t=Aa(e);await l(Sa(e.projectDir,".github/workflows/ci.yml"),t)}function ui(e){return e.databaseProvider!=="neon"&&e.cacheProvider!=="upstash"}function mi(e,t,r){return` - uses: actions/upload-artifact@v4
1320
+ workflow_dispatch:`,vc=`on:
1321
+ workflow_dispatch:`;async function Di(e){if(!e.mobile)return;let t=wc(e.projectDir,".github/workflows/mobile-release.yml"),r=await bc(t,"utf8");if(!r.includes(xi))throw new Error(`Cannot configure the mobile release trigger: the expected workflow_run block was not found in ${t}. The boilerplate workflow may have drifted.`);await c(t,r.replace(xi,vc))}import{join as kc}from"path";async function Li(e){let t=Ec(e);await c(kc(e.projectDir,".github/workflows/ci.yml"),t)}function Ni(e){return e.databaseProvider!=="neon"&&e.cacheProvider!=="upstash"}function Ci(e,t,r){return` - uses: actions/upload-artifact@v4
1155
1322
  if: \${{ ${r} }}
1156
1323
  with:
1157
1324
  name: playwright-report-${e}
@@ -1160,7 +1327,7 @@ $1`),n===r){if(r.includes("UPSTASH_REDIS_REST_URL"))return;throw new Error(`prun
1160
1327
  ${t}/test-results
1161
1328
  if-no-files-found: ignore
1162
1329
  retention-days: 7
1163
- `}function Aa(e){let t=e.cacheProvider==="upstash",r=t?"":` redis:
1330
+ `}function Ec(e){let t=e.cacheProvider==="upstash",r=t?"":` redis:
1164
1331
  image: redis:8-alpine
1165
1332
  ports:
1166
1333
  - 6379:6379
@@ -1204,7 +1371,7 @@ ${r} mailpit:
1204
1371
  # suite's port is derived per checkout - a baked URL would name a port nothing serves.
1205
1372
  - name: Start Inngest
1206
1373
  run: docker run -d --name inngest --network host inngest/inngest:v1.17.4 inngest dev
1207
- `,p=ui(e)?`
1374
+ `,d=Ni(e)?`
1208
1375
  # The end-to-end suite, on every push and PR. It needs no keys and no internet: the harness
1209
1376
  # preflights the services below, builds and starts the app itself, provisions a throwaway database,
1210
1377
  # and answers every vendor from a local fake. Any other outbound host fails the spec by name.
@@ -1236,7 +1403,7 @@ ${a}
1236
1403
  env:
1237
1404
  NODE_OPTIONS: --max-old-space-size=6144
1238
1405
 
1239
- ${mi("web","apps/web","!cancelled()")}`:"\n # No end-to-end job: this project's stack cannot run one in CI without credentials.\n # - a `neon` database talks over a serverless websocket that only reaches a managed endpoint,\n # so a Postgres service container is unreachable from the app;\n # - an `upstash` cache is a hosted REST endpoint, and this file carries no secrets.\n # The suite itself works locally: `pnpm infra` for a project on self-hosted Postgres and Redis,\n # then `pnpm e2e:ci`. To run it here, add a job with those services plus your own credentials as\n # repository secrets, or point a second CI environment at a disposable managed instance.\n",u=e.desktop&&ui(e),v=u?`
1406
+ ${Ci("web","apps/web","!cancelled()")}`:"\n # No end-to-end job: this project's stack cannot run one in CI without credentials.\n # - a `neon` database talks over a serverless websocket that only reaches a managed endpoint,\n # so a Postgres service container is unreachable from the app;\n # - an `upstash` cache is a hosted REST endpoint, and this file carries no secrets.\n # The suite itself works locally: `pnpm infra` for a project on self-hosted Postgres and Redis,\n # then `pnpm e2e:ci`. To run it here, add a job with those services plus your own credentials as\n # repository secrets, or point a second CI environment at a disposable managed instance.\n",u=e.desktop&&Ni(e),w=u?`
1240
1407
  # The DESKTOP end-to-end suite, on a manual "Run workflow" dispatch only: it builds the Electron
1241
1408
  # bundles AND a Next production build, which is too slow to pay for on every push. It has its own
1242
1409
  # port and its own build directory, so it can never collide with the web job above.
@@ -1270,18 +1437,18 @@ ${a}
1270
1437
  env:
1271
1438
  NODE_OPTIONS: --max-old-space-size=6144
1272
1439
 
1273
- ${mi("desktop","apps/desktop","failure()")}`:"",g=u?"\n # Manual trigger for the opt-in desktop E2E job (see its `if:` below).\n workflow_dispatch:":"",w=e.databaseProvider==="neon"?`
1440
+ ${Ci("desktop","apps/desktop","failure()")}`:"",y=u?"\n # Manual trigger for the opt-in desktop E2E job (see its `if:` below).\n workflow_dispatch:":"",P=e.databaseProvider==="neon"?`
1274
1441
  # Required for neon: the api suite's embedded Postgres is TCP-only and the neon
1275
1442
  # websocket driver cannot reach it. Add this repository secret (any Postgres this
1276
1443
  # suite may CREATE DATABASE on - a Neon branch works).
1277
- GENERATESAAS_TEST_DATABASE_ADMIN_URL: \${{ secrets.GENERATESAAS_TEST_DATABASE_ADMIN_URL }}`:"",P=t?`
1444
+ GENERATESAAS_TEST_DATABASE_ADMIN_URL: \${{ secrets.GENERATESAAS_TEST_DATABASE_ADMIN_URL }}`:"",I=t?`
1278
1445
  UPSTASH_REDIS_REST_URL: https://test.upstash.io
1279
1446
  UPSTASH_REDIS_REST_TOKEN: test-token`:`
1280
- REDIS_URL: redis://localhost:6379`,f=(()=>{switch(e.paymentProvider){case"stripe":return`
1447
+ REDIS_URL: redis://localhost:6379`,R=(()=>{switch(e.paymentProvider){case"stripe":return`
1281
1448
  STRIPE_SECRET_KEY: test
1282
1449
  STRIPE_WEBHOOK_SECRET: test`;case"polar":return`
1283
1450
  POLAR_ACCESS_TOKEN: test
1284
- POLAR_WEBHOOK_SECRET: test`;case"none":return"";default:{let M=e.paymentProvider;throw new Error(`buildCiYaml: unhandled payment provider "${String(M)}"`)}}})();return`# Deployment is handled by the hosting platform (Vercel, Coolify, etc.)
1451
+ POLAR_WEBHOOK_SECRET: test`;case"none":return"";default:{let O=e.paymentProvider;throw new Error(`buildCiYaml: unhandled payment provider "${String(O)}"`)}}})();return`# Deployment is handled by the hosting platform (Vercel, Coolify, etc.)
1285
1452
  # which auto-deploys on push. CI runs in parallel as a quality gate.
1286
1453
  # For PR-based workflows, enable GitHub branch protection to require CI before merging.
1287
1454
 
@@ -1291,7 +1458,7 @@ on:
1291
1458
  push:
1292
1459
  branches: [main]
1293
1460
  pull_request:
1294
- branches: [main]${g}
1461
+ branches: [main]${y}
1295
1462
 
1296
1463
  concurrency:
1297
1464
  group: ci-\${{ github.ref }}
@@ -1310,7 +1477,7 @@ jobs:
1310
1477
  # "cancelled", which reads as a mystery rather than as a timeout. 40 is roughly double that.
1311
1478
  timeout-minutes: 40
1312
1479
  ${n} env:
1313
- CONTENT_API_KEY: test-contentapi-key-16chars${w}${P}${f}
1480
+ CONTENT_API_KEY: test-contentapi-key-16chars${P}${I}${R}
1314
1481
  STORAGE_REGION: test
1315
1482
  STORAGE_ENDPOINT: http://test
1316
1483
  STORAGE_ACCESS_KEY_ID: test
@@ -1326,21 +1493,21 @@ ${n} env:
1326
1493
  - run: pnpm check-types
1327
1494
 
1328
1495
  - run: pnpm test
1329
- ${p}${v}`}import{readFile as hi}from"fs/promises";import{existsSync as Pa}from"fs";import{join as Pr}from"path";var gi="@repo/database";function Ra(e){return e?"pnpm -F @repo/database reset && pnpm -F @repo/database run deploy -- --push":"pnpm -F @repo/database run deploy"}function Ia(e){switch(e){case"fullstack":return"web";case"separate":return"backend";default:{let t=e;throw new Error(`schemaOwnerApp: unhandled architecture "${String(t)}"`)}}}async function Ta(e,t,r){let n=await hi(e,"utf-8"),i=JSON.parse(n),o=i.scripts?.[t];if(!o)throw new Error(`Cannot prepend to missing script "${t}" in ${e}`);o.includes(gi)||(i.scripts={...i.scripts,[t]:`${r} && ${o}`},await l(e,JSON.stringify(i,null," ")+`
1330
- `))}async function _a(e,t){let r=Pa(e)?JSON.parse(await hi(e,"utf-8")):{$schema:"https://openapi.vercel.sh/vercel.json"},n=r.buildCommand?.trim()||"pnpm build";n.includes(gi)||(r.buildCommand=`${t} && ${n}`,await l(e,JSON.stringify(r,null," ")+`
1331
- `))}async function yi(e){let t=Ra(e.demo===!0),r=Ia(e.architecture),n=Pr(e.projectDir,"apps",r);switch(e.deploymentTarget){case"node":await Ta(Pr(n,"package.json"),"start",t);return;case"vercel":await _a(Pr(n,"vercel.json"),t);return;default:{let i=e.deploymentTarget;throw new Error(`generateDeployScripts: unhandled deployment target "${String(i)}"`)}}}import{readFile as Oa}from"fs/promises";import{existsSync as xa}from"fs";import{join as Gt}from"path";var vi=["stripe","polar"],Da=[...vi,"none"];async function wi(e){let t=Gt(e.projectDir,"packages/payments/src"),r=e.paymentProvider,i=`// Active payment provider. To switch, change the path below to
1332
- // ${Da.filter(o=>o!==r).map(o=>`"./${o}/index"`).join(" or ")} (other folders are kept in place).
1496
+ ${d}${w}`}import{readFile as $i}from"fs/promises";import{existsSync as Sc}from"fs";import{join as Ur}from"path";var ji="@repo/database";function Pc(e){return e?"pnpm -F @repo/database reset && pnpm -F @repo/database run deploy -- --push":"pnpm -F @repo/database run deploy"}function Ac(e){switch(e){case"fullstack":return"web";case"separate":return"backend";default:{let t=e;throw new Error(`schemaOwnerApp: unhandled architecture "${String(t)}"`)}}}async function Ic(e,t,r){let n=await $i(e,"utf-8"),i=JSON.parse(n),o=i.scripts?.[t];if(!o)throw new Error(`Cannot prepend to missing script "${t}" in ${e}`);o.includes(ji)||(i.scripts={...i.scripts,[t]:`${r} && ${o}`},await c(e,JSON.stringify(i,null," ")+`
1497
+ `))}async function Rc(e,t){let r=Sc(e)?JSON.parse(await $i(e,"utf-8")):{$schema:"https://openapi.vercel.sh/vercel.json"},n=r.buildCommand?.trim()||"pnpm build";n.includes(ji)||(r.buildCommand=`${t} && ${n}`,await c(e,JSON.stringify(r,null," ")+`
1498
+ `))}async function Mi(e){let t=Pc(e.demo===!0),r=Ac(e.architecture),n=Ur(e.projectDir,"apps",r);switch(e.deploymentTarget){case"node":await Ic(Ur(n,"package.json"),"start",t);return;case"vercel":await Rc(Ur(n,"vercel.json"),t);return;default:{let i=e.deploymentTarget;throw new Error(`generateDeployScripts: unhandled deployment target "${String(i)}"`)}}}import{readFile as Tc}from"fs/promises";import{existsSync as _c}from"fs";import{join as er}from"path";var Ui=["stripe","polar"],Oc=[...Ui,"none"];async function Fi(e){let t=er(e.projectDir,"packages/payments/src"),r=e.paymentProvider,i=`// Active payment provider. To switch, change the path below to
1499
+ // ${Oc.filter(o=>o!==r).map(o=>`"./${o}/index"`).join(" or ")} (other folders are kept in place).
1333
1500
  export { ops } from "./${r}/index";
1334
- `;await l(Gt(t,"providers/index.ts"),i),await l(Gt(t,"index.ts"),await Na(t,r))}async function Na(e,t){let r=Gt(e,"index.ts");if(!xa(r))throw new Error(`generatePaymentBarrel: expected ${r} to exist - did packages/payments move?`);let n=await Oa(r,"utf-8"),i=vi.filter(s=>s!==t);return n.split(`
1501
+ `;await c(er(t,"providers/index.ts"),i),await c(er(t,"index.ts"),await xc(t,r))}async function xc(e,t){let r=er(e,"index.ts");if(!_c(r))throw new Error(`generatePaymentBarrel: expected ${r} to exist - did packages/payments move?`);let n=await Tc(r,"utf-8"),i=Ui.filter(s=>s!==t);return n.split(`
1335
1502
  `).filter(s=>!i.some(a=>s.includes(`./providers/${a}/`))).join(`
1336
- `)}import{access as Ca,readFile as Wt,rm as Ht}from"fs/promises";import{join as z}from"path";var Ai=["sentry","posthog"],Rr={sentry:"src/providers/sentry",posthog:"src/providers/posthog"},Yt={sentry:["tests/sentry-load-guard.test.ts"],posthog:["tests/posthog-load-guard.test.ts","tests/llm-analytics.test.ts"]},La=["tests/resolution.test.ts","tests/export-isolation.test.ts"],$a=["src/providers/sentry/desktop-main.ts","src/providers/sentry/desktop-renderer.ts","src/resolve-desktop-main.ts","src/resolve-desktop-renderer.ts"],ja=["./desktop-main","./desktop-renderer"],Ua=[`vi.mock("@sentry/electron/main", sdk.factories["@sentry/electron/main"]);
1503
+ `)}import{access as Dc,readFile as nr,rm as tr}from"fs/promises";import{join as te}from"path";var Hi=["sentry","posthog"],Fr={sentry:"src/providers/sentry",posthog:"src/providers/posthog"},rr={sentry:["tests/sentry-load-guard.test.ts"],posthog:["tests/posthog-load-guard.test.ts","tests/llm-analytics.test.ts"]},Nc=["tests/resolution.test.ts","tests/export-isolation.test.ts"],Cc=["src/providers/sentry/desktop-main.ts","src/providers/sentry/desktop-renderer.ts","src/resolve-desktop-main.ts","src/resolve-desktop-renderer.ts"],Lc=["./desktop-main","./desktop-renderer"],$c=[`vi.mock("@sentry/electron/main", sdk.factories["@sentry/electron/main"]);
1337
1504
  `,`vi.mock("@sentry/electron/renderer", sdk.factories["@sentry/electron/renderer"]);
1338
- `],Ma=[' describeGate("desktop main", "electronMain", async () => {',' describeGate("desktop renderer", "electronRenderer", async () => {',' describe("the desktop main init options", () => {',' describe("the desktop renderer init", () => {'],bi=`
1505
+ `],jc=[' describeGate("desktop main", "electronMain", async () => {',' describeGate("desktop renderer", "electronRenderer", async () => {',' describe("the desktop main init options", () => {',' describe("the desktop renderer init", () => {'],Vi=`
1339
1506
  });
1340
- `,Fa=" ";async function Pi(e){let t=z(e.projectDir,"packages/observability");await ki(t,"packages/observability");let r=e.observability,n=r.filter(s=>!K.includes(s));if(n.length>0)throw new Error(`observability names ${JSON.stringify(n)}, which is not a reporting provider. Every entry must be one of: ${K.join(", ")}. Shaping stops here so the project is not left half-stripped.`);for(let s of r)await ki(z(t,Rr[s]),`packages/observability/${Rr[s]}`);let i=e.desktop?[...Si,...Ac]:Si,o=new Map;for(let s of i)o.set(s.file,await Wt(z(t,s.file),"utf-8"));for(let s of Ja(r,e.desktop))await Xa(t,s);for(let s of Ai)if(!r.includes(s)){await Ht(z(t,Rr[s]),{recursive:!0,force:!0});for(let a of Yt[s])await Ht(z(t,a),{force:!0})}for(let s of La)await Ht(z(t,s),{force:!0});e.desktop||await Va(t,r);for(let s of i){let a=o.get(s.file);a!==void 0&&await l(z(t,s.file),Ga(a,s,r))}}async function ki(e,t){try{await Ca(e)}catch{throw new Error(`generateObservabilityProvider: expected ${t} to exist - did the observability package move?`)}}async function Va(e,t){for(let i of $a)await Ht(z(e,i),{force:!0});t.includes("sentry")&&await Ba(e);let r=z(e,"package.json"),n=JSON.parse(await Wt(r,"utf-8"));for(let i of ja){if(!n.exports?.[i])throw new Error(`generateObservabilityProvider: expected packages/observability/package.json to carry the ${i} export`);delete n.exports[i]}await $e(r,n," ","generateObservabilityProvider")}async function Ba(e){let t=z(e,Yt.sentry[0]),r=await Wt(t,"utf-8");r=Ir(r,Ua.map(n=>({find:n,replace:""})),Yt.sentry[0]);for(let n of Ma)r=Ka(r,n,Yt.sentry[0]);await l(t,r)}function Ka(e,t,r){let n=e.indexOf(t),i=n===-1?-1:e.indexOf(bi,n);if(n===-1||i===-1)throw new Error(`generateObservabilityProvider: ${r} no longer contains the block this strip removes - re-point the anchor. Missing: ${t.trim()}`);if(e.indexOf(t,n+t.length)!==-1)throw new Error(`generateObservabilityProvider: ${r} now opens more than one block with the same line, so this strip cannot tell which one to remove - re-point the anchor. Block: ${t.trim()}`);let o=e.slice(n+t.length,i).split(`
1341
- `).find(a=>a.trim().length>0&&!a.startsWith(`${Fa} `));if(o!==void 0)throw new Error(`generateObservabilityProvider: ${r} no longer closes the block this strip removes at its own indent - re-point the anchor. Block: ${t.trim()}; first line inside it that is not part of its body: ${o.trim()}`);let s=i+bi.length;return e[s]===`
1342
- `&&(s+=1),e.slice(0,n)+e.slice(s)}function Ga(e,t,r){let n=Ai.filter(s=>!r.includes(s)),i=e,o=[];for(let s of n){let a=Ha(i,s);i=a.source,o.push(...a.bindings)}t.dualMode!==!1&&(i=Ir(i,[{find:Wa,replace:qa(r)}],t.file)),i=Ir(i,t.edits(r),t.file);for(let s of n)if(i.includes(`./providers/${s}`))throw new Error(`generateObservabilityProvider: ${t.file} still reaches providers/${s} after the strip - the boilerplate grew an import shape the drop does not know.`);for(let s of o)if(new RegExp(String.raw`\b${s}\b`).test(i))throw new Error(`generateObservabilityProvider: ${t.file} still names ${s}, whose import the strip removed - the boilerplate grew a use the pinned rewrites do not cover.`);return i}function Ir(e,t,r){let n=e;for(let i of t){if(!n.includes(i.find))throw new Error(`generateObservabilityProvider: ${r} no longer contains the text this strip rewrites - re-point the anchor. Missing: ${i.find.split(`
1343
- `)[0]}`);n=n.replace(i.find,i.replace)}return n}function Ha(e,t){let r=new RegExp(String.raw`^import[^;]*?from "\./providers/${t}/[^"]*";\n`,"gm"),n=[];return{source:e.replace(r,o=>(n.push(...Ya(o)),"")),bindings:n}}function Ya(e){let t=/^import\s+(?:type\s+)?([\s\S]*?)\s+from\s/.exec(e)?.[1]??"",r=[],n=/\{([\s\S]*?)\}/.exec(t);n?.[1]&&r.push(...n[1].split(",").map(s=>s.trim().replace(/^type\s+/,"")).map(s=>s.split(/\s+as\s+/).at(-1)??"").filter(s=>s.length>0));let i=t.replace(/\{[\s\S]*?\}/,"").replace(/,/g," ").trim(),o=/^\*\s+as\s+([A-Za-z_$][\w$]*)$/.exec(i);return o?.[1]?r.push(o[1]):/^[A-Za-z_$][\w$]*$/.test(i)&&r.push(i),r}var Wa=" * DUAL-MODE, exactly as `packages/payments/src/providers/index.ts` does it. This repo resolves the\n * provider at RUNTIME so dev and the test suite exercise every one of them. A generated project has\n * exactly one provider, so the CLI overwrites this file with a direct re-export of it and the rest\n * become unreachable and are pruned; the generator is the observability twin of\n * `packages/cli/src/generators/payment-barrel.ts` and lands with the manifest option.";function qa(e){let t=e.map(n=>n==="sentry"?"Sentry":"PostHog").join(" and ");return`${e.length===0?` * Your project was generated with observability OFF, so no provider adapter and no reporting
1507
+ `,Mc=" ";async function Yi(e){let t=te(e.projectDir,"packages/observability");await Bi(t,"packages/observability");let r=e.observability,n=r.filter(s=>!K.includes(s));if(n.length>0)throw new Error(`observability names ${JSON.stringify(n)}, which is not a reporting provider. Every entry must be one of: ${K.join(", ")}. Shaping stops here so the project is not left half-stripped.`);for(let s of r)await Bi(te(t,Fr[s]),`packages/observability/${Fr[s]}`);let i=e.desktop?[...Ki,...El]:Ki,o=new Map;for(let s of i)o.set(s.file,await nr(te(t,s.file),"utf-8"));for(let s of zc(r,e.desktop))await qc(t,s);for(let s of Hi)if(!r.includes(s)){await tr(te(t,Fr[s]),{recursive:!0,force:!0});for(let a of rr[s])await tr(te(t,a),{force:!0})}for(let s of Nc)await tr(te(t,s),{force:!0});e.desktop||await Uc(t,r);for(let s of i){let a=o.get(s.file);a!==void 0&&await c(te(t,s.file),Bc(a,s,r))}}async function Bi(e,t){try{await Dc(e)}catch{throw new Error(`generateObservabilityProvider: expected ${t} to exist - did the observability package move?`)}}async function Uc(e,t){for(let i of Cc)await tr(te(e,i),{force:!0});t.includes("sentry")&&await Fc(e);let r=te(e,"package.json"),n=JSON.parse(await nr(r,"utf-8"));for(let i of Lc){if(!n.exports?.[i])throw new Error(`generateObservabilityProvider: expected packages/observability/package.json to carry the ${i} export`);delete n.exports[i]}await Ee(r,n," ","generateObservabilityProvider")}async function Fc(e){let t=te(e,rr.sentry[0]),r=await nr(t,"utf-8");r=Vr(r,$c.map(n=>({find:n,replace:""})),rr.sentry[0]);for(let n of jc)r=Vc(r,n,rr.sentry[0]);await c(t,r)}function Vc(e,t,r){let n=e.indexOf(t),i=n===-1?-1:e.indexOf(Vi,n);if(n===-1||i===-1)throw new Error(`generateObservabilityProvider: ${r} no longer contains the block this strip removes - re-point the anchor. Missing: ${t.trim()}`);if(e.indexOf(t,n+t.length)!==-1)throw new Error(`generateObservabilityProvider: ${r} now opens more than one block with the same line, so this strip cannot tell which one to remove - re-point the anchor. Block: ${t.trim()}`);let o=e.slice(n+t.length,i).split(`
1508
+ `).find(a=>a.trim().length>0&&!a.startsWith(`${Mc} `));if(o!==void 0)throw new Error(`generateObservabilityProvider: ${r} no longer closes the block this strip removes at its own indent - re-point the anchor. Block: ${t.trim()}; first line inside it that is not part of its body: ${o.trim()}`);let s=i+Vi.length;return e[s]===`
1509
+ `&&(s+=1),e.slice(0,n)+e.slice(s)}function Bc(e,t,r){let n=Hi.filter(s=>!r.includes(s)),i=e,o=[];for(let s of n){let a=Gc(i,s);i=a.source,o.push(...a.bindings)}t.dualMode!==!1&&(i=Vr(i,[{find:Hc,replace:Yc(r)}],t.file)),i=Vr(i,t.edits(r),t.file);for(let s of n)if(i.includes(`./providers/${s}`))throw new Error(`generateObservabilityProvider: ${t.file} still reaches providers/${s} after the strip - the boilerplate grew an import shape the drop does not know.`);for(let s of o)if(new RegExp(String.raw`\b${s}\b`).test(i))throw new Error(`generateObservabilityProvider: ${t.file} still names ${s}, whose import the strip removed - the boilerplate grew a use the pinned rewrites do not cover.`);return i}function Vr(e,t,r){let n=e;for(let i of t){if(!n.includes(i.find))throw new Error(`generateObservabilityProvider: ${r} no longer contains the text this strip rewrites - re-point the anchor. Missing: ${i.find.split(`
1510
+ `)[0]}`);n=n.replace(i.find,i.replace)}return n}function Gc(e,t){let r=new RegExp(String.raw`^import[^;]*?from "\./providers/${t}/[^"]*";\n`,"gm"),n=[];return{source:e.replace(r,o=>(n.push(...Kc(o)),"")),bindings:n}}function Kc(e){let t=/^import\s+(?:type\s+)?([\s\S]*?)\s+from\s/.exec(e)?.[1]??"",r=[],n=/\{([\s\S]*?)\}/.exec(t);n?.[1]&&r.push(...n[1].split(",").map(s=>s.trim().replace(/^type\s+/,"")).map(s=>s.split(/\s+as\s+/).at(-1)??"").filter(s=>s.length>0));let i=t.replace(/\{[\s\S]*?\}/,"").replace(/,/g," ").trim(),o=/^\*\s+as\s+([A-Za-z_$][\w$]*)$/.exec(i);return o?.[1]?r.push(o[1]):/^[A-Za-z_$][\w$]*$/.test(i)&&r.push(i),r}var Hc=" * DUAL-MODE, exactly as `packages/payments/src/providers/index.ts` does it. This repo resolves the\n * provider at RUNTIME so dev and the test suite exercise every one of them. A generated project has\n * exactly one provider, so the CLI overwrites this file with a direct re-export of it and the rest\n * become unreachable and are pruned; the generator is the observability twin of\n * `packages/cli/src/generators/payment-barrel.ts` and lands with the manifest option.";function Yc(e){let t=e.map(n=>n==="sentry"?"Sentry":"PostHog").join(" and ");return`${e.length===0?` * Your project was generated with observability OFF, so no provider adapter and no reporting
1344
1511
  * SDK were shipped: this file is the whole layer, and every call is a no-op that loads nothing.`:` * Your project reports to ${t}. No other provider's adapter or SDK was generated, so
1345
1512
  * there is nothing here for a bundler to fold away and nothing for pnpm to install.`}
1346
1513
  *
@@ -1348,123 +1515,128 @@ export { ops } from "./${r}/index";
1348
1515
  * run \`generatesaas update\`: re-shaping brings that provider's adapter and its dependency back. The
1349
1516
  * update stages nothing while you are already on the latest version, so the switch lands with the
1350
1517
  * next release. Pasting the other provider's credential into \`config.observability\` on its own
1351
- * cannot: the adapter it would need is not here.`}var Ei={source:"src/providers/sentry/browser.ts",name:"SentryBrowserInitOptions",fields:["environment: string;"]},za={source:"src/providers/sentry/desktop-main.ts",name:"SentryDesktopMainInitOptions",fields:["environment: string;","appIsReady: boolean;"]};function Ja(e,t){return e.includes("sentry")?[]:t?[Ei,za]:[Ei]}async function Xa(e,t){let r=await Wt(z(e,t.source),"utf-8"),n=`export interface ${t.name} {
1518
+ * cannot: the adapter it would need is not here.`}var Gi={source:"src/providers/sentry/browser.ts",name:"SentryBrowserInitOptions",fields:["environment: string;"]},Wc={source:"src/providers/sentry/desktop-main.ts",name:"SentryDesktopMainInitOptions",fields:["environment: string;","appIsReady: boolean;"]};function zc(e,t){return e.includes("sentry")?[]:t?[Gi,Wc]:[Gi]}async function qc(e,t){let r=await nr(te(e,t.source),"utf-8"),n=`export interface ${t.name} {
1352
1519
  `,i=r.indexOf(n),o=r.indexOf(`
1353
1520
  }
1354
1521
  `,i);if(i===-1||o===-1)throw new Error(`generateObservabilityProvider: ${t.source} no longer exports ${t.name} - the stand-in this strip emits for a project without Sentry has nothing left to track.`);let s=r.slice(i+n.length,o).split(`
1355
- `).map(a=>a.trim()).filter(a=>/^[A-Za-z_$][\w$]*\??: .+;$/.test(a));if(s.join(" ")!==t.fields.join(" "))throw new Error(`generateObservabilityProvider: ${t.name} now declares [${s.join(" ")}] but the stand-in emitted for a project without Sentry declares [${t.fields.join(" ")}] - update the stand-in so switching providers on an update cannot break a buyer's call site.`)}var Za=" into every browser bundle. `tests/export-isolation.test.ts` walks the real module graph to keep\n * that from ever landing quietly.",Qa=` if (resolveSentryDsn(observability, process.env.SENTRY_DSN)) ops.push(sentryServerOps);
1356
- `,ec=` if (resolvePosthogCredentials(observability, process.env.POSTHOG_API_KEY)) {
1522
+ `).map(a=>a.trim()).filter(a=>/^[A-Za-z_$][\w$]*\??: .+;$/.test(a));if(s.join(" ")!==t.fields.join(" "))throw new Error(`generateObservabilityProvider: ${t.name} now declares [${s.join(" ")}] but the stand-in emitted for a project without Sentry declares [${t.fields.join(" ")}] - update the stand-in so switching providers on an update cannot break a buyer's call site.`)}var Jc=" into every browser bundle. `tests/export-isolation.test.ts` walks the real module graph to keep\n * that from ever landing quietly.",Xc=` if (resolveSentryDsn(observability, process.env.SENTRY_DSN)) ops.push(sentryServerOps);
1523
+ `,Zc=` if (resolvePosthogCredentials(observability, process.env.POSTHOG_API_KEY)) {
1357
1524
  ops.push(posthogServerOps);
1358
1525
  }
1359
- `,tc=` if (resolveSentryDsn(config.observability, process.env.SENTRY_DSN)) {
1526
+ `,Qc=` if (resolveSentryDsn(config.observability, process.env.SENTRY_DSN)) {
1360
1527
  await initSentryServer();
1361
1528
  }
1362
- `,rc=` if (observability?.sentry?.dsn) ops.push(sentryBrowserOps);
1363
- `,nc=` if (observability?.posthog?.publicKey) ops.push(posthogBrowserOps);
1364
- `,ic=` if (config.observability?.sentry?.dsn) await initSentryBrowser(options);
1365
- `,Ri=` if (observability?.posthog?.publicKey && !observability?.sentry?.dsn) {
1529
+ `,el=` if (observability?.sentry?.dsn) ops.push(sentryBrowserOps);
1530
+ `,tl=` if (observability?.posthog?.publicKey) ops.push(posthogBrowserOps);
1531
+ `,rl=` if (config.observability?.sentry?.dsn) await initSentryBrowser(options);
1532
+ `,Wi=` if (observability?.posthog?.publicKey && !observability?.sentry?.dsn) {
1366
1533
  announceMissingDesktopSdk("posthog");
1367
1534
  }
1368
- `,sc="export type BrowserReportingOptions = SentryBrowserInitOptions;",oc=`export interface BrowserReportingOptions {
1535
+ `,nl="export type BrowserReportingOptions = SentryBrowserInitOptions;",il=`export interface BrowserReportingOptions {
1369
1536
  /** The deployment environment reports are tagged with. */
1370
1537
  environment: string;
1371
- }`,ac=` return observability?.sentry?.dsn ? [sentryDesktopMainOps] : [];
1372
- `,cc=` if (observability?.sentry?.dsn) await initSentryDesktopMain(options);
1373
- `,lc="export type DesktopMainReportingOptions = SentryDesktopMainInitOptions;",pc=`export interface DesktopMainReportingOptions {
1538
+ }`,sl=` return observability?.sentry?.dsn ? [sentryDesktopMainOps] : [];
1539
+ `,ol=` if (observability?.sentry?.dsn) await initSentryDesktopMain(options);
1540
+ `,al="export type DesktopMainReportingOptions = SentryDesktopMainInitOptions;",cl=`export interface DesktopMainReportingOptions {
1374
1541
  /** The deployment environment reports are tagged with. */
1375
1542
  environment: string;
1376
1543
  /** Whether Electron's \`app\` has emitted \`ready\` yet. */
1377
1544
  appIsReady: boolean;
1378
- }`,dc=` return observability?.sentry?.dsn ? [sentryDesktopRendererOps] : [];
1379
- `,uc=` if (observability?.sentry?.dsn) await initSentryDesktopRenderer();
1380
- `;function Ii(e,t){let r=[];return e.includes("sentry")||r.push({find:t.sentry,replace:""}),e.includes("posthog")||r.push({find:t.posthog,replace:""}),r}function mc(e){let t=[{find:Za,replace:" into every browser bundle."},...Ii(e,{sentry:Qa,posthog:ec})];return e.includes("sentry")||t.push({find:tc,replace:""}),t}function fc(e){let t=[...Ii(e,{sentry:rc,posthog:nc})];return e.includes("sentry")||t.push({find:ic,replace:""},hc),t}var hc={find:sc,replace:oc};function gc(e){let t=[];return e.includes("posthog")||t.push({find:Ri,replace:""}),e.includes("sentry")||t.push({find:ac,replace:` return [];
1381
- `},{find:cc,replace:""},{find:lc,replace:pc}),t}function yc(e){let t=[];return e.includes("posthog")||t.push({find:Ri,replace:""}),e.includes("sentry")||t.push({find:dc,replace:` return [];
1382
- `},{find:uc,replace:""}),t}var vc=` if (!posthogConfigured()) return;
1545
+ }`,ll=` return observability?.sentry?.dsn ? [sentryDesktopRendererOps] : [];
1546
+ `,pl=` if (observability?.sentry?.dsn) await initSentryDesktopRenderer();
1547
+ `;function zi(e,t){let r=[];return e.includes("sentry")||r.push({find:t.sentry,replace:""}),e.includes("posthog")||r.push({find:t.posthog,replace:""}),r}function dl(e){let t=[{find:Jc,replace:" into every browser bundle."},...zi(e,{sentry:Xc,posthog:Zc})];return e.includes("sentry")||t.push({find:Qc,replace:""}),t}function ul(e){let t=[...zi(e,{sentry:el,posthog:tl})];return e.includes("sentry")||t.push({find:rl,replace:""},ml),t}var ml={find:nl,replace:il};function hl(e){let t=[];return e.includes("posthog")||t.push({find:Wi,replace:""}),e.includes("sentry")||t.push({find:sl,replace:` return [];
1548
+ `},{find:ol,replace:""},{find:al,replace:cl}),t}function fl(e){let t=[];return e.includes("posthog")||t.push({find:Wi,replace:""}),e.includes("sentry")||t.push({find:ll,replace:` return [];
1549
+ `},{find:pl,replace:""}),t}var gl=` if (!posthogConfigured()) return;
1383
1550
  try {
1384
1551
  const { captureAiGeneration: capture } = await import("./providers/posthog/llm");
1385
1552
  await capture(event);
1386
1553
  } catch (reportingError) {
1387
1554
  logReportingFailure("[observability] failed to report an AI generation", reportingError);
1388
1555
  }
1389
- `,wc=` // No PostHog adapter was generated for this project, so there is nothing to report to.
1390
- `,bc=` // No PostHog adapter was generated for this project, so there is nothing queued to drain.
1391
- `,kc=`/** Whether PostHog is configured on this server, without touching the PostHog folder. */
1556
+ `,yl=` // No PostHog adapter was generated for this project, so there is nothing to report to.
1557
+ `,bl=` // No PostHog adapter was generated for this project, so there is nothing queued to drain.
1558
+ `,wl=`/** Whether PostHog is configured on this server, without touching the PostHog folder. */
1392
1559
  function posthogConfigured(): boolean {
1393
1560
  return Boolean(process.env.POSTHOG_API_KEY || config.observability?.posthog?.publicKey);
1394
1561
  }
1395
1562
 
1396
- `,Ec=` if (!posthogConfigured()) return;
1563
+ `,vl=` if (!posthogConfigured()) return;
1397
1564
  try {
1398
1565
  const { flushAiGenerations: flush } = await import("./providers/posthog/llm");
1399
1566
  await flush();
1400
1567
  } catch (reportingError) {
1401
1568
  logReportingFailure("[observability] failed to flush queued AI generations", reportingError);
1402
1569
  }
1403
- `;function Sc(e){return e.includes("posthog")?[]:[{find:vc,replace:wc},{find:Ec,replace:bc},{find:kc,replace:""},{find:`import { logReportingFailure } from "./logger";
1570
+ `;function kl(e){return e.includes("posthog")?[]:[{find:gl,replace:yl},{find:vl,replace:bl},{find:wl,replace:""},{find:`import { logReportingFailure } from "./logger";
1404
1571
  `,replace:""},{find:`import { config } from "@repo/config";
1405
- `,replace:""},{find:"export async function captureAiGeneration(event: AiGenerationEvent): Promise<void> {",replace:"export async function captureAiGeneration(_event: AiGenerationEvent): Promise<void> {"},{find:" * @param event - The call to record.",replace:" * @param _event - Ignored: no PostHog adapter was generated for this project."}]}var Si=[{file:"src/resolve-server.ts",edits:mc},{file:"src/resolve-browser.ts",edits:fc},{file:"src/llm-analytics.ts",edits:Sc,dualMode:!1}],Ac=[{file:"src/resolve-desktop-main.ts",edits:gc},{file:"src/resolve-desktop-renderer.ts",edits:yc}];import{readdir as Pc,readFile as Rc}from"fs/promises";import{join as Ti}from"path";async function _i(e){if(e.demo)return;let t=e.appName.trim()||e.projectName,r=JSON.stringify(t).slice(1,-1),n=Ti(e.projectDir,"packages/i18n/translations"),i;try{i=await Pc(n)}catch{return}for(let o of i){let s=Ti(n,o,"web.json"),a;try{a=await Rc(s,"utf-8")}catch{continue}let p=a.replaceAll("GenerateSaaS",()=>r);p!==a&&await l(s,p)}}async function qt(e){let t=e.projectDir;e.demo||(await yn(t),await vn(t),await wn(t),await bn(t)),await kn(t),await Sn(e),await Pn(e),await Rn(e),e.demo||await In(e),await xn(e),await Dn(e),await Nn(e),await jn(e),await Mn(e),await Vn(e),await Kn(e),await Hn(e),await qn(e),await ri(e),await fi(e),await ni(e),await ii(e),await si(e),await oi(e),await ai(e),await ci(e),await li(e),await di(e),await Ar(e),await yi(e),await wi(e),await Pi(e),await _i(e),await Jn(e)}import{basename as xi,join as Di,relative as Tc}from"path";import{createHash as Oi}from"crypto";import{readFile as Ic}from"fs/promises";async function zt(e){let t=await Ic(e);return Oi("sha256").update(t).digest("hex")}function Tr(e){return Oi("sha256").update(e).digest("hex")}var _c=new Set(["data",q]);function Oc(e){let t=e.split("/");for(let r of t)if(pr.has(r)||dr.has(r)||_c.has(r)||r.startsWith(".env")&&!r.includes("example"))return!0;return!1}function Ni(e,t){return{projectName:e.projectName??xi(t),appName:e.appName??e.projectName??xi(t),projectDir:t,architecture:e.architecture??"fullstack",paymentProvider:e.paymentProvider??"none",emailProvider:e.emailProvider??"smtp",multiTenancy:e.multiTenancy??!1,projects:e.projects??!1,docs:e.docs??!1,desktop:e.desktop??!1,ai:e.ai??!1,webAi:e.webAi??!0,runner:e.runner??!1,revenueSharing:e.revenueSharing??!1,credits:e.credits??!1,defaultCurrency:e.defaultCurrency??"USD",deploymentTarget:e.deploymentTarget??"node",databaseProvider:e.databaseProvider??"postgres",cacheProvider:e.cacheProvider??"redis",observability:e.observability??[],version:e.version,baseUrl:e.baseUrl,credentials:{},demo:!1}}function xc(e,t){return{version:e.version,initialVersion:e.version,appName:e.appName,projectName:e.projectName,frontend:rn,architecture:e.architecture,paymentProvider:e.paymentProvider,emailProvider:e.emailProvider,multiTenancy:e.multiTenancy,projects:e.projects,docs:e.docs,desktop:e.desktop,ai:e.ai,webAi:e.webAi,runner:e.runner,credits:e.credits,revenueSharing:e.revenueSharing,defaultCurrency:e.defaultCurrency,deploymentTarget:e.deploymentTarget,databaseProvider:e.databaseProvider,cacheProvider:e.cacheProvider,observability:e.observability,...e.baseUrl?{baseUrl:e.baseUrl}:{},...t&&{licenseToken:t.token,licenseKeyHash:t.keyHash,installId:t.installId}}}async function Ci(e,t){let n=(await de(e.projectDir,e.projectDir,Oc)).sort(),i=await Promise.all(n.map(async a=>[Ce(Tc(e.projectDir,a)),await zt(a)])),o=Object.fromEntries(i),s=xc(e,t);await l(Di(e.projectDir,ve),JSON.stringify(s,null," ")+`
1406
- `),await l(Di(e.projectDir,nn),JSON.stringify(o,null," ")+`
1407
- `)}import{relative as Dc}from"path";async function ot(e){let r=(await de(e,e,tt)).sort(),n=await Promise.all(r.map(async i=>[Ce(Dc(e,i)),await zt(i)]));return Object.fromEntries(n)}import{copyFile as Nc,mkdir as Cc,rm as Lc}from"fs/promises";import{dirname as $c,join as Li,relative as jc}from"path";import{existsSync as Uc}from"fs";async function _r(e,t){Uc(t)&&await Lc(t,{recursive:!0,force:!0});let n=(await de(e,e,tt)).map(i=>({file:i,dest:Li(t,Ce(jc(e,i)))}));await Promise.all([...new Set(n.map(({dest:i})=>$c(i)))].map(i=>Cc(i,{recursive:!0}))),await Promise.all(n.map(({file:i,dest:o})=>Nc(i,o)))}async function $i(e,t){await _r(e,Li(t,Dt))}import{readFile as Ui,readdir as Mc}from"fs/promises";import{existsSync as Fc}from"fs";import{join as te,dirname as Vc,resolve as Or,sep as xr}from"path";import{fileURLToPath as Bc}from"url";var Kc={"claude-code":".claude/skills",cursor:".cursor/skills",codex:".agents/skills","gemini-cli":".gemini/skills",windsurf:".windsurf/skills"},Ue=Object.values(Kc),Dr=["generatesaas-update"],Gc=Vc(Bc(import.meta.url));function Hc(){return $t(Gc,["skill","content"],"content")}async function Nr(e,t){for(let r of Ue){let n=te(e,r);for(let[i,o]of Object.entries(t)){let s=Or(n,i);if(!s.startsWith(n+xr))continue;let a=te(s,"scripts"),p=te(s,"references");await Ut(a),await Ut(p),await l(te(s,"SKILL.md"),o.skillMd.replaceAll("__SKILL_ROOT__",r));for(let[v,g]of Object.entries(o.scripts)){let w=Or(a,v);w.startsWith(a+xr)&&await l(w,g)}let u=Object.entries(o.references);u.length===0&&await l(te(p,".gitkeep"),"");for(let[v,g]of u){let w=Or(p,v);w.startsWith(p+xr)&&await l(w,g)}}}}async function ji(e){if(!Fc(e))return{};let t={};for(let r of await Mc(e))r!==".gitkeep"&&(t[r]=await Ui(te(e,r),"utf-8"));return t}async function Mi(e){let t=Hc(),r={};for(let n of Dr){let i=te(t,n);r[n]={skillMd:await Ui(te(i,"SKILL.md"),"utf-8"),scripts:await ji(te(i,"scripts")),references:await ji(te(i,"references"))}}await Nr(e,r)}import{execFile as Yc,execFileSync as Wc}from"child_process";import{access as Fi,readFile as qc}from"fs/promises";import{join as Cr}from"path";import*as C from"@clack/prompts";function Fe(e){try{let t=process.platform==="win32"?"where":"which";return Wc(t,[e],{stdio:"ignore"}),!0}catch{return!1}}function Me(e,t,r,n=3e5){return new Promise((i,o)=>{Yc(e,t,{cwd:r,timeout:n,maxBuffer:50*1024*1024},(s,a,p)=>{if(s){let u=String(a||"").trim(),g=[String(p||"").trim(),u].filter(Boolean).join(`
1408
- `);o(new Error(g?`${s.message}
1409
- ${g}`:s.message))}else i()})})}async function Vi(e){if(!Fe("pnpm"))return C.log.warn("pnpm not found. Skipping lockfile regeneration."),!1;try{return await Me("pnpm",["install","--lockfile-only","--no-frozen-lockfile","--config.minimumReleaseAge=0"],e),!0}catch(t){let r=t instanceof Error?t.message:String(t);return C.log.warn(`Lockfile regeneration failed: ${r}`),C.log.warn("Deploys using --frozen-lockfile may fail."),!1}}async function Bi(e){if(!Fe("pnpm"))return C.log.warn("pnpm not found. Skipping dependency installation."),C.log.info("Install pnpm: https://pnpm.io/installation"),!1;let t=C.spinner();t.start("Installing dependencies (this may take a minute)...");try{return await Me("pnpm",["install","--config.minimumReleaseAge=0"],e),t.stop("Dependencies installed."),!0}catch(r){t.stop("Dependency installation failed.");let n=r instanceof Error?r.message:String(r);return C.log.warn(`pnpm install failed: ${n}`),C.log.warn("You can run it manually later."),!1}}async function Ki(e){if(!Fe("pnpm"))return!1;let t=C.spinner();t.start("Generating baseline database migration...");try{return await Me("pnpm",["-F","@repo/database","generate"],e),t.stop("Baseline migration generated."),!0}catch(r){t.stop("Baseline migration generation failed.");let n=r instanceof Error?r.message:String(r);return C.log.warn(`Could not generate baseline migration: ${n}`),C.log.warn("Run 'pnpm -F @repo/database generate' before your first deploy."),!1}}async function Gi(e){try{return await Fi(Cr(e,".git")),C.log.info("Git repository already exists, skipping init."),!0}catch{}if(!Fe("git"))return C.log.warn("git not found. Skipping repository initialization."),!1;let t=C.spinner();t.start("Initializing git repository...");try{return await Me("git",["init"],e),await Me("git",["add","-A"],e),await Me("git",["commit","--no-verify","-m","Initial commit from GenerateSaaS"],e),t.stop("Git repository initialized."),!0}catch{return t.stop("Git initialization failed."),C.log.warn("You can run git init manually later."),!1}}async function Hi(e){if(!Fe("pnpm"))return!1;try{await Fi(Cr(e,".git"))}catch{return!1}try{let t=JSON.parse(await qc(Cr(e,"package.json"),"utf-8")),r=!!t.devDependencies?.["simple-git-hooks"],n=!!t["simple-git-hooks"];if(!r||!n)return!1}catch{return!1}try{return await Me("pnpm",["exec","simple-git-hooks"],e),!0}catch{return C.log.warn("Could not install git hooks. Run 'pnpm exec simple-git-hooks' manually."),!1}}import*as Ve from"@clack/prompts";import J from"picocolors";function Yi(e,t){let r=ze(e);t.dockerAvailable||Ve.log.warn("Docker not found. Install Docker to run local services: https://docs.docker.com/get-docker/"),t.baselineMigrationMissing&&Ve.log.warn("No database migration was generated (the install did not complete). Run `pnpm install` then `pnpm --filter @repo/database generate` and commit the result BEFORE deploying - otherwise your first deploy pushes the schema untracked.");let n=[];n.push(`cd ${e.projectDir}`),t.pnpmInstalled||n.push("pnpm install"),n.push(`pnpm infra ${J.dim(`# ${r.map(a=>Hr[a].label).join(", ")}`)}`),n.push(`pnpm dev ${J.dim("# http://localhost:3000")}`),t.skippedCredentials.length>0&&(n.push(""),n.push(J.dim("Fill in remaining TODO values in .env"))),Ve.note(n.join(`
1410
- `),J.yellow("Start Development"));let i=[];i.push(`App ${J.cyan("http://localhost:3000")}`),e.architecture==="separate"&&i.push(`API ${J.cyan("http://localhost:3010")}`),r.includes("mailpit")&&i.push(`Mailpit ${J.cyan("http://localhost:8025")}`),r.includes("inngest")&&i.push(`Inngest ${J.cyan("http://localhost:8288")}`),Ve.note(i.join(`
1411
- `),J.yellow("Dev Tools"));let o=[],s=zc(e);s.length>0&&o.push(`Set in production: ${J.dim(s.join(", "))}`),o.push("pnpm db:setup # Apply the database schema"),o.push(Jc(e)),Ve.note(o.join(`
1412
- `),J.yellow("Deployment"))}function zc(e){let t=["DATABASE_URL","BETTER_AUTH_SECRET","INNGEST_APP_ID","INNGEST_EVENT_KEY","INNGEST_BASE_URL"];return e.cacheProvider==="upstash"?t.push("UPSTASH_REDIS_REST_URL","UPSTASH_REDIS_REST_TOKEN"):t.push("REDIS_URL"),e.paymentProvider==="stripe"?t.push("STRIPE_SECRET_KEY","STRIPE_WEBHOOK_SECRET"):e.paymentProvider==="polar"&&t.push("POLAR_ACCESS_TOKEN","POLAR_WEBHOOK_SECRET"),e.emailProvider==="ses"?t.push("AMAZON_SES_REGION","AMAZON_SES_KEY","AMAZON_SES_SECRET"):e.emailProvider==="resend"?t.push("RESEND_API_KEY"):t.push("SMTP_HOST","SMTP_PORT"),t}function Jc(e){switch(e.deploymentTarget){case"node":return"Deploy with Docker or your preferred Node.js host";case"vercel":return"vercel deploy # Deploy to Vercel"}}function at(e){let r=(e.startsWith("v")?e.slice(1):e).match(/^(\d+)\.(\d+)\.(\d+)$/);return r?[Number(r[1]),Number(r[2]),Number(r[3])]:null}function Be(e,t){let r=at(e),n=at(t);if(!r||!n)return 0;for(let i=0;i<3;i++)if(r[i]!==n[i])return r[i]-n[i];return 0}function Jt(e,t){return e.versions.find(r=>r.version===t)?.cli}function Wi(e){return process.env.GENERATESAAS_TEMPLATE_TARBALL||process.env.GENERATESAAS_TEMPLATE_TARBALL_DIR||process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?!0:at(e)===null}function Xt(e,t,r="3.6.0"){if(Wi(r))return{ok:!0};let n=Jt(e,e.latest);return!n||at(n)===null?{ok:!0}:Be(r,n)>=0?{ok:!0}:{ok:!1,message:`Your installed generatesaas CLI (v${r}) is too old for template v${e.latest}, which needs CLI v${n} or newer. Run npx generatesaas@latest ${t} so npm fetches the current CLI. If the release happened minutes ago, npm may still be propagating it - retry shortly.`}}function qi(e,t,r="3.6.0"){if(Wi(r))return{ok:!0};let n=Jt(e,t);return!n||at(n)===null?{ok:!0}:Be(r,n)===0?{ok:!0}:{ok:!1,message:`Template v${t} must be scaffolded by CLI v${n} exactly (the CLI whose generators produced that release). Run npx generatesaas@${n} init.`}}import rl from"picocolors";var nl="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";async function il(e){let t=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(`generatesaas-demo:${e}`)),r=[...new Uint8Array(t).subarray(0,16)].map(n=>n.toString(16).padStart(2,"0")).join("");return`${r.slice(0,8)}-${r.slice(8,12)}-${r.slice(12,16)}-${r.slice(16,20)}-${r.slice(20,32)}`}function zi(e){e.command("init").description("Scaffold a new GenerateSaaS project").argument("[apiKey]","license key (same as --api-key)").option("-n, --name <name>","project name (lowercase, hyphens, starts with letter)").option("--app-name <name>","display name for the app").option("-l, --location <path>","project directory (default: ./{name})").addOption(new re("--architecture <type>","fullstack or separate").choices([...Je])).addOption(new re("--payment <provider>","payment provider").choices([...Xe])).addOption(new re("--email <provider>","email provider").choices([...Ze])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").option("--projects","enable projects (a switcher that scopes a user's work)").option("--no-projects","disable projects").option("--docs","include the docs app (apps/docs, Fumadocs)").option("--no-docs","exclude the docs app").option("--desktop","include the desktop app (apps/desktop, built with Electron)").option("--no-desktop","exclude the desktop app").option("--ai","include the AI features (chat, models, automations, integrations); unlocks the runner").option("--no-ai","exclude the AI features").option("--web-ai","show the AI features in the web app (the default)").option("--no-web-ai","desktop-first: keep the AI features out of the web app, which is reduced to account, login and billing - the desktop app is the AI product. Requires --desktop and --ai, and cannot be combined with --runner").option("--runner","include the runner feature - your web users connect a machine that runs their own AI subscriptions for this app").option("--no-runner","exclude the backend runner feature").option("--revenue-sharing","enable revenue sharing").option("--no-revenue-sharing","disable revenue sharing").option("--credits","enable credits system").option("--no-credits","disable credits system (subscription-only)").addOption(new re("--currency <code>","default currency for billing").choices([...se])).addOption(new re("--preset <preset>","infrastructure preset: serverless (Vercel + Neon + Upstash) or selfhost (Docker or Node + Postgres + Redis); --deploy/--database/--cache override it piecewise").choices([...xe])).addOption(new re("--deploy <target>","deployment target").choices([...oe])).addOption(new re("--database <provider>","database provider").choices([...ae])).addOption(new re("--cache <provider>","cache provider").choices([...ce])).option("--observability <providers>",`comma-separated observability providers (${K.join(", ")}); anything unselected ships no SDK`).option("--api-key <key>","API key (skips interactive prompt)").option("--base-url <url>","public base URL (e.g. https://example.com) - bakes into canonical/og/sitemap").option("-y, --yes","accept defaults for unspecified options (non-interactive)").addOption(new re("--demo","first-party demo build: keep sample content, mark site non-indexable - requires CI API key").hideHelp()).addOption(new re("--no-db-migration","skip generating the baseline DB migration (internal: demos/CI/playground)").hideHelp()).action(async(t,r)=>{await sl(t?{...r,apiKey:t}:r)})}async function sl(e){let t=performance.now();Kr("3.6.0");let r;try{r=Qr(e)}catch(b){k.cancel(N(b)),process.exit(1)}let n=k.spinner(),i;try{i=await et({apiKey:e.apiKey,prompt:!e.yes})}catch(b){k.cancel(N(b)),process.exit(1)}e.demo&&Tr(i)!==nl&&(k.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let o=pe(i),s,a=async()=>{s=await ke(o);let b=Xt(s,"init");if(!b.ok)throw new Error(b.message);return{latestVersion:s.latest,selectedVersion:s.latest,desktopAllowed:s.entitlements?.desktopAllowed??!0,runnerAllowed:s.entitlements?.runnerAllowed??!0}};n.start("Verifying access...");let p,u,v=!0,g=!0;try{({latestVersion:p,selectedVersion:u,desktopAllowed:v,runnerAllowed:g}=await a()),n.stop("Access verified."),Ne(i)}catch(b){if(n.stop("Access verification failed."),b instanceof j&&b.status===401){e.yes&&(k.cancel("Invalid API key. Cannot prompt in non-interactive mode."),process.exit(1)),k.log.warning("Invalid API key."),i=await ft(),o=pe(i),n.start("Verifying access...");try{({latestVersion:p,selectedVersion:u,desktopAllowed:v,runnerAllowed:g}=await a()),n.stop("Access verified."),Ne(i)}catch(D){n.stop("Access verification failed."),k.cancel(D instanceof j&&D.status===401?"Invalid API key.":N(D)),process.exit(1)}}else k.cancel(N(b)),process.exit(1)}k.log.success(`Latest version: ${p}`),r.desktop===!0&&!v&&(k.cancel("The desktop app is available on the Pro plan and up. Upgrade your plan at generatesaas.com."),process.exit(1)),r.runner===!0&&!g&&(k.cancel("The runner is available on the Pro plan and up. Upgrade your plan at generatesaas.com."),process.exit(1));let w;e.yes?w=en({...r,...v?{}:{desktop:!1},...g?{}:{runner:!1}}):w=await tn(r,{desktopAllowed:v,runnerAllowed:g});let P;n.start("Activating license...");try{let b=e.demo&&w.baseUrl?await il(w.baseUrl):crypto.randomUUID(),D=()=>({frontend:ar,version:u,installId:b,projectName:w.projectName,options:ln(w)}),$;try{$=await cr(o,D())}catch(ge){let F=Nt(ge);if(!F?.lastAllowedVersion||!s)throw ge;n.stop("License activation failed.");let V=qi(s,F.lastAllowedVersion);if(V.ok||(k.cancel(`${F.message} ${V.message}`),process.exit(1)),e.yes){let Re=Jt(s,F.lastAllowedVersion);k.cancel(`${F.message} Re-run ${Re?`npx generatesaas@${Re} init`:"init"} without --yes to continue with v${F.lastAllowedVersion} (the last version your license covers).`),process.exit(1)}let B=await k.confirm({message:`Your update window has ended. Continue with v${F.lastAllowedVersion} (the last version your license covers)?`});(k.isCancel(B)||!B)&&(k.cancel("Setup cancelled."),process.exit(0)),u=F.lastAllowedVersion,n.start(`Activating license for v${u}...`),$=await cr(o,D())}P={token:$.token,keyHash:Tr(i),installId:$.installId??b},n.stop("License activated.")}catch(b){n.stop("License activation failed."),k.cancel(N(b)),process.exit(1)}let f=tl(w.projectDir);if(Xc(f)&&Zc(f).length>0)if(e.yes)k.log.info(`Directory ${f} is not empty. Merging (keeping existing files, overwriting conflicts).`);else{let D=await k.select({message:`Directory ${f} is not empty.`,options:[{value:"merge",label:"Merge",hint:"keep existing files, overwrite conflicts"},{value:"overwrite",label:"Overwrite",hint:"delete everything and start fresh"},{value:"cancel",label:"Cancel"}]});(k.isCancel(D)||D==="cancel")&&(k.cancel("Setup cancelled."),process.exit(0)),D==="overwrite"&&Qc(f,{recursive:!0,force:!0})}let L={...w,projectDir:f,version:u,...e.demo?{docs:!1}:{}};n.start("Downloading template...");try{await Ct(o,u,f),n.stop("Template downloaded.")}catch(b){n.stop("Download failed."),k.cancel(N(b)),process.exit(1)}n.start("Generating project files...");try{if(await qt(L),!e.demo){let b=await ot(f);await l(el(f,xt),JSON.stringify(b,null," ")+`
1413
- `),await $i(f,f)}await Mi(f),await Ci(L,P),n.stop("Project files generated.")}catch(b){n.stop("Generation failed."),k.cancel(N(b)),process.exit(1)}!await Vi(f)&&L.demo===!0&&(k.cancel("Lockfile regeneration failed on a demo build - refusing to ship a stale lockfile."),process.exit(1));let fe=await Bi(f),Z=L.demo!==!0&&e.dbMigration!==!1,he=!1;Z&&fe&&(await Ki(f),he=!0),await Gi(f),fe&&await Hi(f);let Se=Fe("docker"),Pe=ir(L).map(b=>b.key).filter(b=>!L.credentials?.[b]);Yi(L,{pnpmInstalled:fe,dockerAvailable:Se,skippedCredentials:Pe,baselineMigrationMissing:Z&&!he}),Gr(),k.log.info(rl.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as Zt}from"fs";import{readFile as is}from"fs/promises";import{join as Ke,resolve as ml}from"path";import*as y from"@clack/prompts";import T from"picocolors";import{mkdtemp as ol,rm as al}from"fs/promises";import{tmpdir as cl}from"os";import{join as ll}from"path";async function Lr(e,t,r,n){let i=await ol(ll(cl(),"generatesaas-stage-"));try{await Ct(e,t,i),await qt({...r,projectDir:i}),await _r(i,n)}finally{await al(i,{recursive:!0,force:!0})}}function At(e){return e.paymentProvider!==void 0&&e.paymentProvider!=="none"}var $r=[{key:"architecture",label:"Architecture",hint:"Fullstack (frontend hosts the API) or a standalone Hono backend.",category:"Infrastructure",kind:"enum",default:"fullstack",choices:Je,adoptable:!1,impact:"structural",shapesTree:!0},{key:"deploymentTarget",label:"Deployment target",hint:"Node.js / Docker or Vercel serverless.",category:"Infrastructure",kind:"enum",default:"node",choices:oe,adoptable:!1,impact:"config",shapesTree:!0},{key:"databaseProvider",label:"Database provider",hint:"Self-hosted PostgreSQL, Neon, or Supabase.",category:"Infrastructure",kind:"enum",default:"postgres",choices:ae,adoptable:!1,impact:"config",shapesTree:!0},{key:"cacheProvider",label:"Cache provider",hint:"Self-hosted Redis or Upstash.",category:"Infrastructure",kind:"enum",default:"redis",choices:ce,adoptable:!1,impact:"config",shapesTree:!0},{key:"paymentProvider",label:"Payment provider",hint:"Stripe, Polar, or none.",category:"Features",kind:"enum",default:"none",choices:Xe,adoptable:!1,impact:"structural",shapesTree:!0},{key:"defaultCurrency",label:"Default currency",hint:"Billing and pricing currency.",category:"Features",kind:"enum",default:"USD",choices:se,adoptable:!1,impact:"config",shapesTree:!1},{key:"emailProvider",label:"Email provider",hint:"SMTP, Amazon SES, or Resend.",category:"Features",kind:"enum",default:"smtp",choices:Ze,adoptable:!1,impact:"config",shapesTree:!1},{key:"observability",label:"Observability",hint:"Which services receive your app's errors, and on PostHog its LLM analytics. Pick any combination - anything you do not pick costs nothing, since its SDK and its adapter are never generated.",category:"Features",kind:"multiselect",default:[],choices:K,adoptable:!0,impact:"config",shapesTree:!0},{key:"multiTenancy",label:"Multi-tenancy (organizations)",hint:"Adds organizations - teams, members, and shared resources.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural",shapesTree:!1},{key:"projects",label:"Projects",hint:"Adds projects - a switcher that scopes a user's work inside their account or organization.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"config",shapesTree:!1},{key:"docs",label:"Docs app",hint:"Adds the self-hosted Fumadocs documentation site (apps/docs).",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural",shapesTree:!0},{key:"desktop",label:"Desktop app",hint:"Adds the cross-platform desktop app (apps/desktop, built with Electron).",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural",shapesTree:!0},{key:"ai",label:"AI features",hint:"Adds the AI surface (chat, models, automations, integrations) and unlocks the runner option. A desktop project ALWAYS ships the app's local AI stack: this option sets config.desktop.agents.enabled (the runtime gate on those screens). Off keeps config.ai.enabled false so the AI nav stays hidden - the AI packages still ship, because packages/translate depends on @repo/ai for the i18n pipeline every project uses.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural",shapesTree:!0},{key:"webAi",label:"AI in the web app",hint:"Whether the WEB app shows the AI surfaces - chat, automations, model and integration settings, and their nav entries. Off is the desktop-first shape: the desktop app is where your AI product lives and the website keeps account, billing and licensing only. The AI SERVICE is untouched either way (config.ai.enabled, the /api/ai routes and the desktop agents all stay exactly as your AI option set them), so the desktop app keeps talking to the same backend. Only meaningful with both AI features and the desktop app, and never with the runner - its pairing and device screens are part of the web AI surface.",category:"Features",kind:"boolean",default:!0,adoptable:!0,impact:"config",shapesTree:!1,requires:e=>e.ai===!0&&e.desktop===!0&&e.runner!==!0,requiresLabel:"requires AI features and the desktop app, and is unavailable with the runner (its pairing and device screens live in the web AI surface)"},{key:"runner",label:"Runner daemon",hint:"Lets your users connect a machine that runs their own AI subscriptions (and any device-side work you add) on their hardware. They install AgentRunner, the daemon we publish and keep updated - your project ships the pairing, dispatch and Runners surface, never the daemon. Needs an always-on backend: the transport holds one connection open per paired device, which serverless (Vercel) cannot do.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural",shapesTree:!0,requires:e=>e.ai===!0&&e.webAi!==!1&&ne({...e,runner:!0})===void 0,requiresLabel:"requires AI features, the web AI surface, and an always-on deployment target (not Vercel)"},{key:"credits",label:"Metered credits",hint:"Adds metered usage credits on top of subscription plans.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"config",shapesTree:!1,requires:e=>At(e),requiresLabel:"requires a payment provider"},{key:"revenueSharing",label:"Revenue sharing",hint:"Opt-in MRR leaderboard with dofollow backlinks.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"config",shapesTree:!1}];function pl(e,t){return e[t]!==void 0}function Ji(e){return $r.filter(t=>t.adoptable&&!pl(e,t.key)&&(t.requires?t.requires(e):!0)).map(t=>({key:t.key,label:t.label,hint:t.hint,kind:t.kind,default:typeof t.default=="function"?t.default(e):t.default,...t.choices?{choices:t.choices}:{},impact:t.impact,...t.requiresLabel?{requiresLabel:t.requiresLabel}:{}}))}function dl(e,t){switch(e.kind){case"boolean":return typeof t=="boolean";case"enum":return typeof t=="string"&&(e.choices??[]).includes(t);case"multiselect":return Array.isArray(t)&&t.every(r=>typeof r=="string"&&(e.choices??[]).includes(r))}}function ul(e){let t=(e.choices??[]).map(r=>JSON.stringify(r)).join(", ");switch(e.kind){case"boolean":return"true or false";case"enum":return`one of ${t}`;case"multiselect":return`an array of ${t}`}}function Xi(e){let t=[];for(let r of $r){let n=e[r.key];n!==void 0&&(dl(r,n)||t.push(`\`${r.key}\` is ${JSON.stringify(n)}, but it must be ${ul(r)}.`))}return t}function Zi(e){let t={};for(let r of $r)if(typeof r.default=="function"){if(r.requires&&!r.requires(e))continue;t[r.key]=r.default(e)}else t[r.key]=Array.isArray(r.default)?[...r.default]:r.default;return{...t,...e}}function Qi(e){return e?"ON":"OFF"}var ct="packages/config/src/index.ts";function es(e){let t=e.databaseProvider!==void 0&&e.databaseProvider!=="postgres",r=e.cacheProvider!==void 0&&e.cacheProvider!=="redis";return[...e.dockerServices??[]].filter(n=>!(n==="postgres"&&t)&&!(n==="redis"&&r)).sort()}function ts(e){return[...ze({databaseProvider:e.databaseProvider??"postgres",cacheProvider:e.cacheProvider??"redis",emailProvider:e.emailProvider??"smtp"})].sort()}var jr=[{key:"errorTracking",severity:"warn",normalize:e=>{e.errorTracking===void 0||e.observability!==void 0||(e.observability=e.errorTracking==="none"?[]:[e.errorTracking])},preserved:()=>!0,describe:()=>"Retired option `errorTracking`: it is now `observability`, an array, because a project may run more than one provider at once. Your recorded choice was migrated across automatically."},{key:"companion",severity:"warn",normalize:e=>{e.companion===void 0||e.runner!==void 0||(e.runner=e.companion)},preserved:()=>!0,describe:()=>"Retired option `companion`: it is now `runner`, because the daemon it names is AgentRunner. Your recorded choice was migrated across automatically - the feature itself is unchanged."},{key:"companionSource",severity:"warn",preserved:()=>!1,describe:e=>`Retired option \`companionSource\`: your project recorded a \`${String(e.companionSource)}\` daemon source. There is now one runner daemon - AgentRunner - which we publish and your users install from its own releases. Your project keeps the runner feature and its own pairing, dispatch and Runners page; it no longer holds or brands a daemon.`},{key:"companionName",severity:"warn",preserved:()=>!1,describe:e=>`Retired option \`companionName\`: your project recorded a branded daemon called ${e.companionName}. Buyers no longer brand or publish a daemon - your users install AgentRunner, which we maintain. Their existing install of your old daemon keeps running until they switch.`},{key:"companionRepoUrl",severity:"warn",preserved:()=>!1,describe:e=>`Retired option \`companionRepoUrl\`: your project pointed its install flow at ${e.companionRepoUrl}. It now points at AgentRunner, the one daemon we publish.`},{key:"companionLocalPath",severity:"warn",preserved:e=>(e.companionLocalPath??"").trim()==="",describe:e=>`Retired option \`companionLocalPath\`: your project recorded a local daemon checkout at ${e.companionLocalPath}. The option is gone, and so is the choice behind it - there is one runner daemon (AgentRunner) and we publish it, so nothing in your project is built from a local checkout any more.`},{key:"desktopAutoRelease",severity:"warn",applies:e=>e.desktop===!0,preserved:e=>e.desktopAutoRelease!==!0,describe:()=>"Retired option `desktopAutoRelease`: your project recorded automatic desktop releases, but the release trigger is no longer an option - cutting a release publishes signed installers from your repository and spends your Actions minutes, so it is always deliberate now. The staged `.github/workflows/desktop-release.yml` runs on `workflow_dispatch` only (the Actions tab). Add a `workflow_run` block to it if you want the CI-chained trigger back."},{key:"billingScope",severity:"ask",applies:e=>e.multiTenancy===!0,preserved:e=>e.billingScope!=="organization",describe:()=>"Retired option `billingScope`: your project recorded organization-scoped billing, and organization billing is GONE - not moved to config, removed. Money now lives only on a USER: an organization has no balance, no plan and no payment-provider customer of its own, and a workspace is funded by that organization's OWNER, whose personal balance every credit spent in it is drawn from. Payments already in flight are safe - a webhook arriving with old organization metadata is credited to that organization's owner rather than dropped - but a STORED balance is not: the `credits` and `plan` columns an organization holds them in are going away, and this update does NOT migrate them for you. MOVE every paying organization's credits and plan onto its owner BEFORE you take new checkouts, or that balance goes with the columns. How should this update proceed?"},{key:"socialProviders",severity:"warn",preserved:e=>(e.socialProviders??[]).length===0,describe:e=>`Retired option \`socialProviders\`: your project recorded ${(e.socialProviders??[]).join(", ")}, but it is now a plain config value that starts empty. Add those IDs back to \`config.auth.socialProviders\` in ${ct} or their sign-in buttons stop rendering.`},{key:"mcpServer",severity:"warn",preserved:e=>e.mcpServer!==!0,describe:()=>`Retired option \`mcpServer\`: your project recorded the outward MCP server ON, but its code now ships to every project behind a config gate generated OFF. Set \`config.mcpServer.enabled\` to \`true\` in ${ct} to keep your /mcp endpoint and its settings tab.`},{key:"emailTracking",severity:"ask",preserved:e=>e.emailTracking!==!1,describe:()=>`Retired option \`emailTracking\`: your project recorded email open/click tracking OFF, but it is now a plain config value generated ON. Applying this update as-is starts tracking opens and clicks for every user you mail. Put it back by setting \`config.email.tracking.enabled\` to \`false\` in ${ct}. How should this update proceed?`},{key:"aiBuiltin",severity:"warn",normalize:e=>{e.aiBuiltin===void 0&&e.aiByok!==void 0&&(e.aiBuiltin=e.aiByok===!1)},applies:e=>e.ai===!0,preserved:e=>e.aiBuiltin===At(e),describe:e=>`Retired option \`aiBuiltin\`: your project recorded built-in AI models ${Qi(e.aiBuiltin)}, but it is now a plain config value generated ${Qi(At(e))} for a project ${At(e)?"with":"without"} a payment provider. Set \`config.ai.builtin\` to \`${String(e.aiBuiltin===!0)}\` in ${ct} to keep your choice.`},{key:"aiByok",severity:"warn",applies:e=>e.ai===!0,preserved:e=>e.aiByok!==!1,describe:()=>`Retired option \`aiByok\`: your project recorded bring-your-own-key AI OFF, but it is now a plain config value generated ON. Set \`config.ai.byok\` to \`false\` in ${ct} to keep it off.`},{key:"dockerServices",severity:"warn",preserved:e=>es(e).join(",")===ts(e).join(","),describe:e=>`Retired option \`dockerServices\`: your project recorded ${es(e).join(", ")||"no services"}, but the local Docker services are now DERIVED from your stack - postgres when you self-host Postgres, redis when you self-host Redis, mailpit on SMTP, inngest always - so an inconsistent selection is no longer possible. Your regenerated \`infra/docker-compose.yml\` will contain ${ts(e).join(", ")}. Add any extra service back by hand; the file is yours to edit.`},{key:"aiTools",severity:"warn",preserved:e=>(e.aiTools??[]).length===Wr.length,describe:e=>`Retired option \`aiTools\`: your project recorded ${(e.aiTools??[]).join(", ")||"no AI tools"}, but the \`generatesaas-update\` skill bundle now ships to every AI-tool root (${Ue.join(", ")}) so it is there whichever assistant you switch to. Delete the roots you do not use - they are plain text files with no runtime path.`},{key:"blog",severity:"warn",preserved:e=>e.blog!==!1,describe:()=>`Retired option \`blog\`: your project recorded the blog OFF, but content is no longer an option. This update adds the \`/blog\`, \`/alternatives\` and \`/compare\` route code to your project; every one of them 404s until you add a file under \`packages/content/{locale}/{section}/\`, and none of them appears in your navbar, footer, sitemap or llms.txt while it is empty. Delete an entry from \`config.content.sections\` in ${ct} to drop a section for good.`}],_f=jr.map(e=>e.key);function rs(e){for(let t of jr)t.normalize?.(e)}function ns(e){let t=[];for(let r of jr){if(r.normalize?.(e),e[r.key]===void 0)continue;(!r.applies||r.applies(e))&&!r.preserved(e)&&t.push({key:r.key,message:r.describe(e),severity:r.severity}),delete e[r.key]}return t}function ss(e){e.command("update").description("Update AI skill files and stage template updates").argument("[mode]","pass 'auto' to mark the staged update for unattended apply by your AI assistant").option("--cwd <path>","project directory (default: current directory)").action(async(t,r)=>{let n=t==="auto"?"auto":void 0,i=ml(r.cwd??process.cwd()),o=Ke(i,ve),s;try{s=JSON.parse(await is(o,"utf-8"))}catch{y.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}s.frontend===sr&&(y.cancel(`This project was generated on the Nuxt frontend (\`.generatesaas/manifest.json\` records \`frontend: "${sr}"\`).
1572
+ `,replace:""},{find:"export async function captureAiGeneration(event: AiGenerationEvent): Promise<void> {",replace:"export async function captureAiGeneration(_event: AiGenerationEvent): Promise<void> {"},{find:" * @param event - The call to record.",replace:" * @param _event - Ignored: no PostHog adapter was generated for this project."}]}var Ki=[{file:"src/resolve-server.ts",edits:dl},{file:"src/resolve-browser.ts",edits:ul},{file:"src/llm-analytics.ts",edits:kl,dualMode:!1}],El=[{file:"src/resolve-desktop-main.ts",edits:hl},{file:"src/resolve-desktop-renderer.ts",edits:fl}];import{readdir as Sl,readFile as Pl}from"fs/promises";import{join as qi}from"path";async function Ji(e){if(e.demo)return;let t=e.appName.trim()||e.projectName,r=JSON.stringify(t).slice(1,-1),n=qi(e.projectDir,"packages/i18n/translations"),i;try{i=await Sl(n)}catch{return}for(let o of i){let s=qi(n,o,"web.json"),a;try{a=await Pl(s,"utf-8")}catch{continue}let d=a.replaceAll("GenerateSaaS",()=>r);d!==a&&await c(s,d)}}async function ir(e){let t=e.projectDir;e.demo||(await Nn(t),await Cn(t),await Ln(t),await $n(t)),await jn(t),await Un(e),await Vn(e),await Bn(e),e.demo||await Gn(e),await Yn(e),await Wn(e),await zn(e),await Zn(e),await ei(e),await ri(e),await li(e),await di(e),await hi(e),await wi(e),await Li(e),await vi(e),await ki(e),await Ei(e),await Si(e),await Pi(e),await Ai(e),await Ii(e),await Oi(e),await Ri(e),await Ti(e),await Di(e),await Mr(e),await Mi(e),await Fi(e),await Yi(e),await ai(e),await Ji(e),await gi(e)}import{basename as Zi,join as Qi,relative as Il}from"path";import{createHash as Xi}from"crypto";import{readFile as Al}from"fs/promises";async function sr(e){let t=await Al(e);return Xi("sha256").update(t).digest("hex")}function Br(e){return Xi("sha256").update(e).digest("hex")}var Rl=new Set(["data",Q]);function Tl(e){let t=e.split("/");for(let r of t)if(Sr.has(r)||Pr.has(r)||Rl.has(r)||r.startsWith(".env")&&!r.includes("example"))return!0;return!1}function es(e,t){return{projectName:e.projectName??Zi(t),appName:e.appName??e.projectName??Zi(t),projectDir:t,architecture:e.architecture??"fullstack",paymentProvider:e.paymentProvider??"none",emailProvider:e.emailProvider??"smtp",multiTenancy:e.multiTenancy??!1,projects:e.projects??!1,docs:e.docs??!1,desktop:e.desktop??!1,mobile:e.mobile??!1,mobilePurchases:e.mobilePurchases??"none",ai:e.ai??!1,webAi:e.webAi??!0,runner:e.runner??!1,revenueSharing:e.revenueSharing??!1,credits:e.credits??!1,defaultCurrency:e.defaultCurrency??"USD",deploymentTarget:e.deploymentTarget??"node",databaseProvider:e.databaseProvider??"postgres",cacheProvider:e.cacheProvider??"redis",observability:e.observability??[],version:e.version,baseUrl:e.baseUrl,credentials:{},demo:!1}}function _l(e,t){return{version:e.version,initialVersion:e.version,appName:e.appName,projectName:e.projectName,frontend:wn,architecture:e.architecture,paymentProvider:e.paymentProvider,emailProvider:e.emailProvider,multiTenancy:e.multiTenancy,projects:e.projects,docs:e.docs,desktop:e.desktop,mobile:e.mobile,...e.mobile&&oe(e)?{mobilePurchases:e.mobilePurchases}:{},ai:e.ai,webAi:e.webAi,runner:e.runner,credits:e.credits,revenueSharing:e.revenueSharing,defaultCurrency:e.defaultCurrency,deploymentTarget:e.deploymentTarget,databaseProvider:e.databaseProvider,cacheProvider:e.cacheProvider,observability:e.observability,...e.baseUrl?{baseUrl:e.baseUrl}:{},...t&&{licenseToken:t.token,licenseKeyHash:t.keyHash,installId:t.installId}}}async function ts(e,t){let n=(await ke(e.projectDir,e.projectDir,Tl)).sort(),i=await Promise.all(n.map(async a=>[Ve(Il(e.projectDir,a)),await sr(a)])),o=Object.fromEntries(i),s=_l(e,t);await c(Qi(e.projectDir,Ae),JSON.stringify(s,null," ")+`
1573
+ `),await c(Qi(e.projectDir,vn),JSON.stringify(o,null," ")+`
1574
+ `)}import{relative as Ol}from"path";async function dt(e){let r=(await ke(e,e,st)).sort(),n=await Promise.all(r.map(async i=>[Ve(Ol(e,i)),await sr(i)]));return Object.fromEntries(n)}import{copyFile as xl,mkdir as Dl,rm as Nl}from"fs/promises";import{dirname as Cl,join as rs,relative as Ll}from"path";import{existsSync as $l}from"fs";async function Gr(e,t){$l(t)&&await Nl(t,{recursive:!0,force:!0});let n=(await ke(e,e,st)).map(i=>({file:i,dest:rs(t,Ve(Ll(e,i)))}));await Promise.all([...new Set(n.map(({dest:i})=>Cl(i)))].map(i=>Dl(i,{recursive:!0}))),await Promise.all(n.map(({file:i,dest:o})=>xl(i,o)))}async function ns(e,t){await Gr(e,rs(t,Ft))}import{readFile as ss,readdir as jl}from"fs/promises";import{existsSync as Ml}from"fs";import{join as pe,dirname as Ul,resolve as Kr,sep as Hr}from"path";import{fileURLToPath as Fl}from"url";var Vl={"claude-code":".claude/skills",cursor:".cursor/skills",codex:".agents/skills","gemini-cli":".gemini/skills",windsurf:".windsurf/skills"},Be=Object.values(Vl),Yr=["generatesaas-update"],Bl=Ul(Fl(import.meta.url));function Gl(){return Kt(Bl,["skill","content"],"content")}async function Wr(e,t){for(let r of Be){let n=pe(e,r);for(let[i,o]of Object.entries(t)){let s=Kr(n,i);if(!s.startsWith(n+Hr))continue;let a=pe(s,"scripts"),d=pe(s,"references");await Yt(a),await Yt(d),await c(pe(s,"SKILL.md"),o.skillMd.replaceAll("__SKILL_ROOT__",r));for(let[w,y]of Object.entries(o.scripts)){let P=Kr(a,w);P.startsWith(a+Hr)&&await c(P,y)}let u=Object.entries(o.references);u.length===0&&await c(pe(d,".gitkeep"),"");for(let[w,y]of u){let P=Kr(d,w);P.startsWith(d+Hr)&&await c(P,y)}}}}async function is(e){if(!Ml(e))return{};let t={};for(let r of await jl(e))r!==".gitkeep"&&(t[r]=await ss(pe(e,r),"utf-8"));return t}async function os(e){let t=Gl(),r={};for(let n of Yr){let i=pe(t,n);r[n]={skillMd:await ss(pe(i,"SKILL.md"),"utf-8"),scripts:await is(pe(i,"scripts")),references:await is(pe(i,"references"))}}await Wr(e,r)}import{execFile as Kl,execFileSync as Hl}from"child_process";import{access as as,readFile as Yl}from"fs/promises";import{join as zr}from"path";import*as M from"@clack/prompts";function Ke(e){try{let t=process.platform==="win32"?"where":"which";return Hl(t,[e],{stdio:"ignore"}),!0}catch{return!1}}function Ge(e,t,r,n=3e5){return new Promise((i,o)=>{Kl(e,t,{cwd:r,timeout:n,maxBuffer:50*1024*1024},(s,a,d)=>{if(s){let u=String(a||"").trim(),y=[String(d||"").trim(),u].filter(Boolean).join(`
1575
+ `);o(new Error(y?`${s.message}
1576
+ ${y}`:s.message))}else i()})})}async function cs(e){if(!Ke("pnpm"))return M.log.warn("pnpm not found. Skipping lockfile regeneration."),!1;try{return await Ge("pnpm",["install","--lockfile-only","--no-frozen-lockfile","--config.minimumReleaseAge=0"],e),!0}catch(t){let r=t instanceof Error?t.message:String(t);return M.log.warn(`Lockfile regeneration failed: ${r}`),M.log.warn("Deploys using --frozen-lockfile may fail."),!1}}async function ls(e){if(!Ke("pnpm"))return M.log.warn("pnpm not found. Skipping dependency installation."),M.log.info("Install pnpm: https://pnpm.io/installation"),!1;let t=M.spinner();t.start("Installing dependencies (this may take a minute)...");try{return await Ge("pnpm",["install","--config.minimumReleaseAge=0"],e),t.stop("Dependencies installed."),!0}catch(r){t.stop("Dependency installation failed.");let n=r instanceof Error?r.message:String(r);return M.log.warn(`pnpm install failed: ${n}`),M.log.warn("You can run it manually later."),!1}}async function ps(e){if(!Ke("pnpm"))return!1;let t=M.spinner();t.start("Generating baseline database migration...");try{return await Ge("pnpm",["-F","@repo/database","generate"],e),t.stop("Baseline migration generated."),!0}catch(r){t.stop("Baseline migration generation failed.");let n=r instanceof Error?r.message:String(r);return M.log.warn(`Could not generate baseline migration: ${n}`),M.log.warn("Run 'pnpm -F @repo/database generate' before your first deploy."),!1}}async function ds(e){try{return await as(zr(e,".git")),M.log.info("Git repository already exists, skipping init."),!0}catch{}if(!Ke("git"))return M.log.warn("git not found. Skipping repository initialization."),!1;let t=M.spinner();t.start("Initializing git repository...");try{return await Ge("git",["init"],e),await Ge("git",["add","-A"],e),await Ge("git",["commit","--no-verify","-m","Initial commit from GenerateSaaS"],e),t.stop("Git repository initialized."),!0}catch{return t.stop("Git initialization failed."),M.log.warn("You can run git init manually later."),!1}}async function us(e){if(!Ke("pnpm"))return!1;try{await as(zr(e,".git"))}catch{return!1}try{let t=JSON.parse(await Yl(zr(e,"package.json"),"utf-8")),r=!!t.devDependencies?.["simple-git-hooks"],n=!!t["simple-git-hooks"];if(!r||!n)return!1}catch{return!1}try{return await Ge("pnpm",["exec","simple-git-hooks"],e),!0}catch{return M.log.warn("Could not install git hooks. Run 'pnpm exec simple-git-hooks' manually."),!1}}import*as He from"@clack/prompts";import re from"picocolors";function ms(e,t){let r=Qe(e);t.dockerAvailable||He.log.warn("Docker not found. Install Docker to run local services: https://docs.docker.com/get-docker/"),t.baselineMigrationMissing&&He.log.warn("No database migration was generated (the install did not complete). Run `pnpm install` then `pnpm --filter @repo/database generate` and commit the result BEFORE deploying - otherwise your first deploy pushes the schema untracked.");let n=[];n.push(`cd ${e.projectDir}`),t.pnpmInstalled||n.push("pnpm install"),n.push(`pnpm infra ${re.dim(`# ${r.map(a=>sn[a].label).join(", ")}`)}`),n.push(`pnpm dev ${re.dim("# http://localhost:3000")}`),t.skippedCredentials.length>0&&(n.push(""),n.push(re.dim("Fill in remaining TODO values in .env"))),He.note(n.join(`
1577
+ `),re.yellow("Start Development"));let i=[];i.push(`App ${re.cyan("http://localhost:3000")}`),e.architecture==="separate"&&i.push(`API ${re.cyan("http://localhost:3010")}`),r.includes("mailpit")&&i.push(`Mailpit ${re.cyan("http://localhost:8025")}`),r.includes("inngest")&&i.push(`Inngest ${re.cyan("http://localhost:8288")}`),He.note(i.join(`
1578
+ `),re.yellow("Dev Tools"));let o=[],s=Wl(e);s.length>0&&o.push(`Set in production: ${re.dim(s.join(", "))}`),o.push("pnpm db:setup # Apply the database schema"),o.push(zl(e)),He.note(o.join(`
1579
+ `),re.yellow("Deployment"))}function Wl(e){let t=["DATABASE_URL","BETTER_AUTH_SECRET","INNGEST_APP_ID","INNGEST_EVENT_KEY","INNGEST_BASE_URL"];return e.cacheProvider==="upstash"?t.push("UPSTASH_REDIS_REST_URL","UPSTASH_REDIS_REST_TOKEN"):t.push("REDIS_URL"),e.paymentProvider==="stripe"?t.push("STRIPE_SECRET_KEY","STRIPE_WEBHOOK_SECRET"):e.paymentProvider==="polar"&&t.push("POLAR_ACCESS_TOKEN","POLAR_WEBHOOK_SECRET"),e.emailProvider==="ses"?t.push("AMAZON_SES_REGION","AMAZON_SES_KEY","AMAZON_SES_SECRET"):e.emailProvider==="resend"?t.push("RESEND_API_KEY"):t.push("SMTP_HOST","SMTP_PORT"),t}function zl(e){switch(e.deploymentTarget){case"node":return"Deploy with Docker or your preferred Node.js host";case"vercel":return"vercel deploy # Deploy to Vercel"}}function ut(e){let r=(e.startsWith("v")?e.slice(1):e).match(/^(\d+)\.(\d+)\.(\d+)$/);return r?[Number(r[1]),Number(r[2]),Number(r[3])]:null}function Ye(e,t){let r=ut(e),n=ut(t);if(!r||!n)return 0;for(let i=0;i<3;i++)if(r[i]!==n[i])return r[i]-n[i];return 0}function or(e,t){return e.versions.find(r=>r.version===t)?.cli}function hs(e){return process.env.GENERATESAAS_TEMPLATE_TARBALL||process.env.GENERATESAAS_TEMPLATE_TARBALL_DIR||process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?!0:ut(e)===null}function ar(e,t,r="3.7.0"){if(hs(r))return{ok:!0};let n=or(e,e.latest);return!n||ut(n)===null?{ok:!0}:Ye(r,n)>=0?{ok:!0}:{ok:!1,message:`Your installed generatesaas CLI (v${r}) is too old for template v${e.latest}, which needs CLI v${n} or newer. Run npx generatesaas@latest ${t} so npm fetches the current CLI. If the release happened minutes ago, npm may still be propagating it - retry shortly.`}}function fs(e,t,r="3.7.0"){if(hs(r))return{ok:!0};let n=or(e,t);return!n||ut(n)===null?{ok:!0}:Ye(r,n)===0?{ok:!0}:{ok:!1,message:`Template v${t} must be scaffolded by CLI v${n} exactly (the CLI whose generators produced that release). Run npx generatesaas@${n} init.`}}import ep from"picocolors";var tp="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";async function rp(e){let t=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(`generatesaas-demo:${e}`)),r=[...new Uint8Array(t).subarray(0,16)].map(n=>n.toString(16).padStart(2,"0")).join("");return`${r.slice(0,8)}-${r.slice(8,12)}-${r.slice(12,16)}-${r.slice(16,20)}-${r.slice(20,32)}`}function gs(e){e.command("init").description("Scaffold a new GenerateSaaS project").argument("[apiKey]","license key (same as --api-key)").option("-n, --name <name>","project name (lowercase, hyphens, starts with letter)").option("--app-name <name>","display name for the app").option("-l, --location <path>","project directory (default: ./{name})").addOption(new ne("--architecture <type>","fullstack or separate").choices([...et])).addOption(new ne("--payment <provider>","payment provider").choices([...tt])).addOption(new ne("--email <provider>","email provider").choices([...rt])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").option("--projects","enable projects (a switcher that scopes a user's work)").option("--no-projects","disable projects").option("--docs","include the docs app (apps/docs, Fumadocs)").option("--no-docs","exclude the docs app").option("--desktop","include the desktop app (apps/desktop, built with Electron)").option("--no-desktop","exclude the desktop app").option("--mobile","include the mobile app (apps/mobile, built with Expo)").option("--no-mobile","exclude the mobile app").addOption(new ne("--mobile-purchases <lane>","in-app purchases in the mobile app: revenuecat adds the store billing lane on top of your web payment provider, none keeps web billing only").choices([...Me])).option("--ai","include the AI features (chat, models, automations, integrations); unlocks the runner").option("--no-ai","exclude the AI features").option("--web-ai","show the AI features in the web app (the default)").option("--no-web-ai","desktop-first: keep the AI features out of the web app, which is reduced to account, login and billing - the desktop or mobile app is the AI product. Requires --desktop or --mobile, plus --ai, and cannot be combined with --runner").option("--runner","include the runner feature - your web users connect a machine that runs their own AI subscriptions for this app").option("--no-runner","exclude the backend runner feature").option("--revenue-sharing","enable revenue sharing").option("--no-revenue-sharing","disable revenue sharing").option("--credits","enable credits system").option("--no-credits","disable credits system (subscription-only)").addOption(new ne("--currency <code>","default currency for billing").choices([...fe])).addOption(new ne("--preset <preset>","infrastructure preset: serverless (Vercel + Neon + Upstash) or selfhost (Docker or Node + Postgres + Redis); --deploy/--database/--cache override it piecewise").choices([...je])).addOption(new ne("--deploy <target>","deployment target").choices([...ge])).addOption(new ne("--database <provider>","database provider").choices([...ye])).addOption(new ne("--cache <provider>","cache provider").choices([...be])).option("--observability <providers>",`comma-separated observability providers (${K.join(", ")}); anything unselected ships no SDK`).option("--api-key <key>","API key (skips interactive prompt)").option("--base-url <url>","public base URL (e.g. https://example.com) - bakes into canonical/og/sitemap").option("-y, --yes","accept defaults for unspecified options (non-interactive)").addOption(new ne("--demo","first-party demo build: keep sample content, mark site non-indexable - requires CI API key").hideHelp()).addOption(new ne("--no-db-migration","skip generating the baseline DB migration (internal: demos/CI/playground)").hideHelp()).action(async(t,r)=>{await np(t?{...r,apiKey:t}:r)})}async function np(e){let t=performance.now();rn("3.7.0");let r;try{r=gn(e)}catch(v){k.cancel(j(v)),process.exit(1)}let n=k.spinner(),i;try{i=await it({apiKey:e.apiKey,prompt:!e.yes})}catch(v){k.cancel(j(v)),process.exit(1)}e.demo&&Br(i)!==tp&&(k.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let o=ve(i),s,a=async()=>{s=await Te(o);let v=ar(s,"init");if(!v.ok)throw new Error(v.message);return{latestVersion:s.latest,selectedVersion:s.latest,desktopAllowed:s.entitlements?.desktopAllowed??!0,runnerAllowed:s.entitlements?.runnerAllowed??!0,mobileAllowed:s.entitlements?.mobileAllowed??!0}};n.start("Verifying access...");let d,u,w=!0,y=!0,P=!0;try{({latestVersion:d,selectedVersion:u,desktopAllowed:w,runnerAllowed:y,mobileAllowed:P}=await a()),n.stop("Access verified."),Fe(i)}catch(v){if(n.stop("Access verification failed."),v instanceof F&&v.status===401){e.yes&&(k.cancel("Invalid API key. Cannot prompt in non-interactive mode."),process.exit(1)),k.log.warning("Invalid API key."),i=await kt(),o=ve(i),n.start("Verifying access...");try{({latestVersion:d,selectedVersion:u,desktopAllowed:w,runnerAllowed:y,mobileAllowed:P}=await a()),n.stop("Access verified."),Fe(i)}catch(U){n.stop("Access verification failed."),k.cancel(U instanceof F&&U.status===401?"Invalid API key.":j(U)),process.exit(1)}}else k.cancel(j(v)),process.exit(1)}k.log.success(`Latest version: ${d}`),r.desktop===!0&&!w&&(k.cancel("The desktop app is available on the Pro plan and up. Upgrade your plan at generatesaas.com."),process.exit(1)),r.runner===!0&&!y&&(k.cancel("The runner is available on the Pro plan and up. Upgrade your plan at generatesaas.com."),process.exit(1)),r.mobile===!0&&!P&&(k.cancel("The mobile app is available on the Pro plan and up. Upgrade your plan at generatesaas.com."),process.exit(1));let I;e.yes?I=yn({...r,...w?{}:{desktop:!1},...y?{}:{runner:!1},...P?{}:{mobile:!1}}):I=await bn(r,{desktopAllowed:w,runnerAllowed:y,mobileAllowed:P});let R;n.start("Activating license...");try{let v=e.demo&&I.baseUrl?await rp(I.baseUrl):crypto.randomUUID(),U=()=>({frontend:vr,version:u,installId:v,projectName:I.projectName,options:An(I)}),G;try{G=await kr(o,U())}catch(se){let C=Vt(se);if(!C?.lastAllowedVersion||!s)throw se;n.stop("License activation failed.");let de=fs(s,C.lastAllowedVersion);if(de.ok||(k.cancel(`${C.message} ${de.message}`),process.exit(1)),e.yes){let ue=or(s,C.lastAllowedVersion);k.cancel(`${C.message} Re-run ${ue?`npx generatesaas@${ue} init`:"init"} without --yes to continue with v${C.lastAllowedVersion} (the last version your license covers).`),process.exit(1)}let X=await k.confirm({message:`Your update window has ended. Continue with v${C.lastAllowedVersion} (the last version your license covers)?`});(k.isCancel(X)||!X)&&(k.cancel("Setup cancelled."),process.exit(0)),u=C.lastAllowedVersion,n.start(`Activating license for v${u}...`),G=await kr(o,U())}R={token:G.token,keyHash:Br(i),installId:G.installId??v},n.stop("License activated.")}catch(v){n.stop("License activation failed."),k.cancel(j(v)),process.exit(1)}let f=Ql(I.projectDir);if(ql(f)&&Jl(f).length>0)if(e.yes)k.log.info(`Directory ${f} is not empty. Merging (keeping existing files, overwriting conflicts).`);else{let U=await k.select({message:`Directory ${f} is not empty.`,options:[{value:"merge",label:"Merge",hint:"keep existing files, overwrite conflicts"},{value:"overwrite",label:"Overwrite",hint:"delete everything and start fresh"},{value:"cancel",label:"Cancel"}]});(k.isCancel(U)||U==="cancel")&&(k.cancel("Setup cancelled."),process.exit(0)),U==="overwrite"&&Xl(f,{recursive:!0,force:!0})}let O={...I,projectDir:f,version:u,...e.demo?{docs:!1}:{}};n.start("Downloading template...");try{await Bt(o,u,f),n.stop("Template downloaded.")}catch(v){n.stop("Download failed."),k.cancel(j(v)),process.exit(1)}n.start("Generating project files...");try{if(await ir(O),!e.demo){let v=await dt(f);await c(Zl(f,Ut),JSON.stringify(v,null," ")+`
1580
+ `),await ns(f,f)}await os(f),await ts(O,R),n.stop("Project files generated.")}catch(v){n.stop("Generation failed."),k.cancel(j(v)),process.exit(1)}!await cs(f)&&O.demo===!0&&(k.cancel("Lockfile regeneration failed on a demo build - refusing to ship a stale lockfile."),process.exit(1));let q=await ls(f),J=O.demo!==!0&&e.dbMigration!==!1,Oe=!1;J&&q&&(await ps(f),Oe=!0),await ds(f),q&&await us(f);let xe=Ke("docker"),Je=yr(O).map(v=>v.key).filter(v=>!O.credentials?.[v]);ms(O,{pnpmInstalled:q,dockerAvailable:xe,skippedCredentials:Je,baselineMigrationMissing:J&&!Oe}),nn(),k.log.info(ep.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as cr}from"fs";import{readFile as Es}from"fs/promises";import{join as We,resolve as cp}from"path";import*as b from"@clack/prompts";import D from"picocolors";import{mkdtemp as ip,rm as sp}from"fs/promises";import{tmpdir as op}from"os";import{join as ap}from"path";async function qr(e,t,r,n){let i=await ip(ap(op(),"generatesaas-stage-"));try{await Bt(e,t,i),await ir({...r,projectDir:i}),await Gr(i,n)}finally{await sp(i,{recursive:!0,force:!0})}}function ys(e){return e?"ON":"OFF"}var mt="packages/config/src/index.ts";function bs(e){let t=e.databaseProvider!==void 0&&e.databaseProvider!=="postgres",r=e.cacheProvider!==void 0&&e.cacheProvider!=="redis";return[...e.dockerServices??[]].filter(n=>!(n==="postgres"&&t)&&!(n==="redis"&&r)).sort()}function ws(e){return[...Qe({databaseProvider:e.databaseProvider??"postgres",cacheProvider:e.cacheProvider??"redis",emailProvider:e.emailProvider??"smtp"})].sort()}var Jr=[{key:"errorTracking",severity:"warn",normalize:e=>{e.errorTracking===void 0||e.observability!==void 0||(e.observability=e.errorTracking==="none"?[]:[e.errorTracking])},preserved:()=>!0,describe:()=>"Retired option `errorTracking`: it is now `observability`, an array, because a project may run more than one provider at once. Your recorded choice was migrated across automatically."},{key:"companion",severity:"warn",normalize:e=>{e.companion===void 0||e.runner!==void 0||(e.runner=e.companion)},preserved:()=>!0,describe:()=>"Retired option `companion`: it is now `runner`, because the daemon it names is AgentRunner. Your recorded choice was migrated across automatically - the feature itself is unchanged."},{key:"companionSource",severity:"warn",preserved:()=>!1,describe:e=>`Retired option \`companionSource\`: your project recorded a \`${String(e.companionSource)}\` daemon source. There is now one runner daemon - AgentRunner - which we publish and your users install from its own releases. Your project keeps the runner feature and its own pairing, dispatch and Runners page; it no longer holds or brands a daemon.`},{key:"companionName",severity:"warn",preserved:()=>!1,describe:e=>`Retired option \`companionName\`: your project recorded a branded daemon called ${e.companionName}. Buyers no longer brand or publish a daemon - your users install AgentRunner, which we maintain. Their existing install of your old daemon keeps running until they switch.`},{key:"companionRepoUrl",severity:"warn",preserved:()=>!1,describe:e=>`Retired option \`companionRepoUrl\`: your project pointed its install flow at ${e.companionRepoUrl}. It now points at AgentRunner, the one daemon we publish.`},{key:"companionLocalPath",severity:"warn",preserved:e=>(e.companionLocalPath??"").trim()==="",describe:e=>`Retired option \`companionLocalPath\`: your project recorded a local daemon checkout at ${e.companionLocalPath}. The option is gone, and so is the choice behind it - there is one runner daemon (AgentRunner) and we publish it, so nothing in your project is built from a local checkout any more.`},{key:"desktopAutoRelease",severity:"warn",applies:e=>e.desktop===!0,preserved:e=>e.desktopAutoRelease!==!0,describe:()=>"Retired option `desktopAutoRelease`: your project recorded automatic desktop releases, but the release trigger is no longer an option - cutting a release publishes signed installers from your repository and spends your Actions minutes, so it is always deliberate now. The staged `.github/workflows/desktop-release.yml` runs on `workflow_dispatch` only (the Actions tab). Add a `workflow_run` block to it if you want the CI-chained trigger back."},{key:"billingScope",severity:"ask",applies:e=>e.multiTenancy===!0,preserved:e=>e.billingScope!=="organization",describe:()=>"Retired option `billingScope`: your project recorded organization-scoped billing, and organization billing is GONE - not moved to config, removed. Money now lives only on a USER: an organization has no balance, no plan and no payment-provider customer of its own, and a workspace is funded by that organization's OWNER, whose personal balance every credit spent in it is drawn from. Payments already in flight are safe - a webhook arriving with old organization metadata is credited to that organization's owner rather than dropped - but a STORED balance is not: the `credits` and `plan` columns an organization holds them in are going away, and this update does NOT migrate them for you. MOVE every paying organization's credits and plan onto its owner BEFORE you take new checkouts, or that balance goes with the columns. How should this update proceed?"},{key:"socialProviders",severity:"warn",preserved:e=>(e.socialProviders??[]).length===0,describe:e=>`Retired option \`socialProviders\`: your project recorded ${(e.socialProviders??[]).join(", ")}, but it is now a plain config value that starts empty. Add those IDs back to \`config.auth.socialProviders\` in ${mt} or their sign-in buttons stop rendering.`},{key:"mcpServer",severity:"warn",preserved:e=>e.mcpServer!==!0,describe:()=>`Retired option \`mcpServer\`: your project recorded the outward MCP server ON, but its code now ships to every project behind a config gate generated OFF. Set \`config.mcpServer.enabled\` to \`true\` in ${mt} to keep your /mcp endpoint and its settings tab.`},{key:"emailTracking",severity:"ask",preserved:e=>e.emailTracking!==!1,describe:()=>`Retired option \`emailTracking\`: your project recorded email open/click tracking OFF, but it is now a plain config value generated ON. Applying this update as-is starts tracking opens and clicks for every user you mail. Put it back by setting \`config.email.tracking.enabled\` to \`false\` in ${mt}. How should this update proceed?`},{key:"aiBuiltin",severity:"warn",normalize:e=>{e.aiBuiltin===void 0&&e.aiByok!==void 0&&(e.aiBuiltin=e.aiByok===!1)},applies:e=>e.ai===!0,preserved:e=>e.aiBuiltin===oe(e),describe:e=>`Retired option \`aiBuiltin\`: your project recorded built-in AI models ${ys(e.aiBuiltin)}, but it is now a plain config value generated ${ys(oe(e))} for a project ${oe(e)?"with":"without"} a payment provider. Set \`config.ai.builtin\` to \`${String(e.aiBuiltin===!0)}\` in ${mt} to keep your choice.`},{key:"aiByok",severity:"warn",applies:e=>e.ai===!0,preserved:e=>e.aiByok!==!1,describe:()=>`Retired option \`aiByok\`: your project recorded bring-your-own-key AI OFF, but it is now a plain config value generated ON. Set \`config.ai.byok\` to \`false\` in ${mt} to keep it off.`},{key:"dockerServices",severity:"warn",preserved:e=>bs(e).join(",")===ws(e).join(","),describe:e=>`Retired option \`dockerServices\`: your project recorded ${bs(e).join(", ")||"no services"}, but the local Docker services are now DERIVED from your stack - postgres when you self-host Postgres, redis when you self-host Redis, mailpit on SMTP, inngest always - so an inconsistent selection is no longer possible. Your regenerated \`infra/docker-compose.yml\` will contain ${ws(e).join(", ")}. Add any extra service back by hand; the file is yours to edit.`},{key:"aiTools",severity:"warn",preserved:e=>(e.aiTools??[]).length===an.length,describe:e=>`Retired option \`aiTools\`: your project recorded ${(e.aiTools??[]).join(", ")||"no AI tools"}, but the \`generatesaas-update\` skill bundle now ships to every AI-tool root (${Be.join(", ")}) so it is there whichever assistant you switch to. Delete the roots you do not use - they are plain text files with no runtime path.`},{key:"blog",severity:"warn",preserved:e=>e.blog!==!1,describe:()=>`Retired option \`blog\`: your project recorded the blog OFF, but content is no longer an option. This update adds the \`/blog\`, \`/alternatives\` and \`/compare\` route code to your project; every one of them 404s until you add a file under \`packages/content/{locale}/{section}/\`, and none of them appears in your navbar, footer, sitemap or llms.txt while it is empty. Delete an entry from \`config.content.sections\` in ${mt} to drop a section for good.`}],Mf=Jr.map(e=>e.key);function vs(e){for(let t of Jr)t.normalize?.(e)}function ks(e){let t=[];for(let r of Jr){if(r.normalize?.(e),e[r.key]===void 0)continue;(!r.applies||r.applies(e))&&!r.preserved(e)&&t.push({key:r.key,message:r.describe(e),severity:r.severity}),delete e[r.key]}return t}function Ss(e){e.command("update").description("Update AI skill files and stage template updates").argument("[mode]","pass 'auto' to mark the staged update for unattended apply by your AI assistant").option("--cwd <path>","project directory (default: current directory)").action(async(t,r)=>{let n=t==="auto"?"auto":void 0,i=cp(r.cwd??process.cwd()),o=We(i,Ae),s;try{s=JSON.parse(await Es(o,"utf-8"))}catch{b.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}s.frontend===br&&(b.cancel(`This project was generated on the Nuxt frontend (\`.generatesaas/manifest.json\` records \`frontend: "${br}"\`).
1414
1581
 
1415
- v${or} is the last release that ships a Nuxt frontend, and this CLI cannot update or migrate a Nuxt project to Next.js.
1582
+ v${wr} is the last release that ships a Nuxt frontend, and this CLI cannot update or migrate a Nuxt project to Next.js.
1416
1583
 
1417
- Your project keeps working exactly as it is - stay on v${or}, or start a fresh project on Next.js with ${T.cyan("generatesaas init")} when you are ready to move.`),process.exit(1));let a=Xi(s);a.length>0&&(y.cancel(`.generatesaas/manifest.json carries a value this CLI cannot shape a project from:
1584
+ Your project keeps working exactly as it is - stay on v${wr}, or start a fresh project on Next.js with ${D.cyan("generatesaas init")} when you are ready to move.`),process.exit(1));let a=ln(s);a.length>0&&(b.cancel(`.generatesaas/manifest.json carries a value this CLI cannot shape a project from:
1418
1585
 
1419
1586
  ${a.map(f=>` - ${f}`).join(`
1420
1587
  `)}
1421
1588
 
1422
- Correct it there, then run ${T.cyan("generatesaas update")} again.`),process.exit(1)),rs(s);let p=ne(s);p&&(y.cancel(`.generatesaas/manifest.json turns \`${p.feature}\` on with \`deploymentTarget: "${p.target}"\`, which cannot run it.
1589
+ Correct it there, then run ${D.cyan("generatesaas update")} again.`),process.exit(1)),vs(s);let d=me(s);d&&(b.cancel(`.generatesaas/manifest.json turns \`${d.feature}\` on with \`deploymentTarget: "${d.target}"\`, which cannot run it.
1423
1590
 
1424
- ${p.reason}
1591
+ ${d.reason}
1425
1592
 
1426
- Correct it there, then run ${T.cyan("generatesaas update")} again.`),process.exit(1));let u=rr(s);u.length>0&&(y.cancel(`.generatesaas/manifest.json records a combination this project cannot run:
1593
+ Correct it there, then run ${D.cyan("generatesaas update")} again.`),process.exit(1));let u=mr(s);u.length>0&&(b.cancel(`.generatesaas/manifest.json records a combination this project cannot run:
1427
1594
 
1428
1595
  ${u.map(f=>` - ${f}`).join(`
1429
1596
  `)}
1430
1597
 
1431
- Correct it there, then run ${T.cyan("generatesaas update")} again.`),process.exit(1));let v=Xr(s);v.length>0&&(y.cancel(`.generatesaas/manifest.json turns the web AI surface off (\`webAi: false\`) in a project that cannot run without it:
1598
+ Correct it there, then run ${D.cyan("generatesaas update")} again.`),process.exit(1));let w=mn(s);w.length>0&&(b.cancel(`.generatesaas/manifest.json turns the web AI surface off (\`webAi: false\`) in a project that cannot run without it:
1599
+
1600
+ ${w.map(f=>` - ${f}`).join(`
1601
+ `)}
1602
+
1603
+ Correct it there, then run ${D.cyan("generatesaas update")} again.`),process.exit(1));let y=hn(s);y.length>0&&(b.cancel(`.generatesaas/manifest.json turns on mobile store purchases (\`mobilePurchases: "revenuecat"\`) in a project that cannot run them:
1432
1604
 
1433
- ${v.map(f=>` - ${f}`).join(`
1605
+ ${y.map(f=>` - ${f}`).join(`
1434
1606
  `)}
1435
1607
 
1436
- Correct it there, then run ${T.cyan("generatesaas update")} again.`),process.exit(1));let g;try{g=await et()}catch(f){y.cancel(N(f)),process.exit(1)}let w=pe(g),P=y.spinner();try{P.start("Verifying access...");let f;try{f=await ke(w)}catch(I){throw I instanceof j&&I.status===401?new Error("Your saved API key was rejected. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):I}let L=Xt(f,"update");if(!L.ok)throw new Error(L.message);P.stop("Access verified."),Ne(g),P.start("Fetching latest skill files...");let M=await cn(w,f.latest);await Nr(i,M.skills);let fe=Ue;if(P.stop("Skills updated."),y.log.success(`Skill files installed to ${T.cyan(fe.length.toString())} locations.`),s.version===f.latest){y.log.info(`Already on the latest version (${s.version}).`);return}let Z=f.entitlements;Z&&(s.desktop&&!Z.desktopAllowed&&(y.cancel("This project includes the desktop app, which is available on the Pro plan and up. Upgrade your plan at generatesaas.com to keep updating it."),process.exit(1)),s.runner&&!Z.runnerAllowed&&(y.cancel("This project includes the runner, which is available on the Pro plan and up. Upgrade your plan at generatesaas.com to keep updating it."),process.exit(1)));let he=JSON.stringify(s);Jr(s),await fl(s,Zr(s),n),await gl(ns(s),n);let Se=Ji(s),Ae=Zi(s),Pe=rr(Ae);Pe.length>0&&(y.cancel(`.generatesaas/manifest.json is missing a provider key, and its default would create a combination this project cannot run:
1608
+ Correct it there, then run ${D.cyan("generatesaas update")} again.`),process.exit(1));let P;try{P=await it()}catch(f){b.cancel(j(f)),process.exit(1)}let I=ve(P),R=b.spinner();try{R.start("Verifying access...");let f;try{f=await Te(I)}catch(N){throw N instanceof F&&N.status===401?new Error("Your saved API key was rejected. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):N}let O=ar(f,"update");if(!O.ok)throw new Error(O.message);R.stop("Access verified."),Fe(P),R.start("Fetching latest skill files...");let _e=await Pn(I,f.latest);await Wr(i,_e.skills);let q=Be;if(R.stop("Skills updated."),b.log.success(`Skill files installed to ${D.cyan(q.length.toString())} locations.`),s.version===f.latest){b.log.info(`Already on the latest version (${s.version}).`);return}let J=f.entitlements;J&&(s.desktop&&!J.desktopAllowed&&(b.cancel("This project includes the desktop app, which is available on the Pro plan and up. Upgrade your plan at generatesaas.com to keep updating it."),process.exit(1)),s.runner&&!J.runnerAllowed&&(b.cancel("This project includes the runner, which is available on the Pro plan and up. Upgrade your plan at generatesaas.com to keep updating it."),process.exit(1)),s.mobile&&J.mobileAllowed===!1&&(b.cancel("This project includes the mobile app, which is available on the Pro plan and up. Upgrade your plan at generatesaas.com to keep updating it."),process.exit(1)));let Oe=JSON.stringify(s);un(s),await lp(s,fn(s),n),await dp(ks(s),n);let xe=cn(s),De=pn(s),Je=mr(De);Je.length>0&&(b.cancel(`.generatesaas/manifest.json is missing a provider key, and its default would create a combination this project cannot run:
1437
1609
 
1438
- ${Pe.map(I=>` - ${I}`).join(`
1610
+ ${Je.map(N=>` - ${N}`).join(`
1439
1611
  `)}
1440
1612
 
1441
- Add the key to .generatesaas/manifest.json with a value your deployment target supports, then run ${T.cyan("generatesaas update")} again.`),process.exit(1));let b=JSON.stringify(Ae)!==he;if(s=Ae,s.licenseToken)try{let I=await pn(w,{currentToken:s.licenseToken,newVersion:f.latest});s.licenseToken=I.token,I.licenseKeyHash&&(s.licenseKeyHash=I.licenseKeyHash),await l(o,JSON.stringify(s,null," ")+`
1442
- `),b=!1,y.log.success("License refreshed.")}catch(I){let Ie=Nt(I);Ie&&(y.cancel(Ie.message),process.exit(1)),y.log.warn("License refresh skipped.")}b&&await l(o,JSON.stringify(s,null," ")+`
1443
- `);let D=Ni(s,i),$=Ke(i,sn);P.start(`Staging v${f.latest} (shaped for your config)...`),await Lr(w,f.latest,D,$),P.stop("Template staged.");let{text:ge,title:F}=await wl(w,f,s.version);ge&&y.note(ge,F);let V=Ke(i,xt),B=Ke(i,Dt),Re=!Zt(B),Ye=!Zt(V);if(Re){if(P.start("Building baseline template (one-time migration)..."),await Lr(w,s.version,D,B),Ye){let I=await ot(B);await l(V,JSON.stringify(I,null," ")+`
1444
- `)}if(P.stop("Baseline template stored."),!Ye){let I=await bl(V,B);I>0&&y.log.warn(`Rebuilt baseline differs from the original for ${I} file(s) (the CLI's shaping evolved since this project was scaffolded). Classification still follows the committed template-hashes.json; upstream diffs for those files may include unrelated noise.`)}}else if(Ye){P.start("Computing baseline template hashes...");let I=await ot(B);await l(V,JSON.stringify(I,null," ")+`
1445
- `),P.stop("Baseline hashes computed.")}await l(Ke(i,on),JSON.stringify({currentVersion:s.version,targetVersion:f.latest,changelog:ge,stagedAt:new Date().toISOString(),...Se.length>0?{newOptions:Se}:{},...n?{mode:n}:{}},null," ")+`
1446
- `),y.log.info(`Update staged: ${T.cyan(s.version)} \u2192 ${T.cyan(f.latest)}`),y.log.info(`Ask your AI coding assistant to ${T.cyan("'update my GenerateSaaS project'")}.`),s.desktop===!0&&Zt(Ke(i,"apps/tauri"))&&y.log.warn(`Desktop: the app moved to an Electron shell (${T.cyan("apps/desktop")}). Your project still has the old Tauri shell (${T.cyan("apps/tauri")}) - remove it after verifying the Electron app builds and runs. See ${T.cyan("/desktop/updates")}.`),s.desktop===!0&&Zt(Ke(i,"apps/electron"))&&y.log.warn(`Desktop: the app now lives at ${T.cyan("apps/desktop")}. Your project still has the old ${T.cyan("apps/electron")} directory - move your local changes across, remove it, then run ${T.cyan("pnpm install")}. See ${T.cyan("/desktop/updates")}.`)}catch(f){P.stop("Failed."),y.cancel(`Update failed: ${N(f)}`),process.exit(1)}})}async function fl(e,t,r){if(t.kind==="none")return;let n=vl[t.kind];r==="auto"&&(y.cancel(`${n.message}
1613
+ Add the key to .generatesaas/manifest.json with a value your deployment target supports, then run ${D.cyan("generatesaas update")} again.`),process.exit(1));let v=JSON.stringify(De)!==Oe;if(s=De,s.licenseToken)try{let N=await In(I,{currentToken:s.licenseToken,newVersion:f.latest});s.licenseToken=N.token,N.licenseKeyHash&&(s.licenseKeyHash=N.licenseKeyHash),await c(o,JSON.stringify(s,null," ")+`
1614
+ `),v=!1,b.log.success("License refreshed.")}catch(N){let ht=Vt(N);ht&&(b.cancel(ht.message),process.exit(1)),b.log.warn("License refresh skipped.")}v&&await c(o,JSON.stringify(s,null," ")+`
1615
+ `);let U=es(s,i),G=We(i,kn);R.start(`Staging v${f.latest} (shaped for your config)...`),await qr(I,f.latest,U,G),R.stop("Template staged.");let{text:se,title:C}=await hp(I,f,s.version);se&&b.note(se,C);let de=We(i,Ut),X=We(i,Ft),ue=!cr(X),Ne=!cr(de);if(ue){if(R.start("Building baseline template (one-time migration)..."),await qr(I,s.version,U,X),Ne){let N=await dt(X);await c(de,JSON.stringify(N,null," ")+`
1616
+ `)}if(R.stop("Baseline template stored."),!Ne){let N=await fp(de,X);N>0&&b.log.warn(`Rebuilt baseline differs from the original for ${N} file(s) (the CLI's shaping evolved since this project was scaffolded). Classification still follows the committed template-hashes.json; upstream diffs for those files may include unrelated noise.`)}}else if(Ne){R.start("Computing baseline template hashes...");let N=await dt(X);await c(de,JSON.stringify(N,null," ")+`
1617
+ `),R.stop("Baseline hashes computed.")}await c(We(i,En),JSON.stringify({currentVersion:s.version,targetVersion:f.latest,changelog:se,stagedAt:new Date().toISOString(),...xe.length>0?{newOptions:xe}:{},...n?{mode:n}:{}},null," ")+`
1618
+ `),b.log.info(`Update staged: ${D.cyan(s.version)} \u2192 ${D.cyan(f.latest)}`),b.log.info(`Ask your AI coding assistant to ${D.cyan("'update my GenerateSaaS project'")}.`),s.desktop===!0&&cr(We(i,"apps/tauri"))&&b.log.warn(`Desktop: the app moved to an Electron shell (${D.cyan("apps/desktop")}). Your project still has the old Tauri shell (${D.cyan("apps/tauri")}) - remove it after verifying the Electron app builds and runs. See ${D.cyan("/desktop/updates")}.`),s.desktop===!0&&cr(We(i,"apps/electron"))&&b.log.warn(`Desktop: the app now lives at ${D.cyan("apps/desktop")}. Your project still has the old ${D.cyan("apps/electron")} directory - move your local changes across, remove it, then run ${D.cyan("pnpm install")}. See ${D.cyan("/desktop/updates")}.`)}catch(f){R.stop("Failed."),b.cancel(`Update failed: ${j(f)}`),process.exit(1)}})}async function lp(e,t,r){if(t.kind==="none")return;let n=mp[t.kind];r==="auto"&&(b.cancel(`${n.message}
1447
1619
 
1448
- This is a decision only you can make, so an unattended update will not guess it. Run ${T.cyan("generatesaas update")} without ${T.cyan("auto")} and answer it.`),process.exit(1));let i=await y.select({message:n.message,options:n.options});y.isCancel(i)&&(y.cancel("Update cancelled. Nothing was changed or staged."),process.exit(1)),i.apply(e)}var hl={billingScope:[{value:"stage",label:"Stage it - I will move each organization's balance to its owner myself",hint:"move every paying organization's credits and plan onto its owner before you take new checkouts - the columns holding them are going away, and a stored balance is not migrated for you"},{value:"stop",label:"Stop, and stage nothing",hint:"migrate those balances in your database first, then re-run update - this is buyer DATA, not a config line, so nothing the update stages can put it back"}],emailTracking:[{value:"stage",label:"Stage it - I will turn tracking back off myself",hint:"set `config.email.tracking.enabled` to false in packages/config/src/index.ts before your next send"},{value:"stop",label:"Stop, and stage nothing",hint:"edit packages/config/src/index.ts first, then re-run update - a file you have edited is walked as a 3-way merge instead of being replaced"}]};async function gl(e,t){let r=[];for(let n of e){let i=n.severity==="ask"?hl[n.key]:void 0;if(!i){y.log.warn(n.message);continue}r.push({notice:n,answers:i})}for(let{notice:n,answers:i}of r){t==="auto"&&(y.cancel(`${n.message}
1620
+ This is a decision only you can make, so an unattended update will not guess it. Run ${D.cyan("generatesaas update")} without ${D.cyan("auto")} and answer it.`),process.exit(1));let i=await b.select({message:n.message,options:n.options});b.isCancel(i)&&(b.cancel("Update cancelled. Nothing was changed or staged."),process.exit(1)),i.apply(e)}var pp={billingScope:[{value:"stage",label:"Stage it - I will move each organization's balance to its owner myself",hint:"move every paying organization's credits and plan onto its owner before you take new checkouts - the columns holding them are going away, and a stored balance is not migrated for you"},{value:"stop",label:"Stop, and stage nothing",hint:"migrate those balances in your database first, then re-run update - this is buyer DATA, not a config line, so nothing the update stages can put it back"}],emailTracking:[{value:"stage",label:"Stage it - I will turn tracking back off myself",hint:"set `config.email.tracking.enabled` to false in packages/config/src/index.ts before your next send"},{value:"stop",label:"Stop, and stage nothing",hint:"edit packages/config/src/index.ts first, then re-run update - a file you have edited is walked as a 3-way merge instead of being replaced"}]};async function dp(e,t){let r=[];for(let n of e){let i=n.severity==="ask"?pp[n.key]:void 0;if(!i){b.log.warn(n.message);continue}r.push({notice:n,answers:i})}for(let{notice:n,answers:i}of r){t==="auto"&&(b.cancel(`${n.message}
1449
1621
 
1450
- This is a decision only you can make, so an unattended update will not guess it. Run ${T.cyan("generatesaas update")} without ${T.cyan("auto")} and answer it.`),process.exit(1));let o=await y.select({message:n.message,options:i});(y.isCancel(o)||o==="stop")&&(y.cancel("Update cancelled. Nothing was changed or staged."),process.exit(1))}}function yl(e){e.desktop=!1}var vl={"legacy-desktop-ai":{message:"Your desktop app runs the local AI stack through the retired desktop-AI option. Desktop AI now follows the AI option. Keep it?",options:[{value:{apply:e=>{e.ai=!0}},label:"Turn AI on, keep the desktop AI stack",hint:"also makes your website's AI surface available - keep it hidden with config.ai.enabled: false"},{value:{apply:e=>{e.ai=!1}},label:"Turn desktop AI off",hint:"your desktop app becomes a client shell: no AI, no Terminal"}]},"desktop-gains-ai":{message:"Your desktop app has no AI today, but your project has the AI option on. With the desktop-AI option retired, AI on a desktop project now ships the app's local AI stack. Keep it?",options:[{value:{apply:()=>{}},label:"Keep AI - the desktop app gains the AI stack",hint:"adds the local AI stack and the Terminal to your desktop app"},{value:{apply:e=>{e.ai=!1,e.runner=!1}},label:"Drop AI",hint:"your website's AI surface hides, and this also drops the web runner - it requires AI"},{value:{apply:yl},label:"Drop the desktop app",hint:"your website keeps its AI surface exactly as it is today"}]}};async function wl(e,t,r){let n=t.latest,i=t.versions.filter(p=>Be(p.version,r)>0&&Be(p.version,n)<=0).sort((p,u)=>Be(p.version,u.version)),o=async p=>{try{return await an(e,p)}catch{return null}};if(i.length<=1)return{text:await o(n),title:`Changelog v${n}`};let s=await Promise.all(i.map(p=>o(p.version)));return{text:i.map((p,u)=>{let v=p.date?` (${p.date.slice(0,10)})`:"",g=s[u]??"_No changelog available for this release._";return`# v${p.version}${v}
1622
+ This is a decision only you can make, so an unattended update will not guess it. Run ${D.cyan("generatesaas update")} without ${D.cyan("auto")} and answer it.`),process.exit(1));let o=await b.select({message:n.message,options:i});(b.isCancel(o)||o==="stop")&&(b.cancel("Update cancelled. Nothing was changed or staged."),process.exit(1))}}function up(e){e.desktop=!1}var mp={"legacy-desktop-ai":{message:"Your desktop app runs the local AI stack through the retired desktop-AI option. Desktop AI now follows the AI option. Keep it?",options:[{value:{apply:e=>{e.ai=!0}},label:"Turn AI on, keep the desktop AI stack",hint:"also makes your website's AI surface available - keep it hidden with config.ai.enabled: false"},{value:{apply:e=>{e.ai=!1}},label:"Turn desktop AI off",hint:"your desktop app becomes a client shell: no AI, no Terminal"}]},"desktop-gains-ai":{message:"Your desktop app has no AI today, but your project has the AI option on. With the desktop-AI option retired, AI on a desktop project now ships the app's local AI stack. Keep it?",options:[{value:{apply:()=>{}},label:"Keep AI - the desktop app gains the AI stack",hint:"adds the local AI stack and the Terminal to your desktop app"},{value:{apply:e=>{e.ai=!1,e.runner=!1}},label:"Drop AI",hint:"your website's AI surface hides, and this also drops the web runner - it requires AI"},{value:{apply:up},label:"Drop the desktop app",hint:"your website keeps its AI surface exactly as it is today"}]}};async function hp(e,t,r){let n=t.latest,i=t.versions.filter(d=>Ye(d.version,r)>0&&Ye(d.version,n)<=0).sort((d,u)=>Ye(d.version,u.version)),o=async d=>{try{return await Sn(e,d)}catch{return null}};if(i.length<=1)return{text:await o(n),title:`Changelog v${n}`};let s=await Promise.all(i.map(d=>o(d.version)));return{text:i.map((d,u)=>{let w=d.date?` (${d.date.slice(0,10)})`:"",y=s[u]??"_No changelog available for this release._";return`# v${d.version}${w}
1451
1623
 
1452
- ${g}`}).join(`
1624
+ ${y}`}).join(`
1453
1625
 
1454
- `),title:`Changelog v${r} \u2192 v${n}`}}async function bl(e,t){let r=JSON.parse(await is(e,"utf-8")),n=await ot(t),i=0;for(let[o,s]of Object.entries(r))tt(o)||n[o]!==s&&i++;for(let o of Object.keys(n))o in r||i++;return i}import*as Y from"@clack/prompts";import me from"picocolors";import{readFile as kl}from"fs/promises";import{join as El,resolve as Sl}from"path";function os(e){e.command("status").description("Show project status and check for updates").option("--cwd <path>","project directory (default: current directory)").action(async t=>{let r=Sl(t.cwd??process.cwd()),n=El(r,ve),i;try{i=JSON.parse(await kl(n,"utf-8"))}catch{Y.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let o=[`Version: ${me.cyan(i.version)}`,i.deploymentTarget?`Deploy target: ${me.cyan(i.deploymentTarget)}`:null,i.databaseProvider?`Database: ${me.cyan(i.databaseProvider)}`:null,i.cacheProvider?`Cache: ${me.cyan(i.cacheProvider)}`:null].filter(Boolean).join(`
1455
- `);Y.note(o,me.bold("Project Status"));let s=Y.spinner();s.start("Checking for updates...");try{let a=await et(),p=pe(a),v=(await ke(p)).latest;i.version===v?(s.stop("Up to date."),Y.log.success(`Already on the latest version (${me.green(v)})`)):(s.stop("Update available."),Y.log.warning(`Update available: ${me.yellow(i.version)} \u2192 ${me.green(v)}`),Y.log.info(`Open this project in your AI coding agent and ask it to ${me.cyan("update my GenerateSaaS project")} - it fetches and applies the update for you.`))}catch(a){s.stop("Check failed."),a instanceof j&&a.status===401?Y.log.warning("Invalid API key. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):Y.log.warning(`Could not check for updates: ${N(a)}`)}})}import{readFile as Al}from"fs/promises";import*as x from"@clack/prompts";import O from"picocolors";function Pl(){return process.env.GENERATESAAS_API_KEY??mt()}function Rl(e){return{verdict:e.verdict,plan:e.license?.plan??e.domainInstalls.find(t=>t.ownerPlan)?.ownerPlan??null,mismatchDomain:e.install?.domain??null,ejectedAt:e.install?.ejectedAt??e.domainInstalls.find(t=>t.ejectedAt)?.ejectedAt??null}}function Il(e){return{verdict:e.verdict??"unknown",ejectedAt:e.ejectedAt??null}}function Tl(e){switch(e.verdict){case"licensed":return x.log.success(`${O.green("LICENSED")} - resolves to an account with an active${e.plan?` ${e.plan}`:""} license.`),!0;case"ejected":return x.log.success(`${O.green("EJECTED")} - a licensed buyer opted this install out of telemetry${e.ejectedAt?` on ${e.ejectedAt.slice(0,10)}`:""}. The site is legitimate.`),!0;case"revoked":return x.log.error(`${O.red("REVOKED")} - the owning account no longer holds a plan (refund or chargeback). This deployment is no longer licensed.`),!1;case"token_domain_mismatch":return x.log.error(`${O.red("LEAKED TOKEN")} - this license belongs to a different deployment${e.mismatchDomain?` (${O.cyan(e.mismatchDomain)})`:""}, not this site. The token was copied from a licensed project.`),!1;case"no_license_history":return x.log.error(`${O.red("NO LICENSE HISTORY")} - no license has ever been associated with this site. If it runs GenerateSaaS, treat it as unlicensed.`),!1;default:return x.log.warn(`${O.yellow("UNKNOWN")} - could not cross-reference the records right now. Try again shortly.`),!1}}async function as(e,t){let r=process.env.GENERATESAAS_API_URL??ut,n=Pl();e.start("Cross-referencing license records...");try{let i=n?Rl(await dn(r,n,{lkh:t.lkh,nid:t.nid,domain:t.domain})):Il(await lr(r,{token:t.token,domain:t.domain}));return e.stop(`${O.green("Checked")} - records cross-referenced`),Tl(i)}catch(i){return e.stop(`${O.yellow("Skipped")} - ${N(i)}`),null}}function _l(e){let t=e.split(".");if(t.length!==3||!t[1])throw new Error("Invalid JWT format");let r=Buffer.from(t[1],"base64url").toString("utf-8");return JSON.parse(r)}function Ur(e){return typeof e!="number"?"unknown":new Date(e*1e3).toISOString().split("T")[0]}function cs(e){x.note([`License ID: ${O.cyan(String(e.lid??"unknown"))}`,`Version: ${O.cyan(String(e.ver??"unknown"))}`,`Init version: ${String(e.iver??"unknown")}`,`Frontend: ${String(e.fe??"unknown")}`,`Created: ${Ur(e.pat)}`,`Last updated: ${Ur(e.uat)}`,`Expires: ${Ur(e.exp)}`,`Install ID: ${String(e.nid??"unknown")}`].join(`
1456
- `),O.yellow("License Details"))}function Ol(e){let r=(/^https?:\/\//i.test(e)?e:`https://${e}`).replace(/\/+$/,"");if(r.endsWith("/api"))return[`${r}/license`];try{if(new URL(r).pathname!=="/")return[`${r}/license`]}catch{return[`${r}/license`]}return[`${r}/api/license`,`${r}/license`]}async function ls(e){let t=x.spinner(),r=null,n="no candidates";for(let s of Ol(e)){t.start(`Checking ${s}...`);try{let a=await fetch(s);if(!a.ok){n=`${s} returned ${a.status}`,t.stop(`${O.yellow("Not here")} - ${n}`);continue}let p=(await a.text()).trim();if(!p||p.split(".").length!==3){n=`${s} did not return a JWT`,t.stop(`${O.yellow("Not here")} - ${n}`);continue}r=p,t.stop(`${O.green("Found")} - license endpoint responded`);break}catch(a){n=`${s}: ${N(a)}`,t.stop(`${O.yellow("Unreachable")} - ${n}`)}}if(r===null){x.log.warn(`No license endpoint found (last: ${n}). The site may be ejected, not a GenerateSaaS app, or serving its API elsewhere.`);let s=ps(e);return s?await as(t,{domain:s})??!1:!1}let i;try{i=_l(r)}catch{return x.log.error("Could not decode JWT payload."),!1}t.start("Verifying signature...");try{let s=process.env.GENERATESAAS_API_URL??ut,a=await lr(s,{token:r});if(a.valid)t.stop(`${O.green("Valid")} - signature verified`);else return t.stop(`${O.red("Invalid")} - ${a.reason}`),!1}catch{return t.stop(`${O.yellow("Skipped")} - could not reach verification service`),x.log.warn("Signature not verified. Displaying unverified claims:"),cs(i),!1}return cs(i),await as(t,{token:r,lkh:typeof i.lkh=="string"?i.lkh:void 0,nid:typeof i.nid=="string"?i.nid:void 0,domain:ps(e)})??!0}function ps(e){try{return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`).hostname}catch{return}}function ds(e){e.command("verify").description("Verify a GenerateSaaS license on a deployed site").argument("[url]","URL of the site to verify (e.g. https://example.com or https://example.com/api)").option("--file <path>","file with URLs to check, one per line").action(async(t,r)=>{if(!t&&!r.file&&(x.cancel("Provide a URL or --file <path>."),process.exit(1)),r.file){let i=(await Al(r.file,"utf-8")).split(`
1457
- `).map(s=>s.trim()).filter(s=>s&&!s.startsWith("#"));i.length===0&&(x.cancel("No URLs found in file."),process.exit(1));let o=0;for(let s of i)await ls(s)&&o++,x.log.info("");x.log.success(`${o}/${i.length} sites verified.`)}else await ls(t)||process.exit(1)})}import{existsSync as xl,rmSync as Dl}from"fs";import*as X from"@clack/prompts";function us(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){xl(le)?(Dl(le),X.log.success("API key removed.")):X.log.info("No API key configured.");return}let r=mt();r?X.log.info(`Current API key: ****${r.slice(-4)}`):X.log.info("No API key configured.");let n=await ft(),i=pe(n),o=X.spinner();o.start("Verifying API key...");try{await ke(i),o.stop("API key verified."),Ne(n),X.log.success("API key saved.")}catch(s){o.stop("Verification failed."),s instanceof j&&s.status===401?X.cancel("Invalid API key."):X.cancel(N(s)),process.exit(1)}})}import{existsSync as Qt,rmSync as Nl,readFileSync as Fr,writeFileSync as ms}from"fs";import{join as Ge}from"path";import*as U from"@clack/prompts";var Cl=["packages/api/src/functions/maintenance/license-heartbeat.ts","packages/api/src/lib/cron-spread.ts","packages/api/src/lib/manifest.ts","packages/api/src/routes/internal/license.ts"],Ll=[{file:"packages/api/src/routes/inngest.ts",removals:[`import { licenseHeartbeatFunction } from "../functions/maintenance/license-heartbeat";
1626
+ `),title:`Changelog v${r} \u2192 v${n}`}}async function fp(e,t){let r=JSON.parse(await Es(e,"utf-8")),n=await dt(t),i=0;for(let[o,s]of Object.entries(r))st(o)||n[o]!==s&&i++;for(let o of Object.keys(n))o in r||i++;return i}import*as z from"@clack/prompts";import Se from"picocolors";import{readFile as gp}from"fs/promises";import{join as yp,resolve as bp}from"path";function Ps(e){e.command("status").description("Show project status and check for updates").option("--cwd <path>","project directory (default: current directory)").action(async t=>{let r=bp(t.cwd??process.cwd()),n=yp(r,Ae),i;try{i=JSON.parse(await gp(n,"utf-8"))}catch{z.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let o=[`Version: ${Se.cyan(i.version)}`,i.deploymentTarget?`Deploy target: ${Se.cyan(i.deploymentTarget)}`:null,i.databaseProvider?`Database: ${Se.cyan(i.databaseProvider)}`:null,i.cacheProvider?`Cache: ${Se.cyan(i.cacheProvider)}`:null].filter(Boolean).join(`
1627
+ `);z.note(o,Se.bold("Project Status"));let s=z.spinner();s.start("Checking for updates...");try{let a=await it(),d=ve(a),w=(await Te(d)).latest;i.version===w?(s.stop("Up to date."),z.log.success(`Already on the latest version (${Se.green(w)})`)):(s.stop("Update available."),z.log.warning(`Update available: ${Se.yellow(i.version)} \u2192 ${Se.green(w)}`),z.log.info(`Open this project in your AI coding agent and ask it to ${Se.cyan("update my GenerateSaaS project")} - it fetches and applies the update for you.`))}catch(a){s.stop("Check failed."),a instanceof F&&a.status===401?z.log.warning("Invalid API key. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):z.log.warning(`Could not check for updates: ${j(a)}`)}})}import{readFile as wp}from"fs/promises";import*as $ from"@clack/prompts";import L from"picocolors";function vp(){return process.env.GENERATESAAS_API_KEY??vt()}function kp(e){return{verdict:e.verdict,plan:e.license?.plan??e.domainInstalls.find(t=>t.ownerPlan)?.ownerPlan??null,mismatchDomain:e.install?.domain??null,ejectedAt:e.install?.ejectedAt??e.domainInstalls.find(t=>t.ejectedAt)?.ejectedAt??null}}function Ep(e){return{verdict:e.verdict??"unknown",ejectedAt:e.ejectedAt??null}}function Sp(e){switch(e.verdict){case"licensed":return $.log.success(`${L.green("LICENSED")} - resolves to an account with an active${e.plan?` ${e.plan}`:""} license.`),!0;case"ejected":return $.log.success(`${L.green("EJECTED")} - a licensed buyer opted this install out of telemetry${e.ejectedAt?` on ${e.ejectedAt.slice(0,10)}`:""}. The site is legitimate.`),!0;case"revoked":return $.log.error(`${L.red("REVOKED")} - the owning account no longer holds a plan (refund or chargeback). This deployment is no longer licensed.`),!1;case"token_domain_mismatch":return $.log.error(`${L.red("LEAKED TOKEN")} - this license belongs to a different deployment${e.mismatchDomain?` (${L.cyan(e.mismatchDomain)})`:""}, not this site. The token was copied from a licensed project.`),!1;case"no_license_history":return $.log.error(`${L.red("NO LICENSE HISTORY")} - no license has ever been associated with this site. If it runs GenerateSaaS, treat it as unlicensed.`),!1;default:return $.log.warn(`${L.yellow("UNKNOWN")} - could not cross-reference the records right now. Try again shortly.`),!1}}async function As(e,t){let r=process.env.GENERATESAAS_API_URL??wt,n=vp();e.start("Cross-referencing license records...");try{let i=n?kp(await Rn(r,n,{lkh:t.lkh,nid:t.nid,domain:t.domain})):Ep(await Er(r,{token:t.token,domain:t.domain}));return e.stop(`${L.green("Checked")} - records cross-referenced`),Sp(i)}catch(i){return e.stop(`${L.yellow("Skipped")} - ${j(i)}`),null}}function Pp(e){let t=e.split(".");if(t.length!==3||!t[1])throw new Error("Invalid JWT format");let r=Buffer.from(t[1],"base64url").toString("utf-8");return JSON.parse(r)}function Xr(e){return typeof e!="number"?"unknown":new Date(e*1e3).toISOString().split("T")[0]}function Is(e){$.note([`License ID: ${L.cyan(String(e.lid??"unknown"))}`,`Version: ${L.cyan(String(e.ver??"unknown"))}`,`Init version: ${String(e.iver??"unknown")}`,`Frontend: ${String(e.fe??"unknown")}`,`Created: ${Xr(e.pat)}`,`Last updated: ${Xr(e.uat)}`,`Expires: ${Xr(e.exp)}`,`Install ID: ${String(e.nid??"unknown")}`].join(`
1628
+ `),L.yellow("License Details"))}function Ap(e){let r=(/^https?:\/\//i.test(e)?e:`https://${e}`).replace(/\/+$/,"");if(r.endsWith("/api"))return[`${r}/license`];try{if(new URL(r).pathname!=="/")return[`${r}/license`]}catch{return[`${r}/license`]}return[`${r}/api/license`,`${r}/license`]}async function Rs(e){let t=$.spinner(),r=null,n="no candidates";for(let s of Ap(e)){t.start(`Checking ${s}...`);try{let a=await fetch(s);if(!a.ok){n=`${s} returned ${a.status}`,t.stop(`${L.yellow("Not here")} - ${n}`);continue}let d=(await a.text()).trim();if(!d||d.split(".").length!==3){n=`${s} did not return a JWT`,t.stop(`${L.yellow("Not here")} - ${n}`);continue}r=d,t.stop(`${L.green("Found")} - license endpoint responded`);break}catch(a){n=`${s}: ${j(a)}`,t.stop(`${L.yellow("Unreachable")} - ${n}`)}}if(r===null){$.log.warn(`No license endpoint found (last: ${n}). The site may be ejected, not a GenerateSaaS app, or serving its API elsewhere.`);let s=Ts(e);return s?await As(t,{domain:s})??!1:!1}let i;try{i=Pp(r)}catch{return $.log.error("Could not decode JWT payload."),!1}t.start("Verifying signature...");try{let s=process.env.GENERATESAAS_API_URL??wt,a=await Er(s,{token:r});if(a.valid)t.stop(`${L.green("Valid")} - signature verified`);else return t.stop(`${L.red("Invalid")} - ${a.reason}`),!1}catch{return t.stop(`${L.yellow("Skipped")} - could not reach verification service`),$.log.warn("Signature not verified. Displaying unverified claims:"),Is(i),!1}return Is(i),await As(t,{token:r,lkh:typeof i.lkh=="string"?i.lkh:void 0,nid:typeof i.nid=="string"?i.nid:void 0,domain:Ts(e)})??!0}function Ts(e){try{return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`).hostname}catch{return}}function _s(e){e.command("verify").description("Verify a GenerateSaaS license on a deployed site").argument("[url]","URL of the site to verify (e.g. https://example.com or https://example.com/api)").option("--file <path>","file with URLs to check, one per line").action(async(t,r)=>{if(!t&&!r.file&&($.cancel("Provide a URL or --file <path>."),process.exit(1)),r.file){let i=(await wp(r.file,"utf-8")).split(`
1629
+ `).map(s=>s.trim()).filter(s=>s&&!s.startsWith("#"));i.length===0&&($.cancel("No URLs found in file."),process.exit(1));let o=0;for(let s of i)await Rs(s)&&o++,$.log.info("");$.log.success(`${o}/${i.length} sites verified.`)}else await Rs(t)||process.exit(1)})}import{existsSync as Ip,rmSync as Rp}from"fs";import*as ie from"@clack/prompts";function Os(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){Ip(we)?(Rp(we),ie.log.success("API key removed.")):ie.log.info("No API key configured.");return}let r=vt();r?ie.log.info(`Current API key: ****${r.slice(-4)}`):ie.log.info("No API key configured.");let n=await kt(),i=ve(n),o=ie.spinner();o.start("Verifying API key...");try{await Te(i),o.stop("API key verified."),Fe(n),ie.log.success("API key saved.")}catch(s){o.stop("Verification failed."),s instanceof F&&s.status===401?ie.cancel("Invalid API key."):ie.cancel(j(s)),process.exit(1)}})}import{existsSync as lr,rmSync as Tp,readFileSync as Qr,writeFileSync as xs}from"fs";import{join as ze}from"path";import*as B from"@clack/prompts";var _p=["packages/api/src/functions/maintenance/license-heartbeat.ts","packages/api/src/lib/cron-spread.ts","packages/api/src/lib/manifest.ts","packages/api/src/routes/internal/license.ts"],Op=[{file:"packages/api/src/routes/inngest.ts",removals:[`import { licenseHeartbeatFunction } from "../functions/maintenance/license-heartbeat";
1458
1630
  `,` licenseHeartbeatFunction,
1459
1631
  `]},{file:"packages/api/src/routes/internal/index.ts",removals:[`import licenseRoutes from "./license";
1460
1632
  `,` .route("/license", licenseRoutes)
1461
1633
  `]},{file:"packages/api/src/routes/public/v1/health.ts",removals:[`import { manifest } from "../../../lib/manifest";
1462
- `],replacements:[{from:'c.json({ status: "success" as const, version: manifest?.version })',to:'c.json({ status: "success" as const })'}]}];function $l(){return Ue.flatMap(e=>Dr.map(t=>Ge(e,t)))}function Mr(e){return Qt(e)?(Nl(e,{recursive:!0}),!0):!1}function jl(e,t,r=[]){if(!Qt(e))return!1;let n=Fr(e,"utf-8"),i=n;for(let o of t)i=i.replace(o,"");for(let{from:o,to:s}of r)i=i.replace(o,s);return i===n?!1:(ms(e,i,"utf-8"),!0)}function Ul(e){let t=Ge(e,".gitignore");if(!Qt(t))return!1;let r=Fr(t,"utf-8"),n=r.split(`
1634
+ `],replacements:[{from:'c.json({ status: "success" as const, version: manifest?.version })',to:'c.json({ status: "success" as const })'}]}];function xp(){return Be.flatMap(e=>Yr.map(t=>ze(e,t)))}function Zr(e){return lr(e)?(Tp(e,{recursive:!0}),!0):!1}function Dp(e,t,r=[]){if(!lr(e))return!1;let n=Qr(e,"utf-8"),i=n;for(let o of t)i=i.replace(o,"");for(let{from:o,to:s}of r)i=i.replace(o,s);return i===n?!1:(xs(e,i,"utf-8"),!0)}function Np(e){let t=ze(e,".gitignore");if(!lr(t))return!1;let r=Qr(t,"utf-8"),n=r.split(`
1463
1635
  `).filter(i=>!i.includes(".generatesaas")).join(`
1464
- `);return n===r?!1:(ms(t,n,"utf-8"),!0)}function fs(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=Ge(t,ve),n;try{n=JSON.parse(Fr(r,"utf-8"))}catch{U.cancel("No GenerateSaaS project found in this directory."),process.exit(1)}let i=await U.text({message:'Type "eject" to confirm (this cannot be undone):',validate:a=>{if(a!=="eject")return'Type "eject" to confirm, or press Ctrl+C to cancel.'}});if(U.isCancel(i)&&(U.cancel("Eject cancelled."),process.exit(0)),n.licenseToken)try{await fetch("https://generatesaas.com/api/v1/heartbeat",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${n.licenseToken}`},body:JSON.stringify({event:"eject",version:n.version,frontend:n.frontend}),signal:AbortSignal.timeout(5e3)}),U.log.info("Recorded the opt-out with generatesaas.com (final event - nothing is sent after this).")}catch{U.log.warn("Could not reach generatesaas.com to record the opt-out. Ejecting anyway.")}let o=[],s=[];for(let a of $l())Mr(Ge(t,a))&&o.push(a);for(let a of Cl)Mr(Ge(t,a))&&o.push(a);Mr(Ge(t,q))&&o.push(q+"/");for(let a of Ll){let p=Ge(t,a.file);jl(p,a.removals,a.replacements)?s.push(a.file):Qt(p)&&U.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}Ul(t)&&s.push(".gitignore");for(let a of o)U.log.info(`Deleted ${a}`);for(let a of s)U.log.info(`Modified ${a}`);U.log.success("Ejected successfully. This project is now fully standalone.")})}var He=new Ml().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("3.6.0").addHelpText("after",`
1636
+ `);return n===r?!1:(xs(t,n,"utf-8"),!0)}function Ds(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=ze(t,Ae),n;try{n=JSON.parse(Qr(r,"utf-8"))}catch{B.cancel("No GenerateSaaS project found in this directory."),process.exit(1)}let i=await B.text({message:'Type "eject" to confirm (this cannot be undone):',validate:a=>{if(a!=="eject")return'Type "eject" to confirm, or press Ctrl+C to cancel.'}});if(B.isCancel(i)&&(B.cancel("Eject cancelled."),process.exit(0)),n.licenseToken)try{await fetch("https://generatesaas.com/api/v1/heartbeat",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${n.licenseToken}`},body:JSON.stringify({event:"eject",version:n.version,frontend:n.frontend}),signal:AbortSignal.timeout(5e3)}),B.log.info("Recorded the opt-out with generatesaas.com (final event - nothing is sent after this).")}catch{B.log.warn("Could not reach generatesaas.com to record the opt-out. Ejecting anyway.")}let o=[],s=[];for(let a of xp())Zr(ze(t,a))&&o.push(a);for(let a of _p)Zr(ze(t,a))&&o.push(a);Zr(ze(t,Q))&&o.push(Q+"/");for(let a of Op){let d=ze(t,a.file);Dp(d,a.removals,a.replacements)?s.push(a.file):lr(d)&&B.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}Np(t)&&s.push(".gitignore");for(let a of o)B.log.info(`Deleted ${a}`);for(let a of s)B.log.info(`Modified ${a}`);B.log.success("Ejected successfully. This project is now fully standalone.")})}var qe=new Cp().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("3.7.0").addHelpText("after",`
1465
1637
  Examples:
1466
1638
  $ generatesaas init Interactive setup
1467
1639
  $ generatesaas init -n my-app -y Quick setup with defaults
1468
1640
  $ generatesaas status Check for updates
1469
1641
  $ generatesaas auth Set or update API key
1470
- `);zi(He);ss(He);os(He);ds(He);us(He);fs(He);He.parseAsync().catch(e=>{hs.cancel("An unexpected error occurred."),console.error(e),process.exit(1)});
1642
+ `);gs(qe);Ss(qe);Ps(qe);_s(qe);Os(qe);Ds(qe);qe.parseAsync().catch(e=>{Ns.cancel("An unexpected error occurred."),console.error(e),process.exit(1)});