generatesaas 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +80 -80
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
- import{Command as ts}from"commander";import*as Tn from"@clack/prompts";import{existsSync as ko,readdirSync as _o,rmSync as Ro}from"fs";import{join as Oo,resolve as xo}from"path";import{Option as j}from"commander";import*as E from"@clack/prompts";import*as ze from"@clack/prompts";import St from"picocolors";function Gt(e){let t=e?` GenerateSaaS v${e} `:" GenerateSaaS ";ze.intro(St.bgYellow(St.black(t)))}function Kt(){ze.outro(St.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)"}},He={stripe:{label:"Stripe"},polar:{label:"Polar"},none:{label:"None",hint:"disable payments"}},Ye={smtp:{label:"SMTP",hint:"Mailpit for local dev"},ses:{label:"Amazon SES"},resend:{label:"Resend"}},Je={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 F={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}},D={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"}]}},B={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)."}],zt=["Local file storage (sharp, geoip-lite)","SMTP email (use Resend or SES instead)","Content API git integration"];function qe(e){let t=D[e.databaseProvider].managed,r=B[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 Ue=["nextjs","nuxt"],We=["fullstack","separate"],Xe=["stripe","polar","none"],Ze=["smtp","ses","resend"],Qe=["postgres","redis","inngest","mailpit"],et=["claude-code","cursor","codex","gemini-cli","windsurf"],tt=["user","organization"],ce=["USD","EUR","GBP","CAD","AUD","BRL","JPY"],le=["node","vercel"],pe=["postgres","neon","supabase"],de=["redis","upstash"],rt=["google","github","facebook","discord","x"];function nt(e){return e.split("-").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function it(e){return/^[a-z][a-z0-9-]*$/.test(e)}function I(e){return e instanceof Error?e.message:String(e)}function w(e){u.isCancel(e)&&(u.cancel("Setup cancelled."),process.exit(0))}function Et(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 Ht(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(!it(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 u.text({message:"App name:",initialValue:nt(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 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:F[p].label,hint:F[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=We.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 ${F[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(O=>O.target===s&&O.provider===v));if(c.length===1){let v=c[0];return u.log.info(`Auto-selected ${D[v].label} (only compatible option for ${F[s].label}).`),v}let p=await u.select({message:"Database provider:",options:c.map(v=>({value:v,label:D[v].label,hint:D[v].hint}))});return w(p),p})(),S=e?.cacheProvider??await(async()=>{t=!0;let c=de.filter(v=>!Le.some(O=>O.target===s&&O.provider===v));if(c.length===1){let v=c[0];return u.log.info(`Auto-selected ${B[v].label} (only compatible option for ${F[s].label}).`),v}let p=await u.select({message:"Cache provider:",options:c.map(v=>({value:v,label:B[v].label,hint:B[v].hint}))});return w(p),p})();if(F[s]?.edgeRuntime){let c=zt.map(p=>` - ${p}`).join(`
3
- `);u.note(c,"Unavailable on edge runtime")}u.log.info(f.bold("Features"));let b=e?.paymentProvider??await(async()=>{t=!0;let c=await u.select({message:"Payment provider:",options:Xe.map(p=>({value:p,label:He[p].label,hint:He[p].hint}))});return w(c),c})(),H=e?.defaultCurrency??await(async()=>{if(b==="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})(),R=e?.emailProvider??await(async()=>{t=!0;let c=await u.select({message:"Email provider:",options:Ze.map(p=>({value:p,label:Ye[p].label,hint:Ye[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:tt.map(p=>({value:p,label:Je[p].label,hint:Je[p].hint}))});w(c),oe=c}let T=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})(),k=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=b==="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=rt.map(v=>({value:v,label:W[v].label,hint:`requires ${W[v].envVars.map(O=>O.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 q=e?.dockerServices??await(async()=>{t=!0;let c=[...Qe].filter(x=>x!=="mailpit");R==="smtp"&&c.push("mailpit");let p=c.map(x=>({value:x,label:we[x].label,hint:we[x].hint})),v=p.map(x=>x.value).filter(x=>!(x==="postgres"&&(m==="neon"||m==="supabase")||x==="redis"&&S==="upstash")),O=await u.multiselect({message:"Which services should we set up in Docker for you?",options:p,initialValues:v,required:!1});return w(O),O})(),Ee=e?.aiTools??await(async()=>{t=!0;let c=et.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})(),vt=e?.demo,Ke=Et({databaseProvider:m,cacheProvider:S,paymentProvider:b,emailProvider:R,socialProviders:J,demo:vt}),xe={};if(Ke.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 Ke)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(i)}`,` Location: ${f.cyan(n)}`,` Frontend: ${f.cyan(De[o].label)}`,` Architecture: ${f.cyan(Ce[g].label)}`].join(`
4
- `),p=[` Deploy target: ${f.cyan(F[s]?.label??"Node.js / Docker")}`,` Database: ${f.cyan(D[m].label)}`,` Cache: ${f.cyan(B[S].label)}`,q.length>0?` Docker: ${f.cyan(q.map(se=>we[se].label).join(", "))}`:` Docker: ${f.dim("none")}`].filter(Boolean).join(`
5
- `),v=[b!=="none"?` Payment: ${f.cyan(He[b].label)} (${H})`:` Payment: ${f.dim("none")}`,` Credits: ${Y?f.cyan("Yes"):f.dim("No")}`,` Email: ${f.cyan(Ye[R].label)}`,` Multi-tenancy: ${ie?f.cyan("Yes")+` (billing: ${Je[oe].label})`:f.dim("No")}`,` Blog: ${T?f.cyan("Yes"):f.dim("No")}`,` Docs app: ${y?f.cyan("Yes"):f.dim("No")}`,` Rev. sharing: ${k?f.cyan("Yes"):f.dim("No")}`,J.length>0?` Social login: ${f.cyan(J.map(se=>W[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
- `),O=[f.bold("Project"),c,"",f.bold("Infrastructure"),p,"",f.bold("Features"),v];if(Ke.length>0){let se=Ke.map(Bt=>{let An=xe[Bt.key]?f.green("provided"):f.dim("skipped");return` ${Bt.key}: ${An}`}).join(`
7
- `);O.push("",f.bold("Credentials"),se)}u.note(O.join(`
8
- `),"Summary");let x=await u.confirm({message:"Proceed with these settings?"});(u.isCancel(x)||!x)&&(u.cancel("Setup cancelled."),process.exit(0))}return{projectName:r,appName:i,projectDir:n,frontend:o,architecture:g,deploymentTarget:s,databaseProvider:m,cacheProvider:S,paymentProvider:b,emailProvider:R,multiTenancy:ie,billingScope:oe,blog:T,docs:y,revenueSharing:k,credits:Y,dockerServices:q,aiTools:Ee,socialProviders:J,defaultCurrency:H,...Object.keys(xe).length>0?{credentials:xe}:{},...e?.baseUrl!==void 0?{baseUrl:e.baseUrl}:{},...vt!==void 0?{demo:vt}:{}}}import{createReadStream as Cn}from"fs";import{mkdir as Nn}from"fs/promises";import{Readable as Ln}from"stream";import{pipeline as tr}from"stream/promises";import{extract as $n}from"tar";import{join as ue}from"path";import{homedir as In}from"os";var ot=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",$=".generatesaas",Q=ue($,"manifest.json"),Yt=ue($,"hashes.json"),st=ue($,"template-hashes.json"),at=ue($,"template"),Jt=ue($,"staging"),qt=ue($,"staging.json"),X=ue(In(),".generatesaas");var _=class extends Error{constructor(r,i,n){super(i);this.status=r;this.body=n}status;body;name="ApiError"};function Z(e){return{apiKey:e,baseUrl:ot}}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 _(n.status,o,s)}return n}import{existsSync as Pn,readFileSync as kn,writeFileSync as _n,mkdirSync as Rn}from"fs";import{dirname as On}from"path";import*as te from"@clack/prompts";function wt(){if(!Pn(X))return null;try{let e=JSON.parse(kn(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){Rn(On(X),{recursive:!0}),_n(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=wt();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 je()}async function je(){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 Wt(e,t){try{return await(await ee(e,`/changelog/${encodeURIComponent(t)}`)).text()}catch(r){if(r instanceof _&&r.status===404)return null;throw r}}async function Xt(e,t){return await(await ee(e,`/skill/${encodeURIComponent(t)}`)).json()}function ct(e){if(!(e instanceof _)||e.status!==403)return null;let t=e.body;return!t||t.code!=="update_window_expired"?null:{message:e.message,lastAllowedVersion:t.lastAllowedVersion??null}}function Zt(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 Qt(e,t){return await(await ee(e,"/license/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function er(e,t){let r=await fetch(`${e}/license/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:t})});if(!r.ok)throw new Error(`Verification service returned ${r.status}`);return await r.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"]),xn=new Set(["data","mksaas","references","scripts",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","README.md","TODO.md","OVERVIEW.md"]),Dn=["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 Te(e){let t=e.split("/");for(let r of t)if(Tt.has(r))return!0;if(xn.has(t[0]))return!0;for(let r of Dn)if(e===r||e.startsWith(r+"/"))return!0;return!1}async function lt(e,t,r){await Nn(r,{recursive:!0});let i=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(i){await tr(Cn(i),rr(r));return}let n=await ee(e,`/template/${encodeURIComponent(t)}`);if(!n.body)throw new Error("Empty response body");let o=Ln.fromWeb(n.body);await tr(o,rr(r))}function rr(e){return $n({cwd:e,strip:1,filter:t=>{let r=t.replace(/^[^/]+\//,"");return r?!Te(r):!0},sync:!1})}import{readFile as Un,rm as nr,writeFile as jn}from"fs/promises";import{join as At}from"path";var Mn=["apps/web-nuxt/public/images/blog","apps/web-next/public/images/blog"];async function ir(e){await Promise.all(Mn.map(t=>nr(At(e,t),{recursive:!0,force:!0})))}async function or(e,t){t.includes("claude-code")||await nr(At(e,".claude"),{recursive:!0,force:!0})}async function sr(e,t){let r=At(e,".claude","settings.json"),i;try{i=await Un(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=>Vn(o,t))),await jn(r,JSON.stringify(n,null," ")+`
10
- `)}function Vn(e,t){return!(e.startsWith("mcp__")||t!=="nuxt"&&e.includes("nuxt"))}import{join as ar}from"path";import{mkdir as Fn,readdir as Bn,rm as Gn,rmdir as Kn,writeFile as zn}from"fs/promises";import{dirname as pt,join as Hn,relative as Yn}from"path";async function dt(e){await Fn(e,{recursive:!0})}async function l(e,t){await dt(pt(e)),await zn(e,t,"utf-8")}async function It(e,t){await Gn(e,{force:!0});let r=pt(e);for(;r!==t&&r!==pt(r);){try{await Kn(r)}catch{return}r=pt(r)}}async function fe(e,t,r){let i=[],n=await Bn(e,{withFileTypes:!0});for(let o of n){let s=Hn(e,o.name),a=Yn(t,s);r(a)||(o.isDirectory()?i.push(...await fe(s,t,r)):o.isFile()&&i.push(s))}return i}var Jn={postgres:"Postgres",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"},qn={resend:"Resend",ses:"Amazon SES",smtp:"SMTP"},Wn={redis:"Redis",upstash:"Upstash Redis"},Xn={node:"Node.js / Docker",vercel:"Vercel"},Zn={stripe:"Stripe",polar:"Polar"};function Qn(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=Wn[e.cacheProvider],h=Xn[e.deploymentTarget],g=e.paymentProvider==="none"?"":`
11
- - **Payments:** ${Zn[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 as}from"commander";import*as Pn from"@clack/prompts";import{existsSync as xo,readdirSync as Do,rmSync as Co}from"fs";import{join as No,resolve as Lo}from"path";import{Option as M}from"commander";import*as E from"@clack/prompts";import*as Ye from"@clack/prompts";import Et from"picocolors";function Gt(e){let t=e?` GenerateSaaS v${e} `:" GenerateSaaS ";Ye.intro(Et.bgYellow(Et.black(t)))}function Kt(){Ye.outro(Et.yellow("Happy building!"))}import*as u from"@clack/prompts";import g 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"}},qe={smtp:{label:"SMTP",hint:"Mailpit for local dev"},ses:{label:"Amazon SES"},resend:{label:"Resend"}},We={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)."}],zt=["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(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 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 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 Ht(e){let t=!1;u.log.info(g.bold("Project"));let r=e?.projectName??await(async()=>{t=!0;let c=await u.text({message:"Project name:",placeholder:"my-saas",validate:d=>{if(!d?.trim())return"Project name is required.";if(!st(d))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 u.text({message:"App name:",initialValue:ot(r),validate:d=>{if(!d?.trim())return"App name is required."}});return w(c),c})(),n=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),d=await u.select({message:"Frontend framework:",options:c.map(v=>({value:v,label:De[v].label,hint:De[v].hint}))});return w(d),d})();u.log.info(g.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(d=>({value:d,label:B[d].label,hint:B[d].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 l=new Set($e.filter(c=>c.target===s).map(c=>c.architecture)),f=Ze.filter(c=>!l.has(c)),h=e?.architecture??await(async()=>{if(f.length===1){let d=f[0];return u.log.info(`Auto-selected ${Ce[d].label} architecture (only compatible option for ${B[s].label}).`),d}t=!0;let c=await u.select({message:"Architecture:",options:f.map(d=>({value:d,label:Ce[d].label,hint:Ce[d].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 d=await u.select({message:"Database provider:",options:c.map(v=>({value:v,label:L[v].label,hint:L[v].hint}))});return w(d),d})(),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 d=await u.select({message:"Cache provider:",options:c.map(v=>({value:v,label:G[v].label,hint:G[v].hint}))});return w(d),d})();if(B[s]?.edgeRuntime){let c=zt.map(d=>` - ${d}`).join(`
3
+ `);u.note(c,"Unavailable on edge runtime")}u.log.info(g.bold("Features"));let A=e?.paymentProvider??await(async()=>{t=!0;let c=await u.select({message:"Payment provider:",options:Qe.map(d=>({value:d,label:Je[d].label,hint:Je[d].hint}))});return w(c),c})(),H=e?.defaultCurrency??await(async()=>{if(A==="none")return"USD";t=!0;let c=await u.select({message:"Default currency:",options:ce.map(d=>({value:d,label:d,hint:ae[d].name}))});return w(c),c})(),D=e?.emailProvider??await(async()=>{t=!0;let c=await u.select({message:"Email provider:",options:et.map(d=>({value:d,label:qe[d].label,hint:qe[d].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(d=>({value:d,label:We[d].label,hint:We[d].hint}))});w(c),oe=c}let T=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=A==="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:W[v].label,hint:`requires ${W[v].envVars.map(C=>C.name).join(" / ")}`})),d=await u.multiselect({message:"Which social login providers should the sign-in screen show?",options:c,initialValues:[],required:!1});return w(d),d})();u.log.info(g.bold("Tooling"));let q=e?.dockerServices??await(async()=>{t=!0;let c=[...tt].filter(N=>N!=="mailpit");D==="smtp"&&c.push("mailpit");let d=c.map(N=>({value:N,label:we[N].label,hint:we[N].hint})),v=d.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:d,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})),d=await u.multiselect({message:"Which AI coding tools do you use?",options:c,initialValues:[],required:!1});return w(d),d})(),St=e?.demo,He=wt({databaseProvider:m,cacheProvider:S,paymentProvider:A,emailProvider:D,socialProviders:J,demo:St}),xe={};if(He.length>0&&t){u.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 d=await u.password({message:c.message,mask:"*"});w(d),typeof d=="string"&&d.trim()&&(xe[c.key]=d.trim())}else{let d=await u.text({message:c.message,placeholder:c.placeholder});w(d),typeof d=="string"&&d.trim()&&(xe[c.key]=d.trim())}}if(t){let c=[` Name: ${g.cyan(r)}`,` App name: ${g.cyan(i)}`,` Location: ${g.cyan(n)}`,` Frontend: ${g.cyan(De[o].label)}`,` Architecture: ${g.cyan(Ce[h].label)}`].join(`
4
+ `),d=[` Deploy target: ${g.cyan(B[s]?.label??"Node.js / Docker")}`,` Database: ${g.cyan(L[m].label)}`,` Cache: ${g.cyan(G[S].label)}`,q.length>0?` Docker: ${g.cyan(q.map(se=>we[se].label).join(", "))}`:` Docker: ${g.dim("none")}`].filter(Boolean).join(`
5
+ `),v=[A!=="none"?` Payment: ${g.cyan(Je[A].label)} (${H})`:` Payment: ${g.dim("none")}`,` Credits: ${Y?g.cyan("Yes"):g.dim("No")}`,` Email: ${g.cyan(qe[D].label)}`,` Multi-tenancy: ${ie?g.cyan("Yes")+` (billing: ${We[oe].label})`:g.dim("No")}`,` Blog: ${T?g.cyan("Yes"):g.dim("No")}`,` Docs app: ${y?g.cyan("Yes"):g.dim("No")}`,` Rev. sharing: ${R?g.cyan("Yes"):g.dim("No")}`,J.length>0?` Social login: ${g.cyan(J.map(se=>W[se].label).join(", "))}`:` Social login: ${g.dim("none")}`,Ee.length>0?` AI tools: ${g.cyan(Ee.map(se=>Ne[se].label).join(", "))}`:` AI tools: ${g.dim("none")}`].join(`
6
+ `),C=[g.bold("Project"),c,"",g.bold("Infrastructure"),d,"",g.bold("Features"),v];if(He.length>0){let se=He.map(Bt=>{let _n=xe[Bt.key]?g.green("provided"):g.dim("skipped");return` ${Bt.key}: ${_n}`}).join(`
7
+ `);C.push("",g.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:i,projectDir:n,frontend:o,architecture:h,deploymentTarget:s,databaseProvider:m,cacheProvider:S,paymentProvider:A,emailProvider:D,multiTenancy:ie,billingScope:oe,blog:T,docs:y,revenueSharing:R,credits:Y,dockerServices:q,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 Un}from"fs";import{mkdir as jn}from"fs/promises";import{Readable as Mn}from"stream";import{pipeline as rr}from"stream/promises";import{extract as Vn}from"tar";import{join as ue}from"path";import{homedir as Rn}from"os";var je=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",U=".generatesaas",Q=ue(U,"manifest.json"),Yt=ue(U,"hashes.json"),at=ue(U,"template-hashes.json"),ct=ue(U,"template"),Jt=ue(U,"staging"),qt=ue(U,"staging.json"),X=ue(Rn(),".generatesaas");var O=class extends Error{constructor(r,i,n){super(i);this.status=r;this.body=n}status;body;name="ApiError"};function Z(e){return{apiKey:e,baseUrl:je}}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 O(n.status,o,s)}return n}import{existsSync as On,readFileSync as xn,writeFileSync as Dn,mkdirSync as Cn}from"fs";import{dirname as Nn}from"path";import*as te from"@clack/prompts";function Me(){if(!On(X))return null;try{let e=JSON.parse(xn(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){Cn(Nn(X),{recursive:!0}),Dn(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=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 Ve()}async function Ve(){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 Wt(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 Xt(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 Zt(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 Qt(e,t){return await(await ee(e,"/license/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function er(e,t){let r=await fetch(`${e}/license/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:t})});if(!r.ok)throw new Error(`Verification service returned ${r.status}`);return await r.json()}async function tr(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 At=new Set([".git","node_modules",".pnpm-store",".env",".env.test",".turbo",".nuxt",".output",".data","dist",".next",".svelte-kit",".devcontainer","playwright-report","test-results"]),Ln=new Set(["data","mksaas","references","scripts",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","README.md","TODO.md","OVERVIEW.md"]),$n=["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(At.has(r))return!0;if(Ln.has(t[0]))return!0;for(let r of $n)if(e===r||e.startsWith(r+"/"))return!0;return!1}async function pt(e,t,r){await jn(r,{recursive:!0});let i=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(i){await rr(Un(i),nr(r));return}let n=await ee(e,`/template/${encodeURIComponent(t)}`);if(!n.body)throw new Error("Empty response body");let o=Mn.fromWeb(n.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 Fn,rm as ir,writeFile as Bn}from"fs/promises";import{join as Tt}from"path";var Gn=["apps/web-nuxt/public/images/blog","apps/web-next/public/images/blog"];async function or(e){await Promise.all(Gn.map(t=>ir(Tt(e,t),{recursive:!0,force:!0})))}async function sr(e,t){t.includes("claude-code")||await ir(Tt(e,".claude"),{recursive:!0,force:!0})}async function ar(e,t){let r=Tt(e,".claude","settings.json"),i;try{i=await Fn(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=>Kn(o,t))),await Bn(r,JSON.stringify(n,null," ")+`
10
+ `)}function Kn(e,t){return!(e.startsWith("mcp__")||t!=="nuxt"&&e.includes("nuxt"))}import{join as cr}from"path";import{mkdir as zn,readdir as Hn,rm as Yn,rmdir as Jn,writeFile as qn}from"fs/promises";import{dirname as dt,join as Wn,relative as Xn}from"path";async function ut(e){await zn(e,{recursive:!0})}async function p(e,t){await ut(dt(e)),await qn(e,t,"utf-8")}async function It(e,t){await Yn(e,{force:!0});let r=dt(e);for(;r!==t&&r!==dt(r);){try{await Jn(r)}catch{return}r=dt(r)}}async function fe(e,t,r){let i=[],n=await Hn(e,{withFileTypes:!0});for(let o of n){let s=Wn(e,o.name),a=Xn(t,s);r(a)||(o.isDirectory()?i.push(...await fe(s,t,r)):o.isFile()&&i.push(s))}return i}var Zn={postgres:"Postgres",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"},Qn={resend:"Resend",ses:"Amazon SES",smtp:"SMTP"},ei={redis:"Redis",upstash:"Upstash Redis"},ti={node:"Node.js / Docker",vercel:"Vercel"},ri={stripe:"Stripe",polar:"Polar"};function ni(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.",l=ei[e.cacheProvider],f=ti[e.deploymentTarget],h=e.paymentProvider==="none"?"":`
11
+ - **Payments:** ${ri[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
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
@@ -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 + ${Jn[e.databaseProvider]}
40
- - **Cache + jobs:** ${d} + Inngest
41
- - **Auth:** Better Auth${g}
42
- - **Email:** ${qn[e.emailProvider]}
43
- - **Deploy:** ${h}
39
+ - **Database:** Drizzle ORM + ${Zn[e.databaseProvider]}
40
+ - **Cache + jobs:** ${l} + Inngest
41
+ - **Auth:** Better Auth${h}
42
+ - **Email:** ${Qn[e.emailProvider]}
43
+ - **Deploy:** ${f}
44
44
 
45
45
  ## Where things live (extend these - don't reinvent)
46
46
 
@@ -80,12 +80,12 @@ 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 cr(e){await l(ar(e.projectDir,"AGENTS.md"),Qn(e)),await l(ar(e.projectDir,"CLAUDE.md"),`@AGENTS.md
84
- `)}import{join as ei}from"path";var ti={postgres:"Postgres (self-hosted)",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"};function ri(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=ti[e.databaseProvider],s=qe(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
83
+ `}async function lr(e){await p(cr(e.projectDir,"AGENTS.md"),ni(e)),await p(cr(e.projectDir,"CLAUDE.md"),`@AGENTS.md
84
+ `)}import{join as ii}from"path";var oi={postgres:"Postgres (self-hosted)",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"};function si(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=oi[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/\`.`,l=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`,f=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
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}
91
91
 
@@ -107,10 +107,10 @@ 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
+ ${f}pnpm dev
111
111
  \`\`\`
112
112
 
113
- ${d}
113
+ ${l}
114
114
 
115
115
  ## Configuration
116
116
 
@@ -125,7 +125,7 @@ pnpm -F @repo/database studio # open Drizzle Studio
125
125
  pnpm auth:generate # regenerate Better Auth schema after config changes
126
126
  \`\`\`
127
127
 
128
- ${g}
128
+ ${h}
129
129
 
130
130
  ## Removing the GenerateSaaS license
131
131
 
@@ -138,11 +138,11 @@ pnpm dlx generatesaas eject
138
138
  ## Updates
139
139
 
140
140
  ${m}
141
- `}async function lr(e){await l(ei(e.projectDir,"README.md"),ri(e))}function ni(e){let t=e.split(".");return t.length>=3?t.slice(1).join("."):e}async function pr(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=ni(n),s=e.demo?"false":"true",a=e.demo?`
141
+ `}async function pr(e){await p(ii(e.projectDir,"README.md"),si(e))}function ai(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",i=e.baseUrl??"http://localhost:3000",n=e.baseUrl?new URL(e.baseUrl).hostname:"example.com",o=ai(n),s=e.demo?"false":"true",a=e.demo?`
142
142
  support: {
143
143
  enableInDev: true,
144
144
  crisp: { websiteId: "7e221cec-ed61-46b7-b1b4-8cbc16557cca" }
145
- },`:"",d=e.frontend==="nextjs"&&e.architecture==="fullstack"?"false":"true",h=`import type { AppConfig } from "@repo/config/types";
145
+ },`:"",l=e.frontend==="nextjs"&&e.architecture==="fullstack"?"false":"true",f=`import type { AppConfig } from "@repo/config/types";
146
146
 
147
147
  const trustedOrigins = process.env.TRUSTED_ORIGINS?.split(",").map((s) => s.trim()).filter(Boolean);
148
148
 
@@ -212,7 +212,7 @@ export const config: AppConfig = {
212
212
  prefix: "key_"
213
213
  },
214
214
  apiDocs: true,
215
- performanceMonitor: { enabled: ${d} },
215
+ performanceMonitor: { enabled: ${l} },
216
216
  notifications: {
217
217
  enabled: true,
218
218
  maxPerUser: 100,
@@ -303,8 +303,8 @@ export * from "./pricing";
303
303
  export * from "./roles";
304
304
  export * from "./section-tabs";
305
305
  export * from "./tenancy";
306
- `,g=`${e.projectDir}/packages/config/src/index.ts`;await l(g,h)}function ii(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(`
307
- `)}async function dr(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";
306
+ `,h=`${e.projectDir}/packages/config/src/index.ts`;await p(h,f)}function ci(e){return e==="stripe"?' stripePriceId: "",':' polarProductId: "",'}function kt(e){let t=[];return e.withCredits&&(t.push(` credits: ${e.credits},`),t.push(" creditInterval: 30,")),t.push(` apiRateLimit: { maxRequests: ${e.rateLimit} }`),t.join(`
307
+ `)}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";
308
308
 
309
309
  export const pricingConfig: PricingConfig = {
310
310
  defaultPlan: "free",
@@ -333,7 +333,7 @@ export const pricingConfig: PricingConfig = {
333
333
  credits: { enabled: false },
334
334
  products: { enabled: false, items: [] }
335
335
  };
336
- `;await l(t,m);return}let i=e.paymentProvider,n=ii(i),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";
336
+ `;await p(t,m);return}let i=e.paymentProvider,n=ci(i),o=e.credits,s=kt({credits:5,rateLimit:100,withCredits:o}),a=kt({credits:10,rateLimit:1e3,withCredits:o}),l=kt({credits:50,rateLimit:5e3,withCredits:o}),h=`import type { PricingConfig } from "@repo/config/types";
337
337
 
338
338
  export const pricingConfig: PricingConfig = {
339
339
  defaultPlan: "free",
@@ -414,7 +414,7 @@ ${n}
414
414
  anchorAmounts: { ${r}: 349 }
415
415
  }
416
416
  ],
417
- ${d}
417
+ ${l}
418
418
  }
419
419
  ],
420
420
  ${o?" credits: { enabled: true }":" credits: { enabled: false }"},
@@ -423,11 +423,11 @@ ${o?" credits: { enabled: true }":" credits: { enabled: false }"},
423
423
  items: []
424
424
  }
425
425
  };
426
- `;await l(t,g)}var oi={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 si(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 ai={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 Ae(e,t){return t?e.map(r=>{let i=t[r.key];return i?{...r,defaultValue:i,comment:void 0}:r}):e}function Ie(e,t){for(let r of e)r.comment&&t.push(r.comment),r.defaultValue!==void 0?t.push(`${r.key}=${r.defaultValue}`):t.push(`#${r.key}=`)}function kt(e){return Array.from(crypto.getRandomValues(new Uint8Array(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}function mr(e){return e.architecture==="fullstack"?{apiUrl:"http://localhost:3000/api",baseUrl:"http://localhost:3000"}:{apiUrl:"http://localhost:3010",baseUrl:"http://localhost:3000"}}function ci(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 ut(e,t,r,i){e.push(i==="example"?`${t}=`:`${t}=${r}`)}function ur(e,t){let r=t==="example"?void 0:e.credentials,{apiUrl:i,baseUrl:n}=mr(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"),Ie(Ae(D[e.databaseProvider].envVars,r),o),o.push("","# Cache"),Ie(Ae(B[e.cacheProvider].envVars,r),o),o.push("","# Authentication"),t==="example"&&o.push("# Generate a strong secret, e.g. `openssl rand -hex 32`"),ut(o,"BETTER_AUTH_SECRET",crypto.randomUUID(),t),o.push("","# Content API (random secret; required when contentApi feature is enabled in @repo/config)"),ut(o,"CONTENT_API_KEY",kt(32),t),o.push("","# Job Queue - Inngest","INNGEST_APP_ID=api"),ut(o,"INNGEST_EVENT_KEY",kt(32),t),ut(o,"INNGEST_SIGNING_KEY",kt(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=oi[e.emailProvider];if(s&&(o.push("","# Email"),Ie(Ae(s,r),o)),e.paymentProvider!=="none"){let a=ai[e.paymentProvider];a&&(o.push("","# Payment"),Ie(Ae(a,r),o))}if(e.socialProviders.length>0){o.push("","# Social auth");for(let a of e.socialProviders)Ie(Ae(si(a),r),o)}return e.demo&&(o.push("","# Captcha (Cloudflare Turnstile)"),Ie(Ae([{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(`
427
- `)}function li(e){let{apiUrl:t}=mr(e),r=e.frontend==="nextjs"?"NEXT_PUBLIC_API_URL":"NUXT_PUBLIC_API_URL",i=["# API Configuration",`${r}=${t}`],n=ci(e);return n&&e.architecture==="separate"&&i.push("","# Production (uncomment and replace with your deployed hostnames):",`# ${r}=${n.backend}`),i.push(""),i.join(`
428
- `)}async function fr(e){let t=li(e);await l(`${e.projectDir}/.env`,t+`
429
- `+ur(e,"env")),await l(`${e.projectDir}/.env.example`,t+`
430
- `+ur(e,"example"))}async function gr(e){let t=[...e.dockerServices];if(D[e.databaseProvider].managed&&(t=t.filter(o=>o!=="postgres")),B[e.cacheProvider].managed&&(t=t.filter(o=>o!=="redis")),t.length===0)return!1;let r=[],i=[];t.includes("postgres")&&(r.push(` postgres:
426
+ `;await p(t,h)}var li={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 pi(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 di={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 Ie(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 Pt(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 ui(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 mr(e,t){let r=t==="example"?void 0:e.credentials,{apiUrl:i,baseUrl:n}=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=${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"),Ie(Te(L[e.databaseProvider].envVars,r),o),o.push("","# Cache"),Ie(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",Pt(32),t),o.push("","# Job Queue - Inngest","INNGEST_APP_ID=api"),mt(o,"INNGEST_EVENT_KEY",Pt(32),t),mt(o,"INNGEST_SIGNING_KEY",Pt(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=li[e.emailProvider];if(s&&(o.push("","# Email"),Ie(Te(s,r),o)),e.paymentProvider!=="none"){let a=di[e.paymentProvider];a&&(o.push("","# Payment"),Ie(Te(a,r),o))}if(e.socialProviders.length>0){o.push("","# Social auth");for(let a of e.socialProviders)Ie(Te(pi(a),r),o)}return e.demo&&(o.push("","# Captcha (Cloudflare Turnstile)"),Ie(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(`
427
+ `)}function mi(e){let{apiUrl:t}=fr(e),r=e.frontend==="nextjs"?"NEXT_PUBLIC_API_URL":"NUXT_PUBLIC_API_URL",i=["# API Configuration",`${r}=${t}`],n=ui(e);return n&&e.architecture==="separate"&&i.push("","# Production (uncomment and replace with your deployed hostnames):",`# ${r}=${n.backend}`),i.push(""),i.join(`
428
+ `)}async function gr(e){let t=mi(e);await p(`${e.projectDir}/.env`,t+`
429
+ `+mr(e,"env")),await p(`${e.projectDir}/.env.example`,t+`
430
+ `+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=[],i=[];t.includes("postgres")&&(r.push(` postgres:
431
431
  image: postgres:18-alpine
432
432
  ports:
433
433
  - "\${POSTGRES_PORT:-5432}:5432"
@@ -471,8 +471,8 @@ ${r.join(`
471
471
  volumes:
472
472
  ${i.join(`
473
473
  `)}
474
- `),await l(`${e.projectDir}/infra/docker-compose.yml`,n),!0}function pi(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 di(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 hr(e){let t={version:"0.2.0",configurations:pi(e),compounds:di(e)};await l(`${e.projectDir}/.vscode/launch.json`,JSON.stringify(t,null," ")+`
475
- `)}async function yr(e){if(e.architecture!=="separate")return;await l(`${e.projectDir}/apps/backend/src/index.ts`,`import { serve } from "@hono/node-server";
474
+ `),await p(`${e.projectDir}/infra/docker-compose.yml`,n),!0}function fi(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 gi(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:fi(e),compounds:gi(e)};await p(`${e.projectDir}/.vscode/launch.json`,JSON.stringify(t,null," ")+`
475
+ `)}async function vr(e){if(e.architecture!=="separate")return;await p(`${e.projectDir}/apps/backend/src/index.ts`,`import { serve } from "@hono/node-server";
476
476
  import app from "@repo/api";
477
477
  import { closeRedis, env, logger } from "@repo/runtime";
478
478
 
@@ -503,9 +503,9 @@ bootstrap().catch((error) => {
503
503
  logger.error("[Backend] Fatal error", error);
504
504
  process.exit(1);
505
505
  });
506
- `)}import{readFile as ui}from"fs/promises";import{join as mi}from"path";var fi=`export * from "./db/auth";
506
+ `)}import{readFile as hi}from"fs/promises";import{join as yi}from"path";var vi=`export * from "./db/auth";
507
507
  export * from "./db/schema";
508
- export type { User, Account, Organization, Member } from "./db/auth";`;async function vr(e){let t=mi(e.projectDir,"packages/database/src/index.ts"),r=fi;try{let n=await ui(t,"utf-8"),o=gi(n);o&&(r=o)}catch{}let i;switch(e.databaseProvider){case"postgres":i=hi(r);break;case"neon":i=yi(r);break;case"supabase":i=vi(r);break}await l(t,i)}function gi(e){return e.split(`
508
+ export type { User, Account, Organization, Member } from "./db/auth";`;async function Sr(e){let t=yi(e.projectDir,"packages/database/src/index.ts"),r=vi;try{let n=await hi(t,"utf-8"),o=Si(n);o&&(r=o)}catch{}let i;switch(e.databaseProvider){case"postgres":i=Ei(r);break;case"neon":i=wi(r);break;case"supabase":i=bi(r);break}await p(t,i)}function Si(e){return e.split(`
509
509
  `).filter(i=>i.startsWith("export type ")||i.startsWith("export * from")).join(`
510
510
  `)}var _t=`
511
511
  /** Extract affected-row count from a delete/update result (works for pg + postgres-js). */
@@ -514,7 +514,7 @@ export function affectedRowCount(result: unknown): number {
514
514
  const r = result as { rowCount?: number; count?: number };
515
515
  return r.rowCount ?? r.count ?? 0;
516
516
  }
517
- `;function hi(e){return`import { drizzle } from "drizzle-orm/node-postgres";
517
+ `;function Ei(e){return`import { drizzle } from "drizzle-orm/node-postgres";
518
518
  import { z } from "zod";
519
519
  import * as authSchema from "./db/auth";
520
520
  import * as appSchema from "./db/schema";
@@ -529,7 +529,7 @@ export const db = drizzle(parsed.data.DATABASE_URL, { schema });
529
529
  export const pool = db.$client;
530
530
 
531
531
  ${e}
532
- ${_t}`}function yi(e){return`import { neon } from "@neondatabase/serverless";
532
+ ${_t}`}function wi(e){return`import { neon } from "@neondatabase/serverless";
533
533
  import { drizzle } from "drizzle-orm/neon-http";
534
534
  import { z } from "zod";
535
535
  import * as authSchema from "./db/auth";
@@ -545,7 +545,7 @@ const sql = neon(parsed.data.DATABASE_URL);
545
545
  export const db = drizzle(sql, { schema });
546
546
 
547
547
  ${e}
548
- ${_t}`}function vi(e){return`import { drizzle } from "drizzle-orm/postgres-js";
548
+ ${_t}`}function bi(e){return`import { drizzle } from "drizzle-orm/postgres-js";
549
549
  import postgres from "postgres";
550
550
  import { z } from "zod";
551
551
  import * as authSchema from "./db/auth";
@@ -561,7 +561,7 @@ const client = postgres(parsed.data.DATABASE_URL);
561
561
  export const db = drizzle(client, { schema });
562
562
 
563
563
  ${e}
564
- ${_t}`}async function Sr(e){switch(e.cacheProvider){case"redis":await Si(e),await Ei(e);break;case"upstash":await wi(e),await bi(e);break}}async function Si(e){await l(`${e.projectDir}/packages/runtime/src/redis.ts`,`import type { Store } from "hono-rate-limiter";
564
+ ${_t}`}async function Er(e){switch(e.cacheProvider){case"redis":await Ai(e),await Ti(e);break;case"upstash":await Ii(e),await ki(e);break}}async function Ai(e){await p(`${e.projectDir}/packages/runtime/src/redis.ts`,`import type { Store } from "hono-rate-limiter";
565
565
  import { Redis } from "ioredis";
566
566
  import { RedisStore, type RedisReply } from "rate-limit-redis";
567
567
  import { env } from "./env";
@@ -664,7 +664,7 @@ export async function closeRedis() {
664
664
  closed = true;
665
665
  }
666
666
  }
667
- `)}async function Ei(e){await l(`${e.projectDir}/packages/runtime/src/mutex.ts`,`import { Mutex } from "redis-semaphore";
667
+ `)}async function Ti(e){await p(`${e.projectDir}/packages/runtime/src/mutex.ts`,`import { Mutex } from "redis-semaphore";
668
668
  import { redis } from "./redis";
669
669
 
670
670
  export class MutexTimeoutError extends Error {
@@ -701,7 +701,7 @@ export async function withMutex<T>(
701
701
  await mutex.release();
702
702
  }
703
703
  }
704
- `)}async function wi(e){await l(`${e.projectDir}/packages/runtime/src/redis.ts`,`import { Redis } from "@upstash/redis";
704
+ `)}async function Ii(e){await p(`${e.projectDir}/packages/runtime/src/redis.ts`,`import { Redis } from "@upstash/redis";
705
705
  import type { Store } from "hono-rate-limiter";
706
706
  import { env } from "./env";
707
707
 
@@ -814,7 +814,7 @@ export const limiterStore: Store = createLimiterStore();
814
814
 
815
815
  /** No persistent connection to close with Upstash REST. */
816
816
  export async function closeRedis(): Promise<void> {}
817
- `)}async function bi(e){await l(`${e.projectDir}/packages/runtime/src/mutex.ts`,`import { Lock } from "@upstash/lock";
817
+ `)}async function ki(e){await p(`${e.projectDir}/packages/runtime/src/mutex.ts`,`import { Lock } from "@upstash/lock";
818
818
  import { redis } from "./redis";
819
819
 
820
820
  export class MutexTimeoutError extends Error {
@@ -859,7 +859,7 @@ export async function withMutex<T>(
859
859
  await lock.release();
860
860
  }
861
861
  }
862
- `)}async function Er(e){await l(`${e.projectDir}/packages/runtime/src/env.ts`,Ti(e))}function Ti(e){return`import { z } from "zod";
862
+ `)}async function wr(e){await p(`${e.projectDir}/packages/runtime/src/env.ts`,Pi(e))}function Pi(e){return`import { z } from "zod";
863
863
 
864
864
  const EnvSchema = z.object({
865
865
  NODE_ENV: z.enum(["development", "production"]).default("development"),
@@ -977,13 +977,13 @@ export const env = (() => {
977
977
  const parsedApiUrl = new URL(env.API_URL);
978
978
  export const apiBasePath = parsedApiUrl.pathname === "/" ? "" : parsedApiUrl.pathname;
979
979
  export const apiOrigin = parsedApiUrl.origin;
980
- `}import{readFile as Ai}from"fs/promises";import{join as G}from"path";var mt={"@upstash/redis":"^1.37.0","@upstash/lock":"^0.2.1","@neondatabase/serverless":"^1.0.1",postgres:"^3.4.7"};async function ke(e){let t=await Ai(e,"utf-8");return JSON.parse(t)}async function _e(e,t){await l(e,JSON.stringify(t,null," ")+`
981
- `)}function Me(e,t){for(let r of t)delete e.dependencies?.[r],delete e.devDependencies?.[r]}function Pe(e,t,r,i=!1){let n=i?"devDependencies":"dependencies";e[n]||(e[n]={}),e[n][t]=r}async function wr(e){await Ii(e),await Pi(e),await ki(e),await _i(e),e.frontend==="nextjs"?await Oi(e):await Ri(e)}async function Ii(e){let t=G(e.projectDir,"packages/api/package.json"),r=await ke(t);Me(r,["sharp","@types/sharp"]),await _e(t,r)}async function Pi(e){let t=G(e.projectDir,"packages/runtime/package.json"),r=await ke(t);e.cacheProvider==="upstash"&&(Me(r,["ioredis","rate-limit-redis","redis-semaphore"]),Pe(r,"@upstash/redis",mt["@upstash/redis"]),Pe(r,"@upstash/lock",mt["@upstash/lock"])),await _e(t,r)}async function ki(e){let t=G(e.projectDir,"packages/database/package.json"),r=await ke(t);e.databaseProvider==="neon"?(Me(r,["pg","@types/pg"]),Pe(r,"@neondatabase/serverless",mt["@neondatabase/serverless"])):e.databaseProvider==="supabase"&&(Me(r,["pg","@types/pg"]),Pe(r,"postgres",mt.postgres)),await _e(t,r)}async function _i(e){if(e.architecture!=="separate")return;let t=G(e.projectDir,"apps/backend/package.json"),r=await ke(t);e.deploymentTarget!=="node"&&Me(r,["@hono/node-server"]),await _e(t,r)}async function Ri(e){if(e.architecture!=="separate")return;let t=G(e.projectDir,"apps/web-nuxt");await It(G(t,"server/api/[...paths].ts"),t);let r=G(t,"package.json"),i=await ke(r),n=i.dependencies?.["@repo/api"];n&&(delete i.dependencies?.["@repo/api"],Pe(i,"@repo/api",n,!0)),await _e(r,i)}async function Oi(e){if(e.architecture!=="separate")return;let t=G(e.projectDir,"apps/web-next");await It(G(t,"app/api/[[...rest]]/route.ts"),t);let r=G(t,"package.json"),i=await ke(r),n=i.dependencies?.["@repo/api"];n&&(delete i.dependencies?.["@repo/api"],Pe(i,"@repo/api",n,!0)),await _e(r,i)}import{readFile as Tr}from"fs/promises";import{join as Ar}from"path";async function Ir(e){let t=Ar(e.projectDir,"turbo.json"),r;try{r=await Tr(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(h=>h!==o);d.length!==n.env.length&&(n.env=d,a=!0)}if(Array.isArray(n.outputs)){let d=n.outputs.filter(h=>!s.has(h));d.length!==n.outputs.length&&(n.outputs=d,a=!0)}a&&await l(t,JSON.stringify(i,null," ")+`
982
- `)}var xi=["base.json","node.json","next.json"],br="GenerateSaaS ";async function Pr(e){if(!e.demo)for(let t of xi){let r=Ar(e.projectDir,"tooling/typescript",t),i;try{i=await Tr(r,"utf-8")}catch{continue}let n=JSON.parse(i);typeof n.display!="string"||!n.display.startsWith(br)||(n.display=n.display.slice(br.length),await l(r,JSON.stringify(n,null," ")+`
983
- `))}}import{readFile as Di,rm as Ci}from"fs/promises";import{existsSync as kr}from"fs";import{join as _r}from"path";async function Rr(e){if(e.frontend==="nuxt")return;let t=_r(e.projectDir,"packages/i18n/package.json");if(!kr(t))throw new Error(`pruneI18nNuxt: expected ${t} to exist - did the i18n package move?`);let r=JSON.parse(await Di(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," ")+`
984
- `);let o=_r(e.projectDir,"packages/i18n/nuxt");if(i&&!kr(o))throw new Error(`pruneI18nNuxt: packages/i18n declares a Nuxt export surface but ${o} is missing - did the i18n Nuxt module move?`);await Ci(o,{recursive:!0,force:!0})}import{readFile as Or,rm as Ni}from"fs/promises";import{join as Rt}from"path";async function xr(e){e.cacheProvider==="upstash"&&await Promise.all([Li(e.projectDir),$i(e.projectDir),Ni(Rt(e.projectDir,"packages/runtime/tests/redis.test.ts"),{force:!0})])}async function Li(e){let t=Rt(e,"packages/runtime/tests/setup.ts"),r;try{r=await Or(t,"utf-8")}catch{return}let i=r.replace(/\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
980
+ `}import{readFile as _i}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 _i(e,"utf-8");return JSON.parse(t)}async function _e(e,t){await p(e,JSON.stringify(t,null," ")+`
981
+ `)}function Fe(e,t){for(let r of t)delete e.dependencies?.[r],delete e.devDependencies?.[r]}function ke(e,t,r,i=!1){let n=i?"devDependencies":"dependencies";e[n]||(e[n]={}),e[n][t]=r}async function br(e){await Ri(e),await Oi(e),await xi(e),await Di(e),e.frontend==="nextjs"?await Ni(e):await Ci(e)}async function Ri(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 Oi(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"]),ke(r,"@upstash/redis",ft["@upstash/redis"]),ke(r,"@upstash/lock",ft["@upstash/lock"])),await _e(t,r)}async function xi(e){let t=K(e.projectDir,"packages/database/package.json"),r=await Pe(t);e.databaseProvider==="neon"?(Fe(r,["pg","@types/pg"]),ke(r,"@neondatabase/serverless",ft["@neondatabase/serverless"])):e.databaseProvider==="supabase"&&(Fe(r,["pg","@types/pg"]),ke(r,"postgres",ft.postgres)),await _e(t,r)}async function Di(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 Ci(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"),i=await Pe(r),n=i.dependencies?.["@repo/api"];n&&(delete i.dependencies?.["@repo/api"],ke(i,"@repo/api",n,!0)),await _e(r,i)}async function Ni(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"),i=await Pe(r),n=i.dependencies?.["@repo/api"];n&&(delete i.dependencies?.["@repo/api"],ke(i,"@repo/api",n,!0)),await _e(r,i)}import{readFile as Tr}from"fs/promises";import{join as Ir}from"path";async function kr(e){let t=Ir(e.projectDir,"turbo.json"),r;try{r=await Tr(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 l=n.env.filter(f=>f!==o);l.length!==n.env.length&&(n.env=l,a=!0)}if(Array.isArray(n.outputs)){let l=n.outputs.filter(f=>!s.has(f));l.length!==n.outputs.length&&(n.outputs=l,a=!0)}a&&await p(t,JSON.stringify(i,null," ")+`
982
+ `)}var Li=["base.json","node.json","next.json"],Ar="GenerateSaaS ";async function Pr(e){if(!e.demo)for(let t of Li){let r=Ir(e.projectDir,"tooling/typescript",t),i;try{i=await Tr(r,"utf-8")}catch{continue}let n=JSON.parse(i);typeof n.display!="string"||!n.display.startsWith(Ar)||(n.display=n.display.slice(Ar.length),await p(r,JSON.stringify(n,null," ")+`
983
+ `))}}import{readFile as $i,rm as Ui}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 $i(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 p(t,JSON.stringify(r,null," ")+`
984
+ `);let o=Rr(e.projectDir,"packages/i18n/nuxt");if(i&&!_r(o))throw new Error(`pruneI18nNuxt: packages/i18n declares a Nuxt export surface but ${o} is missing - did the i18n Nuxt module move?`);await Ui(o,{recursive:!0,force:!0})}import{readFile as xr,rm as ji}from"fs/promises";import{join as Rt}from"path";async function Dr(e){e.cacheProvider==="upstash"&&await Promise.all([Mi(e.projectDir),Vi(e.projectDir),ji(Rt(e.projectDir,"packages/runtime/tests/redis.test.ts"),{force:!0})])}async function Mi(e){let t=Rt(e,"packages/runtime/tests/setup.ts"),r;try{r=await xr(t,"utf-8")}catch{return}let i=r.replace(/\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
985
985
  UPSTASH_REDIS_REST_TOKEN: "test-token",
986
- `);i!==r&&await l(t,i)}async function $i(e){let t=Rt(e,"packages/api/tests/setup.ts"),r;try{r=await Or(t,"utf-8")}catch{return}let i=r;i=i.replace(/\tREDIS_URL:\s*"[^"]*",?\n/g,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
986
+ `);i!==r&&await p(t,i)}async function Vi(e){let t=Rt(e,"packages/api/tests/setup.ts"),r;try{r=await xr(t,"utf-8")}catch{return}let i=r;i=i.replace(/\tREDIS_URL:\s*"[^"]*",?\n/g,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
987
987
  UPSTASH_REDIS_REST_TOKEN: "test-token",
988
988
  `),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",
989
989
  UPSTASH_REDIS_REST_TOKEN: "test-token",
@@ -1004,8 +1004,8 @@ vi.mock("@upstash/lock", () => {
1004
1004
  return { Lock };
1005
1005
  });
1006
1006
 
1007
- $1`),i!==r&&await l(t,i)}import{readdir as Ui,readFile as ji,rm as Ve}from"fs/promises";import{join as ge}from"path";var Mi=new Set(["ci.yml"]),Vi=["cli","cli:clean","demo:bench","playground:regen","playground:test","playground:test:units"];async function Dr(e){let t=ge(e.projectDir,".github/workflows"),r=await Ui(t).catch(()=>[]);for(let i of r)Mi.has(i)||await Ve(ge(t,i),{recursive:!0,force:!0})}async function Cr(e){let t=ge(e.projectDir,"package.json"),r=await ji(t,"utf-8"),i=JSON.parse(r),n=!1;if(i.scripts){for(let o of Vi)o in i.scripts&&(delete i.scripts[o],n=!0);if(!qe(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," ")+`
1008
- `)}async function Nr(e){let t=e.frontend==="nextjs"?"apps/web-nuxt":"apps/web-next";await Ve(ge(e.projectDir,t),{recursive:!0,force:!0})}async function Lr(e){e.docs||await Ve(ge(e.projectDir,"apps/docs"),{recursive:!0})}async function $r(e){let t=e.frontend==="nextjs"?"docs/nuxt":"docs/next";await Ve(ge(e.projectDir,t),{recursive:!0,force:!0}),await Ve(ge(e.projectDir,"docs/index.mdx"))}import{join as Fi}from"path";async function Ur(e){let t=Bi(e);await l(Fi(e.projectDir,".github/workflows/ci.yml"),t)}function Bi(e){let t=D[e.databaseProvider].managed,r=e.cacheProvider==="upstash",i=e.frontend==="nuxt",n=t?"":` services:
1007
+ $1`),i!==r&&await p(t,i)}import{readdir as Fi,readFile as Bi,rm as Be}from"fs/promises";import{join as ge}from"path";var Gi=new Set(["ci.yml"]),Ki=["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 Fi(t).catch(()=>[]);for(let i of r)Gi.has(i)||await Be(ge(t,i),{recursive:!0,force:!0})}async function Nr(e){let t=ge(e.projectDir,"package.json"),r=await Bi(t,"utf-8"),i=JSON.parse(r),n=!1;if(i.scripts){for(let o of Ki)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 p(t,JSON.stringify(i,null," ")+`
1008
+ `)}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 zi}from"path";async function jr(e){let t=Hi(e);await p(zi(e.projectDir,".github/workflows/ci.yml"),t)}function Hi(e){let t=L[e.databaseProvider].managed,r=e.cacheProvider==="upstash",i=e.frontend==="nuxt",n=t?"":` services:
1009
1009
  postgres:
1010
1010
  image: postgres:18-alpine
1011
1011
  env:
@@ -1025,7 +1025,7 @@ $1`),i!==r&&await l(t,i)}import{readdir as Ui,readFile as ji,rm as Ve}from"fs/pr
1025
1025
  STRIPE_SECRET_KEY: test
1026
1026
  STRIPE_WEBHOOK_SECRET: test`;case"polar":return`
1027
1027
  POLAR_ACCESS_TOKEN: test
1028
- 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=>W[m].envVars.map(S=>`
1028
+ POLAR_WEBHOOK_SECRET: test`;case"none":return"";default:{let m=e.paymentProvider;throw new Error(`buildCiYaml: unhandled payment provider "${String(m)}"`)}}})(),l=e.socialProviders.map(m=>W[m].envVars.map(S=>`
1029
1029
  ${S.name}: test`).join("")).join("");return`# Deployment is handled by the hosting platform (Vercel, Coolify, etc.)
1030
1030
  # which auto-deploys on push. CI runs in parallel as a quality gate.
1031
1031
  # For PR-based workflows, enable GitHub branch protection to require CI before merging.
@@ -1071,7 +1071,7 @@ ${i?` # vue-tsc on web-nuxt OOMs on the GitHub runner's default heap once
1071
1071
  runs-on: ubuntu-latest
1072
1072
  timeout-minutes: 10
1073
1073
  ${n} env:
1074
- CONTENT_API_KEY: test-contentapi-key-16chars${s}${a}${d}
1074
+ CONTENT_API_KEY: test-contentapi-key-16chars${s}${a}${l}
1075
1075
  STORAGE_REGION: test
1076
1076
  STORAGE_ENDPOINT: http://test
1077
1077
  STORAGE_ACCESS_KEY_ID: test
@@ -1085,41 +1085,41 @@ ${n} env:
1085
1085
  run: echo "DATABASE_URL=${o}" > .env
1086
1086
 
1087
1087
  - run: pnpm test
1088
- `}import{readFile as jr}from"fs/promises";import{existsSync as Gi}from"fs";import{join as Ot}from"path";var Mr="@repo/database";function Ki(e){return e?"pnpm -F @repo/database reset && pnpm -F @repo/database push --force":"pnpm -F @repo/database migrate"}function zi(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 Hi(e,t,r){let i=await jr(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(Mr)||(n.scripts={...n.scripts,[t]:`${r} && ${o}`},await l(e,JSON.stringify(n,null," ")+`
1089
- `))}async function Yi(e,t){let r=Gi(e)?JSON.parse(await jr(e,"utf-8")):{$schema:"https://openapi.vercel.sh/vercel.json"},i=r.buildCommand?.trim()||"pnpm build";i.includes(Mr)||(r.buildCommand=`${t} && ${i}`,await l(e,JSON.stringify(r,null," ")+`
1090
- `))}async function Vr(e){let t=Ki(e.demo===!0),r=zi(e.architecture,e.frontend),i=Ot(e.projectDir,"apps",r);switch(e.deploymentTarget){case"node":await Hi(Ot(i,"package.json"),"start",t);return;case"vercel":await Yi(Ot(i,"vercel.json"),t);return;default:{let n=e.deploymentTarget;throw new Error(`generateDeployScripts: unhandled deployment target "${String(n)}"`)}}}import{readFile as Ji}from"fs/promises";import{existsSync as qi}from"fs";import{join as ft}from"path";var Wi=["stripe","polar"];async function Fr(e){let t=ft(e.projectDir,"packages/payments/src"),r=e.paymentProvider,i=`// Active payment provider. To switch, change the path below to
1088
+ `}import{readFile as Mr}from"fs/promises";import{existsSync as Yi}from"fs";import{join as Ot}from"path";var Vr="@repo/database";function Ji(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 Wi(e,t,r){let i=await Mr(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(Vr)||(n.scripts={...n.scripts,[t]:`${r} && ${o}`},await p(e,JSON.stringify(n,null," ")+`
1089
+ `))}async function Xi(e,t){let r=Yi(e)?JSON.parse(await Mr(e,"utf-8")):{$schema:"https://openapi.vercel.sh/vercel.json"},i=r.buildCommand?.trim()||"pnpm build";i.includes(Vr)||(r.buildCommand=`${t} && ${i}`,await p(e,JSON.stringify(r,null," ")+`
1090
+ `))}async function Fr(e){let t=Ji(e.demo===!0),r=qi(e.architecture,e.frontend),i=Ot(e.projectDir,"apps",r);switch(e.deploymentTarget){case"node":await Wi(Ot(i,"package.json"),"start",t);return;case"vercel":await Xi(Ot(i,"vercel.json"),t);return;default:{let n=e.deploymentTarget;throw new Error(`generateDeployScripts: unhandled deployment target "${String(n)}"`)}}}import{readFile as Zi}from"fs/promises";import{existsSync as Qi}from"fs";import{join as gt}from"path";var eo=["stripe","polar"];async function Br(e){let t=gt(e.projectDir,"packages/payments/src"),r=e.paymentProvider,i=`// Active payment provider. To switch, change the path below to
1091
1091
  // "./polar/index" or "./none/index" (other folders are kept in place).
1092
1092
  export { ops } from "./${r}/index";
1093
- `;await l(ft(t,"providers/index.ts"),i),await l(ft(t,"index.ts"),await Xi(t,r))}async function Xi(e,t){let r=ft(e,"index.ts");if(!qi(r))throw new Error(`generatePaymentBarrel: expected ${r} to exist - did packages/payments move?`);let i=await Ji(r,"utf-8"),n=Wi.filter(s=>s!==t);return i.split(`
1093
+ `;await p(gt(t,"providers/index.ts"),i),await p(gt(t,"index.ts"),await to(t,r))}async function to(e,t){let r=gt(e,"index.ts");if(!Qi(r))throw new Error(`generatePaymentBarrel: expected ${r} to exist - did packages/payments move?`);let i=await Zi(r,"utf-8"),n=eo.filter(s=>s!==t);return i.split(`
1094
1094
  `).filter(s=>!n.some(a=>s.includes(`./providers/${a}/`))).join(`
1095
- `)}import{readdir as Zi,readFile as Qi}from"fs/promises";import{join as Br}from"path";async function Gr(e){if(e.demo)return;let t=e.appName.trim()||e.projectName,r=JSON.stringify(t).slice(1,-1),i=Br(e.projectDir,"packages/i18n/translations"),n;try{n=await Zi(i)}catch{return}for(let o of n){let s=Br(i,o,"web.json"),a;try{a=await Qi(s,"utf-8")}catch{continue}let d=a.replaceAll("GenerateSaaS",r);d!==a&&await l(s,d)}}import{readFile as eo}from"fs/promises";import{join as Kr}from"path";var to=[".nuxt/",".nuxt",".nitro/",".nitro",".output/",".output","_locales/"],ro=[".next/",".next",".svelte-kit/",".svelte-kit",".wrangler/",".wrangler",".dev.vars"];async function Hr(e){let r=e.frontend==="nuxt"?ro:to;await zr(Kr(e.projectDir,".gitignore"),r),await zr(Kr(e.projectDir,".dockerignore"),r)}async function zr(e,t){let r;try{r=await eo(e,"utf-8")}catch{return}let i=new Set(t),n=r.split(`
1095
+ `)}import{readdir as ro,readFile as no}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),i=Gr(e.projectDir,"packages/i18n/translations"),n;try{n=await ro(i)}catch{return}for(let o of n){let s=Gr(i,o,"web.json"),a;try{a=await no(s,"utf-8")}catch{continue}let l=a.replaceAll("GenerateSaaS",r);l!==a&&await p(s,l)}}import{readFile as io}from"fs/promises";import{join as zr}from"path";var oo=[".nuxt/",".nuxt",".nitro/",".nitro",".output/",".output","_locales/"],so=[".next/",".next",".svelte-kit/",".svelte-kit",".wrangler/",".wrangler",".dev.vars"];async function Yr(e){let r=e.frontend==="nuxt"?so:oo;await Hr(zr(e.projectDir,".gitignore"),r),await Hr(zr(e.projectDir,".dockerignore"),r)}async function Hr(e,t){let r;try{r=await io(e,"utf-8")}catch{return}let i=new Set(t),n=r.split(`
1096
1096
  `).filter(o=>!i.has(o.trim()));n.length!==r.split(`
1097
- `).length&&await l(e,n.join(`
1098
- `))}async function gt(e){let t=e.projectDir;e.demo||await ir(t),await or(t,e.aiTools),await sr(t,e.frontend),await cr(e),await lr(e),await pr(e),e.demo||await dr(e),await fr(e);let r=await gr(e);return await hr(e),await yr(e),await vr(e),await Sr(e),await Er(e),await wr(e),await Ir(e),await Pr(e),await Rr(e),await xr(e),await Dr(e),await Ur(e),await Cr(e),await Nr(e),await Lr(e),await $r(e),await Vr(e),await Fr(e),await Gr(e),await Hr(e),{dockerComposeGenerated:r}}import{basename as Jr,join as qr,relative as io}from"path";import{createHash as Yr}from"crypto";import{readFile as no}from"fs/promises";async function ht(e){let t=await no(e);return Yr("sha256").update(t).digest("hex")}function xt(e){return Yr("sha256").update(e).digest("hex")}var oo=new Set(["data",$]);function so(e){let t=e.split("/");for(let r of t)if(Tt.has(r)||oo.has(r)||r.startsWith(".env")&&!r.includes("example"))return!0;return!1}function Wr(e,t){return{projectName:e.projectName??Jr(t),appName:e.appName??e.projectName??Jr(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 Xr(e,t){let i=(await fe(e.projectDir,e.projectDir,so)).sort(),n=await Promise.all(i.map(async a=>[io(e.projectDir,a),await ht(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," ")+`
1099
- `),await l(qr(e.projectDir,Yt),JSON.stringify(o,null," ")+`
1100
- `)}import{relative as ao}from"path";async function Fe(e){let r=(await fe(e,e,Te)).sort(),i=await Promise.all(r.map(async n=>[ao(e,n),await ht(n)]));return Object.fromEntries(i)}import{copyFile as co,mkdir as lo,rm as po}from"fs/promises";import{dirname as uo,join as Zr,relative as mo}from"path";import{existsSync as fo}from"fs";async function Dt(e,t){fo(t)&&await po(t,{recursive:!0,force:!0});let r=await fe(e,e,Te);for(let i of r){let n=mo(e,i),o=Zr(t,n);await lo(uo(o),{recursive:!0}),await co(i,o)}}async function Qr(e,t){await Dt(e,Zr(t,at))}import{existsSync as go}from"fs";import{readFile as en,readdir as ho}from"fs/promises";import{join as K,dirname as yo,resolve as vo,sep as So}from"path";import{fileURLToPath as Eo}from"url";var Be={"claude-code":".claude/skills",cursor:".cursor/skills",codex:".agents/skills","gemini-cli":".gemini/skills",windsurf:".windsurf/skills"},dl=Object.values(Be),Ct="generatesaas-update",tn=yo(Eo(import.meta.url));function wo(){let e=K(tn,"skill","content");return go(e)?e:K(tn,"content")}function Nt(e){return!e||e.length===0?[]:e.map(t=>Be[t])}async function Lt(e,t,r,i){let n=Nt(i);for(let o of n){let s=K(e,o,Ct),a=K(s,"scripts"),d=K(s,"references");await dt(a),await dt(d),await l(K(s,"SKILL.md"),t.replaceAll("__SKILL_ROOT__",o)),await l(K(d,".gitkeep"),"");for(let[h,g]of Object.entries(r)){let m=vo(a,h);m.startsWith(a+So)&&await l(m,g)}}}async function rn(e,t){let r=wo(),i=await en(K(r,"SKILL.md"),"utf-8"),n=K(r,"scripts"),o=await ho(n),s={};for(let a of o)a!==".gitkeep"&&(s[a]=await en(K(n,a),"utf-8"));await Lt(e,i,s,t)}import{execFile as bo,execFileSync as To}from"child_process";import{access as nn,readFile as Ao}from"fs/promises";import{join as $t}from"path";import*as A from"@clack/prompts";function ye(e){try{let t=process.platform==="win32"?"where":"which";return To(t,[e],{stdio:"ignore"}),!0}catch{return!1}}function he(e,t,r,i=3e5){return new Promise((n,o)=>{bo(e,t,{cwd:r,timeout:i},(s,a,d)=>{if(s){let h=String(a||"").trim(),m=[String(d||"").trim(),h].filter(Boolean).join(`
1097
+ `).length&&await p(e,n.join(`
1098
+ `))}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 kr(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 qr,join as Wr,relative as co}from"path";import{createHash as Jr}from"crypto";import{readFile as ao}from"fs/promises";async function yt(e){let t=await ao(e);return Jr("sha256").update(t).digest("hex")}function xt(e){return Jr("sha256").update(e).digest("hex")}var lo=new Set(["data",U]);function po(e){let t=e.split("/");for(let r of t)if(At.has(r)||lo.has(r)||r.startsWith(".env")&&!r.includes("example"))return!0;return!1}function Xr(e,t){return{projectName:e.projectName??qr(t),appName:e.appName??e.projectName??qr(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 i=(await fe(e.projectDir,e.projectDir,po)).sort(),n=await Promise.all(i.map(async a=>[co(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 p(Wr(e.projectDir,Q),JSON.stringify(s,null," ")+`
1099
+ `),await p(Wr(e.projectDir,Yt),JSON.stringify(o,null," ")+`
1100
+ `)}import{relative as uo}from"path";async function Ge(e){let r=(await fe(e,e,Ae)).sort(),i=await Promise.all(r.map(async n=>[uo(e,n),await yt(n)]));return Object.fromEntries(i)}import{copyFile as mo,mkdir as fo,rm as go}from"fs/promises";import{dirname as ho,join as Qr,relative as yo}from"path";import{existsSync as vo}from"fs";async function Dt(e,t){vo(t)&&await go(t,{recursive:!0,force:!0});let r=await fe(e,e,Ae);for(let i of r){let n=yo(e,i),o=Qr(t,n);await fo(ho(o),{recursive:!0}),await mo(i,o)}}async function en(e,t){await Dt(e,Qr(t,ct))}import{existsSync as So}from"fs";import{readFile as tn,readdir as Eo}from"fs/promises";import{join as z,dirname as wo,resolve as bo,sep as Ao}from"path";import{fileURLToPath as To}from"url";var Ke={"claude-code":".claude/skills",cursor:".cursor/skills",codex:".agents/skills","gemini-cli":".gemini/skills",windsurf:".windsurf/skills"},yl=Object.values(Ke),Ct="generatesaas-update",rn=wo(To(import.meta.url));function Io(){let e=z(rn,"skill","content");return So(e)?e:z(rn,"content")}function Nt(e){return!e||e.length===0?[]:e.map(t=>Ke[t])}async function Lt(e,t,r,i){let n=Nt(i);for(let o of n){let s=z(e,o,Ct),a=z(s,"scripts"),l=z(s,"references");await ut(a),await ut(l),await p(z(s,"SKILL.md"),t.replaceAll("__SKILL_ROOT__",o)),await p(z(l,".gitkeep"),"");for(let[f,h]of Object.entries(r)){let m=bo(a,f);m.startsWith(a+Ao)&&await p(m,h)}}}async function nn(e,t){let r=Io(),i=await tn(z(r,"SKILL.md"),"utf-8"),n=z(r,"scripts"),o=await Eo(n),s={};for(let a of o)a!==".gitkeep"&&(s[a]=await tn(z(n,a),"utf-8"));await Lt(e,i,s,t)}import{execFile as ko,execFileSync as Po}from"child_process";import{access as on,readFile as _o}from"fs/promises";import{join as $t}from"path";import*as k from"@clack/prompts";function ye(e){try{let t=process.platform==="win32"?"where":"which";return Po(t,[e],{stdio:"ignore"}),!0}catch{return!1}}function he(e,t,r,i=3e5){return new Promise((n,o)=>{ko(e,t,{cwd:r,timeout:i},(s,a,l)=>{if(s){let f=String(a||"").trim(),m=[String(l||"").trim(),f].filter(Boolean).join(`
1101
1101
  `);o(new Error(m?`${s.message}
1102
- ${m}`:s.message))}else n()})})}async function on(e){if(!ye("pnpm"))return A.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 A.log.warn(`Lockfile regeneration failed: ${r}`),A.log.warn("Deploys using --frozen-lockfile may fail."),!1}}async function sn(e){if(!ye("pnpm"))return A.log.warn("pnpm not found. Skipping dependency installation."),A.log.info("Install pnpm: https://pnpm.io/installation"),!1;let t=A.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 i=r instanceof Error?r.message:String(r);return A.log.warn(`pnpm install failed: ${i}`),A.log.warn("You can run it manually later."),!1}}async function an(e){if(!ye("pnpm"))return!1;let t=A.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 i=r instanceof Error?r.message:String(r);return A.log.warn(`Could not generate baseline migration: ${i}`),A.log.warn("Run 'pnpm -F @repo/database generate' before your first deploy."),!1}}async function cn(e){try{return await nn($t(e,".git")),A.log.info("Git repository already exists, skipping init."),!0}catch{}if(!ye("git"))return A.log.warn("git not found. Skipping repository initialization."),!1;let t=A.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."),A.log.warn("You can run git init manually later."),!1}}async function ln(e){if(!ye("pnpm"))return!1;try{await nn($t(e,".git"))}catch{return!1}try{let t=JSON.parse(await Ao($t(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 he("pnpm",["exec","simple-git-hooks"],e),!0}catch{return A.log.warn("Could not install git hooks. Run 'pnpm exec simple-git-hooks' manually."),!1}}import*as Re from"@clack/prompts";import U from"picocolors";function pn(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 ${U.dim(`# ${o}`)}`)}if(r.push(`pnpm dev ${U.dim("# http://localhost:3000")}`),t.skippedCredentials.length>0&&(r.push(""),r.push(U.dim("Fill in remaining TODO values in .env"))),Re.note(r.join(`
1103
- `),U.yellow("Start Development")),t.dockerComposeGenerated){let o=[];o.push(`App ${U.cyan("http://localhost:3000")}`),e.architecture==="separate"&&o.push(`API ${U.cyan("http://localhost:3010")}`),e.dockerServices.includes("mailpit")&&o.push(`Mailpit ${U.cyan("http://localhost:8025")}`),e.dockerServices.includes("inngest")&&o.push(`Inngest ${U.cyan("http://localhost:8288")}`),Re.note(o.join(`
1104
- `),U.yellow("Dev Tools"))}let i=[],n=Io(e);n.length>0&&i.push(`Set in production: ${U.dim(n.join(", "))}`),i.push("pnpm db:push # Run database migrations"),i.push(Po(e)),Re.note(i.join(`
1105
- `),U.yellow("Deployment"))}function Io(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 Po(e){switch(e.deploymentTarget){case"node":return"Deploy with Docker or your preferred Node.js host";case"vercel":return"vercel deploy # Deploy to Vercel"}}function dn(e){let t={};if(e.name!==void 0){if(!it(e.name))throw new Error(`Invalid project name "${e.name}". Use lowercase letters, numbers, and hyphens only. Must start with a letter.`);t.projectName=e.name}if(e.appName!==void 0){if(!e.appName.trim())throw new Error("App name cannot be empty.");t.appName=e.appName}if(e.location!==void 0?t.projectDir=e.location==="."?process.cwd():e.location:t.projectName!==void 0&&(t.projectDir=`./${t.projectName}`),e.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=Ut(e.docker,Qe,"docker service")),e.aiTools!==void 0&&(t.aiTools=Ut(e.aiTools,et,"AI tool")),e.socialProviders!==void 0&&(t.socialProviders=Ut(e.socialProviders,rt,"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 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 un(e){let t=e.projectName??ne.projectName,r=e.projectDir??`./${t}`,i=e.appName??nt(t),n=e.deploymentTarget??ne.deploymentTarget,o=F[n]?.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:i,projectDir:r,deploymentTarget:n,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 Ut(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 Do from"picocolors";var Co="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";function No(e){if(e===void 0)return;let t=e.trim().replace(/^v/,"");if(!/^\d+\.\d+\.\d+$/.test(t))throw new Error(`Invalid template version "${e}". Use semver like 1.2.3.`);return t}function mn(e){e.command("init").description("Scaffold a new GenerateSaaS project").argument("[apiKey]","license key (same as --api-key)").option("-n, --name <name>","project name (lowercase, hyphens, starts with letter)").option("--app-name <name>","display name for the app").option("-l, --location <path>","project directory (default: ./{name})").addOption(new j("--frontend <type>","frontend framework").choices([...Ue])).addOption(new j("--architecture <type>","fullstack or separate").choices([...We])).addOption(new j("--payment <provider>","payment provider").choices([...Xe])).addOption(new j("--email <provider>","email provider").choices([...Ze])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").addOption(new j("--billing-scope <scope>","billing scope (requires --org)").choices([...tt])).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 j("--currency <code>","default currency for billing").choices([...ce])).addOption(new j("--deploy <target>","deployment target").choices([...le])).addOption(new j("--database <provider>","database provider").choices([...pe])).addOption(new j("--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 j("--demo","first-party demo build: keep sample content, mark site non-indexable - requires CI API key").hideHelp()).addOption(new j("--no-db-migration","skip generating the baseline DB migration (internal: demos/CI/playground)").hideHelp()).action(async(t,r)=>{await Lo(t?{...r,apiKey:t}:r)})}async function Lo(e){let t=performance.now();Gt("1.7.0");let r,i;try{r=dn(e),i=No(e.templateVersion)}catch(y){E.cancel(I(y)),process.exit(1)}let n=E.spinner(),o;try{o=await be({apiKey:e.apiKey,prompt:!e.yes})}catch(y){E.cancel(I(y)),process.exit(1)}e.demo&&xt(o)!==Co&&(E.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let s=Z(o),a=async()=>{let y=await re(s),k=y.latest,Y=i??k;if(i&&!y.versions.some(J=>J.version===Y))throw new Error(`Template version "${i}" is not available.`);return{latestVersion:k,selectedVersion:Y}};n.start("Verifying access...");let d,h;try{({latestVersion:d,selectedVersion:h}=await a()),n.stop("Access verified."),me(o)}catch(y){if(n.stop("Access verification failed."),y instanceof _&&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 je(),s=Z(o),n.start("Verifying access...");try{({latestVersion:d,selectedVersion:h}=await a()),n.stop("Access verified."),me(o)}catch(k){n.stop("Access verification failed."),E.cancel(k instanceof _&&k.status===401?"Invalid API key.":I(k)),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=un(r):g=await Ht(r);let m;n.start("Activating license...");try{let y=crypto.randomUUID(),k=()=>({frontend:g.frontend,version:h,installId:y,projectName:g.projectName,options:Zt(g)}),Y;try{Y=await bt(s,k())}catch(J){let q=ct(J);if(!q?.lastAllowedVersion)throw J;n.stop("License activation failed."),e.yes&&(E.cancel(`${q.message} Re-run with --template-version ${q.lastAllowedVersion}.`),process.exit(1));let Ee=await E.confirm({message:`Your update window has ended. Continue with v${q.lastAllowedVersion} (the last version your license covers)?`});(E.isCancel(Ee)||!Ee)&&(E.cancel("Setup cancelled."),process.exit(0)),h=q.lastAllowedVersion,n.start(`Activating license for v${h}...`),Y=await bt(s,k())}m={token:Y.token,keyHash:xt(o),installId:y},n.stop("License activated.")}catch(y){n.stop("License activation failed."),E.cancel(I(y)),process.exit(1)}let S=xo(g.projectDir);if(ko(S)&&_o(S).length>0)if(e.yes)E.log.info(`Directory ${S} is not empty. Merging (keeping existing files, overwriting conflicts).`);else{let k=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(k)||k==="cancel")&&(E.cancel("Setup cancelled."),process.exit(0)),k==="overwrite"&&Ro(S,{recursive:!0,force:!0})}let b={...g,projectDir:S,version:h,...e.demo?{docs:!1}:{}};n.start("Downloading template...");try{await lt(s,h,S),n.stop("Template downloaded.")}catch(y){n.stop("Download failed."),E.cancel(I(y)),process.exit(1)}let H;n.start("Generating project files...");try{if({dockerComposeGenerated:H}=await gt(b),!e.demo){let y=await Fe(S);await l(Oo(S,st),JSON.stringify(y,null," ")+`
1106
- `),await Qr(S,S)}await rn(S,b.aiTools),await Xr(b,m),n.stop("Project files generated.")}catch(y){n.stop("Generation failed."),E.cancel(I(y)),process.exit(1)}await on(S);let R=await sn(S);R&&b.demo!==!0&&e.dbMigration!==!1&&await an(S),await cn(S),R&&await ln(S);let ie=ye("docker"),T=Et(b).map(y=>y.key).filter(y=>!b.credentials?.[y]);pn(b,{pnpmInstalled:R,dockerComposeGenerated:H,dockerAvailable:ie,skippedCredentials:T}),Kt(),E.log.info(Do.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as fn}from"fs";import{readFile as Vo}from"fs/promises";import{join as Ge,resolve as Fo}from"path";import*as P from"@clack/prompts";import Oe from"picocolors";import{mkdtemp as $o,rm as Uo}from"fs/promises";import{tmpdir as jo}from"os";import{join as Mo}from"path";async function jt(e,t,r,i){let n=await $o(Mo(jo(),"generatesaas-stage-"));try{await lt(e,t,n),await gt({...r,projectDir:n}),await Dt(n,i)}finally{await Uo(n,{recursive:!0,force:!0})}}function gn(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=Fo(t.cwd??process.cwd()),i=Ge(r,Q),n;try{n=JSON.parse(await Vo(i,"utf-8"))}catch{P.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let o;try{o=await be()}catch(d){P.cancel(I(d)),process.exit(1)}let s=Z(o),a=P.spinner();try{a.start("Verifying access...");let d;try{d=await re(s)}catch(T){throw T instanceof _&&T.status===401?new Error("Your saved API key was rejected. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):T}a.stop("Access verified."),me(o),a.start("Fetching latest skill files...");let h=await Xt(s,d.latest);await Lt(r,h.skillMd,h.scripts,n.aiTools);let g=Nt(n.aiTools);if(a.stop("Skills updated."),P.log.success(`Skill files installed to ${Oe.cyan(g.length.toString())} locations.`),n.version===d.latest){P.log.info(`Already on the latest version (${n.version}).`);return}if(n.licenseToken)try{let T=await Qt(s,{currentToken:n.licenseToken,newVersion:d.latest});n.licenseToken=T.token,await l(i,JSON.stringify(n,null," ")+`
1107
- `),P.log.success("License refreshed.")}catch(T){let y=ct(T);y&&(P.cancel(y.message),process.exit(1)),P.log.warn("License refresh skipped.")}let m=Wr(n,r),S=Ge(r,Jt);a.start(`Staging v${d.latest} (shaped for your config)...`),await jt(s,d.latest,m,S),a.stop("Template staged.");let b=await Wt(s,d.latest);b&&P.note(b,`Changelog v${d.latest}`);let H=Ge(r,st),R=Ge(r,at),ie=!fn(R),oe=!fn(H);if(ie){if(a.start("Building baseline template (one-time migration)..."),await jt(s,n.version,m,R),oe){let T=await Fe(R);await l(H,JSON.stringify(T,null," ")+`
1108
- `)}a.stop("Baseline template stored.")}else if(oe){a.start("Computing baseline template hashes...");let T=await Fe(R);await l(H,JSON.stringify(T,null," ")+`
1109
- `),a.stop("Baseline hashes computed.")}if(await l(Ge(r,qt),JSON.stringify({currentVersion:n.version,targetVersion:d.latest,changelog:b,stagedAt:new Date().toISOString()},null," ")+`
1110
- `),P.log.info(`Update staged: ${Oe.cyan(n.version)} \u2192 ${Oe.cyan(d.latest)}`),n.aiTools&&n.aiTools.length>0){let T=n.aiTools[0],y=Ne[T].label;P.log.info(`Open your project in ${Oe.cyan(y)} and ask: ${Oe.cyan("'update my GenerateSaaS project'")}`)}else P.log.info(`Ask your AI coding assistant to ${Oe.cyan("'update my GenerateSaaS project'")}.`)}catch(d){a.stop("Failed."),P.cancel(`Update failed: ${I(d)}`),process.exit(1)}})}import*as C from"@clack/prompts";import M from"picocolors";import{readFile as Bo}from"fs/promises";import{join as Go,resolve as Ko}from"path";function hn(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=Ko(t.cwd??process.cwd()),i=Go(r,Q),n;try{n=JSON.parse(await Bo(i,"utf-8"))}catch{C.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(`
1111
- `);C.note(o,M.bold("Project Status"));let s=C.spinner();s.start("Checking for updates...");try{let a=await be(),d=Z(a),g=(await re(d)).latest;n.version===g?(s.stop("Up to date."),C.log.success(`Already on the latest version (${M.green(g)})`)):(s.stop("Update available."),C.log.warning(`Update available: ${M.yellow(n.version)} \u2192 ${M.green(g)}`),C.log.info(`Run ${M.cyan("generatesaas update")} to update skill files, then ask your AI assistant to apply the update.`))}catch(a){s.stop("Check failed."),a instanceof _&&a.status===401?C.log.warning("Invalid API key. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):C.log.warning(`Could not check for updates: ${I(a)}`)}})}import{readFile as zo}from"fs/promises";import*as N from"@clack/prompts";import z from"picocolors";function Ho(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 yn(e){N.note([`License ID: ${z.cyan(String(e.lid??"unknown"))}`,`Version: ${z.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(`
1112
- `),z.yellow("License Details"))}async function vn(e){let t=N.spinner(),r=e.replace(/\/+$/,"");t.start(`Checking ${r}/license...`);let i;try{let o=await fetch(`${r}/license`);if(!o.ok)return t.stop(`${z.red("Not found")} - ${r}/license returned ${o.status}`),!1;if(i=await o.text(),!i||i.split(".").length!==3)return t.stop(`${z.red("Invalid")} - response is not a JWT`),!1;t.stop(`${z.green("Found")} - license endpoint responded`)}catch(o){return t.stop(`${z.red("Failed")} - ${I(o)}`),!1}let n;try{n=Ho(i)}catch{return N.log.error("Could not decode JWT payload."),!1}t.start("Verifying signature...");try{let o=process.env.GENERATESAAS_API_URL??ot,s=await er(o,i);if(s.valid)t.stop(`${z.green("Valid")} - signature verified`);else return t.stop(`${z.red("Invalid")} - ${s.reason}`),!1}catch{return t.stop(`${z.yellow("Skipped")} - could not reach verification service`),N.log.warn("Signature not verified. Displaying unverified claims:"),yn(n),!1}return yn(n),!0}function Sn(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/api)").option("--file <path>","file with URLs to check, one per line").action(async(t,r)=>{if(!t&&!r.file&&(N.cancel("Provide a URL or --file <path>."),process.exit(1)),r.file){let n=(await zo(r.file,"utf-8")).split(`
1113
- `).map(s=>s.trim()).filter(s=>s&&!s.startsWith("#"));n.length===0&&(N.cancel("No URLs found in file."),process.exit(1));let o=0;for(let s of n)await vn(s)&&o++,N.log.info("");N.log.success(`${o}/${n.length} sites verified.`)}else await vn(t)||process.exit(1)})}import{existsSync as Yo,rmSync as Jo}from"fs";import*as V from"@clack/prompts";function En(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){Yo(X)?(Jo(X),V.log.success("API key removed.")):V.log.info("No API key configured.");return}let r=wt();r?V.log.info(`Current API key: ****${r.slice(-4)}`):V.log.info("No API key configured.");let i=await je(),n=Z(i),o=V.spinner();o.start("Verifying API key...");try{await re(n),o.stop("API key verified."),me(i),V.log.success("API key saved.")}catch(s){o.stop("Verification failed."),s instanceof _&&s.status===401?V.cancel("Invalid API key."):V.cancel(I(s)),process.exit(1)}})}import{existsSync as yt,rmSync as qo,readFileSync as Ft,writeFileSync as wn}from"fs";import{join as ve}from"path";import*as L from"@clack/prompts";var Wo=["packages/api/src/functions/maintenance/license-heartbeat.ts","packages/api/src/lib/manifest.ts","packages/api/src/routes/internal/license.ts"],Xo=[{file:"packages/api/src/routes/inngest.ts",removals:[`import { licenseHeartbeatFunction } from "../functions/maintenance/license-heartbeat";
1102
+ ${m}`:s.message))}else n()})})}async function sn(e){if(!ye("pnpm"))return k.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 k.log.warn(`Lockfile regeneration failed: ${r}`),k.log.warn("Deploys using --frozen-lockfile may fail."),!1}}async function an(e){if(!ye("pnpm"))return k.log.warn("pnpm not found. Skipping dependency installation."),k.log.info("Install pnpm: https://pnpm.io/installation"),!1;let t=k.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 i=r instanceof Error?r.message:String(r);return k.log.warn(`pnpm install failed: ${i}`),k.log.warn("You can run it manually later."),!1}}async function cn(e){if(!ye("pnpm"))return!1;let t=k.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 i=r instanceof Error?r.message:String(r);return k.log.warn(`Could not generate baseline migration: ${i}`),k.log.warn("Run 'pnpm -F @repo/database generate' before your first deploy."),!1}}async function ln(e){try{return await on($t(e,".git")),k.log.info("Git repository already exists, skipping init."),!0}catch{}if(!ye("git"))return k.log.warn("git not found. Skipping repository initialization."),!1;let t=k.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."),k.log.warn("You can run git init manually later."),!1}}async function pn(e){if(!ye("pnpm"))return!1;try{await on($t(e,".git"))}catch{return!1}try{let t=JSON.parse(await _o($t(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 he("pnpm",["exec","simple-git-hooks"],e),!0}catch{return k.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(`
1103
+ `),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(`
1104
+ `),j.yellow("Dev Tools"))}let i=[],n=Ro(e);n.length>0&&i.push(`Set in production: ${j.dim(n.join(", "))}`),i.push("pnpm db:push # Run database migrations"),i.push(Oo(e)),Re.note(i.join(`
1105
+ `),j.yellow("Deployment"))}function Ro(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 Oo(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=Ut(e.docker,tt,"docker service")),e.aiTools!==void 0&&(t.aiTools=Ut(e.aiTools,rt,"AI tool")),e.socialProviders!==void 0&&(t.socialProviders=Ut(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 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 mn(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),l=e.emailProvider??(o?"resend":ne.emailProvider),f=e.dockerServices??(o?ne.dockerServices.filter(m=>m!=="postgres"&&m!=="redis"):ne.dockerServices),h={...ne,...e,projectName:t,appName:i,projectDir:r,deploymentTarget:n,databaseProvider:s,cacheProvider:a,emailProvider:l,dockerServices:f};h.paymentProvider==="none"&&(h.credits=!1);for(let m of Le){if(h.deploymentTarget!==m.target)continue;let S=h.databaseProvider===m.provider?"database":"cache";if(h.databaseProvider===m.provider||h.cacheProvider===m.provider)throw new Error(`Incompatible: --deploy ${m.target} + --${S} ${m.provider}. ${m.reason}`)}for(let m of $e)if(h.architecture===m.architecture&&h.deploymentTarget===m.target)throw new Error(`Incompatible: --architecture ${m.architecture} + --deploy ${m.target}. ${m.reason}`);return h}function Ut(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 $o from"picocolors";var Uo="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";function jo(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 M("--frontend <type>","frontend framework").choices([...Ue])).addOption(new M("--architecture <type>","fullstack or separate").choices([...Ze])).addOption(new M("--payment <provider>","payment provider").choices([...Qe])).addOption(new M("--email <provider>","email provider").choices([...et])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").addOption(new M("--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 M("--currency <code>","default currency for billing").choices([...ce])).addOption(new M("--deploy <target>","deployment target").choices([...le])).addOption(new M("--database <provider>","database provider").choices([...pe])).addOption(new M("--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 M("--demo","first-party demo build: keep sample content, mark site non-indexable - requires CI API key").hideHelp()).addOption(new M("--no-db-migration","skip generating the baseline DB migration (internal: demos/CI/playground)").hideHelp()).action(async(t,r)=>{await Mo(t?{...r,apiKey:t}:r)})}async function Mo(e){let t=performance.now();Gt("1.8.0");let r,i;try{r=un(e),i=jo(e.templateVersion)}catch(y){E.cancel(I(y)),process.exit(1)}let n=E.spinner(),o;try{o=await be({apiKey:e.apiKey,prompt:!e.yes})}catch(y){E.cancel(I(y)),process.exit(1)}e.demo&&xt(o)!==Uo&&(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=i??R;if(i&&!y.versions.some(J=>J.version===Y))throw new Error(`Template version "${i}" is not available.`);return{latestVersion:R,selectedVersion:Y}};n.start("Verifying access...");let l,f;try{({latestVersion:l,selectedVersion:f}=await a()),n.stop("Access verified."),me(o)}catch(y){if(n.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 Ve(),s=Z(o),n.start("Verifying access...");try{({latestVersion:l,selectedVersion:f}=await a()),n.stop("Access verified."),me(o)}catch(R){n.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: ${l}`),f!==l&&E.log.success(`Using template version: ${f}`);let h;e.yes?h=mn(r):h=await Ht(r);let m;n.start("Activating license...");try{let y=crypto.randomUUID(),R=()=>({frontend:h.frontend,version:f,installId:y,projectName:h.projectName,options:Zt(h)}),Y;try{Y=await bt(s,R())}catch(J){let q=lt(J);if(!q?.lastAllowedVersion)throw J;n.stop("License activation failed."),e.yes&&(E.cancel(`${q.message} Re-run with --template-version ${q.lastAllowedVersion}.`),process.exit(1));let Ee=await E.confirm({message:`Your update window has ended. Continue with v${q.lastAllowedVersion} (the last version your license covers)?`});(E.isCancel(Ee)||!Ee)&&(E.cancel("Setup cancelled."),process.exit(0)),f=q.lastAllowedVersion,n.start(`Activating license for v${f}...`),Y=await bt(s,R())}m={token:Y.token,keyHash:xt(o),installId:y},n.stop("License activated.")}catch(y){n.stop("License activation failed."),E.cancel(I(y)),process.exit(1)}let S=Lo(h.projectDir);if(xo(S)&&Do(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"&&Co(S,{recursive:!0,force:!0})}let A={...h,projectDir:S,version:f,...e.demo?{docs:!1}:{}};n.start("Downloading template...");try{await pt(s,f,S),n.stop("Template downloaded.")}catch(y){n.stop("Download failed."),E.cancel(I(y)),process.exit(1)}let H;n.start("Generating project files...");try{if({dockerComposeGenerated:H}=await ht(A),!e.demo){let y=await Ge(S);await p(No(S,at),JSON.stringify(y,null," ")+`
1106
+ `),await en(S,S)}await nn(S,A.aiTools),await Zr(A,m),n.stop("Project files generated.")}catch(y){n.stop("Generation failed."),E.cancel(I(y)),process.exit(1)}await sn(S);let D=await an(S);D&&A.demo!==!0&&e.dbMigration!==!1&&await cn(S),await ln(S),D&&await pn(S);let ie=ye("docker"),T=wt(A).map(y=>y.key).filter(y=>!A.credentials?.[y]);dn(A,{pnpmInstalled:D,dockerComposeGenerated:H,dockerAvailable:ie,skippedCredentials:T}),Kt(),E.log.info($o.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as gn}from"fs";import{readFile as Ko}from"fs/promises";import{join as ze,resolve as zo}from"path";import*as _ from"@clack/prompts";import Oe from"picocolors";import{mkdtemp as Vo,rm as Fo}from"fs/promises";import{tmpdir as Bo}from"os";import{join as Go}from"path";async function jt(e,t,r,i){let n=await Vo(Go(Bo(),"generatesaas-stage-"));try{await pt(e,t,n),await ht({...r,projectDir:n}),await Dt(n,i)}finally{await Fo(n,{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=zo(t.cwd??process.cwd()),i=ze(r,Q),n;try{n=JSON.parse(await Ko(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(l){_.cancel(I(l)),process.exit(1)}let s=Z(o),a=_.spinner();try{a.start("Verifying access...");let l;try{l=await re(s)}catch(T){throw T instanceof O&&T.status===401?new Error("Your saved API key was rejected. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):T}a.stop("Access verified."),me(o),a.start("Fetching latest skill files...");let f=await Xt(s,l.latest);await Lt(r,f.skillMd,f.scripts,n.aiTools);let h=Nt(n.aiTools);if(a.stop("Skills updated."),_.log.success(`Skill files installed to ${Oe.cyan(h.length.toString())} locations.`),n.version===l.latest){_.log.info(`Already on the latest version (${n.version}).`);return}if(n.licenseToken)try{let T=await Qt(s,{currentToken:n.licenseToken,newVersion:l.latest});n.licenseToken=T.token,await p(i,JSON.stringify(n,null," ")+`
1107
+ `),_.log.success("License refreshed.")}catch(T){let y=lt(T);y&&(_.cancel(y.message),process.exit(1)),_.log.warn("License refresh skipped.")}let m=Xr(n,r),S=ze(r,Jt);a.start(`Staging v${l.latest} (shaped for your config)...`),await jt(s,l.latest,m,S),a.stop("Template staged.");let A=await Wt(s,l.latest);A&&_.note(A,`Changelog v${l.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 jt(s,n.version,m,D),oe){let T=await Ge(D);await p(H,JSON.stringify(T,null," ")+`
1108
+ `)}a.stop("Baseline template stored.")}else if(oe){a.start("Computing baseline template hashes...");let T=await Ge(D);await p(H,JSON.stringify(T,null," ")+`
1109
+ `),a.stop("Baseline hashes computed.")}if(await p(ze(r,qt),JSON.stringify({currentVersion:n.version,targetVersion:l.latest,changelog:A,stagedAt:new Date().toISOString()},null," ")+`
1110
+ `),_.log.info(`Update staged: ${Oe.cyan(n.version)} \u2192 ${Oe.cyan(l.latest)}`),n.aiTools&&n.aiTools.length>0){let T=n.aiTools[0],y=Ne[T].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(l){a.stop("Failed."),_.cancel(`Update failed: ${I(l)}`),process.exit(1)}})}import*as $ from"@clack/prompts";import V from"picocolors";import{readFile as Ho}from"fs/promises";import{join as Yo,resolve as Jo}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=Jo(t.cwd??process.cwd()),i=Yo(r,Q),n;try{n=JSON.parse(await Ho(i,"utf-8"))}catch{$.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let o=[`Version: ${V.cyan(n.version)}`,`Frontend: ${V.cyan(n.frontend)}`,n.deploymentTarget?`Deploy target: ${V.cyan(n.deploymentTarget)}`:null,n.databaseProvider?`Database: ${V.cyan(n.databaseProvider)}`:null,n.cacheProvider?`Cache: ${V.cyan(n.cacheProvider)}`:null,n.aiTools&&n.aiTools.length>0?`AI tools: ${V.cyan(n.aiTools.join(", "))}`:null].filter(Boolean).join(`
1111
+ `);$.note(o,V.bold("Project Status"));let s=$.spinner();s.start("Checking for updates...");try{let a=await be(),l=Z(a),h=(await re(l)).latest;n.version===h?(s.stop("Up to date."),$.log.success(`Already on the latest version (${V.green(h)})`)):(s.stop("Update available."),$.log.warning(`Update available: ${V.yellow(n.version)} \u2192 ${V.green(h)}`),$.log.info(`Run ${V.cyan("generatesaas update")} to update skill files, then ask your AI assistant to apply the update.`))}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 qo}from"fs/promises";import*as b from"@clack/prompts";import P from"picocolors";function vn(){return process.env.GENERATESAAS_API_KEY??Me()}function Wo(e){switch(e.verdict){case"licensed":{let t=e.license?.plan??e.domainInstalls.find(r=>r.ownerPlan)?.ownerPlan;return b.log.success(`${P.green("LICENSED")} - resolves to an account with an active${t?` ${t}`:""} plan.`),!0}case"ejected":{let t=e.install?.ejectedAt??e.domainInstalls.find(r=>r.ejectedAt)?.ejectedAt;return b.log.success(`${P.green("EJECTED")} - a licensed buyer opted this install out of telemetry${t?` on ${t.slice(0,10)}`:""}. The site is legitimate.`),!0}case"revoked":return b.log.error(`${P.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 b.log.error(`${P.red("LEAKED TOKEN")} - this license's install reports ${P.cyan(e.install?.domain??"another domain")}, not this site. The token was copied from a licensed project.`),!1;case"no_license_history":return b.log.error(`${P.red("NO LICENSE HISTORY")} - no license has ever been associated with this site. If it runs GenerateSaaS, treat it as unlicensed.`),!1}}async function Sn(e,t,r){let i=process.env.GENERATESAAS_API_URL??je;e.start("Cross-referencing license records...");try{let n=await tr(i,t,r);return e.stop(`${P.green("Checked")} - records cross-referenced`),Wo(n)}catch(n){return e.stop(`${P.yellow("Skipped")} - ${I(n)}`),null}}function Xo(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 En(e){b.note([`License ID: ${P.cyan(String(e.lid??"unknown"))}`,`Version: ${P.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(`
1112
+ `),P.yellow("License Details"))}function Zo(e){let r=(/^https?:\/\//i.test(e)?e:`https://${e}`).replace(/\/+$/,"");if(r.endsWith("/api"))return[`${r}/license`];try{if(new URL(r).pathname!=="/")return[`${r}/license`]}catch{return[`${r}/license`]}return[`${r}/api/license`,`${r}/license`]}async function wn(e){let t=b.spinner(),r=null,i="no candidates";for(let a of Zo(e)){t.start(`Checking ${a}...`);try{let l=await fetch(a);if(!l.ok){i=`${a} returned ${l.status}`,t.stop(`${P.yellow("Not here")} - ${i}`);continue}let f=(await l.text()).trim();if(!f||f.split(".").length!==3){i=`${a} did not return a JWT`,t.stop(`${P.yellow("Not here")} - ${i}`);continue}r=f,t.stop(`${P.green("Found")} - license endpoint responded`);break}catch(l){i=`${a}: ${I(l)}`,t.stop(`${P.yellow("Unreachable")} - ${i}`)}}if(r===null){b.log.warn(`No license endpoint found (last: ${i}). The site may be ejected, not a GenerateSaaS app, or serving its API elsewhere.`);let a=bn(e),l=vn();return!l||!a?(l||b.log.info("Set GENERATESAAS_API_KEY to your owner key for a records-backed verdict on this domain."),!1):await Sn(t,l,{domain:a})??!1}let n;try{n=Xo(r)}catch{return b.log.error("Could not decode JWT payload."),!1}t.start("Verifying signature...");try{let a=process.env.GENERATESAAS_API_URL??je,l=await er(a,r);if(l.valid)t.stop(`${P.green("Valid")} - signature verified`);else return t.stop(`${P.red("Invalid")} - ${l.reason}`),!1}catch{return t.stop(`${P.yellow("Skipped")} - could not reach verification service`),b.log.warn("Signature not verified. Displaying unverified claims:"),En(n),!1}En(n);let o=vn();return o?await Sn(t,o,{lkh:typeof n.lkh=="string"?n.lkh:void 0,nid:typeof n.nid=="string"?n.nid:void 0,domain:bn(e)})??!0:(b.log.info("Signature is genuine. Set GENERATESAAS_API_KEY to your owner key for the records-backed verdict (licensed / revoked / leaked)."),!0)}function bn(e){try{return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`).hostname}catch{return}}function An(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&&(b.cancel("Provide a URL or --file <path>."),process.exit(1)),r.file){let n=(await qo(r.file,"utf-8")).split(`
1113
+ `).map(s=>s.trim()).filter(s=>s&&!s.startsWith("#"));n.length===0&&(b.cancel("No URLs found in file."),process.exit(1));let o=0;for(let s of n)await wn(s)&&o++,b.log.info("");b.log.success(`${o}/${n.length} sites verified.`)}else await wn(t)||process.exit(1)})}import{existsSync as Qo,rmSync as es}from"fs";import*as F from"@clack/prompts";function Tn(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){Qo(X)?(es(X),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 Ve(),n=Z(i),o=F.spinner();o.start("Verifying API key...");try{await re(n),o.stop("API key verified."),me(i),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 ts,readFileSync as Ft,writeFileSync as In}from"fs";import{join as ve}from"path";import*as x from"@clack/prompts";var rs=["packages/api/src/functions/maintenance/license-heartbeat.ts","packages/api/src/lib/manifest.ts","packages/api/src/routes/internal/license.ts"],ns=[{file:"packages/api/src/routes/inngest.ts",removals:[`import { licenseHeartbeatFunction } from "../functions/maintenance/license-heartbeat";
1114
1114
  `,` licenseHeartbeatFunction,
1115
1115
  `]},{file:"packages/api/src/routes/internal/index.ts",removals:[`import licenseRoutes from "./license";
1116
1116
  `,` .route("/license", licenseRoutes)
1117
- `]}];function Zo(e){return(e&&e.length>0?e.map(r=>Be[r]):Object.values(Be)).map(r=>ve(r,Ct))}function Vt(e){return yt(e)?(qo(e,{recursive:!0}),!0):!1}function Qo(e,t){if(!yt(e))return!1;let r=Ft(e,"utf-8"),i=r;for(let n of t)i=i.replace(n,"");return i===r?!1:(wn(e,i,"utf-8"),!0)}function es(e){let t=ve(e,".gitignore");if(!yt(t))return!1;let r=Ft(t,"utf-8"),i=r.split(`
1117
+ `]}];function is(e){return(e&&e.length>0?e.map(r=>Ke[r]):Object.values(Ke)).map(r=>ve(r,Ct))}function Vt(e){return vt(e)?(ts(e,{recursive:!0}),!0):!1}function os(e,t){if(!vt(e))return!1;let r=Ft(e,"utf-8"),i=r;for(let n of t)i=i.replace(n,"");return i===r?!1:(In(e,i,"utf-8"),!0)}function ss(e){let t=ve(e,".gitignore");if(!vt(t))return!1;let r=Ft(t,"utf-8"),i=r.split(`
1118
1118
  `).filter(n=>!n.includes(".generatesaas")).join(`
1119
- `);return i===r?!1:(wn(t,i,"utf-8"),!0)}function bn(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=ve(t,Q),i;try{i=JSON.parse(Ft(r,"utf-8"))}catch{L.cancel("No GenerateSaaS project found in this directory."),process.exit(1)}let n=await L.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.'}});L.isCancel(n)&&(L.cancel("Eject cancelled."),process.exit(0));let o=[],s=[];for(let a of Zo(i.aiTools))Vt(ve(t,a))&&o.push(a);for(let a of Wo)Vt(ve(t,a))&&o.push(a);Vt(ve(t,$))&&o.push($+"/");for(let a of Xo){let d=ve(t,a.file);Qo(d,a.removals)?s.push(a.file):yt(d)&&L.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}es(t)&&s.push(".gitignore");for(let a of o)L.log.info(`Deleted ${a}`);for(let a of s)L.log.info(`Modified ${a}`);L.log.success("Ejected successfully. This project is now fully standalone.")})}var Se=new ts().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("1.7.0").addHelpText("after",`
1119
+ `);return i===r?!1:(In(t,i,"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),i;try{i=JSON.parse(Ft(r,"utf-8"))}catch{x.cancel("No GenerateSaaS project found in this directory."),process.exit(1)}let n=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(n)&&(x.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)}),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 is(i.aiTools))Vt(ve(t,a))&&o.push(a);for(let a of rs)Vt(ve(t,a))&&o.push(a);Vt(ve(t,U))&&o.push(U+"/");for(let a of ns){let l=ve(t,a.file);os(l,a.removals)?s.push(a.file):vt(l)&&x.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}ss(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 as().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("1.8.0").addHelpText("after",`
1120
1120
  Examples:
1121
1121
  $ generatesaas init Interactive setup
1122
1122
  $ generatesaas init -n my-app -y Quick setup with defaults
1123
1123
  $ generatesaas status Check for updates
1124
1124
  $ generatesaas auth Set or update API key
1125
- `);mn(Se);gn(Se);hn(Se);Sn(Se);En(Se);bn(Se);Se.parseAsync().catch(e=>{Tn.cancel("An unexpected error occurred."),console.error(e),process.exit(1)});
1125
+ `);fn(Se);hn(Se);yn(Se);An(Se);Tn(Se);kn(Se);Se.parseAsync().catch(e=>{Pn.cancel("An unexpected error occurred."),console.error(e),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "generatesaas",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "type": "module",
5
5
  "description": "CLI for scaffolding and managing GenerateSaaS projects",
6
6
  "bin": {