generatesaas 1.10.0 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import{Command as ls}from"commander";import*as In from"@clack/prompts";import{existsSync as Oo,readdirSync as xo,rmSync as Do}from"fs";import{join as Co,resolve as No}from"path";import{Option as V}from"commander";import*as E from"@clack/prompts";import*as Ye from"@clack/prompts";import Et from"picocolors";function Kt(e){let t=e?` GenerateSaaS v${e} `:" GenerateSaaS ";Ye.intro(Et.bgYellow(Et.black(t)))}function zt(){Ye.outro(Et.yellow("Happy building!"))}import*as u from"@clack/prompts";import f from"picocolors";var De={nextjs:{label:"Next.js",hint:"React 19 + Next.js 16"},nuxt:{label:"Nuxt",hint:"Vue 3 + Nuxt 4"}},Ce={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)"}},Je={stripe:{label:"Stripe"},polar:{label:"Polar"},none:{label:"None",hint:"disable payments"}},We={smtp:{label:"SMTP",hint:"Mailpit for local dev"},ses:{label:"Amazon SES"},resend:{label:"Resend"}},qe={user:{label:"Per user",hint:"each user has their own subscription"},organization:{label:"Per organization",hint:"org subscription shared by members"}},we={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}},Ne={"claude-code":{label:"Claude Code"},cursor:{label:"Cursor"},codex:{label:"Codex"},"gemini-cli":{label:"Gemini CLI"},windsurf:{label:"Windsurf"}};var ae={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 B={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}},L={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"}]}},G={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"}]}},Le=[{target:"vercel",provider:"redis",reason:"Vercel serverless cannot maintain persistent Redis connections. Consider Upstash."}],$e=[{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)."}],Ht=["Local file storage (sharp, geoip-lite)","SMTP email (use Resend or SES instead)","Content API git integration"];function Xe(e){let t=L[e.databaseProvider].managed,r=G[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 Ue=["nextjs","nuxt"],Ze=["fullstack","separate"],Qe=["stripe","polar","none"],et=["smtp","ses","resend"],tt=["postgres","redis","inngest","mailpit"],rt=["claude-code","cursor","codex","gemini-cli","windsurf"],nt=["user","organization"],ce=["USD","EUR","GBP","CAD","AUD","BRL","JPY"],le=["node","vercel"],pe=["postgres","neon","supabase"],de=["redis","upstash"],it=["google","github","facebook","discord","x"];function ot(e){return e.split("-").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function st(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 wt(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 i of n.envVars)t.push({key:i.name,message:`${i.name} (${n.label}, optional):`,secret:i.secret})}return e.demo&&t.push({key:"TURNSTILE_SECRET_KEY",message:"Cloudflare Turnstile secret key (optional):",secret:!0}),t}async function Yt(e){let t=!1;u.log.info(f.bold("Project"));let r=e?.projectName??await(async()=>{t=!0;let c=await u.text({message:"Project name:",placeholder:"my-saas",validate:p=>{if(!p?.trim())return"Project name is required.";if(!st(p))return"Use lowercase letters, numbers, and hyphens only. Must start with a letter."}});return w(c),c})(),n=e?.appName??await(async()=>{t=!0;let c=await u.text({message:"App name:",initialValue:ot(r),validate:p=>{if(!p?.trim())return"App name is required."}});return w(c),c})(),i=e?.projectDir??await(async()=>{t=!0;let c=await u.text({message:"Project location:",initialValue:`./${r}`});return w(c),c==="."?process.cwd():c})(),o=e?.frontend??await(async()=>{t=!0;let c=Object.keys(De),p=await u.select({message:"Frontend framework:",options:c.map(v=>({value:v,label:De[v].label,hint:De[v].hint}))});return w(p),p})();u.log.info(f.bold("Infrastructure"));let s=e?.deploymentTarget??"node";if(e?.deploymentTarget===void 0){t=!0;let c=await u.select({message:"Deployment target:",options:le.map(p=>({value:p,label:B[p].label,hint:B[p].hint}))});w(c),s=c}let a=e?.architecture?$e.find(c=>c.architecture===e.architecture&&c.target===s):void 0;if(a)throw new Error(`Incompatible: --architecture ${a.architecture} + --deploy ${a.target}. ${a.reason}`);let d=new Set($e.filter(c=>c.target===s).map(c=>c.architecture)),h=Ze.filter(c=>!d.has(c)),g=e?.architecture??await(async()=>{if(h.length===1){let p=h[0];return u.log.info(`Auto-selected ${Ce[p].label} architecture (only compatible option for ${B[s].label}).`),p}t=!0;let c=await u.select({message:"Architecture:",options:h.map(p=>({value:p,label:Ce[p].label,hint:Ce[p].hint}))});return w(c),c})(),m=e?.databaseProvider??await(async()=>{t=!0;let c=pe.filter(v=>!Le.some(C=>C.target===s&&C.provider===v));if(c.length===1){let v=c[0];return u.log.info(`Auto-selected ${L[v].label} (only compatible option for ${B[s].label}).`),v}let p=await u.select({message:"Database provider:",options:c.map(v=>({value:v,label:L[v].label,hint:L[v].hint}))});return w(p),p})(),S=e?.cacheProvider??await(async()=>{t=!0;let c=de.filter(v=>!Le.some(C=>C.target===s&&C.provider===v));if(c.length===1){let v=c[0];return u.log.info(`Auto-selected ${G[v].label} (only compatible option for ${B[s].label}).`),v}let p=await u.select({message:"Cache provider:",options:c.map(v=>({value:v,label:G[v].label,hint:G[v].hint}))});return w(p),p})();if(B[s]?.edgeRuntime){let c=Ht.map(p=>` - ${p}`).join(`
3
- `);u.note(c,"Unavailable on edge runtime")}u.log.info(f.bold("Features"));let T=e?.paymentProvider??await(async()=>{t=!0;let c=await u.select({message:"Payment provider:",options:Qe.map(p=>({value:p,label:Je[p].label,hint:Je[p].hint}))});return w(c),c})(),H=e?.defaultCurrency??await(async()=>{if(T==="none")return"USD";t=!0;let c=await u.select({message:"Default currency:",options:ce.map(p=>({value:p,label:p,hint:ae[p].name}))});return w(c),c})(),D=e?.emailProvider??await(async()=>{t=!0;let c=await u.select({message:"Email provider:",options:et.map(p=>({value:p,label:We[p].label,hint:We[p].hint}))});return w(c),c})(),ie=e?.multiTenancy??await(async()=>{t=!0;let c=await u.confirm({message:"Enable multi-tenancy (organizations)?",initialValue:!1});return w(c),c})(),oe=e?.billingScope??"user";if(ie&&e?.billingScope===void 0){t=!0;let c=await u.select({message:"Billing scope:",options:nt.map(p=>({value:p,label:qe[p].label,hint:qe[p].hint}))});w(c),oe=c}let k=e?.blog??await(async()=>{t=!0;let c=await u.confirm({message:"Enable blog?",initialValue:!0});return w(c),c})(),y=e?.docs??await(async()=>{t=!0;let c=await u.confirm({message:"Include docs app? (self-hosted Fumadocs documentation site)",initialValue:!1});return w(c),c})(),R=e?.revenueSharing??await(async()=>{t=!0;let c=await u.confirm({message:"Enable revenue sharing? (opt-in MRR leaderboard with dofollow backlinks)",initialValue:!1});return w(c),c})(),Y=T==="none"?!1:e?.credits??await(async()=>{t=!0;let c=await u.confirm({message:"Enable credits? (metered usage on top of subscription plans)",initialValue:!0});return w(c),c})(),J=e?.socialProviders??await(async()=>{t=!0;let c=it.map(v=>({value:v,label:q[v].label,hint:`requires ${q[v].envVars.map(C=>C.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 W=e?.dockerServices??await(async()=>{t=!0;let c=[...tt].filter(N=>N!=="mailpit");D==="smtp"&&c.push("mailpit");let p=c.map(N=>({value:N,label:we[N].label,hint:we[N].hint})),v=p.map(N=>N.value).filter(N=>!(N==="postgres"&&(m==="neon"||m==="supabase")||N==="redis"&&S==="upstash")),C=await u.multiselect({message:"Which services should we set up in Docker for you?",options:p,initialValues:v,required:!1});return w(C),C})(),Ee=e?.aiTools??await(async()=>{t=!0;let c=rt.map(v=>({value:v,label:Ne[v].label})),p=await u.multiselect({message:"Which AI coding tools do you use?",options:c,initialValues:[],required:!1});return w(p),p})(),St=e?.demo,He=wt({databaseProvider:m,cacheProvider:S,paymentProvider:T,emailProvider:D,socialProviders:J,demo:St}),xe={};if(He.length>0&&t){u.log.info(f.bold("Credentials")+f.dim(" all optional - press Enter to skip, fill in .env later"));for(let c of He)if(t=!0,c.secret){let p=await u.password({message:c.message,mask:"*"});w(p),typeof p=="string"&&p.trim()&&(xe[c.key]=p.trim())}else{let p=await u.text({message:c.message,placeholder:c.placeholder});w(p),typeof p=="string"&&p.trim()&&(xe[c.key]=p.trim())}}if(t){let c=[` Name: ${f.cyan(r)}`,` App name: ${f.cyan(n)}`,` Location: ${f.cyan(i)}`,` Frontend: ${f.cyan(De[o].label)}`,` Architecture: ${f.cyan(Ce[g].label)}`].join(`
4
- `),p=[` Deploy target: ${f.cyan(B[s]?.label??"Node.js / Docker")}`,` Database: ${f.cyan(L[m].label)}`,` Cache: ${f.cyan(G[S].label)}`,W.length>0?` Docker: ${f.cyan(W.map(se=>we[se].label).join(", "))}`:` Docker: ${f.dim("none")}`].filter(Boolean).join(`
5
- `),v=[T!=="none"?` Payment: ${f.cyan(Je[T].label)} (${H})`:` Payment: ${f.dim("none")}`,` Credits: ${Y?f.cyan("Yes"):f.dim("No")}`,` Email: ${f.cyan(We[D].label)}`,` Multi-tenancy: ${ie?f.cyan("Yes")+` (billing: ${qe[oe].label})`:f.dim("No")}`,` Blog: ${k?f.cyan("Yes"):f.dim("No")}`,` Docs app: ${y?f.cyan("Yes"):f.dim("No")}`,` Rev. sharing: ${R?f.cyan("Yes"):f.dim("No")}`,J.length>0?` Social login: ${f.cyan(J.map(se=>q[se].label).join(", "))}`:` Social login: ${f.dim("none")}`,Ee.length>0?` AI tools: ${f.cyan(Ee.map(se=>Ne[se].label).join(", "))}`:` AI tools: ${f.dim("none")}`].join(`
6
- `),C=[f.bold("Project"),c,"",f.bold("Infrastructure"),p,"",f.bold("Features"),v];if(He.length>0){let se=He.map(Gt=>{let Pn=xe[Gt.key]?f.green("provided"):f.dim("skipped");return` ${Gt.key}: ${Pn}`}).join(`
7
- `);C.push("",f.bold("Credentials"),se)}u.note(C.join(`
8
- `),"Summary");let N=await u.confirm({message:"Proceed with these settings?"});(u.isCancel(N)||!N)&&(u.cancel("Setup cancelled."),process.exit(0))}return{projectName:r,appName:n,projectDir:i,frontend:o,architecture:g,deploymentTarget:s,databaseProvider:m,cacheProvider:S,paymentProvider:T,emailProvider:D,multiTenancy:ie,billingScope:oe,blog:k,docs:y,revenueSharing:R,credits:Y,dockerServices:W,aiTools:Ee,socialProviders:J,defaultCurrency:H,...Object.keys(xe).length>0?{credentials:xe}:{},...e?.baseUrl!==void 0?{baseUrl:e.baseUrl}:{},...St!==void 0?{demo:St}:{}}}import{createReadStream as $n}from"fs";import{mkdir as Un}from"fs/promises";import{Readable as jn}from"stream";import{pipeline as rr}from"stream/promises";import{extract as Vn}from"tar";import{join as ue}from"path";import{homedir as _n}from"os";var je=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",U=".generatesaas",Q=ue(U,"manifest.json"),Jt=ue(U,"hashes.json"),at=ue(U,"template-hashes.json"),ct=ue(U,"template"),Wt=ue(U,"staging"),qt=ue(U,"staging.json"),X=ue(_n(),".generatesaas");var O=class extends Error{constructor(r,n,i){super(n);this.status=r;this.body=i}status;body;name="ApiError"};function Z(e){return{apiKey:e,baseUrl:je}}async function ee(e,t,r){let n=`${e.baseUrl}${t}`,i=await fetch(n,{...r,headers:{...r?.headers,Authorization:`Bearer ${e.apiKey}`,"User-Agent":"generatesaas-cli"}});if(!i.ok){let o,s;try{s=await i.json(),o=s.error??`API ${i.status}: ${t}`}catch{o=`API ${i.status}: ${t}`}throw new O(i.status,o,s)}return i}import{existsSync as Rn,readFileSync as On,writeFileSync as xn,mkdirSync as Dn}from"fs";import{dirname as Cn}from"path";import*as te from"@clack/prompts";function Ve(){if(!Rn(X))return null;try{let e=JSON.parse(On(X,"utf-8"));return e.apiKey?e.apiKey:(e.token&&!e.apiKey&&te.log.warning(`Found old GitHub token in ${X}. Run 'generatesaas init' to set up your API key.`),null)}catch{return null}}function me(e){Dn(Cn(X),{recursive:!0}),xn(X,JSON.stringify({apiKey:e},null," ")+`
9
- `,{mode:384})}async function be(e){if(e?.apiKey)return e.apiKey;let t=process.env.GENERATESAAS_API_KEY;if(t)return t;let r=Ve();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 Me()}async function Me(){let e=await te.text({message:"Enter your GenerateSaaS API key:",placeholder:"gs_live_...",validate:t=>{if(!t?.trim())return"API key is required."}});return te.isCancel(e)&&(te.cancel("Setup cancelled."),process.exit(0)),e.trim()}async function re(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}]}:await(await ee(e,"/versions")).json()}async function Xt(e,t){try{return await(await ee(e,`/changelog/${encodeURIComponent(t)}`)).text()}catch(r){if(r instanceof O&&r.status===404)return null;throw r}}async function Zt(e,t){return await(await ee(e,`/skill/${encodeURIComponent(t)}`)).json()}function lt(e){if(!(e instanceof O)||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 Qt(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,credits:e.credits,revenueSharing:e.revenueSharing,socialProviders:e.socialProviders,aiTools:e.aiTools,currency:e.defaultCurrency}}async function bt(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:"offline-test-token",licenseId:"offline-test-license-id"}:await(await ee(e,"/license/sign",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function er(e,t){return await(await ee(e,"/license/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function At(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 tr(e,t,r){let n=await fetch(`${e}/license/inspect`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify(r)});if(!n.ok){let i=await n.json().catch(()=>null);throw new Error(i?.error??`Inspect endpoint returned ${n.status}`)}return await n.json()}var Tt=new Set([".git","node_modules",".pnpm-store",".env",".env.test",".turbo",".nuxt",".output",".data","dist",".next",".svelte-kit",".devcontainer","playwright-report","test-results"]),Nn=new Set(["data","mksaas","references","scripts",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","README.md","TODO.md","OVERVIEW.md"]),Ln=["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 Ae(e){let t=e.split("/");for(let r of t)if(Tt.has(r))return!0;if(Nn.has(t[0]))return!0;for(let r of Ln)if(e===r||e.startsWith(r+"/"))return!0;return!1}async function pt(e,t,r){await Un(r,{recursive:!0});let n=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(n){await rr($n(n),nr(r));return}let i=await ee(e,`/template/${encodeURIComponent(t)}`);if(!i.body)throw new Error("Empty response body");let o=jn.fromWeb(i.body);await rr(o,nr(r))}function nr(e){return Vn({cwd:e,strip:1,filter:t=>{let r=t.replace(/^[^/]+\//,"");return r?!Ae(r):!0},sync:!1})}import{readFile as Mn,rm as ir,writeFile as Fn}from"fs/promises";import{join as kt}from"path";var Bn=["apps/web-nuxt/public/images/blog","apps/web-next/public/images/blog","packages/content/en/blog","packages/content/ro/blog"];async function or(e){await Promise.all(Bn.map(t=>ir(kt(e,t),{recursive:!0,force:!0})))}async function sr(e,t){t.includes("claude-code")||await ir(kt(e,".claude"),{recursive:!0,force:!0})}async function ar(e,t){let r=kt(e,".claude","settings.json"),n;try{n=await Mn(r,"utf8")}catch{return}let i=JSON.parse(n);delete i.alwaysThinkingEnabled,delete i.enableAllProjectMcpServers,i.env&&(delete i.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS,Object.keys(i.env).length===0&&delete i.env),i.permissions?.allow&&(i.permissions.allow=i.permissions.allow.filter(o=>Gn(o,t))),await Fn(r,JSON.stringify(i,null," ")+`
10
- `)}function Gn(e,t){return!(e.startsWith("mcp__")||t!=="nuxt"&&e.includes("nuxt"))}import{join as cr}from"path";import{mkdir as Kn,readdir as zn,rm as Hn,rmdir as Yn,writeFile as Jn}from"fs/promises";import{dirname as dt,join as Wn,relative as qn}from"path";async function ut(e){await Kn(e,{recursive:!0})}async function l(e,t){await ut(dt(e)),await Jn(e,t,"utf-8")}async function It(e,t){await Hn(e,{force:!0});let r=dt(e);for(;r!==t&&r!==dt(r);){try{await Yn(r)}catch{return}r=dt(r)}}async function fe(e,t,r){let n=[],i=await zn(e,{withFileTypes:!0});for(let o of i){let s=Wn(e,o.name),a=qn(t,s);r(a)||(o.isDirectory()?n.push(...await fe(s,t,r)):o.isFile()&&n.push(s))}return n}var Xn={postgres:"Postgres",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"},Zn={resend:"Resend",ses:"Amazon SES",smtp:"SMTP"},Qn={redis:"Redis",upstash:"Upstash Redis"},ei={node:"Node.js / Docker",vercel:"Vercel"},ti={stripe:"Stripe",polar:"Polar"};function ri(e){let t=e.frontend==="nuxt",r=t?"Nuxt 4":"Next.js 16",n=t?"apps/web-nuxt":"apps/web-next",i=t?"`app/` pages + components + composables":"`app/` routes, `components/`, `lib/`",o=e.architecture==="fullstack",s=o?"(fullstack - Hono API mounted inside the app)":"(separate - standalone Hono backend)",a=o?`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=Qn[e.cacheProvider],h=ei[e.deploymentTarget],g=e.paymentProvider==="none"?"":`
11
- - **Payments:** ${ti[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`.",S=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 gs}from"commander";import*as xn from"@clack/prompts";import{existsSync as Lo,readdirSync as $o,rmSync as Uo}from"fs";import{join as jo,resolve as Vo}from"path";import{Option as V}from"commander";import*as E from"@clack/prompts";import*as Ye from"@clack/prompts";import wt from"picocolors";function Jt(e){let t=e?` GenerateSaaS v${e} `:" GenerateSaaS ";Ye.intro(wt.bgYellow(wt.black(t)))}function Wt(){Ye.outro(wt.yellow("Happy building!"))}import*as m from"@clack/prompts";import g from"picocolors";var Ce={nextjs:{label:"Next.js",hint:"React 19 + Next.js 16"},nuxt:{label:"Nuxt",hint:"Vue 3 + Nuxt 4"}},Ne={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)"}},Je={stripe:{label:"Stripe"},polar:{label:"Polar"},none:{label:"None",hint:"disable payments"}},We={smtp:{label:"SMTP",hint:"Mailpit for local dev"},ses:{label:"Amazon SES"},resend:{label:"Resend"}},qe={user:{label:"Per user",hint:"each user has their own subscription"},organization:{label:"Per organization",hint:"org subscription shared by members"}},we={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}},Le={"claude-code":{label:"Claude Code"},cursor:{label:"Cursor"},codex:{label:"Codex"},"gemini-cli":{label:"Gemini CLI"},windsurf:{label:"Windsurf"}};var se={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 B={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}},N={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"}]}},G={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"}]}},$e=[{target:"vercel",provider:"redis",reason:"Vercel serverless cannot maintain persistent Redis connections. Consider Upstash."}],Ue=[{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)."}],qt=["Local file storage (sharp, geoip-lite)","SMTP email (use Resend or SES instead)","Content API git integration"];function Xe(e){let t=N[e.databaseProvider].managed,r=G[e.cacheProvider].managed;return e.dockerServices.some(i=>!(i==="postgres"&&t||i==="redis"&&r))}var W={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 je=["nextjs","nuxt"],Ze=["fullstack","separate"],Qe=["stripe","polar","none"],et=["smtp","ses","resend"],tt=["postgres","redis","inngest","mailpit"],rt=["claude-code","cursor","codex","gemini-cli","windsurf"],nt=["user","organization"],ae=["USD","EUR","GBP","CAD","AUD","BRL","JPY"],ce=["node","vercel"],le=["postgres","neon","supabase"],pe=["redis","upstash"],it=["google","github","facebook","discord","x"];function ot(e){return e.split("-").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function st(e){return/^[a-z][a-z0-9-]*$/.test(e)}function I(e){return e instanceof Error?e.message:String(e)}function w(e){m.isCancel(e)&&(m.cancel("Setup cancelled."),process.exit(0))}function bt(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 i=W[r];for(let n of i.envVars)t.push({key:n.name,message:`${n.name} (${i.label}, optional):`,secret:n.secret})}return e.demo&&t.push({key:"TURNSTILE_SECRET_KEY",message:"Cloudflare Turnstile secret key (optional):",secret:!0}),t}async function Xt(e){let t=!1;m.log.info(g.bold("Project"));let r=e?.projectName??await(async()=>{t=!0;let c=await m.text({message:"Project name:",placeholder:"my-saas",validate:p=>{if(!p?.trim())return"Project name is required.";if(!st(p))return"Use lowercase letters, numbers, and hyphens only. Must start with a letter."}});return w(c),c})(),i=e?.appName??await(async()=>{t=!0;let c=await m.text({message:"App name:",initialValue:ot(r),validate:p=>{if(!p?.trim())return"App name is required."}});return w(c),c})(),n=e?.projectDir??await(async()=>{t=!0;let c=await m.text({message:"Project location:",initialValue:`./${r}`});return w(c),c==="."?process.cwd():c})(),o=e?.frontend??await(async()=>{t=!0;let c=Object.keys(Ce),p=await m.select({message:"Frontend framework:",options:c.map(v=>({value:v,label:Ce[v].label,hint:Ce[v].hint}))});return w(p),p})();m.log.info(g.bold("Infrastructure"));let s=e?.deploymentTarget??"node";if(e?.deploymentTarget===void 0){t=!0;let c=await m.select({message:"Deployment target:",options:ce.map(p=>({value:p,label:B[p].label,hint:B[p].hint}))});w(c),s=c}let a=e?.architecture?Ue.find(c=>c.architecture===e.architecture&&c.target===s):void 0;if(a)throw new Error(`Incompatible: --architecture ${a.architecture} + --deploy ${a.target}. ${a.reason}`);let d=new Set(Ue.filter(c=>c.target===s).map(c=>c.architecture)),y=Ze.filter(c=>!d.has(c)),h=e?.architecture??await(async()=>{if(y.length===1){let p=y[0];return m.log.info(`Auto-selected ${Ne[p].label} architecture (only compatible option for ${B[s].label}).`),p}t=!0;let c=await m.select({message:"Architecture:",options:y.map(p=>({value:p,label:Ne[p].label,hint:Ne[p].hint}))});return w(c),c})(),f=e?.databaseProvider??await(async()=>{t=!0;let c=le.filter(v=>!$e.some(D=>D.target===s&&D.provider===v));if(c.length===1){let v=c[0];return m.log.info(`Auto-selected ${N[v].label} (only compatible option for ${B[s].label}).`),v}let p=await m.select({message:"Database provider:",options:c.map(v=>({value:v,label:N[v].label,hint:N[v].hint}))});return w(p),p})(),S=e?.cacheProvider??await(async()=>{t=!0;let c=pe.filter(v=>!$e.some(D=>D.target===s&&D.provider===v));if(c.length===1){let v=c[0];return m.log.info(`Auto-selected ${G[v].label} (only compatible option for ${B[s].label}).`),v}let p=await m.select({message:"Cache provider:",options:c.map(v=>({value:v,label:G[v].label,hint:G[v].hint}))});return w(p),p})();if(B[s]?.edgeRuntime){let c=qt.map(p=>` - ${p}`).join(`
3
+ `);m.note(c,"Unavailable on edge runtime")}m.log.info(g.bold("Features"));let k=e?.paymentProvider??await(async()=>{t=!0;let c=await m.select({message:"Payment provider:",options:Qe.map(p=>({value:p,label:Je[p].label,hint:Je[p].hint}))});return w(c),c})(),ie=e?.defaultCurrency??await(async()=>{if(k==="none")return"USD";t=!0;let c=await m.select({message:"Default currency:",options:ae.map(p=>({value:p,label:p,hint:se[p].name}))});return w(c),c})(),x=e?.emailProvider??await(async()=>{t=!0;let c=await m.select({message:"Email provider:",options:et.map(p=>({value:p,label:We[p].label,hint:We[p].hint}))});return w(c),c})(),$=e?.multiTenancy??await(async()=>{t=!0;let c=await m.confirm({message:"Enable multi-tenancy (organizations)?",initialValue:!1});return w(c),c})(),Se=e?.billingScope??"user";if($&&e?.billingScope===void 0){t=!0;let c=await m.select({message:"Billing scope:",options:nt.map(p=>({value:p,label:qe[p].label,hint:qe[p].hint}))});w(c),Se=c}let Z=e?.blog??await(async()=>{t=!0;let c=await m.confirm({message:"Enable blog?",initialValue:!0});return w(c),c})(),u=e?.docs??await(async()=>{t=!0;let c=await m.confirm({message:"Include docs app? (self-hosted Fumadocs documentation site)",initialValue:!1});return w(c),c})(),b=e?.revenueSharing??await(async()=>{t=!0;let c=await m.confirm({message:"Enable revenue sharing? (opt-in MRR leaderboard with dofollow backlinks)",initialValue:!1});return w(c),c})(),H=k==="none"?!1:e?.credits??await(async()=>{t=!0;let c=await m.confirm({message:"Enable credits? (metered usage on top of subscription plans)",initialValue:!0});return w(c),c})(),Y=e?.socialProviders??await(async()=>{t=!0;let c=it.map(v=>({value:v,label:W[v].label,hint:`requires ${W[v].envVars.map(D=>D.name).join(" / ")}`})),p=await m.multiselect({message:"Which social login providers should the sign-in screen show?",options:c,initialValues:[],required:!1});return w(p),p})();m.log.info(g.bold("Tooling"));let J=e?.dockerServices??await(async()=>{t=!0;let c=[...tt].filter(C=>C!=="mailpit");x==="smtp"&&c.push("mailpit");let p=c.map(C=>({value:C,label:we[C].label,hint:we[C].hint})),v=p.map(C=>C.value).filter(C=>!(C==="postgres"&&(f==="neon"||f==="supabase")||C==="redis"&&S==="upstash")),D=await m.multiselect({message:"Which services should we set up in Docker for you?",options:p,initialValues:v,required:!1});return w(D),D})(),Ee=e?.aiTools??await(async()=>{t=!0;let c=rt.map(v=>({value:v,label:Le[v].label})),p=await m.multiselect({message:"Which AI coding tools do you use?",options:c,initialValues:[],required:!1});return w(p),p})(),Et=e?.demo,He=bt({databaseProvider:f,cacheProvider:S,paymentProvider:k,emailProvider:x,socialProviders:Y,demo:Et}),De={};if(He.length>0&&t){m.log.info(g.bold("Credentials")+g.dim(" all optional - press Enter to skip, fill in .env later"));for(let c of He)if(t=!0,c.secret){let p=await m.password({message:c.message,mask:"*"});w(p),typeof p=="string"&&p.trim()&&(De[c.key]=p.trim())}else{let p=await m.text({message:c.message,placeholder:c.placeholder});w(p),typeof p=="string"&&p.trim()&&(De[c.key]=p.trim())}}if(t){let c=[` Name: ${g.cyan(r)}`,` App name: ${g.cyan(i)}`,` Location: ${g.cyan(n)}`,` Frontend: ${g.cyan(Ce[o].label)}`,` Architecture: ${g.cyan(Ne[h].label)}`].join(`
4
+ `),p=[` Deploy target: ${g.cyan(B[s]?.label??"Node.js / Docker")}`,` Database: ${g.cyan(N[f].label)}`,` Cache: ${g.cyan(G[S].label)}`,J.length>0?` Docker: ${g.cyan(J.map(oe=>we[oe].label).join(", "))}`:` Docker: ${g.dim("none")}`].filter(Boolean).join(`
5
+ `),v=[k!=="none"?` Payment: ${g.cyan(Je[k].label)} (${ie})`:` Payment: ${g.dim("none")}`,` Credits: ${H?g.cyan("Yes"):g.dim("No")}`,` Email: ${g.cyan(We[x].label)}`,` Multi-tenancy: ${$?g.cyan("Yes")+` (billing: ${qe[Se].label})`:g.dim("No")}`,` Blog: ${Z?g.cyan("Yes"):g.dim("No")}`,` Docs app: ${u?g.cyan("Yes"):g.dim("No")}`,` Rev. sharing: ${b?g.cyan("Yes"):g.dim("No")}`,Y.length>0?` Social login: ${g.cyan(Y.map(oe=>W[oe].label).join(", "))}`:` Social login: ${g.dim("none")}`,Ee.length>0?` AI tools: ${g.cyan(Ee.map(oe=>Le[oe].label).join(", "))}`:` AI tools: ${g.dim("none")}`].join(`
6
+ `),D=[g.bold("Project"),c,"",g.bold("Infrastructure"),p,"",g.bold("Features"),v];if(He.length>0){let oe=He.map(Yt=>{let Dn=De[Yt.key]?g.green("provided"):g.dim("skipped");return` ${Yt.key}: ${Dn}`}).join(`
7
+ `);D.push("",g.bold("Credentials"),oe)}m.note(D.join(`
8
+ `),"Summary");let C=await m.confirm({message:"Proceed with these settings?"});(m.isCancel(C)||!C)&&(m.cancel("Setup cancelled."),process.exit(0))}return{projectName:r,appName:i,projectDir:n,frontend:o,architecture:h,deploymentTarget:s,databaseProvider:f,cacheProvider:S,paymentProvider:k,emailProvider:x,multiTenancy:$,billingScope:Se,blog:Z,docs:u,revenueSharing:b,credits:H,dockerServices:J,aiTools:Ee,socialProviders:Y,defaultCurrency:ie,...Object.keys(De).length>0?{credentials:De}:{},...e?.baseUrl!==void 0?{baseUrl:e.baseUrl}:{},...Et!==void 0?{demo:Et}:{}}}import{createReadStream as Fn}from"fs";import{mkdir as Bn}from"fs/promises";import{Readable as Gn}from"stream";import{pipeline as or}from"stream/promises";import{extract as Kn}from"tar";import{join as de}from"path";import{homedir as Cn}from"os";var Ve=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",U=".generatesaas",Q=de(U,"manifest.json"),Zt=de(U,"hashes.json"),at=de(U,"template-hashes.json"),ct=de(U,"template"),Qt=de(U,"staging"),er=de(U,"staging.json"),q=de(Cn(),".generatesaas");var R=class extends Error{constructor(r,i,n){super(i);this.status=r;this.body=n}status;body;name="ApiError"};function X(e){return{apiKey:e,baseUrl:Ve}}async function ee(e,t,r){let i=`${e.baseUrl}${t}`,n=await fetch(i,{...r,headers:{...r?.headers,Authorization:`Bearer ${e.apiKey}`,"User-Agent":"generatesaas-cli"}});if(!n.ok){let o,s;try{s=await n.json(),o=s.error??`API ${n.status}: ${t}`}catch{o=`API ${n.status}: ${t}`}throw new R(n.status,o,s)}return n}import{existsSync as Nn,readFileSync as Ln,writeFileSync as $n,mkdirSync as Un}from"fs";import{dirname as jn}from"path";import*as te from"@clack/prompts";function Me(){if(!Nn(q))return null;try{let e=JSON.parse(Ln(q,"utf-8"));return e.apiKey?e.apiKey:(e.token&&!e.apiKey&&te.log.warning(`Found old GitHub token in ${q}. Run 'generatesaas init' to set up your API key.`),null)}catch{return null}}function ue(e){Un(jn(q),{recursive:!0}),$n(q,JSON.stringify({apiKey:e},null," ")+`
9
+ `,{mode:384})}async function be(e){if(e?.apiKey)return e.apiKey;let t=process.env.GENERATESAAS_API_KEY;if(t)return t;let r=Me();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 Fe()}async function Fe(){let e=await te.text({message:"Enter your GenerateSaaS API key:",placeholder:"gs_live_...",validate:t=>{if(!t?.trim())return"API key is required."}});return te.isCancel(e)&&(te.cancel("Setup cancelled."),process.exit(0)),e.trim()}async function re(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}]}:await(await ee(e,"/versions")).json()}async function At(e,t){try{return await(await ee(e,`/changelog/${encodeURIComponent(t)}`)).text()}catch(r){if(r instanceof R&&r.status===404)return null;throw r}}async function tr(e,t){return await(await ee(e,`/skill/${encodeURIComponent(t)}`)).json()}function lt(e){if(!(e instanceof R)||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 rr(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,credits:e.credits,revenueSharing:e.revenueSharing,socialProviders:e.socialProviders,aiTools:e.aiTools,currency:e.defaultCurrency}}async function Tt(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:"offline-test-token",licenseId:"offline-test-license-id"}:await(await ee(e,"/license/sign",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function nr(e,t){return await(await ee(e,"/license/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function kt(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 ir(e,t,r){let i=await fetch(`${e}/license/inspect`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify(r)});if(!i.ok){let n=await i.json().catch(()=>null);throw new Error(n?.error??`Inspect endpoint returned ${i.status}`)}return await i.json()}var It=new Set([".git","node_modules",".pnpm-store",".env",".env.test",".turbo",".nuxt",".output",".data","dist",".next",".svelte-kit",".wrangler",".devcontainer","playwright-report","test-results"]),Pt=new Set(["pnpm-lock.yaml"]);function Ae(e){if(_t(e))return!0;for(let t of e.split("/"))if(Pt.has(t))return!0;return!1}var Vn=new Set(["data","mksaas","references","scripts",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","README.md","TODO.md","OVERVIEW.md"]),Mn=["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(It.has(r))return!0;if(Vn.has(t[0]))return!0;for(let r of Mn)if(e===r||e.startsWith(r+"/"))return!0;return!1}async function pt(e,t,r){await Bn(r,{recursive:!0});let i=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(i){await or(Fn(i),sr(r));return}let n=await ee(e,`/template/${encodeURIComponent(t)}`);if(!n.body)throw new Error("Empty response body");let o=Gn.fromWeb(n.body);await or(o,sr(r))}function sr(e){return Kn({cwd:e,strip:1,filter:t=>{let r=t.replace(/^[^/]+\//,"");return r?!_t(r):!0},sync:!1})}import{readFile as zn,rm as ar,writeFile as Hn}from"fs/promises";import{join as Rt}from"path";var Yn=["apps/web-nuxt/public/images/blog","apps/web-next/public/images/blog","packages/content/en/blog","packages/content/ro/blog"];async function cr(e){await Promise.all(Yn.map(t=>ar(Rt(e,t),{recursive:!0,force:!0})))}async function lr(e,t){t.includes("claude-code")||await ar(Rt(e,".claude"),{recursive:!0,force:!0})}async function pr(e,t){let r=Rt(e,".claude","settings.json"),i;try{i=await zn(r,"utf8")}catch{return}let n=JSON.parse(i);delete n.alwaysThinkingEnabled,delete n.enableAllProjectMcpServers,n.env&&(delete n.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS,Object.keys(n.env).length===0&&delete n.env),n.permissions?.allow&&(n.permissions.allow=n.permissions.allow.filter(o=>Jn(o,t))),await Hn(r,JSON.stringify(n,null," ")+`
10
+ `)}function Jn(e,t){return!(e.startsWith("mcp__")||t!=="nuxt"&&e.includes("nuxt"))}import{join as dr}from"path";import{mkdir as Wn,readdir as qn,rm as Xn,rmdir as Zn,writeFile as Qn}from"fs/promises";import{dirname as dt,join as ei,relative as ti}from"path";async function ut(e){await Wn(e,{recursive:!0})}async function l(e,t){await ut(dt(e)),await Qn(e,t,"utf-8")}async function Ot(e,t){await Xn(e,{force:!0});let r=dt(e);for(;r!==t&&r!==dt(r);){try{await Zn(r)}catch{return}r=dt(r)}}async function me(e,t,r){let i=[],n=await qn(e,{withFileTypes:!0});for(let o of n){let s=ei(e,o.name),a=ti(t,s);r(a)||(o.isDirectory()?i.push(...await me(s,t,r)):o.isFile()&&i.push(s))}return i}var ri={postgres:"Postgres",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"},ni={resend:"Resend",ses:"Amazon SES",smtp:"SMTP"},ii={redis:"Redis",upstash:"Upstash Redis"},oi={node:"Node.js / Docker",vercel:"Vercel"},si={stripe:"Stripe",polar:"Polar"};function ai(e){let t=e.frontend==="nuxt",r=t?"Nuxt 4":"Next.js 16",i=t?"apps/web-nuxt":"apps/web-next",n=t?"`app/` pages + components + composables":"`app/` routes, `components/`, `lib/`",o=e.architecture==="fullstack",s=o?"(fullstack - Hono API mounted inside the app)":"(separate - standalone Hono backend)",a=o?`Mounted inside \`${i}\`. 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=ii[e.cacheProvider],y=oi[e.deploymentTarget],h=e.paymentProvider==="none"?"":`
11
+ - **Payments:** ${si[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`.",S=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
12
12
 
13
13
  Guidelines for AI coding agents (Claude Code, Codex, Cursor, \u2026) in this project.
14
14
  Keep this file tight: add a rule only when it prevents a recurring mistake - too
@@ -22,7 +22,7 @@ and go find it:
22
22
 
23
23
  1. \`docs/${t?"nuxt":"next"}/index.mdx\` - the catalog of every shipped feature and its config flag.
24
24
  2. \`packages/config/src/index.ts\` - the flag that turns the feature on/off (check it before rendering).
25
- 3. Search \`packages/\` and \`${n}/\` for the name before creating anything.
25
+ 3. Search \`packages/\` and \`${i}/\` for the name before creating anything.
26
26
 
27
27
  Already built - extend these, never re-implement: auth (email / OAuth / 2FA / passkeys),
28
28
  billing & subscriptions, credits, organizations & teams, notifications, email, SMS,
@@ -36,11 +36,11 @@ flag, route, or translation is the most common and most costly mistake in this r
36
36
 
37
37
  - **Frontend:** ${r} ${s}
38
38
  - **API:** Hono, RPC-typed. ${a}
39
- - **Database:** Drizzle ORM + ${Xn[e.databaseProvider]}
39
+ - **Database:** Drizzle ORM + ${ri[e.databaseProvider]}
40
40
  - **Cache + jobs:** ${d} + Inngest
41
- - **Auth:** Better Auth${g}
42
- - **Email:** ${Zn[e.emailProvider]}
43
- - **Deploy:** ${h}
41
+ - **Auth:** Better Auth${h}
42
+ - **Email:** ${ni[e.emailProvider]}
43
+ - **Deploy:** ${y}
44
44
 
45
45
  ## Where things live (extend these - don't reinvent)
46
46
 
@@ -50,7 +50,7 @@ flag, route, or translation is the most common and most costly mistake in this r
50
50
  - \`packages/auth/src/config.ts\` - Better Auth config.
51
51
  - \`packages/runtime/src/env.ts\` - the validated (Zod) env schema. New env var \u2192 add it here **and** to \`.env.example\` (the committed reference); set the local value in \`.env\`.
52
52
  - \`packages/{payments,mail,sms,storage,notifications}\` - config-gated integrations. Every provider's files stay even when its feature is off, so flipping a flag is enough to enable it.
53
- - \`${n}\` - the ${r} app (${i}).
53
+ - \`${i}\` - the ${r} app (${n}).
54
54
  - \`docs/${t?"nuxt":"next"}/\` - feature & architecture documentation for this stack (Markdown). Consult it before searching from scratch; it cites real config keys, package names, and file paths you can act on.
55
55
 
56
56
  ## Code style
@@ -68,7 +68,7 @@ flag, route, or translation is the most common and most costly mistake in this r
68
68
 
69
69
  - **Hono routes:** keep the method chain (\`.get().post()\`) - RPC type inference depends on it. Validate with \`sValidator\` from \`@hono/standard-validator\`.
70
70
  - **Feature flags:** respect \`config.*\` - hide/skip a feature when its flag is off (the files stay so you can flip it later).
71
- - **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.
71
+ - **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.
72
72
  - ${S}
73
73
  - **Routes:** use \`config.routes.*\`, never hardcoded path strings.
74
74
  - **Secrets:** never send them to an external service; generate tokens/QR codes client-side.
@@ -80,20 +80,20 @@ In \`@repo/*\` backend packages prefer web-standard APIs: \`crypto.randomUUID()\
80
80
  ## This project
81
81
 
82
82
  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\`.
83
- `}async function lr(e){await l(cr(e.projectDir,"AGENTS.md"),ri(e)),await l(cr(e.projectDir,"CLAUDE.md"),`@AGENTS.md
84
- `)}import{join as ni}from"path";var ii={postgres:"Postgres (self-hosted)",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"};function oi(e){let t=e.appName.trim()||e.projectName,r=e.frontend==="nuxt",n=r?"Nuxt 4":"Next.js 16",i=r?"apps/web-nuxt":"apps/web-next",o=ii[e.databaseProvider],s=Xe(e),a=e.architecture==="fullstack"?`${n} app at \`${i}/\` 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 \`${i}/\` and a separate Hono backend at \`apps/backend/\`.`,d=e.architecture==="fullstack"?`- App + API: http://localhost:3000
83
+ `}async function ur(e){await l(dr(e.projectDir,"AGENTS.md"),ai(e)),await l(dr(e.projectDir,"CLAUDE.md"),`@AGENTS.md
84
+ `)}import{join as ci}from"path";var li={postgres:"Postgres (self-hosted)",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"};function pi(e){let t=e.appName.trim()||e.projectName,r=e.frontend==="nuxt",i=r?"Nuxt 4":"Next.js 16",n=r?"apps/web-nuxt":"apps/web-next",o=li[e.databaseProvider],s=Xe(e),a=e.architecture==="fullstack"?`${i} app at \`${n}/\` 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.`:`${i} app at \`${n}/\` and a separate Hono backend at \`apps/backend/\`.`,d=e.architecture==="fullstack"?`- App + API: http://localhost:3000
85
85
  - Inngest dev server: http://127.0.0.1:8288`:`- App: http://localhost:3000
86
86
  - API: http://localhost:3010
87
- - Inngest dev server: http://127.0.0.1:8288`,h=s?`pnpm infra # optional: starts local Docker services (Postgres / Redis / Inngest / Mailpit)
88
- `:"",g=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
87
+ - Inngest dev server: http://127.0.0.1:8288`,y=s?`pnpm infra # optional: starts local Docker services (Postgres / Redis / Inngest / Mailpit)
88
+ `:"",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
89
89
 
90
- 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}
90
+ 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}
91
91
 
92
92
  ${a}
93
93
 
94
94
  ## Stack
95
95
 
96
- - **Frontend:** ${n}
96
+ - **Frontend:** ${i}
97
97
  - **Backend:** Hono (TypeScript, RPC-typed)
98
98
  - **Auth:** Better Auth
99
99
  - **ORM / DB:** Drizzle + ${o}
@@ -107,7 +107,7 @@ pnpm install
107
107
  # A ready-to-run \`.env\` was generated for you - open it and fill in the
108
108
  # values marked \`# TODO\` (provider API keys). \`.env.example\` is the committed
109
109
  # reference. All apps load the single root \`.env\`.
110
- ${h}pnpm dev
110
+ ${y}pnpm dev
111
111
  \`\`\`
112
112
 
113
113
  ${d}
@@ -129,7 +129,7 @@ pnpm -F @repo/database studio # open Drizzle Studio
129
129
  pnpm auth:generate # regenerate Better Auth schema after config changes
130
130
  \`\`\`
131
131
 
132
- ${g}
132
+ ${h}
133
133
 
134
134
  ## Removing the GenerateSaaS license
135
135
 
@@ -141,20 +141,20 @@ pnpm dlx generatesaas eject
141
141
 
142
142
  ## Updates
143
143
 
144
- ${m}
145
- `}async function pr(e){await l(ni(e.projectDir,"README.md"),oi(e))}function si(e){let t=e.split(".");return t.length>=3?t.slice(1).join("."):e}async function dr(e){let t=e.appName.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),r=e.paymentProvider!=="none",n=e.baseUrl??"http://localhost:3000",i=e.baseUrl?new URL(e.baseUrl).hostname:"example.com",o=si(i),s=e.demo?"false":"true",a=e.demo?`
144
+ ${f}
145
+ `}async function mr(e){await l(ci(e.projectDir,"README.md"),pi(e))}function di(e){let t=e.split(".");return t.length>=3?t.slice(1).join("."):e}async function fr(e){let t=e.appName.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),r=e.paymentProvider!=="none",i=e.baseUrl??"http://localhost:3000",n=e.baseUrl?new URL(e.baseUrl).hostname:"example.com",o=di(n),s=e.demo?"false":"true",a=e.demo?`
146
146
  support: {
147
147
  enableInDev: true,
148
148
  crisp: { websiteId: "7e221cec-ed61-46b7-b1b4-8cbc16557cca" }
149
- },`:"",d=e.frontend==="nextjs"&&e.architecture==="fullstack"?"false":"true",h=`import type { AppConfig } from "@repo/config/types";
149
+ },`:"",d=e.frontend==="nextjs"&&e.architecture==="fullstack"?"false":"true",y=`import type { AppConfig } from "@repo/config/types";
150
150
 
151
151
  const trustedOrigins = process.env.TRUSTED_ORIGINS?.split(",").map((s) => s.trim()).filter(Boolean);
152
152
 
153
153
  export const config: AppConfig = {
154
154
  siteName: "${t}",
155
155
  fullSiteName: "${t}",
156
- domain: "${i}",
157
- baseUrl: process.env.BASE_URL ?? "${n}",
156
+ domain: "${n}",
157
+ baseUrl: process.env.BASE_URL ?? "${i}",
158
158
  indexable: ${s},
159
159
  logo: {
160
160
  main: "/images/logo.svg",
@@ -255,7 +255,7 @@ export const config: AppConfig = {
255
255
  }`:`{
256
256
  base: "${e.defaultCurrency}",
257
257
  list: [
258
- { symbol: "${ae[e.defaultCurrency].symbol}", name: "${ae[e.defaultCurrency].name}", code: "${e.defaultCurrency}", place: "${ae[e.defaultCurrency].place}", space: ${ae[e.defaultCurrency].space} }
258
+ { symbol: "${se[e.defaultCurrency].symbol}", name: "${se[e.defaultCurrency].name}", code: "${e.defaultCurrency}", place: "${se[e.defaultCurrency].place}", space: ${se[e.defaultCurrency].space} }
259
259
  ],
260
260
  countryMap: {
261
261
  default: "${e.defaultCurrency}"
@@ -308,8 +308,8 @@ export * from "./pricing";
308
308
  export * from "./roles";
309
309
  export * from "./section-tabs";
310
310
  export * from "./tenancy";
311
- `,g=`${e.projectDir}/packages/config/src/index.ts`;await l(g,h)}function ai(e){return e==="stripe"?' stripePriceId: "",':' polarProductId: "",'}function Pt(e){let t=[];return e.withCredits&&(t.push(` credits: ${e.credits},`),t.push(" creditInterval: 30,")),t.push(` apiRateLimit: { maxRequests: ${e.rateLimit} }`),t.join(`
312
- `)}async function ur(e){let t=`${e.projectDir}/packages/config/src/pricing.ts`,r=e.defaultCurrency;if(e.paymentProvider==="none"){let m=`import type { PricingConfig } from "@repo/config/types";
311
+ `,h=`${e.projectDir}/packages/config/src/index.ts`;await l(h,y)}function ui(e){return e==="stripe"?' stripePriceId: "",':' polarProductId: "",'}function xt(e){let t=[];return e.withCredits&&(t.push(` credits: ${e.credits},`),t.push(" creditInterval: 30,")),t.push(` apiRateLimit: { maxRequests: ${e.rateLimit} }`),t.join(`
312
+ `)}async function gr(e){let t=`${e.projectDir}/packages/config/src/pricing.ts`,r=e.defaultCurrency;if(e.paymentProvider==="none"){let f=`import type { PricingConfig } from "@repo/config/types";
313
313
 
314
314
  export const pricingConfig: PricingConfig = {
315
315
  defaultPlan: "free",
@@ -338,7 +338,7 @@ export const pricingConfig: PricingConfig = {
338
338
  credits: { enabled: false },
339
339
  products: { enabled: false, items: [] }
340
340
  };
341
- `;await l(t,m);return}let n=e.paymentProvider,i=ai(n),o=e.credits,s=Pt({credits:5,rateLimit:100,withCredits:o}),a=Pt({credits:10,rateLimit:1e3,withCredits:o}),d=Pt({credits:50,rateLimit:5e3,withCredits:o}),g=`import type { PricingConfig } from "@repo/config/types";
341
+ `;await l(t,f);return}let i=e.paymentProvider,n=ui(i),o=e.credits,s=xt({credits:5,rateLimit:100,withCredits:o}),a=xt({credits:10,rateLimit:1e3,withCredits:o}),d=xt({credits:50,rateLimit:5e3,withCredits:o}),h=`import type { PricingConfig } from "@repo/config/types";
342
342
 
343
343
  export const pricingConfig: PricingConfig = {
344
344
  defaultPlan: "free",
@@ -377,12 +377,12 @@ ${s}
377
377
  ],
378
378
  prices: [
379
379
  {
380
- ${i}
380
+ ${n}
381
381
  interval: "month",
382
382
  amounts: { ${r}: 9 }
383
383
  },
384
384
  {
385
- ${i}
385
+ ${n}
386
386
  interval: "year",
387
387
  amounts: { ${r}: 90 },
388
388
  anchorAmounts: { ${r}: 109 }
@@ -406,14 +406,14 @@ ${a}
406
406
  ],
407
407
  prices: [
408
408
  {
409
- ${i}
409
+ ${n}
410
410
  interval: "month",
411
411
  amounts: { ${r}: 29 },
412
412
  anchorAmounts: { ${r}: 39 },
413
413
  featured: true
414
414
  },
415
415
  {
416
- ${i}
416
+ ${n}
417
417
  interval: "year",
418
418
  amounts: { ${r}: 290 },
419
419
  anchorAmounts: { ${r}: 349 }
@@ -428,11 +428,11 @@ ${o?" credits: { enabled: true }":" credits: { enabled: false }"},
428
428
  items: []
429
429
  }
430
430
  };
431
- `;await l(t,g)}var ci={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 li(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 pi={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 Te(e,t){return t?e.map(r=>{let n=t[r.key];return n?{...r,defaultValue:n,comment:void 0}:r}):e}function ke(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 _t(e){return Array.from(crypto.getRandomValues(new Uint8Array(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}function fr(e){return e.architecture==="fullstack"?{apiUrl:"http://localhost:3000/api",baseUrl:"http://localhost:3000"}:{apiUrl:"http://localhost:3010",baseUrl:"http://localhost:3000"}}function di(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 mt(e,t,r,n){e.push(n==="example"?`${t}=`:`${t}=${r}`)}function mr(e,t){let r=t==="example"?void 0:e.credentials,{apiUrl:n,baseUrl:i}=fr(e),o=[];e.architecture==="separate"?o.push("# API Configuration","# Standalone backend's own URL (the frontend reaches it via the public var above).",`API_URL=${n}`,`BASE_URL=${i}`):o.push("# App","# (API_URL is derived from the frontend's *_PUBLIC_API_URL by the runtime env schema.)",`BASE_URL=${i}`),o.push("","# Database"),ke(Te(L[e.databaseProvider].envVars,r),o),o.push("","# Cache"),ke(Te(G[e.cacheProvider].envVars,r),o),o.push("","# Authentication"),t==="example"&&o.push("# Generate a strong secret, e.g. `openssl rand -hex 32`"),mt(o,"BETTER_AUTH_SECRET",crypto.randomUUID(),t),o.push("","# Content API (random secret; required when contentApi feature is enabled in @repo/config)"),mt(o,"CONTENT_API_KEY",_t(32),t),o.push("","# Job Queue - Inngest","INNGEST_APP_ID=api"),mt(o,"INNGEST_EVENT_KEY",_t(32),t),mt(o,"INNGEST_SIGNING_KEY",_t(32),t),o.push("INNGEST_BASE_URL=http://127.0.0.1:8288"),e.architecture==="separate"&&(o.push("","# API Port (standalone backend)","API_PORT=3010"),o.push("","# CORS + cross-subdomain cookies (production only - leave unset for local dev)","# TRUSTED_ORIGINS=https://your-app.example.com","# AUTH_COOKIE_DOMAIN=.example.com"));let s=ci[e.emailProvider];if(s&&(o.push("","# Email"),ke(Te(s,r),o)),e.paymentProvider!=="none"){let a=pi[e.paymentProvider];a&&(o.push("","# Payment"),ke(Te(a,r),o))}if(e.socialProviders.length>0){o.push("","# Social auth");for(let a of e.socialProviders)ke(Te(li(a),r),o)}return e.demo&&(o.push("","# Captcha (Cloudflare Turnstile)"),ke(Te([{key:"TURNSTILE_SECRET_KEY",comment:"# TODO: Add your Cloudflare Turnstile secret key"}],r),o)),o.push("","# Translations - OpenRouter (optional)","# Set to auto-translate en/* into your other locales with `pnpm translate`.","# The pre-commit hook runs it on commit; without a key it skips and untranslated","# locales fall back to the default locale. Get a key: https://openrouter.ai/keys","#OPENROUTER_API_KEY="),o.push(""),o.join(`
432
- `)}function ui(e){let{apiUrl:t}=fr(e),r=e.frontend==="nextjs"?"NEXT_PUBLIC_API_URL":"NUXT_PUBLIC_API_URL",n=["# API Configuration",`${r}=${t}`],i=di(e);return i&&e.architecture==="separate"&&n.push("","# Production (uncomment and replace with your deployed hostnames):",`# ${r}=${i.backend}`),n.push(""),n.join(`
433
- `)}async function gr(e){let t=ui(e);await l(`${e.projectDir}/.env`,t+`
434
- `+mr(e,"env")),await l(`${e.projectDir}/.env.example`,t+`
435
- `+mr(e,"example"))}async function hr(e){let t=[...e.dockerServices];if(L[e.databaseProvider].managed&&(t=t.filter(o=>o!=="postgres")),G[e.cacheProvider].managed&&(t=t.filter(o=>o!=="redis")),t.length===0)return!1;let r=[],n=[];t.includes("postgres")&&(r.push(` postgres:
431
+ `;await l(t,h)}var mi={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 fi(e){let t=W[e];return t.envVars.map((r,i)=>({key:r.name,...i===0?{comment:`# TODO: Add your ${t.label} OAuth credentials`}:{}}))}var gi={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 Te(e,t){return t?e.map(r=>{let i=t[r.key];return i?{...r,defaultValue:i,comment:void 0}:r}):e}function ke(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 Dt(e){return Array.from(crypto.getRandomValues(new Uint8Array(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}function yr(e){return e.architecture==="fullstack"?{apiUrl:"http://localhost:3000/api",baseUrl:"http://localhost:3000"}:{apiUrl:"http://localhost:3010",baseUrl:"http://localhost:3000"}}function hi(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 mt(e,t,r,i){e.push(i==="example"?`${t}=`:`${t}=${r}`)}function hr(e,t){let r=t==="example"?void 0:e.credentials,{apiUrl:i,baseUrl:n}=yr(e),o=[];e.architecture==="separate"?o.push("# API Configuration","# Standalone backend's own URL (the frontend reaches it via the public var above).",`API_URL=${i}`,`BASE_URL=${n}`):o.push("# App","# (API_URL is derived from the frontend's *_PUBLIC_API_URL by the runtime env schema.)",`BASE_URL=${n}`),o.push("","# Database"),ke(Te(N[e.databaseProvider].envVars,r),o),o.push("","# Cache"),ke(Te(G[e.cacheProvider].envVars,r),o),o.push("","# Authentication"),t==="example"&&o.push("# Generate a strong secret, e.g. `openssl rand -hex 32`"),mt(o,"BETTER_AUTH_SECRET",crypto.randomUUID(),t),o.push("","# Content API (random secret; required when contentApi feature is enabled in @repo/config)"),mt(o,"CONTENT_API_KEY",Dt(32),t),o.push("","# Job Queue - Inngest","INNGEST_APP_ID=api"),mt(o,"INNGEST_EVENT_KEY",Dt(32),t),mt(o,"INNGEST_SIGNING_KEY",Dt(32),t),o.push("INNGEST_BASE_URL=http://127.0.0.1:8288"),e.architecture==="separate"&&(o.push("","# API Port (standalone backend)","API_PORT=3010"),o.push("","# CORS + cross-subdomain cookies (production only - leave unset for local dev)","# TRUSTED_ORIGINS=https://your-app.example.com","# AUTH_COOKIE_DOMAIN=.example.com"));let s=mi[e.emailProvider];if(s&&(o.push("","# Email"),ke(Te(s,r),o)),e.paymentProvider!=="none"){let a=gi[e.paymentProvider];a&&(o.push("","# Payment"),ke(Te(a,r),o))}if(e.socialProviders.length>0){o.push("","# Social auth");for(let a of e.socialProviders)ke(Te(fi(a),r),o)}return e.demo&&(o.push("","# Captcha (Cloudflare Turnstile)"),ke(Te([{key:"TURNSTILE_SECRET_KEY",comment:"# TODO: Add your Cloudflare Turnstile secret key"}],r),o)),o.push("","# Translations - OpenRouter (optional)","# Set to auto-translate en/* into your other locales with `pnpm translate`.","# The pre-commit hook runs it on commit; without a key it skips and untranslated","# locales fall back to the default locale. Get a key: https://openrouter.ai/keys","#OPENROUTER_API_KEY="),o.push(""),o.join(`
432
+ `)}function yi(e){let{apiUrl:t}=yr(e),r=e.frontend==="nextjs"?"NEXT_PUBLIC_API_URL":"NUXT_PUBLIC_API_URL",i=["# API Configuration",`${r}=${t}`],n=hi(e);return n&&e.architecture==="separate"&&i.push("","# Production (uncomment and replace with your deployed hostnames):",`# ${r}=${n.backend}`),i.push(""),i.join(`
433
+ `)}async function vr(e){let t=yi(e);await l(`${e.projectDir}/.env`,t+`
434
+ `+hr(e,"env")),await l(`${e.projectDir}/.env.example`,t+`
435
+ `+hr(e,"example"))}async function Sr(e){let t=[...e.dockerServices];if(N[e.databaseProvider].managed&&(t=t.filter(o=>o!=="postgres")),G[e.cacheProvider].managed&&(t=t.filter(o=>o!=="redis")),t.length===0)return!1;let r=[],i=[];t.includes("postgres")&&(r.push(` postgres:
436
436
  image: postgres:18-alpine
437
437
  ports:
438
438
  - "\${POSTGRES_PORT:-5432}:5432"
@@ -447,7 +447,7 @@ ${o?" credits: { enabled: true }":" credits: { enabled: false }"},
447
447
  test: ["CMD-SHELL", "pg_isready -U postgres"]
448
448
  interval: 5s
449
449
  timeout: 5s
450
- retries: 5`),n.push(" postgres_data:")),t.includes("redis")&&(r.push(` redis:
450
+ retries: 5`),i.push(" postgres_data:")),t.includes("redis")&&(r.push(` redis:
451
451
  image: redis:8-alpine
452
452
  ports:
453
453
  - "\${REDIS_PORT:-6379}:6379"
@@ -457,7 +457,7 @@ ${o?" credits: { enabled: true }":" credits: { enabled: false }"},
457
457
  test: ["CMD", "redis-cli", "ping"]
458
458
  interval: 5s
459
459
  timeout: 5s
460
- retries: 5`),n.push(" redis_data:")),t.includes("mailpit")&&r.push(` mailpit:
460
+ retries: 5`),i.push(" redis_data:")),t.includes("mailpit")&&r.push(` mailpit:
461
461
  image: axllent/mailpit
462
462
  ports:
463
463
  - "\${MAILPIT_SMTP_PORT:-1025}:1025"
@@ -468,16 +468,16 @@ ${o?" credits: { enabled: true }":" credits: { enabled: false }"},
468
468
  image: inngest/inngest:v1.17.4
469
469
  ports:
470
470
  - "\${INNGEST_PORT:-8288}:8288"
471
- command: inngest dev`);let i=`services:
471
+ command: inngest dev`);let n=`services:
472
472
  ${r.join(`
473
473
 
474
474
  `)}
475
- `;return n.length>0&&(i+=`
475
+ `;return i.length>0&&(n+=`
476
476
  volumes:
477
- ${n.join(`
477
+ ${i.join(`
478
478
  `)}
479
- `),await l(`${e.projectDir}/infra/docker-compose.yml`,i),!0}function mi(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"}}),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 fi(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 yr(e){let t={version:"0.2.0",configurations:mi(e),compounds:fi(e)};await l(`${e.projectDir}/.vscode/launch.json`,JSON.stringify(t,null," ")+`
480
- `)}async function vr(e){if(e.architecture!=="separate")return;await l(`${e.projectDir}/apps/backend/src/index.ts`,`import { serve } from "@hono/node-server";
479
+ `),await l(`${e.projectDir}/infra/docker-compose.yml`,n),!0}function vi(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"}}),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 i=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 ${i}`,cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"]})}return t}function Si(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 Er(e){let t={version:"0.2.0",configurations:vi(e),compounds:Si(e)};await l(`${e.projectDir}/.vscode/launch.json`,JSON.stringify(t,null," ")+`
480
+ `)}async function wr(e){if(e.architecture!=="separate")return;await l(`${e.projectDir}/apps/backend/src/index.ts`,`import { serve } from "@hono/node-server";
481
481
  import app from "@repo/api";
482
482
  import { closeRedis, env, logger } from "@repo/runtime";
483
483
 
@@ -508,18 +508,18 @@ bootstrap().catch((error) => {
508
508
  logger.error("[Backend] Fatal error", error);
509
509
  process.exit(1);
510
510
  });
511
- `)}import{readFile as gi}from"fs/promises";import{join as hi}from"path";var yi=`export * from "./db/auth";
511
+ `)}import{readFile as Ei}from"fs/promises";import{join as wi}from"path";var bi=`export * from "./db/auth";
512
512
  export * from "./db/schema";
513
- export type { User, Account, Organization, Member } from "./db/auth";`;async function Sr(e){let t=hi(e.projectDir,"packages/database/src/index.ts"),r=yi;try{let i=await gi(t,"utf-8"),o=vi(i);o&&(r=o)}catch{}let n;switch(e.databaseProvider){case"postgres":n=Si(r);break;case"neon":n=Ei(r);break;case"supabase":n=wi(r);break}await l(t,n)}function vi(e){return e.split(`
514
- `).filter(n=>n.startsWith("export type ")||n.startsWith("export * from")).join(`
515
- `)}var Rt=`
513
+ export type { User, Account, Organization, Member } from "./db/auth";`;async function br(e){let t=wi(e.projectDir,"packages/database/src/index.ts"),r=bi;try{let n=await Ei(t,"utf-8"),o=Ai(n);o&&(r=o)}catch{}let i;switch(e.databaseProvider){case"postgres":i=Ti(r);break;case"neon":i=ki(r);break;case"supabase":i=Ii(r);break}await l(t,i)}function Ai(e){return e.split(`
514
+ `).filter(i=>i.startsWith("export type ")||i.startsWith("export * from")).join(`
515
+ `)}var Ct=`
516
516
  /** Extract affected-row count from a delete/update result (works for pg + postgres-js). */
517
517
  export function affectedRowCount(result: unknown): number {
518
518
  if (typeof result !== "object" || result === null) return 0;
519
519
  const r = result as { rowCount?: number; count?: number };
520
520
  return r.rowCount ?? r.count ?? 0;
521
521
  }
522
- `;function Si(e){return`import { drizzle } from "drizzle-orm/node-postgres";
522
+ `;function Ti(e){return`import { drizzle } from "drizzle-orm/node-postgres";
523
523
  import { z } from "zod";
524
524
  import * as authSchema from "./db/auth";
525
525
  import * as appSchema from "./db/schema";
@@ -534,7 +534,7 @@ export const db = drizzle(parsed.data.DATABASE_URL, { schema });
534
534
  export const pool = db.$client;
535
535
 
536
536
  ${e}
537
- ${Rt}`}function Ei(e){return`import { neon } from "@neondatabase/serverless";
537
+ ${Ct}`}function ki(e){return`import { neon } from "@neondatabase/serverless";
538
538
  import { drizzle } from "drizzle-orm/neon-http";
539
539
  import { z } from "zod";
540
540
  import * as authSchema from "./db/auth";
@@ -550,7 +550,7 @@ const sql = neon(parsed.data.DATABASE_URL);
550
550
  export const db = drizzle(sql, { schema });
551
551
 
552
552
  ${e}
553
- ${Rt}`}function wi(e){return`import { drizzle } from "drizzle-orm/postgres-js";
553
+ ${Ct}`}function Ii(e){return`import { drizzle } from "drizzle-orm/postgres-js";
554
554
  import postgres from "postgres";
555
555
  import { z } from "zod";
556
556
  import * as authSchema from "./db/auth";
@@ -566,7 +566,7 @@ const client = postgres(parsed.data.DATABASE_URL);
566
566
  export const db = drizzle(client, { schema });
567
567
 
568
568
  ${e}
569
- ${Rt}`}async function Er(e){switch(e.cacheProvider){case"redis":await bi(e),await Ai(e);break;case"upstash":await Ti(e),await ki(e);break}}async function bi(e){await l(`${e.projectDir}/packages/runtime/src/redis.ts`,`import type { Store } from "hono-rate-limiter";
569
+ ${Ct}`}async function Ar(e){switch(e.cacheProvider){case"redis":await Pi(e),await _i(e);break;case"upstash":await Ri(e),await Oi(e);break}}async function Pi(e){await l(`${e.projectDir}/packages/runtime/src/redis.ts`,`import type { Store } from "hono-rate-limiter";
570
570
  import { Redis } from "ioredis";
571
571
  import { RedisStore, type RedisReply } from "rate-limit-redis";
572
572
  import { env } from "./env";
@@ -690,7 +690,7 @@ export async function closeRedis() {
690
690
  closed = true;
691
691
  }
692
692
  }
693
- `)}async function Ai(e){await l(`${e.projectDir}/packages/runtime/src/mutex.ts`,`import { Mutex } from "redis-semaphore";
693
+ `)}async function _i(e){await l(`${e.projectDir}/packages/runtime/src/mutex.ts`,`import { Mutex } from "redis-semaphore";
694
694
  import { redis } from "./redis";
695
695
 
696
696
  export class MutexTimeoutError extends Error {
@@ -727,7 +727,7 @@ export async function withMutex<T>(
727
727
  await mutex.release();
728
728
  }
729
729
  }
730
- `)}async function Ti(e){await l(`${e.projectDir}/packages/runtime/src/redis.ts`,`import { Redis } from "@upstash/redis";
730
+ `)}async function Ri(e){await l(`${e.projectDir}/packages/runtime/src/redis.ts`,`import { Redis } from "@upstash/redis";
731
731
  import type { Store } from "hono-rate-limiter";
732
732
  import { env } from "./env";
733
733
 
@@ -840,7 +840,7 @@ export const limiterStore: Store = createLimiterStore();
840
840
 
841
841
  /** No persistent connection to close with Upstash REST. */
842
842
  export async function closeRedis(): Promise<void> {}
843
- `)}async function ki(e){await l(`${e.projectDir}/packages/runtime/src/mutex.ts`,`import { Lock } from "@upstash/lock";
843
+ `)}async function Oi(e){await l(`${e.projectDir}/packages/runtime/src/mutex.ts`,`import { Lock } from "@upstash/lock";
844
844
  import { redis } from "./redis";
845
845
 
846
846
  export class MutexTimeoutError extends Error {
@@ -885,7 +885,7 @@ export async function withMutex<T>(
885
885
  await lock.release();
886
886
  }
887
887
  }
888
- `)}async function wr(e){await l(`${e.projectDir}/packages/runtime/src/env.ts`,Ii(e))}function Ii(e){return`import { z } from "zod";
888
+ `)}async function Tr(e){await l(`${e.projectDir}/packages/runtime/src/env.ts`,xi(e))}function xi(e){return`import { z } from "zod";
889
889
 
890
890
  const EnvSchema = z.object({
891
891
  NODE_ENV: z.enum(["development", "production"]).default("development"),
@@ -1003,17 +1003,17 @@ export const env = (() => {
1003
1003
  const parsedApiUrl = new URL(env.API_URL);
1004
1004
  export const apiBasePath = parsedApiUrl.pathname === "/" ? "" : parsedApiUrl.pathname;
1005
1005
  export const apiOrigin = parsedApiUrl.origin;
1006
- `}import{readFile as Pi}from"fs/promises";import{join as K}from"path";var ft={"@upstash/redis":"^1.37.0","@upstash/lock":"^0.2.1","@neondatabase/serverless":"^1.0.1",postgres:"^3.4.7"};async function Pe(e){let t=await Pi(e,"utf-8");return JSON.parse(t)}async function _e(e,t){await l(e,JSON.stringify(t,null," ")+`
1007
- `)}function Fe(e,t){for(let r of t)delete e.dependencies?.[r],delete e.devDependencies?.[r]}function Ie(e,t,r,n=!1){let i=n?"devDependencies":"dependencies";e[i]||(e[i]={}),e[i][t]=r}async function br(e){await _i(e),await Ri(e),await Oi(e),await xi(e),e.frontend==="nextjs"?await Ci(e):await Di(e)}async function _i(e){let t=K(e.projectDir,"packages/api/package.json"),r=await Pe(t);Fe(r,["sharp","@types/sharp"]),await _e(t,r)}async function Ri(e){let t=K(e.projectDir,"packages/runtime/package.json"),r=await Pe(t);e.cacheProvider==="upstash"&&(Fe(r,["ioredis","rate-limit-redis","redis-semaphore"]),Ie(r,"@upstash/redis",ft["@upstash/redis"]),Ie(r,"@upstash/lock",ft["@upstash/lock"])),await _e(t,r)}async function Oi(e){let t=K(e.projectDir,"packages/database/package.json"),r=await Pe(t);e.databaseProvider==="neon"?(Fe(r,["pg","@types/pg"]),Ie(r,"@neondatabase/serverless",ft["@neondatabase/serverless"])):e.databaseProvider==="supabase"&&(Fe(r,["pg","@types/pg"]),Ie(r,"postgres",ft.postgres)),await _e(t,r)}async function xi(e){if(e.architecture!=="separate")return;let t=K(e.projectDir,"apps/backend/package.json"),r=await Pe(t);e.deploymentTarget!=="node"&&Fe(r,["@hono/node-server"]),await _e(t,r)}async function Di(e){if(e.architecture!=="separate")return;let t=K(e.projectDir,"apps/web-nuxt");await It(K(t,"server/api/[...paths].ts"),t);let r=K(t,"package.json"),n=await Pe(r),i=n.dependencies?.["@repo/api"];i&&(delete n.dependencies?.["@repo/api"],Ie(n,"@repo/api",i,!0)),await _e(r,n)}async function Ci(e){if(e.architecture!=="separate")return;let t=K(e.projectDir,"apps/web-next");await It(K(t,"app/api/[[...rest]]/route.ts"),t);let r=K(t,"package.json"),n=await Pe(r),i=n.dependencies?.["@repo/api"];i&&(delete n.dependencies?.["@repo/api"],Ie(n,"@repo/api",i,!0)),await _e(r,n)}import{readFile as Tr}from"fs/promises";import{join as kr}from"path";async function Ir(e){let t=kr(e.projectDir,"turbo.json"),r;try{r=await Tr(t,"utf-8")}catch{return}let n=JSON.parse(r),i=n.tasks?.build;if(!i)return;let o=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(i.env)){let d=i.env.filter(h=>h!==o);d.length!==i.env.length&&(i.env=d,a=!0)}if(Array.isArray(i.outputs)){let d=i.outputs.filter(h=>!s.has(h));d.length!==i.outputs.length&&(i.outputs=d,a=!0)}a&&await l(t,JSON.stringify(n,null," ")+`
1008
- `)}var Ni=["base.json","node.json","next.json"],Ar="GenerateSaaS ";async function Pr(e){if(!e.demo)for(let t of Ni){let r=kr(e.projectDir,"tooling/typescript",t),n;try{n=await Tr(r,"utf-8")}catch{continue}let i=JSON.parse(n);typeof i.display!="string"||!i.display.startsWith(Ar)||(i.display=i.display.slice(Ar.length),await l(r,JSON.stringify(i,null," ")+`
1009
- `))}}import{readFile as Li,rm as $i}from"fs/promises";import{existsSync as _r}from"fs";import{join as Rr}from"path";async function Or(e){if(e.frontend==="nuxt")return;let t=Rr(e.projectDir,"packages/i18n/package.json");if(!_r(t))throw new Error(`pruneI18nNuxt: expected ${t} to exist - did the i18n package move?`);let r=JSON.parse(await Li(t,"utf-8")),n=!!(r.exports?.["./module"]??r.exports?.["./nuxt"]),i=!1;if(r.exports)for(let s of["./module","./nuxt"])s in r.exports&&(delete r.exports[s],i=!0);r.devDependencies&&"@nuxt/kit"in r.devDependencies&&(delete r.devDependencies["@nuxt/kit"],i=!0),i&&await l(t,JSON.stringify(r,null," ")+`
1010
- `);let o=Rr(e.projectDir,"packages/i18n/nuxt");if(n&&!_r(o))throw new Error(`pruneI18nNuxt: packages/i18n declares a Nuxt export surface but ${o} is missing - did the i18n Nuxt module move?`);await $i(o,{recursive:!0,force:!0})}import{readFile as xr,rm as Ui}from"fs/promises";import{join as Ot}from"path";async function Dr(e){e.cacheProvider==="upstash"&&await Promise.all([ji(e.projectDir),Vi(e.projectDir),Ui(Ot(e.projectDir,"packages/runtime/tests/redis.test.ts"),{force:!0})])}async function ji(e){let t=Ot(e,"packages/runtime/tests/setup.ts"),r;try{r=await xr(t,"utf-8")}catch{return}let n=r.replace(/\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1006
+ `}import{readFile as Di}from"fs/promises";import{join as K}from"path";var ft={"@upstash/redis":"^1.37.0","@upstash/lock":"^0.2.1","@neondatabase/serverless":"^1.0.1",postgres:"^3.4.7"};async function Pe(e){let t=await Di(e,"utf-8");return JSON.parse(t)}async function _e(e,t){await l(e,JSON.stringify(t,null," ")+`
1007
+ `)}function Be(e,t){for(let r of t)delete e.dependencies?.[r],delete e.devDependencies?.[r]}function Ie(e,t,r,i=!1){let n=i?"devDependencies":"dependencies";e[n]||(e[n]={}),e[n][t]=r}async function kr(e){await Ci(e),await Ni(e),await Li(e),await $i(e),e.frontend==="nextjs"?await ji(e):await Ui(e)}async function Ci(e){let t=K(e.projectDir,"packages/api/package.json"),r=await Pe(t);Be(r,["sharp","@types/sharp"]),await _e(t,r)}async function Ni(e){let t=K(e.projectDir,"packages/runtime/package.json"),r=await Pe(t);e.cacheProvider==="upstash"&&(Be(r,["ioredis","rate-limit-redis","redis-semaphore"]),Ie(r,"@upstash/redis",ft["@upstash/redis"]),Ie(r,"@upstash/lock",ft["@upstash/lock"])),await _e(t,r)}async function Li(e){let t=K(e.projectDir,"packages/database/package.json"),r=await Pe(t);e.databaseProvider==="neon"?(Be(r,["pg","@types/pg"]),Ie(r,"@neondatabase/serverless",ft["@neondatabase/serverless"])):e.databaseProvider==="supabase"&&(Be(r,["pg","@types/pg"]),Ie(r,"postgres",ft.postgres)),await _e(t,r)}async function $i(e){if(e.architecture!=="separate")return;let t=K(e.projectDir,"apps/backend/package.json"),r=await Pe(t);e.deploymentTarget!=="node"&&Be(r,["@hono/node-server"]),await _e(t,r)}async function Ui(e){if(e.architecture!=="separate")return;let t=K(e.projectDir,"apps/web-nuxt");await Ot(K(t,"server/api/[...paths].ts"),t);let r=K(t,"package.json"),i=await Pe(r),n=i.dependencies?.["@repo/api"];n&&(delete i.dependencies?.["@repo/api"],Ie(i,"@repo/api",n,!0)),await _e(r,i)}async function ji(e){if(e.architecture!=="separate")return;let t=K(e.projectDir,"apps/web-next");await Ot(K(t,"app/api/[[...rest]]/route.ts"),t);let r=K(t,"package.json"),i=await Pe(r),n=i.dependencies?.["@repo/api"];n&&(delete i.dependencies?.["@repo/api"],Ie(i,"@repo/api",n,!0)),await _e(r,i)}import{readFile as Pr}from"fs/promises";import{join as _r}from"path";async function Rr(e){let t=_r(e.projectDir,"turbo.json"),r;try{r=await Pr(t,"utf-8")}catch{return}let i=JSON.parse(r),n=i.tasks?.build;if(!n)return;let o=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(n.env)){let d=n.env.filter(y=>y!==o);d.length!==n.env.length&&(n.env=d,a=!0)}if(Array.isArray(n.outputs)){let d=n.outputs.filter(y=>!s.has(y));d.length!==n.outputs.length&&(n.outputs=d,a=!0)}a&&await l(t,JSON.stringify(i,null," ")+`
1008
+ `)}var Vi=["base.json","node.json","next.json"],Ir="GenerateSaaS ";async function Or(e){if(!e.demo)for(let t of Vi){let r=_r(e.projectDir,"tooling/typescript",t),i;try{i=await Pr(r,"utf-8")}catch{continue}let n=JSON.parse(i);typeof n.display!="string"||!n.display.startsWith(Ir)||(n.display=n.display.slice(Ir.length),await l(r,JSON.stringify(n,null," ")+`
1009
+ `))}}import{readFile as Mi,rm as Fi}from"fs/promises";import{existsSync as xr}from"fs";import{join as Dr}from"path";async function Cr(e){if(e.frontend==="nuxt")return;let t=Dr(e.projectDir,"packages/i18n/package.json");if(!xr(t))throw new Error(`pruneI18nNuxt: expected ${t} to exist - did the i18n package move?`);let r=JSON.parse(await Mi(t,"utf-8")),i=!!(r.exports?.["./module"]??r.exports?.["./nuxt"]),n=!1;if(r.exports)for(let s of["./module","./nuxt"])s in r.exports&&(delete r.exports[s],n=!0);r.devDependencies&&"@nuxt/kit"in r.devDependencies&&(delete r.devDependencies["@nuxt/kit"],n=!0),n&&await l(t,JSON.stringify(r,null," ")+`
1010
+ `);let o=Dr(e.projectDir,"packages/i18n/nuxt");if(i&&!xr(o))throw new Error(`pruneI18nNuxt: packages/i18n declares a Nuxt export surface but ${o} is missing - did the i18n Nuxt module move?`);await Fi(o,{recursive:!0,force:!0})}import{readFile as Nr,rm as Bi}from"fs/promises";import{join as Nt}from"path";async function Lr(e){e.cacheProvider==="upstash"&&await Promise.all([Gi(e.projectDir),Ki(e.projectDir),Bi(Nt(e.projectDir,"packages/runtime/tests/redis.test.ts"),{force:!0})])}async function Gi(e){let t=Nt(e,"packages/runtime/tests/setup.ts"),r;try{r=await Nr(t,"utf-8")}catch{return}let i=r.replace(/\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1011
1011
  UPSTASH_REDIS_REST_TOKEN: "test-token",
1012
- `);n!==r&&await l(t,n)}async function Vi(e){let t=Ot(e,"packages/api/tests/setup.ts"),r;try{r=await xr(t,"utf-8")}catch{return}let n=r;n=n.replace(/\tREDIS_URL:\s*"[^"]*",?\n/g,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1012
+ `);i!==r&&await l(t,i)}async function Ki(e){let t=Nt(e,"packages/api/tests/setup.ts"),r;try{r=await Nr(t,"utf-8")}catch{return}let i=r;i=i.replace(/\tREDIS_URL:\s*"[^"]*",?\n/g,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1013
1013
  UPSTASH_REDIS_REST_TOKEN: "test-token",
1014
- `),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",
1014
+ `),i=i.replace(/vi\.mock\("ioredis"[\s\S]*?\n\}\);\n\n?/,""),i=i.replace(/vi\.mock\("rate-limit-redis"[\s\S]*?\n\}\);\n\n?/,""),i=i.replace(/\t\t\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1015
1015
  UPSTASH_REDIS_REST_TOKEN: "test-token",
1016
- `),n=n.replace(/(^vi\.mock\("@repo\/runtime")/m,`vi.mock("@upstash/redis", () => {
1016
+ `),i=i.replace(/(^vi\.mock\("@repo\/runtime")/m,`vi.mock("@upstash/redis", () => {
1017
1017
  class Redis {
1018
1018
  get = vi.fn().mockResolvedValue(null);
1019
1019
  set = vi.fn().mockResolvedValue("OK");
@@ -1030,8 +1030,8 @@ vi.mock("@upstash/lock", () => {
1030
1030
  return { Lock };
1031
1031
  });
1032
1032
 
1033
- $1`),n!==r&&await l(t,n)}import{readdir as Mi,readFile as Fi,rm as Be}from"fs/promises";import{join as ge}from"path";var Bi=new Set(["ci.yml"]),Gi=["cli","cli:clean","demo:bench","playground:regen","playground:test","playground:test:units"];async function Cr(e){let t=ge(e.projectDir,".github/workflows"),r=await Mi(t).catch(()=>[]);for(let n of r)Bi.has(n)||await Be(ge(t,n),{recursive:!0,force:!0})}async function Nr(e){let t=ge(e.projectDir,"package.json"),r=await Fi(t,"utf-8"),n=JSON.parse(r),i=!1;if(n.scripts){for(let o of Gi)o in n.scripts&&(delete n.scripts[o],i=!0);if(!Xe(e))for(let o of["infra","infra:stop"])o in n.scripts&&(delete n.scripts[o],i=!0)}i&&await l(t,JSON.stringify(n,null," ")+`
1034
- `)}async function Lr(e){let t=e.frontend==="nextjs"?"apps/web-nuxt":"apps/web-next";await Be(ge(e.projectDir,t),{recursive:!0,force:!0})}async function $r(e){e.docs||await Be(ge(e.projectDir,"apps/docs"),{recursive:!0})}async function Ur(e){let t=e.frontend==="nextjs"?"docs/nuxt":"docs/next";await Be(ge(e.projectDir,t),{recursive:!0,force:!0}),await Be(ge(e.projectDir,"docs/index.mdx"))}import{join as Ki}from"path";async function jr(e){let t=zi(e);await l(Ki(e.projectDir,".github/workflows/ci.yml"),t)}function zi(e){let t=L[e.databaseProvider].managed,r=e.cacheProvider==="upstash",n=e.frontend==="nuxt",i=t?"":` services:
1033
+ $1`),i!==r&&await l(t,i)}import{readdir as zi,readFile as Hi,rm as Ge}from"fs/promises";import{join as fe}from"path";var Yi=new Set(["ci.yml"]),Ji=["cli","cli:clean","demo:bench","playground:regen","playground:test","playground:test:units"];async function $r(e){let t=fe(e.projectDir,".github/workflows"),r=await zi(t).catch(()=>[]);for(let i of r)Yi.has(i)||await Ge(fe(t,i),{recursive:!0,force:!0})}async function Ur(e){let t=fe(e.projectDir,"package.json"),r=await Hi(t,"utf-8"),i=JSON.parse(r),n=!1;if(i.scripts){for(let o of Ji)o in i.scripts&&(delete i.scripts[o],n=!0);if(!Xe(e))for(let o of["infra","infra:stop"])o in i.scripts&&(delete i.scripts[o],n=!0)}n&&await l(t,JSON.stringify(i,null," ")+`
1034
+ `)}async function jr(e){let t=e.frontend==="nextjs"?"apps/web-nuxt":"apps/web-next";await Ge(fe(e.projectDir,t),{recursive:!0,force:!0})}async function Vr(e){e.docs||await Ge(fe(e.projectDir,"apps/docs"),{recursive:!0})}async function Mr(e){let t=e.frontend==="nextjs"?"docs/nuxt":"docs/next";await Ge(fe(e.projectDir,t),{recursive:!0,force:!0}),await Ge(fe(e.projectDir,"docs/index.mdx"))}import{join as Wi}from"path";async function Fr(e){let t=qi(e);await l(Wi(e.projectDir,".github/workflows/ci.yml"),t)}function qi(e){let t=N[e.databaseProvider].managed,r=e.cacheProvider==="upstash",i=e.frontend==="nuxt",n=t?"":` services:
1035
1035
  postgres:
1036
1036
  image: postgres:18-alpine
1037
1037
  env:
@@ -1051,7 +1051,7 @@ $1`),n!==r&&await l(t,n)}import{readdir as Mi,readFile as Fi,rm as Be}from"fs/pr
1051
1051
  STRIPE_SECRET_KEY: test
1052
1052
  STRIPE_WEBHOOK_SECRET: test`;case"polar":return`
1053
1053
  POLAR_ACCESS_TOKEN: test
1054
- 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(S=>`
1054
+ 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=>W[f].envVars.map(S=>`
1055
1055
  ${S.name}: test`).join("")).join("");return`# Deployment is handled by the hosting platform (Vercel, Coolify, etc.)
1056
1056
  # which auto-deploys on push. CI runs in parallel as a quality gate.
1057
1057
  # For PR-based workflows, enable GitHub branch protection to require CI before merging.
@@ -1085,7 +1085,7 @@ jobs:
1085
1085
  steps:
1086
1086
  - uses: actions/checkout@v6
1087
1087
  - uses: ./.github/actions/setup
1088
- ${n?` # vue-tsc on web-nuxt OOMs on the GitHub runner's default heap once
1088
+ ${i?` # vue-tsc on web-nuxt OOMs on the GitHub runner's default heap once
1089
1089
  # the type graph (Nuxt + Pinia + vue-i18n + content collections)
1090
1090
  # crosses a threshold. Bump to 6 GB; ubuntu-latest has ~7 GB RAM.
1091
1091
  - run: pnpm check-types
@@ -1096,7 +1096,7 @@ ${n?` # vue-tsc on web-nuxt OOMs on the GitHub runner's default heap once
1096
1096
  name: Test
1097
1097
  runs-on: ubuntu-latest
1098
1098
  timeout-minutes: 10
1099
- ${i} env:
1099
+ ${n} env:
1100
1100
  CONTENT_API_KEY: test-contentapi-key-16chars${s}${a}${d}
1101
1101
  STORAGE_REGION: test
1102
1102
  STORAGE_ENDPOINT: http://test
@@ -1111,41 +1111,45 @@ ${i} env:
1111
1111
  run: echo "DATABASE_URL=${o}" > .env
1112
1112
 
1113
1113
  - run: pnpm test
1114
- `}import{readFile as Vr}from"fs/promises";import{existsSync as Hi}from"fs";import{join as xt}from"path";var Mr="@repo/database";function Yi(e){return e?"pnpm -F @repo/database reset && pnpm -F @repo/database push --force":"pnpm -F @repo/database migrate"}function Ji(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 Wi(e,t,r){let n=await Vr(e,"utf-8"),i=JSON.parse(n),o=i.scripts?.[t];if(!o)throw new Error(`Cannot prepend to missing script "${t}" in ${e}`);o.includes(Mr)||(i.scripts={...i.scripts,[t]:`${r} && ${o}`},await l(e,JSON.stringify(i,null," ")+`
1115
- `))}async function qi(e,t){let r=Hi(e)?JSON.parse(await Vr(e,"utf-8")):{$schema:"https://openapi.vercel.sh/vercel.json"},n=r.buildCommand?.trim()||"pnpm build";n.includes(Mr)||(r.buildCommand=`${t} && ${n}`,await l(e,JSON.stringify(r,null," ")+`
1116
- `))}async function Fr(e){let t=Yi(e.demo===!0),r=Ji(e.architecture,e.frontend),n=xt(e.projectDir,"apps",r);switch(e.deploymentTarget){case"node":await Wi(xt(n,"package.json"),"start",t);return;case"vercel":await qi(xt(n,"vercel.json"),t);return;default:{let i=e.deploymentTarget;throw new Error(`generateDeployScripts: unhandled deployment target "${String(i)}"`)}}}import{readFile as Xi}from"fs/promises";import{existsSync as Zi}from"fs";import{join as gt}from"path";var Qi=["stripe","polar"];async function Br(e){let t=gt(e.projectDir,"packages/payments/src"),r=e.paymentProvider,n=`// Active payment provider. To switch, change the path below to
1114
+ `}import{readFile as Br}from"fs/promises";import{existsSync as Xi}from"fs";import{join as Lt}from"path";var Gr="@repo/database";function Zi(e){return e?"pnpm -F @repo/database reset && pnpm -F @repo/database push --force":"pnpm -F @repo/database migrate"}function Qi(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 eo(e,t,r){let i=await Br(e,"utf-8"),n=JSON.parse(i),o=n.scripts?.[t];if(!o)throw new Error(`Cannot prepend to missing script "${t}" in ${e}`);o.includes(Gr)||(n.scripts={...n.scripts,[t]:`${r} && ${o}`},await l(e,JSON.stringify(n,null," ")+`
1115
+ `))}async function to(e,t){let r=Xi(e)?JSON.parse(await Br(e,"utf-8")):{$schema:"https://openapi.vercel.sh/vercel.json"},i=r.buildCommand?.trim()||"pnpm build";i.includes(Gr)||(r.buildCommand=`${t} && ${i}`,await l(e,JSON.stringify(r,null," ")+`
1116
+ `))}async function Kr(e){let t=Zi(e.demo===!0),r=Qi(e.architecture,e.frontend),i=Lt(e.projectDir,"apps",r);switch(e.deploymentTarget){case"node":await eo(Lt(i,"package.json"),"start",t);return;case"vercel":await to(Lt(i,"vercel.json"),t);return;default:{let n=e.deploymentTarget;throw new Error(`generateDeployScripts: unhandled deployment target "${String(n)}"`)}}}import{readFile as ro}from"fs/promises";import{existsSync as no}from"fs";import{join as gt}from"path";var io=["stripe","polar"];async function zr(e){let t=gt(e.projectDir,"packages/payments/src"),r=e.paymentProvider,i=`// Active payment provider. To switch, change the path below to
1117
1117
  // "./polar/index" or "./none/index" (other folders are kept in place).
1118
1118
  export { ops } from "./${r}/index";
1119
- `;await l(gt(t,"providers/index.ts"),n),await l(gt(t,"index.ts"),await eo(t,r))}async function eo(e,t){let r=gt(e,"index.ts");if(!Zi(r))throw new Error(`generatePaymentBarrel: expected ${r} to exist - did packages/payments move?`);let n=await Xi(r,"utf-8"),i=Qi.filter(s=>s!==t);return n.split(`
1120
- `).filter(s=>!i.some(a=>s.includes(`./providers/${a}/`))).join(`
1121
- `)}import{readdir as to,readFile as ro}from"fs/promises";import{join as Gr}from"path";async function Kr(e){if(e.demo)return;let t=e.appName.trim()||e.projectName,r=JSON.stringify(t).slice(1,-1),n=Gr(e.projectDir,"packages/i18n/translations"),i;try{i=await to(n)}catch{return}for(let o of i){let s=Gr(n,o,"web.json"),a;try{a=await ro(s,"utf-8")}catch{continue}let d=a.replaceAll("GenerateSaaS",r);d!==a&&await l(s,d)}}import{readFile as no}from"fs/promises";import{join as zr}from"path";var io=[".nuxt/",".nuxt",".nitro/",".nitro",".output/",".output","_locales/"],oo=[".next/",".next",".svelte-kit/",".svelte-kit",".wrangler/",".wrangler",".dev.vars"];async function Yr(e){let r=e.frontend==="nuxt"?oo:io;await Hr(zr(e.projectDir,".gitignore"),r),await Hr(zr(e.projectDir,".dockerignore"),r)}async function Hr(e,t){let r;try{r=await no(e,"utf-8")}catch{return}let n=new Set(t),i=r.split(`
1122
- `).filter(o=>!n.has(o.trim()));i.length!==r.split(`
1123
- `).length&&await l(e,i.join(`
1124
- `))}async function ht(e){let t=e.projectDir;e.demo||await or(t),await sr(t,e.aiTools),await ar(t,e.frontend),await lr(e),await pr(e),await dr(e),e.demo||await ur(e),await gr(e);let r=await hr(e);return await yr(e),await vr(e),await Sr(e),await Er(e),await wr(e),await br(e),await Ir(e),await Pr(e),await Or(e),await Dr(e),await Cr(e),await jr(e),await Nr(e),await Lr(e),await $r(e),await Ur(e),await Fr(e),await Br(e),await Kr(e),await Yr(e),{dockerComposeGenerated:r}}import{basename as Wr,join as qr,relative as ao}from"path";import{createHash as Jr}from"crypto";import{readFile as so}from"fs/promises";async function yt(e){let t=await so(e);return Jr("sha256").update(t).digest("hex")}function Dt(e){return Jr("sha256").update(e).digest("hex")}var co=new Set(["data",U]);function lo(e){let t=e.split("/");for(let r of t)if(Tt.has(r)||co.has(r)||r.startsWith(".env")&&!r.includes("example"))return!0;return!1}function Xr(e,t){return{projectName:e.projectName??Wr(t),appName:e.appName??e.projectName??Wr(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,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 Zr(e,t){let n=(await fe(e.projectDir,e.projectDir,lo)).sort(),i=await Promise.all(n.map(async a=>[ao(e.projectDir,a),await yt(a)])),o=Object.fromEntries(i),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,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(qr(e.projectDir,Q),JSON.stringify(s,null," ")+`
1125
- `),await l(qr(e.projectDir,Jt),JSON.stringify(o,null," ")+`
1126
- `)}import{relative as po}from"path";async function Ge(e){let r=(await fe(e,e,Ae)).sort(),n=await Promise.all(r.map(async i=>[po(e,i),await yt(i)]));return Object.fromEntries(n)}import{copyFile as uo,mkdir as mo,rm as fo}from"fs/promises";import{dirname as go,join as Qr,relative as ho}from"path";import{existsSync as yo}from"fs";async function Ct(e,t){yo(t)&&await fo(t,{recursive:!0,force:!0});let r=await fe(e,e,Ae);for(let n of r){let i=ho(e,n),o=Qr(t,i);await mo(go(o),{recursive:!0}),await uo(n,o)}}async function en(e,t){await Ct(e,Qr(t,ct))}import{existsSync as vo}from"fs";import{readFile as tn,readdir as So}from"fs/promises";import{join as z,dirname as Eo,resolve as wo,sep as bo}from"path";import{fileURLToPath as Ao}from"url";var Ke={"claude-code":".claude/skills",cursor:".cursor/skills",codex:".agents/skills","gemini-cli":".gemini/skills",windsurf:".windsurf/skills"},Sl=Object.values(Ke),Nt="generatesaas-update",rn=Eo(Ao(import.meta.url));function To(){let e=z(rn,"skill","content");return vo(e)?e:z(rn,"content")}function Lt(e){return!e||e.length===0?[]:e.map(t=>Ke[t])}async function $t(e,t,r,n){let i=Lt(n);for(let o of i){let s=z(e,o,Nt),a=z(s,"scripts"),d=z(s,"references");await ut(a),await ut(d),await l(z(s,"SKILL.md"),t.replaceAll("__SKILL_ROOT__",o)),await l(z(d,".gitkeep"),"");for(let[h,g]of Object.entries(r)){let m=wo(a,h);m.startsWith(a+bo)&&await l(m,g)}}}async function nn(e,t){let r=To(),n=await tn(z(r,"SKILL.md"),"utf-8"),i=z(r,"scripts"),o=await So(i),s={};for(let a of o)a!==".gitkeep"&&(s[a]=await tn(z(i,a),"utf-8"));await $t(e,n,s,t)}import{execFile as ko,execFileSync as Io}from"child_process";import{access as on,readFile as Po}from"fs/promises";import{join as Ut}from"path";import*as P from"@clack/prompts";function ye(e){try{let t=process.platform==="win32"?"where":"which";return Io(t,[e],{stdio:"ignore"}),!0}catch{return!1}}function he(e,t,r,n=3e5){return new Promise((i,o)=>{ko(e,t,{cwd:r,timeout:n},(s,a,d)=>{if(s){let h=String(a||"").trim(),m=[String(d||"").trim(),h].filter(Boolean).join(`
1127
- `);o(new Error(m?`${s.message}
1128
- ${m}`:s.message))}else i()})})}async function sn(e){if(!ye("pnpm"))return P.log.warn("pnpm not found. Skipping lockfile regeneration."),!1;try{return await he("pnpm",["install","--lockfile-only","--no-frozen-lockfile","--config.minimumReleaseAge=0"],e),!0}catch(t){let r=t instanceof Error?t.message:String(t);return P.log.warn(`Lockfile regeneration failed: ${r}`),P.log.warn("Deploys using --frozen-lockfile may fail."),!1}}async function an(e){if(!ye("pnpm"))return P.log.warn("pnpm not found. Skipping dependency installation."),P.log.info("Install pnpm: https://pnpm.io/installation"),!1;let t=P.spinner();t.start("Installing dependencies (this may take a minute)...");try{return await he("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 P.log.warn(`pnpm install failed: ${n}`),P.log.warn("You can run it manually later."),!1}}async function cn(e){if(!ye("pnpm"))return!1;let t=P.spinner();t.start("Generating baseline database migration...");try{return await he("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 P.log.warn(`Could not generate baseline migration: ${n}`),P.log.warn("Run 'pnpm -F @repo/database generate' before your first deploy."),!1}}async function ln(e){try{return await on(Ut(e,".git")),P.log.info("Git repository already exists, skipping init."),!0}catch{}if(!ye("git"))return P.log.warn("git not found. Skipping repository initialization."),!1;let t=P.spinner();t.start("Initializing git repository...");try{return await he("git",["init"],e),await he("git",["add","-A"],e),await he("git",["commit","--no-verify","-m","Initial commit from GenerateSaaS"],e),t.stop("Git repository initialized."),!0}catch{return t.stop("Git initialization failed."),P.log.warn("You can run git init manually later."),!1}}async function pn(e){if(!ye("pnpm"))return!1;try{await on(Ut(e,".git"))}catch{return!1}try{let t=JSON.parse(await Po(Ut(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 he("pnpm",["exec","simple-git-hooks"],e),!0}catch{return P.log.warn("Could not install git hooks. Run 'pnpm exec simple-git-hooks' manually."),!1}}import*as Re from"@clack/prompts";import j from"picocolors";function dn(e,t){t.dockerComposeGenerated&&!t.dockerAvailable&&Re.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 o=e.dockerServices.map(s=>we[s].label).join(", ");r.push(`pnpm infra ${j.dim(`# ${o}`)}`)}if(r.push(`pnpm dev ${j.dim("# http://localhost:3000")}`),t.skippedCredentials.length>0&&(r.push(""),r.push(j.dim("Fill in remaining TODO values in .env"))),Re.note(r.join(`
1129
- `),j.yellow("Start Development")),t.dockerComposeGenerated){let o=[];o.push(`App ${j.cyan("http://localhost:3000")}`),e.architecture==="separate"&&o.push(`API ${j.cyan("http://localhost:3010")}`),e.dockerServices.includes("mailpit")&&o.push(`Mailpit ${j.cyan("http://localhost:8025")}`),e.dockerServices.includes("inngest")&&o.push(`Inngest ${j.cyan("http://localhost:8288")}`),Re.note(o.join(`
1130
- `),j.yellow("Dev Tools"))}let n=[],i=_o(e);i.length>0&&n.push(`Set in production: ${j.dim(i.join(", "))}`),n.push("pnpm db:push # Run database migrations"),n.push(Ro(e)),Re.note(n.join(`
1131
- `),j.yellow("Deployment"))}function _o(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 Ro(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 un(e){let t={};if(e.name!==void 0){if(!st(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(!Ue.includes(e.frontend))throw new Error(`Invalid frontend "${e.frontend}". Valid values: ${Ue.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.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=jt(e.docker,tt,"docker service")),e.aiTools!==void 0&&(t.aiTools=jt(e.aiTools,rt,"AI tool")),e.socialProviders!==void 0&&(t.socialProviders=jt(e.socialProviders,it,"social provider")),e.currency!==void 0){if(!ce.includes(e.currency))throw new Error(`Invalid currency "${e.currency}". Valid values: ${ce.join(", ")}`);t.defaultCurrency=e.currency}if(e.deploy!==void 0){if(!le.includes(e.deploy))throw new Error(`Invalid deployment target "${e.deploy}". Valid values: ${le.join(", ")}`);t.deploymentTarget=e.deploy}if(e.database!==void 0){if(!pe.includes(e.database))throw new Error(`Invalid database provider "${e.database}". Valid values: ${pe.join(", ")}`);t.databaseProvider=e.database}if(e.cache!==void 0){if(!de.includes(e.cache))throw new Error(`Invalid cache provider "${e.cache}". Valid values: ${de.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 ne={projectName:"my-saas",frontend:"nextjs",architecture:"fullstack",paymentProvider:"stripe",emailProvider:"smtp",multiTenancy:!1,billingScope:"user",blog:!0,docs:!1,revenueSharing:!1,credits:!0,dockerServices:["postgres","redis","inngest"],aiTools:[],socialProviders:[],defaultCurrency:"USD",deploymentTarget:"node",databaseProvider:"postgres",cacheProvider:"redis"};function mn(e){let t=e.projectName??ne.projectName,r=e.projectDir??`./${t}`,n=e.appName??ot(t),i=e.deploymentTarget??ne.deploymentTarget,o=B[i]?.edgeRuntime??!1,s=e.databaseProvider??(o?"neon":ne.databaseProvider),a=e.cacheProvider??(o?"upstash":ne.cacheProvider),d=e.emailProvider??(o?"resend":ne.emailProvider),h=e.dockerServices??(o?ne.dockerServices.filter(m=>m!=="postgres"&&m!=="redis"):ne.dockerServices),g={...ne,...e,projectName:t,appName:n,projectDir:r,deploymentTarget:i,databaseProvider:s,cacheProvider:a,emailProvider:d,dockerServices:h};g.paymentProvider==="none"&&(g.credits=!1);for(let m of Le){if(g.deploymentTarget!==m.target)continue;let S=g.databaseProvider===m.provider?"database":"cache";if(g.databaseProvider===m.provider||g.cacheProvider===m.provider)throw new Error(`Incompatible: --deploy ${m.target} + --${S} ${m.provider}. ${m.reason}`)}for(let m of $e)if(g.architecture===m.architecture&&g.deploymentTarget===m.target)throw new Error(`Incompatible: --architecture ${m.architecture} + --deploy ${m.target}. ${m.reason}`);return g}function jt(e,t,r){if(e.trim()==="")return[];let n=e.split(",").map(o=>o.trim()).filter(Boolean),i=n.filter(o=>!t.includes(o));if(i.length>0)throw new Error(`Invalid ${r}(s): ${i.join(", ")}. Valid values: ${t.join(", ")}`);return n}import Lo from"picocolors";var $o="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";function Uo(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 fn(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 V("--frontend <type>","frontend framework").choices([...Ue])).addOption(new V("--architecture <type>","fullstack or separate").choices([...Ze])).addOption(new V("--payment <provider>","payment provider").choices([...Qe])).addOption(new V("--email <provider>","email provider").choices([...et])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").addOption(new V("--billing-scope <scope>","billing scope (requires --org)").choices([...nt])).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("--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 V("--currency <code>","default currency for billing").choices([...ce])).addOption(new V("--deploy <target>","deployment target").choices([...le])).addOption(new V("--database <provider>","database provider").choices([...pe])).addOption(new V("--cache <provider>","cache provider").choices([...de])).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 V("--demo","first-party demo build: keep sample content, mark site non-indexable - requires CI API key").hideHelp()).addOption(new V("--no-db-migration","skip generating the baseline DB migration (internal: demos/CI/playground)").hideHelp()).action(async(t,r)=>{await jo(t?{...r,apiKey:t}:r)})}async function jo(e){let t=performance.now();Kt("1.10.0");let r,n;try{r=un(e),n=Uo(e.templateVersion)}catch(y){E.cancel(I(y)),process.exit(1)}let i=E.spinner(),o;try{o=await be({apiKey:e.apiKey,prompt:!e.yes})}catch(y){E.cancel(I(y)),process.exit(1)}e.demo&&Dt(o)!==$o&&(E.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let s=Z(o),a=async()=>{let y=await re(s),R=y.latest,Y=n??R;if(n&&!y.versions.some(J=>J.version===Y))throw new Error(`Template version "${n}" is not available.`);return{latestVersion:R,selectedVersion:Y}};i.start("Verifying access...");let d,h;try{({latestVersion:d,selectedVersion:h}=await a()),i.stop("Access verified."),me(o)}catch(y){if(i.stop("Access verification failed."),y instanceof O&&y.status===401){e.yes&&(E.cancel("Invalid API key. Cannot prompt in non-interactive mode."),process.exit(1)),E.log.warning("Invalid API key."),o=await Me(),s=Z(o),i.start("Verifying access...");try{({latestVersion:d,selectedVersion:h}=await a()),i.stop("Access verified."),me(o)}catch(R){i.stop("Access verification failed."),E.cancel(R instanceof O&&R.status===401?"Invalid API key.":I(R)),process.exit(1)}}else E.cancel(I(y)),process.exit(1)}E.log.success(`Latest version: ${d}`),h!==d&&E.log.success(`Using template version: ${h}`);let g;e.yes?g=mn(r):g=await Yt(r);let m;i.start("Activating license...");try{let y=crypto.randomUUID(),R=()=>({frontend:g.frontend,version:h,installId:y,projectName:g.projectName,options:Qt(g)}),Y;try{Y=await bt(s,R())}catch(J){let W=lt(J);if(!W?.lastAllowedVersion)throw J;i.stop("License activation failed."),e.yes&&(E.cancel(`${W.message} Re-run with --template-version ${W.lastAllowedVersion}.`),process.exit(1));let Ee=await E.confirm({message:`Your update window has ended. Continue with v${W.lastAllowedVersion} (the last version your license covers)?`});(E.isCancel(Ee)||!Ee)&&(E.cancel("Setup cancelled."),process.exit(0)),h=W.lastAllowedVersion,i.start(`Activating license for v${h}...`),Y=await bt(s,R())}m={token:Y.token,keyHash:Dt(o),installId:y},i.stop("License activated.")}catch(y){i.stop("License activation failed."),E.cancel(I(y)),process.exit(1)}let S=No(g.projectDir);if(Oo(S)&&xo(S).length>0)if(e.yes)E.log.info(`Directory ${S} is not empty. Merging (keeping existing files, overwriting conflicts).`);else{let R=await E.select({message:`Directory ${S} 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"}]});(E.isCancel(R)||R==="cancel")&&(E.cancel("Setup cancelled."),process.exit(0)),R==="overwrite"&&Do(S,{recursive:!0,force:!0})}let T={...g,projectDir:S,version:h,...e.demo?{docs:!1}:{}};i.start("Downloading template...");try{await pt(s,h,S),i.stop("Template downloaded.")}catch(y){i.stop("Download failed."),E.cancel(I(y)),process.exit(1)}let H;i.start("Generating project files...");try{if({dockerComposeGenerated:H}=await ht(T),!e.demo){let y=await Ge(S);await l(Co(S,at),JSON.stringify(y,null," ")+`
1132
- `),await en(S,S)}await nn(S,T.aiTools),await Zr(T,m),i.stop("Project files generated.")}catch(y){i.stop("Generation failed."),E.cancel(I(y)),process.exit(1)}await sn(S);let D=await an(S);D&&T.demo!==!0&&e.dbMigration!==!1&&await cn(S),await ln(S),D&&await pn(S);let ie=ye("docker"),k=wt(T).map(y=>y.key).filter(y=>!T.credentials?.[y]);dn(T,{pnpmInstalled:D,dockerComposeGenerated:H,dockerAvailable:ie,skippedCredentials:k}),zt(),E.log.info(Lo.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as gn}from"fs";import{readFile as Go}from"fs/promises";import{join as ze,resolve as Ko}from"path";import*as _ from"@clack/prompts";import Oe from"picocolors";import{mkdtemp as Vo,rm as Mo}from"fs/promises";import{tmpdir as Fo}from"os";import{join as Bo}from"path";async function Vt(e,t,r,n){let i=await Vo(Bo(Fo(),"generatesaas-stage-"));try{await pt(e,t,i),await ht({...r,projectDir:i}),await Ct(i,n)}finally{await Mo(i,{recursive:!0,force:!0})}}function hn(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=Ko(t.cwd??process.cwd()),n=ze(r,Q),i;try{i=JSON.parse(await Go(n,"utf-8"))}catch{_.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let o;try{o=await be()}catch(d){_.cancel(I(d)),process.exit(1)}let s=Z(o),a=_.spinner();try{a.start("Verifying access...");let d;try{d=await re(s)}catch(k){throw k instanceof O&&k.status===401?new Error("Your saved API key was rejected. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):k}a.stop("Access verified."),me(o),a.start("Fetching latest skill files...");let h=await Zt(s,d.latest);await $t(r,h.skillMd,h.scripts,i.aiTools);let g=Lt(i.aiTools);if(a.stop("Skills updated."),_.log.success(`Skill files installed to ${Oe.cyan(g.length.toString())} locations.`),i.version===d.latest){_.log.info(`Already on the latest version (${i.version}).`);return}if(i.licenseToken)try{let k=await er(s,{currentToken:i.licenseToken,newVersion:d.latest});i.licenseToken=k.token,await l(n,JSON.stringify(i,null," ")+`
1133
- `),_.log.success("License refreshed.")}catch(k){let y=lt(k);y&&(_.cancel(y.message),process.exit(1)),_.log.warn("License refresh skipped.")}let m=Xr(i,r),S=ze(r,Wt);a.start(`Staging v${d.latest} (shaped for your config)...`),await Vt(s,d.latest,m,S),a.stop("Template staged.");let T=await Xt(s,d.latest);T&&_.note(T,`Changelog v${d.latest}`);let H=ze(r,at),D=ze(r,ct),ie=!gn(D),oe=!gn(H);if(ie){if(a.start("Building baseline template (one-time migration)..."),await Vt(s,i.version,m,D),oe){let k=await Ge(D);await l(H,JSON.stringify(k,null," ")+`
1134
- `)}a.stop("Baseline template stored.")}else if(oe){a.start("Computing baseline template hashes...");let k=await Ge(D);await l(H,JSON.stringify(k,null," ")+`
1135
- `),a.stop("Baseline hashes computed.")}if(await l(ze(r,qt),JSON.stringify({currentVersion:i.version,targetVersion:d.latest,changelog:T,stagedAt:new Date().toISOString()},null," ")+`
1136
- `),_.log.info(`Update staged: ${Oe.cyan(i.version)} \u2192 ${Oe.cyan(d.latest)}`),i.aiTools&&i.aiTools.length>0){let k=i.aiTools[0],y=Ne[k].label;_.log.info(`Open your project in ${Oe.cyan(y)} and ask: ${Oe.cyan("'update my GenerateSaaS project'")}`)}else _.log.info(`Ask your AI coding assistant to ${Oe.cyan("'update my GenerateSaaS project'")}.`)}catch(d){a.stop("Failed."),_.cancel(`Update failed: ${I(d)}`),process.exit(1)}})}import*as $ from"@clack/prompts";import M from"picocolors";import{readFile as zo}from"fs/promises";import{join as Ho,resolve as Yo}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=Yo(t.cwd??process.cwd()),n=Ho(r,Q),i;try{i=JSON.parse(await zo(n,"utf-8"))}catch{$.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let o=[`Version: ${M.cyan(i.version)}`,`Frontend: ${M.cyan(i.frontend)}`,i.deploymentTarget?`Deploy target: ${M.cyan(i.deploymentTarget)}`:null,i.databaseProvider?`Database: ${M.cyan(i.databaseProvider)}`:null,i.cacheProvider?`Cache: ${M.cyan(i.cacheProvider)}`:null,i.aiTools&&i.aiTools.length>0?`AI tools: ${M.cyan(i.aiTools.join(", "))}`:null].filter(Boolean).join(`
1137
- `);$.note(o,M.bold("Project Status"));let s=$.spinner();s.start("Checking for updates...");try{let a=await be(),d=Z(a),g=(await re(d)).latest;i.version===g?(s.stop("Up to date."),$.log.success(`Already on the latest version (${M.green(g)})`)):(s.stop("Update available."),$.log.warning(`Update available: ${M.yellow(i.version)} \u2192 ${M.green(g)}`),$.log.info(`Open this project in your AI coding agent and ask it to ${M.cyan("update my GenerateSaaS project")} - it fetches and applies the update for you.`))}catch(a){s.stop("Check failed."),a instanceof O&&a.status===401?$.log.warning("Invalid API key. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):$.log.warning(`Could not check for updates: ${I(a)}`)}})}import{readFile as Jo}from"fs/promises";import*as A from"@clack/prompts";import b from"picocolors";function Wo(){return process.env.GENERATESAAS_API_KEY??Ve()}function qo(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 Xo(e){return{verdict:e.verdict??"unknown",ejectedAt:e.ejectedAt??null}}function Zo(e){switch(e.verdict){case"licensed":return A.log.success(`${b.green("LICENSED")} - resolves to an account with an active${e.plan?` ${e.plan}`:""} license.`),!0;case"ejected":return A.log.success(`${b.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(`${b.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(`${b.red("LEAKED TOKEN")} - this license belongs to a different deployment${e.mismatchDomain?` (${b.cyan(e.mismatchDomain)})`:""}, not this site. The token was copied from a licensed project.`),!1;case"no_license_history":return A.log.error(`${b.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(`${b.yellow("UNKNOWN")} - could not cross-reference the records right now. Try again shortly.`),!1}}async function vn(e,t){let r=process.env.GENERATESAAS_API_URL??je,n=Wo();e.start("Cross-referencing license records...");try{let i=n?qo(await tr(r,n,{lkh:t.lkh,nid:t.nid,domain:t.domain})):Xo(await At(r,{token:t.token,domain:t.domain}));return e.stop(`${b.green("Checked")} - records cross-referenced`),Zo(i)}catch(i){return e.stop(`${b.yellow("Skipped")} - ${I(i)}`),null}}function Qo(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 Mt(e){return typeof e!="number"?"unknown":new Date(e*1e3).toISOString().split("T")[0]}function Sn(e){A.note([`License ID: ${b.cyan(String(e.lid??"unknown"))}`,`Version: ${b.cyan(String(e.ver??"unknown"))}`,`Init version: ${String(e.iver??"unknown")}`,`Frontend: ${String(e.fe??"unknown")}`,`Created: ${Mt(e.pat)}`,`Last updated: ${Mt(e.uat)}`,`Expires: ${Mt(e.exp)}`,`Install ID: ${String(e.nid??"unknown")}`].join(`
1138
- `),b.yellow("License Details"))}function es(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 En(e){let t=A.spinner(),r=null,n="no candidates";for(let s of es(e)){t.start(`Checking ${s}...`);try{let a=await fetch(s);if(!a.ok){n=`${s} returned ${a.status}`,t.stop(`${b.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(`${b.yellow("Not here")} - ${n}`);continue}r=d,t.stop(`${b.green("Found")} - license endpoint responded`);break}catch(a){n=`${s}: ${I(a)}`,t.stop(`${b.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=wn(e);return s?await vn(t,{domain:s})??!1:!1}let i;try{i=Qo(r)}catch{return A.log.error("Could not decode JWT payload."),!1}t.start("Verifying signature...");try{let s=process.env.GENERATESAAS_API_URL??je,a=await At(s,{token:r});if(a.valid)t.stop(`${b.green("Valid")} - signature verified`);else return t.stop(`${b.red("Invalid")} - ${a.reason}`),!1}catch{return t.stop(`${b.yellow("Skipped")} - could not reach verification service`),A.log.warn("Signature not verified. Displaying unverified claims:"),Sn(i),!1}return Sn(i),await vn(t,{token:r,lkh:typeof i.lkh=="string"?i.lkh:void 0,nid:typeof i.nid=="string"?i.nid:void 0,domain:wn(e)})??!0}function wn(e){try{return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`).hostname}catch{return}}function bn(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 i=(await Jo(r.file,"utf-8")).split(`
1139
- `).map(s=>s.trim()).filter(s=>s&&!s.startsWith("#"));i.length===0&&(A.cancel("No URLs found in file."),process.exit(1));let o=0;for(let s of i)await En(s)&&o++,A.log.info("");A.log.success(`${o}/${i.length} sites verified.`)}else await En(t)||process.exit(1)})}import{existsSync as ts,rmSync as rs}from"fs";import*as F from"@clack/prompts";function An(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){ts(X)?(rs(X),F.log.success("API key removed.")):F.log.info("No API key configured.");return}let r=Ve();r?F.log.info(`Current API key: ****${r.slice(-4)}`):F.log.info("No API key configured.");let n=await Me(),i=Z(n),o=F.spinner();o.start("Verifying API key...");try{await re(i),o.stop("API key verified."),me(n),F.log.success("API key saved.")}catch(s){o.stop("Verification failed."),s instanceof O&&s.status===401?F.cancel("Invalid API key."):F.cancel(I(s)),process.exit(1)}})}import{existsSync as vt,rmSync as ns,readFileSync as Bt,writeFileSync as Tn}from"fs";import{join as ve}from"path";import*as x from"@clack/prompts";var is=["packages/api/src/functions/maintenance/license-heartbeat.ts","packages/api/src/lib/manifest.ts","packages/api/src/routes/internal/license.ts"],os=[{file:"packages/api/src/routes/inngest.ts",removals:[`import { licenseHeartbeatFunction } from "../functions/maintenance/license-heartbeat";
1119
+ `;await l(gt(t,"providers/index.ts"),i),await l(gt(t,"index.ts"),await oo(t,r))}async function oo(e,t){let r=gt(e,"index.ts");if(!no(r))throw new Error(`generatePaymentBarrel: expected ${r} to exist - did packages/payments move?`);let i=await ro(r,"utf-8"),n=io.filter(s=>s!==t);return i.split(`
1120
+ `).filter(s=>!n.some(a=>s.includes(`./providers/${a}/`))).join(`
1121
+ `)}import{readdir as so,readFile as ao}from"fs/promises";import{join as Hr}from"path";async function Yr(e){if(e.demo)return;let t=e.appName.trim()||e.projectName,r=JSON.stringify(t).slice(1,-1),i=Hr(e.projectDir,"packages/i18n/translations"),n;try{n=await so(i)}catch{return}for(let o of n){let s=Hr(i,o,"web.json"),a;try{a=await ao(s,"utf-8")}catch{continue}let d=a.replaceAll("GenerateSaaS",r);d!==a&&await l(s,d)}}import{readFile as co}from"fs/promises";import{join as Jr}from"path";var lo=[".nuxt/",".nuxt",".nitro/",".nitro",".output/",".output","_locales/"],po=[".next/",".next",".svelte-kit/",".svelte-kit",".wrangler/",".wrangler",".dev.vars"];async function qr(e){let r=e.frontend==="nuxt"?po:lo;await Wr(Jr(e.projectDir,".gitignore"),r),await Wr(Jr(e.projectDir,".dockerignore"),r)}async function Wr(e,t){let r;try{r=await co(e,"utf-8")}catch{return}let i=new Set(t),n=r.split(`
1122
+ `).filter(o=>!i.has(o.trim()));n.length!==r.split(`
1123
+ `).length&&await l(e,n.join(`
1124
+ `))}async function ht(e){let t=e.projectDir;e.demo||await cr(t),await lr(t,e.aiTools),await pr(t,e.frontend),await ur(e),await mr(e),await fr(e),e.demo||await gr(e),await vr(e);let r=await Sr(e);return await Er(e),await wr(e),await br(e),await Ar(e),await Tr(e),await kr(e),await Rr(e),await Or(e),await Cr(e),await Lr(e),await $r(e),await Fr(e),await Ur(e),await jr(e),await Vr(e),await Mr(e),await Kr(e),await zr(e),await Yr(e),await qr(e),{dockerComposeGenerated:r}}import{basename as Zr,join as Qr,relative as mo}from"path";import{createHash as Xr}from"crypto";import{readFile as uo}from"fs/promises";async function yt(e){let t=await uo(e);return Xr("sha256").update(t).digest("hex")}function $t(e){return Xr("sha256").update(e).digest("hex")}var fo=new Set(["data",U]);function go(e){let t=e.split("/");for(let r of t)if(It.has(r)||Pt.has(r)||fo.has(r)||r.startsWith(".env")&&!r.includes("example"))return!0;return!1}function en(e,t){return{projectName:e.projectName??Zr(t),appName:e.appName??e.projectName??Zr(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,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 tn(e,t){let i=(await me(e.projectDir,e.projectDir,go)).sort(),n=await Promise.all(i.map(async a=>[mo(e.projectDir,a),await yt(a)])),o=Object.fromEntries(n),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,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(Qr(e.projectDir,Q),JSON.stringify(s,null," ")+`
1125
+ `),await l(Qr(e.projectDir,Zt),JSON.stringify(o,null," ")+`
1126
+ `)}import{relative as ho}from"path";async function Re(e){let r=(await me(e,e,Ae)).sort(),i=await Promise.all(r.map(async n=>[ho(e,n),await yt(n)]));return Object.fromEntries(i)}import{copyFile as yo,mkdir as vo,rm as So}from"fs/promises";import{dirname as Eo,join as rn,relative as wo}from"path";import{existsSync as bo}from"fs";async function Ut(e,t){bo(t)&&await So(t,{recursive:!0,force:!0});let r=await me(e,e,Ae);for(let i of r){let n=wo(e,i),o=rn(t,n);await vo(Eo(o),{recursive:!0}),await yo(i,o)}}async function nn(e,t){await Ut(e,rn(t,ct))}import{existsSync as Ao}from"fs";import{readFile as on,readdir as To}from"fs/promises";import{join as z,dirname as ko,resolve as Io,sep as Po}from"path";import{fileURLToPath as _o}from"url";var Ke={"claude-code":".claude/skills",cursor:".cursor/skills",codex:".agents/skills","gemini-cli":".gemini/skills",windsurf:".windsurf/skills"},kl=Object.values(Ke),jt="generatesaas-update",sn=ko(_o(import.meta.url));function Ro(){let e=z(sn,"skill","content");return Ao(e)?e:z(sn,"content")}function Vt(e){return!e||e.length===0?[]:e.map(t=>Ke[t])}async function Mt(e,t,r,i){let n=Vt(i);for(let o of n){let s=z(e,o,jt),a=z(s,"scripts"),d=z(s,"references");await ut(a),await ut(d),await l(z(s,"SKILL.md"),t.replaceAll("__SKILL_ROOT__",o)),await l(z(d,".gitkeep"),"");for(let[y,h]of Object.entries(r)){let f=Io(a,y);f.startsWith(a+Po)&&await l(f,h)}}}async function an(e,t){let r=Ro(),i=await on(z(r,"SKILL.md"),"utf-8"),n=z(r,"scripts"),o=await To(n),s={};for(let a of o)a!==".gitkeep"&&(s[a]=await on(z(n,a),"utf-8"));await Mt(e,i,s,t)}import{execFile as Oo,execFileSync as xo}from"child_process";import{access as cn,readFile as Do}from"fs/promises";import{join as Ft}from"path";import*as P from"@clack/prompts";function he(e){try{let t=process.platform==="win32"?"where":"which";return xo(t,[e],{stdio:"ignore"}),!0}catch{return!1}}function ge(e,t,r,i=3e5){return new Promise((n,o)=>{Oo(e,t,{cwd:r,timeout:i},(s,a,d)=>{if(s){let y=String(a||"").trim(),f=[String(d||"").trim(),y].filter(Boolean).join(`
1127
+ `);o(new Error(f?`${s.message}
1128
+ ${f}`:s.message))}else n()})})}async function ln(e){if(!he("pnpm"))return P.log.warn("pnpm not found. Skipping lockfile regeneration."),!1;try{return await ge("pnpm",["install","--lockfile-only","--no-frozen-lockfile","--config.minimumReleaseAge=0"],e),!0}catch(t){let r=t instanceof Error?t.message:String(t);return P.log.warn(`Lockfile regeneration failed: ${r}`),P.log.warn("Deploys using --frozen-lockfile may fail."),!1}}async function pn(e){if(!he("pnpm"))return P.log.warn("pnpm not found. Skipping dependency installation."),P.log.info("Install pnpm: https://pnpm.io/installation"),!1;let t=P.spinner();t.start("Installing dependencies (this may take a minute)...");try{return await ge("pnpm",["install","--config.minimumReleaseAge=0"],e),t.stop("Dependencies installed."),!0}catch(r){t.stop("Dependency installation failed.");let i=r instanceof Error?r.message:String(r);return P.log.warn(`pnpm install failed: ${i}`),P.log.warn("You can run it manually later."),!1}}async function dn(e){if(!he("pnpm"))return!1;let t=P.spinner();t.start("Generating baseline database migration...");try{return await ge("pnpm",["-F","@repo/database","generate"],e),t.stop("Baseline migration generated."),!0}catch(r){t.stop("Baseline migration generation failed.");let i=r instanceof Error?r.message:String(r);return P.log.warn(`Could not generate baseline migration: ${i}`),P.log.warn("Run 'pnpm -F @repo/database generate' before your first deploy."),!1}}async function un(e){try{return await cn(Ft(e,".git")),P.log.info("Git repository already exists, skipping init."),!0}catch{}if(!he("git"))return P.log.warn("git not found. Skipping repository initialization."),!1;let t=P.spinner();t.start("Initializing git repository...");try{return await ge("git",["init"],e),await ge("git",["add","-A"],e),await ge("git",["commit","--no-verify","-m","Initial commit from GenerateSaaS"],e),t.stop("Git repository initialized."),!0}catch{return t.stop("Git initialization failed."),P.log.warn("You can run git init manually later."),!1}}async function mn(e){if(!he("pnpm"))return!1;try{await cn(Ft(e,".git"))}catch{return!1}try{let t=JSON.parse(await Do(Ft(e,"package.json"),"utf-8")),r=!!t.devDependencies?.["simple-git-hooks"],i=!!t["simple-git-hooks"];if(!r||!i)return!1}catch{return!1}try{return await ge("pnpm",["exec","simple-git-hooks"],e),!0}catch{return P.log.warn("Could not install git hooks. Run 'pnpm exec simple-git-hooks' manually."),!1}}import*as Oe from"@clack/prompts";import j from"picocolors";function fn(e,t){t.dockerComposeGenerated&&!t.dockerAvailable&&Oe.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 o=e.dockerServices.map(s=>we[s].label).join(", ");r.push(`pnpm infra ${j.dim(`# ${o}`)}`)}if(r.push(`pnpm dev ${j.dim("# http://localhost:3000")}`),t.skippedCredentials.length>0&&(r.push(""),r.push(j.dim("Fill in remaining TODO values in .env"))),Oe.note(r.join(`
1129
+ `),j.yellow("Start Development")),t.dockerComposeGenerated){let o=[];o.push(`App ${j.cyan("http://localhost:3000")}`),e.architecture==="separate"&&o.push(`API ${j.cyan("http://localhost:3010")}`),e.dockerServices.includes("mailpit")&&o.push(`Mailpit ${j.cyan("http://localhost:8025")}`),e.dockerServices.includes("inngest")&&o.push(`Inngest ${j.cyan("http://localhost:8288")}`),Oe.note(o.join(`
1130
+ `),j.yellow("Dev Tools"))}let i=[],n=Co(e);n.length>0&&i.push(`Set in production: ${j.dim(n.join(", "))}`),i.push("pnpm db:push # Run database migrations"),i.push(No(e)),Oe.note(i.join(`
1131
+ `),j.yellow("Deployment"))}function Co(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 No(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 gn(e){let t={};if(e.name!==void 0){if(!st(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(!je.includes(e.frontend))throw new Error(`Invalid frontend "${e.frontend}". Valid values: ${je.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.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=Bt(e.docker,tt,"docker service")),e.aiTools!==void 0&&(t.aiTools=Bt(e.aiTools,rt,"AI tool")),e.socialProviders!==void 0&&(t.socialProviders=Bt(e.socialProviders,it,"social provider")),e.currency!==void 0){if(!ae.includes(e.currency))throw new Error(`Invalid currency "${e.currency}". Valid values: ${ae.join(", ")}`);t.defaultCurrency=e.currency}if(e.deploy!==void 0){if(!ce.includes(e.deploy))throw new Error(`Invalid deployment target "${e.deploy}". Valid values: ${ce.join(", ")}`);t.deploymentTarget=e.deploy}if(e.database!==void 0){if(!le.includes(e.database))throw new Error(`Invalid database provider "${e.database}". Valid values: ${le.join(", ")}`);t.databaseProvider=e.database}if(e.cache!==void 0){if(!pe.includes(e.cache))throw new Error(`Invalid cache provider "${e.cache}". Valid values: ${pe.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 i;try{i=new URL(r)}catch{throw new Error(`Invalid --base-url "${e.baseUrl}". Must be an absolute URL like https://example.com.`)}if(i.protocol!=="http:"&&i.protocol!=="https:")throw new Error(`Invalid --base-url "${e.baseUrl}". Must use http or https.`);t.baseUrl=`${i.protocol}//${i.host}`}return t}var ne={projectName:"my-saas",frontend:"nextjs",architecture:"fullstack",paymentProvider:"stripe",emailProvider:"smtp",multiTenancy:!1,billingScope:"user",blog:!0,docs:!1,revenueSharing:!1,credits:!0,dockerServices:["postgres","redis","inngest"],aiTools:[],socialProviders:[],defaultCurrency:"USD",deploymentTarget:"node",databaseProvider:"postgres",cacheProvider:"redis"};function hn(e){let t=e.projectName??ne.projectName,r=e.projectDir??`./${t}`,i=e.appName??ot(t),n=e.deploymentTarget??ne.deploymentTarget,o=B[n]?.edgeRuntime??!1,s=e.databaseProvider??(o?"neon":ne.databaseProvider),a=e.cacheProvider??(o?"upstash":ne.cacheProvider),d=e.emailProvider??(o?"resend":ne.emailProvider),y=e.dockerServices??(o?ne.dockerServices.filter(f=>f!=="postgres"&&f!=="redis"):ne.dockerServices),h={...ne,...e,projectName:t,appName:i,projectDir:r,deploymentTarget:n,databaseProvider:s,cacheProvider:a,emailProvider:d,dockerServices:y};h.paymentProvider==="none"&&(h.credits=!1);for(let f of $e){if(h.deploymentTarget!==f.target)continue;let S=h.databaseProvider===f.provider?"database":"cache";if(h.databaseProvider===f.provider||h.cacheProvider===f.provider)throw new Error(`Incompatible: --deploy ${f.target} + --${S} ${f.provider}. ${f.reason}`)}for(let f of Ue)if(h.architecture===f.architecture&&h.deploymentTarget===f.target)throw new Error(`Incompatible: --architecture ${f.architecture} + --deploy ${f.target}. ${f.reason}`);return h}function Bt(e,t,r){if(e.trim()==="")return[];let i=e.split(",").map(o=>o.trim()).filter(Boolean),n=i.filter(o=>!t.includes(o));if(n.length>0)throw new Error(`Invalid ${r}(s): ${n.join(", ")}. Valid values: ${t.join(", ")}`);return i}import Mo from"picocolors";var Fo="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";function Bo(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 yn(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 V("--frontend <type>","frontend framework").choices([...je])).addOption(new V("--architecture <type>","fullstack or separate").choices([...Ze])).addOption(new V("--payment <provider>","payment provider").choices([...Qe])).addOption(new V("--email <provider>","email provider").choices([...et])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").addOption(new V("--billing-scope <scope>","billing scope (requires --org)").choices([...nt])).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("--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 V("--currency <code>","default currency for billing").choices([...ae])).addOption(new V("--deploy <target>","deployment target").choices([...ce])).addOption(new V("--database <provider>","database provider").choices([...le])).addOption(new V("--cache <provider>","cache provider").choices([...pe])).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 V("--demo","first-party demo build: keep sample content, mark site non-indexable - requires CI API key").hideHelp()).addOption(new V("--no-db-migration","skip generating the baseline DB migration (internal: demos/CI/playground)").hideHelp()).action(async(t,r)=>{await Go(t?{...r,apiKey:t}:r)})}async function Go(e){let t=performance.now();Jt("1.11.1");let r,i;try{r=gn(e),i=Bo(e.templateVersion)}catch(u){E.cancel(I(u)),process.exit(1)}let n=E.spinner(),o;try{o=await be({apiKey:e.apiKey,prompt:!e.yes})}catch(u){E.cancel(I(u)),process.exit(1)}e.demo&&$t(o)!==Fo&&(E.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let s=X(o),a=async()=>{let u=await re(s),b=u.latest,H=i??b;if(i&&!u.versions.some(Y=>Y.version===H))throw new Error(`Template version "${i}" is not available.`);return{latestVersion:b,selectedVersion:H}};n.start("Verifying access...");let d,y;try{({latestVersion:d,selectedVersion:y}=await a()),n.stop("Access verified."),ue(o)}catch(u){if(n.stop("Access verification failed."),u instanceof R&&u.status===401){e.yes&&(E.cancel("Invalid API key. Cannot prompt in non-interactive mode."),process.exit(1)),E.log.warning("Invalid API key."),o=await Fe(),s=X(o),n.start("Verifying access...");try{({latestVersion:d,selectedVersion:y}=await a()),n.stop("Access verified."),ue(o)}catch(b){n.stop("Access verification failed."),E.cancel(b instanceof R&&b.status===401?"Invalid API key.":I(b)),process.exit(1)}}else E.cancel(I(u)),process.exit(1)}E.log.success(`Latest version: ${d}`),y!==d&&E.log.success(`Using template version: ${y}`);let h;e.yes?h=hn(r):h=await Xt(r);let f;n.start("Activating license...");try{let u=crypto.randomUUID(),b=()=>({frontend:h.frontend,version:y,installId:u,projectName:h.projectName,options:rr(h)}),H;try{H=await Tt(s,b())}catch(Y){let J=lt(Y);if(!J?.lastAllowedVersion)throw Y;n.stop("License activation failed."),e.yes&&(E.cancel(`${J.message} Re-run with --template-version ${J.lastAllowedVersion}.`),process.exit(1));let Ee=await E.confirm({message:`Your update window has ended. Continue with v${J.lastAllowedVersion} (the last version your license covers)?`});(E.isCancel(Ee)||!Ee)&&(E.cancel("Setup cancelled."),process.exit(0)),y=J.lastAllowedVersion,n.start(`Activating license for v${y}...`),H=await Tt(s,b())}f={token:H.token,keyHash:$t(o),installId:u},n.stop("License activated.")}catch(u){n.stop("License activation failed."),E.cancel(I(u)),process.exit(1)}let S=Vo(h.projectDir);if(Lo(S)&&$o(S).length>0)if(e.yes)E.log.info(`Directory ${S} is not empty. Merging (keeping existing files, overwriting conflicts).`);else{let b=await E.select({message:`Directory ${S} 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"}]});(E.isCancel(b)||b==="cancel")&&(E.cancel("Setup cancelled."),process.exit(0)),b==="overwrite"&&Uo(S,{recursive:!0,force:!0})}let k={...h,projectDir:S,version:y,...e.demo?{docs:!1}:{}};n.start("Downloading template...");try{await pt(s,y,S),n.stop("Template downloaded.")}catch(u){n.stop("Download failed."),E.cancel(I(u)),process.exit(1)}let ie;n.start("Generating project files...");try{if({dockerComposeGenerated:ie}=await ht(k),!e.demo){let u=await Re(S);await l(jo(S,at),JSON.stringify(u,null," ")+`
1132
+ `),await nn(S,S)}await an(S,k.aiTools),await tn(k,f),n.stop("Project files generated.")}catch(u){n.stop("Generation failed."),E.cancel(I(u)),process.exit(1)}await ln(S);let x=await pn(S);x&&k.demo!==!0&&e.dbMigration!==!1&&await dn(S),await un(S),x&&await mn(S);let $=he("docker"),Z=bt(k).map(u=>u.key).filter(u=>!k.credentials?.[u]);fn(k,{pnpmInstalled:x,dockerComposeGenerated:ie,dockerAvailable:$,skippedCredentials:Z}),Wt(),E.log.info(Mo.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as Sn}from"fs";import{readFile as En}from"fs/promises";import{join as ze,resolve as Jo}from"path";import*as _ from"@clack/prompts";import xe from"picocolors";import{mkdtemp as Ko,rm as zo}from"fs/promises";import{tmpdir as Ho}from"os";import{join as Yo}from"path";async function Gt(e,t,r,i){let n=await Ko(Yo(Ho(),"generatesaas-stage-"));try{await pt(e,t,n),await ht({...r,projectDir:n}),await Ut(n,i)}finally{await zo(n,{recursive:!0,force:!0})}}function vn(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 vt(e,t){let r=vn(e),i=vn(t);if(!r||!i)return 0;for(let n=0;n<3;n++)if(r[n]!==i[n])return r[n]-i[n];return 0}function wn(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=Jo(t.cwd??process.cwd()),i=ze(r,Q),n;try{n=JSON.parse(await En(i,"utf-8"))}catch{_.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let o;try{o=await be()}catch(d){_.cancel(I(d)),process.exit(1)}let s=X(o),a=_.spinner();try{a.start("Verifying access...");let d;try{d=await re(s)}catch(u){throw u instanceof R&&u.status===401?new Error("Your saved API key was rejected. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):u}a.stop("Access verified."),ue(o),a.start("Fetching latest skill files...");let y=await tr(s,d.latest);await Mt(r,y.skillMd,y.scripts,n.aiTools);let h=Vt(n.aiTools);if(a.stop("Skills updated."),_.log.success(`Skill files installed to ${xe.cyan(h.length.toString())} locations.`),n.version===d.latest){_.log.info(`Already on the latest version (${n.version}).`);return}if(n.licenseToken)try{let u=await nr(s,{currentToken:n.licenseToken,newVersion:d.latest});n.licenseToken=u.token,await l(i,JSON.stringify(n,null," ")+`
1133
+ `),_.log.success("License refreshed.")}catch(u){let b=lt(u);b&&(_.cancel(b.message),process.exit(1)),_.log.warn("License refresh skipped.")}let f=en(n,r),S=ze(r,Qt);a.start(`Staging v${d.latest} (shaped for your config)...`),await Gt(s,d.latest,f,S),a.stop("Template staged.");let{text:k,title:ie}=await Wo(s,d,n.version);k&&_.note(k,ie);let x=ze(r,at),$=ze(r,ct),Se=!Sn($),Z=!Sn(x);if(Se){if(a.start("Building baseline template (one-time migration)..."),await Gt(s,n.version,f,$),Z){let u=await Re($);await l(x,JSON.stringify(u,null," ")+`
1134
+ `)}if(a.stop("Baseline template stored."),!Z){let u=await qo(x,$);u>0&&_.log.warn(`Rebuilt baseline differs from the original for ${u} 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(Z){a.start("Computing baseline template hashes...");let u=await Re($);await l(x,JSON.stringify(u,null," ")+`
1135
+ `),a.stop("Baseline hashes computed.")}if(await l(ze(r,er),JSON.stringify({currentVersion:n.version,targetVersion:d.latest,changelog:k,stagedAt:new Date().toISOString()},null," ")+`
1136
+ `),_.log.info(`Update staged: ${xe.cyan(n.version)} \u2192 ${xe.cyan(d.latest)}`),n.aiTools&&n.aiTools.length>0){let u=n.aiTools[0],b=Le[u].label;_.log.info(`Open your project in ${xe.cyan(b)} and ask: ${xe.cyan("'update my GenerateSaaS project'")}`)}else _.log.info(`Ask your AI coding assistant to ${xe.cyan("'update my GenerateSaaS project'")}.`)}catch(d){a.stop("Failed."),_.cancel(`Update failed: ${I(d)}`),process.exit(1)}})}async function Wo(e,t,r){let i=t.latest,n=t.versions.filter(s=>vt(s.version,r)>0&&vt(s.version,i)<=0).sort((s,a)=>vt(s.version,a.version));if(n.length<=1)return{text:await At(e,i),title:`Changelog v${i}`};let o=[];for(let s of n){let a=null;try{a=await At(e,s.version)}catch{a=null}let d=s.date?` (${s.date.slice(0,10)})`:"",y=s.breaking?" [BREAKING]":"";o.push(`# v${s.version}${d}${y}
1137
+
1138
+ ${a??"_No changelog available for this release._"}`)}return{text:o.join(`
1139
+
1140
+ `),title:`Changelog v${r} \u2192 v${i}`}}async function qo(e,t){let r=JSON.parse(await En(e,"utf-8")),i=await Re(t),n=0;for(let[o,s]of Object.entries(r))Ae(o)||i[o]!==s&&n++;for(let o of Object.keys(i))o in r||n++;return n}import*as L from"@clack/prompts";import M from"picocolors";import{readFile as Xo}from"fs/promises";import{join as Zo,resolve as Qo}from"path";function bn(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=Qo(t.cwd??process.cwd()),i=Zo(r,Q),n;try{n=JSON.parse(await Xo(i,"utf-8"))}catch{L.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let o=[`Version: ${M.cyan(n.version)}`,`Frontend: ${M.cyan(n.frontend)}`,n.deploymentTarget?`Deploy target: ${M.cyan(n.deploymentTarget)}`:null,n.databaseProvider?`Database: ${M.cyan(n.databaseProvider)}`:null,n.cacheProvider?`Cache: ${M.cyan(n.cacheProvider)}`:null,n.aiTools&&n.aiTools.length>0?`AI tools: ${M.cyan(n.aiTools.join(", "))}`:null].filter(Boolean).join(`
1141
+ `);L.note(o,M.bold("Project Status"));let s=L.spinner();s.start("Checking for updates...");try{let a=await be(),d=X(a),h=(await re(d)).latest;n.version===h?(s.stop("Up to date."),L.log.success(`Already on the latest version (${M.green(h)})`)):(s.stop("Update available."),L.log.warning(`Update available: ${M.yellow(n.version)} \u2192 ${M.green(h)}`),L.log.info(`Open this project in your AI coding agent and ask it to ${M.cyan("update my GenerateSaaS project")} - it fetches and applies the update for you.`))}catch(a){s.stop("Check failed."),a instanceof R&&a.status===401?L.log.warning("Invalid API key. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):L.log.warning(`Could not check for updates: ${I(a)}`)}})}import{readFile as es}from"fs/promises";import*as T from"@clack/prompts";import A from"picocolors";function ts(){return process.env.GENERATESAAS_API_KEY??Me()}function rs(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 ns(e){return{verdict:e.verdict??"unknown",ejectedAt:e.ejectedAt??null}}function is(e){switch(e.verdict){case"licensed":return T.log.success(`${A.green("LICENSED")} - resolves to an account with an active${e.plan?` ${e.plan}`:""} license.`),!0;case"ejected":return T.log.success(`${A.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 T.log.error(`${A.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 T.log.error(`${A.red("LEAKED TOKEN")} - this license belongs to a different deployment${e.mismatchDomain?` (${A.cyan(e.mismatchDomain)})`:""}, not this site. The token was copied from a licensed project.`),!1;case"no_license_history":return T.log.error(`${A.red("NO LICENSE HISTORY")} - no license has ever been associated with this site. If it runs GenerateSaaS, treat it as unlicensed.`),!1;default:return T.log.warn(`${A.yellow("UNKNOWN")} - could not cross-reference the records right now. Try again shortly.`),!1}}async function An(e,t){let r=process.env.GENERATESAAS_API_URL??Ve,i=ts();e.start("Cross-referencing license records...");try{let n=i?rs(await ir(r,i,{lkh:t.lkh,nid:t.nid,domain:t.domain})):ns(await kt(r,{token:t.token,domain:t.domain}));return e.stop(`${A.green("Checked")} - records cross-referenced`),is(n)}catch(n){return e.stop(`${A.yellow("Skipped")} - ${I(n)}`),null}}function os(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 Kt(e){return typeof e!="number"?"unknown":new Date(e*1e3).toISOString().split("T")[0]}function Tn(e){T.note([`License ID: ${A.cyan(String(e.lid??"unknown"))}`,`Version: ${A.cyan(String(e.ver??"unknown"))}`,`Init version: ${String(e.iver??"unknown")}`,`Frontend: ${String(e.fe??"unknown")}`,`Created: ${Kt(e.pat)}`,`Last updated: ${Kt(e.uat)}`,`Expires: ${Kt(e.exp)}`,`Install ID: ${String(e.nid??"unknown")}`].join(`
1142
+ `),A.yellow("License Details"))}function ss(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 kn(e){let t=T.spinner(),r=null,i="no candidates";for(let s of ss(e)){t.start(`Checking ${s}...`);try{let a=await fetch(s);if(!a.ok){i=`${s} returned ${a.status}`,t.stop(`${A.yellow("Not here")} - ${i}`);continue}let d=(await a.text()).trim();if(!d||d.split(".").length!==3){i=`${s} did not return a JWT`,t.stop(`${A.yellow("Not here")} - ${i}`);continue}r=d,t.stop(`${A.green("Found")} - license endpoint responded`);break}catch(a){i=`${s}: ${I(a)}`,t.stop(`${A.yellow("Unreachable")} - ${i}`)}}if(r===null){T.log.warn(`No license endpoint found (last: ${i}). The site may be ejected, not a GenerateSaaS app, or serving its API elsewhere.`);let s=In(e);return s?await An(t,{domain:s})??!1:!1}let n;try{n=os(r)}catch{return T.log.error("Could not decode JWT payload."),!1}t.start("Verifying signature...");try{let s=process.env.GENERATESAAS_API_URL??Ve,a=await kt(s,{token:r});if(a.valid)t.stop(`${A.green("Valid")} - signature verified`);else return t.stop(`${A.red("Invalid")} - ${a.reason}`),!1}catch{return t.stop(`${A.yellow("Skipped")} - could not reach verification service`),T.log.warn("Signature not verified. Displaying unverified claims:"),Tn(n),!1}return Tn(n),await An(t,{token:r,lkh:typeof n.lkh=="string"?n.lkh:void 0,nid:typeof n.nid=="string"?n.nid:void 0,domain:In(e)})??!0}function In(e){try{return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`).hostname}catch{return}}function Pn(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&&(T.cancel("Provide a URL or --file <path>."),process.exit(1)),r.file){let n=(await es(r.file,"utf-8")).split(`
1143
+ `).map(s=>s.trim()).filter(s=>s&&!s.startsWith("#"));n.length===0&&(T.cancel("No URLs found in file."),process.exit(1));let o=0;for(let s of n)await kn(s)&&o++,T.log.info("");T.log.success(`${o}/${n.length} sites verified.`)}else await kn(t)||process.exit(1)})}import{existsSync as as,rmSync as cs}from"fs";import*as F from"@clack/prompts";function _n(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){as(q)?(cs(q),F.log.success("API key removed.")):F.log.info("No API key configured.");return}let r=Me();r?F.log.info(`Current API key: ****${r.slice(-4)}`):F.log.info("No API key configured.");let i=await Fe(),n=X(i),o=F.spinner();o.start("Verifying API key...");try{await re(n),o.stop("API key verified."),ue(i),F.log.success("API key saved.")}catch(s){o.stop("Verification failed."),s instanceof R&&s.status===401?F.cancel("Invalid API key."):F.cancel(I(s)),process.exit(1)}})}import{existsSync as St,rmSync as ls,readFileSync as Ht,writeFileSync as Rn}from"fs";import{join as ye}from"path";import*as O from"@clack/prompts";var ps=["packages/api/src/functions/maintenance/license-heartbeat.ts","packages/api/src/lib/manifest.ts","packages/api/src/routes/internal/license.ts"],ds=[{file:"packages/api/src/routes/inngest.ts",removals:[`import { licenseHeartbeatFunction } from "../functions/maintenance/license-heartbeat";
1140
1144
  `,` licenseHeartbeatFunction,
1141
1145
  `]},{file:"packages/api/src/routes/internal/index.ts",removals:[`import licenseRoutes from "./license";
1142
1146
  `,` .route("/license", licenseRoutes)
1143
- `]}];function ss(e){return(e&&e.length>0?e.map(r=>Ke[r]):Object.values(Ke)).map(r=>ve(r,Nt))}function Ft(e){return vt(e)?(ns(e,{recursive:!0}),!0):!1}function as(e,t){if(!vt(e))return!1;let r=Bt(e,"utf-8"),n=r;for(let i of t)n=n.replace(i,"");return n===r?!1:(Tn(e,n,"utf-8"),!0)}function cs(e){let t=ve(e,".gitignore");if(!vt(t))return!1;let r=Bt(t,"utf-8"),n=r.split(`
1144
- `).filter(i=>!i.includes(".generatesaas")).join(`
1145
- `);return n===r?!1:(Tn(t,n,"utf-8"),!0)}function kn(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=ve(t,Q),n;try{n=JSON.parse(Bt(r,"utf-8"))}catch{x.cancel("No GenerateSaaS project found in this directory."),process.exit(1)}let i=await x.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(x.isCancel(i)&&(x.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)}),x.log.info("Recorded the opt-out with generatesaas.com (final event - nothing is sent after this).")}catch{x.log.warn("Could not reach generatesaas.com to record the opt-out. Ejecting anyway.")}let o=[],s=[];for(let a of ss(n.aiTools))Ft(ve(t,a))&&o.push(a);for(let a of is)Ft(ve(t,a))&&o.push(a);Ft(ve(t,U))&&o.push(U+"/");for(let a of os){let d=ve(t,a.file);as(d,a.removals)?s.push(a.file):vt(d)&&x.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}cs(t)&&s.push(".gitignore");for(let a of o)x.log.info(`Deleted ${a}`);for(let a of s)x.log.info(`Modified ${a}`);x.log.success("Ejected successfully. This project is now fully standalone.")})}var Se=new ls().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("1.10.0").addHelpText("after",`
1147
+ `]}];function us(e){return(e&&e.length>0?e.map(r=>Ke[r]):Object.values(Ke)).map(r=>ye(r,jt))}function zt(e){return St(e)?(ls(e,{recursive:!0}),!0):!1}function ms(e,t){if(!St(e))return!1;let r=Ht(e,"utf-8"),i=r;for(let n of t)i=i.replace(n,"");return i===r?!1:(Rn(e,i,"utf-8"),!0)}function fs(e){let t=ye(e,".gitignore");if(!St(t))return!1;let r=Ht(t,"utf-8"),i=r.split(`
1148
+ `).filter(n=>!n.includes(".generatesaas")).join(`
1149
+ `);return i===r?!1:(Rn(t,i,"utf-8"),!0)}function On(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=ye(t,Q),i;try{i=JSON.parse(Ht(r,"utf-8"))}catch{O.cancel("No GenerateSaaS project found in this directory."),process.exit(1)}let n=await O.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(O.isCancel(n)&&(O.cancel("Eject cancelled."),process.exit(0)),i.licenseToken)try{await fetch("https://generatesaas.com/api/v1/heartbeat",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${i.licenseToken}`},body:JSON.stringify({event:"eject",version:i.version,frontend:i.frontend}),signal:AbortSignal.timeout(5e3)}),O.log.info("Recorded the opt-out with generatesaas.com (final event - nothing is sent after this).")}catch{O.log.warn("Could not reach generatesaas.com to record the opt-out. Ejecting anyway.")}let o=[],s=[];for(let a of us(i.aiTools))zt(ye(t,a))&&o.push(a);for(let a of ps)zt(ye(t,a))&&o.push(a);zt(ye(t,U))&&o.push(U+"/");for(let a of ds){let d=ye(t,a.file);ms(d,a.removals)?s.push(a.file):St(d)&&O.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}fs(t)&&s.push(".gitignore");for(let a of o)O.log.info(`Deleted ${a}`);for(let a of s)O.log.info(`Modified ${a}`);O.log.success("Ejected successfully. This project is now fully standalone.")})}var ve=new gs().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("1.11.1").addHelpText("after",`
1146
1150
  Examples:
1147
1151
  $ generatesaas init Interactive setup
1148
1152
  $ generatesaas init -n my-app -y Quick setup with defaults
1149
1153
  $ generatesaas status Check for updates
1150
1154
  $ generatesaas auth Set or update API key
1151
- `);fn(Se);hn(Se);yn(Se);bn(Se);An(Se);kn(Se);Se.parseAsync().catch(e=>{In.cancel("An unexpected error occurred."),console.error(e),process.exit(1)});
1155
+ `);yn(ve);wn(ve);bn(ve);Pn(ve);_n(ve);On(ve);ve.parseAsync().catch(e=>{xn.cancel("An unexpected error occurred."),console.error(e),process.exit(1)});
@@ -105,14 +105,21 @@ Creates:
105
105
  file's project-relative path with `.diff` appended (e.g.
106
106
  `references/diffs/packages/config/src/index.ts.diff`). Diffs cover `modified` +
107
107
  `added` files; `unmodified`, `deleted`, and `removed` files have none.
108
- - `references/update-manifest.json` - lists of added, modified, and removed files
108
+ - `references/update-manifest.json` - lists of added, modified, and removed files,
109
+ plus `renamed` (upstream renames detected by identical content: `[{from, to}]`)
110
+ and `sensitive` (database schema and `.env.example` changes that carry
111
+ follow-up work: migrations, new env vars)
109
112
 
110
113
  ### Step 2: Present Changelog
111
114
 
112
- Read `references/changelog.md`. It may be one of two shapes:
115
+ Read `references/changelog.md`. When the update spans multiple releases, it contains one `# vX.Y.Z` section per release, oldest first - read ALL of them, not just the last; a breaking change can sit in any intermediate release. A `[BREAKING]` marker on a section header is authoritative (it comes from the version index: a major bump or release notes flagged BREAKING) - always call those releases out explicitly.
116
+
117
+ Each release's notes may be one of two shapes:
113
118
 
114
119
  - **Curated** - contains explicit `## Breaking`, `## Migration`, `## Features`, `## Fixes` (or similar) sections. Use those sections verbatim; they are authoritative.
115
- - **Raw** - auto-generated release notes (a flat list of PR titles, no sections). This is the common case. You can group PR titles into features/fixes by reading them, but you **cannot** reliably infer breaking changes or migration steps from PR titles.
120
+ - **Raw** - an auto-generated flat list of change titles (one line per commit or PR, no sections). This is the common case. You can group the titles into features/fixes by reading them, but you **cannot** reliably infer breaking changes or migration steps from titles alone.
121
+
122
+ Also read `sensitive` in `references/update-manifest.json`: database schema changes mean a migration is likely required, and `.env.example` changes mean new or changed environment variables. Mention both in the summary and again in the final post-update steps.
116
123
 
117
124
  Present a clear, organized summary:
118
125
 
@@ -276,6 +283,12 @@ If `.generatesaas/template/<path>` does not exist (backward compat), fall back t
276
283
 
277
284
  #### For Removed Files
278
285
 
286
+ **Check for renames first.** Look up the file in `renamed` in `references/update-manifest.json`. If it is the `from` of a rename, the same upstream content now lives at the `to` path (auto-created in Step 6). Present it as a rename, not an unrelated removal:
287
+
288
+ - If the user never modified the old file: delete it and note the rename - nothing is lost.
289
+ - If the user **modified** the old file: their customizations exist nowhere in the new version. Port them to the `to` path (show the proposed result) before deleting the old file. Never let a rename silently drop customizations.
290
+ - Imports of the old path elsewhere in the project must be updated to the new path - run the import search below for the old path either way.
291
+
279
292
  Before presenting each removed file, **search the entire project** for imports of it. Use grep to find `import ... from` or `require(...)` statements referencing the file's path (check relative paths, aliases, and package paths). This is critical - the classification script only scans files in the update, not user-created files that may also depend on the removed file.
280
293
 
281
294
  ```
@@ -377,6 +390,13 @@ After all files are processed:
377
390
 
378
391
  If no files were held back, skip creating this file.
379
392
 
393
+ > Note: the completion script runs a safety net that auto-holds-back upstream
394
+ > changes which were demonstrably never applied (the file was not touched during
395
+ > the entire update). Do NOT rely on it - it cannot detect a file you edited
396
+ > during a merge or that the user customized between updates - always write
397
+ > held-back.json yourself. Treat its warnings as a signal that your bookkeeping
398
+ > missed something.
399
+
380
400
  2. **Run the completion script:**
381
401
 
382
402
  ```bash
@@ -39,14 +39,17 @@ function hashFile(filePath) {
39
39
 
40
40
  // ── File Walking ──
41
41
 
42
- // Broader than strictly needed, but aligned with the CLI's exclusions.ts to ensure
43
- // hash consistency across update cycles. The staging tarball is pre-filtered by
44
- // exclusions.ts, so most of these only matter when walking the live project root.
42
+ // MUST stay in sync with the CLI's template/exclusions.ts: exactly
43
+ // EXCLUDED_NAMES + SNAPSHOT_EXCLUDED_NAMES plus the two project-root extras
44
+ // ("data", INTERNAL_DIR). A drift silently corrupts classification - a path the
45
+ // CLI stages but this walker skips never gets diffed or hashed. The
46
+ // exclusions-parity test in the CLI repo asserts this set equality.
45
47
  const WALK_EXCLUSIONS = new Set([
46
48
  ".git", "node_modules", ".pnpm-store", ".env", ".env.test",
47
49
  ".turbo", ".nuxt", ".output", ".data", "dist", "data",
48
50
  ".next", ".svelte-kit", ".wrangler",
49
51
  ".devcontainer", "playwright-report", "test-results",
52
+ "pnpm-lock.yaml",
50
53
  INTERNAL_DIR,
51
54
  ]);
52
55
 
@@ -86,6 +89,8 @@ module.exports = {
86
89
  ensureDir,
87
90
  hashFile,
88
91
  walkDir,
92
+ shouldExcludeWalk,
93
+ WALK_EXCLUSIONS,
89
94
  INTERNAL_DIR,
90
95
  MANIFEST_FILE,
91
96
  HASHES_FILE,
@@ -15,43 +15,43 @@ const fs = require("node:fs");
15
15
  const path = require("node:path");
16
16
  const { findProjectRoot, hashFile, walkDir, ensureDir, MANIFEST_FILE, HASHES_FILE, TEMPLATE_HASHES_FILE, TEMPLATE_DIR, STAGING_DIR, STAGING_META_FILE, INTERNAL_DIR } = require("./_helpers.js");
17
17
 
18
- const HASH_EXCLUSIONS = new Set([
19
- ".git", "node_modules", ".pnpm-store", ".env", ".env.test",
20
- ".turbo", ".nuxt", ".output", ".data", "dist", "data",
21
- ".next", ".svelte-kit", ".wrangler",
22
- ".devcontainer", "playwright-report", "test-results",
23
- INTERNAL_DIR,
24
- ]);
25
-
26
- /** Check if a relative path should be excluded from hashing. */
27
- function shouldExcludeFromHash(relativePath) {
28
- const parts = relativePath.split("/");
29
- for (const part of parts) {
30
- if (HASH_EXCLUSIONS.has(part)) return true;
31
- if (part.startsWith(".env") && !part.includes("example")) return true;
32
- }
33
- return false;
34
- }
35
-
36
- /** Recursively collect all file paths for project hashing. */
37
- function collectProjectFiles(dir, baseDir) {
38
- const files = [];
39
- const entries = fs.readdirSync(dir, { withFileTypes: true });
40
-
41
- for (const entry of entries) {
42
- const fullPath = path.join(dir, entry.name);
43
- const rel = path.relative(baseDir, fullPath);
44
-
45
- if (shouldExcludeFromHash(rel)) continue;
46
-
47
- if (entry.isDirectory()) {
48
- files.push(...collectProjectFiles(fullPath, baseDir));
49
- } else if (entry.isFile()) {
50
- files.push(fullPath);
51
- }
18
+ /**
19
+ * Safety net for the held-back list. An upstream change was demonstrably never
20
+ * applied when the file (a) really changed upstream (old template hash differs
21
+ * from staging), (b) still exists on disk, and (c) was not touched during the
22
+ * entire update (its disk hash still equals the previous hashes.json entry).
23
+ * Such files MUST be held back - otherwise the template baseline advances past
24
+ * a change the project never received and the next update's diff silently
25
+ * drops it. Returns the detected paths (sorted) and adds them to `heldBack`.
26
+ *
27
+ * This is a net, not a replacement for the AI-written held-back.json: a file
28
+ * the user customized since the last update and then kept during this one
29
+ * cannot be distinguished from a merged file by hashes alone.
30
+ */
31
+ function applyHeldBackSafetyNet(root, stagingDir, stagingFiles, heldBack) {
32
+ const templateHashesPath = path.join(root, TEMPLATE_HASHES_FILE);
33
+ const hashesPath = path.join(root, HASHES_FILE);
34
+ const oldTemplateHashes = fs.existsSync(templateHashesPath)
35
+ ? JSON.parse(fs.readFileSync(templateHashesPath, "utf-8"))
36
+ : {};
37
+ const prevProjectHashes = fs.existsSync(hashesPath)
38
+ ? JSON.parse(fs.readFileSync(hashesPath, "utf-8"))
39
+ : {};
40
+
41
+ const netted = [];
42
+ for (const rel of stagingFiles) {
43
+ if (heldBack.has(rel)) continue;
44
+ const oldHash = oldTemplateHashes[rel];
45
+ const prevHash = prevProjectHashes[rel];
46
+ if (!oldHash || !prevHash) continue;
47
+ const diskPath = path.join(root, rel);
48
+ if (!fs.existsSync(diskPath)) continue;
49
+ if (hashFile(path.join(stagingDir, rel)) === oldHash) continue;
50
+ if (hashFile(diskPath) !== prevHash) continue;
51
+ heldBack.add(rel);
52
+ netted.push(rel);
52
53
  }
53
-
54
- return files;
54
+ return netted.sort();
55
55
  }
56
56
 
57
57
  /**
@@ -133,6 +133,15 @@ function main() {
133
133
  console.log("Updating template directory from staging...");
134
134
  const stagingFiles = new Set(walkDir(stagingDir, stagingDir));
135
135
 
136
+ // Safety net: auto-hold-back upstream changes that were demonstrably
137
+ // never applied but are missing from held-back.json.
138
+ const netted = applyHeldBackSafetyNet(root, stagingDir, stagingFiles, heldBack);
139
+ if (netted.length > 0) {
140
+ console.warn(`Safety net: ${netted.length} upstream change(s) were never applied and were missing from held-back.json.`);
141
+ console.warn("Holding them back so the next update re-surfaces them:");
142
+ for (const rel of netted) console.warn(` - ${rel}`);
143
+ }
144
+
136
145
  // Validate held-back entries (non-fatal). A held-back file is supposed to be
137
146
  // a real upstream change the user chose to skip - preserving its old template
138
147
  // version so the next update re-surfaces the missed change. Warn on entries
@@ -219,11 +228,10 @@ function main() {
219
228
 
220
229
  // Re-hash all project files
221
230
  console.log("Hashing project files...");
222
- const files = collectProjectFiles(root, root);
231
+ const files = walkDir(root, root);
223
232
  const fileHashes = {};
224
- for (const file of files.sort()) {
225
- const rel = path.relative(root, file);
226
- fileHashes[rel] = hashFile(file);
233
+ for (const rel of files.sort()) {
234
+ fileHashes[rel] = hashFile(path.join(root, rel));
227
235
  }
228
236
 
229
237
  // Update version in manifest
@@ -9,7 +9,15 @@
9
9
  const fs = require("node:fs");
10
10
  const path = require("node:path");
11
11
  const { execFileSync } = require("node:child_process");
12
- const { findProjectRoot, ensureDir, hashFile, walkDir, TEMPLATE_HASHES_FILE, TEMPLATE_DIR, STAGING_DIR, STAGING_META_FILE } = require("./_helpers.js");
12
+ const { findProjectRoot, ensureDir, hashFile, walkDir, shouldExcludeWalk, TEMPLATE_HASHES_FILE, TEMPLATE_DIR, STAGING_DIR, STAGING_META_FILE } = require("./_helpers.js");
13
+
14
+ // Paths whose upstream changes always carry follow-up work for the user:
15
+ // database schema changes may need a migration, .env.example changes may need
16
+ // new environment variables. Surfaced as `sensitive` in update-manifest.json.
17
+ const SENSITIVE_PATTERNS = [
18
+ /^packages\/database\/src\/db\//,
19
+ /(^|\/)\.env\.example$/,
20
+ ];
13
21
 
14
22
  function main() {
15
23
  const root = findProjectRoot();
@@ -36,11 +44,17 @@ function main() {
36
44
 
37
45
  console.log(`Preparing update: ${currentVersion} → ${targetVersion}`);
38
46
 
39
- // Read old template hashes
47
+ // Read old template hashes. Entries that are no longer part of the template
48
+ // universe (e.g. pnpm-lock.yaml after an exclusions update) are dropped so
49
+ // they don't surface as phantom "removed upstream" files.
40
50
  const templateHashesPath = path.join(root, TEMPLATE_HASHES_FILE);
41
- const oldHashes = fs.existsSync(templateHashesPath)
51
+ const oldHashesRaw = fs.existsSync(templateHashesPath)
42
52
  ? JSON.parse(fs.readFileSync(templateHashesPath, "utf-8"))
43
53
  : {};
54
+ const oldHashes = {};
55
+ for (const [filePath, hash] of Object.entries(oldHashesRaw)) {
56
+ if (!shouldExcludeWalk(filePath)) oldHashes[filePath] = hash;
57
+ }
44
58
 
45
59
  // Walk staging dir and compute new template hashes
46
60
  const stagingFiles = walkDir(stagingDir, stagingDir);
@@ -74,7 +88,25 @@ function main() {
74
88
  modified.sort();
75
89
  removed.sort();
76
90
 
91
+ // Detect upstream renames: a removed file whose baseline content reappears
92
+ // verbatim at exactly one added path (and vice versa). Surfaced so user
93
+ // customizations on the old path can be ported before it is deleted -
94
+ // otherwise a rename reads as an unrelated add + remove and customizations
95
+ // on the old path are silently lost.
96
+ const renamed = detectRenames(oldHashes, newHashes, added, removed);
97
+
98
+ // Flag changes that always carry follow-up work (migrations, new env vars).
99
+ const sensitive = [...modified, ...added]
100
+ .filter((filePath) => SENSITIVE_PATTERNS.some((pattern) => pattern.test(filePath)))
101
+ .sort();
102
+
77
103
  console.log(` Added: ${added.length}, Modified: ${modified.length}, Removed: ${removed.length}`);
104
+ if (renamed.length > 0) {
105
+ console.log(` Renamed: ${renamed.length} (see update-manifest.json - port customizations before deleting the old paths)`);
106
+ }
107
+ if (sensitive.length > 0) {
108
+ console.log(` Sensitive (schema / env example): ${sensitive.length}`);
109
+ }
78
110
 
79
111
  // Determine output directory
80
112
  const scriptDir = __dirname;
@@ -124,13 +156,15 @@ function main() {
124
156
  }
125
157
  console.log(`Written: ${diffCount} diff files`);
126
158
 
127
- // Write update manifest (same format as before)
159
+ // Write update manifest (same format as before, plus renamed + sensitive)
128
160
  const updateManifest = {
129
161
  currentVersion,
130
162
  targetVersion,
131
163
  added,
132
164
  modified,
133
165
  removed,
166
+ renamed,
167
+ sensitive,
134
168
  };
135
169
  fs.writeFileSync(
136
170
  path.join(refsDir, "update-manifest.json"),
@@ -142,6 +176,35 @@ function main() {
142
176
  console.log("\nNext: Review references/changelog.md, then run classify-files.js");
143
177
  }
144
178
 
179
+ /**
180
+ * Detect upstream renames by exact content hash: a removed path whose baseline
181
+ * hash matches exactly one added path's staging hash (1:1 both ways - ambiguous
182
+ * matches are skipped). Returns [{ from, to }] sorted by old path.
183
+ */
184
+ function detectRenames(oldHashes, newHashes, added, removed) {
185
+ if (added.length === 0 || removed.length === 0) return [];
186
+
187
+ const addedByHash = new Map();
188
+ for (const filePath of added) {
189
+ const hash = newHashes[filePath];
190
+ addedByHash.set(hash, (addedByHash.get(hash) || []).concat(filePath));
191
+ }
192
+ const removedByHash = new Map();
193
+ for (const filePath of removed) {
194
+ const hash = oldHashes[filePath];
195
+ removedByHash.set(hash, (removedByHash.get(hash) || []).concat(filePath));
196
+ }
197
+
198
+ const renamed = [];
199
+ for (const [hash, fromPaths] of removedByHash) {
200
+ const toPaths = addedByHash.get(hash);
201
+ if (fromPaths.length === 1 && toPaths && toPaths.length === 1) {
202
+ renamed.push({ from: fromPaths[0], to: toPaths[0] });
203
+ }
204
+ }
205
+ return renamed.sort((a, b) => a.from.localeCompare(b.from));
206
+ }
207
+
145
208
  /** Generate a unified diff between two files. Returns null if diff is unavailable. */
146
209
  function generateDiff(userFile, stagingFile, relativePath) {
147
210
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "generatesaas",
3
- "version": "1.10.0",
3
+ "version": "1.11.1",
4
4
  "type": "module",
5
5
  "description": "CLI for scaffolding and managing GenerateSaaS projects",
6
6
  "bin": {