generatesaas 1.18.0 → 1.19.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,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import{Command as Fs}from"commander";import*as Jn from"@clack/prompts";import{existsSync as as,readdirSync as cs,rmSync as ls}from"fs";import{join as ps,resolve as ds}from"path";import{Option as B}from"commander";import*as S from"@clack/prompts";import*as Xe from"@clack/prompts";import Rt from"picocolors";function nr(e){let t=e?` GenerateSaaS v${e} `:" GenerateSaaS ";Xe.intro(Rt.bgYellow(Rt.black(t)))}function or(){Xe.outro(Rt.yellow("Happy building!"))}import*as u from"@clack/prompts";import f from"picocolors";var Ue={nextjs:{label:"Next.js",hint:"React 19 + Next.js 16"},nuxt:{label:"Nuxt",hint:"Vue 3 + Nuxt 4"}},je={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)"}},Ze={stripe:{label:"Stripe"},polar:{label:"Polar"},none:{label:"None",hint:"disable payments"}},Qe={smtp:{label:"SMTP",hint:"Mailpit for local dev"},ses:{label:"Amazon SES"},resend:{label:"Resend"}},et={user:{label:"Per user",hint:"each user has their own subscription"},organization:{label:"Per organization",hint:"org subscription shared by members"}},ke={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}},Ve={"claude-code":{label:"Claude Code"},cursor:{label:"Cursor"},codex:{label:"Codex"},"gemini-cli":{label:"Gemini CLI"},windsurf:{label:"Windsurf"}};var pe={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 H={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}},$={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"}]}},Y={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"}]}},Me=[{target:"vercel",provider:"redis",reason:"Vercel serverless cannot maintain persistent Redis connections. Consider Upstash."}],Fe=[{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)."}],ir=["Local file storage (sharp, geoip-lite)","SMTP email (use Resend or SES instead)","Content API git integration"];function tt(e){let t=$[e.databaseProvider].managed,r=Y[e.cacheProvider].managed;return e.dockerServices.some(n=>!(n==="postgres"&&t||n==="redis"&&r))}var q={google:{label:"Google",envVars:[{name:"GOOGLE_CLIENT_ID",secret:!1},{name:"GOOGLE_CLIENT_SECRET",secret:!0}]},github:{label:"GitHub",envVars:[{name:"GITHUB_CLIENT_ID",secret:!1},{name:"GITHUB_CLIENT_SECRET",secret:!0}]},facebook:{label:"Facebook",envVars:[{name:"FACEBOOK_CLIENT_ID",secret:!1},{name:"FACEBOOK_CLIENT_SECRET",secret:!0}]},discord:{label:"Discord",envVars:[{name:"DISCORD_CLIENT_ID",secret:!1},{name:"DISCORD_CLIENT_SECRET",secret:!0}]},x:{label:"X",envVars:[{name:"TWITTER_CLIENT_ID",secret:!1},{name:"TWITTER_CLIENT_SECRET",secret:!0}]}};var Be=["nextjs","nuxt"],rt=["fullstack","separate"],nt=["stripe","polar","none"],ot=["smtp","ses","resend"],it=["postgres","redis","inngest","mailpit"],st=["claude-code","cursor","codex","gemini-cli","windsurf"],at=["user","organization"],de=["USD","EUR","GBP","CAD","AUD","BRL","JPY"],ue=["node","vercel"],me=["postgres","neon","supabase"],fe=["redis","upstash"],ct=["google","github","facebook","discord","x"];function lt(e){return e.split("-").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function pt(e){return/^[a-z][a-z0-9-]*$/.test(e)}function I(e){return e instanceof Error?e.message:String(e)}function w(e){u.isCancel(e)&&(u.cancel("Setup cancelled."),process.exit(0))}function Ot(e){let t=[];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}));for(let r of e.socialProviders){let n=q[r];for(let o of n.envVars)t.push({key:o.name,message:`${o.name} (${n.label}, optional):`,secret:o.secret})}return e.demo&&t.push({key:"TURNSTILE_SECRET_KEY",message:"Cloudflare Turnstile secret key (optional):",secret:!0}),t}async function sr(e,t){let r=!1;u.log.info(f.bold("Project"));let n=e?.projectName??await(async()=>{r=!0;let c=await u.text({message:"Project name:",placeholder:"my-saas",validate:p=>{if(!p?.trim())return"Project name is required.";if(!pt(p))return"Use lowercase letters, numbers, and hyphens only. Must start with a letter."}});return w(c),c})(),o=e?.appName??await(async()=>{r=!0;let c=await u.text({message:"App name:",initialValue:lt(n),validate:p=>{if(!p?.trim())return"App name is required."}});return w(c),c})(),i=e?.projectDir??await(async()=>{r=!0;let c=await u.text({message:"Project location:",initialValue:`./${n}`});return w(c),c==="."?process.cwd():c})(),s=e?.frontend??await(async()=>{r=!0;let c=Object.keys(Ue),p=await u.select({message:"Frontend framework:",options:c.map(v=>({value:v,label:Ue[v].label,hint:Ue[v].hint}))});return w(p),p})();u.log.info(f.bold("Infrastructure"));let a=e?.deploymentTarget??"node";if(e?.deploymentTarget===void 0){r=!0;let c=await u.select({message:"Deployment target:",options:ue.map(p=>({value:p,label:H[p].label,hint:H[p].hint}))});w(c),a=c}let d=e?.architecture?Fe.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 g=new Set(Fe.filter(c=>c.target===a).map(c=>c.architecture)),h=rt.filter(c=>!g.has(c)),m=e?.architecture??await(async()=>{if(h.length===1){let p=h[0];return u.log.info(`Auto-selected ${je[p].label} architecture (only compatible option for ${H[a].label}).`),p}r=!0;let c=await u.select({message:"Architecture:",options:h.map(p=>({value:p,label:je[p].label,hint:je[p].hint}))});return w(c),c})(),_=e?.databaseProvider??await(async()=>{r=!0;let c=me.filter(v=>!Me.some(N=>N.target===a&&N.provider===v));if(c.length===1){let v=c[0];return u.log.info(`Auto-selected ${$[v].label} (only compatible option for ${H[a].label}).`),v}let p=await u.select({message:"Database provider:",options:c.map(v=>({value:v,label:$[v].label,hint:$[v].hint}))});return w(p),p})(),E=e?.cacheProvider??await(async()=>{r=!0;let c=fe.filter(v=>!Me.some(N=>N.target===a&&N.provider===v));if(c.length===1){let v=c[0];return u.log.info(`Auto-selected ${Y[v].label} (only compatible option for ${H[a].label}).`),v}let p=await u.select({message:"Cache provider:",options:c.map(v=>({value:v,label:Y[v].label,hint:Y[v].hint}))});return w(p),p})();if(H[a]?.edgeRuntime){let c=ir.map(p=>` - ${p}`).join(`
3
- `);u.note(c,"Unavailable on edge runtime")}u.log.info(f.bold("Features"));let R=e?.paymentProvider??await(async()=>{r=!0;let c=await u.select({message:"Payment provider:",options:nt.map(p=>({value:p,label:Ze[p].label,hint:Ze[p].hint}))});return w(c),c})(),z=e?.defaultCurrency??await(async()=>{if(R==="none")return"USD";r=!0;let c=await u.select({message:"Default currency:",options:de.map(p=>({value:p,label:p,hint:pe[p].name}))});return w(c),c})(),D=e?.emailProvider??await(async()=>{r=!0;let c=await u.select({message:"Email provider:",options:ot.map(p=>({value:p,label:Qe[p].label,hint:Qe[p].hint}))});return w(c),c})(),ae=e?.multiTenancy??await(async()=>{r=!0;let c=await u.confirm({message:"Enable multi-tenancy (organizations)?",initialValue:!1});return w(c),c})(),ee=e?.billingScope??"user";if(ae&&e?.billingScope===void 0){r=!0;let c=await u.select({message:"Billing scope:",options:at.map(p=>({value:p,label:et[p].label,hint:et[p].hint}))});w(c),ee=c}let b=e?.blog??await(async()=>{r=!0;let c=await u.confirm({message:"Enable blog?",initialValue:!0});return w(c),c})(),y=e?.docs??await(async()=>{r=!0;let c=await u.confirm({message:"Include docs app? (self-hosted Fumadocs documentation site)",initialValue:!1});return w(c),c})(),O=t?.desktopAllowed!==!1,V=e?.desktop??(O?await(async()=>{r=!0;let c=await u.confirm({message:"Include the Electron desktop app? (apps/desktop - cross-platform, device-auth)",initialValue:!1});return w(c),c})():(u.log.info(f.dim("Desktop app: available on the Pro plan and up - skipped.")),!1)),ce=V?e?.desktopAutoRelease??await(async()=>{r=!0;let c=await u.select({message:"Desktop release trigger:",options:[{value:!1,label:"Manual only",hint:"run from the Actions tab (saves CI minutes)"},{value:!0,label:"Automatic",hint:"release on every CI success on main"}],initialValue:!1});return w(c),c})():!1,te=e?.revenueSharing??await(async()=>{r=!0;let c=await u.confirm({message:"Enable revenue sharing? (opt-in MRR leaderboard with dofollow backlinks)",initialValue:!1});return w(c),c})(),Le=R==="none"?!1:e?.credits??await(async()=>{r=!0;let c=await u.confirm({message:"Enable credits? (metered usage on top of subscription plans)",initialValue:!0});return w(c),c})(),We=e?.socialProviders??await(async()=>{r=!0;let c=ct.map(v=>({value:v,label:q[v].label,hint:`requires ${q[v].envVars.map(N=>N.name).join(" / ")}`})),p=await u.multiselect({message:"Which social login providers should the sign-in screen show?",options:c,initialValues:[],required:!1});return w(p),p})();u.log.info(f.bold("Tooling"));let Tt=e?.dockerServices??await(async()=>{r=!0;let c=[...it].filter(L=>L!=="mailpit");D==="smtp"&&c.push("mailpit");let p=c.map(L=>({value:L,label:ke[L].label,hint:ke[L].hint})),v=p.map(L=>L.value).filter(L=>!(L==="postgres"&&(_==="neon"||_==="supabase")||L==="redis"&&E==="upstash")),N=await u.multiselect({message:"Which services should we set up in Docker for you?",options:p,initialValues:v,required:!1});return w(N),N})(),Pt=e?.aiTools??await(async()=>{r=!0;let c=st.map(v=>({value:v,label:Ve[v].label})),p=await u.multiselect({message:"Which AI coding tools do you use?",options:c,initialValues:[],required:!1});return w(p),p})(),_t=e?.demo,qe=Ot({databaseProvider:_,cacheProvider:E,paymentProvider:R,emailProvider:D,socialProviders:We,demo:_t}),$e={};if(qe.length>0&&r){u.log.info(f.bold("Credentials")+f.dim(" all optional - press Enter to skip, fill in .env later"));for(let c of qe)if(r=!0,c.secret){let p=await u.password({message:c.message,mask:"*"});w(p),typeof p=="string"&&p.trim()&&($e[c.key]=p.trim())}else{let p=await u.text({message:c.message,placeholder:c.placeholder});w(p),typeof p=="string"&&p.trim()&&($e[c.key]=p.trim())}}if(r){let c=[` Name: ${f.cyan(n)}`,` App name: ${f.cyan(o)}`,` Location: ${f.cyan(i)}`,` Frontend: ${f.cyan(Ue[s].label)}`,` Architecture: ${f.cyan(je[m].label)}`].join(`
4
- `),p=[` Deploy target: ${f.cyan(H[a]?.label??"Node.js / Docker")}`,` Database: ${f.cyan($[_].label)}`,` Cache: ${f.cyan(Y[E].label)}`,Tt.length>0?` Docker: ${f.cyan(Tt.map(le=>ke[le].label).join(", "))}`:` Docker: ${f.dim("none")}`].filter(Boolean).join(`
5
- `),v=[R!=="none"?` Payment: ${f.cyan(Ze[R].label)} (${z})`:` Payment: ${f.dim("none")}`,` Credits: ${Le?f.cyan("Yes"):f.dim("No")}`,` Email: ${f.cyan(Qe[D].label)}`,` Multi-tenancy: ${ae?f.cyan("Yes")+` (billing: ${et[ee].label})`:f.dim("No")}`,` Blog: ${b?f.cyan("Yes"):f.dim("No")}`,` Docs app: ${y?f.cyan("Yes"):f.dim("No")}`,` Desktop app: ${V?f.cyan("Yes"):f.dim("No")}`,...V?[` Releases: ${f.cyan(ce?"Automatic on CI":"Manual only")}`]:[],` Rev. sharing: ${te?f.cyan("Yes"):f.dim("No")}`,We.length>0?` Social login: ${f.cyan(We.map(le=>q[le].label).join(", "))}`:` Social login: ${f.dim("none")}`,Pt.length>0?` AI tools: ${f.cyan(Pt.map(le=>Ve[le].label).join(", "))}`:` AI tools: ${f.dim("none")}`].join(`
6
- `),N=[f.bold("Project"),c,"",f.bold("Infrastructure"),p,"",f.bold("Features"),v];if(qe.length>0){let le=qe.map(rr=>{let Wn=$e[rr.key]?f.green("provided"):f.dim("skipped");return` ${rr.key}: ${Wn}`}).join(`
7
- `);N.push("",f.bold("Credentials"),le)}u.note(N.join(`
8
- `),"Summary");let L=await u.confirm({message:"Proceed with these settings?"});(u.isCancel(L)||!L)&&(u.cancel("Setup cancelled."),process.exit(0))}return{projectName:n,appName:o,projectDir:i,frontend:s,architecture:m,deploymentTarget:a,databaseProvider:_,cacheProvider:E,paymentProvider:R,emailProvider:D,multiTenancy:ae,billingScope:ee,blog:b,docs:y,desktop:V,desktopAutoRelease:ce,revenueSharing:te,credits:Le,dockerServices:Tt,aiTools:Pt,socialProviders:We,defaultCurrency:z,...Object.keys($e).length>0?{credentials:$e}:{},...e?.baseUrl!==void 0?{baseUrl:e.baseUrl}:{},..._t!==void 0?{demo:_t}:{}}}import{createReadStream as oo}from"fs";import{mkdir as io}from"fs/promises";import{Readable as so}from"stream";import{pipeline as fr}from"stream/promises";import{extract as ao}from"tar";import{join as ge}from"path";import{homedir as qn}from"os";var Ge=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",M=".generatesaas",re=ge(M,"manifest.json"),ar=ge(M,"hashes.json"),dt=ge(M,"template-hashes.json"),ut=ge(M,"template"),cr=ge(M,"staging"),lr=ge(M,"staging.json"),X=ge(qn(),".generatesaas");var x=class extends Error{constructor(r,n,o){super(n);this.status=r;this.body=o}status;body;name="ApiError"};function Z(e){return{apiKey:e,baseUrl:Ge}}async function ne(e,t,r){let n=`${e.baseUrl}${t}`,o=await fetch(n,{...r,headers:{...r?.headers,Authorization:`Bearer ${e.apiKey}`,"User-Agent":"generatesaas-cli"}});if(!o.ok){let i,s;try{s=await o.json(),i=s.error??`API ${o.status}: ${t}`}catch{i=`API ${o.status}: ${t}`}throw new x(o.status,i,s)}return o}import{existsSync as Xn,readFileSync as Zn,writeFileSync as Qn,mkdirSync as eo}from"fs";import{dirname as to}from"path";import*as oe from"@clack/prompts";function Ke(){if(!Xn(X))return null;try{let e=JSON.parse(Zn(X,"utf-8"));return e.apiKey?e.apiKey:(e.token&&!e.apiKey&&oe.log.warning(`Found old GitHub token in ${X}. Run 'generatesaas init' to set up your API key.`),null)}catch{return null}}function he(e){eo(to(X),{recursive:!0}),Qn(X,JSON.stringify({apiKey:e},null," ")+`
9
- `,{mode:384})}async function Ae(e){if(e?.apiKey)return e.apiKey;let t=process.env.GENERATESAAS_API_KEY;if(t)return t;let r=Ke();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 ze()}async function ze(){let e=await oe.text({message:"Enter your GenerateSaaS API key:",placeholder:"gs_live_...",validate:t=>{if(!t?.trim())return"API key is required."}});return oe.isCancel(e)&&(oe.cancel("Setup cancelled."),process.exit(0)),e.trim()}async function ie(e){return 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 ne(e,"/versions")).json()}async function xt(e,t){try{return await(await ne(e,`/changelog/${encodeURIComponent(t)}`)).text()}catch(r){if(r instanceof x&&r.status===404)return null;throw r}}async function pr(e,t){return await(await ne(e,`/skill/${encodeURIComponent(t)}`)).json()}function mt(e){if(!(e instanceof x)||e.status!==403)return null;let t=e.body;return!t||t.code!=="update_window_expired"?null:{message:e.message,lastAllowedVersion:t.lastAllowedVersion??null}}function dr(e){return{frontend:e.frontend,architecture:e.architecture,deployTarget:e.deploymentTarget,database:e.databaseProvider,cache:e.cacheProvider,payment:e.paymentProvider,email:e.emailProvider,multiTenancy:e.multiTenancy,billingScope:e.billingScope,blog:e.blog,docs:e.docs,desktop:e.desktop,credits:e.credits,revenueSharing:e.revenueSharing,socialProviders:e.socialProviders,aiTools:e.aiTools,currency:e.defaultCurrency}}async function Dt(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:"offline-test-token",licenseId:"offline-test-license-id"}:await(await ne(e,"/license/sign",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function ur(e,t){return await(await ne(e,"/license/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function Ct(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 mr(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 o=await n.json().catch(()=>null);throw new Error(o?.error??`Inspect endpoint returned ${n.status}`)}return await n.json()}var Nt=new Set([".git","node_modules",".pnpm-store",".env",".env.test",".turbo",".nuxt",".output",".data","dist",".next",".svelte-kit",".wrangler",".devcontainer","playwright-report","test-results"]),Lt=new Set(["pnpm-lock.yaml"]);function Ie(e){if($t(e))return!0;for(let t of e.split("/"))if(Lt.has(t))return!0;return!1}var ro=new Set(["data","mksaas","references","scripts",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","README.md","TODO.md","OVERVIEW.md"]),no=["docs/superpowers","packages/cli","packages/cli-api","infra/docker-compose.yml",".claude/commands",".claude/skills/web-next-port",".claude/skills/web-next-port-workspace",".claude/settings.local.json",".claude/worktrees"];function $t(e){let t=e.split("/");for(let r of t)if(Nt.has(r))return!0;if(ro.has(t[0]))return!0;for(let r of no)if(e===r||e.startsWith(r+"/"))return!0;return!1}async function ft(e,t,r){await io(r,{recursive:!0});let n=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(n){await fr(oo(n),gr(r));return}let o=await ne(e,`/template/${encodeURIComponent(t)}`);if(!o.body)throw new Error("Empty response body");let i=so.fromWeb(o.body);await fr(i,gr(r))}function gr(e){return ao({cwd:e,strip:1,filter:t=>{let r=t.replace(/^[^/]+\//,"");return r?!$t(r):!0},sync:!1})}import{readdir as co,readFile as Ut,rm as yr,writeFile as jt}from"fs/promises";import{join as Te}from"path";var lo=["apps/web-nuxt/public/images/blog","apps/web-next/public/images/blog","packages/content/en/blog","packages/content/ro/blog"];async function vr(e){await Promise.all(lo.map(t=>yr(Te(e,t),{recursive:!0,force:!0})))}var po="packages/config/src/blog.ts";async function Sr(e){let t=Te(e,po),r=await Ut(t,"utf-8"),n=hr(hr(r,"blogCategories",t),"blogAuthors",t);await jt(t,n)}function hr(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 Er(e){let t=Te(e,"packages/i18n/translations"),r;try{r=await co(t)}catch{return}for(let n of r){let o=Te(t,n,"web.json"),i;try{i=await Ut(o,"utf-8")}catch{continue}let s=JSON.parse(i);!s.blog||s.blog.categories===void 0||(s.blog.categories={},await jt(o,JSON.stringify(s,null," ")+`
10
- `))}}async function wr(e,t){t.includes("claude-code")||await yr(Te(e,".claude"),{recursive:!0,force:!0})}async function br(e,t){let r=Te(e,".claude","settings.json"),n;try{n=await Ut(r,"utf8")}catch{return}let o=JSON.parse(n);delete o.alwaysThinkingEnabled,delete o.enableAllProjectMcpServers,o.env&&(delete o.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS,Object.keys(o.env).length===0&&delete o.env),o.permissions?.allow&&(o.permissions.allow=o.permissions.allow.filter(i=>uo(i,t))),await jt(r,JSON.stringify(o,null," ")+`
11
- `)}function uo(e,t){return!(e.startsWith("mcp__")||t!=="nuxt"&&e.includes("nuxt"))}import{join as kr}from"path";import{mkdir as mo,readdir as fo,rm as go,rmdir as ho,writeFile as yo}from"fs/promises";import{dirname as gt,join as vo,relative as So,sep as Eo}from"path";function ye(e){return e.split(Eo).join("/")}async function ht(e){await mo(e,{recursive:!0})}async function l(e,t){await ht(gt(e)),await yo(e,t,"utf-8")}async function Vt(e,t){await go(e,{force:!0});let r=gt(e);for(;r!==t&&r!==gt(r);){try{await ho(r)}catch{return}r=gt(r)}}async function ve(e,t,r){let n=[],o=await fo(e,{withFileTypes:!0});for(let i of o){let s=vo(e,i.name),a=ye(So(t,s));r(a)||(i.isDirectory()?n.push(...await ve(s,t,r)):i.isFile()&&n.push(s))}return n}var wo={postgres:"Postgres",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"},bo={resend:"Resend",ses:"Amazon SES",smtp:"SMTP"},ko={redis:"Redis",upstash:"Upstash Redis"},Ao={node:"Node.js / Docker",vercel:"Vercel"},Io={stripe:"Stripe",polar:"Polar"};function To(e){let t=e.frontend==="nuxt",r=t?"Nuxt 4":"Next.js 16",n=t?"apps/web-nuxt":"apps/web-next",o=t?"`app/` pages + components + composables":"`app/` routes, `components/`, `lib/`",i=e.architecture==="fullstack",s=i?"(fullstack - Hono API mounted inside the app)":"(separate - standalone Hono backend)",a=i?`Mounted inside \`${n}\`. 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.",d=ko[e.cacheProvider],g=Ao[e.deploymentTarget],h=e.paymentProvider==="none"?"":`
12
- - **Payments:** ${Io[e.paymentProvider]}`,m=t?"`$t('key')` in templates (global helper); `useI18n()` from `vue-i18n` in `<script setup>` for `locale`/`setLocale`/`t()`.":"`useTranslations()` from `next-intl` in components; messages are loaded in `apps/web-next/i18n/request.ts`.",_=t?"**Navigation:** always use `localePath()` for paths (hardcoded paths break non-default locales); `await navigateTo()` in SSR.":"**Navigation:** `next/link` for links; `redirect()` from `next/navigation` for programmatic redirects in Server Components.";return`# AGENTS.md
2
+ import{Command as Zs}from"commander";import*as ro from"@clack/prompts";import{existsSync as hs,readdirSync as ys,rmSync as vs}from"fs";import{join as Ss,resolve as Es}from"path";import{Option as Y}from"commander";import*as k from"@clack/prompts";import*as at from"@clack/prompts";import Dt from"picocolors";function ar(e){let t=e?` GenerateSaaS v${e} `:" GenerateSaaS ";at.intro(Dt.bgYellow(Dt.black(t)))}function cr(){at.outro(Dt.yellow("Happy building!"))}import*as g from"@clack/prompts";import h from"picocolors";var qe={nextjs:{label:"Next.js",hint:"React 19 + Next.js 16"},nuxt:{label:"Nuxt",hint:"Vue 3 + Nuxt 4"}},Je={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"}},pt={user:{label:"Per user",hint:"each user has their own subscription"},organization:{label:"Per organization",hint:"org subscription shared by members"}},Te={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}},We={"claude-code":{label:"Claude Code"},cursor:{label:"Cursor"},codex:{label:"Codex"},"gemini-cli":{label:"Gemini CLI"},windsurf:{label:"Windsurf"}};var he={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 Q={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}},V={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"}]}},ee={redis:{label:"Redis (self-hosted)",hint:"local Docker, ioredis",managed:!1,envVars:[{key:"REDIS_URL",defaultValue:"redis://localhost:6379"}]},upstash:{label:"Upstash",hint:"serverless Redis",managed:!0,envVars:[{key:"UPSTASH_REDIS_REST_URL",comment:"# TODO: Add your Upstash REST URL"},{key:"UPSTASH_REDIS_REST_TOKEN",comment:"# TODO: Add your Upstash REST token"}]}},Xe=[{target:"vercel",provider:"redis",reason:"Vercel serverless cannot maintain persistent Redis connections. Consider Upstash."}],Ze=[{architecture:"separate",target:"vercel",reason:"Standalone backends need a long-running runtime. Vercel's per-function TypeScript check conflicts with pnpm's isolated install. Use --architecture fullstack for serverless, or deploy the standalone backend to a long-running runtime (Render, Fly.io, Railway, Coolify, Dokploy, or your own VPS via Docker)."}],lr=["Local file storage (sharp, geoip-lite)","SMTP email (use Resend or SES instead)","Content API git integration"];function dt(e){let t=V[e.databaseProvider].managed,r=ee[e.cacheProvider].managed;return e.dockerServices.some(n=>!(n==="postgres"&&t||n==="redis"&&r))}var ne={google:{label:"Google",envVars:[{name:"GOOGLE_CLIENT_ID",secret:!1},{name:"GOOGLE_CLIENT_SECRET",secret:!0}]},github:{label:"GitHub",envVars:[{name:"GITHUB_CLIENT_ID",secret:!1},{name:"GITHUB_CLIENT_SECRET",secret:!0}]},facebook:{label:"Facebook",envVars:[{name:"FACEBOOK_CLIENT_ID",secret:!1},{name:"FACEBOOK_CLIENT_SECRET",secret:!0}]},discord:{label:"Discord",envVars:[{name:"DISCORD_CLIENT_ID",secret:!1},{name:"DISCORD_CLIENT_SECRET",secret:!0}]},x:{label:"X",envVars:[{name:"TWITTER_CLIENT_ID",secret:!1},{name:"TWITTER_CLIENT_SECRET",secret:!0}]}};var ye=["nextjs","nuxt"],Re=["fullstack","separate"],_e=["stripe","polar","none"],Oe=["smtp","ses","resend"],xe=["postgres","redis","inngest","mailpit"],De=["claude-code","cursor","codex","gemini-cli","windsurf"],Ce=["user","organization"],oe=["USD","EUR","GBP","CAD","AUD","BRL","JPY"],ie=["node","vercel"],se=["postgres","neon","supabase"],ae=["redis","upstash"],Ne=["google","github","facebook","discord","x"];function ut(e){return e.split("-").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function mt(e){return/^[a-z][a-z0-9-]*$/.test(e)}function O(e){return e instanceof Error?e.message:String(e)}function I(e){g.isCancel(e)&&(g.cancel("Setup cancelled."),process.exit(0))}function Ct(e){let t=[];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}));for(let r of e.socialProviders){let n=ne[r];for(let o of n.envVars)t.push({key:o.name,message:`${o.name} (${n.label}, optional):`,secret:o.secret})}return e.demo&&t.push({key:"TURNSTILE_SECRET_KEY",message:"Cloudflare Turnstile secret key (optional):",secret:!0}),t}async function pr(e,t){let r=!1;g.log.info(h.bold("Project"));let n=e?.projectName??await(async()=>{r=!0;let c=await g.text({message:"Project name:",placeholder:"my-saas",validate:p=>{if(!p?.trim())return"Project name is required.";if(!mt(p))return"Use lowercase letters, numbers, and hyphens only. Must start with a letter."}});return I(c),c})(),o=e?.appName??await(async()=>{r=!0;let c=await g.text({message:"App name:",initialValue:ut(n),validate:p=>{if(!p?.trim())return"App name is required."}});return I(c),c})(),i=e?.projectDir??await(async()=>{r=!0;let c=await g.text({message:"Project location:",initialValue:`./${n}`});return I(c),c==="."?process.cwd():c})(),s=e?.frontend??await(async()=>{r=!0;let c=Object.keys(qe),p=await g.select({message:"Frontend framework:",options:c.map(E=>({value:E,label:qe[E].label,hint:qe[E].hint}))});return I(p),p})();g.log.info(h.bold("Infrastructure"));let a=e?.deploymentTarget??"node";if(e?.deploymentTarget===void 0){r=!0;let c=await g.select({message:"Deployment target:",options:ie.map(p=>({value:p,label:Q[p].label,hint:Q[p].hint}))});I(c),a=c}let d=e?.architecture?Ze.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 u=new Set(Ze.filter(c=>c.target===a).map(c=>c.architecture)),m=Re.filter(c=>!u.has(c)),f=e?.architecture??await(async()=>{if(m.length===1){let p=m[0];return g.log.info(`Auto-selected ${Je[p].label} architecture (only compatible option for ${Q[a].label}).`),p}r=!0;let c=await g.select({message:"Architecture:",options:m.map(p=>({value:p,label:Je[p].label,hint:Je[p].hint}))});return I(c),c})(),v=e?.databaseProvider??await(async()=>{r=!0;let c=se.filter(E=>!Xe.some(U=>U.target===a&&U.provider===E));if(c.length===1){let E=c[0];return g.log.info(`Auto-selected ${V[E].label} (only compatible option for ${Q[a].label}).`),E}let p=await g.select({message:"Database provider:",options:c.map(E=>({value:E,label:V[E].label,hint:V[E].hint}))});return I(p),p})(),b=e?.cacheProvider??await(async()=>{r=!0;let c=ae.filter(E=>!Xe.some(U=>U.target===a&&U.provider===E));if(c.length===1){let E=c[0];return g.log.info(`Auto-selected ${ee[E].label} (only compatible option for ${Q[a].label}).`),E}let p=await g.select({message:"Cache provider:",options:c.map(E=>({value:E,label:ee[E].label,hint:ee[E].hint}))});return I(p),p})();if(Q[a]?.edgeRuntime){let c=lr.map(p=>` - ${p}`).join(`
3
+ `);g.note(c,"Unavailable on edge runtime")}g.log.info(h.bold("Features"));let P=e?.paymentProvider??await(async()=>{r=!0;let c=await g.select({message:"Payment provider:",options:_e.map(p=>({value:p,label:ct[p].label,hint:ct[p].hint}))});return I(c),c})(),B=e?.defaultCurrency??await(async()=>{if(P==="none")return"USD";r=!0;let c=await g.select({message:"Default currency:",options:oe.map(p=>({value:p,label:p,hint:he[p].name}))});return I(c),c})(),C=e?.emailProvider??await(async()=>{r=!0;let c=await g.select({message:"Email provider:",options:Oe.map(p=>({value:p,label:lt[p].label,hint:lt[p].hint}))});return I(c),c})(),W=e?.multiTenancy??await(async()=>{r=!0;let c=await g.confirm({message:"Enable multi-tenancy (organizations)?",initialValue:!1});return I(c),c})(),X=e?.billingScope??"user";if(W&&e?.billingScope===void 0){r=!0;let c=await g.select({message:"Billing scope:",options:Ce.map(p=>({value:p,label:pt[p].label,hint:pt[p].hint}))});I(c),X=c}let Pe=e?.blog??await(async()=>{r=!0;let c=await g.confirm({message:"Enable blog?",initialValue:!0});return I(c),c})(),S=e?.docs??await(async()=>{r=!0;let c=await g.confirm({message:"Include docs app? (self-hosted Fumadocs documentation site)",initialValue:!1});return I(c),c})(),T=t?.desktopAllowed!==!1,N=e?.desktop??(T?await(async()=>{r=!0;let c=await g.confirm({message:"Include the Electron desktop app? (apps/desktop - cross-platform, device-auth)",initialValue:!1});return I(c),c})():(g.log.info(h.dim("Desktop app: available on the Pro plan and up - skipped.")),!1)),G=N?e?.desktopAutoRelease??await(async()=>{r=!0;let c=await g.select({message:"Desktop release trigger:",options:[{value:!1,label:"Manual only",hint:"run from the Actions tab (saves CI minutes)"},{value:!0,label:"Automatic",hint:"release on every CI success on main"}],initialValue:!1});return I(c),c})():!1,A=e?.revenueSharing??await(async()=>{r=!0;let c=await g.confirm({message:"Enable revenue sharing? (opt-in MRR leaderboard with dofollow backlinks)",initialValue:!1});return I(c),c})(),Z=P==="none"?!1:e?.credits??await(async()=>{r=!0;let c=await g.confirm({message:"Enable credits? (metered usage on top of subscription plans)",initialValue:!0});return I(c),c})(),it=e?.socialProviders??await(async()=>{r=!0;let c=Ne.map(E=>({value:E,label:ne[E].label,hint:`requires ${ne[E].envVars.map(U=>U.name).join(" / ")}`})),p=await g.multiselect({message:"Which social login providers should the sign-in screen show?",options:c,initialValues:[],required:!1});return I(p),p})();g.log.info(h.bold("Tooling"));let _t=e?.dockerServices??await(async()=>{r=!0;let c=[...xe].filter(M=>M!=="mailpit");C==="smtp"&&c.push("mailpit");let p=c.map(M=>({value:M,label:Te[M].label,hint:Te[M].hint})),E=p.map(M=>M.value).filter(M=>!(M==="postgres"&&(v==="neon"||v==="supabase")||M==="redis"&&b==="upstash")),U=await g.multiselect({message:"Which services should we set up in Docker for you?",options:p,initialValues:E,required:!1});return I(U),U})(),Ot=e?.aiTools??await(async()=>{r=!0;let c=De.map(E=>({value:E,label:We[E].label})),p=await g.multiselect({message:"Which AI coding tools do you use?",options:c,initialValues:[],required:!1});return I(p),p})(),xt=e?.demo,st=Ct({databaseProvider:v,cacheProvider:b,paymentProvider:P,emailProvider:C,socialProviders:it,demo:xt}),Ye={};if(st.length>0&&r){g.log.info(h.bold("Credentials")+h.dim(" all optional - press Enter to skip, fill in .env later"));for(let c of st)if(r=!0,c.secret){let p=await g.password({message:c.message,mask:"*"});I(p),typeof p=="string"&&p.trim()&&(Ye[c.key]=p.trim())}else{let p=await g.text({message:c.message,placeholder:c.placeholder});I(p),typeof p=="string"&&p.trim()&&(Ye[c.key]=p.trim())}}if(r){let c=[` Name: ${h.cyan(n)}`,` App name: ${h.cyan(o)}`,` Location: ${h.cyan(i)}`,` Frontend: ${h.cyan(qe[s].label)}`,` Architecture: ${h.cyan(Je[f].label)}`].join(`
4
+ `),p=[` Deploy target: ${h.cyan(Q[a]?.label??"Node.js / Docker")}`,` Database: ${h.cyan(V[v].label)}`,` Cache: ${h.cyan(ee[b].label)}`,_t.length>0?` Docker: ${h.cyan(_t.map(ge=>Te[ge].label).join(", "))}`:` Docker: ${h.dim("none")}`].filter(Boolean).join(`
5
+ `),E=[P!=="none"?` Payment: ${h.cyan(ct[P].label)} (${B})`:` Payment: ${h.dim("none")}`,` Credits: ${Z?h.cyan("Yes"):h.dim("No")}`,` Email: ${h.cyan(lt[C].label)}`,` Multi-tenancy: ${W?h.cyan("Yes")+` (billing: ${pt[X].label})`:h.dim("No")}`,` Blog: ${Pe?h.cyan("Yes"):h.dim("No")}`,` Docs app: ${S?h.cyan("Yes"):h.dim("No")}`,` Desktop app: ${N?h.cyan("Yes"):h.dim("No")}`,...N?[` Releases: ${h.cyan(G?"Automatic on CI":"Manual only")}`]:[],` Rev. sharing: ${A?h.cyan("Yes"):h.dim("No")}`,it.length>0?` Social login: ${h.cyan(it.map(ge=>ne[ge].label).join(", "))}`:` Social login: ${h.dim("none")}`,Ot.length>0?` AI tools: ${h.cyan(Ot.map(ge=>We[ge].label).join(", "))}`:` AI tools: ${h.dim("none")}`].join(`
6
+ `),U=[h.bold("Project"),c,"",h.bold("Infrastructure"),p,"",h.bold("Features"),E];if(st.length>0){let ge=st.map(sr=>{let no=Ye[sr.key]?h.green("provided"):h.dim("skipped");return` ${sr.key}: ${no}`}).join(`
7
+ `);U.push("",h.bold("Credentials"),ge)}g.note(U.join(`
8
+ `),"Summary");let M=await g.confirm({message:"Proceed with these settings?"});(g.isCancel(M)||!M)&&(g.cancel("Setup cancelled."),process.exit(0))}return{projectName:n,appName:o,projectDir:i,frontend:s,architecture:f,deploymentTarget:a,databaseProvider:v,cacheProvider:b,paymentProvider:P,emailProvider:C,multiTenancy:W,billingScope:X,blog:Pe,docs:S,desktop:N,desktopAutoRelease:G,desktopAi:N?e?.desktopAi??!1:!1,revenueSharing:A,credits:Z,dockerServices:_t,aiTools:Ot,socialProviders:it,defaultCurrency:B,...Object.keys(Ye).length>0?{credentials:Ye}:{},...e?.baseUrl!==void 0?{baseUrl:e.baseUrl}:{},...xt!==void 0?{demo:xt}:{}}}import{createReadStream as mo}from"fs";import{mkdir as fo}from"fs/promises";import{Readable as go}from"stream";import{pipeline as vr}from"stream/promises";import{extract as ho}from"tar";import{join as ve}from"path";import{homedir as oo}from"os";var Qe=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",K=".generatesaas",pe=ve(K,"manifest.json"),dr=ve(K,"hashes.json"),ft=ve(K,"template-hashes.json"),gt=ve(K,"template"),ur=ve(K,"staging"),mr=ve(K,"staging.json"),ce=ve(oo(),".generatesaas");var L=class extends Error{constructor(r,n,o){super(n);this.status=r;this.body=o}status;body;name="ApiError"};function le(e){return{apiKey:e,baseUrl:Qe}}async function de(e,t,r){let n=`${e.baseUrl}${t}`,o=await fetch(n,{...r,headers:{...r?.headers,Authorization:`Bearer ${e.apiKey}`,"User-Agent":"generatesaas-cli"}});if(!o.ok){let i,s;try{s=await o.json(),i=s.error??`API ${o.status}: ${t}`}catch{i=`API ${o.status}: ${t}`}throw new L(o.status,i,s)}return o}import{existsSync as io,readFileSync as so,writeFileSync as ao,mkdirSync as co}from"fs";import{dirname as lo}from"path";import*as ue from"@clack/prompts";function et(){if(!io(ce))return null;try{let e=JSON.parse(so(ce,"utf-8"));return e.apiKey?e.apiKey:(e.token&&!e.apiKey&&ue.log.warning(`Found old GitHub token in ${ce}. Run 'generatesaas init' to set up your API key.`),null)}catch{return null}}function Se(e){co(lo(ce),{recursive:!0}),ao(ce,JSON.stringify({apiKey:e},null," ")+`
9
+ `,{mode:384})}async function Le(e){if(e?.apiKey)return e.apiKey;let t=process.env.GENERATESAAS_API_KEY;if(t)return t;let r=et();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 tt()}async function tt(){let e=await ue.text({message:"Enter your GenerateSaaS API key:",placeholder:"gs_live_...",validate:t=>{if(!t?.trim())return"API key is required."}});return ue.isCancel(e)&&(ue.cancel("Setup cancelled."),process.exit(0)),e.trim()}async function me(e){return 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 de(e,"/versions")).json()}async function Nt(e,t){try{return await(await de(e,`/changelog/${encodeURIComponent(t)}`)).text()}catch(r){if(r instanceof L&&r.status===404)return null;throw r}}async function fr(e,t){return await(await de(e,`/skill/${encodeURIComponent(t)}`)).json()}function ht(e){if(!(e instanceof L)||e.status!==403)return null;let t=e.body;return!t||t.code!=="update_window_expired"?null:{message:e.message,lastAllowedVersion:t.lastAllowedVersion??null}}function gr(e){return{frontend:e.frontend,architecture:e.architecture,deployTarget:e.deploymentTarget,database:e.databaseProvider,cache:e.cacheProvider,payment:e.paymentProvider,email:e.emailProvider,multiTenancy:e.multiTenancy,billingScope:e.billingScope,blog:e.blog,docs:e.docs,desktop:e.desktop,credits:e.credits,revenueSharing:e.revenueSharing,socialProviders:e.socialProviders,aiTools:e.aiTools,currency:e.defaultCurrency}}async function Lt(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:"offline-test-token",licenseId:"offline-test-license-id",installId:t.installId}:await(await de(e,"/license/sign",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function hr(e,t){return await(await de(e,"/license/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function $t(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 yr(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 o=await n.json().catch(()=>null);throw new Error(o?.error??`Inspect endpoint returned ${n.status}`)}return await n.json()}var jt=new Set([".git","node_modules",".pnpm-store",".env",".env.test",".turbo",".nuxt",".output",".data","dist",".next",".svelte-kit",".wrangler",".devcontainer","playwright-report","test-results"]),Ut=new Set(["pnpm-lock.yaml"]);function $e(e){if(Mt(e))return!0;for(let t of e.split("/"))if(Ut.has(t))return!0;return!1}var po=new Set(["data","mksaas","references","scripts",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","README.md","TODO.md","OVERVIEW.md"]),uo=["docs/superpowers","packages/cli","packages/cli-api","infra/docker-compose.yml",".claude/commands",".claude/skills/web-next-port",".claude/skills/web-next-port-workspace",".claude/settings.local.json",".claude/worktrees"];function Mt(e){let t=e.split("/");for(let r of t)if(jt.has(r))return!0;if(po.has(t[0]))return!0;for(let r of uo)if(e===r||e.startsWith(r+"/"))return!0;return!1}async function yt(e,t,r){await fo(r,{recursive:!0});let n=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(n){await vr(mo(n),Sr(r));return}let o=await de(e,`/template/${encodeURIComponent(t)}`);if(!o.body)throw new Error("Empty response body");let i=go.fromWeb(o.body);await vr(i,Sr(r))}function Sr(e){return ho({cwd:e,strip:1,filter:t=>{let r=t.replace(/^[^/]+\//,"");return r?!Mt(r):!0},sync:!1})}import{readdir as yo,readFile as Vt,rm as kr,writeFile as Ft}from"fs/promises";import{join as je}from"path";var vo=["apps/web-nuxt/public/images/blog","apps/web-next/public/images/blog","packages/content/en/blog","packages/content/ro/blog"];async function br(e){await Promise.all(vo.map(t=>kr(je(e,t),{recursive:!0,force:!0})))}var So="packages/config/src/blog.ts";async function wr(e){let t=je(e,So),r=await Vt(t,"utf-8"),n=Er(Er(r,"blogCategories",t),"blogAuthors",t);await Ft(t,n)}function Er(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 Ar(e){let t=je(e,"packages/i18n/translations"),r;try{r=await yo(t)}catch{return}for(let n of r){let o=je(t,n,"web.json"),i;try{i=await Vt(o,"utf-8")}catch{continue}let s=JSON.parse(i);!s.blog||s.blog.categories===void 0||(s.blog.categories={},await Ft(o,JSON.stringify(s,null," ")+`
10
+ `))}}async function Ir(e,t){t.includes("claude-code")||await kr(je(e,".claude"),{recursive:!0,force:!0})}async function Pr(e,t){let r=je(e,".claude","settings.json"),n;try{n=await Vt(r,"utf8")}catch{return}let o=JSON.parse(n);delete o.alwaysThinkingEnabled,delete o.enableAllProjectMcpServers,o.env&&(delete o.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS,Object.keys(o.env).length===0&&delete o.env),o.permissions?.allow&&(o.permissions.allow=o.permissions.allow.filter(i=>Eo(i,t))),await Ft(r,JSON.stringify(o,null," ")+`
11
+ `)}function Eo(e,t){return!(e.startsWith("mcp__")||t!=="nuxt"&&e.includes("nuxt"))}import{join as Tr}from"path";import{mkdir as ko,readdir as bo,rm as wo,rmdir as Ao,writeFile as Io}from"fs/promises";import{dirname as vt,join as Po,relative as To,sep as Ro}from"path";function Ee(e){return e.split(Ro).join("/")}async function St(e){await ko(e,{recursive:!0})}async function l(e,t){await St(vt(e)),await Io(e,t,"utf-8")}async function Bt(e,t){await wo(e,{force:!0});let r=vt(e);for(;r!==t&&r!==vt(r);){try{await Ao(r)}catch{return}r=vt(r)}}async function ke(e,t,r){let n=[],o=await bo(e,{withFileTypes:!0});for(let i of o){let s=Po(e,i.name),a=Ee(To(t,s));r(a)||(i.isDirectory()?n.push(...await ke(s,t,r)):i.isFile()&&n.push(s))}return n}var _o={postgres:"Postgres",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"},Oo={resend:"Resend",ses:"Amazon SES",smtp:"SMTP"},xo={redis:"Redis",upstash:"Upstash Redis"},Do={node:"Node.js / Docker",vercel:"Vercel"},Co={stripe:"Stripe",polar:"Polar"};function No(e){let t=e.frontend==="nuxt",r=t?"Nuxt 4":"Next.js 16",n=t?"apps/web-nuxt":"apps/web-next",o=t?"`app/` pages + components + composables":"`app/` routes, `components/`, `lib/`",i=e.architecture==="fullstack",s=i?"(fullstack - Hono API mounted inside the app)":"(separate - standalone Hono backend)",a=i?`Mounted inside \`${n}\`. 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.",d=xo[e.cacheProvider],u=Do[e.deploymentTarget],m=e.paymentProvider==="none"?"":`
12
+ - **Payments:** ${Co[e.paymentProvider]}`,f=t?"`$t('key')` in templates (global helper); `useI18n()` from `vue-i18n` in `<script setup>` for `locale`/`setLocale`/`t()`.":"`useTranslations()` from `next-intl` in components; messages are loaded in `apps/web-next/i18n/request.ts`.",v=t?"**Navigation:** always use `localePath()` for paths (hardcoded paths break non-default locales); `await navigateTo()` in SSR.":"**Navigation:** `next/link` for links; `redirect()` from `next/navigation` for programmatic redirects in Server Components.";return`# AGENTS.md
13
13
 
