generatesaas 3.17.1 → 3.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +46 -46
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import{Command as Ip}from"commander";import*as vs from"@clack/prompts";import{existsSync as Ml,readdirSync as Fl,rmSync as Vl}from"fs";import{join as Bl,resolve as Gl}from"path";import{Option as X}from"commander";import*as S from"@clack/prompts";import*as kt from"@clack/prompts";import Zt from"picocolors";function Vr(e){let t=e?` GenerateSaaS v${e} `:" GenerateSaaS ";kt.intro(Zt.bgYellow(Zt.black(t)))}function Br(){kt.outro(Zt.yellow("Happy building!"))}import*as f from"@clack/prompts";import y from"picocolors";var lt={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)"}},Et={stripe:{label:"Stripe"},polar:{label:"Polar"},none:{label:"None",hint:"disable payments"}},St={smtp:{label:"SMTP",hint:"Mailpit for local dev"},ses:{label:"Amazon SES"},resend:{label:"Resend"}},Pt={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"}},Gr={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 _e={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 te={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}},Oe={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"}]}},xe={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"}]}},qe=[{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."}],Je=[{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)."}],le={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"}}},De=Object.keys(le);function Qt(e){let t=[];for(let r of qe)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 Je)e.deploymentTarget===r.target&&e.architecture===r.architecture&&t.push(`\`architecture: "${r.architecture}"\` with \`deploymentTarget: "${r.target}"\` - ${r.reason}`);return t}var Kr=["Local file storage (sharp, geoip-lite)","SMTP email (use Resend or SES instead)","Content API git integration"];function Xe(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 Ze=["fullstack","separate"],Qe=["stripe","polar","none"],et=["smtp","ses","resend"];var Hr=["claude-code","cursor","codex","gemini-cli","windsurf"],pe=["USD","EUR","GBP","CAD","AUD","BRL","JPY"],de=["node","vercel"],ue=["postgres","neon","supabase"],me=["redis","upstash"],F=["sentry","posthog"],Ne=["revenuecat","none"];function re(e){return e.paymentProvider!==void 0&&e.paymentProvider!=="none"}var er=[{key:"architecture",label:"Architecture",hint:"Fullstack (frontend hosts the API) or a standalone Hono backend.",category:"Infrastructure",kind:"enum",default:"fullstack",choices:Ze,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:de,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:ue,adoptable:!1,impact:"config",shapesTree:!0},{key:"cacheProvider",label:"Cache provider",hint:"Self-hosted Redis or Upstash.",category:"Infrastructure",kind:"enum",default:"redis",choices:me,adoptable:!1,impact:"config",shapesTree:!0},{key:"paymentProvider",label:"Payment provider",hint:"Stripe, Polar, or none.",category:"Features",kind:"enum",default:"none",choices:Qe,adoptable:!1,impact:"structural",shapesTree:!0},{key:"defaultCurrency",label:"Default currency",hint:"Billing and pricing currency.",category:"Features",kind:"enum",default:"USD",choices:pe,adoptable:!1,impact:"config",shapesTree:!1},{key:"emailProvider",label:"Email provider",hint:"SMTP, Amazon SES, or Resend.",category:"Features",kind:"enum",default:"smtp",choices:et,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:F,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:Ne,adoptable:!0,impact:"structural",shapesTree:!0,requires:e=>e.mobile===!0&&re(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). 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.",category:"Features",kind:"boolean",default:!0,adoptable:!0,impact:"config",shapesTree:!1,requires:e=>e.ai===!0&&(e.desktop===!0||e.mobile===!0),requiresLabel:"requires AI features and an app to host them (the desktop app or the mobile app)"},{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=>re(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 ks(e,t){return e[t]!==void 0}function Yr(e){return er.filter(t=>t.adoptable&&!ks(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 Es(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 Ss(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 Wr(e){let t=[];for(let r of er){let n=e[r.key];n!==void 0&&(Es(r,n)||t.push(`\`${r.key}\` is ${JSON.stringify(n)}, but it must be ${Ss(r)}.`))}return t}function zr(e){let t={};for(let r of er)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 tr(e,t){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||t}function At(e){return e.split("-").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function It(e){return/^[a-z][a-z0-9-]*$/.test(e)}function N(e){return e instanceof Error?e.message:String(e)}function pt(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 dt(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.");return e}function qr(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."),t}function Jr(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`."),re(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 Xr(e){let t=Ps(e,e.desktopAi);return delete e.desktopAi,t}function Ps(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 Zr(e){let t={};if(e.name!==void 0){if(!It(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(!Ne.includes(e.mobilePurchases))throw new Error(`Invalid mobile purchases "${e.mobilePurchases}". Valid values: ${Ne.join(", ")}`);t.mobilePurchases=e.mobilePurchases}if(e.ai!==void 0&&(t.ai=e.ai),e.webAi!==void 0&&(t.webAi=e.webAi),pt(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(!pe.includes(e.currency))throw new Error(`Invalid currency "${e.currency}". Valid values: ${pe.join(", ")}`);t.defaultCurrency=e.currency}if(e.deploy!==void 0){if(!de.includes(e.deploy))throw new Error(`Invalid deployment target "${e.deploy}". Valid values: ${de.join(", ")}`);t.deploymentTarget=e.deploy}if(e.database!==void 0){if(!ue.includes(e.database))throw new Error(`Invalid database provider "${e.database}". Valid values: ${ue.join(", ")}`);t.databaseProvider=e.database}if(e.cache!==void 0){if(!me.includes(e.cache))throw new Error(`Invalid cache provider "${e.cache}". Valid values: ${me.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=>!F.includes(i));if(n.length>0)throw new Error(`Invalid observability provider ${JSON.stringify(n.join(", "))}. Valid values: ${F.join(", ")}`);t.observability=r}if(e.preset!==void 0){if(!De.includes(e.preset))throw new Error(`Invalid preset "${e.preset}". Valid values: ${De.join(", ")}`);let r=le[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 tt={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,revenueSharing:!1,credits:!0,defaultCurrency:"USD",deploymentTarget:"node",databaseProvider:"postgres",cacheProvider:"redis",observability:[]};function Qr(e){let t=e.projectName??tt.projectName,r=e.projectDir??`./${t}`,n=e.appName??At(t),i=e.deploymentTarget??tt.deploymentTarget,o=te[i]?.edgeRuntime??!1,s=e.databaseProvider??(o?"neon":tt.databaseProvider),a=e.cacheProvider??(o?"upstash":tt.cacheProvider),d=e.emailProvider??(o?"resend":tt.emailProvider),h={...tt,...e,projectName:t,appName:n,projectDir:r,deploymentTarget:i,databaseProvider:s,cacheProvider:a,emailProvider:d};h.paymentProvider==="none"&&(h.credits=!1),(!h.mobile||h.paymentProvider==="none")&&(h.mobilePurchases="none"),pt(h);for(let g of qe){if(h.deploymentTarget!==g.target)continue;let k=h.databaseProvider===g.provider?"database":"cache";if(h.databaseProvider===g.provider||h.cacheProvider===g.provider)throw new Error(`Incompatible: --deploy ${g.target} + --${k} ${g.provider}. ${g.reason}`)}for(let g of Je)if(h.architecture===g.architecture&&h.deploymentTarget===g.target)throw new Error(`Incompatible: --architecture ${g.architecture} + --deploy ${g.target}. ${g.reason}`);return dt(h),h}function I(e){f.isCancel(e)&&(f.cancel("Setup cancelled."),process.exit(0))}function rr(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 en(e,t){let r=!1;e&&pt(e),e&&dt(e),f.log.info(y.bold("Project"));let n=e?.projectName??await(async()=>{r=!0;let c=await f.text({message:"Project name:",placeholder:"my-saas",validate:u=>{if(!u?.trim())return"Project name is required.";if(!It(u))return"Use lowercase letters, numbers, and hyphens only. Must start with a letter."}});return I(c),c})(),i=e?.appName??await(async()=>{r=!0;let c=await f.text({message:"App name:",initialValue:At(n),validate:u=>{if(!u?.trim())return"App name is required."}});return I(c),c})(),o=e?.projectDir??await(async()=>{r=!0;let c=await f.text({message:"Project location:",initialValue:`./${n}`});return I(c),c==="."?process.cwd():c})();f.log.info(y.bold("Infrastructure"));let s={};if(e?.deploymentTarget===void 0&&e?.databaseProvider===void 0&&e?.cacheProvider===void 0){r=!0;let c=await f.select({message:"Infrastructure:",options:[...De.map(u=>({value:u,label:le[u].label,hint:le[u].hint})),{value:"custom",label:"Customize",hint:"choose each piece yourself"}]});if(I(c),c!=="custom"){let u=le[c].values;s.deploymentTarget=u.deploymentTarget,s.databaseProvider=u.databaseProvider,s.cacheProvider=u.cacheProvider,f.log.info(`${le[c].label}: ${le[c].hint}.`)}}let a=e?.deploymentTarget??s.deploymentTarget??"node";if(e?.deploymentTarget===void 0&&s.deploymentTarget===void 0){r=!0;let c=await f.select({message:"Deployment target:",options:de.map(u=>({value:u,label:te[u].label,hint:te[u].hint}))});I(c),a=c}let d=e?.architecture?Je.find(c=>c.architecture===e.architecture&&c.target===a):void 0;if(d)throw new Error(`Incompatible: --architecture ${d.architecture} + --deploy ${d.target}. ${d.reason}`);let h=new Set(Je.filter(c=>c.target===a).map(c=>c.architecture)),g=Ze.filter(c=>!h.has(c)),k=e?.architecture??await(async()=>{if(g.length===1){let u=g[0];return f.log.info(`Auto-selected ${lt[u].label} architecture (only compatible option for ${te[a].label}).`),u}r=!0;let c=await f.select({message:"Architecture:",options:g.map(u=>({value:u,label:lt[u].label,hint:lt[u].hint}))});return I(c),c})(),v=e?.databaseProvider??s.databaseProvider??await(async()=>{r=!0;let c=ue.filter(R=>!qe.some(Ee=>Ee.target===a&&Ee.provider===R));if(c.length===1){let R=c[0];return f.log.info(`Auto-selected ${Oe[R].label} (only compatible option for ${te[a].label}).`),R}let u=await f.select({message:"Database provider:",options:c.map(R=>({value:R,label:Oe[R].label,hint:Oe[R].hint}))});return I(u),u})(),P=e?.cacheProvider??s.cacheProvider??await(async()=>{r=!0;let c=me.filter(R=>!qe.some(Ee=>Ee.target===a&&Ee.provider===R));if(c.length===1){let R=c[0];return f.log.info(`Auto-selected ${xe[R].label} (only compatible option for ${te[a].label}).`),R}let u=await f.select({message:"Cache provider:",options:c.map(R=>({value:R,label:xe[R].label,hint:xe[R].hint}))});return I(u),u})();if(te[a]?.edgeRuntime){let c=Kr.map(u=>` - ${u}`).join(`
3
- `);f.note(c,"Unavailable on edge runtime")}f.log.info(y.bold("Features"));let m=e?.paymentProvider??await(async()=>{r=!0;let c=await f.select({message:"Payment provider:",options:Qe.map(u=>({value:u,label:Et[u].label,hint:Et[u].hint}))});return I(c),c})(),D=e?.defaultCurrency??await(async()=>{if(m==="none")return"USD";r=!0;let c=await f.select({message:"Default currency:",options:pe.map(u=>({value:u,label:u,hint:_e[u].name}))});return I(c),c})(),ve=e?.emailProvider??await(async()=>{r=!0;let c=await f.select({message:"Email provider:",options:et.map(u=>({value:u,label:St[u].label,hint:St[u].hint}))});return I(c),c})(),G=e?.observability??await(async()=>{r=!0;let c=await f.multiselect({message:"Observability (space to select, enter to confirm):",required:!1,initialValues:[],options:F.map(u=>({value:u,label:Pt[u].label,hint:Pt[u].hint}))});return I(c),c})(),ae=e?.multiTenancy??await(async()=>{r=!0;let c=await f.confirm({message:"Enable multi-tenancy (organizations)?",initialValue:!1});return I(c),c})(),Te=e?.projects??await(async()=>{r=!0;let c=await f.confirm({message:"Enable projects (a switcher that scopes a user's work)?",initialValue:!1});return I(c),c})(),Re=e?.docs??await(async()=>{r=!0;let c=await f.confirm({message:"Include docs app? (self-hosted Fumadocs documentation site)",initialValue:!1});return I(c),c})(),ze=t?.desktopAllowed!==!1,ce=e?.desktop??(ze?await(async()=>{r=!0;let c=await f.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 I(c),c})():(f.log.info(y.dim("Desktop app: available on the Pro plan and up - skipped.")),!1)),E=t?.mobileAllowed!==!1,_=e?.mobile??(E?await(async()=>{r=!0;let c=await f.confirm({message:"Include the mobile app? (apps/mobile - iOS and Android, built with Expo, with push notifications and universal links)",initialValue:!1});return I(c),c})():(f.log.info(y.dim("Mobile app: available on the Pro plan and up - skipped.")),!1)),Q=!_||m==="none"?"none":e?.mobilePurchases??await(async()=>{r=!0;let R=await f.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 I(R),R})();pt({mobile:_,mobilePurchases:Q,paymentProvider:m});let K=e?.ai??await(async()=>{r=!0;let c=await f.confirm({message:"Add AI features? (chat, models, automations, integrations)",initialValue:!1});return I(c),c})(),j=e?.webAi??((ce||_)&&K?await(async()=>{r=!0;let c=await f.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 I(c),c})():!0);dt({webAi:j,ai:K,desktop:ce,mobile:_});let ee=e?.revenueSharing??await(async()=>{r=!0;let c=await f.confirm({message:"Enable revenue sharing? (opt-in MRR leaderboard with dofollow backlinks)",initialValue:!1});return I(c),c})(),H=m==="none"?!1:e?.credits??await(async()=>{r=!0;let c=await f.confirm({message:"Enable credits? (metered usage on top of subscription plans)",initialValue:!0});return I(c),c})(),we=e?.demo,ke=rr({databaseProvider:v,cacheProvider:P,paymentProvider:m,emailProvider:ve,demo:we}),A={};if(ke.length>0&&r){f.log.info(y.bold("Credentials")+y.dim(" all optional - press Enter to skip, fill in .env later"));for(let c of ke)if(r=!0,c.secret){let u=await f.password({message:c.message,mask:"*"});I(u),typeof u=="string"&&u.trim()&&(A[c.key]=u.trim())}else{let u=await f.text({message:c.message,placeholder:c.placeholder});I(u),typeof u=="string"&&u.trim()&&(A[c.key]=u.trim())}}if(r){let c=[` Name: ${y.cyan(n)}`,` App name: ${y.cyan(i)}`,` Location: ${y.cyan(o)}`,` Architecture: ${y.cyan(lt[k].label)}`].join(`
2
+ import{Command as Ip}from"commander";import*as vs from"@clack/prompts";import{existsSync as Ul,readdirSync as Fl,rmSync as Vl}from"fs";import{join as Bl,resolve as Kl}from"path";import{Option as X}from"commander";import*as S from"@clack/prompts";import*as kt from"@clack/prompts";import Zt from"picocolors";function Vr(e){let t=e?` GenerateSaaS v${e} `:" GenerateSaaS ";kt.intro(Zt.bgYellow(Zt.black(t)))}function Br(){kt.outro(Zt.yellow("Happy building!"))}import*as f from"@clack/prompts";import y from"picocolors";var lt={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)"}},Et={stripe:{label:"Stripe"},polar:{label:"Polar"},none:{label:"None",hint:"disable payments"}},St={smtp:{label:"SMTP",hint:"Mailpit for local dev"},ses:{label:"Amazon SES"},resend:{label:"Resend"}},Pt={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"}},Kr={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 _e={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 te={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}},Oe={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"}]}},xe={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"}]}},qe=[{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."}],Je=[{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)."}],le={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"}}},De=Object.keys(le);function Qt(e){let t=[];for(let r of qe)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 Je)e.deploymentTarget===r.target&&e.architecture===r.architecture&&t.push(`\`architecture: "${r.architecture}"\` with \`deploymentTarget: "${r.target}"\` - ${r.reason}`);return t}var Gr=["Local file storage (sharp, geoip-lite)","SMTP email (use Resend or SES instead)","Content API git integration"];function Xe(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 Ze=["fullstack","separate"],Qe=["stripe","polar","none"],et=["smtp","ses","resend"];var Hr=["claude-code","cursor","codex","gemini-cli","windsurf"],pe=["USD","EUR","GBP","CAD","AUD","BRL","JPY"],de=["node","vercel"],ue=["postgres","neon","supabase"],me=["redis","upstash"],F=["sentry","posthog"],Ne=["revenuecat","none"];function re(e){return e.paymentProvider!==void 0&&e.paymentProvider!=="none"}var er=[{key:"architecture",label:"Architecture",hint:"Fullstack (frontend hosts the API) or a standalone Hono backend.",category:"Infrastructure",kind:"enum",default:"fullstack",choices:Ze,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:de,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:ue,adoptable:!1,impact:"config",shapesTree:!0},{key:"cacheProvider",label:"Cache provider",hint:"Self-hosted Redis or Upstash.",category:"Infrastructure",kind:"enum",default:"redis",choices:me,adoptable:!1,impact:"config",shapesTree:!0},{key:"paymentProvider",label:"Payment provider",hint:"Stripe, Polar, or none.",category:"Features",kind:"enum",default:"none",choices:Qe,adoptable:!1,impact:"structural",shapesTree:!0},{key:"defaultCurrency",label:"Default currency",hint:"Billing and pricing currency.",category:"Features",kind:"enum",default:"USD",choices:pe,adoptable:!1,impact:"config",shapesTree:!1},{key:"emailProvider",label:"Email provider",hint:"SMTP, Amazon SES, or Resend.",category:"Features",kind:"enum",default:"smtp",choices:et,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:F,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:Ne,adoptable:!0,impact:"structural",shapesTree:!0,requires:e=>e.mobile===!0&&re(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). 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.",category:"Features",kind:"boolean",default:!0,adoptable:!0,impact:"config",shapesTree:!1,requires:e=>e.ai===!0&&(e.desktop===!0||e.mobile===!0),requiresLabel:"requires AI features and an app to host them (the desktop app or the mobile app)"},{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=>re(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 ks(e,t){return e[t]!==void 0}function Yr(e){return er.filter(t=>t.adoptable&&!ks(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 Es(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 Ss(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 Wr(e){let t=[];for(let r of er){let n=e[r.key];n!==void 0&&(Es(r,n)||t.push(`\`${r.key}\` is ${JSON.stringify(n)}, but it must be ${Ss(r)}.`))}return t}function zr(e){let t={};for(let r of er)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 tr(e,t){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||t}function At(e){return e.split("-").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function It(e){return/^[a-z][a-z0-9-]*$/.test(e)}function N(e){return e instanceof Error?e.message:String(e)}function pt(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 dt(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.");return e}function qr(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."),t}function Jr(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`."),re(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 Xr(e){let t=Ps(e,e.desktopAi);return delete e.desktopAi,t}function Ps(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 Zr(e){let t={};if(e.name!==void 0){if(!It(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(!Ne.includes(e.mobilePurchases))throw new Error(`Invalid mobile purchases "${e.mobilePurchases}". Valid values: ${Ne.join(", ")}`);t.mobilePurchases=e.mobilePurchases}if(e.ai!==void 0&&(t.ai=e.ai),e.webAi!==void 0&&(t.webAi=e.webAi),pt(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(!pe.includes(e.currency))throw new Error(`Invalid currency "${e.currency}". Valid values: ${pe.join(", ")}`);t.defaultCurrency=e.currency}if(e.deploy!==void 0){if(!de.includes(e.deploy))throw new Error(`Invalid deployment target "${e.deploy}". Valid values: ${de.join(", ")}`);t.deploymentTarget=e.deploy}if(e.database!==void 0){if(!ue.includes(e.database))throw new Error(`Invalid database provider "${e.database}". Valid values: ${ue.join(", ")}`);t.databaseProvider=e.database}if(e.cache!==void 0){if(!me.includes(e.cache))throw new Error(`Invalid cache provider "${e.cache}". Valid values: ${me.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=>!F.includes(i));if(n.length>0)throw new Error(`Invalid observability provider ${JSON.stringify(n.join(", "))}. Valid values: ${F.join(", ")}`);t.observability=r}if(e.preset!==void 0){if(!De.includes(e.preset))throw new Error(`Invalid preset "${e.preset}". Valid values: ${De.join(", ")}`);let r=le[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 tt={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,revenueSharing:!1,credits:!0,defaultCurrency:"USD",deploymentTarget:"node",databaseProvider:"postgres",cacheProvider:"redis",observability:[]};function Qr(e){let t=e.projectName??tt.projectName,r=e.projectDir??`./${t}`,n=e.appName??At(t),i=e.deploymentTarget??tt.deploymentTarget,o=te[i]?.edgeRuntime??!1,s=e.databaseProvider??(o?"neon":tt.databaseProvider),a=e.cacheProvider??(o?"upstash":tt.cacheProvider),d=e.emailProvider??(o?"resend":tt.emailProvider),h={...tt,...e,projectName:t,appName:n,projectDir:r,deploymentTarget:i,databaseProvider:s,cacheProvider:a,emailProvider:d};h.paymentProvider==="none"&&(h.credits=!1),(!h.mobile||h.paymentProvider==="none")&&(h.mobilePurchases="none"),pt(h);for(let g of qe){if(h.deploymentTarget!==g.target)continue;let k=h.databaseProvider===g.provider?"database":"cache";if(h.databaseProvider===g.provider||h.cacheProvider===g.provider)throw new Error(`Incompatible: --deploy ${g.target} + --${k} ${g.provider}. ${g.reason}`)}for(let g of Je)if(h.architecture===g.architecture&&h.deploymentTarget===g.target)throw new Error(`Incompatible: --architecture ${g.architecture} + --deploy ${g.target}. ${g.reason}`);return dt(h),h}function I(e){f.isCancel(e)&&(f.cancel("Setup cancelled."),process.exit(0))}function rr(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 en(e,t){let r=!1;e&&pt(e),e&&dt(e),f.log.info(y.bold("Project"));let n=e?.projectName??await(async()=>{r=!0;let c=await f.text({message:"Project name:",placeholder:"my-saas",validate:u=>{if(!u?.trim())return"Project name is required.";if(!It(u))return"Use lowercase letters, numbers, and hyphens only. Must start with a letter."}});return I(c),c})(),i=e?.appName??await(async()=>{r=!0;let c=await f.text({message:"App name:",initialValue:At(n),validate:u=>{if(!u?.trim())return"App name is required."}});return I(c),c})(),o=e?.projectDir??await(async()=>{r=!0;let c=await f.text({message:"Project location:",initialValue:`./${n}`});return I(c),c==="."?process.cwd():c})();f.log.info(y.bold("Infrastructure"));let s={};if(e?.deploymentTarget===void 0&&e?.databaseProvider===void 0&&e?.cacheProvider===void 0){r=!0;let c=await f.select({message:"Infrastructure:",options:[...De.map(u=>({value:u,label:le[u].label,hint:le[u].hint})),{value:"custom",label:"Customize",hint:"choose each piece yourself"}]});if(I(c),c!=="custom"){let u=le[c].values;s.deploymentTarget=u.deploymentTarget,s.databaseProvider=u.databaseProvider,s.cacheProvider=u.cacheProvider,f.log.info(`${le[c].label}: ${le[c].hint}.`)}}let a=e?.deploymentTarget??s.deploymentTarget??"node";if(e?.deploymentTarget===void 0&&s.deploymentTarget===void 0){r=!0;let c=await f.select({message:"Deployment target:",options:de.map(u=>({value:u,label:te[u].label,hint:te[u].hint}))});I(c),a=c}let d=e?.architecture?Je.find(c=>c.architecture===e.architecture&&c.target===a):void 0;if(d)throw new Error(`Incompatible: --architecture ${d.architecture} + --deploy ${d.target}. ${d.reason}`);let h=new Set(Je.filter(c=>c.target===a).map(c=>c.architecture)),g=Ze.filter(c=>!h.has(c)),k=e?.architecture??await(async()=>{if(g.length===1){let u=g[0];return f.log.info(`Auto-selected ${lt[u].label} architecture (only compatible option for ${te[a].label}).`),u}r=!0;let c=await f.select({message:"Architecture:",options:g.map(u=>({value:u,label:lt[u].label,hint:lt[u].hint}))});return I(c),c})(),v=e?.databaseProvider??s.databaseProvider??await(async()=>{r=!0;let c=ue.filter(R=>!qe.some(Ee=>Ee.target===a&&Ee.provider===R));if(c.length===1){let R=c[0];return f.log.info(`Auto-selected ${Oe[R].label} (only compatible option for ${te[a].label}).`),R}let u=await f.select({message:"Database provider:",options:c.map(R=>({value:R,label:Oe[R].label,hint:Oe[R].hint}))});return I(u),u})(),P=e?.cacheProvider??s.cacheProvider??await(async()=>{r=!0;let c=me.filter(R=>!qe.some(Ee=>Ee.target===a&&Ee.provider===R));if(c.length===1){let R=c[0];return f.log.info(`Auto-selected ${xe[R].label} (only compatible option for ${te[a].label}).`),R}let u=await f.select({message:"Cache provider:",options:c.map(R=>({value:R,label:xe[R].label,hint:xe[R].hint}))});return I(u),u})();if(te[a]?.edgeRuntime){let c=Gr.map(u=>` - ${u}`).join(`
3
+ `);f.note(c,"Unavailable on edge runtime")}f.log.info(y.bold("Features"));let m=e?.paymentProvider??await(async()=>{r=!0;let c=await f.select({message:"Payment provider:",options:Qe.map(u=>({value:u,label:Et[u].label,hint:Et[u].hint}))});return I(c),c})(),D=e?.defaultCurrency??await(async()=>{if(m==="none")return"USD";r=!0;let c=await f.select({message:"Default currency:",options:pe.map(u=>({value:u,label:u,hint:_e[u].name}))});return I(c),c})(),ve=e?.emailProvider??await(async()=>{r=!0;let c=await f.select({message:"Email provider:",options:et.map(u=>({value:u,label:St[u].label,hint:St[u].hint}))});return I(c),c})(),K=e?.observability??await(async()=>{r=!0;let c=await f.multiselect({message:"Observability (space to select, enter to confirm):",required:!1,initialValues:[],options:F.map(u=>({value:u,label:Pt[u].label,hint:Pt[u].hint}))});return I(c),c})(),ae=e?.multiTenancy??await(async()=>{r=!0;let c=await f.confirm({message:"Enable multi-tenancy (organizations)?",initialValue:!1});return I(c),c})(),Te=e?.projects??await(async()=>{r=!0;let c=await f.confirm({message:"Enable projects (a switcher that scopes a user's work)?",initialValue:!1});return I(c),c})(),Re=e?.docs??await(async()=>{r=!0;let c=await f.confirm({message:"Include docs app? (self-hosted Fumadocs documentation site)",initialValue:!1});return I(c),c})(),ze=t?.desktopAllowed!==!1,ce=e?.desktop??(ze?await(async()=>{r=!0;let c=await f.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 I(c),c})():(f.log.info(y.dim("Desktop app: available on the Pro plan and up - skipped.")),!1)),E=t?.mobileAllowed!==!1,_=e?.mobile??(E?await(async()=>{r=!0;let c=await f.confirm({message:"Include the mobile app? (apps/mobile - iOS and Android, built with Expo, with push notifications and universal links)",initialValue:!1});return I(c),c})():(f.log.info(y.dim("Mobile app: available on the Pro plan and up - skipped.")),!1)),Q=!_||m==="none"?"none":e?.mobilePurchases??await(async()=>{r=!0;let R=await f.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 I(R),R})();pt({mobile:_,mobilePurchases:Q,paymentProvider:m});let G=e?.ai??await(async()=>{r=!0;let c=await f.confirm({message:"Add AI features? (chat, models, automations, integrations)",initialValue:!1});return I(c),c})(),j=e?.webAi??((ce||_)&&G?await(async()=>{r=!0;let c=await f.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 I(c),c})():!0);dt({webAi:j,ai:G,desktop:ce,mobile:_});let ee=e?.revenueSharing??await(async()=>{r=!0;let c=await f.confirm({message:"Enable revenue sharing? (opt-in MRR leaderboard with dofollow backlinks)",initialValue:!1});return I(c),c})(),H=m==="none"?!1:e?.credits??await(async()=>{r=!0;let c=await f.confirm({message:"Enable credits? (metered usage on top of subscription plans)",initialValue:!0});return I(c),c})(),we=e?.demo,ke=rr({databaseProvider:v,cacheProvider:P,paymentProvider:m,emailProvider:ve,demo:we}),A={};if(ke.length>0&&r){f.log.info(y.bold("Credentials")+y.dim(" all optional - press Enter to skip, fill in .env later"));for(let c of ke)if(r=!0,c.secret){let u=await f.password({message:c.message,mask:"*"});I(u),typeof u=="string"&&u.trim()&&(A[c.key]=u.trim())}else{let u=await f.text({message:c.message,placeholder:c.placeholder});I(u),typeof u=="string"&&u.trim()&&(A[c.key]=u.trim())}}if(r){let c=[` Name: ${y.cyan(n)}`,` App name: ${y.cyan(i)}`,` Location: ${y.cyan(o)}`,` Architecture: ${y.cyan(lt[k].label)}`].join(`
4
4
  `),u=[` Deploy target: ${y.cyan(te[a]?.label??"Node.js / Docker")}`,` Database: ${y.cyan(Oe[v].label)}`,` Cache: ${y.cyan(xe[P].label)}`].filter(Boolean).join(`
5
- `),R=[m!=="none"?` Payment: ${y.cyan(Et[m].label)} (${D})`:` Payment: ${y.dim("none")}`,` Credits: ${H?y.cyan("Yes"):y.dim("No")}`,` AI features: ${K?y.cyan("Yes"):y.dim("No")}`,...j?[]:[` Web AI: ${y.dim("No")} (app-first)`],` Email: ${y.cyan(St[ve].label)}`,` Observability: ${G.length===0?y.dim("none"):y.cyan(G.map(Xt=>Pt[Xt].label).join(", "))}`,` Multi-tenancy: ${ae?y.cyan("Yes"):y.dim("No")}`,` Projects: ${Te?y.cyan("Yes"):y.dim("No")}`,` Docs app: ${Re?y.cyan("Yes"):y.dim("No")}`,` Desktop app: ${ce?y.cyan("Yes"):y.dim("No")}`,` Mobile app: ${_?y.cyan("Yes"):y.dim("No")}`,...Q==="none"?[]:[` In-app purch.: ${y.cyan("RevenueCat")}`],` Rev. sharing: ${ee?y.cyan("Yes"):y.dim("No")}`].join(`
5
+ `),R=[m!=="none"?` Payment: ${y.cyan(Et[m].label)} (${D})`:` Payment: ${y.dim("none")}`,` Credits: ${H?y.cyan("Yes"):y.dim("No")}`,` AI features: ${G?y.cyan("Yes"):y.dim("No")}`,...j?[]:[` Web AI: ${y.dim("No")} (app-first)`],` Email: ${y.cyan(St[ve].label)}`,` Observability: ${K.length===0?y.dim("none"):y.cyan(K.map(Xt=>Pt[Xt].label).join(", "))}`,` Multi-tenancy: ${ae?y.cyan("Yes"):y.dim("No")}`,` Projects: ${Te?y.cyan("Yes"):y.dim("No")}`,` Docs app: ${Re?y.cyan("Yes"):y.dim("No")}`,` Desktop app: ${ce?y.cyan("Yes"):y.dim("No")}`,` Mobile app: ${_?y.cyan("Yes"):y.dim("No")}`,...Q==="none"?[]:[` In-app purch.: ${y.cyan("RevenueCat")}`],` Rev. sharing: ${ee?y.cyan("Yes"):y.dim("No")}`].join(`
6
6
  `),Ee=[y.bold("Project"),c,"",y.bold("Infrastructure"),u,"",y.bold("Features"),R];if(ke.length>0){let Xt=ke.map(Fr=>{let ws=A[Fr.key]?y.green("provided"):y.dim("skipped");return` ${Fr.key}: ${ws}`}).join(`
7
7
  `);Ee.push("",y.bold("Credentials"),Xt)}f.note(Ee.join(`
8
- `),"Summary");let Mr=await f.confirm({message:"Proceed with these settings?"});(f.isCancel(Mr)||!Mr)&&(f.cancel("Setup cancelled."),process.exit(0))}return{projectName:n,appName:i,projectDir:o,architecture:k,deploymentTarget:a,databaseProvider:v,cacheProvider:P,paymentProvider:m,emailProvider:ve,observability:G,multiTenancy:ae,projects:Te,docs:Re,desktop:ce,mobile:_,mobilePurchases:Q,ai:K,webAi:j,revenueSharing:ee,credits:H,defaultCurrency:D,...Object.keys(A).length>0?{credentials:A}:{},...e?.baseUrl!==void 0?{baseUrl:e.baseUrl}:{},...we!==void 0?{demo:we}:{}}}import{createReadStream as un,existsSync as Cs}from"fs";import{mkdir as Ls}from"fs/promises";import{join as $s}from"path";import{Readable as js}from"stream";import{pipeline as dr}from"stream/promises";import{extract as Us}from"tar";import{join as Ce}from"path";import{homedir as As}from"os";var ut=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",Y=".generatesaas",he=Ce(Y,"manifest.json"),tn="nextjs",nr="nuxt",ir="2.4.1",rn=Ce(Y,"hashes.json"),Tt=Ce(Y,"template-hashes.json"),Rt=Ce(Y,"template"),nn=Ce(Y,"staging"),sn=Ce(Y,"staging.json"),fe=Ce(As(),".generatesaas");var $=class extends Error{constructor(r,n,i){super(n);this.status=r;this.body=i}status;body;name="ApiError"};function ge(e){return{apiKey:e,baseUrl:ut}}async function Se(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.17.1"}});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 $(i.status,o,s)}return i}import{existsSync as Is,mkdirSync as Ts,readFileSync as Rs,writeFileSync as _s}from"fs";import{dirname as Os}from"path";import*as Pe from"@clack/prompts";function mt(){if(!Is(fe))return null;try{let e=JSON.parse(Rs(fe,"utf-8"));return e.apiKey?e.apiKey:(e.token&&!e.apiKey&&Pe.log.warning(`Found old GitHub token in ${fe}. Run 'generatesaas init' to set up your API key.`),null)}catch{return null}}function Le(e){Ts(Os(fe),{recursive:!0}),_s(fe,`${JSON.stringify({apiKey:e},null," ")}
8
+ `),"Summary");let Ur=await f.confirm({message:"Proceed with these settings?"});(f.isCancel(Ur)||!Ur)&&(f.cancel("Setup cancelled."),process.exit(0))}return{projectName:n,appName:i,projectDir:o,architecture:k,deploymentTarget:a,databaseProvider:v,cacheProvider:P,paymentProvider:m,emailProvider:ve,observability:K,multiTenancy:ae,projects:Te,docs:Re,desktop:ce,mobile:_,mobilePurchases:Q,ai:G,webAi:j,revenueSharing:ee,credits:H,defaultCurrency:D,...Object.keys(A).length>0?{credentials:A}:{},...e?.baseUrl!==void 0?{baseUrl:e.baseUrl}:{},...we!==void 0?{demo:we}:{}}}import{createReadStream as un,existsSync as Cs}from"fs";import{mkdir as Ls}from"fs/promises";import{join as $s}from"path";import{Readable as js}from"stream";import{pipeline as dr}from"stream/promises";import{extract as Ms}from"tar";import{join as Ce}from"path";import{homedir as As}from"os";var ut=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",Y=".generatesaas",he=Ce(Y,"manifest.json"),tn="nextjs",nr="nuxt",ir="2.4.1",rn=Ce(Y,"hashes.json"),Tt=Ce(Y,"template-hashes.json"),Rt=Ce(Y,"template"),nn=Ce(Y,"staging"),sn=Ce(Y,"staging.json"),fe=Ce(As(),".generatesaas");var $=class extends Error{constructor(r,n,i){super(n);this.status=r;this.body=i}status;body;name="ApiError"};function ge(e){return{apiKey:e,baseUrl:ut}}async function Se(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.18.1"}});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 $(i.status,o,s)}return i}import{existsSync as Is,mkdirSync as Ts,readFileSync as Rs,writeFileSync as _s}from"fs";import{dirname as Os}from"path";import*as Pe from"@clack/prompts";function mt(){if(!Is(fe))return null;try{let e=JSON.parse(Rs(fe,"utf-8"));return e.apiKey?e.apiKey:(e.token&&!e.apiKey&&Pe.log.warning(`Found old GitHub token in ${fe}. Run 'generatesaas init' to set up your API key.`),null)}catch{return null}}function Le(e){Ts(Os(fe),{recursive:!0}),_s(fe,`${JSON.stringify({apiKey:e},null," ")}
9
9
  `,{mode:384})}async function rt(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 ht()}async function ht(){let e=await Pe.text({message:"Enter your GenerateSaaS API key:",placeholder:"gs_live_...",validate:t=>{if(!t?.trim())return"API key is required."}});return Pe.isCancel(e)&&(Pe.cancel("Setup cancelled."),process.exit(0)),e.trim()}import{readFile as xs}from"fs/promises";async function Ae(e){let t=process.env.GENERATESAAS_VERSION_INDEX;return t?JSON.parse(await xs(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 Se(e,"/versions")).json()}async function on(e,t){if(process.env.GENERATESAAS_OFFLINE_LICENSE==="1")return null;try{return await(await Se(e,`/changelog/${encodeURIComponent(t)}`)).text()}catch(r){if(r instanceof $&&r.status===404)return null;throw r}}async function an(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{skills:{"generatesaas-update":{skillMd:`# Offline skill stub
10
- `,scripts:{},references:{}}}}:await(await Se(e,`/skills/${encodeURIComponent(t)}`)).json()}function _t(e){if(!(e instanceof $)||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 sr="nextjs";function cn(e){return{frontend:sr,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:!1,credits:e.credits,revenueSharing:e.revenueSharing,currency:e.defaultCurrency}}async function or(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:"offline-test-token",licenseId:"offline-test-license-id",installId:t.installId}:await(await Se(e,"/license/sign",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function ln(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:t.currentToken}:await(await Se(e,"/license/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function ar(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 pn(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 cr=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"]),lr=new Set(["pnpm-lock.yaml"]);function nt(e){if(pr(e))return!0;for(let t of e.split("/"))if(lr.has(t))return!0;return!1}var Ds=new Set(["data","mksaas","references","scripts",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","vitest.config.ts","README.md","TODO.md","OVERVIEW.md","HANDOFF.md"]),dn=["scripts/docs-lint.ts"],Ns=["docs/superpowers","packages/cli","packages/cli-api","packages/database/tests/no-migration-history.test.ts","infra/docker-compose.yml",".claude/commands",".claude/skills/web-port",".claude/skills/web-port-workspace",".claude/settings.local.json",".claude/worktrees"];function pr(e){let t=e.split("/");for(let r of t)if(cr.has(r))return!0;if(dn.includes(e)||dn.some(r=>r.startsWith(`${e}/`)))return!1;if(Ds.has(t[0]))return!0;for(let r of Ns)if(e===r||e.startsWith(`${r}/`))return!0;return!1}async function Ot(e,t,r){await Ls(r,{recursive:!0});let n=process.env.GENERATESAAS_TEMPLATE_TARBALL_DIR;if(n){let a=$s(n,`v${t}.tar.gz`);if(!Cs(a))throw new Error(`GENERATESAAS_TEMPLATE_TARBALL_DIR is set but ${a} does not exist.`);await dr(un(a),ur(r));return}let i=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(i){await dr(un(i),ur(r));return}let o=await Se(e,`/template/${encodeURIComponent(t)}`);if(!o.body)throw new Error("Empty response body");let s=js.fromWeb(o.body);await dr(s,ur(r))}function ur(e){return Us({cwd:e,strip:1,filter:t=>{let r=t.replace(/^[^/]+\//,"");return r?!pr(r):!0},sync:!1})}import{readdir as xt,readFile as it,rm as hn,rmdir as Ms,writeFile as ft}from"fs/promises";import{join as V}from"path";async function fn(e){let t;try{t=await xt(e,{withFileTypes:!0})}catch{return!1}let r=!1;for(let n of t){let i=V(e,n.name);if(n.isDirectory()){await fn(i)&&(r=!0);continue}if(n.name.startsWith("_")){r=!0;continue}await hn(i,{force:!0})}return r||await Ms(e).catch(()=>{}),r}async function gn(e){let t=V(e,"packages/content"),r=[];try{r=(await xt(t,{withFileTypes:!0})).filter(s=>s.isDirectory()).map(s=>s.name)}catch{return}let n=new Set;for(let s of r){let a=V(t,s),d;try{d=(await xt(a,{withFileTypes:!0})).filter(h=>h.isDirectory())}catch{continue}for(let h of d)n.add(h.name),await fn(V(a,h.name))}let i=await Gs(e),o=new Set([...n].filter(s=>i===void 0||i.has(s)));await Promise.all([...o].map(s=>hn(V(e,"apps/web/public/images",s),{recursive:!0,force:!0}))),await Hs(e,o)}var Fs="packages/config/src/index.ts",Vs=/\bcontent:\s*\{[\s\S]*?\bsections:\s*\[([\s\S]*?)\]/,Bs=/\bslug:\s*"([^"]+)"/g;async function Gs(e){let t;try{t=await it(V(e,Fs),"utf-8")}catch{return}let r=Vs.exec(t)?.[1];if(r===void 0)return;let n=[...r.matchAll(Bs)].map(i=>i[1]).filter(i=>i!==void 0);return n.length>0?new Set(n):void 0}var Ks="packages/translate/.meta/mdx-translation-meta.json";async function Hs(e,t){if(t.size===0)return;let r=V(e,Ks),n;try{n=JSON.parse(await it(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 h=a.split("/"),g=h.length>1?h[0]:void 0,k=h[h.length-1]?.startsWith("_")===!0;g!==void 0&&t.has(g)&&!k||(s[a]=d)}await ft(r,`${JSON.stringify({...i,files:s},null," ")}
10
+ `,scripts:{},references:{}}}}:await(await Se(e,`/skills/${encodeURIComponent(t)}`)).json()}function _t(e){if(!(e instanceof $)||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 sr="nextjs";function cn(e){return{frontend:sr,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:!1,credits:e.credits,revenueSharing:e.revenueSharing,currency:e.defaultCurrency}}async function or(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:"offline-test-token",licenseId:"offline-test-license-id",installId:t.installId}:await(await Se(e,"/license/sign",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function ln(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:t.currentToken}:await(await Se(e,"/license/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function ar(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 pn(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 cr=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"]),lr=new Set(["pnpm-lock.yaml"]);function nt(e){if(pr(e))return!0;for(let t of e.split("/"))if(lr.has(t))return!0;return!1}var Ds=new Set(["data","mksaas","references","scripts",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","vitest.config.ts","README.md","TODO.md","OVERVIEW.md","HANDOFF.md"]),dn=["scripts/docs-lint.ts"],Ns=["docs/superpowers","packages/cli","packages/cli-api","packages/database/tests/no-migration-history.test.ts","infra/docker-compose.yml",".claude/commands",".claude/skills/web-port",".claude/skills/web-port-workspace",".claude/settings.local.json",".claude/worktrees"];function pr(e){let t=e.split("/");for(let r of t)if(cr.has(r))return!0;if(dn.includes(e)||dn.some(r=>r.startsWith(`${e}/`)))return!1;if(Ds.has(t[0]))return!0;for(let r of Ns)if(e===r||e.startsWith(`${r}/`))return!0;return!1}async function Ot(e,t,r){await Ls(r,{recursive:!0});let n=process.env.GENERATESAAS_TEMPLATE_TARBALL_DIR;if(n){let a=$s(n,`v${t}.tar.gz`);if(!Cs(a))throw new Error(`GENERATESAAS_TEMPLATE_TARBALL_DIR is set but ${a} does not exist.`);await dr(un(a),ur(r));return}let i=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(i){await dr(un(i),ur(r));return}let o=await Se(e,`/template/${encodeURIComponent(t)}`);if(!o.body)throw new Error("Empty response body");let s=js.fromWeb(o.body);await dr(s,ur(r))}function ur(e){return Ms({cwd:e,strip:1,filter:t=>{let r=t.replace(/^[^/]+\//,"");return r?!pr(r):!0},sync:!1})}import{readdir as xt,readFile as it,rm as hn,rmdir as Us,writeFile as ft}from"fs/promises";import{join as V}from"path";async function fn(e){let t;try{t=await xt(e,{withFileTypes:!0})}catch{return!1}let r=!1;for(let n of t){let i=V(e,n.name);if(n.isDirectory()){await fn(i)&&(r=!0);continue}if(n.name.startsWith("_")){r=!0;continue}await hn(i,{force:!0})}return r||await Us(e).catch(()=>{}),r}async function gn(e){let t=V(e,"packages/content"),r=[];try{r=(await xt(t,{withFileTypes:!0})).filter(s=>s.isDirectory()).map(s=>s.name)}catch{return}let n=new Set;for(let s of r){let a=V(t,s),d;try{d=(await xt(a,{withFileTypes:!0})).filter(h=>h.isDirectory())}catch{continue}for(let h of d)n.add(h.name),await fn(V(a,h.name))}let i=await Ks(e),o=new Set([...n].filter(s=>i===void 0||i.has(s)));await Promise.all([...o].map(s=>hn(V(e,"apps/web/public/images",s),{recursive:!0,force:!0}))),await Hs(e,o)}var Fs="packages/config/src/index.ts",Vs=/\bcontent:\s*\{[\s\S]*?\bsections:\s*\[([\s\S]*?)\]/,Bs=/\bslug:\s*"([^"]+)"/g;async function Ks(e){let t;try{t=await it(V(e,Fs),"utf-8")}catch{return}let r=Vs.exec(t)?.[1];if(r===void 0)return;let n=[...r.matchAll(Bs)].map(i=>i[1]).filter(i=>i!==void 0);return n.length>0?new Set(n):void 0}var Gs="packages/translate/.meta/mdx-translation-meta.json";async function Hs(e,t){if(t.size===0)return;let r=V(e,Gs),n;try{n=JSON.parse(await it(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 h=a.split("/"),g=h.length>1?h[0]:void 0,k=h[h.length-1]?.startsWith("_")===!0;g!==void 0&&t.has(g)&&!k||(s[a]=d)}await ft(r,`${JSON.stringify({...i,files:s},null," ")}
11
11
  `)}var Ys="packages/config/src/blog.ts";async function yn(e){let t=V(e,Ys),r=await it(t,"utf-8"),n=mn(mn(r,"blogCategories",t),"blogAuthors",t);await ft(t,n)}function mn(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 bn(e){let t=V(e,"packages/i18n/translations"),r;try{r=await xt(t)}catch{return}for(let n of r){let i=V(t,n,"web.json"),o;try{o=await it(i,"utf-8")}catch{continue}let s=JSON.parse(o);!s.content?.categories||s.content.categories.blog===void 0||(s.content.categories.blog={},await ft(i,`${JSON.stringify(s,null," ")}
12
12
  `))}}var Ws="packages/config/src/ai/tasks.ts",zs="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.",qs="The array starts EMPTY, which hides the Tasks tab until you declare your first task.";async function vn(e){let t=V(e,Ws),r=await it(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(zs,qs);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 ft(t,i)}async function wn(e){let t=V(e,".claude","settings.json"),r;try{r=await it(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(Js)),await ft(t,`${JSON.stringify(n,null," ")}
13
13
  `)}function Js(e){return!(e.startsWith("mcp__")||e.includes("nuxt"))}import{readFileSync as oo}from"fs";import{dirname as ao,join as En}from"path";import{fileURLToPath as co}from"url";import{existsSync as Xs}from"fs";import{join as kn}from"path";function Dt(e,t,r){let n=kn(e,...t);return Xs(n)?n:kn(e,r)}import{mkdir as Zs,writeFile as Qs,readdir as eo,rm as to,rmdir as ro}from"fs/promises";import{dirname as Nt,join as no,relative as io,sep as so}from"path";function $e(e){return e.split(so).join("/")}async function Ct(e){await Zs(e,{recursive:!0})}async function l(e,t){await Ct(Nt(e)),await Qs(e,t,"utf-8")}async function st(e,t){await to(e,{force:!0});let r=Nt(e);for(;r!==t&&r!==Nt(r);){try{await ro(r)}catch{return}r=Nt(r)}}async function ye(e,t,r,n){let i=[],o=await eo(e,{withFileTypes:!0});for(let s of o){let a=no(e,s.name),d=$e(io(t,a));r(d)||(s.isDirectory()?i.push(...await ye(a,t,r,n)):s.isFile()&&(n===void 0||n(d))&&i.push(a))}return i}var lo={postgres:"Postgres",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"},po={resend:"Resend",ses:"Amazon SES",smtp:"SMTP"},uo={redis:"Redis",upstash:"Upstash Redis"},mo={node:"Node.js / Docker",vercel:"Vercel"},mr="apps/web",ho="Next.js 16",fo="`app/` routes, `components/`, `lib/`",go={stripe:"Stripe",polar:"Polar"};function yo(e){let t=e.architecture==="fullstack",r=t?"(fullstack - Hono API mounted inside the app)":"(separate - standalone Hono backend)",n=t?`Mounted inside \`${mr}\`. 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=uo[e.cacheProvider],o=mo[e.deploymentTarget],s=e.paymentProvider==="none"?"":`
@@ -454,7 +454,7 @@ export * from "./robots";
454
454
  export * from "./roles";
455
455
  export * from "./section-tabs";
456
456
  export * from "./tenancy";
457
- `,G=gt(e.projectDir,"packages/config/src/index.ts");await l(G,ve),await l(gt(e.projectDir,"packages/config/src/support.ts"),`import type { SupportConfig } from "@repo/config/types";
457
+ `,K=gt(e.projectDir,"packages/config/src/index.ts");await l(K,ve),await l(gt(e.projectDir,"packages/config/src/support.ts"),`import type { SupportConfig } from "@repo/config/types";
458
458
 
459
459
  /**
460
460
  * Live-chat (support widget) configuration, re-exported by the config index as
@@ -780,7 +780,7 @@ ${k},
780
780
  items: []
781
781
  }
782
782
  };
783
- `;await l(t,v)}var Do={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"}]},No={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 Lt(e,t){return t?e.map(r=>{let n=t[r.key];return n?{...r,defaultValue:n,comment:void 0}:r}):e}function yt(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 gr(e){return Array.from(crypto.getRandomValues(new Uint8Array(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}function yr(e){return e.architecture==="fullstack"?{apiUrl:"http://localhost:3000/api",baseUrl:"http://localhost:3000"}:{apiUrl:"http://localhost:3010",baseUrl:"http://localhost:3000"}}var Co="https://your-app.example.com/api";function $t(e,t,r,n){e.push(n==="example"?`${t}=`:`${t}=${r}`)}function Rn(e,t){let r=t==="example"?void 0:e.credentials,{apiUrl:n,baseUrl:i}=yr(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"),yt(Lt(Oe[e.databaseProvider].envVars,r),o),o.push("","# Cache"),yt(Lt(xe[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`"),$t(o,"BETTER_AUTH_SECRET",crypto.randomUUID(),t),o.push("","# Content API (random secret; required when contentApi feature is enabled in @repo/config)"),$t(o,"CONTENT_API_KEY",gr(32),t),o.push("","# Job Queue - Inngest","INNGEST_APP_ID=api"),$t(o,"INNGEST_EVENT_KEY",gr(32),t),o.push("# REQUIRED in production: it is how the /inngest endpoint tells Inngest from anyone else who","# finds the URL. The app refuses to boot without it unless INNGEST_DEV is set (local dev server).","# Take the real value from the Inngest dashboard."),$t(o,"INNGEST_SIGNING_KEY",gr(32),t),o.push("# The local Inngest dev server (pnpm dev:inngest). In production on Inngest Cloud leave this UNSET:","# Cloud takes events and registration on two hosts, which no single URL can name. Set it only for a","# self-hosted Inngest, to its one host.","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=Do[e.emailProvider];if(s&&(o.push("","# Email"),yt(Lt(s,r),o)),e.paymentProvider!=="none"){let d=No[e.paymentProvider];d&&(o.push("","# Payment"),yt(Lt(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)"),yt(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, on chat (on by default), the desktop app-tools","# lane, automations and the MCP server. A key's presence is the whole switch: set one","# and the model gets its tools, set none and chat is unchanged. config.ai.webSearch:","# false switches every lane off.","# ANY one key alone covers both capabilities. With several set, auto-detection takes the","# CHEAPEST first - TinyFish, then Firecrawl, then Parallel - per capability; naming","# config.ai.webTools.searchEngine or .extractEngine wins over that order, and a named","# engine whose key is missing ships no tool at all.","# WHAT A CALL COSTS THE USER: the vendor price x credits.creditsPerUsd x the","# webSearch/webExtract markup (shipped at 100, so twice cost). Override the vendor price","# with config.ai.webTools.searchCostUsd / .extractCostUsd.","# TinyFish (the auto-detect default) - both tools at a $0 list price, so a call debits","# nothing, writes no ledger row and is never refused for an empty balance. Rate-limited","# instead (30 searches and 150 pages a minute per API key by default, shared by every","# user), so set a price on a busy app. Note its terms include a license to train on","# submitted content.","# https://agent.tinyfish.ai/","#TINYFISH_API_KEY=","# Firecrawl (the recommended PAID engine) - `web_search` AND `web_extract` (reads one","# page) from one signup, and the only vendor here whose terms carry no license over what","# you submit.","# https://firecrawl.dev/","#FIRECRAWL_API_KEY=","# Parallel (optional) - both tools, cheaper than Firecrawl 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.","# https://platform.parallel.ai/","#PARALLEL_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(`
783
+ `;await l(t,v)}var Do={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"}]},No={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 Lt(e,t){return t?e.map(r=>{let n=t[r.key];return n?{...r,defaultValue:n,comment:void 0}:r}):e}function yt(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 gr(e){return Array.from(crypto.getRandomValues(new Uint8Array(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}function yr(e){return e.architecture==="fullstack"?{apiUrl:"http://localhost:3000/api",baseUrl:"http://localhost:3000"}:{apiUrl:"http://localhost:3010",baseUrl:"http://localhost:3000"}}var Co="https://your-app.example.com/api";function $t(e,t,r,n){e.push(n==="example"?`${t}=`:`${t}=${r}`)}function Rn(e,t){let r=t==="example"?void 0:e.credentials,{apiUrl:n,baseUrl:i}=yr(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"),yt(Lt(Oe[e.databaseProvider].envVars,r),o),o.push("","# Cache"),yt(Lt(xe[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`"),$t(o,"BETTER_AUTH_SECRET",crypto.randomUUID(),t),o.push("","# Content API (random secret; required when contentApi feature is enabled in @repo/config)"),$t(o,"CONTENT_API_KEY",gr(32),t),o.push("","# Job Queue - Inngest","INNGEST_APP_ID=api"),$t(o,"INNGEST_EVENT_KEY",gr(32),t),o.push("# REQUIRED in production: it is how the /inngest endpoint tells Inngest from anyone else who","# finds the URL. The app refuses to boot without it unless INNGEST_DEV is set (local dev server).","# Take the real value from the Inngest dashboard."),$t(o,"INNGEST_SIGNING_KEY",gr(32),t),o.push("# The local Inngest dev server (pnpm dev:inngest). In production on Inngest Cloud leave this UNSET:","# Cloud takes events and registration on two hosts, which no single URL can name. Set it only for a","# self-hosted Inngest, to its one host.","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=Do[e.emailProvider];if(s&&(o.push("","# Email"),yt(Lt(s,r),o)),e.paymentProvider!=="none"){let d=No[e.paymentProvider];d&&(o.push("","# Payment"),yt(Lt(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: APPLE_TEAM_ID + APPLE_KEY_ID + APPLE_PRIVATE_KEY are the signing key. Add APPLE_CLIENT_ID",'# (a Service ID) for the web button, and list "apple" in config.auth.socialProviders.',"#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)"),yt(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, on chat (on by default), the desktop app-tools","# lane, automations and the MCP server. A key's presence is the whole switch: set one","# and the model gets its tools, set none and chat is unchanged. config.ai.webSearch:","# false switches every lane off.","# ANY one key alone covers both capabilities. With several set, auto-detection takes the","# CHEAPEST first - TinyFish, then Firecrawl, then Parallel - per capability; naming","# config.ai.webTools.searchEngine or .extractEngine wins over that order, and a named","# engine whose key is missing ships no tool at all.","# WHAT A CALL COSTS THE USER: the vendor price x credits.creditsPerUsd x the","# webSearch/webExtract markup (shipped at 100, so twice cost). Override the vendor price","# with config.ai.webTools.searchCostUsd / .extractCostUsd.","# TinyFish (the auto-detect default) - both tools at a $0 list price, so a call debits","# nothing, writes no ledger row and is never refused for an empty balance. Rate-limited","# instead (30 searches and 150 pages a minute per API key by default, shared by every","# user), so set a price on a busy app. Note its terms include a license to train on","# submitted content.","# https://agent.tinyfish.ai/","#TINYFISH_API_KEY=","# Firecrawl (the recommended PAID engine) - `web_search` AND `web_extract` (reads one","# page) from one signup, and the only vendor here whose terms carry no license over what","# you submit.","# https://firecrawl.dev/","#FIRECRAWL_API_KEY=","# Parallel (optional) - both tools, cheaper than Firecrawl 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.","# https://platform.parallel.ai/","#PARALLEL_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. With the three Apple signing values","# and no APPLE_CLIENT_ID, it alone turns on native-only Sign in with Apple: the iOS app shows","# Apple beside Google (it reads that at runtime) and the web shows no Apple button.","#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(`
784
784
  `)}var _n="NEXT_PUBLIC_API_URL";function Lo(e){let{apiUrl:t}=yr(e),r=["# API Configuration",`${_n}=${t}`];return e.architecture==="separate"&&r.push("","# Production (uncomment and replace with your deployed hostnames):",`# ${_n}=${Co}`),r.push(""),r.join(`
785
785
  `)}function $o(e){let{apiUrl:t,baseUrl:r}=yr(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(`
786
786
  `)}async function On(e){let t=Lo(e);if(await l(`${e.projectDir}/.env`,`${t}
@@ -847,13 +847,13 @@ ${r.join(`
847
847
  volumes:
848
848
  ${n.join(`
849
849
  `)}
850
- `),await l(jo(e.projectDir,"infra/docker-compose.yml"),o)}import{join as Uo}from"path";var bt="Next.js";function Mo(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:bt,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 Fo(e){let t=[];return e.architecture==="separate"?t.push({name:`Dev (${bt} + Backend + Inngest)`,configurations:["Backend",bt,"Inngest"]}):t.push({name:`Dev (${bt} + Inngest)`,configurations:[bt,"Inngest"]}),t}async function Dn(e){let t={version:"0.2.0",configurations:Mo(e),compounds:Fo(e)};await l(Uo(e.projectDir,".vscode/launch.json"),`${JSON.stringify(t,null," ")}
850
+ `),await l(jo(e.projectDir,"infra/docker-compose.yml"),o)}import{join as Mo}from"path";var bt="Next.js";function Uo(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:bt,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 Fo(e){let t=[];return e.architecture==="separate"?t.push({name:`Dev (${bt} + Backend + Inngest)`,configurations:["Backend",bt,"Inngest"]}):t.push({name:`Dev (${bt} + Inngest)`,configurations:[bt,"Inngest"]}),t}async function Dn(e){let t={version:"0.2.0",configurations:Uo(e),compounds:Fo(e)};await l(Mo(e.projectDir,".vscode/launch.json"),`${JSON.stringify(t,null," ")}
851
851
  `)}import{readFile as Cn}from"fs/promises";import{join as Ln}from"path";var br='import { drizzle } from "drizzle-orm/node-postgres";',vr=`export const db = drizzle(parsed.data.DATABASE_URL, { schema });
852
852
  export const pool = db.$client;`,Vo={postgres:{importLine:br,instantiation:vr},neon:{importLine:`import { Pool } from "@neondatabase/serverless";
853
853
  import { drizzle } from "drizzle-orm/neon-serverless";`,instantiation:`const pool = new Pool({ connectionString: parsed.data.DATABASE_URL });
854
854
  export const db = drizzle(pool, { schema });`},supabase:{importLine:`import { drizzle } from "drizzle-orm/postgres-js";
855
855
  import postgres from "postgres";`,instantiation:`const client = postgres(parsed.data.DATABASE_URL);
856
- export const db = drizzle(client, { schema });`}};async function $n(e){let t=Ln(e.projectDir,"packages/database/src/index.ts"),r=await Cn(t,"utf-8");if(!r.includes(br)||!r.includes(vr))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=Vo[e.databaseProvider],i=r.replace(br,n.importLine).replace(vr,n.instantiation);await l(t,i),await Go(e)}var Nn='import { Client } from "pg";',wr=` const client = new Client({ connectionString });
856
+ export const db = drizzle(client, { schema });`}};async function $n(e){let t=Ln(e.projectDir,"packages/database/src/index.ts"),r=await Cn(t,"utf-8");if(!r.includes(br)||!r.includes(vr))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=Vo[e.databaseProvider],i=r.replace(br,n.importLine).replace(vr,n.instantiation);await l(t,i),await Ko(e)}var Nn='import { Client } from "pg";',wr=` const client = new Client({ connectionString });
857
857
  await client.connect();
858
858
  return {
859
859
  query: async (text) => ({ rows: (await client.query<LiveColumnRow>(text)).rows }),
@@ -862,7 +862,7 @@ export const db = drizzle(client, { schema });`}};async function $n(e){let t=Ln(
862
862
  return {
863
863
  query: async (text) => ({ rows: await sql.unsafe(text) }),
864
864
  end: () => sql.end()
865
- };`}};async function Go(e){let t=Bo[e.databaseProvider];if(!t)return;let r=Ln(e.projectDir,"packages/database/src/db-verify.ts"),n=await Cn(r,"utf-8");if(!n.includes(Nn)||!n.includes(wr))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(Nn,t.importLine).replace(wr,t.open))}import{join as jn}from"path";async function Un(e){e.cacheProvider==="upstash"&&(await Ko(e),await Ho(e))}async function Ko(e){await l(jn(e.projectDir,"packages/runtime/src/redis.ts"),`import { Redis } from "@upstash/redis";
865
+ };`}};async function Ko(e){let t=Bo[e.databaseProvider];if(!t)return;let r=Ln(e.projectDir,"packages/database/src/db-verify.ts"),n=await Cn(r,"utf-8");if(!n.includes(Nn)||!n.includes(wr))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(Nn,t.importLine).replace(wr,t.open))}import{join as jn}from"path";async function Mn(e){e.cacheProvider==="upstash"&&(await Go(e),await Ho(e))}async function Go(e){await l(jn(e.projectDir,"packages/runtime/src/redis.ts"),`import { Redis } from "@upstash/redis";
866
866
  import type { Store } from "hono-rate-limiter";
867
867
  import { env } from "./env";
868
868
 
@@ -1110,17 +1110,17 @@ export async function withMutex<T>(
1110
1110
  });
1111
1111
  }
1112
1112
  }
1113
- `)}import{readFile as Yo}from"fs/promises";import{join as Wo}from"path";var Mn=" REDIS_URL: z.string(),",zo=` UPSTASH_REDIS_REST_URL: z.string(),
1114
- UPSTASH_REDIS_REST_TOKEN: z.string(),`;async function Fn(e){let t=Wo(e.projectDir,"packages/runtime/src/env.ts"),r=await Yo(t,"utf-8");if(!r.includes(Mn))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(Mn,zo):r;await l(t,n)}import{readFile as Hn}from"fs/promises";import{join as U}from"path";import{readFile as Er,rm as Xo}from"fs/promises";import{join as Me}from"path";import{readFile as kr}from"fs/promises";function qo(e){return new RegExp(`^[ \\t]*(?:\\/\\/|\\{\\/\\*|\\*) gsaas:${e}-start(?: \\*\\/\\})?[ \\t]*\\r?\\n[\\s\\S]*?^[ \\t]*(?:\\/\\/|\\{\\/\\*|\\*) gsaas:${e}-end(?: \\*\\/\\})?[ \\t]*\\r?\\n`,"gm")}function je(e,t,r,n){let i=qo(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,`
1113
+ `)}import{readFile as Yo}from"fs/promises";import{join as Wo}from"path";var Un=" REDIS_URL: z.string(),",zo=` UPSTASH_REDIS_REST_URL: z.string(),
1114
+ UPSTASH_REDIS_REST_TOKEN: z.string(),`;async function Fn(e){let t=Wo(e.projectDir,"packages/runtime/src/env.ts"),r=await Yo(t,"utf-8");if(!r.includes(Un))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(Un,zo):r;await l(t,n)}import{readFile as Hn}from"fs/promises";import{join as M}from"path";import{readFile as Er,rm as Xo}from"fs/promises";import{join as Ue}from"path";import{readFile as kr}from"fs/promises";function qo(e){return new RegExp(`^[ \\t]*(?:\\/\\/|\\{\\/\\*|\\*) gsaas:${e}-start(?: \\*\\/\\})?[ \\t]*\\r?\\n[\\s\\S]*?^[ \\t]*(?:\\/\\/|\\{\\/\\*|\\*) gsaas:${e}-end(?: \\*\\/\\})?[ \\t]*\\r?\\n`,"gm")}function je(e,t,r,n){let i=qo(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,`
1115
1115
 
1116
- `).replace(/,(\r?\n[ \t]*[\]})])/g,"$1")}async function T(e,t,r,n,i){let o=await kr(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.includes(t,s+t.length))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 Ue(e,t,r,n){let i=/\n([\t ]+)"/.exec(await kr(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)}
1116
+ `).replace(/,(\r?\n[ \t]*[\]})])/g,"$1")}async function T(e,t,r,n,i){let o=await kr(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.includes(t,s+t.length))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 Me(e,t,r,n){let i=/\n([\t ]+)"/.exec(await kr(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)}
1117
1117
  `)}function Jo(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]!==`
1118
1118
  `;)r++;continue}if(n==="/"&&i==="*"){for(r+=2;r<e.length&&!(e[r]==="*"&&e[r+1]==="/");)e[r]===`
1119
1119
  `&&(t+=`
1120
1120
  `),r++;r+=2;continue}if(n==='"'||n==="'"||n==="`"){let o=n;for(r++;r<e.length&&e[r]!==o;)e[r]==="\\"?r++:e[r]===`
1121
1121
  `&&(t+=`
1122
1122
  `),r++;r++;continue}t+=n,r++}return t}async function Vn(e){let t=`import { config } from "@repo/config";
1123
- `,r=await kr(e,"utf-8");if(!r.includes(t))return;let n=r.replace(t,""),i=Jo(n);/\bconfig\b/.test(i)||await l(e,n)}var Fe="stripMobileObservability",Bn=["apps/mobile/lib/observability.ts","apps/mobile/types/env.d.ts","apps/mobile/tests/settings-privacy-screen.test.tsx"],Gn="apps/mobile/tests/observability.test.ts",Zo="mobile-obs-any",Qo=["apps/mobile/tests/settings-privacy-screen.test.tsx"],jt={sentry:["@sentry/react-native","@sentry/cli"],posthog:["posthog-react-native"]},ea=`,
1123
+ `,r=await kr(e,"utf-8");if(!r.includes(t))return;let n=r.replace(t,""),i=Jo(n);/\bconfig\b/.test(i)||await l(e,n)}var Fe="stripMobileObservability",Bn=["apps/mobile/lib/observability.ts","apps/mobile/types/env.d.ts","apps/mobile/tests/settings-privacy-screen.test.tsx"],Kn="apps/mobile/tests/observability.test.ts",Zo="mobile-obs-any",Qo=["apps/mobile/tests/settings-privacy-screen.test.tsx"],jt={sentry:["@sentry/react-native","@sentry/cli"],posthog:["posthog-react-native"]},ea=`,
1124
1124
  ...sentryExpoPlugin()`,ta=["apps/mobile/app.config.ts"],ra="|@sentry/react-native",na=` * DUAL-PROVIDER, the way \`packages/observability\` is. This repo carries the Sentry and the PostHog
1125
1125
  * halves at once so dev and the test suite exercise both. A generated project carries only the
1126
1126
  * providers its \`observability\` manifest option names: the CLI removes the other one's marked region
@@ -1140,8 +1140,8 @@ export async function withMutex<T>(
1140
1140
  * To change providers, edit \`observability\` in .generatesaas/manifest.json and run
1141
1141
  * \`generatesaas update\`: re-shaping brings that provider's half of this file, its SDK and its env
1142
1142
  * key back. Pasting the other provider's credential into \`config.observability\` on its own cannot -
1143
- * the start it would need is not here.`}async function Kn(e){if(!e.mobile)return;let t=e.projectDir,r=["sentry","posthog"].filter(o=>!e.observability.includes(o));e.observability.length===0&&await Xo(Me(t,Gn),{force:!0});let n=e.observability.length===0?Bn:[...Bn,Gn];for(let o of r)for(let s of n){let a=Me(t,s);await l(a,je(await Er(a,"utf-8"),s,`mobile-obs-${o}`,Fe))}if(e.observability.length===0)for(let o of Qo){let s=Me(t,o);await l(s,je(await Er(s,"utf-8"),o,Zo,Fe))}if(r.includes("sentry")){for(let o of ta){let s=Me(t,o);await l(s,je(await Er(s,"utf-8"),o,"mobile-obs-sentry",Fe))}await T(Me(t,"apps/mobile/app.config.ts"),ea,"","app.config.ts Sentry Expo plugin",Fe),await T(Me(t,"apps/mobile/jest.config.js"),ra,"","jest.config.js Sentry transform allowance",Fe)}let i=Me(t,"apps/mobile/lib/observability.ts");await T(i,na,oa(e.observability),"lib/observability.ts dual-provider note",Fe),e.observability.length===0&&(await T(i,ia,sa,"lib/observability.ts privacy import",Fe),await Vn(i))}var Ut={"@upstash/redis":"^1.37.0","@upstash/lock":"^0.2.1","@neondatabase/serverless":"^1.0.1",postgres:"^3.4.7"};async function ne(e){let t=await Hn(e,"utf-8");return JSON.parse(t)}async function ie(e,t){let r=/\n([\t ]+)"/.exec(await Hn(e,"utf-8"))?.[1]??" ";await l(e,`${JSON.stringify(t,null,r)}
1144
- `)}function W(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 vt(e,t,r,n=!1){let i=n?"devDependencies":"dependencies";e[i]||(e[i]={}),e[i][t]=r}async function Yn(e){await la(e),await pa(e),await da(e),await fa(e),await ga(e),await wa(e),await ca(e)}async function ca(e){if(!e.mobile){let t=U(e.projectDir,"packages/auth/package.json"),r=await ne(t);W(r,["@better-auth/expo"]),await ie(t,r);let n=U(e.projectDir,"tooling/styles/package.json"),i=await ne(n);W(i,["tsx"]),aa(i),await ie(n,i)}if(e.mobile&&e.mobilePurchases!=="revenuecat"){let t=U(e.projectDir,"apps/mobile/package.json"),r=await ne(t);W(r,["react-native-purchases","react-native-purchases-ui"]),await ie(t,r)}}async function la(e){let t=U(e.projectDir,"packages/api/package.json"),r=await ne(t);W(r,["sharp","@types/sharp"]),e.cacheProvider==="upstash"&&W(r,["ioredis"]),await ie(t,r)}async function pa(e){let t=U(e.projectDir,"apps/web/package.json"),r=await ne(t);W(r,["@upstash/ratelimit","@upstash/redis"]),await ie(t,r)}async function da(e){let t=U(e.projectDir,"packages/runtime/package.json"),r=await ne(t);e.cacheProvider==="upstash"&&(W(r,["ioredis","rate-limit-redis","redis-semaphore"]),vt(r,"@upstash/redis",Ut["@upstash/redis"]),vt(r,"@upstash/lock",Ut["@upstash/lock"])),await ie(t,r)}var Sr={sentry:["@sentry/node","@sentry/browser","@sentry/electron"],posthog:["posthog-node","posthog-js"]},ua=["@sentry/electron"],ma=[...Sr.sentry,...Sr.posthog],ha=[...jt.sentry,...jt.posthog];async function fa(e){let t=U(e.projectDir,"packages/observability/package.json"),r=await ne(t),n=new Set(e.observability.flatMap(a=>Sr[a]));if(!e.desktop)for(let a of ua)n.delete(a);if(W(r,ma.filter(a=>!n.has(a))),await ie(t,r),!e.mobile)return;let i=U(e.projectDir,"apps/mobile/package.json"),o=await ne(i),s=new Set(e.observability.flatMap(a=>jt[a]));W(o,ha.filter(a=>!s.has(a))),await ie(i,o)}async function ga(e){let t=U(e.projectDir,"packages/database/package.json"),r=await ne(t);e.databaseProvider==="neon"?(W(r,["pg","@types/pg"]),vt(r,"@neondatabase/serverless",Ut["@neondatabase/serverless"])):e.databaseProvider==="supabase"&&(W(r,["pg","@types/pg"]),vt(r,"postgres",Ut.postgres)),await ie(t,r)}var ya=["app/.well-known/oauth-authorization-server/[[...path]]/route.ts","app/.well-known/oauth-protected-resource/[[...path]]/route.ts"];async function ba(e){let t=await ne(e),r=t.dependencies?.["@repo/api"];r&&(delete t.dependencies?.["@repo/api"],vt(t,"@repo/api",r,!0),await ie(e,t))}var va=["instrumentation.ts","tests/instrumentation.test.ts"];async function wa(e){if(e.architecture!=="separate")return;let t=U(e.projectDir,"apps/web");await st(U(t,"app/api/[[...rest]]/route.ts"),t);for(let r of ya)await st(U(t,r),t);for(let r of va)await st(U(t,r),t);await ba(U(t,"package.json"))}import{readFile as ka}from"fs/promises";import{join as Ea}from"path";var Sa=["base.json","node.json","next.json"],Wn="GenerateSaaS ";async function zn(e){if(!e.demo)for(let t of Sa){let r=Ea(e.projectDir,"tooling/typescript",t),n;try{n=await ka(r,"utf-8")}catch{continue}let i=JSON.parse(n);typeof i.display!="string"||!i.display.startsWith(Wn)||(i.display=i.display.slice(Wn.length),await l(r,`${JSON.stringify(i,null," ")}
1143
+ * the start it would need is not here.`}async function Gn(e){if(!e.mobile)return;let t=e.projectDir,r=["sentry","posthog"].filter(o=>!e.observability.includes(o));e.observability.length===0&&await Xo(Ue(t,Kn),{force:!0});let n=e.observability.length===0?Bn:[...Bn,Kn];for(let o of r)for(let s of n){let a=Ue(t,s);await l(a,je(await Er(a,"utf-8"),s,`mobile-obs-${o}`,Fe))}if(e.observability.length===0)for(let o of Qo){let s=Ue(t,o);await l(s,je(await Er(s,"utf-8"),o,Zo,Fe))}if(r.includes("sentry")){for(let o of ta){let s=Ue(t,o);await l(s,je(await Er(s,"utf-8"),o,"mobile-obs-sentry",Fe))}await T(Ue(t,"apps/mobile/app.config.ts"),ea,"","app.config.ts Sentry Expo plugin",Fe),await T(Ue(t,"apps/mobile/jest.config.js"),ra,"","jest.config.js Sentry transform allowance",Fe)}let i=Ue(t,"apps/mobile/lib/observability.ts");await T(i,na,oa(e.observability),"lib/observability.ts dual-provider note",Fe),e.observability.length===0&&(await T(i,ia,sa,"lib/observability.ts privacy import",Fe),await Vn(i))}var Mt={"@upstash/redis":"^1.37.0","@upstash/lock":"^0.2.1","@neondatabase/serverless":"^1.0.1",postgres:"^3.4.7"};async function ne(e){let t=await Hn(e,"utf-8");return JSON.parse(t)}async function ie(e,t){let r=/\n([\t ]+)"/.exec(await Hn(e,"utf-8"))?.[1]??" ";await l(e,`${JSON.stringify(t,null,r)}
1144
+ `)}function W(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 vt(e,t,r,n=!1){let i=n?"devDependencies":"dependencies";e[i]||(e[i]={}),e[i][t]=r}async function Yn(e){await la(e),await pa(e),await da(e),await fa(e),await ga(e),await wa(e),await ca(e)}async function ca(e){if(!e.mobile){let t=M(e.projectDir,"packages/auth/package.json"),r=await ne(t);W(r,["@better-auth/expo"]),await ie(t,r);let n=M(e.projectDir,"tooling/styles/package.json"),i=await ne(n);W(i,["tsx"]),aa(i),await ie(n,i)}if(e.mobile&&e.mobilePurchases!=="revenuecat"){let t=M(e.projectDir,"apps/mobile/package.json"),r=await ne(t);W(r,["react-native-purchases","react-native-purchases-ui"]),await ie(t,r)}}async function la(e){let t=M(e.projectDir,"packages/api/package.json"),r=await ne(t);W(r,["sharp","@types/sharp"]),e.cacheProvider==="upstash"&&W(r,["ioredis"]),await ie(t,r)}async function pa(e){let t=M(e.projectDir,"apps/web/package.json"),r=await ne(t);W(r,["@upstash/ratelimit","@upstash/redis"]),await ie(t,r)}async function da(e){let t=M(e.projectDir,"packages/runtime/package.json"),r=await ne(t);e.cacheProvider==="upstash"&&(W(r,["ioredis","rate-limit-redis","redis-semaphore"]),vt(r,"@upstash/redis",Mt["@upstash/redis"]),vt(r,"@upstash/lock",Mt["@upstash/lock"])),await ie(t,r)}var Sr={sentry:["@sentry/node","@sentry/browser","@sentry/electron"],posthog:["posthog-node","posthog-js"]},ua=["@sentry/electron"],ma=[...Sr.sentry,...Sr.posthog],ha=[...jt.sentry,...jt.posthog];async function fa(e){let t=M(e.projectDir,"packages/observability/package.json"),r=await ne(t),n=new Set(e.observability.flatMap(a=>Sr[a]));if(!e.desktop)for(let a of ua)n.delete(a);if(W(r,ma.filter(a=>!n.has(a))),await ie(t,r),!e.mobile)return;let i=M(e.projectDir,"apps/mobile/package.json"),o=await ne(i),s=new Set(e.observability.flatMap(a=>jt[a]));W(o,ha.filter(a=>!s.has(a))),await ie(i,o)}async function ga(e){let t=M(e.projectDir,"packages/database/package.json"),r=await ne(t);e.databaseProvider==="neon"?(W(r,["pg","@types/pg"]),vt(r,"@neondatabase/serverless",Mt["@neondatabase/serverless"])):e.databaseProvider==="supabase"&&(W(r,["pg","@types/pg"]),vt(r,"postgres",Mt.postgres)),await ie(t,r)}var ya=["app/.well-known/oauth-authorization-server/[[...path]]/route.ts","app/.well-known/oauth-protected-resource/[[...path]]/route.ts"];async function ba(e){let t=await ne(e),r=t.dependencies?.["@repo/api"];r&&(delete t.dependencies?.["@repo/api"],vt(t,"@repo/api",r,!0),await ie(e,t))}var va=["instrumentation.ts","tests/instrumentation.test.ts"];async function wa(e){if(e.architecture!=="separate")return;let t=M(e.projectDir,"apps/web");await st(M(t,"app/api/[[...rest]]/route.ts"),t);for(let r of ya)await st(M(t,r),t);for(let r of va)await st(M(t,r),t);await ba(M(t,"package.json"))}import{readFile as ka}from"fs/promises";import{join as Ea}from"path";var Sa=["base.json","node.json","next.json"],Wn="GenerateSaaS ";async function zn(e){if(!e.demo)for(let t of Sa){let r=Ea(e.projectDir,"tooling/typescript",t),n;try{n=await ka(r,"utf-8")}catch{continue}let i=JSON.parse(n);typeof i.display!="string"||!i.display.startsWith(Wn)||(i.display=i.display.slice(Wn.length),await l(r,`${JSON.stringify(i,null," ")}
1145
1145
  `))}}import{readFile as Jn,rm as Pa}from"fs/promises";import{join as wt}from"path";async function Xn(e){e.cacheProvider==="upstash"&&(await Promise.all([qn(wt(e.projectDir,"packages/runtime/tests/setup.ts")),qn(wt(e.projectDir,"packages/mail/tests/setup.ts")),Ia(e.projectDir),Pa(wt(e.projectDir,"packages/runtime/tests/redis.test.ts"),{force:!0})]),await Aa(e.projectDir))}async function Aa(e){await l(wt(e,"packages/runtime/tests/redis.test.ts"),`import { beforeEach, describe, expect, it, vi } from "vitest";
1146
1146
 
1147
1147
  /**
@@ -1295,19 +1295,19 @@ vi.mock("@upstash/lock", () => {
1295
1295
  return { Lock };
1296
1296
  });
1297
1297
 
1298
- $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 Ta}from"fs/promises";var Ra=/^[ \t]*(?:\/\/|\{\/\*|\*) gsaas:[a-z0-9-]+-(?:start|end)(?: \*\/\})?[ \t]*\r?\n/gm,_a=new Set([".ts",".tsx",".vue",".mjs",".js",".jsx",".md",".mdx"]),Zn=32;function Oa(e){let t=e.lastIndexOf(".");return t>=0&&_a.has(e.slice(t))}async function xa(e){let t=await Ta(e,"utf-8");if(!t.includes("gsaas:"))return;let r=t.replace(Ra,"");r!==t&&await l(e,r)}async function Qn(e){let t=await ye(e.projectDir,e.projectDir,r=>r==="node_modules"||r.startsWith("node_modules/")||r===".git"||r.startsWith(".git/"),Oa);for(let r=0;r<t.length;r+=Zn)await Promise.all(t.slice(r,r+Zn).map(xa))}import{readdir as Ft,readFile as se,rm as w}from"fs/promises";import{join as p}from"path";var Da=new Set(["ci.yml","desktop-release.yml","mobile-release.yml"]),Na=["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"],Ca=["packages/auth/src/config.ts","packages/api/src/routes/internal/index.ts","packages/api/src/index.ts"],La=["packages/api/src/routes/internal/index.ts","packages/api/src/routes/inngest.ts","packages/notifications/src/index.ts","packages/config/src/cache.ts"],$a=["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"],ja=["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"],Ua=["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"],Ma=["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"],Fa=[{dir:"apps/mobile/components/billing",prefixes:["paywall","credit-packs","store-"]},{dir:"apps/mobile/tests",prefixes:["purchases-"]}],Va=["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"],Ba=[","," {",' 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(`
1299
- `),Ga=[["## 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
+ $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 Ta}from"fs/promises";var Ra=/^[ \t]*(?:\/\/|\{\/\*|\*) gsaas:[a-z0-9-]+-(?:start|end)(?: \*\/\})?[ \t]*\r?\n/gm,_a=new Set([".ts",".tsx",".vue",".mjs",".js",".jsx",".md",".mdx"]),Zn=32;function Oa(e){let t=e.lastIndexOf(".");return t>=0&&_a.has(e.slice(t))}async function xa(e){let t=await Ta(e,"utf-8");if(!t.includes("gsaas:"))return;let r=t.replace(Ra,"");r!==t&&await l(e,r)}async function Qn(e){let t=await ye(e.projectDir,e.projectDir,r=>r==="node_modules"||r.startsWith("node_modules/")||r===".git"||r.startsWith(".git/"),Oa);for(let r=0;r<t.length;r+=Zn)await Promise.all(t.slice(r,r+Zn).map(xa))}import{readdir as Ft,readFile as se,rm as w}from"fs/promises";import{join as p}from"path";var Da=new Set(["ci.yml","desktop-release.yml","mobile-release.yml"]),Na=["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"],Ca=["packages/auth/src/config.ts","packages/api/src/routes/internal/index.ts","packages/api/src/index.ts"],La=["packages/api/src/routes/internal/index.ts","packages/api/src/routes/inngest.ts","packages/notifications/src/index.ts","packages/config/src/cache.ts"],$a=["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"],ja=["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"],Ma=["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"],Ua=["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"],Fa=[{dir:"apps/mobile/components/billing",prefixes:["paywall","credit-packs","store-"]},{dir:"apps/mobile/tests",prefixes:["purchases-"]}],Va=["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"],Ba=[","," {",' 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(`
1299
+ `),Ka=[["## 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(`
1300
1300
  `),["#### 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(`
1301
1301
  `),["#### 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(`
1302
- `)],Ka=["apps/web/app/[locale]/(marketing)/download","apps/web/tests/routes/download-page.test.tsx"],Ha=["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"];async function ei(e){let t=p(e.projectDir,".github/workflows"),r=await Ft(t).catch(()=>[]);for(let n of r)Da.has(n)||await w(p(t,n),{recursive:!0,force:!0})}async function Ya(e,t){let r=p(e,"package.json"),n=JSON.parse(await se(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 se(r,"utf-8"))?.[1]??" ";await l(r,`${JSON.stringify(n,null,o)}
1302
+ `)],Ga=["apps/web/app/[locale]/(marketing)/download","apps/web/tests/routes/download-page.test.tsx"],Ha=["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"];async function ei(e){let t=p(e.projectDir,".github/workflows"),r=await Ft(t).catch(()=>[]);for(let n of r)Da.has(n)||await w(p(t,n),{recursive:!0,force:!0})}async function Ya(e,t){let r=p(e,"package.json"),n=JSON.parse(await se(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 se(r,"utf-8"))?.[1]??" ";await l(r,`${JSON.stringify(n,null,o)}
1303
1303
  `)}async function ti(e){await Ya(e.projectDir,[...Ha])}async function ri(e){let t=p(e.projectDir,"package.json"),r=await se(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," ")}
1304
1304
  `))}async function ni(e){let t=p(e.projectDir,"package.json"),r=JSON.parse(await se(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," ")}
1305
- `)}async function ii(e){e.docs||await w(p(e.projectDir,"apps/docs"),{recursive:!0})}async function si(e){let t=p(e.projectDir,"docs/(platform)/meta.json"),r=JSON.parse(await se(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 Ue(t,r," ","stripUnusedDocsSilo"),e.desktop||await w(p(e.projectDir,"docs/desktop"),{recursive:!0,force:!0}),e.mobile||await w(p(e.projectDir,"docs/mobile"),{recursive:!0,force:!0}),await Wa(e),await Ja(e)}async function z(e,t,r,n){await l(e,je(await se(e,"utf-8"),t,r,n))}async function Wa(e){let t=e.projectDir,r="stripUnusedDocsSilo",n=p(t,"docs/(platform)/configuration.mdx"),i=e.desktop;e.desktop||(await T(n,"| Desktop app + device sign-in - see [desktop app](/desktop) |","| Desktop app + device sign-in |","configuration.mdx desktop config-table link",r),await z(n,"configuration.mdx desktop TRUSTED_ORIGINS callout","desktop-origins-docs",r),await T(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 z(p(t,"docs/(platform)/payments/organizations.mdx"),"payments organizations.mdx desktop tenancy callout","desktop-tenancy-docs",r),await z(p(t,"docs/(platform)/projects.mdx"),"projects.mdx desktop scoping callout","desktop-projects-docs",r),await z(p(t,"docs/(platform)/client-architecture.mdx"),"client-architecture.mdx desktop card","desktop-architecture-docs",r),await z(p(t,"docs/(platform)/screens.mdx"),"screens.mdx desktop halves","screens-desktop-docs",r),await z(p(t,"docs/(platform)/testing/e2e.mdx"),"testing/e2e.mdx desktop e2e callout","desktop-testing-docs",r),await T(p(t,"docs/(platform)/index.mdx"),` <Card href="/desktop" title="Desktop app" description="Cross-platform desktop app with device sign-in and AI agents." />
1306
- `,"","index.mdx desktop app card",r),await z(p(t,"docs/(platform)/ai.mdx"),"ai.mdx automation lanes row + section","desktop-lanes-docs",r)),i||(await z(n,"configuration.mdx desktop-AI paragraph","desktop-ai-docs",r),await T(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))}async function oi(e){if(e.desktop)return;await w(p(e.projectDir,"apps/desktop"),{recursive:!0});let t=p(e.projectDir,"packages/i18n/translations");for(let i of await Ft(t,{withFileTypes:!0}))i.isDirectory()&&await w(p(t,i.name,"desktop.json"),{force:!0});await w(p(e.projectDir,".github/workflows/desktop-release.yml"));for(let i of Ka)await w(p(e.projectDir,i),{recursive:!0});await w(p(e.projectDir,"packages/utils/src/desktop-download.ts")),await w(p(e.projectDir,"packages/utils/tests/desktop-download.test.ts"));let r=p(e.projectDir,"packages/utils/package.json"),n=JSON.parse(await se(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 Ue(r,n," ","stripDesktopApp")}function za(e){return e.desktop}async function ai(e){za(e)||await w(p(e.projectDir,"packages/agent-core"),{recursive:!0})}async function qa(e,t,r,n){let i=await Ft(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 w(p(e,a),{recursive:!0})}}async function Mt(e,t,r,n){await l(e,je(await se(e,"utf-8"),t,r,n))}async function Ja(e){let t=e.projectDir,r="stripUnusedDocsSilo",n=e.mobile&&e.mobilePurchases==="revenuecat";e.mobile||(await T(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
+ `)}async function ii(e){e.docs||await w(p(e.projectDir,"apps/docs"),{recursive:!0})}async function si(e){let t=p(e.projectDir,"docs/(platform)/meta.json"),r=JSON.parse(await se(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 Me(t,r," ","stripUnusedDocsSilo"),e.desktop||await w(p(e.projectDir,"docs/desktop"),{recursive:!0,force:!0}),e.mobile||await w(p(e.projectDir,"docs/mobile"),{recursive:!0,force:!0}),await Wa(e),await Ja(e)}async function z(e,t,r,n){await l(e,je(await se(e,"utf-8"),t,r,n))}async function Wa(e){let t=e.projectDir,r="stripUnusedDocsSilo",n=p(t,"docs/(platform)/configuration.mdx"),i=e.desktop;e.desktop||(await T(n,"| Desktop app + device sign-in - see [desktop app](/desktop) |","| Desktop app + device sign-in |","configuration.mdx desktop config-table link",r),await z(n,"configuration.mdx desktop TRUSTED_ORIGINS callout","desktop-origins-docs",r),await T(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 z(p(t,"docs/(platform)/payments/organizations.mdx"),"payments organizations.mdx desktop tenancy callout","desktop-tenancy-docs",r),await z(p(t,"docs/(platform)/projects.mdx"),"projects.mdx desktop scoping callout","desktop-projects-docs",r),await z(p(t,"docs/(platform)/client-architecture.mdx"),"client-architecture.mdx desktop card","desktop-architecture-docs",r),await z(p(t,"docs/(platform)/screens.mdx"),"screens.mdx desktop halves","screens-desktop-docs",r),await z(p(t,"docs/(platform)/testing/e2e.mdx"),"testing/e2e.mdx desktop e2e callout","desktop-testing-docs",r),await T(p(t,"docs/(platform)/index.mdx"),` <Card href="/desktop" title="Desktop app" description="Cross-platform desktop app with device sign-in and AI agents." />
1306
+ `,"","index.mdx desktop app card",r),await z(p(t,"docs/(platform)/ai.mdx"),"ai.mdx automation lanes row + section","desktop-lanes-docs",r)),i||(await z(n,"configuration.mdx desktop-AI paragraph","desktop-ai-docs",r),await T(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))}async function oi(e){if(e.desktop)return;await w(p(e.projectDir,"apps/desktop"),{recursive:!0});let t=p(e.projectDir,"packages/i18n/translations");for(let i of await Ft(t,{withFileTypes:!0}))i.isDirectory()&&await w(p(t,i.name,"desktop.json"),{force:!0});await w(p(e.projectDir,".github/workflows/desktop-release.yml"));for(let i of Ga)await w(p(e.projectDir,i),{recursive:!0});await w(p(e.projectDir,"packages/utils/src/desktop-download.ts")),await w(p(e.projectDir,"packages/utils/tests/desktop-download.test.ts"));let r=p(e.projectDir,"packages/utils/package.json"),n=JSON.parse(await se(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 Me(r,n," ","stripDesktopApp")}function za(e){return e.desktop}async function ai(e){za(e)||await w(p(e.projectDir,"packages/agent-core"),{recursive:!0})}async function qa(e,t,r,n){let i=await Ft(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 w(p(e,a),{recursive:!0})}}async function Ut(e,t,r,n){await l(e,je(await se(e,"utf-8"),t,r,n))}async function Ja(e){let t=e.projectDir,r="stripUnusedDocsSilo",n=e.mobile&&e.mobilePurchases==="revenuecat";e.mobile||(await T(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." />
1307
1307
  `,"","index.mdx mobile app card",r),await T(p(t,"docs/(platform)/configuration.mdx")," - see [mobile app](/mobile)","","configuration.mdx mobile config-table link",r),await T(p(t,"docs/(platform)/client-architecture.mdx"),` <Card href="/mobile" title="Mobile app">The Expo app - the third surface this client core serves.</Card>
1308
- `,"","client-architecture.mdx mobile app card",r),await T(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 T(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 T(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 T(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 T(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 z(p(t,"docs/(platform)/testing/e2e.mdx"),"testing e2e.mdx mobile Maestro section","mobile-testing-docs",r),await z(p(t,"docs/(platform)/environment-variables/index.mdx"),"environment-variables mobile section","mobile-env-docs",r)),n||(await T(p(t,"docs/(platform)/payments/meta.json"),', "mobile-billing"',"","payments meta.json mobile-billing entry",r),await T(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." />
1308
+ `,"","client-architecture.mdx mobile app card",r),await T(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 T(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 T(p(t,"docs/(platform)/authentication/social-oauth.mdx")," - the native flow adds `APPLE_APP_BUNDLE_IDENTIFIER`; without `APPLE_CLIENT_ID` it runs native-only (no web button), see [mobile auth](/mobile/auth) |"," |","social-oauth.mdx Apple native-flow tail",r),await T(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 T(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 z(p(t,"docs/(platform)/testing/e2e.mdx"),"testing e2e.mdx mobile Maestro section","mobile-testing-docs",r),await z(p(t,"docs/(platform)/environment-variables/index.mdx"),"environment-variables mobile section","mobile-env-docs",r)),n||(await T(p(t,"docs/(platform)/payments/meta.json"),', "mobile-billing"',"","payments meta.json mobile-billing entry",r),await T(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." />
1309
1309
  `,"","payments index.mdx mobile store billing card",r),await w(p(t,"docs/(platform)/payments/mobile-billing.mdx"))),e.mobile&&!n&&(await T(p(t,"docs/mobile/meta.json"),', "billing"',"","mobile meta.json billing entry",r),await T(p(t,"docs/mobile/index.mdx"),` <Card href="/mobile/billing" title="Billing" description="Store purchases through RevenueCat, and the web-billing shape." />
1310
- `,"","mobile index.mdx billing card",r),await z(p(t,"docs/mobile/index.mdx"),"mobile index.mdx store-purchases block","mobile-purchases-docs",r),await w(p(t,"docs/mobile/billing"),{recursive:!0,force:!0}))}async function ci(e){if(e.mobile)return;await w(p(e.projectDir,"apps/mobile"),{recursive:!0});let t=p(e.projectDir,"packages/i18n/translations");for(let i of await Ft(t,{withFileTypes:!0}))i.isDirectory()&&await w(p(t,i.name,"mobile.json"),{force:!0});await w(p(e.projectDir,"packages/i18n/tests/mobile-notification-labels.test.ts")),await w(p(e.projectDir,"packages/i18n/tests/mobile-brand-literals.test.ts")),await w(p(e.projectDir,"packages/i18n/tests/mobile-shared-merge.test.ts")),await w(p(e.projectDir,"packages/auth/tests/expo-plugin.test.ts")),await w(p(e.projectDir,"packages/auth/tests/expo-authorization-allowlist.test.ts")),await w(p(e.projectDir,"packages/auth/src/expo-authorization-allowlist.ts")),await w(p(e.projectDir,".github/workflows/mobile-release.yml")),await w(p(e.projectDir,"tooling/styles/scripts/build-mobile-css.ts"));let r=p(e.projectDir,"tooling/styles/package.json"),n=JSON.parse(await se(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 Ue(r,n," ","stripMobileApp");for(let i of Na)await st(p(e.projectDir,i),p(e.projectDir,"apps/web"));await w(p(e.projectDir,"packages/api/src/routes/internal/well-known.ts")),await w(p(e.projectDir,"packages/api/tests/routes/well-known-mobile.test.ts")),await w(p(e.projectDir,"packages/api/src/routes/internal/mobile-auth.ts")),await w(p(e.projectDir,"packages/api/tests/routes/mobile-auth.test.ts")),await w(p(e.projectDir,"apps/web/public/images/stores"),{recursive:!0});for(let i of Ca)await Mt(p(e.projectDir,i),i,"mobile-auth","stripMobileApp");for(let i of La)await Mt(p(e.projectDir,i),i,"mobile-push","stripMobileApp");for(let i of $a)await w(p(e.projectDir,i),{recursive:!0})}async function li(e){if(e.mobile&&e.mobilePurchases==="revenuecat")return;for(let n of ja)await w(p(e.projectDir,n),{recursive:!0});let t=p(e.projectDir,"packages/payments/package.json"),r=JSON.parse(await se(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 Ue(t,r," ","stripMobilePurchases");for(let n of Ua)await Mt(p(e.projectDir,n),n,"mobile-purchases","stripMobilePurchases");if(e.mobile){for(let n of Ma)await w(p(e.projectDir,n),{recursive:!0});for(let{dir:n,prefixes:i}of Fa)await qa(p(e.projectDir,n),i,n,"stripMobilePurchases");for(let n of Va)await Mt(p(e.projectDir,n),n,"mobile-purchases","stripMobilePurchases");for(let n of Ga)await T(p(e.projectDir,"apps/mobile/e2e/README.md"),n,"","e2e README purchases row block","stripMobilePurchases");await T(p(e.projectDir,"apps/mobile/e2e/tags.ts"),Ba,`
1310
+ `,"","mobile index.mdx billing card",r),await z(p(t,"docs/mobile/index.mdx"),"mobile index.mdx store-purchases block","mobile-purchases-docs",r),await w(p(t,"docs/mobile/billing"),{recursive:!0,force:!0}))}async function ci(e){if(e.mobile)return;await w(p(e.projectDir,"apps/mobile"),{recursive:!0});let t=p(e.projectDir,"packages/i18n/translations");for(let i of await Ft(t,{withFileTypes:!0}))i.isDirectory()&&await w(p(t,i.name,"mobile.json"),{force:!0});await w(p(e.projectDir,"packages/i18n/tests/mobile-notification-labels.test.ts")),await w(p(e.projectDir,"packages/i18n/tests/mobile-brand-literals.test.ts")),await w(p(e.projectDir,"packages/i18n/tests/mobile-shared-merge.test.ts")),await w(p(e.projectDir,"packages/auth/tests/expo-plugin.test.ts")),await w(p(e.projectDir,"packages/auth/tests/expo-authorization-allowlist.test.ts")),await w(p(e.projectDir,"packages/auth/src/expo-authorization-allowlist.ts")),await w(p(e.projectDir,".github/workflows/mobile-release.yml")),await w(p(e.projectDir,"tooling/styles/scripts/build-mobile-css.ts"));let r=p(e.projectDir,"tooling/styles/package.json"),n=JSON.parse(await se(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 Me(r,n," ","stripMobileApp");for(let i of Na)await st(p(e.projectDir,i),p(e.projectDir,"apps/web"));await w(p(e.projectDir,"packages/api/src/routes/internal/well-known.ts")),await w(p(e.projectDir,"packages/api/tests/routes/well-known-mobile.test.ts")),await w(p(e.projectDir,"packages/api/src/routes/internal/mobile-auth.ts")),await w(p(e.projectDir,"packages/api/tests/routes/mobile-auth.test.ts")),await w(p(e.projectDir,"apps/web/public/images/stores"),{recursive:!0});for(let i of Ca)await Ut(p(e.projectDir,i),i,"mobile-auth","stripMobileApp");for(let i of La)await Ut(p(e.projectDir,i),i,"mobile-push","stripMobileApp");for(let i of $a)await w(p(e.projectDir,i),{recursive:!0})}async function li(e){if(e.mobile&&e.mobilePurchases==="revenuecat")return;for(let n of ja)await w(p(e.projectDir,n),{recursive:!0});let t=p(e.projectDir,"packages/payments/package.json"),r=JSON.parse(await se(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 Me(t,r," ","stripMobilePurchases");for(let n of Ma)await Ut(p(e.projectDir,n),n,"mobile-purchases","stripMobilePurchases");if(e.mobile){for(let n of Ua)await w(p(e.projectDir,n),{recursive:!0});for(let{dir:n,prefixes:i}of Fa)await qa(p(e.projectDir,n),i,n,"stripMobilePurchases");for(let n of Va)await Ut(p(e.projectDir,n),n,"mobile-purchases","stripMobilePurchases");for(let n of Ka)await T(p(e.projectDir,"apps/mobile/e2e/README.md"),n,"","e2e README purchases row block","stripMobilePurchases");await T(p(e.projectDir,"apps/mobile/e2e/tags.ts"),Ba,`
1311
1311
  `,"e2e tags.ts purchases inventory entries","stripMobilePurchases")}}import{readFile as Xa}from"fs/promises";import{join as Za}from"path";var pi=`on:
1312
1312
  workflow_run:
1313
1313
  workflows: ["Tag Release"]
@@ -1510,11 +1510,11 @@ ${d}${g}`}import{readFile as yi}from"fs/promises";import{existsSync as sc}from"f
1510
1510
  export { ops } from "./${r}/index";
1511
1511
  `;await l(Vt(t,"providers/index.ts"),i),await l(Vt(t,"index.ts"),await mc(t,r))}async function mc(e,t){let r=Vt(e,"index.ts");if(!dc(r))throw new Error(`generatePaymentBarrel: expected ${r} to exist - did packages/payments move?`);let n=await pc(r,"utf-8"),i=wi.filter(s=>s!==t);return n.split(`
1512
1512
  `).filter(s=>!i.some(a=>s.includes(`./providers/${a}/`))).join(`
1513
- `)}import{access as hc,readFile as Kt,rm as Bt}from"fs/promises";import{join as q}from"path";var Ii=["sentry","posthog"],Ar={sentry:"src/providers/sentry",posthog:"src/providers/posthog"},Gt={sentry:["tests/sentry-load-guard.test.ts"],posthog:["tests/posthog-load-guard.test.ts","tests/llm-analytics.test.ts"]},fc=["tests/resolution.test.ts","tests/export-isolation.test.ts"],gc=["src/providers/sentry/desktop-main.ts","src/providers/sentry/desktop-renderer.ts","src/resolve-desktop-main.ts","src/resolve-desktop-renderer.ts"],yc=["./desktop-main","./desktop-renderer"],bc=[`vi.mock("@sentry/electron/main", sdk.factories["@sentry/electron/main"]);
1513
+ `)}import{access as hc,readFile as Gt,rm as Bt}from"fs/promises";import{join as q}from"path";var Ii=["sentry","posthog"],Ar={sentry:"src/providers/sentry",posthog:"src/providers/posthog"},Kt={sentry:["tests/sentry-load-guard.test.ts"],posthog:["tests/posthog-load-guard.test.ts","tests/llm-analytics.test.ts"]},fc=["tests/resolution.test.ts","tests/export-isolation.test.ts"],gc=["src/providers/sentry/desktop-main.ts","src/providers/sentry/desktop-renderer.ts","src/resolve-desktop-main.ts","src/resolve-desktop-renderer.ts"],yc=["./desktop-main","./desktop-renderer"],bc=[`vi.mock("@sentry/electron/main", sdk.factories["@sentry/electron/main"]);
1514
1514
  `,`vi.mock("@sentry/electron/renderer", sdk.factories["@sentry/electron/renderer"]);
1515
1515
  `],vc=[' describeGate("desktop main", "electronMain", async () => {',' describeGate("desktop renderer", "electronRenderer", async () => {',' describe("the desktop main init options", () => {',' describe("the desktop renderer init", () => {'],Ei=`
1516
1516
  });
1517
- `,wc=" ";async function Ti(e){let t=q(e.projectDir,"packages/observability");await Si(t,"packages/observability");let r=e.observability,n=r.filter(s=>!F.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: ${F.join(", ")}. Shaping stops here so the project is not left half-stripped.`);for(let s of r)await Si(q(t,Ar[s]),`packages/observability/${Ar[s]}`);let i=e.desktop?[...Ai,...ul]:Ai,o=new Map;for(let s of i)o.set(s.file,await Kt(q(t,s.file),"utf-8"));for(let s of Oc(r,e.desktop))await xc(t,s);for(let s of Ii)if(!r.includes(s)){await Bt(q(t,Ar[s]),{recursive:!0,force:!0});for(let a of Gt[s])await Bt(q(t,a),{force:!0})}for(let s of fc)await Bt(q(t,s),{force:!0});e.desktop||await kc(t,r);for(let s of i){let a=o.get(s.file);a!==void 0&&await l(q(t,s.file),Pc(a,s,r))}}async function Si(e,t){try{await hc(e)}catch{throw new Error(`generateObservabilityProvider: expected ${t} to exist - did the observability package move?`)}}async function kc(e,t){for(let i of gc)await Bt(q(e,i),{force:!0});t.includes("sentry")&&await Ec(e);let r=q(e,"package.json"),n=JSON.parse(await Kt(r,"utf-8"));for(let i of yc){if(!n.exports?.[i])throw new Error(`generateObservabilityProvider: expected packages/observability/package.json to carry the ${i} export`);delete n.exports[i]}await Ue(r,n," ","generateObservabilityProvider")}async function Ec(e){let t=q(e,Gt.sentry[0]),r=await Kt(t,"utf-8");r=Ir(r,bc.map(n=>({find:n,replace:""})),Gt.sentry[0]);for(let n of vc)r=Sc(r,n,Gt.sentry[0]);await l(t,r)}function Sc(e,t,r){let n=e.indexOf(t),i=n===-1?-1:e.indexOf(Ei,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.includes(t,n+t.length))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(`
1517
+ `,wc=" ";async function Ti(e){let t=q(e.projectDir,"packages/observability");await Si(t,"packages/observability");let r=e.observability,n=r.filter(s=>!F.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: ${F.join(", ")}. Shaping stops here so the project is not left half-stripped.`);for(let s of r)await Si(q(t,Ar[s]),`packages/observability/${Ar[s]}`);let i=e.desktop?[...Ai,...ul]:Ai,o=new Map;for(let s of i)o.set(s.file,await Gt(q(t,s.file),"utf-8"));for(let s of Oc(r,e.desktop))await xc(t,s);for(let s of Ii)if(!r.includes(s)){await Bt(q(t,Ar[s]),{recursive:!0,force:!0});for(let a of Kt[s])await Bt(q(t,a),{force:!0})}for(let s of fc)await Bt(q(t,s),{force:!0});e.desktop||await kc(t,r);for(let s of i){let a=o.get(s.file);a!==void 0&&await l(q(t,s.file),Pc(a,s,r))}}async function Si(e,t){try{await hc(e)}catch{throw new Error(`generateObservabilityProvider: expected ${t} to exist - did the observability package move?`)}}async function kc(e,t){for(let i of gc)await Bt(q(e,i),{force:!0});t.includes("sentry")&&await Ec(e);let r=q(e,"package.json"),n=JSON.parse(await Gt(r,"utf-8"));for(let i of yc){if(!n.exports?.[i])throw new Error(`generateObservabilityProvider: expected packages/observability/package.json to carry the ${i} export`);delete n.exports[i]}await Me(r,n," ","generateObservabilityProvider")}async function Ec(e){let t=q(e,Kt.sentry[0]),r=await Gt(t,"utf-8");r=Ir(r,bc.map(n=>({find:n,replace:""})),Kt.sentry[0]);for(let n of vc)r=Sc(r,n,Kt.sentry[0]);await l(t,r)}function Sc(e,t,r){let n=e.indexOf(t),i=n===-1?-1:e.indexOf(Ei,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.includes(t,n+t.length))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(`
1518
1518
  `).find(a=>a.trim().length>0&&!a.startsWith(`${wc} `));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+Ei.length;return e[s]===`
1519
1519
  `&&(s+=1),e.slice(0,n)+e.slice(s)}function Pc(e,t,r){let n=Ii.filter(s=>!r.includes(s)),i=e,o=[];for(let s of n){let a=Ac(i,s);i=a.source,o.push(...a.bindings)}t.dualMode!==!1&&(i=Ir(i,[{find:Tc,replace:Rc(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){let o=n.indexOf(i.find);if(o===-1)throw new Error(`generateObservabilityProvider: ${r} no longer contains the text this strip rewrites - re-point the anchor. Missing: ${i.find.split(`
1520
1520
  `)[0]}`);if(i.all!==!0&&n.includes(i.find,o+i.find.length))throw new Error(`generateObservabilityProvider: ${r} now contains the text this strip rewrites more than once, so rewriting the first would leave the rest in the buyer's copy - make the anchor unique or mark the edit \`all\`. Ambiguous: ${i.find.split(`
@@ -1526,7 +1526,7 @@ export { ops } from "./${r}/index";
1526
1526
  * run \`generatesaas update\`: re-shaping brings that provider's adapter and its dependency back. The
1527
1527
  * update stages nothing while you are already on the latest version, so the switch lands with the
1528
1528
  * next release. Pasting the other provider's credential into \`config.observability\` on its own
1529
- * cannot: the adapter it would need is not here.`}var Pi={source:"src/providers/sentry/browser.ts",name:"SentryBrowserInitOptions",fields:["environment: string;"]},_c={source:"src/providers/sentry/desktop-main.ts",name:"SentryDesktopMainInitOptions",fields:["environment: string;","appIsReady: boolean;"]};function Oc(e,t){return e.includes("sentry")?[]:t?[Pi,_c]:[Pi]}async function xc(e,t){let r=await Kt(q(e,t.source),"utf-8"),n=`export interface ${t.name} {
1529
+ * cannot: the adapter it would need is not here.`}var Pi={source:"src/providers/sentry/browser.ts",name:"SentryBrowserInitOptions",fields:["environment: string;"]},_c={source:"src/providers/sentry/desktop-main.ts",name:"SentryDesktopMainInitOptions",fields:["environment: string;","appIsReady: boolean;"]};function Oc(e,t){return e.includes("sentry")?[]:t?[Pi,_c]:[Pi]}async function xc(e,t){let r=await Gt(q(e,t.source),"utf-8"),n=`export interface ${t.name} {
1530
1530
  `,i=r.indexOf(n),o=r.indexOf(`
1531
1531
  }
1532
1532
  `,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(`
@@ -1539,8 +1539,8 @@ export { ops } from "./${r}/index";
1539
1539
  }
1540
1540
  `,$c=` if (observability?.sentry?.dsn) ops.push(sentryBrowserOps);
1541
1541
  `,jc=` if (observability?.posthog?.publicKey) ops.push(posthogBrowserOps);
1542
- `,Uc=` if (config.observability?.sentry?.dsn) await initSentryBrowser(options);
1543
- `,Mc=` if (observability?.posthog?.publicKey && !observability?.sentry?.dsn) {
1542
+ `,Mc=` if (config.observability?.sentry?.dsn) await initSentryBrowser(options);
1543
+ `,Uc=` if (observability?.posthog?.publicKey && !observability?.sentry?.dsn) {
1544
1544
  announceMissingDesktopSdk("posthog");
1545
1545
  }
1546
1546
  `,Fc=` // PostHog ships no desktop SDK, so a project configured ONLY for PostHog reports nothing here -
@@ -1565,7 +1565,7 @@ const announceMissingDesktopSdk = createProviderGapAnnouncer({
1565
1565
  remedy: "Select sentry to report errors from the desktop app."
1566
1566
  });
1567
1567
 
1568
- `,Gc=`/**
1568
+ `,Kc=`/**
1569
1569
  * Say once that the selected provider cannot report from the desktop renderer.
1570
1570
  *
1571
1571
  * A WARNING and never an error: PostHog ships no Electron SDK, and a buyer running PostHog on the
@@ -1579,7 +1579,7 @@ const announceMissingDesktopSdk = createProviderGapAnnouncer({
1579
1579
  remedy: "Select sentry to report errors from the desktop app."
1580
1580
  });
1581
1581
 
1582
- `,Kc=` const observability = config.observability;
1582
+ `,Gc=` const observability = config.observability;
1583
1583
  `,Hc=`import { config } from "@repo/config";
1584
1584
  `,Yc=` // No error-reporting adapter was generated for this project, so there is nothing to arm.
1585
1585
  `,Wc="export type BrowserReportingOptions = SentryBrowserInitOptions;",zc=`export interface BrowserReportingOptions {
@@ -1594,9 +1594,9 @@ const announceMissingDesktopSdk = createProviderGapAnnouncer({
1594
1594
  appIsReady: boolean;
1595
1595
  }`,Qc=` return observability?.sentry?.dsn ? [sentryDesktopRendererOps] : [];
1596
1596
  `,el=` if (observability?.sentry?.dsn) await initSentryDesktopRenderer();
1597
- `;function Ri(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 tl(e){let t=[{find:Dc,replace:" into every browser bundle."},...Ri(e,{sentry:Nc,posthog:Cc})];return e.includes("sentry")||t.push({find:Lc,replace:""}),t}function rl(e){let t=[...Ri(e,{sentry:$c,posthog:jc})];return e.includes("sentry")||t.push({find:Uc,replace:""},nl),t}var nl={find:Wc,replace:zc};function il(e){let t=[..._i(e,Bc)];return e.includes("sentry")||t.push({find:qc,replace:` return [];
1598
- `},{find:Jc,replace:xi(e)},{find:Xc,replace:Zc}),t.push(...Oi(e)),t}function sl(e){let t=[..._i(e,Gc)];return e.includes("sentry")||t.push({find:Qc,replace:` return [];
1599
- `},{find:el,replace:xi(e)}),t.push(...Oi(e)),t}function _i(e,t){return e.includes("posthog")?[]:[{find:Fc,replace:""},{find:Mc,replace:"",all:!0},{find:t,replace:""},{find:Vc,replace:""}]}function Oi(e){return e.length>0?[]:[{find:Kc,replace:"",all:!0},{find:Hc,replace:""}]}function xi(e){return e.length>0?"":Yc}var ol=` if (!posthogConfigured()) return;
1597
+ `;function Ri(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 tl(e){let t=[{find:Dc,replace:" into every browser bundle."},...Ri(e,{sentry:Nc,posthog:Cc})];return e.includes("sentry")||t.push({find:Lc,replace:""}),t}function rl(e){let t=[...Ri(e,{sentry:$c,posthog:jc})];return e.includes("sentry")||t.push({find:Mc,replace:""},nl),t}var nl={find:Wc,replace:zc};function il(e){let t=[..._i(e,Bc)];return e.includes("sentry")||t.push({find:qc,replace:` return [];
1598
+ `},{find:Jc,replace:xi(e)},{find:Xc,replace:Zc}),t.push(...Oi(e)),t}function sl(e){let t=[..._i(e,Kc)];return e.includes("sentry")||t.push({find:Qc,replace:` return [];
1599
+ `},{find:el,replace:xi(e)}),t.push(...Oi(e)),t}function _i(e,t){return e.includes("posthog")?[]:[{find:Fc,replace:""},{find:Uc,replace:"",all:!0},{find:t,replace:""},{find:Vc,replace:""}]}function Oi(e){return e.length>0?[]:[{find:Gc,replace:"",all:!0},{find:Hc,replace:""}]}function xi(e){return e.length>0?"":Yc}var ol=` if (!posthogConfigured()) return;
1600
1600
  try {
1601
1601
  const { captureAiGeneration: capture } = await import("./providers/posthog/llm");
1602
1602
  await capture(event);
@@ -1619,15 +1619,15 @@ function posthogConfigured(): boolean {
1619
1619
  }
1620
1620
  `;function dl(e){return e.includes("posthog")?[]:[{find:ol,replace:al},{find:pl,replace:cl},{find:ll,replace:""},{find:`import { logReportingFailure } from "./logger";
1621
1621
  `,replace:""},{find:`import { config } from "@repo/config";
1622
- `,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 Ai=[{file:"src/resolve-server.ts",edits:tl},{file:"src/resolve-browser.ts",edits:rl},{file:"src/llm-analytics.ts",edits:dl,dualMode:!1}],ul=[{file:"src/resolve-desktop-main.ts",edits:il},{file:"src/resolve-desktop-renderer.ts",edits:sl}];import{readdir as ml,readFile as hl}from"fs/promises";import{join as Di}from"path";async function Ni(e){if(e.demo)return;let t=e.appName.trim()||e.projectName,r=JSON.stringify(t).slice(1,-1),n=Di(e.projectDir,"packages/i18n/translations"),i;try{i=await ml(n)}catch{return}for(let o of i){let s=Di(n,o,"web.json"),a;try{a=await hl(s,"utf-8")}catch{continue}let d=a.replaceAll("GenerateSaaS",()=>r);d!==a&&await l(s,d)}}async function Ht(e){let t=e.projectDir;e.demo||(await gn(t),await yn(t),await bn(t),await vn(t)),await wn(t),await Sn(e),await An(e),await In(e),e.demo||await Tn(e),await On(e),await xn(e),await Dn(e),await $n(e),await Un(e),await Fn(e),await Yn(e),await zn(e),await Xn(e),await ei(e),await gi(e),await ti(e),await ri(e),await ni(e),await ii(e),await si(e),await oi(e),await ai(e),await di(e),await ci(e),await li(e),await mi(e),await vi(e),await ki(e),await Ti(e),await Kn(e),await Ni(e),await Qn(e)}import{basename as Li,join as $i,relative as gl}from"path";import{createHash as Ci}from"crypto";import{readFile as fl}from"fs/promises";async function Yt(e){let t=await fl(e);return Ci("sha256").update(t).digest("hex")}function Tr(e){return Ci("sha256").update(e).digest("hex")}var yl=new Set(["data",Y]);function bl(e){let t=e.split("/");for(let r of t)if(cr.has(r)||lr.has(r)||yl.has(r)||r.startsWith(".env")&&!r.includes("example"))return!0;return!1}function ji(e,t){return{projectName:e.projectName??Li(t),appName:e.appName??e.projectName??Li(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,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 vl(e,t){return{version:e.version,initialVersion:e.version,appName:e.appName,projectName:e.projectName,frontend:tn,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&&re(e)?{mobilePurchases:e.mobilePurchases}:{},ai:e.ai,webAi:e.webAi,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 Ui(e,t){let n=(await ye(e.projectDir,e.projectDir,bl)).sort(),i=await Promise.all(n.map(async a=>[$e(gl(e.projectDir,a)),await Yt(a)])),o=Object.fromEntries(i),s=vl(e,t);await l($i(e.projectDir,he),`${JSON.stringify(s,null," ")}
1622
+ `,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 Ai=[{file:"src/resolve-server.ts",edits:tl},{file:"src/resolve-browser.ts",edits:rl},{file:"src/llm-analytics.ts",edits:dl,dualMode:!1}],ul=[{file:"src/resolve-desktop-main.ts",edits:il},{file:"src/resolve-desktop-renderer.ts",edits:sl}];import{readdir as ml,readFile as hl}from"fs/promises";import{join as Di}from"path";async function Ni(e){if(e.demo)return;let t=e.appName.trim()||e.projectName,r=JSON.stringify(t).slice(1,-1),n=Di(e.projectDir,"packages/i18n/translations"),i;try{i=await ml(n)}catch{return}for(let o of i){let s=Di(n,o,"web.json"),a;try{a=await hl(s,"utf-8")}catch{continue}let d=a.replaceAll("GenerateSaaS",()=>r);d!==a&&await l(s,d)}}async function Ht(e){let t=e.projectDir;e.demo||(await gn(t),await yn(t),await bn(t),await vn(t)),await wn(t),await Sn(e),await An(e),await In(e),e.demo||await Tn(e),await On(e),await xn(e),await Dn(e),await $n(e),await Mn(e),await Fn(e),await Yn(e),await zn(e),await Xn(e),await ei(e),await gi(e),await ti(e),await ri(e),await ni(e),await ii(e),await si(e),await oi(e),await ai(e),await di(e),await ci(e),await li(e),await mi(e),await vi(e),await ki(e),await Ti(e),await Gn(e),await Ni(e),await Qn(e)}import{basename as Li,join as $i,relative as gl}from"path";import{createHash as Ci}from"crypto";import{readFile as fl}from"fs/promises";async function Yt(e){let t=await fl(e);return Ci("sha256").update(t).digest("hex")}function Tr(e){return Ci("sha256").update(e).digest("hex")}var yl=new Set(["data",Y]);function bl(e){let t=e.split("/");for(let r of t)if(cr.has(r)||lr.has(r)||yl.has(r)||r.startsWith(".env")&&!r.includes("example"))return!0;return!1}function ji(e,t){return{projectName:e.projectName??Li(t),appName:e.appName??e.projectName??Li(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,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 vl(e,t){return{version:e.version,initialVersion:e.version,appName:e.appName,projectName:e.projectName,frontend:tn,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&&re(e)?{mobilePurchases:e.mobilePurchases}:{},ai:e.ai,webAi:e.webAi,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 Mi(e,t){let n=(await ye(e.projectDir,e.projectDir,bl)).sort(),i=await Promise.all(n.map(async a=>[$e(gl(e.projectDir,a)),await Yt(a)])),o=Object.fromEntries(i),s=vl(e,t);await l($i(e.projectDir,he),`${JSON.stringify(s,null," ")}
1623
1623
  `),await l($i(e.projectDir,rn),`${JSON.stringify(o,null," ")}
1624
- `)}import{relative as wl}from"path";async function ot(e){let r=(await ye(e,e,nt)).sort(),n=await Promise.all(r.map(async i=>[$e(wl(e,i)),await Yt(i)]));return Object.fromEntries(n)}import{copyFile as kl,mkdir as El,rm as Sl}from"fs/promises";import{dirname as Pl,join as Mi,relative as Al}from"path";import{existsSync as Il}from"fs";async function Rr(e,t){Il(t)&&await Sl(t,{recursive:!0,force:!0});let n=(await ye(e,e,nt)).map(i=>({file:i,dest:Mi(t,$e(Al(e,i)))}));await Promise.all([...new Set(n.map(({dest:i})=>Pl(i)))].map(i=>El(i,{recursive:!0}))),await Promise.all(n.map(({file:i,dest:o})=>kl(i,o)))}async function Fi(e,t){await Rr(e,Mi(t,Rt))}import{readdir as Tl,readFile as Bi}from"fs/promises";import{existsSync as Rl}from"fs";import{dirname as _l,join as oe,resolve as _r,sep as Or}from"path";import{fileURLToPath as Ol}from"url";var xl={"claude-code":".claude/skills",cursor:".cursor/skills",codex:".agents/skills","gemini-cli":".gemini/skills",windsurf:".windsurf/skills"},Ve=Object.values(xl),xr=["generatesaas-update"],Dl=_l(Ol(import.meta.url));function Nl(){return Dt(Dl,["skill","content"],"content")}async function Dr(e,t){for(let r of Ve){let n=oe(e,r);for(let[i,o]of Object.entries(t)){let s=_r(n,i);if(!s.startsWith(n+Or))continue;let a=oe(s,"scripts"),d=oe(s,"references");await Ct(a),await Ct(d),await l(oe(s,"SKILL.md"),o.skillMd.replaceAll("__SKILL_ROOT__",r));for(let[g,k]of Object.entries(o.scripts)){let v=_r(a,g);v.startsWith(a+Or)&&await l(v,k)}let h=Object.entries(o.references);h.length===0&&await l(oe(d,".gitkeep"),"");for(let[g,k]of h){let v=_r(d,g);v.startsWith(d+Or)&&await l(v,k)}}}}async function Vi(e){if(!Rl(e))return{};let t={};for(let r of await Tl(e))r!==".gitkeep"&&(t[r]=await Bi(oe(e,r),"utf-8"));return t}async function Gi(e){let t=Nl(),r={};for(let n of xr){let i=oe(t,n);r[n]={skillMd:await Bi(oe(i,"SKILL.md"),"utf-8"),scripts:await Vi(oe(i,"scripts")),references:await Vi(oe(i,"references"))}}await Dr(e,r)}import{execFile as Cl,execFileSync as Ll}from"child_process";import{access as Ki,readFile as $l}from"fs/promises";import{join as Nr}from"path";import*as C from"@clack/prompts";function Ge(e){try{let t=process.platform==="win32"?"where":"which";return Ll(t,[e],{stdio:"ignore"}),!0}catch{return!1}}function Be(e,t,r,n=3e5){return new Promise((i,o)=>{Cl(e,t,{cwd:r,timeout:n,maxBuffer:50*1024*1024},(s,a,d)=>{if(s){let h=String(a||"").trim(),k=[String(d||"").trim(),h].filter(Boolean).join(`
1624
+ `)}import{relative as wl}from"path";async function ot(e){let r=(await ye(e,e,nt)).sort(),n=await Promise.all(r.map(async i=>[$e(wl(e,i)),await Yt(i)]));return Object.fromEntries(n)}import{copyFile as kl,mkdir as El,rm as Sl}from"fs/promises";import{dirname as Pl,join as Ui,relative as Al}from"path";import{existsSync as Il}from"fs";async function Rr(e,t){Il(t)&&await Sl(t,{recursive:!0,force:!0});let n=(await ye(e,e,nt)).map(i=>({file:i,dest:Ui(t,$e(Al(e,i)))}));await Promise.all([...new Set(n.map(({dest:i})=>Pl(i)))].map(i=>El(i,{recursive:!0}))),await Promise.all(n.map(({file:i,dest:o})=>kl(i,o)))}async function Fi(e,t){await Rr(e,Ui(t,Rt))}import{readdir as Tl,readFile as Bi}from"fs/promises";import{existsSync as Rl}from"fs";import{dirname as _l,join as oe,resolve as _r,sep as Or}from"path";import{fileURLToPath as Ol}from"url";var xl={"claude-code":".claude/skills",cursor:".cursor/skills",codex:".agents/skills","gemini-cli":".gemini/skills",windsurf:".windsurf/skills"},Ve=Object.values(xl),xr=["generatesaas-update"],Dl=_l(Ol(import.meta.url));function Nl(){return Dt(Dl,["skill","content"],"content")}async function Dr(e,t){for(let r of Ve){let n=oe(e,r);for(let[i,o]of Object.entries(t)){let s=_r(n,i);if(!s.startsWith(n+Or))continue;let a=oe(s,"scripts"),d=oe(s,"references");await Ct(a),await Ct(d),await l(oe(s,"SKILL.md"),o.skillMd.replaceAll("__SKILL_ROOT__",r));for(let[g,k]of Object.entries(o.scripts)){let v=_r(a,g);v.startsWith(a+Or)&&await l(v,k)}let h=Object.entries(o.references);h.length===0&&await l(oe(d,".gitkeep"),"");for(let[g,k]of h){let v=_r(d,g);v.startsWith(d+Or)&&await l(v,k)}}}}async function Vi(e){if(!Rl(e))return{};let t={};for(let r of await Tl(e))r!==".gitkeep"&&(t[r]=await Bi(oe(e,r),"utf-8"));return t}async function Ki(e){let t=Nl(),r={};for(let n of xr){let i=oe(t,n);r[n]={skillMd:await Bi(oe(i,"SKILL.md"),"utf-8"),scripts:await Vi(oe(i,"scripts")),references:await Vi(oe(i,"references"))}}await Dr(e,r)}import{execFile as Cl,execFileSync as Ll}from"child_process";import{access as Gi,readFile as $l}from"fs/promises";import{join as Nr}from"path";import*as C from"@clack/prompts";function Ke(e){try{let t=process.platform==="win32"?"where":"which";return Ll(t,[e],{stdio:"ignore"}),!0}catch{return!1}}function Be(e,t,r,n=3e5){return new Promise((i,o)=>{Cl(e,t,{cwd:r,timeout:n,maxBuffer:50*1024*1024},(s,a,d)=>{if(s){let h=String(a||"").trim(),k=[String(d||"").trim(),h].filter(Boolean).join(`
1625
1625
  `);o(new Error(k?`${s.message}
1626
- ${k}`:s.message))}else i()})})}async function Hi(e){if(!Ge("pnpm"))return C.log.warn("pnpm not found. Skipping lockfile regeneration."),!1;try{return await Be("pnpm",["install","--lockfile-only","--no-frozen-lockfile"],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 Yi(e){if(!Ge("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 Be("pnpm",["install"],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 Wi(e){if(!Ge("pnpm"))return!1;let t=C.spinner();t.start("Generating baseline database migration...");try{return await Be("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 zi(e){try{return await Ki(Nr(e,".git")),C.log.info("Git repository already exists, skipping init."),!0}catch{}if(!Ge("git"))return C.log.warn("git not found. Skipping repository initialization."),!1;let t=C.spinner();t.start("Initializing git repository...");try{return await Be("git",["init"],e),await Be("git",["add","-A"],e),await Be("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 qi(e){if(!Ge("pnpm"))return!1;try{await Ki(Nr(e,".git"))}catch{return!1}try{let t=JSON.parse(await $l(Nr(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 Be("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 Ke from"@clack/prompts";import J from"picocolors";function Ji(e,t){let r=Xe(e);t.dockerAvailable||Ke.log.warn("Docker not found. Install Docker to run local services: https://docs.docker.com/get-docker/"),t.baselineMigrationMissing&&Ke.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=>Gr[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"))),Ke.note(n.join(`
1627
- `),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")}`),Ke.note(i.join(`
1628
- `),J.yellow("Dev Tools"));let o=[],s=jl(e);s.length>0&&o.push(`Set in production: ${J.dim(s.join(", "))}`),o.push("pnpm db:setup # Apply the database schema"),o.push(Ul(e)),Ke.note(o.join(`
1629
- `),J.yellow("Deployment"))}function jl(e){let t=["DATABASE_URL","BETTER_AUTH_SECRET","INNGEST_APP_ID","INNGEST_EVENT_KEY","INNGEST_SIGNING_KEY"];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 Ul(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 Ie(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 Wt(e,t){return e.versions.find(r=>r.version===t)?.cli}function Xi(e){return process.env.GENERATESAAS_TEMPLATE_TARBALL||process.env.GENERATESAAS_TEMPLATE_TARBALL_DIR||process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?!0:at(e)===null}function zt(e,t,r="3.17.1"){if(Xi(r))return{ok:!0};let n=Wt(e,e.latest);return!n||at(n)===null?{ok:!0}:Ie(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 Zi(e,t,r="3.17.1"){if(Xi(r))return{ok:!0};let n=Wt(e,t);return!n||at(n)===null?{ok:!0}:Ie(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 Kl from"picocolors";var Hl="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";async function Yl(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 Qi(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 X("--architecture <type>","fullstack or separate").choices([...Ze])).addOption(new X("--payment <provider>","payment provider").choices([...Qe])).addOption(new X("--email <provider>","email provider").choices([...et])).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 X("--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([...Ne])).option("--ai","include the AI features (chat, models, automations, integrations)").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").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 X("--currency <code>","default currency for billing").choices([...pe])).addOption(new X("--preset <preset>","infrastructure preset: serverless (Vercel + Neon + Upstash) or selfhost (Docker or Node + Postgres + Redis); --deploy/--database/--cache override it piecewise").choices([...De])).addOption(new X("--deploy <target>","deployment target").choices([...de])).addOption(new X("--database <provider>","database provider").choices([...ue])).addOption(new X("--cache <provider>","cache provider").choices([...me])).option("--observability <providers>",`comma-separated observability providers (${F.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 X("--demo","first-party demo build: keep sample content, mark site non-indexable - requires CI API key").hideHelp()).addOption(new X("--no-db-migration","skip generating the baseline DB migration (internal: demos/CI/playground)").hideHelp()).action(async(t,r)=>{await Wl(t?{...r,apiKey:t}:r)})}async function Wl(e){let t=performance.now();Vr("3.17.1");let r;try{r=Zr(e)}catch(E){S.cancel(N(E)),process.exit(1)}let n=S.spinner(),i;try{i=await rt({apiKey:e.apiKey,prompt:!e.yes})}catch(E){S.cancel(N(E)),process.exit(1)}e.demo&&Tr(i)!==Hl&&(S.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let o=ge(i),s,a=async()=>{s=await Ae(o);let E=zt(s,"init");if(!E.ok)throw new Error(E.message);return{latestVersion:s.latest,selectedVersion:s.latest,desktopAllowed:s.entitlements?.desktopAllowed??!0,mobileAllowed:s.entitlements?.mobileAllowed??!0}};n.start("Verifying access...");let d,h,g=!0,k=!0;try{({latestVersion:d,selectedVersion:h,desktopAllowed:g,mobileAllowed:k}=await a()),n.stop("Access verified."),Le(i)}catch(E){if(n.stop("Access verification failed."),E instanceof $&&E.status===401){e.yes&&(S.cancel("Invalid API key. Cannot prompt in non-interactive mode."),process.exit(1)),S.log.warning("Invalid API key."),i=await ht(),o=ge(i),n.start("Verifying access...");try{({latestVersion:d,selectedVersion:h,desktopAllowed:g,mobileAllowed:k}=await a()),n.stop("Access verified."),Le(i)}catch(_){n.stop("Access verification failed."),S.cancel(_ instanceof $&&_.status===401?"Invalid API key.":N(_)),process.exit(1)}}else S.cancel(N(E)),process.exit(1)}S.log.success(`Latest version: ${d}`),r.desktop===!0&&!g&&(S.cancel("The desktop app is available on the Pro plan and up. Upgrade your plan at generatesaas.com."),process.exit(1)),r.mobile===!0&&!k&&(S.cancel("The mobile app is available on the Pro plan and up. Upgrade your plan at generatesaas.com."),process.exit(1));let v;e.yes?v=Qr({...r,...g?{}:{desktop:!1},...k?{}:{mobile:!1}}):v=await en(r,{desktopAllowed:g,mobileAllowed:k});let P;n.start("Activating license...");try{let E=e.demo&&v.baseUrl?await Yl(v.baseUrl):crypto.randomUUID(),_=()=>({frontend:sr,version:h,installId:E,projectName:v.projectName,options:cn(v)}),Q;try{Q=await or(o,_())}catch(K){let j=_t(K);if(!j?.lastAllowedVersion||!s)throw K;n.stop("License activation failed.");let ee=Zi(s,j.lastAllowedVersion);if(ee.ok||(S.cancel(`${j.message} ${ee.message}`),process.exit(1)),e.yes){let we=Wt(s,j.lastAllowedVersion);S.cancel(`${j.message} Re-run ${we?`npx generatesaas@${we} init`:"init"} without --yes to continue with v${j.lastAllowedVersion} (the last version your license covers).`),process.exit(1)}let H=await S.confirm({message:`Your update window has ended. Continue with v${j.lastAllowedVersion} (the last version your license covers)?`});(S.isCancel(H)||!H)&&(S.cancel("Setup cancelled."),process.exit(0)),h=j.lastAllowedVersion,n.start(`Activating license for v${h}...`),Q=await or(o,_())}P={token:Q.token,keyHash:Tr(i),installId:Q.installId??E},n.stop("License activated.")}catch(E){n.stop("License activation failed."),S.cancel(N(E)),process.exit(1)}let m=Gl(v.projectDir);if(Ml(m)&&Fl(m).length>0)if(e.yes)S.log.info(`Directory ${m} is not empty. Merging (keeping existing files, overwriting conflicts).`);else{let _=await S.select({message:`Directory ${m} 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"}]});(S.isCancel(_)||_==="cancel")&&(S.cancel("Setup cancelled."),process.exit(0)),_==="overwrite"&&Vl(m,{recursive:!0,force:!0})}let D={...v,projectDir:m,version:h,...e.demo?{docs:!1}:{}};n.start("Downloading template...");try{await Ot(o,h,m),n.stop("Template downloaded.")}catch(E){n.stop("Download failed."),S.cancel(N(E)),process.exit(1)}n.start("Generating project files...");try{if(await Ht(D),!e.demo){let E=await ot(m);await l(Bl(m,Tt),`${JSON.stringify(E,null," ")}
1630
- `),await Fi(m,m)}await Gi(m),await Ui(D,P),n.stop("Project files generated.")}catch(E){n.stop("Generation failed."),S.cancel(N(E)),process.exit(1)}!await Hi(m)&&D.demo===!0&&(S.cancel("Lockfile regeneration failed on a demo build - refusing to ship a stale lockfile."),process.exit(1));let G=await Yi(m),ae=D.demo!==!0&&e.dbMigration!==!1,Te=!1;ae&&G&&(await Wi(m),Te=!0),await zi(m),G&&await qi(m);let Re=Ge("docker"),ce=rr(D).map(E=>E.key).filter(E=>!D.credentials?.[E]);Ji(D,{pnpmInstalled:G,dockerAvailable:Re,skippedCredentials:ce,baselineMigrationMissing:ae&&!Te}),Br(),S.log.info(Kl.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as qt}from"fs";import{readFile as ss}from"fs/promises";import{join as He,resolve as Zl}from"path";import*as b from"@clack/prompts";import L from"picocolors";import{mkdtemp as zl,rm as ql}from"fs/promises";import{tmpdir as Jl}from"os";import{join as Xl}from"path";async function Cr(e,t,r,n){let i=await zl(Xl(Jl(),"generatesaas-stage-"));try{await Ot(e,t,i),await Ht({...r,projectDir:i}),await Rr(i,n)}finally{await ql(i,{recursive:!0,force:!0})}}function es(e){return e?"ON":"OFF"}var ct="packages/config/src/index.ts";function ts(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 rs(e){return[...Xe({databaseProvider:e.databaseProvider??"postgres",cacheProvider:e.cacheProvider??"redis",emailProvider:e.emailProvider??"smtp"})].sort()}var Lr=[{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 was renamed to `runner`, and that lane has since been removed. Your recorded choice was migrated across automatically so the removal is reported under one name."},{key:"runner",severity:"warn",preserved:e=>e.runner!==!0,describe:()=>"Retired option `runner`: your project recorded the runner ON, and the paired-device lane is GONE - not moved to config, removed. This update deletes the pairing endpoint, the dispatch transport, the relay, the `/runner/*` routes and the Runners page from your project, so machines your users had paired are no longer reachable from it. Chat and automations keep running on your app's own AI backend (`config.ai`); nothing else about them changes."},{key:"companionSource",severity:"warn",preserved:()=>!1,describe:e=>`Retired option \`companionSource\`: your project recorded a \`${String(e.companionSource)}\` daemon source. The paired-device lane that daemon served has been removed from your project, so there is no daemon left to source.`},{key:"companionName",severity:"warn",preserved:()=>!1,describe:e=>`Retired option \`companionName\`: your project recorded a branded daemon called ${e.companionName}. The paired-device lane is gone, so your project no longer ships an install flow to brand. An existing install on a user's machine keeps running; it has nothing left to pair with here.`},{key:"companionRepoUrl",severity:"warn",preserved:()=>!1,describe:e=>`Retired option \`companionRepoUrl\`: your project pointed its install flow at ${e.companionRepoUrl}. The paired-device lane is gone, so there is no install flow left to point anywhere.`},{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 paired-device lane behind it - nothing in your project is built from a local daemon 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===re(e),describe:e=>`Retired option \`aiBuiltin\`: your project recorded built-in AI models ${es(e.aiBuiltin)}, but it is now a plain config value generated ${es(re(e))} for a project ${re(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=>ts(e).join(",")===rs(e).join(","),describe:e=>`Retired option \`dockerServices\`: your project recorded ${ts(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 ${rs(e).join(", ")}. Add any extra service back by hand; the file is yours to edit.`},{key:"aiTools",severity:"warn",preserved:e=>(e.aiTools??[]).length===Hr.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 (${Ve.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.`}],hf=Lr.map(e=>e.key);function ns(e){for(let t of Lr)t.normalize?.(e)}function is(e){let t=[];for(let r of Lr){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 os(e){e.command("update").description("Update AI skill files and stage template updates").allowExcessArguments(!0).option("--cwd <path>","project directory (default: current directory)").option("--answer <id=choice>","answer a question that would otherwise prompt (repeatable), e.g. --answer desktop-migration=keep-ai",Ql,[]).action(async(t,r)=>{r.args.length>0&&b.log.warn("`auto` is now the default and the argument is ignored.");let n=ep(t.answer),i=Zl(t.cwd??process.cwd()),o=He(i,he),s;try{s=JSON.parse(await ss(o,"utf-8"))}catch{b.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}s.frontend===nr&&(b.cancel(`This project was generated on the Nuxt frontend (\`.generatesaas/manifest.json\` records \`frontend: "${nr}"\`).
1626
+ ${k}`:s.message))}else i()})})}async function Hi(e){if(!Ke("pnpm"))return C.log.warn("pnpm not found. Skipping lockfile regeneration."),!1;try{return await Be("pnpm",["install","--lockfile-only","--no-frozen-lockfile"],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 Yi(e){if(!Ke("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 Be("pnpm",["install"],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 Wi(e){if(!Ke("pnpm"))return!1;let t=C.spinner();t.start("Generating baseline database migration...");try{return await Be("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 zi(e){try{return await Gi(Nr(e,".git")),C.log.info("Git repository already exists, skipping init."),!0}catch{}if(!Ke("git"))return C.log.warn("git not found. Skipping repository initialization."),!1;let t=C.spinner();t.start("Initializing git repository...");try{return await Be("git",["init"],e),await Be("git",["add","-A"],e),await Be("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 qi(e){if(!Ke("pnpm"))return!1;try{await Gi(Nr(e,".git"))}catch{return!1}try{let t=JSON.parse(await $l(Nr(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 Be("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 Ge from"@clack/prompts";import J from"picocolors";function Ji(e,t){let r=Xe(e);t.dockerAvailable||Ge.log.warn("Docker not found. Install Docker to run local services: https://docs.docker.com/get-docker/"),t.baselineMigrationMissing&&Ge.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=>Kr[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"))),Ge.note(n.join(`
1627
+ `),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")}`),Ge.note(i.join(`
1628
+ `),J.yellow("Dev Tools"));let o=[],s=jl(e);s.length>0&&o.push(`Set in production: ${J.dim(s.join(", "))}`),o.push("pnpm db:setup # Apply the database schema"),o.push(Ml(e)),Ge.note(o.join(`
1629
+ `),J.yellow("Deployment"))}function jl(e){let t=["DATABASE_URL","BETTER_AUTH_SECRET","INNGEST_APP_ID","INNGEST_EVENT_KEY","INNGEST_SIGNING_KEY"];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 Ml(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 Ie(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 Wt(e,t){return e.versions.find(r=>r.version===t)?.cli}function Xi(e){return process.env.GENERATESAAS_TEMPLATE_TARBALL||process.env.GENERATESAAS_TEMPLATE_TARBALL_DIR||process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?!0:at(e)===null}function zt(e,t,r="3.18.1"){if(Xi(r))return{ok:!0};let n=Wt(e,e.latest);return!n||at(n)===null?{ok:!0}:Ie(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 Zi(e,t,r="3.18.1"){if(Xi(r))return{ok:!0};let n=Wt(e,t);return!n||at(n)===null?{ok:!0}:Ie(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 Gl from"picocolors";var Hl="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";async function Yl(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 Qi(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 X("--architecture <type>","fullstack or separate").choices([...Ze])).addOption(new X("--payment <provider>","payment provider").choices([...Qe])).addOption(new X("--email <provider>","email provider").choices([...et])).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 X("--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([...Ne])).option("--ai","include the AI features (chat, models, automations, integrations)").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").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 X("--currency <code>","default currency for billing").choices([...pe])).addOption(new X("--preset <preset>","infrastructure preset: serverless (Vercel + Neon + Upstash) or selfhost (Docker or Node + Postgres + Redis); --deploy/--database/--cache override it piecewise").choices([...De])).addOption(new X("--deploy <target>","deployment target").choices([...de])).addOption(new X("--database <provider>","database provider").choices([...ue])).addOption(new X("--cache <provider>","cache provider").choices([...me])).option("--observability <providers>",`comma-separated observability providers (${F.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 X("--demo","first-party demo build: keep sample content, mark site non-indexable - requires CI API key").hideHelp()).addOption(new X("--no-db-migration","skip generating the baseline DB migration (internal: demos/CI/playground)").hideHelp()).action(async(t,r)=>{await Wl(t?{...r,apiKey:t}:r)})}async function Wl(e){let t=performance.now();Vr("3.18.1");let r;try{r=Zr(e)}catch(E){S.cancel(N(E)),process.exit(1)}let n=S.spinner(),i;try{i=await rt({apiKey:e.apiKey,prompt:!e.yes})}catch(E){S.cancel(N(E)),process.exit(1)}e.demo&&Tr(i)!==Hl&&(S.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let o=ge(i),s,a=async()=>{s=await Ae(o);let E=zt(s,"init");if(!E.ok)throw new Error(E.message);return{latestVersion:s.latest,selectedVersion:s.latest,desktopAllowed:s.entitlements?.desktopAllowed??!0,mobileAllowed:s.entitlements?.mobileAllowed??!0}};n.start("Verifying access...");let d,h,g=!0,k=!0;try{({latestVersion:d,selectedVersion:h,desktopAllowed:g,mobileAllowed:k}=await a()),n.stop("Access verified."),Le(i)}catch(E){if(n.stop("Access verification failed."),E instanceof $&&E.status===401){e.yes&&(S.cancel("Invalid API key. Cannot prompt in non-interactive mode."),process.exit(1)),S.log.warning("Invalid API key."),i=await ht(),o=ge(i),n.start("Verifying access...");try{({latestVersion:d,selectedVersion:h,desktopAllowed:g,mobileAllowed:k}=await a()),n.stop("Access verified."),Le(i)}catch(_){n.stop("Access verification failed."),S.cancel(_ instanceof $&&_.status===401?"Invalid API key.":N(_)),process.exit(1)}}else S.cancel(N(E)),process.exit(1)}S.log.success(`Latest version: ${d}`),r.desktop===!0&&!g&&(S.cancel("The desktop app is available on the Pro plan and up. Upgrade your plan at generatesaas.com."),process.exit(1)),r.mobile===!0&&!k&&(S.cancel("The mobile app is available on the Pro plan and up. Upgrade your plan at generatesaas.com."),process.exit(1));let v;e.yes?v=Qr({...r,...g?{}:{desktop:!1},...k?{}:{mobile:!1}}):v=await en(r,{desktopAllowed:g,mobileAllowed:k});let P;n.start("Activating license...");try{let E=e.demo&&v.baseUrl?await Yl(v.baseUrl):crypto.randomUUID(),_=()=>({frontend:sr,version:h,installId:E,projectName:v.projectName,options:cn(v)}),Q;try{Q=await or(o,_())}catch(G){let j=_t(G);if(!j?.lastAllowedVersion||!s)throw G;n.stop("License activation failed.");let ee=Zi(s,j.lastAllowedVersion);if(ee.ok||(S.cancel(`${j.message} ${ee.message}`),process.exit(1)),e.yes){let we=Wt(s,j.lastAllowedVersion);S.cancel(`${j.message} Re-run ${we?`npx generatesaas@${we} init`:"init"} without --yes to continue with v${j.lastAllowedVersion} (the last version your license covers).`),process.exit(1)}let H=await S.confirm({message:`Your update window has ended. Continue with v${j.lastAllowedVersion} (the last version your license covers)?`});(S.isCancel(H)||!H)&&(S.cancel("Setup cancelled."),process.exit(0)),h=j.lastAllowedVersion,n.start(`Activating license for v${h}...`),Q=await or(o,_())}P={token:Q.token,keyHash:Tr(i),installId:Q.installId??E},n.stop("License activated.")}catch(E){n.stop("License activation failed."),S.cancel(N(E)),process.exit(1)}let m=Kl(v.projectDir);if(Ul(m)&&Fl(m).length>0)if(e.yes)S.log.info(`Directory ${m} is not empty. Merging (keeping existing files, overwriting conflicts).`);else{let _=await S.select({message:`Directory ${m} 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"}]});(S.isCancel(_)||_==="cancel")&&(S.cancel("Setup cancelled."),process.exit(0)),_==="overwrite"&&Vl(m,{recursive:!0,force:!0})}let D={...v,projectDir:m,version:h,...e.demo?{docs:!1}:{}};n.start("Downloading template...");try{await Ot(o,h,m),n.stop("Template downloaded.")}catch(E){n.stop("Download failed."),S.cancel(N(E)),process.exit(1)}n.start("Generating project files...");try{if(await Ht(D),!e.demo){let E=await ot(m);await l(Bl(m,Tt),`${JSON.stringify(E,null," ")}
1630
+ `),await Fi(m,m)}await Ki(m),await Mi(D,P),n.stop("Project files generated.")}catch(E){n.stop("Generation failed."),S.cancel(N(E)),process.exit(1)}!await Hi(m)&&D.demo===!0&&(S.cancel("Lockfile regeneration failed on a demo build - refusing to ship a stale lockfile."),process.exit(1));let K=await Yi(m),ae=D.demo!==!0&&e.dbMigration!==!1,Te=!1;ae&&K&&(await Wi(m),Te=!0),await zi(m),K&&await qi(m);let Re=Ke("docker"),ce=rr(D).map(E=>E.key).filter(E=>!D.credentials?.[E]);Ji(D,{pnpmInstalled:K,dockerAvailable:Re,skippedCredentials:ce,baselineMigrationMissing:ae&&!Te}),Br(),S.log.info(Gl.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as qt}from"fs";import{readFile as ss}from"fs/promises";import{join as He,resolve as Zl}from"path";import*as b from"@clack/prompts";import L from"picocolors";import{mkdtemp as zl,rm as ql}from"fs/promises";import{tmpdir as Jl}from"os";import{join as Xl}from"path";async function Cr(e,t,r,n){let i=await zl(Xl(Jl(),"generatesaas-stage-"));try{await Ot(e,t,i),await Ht({...r,projectDir:i}),await Rr(i,n)}finally{await ql(i,{recursive:!0,force:!0})}}function es(e){return e?"ON":"OFF"}var ct="packages/config/src/index.ts";function ts(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 rs(e){return[...Xe({databaseProvider:e.databaseProvider??"postgres",cacheProvider:e.cacheProvider??"redis",emailProvider:e.emailProvider??"smtp"})].sort()}var Lr=[{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 was renamed to `runner`, and that lane has since been removed. Your recorded choice was migrated across automatically so the removal is reported under one name."},{key:"runner",severity:"warn",preserved:e=>e.runner!==!0,describe:()=>"Retired option `runner`: your project recorded the runner ON, and the paired-device lane is GONE - not moved to config, removed. This update deletes the pairing endpoint, the dispatch transport, the relay, the `/runner/*` routes and the Runners page from your project, so machines your users had paired are no longer reachable from it. Chat and automations keep running on your app's own AI backend (`config.ai`); nothing else about them changes."},{key:"companionSource",severity:"warn",preserved:()=>!1,describe:e=>`Retired option \`companionSource\`: your project recorded a \`${String(e.companionSource)}\` daemon source. The paired-device lane that daemon served has been removed from your project, so there is no daemon left to source.`},{key:"companionName",severity:"warn",preserved:()=>!1,describe:e=>`Retired option \`companionName\`: your project recorded a branded daemon called ${e.companionName}. The paired-device lane is gone, so your project no longer ships an install flow to brand. An existing install on a user's machine keeps running; it has nothing left to pair with here.`},{key:"companionRepoUrl",severity:"warn",preserved:()=>!1,describe:e=>`Retired option \`companionRepoUrl\`: your project pointed its install flow at ${e.companionRepoUrl}. The paired-device lane is gone, so there is no install flow left to point anywhere.`},{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 paired-device lane behind it - nothing in your project is built from a local daemon 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===re(e),describe:e=>`Retired option \`aiBuiltin\`: your project recorded built-in AI models ${es(e.aiBuiltin)}, but it is now a plain config value generated ${es(re(e))} for a project ${re(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=>ts(e).join(",")===rs(e).join(","),describe:e=>`Retired option \`dockerServices\`: your project recorded ${ts(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 ${rs(e).join(", ")}. Add any extra service back by hand; the file is yours to edit.`},{key:"aiTools",severity:"warn",preserved:e=>(e.aiTools??[]).length===Hr.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 (${Ve.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.`}],hf=Lr.map(e=>e.key);function ns(e){for(let t of Lr)t.normalize?.(e)}function is(e){let t=[];for(let r of Lr){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 os(e){e.command("update").description("Update AI skill files and stage template updates").allowExcessArguments(!0).option("--cwd <path>","project directory (default: current directory)").option("--answer <id=choice>","answer a question that would otherwise prompt (repeatable), e.g. --answer desktop-migration=keep-ai",Ql,[]).action(async(t,r)=>{r.args.length>0&&b.log.warn("`auto` is now the default and the argument is ignored.");let n=ep(t.answer),i=Zl(t.cwd??process.cwd()),o=He(i,he),s;try{s=JSON.parse(await ss(o,"utf-8"))}catch{b.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}s.frontend===nr&&(b.cancel(`This project was generated on the Nuxt frontend (\`.generatesaas/manifest.json\` records \`frontend: "${nr}"\`).
1631
1631
 
1632
1632
  v${ir} is the last release that ships a Nuxt frontend, and this CLI cannot update or migrate a Nuxt project to Next.js.
1633
1633
 
@@ -1651,16 +1651,16 @@ Correct it there, then run ${L.cyan("generatesaas update")} again.`),process.exi
1651
1651
  ${g.map(m=>` - ${m}`).join(`
1652
1652
  `)}
1653
1653
 
1654
- Correct it there, then run ${L.cyan("generatesaas update")} again.`),process.exit(1));let k;try{k=await rt()}catch(m){b.cancel(N(m)),process.exit(1)}let v=ge(k),P=b.spinner();try{P.start("Verifying access...");let m;try{m=await Ae(v)}catch(A){throw A instanceof $&&A.status===401?new Error("Your saved API key was rejected. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):A}let D=zt(m,"update");if(!D.ok)throw new Error(D.message);P.stop("Access verified."),Le(k),P.start("Fetching latest skill files...");let ve=await an(v,m.latest);await Dr(i,ve.skills);let G=Ve;if(P.stop("Skills updated."),b.log.success(`Skill files installed to ${L.cyan(G.length.toString())} locations.`),s.version===m.latest){b.log.info(`Already on the latest version (${s.version}).`);return}Ie(s.version,m.latest)>0&&(b.cancel(`This project is on ${s.version}, which is newer than the latest release (${m.latest}). Updating would stage the older template over it. Check that your API key points at the right account, or correct \`version\` in ${he}.`),process.exit(1));let ae=m.entitlements;ae&&(s.desktop&&!ae.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.mobile&&ae.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 Te=JSON.stringify(s);await tp(s,Xr(s),n),await rp(is(s),n);let Re=Yr(s),ze=zr(s),ce=Qt(ze);ce.length>0&&(b.cancel(`.generatesaas/manifest.json is missing a provider key, and its default would create a combination this project cannot run:
1654
+ Correct it there, then run ${L.cyan("generatesaas update")} again.`),process.exit(1));let k;try{k=await rt()}catch(m){b.cancel(N(m)),process.exit(1)}let v=ge(k),P=b.spinner();try{P.start("Verifying access...");let m;try{m=await Ae(v)}catch(A){throw A instanceof $&&A.status===401?new Error("Your saved API key was rejected. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):A}let D=zt(m,"update");if(!D.ok)throw new Error(D.message);P.stop("Access verified."),Le(k),P.start("Fetching latest skill files...");let ve=await an(v,m.latest);await Dr(i,ve.skills);let K=Ve;if(P.stop("Skills updated."),b.log.success(`Skill files installed to ${L.cyan(K.length.toString())} locations.`),s.version===m.latest){b.log.info(`Already on the latest version (${s.version}).`);return}Ie(s.version,m.latest)>0&&(b.cancel(`This project is on ${s.version}, which is newer than the latest release (${m.latest}). Updating would stage the older template over it. Check that your API key points at the right account, or correct \`version\` in ${he}.`),process.exit(1));let ae=m.entitlements;ae&&(s.desktop&&!ae.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.mobile&&ae.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 Te=JSON.stringify(s);await tp(s,Xr(s),n),await rp(is(s),n);let Re=Yr(s),ze=zr(s),ce=Qt(ze);ce.length>0&&(b.cancel(`.generatesaas/manifest.json is missing a provider key, and its default would create a combination this project cannot run:
1655
1655
 
1656
1656
  ${ce.map(A=>` - ${A}`).join(`
1657
1657
  `)}
1658
1658
 
1659
1659
  Add the key to .generatesaas/manifest.json with a value your deployment target supports, then run ${L.cyan("generatesaas update")} again.`),process.exit(1));let E=JSON.stringify(ze)!==Te;if(s=ze,s.licenseToken)try{let A=await ln(v,{currentToken:s.licenseToken,newVersion:m.latest});s.licenseToken=A.token,A.licenseKeyHash&&(s.licenseKeyHash=A.licenseKeyHash),await l(o,`${JSON.stringify(s,null," ")}
1660
1660
  `),E=!1,b.log.success("License refreshed.")}catch(A){let c=_t(A);c&&(b.cancel(c.message),process.exit(1)),b.log.warn("License refresh skipped.")}E&&await l(o,`${JSON.stringify(s,null," ")}
1661
- `);let _=ji(s,i),Q=He(i,nn);P.start(`Staging v${m.latest} (shaped for your config)...`),await Cr(v,m.latest,_,Q),P.stop("Template staged.");let{text:K,title:j}=await sp(v,m,s.version);K&&b.note(K,j);let ee=He(i,Tt),H=He(i,Rt),we=!qt(H),ke=!qt(ee);if(we){if(P.start("Building baseline template (one-time migration)..."),await Cr(v,s.version,_,H),ke){let A=await ot(H);await l(ee,`${JSON.stringify(A,null," ")}
1661
+ `);let _=ji(s,i),Q=He(i,nn);P.start(`Staging v${m.latest} (shaped for your config)...`),await Cr(v,m.latest,_,Q),P.stop("Template staged.");let{text:G,title:j}=await sp(v,m,s.version);G&&b.note(G,j);let ee=He(i,Tt),H=He(i,Rt),we=!qt(H),ke=!qt(ee);if(we){if(P.start("Building baseline template (one-time migration)..."),await Cr(v,s.version,_,H),ke){let A=await ot(H);await l(ee,`${JSON.stringify(A,null," ")}
1662
1662
  `)}if(P.stop("Baseline template stored."),!ke){let A=await op(ee,H);A>0&&b.log.warn(`Rebuilt baseline differs from the original for ${A} 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(ke){P.start("Computing baseline template hashes...");let A=await ot(H);await l(ee,`${JSON.stringify(A,null," ")}
1663
- `),P.stop("Baseline hashes computed.")}await l(He(i,sn),`${JSON.stringify({currentVersion:s.version,targetVersion:m.latest,changelog:K,stagedAt:new Date().toISOString(),...Re.length>0?{newOptions:Re}:{}},null," ")}
1663
+ `),P.stop("Baseline hashes computed.")}await l(He(i,sn),`${JSON.stringify({currentVersion:s.version,targetVersion:m.latest,changelog:G,stagedAt:new Date().toISOString(),...Re.length>0?{newOptions:Re}:{}},null," ")}
1664
1664
  `),b.log.info(`Update staged: ${L.cyan(s.version)} \u2192 ${L.cyan(m.latest)}`),b.log.info(`Ask your AI coding assistant to ${L.cyan("'update my GenerateSaaS project'")}.`),s.desktop===!0&&qt(He(i,"apps/tauri"))&&b.log.warn(`Desktop: the app moved to an Electron shell (${L.cyan("apps/desktop")}). Your project still has the old Tauri shell (${L.cyan("apps/tauri")}) - remove it after verifying the Electron app builds and runs. See ${L.cyan("/desktop/updates")}.`),s.desktop===!0&&qt(He(i,"apps/electron"))&&b.log.warn(`Desktop: the app now lives at ${L.cyan("apps/desktop")}. Your project still has the old ${L.cyan("apps/electron")} directory - move your local changes across, remove it, then run ${L.cyan("pnpm install")}. See ${L.cyan("/desktop/updates")}.`)}catch(m){P.stop("Failed."),b.cancel(`Update failed: ${N(m)}`),process.exit(1)}})}var as="desktop-migration";function Ql(e,t){return t.push(e),t}function ep(e){let t=new Set([as,...Object.keys(ls).map(n=>`retired:${n}`)]),r=new Map;for(let n of e){let i=n.indexOf("=");(i<=0||i===n.length-1)&&(b.cancel(`\`--answer ${n}\` is malformed. Use \`--answer <id>=<choice>\`.`),process.exit(1));let o=n.slice(0,i);t.has(o)||(b.cancel(`\`--answer ${n}\` names a question this update does not ask. Valid ids: ${[...t].join(", ")}.`),process.exit(1)),r.set(o,n.slice(i+1))}return r}async function cs(e,t){let r=t.get(e.id);if(r!==void 0){let n=e.choices.find(i=>i.id===r);return n||(b.cancel(`\`--answer ${e.id}=${r}\` is not a valid choice. Valid choices: ${e.choices.map(i=>i.id).join(", ")}.`),process.exit(1)),n}if(process.stdin.isTTY){let n=await b.select({message:e.message,options:e.choices.map(o=>({value:o.id,label:o.label,...o.hint?{hint:o.hint}:{}}))});b.isCancel(n)&&(b.cancel("Update cancelled. Nothing was changed or staged."),process.exit(1));let i=e.choices.find(o=>o.id===n);return i||(b.cancel("Update cancelled. Nothing was changed or staged."),process.exit(1)),i}b.cancel(`${e.message}
1665
1665
 
1666
1666
  This update is running without an interactive terminal, so it cannot ask you directly. Answer the question \`${e.id}\` and re-run, for example:
@@ -1676,14 +1676,14 @@ ${k}`}).join(`
1676
1676
  `),title:`Changelog v${r} \u2192 v${n}`}}async function op(e,t){let r=JSON.parse(await ss(e,"utf-8")),n=await ot(t),i=0;for(let[o,s]of Object.entries(r))nt(o)||n[o]!==s&&i++;for(let o of Object.keys(n))o in r||i++;return i}import*as B from"@clack/prompts";import be from"picocolors";import{readFile as ap}from"fs/promises";import{join as cp,resolve as lp}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=lp(t.cwd??process.cwd()),n=cp(r,he),i;try{i=JSON.parse(await ap(n,"utf-8"))}catch{B.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let o=[`Version: ${be.cyan(i.version)}`,i.deploymentTarget?`Deploy target: ${be.cyan(i.deploymentTarget)}`:null,i.databaseProvider?`Database: ${be.cyan(i.databaseProvider)}`:null,i.cacheProvider?`Cache: ${be.cyan(i.cacheProvider)}`:null].filter(Boolean).join(`
1677
1677
  `);B.note(o,be.bold("Project Status"));let s=B.spinner();s.start("Checking for updates...");try{let a=await rt(),d=ge(a),g=(await Ae(d)).latest;i.version===g?(s.stop("Up to date."),B.log.success(`Already on the latest version (${be.green(g)})`)):(s.stop("Update available."),B.log.warning(`Update available: ${be.yellow(i.version)} \u2192 ${be.green(g)}`),B.log.info(`Open this project in your AI coding agent and ask it to ${be.cyan("update my GenerateSaaS project")} - it fetches and applies the update for you.`))}catch(a){s.stop("Check failed."),a instanceof $&&a.status===401?B.log.warning("Invalid API key. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):B.log.warning(`Could not check for updates: ${N(a)}`)}})}import{Buffer as pp}from"buffer";import{readFile as dp}from"fs/promises";import*as x from"@clack/prompts";import O from"picocolors";function up(){return process.env.GENERATESAAS_API_KEY??mt()}function mp(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 hp(e){return{verdict:e.verdict??"unknown",ejectedAt:e.ejectedAt??null}}function fp(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 ds(e,t){let r=process.env.GENERATESAAS_API_URL??ut,n=up();e.start("Cross-referencing license records...");try{let i=n?mp(await pn(r,n,{lkh:t.lkh,nid:t.nid,domain:t.domain})):hp(await ar(r,{token:t.token,domain:t.domain}));return e.stop(`${O.green("Checked")} - records cross-referenced`),fp(i)}catch(i){return e.stop(`${O.yellow("Skipped")} - ${N(i)}`),null}}function gp(e){let t=e.split(".");if(t.length!==3||!t[1])throw new Error("Invalid JWT format");let r=pp.from(t[1],"base64url").toString("utf-8");return JSON.parse(r)}function $r(e){return typeof e!="number"?"unknown":new Date(e*1e3).toISOString().split("T")[0]}function us(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: ${$r(e.pat)}`,`Last updated: ${$r(e.uat)}`,`Expires: ${$r(e.exp)}`,`Install ID: ${String(e.nid??"unknown")}`].join(`
1678
1678
  `),O.yellow("License Details"))}function yp(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 ms(e){let t=x.spinner(),r=null,n="no candidates";for(let s of yp(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 d=(await a.text()).trim();if(!d||d.split(".").length!==3){n=`${s} did not return a JWT`,t.stop(`${O.yellow("Not here")} - ${n}`);continue}r=d,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=hs(e);return s?await ds(t,{domain:s})??!1:!1}let i;try{i=gp(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 ar(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:"),us(i),!1}return us(i),await ds(t,{token:r,lkh:typeof i.lkh=="string"?i.lkh:void 0,nid:typeof i.nid=="string"?i.nid:void 0,domain:hs(e)})??!0}function hs(e){try{return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`).hostname}catch{return}}function fs(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 dp(r.file,"utf-8")).split(`
1679
- `).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 ms(s)&&o++,x.log.info("");x.log.success(`${o}/${i.length} sites verified.`)}else await ms(t)||process.exit(1)})}import{existsSync as bp,rmSync as vp}from"fs";import*as Z from"@clack/prompts";function gs(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){bp(fe)?(vp(fe),Z.log.success("API key removed.")):Z.log.info("No API key configured.");return}let r=mt();r?Z.log.info(`Current API key: ****${r.slice(-4)}`):Z.log.info("No API key configured.");let n=await ht(),i=ge(n),o=Z.spinner();o.start("Verifying API key...");try{await Ae(i),o.stop("API key verified."),Le(n),Z.log.success("API key saved.")}catch(s){o.stop("Verification failed."),s instanceof $&&s.status===401?Z.cancel("Invalid API key."):Z.cancel(N(s)),process.exit(1)}})}import{existsSync as Jt,readFileSync as Ur,rmSync as wp,writeFileSync as ys}from"fs";import{join as Ye}from"path";import*as M from"@clack/prompts";var kp=["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","packages/api/tests/functions/license-heartbeat.test.ts","packages/api/tests/lib/cron-spread.test.ts"],Ep=[{file:"packages/api/src/routes/inngest.ts",removals:[`import { licenseHeartbeatFunction } from "../functions/maintenance/license-heartbeat";
1679
+ `).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 ms(s)&&o++,x.log.info("");x.log.success(`${o}/${i.length} sites verified.`)}else await ms(t)||process.exit(1)})}import{existsSync as bp,rmSync as vp}from"fs";import*as Z from"@clack/prompts";function gs(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){bp(fe)?(vp(fe),Z.log.success("API key removed.")):Z.log.info("No API key configured.");return}let r=mt();r?Z.log.info(`Current API key: ****${r.slice(-4)}`):Z.log.info("No API key configured.");let n=await ht(),i=ge(n),o=Z.spinner();o.start("Verifying API key...");try{await Ae(i),o.stop("API key verified."),Le(n),Z.log.success("API key saved.")}catch(s){o.stop("Verification failed."),s instanceof $&&s.status===401?Z.cancel("Invalid API key."):Z.cancel(N(s)),process.exit(1)}})}import{existsSync as Jt,readFileSync as Mr,rmSync as wp,writeFileSync as ys}from"fs";import{join as Ye}from"path";import*as U from"@clack/prompts";var kp=["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","packages/api/tests/functions/license-heartbeat.test.ts","packages/api/tests/lib/cron-spread.test.ts"],Ep=[{file:"packages/api/src/routes/inngest.ts",removals:[`import { licenseHeartbeatFunction } from "../functions/maintenance/license-heartbeat";
1680
1680
  `,` licenseHeartbeatFunction,
1681
1681
  `]},{file:"packages/api/src/routes/internal/index.ts",removals:[`import licenseRoutes from "./license";
1682
1682
  `,` .route("/license", licenseRoutes)
1683
1683
  `]},{file:"packages/api/src/routes/public/v1/health.ts",removals:[`import { manifest } from "../../../lib/manifest";
1684
- `],replacements:[{from:'c.json({ status: "success" as const, version: manifest?.version })',to:'c.json({ status: "success" as const })'}]}];function Sp(){return Ve.flatMap(e=>xr.map(t=>Ye(e,t)))}function jr(e){return Jt(e)?(wp(e,{recursive:!0}),!0):!1}function Pp(e,t,r=[]){if(!Jt(e))return!1;let n=Ur(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:(ys(e,i,"utf-8"),!0)}function Ap(e){let t=Ye(e,".gitignore");if(!Jt(t))return!1;let r=Ur(t,"utf-8"),n=r.split(`
1684
+ `],replacements:[{from:'c.json({ status: "success" as const, version: manifest?.version })',to:'c.json({ status: "success" as const })'}]}];function Sp(){return Ve.flatMap(e=>xr.map(t=>Ye(e,t)))}function jr(e){return Jt(e)?(wp(e,{recursive:!0}),!0):!1}function Pp(e,t,r=[]){if(!Jt(e))return!1;let n=Mr(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:(ys(e,i,"utf-8"),!0)}function Ap(e){let t=Ye(e,".gitignore");if(!Jt(t))return!1;let r=Mr(t,"utf-8"),n=r.split(`
1685
1685
  `).filter(i=>!i.includes(".generatesaas")).join(`
1686
- `);return n===r?!1:(ys(t,n,"utf-8"),!0)}function bs(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=Ye(t,he),n;try{n=JSON.parse(Ur(r,"utf-8"))}catch{M.cancel("No GenerateSaaS project found in this directory."),process.exit(1)}let i=await M.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(M.isCancel(i)&&(M.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)}),M.log.info("Recorded the opt-out with generatesaas.com (final event - nothing is sent after this).")}catch{M.log.warn("Could not reach generatesaas.com to record the opt-out. Ejecting anyway.")}let o=[],s=[];for(let a of Sp())jr(Ye(t,a))&&o.push(a);for(let a of kp)jr(Ye(t,a))&&o.push(a);jr(Ye(t,Y))&&o.push(`${Y}/`);for(let a of Ep){let d=Ye(t,a.file);Pp(d,a.removals,a.replacements)?s.push(a.file):Jt(d)&&M.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}Ap(t)&&s.push(".gitignore");for(let a of o)M.log.info(`Deleted ${a}`);for(let a of s)M.log.info(`Modified ${a}`);M.log.success("Ejected successfully. This project is now fully standalone.")})}var We=new Ip().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("3.17.1").addHelpText("after",`
1686
+ `);return n===r?!1:(ys(t,n,"utf-8"),!0)}function bs(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=Ye(t,he),n;try{n=JSON.parse(Mr(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 Sp())jr(Ye(t,a))&&o.push(a);for(let a of kp)jr(Ye(t,a))&&o.push(a);jr(Ye(t,Y))&&o.push(`${Y}/`);for(let a of Ep){let d=Ye(t,a.file);Pp(d,a.removals,a.replacements)?s.push(a.file):Jt(d)&&U.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}Ap(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 We=new Ip().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("3.18.1").addHelpText("after",`
1687
1687
  Examples:
1688
1688
  $ generatesaas init Interactive setup
1689
1689
  $ generatesaas init -n my-app -y Quick setup with defaults
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "generatesaas",
3
- "version": "3.17.1",
3
+ "version": "3.18.1",
4
4
  "type": "module",
5
5
  "description": "CLI for scaffolding and managing GenerateSaaS projects",
6
6
  "license": "UNLICENSED",