generatesaas 1.9.3 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as
|
|
3
|
-
`);
|
|
4
|
-
`),p=[` Deploy target: ${
|
|
5
|
-
`),v=[
|
|
6
|
-
`),
|
|
7
|
-
`);
|
|
8
|
-
`),"Summary");let
|
|
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=
|
|
10
|
-
`)}function
|
|
11
|
-
- **Payments:** ${
|
|
2
|
+
import{Command as gs}from"commander";import*as xn from"@clack/prompts";import{existsSync as Lo,readdirSync as $o,rmSync as Uo}from"fs";import{join as jo,resolve as Vo}from"path";import{Option as V}from"commander";import*as E from"@clack/prompts";import*as Ye from"@clack/prompts";import wt from"picocolors";function Jt(e){let t=e?` GenerateSaaS v${e} `:" GenerateSaaS ";Ye.intro(wt.bgYellow(wt.black(t)))}function Wt(){Ye.outro(wt.yellow("Happy building!"))}import*as m from"@clack/prompts";import g from"picocolors";var Ce={nextjs:{label:"Next.js",hint:"React 19 + Next.js 16"},nuxt:{label:"Nuxt",hint:"Vue 3 + Nuxt 4"}},Ne={fullstack:{label:"Fullstack",hint:"Frontend hosts the API - works on serverless or long-running"},separate:{label:"Separate",hint:"Standalone Hono backend - long-running runtimes only (Docker, Render, Fly.io, Railway, Coolify, Dokploy)"}},Je={stripe:{label:"Stripe"},polar:{label:"Polar"},none:{label:"None",hint:"disable payments"}},We={smtp:{label:"SMTP",hint:"Mailpit for local dev"},ses:{label:"Amazon SES"},resend:{label:"Resend"}},qe={user:{label:"Per user",hint:"each user has their own subscription"},organization:{label:"Per organization",hint:"org subscription shared by members"}},we={postgres:{label:"PostgreSQL",hint:"port 5432",port:5432},redis:{label:"Redis",hint:"port 6379",port:6379},inngest:{label:"Inngest",hint:"port 8288",port:8288},mailpit:{label:"Mailpit",hint:"port 1025",port:1025}},Le={"claude-code":{label:"Claude Code"},cursor:{label:"Cursor"},codex:{label:"Codex"},"gemini-cli":{label:"Gemini CLI"},windsurf:{label:"Windsurf"}};var se={USD:{symbol:"$",name:"US Dollar",place:"left",space:!1},EUR:{symbol:"\u20AC",name:"Euro",place:"right",space:!1},GBP:{symbol:"\xA3",name:"British Pound",place:"left",space:!1},CAD:{symbol:"CA$",name:"Canadian Dollar",place:"left",space:!1},AUD:{symbol:"A$",name:"Australian Dollar",place:"left",space:!1},BRL:{symbol:"R$",name:"Brazilian Real",place:"left",space:!1},JPY:{symbol:"\xA5",name:"Japanese Yen",place:"left",space:!1}};var B={node:{label:"Node.js / Docker",hint:"long-running runtime - Render, Fly.io, Railway, Coolify, Dokploy, VPS",edgeRuntime:!1},vercel:{label:"Vercel",hint:"serverless functions",edgeRuntime:!0}},N={postgres:{label:"PostgreSQL (self-hosted)",hint:"local Docker, drizzle-orm/node-postgres",managed:!1,envVars:[{key:"DATABASE_URL",defaultValue:"postgres://postgres:postgres@localhost:5432/saas"}]},neon:{label:"Neon",hint:"serverless Postgres",managed:!0,envVars:[{key:"DATABASE_URL",comment:"# TODO: Add your Neon connection string"}]},supabase:{label:"Supabase",hint:"managed Postgres",managed:!0,envVars:[{key:"DATABASE_URL",comment:"# TODO: Add your Supabase connection string"}]}},G={redis:{label:"Redis (self-hosted)",hint:"local Docker, ioredis",managed:!1,envVars:[{key:"REDIS_URL",defaultValue:"redis://localhost:6379"}]},upstash:{label:"Upstash",hint:"serverless Redis",managed:!0,envVars:[{key:"UPSTASH_REDIS_REST_URL",comment:"# TODO: Add your Upstash REST URL"},{key:"UPSTASH_REDIS_REST_TOKEN",comment:"# TODO: Add your Upstash REST token"}]}},$e=[{target:"vercel",provider:"redis",reason:"Vercel serverless cannot maintain persistent Redis connections. Consider Upstash."}],Ue=[{architecture:"separate",target:"vercel",reason:"Standalone backends need a long-running runtime. Vercel's per-function TypeScript check conflicts with pnpm's isolated install. Use --architecture fullstack for serverless, or deploy the standalone backend to a long-running runtime (Render, Fly.io, Railway, Coolify, Dokploy, or your own VPS via Docker)."}],qt=["Local file storage (sharp, geoip-lite)","SMTP email (use Resend or SES instead)","Content API git integration"];function Xe(e){let t=N[e.databaseProvider].managed,r=G[e.cacheProvider].managed;return e.dockerServices.some(i=>!(i==="postgres"&&t||i==="redis"&&r))}var W={google:{label:"Google",envVars:[{name:"GOOGLE_CLIENT_ID",secret:!1},{name:"GOOGLE_CLIENT_SECRET",secret:!0}]},github:{label:"GitHub",envVars:[{name:"GITHUB_CLIENT_ID",secret:!1},{name:"GITHUB_CLIENT_SECRET",secret:!0}]},facebook:{label:"Facebook",envVars:[{name:"FACEBOOK_CLIENT_ID",secret:!1},{name:"FACEBOOK_CLIENT_SECRET",secret:!0}]},discord:{label:"Discord",envVars:[{name:"DISCORD_CLIENT_ID",secret:!1},{name:"DISCORD_CLIENT_SECRET",secret:!0}]},x:{label:"X",envVars:[{name:"TWITTER_CLIENT_ID",secret:!1},{name:"TWITTER_CLIENT_SECRET",secret:!0}]}};var je=["nextjs","nuxt"],Ze=["fullstack","separate"],Qe=["stripe","polar","none"],et=["smtp","ses","resend"],tt=["postgres","redis","inngest","mailpit"],rt=["claude-code","cursor","codex","gemini-cli","windsurf"],nt=["user","organization"],ae=["USD","EUR","GBP","CAD","AUD","BRL","JPY"],ce=["node","vercel"],le=["postgres","neon","supabase"],pe=["redis","upstash"],it=["google","github","facebook","discord","x"];function ot(e){return e.split("-").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function st(e){return/^[a-z][a-z0-9-]*$/.test(e)}function I(e){return e instanceof Error?e.message:String(e)}function w(e){m.isCancel(e)&&(m.cancel("Setup cancelled."),process.exit(0))}function bt(e){let t=[];e.databaseProvider==="neon"&&t.push({key:"DATABASE_URL",message:"Neon connection string (optional):",placeholder:"postgres://...",secret:!0}),e.databaseProvider==="supabase"&&t.push({key:"DATABASE_URL",message:"Supabase connection string (optional):",placeholder:"postgres://...",secret:!0}),e.cacheProvider==="upstash"&&(t.push({key:"UPSTASH_REDIS_REST_URL",message:"Upstash REST URL (optional):",placeholder:"https://...",secret:!1}),t.push({key:"UPSTASH_REDIS_REST_TOKEN",message:"Upstash REST token (optional):",secret:!0})),e.paymentProvider==="stripe"&&(t.push({key:"STRIPE_SECRET_KEY",message:"Stripe secret key (optional):",placeholder:"sk_test_...",secret:!0}),t.push({key:"STRIPE_WEBHOOK_SECRET",message:"Stripe webhook secret (optional):",placeholder:"whsec_...",secret:!0})),e.paymentProvider==="polar"&&(t.push({key:"POLAR_ACCESS_TOKEN",message:"Polar access token (optional):",secret:!0}),t.push({key:"POLAR_WEBHOOK_SECRET",message:"Polar webhook secret (optional):",secret:!0})),e.emailProvider==="resend"&&t.push({key:"RESEND_API_KEY",message:"Resend API key (optional):",placeholder:"re_...",secret:!0}),e.emailProvider==="ses"&&(t.push({key:"AMAZON_SES_REGION",message:"Amazon SES region (optional):",placeholder:"us-east-1",secret:!1}),t.push({key:"AMAZON_SES_KEY",message:"Amazon SES access key (optional):",secret:!0}),t.push({key:"AMAZON_SES_SECRET",message:"Amazon SES secret (optional):",secret:!0}));for(let r of e.socialProviders){let i=W[r];for(let n of i.envVars)t.push({key:n.name,message:`${n.name} (${i.label}, optional):`,secret:n.secret})}return e.demo&&t.push({key:"TURNSTILE_SECRET_KEY",message:"Cloudflare Turnstile secret key (optional):",secret:!0}),t}async function Xt(e){let t=!1;m.log.info(g.bold("Project"));let r=e?.projectName??await(async()=>{t=!0;let c=await m.text({message:"Project name:",placeholder:"my-saas",validate:p=>{if(!p?.trim())return"Project name is required.";if(!st(p))return"Use lowercase letters, numbers, and hyphens only. Must start with a letter."}});return w(c),c})(),i=e?.appName??await(async()=>{t=!0;let c=await m.text({message:"App name:",initialValue:ot(r),validate:p=>{if(!p?.trim())return"App name is required."}});return w(c),c})(),n=e?.projectDir??await(async()=>{t=!0;let c=await m.text({message:"Project location:",initialValue:`./${r}`});return w(c),c==="."?process.cwd():c})(),o=e?.frontend??await(async()=>{t=!0;let c=Object.keys(Ce),p=await m.select({message:"Frontend framework:",options:c.map(v=>({value:v,label:Ce[v].label,hint:Ce[v].hint}))});return w(p),p})();m.log.info(g.bold("Infrastructure"));let s=e?.deploymentTarget??"node";if(e?.deploymentTarget===void 0){t=!0;let c=await m.select({message:"Deployment target:",options:ce.map(p=>({value:p,label:B[p].label,hint:B[p].hint}))});w(c),s=c}let a=e?.architecture?Ue.find(c=>c.architecture===e.architecture&&c.target===s):void 0;if(a)throw new Error(`Incompatible: --architecture ${a.architecture} + --deploy ${a.target}. ${a.reason}`);let d=new Set(Ue.filter(c=>c.target===s).map(c=>c.architecture)),y=Ze.filter(c=>!d.has(c)),h=e?.architecture??await(async()=>{if(y.length===1){let p=y[0];return m.log.info(`Auto-selected ${Ne[p].label} architecture (only compatible option for ${B[s].label}).`),p}t=!0;let c=await m.select({message:"Architecture:",options:y.map(p=>({value:p,label:Ne[p].label,hint:Ne[p].hint}))});return w(c),c})(),f=e?.databaseProvider??await(async()=>{t=!0;let c=le.filter(v=>!$e.some(D=>D.target===s&&D.provider===v));if(c.length===1){let v=c[0];return m.log.info(`Auto-selected ${N[v].label} (only compatible option for ${B[s].label}).`),v}let p=await m.select({message:"Database provider:",options:c.map(v=>({value:v,label:N[v].label,hint:N[v].hint}))});return w(p),p})(),S=e?.cacheProvider??await(async()=>{t=!0;let c=pe.filter(v=>!$e.some(D=>D.target===s&&D.provider===v));if(c.length===1){let v=c[0];return m.log.info(`Auto-selected ${G[v].label} (only compatible option for ${B[s].label}).`),v}let p=await m.select({message:"Cache provider:",options:c.map(v=>({value:v,label:G[v].label,hint:G[v].hint}))});return w(p),p})();if(B[s]?.edgeRuntime){let c=qt.map(p=>` - ${p}`).join(`
|
|
3
|
+
`);m.note(c,"Unavailable on edge runtime")}m.log.info(g.bold("Features"));let k=e?.paymentProvider??await(async()=>{t=!0;let c=await m.select({message:"Payment provider:",options:Qe.map(p=>({value:p,label:Je[p].label,hint:Je[p].hint}))});return w(c),c})(),ie=e?.defaultCurrency??await(async()=>{if(k==="none")return"USD";t=!0;let c=await m.select({message:"Default currency:",options:ae.map(p=>({value:p,label:p,hint:se[p].name}))});return w(c),c})(),x=e?.emailProvider??await(async()=>{t=!0;let c=await m.select({message:"Email provider:",options:et.map(p=>({value:p,label:We[p].label,hint:We[p].hint}))});return w(c),c})(),$=e?.multiTenancy??await(async()=>{t=!0;let c=await m.confirm({message:"Enable multi-tenancy (organizations)?",initialValue:!1});return w(c),c})(),Se=e?.billingScope??"user";if($&&e?.billingScope===void 0){t=!0;let c=await m.select({message:"Billing scope:",options:nt.map(p=>({value:p,label:qe[p].label,hint:qe[p].hint}))});w(c),Se=c}let Z=e?.blog??await(async()=>{t=!0;let c=await m.confirm({message:"Enable blog?",initialValue:!0});return w(c),c})(),u=e?.docs??await(async()=>{t=!0;let c=await m.confirm({message:"Include docs app? (self-hosted Fumadocs documentation site)",initialValue:!1});return w(c),c})(),b=e?.revenueSharing??await(async()=>{t=!0;let c=await m.confirm({message:"Enable revenue sharing? (opt-in MRR leaderboard with dofollow backlinks)",initialValue:!1});return w(c),c})(),H=k==="none"?!1:e?.credits??await(async()=>{t=!0;let c=await m.confirm({message:"Enable credits? (metered usage on top of subscription plans)",initialValue:!0});return w(c),c})(),Y=e?.socialProviders??await(async()=>{t=!0;let c=it.map(v=>({value:v,label:W[v].label,hint:`requires ${W[v].envVars.map(D=>D.name).join(" / ")}`})),p=await m.multiselect({message:"Which social login providers should the sign-in screen show?",options:c,initialValues:[],required:!1});return w(p),p})();m.log.info(g.bold("Tooling"));let J=e?.dockerServices??await(async()=>{t=!0;let c=[...tt].filter(C=>C!=="mailpit");x==="smtp"&&c.push("mailpit");let p=c.map(C=>({value:C,label:we[C].label,hint:we[C].hint})),v=p.map(C=>C.value).filter(C=>!(C==="postgres"&&(f==="neon"||f==="supabase")||C==="redis"&&S==="upstash")),D=await m.multiselect({message:"Which services should we set up in Docker for you?",options:p,initialValues:v,required:!1});return w(D),D})(),Ee=e?.aiTools??await(async()=>{t=!0;let c=rt.map(v=>({value:v,label:Le[v].label})),p=await m.multiselect({message:"Which AI coding tools do you use?",options:c,initialValues:[],required:!1});return w(p),p})(),Et=e?.demo,He=bt({databaseProvider:f,cacheProvider:S,paymentProvider:k,emailProvider:x,socialProviders:Y,demo:Et}),De={};if(He.length>0&&t){m.log.info(g.bold("Credentials")+g.dim(" all optional - press Enter to skip, fill in .env later"));for(let c of He)if(t=!0,c.secret){let p=await m.password({message:c.message,mask:"*"});w(p),typeof p=="string"&&p.trim()&&(De[c.key]=p.trim())}else{let p=await m.text({message:c.message,placeholder:c.placeholder});w(p),typeof p=="string"&&p.trim()&&(De[c.key]=p.trim())}}if(t){let c=[` Name: ${g.cyan(r)}`,` App name: ${g.cyan(i)}`,` Location: ${g.cyan(n)}`,` Frontend: ${g.cyan(Ce[o].label)}`,` Architecture: ${g.cyan(Ne[h].label)}`].join(`
|
|
4
|
+
`),p=[` Deploy target: ${g.cyan(B[s]?.label??"Node.js / Docker")}`,` Database: ${g.cyan(N[f].label)}`,` Cache: ${g.cyan(G[S].label)}`,J.length>0?` Docker: ${g.cyan(J.map(oe=>we[oe].label).join(", "))}`:` Docker: ${g.dim("none")}`].filter(Boolean).join(`
|
|
5
|
+
`),v=[k!=="none"?` Payment: ${g.cyan(Je[k].label)} (${ie})`:` Payment: ${g.dim("none")}`,` Credits: ${H?g.cyan("Yes"):g.dim("No")}`,` Email: ${g.cyan(We[x].label)}`,` Multi-tenancy: ${$?g.cyan("Yes")+` (billing: ${qe[Se].label})`:g.dim("No")}`,` Blog: ${Z?g.cyan("Yes"):g.dim("No")}`,` Docs app: ${u?g.cyan("Yes"):g.dim("No")}`,` Rev. sharing: ${b?g.cyan("Yes"):g.dim("No")}`,Y.length>0?` Social login: ${g.cyan(Y.map(oe=>W[oe].label).join(", "))}`:` Social login: ${g.dim("none")}`,Ee.length>0?` AI tools: ${g.cyan(Ee.map(oe=>Le[oe].label).join(", "))}`:` AI tools: ${g.dim("none")}`].join(`
|
|
6
|
+
`),D=[g.bold("Project"),c,"",g.bold("Infrastructure"),p,"",g.bold("Features"),v];if(He.length>0){let oe=He.map(Yt=>{let Dn=De[Yt.key]?g.green("provided"):g.dim("skipped");return` ${Yt.key}: ${Dn}`}).join(`
|
|
7
|
+
`);D.push("",g.bold("Credentials"),oe)}m.note(D.join(`
|
|
8
|
+
`),"Summary");let C=await m.confirm({message:"Proceed with these settings?"});(m.isCancel(C)||!C)&&(m.cancel("Setup cancelled."),process.exit(0))}return{projectName:r,appName:i,projectDir:n,frontend:o,architecture:h,deploymentTarget:s,databaseProvider:f,cacheProvider:S,paymentProvider:k,emailProvider:x,multiTenancy:$,billingScope:Se,blog:Z,docs:u,revenueSharing:b,credits:H,dockerServices:J,aiTools:Ee,socialProviders:Y,defaultCurrency:ie,...Object.keys(De).length>0?{credentials:De}:{},...e?.baseUrl!==void 0?{baseUrl:e.baseUrl}:{},...Et!==void 0?{demo:Et}:{}}}import{createReadStream as Fn}from"fs";import{mkdir as Bn}from"fs/promises";import{Readable as Gn}from"stream";import{pipeline as or}from"stream/promises";import{extract as Kn}from"tar";import{join as de}from"path";import{homedir as Cn}from"os";var Ve=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",U=".generatesaas",Q=de(U,"manifest.json"),Zt=de(U,"hashes.json"),at=de(U,"template-hashes.json"),ct=de(U,"template"),Qt=de(U,"staging"),er=de(U,"staging.json"),q=de(Cn(),".generatesaas");var R=class extends Error{constructor(r,i,n){super(i);this.status=r;this.body=n}status;body;name="ApiError"};function X(e){return{apiKey:e,baseUrl:Ve}}async function ee(e,t,r){let i=`${e.baseUrl}${t}`,n=await fetch(i,{...r,headers:{...r?.headers,Authorization:`Bearer ${e.apiKey}`,"User-Agent":"generatesaas-cli"}});if(!n.ok){let o,s;try{s=await n.json(),o=s.error??`API ${n.status}: ${t}`}catch{o=`API ${n.status}: ${t}`}throw new R(n.status,o,s)}return n}import{existsSync as Nn,readFileSync as Ln,writeFileSync as $n,mkdirSync as Un}from"fs";import{dirname as jn}from"path";import*as te from"@clack/prompts";function Me(){if(!Nn(q))return null;try{let e=JSON.parse(Ln(q,"utf-8"));return e.apiKey?e.apiKey:(e.token&&!e.apiKey&&te.log.warning(`Found old GitHub token in ${q}. Run 'generatesaas init' to set up your API key.`),null)}catch{return null}}function ue(e){Un(jn(q),{recursive:!0}),$n(q,JSON.stringify({apiKey:e},null," ")+`
|
|
9
|
+
`,{mode:384})}async function be(e){if(e?.apiKey)return e.apiKey;let t=process.env.GENERATESAAS_API_KEY;if(t)return t;let r=Me();if(r)return r;if(!e?.prompt)throw new Error("API key not found. Set GENERATESAAS_API_KEY or run 'generatesaas init' to configure.");return Fe()}async function Fe(){let e=await te.text({message:"Enter your GenerateSaaS API key:",placeholder:"gs_live_...",validate:t=>{if(!t?.trim())return"API key is required."}});return te.isCancel(e)&&(te.cancel("Setup cancelled."),process.exit(0)),e.trim()}async function re(e){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{latest:"0.0.0-ci",versions:[{version:"0.0.0-ci",date:new Date().toISOString(),breaking:!1}]}:await(await ee(e,"/versions")).json()}async function At(e,t){try{return await(await ee(e,`/changelog/${encodeURIComponent(t)}`)).text()}catch(r){if(r instanceof R&&r.status===404)return null;throw r}}async function tr(e,t){return await(await ee(e,`/skill/${encodeURIComponent(t)}`)).json()}function lt(e){if(!(e instanceof R)||e.status!==403)return null;let t=e.body;return!t||t.code!=="update_window_expired"?null:{message:e.message,lastAllowedVersion:t.lastAllowedVersion??null}}function rr(e){return{frontend:e.frontend,architecture:e.architecture,deployTarget:e.deploymentTarget,database:e.databaseProvider,cache:e.cacheProvider,payment:e.paymentProvider,email:e.emailProvider,multiTenancy:e.multiTenancy,billingScope:e.billingScope,blog:e.blog,docs:e.docs,credits:e.credits,revenueSharing:e.revenueSharing,socialProviders:e.socialProviders,aiTools:e.aiTools,currency:e.defaultCurrency}}async function Tt(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:"offline-test-token",licenseId:"offline-test-license-id"}:await(await ee(e,"/license/sign",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function nr(e,t){return await(await ee(e,"/license/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function kt(e,t){let r=await fetch(`${e}/license/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok)throw new Error(`Verification service returned ${r.status}`);return await r.json()}async function ir(e,t,r){let i=await fetch(`${e}/license/inspect`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify(r)});if(!i.ok){let n=await i.json().catch(()=>null);throw new Error(n?.error??`Inspect endpoint returned ${i.status}`)}return await i.json()}var It=new Set([".git","node_modules",".pnpm-store",".env",".env.test",".turbo",".nuxt",".output",".data","dist",".next",".svelte-kit",".wrangler",".devcontainer","playwright-report","test-results"]),Pt=new Set(["pnpm-lock.yaml"]);function Ae(e){if(_t(e))return!0;for(let t of e.split("/"))if(Pt.has(t))return!0;return!1}var Vn=new Set(["data","mksaas","references","scripts",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","README.md","TODO.md","OVERVIEW.md"]),Mn=["docs/superpowers","packages/cli","packages/cli-api","infra/docker-compose.yml",".claude/commands",".claude/skills/web-next-port",".claude/skills/web-next-port-workspace",".claude/settings.local.json",".claude/worktrees"];function _t(e){let t=e.split("/");for(let r of t)if(It.has(r))return!0;if(Vn.has(t[0]))return!0;for(let r of Mn)if(e===r||e.startsWith(r+"/"))return!0;return!1}async function pt(e,t,r){await Bn(r,{recursive:!0});let i=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(i){await or(Fn(i),sr(r));return}let n=await ee(e,`/template/${encodeURIComponent(t)}`);if(!n.body)throw new Error("Empty response body");let o=Gn.fromWeb(n.body);await or(o,sr(r))}function sr(e){return Kn({cwd:e,strip:1,filter:t=>{let r=t.replace(/^[^/]+\//,"");return r?!_t(r):!0},sync:!1})}import{readFile as zn,rm as ar,writeFile as Hn}from"fs/promises";import{join as Rt}from"path";var Yn=["apps/web-nuxt/public/images/blog","apps/web-next/public/images/blog","packages/content/en/blog","packages/content/ro/blog"];async function cr(e){await Promise.all(Yn.map(t=>ar(Rt(e,t),{recursive:!0,force:!0})))}async function lr(e,t){t.includes("claude-code")||await ar(Rt(e,".claude"),{recursive:!0,force:!0})}async function pr(e,t){let r=Rt(e,".claude","settings.json"),i;try{i=await zn(r,"utf8")}catch{return}let n=JSON.parse(i);delete n.alwaysThinkingEnabled,delete n.enableAllProjectMcpServers,n.env&&(delete n.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS,Object.keys(n.env).length===0&&delete n.env),n.permissions?.allow&&(n.permissions.allow=n.permissions.allow.filter(o=>Jn(o,t))),await Hn(r,JSON.stringify(n,null," ")+`
|
|
10
|
+
`)}function Jn(e,t){return!(e.startsWith("mcp__")||t!=="nuxt"&&e.includes("nuxt"))}import{join as dr}from"path";import{mkdir as Wn,readdir as qn,rm as Xn,rmdir as Zn,writeFile as Qn}from"fs/promises";import{dirname as dt,join as ei,relative as ti}from"path";async function ut(e){await Wn(e,{recursive:!0})}async function l(e,t){await ut(dt(e)),await Qn(e,t,"utf-8")}async function Ot(e,t){await Xn(e,{force:!0});let r=dt(e);for(;r!==t&&r!==dt(r);){try{await Zn(r)}catch{return}r=dt(r)}}async function me(e,t,r){let i=[],n=await qn(e,{withFileTypes:!0});for(let o of n){let s=ei(e,o.name),a=ti(t,s);r(a)||(o.isDirectory()?i.push(...await me(s,t,r)):o.isFile()&&i.push(s))}return i}var ri={postgres:"Postgres",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"},ni={resend:"Resend",ses:"Amazon SES",smtp:"SMTP"},ii={redis:"Redis",upstash:"Upstash Redis"},oi={node:"Node.js / Docker",vercel:"Vercel"},si={stripe:"Stripe",polar:"Polar"};function ai(e){let t=e.frontend==="nuxt",r=t?"Nuxt 4":"Next.js 16",i=t?"apps/web-nuxt":"apps/web-next",n=t?"`app/` pages + components + composables":"`app/` routes, `components/`, `lib/`",o=e.architecture==="fullstack",s=o?"(fullstack - Hono API mounted inside the app)":"(separate - standalone Hono backend)",a=o?`Mounted inside \`${i}\`. A standalone \`apps/backend\` is also kept (inert) so you can switch to separate later.`:"Runs from `apps/backend`; the frontend reaches it over HTTP.",d=ii[e.cacheProvider],y=oi[e.deploymentTarget],h=e.paymentProvider==="none"?"":`
|
|
11
|
+
- **Payments:** ${si[e.paymentProvider]}`,f=t?"`$t('key')` in templates (global helper); `useI18n()` from `vue-i18n` in `<script setup>` for `locale`/`setLocale`/`t()`.":"`useTranslations()` from `next-intl` in components; messages are loaded in `apps/web-next/i18n/request.ts`.",S=t?"**Navigation:** always use `localePath()` for paths (hardcoded paths break non-default locales); `await navigateTo()` in SSR.":"**Navigation:** `next/link` for links; `redirect()` from `next/navigation` for programmatic redirects in Server Components.";return`# AGENTS.md
|
|
12
12
|
|
|
13
13
|
Guidelines for AI coding agents (Claude Code, Codex, Cursor, \u2026) in this project.
|
|
14
14
|
Keep this file tight: add a rule only when it prevents a recurring mistake - too
|
|
@@ -22,7 +22,7 @@ and go find it:
|
|
|
22
22
|
|
|
23
23
|
1. \`docs/${t?"nuxt":"next"}/index.mdx\` - the catalog of every shipped feature and its config flag.
|
|
24
24
|
2. \`packages/config/src/index.ts\` - the flag that turns the feature on/off (check it before rendering).
|
|
25
|
-
3. Search \`packages/\` and \`${
|
|
25
|
+
3. Search \`packages/\` and \`${i}/\` for the name before creating anything.
|
|
26
26
|
|
|
27
27
|
Already built - extend these, never re-implement: auth (email / OAuth / 2FA / passkeys),
|
|
28
28
|
billing & subscriptions, credits, organizations & teams, notifications, email, SMS,
|
|
@@ -36,11 +36,11 @@ flag, route, or translation is the most common and most costly mistake in this r
|
|
|
36
36
|
|
|
37
37
|
- **Frontend:** ${r} ${s}
|
|
38
38
|
- **API:** Hono, RPC-typed. ${a}
|
|
39
|
-
- **Database:** Drizzle ORM + ${
|
|
39
|
+
- **Database:** Drizzle ORM + ${ri[e.databaseProvider]}
|
|
40
40
|
- **Cache + jobs:** ${d} + Inngest
|
|
41
|
-
- **Auth:** Better Auth${
|
|
42
|
-
- **Email:** ${
|
|
43
|
-
- **Deploy:** ${
|
|
41
|
+
- **Auth:** Better Auth${h}
|
|
42
|
+
- **Email:** ${ni[e.emailProvider]}
|
|
43
|
+
- **Deploy:** ${y}
|
|
44
44
|
|
|
45
45
|
## Where things live (extend these - don't reinvent)
|
|
46
46
|
|
|
@@ -50,7 +50,7 @@ flag, route, or translation is the most common and most costly mistake in this r
|
|
|
50
50
|
- \`packages/auth/src/config.ts\` - Better Auth config.
|
|
51
51
|
- \`packages/runtime/src/env.ts\` - the validated (Zod) env schema. New env var \u2192 add it here **and** to \`.env.example\` (the committed reference); set the local value in \`.env\`.
|
|
52
52
|
- \`packages/{payments,mail,sms,storage,notifications}\` - config-gated integrations. Every provider's files stay even when its feature is off, so flipping a flag is enough to enable it.
|
|
53
|
-
- \`${
|
|
53
|
+
- \`${i}\` - the ${r} app (${n}).
|
|
54
54
|
- \`docs/${t?"nuxt":"next"}/\` - feature & architecture documentation for this stack (Markdown). Consult it before searching from scratch; it cites real config keys, package names, and file paths you can act on.
|
|
55
55
|
|
|
56
56
|
## Code style
|
|
@@ -68,7 +68,7 @@ flag, route, or translation is the most common and most costly mistake in this r
|
|
|
68
68
|
|
|
69
69
|
- **Hono routes:** keep the method chain (\`.get().post()\`) - RPC type inference depends on it. Validate with \`sValidator\` from \`@hono/standard-validator\`.
|
|
70
70
|
- **Feature flags:** respect \`config.*\` - hide/skip a feature when its flag is off (the files stay so you can flip it later).
|
|
71
|
-
- **i18n:** strings live in \`packages/i18n/translations/{locale}/{scope}.json\`; edit \`en/\` only, keep keys generic. ${
|
|
71
|
+
- **i18n:** strings live in \`packages/i18n/translations/{locale}/{scope}.json\`; edit \`en/\` only, keep keys generic. ${f} A pre-commit hook runs \`pnpm translate\` (needs \`OPENROUTER_API_KEY\`) to sync other locales from \`en\`; without the key it skips.
|
|
72
72
|
- ${S}
|
|
73
73
|
- **Routes:** use \`config.routes.*\`, never hardcoded path strings.
|
|
74
74
|
- **Secrets:** never send them to an external service; generate tokens/QR codes client-side.
|
|
@@ -80,20 +80,20 @@ In \`@repo/*\` backend packages prefer web-standard APIs: \`crypto.randomUUID()\
|
|
|
80
80
|
## This project
|
|
81
81
|
|
|
82
82
|
Scaffolded from the GenerateSaaS boilerplate. To pull upstream boilerplate updates, ask your agent to **"update my GenerateSaaS project"**. To remove the license heartbeat + manifest: \`pnpm dlx generatesaas eject\`.
|
|
83
|
-
`}async function
|
|
84
|
-
`)}import{join as
|
|
83
|
+
`}async function ur(e){await l(dr(e.projectDir,"AGENTS.md"),ai(e)),await l(dr(e.projectDir,"CLAUDE.md"),`@AGENTS.md
|
|
84
|
+
`)}import{join as ci}from"path";var li={postgres:"Postgres (self-hosted)",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"};function pi(e){let t=e.appName.trim()||e.projectName,r=e.frontend==="nuxt",i=r?"Nuxt 4":"Next.js 16",n=r?"apps/web-nuxt":"apps/web-next",o=li[e.databaseProvider],s=Xe(e),a=e.architecture==="fullstack"?`${i} app at \`${n}/\` with the Hono API mounted inside it. A standalone \`apps/backend/\` is also included (inert in this fullstack setup) so you can split the API into a separate service later without re-scaffolding.`:`${i} app at \`${n}/\` and a separate Hono backend at \`apps/backend/\`.`,d=e.architecture==="fullstack"?`- App + API: http://localhost:3000
|
|
85
85
|
- Inngest dev server: http://127.0.0.1:8288`:`- App: http://localhost:3000
|
|
86
86
|
- API: http://localhost:3010
|
|
87
|
-
- Inngest dev server: http://127.0.0.1:8288`,
|
|
88
|
-
`:"",
|
|
87
|
+
- Inngest dev server: http://127.0.0.1:8288`,y=s?`pnpm infra # optional: starts local Docker services (Postgres / Redis / Inngest / Mailpit)
|
|
88
|
+
`:"",h=e.deploymentTarget==="vercel"?"### Deployment\n\nDeploy to Vercel: `vercel deploy` (or connect the repo in the Vercel dashboard). Required environment variables are listed in `.env`.":`### Deployment
|
|
89
89
|
|
|
90
|
-
This project ships with Dockerfiles for each app. Build images with \`docker build\` and deploy to your runtime of choice (Render / Fly.io / Railway / Coolify / Dokploy / your own VPS).${s?"\n\nThe `infra/` directory ships a Docker Compose file for the local-only services (Postgres / Redis / Inngest / Mailpit, filtered by your provider choices).":""}`,
|
|
90
|
+
This project ships with Dockerfiles for each app. Build images with \`docker build\` and deploy to your runtime of choice (Render / Fly.io / Railway / Coolify / Dokploy / your own VPS).${s?"\n\nThe `infra/` directory ships a Docker Compose file for the local-only services (Postgres / Redis / Inngest / Mailpit, filtered by your provider choices).":""}`,f=e.aiTools.length>0?"To pull the latest boilerplate changes into this project, open it in your AI coding agent and ask: `update my GenerateSaaS project`.":"To pull the latest boilerplate changes into this project, install an AI coding agent (Claude Code / Cursor / Codex / Gemini CLI / Windsurf) and ask: `update my GenerateSaaS project`. The skill bundle that drives the update lives under each tool's skill root.";return`# ${t}
|
|
91
91
|
|
|
92
92
|
${a}
|
|
93
93
|
|
|
94
94
|
## Stack
|
|
95
95
|
|
|
96
|
-
- **Frontend:** ${
|
|
96
|
+
- **Frontend:** ${i}
|
|
97
97
|
- **Backend:** Hono (TypeScript, RPC-typed)
|
|
98
98
|
- **Auth:** Better Auth
|
|
99
99
|
- **ORM / DB:** Drizzle + ${o}
|
|
@@ -107,7 +107,7 @@ pnpm install
|
|
|
107
107
|
# A ready-to-run \`.env\` was generated for you - open it and fill in the
|
|
108
108
|
# values marked \`# TODO\` (provider API keys). \`.env.example\` is the committed
|
|
109
109
|
# reference. All apps load the single root \`.env\`.
|
|
110
|
-
${
|
|
110
|
+
${y}pnpm dev
|
|
111
111
|
\`\`\`
|
|
112
112
|
|
|
113
113
|
${d}
|
|
@@ -129,7 +129,7 @@ pnpm -F @repo/database studio # open Drizzle Studio
|
|
|
129
129
|
pnpm auth:generate # regenerate Better Auth schema after config changes
|
|
130
130
|
\`\`\`
|
|
131
131
|
|
|
132
|
-
${
|
|
132
|
+
${h}
|
|
133
133
|
|
|
134
134
|
## Removing the GenerateSaaS license
|
|
135
135
|
|
|
@@ -141,20 +141,20 @@ pnpm dlx generatesaas eject
|
|
|
141
141
|
|
|
142
142
|
## Updates
|
|
143
143
|
|
|
144
|
-
${
|
|
145
|
-
`}async function
|
|
144
|
+
${f}
|
|
145
|
+
`}async function mr(e){await l(ci(e.projectDir,"README.md"),pi(e))}function di(e){let t=e.split(".");return t.length>=3?t.slice(1).join("."):e}async function fr(e){let t=e.appName.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),r=e.paymentProvider!=="none",i=e.baseUrl??"http://localhost:3000",n=e.baseUrl?new URL(e.baseUrl).hostname:"example.com",o=di(n),s=e.demo?"false":"true",a=e.demo?`
|
|
146
146
|
support: {
|
|
147
147
|
enableInDev: true,
|
|
148
148
|
crisp: { websiteId: "7e221cec-ed61-46b7-b1b4-8cbc16557cca" }
|
|
149
|
-
},`:"",d=e.frontend==="nextjs"&&e.architecture==="fullstack"?"false":"true",
|
|
149
|
+
},`:"",d=e.frontend==="nextjs"&&e.architecture==="fullstack"?"false":"true",y=`import type { AppConfig } from "@repo/config/types";
|
|
150
150
|
|
|
151
151
|
const trustedOrigins = process.env.TRUSTED_ORIGINS?.split(",").map((s) => s.trim()).filter(Boolean);
|
|
152
152
|
|
|
153
153
|
export const config: AppConfig = {
|
|
154
154
|
siteName: "${t}",
|
|
155
155
|
fullSiteName: "${t}",
|
|
156
|
-
domain: "${
|
|
157
|
-
baseUrl: process.env.BASE_URL ?? "${
|
|
156
|
+
domain: "${n}",
|
|
157
|
+
baseUrl: process.env.BASE_URL ?? "${i}",
|
|
158
158
|
indexable: ${s},
|
|
159
159
|
logo: {
|
|
160
160
|
main: "/images/logo.svg",
|
|
@@ -174,6 +174,7 @@ export const config: AppConfig = {
|
|
|
174
174
|
background: "staticLightRays"
|
|
175
175
|
},
|
|
176
176
|
cookieBanner: "auto",
|
|
177
|
+
consentPolicy: "gdpr-and-unknown",
|
|
177
178
|
auth: {
|
|
178
179
|
socialProviders: ${JSON.stringify(e.socialProviders)}
|
|
179
180
|
},
|
|
@@ -254,7 +255,7 @@ export const config: AppConfig = {
|
|
|
254
255
|
}`:`{
|
|
255
256
|
base: "${e.defaultCurrency}",
|
|
256
257
|
list: [
|
|
257
|
-
{ symbol: "${
|
|
258
|
+
{ symbol: "${se[e.defaultCurrency].symbol}", name: "${se[e.defaultCurrency].name}", code: "${e.defaultCurrency}", place: "${se[e.defaultCurrency].place}", space: ${se[e.defaultCurrency].space} }
|
|
258
259
|
],
|
|
259
260
|
countryMap: {
|
|
260
261
|
default: "${e.defaultCurrency}"
|
|
@@ -307,8 +308,8 @@ export * from "./pricing";
|
|
|
307
308
|
export * from "./roles";
|
|
308
309
|
export * from "./section-tabs";
|
|
309
310
|
export * from "./tenancy";
|
|
310
|
-
`,
|
|
311
|
-
`)}async function
|
|
311
|
+
`,h=`${e.projectDir}/packages/config/src/index.ts`;await l(h,y)}function ui(e){return e==="stripe"?' stripePriceId: "",':' polarProductId: "",'}function xt(e){let t=[];return e.withCredits&&(t.push(` credits: ${e.credits},`),t.push(" creditInterval: 30,")),t.push(` apiRateLimit: { maxRequests: ${e.rateLimit} }`),t.join(`
|
|
312
|
+
`)}async function gr(e){let t=`${e.projectDir}/packages/config/src/pricing.ts`,r=e.defaultCurrency;if(e.paymentProvider==="none"){let f=`import type { PricingConfig } from "@repo/config/types";
|
|
312
313
|
|
|
313
314
|
export const pricingConfig: PricingConfig = {
|
|
314
315
|
defaultPlan: "free",
|
|
@@ -337,7 +338,7 @@ export const pricingConfig: PricingConfig = {
|
|
|
337
338
|
credits: { enabled: false },
|
|
338
339
|
products: { enabled: false, items: [] }
|
|
339
340
|
};
|
|
340
|
-
`;await l(t,
|
|
341
|
+
`;await l(t,f);return}let i=e.paymentProvider,n=ui(i),o=e.credits,s=xt({credits:5,rateLimit:100,withCredits:o}),a=xt({credits:10,rateLimit:1e3,withCredits:o}),d=xt({credits:50,rateLimit:5e3,withCredits:o}),h=`import type { PricingConfig } from "@repo/config/types";
|
|
341
342
|
|
|
342
343
|
export const pricingConfig: PricingConfig = {
|
|
343
344
|
defaultPlan: "free",
|
|
@@ -376,12 +377,12 @@ ${s}
|
|
|
376
377
|
],
|
|
377
378
|
prices: [
|
|
378
379
|
{
|
|
379
|
-
${
|
|
380
|
+
${n}
|
|
380
381
|
interval: "month",
|
|
381
382
|
amounts: { ${r}: 9 }
|
|
382
383
|
},
|
|
383
384
|
{
|
|
384
|
-
${
|
|
385
|
+
${n}
|
|
385
386
|
interval: "year",
|
|
386
387
|
amounts: { ${r}: 90 },
|
|
387
388
|
anchorAmounts: { ${r}: 109 }
|
|
@@ -405,14 +406,14 @@ ${a}
|
|
|
405
406
|
],
|
|
406
407
|
prices: [
|
|
407
408
|
{
|
|
408
|
-
${
|
|
409
|
+
${n}
|
|
409
410
|
interval: "month",
|
|
410
411
|
amounts: { ${r}: 29 },
|
|
411
412
|
anchorAmounts: { ${r}: 39 },
|
|
412
413
|
featured: true
|
|
413
414
|
},
|
|
414
415
|
{
|
|
415
|
-
${
|
|
416
|
+
${n}
|
|
416
417
|
interval: "year",
|
|
417
418
|
amounts: { ${r}: 290 },
|
|
418
419
|
anchorAmounts: { ${r}: 349 }
|
|
@@ -427,11 +428,11 @@ ${o?" credits: { enabled: true }":" credits: { enabled: false }"},
|
|
|
427
428
|
items: []
|
|
428
429
|
}
|
|
429
430
|
};
|
|
430
|
-
`;await l(t,
|
|
431
|
-
`)}function
|
|
432
|
-
`)}async function
|
|
433
|
-
`+
|
|
434
|
-
`+
|
|
431
|
+
`;await l(t,h)}var mi={smtp:[{key:"SMTP_HOST",defaultValue:"localhost"},{key:"SMTP_PORT",defaultValue:"1025"}],ses:[{key:"AMAZON_SES_REGION",comment:"# TODO: Configure Amazon SES credentials (e.g. us-east-1)"},{key:"AMAZON_SES_KEY"},{key:"AMAZON_SES_SECRET"}],resend:[{key:"RESEND_API_KEY",comment:"# TODO: Add your Resend API key"}]};function fi(e){let t=W[e];return t.envVars.map((r,i)=>({key:r.name,...i===0?{comment:`# TODO: Add your ${t.label} OAuth credentials`}:{}}))}var gi={stripe:[{key:"STRIPE_SECRET_KEY",comment:"# TODO: Add your Stripe keys"},{key:"STRIPE_WEBHOOK_SECRET"}],polar:[{key:"POLAR_ACCESS_TOKEN",comment:"# TODO: Add your Polar keys"},{key:"POLAR_WEBHOOK_SECRET"}]};function Te(e,t){return t?e.map(r=>{let i=t[r.key];return i?{...r,defaultValue:i,comment:void 0}:r}):e}function ke(e,t){for(let r of e)r.comment&&t.push(r.comment),r.defaultValue!==void 0?t.push(`${r.key}=${r.defaultValue}`):t.push(`#${r.key}=`)}function Dt(e){return Array.from(crypto.getRandomValues(new Uint8Array(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}function yr(e){return e.architecture==="fullstack"?{apiUrl:"http://localhost:3000/api",baseUrl:"http://localhost:3000"}:{apiUrl:"http://localhost:3010",baseUrl:"http://localhost:3000"}}function hi(e){let{architecture:t,deploymentTarget:r}=e;return t==="fullstack"?r==="vercel"?{frontend:"https://your-app.vercel.app",backend:"https://your-app.vercel.app"}:r==="node"?{frontend:"https://your-app.example.com",backend:"https://your-app.example.com"}:null:{frontend:"https://your-app.example.com",backend:"https://your-app.example.com/api"}}function mt(e,t,r,i){e.push(i==="example"?`${t}=`:`${t}=${r}`)}function hr(e,t){let r=t==="example"?void 0:e.credentials,{apiUrl:i,baseUrl:n}=yr(e),o=[];e.architecture==="separate"?o.push("# API Configuration","# Standalone backend's own URL (the frontend reaches it via the public var above).",`API_URL=${i}`,`BASE_URL=${n}`):o.push("# App","# (API_URL is derived from the frontend's *_PUBLIC_API_URL by the runtime env schema.)",`BASE_URL=${n}`),o.push("","# Database"),ke(Te(N[e.databaseProvider].envVars,r),o),o.push("","# Cache"),ke(Te(G[e.cacheProvider].envVars,r),o),o.push("","# Authentication"),t==="example"&&o.push("# Generate a strong secret, e.g. `openssl rand -hex 32`"),mt(o,"BETTER_AUTH_SECRET",crypto.randomUUID(),t),o.push("","# Content API (random secret; required when contentApi feature is enabled in @repo/config)"),mt(o,"CONTENT_API_KEY",Dt(32),t),o.push("","# Job Queue - Inngest","INNGEST_APP_ID=api"),mt(o,"INNGEST_EVENT_KEY",Dt(32),t),mt(o,"INNGEST_SIGNING_KEY",Dt(32),t),o.push("INNGEST_BASE_URL=http://127.0.0.1:8288"),e.architecture==="separate"&&(o.push("","# API Port (standalone backend)","API_PORT=3010"),o.push("","# CORS + cross-subdomain cookies (production only - leave unset for local dev)","# TRUSTED_ORIGINS=https://your-app.example.com","# AUTH_COOKIE_DOMAIN=.example.com"));let s=mi[e.emailProvider];if(s&&(o.push("","# Email"),ke(Te(s,r),o)),e.paymentProvider!=="none"){let a=gi[e.paymentProvider];a&&(o.push("","# Payment"),ke(Te(a,r),o))}if(e.socialProviders.length>0){o.push("","# Social auth");for(let a of e.socialProviders)ke(Te(fi(a),r),o)}return e.demo&&(o.push("","# Captcha (Cloudflare Turnstile)"),ke(Te([{key:"TURNSTILE_SECRET_KEY",comment:"# TODO: Add your Cloudflare Turnstile secret key"}],r),o)),o.push("","# Translations - OpenRouter (optional)","# Set to auto-translate en/* into your other locales with `pnpm translate`.","# The pre-commit hook runs it on commit; without a key it skips and untranslated","# locales fall back to the default locale. Get a key: https://openrouter.ai/keys","#OPENROUTER_API_KEY="),o.push(""),o.join(`
|
|
432
|
+
`)}function yi(e){let{apiUrl:t}=yr(e),r=e.frontend==="nextjs"?"NEXT_PUBLIC_API_URL":"NUXT_PUBLIC_API_URL",i=["# API Configuration",`${r}=${t}`],n=hi(e);return n&&e.architecture==="separate"&&i.push("","# Production (uncomment and replace with your deployed hostnames):",`# ${r}=${n.backend}`),i.push(""),i.join(`
|
|
433
|
+
`)}async function vr(e){let t=yi(e);await l(`${e.projectDir}/.env`,t+`
|
|
434
|
+
`+hr(e,"env")),await l(`${e.projectDir}/.env.example`,t+`
|
|
435
|
+
`+hr(e,"example"))}async function Sr(e){let t=[...e.dockerServices];if(N[e.databaseProvider].managed&&(t=t.filter(o=>o!=="postgres")),G[e.cacheProvider].managed&&(t=t.filter(o=>o!=="redis")),t.length===0)return!1;let r=[],i=[];t.includes("postgres")&&(r.push(` postgres:
|
|
435
436
|
image: postgres:18-alpine
|
|
436
437
|
ports:
|
|
437
438
|
- "\${POSTGRES_PORT:-5432}:5432"
|
|
@@ -446,7 +447,7 @@ ${o?" credits: { enabled: true }":" credits: { enabled: false }"},
|
|
|
446
447
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
447
448
|
interval: 5s
|
|
448
449
|
timeout: 5s
|
|
449
|
-
retries: 5`),
|
|
450
|
+
retries: 5`),i.push(" postgres_data:")),t.includes("redis")&&(r.push(` redis:
|
|
450
451
|
image: redis:8-alpine
|
|
451
452
|
ports:
|
|
452
453
|
- "\${REDIS_PORT:-6379}:6379"
|
|
@@ -456,7 +457,7 @@ ${o?" credits: { enabled: true }":" credits: { enabled: false }"},
|
|
|
456
457
|
test: ["CMD", "redis-cli", "ping"]
|
|
457
458
|
interval: 5s
|
|
458
459
|
timeout: 5s
|
|
459
|
-
retries: 5`),
|
|
460
|
+
retries: 5`),i.push(" redis_data:")),t.includes("mailpit")&&r.push(` mailpit:
|
|
460
461
|
image: axllent/mailpit
|
|
461
462
|
ports:
|
|
462
463
|
- "\${MAILPIT_SMTP_PORT:-1025}:1025"
|
|
@@ -467,16 +468,16 @@ ${o?" credits: { enabled: true }":" credits: { enabled: false }"},
|
|
|
467
468
|
image: inngest/inngest:v1.17.4
|
|
468
469
|
ports:
|
|
469
470
|
- "\${INNGEST_PORT:-8288}:8288"
|
|
470
|
-
command: inngest dev`);let
|
|
471
|
+
command: inngest dev`);let n=`services:
|
|
471
472
|
${r.join(`
|
|
472
473
|
|
|
473
474
|
`)}
|
|
474
|
-
`;return
|
|
475
|
+
`;return i.length>0&&(n+=`
|
|
475
476
|
volumes:
|
|
476
|
-
${
|
|
477
|
+
${i.join(`
|
|
477
478
|
`)}
|
|
478
|
-
`),await l(`${e.projectDir}/infra/docker-compose.yml`,
|
|
479
|
-
`)}async function
|
|
479
|
+
`),await l(`${e.projectDir}/infra/docker-compose.yml`,n),!0}function vi(e){let t=[];e.architecture==="separate"&&t.push({type:"node-terminal",request:"launch",name:"Backend",command:"pnpm dev",cwd:"${workspaceFolder}/apps/backend",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}});let r=e.frontend==="nextjs"?"Next.js":"Nuxt";if(t.push({type:"node-terminal",request:"launch",name:r,command:"pnpm dev",cwd:`\${workspaceFolder}/apps/web-${e.frontend==="nextjs"?"next":"nuxt"}`,skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),e.docs&&t.push({type:"node-terminal",request:"launch",name:"Docs",command:"pnpm dev",cwd:"${workspaceFolder}/apps/docs",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),t.push({type:"node-terminal",request:"launch",name:"Inngest",command:"pnpm dev:inngest",cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),t.push({type:"node-terminal",request:"launch",name:"Mail",command:"pnpm dev:mail",cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),e.paymentProvider==="stripe"){let i=e.architecture==="separate"?"localhost:3010/auth/stripe/webhook":"localhost:3000/api/auth/stripe/webhook";t.push({type:"node-terminal",request:"launch",name:"Stripe",command:`stripe listen --forward-to ${i}`,cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"]})}return t}function Si(e){let t=e.frontend==="nextjs"?"Next.js":"Nuxt",r=[];return e.architecture==="separate"?r.push({name:`Dev (${t} + Backend + Inngest)`,configurations:["Backend",t,"Inngest"]}):r.push({name:`Dev (${t} + Inngest)`,configurations:[t,"Inngest"]}),r}async function Er(e){let t={version:"0.2.0",configurations:vi(e),compounds:Si(e)};await l(`${e.projectDir}/.vscode/launch.json`,JSON.stringify(t,null," ")+`
|
|
480
|
+
`)}async function wr(e){if(e.architecture!=="separate")return;await l(`${e.projectDir}/apps/backend/src/index.ts`,`import { serve } from "@hono/node-server";
|
|
480
481
|
import app from "@repo/api";
|
|
481
482
|
import { closeRedis, env, logger } from "@repo/runtime";
|
|
482
483
|
|
|
@@ -507,18 +508,18 @@ bootstrap().catch((error) => {
|
|
|
507
508
|
logger.error("[Backend] Fatal error", error);
|
|
508
509
|
process.exit(1);
|
|
509
510
|
});
|
|
510
|
-
`)}import{readFile as
|
|
511
|
+
`)}import{readFile as Ei}from"fs/promises";import{join as wi}from"path";var bi=`export * from "./db/auth";
|
|
511
512
|
export * from "./db/schema";
|
|
512
|
-
export type { User, Account, Organization, Member } from "./db/auth";`;async function
|
|
513
|
-
`).filter(
|
|
514
|
-
`)}var
|
|
513
|
+
export type { User, Account, Organization, Member } from "./db/auth";`;async function br(e){let t=wi(e.projectDir,"packages/database/src/index.ts"),r=bi;try{let n=await Ei(t,"utf-8"),o=Ai(n);o&&(r=o)}catch{}let i;switch(e.databaseProvider){case"postgres":i=Ti(r);break;case"neon":i=ki(r);break;case"supabase":i=Ii(r);break}await l(t,i)}function Ai(e){return e.split(`
|
|
514
|
+
`).filter(i=>i.startsWith("export type ")||i.startsWith("export * from")).join(`
|
|
515
|
+
`)}var Ct=`
|
|
515
516
|
/** Extract affected-row count from a delete/update result (works for pg + postgres-js). */
|
|
516
517
|
export function affectedRowCount(result: unknown): number {
|
|
517
518
|
if (typeof result !== "object" || result === null) return 0;
|
|
518
519
|
const r = result as { rowCount?: number; count?: number };
|
|
519
520
|
return r.rowCount ?? r.count ?? 0;
|
|
520
521
|
}
|
|
521
|
-
`;function
|
|
522
|
+
`;function Ti(e){return`import { drizzle } from "drizzle-orm/node-postgres";
|
|
522
523
|
import { z } from "zod";
|
|
523
524
|
import * as authSchema from "./db/auth";
|
|
524
525
|
import * as appSchema from "./db/schema";
|
|
@@ -533,7 +534,7 @@ export const db = drizzle(parsed.data.DATABASE_URL, { schema });
|
|
|
533
534
|
export const pool = db.$client;
|
|
534
535
|
|
|
535
536
|
${e}
|
|
536
|
-
${
|
|
537
|
+
${Ct}`}function ki(e){return`import { neon } from "@neondatabase/serverless";
|
|
537
538
|
import { drizzle } from "drizzle-orm/neon-http";
|
|
538
539
|
import { z } from "zod";
|
|
539
540
|
import * as authSchema from "./db/auth";
|
|
@@ -549,7 +550,7 @@ const sql = neon(parsed.data.DATABASE_URL);
|
|
|
549
550
|
export const db = drizzle(sql, { schema });
|
|
550
551
|
|
|
551
552
|
${e}
|
|
552
|
-
${
|
|
553
|
+
${Ct}`}function Ii(e){return`import { drizzle } from "drizzle-orm/postgres-js";
|
|
553
554
|
import postgres from "postgres";
|
|
554
555
|
import { z } from "zod";
|
|
555
556
|
import * as authSchema from "./db/auth";
|
|
@@ -565,7 +566,7 @@ const client = postgres(parsed.data.DATABASE_URL);
|
|
|
565
566
|
export const db = drizzle(client, { schema });
|
|
566
567
|
|
|
567
568
|
${e}
|
|
568
|
-
${
|
|
569
|
+
${Ct}`}async function Ar(e){switch(e.cacheProvider){case"redis":await Pi(e),await _i(e);break;case"upstash":await Ri(e),await Oi(e);break}}async function Pi(e){await l(`${e.projectDir}/packages/runtime/src/redis.ts`,`import type { Store } from "hono-rate-limiter";
|
|
569
570
|
import { Redis } from "ioredis";
|
|
570
571
|
import { RedisStore, type RedisReply } from "rate-limit-redis";
|
|
571
572
|
import { env } from "./env";
|
|
@@ -689,7 +690,7 @@ export async function closeRedis() {
|
|
|
689
690
|
closed = true;
|
|
690
691
|
}
|
|
691
692
|
}
|
|
692
|
-
`)}async function
|
|
693
|
+
`)}async function _i(e){await l(`${e.projectDir}/packages/runtime/src/mutex.ts`,`import { Mutex } from "redis-semaphore";
|
|
693
694
|
import { redis } from "./redis";
|
|
694
695
|
|
|
695
696
|
export class MutexTimeoutError extends Error {
|
|
@@ -726,7 +727,7 @@ export async function withMutex<T>(
|
|
|
726
727
|
await mutex.release();
|
|
727
728
|
}
|
|
728
729
|
}
|
|
729
|
-
`)}async function
|
|
730
|
+
`)}async function Ri(e){await l(`${e.projectDir}/packages/runtime/src/redis.ts`,`import { Redis } from "@upstash/redis";
|
|
730
731
|
import type { Store } from "hono-rate-limiter";
|
|
731
732
|
import { env } from "./env";
|
|
732
733
|
|
|
@@ -839,7 +840,7 @@ export const limiterStore: Store = createLimiterStore();
|
|
|
839
840
|
|
|
840
841
|
/** No persistent connection to close with Upstash REST. */
|
|
841
842
|
export async function closeRedis(): Promise<void> {}
|
|
842
|
-
`)}async function
|
|
843
|
+
`)}async function Oi(e){await l(`${e.projectDir}/packages/runtime/src/mutex.ts`,`import { Lock } from "@upstash/lock";
|
|
843
844
|
import { redis } from "./redis";
|
|
844
845
|
|
|
845
846
|
export class MutexTimeoutError extends Error {
|
|
@@ -884,7 +885,7 @@ export async function withMutex<T>(
|
|
|
884
885
|
await lock.release();
|
|
885
886
|
}
|
|
886
887
|
}
|
|
887
|
-
`)}async function
|
|
888
|
+
`)}async function Tr(e){await l(`${e.projectDir}/packages/runtime/src/env.ts`,xi(e))}function xi(e){return`import { z } from "zod";
|
|
888
889
|
|
|
889
890
|
const EnvSchema = z.object({
|
|
890
891
|
NODE_ENV: z.enum(["development", "production"]).default("development"),
|
|
@@ -1002,17 +1003,17 @@ export const env = (() => {
|
|
|
1002
1003
|
const parsedApiUrl = new URL(env.API_URL);
|
|
1003
1004
|
export const apiBasePath = parsedApiUrl.pathname === "/" ? "" : parsedApiUrl.pathname;
|
|
1004
1005
|
export const apiOrigin = parsedApiUrl.origin;
|
|
1005
|
-
`}import{readFile as
|
|
1006
|
-
`)}function
|
|
1007
|
-
`)}var
|
|
1008
|
-
`))}}import{readFile as
|
|
1009
|
-
`);let o=
|
|
1006
|
+
`}import{readFile as Di}from"fs/promises";import{join as K}from"path";var ft={"@upstash/redis":"^1.37.0","@upstash/lock":"^0.2.1","@neondatabase/serverless":"^1.0.1",postgres:"^3.4.7"};async function Pe(e){let t=await Di(e,"utf-8");return JSON.parse(t)}async function _e(e,t){await l(e,JSON.stringify(t,null," ")+`
|
|
1007
|
+
`)}function Be(e,t){for(let r of t)delete e.dependencies?.[r],delete e.devDependencies?.[r]}function Ie(e,t,r,i=!1){let n=i?"devDependencies":"dependencies";e[n]||(e[n]={}),e[n][t]=r}async function kr(e){await Ci(e),await Ni(e),await Li(e),await $i(e),e.frontend==="nextjs"?await ji(e):await Ui(e)}async function Ci(e){let t=K(e.projectDir,"packages/api/package.json"),r=await Pe(t);Be(r,["sharp","@types/sharp"]),await _e(t,r)}async function Ni(e){let t=K(e.projectDir,"packages/runtime/package.json"),r=await Pe(t);e.cacheProvider==="upstash"&&(Be(r,["ioredis","rate-limit-redis","redis-semaphore"]),Ie(r,"@upstash/redis",ft["@upstash/redis"]),Ie(r,"@upstash/lock",ft["@upstash/lock"])),await _e(t,r)}async function Li(e){let t=K(e.projectDir,"packages/database/package.json"),r=await Pe(t);e.databaseProvider==="neon"?(Be(r,["pg","@types/pg"]),Ie(r,"@neondatabase/serverless",ft["@neondatabase/serverless"])):e.databaseProvider==="supabase"&&(Be(r,["pg","@types/pg"]),Ie(r,"postgres",ft.postgres)),await _e(t,r)}async function $i(e){if(e.architecture!=="separate")return;let t=K(e.projectDir,"apps/backend/package.json"),r=await Pe(t);e.deploymentTarget!=="node"&&Be(r,["@hono/node-server"]),await _e(t,r)}async function Ui(e){if(e.architecture!=="separate")return;let t=K(e.projectDir,"apps/web-nuxt");await Ot(K(t,"server/api/[...paths].ts"),t);let r=K(t,"package.json"),i=await Pe(r),n=i.dependencies?.["@repo/api"];n&&(delete i.dependencies?.["@repo/api"],Ie(i,"@repo/api",n,!0)),await _e(r,i)}async function ji(e){if(e.architecture!=="separate")return;let t=K(e.projectDir,"apps/web-next");await Ot(K(t,"app/api/[[...rest]]/route.ts"),t);let r=K(t,"package.json"),i=await Pe(r),n=i.dependencies?.["@repo/api"];n&&(delete i.dependencies?.["@repo/api"],Ie(i,"@repo/api",n,!0)),await _e(r,i)}import{readFile as Pr}from"fs/promises";import{join as _r}from"path";async function Rr(e){let t=_r(e.projectDir,"turbo.json"),r;try{r=await Pr(t,"utf-8")}catch{return}let i=JSON.parse(r),n=i.tasks?.build;if(!n)return;let o=e.frontend==="nextjs"?"NUXT_PUBLIC_*":"NEXT_PUBLIC_*",s=e.frontend==="nextjs"?new Set([".nuxt/**",".output/**"]):new Set([".next/**","!.next/cache/**"]),a=!1;if(Array.isArray(n.env)){let d=n.env.filter(y=>y!==o);d.length!==n.env.length&&(n.env=d,a=!0)}if(Array.isArray(n.outputs)){let d=n.outputs.filter(y=>!s.has(y));d.length!==n.outputs.length&&(n.outputs=d,a=!0)}a&&await l(t,JSON.stringify(i,null," ")+`
|
|
1008
|
+
`)}var Vi=["base.json","node.json","next.json"],Ir="GenerateSaaS ";async function Or(e){if(!e.demo)for(let t of Vi){let r=_r(e.projectDir,"tooling/typescript",t),i;try{i=await Pr(r,"utf-8")}catch{continue}let n=JSON.parse(i);typeof n.display!="string"||!n.display.startsWith(Ir)||(n.display=n.display.slice(Ir.length),await l(r,JSON.stringify(n,null," ")+`
|
|
1009
|
+
`))}}import{readFile as Mi,rm as Fi}from"fs/promises";import{existsSync as xr}from"fs";import{join as Dr}from"path";async function Cr(e){if(e.frontend==="nuxt")return;let t=Dr(e.projectDir,"packages/i18n/package.json");if(!xr(t))throw new Error(`pruneI18nNuxt: expected ${t} to exist - did the i18n package move?`);let r=JSON.parse(await Mi(t,"utf-8")),i=!!(r.exports?.["./module"]??r.exports?.["./nuxt"]),n=!1;if(r.exports)for(let s of["./module","./nuxt"])s in r.exports&&(delete r.exports[s],n=!0);r.devDependencies&&"@nuxt/kit"in r.devDependencies&&(delete r.devDependencies["@nuxt/kit"],n=!0),n&&await l(t,JSON.stringify(r,null," ")+`
|
|
1010
|
+
`);let o=Dr(e.projectDir,"packages/i18n/nuxt");if(i&&!xr(o))throw new Error(`pruneI18nNuxt: packages/i18n declares a Nuxt export surface but ${o} is missing - did the i18n Nuxt module move?`);await Fi(o,{recursive:!0,force:!0})}import{readFile as Nr,rm as Bi}from"fs/promises";import{join as Nt}from"path";async function Lr(e){e.cacheProvider==="upstash"&&await Promise.all([Gi(e.projectDir),Ki(e.projectDir),Bi(Nt(e.projectDir,"packages/runtime/tests/redis.test.ts"),{force:!0})])}async function Gi(e){let t=Nt(e,"packages/runtime/tests/setup.ts"),r;try{r=await Nr(t,"utf-8")}catch{return}let i=r.replace(/\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
|
|
1010
1011
|
UPSTASH_REDIS_REST_TOKEN: "test-token",
|
|
1011
|
-
`);
|
|
1012
|
+
`);i!==r&&await l(t,i)}async function Ki(e){let t=Nt(e,"packages/api/tests/setup.ts"),r;try{r=await Nr(t,"utf-8")}catch{return}let i=r;i=i.replace(/\tREDIS_URL:\s*"[^"]*",?\n/g,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
|
|
1012
1013
|
UPSTASH_REDIS_REST_TOKEN: "test-token",
|
|
1013
|
-
`),
|
|
1014
|
+
`),i=i.replace(/vi\.mock\("ioredis"[\s\S]*?\n\}\);\n\n?/,""),i=i.replace(/vi\.mock\("rate-limit-redis"[\s\S]*?\n\}\);\n\n?/,""),i=i.replace(/\t\t\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
|
|
1014
1015
|
UPSTASH_REDIS_REST_TOKEN: "test-token",
|
|
1015
|
-
`),
|
|
1016
|
+
`),i=i.replace(/(^vi\.mock\("@repo\/runtime")/m,`vi.mock("@upstash/redis", () => {
|
|
1016
1017
|
class Redis {
|
|
1017
1018
|
get = vi.fn().mockResolvedValue(null);
|
|
1018
1019
|
set = vi.fn().mockResolvedValue("OK");
|
|
@@ -1029,8 +1030,8 @@ vi.mock("@upstash/lock", () => {
|
|
|
1029
1030
|
return { Lock };
|
|
1030
1031
|
});
|
|
1031
1032
|
|
|
1032
|
-
$1`),
|
|
1033
|
-
`)}async function
|
|
1033
|
+
$1`),i!==r&&await l(t,i)}import{readdir as zi,readFile as Hi,rm as Ge}from"fs/promises";import{join as fe}from"path";var Yi=new Set(["ci.yml"]),Ji=["cli","cli:clean","demo:bench","playground:regen","playground:test","playground:test:units"];async function $r(e){let t=fe(e.projectDir,".github/workflows"),r=await zi(t).catch(()=>[]);for(let i of r)Yi.has(i)||await Ge(fe(t,i),{recursive:!0,force:!0})}async function Ur(e){let t=fe(e.projectDir,"package.json"),r=await Hi(t,"utf-8"),i=JSON.parse(r),n=!1;if(i.scripts){for(let o of Ji)o in i.scripts&&(delete i.scripts[o],n=!0);if(!Xe(e))for(let o of["infra","infra:stop"])o in i.scripts&&(delete i.scripts[o],n=!0)}n&&await l(t,JSON.stringify(i,null," ")+`
|
|
1034
|
+
`)}async function jr(e){let t=e.frontend==="nextjs"?"apps/web-nuxt":"apps/web-next";await Ge(fe(e.projectDir,t),{recursive:!0,force:!0})}async function Vr(e){e.docs||await Ge(fe(e.projectDir,"apps/docs"),{recursive:!0})}async function Mr(e){let t=e.frontend==="nextjs"?"docs/nuxt":"docs/next";await Ge(fe(e.projectDir,t),{recursive:!0,force:!0}),await Ge(fe(e.projectDir,"docs/index.mdx"))}import{join as Wi}from"path";async function Fr(e){let t=qi(e);await l(Wi(e.projectDir,".github/workflows/ci.yml"),t)}function qi(e){let t=N[e.databaseProvider].managed,r=e.cacheProvider==="upstash",i=e.frontend==="nuxt",n=t?"":` services:
|
|
1034
1035
|
postgres:
|
|
1035
1036
|
image: postgres:18-alpine
|
|
1036
1037
|
env:
|
|
@@ -1050,7 +1051,7 @@ $1`),n!==r&&await l(t,n)}import{readdir as Mi,readFile as Fi,rm as Be}from"fs/pr
|
|
|
1050
1051
|
STRIPE_SECRET_KEY: test
|
|
1051
1052
|
STRIPE_WEBHOOK_SECRET: test`;case"polar":return`
|
|
1052
1053
|
POLAR_ACCESS_TOKEN: test
|
|
1053
|
-
POLAR_WEBHOOK_SECRET: test`;case"none":return"";default:{let
|
|
1054
|
+
POLAR_WEBHOOK_SECRET: test`;case"none":return"";default:{let f=e.paymentProvider;throw new Error(`buildCiYaml: unhandled payment provider "${String(f)}"`)}}})(),d=e.socialProviders.map(f=>W[f].envVars.map(S=>`
|
|
1054
1055
|
${S.name}: test`).join("")).join("");return`# Deployment is handled by the hosting platform (Vercel, Coolify, etc.)
|
|
1055
1056
|
# which auto-deploys on push. CI runs in parallel as a quality gate.
|
|
1056
1057
|
# For PR-based workflows, enable GitHub branch protection to require CI before merging.
|
|
@@ -1084,7 +1085,7 @@ jobs:
|
|
|
1084
1085
|
steps:
|
|
1085
1086
|
- uses: actions/checkout@v6
|
|
1086
1087
|
- uses: ./.github/actions/setup
|
|
1087
|
-
${
|
|
1088
|
+
${i?` # vue-tsc on web-nuxt OOMs on the GitHub runner's default heap once
|
|
1088
1089
|
# the type graph (Nuxt + Pinia + vue-i18n + content collections)
|
|
1089
1090
|
# crosses a threshold. Bump to 6 GB; ubuntu-latest has ~7 GB RAM.
|
|
1090
1091
|
- run: pnpm check-types
|
|
@@ -1095,7 +1096,7 @@ ${n?` # vue-tsc on web-nuxt OOMs on the GitHub runner's default heap once
|
|
|
1095
1096
|
name: Test
|
|
1096
1097
|
runs-on: ubuntu-latest
|
|
1097
1098
|
timeout-minutes: 10
|
|
1098
|
-
${
|
|
1099
|
+
${n} env:
|
|
1099
1100
|
CONTENT_API_KEY: test-contentapi-key-16chars${s}${a}${d}
|
|
1100
1101
|
STORAGE_REGION: test
|
|
1101
1102
|
STORAGE_ENDPOINT: http://test
|
|
@@ -1110,41 +1111,45 @@ ${i} env:
|
|
|
1110
1111
|
run: echo "DATABASE_URL=${o}" > .env
|
|
1111
1112
|
|
|
1112
1113
|
- run: pnpm test
|
|
1113
|
-
`}import{readFile as
|
|
1114
|
-
`))}async function
|
|
1115
|
-
`))}async function
|
|
1114
|
+
`}import{readFile as Br}from"fs/promises";import{existsSync as Xi}from"fs";import{join as Lt}from"path";var Gr="@repo/database";function Zi(e){return e?"pnpm -F @repo/database reset && pnpm -F @repo/database push --force":"pnpm -F @repo/database migrate"}function Qi(e,t){switch(e){case"fullstack":return t==="nextjs"?"web-next":"web-nuxt";case"separate":return"backend";default:{let r=e;throw new Error(`schemaOwnerApp: unhandled architecture "${String(r)}"`)}}}async function eo(e,t,r){let i=await Br(e,"utf-8"),n=JSON.parse(i),o=n.scripts?.[t];if(!o)throw new Error(`Cannot prepend to missing script "${t}" in ${e}`);o.includes(Gr)||(n.scripts={...n.scripts,[t]:`${r} && ${o}`},await l(e,JSON.stringify(n,null," ")+`
|
|
1115
|
+
`))}async function to(e,t){let r=Xi(e)?JSON.parse(await Br(e,"utf-8")):{$schema:"https://openapi.vercel.sh/vercel.json"},i=r.buildCommand?.trim()||"pnpm build";i.includes(Gr)||(r.buildCommand=`${t} && ${i}`,await l(e,JSON.stringify(r,null," ")+`
|
|
1116
|
+
`))}async function Kr(e){let t=Zi(e.demo===!0),r=Qi(e.architecture,e.frontend),i=Lt(e.projectDir,"apps",r);switch(e.deploymentTarget){case"node":await eo(Lt(i,"package.json"),"start",t);return;case"vercel":await to(Lt(i,"vercel.json"),t);return;default:{let n=e.deploymentTarget;throw new Error(`generateDeployScripts: unhandled deployment target "${String(n)}"`)}}}import{readFile as ro}from"fs/promises";import{existsSync as no}from"fs";import{join as gt}from"path";var io=["stripe","polar"];async function zr(e){let t=gt(e.projectDir,"packages/payments/src"),r=e.paymentProvider,i=`// Active payment provider. To switch, change the path below to
|
|
1116
1117
|
// "./polar/index" or "./none/index" (other folders are kept in place).
|
|
1117
1118
|
export { ops } from "./${r}/index";
|
|
1118
|
-
`;await l(gt(t,"providers/index.ts"),
|
|
1119
|
-
`).filter(s=>!
|
|
1120
|
-
`)}import{readdir as
|
|
1121
|
-
`).filter(o=>!
|
|
1122
|
-
`).length&&await l(e,
|
|
1123
|
-
`))}async function ht(e){let t=e.projectDir;e.demo||await
|
|
1124
|
-
`),await l(
|
|
1125
|
-
`)}import{relative as
|
|
1126
|
-
`);o(new Error(
|
|
1127
|
-
${
|
|
1128
|
-
`),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")}`),
|
|
1129
|
-
`),j.yellow("Dev Tools"))}let
|
|
1130
|
-
`),j.yellow("Deployment"))}function _o(e){let t=["DATABASE_URL","BETTER_AUTH_SECRET"];return e.cacheProvider==="upstash"?t.push("UPSTASH_REDIS_REST_URL","UPSTASH_REDIS_REST_TOKEN"):t.push("REDIS_URL"),e.paymentProvider==="stripe"?t.push("STRIPE_SECRET_KEY","STRIPE_WEBHOOK_SECRET"):e.paymentProvider==="polar"&&t.push("POLAR_ACCESS_TOKEN","POLAR_WEBHOOK_SECRET"),e.emailProvider==="ses"?t.push("AMAZON_SES_REGION","AMAZON_SES_KEY","AMAZON_SES_SECRET"):e.emailProvider==="resend"?t.push("RESEND_API_KEY"):t.push("SMTP_HOST","SMTP_PORT"),t}function Ro(e){switch(e.deploymentTarget){case"node":return"Deploy with Docker or your preferred Node.js host";case"vercel":return"vercel deploy # Deploy to Vercel"}}function un(e){let t={};if(e.name!==void 0){if(!st(e.name))throw new Error(`Invalid project name "${e.name}". Use lowercase letters, numbers, and hyphens only. Must start with a letter.`);t.projectName=e.name}if(e.appName!==void 0){if(!e.appName.trim())throw new Error("App name cannot be empty.");t.appName=e.appName}if(e.location!==void 0?t.projectDir=e.location==="."?process.cwd():e.location:t.projectName!==void 0&&(t.projectDir=`./${t.projectName}`),e.frontend!==void 0){if(!Ue.includes(e.frontend))throw new Error(`Invalid frontend "${e.frontend}". Valid values: ${Ue.join(", ")}`);t.frontend=e.frontend}if(e.architecture!==void 0&&(t.architecture=e.architecture),e.payment!==void 0&&(t.paymentProvider=e.payment),e.email!==void 0&&(t.emailProvider=e.email),e.org!==void 0&&(t.multiTenancy=e.org),e.billingScope!==void 0){if(e.org===!1)throw new Error("--billing-scope requires --org to be enabled.");t.billingScope=e.billingScope}if(e.blog!==void 0&&(t.blog=e.blog),e.docs!==void 0&&(t.docs=e.docs),e.revenueSharing!==void 0&&(t.revenueSharing=e.revenueSharing),e.credits!==void 0){if(e.credits===!0&&e.payment==="none")throw new Error("--credits requires a payment provider (got --payment none).");t.credits=e.credits}if(e.docker!==void 0&&(t.dockerServices=jt(e.docker,tt,"docker service")),e.aiTools!==void 0&&(t.aiTools=jt(e.aiTools,rt,"AI tool")),e.socialProviders!==void 0&&(t.socialProviders=jt(e.socialProviders,it,"social provider")),e.currency!==void 0){if(!ce.includes(e.currency))throw new Error(`Invalid currency "${e.currency}". Valid values: ${ce.join(", ")}`);t.defaultCurrency=e.currency}if(e.deploy!==void 0){if(!le.includes(e.deploy))throw new Error(`Invalid deployment target "${e.deploy}". Valid values: ${le.join(", ")}`);t.deploymentTarget=e.deploy}if(e.database!==void 0){if(!pe.includes(e.database))throw new Error(`Invalid database provider "${e.database}". Valid values: ${pe.join(", ")}`);t.databaseProvider=e.database}if(e.cache!==void 0){if(!de.includes(e.cache))throw new Error(`Invalid cache provider "${e.cache}". Valid values: ${de.join(", ")}`);t.cacheProvider=e.cache}if(e.demo===!0&&(t.demo=!0),e.baseUrl!==void 0){let r=e.baseUrl.trim();if(r==="")throw new Error("--base-url cannot be empty. Provide an absolute URL like https://example.com.");let n;try{n=new URL(r)}catch{throw new Error(`Invalid --base-url "${e.baseUrl}". Must be an absolute URL like https://example.com.`)}if(n.protocol!=="http:"&&n.protocol!=="https:")throw new Error(`Invalid --base-url "${e.baseUrl}". Must use http or https.`);t.baseUrl=`${n.protocol}//${n.host}`}return t}var ne={projectName:"my-saas",frontend:"nextjs",architecture:"fullstack",paymentProvider:"stripe",emailProvider:"smtp",multiTenancy:!1,billingScope:"user",blog:!0,docs:!1,revenueSharing:!1,credits:!0,dockerServices:["postgres","redis","inngest"],aiTools:[],socialProviders:[],defaultCurrency:"USD",deploymentTarget:"node",databaseProvider:"postgres",cacheProvider:"redis"};function mn(e){let t=e.projectName??ne.projectName,r=e.projectDir??`./${t}`,n=e.appName??ot(t),i=e.deploymentTarget??ne.deploymentTarget,o=B[i]?.edgeRuntime??!1,s=e.databaseProvider??(o?"neon":ne.databaseProvider),a=e.cacheProvider??(o?"upstash":ne.cacheProvider),d=e.emailProvider??(o?"resend":ne.emailProvider),h=e.dockerServices??(o?ne.dockerServices.filter(m=>m!=="postgres"&&m!=="redis"):ne.dockerServices),g={...ne,...e,projectName:t,appName:n,projectDir:r,deploymentTarget:i,databaseProvider:s,cacheProvider:a,emailProvider:d,dockerServices:h};g.paymentProvider==="none"&&(g.credits=!1);for(let m of Le){if(g.deploymentTarget!==m.target)continue;let S=g.databaseProvider===m.provider?"database":"cache";if(g.databaseProvider===m.provider||g.cacheProvider===m.provider)throw new Error(`Incompatible: --deploy ${m.target} + --${S} ${m.provider}. ${m.reason}`)}for(let m of $e)if(g.architecture===m.architecture&&g.deploymentTarget===m.target)throw new Error(`Incompatible: --architecture ${m.architecture} + --deploy ${m.target}. ${m.reason}`);return g}function jt(e,t,r){if(e.trim()==="")return[];let n=e.split(",").map(o=>o.trim()).filter(Boolean),i=n.filter(o=>!t.includes(o));if(i.length>0)throw new Error(`Invalid ${r}(s): ${i.join(", ")}. Valid values: ${t.join(", ")}`);return n}import Lo from"picocolors";var $o="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";function Uo(e){if(e===void 0)return;let t=e.trim().replace(/^v/,"");if(!/^\d+\.\d+\.\d+$/.test(t))throw new Error(`Invalid template version "${e}". Use semver like 1.2.3.`);return t}function fn(e){e.command("init").description("Scaffold a new GenerateSaaS project").argument("[apiKey]","license key (same as --api-key)").option("-n, --name <name>","project name (lowercase, hyphens, starts with letter)").option("--app-name <name>","display name for the app").option("-l, --location <path>","project directory (default: ./{name})").addOption(new V("--frontend <type>","frontend framework").choices([...Ue])).addOption(new V("--architecture <type>","fullstack or separate").choices([...Ze])).addOption(new V("--payment <provider>","payment provider").choices([...Qe])).addOption(new V("--email <provider>","email provider").choices([...et])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").addOption(new V("--billing-scope <scope>","billing scope (requires --org)").choices([...nt])).option("--blog","enable blog").option("--no-blog","disable blog").option("--docs","include the docs app (apps/docs, Fumadocs)").option("--no-docs","exclude the docs app").option("--revenue-sharing","enable revenue sharing").option("--no-revenue-sharing","disable revenue sharing").option("--credits","enable credits system").option("--no-credits","disable credits system (subscription-only)").option("--docker <services>","comma-separated: postgres,redis,inngest,mailpit").option("--ai-tools <tools>","comma-separated: claude-code,cursor,codex,gemini-cli,windsurf").option("--social-providers <providers>","comma-separated: google,github,facebook,discord,x").addOption(new V("--currency <code>","default currency for billing").choices([...ce])).addOption(new V("--deploy <target>","deployment target").choices([...le])).addOption(new V("--database <provider>","database provider").choices([...pe])).addOption(new V("--cache <provider>","cache provider").choices([...de])).option("--template-version <version>","specific template version to scaffold").option("--api-key <key>","API key (skips interactive prompt)").option("--base-url <url>","public base URL (e.g. https://example.com) - bakes into canonical/og/sitemap").option("-y, --yes","accept defaults for unspecified options (non-interactive)").addOption(new V("--demo","first-party demo build: keep sample content, mark site non-indexable - requires CI API key").hideHelp()).addOption(new V("--no-db-migration","skip generating the baseline DB migration (internal: demos/CI/playground)").hideHelp()).action(async(t,r)=>{await jo(t?{...r,apiKey:t}:r)})}async function jo(e){let t=performance.now();Kt("1.9.3");let r,n;try{r=un(e),n=Uo(e.templateVersion)}catch(y){E.cancel(I(y)),process.exit(1)}let i=E.spinner(),o;try{o=await be({apiKey:e.apiKey,prompt:!e.yes})}catch(y){E.cancel(I(y)),process.exit(1)}e.demo&&Dt(o)!==$o&&(E.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let s=Z(o),a=async()=>{let y=await re(s),R=y.latest,Y=n??R;if(n&&!y.versions.some(J=>J.version===Y))throw new Error(`Template version "${n}" is not available.`);return{latestVersion:R,selectedVersion:Y}};i.start("Verifying access...");let d,h;try{({latestVersion:d,selectedVersion:h}=await a()),i.stop("Access verified."),me(o)}catch(y){if(i.stop("Access verification failed."),y instanceof O&&y.status===401){e.yes&&(E.cancel("Invalid API key. Cannot prompt in non-interactive mode."),process.exit(1)),E.log.warning("Invalid API key."),o=await Me(),s=Z(o),i.start("Verifying access...");try{({latestVersion:d,selectedVersion:h}=await a()),i.stop("Access verified."),me(o)}catch(R){i.stop("Access verification failed."),E.cancel(R instanceof O&&R.status===401?"Invalid API key.":I(R)),process.exit(1)}}else E.cancel(I(y)),process.exit(1)}E.log.success(`Latest version: ${d}`),h!==d&&E.log.success(`Using template version: ${h}`);let g;e.yes?g=mn(r):g=await Yt(r);let m;i.start("Activating license...");try{let y=crypto.randomUUID(),R=()=>({frontend:g.frontend,version:h,installId:y,projectName:g.projectName,options:Qt(g)}),Y;try{Y=await bt(s,R())}catch(J){let W=lt(J);if(!W?.lastAllowedVersion)throw J;i.stop("License activation failed."),e.yes&&(E.cancel(`${W.message} Re-run with --template-version ${W.lastAllowedVersion}.`),process.exit(1));let Ee=await E.confirm({message:`Your update window has ended. Continue with v${W.lastAllowedVersion} (the last version your license covers)?`});(E.isCancel(Ee)||!Ee)&&(E.cancel("Setup cancelled."),process.exit(0)),h=W.lastAllowedVersion,i.start(`Activating license for v${h}...`),Y=await bt(s,R())}m={token:Y.token,keyHash:Dt(o),installId:y},i.stop("License activated.")}catch(y){i.stop("License activation failed."),E.cancel(I(y)),process.exit(1)}let S=No(g.projectDir);if(Oo(S)&&xo(S).length>0)if(e.yes)E.log.info(`Directory ${S} is not empty. Merging (keeping existing files, overwriting conflicts).`);else{let R=await E.select({message:`Directory ${S} is not empty.`,options:[{value:"merge",label:"Merge",hint:"keep existing files, overwrite conflicts"},{value:"overwrite",label:"Overwrite",hint:"delete everything and start fresh"},{value:"cancel",label:"Cancel"}]});(E.isCancel(R)||R==="cancel")&&(E.cancel("Setup cancelled."),process.exit(0)),R==="overwrite"&&Do(S,{recursive:!0,force:!0})}let T={...g,projectDir:S,version:h,...e.demo?{docs:!1}:{}};i.start("Downloading template...");try{await pt(s,h,S),i.stop("Template downloaded.")}catch(y){i.stop("Download failed."),E.cancel(I(y)),process.exit(1)}let H;i.start("Generating project files...");try{if({dockerComposeGenerated:H}=await ht(T),!e.demo){let y=await Ge(S);await l(Co(S,at),JSON.stringify(y,null," ")+`
|
|
1131
|
-
`),await
|
|
1132
|
-
`),_.log.success("License refreshed.")}catch(
|
|
1133
|
-
`)}a.stop("Baseline template stored.")}else if(
|
|
1134
|
-
`),a.stop("Baseline hashes computed.")}if(await l(ze(r,
|
|
1135
|
-
`),_.log.info(`Update staged: ${
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1119
|
+
`;await l(gt(t,"providers/index.ts"),i),await l(gt(t,"index.ts"),await oo(t,r))}async function oo(e,t){let r=gt(e,"index.ts");if(!no(r))throw new Error(`generatePaymentBarrel: expected ${r} to exist - did packages/payments move?`);let i=await ro(r,"utf-8"),n=io.filter(s=>s!==t);return i.split(`
|
|
1120
|
+
`).filter(s=>!n.some(a=>s.includes(`./providers/${a}/`))).join(`
|
|
1121
|
+
`)}import{readdir as so,readFile as ao}from"fs/promises";import{join as Hr}from"path";async function Yr(e){if(e.demo)return;let t=e.appName.trim()||e.projectName,r=JSON.stringify(t).slice(1,-1),i=Hr(e.projectDir,"packages/i18n/translations"),n;try{n=await so(i)}catch{return}for(let o of n){let s=Hr(i,o,"web.json"),a;try{a=await ao(s,"utf-8")}catch{continue}let d=a.replaceAll("GenerateSaaS",r);d!==a&&await l(s,d)}}import{readFile as co}from"fs/promises";import{join as Jr}from"path";var lo=[".nuxt/",".nuxt",".nitro/",".nitro",".output/",".output","_locales/"],po=[".next/",".next",".svelte-kit/",".svelte-kit",".wrangler/",".wrangler",".dev.vars"];async function qr(e){let r=e.frontend==="nuxt"?po:lo;await Wr(Jr(e.projectDir,".gitignore"),r),await Wr(Jr(e.projectDir,".dockerignore"),r)}async function Wr(e,t){let r;try{r=await co(e,"utf-8")}catch{return}let i=new Set(t),n=r.split(`
|
|
1122
|
+
`).filter(o=>!i.has(o.trim()));n.length!==r.split(`
|
|
1123
|
+
`).length&&await l(e,n.join(`
|
|
1124
|
+
`))}async function ht(e){let t=e.projectDir;e.demo||await cr(t),await lr(t,e.aiTools),await pr(t,e.frontend),await ur(e),await mr(e),await fr(e),e.demo||await gr(e),await vr(e);let r=await Sr(e);return await Er(e),await wr(e),await br(e),await Ar(e),await Tr(e),await kr(e),await Rr(e),await Or(e),await Cr(e),await Lr(e),await $r(e),await Fr(e),await Ur(e),await jr(e),await Vr(e),await Mr(e),await Kr(e),await zr(e),await Yr(e),await qr(e),{dockerComposeGenerated:r}}import{basename as Zr,join as Qr,relative as mo}from"path";import{createHash as Xr}from"crypto";import{readFile as uo}from"fs/promises";async function yt(e){let t=await uo(e);return Xr("sha256").update(t).digest("hex")}function $t(e){return Xr("sha256").update(e).digest("hex")}var fo=new Set(["data",U]);function go(e){let t=e.split("/");for(let r of t)if(It.has(r)||Pt.has(r)||fo.has(r)||r.startsWith(".env")&&!r.includes("example"))return!0;return!1}function en(e,t){return{projectName:e.projectName??Zr(t),appName:e.appName??e.projectName??Zr(t),projectDir:t,frontend:e.frontend==="nextjs"?"nextjs":"nuxt",architecture:e.architecture??"fullstack",paymentProvider:e.paymentProvider??"none",emailProvider:e.emailProvider??"smtp",multiTenancy:e.multiTenancy??!1,billingScope:e.billingScope??"user",blog:e.blog??!1,docs:e.docs??!1,revenueSharing:e.revenueSharing??!1,credits:e.credits??!1,dockerServices:e.dockerServices??[],aiTools:e.aiTools??[],socialProviders:e.socialProviders??[],defaultCurrency:e.defaultCurrency??"USD",deploymentTarget:e.deploymentTarget??"node",databaseProvider:e.databaseProvider??"postgres",cacheProvider:e.cacheProvider??"redis",version:e.version,baseUrl:e.baseUrl,credentials:{},demo:!1}}async function tn(e,t){let i=(await me(e.projectDir,e.projectDir,go)).sort(),n=await Promise.all(i.map(async a=>[mo(e.projectDir,a),await yt(a)])),o=Object.fromEntries(n),s={version:e.version,initialVersion:e.version,repo:"Duzbee/GenerateSaaS",appName:e.appName,projectName:e.projectName,frontend:e.frontend,architecture:e.architecture,paymentProvider:e.paymentProvider,emailProvider:e.emailProvider,multiTenancy:e.multiTenancy,billingScope:e.billingScope,blog:e.blog,docs:e.docs,credits:e.credits,revenueSharing:e.revenueSharing,defaultCurrency:e.defaultCurrency,dockerServices:e.dockerServices,socialProviders:e.socialProviders,deploymentTarget:e.deploymentTarget,databaseProvider:e.databaseProvider,cacheProvider:e.cacheProvider,aiTools:e.aiTools,...e.baseUrl?{baseUrl:e.baseUrl}:{},...t&&{licenseToken:t.token,licenseKeyHash:t.keyHash,installId:t.installId}};await l(Qr(e.projectDir,Q),JSON.stringify(s,null," ")+`
|
|
1125
|
+
`),await l(Qr(e.projectDir,Zt),JSON.stringify(o,null," ")+`
|
|
1126
|
+
`)}import{relative as ho}from"path";async function Re(e){let r=(await me(e,e,Ae)).sort(),i=await Promise.all(r.map(async n=>[ho(e,n),await yt(n)]));return Object.fromEntries(i)}import{copyFile as yo,mkdir as vo,rm as So}from"fs/promises";import{dirname as Eo,join as rn,relative as wo}from"path";import{existsSync as bo}from"fs";async function Ut(e,t){bo(t)&&await So(t,{recursive:!0,force:!0});let r=await me(e,e,Ae);for(let i of r){let n=wo(e,i),o=rn(t,n);await vo(Eo(o),{recursive:!0}),await yo(i,o)}}async function nn(e,t){await Ut(e,rn(t,ct))}import{existsSync as Ao}from"fs";import{readFile as on,readdir as To}from"fs/promises";import{join as z,dirname as ko,resolve as Io,sep as Po}from"path";import{fileURLToPath as _o}from"url";var Ke={"claude-code":".claude/skills",cursor:".cursor/skills",codex:".agents/skills","gemini-cli":".gemini/skills",windsurf:".windsurf/skills"},kl=Object.values(Ke),jt="generatesaas-update",sn=ko(_o(import.meta.url));function Ro(){let e=z(sn,"skill","content");return Ao(e)?e:z(sn,"content")}function Vt(e){return!e||e.length===0?[]:e.map(t=>Ke[t])}async function Mt(e,t,r,i){let n=Vt(i);for(let o of n){let s=z(e,o,jt),a=z(s,"scripts"),d=z(s,"references");await ut(a),await ut(d),await l(z(s,"SKILL.md"),t.replaceAll("__SKILL_ROOT__",o)),await l(z(d,".gitkeep"),"");for(let[y,h]of Object.entries(r)){let f=Io(a,y);f.startsWith(a+Po)&&await l(f,h)}}}async function an(e,t){let r=Ro(),i=await on(z(r,"SKILL.md"),"utf-8"),n=z(r,"scripts"),o=await To(n),s={};for(let a of o)a!==".gitkeep"&&(s[a]=await on(z(n,a),"utf-8"));await Mt(e,i,s,t)}import{execFile as Oo,execFileSync as xo}from"child_process";import{access as cn,readFile as Do}from"fs/promises";import{join as Ft}from"path";import*as P from"@clack/prompts";function he(e){try{let t=process.platform==="win32"?"where":"which";return xo(t,[e],{stdio:"ignore"}),!0}catch{return!1}}function ge(e,t,r,i=3e5){return new Promise((n,o)=>{Oo(e,t,{cwd:r,timeout:i},(s,a,d)=>{if(s){let y=String(a||"").trim(),f=[String(d||"").trim(),y].filter(Boolean).join(`
|
|
1127
|
+
`);o(new Error(f?`${s.message}
|
|
1128
|
+
${f}`:s.message))}else n()})})}async function ln(e){if(!he("pnpm"))return P.log.warn("pnpm not found. Skipping lockfile regeneration."),!1;try{return await ge("pnpm",["install","--lockfile-only","--no-frozen-lockfile","--config.minimumReleaseAge=0"],e),!0}catch(t){let r=t instanceof Error?t.message:String(t);return P.log.warn(`Lockfile regeneration failed: ${r}`),P.log.warn("Deploys using --frozen-lockfile may fail."),!1}}async function pn(e){if(!he("pnpm"))return P.log.warn("pnpm not found. Skipping dependency installation."),P.log.info("Install pnpm: https://pnpm.io/installation"),!1;let t=P.spinner();t.start("Installing dependencies (this may take a minute)...");try{return await ge("pnpm",["install","--config.minimumReleaseAge=0"],e),t.stop("Dependencies installed."),!0}catch(r){t.stop("Dependency installation failed.");let i=r instanceof Error?r.message:String(r);return P.log.warn(`pnpm install failed: ${i}`),P.log.warn("You can run it manually later."),!1}}async function dn(e){if(!he("pnpm"))return!1;let t=P.spinner();t.start("Generating baseline database migration...");try{return await ge("pnpm",["-F","@repo/database","generate"],e),t.stop("Baseline migration generated."),!0}catch(r){t.stop("Baseline migration generation failed.");let i=r instanceof Error?r.message:String(r);return P.log.warn(`Could not generate baseline migration: ${i}`),P.log.warn("Run 'pnpm -F @repo/database generate' before your first deploy."),!1}}async function un(e){try{return await cn(Ft(e,".git")),P.log.info("Git repository already exists, skipping init."),!0}catch{}if(!he("git"))return P.log.warn("git not found. Skipping repository initialization."),!1;let t=P.spinner();t.start("Initializing git repository...");try{return await ge("git",["init"],e),await ge("git",["add","-A"],e),await ge("git",["commit","--no-verify","-m","Initial commit from GenerateSaaS"],e),t.stop("Git repository initialized."),!0}catch{return t.stop("Git initialization failed."),P.log.warn("You can run git init manually later."),!1}}async function mn(e){if(!he("pnpm"))return!1;try{await cn(Ft(e,".git"))}catch{return!1}try{let t=JSON.parse(await Do(Ft(e,"package.json"),"utf-8")),r=!!t.devDependencies?.["simple-git-hooks"],i=!!t["simple-git-hooks"];if(!r||!i)return!1}catch{return!1}try{return await ge("pnpm",["exec","simple-git-hooks"],e),!0}catch{return P.log.warn("Could not install git hooks. Run 'pnpm exec simple-git-hooks' manually."),!1}}import*as Oe from"@clack/prompts";import j from"picocolors";function fn(e,t){t.dockerComposeGenerated&&!t.dockerAvailable&&Oe.log.warn("Docker not found. Install Docker to run local services: https://docs.docker.com/get-docker/");let r=[];if(r.push(`cd ${e.projectDir}`),t.pnpmInstalled||r.push("pnpm install"),t.dockerComposeGenerated){let o=e.dockerServices.map(s=>we[s].label).join(", ");r.push(`pnpm infra ${j.dim(`# ${o}`)}`)}if(r.push(`pnpm dev ${j.dim("# http://localhost:3000")}`),t.skippedCredentials.length>0&&(r.push(""),r.push(j.dim("Fill in remaining TODO values in .env"))),Oe.note(r.join(`
|
|
1129
|
+
`),j.yellow("Start Development")),t.dockerComposeGenerated){let o=[];o.push(`App ${j.cyan("http://localhost:3000")}`),e.architecture==="separate"&&o.push(`API ${j.cyan("http://localhost:3010")}`),e.dockerServices.includes("mailpit")&&o.push(`Mailpit ${j.cyan("http://localhost:8025")}`),e.dockerServices.includes("inngest")&&o.push(`Inngest ${j.cyan("http://localhost:8288")}`),Oe.note(o.join(`
|
|
1130
|
+
`),j.yellow("Dev Tools"))}let i=[],n=Co(e);n.length>0&&i.push(`Set in production: ${j.dim(n.join(", "))}`),i.push("pnpm db:push # Run database migrations"),i.push(No(e)),Oe.note(i.join(`
|
|
1131
|
+
`),j.yellow("Deployment"))}function Co(e){let t=["DATABASE_URL","BETTER_AUTH_SECRET"];return e.cacheProvider==="upstash"?t.push("UPSTASH_REDIS_REST_URL","UPSTASH_REDIS_REST_TOKEN"):t.push("REDIS_URL"),e.paymentProvider==="stripe"?t.push("STRIPE_SECRET_KEY","STRIPE_WEBHOOK_SECRET"):e.paymentProvider==="polar"&&t.push("POLAR_ACCESS_TOKEN","POLAR_WEBHOOK_SECRET"),e.emailProvider==="ses"?t.push("AMAZON_SES_REGION","AMAZON_SES_KEY","AMAZON_SES_SECRET"):e.emailProvider==="resend"?t.push("RESEND_API_KEY"):t.push("SMTP_HOST","SMTP_PORT"),t}function No(e){switch(e.deploymentTarget){case"node":return"Deploy with Docker or your preferred Node.js host";case"vercel":return"vercel deploy # Deploy to Vercel"}}function gn(e){let t={};if(e.name!==void 0){if(!st(e.name))throw new Error(`Invalid project name "${e.name}". Use lowercase letters, numbers, and hyphens only. Must start with a letter.`);t.projectName=e.name}if(e.appName!==void 0){if(!e.appName.trim())throw new Error("App name cannot be empty.");t.appName=e.appName}if(e.location!==void 0?t.projectDir=e.location==="."?process.cwd():e.location:t.projectName!==void 0&&(t.projectDir=`./${t.projectName}`),e.frontend!==void 0){if(!je.includes(e.frontend))throw new Error(`Invalid frontend "${e.frontend}". Valid values: ${je.join(", ")}`);t.frontend=e.frontend}if(e.architecture!==void 0&&(t.architecture=e.architecture),e.payment!==void 0&&(t.paymentProvider=e.payment),e.email!==void 0&&(t.emailProvider=e.email),e.org!==void 0&&(t.multiTenancy=e.org),e.billingScope!==void 0){if(e.org===!1)throw new Error("--billing-scope requires --org to be enabled.");t.billingScope=e.billingScope}if(e.blog!==void 0&&(t.blog=e.blog),e.docs!==void 0&&(t.docs=e.docs),e.revenueSharing!==void 0&&(t.revenueSharing=e.revenueSharing),e.credits!==void 0){if(e.credits===!0&&e.payment==="none")throw new Error("--credits requires a payment provider (got --payment none).");t.credits=e.credits}if(e.docker!==void 0&&(t.dockerServices=Bt(e.docker,tt,"docker service")),e.aiTools!==void 0&&(t.aiTools=Bt(e.aiTools,rt,"AI tool")),e.socialProviders!==void 0&&(t.socialProviders=Bt(e.socialProviders,it,"social provider")),e.currency!==void 0){if(!ae.includes(e.currency))throw new Error(`Invalid currency "${e.currency}". Valid values: ${ae.join(", ")}`);t.defaultCurrency=e.currency}if(e.deploy!==void 0){if(!ce.includes(e.deploy))throw new Error(`Invalid deployment target "${e.deploy}". Valid values: ${ce.join(", ")}`);t.deploymentTarget=e.deploy}if(e.database!==void 0){if(!le.includes(e.database))throw new Error(`Invalid database provider "${e.database}". Valid values: ${le.join(", ")}`);t.databaseProvider=e.database}if(e.cache!==void 0){if(!pe.includes(e.cache))throw new Error(`Invalid cache provider "${e.cache}". Valid values: ${pe.join(", ")}`);t.cacheProvider=e.cache}if(e.demo===!0&&(t.demo=!0),e.baseUrl!==void 0){let r=e.baseUrl.trim();if(r==="")throw new Error("--base-url cannot be empty. Provide an absolute URL like https://example.com.");let i;try{i=new URL(r)}catch{throw new Error(`Invalid --base-url "${e.baseUrl}". Must be an absolute URL like https://example.com.`)}if(i.protocol!=="http:"&&i.protocol!=="https:")throw new Error(`Invalid --base-url "${e.baseUrl}". Must use http or https.`);t.baseUrl=`${i.protocol}//${i.host}`}return t}var ne={projectName:"my-saas",frontend:"nextjs",architecture:"fullstack",paymentProvider:"stripe",emailProvider:"smtp",multiTenancy:!1,billingScope:"user",blog:!0,docs:!1,revenueSharing:!1,credits:!0,dockerServices:["postgres","redis","inngest"],aiTools:[],socialProviders:[],defaultCurrency:"USD",deploymentTarget:"node",databaseProvider:"postgres",cacheProvider:"redis"};function hn(e){let t=e.projectName??ne.projectName,r=e.projectDir??`./${t}`,i=e.appName??ot(t),n=e.deploymentTarget??ne.deploymentTarget,o=B[n]?.edgeRuntime??!1,s=e.databaseProvider??(o?"neon":ne.databaseProvider),a=e.cacheProvider??(o?"upstash":ne.cacheProvider),d=e.emailProvider??(o?"resend":ne.emailProvider),y=e.dockerServices??(o?ne.dockerServices.filter(f=>f!=="postgres"&&f!=="redis"):ne.dockerServices),h={...ne,...e,projectName:t,appName:i,projectDir:r,deploymentTarget:n,databaseProvider:s,cacheProvider:a,emailProvider:d,dockerServices:y};h.paymentProvider==="none"&&(h.credits=!1);for(let f of $e){if(h.deploymentTarget!==f.target)continue;let S=h.databaseProvider===f.provider?"database":"cache";if(h.databaseProvider===f.provider||h.cacheProvider===f.provider)throw new Error(`Incompatible: --deploy ${f.target} + --${S} ${f.provider}. ${f.reason}`)}for(let f of Ue)if(h.architecture===f.architecture&&h.deploymentTarget===f.target)throw new Error(`Incompatible: --architecture ${f.architecture} + --deploy ${f.target}. ${f.reason}`);return h}function Bt(e,t,r){if(e.trim()==="")return[];let i=e.split(",").map(o=>o.trim()).filter(Boolean),n=i.filter(o=>!t.includes(o));if(n.length>0)throw new Error(`Invalid ${r}(s): ${n.join(", ")}. Valid values: ${t.join(", ")}`);return i}import Mo from"picocolors";var Fo="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";function Bo(e){if(e===void 0)return;let t=e.trim().replace(/^v/,"");if(!/^\d+\.\d+\.\d+$/.test(t))throw new Error(`Invalid template version "${e}". Use semver like 1.2.3.`);return t}function yn(e){e.command("init").description("Scaffold a new GenerateSaaS project").argument("[apiKey]","license key (same as --api-key)").option("-n, --name <name>","project name (lowercase, hyphens, starts with letter)").option("--app-name <name>","display name for the app").option("-l, --location <path>","project directory (default: ./{name})").addOption(new V("--frontend <type>","frontend framework").choices([...je])).addOption(new V("--architecture <type>","fullstack or separate").choices([...Ze])).addOption(new V("--payment <provider>","payment provider").choices([...Qe])).addOption(new V("--email <provider>","email provider").choices([...et])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").addOption(new V("--billing-scope <scope>","billing scope (requires --org)").choices([...nt])).option("--blog","enable blog").option("--no-blog","disable blog").option("--docs","include the docs app (apps/docs, Fumadocs)").option("--no-docs","exclude the docs app").option("--revenue-sharing","enable revenue sharing").option("--no-revenue-sharing","disable revenue sharing").option("--credits","enable credits system").option("--no-credits","disable credits system (subscription-only)").option("--docker <services>","comma-separated: postgres,redis,inngest,mailpit").option("--ai-tools <tools>","comma-separated: claude-code,cursor,codex,gemini-cli,windsurf").option("--social-providers <providers>","comma-separated: google,github,facebook,discord,x").addOption(new V("--currency <code>","default currency for billing").choices([...ae])).addOption(new V("--deploy <target>","deployment target").choices([...ce])).addOption(new V("--database <provider>","database provider").choices([...le])).addOption(new V("--cache <provider>","cache provider").choices([...pe])).option("--template-version <version>","specific template version to scaffold").option("--api-key <key>","API key (skips interactive prompt)").option("--base-url <url>","public base URL (e.g. https://example.com) - bakes into canonical/og/sitemap").option("-y, --yes","accept defaults for unspecified options (non-interactive)").addOption(new V("--demo","first-party demo build: keep sample content, mark site non-indexable - requires CI API key").hideHelp()).addOption(new V("--no-db-migration","skip generating the baseline DB migration (internal: demos/CI/playground)").hideHelp()).action(async(t,r)=>{await Go(t?{...r,apiKey:t}:r)})}async function Go(e){let t=performance.now();Jt("1.11.0");let r,i;try{r=gn(e),i=Bo(e.templateVersion)}catch(u){E.cancel(I(u)),process.exit(1)}let n=E.spinner(),o;try{o=await be({apiKey:e.apiKey,prompt:!e.yes})}catch(u){E.cancel(I(u)),process.exit(1)}e.demo&&$t(o)!==Fo&&(E.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let s=X(o),a=async()=>{let u=await re(s),b=u.latest,H=i??b;if(i&&!u.versions.some(Y=>Y.version===H))throw new Error(`Template version "${i}" is not available.`);return{latestVersion:b,selectedVersion:H}};n.start("Verifying access...");let d,y;try{({latestVersion:d,selectedVersion:y}=await a()),n.stop("Access verified."),ue(o)}catch(u){if(n.stop("Access verification failed."),u instanceof R&&u.status===401){e.yes&&(E.cancel("Invalid API key. Cannot prompt in non-interactive mode."),process.exit(1)),E.log.warning("Invalid API key."),o=await Fe(),s=X(o),n.start("Verifying access...");try{({latestVersion:d,selectedVersion:y}=await a()),n.stop("Access verified."),ue(o)}catch(b){n.stop("Access verification failed."),E.cancel(b instanceof R&&b.status===401?"Invalid API key.":I(b)),process.exit(1)}}else E.cancel(I(u)),process.exit(1)}E.log.success(`Latest version: ${d}`),y!==d&&E.log.success(`Using template version: ${y}`);let h;e.yes?h=hn(r):h=await Xt(r);let f;n.start("Activating license...");try{let u=crypto.randomUUID(),b=()=>({frontend:h.frontend,version:y,installId:u,projectName:h.projectName,options:rr(h)}),H;try{H=await Tt(s,b())}catch(Y){let J=lt(Y);if(!J?.lastAllowedVersion)throw Y;n.stop("License activation failed."),e.yes&&(E.cancel(`${J.message} Re-run with --template-version ${J.lastAllowedVersion}.`),process.exit(1));let Ee=await E.confirm({message:`Your update window has ended. Continue with v${J.lastAllowedVersion} (the last version your license covers)?`});(E.isCancel(Ee)||!Ee)&&(E.cancel("Setup cancelled."),process.exit(0)),y=J.lastAllowedVersion,n.start(`Activating license for v${y}...`),H=await Tt(s,b())}f={token:H.token,keyHash:$t(o),installId:u},n.stop("License activated.")}catch(u){n.stop("License activation failed."),E.cancel(I(u)),process.exit(1)}let S=Vo(h.projectDir);if(Lo(S)&&$o(S).length>0)if(e.yes)E.log.info(`Directory ${S} is not empty. Merging (keeping existing files, overwriting conflicts).`);else{let b=await E.select({message:`Directory ${S} is not empty.`,options:[{value:"merge",label:"Merge",hint:"keep existing files, overwrite conflicts"},{value:"overwrite",label:"Overwrite",hint:"delete everything and start fresh"},{value:"cancel",label:"Cancel"}]});(E.isCancel(b)||b==="cancel")&&(E.cancel("Setup cancelled."),process.exit(0)),b==="overwrite"&&Uo(S,{recursive:!0,force:!0})}let k={...h,projectDir:S,version:y,...e.demo?{docs:!1}:{}};n.start("Downloading template...");try{await pt(s,y,S),n.stop("Template downloaded.")}catch(u){n.stop("Download failed."),E.cancel(I(u)),process.exit(1)}let ie;n.start("Generating project files...");try{if({dockerComposeGenerated:ie}=await ht(k),!e.demo){let u=await Re(S);await l(jo(S,at),JSON.stringify(u,null," ")+`
|
|
1132
|
+
`),await nn(S,S)}await an(S,k.aiTools),await tn(k,f),n.stop("Project files generated.")}catch(u){n.stop("Generation failed."),E.cancel(I(u)),process.exit(1)}await ln(S);let x=await pn(S);x&&k.demo!==!0&&e.dbMigration!==!1&&await dn(S),await un(S),x&&await mn(S);let $=he("docker"),Z=bt(k).map(u=>u.key).filter(u=>!k.credentials?.[u]);fn(k,{pnpmInstalled:x,dockerComposeGenerated:ie,dockerAvailable:$,skippedCredentials:Z}),Wt(),E.log.info(Mo.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as Sn}from"fs";import{readFile as En}from"fs/promises";import{join as ze,resolve as Jo}from"path";import*as _ from"@clack/prompts";import xe from"picocolors";import{mkdtemp as Ko,rm as zo}from"fs/promises";import{tmpdir as Ho}from"os";import{join as Yo}from"path";async function Gt(e,t,r,i){let n=await Ko(Yo(Ho(),"generatesaas-stage-"));try{await pt(e,t,n),await ht({...r,projectDir:n}),await Ut(n,i)}finally{await zo(n,{recursive:!0,force:!0})}}function vn(e){let r=(e.startsWith("v")?e.slice(1):e).match(/^(\d+)\.(\d+)\.(\d+)$/);return r?[Number(r[1]),Number(r[2]),Number(r[3])]:null}function vt(e,t){let r=vn(e),i=vn(t);if(!r||!i)return 0;for(let n=0;n<3;n++)if(r[n]!==i[n])return r[n]-i[n];return 0}function wn(e){e.command("update").description("Update AI skill files and stage template updates").option("--cwd <path>","project directory (default: current directory)").action(async t=>{let r=Jo(t.cwd??process.cwd()),i=ze(r,Q),n;try{n=JSON.parse(await En(i,"utf-8"))}catch{_.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let o;try{o=await be()}catch(d){_.cancel(I(d)),process.exit(1)}let s=X(o),a=_.spinner();try{a.start("Verifying access...");let d;try{d=await re(s)}catch(u){throw u instanceof R&&u.status===401?new Error("Your saved API key was rejected. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):u}a.stop("Access verified."),ue(o),a.start("Fetching latest skill files...");let y=await tr(s,d.latest);await Mt(r,y.skillMd,y.scripts,n.aiTools);let h=Vt(n.aiTools);if(a.stop("Skills updated."),_.log.success(`Skill files installed to ${xe.cyan(h.length.toString())} locations.`),n.version===d.latest){_.log.info(`Already on the latest version (${n.version}).`);return}if(n.licenseToken)try{let u=await nr(s,{currentToken:n.licenseToken,newVersion:d.latest});n.licenseToken=u.token,await l(i,JSON.stringify(n,null," ")+`
|
|
1133
|
+
`),_.log.success("License refreshed.")}catch(u){let b=lt(u);b&&(_.cancel(b.message),process.exit(1)),_.log.warn("License refresh skipped.")}let f=en(n,r),S=ze(r,Qt);a.start(`Staging v${d.latest} (shaped for your config)...`),await Gt(s,d.latest,f,S),a.stop("Template staged.");let{text:k,title:ie}=await Wo(s,d,n.version);k&&_.note(k,ie);let x=ze(r,at),$=ze(r,ct),Se=!Sn($),Z=!Sn(x);if(Se){if(a.start("Building baseline template (one-time migration)..."),await Gt(s,n.version,f,$),Z){let u=await Re($);await l(x,JSON.stringify(u,null," ")+`
|
|
1134
|
+
`)}if(a.stop("Baseline template stored."),!Z){let u=await qo(x,$);u>0&&_.log.warn(`Rebuilt baseline differs from the original for ${u} file(s) (the CLI's shaping evolved since this project was scaffolded). Classification still follows the committed template-hashes.json; upstream diffs for those files may include unrelated noise.`)}}else if(Z){a.start("Computing baseline template hashes...");let u=await Re($);await l(x,JSON.stringify(u,null," ")+`
|
|
1135
|
+
`),a.stop("Baseline hashes computed.")}if(await l(ze(r,er),JSON.stringify({currentVersion:n.version,targetVersion:d.latest,changelog:k,stagedAt:new Date().toISOString()},null," ")+`
|
|
1136
|
+
`),_.log.info(`Update staged: ${xe.cyan(n.version)} \u2192 ${xe.cyan(d.latest)}`),n.aiTools&&n.aiTools.length>0){let u=n.aiTools[0],b=Le[u].label;_.log.info(`Open your project in ${xe.cyan(b)} and ask: ${xe.cyan("'update my GenerateSaaS project'")}`)}else _.log.info(`Ask your AI coding assistant to ${xe.cyan("'update my GenerateSaaS project'")}.`)}catch(d){a.stop("Failed."),_.cancel(`Update failed: ${I(d)}`),process.exit(1)}})}async function Wo(e,t,r){let i=t.latest,n=t.versions.filter(s=>vt(s.version,r)>0&&vt(s.version,i)<=0).sort((s,a)=>vt(s.version,a.version));if(n.length<=1)return{text:await At(e,i),title:`Changelog v${i}`};let o=[];for(let s of n){let a=null;try{a=await At(e,s.version)}catch{a=null}let d=s.date?` (${s.date.slice(0,10)})`:"",y=s.breaking?" [BREAKING]":"";o.push(`# v${s.version}${d}${y}
|
|
1137
|
+
|
|
1138
|
+
${a??"_No changelog available for this release._"}`)}return{text:o.join(`
|
|
1139
|
+
|
|
1140
|
+
`),title:`Changelog v${r} \u2192 v${i}`}}async function qo(e,t){let r=JSON.parse(await En(e,"utf-8")),i=await Re(t),n=0;for(let[o,s]of Object.entries(r))Ae(o)||i[o]!==s&&n++;for(let o of Object.keys(i))o in r||n++;return n}import*as L from"@clack/prompts";import M from"picocolors";import{readFile as Xo}from"fs/promises";import{join as Zo,resolve as Qo}from"path";function bn(e){e.command("status").description("Show project status and check for updates").option("--cwd <path>","project directory (default: current directory)").action(async t=>{let r=Qo(t.cwd??process.cwd()),i=Zo(r,Q),n;try{n=JSON.parse(await Xo(i,"utf-8"))}catch{L.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let o=[`Version: ${M.cyan(n.version)}`,`Frontend: ${M.cyan(n.frontend)}`,n.deploymentTarget?`Deploy target: ${M.cyan(n.deploymentTarget)}`:null,n.databaseProvider?`Database: ${M.cyan(n.databaseProvider)}`:null,n.cacheProvider?`Cache: ${M.cyan(n.cacheProvider)}`:null,n.aiTools&&n.aiTools.length>0?`AI tools: ${M.cyan(n.aiTools.join(", "))}`:null].filter(Boolean).join(`
|
|
1141
|
+
`);L.note(o,M.bold("Project Status"));let s=L.spinner();s.start("Checking for updates...");try{let a=await be(),d=X(a),h=(await re(d)).latest;n.version===h?(s.stop("Up to date."),L.log.success(`Already on the latest version (${M.green(h)})`)):(s.stop("Update available."),L.log.warning(`Update available: ${M.yellow(n.version)} \u2192 ${M.green(h)}`),L.log.info(`Open this project in your AI coding agent and ask it to ${M.cyan("update my GenerateSaaS project")} - it fetches and applies the update for you.`))}catch(a){s.stop("Check failed."),a instanceof R&&a.status===401?L.log.warning("Invalid API key. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):L.log.warning(`Could not check for updates: ${I(a)}`)}})}import{readFile as es}from"fs/promises";import*as T from"@clack/prompts";import A from"picocolors";function ts(){return process.env.GENERATESAAS_API_KEY??Me()}function rs(e){return{verdict:e.verdict,plan:e.license?.plan??e.domainInstalls.find(t=>t.ownerPlan)?.ownerPlan??null,mismatchDomain:e.install?.domain??null,ejectedAt:e.install?.ejectedAt??e.domainInstalls.find(t=>t.ejectedAt)?.ejectedAt??null}}function ns(e){return{verdict:e.verdict??"unknown",ejectedAt:e.ejectedAt??null}}function is(e){switch(e.verdict){case"licensed":return T.log.success(`${A.green("LICENSED")} - resolves to an account with an active${e.plan?` ${e.plan}`:""} license.`),!0;case"ejected":return T.log.success(`${A.green("EJECTED")} - a licensed buyer opted this install out of telemetry${e.ejectedAt?` on ${e.ejectedAt.slice(0,10)}`:""}. The site is legitimate.`),!0;case"revoked":return T.log.error(`${A.red("REVOKED")} - the owning account no longer holds a plan (refund or chargeback). This deployment is no longer licensed.`),!1;case"token_domain_mismatch":return T.log.error(`${A.red("LEAKED TOKEN")} - this license belongs to a different deployment${e.mismatchDomain?` (${A.cyan(e.mismatchDomain)})`:""}, not this site. The token was copied from a licensed project.`),!1;case"no_license_history":return T.log.error(`${A.red("NO LICENSE HISTORY")} - no license has ever been associated with this site. If it runs GenerateSaaS, treat it as unlicensed.`),!1;default:return T.log.warn(`${A.yellow("UNKNOWN")} - could not cross-reference the records right now. Try again shortly.`),!1}}async function An(e,t){let r=process.env.GENERATESAAS_API_URL??Ve,i=ts();e.start("Cross-referencing license records...");try{let n=i?rs(await ir(r,i,{lkh:t.lkh,nid:t.nid,domain:t.domain})):ns(await kt(r,{token:t.token,domain:t.domain}));return e.stop(`${A.green("Checked")} - records cross-referenced`),is(n)}catch(n){return e.stop(`${A.yellow("Skipped")} - ${I(n)}`),null}}function os(e){let t=e.split(".");if(t.length!==3||!t[1])throw new Error("Invalid JWT format");let r=Buffer.from(t[1],"base64url").toString("utf-8");return JSON.parse(r)}function Kt(e){return typeof e!="number"?"unknown":new Date(e*1e3).toISOString().split("T")[0]}function Tn(e){T.note([`License ID: ${A.cyan(String(e.lid??"unknown"))}`,`Version: ${A.cyan(String(e.ver??"unknown"))}`,`Init version: ${String(e.iver??"unknown")}`,`Frontend: ${String(e.fe??"unknown")}`,`Created: ${Kt(e.pat)}`,`Last updated: ${Kt(e.uat)}`,`Expires: ${Kt(e.exp)}`,`Install ID: ${String(e.nid??"unknown")}`].join(`
|
|
1142
|
+
`),A.yellow("License Details"))}function ss(e){let r=(/^https?:\/\//i.test(e)?e:`https://${e}`).replace(/\/+$/,"");if(r.endsWith("/api"))return[`${r}/license`];try{if(new URL(r).pathname!=="/")return[`${r}/license`]}catch{return[`${r}/license`]}return[`${r}/api/license`,`${r}/license`]}async function kn(e){let t=T.spinner(),r=null,i="no candidates";for(let s of ss(e)){t.start(`Checking ${s}...`);try{let a=await fetch(s);if(!a.ok){i=`${s} returned ${a.status}`,t.stop(`${A.yellow("Not here")} - ${i}`);continue}let d=(await a.text()).trim();if(!d||d.split(".").length!==3){i=`${s} did not return a JWT`,t.stop(`${A.yellow("Not here")} - ${i}`);continue}r=d,t.stop(`${A.green("Found")} - license endpoint responded`);break}catch(a){i=`${s}: ${I(a)}`,t.stop(`${A.yellow("Unreachable")} - ${i}`)}}if(r===null){T.log.warn(`No license endpoint found (last: ${i}). The site may be ejected, not a GenerateSaaS app, or serving its API elsewhere.`);let s=In(e);return s?await An(t,{domain:s})??!1:!1}let n;try{n=os(r)}catch{return T.log.error("Could not decode JWT payload."),!1}t.start("Verifying signature...");try{let s=process.env.GENERATESAAS_API_URL??Ve,a=await kt(s,{token:r});if(a.valid)t.stop(`${A.green("Valid")} - signature verified`);else return t.stop(`${A.red("Invalid")} - ${a.reason}`),!1}catch{return t.stop(`${A.yellow("Skipped")} - could not reach verification service`),T.log.warn("Signature not verified. Displaying unverified claims:"),Tn(n),!1}return Tn(n),await An(t,{token:r,lkh:typeof n.lkh=="string"?n.lkh:void 0,nid:typeof n.nid=="string"?n.nid:void 0,domain:In(e)})??!0}function In(e){try{return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`).hostname}catch{return}}function Pn(e){e.command("verify").description("Verify a GenerateSaaS license on a deployed site").argument("[url]","URL of the site to verify (e.g. https://example.com or https://example.com/api)").option("--file <path>","file with URLs to check, one per line").action(async(t,r)=>{if(!t&&!r.file&&(T.cancel("Provide a URL or --file <path>."),process.exit(1)),r.file){let n=(await es(r.file,"utf-8")).split(`
|
|
1143
|
+
`).map(s=>s.trim()).filter(s=>s&&!s.startsWith("#"));n.length===0&&(T.cancel("No URLs found in file."),process.exit(1));let o=0;for(let s of n)await kn(s)&&o++,T.log.info("");T.log.success(`${o}/${n.length} sites verified.`)}else await kn(t)||process.exit(1)})}import{existsSync as as,rmSync as cs}from"fs";import*as F from"@clack/prompts";function _n(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){as(q)?(cs(q),F.log.success("API key removed.")):F.log.info("No API key configured.");return}let r=Me();r?F.log.info(`Current API key: ****${r.slice(-4)}`):F.log.info("No API key configured.");let i=await Fe(),n=X(i),o=F.spinner();o.start("Verifying API key...");try{await re(n),o.stop("API key verified."),ue(i),F.log.success("API key saved.")}catch(s){o.stop("Verification failed."),s instanceof R&&s.status===401?F.cancel("Invalid API key."):F.cancel(I(s)),process.exit(1)}})}import{existsSync as St,rmSync as ls,readFileSync as Ht,writeFileSync as Rn}from"fs";import{join as ye}from"path";import*as O from"@clack/prompts";var ps=["packages/api/src/functions/maintenance/license-heartbeat.ts","packages/api/src/lib/manifest.ts","packages/api/src/routes/internal/license.ts"],ds=[{file:"packages/api/src/routes/inngest.ts",removals:[`import { licenseHeartbeatFunction } from "../functions/maintenance/license-heartbeat";
|
|
1139
1144
|
`,` licenseHeartbeatFunction,
|
|
1140
1145
|
`]},{file:"packages/api/src/routes/internal/index.ts",removals:[`import licenseRoutes from "./license";
|
|
1141
1146
|
`,` .route("/license", licenseRoutes)
|
|
1142
|
-
`]}];function
|
|
1143
|
-
`).filter(
|
|
1144
|
-
`);return
|
|
1147
|
+
`]}];function us(e){return(e&&e.length>0?e.map(r=>Ke[r]):Object.values(Ke)).map(r=>ye(r,jt))}function zt(e){return St(e)?(ls(e,{recursive:!0}),!0):!1}function ms(e,t){if(!St(e))return!1;let r=Ht(e,"utf-8"),i=r;for(let n of t)i=i.replace(n,"");return i===r?!1:(Rn(e,i,"utf-8"),!0)}function fs(e){let t=ye(e,".gitignore");if(!St(t))return!1;let r=Ht(t,"utf-8"),i=r.split(`
|
|
1148
|
+
`).filter(n=>!n.includes(".generatesaas")).join(`
|
|
1149
|
+
`);return i===r?!1:(Rn(t,i,"utf-8"),!0)}function On(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=ye(t,Q),i;try{i=JSON.parse(Ht(r,"utf-8"))}catch{O.cancel("No GenerateSaaS project found in this directory."),process.exit(1)}let n=await O.text({message:'Type "eject" to confirm (this cannot be undone):',validate:a=>{if(a!=="eject")return'Type "eject" to confirm, or press Ctrl+C to cancel.'}});if(O.isCancel(n)&&(O.cancel("Eject cancelled."),process.exit(0)),i.licenseToken)try{await fetch("https://generatesaas.com/api/v1/heartbeat",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${i.licenseToken}`},body:JSON.stringify({event:"eject",version:i.version,frontend:i.frontend}),signal:AbortSignal.timeout(5e3)}),O.log.info("Recorded the opt-out with generatesaas.com (final event - nothing is sent after this).")}catch{O.log.warn("Could not reach generatesaas.com to record the opt-out. Ejecting anyway.")}let o=[],s=[];for(let a of us(i.aiTools))zt(ye(t,a))&&o.push(a);for(let a of ps)zt(ye(t,a))&&o.push(a);zt(ye(t,U))&&o.push(U+"/");for(let a of ds){let d=ye(t,a.file);ms(d,a.removals)?s.push(a.file):St(d)&&O.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}fs(t)&&s.push(".gitignore");for(let a of o)O.log.info(`Deleted ${a}`);for(let a of s)O.log.info(`Modified ${a}`);O.log.success("Ejected successfully. This project is now fully standalone.")})}var ve=new gs().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("1.11.0").addHelpText("after",`
|
|
1145
1150
|
Examples:
|
|
1146
1151
|
$ generatesaas init Interactive setup
|
|
1147
1152
|
$ generatesaas init -n my-app -y Quick setup with defaults
|
|
1148
1153
|
$ generatesaas status Check for updates
|
|
1149
1154
|
$ generatesaas auth Set or update API key
|
|
1150
|
-
`);
|
|
1155
|
+
`);yn(ve);wn(ve);bn(ve);Pn(ve);_n(ve);On(ve);ve.parseAsync().catch(e=>{xn.cancel("An unexpected error occurred."),console.error(e),process.exit(1)});
|