14
14
  Guidelines for AI coding agents (Claude Code, Codex, Cursor, \u2026) in this project.
15
15
  Keep this file tight: add a rule only when it prevents a recurring mistake - too
@@ -37,11 +37,11 @@ flag, route, or translation is the most common and most costly mistake in this r
37
37
 
38
38
  - **Frontend:** ${r} ${s}
39
39
  - **API:** Hono, RPC-typed. ${a}
40
- - **Database:** Drizzle ORM + ${wo[e.databaseProvider]}
40
+ - **Database:** Drizzle ORM + ${_o[e.databaseProvider]}
41
41
  - **Cache + jobs:** ${d} + Inngest
42
- - **Auth:** Better Auth${h}
43
- - **Email:** ${bo[e.emailProvider]}
44
- - **Deploy:** ${g}
42
+ - **Auth:** Better Auth${m}
43
+ - **Email:** ${Oo[e.emailProvider]}
44
+ - **Deploy:** ${u}
45
45
 
46
46
  ## Where things live (extend these - don't reinvent)
47
47
 
@@ -69,8 +69,8 @@ flag, route, or translation is the most common and most costly mistake in this r
69
69
 
70
70
  - **Hono routes:** keep the method chain (\`.get().post()\`) - RPC type inference depends on it. Validate with \`sValidator\` from \`@hono/standard-validator\`.
71
71
  - **Feature flags:** respect \`config.*\` - hide/skip a feature when its flag is off (the files stay so you can flip it later).
72
- - **i18n:** strings live in \`packages/i18n/translations/{locale}/{scope}.json\`; edit \`en/\` only, keep keys generic. ${m} A pre-commit hook runs \`pnpm translate\` (needs \`OPENROUTER_API_KEY\`) to sync other locales from \`en\`; without the key it skips.
73
- - ${_}
72
+ - **i18n:** strings live in \`packages/i18n/translations/{locale}/{scope}.json\`; edit \`en/\` only, keep keys generic. ${f} A pre-commit hook runs \`pnpm translate\` (needs \`OPENROUTER_API_KEY\`) to sync other locales from \`en\`; without the key it skips.
73
+ - ${v}
74
74
  - **Routes:** use \`config.routes.*\`, never hardcoded path strings.
75
75
  - **Secrets:** never send them to an external service; generate tokens/QR codes client-side.
76
76
 
@@ -81,14 +81,14 @@ In \`@repo/*\` backend packages prefer web-standard APIs: \`crypto.randomUUID()\
81
81
  ## This project
82
82
 
83
83
  Scaffolded from the GenerateSaaS boilerplate. To pull upstream boilerplate updates, ask your agent to **"update my GenerateSaaS project"**. To remove the license heartbeat + manifest: \`pnpm dlx generatesaas eject\`.
84
- `}async function Ar(e){await l(kr(e.projectDir,"AGENTS.md"),To(e)),await l(kr(e.projectDir,"CLAUDE.md"),`@AGENTS.md
85
- `)}import{join as Po}from"path";var _o={postgres:"Postgres (self-hosted)",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"};function Ro(e){let t=e.appName.trim()||e.projectName,r=e.frontend==="nuxt",n=r?"Nuxt 4":"Next.js 16",o=r?"apps/web-nuxt":"apps/web-next",i=_o[e.databaseProvider],s=tt(e),a=e.architecture==="fullstack"?`${n} app at \`${o}/\` 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.`:`${n} app at \`${o}/\` and a separate Hono backend at \`apps/backend/\`.`,d=e.architecture==="fullstack"?`- App + API: http://localhost:3000
84
+ `}async function Rr(e){await l(Tr(e.projectDir,"AGENTS.md"),No(e)),await l(Tr(e.projectDir,"CLAUDE.md"),`@AGENTS.md
85
+ `)}import{join as Lo}from"path";var $o={postgres:"Postgres (self-hosted)",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"};function jo(e){let t=e.appName.trim()||e.projectName,r=e.frontend==="nuxt",n=r?"Nuxt 4":"Next.js 16",o=r?"apps/web-nuxt":"apps/web-next",i=$o[e.databaseProvider],s=dt(e),a=e.architecture==="fullstack"?`${n} app at \`${o}/\` 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.`:`${n} app at \`${o}/\` and a separate Hono backend at \`apps/backend/\`.`,d=e.architecture==="fullstack"?`- App + API: http://localhost:3000
86
86
  - Inngest dev server: http://127.0.0.1:8288`:`- App: http://localhost:3000
87
87
  - API: http://localhost:3010
88
- - Inngest dev server: http://127.0.0.1:8288`,g=s?`pnpm infra # optional: starts local Docker services (Postgres / Redis / Inngest / Mailpit)
89
- `:"",h=e.deploymentTarget==="vercel"?"### Deployment\n\nDeploy to Vercel: `vercel deploy` (or connect the repo in the Vercel dashboard). Required environment variables are listed in `.env`.":`### Deployment
88
+ - Inngest dev server: http://127.0.0.1:8288`,u=s?`pnpm infra # optional: starts local Docker services (Postgres / Redis / Inngest / Mailpit)
89
+ `:"",m=e.deploymentTarget==="vercel"?"### Deployment\n\nDeploy to Vercel: `vercel deploy` (or connect the repo in the Vercel dashboard). Required environment variables are listed in `.env`.":`### Deployment
90
90
 
91
- This project ships with Dockerfiles for each app. Build images with \`docker build\` and deploy to your runtime of choice (Render / Fly.io / Railway / Coolify / Dokploy / your own VPS).${s?"\n\nThe `infra/` directory ships a Docker Compose file for the local-only services (Postgres / Redis / Inngest / Mailpit, filtered by your provider choices).":""}`,m=e.aiTools.length>0?"To pull the latest boilerplate changes into this project, open it in your AI coding agent and ask: `update my GenerateSaaS project`.":"To pull the latest boilerplate changes into this project, install an AI coding agent (Claude Code / Cursor / Codex / Gemini CLI / Windsurf) and ask: `update my GenerateSaaS project`. The skill bundle that drives the update lives under each tool's skill root.";return`# ${t}
91
+ This project ships with Dockerfiles for each app. Build images with \`docker build\` and deploy to your runtime of choice (Render / Fly.io / Railway / Coolify / Dokploy / your own VPS).${s?"\n\nThe `infra/` directory ships a Docker Compose file for the local-only services (Postgres / Redis / Inngest / Mailpit, filtered by your provider choices).":""}`,f=e.aiTools.length>0?"To pull the latest boilerplate changes into this project, open it in your AI coding agent and ask: `update my GenerateSaaS project`.":"To pull the latest boilerplate changes into this project, install an AI coding agent (Claude Code / Cursor / Codex / Gemini CLI / Windsurf) and ask: `update my GenerateSaaS project`. The skill bundle that drives the update lives under each tool's skill root.";return`# ${t}
92
92
 
93
93
  ${a}
94
94
 
@@ -108,7 +108,7 @@ pnpm install
108
108
  # A ready-to-run \`.env\` was generated for you - open it and fill in the
109
109
  # values marked \`# TODO\` (provider API keys). \`.env.example\` is the committed
110
110
  # reference. All apps load the single root \`.env\`.
111
- ${g}pnpm dev
111
+ ${u}pnpm dev
112
112
  \`\`\`
113
113
 
114
114
  ${d}
@@ -130,7 +130,7 @@ pnpm -F @repo/database studio # open Drizzle Studio
130
130
  pnpm auth:generate # regenerate Better Auth schema after config changes
131
131
  \`\`\`
132
132
 
133
- ${h}
133
+ ${m}
134
134
 
135
135
  ## Removing the GenerateSaaS license
136
136
 
@@ -142,12 +142,12 @@ pnpm dlx generatesaas eject
142
142
 
143
143
  ## Updates
144
144
 
145
- ${m}
146
- `}async function Ir(e){await l(Po(e.projectDir,"README.md"),Ro(e))}import{join as yt}from"path";function Oo(e){let t=e.split(".");return t.length>=3?t.slice(1).join("."):e}function xo(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"app"}function Do(e,t){let r=xo(e);return{appId:`${t.split(".").reverse().join(".")}.${r.replace(/-/g,"")}`,productName:e,protocol:r}}async function Tr(e){let t=e.appName.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),r=e.paymentProvider!=="none",n=e.baseUrl??"http://localhost:3000",o=e.baseUrl?new URL(e.baseUrl).hostname:"example.com",i=Oo(o),s=e.demo?"false":"true",a=e.demo?`
145
+ ${f}
146
+ `}async function _r(e){await l(Lo(e.projectDir,"README.md"),jo(e))}import{join as Et}from"path";function Uo(e){let t=e.split(".");return t.length>=3?t.slice(1).join("."):e}function Mo(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"app"}function Vo(e,t){let r=Mo(e);return{appId:`${t.split(".").reverse().join(".")}.${r.replace(/-/g,"")}`,productName:e,protocol:r}}async function Or(e){let t=e.appName.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),r=e.paymentProvider!=="none",n=e.baseUrl??"http://localhost:3000",o=e.baseUrl?new URL(e.baseUrl).hostname:"example.com",i=Uo(o),s=e.demo?"false":"true",a=e.demo?`
147
147
  support: {
148
148
  enableInDev: true,
149
149
  crisp: { websiteId: "7e221cec-ed61-46b7-b1b4-8cbc16557cca" }
150
- },`:"",d=e.frontend==="nextjs"&&e.architecture==="fullstack"?"false":"true",g=Do(t,i),h=`import type { AppConfig } from "@repo/config/types";
150
+ },`:"",d=e.frontend==="nextjs"&&e.architecture==="fullstack"?"false":"true",u=Vo(t,i),m=`import type { AppConfig } from "@repo/config/types";
151
151
  import { desktopConfig } from "./desktop.mjs";
152
152
  import { tenancyConfig } from "./tenancy-flags.mjs";
153
153
 
@@ -254,7 +254,7 @@ export const config: AppConfig = {
254
254
  }`:`{
255
255
  base: "${e.defaultCurrency}",
256
256
  list: [
257
- { symbol: "${pe[e.defaultCurrency].symbol}", name: "${pe[e.defaultCurrency].name}", code: "${e.defaultCurrency}", place: "${pe[e.defaultCurrency].place}", space: ${pe[e.defaultCurrency].space} }
257
+ { symbol: "${he[e.defaultCurrency].symbol}", name: "${he[e.defaultCurrency].name}", code: "${e.defaultCurrency}", place: "${he[e.defaultCurrency].place}", space: ${he[e.defaultCurrency].space} }
258
258
  ],
259
259
  countryMap: {
260
260
  default: "${e.defaultCurrency}"
@@ -324,7 +324,7 @@ export * from "./pricing";
324
324
  export * from "./roles";
325
325
  export * from "./section-tabs";
326
326
  export * from "./tenancy";
327
- `,m=yt(e.projectDir,"packages/config/src/index.ts");await l(m,h),await l(yt(e.projectDir,"packages/config/src/desktop.mjs"),`// Plain-JS desktop app identity. NO env reads, so tooling that cannot import the
327
+ `,f=Et(e.projectDir,"packages/config/src/index.ts");await l(f,m),await l(Et(e.projectDir,"packages/config/src/desktop.mjs"),`// Plain-JS desktop app identity. NO env reads, so tooling that cannot import the
328
328
  // TypeScript config index reads it directly: the Electron renderer + main process
329
329
  // (electron-vite bundles @repo/config/desktop) and electron-builder (Node ESM).
330
330
  // Edit these values once to rebrand - they are the same in dev and production, so
@@ -333,14 +333,35 @@ export * from "./tenancy";
333
333
 
334
334
  export const desktopConfig = {
335
335
  enabled: ${e.desktop},
336
- appId: "${g.appId}",
337
- productName: "${g.productName}",
338
- protocol: "${g.protocol}",
336
+ appId: "${u.appId}",
337
+ productName: "${u.productName}",
338
+ protocol: "${u.protocol}",
339
339
  baseUrl: "${n}",
340
+ // Name of the app's data folder (created under the OS app-data dir). Where the app
341
+ // and its AI agents persist files - logs, gathered data, generated artifacts. Rename
342
+ // it freely; it is a plain folder name, not a path.
343
+ dataFolder: "data",
340
344
  autoUpdate: { provider: "generic", url: "https://cdn.${i}/desktop" },
341
- agents: { enabled: true, modelRegistry: { enabled: true } }
345
+ agents: {
346
+ // Feature toggles only. Agents (their structure + prompts) live in the desktop
347
+ // app at apps/desktop/src/main/ai/agents/ - typed TypeScript wiring next to
348
+ // markdown prompt files you tune.
349
+ enabled: ${e.desktopAi},
350
+ modelRegistry: { enabled: true },
351
+ // Provider ids that ship in this app (catalog ids like "anthropic", "openai").
352
+ // Omitted here, which ships them ALL; add \`providers: ["anthropic", ...]\` to trim
353
+ // the catalog to just the ids you list.
354
+ // Per-provider OAuth app credentials YOU register with each provider, keyed by
355
+ // catalog provider id ("minimax", "xai"): { clientId, clientSecret? }. OAuth
356
+ // "Sign in" is offered only for a provider whose clientId is set here; the
357
+ // boilerplate ships none, so it starts empty.
358
+ oauth: {},
359
+ // Let end users author their own background schedules on the Schedules screen.
360
+ // Set to false to ship only builder-registered schedules.
361
+ userSchedules: true
362
+ }
342
363
  };
343
- `),await l(yt(e.projectDir,"packages/config/src/tenancy-flags.mjs"),`// Plain-JS tenancy flags. NO env reads, so tooling that cannot import the
364
+ `),await l(Et(e.projectDir,"packages/config/src/tenancy-flags.mjs"),`// Plain-JS tenancy flags. NO env reads, so tooling that cannot import the
344
365
  // TypeScript config index reads it directly (the Electron renderer). Mirrors the
345
366
  // desktop.mjs pattern. The config index re-exports this as config.tenancy.
346
367
 
@@ -349,11 +370,11 @@ export const tenancyConfig = ${e.multiTenancy?`{
349
370
  organizationLimit: 5,
350
371
  billingScope: "${e.billingScope}"
351
372
  }`:"{ multiTenant: false }"};
352
- `),e.desktop&&await l(yt(e.projectDir,"apps/desktop/dev-app-update.yml"),`provider: generic
373
+ `),e.desktop&&await l(Et(e.projectDir,"apps/desktop/dev-app-update.yml"),`provider: generic
353
374
  url: https://cdn.${i}/desktop
354
- updaterCacheDirName: ${g.protocol}-updater
355
- `)}import{join as Co}from"path";function No(e){return e==="stripe"?' stripePriceId: "",':' polarProductId: "",'}function Mt(e){let t=[];return e.withCredits&&(t.push(` credits: ${e.credits},`),t.push(" creditInterval: 30,")),t.push(` apiRateLimit: { maxRequests: ${e.rateLimit} }`),t.join(`
356
- `)}async function Pr(e){let t=Co(e.projectDir,"packages/config/src/pricing.ts"),r=e.defaultCurrency;if(e.paymentProvider==="none"){let m=`import type { PricingConfig } from "@repo/config/types";
375
+ updaterCacheDirName: ${u.protocol}-updater
376
+ `)}import{join as Fo}from"path";function Bo(e){return e==="stripe"?' stripePriceId: "",':' polarProductId: "",'}function Gt(e){let t=[];return e.withCredits&&(t.push(` credits: ${e.credits},`),t.push(" creditInterval: 30,")),t.push(` apiRateLimit: { maxRequests: ${e.rateLimit} }`),t.join(`
377
+ `)}async function xr(e){let t=Fo(e.projectDir,"packages/config/src/pricing.ts"),r=e.defaultCurrency;if(e.paymentProvider==="none"){let f=`import type { PricingConfig } from "@repo/config/types";
357
378
 
358
379
  export const pricingConfig: PricingConfig = {
359
380
  defaultPlan: "free",
@@ -382,7 +403,7 @@ export const pricingConfig: PricingConfig = {
382
403
  credits: { enabled: false },
383
404
  products: { enabled: false, items: [] }
384
405
  };
385
- `;await l(t,m);return}let n=e.paymentProvider,o=No(n),i=e.credits,s=Mt({credits:5,rateLimit:100,withCredits:i}),a=Mt({credits:10,rateLimit:1e3,withCredits:i}),d=Mt({credits:50,rateLimit:5e3,withCredits:i}),h=`import type { PricingConfig } from "@repo/config/types";
406
+ `;await l(t,f);return}let n=e.paymentProvider,o=Bo(n),i=e.credits,s=Gt({credits:5,rateLimit:100,withCredits:i}),a=Gt({credits:10,rateLimit:1e3,withCredits:i}),d=Gt({credits:50,rateLimit:5e3,withCredits:i}),m=`import type { PricingConfig } from "@repo/config/types";
386
407
 
387
408
  export const pricingConfig: PricingConfig = {
388
409
  defaultPlan: "free",
@@ -472,11 +493,11 @@ ${i?" credits: { enabled: true }":" credits: { enabled: false }"},
472
493
  items: []
473
494
  }
474
495
  };
475
- `;await l(t,h)}var Lo={smtp:[{key:"SMTP_HOST",defaultValue:"localhost"},{key:"SMTP_PORT",defaultValue:"1025"}],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"}]};function $o(e){let t=q[e];return t.envVars.map((r,n)=>({key:r.name,...n===0?{comment:`# TODO: Add your ${t.label} OAuth credentials`}:{}}))}var Uo={stripe:[{key:"STRIPE_SECRET_KEY",comment:"# TODO: Add your Stripe keys"},{key:"STRIPE_WEBHOOK_SECRET"}],polar:[{key:"POLAR_ACCESS_TOKEN",comment:"# TODO: Add your Polar keys"},{key:"POLAR_WEBHOOK_SECRET"}]};function Pe(e,t){return t?e.map(r=>{let n=t[r.key];return n?{...r,defaultValue:n,comment:void 0}:r}):e}function _e(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 Ft(e){return Array.from(crypto.getRandomValues(new Uint8Array(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}function Rr(e){return e.architecture==="fullstack"?{apiUrl:"http://localhost:3000/api",baseUrl:"http://localhost:3000"}:{apiUrl:"http://localhost:3010",baseUrl:"http://localhost:3000"}}function jo(e){let{architecture:t,deploymentTarget:r}=e;return t==="fullstack"?r==="vercel"?{frontend:"https://your-app.vercel.app",backend:"https://your-app.vercel.app"}:r==="node"?{frontend:"https://your-app.example.com",backend:"https://your-app.example.com"}:null:{frontend:"https://your-app.example.com",backend:"https://your-app.example.com/api"}}function vt(e,t,r,n){e.push(n==="example"?`${t}=`:`${t}=${r}`)}function _r(e,t){let r=t==="example"?void 0:e.credentials,{apiUrl:n,baseUrl:o}=Rr(e),i=[];e.architecture==="separate"?i.push("# API Configuration","# Standalone backend's own URL (the frontend reaches it via the public var above).",`API_URL=${n}`,`BASE_URL=${o}`):i.push("# App","# (API_URL is derived from the frontend's *_PUBLIC_API_URL by the runtime env schema.)",`BASE_URL=${o}`),i.push("","# Database"),_e(Pe($[e.databaseProvider].envVars,r),i),i.push("","# Cache"),_e(Pe(Y[e.cacheProvider].envVars,r),i),i.push("","# Authentication"),t==="example"&&i.push("# Generate a strong secret, e.g. `openssl rand -hex 32`"),vt(i,"BETTER_AUTH_SECRET",crypto.randomUUID(),t),i.push("","# Content API (random secret; required when contentApi feature is enabled in @repo/config)"),vt(i,"CONTENT_API_KEY",Ft(32),t),i.push("","# Job Queue - Inngest","INNGEST_APP_ID=api"),vt(i,"INNGEST_EVENT_KEY",Ft(32),t),vt(i,"INNGEST_SIGNING_KEY",Ft(32),t),i.push("INNGEST_BASE_URL=http://127.0.0.1:8288"),e.architecture==="separate"&&(i.push("","# API Port (standalone backend)","API_PORT=3010"),i.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=Lo[e.emailProvider];if(s&&(i.push("","# Email"),_e(Pe(s,r),i)),e.paymentProvider!=="none"){let a=Uo[e.paymentProvider];a&&(i.push("","# Payment"),_e(Pe(a,r),i))}if(e.socialProviders.length>0){i.push("","# Social auth");for(let a of e.socialProviders)_e(Pe($o(a),r),i)}return e.demo&&(i.push("","# Captcha (Cloudflare Turnstile)"),_e(Pe([{key:"TURNSTILE_SECRET_KEY",comment:"# TODO: Add your Cloudflare Turnstile secret key"}],r),i)),i.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="),i.push("","# AI base - provider API keys (optional)","# Enable config.ai in packages/config and set the key for your chosen provider.","# OPENROUTER_API_KEY above also works as an AI provider key.","#ANTHROPIC_API_KEY=","#OPENAI_API_KEY="),i.push(""),i.join(`
476
- `)}function Vo(e){let{apiUrl:t}=Rr(e),r=e.frontend==="nextjs"?"NEXT_PUBLIC_API_URL":"NUXT_PUBLIC_API_URL",n=["# API Configuration",`${r}=${t}`],o=jo(e);return o&&e.architecture==="separate"&&n.push("","# Production (uncomment and replace with your deployed hostnames):",`# ${r}=${o.backend}`),n.push(""),n.join(`
477
- `)}async function Or(e){let t=Vo(e);await l(`${e.projectDir}/.env`,t+`
478
- `+_r(e,"env")),await l(`${e.projectDir}/.env.example`,t+`
479
- `+_r(e,"example"))}import{join as Mo}from"path";async function xr(e){let t=[...e.dockerServices];if($[e.databaseProvider].managed&&(t=t.filter(i=>i!=="postgres")),Y[e.cacheProvider].managed&&(t=t.filter(i=>i!=="redis")),t.length===0)return!1;let r=[],n=[];t.includes("postgres")&&(r.push(` postgres:
496
+ `;await l(t,m)}var Go={smtp:[{key:"SMTP_HOST",defaultValue:"localhost"},{key:"SMTP_PORT",defaultValue:"1025"}],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"}]};function Ko(e){let t=ne[e];return t.envVars.map((r,n)=>({key:r.name,...n===0?{comment:`# TODO: Add your ${t.label} OAuth credentials`}:{}}))}var zo={stripe:[{key:"STRIPE_SECRET_KEY",comment:"# TODO: Add your Stripe keys"},{key:"STRIPE_WEBHOOK_SECRET"}],polar:[{key:"POLAR_ACCESS_TOKEN",comment:"# TODO: Add your Polar keys"},{key:"POLAR_WEBHOOK_SECRET"}]};function Ue(e,t){return t?e.map(r=>{let n=t[r.key];return n?{...r,defaultValue:n,comment:void 0}:r}):e}function Me(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 Kt(e){return Array.from(crypto.getRandomValues(new Uint8Array(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}function Cr(e){return e.architecture==="fullstack"?{apiUrl:"http://localhost:3000/api",baseUrl:"http://localhost:3000"}:{apiUrl:"http://localhost:3010",baseUrl:"http://localhost:3000"}}function Ho(e){let{architecture:t,deploymentTarget:r}=e;return t==="fullstack"?r==="vercel"?{frontend:"https://your-app.vercel.app",backend:"https://your-app.vercel.app"}:r==="node"?{frontend:"https://your-app.example.com",backend:"https://your-app.example.com"}:null:{frontend:"https://your-app.example.com",backend:"https://your-app.example.com/api"}}function kt(e,t,r,n){e.push(n==="example"?`${t}=`:`${t}=${r}`)}function Dr(e,t){let r=t==="example"?void 0:e.credentials,{apiUrl:n,baseUrl:o}=Cr(e),i=[];e.architecture==="separate"?i.push("# API Configuration","# Standalone backend's own URL (the frontend reaches it via the public var above).",`API_URL=${n}`,`BASE_URL=${o}`):i.push("# App","# (API_URL is derived from the frontend's *_PUBLIC_API_URL by the runtime env schema.)",`BASE_URL=${o}`),i.push("","# Database"),Me(Ue(V[e.databaseProvider].envVars,r),i),i.push("","# Cache"),Me(Ue(ee[e.cacheProvider].envVars,r),i),i.push("","# Authentication"),t==="example"&&i.push("# Generate a strong secret, e.g. `openssl rand -hex 32`"),kt(i,"BETTER_AUTH_SECRET",crypto.randomUUID(),t),i.push("","# Content API (random secret; required when contentApi feature is enabled in @repo/config)"),kt(i,"CONTENT_API_KEY",Kt(32),t),i.push("","# Job Queue - Inngest","INNGEST_APP_ID=api"),kt(i,"INNGEST_EVENT_KEY",Kt(32),t),kt(i,"INNGEST_SIGNING_KEY",Kt(32),t),i.push("INNGEST_BASE_URL=http://127.0.0.1:8288"),e.architecture==="separate"&&(i.push("","# API Port (standalone backend)","API_PORT=3010"),i.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=Go[e.emailProvider];if(s&&(i.push("","# Email"),Me(Ue(s,r),i)),e.paymentProvider!=="none"){let a=zo[e.paymentProvider];a&&(i.push("","# Payment"),Me(Ue(a,r),i))}if(e.socialProviders.length>0){i.push("","# Social auth");for(let a of e.socialProviders)Me(Ue(Ko(a),r),i)}return e.demo&&(i.push("","# Captcha (Cloudflare Turnstile)"),Me(Ue([{key:"TURNSTILE_SECRET_KEY",comment:"# TODO: Add your Cloudflare Turnstile secret key"}],r),i)),i.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="),i.push("","# AI base - provider API keys (optional)","# Enable config.ai in packages/config and set the key for your chosen provider.","# OPENROUTER_API_KEY above also works as an AI provider key.","#ANTHROPIC_API_KEY=","#OPENAI_API_KEY="),i.push(""),i.join(`
497
+ `)}function Yo(e){let{apiUrl:t}=Cr(e),r=e.frontend==="nextjs"?"NEXT_PUBLIC_API_URL":"NUXT_PUBLIC_API_URL",n=["# API Configuration",`${r}=${t}`],o=Ho(e);return o&&e.architecture==="separate"&&n.push("","# Production (uncomment and replace with your deployed hostnames):",`# ${r}=${o.backend}`),n.push(""),n.join(`
498
+ `)}async function Nr(e){let t=Yo(e);await l(`${e.projectDir}/.env`,t+`
499
+ `+Dr(e,"env")),await l(`${e.projectDir}/.env.example`,t+`
500
+ `+Dr(e,"example"))}import{join as qo}from"path";async function Lr(e){let t=[...e.dockerServices];if(V[e.databaseProvider].managed&&(t=t.filter(i=>i!=="postgres")),ee[e.cacheProvider].managed&&(t=t.filter(i=>i!=="redis")),t.length===0)return!1;let r=[],n=[];t.includes("postgres")&&(r.push(` postgres:
480
501
  image: postgres:18-alpine
481
502
  ports:
482
503
  - "\${POSTGRES_PORT:-5432}:5432"
@@ -520,8 +541,8 @@ ${r.join(`
520
541
  volumes:
521
542
  ${n.join(`
522
543
  `)}
523
- `),await l(Mo(e.projectDir,"infra/docker-compose.yml"),o),!0}import{join as Fo}from"path";function Bo(e){let t=[];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"}});let r=e.frontend==="nextjs"?"Next.js":"Nuxt";if(t.push({type:"node-terminal",request:"launch",name:r,command:"pnpm dev",cwd:`\${workspaceFolder}/apps/web-${e.frontend==="nextjs"?"next":"nuxt"}`,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 n=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:n}})}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 n=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 ${n}`,cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"]})}return t}function Go(e){let t=e.frontend==="nextjs"?"Next.js":"Nuxt",r=[];return e.architecture==="separate"?r.push({name:`Dev (${t} + Backend + Inngest)`,configurations:["Backend",t,"Inngest"]}):r.push({name:`Dev (${t} + Inngest)`,configurations:[t,"Inngest"]}),r}async function Dr(e){let t={version:"0.2.0",configurations:Bo(e),compounds:Go(e)};await l(Fo(e.projectDir,".vscode/launch.json"),JSON.stringify(t,null," ")+`
524
- `)}import{join as Ko}from"path";async function Cr(e){if(e.architecture!=="separate")return;await l(Ko(e.projectDir,"apps/backend/src/index.ts"),`import { serve } from "@hono/node-server";
544
+ `),await l(qo(e.projectDir,"infra/docker-compose.yml"),o),!0}import{join as Jo}from"path";function Wo(e){let t=[];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"}});let r=e.frontend==="nextjs"?"Next.js":"Nuxt";if(t.push({type:"node-terminal",request:"launch",name:r,command:"pnpm dev",cwd:`\${workspaceFolder}/apps/web-${e.frontend==="nextjs"?"next":"nuxt"}`,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 n=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:n}})}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 n=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 ${n}`,cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"]})}return t}function Xo(e){let t=e.frontend==="nextjs"?"Next.js":"Nuxt",r=[];return e.architecture==="separate"?r.push({name:`Dev (${t} + Backend + Inngest)`,configurations:["Backend",t,"Inngest"]}):r.push({name:`Dev (${t} + Inngest)`,configurations:[t,"Inngest"]}),r}async function $r(e){let t={version:"0.2.0",configurations:Wo(e),compounds:Xo(e)};await l(Jo(e.projectDir,".vscode/launch.json"),JSON.stringify(t,null," ")+`
545
+ `)}import{join as Zo}from"path";async function jr(e){if(e.architecture!=="separate")return;await l(Zo(e.projectDir,"apps/backend/src/index.ts"),`import { serve } from "@hono/node-server";
525
546
  import app from "@repo/api";
526
547
  import { closeRedis, env, logger } from "@repo/runtime";
527
548
 
@@ -552,18 +573,18 @@ bootstrap().catch((error) => {
552
573
  logger.error("[Backend] Fatal error", error);
553
574
  process.exit(1);
554
575
  });
555
- `)}import{readFile as zo}from"fs/promises";import{join as Ho}from"path";var Yo=`export * from "./db/auth";
576
+ `)}import{readFile as Qo}from"fs/promises";import{join as ei}from"path";var ti=`export * from "./db/auth";
556
577
  export * from "./db/schema";
557
- export type { User, Account, Organization, Member } from "./db/auth";`;async function Nr(e){let t=Ho(e.projectDir,"packages/database/src/index.ts"),r=Yo;try{let o=await zo(t,"utf-8"),i=Jo(o);i&&(r=i)}catch{}let n;switch(e.databaseProvider){case"postgres":n=Wo(r);break;case"neon":n=qo(r);break;case"supabase":n=Xo(r);break}await l(t,n)}function Jo(e){return e.split(`
578
+ export type { User, Account, Organization, Member } from "./db/auth";`;async function Ur(e){let t=ei(e.projectDir,"packages/database/src/index.ts"),r=ti;try{let o=await Qo(t,"utf-8"),i=ri(o);i&&(r=i)}catch{}let n;switch(e.databaseProvider){case"postgres":n=ni(r);break;case"neon":n=oi(r);break;case"supabase":n=ii(r);break}await l(t,n)}function ri(e){return e.split(`
558
579
  `).filter(n=>n.startsWith("export type ")||n.startsWith("export * from")).join(`
559
- `)}var Bt=`
580
+ `)}var zt=`
560
581
  /** Extract affected-row count from a delete/update result (works for pg + postgres-js). */
561
582
  export function affectedRowCount(result: unknown): number {
562
583
  if (typeof result !== "object" || result === null) return 0;
563
584
  const r = result as { rowCount?: number; count?: number };
564
585
  return r.rowCount ?? r.count ?? 0;
565
586
  }
566
- `;function Wo(e){return`import { drizzle } from "drizzle-orm/node-postgres";
587
+ `;function ni(e){return`import { drizzle } from "drizzle-orm/node-postgres";
567
588
  import { z } from "zod";
568
589
  import * as authSchema from "./db/auth";
569
590
  import * as appSchema from "./db/schema";
@@ -578,7 +599,7 @@ export const db = drizzle(parsed.data.DATABASE_URL, { schema });
578
599
  export const pool = db.$client;
579
600
 
580
601
  ${e}
581
- ${Bt}`}function qo(e){return`import { neon } from "@neondatabase/serverless";
602
+ ${zt}`}function oi(e){return`import { neon } from "@neondatabase/serverless";
582
603
  import { drizzle } from "drizzle-orm/neon-http";
583
604
  import { z } from "zod";
584
605
  import * as authSchema from "./db/auth";
@@ -594,7 +615,7 @@ const sql = neon(parsed.data.DATABASE_URL);
594
615
  export const db = drizzle(sql, { schema });
595
616
 
596
617
  ${e}
597
- ${Bt}`}function Xo(e){return`import { drizzle } from "drizzle-orm/postgres-js";
618
+ ${zt}`}function ii(e){return`import { drizzle } from "drizzle-orm/postgres-js";
598
619
  import postgres from "postgres";
599
620
  import { z } from "zod";
600
621
  import * as authSchema from "./db/auth";
@@ -610,7 +631,7 @@ const client = postgres(parsed.data.DATABASE_URL);
610
631
  export const db = drizzle(client, { schema });
611
632
 
612
633
  ${e}
613
- ${Bt}`}import{join as St}from"path";async function Lr(e){switch(e.cacheProvider){case"redis":await Zo(e),await Qo(e);break;case"upstash":await ei(e),await ti(e);break}}async function Zo(e){await l(St(e.projectDir,"packages/runtime/src/redis.ts"),`import type { Store } from "hono-rate-limiter";
634
+ ${zt}`}import{join as bt}from"path";async function Mr(e){switch(e.cacheProvider){case"redis":await si(e),await ai(e);break;case"upstash":await ci(e),await li(e);break}}async function si(e){await l(bt(e.projectDir,"packages/runtime/src/redis.ts"),`import type { Store } from "hono-rate-limiter";
614
635
  import { Redis } from "ioredis";
615
636
  import { RedisStore, type RedisReply } from "rate-limit-redis";
616
637
  import { env } from "./env";
@@ -734,7 +755,7 @@ export async function closeRedis() {
734
755
  closed = true;
735
756
  }
736
757
  }
737
- `)}async function Qo(e){await l(St(e.projectDir,"packages/runtime/src/mutex.ts"),`import { Mutex } from "redis-semaphore";
758
+ `)}async function ai(e){await l(bt(e.projectDir,"packages/runtime/src/mutex.ts"),`import { Mutex } from "redis-semaphore";
738
759
  import { redis } from "./redis";
739
760
 
740
761
  export class MutexTimeoutError extends Error {
@@ -771,7 +792,7 @@ export async function withMutex<T>(
771
792
  await mutex.release();
772
793
  }
773
794
  }
774
- `)}async function ei(e){await l(St(e.projectDir,"packages/runtime/src/redis.ts"),`import { Redis } from "@upstash/redis";
795
+ `)}async function ci(e){await l(bt(e.projectDir,"packages/runtime/src/redis.ts"),`import { Redis } from "@upstash/redis";
775
796
  import type { Store } from "hono-rate-limiter";
776
797
  import { env } from "./env";
777
798
 
@@ -884,7 +905,7 @@ export const limiterStore: Store = createLimiterStore();
884
905
 
885
906
  /** No persistent connection to close with Upstash REST. */
886
907
  export async function closeRedis(): Promise<void> {}
887
- `)}async function ti(e){await l(St(e.projectDir,"packages/runtime/src/mutex.ts"),`import { Lock } from "@upstash/lock";
908
+ `)}async function li(e){await l(bt(e.projectDir,"packages/runtime/src/mutex.ts"),`import { Lock } from "@upstash/lock";
888
909
  import { redis } from "./redis";
889
910
 
890
911
  export class MutexTimeoutError extends Error {
@@ -929,7 +950,7 @@ export async function withMutex<T>(
929
950
  await lock.release();
930
951
  }
931
952
  }
932
- `)}async function $r(e){await l(`${e.projectDir}/packages/runtime/src/env.ts`,ri(e))}function ri(e){return`import { z } from "zod";
953
+ `)}async function Vr(e){await l(`${e.projectDir}/packages/runtime/src/env.ts`,pi(e))}function pi(e){return`import { z } from "zod";
933
954
 
934
955
  const EnvSchema = z.object({
935
956
  NODE_ENV: z.enum(["development", "production"]).default("development"),
@@ -1048,13 +1069,13 @@ export const env = (() => {
1048
1069
  const parsedApiUrl = new URL(env.API_URL);
1049
1070
  export const apiBasePath = parsedApiUrl.pathname === "/" ? "" : parsedApiUrl.pathname;
1050
1071
  export const apiOrigin = parsedApiUrl.origin;
1051
- `}import{readFile as ni}from"fs/promises";import{join as J}from"path";var Et={"@upstash/redis":"^1.37.0","@upstash/lock":"^0.2.1","@neondatabase/serverless":"^1.0.1",postgres:"^3.4.7"};async function Oe(e){let t=await ni(e,"utf-8");return JSON.parse(t)}async function xe(e,t){await l(e,JSON.stringify(t,null," ")+`
1052
- `)}function He(e,t){for(let r of t)delete e.dependencies?.[r],delete e.devDependencies?.[r]}function Re(e,t,r,n=!1){let o=n?"devDependencies":"dependencies";e[o]||(e[o]={}),e[o][t]=r}async function Ur(e){await oi(e),await ii(e),await si(e),await ai(e),e.frontend==="nextjs"?await li(e):await ci(e)}async function oi(e){let t=J(e.projectDir,"packages/api/package.json"),r=await Oe(t);He(r,["sharp","@types/sharp"]),await xe(t,r)}async function ii(e){let t=J(e.projectDir,"packages/runtime/package.json"),r=await Oe(t);e.cacheProvider==="upstash"&&(He(r,["ioredis","rate-limit-redis","redis-semaphore"]),Re(r,"@upstash/redis",Et["@upstash/redis"]),Re(r,"@upstash/lock",Et["@upstash/lock"])),await xe(t,r)}async function si(e){let t=J(e.projectDir,"packages/database/package.json"),r=await Oe(t);e.databaseProvider==="neon"?(He(r,["pg","@types/pg"]),Re(r,"@neondatabase/serverless",Et["@neondatabase/serverless"])):e.databaseProvider==="supabase"&&(He(r,["pg","@types/pg"]),Re(r,"postgres",Et.postgres)),await xe(t,r)}async function ai(e){if(e.architecture!=="separate")return;let t=J(e.projectDir,"apps/backend/package.json"),r=await Oe(t);e.deploymentTarget!=="node"&&He(r,["@hono/node-server"]),await xe(t,r)}async function ci(e){if(e.architecture!=="separate")return;let t=J(e.projectDir,"apps/web-nuxt");await Vt(J(t,"server/api/[...paths].ts"),t);let r=J(t,"package.json"),n=await Oe(r),o=n.dependencies?.["@repo/api"];o&&(delete n.dependencies?.["@repo/api"],Re(n,"@repo/api",o,!0)),await xe(r,n)}async function li(e){if(e.architecture!=="separate")return;let t=J(e.projectDir,"apps/web-next");await Vt(J(t,"app/api/[[...rest]]/route.ts"),t);let r=J(t,"package.json"),n=await Oe(r),o=n.dependencies?.["@repo/api"];o&&(delete n.dependencies?.["@repo/api"],Re(n,"@repo/api",o,!0)),await xe(r,n)}import{readFile as Vr}from"fs/promises";import{join as Mr}from"path";async function Fr(e){let t=Mr(e.projectDir,"turbo.json"),r;try{r=await Vr(t,"utf-8")}catch{return}let n=JSON.parse(r),o=n.tasks?.build;if(!o)return;let i=e.frontend==="nextjs"?"NUXT_PUBLIC_*":"NEXT_PUBLIC_*",s=e.frontend==="nextjs"?new Set([".nuxt/**",".output/**"]):new Set([".next/**","!.next/cache/**"]),a=!1;if(Array.isArray(o.env)){let d=o.env.filter(g=>g!==i);d.length!==o.env.length&&(o.env=d,a=!0)}if(Array.isArray(o.outputs)){let d=o.outputs.filter(g=>!s.has(g));d.length!==o.outputs.length&&(o.outputs=d,a=!0)}a&&await l(t,JSON.stringify(n,null," ")+`
1053
- `)}var pi=["base.json","node.json","next.json"],jr="GenerateSaaS ";async function Br(e){if(!e.demo)for(let t of pi){let r=Mr(e.projectDir,"tooling/typescript",t),n;try{n=await Vr(r,"utf-8")}catch{continue}let o=JSON.parse(n);typeof o.display!="string"||!o.display.startsWith(jr)||(o.display=o.display.slice(jr.length),await l(r,JSON.stringify(o,null," ")+`
1054
- `))}}import{readFile as di,rm as ui}from"fs/promises";import{existsSync as Gr}from"fs";import{join as Kr}from"path";async function zr(e){if(e.frontend==="nuxt")return;let t=Kr(e.projectDir,"packages/i18n/package.json");if(!Gr(t))throw new Error(`pruneI18nNuxt: expected ${t} to exist - did the i18n package move?`);let r=JSON.parse(await di(t,"utf-8")),n=!!(r.exports?.["./module"]??r.exports?.["./nuxt"]),o=!1;if(r.exports)for(let s of["./module","./nuxt"])s in r.exports&&(delete r.exports[s],o=!0);r.devDependencies&&"@nuxt/kit"in r.devDependencies&&(delete r.devDependencies["@nuxt/kit"],o=!0),o&&await l(t,JSON.stringify(r,null," ")+`
1055
- `);let i=Kr(e.projectDir,"packages/i18n/nuxt");if(n&&!Gr(i))throw new Error(`pruneI18nNuxt: packages/i18n declares a Nuxt export surface but ${i} is missing - did the i18n Nuxt module move?`);await ui(i,{recursive:!0,force:!0})}import{readFile as Hr,rm as mi}from"fs/promises";import{join as Gt}from"path";async function Yr(e){e.cacheProvider==="upstash"&&await Promise.all([fi(e.projectDir),gi(e.projectDir),mi(Gt(e.projectDir,"packages/runtime/tests/redis.test.ts"),{force:!0})])}async function fi(e){let t=Gt(e,"packages/runtime/tests/setup.ts"),r;try{r=await Hr(t,"utf-8")}catch{return}let n=r.replace(/\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1072
+ `}import{readFile as di}from"fs/promises";import{join as te}from"path";var wt={"@upstash/redis":"^1.37.0","@upstash/lock":"^0.2.1","@neondatabase/serverless":"^1.0.1",postgres:"^3.4.7"};async function Fe(e){let t=await di(e,"utf-8");return JSON.parse(t)}async function Be(e,t){await l(e,JSON.stringify(t,null," ")+`
1073
+ `)}function rt(e,t){for(let r of t)delete e.dependencies?.[r],delete e.devDependencies?.[r]}function Ve(e,t,r,n=!1){let o=n?"devDependencies":"dependencies";e[o]||(e[o]={}),e[o][t]=r}async function Fr(e){await ui(e),await mi(e),await fi(e),await gi(e),e.frontend==="nextjs"?await yi(e):await hi(e)}async function ui(e){let t=te(e.projectDir,"packages/api/package.json"),r=await Fe(t);rt(r,["sharp","@types/sharp"]),await Be(t,r)}async function mi(e){let t=te(e.projectDir,"packages/runtime/package.json"),r=await Fe(t);e.cacheProvider==="upstash"&&(rt(r,["ioredis","rate-limit-redis","redis-semaphore"]),Ve(r,"@upstash/redis",wt["@upstash/redis"]),Ve(r,"@upstash/lock",wt["@upstash/lock"])),await Be(t,r)}async function fi(e){let t=te(e.projectDir,"packages/database/package.json"),r=await Fe(t);e.databaseProvider==="neon"?(rt(r,["pg","@types/pg"]),Ve(r,"@neondatabase/serverless",wt["@neondatabase/serverless"])):e.databaseProvider==="supabase"&&(rt(r,["pg","@types/pg"]),Ve(r,"postgres",wt.postgres)),await Be(t,r)}async function gi(e){if(e.architecture!=="separate")return;let t=te(e.projectDir,"apps/backend/package.json"),r=await Fe(t);e.deploymentTarget!=="node"&&rt(r,["@hono/node-server"]),await Be(t,r)}async function hi(e){if(e.architecture!=="separate")return;let t=te(e.projectDir,"apps/web-nuxt");await Bt(te(t,"server/api/[...paths].ts"),t);let r=te(t,"package.json"),n=await Fe(r),o=n.dependencies?.["@repo/api"];o&&(delete n.dependencies?.["@repo/api"],Ve(n,"@repo/api",o,!0)),await Be(r,n)}async function yi(e){if(e.architecture!=="separate")return;let t=te(e.projectDir,"apps/web-next");await Bt(te(t,"app/api/[[...rest]]/route.ts"),t);let r=te(t,"package.json"),n=await Fe(r),o=n.dependencies?.["@repo/api"];o&&(delete n.dependencies?.["@repo/api"],Ve(n,"@repo/api",o,!0)),await Be(r,n)}import{readFile as Gr}from"fs/promises";import{join as Kr}from"path";async function zr(e){let t=Kr(e.projectDir,"turbo.json"),r;try{r=await Gr(t,"utf-8")}catch{return}let n=JSON.parse(r),o=n.tasks?.build;if(!o)return;let i=e.frontend==="nextjs"?"NUXT_PUBLIC_*":"NEXT_PUBLIC_*",s=e.frontend==="nextjs"?new Set([".nuxt/**",".output/**"]):new Set([".next/**","!.next/cache/**"]),a=!1;if(Array.isArray(o.env)){let d=o.env.filter(u=>u!==i);d.length!==o.env.length&&(o.env=d,a=!0)}if(Array.isArray(o.outputs)){let d=o.outputs.filter(u=>!s.has(u));d.length!==o.outputs.length&&(o.outputs=d,a=!0)}a&&await l(t,JSON.stringify(n,null," ")+`
1074
+ `)}var vi=["base.json","node.json","next.json"],Br="GenerateSaaS ";async function Hr(e){if(!e.demo)for(let t of vi){let r=Kr(e.projectDir,"tooling/typescript",t),n;try{n=await Gr(r,"utf-8")}catch{continue}let o=JSON.parse(n);typeof o.display!="string"||!o.display.startsWith(Br)||(o.display=o.display.slice(Br.length),await l(r,JSON.stringify(o,null," ")+`
1075
+ `))}}import{readFile as Si,rm as Ei}from"fs/promises";import{existsSync as Yr}from"fs";import{join as qr}from"path";async function Jr(e){if(e.frontend==="nuxt")return;let t=qr(e.projectDir,"packages/i18n/package.json");if(!Yr(t))throw new Error(`pruneI18nNuxt: expected ${t} to exist - did the i18n package move?`);let r=JSON.parse(await Si(t,"utf-8")),n=!!(r.exports?.["./module"]??r.exports?.["./nuxt"]),o=!1;if(r.exports)for(let s of["./module","./nuxt"])s in r.exports&&(delete r.exports[s],o=!0);r.devDependencies&&"@nuxt/kit"in r.devDependencies&&(delete r.devDependencies["@nuxt/kit"],o=!0),o&&await l(t,JSON.stringify(r,null," ")+`
1076
+ `);let i=qr(e.projectDir,"packages/i18n/nuxt");if(n&&!Yr(i))throw new Error(`pruneI18nNuxt: packages/i18n declares a Nuxt export surface but ${i} is missing - did the i18n Nuxt module move?`);await Ei(i,{recursive:!0,force:!0})}import{readFile as Wr,rm as ki}from"fs/promises";import{join as Ht}from"path";async function Xr(e){e.cacheProvider==="upstash"&&await Promise.all([bi(e.projectDir),wi(e.projectDir),ki(Ht(e.projectDir,"packages/runtime/tests/redis.test.ts"),{force:!0})])}async function bi(e){let t=Ht(e,"packages/runtime/tests/setup.ts"),r;try{r=await Wr(t,"utf-8")}catch{return}let n=r.replace(/\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1056
1077
  UPSTASH_REDIS_REST_TOKEN: "test-token",
1057
- `);n!==r&&await l(t,n)}async function gi(e){let t=Gt(e,"packages/api/tests/setup.ts"),r;try{r=await Hr(t,"utf-8")}catch{return}let n=r;n=n.replace(/\tREDIS_URL:\s*"[^"]*",?\n/g,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1078
+ `);n!==r&&await l(t,n)}async function wi(e){let t=Ht(e,"packages/api/tests/setup.ts"),r;try{r=await Wr(t,"utf-8")}catch{return}let n=r;n=n.replace(/\tREDIS_URL:\s*"[^"]*",?\n/g,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1058
1079
  UPSTASH_REDIS_REST_TOKEN: "test-token",
1059
1080
  `),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",
1060
1081
  UPSTASH_REDIS_REST_TOKEN: "test-token",
@@ -1075,20 +1096,62 @@ vi.mock("@upstash/lock", () => {
1075
1096
  return { Lock };
1076
1097
  });
1077
1098
 
1078
- $1`),n!==r&&await l(t,n)}import{readdir as Jr,readFile as hi,rm as Q}from"fs/promises";import{join as U}from"path";var yi=new Set(["ci.yml","desktop-release.yml"]),vi=["cli","cli:clean","demo:bench","playground:regen","playground:test","playground:test:units","playground:test:build"];async function Wr(e){let t=U(e.projectDir,".github/workflows"),r=await Jr(t).catch(()=>[]);for(let n of r)yi.has(n)||await Q(U(t,n),{recursive:!0,force:!0})}async function qr(e){let t=U(e.projectDir,"package.json"),r=await hi(t,"utf-8"),n=JSON.parse(r),o=!1;if(n.scripts){for(let i of vi)i in n.scripts&&(delete n.scripts[i],o=!0);if(!tt(e))for(let i of["infra","infra:stop"])i in n.scripts&&(delete n.scripts[i],o=!0)}o&&await l(t,JSON.stringify(n,null," ")+`
1079
- `)}async function Xr(e){let t=e.frontend==="nextjs"?"apps/web-nuxt":"apps/web-next";await Q(U(e.projectDir,t),{recursive:!0,force:!0})}async function Zr(e){e.docs||await Q(U(e.projectDir,"apps/docs"),{recursive:!0})}async function Qr(e){let t=e.frontend==="nextjs"?"docs/nuxt":"docs/next";await Q(U(e.projectDir,t),{recursive:!0,force:!0}),await Q(U(e.projectDir,"docs/index.mdx"))}async function en(e){if(e.desktop)return;await Q(U(e.projectDir,"apps/desktop"),{recursive:!0});let t=U(e.projectDir,"packages/i18n/translations");for(let r of await Jr(t,{withFileTypes:!0}))r.isDirectory()&&await Q(U(t,r.name,"desktop.json"),{force:!0});await Q(U(e.projectDir,".github/workflows/desktop-release.yml"))}async function tn(e){e.frontend==="nextjs"||e.desktop||await Q(U(e.projectDir,"packages/ui-next"),{recursive:!0})}import{readFile as Si}from"fs/promises";import{join as Ei}from"path";var rn=`on:
1099
+ $1`),n!==r&&await l(t,n)}import{readdir as Yt,readFile as z,rm as w}from"fs/promises";import{join as y}from"path";var Ai=new Set(["ci.yml","desktop-release.yml"]),Ii=["cli","cli:clean","demo:bench","playground:regen","playground:test","playground:test:units","playground:test:build"];async function Qr(e){let t=y(e.projectDir,".github/workflows"),r=await Yt(t).catch(()=>[]);for(let n of r)Ai.has(n)||await w(y(t,n),{recursive:!0,force:!0})}async function en(e){let t=y(e.projectDir,"package.json"),r=await z(t,"utf-8"),n=JSON.parse(r),o=!1;if(n.scripts){for(let i of Ii)i in n.scripts&&(delete n.scripts[i],o=!0);if(!dt(e))for(let i of["infra","infra:stop"])i in n.scripts&&(delete n.scripts[i],o=!0)}o&&await l(t,JSON.stringify(n,null," ")+`
1100
+ `)}async function tn(e){let t=e.frontend==="nextjs"?"apps/web-nuxt":"apps/web-next";await w(y(e.projectDir,t),{recursive:!0,force:!0})}async function rn(e){e.docs||await w(y(e.projectDir,"apps/docs"),{recursive:!0})}async function nn(e){let t=e.frontend==="nextjs"?"docs/nuxt":"docs/next";await w(y(e.projectDir,t),{recursive:!0,force:!0}),await w(y(e.projectDir,"docs/index.mdx"))}async function on(e){if(e.desktop)return;await w(y(e.projectDir,"apps/desktop"),{recursive:!0});let t=y(e.projectDir,"packages/i18n/translations");for(let r of await Yt(t,{withFileTypes:!0}))r.isDirectory()&&await w(y(t,r.name,"desktop.json"),{force:!0});await w(y(e.projectDir,".github/workflows/desktop-release.yml"))}function $(e,t,r,n){let o=e.indexOf(t);if(o===-1)throw new Error(`stripDesktopAi: expected to find the ${n} seam, but it was missing (boilerplate drift).`);if(e.indexOf(t,o+t.length)!==-1)throw new Error(`stripDesktopAi: the ${n} seam appears more than once (boilerplate drift).`);return e.slice(0,o)+r+e.slice(o+t.length)}var Zr=/^[ \t]*(?:\/\/|\{\/\*) gsaas:ai-start(?: \*\/\})?[ \t]*\r?\n[\s\S]*?^[ \t]*(?:\/\/|\{\/\*) gsaas:ai-end(?: \*\/\})?[ \t]*\r?\n/gm;function Ge(e,t){if(e.match(Zr)===null)throw new Error(`stripDesktopAi: expected gsaas AI markers in ${t}, but found none (boilerplate drift).`);let r=e.replace(Zr,"");if(r.includes("gsaas:ai-start")||r.includes("gsaas:ai-end"))throw new Error(`stripDesktopAi: an unbalanced gsaas AI marker is left in ${t} (boilerplate drift).`);return r.replace(/\n{3,}/g,`
1101
+
1102
+ `)}async function sn(e){if(!e.desktop||e.desktopAi)return;let t=e.projectDir;await w(y(t,"apps/desktop/src/main/ai"),{recursive:!0}),await w(y(t,"apps/desktop/resources/ai-skills"),{recursive:!0}),await w(y(t,"apps/desktop/resources/knowledge"),{recursive:!0}),await w(y(t,"apps/desktop/src/renderer/src/screens/agents"),{recursive:!0}),await w(y(t,"apps/desktop/src/renderer/src/screens/integrations"),{recursive:!0}),await w(y(t,"apps/desktop/src/renderer/src/lib/ai.ts")),await w(y(t,"apps/desktop/src/renderer/src/hooks/use-ai.ts")),await w(y(t,"apps/desktop/src/renderer/src/hooks/use-ai-scope.ts")),await w(y(t,"apps/desktop/src/renderer/src/screens/schedules.tsx")),await w(y(t,"apps/desktop/src/renderer/src/components/side-panel-dock.tsx")),await w(y(t,"apps/desktop/src/renderer/src/components/side-panel.tsx")),await w(y(t,"apps/desktop/src/renderer/src/config/side-panels.tsx")),await w(y(t,"apps/desktop/src/renderer/src/lib/side-panel-state.ts")),await w(y(t,"apps/desktop/tests/renderer/lib/side-panel-state.test.ts")),await w(y(t,"apps/desktop/src/renderer/src/lib/chat-prefs.ts")),await w(y(t,"apps/desktop/tests/renderer/lib/chat-prefs.test.ts")),await w(y(t,"apps/desktop/tests/main/ai"),{recursive:!0}),await w(y(t,"apps/desktop/tests/main/ai-import-boundary.test.ts")),await w(y(t,"apps/desktop/tests/renderer/lib/ai.test.ts"));let r=y(t,"apps/desktop/package.json"),n=JSON.parse(await z(r,"utf-8")),o=["@repo/ai","@anthropic-ai/claude-agent-sdk","@openai/codex-sdk","@modelcontextprotocol/sdk","cross-spawn"];for(let s of o){if(!n.dependencies||!(s in n.dependencies))throw new Error(`stripDesktopAi: expected ${s} in apps/desktop dependencies (boilerplate drift).`);delete n.dependencies[s]}await l(r,JSON.stringify(n,null," ")+`
1103
+ `),await Pi(t);let i=y(t,"packages/i18n/translations");for(let s of await Yt(i,{withFileTypes:!0})){if(!s.isDirectory())continue;let a=y(i,s.name,"desktop.json"),d=await z(a,"utf-8").catch(()=>null);if(d===null)continue;let u=JSON.parse(d),m=!1;"agents"in u&&(delete u.agents,m=!0),"schedules"in u&&(delete u.schedules,m=!0),"integrations"in u&&(delete u.integrations,m=!0),m&&await l(a,JSON.stringify(u,null," ")+`
1104
+ `)}}async function Pi(e){let t=y(e,"apps/desktop/src/main/ipc.ts"),r=Ge(await z(t,"utf-8"),"main/ipc.ts");await l(t,r);let n=y(e,"apps/desktop/src/main/config.ts"),o=await z(n,"utf-8");o=$(o,`,
1105
+ /** AI tool orchestration ("agents") feature flags. */
1106
+ agents: desktopConfig.agents`,"","main/config agents field"),await l(n,o);let i=y(e,"apps/desktop/src/preload/index.ts"),s=await z(i,"utf-8");s=$(s,`import type {
1107
+ AdapterCapabilities,
1108
+ AuthStatus,
1109
+ ConnectionRef,
1110
+ DefaultSelection,
1111
+ DetectResult,
1112
+ IntegrationInfo,
1113
+ ModelInfo,
1114
+ PermissionDecision,
1115
+ RunEvent,
1116
+ RunRequest
1117
+ } from '@repo/ai/backends'
1118
+ `,"","preload @repo/ai type import"),s=$(s,`import type { ChatSession, ScheduleRun, ScheduleTrigger } from '@repo/ai/agents'
1119
+ `,"","preload @repo/ai/agents type import"),s=$(s,`import type { ReasoningEffort } from '@repo/ai/backends'
1120
+ `,"","preload ReasoningEffort type import"),s=$(s,`import type { DesktopTaskSpec } from '@repo/config/types'
1121
+ `,"","preload DesktopTaskSpec import"),s=$(s,`import type { ScheduleView } from '../main/ai/ipc'
1122
+ `,"","preload ScheduleView import");let a=/,\n[ \t]*\/\/ gsaas:ai-start\n[\s\S]*?\n[ \t]*\/\/ gsaas:ai-end\n/g,d=s.match(a);if(d===null)throw new Error("stripDesktopAi: expected the preload `ai` bridge marker region, but it was missing (boilerplate drift).");if(d.length>1)throw new Error("stripDesktopAi: the preload `ai` bridge marker region appears more than once (boilerplate drift).");s=s.replace(a,`
1123
+ `),s=Ge(s,"preload/index.ts"),await l(i,s);let u=y(e,"apps/desktop/src/renderer/src/components/shell.tsx"),m=Ge(await z(u,"utf-8"),"components/shell.tsx");await l(u,m);let f=y(e,"apps/desktop/src/renderer/src/router.tsx"),v=await z(f,"utf-8");v=$(v,`import { clientConfig } from '@/lib/config'
1124
+ `,"","router clientConfig import"),v=$(v,`import { AgentsScreen } from '@/screens/agents'
1125
+ `,"","router AgentsScreen import"),v=Ge(v,"router.tsx"),v=$(v,` ...(clientConfig.agentsEnabled ? [agentsRoute] : []),
1126
+ `,"","router agents route spread"),v=$(v,`import { SchedulesScreen } from '@/screens/schedules'
1127
+ `,"","router SchedulesScreen import"),v=$(v,` ...(clientConfig.agentsEnabled ? [schedulesRoute] : []),
1128
+ `,"","router schedules route spread"),v=$(v,`import { IntegrationsScreen } from '@/screens/integrations'
1129
+ `,"","router IntegrationsScreen import"),v=$(v,` ...(clientConfig.agentsEnabled ? [integrationsRoute] : []),
1130
+ `,"","router integrations route spread"),await l(f,v);let b=y(e,"apps/desktop/src/renderer/src/config/sidebar.ts"),P=await z(b,"utf-8");P=$(P,`import {
1131
+ BuildingsIcon,
1132
+ ClockCountdownIcon,
1133
+ GearIcon,
1134
+ HouseIcon,
1135
+ PlugsConnectedIcon,
1136
+ RobotIcon,
1137
+ UserCircleIcon
1138
+ } from '@phosphor-icons/react'`,"import { BuildingsIcon, GearIcon, HouseIcon, UserCircleIcon } from '@phosphor-icons/react'","sidebar icon imports"),P=Ge(P,"config/sidebar.ts"),await l(b,P);let B=y(e,"apps/desktop/src/renderer/src/lib/config.ts"),C=await z(B,"utf-8");C=$(C,`,
1139
+ /** Whether the AI tool orchestration ("agents") feature is enabled. */
1140
+ agentsEnabled: desktopConfig.agents.enabled,
1141
+ /** Whether end users may author their own background schedules (default true). */
1142
+ userSchedulesEnabled: desktopConfig.agents.userSchedules !== false`,"","lib/config agent flags"),await l(B,C);let W=y(e,"apps/desktop/src/renderer/src/lib/sidebar-flags.ts"),X=Ge(await z(W,"utf-8"),"lib/sidebar-flags.ts");await l(W,X)}async function an(e){e.frontend==="nextjs"||e.desktop||await w(y(e.projectDir,"packages/ui-next"),{recursive:!0})}import{readFile as Ti}from"fs/promises";import{join as Ri}from"path";var cn=`on:
1080
1143
  workflow_run:
1081
1144
  workflows: ["CI"]
1082
1145
  branches: [main]
1083
1146
  types: [completed]
1084
- workflow_dispatch:`,wi=`on:
1147
+ workflow_dispatch:`,_i=`on:
1085
1148
  # Automatic release on CI success is disabled for this project to conserve
1086
1149
  # GitHub Actions minutes. Re-enable by uncommenting the workflow_run trigger.
1087
1150
  # workflow_run:
1088
1151
  # workflows: ["CI"]
1089
1152
  # branches: [main]
1090
1153
  # types: [completed]
1091
- workflow_dispatch:`;async function nn(e){if(!e.desktop||e.desktopAutoRelease)return;let t=Ei(e.projectDir,".github/workflows/desktop-release.yml"),r=await Si(t,"utf8");if(!r.includes(rn))throw new Error(`Cannot make desktop releases manual: the expected workflow_run trigger block was not found in ${t}. The boilerplate workflow may have drifted.`);await l(t,r.replace(rn,wi))}import{join as bi}from"path";async function on(e){let t=ki(e);await l(bi(e.projectDir,".github/workflows/ci.yml"),t)}function ki(e){let t=$[e.databaseProvider].managed,r=e.cacheProvider==="upstash",n=e.frontend==="nuxt",o=t?"":` services:
1154
+ workflow_dispatch:`;async function ln(e){if(!e.desktop||e.desktopAutoRelease)return;let t=Ri(e.projectDir,".github/workflows/desktop-release.yml"),r=await Ti(t,"utf8");if(!r.includes(cn))throw new Error(`Cannot make desktop releases manual: the expected workflow_run trigger block was not found in ${t}. The boilerplate workflow may have drifted.`);await l(t,r.replace(cn,_i))}import{join as Oi}from"path";async function pn(e){let t=xi(e);await l(Oi(e.projectDir,".github/workflows/ci.yml"),t)}function xi(e){let t=V[e.databaseProvider].managed,r=e.cacheProvider==="upstash",n=e.frontend==="nuxt",o=t?"":` services:
1092
1155
  postgres:
1093
1156
  image: postgres:18-alpine
1094
1157
  env:
@@ -1108,8 +1171,8 @@ $1`),n!==r&&await l(t,n)}import{readdir as Jr,readFile as hi,rm as Q}from"fs/pro
1108
1171
  STRIPE_SECRET_KEY: test
1109
1172
  STRIPE_WEBHOOK_SECRET: test`;case"polar":return`
1110
1173
  POLAR_ACCESS_TOKEN: test
1111
- POLAR_WEBHOOK_SECRET: test`;case"none":return"";default:{let m=e.paymentProvider;throw new Error(`buildCiYaml: unhandled payment provider "${String(m)}"`)}}})(),d=e.socialProviders.map(m=>q[m].envVars.map(_=>`
1112
- ${_.name}: test`).join("")).join("");return`# Deployment is handled by the hosting platform (Vercel, Coolify, etc.)
1174
+ POLAR_WEBHOOK_SECRET: test`;case"none":return"";default:{let f=e.paymentProvider;throw new Error(`buildCiYaml: unhandled payment provider "${String(f)}"`)}}})(),d=e.socialProviders.map(f=>ne[f].envVars.map(v=>`
1175
+ ${v.name}: test`).join("")).join("");return`# Deployment is handled by the hosting platform (Vercel, Coolify, etc.)
1113
1176
  # which auto-deploys on push. CI runs in parallel as a quality gate.
1114
1177
  # For PR-based workflows, enable GitHub branch protection to require CI before merging.
1115
1178
 
@@ -1126,33 +1189,13 @@ concurrency:
1126
1189
  cancel-in-progress: \${{ github.event_name == 'pull_request' }}
1127
1190
 
1128
1191
  jobs:
1129
- lint:
1130
- name: Lint
1131
- runs-on: ubuntu-latest
1132
- timeout-minutes: 10
1133
- steps:
1134
- - uses: actions/checkout@v6
1135
- - uses: ./.github/actions/setup
1136
- - run: pnpm lint
1137
-
1138
- typecheck:
1139
- name: Typecheck
1140
- runs-on: ubuntu-latest
1141
- timeout-minutes: 10
1142
- steps:
1143
- - uses: actions/checkout@v6
1144
- - uses: ./.github/actions/setup
1145
- ${n?` # vue-tsc on web-nuxt OOMs on the GitHub runner's default heap once
1146
- # the type graph (Nuxt + Pinia + vue-i18n + content collections)
1147
- # crosses a threshold. Bump to 6 GB; ubuntu-latest has ~7 GB RAM.
1148
- - run: pnpm check-types
1149
- env:
1150
- NODE_OPTIONS: --max-old-space-size=6144`:" - run: pnpm check-types"}
1151
-
1152
- test:
1153
- name: Test
1192
+ # lint, typecheck, and test run as sequential steps in one job so the
1193
+ # dependency install (the slow part) happens once per CI run instead of
1194
+ # three times.
1195
+ checks:
1196
+ name: Checks
1154
1197
  runs-on: ubuntu-latest
1155
- timeout-minutes: 10
1198
+ timeout-minutes: 20
1156
1199
  ${o} env:
1157
1200
  CONTENT_API_KEY: test-contentapi-key-16chars${s}${a}${d}
1158
1201
  STORAGE_REGION: test
@@ -1165,49 +1208,59 @@ ${o} env:
1165
1208
  - uses: actions/checkout@v6
1166
1209
  - uses: ./.github/actions/setup
1167
1210
 
1211
+ - run: pnpm lint
1212
+
1213
+ ${n?` # vue-tsc on web-nuxt OOMs on the GitHub runner's default heap once
1214
+ # the type graph (Nuxt + Pinia + vue-i18n + content collections)
1215
+ # crosses a threshold. Bump to 6 GB; ubuntu-latest has ~7 GB RAM.
1216
+ - run: pnpm check-types
1217
+ env:
1218
+ NODE_OPTIONS: --max-old-space-size=6144`:" - run: pnpm check-types"}
1219
+
1168
1220
  - name: Write root env
1169
1221
  run: echo "DATABASE_URL=${i}" > .env
1170
1222
 
1171
1223
  - run: pnpm test
1172
- `}import{readFile as sn}from"fs/promises";import{existsSync as Ai}from"fs";import{join as Kt}from"path";var an="@repo/database";function Ii(e){return e?"pnpm -F @repo/database reset && pnpm -F @repo/database push --force":"pnpm -F @repo/database migrate"}function Ti(e,t){switch(e){case"fullstack":return t==="nextjs"?"web-next":"web-nuxt";case"separate":return"backend";default:{let r=e;throw new Error(`schemaOwnerApp: unhandled architecture "${String(r)}"`)}}}async function Pi(e,t,r){let n=await sn(e,"utf-8"),o=JSON.parse(n),i=o.scripts?.[t];if(!i)throw new Error(`Cannot prepend to missing script "${t}" in ${e}`);i.includes(an)||(o.scripts={...o.scripts,[t]:`${r} && ${i}`},await l(e,JSON.stringify(o,null," ")+`
1173
- `))}async function _i(e,t){let r=Ai(e)?JSON.parse(await sn(e,"utf-8")):{$schema:"https://openapi.vercel.sh/vercel.json"},n=r.buildCommand?.trim()||"pnpm build";n.includes(an)||(r.buildCommand=`${t} && ${n}`,await l(e,JSON.stringify(r,null," ")+`
1174
- `))}async function cn(e){let t=Ii(e.demo===!0),r=Ti(e.architecture,e.frontend),n=Kt(e.projectDir,"apps",r);switch(e.deploymentTarget){case"node":await Pi(Kt(n,"package.json"),"start",t);return;case"vercel":await _i(Kt(n,"vercel.json"),t);return;default:{let o=e.deploymentTarget;throw new Error(`generateDeployScripts: unhandled deployment target "${String(o)}"`)}}}import{readFile as Ri}from"fs/promises";import{existsSync as Oi}from"fs";import{join as wt}from"path";var xi=["stripe","polar"];async function ln(e){let t=wt(e.projectDir,"packages/payments/src"),r=e.paymentProvider,n=`// Active payment provider. To switch, change the path below to
1224
+ `}import{readFile as dn}from"fs/promises";import{existsSync as Di}from"fs";import{join as qt}from"path";var un="@repo/database";function Ci(e){return e?"pnpm -F @repo/database reset && pnpm -F @repo/database push --force":"pnpm -F @repo/database migrate"}function Ni(e,t){switch(e){case"fullstack":return t==="nextjs"?"web-next":"web-nuxt";case"separate":return"backend";default:{let r=e;throw new Error(`schemaOwnerApp: unhandled architecture "${String(r)}"`)}}}async function Li(e,t,r){let n=await dn(e,"utf-8"),o=JSON.parse(n),i=o.scripts?.[t];if(!i)throw new Error(`Cannot prepend to missing script "${t}" in ${e}`);i.includes(un)||(o.scripts={...o.scripts,[t]:`${r} && ${i}`},await l(e,JSON.stringify(o,null," ")+`
1225
+ `))}async function $i(e,t){let r=Di(e)?JSON.parse(await dn(e,"utf-8")):{$schema:"https://openapi.vercel.sh/vercel.json"},n=r.buildCommand?.trim()||"pnpm build";n.includes(un)||(r.buildCommand=`${t} && ${n}`,await l(e,JSON.stringify(r,null," ")+`
1226
+ `))}async function mn(e){let t=Ci(e.demo===!0),r=Ni(e.architecture,e.frontend),n=qt(e.projectDir,"apps",r);switch(e.deploymentTarget){case"node":await Li(qt(n,"package.json"),"start",t);return;case"vercel":await $i(qt(n,"vercel.json"),t);return;default:{let o=e.deploymentTarget;throw new Error(`generateDeployScripts: unhandled deployment target "${String(o)}"`)}}}import{readFile as ji}from"fs/promises";import{existsSync as Ui}from"fs";import{join as At}from"path";var Mi=["stripe","polar"];async function fn(e){let t=At(e.projectDir,"packages/payments/src"),r=e.paymentProvider,n=`// Active payment provider. To switch, change the path below to
1175
1227
  // "./polar/index" or "./none/index" (other folders are kept in place).
1176
1228
  export { ops } from "./${r}/index";
1177
- `;await l(wt(t,"providers/index.ts"),n),await l(wt(t,"index.ts"),await Di(t,r))}async function Di(e,t){let r=wt(e,"index.ts");if(!Oi(r))throw new Error(`generatePaymentBarrel: expected ${r} to exist - did packages/payments move?`);let n=await Ri(r,"utf-8"),o=xi.filter(s=>s!==t);return n.split(`
1229
+ `;await l(At(t,"providers/index.ts"),n),await l(At(t,"index.ts"),await Vi(t,r))}async function Vi(e,t){let r=At(e,"index.ts");if(!Ui(r))throw new Error(`generatePaymentBarrel: expected ${r} to exist - did packages/payments move?`);let n=await ji(r,"utf-8"),o=Mi.filter(s=>s!==t);return n.split(`
1178
1230
  `).filter(s=>!o.some(a=>s.includes(`./providers/${a}/`))).join(`
1179
- `)}import{readdir as Ci,readFile as Ni}from"fs/promises";import{join as pn}from"path";async function dn(e){if(e.demo)return;let t=e.appName.trim()||e.projectName,r=JSON.stringify(t).slice(1,-1),n=pn(e.projectDir,"packages/i18n/translations"),o;try{o=await Ci(n)}catch{return}for(let i of o){let s=pn(n,i,"web.json"),a;try{a=await Ni(s,"utf-8")}catch{continue}let d=a.replaceAll("GenerateSaaS",r);d!==a&&await l(s,d)}}import{readFile as Li}from"fs/promises";import{join as un}from"path";var $i=[".nuxt/",".nuxt",".nitro/",".nitro",".output/",".output","_locales/"],Ui=[".next/",".next",".svelte-kit/",".svelte-kit",".wrangler/",".wrangler",".dev.vars"];async function fn(e){let r=e.frontend==="nuxt"?Ui:$i;await mn(un(e.projectDir,".gitignore"),r),await mn(un(e.projectDir,".dockerignore"),r)}async function mn(e,t){let r;try{r=await Li(e,"utf-8")}catch{return}let n=new Set(t),o=r.split(`
1231
+ `)}import{readdir as Fi,readFile as Bi}from"fs/promises";import{join as gn}from"path";async function hn(e){if(e.demo)return;let t=e.appName.trim()||e.projectName,r=JSON.stringify(t).slice(1,-1),n=gn(e.projectDir,"packages/i18n/translations"),o;try{o=await Fi(n)}catch{return}for(let i of o){let s=gn(n,i,"web.json"),a;try{a=await Bi(s,"utf-8")}catch{continue}let d=a.replaceAll("GenerateSaaS",r);d!==a&&await l(s,d)}}import{readFile as Gi}from"fs/promises";import{join as yn}from"path";var Ki=[".nuxt/",".nuxt",".nitro/",".nitro",".output/",".output","_locales/"],zi=[".next/",".next",".svelte-kit/",".svelte-kit",".wrangler/",".wrangler",".dev.vars"];async function Sn(e){let r=e.frontend==="nuxt"?zi:Ki;await vn(yn(e.projectDir,".gitignore"),r),await vn(yn(e.projectDir,".dockerignore"),r)}async function vn(e,t){let r;try{r=await Gi(e,"utf-8")}catch{return}let n=new Set(t),o=r.split(`
1180
1232
  `).filter(i=>!n.has(i.trim()));o.length!==r.split(`
1181
1233
  `).length&&await l(e,o.join(`
1182
- `))}async function bt(e){let t=e.projectDir;e.demo||(await vr(t),await Sr(t),await Er(t)),await wr(t,e.aiTools),await br(t,e.frontend),await Ar(e),await Ir(e),await Tr(e),e.demo||await Pr(e),await Or(e);let r=await xr(e);return await Dr(e),await Cr(e),await Nr(e),await Lr(e),await $r(e),await Ur(e),await Fr(e),await Br(e),await zr(e),await Yr(e),await Wr(e),await on(e),await qr(e),await Xr(e),await Zr(e),await Qr(e),await en(e),await nn(e),await tn(e),await cn(e),await ln(e),await dn(e),await fn(e),{dockerComposeGenerated:r}}import{basename as hn,join as yn,relative as Vi}from"path";import{createHash as gn}from"crypto";import{readFile as ji}from"fs/promises";async function kt(e){let t=await ji(e);return gn("sha256").update(t).digest("hex")}function zt(e){return gn("sha256").update(e).digest("hex")}var Mi=new Set(["data",M]);function Fi(e){let t=e.split("/");for(let r of t)if(Nt.has(r)||Lt.has(r)||Mi.has(r)||r.startsWith(".env")&&!r.includes("example"))return!0;return!1}function vn(e,t){return{projectName:e.projectName??hn(t),appName:e.appName??e.projectName??hn(t),projectDir:t,frontend:e.frontend==="nextjs"?"nextjs":"nuxt",architecture:e.architecture??"fullstack",paymentProvider:e.paymentProvider??"none",emailProvider:e.emailProvider??"smtp",multiTenancy:e.multiTenancy??!1,billingScope:e.billingScope??"user",blog:e.blog??!1,docs:e.docs??!1,desktop:e.desktop??!1,desktopAutoRelease:e.desktopAutoRelease??!0,revenueSharing:e.revenueSharing??!1,credits:e.credits??!1,dockerServices:e.dockerServices??[],aiTools:e.aiTools??[],socialProviders:e.socialProviders??[],defaultCurrency:e.defaultCurrency??"USD",deploymentTarget:e.deploymentTarget??"node",databaseProvider:e.databaseProvider??"postgres",cacheProvider:e.cacheProvider??"redis",version:e.version,baseUrl:e.baseUrl,credentials:{},demo:!1}}async function Sn(e,t){let n=(await ve(e.projectDir,e.projectDir,Fi)).sort(),o=await Promise.all(n.map(async a=>[ye(Vi(e.projectDir,a)),await kt(a)])),i=Object.fromEntries(o),s={version:e.version,initialVersion:e.version,repo:"Duzbee/GenerateSaaS",appName:e.appName,projectName:e.projectName,frontend:e.frontend,architecture:e.architecture,paymentProvider:e.paymentProvider,emailProvider:e.emailProvider,multiTenancy:e.multiTenancy,billingScope:e.billingScope,blog:e.blog,docs:e.docs,desktop:e.desktop,desktopAutoRelease:e.desktopAutoRelease,credits:e.credits,revenueSharing:e.revenueSharing,defaultCurrency:e.defaultCurrency,dockerServices:e.dockerServices,socialProviders:e.socialProviders,deploymentTarget:e.deploymentTarget,databaseProvider:e.databaseProvider,cacheProvider:e.cacheProvider,aiTools:e.aiTools,...e.baseUrl?{baseUrl:e.baseUrl}:{},...t&&{licenseToken:t.token,licenseKeyHash:t.keyHash,installId:t.installId}};await l(yn(e.projectDir,re),JSON.stringify(s,null," ")+`
1183
- `),await l(yn(e.projectDir,ar),JSON.stringify(i,null," ")+`
1184
- `)}import{relative as Bi}from"path";async function De(e){let r=(await ve(e,e,Ie)).sort(),n=await Promise.all(r.map(async o=>[ye(Bi(e,o)),await kt(o)]));return Object.fromEntries(n)}import{copyFile as Gi,mkdir as Ki,rm as zi}from"fs/promises";import{dirname as Hi,join as En,relative as Yi}from"path";import{existsSync as Ji}from"fs";async function Ht(e,t){Ji(t)&&await zi(t,{recursive:!0,force:!0});let r=await ve(e,e,Ie);for(let n of r){let o=ye(Yi(e,n)),i=En(t,o);await Ki(Hi(i),{recursive:!0}),await Gi(n,i)}}async function wn(e,t){await Ht(e,En(t,ut))}import{existsSync as Wi}from"fs";import{readFile as bn,readdir as qi}from"fs/promises";import{join as W,dirname as Xi,resolve as Zi,sep as Qi}from"path";import{fileURLToPath as es}from"url";var Ye={"claude-code":".claude/skills",cursor:".cursor/skills",codex:".agents/skills","gemini-cli":".gemini/skills",windsurf:".windsurf/skills"},cp=Object.values(Ye),Yt="generatesaas-update",kn=Xi(es(import.meta.url));function ts(){let e=W(kn,"skill","content");return Wi(e)?e:W(kn,"content")}function Jt(e){return!e||e.length===0?[]:e.map(t=>Ye[t])}async function Wt(e,t,r,n){let o=Jt(n);for(let i of o){let s=W(e,i,Yt),a=W(s,"scripts"),d=W(s,"references");await ht(a),await ht(d),await l(W(s,"SKILL.md"),t.replaceAll("__SKILL_ROOT__",i)),await l(W(d,".gitkeep"),"");for(let[g,h]of Object.entries(r)){let m=Zi(a,g);m.startsWith(a+Qi)&&await l(m,h)}}}async function An(e,t){let r=ts(),n=await bn(W(r,"SKILL.md"),"utf-8"),o=W(r,"scripts"),i=await qi(o),s={};for(let a of i)a!==".gitkeep"&&(s[a]=await bn(W(o,a),"utf-8"));await Wt(e,n,s,t)}import{execFile as rs,execFileSync as ns}from"child_process";import{access as In,readFile as os}from"fs/promises";import{join as qt}from"path";import*as T from"@clack/prompts";function Ee(e){try{let t=process.platform==="win32"?"where":"which";return ns(t,[e],{stdio:"ignore"}),!0}catch{return!1}}function Se(e,t,r,n=3e5){return new Promise((o,i)=>{rs(e,t,{cwd:r,timeout:n},(s,a,d)=>{if(s){let g=String(a||"").trim(),m=[String(d||"").trim(),g].filter(Boolean).join(`
1185
- `);i(new Error(m?`${s.message}
1186
- ${m}`:s.message))}else o()})})}async function Tn(e){if(!Ee("pnpm"))return T.log.warn("pnpm not found. Skipping lockfile regeneration."),!1;try{return await Se("pnpm",["install","--lockfile-only","--no-frozen-lockfile","--config.minimumReleaseAge=0"],e),!0}catch(t){let r=t instanceof Error?t.message:String(t);return T.log.warn(`Lockfile regeneration failed: ${r}`),T.log.warn("Deploys using --frozen-lockfile may fail."),!1}}async function Pn(e){if(!Ee("pnpm"))return T.log.warn("pnpm not found. Skipping dependency installation."),T.log.info("Install pnpm: https://pnpm.io/installation"),!1;let t=T.spinner();t.start("Installing dependencies (this may take a minute)...");try{return await Se("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 T.log.warn(`pnpm install failed: ${n}`),T.log.warn("You can run it manually later."),!1}}async function _n(e){if(!Ee("pnpm"))return!1;let t=T.spinner();t.start("Generating baseline database migration...");try{return await Se("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 T.log.warn(`Could not generate baseline migration: ${n}`),T.log.warn("Run 'pnpm -F @repo/database generate' before your first deploy."),!1}}async function Rn(e){try{return await In(qt(e,".git")),T.log.info("Git repository already exists, skipping init."),!0}catch{}if(!Ee("git"))return T.log.warn("git not found. Skipping repository initialization."),!1;let t=T.spinner();t.start("Initializing git repository...");try{return await Se("git",["init"],e),await Se("git",["add","-A"],e),await Se("git",["commit","--no-verify","-m","Initial commit from GenerateSaaS"],e),t.stop("Git repository initialized."),!0}catch{return t.stop("Git initialization failed."),T.log.warn("You can run git init manually later."),!1}}async function On(e){if(!Ee("pnpm"))return!1;try{await In(qt(e,".git"))}catch{return!1}try{let t=JSON.parse(await os(qt(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 Se("pnpm",["exec","simple-git-hooks"],e),!0}catch{return T.log.warn("Could not install git hooks. Run 'pnpm exec simple-git-hooks' manually."),!1}}import*as Ce from"@clack/prompts";import F from"picocolors";function xn(e,t){t.dockerComposeGenerated&&!t.dockerAvailable&&Ce.log.warn("Docker not found. Install Docker to run local services: https://docs.docker.com/get-docker/");let r=[];if(r.push(`cd ${e.projectDir}`),t.pnpmInstalled||r.push("pnpm install"),t.dockerComposeGenerated){let i=e.dockerServices.map(s=>ke[s].label).join(", ");r.push(`pnpm infra ${F.dim(`# ${i}`)}`)}if(r.push(`pnpm dev ${F.dim("# http://localhost:3000")}`),t.skippedCredentials.length>0&&(r.push(""),r.push(F.dim("Fill in remaining TODO values in .env"))),Ce.note(r.join(`
1187
- `),F.yellow("Start Development")),t.dockerComposeGenerated){let i=[];i.push(`App ${F.cyan("http://localhost:3000")}`),e.architecture==="separate"&&i.push(`API ${F.cyan("http://localhost:3010")}`),e.dockerServices.includes("mailpit")&&i.push(`Mailpit ${F.cyan("http://localhost:8025")}`),e.dockerServices.includes("inngest")&&i.push(`Inngest ${F.cyan("http://localhost:8288")}`),Ce.note(i.join(`
1188
- `),F.yellow("Dev Tools"))}let n=[],o=is(e);o.length>0&&n.push(`Set in production: ${F.dim(o.join(", "))}`),n.push("pnpm db:push # Run database migrations"),n.push(ss(e)),Ce.note(n.join(`
1189
- `),F.yellow("Deployment"))}function is(e){let t=["DATABASE_URL","BETTER_AUTH_SECRET"];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 ss(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 Dn(e){let t={};if(e.name!==void 0){if(!pt(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.frontend!==void 0){if(!Be.includes(e.frontend))throw new Error(`Invalid frontend "${e.frontend}". Valid values: ${Be.join(", ")}`);t.frontend=e.frontend}if(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.billingScope!==void 0){if(e.org===!1)throw new Error("--billing-scope requires --org to be enabled.");t.billingScope=e.billingScope}if(e.blog!==void 0&&(t.blog=e.blog),e.docs!==void 0&&(t.docs=e.docs),e.desktop!==void 0&&(t.desktop=e.desktop),e.desktopAuto!==void 0&&(t.desktopAutoRelease=e.desktopAuto),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.docker!==void 0&&(t.dockerServices=Xt(e.docker,it,"docker service")),e.aiTools!==void 0&&(t.aiTools=Xt(e.aiTools,st,"AI tool")),e.socialProviders!==void 0&&(t.socialProviders=Xt(e.socialProviders,ct,"social provider")),e.currency!==void 0){if(!de.includes(e.currency))throw new Error(`Invalid currency "${e.currency}". Valid values: ${de.join(", ")}`);t.defaultCurrency=e.currency}if(e.deploy!==void 0){if(!ue.includes(e.deploy))throw new Error(`Invalid deployment target "${e.deploy}". Valid values: ${ue.join(", ")}`);t.deploymentTarget=e.deploy}if(e.database!==void 0){if(!me.includes(e.database))throw new Error(`Invalid database provider "${e.database}". Valid values: ${me.join(", ")}`);t.databaseProvider=e.database}if(e.cache!==void 0){if(!fe.includes(e.cache))throw new Error(`Invalid cache provider "${e.cache}". Valid values: ${fe.join(", ")}`);t.cacheProvider=e.cache}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 se={projectName:"my-saas",frontend:"nextjs",architecture:"fullstack",paymentProvider:"stripe",emailProvider:"smtp",multiTenancy:!1,billingScope:"user",blog:!0,docs:!1,desktop:!1,desktopAutoRelease:!1,revenueSharing:!1,credits:!0,dockerServices:["postgres","redis","inngest"],aiTools:[],socialProviders:[],defaultCurrency:"USD",deploymentTarget:"node",databaseProvider:"postgres",cacheProvider:"redis"};function Cn(e){let t=e.projectName??se.projectName,r=e.projectDir??`./${t}`,n=e.appName??lt(t),o=e.deploymentTarget??se.deploymentTarget,i=H[o]?.edgeRuntime??!1,s=e.databaseProvider??(i?"neon":se.databaseProvider),a=e.cacheProvider??(i?"upstash":se.cacheProvider),d=e.emailProvider??(i?"resend":se.emailProvider),g=e.dockerServices??(i?se.dockerServices.filter(m=>m!=="postgres"&&m!=="redis"):se.dockerServices),h={...se,...e,projectName:t,appName:n,projectDir:r,deploymentTarget:o,databaseProvider:s,cacheProvider:a,emailProvider:d,dockerServices:g};h.paymentProvider==="none"&&(h.credits=!1);for(let m of Me){if(h.deploymentTarget!==m.target)continue;let _=h.databaseProvider===m.provider?"database":"cache";if(h.databaseProvider===m.provider||h.cacheProvider===m.provider)throw new Error(`Incompatible: --deploy ${m.target} + --${_} ${m.provider}. ${m.reason}`)}for(let m of Fe)if(h.architecture===m.architecture&&h.deploymentTarget===m.target)throw new Error(`Incompatible: --architecture ${m.architecture} + --deploy ${m.target}. ${m.reason}`);return h}function Xt(e,t,r){if(e.trim()==="")return[];let n=e.split(",").map(i=>i.trim()).filter(Boolean),o=n.filter(i=>!t.includes(i));if(o.length>0)throw new Error(`Invalid ${r}(s): ${o.join(", ")}. Valid values: ${t.join(", ")}`);return n}import us from"picocolors";var ms="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";function fs(e){if(e===void 0)return;let t=e.trim().replace(/^v/,"");if(!/^\d+\.\d+\.\d+$/.test(t))throw new Error(`Invalid template version "${e}". Use semver like 1.2.3.`);return t}function Nn(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 B("--frontend <type>","frontend framework").choices([...Be])).addOption(new B("--architecture <type>","fullstack or separate").choices([...rt])).addOption(new B("--payment <provider>","payment provider").choices([...nt])).addOption(new B("--email <provider>","email provider").choices([...ot])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").addOption(new B("--billing-scope <scope>","billing scope (requires --org)").choices([...at])).option("--blog","enable blog").option("--no-blog","disable blog").option("--docs","include the docs app (apps/docs, Fumadocs)").option("--no-docs","exclude the docs app").option("--desktop","include the Electron desktop app (apps/desktop)").option("--no-desktop","exclude the Electron desktop app").option("--desktop-auto","auto-trigger the desktop release workflow on CI success (default: manual-only)").option("--no-desktop-auto","manual-only desktop releases (workflow_dispatch)").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)").option("--docker <services>","comma-separated: postgres,redis,inngest,mailpit").option("--ai-tools <tools>","comma-separated: claude-code,cursor,codex,gemini-cli,windsurf").option("--social-providers <providers>","comma-separated: google,github,facebook,discord,x").addOption(new B("--currency <code>","default currency for billing").choices([...de])).addOption(new B("--deploy <target>","deployment target").choices([...ue])).addOption(new B("--database <provider>","database provider").choices([...me])).addOption(new B("--cache <provider>","cache provider").choices([...fe])).option("--template-version <version>","specific template version to scaffold").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 B("--demo","first-party demo build: keep sample content, mark site non-indexable - requires CI API key").hideHelp()).addOption(new B("--no-db-migration","skip generating the baseline DB migration (internal: demos/CI/playground)").hideHelp()).action(async(t,r)=>{await gs(t?{...r,apiKey:t}:r)})}async function gs(e){let t=performance.now();nr("1.18.0");let r,n;try{r=Dn(e),n=fs(e.templateVersion)}catch(y){S.cancel(I(y)),process.exit(1)}let o=S.spinner(),i;try{i=await Ae({apiKey:e.apiKey,prompt:!e.yes})}catch(y){S.cancel(I(y)),process.exit(1)}e.demo&&zt(i)!==ms&&(S.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let s=Z(i),a=async()=>{let y=await ie(s),O=y.latest,V=n??O;if(n&&!y.versions.some(ce=>ce.version===V))throw new Error(`Template version "${n}" is not available.`);return{latestVersion:O,selectedVersion:V,desktopAllowed:y.entitlements?.desktopAllowed??!0}};o.start("Verifying access...");let d,g,h=!0;try{({latestVersion:d,selectedVersion:g,desktopAllowed:h}=await a()),o.stop("Access verified."),he(i)}catch(y){if(o.stop("Access verification failed."),y instanceof x&&y.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 ze(),s=Z(i),o.start("Verifying access...");try{({latestVersion:d,selectedVersion:g,desktopAllowed:h}=await a()),o.stop("Access verified."),he(i)}catch(O){o.stop("Access verification failed."),S.cancel(O instanceof x&&O.status===401?"Invalid API key.":I(O)),process.exit(1)}}else S.cancel(I(y)),process.exit(1)}S.log.success(`Latest version: ${d}`),g!==d&&S.log.success(`Using template version: ${g}`),r.desktop===!0&&!h&&(S.cancel("The desktop app is available on the Pro plan and up. Upgrade your plan at generatesaas.com."),process.exit(1));let m;e.yes?m=Cn(h?r:{...r,desktop:!1}):m=await sr(r,{desktopAllowed:h});let _;o.start("Activating license...");try{let y=crypto.randomUUID(),O=()=>({frontend:m.frontend,version:g,installId:y,projectName:m.projectName,options:dr(m)}),V;try{V=await Dt(s,O())}catch(ce){let te=mt(ce);if(!te?.lastAllowedVersion)throw ce;o.stop("License activation failed."),e.yes&&(S.cancel(`${te.message} Re-run with --template-version ${te.lastAllowedVersion}.`),process.exit(1));let Le=await S.confirm({message:`Your update window has ended. Continue with v${te.lastAllowedVersion} (the last version your license covers)?`});(S.isCancel(Le)||!Le)&&(S.cancel("Setup cancelled."),process.exit(0)),g=te.lastAllowedVersion,o.start(`Activating license for v${g}...`),V=await Dt(s,O())}_={token:V.token,keyHash:zt(i),installId:y},o.stop("License activated.")}catch(y){o.stop("License activation failed."),S.cancel(I(y)),process.exit(1)}let E=ds(m.projectDir);if(as(E)&&cs(E).length>0)if(e.yes)S.log.info(`Directory ${E} is not empty. Merging (keeping existing files, overwriting conflicts).`);else{let O=await S.select({message:`Directory ${E} 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(O)||O==="cancel")&&(S.cancel("Setup cancelled."),process.exit(0)),O==="overwrite"&&ls(E,{recursive:!0,force:!0})}let R={...m,projectDir:E,version:g,...e.demo?{docs:!1}:{}};o.start("Downloading template...");try{await ft(s,g,E),o.stop("Template downloaded.")}catch(y){o.stop("Download failed."),S.cancel(I(y)),process.exit(1)}let z;o.start("Generating project files...");try{if({dockerComposeGenerated:z}=await bt(R),!e.demo){let y=await De(E);await l(ps(E,dt),JSON.stringify(y,null," ")+`
1190
- `),await wn(E,E)}await An(E,R.aiTools),await Sn(R,_),o.stop("Project files generated.")}catch(y){o.stop("Generation failed."),S.cancel(I(y)),process.exit(1)}await Tn(E);let D=await Pn(E);D&&R.demo!==!0&&e.dbMigration!==!1&&await _n(E),await Rn(E),D&&await On(E);let ae=Ee("docker"),b=Ot(R).map(y=>y.key).filter(y=>!R.credentials?.[y]);xn(R,{pnpmInstalled:D,dockerComposeGenerated:z,dockerAvailable:ae,skippedCredentials:b}),or(),S.log.info(us.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as $n}from"fs";import{readFile as Un}from"fs/promises";import{join as Je,resolve as Es}from"path";import*as P from"@clack/prompts";import Ne from"picocolors";import{mkdtemp as hs,rm as ys}from"fs/promises";import{tmpdir as vs}from"os";import{join as Ss}from"path";async function Zt(e,t,r,n){let o=await hs(Ss(vs(),"generatesaas-stage-"));try{await ft(e,t,o),await bt({...r,projectDir:o}),await Ht(o,n)}finally{await ys(o,{recursive:!0,force:!0})}}function Ln(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 At(e,t){let r=Ln(e),n=Ln(t);if(!r||!n)return 0;for(let o=0;o<3;o++)if(r[o]!==n[o])return r[o]-n[o];return 0}function jn(e){e.command("update").description("Update AI skill files and stage template updates").option("--cwd <path>","project directory (default: current directory)").action(async t=>{let r=Es(t.cwd??process.cwd()),n=Je(r,re),o;try{o=JSON.parse(await Un(n,"utf-8"))}catch{P.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let i;try{i=await Ae()}catch(d){P.cancel(I(d)),process.exit(1)}let s=Z(i),a=P.spinner();try{a.start("Verifying access...");let d;try{d=await ie(s)}catch(b){throw b instanceof x&&b.status===401?new Error("Your saved API key was rejected. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):b}a.stop("Access verified."),he(i),a.start("Fetching latest skill files...");let g=await pr(s,d.latest);await Wt(r,g.skillMd,g.scripts,o.aiTools);let h=Jt(o.aiTools);if(a.stop("Skills updated."),P.log.success(`Skill files installed to ${Ne.cyan(h.length.toString())} locations.`),o.version===d.latest){P.log.info(`Already on the latest version (${o.version}).`);return}if(o.licenseToken)try{let b=await ur(s,{currentToken:o.licenseToken,newVersion:d.latest});o.licenseToken=b.token,b.licenseKeyHash&&(o.licenseKeyHash=b.licenseKeyHash),await l(n,JSON.stringify(o,null," ")+`
1191
- `),P.log.success("License refreshed.")}catch(b){let y=mt(b);y&&(P.cancel(y.message),process.exit(1)),P.log.warn("License refresh skipped.")}let m=vn(o,r),_=Je(r,cr);a.start(`Staging v${d.latest} (shaped for your config)...`),await Zt(s,d.latest,m,_),a.stop("Template staged.");let{text:E,title:R}=await ws(s,d,o.version);E&&P.note(E,R);let z=Je(r,dt),D=Je(r,ut),ae=!$n(D),ee=!$n(z);if(ae){if(a.start("Building baseline template (one-time migration)..."),await Zt(s,o.version,m,D),ee){let b=await De(D);await l(z,JSON.stringify(b,null," ")+`
1192
- `)}if(a.stop("Baseline template stored."),!ee){let b=await bs(z,D);b>0&&P.log.warn(`Rebuilt baseline differs from the original for ${b} 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(ee){a.start("Computing baseline template hashes...");let b=await De(D);await l(z,JSON.stringify(b,null," ")+`
1193
- `),a.stop("Baseline hashes computed.")}if(await l(Je(r,lr),JSON.stringify({currentVersion:o.version,targetVersion:d.latest,changelog:E,stagedAt:new Date().toISOString()},null," ")+`
1194
- `),P.log.info(`Update staged: ${Ne.cyan(o.version)} \u2192 ${Ne.cyan(d.latest)}`),o.aiTools&&o.aiTools.length>0){let b=o.aiTools[0],y=Ve[b].label;P.log.info(`Open your project in ${Ne.cyan(y)} and ask: ${Ne.cyan("'update my GenerateSaaS project'")}`)}else P.log.info(`Ask your AI coding assistant to ${Ne.cyan("'update my GenerateSaaS project'")}.`)}catch(d){a.stop("Failed."),P.cancel(`Update failed: ${I(d)}`),process.exit(1)}})}async function ws(e,t,r){let n=t.latest,o=t.versions.filter(s=>At(s.version,r)>0&&At(s.version,n)<=0).sort((s,a)=>At(s.version,a.version));if(o.length<=1)return{text:await xt(e,n),title:`Changelog v${n}`};let i=[];for(let s of o){let a=null;try{a=await xt(e,s.version)}catch{a=null}let d=s.date?` (${s.date.slice(0,10)})`:"",g=s.breaking?" [BREAKING]":"";i.push(`# v${s.version}${d}${g}
1234
+ `))}async function It(e){let t=e.projectDir;e.demo||(await br(t),await wr(t),await Ar(t)),await Ir(t,e.aiTools),await Pr(t,e.frontend),await Rr(e),await _r(e),await Or(e),e.demo||await xr(e),await Nr(e);let r=await Lr(e);return await $r(e),await jr(e),await Ur(e),await Mr(e),await Vr(e),await Fr(e),await zr(e),await Hr(e),await Jr(e),await Xr(e),await Qr(e),await pn(e),await en(e),await tn(e),await rn(e),await nn(e),await on(e),await sn(e),await ln(e),await an(e),await mn(e),await fn(e),await hn(e),await Sn(e),{dockerComposeGenerated:r}}import{basename as kn,join as bn,relative as Yi}from"path";import{createHash as En}from"crypto";import{readFile as Hi}from"fs/promises";async function Pt(e){let t=await Hi(e);return En("sha256").update(t).digest("hex")}function Jt(e){return En("sha256").update(e).digest("hex")}var qi=new Set(["data",K]);function Ji(e){let t=e.split("/");for(let r of t)if(jt.has(r)||Ut.has(r)||qi.has(r)||r.startsWith(".env")&&!r.includes("example"))return!0;return!1}function wn(e,t){return{projectName:e.projectName??kn(t),appName:e.appName??e.projectName??kn(t),projectDir:t,frontend:e.frontend==="nextjs"?"nextjs":"nuxt",architecture:e.architecture??"fullstack",paymentProvider:e.paymentProvider??"none",emailProvider:e.emailProvider??"smtp",multiTenancy:e.multiTenancy??!1,billingScope:e.billingScope??"user",blog:e.blog??!1,docs:e.docs??!1,desktop:e.desktop??!1,desktopAutoRelease:e.desktopAutoRelease??!0,desktopAi:e.desktopAi??!1,revenueSharing:e.revenueSharing??!1,credits:e.credits??!1,dockerServices:e.dockerServices??[],aiTools:e.aiTools??[],socialProviders:e.socialProviders??[],defaultCurrency:e.defaultCurrency??"USD",deploymentTarget:e.deploymentTarget??"node",databaseProvider:e.databaseProvider??"postgres",cacheProvider:e.cacheProvider??"redis",version:e.version,baseUrl:e.baseUrl,credentials:{},demo:!1}}function Wi(e,t){return{version:e.version,initialVersion:e.version,repo:"Duzbee/GenerateSaaS",appName:e.appName,projectName:e.projectName,frontend:e.frontend,architecture:e.architecture,paymentProvider:e.paymentProvider,emailProvider:e.emailProvider,multiTenancy:e.multiTenancy,billingScope:e.billingScope,blog:e.blog,docs:e.docs,desktop:e.desktop,desktopAutoRelease:e.desktopAutoRelease,desktopAi:e.desktopAi,credits:e.credits,revenueSharing:e.revenueSharing,defaultCurrency:e.defaultCurrency,dockerServices:e.dockerServices,socialProviders:e.socialProviders,deploymentTarget:e.deploymentTarget,databaseProvider:e.databaseProvider,cacheProvider:e.cacheProvider,aiTools:e.aiTools,...e.baseUrl?{baseUrl:e.baseUrl}:{},...t&&{licenseToken:t.token,licenseKeyHash:t.keyHash,installId:t.installId}}}async function An(e,t){let n=(await ke(e.projectDir,e.projectDir,Ji)).sort(),o=await Promise.all(n.map(async a=>[Ee(Yi(e.projectDir,a)),await Pt(a)])),i=Object.fromEntries(o),s=Wi(e,t);await l(bn(e.projectDir,pe),JSON.stringify(s,null," ")+`
1235
+ `),await l(bn(e.projectDir,dr),JSON.stringify(i,null," ")+`
1236
+ `)}import{relative as Xi}from"path";async function Ke(e){let r=(await ke(e,e,$e)).sort(),n=await Promise.all(r.map(async o=>[Ee(Xi(e,o)),await Pt(o)]));return Object.fromEntries(n)}import{copyFile as Zi,mkdir as Qi,rm as es}from"fs/promises";import{dirname as ts,join as In,relative as rs}from"path";import{existsSync as ns}from"fs";async function Wt(e,t){ns(t)&&await es(t,{recursive:!0,force:!0});let r=await ke(e,e,$e);for(let n of r){let o=Ee(rs(e,n)),i=In(t,o);await Qi(ts(i),{recursive:!0}),await Zi(n,i)}}async function Pn(e,t){await Wt(e,In(t,gt))}import{existsSync as os}from"fs";import{readFile as Tn,readdir as is}from"fs/promises";import{join as re,dirname as ss,resolve as as,sep as cs}from"path";import{fileURLToPath as ls}from"url";var nt={"claude-code":".claude/skills",cursor:".cursor/skills",codex:".agents/skills","gemini-cli":".gemini/skills",windsurf:".windsurf/skills"},Sp=Object.values(nt),Xt="generatesaas-update",Rn=ss(ls(import.meta.url));function ps(){let e=re(Rn,"skill","content");return os(e)?e:re(Rn,"content")}function Zt(e){return!e||e.length===0?[]:e.map(t=>nt[t])}async function Qt(e,t,r,n){let o=Zt(n);for(let i of o){let s=re(e,i,Xt),a=re(s,"scripts"),d=re(s,"references");await St(a),await St(d),await l(re(s,"SKILL.md"),t.replaceAll("__SKILL_ROOT__",i)),await l(re(d,".gitkeep"),"");for(let[u,m]of Object.entries(r)){let f=as(a,u);f.startsWith(a+cs)&&await l(f,m)}}}async function _n(e,t){let r=ps(),n=await Tn(re(r,"SKILL.md"),"utf-8"),o=re(r,"scripts"),i=await is(o),s={};for(let a of i)a!==".gitkeep"&&(s[a]=await Tn(re(o,a),"utf-8"));await Qt(e,n,s,t)}import{execFile as ds,execFileSync as us}from"child_process";import{access as On,readFile as ms}from"fs/promises";import{join as er}from"path";import*as x from"@clack/prompts";function we(e){try{let t=process.platform==="win32"?"where":"which";return us(t,[e],{stdio:"ignore"}),!0}catch{return!1}}function be(e,t,r,n=3e5){return new Promise((o,i)=>{ds(e,t,{cwd:r,timeout:n},(s,a,d)=>{if(s){let u=String(a||"").trim(),f=[String(d||"").trim(),u].filter(Boolean).join(`
1237
+ `);i(new Error(f?`${s.message}
1238
+ ${f}`:s.message))}else o()})})}async function xn(e){if(!we("pnpm"))return x.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 x.log.warn(`Lockfile regeneration failed: ${r}`),x.log.warn("Deploys using --frozen-lockfile may fail."),!1}}async function Dn(e){if(!we("pnpm"))return x.log.warn("pnpm not found. Skipping dependency installation."),x.log.info("Install pnpm: https://pnpm.io/installation"),!1;let t=x.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 x.log.warn(`pnpm install failed: ${n}`),x.log.warn("You can run it manually later."),!1}}async function Cn(e){if(!we("pnpm"))return!1;let t=x.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 x.log.warn(`Could not generate baseline migration: ${n}`),x.log.warn("Run 'pnpm -F @repo/database generate' before your first deploy."),!1}}async function Nn(e){try{return await On(er(e,".git")),x.log.info("Git repository already exists, skipping init."),!0}catch{}if(!we("git"))return x.log.warn("git not found. Skipping repository initialization."),!1;let t=x.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."),x.log.warn("You can run git init manually later."),!1}}async function Ln(e){if(!we("pnpm"))return!1;try{await On(er(e,".git"))}catch{return!1}try{let t=JSON.parse(await ms(er(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 x.log.warn("Could not install git hooks. Run 'pnpm exec simple-git-hooks' manually."),!1}}import*as ze from"@clack/prompts";import H from"picocolors";function $n(e,t){t.dockerComposeGenerated&&!t.dockerAvailable&&ze.log.warn("Docker not found. Install Docker to run local services: https://docs.docker.com/get-docker/");let r=[];if(r.push(`cd ${e.projectDir}`),t.pnpmInstalled||r.push("pnpm install"),t.dockerComposeGenerated){let i=e.dockerServices.map(s=>Te[s].label).join(", ");r.push(`pnpm infra ${H.dim(`# ${i}`)}`)}if(r.push(`pnpm dev ${H.dim("# http://localhost:3000")}`),t.skippedCredentials.length>0&&(r.push(""),r.push(H.dim("Fill in remaining TODO values in .env"))),ze.note(r.join(`
1239
+ `),H.yellow("Start Development")),t.dockerComposeGenerated){let i=[];i.push(`App ${H.cyan("http://localhost:3000")}`),e.architecture==="separate"&&i.push(`API ${H.cyan("http://localhost:3010")}`),e.dockerServices.includes("mailpit")&&i.push(`Mailpit ${H.cyan("http://localhost:8025")}`),e.dockerServices.includes("inngest")&&i.push(`Inngest ${H.cyan("http://localhost:8288")}`),ze.note(i.join(`
1240
+ `),H.yellow("Dev Tools"))}let n=[],o=fs(e);o.length>0&&n.push(`Set in production: ${H.dim(o.join(", "))}`),n.push("pnpm db:push # Run database migrations"),n.push(gs(e)),ze.note(n.join(`
1241
+ `),H.yellow("Deployment"))}function fs(e){let t=["DATABASE_URL","BETTER_AUTH_SECRET"];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 gs(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 jn(e){let t={};if(e.name!==void 0){if(!mt(e.name))throw new Error(`Invalid project name "${e.name}". Use lowercase letters, numbers, and hyphens only. Must start with a letter.`);t.projectName=e.name}if(e.appName!==void 0){if(!e.appName.trim())throw new Error("App name cannot be empty.");t.appName=e.appName}if(e.location!==void 0?t.projectDir=e.location==="."?process.cwd():e.location:t.projectName!==void 0&&(t.projectDir=`./${t.projectName}`),e.frontend!==void 0){if(!ye.includes(e.frontend))throw new Error(`Invalid frontend "${e.frontend}". Valid values: ${ye.join(", ")}`);t.frontend=e.frontend}if(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.billingScope!==void 0){if(e.org===!1)throw new Error("--billing-scope requires --org to be enabled.");t.billingScope=e.billingScope}if(e.blog!==void 0&&(t.blog=e.blog),e.docs!==void 0&&(t.docs=e.docs),e.desktop!==void 0&&(t.desktop=e.desktop),e.desktopAuto!==void 0&&(t.desktopAutoRelease=e.desktopAuto),e.desktopAi!==void 0){if(e.desktopAi===!0&&e.desktop!==!0)throw new Error("--desktop-ai requires --desktop to be enabled.");t.desktopAi=e.desktopAi}if(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.docker!==void 0&&(t.dockerServices=tr(e.docker,xe,"docker service")),e.aiTools!==void 0&&(t.aiTools=tr(e.aiTools,De,"AI tool")),e.socialProviders!==void 0&&(t.socialProviders=tr(e.socialProviders,Ne,"social provider")),e.currency!==void 0){if(!oe.includes(e.currency))throw new Error(`Invalid currency "${e.currency}". Valid values: ${oe.join(", ")}`);t.defaultCurrency=e.currency}if(e.deploy!==void 0){if(!ie.includes(e.deploy))throw new Error(`Invalid deployment target "${e.deploy}". Valid values: ${ie.join(", ")}`);t.deploymentTarget=e.deploy}if(e.database!==void 0){if(!se.includes(e.database))throw new Error(`Invalid database provider "${e.database}". Valid values: ${se.join(", ")}`);t.databaseProvider=e.database}if(e.cache!==void 0){if(!ae.includes(e.cache))throw new Error(`Invalid cache provider "${e.cache}". Valid values: ${ae.join(", ")}`);t.cacheProvider=e.cache}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 fe={projectName:"my-saas",frontend:"nextjs",architecture:"fullstack",paymentProvider:"stripe",emailProvider:"smtp",multiTenancy:!1,billingScope:"user",blog:!0,docs:!1,desktop:!1,desktopAutoRelease:!1,desktopAi:!1,revenueSharing:!1,credits:!0,dockerServices:["postgres","redis","inngest"],aiTools:[],socialProviders:[],defaultCurrency:"USD",deploymentTarget:"node",databaseProvider:"postgres",cacheProvider:"redis"};function Un(e){let t=e.projectName??fe.projectName,r=e.projectDir??`./${t}`,n=e.appName??ut(t),o=e.deploymentTarget??fe.deploymentTarget,i=Q[o]?.edgeRuntime??!1,s=e.databaseProvider??(i?"neon":fe.databaseProvider),a=e.cacheProvider??(i?"upstash":fe.cacheProvider),d=e.emailProvider??(i?"resend":fe.emailProvider),u=e.dockerServices??(i?fe.dockerServices.filter(f=>f!=="postgres"&&f!=="redis"):fe.dockerServices),m={...fe,...e,projectName:t,appName:n,projectDir:r,deploymentTarget:o,databaseProvider:s,cacheProvider:a,emailProvider:d,dockerServices:u};m.paymentProvider==="none"&&(m.credits=!1);for(let f of Xe){if(m.deploymentTarget!==f.target)continue;let v=m.databaseProvider===f.provider?"database":"cache";if(m.databaseProvider===f.provider||m.cacheProvider===f.provider)throw new Error(`Incompatible: --deploy ${f.target} + --${v} ${f.provider}. ${f.reason}`)}for(let f of Ze)if(m.architecture===f.architecture&&m.deploymentTarget===f.target)throw new Error(`Incompatible: --architecture ${f.architecture} + --deploy ${f.target}. ${f.reason}`);return m}function tr(e,t,r){if(e.trim()==="")return[];let n=e.split(",").map(i=>i.trim()).filter(Boolean),o=n.filter(i=>!t.includes(i));if(o.length>0)throw new Error(`Invalid ${r}(s): ${o.join(", ")}. Valid values: ${t.join(", ")}`);return n}import ks from"picocolors";var bs="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";async function ws(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 As(e){if(e===void 0)return;let t=e.trim().replace(/^v/,"");if(!/^\d+\.\d+\.\d+$/.test(t))throw new Error(`Invalid template version "${e}". Use semver like 1.2.3.`);return t}function Mn(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 Y("--frontend <type>","frontend framework").choices([...ye])).addOption(new Y("--architecture <type>","fullstack or separate").choices([...Re])).addOption(new Y("--payment <provider>","payment provider").choices([..._e])).addOption(new Y("--email <provider>","email provider").choices([...Oe])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").addOption(new Y("--billing-scope <scope>","billing scope (requires --org)").choices([...Ce])).option("--blog","enable blog").option("--no-blog","disable blog").option("--docs","include the docs app (apps/docs, Fumadocs)").option("--no-docs","exclude the docs app").option("--desktop","include the Electron desktop app (apps/desktop)").option("--no-desktop","exclude the Electron desktop app").option("--desktop-auto","auto-trigger the desktop release workflow on CI success (default: manual-only)").option("--no-desktop-auto","manual-only desktop releases (workflow_dispatch)").option("--desktop-ai","include the desktop AI orchestration layer (requires --desktop)").option("--no-desktop-ai","exclude the desktop AI orchestration layer").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)").option("--docker <services>","comma-separated: postgres,redis,inngest,mailpit").option("--ai-tools <tools>","comma-separated: claude-code,cursor,codex,gemini-cli,windsurf").option("--social-providers <providers>","comma-separated: google,github,facebook,discord,x").addOption(new Y("--currency <code>","default currency for billing").choices([...oe])).addOption(new Y("--deploy <target>","deployment target").choices([...ie])).addOption(new Y("--database <provider>","database provider").choices([...se])).addOption(new Y("--cache <provider>","cache provider").choices([...ae])).option("--template-version <version>","specific template version to scaffold").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 Y("--demo","first-party demo build: keep sample content, mark site non-indexable - requires CI API key").hideHelp()).addOption(new Y("--no-db-migration","skip generating the baseline DB migration (internal: demos/CI/playground)").hideHelp()).action(async(t,r)=>{await Is(t?{...r,apiKey:t}:r)})}async function Is(e){let t=performance.now();ar("1.19.0");let r,n;try{r=jn(e),n=As(e.templateVersion)}catch(S){k.cancel(O(S)),process.exit(1)}let o=k.spinner(),i;try{i=await Le({apiKey:e.apiKey,prompt:!e.yes})}catch(S){k.cancel(O(S)),process.exit(1)}e.demo&&Jt(i)!==bs&&(k.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let s=le(i),a=async()=>{let S=await me(s),T=S.latest,N=n??T;if(n&&!S.versions.some(G=>G.version===N))throw new Error(`Template version "${n}" is not available.`);return{latestVersion:T,selectedVersion:N,desktopAllowed:S.entitlements?.desktopAllowed??!0}};o.start("Verifying access...");let d,u,m=!0;try{({latestVersion:d,selectedVersion:u,desktopAllowed:m}=await a()),o.stop("Access verified."),Se(i)}catch(S){if(o.stop("Access verification failed."),S instanceof L&&S.status===401){e.yes&&(k.cancel("Invalid API key. Cannot prompt in non-interactive mode."),process.exit(1)),k.log.warning("Invalid API key."),i=await tt(),s=le(i),o.start("Verifying access...");try{({latestVersion:d,selectedVersion:u,desktopAllowed:m}=await a()),o.stop("Access verified."),Se(i)}catch(T){o.stop("Access verification failed."),k.cancel(T instanceof L&&T.status===401?"Invalid API key.":O(T)),process.exit(1)}}else k.cancel(O(S)),process.exit(1)}k.log.success(`Latest version: ${d}`),u!==d&&k.log.success(`Using template version: ${u}`),r.desktop===!0&&!m&&(k.cancel("The desktop app is available on the Pro plan and up. Upgrade your plan at generatesaas.com."),process.exit(1));let f;e.yes?f=Un(m?r:{...r,desktop:!1}):f=await pr(r,{desktopAllowed:m});let v;o.start("Activating license...");try{let S=e.demo&&f.baseUrl?await ws(f.baseUrl):crypto.randomUUID(),T=()=>({frontend:f.frontend,version:u,installId:S,projectName:f.projectName,options:gr(f)}),N;try{N=await Lt(s,T())}catch(G){let A=ht(G);if(!A?.lastAllowedVersion)throw G;o.stop("License activation failed."),e.yes&&(k.cancel(`${A.message} Re-run with --template-version ${A.lastAllowedVersion}.`),process.exit(1));let Z=await k.confirm({message:`Your update window has ended. Continue with v${A.lastAllowedVersion} (the last version your license covers)?`});(k.isCancel(Z)||!Z)&&(k.cancel("Setup cancelled."),process.exit(0)),u=A.lastAllowedVersion,o.start(`Activating license for v${u}...`),N=await Lt(s,T())}v={token:N.token,keyHash:Jt(i),installId:N.installId??S},o.stop("License activated.")}catch(S){o.stop("License activation failed."),k.cancel(O(S)),process.exit(1)}let b=Es(f.projectDir);if(hs(b)&&ys(b).length>0)if(e.yes)k.log.info(`Directory ${b} is not empty. Merging (keeping existing files, overwriting conflicts).`);else{let T=await k.select({message:`Directory ${b} is not empty.`,options:[{value:"merge",label:"Merge",hint:"keep existing files, overwrite conflicts"},{value:"overwrite",label:"Overwrite",hint:"delete everything and start fresh"},{value:"cancel",label:"Cancel"}]});(k.isCancel(T)||T==="cancel")&&(k.cancel("Setup cancelled."),process.exit(0)),T==="overwrite"&&vs(b,{recursive:!0,force:!0})}let P={...f,projectDir:b,version:u,...e.demo?{docs:!1}:{}};o.start("Downloading template...");try{await yt(s,u,b),o.stop("Template downloaded.")}catch(S){o.stop("Download failed."),k.cancel(O(S)),process.exit(1)}let B;o.start("Generating project files...");try{if({dockerComposeGenerated:B}=await It(P),!e.demo){let S=await Ke(b);await l(Ss(b,ft),JSON.stringify(S,null," ")+`
1242
+ `),await Pn(b,b)}await _n(b,P.aiTools),await An(P,v),o.stop("Project files generated.")}catch(S){o.stop("Generation failed."),k.cancel(O(S)),process.exit(1)}await xn(b);let C=await Dn(b);C&&P.demo!==!0&&e.dbMigration!==!1&&await Cn(b),await Nn(b),C&&await Ln(b);let W=we("docker"),Pe=Ct(P).map(S=>S.key).filter(S=>!P.credentials?.[S]);$n(P,{pnpmInstalled:C,dockerComposeGenerated:B,dockerAvailable:W,skippedCredentials:Pe}),cr(),k.log.info(ks.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as Kn}from"fs";import{readFile as zn}from"fs/promises";import{join as ot,resolve as xs}from"path";import*as D from"@clack/prompts";import He from"picocolors";import{mkdtemp as Ps,rm as Ts}from"fs/promises";import{tmpdir as Rs}from"os";import{join as _s}from"path";async function rr(e,t,r,n){let o=await Ps(_s(Rs(),"generatesaas-stage-"));try{await yt(e,t,o),await It({...r,projectDir:o}),await Wt(o,n)}finally{await Ts(o,{recursive:!0,force:!0})}}var Vn=[{key:"frontend",label:"Frontend framework",hint:"Nuxt (Vue) or Next.js (React).",category:"Project",kind:"enum",default:"nuxt",choices:ye,adoptable:!1,impact:"structural"},{key:"architecture",label:"Architecture",hint:"Fullstack (frontend hosts the API) or a standalone Hono backend.",category:"Infrastructure",kind:"enum",default:"fullstack",choices:Re,adoptable:!1,impact:"structural"},{key:"deploymentTarget",label:"Deployment target",hint:"Node.js / Docker or Vercel serverless.",category:"Infrastructure",kind:"enum",default:"node",choices:ie,adoptable:!1,impact:"config"},{key:"databaseProvider",label:"Database provider",hint:"Self-hosted PostgreSQL, Neon, or Supabase.",category:"Infrastructure",kind:"enum",default:"postgres",choices:se,adoptable:!1,impact:"config"},{key:"cacheProvider",label:"Cache provider",hint:"Self-hosted Redis or Upstash.",category:"Infrastructure",kind:"enum",default:"redis",choices:ae,adoptable:!1,impact:"config"},{key:"paymentProvider",label:"Payment provider",hint:"Stripe, Polar, or none.",category:"Features",kind:"enum",default:"none",choices:_e,adoptable:!1,impact:"structural"},{key:"defaultCurrency",label:"Default currency",hint:"Billing and pricing currency.",category:"Features",kind:"enum",default:"USD",choices:oe,adoptable:!1,impact:"config"},{key:"emailProvider",label:"Email provider",hint:"SMTP, Amazon SES, or Resend.",category:"Features",kind:"enum",default:"smtp",choices:Oe,adoptable:!1,impact:"config"},{key:"multiTenancy",label:"Multi-tenancy (organizations)",hint:"Adds organizations - teams, members, and shared resources.",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural"},{key:"billingScope",label:"Billing scope",hint:"Whether subscriptions belong to a user or an organization.",category:"Features",kind:"enum",default:"user",choices:Ce,adoptable:!1,impact:"config",requires:e=>e.multiTenancy===!0,requiresLabel:"requires multi-tenancy"},{key:"blog",label:"Blog",hint:"Adds the marketing blog (content collection, list and post pages).",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural"},{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"},{key:"desktop",label:"Desktop app",hint:"Adds the cross-platform Electron desktop app (apps/desktop).",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural"},{key:"desktopAutoRelease",label:"Desktop auto-release",hint:"Release the desktop app on every CI success (vs manual-only).",category:"Features",kind:"boolean",default:!0,adoptable:!1,impact:"config",requires:e=>e.desktop===!0,requiresLabel:"requires the desktop app"},{key:"desktopAi",label:"Desktop AI orchestration",hint:"Adds the desktop AI agents layer (requires the desktop app).",category:"Features",kind:"boolean",default:!1,adoptable:!0,impact:"structural",requires:e=>e.desktop===!0,requiresLabel:"requires the desktop 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",requires:e=>e.paymentProvider!==void 0&&e.paymentProvider!=="none",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"},{key:"socialProviders",label:"Social login providers",hint:"Which social sign-in buttons the auth screen shows.",category:"Features",kind:"multiselect",default:[],choices:Ne,adoptable:!1,impact:"config"},{key:"dockerServices",label:"Docker services",hint:"Local services scaffolded in docker-compose.",category:"Tooling",kind:"multiselect",default:[],choices:xe,adoptable:!1,impact:"config"},{key:"aiTools",label:"AI coding tools",hint:"Which AI assistants receive the bundled skill files.",category:"Tooling",kind:"multiselect",default:[],choices:De,adoptable:!1,impact:"config"}];function Os(e,t){return e[t]!==void 0}function Fn(e){return Vn.filter(t=>t.adoptable&&!Os(e,t.key)&&(t.requires?t.requires(e):!0)).map(t=>({key:t.key,label:t.label,hint:t.hint,kind:t.kind,default:t.default,...t.choices?{choices:t.choices}:{},impact:t.impact,...t.requiresLabel?{requiresLabel:t.requiresLabel}:{}}))}function Bn(e){let t={};for(let r of Vn)t[r.key]=Array.isArray(r.default)?[...r.default]:r.default;return{...t,...e}}function Gn(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 Tt(e,t){let r=Gn(e),n=Gn(t);if(!r||!n)return 0;for(let o=0;o<3;o++)if(r[o]!==n[o])return r[o]-n[o];return 0}function Hn(e){e.command("update").description("Update AI skill files and stage template updates").argument("[mode]","pass 'auto' to mark the staged update for unattended apply by your AI assistant").option("--cwd <path>","project directory (default: current directory)").action(async(t,r)=>{let n=t==="auto"?"auto":void 0,o=xs(r.cwd??process.cwd()),i=ot(o,pe),s;try{s=JSON.parse(await zn(i,"utf-8"))}catch{D.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let a;try{a=await Le()}catch(m){D.cancel(O(m)),process.exit(1)}let d=le(a),u=D.spinner();try{u.start("Verifying access...");let m;try{m=await me(d)}catch(A){throw A instanceof L&&A.status===401?new Error("Your saved API key was rejected. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):A}u.stop("Access verified."),Se(a),u.start("Fetching latest skill files...");let f=await fr(d,m.latest);await Qt(o,f.skillMd,f.scripts,s.aiTools);let v=Zt(s.aiTools);if(u.stop("Skills updated."),D.log.success(`Skill files installed to ${He.cyan(v.length.toString())} locations.`),s.version===m.latest){D.log.info(`Already on the latest version (${s.version}).`);return}let b=Fn(s),P=Bn(s),B=JSON.stringify(P)!==JSON.stringify(s);if(s=P,s.licenseToken)try{let A=await hr(d,{currentToken:s.licenseToken,newVersion:m.latest});s.licenseToken=A.token,A.licenseKeyHash&&(s.licenseKeyHash=A.licenseKeyHash),await l(i,JSON.stringify(s,null," ")+`
1243
+ `),B=!1,D.log.success("License refreshed.")}catch(A){let Z=ht(A);Z&&(D.cancel(Z.message),process.exit(1)),D.log.warn("License refresh skipped.")}B&&await l(i,JSON.stringify(s,null," ")+`
1244
+ `);let C=wn(s,o),W=ot(o,ur);u.start(`Staging v${m.latest} (shaped for your config)...`),await rr(d,m.latest,C,W),u.stop("Template staged.");let{text:X,title:Pe}=await Ds(d,m,s.version);X&&D.note(X,Pe);let S=ot(o,ft),T=ot(o,gt),N=!Kn(T),G=!Kn(S);if(N){if(u.start("Building baseline template (one-time migration)..."),await rr(d,s.version,C,T),G){let A=await Ke(T);await l(S,JSON.stringify(A,null," ")+`
1245
+ `)}if(u.stop("Baseline template stored."),!G){let A=await Cs(S,T);A>0&&D.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(G){u.start("Computing baseline template hashes...");let A=await Ke(T);await l(S,JSON.stringify(A,null," ")+`
1246
+ `),u.stop("Baseline hashes computed.")}if(await l(ot(o,mr),JSON.stringify({currentVersion:s.version,targetVersion:m.latest,changelog:X,stagedAt:new Date().toISOString(),...b.length>0?{newOptions:b}:{},...n?{mode:n}:{}},null," ")+`
1247
+ `),D.log.info(`Update staged: ${He.cyan(s.version)} \u2192 ${He.cyan(m.latest)}`),s.aiTools&&s.aiTools.length>0){let A=s.aiTools[0],Z=We[A].label;D.log.info(`Open your project in ${He.cyan(Z)} and ask: ${He.cyan("'update my GenerateSaaS project'")}`)}else D.log.info(`Ask your AI coding assistant to ${He.cyan("'update my GenerateSaaS project'")}.`)}catch(m){u.stop("Failed."),D.cancel(`Update failed: ${O(m)}`),process.exit(1)}})}async function Ds(e,t,r){let n=t.latest,o=t.versions.filter(s=>Tt(s.version,r)>0&&Tt(s.version,n)<=0).sort((s,a)=>Tt(s.version,a.version));if(o.length<=1)return{text:await Nt(e,n),title:`Changelog v${n}`};let i=[];for(let s of o){let a=null;try{a=await Nt(e,s.version)}catch{a=null}let d=s.date?` (${s.date.slice(0,10)})`:"",u=s.breaking?" [BREAKING]":"";i.push(`# v${s.version}${d}${u}
1195
1248
 
1196
1249
  ${a??"_No changelog available for this release._"}`)}return{text:i.join(`
1197
1250
 
1198
- `),title:`Changelog v${r} \u2192 v${n}`}}async function bs(e,t){let r=JSON.parse(await Un(e,"utf-8")),n=await De(t),o=0;for(let[i,s]of Object.entries(r))Ie(i)||n[i]!==s&&o++;for(let i of Object.keys(n))i in r||o++;return o}import*as j from"@clack/prompts";import G from"picocolors";import{readFile as ks}from"fs/promises";import{join as As,resolve as Is}from"path";function Vn(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=Is(t.cwd??process.cwd()),n=As(r,re),o;try{o=JSON.parse(await ks(n,"utf-8"))}catch{j.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let i=[`Version: ${G.cyan(o.version)}`,`Frontend: ${G.cyan(o.frontend)}`,o.deploymentTarget?`Deploy target: ${G.cyan(o.deploymentTarget)}`:null,o.databaseProvider?`Database: ${G.cyan(o.databaseProvider)}`:null,o.cacheProvider?`Cache: ${G.cyan(o.cacheProvider)}`:null,o.aiTools&&o.aiTools.length>0?`AI tools: ${G.cyan(o.aiTools.join(", "))}`:null].filter(Boolean).join(`
1199
- `);j.note(i,G.bold("Project Status"));let s=j.spinner();s.start("Checking for updates...");try{let a=await Ae(),d=Z(a),h=(await ie(d)).latest;o.version===h?(s.stop("Up to date."),j.log.success(`Already on the latest version (${G.green(h)})`)):(s.stop("Update available."),j.log.warning(`Update available: ${G.yellow(o.version)} \u2192 ${G.green(h)}`),j.log.info(`Open this project in your AI coding agent and ask it to ${G.cyan("update my GenerateSaaS project")} - it fetches and applies the update for you.`))}catch(a){s.stop("Check failed."),a instanceof x&&a.status===401?j.log.warning("Invalid API key. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):j.log.warning(`Could not check for updates: ${I(a)}`)}})}import{readFile as Ts}from"fs/promises";import*as A from"@clack/prompts";import k from"picocolors";function Ps(){return process.env.GENERATESAAS_API_KEY??Ke()}function _s(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 Rs(e){return{verdict:e.verdict??"unknown",ejectedAt:e.ejectedAt??null}}function Os(e){switch(e.verdict){case"licensed":return A.log.success(`${k.green("LICENSED")} - resolves to an account with an active${e.plan?` ${e.plan}`:""} license.`),!0;case"ejected":return A.log.success(`${k.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 A.log.error(`${k.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 A.log.error(`${k.red("LEAKED TOKEN")} - this license belongs to a different deployment${e.mismatchDomain?` (${k.cyan(e.mismatchDomain)})`:""}, not this site. The token was copied from a licensed project.`),!1;case"no_license_history":return A.log.error(`${k.red("NO LICENSE HISTORY")} - no license has ever been associated with this site. If it runs GenerateSaaS, treat it as unlicensed.`),!1;default:return A.log.warn(`${k.yellow("UNKNOWN")} - could not cross-reference the records right now. Try again shortly.`),!1}}async function Mn(e,t){let r=process.env.GENERATESAAS_API_URL??Ge,n=Ps();e.start("Cross-referencing license records...");try{let o=n?_s(await mr(r,n,{lkh:t.lkh,nid:t.nid,domain:t.domain})):Rs(await Ct(r,{token:t.token,domain:t.domain}));return e.stop(`${k.green("Checked")} - records cross-referenced`),Os(o)}catch(o){return e.stop(`${k.yellow("Skipped")} - ${I(o)}`),null}}function xs(e){let t=e.split(".");if(t.length!==3||!t[1])throw new Error("Invalid JWT format");let r=Buffer.from(t[1],"base64url").toString("utf-8");return JSON.parse(r)}function Qt(e){return typeof e!="number"?"unknown":new Date(e*1e3).toISOString().split("T")[0]}function Fn(e){A.note([`License ID: ${k.cyan(String(e.lid??"unknown"))}`,`Version: ${k.cyan(String(e.ver??"unknown"))}`,`Init version: ${String(e.iver??"unknown")}`,`Frontend: ${String(e.fe??"unknown")}`,`Created: ${Qt(e.pat)}`,`Last updated: ${Qt(e.uat)}`,`Expires: ${Qt(e.exp)}`,`Install ID: ${String(e.nid??"unknown")}`].join(`
1200
- `),k.yellow("License Details"))}function Ds(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 Bn(e){let t=A.spinner(),r=null,n="no candidates";for(let s of Ds(e)){t.start(`Checking ${s}...`);try{let a=await fetch(s);if(!a.ok){n=`${s} returned ${a.status}`,t.stop(`${k.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(`${k.yellow("Not here")} - ${n}`);continue}r=d,t.stop(`${k.green("Found")} - license endpoint responded`);break}catch(a){n=`${s}: ${I(a)}`,t.stop(`${k.yellow("Unreachable")} - ${n}`)}}if(r===null){A.log.warn(`No license endpoint found (last: ${n}). The site may be ejected, not a GenerateSaaS app, or serving its API elsewhere.`);let s=Gn(e);return s?await Mn(t,{domain:s})??!1:!1}let o;try{o=xs(r)}catch{return A.log.error("Could not decode JWT payload."),!1}t.start("Verifying signature...");try{let s=process.env.GENERATESAAS_API_URL??Ge,a=await Ct(s,{token:r});if(a.valid)t.stop(`${k.green("Valid")} - signature verified`);else return t.stop(`${k.red("Invalid")} - ${a.reason}`),!1}catch{return t.stop(`${k.yellow("Skipped")} - could not reach verification service`),A.log.warn("Signature not verified. Displaying unverified claims:"),Fn(o),!1}return Fn(o),await Mn(t,{token:r,lkh:typeof o.lkh=="string"?o.lkh:void 0,nid:typeof o.nid=="string"?o.nid:void 0,domain:Gn(e)})??!0}function Gn(e){try{return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`).hostname}catch{return}}function Kn(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&&(A.cancel("Provide a URL or --file <path>."),process.exit(1)),r.file){let o=(await Ts(r.file,"utf-8")).split(`
1201
- `).map(s=>s.trim()).filter(s=>s&&!s.startsWith("#"));o.length===0&&(A.cancel("No URLs found in file."),process.exit(1));let i=0;for(let s of o)await Bn(s)&&i++,A.log.info("");A.log.success(`${i}/${o.length} sites verified.`)}else await Bn(t)||process.exit(1)})}import{existsSync as Cs,rmSync as Ns}from"fs";import*as K from"@clack/prompts";function zn(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){Cs(X)?(Ns(X),K.log.success("API key removed.")):K.log.info("No API key configured.");return}let r=Ke();r?K.log.info(`Current API key: ****${r.slice(-4)}`):K.log.info("No API key configured.");let n=await ze(),o=Z(n),i=K.spinner();i.start("Verifying API key...");try{await ie(o),i.stop("API key verified."),he(n),K.log.success("API key saved.")}catch(s){i.stop("Verification failed."),s instanceof x&&s.status===401?K.cancel("Invalid API key."):K.cancel(I(s)),process.exit(1)}})}import{existsSync as It,rmSync as Ls,readFileSync as tr,writeFileSync as Hn}from"fs";import{join as we}from"path";import*as C from"@clack/prompts";var $s=["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"],Us=[{file:"packages/api/src/routes/inngest.ts",removals:[`import { licenseHeartbeatFunction } from "../functions/maintenance/license-heartbeat";
1251
+ `),title:`Changelog v${r} \u2192 v${n}`}}async function Cs(e,t){let r=JSON.parse(await zn(e,"utf-8")),n=await Ke(t),o=0;for(let[i,s]of Object.entries(r))$e(i)||n[i]!==s&&o++;for(let i of Object.keys(n))i in r||o++;return o}import*as F from"@clack/prompts";import q from"picocolors";import{readFile as Ns}from"fs/promises";import{join as Ls,resolve as $s}from"path";function Yn(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=$s(t.cwd??process.cwd()),n=Ls(r,pe),o;try{o=JSON.parse(await Ns(n,"utf-8"))}catch{F.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let i=[`Version: ${q.cyan(o.version)}`,`Frontend: ${q.cyan(o.frontend)}`,o.deploymentTarget?`Deploy target: ${q.cyan(o.deploymentTarget)}`:null,o.databaseProvider?`Database: ${q.cyan(o.databaseProvider)}`:null,o.cacheProvider?`Cache: ${q.cyan(o.cacheProvider)}`:null,o.aiTools&&o.aiTools.length>0?`AI tools: ${q.cyan(o.aiTools.join(", "))}`:null].filter(Boolean).join(`
1252
+ `);F.note(i,q.bold("Project Status"));let s=F.spinner();s.start("Checking for updates...");try{let a=await Le(),d=le(a),m=(await me(d)).latest;o.version===m?(s.stop("Up to date."),F.log.success(`Already on the latest version (${q.green(m)})`)):(s.stop("Update available."),F.log.warning(`Update available: ${q.yellow(o.version)} \u2192 ${q.green(m)}`),F.log.info(`Open this project in your AI coding agent and ask it to ${q.cyan("update my GenerateSaaS project")} - it fetches and applies the update for you.`))}catch(a){s.stop("Check failed."),a instanceof L&&a.status===401?F.log.warning("Invalid API key. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):F.log.warning(`Could not check for updates: ${O(a)}`)}})}import{readFile as js}from"fs/promises";import*as _ from"@clack/prompts";import R from"picocolors";function Us(){return process.env.GENERATESAAS_API_KEY??et()}function Ms(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 Vs(e){return{verdict:e.verdict??"unknown",ejectedAt:e.ejectedAt??null}}function Fs(e){switch(e.verdict){case"licensed":return _.log.success(`${R.green("LICENSED")} - resolves to an account with an active${e.plan?` ${e.plan}`:""} license.`),!0;case"ejected":return _.log.success(`${R.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(`${R.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(`${R.red("LEAKED TOKEN")} - this license belongs to a different deployment${e.mismatchDomain?` (${R.cyan(e.mismatchDomain)})`:""}, not this site. The token was copied from a licensed project.`),!1;case"no_license_history":return _.log.error(`${R.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(`${R.yellow("UNKNOWN")} - could not cross-reference the records right now. Try again shortly.`),!1}}async function qn(e,t){let r=process.env.GENERATESAAS_API_URL??Qe,n=Us();e.start("Cross-referencing license records...");try{let o=n?Ms(await yr(r,n,{lkh:t.lkh,nid:t.nid,domain:t.domain})):Vs(await $t(r,{token:t.token,domain:t.domain}));return e.stop(`${R.green("Checked")} - records cross-referenced`),Fs(o)}catch(o){return e.stop(`${R.yellow("Skipped")} - ${O(o)}`),null}}function Bs(e){let t=e.split(".");if(t.length!==3||!t[1])throw new Error("Invalid JWT format");let r=Buffer.from(t[1],"base64url").toString("utf-8");return JSON.parse(r)}function nr(e){return typeof e!="number"?"unknown":new Date(e*1e3).toISOString().split("T")[0]}function Jn(e){_.note([`License ID: ${R.cyan(String(e.lid??"unknown"))}`,`Version: ${R.cyan(String(e.ver??"unknown"))}`,`Init version: ${String(e.iver??"unknown")}`,`Frontend: ${String(e.fe??"unknown")}`,`Created: ${nr(e.pat)}`,`Last updated: ${nr(e.uat)}`,`Expires: ${nr(e.exp)}`,`Install ID: ${String(e.nid??"unknown")}`].join(`
1253
+ `),R.yellow("License Details"))}function Gs(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 Wn(e){let t=_.spinner(),r=null,n="no candidates";for(let s of Gs(e)){t.start(`Checking ${s}...`);try{let a=await fetch(s);if(!a.ok){n=`${s} returned ${a.status}`,t.stop(`${R.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(`${R.yellow("Not here")} - ${n}`);continue}r=d,t.stop(`${R.green("Found")} - license endpoint responded`);break}catch(a){n=`${s}: ${O(a)}`,t.stop(`${R.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=Xn(e);return s?await qn(t,{domain:s})??!1:!1}let o;try{o=Bs(r)}catch{return _.log.error("Could not decode JWT payload."),!1}t.start("Verifying signature...");try{let s=process.env.GENERATESAAS_API_URL??Qe,a=await $t(s,{token:r});if(a.valid)t.stop(`${R.green("Valid")} - signature verified`);else return t.stop(`${R.red("Invalid")} - ${a.reason}`),!1}catch{return t.stop(`${R.yellow("Skipped")} - could not reach verification service`),_.log.warn("Signature not verified. Displaying unverified claims:"),Jn(o),!1}return Jn(o),await qn(t,{token:r,lkh:typeof o.lkh=="string"?o.lkh:void 0,nid:typeof o.nid=="string"?o.nid:void 0,domain:Xn(e)})??!0}function Xn(e){try{return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`).hostname}catch{return}}function Zn(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 o=(await js(r.file,"utf-8")).split(`
1254
+ `).map(s=>s.trim()).filter(s=>s&&!s.startsWith("#"));o.length===0&&(_.cancel("No URLs found in file."),process.exit(1));let i=0;for(let s of o)await Wn(s)&&i++,_.log.info("");_.log.success(`${i}/${o.length} sites verified.`)}else await Wn(t)||process.exit(1)})}import{existsSync as Ks,rmSync as zs}from"fs";import*as J from"@clack/prompts";function Qn(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){Ks(ce)?(zs(ce),J.log.success("API key removed.")):J.log.info("No API key configured.");return}let r=et();r?J.log.info(`Current API key: ****${r.slice(-4)}`):J.log.info("No API key configured.");let n=await tt(),o=le(n),i=J.spinner();i.start("Verifying API key...");try{await me(o),i.stop("API key verified."),Se(n),J.log.success("API key saved.")}catch(s){i.stop("Verification failed."),s instanceof L&&s.status===401?J.cancel("Invalid API key."):J.cancel(O(s)),process.exit(1)}})}import{existsSync as Rt,rmSync as Hs,readFileSync as ir,writeFileSync as eo}from"fs";import{join as Ae}from"path";import*as j from"@clack/prompts";var Ys=["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"],qs=[{file:"packages/api/src/routes/inngest.ts",removals:[`import { licenseHeartbeatFunction } from "../functions/maintenance/license-heartbeat";
1202
1255
  `,` licenseHeartbeatFunction,
1203
1256
  `]},{file:"packages/api/src/routes/internal/index.ts",removals:[`import licenseRoutes from "./license";
1204
1257
  `,` .route("/license", licenseRoutes)
1205
- `]}];function js(e){return(e&&e.length>0?e.map(r=>Ye[r]):Object.values(Ye)).map(r=>we(r,Yt))}function er(e){return It(e)?(Ls(e,{recursive:!0}),!0):!1}function Vs(e,t){if(!It(e))return!1;let r=tr(e,"utf-8"),n=r;for(let o of t)n=n.replace(o,"");return n===r?!1:(Hn(e,n,"utf-8"),!0)}function Ms(e){let t=we(e,".gitignore");if(!It(t))return!1;let r=tr(t,"utf-8"),n=r.split(`
1258
+ `]}];function Js(e){return(e&&e.length>0?e.map(r=>nt[r]):Object.values(nt)).map(r=>Ae(r,Xt))}function or(e){return Rt(e)?(Hs(e,{recursive:!0}),!0):!1}function Ws(e,t){if(!Rt(e))return!1;let r=ir(e,"utf-8"),n=r;for(let o of t)n=n.replace(o,"");return n===r?!1:(eo(e,n,"utf-8"),!0)}function Xs(e){let t=Ae(e,".gitignore");if(!Rt(t))return!1;let r=ir(t,"utf-8"),n=r.split(`
1206
1259
  `).filter(o=>!o.includes(".generatesaas")).join(`
1207
- `);return n===r?!1:(Hn(t,n,"utf-8"),!0)}function Yn(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=we(t,re),n;try{n=JSON.parse(tr(r,"utf-8"))}catch{C.cancel("No GenerateSaaS project found in this directory."),process.exit(1)}let o=await C.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(C.isCancel(o)&&(C.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)}),C.log.info("Recorded the opt-out with generatesaas.com (final event - nothing is sent after this).")}catch{C.log.warn("Could not reach generatesaas.com to record the opt-out. Ejecting anyway.")}let i=[],s=[];for(let a of js(n.aiTools))er(we(t,a))&&i.push(a);for(let a of $s)er(we(t,a))&&i.push(a);er(we(t,M))&&i.push(M+"/");for(let a of Us){let d=we(t,a.file);Vs(d,a.removals)?s.push(a.file):It(d)&&C.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}Ms(t)&&s.push(".gitignore");for(let a of i)C.log.info(`Deleted ${a}`);for(let a of s)C.log.info(`Modified ${a}`);C.log.success("Ejected successfully. This project is now fully standalone.")})}var be=new Fs().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("1.18.0").addHelpText("after",`
1260
+ `);return n===r?!1:(eo(t,n,"utf-8"),!0)}function to(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=Ae(t,pe),n;try{n=JSON.parse(ir(r,"utf-8"))}catch{j.cancel("No GenerateSaaS project found in this directory."),process.exit(1)}let o=await j.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(j.isCancel(o)&&(j.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)}),j.log.info("Recorded the opt-out with generatesaas.com (final event - nothing is sent after this).")}catch{j.log.warn("Could not reach generatesaas.com to record the opt-out. Ejecting anyway.")}let i=[],s=[];for(let a of Js(n.aiTools))or(Ae(t,a))&&i.push(a);for(let a of Ys)or(Ae(t,a))&&i.push(a);or(Ae(t,K))&&i.push(K+"/");for(let a of qs){let d=Ae(t,a.file);Ws(d,a.removals)?s.push(a.file):Rt(d)&&j.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}Xs(t)&&s.push(".gitignore");for(let a of i)j.log.info(`Deleted ${a}`);for(let a of s)j.log.info(`Modified ${a}`);j.log.success("Ejected successfully. This project is now fully standalone.")})}var Ie=new Zs().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("1.19.0").addHelpText("after",`
1208
1261
  Examples:
1209
1262
  $ generatesaas init Interactive setup
1210
1263
  $ generatesaas init -n my-app -y Quick setup with defaults
1211
1264
  $ generatesaas status Check for updates
1212
1265
  $ generatesaas auth Set or update API key
1213
- `);Nn(be);jn(be);Vn(be);Kn(be);zn(be);Yn(be);be.parseAsync().catch(e=>{Jn.cancel("An unexpected error occurred."),console.error(e),process.exit(1)});
1266
+ `);Mn(Ie);Hn(Ie);Yn(Ie);Zn(Ie);Qn(Ie);to(Ie);Ie.parseAsync().catch(e=>{ro.cancel("An unexpected error occurred."),console.error(e),process.exit(1)});
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: update
3
- description: Update a GenerateSaaS project to the latest boilerplate version. Use this whenever the user asks to "update my GenerateSaaS project", update their GenerateSaaS boilerplate/template, or pull the latest GenerateSaaS changes. Fetches and stages the new version, classifies files, analyzes dependencies, and applies changes while preserving user customizations - the user controls every decision.
3
+ description: Update a GenerateSaaS project to the latest boilerplate version. Use this whenever the user asks to "update my GenerateSaaS project", "update auto", update their GenerateSaaS boilerplate/template, or pull the latest GenerateSaaS changes. Fetches and stages the new version, surfaces newly available options, classifies files, analyzes dependencies, and applies changes while preserving user customizations - the user controls every decision. Saying "auto" runs the whole update unattended, stopping only for risky or uncertain decisions.
4
4
  ---
5
5
 
6
6
  # GenerateSaaS Update
@@ -20,6 +20,7 @@ Update your project to the latest GenerateSaaS boilerplate version while preserv
20
20
  Step 0: Pre-flight safety checks
21
21
  Step 1: Fetch + stage + prepare update data (you run the CLI, then the script)
22
22
  Step 2: Present changelog to user
23
+ Step 2.5: Review newly available options - adopt or keep default (re-stage if adopted)
23
24
  Step 3: Classify files (script)
24
25
  Step 4: Analyze dependencies
25
26
  Step 5: Present update plan - user chooses interaction mode
@@ -33,6 +34,15 @@ Step 9: Complete update (script)
33
34
  everything end-to-end - including fetching and staging the new version (Step 1).
34
35
  The user does not run any CLI command themselves.
35
36
 
37
+ **Automatic mode.** If the user's request includes the word "auto" (e.g. "update auto",
38
+ "update my GenerateSaaS project, auto"), or `staging.json` has `"mode": "auto"` (set by
39
+ `generatesaas update auto`), run the entire update **unattended** in Automatic mode: skip
40
+ the Step 5 mode prompt and proceed start to finish, making the best decisions to bring in
41
+ upstream changes while preserving the user's customizations. Automatic mode still **stops to
42
+ ask** whenever a decision is genuinely risky or you are not confident - and it always runs
43
+ the Step 2.5 new-options review (surfacing a new feature is never a decision to assume). The
44
+ exact always-ask triggers are listed in Step 5 and "When to Always Ask".
45
+
36
46
  **What the staged update contains (important):** `generatesaas update` re-shapes the
37
47
  new version's template into THIS project's exact configuration before staging - same
38
48
  frontend, payment provider, database, feature flags, etc. So everything in
@@ -136,6 +146,58 @@ Then ask: *"Ready to proceed with the file analysis?"*
136
146
 
137
147
  Wait for the user to confirm before continuing.
138
148
 
149
+ ### Step 2.5: Review Newly Available Options
150
+
151
+ The boilerplate adds features over time. When this project predates one, `generatesaas update`
152
+ records it in `staging.json` under `newOptions` (and has already default-filled the manifest so
153
+ it stays complete). Read `newOptions` from `.generatesaas/staging.json`. **If it is absent or
154
+ empty, skip this step entirely.**
155
+
156
+ Otherwise, present each option so the user can decide whether to adopt it. Each entry has
157
+ `{ key, label, hint, default, impact, requiresLabel? }`. For each one:
158
+
159
+ ```
160
+ ### New since your version: {label}
161
+
162
+ {hint}
163
+ {If impact is "structural": "This adds a new app/package to your project."}
164
+ {If impact is "config": "This flips a configuration flag - a small, contained change."}
165
+
166
+ **Default:** keep it off (your project's current behavior is unchanged).
167
+ **My recommendation:** {your recommendation - e.g. "Keep off unless you want the desktop app;
168
+ it is a large surface you didn't originally choose" or "Safe to adopt - it is purely additive"}
169
+
170
+ > **What would you like to do?**
171
+ > 1. **Keep the default** - don't add this
172
+ > 2. **Adopt it** - turn this feature on
173
+ ```
174
+
175
+ **Ask the user about every new option, in every mode - including Automatic.** Surfacing a new
176
+ feature is exactly the kind of decision the update must never assume. Give your recommendation,
177
+ but the user decides.
178
+
179
+ **Applying the decisions:**
180
+
181
+ 1. For each option the user **adopts**, write its enabling value into `.generatesaas/manifest.json`
182
+ (e.g. set `"desktop": true`). The manifest is the single shaping input.
183
+ 2. **If the user adopted at least one option, re-stage once** so re-shaping produces the
184
+ feature's files. Re-run the CLI (append `auto` if you are in Automatic mode, so the staged
185
+ metadata keeps the unattended signal):
186
+
187
+ ```bash
188
+ npx generatesaas@latest update
189
+ ```
190
+
191
+ Because the manifest now carries the adopted flags, the new `.generatesaas/staging/` includes
192
+ the feature - config flags and whole new trees alike - with no hand-editing. This is why
193
+ adoption is handled the same way for `config` and `structural` options: both are applied by
194
+ re-shaping, never by mimicking the generator. The adopted keys are now present in the
195
+ manifest, so they will not re-appear in `newOptions`.
196
+ 3. Re-read `staging.json`, then continue to Step 3. Classification runs on the final staging, so
197
+ the adopted feature's files are classified as `new` and created in Step 6. Nothing is applied
198
+ before this point, so a re-stage never leaves a half-applied tree.
199
+ 4. If the user adopted nothing, continue to Step 3 directly.
200
+
139
201
  ### Step 3: Classify Files
140
202
 
141
203
  ```bash
@@ -204,21 +266,52 @@ Removed in the new version - you decide whether to delete:
204
266
  {Empty - items move here during execution with reasons}
205
267
  ```
206
268
 
207
- Then ask the user to choose their interaction level:
269
+ **If Automatic mode was triggered** (the request contained "auto", or `staging.json.mode === "auto"`),
270
+ **skip this prompt** - do not ask the user to choose a mode. Use Automatic mode (described below)
271
+ and proceed. Note in the plan that Automatic mode was selected.
272
+
273
+ Otherwise, ask the user to choose their interaction level:
208
274
 
209
275
  > **How would you like to handle this update?**
210
276
  >
211
277
  > 1. **Recommended** - Auto-apply safe files, then I'll walk you through each modified file with my recommendation. You approve or adjust each one.
212
278
  > 2. **Careful** - I'll present every single change for your approval, including auto-updates. Nothing happens without your explicit OK.
213
279
  > 3. **Quick** - Auto-apply safe files, auto-apply my recommended merges for low-risk files, and only ask you about medium/high-risk changes.
280
+ > 4. **Automatic** - Run the whole update unattended: auto-apply safe files and auto-merge low- and medium-risk files I'm confident about, only stopping to ask when a decision is genuinely risky or I'm unsure. (This is what `update auto` selects.)
214
281
 
215
282
  **Wait for the user's choice.** Default to **Recommended** if they just say "proceed" or similar.
216
283
 
284
+ #### Automatic mode behavior
285
+
286
+ In Automatic mode, run the entire flow without prompting **except** at the mandatory stops below:
287
+
288
+ - **Auto-apply** safe (`unmodified`) and genuinely-`new` files, same as Recommended.
289
+ - **Auto-merge Low AND Medium risk** modified files when you are confident the merge preserves
290
+ the user's customizations (this is the difference from Quick, which stops at Low). Report each
291
+ merge you apply.
292
+ - **Always stop and ask** - even in Automatic mode - for:
293
+ - High-risk or structurally conflicting files, and files the user heavily rewrote (>30% diverged).
294
+ - Database schema changes (never auto-merge schema).
295
+ - `.env.example` changes that imply a decision (new required variables).
296
+ - Removed-upstream files that other project code imports (run the import search first).
297
+ - Renames where the user customized the old path (porting customizations is a judgment call).
298
+ - **Any merge you are not confident preserves the user's intent.** When unsure, ask. It is
299
+ always better to interrupt than to assume.
300
+ - **Removed files:** auto-delete only when the user never modified the file AND no importers are
301
+ found; otherwise ask.
302
+ - **New files in deleted trees (`newInDeletedTree`):** keep them deleted (never resurrect a tree
303
+ the user removed); note it in the summary, no prompt needed.
304
+ - **Validation (Step 8):** run the type check and the test suite; if the update introduced
305
+ breakage you can fix with confidence, fix it and re-verify; if not, stop and ask.
306
+
307
+ The Step 2.5 new-options review still runs and still asks per option - Automatic mode never
308
+ auto-adopts a new feature.
309
+
217
310
  ### Step 6: Apply Safe Auto-Updates
218
311
 
219
312
  Based on the user's chosen mode:
220
313
 
221
- **Recommended / Quick mode:**
314
+ **Recommended / Quick / Automatic mode:**
222
315
 
223
316
  Preview first (especially useful in Careful mode or when unsure), then apply:
224
317
  ```bash
@@ -277,6 +370,8 @@ If `.generatesaas/template/<path>` does not exist (backward compat), fall back t
277
370
 
278
371
  **In Quick mode:** For **Low risk** files, apply the recommended merge automatically and report what was done. For **Medium/High risk**, always ask.
279
372
 
373
+ **In Automatic mode:** For **Low and Medium risk** files, apply the recommended merge automatically when you are confident it preserves the user's customizations, and report what was done. For **High risk** files - and any merge you are not confident about - always ask (see "Automatic mode behavior" in Step 5).
374
+
280
375
  **In Recommended mode:** Present each file and wait for the user's decision.
281
376
 
282
377
  **In Careful mode:** Present each file with the full proposed merged code shown, and wait for the user's decision.
@@ -499,7 +594,7 @@ When skipping, always explain:
499
594
 
500
595
  ## When to Always Ask
501
596
 
502
- Even in Quick mode, always ask the user about:
597
+ Even in Quick or Automatic mode, always ask the user about:
503
598
 
504
599
  - Database schema changes
505
600
  - Files where user modifications are extensive (>30% of the file differs from template)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "generatesaas",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "type": "module",
5
5
  "description": "CLI for scaffolding and managing GenerateSaaS projects",
6
6
  "license": "UNLICENSED",