generatesaas 3.11.0 → 3.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import{Command as Xp}from"commander";import*as Us from"@clack/prompts";import{existsSync as pp,readdirSync as dp,rmSync as up}from"fs";import{join as mp,resolve as hp}from"path";import{Option as ne}from"commander";import*as S from"@clack/prompts";import*as Nt from"@clack/prompts";import ur from"picocolors";function sn(e){let t=e?` GenerateSaaS v${e} `:" GenerateSaaS ";Nt.intro(ur.bgYellow(ur.black(t)))}function on(){Nt.outro(ur.yellow("Happy building!"))}import*as h from"@clack/prompts";import g from"picocolors";var gt={fullstack:{label:"Fullstack",hint:"Frontend hosts the API - works on serverless or long-running"},separate:{label:"Separate",hint:"Standalone Hono backend - long-running runtimes only (Docker, Render, Fly.io, Railway, Coolify, Dokploy)"}},Ct={stripe:{label:"Stripe"},polar:{label:"Polar"},none:{label:"None",hint:"disable payments"}},Lt={smtp:{label:"SMTP",hint:"Mailpit for local dev"},ses:{label:"Amazon SES"},resend:{label:"Resend"}},$t={sentry:{label:"Sentry",hint:"web, server and desktop - release health, tracing, alerting"},posthog:{label:"PostHog",hint:"web and server, plus LLM analytics - no Electron SDK, so a desktop build reports nothing"}},an={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 Le={USD:{symbol:"$",name:"US Dollar",place:"left",space:!1},EUR:{symbol:"\u20AC",name:"Euro",place:"right",space:!1},GBP:{symbol:"\xA3",name:"British Pound",place:"left",space:!1},CAD:{symbol:"CA$",name:"Canadian Dollar",place:"left",space:!1},AUD:{symbol:"A$",name:"Australian Dollar",place:"left",space:!1},BRL:{symbol:"R$",name:"Brazilian Real",place:"left",space:!1},JPY:{symbol:"\xA5",name:"Japanese Yen",place:"left",space:!1}};var Z={node:{label:"Node.js / Docker",hint:"long-running runtime - Render, Fly.io, Railway, Coolify, Dokploy, VPS",edgeRuntime:!1},vercel:{label:"Vercel",hint:"serverless functions",edgeRuntime:!0}},$e={postgres:{label:"PostgreSQL (self-hosted)",hint:"local Docker, drizzle-orm/node-postgres",managed:!1,envVars:[{key:"DATABASE_URL",defaultValue:"postgres://postgres:postgres@localhost:5432/saas"}]},neon:{label:"Neon",hint:"serverless Postgres",managed:!0,envVars:[{key:"DATABASE_URL",comment:"# TODO: Add your Neon connection string"}]},supabase:{label:"Supabase",hint:"managed Postgres",managed:!0,envVars:[{key:"DATABASE_URL",comment:"# TODO: Add your Supabase connection string"}]}},je={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."}],et=[{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)."}],Fs=[{feature:"runner",target:"vercel",reason:"Serverless (Vercel) cannot hold a connection open, and the runner holds one open per paired device. Deploy to a long-running runtime instead (Node.js / Docker on Render, Fly.io, Railway, Coolify, Dokploy, or your own VPS)."}];function me(e){return Fs.find(t=>e[t.feature]===!0&&e.deploymentTarget===t.target)}var he={serverless:{label:"Serverless",hint:"Vercel + Neon + Upstash",values:{deploymentTarget:"vercel",databaseProvider:"neon",cacheProvider:"upstash"}},selfhost:{label:"Self-host",hint:"Docker or Node + Postgres + Redis",values:{deploymentTarget:"node",databaseProvider:"postgres",cacheProvider:"redis"}}},Ue=Object.keys(he);function mr(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 et)e.deploymentTarget===r.target&&e.architecture===r.architecture&&t.push(`\`architecture: "${r.architecture}"\` with \`deploymentTarget: "${r.target}"\` - ${r.reason}`);return t}var cn=["Local file storage (sharp, geoip-lite)","SMTP email (use Resend or SES instead)","Content API git integration"];function tt(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 rt=["fullstack","separate"],nt=["stripe","polar","none"],it=["smtp","ses","resend"];var ln=["claude-code","cursor","codex","gemini-cli","windsurf"],fe=["USD","EUR","GBP","CAD","AUD","BRL","JPY"],ge=["node","vercel"],ye=["postgres","neon","supabase"],be=["redis","upstash"],Y=["sentry","posthog"],Me=["revenuecat","none"];function oe(e){return e.paymentProvider!==void 0&&e.paymentProvider!=="none"}var hr=[{key:"architecture",label:"Architecture",hint:"Fullstack (frontend hosts the API) or a standalone Hono backend.",category:"Infrastructure",kind:"enum",default:"fullstack",choices:rt,adoptable:!1,impact:"structural",shapesTree:!0},{key:"deploymentTarget",label:"Deployment target",hint:"Node.js / Docker or Vercel serverless.",category:"Infrastructure",kind:"enum",default:"node",choices:ge,adoptable:!1,impact:"config",shapesTree:!0},{key:"databaseProvider",label:"Database provider",hint:"Self-hosted PostgreSQL, Neon, or Supabase.",category:"Infrastructure",kind:"enum",default:"postgres",choices:ye,adoptable:!1,impact:"config",shapesTree:!0},{key:"cacheProvider",label:"Cache provider",hint:"Self-hosted Redis or Upstash.",category:"Infrastructure",kind:"enum",default:"redis",choices:be,adoptable:!1,impact:"config",shapesTree:!0},{key:"paymentProvider",label:"Payment provider",hint:"Stripe, Polar, or none.",category:"Features",kind:"enum",default:"none",choices:nt,adoptable:!1,impact:"structural",shapesTree:!0},{key:"defaultCurrency",label:"Default currency",hint:"Billing and pricing currency.",category:"Features",kind:"enum",default:"USD",choices:fe,adoptable:!1,impact:"config",shapesTree:!1},{key:"emailProvider",label:"Email provider",hint:"SMTP, Amazon SES, or Resend.",category:"Features",kind:"enum",default:"smtp",choices:it,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:Y,adoptable:!0,impact:"config",shapesTree:!0},{key:"multiTenancy",label:"Multi-tenancy (organizations)",hint:"Adds organizations - teams, members, and shared resources.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural",shapesTree:!1},{key:"projects",label:"Projects",hint:"Adds projects - a switcher that scopes a user's work inside their account or organization.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"config",shapesTree:!1},{key:"docs",label:"Docs app",hint:"Adds the self-hosted Fumadocs documentation site (apps/docs).",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural",shapesTree:!0},{key:"desktop",label:"Desktop app",hint:"Adds the cross-platform desktop app (apps/desktop, built with Electron).",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural",shapesTree:!0},{key:"mobile",label:"Mobile app",hint:"Adds the cross-platform mobile app (apps/mobile, built with Expo).",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural",shapesTree:!0},{key:"mobilePurchases",label:"Mobile store purchases",hint:"In-app purchases in the mobile app, through RevenueCat. Store purchases sit ON TOP of your web payment provider - a subscriber who buys in the app is the same account, billed by Apple or Google instead of by you. Off keeps the app on web billing, where the plan card says the subscription is managed on your website.",category:"Features",kind:"enum",default:()=>"none",choices:Me,adoptable:!0,impact:"structural",shapesTree:!0,requires:e=>e.mobile===!0&&oe(e),requiresLabel:"requires the mobile app and a web payment provider - store purchases are additive to the web lane, never a replacement"},{key:"ai",label:"AI features",hint:"Adds the AI surface (chat, models, automations, integrations) and unlocks the runner option. A desktop project ALWAYS ships the app's local AI stack: this option sets config.desktop.agents.enabled (the runtime gate on those screens). Off keeps config.ai.enabled false so the AI nav stays hidden - the AI packages still ship, because packages/translate depends on @repo/ai for the i18n pipeline every project uses.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural",shapesTree:!0},{key:"webAi",label:"AI in the web app",hint:"Whether the WEB app shows the AI surfaces - chat, automations, model and integration settings, and their nav entries. Off is the app-first shape: your desktop or mobile app is where your AI product lives and the website keeps account, billing and licensing only. The AI SERVICE is untouched either way (config.ai.enabled, the /api/ai routes and the desktop agents all stay exactly as your AI option set them), so both apps keep talking to the same backend. Only meaningful with AI features and at least one app - the desktop app or the mobile app - and never with the runner: its pairing and device screens are part of the web AI surface.",category:"Features",kind:"boolean",default:!0,adoptable:!0,impact:"config",shapesTree:!1,requires:e=>e.ai===!0&&(e.desktop===!0||e.mobile===!0)&&e.runner!==!0,requiresLabel:"requires AI features and an app to host them (the desktop app or the mobile app), and is unavailable with the runner (its pairing and device screens live in the web AI surface)"},{key:"runner",label:"Runner daemon",hint:"Lets your users connect a machine that runs their own AI subscriptions (and any device-side work you add) on their hardware. They install AgentRunner, the daemon we publish and keep updated - your project ships the pairing, dispatch and Runners surface, never the daemon. Needs an always-on backend: the transport holds one connection open per paired device, which serverless (Vercel) cannot do.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural",shapesTree:!0,requires:e=>e.ai===!0&&e.webAi!==!1&&me({...e,runner:!0})===void 0,requiresLabel:"requires AI features, the web AI surface, and an always-on deployment target (not Vercel)"},{key:"credits",label:"Metered credits",hint:"Adds metered usage credits on top of subscription plans.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"config",shapesTree:!1,requires:e=>oe(e),requiresLabel:"requires a payment provider"},{key:"revenueSharing",label:"Revenue sharing",hint:"Opt-in MRR leaderboard with dofollow backlinks.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"config",shapesTree:!1}];function Vs(e,t){return e[t]!==void 0}function pn(e){return hr.filter(t=>t.adoptable&&!Vs(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 Bs(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 Gs(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 dn(e){let t=[];for(let r of hr){let n=e[r.key];n!==void 0&&(Bs(r,n)||t.push(`\`${r.key}\` is ${JSON.stringify(n)}, but it must be ${Gs(r)}.`))}return t}function un(e){let t={};for(let r of hr)if(typeof r.default=="function"){if(r.requires&&!r.requires(e))continue;t[r.key]=r.default(e)}else t[r.key]=Array.isArray(r.default)?[...r.default]:r.default;return{...t,...e}}function fr(e,t){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||t}function jt(e){return e.split("-").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function Ut(e){return/^[a-z][a-z0-9-]*$/.test(e)}function j(e){return e instanceof Error?e.message:String(e)}var mn=["runner"];function gr(e){if(!mn.some(r=>e[r]===!0))return e;if(e.ai===!1)throw new Error("--runner requires AI features; remove --no-ai.");return e.ai===void 0&&(e.ai=!0),e}function yt(e){if(e.mobilePurchases!=="revenuecat")return e;if(e.mobile===!1)throw new Error("--mobile-purchases revenuecat requires the mobile app; remove --no-mobile.");if(e.paymentProvider==="none")throw new Error("--mobile-purchases revenuecat requires a payment provider (got --payment none): store purchases are additive to your web billing, never a replacement.");return e.mobile===void 0&&(e.mobile=!0),e}function bt(e){if(e.webAi!==!1)return e;if(e.ai===!1)throw new Error("--no-web-ai requires --ai (the desktop and mobile apps are where AI lives when the web surface is off).");if(e.desktop===!1&&e.mobile===!1)throw new Error("--no-web-ai requires --desktop or --mobile - with the web AI surface off, the desktop and mobile apps are the only places your AI features exist.");if(e.runner===!0)throw new Error("--runner needs the web AI surface; drop --runner or keep web AI.");return e}function hn(e){return mn.some(r=>e[r]===!0)&&(e.ai=!0),e}function fn(e){if(e.webAi!==!1)return[];let t=[];return e.desktop!==!0&&e.mobile!==!0&&t.push("neither `desktop` nor `mobile` is true, so this project has no desktop or mobile app - and with the web AI surface off, those apps are the only places its AI features would exist."),e.runner===!0&&t.push("`runner` is true, and the runner's pairing, install and Runners screens are part of the web AI surface."),t}function gn(e){if(e.mobilePurchases!=="revenuecat")return[];let t=[];return e.mobile!==!0&&t.push("`mobile` is not true, so this project has no app to sell in - the store lane's paywall, purchase and restore surfaces all live in `apps/mobile`."),oe(e)||t.push('`paymentProvider` is absent or `"none"`, and store purchases are additive to your web billing - there is nothing here for them to sit on top of.'),t}function yn(e){let t=Ks(e,e.desktopAi);return delete e.desktopAi,t}function Ks(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 bn(e){let t={};if(e.name!==void 0){if(!Ut(e.name))throw new Error(`Invalid project name "${e.name}". Use lowercase letters, numbers, and hyphens only. Must start with a letter.`);t.projectName=e.name}if(e.appName!==void 0){if(!e.appName.trim())throw new Error("App name cannot be empty.");t.appName=e.appName}if(e.location!==void 0?t.projectDir=e.location==="."?process.cwd():e.location:t.projectName!==void 0&&(t.projectDir=`./${t.projectName}`),e.architecture!==void 0&&(t.architecture=e.architecture),e.payment!==void 0&&(t.paymentProvider=e.payment),e.email!==void 0&&(t.emailProvider=e.email),e.org!==void 0&&(t.multiTenancy=e.org),e.projects!==void 0&&(t.projects=e.projects),e.docs!==void 0&&(t.docs=e.docs),e.desktop!==void 0&&(t.desktop=e.desktop),e.mobile!==void 0&&(t.mobile=e.mobile),e.mobilePurchases!==void 0){if(!Me.includes(e.mobilePurchases))throw new Error(`Invalid mobile purchases "${e.mobilePurchases}". Valid values: ${Me.join(", ")}`);t.mobilePurchases=e.mobilePurchases}if(e.ai!==void 0&&(t.ai=e.ai),e.webAi!==void 0&&(t.webAi=e.webAi),e.runner!==void 0&&(t.runner=e.runner),gr(t),yt(t),bt(t),e.revenueSharing!==void 0&&(t.revenueSharing=e.revenueSharing),e.credits!==void 0){if(e.credits===!0&&e.payment==="none")throw new Error("--credits requires a payment provider (got --payment none).");t.credits=e.credits}if(e.currency!==void 0){if(!fe.includes(e.currency))throw new Error(`Invalid currency "${e.currency}". Valid values: ${fe.join(", ")}`);t.defaultCurrency=e.currency}if(e.deploy!==void 0){if(!ge.includes(e.deploy))throw new Error(`Invalid deployment target "${e.deploy}". Valid values: ${ge.join(", ")}`);t.deploymentTarget=e.deploy}if(e.database!==void 0){if(!ye.includes(e.database))throw new Error(`Invalid database provider "${e.database}". Valid values: ${ye.join(", ")}`);t.databaseProvider=e.database}if(e.cache!==void 0){if(!be.includes(e.cache))throw new Error(`Invalid cache provider "${e.cache}". Valid values: ${be.join(", ")}`);t.cacheProvider=e.cache}if(e.observability!==void 0){let r=e.observability.split(",").map(i=>i.trim()).filter(Boolean),n=r.filter(i=>!Y.includes(i));if(n.length>0)throw new Error(`Invalid observability provider ${JSON.stringify(n.join(", "))}. Valid values: ${Y.join(", ")}`);t.observability=r}if(e.preset!==void 0){if(!Ue.includes(e.preset))throw new Error(`Invalid preset "${e.preset}". Valid values: ${Ue.join(", ")}`);let r=he[e.preset].values;t.deploymentTarget??=r.deploymentTarget,t.databaseProvider??=r.databaseProvider,t.cacheProvider??=r.cacheProvider}if(e.demo===!0&&(t.demo=!0),e.baseUrl!==void 0){let r=e.baseUrl.trim();if(r==="")throw new Error("--base-url cannot be empty. Provide an absolute URL like https://example.com.");let n;try{n=new URL(r)}catch{throw new Error(`Invalid --base-url "${e.baseUrl}". Must be an absolute URL like https://example.com.`)}if(n.protocol!=="http:"&&n.protocol!=="https:")throw new Error(`Invalid --base-url "${e.baseUrl}". Must use http or https.`);t.baseUrl=`${n.protocol}//${n.host}`}return t}var st={projectName:"my-saas",architecture:"fullstack",paymentProvider:"stripe",emailProvider:"smtp",multiTenancy:!1,projects:!1,docs:!1,desktop:!1,mobile:!1,mobilePurchases:"none",ai:!1,webAi:!0,runner:!1,revenueSharing:!1,credits:!0,defaultCurrency:"USD",deploymentTarget:"node",databaseProvider:"postgres",cacheProvider:"redis",observability:[]};function wn(e){let t=e.projectName??st.projectName,r=e.projectDir??`./${t}`,n=e.appName??jt(t),i=e.deploymentTarget??st.deploymentTarget,o=Z[i]?.edgeRuntime??!1,s=e.databaseProvider??(o?"neon":st.databaseProvider),a=e.cacheProvider??(o?"upstash":st.cacheProvider),d=e.emailProvider??(o?"resend":st.emailProvider),u={...st,...e,projectName:t,appName:n,projectDir:r,deploymentTarget:i,databaseProvider:s,cacheProvider:a,emailProvider:d};u.paymentProvider==="none"&&(u.credits=!1),(!u.mobile||u.paymentProvider==="none")&&(u.mobilePurchases="none"),yt(u);for(let y of Qe){if(u.deploymentTarget!==y.target)continue;let P=u.databaseProvider===y.provider?"database":"cache";if(u.databaseProvider===y.provider||u.cacheProvider===y.provider)throw new Error(`Incompatible: --deploy ${y.target} + --${P} ${y.provider}. ${y.reason}`)}for(let y of et)if(u.architecture===y.architecture&&u.deploymentTarget===y.target)throw new Error(`Incompatible: --architecture ${y.architecture} + --deploy ${y.target}. ${y.reason}`);let w=me(u);if(w)throw new Error(`Incompatible: --${w.feature} + --deploy ${w.target}. ${w.reason}`);return bt(u),u}function R(e){h.isCancel(e)&&(h.cancel("Setup cancelled."),process.exit(0))}function yr(e){let t=[];return e.databaseProvider==="neon"&&t.push({key:"DATABASE_URL",message:"Neon connection string (optional):",placeholder:"postgres://...",secret:!0}),e.databaseProvider==="supabase"&&t.push({key:"DATABASE_URL",message:"Supabase connection string (optional):",placeholder:"postgres://...",secret:!0}),e.cacheProvider==="upstash"&&(t.push({key:"UPSTASH_REDIS_REST_URL",message:"Upstash REST URL (optional):",placeholder:"https://...",secret:!1}),t.push({key:"UPSTASH_REDIS_REST_TOKEN",message:"Upstash REST token (optional):",secret:!0})),e.paymentProvider==="stripe"&&(t.push({key:"STRIPE_SECRET_KEY",message:"Stripe secret key (optional):",placeholder:"sk_test_...",secret:!0}),t.push({key:"STRIPE_WEBHOOK_SECRET",message:"Stripe webhook secret (optional):",placeholder:"whsec_...",secret:!0})),e.paymentProvider==="polar"&&(t.push({key:"POLAR_ACCESS_TOKEN",message:"Polar access token (optional):",secret:!0}),t.push({key:"POLAR_WEBHOOK_SECRET",message:"Polar webhook secret (optional):",secret:!0})),e.emailProvider==="resend"&&t.push({key:"RESEND_API_KEY",message:"Resend API key (optional):",placeholder:"re_...",secret:!0}),e.emailProvider==="ses"&&(t.push({key:"AMAZON_SES_REGION",message:"Amazon SES region (optional):",placeholder:"us-east-1",secret:!1}),t.push({key:"AMAZON_SES_KEY",message:"Amazon SES access key (optional):",secret:!0}),t.push({key:"AMAZON_SES_SECRET",message:"Amazon SES secret (optional):",secret:!0})),e.demo&&t.push({key:"TURNSTILE_SECRET_KEY",message:"Cloudflare Turnstile secret key (optional):",secret:!0}),t}async function vn(e,t){let r=!1;e&&gr(e),e&&yt(e),e&&bt(e),h.log.info(g.bold("Project"));let n=e?.projectName??await(async()=>{r=!0;let l=await h.text({message:"Project name:",placeholder:"my-saas",validate:m=>{if(!m?.trim())return"Project name is required.";if(!Ut(m))return"Use lowercase letters, numbers, and hyphens only. Must start with a letter."}});return R(l),l})(),i=e?.appName??await(async()=>{r=!0;let l=await h.text({message:"App name:",initialValue:jt(n),validate:m=>{if(!m?.trim())return"App name is required."}});return R(l),l})(),o=e?.projectDir??await(async()=>{r=!0;let l=await h.text({message:"Project location:",initialValue:`./${n}`});return R(l),l==="."?process.cwd():l})();h.log.info(g.bold("Infrastructure"));let s={};if(e?.deploymentTarget===void 0&&e?.databaseProvider===void 0&&e?.cacheProvider===void 0){r=!0;let l=await h.select({message:"Infrastructure:",options:[...Ue.map(m=>({value:m,label:he[m].label,hint:he[m].hint})),{value:"custom",label:"Customize",hint:"choose each piece yourself"}]});if(R(l),l!=="custom"){let m=he[l].values;s.deploymentTarget=m.deploymentTarget,s.databaseProvider=m.databaseProvider,s.cacheProvider=m.cacheProvider,h.log.info(`${he[l].label}: ${he[l].hint}.`)}}let a=e?.deploymentTarget??s.deploymentTarget??"node";if(e?.deploymentTarget===void 0&&s.deploymentTarget===void 0){r=!0;let l=await h.select({message:"Deployment target:",options:ge.map(m=>({value:m,label:Z[m].label,hint:Z[m].hint}))});R(l),a=l}let d=e?.architecture?et.find(l=>l.architecture===e.architecture&&l.target===a):void 0;if(d)throw new Error(`Incompatible: --architecture ${d.architecture} + --deploy ${d.target}. ${d.reason}`);let u=me({...e,deploymentTarget:a});if(u)throw new Error(`Incompatible: --${u.feature} + --deploy ${u.target}. ${u.reason}`);let w=me({runner:!0,deploymentTarget:a})!==void 0,y=new Set(et.filter(l=>l.target===a).map(l=>l.architecture)),P=rt.filter(l=>!y.has(l)),I=e?.architecture??await(async()=>{if(P.length===1){let m=P[0];return h.log.info(`Auto-selected ${gt[m].label} architecture (only compatible option for ${Z[a].label}).`),m}r=!0;let l=await h.select({message:"Architecture:",options:P.map(m=>({value:m,label:gt[m].label,hint:gt[m].hint}))});return R(l),l})(),_=e?.databaseProvider??s.databaseProvider??await(async()=>{r=!0;let l=ye.filter(x=>!Qe.some(Ae=>Ae.target===a&&Ae.provider===x));if(l.length===1){let x=l[0];return h.log.info(`Auto-selected ${$e[x].label} (only compatible option for ${Z[a].label}).`),x}let m=await h.select({message:"Database provider:",options:l.map(x=>({value:x,label:$e[x].label,hint:$e[x].hint}))});return R(m),m})(),f=e?.cacheProvider??s.cacheProvider??await(async()=>{r=!0;let l=be.filter(x=>!Qe.some(Ae=>Ae.target===a&&Ae.provider===x));if(l.length===1){let x=l[0];return h.log.info(`Auto-selected ${je[x].label} (only compatible option for ${Z[a].label}).`),x}let m=await h.select({message:"Cache provider:",options:l.map(x=>({value:x,label:je[x].label,hint:je[x].hint}))});return R(m),m})();if(Z[a]?.edgeRuntime){let l=cn.map(m=>` - ${m}`).join(`
3
- `);h.note(l,"Unavailable on edge runtime")}h.log.info(g.bold("Features"));let O=e?.paymentProvider??await(async()=>{r=!0;let l=await h.select({message:"Payment provider:",options:nt.map(m=>({value:m,label:Ct[m].label,hint:Ct[m].hint}))});return R(l),l})(),Oe=e?.defaultCurrency??await(async()=>{if(O==="none")return"USD";r=!0;let l=await h.select({message:"Default currency:",options:fe.map(m=>({value:m,label:m,hint:Le[m].name}))});return R(l),l})(),q=e?.emailProvider??await(async()=>{r=!0;let l=await h.select({message:"Email provider:",options:it.map(m=>({value:m,label:Lt[m].label,hint:Lt[m].hint}))});return R(l),l})(),J=e?.observability??await(async()=>{r=!0;let l=await h.multiselect({message:"Observability (space to select, enter to confirm):",required:!1,initialValues:[],options:Y.map(m=>({value:m,label:$t[m].label,hint:$t[m].hint}))});return R(l),l})(),xe=e?.multiTenancy??await(async()=>{r=!0;let l=await h.confirm({message:"Enable multi-tenancy (organizations)?",initialValue:!1});return R(l),l})(),De=e?.projects??await(async()=>{r=!0;let l=await h.confirm({message:"Enable projects (a switcher that scopes a user's work)?",initialValue:!1});return R(l),l})(),Ne=e?.docs??await(async()=>{r=!0;let l=await h.confirm({message:"Include docs app? (self-hosted Fumadocs documentation site)",initialValue:!1});return R(l),l})(),Ze=t?.desktopAllowed!==!1,v=e?.desktop??(Ze?await(async()=>{r=!0;let l=await h.confirm({message:"Include the desktop app? (apps/desktop - cross-platform, built with Electron, device-auth). With AI on, it also runs a local AI stack.",initialValue:!1});return R(l),l})():(h.log.info(g.dim("Desktop app: available on the Pro plan and up - skipped.")),!1)),M=t?.mobileAllowed!==!1,G=e?.mobile??(M?await(async()=>{r=!0;let l=await h.confirm({message:"Include the mobile app? (apps/mobile - iOS and Android, built with Expo, with push notifications and universal links)",initialValue:!1});return R(l),l})():(h.log.info(g.dim("Mobile app: available on the Pro plan and up - skipped.")),!1)),se=!G||O==="none"?"none":e?.mobilePurchases??await(async()=>{r=!0;let x=await h.select({message:"Mobile in-app purchases?",options:[{value:"revenuecat",label:"RevenueCat",hint:"store billing on top of your web provider - Apple and Google charge, your app grants"},{value:"none",label:"None",hint:"web billing only; the app shows the plan, never a paywall"}],initialValue:"none"});return R(x),x})();yt({mobile:G,mobilePurchases:se,paymentProvider:O});let N=e?.ai??await(async()=>{r=!0;let l=await h.confirm({message:"Add AI features? (chat, models, automations, integrations - and unlocks the runner)",initialValue:!1});return R(l),l})(),de=t?.runnerAllowed!==!1,X=e?.webAi===!1,ue=!N||w||X?!1:e?.runner??(de?await(async()=>{r=!0;let l=await h.confirm({message:"Add the runner daemon for your WEB users? A small program they install on their own machine or VPS to run their Claude Code / Codex subscription for this app 24/7 - the AI runs on their hardware, not yours. Off means no install UI and no pairing endpoint on your site; you can adopt it later.",initialValue:!1});return R(l),l})():(h.log.info(g.dim("Runner: available on the Pro plan and up - skipped.")),!1));N&&w?h.log.info(g.dim(`Runner: ${Z[a].label} cannot hold a connection open - skipped.`)):N&&X&&h.log.info(g.dim("Runner: its pairing and device screens live in the web AI surface, which is off - skipped."));let Ce=e?.webAi??((v||G)&&N&&!ue?await(async()=>{r=!0;let l=await h.confirm({message:"Show the AI surfaces in the web app? (No: app-first - your desktop or mobile app is where your AI product lives, and the web keeps account, billing, and licensing only)",initialValue:!0});return R(l),l})():!0);bt({webAi:Ce,ai:N,desktop:v,mobile:G,runner:ue});let D=e?.revenueSharing??await(async()=>{r=!0;let l=await h.confirm({message:"Enable revenue sharing? (opt-in MRR leaderboard with dofollow backlinks)",initialValue:!1});return R(l),l})(),ht=O==="none"?!1:e?.credits??await(async()=>{r=!0;let l=await h.confirm({message:"Enable credits? (metered usage on top of subscription plans)",initialValue:!0});return R(l),l})(),pr=e?.demo,Dt=yr({databaseProvider:_,cacheProvider:f,paymentProvider:O,emailProvider:q,demo:pr}),ft={};if(Dt.length>0&&r){h.log.info(g.bold("Credentials")+g.dim(" all optional - press Enter to skip, fill in .env later"));for(let l of Dt)if(r=!0,l.secret){let m=await h.password({message:l.message,mask:"*"});R(m),typeof m=="string"&&m.trim()&&(ft[l.key]=m.trim())}else{let m=await h.text({message:l.message,placeholder:l.placeholder});R(m),typeof m=="string"&&m.trim()&&(ft[l.key]=m.trim())}}if(r){let l=[` Name: ${g.cyan(n)}`,` App name: ${g.cyan(i)}`,` Location: ${g.cyan(o)}`,` Architecture: ${g.cyan(gt[I].label)}`].join(`
4
- `),m=[` Deploy target: ${g.cyan(Z[a]?.label??"Node.js / Docker")}`,` Database: ${g.cyan($e[_].label)}`,` Cache: ${g.cyan(je[f].label)}`].filter(Boolean).join(`
5
- `),x=[O!=="none"?` Payment: ${g.cyan(Ct[O].label)} (${Oe})`:` Payment: ${g.dim("none")}`,` Credits: ${ht?g.cyan("Yes"):g.dim("No")}`,` AI features: ${N?g.cyan("Yes"):g.dim("No")}`,...N?[` Runner: ${ue?g.cyan("Yes"):g.dim("No")}`]:[],...Ce?[]:[` Web AI: ${g.dim("No")} (app-first)`],` Email: ${g.cyan(Lt[q].label)}`,` Observability: ${J.length===0?g.dim("none"):g.cyan(J.map(dr=>$t[dr].label).join(", "))}`,` Multi-tenancy: ${xe?g.cyan("Yes"):g.dim("No")}`,` Projects: ${De?g.cyan("Yes"):g.dim("No")}`,` Docs app: ${Ne?g.cyan("Yes"):g.dim("No")}`,` Desktop app: ${v?g.cyan("Yes"):g.dim("No")}`,` Mobile app: ${G?g.cyan("Yes"):g.dim("No")}`,...se==="none"?[]:[` In-app purch.: ${g.cyan("RevenueCat")}`],` Rev. sharing: ${D?g.cyan("Yes"):g.dim("No")}`].join(`
6
- `),Ae=[g.bold("Project"),l,"",g.bold("Infrastructure"),m,"",g.bold("Features"),x];if(Dt.length>0){let dr=Dt.map(nn=>{let Ms=ft[nn.key]?g.green("provided"):g.dim("skipped");return` ${nn.key}: ${Ms}`}).join(`
7
- `);Ae.push("",g.bold("Credentials"),dr)}h.note(Ae.join(`
8
- `),"Summary");let rn=await h.confirm({message:"Proceed with these settings?"});(h.isCancel(rn)||!rn)&&(h.cancel("Setup cancelled."),process.exit(0))}return{projectName:n,appName:i,projectDir:o,architecture:I,deploymentTarget:a,databaseProvider:_,cacheProvider:f,paymentProvider:O,emailProvider:q,observability:J,multiTenancy:xe,projects:De,docs:Ne,desktop:v,mobile:G,mobilePurchases:se,ai:N,webAi:Ce,runner:ue,revenueSharing:D,credits:ht,defaultCurrency:Oe,...Object.keys(ft).length>0?{credentials:ft}:{},...e?.baseUrl!==void 0?{baseUrl:e.baseUrl}:{},...pr!==void 0?{demo:pr}:{}}}import{createReadStream as xn,existsSync as eo}from"fs";import{mkdir as to}from"fs/promises";import{join as ro}from"path";import{Readable as no}from"stream";import{pipeline as Ir}from"stream/promises";import{extract as io}from"tar";import{join as Fe}from"path";import{homedir as Hs}from"os";var wt=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",Q=".generatesaas",we=Fe(Q,"manifest.json"),kn="nextjs",br="nuxt",wr="2.4.1",En=Fe(Q,"hashes.json"),Mt=Fe(Q,"template-hashes.json"),Ft=Fe(Q,"template"),Sn=Fe(Q,"staging"),Pn=Fe(Q,"staging.json"),ve=Fe(Hs(),".generatesaas");var F=class extends Error{constructor(r,n,i){super(n);this.status=r;this.body=i}status;body;name="ApiError"};function ke(e){return{apiKey:e,baseUrl:wt}}async function Ie(e,t,r){let n=`${e.baseUrl}${t}`,i=await fetch(n,{...r,headers:{...r?.headers,Authorization:`Bearer ${e.apiKey}`,"User-Agent":"generatesaas-cli/3.11.0"}});if(!i.ok){let o,s;try{s=await i.json(),o=s.error??`API ${i.status}: ${t}`}catch{o=`API ${i.status}: ${t}`}throw new F(i.status,o,s)}return i}import{existsSync as Ys,mkdirSync as Ws,readFileSync as zs,writeFileSync as qs}from"fs";import{dirname as Js}from"path";import*as Re from"@clack/prompts";function vt(){if(!Ys(ve))return null;try{let e=JSON.parse(zs(ve,"utf-8"));return e.apiKey?e.apiKey:(e.token&&!e.apiKey&&Re.log.warning(`Found old GitHub token in ${ve}. Run 'generatesaas init' to set up your API key.`),null)}catch{return null}}function Ve(e){Ws(Js(ve),{recursive:!0}),qs(ve,`${JSON.stringify({apiKey:e},null," ")}
9
- `,{mode:384})}async function ot(e){if(e?.apiKey)return e.apiKey;let t=process.env.GENERATESAAS_API_KEY;if(t)return t;let r=vt();if(r)return r;if(!e?.prompt)throw new Error("API key not found. Set GENERATESAAS_API_KEY or run 'generatesaas init' to configure.");return kt()}async function kt(){let e=await Re.text({message:"Enter your GenerateSaaS API key:",placeholder:"gs_live_...",validate:t=>{if(!t?.trim())return"API key is required."}});return Re.isCancel(e)&&(Re.cancel("Setup cancelled."),process.exit(0)),e.trim()}import{readFile as Xs}from"fs/promises";async function Te(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 Ie(e,"/versions")).json()}async function An(e,t){if(process.env.GENERATESAAS_OFFLINE_LICENSE==="1")return null;try{return await(await Ie(e,`/changelog/${encodeURIComponent(t)}`)).text()}catch(r){if(r instanceof F&&r.status===404)return null;throw r}}async function In(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{skills:{"generatesaas-update":{skillMd:`# Offline skill stub
10
- `,scripts:{},references:{}}}}:await(await Ie(e,`/skills/${encodeURIComponent(t)}`)).json()}function Vt(e){if(!(e instanceof F)||e.status!==403)return null;let t=e.body;return!t||t.code!=="update_window_expired"?null:{message:e.message,lastAllowedVersion:t.lastAllowedVersion??null}}var vr="nextjs";function Rn(e){return{frontend:vr,architecture:e.architecture,deployTarget:e.deploymentTarget,database:e.databaseProvider,cache:e.cacheProvider,payment:e.paymentProvider,email:e.emailProvider,multiTenancy:e.multiTenancy,blog:!0,docs:e.docs,desktop:e.desktop,mobile:e.mobile,runner:e.runner,credits:e.credits,revenueSharing:e.revenueSharing,currency:e.defaultCurrency}}async function kr(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:"offline-test-token",licenseId:"offline-test-license-id",installId:t.installId}:await(await Ie(e,"/license/sign",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function Tn(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:t.currentToken}:await(await Ie(e,"/license/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function Er(e,t){let r=await fetch(`${e}/license/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok)throw new Error(`Verification service returned ${r.status}`);return await r.json()}async function _n(e,t,r){let n=await fetch(`${e}/license/inspect`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify(r)});if(!n.ok){let i=await n.json().catch(()=>null);throw new Error(i?.error??`Inspect endpoint returned ${n.status}`)}return await n.json()}var Sr=new Set([".git","node_modules",".pnpm-store",".env",".env.test",".turbo",".data","dist",".next",".next-e2e-web",".next-e2e-desktop",".svelte-kit",".wrangler",".devcontainer","playwright-report","test-results",".drive-out",".mastra"]),Pr=new Set(["pnpm-lock.yaml"]);function at(e){if(Ar(e))return!0;for(let t of e.split("/"))if(Pr.has(t))return!0;return!1}var Zs=new Set(["data","mksaas","references","scripts",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","vitest.config.ts","README.md","TODO.md","OVERVIEW.md","HANDOFF.md"]),On=["scripts/docs-lint.ts"],Qs=["docs/superpowers","packages/cli","packages/cli-api","packages/config/tests/runner-brand-lockstep.test.ts","packages/database/tests/no-migration-history.test.ts","apps/runner","docs/agentrunner","infra/docker-compose.yml",".claude/commands",".claude/skills/web-port",".claude/skills/web-port-workspace",".claude/settings.local.json",".claude/worktrees"];function Ar(e){let t=e.split("/");for(let r of t)if(Sr.has(r))return!0;if(On.includes(e)||On.some(r=>r.startsWith(`${e}/`)))return!1;if(Zs.has(t[0]))return!0;for(let r of Qs)if(e===r||e.startsWith(`${r}/`))return!0;return!1}async function Bt(e,t,r){await to(r,{recursive:!0});let n=process.env.GENERATESAAS_TEMPLATE_TARBALL_DIR;if(n){let a=ro(n,`v${t}.tar.gz`);if(!eo(a))throw new Error(`GENERATESAAS_TEMPLATE_TARBALL_DIR is set but ${a} does not exist.`);await Ir(xn(a),Rr(r));return}let i=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(i){await Ir(xn(i),Rr(r));return}let o=await Ie(e,`/template/${encodeURIComponent(t)}`);if(!o.body)throw new Error("Empty response body");let s=no.fromWeb(o.body);await Ir(s,Rr(r))}function Rr(e){return io({cwd:e,strip:1,filter:t=>{let r=t.replace(/^[^/]+\//,"");return r?!Ar(r):!0},sync:!1})}import{readdir as Gt,readFile as ct,rm as Nn,rmdir as so,writeFile as Et}from"fs/promises";import{join as W}from"path";async function Cn(e){let t;try{t=await Gt(e,{withFileTypes:!0})}catch{return!1}let r=!1;for(let n of t){let i=W(e,n.name);if(n.isDirectory()){await Cn(i)&&(r=!0);continue}if(n.name.startsWith("_")){r=!0;continue}await Nn(i,{force:!0})}return r||await so(e).catch(()=>{}),r}async function Ln(e){let t=W(e,"packages/content"),r=[];try{r=(await Gt(t,{withFileTypes:!0})).filter(s=>s.isDirectory()).map(s=>s.name)}catch{return}let n=new Set;for(let s of r){let a=W(t,s),d;try{d=(await Gt(a,{withFileTypes:!0})).filter(u=>u.isDirectory())}catch{continue}for(let u of d)n.add(u.name),await Cn(W(a,u.name))}let i=await lo(e),o=new Set([...n].filter(s=>i===void 0||i.has(s)));await Promise.all([...o].map(s=>Nn(W(e,"apps/web/public/images",s),{recursive:!0,force:!0}))),await uo(e,o)}var oo="packages/config/src/index.ts",ao=/\bcontent:\s*\{[\s\S]*?\bsections:\s*\[([\s\S]*?)\]/,co=/\bslug:\s*"([^"]+)"/g;async function lo(e){let t;try{t=await ct(W(e,oo),"utf-8")}catch{return}let r=ao.exec(t)?.[1];if(r===void 0)return;let n=[...r.matchAll(co)].map(i=>i[1]).filter(i=>i!==void 0);return n.length>0?new Set(n):void 0}var po="packages/translate/.meta/mdx-translation-meta.json";async function uo(e,t){if(t.size===0)return;let r=W(e,po),n;try{n=JSON.parse(await ct(r,"utf-8"))}catch{return}if(typeof n!="object"||n===null)return;let i=n;if(typeof i.files!="object"||i.files===null)return;let o=i.files,s={};for(let[a,d]of Object.entries(o)){let u=a.split("/"),w=u.length>1?u[0]:void 0,y=u[u.length-1]?.startsWith("_")===!0;w!==void 0&&t.has(w)&&!y||(s[a]=d)}await Et(r,`${JSON.stringify({...i,files:s},null," ")}
11
- `)}var mo="packages/config/src/blog.ts";async function $n(e){let t=W(e,mo),r=await ct(t,"utf-8"),n=Dn(Dn(r,"blogCategories",t),"blogAuthors",t);await Et(t,n)}function Dn(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 jn(e){let t=W(e,"packages/i18n/translations"),r;try{r=await Gt(t)}catch{return}for(let n of r){let i=W(t,n,"web.json"),o;try{o=await ct(i,"utf-8")}catch{continue}let s=JSON.parse(o);!s.content?.categories||s.content.categories.blog===void 0||(s.content.categories.blog={},await Et(i,`${JSON.stringify(s,null," ")}
12
- `))}}var ho="packages/config/src/ai/tasks.ts",fo="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.",go="The array starts EMPTY, which hides the Tasks tab until you declare your first task.";async function Un(e){let t=W(e,ho),r=await ct(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(fo,go);if(i===n)throw new Error(`removeDemoAiTasks: the demo-task paragraph in ${t} did not match, so the shipped file would describe a demo task it no longer carries (boilerplate drift).`);await Et(t,i)}async function Mn(e){let t=W(e,".claude","settings.json"),r;try{r=await ct(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(yo)),await Et(t,`${JSON.stringify(n,null," ")}
13
- `)}function yo(e){return!(e.startsWith("mcp__")||e.includes("nuxt"))}import{readFileSync as Ro}from"fs";import{dirname as To,join as _r}from"path";import{fileURLToPath as _o}from"url";import{existsSync as bo}from"fs";import{join as Fn}from"path";function Kt(e,t,r){let n=Fn(e,...t);return bo(n)?n:Fn(e,r)}import{mkdir as wo,writeFile as vo,readdir as ko,rm as Eo,rmdir as So}from"fs/promises";import{dirname as Ht,join as Po,relative as Ao,sep as Io}from"path";function Be(e){return e.split(Io).join("/")}async function Yt(e){await wo(e,{recursive:!0})}async function c(e,t){await Yt(Ht(e)),await vo(e,t,"utf-8")}async function lt(e,t){await Eo(e,{force:!0});let r=Ht(e);for(;r!==t&&r!==Ht(r);){try{await So(r)}catch{return}r=Ht(r)}}async function Ee(e,t,r,n){let i=[],o=await ko(e,{withFileTypes:!0});for(let s of o){let a=Po(e,s.name),d=Be(Ao(t,a));r(d)||(s.isDirectory()?i.push(...await Ee(a,t,r,n)):s.isFile()&&(n===void 0||n(d))&&i.push(a))}return i}var Oo={postgres:"Postgres",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"},xo={resend:"Resend",ses:"Amazon SES",smtp:"SMTP"},Do={redis:"Redis",upstash:"Upstash Redis"},No={node:"Node.js / Docker",vercel:"Vercel"},Tr="apps/web",Co="Next.js 16",Lo="`app/` routes, `components/`, `lib/`",$o={stripe:"Stripe",polar:"Polar"};function jo(e){let t=e.architecture==="fullstack",r=t?"(fullstack - Hono API mounted inside the app)":"(separate - standalone Hono backend)",n=t?`Mounted inside \`${Tr}\`. A standalone \`apps/backend\` is also kept (inert) so you can switch to separate later.`:"Runs from `apps/backend`; the frontend reaches it over HTTP.",i=Do[e.cacheProvider],o=No[e.deploymentTarget],s=e.paymentProvider==="none"?"":`
14
- - **Payments:** ${$o[e.paymentProvider]}`,a=e.desktop?e.mobile?", the desktop app (built with Electron) under `docs/desktop/` and the mobile app (built with Expo) under `docs/mobile/`":", and the desktop app (built with Electron) under `docs/desktop/`":e.mobile?", and the mobile app (built with Expo) under `docs/mobile/`":"",d=e.runner?"\n- **Runner daemon** - your users install AgentRunner on a machine they control and pair it with their account. Everything you own lives in this repo: the `/runner/*` routes, the relay, the Runners page, and the capability layer runs get. The daemon itself is published separately and auto-updates - there is nothing to build or release here.":"",u=e.desktop?" With the desktop app on, both shells render the same body, so one change lands in both.":"",w=`\`useTranslations()\` from \`next-intl\` in components; messages are loaded in \`${Tr}/i18n/request.ts\`.`;return Mo({archSuffix:r,apiLocation:n,databaseName:Oo[e.databaseProvider],cacheName:i,paymentLine:s,emailName:xo[e.emailProvider],deployName:o,runnerBullet:d,appDocsRef:a,sharedScreensBothShells:u,i18nRule:w,webAppDir:Tr,webAppName:Co,webAppLayout:Lo})}function Uo(){return Kt(To(_o(import.meta.url)),["generators","content"],"content")}function Mo(e){let t=Ro(_r(Uo(),"AGENTS.template.md"),"utf-8"),r=new Map(Object.entries(e)),n=t.replace(/\{\{(\w+)\}\}/g,(o,s)=>{let a=r.get(s);if(a===void 0)throw new Error(`AGENTS.template.md uses unknown token {{${s}}}`);return a}),i=/\{\{[^}]*\}\}/.exec(n);if(i)throw new Error(`AGENTS.template.md carries an unrendered token: ${i[0]}`);return n}async function Vn(e){await c(_r(e.projectDir,"AGENTS.md"),jo(e)),await c(_r(e.projectDir,"CLAUDE.md"),`@AGENTS.md
15
- `)}import{join as Fo}from"path";var Bn="apps/web",Or="Next.js 16",Vo={postgres:"Postgres (self-hosted)",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"};function Bo(e){let t=e.appName.trim()||e.projectName,r=Vo[e.databaseProvider],n=e.architecture==="fullstack"?`${Or} app at \`${Bn}/\` with the Hono API mounted inside it. A standalone \`apps/backend/\` is also included (inert in this fullstack setup) so you can split the API into a separate service later without re-scaffolding.`:`${Or} app at \`${Bn}/\` and a separate Hono backend at \`apps/backend/\`.`,i=e.architecture==="fullstack"?`- App + API: http://localhost:3000
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 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 Br(e){let t=e?` GenerateSaaS v${e} `:" GenerateSaaS ";kt.intro(Zt.bgYellow(Zt.black(t)))}function Gr(){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 Hr=["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 Yr=["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 Wr(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 zr(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 qr(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 Jr(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 Xr(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 Zr(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 Qr(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 en(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 tn(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=Hr.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(`
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(`
6
+ `),Ee=[y.bold("Project"),c,"",y.bold("Infrastructure"),u,"",y.bold("Features"),R];if(ke.length>0){let Xt=ke.map(Vr=>{let ws=A[Vr.key]?y.green("provided"):y.dim("skipped");return` ${Vr.key}: ${ws}`}).join(`
7
+ `);Ee.push("",y.bold("Credentials"),Xt)}f.note(Ee.join(`
8
+ `),"Summary");let Fr=await f.confirm({message:"Proceed with these settings?"});(f.isCancel(Fr)||!Fr)&&(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 mn,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"),rn="nextjs",nr="nuxt",ir="2.4.1",nn=Ce(Y,"hashes.json"),Tt=Ce(Y,"template-hashes.json"),Rt=Ce(Y,"template"),sn=Ce(Y,"staging"),on=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.12.0"}});if(!i.ok){let o,s;try{s=await i.json(),o=s.error??`API ${i.status}: ${t}`}catch{o=`API ${i.status}: ${t}`}throw new $(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
+ `,{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 an(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 cn(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 ln(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 pn(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 dn(e,t,r){let n=await fetch(`${e}/license/inspect`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify(r)});if(!n.ok){let i=await n.json().catch(()=>null);throw new Error(i?.error??`Inspect endpoint returned ${n.status}`)}return await n.json()}var 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"]),un=["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(un.includes(e)||un.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(mn(a),ur(r));return}let i=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(i){await dr(mn(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 fn,rmdir as Us,writeFile as ft}from"fs/promises";import{join as V}from"path";async function gn(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 gn(i)&&(r=!0);continue}if(n.name.startsWith("_")){r=!0;continue}await fn(i,{force:!0})}return r||await Us(e).catch(()=>{}),r}async function yn(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 gn(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=>fn(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," ")}
11
+ `)}var Ys="packages/config/src/blog.ts";async function bn(e){let t=V(e,Ys),r=await it(t,"utf-8"),n=hn(hn(r,"blogCategories",t),"blogAuthors",t);await ft(t,n)}function hn(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 vn(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
+ `))}}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 wn(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 kn(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
+ `)}function Js(e){return!(e.startsWith("mcp__")||e.includes("nuxt"))}import{readFileSync as oo}from"fs";import{dirname as ao,join as hr}from"path";import{fileURLToPath as co}from"url";import{existsSync as Xs}from"fs";import{join as En}from"path";function Dt(e,t,r){let n=En(e,...t);return Xs(n)?n:En(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"?"":`
14
+ - **Payments:** ${go[e.paymentProvider]}`,a=e.desktop?e.mobile?", the desktop app (built with Electron) under `docs/desktop/` and the mobile app (built with Expo) under `docs/mobile/`":", and the desktop app (built with Electron) under `docs/desktop/`":e.mobile?", and the mobile app (built with Expo) under `docs/mobile/`":"",d=e.desktop?" With the desktop app on, both shells render the same body, so one change lands in both.":"",h=`\`useTranslations()\` from \`next-intl\` in components; messages are loaded in \`${mr}/i18n/request.ts\`.`;return vo({archSuffix:r,apiLocation:n,databaseName:lo[e.databaseProvider],cacheName:i,paymentLine:s,emailName:po[e.emailProvider],deployName:o,appDocsRef:a,sharedScreensBothShells:d,i18nRule:h,webAppDir:mr,webAppName:ho,webAppLayout:fo})}function bo(){return Dt(ao(co(import.meta.url)),["generators","content"],"content")}function vo(e){let t=oo(hr(bo(),"AGENTS.template.md"),"utf-8"),r=new Map(Object.entries(e)),n=t.replace(/\{\{(\w+)\}\}/g,(o,s)=>{let a=r.get(s);if(a===void 0)throw new Error(`AGENTS.template.md uses unknown token {{${s}}}`);return a}),i=/\{\{[^}]*\}\}/.exec(n);if(i)throw new Error(`AGENTS.template.md carries an unrendered token: ${i[0]}`);return n}async function Sn(e){await l(hr(e.projectDir,"AGENTS.md"),yo(e)),await l(hr(e.projectDir,"CLAUDE.md"),`@AGENTS.md
15
+ `)}import{join as wo}from"path";var Pn="apps/web",fr="Next.js 16",ko={postgres:"Postgres (self-hosted)",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"};function Eo(e){let t=e.appName.trim()||e.projectName,r=ko[e.databaseProvider],n=e.architecture==="fullstack"?`${fr} app at \`${Pn}/\` with the Hono API mounted inside it. A standalone \`apps/backend/\` is also included (inert in this fullstack setup) so you can split the API into a separate service later without re-scaffolding.`:`${fr} app at \`${Pn}/\` and a separate Hono backend at \`apps/backend/\`.`,i=e.architecture==="fullstack"?`- App + API: http://localhost:3000
16
16
  - Inngest dev server: http://127.0.0.1:8288`:`- App: http://localhost:3000
17
17
  - API: http://localhost:3010
18
18
  - Inngest dev server: http://127.0.0.1:8288`,o=e.ai?`pnpm mastra:studio # optional: inspect the AI assistant and its memory (localhost:4111)
@@ -23,7 +23,7 @@ ${n}
23
23
 
24
24
  ## Stack
25
25
 
26
- - **Frontend:** ${Or}
26
+ - **Frontend:** ${fr}
27
27
  - **Backend:** Hono (TypeScript, RPC-typed)
28
28
  - **Auth:** Better Auth
29
29
  - **ORM / DB:** Drizzle + ${r}
@@ -75,28 +75,27 @@ pnpm dlx generatesaas eject
75
75
  ## Updates
76
76
 
77
77
  To pull the latest boilerplate changes into this project, open it in your AI coding agent and ask: \`update my GenerateSaaS project\`.
78
- `}async function Gn(e){await c(Fo(e.projectDir,"README.md"),Bo(e))}import{access as Go}from"fs/promises";import{join as St}from"path";async function Ko(e,t,r){let n=St(e,"apps/desktop/dev-app-update.yml");try{await Go(n)}catch{throw new Error(`Cannot neutralize the desktop dev update feed: ${n} is missing (boilerplate drift).`)}await c(n,`provider: generic
78
+ `}async function An(e){await l(wo(e.projectDir,"README.md"),Eo(e))}import{access as So}from"fs/promises";import{join as gt}from"path";async function Po(e,t,r){let n=gt(e,"apps/desktop/dev-app-update.yml");try{await So(n)}catch{throw new Error(`Cannot neutralize the desktop dev update feed: ${n} is missing (boilerplate drift).`)}await l(n,`provider: generic
79
79
  url: ${t}
80
80
  updaterCacheDirName: ${r}
81
- `)}function Ho(e){let t=e.split(".");return t.length>=3?t.slice(1).join("."):e}function Yo(e,t){let r=fr(e,"app");return{appId:`${t.split(".").reverse().join(".")}.${r.replace(/-/g,"")}`,productName:e,protocol:r}}function Wo(e){let t=e.toLowerCase().replace(/[^a-z0-9_]/g,"");return/^[a-z]/.test(t)?t:`app${t}`}function zo(e,t){let r=fr(e,"app");return{appName:e,slug:r,bundleId:[...t.split(".").reverse(),r.replace(/-/g,"")].map(Wo).join("."),scheme:/^[a-z]/.test(r)?r:`app-${r}`}}var pt={name:"AgentRunner",binary:"agentrunner",envPrefix:"AGENTRUNNER",repoUrl:"https://github.com/AlexChelan/AgentRunner",installBase:"https://github.com/AlexChelan/AgentRunner/releases/latest/download",image:"ghcr.io/alexchelan/agentrunner"};function qo(e){return e.runner?["{"," enabled: true,",' clientId: "runner",'," daemon: {",` name: ${JSON.stringify(pt.name)},`,` binary: ${JSON.stringify(pt.binary)},`,` envPrefix: ${JSON.stringify(pt.envPrefix)},`,` repoUrl: ${JSON.stringify(pt.repoUrl)},`,` installBase: ${JSON.stringify(pt.installBase)},`,` image: ${JSON.stringify(pt.image)}`," }"," }"].join(`
82
- `):'{ enabled: false, clientId: "runner" }'}function Jo(e){let t=[" // PRESENCE IS THE SWITCH: reporting runs as soon as the credential below is non-empty. There"," // is no separate `enabled` flag to remember - pasting the key IS turning it on, and clearing it"," // is turning it off. These are PUBLISHABLE credentials: they only authorize SENDING events,"," // never reading them, which is why they live here rather than in env and why a packaged desktop"," // app can carry them. Server-side, SENTRY_DSN and POSTHOG_API_KEY override them."].join(`
81
+ `)}function Ao(e){let t=e.split(".");return t.length>=3?t.slice(1).join("."):e}function Io(e,t){let r=tr(e,"app");return{appId:`${t.split(".").reverse().join(".")}.${r.replace(/-/g,"")}`,productName:e,protocol:r}}function To(e){let t=e.toLowerCase().replace(/[^a-z0-9_]/g,"");return/^[a-z]/.test(t)?t:`app${t}`}function Ro(e,t){let r=tr(e,"app");return{appName:e,slug:r,bundleId:[...t.split(".").reverse(),r.replace(/-/g,"")].map(To).join("."),scheme:/^[a-z]/.test(r)?r:`app-${r}`}}function _o(e){let t=[" // PRESENCE IS THE SWITCH: reporting runs as soon as the credential below is non-empty. There"," // is no separate `enabled` flag to remember - pasting the key IS turning it on, and clearing it"," // is turning it off. These are PUBLISHABLE credentials: they only authorize SENDING events,"," // never reading them, which is why they live here rather than in env and why a packaged desktop"," // app can carry them. Server-side, SENTRY_DSN and POSTHOG_API_KEY override them."].join(`
83
82
  `),r=[];return e.includes("sentry")&&r.push(' sentry: { dsn: "" }'),e.includes("posthog")&&r.push([" // llmContent sends your users' prompts and completions to PostHog. Every number (tokens,"," // cost, latency, errors) is captured either way - turn it on only if your privacy policy"," // covers sending their message text to a third party.",' posthog: { publicKey: "", host: "https://us.i.posthog.com", llmContent: false }'].join(`
84
83
  `)),r.length===0?[" // Error and product reporting is OFF: no provider was selected at init, so neither adapter",' // nor SDK was generated. To turn one on, add `"sentry"`, `"posthog"` or both to the'," // `observability` array in `.generatesaas/manifest.json`, then run `generatesaas update` -"," // which re-shapes the project and installs what it needs. Note that update stages nothing"," // while you are already on the latest version, so take the upgrade first if it reports no"," // changes."," observability: {},"].join(`
85
84
  `):`${t}
86
85
  observability: {
87
86
  ${r.join(`,
88
87
  `)}
89
- },`}async function Kn(e){let t=e.appName.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),r=e.paymentProvider!=="none",n=e.baseUrl??"http://localhost:3000",i=e.baseUrl?new URL(e.baseUrl).hostname:"example.com",o=Ho(i),s=e.demo?"false":"true",a=e.multiTenancy?`{
88
+ },`}async function In(e){let t=e.appName.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),r=e.paymentProvider!=="none",n=e.baseUrl??"http://localhost:3000",i=e.baseUrl?new URL(e.baseUrl).hostname:"example.com",o=Ao(i),s=e.demo?"false":"true",a=e.multiTenancy?`{
90
89
  multiTenant: true,
91
90
  organizationLimit: 5,
92
91
  defaultOrganizationName: "Personal"
93
- }`:"{ multiTenant: false }",d=e.projects?"{ enabled: true }":"{ enabled: false }",u=Jo(e.observability),w=e.demo?`{
92
+ }`:"{ multiTenant: false }",d=e.projects?"{ enabled: true }":"{ enabled: false }",h=_o(e.observability),g=e.demo?`{
94
93
  enableInDev: true,
95
94
  chatwoot: {
96
95
  baseUrl: "https://support.alexchelan.com",
97
96
  websiteToken: "1uWvJLHkhWLihkK2bxLaj2Fg"
98
97
  }
99
- }`:"{}",y=e.architecture==="fullstack"?"false":"true",P=Yo(t,o),I=`https://cdn.${o}/desktop`,_=zo(t,o),f=qo(e),O=` // Store purchases (RevenueCat) for the mobile app - ADDITIVE to the web provider, never a
98
+ }`:"{}",k=e.architecture==="fullstack"?"false":"true",v=Io(t,o),P=`https://cdn.${o}/desktop`,m=Ro(t,o),D=` // Store purchases (RevenueCat) for the mobile app - ADDITIVE to the web provider, never a
100
99
  // replacement: store rules require digital goods to be sold through in-app purchase.
101
100
  mobile: {
102
101
  enabled: ${e.mobilePurchases==="revenuecat"},
@@ -105,7 +104,7 @@ ${r.join(`,
105
104
  grantCreditsToFamilyShare: false,
106
105
  externalPurchaseLinks: "off",
107
106
  customerCenter: true
108
- }`,Oe=`import type { AgentsConfig, AppConfig } from "@repo/config/types";
107
+ }`,ve=`import type { AgentsConfig, AppConfig } from "@repo/config/types";
109
108
  import { resolveAgentsConfig } from "./agents";
110
109
  import { desktopConfig } from "./desktop.mjs";
111
110
  import { mobileConfig } from "./mobile.mjs";
@@ -189,10 +188,10 @@ export const config: AppConfig = {
189
188
  enabled: true,
190
189
  provider: "${e.paymentProvider}",
191
190
  bannedCountries: ["BY", "CU", "IR", "KP", "RU", "SY"],
192
- ${O}
191
+ ${D}
193
192
  }`:`{
194
193
  enabled: false,
195
- ${O}
194
+ ${D}
196
195
  }`},
197
196
  sms: { enabled: false },
198
197
  newsletter: { enabled: false },
@@ -201,8 +200,8 @@ ${O}
201
200
  prefix: "key_"
202
201
  },
203
202
  apiDocs: true,
204
- performanceMonitor: { enabled: ${y} },
205
- ${u}
203
+ performanceMonitor: { enabled: ${k} },
204
+ ${h}
206
205
  notifications: {
207
206
  enabled: NOTIFICATIONS_ENABLED,
208
207
  maxPerUser: 100,
@@ -246,7 +245,7 @@ ${u}
246
245
  }`:`{
247
246
  base: "${e.defaultCurrency}",
248
247
  list: [
249
- { symbol: "${Le[e.defaultCurrency].symbol}", name: "${Le[e.defaultCurrency].name}", code: "${e.defaultCurrency}", place: "${Le[e.defaultCurrency].place}", space: ${Le[e.defaultCurrency].space} }
248
+ { symbol: "${_e[e.defaultCurrency].symbol}", name: "${_e[e.defaultCurrency].name}", code: "${e.defaultCurrency}", place: "${_e[e.defaultCurrency].place}", space: ${_e[e.defaultCurrency].space} }
250
249
  ],
251
250
  countryMap: {
252
251
  default: "${e.defaultCurrency}"
@@ -379,8 +378,6 @@ ${u}
379
378
  },
380
379
  desktop: desktopConfig,
381
380
  mobile: mobileConfig,
382
- // Runner: the AgentRunner daemon your users install to run their own subscription CLIs for this app. Off by default.
383
- runner: ${f},
384
381
  // Outward MCP server exposing the capability layer to external agents. Its code ships with every
385
382
  // project, so \`enabled\` alone controls the /mcp endpoint and the MCP settings tab.
386
383
  mcpServer: { enabled: ${e.demo?"true":"false"} },
@@ -415,7 +412,6 @@ ${u}
415
412
  automations: "/automations",
416
413
  settingsModels: "/settings/models",
417
414
  settingsIntegrations: "/settings/integrations",
418
- settingsRunners: "/settings/runners",
419
415
  settingsMcp: "/settings/mcp",
420
416
  settingsBilling: "/settings/billing",
421
417
  settingsBillingHistory: "/settings/billing/history",
@@ -446,7 +442,6 @@ export * from "./social-providers";
446
442
  export * from "./blog";
447
443
  export * from "./cache";
448
444
  export * from "./content";
449
- export * from "./runner";
450
445
  export * from "./footer";
451
446
  export * from "./i18n";
452
447
  export * from "./landing";
@@ -459,7 +454,7 @@ export * from "./robots";
459
454
  export * from "./roles";
460
455
  export * from "./section-tabs";
461
456
  export * from "./tenancy";
462
- `,q=St(e.projectDir,"packages/config/src/index.ts");await c(q,Oe),await c(St(e.projectDir,"packages/config/src/support.ts"),`import type { SupportConfig } from "@repo/config/types";
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";
463
458
 
464
459
  /**
465
460
  * Live-chat (support widget) configuration, re-exported by the config index as
@@ -471,8 +466,8 @@ export * from "./tenancy";
471
466
  * Node, which cannot resolve the index's extensionless relative imports. Keep any relative
472
467
  * import here type-only.
473
468
  */
474
- export const supportConfig: SupportConfig = ${w};
475
- `),await c(St(e.projectDir,"packages/config/src/desktop.mjs"),`// Plain-JS desktop app identity. NO env reads, so tooling that cannot import the
469
+ export const supportConfig: SupportConfig = ${g};
470
+ `),await l(gt(e.projectDir,"packages/config/src/desktop.mjs"),`// Plain-JS desktop app identity. NO env reads, so tooling that cannot import the
476
471
  // TypeScript config index reads it directly: the Electron main + renderer bundles,
477
472
  // the CSP policy builder, and the electron-builder packaging config. Edit these
478
473
  // values once to rebrand - they are the same in dev and production, so they live in
@@ -480,14 +475,14 @@ export const supportConfig: SupportConfig = ${w};
480
475
 
481
476
  export const desktopConfig = {
482
477
  enabled: ${e.desktop},
483
- appId: "${P.appId}",
484
- productName: "${P.productName}",
485
- protocol: "${P.protocol}",
478
+ appId: "${v.appId}",
479
+ productName: "${v.productName}",
480
+ protocol: "${v.protocol}",
486
481
  baseUrl: "${n}",
487
482
  // Public base URL the release workflow uploads to; electron-updater polls
488
483
  // \`<url>/latest-mac.yml\` (\`latest.yml\` on Windows, \`latest-linux.yml\` on Linux) plus
489
484
  // the per-installer blockmaps, and the stable installer download links live under it.
490
- autoUpdate: { url: "${I}" },
485
+ autoUpdate: { url: "${P}" },
491
486
  // Whether the support live-chat widget you configured in config.support (Crisp,
492
487
  // Chatwoot) also mounts in the DESKTOP app. On by default, so one provider reaches
493
488
  // every surface. Set false to keep live chat on your website only - the app then
@@ -523,7 +518,7 @@ export const desktopConfig = {
523
518
  background: false
524
519
  }
525
520
  };
526
- `),await c(St(e.projectDir,"packages/config/src/mobile.mjs"),`// Plain-JS mobile app identity. NO env reads, so tooling that cannot import the
521
+ `),await l(gt(e.projectDir,"packages/config/src/mobile.mjs"),`// Plain-JS mobile app identity. NO env reads, so tooling that cannot import the
527
522
  // TypeScript config index reads it directly: app.config.ts, the EAS build config,
528
523
  // Metro, and the app bundle itself (\`+native-intent\` reads UNIVERSAL_LINK_PATHS).
529
524
  // The CLI wrote these values from your app name + domain at init; edit them here
@@ -536,14 +531,14 @@ import { NOTIFICATIONS_ENABLED } from "./notifications-flag.mjs";
536
531
  export const mobileConfig = {
537
532
  enabled: ${e.mobile},
538
533
  // Home-screen and store display name.
539
- appName: "${_.appName}",
534
+ appName: "${m.appName}",
540
535
  // Expo slug, EAS project name, and the prefix the auth client namespaces its
541
536
  // stored session under.
542
- slug: "${_.slug}",
537
+ slug: "${m.slug}",
543
538
  // iOS bundle id and Android application id.
544
- bundleId: "${_.bundleId}",
539
+ bundleId: "${m.bundleId}",
545
540
  // Deep-link scheme, with NO \`://\` - the code appends the separator it needs.
546
- scheme: "${_.scheme}",
541
+ scheme: "${m.scheme}",
547
542
  // The web host whose /.well-known files claim this app's universal links. Your
548
543
  // PROJECT domain, subdomain included: it must match the host your links carry.
549
544
  universalLinks: { host: "${i}" },
@@ -636,9 +631,9 @@ export const SETTINGS_PRIVACY_PATH = "/settings/privacy";
636
631
  export const SETTINGS_APP_LOCK_PATH = "/settings/app-lock";
637
632
  /** app/(app)/settings/about.tsx - version, legal links and support. */
638
633
  export const SETTINGS_ABOUT_PATH = "/settings/about";
639
- `),e.desktop&&await Ko(e.projectDir,I,`${P.protocol}-updater`)}import{join as Xo}from"path";function Zo(e,t){let r=[e==="stripe"?' stripePriceId: "",':' polarProductId: "",'];return t&&r.push(' appleProductId: "",',' googleProductId: "",'," storeAmounts: {},"),r.join(`
640
- `)}function xr(e){let t=[];return e.withCredits&&(t.push(` credits: ${e.credits},`),e.recurring!==!1&&t.push(" creditInterval: 30,")),e.maxConcurrentRuns!==void 0?(t.push(` apiRateLimit: { maxRequests: ${e.rateLimit} },`),t.push(` maxConcurrentRuns: ${e.maxConcurrentRuns}`)):t.push(` apiRateLimit: { maxRequests: ${e.rateLimit} }`),t.join(`
641
- `)}async function Hn(e){let t=Xo(e.projectDir,"packages/config/src/pricing.ts"),r=e.defaultCurrency;if(e.paymentProvider==="none"){let I=`import type { PricingConfig } from "@repo/config/types";
634
+ `),e.desktop&&await Po(e.projectDir,P,`${v.protocol}-updater`)}import{join as Oo}from"path";function xo(e,t){let r=[e==="stripe"?' stripePriceId: "",':' polarProductId: "",'];return t&&r.push(' appleProductId: "",',' googleProductId: "",'," storeAmounts: {},"),r.join(`
635
+ `)}function gr(e){let t=[];return e.withCredits&&(t.push(` credits: ${e.credits},`),e.recurring!==!1&&t.push(" creditInterval: 30,")),e.maxConcurrentRuns!==void 0?(t.push(` apiRateLimit: { maxRequests: ${e.rateLimit} },`),t.push(` maxConcurrentRuns: ${e.maxConcurrentRuns}`)):t.push(` apiRateLimit: { maxRequests: ${e.rateLimit} }`),t.join(`
636
+ `)}async function Tn(e){let t=Oo(e.projectDir,"packages/config/src/pricing.ts"),r=e.defaultCurrency;if(e.paymentProvider==="none"){let P=`import type { PricingConfig } from "@repo/config/types";
642
637
 
643
638
  export const pricingConfig: PricingConfig = {
644
639
  defaultPlan: "free",
@@ -667,7 +662,7 @@ export const pricingConfig: PricingConfig = {
667
662
  credits: { enabled: false },
668
663
  products: { enabled: false, items: [] }
669
664
  };
670
- `;await c(t,I);return}let n=e.paymentProvider,i=Zo(n,e.mobilePurchases==="revenuecat"),o=e.credits,s=!!e.ai,a=xr({credits:5,rateLimit:100,withCredits:o,recurring:!1,...s&&{maxConcurrentRuns:2}}),d=xr({credits:10,rateLimit:1e3,withCredits:o,...s&&{maxConcurrentRuns:5}}),u=xr({credits:40,rateLimit:5e3,withCredits:o,...s&&{maxConcurrentRuns:10}}),w=r==="USD"?"":` // !! ${r} pricing WARNING: creditsPerUsd anchors credit cost to USD (AI providers bill in
665
+ `;await l(t,P);return}let n=e.paymentProvider,i=xo(n,e.mobilePurchases==="revenuecat"),o=e.credits,s=!!e.ai,a=gr({credits:5,rateLimit:100,withCredits:o,recurring:!1,...s&&{maxConcurrentRuns:2}}),d=gr({credits:10,rateLimit:1e3,withCredits:o,...s&&{maxConcurrentRuns:5}}),h=gr({credits:40,rateLimit:5e3,withCredits:o,...s&&{maxConcurrentRuns:10}}),g=r==="USD"?"":` // !! ${r} pricing WARNING: creditsPerUsd anchors credit cost to USD (AI providers bill in
671
666
  // !! USD), but your plan prices and credit purchases are denominated in ${r}. Verify that the
672
667
  // !! ${r} amounts above cover the USD cost per credit (~0.50 USD at creditsPerUsd 1 with the
673
668
  // !! 100% markup) - and raise prices or creditsPerUsd if they do not.
@@ -676,7 +671,7 @@ export const pricingConfig: PricingConfig = {
676
671
  !! plan prices and credit purchases are denominated in ${r}. At the shipped economics 1 credit
677
672
  !! carries ~0.50 USD of AI cost - verify every ${r} price still covers it (see the comment on the
678
673
  !! credits block in packages/config/src/pricing.ts), or raise prices / creditsPerUsd.
679
- `);let y=o?` // creditsPerUsd scales USD cost -> credits (1 = 1 credit carries ~0.50 USD of cost after the
674
+ `);let k=o?` // creditsPerUsd scales USD cost -> credits (1 = 1 credit carries ~0.50 USD of cost after the
680
675
  // 100% markup); markupPercent is the DEFAULT margin for everything that debits credits (AI,
681
676
  // external APIs, your own features), and credits.markupByCategory overrides it per metered
682
677
  // service ("ai", "webSearch", "webExtract"). Two optional knobs shape each debit's size:
@@ -687,7 +682,7 @@ export const pricingConfig: PricingConfig = {
687
682
  // you change plan credits, prices, or any markup: each plan's credits / (creditsPerUsd *
688
683
  // (1 + your LOWEST markup)) must stay BELOW what that plan earns per credit interval, or full
689
684
  // utilization runs you at a loss.
690
- ${w} credits: { enabled: true, creditsPerUsd: 1, markupPercent: 100 }`:" credits: { enabled: false }",P=`import type { PricingConfig } from "@repo/config/types";
685
+ ${g} credits: { enabled: true, creditsPerUsd: 1, markupPercent: 100 }`:" credits: { enabled: false }",v=`import type { PricingConfig } from "@repo/config/types";
691
686
 
692
687
  export const pricingConfig: PricingConfig = {
693
688
  defaultPlan: "free",
@@ -768,21 +763,21 @@ ${i}
768
763
  anchorAmounts: { ${r}: 349 }
769
764
  }
770
765
  ],
771
- ${u}
766
+ ${h}
772
767
  }
773
768
  ],
774
- ${y},
769
+ ${k},
775
770
  products: {
776
771
  enabled: false,
777
772
  items: []
778
773
  }
779
774
  };
780
- `;await c(t,P)}var Qo={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"}]},ea={stripe:[{key:"STRIPE_SECRET_KEY",comment:"# TODO: Add your Stripe keys"},{key:"STRIPE_WEBHOOK_SECRET"},{key:"STRIPE_API_BASE",comment:"# Point the Stripe SDK at another host - a local mock, or a proxy of your own. Unset means api.stripe.com."}],polar:[{key:"POLAR_ACCESS_TOKEN",comment:"# TODO: Add your Polar keys"},{key:"POLAR_WEBHOOK_SECRET"}]};function Wt(e,t){return t?e.map(r=>{let n=t[r.key];return n?{...r,defaultValue:n,comment:void 0}:r}):e}function Pt(e,t){for(let r of e)r.comment&&t.push(r.comment),r.defaultValue!==void 0?t.push(`${r.key}=${r.defaultValue}`):t.push(`#${r.key}=`)}function Dr(e){return Array.from(crypto.getRandomValues(new Uint8Array(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}function Nr(e){return e.architecture==="fullstack"?{apiUrl:"http://localhost:3000/api",baseUrl:"http://localhost:3000"}:{apiUrl:"http://localhost:3010",baseUrl:"http://localhost:3000"}}var ta="https://your-app.example.com/api";function zt(e,t,r,n){e.push(n==="example"?`${t}=`:`${t}=${r}`)}function Yn(e,t){let r=t==="example"?void 0:e.credentials,{apiUrl:n,baseUrl:i}=Nr(e),o=[];e.architecture==="separate"?o.push("# API Configuration","# Standalone backend's own URL (the frontend reaches it via the public var above).",`API_URL=${n}`,`BASE_URL=${i}`):o.push("# App","# (API_URL is derived from the frontend's *_PUBLIC_API_URL by the runtime env schema.)",`BASE_URL=${i}`),o.push("","# Client IP (production)","# The backend rate-limits by client IP and stamps it on audit entries. YOU ALMOST","# CERTAINLY DO NOT NEED THIS: it detects Vercel, Fly.io, Render, Railway, Netlify and","# Coolify from their own env vars, and treats a private connection peer (Docker, compose,","# Kubernetes, Dokploy, an nginx/Traefik/Caddy on the same host) as proof that something is","# forwarding to it. A forwarded header is trusted only in those cases, never on a server","# exposed directly - there, a caller could have sent it themselves."),e.architecture==="fullstack"?o.push("# This build runs the API in the frontend's route handler, which has no connection","# address, so on a host the backend does not recognise there is nothing to fall back on","# and every caller shares ONE rate-limit bucket: the 10/min sign-in limit applies to","# your whole app rather than per person, and audit entries record no address."):o.push("# This build reads the connection peer, so a server exposed directly identifies its","# callers correctly with nothing set here."),o.push("# SET IT when nothing above can see your topology - the common case is a CDN in front of a","# directly exposed server, where connections arrive from a public CDN address:","# Cloudflare -> TRUSTED_PROXY=cf-connecting-ip","# another proxy in front -> TRUSTED_PROXY=1 (the number of proxies)","# Whatever you set wins over detection; `false` turns detection off and trusts nothing.","# TRUSTED_PROXY=1"),o.push("","# Auth rate limits","# Raises every Better Auth rate limit by a whole number (1-50). Unset = 1 = the","# shipped limits. No value disables them: 0, empty or unparseable fails at boot.","# You almost certainly want the default. The reason to raise it is an automated suite,","# which drives ONE address and so shares one bucket - and the e2e config already sets","# its own value, so it needs nothing here.","# AUTH_RATE_LIMIT_MULTIPLIER=1"),o.push("","# Database"),Pt(Wt($e[e.databaseProvider].envVars,r),o),o.push("","# Cache"),Pt(Wt(je[e.cacheProvider].envVars,r),o),e.cacheProvider==="redis"&&o.push("# Optional prefix on every Redis key the app writes. Set it when several deployments share","# one Redis instance, or to keep a load test out of the live keyspace.","# Set it once, before first boot. Changing it on a running deployment orphans in-flight","# auto-top-up claims and distributed locks (autotopup:pending:*, withMutex) - drain or use","# a maintenance window first.","#REDIS_KEY_PREFIX="),o.push("","# Authentication"),t==="example"&&o.push("# Generate a strong secret, e.g. `openssl rand -hex 32`"),zt(o,"BETTER_AUTH_SECRET",crypto.randomUUID(),t),o.push("","# Content API (random secret; required when contentApi feature is enabled in @repo/config)"),zt(o,"CONTENT_API_KEY",Dr(32),t),o.push("","# Job Queue - Inngest","INNGEST_APP_ID=api"),zt(o,"INNGEST_EVENT_KEY",Dr(32),t),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."),zt(o,"INNGEST_SIGNING_KEY",Dr(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=Qo[e.emailProvider];if(s&&(o.push("","# Email"),Pt(Wt(s,r),o)),e.paymentProvider!=="none"){let d=ea[e.paymentProvider];d&&(o.push("","# Payment"),Pt(Wt(d,r),o))}o.push("","# Storage - S3 Compatible (required for uploads: avatars, files)","# Works with AWS S3, Cloudflare R2, MinIO, and other S3-compatible services.","# Uploads fail until these are set - storage is enabled in @repo/config by default.","#STORAGE_REGION=us-east-1","#STORAGE_ENDPOINT=https://your-account.r2.cloudflarestorage.com","#STORAGE_ACCESS_KEY_ID=","#STORAGE_SECRET_ACCESS_KEY=","# Two buckets: one publicly readable (avatars, assets), one credentials-only (private","# uploads served through the app). The bucket boundary is the privacy guarantee.","#STORAGE_PUBLIC_BUCKET=","#STORAGE_PRIVATE_BUCKET=",'# Public base URL for the public bucket. Public file URLs are STORAGE_PUBLIC_URL + "/" + key.',"#STORAGE_PUBLIC_URL=https://cdn.example.com","","# Storage - Local Filesystem (optional overrides)","# Defaults work out of the box; override for Docker volume mounts.","#STORAGE_LOCAL_PATH=/data","#STORAGE_LOCAL_PUBLIC_URL=https://cdn.example.com/files"),o.push("","# Admin Notifications (optional)","# Real-time notifications when business events occur (signups, purchases, etc.).","# Each provider is auto-enabled when its env vars are set.","#DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...","#SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...","#TELEGRAM_BOT_TOKEN=123456789:ABCdefGHIjklMNOpqrsTUVwxyz","#TELEGRAM_CHAT_ID=-1001234567890","#TELEGRAM_TOPIC_ID=123"),o.push("","# Social sign-in (optional)","# Enable the matching provider in @repo/config, then set its pair here.","#GOOGLE_CLIENT_ID=","#GOOGLE_CLIENT_SECRET=","#GITHUB_CLIENT_ID=","#GITHUB_CLIENT_SECRET=","# Apple needs four values rather than a pair, and they also back the mobile app's native","# Sign in with Apple - the same client is what the backend accepts a native token from.","#APPLE_CLIENT_ID=","#APPLE_TEAM_ID=","#APPLE_KEY_ID=","#APPLE_PRIVATE_KEY="),o.push("","# SMS - Twilio (optional)","#TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx","#TWILIO_AUTH_TOKEN=","#TWILIO_PHONE_NUMBER=+1234567890"),o.push("","# Newsletter (optional)","#RESEND_AUDIENCE_ID=","#LISTMONK_URL=https://listmonk.yourdomain.com","#LISTMONK_API_USER=admin","#LISTMONK_API_TOKEN=","#LISTMONK_LIST_ID=1"),o.push("","# Translations - OpenRouter (optional)","# Set to auto-translate en/* into your other locales with `pnpm translate`.","# The pre-commit hook runs it on commit; without a key it skips and untranslated","# locales fall back to the default locale. Get a key: https://openrouter.ai/keys","#OPENROUTER_API_KEY=");let a=[];return e.observability.includes("sentry")&&a.push({key:"SENTRY_DSN",comment:"# Overrides config.observability.sentry.dsn for the server only."}),e.observability.includes("posthog")&&a.push({key:"POSTHOG_API_KEY",comment:"# Overrides config.observability.posthog.publicKey for the server only (host stays in config)."}),a.length>0&&(o.push("","# Error and product reporting (optional)"),Pt(a,o)),o.push("","# AI base (optional)","# Enable config.ai in packages/config. Built-in AI resolves through OpenRouter only,","# so OPENROUTER_API_KEY above is the only MODEL provider key the backend reads.","# BYOK is per-user and never an env var: users paste their own key in the app and it","# is stored encrypted in the database.","# Override the OpenRouter API base - a gateway or a proxy of your own. It must carry the","# /api/v1 suffix. Unset uses the hosted API.","#OPENROUTER_BASE_URL=https://openrouter.ai/api/v1","","# AI web tools (optional)","# Give the assistant its own web access. A key's presence is the whole switch: set one","# and the model gets its tools, set neither and chat is unchanged. Users still opt in","# per chat with the composer's web-search toggle, and config.ai.webSearch: false","# disables both. Calls bill to user credits - set the price and engine in","# config.ai.webTools.","# ANY one key alone covers both capabilities. Firecrawl is the recommended one, so a","# single signup is the whole setup, and with several keys set Firecrawl serves search","# and extract.","# Firecrawl (recommended) - `web_search` AND `web_extract` (reads one page).","# https://firecrawl.dev/","#FIRECRAWL_API_KEY=","# Parallel (optional) - both tools, cheaper on each, at high quality; extract is slower","# on a page it has not already indexed. Note its customer terms include a license to","# train on submitted queries. Set config.ai.webTools.searchEngine or .extractEngine to",'# "parallel" to use it alongside a Firecrawl key.',"# https://platform.parallel.ai/","#PARALLEL_API_KEY=","# TinyFish (optional) - both tools at a $0 list price, rate-limited instead (30 searches","# and 150 pages a minute per API key by default, shared by every user), so a call bills","# the user nothing unless you set a price in config.ai.webTools. Note its terms include","# a license to train on submitted content. Set config.ai.webTools.searchEngine or",'# .extractEngine to "tinyfish" to use it alongside another key.',"# https://agent.tinyfish.ai/","#TINYFISH_API_KEY="),e.desktop&&o.push("","# Desktop app (optional)","# Base URL the marketing download page reads the release feed from.","# Unset uses config.desktop.autoUpdate.url.","#DESKTOP_FEED_BASE_URL="),e.mobile&&(o.push("","# Mobile app","# The bundle identifier the native Sign in with Apple token is issued to - it joins","# APPLE_CLIENT_ID in the audience the backend accepts.","#APPLE_APP_BUNDLE_IDENTIFIER=","# Comma-separated Android signing certificate SHA-256 fingerprints. They feed","# .well-known/assetlinks.json (which verifies your universal links) and the passkey","# Android origins.","#MOBILE_ANDROID_SHA256_FINGERPRINTS=","# Optional. An Expo access token turns on enhanced security for push sends.","#EXPO_ACCESS_TOKEN="),e.mobilePurchases==="revenuecat"&&o.push("","# Mobile store purchases - RevenueCat (server)","# The first two verify incoming webhooks; the third is read by the sync route and the","# nightly reconcile pass.","#REVENUECAT_WEBHOOK_SECRET=","#REVENUECAT_AUTH_HEADER=","#REVENUECAT_API_KEY=")),o.push("","# Build output (optional)","# Next's distDir for apps/web. Unset uses .next; the e2e harness sets its own so a test","# build never overwrites a dev server's.","#NEXT_DIST_DIR="),o.push(""),o.join(`
781
- `)}var Wn="NEXT_PUBLIC_API_URL";function ra(e){let{apiUrl:t}=Nr(e),r=["# API Configuration",`${Wn}=${t}`];return e.architecture==="separate"&&r.push("","# Production (uncomment and replace with your deployed hostnames):",`# ${Wn}=${ta}`),r.push(""),r.join(`
782
- `)}function na(e){let{apiUrl:t,baseUrl:r}=Nr(e),n=["# Expo reads env from THIS directory, never the monorepo root.","# Every EXPO_PUBLIC_* value is inlined into the app bundle - never put a secret here.","# Android emulator: replace localhost with 10.0.2.2; a physical device needs your machine's LAN IP","# EXPO_PUBLIC_API_URL is the value to fill first - without it the app throws at launch and names it.",`EXPO_PUBLIC_API_URL=${t}`,`EXPO_PUBLIC_WEB_URL=${r}`,"","# Native Google sign-in (optional)","#EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID=","#EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID="];e.mobilePurchases==="revenuecat"&&n.push("","# RevenueCat public SDK keys (optional until you create your products)","#EXPO_PUBLIC_REVENUECAT_IOS_KEY=","#EXPO_PUBLIC_REVENUECAT_ANDROID_KEY=");let i=[];return e.observability.includes("sentry")&&i.push("#EXPO_PUBLIC_SENTRY_DSN="),e.observability.includes("posthog")&&i.push("#EXPO_PUBLIC_POSTHOG_KEY="),i.length>0&&n.push("","# Observability (optional)",...i),n.push(""),n.join(`
783
- `)}async function zn(e){let t=ra(e);if(await c(`${e.projectDir}/.env`,`${t}
784
- ${Yn(e,"env")}`),await c(`${e.projectDir}/.env.example`,`${t}
785
- ${Yn(e,"example")}`),e.mobile){let r=na(e);await c(`${e.projectDir}/apps/mobile/.env`,r),await c(`${e.projectDir}/apps/mobile/.env.example`,r)}}import{join as ia}from"path";async function qn(e){let t=tt(e),r=[],n=[];if(t.includes("postgres")&&(r.push(` postgres:
775
+ `;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 yr(e){return Array.from(crypto.getRandomValues(new Uint8Array(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}function br(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}=br(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",yr(32),t),o.push("","# Job Queue - Inngest","INNGEST_APP_ID=api"),$t(o,"INNGEST_EVENT_KEY",yr(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",yr(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. A key's presence is the whole switch: set one","# and the model gets its tools, set neither and chat is unchanged. Users still opt in","# per chat with the composer's web-search toggle, and config.ai.webSearch: false","# disables both. Calls bill to user credits - set the price and engine in","# config.ai.webTools.","# ANY one key alone covers both capabilities. Firecrawl is the recommended one, so a","# single signup is the whole setup, and with several keys set Firecrawl serves search","# and extract.","# Firecrawl (recommended) - `web_search` AND `web_extract` (reads one page).","# https://firecrawl.dev/","#FIRECRAWL_API_KEY=","# Parallel (optional) - both tools, cheaper on each, at high quality; extract is slower","# on a page it has not already indexed. Note its customer terms include a license to","# train on submitted queries. Set config.ai.webTools.searchEngine or .extractEngine to",'# "parallel" to use it alongside a Firecrawl key.',"# https://platform.parallel.ai/","#PARALLEL_API_KEY=","# TinyFish (optional) - both tools at a $0 list price, rate-limited instead (30 searches","# and 150 pages a minute per API key by default, shared by every user), so a call bills","# the user nothing unless you set a price in config.ai.webTools. Note its terms include","# a license to train on submitted content. Set config.ai.webTools.searchEngine or",'# .extractEngine to "tinyfish" to use it alongside another key.',"# https://agent.tinyfish.ai/","#TINYFISH_API_KEY="),e.desktop&&o.push("","# Desktop app (optional)","# Base URL the marketing download page reads the release feed from.","# Unset uses config.desktop.autoUpdate.url.","#DESKTOP_FEED_BASE_URL="),e.mobile&&(o.push("","# Mobile app","# The bundle identifier the native Sign in with Apple token is issued to - it joins","# APPLE_CLIENT_ID in the audience the backend accepts.","#APPLE_APP_BUNDLE_IDENTIFIER=","# Comma-separated Android signing certificate SHA-256 fingerprints. They feed","# .well-known/assetlinks.json (which verifies your universal links) and the passkey","# Android origins.","#MOBILE_ANDROID_SHA256_FINGERPRINTS=","# Optional. An Expo access token turns on enhanced security for push sends.","#EXPO_ACCESS_TOKEN="),e.mobilePurchases==="revenuecat"&&o.push("","# Mobile store purchases - RevenueCat (server)","# The first two verify incoming webhooks; the third is read by the sync route and the","# nightly reconcile pass.","#REVENUECAT_WEBHOOK_SECRET=","#REVENUECAT_AUTH_HEADER=","#REVENUECAT_API_KEY=")),o.push("","# Build output (optional)","# Next's distDir for apps/web. Unset uses .next; the e2e harness sets its own so a test","# build never overwrites a dev server's.","#NEXT_DIST_DIR="),o.push(""),o.join(`
776
+ `)}var _n="NEXT_PUBLIC_API_URL";function Lo(e){let{apiUrl:t}=br(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(`
777
+ `)}function $o(e){let{apiUrl:t,baseUrl:r}=br(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(`
778
+ `)}async function On(e){let t=Lo(e);if(await l(`${e.projectDir}/.env`,`${t}
779
+ ${Rn(e,"env")}`),await l(`${e.projectDir}/.env.example`,`${t}
780
+ ${Rn(e,"example")}`),e.mobile){let r=$o(e);await l(`${e.projectDir}/apps/mobile/.env`,r),await l(`${e.projectDir}/apps/mobile/.env.example`,r)}}import{join as jo}from"path";async function xn(e){let t=Xe(e),r=[],n=[];if(t.includes("postgres")&&(r.push(` postgres:
786
781
  image: postgres:18-alpine
787
782
  restart: unless-stopped
788
783
  ports:
@@ -844,22 +839,22 @@ ${r.join(`
844
839
  volumes:
845
840
  ${n.join(`
846
841
  `)}
847
- `),await c(ia(e.projectDir,"infra/docker-compose.yml"),o)}import{join as sa}from"path";var At="Next.js";function oa(e){let t=[];if(t.push({type:"node-terminal",request:"launch",name:"Dev",command:"pnpm dev",cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),e.architecture==="separate"&&t.push({type:"node-terminal",request:"launch",name:"Backend",command:"pnpm dev",cwd:"${workspaceFolder}/apps/backend",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),t.push({type:"node-terminal",request:"launch",name:At,command:"pnpm dev",cwd:"${workspaceFolder}/apps/web",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),e.docs&&t.push({type:"node-terminal",request:"launch",name:"Docs",command:"pnpm dev",cwd:"${workspaceFolder}/apps/docs",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),e.desktop){let r=e.architecture==="separate"?"http://localhost:3010":"http://localhost:3000/api";t.push({type:"node-terminal",request:"launch",name:"Desktop",command:"pnpm dev",cwd:"${workspaceFolder}/apps/desktop",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development",BASE_URL:"http://localhost:3000",PUBLIC_API_URL:r}})}if(t.push({type:"node-terminal",request:"launch",name:"Inngest",command:"pnpm dev:inngest",cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),t.push({type:"node-terminal",request:"launch",name:"Mail",command:"pnpm dev:mail",cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),e.paymentProvider==="stripe"){let r=e.architecture==="separate"?"localhost:3010/auth/stripe/webhook":"localhost:3000/api/auth/stripe/webhook";t.push({type:"node-terminal",request:"launch",name:"Stripe",command:`stripe listen --forward-to ${r}`,cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"]})}return t}function aa(e){let t=[];return e.architecture==="separate"?t.push({name:`Dev (${At} + Backend + Inngest)`,configurations:["Backend",At,"Inngest"]}):t.push({name:`Dev (${At} + Inngest)`,configurations:[At,"Inngest"]}),t}async function Jn(e){let t={version:"0.2.0",configurations:oa(e),compounds:aa(e)};await c(sa(e.projectDir,".vscode/launch.json"),`${JSON.stringify(t,null," ")}
848
- `)}import{readFile as Zn}from"fs/promises";import{join as Qn}from"path";var Cr='import { drizzle } from "drizzle-orm/node-postgres";',Lr=`export const db = drizzle(parsed.data.DATABASE_URL, { schema });
849
- export const pool = db.$client;`,ca={postgres:{importLine:Cr,instantiation:Lr},neon:{importLine:`import { Pool } from "@neondatabase/serverless";
842
+ `),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," ")}
843
+ `)}import{readFile as Cn}from"fs/promises";import{join as Ln}from"path";var vr='import { drizzle } from "drizzle-orm/node-postgres";',wr=`export const db = drizzle(parsed.data.DATABASE_URL, { schema });
844
+ export const pool = db.$client;`,Vo={postgres:{importLine:vr,instantiation:wr},neon:{importLine:`import { Pool } from "@neondatabase/serverless";
850
845
  import { drizzle } from "drizzle-orm/neon-serverless";`,instantiation:`const pool = new Pool({ connectionString: parsed.data.DATABASE_URL });
851
846
  export const db = drizzle(pool, { schema });`},supabase:{importLine:`import { drizzle } from "drizzle-orm/postgres-js";
852
847
  import postgres from "postgres";`,instantiation:`const client = postgres(parsed.data.DATABASE_URL);
853
- export const db = drizzle(client, { schema });`}};async function ei(e){let t=Qn(e.projectDir,"packages/database/src/index.ts"),r=await Zn(t,"utf-8");if(!r.includes(Cr)||!r.includes(Lr))throw new Error("generateDbDriver: boilerplate packages/database/src/index.ts is missing the node-postgres driver anchors (the import line or the `db` instantiation block); the boilerplate drifted.");let n=ca[e.databaseProvider],i=r.replace(Cr,n.importLine).replace(Lr,n.instantiation);await c(t,i),await pa(e)}var Xn='import { Client } from "pg";',$r=` const client = new Client({ connectionString });
848
+ 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(vr)||!r.includes(wr))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(vr,n.importLine).replace(wr,n.instantiation);await l(t,i),await Go(e)}var Nn='import { Client } from "pg";',kr=` const client = new Client({ connectionString });
854
849
  await client.connect();
855
850
  return {
856
851
  query: async (text) => ({ rows: (await client.query<LiveColumnRow>(text)).rows }),
857
852
  end: () => client.end()
858
- };`,la={postgres:null,neon:{importLine:'import { Client } from "@neondatabase/serverless";',open:$r},supabase:{importLine:'import postgres from "postgres";',open:` const sql = postgres(connectionString, { prepare: false });
853
+ };`,Bo={postgres:null,neon:{importLine:'import { Client } from "@neondatabase/serverless";',open:kr},supabase:{importLine:'import postgres from "postgres";',open:` const sql = postgres(connectionString, { prepare: false });
859
854
  return {
860
855
  query: async (text) => ({ rows: await sql.unsafe(text) }),
861
856
  end: () => sql.end()
862
- };`}};async function pa(e){let t=la[e.databaseProvider];if(!t)return;let r=Qn(e.projectDir,"packages/database/src/db-verify.ts"),n=await Zn(r,"utf-8");if(!n.includes(Xn)||!n.includes($r))throw new Error("generateDbDriver: boilerplate packages/database/src/db-verify.ts is missing the node-postgres verifier anchors (the import line or the openVerifyClient body); the boilerplate drifted.");await c(r,n.replace(Xn,t.importLine).replace($r,t.open))}import{join as ti}from"path";async function ri(e){e.cacheProvider==="upstash"&&(await da(e),await ua(e))}async function da(e){await c(ti(e.projectDir,"packages/runtime/src/redis.ts"),`import { Redis } from "@upstash/redis";
857
+ };`}};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(kr))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(kr,t.open))}import{join as jn}from"path";async function Mn(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";
863
858
  import type { Store } from "hono-rate-limiter";
864
859
  import { env } from "./env";
865
860
 
@@ -1029,7 +1024,7 @@ export function createLimiterStore(): Store {
1029
1024
 
1030
1025
  /** No persistent connection to close with Upstash REST. */
1031
1026
  export async function closeRedis(): Promise<void> {}
1032
- `)}async function ua(e){await c(ti(e.projectDir,"packages/runtime/src/mutex.ts"),`import { Lock } from "@upstash/lock";
1027
+ `)}async function Ho(e){await l(jn(e.projectDir,"packages/runtime/src/mutex.ts"),`import { Lock } from "@upstash/lock";
1033
1028
  import { redis } from "./redis";
1034
1029
 
1035
1030
  export class MutexTimeoutError extends Error {
@@ -1107,23 +1102,18 @@ export async function withMutex<T>(
1107
1102
  });
1108
1103
  }
1109
1104
  }
1110
- `)}import{readFile as ma}from"fs/promises";import{join as ha}from"path";var ni=" REDIS_URL: z.string(),",fa=` UPSTASH_REDIS_REST_URL: z.string(),
1111
- UPSTASH_REDIS_REST_TOKEN: z.string(),`;async function ii(e){let t=ha(e.projectDir,"packages/runtime/src/env.ts"),r=await ma(t,"utf-8");if(!r.includes(ni))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(ni,fa):r;await c(t,n)}import{readFile as li}from"fs/promises";import{join as V}from"path";import{readFile as jr,rm as ka}from"fs/promises";import{join as Ge}from"path";import{readFile as Rt,rm as ga}from"fs/promises";import{join as It}from"path";function ya(e){return new RegExp(`^[ \\t]*(?:\\/\\/|\\{\\/\\*|\\*) gsaas:${e}-start(?: \\*\\/\\})?[ \\t]*\\r?\\n[\\s\\S]*?^[ \\t]*(?:\\/\\/|\\{\\/\\*|\\*) gsaas:${e}-end(?: \\*\\/\\})?[ \\t]*\\r?\\n`,"gm")}function K(e,t,r,n){let i=ya(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,`
1112
-
1113
- `).replace(/,(\r?\n[ \t]*[\]})])/g,"$1")}async function k(e,t,r,n,i){let o=await Rt(e,"utf-8"),s=o.indexOf(t);if(s===-1)throw new Error(`${i}: expected to find the ${n} seam, but it was missing (boilerplate drift).`);if(o.includes(t,s+t.length))throw new Error(`${i}: the ${n} seam appears more than once (boilerplate drift).`);await c(e,o.slice(0,s)+r+o.slice(s+t.length))}async function Se(e,t,r,n){let i=/\n([\t ]+)"/.exec(await Rt(e,"utf-8"))?.[1];if(i!==r){let o=s=>s===" "?"tab":`${s.length}-space`;throw new Error(`${n}: expected ${e} to be ${o(r)}-indented, but it is ${i===void 0?"not indented":o(i)}-indented (boilerplate drift). Re-serializing it with the wrong indent would reformat the whole file.`)}await c(e,`${JSON.stringify(t,null,r)}
1114
- `)}async function ba(e,t,r){let n=It(e,"ai.mdx"),o=(await Rt(n,"utf-8")).split(`
1115
- `).find(s=>s.includes(`href="${t}"`));if(o===void 0)throw new Error(`${r}: expected the ai.mdx card linking to ${t} (boilerplate drift).`);await k(n,`${o}
1116
- `,"",`ai.mdx ${t} card`,r)}async function wa(e,t,r){let n=It(e,"ai.mdx"),i=`| **${t}** |`,s=(await Rt(n,"utf-8")).split(`
1117
- `).find(a=>a.startsWith(i));if(s===void 0)throw new Error(`${r}: expected the ai.mdx "${t}" hub-table row (boilerplate drift).`);await k(n,`${s}
1118
- `,"",`ai.mdx ${t} hub-table row`,r)}async function si(e,t,r,n,i){let o=It(e,"docs/(platform)");await ga(It(o,t),{recursive:!0}),await k(It(o,"meta.json"),` "${t}",
1119
- `,"",`docs meta.json ${t} nav entry`,n);for(let s of r)await ba(o,s,n);i!==void 0&&await wa(o,i,n)}function va(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]!==`
1105
+ `)}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(),
1106
+ 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 Sr,rm as Xo}from"fs/promises";import{join as Ue}from"path";import{readFile as Er}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,`
1107
+
1108
+ `).replace(/,(\r?\n[ \t]*[\]})])/g,"$1")}async function T(e,t,r,n,i){let o=await Er(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 Er(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)}
1109
+ `)}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]!==`
1120
1110
  `;)r++;continue}if(n==="/"&&i==="*"){for(r+=2;r<e.length&&!(e[r]==="*"&&e[r+1]==="/");)e[r]===`
1121
1111
  `&&(t+=`
1122
1112
  `),r++;r+=2;continue}if(n==='"'||n==="'"||n==="`"){let o=n;for(r++;r<e.length&&e[r]!==o;)e[r]==="\\"?r++:e[r]===`
1123
1113
  `&&(t+=`
1124
- `),r++;r++;continue}t+=n,r++}return t}async function qt(e){let t=`import { config } from "@repo/config";
1125
- `,r=await Rt(e,"utf-8");if(!r.includes(t))return;let n=r.replace(t,""),i=va(n);/\bconfig\b/.test(i)||await c(e,n)}var Ke="stripMobileObservability",oi=["apps/mobile/lib/observability.ts","apps/mobile/types/env.d.ts","apps/mobile/tests/settings-privacy-screen.test.tsx"],ai="apps/mobile/tests/observability.test.ts",Ea="mobile-obs-any",Sa=["apps/mobile/tests/settings-privacy-screen.test.tsx"],Jt={sentry:["@sentry/react-native","@sentry/cli"],posthog:["posthog-react-native"]},Pa=`,
1126
- ...sentryExpoPlugin()`,Aa=["apps/mobile/app.config.ts"],Ia="|@sentry/react-native",Ra=` * DUAL-PROVIDER, the way \`packages/observability\` is. This repo carries the Sentry and the PostHog
1114
+ `),r++;r++;continue}t+=n,r++}return t}async function Vn(e){let t=`import { config } from "@repo/config";
1115
+ `,r=await Er(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=`,
1116
+ ...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
1127
1117
  * halves at once so dev and the test suite exercise both. A generated project carries only the
1128
1118
  * providers its \`observability\` manifest option names: the CLI removes the other one's marked region
1129
1119
  * from this file, its SDK from \`apps/mobile/package.json\` and, for Sentry, its Expo config plugin
@@ -1134,7 +1124,7 @@ export async function withMutex<T>(
1134
1124
  * cross-provider \`{@link}\` and every paragraph about one provider's credentials sits INSIDE that
1135
1125
  * provider's own region, and what is left between them is provider-neutral - the {@link PROVIDERS}
1136
1126
  * list and the two exported entry points that walk it, which is what a project generated with
1137
- * \`observability: []\` keeps and starts nothing with.`,Ta='import { readPrivacySettings, type PrivacySettings } from "@/lib/privacy";',_a='import type { PrivacySettings } from "@/lib/privacy";';function Oa(e){let t=e.map(n=>n==="sentry"?"Sentry":"PostHog").join(" and ");return`${e.length===0?` * Your project was generated with observability OFF, so this app received no reporting adapter
1127
+ * \`observability: []\` keeps and starts nothing with.`,ia='import { readPrivacySettings, type PrivacySettings } from "@/lib/privacy";',sa='import type { PrivacySettings } from "@/lib/privacy";';function oa(e){let t=e.map(n=>n==="sentry"?"Sentry":"PostHog").join(" and ");return`${e.length===0?` * Your project was generated with observability OFF, so this app received no reporting adapter
1138
1128
  * and no reporting SDK: {@link PROVIDERS} is empty, and both functions below are no-ops that load
1139
1129
  * nothing and send nothing.`:` * This app reports to ${t}. No other provider's start, SDK or Expo config plugin was
1140
1130
  * generated, so there is nothing here for Metro to fold away and nothing for pnpm to install.`}
@@ -1142,9 +1132,9 @@ export async function withMutex<T>(
1142
1132
  * To change providers, edit \`observability\` in .generatesaas/manifest.json and run
1143
1133
  * \`generatesaas update\`: re-shaping brings that provider's half of this file, its SDK and its env
1144
1134
  * key back. Pasting the other provider's credential into \`config.observability\` on its own cannot -
1145
- * the start it would need is not here.`}async function ci(e){if(!e.mobile)return;let t=e.projectDir,r=["sentry","posthog"].filter(o=>!e.observability.includes(o));e.observability.length===0&&await ka(Ge(t,ai),{force:!0});let n=e.observability.length===0?oi:[...oi,ai];for(let o of r)for(let s of n){let a=Ge(t,s);await c(a,K(await jr(a,"utf-8"),s,`mobile-obs-${o}`,Ke))}if(e.observability.length===0)for(let o of Sa){let s=Ge(t,o);await c(s,K(await jr(s,"utf-8"),o,Ea,Ke))}if(r.includes("sentry")){for(let o of Aa){let s=Ge(t,o);await c(s,K(await jr(s,"utf-8"),o,"mobile-obs-sentry",Ke))}await k(Ge(t,"apps/mobile/app.config.ts"),Pa,"","app.config.ts Sentry Expo plugin",Ke),await k(Ge(t,"apps/mobile/jest.config.js"),Ia,"","jest.config.js Sentry transform allowance",Ke)}let i=Ge(t,"apps/mobile/lib/observability.ts");await k(i,Ra,Oa(e.observability),"lib/observability.ts dual-provider note",Ke),e.observability.length===0&&(await k(i,Ta,_a,"lib/observability.ts privacy import",Ke),await qt(i))}var Xt={"@upstash/redis":"^1.37.0","@upstash/lock":"^0.2.1","@neondatabase/serverless":"^1.0.1",postgres:"^3.4.7"};async function ae(e){let t=await li(e,"utf-8");return JSON.parse(t)}async function ce(e,t){let r=/\n([\t ]+)"/.exec(await li(e,"utf-8"))?.[1]??" ";await c(e,`${JSON.stringify(t,null,r)}
1146
- `)}function ee(e,t){for(let r of t)delete e.dependencies?.[r],delete e.devDependencies?.[r]}function xa(e){e.dependencies&&Object.keys(e.dependencies).length===0&&delete e.dependencies,e.devDependencies&&Object.keys(e.devDependencies).length===0&&delete e.devDependencies}function Tt(e,t,r,n=!1){let i=n?"devDependencies":"dependencies";e[i]||(e[i]={}),e[i][t]=r}async function pi(e){await Na(e),await Ca(e),await La(e),await Ma(e),await Fa(e),await Ka(e),await Da(e)}async function Da(e){if(!e.mobile){let t=V(e.projectDir,"packages/auth/package.json"),r=await ae(t);ee(r,["@better-auth/expo"]),await ce(t,r);let n=V(e.projectDir,"tooling/styles/package.json"),i=await ae(n);ee(i,["tsx"]),xa(i),await ce(n,i)}if(e.mobile&&e.mobilePurchases!=="revenuecat"){let t=V(e.projectDir,"apps/mobile/package.json"),r=await ae(t);ee(r,["react-native-purchases","react-native-purchases-ui"]),await ce(t,r)}}async function Na(e){let t=V(e.projectDir,"packages/api/package.json"),r=await ae(t);ee(r,["sharp","@types/sharp"]),e.cacheProvider==="upstash"&&ee(r,["ioredis"]),await ce(t,r)}async function Ca(e){let t=V(e.projectDir,"apps/web/package.json"),r=await ae(t);ee(r,["@upstash/ratelimit","@upstash/redis"]),await ce(t,r)}async function La(e){let t=V(e.projectDir,"packages/runtime/package.json"),r=await ae(t);e.cacheProvider==="upstash"&&(ee(r,["ioredis","rate-limit-redis","redis-semaphore"]),Tt(r,"@upstash/redis",Xt["@upstash/redis"]),Tt(r,"@upstash/lock",Xt["@upstash/lock"])),await ce(t,r)}var Ur={sentry:["@sentry/node","@sentry/browser","@sentry/electron"],posthog:["posthog-node","posthog-js"]},$a=["@sentry/electron"],ja=[...Ur.sentry,...Ur.posthog],Ua=[...Jt.sentry,...Jt.posthog];async function Ma(e){let t=V(e.projectDir,"packages/observability/package.json"),r=await ae(t),n=new Set(e.observability.flatMap(a=>Ur[a]));if(!e.desktop)for(let a of $a)n.delete(a);if(ee(r,ja.filter(a=>!n.has(a))),await ce(t,r),!e.mobile)return;let i=V(e.projectDir,"apps/mobile/package.json"),o=await ae(i),s=new Set(e.observability.flatMap(a=>Jt[a]));ee(o,Ua.filter(a=>!s.has(a))),await ce(i,o)}async function Fa(e){let t=V(e.projectDir,"packages/database/package.json"),r=await ae(t);e.databaseProvider==="neon"?(ee(r,["pg","@types/pg"]),Tt(r,"@neondatabase/serverless",Xt["@neondatabase/serverless"])):e.databaseProvider==="supabase"&&(ee(r,["pg","@types/pg"]),Tt(r,"postgres",Xt.postgres)),await ce(t,r)}var Va=["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 ae(e),r=t.dependencies?.["@repo/api"];r&&(delete t.dependencies?.["@repo/api"],Tt(t,"@repo/api",r,!0),await ce(e,t))}var Ga=["instrumentation.ts","tests/instrumentation.test.ts"];async function Ka(e){if(e.architecture!=="separate")return;let t=V(e.projectDir,"apps/web");await lt(V(t,"app/api/[[...rest]]/route.ts"),t);for(let r of Va)await lt(V(t,r),t);for(let r of Ga)await lt(V(t,r),t);await Ba(V(t,"package.json"))}import{readFile as Ha}from"fs/promises";import{join as Ya}from"path";var Wa=["base.json","node.json","next.json"],di="GenerateSaaS ";async function ui(e){if(!e.demo)for(let t of Wa){let r=Ya(e.projectDir,"tooling/typescript",t),n;try{n=await Ha(r,"utf-8")}catch{continue}let i=JSON.parse(n);typeof i.display!="string"||!i.display.startsWith(di)||(i.display=i.display.slice(di.length),await c(r,`${JSON.stringify(i,null," ")}
1147
- `))}}import{readFile as hi,rm as za}from"fs/promises";import{join as _t}from"path";async function fi(e){e.cacheProvider==="upstash"&&(await Promise.all([mi(_t(e.projectDir,"packages/runtime/tests/setup.ts")),mi(_t(e.projectDir,"packages/mail/tests/setup.ts")),Ja(e.projectDir),za(_t(e.projectDir,"packages/runtime/tests/redis.test.ts"),{force:!0})]),await qa(e.projectDir))}async function qa(e){await c(_t(e,"packages/runtime/tests/redis.test.ts"),`import { beforeEach, describe, expect, it, vi } from "vitest";
1135
+ * 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(Ue(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=Ue(t,s);await l(a,je(await Sr(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 Sr(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 Sr(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)}
1136
+ `)}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 Pr={sentry:["@sentry/node","@sentry/browser","@sentry/electron"],posthog:["posthog-node","posthog-js"]},ua=["@sentry/electron"],ma=[...Pr.sentry,...Pr.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=>Pr[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," ")}
1137
+ `))}}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";
1148
1138
 
1149
1139
  /**
1150
1140
  * Regression guards for the Upstash-backed cache layer in src/redis.ts.
@@ -1266,9 +1256,9 @@ describe("cacheScan", () => {
1266
1256
  expect(seen.sort()).toEqual(["scan:1", "scan:2", "scan:3"]);
1267
1257
  });
1268
1258
  });
1269
- `)}async function mi(e){let t;try{t=await hi(e,"utf-8")}catch{throw new Error(`pruneCacheTests: expected ${e} for the Upstash cache swap (boilerplate drift).`)}let r=t.replace(/\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1259
+ `)}async function qn(e){let t;try{t=await Jn(e,"utf-8")}catch{throw new Error(`pruneCacheTests: expected ${e} for the Upstash cache swap (boilerplate drift).`)}let r=t.replace(/\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1270
1260
  UPSTASH_REDIS_REST_TOKEN: "test-token",
1271
- `);if(r===t){if(t.includes("UPSTASH_REDIS_REST_URL"))return;throw new Error(`pruneCacheTests: found no REDIS_URL default to swap in ${e} (boilerplate drift).`)}await c(e,r)}async function Ja(e){let t=_t(e,"packages/api/tests/setup.ts"),r;try{r=await hi(t,"utf-8")}catch{throw new Error(`pruneCacheTests: expected ${t} for the Upstash cache swap (boilerplate drift).`)}let n=r;if(n=n.replace(/\tREDIS_URL:\s*"[^"]*",?\n/g,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1261
+ `);if(r===t){if(t.includes("UPSTASH_REDIS_REST_URL"))return;throw new Error(`pruneCacheTests: found no REDIS_URL default to swap in ${e} (boilerplate drift).`)}await l(e,r)}async function Ia(e){let t=wt(e,"packages/api/tests/setup.ts"),r;try{r=await Jn(t,"utf-8")}catch{throw new Error(`pruneCacheTests: expected ${t} for the Upstash cache swap (boilerplate drift).`)}let n=r;if(n=n.replace(/\tREDIS_URL:\s*"[^"]*",?\n/g,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1272
1262
  UPSTASH_REDIS_REST_TOKEN: "test-token",
1273
1263
  `),n=n.replace(/vi\.mock\("ioredis"[\s\S]*?\n\}\);\n\n?/,""),n=n.replace(/vi\.mock\("rate-limit-redis"[\s\S]*?\n\}\);\n\n?/,""),n=n.replace(/\t\t\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1274
1264
  UPSTASH_REDIS_REST_TOKEN: "test-token",
@@ -1297,36 +1287,32 @@ vi.mock("@upstash/lock", () => {
1297
1287
  return { Lock };
1298
1288
  });
1299
1289
 
1300
- $1`),n===r){if(r.includes("UPSTASH_REDIS_REST_URL"))return;throw new Error(`pruneCacheTests: nothing to rewrite in ${t} for the Upstash swap (boilerplate drift).`)}for(let o of['vi.mock("ioredis"','vi.mock("rate-limit-redis"',"REDIS_URL:"])if(n.includes(o))throw new Error(`pruneCacheTests: ${t} still contains ${o} after the Upstash swap (boilerplate drift).`);await c(t,n)}import{readFile as Xa}from"fs/promises";var Za=/^[ \t]*(?:\/\/|\{\/\*|\*) gsaas:[a-z0-9-]+-(?:start|end)(?: \*\/\})?[ \t]*\r?\n/gm,Qa=new Set([".ts",".tsx",".vue",".mjs",".js",".jsx",".md",".mdx"]),gi=32;function ec(e){let t=e.lastIndexOf(".");return t>=0&&Qa.has(e.slice(t))}async function tc(e){let t=await Xa(e,"utf-8");if(!t.includes("gsaas:"))return;let r=t.replace(Za,"");r!==t&&await c(e,r)}async function yi(e){let t=await Ee(e.projectDir,e.projectDir,r=>r==="node_modules"||r.startsWith("node_modules/")||r===".git"||r.startsWith(".git/"),ec);for(let r=0;r<t.length;r+=gi)await Promise.all(t.slice(r,r+gi).map(tc))}import{readdir as Qt,readFile as le,rm as E}from"fs/promises";import{join as p}from"path";import{existsSync as rc}from"fs";import{readFile as Mr,rm as T}from"fs/promises";import{join as A}from"path";import{readFile as Ot}from"fs/promises";import{join as xt}from"path";async function bi(e,t){let r=xt(e,"packages/api/src/routes/internal/index.ts");await c(r,K(await Ot(r,"utf-8"),"internal router","runner",t)),await qt(r);let n=xt(e,"packages/api/src/ai/automation-runner.ts");await c(n,K(await Ot(n,"utf-8"),"automation-runner.ts","runner",t));for(let s of["packages/api/src/index.ts","packages/api/src/routes/internal/ai/chat-router.ts","packages/api/src/routes/internal/ai/automations-router.ts","packages/api/src/ai/byok.ts","packages/api/tests/ai/automations.test.ts","packages/api/tests/ai/automations-runs-route.test.ts","packages/api/tests/routes/ai.test.ts","packages/api/tests/ai/summarize-cap.test.ts"]){let a=xt(e,s);await c(a,K(await Ot(a,"utf-8"),s,"runner",t))}let i=xt(e,"packages/api/src/ai/settings-store.ts");await c(i,K(await Ot(i,"utf-8"),"settings-store.ts","runner",t));let o=xt(e,"packages/api/tests/ai/settings-store.test.ts");await c(o,K(await Ot(o,"utf-8"),"settings-store.test.ts","runner",t))}async function Fr(e){if(e.runner)return;let t=e.projectDir,r="stripRunner";await T(A(t,"packages/api/src/relay"),{recursive:!0}),await T(A(t,"packages/api/src/runner"),{recursive:!0}),await T(A(t,"packages/api/src/ai/runner-model.ts")),await T(A(t,"packages/api/src/ai/runner-routing.ts")),await T(A(t,"packages/api/tests/relay"),{recursive:!0}),await T(A(t,"packages/api/tests/runner"),{recursive:!0}),await T(A(t,"packages/api/src/routes/internal/runner.ts")),await T(A(t,"packages/api/src/routes/internal/runner-transport.ts")),await T(A(t,"packages/api/src/routes/internal/runner-limits.ts")),await T(A(t,"packages/api/tests/routes/runner.test.ts")),await T(A(t,"packages/api/tests/routes/runner-transport-harness.ts")),await T(A(t,"packages/api/tests/routes/runner-transport-connect.test.ts")),await T(A(t,"packages/api/tests/routes/runner-transport-events.test.ts")),await T(A(t,"packages/api/tests/ai/automation-coalesce.test.ts")),await T(A(t,"packages/api/tests/ai/automations-lazy-expiry.test.ts")),await T(A(t,"packages/api/tests/ai/runner-model.test.ts")),await T(A(t,"packages/api/tests/unit/runner-offline-routing.test.ts")),await T(A(t,"packages/api/tests/unit/runner-offline-interactive.test.ts")),await T(A(t,"packages/api/tests/unit/runner-unavailable-routing.test.ts")),await T(A(t,"packages/api/tests/unit/runner-unavailable-interactive.test.ts")),await T(A(t,"packages/api/tests/unit/runner-arm-harness.ts")),await T(A(t,"packages/api/tests/ai/runner-chat.test.ts")),await T(A(t,"packages/api/tests/helpers/runner-protocol.ts")),await T(A(t,"packages/api/tests/helpers/runner-frames.ts")),await T(A(t,"packages/api/tests/helpers/relay-runtime-mock.ts"));let n=A(t,"packages/api/package.json"),i=JSON.parse(await Mr(n,"utf-8"));if(!i.exports?.["./runner"])throw new Error(`${r}: expected packages/api/package.json to carry the ./runner export`);delete i.exports["./runner"],await Se(n,i," ",r),await bi(t,r),await si(t,"runner",["/runner"],r,"Runners"),await k(A(t,"docs/(platform)/ai.mdx"),"Runs on a paired [runner](/runner) CLI are never metered either - the user's own subscription executes them. ","","ai.mdx runner metering sentence",r),await k(A(t,"docs/(platform)/caching.mdx")," - see [per-runner request budgets](/runner/install#per-runner-request-budgets)","","caching.mdx runner budgets link",r);let o=A(t,"docs/(platform)/configuration.mdx"),s="| `runner.enabled` |",a=(await Mr(o,"utf-8")).split(`
1301
- `).find(u=>u.startsWith(s));if(a===void 0)throw new Error(`${r}: expected the configuration.mdx \`runner.enabled\` row (boilerplate drift).`);await k(o,`${a}
1302
- `,"","configuration.mdx runner config-table row",r);let d=A(t,"docs/desktop/ai/index.mdx");if(rc(d)){let u="| **Runners** | Settings |",w=(await Mr(d,"utf-8")).split(`
1303
- `).find(y=>y.startsWith(u));if(w===void 0)throw new Error(`${r}: expected the desktop AI index \`Runners\` row (boilerplate drift).`);await k(d,`${w}
1304
- `,"","desktop AI index Runners row",r)}}var nc=new Set(["ci.yml","desktop-release.yml","mobile-release.yml"]),ic=["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"],sc=["packages/auth/src/config.ts","packages/api/src/routes/internal/index.ts","packages/api/src/index.ts"],oc=["packages/api/src/routes/internal/index.ts","packages/api/src/routes/inngest.ts","packages/notifications/src/index.ts","packages/config/src/cache.ts"],ac=["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"],cc=["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"],lc=["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"],pc=["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"],dc=[{dir:"apps/mobile/components/billing",prefixes:["paywall","credit-packs","store-"]},{dir:"apps/mobile/tests",prefixes:["purchases-"]}],uc=["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"],mc=[","," {",' 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(`
1305
- `),hc=[["## 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(`
1290
+ $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(`
1291
+ `),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(`
1306
1292
  `),["#### 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(`
1307
1293
  `),["#### 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(`
1308
- `)],fc=["apps/web/app/[locale]/(marketing)/download","apps/web/tests/routes/download-page.test.tsx"],gc=["cli","cli:clean","demo:bench","playground:regen","playground:test","playground:test:repo","playground:test:audit","playground:test:units","playground:test:e2e","playground:test:build","playground:test:mobile","check-types:scripts","ai:parity","runner:export","runner:verify","runner:publish"];async function wi(e){let t=p(e.projectDir,".github/workflows"),r=await Qt(t).catch(()=>[]);for(let n of r)nc.has(n)||await E(p(t,n),{recursive:!0,force:!0})}async function yc(e,t){let r=p(e,"package.json"),n=JSON.parse(await le(r,"utf-8"));if(!n.scripts)return;let i=!1;for(let s of t)s in n.scripts&&(delete n.scripts[s],i=!0);if(!i)return;let o=/\n([\t ]+)"/.exec(await le(r,"utf-8"))?.[1]??" ";await c(r,`${JSON.stringify(n,null,o)}
1309
- `)}async function vi(e){await yc(e.projectDir,[...gc])}async function ki(e){let t=p(e.projectDir,"package.json"),r=await le(t,"utf-8"),n=JSON.parse(r);!n.scripts||!("dev"in n.scripts)||(n.scripts.dev=e.architecture==="separate"?"turbo run dev --continue":"turbo run dev --continue --filter=!backend",await c(t,`${JSON.stringify(n,null," ")}
1310
- `))}async function Ei(e){let t=p(e.projectDir,"package.json"),r=JSON.parse(await le(t,"utf-8")),n=r.scripts?.test;if(n!=="turbo run test --concurrency=4 && vitest run")throw new Error(`rewriteRootTestScript: expected the root test script to be "turbo run test --concurrency=4 && vitest run", got ${JSON.stringify(n)} (boilerplate drift).`);r.scripts.test="turbo run test --concurrency=4",await c(t,`${JSON.stringify(r,null," ")}
1311
- `)}async function Si(e){e.docs||await E(p(e.projectDir,"apps/docs"),{recursive:!0})}async function Pi(e){let t=p(e.projectDir,"docs/(platform)/meta.json"),r=JSON.parse(await le(t,"utf-8"));if(r.title!=="GenerateSaaS")throw new Error(`stripUnusedDocsSilo: expected the feature-page group to be titled "GenerateSaaS", found ${JSON.stringify(r.title)} (boilerplate drift).`);r.title=e.appName,await Se(t,r," ","stripUnusedDocsSilo"),e.desktop||await E(p(e.projectDir,"docs/desktop"),{recursive:!0,force:!0}),e.mobile||await E(p(e.projectDir,"docs/mobile"),{recursive:!0,force:!0}),await bc(e),await kc(e)}async function H(e,t,r,n){await c(e,K(await le(e,"utf-8"),t,r,n))}async function bc(e){let t=e.projectDir,r="stripUnusedDocsSilo",n=p(t,"docs/(platform)/configuration.mdx"),i=e.desktop,o=e.desktop&&e.ai;e.desktop||(await k(n,"| Desktop app + device sign-in - see [desktop app](/desktop) |","| Desktop app + device sign-in |","configuration.mdx desktop config-table link",r),await H(n,"configuration.mdx desktop TRUSTED_ORIGINS callout","desktop-origins-docs",r),await k(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 H(p(t,"docs/(platform)/payments/organizations.mdx"),"payments organizations.mdx desktop tenancy callout","desktop-tenancy-docs",r),await H(p(t,"docs/(platform)/projects.mdx"),"projects.mdx desktop scoping callout","desktop-projects-docs",r),await H(p(t,"docs/(platform)/client-architecture.mdx"),"client-architecture.mdx desktop card","desktop-architecture-docs",r),await H(p(t,"docs/(platform)/screens.mdx"),"screens.mdx desktop halves","screens-desktop-docs",r),await H(p(t,"docs/(platform)/testing/e2e.mdx"),"testing/e2e.mdx desktop e2e callout","desktop-testing-docs",r),await k(p(t,"docs/(platform)/index.mdx"),` <Card href="/desktop" title="Desktop app" description="Cross-platform desktop app with device sign-in and AI agents." />
1312
- `,"","index.mdx desktop app card",r),await H(p(t,"docs/(platform)/ai.mdx"),"ai.mdx automation lanes row + section","desktop-lanes-docs",r)),i||(await H(n,"configuration.mdx desktop-AI paragraph","desktop-ai-docs",r),await k(p(t,"docs/(platform)/navigation.mdx"),"gates the desktop app's Chat and Automations, see [desktop AI](/desktop/ai) |","gates the desktop app's Chat and Automations |","navigation.mdx sidebar-flag agents-row link",r)),!o&&e.runner&&(await H(p(t,"docs/(platform)/runner/index.mdx"),"runner index.mdx desktop-setup section","desktop-setup-docs",r),await H(p(t,"docs/(platform)/runner/terminal.mdx"),"runner terminal.mdx desktop-panel section","desktop-terminal-docs",r))}async function Ai(e){if(e.desktop)return;await E(p(e.projectDir,"apps/desktop"),{recursive:!0});let t=p(e.projectDir,"packages/i18n/translations");for(let i of await Qt(t,{withFileTypes:!0}))i.isDirectory()&&await E(p(t,i.name,"desktop.json"),{force:!0});await E(p(e.projectDir,".github/workflows/desktop-release.yml"));for(let i of fc)await E(p(e.projectDir,i),{recursive:!0});await E(p(e.projectDir,"packages/utils/src/desktop-download.ts")),await E(p(e.projectDir,"packages/utils/tests/desktop-download.test.ts"));let r=p(e.projectDir,"packages/utils/package.json"),n=JSON.parse(await le(r,"utf-8"));if(!n.exports?.["./desktop-download"])throw new Error("stripDesktopApp: expected packages/utils/package.json to carry the ./desktop-download export");delete n.exports["./desktop-download"],await Se(r,n," ","stripDesktopApp")}function wc(e){return e.desktop}async function Ii(e){wc(e)||await E(p(e.projectDir,"packages/agent-core"),{recursive:!0})}async function vc(e,t,r,n){let i=await Qt(e);for(let o of t){let s=i.filter(a=>a.startsWith(o));if(s.length===0)throw new Error(`${n}: expected ${r} to hold at least one ${o}* entry`);for(let a of s)await E(p(e,a),{recursive:!0})}}async function Zt(e,t,r,n){await c(e,K(await le(e,"utf-8"),t,r,n))}async function kc(e){let t=e.projectDir,r="stripUnusedDocsSilo",n=e.mobile&&e.mobilePurchases==="revenuecat";e.mobile||(await k(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." />
1313
- `,"","index.mdx mobile app card",r),await k(p(t,"docs/(platform)/configuration.mdx")," - see [mobile app](/mobile)","","configuration.mdx mobile config-table link",r),await k(p(t,"docs/(platform)/client-architecture.mdx"),` <Card href="/mobile" title="Mobile app">The Expo app - the third surface this client core serves.</Card>
1314
- `,"","client-architecture.mdx mobile app card",r),await k(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 k(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 k(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 k(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 k(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 H(p(t,"docs/(platform)/testing/e2e.mdx"),"testing e2e.mdx mobile Maestro section","mobile-testing-docs",r),await H(p(t,"docs/(platform)/environment-variables/index.mdx"),"environment-variables mobile section","mobile-env-docs",r)),n||(await k(p(t,"docs/(platform)/payments/meta.json"),', "mobile-billing"',"","payments meta.json mobile-billing entry",r),await k(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." />
1315
- `,"","payments index.mdx mobile store billing card",r),await E(p(t,"docs/(platform)/payments/mobile-billing.mdx"))),e.mobile&&!n&&(await k(p(t,"docs/mobile/meta.json"),', "billing"',"","mobile meta.json billing entry",r),await k(p(t,"docs/mobile/index.mdx"),` <Card href="/mobile/billing" title="Billing" description="Store purchases through RevenueCat, and the web-billing shape." />
1316
- `,"","mobile index.mdx billing card",r),await H(p(t,"docs/mobile/index.mdx"),"mobile index.mdx store-purchases block","mobile-purchases-docs",r),await E(p(t,"docs/mobile/billing"),{recursive:!0,force:!0}))}async function Ri(e){if(e.mobile)return;await E(p(e.projectDir,"apps/mobile"),{recursive:!0});let t=p(e.projectDir,"packages/i18n/translations");for(let i of await Qt(t,{withFileTypes:!0}))i.isDirectory()&&await E(p(t,i.name,"mobile.json"),{force:!0});await E(p(e.projectDir,"packages/i18n/tests/mobile-notification-labels.test.ts")),await E(p(e.projectDir,"packages/i18n/tests/mobile-brand-literals.test.ts")),await E(p(e.projectDir,"packages/i18n/tests/mobile-shared-merge.test.ts")),await E(p(e.projectDir,"packages/auth/tests/expo-plugin.test.ts")),await E(p(e.projectDir,"packages/auth/tests/expo-authorization-allowlist.test.ts")),await E(p(e.projectDir,"packages/auth/src/expo-authorization-allowlist.ts")),await E(p(e.projectDir,".github/workflows/mobile-release.yml")),await E(p(e.projectDir,"tooling/styles/scripts/build-mobile-css.ts"));let r=p(e.projectDir,"tooling/styles/package.json"),n=JSON.parse(await le(r,"utf-8"));if(!n.scripts?.["mobile-css"])throw new Error("stripMobileApp: expected tooling/styles/package.json to carry the mobile-css script");delete n.scripts["mobile-css"],Object.keys(n.scripts).length===0&&delete n.scripts,await Se(r,n," ","stripMobileApp");for(let i of ic)await lt(p(e.projectDir,i),p(e.projectDir,"apps/web"));await E(p(e.projectDir,"packages/api/src/routes/internal/well-known.ts")),await E(p(e.projectDir,"packages/api/tests/routes/well-known-mobile.test.ts")),await E(p(e.projectDir,"packages/api/src/routes/internal/mobile-auth.ts")),await E(p(e.projectDir,"packages/api/tests/routes/mobile-auth.test.ts")),await E(p(e.projectDir,"apps/web/public/images/stores"),{recursive:!0});for(let i of sc)await Zt(p(e.projectDir,i),i,"mobile-auth","stripMobileApp");for(let i of oc)await Zt(p(e.projectDir,i),i,"mobile-push","stripMobileApp");for(let i of ac)await E(p(e.projectDir,i),{recursive:!0})}async function Ti(e){if(e.mobile&&e.mobilePurchases==="revenuecat")return;for(let n of cc)await E(p(e.projectDir,n),{recursive:!0});let t=p(e.projectDir,"packages/payments/package.json"),r=JSON.parse(await le(t,"utf-8"));if(!r.sideEffects?.includes("./src/store/revenuecat/index.ts"))throw new Error("stripMobilePurchases: expected packages/payments/package.json to list the store lane in sideEffects");r.sideEffects=r.sideEffects.filter(n=>n!=="./src/store/revenuecat/index.ts"),await Se(t,r," ","stripMobilePurchases");for(let n of lc)await Zt(p(e.projectDir,n),n,"mobile-purchases","stripMobilePurchases");if(e.mobile){for(let n of pc)await E(p(e.projectDir,n),{recursive:!0});for(let{dir:n,prefixes:i}of dc)await vc(p(e.projectDir,n),i,n,"stripMobilePurchases");for(let n of uc)await Zt(p(e.projectDir,n),n,"mobile-purchases","stripMobilePurchases");for(let n of hc)await k(p(e.projectDir,"apps/mobile/e2e/README.md"),n,"","e2e README purchases row block","stripMobilePurchases");await k(p(e.projectDir,"apps/mobile/e2e/tags.ts"),mc,`
1317
- `,"e2e tags.ts purchases inventory entries","stripMobilePurchases")}}import{readFile as Ec}from"fs/promises";import{join as Sc}from"path";var _i=`on:
1294
+ `)],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)}
1295
+ `)}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," ")}
1296
+ `))}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," ")}
1297
+ `)}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." />
1298
+ `,"","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 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." />
1299
+ `,"","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>
1300
+ `,"","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." />
1301
+ `,"","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." />
1302
+ `,"","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 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,`
1303
+ `,"e2e tags.ts purchases inventory entries","stripMobilePurchases")}}import{readFile as Xa}from"fs/promises";import{join as Za}from"path";var pi=`on:
1318
1304
  workflow_run:
1319
1305
  workflows: ["Tag Release"]
1320
1306
  branches: [main]
1321
1307
  types: [completed]
1322
- workflow_dispatch:`,Pc=`on:
1323
- workflow_dispatch:`;async function Oi(e){if(!e.desktop)return;let t=Sc(e.projectDir,".github/workflows/desktop-release.yml"),r=await Ec(t,"utf8");if(!r.includes(_i))throw new Error(`Cannot configure the desktop release trigger: the expected workflow_run block was not found in ${t}. The boilerplate workflow may have drifted.`);await c(t,r.replace(_i,Pc))}import{readFile as Ac}from"fs/promises";import{join as Ic}from"path";var xi=`on:
1308
+ workflow_dispatch:`,Qa=`on:
1309
+ workflow_dispatch:`;async function di(e){if(!e.desktop)return;let t=Za(e.projectDir,".github/workflows/desktop-release.yml"),r=await Xa(t,"utf8");if(!r.includes(pi))throw new Error(`Cannot configure the desktop release trigger: the expected workflow_run block was not found in ${t}. The boilerplate workflow may have drifted.`);await l(t,r.replace(pi,Qa))}import{readFile as ec}from"fs/promises";import{join as tc}from"path";var ui=`on:
1324
1310
  workflow_run:
1325
1311
  workflows: ["Tag Release"]
1326
1312
  branches: [main]
1327
1313
  types: [completed]
1328
- workflow_dispatch:`,Rc=`on:
1329
- workflow_dispatch:`;async function Di(e){if(!e.mobile)return;let t=Ic(e.projectDir,".github/workflows/mobile-release.yml"),r=await Ac(t,"utf8");if(!r.includes(xi))throw new Error(`Cannot configure the mobile release trigger: the expected workflow_run block was not found in ${t}. The boilerplate workflow may have drifted.`);await c(t,r.replace(xi,Rc))}import{join as Tc}from"path";async function Li(e){let t=_c(e);await c(Tc(e.projectDir,".github/workflows/ci.yml"),t)}function Ni(e){return e.databaseProvider!=="neon"&&e.cacheProvider!=="upstash"}function Ci(e,t,r){return` - uses: actions/upload-artifact@v4
1314
+ workflow_dispatch:`,rc=`on:
1315
+ workflow_dispatch:`;async function mi(e){if(!e.mobile)return;let t=tc(e.projectDir,".github/workflows/mobile-release.yml"),r=await ec(t,"utf8");if(!r.includes(ui))throw new Error(`Cannot configure the mobile release trigger: the expected workflow_run block was not found in ${t}. The boilerplate workflow may have drifted.`);await l(t,r.replace(ui,rc))}import{join as nc}from"path";async function gi(e){let t=ic(e);await l(nc(e.projectDir,".github/workflows/ci.yml"),t)}function hi(e){return e.databaseProvider!=="neon"&&e.cacheProvider!=="upstash"}function fi(e,t,r){return` - uses: actions/upload-artifact@v4
1330
1316
  if: \${{ ${r} }}
1331
1317
  with:
1332
1318
  name: playwright-report-${e}
@@ -1335,7 +1321,7 @@ $1`),n===r){if(r.includes("UPSTASH_REDIS_REST_URL"))return;throw new Error(`prun
1335
1321
  ${t}/test-results
1336
1322
  if-no-files-found: ignore
1337
1323
  retention-days: 7
1338
- `}function _c(e){let t=e.cacheProvider==="upstash",r=t?"":` redis:
1324
+ `}function ic(e){let t=e.cacheProvider==="upstash",r=t?"":` redis:
1339
1325
  image: redis:8-alpine
1340
1326
  ports:
1341
1327
  - 6379:6379
@@ -1379,7 +1365,7 @@ ${r} mailpit:
1379
1365
  # suite's port is derived per checkout - a baked URL would name a port nothing serves.
1380
1366
  - name: Start Inngest
1381
1367
  run: docker run -d --name inngest --network host inngest/inngest:v1.17.4 inngest dev
1382
- `,d=Ni(e)?`
1368
+ `,d=hi(e)?`
1383
1369
  # The end-to-end suite, on a manual "Run workflow" dispatch only - it builds the app in production
1384
1370
  # mode and drives it with Playwright, too slow to pay for on every PR. It needs no keys and no
1385
1371
  # internet: the harness preflights the services below, builds and starts the app itself, provisions
@@ -1414,7 +1400,7 @@ ${a}
1414
1400
  env:
1415
1401
  NODE_OPTIONS: --max-old-space-size=6144
1416
1402
 
1417
- ${Ci("web","apps/web","!cancelled()")}`:"\n # No end-to-end job: this project's stack cannot run one in CI without credentials.\n # - a `neon` database talks over a serverless websocket that only reaches a managed endpoint,\n # so a Postgres service container is unreachable from the app;\n # - an `upstash` cache is a hosted REST endpoint, and this file carries no secrets.\n # The suite itself works locally: `pnpm infra` for a project on self-hosted Postgres and Redis,\n # then `pnpm e2e:ci`. To run it here, add a job with those services plus your own credentials as\n # repository secrets, or point a second CI environment at a disposable managed instance.\n",w=e.desktop&&Ni(e)?`
1403
+ ${fi("web","apps/web","!cancelled()")}`:"\n # No end-to-end job: this project's stack cannot run one in CI without credentials.\n # - a `neon` database talks over a serverless websocket that only reaches a managed endpoint,\n # so a Postgres service container is unreachable from the app;\n # - an `upstash` cache is a hosted REST endpoint, and this file carries no secrets.\n # The suite itself works locally: `pnpm infra` for a project on self-hosted Postgres and Redis,\n # then `pnpm e2e:ci`. To run it here, add a job with those services plus your own credentials as\n # repository secrets, or point a second CI environment at a disposable managed instance.\n",g=e.desktop&&hi(e)?`
1418
1404
  # The DESKTOP end-to-end suite, on a manual "Run workflow" dispatch only: it builds the Electron
1419
1405
  # bundles AND a Next production build, which is too slow to pay for on every push. It has its own
1420
1406
  # port and its own build directory, so it can never collide with the web job above.
@@ -1448,18 +1434,18 @@ ${a}
1448
1434
  env:
1449
1435
  NODE_OPTIONS: --max-old-space-size=6144
1450
1436
 
1451
- ${Ci("desktop","apps/desktop","failure()")}`:"",y=e.databaseProvider==="neon"?`
1437
+ ${fi("desktop","apps/desktop","failure()")}`:"",k=e.databaseProvider==="neon"?`
1452
1438
  # Required for neon: the api suite's embedded Postgres is TCP-only and the neon
1453
1439
  # websocket driver cannot reach it. Add this repository secret (any Postgres this
1454
1440
  # suite may CREATE DATABASE on - a Neon branch works).
1455
- GENERATESAAS_TEST_DATABASE_ADMIN_URL: \${{ secrets.GENERATESAAS_TEST_DATABASE_ADMIN_URL }}`:"",P=t?`
1441
+ GENERATESAAS_TEST_DATABASE_ADMIN_URL: \${{ secrets.GENERATESAAS_TEST_DATABASE_ADMIN_URL }}`:"",v=t?`
1456
1442
  UPSTASH_REDIS_REST_URL: https://test.upstash.io
1457
1443
  UPSTASH_REDIS_REST_TOKEN: test-token`:`
1458
- REDIS_URL: redis://localhost:6379`,I=(()=>{switch(e.paymentProvider){case"stripe":return`
1444
+ REDIS_URL: redis://localhost:6379`,P=(()=>{switch(e.paymentProvider){case"stripe":return`
1459
1445
  STRIPE_SECRET_KEY: test
1460
1446
  STRIPE_WEBHOOK_SECRET: test`;case"polar":return`
1461
1447
  POLAR_ACCESS_TOKEN: test
1462
- POLAR_WEBHOOK_SECRET: test`;case"none":return"";default:{let f=e.paymentProvider;throw new Error(`buildCiYaml: unhandled payment provider "${String(f)}"`)}}})();return`# CI policy - a push runs NOTHING here. Your host (Vercel, Coolify, etc.) auto-deploys the push;
1448
+ POLAR_WEBHOOK_SECRET: test`;case"none":return"";default:{let D=e.paymentProvider;throw new Error(`buildCiYaml: unhandled payment provider "${String(D)}"`)}}})();return`# CI policy - a push runs NOTHING here. Your host (Vercel, Coolify, etc.) auto-deploys the push;
1463
1449
  # CI is a quality gate, not on a deploy's critical path. A pull request runs the Checks job (lint,
1464
1450
  # type-check, unit tests). The FULL set - Checks plus the end-to-end jobs - runs only on a manual
1465
1451
  # dispatch: open the Actions tab, pick "CI", and press "Run workflow". Run that full set and confirm
@@ -1493,7 +1479,7 @@ jobs:
1493
1479
  # "cancelled", which reads as a mystery rather than as a timeout. 40 is roughly double that.
1494
1480
  timeout-minutes: 40
1495
1481
  ${n} env:
1496
- CONTENT_API_KEY: test-contentapi-key-16chars${y}${P}${I}
1482
+ CONTENT_API_KEY: test-contentapi-key-16chars${k}${v}${P}
1497
1483
  STORAGE_REGION: test
1498
1484
  STORAGE_ENDPOINT: http://test
1499
1485
  STORAGE_ACCESS_KEY_ID: test
@@ -1509,22 +1495,22 @@ ${n} env:
1509
1495
  - run: pnpm check-types
1510
1496
 
1511
1497
  - run: pnpm test
1512
- ${d}${w}`}import{readFile as $i}from"fs/promises";import{existsSync as Oc}from"fs";import{join as Vr}from"path";var ji="@repo/database";function xc(e){return e?"pnpm -F @repo/database reset && pnpm -F @repo/database run deploy -- --push":"pnpm -F @repo/database run deploy"}function Dc(e){switch(e){case"fullstack":return"web";case"separate":return"backend";default:{let t=e;throw new Error(`schemaOwnerApp: unhandled architecture "${String(t)}"`)}}}async function Nc(e,t,r){let n=await $i(e,"utf-8"),i=JSON.parse(n),o=i.scripts?.[t];if(!o)throw new Error(`Cannot prepend to missing script "${t}" in ${e}`);o.includes(ji)||(i.scripts={...i.scripts,[t]:`${r} && ${o}`},await c(e,`${JSON.stringify(i,null," ")}
1513
- `))}async function Cc(e,t){let r=Oc(e)?JSON.parse(await $i(e,"utf-8")):{$schema:"https://openapi.vercel.sh/vercel.json"},n=r.buildCommand?.trim()||"pnpm build";n.includes(ji)||(r.buildCommand=`${t} && ${n}`,await c(e,`${JSON.stringify(r,null," ")}
1514
- `))}async function Ui(e){let t=xc(e.demo===!0),r=Dc(e.architecture),n=Vr(e.projectDir,"apps",r);switch(e.deploymentTarget){case"node":await Nc(Vr(n,"package.json"),"start",t);return;case"vercel":await Cc(Vr(n,"vercel.json"),t);return;default:{let i=e.deploymentTarget;throw new Error(`generateDeployScripts: unhandled deployment target "${String(i)}"`)}}}import{readFile as Lc}from"fs/promises";import{existsSync as $c}from"fs";import{join as er}from"path";var Mi=["stripe","polar"],jc=[...Mi,"none"];async function Fi(e){let t=er(e.projectDir,"packages/payments/src"),r=e.paymentProvider,i=`// Active payment provider. To switch, change the path below to
1515
- // ${jc.filter(o=>o!==r).map(o=>`"./${o}/index"`).join(" or ")} (other folders are kept in place).
1498
+ ${d}${g}`}import{readFile as yi}from"fs/promises";import{existsSync as sc}from"fs";import{join as Ar}from"path";var bi="@repo/database";function oc(e){return e?"pnpm -F @repo/database reset && pnpm -F @repo/database run deploy -- --push":"pnpm -F @repo/database run deploy"}function ac(e){switch(e){case"fullstack":return"web";case"separate":return"backend";default:{let t=e;throw new Error(`schemaOwnerApp: unhandled architecture "${String(t)}"`)}}}async function cc(e,t,r){let n=await yi(e,"utf-8"),i=JSON.parse(n),o=i.scripts?.[t];if(!o)throw new Error(`Cannot prepend to missing script "${t}" in ${e}`);o.includes(bi)||(i.scripts={...i.scripts,[t]:`${r} && ${o}`},await l(e,`${JSON.stringify(i,null," ")}
1499
+ `))}async function lc(e,t){let r=sc(e)?JSON.parse(await yi(e,"utf-8")):{$schema:"https://openapi.vercel.sh/vercel.json"},n=r.buildCommand?.trim()||"pnpm build";n.includes(bi)||(r.buildCommand=`${t} && ${n}`,await l(e,`${JSON.stringify(r,null," ")}
1500
+ `))}async function vi(e){let t=oc(e.demo===!0),r=ac(e.architecture),n=Ar(e.projectDir,"apps",r);switch(e.deploymentTarget){case"node":await cc(Ar(n,"package.json"),"start",t);return;case"vercel":await lc(Ar(n,"vercel.json"),t);return;default:{let i=e.deploymentTarget;throw new Error(`generateDeployScripts: unhandled deployment target "${String(i)}"`)}}}import{readFile as pc}from"fs/promises";import{existsSync as dc}from"fs";import{join as Vt}from"path";var wi=["stripe","polar"],uc=[...wi,"none"];async function ki(e){let t=Vt(e.projectDir,"packages/payments/src"),r=e.paymentProvider,i=`// Active payment provider. To switch, change the path below to
1501
+ // ${uc.filter(o=>o!==r).map(o=>`"./${o}/index"`).join(" or ")} (other folders are kept in place).
1516
1502
  export { ops } from "./${r}/index";
1517
- `;await c(er(t,"providers/index.ts"),i),await c(er(t,"index.ts"),await Uc(t,r))}async function Uc(e,t){let r=er(e,"index.ts");if(!$c(r))throw new Error(`generatePaymentBarrel: expected ${r} to exist - did packages/payments move?`);let n=await Lc(r,"utf-8"),i=Mi.filter(s=>s!==t);return n.split(`
1503
+ `;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(`
1518
1504
  `).filter(s=>!i.some(a=>s.includes(`./providers/${a}/`))).join(`
1519
- `)}import{access as Mc,readFile as nr,rm as tr}from"fs/promises";import{join as te}from"path";var Hi=["sentry","posthog"],Br={sentry:"src/providers/sentry",posthog:"src/providers/posthog"},rr={sentry:["tests/sentry-load-guard.test.ts"],posthog:["tests/posthog-load-guard.test.ts","tests/llm-analytics.test.ts"]},Fc=["tests/resolution.test.ts","tests/export-isolation.test.ts"],Vc=["src/providers/sentry/desktop-main.ts","src/providers/sentry/desktop-renderer.ts","src/resolve-desktop-main.ts","src/resolve-desktop-renderer.ts"],Bc=["./desktop-main","./desktop-renderer"],Gc=[`vi.mock("@sentry/electron/main", sdk.factories["@sentry/electron/main"]);
1505
+ `)}import{access as hc,readFile as Kt,rm as Bt}from"fs/promises";import{join as q}from"path";var Ii=["sentry","posthog"],Ir={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"]);
1520
1506
  `,`vi.mock("@sentry/electron/renderer", sdk.factories["@sentry/electron/renderer"]);
1521
- `],Kc=[' describeGate("desktop main", "electronMain", async () => {',' describeGate("desktop renderer", "electronRenderer", async () => {',' describe("the desktop main init options", () => {',' describe("the desktop renderer init", () => {'],Vi=`
1507
+ `],vc=[' describeGate("desktop main", "electronMain", async () => {',' describeGate("desktop renderer", "electronRenderer", async () => {',' describe("the desktop main init options", () => {',' describe("the desktop renderer init", () => {'],Ei=`
1522
1508
  });
1523
- `,Hc=" ";async function Yi(e){let t=te(e.projectDir,"packages/observability");await Bi(t,"packages/observability");let r=e.observability,n=r.filter(s=>!Y.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: ${Y.join(", ")}. Shaping stops here so the project is not left half-stripped.`);for(let s of r)await Bi(te(t,Br[s]),`packages/observability/${Br[s]}`);let i=e.desktop?[...Ki,...jl]:Ki,o=new Map;for(let s of i)o.set(s.file,await nr(te(t,s.file),"utf-8"));for(let s of tl(r,e.desktop))await rl(t,s);for(let s of Hi)if(!r.includes(s)){await tr(te(t,Br[s]),{recursive:!0,force:!0});for(let a of rr[s])await tr(te(t,a),{force:!0})}for(let s of Fc)await tr(te(t,s),{force:!0});e.desktop||await Yc(t,r);for(let s of i){let a=o.get(s.file);a!==void 0&&await c(te(t,s.file),qc(a,s,r))}}async function Bi(e,t){try{await Mc(e)}catch{throw new Error(`generateObservabilityProvider: expected ${t} to exist - did the observability package move?`)}}async function Yc(e,t){for(let i of Vc)await tr(te(e,i),{force:!0});t.includes("sentry")&&await Wc(e);let r=te(e,"package.json"),n=JSON.parse(await nr(r,"utf-8"));for(let i of Bc){if(!n.exports?.[i])throw new Error(`generateObservabilityProvider: expected packages/observability/package.json to carry the ${i} export`);delete n.exports[i]}await Se(r,n," ","generateObservabilityProvider")}async function Wc(e){let t=te(e,rr.sentry[0]),r=await nr(t,"utf-8");r=Gr(r,Gc.map(n=>({find:n,replace:""})),rr.sentry[0]);for(let n of Kc)r=zc(r,n,rr.sentry[0]);await c(t,r)}function zc(e,t,r){let n=e.indexOf(t),i=n===-1?-1:e.indexOf(Vi,n);if(n===-1||i===-1)throw new Error(`generateObservabilityProvider: ${r} no longer contains the block this strip removes - re-point the anchor. Missing: ${t.trim()}`);if(e.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(`
1524
- `).find(a=>a.trim().length>0&&!a.startsWith(`${Hc} `));if(o!==void 0)throw new Error(`generateObservabilityProvider: ${r} no longer closes the block this strip removes at its own indent - re-point the anchor. Block: ${t.trim()}; first line inside it that is not part of its body: ${o.trim()}`);let s=i+Vi.length;return e[s]===`
1525
- `&&(s+=1),e.slice(0,n)+e.slice(s)}function qc(e,t,r){let n=Hi.filter(s=>!r.includes(s)),i=e,o=[];for(let s of n){let a=Jc(i,s);i=a.source,o.push(...a.bindings)}t.dualMode!==!1&&(i=Gr(i,[{find:Zc,replace:Qc(r)}],t.file)),i=Gr(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 Gr(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(`
1509
+ `,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,Ir[s]),`packages/observability/${Ir[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,Ir[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 Me(r,n," ","generateObservabilityProvider")}async function Ec(e){let t=q(e,Gt.sentry[0]),r=await Kt(t,"utf-8");r=Tr(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(`
1510
+ `).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]===`
1511
+ `&&(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=Tr(i,[{find:Tc,replace:Rc(r)}],t.file)),i=Tr(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 Tr(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(`
1526
1512
  `)[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(`
1527
- `)[0]}`);n=i.all===!0?n.replaceAll(i.find,i.replace):n.replace(i.find,i.replace)}return n}function Jc(e,t){let r=new RegExp(String.raw`^import[^;]*?from "\./providers/${t}/[^"]*";\n`,"gm"),n=[];return{source:e.replace(r,o=>(n.push(...Xc(o)),"")),bindings:n}}function Xc(e){let t=/^import\s+(?:type\s+)?(\S(?:[\s\S]*?\S)?)\s+from\s/.exec(e)?.[1]??"",r=[],n=/\{([\s\S]*?)\}/.exec(t);n?.[1]&&r.push(...n[1].split(",").map(s=>s.trim().replace(/^type\s+/,"")).map(s=>s.split(/\s+as\s+/).at(-1)??"").filter(s=>s.length>0));let i=t.replace(/\{[\s\S]*?\}/,"").replace(/,/g," ").trim(),o=/^\*\s+as\s+([A-Za-z_$][\w$]*)$/.exec(i);return o?.[1]?r.push(o[1]):/^[A-Z_$][\w$]*$/i.test(i)&&r.push(i),r}var Zc=" * DUAL-MODE, exactly as `packages/payments/src/providers/index.ts` does it. This repo resolves the\n * provider at RUNTIME so dev and the test suite exercise every one of them. A generated project has\n * exactly one provider, so the CLI overwrites this file with a direct re-export of it and the rest\n * become unreachable and are pruned; the generator is the observability twin of\n * `packages/cli/src/generators/payment-barrel.ts` and lands with the manifest option.";function Qc(e){let t=e.map(n=>n==="sentry"?"Sentry":"PostHog").join(" and ");return`${e.length===0?` * Your project was generated with observability OFF, so no provider adapter and no reporting
1513
+ `)[0]}`);n=i.all===!0?n.replaceAll(i.find,i.replace):n.replace(i.find,i.replace)}return n}function Ac(e,t){let r=new RegExp(String.raw`^import[^;]*?from "\./providers/${t}/[^"]*";\n`,"gm"),n=[];return{source:e.replace(r,o=>(n.push(...Ic(o)),"")),bindings:n}}function Ic(e){let t=/^import\s+(?:type\s+)?(\S(?:[\s\S]*?\S)?)\s+from\s/.exec(e)?.[1]??"",r=[],n=/\{([\s\S]*?)\}/.exec(t);n?.[1]&&r.push(...n[1].split(",").map(s=>s.trim().replace(/^type\s+/,"")).map(s=>s.split(/\s+as\s+/).at(-1)??"").filter(s=>s.length>0));let i=t.replace(/\{[\s\S]*?\}/,"").replace(/,/g," ").trim(),o=/^\*\s+as\s+([A-Za-z_$][\w$]*)$/.exec(i);return o?.[1]?r.push(o[1]):/^[A-Z_$][\w$]*$/i.test(i)&&r.push(i),r}var Tc=" * DUAL-MODE, exactly as `packages/payments/src/providers/index.ts` does it. This repo resolves the\n * provider at RUNTIME so dev and the test suite exercise every one of them. A generated project has\n * exactly one provider, so the CLI overwrites this file with a direct re-export of it and the rest\n * become unreachable and are pruned; the generator is the observability twin of\n * `packages/cli/src/generators/payment-barrel.ts` and lands with the manifest option.";function Rc(e){let t=e.map(n=>n==="sentry"?"Sentry":"PostHog").join(" and ");return`${e.length===0?` * Your project was generated with observability OFF, so no provider adapter and no reporting
1528
1514
  * SDK were shipped: this file is the whole layer, and every call is a no-op that loads nothing.`:` * Your project reports to ${t}. No other provider's adapter or SDK was generated, so
1529
1515
  * there is nothing here for a bundler to fold away and nothing for pnpm to install.`}
1530
1516
  *
@@ -1532,28 +1518,28 @@ export { ops } from "./${r}/index";
1532
1518
  * run \`generatesaas update\`: re-shaping brings that provider's adapter and its dependency back. The
1533
1519
  * update stages nothing while you are already on the latest version, so the switch lands with the
1534
1520
  * next release. Pasting the other provider's credential into \`config.observability\` on its own
1535
- * cannot: the adapter it would need is not here.`}var Gi={source:"src/providers/sentry/browser.ts",name:"SentryBrowserInitOptions",fields:["environment: string;"]},el={source:"src/providers/sentry/desktop-main.ts",name:"SentryDesktopMainInitOptions",fields:["environment: string;","appIsReady: boolean;"]};function tl(e,t){return e.includes("sentry")?[]:t?[Gi,el]:[Gi]}async function rl(e,t){let r=await nr(te(e,t.source),"utf-8"),n=`export interface ${t.name} {
1521
+ * 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} {
1536
1522
  `,i=r.indexOf(n),o=r.indexOf(`
1537
1523
  }
1538
1524
  `,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
- `).map(a=>a.trim()).filter(a=>/^[A-Z_$][\w$]*\??: .+;$/i.test(a));if(s.join(" ")!==t.fields.join(" "))throw new Error(`generateObservabilityProvider: ${t.name} now declares [${s.join(" ")}] but the stand-in emitted for a project without Sentry declares [${t.fields.join(" ")}] - update the stand-in so switching providers on an update cannot break a buyer's call site.`)}var nl=" into every browser bundle. `tests/export-isolation.test.ts` walks the real module graph to keep\n * that from ever landing quietly.",il=` if (resolveSentryDsn(observability, process.env.SENTRY_DSN)) ops.push(sentryServerOps);
1540
- `,sl=` if (resolvePosthogCredentials(observability, process.env.POSTHOG_API_KEY)) {
1525
+ `).map(a=>a.trim()).filter(a=>/^[A-Z_$][\w$]*\??: .+;$/i.test(a));if(s.join(" ")!==t.fields.join(" "))throw new Error(`generateObservabilityProvider: ${t.name} now declares [${s.join(" ")}] but the stand-in emitted for a project without Sentry declares [${t.fields.join(" ")}] - update the stand-in so switching providers on an update cannot break a buyer's call site.`)}var Dc=" into every browser bundle. `tests/export-isolation.test.ts` walks the real module graph to keep\n * that from ever landing quietly.",Nc=` if (resolveSentryDsn(observability, process.env.SENTRY_DSN)) ops.push(sentryServerOps);
1526
+ `,Cc=` if (resolvePosthogCredentials(observability, process.env.POSTHOG_API_KEY)) {
1541
1527
  ops.push(posthogServerOps);
1542
1528
  }
1543
- `,ol=` if (resolveSentryDsn(config.observability, process.env.SENTRY_DSN)) {
1529
+ `,Lc=` if (resolveSentryDsn(config.observability, process.env.SENTRY_DSN)) {
1544
1530
  await initSentryServer();
1545
1531
  }
1546
- `,al=` if (observability?.sentry?.dsn) ops.push(sentryBrowserOps);
1547
- `,cl=` if (observability?.posthog?.publicKey) ops.push(posthogBrowserOps);
1548
- `,ll=` if (config.observability?.sentry?.dsn) await initSentryBrowser(options);
1549
- `,pl=` if (observability?.posthog?.publicKey && !observability?.sentry?.dsn) {
1532
+ `,$c=` if (observability?.sentry?.dsn) ops.push(sentryBrowserOps);
1533
+ `,jc=` if (observability?.posthog?.publicKey) ops.push(posthogBrowserOps);
1534
+ `,Mc=` if (config.observability?.sentry?.dsn) await initSentryBrowser(options);
1535
+ `,Uc=` if (observability?.posthog?.publicKey && !observability?.sentry?.dsn) {
1550
1536
  announceMissingDesktopSdk("posthog");
1551
1537
  }
1552
- `,dl=` // PostHog ships no desktop SDK, so a project configured ONLY for PostHog reports nothing here -
1538
+ `,Fc=` // PostHog ships no desktop SDK, so a project configured ONLY for PostHog reports nothing here -
1553
1539
  // announced rather than silent, because a desktop build that quietly drops every error looks
1554
1540
  // identical to one with reporting off.
1555
- `,ul=`import { createProviderGapAnnouncer } from "./announce";
1556
- `,ml=`/**
1541
+ `,Vc=`import { createProviderGapAnnouncer } from "./announce";
1542
+ `,Bc=`/**
1557
1543
  * Say once that the selected provider cannot report from the desktop main process.
1558
1544
  *
1559
1545
  * A WARNING and never an error: PostHog ships no Electron SDK, and a buyer running PostHog on the
@@ -1571,7 +1557,7 @@ const announceMissingDesktopSdk = createProviderGapAnnouncer({
1571
1557
  remedy: "Select sentry to report errors from the desktop app."
1572
1558
  });
1573
1559
 
1574
- `,hl=`/**
1560
+ `,Gc=`/**
1575
1561
  * Say once that the selected provider cannot report from the desktop renderer.
1576
1562
  *
1577
1563
  * A WARNING and never an error: PostHog ships no Electron SDK, and a buyer running PostHog on the
@@ -1585,118 +1571,114 @@ const announceMissingDesktopSdk = createProviderGapAnnouncer({
1585
1571
  remedy: "Select sentry to report errors from the desktop app."
1586
1572
  });
1587
1573
 
1588
- `,fl=` const observability = config.observability;
1589
- `,gl=`import { config } from "@repo/config";
1590
- `,yl=` // No error-reporting adapter was generated for this project, so there is nothing to arm.
1591
- `,bl="export type BrowserReportingOptions = SentryBrowserInitOptions;",wl=`export interface BrowserReportingOptions {
1574
+ `,Kc=` const observability = config.observability;
1575
+ `,Hc=`import { config } from "@repo/config";
1576
+ `,Yc=` // No error-reporting adapter was generated for this project, so there is nothing to arm.
1577
+ `,Wc="export type BrowserReportingOptions = SentryBrowserInitOptions;",zc=`export interface BrowserReportingOptions {
1592
1578
  /** The deployment environment reports are tagged with. */
1593
1579
  environment: string;
1594
- }`,vl=` return observability?.sentry?.dsn ? [sentryDesktopMainOps] : [];
1595
- `,kl=` if (observability?.sentry?.dsn) await initSentryDesktopMain(options);
1596
- `,El="export type DesktopMainReportingOptions = SentryDesktopMainInitOptions;",Sl=`export interface DesktopMainReportingOptions {
1580
+ }`,qc=` return observability?.sentry?.dsn ? [sentryDesktopMainOps] : [];
1581
+ `,Jc=` if (observability?.sentry?.dsn) await initSentryDesktopMain(options);
1582
+ `,Xc="export type DesktopMainReportingOptions = SentryDesktopMainInitOptions;",Zc=`export interface DesktopMainReportingOptions {
1597
1583
  /** The deployment environment reports are tagged with. */
1598
1584
  environment: string;
1599
1585
  /** Whether Electron's \`app\` has emitted \`ready\` yet. */
1600
1586
  appIsReady: boolean;
1601
- }`,Pl=` return observability?.sentry?.dsn ? [sentryDesktopRendererOps] : [];
1602
- `,Al=` if (observability?.sentry?.dsn) await initSentryDesktopRenderer();
1603
- `;function Wi(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 Il(e){let t=[{find:nl,replace:" into every browser bundle."},...Wi(e,{sentry:il,posthog:sl})];return e.includes("sentry")||t.push({find:ol,replace:""}),t}function Rl(e){let t=[...Wi(e,{sentry:al,posthog:cl})];return e.includes("sentry")||t.push({find:ll,replace:""},Tl),t}var Tl={find:bl,replace:wl};function _l(e){let t=[...zi(e,ml)];return e.includes("sentry")||t.push({find:vl,replace:` return [];
1604
- `},{find:kl,replace:Ji(e)},{find:El,replace:Sl}),t.push(...qi(e)),t}function Ol(e){let t=[...zi(e,hl)];return e.includes("sentry")||t.push({find:Pl,replace:` return [];
1605
- `},{find:Al,replace:Ji(e)}),t.push(...qi(e)),t}function zi(e,t){return e.includes("posthog")?[]:[{find:dl,replace:""},{find:pl,replace:"",all:!0},{find:t,replace:""},{find:ul,replace:""}]}function qi(e){return e.length>0?[]:[{find:fl,replace:"",all:!0},{find:gl,replace:""}]}function Ji(e){return e.length>0?"":yl}var xl=` if (!posthogConfigured()) return;
1587
+ }`,Qc=` return observability?.sentry?.dsn ? [sentryDesktopRendererOps] : [];
1588
+ `,el=` if (observability?.sentry?.dsn) await initSentryDesktopRenderer();
1589
+ `;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 [];
1590
+ `},{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 [];
1591
+ `},{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:Kc,replace:"",all:!0},{find:Hc,replace:""}]}function xi(e){return e.length>0?"":Yc}var ol=` if (!posthogConfigured()) return;
1606
1592
  try {
1607
1593
  const { captureAiGeneration: capture } = await import("./providers/posthog/llm");
1608
1594
  await capture(event);
1609
1595
  } catch (reportingError) {
1610
1596
  logReportingFailure("[observability] failed to report an AI generation", reportingError);
1611
1597
  }
1612
- `,Dl=` // No PostHog adapter was generated for this project, so there is nothing to report to.
1613
- `,Nl=` // No PostHog adapter was generated for this project, so there is nothing queued to drain.
1614
- `,Cl=`/** Whether PostHog is configured on this server, without touching the PostHog folder. */
1598
+ `,al=` // No PostHog adapter was generated for this project, so there is nothing to report to.
1599
+ `,cl=` // No PostHog adapter was generated for this project, so there is nothing queued to drain.
1600
+ `,ll=`/** Whether PostHog is configured on this server, without touching the PostHog folder. */
1615
1601
  function posthogConfigured(): boolean {
1616
1602
  return Boolean(process.env.POSTHOG_API_KEY || config.observability?.posthog?.publicKey);
1617
1603
  }
1618
1604
 
1619
- `,Ll=` if (!posthogConfigured()) return;
1605
+ `,pl=` if (!posthogConfigured()) return;
1620
1606
  try {
1621
1607
  const { flushAiGenerations: flush } = await import("./providers/posthog/llm");
1622
1608
  await flush();
1623
1609
  } catch (reportingError) {
1624
1610
  logReportingFailure("[observability] failed to flush queued AI generations", reportingError);
1625
1611
  }
1626
- `;function $l(e){return e.includes("posthog")?[]:[{find:xl,replace:Dl},{find:Ll,replace:Nl},{find:Cl,replace:""},{find:`import { logReportingFailure } from "./logger";
1612
+ `;function dl(e){return e.includes("posthog")?[]:[{find:ol,replace:al},{find:pl,replace:cl},{find:ll,replace:""},{find:`import { logReportingFailure } from "./logger";
1627
1613
  `,replace:""},{find:`import { config } from "@repo/config";
1628
- `,replace:""},{find:"export async function captureAiGeneration(event: AiGenerationEvent): Promise<void> {",replace:"export async function captureAiGeneration(_event: AiGenerationEvent): Promise<void> {"},{find:" * @param event - The call to record.",replace:" * @param _event - Ignored: no PostHog adapter was generated for this project."}]}var Ki=[{file:"src/resolve-server.ts",edits:Il},{file:"src/resolve-browser.ts",edits:Rl},{file:"src/llm-analytics.ts",edits:$l,dualMode:!1}],jl=[{file:"src/resolve-desktop-main.ts",edits:_l},{file:"src/resolve-desktop-renderer.ts",edits:Ol}];import{readdir as Ul,readFile as Ml}from"fs/promises";import{join as Xi}from"path";async function Zi(e){if(e.demo)return;let t=e.appName.trim()||e.projectName,r=JSON.stringify(t).slice(1,-1),n=Xi(e.projectDir,"packages/i18n/translations"),i;try{i=await Ul(n)}catch{return}for(let o of i){let s=Xi(n,o,"web.json"),a;try{a=await Ml(s,"utf-8")}catch{continue}let d=a.replaceAll("GenerateSaaS",()=>r);d!==a&&await c(s,d)}}async function ir(e){let t=e.projectDir;e.demo||(await Ln(t),await $n(t),await jn(t),await Un(t)),await Mn(t),await Vn(e),await Gn(e),await Kn(e),e.demo||await Hn(e),await zn(e),await qn(e),await Jn(e),await ei(e),await ri(e),await ii(e),await pi(e),await ui(e),await fi(e),await wi(e),await Li(e),await vi(e),await ki(e),await Ei(e),await Si(e),await Pi(e),await Ai(e),await Ii(e),await Oi(e),await Ri(e),await Ti(e),await Di(e),await Fr(e),await Ui(e),await Fi(e),await Yi(e),await ci(e),await Zi(e),await yi(e)}import{basename as es,join as ts,relative as Vl}from"path";import{createHash as Qi}from"crypto";import{readFile as Fl}from"fs/promises";async function sr(e){let t=await Fl(e);return Qi("sha256").update(t).digest("hex")}function Kr(e){return Qi("sha256").update(e).digest("hex")}var Bl=new Set(["data",Q]);function Gl(e){let t=e.split("/");for(let r of t)if(Sr.has(r)||Pr.has(r)||Bl.has(r)||r.startsWith(".env")&&!r.includes("example"))return!0;return!1}function rs(e,t){return{projectName:e.projectName??es(t),appName:e.appName??e.projectName??es(t),projectDir:t,architecture:e.architecture??"fullstack",paymentProvider:e.paymentProvider??"none",emailProvider:e.emailProvider??"smtp",multiTenancy:e.multiTenancy??!1,projects:e.projects??!1,docs:e.docs??!1,desktop:e.desktop??!1,mobile:e.mobile??!1,mobilePurchases:e.mobilePurchases??"none",ai:e.ai??!1,webAi:e.webAi??!0,runner:e.runner??!1,revenueSharing:e.revenueSharing??!1,credits:e.credits??!1,defaultCurrency:e.defaultCurrency??"USD",deploymentTarget:e.deploymentTarget??"node",databaseProvider:e.databaseProvider??"postgres",cacheProvider:e.cacheProvider??"redis",observability:e.observability??[],version:e.version,baseUrl:e.baseUrl,credentials:{},demo:!1}}function Kl(e,t){return{version:e.version,initialVersion:e.version,appName:e.appName,projectName:e.projectName,frontend:kn,architecture:e.architecture,paymentProvider:e.paymentProvider,emailProvider:e.emailProvider,multiTenancy:e.multiTenancy,projects:e.projects,docs:e.docs,desktop:e.desktop,mobile:e.mobile,...e.mobile&&oe(e)?{mobilePurchases:e.mobilePurchases}:{},ai:e.ai,webAi:e.webAi,runner:e.runner,credits:e.credits,revenueSharing:e.revenueSharing,defaultCurrency:e.defaultCurrency,deploymentTarget:e.deploymentTarget,databaseProvider:e.databaseProvider,cacheProvider:e.cacheProvider,observability:e.observability,...e.baseUrl?{baseUrl:e.baseUrl}:{},...t&&{licenseToken:t.token,licenseKeyHash:t.keyHash,installId:t.installId}}}async function ns(e,t){let n=(await Ee(e.projectDir,e.projectDir,Gl)).sort(),i=await Promise.all(n.map(async a=>[Be(Vl(e.projectDir,a)),await sr(a)])),o=Object.fromEntries(i),s=Kl(e,t);await c(ts(e.projectDir,we),`${JSON.stringify(s,null," ")}
1629
- `),await c(ts(e.projectDir,En),`${JSON.stringify(o,null," ")}
1630
- `)}import{relative as Hl}from"path";async function dt(e){let r=(await Ee(e,e,at)).sort(),n=await Promise.all(r.map(async i=>[Be(Hl(e,i)),await sr(i)]));return Object.fromEntries(n)}import{copyFile as Yl,mkdir as Wl,rm as zl}from"fs/promises";import{dirname as ql,join as is,relative as Jl}from"path";import{existsSync as Xl}from"fs";async function Hr(e,t){Xl(t)&&await zl(t,{recursive:!0,force:!0});let n=(await Ee(e,e,at)).map(i=>({file:i,dest:is(t,Be(Jl(e,i)))}));await Promise.all([...new Set(n.map(({dest:i})=>ql(i)))].map(i=>Wl(i,{recursive:!0}))),await Promise.all(n.map(({file:i,dest:o})=>Yl(i,o)))}async function ss(e,t){await Hr(e,is(t,Ft))}import{readdir as Zl,readFile as as}from"fs/promises";import{existsSync as Ql}from"fs";import{dirname as ep,join as pe,resolve as Yr,sep as Wr}from"path";import{fileURLToPath as tp}from"url";var rp={"claude-code":".claude/skills",cursor:".cursor/skills",codex:".agents/skills","gemini-cli":".gemini/skills",windsurf:".windsurf/skills"},He=Object.values(rp),zr=["generatesaas-update"],np=ep(tp(import.meta.url));function ip(){return Kt(np,["skill","content"],"content")}async function qr(e,t){for(let r of He){let n=pe(e,r);for(let[i,o]of Object.entries(t)){let s=Yr(n,i);if(!s.startsWith(n+Wr))continue;let a=pe(s,"scripts"),d=pe(s,"references");await Yt(a),await Yt(d),await c(pe(s,"SKILL.md"),o.skillMd.replaceAll("__SKILL_ROOT__",r));for(let[w,y]of Object.entries(o.scripts)){let P=Yr(a,w);P.startsWith(a+Wr)&&await c(P,y)}let u=Object.entries(o.references);u.length===0&&await c(pe(d,".gitkeep"),"");for(let[w,y]of u){let P=Yr(d,w);P.startsWith(d+Wr)&&await c(P,y)}}}}async function os(e){if(!Ql(e))return{};let t={};for(let r of await Zl(e))r!==".gitkeep"&&(t[r]=await as(pe(e,r),"utf-8"));return t}async function cs(e){let t=ip(),r={};for(let n of zr){let i=pe(t,n);r[n]={skillMd:await as(pe(i,"SKILL.md"),"utf-8"),scripts:await os(pe(i,"scripts")),references:await os(pe(i,"references"))}}await qr(e,r)}import{execFile as sp,execFileSync as op}from"child_process";import{access as ls,readFile as ap}from"fs/promises";import{join as Jr}from"path";import*as U from"@clack/prompts";function We(e){try{let t=process.platform==="win32"?"where":"which";return op(t,[e],{stdio:"ignore"}),!0}catch{return!1}}function Ye(e,t,r,n=3e5){return new Promise((i,o)=>{sp(e,t,{cwd:r,timeout:n,maxBuffer:50*1024*1024},(s,a,d)=>{if(s){let u=String(a||"").trim(),y=[String(d||"").trim(),u].filter(Boolean).join(`
1631
- `);o(new Error(y?`${s.message}
1632
- ${y}`:s.message))}else i()})})}async function ps(e){if(!We("pnpm"))return U.log.warn("pnpm not found. Skipping lockfile regeneration."),!1;try{return await Ye("pnpm",["install","--lockfile-only","--no-frozen-lockfile","--config.minimumReleaseAge=0"],e),!0}catch(t){let r=t instanceof Error?t.message:String(t);return U.log.warn(`Lockfile regeneration failed: ${r}`),U.log.warn("Deploys using --frozen-lockfile may fail."),!1}}async function ds(e){if(!We("pnpm"))return U.log.warn("pnpm not found. Skipping dependency installation."),U.log.info("Install pnpm: https://pnpm.io/installation"),!1;let t=U.spinner();t.start("Installing dependencies (this may take a minute)...");try{return await Ye("pnpm",["install","--config.minimumReleaseAge=0"],e),t.stop("Dependencies installed."),!0}catch(r){t.stop("Dependency installation failed.");let n=r instanceof Error?r.message:String(r);return U.log.warn(`pnpm install failed: ${n}`),U.log.warn("You can run it manually later."),!1}}async function us(e){if(!We("pnpm"))return!1;let t=U.spinner();t.start("Generating baseline database migration...");try{return await Ye("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 U.log.warn(`Could not generate baseline migration: ${n}`),U.log.warn("Run 'pnpm -F @repo/database generate' before your first deploy."),!1}}async function ms(e){try{return await ls(Jr(e,".git")),U.log.info("Git repository already exists, skipping init."),!0}catch{}if(!We("git"))return U.log.warn("git not found. Skipping repository initialization."),!1;let t=U.spinner();t.start("Initializing git repository...");try{return await Ye("git",["init"],e),await Ye("git",["add","-A"],e),await Ye("git",["commit","--no-verify","-m","Initial commit from GenerateSaaS"],e),t.stop("Git repository initialized."),!0}catch{return t.stop("Git initialization failed."),U.log.warn("You can run git init manually later."),!1}}async function hs(e){if(!We("pnpm"))return!1;try{await ls(Jr(e,".git"))}catch{return!1}try{let t=JSON.parse(await ap(Jr(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 Ye("pnpm",["exec","simple-git-hooks"],e),!0}catch{return U.log.warn("Could not install git hooks. Run 'pnpm exec simple-git-hooks' manually."),!1}}import*as ze from"@clack/prompts";import re from"picocolors";function fs(e,t){let r=tt(e);t.dockerAvailable||ze.log.warn("Docker not found. Install Docker to run local services: https://docs.docker.com/get-docker/"),t.baselineMigrationMissing&&ze.log.warn("No database migration was generated (the install did not complete). Run `pnpm install` then `pnpm --filter @repo/database generate` and commit the result BEFORE deploying - otherwise your first deploy pushes the schema untracked.");let n=[];n.push(`cd ${e.projectDir}`),t.pnpmInstalled||n.push("pnpm install"),n.push(`pnpm infra ${re.dim(`# ${r.map(a=>an[a].label).join(", ")}`)}`),n.push(`pnpm dev ${re.dim("# http://localhost:3000")}`),t.skippedCredentials.length>0&&(n.push(""),n.push(re.dim("Fill in remaining TODO values in .env"))),ze.note(n.join(`
1633
- `),re.yellow("Start Development"));let i=[];i.push(`App ${re.cyan("http://localhost:3000")}`),e.architecture==="separate"&&i.push(`API ${re.cyan("http://localhost:3010")}`),r.includes("mailpit")&&i.push(`Mailpit ${re.cyan("http://localhost:8025")}`),r.includes("inngest")&&i.push(`Inngest ${re.cyan("http://localhost:8288")}`),ze.note(i.join(`
1634
- `),re.yellow("Dev Tools"));let o=[],s=cp(e);s.length>0&&o.push(`Set in production: ${re.dim(s.join(", "))}`),o.push("pnpm db:setup # Apply the database schema"),o.push(lp(e)),ze.note(o.join(`
1635
- `),re.yellow("Deployment"))}function cp(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 lp(e){switch(e.deploymentTarget){case"node":return"Deploy with Docker or your preferred Node.js host";case"vercel":return"vercel deploy # Deploy to Vercel"}}function ut(e){let r=(e.startsWith("v")?e.slice(1):e).match(/^(\d+)\.(\d+)\.(\d+)$/);return r?[Number(r[1]),Number(r[2]),Number(r[3])]:null}function _e(e,t){let r=ut(e),n=ut(t);if(!r||!n)return 0;for(let i=0;i<3;i++)if(r[i]!==n[i])return r[i]-n[i];return 0}function or(e,t){return e.versions.find(r=>r.version===t)?.cli}function gs(e){return process.env.GENERATESAAS_TEMPLATE_TARBALL||process.env.GENERATESAAS_TEMPLATE_TARBALL_DIR||process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?!0:ut(e)===null}function ar(e,t,r="3.11.0"){if(gs(r))return{ok:!0};let n=or(e,e.latest);return!n||ut(n)===null?{ok:!0}:_e(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 ys(e,t,r="3.11.0"){if(gs(r))return{ok:!0};let n=or(e,t);return!n||ut(n)===null?{ok:!0}:_e(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 fp from"picocolors";var gp="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";async function yp(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 bs(e){e.command("init").description("Scaffold a new GenerateSaaS project").argument("[apiKey]","license key (same as --api-key)").option("-n, --name <name>","project name (lowercase, hyphens, starts with letter)").option("--app-name <name>","display name for the app").option("-l, --location <path>","project directory (default: ./{name})").addOption(new ne("--architecture <type>","fullstack or separate").choices([...rt])).addOption(new ne("--payment <provider>","payment provider").choices([...nt])).addOption(new ne("--email <provider>","email provider").choices([...it])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").option("--projects","enable projects (a switcher that scopes a user's work)").option("--no-projects","disable projects").option("--docs","include the docs app (apps/docs, Fumadocs)").option("--no-docs","exclude the docs app").option("--desktop","include the desktop app (apps/desktop, built with Electron)").option("--no-desktop","exclude the desktop app").option("--mobile","include the mobile app (apps/mobile, built with Expo)").option("--no-mobile","exclude the mobile app").addOption(new ne("--mobile-purchases <lane>","in-app purchases in the mobile app: revenuecat adds the store billing lane on top of your web payment provider, none keeps web billing only").choices([...Me])).option("--ai","include the AI features (chat, models, automations, integrations); unlocks the runner").option("--no-ai","exclude the AI features").option("--web-ai","show the AI features in the web app (the default)").option("--no-web-ai","desktop-first: keep the AI features out of the web app, which is reduced to account, login and billing - the desktop or mobile app is the AI product. Requires --desktop or --mobile, plus --ai, and cannot be combined with --runner").option("--runner","include the runner feature - your web users connect a machine that runs their own AI subscriptions for this app").option("--no-runner","exclude the backend runner feature").option("--revenue-sharing","enable revenue sharing").option("--no-revenue-sharing","disable revenue sharing").option("--credits","enable credits system").option("--no-credits","disable credits system (subscription-only)").addOption(new ne("--currency <code>","default currency for billing").choices([...fe])).addOption(new ne("--preset <preset>","infrastructure preset: serverless (Vercel + Neon + Upstash) or selfhost (Docker or Node + Postgres + Redis); --deploy/--database/--cache override it piecewise").choices([...Ue])).addOption(new ne("--deploy <target>","deployment target").choices([...ge])).addOption(new ne("--database <provider>","database provider").choices([...ye])).addOption(new ne("--cache <provider>","cache provider").choices([...be])).option("--observability <providers>",`comma-separated observability providers (${Y.join(", ")}); anything unselected ships no SDK`).option("--api-key <key>","API key (skips interactive prompt)").option("--base-url <url>","public base URL (e.g. https://example.com) - bakes into canonical/og/sitemap").option("-y, --yes","accept defaults for unspecified options (non-interactive)").addOption(new ne("--demo","first-party demo build: keep sample content, mark site non-indexable - requires CI API key").hideHelp()).addOption(new ne("--no-db-migration","skip generating the baseline DB migration (internal: demos/CI/playground)").hideHelp()).action(async(t,r)=>{await bp(t?{...r,apiKey:t}:r)})}async function bp(e){let t=performance.now();sn("3.11.0");let r;try{r=bn(e)}catch(v){S.cancel(j(v)),process.exit(1)}let n=S.spinner(),i;try{i=await ot({apiKey:e.apiKey,prompt:!e.yes})}catch(v){S.cancel(j(v)),process.exit(1)}e.demo&&Kr(i)!==gp&&(S.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let o=ke(i),s,a=async()=>{s=await Te(o);let v=ar(s,"init");if(!v.ok)throw new Error(v.message);return{latestVersion:s.latest,selectedVersion:s.latest,desktopAllowed:s.entitlements?.desktopAllowed??!0,runnerAllowed:s.entitlements?.runnerAllowed??!0,mobileAllowed:s.entitlements?.mobileAllowed??!0}};n.start("Verifying access...");let d,u,w=!0,y=!0,P=!0;try{({latestVersion:d,selectedVersion:u,desktopAllowed:w,runnerAllowed:y,mobileAllowed:P}=await a()),n.stop("Access verified."),Ve(i)}catch(v){if(n.stop("Access verification failed."),v instanceof F&&v.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 kt(),o=ke(i),n.start("Verifying access...");try{({latestVersion:d,selectedVersion:u,desktopAllowed:w,runnerAllowed:y,mobileAllowed:P}=await a()),n.stop("Access verified."),Ve(i)}catch(M){n.stop("Access verification failed."),S.cancel(M instanceof F&&M.status===401?"Invalid API key.":j(M)),process.exit(1)}}else S.cancel(j(v)),process.exit(1)}S.log.success(`Latest version: ${d}`),r.desktop===!0&&!w&&(S.cancel("The desktop app is available on the Pro plan and up. Upgrade your plan at generatesaas.com."),process.exit(1)),r.runner===!0&&!y&&(S.cancel("The runner is available on the Pro plan and up. Upgrade your plan at generatesaas.com."),process.exit(1)),r.mobile===!0&&!P&&(S.cancel("The mobile app is available on the Pro plan and up. Upgrade your plan at generatesaas.com."),process.exit(1));let I;e.yes?I=wn({...r,...w?{}:{desktop:!1},...y?{}:{runner:!1},...P?{}:{mobile:!1}}):I=await vn(r,{desktopAllowed:w,runnerAllowed:y,mobileAllowed:P});let _;n.start("Activating license...");try{let v=e.demo&&I.baseUrl?await yp(I.baseUrl):crypto.randomUUID(),M=()=>({frontend:vr,version:u,installId:v,projectName:I.projectName,options:Rn(I)}),G;try{G=await kr(o,M())}catch(se){let N=Vt(se);if(!N?.lastAllowedVersion||!s)throw se;n.stop("License activation failed.");let de=ys(s,N.lastAllowedVersion);if(de.ok||(S.cancel(`${N.message} ${de.message}`),process.exit(1)),e.yes){let ue=or(s,N.lastAllowedVersion);S.cancel(`${N.message} Re-run ${ue?`npx generatesaas@${ue} init`:"init"} without --yes to continue with v${N.lastAllowedVersion} (the last version your license covers).`),process.exit(1)}let X=await S.confirm({message:`Your update window has ended. Continue with v${N.lastAllowedVersion} (the last version your license covers)?`});(S.isCancel(X)||!X)&&(S.cancel("Setup cancelled."),process.exit(0)),u=N.lastAllowedVersion,n.start(`Activating license for v${u}...`),G=await kr(o,M())}_={token:G.token,keyHash:Kr(i),installId:G.installId??v},n.stop("License activated.")}catch(v){n.stop("License activation failed."),S.cancel(j(v)),process.exit(1)}let f=hp(I.projectDir);if(pp(f)&&dp(f).length>0)if(e.yes)S.log.info(`Directory ${f} is not empty. Merging (keeping existing files, overwriting conflicts).`);else{let M=await S.select({message:`Directory ${f} is not empty.`,options:[{value:"merge",label:"Merge",hint:"keep existing files, overwrite conflicts"},{value:"overwrite",label:"Overwrite",hint:"delete everything and start fresh"},{value:"cancel",label:"Cancel"}]});(S.isCancel(M)||M==="cancel")&&(S.cancel("Setup cancelled."),process.exit(0)),M==="overwrite"&&up(f,{recursive:!0,force:!0})}let O={...I,projectDir:f,version:u,...e.demo?{docs:!1}:{}};n.start("Downloading template...");try{await Bt(o,u,f),n.stop("Template downloaded.")}catch(v){n.stop("Download failed."),S.cancel(j(v)),process.exit(1)}n.start("Generating project files...");try{if(await ir(O),!e.demo){let v=await dt(f);await c(mp(f,Mt),`${JSON.stringify(v,null," ")}
1636
- `),await ss(f,f)}await cs(f),await ns(O,_),n.stop("Project files generated.")}catch(v){n.stop("Generation failed."),S.cancel(j(v)),process.exit(1)}!await ps(f)&&O.demo===!0&&(S.cancel("Lockfile regeneration failed on a demo build - refusing to ship a stale lockfile."),process.exit(1));let q=await ds(f),J=O.demo!==!0&&e.dbMigration!==!1,xe=!1;J&&q&&(await us(f),xe=!0),await ms(f),q&&await hs(f);let De=We("docker"),Ze=yr(O).map(v=>v.key).filter(v=>!O.credentials?.[v]);fs(O,{pnpmInstalled:q,dockerAvailable:De,skippedCredentials:Ze,baselineMigrationMissing:J&&!xe}),on(),S.log.info(fp.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as cr}from"fs";import{readFile as Ps}from"fs/promises";import{join as qe,resolve as Sp}from"path";import*as b from"@clack/prompts";import C from"picocolors";import{mkdtemp as wp,rm as vp}from"fs/promises";import{tmpdir as kp}from"os";import{join as Ep}from"path";async function Xr(e,t,r,n){let i=await wp(Ep(kp(),"generatesaas-stage-"));try{await Bt(e,t,i),await ir({...r,projectDir:i}),await Hr(i,n)}finally{await vp(i,{recursive:!0,force:!0})}}function ws(e){return e?"ON":"OFF"}var mt="packages/config/src/index.ts";function vs(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 ks(e){return[...tt({databaseProvider:e.databaseProvider??"postgres",cacheProvider:e.cacheProvider??"redis",emailProvider:e.emailProvider??"smtp"})].sort()}var Zr=[{key:"errorTracking",severity:"warn",normalize:e=>{e.errorTracking===void 0||e.observability!==void 0||(e.observability=e.errorTracking==="none"?[]:[e.errorTracking])},preserved:()=>!0,describe:()=>"Retired option `errorTracking`: it is now `observability`, an array, because a project may run more than one provider at once. Your recorded choice was migrated across automatically."},{key:"companion",severity:"warn",normalize:e=>{e.companion===void 0||e.runner!==void 0||(e.runner=e.companion)},preserved:()=>!0,describe:()=>"Retired option `companion`: it is now `runner`, because the daemon it names is AgentRunner. Your recorded choice was migrated across automatically - the feature itself is unchanged."},{key:"companionSource",severity:"warn",preserved:()=>!1,describe:e=>`Retired option \`companionSource\`: your project recorded a \`${String(e.companionSource)}\` daemon source. There is now one runner daemon - AgentRunner - which we publish and your users install from its own releases. Your project keeps the runner feature and its own pairing, dispatch and Runners page; it no longer holds or brands a daemon.`},{key:"companionName",severity:"warn",preserved:()=>!1,describe:e=>`Retired option \`companionName\`: your project recorded a branded daemon called ${e.companionName}. Buyers no longer brand or publish a daemon - your users install AgentRunner, which we maintain. Their existing install of your old daemon keeps running until they switch.`},{key:"companionRepoUrl",severity:"warn",preserved:()=>!1,describe:e=>`Retired option \`companionRepoUrl\`: your project pointed its install flow at ${e.companionRepoUrl}. It now points at AgentRunner, the one daemon we publish.`},{key:"companionLocalPath",severity:"warn",preserved:e=>(e.companionLocalPath??"").trim()==="",describe:e=>`Retired option \`companionLocalPath\`: your project recorded a local daemon checkout at ${e.companionLocalPath}. The option is gone, and so is the choice behind it - there is one runner daemon (AgentRunner) and we publish it, so nothing in your project is built from a local checkout any more.`},{key:"desktopAutoRelease",severity:"warn",applies:e=>e.desktop===!0,preserved:e=>e.desktopAutoRelease!==!0,describe:()=>"Retired option `desktopAutoRelease`: your project recorded automatic desktop releases, but the release trigger is no longer an option - cutting a release publishes signed installers from your repository and spends your Actions minutes, so it is always deliberate now. The staged `.github/workflows/desktop-release.yml` runs on `workflow_dispatch` only (the Actions tab). Add a `workflow_run` block to it if you want the CI-chained trigger back."},{key:"billingScope",severity:"ask",applies:e=>e.multiTenancy===!0,preserved:e=>e.billingScope!=="organization",describe:()=>"Retired option `billingScope`: your project recorded organization-scoped billing, and organization billing is GONE - not moved to config, removed. Money now lives only on a USER: an organization has no balance, no plan and no payment-provider customer of its own, and a workspace is funded by that organization's OWNER, whose personal balance every credit spent in it is drawn from. Payments already in flight are safe - a webhook arriving with old organization metadata is credited to that organization's owner rather than dropped - but a STORED balance is not: the `credits` and `plan` columns an organization holds them in are going away, and this update does NOT migrate them for you. MOVE every paying organization's credits and plan onto its owner BEFORE you take new checkouts, or that balance goes with the columns. How should this update proceed?"},{key:"socialProviders",severity:"warn",preserved:e=>(e.socialProviders??[]).length===0,describe:e=>`Retired option \`socialProviders\`: your project recorded ${(e.socialProviders??[]).join(", ")}, but it is now a plain config value that starts empty. Add those IDs back to \`config.auth.socialProviders\` in ${mt} or their sign-in buttons stop rendering.`},{key:"mcpServer",severity:"warn",preserved:e=>e.mcpServer!==!0,describe:()=>`Retired option \`mcpServer\`: your project recorded the outward MCP server ON, but its code now ships to every project behind a config gate generated OFF. Set \`config.mcpServer.enabled\` to \`true\` in ${mt} to keep your /mcp endpoint and its settings tab.`},{key:"emailTracking",severity:"ask",preserved:e=>e.emailTracking!==!1,describe:()=>`Retired option \`emailTracking\`: your project recorded email open/click tracking OFF, but it is now a plain config value generated ON. Applying this update as-is starts tracking opens and clicks for every user you mail. Put it back by setting \`config.email.tracking.enabled\` to \`false\` in ${mt}. How should this update proceed?`},{key:"aiBuiltin",severity:"warn",normalize:e=>{e.aiBuiltin===void 0&&e.aiByok!==void 0&&(e.aiBuiltin=e.aiByok===!1)},applies:e=>e.ai===!0,preserved:e=>e.aiBuiltin===oe(e),describe:e=>`Retired option \`aiBuiltin\`: your project recorded built-in AI models ${ws(e.aiBuiltin)}, but it is now a plain config value generated ${ws(oe(e))} for a project ${oe(e)?"with":"without"} a payment provider. Set \`config.ai.builtin\` to \`${String(e.aiBuiltin===!0)}\` in ${mt} to keep your choice.`},{key:"aiByok",severity:"warn",applies:e=>e.ai===!0,preserved:e=>e.aiByok!==!1,describe:()=>`Retired option \`aiByok\`: your project recorded bring-your-own-key AI OFF, but it is now a plain config value generated ON. Set \`config.ai.byok\` to \`false\` in ${mt} to keep it off.`},{key:"dockerServices",severity:"warn",preserved:e=>vs(e).join(",")===ks(e).join(","),describe:e=>`Retired option \`dockerServices\`: your project recorded ${vs(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 ${ks(e).join(", ")}. Add any extra service back by hand; the file is yours to edit.`},{key:"aiTools",severity:"warn",preserved:e=>(e.aiTools??[]).length===ln.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 (${He.join(", ")}) so it is there whichever assistant you switch to. Delete the roots you do not use - they are plain text files with no runtime path.`},{key:"blog",severity:"warn",preserved:e=>e.blog!==!1,describe:()=>`Retired option \`blog\`: your project recorded the blog OFF, but content is no longer an option. This update adds the \`/blog\`, \`/alternatives\` and \`/compare\` route code to your project; every one of them 404s until you add a file under \`packages/content/{locale}/{section}/\`, and none of them appears in your navbar, footer, sitemap or llms.txt while it is empty. Delete an entry from \`config.content.sections\` in ${mt} to drop a section for good.`}],Qf=Zr.map(e=>e.key);function Es(e){for(let t of Zr)t.normalize?.(e)}function Ss(e){let t=[];for(let r of Zr){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 As(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",Pp,[]).action(async(t,r)=>{r.args.length>0&&b.log.warn("`auto` is now the default and the argument is ignored.");let n=Ap(t.answer),i=Sp(t.cwd??process.cwd()),o=qe(i,we),s;try{s=JSON.parse(await Ps(o,"utf-8"))}catch{b.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}s.frontend===br&&(b.cancel(`This project was generated on the Nuxt frontend (\`.generatesaas/manifest.json\` records \`frontend: "${br}"\`).
1614
+ `,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 yn(t),await bn(t),await vn(t),await wn(t)),await kn(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 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 Rr(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:rn,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," ")}
1615
+ `),await l($i(e.projectDir,nn),`${JSON.stringify(o,null," ")}
1616
+ `)}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 _r(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 _r(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 Or,sep as xr}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),Dr=["generatesaas-update"],Dl=_l(Ol(import.meta.url));function Nl(){return Dt(Dl,["skill","content"],"content")}async function Nr(e,t){for(let r of Ve){let n=oe(e,r);for(let[i,o]of Object.entries(t)){let s=Or(n,i);if(!s.startsWith(n+xr))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=Or(a,g);v.startsWith(a+xr)&&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=Or(d,g);v.startsWith(d+xr)&&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 Dr){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 Nr(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 Cr}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(`
1617
+ `);o(new Error(k?`${s.message}
1618
+ ${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","--config.minimumReleaseAge=0"],e),!0}catch(t){let r=t instanceof Error?t.message:String(t);return C.log.warn(`Lockfile regeneration failed: ${r}`),C.log.warn("Deploys using --frozen-lockfile may fail."),!1}}async function 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","--config.minimumReleaseAge=0"],e),t.stop("Dependencies installed."),!0}catch(r){t.stop("Dependency installation failed.");let n=r instanceof Error?r.message:String(r);return C.log.warn(`pnpm install failed: ${n}`),C.log.warn("You can run it manually later."),!1}}async function 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(Cr(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(Cr(e,".git"))}catch{return!1}try{let t=JSON.parse(await $l(Cr(e,"package.json"),"utf-8")),r=!!t.devDependencies?.["simple-git-hooks"],n=!!t["simple-git-hooks"];if(!r||!n)return!1}catch{return!1}try{return await 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=>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"))),Ke.note(n.join(`
1619
+ `),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(`
1620
+ `),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)),Ke.note(o.join(`
1621
+ `),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.12.0"){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.12.0"){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();Br("3.12.0");let r;try{r=Qr(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&&Rr(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=en({...r,...g?{}:{desktop:!1},...k?{}:{mobile:!1}}):v=await tn(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:ln(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:Rr(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(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," ")}
1622
+ `),await Fi(m,m)}await Gi(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 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}),Gr(),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 Lr(e,t,r,n){let i=await zl(Xl(Jl(),"generatesaas-stage-"));try{await Ot(e,t,i),await Ht({...r,projectDir:i}),await _r(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 $r=[{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===Yr.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=$r.map(e=>e.key);function ns(e){for(let t of $r)t.normalize?.(e)}function is(e){let t=[];for(let r of $r){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}"\`).
1637
1623
 
1638
- v${wr} is the last release that ships a Nuxt frontend, and this CLI cannot update or migrate a Nuxt project to Next.js.
1624
+ v${ir} is the last release that ships a Nuxt frontend, and this CLI cannot update or migrate a Nuxt project to Next.js.
1639
1625
 
1640
- Your project keeps working exactly as it is - stay on v${wr}, or start a fresh project on Next.js with ${C.cyan("generatesaas init")} when you are ready to move.`),process.exit(1));let a=dn(s);a.length>0&&(b.cancel(`.generatesaas/manifest.json carries a value this CLI cannot shape a project from:
1626
+ Your project keeps working exactly as it is - stay on v${ir}, or start a fresh project on Next.js with ${L.cyan("generatesaas init")} when you are ready to move.`),process.exit(1));let a=zr(s);a.length>0&&(b.cancel(`.generatesaas/manifest.json carries a value this CLI cannot shape a project from:
1641
1627
 
1642
- ${a.map(f=>` - ${f}`).join(`
1628
+ ${a.map(m=>` - ${m}`).join(`
1643
1629
  `)}
1644
1630
 
1645
- Correct it there, then run ${C.cyan("generatesaas update")} again.`),process.exit(1)),Es(s);let d=me(s);d&&(b.cancel(`.generatesaas/manifest.json turns \`${d.feature}\` on with \`deploymentTarget: "${d.target}"\`, which cannot run it.
1646
-
1647
- ${d.reason}
1648
-
1649
- Correct it there, then run ${C.cyan("generatesaas update")} again.`),process.exit(1));let u=mr(s);u.length>0&&(b.cancel(`.generatesaas/manifest.json records a combination this project cannot run:
1631
+ Correct it there, then run ${L.cyan("generatesaas update")} again.`),process.exit(1)),ns(s);let d=Qt(s);d.length>0&&(b.cancel(`.generatesaas/manifest.json records a combination this project cannot run:
1650
1632
 
1651
- ${u.map(f=>` - ${f}`).join(`
1633
+ ${d.map(m=>` - ${m}`).join(`
1652
1634
  `)}
1653
1635
 
1654
- Correct it there, then run ${C.cyan("generatesaas update")} again.`),process.exit(1));let w=fn(s);w.length>0&&(b.cancel(`.generatesaas/manifest.json turns the web AI surface off (\`webAi: false\`) in a project that cannot run without it:
1636
+ Correct it there, then run ${L.cyan("generatesaas update")} again.`),process.exit(1));let h=Jr(s);h.length>0&&(b.cancel(`.generatesaas/manifest.json turns the web AI surface off (\`webAi: false\`) in a project that cannot run without it:
1655
1637
 
1656
- ${w.map(f=>` - ${f}`).join(`
1638
+ ${h.map(m=>` - ${m}`).join(`
1657
1639
  `)}
1658
1640
 
1659
- Correct it there, then run ${C.cyan("generatesaas update")} again.`),process.exit(1));let y=gn(s);y.length>0&&(b.cancel(`.generatesaas/manifest.json turns on mobile store purchases (\`mobilePurchases: "revenuecat"\`) in a project that cannot run them:
1641
+ Correct it there, then run ${L.cyan("generatesaas update")} again.`),process.exit(1));let g=Xr(s);g.length>0&&(b.cancel(`.generatesaas/manifest.json turns on mobile store purchases (\`mobilePurchases: "revenuecat"\`) in a project that cannot run them:
1660
1642
 
1661
- ${y.map(f=>` - ${f}`).join(`
1643
+ ${g.map(m=>` - ${m}`).join(`
1662
1644
  `)}
1663
1645
 
1664
- Correct it there, then run ${C.cyan("generatesaas update")} again.`),process.exit(1));let P;try{P=await ot()}catch(f){b.cancel(j(f)),process.exit(1)}let I=ke(P),_=b.spinner();try{_.start("Verifying access...");let f;try{f=await Te(I)}catch(D){throw D instanceof F&&D.status===401?new Error("Your saved API key was rejected. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):D}let O=ar(f,"update");if(!O.ok)throw new Error(O.message);_.stop("Access verified."),Ve(P),_.start("Fetching latest skill files...");let Oe=await In(I,f.latest);await qr(i,Oe.skills);let q=He;if(_.stop("Skills updated."),b.log.success(`Skill files installed to ${C.cyan(q.length.toString())} locations.`),s.version===f.latest){b.log.info(`Already on the latest version (${s.version}).`);return}_e(s.version,f.latest)>0&&(b.cancel(`This project is on ${s.version}, which is newer than the latest release (${f.latest}). Updating would stage the older template over it. Check that your API key points at the right account, or correct \`version\` in ${we}.`),process.exit(1));let J=f.entitlements;J&&(s.desktop&&!J.desktopAllowed&&(b.cancel("This project includes the desktop app, which is available on the Pro plan and up. Upgrade your plan at generatesaas.com to keep updating it."),process.exit(1)),s.runner&&!J.runnerAllowed&&(b.cancel("This project includes the runner, which is available on the Pro plan and up. Upgrade your plan at generatesaas.com to keep updating it."),process.exit(1)),s.mobile&&J.mobileAllowed===!1&&(b.cancel("This project includes the mobile app, which is available on the Pro plan and up. Upgrade your plan at generatesaas.com to keep updating it."),process.exit(1)));let xe=JSON.stringify(s);hn(s),await Ip(s,yn(s),n),await Rp(Ss(s),n);let De=pn(s),Ne=un(s),Ze=mr(Ne);Ze.length>0&&(b.cancel(`.generatesaas/manifest.json is missing a provider key, and its default would create a combination this project cannot run:
1646
+ 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 cn(v,m.latest);await Nr(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,Zr(s),n),await rp(is(s),n);let Re=Wr(s),ze=qr(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:
1665
1647
 
1666
- ${Ze.map(D=>` - ${D}`).join(`
1648
+ ${ce.map(A=>` - ${A}`).join(`
1667
1649
  `)}
1668
1650
 
1669
- Add the key to .generatesaas/manifest.json with a value your deployment target supports, then run ${C.cyan("generatesaas update")} again.`),process.exit(1));let v=JSON.stringify(Ne)!==xe;if(s=Ne,s.licenseToken)try{let D=await Tn(I,{currentToken:s.licenseToken,newVersion:f.latest});s.licenseToken=D.token,D.licenseKeyHash&&(s.licenseKeyHash=D.licenseKeyHash),await c(o,`${JSON.stringify(s,null," ")}
1670
- `),v=!1,b.log.success("License refreshed.")}catch(D){let ht=Vt(D);ht&&(b.cancel(ht.message),process.exit(1)),b.log.warn("License refresh skipped.")}v&&await c(o,`${JSON.stringify(s,null," ")}
1671
- `);let M=rs(s,i),G=qe(i,Sn);_.start(`Staging v${f.latest} (shaped for your config)...`),await Xr(I,f.latest,M,G),_.stop("Template staged.");let{text:se,title:N}=await Op(I,f,s.version);se&&b.note(se,N);let de=qe(i,Mt),X=qe(i,Ft),ue=!cr(X),Ce=!cr(de);if(ue){if(_.start("Building baseline template (one-time migration)..."),await Xr(I,s.version,M,X),Ce){let D=await dt(X);await c(de,`${JSON.stringify(D,null," ")}
1672
- `)}if(_.stop("Baseline template stored."),!Ce){let D=await xp(de,X);D>0&&b.log.warn(`Rebuilt baseline differs from the original for ${D} 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(Ce){_.start("Computing baseline template hashes...");let D=await dt(X);await c(de,`${JSON.stringify(D,null," ")}
1673
- `),_.stop("Baseline hashes computed.")}await c(qe(i,Pn),`${JSON.stringify({currentVersion:s.version,targetVersion:f.latest,changelog:se,stagedAt:new Date().toISOString(),...De.length>0?{newOptions:De}:{}},null," ")}
1674
- `),b.log.info(`Update staged: ${C.cyan(s.version)} \u2192 ${C.cyan(f.latest)}`),b.log.info(`Ask your AI coding assistant to ${C.cyan("'update my GenerateSaaS project'")}.`),s.desktop===!0&&cr(qe(i,"apps/tauri"))&&b.log.warn(`Desktop: the app moved to an Electron shell (${C.cyan("apps/desktop")}). Your project still has the old Tauri shell (${C.cyan("apps/tauri")}) - remove it after verifying the Electron app builds and runs. See ${C.cyan("/desktop/updates")}.`),s.desktop===!0&&cr(qe(i,"apps/electron"))&&b.log.warn(`Desktop: the app now lives at ${C.cyan("apps/desktop")}. Your project still has the old ${C.cyan("apps/electron")} directory - move your local changes across, remove it, then run ${C.cyan("pnpm install")}. See ${C.cyan("/desktop/updates")}.`)}catch(f){_.stop("Failed."),b.cancel(`Update failed: ${j(f)}`),process.exit(1)}})}var Is="desktop-migration";function Pp(e,t){return t.push(e),t}function Ap(e){let t=new Set([Is,...Object.keys(Ts).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 Rs(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}
1651
+ 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 pn(v,{currentToken:s.licenseToken,newVersion:m.latest});s.licenseToken=A.token,A.licenseKeyHash&&(s.licenseKeyHash=A.licenseKeyHash),await l(o,`${JSON.stringify(s,null," ")}
1652
+ `),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," ")}
1653
+ `);let _=ji(s,i),Q=He(i,sn);P.start(`Staging v${m.latest} (shaped for your config)...`),await Lr(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 Lr(v,s.version,_,H),ke){let A=await ot(H);await l(ee,`${JSON.stringify(A,null," ")}
1654
+ `)}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," ")}
1655
+ `),P.stop("Baseline hashes computed.")}await l(He(i,on),`${JSON.stringify({currentVersion:s.version,targetVersion:m.latest,changelog:K,stagedAt:new Date().toISOString(),...Re.length>0?{newOptions:Re}:{}},null," ")}
1656
+ `),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}
1675
1657
 
1676
1658
  This update is running without an interactive terminal, so it cannot ask you directly. Answer the question \`${e.id}\` and re-run, for example:
1677
1659
 
1678
- ${C.cyan(`generatesaas update --answer ${e.id}=${e.choices[0].id}`)}
1660
+ ${L.cyan(`generatesaas update --answer ${e.id}=${e.choices[0].id}`)}
1679
1661
 
1680
1662
  Choices:
1681
1663
  ${e.choices.map(n=>` - ${n.id}: ${n.label}${n.hint?` (${n.hint})`:""}`).join(`
1682
- `)}`),process.exit(1)}async function Ip(e,t,r){if(t.kind==="none")return;let n=_p[t.kind];(await Rs({id:Is,message:n.message,choices:n.choices},r)).apply(e)}var Ts={billingScope:[{value:"stage",label:"Stage it - I will move each organization's balance to its owner myself",hint:"move every paying organization's credits and plan onto its owner before you take new checkouts - the columns holding them are going away, and a stored balance is not migrated for you"},{value:"stop",label:"Stop, and stage nothing",hint:"migrate those balances in your database first, then re-run update - this is buyer DATA, not a config line, so nothing the update stages can put it back"}],emailTracking:[{value:"stage",label:"Stage it - I will turn tracking back off myself",hint:"set `config.email.tracking.enabled` to false in packages/config/src/index.ts before your next send"},{value:"stop",label:"Stop, and stage nothing",hint:"edit packages/config/src/index.ts first, then re-run update - a file you have edited is walked as a 3-way merge instead of being replaced"}]};async function Rp(e,t){let r=[];for(let n of e){let i=n.severity==="ask"?Ts[n.key]:void 0;if(!i){b.log.warn(n.message);continue}r.push({notice:n,answers:i})}for(let{notice:n,answers:i}of r)(await Rs({id:`retired:${n.key}`,message:n.message,choices:i.map(s=>({id:s.value,label:s.label,hint:s.hint}))},t)).id==="stop"&&(b.cancel("Update cancelled. Nothing was changed or staged."),process.exit(1))}function Tp(e){e.desktop=!1}var _p={"legacy-desktop-ai":{message:"Your desktop app runs the local AI stack through the retired desktop-AI option. Desktop AI now follows the AI option. Keep it?",choices:[{id:"keep-ai",label:"Turn AI on, keep the desktop AI stack",hint:"also makes your website's AI surface available - keep it hidden with config.ai.enabled: false",apply:e=>{e.ai=!0}},{id:"drop-ai",label:"Turn desktop AI off",hint:"your desktop app becomes a client shell: no AI, no Terminal",apply:e=>{e.ai=!1}}]},"desktop-gains-ai":{message:"Your desktop app has no AI today, but your project has the AI option on. With the desktop-AI option retired, AI on a desktop project now ships the app's local AI stack. Keep it?",choices:[{id:"keep-ai",label:"Keep AI - the desktop app gains the AI stack",hint:"adds the local AI stack and the Terminal to your desktop app",apply:()=>{}},{id:"drop-ai",label:"Drop AI",hint:"your website's AI surface hides, and this also drops the web runner - it requires AI",apply:e=>{e.ai=!1,e.runner=!1}},{id:"drop-desktop",label:"Drop the desktop app",hint:"your website keeps its AI surface exactly as it is today",apply:Tp}]}};async function Op(e,t,r){let n=t.latest,i=t.versions.filter(d=>_e(d.version,r)>0&&_e(d.version,n)<=0).sort((d,u)=>_e(d.version,u.version)),o=async d=>{try{return await An(e,d)}catch{return null}};if(i.length<=1)return{text:await o(n),title:`Changelog v${n}`};let s=await Promise.all(i.map(d=>o(d.version)));return{text:i.map((d,u)=>{let w=d.date?` (${d.date.slice(0,10)})`:"",y=s[u]??"_No changelog available for this release._";return`# v${d.version}${w}
1664
+ `)}`),process.exit(1)}async function tp(e,t,r){if(t.kind==="none")return;let n=ip[t.kind];(await cs({id:as,message:n.message,choices:n.choices},r)).apply(e)}var ls={billingScope:[{value:"stage",label:"Stage it - I will move each organization's balance to its owner myself",hint:"move every paying organization's credits and plan onto its owner before you take new checkouts - the columns holding them are going away, and a stored balance is not migrated for you"},{value:"stop",label:"Stop, and stage nothing",hint:"migrate those balances in your database first, then re-run update - this is buyer DATA, not a config line, so nothing the update stages can put it back"}],emailTracking:[{value:"stage",label:"Stage it - I will turn tracking back off myself",hint:"set `config.email.tracking.enabled` to false in packages/config/src/index.ts before your next send"},{value:"stop",label:"Stop, and stage nothing",hint:"edit packages/config/src/index.ts first, then re-run update - a file you have edited is walked as a 3-way merge instead of being replaced"}]};async function rp(e,t){let r=[];for(let n of e){let i=n.severity==="ask"?ls[n.key]:void 0;if(!i){b.log.warn(n.message);continue}r.push({notice:n,answers:i})}for(let{notice:n,answers:i}of r)(await cs({id:`retired:${n.key}`,message:n.message,choices:i.map(s=>({id:s.value,label:s.label,hint:s.hint}))},t)).id==="stop"&&(b.cancel("Update cancelled. Nothing was changed or staged."),process.exit(1))}function np(e){e.desktop=!1}var ip={"legacy-desktop-ai":{message:"Your desktop app runs the local AI stack through the retired desktop-AI option. Desktop AI now follows the AI option. Keep it?",choices:[{id:"keep-ai",label:"Turn AI on, keep the desktop AI stack",hint:"also makes your website's AI surface available - keep it hidden with config.ai.enabled: false",apply:e=>{e.ai=!0}},{id:"drop-ai",label:"Turn desktop AI off",hint:"your desktop app becomes a client shell: no AI, no Terminal",apply:e=>{e.ai=!1}}]},"desktop-gains-ai":{message:"Your desktop app has no AI today, but your project has the AI option on. With the desktop-AI option retired, AI on a desktop project now ships the app's local AI stack. Keep it?",choices:[{id:"keep-ai",label:"Keep AI - the desktop app gains the AI stack",hint:"adds the local AI stack and the Terminal to your desktop app",apply:()=>{}},{id:"drop-ai",label:"Drop AI",hint:"your website's AI surface hides",apply:e=>{e.ai=!1}},{id:"drop-desktop",label:"Drop the desktop app",hint:"your website keeps its AI surface exactly as it is today",apply:np}]}};async function sp(e,t,r){let n=t.latest,i=t.versions.filter(d=>Ie(d.version,r)>0&&Ie(d.version,n)<=0).sort((d,h)=>Ie(d.version,h.version)),o=async d=>{try{return await an(e,d)}catch{return null}};if(i.length<=1)return{text:await o(n),title:`Changelog v${n}`};let s=await Promise.all(i.map(d=>o(d.version)));return{text:i.map((d,h)=>{let g=d.date?` (${d.date.slice(0,10)})`:"",k=s[h]??"_No changelog available for this release._";return`# v${d.version}${g}
1683
1665
 
1684
- ${y}`}).join(`
1666
+ ${k}`}).join(`
1685
1667
 
1686
- `),title:`Changelog v${r} \u2192 v${n}`}}async function xp(e,t){let r=JSON.parse(await Ps(e,"utf-8")),n=await dt(t),i=0;for(let[o,s]of Object.entries(r))at(o)||n[o]!==s&&i++;for(let o of Object.keys(n))o in r||i++;return i}import*as z from"@clack/prompts";import Pe from"picocolors";import{readFile as Dp}from"fs/promises";import{join as Np,resolve as Cp}from"path";function _s(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=Cp(t.cwd??process.cwd()),n=Np(r,we),i;try{i=JSON.parse(await Dp(n,"utf-8"))}catch{z.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let o=[`Version: ${Pe.cyan(i.version)}`,i.deploymentTarget?`Deploy target: ${Pe.cyan(i.deploymentTarget)}`:null,i.databaseProvider?`Database: ${Pe.cyan(i.databaseProvider)}`:null,i.cacheProvider?`Cache: ${Pe.cyan(i.cacheProvider)}`:null].filter(Boolean).join(`
1687
- `);z.note(o,Pe.bold("Project Status"));let s=z.spinner();s.start("Checking for updates...");try{let a=await ot(),d=ke(a),w=(await Te(d)).latest;i.version===w?(s.stop("Up to date."),z.log.success(`Already on the latest version (${Pe.green(w)})`)):(s.stop("Update available."),z.log.warning(`Update available: ${Pe.yellow(i.version)} \u2192 ${Pe.green(w)}`),z.log.info(`Open this project in your AI coding agent and ask it to ${Pe.cyan("update my GenerateSaaS project")} - it fetches and applies the update for you.`))}catch(a){s.stop("Check failed."),a instanceof F&&a.status===401?z.log.warning("Invalid API key. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):z.log.warning(`Could not check for updates: ${j(a)}`)}})}import{Buffer as Lp}from"buffer";import{readFile as $p}from"fs/promises";import*as $ from"@clack/prompts";import L from"picocolors";function jp(){return process.env.GENERATESAAS_API_KEY??vt()}function Up(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 Mp(e){return{verdict:e.verdict??"unknown",ejectedAt:e.ejectedAt??null}}function Fp(e){switch(e.verdict){case"licensed":return $.log.success(`${L.green("LICENSED")} - resolves to an account with an active${e.plan?` ${e.plan}`:""} license.`),!0;case"ejected":return $.log.success(`${L.green("EJECTED")} - a licensed buyer opted this install out of telemetry${e.ejectedAt?` on ${e.ejectedAt.slice(0,10)}`:""}. The site is legitimate.`),!0;case"revoked":return $.log.error(`${L.red("REVOKED")} - the owning account no longer holds a plan (refund or chargeback). This deployment is no longer licensed.`),!1;case"token_domain_mismatch":return $.log.error(`${L.red("LEAKED TOKEN")} - this license belongs to a different deployment${e.mismatchDomain?` (${L.cyan(e.mismatchDomain)})`:""}, not this site. The token was copied from a licensed project.`),!1;case"no_license_history":return $.log.error(`${L.red("NO LICENSE HISTORY")} - no license has ever been associated with this site. If it runs GenerateSaaS, treat it as unlicensed.`),!1;default:return $.log.warn(`${L.yellow("UNKNOWN")} - could not cross-reference the records right now. Try again shortly.`),!1}}async function Os(e,t){let r=process.env.GENERATESAAS_API_URL??wt,n=jp();e.start("Cross-referencing license records...");try{let i=n?Up(await _n(r,n,{lkh:t.lkh,nid:t.nid,domain:t.domain})):Mp(await Er(r,{token:t.token,domain:t.domain}));return e.stop(`${L.green("Checked")} - records cross-referenced`),Fp(i)}catch(i){return e.stop(`${L.yellow("Skipped")} - ${j(i)}`),null}}function Vp(e){let t=e.split(".");if(t.length!==3||!t[1])throw new Error("Invalid JWT format");let r=Lp.from(t[1],"base64url").toString("utf-8");return JSON.parse(r)}function Qr(e){return typeof e!="number"?"unknown":new Date(e*1e3).toISOString().split("T")[0]}function xs(e){$.note([`License ID: ${L.cyan(String(e.lid??"unknown"))}`,`Version: ${L.cyan(String(e.ver??"unknown"))}`,`Init version: ${String(e.iver??"unknown")}`,`Frontend: ${String(e.fe??"unknown")}`,`Created: ${Qr(e.pat)}`,`Last updated: ${Qr(e.uat)}`,`Expires: ${Qr(e.exp)}`,`Install ID: ${String(e.nid??"unknown")}`].join(`
1688
- `),L.yellow("License Details"))}function Bp(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 Ds(e){let t=$.spinner(),r=null,n="no candidates";for(let s of Bp(e)){t.start(`Checking ${s}...`);try{let a=await fetch(s);if(!a.ok){n=`${s} returned ${a.status}`,t.stop(`${L.yellow("Not here")} - ${n}`);continue}let d=(await a.text()).trim();if(!d||d.split(".").length!==3){n=`${s} did not return a JWT`,t.stop(`${L.yellow("Not here")} - ${n}`);continue}r=d,t.stop(`${L.green("Found")} - license endpoint responded`);break}catch(a){n=`${s}: ${j(a)}`,t.stop(`${L.yellow("Unreachable")} - ${n}`)}}if(r===null){$.log.warn(`No license endpoint found (last: ${n}). The site may be ejected, not a GenerateSaaS app, or serving its API elsewhere.`);let s=Ns(e);return s?await Os(t,{domain:s})??!1:!1}let i;try{i=Vp(r)}catch{return $.log.error("Could not decode JWT payload."),!1}t.start("Verifying signature...");try{let s=process.env.GENERATESAAS_API_URL??wt,a=await Er(s,{token:r});if(a.valid)t.stop(`${L.green("Valid")} - signature verified`);else return t.stop(`${L.red("Invalid")} - ${a.reason}`),!1}catch{return t.stop(`${L.yellow("Skipped")} - could not reach verification service`),$.log.warn("Signature not verified. Displaying unverified claims:"),xs(i),!1}return xs(i),await Os(t,{token:r,lkh:typeof i.lkh=="string"?i.lkh:void 0,nid:typeof i.nid=="string"?i.nid:void 0,domain:Ns(e)})??!0}function Ns(e){try{return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`).hostname}catch{return}}function Cs(e){e.command("verify").description("Verify a GenerateSaaS license on a deployed site").argument("[url]","URL of the site to verify (e.g. https://example.com or https://example.com/api)").option("--file <path>","file with URLs to check, one per line").action(async(t,r)=>{if(!t&&!r.file&&($.cancel("Provide a URL or --file <path>."),process.exit(1)),r.file){let i=(await $p(r.file,"utf-8")).split(`
1689
- `).map(s=>s.trim()).filter(s=>s&&!s.startsWith("#"));i.length===0&&($.cancel("No URLs found in file."),process.exit(1));let o=0;for(let s of i)await Ds(s)&&o++,$.log.info("");$.log.success(`${o}/${i.length} sites verified.`)}else await Ds(t)||process.exit(1)})}import{existsSync as Gp,rmSync as Kp}from"fs";import*as ie from"@clack/prompts";function Ls(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){Gp(ve)?(Kp(ve),ie.log.success("API key removed.")):ie.log.info("No API key configured.");return}let r=vt();r?ie.log.info(`Current API key: ****${r.slice(-4)}`):ie.log.info("No API key configured.");let n=await kt(),i=ke(n),o=ie.spinner();o.start("Verifying API key...");try{await Te(i),o.stop("API key verified."),Ve(n),ie.log.success("API key saved.")}catch(s){o.stop("Verification failed."),s instanceof F&&s.status===401?ie.cancel("Invalid API key."):ie.cancel(j(s)),process.exit(1)}})}import{existsSync as lr,readFileSync as tn,rmSync as Hp,writeFileSync as $s}from"fs";import{join as Je}from"path";import*as B from"@clack/prompts";var Yp=["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"],Wp=[{file:"packages/api/src/routes/inngest.ts",removals:[`import { licenseHeartbeatFunction } from "../functions/maintenance/license-heartbeat";
1668
+ `),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(`
1669
+ `);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 dn(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 jr(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: ${jr(e.pat)}`,`Last updated: ${jr(e.uat)}`,`Expires: ${jr(e.exp)}`,`Install ID: ${String(e.nid??"unknown")}`].join(`
1670
+ `),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(`
1671
+ `).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 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";
1690
1672
  `,` licenseHeartbeatFunction,
1691
1673
  `]},{file:"packages/api/src/routes/internal/index.ts",removals:[`import licenseRoutes from "./license";
1692
1674
  `,` .route("/license", licenseRoutes)
1693
1675
  `]},{file:"packages/api/src/routes/public/v1/health.ts",removals:[`import { manifest } from "../../../lib/manifest";
1694
- `],replacements:[{from:'c.json({ status: "success" as const, version: manifest?.version })',to:'c.json({ status: "success" as const })'}]}];function zp(){return He.flatMap(e=>zr.map(t=>Je(e,t)))}function en(e){return lr(e)?(Hp(e,{recursive:!0}),!0):!1}function qp(e,t,r=[]){if(!lr(e))return!1;let n=tn(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:($s(e,i,"utf-8"),!0)}function Jp(e){let t=Je(e,".gitignore");if(!lr(t))return!1;let r=tn(t,"utf-8"),n=r.split(`
1676
+ `],replacements:[{from:'c.json({ status: "success" as const, version: manifest?.version })',to:'c.json({ status: "success" as const })'}]}];function Sp(){return Ve.flatMap(e=>Dr.map(t=>Ye(e,t)))}function Mr(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(`
1695
1677
  `).filter(i=>!i.includes(".generatesaas")).join(`
1696
- `);return n===r?!1:($s(t,n,"utf-8"),!0)}function js(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=Je(t,we),n;try{n=JSON.parse(tn(r,"utf-8"))}catch{B.cancel("No GenerateSaaS project found in this directory."),process.exit(1)}let i=await B.text({message:'Type "eject" to confirm (this cannot be undone):',validate:a=>{if(a!=="eject")return'Type "eject" to confirm, or press Ctrl+C to cancel.'}});if(B.isCancel(i)&&(B.cancel("Eject cancelled."),process.exit(0)),n.licenseToken)try{await fetch("https://generatesaas.com/api/v1/heartbeat",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${n.licenseToken}`},body:JSON.stringify({event:"eject",version:n.version,frontend:n.frontend}),signal:AbortSignal.timeout(5e3)}),B.log.info("Recorded the opt-out with generatesaas.com (final event - nothing is sent after this).")}catch{B.log.warn("Could not reach generatesaas.com to record the opt-out. Ejecting anyway.")}let o=[],s=[];for(let a of zp())en(Je(t,a))&&o.push(a);for(let a of Yp)en(Je(t,a))&&o.push(a);en(Je(t,Q))&&o.push(`${Q}/`);for(let a of Wp){let d=Je(t,a.file);qp(d,a.removals,a.replacements)?s.push(a.file):lr(d)&&B.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}Jp(t)&&s.push(".gitignore");for(let a of o)B.log.info(`Deleted ${a}`);for(let a of s)B.log.info(`Modified ${a}`);B.log.success("Ejected successfully. This project is now fully standalone.")})}var Xe=new Xp().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("3.11.0").addHelpText("after",`
1678
+ `);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{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())Mr(Ye(t,a))&&o.push(a);for(let a of kp)Mr(Ye(t,a))&&o.push(a);Mr(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.12.0").addHelpText("after",`
1697
1679
  Examples:
1698
1680
  $ generatesaas init Interactive setup
1699
1681
  $ generatesaas init -n my-app -y Quick setup with defaults
1700
1682
  $ generatesaas status Check for updates
1701
1683
  $ generatesaas auth Set or update API key
1702
- `);bs(Xe);As(Xe);_s(Xe);Cs(Xe);Ls(Xe);js(Xe);Xe.parseAsync().catch(e=>{Us.cancel("An unexpected error occurred."),console.error(e),process.exit(1)});
1684
+ `);Qi(We);os(We);ps(We);fs(We);gs(We);bs(We);We.parseAsync().catch(e=>{vs.cancel("An unexpected error occurred."),console.error(e),process.exit(1)});