generatesaas 1.11.1 → 1.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +67 -67
- package/dist/skill/content/scripts/_helpers.js +3 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as
|
|
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:
|
|
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=>
|
|
5
|
-
`),v=[k!=="none"?` Payment: ${g.cyan(
|
|
6
|
-
`),D=[g.bold("Project"),c,"",g.bold("Infrastructure"),p,"",g.bold("Features"),v];if(
|
|
2
|
+
import{Command as As}from"commander";import*as Cn from"@clack/prompts";import{existsSync as Go,readdirSync as Ko,rmSync as zo}from"fs";import{join as Ho,resolve as Yo}from"path";import{Option as V}from"commander";import*as E from"@clack/prompts";import*as Je from"@clack/prompts";import At from"picocolors";function qt(e){let t=e?` GenerateSaaS v${e} `:" GenerateSaaS ";Je.intro(At.bgYellow(At.black(t)))}function Xt(){Je.outro(At.yellow("Happy building!"))}import*as m from"@clack/prompts";import g from"picocolors";var Ne={nextjs:{label:"Next.js",hint:"React 19 + Next.js 16"},nuxt:{label:"Nuxt",hint:"Vue 3 + Nuxt 4"}},Le={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)"}},We={stripe:{label:"Stripe"},polar:{label:"Polar"},none:{label:"None",hint:"disable payments"}},qe={smtp:{label:"SMTP",hint:"Mailpit for local dev"},ses:{label:"Amazon SES"},resend:{label:"Resend"}},Xe={user:{label:"Per user",hint:"each user has their own subscription"},organization:{label:"Per organization",hint:"org subscription shared by members"}},be={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}},$e={"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"}]}},Ue=[{target:"vercel",provider:"redis",reason:"Vercel serverless cannot maintain persistent Redis connections. Consider Upstash."}],je=[{architecture:"separate",target:"vercel",reason:"Standalone backends need a long-running runtime. Vercel's per-function TypeScript check conflicts with pnpm's isolated install. Use --architecture fullstack for serverless, or deploy the standalone backend to a long-running runtime (Render, Fly.io, Railway, Coolify, Dokploy, or your own VPS via Docker)."}],Zt=["Local file storage (sharp, geoip-lite)","SMTP email (use Resend or SES instead)","Content API git integration"];function Ze(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 Ve=["nextjs","nuxt"],Qe=["fullstack","separate"],et=["stripe","polar","none"],tt=["smtp","ses","resend"],rt=["postgres","redis","inngest","mailpit"],nt=["claude-code","cursor","codex","gemini-cli","windsurf"],it=["user","organization"],ae=["USD","EUR","GBP","CAD","AUD","BRL","JPY"],ce=["node","vercel"],le=["postgres","neon","supabase"],pe=["redis","upstash"],ot=["google","github","facebook","discord","x"];function st(e){return e.split("-").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function at(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 Tt(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 Qt(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(!at(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:st(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(Ne),p=await m.select({message:"Frontend framework:",options:c.map(v=>({value:v,label:Ne[v].label,hint:Ne[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?je.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(je.filter(c=>c.target===s).map(c=>c.architecture)),y=Qe.filter(c=>!d.has(c)),h=e?.architecture??await(async()=>{if(y.length===1){let p=y[0];return m.log.info(`Auto-selected ${Le[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:Le[p].label,hint:Le[p].hint}))});return w(c),c})(),f=e?.databaseProvider??await(async()=>{t=!0;let c=le.filter(v=>!Ue.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=>!Ue.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=Zt.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:et.map(p=>({value:p,label:We[p].label,hint:We[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:tt.map(p=>({value:p,label:qe[p].label,hint:qe[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})(),Ee=e?.billingScope??"user";if($&&e?.billingScope===void 0){t=!0;let c=await m.select({message:"Billing scope:",options:it.map(p=>({value:p,label:Xe[p].label,hint:Xe[p].hint}))});w(c),Ee=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=ot.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=[...rt].filter(C=>C!=="mailpit");x==="smtp"&&c.push("mailpit");let p=c.map(C=>({value:C,label:be[C].label,hint:be[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})(),we=e?.aiTools??await(async()=>{t=!0;let c=nt.map(v=>({value:v,label:$e[v].label})),p=await m.multiselect({message:"Which AI coding tools do you use?",options:c,initialValues:[],required:!1});return w(p),p})(),bt=e?.demo,Ye=Tt({databaseProvider:f,cacheProvider:S,paymentProvider:k,emailProvider:x,socialProviders:Y,demo:bt}),Ce={};if(Ye.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 Ye)if(t=!0,c.secret){let p=await m.password({message:c.message,mask:"*"});w(p),typeof p=="string"&&p.trim()&&(Ce[c.key]=p.trim())}else{let p=await m.text({message:c.message,placeholder:c.placeholder});w(p),typeof p=="string"&&p.trim()&&(Ce[c.key]=p.trim())}}if(t){let c=[` Name: ${g.cyan(r)}`,` App name: ${g.cyan(i)}`,` Location: ${g.cyan(n)}`,` Frontend: ${g.cyan(Ne[o].label)}`,` Architecture: ${g.cyan(Le[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=>be[oe].label).join(", "))}`:` Docker: ${g.dim("none")}`].filter(Boolean).join(`
|
|
5
|
+
`),v=[k!=="none"?` Payment: ${g.cyan(We[k].label)} (${ie})`:` Payment: ${g.dim("none")}`,` Credits: ${H?g.cyan("Yes"):g.dim("No")}`,` Email: ${g.cyan(qe[x].label)}`,` Multi-tenancy: ${$?g.cyan("Yes")+` (billing: ${Xe[Ee].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")}`,we.length>0?` AI tools: ${g.cyan(we.map(oe=>$e[oe].label).join(", "))}`:` AI tools: ${g.dim("none")}`].join(`
|
|
6
|
+
`),D=[g.bold("Project"),c,"",g.bold("Infrastructure"),p,"",g.bold("Features"),v];if(Ye.length>0){let oe=Ye.map(Wt=>{let Nn=Ce[Wt.key]?g.green("provided"):g.dim("skipped");return` ${Wt.key}: ${Nn}`}).join(`
|
|
7
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:
|
|
9
|
-
`,{mode:384})}async function
|
|
10
|
-
`)}function
|
|
11
|
-
- **Payments:** ${
|
|
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:Ee,blog:Z,docs:u,revenueSharing:b,credits:H,dockerServices:J,aiTools:we,socialProviders:Y,defaultCurrency:ie,...Object.keys(Ce).length>0?{credentials:Ce}:{},...e?.baseUrl!==void 0?{baseUrl:e.baseUrl}:{},...bt!==void 0?{demo:bt}:{}}}import{createReadStream as Gn}from"fs";import{mkdir as Kn}from"fs/promises";import{Readable as zn}from"stream";import{pipeline as ar}from"stream/promises";import{extract as Hn}from"tar";import{join as de}from"path";import{homedir as Ln}from"os";var Me=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",U=".generatesaas",Q=de(U,"manifest.json"),er=de(U,"hashes.json"),ct=de(U,"template-hashes.json"),lt=de(U,"template"),tr=de(U,"staging"),rr=de(U,"staging.json"),q=de(Ln(),".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:Me}}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 $n,readFileSync as Un,writeFileSync as jn,mkdirSync as Vn}from"fs";import{dirname as Mn}from"path";import*as te from"@clack/prompts";function Fe(){if(!$n(q))return null;try{let e=JSON.parse(Un(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){Vn(Mn(q),{recursive:!0}),jn(q,JSON.stringify({apiKey:e},null," ")+`
|
|
9
|
+
`,{mode:384})}async function Ae(e){if(e?.apiKey)return e.apiKey;let t=process.env.GENERATESAAS_API_KEY;if(t)return t;let r=Fe();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 Be()}async function Be(){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 kt(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 nr(e,t){return await(await ee(e,`/skill/${encodeURIComponent(t)}`)).json()}function pt(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 ir(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 It(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 or(e,t){return await(await ee(e,"/license/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function Pt(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 sr(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 _t=new Set([".git","node_modules",".pnpm-store",".env",".env.test",".turbo",".nuxt",".output",".data","dist",".next",".svelte-kit",".wrangler",".devcontainer","playwright-report","test-results"]),Rt=new Set(["pnpm-lock.yaml"]);function Te(e){if(Ot(e))return!0;for(let t of e.split("/"))if(Rt.has(t))return!0;return!1}var Fn=new Set(["data","mksaas","references","scripts",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","README.md","TODO.md","OVERVIEW.md"]),Bn=["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 Ot(e){let t=e.split("/");for(let r of t)if(_t.has(r))return!0;if(Fn.has(t[0]))return!0;for(let r of Bn)if(e===r||e.startsWith(r+"/"))return!0;return!1}async function dt(e,t,r){await Kn(r,{recursive:!0});let i=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(i){await ar(Gn(i),cr(r));return}let n=await ee(e,`/template/${encodeURIComponent(t)}`);if(!n.body)throw new Error("Empty response body");let o=zn.fromWeb(n.body);await ar(o,cr(r))}function cr(e){return Hn({cwd:e,strip:1,filter:t=>{let r=t.replace(/^[^/]+\//,"");return r?!Ot(r):!0},sync:!1})}import{readFile as Yn,rm as lr,writeFile as Jn}from"fs/promises";import{join as xt}from"path";var Wn=["apps/web-nuxt/public/images/blog","apps/web-next/public/images/blog","packages/content/en/blog","packages/content/ro/blog"];async function pr(e){await Promise.all(Wn.map(t=>lr(xt(e,t),{recursive:!0,force:!0})))}async function dr(e,t){t.includes("claude-code")||await lr(xt(e,".claude"),{recursive:!0,force:!0})}async function ur(e,t){let r=xt(e,".claude","settings.json"),i;try{i=await Yn(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=>qn(o,t))),await Jn(r,JSON.stringify(n,null," ")+`
|
|
10
|
+
`)}function qn(e,t){return!(e.startsWith("mcp__")||t!=="nuxt"&&e.includes("nuxt"))}import{join as mr}from"path";import{mkdir as Xn,readdir as Zn,rm as Qn,rmdir as ei,writeFile as ti}from"fs/promises";import{dirname as ut,join as ri,relative as ni,sep as ii}from"path";function me(e){return e.split(ii).join("/")}async function mt(e){await Xn(e,{recursive:!0})}async function l(e,t){await mt(ut(e)),await ti(e,t,"utf-8")}async function Dt(e,t){await Qn(e,{force:!0});let r=ut(e);for(;r!==t&&r!==ut(r);){try{await ei(r)}catch{return}r=ut(r)}}async function fe(e,t,r){let i=[],n=await Zn(e,{withFileTypes:!0});for(let o of n){let s=ri(e,o.name),a=me(ni(t,s));r(a)||(o.isDirectory()?i.push(...await fe(s,t,r)):o.isFile()&&i.push(s))}return i}var oi={postgres:"Postgres",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"},si={resend:"Resend",ses:"Amazon SES",smtp:"SMTP"},ai={redis:"Redis",upstash:"Upstash Redis"},ci={node:"Node.js / Docker",vercel:"Vercel"},li={stripe:"Stripe",polar:"Polar"};function pi(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=ai[e.cacheProvider],y=ci[e.deploymentTarget],h=e.paymentProvider==="none"?"":`
|
|
11
|
+
- **Payments:** ${li[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
|
|
@@ -36,10 +36,10 @@ 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 + ${oi[e.databaseProvider]}
|
|
40
40
|
- **Cache + jobs:** ${d} + Inngest
|
|
41
41
|
- **Auth:** Better Auth${h}
|
|
42
|
-
- **Email:** ${
|
|
42
|
+
- **Email:** ${si[e.emailProvider]}
|
|
43
43
|
- **Deploy:** ${y}
|
|
44
44
|
|
|
45
45
|
## Where things live (extend these - don't reinvent)
|
|
@@ -80,8 +80,8 @@ 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 fr(e){await l(mr(e.projectDir,"AGENTS.md"),pi(e)),await l(mr(e.projectDir,"CLAUDE.md"),`@AGENTS.md
|
|
84
|
+
`)}import{join as di}from"path";var ui={postgres:"Postgres (self-hosted)",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"};function mi(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=ui[e.databaseProvider],s=Ze(e),a=e.architecture==="fullstack"?`${i} app at \`${n}/\` with the Hono API mounted inside it. A standalone \`apps/backend/\` is also included (inert in this fullstack setup) so you can split the API into a separate service later without re-scaffolding.`:`${i} app at \`${n}/\` and a separate Hono backend at \`apps/backend/\`.`,d=e.architecture==="fullstack"?`- App + API: http://localhost:3000
|
|
85
85
|
- Inngest dev server: http://127.0.0.1:8288`:`- App: http://localhost:3000
|
|
86
86
|
- API: http://localhost:3010
|
|
87
87
|
- Inngest dev server: http://127.0.0.1:8288`,y=s?`pnpm infra # optional: starts local Docker services (Postgres / Redis / Inngest / Mailpit)
|
|
@@ -142,7 +142,7 @@ pnpm dlx generatesaas eject
|
|
|
142
142
|
## Updates
|
|
143
143
|
|
|
144
144
|
${f}
|
|
145
|
-
`}async function
|
|
145
|
+
`}async function gr(e){await l(di(e.projectDir,"README.md"),mi(e))}import{join as fi}from"path";function gi(e){let t=e.split(".");return t.length>=3?t.slice(1).join("."):e}async function hr(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=gi(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" }
|
|
@@ -308,8 +308,8 @@ export * from "./pricing";
|
|
|
308
308
|
export * from "./roles";
|
|
309
309
|
export * from "./section-tabs";
|
|
310
310
|
export * from "./tenancy";
|
|
311
|
-
`,h
|
|
312
|
-
`)}async function
|
|
311
|
+
`,h=fi(e.projectDir,"packages/config/src/index.ts");await l(h,y)}import{join as hi}from"path";function yi(e){return e==="stripe"?' stripePriceId: "",':' polarProductId: "",'}function Ct(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 yr(e){let t=hi(e.projectDir,"packages/config/src/pricing.ts"),r=e.defaultCurrency;if(e.paymentProvider==="none"){let f=`import type { PricingConfig } from "@repo/config/types";
|
|
313
313
|
|
|
314
314
|
export const pricingConfig: PricingConfig = {
|
|
315
315
|
defaultPlan: "free",
|
|
@@ -338,7 +338,7 @@ export const pricingConfig: PricingConfig = {
|
|
|
338
338
|
credits: { enabled: false },
|
|
339
339
|
products: { enabled: false, items: [] }
|
|
340
340
|
};
|
|
341
|
-
`;await l(t,f);return}let i=e.paymentProvider,n=
|
|
341
|
+
`;await l(t,f);return}let i=e.paymentProvider,n=yi(i),o=e.credits,s=Ct({credits:5,rateLimit:100,withCredits:o}),a=Ct({credits:10,rateLimit:1e3,withCredits:o}),d=Ct({credits:50,rateLimit:5e3,withCredits:o}),h=`import type { PricingConfig } from "@repo/config/types";
|
|
342
342
|
|
|
343
343
|
export const pricingConfig: PricingConfig = {
|
|
344
344
|
defaultPlan: "free",
|
|
@@ -428,11 +428,11 @@ ${o?" credits: { enabled: true }":" credits: { enabled: false }"},
|
|
|
428
428
|
items: []
|
|
429
429
|
}
|
|
430
430
|
};
|
|
431
|
-
`;await l(t,h)}var
|
|
432
|
-
`)}function
|
|
433
|
-
`)}async function
|
|
434
|
-
`+
|
|
435
|
-
`+
|
|
431
|
+
`;await l(t,h)}var vi={smtp:[{key:"SMTP_HOST",defaultValue:"localhost"},{key:"SMTP_PORT",defaultValue:"1025"}],ses:[{key:"AMAZON_SES_REGION",comment:"# TODO: Configure Amazon SES credentials (e.g. us-east-1)"},{key:"AMAZON_SES_KEY"},{key:"AMAZON_SES_SECRET"}],resend:[{key:"RESEND_API_KEY",comment:"# TODO: Add your Resend API key"}]};function Si(e){let t=W[e];return t.envVars.map((r,i)=>({key:r.name,...i===0?{comment:`# TODO: Add your ${t.label} OAuth credentials`}:{}}))}var Ei={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 ke(e,t){return t?e.map(r=>{let i=t[r.key];return i?{...r,defaultValue:i,comment:void 0}:r}):e}function Ie(e,t){for(let r of e)r.comment&&t.push(r.comment),r.defaultValue!==void 0?t.push(`${r.key}=${r.defaultValue}`):t.push(`#${r.key}=`)}function Nt(e){return Array.from(crypto.getRandomValues(new Uint8Array(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}function Sr(e){return e.architecture==="fullstack"?{apiUrl:"http://localhost:3000/api",baseUrl:"http://localhost:3000"}:{apiUrl:"http://localhost:3010",baseUrl:"http://localhost:3000"}}function wi(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 ft(e,t,r,i){e.push(i==="example"?`${t}=`:`${t}=${r}`)}function vr(e,t){let r=t==="example"?void 0:e.credentials,{apiUrl:i,baseUrl:n}=Sr(e),o=[];e.architecture==="separate"?o.push("# API Configuration","# Standalone backend's own URL (the frontend reaches it via the public var above).",`API_URL=${i}`,`BASE_URL=${n}`):o.push("# App","# (API_URL is derived from the frontend's *_PUBLIC_API_URL by the runtime env schema.)",`BASE_URL=${n}`),o.push("","# Database"),Ie(ke(N[e.databaseProvider].envVars,r),o),o.push("","# Cache"),Ie(ke(G[e.cacheProvider].envVars,r),o),o.push("","# Authentication"),t==="example"&&o.push("# Generate a strong secret, e.g. `openssl rand -hex 32`"),ft(o,"BETTER_AUTH_SECRET",crypto.randomUUID(),t),o.push("","# Content API (random secret; required when contentApi feature is enabled in @repo/config)"),ft(o,"CONTENT_API_KEY",Nt(32),t),o.push("","# Job Queue - Inngest","INNGEST_APP_ID=api"),ft(o,"INNGEST_EVENT_KEY",Nt(32),t),ft(o,"INNGEST_SIGNING_KEY",Nt(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=vi[e.emailProvider];if(s&&(o.push("","# Email"),Ie(ke(s,r),o)),e.paymentProvider!=="none"){let a=Ei[e.paymentProvider];a&&(o.push("","# Payment"),Ie(ke(a,r),o))}if(e.socialProviders.length>0){o.push("","# Social auth");for(let a of e.socialProviders)Ie(ke(Si(a),r),o)}return e.demo&&(o.push("","# Captcha (Cloudflare Turnstile)"),Ie(ke([{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 bi(e){let{apiUrl:t}=Sr(e),r=e.frontend==="nextjs"?"NEXT_PUBLIC_API_URL":"NUXT_PUBLIC_API_URL",i=["# API Configuration",`${r}=${t}`],n=wi(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 Er(e){let t=bi(e);await l(`${e.projectDir}/.env`,t+`
|
|
434
|
+
`+vr(e,"env")),await l(`${e.projectDir}/.env.example`,t+`
|
|
435
|
+
`+vr(e,"example"))}import{join as Ai}from"path";async function wr(e){let t=[...e.dockerServices];if(N[e.databaseProvider].managed&&(t=t.filter(o=>o!=="postgres")),G[e.cacheProvider].managed&&(t=t.filter(o=>o!=="redis")),t.length===0)return!1;let r=[],i=[];t.includes("postgres")&&(r.push(` postgres:
|
|
436
436
|
image: postgres:18-alpine
|
|
437
437
|
ports:
|
|
438
438
|
- "\${POSTGRES_PORT:-5432}:5432"
|
|
@@ -476,8 +476,8 @@ ${r.join(`
|
|
|
476
476
|
volumes:
|
|
477
477
|
${i.join(`
|
|
478
478
|
`)}
|
|
479
|
-
`),await l(
|
|
480
|
-
`)}async function
|
|
479
|
+
`),await l(Ai(e.projectDir,"infra/docker-compose.yml"),n),!0}import{join as Ti}from"path";function ki(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 Ii(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 br(e){let t={version:"0.2.0",configurations:ki(e),compounds:Ii(e)};await l(Ti(e.projectDir,".vscode/launch.json"),JSON.stringify(t,null," ")+`
|
|
480
|
+
`)}import{join as Pi}from"path";async function Ar(e){if(e.architecture!=="separate")return;await l(Pi(e.projectDir,"apps/backend/src/index.ts"),`import { serve } from "@hono/node-server";
|
|
481
481
|
import app from "@repo/api";
|
|
482
482
|
import { closeRedis, env, logger } from "@repo/runtime";
|
|
483
483
|
|
|
@@ -508,18 +508,18 @@ bootstrap().catch((error) => {
|
|
|
508
508
|
logger.error("[Backend] Fatal error", error);
|
|
509
509
|
process.exit(1);
|
|
510
510
|
});
|
|
511
|
-
`)}import{readFile as
|
|
511
|
+
`)}import{readFile as _i}from"fs/promises";import{join as Ri}from"path";var Oi=`export * from "./db/auth";
|
|
512
512
|
export * from "./db/schema";
|
|
513
|
-
export type { User, Account, Organization, Member } from "./db/auth";`;async function
|
|
513
|
+
export type { User, Account, Organization, Member } from "./db/auth";`;async function Tr(e){let t=Ri(e.projectDir,"packages/database/src/index.ts"),r=Oi;try{let n=await _i(t,"utf-8"),o=xi(n);o&&(r=o)}catch{}let i;switch(e.databaseProvider){case"postgres":i=Di(r);break;case"neon":i=Ci(r);break;case"supabase":i=Ni(r);break}await l(t,i)}function xi(e){return e.split(`
|
|
514
514
|
`).filter(i=>i.startsWith("export type ")||i.startsWith("export * from")).join(`
|
|
515
|
-
`)}var
|
|
515
|
+
`)}var Lt=`
|
|
516
516
|
/** Extract affected-row count from a delete/update result (works for pg + postgres-js). */
|
|
517
517
|
export function affectedRowCount(result: unknown): number {
|
|
518
518
|
if (typeof result !== "object" || result === null) return 0;
|
|
519
519
|
const r = result as { rowCount?: number; count?: number };
|
|
520
520
|
return r.rowCount ?? r.count ?? 0;
|
|
521
521
|
}
|
|
522
|
-
`;function
|
|
522
|
+
`;function Di(e){return`import { drizzle } from "drizzle-orm/node-postgres";
|
|
523
523
|
import { z } from "zod";
|
|
524
524
|
import * as authSchema from "./db/auth";
|
|
525
525
|
import * as appSchema from "./db/schema";
|
|
@@ -534,7 +534,7 @@ export const db = drizzle(parsed.data.DATABASE_URL, { schema });
|
|
|
534
534
|
export const pool = db.$client;
|
|
535
535
|
|
|
536
536
|
${e}
|
|
537
|
-
${
|
|
537
|
+
${Lt}`}function Ci(e){return`import { neon } from "@neondatabase/serverless";
|
|
538
538
|
import { drizzle } from "drizzle-orm/neon-http";
|
|
539
539
|
import { z } from "zod";
|
|
540
540
|
import * as authSchema from "./db/auth";
|
|
@@ -550,7 +550,7 @@ const sql = neon(parsed.data.DATABASE_URL);
|
|
|
550
550
|
export const db = drizzle(sql, { schema });
|
|
551
551
|
|
|
552
552
|
${e}
|
|
553
|
-
${
|
|
553
|
+
${Lt}`}function Ni(e){return`import { drizzle } from "drizzle-orm/postgres-js";
|
|
554
554
|
import postgres from "postgres";
|
|
555
555
|
import { z } from "zod";
|
|
556
556
|
import * as authSchema from "./db/auth";
|
|
@@ -566,7 +566,7 @@ const client = postgres(parsed.data.DATABASE_URL);
|
|
|
566
566
|
export const db = drizzle(client, { schema });
|
|
567
567
|
|
|
568
568
|
${e}
|
|
569
|
-
${
|
|
569
|
+
${Lt}`}import{join as gt}from"path";async function kr(e){switch(e.cacheProvider){case"redis":await Li(e),await $i(e);break;case"upstash":await Ui(e),await ji(e);break}}async function Li(e){await l(gt(e.projectDir,"packages/runtime/src/redis.ts"),`import type { Store } from "hono-rate-limiter";
|
|
570
570
|
import { Redis } from "ioredis";
|
|
571
571
|
import { RedisStore, type RedisReply } from "rate-limit-redis";
|
|
572
572
|
import { env } from "./env";
|
|
@@ -690,7 +690,7 @@ export async function closeRedis() {
|
|
|
690
690
|
closed = true;
|
|
691
691
|
}
|
|
692
692
|
}
|
|
693
|
-
`)}async function
|
|
693
|
+
`)}async function $i(e){await l(gt(e.projectDir,"packages/runtime/src/mutex.ts"),`import { Mutex } from "redis-semaphore";
|
|
694
694
|
import { redis } from "./redis";
|
|
695
695
|
|
|
696
696
|
export class MutexTimeoutError extends Error {
|
|
@@ -727,7 +727,7 @@ export async function withMutex<T>(
|
|
|
727
727
|
await mutex.release();
|
|
728
728
|
}
|
|
729
729
|
}
|
|
730
|
-
`)}async function
|
|
730
|
+
`)}async function Ui(e){await l(gt(e.projectDir,"packages/runtime/src/redis.ts"),`import { Redis } from "@upstash/redis";
|
|
731
731
|
import type { Store } from "hono-rate-limiter";
|
|
732
732
|
import { env } from "./env";
|
|
733
733
|
|
|
@@ -840,7 +840,7 @@ export const limiterStore: Store = createLimiterStore();
|
|
|
840
840
|
|
|
841
841
|
/** No persistent connection to close with Upstash REST. */
|
|
842
842
|
export async function closeRedis(): Promise<void> {}
|
|
843
|
-
`)}async function
|
|
843
|
+
`)}async function ji(e){await l(gt(e.projectDir,"packages/runtime/src/mutex.ts"),`import { Lock } from "@upstash/lock";
|
|
844
844
|
import { redis } from "./redis";
|
|
845
845
|
|
|
846
846
|
export class MutexTimeoutError extends Error {
|
|
@@ -885,7 +885,7 @@ export async function withMutex<T>(
|
|
|
885
885
|
await lock.release();
|
|
886
886
|
}
|
|
887
887
|
}
|
|
888
|
-
`)}async function
|
|
888
|
+
`)}async function Ir(e){await l(`${e.projectDir}/packages/runtime/src/env.ts`,Vi(e))}function Vi(e){return`import { z } from "zod";
|
|
889
889
|
|
|
890
890
|
const EnvSchema = z.object({
|
|
891
891
|
NODE_ENV: z.enum(["development", "production"]).default("development"),
|
|
@@ -1003,13 +1003,13 @@ export const env = (() => {
|
|
|
1003
1003
|
const parsedApiUrl = new URL(env.API_URL);
|
|
1004
1004
|
export const apiBasePath = parsedApiUrl.pathname === "/" ? "" : parsedApiUrl.pathname;
|
|
1005
1005
|
export const apiOrigin = parsedApiUrl.origin;
|
|
1006
|
-
`}import{readFile as
|
|
1007
|
-
`)}function
|
|
1008
|
-
`)}var
|
|
1009
|
-
`))}}import{readFile as
|
|
1010
|
-
`);let o=
|
|
1006
|
+
`}import{readFile as Mi}from"fs/promises";import{join as K}from"path";var ht={"@upstash/redis":"^1.37.0","@upstash/lock":"^0.2.1","@neondatabase/serverless":"^1.0.1",postgres:"^3.4.7"};async function _e(e){let t=await Mi(e,"utf-8");return JSON.parse(t)}async function Re(e,t){await l(e,JSON.stringify(t,null," ")+`
|
|
1007
|
+
`)}function Ge(e,t){for(let r of t)delete e.dependencies?.[r],delete e.devDependencies?.[r]}function Pe(e,t,r,i=!1){let n=i?"devDependencies":"dependencies";e[n]||(e[n]={}),e[n][t]=r}async function Pr(e){await Fi(e),await Bi(e),await Gi(e),await Ki(e),e.frontend==="nextjs"?await Hi(e):await zi(e)}async function Fi(e){let t=K(e.projectDir,"packages/api/package.json"),r=await _e(t);Ge(r,["sharp","@types/sharp"]),await Re(t,r)}async function Bi(e){let t=K(e.projectDir,"packages/runtime/package.json"),r=await _e(t);e.cacheProvider==="upstash"&&(Ge(r,["ioredis","rate-limit-redis","redis-semaphore"]),Pe(r,"@upstash/redis",ht["@upstash/redis"]),Pe(r,"@upstash/lock",ht["@upstash/lock"])),await Re(t,r)}async function Gi(e){let t=K(e.projectDir,"packages/database/package.json"),r=await _e(t);e.databaseProvider==="neon"?(Ge(r,["pg","@types/pg"]),Pe(r,"@neondatabase/serverless",ht["@neondatabase/serverless"])):e.databaseProvider==="supabase"&&(Ge(r,["pg","@types/pg"]),Pe(r,"postgres",ht.postgres)),await Re(t,r)}async function Ki(e){if(e.architecture!=="separate")return;let t=K(e.projectDir,"apps/backend/package.json"),r=await _e(t);e.deploymentTarget!=="node"&&Ge(r,["@hono/node-server"]),await Re(t,r)}async function zi(e){if(e.architecture!=="separate")return;let t=K(e.projectDir,"apps/web-nuxt");await Dt(K(t,"server/api/[...paths].ts"),t);let r=K(t,"package.json"),i=await _e(r),n=i.dependencies?.["@repo/api"];n&&(delete i.dependencies?.["@repo/api"],Pe(i,"@repo/api",n,!0)),await Re(r,i)}async function Hi(e){if(e.architecture!=="separate")return;let t=K(e.projectDir,"apps/web-next");await Dt(K(t,"app/api/[[...rest]]/route.ts"),t);let r=K(t,"package.json"),i=await _e(r),n=i.dependencies?.["@repo/api"];n&&(delete i.dependencies?.["@repo/api"],Pe(i,"@repo/api",n,!0)),await Re(r,i)}import{readFile as Rr}from"fs/promises";import{join as Or}from"path";async function xr(e){let t=Or(e.projectDir,"turbo.json"),r;try{r=await Rr(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 Yi=["base.json","node.json","next.json"],_r="GenerateSaaS ";async function Dr(e){if(!e.demo)for(let t of Yi){let r=Or(e.projectDir,"tooling/typescript",t),i;try{i=await Rr(r,"utf-8")}catch{continue}let n=JSON.parse(i);typeof n.display!="string"||!n.display.startsWith(_r)||(n.display=n.display.slice(_r.length),await l(r,JSON.stringify(n,null," ")+`
|
|
1009
|
+
`))}}import{readFile as Ji,rm as Wi}from"fs/promises";import{existsSync as Cr}from"fs";import{join as Nr}from"path";async function Lr(e){if(e.frontend==="nuxt")return;let t=Nr(e.projectDir,"packages/i18n/package.json");if(!Cr(t))throw new Error(`pruneI18nNuxt: expected ${t} to exist - did the i18n package move?`);let r=JSON.parse(await Ji(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=Nr(e.projectDir,"packages/i18n/nuxt");if(i&&!Cr(o))throw new Error(`pruneI18nNuxt: packages/i18n declares a Nuxt export surface but ${o} is missing - did the i18n Nuxt module move?`);await Wi(o,{recursive:!0,force:!0})}import{readFile as $r,rm as qi}from"fs/promises";import{join as $t}from"path";async function Ur(e){e.cacheProvider==="upstash"&&await Promise.all([Xi(e.projectDir),Zi(e.projectDir),qi($t(e.projectDir,"packages/runtime/tests/redis.test.ts"),{force:!0})])}async function Xi(e){let t=$t(e,"packages/runtime/tests/setup.ts"),r;try{r=await $r(t,"utf-8")}catch{return}let i=r.replace(/\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
|
|
1011
1011
|
UPSTASH_REDIS_REST_TOKEN: "test-token",
|
|
1012
|
-
`);i!==r&&await l(t,i)}async function
|
|
1012
|
+
`);i!==r&&await l(t,i)}async function Zi(e){let t=$t(e,"packages/api/tests/setup.ts"),r;try{r=await $r(t,"utf-8")}catch{return}let i=r;i=i.replace(/\tREDIS_URL:\s*"[^"]*",?\n/g,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
|
|
1013
1013
|
UPSTASH_REDIS_REST_TOKEN: "test-token",
|
|
1014
1014
|
`),i=i.replace(/vi\.mock\("ioredis"[\s\S]*?\n\}\);\n\n?/,""),i=i.replace(/vi\.mock\("rate-limit-redis"[\s\S]*?\n\}\);\n\n?/,""),i=i.replace(/\t\t\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
|
|
1015
1015
|
UPSTASH_REDIS_REST_TOKEN: "test-token",
|
|
@@ -1030,8 +1030,8 @@ vi.mock("@upstash/lock", () => {
|
|
|
1030
1030
|
return { Lock };
|
|
1031
1031
|
});
|
|
1032
1032
|
|
|
1033
|
-
$1`),i!==r&&await l(t,i)}import{readdir as
|
|
1034
|
-
`)}async function
|
|
1033
|
+
$1`),i!==r&&await l(t,i)}import{readdir as Qi,readFile as eo,rm as Ke}from"fs/promises";import{join as ge}from"path";var to=new Set(["ci.yml"]),ro=["cli","cli:clean","demo:bench","playground:regen","playground:test","playground:test:units"];async function jr(e){let t=ge(e.projectDir,".github/workflows"),r=await Qi(t).catch(()=>[]);for(let i of r)to.has(i)||await Ke(ge(t,i),{recursive:!0,force:!0})}async function Vr(e){let t=ge(e.projectDir,"package.json"),r=await eo(t,"utf-8"),i=JSON.parse(r),n=!1;if(i.scripts){for(let o of ro)o in i.scripts&&(delete i.scripts[o],n=!0);if(!Ze(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 Mr(e){let t=e.frontend==="nextjs"?"apps/web-nuxt":"apps/web-next";await Ke(ge(e.projectDir,t),{recursive:!0,force:!0})}async function Fr(e){e.docs||await Ke(ge(e.projectDir,"apps/docs"),{recursive:!0})}async function Br(e){let t=e.frontend==="nextjs"?"docs/nuxt":"docs/next";await Ke(ge(e.projectDir,t),{recursive:!0,force:!0}),await Ke(ge(e.projectDir,"docs/index.mdx"))}import{join as no}from"path";async function Gr(e){let t=io(e);await l(no(e.projectDir,".github/workflows/ci.yml"),t)}function io(e){let t=N[e.databaseProvider].managed,r=e.cacheProvider==="upstash",i=e.frontend==="nuxt",n=t?"":` services:
|
|
1035
1035
|
postgres:
|
|
1036
1036
|
image: postgres:18-alpine
|
|
1037
1037
|
env:
|
|
@@ -1111,45 +1111,45 @@ ${n} env:
|
|
|
1111
1111
|
run: echo "DATABASE_URL=${o}" > .env
|
|
1112
1112
|
|
|
1113
1113
|
- run: pnpm test
|
|
1114
|
-
`}import{readFile as
|
|
1115
|
-
`))}async function
|
|
1116
|
-
`))}async function
|
|
1114
|
+
`}import{readFile as Kr}from"fs/promises";import{existsSync as oo}from"fs";import{join as Ut}from"path";var zr="@repo/database";function so(e){return e?"pnpm -F @repo/database reset && pnpm -F @repo/database push --force":"pnpm -F @repo/database migrate"}function ao(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 co(e,t,r){let i=await Kr(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(zr)||(n.scripts={...n.scripts,[t]:`${r} && ${o}`},await l(e,JSON.stringify(n,null," ")+`
|
|
1115
|
+
`))}async function lo(e,t){let r=oo(e)?JSON.parse(await Kr(e,"utf-8")):{$schema:"https://openapi.vercel.sh/vercel.json"},i=r.buildCommand?.trim()||"pnpm build";i.includes(zr)||(r.buildCommand=`${t} && ${i}`,await l(e,JSON.stringify(r,null," ")+`
|
|
1116
|
+
`))}async function Hr(e){let t=so(e.demo===!0),r=ao(e.architecture,e.frontend),i=Ut(e.projectDir,"apps",r);switch(e.deploymentTarget){case"node":await co(Ut(i,"package.json"),"start",t);return;case"vercel":await lo(Ut(i,"vercel.json"),t);return;default:{let n=e.deploymentTarget;throw new Error(`generateDeployScripts: unhandled deployment target "${String(n)}"`)}}}import{readFile as po}from"fs/promises";import{existsSync as uo}from"fs";import{join as yt}from"path";var mo=["stripe","polar"];async function Yr(e){let t=yt(e.projectDir,"packages/payments/src"),r=e.paymentProvider,i=`// Active payment provider. To switch, change the path below to
|
|
1117
1117
|
// "./polar/index" or "./none/index" (other folders are kept in place).
|
|
1118
1118
|
export { ops } from "./${r}/index";
|
|
1119
|
-
`;await l(
|
|
1119
|
+
`;await l(yt(t,"providers/index.ts"),i),await l(yt(t,"index.ts"),await fo(t,r))}async function fo(e,t){let r=yt(e,"index.ts");if(!uo(r))throw new Error(`generatePaymentBarrel: expected ${r} to exist - did packages/payments move?`);let i=await po(r,"utf-8"),n=mo.filter(s=>s!==t);return i.split(`
|
|
1120
1120
|
`).filter(s=>!n.some(a=>s.includes(`./providers/${a}/`))).join(`
|
|
1121
|
-
`)}import{readdir as
|
|
1121
|
+
`)}import{readdir as go,readFile as ho}from"fs/promises";import{join as Jr}from"path";async function Wr(e){if(e.demo)return;let t=e.appName.trim()||e.projectName,r=JSON.stringify(t).slice(1,-1),i=Jr(e.projectDir,"packages/i18n/translations"),n;try{n=await go(i)}catch{return}for(let o of n){let s=Jr(i,o,"web.json"),a;try{a=await ho(s,"utf-8")}catch{continue}let d=a.replaceAll("GenerateSaaS",r);d!==a&&await l(s,d)}}import{readFile as yo}from"fs/promises";import{join as qr}from"path";var vo=[".nuxt/",".nuxt",".nitro/",".nitro",".output/",".output","_locales/"],So=[".next/",".next",".svelte-kit/",".svelte-kit",".wrangler/",".wrangler",".dev.vars"];async function Zr(e){let r=e.frontend==="nuxt"?So:vo;await Xr(qr(e.projectDir,".gitignore"),r),await Xr(qr(e.projectDir,".dockerignore"),r)}async function Xr(e,t){let r;try{r=await yo(e,"utf-8")}catch{return}let i=new Set(t),n=r.split(`
|
|
1122
1122
|
`).filter(o=>!i.has(o.trim()));n.length!==r.split(`
|
|
1123
1123
|
`).length&&await l(e,n.join(`
|
|
1124
|
-
`))}async function
|
|
1125
|
-
`),await l(
|
|
1126
|
-
`)}import{relative as
|
|
1124
|
+
`))}async function vt(e){let t=e.projectDir;e.demo||await pr(t),await dr(t,e.aiTools),await ur(t,e.frontend),await fr(e),await gr(e),await hr(e),e.demo||await yr(e),await Er(e);let r=await wr(e);return await br(e),await Ar(e),await Tr(e),await kr(e),await Ir(e),await Pr(e),await xr(e),await Dr(e),await Lr(e),await Ur(e),await jr(e),await Gr(e),await Vr(e),await Mr(e),await Fr(e),await Br(e),await Hr(e),await Yr(e),await Wr(e),await Zr(e),{dockerComposeGenerated:r}}import{basename as en,join as tn,relative as wo}from"path";import{createHash as Qr}from"crypto";import{readFile as Eo}from"fs/promises";async function St(e){let t=await Eo(e);return Qr("sha256").update(t).digest("hex")}function jt(e){return Qr("sha256").update(e).digest("hex")}var bo=new Set(["data",U]);function Ao(e){let t=e.split("/");for(let r of t)if(_t.has(r)||Rt.has(r)||bo.has(r)||r.startsWith(".env")&&!r.includes("example"))return!0;return!1}function rn(e,t){return{projectName:e.projectName??en(t),appName:e.appName??e.projectName??en(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 nn(e,t){let i=(await fe(e.projectDir,e.projectDir,Ao)).sort(),n=await Promise.all(i.map(async a=>[me(wo(e.projectDir,a)),await St(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(tn(e.projectDir,Q),JSON.stringify(s,null," ")+`
|
|
1125
|
+
`),await l(tn(e.projectDir,er),JSON.stringify(o,null," ")+`
|
|
1126
|
+
`)}import{relative as To}from"path";async function Oe(e){let r=(await fe(e,e,Te)).sort(),i=await Promise.all(r.map(async n=>[me(To(e,n)),await St(n)]));return Object.fromEntries(i)}import{copyFile as ko,mkdir as Io,rm as Po}from"fs/promises";import{dirname as _o,join as on,relative as Ro}from"path";import{existsSync as Oo}from"fs";async function Vt(e,t){Oo(t)&&await Po(t,{recursive:!0,force:!0});let r=await fe(e,e,Te);for(let i of r){let n=me(Ro(e,i)),o=on(t,n);await Io(_o(o),{recursive:!0}),await ko(i,o)}}async function sn(e,t){await Vt(e,on(t,lt))}import{existsSync as xo}from"fs";import{readFile as an,readdir as Do}from"fs/promises";import{join as z,dirname as Co,resolve as No,sep as Lo}from"path";import{fileURLToPath as $o}from"url";var ze={"claude-code":".claude/skills",cursor:".cursor/skills",codex:".agents/skills","gemini-cli":".gemini/skills",windsurf:".windsurf/skills"},Vl=Object.values(ze),Mt="generatesaas-update",cn=Co($o(import.meta.url));function Uo(){let e=z(cn,"skill","content");return xo(e)?e:z(cn,"content")}function Ft(e){return!e||e.length===0?[]:e.map(t=>ze[t])}async function Bt(e,t,r,i){let n=Ft(i);for(let o of n){let s=z(e,o,Mt),a=z(s,"scripts"),d=z(s,"references");await mt(a),await mt(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=No(a,y);f.startsWith(a+Lo)&&await l(f,h)}}}async function ln(e,t){let r=Uo(),i=await an(z(r,"SKILL.md"),"utf-8"),n=z(r,"scripts"),o=await Do(n),s={};for(let a of o)a!==".gitkeep"&&(s[a]=await an(z(n,a),"utf-8"));await Bt(e,i,s,t)}import{execFile as jo,execFileSync as Vo}from"child_process";import{access as pn,readFile as Mo}from"fs/promises";import{join as Gt}from"path";import*as P from"@clack/prompts";function ye(e){try{let t=process.platform==="win32"?"where":"which";return Vo(t,[e],{stdio:"ignore"}),!0}catch{return!1}}function he(e,t,r,i=3e5){return new Promise((n,o)=>{jo(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
1127
|
`);o(new Error(f?`${s.message}
|
|
1128
|
-
${f}`:s.message))}else n()})})}async function
|
|
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")}`),
|
|
1130
|
-
`),j.yellow("Dev Tools"))}let i=[],n=
|
|
1131
|
-
`),j.yellow("Deployment"))}function Co(e){let t=["DATABASE_URL","BETTER_AUTH_SECRET"];return e.cacheProvider==="upstash"?t.push("UPSTASH_REDIS_REST_URL","UPSTASH_REDIS_REST_TOKEN"):t.push("REDIS_URL"),e.paymentProvider==="stripe"?t.push("STRIPE_SECRET_KEY","STRIPE_WEBHOOK_SECRET"):e.paymentProvider==="polar"&&t.push("POLAR_ACCESS_TOKEN","POLAR_WEBHOOK_SECRET"),e.emailProvider==="ses"?t.push("AMAZON_SES_REGION","AMAZON_SES_KEY","AMAZON_SES_SECRET"):e.emailProvider==="resend"?t.push("RESEND_API_KEY"):t.push("SMTP_HOST","SMTP_PORT"),t}function No(e){switch(e.deploymentTarget){case"node":return"Deploy with Docker or your preferred Node.js host";case"vercel":return"vercel deploy # Deploy to Vercel"}}function gn(e){let t={};if(e.name!==void 0){if(!st(e.name))throw new Error(`Invalid project name "${e.name}". Use lowercase letters, numbers, and hyphens only. Must start with a letter.`);t.projectName=e.name}if(e.appName!==void 0){if(!e.appName.trim())throw new Error("App name cannot be empty.");t.appName=e.appName}if(e.location!==void 0?t.projectDir=e.location==="."?process.cwd():e.location:t.projectName!==void 0&&(t.projectDir=`./${t.projectName}`),e.frontend!==void 0){if(!je.includes(e.frontend))throw new Error(`Invalid frontend "${e.frontend}". Valid values: ${je.join(", ")}`);t.frontend=e.frontend}if(e.architecture!==void 0&&(t.architecture=e.architecture),e.payment!==void 0&&(t.paymentProvider=e.payment),e.email!==void 0&&(t.emailProvider=e.email),e.org!==void 0&&(t.multiTenancy=e.org),e.billingScope!==void 0){if(e.org===!1)throw new Error("--billing-scope requires --org to be enabled.");t.billingScope=e.billingScope}if(e.blog!==void 0&&(t.blog=e.blog),e.docs!==void 0&&(t.docs=e.docs),e.revenueSharing!==void 0&&(t.revenueSharing=e.revenueSharing),e.credits!==void 0){if(e.credits===!0&&e.payment==="none")throw new Error("--credits requires a payment provider (got --payment none).");t.credits=e.credits}if(e.docker!==void 0&&(t.dockerServices=Bt(e.docker,tt,"docker service")),e.aiTools!==void 0&&(t.aiTools=Bt(e.aiTools,rt,"AI tool")),e.socialProviders!==void 0&&(t.socialProviders=Bt(e.socialProviders,it,"social provider")),e.currency!==void 0){if(!ae.includes(e.currency))throw new Error(`Invalid currency "${e.currency}". Valid values: ${ae.join(", ")}`);t.defaultCurrency=e.currency}if(e.deploy!==void 0){if(!ce.includes(e.deploy))throw new Error(`Invalid deployment target "${e.deploy}". Valid values: ${ce.join(", ")}`);t.deploymentTarget=e.deploy}if(e.database!==void 0){if(!le.includes(e.database))throw new Error(`Invalid database provider "${e.database}". Valid values: ${le.join(", ")}`);t.databaseProvider=e.database}if(e.cache!==void 0){if(!pe.includes(e.cache))throw new Error(`Invalid cache provider "${e.cache}". Valid values: ${pe.join(", ")}`);t.cacheProvider=e.cache}if(e.demo===!0&&(t.demo=!0),e.baseUrl!==void 0){let r=e.baseUrl.trim();if(r==="")throw new Error("--base-url cannot be empty. Provide an absolute URL like https://example.com.");let i;try{i=new URL(r)}catch{throw new Error(`Invalid --base-url "${e.baseUrl}". Must be an absolute URL like https://example.com.`)}if(i.protocol!=="http:"&&i.protocol!=="https:")throw new Error(`Invalid --base-url "${e.baseUrl}". Must use http or https.`);t.baseUrl=`${i.protocol}//${i.host}`}return t}var ne={projectName:"my-saas",frontend:"nextjs",architecture:"fullstack",paymentProvider:"stripe",emailProvider:"smtp",multiTenancy:!1,billingScope:"user",blog:!0,docs:!1,revenueSharing:!1,credits:!0,dockerServices:["postgres","redis","inngest"],aiTools:[],socialProviders:[],defaultCurrency:"USD",deploymentTarget:"node",databaseProvider:"postgres",cacheProvider:"redis"};function hn(e){let t=e.projectName??ne.projectName,r=e.projectDir??`./${t}`,i=e.appName??ot(t),n=e.deploymentTarget??ne.deploymentTarget,o=B[n]?.edgeRuntime??!1,s=e.databaseProvider??(o?"neon":ne.databaseProvider),a=e.cacheProvider??(o?"upstash":ne.cacheProvider),d=e.emailProvider??(o?"resend":ne.emailProvider),y=e.dockerServices??(o?ne.dockerServices.filter(f=>f!=="postgres"&&f!=="redis"):ne.dockerServices),h={...ne,...e,projectName:t,appName:i,projectDir:r,deploymentTarget:n,databaseProvider:s,cacheProvider:a,emailProvider:d,dockerServices:y};h.paymentProvider==="none"&&(h.credits=!1);for(let f of $e){if(h.deploymentTarget!==f.target)continue;let S=h.databaseProvider===f.provider?"database":"cache";if(h.databaseProvider===f.provider||h.cacheProvider===f.provider)throw new Error(`Incompatible: --deploy ${f.target} + --${S} ${f.provider}. ${f.reason}`)}for(let f of Ue)if(h.architecture===f.architecture&&h.deploymentTarget===f.target)throw new Error(`Incompatible: --architecture ${f.architecture} + --deploy ${f.target}. ${f.reason}`);return h}function Bt(e,t,r){if(e.trim()==="")return[];let i=e.split(",").map(o=>o.trim()).filter(Boolean),n=i.filter(o=>!t.includes(o));if(n.length>0)throw new Error(`Invalid ${r}(s): ${n.join(", ")}. Valid values: ${t.join(", ")}`);return i}import Mo from"picocolors";var Fo="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";function Bo(e){if(e===void 0)return;let t=e.trim().replace(/^v/,"");if(!/^\d+\.\d+\.\d+$/.test(t))throw new Error(`Invalid template version "${e}". Use semver like 1.2.3.`);return t}function yn(e){e.command("init").description("Scaffold a new GenerateSaaS project").argument("[apiKey]","license key (same as --api-key)").option("-n, --name <name>","project name (lowercase, hyphens, starts with letter)").option("--app-name <name>","display name for the app").option("-l, --location <path>","project directory (default: ./{name})").addOption(new V("--frontend <type>","frontend framework").choices([...je])).addOption(new V("--architecture <type>","fullstack or separate").choices([...Ze])).addOption(new V("--payment <provider>","payment provider").choices([...Qe])).addOption(new V("--email <provider>","email provider").choices([...et])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").addOption(new V("--billing-scope <scope>","billing scope (requires --org)").choices([...nt])).option("--blog","enable blog").option("--no-blog","disable blog").option("--docs","include the docs app (apps/docs, Fumadocs)").option("--no-docs","exclude the docs app").option("--revenue-sharing","enable revenue sharing").option("--no-revenue-sharing","disable revenue sharing").option("--credits","enable credits system").option("--no-credits","disable credits system (subscription-only)").option("--docker <services>","comma-separated: postgres,redis,inngest,mailpit").option("--ai-tools <tools>","comma-separated: claude-code,cursor,codex,gemini-cli,windsurf").option("--social-providers <providers>","comma-separated: google,github,facebook,discord,x").addOption(new V("--currency <code>","default currency for billing").choices([...ae])).addOption(new V("--deploy <target>","deployment target").choices([...ce])).addOption(new V("--database <provider>","database provider").choices([...le])).addOption(new V("--cache <provider>","cache provider").choices([...pe])).option("--template-version <version>","specific template version to scaffold").option("--api-key <key>","API key (skips interactive prompt)").option("--base-url <url>","public base URL (e.g. https://example.com) - bakes into canonical/og/sitemap").option("-y, --yes","accept defaults for unspecified options (non-interactive)").addOption(new V("--demo","first-party demo build: keep sample content, mark site non-indexable - requires CI API key").hideHelp()).addOption(new V("--no-db-migration","skip generating the baseline DB migration (internal: demos/CI/playground)").hideHelp()).action(async(t,r)=>{await Go(t?{...r,apiKey:t}:r)})}async function Go(e){let t=performance.now();Jt("1.11.1");let r,i;try{r=gn(e),i=Bo(e.templateVersion)}catch(u){E.cancel(I(u)),process.exit(1)}let n=E.spinner(),o;try{o=await be({apiKey:e.apiKey,prompt:!e.yes})}catch(u){E.cancel(I(u)),process.exit(1)}e.demo&&$t(o)!==Fo&&(E.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let s=X(o),a=async()=>{let u=await re(s),b=u.latest,H=i??b;if(i&&!u.versions.some(Y=>Y.version===H))throw new Error(`Template version "${i}" is not available.`);return{latestVersion:b,selectedVersion:H}};n.start("Verifying access...");let d,y;try{({latestVersion:d,selectedVersion:y}=await a()),n.stop("Access verified."),ue(o)}catch(u){if(n.stop("Access verification failed."),u instanceof R&&u.status===401){e.yes&&(E.cancel("Invalid API key. Cannot prompt in non-interactive mode."),process.exit(1)),E.log.warning("Invalid API key."),o=await Fe(),s=X(o),n.start("Verifying access...");try{({latestVersion:d,selectedVersion:y}=await a()),n.stop("Access verified."),ue(o)}catch(b){n.stop("Access verification failed."),E.cancel(b instanceof R&&b.status===401?"Invalid API key.":I(b)),process.exit(1)}}else E.cancel(I(u)),process.exit(1)}E.log.success(`Latest version: ${d}`),y!==d&&E.log.success(`Using template version: ${y}`);let h;e.yes?h=hn(r):h=await Xt(r);let f;n.start("Activating license...");try{let u=crypto.randomUUID(),b=()=>({frontend:h.frontend,version:y,installId:u,projectName:h.projectName,options:rr(h)}),H;try{H=await Tt(s,b())}catch(Y){let J=lt(Y);if(!J?.lastAllowedVersion)throw Y;n.stop("License activation failed."),e.yes&&(E.cancel(`${J.message} Re-run with --template-version ${J.lastAllowedVersion}.`),process.exit(1));let Ee=await E.confirm({message:`Your update window has ended. Continue with v${J.lastAllowedVersion} (the last version your license covers)?`});(E.isCancel(Ee)||!Ee)&&(E.cancel("Setup cancelled."),process.exit(0)),y=J.lastAllowedVersion,n.start(`Activating license for v${y}...`),H=await Tt(s,b())}f={token:H.token,keyHash:$t(o),installId:u},n.stop("License activated.")}catch(u){n.stop("License activation failed."),E.cancel(I(u)),process.exit(1)}let S=Vo(h.projectDir);if(Lo(S)&&$o(S).length>0)if(e.yes)E.log.info(`Directory ${S} is not empty. Merging (keeping existing files, overwriting conflicts).`);else{let b=await E.select({message:`Directory ${S} is not empty.`,options:[{value:"merge",label:"Merge",hint:"keep existing files, overwrite conflicts"},{value:"overwrite",label:"Overwrite",hint:"delete everything and start fresh"},{value:"cancel",label:"Cancel"}]});(E.isCancel(b)||b==="cancel")&&(E.cancel("Setup cancelled."),process.exit(0)),b==="overwrite"&&Uo(S,{recursive:!0,force:!0})}let k={...h,projectDir:S,version:y,...e.demo?{docs:!1}:{}};n.start("Downloading template...");try{await pt(s,y,S),n.stop("Template downloaded.")}catch(u){n.stop("Download failed."),E.cancel(I(u)),process.exit(1)}let ie;n.start("Generating project files...");try{if({dockerComposeGenerated:ie}=await ht(k),!e.demo){let u=await Re(S);await l(jo(S,at),JSON.stringify(u,null," ")+`
|
|
1132
|
-
`),await
|
|
1133
|
-
`),_.log.success("License refreshed.")}catch(u){let b=
|
|
1134
|
-
`)}if(a.stop("Baseline template stored."),!Z){let u=await
|
|
1135
|
-
`),a.stop("Baseline hashes computed.")}if(await l(
|
|
1136
|
-
`),_.log.info(`Update staged: ${
|
|
1128
|
+
${f}`:s.message))}else n()})})}async function dn(e){if(!ye("pnpm"))return P.log.warn("pnpm not found. Skipping lockfile regeneration."),!1;try{return await he("pnpm",["install","--lockfile-only","--no-frozen-lockfile","--config.minimumReleaseAge=0"],e),!0}catch(t){let r=t instanceof Error?t.message:String(t);return P.log.warn(`Lockfile regeneration failed: ${r}`),P.log.warn("Deploys using --frozen-lockfile may fail."),!1}}async function un(e){if(!ye("pnpm"))return P.log.warn("pnpm not found. Skipping dependency installation."),P.log.info("Install pnpm: https://pnpm.io/installation"),!1;let t=P.spinner();t.start("Installing dependencies (this may take a minute)...");try{return await he("pnpm",["install","--config.minimumReleaseAge=0"],e),t.stop("Dependencies installed."),!0}catch(r){t.stop("Dependency installation failed.");let 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 mn(e){if(!ye("pnpm"))return!1;let t=P.spinner();t.start("Generating baseline database migration...");try{return await he("pnpm",["-F","@repo/database","generate"],e),t.stop("Baseline migration generated."),!0}catch(r){t.stop("Baseline migration generation failed.");let 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 fn(e){try{return await pn(Gt(e,".git")),P.log.info("Git repository already exists, skipping init."),!0}catch{}if(!ye("git"))return P.log.warn("git not found. Skipping repository initialization."),!1;let t=P.spinner();t.start("Initializing git repository...");try{return await he("git",["init"],e),await he("git",["add","-A"],e),await he("git",["commit","--no-verify","-m","Initial commit from GenerateSaaS"],e),t.stop("Git repository initialized."),!0}catch{return t.stop("Git initialization failed."),P.log.warn("You can run git init manually later."),!1}}async function gn(e){if(!ye("pnpm"))return!1;try{await pn(Gt(e,".git"))}catch{return!1}try{let t=JSON.parse(await Mo(Gt(e,"package.json"),"utf-8")),r=!!t.devDependencies?.["simple-git-hooks"],i=!!t["simple-git-hooks"];if(!r||!i)return!1}catch{return!1}try{return await he("pnpm",["exec","simple-git-hooks"],e),!0}catch{return P.log.warn("Could not install git hooks. Run 'pnpm exec simple-git-hooks' manually."),!1}}import*as xe from"@clack/prompts";import j from"picocolors";function hn(e,t){t.dockerComposeGenerated&&!t.dockerAvailable&&xe.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=>be[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"))),xe.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")}`),xe.note(o.join(`
|
|
1130
|
+
`),j.yellow("Dev Tools"))}let i=[],n=Fo(e);n.length>0&&i.push(`Set in production: ${j.dim(n.join(", "))}`),i.push("pnpm db:push # Run database migrations"),i.push(Bo(e)),xe.note(i.join(`
|
|
1131
|
+
`),j.yellow("Deployment"))}function Fo(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 Bo(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 yn(e){let t={};if(e.name!==void 0){if(!at(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(!Ve.includes(e.frontend))throw new Error(`Invalid frontend "${e.frontend}". Valid values: ${Ve.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=Kt(e.docker,rt,"docker service")),e.aiTools!==void 0&&(t.aiTools=Kt(e.aiTools,nt,"AI tool")),e.socialProviders!==void 0&&(t.socialProviders=Kt(e.socialProviders,ot,"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 vn(e){let t=e.projectName??ne.projectName,r=e.projectDir??`./${t}`,i=e.appName??st(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 Ue){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 je)if(h.architecture===f.architecture&&h.deploymentTarget===f.target)throw new Error(`Incompatible: --architecture ${f.architecture} + --deploy ${f.target}. ${f.reason}`);return h}function Kt(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 Jo from"picocolors";var Wo="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";function qo(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 Sn(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([...Ve])).addOption(new V("--architecture <type>","fullstack or separate").choices([...Qe])).addOption(new V("--payment <provider>","payment provider").choices([...et])).addOption(new V("--email <provider>","email provider").choices([...tt])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").addOption(new V("--billing-scope <scope>","billing scope (requires --org)").choices([...it])).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 Xo(t?{...r,apiKey:t}:r)})}async function Xo(e){let t=performance.now();qt("1.11.2");let r,i;try{r=yn(e),i=qo(e.templateVersion)}catch(u){E.cancel(I(u)),process.exit(1)}let n=E.spinner(),o;try{o=await Ae({apiKey:e.apiKey,prompt:!e.yes})}catch(u){E.cancel(I(u)),process.exit(1)}e.demo&&jt(o)!==Wo&&(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 Be(),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=vn(r):h=await Qt(r);let f;n.start("Activating license...");try{let u=crypto.randomUUID(),b=()=>({frontend:h.frontend,version:y,installId:u,projectName:h.projectName,options:ir(h)}),H;try{H=await It(s,b())}catch(Y){let J=pt(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 we=await E.confirm({message:`Your update window has ended. Continue with v${J.lastAllowedVersion} (the last version your license covers)?`});(E.isCancel(we)||!we)&&(E.cancel("Setup cancelled."),process.exit(0)),y=J.lastAllowedVersion,n.start(`Activating license for v${y}...`),H=await It(s,b())}f={token:H.token,keyHash:jt(o),installId:u},n.stop("License activated.")}catch(u){n.stop("License activation failed."),E.cancel(I(u)),process.exit(1)}let S=Yo(h.projectDir);if(Go(S)&&Ko(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"&&zo(S,{recursive:!0,force:!0})}let k={...h,projectDir:S,version:y,...e.demo?{docs:!1}:{}};n.start("Downloading template...");try{await dt(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 vt(k),!e.demo){let u=await Oe(S);await l(Ho(S,ct),JSON.stringify(u,null," ")+`
|
|
1132
|
+
`),await sn(S,S)}await ln(S,k.aiTools),await nn(k,f),n.stop("Project files generated.")}catch(u){n.stop("Generation failed."),E.cancel(I(u)),process.exit(1)}await dn(S);let x=await un(S);x&&k.demo!==!0&&e.dbMigration!==!1&&await mn(S),await fn(S),x&&await gn(S);let $=ye("docker"),Z=Tt(k).map(u=>u.key).filter(u=>!k.credentials?.[u]);hn(k,{pnpmInstalled:x,dockerComposeGenerated:ie,dockerAvailable:$,skippedCredentials:Z}),Xt(),E.log.info(Jo.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as wn}from"fs";import{readFile as bn}from"fs/promises";import{join as He,resolve as rs}from"path";import*as _ from"@clack/prompts";import De from"picocolors";import{mkdtemp as Zo,rm as Qo}from"fs/promises";import{tmpdir as es}from"os";import{join as ts}from"path";async function zt(e,t,r,i){let n=await Zo(ts(es(),"generatesaas-stage-"));try{await dt(e,t,n),await vt({...r,projectDir:n}),await Vt(n,i)}finally{await Qo(n,{recursive:!0,force:!0})}}function En(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 Et(e,t){let r=En(e),i=En(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 An(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=rs(t.cwd??process.cwd()),i=He(r,Q),n;try{n=JSON.parse(await bn(i,"utf-8"))}catch{_.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let o;try{o=await Ae()}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 nr(s,d.latest);await Bt(r,y.skillMd,y.scripts,n.aiTools);let h=Ft(n.aiTools);if(a.stop("Skills updated."),_.log.success(`Skill files installed to ${De.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 or(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=pt(u);b&&(_.cancel(b.message),process.exit(1)),_.log.warn("License refresh skipped.")}let f=rn(n,r),S=He(r,tr);a.start(`Staging v${d.latest} (shaped for your config)...`),await zt(s,d.latest,f,S),a.stop("Template staged.");let{text:k,title:ie}=await ns(s,d,n.version);k&&_.note(k,ie);let x=He(r,ct),$=He(r,lt),Ee=!wn($),Z=!wn(x);if(Ee){if(a.start("Building baseline template (one-time migration)..."),await zt(s,n.version,f,$),Z){let u=await Oe($);await l(x,JSON.stringify(u,null," ")+`
|
|
1134
|
+
`)}if(a.stop("Baseline template stored."),!Z){let u=await is(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 Oe($);await l(x,JSON.stringify(u,null," ")+`
|
|
1135
|
+
`),a.stop("Baseline hashes computed.")}if(await l(He(r,rr),JSON.stringify({currentVersion:n.version,targetVersion:d.latest,changelog:k,stagedAt:new Date().toISOString()},null," ")+`
|
|
1136
|
+
`),_.log.info(`Update staged: ${De.cyan(n.version)} \u2192 ${De.cyan(d.latest)}`),n.aiTools&&n.aiTools.length>0){let u=n.aiTools[0],b=$e[u].label;_.log.info(`Open your project in ${De.cyan(b)} and ask: ${De.cyan("'update my GenerateSaaS project'")}`)}else _.log.info(`Ask your AI coding assistant to ${De.cyan("'update my GenerateSaaS project'")}.`)}catch(d){a.stop("Failed."),_.cancel(`Update failed: ${I(d)}`),process.exit(1)}})}async function ns(e,t,r){let i=t.latest,n=t.versions.filter(s=>Et(s.version,r)>0&&Et(s.version,i)<=0).sort((s,a)=>Et(s.version,a.version));if(n.length<=1)return{text:await kt(e,i),title:`Changelog v${i}`};let o=[];for(let s of n){let a=null;try{a=await kt(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
1137
|
|
|
1138
1138
|
${a??"_No changelog available for this release._"}`)}return{text:o.join(`
|
|
1139
1139
|
|
|
1140
|
-
`),title:`Changelog v${r} \u2192 v${i}`}}async function
|
|
1141
|
-
`);L.note(o,M.bold("Project Status"));let s=L.spinner();s.start("Checking for updates...");try{let a=await
|
|
1142
|
-
`),A.yellow("License Details"))}function
|
|
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
|
|
1140
|
+
`),title:`Changelog v${r} \u2192 v${i}`}}async function is(e,t){let r=JSON.parse(await bn(e,"utf-8")),i=await Oe(t),n=0;for(let[o,s]of Object.entries(r))Te(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 os}from"fs/promises";import{join as ss,resolve as as}from"path";function Tn(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=as(t.cwd??process.cwd()),i=ss(r,Q),n;try{n=JSON.parse(await os(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 Ae(),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 cs}from"fs/promises";import*as T from"@clack/prompts";import A from"picocolors";function ls(){return process.env.GENERATESAAS_API_KEY??Fe()}function ps(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 ds(e){return{verdict:e.verdict??"unknown",ejectedAt:e.ejectedAt??null}}function us(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 kn(e,t){let r=process.env.GENERATESAAS_API_URL??Me,i=ls();e.start("Cross-referencing license records...");try{let n=i?ps(await sr(r,i,{lkh:t.lkh,nid:t.nid,domain:t.domain})):ds(await Pt(r,{token:t.token,domain:t.domain}));return e.stop(`${A.green("Checked")} - records cross-referenced`),us(n)}catch(n){return e.stop(`${A.yellow("Skipped")} - ${I(n)}`),null}}function ms(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 Ht(e){return typeof e!="number"?"unknown":new Date(e*1e3).toISOString().split("T")[0]}function In(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: ${Ht(e.pat)}`,`Last updated: ${Ht(e.uat)}`,`Expires: ${Ht(e.exp)}`,`Install ID: ${String(e.nid??"unknown")}`].join(`
|
|
1142
|
+
`),A.yellow("License Details"))}function fs(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 Pn(e){let t=T.spinner(),r=null,i="no candidates";for(let s of fs(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=_n(e);return s?await kn(t,{domain:s})??!1:!1}let n;try{n=ms(r)}catch{return T.log.error("Could not decode JWT payload."),!1}t.start("Verifying signature...");try{let s=process.env.GENERATESAAS_API_URL??Me,a=await Pt(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:"),In(n),!1}return In(n),await kn(t,{token:r,lkh:typeof n.lkh=="string"?n.lkh:void 0,nid:typeof n.nid=="string"?n.nid:void 0,domain:_n(e)})??!0}function _n(e){try{return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`).hostname}catch{return}}function Rn(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 cs(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 Pn(s)&&o++,T.log.info("");T.log.success(`${o}/${n.length} sites verified.`)}else await Pn(t)||process.exit(1)})}import{existsSync as gs,rmSync as hs}from"fs";import*as F from"@clack/prompts";function On(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){gs(q)?(hs(q),F.log.success("API key removed.")):F.log.info("No API key configured.");return}let r=Fe();r?F.log.info(`Current API key: ****${r.slice(-4)}`):F.log.info("No API key configured.");let i=await Be(),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 wt,rmSync as ys,readFileSync as Jt,writeFileSync as xn}from"fs";import{join as ve}from"path";import*as O from"@clack/prompts";var vs=["packages/api/src/functions/maintenance/license-heartbeat.ts","packages/api/src/lib/manifest.ts","packages/api/src/routes/internal/license.ts"],Ss=[{file:"packages/api/src/routes/inngest.ts",removals:[`import { licenseHeartbeatFunction } from "../functions/maintenance/license-heartbeat";
|
|
1144
1144
|
`,` licenseHeartbeatFunction,
|
|
1145
1145
|
`]},{file:"packages/api/src/routes/internal/index.ts",removals:[`import licenseRoutes from "./license";
|
|
1146
1146
|
`,` .route("/license", licenseRoutes)
|
|
1147
|
-
`]}];function
|
|
1147
|
+
`]}];function Es(e){return(e&&e.length>0?e.map(r=>ze[r]):Object.values(ze)).map(r=>ve(r,Mt))}function Yt(e){return wt(e)?(ys(e,{recursive:!0}),!0):!1}function ws(e,t){if(!wt(e))return!1;let r=Jt(e,"utf-8"),i=r;for(let n of t)i=i.replace(n,"");return i===r?!1:(xn(e,i,"utf-8"),!0)}function bs(e){let t=ve(e,".gitignore");if(!wt(t))return!1;let r=Jt(t,"utf-8"),i=r.split(`
|
|
1148
1148
|
`).filter(n=>!n.includes(".generatesaas")).join(`
|
|
1149
|
-
`);return i===r?!1:(
|
|
1149
|
+
`);return i===r?!1:(xn(t,i,"utf-8"),!0)}function Dn(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=ve(t,Q),i;try{i=JSON.parse(Jt(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 Es(i.aiTools))Yt(ve(t,a))&&o.push(a);for(let a of vs)Yt(ve(t,a))&&o.push(a);Yt(ve(t,U))&&o.push(U+"/");for(let a of Ss){let d=ve(t,a.file);ws(d,a.removals)?s.push(a.file):wt(d)&&O.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}bs(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 Se=new As().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("1.11.2").addHelpText("after",`
|
|
1150
1150
|
Examples:
|
|
1151
1151
|
$ generatesaas init Interactive setup
|
|
1152
1152
|
$ generatesaas init -n my-app -y Quick setup with defaults
|
|
1153
1153
|
$ generatesaas status Check for updates
|
|
1154
1154
|
$ generatesaas auth Set or update API key
|
|
1155
|
-
`);
|
|
1155
|
+
`);Sn(Se);An(Se);Tn(Se);Rn(Se);On(Se);Dn(Se);Se.parseAsync().catch(e=>{Cn.cancel("An unexpected error occurred."),console.error(e),process.exit(1)});
|
|
@@ -70,7 +70,9 @@ function walkDir(dir, baseDir) {
|
|
|
70
70
|
|
|
71
71
|
for (const entry of entries) {
|
|
72
72
|
const fullPath = path.join(dir, entry.name);
|
|
73
|
-
|
|
73
|
+
// path.relative() returns backslashes on Windows; exclusion matching and
|
|
74
|
+
// the hash/manifest keys are forward-slash based, so normalize here.
|
|
75
|
+
const rel = path.relative(baseDir, fullPath).split(path.sep).join("/");
|
|
74
76
|
|
|
75
77
|
if (shouldExcludeWalk(rel)) continue;
|
|
76
78
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "generatesaas",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI for scaffolding and managing GenerateSaaS projects",
|
|
6
6
|
"bin": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"build": "tsup",
|
|
30
|
-
"postbuild": "
|
|
30
|
+
"postbuild": "node -e \"const fs=require('node:fs');fs.rmSync('dist/skill/content',{recursive:true,force:true});fs.mkdirSync('dist/skill',{recursive:true});fs.cpSync('src/skill/content','dist/skill/content',{recursive:true})\"",
|
|
31
31
|
"dev": "tsup --watch",
|
|
32
32
|
"check-types": "tsc --noEmit",
|
|
33
33
|
"test": "vitest run",
|