generatesaas 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +27 -27
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as Qo}from"commander";import*as wn from"@clack/prompts";import{existsSync as Ao,readdirSync as ko,rmSync as Po}from"fs";import{join as _o,resolve as Ro}from"path";import{Option as j}from"commander";import*as E from"@clack/prompts";import*as
|
|
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:
|
|
4
|
-
`),p=[` Deploy target: ${f.cyan(F[s]?.label??"Node.js / Docker")}`,` Database: ${f.cyan(x[m].label)}`,` Cache: ${f.cyan(B[v].label)}`,gt.length>0?` Docker: ${f.cyan(gt.map(ne=>
|
|
5
|
-
`),y=[b!=="none"?` Payment: ${f.cyan(
|
|
6
|
-
`),O=[f.bold("Project"),c,"",f.bold("Infrastructure"),p,"",f.bold("Features"),y];if(
|
|
2
|
+
import{Command as Qo}from"commander";import*as wn from"@clack/prompts";import{existsSync as Ao,readdirSync as ko,rmSync as Po}from"fs";import{join as _o,resolve as Ro}from"path";import{Option as j}from"commander";import*as E from"@clack/prompts";import*as Ge from"@clack/prompts";import vt from"picocolors";function Ft(e){let t=e?` GenerateSaaS v${e} `:" GenerateSaaS ";Ge.intro(vt.bgYellow(vt.black(t)))}function Bt(){Ge.outro(vt.yellow("Happy building!"))}import*as u from"@clack/prompts";import f from"picocolors";var Oe={nextjs:{label:"Next.js",hint:"React 19 + Next.js 16"},nuxt:{label:"Nuxt",hint:"Vue 3 + Nuxt 4"}},De={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)"}},Ke={stripe:{label:"Stripe"},polar:{label:"Polar"},none:{label:"None",hint:"disable payments"}},ze={smtp:{label:"SMTP",hint:"Mailpit for local dev"},ses:{label:"Amazon SES"},resend:{label:"Resend"}},He={user:{label:"Per user",hint:"each user has their own subscription"},organization:{label:"Per organization",hint:"org subscription shared by members"}},ve={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}},xe={"claude-code":{label:"Claude Code"},cursor:{label:"Cursor"},codex:{label:"Codex"},"gemini-cli":{label:"Gemini CLI"},windsurf:{label:"Windsurf"}};var ie={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}},x={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"}]}},Ce=[{target:"vercel",provider:"redis",reason:"Vercel serverless cannot maintain persistent Redis connections. Consider Upstash."}],Ne=[{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)."}],Gt=["Local file storage (sharp, geoip-lite)","SMTP email (use Resend or SES instead)","Content API git integration"];function Ye(e){let t=x[e.databaseProvider].managed,r=B[e.cacheProvider].managed;return e.dockerServices.some(i=>!(i==="postgres"&&t||i==="redis"&&r))}var Y={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 Le=["nextjs","nuxt"],Je=["fullstack","separate"],qe=["stripe","polar","none"],We=["smtp","ses","resend"],Xe=["postgres","redis","inngest","mailpit"],Ze=["claude-code","cursor","codex","gemini-cli","windsurf"],Qe=["user","organization"],oe=["USD","EUR","GBP","CAD","AUD","BRL","JPY"],se=["node","vercel"],ae=["postgres","neon","supabase"],ce=["redis","upstash"],et=["google","github","facebook","discord","x"];function tt(e){return e.split("-").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function rt(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 St(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=Y[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 Kt(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(!rt(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:tt(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(Oe),p=await u.select({message:"Frontend framework:",options:c.map(y=>({value:y,label:Oe[y].label,hint:Oe[y].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:se.map(p=>({value:p,label:F[p].label,hint:F[p].hint}))});w(c),s=c}let a=e?.architecture?Ne.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(Ne.filter(c=>c.target===s).map(c=>c.architecture)),h=Je.filter(c=>!d.has(c)),g=e?.architecture??await(async()=>{if(h.length===1){let p=h[0];return u.log.info(`Auto-selected ${De[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:De[p].label,hint:De[p].hint}))});return w(c),c})(),m=e?.databaseProvider??await(async()=>{t=!0;let c=ae.filter(y=>!Ce.some(O=>O.target===s&&O.provider===y));if(c.length===1){let y=c[0];return u.log.info(`Auto-selected ${x[y].label} (only compatible option for ${F[s].label}).`),y}let p=await u.select({message:"Database provider:",options:c.map(y=>({value:y,label:x[y].label,hint:x[y].hint}))});return w(p),p})(),v=e?.cacheProvider??await(async()=>{t=!0;let c=ce.filter(y=>!Ce.some(O=>O.target===s&&O.provider===y));if(c.length===1){let y=c[0];return u.log.info(`Auto-selected ${B[y].label} (only compatible option for ${F[s].label}).`),y}let p=await u.select({message:"Cache provider:",options:c.map(y=>({value:y,label:B[y].label,hint:B[y].hint}))});return w(p),p})();if(F[s]?.edgeRuntime){let c=Gt.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:qe.map(p=>({value:p,label:Ke[p].label,hint:Ke[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:oe.map(p=>({value:p,label:p,hint:ie[p].name}))});return w(c),c})(),R=e?.emailProvider??await(async()=>{t=!0;let c=await u.select({message:"Email provider:",options:We.map(p=>({value:p,label:ze[p].label,hint:ze[p].hint}))});return w(c),c})(),te=e?.multiTenancy??await(async()=>{t=!0;let c=await u.confirm({message:"Enable multi-tenancy (organizations)?",initialValue:!1});return w(c),c})(),re=e?.billingScope??"user";if(te&&e?.billingScope===void 0){t=!0;let c=await u.select({message:"Billing scope:",options:Qe.map(p=>({value:p,label:He[p].label,hint:He[p].hint}))});w(c),re=c}let A=e?.blog??await(async()=>{t=!0;let c=await u.confirm({message:"Enable blog?",initialValue:!0});return w(c),c})(),S=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})(),P=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})(),_e=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})(),ye=e?.socialProviders??await(async()=>{t=!0;let c=et.map(y=>({value:y,label:Y[y].label,hint:`requires ${Y[y].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 gt=e?.dockerServices??await(async()=>{t=!0;let c=[...Xe].filter(D=>D!=="mailpit");R==="smtp"&&c.push("mailpit");let p=c.map(D=>({value:D,label:ve[D].label,hint:ve[D].hint})),y=p.map(D=>D.value).filter(D=>!(D==="postgres"&&(m==="neon"||m==="supabase")||D==="redis"&&v==="upstash")),O=await u.multiselect({message:"Which services should we set up in Docker for you?",options:p,initialValues:y,required:!1});return w(O),O})(),ht=e?.aiTools??await(async()=>{t=!0;let c=Ze.map(y=>({value:y,label:xe[y].label})),p=await u.multiselect({message:"Which AI coding tools do you use?",options:c,initialValues:[],required:!1});return w(p),p})(),yt=e?.demo,Be=St({databaseProvider:m,cacheProvider:v,paymentProvider:b,emailProvider:R,socialProviders:ye,demo:yt}),Re={};if(Be.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 Be)if(t=!0,c.secret){let p=await u.password({message:c.message,mask:"*"});w(p),typeof p=="string"&&p.trim()&&(Re[c.key]=p.trim())}else{let p=await u.text({message:c.message,placeholder:c.placeholder});w(p),typeof p=="string"&&p.trim()&&(Re[c.key]=p.trim())}}if(t){let c=[` Name: ${f.cyan(r)}`,` App name: ${f.cyan(i)}`,` Location: ${f.cyan(n)}`,` Frontend: ${f.cyan(Oe[o].label)}`,` Architecture: ${f.cyan(De[g].label)}`].join(`
|
|
4
|
+
`),p=[` Deploy target: ${f.cyan(F[s]?.label??"Node.js / Docker")}`,` Database: ${f.cyan(x[m].label)}`,` Cache: ${f.cyan(B[v].label)}`,gt.length>0?` Docker: ${f.cyan(gt.map(ne=>ve[ne].label).join(", "))}`:` Docker: ${f.dim("none")}`].filter(Boolean).join(`
|
|
5
|
+
`),y=[b!=="none"?` Payment: ${f.cyan(Ke[b].label)} (${H})`:` Payment: ${f.dim("none")}`,` Credits: ${_e?f.cyan("Yes"):f.dim("No")}`,` Email: ${f.cyan(ze[R].label)}`,` Multi-tenancy: ${te?f.cyan("Yes")+` (billing: ${He[re].label})`:f.dim("No")}`,` Blog: ${A?f.cyan("Yes"):f.dim("No")}`,` Docs app: ${S?f.cyan("Yes"):f.dim("No")}`,` Rev. sharing: ${P?f.cyan("Yes"):f.dim("No")}`,ye.length>0?` Social login: ${f.cyan(ye.map(ne=>Y[ne].label).join(", "))}`:` Social login: ${f.dim("none")}`,ht.length>0?` AI tools: ${f.cyan(ht.map(ne=>xe[ne].label).join(", "))}`:` AI tools: ${f.dim("none")}`].join(`
|
|
6
|
+
`),O=[f.bold("Project"),c,"",f.bold("Infrastructure"),p,"",f.bold("Features"),y];if(Be.length>0){let ne=Be.map(Vt=>{let bn=Re[Vt.key]?f.green("provided"):f.dim("skipped");return` ${Vt.key}: ${bn}`}).join(`
|
|
7
7
|
`);O.push("",f.bold("Credentials"),ne)}u.note(O.join(`
|
|
8
|
-
`),"Summary");let D=await u.confirm({message:"Proceed with these settings?"});(u.isCancel(D)||!D)&&(u.cancel("Setup cancelled."),process.exit(0))}return{projectName:r,appName:i,projectDir:n,frontend:o,architecture:g,deploymentTarget:s,databaseProvider:m,cacheProvider:v,paymentProvider:b,emailProvider:R,multiTenancy:te,billingScope:re,blog:A,docs:S,revenueSharing:P,credits:_e,dockerServices:gt,aiTools:ht,socialProviders:
|
|
9
|
-
`,{mode:384})}async function
|
|
10
|
-
`)}function jn(e,t){return!(e.startsWith("mcp__")||t!=="nuxt"&&e.includes("nuxt"))}import{join as or}from"path";import{mkdir as Mn,readdir as Vn,rm as Fn,rmdir as Bn,writeFile as Gn}from"fs/promises";import{dirname as
|
|
8
|
+
`),"Summary");let D=await u.confirm({message:"Proceed with these settings?"});(u.isCancel(D)||!D)&&(u.cancel("Setup cancelled."),process.exit(0))}return{projectName:r,appName:i,projectDir:n,frontend:o,architecture:g,deploymentTarget:s,databaseProvider:m,cacheProvider:v,paymentProvider:b,emailProvider:R,multiTenancy:te,billingScope:re,blog:A,docs:S,revenueSharing:P,credits:_e,dockerServices:gt,aiTools:ht,socialProviders:ye,defaultCurrency:H,...Object.keys(Re).length>0?{credentials:Re}:{},...e?.baseUrl!==void 0?{baseUrl:e.baseUrl}:{},...yt!==void 0?{demo:yt}:{}}}import{createReadStream as Dn}from"fs";import{mkdir as xn}from"fs/promises";import{Readable as Cn}from"stream";import{pipeline as Qt}from"stream/promises";import{extract as Nn}from"tar";import{join as le}from"path";import{homedir as Tn}from"os";var nt=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",U=".generatesaas",W=le(U,"manifest.json"),zt=le(U,"hashes.json"),it=le(U,"template-hashes.json"),ot=le(U,"template"),Ht=le(U,"staging"),Yt=le(U,"staging.json"),J=le(Tn(),".generatesaas");var _=class extends Error{constructor(r,i){super(i);this.status=r}status;name="ApiError"};function q(e){return{apiKey:e,baseUrl:nt}}async function X(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;try{o=(await n.json()).error??`API ${n.status}: ${t}`}catch{o=`API ${n.status}: ${t}`}throw new _(n.status,o)}return n}import{existsSync as In,readFileSync as An,writeFileSync as kn,mkdirSync as Pn}from"fs";import{dirname as _n}from"path";import*as Z from"@clack/prompts";function Et(){if(!In(J))return null;try{let e=JSON.parse(An(J,"utf-8"));return e.apiKey?e.apiKey:(e.token&&!e.apiKey&&Z.log.warning(`Found old GitHub token in ${J}. Run 'generatesaas init' to set up your API key.`),null)}catch{return null}}function pe(e){Pn(_n(J),{recursive:!0}),kn(J,JSON.stringify({apiKey:e},null," ")+`
|
|
9
|
+
`,{mode:384})}async function Se(e){if(e?.apiKey)return e.apiKey;let t=process.env.GENERATESAAS_API_KEY;if(t)return t;let r=Et();if(r)return r;if(!e?.prompt)throw new Error("API key not found. Set GENERATESAAS_API_KEY or run 'generatesaas init' to configure.");return Ue()}async function Ue(){let e=await Z.text({message:"Enter your GenerateSaaS API key:",placeholder:"gs_live_...",validate:t=>{if(!t?.trim())return"API key is required."}});return Z.isCancel(e)&&(Z.cancel("Setup cancelled."),process.exit(0)),e.trim()}async function Q(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 X(e,"/versions")).json()}async function Jt(e,t){try{return await(await X(e,`/changelog/${encodeURIComponent(t)}`)).text()}catch(r){if(r instanceof _&&r.status===404)return null;throw r}}async function qt(e,t){return await(await X(e,`/skill/${encodeURIComponent(t)}`)).json()}async function Wt(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:"offline-test-token",licenseId:"offline-test-license-id"}:await(await X(e,"/license/sign",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function Xt(e,t){return await(await X(e,"/license/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function Zt(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 wt=new Set([".git","node_modules",".pnpm-store",".env",".env.test",".turbo",".nuxt",".output",".data","dist",".next",".svelte-kit",".devcontainer","playwright-report","test-results"]),Rn=new Set(["data","mksaas","references","scripts",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","README.md","TODO.md","OVERVIEW.md"]),On=["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 Ee(e){let t=e.split("/");for(let r of t)if(wt.has(r))return!0;if(Rn.has(t[0]))return!0;for(let r of On)if(e===r||e.startsWith(r+"/"))return!0;return!1}async function st(e,t,r){await xn(r,{recursive:!0});let i=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(i){await Qt(Dn(i),er(r));return}let n=await X(e,`/template/${encodeURIComponent(t)}`);if(!n.body)throw new Error("Empty response body");let o=Cn.fromWeb(n.body);await Qt(o,er(r))}function er(e){return Nn({cwd:e,strip:1,filter:t=>{let r=t.replace(/^[^/]+\//,"");return r?!Ee(r):!0},sync:!1})}import{readFile as Ln,rm as tr,writeFile as Un}from"fs/promises";import{join as bt}from"path";var $n=["apps/web-nuxt/public/images/blog","apps/web-next/public/images/blog"];async function rr(e){await Promise.all($n.map(t=>tr(bt(e,t),{recursive:!0,force:!0})))}async function nr(e,t){t.includes("claude-code")||await tr(bt(e,".claude"),{recursive:!0,force:!0})}async function ir(e,t){let r=bt(e,".claude","settings.json"),i;try{i=await Ln(r,"utf8")}catch{return}let n=JSON.parse(i);delete n.alwaysThinkingEnabled,delete n.enableAllProjectMcpServers,n.env&&(delete n.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS,Object.keys(n.env).length===0&&delete n.env),n.permissions?.allow&&(n.permissions.allow=n.permissions.allow.filter(o=>jn(o,t))),await Un(r,JSON.stringify(n,null," ")+`
|
|
10
|
+
`)}function jn(e,t){return!(e.startsWith("mcp__")||t!=="nuxt"&&e.includes("nuxt"))}import{join as or}from"path";import{mkdir as Mn,readdir as Vn,rm as Fn,rmdir as Bn,writeFile as Gn}from"fs/promises";import{dirname as at,join as Kn,relative as zn}from"path";async function ct(e){await Mn(e,{recursive:!0})}async function l(e,t){await ct(at(e)),await Gn(e,t,"utf-8")}async function Tt(e,t){await Fn(e,{force:!0});let r=at(e);for(;r!==t&&r!==at(r);){try{await Bn(r)}catch{return}r=at(r)}}async function de(e,t,r){let i=[],n=await Vn(e,{withFileTypes:!0});for(let o of n){let s=Kn(e,o.name),a=zn(t,s);r(a)||(o.isDirectory()?i.push(...await de(s,t,r)):o.isFile()&&i.push(s))}return i}var Hn={postgres:"Postgres",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"},Yn={resend:"Resend",ses:"Amazon SES",smtp:"SMTP"},Jn={redis:"Redis",upstash:"Upstash Redis"},qn={node:"Node.js / Docker",vercel:"Vercel"},Wn={stripe:"Stripe",polar:"Polar"};function Xn(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=Jn[e.cacheProvider],h=qn[e.deploymentTarget],g=e.paymentProvider==="none"?"":`
|
|
11
11
|
- **Payments:** ${Wn[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`.",v=t?"**Navigation:** always use `localePath()` for paths (hardcoded paths break non-default locales); `await navigateTo()` in SSR.":"**Navigation:** `next/link` for links; `redirect()` from `next/navigation` for programmatic redirects in Server Components.";return`# AGENTS.md
|
|
12
12
|
|
|
13
13
|
Guidelines for AI coding agents (Claude Code, Codex, Cursor, \u2026) in this project.
|
|
@@ -81,7 +81,7 @@ In \`@repo/*\` backend packages prefer web-standard APIs: \`crypto.randomUUID()\
|
|
|
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
83
|
`}async function sr(e){await l(or(e.projectDir,"AGENTS.md"),Xn(e)),await l(or(e.projectDir,"CLAUDE.md"),`@AGENTS.md
|
|
84
|
-
`)}import{join as Zn}from"path";var Qn={postgres:"Postgres (self-hosted)",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"};function ei(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=Qn[e.databaseProvider],s=
|
|
84
|
+
`)}import{join as Zn}from"path";var Qn={postgres:"Postgres (self-hosted)",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"};function ei(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=Qn[e.databaseProvider],s=Ye(e),a=e.architecture==="fullstack"?`${i} app at \`${n}/\` with the Hono API mounted inside it. A standalone \`apps/backend/\` is also included (inert in this fullstack setup) so you can split the API into a separate service later without re-scaffolding.`:`${i} app at \`${n}/\` and a separate Hono backend at \`apps/backend/\`.`,d=e.architecture==="fullstack"?`- App + API: http://localhost:3000
|
|
85
85
|
- Inngest dev server: http://127.0.0.1:8288`:`- App: http://localhost:3000
|
|
86
86
|
- API: http://localhost:3010
|
|
87
87
|
- Inngest dev server: http://127.0.0.1:8288`,h=s?`pnpm infra # optional: starts local Docker services (Postgres / Redis / Inngest / Mailpit)
|
|
@@ -422,7 +422,7 @@ ${o?" credits: { enabled: true }":" credits: { enabled: false }"},
|
|
|
422
422
|
items: []
|
|
423
423
|
}
|
|
424
424
|
};
|
|
425
|
-
`;await l(t,g)}var ni={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 ii(e){let t=Y[e];return t.envVars.map((r,i)=>({key:r.name,...i===0?{comment:`# TODO: Add your ${t.label} OAuth credentials`}:{}}))}var oi={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
|
|
425
|
+
`;await l(t,g)}var ni={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 ii(e){let t=Y[e];return t.envVars.map((r,i)=>({key:r.name,...i===0?{comment:`# TODO: Add your ${t.label} OAuth credentials`}:{}}))}var oi={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 we(e,t){return t?e.map(r=>{let i=t[r.key];return i?{...r,defaultValue:i,comment:void 0}:r}):e}function be(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 At(e){return Array.from(crypto.getRandomValues(new Uint8Array(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}function dr(e){return e.architecture==="fullstack"?{apiUrl:"http://localhost:3000/api",baseUrl:"http://localhost:3000"}:{apiUrl:"http://localhost:3010",baseUrl:"http://localhost:3000"}}function si(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 lt(e,t,r,i){e.push(i==="example"?`${t}=`:`${t}=${r}`)}function pr(e,t){let r=t==="example"?void 0:e.credentials,{apiUrl:i,baseUrl:n}=dr(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"),be(we(x[e.databaseProvider].envVars,r),o),o.push("","# Cache"),be(we(B[e.cacheProvider].envVars,r),o),o.push("","# Authentication"),t==="example"&&o.push("# Generate a strong secret, e.g. `openssl rand -hex 32`"),lt(o,"BETTER_AUTH_SECRET",crypto.randomUUID(),t),o.push("","# Content API (random secret; required when contentApi feature is enabled in @repo/config)"),lt(o,"CONTENT_API_KEY",At(32),t),o.push("","# Job Queue - Inngest","INNGEST_APP_ID=api"),lt(o,"INNGEST_EVENT_KEY",At(32),t),lt(o,"INNGEST_SIGNING_KEY",At(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=ni[e.emailProvider];if(s&&(o.push("","# Email"),be(we(s,r),o)),e.paymentProvider!=="none"){let a=oi[e.paymentProvider];a&&(o.push("","# Payment"),be(we(a,r),o))}if(e.socialProviders.length>0){o.push("","# Social auth");for(let a of e.socialProviders)be(we(ii(a),r),o)}return e.demo&&(o.push("","# Captcha (Cloudflare Turnstile)"),be(we([{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(`
|
|
426
426
|
`)}function ai(e){let{apiUrl:t}=dr(e),r=e.frontend==="nextjs"?"NEXT_PUBLIC_API_URL":"NUXT_PUBLIC_API_URL",i=["# API Configuration",`${r}=${t}`],n=si(e);return n&&e.architecture==="separate"&&i.push("","# Production (uncomment and replace with your deployed hostnames):",`# ${r}=${n.backend}`),i.push(""),i.join(`
|
|
427
427
|
`)}async function ur(e){let t=ai(e);await l(`${e.projectDir}/.env`,t+`
|
|
428
428
|
`+pr(e,"env")),await l(`${e.projectDir}/.env.example`,t+`
|
|
@@ -976,8 +976,8 @@ export const env = (() => {
|
|
|
976
976
|
const parsedApiUrl = new URL(env.API_URL);
|
|
977
977
|
export const apiBasePath = parsedApiUrl.pathname === "/" ? "" : parsedApiUrl.pathname;
|
|
978
978
|
export const apiOrigin = parsedApiUrl.origin;
|
|
979
|
-
`}import{readFile as bi}from"fs/promises";import{join as G}from"path";var
|
|
980
|
-
`)}function $e(e,t){for(let r of t)delete e.dependencies?.[r],delete e.devDependencies?.[r]}function
|
|
979
|
+
`}import{readFile as bi}from"fs/promises";import{join as G}from"path";var pt={"@upstash/redis":"^1.37.0","@upstash/lock":"^0.2.1","@neondatabase/serverless":"^1.0.1",postgres:"^3.4.7"};async function Ie(e){let t=await bi(e,"utf-8");return JSON.parse(t)}async function Ae(e,t){await l(e,JSON.stringify(t,null," ")+`
|
|
980
|
+
`)}function $e(e,t){for(let r of t)delete e.dependencies?.[r],delete e.devDependencies?.[r]}function Te(e,t,r,i=!1){let n=i?"devDependencies":"dependencies";e[n]||(e[n]={}),e[n][t]=r}async function Sr(e){await Ti(e),await Ii(e),await Ai(e),await ki(e),e.frontend==="nextjs"?await _i(e):await Pi(e)}async function Ti(e){let t=G(e.projectDir,"packages/api/package.json"),r=await Ie(t);$e(r,["sharp","@types/sharp"]),await Ae(t,r)}async function Ii(e){let t=G(e.projectDir,"packages/runtime/package.json"),r=await Ie(t);e.cacheProvider==="upstash"&&($e(r,["ioredis","rate-limit-redis","redis-semaphore"]),Te(r,"@upstash/redis",pt["@upstash/redis"]),Te(r,"@upstash/lock",pt["@upstash/lock"])),await Ae(t,r)}async function Ai(e){let t=G(e.projectDir,"packages/database/package.json"),r=await Ie(t);e.databaseProvider==="neon"?($e(r,["pg","@types/pg"]),Te(r,"@neondatabase/serverless",pt["@neondatabase/serverless"])):e.databaseProvider==="supabase"&&($e(r,["pg","@types/pg"]),Te(r,"postgres",pt.postgres)),await Ae(t,r)}async function ki(e){if(e.architecture!=="separate")return;let t=G(e.projectDir,"apps/backend/package.json"),r=await Ie(t);e.deploymentTarget!=="node"&&$e(r,["@hono/node-server"]),await Ae(t,r)}async function Pi(e){if(e.architecture!=="separate")return;let t=G(e.projectDir,"apps/web-nuxt");await Tt(G(t,"server/api/[...paths].ts"),t);let r=G(t,"package.json"),i=await Ie(r),n=i.dependencies?.["@repo/api"];n&&(delete i.dependencies?.["@repo/api"],Te(i,"@repo/api",n,!0)),await Ae(r,i)}async function _i(e){if(e.architecture!=="separate")return;let t=G(e.projectDir,"apps/web-next");await Tt(G(t,"app/api/[[...rest]]/route.ts"),t);let r=G(t,"package.json"),i=await Ie(r),n=i.dependencies?.["@repo/api"];n&&(delete i.dependencies?.["@repo/api"],Te(i,"@repo/api",n,!0)),await Ae(r,i)}import{readFile as wr}from"fs/promises";import{join as br}from"path";async function Tr(e){let t=br(e.projectDir,"turbo.json"),r;try{r=await wr(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," ")+`
|
|
981
981
|
`)}var Ri=["base.json","node.json","next.json"],Er="GenerateSaaS ";async function Ir(e){if(!e.demo)for(let t of Ri){let r=br(e.projectDir,"tooling/typescript",t),i;try{i=await wr(r,"utf-8")}catch{continue}let n=JSON.parse(i);typeof n.display!="string"||!n.display.startsWith(Er)||(n.display=n.display.slice(Er.length),await l(r,JSON.stringify(n,null," ")+`
|
|
982
982
|
`))}}import{readFile as Oi,rm as Di}from"fs/promises";import{existsSync as Ar}from"fs";import{join as kr}from"path";async function Pr(e){if(e.frontend==="nuxt")return;let t=kr(e.projectDir,"packages/i18n/package.json");if(!Ar(t))throw new Error(`pruneI18nNuxt: expected ${t} to exist - did the i18n package move?`);let r=JSON.parse(await Oi(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," ")+`
|
|
983
983
|
`);let o=kr(e.projectDir,"packages/i18n/nuxt");if(i&&!Ar(o))throw new Error(`pruneI18nNuxt: packages/i18n declares a Nuxt export surface but ${o} is missing - did the i18n Nuxt module move?`);await Di(o,{recursive:!0,force:!0})}import{readFile as _r,rm as xi}from"fs/promises";import{join as Pt}from"path";async function Rr(e){e.cacheProvider==="upstash"&&await Promise.all([Ci(e.projectDir),Ni(e.projectDir),xi(Pt(e.projectDir,"packages/runtime/tests/redis.test.ts"),{force:!0})])}async function Ci(e){let t=Pt(e,"packages/runtime/tests/setup.ts"),r;try{r=await _r(t,"utf-8")}catch{return}let i=r.replace(/\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
|
|
@@ -1003,8 +1003,8 @@ vi.mock("@upstash/lock", () => {
|
|
|
1003
1003
|
return { Lock };
|
|
1004
1004
|
});
|
|
1005
1005
|
|
|
1006
|
-
$1`),i!==r&&await l(t,i)}import{readdir as Li,readFile as Ui,rm as
|
|
1007
|
-
`)}async function xr(e){let t=e.frontend==="nextjs"?"apps/web-nuxt":"apps/web-next";await
|
|
1006
|
+
$1`),i!==r&&await l(t,i)}import{readdir as Li,readFile as Ui,rm as je}from"fs/promises";import{join as ue}from"path";var $i=new Set(["ci.yml"]),ji=["cli","cli:clean","demo:bench","playground:regen","playground:test","playground:test:units"];async function Or(e){let t=ue(e.projectDir,".github/workflows"),r=await Li(t).catch(()=>[]);for(let i of r)$i.has(i)||await je(ue(t,i),{recursive:!0,force:!0})}async function Dr(e){let t=ue(e.projectDir,"package.json"),r=await Ui(t,"utf-8"),i=JSON.parse(r),n=!1;if(i.scripts){for(let o of ji)o in i.scripts&&(delete i.scripts[o],n=!0);if(!Ye(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," ")+`
|
|
1007
|
+
`)}async function xr(e){let t=e.frontend==="nextjs"?"apps/web-nuxt":"apps/web-next";await je(ue(e.projectDir,t),{recursive:!0,force:!0})}async function Cr(e){e.docs||await je(ue(e.projectDir,"apps/docs"),{recursive:!0})}async function Nr(e){let t=e.frontend==="nextjs"?"docs/nuxt":"docs/next";await je(ue(e.projectDir,t),{recursive:!0,force:!0}),await je(ue(e.projectDir,"docs/index.mdx"))}import{join as Mi}from"path";async function Lr(e){let t=Vi(e);await l(Mi(e.projectDir,".github/workflows/ci.yml"),t)}function Vi(e){let t=x[e.databaseProvider].managed,r=e.cacheProvider==="upstash",i=e.frontend==="nuxt",n=t?"":` services:
|
|
1008
1008
|
postgres:
|
|
1009
1009
|
image: postgres:18-alpine
|
|
1010
1010
|
env:
|
|
@@ -1096,29 +1096,29 @@ export { ops } from "./${r}/index";
|
|
|
1096
1096
|
`).length&&await l(e,n.join(`
|
|
1097
1097
|
`))}async function ut(e){let t=e.projectDir;e.demo||await rr(t),await nr(t,e.aiTools),await ir(t,e.frontend),await sr(e),await ar(e),await cr(e),e.demo||await lr(e),await ur(e);let r=await mr(e);return await fr(e),await gr(e),await hr(e),await yr(e),await vr(e),await Sr(e),await Tr(e),await Ir(e),await Pr(e),await Rr(e),await Or(e),await Lr(e),await Dr(e),await xr(e),await Cr(e),await Nr(e),await jr(e),await Mr(e),await Fr(e),await Kr(e),{dockerComposeGenerated:r}}import{basename as Hr,join as Yr,relative as ro}from"path";import{createHash as zr}from"crypto";import{readFile as to}from"fs/promises";async function mt(e){let t=await to(e);return zr("sha256").update(t).digest("hex")}function Rt(e){return zr("sha256").update(e).digest("hex")}var no=new Set(["data",U]);function io(e){let t=e.split("/");for(let r of t)if(wt.has(r)||no.has(r)||r.startsWith(".env")&&!r.includes("example"))return!0;return!1}function Jr(e,t){return{projectName:e.projectName??Hr(t),appName:e.appName??e.projectName??Hr(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 qr(e,t){let i=(await de(e.projectDir,e.projectDir,io)).sort(),n=await Promise.all(i.map(async a=>[ro(e.projectDir,a),await mt(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(Yr(e.projectDir,W),JSON.stringify(s,null," ")+`
|
|
1098
1098
|
`),await l(Yr(e.projectDir,zt),JSON.stringify(o,null," ")+`
|
|
1099
|
-
`)}import{relative as oo}from"path";async function
|
|
1099
|
+
`)}import{relative as oo}from"path";async function Me(e){let r=(await de(e,e,Ee)).sort(),i=await Promise.all(r.map(async n=>[oo(e,n),await mt(n)]));return Object.fromEntries(i)}import{copyFile as so,mkdir as ao,rm as co}from"fs/promises";import{dirname as lo,join as Wr,relative as po}from"path";import{existsSync as uo}from"fs";async function Ot(e,t){uo(t)&&await co(t,{recursive:!0,force:!0});let r=await de(e,e,Ee);for(let i of r){let n=po(e,i),o=Wr(t,n);await ao(lo(o),{recursive:!0}),await so(i,o)}}async function Xr(e,t){await Ot(e,Wr(t,ot))}import{existsSync as mo}from"fs";import{readFile as Zr,readdir as fo}from"fs/promises";import{join as K,dirname as go,resolve as ho,sep as yo}from"path";import{fileURLToPath as vo}from"url";var Ve={"claude-code":".claude/skills",cursor:".cursor/skills",codex:".agents/skills","gemini-cli":".gemini/skills",windsurf:".windsurf/skills"},ll=Object.values(Ve),Dt="generatesaas-update",Qr=go(vo(import.meta.url));function So(){let e=K(Qr,"skill","content");return mo(e)?e:K(Qr,"content")}function xt(e){return!e||e.length===0?[]:e.map(t=>Ve[t])}async function Ct(e,t,r,i){let n=xt(i);for(let o of n){let s=K(e,o,Dt),a=K(s,"scripts"),d=K(s,"references");await ct(a),await ct(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=ho(a,h);m.startsWith(a+yo)&&await l(m,g)}}}async function en(e,t){let r=So(),i=await Zr(K(r,"SKILL.md"),"utf-8"),n=K(r,"scripts"),o=await fo(n),s={};for(let a of o)a!==".gitkeep"&&(s[a]=await Zr(K(n,a),"utf-8"));await Ct(e,i,s,t)}import{execFile as Eo,execFileSync as wo}from"child_process";import{access as tn,readFile as bo}from"fs/promises";import{join as Nt}from"path";import*as T from"@clack/prompts";function fe(e){try{let t=process.platform==="win32"?"where":"which";return wo(t,[e],{stdio:"ignore"}),!0}catch{return!1}}function me(e,t,r,i=3e5){return new Promise((n,o)=>{Eo(e,t,{cwd:r,timeout:i},(s,a,d)=>{if(s){let h=String(a||"").trim(),m=[String(d||"").trim(),h].filter(Boolean).join(`
|
|
1100
1100
|
`);o(new Error(m?`${s.message}
|
|
1101
|
-
${m}`:s.message))}else n()})})}async function rn(e){if(!
|
|
1101
|
+
${m}`:s.message))}else n()})})}async function rn(e){if(!fe("pnpm"))return T.log.warn("pnpm not found. Skipping lockfile regeneration."),!1;try{return await me("pnpm",["install","--lockfile-only","--no-frozen-lockfile","--config.minimumReleaseAge=0"],e),!0}catch(t){let r=t instanceof Error?t.message:String(t);return T.log.warn(`Lockfile regeneration failed: ${r}`),T.log.warn("Deploys using --frozen-lockfile may fail."),!1}}async function nn(e){if(!fe("pnpm"))return T.log.warn("pnpm not found. Skipping dependency installation."),T.log.info("Install pnpm: https://pnpm.io/installation"),!1;let t=T.spinner();t.start("Installing dependencies (this may take a minute)...");try{return await me("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 T.log.warn(`pnpm install failed: ${i}`),T.log.warn("You can run it manually later."),!1}}async function on(e){if(!fe("pnpm"))return!1;let t=T.spinner();t.start("Generating baseline database migration...");try{return await me("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 T.log.warn(`Could not generate baseline migration: ${i}`),T.log.warn("Run 'pnpm -F @repo/database generate' before your first deploy."),!1}}async function sn(e){try{return await tn(Nt(e,".git")),T.log.info("Git repository already exists, skipping init."),!0}catch{}if(!fe("git"))return T.log.warn("git not found. Skipping repository initialization."),!1;let t=T.spinner();t.start("Initializing git repository...");try{return await me("git",["init"],e),await me("git",["add","-A"],e),await me("git",["commit","--no-verify","-m","Initial commit from GenerateSaaS"],e),t.stop("Git repository initialized."),!0}catch{return t.stop("Git initialization failed."),T.log.warn("You can run git init manually later."),!1}}async function an(e){if(!fe("pnpm"))return!1;try{await tn(Nt(e,".git"))}catch{return!1}try{let t=JSON.parse(await bo(Nt(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 me("pnpm",["exec","simple-git-hooks"],e),!0}catch{return T.log.warn("Could not install git hooks. Run 'pnpm exec simple-git-hooks' manually."),!1}}import*as ke from"@clack/prompts";import $ from"picocolors";function cn(e,t){t.dockerComposeGenerated&&!t.dockerAvailable&&ke.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=>ve[s].label).join(", ");r.push(`pnpm infra ${$.dim(`# ${o}`)}`)}if(r.push(`pnpm dev ${$.dim("# http://localhost:3000")}`),t.skippedCredentials.length>0&&(r.push(""),r.push($.dim("Fill in remaining TODO values in .env"))),ke.note(r.join(`
|
|
1102
1102
|
`),$.yellow("Start Development")),t.dockerComposeGenerated){let o=[];o.push(`App ${$.cyan("http://localhost:3000")}`),e.architecture==="separate"&&o.push(`API ${$.cyan("http://localhost:3010")}`),e.dockerServices.includes("mailpit")&&o.push(`Mailpit ${$.cyan("http://localhost:8025")}`),e.dockerServices.includes("inngest")&&o.push(`Inngest ${$.cyan("http://localhost:8288")}`),ke.note(o.join(`
|
|
1103
1103
|
`),$.yellow("Dev Tools"))}let i=[],n=To(e);n.length>0&&i.push(`Set in production: ${$.dim(n.join(", "))}`),i.push("pnpm db:push # Run database migrations"),i.push(Io(e)),ke.note(i.join(`
|
|
1104
|
-
`),$.yellow("Deployment"))}function To(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 Io(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 ln(e){let t={};if(e.name!==void 0){if(!tt(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(!Le.includes(e.frontend))throw new Error(`Invalid frontend "${e.frontend}". Valid values: ${Le.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=Lt(e.docker,We,"docker service")),e.aiTools!==void 0&&(t.aiTools=Lt(e.aiTools,Xe,"AI tool")),e.socialProviders!==void 0&&(t.socialProviders=Lt(e.socialProviders,Qe,"social provider")),e.currency!==void 0){if(!oe.includes(e.currency))throw new Error(`Invalid currency "${e.currency}". Valid values: ${oe.join(", ")}`);t.defaultCurrency=e.currency}if(e.deploy!==void 0){if(!se.includes(e.deploy))throw new Error(`Invalid deployment target "${e.deploy}". Valid values: ${se.join(", ")}`);t.deploymentTarget=e.deploy}if(e.database!==void 0){if(!ae.includes(e.database))throw new Error(`Invalid database provider "${e.database}". Valid values: ${ae.join(", ")}`);t.databaseProvider=e.database}if(e.cache!==void 0){if(!ce.includes(e.cache))throw new Error(`Invalid cache provider "${e.cache}". Valid values: ${ce.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 ee={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 pn(e){let t=e.projectName??ee.projectName,r=e.projectDir??`./${t}`,i=e.appName??et(t),n=e.deploymentTarget??ee.deploymentTarget,o=F[n]?.edgeRuntime??!1,s=e.databaseProvider??(o?"neon":ee.databaseProvider),a=e.cacheProvider??(o?"upstash":ee.cacheProvider),d=e.emailProvider??(o?"resend":ee.emailProvider),h=e.dockerServices??(o?ee.dockerServices.filter(m=>m!=="postgres"&&m!=="redis"):ee.dockerServices),g={...ee,...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 Ce){if(g.deploymentTarget!==m.target)continue;let v=g.databaseProvider===m.provider?"database":"cache";if(g.databaseProvider===m.provider||g.cacheProvider===m.provider)throw new Error(`Incompatible: --deploy ${m.target} + --${v} ${m.provider}. ${m.reason}`)}for(let m of Ne)if(g.architecture===m.architecture&&g.deploymentTarget===m.target)throw new Error(`Incompatible: --architecture ${m.architecture} + --deploy ${m.target}. ${m.reason}`);return g}function Lt(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 Oo from"picocolors";var Do="c81635e3715dc7274f33a745e24a394d93d3d081a4004408585c3de86e1aac35";function xo(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 dn(e){e.command("init").description("Scaffold a new GenerateSaaS project").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([...Le])).addOption(new j("--architecture <type>","fullstack or separate").choices([...Ye])).addOption(new j("--payment <provider>","payment provider").choices([...Je])).addOption(new j("--email <provider>","email provider").choices([...qe])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").addOption(new j("--billing-scope <scope>","billing scope (requires --org)").choices([...Ze])).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([...oe])).addOption(new j("--deploy <target>","deployment target").choices([...se])).addOption(new j("--database <provider>","database provider").choices([...ae])).addOption(new j("--cache <provider>","cache provider").choices([...ce])).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=>{await Co(t)})}async function Co(e){let t=performance.now();Ft("1.5.0");let r,i;try{r=ln(e),i=xo(e.templateVersion)}catch(S){E.cancel(I(S)),process.exit(1)}let n=E.spinner(),o;try{o=await ve({apiKey:e.apiKey,prompt:!e.yes})}catch(S){E.cancel(I(S)),process.exit(1)}e.demo&&Rt(o)!==Do&&(E.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let s=q(o),a=async()=>{let S=await Q(s),P=S.latest,_e=i??P;if(i&&!S.versions.some(he=>he.version===_e))throw new Error(`Template version "${i}" is not available.`);return{latestVersion:P,selectedVersion:_e}};n.start("Verifying access...");let d,h;try{({latestVersion:d,selectedVersion:h}=await a()),n.stop("Access verified."),pe(o)}catch(S){if(n.stop("Access verification failed."),S instanceof _&&S.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 Ue(),s=q(o),n.start("Verifying access...");try{({latestVersion:d,selectedVersion:h}=await a()),n.stop("Access verified."),pe(o)}catch(P){n.stop("Access verification failed."),E.cancel(P instanceof _&&P.status===401?"Invalid API key.":I(P)),process.exit(1)}}else E.cancel(I(S)),process.exit(1)}E.log.success(`Latest version: ${d}`),h!==d&&E.log.success(`Using template version: ${h}`);let g;e.yes?g=pn(r):g=await Kt(r);let m;n.start("Activating license...");try{let S=crypto.randomUUID();m={token:(await Wt(s,{frontend:g.frontend,version:h,installId:S})).token,keyHash:Rt(o),installId:S},n.stop("License activated.")}catch(S){n.stop("License activation failed."),E.cancel(I(S)),process.exit(1)}let v=Ro(g.projectDir);if(Ao(v)&&ko(v).length>0)if(e.yes)E.log.info(`Directory ${v} is not empty. Merging (keeping existing files, overwriting conflicts).`);else{let P=await E.select({message:`Directory ${v} 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(P)||P==="cancel")&&(E.cancel("Setup cancelled."),process.exit(0)),P==="overwrite"&&Po(v,{recursive:!0,force:!0})}let b={...g,projectDir:v,version:h,...e.demo?{docs:!1}:{}};n.start("Downloading template...");try{await ot(s,h,v),n.stop("Template downloaded.")}catch(S){n.stop("Download failed."),E.cancel(I(S)),process.exit(1)}let H;n.start("Generating project files...");try{if({dockerComposeGenerated:H}=await ut(b),!e.demo){let S=await je(v);await l(_o(v,nt),JSON.stringify(S,null," ")+`
|
|
1105
|
-
`),await Xr(v,v)}await en(v,b.aiTools),await qr(b,m),n.stop("Project files generated.")}catch(S){n.stop("Generation failed."),E.cancel(I(S)),process.exit(1)}await rn(v);let R=await nn(v);R&&b.demo!==!0&&e.dbMigration!==!1&&await on(v),await sn(v),R&&await an(v);let te=
|
|
1106
|
-
`),k.log.success("License refreshed.")}catch{k.log.warn("License refresh skipped.")}let m=Jr(n,r),v=
|
|
1107
|
-
`)}a.stop("Baseline template stored.")}else if(re){a.start("Computing baseline template hashes...");let A=await
|
|
1108
|
-
`),a.stop("Baseline hashes computed.")}if(await l(
|
|
1104
|
+
`),$.yellow("Deployment"))}function To(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 Io(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 ln(e){let t={};if(e.name!==void 0){if(!rt(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(!Le.includes(e.frontend))throw new Error(`Invalid frontend "${e.frontend}". Valid values: ${Le.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=Lt(e.docker,Xe,"docker service")),e.aiTools!==void 0&&(t.aiTools=Lt(e.aiTools,Ze,"AI tool")),e.socialProviders!==void 0&&(t.socialProviders=Lt(e.socialProviders,et,"social provider")),e.currency!==void 0){if(!oe.includes(e.currency))throw new Error(`Invalid currency "${e.currency}". Valid values: ${oe.join(", ")}`);t.defaultCurrency=e.currency}if(e.deploy!==void 0){if(!se.includes(e.deploy))throw new Error(`Invalid deployment target "${e.deploy}". Valid values: ${se.join(", ")}`);t.deploymentTarget=e.deploy}if(e.database!==void 0){if(!ae.includes(e.database))throw new Error(`Invalid database provider "${e.database}". Valid values: ${ae.join(", ")}`);t.databaseProvider=e.database}if(e.cache!==void 0){if(!ce.includes(e.cache))throw new Error(`Invalid cache provider "${e.cache}". Valid values: ${ce.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 ee={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 pn(e){let t=e.projectName??ee.projectName,r=e.projectDir??`./${t}`,i=e.appName??tt(t),n=e.deploymentTarget??ee.deploymentTarget,o=F[n]?.edgeRuntime??!1,s=e.databaseProvider??(o?"neon":ee.databaseProvider),a=e.cacheProvider??(o?"upstash":ee.cacheProvider),d=e.emailProvider??(o?"resend":ee.emailProvider),h=e.dockerServices??(o?ee.dockerServices.filter(m=>m!=="postgres"&&m!=="redis"):ee.dockerServices),g={...ee,...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 Ce){if(g.deploymentTarget!==m.target)continue;let v=g.databaseProvider===m.provider?"database":"cache";if(g.databaseProvider===m.provider||g.cacheProvider===m.provider)throw new Error(`Incompatible: --deploy ${m.target} + --${v} ${m.provider}. ${m.reason}`)}for(let m of Ne)if(g.architecture===m.architecture&&g.deploymentTarget===m.target)throw new Error(`Incompatible: --architecture ${m.architecture} + --deploy ${m.target}. ${m.reason}`);return g}function Lt(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 Oo from"picocolors";var Do="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";function xo(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 dn(e){e.command("init").description("Scaffold a new GenerateSaaS project").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([...Le])).addOption(new j("--architecture <type>","fullstack or separate").choices([...Je])).addOption(new j("--payment <provider>","payment provider").choices([...qe])).addOption(new j("--email <provider>","email provider").choices([...We])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").addOption(new j("--billing-scope <scope>","billing scope (requires --org)").choices([...Qe])).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([...oe])).addOption(new j("--deploy <target>","deployment target").choices([...se])).addOption(new j("--database <provider>","database provider").choices([...ae])).addOption(new j("--cache <provider>","cache provider").choices([...ce])).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=>{await Co(t)})}async function Co(e){let t=performance.now();Ft("1.5.2");let r,i;try{r=ln(e),i=xo(e.templateVersion)}catch(S){E.cancel(I(S)),process.exit(1)}let n=E.spinner(),o;try{o=await Se({apiKey:e.apiKey,prompt:!e.yes})}catch(S){E.cancel(I(S)),process.exit(1)}e.demo&&Rt(o)!==Do&&(E.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let s=q(o),a=async()=>{let S=await Q(s),P=S.latest,_e=i??P;if(i&&!S.versions.some(ye=>ye.version===_e))throw new Error(`Template version "${i}" is not available.`);return{latestVersion:P,selectedVersion:_e}};n.start("Verifying access...");let d,h;try{({latestVersion:d,selectedVersion:h}=await a()),n.stop("Access verified."),pe(o)}catch(S){if(n.stop("Access verification failed."),S instanceof _&&S.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 Ue(),s=q(o),n.start("Verifying access...");try{({latestVersion:d,selectedVersion:h}=await a()),n.stop("Access verified."),pe(o)}catch(P){n.stop("Access verification failed."),E.cancel(P instanceof _&&P.status===401?"Invalid API key.":I(P)),process.exit(1)}}else E.cancel(I(S)),process.exit(1)}E.log.success(`Latest version: ${d}`),h!==d&&E.log.success(`Using template version: ${h}`);let g;e.yes?g=pn(r):g=await Kt(r);let m;n.start("Activating license...");try{let S=crypto.randomUUID();m={token:(await Wt(s,{frontend:g.frontend,version:h,installId:S})).token,keyHash:Rt(o),installId:S},n.stop("License activated.")}catch(S){n.stop("License activation failed."),E.cancel(I(S)),process.exit(1)}let v=Ro(g.projectDir);if(Ao(v)&&ko(v).length>0)if(e.yes)E.log.info(`Directory ${v} is not empty. Merging (keeping existing files, overwriting conflicts).`);else{let P=await E.select({message:`Directory ${v} 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(P)||P==="cancel")&&(E.cancel("Setup cancelled."),process.exit(0)),P==="overwrite"&&Po(v,{recursive:!0,force:!0})}let b={...g,projectDir:v,version:h,...e.demo?{docs:!1}:{}};n.start("Downloading template...");try{await st(s,h,v),n.stop("Template downloaded.")}catch(S){n.stop("Download failed."),E.cancel(I(S)),process.exit(1)}let H;n.start("Generating project files...");try{if({dockerComposeGenerated:H}=await ut(b),!e.demo){let S=await Me(v);await l(_o(v,it),JSON.stringify(S,null," ")+`
|
|
1105
|
+
`),await Xr(v,v)}await en(v,b.aiTools),await qr(b,m),n.stop("Project files generated.")}catch(S){n.stop("Generation failed."),E.cancel(I(S)),process.exit(1)}await rn(v);let R=await nn(v);R&&b.demo!==!0&&e.dbMigration!==!1&&await on(v),await sn(v),R&&await an(v);let te=fe("docker"),A=St(b).map(S=>S.key).filter(S=>!b.credentials?.[S]);cn(b,{pnpmInstalled:R,dockerComposeGenerated:H,dockerAvailable:te,skippedCredentials:A}),Bt(),E.log.info(Oo.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as un}from"fs";import{readFile as jo}from"fs/promises";import{join as Fe,resolve as Mo}from"path";import*as k from"@clack/prompts";import Pe from"picocolors";import{mkdtemp as No,rm as Lo}from"fs/promises";import{tmpdir as Uo}from"os";import{join as $o}from"path";async function Ut(e,t,r,i){let n=await No($o(Uo(),"generatesaas-stage-"));try{await st(e,t,n),await ut({...r,projectDir:n}),await Ot(n,i)}finally{await Lo(n,{recursive:!0,force:!0})}}function mn(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=Mo(t.cwd??process.cwd()),i=Fe(r,W),n;try{n=JSON.parse(await jo(i,"utf-8"))}catch{k.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let o;try{o=await Se()}catch(d){k.cancel(I(d)),process.exit(1)}let s=q(o),a=k.spinner();try{a.start("Verifying access...");let d;try{d=await Q(s)}catch(A){throw A instanceof _&&A.status===401?new Error("Your saved API key was rejected. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):A}a.stop("Access verified."),pe(o),a.start("Fetching latest skill files...");let h=await qt(s,d.latest);await Ct(r,h.skillMd,h.scripts,n.aiTools);let g=xt(n.aiTools);if(a.stop("Skills updated."),k.log.success(`Skill files installed to ${Pe.cyan(g.length.toString())} locations.`),n.version===d.latest){k.log.info(`Already on the latest version (${n.version}).`);return}if(n.licenseToken)try{let A=await Xt(s,{currentToken:n.licenseToken,newVersion:n.version});n.licenseToken=A.token,await l(i,JSON.stringify(n,null," ")+`
|
|
1106
|
+
`),k.log.success("License refreshed.")}catch{k.log.warn("License refresh skipped.")}let m=Jr(n,r),v=Fe(r,Ht);a.start(`Staging v${d.latest} (shaped for your config)...`),await Ut(s,d.latest,m,v),a.stop("Template staged.");let b=await Jt(s,d.latest);b&&k.note(b,`Changelog v${d.latest}`);let H=Fe(r,it),R=Fe(r,ot),te=!un(R),re=!un(H);if(te){if(a.start("Building baseline template (one-time migration)..."),await Ut(s,n.version,m,R),re){let A=await Me(R);await l(H,JSON.stringify(A,null," ")+`
|
|
1107
|
+
`)}a.stop("Baseline template stored.")}else if(re){a.start("Computing baseline template hashes...");let A=await Me(R);await l(H,JSON.stringify(A,null," ")+`
|
|
1108
|
+
`),a.stop("Baseline hashes computed.")}if(await l(Fe(r,Yt),JSON.stringify({currentVersion:n.version,targetVersion:d.latest,changelog:b,stagedAt:new Date().toISOString()},null," ")+`
|
|
1109
1109
|
`),k.log.info(`Update staged: ${Pe.cyan(n.version)} \u2192 ${Pe.cyan(d.latest)}`),n.aiTools&&n.aiTools.length>0){let A=n.aiTools[0],S=xe[A].label;k.log.info(`Open your project in ${Pe.cyan(S)} and ask: ${Pe.cyan("'update my GenerateSaaS project'")}`)}else k.log.info(`Ask your AI coding assistant to ${Pe.cyan("'update my GenerateSaaS project'")}.`)}catch(d){a.stop("Failed."),k.cancel(`Update failed: ${I(d)}`),process.exit(1)}})}import*as C from"@clack/prompts";import M from"picocolors";import{readFile as Vo}from"fs/promises";import{join as Fo,resolve as Bo}from"path";function fn(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=Bo(t.cwd??process.cwd()),i=Fo(r,W),n;try{n=JSON.parse(await Vo(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(`
|
|
1110
|
-
`);C.note(o,M.bold("Project Status"));let s=C.spinner();s.start("Checking for updates...");try{let a=await
|
|
1111
|
-
`),z.yellow("License Details"))}async function hn(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=Ko(i)}catch{return N.log.error("Could not decode JWT payload."),!1}t.start("Verifying signature...");try{let o=process.env.GENERATESAAS_API_URL??
|
|
1112
|
-
`).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 hn(s)&&o++,N.log.info("");N.log.success(`${o}/${n.length} sites verified.`)}else await hn(t)||process.exit(1)})}import{existsSync as zo,rmSync as Ho}from"fs";import*as V from"@clack/prompts";function vn(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){zo(J)?(Ho(J),V.log.success("API key removed.")):V.log.info("No API key configured.");return}let r=Et();r?V.log.info(`Current API key: ****${r.slice(-4)}`):V.log.info("No API key configured.");let i=await Ue(),n=q(i),o=V.spinner();o.start("Verifying API key...");try{await Q(n),o.stop("API key verified."),pe(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 ft,rmSync as Yo,readFileSync as Mt,writeFileSync as Sn}from"fs";import{join as
|
|
1110
|
+
`);C.note(o,M.bold("Project Status"));let s=C.spinner();s.start("Checking for updates...");try{let a=await Se(),d=q(a),g=(await Q(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 Go}from"fs/promises";import*as N from"@clack/prompts";import z from"picocolors";function Ko(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 $t(e){return typeof e!="number"?"unknown":new Date(e*1e3).toISOString().split("T")[0]}function gn(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: ${$t(e.pat)}`,`Last updated: ${$t(e.uat)}`,`Expires: ${$t(e.exp)}`,`Install ID: ${String(e.nid??"unknown")}`].join(`
|
|
1111
|
+
`),z.yellow("License Details"))}async function hn(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=Ko(i)}catch{return N.log.error("Could not decode JWT payload."),!1}t.start("Verifying signature...");try{let o=process.env.GENERATESAAS_API_URL??nt,s=await Zt(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:"),gn(n),!1}return gn(n),!0}function yn(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 Go(r.file,"utf-8")).split(`
|
|
1112
|
+
`).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 hn(s)&&o++,N.log.info("");N.log.success(`${o}/${n.length} sites verified.`)}else await hn(t)||process.exit(1)})}import{existsSync as zo,rmSync as Ho}from"fs";import*as V from"@clack/prompts";function vn(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){zo(J)?(Ho(J),V.log.success("API key removed.")):V.log.info("No API key configured.");return}let r=Et();r?V.log.info(`Current API key: ****${r.slice(-4)}`):V.log.info("No API key configured.");let i=await Ue(),n=q(i),o=V.spinner();o.start("Verifying API key...");try{await Q(n),o.stop("API key verified."),pe(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 ft,rmSync as Yo,readFileSync as Mt,writeFileSync as Sn}from"fs";import{join as ge}from"path";import*as L from"@clack/prompts";var Jo=["packages/api/src/functions/maintenance/license-heartbeat.ts","packages/api/src/lib/manifest.ts","packages/api/src/routes/internal/license.ts"],qo=[{file:"packages/api/src/routes/inngest.ts",removals:[`import { licenseHeartbeatFunction } from "../functions/maintenance/license-heartbeat";
|
|
1113
1113
|
`,` licenseHeartbeatFunction,
|
|
1114
1114
|
`]},{file:"packages/api/src/routes/internal/index.ts",removals:[`import licenseRoutes from "./license";
|
|
1115
1115
|
`,` .route("/license", licenseRoutes)
|
|
1116
|
-
`]}];function Wo(e){return(e&&e.length>0?e.map(r=>
|
|
1116
|
+
`]}];function Wo(e){return(e&&e.length>0?e.map(r=>Ve[r]):Object.values(Ve)).map(r=>ge(r,Dt))}function jt(e){return ft(e)?(Yo(e,{recursive:!0}),!0):!1}function Xo(e,t){if(!ft(e))return!1;let r=Mt(e,"utf-8"),i=r;for(let n of t)i=i.replace(n,"");return i===r?!1:(Sn(e,i,"utf-8"),!0)}function Zo(e){let t=ge(e,".gitignore");if(!ft(t))return!1;let r=Mt(t,"utf-8"),i=r.split(`
|
|
1117
1117
|
`).filter(n=>!n.includes(".generatesaas")).join(`
|
|
1118
|
-
`);return i===r?!1:(Sn(t,i,"utf-8"),!0)}function En(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=
|
|
1118
|
+
`);return i===r?!1:(Sn(t,i,"utf-8"),!0)}function En(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=ge(t,W),i;try{i=JSON.parse(Mt(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 Wo(i.aiTools))jt(ge(t,a))&&o.push(a);for(let a of Jo)jt(ge(t,a))&&o.push(a);jt(ge(t,U))&&o.push(U+"/");for(let a of qo){let d=ge(t,a.file);Xo(d,a.removals)?s.push(a.file):ft(d)&&L.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}Zo(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 he=new Qo().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("1.5.2").addHelpText("after",`
|
|
1119
1119
|
Examples:
|
|
1120
1120
|
$ generatesaas init Interactive setup
|
|
1121
1121
|
$ generatesaas init -n my-app -y Quick setup with defaults
|
|
1122
1122
|
$ generatesaas status Check for updates
|
|
1123
1123
|
$ generatesaas auth Set or update API key
|
|
1124
|
-
`);dn(
|
|
1124
|
+
`);dn(he);mn(he);fn(he);yn(he);vn(he);En(he);he.parseAsync().catch(e=>{wn.cancel("An unexpected error occurred."),console.error(e),process.exit(1)});
|