generatesaas 1.15.2 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +149 -117
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import{Command as Cs}from"commander";import*as Gn from"@clack/prompts";import{existsSync as Qo,readdirSync as es,rmSync as ts}from"fs";import{join as rs,resolve as ns}from"path";import{Option as M}from"commander";import*as E from"@clack/prompts";import*as We from"@clack/prompts";import It from"picocolors";function er(e){let t=e?` GenerateSaaS v${e} `:" GenerateSaaS ";We.intro(It.bgYellow(It.black(t)))}function tr(){We.outro(It.yellow("Happy building!"))}import*as m from"@clack/prompts";import g from"picocolors";var $e={nextjs:{label:"Next.js",hint:"React 19 + Next.js 16"},nuxt:{label:"Nuxt",hint:"Vue 3 + Nuxt 4"}},je={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)"}},qe={stripe:{label:"Stripe"},polar:{label:"Polar"},none:{label:"None",hint:"disable payments"}},Xe={smtp:{label:"SMTP",hint:"Mailpit for local dev"},ses:{label:"Amazon SES"},resend:{label:"Resend"}},Ze={user:{label:"Per user",hint:"each user has their own subscription"},organization:{label:"Per organization",hint:"org subscription shared by members"}},ke={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}},Ue={"claude-code":{label:"Claude Code"},cursor:{label:"Cursor"},codex:{label:"Codex"},"gemini-cli":{label:"Gemini CLI"},windsurf:{label:"Windsurf"}};var ce={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 K={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"}]}},z={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"}]}},Ve=[{target:"vercel",provider:"redis",reason:"Vercel serverless cannot maintain persistent Redis connections. Consider Upstash."}],Me=[{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)."}],rr=["Local file storage (sharp, geoip-lite)","SMTP email (use Resend or SES instead)","Content API git integration"];function Qe(e){let t=N[e.databaseProvider].managed,r=z[e.cacheProvider].managed;return e.dockerServices.some(n=>!(n==="postgres"&&t||n==="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 Fe=["nextjs","nuxt"],et=["fullstack","separate"],tt=["stripe","polar","none"],rt=["smtp","ses","resend"],nt=["postgres","redis","inngest","mailpit"],it=["claude-code","cursor","codex","gemini-cli","windsurf"],ot=["user","organization"],le=["USD","EUR","GBP","CAD","AUD","BRL","JPY"],pe=["node","vercel"],de=["postgres","neon","supabase"],ue=["redis","upstash"],st=["google","github","facebook","discord","x"];function at(e){return e.split("-").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function ct(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 Pt(e){let t=[];e.databaseProvider==="neon"&&t.push({key:"DATABASE_URL",message:"Neon connection string (optional):",placeholder:"postgres://...",secret:!0}),e.databaseProvider==="supabase"&&t.push({key:"DATABASE_URL",message:"Supabase connection string (optional):",placeholder:"postgres://...",secret:!0}),e.cacheProvider==="upstash"&&(t.push({key:"UPSTASH_REDIS_REST_URL",message:"Upstash REST URL (optional):",placeholder:"https://...",secret:!1}),t.push({key:"UPSTASH_REDIS_REST_TOKEN",message:"Upstash REST token (optional):",secret:!0})),e.paymentProvider==="stripe"&&(t.push({key:"STRIPE_SECRET_KEY",message:"Stripe secret key (optional):",placeholder:"sk_test_...",secret:!0}),t.push({key:"STRIPE_WEBHOOK_SECRET",message:"Stripe webhook secret (optional):",placeholder:"whsec_...",secret:!0})),e.paymentProvider==="polar"&&(t.push({key:"POLAR_ACCESS_TOKEN",message:"Polar access token (optional):",secret:!0}),t.push({key:"POLAR_WEBHOOK_SECRET",message:"Polar webhook secret (optional):",secret:!0})),e.emailProvider==="resend"&&t.push({key:"RESEND_API_KEY",message:"Resend API key (optional):",placeholder:"re_...",secret:!0}),e.emailProvider==="ses"&&(t.push({key:"AMAZON_SES_REGION",message:"Amazon SES region (optional):",placeholder:"us-east-1",secret:!1}),t.push({key:"AMAZON_SES_KEY",message:"Amazon SES access key (optional):",secret:!0}),t.push({key:"AMAZON_SES_SECRET",message:"Amazon SES secret (optional):",secret:!0}));for(let r of e.socialProviders){let n=W[r];for(let i of n.envVars)t.push({key:i.name,message:`${i.name} (${n.label}, optional):`,secret:i.secret})}return e.demo&&t.push({key:"TURNSTILE_SECRET_KEY",message:"Cloudflare Turnstile secret key (optional):",secret:!0}),t}async function nr(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(!ct(p))return"Use lowercase letters, numbers, and hyphens only. Must start with a letter."}});return w(c),c})(),n=e?.appName??await(async()=>{t=!0;let c=await m.text({message:"App name:",initialValue:at(r),validate:p=>{if(!p?.trim())return"App name is required."}});return w(c),c})(),i=e?.projectDir??await(async()=>{t=!0;let c=await 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($e),p=await m.select({message:"Frontend framework:",options:c.map(v=>({value:v,label:$e[v].label,hint:$e[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:pe.map(p=>({value:p,label:K[p].label,hint:K[p].hint}))});w(c),s=c}let a=e?.architecture?Me.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(Me.filter(c=>c.target===s).map(c=>c.architecture)),h=et.filter(c=>!d.has(c)),y=e?.architecture??await(async()=>{if(h.length===1){let p=h[0];return m.log.info(`Auto-selected ${je[p].label} architecture (only compatible option for ${K[s].label}).`),p}t=!0;let c=await m.select({message:"Architecture:",options:h.map(p=>({value:p,label:je[p].label,hint:je[p].hint}))});return w(c),c})(),f=e?.databaseProvider??await(async()=>{t=!0;let c=de.filter(v=>!Ve.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 ${K[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=ue.filter(v=>!Ve.some(D=>D.target===s&&D.provider===v));if(c.length===1){let v=c[0];return m.log.info(`Auto-selected ${z[v].label} (only compatible option for ${K[s].label}).`),v}let p=await m.select({message:"Cache provider:",options:c.map(v=>({value:v,label:z[v].label,hint:z[v].hint}))});return w(p),p})();if(K[s]?.edgeRuntime){let c=rr.map(p=>` - ${p}`).join(`
3
- `);m.note(c,"Unavailable on edge runtime")}m.log.info(g.bold("Features"));let T=e?.paymentProvider??await(async()=>{t=!0;let c=await m.select({message:"Payment provider:",options:tt.map(p=>({value:p,label:qe[p].label,hint:qe[p].hint}))});return w(c),c})(),oe=e?.defaultCurrency??await(async()=>{if(T==="none")return"USD";t=!0;let c=await m.select({message:"Default currency:",options:le.map(p=>({value:p,label:p,hint:ce[p].name}))});return w(c),c})(),x=e?.emailProvider??await(async()=>{t=!0;let c=await m.select({message:"Email provider:",options:rt.map(p=>({value:p,label:Xe[p].label,hint:Xe[p].hint}))});return w(c),c})(),j=e?.multiTenancy??await(async()=>{t=!0;let c=await m.confirm({message:"Enable multi-tenancy (organizations)?",initialValue:!1});return w(c),c})(),we=e?.billingScope??"user";if(j&&e?.billingScope===void 0){t=!0;let c=await m.select({message:"Billing scope:",options:ot.map(p=>({value:p,label:Ze[p].label,hint:Ze[p].hint}))});w(c),we=c}let Q=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?.desktop??await(async()=>{t=!0;let c=await m.confirm({message:"Include the Electron desktop app? (apps/desktop - cross-platform, device-auth)",initialValue:!1});return w(c),c})(),J=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})(),se=T==="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})(),G=e?.socialProviders??await(async()=>{t=!0;let c=st.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 be=e?.dockerServices??await(async()=>{t=!0;let c=[...nt].filter(C=>C!=="mailpit");x==="smtp"&&c.push("mailpit");let p=c.map(C=>({value:C,label:ke[C].label,hint:ke[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})(),At=e?.aiTools??await(async()=>{t=!0;let c=it.map(v=>({value:v,label:Ue[v].label})),p=await m.multiselect({message:"Which AI coding tools do you use?",options:c,initialValues:[],required:!1});return w(p),p})(),Tt=e?.demo,Je=Pt({databaseProvider:f,cacheProvider:S,paymentProvider:T,emailProvider:x,socialProviders:G,demo:Tt}),Le={};if(Je.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 Je)if(t=!0,c.secret){let p=await m.password({message:c.message,mask:"*"});w(p),typeof p=="string"&&p.trim()&&(Le[c.key]=p.trim())}else{let p=await m.text({message:c.message,placeholder:c.placeholder});w(p),typeof p=="string"&&p.trim()&&(Le[c.key]=p.trim())}}if(t){let c=[` Name: ${g.cyan(r)}`,` App name: ${g.cyan(n)}`,` Location: ${g.cyan(i)}`,` Frontend: ${g.cyan($e[o].label)}`,` Architecture: ${g.cyan(je[y].label)}`].join(`
4
- `),p=[` Deploy target: ${g.cyan(K[s]?.label??"Node.js / Docker")}`,` Database: ${g.cyan(N[f].label)}`,` Cache: ${g.cyan(z[S].label)}`,be.length>0?` Docker: ${g.cyan(be.map(ae=>ke[ae].label).join(", "))}`:` Docker: ${g.dim("none")}`].filter(Boolean).join(`
5
- `),v=[T!=="none"?` Payment: ${g.cyan(qe[T].label)} (${oe})`:` Payment: ${g.dim("none")}`,` Credits: ${se?g.cyan("Yes"):g.dim("No")}`,` Email: ${g.cyan(Xe[x].label)}`,` Multi-tenancy: ${j?g.cyan("Yes")+` (billing: ${Ze[we].label})`:g.dim("No")}`,` Blog: ${Q?g.cyan("Yes"):g.dim("No")}`,` Docs app: ${u?g.cyan("Yes"):g.dim("No")}`,` Desktop app: ${b?g.cyan("Yes"):g.dim("No")}`,` Rev. sharing: ${J?g.cyan("Yes"):g.dim("No")}`,G.length>0?` Social login: ${g.cyan(G.map(ae=>W[ae].label).join(", "))}`:` Social login: ${g.dim("none")}`,At.length>0?` AI tools: ${g.cyan(At.map(ae=>Ue[ae].label).join(", "))}`:` AI tools: ${g.dim("none")}`].join(`
6
- `),D=[g.bold("Project"),c,"",g.bold("Infrastructure"),p,"",g.bold("Features"),v];if(Je.length>0){let ae=Je.map(Qt=>{let Kn=Le[Qt.key]?g.green("provided"):g.dim("skipped");return` ${Qt.key}: ${Kn}`}).join(`
7
- `);D.push("",g.bold("Credentials"),ae)}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:n,projectDir:i,frontend:o,architecture:y,deploymentTarget:s,databaseProvider:f,cacheProvider:S,paymentProvider:T,emailProvider:x,multiTenancy:j,billingScope:we,blog:Q,docs:u,desktop:b,revenueSharing:J,credits:se,dockerServices:be,aiTools:At,socialProviders:G,defaultCurrency:oe,...Object.keys(Le).length>0?{credentials:Le}:{},...e?.baseUrl!==void 0?{baseUrl:e.baseUrl}:{},...Tt!==void 0?{demo:Tt}:{}}}import{createReadStream as Qn}from"fs";import{mkdir as ei}from"fs/promises";import{Readable as ti}from"stream";import{pipeline as dr}from"stream/promises";import{extract as ri}from"tar";import{join as me}from"path";import{homedir as zn}from"os";var Be=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",U=".generatesaas",ee=me(U,"manifest.json"),ir=me(U,"hashes.json"),lt=me(U,"template-hashes.json"),pt=me(U,"template"),or=me(U,"staging"),sr=me(U,"staging.json"),q=me(zn(),".generatesaas");var R=class extends Error{constructor(r,n,i){super(n);this.status=r;this.body=i}status;body;name="ApiError"};function X(e){return{apiKey:e,baseUrl:Be}}async function te(e,t,r){let n=`${e.baseUrl}${t}`,i=await fetch(n,{...r,headers:{...r?.headers,Authorization:`Bearer ${e.apiKey}`,"User-Agent":"generatesaas-cli"}});if(!i.ok){let o,s;try{s=await i.json(),o=s.error??`API ${i.status}: ${t}`}catch{o=`API ${i.status}: ${t}`}throw new R(i.status,o,s)}return i}import{existsSync as Hn,readFileSync as Yn,writeFileSync as Jn,mkdirSync as Wn}from"fs";import{dirname as qn}from"path";import*as re from"@clack/prompts";function Ge(){if(!Hn(q))return null;try{let e=JSON.parse(Yn(q,"utf-8"));return e.apiKey?e.apiKey:(e.token&&!e.apiKey&&re.log.warning(`Found old GitHub token in ${q}. Run 'generatesaas init' to set up your API key.`),null)}catch{return null}}function fe(e){Wn(qn(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=Ge();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 Ke()}async function Ke(){let e=await re.text({message:"Enter your GenerateSaaS API key:",placeholder:"gs_live_...",validate:t=>{if(!t?.trim())return"API key is required."}});return re.isCancel(e)&&(re.cancel("Setup cancelled."),process.exit(0)),e.trim()}async function ne(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 te(e,"/versions")).json()}async function _t(e,t){try{return await(await te(e,`/changelog/${encodeURIComponent(t)}`)).text()}catch(r){if(r instanceof R&&r.status===404)return null;throw r}}async function ar(e,t){return await(await te(e,`/skill/${encodeURIComponent(t)}`)).json()}function dt(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 cr(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,desktop:e.desktop,credits:e.credits,revenueSharing:e.revenueSharing,socialProviders:e.socialProviders,aiTools:e.aiTools,currency:e.defaultCurrency}}async function Rt(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:"offline-test-token",licenseId:"offline-test-license-id"}:await(await te(e,"/license/sign",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function lr(e,t){return await(await te(e,"/license/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function Ot(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 pr(e,t,r){let n=await fetch(`${e}/license/inspect`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify(r)});if(!n.ok){let i=await n.json().catch(()=>null);throw new Error(i?.error??`Inspect endpoint returned ${n.status}`)}return await n.json()}var xt=new Set([".git","node_modules",".pnpm-store",".env",".env.test",".turbo",".nuxt",".output",".data","dist",".next",".svelte-kit",".wrangler",".devcontainer","playwright-report","test-results"]),Dt=new Set(["pnpm-lock.yaml"]);function Te(e){if(Ct(e))return!0;for(let t of e.split("/"))if(Dt.has(t))return!0;return!1}var Xn=new Set(["data","mksaas","references","scripts",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","README.md","TODO.md","OVERVIEW.md"]),Zn=["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 Ct(e){let t=e.split("/");for(let r of t)if(xt.has(r))return!0;if(Xn.has(t[0]))return!0;for(let r of Zn)if(e===r||e.startsWith(r+"/"))return!0;return!1}async function ut(e,t,r){await ei(r,{recursive:!0});let n=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(n){await dr(Qn(n),ur(r));return}let i=await te(e,`/template/${encodeURIComponent(t)}`);if(!i.body)throw new Error("Empty response body");let o=ti.fromWeb(i.body);await dr(o,ur(r))}function ur(e){return ri({cwd:e,strip:1,filter:t=>{let r=t.replace(/^[^/]+\//,"");return r?!Ct(r):!0},sync:!1})}import{readdir as ni,readFile as Nt,rm as fr,writeFile as Lt}from"fs/promises";import{join as Ie}from"path";var ii=["apps/web-nuxt/public/images/blog","apps/web-next/public/images/blog","packages/content/en/blog","packages/content/ro/blog"];async function gr(e){await Promise.all(ii.map(t=>fr(Ie(e,t),{recursive:!0,force:!0})))}var oi="packages/config/src/blog.ts";async function hr(e){let t=Ie(e,oi),r=await Nt(t,"utf-8"),n=mr(mr(r,"blogCategories",t),"blogAuthors",t);await Lt(t,n)}function mr(e,t,r){let n=new RegExp(`(export const ${t}\\b[^=]*=\\s*)\\[[\\s\\S]*?\\];`);if(!n.test(e))throw new Error(`emptyBlogConfig: could not find the \`export const ${t} = [...]\` declaration in ${r}. The boilerplate blog config may have been renamed or restructured; update the CLI strip in cleanup.ts.`);return e.replace(n,"$1[];")}async function yr(e){let t=Ie(e,"packages/i18n/translations"),r;try{r=await ni(t)}catch{return}for(let n of r){let i=Ie(t,n,"web.json"),o;try{o=await Nt(i,"utf-8")}catch{continue}let s=JSON.parse(o);!s.blog||s.blog.categories===void 0||(s.blog.categories={},await Lt(i,JSON.stringify(s,null," ")+`
10
- `))}}async function vr(e,t){t.includes("claude-code")||await fr(Ie(e,".claude"),{recursive:!0,force:!0})}async function Sr(e,t){let r=Ie(e,".claude","settings.json"),n;try{n=await Nt(r,"utf8")}catch{return}let i=JSON.parse(n);delete i.alwaysThinkingEnabled,delete i.enableAllProjectMcpServers,i.env&&(delete i.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS,Object.keys(i.env).length===0&&delete i.env),i.permissions?.allow&&(i.permissions.allow=i.permissions.allow.filter(o=>si(o,t))),await Lt(r,JSON.stringify(i,null," ")+`
11
- `)}function si(e,t){return!(e.startsWith("mcp__")||t!=="nuxt"&&e.includes("nuxt"))}import{join as Er}from"path";import{mkdir as ai,readdir as ci,rm as li,rmdir as pi,writeFile as di}from"fs/promises";import{dirname as mt,join as ui,relative as mi,sep as fi}from"path";function ge(e){return e.split(fi).join("/")}async function ft(e){await ai(e,{recursive:!0})}async function l(e,t){await ft(mt(e)),await di(e,t,"utf-8")}async function $t(e,t){await li(e,{force:!0});let r=mt(e);for(;r!==t&&r!==mt(r);){try{await pi(r)}catch{return}r=mt(r)}}async function he(e,t,r){let n=[],i=await ci(e,{withFileTypes:!0});for(let o of i){let s=ui(e,o.name),a=ge(mi(t,s));r(a)||(o.isDirectory()?n.push(...await he(s,t,r)):o.isFile()&&n.push(s))}return n}var gi={postgres:"Postgres",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"},hi={resend:"Resend",ses:"Amazon SES",smtp:"SMTP"},yi={redis:"Redis",upstash:"Upstash Redis"},vi={node:"Node.js / Docker",vercel:"Vercel"},Si={stripe:"Stripe",polar:"Polar"};function Ei(e){let t=e.frontend==="nuxt",r=t?"Nuxt 4":"Next.js 16",n=t?"apps/web-nuxt":"apps/web-next",i=t?"`app/` pages + components + composables":"`app/` routes, `components/`, `lib/`",o=e.architecture==="fullstack",s=o?"(fullstack - Hono API mounted inside the app)":"(separate - standalone Hono backend)",a=o?`Mounted inside \`${n}\`. A standalone \`apps/backend\` is also kept (inert) so you can switch to separate later.`:"Runs from `apps/backend`; the frontend reaches it over HTTP.",d=yi[e.cacheProvider],h=vi[e.deploymentTarget],y=e.paymentProvider==="none"?"":`
12
- - **Payments:** ${Si[e.paymentProvider]}`,f=t?"`$t('key')` in templates (global helper); `useI18n()` from `vue-i18n` in `<script setup>` for `locale`/`setLocale`/`t()`.":"`useTranslations()` from `next-intl` in components; messages are loaded in `apps/web-next/i18n/request.ts`.",S=t?"**Navigation:** always use `localePath()` for paths (hardcoded paths break non-default locales); `await navigateTo()` in SSR.":"**Navigation:** `next/link` for links; `redirect()` from `next/navigation` for programmatic redirects in Server Components.";return`# AGENTS.md
2
+ import{Command as Fs}from"commander";import*as Jn from"@clack/prompts";import{existsSync as as,readdirSync as cs,rmSync as ls}from"fs";import{join as ps,resolve as ds}from"path";import{Option as B}from"commander";import*as S from"@clack/prompts";import*as Xe from"@clack/prompts";import Rt from"picocolors";function nr(e){let t=e?` GenerateSaaS v${e} `:" GenerateSaaS ";Xe.intro(Rt.bgYellow(Rt.black(t)))}function or(){Xe.outro(Rt.yellow("Happy building!"))}import*as u from"@clack/prompts";import f from"picocolors";var Ue={nextjs:{label:"Next.js",hint:"React 19 + Next.js 16"},nuxt:{label:"Nuxt",hint:"Vue 3 + Nuxt 4"}},je={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)"}},Ze={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"}},et={user:{label:"Per user",hint:"each user has their own subscription"},organization:{label:"Per organization",hint:"org subscription shared by members"}},ke={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}},Ve={"claude-code":{label:"Claude Code"},cursor:{label:"Cursor"},codex:{label:"Codex"},"gemini-cli":{label:"Gemini CLI"},windsurf:{label:"Windsurf"}};var pe={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 H={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}},$={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"}]}},Y={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"}]}},Me=[{target:"vercel",provider:"redis",reason:"Vercel serverless cannot maintain persistent Redis connections. Consider Upstash."}],Fe=[{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)."}],ir=["Local file storage (sharp, geoip-lite)","SMTP email (use Resend or SES instead)","Content API git integration"];function tt(e){let t=$[e.databaseProvider].managed,r=Y[e.cacheProvider].managed;return e.dockerServices.some(n=>!(n==="postgres"&&t||n==="redis"&&r))}var q={google:{label:"Google",envVars:[{name:"GOOGLE_CLIENT_ID",secret:!1},{name:"GOOGLE_CLIENT_SECRET",secret:!0}]},github:{label:"GitHub",envVars:[{name:"GITHUB_CLIENT_ID",secret:!1},{name:"GITHUB_CLIENT_SECRET",secret:!0}]},facebook:{label:"Facebook",envVars:[{name:"FACEBOOK_CLIENT_ID",secret:!1},{name:"FACEBOOK_CLIENT_SECRET",secret:!0}]},discord:{label:"Discord",envVars:[{name:"DISCORD_CLIENT_ID",secret:!1},{name:"DISCORD_CLIENT_SECRET",secret:!0}]},x:{label:"X",envVars:[{name:"TWITTER_CLIENT_ID",secret:!1},{name:"TWITTER_CLIENT_SECRET",secret:!0}]}};var Be=["nextjs","nuxt"],rt=["fullstack","separate"],nt=["stripe","polar","none"],ot=["smtp","ses","resend"],it=["postgres","redis","inngest","mailpit"],st=["claude-code","cursor","codex","gemini-cli","windsurf"],at=["user","organization"],de=["USD","EUR","GBP","CAD","AUD","BRL","JPY"],ue=["node","vercel"],me=["postgres","neon","supabase"],fe=["redis","upstash"],ct=["google","github","facebook","discord","x"];function lt(e){return e.split("-").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function pt(e){return/^[a-z][a-z0-9-]*$/.test(e)}function I(e){return e instanceof Error?e.message:String(e)}function w(e){u.isCancel(e)&&(u.cancel("Setup cancelled."),process.exit(0))}function Ot(e){let t=[];e.databaseProvider==="neon"&&t.push({key:"DATABASE_URL",message:"Neon connection string (optional):",placeholder:"postgres://...",secret:!0}),e.databaseProvider==="supabase"&&t.push({key:"DATABASE_URL",message:"Supabase connection string (optional):",placeholder:"postgres://...",secret:!0}),e.cacheProvider==="upstash"&&(t.push({key:"UPSTASH_REDIS_REST_URL",message:"Upstash REST URL (optional):",placeholder:"https://...",secret:!1}),t.push({key:"UPSTASH_REDIS_REST_TOKEN",message:"Upstash REST token (optional):",secret:!0})),e.paymentProvider==="stripe"&&(t.push({key:"STRIPE_SECRET_KEY",message:"Stripe secret key (optional):",placeholder:"sk_test_...",secret:!0}),t.push({key:"STRIPE_WEBHOOK_SECRET",message:"Stripe webhook secret (optional):",placeholder:"whsec_...",secret:!0})),e.paymentProvider==="polar"&&(t.push({key:"POLAR_ACCESS_TOKEN",message:"Polar access token (optional):",secret:!0}),t.push({key:"POLAR_WEBHOOK_SECRET",message:"Polar webhook secret (optional):",secret:!0})),e.emailProvider==="resend"&&t.push({key:"RESEND_API_KEY",message:"Resend API key (optional):",placeholder:"re_...",secret:!0}),e.emailProvider==="ses"&&(t.push({key:"AMAZON_SES_REGION",message:"Amazon SES region (optional):",placeholder:"us-east-1",secret:!1}),t.push({key:"AMAZON_SES_KEY",message:"Amazon SES access key (optional):",secret:!0}),t.push({key:"AMAZON_SES_SECRET",message:"Amazon SES secret (optional):",secret:!0}));for(let r of e.socialProviders){let n=q[r];for(let o of n.envVars)t.push({key:o.name,message:`${o.name} (${n.label}, optional):`,secret:o.secret})}return e.demo&&t.push({key:"TURNSTILE_SECRET_KEY",message:"Cloudflare Turnstile secret key (optional):",secret:!0}),t}async function sr(e,t){let r=!1;u.log.info(f.bold("Project"));let n=e?.projectName??await(async()=>{r=!0;let c=await u.text({message:"Project name:",placeholder:"my-saas",validate:p=>{if(!p?.trim())return"Project name is required.";if(!pt(p))return"Use lowercase letters, numbers, and hyphens only. Must start with a letter."}});return w(c),c})(),o=e?.appName??await(async()=>{r=!0;let c=await u.text({message:"App name:",initialValue:lt(n),validate:p=>{if(!p?.trim())return"App name is required."}});return w(c),c})(),i=e?.projectDir??await(async()=>{r=!0;let c=await u.text({message:"Project location:",initialValue:`./${n}`});return w(c),c==="."?process.cwd():c})(),s=e?.frontend??await(async()=>{r=!0;let c=Object.keys(Ue),p=await u.select({message:"Frontend framework:",options:c.map(v=>({value:v,label:Ue[v].label,hint:Ue[v].hint}))});return w(p),p})();u.log.info(f.bold("Infrastructure"));let a=e?.deploymentTarget??"node";if(e?.deploymentTarget===void 0){r=!0;let c=await u.select({message:"Deployment target:",options:ue.map(p=>({value:p,label:H[p].label,hint:H[p].hint}))});w(c),a=c}let d=e?.architecture?Fe.find(c=>c.architecture===e.architecture&&c.target===a):void 0;if(d)throw new Error(`Incompatible: --architecture ${d.architecture} + --deploy ${d.target}. ${d.reason}`);let g=new Set(Fe.filter(c=>c.target===a).map(c=>c.architecture)),h=rt.filter(c=>!g.has(c)),m=e?.architecture??await(async()=>{if(h.length===1){let p=h[0];return u.log.info(`Auto-selected ${je[p].label} architecture (only compatible option for ${H[a].label}).`),p}r=!0;let c=await u.select({message:"Architecture:",options:h.map(p=>({value:p,label:je[p].label,hint:je[p].hint}))});return w(c),c})(),_=e?.databaseProvider??await(async()=>{r=!0;let c=me.filter(v=>!Me.some(N=>N.target===a&&N.provider===v));if(c.length===1){let v=c[0];return u.log.info(`Auto-selected ${$[v].label} (only compatible option for ${H[a].label}).`),v}let p=await u.select({message:"Database provider:",options:c.map(v=>({value:v,label:$[v].label,hint:$[v].hint}))});return w(p),p})(),E=e?.cacheProvider??await(async()=>{r=!0;let c=fe.filter(v=>!Me.some(N=>N.target===a&&N.provider===v));if(c.length===1){let v=c[0];return u.log.info(`Auto-selected ${Y[v].label} (only compatible option for ${H[a].label}).`),v}let p=await u.select({message:"Cache provider:",options:c.map(v=>({value:v,label:Y[v].label,hint:Y[v].hint}))});return w(p),p})();if(H[a]?.edgeRuntime){let c=ir.map(p=>` - ${p}`).join(`
3
+ `);u.note(c,"Unavailable on edge runtime")}u.log.info(f.bold("Features"));let R=e?.paymentProvider??await(async()=>{r=!0;let c=await u.select({message:"Payment provider:",options:nt.map(p=>({value:p,label:Ze[p].label,hint:Ze[p].hint}))});return w(c),c})(),z=e?.defaultCurrency??await(async()=>{if(R==="none")return"USD";r=!0;let c=await u.select({message:"Default currency:",options:de.map(p=>({value:p,label:p,hint:pe[p].name}))});return w(c),c})(),D=e?.emailProvider??await(async()=>{r=!0;let c=await u.select({message:"Email provider:",options:ot.map(p=>({value:p,label:Qe[p].label,hint:Qe[p].hint}))});return w(c),c})(),ae=e?.multiTenancy??await(async()=>{r=!0;let c=await u.confirm({message:"Enable multi-tenancy (organizations)?",initialValue:!1});return w(c),c})(),ee=e?.billingScope??"user";if(ae&&e?.billingScope===void 0){r=!0;let c=await u.select({message:"Billing scope:",options:at.map(p=>({value:p,label:et[p].label,hint:et[p].hint}))});w(c),ee=c}let b=e?.blog??await(async()=>{r=!0;let c=await u.confirm({message:"Enable blog?",initialValue:!0});return w(c),c})(),y=e?.docs??await(async()=>{r=!0;let c=await u.confirm({message:"Include docs app? (self-hosted Fumadocs documentation site)",initialValue:!1});return w(c),c})(),O=t?.desktopAllowed!==!1,V=e?.desktop??(O?await(async()=>{r=!0;let c=await u.confirm({message:"Include the Electron desktop app? (apps/desktop - cross-platform, device-auth)",initialValue:!1});return w(c),c})():(u.log.info(f.dim("Desktop app: available on the Pro plan and up - skipped.")),!1)),ce=V?e?.desktopAutoRelease??await(async()=>{r=!0;let c=await u.select({message:"Desktop release trigger:",options:[{value:!1,label:"Manual only",hint:"run from the Actions tab (saves CI minutes)"},{value:!0,label:"Automatic",hint:"release on every CI success on main"}],initialValue:!1});return w(c),c})():!1,te=e?.revenueSharing??await(async()=>{r=!0;let c=await u.confirm({message:"Enable revenue sharing? (opt-in MRR leaderboard with dofollow backlinks)",initialValue:!1});return w(c),c})(),Le=R==="none"?!1:e?.credits??await(async()=>{r=!0;let c=await u.confirm({message:"Enable credits? (metered usage on top of subscription plans)",initialValue:!0});return w(c),c})(),We=e?.socialProviders??await(async()=>{r=!0;let c=ct.map(v=>({value:v,label:q[v].label,hint:`requires ${q[v].envVars.map(N=>N.name).join(" / ")}`})),p=await u.multiselect({message:"Which social login providers should the sign-in screen show?",options:c,initialValues:[],required:!1});return w(p),p})();u.log.info(f.bold("Tooling"));let Tt=e?.dockerServices??await(async()=>{r=!0;let c=[...it].filter(L=>L!=="mailpit");D==="smtp"&&c.push("mailpit");let p=c.map(L=>({value:L,label:ke[L].label,hint:ke[L].hint})),v=p.map(L=>L.value).filter(L=>!(L==="postgres"&&(_==="neon"||_==="supabase")||L==="redis"&&E==="upstash")),N=await u.multiselect({message:"Which services should we set up in Docker for you?",options:p,initialValues:v,required:!1});return w(N),N})(),Pt=e?.aiTools??await(async()=>{r=!0;let c=st.map(v=>({value:v,label:Ve[v].label})),p=await u.multiselect({message:"Which AI coding tools do you use?",options:c,initialValues:[],required:!1});return w(p),p})(),_t=e?.demo,qe=Ot({databaseProvider:_,cacheProvider:E,paymentProvider:R,emailProvider:D,socialProviders:We,demo:_t}),$e={};if(qe.length>0&&r){u.log.info(f.bold("Credentials")+f.dim(" all optional - press Enter to skip, fill in .env later"));for(let c of qe)if(r=!0,c.secret){let p=await u.password({message:c.message,mask:"*"});w(p),typeof p=="string"&&p.trim()&&($e[c.key]=p.trim())}else{let p=await u.text({message:c.message,placeholder:c.placeholder});w(p),typeof p=="string"&&p.trim()&&($e[c.key]=p.trim())}}if(r){let c=[` Name: ${f.cyan(n)}`,` App name: ${f.cyan(o)}`,` Location: ${f.cyan(i)}`,` Frontend: ${f.cyan(Ue[s].label)}`,` Architecture: ${f.cyan(je[m].label)}`].join(`
4
+ `),p=[` Deploy target: ${f.cyan(H[a]?.label??"Node.js / Docker")}`,` Database: ${f.cyan($[_].label)}`,` Cache: ${f.cyan(Y[E].label)}`,Tt.length>0?` Docker: ${f.cyan(Tt.map(le=>ke[le].label).join(", "))}`:` Docker: ${f.dim("none")}`].filter(Boolean).join(`
5
+ `),v=[R!=="none"?` Payment: ${f.cyan(Ze[R].label)} (${z})`:` Payment: ${f.dim("none")}`,` Credits: ${Le?f.cyan("Yes"):f.dim("No")}`,` Email: ${f.cyan(Qe[D].label)}`,` Multi-tenancy: ${ae?f.cyan("Yes")+` (billing: ${et[ee].label})`:f.dim("No")}`,` Blog: ${b?f.cyan("Yes"):f.dim("No")}`,` Docs app: ${y?f.cyan("Yes"):f.dim("No")}`,` Desktop app: ${V?f.cyan("Yes"):f.dim("No")}`,...V?[` Releases: ${f.cyan(ce?"Automatic on CI":"Manual only")}`]:[],` Rev. sharing: ${te?f.cyan("Yes"):f.dim("No")}`,We.length>0?` Social login: ${f.cyan(We.map(le=>q[le].label).join(", "))}`:` Social login: ${f.dim("none")}`,Pt.length>0?` AI tools: ${f.cyan(Pt.map(le=>Ve[le].label).join(", "))}`:` AI tools: ${f.dim("none")}`].join(`
6
+ `),N=[f.bold("Project"),c,"",f.bold("Infrastructure"),p,"",f.bold("Features"),v];if(qe.length>0){let le=qe.map(rr=>{let Wn=$e[rr.key]?f.green("provided"):f.dim("skipped");return` ${rr.key}: ${Wn}`}).join(`
7
+ `);N.push("",f.bold("Credentials"),le)}u.note(N.join(`
8
+ `),"Summary");let L=await u.confirm({message:"Proceed with these settings?"});(u.isCancel(L)||!L)&&(u.cancel("Setup cancelled."),process.exit(0))}return{projectName:n,appName:o,projectDir:i,frontend:s,architecture:m,deploymentTarget:a,databaseProvider:_,cacheProvider:E,paymentProvider:R,emailProvider:D,multiTenancy:ae,billingScope:ee,blog:b,docs:y,desktop:V,desktopAutoRelease:ce,revenueSharing:te,credits:Le,dockerServices:Tt,aiTools:Pt,socialProviders:We,defaultCurrency:z,...Object.keys($e).length>0?{credentials:$e}:{},...e?.baseUrl!==void 0?{baseUrl:e.baseUrl}:{},..._t!==void 0?{demo:_t}:{}}}import{createReadStream as oo}from"fs";import{mkdir as io}from"fs/promises";import{Readable as so}from"stream";import{pipeline as fr}from"stream/promises";import{extract as ao}from"tar";import{join as ge}from"path";import{homedir as qn}from"os";var Ge=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",M=".generatesaas",re=ge(M,"manifest.json"),ar=ge(M,"hashes.json"),dt=ge(M,"template-hashes.json"),ut=ge(M,"template"),cr=ge(M,"staging"),lr=ge(M,"staging.json"),X=ge(qn(),".generatesaas");var x=class extends Error{constructor(r,n,o){super(n);this.status=r;this.body=o}status;body;name="ApiError"};function Z(e){return{apiKey:e,baseUrl:Ge}}async function ne(e,t,r){let n=`${e.baseUrl}${t}`,o=await fetch(n,{...r,headers:{...r?.headers,Authorization:`Bearer ${e.apiKey}`,"User-Agent":"generatesaas-cli"}});if(!o.ok){let i,s;try{s=await o.json(),i=s.error??`API ${o.status}: ${t}`}catch{i=`API ${o.status}: ${t}`}throw new x(o.status,i,s)}return o}import{existsSync as Xn,readFileSync as Zn,writeFileSync as Qn,mkdirSync as eo}from"fs";import{dirname as to}from"path";import*as oe from"@clack/prompts";function Ke(){if(!Xn(X))return null;try{let e=JSON.parse(Zn(X,"utf-8"));return e.apiKey?e.apiKey:(e.token&&!e.apiKey&&oe.log.warning(`Found old GitHub token in ${X}. Run 'generatesaas init' to set up your API key.`),null)}catch{return null}}function he(e){eo(to(X),{recursive:!0}),Qn(X,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=Ke();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 ze()}async function ze(){let e=await oe.text({message:"Enter your GenerateSaaS API key:",placeholder:"gs_live_...",validate:t=>{if(!t?.trim())return"API key is required."}});return oe.isCancel(e)&&(oe.cancel("Setup cancelled."),process.exit(0)),e.trim()}async function ie(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}],entitlements:null}:await(await ne(e,"/versions")).json()}async function xt(e,t){try{return await(await ne(e,`/changelog/${encodeURIComponent(t)}`)).text()}catch(r){if(r instanceof x&&r.status===404)return null;throw r}}async function pr(e,t){return await(await ne(e,`/skill/${encodeURIComponent(t)}`)).json()}function mt(e){if(!(e instanceof x)||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 dr(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,desktop:e.desktop,credits:e.credits,revenueSharing:e.revenueSharing,socialProviders:e.socialProviders,aiTools:e.aiTools,currency:e.defaultCurrency}}async function Dt(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:"offline-test-token",licenseId:"offline-test-license-id"}:await(await ne(e,"/license/sign",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function ur(e,t){return await(await ne(e,"/license/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function Ct(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 mr(e,t,r){let n=await fetch(`${e}/license/inspect`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify(r)});if(!n.ok){let o=await n.json().catch(()=>null);throw new Error(o?.error??`Inspect endpoint returned ${n.status}`)}return await n.json()}var Nt=new Set([".git","node_modules",".pnpm-store",".env",".env.test",".turbo",".nuxt",".output",".data","dist",".next",".svelte-kit",".wrangler",".devcontainer","playwright-report","test-results"]),Lt=new Set(["pnpm-lock.yaml"]);function Ie(e){if($t(e))return!0;for(let t of e.split("/"))if(Lt.has(t))return!0;return!1}var ro=new Set(["data","mksaas","references","scripts",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","README.md","TODO.md","OVERVIEW.md"]),no=["docs/superpowers","packages/cli","packages/cli-api","infra/docker-compose.yml",".claude/commands",".claude/skills/web-next-port",".claude/skills/web-next-port-workspace",".claude/settings.local.json",".claude/worktrees"];function $t(e){let t=e.split("/");for(let r of t)if(Nt.has(r))return!0;if(ro.has(t[0]))return!0;for(let r of no)if(e===r||e.startsWith(r+"/"))return!0;return!1}async function ft(e,t,r){await io(r,{recursive:!0});let n=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(n){await fr(oo(n),gr(r));return}let o=await ne(e,`/template/${encodeURIComponent(t)}`);if(!o.body)throw new Error("Empty response body");let i=so.fromWeb(o.body);await fr(i,gr(r))}function gr(e){return ao({cwd:e,strip:1,filter:t=>{let r=t.replace(/^[^/]+\//,"");return r?!$t(r):!0},sync:!1})}import{readdir as co,readFile as Ut,rm as yr,writeFile as jt}from"fs/promises";import{join as Te}from"path";var lo=["apps/web-nuxt/public/images/blog","apps/web-next/public/images/blog","packages/content/en/blog","packages/content/ro/blog"];async function vr(e){await Promise.all(lo.map(t=>yr(Te(e,t),{recursive:!0,force:!0})))}var po="packages/config/src/blog.ts";async function Sr(e){let t=Te(e,po),r=await Ut(t,"utf-8"),n=hr(hr(r,"blogCategories",t),"blogAuthors",t);await jt(t,n)}function hr(e,t,r){let n=new RegExp(`(export const ${t}\\b[^=]*=\\s*)\\[[\\s\\S]*?\\];`);if(!n.test(e))throw new Error(`emptyBlogConfig: could not find the \`export const ${t} = [...]\` declaration in ${r}. The boilerplate blog config may have been renamed or restructured; update the CLI strip in cleanup.ts.`);return e.replace(n,"$1[];")}async function Er(e){let t=Te(e,"packages/i18n/translations"),r;try{r=await co(t)}catch{return}for(let n of r){let o=Te(t,n,"web.json"),i;try{i=await Ut(o,"utf-8")}catch{continue}let s=JSON.parse(i);!s.blog||s.blog.categories===void 0||(s.blog.categories={},await jt(o,JSON.stringify(s,null," ")+`
10
+ `))}}async function wr(e,t){t.includes("claude-code")||await yr(Te(e,".claude"),{recursive:!0,force:!0})}async function br(e,t){let r=Te(e,".claude","settings.json"),n;try{n=await Ut(r,"utf8")}catch{return}let o=JSON.parse(n);delete o.alwaysThinkingEnabled,delete o.enableAllProjectMcpServers,o.env&&(delete o.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS,Object.keys(o.env).length===0&&delete o.env),o.permissions?.allow&&(o.permissions.allow=o.permissions.allow.filter(i=>uo(i,t))),await jt(r,JSON.stringify(o,null," ")+`
11
+ `)}function uo(e,t){return!(e.startsWith("mcp__")||t!=="nuxt"&&e.includes("nuxt"))}import{join as kr}from"path";import{mkdir as mo,readdir as fo,rm as go,rmdir as ho,writeFile as yo}from"fs/promises";import{dirname as gt,join as vo,relative as So,sep as Eo}from"path";function ye(e){return e.split(Eo).join("/")}async function ht(e){await mo(e,{recursive:!0})}async function l(e,t){await ht(gt(e)),await yo(e,t,"utf-8")}async function Vt(e,t){await go(e,{force:!0});let r=gt(e);for(;r!==t&&r!==gt(r);){try{await ho(r)}catch{return}r=gt(r)}}async function ve(e,t,r){let n=[],o=await fo(e,{withFileTypes:!0});for(let i of o){let s=vo(e,i.name),a=ye(So(t,s));r(a)||(i.isDirectory()?n.push(...await ve(s,t,r)):i.isFile()&&n.push(s))}return n}var wo={postgres:"Postgres",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"},bo={resend:"Resend",ses:"Amazon SES",smtp:"SMTP"},ko={redis:"Redis",upstash:"Upstash Redis"},Ao={node:"Node.js / Docker",vercel:"Vercel"},Io={stripe:"Stripe",polar:"Polar"};function To(e){let t=e.frontend==="nuxt",r=t?"Nuxt 4":"Next.js 16",n=t?"apps/web-nuxt":"apps/web-next",o=t?"`app/` pages + components + composables":"`app/` routes, `components/`, `lib/`",i=e.architecture==="fullstack",s=i?"(fullstack - Hono API mounted inside the app)":"(separate - standalone Hono backend)",a=i?`Mounted inside \`${n}\`. A standalone \`apps/backend\` is also kept (inert) so you can switch to separate later.`:"Runs from `apps/backend`; the frontend reaches it over HTTP.",d=ko[e.cacheProvider],g=Ao[e.deploymentTarget],h=e.paymentProvider==="none"?"":`
12
+ - **Payments:** ${Io[e.paymentProvider]}`,m=t?"`$t('key')` in templates (global helper); `useI18n()` from `vue-i18n` in `<script setup>` for `locale`/`setLocale`/`t()`.":"`useTranslations()` from `next-intl` in components; messages are loaded in `apps/web-next/i18n/request.ts`.",_=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
13
13
 
14
14
  Guidelines for AI coding agents (Claude Code, Codex, Cursor, \u2026) in this project.
15
15
  Keep this file tight: add a rule only when it prevents a recurring mistake - too
@@ -37,11 +37,11 @@ flag, route, or translation is the most common and most costly mistake in this r
37
37
 
38
38
  - **Frontend:** ${r} ${s}
39
39
  - **API:** Hono, RPC-typed. ${a}
40
- - **Database:** Drizzle ORM + ${gi[e.databaseProvider]}
40
+ - **Database:** Drizzle ORM + ${wo[e.databaseProvider]}
41
41
  - **Cache + jobs:** ${d} + Inngest
42
- - **Auth:** Better Auth${y}
43
- - **Email:** ${hi[e.emailProvider]}
44
- - **Deploy:** ${h}
42
+ - **Auth:** Better Auth${h}
43
+ - **Email:** ${bo[e.emailProvider]}
44
+ - **Deploy:** ${g}
45
45
 
46
46
  ## Where things live (extend these - don't reinvent)
47
47
 
@@ -51,7 +51,7 @@ flag, route, or translation is the most common and most costly mistake in this r
51
51
  - \`packages/auth/src/config.ts\` - Better Auth config.
52
52
  - \`packages/runtime/src/env.ts\` - the validated (Zod) env schema. New env var \u2192 add it here **and** to \`.env.example\` (the committed reference); set the local value in \`.env\`.
53
53
  - \`packages/{payments,mail,sms,storage,notifications}\` - config-gated integrations. Every provider's files stay even when its feature is off, so flipping a flag is enough to enable it.
54
- - \`${n}\` - the ${r} app (${i}).
54
+ - \`${n}\` - the ${r} app (${o}).
55
55
  - \`docs/${t?"nuxt":"next"}/\` - feature & architecture documentation for this stack (Markdown). Consult it before searching from scratch; it cites real config keys, package names, and file paths you can act on.
56
56
 
57
57
  ## Code style
@@ -69,8 +69,8 @@ flag, route, or translation is the most common and most costly mistake in this r
69
69
 
70
70
  - **Hono routes:** keep the method chain (\`.get().post()\`) - RPC type inference depends on it. Validate with \`sValidator\` from \`@hono/standard-validator\`.
71
71
  - **Feature flags:** respect \`config.*\` - hide/skip a feature when its flag is off (the files stay so you can flip it later).
72
- - **i18n:** strings live in \`packages/i18n/translations/{locale}/{scope}.json\`; edit \`en/\` only, keep keys generic. ${f} A pre-commit hook runs \`pnpm translate\` (needs \`OPENROUTER_API_KEY\`) to sync other locales from \`en\`; without the key it skips.
73
- - ${S}
72
+ - **i18n:** strings live in \`packages/i18n/translations/{locale}/{scope}.json\`; edit \`en/\` only, keep keys generic. ${m} A pre-commit hook runs \`pnpm translate\` (needs \`OPENROUTER_API_KEY\`) to sync other locales from \`en\`; without the key it skips.
73
+ - ${_}
74
74
  - **Routes:** use \`config.routes.*\`, never hardcoded path strings.
75
75
  - **Secrets:** never send them to an external service; generate tokens/QR codes client-side.
76
76
 
@@ -81,14 +81,14 @@ In \`@repo/*\` backend packages prefer web-standard APIs: \`crypto.randomUUID()\
81
81
  ## This project
82
82
 
83
83
  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\`.
84
- `}async function wr(e){await l(Er(e.projectDir,"AGENTS.md"),Ei(e)),await l(Er(e.projectDir,"CLAUDE.md"),`@AGENTS.md
85
- `)}import{join as wi}from"path";var bi={postgres:"Postgres (self-hosted)",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"};function ki(e){let t=e.appName.trim()||e.projectName,r=e.frontend==="nuxt",n=r?"Nuxt 4":"Next.js 16",i=r?"apps/web-nuxt":"apps/web-next",o=bi[e.databaseProvider],s=Qe(e),a=e.architecture==="fullstack"?`${n} app at \`${i}/\` with the Hono API mounted inside it. A standalone \`apps/backend/\` is also included (inert in this fullstack setup) so you can split the API into a separate service later without re-scaffolding.`:`${n} app at \`${i}/\` and a separate Hono backend at \`apps/backend/\`.`,d=e.architecture==="fullstack"?`- App + API: http://localhost:3000
84
+ `}async function Ar(e){await l(kr(e.projectDir,"AGENTS.md"),To(e)),await l(kr(e.projectDir,"CLAUDE.md"),`@AGENTS.md
85
+ `)}import{join as Po}from"path";var _o={postgres:"Postgres (self-hosted)",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"};function Ro(e){let t=e.appName.trim()||e.projectName,r=e.frontend==="nuxt",n=r?"Nuxt 4":"Next.js 16",o=r?"apps/web-nuxt":"apps/web-next",i=_o[e.databaseProvider],s=tt(e),a=e.architecture==="fullstack"?`${n} app at \`${o}/\` with the Hono API mounted inside it. A standalone \`apps/backend/\` is also included (inert in this fullstack setup) so you can split the API into a separate service later without re-scaffolding.`:`${n} app at \`${o}/\` and a separate Hono backend at \`apps/backend/\`.`,d=e.architecture==="fullstack"?`- App + API: http://localhost:3000
86
86
  - Inngest dev server: http://127.0.0.1:8288`:`- App: http://localhost:3000
87
87
  - API: http://localhost:3010
88
- - Inngest dev server: http://127.0.0.1:8288`,h=s?`pnpm infra # optional: starts local Docker services (Postgres / Redis / Inngest / Mailpit)
89
- `:"",y=e.deploymentTarget==="vercel"?"### Deployment\n\nDeploy to Vercel: `vercel deploy` (or connect the repo in the Vercel dashboard). Required environment variables are listed in `.env`.":`### Deployment
88
+ - Inngest dev server: http://127.0.0.1:8288`,g=s?`pnpm infra # optional: starts local Docker services (Postgres / Redis / Inngest / Mailpit)
89
+ `:"",h=e.deploymentTarget==="vercel"?"### Deployment\n\nDeploy to Vercel: `vercel deploy` (or connect the repo in the Vercel dashboard). Required environment variables are listed in `.env`.":`### Deployment
90
90
 
91
- This project ships with Dockerfiles for each app. Build images with \`docker build\` and deploy to your runtime of choice (Render / Fly.io / Railway / Coolify / Dokploy / your own VPS).${s?"\n\nThe `infra/` directory ships a Docker Compose file for the local-only services (Postgres / Redis / Inngest / Mailpit, filtered by your provider choices).":""}`,f=e.aiTools.length>0?"To pull the latest boilerplate changes into this project, open it in your AI coding agent and ask: `update my GenerateSaaS project`.":"To pull the latest boilerplate changes into this project, install an AI coding agent (Claude Code / Cursor / Codex / Gemini CLI / Windsurf) and ask: `update my GenerateSaaS project`. The skill bundle that drives the update lives under each tool's skill root.";return`# ${t}
91
+ This project ships with Dockerfiles for each app. Build images with \`docker build\` and deploy to your runtime of choice (Render / Fly.io / Railway / Coolify / Dokploy / your own VPS).${s?"\n\nThe `infra/` directory ships a Docker Compose file for the local-only services (Postgres / Redis / Inngest / Mailpit, filtered by your provider choices).":""}`,m=e.aiTools.length>0?"To pull the latest boilerplate changes into this project, open it in your AI coding agent and ask: `update my GenerateSaaS project`.":"To pull the latest boilerplate changes into this project, install an AI coding agent (Claude Code / Cursor / Codex / Gemini CLI / Windsurf) and ask: `update my GenerateSaaS project`. The skill bundle that drives the update lives under each tool's skill root.";return`# ${t}
92
92
 
93
93
  ${a}
94
94
 
@@ -97,7 +97,7 @@ ${a}
97
97
  - **Frontend:** ${n}
98
98
  - **Backend:** Hono (TypeScript, RPC-typed)
99
99
  - **Auth:** Better Auth
100
- - **ORM / DB:** Drizzle + ${o}
100
+ - **ORM / DB:** Drizzle + ${i}
101
101
  - **Background jobs:** Inngest
102
102
  - **i18n:** ${r?"@nuxtjs/i18n":"next-intl"}
103
103
 
@@ -108,7 +108,7 @@ pnpm install
108
108
  # A ready-to-run \`.env\` was generated for you - open it and fill in the
109
109
  # values marked \`# TODO\` (provider API keys). \`.env.example\` is the committed
110
110
  # reference. All apps load the single root \`.env\`.
111
- ${h}pnpm dev
111
+ ${g}pnpm dev
112
112
  \`\`\`
113
113
 
114
114
  ${d}
@@ -130,7 +130,7 @@ pnpm -F @repo/database studio # open Drizzle Studio
130
130
  pnpm auth:generate # regenerate Better Auth schema after config changes
131
131
  \`\`\`
132
132
 
133
- ${y}
133
+ ${h}
134
134
 
135
135
  ## Removing the GenerateSaaS license
136
136
 
@@ -142,12 +142,12 @@ pnpm dlx generatesaas eject
142
142
 
143
143
  ## Updates
144
144
 
145
- ${f}
146
- `}async function br(e){await l(wi(e.projectDir,"README.md"),ki(e))}import{join as gt}from"path";function Ai(e){let t=e.split(".");return t.length>=3?t.slice(1).join("."):e}function Ti(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"app"}function Ii(e,t){let r=Ti(e);return{appId:`${t.split(".").reverse().join(".")}.${r.replace(/-/g,"")}`,productName:e,protocol:r}}async function kr(e){let t=e.appName.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),r=e.paymentProvider!=="none",n=e.baseUrl??"http://localhost:3000",i=e.baseUrl?new URL(e.baseUrl).hostname:"example.com",o=Ai(i),s=e.demo?"false":"true",a=e.demo?`
145
+ ${m}
146
+ `}async function Ir(e){await l(Po(e.projectDir,"README.md"),Ro(e))}import{join as yt}from"path";function Oo(e){let t=e.split(".");return t.length>=3?t.slice(1).join("."):e}function xo(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"app"}function Do(e,t){let r=xo(e);return{appId:`${t.split(".").reverse().join(".")}.${r.replace(/-/g,"")}`,productName:e,protocol:r}}async function Tr(e){let t=e.appName.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),r=e.paymentProvider!=="none",n=e.baseUrl??"http://localhost:3000",o=e.baseUrl?new URL(e.baseUrl).hostname:"example.com",i=Oo(o),s=e.demo?"false":"true",a=e.demo?`
147
147
  support: {
148
148
  enableInDev: true,
149
149
  crisp: { websiteId: "7e221cec-ed61-46b7-b1b4-8cbc16557cca" }
150
- },`:"",d=e.frontend==="nextjs"&&e.architecture==="fullstack"?"false":"true",h=Ii(t,o),y=`import type { AppConfig } from "@repo/config/types";
150
+ },`:"",d=e.frontend==="nextjs"&&e.architecture==="fullstack"?"false":"true",g=Do(t,i),h=`import type { AppConfig } from "@repo/config/types";
151
151
  import { desktopConfig } from "./desktop.mjs";
152
152
  import { tenancyConfig } from "./tenancy-flags.mjs";
153
153
 
@@ -156,7 +156,7 @@ const trustedOrigins = process.env.TRUSTED_ORIGINS?.split(",").map((s) => s.trim
156
156
  export const config: AppConfig = {
157
157
  siteName: "${t}",
158
158
  fullSiteName: "${t}",
159
- domain: "${i}",
159
+ domain: "${o}",
160
160
  baseUrl: process.env.BASE_URL ?? "${n}",
161
161
  indexable: ${s},
162
162
  logo: {
@@ -183,16 +183,16 @@ export const config: AppConfig = {
183
183
  limitPerSecond: 3,
184
184
  senders: {
185
185
  transactional: {
186
- email: "noreply@${o}",
186
+ email: "noreply@${i}",
187
187
  senderName: "${t}"
188
188
  },
189
189
  marketing: {
190
- email: "hello@${o}",
190
+ email: "hello@${i}",
191
191
  senderName: "${t}",
192
192
  signatureName: "Alex"
193
193
  },
194
194
  support: {
195
- email: "support@${o}",
195
+ email: "support@${i}",
196
196
  senderName: "${t} Support"
197
197
  }
198
198
  },
@@ -254,7 +254,7 @@ export const config: AppConfig = {
254
254
  }`:`{
255
255
  base: "${e.defaultCurrency}",
256
256
  list: [
257
- { symbol: "${ce[e.defaultCurrency].symbol}", name: "${ce[e.defaultCurrency].name}", code: "${e.defaultCurrency}", place: "${ce[e.defaultCurrency].place}", space: ${ce[e.defaultCurrency].space} }
257
+ { symbol: "${pe[e.defaultCurrency].symbol}", name: "${pe[e.defaultCurrency].name}", code: "${e.defaultCurrency}", place: "${pe[e.defaultCurrency].place}", space: ${pe[e.defaultCurrency].space} }
258
258
  ],
259
259
  countryMap: {
260
260
  default: "${e.defaultCurrency}"
@@ -276,6 +276,22 @@ export const config: AppConfig = {
276
276
  url: "/docs",
277
277
  contentDir: "content/docs"
278
278
  },
279
+ ai: {
280
+ enabled: false,
281
+ defaultModel: "claude-opus-4-8",
282
+ models: {
283
+ // EXAMPLE entry - set provider/modelId for your model and update the
284
+ // rates below to the provider's CURRENT pricing. These numbers are
285
+ // NOT maintained by the boilerplate.
286
+ "claude-opus-4-8": {
287
+ provider: "anthropic",
288
+ modelId: "claude-opus-4-8",
289
+ inputPerMTok: 5,
290
+ outputPerMTok: 25
291
+ }
292
+ },
293
+ credits: { creditsPerUsd: 100, margin: 1, fallbackRatePerMTok: { input: 5, output: 25 } }
294
+ },
279
295
  desktop: desktopConfig,
280
296
  seo: {
281
297
  description: "Production-ready SaaS application",
@@ -308,7 +324,7 @@ export * from "./pricing";
308
324
  export * from "./roles";
309
325
  export * from "./section-tabs";
310
326
  export * from "./tenancy";
311
- `,f=gt(e.projectDir,"packages/config/src/index.ts");await l(f,y),await l(gt(e.projectDir,"packages/config/src/desktop.mjs"),`// Plain-JS desktop app identity. NO env reads, so tooling that cannot import the
327
+ `,m=yt(e.projectDir,"packages/config/src/index.ts");await l(m,h),await l(yt(e.projectDir,"packages/config/src/desktop.mjs"),`// Plain-JS desktop app identity. NO env reads, so tooling that cannot import the
312
328
  // TypeScript config index reads it directly: the Electron renderer + main process
313
329
  // (electron-vite bundles @repo/config/desktop) and electron-builder (Node ESM).
314
330
  // Edit these values once to rebrand - they are the same in dev and production, so
@@ -317,13 +333,13 @@ export * from "./tenancy";
317
333
 
318
334
  export const desktopConfig = {
319
335
  enabled: ${e.desktop},
320
- appId: "${h.appId}",
321
- productName: "${h.productName}",
322
- protocol: "${h.protocol}",
336
+ appId: "${g.appId}",
337
+ productName: "${g.productName}",
338
+ protocol: "${g.protocol}",
323
339
  baseUrl: "${n}",
324
- autoUpdate: { provider: "generic", url: "https://cdn.${o}/desktop" }
340
+ autoUpdate: { provider: "generic", url: "https://cdn.${i}/desktop" }
325
341
  };
326
- `),await l(gt(e.projectDir,"packages/config/src/tenancy-flags.mjs"),`// Plain-JS tenancy flags. NO env reads, so tooling that cannot import the
342
+ `),await l(yt(e.projectDir,"packages/config/src/tenancy-flags.mjs"),`// Plain-JS tenancy flags. NO env reads, so tooling that cannot import the
327
343
  // TypeScript config index reads it directly (the Electron renderer). Mirrors the
328
344
  // desktop.mjs pattern. The config index re-exports this as config.tenancy.
329
345
 
@@ -332,11 +348,11 @@ export const tenancyConfig = ${e.multiTenancy?`{
332
348
  organizationLimit: 5,
333
349
  billingScope: "${e.billingScope}"
334
350
  }`:"{ multiTenant: false }"};
335
- `),e.desktop&&await l(gt(e.projectDir,"apps/desktop/dev-app-update.yml"),`provider: generic
336
- url: https://cdn.${o}/desktop
337
- updaterCacheDirName: ${h.protocol}-updater
338
- `)}import{join as Pi}from"path";function _i(e){return e==="stripe"?' stripePriceId: "",':' polarProductId: "",'}function jt(e){let t=[];return e.withCredits&&(t.push(` credits: ${e.credits},`),t.push(" creditInterval: 30,")),t.push(` apiRateLimit: { maxRequests: ${e.rateLimit} }`),t.join(`
339
- `)}async function Ar(e){let t=Pi(e.projectDir,"packages/config/src/pricing.ts"),r=e.defaultCurrency;if(e.paymentProvider==="none"){let f=`import type { PricingConfig } from "@repo/config/types";
351
+ `),e.desktop&&await l(yt(e.projectDir,"apps/desktop/dev-app-update.yml"),`provider: generic
352
+ url: https://cdn.${i}/desktop
353
+ updaterCacheDirName: ${g.protocol}-updater
354
+ `)}import{join as Co}from"path";function No(e){return e==="stripe"?' stripePriceId: "",':' polarProductId: "",'}function Mt(e){let t=[];return e.withCredits&&(t.push(` credits: ${e.credits},`),t.push(" creditInterval: 30,")),t.push(` apiRateLimit: { maxRequests: ${e.rateLimit} }`),t.join(`
355
+ `)}async function Pr(e){let t=Co(e.projectDir,"packages/config/src/pricing.ts"),r=e.defaultCurrency;if(e.paymentProvider==="none"){let m=`import type { PricingConfig } from "@repo/config/types";
340
356
 
341
357
  export const pricingConfig: PricingConfig = {
342
358
  defaultPlan: "free",
@@ -365,7 +381,7 @@ export const pricingConfig: PricingConfig = {
365
381
  credits: { enabled: false },
366
382
  products: { enabled: false, items: [] }
367
383
  };
368
- `;await l(t,f);return}let n=e.paymentProvider,i=_i(n),o=e.credits,s=jt({credits:5,rateLimit:100,withCredits:o}),a=jt({credits:10,rateLimit:1e3,withCredits:o}),d=jt({credits:50,rateLimit:5e3,withCredits:o}),y=`import type { PricingConfig } from "@repo/config/types";
384
+ `;await l(t,m);return}let n=e.paymentProvider,o=No(n),i=e.credits,s=Mt({credits:5,rateLimit:100,withCredits:i}),a=Mt({credits:10,rateLimit:1e3,withCredits:i}),d=Mt({credits:50,rateLimit:5e3,withCredits:i}),h=`import type { PricingConfig } from "@repo/config/types";
369
385
 
370
386
  export const pricingConfig: PricingConfig = {
371
387
  defaultPlan: "free",
@@ -404,12 +420,12 @@ ${s}
404
420
  ],
405
421
  prices: [
406
422
  {
407
- ${i}
423
+ ${o}
408
424
  interval: "month",
409
425
  amounts: { ${r}: 9 }
410
426
  },
411
427
  {
412
- ${i}
428
+ ${o}
413
429
  interval: "year",
414
430
  amounts: { ${r}: 90 },
415
431
  anchorAmounts: { ${r}: 109 }
@@ -433,14 +449,14 @@ ${a}
433
449
  ],
434
450
  prices: [
435
451
  {
436
- ${i}
452
+ ${o}
437
453
  interval: "month",
438
454
  amounts: { ${r}: 29 },
439
455
  anchorAmounts: { ${r}: 39 },
440
456
  featured: true
441
457
  },
442
458
  {
443
- ${i}
459
+ ${o}
444
460
  interval: "year",
445
461
  amounts: { ${r}: 290 },
446
462
  anchorAmounts: { ${r}: 349 }
@@ -449,17 +465,17 @@ ${i}
449
465
  ${d}
450
466
  }
451
467
  ],
452
- ${o?" credits: { enabled: true }":" credits: { enabled: false }"},
468
+ ${i?" credits: { enabled: true }":" credits: { enabled: false }"},
453
469
  products: {
454
470
  enabled: false,
455
471
  items: []
456
472
  }
457
473
  };
458
- `;await l(t,y)}var Ri={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 Oi(e){let t=W[e];return t.envVars.map((r,n)=>({key:r.name,...n===0?{comment:`# TODO: Add your ${t.label} OAuth credentials`}:{}}))}var xi={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 Pe(e,t){return t?e.map(r=>{let n=t[r.key];return n?{...r,defaultValue:n,comment:void 0}:r}):e}function _e(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 Ut(e){return Array.from(crypto.getRandomValues(new Uint8Array(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}function Ir(e){return e.architecture==="fullstack"?{apiUrl:"http://localhost:3000/api",baseUrl:"http://localhost:3000"}:{apiUrl:"http://localhost:3010",baseUrl:"http://localhost:3000"}}function Di(e){let{architecture:t,deploymentTarget:r}=e;return t==="fullstack"?r==="vercel"?{frontend:"https://your-app.vercel.app",backend:"https://your-app.vercel.app"}:r==="node"?{frontend:"https://your-app.example.com",backend:"https://your-app.example.com"}:null:{frontend:"https://your-app.example.com",backend:"https://your-app.example.com/api"}}function ht(e,t,r,n){e.push(n==="example"?`${t}=`:`${t}=${r}`)}function Tr(e,t){let r=t==="example"?void 0:e.credentials,{apiUrl:n,baseUrl:i}=Ir(e),o=[];e.architecture==="separate"?o.push("# API Configuration","# Standalone backend's own URL (the frontend reaches it via the public var above).",`API_URL=${n}`,`BASE_URL=${i}`):o.push("# App","# (API_URL is derived from the frontend's *_PUBLIC_API_URL by the runtime env schema.)",`BASE_URL=${i}`),o.push("","# Database"),_e(Pe(N[e.databaseProvider].envVars,r),o),o.push("","# Cache"),_e(Pe(z[e.cacheProvider].envVars,r),o),o.push("","# Authentication"),t==="example"&&o.push("# Generate a strong secret, e.g. `openssl rand -hex 32`"),ht(o,"BETTER_AUTH_SECRET",crypto.randomUUID(),t),o.push("","# Content API (random secret; required when contentApi feature is enabled in @repo/config)"),ht(o,"CONTENT_API_KEY",Ut(32),t),o.push("","# Job Queue - Inngest","INNGEST_APP_ID=api"),ht(o,"INNGEST_EVENT_KEY",Ut(32),t),ht(o,"INNGEST_SIGNING_KEY",Ut(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=Ri[e.emailProvider];if(s&&(o.push("","# Email"),_e(Pe(s,r),o)),e.paymentProvider!=="none"){let a=xi[e.paymentProvider];a&&(o.push("","# Payment"),_e(Pe(a,r),o))}if(e.socialProviders.length>0){o.push("","# Social auth");for(let a of e.socialProviders)_e(Pe(Oi(a),r),o)}return e.demo&&(o.push("","# Captcha (Cloudflare Turnstile)"),_e(Pe([{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(`
459
- `)}function Ci(e){let{apiUrl:t}=Ir(e),r=e.frontend==="nextjs"?"NEXT_PUBLIC_API_URL":"NUXT_PUBLIC_API_URL",n=["# API Configuration",`${r}=${t}`],i=Di(e);return i&&e.architecture==="separate"&&n.push("","# Production (uncomment and replace with your deployed hostnames):",`# ${r}=${i.backend}`),n.push(""),n.join(`
460
- `)}async function Pr(e){let t=Ci(e);await l(`${e.projectDir}/.env`,t+`
461
- `+Tr(e,"env")),await l(`${e.projectDir}/.env.example`,t+`
462
- `+Tr(e,"example"))}import{join as Ni}from"path";async function _r(e){let t=[...e.dockerServices];if(N[e.databaseProvider].managed&&(t=t.filter(o=>o!=="postgres")),z[e.cacheProvider].managed&&(t=t.filter(o=>o!=="redis")),t.length===0)return!1;let r=[],n=[];t.includes("postgres")&&(r.push(` postgres:
474
+ `;await l(t,h)}var Lo={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 $o(e){let t=q[e];return t.envVars.map((r,n)=>({key:r.name,...n===0?{comment:`# TODO: Add your ${t.label} OAuth credentials`}:{}}))}var Uo={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 Pe(e,t){return t?e.map(r=>{let n=t[r.key];return n?{...r,defaultValue:n,comment:void 0}:r}):e}function _e(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 Ft(e){return Array.from(crypto.getRandomValues(new Uint8Array(e))).map(t=>t.toString(16).padStart(2,"0")).join("")}function Rr(e){return e.architecture==="fullstack"?{apiUrl:"http://localhost:3000/api",baseUrl:"http://localhost:3000"}:{apiUrl:"http://localhost:3010",baseUrl:"http://localhost:3000"}}function jo(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 vt(e,t,r,n){e.push(n==="example"?`${t}=`:`${t}=${r}`)}function _r(e,t){let r=t==="example"?void 0:e.credentials,{apiUrl:n,baseUrl:o}=Rr(e),i=[];e.architecture==="separate"?i.push("# API Configuration","# Standalone backend's own URL (the frontend reaches it via the public var above).",`API_URL=${n}`,`BASE_URL=${o}`):i.push("# App","# (API_URL is derived from the frontend's *_PUBLIC_API_URL by the runtime env schema.)",`BASE_URL=${o}`),i.push("","# Database"),_e(Pe($[e.databaseProvider].envVars,r),i),i.push("","# Cache"),_e(Pe(Y[e.cacheProvider].envVars,r),i),i.push("","# Authentication"),t==="example"&&i.push("# Generate a strong secret, e.g. `openssl rand -hex 32`"),vt(i,"BETTER_AUTH_SECRET",crypto.randomUUID(),t),i.push("","# Content API (random secret; required when contentApi feature is enabled in @repo/config)"),vt(i,"CONTENT_API_KEY",Ft(32),t),i.push("","# Job Queue - Inngest","INNGEST_APP_ID=api"),vt(i,"INNGEST_EVENT_KEY",Ft(32),t),vt(i,"INNGEST_SIGNING_KEY",Ft(32),t),i.push("INNGEST_BASE_URL=http://127.0.0.1:8288"),e.architecture==="separate"&&(i.push("","# API Port (standalone backend)","API_PORT=3010"),i.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=Lo[e.emailProvider];if(s&&(i.push("","# Email"),_e(Pe(s,r),i)),e.paymentProvider!=="none"){let a=Uo[e.paymentProvider];a&&(i.push("","# Payment"),_e(Pe(a,r),i))}if(e.socialProviders.length>0){i.push("","# Social auth");for(let a of e.socialProviders)_e(Pe($o(a),r),i)}return e.demo&&(i.push("","# Captcha (Cloudflare Turnstile)"),_e(Pe([{key:"TURNSTILE_SECRET_KEY",comment:"# TODO: Add your Cloudflare Turnstile secret key"}],r),i)),i.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="),i.push("","# AI base - provider API keys (optional)","# Enable config.ai in packages/config and set the key for your chosen provider.","# OPENROUTER_API_KEY above also works as an AI provider key.","#ANTHROPIC_API_KEY=","#OPENAI_API_KEY="),i.push(""),i.join(`
475
+ `)}function Vo(e){let{apiUrl:t}=Rr(e),r=e.frontend==="nextjs"?"NEXT_PUBLIC_API_URL":"NUXT_PUBLIC_API_URL",n=["# API Configuration",`${r}=${t}`],o=jo(e);return o&&e.architecture==="separate"&&n.push("","# Production (uncomment and replace with your deployed hostnames):",`# ${r}=${o.backend}`),n.push(""),n.join(`
476
+ `)}async function Or(e){let t=Vo(e);await l(`${e.projectDir}/.env`,t+`
477
+ `+_r(e,"env")),await l(`${e.projectDir}/.env.example`,t+`
478
+ `+_r(e,"example"))}import{join as Mo}from"path";async function xr(e){let t=[...e.dockerServices];if($[e.databaseProvider].managed&&(t=t.filter(i=>i!=="postgres")),Y[e.cacheProvider].managed&&(t=t.filter(i=>i!=="redis")),t.length===0)return!1;let r=[],n=[];t.includes("postgres")&&(r.push(` postgres:
463
479
  image: postgres:18-alpine
464
480
  ports:
465
481
  - "\${POSTGRES_PORT:-5432}:5432"
@@ -495,16 +511,16 @@ ${o?" credits: { enabled: true }":" credits: { enabled: false }"},
495
511
  image: inngest/inngest:v1.17.4
496
512
  ports:
497
513
  - "\${INNGEST_PORT:-8288}:8288"
498
- command: inngest dev`);let i=`services:
514
+ command: inngest dev`);let o=`services:
499
515
  ${r.join(`
500
516
 
501
517
  `)}
502
- `;return n.length>0&&(i+=`
518
+ `;return n.length>0&&(o+=`
503
519
  volumes:
504
520
  ${n.join(`
505
521
  `)}
506
- `),await l(Ni(e.projectDir,"infra/docker-compose.yml"),i),!0}import{join as Li}from"path";function $i(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"}}),e.desktop){let n=e.architecture==="separate"?"http://localhost:3010":"http://localhost:3000/api";t.push({type:"node-terminal",request:"launch",name:"Desktop",command:"pnpm dev",cwd:"${workspaceFolder}/apps/desktop",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development",BASE_URL:"http://localhost:3000",PUBLIC_API_URL:n}})}if(t.push({type:"node-terminal",request:"launch",name:"Inngest",command:"pnpm dev:inngest",cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),t.push({type:"node-terminal",request:"launch",name:"Mail",command:"pnpm dev:mail",cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),e.paymentProvider==="stripe"){let n=e.architecture==="separate"?"localhost:3010/auth/stripe/webhook":"localhost:3000/api/auth/stripe/webhook";t.push({type:"node-terminal",request:"launch",name:"Stripe",command:`stripe listen --forward-to ${n}`,cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"]})}return t}function ji(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 Rr(e){let t={version:"0.2.0",configurations:$i(e),compounds:ji(e)};await l(Li(e.projectDir,".vscode/launch.json"),JSON.stringify(t,null," ")+`
507
- `)}import{join as Ui}from"path";async function Or(e){if(e.architecture!=="separate")return;await l(Ui(e.projectDir,"apps/backend/src/index.ts"),`import { serve } from "@hono/node-server";
522
+ `),await l(Mo(e.projectDir,"infra/docker-compose.yml"),o),!0}import{join as Fo}from"path";function Bo(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"}}),e.desktop){let n=e.architecture==="separate"?"http://localhost:3010":"http://localhost:3000/api";t.push({type:"node-terminal",request:"launch",name:"Desktop",command:"pnpm dev",cwd:"${workspaceFolder}/apps/desktop",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development",BASE_URL:"http://localhost:3000",PUBLIC_API_URL:n}})}if(t.push({type:"node-terminal",request:"launch",name:"Inngest",command:"pnpm dev:inngest",cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),t.push({type:"node-terminal",request:"launch",name:"Mail",command:"pnpm dev:mail",cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),e.paymentProvider==="stripe"){let n=e.architecture==="separate"?"localhost:3010/auth/stripe/webhook":"localhost:3000/api/auth/stripe/webhook";t.push({type:"node-terminal",request:"launch",name:"Stripe",command:`stripe listen --forward-to ${n}`,cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"]})}return t}function Go(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 Dr(e){let t={version:"0.2.0",configurations:Bo(e),compounds:Go(e)};await l(Fo(e.projectDir,".vscode/launch.json"),JSON.stringify(t,null," ")+`
523
+ `)}import{join as Ko}from"path";async function Cr(e){if(e.architecture!=="separate")return;await l(Ko(e.projectDir,"apps/backend/src/index.ts"),`import { serve } from "@hono/node-server";
508
524
  import app from "@repo/api";
509
525
  import { closeRedis, env, logger } from "@repo/runtime";
510
526
 
@@ -535,18 +551,18 @@ bootstrap().catch((error) => {
535
551
  logger.error("[Backend] Fatal error", error);
536
552
  process.exit(1);
537
553
  });
538
- `)}import{readFile as Vi}from"fs/promises";import{join as Mi}from"path";var Fi=`export * from "./db/auth";
554
+ `)}import{readFile as zo}from"fs/promises";import{join as Ho}from"path";var Yo=`export * from "./db/auth";
539
555
  export * from "./db/schema";
540
- export type { User, Account, Organization, Member } from "./db/auth";`;async function xr(e){let t=Mi(e.projectDir,"packages/database/src/index.ts"),r=Fi;try{let i=await Vi(t,"utf-8"),o=Bi(i);o&&(r=o)}catch{}let n;switch(e.databaseProvider){case"postgres":n=Gi(r);break;case"neon":n=Ki(r);break;case"supabase":n=zi(r);break}await l(t,n)}function Bi(e){return e.split(`
556
+ export type { User, Account, Organization, Member } from "./db/auth";`;async function Nr(e){let t=Ho(e.projectDir,"packages/database/src/index.ts"),r=Yo;try{let o=await zo(t,"utf-8"),i=Jo(o);i&&(r=i)}catch{}let n;switch(e.databaseProvider){case"postgres":n=Wo(r);break;case"neon":n=qo(r);break;case"supabase":n=Xo(r);break}await l(t,n)}function Jo(e){return e.split(`
541
557
  `).filter(n=>n.startsWith("export type ")||n.startsWith("export * from")).join(`
542
- `)}var Vt=`
558
+ `)}var Bt=`
543
559
  /** Extract affected-row count from a delete/update result (works for pg + postgres-js). */
544
560
  export function affectedRowCount(result: unknown): number {
545
561
  if (typeof result !== "object" || result === null) return 0;
546
562
  const r = result as { rowCount?: number; count?: number };
547
563
  return r.rowCount ?? r.count ?? 0;
548
564
  }
549
- `;function Gi(e){return`import { drizzle } from "drizzle-orm/node-postgres";
565
+ `;function Wo(e){return`import { drizzle } from "drizzle-orm/node-postgres";
550
566
  import { z } from "zod";
551
567
  import * as authSchema from "./db/auth";
552
568
  import * as appSchema from "./db/schema";
@@ -561,7 +577,7 @@ export const db = drizzle(parsed.data.DATABASE_URL, { schema });
561
577
  export const pool = db.$client;
562
578
 
563
579
  ${e}
564
- ${Vt}`}function Ki(e){return`import { neon } from "@neondatabase/serverless";
580
+ ${Bt}`}function qo(e){return`import { neon } from "@neondatabase/serverless";
565
581
  import { drizzle } from "drizzle-orm/neon-http";
566
582
  import { z } from "zod";
567
583
  import * as authSchema from "./db/auth";
@@ -577,7 +593,7 @@ const sql = neon(parsed.data.DATABASE_URL);
577
593
  export const db = drizzle(sql, { schema });
578
594
 
579
595
  ${e}
580
- ${Vt}`}function zi(e){return`import { drizzle } from "drizzle-orm/postgres-js";
596
+ ${Bt}`}function Xo(e){return`import { drizzle } from "drizzle-orm/postgres-js";
581
597
  import postgres from "postgres";
582
598
  import { z } from "zod";
583
599
  import * as authSchema from "./db/auth";
@@ -593,7 +609,7 @@ const client = postgres(parsed.data.DATABASE_URL);
593
609
  export const db = drizzle(client, { schema });
594
610
 
595
611
  ${e}
596
- ${Vt}`}import{join as yt}from"path";async function Dr(e){switch(e.cacheProvider){case"redis":await Hi(e),await Yi(e);break;case"upstash":await Ji(e),await Wi(e);break}}async function Hi(e){await l(yt(e.projectDir,"packages/runtime/src/redis.ts"),`import type { Store } from "hono-rate-limiter";
612
+ ${Bt}`}import{join as St}from"path";async function Lr(e){switch(e.cacheProvider){case"redis":await Zo(e),await Qo(e);break;case"upstash":await ei(e),await ti(e);break}}async function Zo(e){await l(St(e.projectDir,"packages/runtime/src/redis.ts"),`import type { Store } from "hono-rate-limiter";
597
613
  import { Redis } from "ioredis";
598
614
  import { RedisStore, type RedisReply } from "rate-limit-redis";
599
615
  import { env } from "./env";
@@ -717,7 +733,7 @@ export async function closeRedis() {
717
733
  closed = true;
718
734
  }
719
735
  }
720
- `)}async function Yi(e){await l(yt(e.projectDir,"packages/runtime/src/mutex.ts"),`import { Mutex } from "redis-semaphore";
736
+ `)}async function Qo(e){await l(St(e.projectDir,"packages/runtime/src/mutex.ts"),`import { Mutex } from "redis-semaphore";
721
737
  import { redis } from "./redis";
722
738
 
723
739
  export class MutexTimeoutError extends Error {
@@ -754,7 +770,7 @@ export async function withMutex<T>(
754
770
  await mutex.release();
755
771
  }
756
772
  }
757
- `)}async function Ji(e){await l(yt(e.projectDir,"packages/runtime/src/redis.ts"),`import { Redis } from "@upstash/redis";
773
+ `)}async function ei(e){await l(St(e.projectDir,"packages/runtime/src/redis.ts"),`import { Redis } from "@upstash/redis";
758
774
  import type { Store } from "hono-rate-limiter";
759
775
  import { env } from "./env";
760
776
 
@@ -867,7 +883,7 @@ export const limiterStore: Store = createLimiterStore();
867
883
 
868
884
  /** No persistent connection to close with Upstash REST. */
869
885
  export async function closeRedis(): Promise<void> {}
870
- `)}async function Wi(e){await l(yt(e.projectDir,"packages/runtime/src/mutex.ts"),`import { Lock } from "@upstash/lock";
886
+ `)}async function ti(e){await l(St(e.projectDir,"packages/runtime/src/mutex.ts"),`import { Lock } from "@upstash/lock";
871
887
  import { redis } from "./redis";
872
888
 
873
889
  export class MutexTimeoutError extends Error {
@@ -912,7 +928,7 @@ export async function withMutex<T>(
912
928
  await lock.release();
913
929
  }
914
930
  }
915
- `)}async function Cr(e){await l(`${e.projectDir}/packages/runtime/src/env.ts`,qi(e))}function qi(e){return`import { z } from "zod";
931
+ `)}async function $r(e){await l(`${e.projectDir}/packages/runtime/src/env.ts`,ri(e))}function ri(e){return`import { z } from "zod";
916
932
 
917
933
  const EnvSchema = z.object({
918
934
  NODE_ENV: z.enum(["development", "production"]).default("development"),
@@ -1000,7 +1016,11 @@ ${e.cacheProvider==="upstash"?` UPSTASH_REDIS_REST_URL: z.string(),
1000
1016
 
1001
1017
  TURNSTILE_SECRET_KEY: z.string().optional(),
1002
1018
 
1003
- CONTENT_API_KEY: z.string().min(16).optional()
1019
+ CONTENT_API_KEY: z.string().min(16).optional(),
1020
+
1021
+ ANTHROPIC_API_KEY: z.string().optional(),
1022
+ OPENAI_API_KEY: z.string().optional(),
1023
+ OPENROUTER_API_KEY: z.string().optional()
1004
1024
  });
1005
1025
 
1006
1026
  export const env = (() => {
@@ -1027,13 +1047,13 @@ export const env = (() => {
1027
1047
  const parsedApiUrl = new URL(env.API_URL);
1028
1048
  export const apiBasePath = parsedApiUrl.pathname === "/" ? "" : parsedApiUrl.pathname;
1029
1049
  export const apiOrigin = parsedApiUrl.origin;
1030
- `}import{readFile as Xi}from"fs/promises";import{join as H}from"path";var vt={"@upstash/redis":"^1.37.0","@upstash/lock":"^0.2.1","@neondatabase/serverless":"^1.0.1",postgres:"^3.4.7"};async function Oe(e){let t=await Xi(e,"utf-8");return JSON.parse(t)}async function xe(e,t){await l(e,JSON.stringify(t,null," ")+`
1031
- `)}function ze(e,t){for(let r of t)delete e.dependencies?.[r],delete e.devDependencies?.[r]}function Re(e,t,r,n=!1){let i=n?"devDependencies":"dependencies";e[i]||(e[i]={}),e[i][t]=r}async function Nr(e){await Zi(e),await Qi(e),await eo(e),await to(e),e.frontend==="nextjs"?await no(e):await ro(e)}async function Zi(e){let t=H(e.projectDir,"packages/api/package.json"),r=await Oe(t);ze(r,["sharp","@types/sharp"]),await xe(t,r)}async function Qi(e){let t=H(e.projectDir,"packages/runtime/package.json"),r=await Oe(t);e.cacheProvider==="upstash"&&(ze(r,["ioredis","rate-limit-redis","redis-semaphore"]),Re(r,"@upstash/redis",vt["@upstash/redis"]),Re(r,"@upstash/lock",vt["@upstash/lock"])),await xe(t,r)}async function eo(e){let t=H(e.projectDir,"packages/database/package.json"),r=await Oe(t);e.databaseProvider==="neon"?(ze(r,["pg","@types/pg"]),Re(r,"@neondatabase/serverless",vt["@neondatabase/serverless"])):e.databaseProvider==="supabase"&&(ze(r,["pg","@types/pg"]),Re(r,"postgres",vt.postgres)),await xe(t,r)}async function to(e){if(e.architecture!=="separate")return;let t=H(e.projectDir,"apps/backend/package.json"),r=await Oe(t);e.deploymentTarget!=="node"&&ze(r,["@hono/node-server"]),await xe(t,r)}async function ro(e){if(e.architecture!=="separate")return;let t=H(e.projectDir,"apps/web-nuxt");await $t(H(t,"server/api/[...paths].ts"),t);let r=H(t,"package.json"),n=await Oe(r),i=n.dependencies?.["@repo/api"];i&&(delete n.dependencies?.["@repo/api"],Re(n,"@repo/api",i,!0)),await xe(r,n)}async function no(e){if(e.architecture!=="separate")return;let t=H(e.projectDir,"apps/web-next");await $t(H(t,"app/api/[[...rest]]/route.ts"),t);let r=H(t,"package.json"),n=await Oe(r),i=n.dependencies?.["@repo/api"];i&&(delete n.dependencies?.["@repo/api"],Re(n,"@repo/api",i,!0)),await xe(r,n)}import{readFile as $r}from"fs/promises";import{join as jr}from"path";async function Ur(e){let t=jr(e.projectDir,"turbo.json"),r;try{r=await $r(t,"utf-8")}catch{return}let n=JSON.parse(r),i=n.tasks?.build;if(!i)return;let o=e.frontend==="nextjs"?"NUXT_PUBLIC_*":"NEXT_PUBLIC_*",s=e.frontend==="nextjs"?new Set([".nuxt/**",".output/**"]):new Set([".next/**","!.next/cache/**"]),a=!1;if(Array.isArray(i.env)){let d=i.env.filter(h=>h!==o);d.length!==i.env.length&&(i.env=d,a=!0)}if(Array.isArray(i.outputs)){let d=i.outputs.filter(h=>!s.has(h));d.length!==i.outputs.length&&(i.outputs=d,a=!0)}a&&await l(t,JSON.stringify(n,null," ")+`
1032
- `)}var io=["base.json","node.json","next.json"],Lr="GenerateSaaS ";async function Vr(e){if(!e.demo)for(let t of io){let r=jr(e.projectDir,"tooling/typescript",t),n;try{n=await $r(r,"utf-8")}catch{continue}let i=JSON.parse(n);typeof i.display!="string"||!i.display.startsWith(Lr)||(i.display=i.display.slice(Lr.length),await l(r,JSON.stringify(i,null," ")+`
1033
- `))}}import{readFile as oo,rm as so}from"fs/promises";import{existsSync as Mr}from"fs";import{join as Fr}from"path";async function Br(e){if(e.frontend==="nuxt")return;let t=Fr(e.projectDir,"packages/i18n/package.json");if(!Mr(t))throw new Error(`pruneI18nNuxt: expected ${t} to exist - did the i18n package move?`);let r=JSON.parse(await oo(t,"utf-8")),n=!!(r.exports?.["./module"]??r.exports?.["./nuxt"]),i=!1;if(r.exports)for(let s of["./module","./nuxt"])s in r.exports&&(delete r.exports[s],i=!0);r.devDependencies&&"@nuxt/kit"in r.devDependencies&&(delete r.devDependencies["@nuxt/kit"],i=!0),i&&await l(t,JSON.stringify(r,null," ")+`
1034
- `);let o=Fr(e.projectDir,"packages/i18n/nuxt");if(n&&!Mr(o))throw new Error(`pruneI18nNuxt: packages/i18n declares a Nuxt export surface but ${o} is missing - did the i18n Nuxt module move?`);await so(o,{recursive:!0,force:!0})}import{readFile as Gr,rm as ao}from"fs/promises";import{join as Mt}from"path";async function Kr(e){e.cacheProvider==="upstash"&&await Promise.all([co(e.projectDir),lo(e.projectDir),ao(Mt(e.projectDir,"packages/runtime/tests/redis.test.ts"),{force:!0})])}async function co(e){let t=Mt(e,"packages/runtime/tests/setup.ts"),r;try{r=await Gr(t,"utf-8")}catch{return}let n=r.replace(/\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1050
+ `}import{readFile as ni}from"fs/promises";import{join as J}from"path";var Et={"@upstash/redis":"^1.37.0","@upstash/lock":"^0.2.1","@neondatabase/serverless":"^1.0.1",postgres:"^3.4.7"};async function Oe(e){let t=await ni(e,"utf-8");return JSON.parse(t)}async function xe(e,t){await l(e,JSON.stringify(t,null," ")+`
1051
+ `)}function He(e,t){for(let r of t)delete e.dependencies?.[r],delete e.devDependencies?.[r]}function Re(e,t,r,n=!1){let o=n?"devDependencies":"dependencies";e[o]||(e[o]={}),e[o][t]=r}async function Ur(e){await oi(e),await ii(e),await si(e),await ai(e),e.frontend==="nextjs"?await li(e):await ci(e)}async function oi(e){let t=J(e.projectDir,"packages/api/package.json"),r=await Oe(t);He(r,["sharp","@types/sharp"]),await xe(t,r)}async function ii(e){let t=J(e.projectDir,"packages/runtime/package.json"),r=await Oe(t);e.cacheProvider==="upstash"&&(He(r,["ioredis","rate-limit-redis","redis-semaphore"]),Re(r,"@upstash/redis",Et["@upstash/redis"]),Re(r,"@upstash/lock",Et["@upstash/lock"])),await xe(t,r)}async function si(e){let t=J(e.projectDir,"packages/database/package.json"),r=await Oe(t);e.databaseProvider==="neon"?(He(r,["pg","@types/pg"]),Re(r,"@neondatabase/serverless",Et["@neondatabase/serverless"])):e.databaseProvider==="supabase"&&(He(r,["pg","@types/pg"]),Re(r,"postgres",Et.postgres)),await xe(t,r)}async function ai(e){if(e.architecture!=="separate")return;let t=J(e.projectDir,"apps/backend/package.json"),r=await Oe(t);e.deploymentTarget!=="node"&&He(r,["@hono/node-server"]),await xe(t,r)}async function ci(e){if(e.architecture!=="separate")return;let t=J(e.projectDir,"apps/web-nuxt");await Vt(J(t,"server/api/[...paths].ts"),t);let r=J(t,"package.json"),n=await Oe(r),o=n.dependencies?.["@repo/api"];o&&(delete n.dependencies?.["@repo/api"],Re(n,"@repo/api",o,!0)),await xe(r,n)}async function li(e){if(e.architecture!=="separate")return;let t=J(e.projectDir,"apps/web-next");await Vt(J(t,"app/api/[[...rest]]/route.ts"),t);let r=J(t,"package.json"),n=await Oe(r),o=n.dependencies?.["@repo/api"];o&&(delete n.dependencies?.["@repo/api"],Re(n,"@repo/api",o,!0)),await xe(r,n)}import{readFile as Vr}from"fs/promises";import{join as Mr}from"path";async function Fr(e){let t=Mr(e.projectDir,"turbo.json"),r;try{r=await Vr(t,"utf-8")}catch{return}let n=JSON.parse(r),o=n.tasks?.build;if(!o)return;let i=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(o.env)){let d=o.env.filter(g=>g!==i);d.length!==o.env.length&&(o.env=d,a=!0)}if(Array.isArray(o.outputs)){let d=o.outputs.filter(g=>!s.has(g));d.length!==o.outputs.length&&(o.outputs=d,a=!0)}a&&await l(t,JSON.stringify(n,null," ")+`
1052
+ `)}var pi=["base.json","node.json","next.json"],jr="GenerateSaaS ";async function Br(e){if(!e.demo)for(let t of pi){let r=Mr(e.projectDir,"tooling/typescript",t),n;try{n=await Vr(r,"utf-8")}catch{continue}let o=JSON.parse(n);typeof o.display!="string"||!o.display.startsWith(jr)||(o.display=o.display.slice(jr.length),await l(r,JSON.stringify(o,null," ")+`
1053
+ `))}}import{readFile as di,rm as ui}from"fs/promises";import{existsSync as Gr}from"fs";import{join as Kr}from"path";async function zr(e){if(e.frontend==="nuxt")return;let t=Kr(e.projectDir,"packages/i18n/package.json");if(!Gr(t))throw new Error(`pruneI18nNuxt: expected ${t} to exist - did the i18n package move?`);let r=JSON.parse(await di(t,"utf-8")),n=!!(r.exports?.["./module"]??r.exports?.["./nuxt"]),o=!1;if(r.exports)for(let s of["./module","./nuxt"])s in r.exports&&(delete r.exports[s],o=!0);r.devDependencies&&"@nuxt/kit"in r.devDependencies&&(delete r.devDependencies["@nuxt/kit"],o=!0),o&&await l(t,JSON.stringify(r,null," ")+`
1054
+ `);let i=Kr(e.projectDir,"packages/i18n/nuxt");if(n&&!Gr(i))throw new Error(`pruneI18nNuxt: packages/i18n declares a Nuxt export surface but ${i} is missing - did the i18n Nuxt module move?`);await ui(i,{recursive:!0,force:!0})}import{readFile as Hr,rm as mi}from"fs/promises";import{join as Gt}from"path";async function Yr(e){e.cacheProvider==="upstash"&&await Promise.all([fi(e.projectDir),gi(e.projectDir),mi(Gt(e.projectDir,"packages/runtime/tests/redis.test.ts"),{force:!0})])}async function fi(e){let t=Gt(e,"packages/runtime/tests/setup.ts"),r;try{r=await Hr(t,"utf-8")}catch{return}let n=r.replace(/\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1035
1055
  UPSTASH_REDIS_REST_TOKEN: "test-token",
1036
- `);n!==r&&await l(t,n)}async function lo(e){let t=Mt(e,"packages/api/tests/setup.ts"),r;try{r=await Gr(t,"utf-8")}catch{return}let n=r;n=n.replace(/\tREDIS_URL:\s*"[^"]*",?\n/g,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1056
+ `);n!==r&&await l(t,n)}async function gi(e){let t=Gt(e,"packages/api/tests/setup.ts"),r;try{r=await Hr(t,"utf-8")}catch{return}let n=r;n=n.replace(/\tREDIS_URL:\s*"[^"]*",?\n/g,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1037
1057
  UPSTASH_REDIS_REST_TOKEN: "test-token",
1038
1058
  `),n=n.replace(/vi\.mock\("ioredis"[\s\S]*?\n\}\);\n\n?/,""),n=n.replace(/vi\.mock\("rate-limit-redis"[\s\S]*?\n\}\);\n\n?/,""),n=n.replace(/\t\t\tREDIS_URL:\s*"[^"]*",?\n/,` UPSTASH_REDIS_REST_URL: "https://test.upstash.io",
1039
1059
  UPSTASH_REDIS_REST_TOKEN: "test-token",
@@ -1054,8 +1074,20 @@ vi.mock("@upstash/lock", () => {
1054
1074
  return { Lock };
1055
1075
  });
1056
1076
 
1057
- $1`),n!==r&&await l(t,n)}import{readdir as zr,readFile as po,rm as Z}from"fs/promises";import{join as L}from"path";var uo=new Set(["ci.yml","desktop-release.yml"]),mo=["cli","cli:clean","demo:bench","playground:regen","playground:test","playground:test:units","playground:test:build"];async function Hr(e){let t=L(e.projectDir,".github/workflows"),r=await zr(t).catch(()=>[]);for(let n of r)uo.has(n)||await Z(L(t,n),{recursive:!0,force:!0})}async function Yr(e){let t=L(e.projectDir,"package.json"),r=await po(t,"utf-8"),n=JSON.parse(r),i=!1;if(n.scripts){for(let o of mo)o in n.scripts&&(delete n.scripts[o],i=!0);if(!Qe(e))for(let o of["infra","infra:stop"])o in n.scripts&&(delete n.scripts[o],i=!0)}i&&await l(t,JSON.stringify(n,null," ")+`
1058
- `)}async function Jr(e){let t=e.frontend==="nextjs"?"apps/web-nuxt":"apps/web-next";await Z(L(e.projectDir,t),{recursive:!0,force:!0})}async function Wr(e){e.docs||await Z(L(e.projectDir,"apps/docs"),{recursive:!0})}async function qr(e){let t=e.frontend==="nextjs"?"docs/nuxt":"docs/next";await Z(L(e.projectDir,t),{recursive:!0,force:!0}),await Z(L(e.projectDir,"docs/index.mdx"))}async function Xr(e){if(e.desktop)return;await Z(L(e.projectDir,"apps/desktop"),{recursive:!0});let t=L(e.projectDir,"packages/i18n/translations");for(let r of await zr(t,{withFileTypes:!0}))r.isDirectory()&&await Z(L(t,r.name,"desktop.json"),{force:!0});await Z(L(e.projectDir,".github/workflows/desktop-release.yml"))}async function Zr(e){e.frontend==="nextjs"||e.desktop||await Z(L(e.projectDir,"packages/ui-next"),{recursive:!0})}import{join as fo}from"path";async function Qr(e){let t=go(e);await l(fo(e.projectDir,".github/workflows/ci.yml"),t)}function go(e){let t=N[e.databaseProvider].managed,r=e.cacheProvider==="upstash",n=e.frontend==="nuxt",i=t?"":` services:
1077
+ $1`),n!==r&&await l(t,n)}import{readdir as Jr,readFile as hi,rm as Q}from"fs/promises";import{join as U}from"path";var yi=new Set(["ci.yml","desktop-release.yml"]),vi=["cli","cli:clean","demo:bench","playground:regen","playground:test","playground:test:units","playground:test:build"];async function Wr(e){let t=U(e.projectDir,".github/workflows"),r=await Jr(t).catch(()=>[]);for(let n of r)yi.has(n)||await Q(U(t,n),{recursive:!0,force:!0})}async function qr(e){let t=U(e.projectDir,"package.json"),r=await hi(t,"utf-8"),n=JSON.parse(r),o=!1;if(n.scripts){for(let i of vi)i in n.scripts&&(delete n.scripts[i],o=!0);if(!tt(e))for(let i of["infra","infra:stop"])i in n.scripts&&(delete n.scripts[i],o=!0)}o&&await l(t,JSON.stringify(n,null," ")+`
1078
+ `)}async function Xr(e){let t=e.frontend==="nextjs"?"apps/web-nuxt":"apps/web-next";await Q(U(e.projectDir,t),{recursive:!0,force:!0})}async function Zr(e){e.docs||await Q(U(e.projectDir,"apps/docs"),{recursive:!0})}async function Qr(e){let t=e.frontend==="nextjs"?"docs/nuxt":"docs/next";await Q(U(e.projectDir,t),{recursive:!0,force:!0}),await Q(U(e.projectDir,"docs/index.mdx"))}async function en(e){if(e.desktop)return;await Q(U(e.projectDir,"apps/desktop"),{recursive:!0});let t=U(e.projectDir,"packages/i18n/translations");for(let r of await Jr(t,{withFileTypes:!0}))r.isDirectory()&&await Q(U(t,r.name,"desktop.json"),{force:!0});await Q(U(e.projectDir,".github/workflows/desktop-release.yml"))}async function tn(e){e.frontend==="nextjs"||e.desktop||await Q(U(e.projectDir,"packages/ui-next"),{recursive:!0})}import{readFile as Si}from"fs/promises";import{join as Ei}from"path";var rn=`on:
1079
+ workflow_run:
1080
+ workflows: ["CI"]
1081
+ branches: [main]
1082
+ types: [completed]
1083
+ workflow_dispatch:`,wi=`on:
1084
+ # Automatic release on CI success is disabled for this project to conserve
1085
+ # GitHub Actions minutes. Re-enable by uncommenting the workflow_run trigger.
1086
+ # workflow_run:
1087
+ # workflows: ["CI"]
1088
+ # branches: [main]
1089
+ # types: [completed]
1090
+ workflow_dispatch:`;async function nn(e){if(!e.desktop||e.desktopAutoRelease)return;let t=Ei(e.projectDir,".github/workflows/desktop-release.yml"),r=await Si(t,"utf8");if(!r.includes(rn))throw new Error(`Cannot make desktop releases manual: the expected workflow_run trigger block was not found in ${t}. The boilerplate workflow may have drifted.`);await l(t,r.replace(rn,wi))}import{join as bi}from"path";async function on(e){let t=ki(e);await l(bi(e.projectDir,".github/workflows/ci.yml"),t)}function ki(e){let t=$[e.databaseProvider].managed,r=e.cacheProvider==="upstash",n=e.frontend==="nuxt",o=t?"":` services:
1059
1091
  postgres:
1060
1092
  image: postgres:18-alpine
1061
1093
  env:
@@ -1069,14 +1101,14 @@ $1`),n!==r&&await l(t,n)}import{readdir as zr,readFile as po,rm as Z}from"fs/pro
1069
1101
  --health-interval 10s
1070
1102
  --health-timeout 5s
1071
1103
  --health-retries 5
1072
- `,o=t?"postgres://test:test@localhost:5432/saas_test":"postgres://postgres:postgres@localhost:5432/saas",s=r?`
1104
+ `,i=t?"postgres://test:test@localhost:5432/saas_test":"postgres://postgres:postgres@localhost:5432/saas",s=r?`
1073
1105
  UPSTASH_REDIS_REST_URL: https://test.upstash.io
1074
1106
  UPSTASH_REDIS_REST_TOKEN: test-token`:"",a=(()=>{switch(e.paymentProvider){case"stripe":return`
1075
1107
  STRIPE_SECRET_KEY: test
1076
1108
  STRIPE_WEBHOOK_SECRET: test`;case"polar":return`
1077
1109
  POLAR_ACCESS_TOKEN: test
1078
- POLAR_WEBHOOK_SECRET: test`;case"none":return"";default:{let f=e.paymentProvider;throw new Error(`buildCiYaml: unhandled payment provider "${String(f)}"`)}}})(),d=e.socialProviders.map(f=>W[f].envVars.map(S=>`
1079
- ${S.name}: test`).join("")).join("");return`# Deployment is handled by the hosting platform (Vercel, Coolify, etc.)
1110
+ POLAR_WEBHOOK_SECRET: test`;case"none":return"";default:{let m=e.paymentProvider;throw new Error(`buildCiYaml: unhandled payment provider "${String(m)}"`)}}})(),d=e.socialProviders.map(m=>q[m].envVars.map(_=>`
1111
+ ${_.name}: test`).join("")).join("");return`# Deployment is handled by the hosting platform (Vercel, Coolify, etc.)
1080
1112
  # which auto-deploys on push. CI runs in parallel as a quality gate.
1081
1113
  # For PR-based workflows, enable GitHub branch protection to require CI before merging.
1082
1114
 
@@ -1120,7 +1152,7 @@ ${n?` # vue-tsc on web-nuxt OOMs on the GitHub runner's default heap once
1120
1152
  name: Test
1121
1153
  runs-on: ubuntu-latest
1122
1154
  timeout-minutes: 10
1123
- ${i} env:
1155
+ ${o} env:
1124
1156
  CONTENT_API_KEY: test-contentapi-key-16chars${s}${a}${d}
1125
1157
  STORAGE_REGION: test
1126
1158
  STORAGE_ENDPOINT: http://test
@@ -1133,48 +1165,48 @@ ${i} env:
1133
1165
  - uses: ./.github/actions/setup
1134
1166
 
1135
1167
  - name: Write root env
1136
- run: echo "DATABASE_URL=${o}" > .env
1168
+ run: echo "DATABASE_URL=${i}" > .env
1137
1169
 
1138
1170
  - run: pnpm test
1139
- `}import{readFile as en}from"fs/promises";import{existsSync as ho}from"fs";import{join as Ft}from"path";var tn="@repo/database";function yo(e){return e?"pnpm -F @repo/database reset && pnpm -F @repo/database push --force":"pnpm -F @repo/database migrate"}function vo(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 So(e,t,r){let n=await en(e,"utf-8"),i=JSON.parse(n),o=i.scripts?.[t];if(!o)throw new Error(`Cannot prepend to missing script "${t}" in ${e}`);o.includes(tn)||(i.scripts={...i.scripts,[t]:`${r} && ${o}`},await l(e,JSON.stringify(i,null," ")+`
1140
- `))}async function Eo(e,t){let r=ho(e)?JSON.parse(await en(e,"utf-8")):{$schema:"https://openapi.vercel.sh/vercel.json"},n=r.buildCommand?.trim()||"pnpm build";n.includes(tn)||(r.buildCommand=`${t} && ${n}`,await l(e,JSON.stringify(r,null," ")+`
1141
- `))}async function rn(e){let t=yo(e.demo===!0),r=vo(e.architecture,e.frontend),n=Ft(e.projectDir,"apps",r);switch(e.deploymentTarget){case"node":await So(Ft(n,"package.json"),"start",t);return;case"vercel":await Eo(Ft(n,"vercel.json"),t);return;default:{let i=e.deploymentTarget;throw new Error(`generateDeployScripts: unhandled deployment target "${String(i)}"`)}}}import{readFile as wo}from"fs/promises";import{existsSync as bo}from"fs";import{join as St}from"path";var ko=["stripe","polar"];async function nn(e){let t=St(e.projectDir,"packages/payments/src"),r=e.paymentProvider,n=`// Active payment provider. To switch, change the path below to
1171
+ `}import{readFile as sn}from"fs/promises";import{existsSync as Ai}from"fs";import{join as Kt}from"path";var an="@repo/database";function Ii(e){return e?"pnpm -F @repo/database reset && pnpm -F @repo/database push --force":"pnpm -F @repo/database migrate"}function Ti(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 Pi(e,t,r){let n=await sn(e,"utf-8"),o=JSON.parse(n),i=o.scripts?.[t];if(!i)throw new Error(`Cannot prepend to missing script "${t}" in ${e}`);i.includes(an)||(o.scripts={...o.scripts,[t]:`${r} && ${i}`},await l(e,JSON.stringify(o,null," ")+`
1172
+ `))}async function _i(e,t){let r=Ai(e)?JSON.parse(await sn(e,"utf-8")):{$schema:"https://openapi.vercel.sh/vercel.json"},n=r.buildCommand?.trim()||"pnpm build";n.includes(an)||(r.buildCommand=`${t} && ${n}`,await l(e,JSON.stringify(r,null," ")+`
1173
+ `))}async function cn(e){let t=Ii(e.demo===!0),r=Ti(e.architecture,e.frontend),n=Kt(e.projectDir,"apps",r);switch(e.deploymentTarget){case"node":await Pi(Kt(n,"package.json"),"start",t);return;case"vercel":await _i(Kt(n,"vercel.json"),t);return;default:{let o=e.deploymentTarget;throw new Error(`generateDeployScripts: unhandled deployment target "${String(o)}"`)}}}import{readFile as Ri}from"fs/promises";import{existsSync as Oi}from"fs";import{join as wt}from"path";var xi=["stripe","polar"];async function ln(e){let t=wt(e.projectDir,"packages/payments/src"),r=e.paymentProvider,n=`// Active payment provider. To switch, change the path below to
1142
1174
  // "./polar/index" or "./none/index" (other folders are kept in place).
1143
1175
  export { ops } from "./${r}/index";
1144
- `;await l(St(t,"providers/index.ts"),n),await l(St(t,"index.ts"),await Ao(t,r))}async function Ao(e,t){let r=St(e,"index.ts");if(!bo(r))throw new Error(`generatePaymentBarrel: expected ${r} to exist - did packages/payments move?`);let n=await wo(r,"utf-8"),i=ko.filter(s=>s!==t);return n.split(`
1145
- `).filter(s=>!i.some(a=>s.includes(`./providers/${a}/`))).join(`
1146
- `)}import{readdir as To,readFile as Io}from"fs/promises";import{join as on}from"path";async function sn(e){if(e.demo)return;let t=e.appName.trim()||e.projectName,r=JSON.stringify(t).slice(1,-1),n=on(e.projectDir,"packages/i18n/translations"),i;try{i=await To(n)}catch{return}for(let o of i){let s=on(n,o,"web.json"),a;try{a=await Io(s,"utf-8")}catch{continue}let d=a.replaceAll("GenerateSaaS",r);d!==a&&await l(s,d)}}import{readFile as Po}from"fs/promises";import{join as an}from"path";var _o=[".nuxt/",".nuxt",".nitro/",".nitro",".output/",".output","_locales/"],Ro=[".next/",".next",".svelte-kit/",".svelte-kit",".wrangler/",".wrangler",".dev.vars"];async function ln(e){let r=e.frontend==="nuxt"?Ro:_o;await cn(an(e.projectDir,".gitignore"),r),await cn(an(e.projectDir,".dockerignore"),r)}async function cn(e,t){let r;try{r=await Po(e,"utf-8")}catch{return}let n=new Set(t),i=r.split(`
1147
- `).filter(o=>!n.has(o.trim()));i.length!==r.split(`
1148
- `).length&&await l(e,i.join(`
1149
- `))}async function Et(e){let t=e.projectDir;e.demo||(await gr(t),await hr(t),await yr(t)),await vr(t,e.aiTools),await Sr(t,e.frontend),await wr(e),await br(e),await kr(e),e.demo||await Ar(e),await Pr(e);let r=await _r(e);return await Rr(e),await Or(e),await xr(e),await Dr(e),await Cr(e),await Nr(e),await Ur(e),await Vr(e),await Br(e),await Kr(e),await Hr(e),await Qr(e),await Yr(e),await Jr(e),await Wr(e),await qr(e),await Xr(e),await Zr(e),await rn(e),await nn(e),await sn(e),await ln(e),{dockerComposeGenerated:r}}import{basename as dn,join as un,relative as xo}from"path";import{createHash as pn}from"crypto";import{readFile as Oo}from"fs/promises";async function wt(e){let t=await Oo(e);return pn("sha256").update(t).digest("hex")}function Bt(e){return pn("sha256").update(e).digest("hex")}var Do=new Set(["data",U]);function Co(e){let t=e.split("/");for(let r of t)if(xt.has(r)||Dt.has(r)||Do.has(r)||r.startsWith(".env")&&!r.includes("example"))return!0;return!1}function mn(e,t){return{projectName:e.projectName??dn(t),appName:e.appName??e.projectName??dn(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,desktop:e.desktop??!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 fn(e,t){let n=(await he(e.projectDir,e.projectDir,Co)).sort(),i=await Promise.all(n.map(async a=>[ge(xo(e.projectDir,a)),await wt(a)])),o=Object.fromEntries(i),s={version:e.version,initialVersion:e.version,repo:"Duzbee/GenerateSaaS",appName:e.appName,projectName:e.projectName,frontend:e.frontend,architecture:e.architecture,paymentProvider:e.paymentProvider,emailProvider:e.emailProvider,multiTenancy:e.multiTenancy,billingScope:e.billingScope,blog:e.blog,docs:e.docs,desktop:e.desktop,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(un(e.projectDir,ee),JSON.stringify(s,null," ")+`
1150
- `),await l(un(e.projectDir,ir),JSON.stringify(o,null," ")+`
1151
- `)}import{relative as No}from"path";async function De(e){let r=(await he(e,e,Te)).sort(),n=await Promise.all(r.map(async i=>[ge(No(e,i)),await wt(i)]));return Object.fromEntries(n)}import{copyFile as Lo,mkdir as $o,rm as jo}from"fs/promises";import{dirname as Uo,join as gn,relative as Vo}from"path";import{existsSync as Mo}from"fs";async function Gt(e,t){Mo(t)&&await jo(t,{recursive:!0,force:!0});let r=await he(e,e,Te);for(let n of r){let i=ge(Vo(e,n)),o=gn(t,i);await $o(Uo(o),{recursive:!0}),await Lo(n,o)}}async function hn(e,t){await Gt(e,gn(t,pt))}import{existsSync as Fo}from"fs";import{readFile as yn,readdir as Bo}from"fs/promises";import{join as Y,dirname as Go,resolve as Ko,sep as zo}from"path";import{fileURLToPath as Ho}from"url";var He={"claude-code":".claude/skills",cursor:".cursor/skills",codex:".agents/skills","gemini-cli":".gemini/skills",windsurf:".windsurf/skills"},Wl=Object.values(He),Kt="generatesaas-update",vn=Go(Ho(import.meta.url));function Yo(){let e=Y(vn,"skill","content");return Fo(e)?e:Y(vn,"content")}function zt(e){return!e||e.length===0?[]:e.map(t=>He[t])}async function Ht(e,t,r,n){let i=zt(n);for(let o of i){let s=Y(e,o,Kt),a=Y(s,"scripts"),d=Y(s,"references");await ft(a),await ft(d),await l(Y(s,"SKILL.md"),t.replaceAll("__SKILL_ROOT__",o)),await l(Y(d,".gitkeep"),"");for(let[h,y]of Object.entries(r)){let f=Ko(a,h);f.startsWith(a+zo)&&await l(f,y)}}}async function Sn(e,t){let r=Yo(),n=await yn(Y(r,"SKILL.md"),"utf-8"),i=Y(r,"scripts"),o=await Bo(i),s={};for(let a of o)a!==".gitkeep"&&(s[a]=await yn(Y(i,a),"utf-8"));await Ht(e,n,s,t)}import{execFile as Jo,execFileSync as Wo}from"child_process";import{access as En,readFile as qo}from"fs/promises";import{join as Yt}from"path";import*as P from"@clack/prompts";function ve(e){try{let t=process.platform==="win32"?"where":"which";return Wo(t,[e],{stdio:"ignore"}),!0}catch{return!1}}function ye(e,t,r,n=3e5){return new Promise((i,o)=>{Jo(e,t,{cwd:r,timeout:n},(s,a,d)=>{if(s){let h=String(a||"").trim(),f=[String(d||"").trim(),h].filter(Boolean).join(`
1152
- `);o(new Error(f?`${s.message}
1153
- ${f}`:s.message))}else i()})})}async function wn(e){if(!ve("pnpm"))return P.log.warn("pnpm not found. Skipping lockfile regeneration."),!1;try{return await ye("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 bn(e){if(!ve("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 ye("pnpm",["install","--config.minimumReleaseAge=0"],e),t.stop("Dependencies installed."),!0}catch(r){t.stop("Dependency installation failed.");let n=r instanceof Error?r.message:String(r);return P.log.warn(`pnpm install failed: ${n}`),P.log.warn("You can run it manually later."),!1}}async function kn(e){if(!ve("pnpm"))return!1;let t=P.spinner();t.start("Generating baseline database migration...");try{return await ye("pnpm",["-F","@repo/database","generate"],e),t.stop("Baseline migration generated."),!0}catch(r){t.stop("Baseline migration generation failed.");let n=r instanceof Error?r.message:String(r);return P.log.warn(`Could not generate baseline migration: ${n}`),P.log.warn("Run 'pnpm -F @repo/database generate' before your first deploy."),!1}}async function An(e){try{return await En(Yt(e,".git")),P.log.info("Git repository already exists, skipping init."),!0}catch{}if(!ve("git"))return P.log.warn("git not found. Skipping repository initialization."),!1;let t=P.spinner();t.start("Initializing git repository...");try{return await ye("git",["init"],e),await ye("git",["add","-A"],e),await ye("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 Tn(e){if(!ve("pnpm"))return!1;try{await En(Yt(e,".git"))}catch{return!1}try{let t=JSON.parse(await qo(Yt(e,"package.json"),"utf-8")),r=!!t.devDependencies?.["simple-git-hooks"],n=!!t["simple-git-hooks"];if(!r||!n)return!1}catch{return!1}try{return await ye("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 Ce from"@clack/prompts";import V from"picocolors";function In(e,t){t.dockerComposeGenerated&&!t.dockerAvailable&&Ce.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=>ke[s].label).join(", ");r.push(`pnpm infra ${V.dim(`# ${o}`)}`)}if(r.push(`pnpm dev ${V.dim("# http://localhost:3000")}`),t.skippedCredentials.length>0&&(r.push(""),r.push(V.dim("Fill in remaining TODO values in .env"))),Ce.note(r.join(`
1154
- `),V.yellow("Start Development")),t.dockerComposeGenerated){let o=[];o.push(`App ${V.cyan("http://localhost:3000")}`),e.architecture==="separate"&&o.push(`API ${V.cyan("http://localhost:3010")}`),e.dockerServices.includes("mailpit")&&o.push(`Mailpit ${V.cyan("http://localhost:8025")}`),e.dockerServices.includes("inngest")&&o.push(`Inngest ${V.cyan("http://localhost:8288")}`),Ce.note(o.join(`
1155
- `),V.yellow("Dev Tools"))}let n=[],i=Xo(e);i.length>0&&n.push(`Set in production: ${V.dim(i.join(", "))}`),n.push("pnpm db:push # Run database migrations"),n.push(Zo(e)),Ce.note(n.join(`
1156
- `),V.yellow("Deployment"))}function Xo(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 Zo(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 Pn(e){let t={};if(e.name!==void 0){if(!ct(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(!Fe.includes(e.frontend))throw new Error(`Invalid frontend "${e.frontend}". Valid values: ${Fe.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.desktop!==void 0&&(t.desktop=e.desktop),e.revenueSharing!==void 0&&(t.revenueSharing=e.revenueSharing),e.credits!==void 0){if(e.credits===!0&&e.payment==="none")throw new Error("--credits requires a payment provider (got --payment none).");t.credits=e.credits}if(e.docker!==void 0&&(t.dockerServices=Jt(e.docker,nt,"docker service")),e.aiTools!==void 0&&(t.aiTools=Jt(e.aiTools,it,"AI tool")),e.socialProviders!==void 0&&(t.socialProviders=Jt(e.socialProviders,st,"social provider")),e.currency!==void 0){if(!le.includes(e.currency))throw new Error(`Invalid currency "${e.currency}". Valid values: ${le.join(", ")}`);t.defaultCurrency=e.currency}if(e.deploy!==void 0){if(!pe.includes(e.deploy))throw new Error(`Invalid deployment target "${e.deploy}". Valid values: ${pe.join(", ")}`);t.deploymentTarget=e.deploy}if(e.database!==void 0){if(!de.includes(e.database))throw new Error(`Invalid database provider "${e.database}". Valid values: ${de.join(", ")}`);t.databaseProvider=e.database}if(e.cache!==void 0){if(!ue.includes(e.cache))throw new Error(`Invalid cache provider "${e.cache}". Valid values: ${ue.join(", ")}`);t.cacheProvider=e.cache}if(e.demo===!0&&(t.demo=!0),e.baseUrl!==void 0){let r=e.baseUrl.trim();if(r==="")throw new Error("--base-url cannot be empty. Provide an absolute URL like https://example.com.");let n;try{n=new URL(r)}catch{throw new Error(`Invalid --base-url "${e.baseUrl}". Must be an absolute URL like https://example.com.`)}if(n.protocol!=="http:"&&n.protocol!=="https:")throw new Error(`Invalid --base-url "${e.baseUrl}". Must use http or https.`);t.baseUrl=`${n.protocol}//${n.host}`}return t}var ie={projectName:"my-saas",frontend:"nextjs",architecture:"fullstack",paymentProvider:"stripe",emailProvider:"smtp",multiTenancy:!1,billingScope:"user",blog:!0,docs:!1,desktop:!1,revenueSharing:!1,credits:!0,dockerServices:["postgres","redis","inngest"],aiTools:[],socialProviders:[],defaultCurrency:"USD",deploymentTarget:"node",databaseProvider:"postgres",cacheProvider:"redis"};function _n(e){let t=e.projectName??ie.projectName,r=e.projectDir??`./${t}`,n=e.appName??at(t),i=e.deploymentTarget??ie.deploymentTarget,o=K[i]?.edgeRuntime??!1,s=e.databaseProvider??(o?"neon":ie.databaseProvider),a=e.cacheProvider??(o?"upstash":ie.cacheProvider),d=e.emailProvider??(o?"resend":ie.emailProvider),h=e.dockerServices??(o?ie.dockerServices.filter(f=>f!=="postgres"&&f!=="redis"):ie.dockerServices),y={...ie,...e,projectName:t,appName:n,projectDir:r,deploymentTarget:i,databaseProvider:s,cacheProvider:a,emailProvider:d,dockerServices:h};y.paymentProvider==="none"&&(y.credits=!1);for(let f of Ve){if(y.deploymentTarget!==f.target)continue;let S=y.databaseProvider===f.provider?"database":"cache";if(y.databaseProvider===f.provider||y.cacheProvider===f.provider)throw new Error(`Incompatible: --deploy ${f.target} + --${S} ${f.provider}. ${f.reason}`)}for(let f of Me)if(y.architecture===f.architecture&&y.deploymentTarget===f.target)throw new Error(`Incompatible: --architecture ${f.architecture} + --deploy ${f.target}. ${f.reason}`);return y}function Jt(e,t,r){if(e.trim()==="")return[];let n=e.split(",").map(o=>o.trim()).filter(Boolean),i=n.filter(o=>!t.includes(o));if(i.length>0)throw new Error(`Invalid ${r}(s): ${i.join(", ")}. Valid values: ${t.join(", ")}`);return n}import is from"picocolors";var os="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";function ss(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 Rn(e){e.command("init").description("Scaffold a new GenerateSaaS project").argument("[apiKey]","license key (same as --api-key)").option("-n, --name <name>","project name (lowercase, hyphens, starts with letter)").option("--app-name <name>","display name for the app").option("-l, --location <path>","project directory (default: ./{name})").addOption(new M("--frontend <type>","frontend framework").choices([...Fe])).addOption(new M("--architecture <type>","fullstack or separate").choices([...et])).addOption(new M("--payment <provider>","payment provider").choices([...tt])).addOption(new M("--email <provider>","email provider").choices([...rt])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").addOption(new M("--billing-scope <scope>","billing scope (requires --org)").choices([...ot])).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("--desktop","include the Electron desktop app (apps/desktop)").option("--no-desktop","exclude the Electron desktop app").option("--revenue-sharing","enable revenue sharing").option("--no-revenue-sharing","disable revenue sharing").option("--credits","enable credits system").option("--no-credits","disable credits system (subscription-only)").option("--docker <services>","comma-separated: postgres,redis,inngest,mailpit").option("--ai-tools <tools>","comma-separated: claude-code,cursor,codex,gemini-cli,windsurf").option("--social-providers <providers>","comma-separated: google,github,facebook,discord,x").addOption(new M("--currency <code>","default currency for billing").choices([...le])).addOption(new M("--deploy <target>","deployment target").choices([...pe])).addOption(new M("--database <provider>","database provider").choices([...de])).addOption(new M("--cache <provider>","cache provider").choices([...ue])).option("--template-version <version>","specific template version to scaffold").option("--api-key <key>","API key (skips interactive prompt)").option("--base-url <url>","public base URL (e.g. https://example.com) - bakes into canonical/og/sitemap").option("-y, --yes","accept defaults for unspecified options (non-interactive)").addOption(new M("--demo","first-party demo build: keep sample content, mark site non-indexable - requires CI API key").hideHelp()).addOption(new M("--no-db-migration","skip generating the baseline DB migration (internal: demos/CI/playground)").hideHelp()).action(async(t,r)=>{await as(t?{...r,apiKey:t}:r)})}async function as(e){let t=performance.now();er("1.15.2");let r,n;try{r=Pn(e),n=ss(e.templateVersion)}catch(u){E.cancel(I(u)),process.exit(1)}let i=E.spinner(),o;try{o=await Ae({apiKey:e.apiKey,prompt:!e.yes})}catch(u){E.cancel(I(u)),process.exit(1)}e.demo&&Bt(o)!==os&&(E.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let s=X(o),a=async()=>{let u=await ne(s),b=u.latest,J=n??b;if(n&&!u.versions.some(se=>se.version===J))throw new Error(`Template version "${n}" is not available.`);return{latestVersion:b,selectedVersion:J}};i.start("Verifying access...");let d,h;try{({latestVersion:d,selectedVersion:h}=await a()),i.stop("Access verified."),fe(o)}catch(u){if(i.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 Ke(),s=X(o),i.start("Verifying access...");try{({latestVersion:d,selectedVersion:h}=await a()),i.stop("Access verified."),fe(o)}catch(b){i.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}`),h!==d&&E.log.success(`Using template version: ${h}`);let y;e.yes?y=_n(r):y=await nr(r);let f;i.start("Activating license...");try{let u=crypto.randomUUID(),b=()=>({frontend:y.frontend,version:h,installId:u,projectName:y.projectName,options:cr(y)}),J;try{J=await Rt(s,b())}catch(se){let G=dt(se);if(!G?.lastAllowedVersion)throw se;i.stop("License activation failed."),e.yes&&(E.cancel(`${G.message} Re-run with --template-version ${G.lastAllowedVersion}.`),process.exit(1));let be=await E.confirm({message:`Your update window has ended. Continue with v${G.lastAllowedVersion} (the last version your license covers)?`});(E.isCancel(be)||!be)&&(E.cancel("Setup cancelled."),process.exit(0)),h=G.lastAllowedVersion,i.start(`Activating license for v${h}...`),J=await Rt(s,b())}f={token:J.token,keyHash:Bt(o),installId:u},i.stop("License activated.")}catch(u){i.stop("License activation failed."),E.cancel(I(u)),process.exit(1)}let S=ns(y.projectDir);if(Qo(S)&&es(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"&&ts(S,{recursive:!0,force:!0})}let T={...y,projectDir:S,version:h,...e.demo?{docs:!1}:{}};i.start("Downloading template...");try{await ut(s,h,S),i.stop("Template downloaded.")}catch(u){i.stop("Download failed."),E.cancel(I(u)),process.exit(1)}let oe;i.start("Generating project files...");try{if({dockerComposeGenerated:oe}=await Et(T),!e.demo){let u=await De(S);await l(rs(S,lt),JSON.stringify(u,null," ")+`
1157
- `),await hn(S,S)}await Sn(S,T.aiTools),await fn(T,f),i.stop("Project files generated.")}catch(u){i.stop("Generation failed."),E.cancel(I(u)),process.exit(1)}await wn(S);let x=await bn(S);x&&T.demo!==!0&&e.dbMigration!==!1&&await kn(S),await An(S),x&&await Tn(S);let j=ve("docker"),Q=Pt(T).map(u=>u.key).filter(u=>!T.credentials?.[u]);In(T,{pnpmInstalled:x,dockerComposeGenerated:oe,dockerAvailable:j,skippedCredentials:Q}),tr(),E.log.info(is.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as xn}from"fs";import{readFile as Dn}from"fs/promises";import{join as Ye,resolve as us}from"path";import*as _ from"@clack/prompts";import Ne from"picocolors";import{mkdtemp as cs,rm as ls}from"fs/promises";import{tmpdir as ps}from"os";import{join as ds}from"path";async function Wt(e,t,r,n){let i=await cs(ds(ps(),"generatesaas-stage-"));try{await ut(e,t,i),await Et({...r,projectDir:i}),await Gt(i,n)}finally{await ls(i,{recursive:!0,force:!0})}}function On(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 bt(e,t){let r=On(e),n=On(t);if(!r||!n)return 0;for(let i=0;i<3;i++)if(r[i]!==n[i])return r[i]-n[i];return 0}function Cn(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=us(t.cwd??process.cwd()),n=Ye(r,ee),i;try{i=JSON.parse(await Dn(n,"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 ne(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."),fe(o),a.start("Fetching latest skill files...");let h=await ar(s,d.latest);await Ht(r,h.skillMd,h.scripts,i.aiTools);let y=zt(i.aiTools);if(a.stop("Skills updated."),_.log.success(`Skill files installed to ${Ne.cyan(y.length.toString())} locations.`),i.version===d.latest){_.log.info(`Already on the latest version (${i.version}).`);return}if(i.licenseToken)try{let u=await lr(s,{currentToken:i.licenseToken,newVersion:d.latest});i.licenseToken=u.token,u.licenseKeyHash&&(i.licenseKeyHash=u.licenseKeyHash),await l(n,JSON.stringify(i,null," ")+`
1158
- `),_.log.success("License refreshed.")}catch(u){let b=dt(u);b&&(_.cancel(b.message),process.exit(1)),_.log.warn("License refresh skipped.")}let f=mn(i,r),S=Ye(r,or);a.start(`Staging v${d.latest} (shaped for your config)...`),await Wt(s,d.latest,f,S),a.stop("Template staged.");let{text:T,title:oe}=await ms(s,d,i.version);T&&_.note(T,oe);let x=Ye(r,lt),j=Ye(r,pt),we=!xn(j),Q=!xn(x);if(we){if(a.start("Building baseline template (one-time migration)..."),await Wt(s,i.version,f,j),Q){let u=await De(j);await l(x,JSON.stringify(u,null," ")+`
1159
- `)}if(a.stop("Baseline template stored."),!Q){let u=await fs(x,j);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(Q){a.start("Computing baseline template hashes...");let u=await De(j);await l(x,JSON.stringify(u,null," ")+`
1160
- `),a.stop("Baseline hashes computed.")}if(await l(Ye(r,sr),JSON.stringify({currentVersion:i.version,targetVersion:d.latest,changelog:T,stagedAt:new Date().toISOString()},null," ")+`
1161
- `),_.log.info(`Update staged: ${Ne.cyan(i.version)} \u2192 ${Ne.cyan(d.latest)}`),i.aiTools&&i.aiTools.length>0){let u=i.aiTools[0],b=Ue[u].label;_.log.info(`Open your project in ${Ne.cyan(b)} and ask: ${Ne.cyan("'update my GenerateSaaS project'")}`)}else _.log.info(`Ask your AI coding assistant to ${Ne.cyan("'update my GenerateSaaS project'")}.`)}catch(d){a.stop("Failed."),_.cancel(`Update failed: ${I(d)}`),process.exit(1)}})}async function ms(e,t,r){let n=t.latest,i=t.versions.filter(s=>bt(s.version,r)>0&&bt(s.version,n)<=0).sort((s,a)=>bt(s.version,a.version));if(i.length<=1)return{text:await _t(e,n),title:`Changelog v${n}`};let o=[];for(let s of i){let a=null;try{a=await _t(e,s.version)}catch{a=null}let d=s.date?` (${s.date.slice(0,10)})`:"",h=s.breaking?" [BREAKING]":"";o.push(`# v${s.version}${d}${h}
1162
-
1163
- ${a??"_No changelog available for this release._"}`)}return{text:o.join(`
1164
-
1165
- `),title:`Changelog v${r} \u2192 v${n}`}}async function fs(e,t){let r=JSON.parse(await Dn(e,"utf-8")),n=await De(t),i=0;for(let[o,s]of Object.entries(r))Te(o)||n[o]!==s&&i++;for(let o of Object.keys(n))o in r||i++;return i}import*as $ from"@clack/prompts";import F from"picocolors";import{readFile as gs}from"fs/promises";import{join as hs,resolve as ys}from"path";function Nn(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=ys(t.cwd??process.cwd()),n=hs(r,ee),i;try{i=JSON.parse(await gs(n,"utf-8"))}catch{$.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let o=[`Version: ${F.cyan(i.version)}`,`Frontend: ${F.cyan(i.frontend)}`,i.deploymentTarget?`Deploy target: ${F.cyan(i.deploymentTarget)}`:null,i.databaseProvider?`Database: ${F.cyan(i.databaseProvider)}`:null,i.cacheProvider?`Cache: ${F.cyan(i.cacheProvider)}`:null,i.aiTools&&i.aiTools.length>0?`AI tools: ${F.cyan(i.aiTools.join(", "))}`:null].filter(Boolean).join(`
1166
- `);$.note(o,F.bold("Project Status"));let s=$.spinner();s.start("Checking for updates...");try{let a=await Ae(),d=X(a),y=(await ne(d)).latest;i.version===y?(s.stop("Up to date."),$.log.success(`Already on the latest version (${F.green(y)})`)):(s.stop("Update available."),$.log.warning(`Update available: ${F.yellow(i.version)} \u2192 ${F.green(y)}`),$.log.info(`Open this project in your AI coding agent and ask it to ${F.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?$.log.warning("Invalid API key. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):$.log.warning(`Could not check for updates: ${I(a)}`)}})}import{readFile as vs}from"fs/promises";import*as A from"@clack/prompts";import k from"picocolors";function Ss(){return process.env.GENERATESAAS_API_KEY??Ge()}function Es(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 ws(e){return{verdict:e.verdict??"unknown",ejectedAt:e.ejectedAt??null}}function bs(e){switch(e.verdict){case"licensed":return A.log.success(`${k.green("LICENSED")} - resolves to an account with an active${e.plan?` ${e.plan}`:""} license.`),!0;case"ejected":return A.log.success(`${k.green("EJECTED")} - a licensed buyer opted this install out of telemetry${e.ejectedAt?` on ${e.ejectedAt.slice(0,10)}`:""}. The site is legitimate.`),!0;case"revoked":return A.log.error(`${k.red("REVOKED")} - the owning account no longer holds a plan (refund or chargeback). This deployment is no longer licensed.`),!1;case"token_domain_mismatch":return A.log.error(`${k.red("LEAKED TOKEN")} - this license belongs to a different deployment${e.mismatchDomain?` (${k.cyan(e.mismatchDomain)})`:""}, not this site. The token was copied from a licensed project.`),!1;case"no_license_history":return A.log.error(`${k.red("NO LICENSE HISTORY")} - no license has ever been associated with this site. If it runs GenerateSaaS, treat it as unlicensed.`),!1;default:return A.log.warn(`${k.yellow("UNKNOWN")} - could not cross-reference the records right now. Try again shortly.`),!1}}async function Ln(e,t){let r=process.env.GENERATESAAS_API_URL??Be,n=Ss();e.start("Cross-referencing license records...");try{let i=n?Es(await pr(r,n,{lkh:t.lkh,nid:t.nid,domain:t.domain})):ws(await Ot(r,{token:t.token,domain:t.domain}));return e.stop(`${k.green("Checked")} - records cross-referenced`),bs(i)}catch(i){return e.stop(`${k.yellow("Skipped")} - ${I(i)}`),null}}function ks(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 qt(e){return typeof e!="number"?"unknown":new Date(e*1e3).toISOString().split("T")[0]}function $n(e){A.note([`License ID: ${k.cyan(String(e.lid??"unknown"))}`,`Version: ${k.cyan(String(e.ver??"unknown"))}`,`Init version: ${String(e.iver??"unknown")}`,`Frontend: ${String(e.fe??"unknown")}`,`Created: ${qt(e.pat)}`,`Last updated: ${qt(e.uat)}`,`Expires: ${qt(e.exp)}`,`Install ID: ${String(e.nid??"unknown")}`].join(`
1167
- `),k.yellow("License Details"))}function As(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 jn(e){let t=A.spinner(),r=null,n="no candidates";for(let s of As(e)){t.start(`Checking ${s}...`);try{let a=await fetch(s);if(!a.ok){n=`${s} returned ${a.status}`,t.stop(`${k.yellow("Not here")} - ${n}`);continue}let d=(await a.text()).trim();if(!d||d.split(".").length!==3){n=`${s} did not return a JWT`,t.stop(`${k.yellow("Not here")} - ${n}`);continue}r=d,t.stop(`${k.green("Found")} - license endpoint responded`);break}catch(a){n=`${s}: ${I(a)}`,t.stop(`${k.yellow("Unreachable")} - ${n}`)}}if(r===null){A.log.warn(`No license endpoint found (last: ${n}). The site may be ejected, not a GenerateSaaS app, or serving its API elsewhere.`);let s=Un(e);return s?await Ln(t,{domain:s})??!1:!1}let i;try{i=ks(r)}catch{return A.log.error("Could not decode JWT payload."),!1}t.start("Verifying signature...");try{let s=process.env.GENERATESAAS_API_URL??Be,a=await Ot(s,{token:r});if(a.valid)t.stop(`${k.green("Valid")} - signature verified`);else return t.stop(`${k.red("Invalid")} - ${a.reason}`),!1}catch{return t.stop(`${k.yellow("Skipped")} - could not reach verification service`),A.log.warn("Signature not verified. Displaying unverified claims:"),$n(i),!1}return $n(i),await Ln(t,{token:r,lkh:typeof i.lkh=="string"?i.lkh:void 0,nid:typeof i.nid=="string"?i.nid:void 0,domain:Un(e)})??!0}function Un(e){try{return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`).hostname}catch{return}}function Vn(e){e.command("verify").description("Verify a GenerateSaaS license on a deployed site").argument("[url]","URL of the site to verify (e.g. https://example.com or https://example.com/api)").option("--file <path>","file with URLs to check, one per line").action(async(t,r)=>{if(!t&&!r.file&&(A.cancel("Provide a URL or --file <path>."),process.exit(1)),r.file){let i=(await vs(r.file,"utf-8")).split(`
1168
- `).map(s=>s.trim()).filter(s=>s&&!s.startsWith("#"));i.length===0&&(A.cancel("No URLs found in file."),process.exit(1));let o=0;for(let s of i)await jn(s)&&o++,A.log.info("");A.log.success(`${o}/${i.length} sites verified.`)}else await jn(t)||process.exit(1)})}import{existsSync as Ts,rmSync as Is}from"fs";import*as B from"@clack/prompts";function Mn(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){Ts(q)?(Is(q),B.log.success("API key removed.")):B.log.info("No API key configured.");return}let r=Ge();r?B.log.info(`Current API key: ****${r.slice(-4)}`):B.log.info("No API key configured.");let n=await Ke(),i=X(n),o=B.spinner();o.start("Verifying API key...");try{await ne(i),o.stop("API key verified."),fe(n),B.log.success("API key saved.")}catch(s){o.stop("Verification failed."),s instanceof R&&s.status===401?B.cancel("Invalid API key."):B.cancel(I(s)),process.exit(1)}})}import{existsSync as kt,rmSync as Ps,readFileSync as Zt,writeFileSync as Fn}from"fs";import{join as Se}from"path";import*as O from"@clack/prompts";var _s=["packages/api/src/functions/maintenance/license-heartbeat.ts","packages/api/src/lib/cron-spread.ts","packages/api/src/lib/manifest.ts","packages/api/src/routes/internal/license.ts"],Rs=[{file:"packages/api/src/routes/inngest.ts",removals:[`import { licenseHeartbeatFunction } from "../functions/maintenance/license-heartbeat";
1176
+ `;await l(wt(t,"providers/index.ts"),n),await l(wt(t,"index.ts"),await Di(t,r))}async function Di(e,t){let r=wt(e,"index.ts");if(!Oi(r))throw new Error(`generatePaymentBarrel: expected ${r} to exist - did packages/payments move?`);let n=await Ri(r,"utf-8"),o=xi.filter(s=>s!==t);return n.split(`
1177
+ `).filter(s=>!o.some(a=>s.includes(`./providers/${a}/`))).join(`
1178
+ `)}import{readdir as Ci,readFile as Ni}from"fs/promises";import{join as pn}from"path";async function dn(e){if(e.demo)return;let t=e.appName.trim()||e.projectName,r=JSON.stringify(t).slice(1,-1),n=pn(e.projectDir,"packages/i18n/translations"),o;try{o=await Ci(n)}catch{return}for(let i of o){let s=pn(n,i,"web.json"),a;try{a=await Ni(s,"utf-8")}catch{continue}let d=a.replaceAll("GenerateSaaS",r);d!==a&&await l(s,d)}}import{readFile as Li}from"fs/promises";import{join as un}from"path";var $i=[".nuxt/",".nuxt",".nitro/",".nitro",".output/",".output","_locales/"],Ui=[".next/",".next",".svelte-kit/",".svelte-kit",".wrangler/",".wrangler",".dev.vars"];async function fn(e){let r=e.frontend==="nuxt"?Ui:$i;await mn(un(e.projectDir,".gitignore"),r),await mn(un(e.projectDir,".dockerignore"),r)}async function mn(e,t){let r;try{r=await Li(e,"utf-8")}catch{return}let n=new Set(t),o=r.split(`
1179
+ `).filter(i=>!n.has(i.trim()));o.length!==r.split(`
1180
+ `).length&&await l(e,o.join(`
1181
+ `))}async function bt(e){let t=e.projectDir;e.demo||(await vr(t),await Sr(t),await Er(t)),await wr(t,e.aiTools),await br(t,e.frontend),await Ar(e),await Ir(e),await Tr(e),e.demo||await Pr(e),await Or(e);let r=await xr(e);return await Dr(e),await Cr(e),await Nr(e),await Lr(e),await $r(e),await Ur(e),await Fr(e),await Br(e),await zr(e),await Yr(e),await Wr(e),await on(e),await qr(e),await Xr(e),await Zr(e),await Qr(e),await en(e),await nn(e),await tn(e),await cn(e),await ln(e),await dn(e),await fn(e),{dockerComposeGenerated:r}}import{basename as hn,join as yn,relative as Vi}from"path";import{createHash as gn}from"crypto";import{readFile as ji}from"fs/promises";async function kt(e){let t=await ji(e);return gn("sha256").update(t).digest("hex")}function zt(e){return gn("sha256").update(e).digest("hex")}var Mi=new Set(["data",M]);function Fi(e){let t=e.split("/");for(let r of t)if(Nt.has(r)||Lt.has(r)||Mi.has(r)||r.startsWith(".env")&&!r.includes("example"))return!0;return!1}function vn(e,t){return{projectName:e.projectName??hn(t),appName:e.appName??e.projectName??hn(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,desktop:e.desktop??!1,desktopAutoRelease:e.desktopAutoRelease??!0,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 Sn(e,t){let n=(await ve(e.projectDir,e.projectDir,Fi)).sort(),o=await Promise.all(n.map(async a=>[ye(Vi(e.projectDir,a)),await kt(a)])),i=Object.fromEntries(o),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,desktop:e.desktop,desktopAutoRelease:e.desktopAutoRelease,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(yn(e.projectDir,re),JSON.stringify(s,null," ")+`
1182
+ `),await l(yn(e.projectDir,ar),JSON.stringify(i,null," ")+`
1183
+ `)}import{relative as Bi}from"path";async function De(e){let r=(await ve(e,e,Ie)).sort(),n=await Promise.all(r.map(async o=>[ye(Bi(e,o)),await kt(o)]));return Object.fromEntries(n)}import{copyFile as Gi,mkdir as Ki,rm as zi}from"fs/promises";import{dirname as Hi,join as En,relative as Yi}from"path";import{existsSync as Ji}from"fs";async function Ht(e,t){Ji(t)&&await zi(t,{recursive:!0,force:!0});let r=await ve(e,e,Ie);for(let n of r){let o=ye(Yi(e,n)),i=En(t,o);await Ki(Hi(i),{recursive:!0}),await Gi(n,i)}}async function wn(e,t){await Ht(e,En(t,ut))}import{existsSync as Wi}from"fs";import{readFile as bn,readdir as qi}from"fs/promises";import{join as W,dirname as Xi,resolve as Zi,sep as Qi}from"path";import{fileURLToPath as es}from"url";var Ye={"claude-code":".claude/skills",cursor:".cursor/skills",codex:".agents/skills","gemini-cli":".gemini/skills",windsurf:".windsurf/skills"},cp=Object.values(Ye),Yt="generatesaas-update",kn=Xi(es(import.meta.url));function ts(){let e=W(kn,"skill","content");return Wi(e)?e:W(kn,"content")}function Jt(e){return!e||e.length===0?[]:e.map(t=>Ye[t])}async function Wt(e,t,r,n){let o=Jt(n);for(let i of o){let s=W(e,i,Yt),a=W(s,"scripts"),d=W(s,"references");await ht(a),await ht(d),await l(W(s,"SKILL.md"),t.replaceAll("__SKILL_ROOT__",i)),await l(W(d,".gitkeep"),"");for(let[g,h]of Object.entries(r)){let m=Zi(a,g);m.startsWith(a+Qi)&&await l(m,h)}}}async function An(e,t){let r=ts(),n=await bn(W(r,"SKILL.md"),"utf-8"),o=W(r,"scripts"),i=await qi(o),s={};for(let a of i)a!==".gitkeep"&&(s[a]=await bn(W(o,a),"utf-8"));await Wt(e,n,s,t)}import{execFile as rs,execFileSync as ns}from"child_process";import{access as In,readFile as os}from"fs/promises";import{join as qt}from"path";import*as T from"@clack/prompts";function Ee(e){try{let t=process.platform==="win32"?"where":"which";return ns(t,[e],{stdio:"ignore"}),!0}catch{return!1}}function Se(e,t,r,n=3e5){return new Promise((o,i)=>{rs(e,t,{cwd:r,timeout:n},(s,a,d)=>{if(s){let g=String(a||"").trim(),m=[String(d||"").trim(),g].filter(Boolean).join(`
1184
+ `);i(new Error(m?`${s.message}
1185
+ ${m}`:s.message))}else o()})})}async function Tn(e){if(!Ee("pnpm"))return T.log.warn("pnpm not found. Skipping lockfile regeneration."),!1;try{return await Se("pnpm",["install","--lockfile-only","--no-frozen-lockfile","--config.minimumReleaseAge=0"],e),!0}catch(t){let r=t instanceof Error?t.message:String(t);return T.log.warn(`Lockfile regeneration failed: ${r}`),T.log.warn("Deploys using --frozen-lockfile may fail."),!1}}async function Pn(e){if(!Ee("pnpm"))return T.log.warn("pnpm not found. Skipping dependency installation."),T.log.info("Install pnpm: https://pnpm.io/installation"),!1;let t=T.spinner();t.start("Installing dependencies (this may take a minute)...");try{return await Se("pnpm",["install","--config.minimumReleaseAge=0"],e),t.stop("Dependencies installed."),!0}catch(r){t.stop("Dependency installation failed.");let n=r instanceof Error?r.message:String(r);return T.log.warn(`pnpm install failed: ${n}`),T.log.warn("You can run it manually later."),!1}}async function _n(e){if(!Ee("pnpm"))return!1;let t=T.spinner();t.start("Generating baseline database migration...");try{return await Se("pnpm",["-F","@repo/database","generate"],e),t.stop("Baseline migration generated."),!0}catch(r){t.stop("Baseline migration generation failed.");let n=r instanceof Error?r.message:String(r);return T.log.warn(`Could not generate baseline migration: ${n}`),T.log.warn("Run 'pnpm -F @repo/database generate' before your first deploy."),!1}}async function Rn(e){try{return await In(qt(e,".git")),T.log.info("Git repository already exists, skipping init."),!0}catch{}if(!Ee("git"))return T.log.warn("git not found. Skipping repository initialization."),!1;let t=T.spinner();t.start("Initializing git repository...");try{return await Se("git",["init"],e),await Se("git",["add","-A"],e),await Se("git",["commit","--no-verify","-m","Initial commit from GenerateSaaS"],e),t.stop("Git repository initialized."),!0}catch{return t.stop("Git initialization failed."),T.log.warn("You can run git init manually later."),!1}}async function On(e){if(!Ee("pnpm"))return!1;try{await In(qt(e,".git"))}catch{return!1}try{let t=JSON.parse(await os(qt(e,"package.json"),"utf-8")),r=!!t.devDependencies?.["simple-git-hooks"],n=!!t["simple-git-hooks"];if(!r||!n)return!1}catch{return!1}try{return await Se("pnpm",["exec","simple-git-hooks"],e),!0}catch{return T.log.warn("Could not install git hooks. Run 'pnpm exec simple-git-hooks' manually."),!1}}import*as Ce from"@clack/prompts";import F from"picocolors";function xn(e,t){t.dockerComposeGenerated&&!t.dockerAvailable&&Ce.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 i=e.dockerServices.map(s=>ke[s].label).join(", ");r.push(`pnpm infra ${F.dim(`# ${i}`)}`)}if(r.push(`pnpm dev ${F.dim("# http://localhost:3000")}`),t.skippedCredentials.length>0&&(r.push(""),r.push(F.dim("Fill in remaining TODO values in .env"))),Ce.note(r.join(`
1186
+ `),F.yellow("Start Development")),t.dockerComposeGenerated){let i=[];i.push(`App ${F.cyan("http://localhost:3000")}`),e.architecture==="separate"&&i.push(`API ${F.cyan("http://localhost:3010")}`),e.dockerServices.includes("mailpit")&&i.push(`Mailpit ${F.cyan("http://localhost:8025")}`),e.dockerServices.includes("inngest")&&i.push(`Inngest ${F.cyan("http://localhost:8288")}`),Ce.note(i.join(`
1187
+ `),F.yellow("Dev Tools"))}let n=[],o=is(e);o.length>0&&n.push(`Set in production: ${F.dim(o.join(", "))}`),n.push("pnpm db:push # Run database migrations"),n.push(ss(e)),Ce.note(n.join(`
1188
+ `),F.yellow("Deployment"))}function is(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 ss(e){switch(e.deploymentTarget){case"node":return"Deploy with Docker or your preferred Node.js host";case"vercel":return"vercel deploy # Deploy to Vercel"}}function Dn(e){let t={};if(e.name!==void 0){if(!pt(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(!Be.includes(e.frontend))throw new Error(`Invalid frontend "${e.frontend}". Valid values: ${Be.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.desktop!==void 0&&(t.desktop=e.desktop),e.desktopAuto!==void 0&&(t.desktopAutoRelease=e.desktopAuto),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=Xt(e.docker,it,"docker service")),e.aiTools!==void 0&&(t.aiTools=Xt(e.aiTools,st,"AI tool")),e.socialProviders!==void 0&&(t.socialProviders=Xt(e.socialProviders,ct,"social provider")),e.currency!==void 0){if(!de.includes(e.currency))throw new Error(`Invalid currency "${e.currency}". Valid values: ${de.join(", ")}`);t.defaultCurrency=e.currency}if(e.deploy!==void 0){if(!ue.includes(e.deploy))throw new Error(`Invalid deployment target "${e.deploy}". Valid values: ${ue.join(", ")}`);t.deploymentTarget=e.deploy}if(e.database!==void 0){if(!me.includes(e.database))throw new Error(`Invalid database provider "${e.database}". Valid values: ${me.join(", ")}`);t.databaseProvider=e.database}if(e.cache!==void 0){if(!fe.includes(e.cache))throw new Error(`Invalid cache provider "${e.cache}". Valid values: ${fe.join(", ")}`);t.cacheProvider=e.cache}if(e.demo===!0&&(t.demo=!0),e.baseUrl!==void 0){let r=e.baseUrl.trim();if(r==="")throw new Error("--base-url cannot be empty. Provide an absolute URL like https://example.com.");let n;try{n=new URL(r)}catch{throw new Error(`Invalid --base-url "${e.baseUrl}". Must be an absolute URL like https://example.com.`)}if(n.protocol!=="http:"&&n.protocol!=="https:")throw new Error(`Invalid --base-url "${e.baseUrl}". Must use http or https.`);t.baseUrl=`${n.protocol}//${n.host}`}return t}var se={projectName:"my-saas",frontend:"nextjs",architecture:"fullstack",paymentProvider:"stripe",emailProvider:"smtp",multiTenancy:!1,billingScope:"user",blog:!0,docs:!1,desktop:!1,desktopAutoRelease:!1,revenueSharing:!1,credits:!0,dockerServices:["postgres","redis","inngest"],aiTools:[],socialProviders:[],defaultCurrency:"USD",deploymentTarget:"node",databaseProvider:"postgres",cacheProvider:"redis"};function Cn(e){let t=e.projectName??se.projectName,r=e.projectDir??`./${t}`,n=e.appName??lt(t),o=e.deploymentTarget??se.deploymentTarget,i=H[o]?.edgeRuntime??!1,s=e.databaseProvider??(i?"neon":se.databaseProvider),a=e.cacheProvider??(i?"upstash":se.cacheProvider),d=e.emailProvider??(i?"resend":se.emailProvider),g=e.dockerServices??(i?se.dockerServices.filter(m=>m!=="postgres"&&m!=="redis"):se.dockerServices),h={...se,...e,projectName:t,appName:n,projectDir:r,deploymentTarget:o,databaseProvider:s,cacheProvider:a,emailProvider:d,dockerServices:g};h.paymentProvider==="none"&&(h.credits=!1);for(let m of Me){if(h.deploymentTarget!==m.target)continue;let _=h.databaseProvider===m.provider?"database":"cache";if(h.databaseProvider===m.provider||h.cacheProvider===m.provider)throw new Error(`Incompatible: --deploy ${m.target} + --${_} ${m.provider}. ${m.reason}`)}for(let m of Fe)if(h.architecture===m.architecture&&h.deploymentTarget===m.target)throw new Error(`Incompatible: --architecture ${m.architecture} + --deploy ${m.target}. ${m.reason}`);return h}function Xt(e,t,r){if(e.trim()==="")return[];let n=e.split(",").map(i=>i.trim()).filter(Boolean),o=n.filter(i=>!t.includes(i));if(o.length>0)throw new Error(`Invalid ${r}(s): ${o.join(", ")}. Valid values: ${t.join(", ")}`);return n}import us from"picocolors";var ms="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";function fs(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 Nn(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 B("--frontend <type>","frontend framework").choices([...Be])).addOption(new B("--architecture <type>","fullstack or separate").choices([...rt])).addOption(new B("--payment <provider>","payment provider").choices([...nt])).addOption(new B("--email <provider>","email provider").choices([...ot])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").addOption(new B("--billing-scope <scope>","billing scope (requires --org)").choices([...at])).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("--desktop","include the Electron desktop app (apps/desktop)").option("--no-desktop","exclude the Electron desktop app").option("--desktop-auto","auto-trigger the desktop release workflow on CI success (default: manual-only)").option("--no-desktop-auto","manual-only desktop releases (workflow_dispatch)").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 B("--currency <code>","default currency for billing").choices([...de])).addOption(new B("--deploy <target>","deployment target").choices([...ue])).addOption(new B("--database <provider>","database provider").choices([...me])).addOption(new B("--cache <provider>","cache provider").choices([...fe])).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 B("--demo","first-party demo build: keep sample content, mark site non-indexable - requires CI API key").hideHelp()).addOption(new B("--no-db-migration","skip generating the baseline DB migration (internal: demos/CI/playground)").hideHelp()).action(async(t,r)=>{await gs(t?{...r,apiKey:t}:r)})}async function gs(e){let t=performance.now();nr("1.17.0");let r,n;try{r=Dn(e),n=fs(e.templateVersion)}catch(y){S.cancel(I(y)),process.exit(1)}let o=S.spinner(),i;try{i=await Ae({apiKey:e.apiKey,prompt:!e.yes})}catch(y){S.cancel(I(y)),process.exit(1)}e.demo&&zt(i)!==ms&&(S.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let s=Z(i),a=async()=>{let y=await ie(s),O=y.latest,V=n??O;if(n&&!y.versions.some(ce=>ce.version===V))throw new Error(`Template version "${n}" is not available.`);return{latestVersion:O,selectedVersion:V,desktopAllowed:y.entitlements?.desktopAllowed??!0}};o.start("Verifying access...");let d,g,h=!0;try{({latestVersion:d,selectedVersion:g,desktopAllowed:h}=await a()),o.stop("Access verified."),he(i)}catch(y){if(o.stop("Access verification failed."),y instanceof x&&y.status===401){e.yes&&(S.cancel("Invalid API key. Cannot prompt in non-interactive mode."),process.exit(1)),S.log.warning("Invalid API key."),i=await ze(),s=Z(i),o.start("Verifying access...");try{({latestVersion:d,selectedVersion:g,desktopAllowed:h}=await a()),o.stop("Access verified."),he(i)}catch(O){o.stop("Access verification failed."),S.cancel(O instanceof x&&O.status===401?"Invalid API key.":I(O)),process.exit(1)}}else S.cancel(I(y)),process.exit(1)}S.log.success(`Latest version: ${d}`),g!==d&&S.log.success(`Using template version: ${g}`),r.desktop===!0&&!h&&(S.cancel("The desktop app is available on the Pro plan and up. Upgrade your plan at generatesaas.com."),process.exit(1));let m;e.yes?m=Cn(h?r:{...r,desktop:!1}):m=await sr(r,{desktopAllowed:h});let _;o.start("Activating license...");try{let y=crypto.randomUUID(),O=()=>({frontend:m.frontend,version:g,installId:y,projectName:m.projectName,options:dr(m)}),V;try{V=await Dt(s,O())}catch(ce){let te=mt(ce);if(!te?.lastAllowedVersion)throw ce;o.stop("License activation failed."),e.yes&&(S.cancel(`${te.message} Re-run with --template-version ${te.lastAllowedVersion}.`),process.exit(1));let Le=await S.confirm({message:`Your update window has ended. Continue with v${te.lastAllowedVersion} (the last version your license covers)?`});(S.isCancel(Le)||!Le)&&(S.cancel("Setup cancelled."),process.exit(0)),g=te.lastAllowedVersion,o.start(`Activating license for v${g}...`),V=await Dt(s,O())}_={token:V.token,keyHash:zt(i),installId:y},o.stop("License activated.")}catch(y){o.stop("License activation failed."),S.cancel(I(y)),process.exit(1)}let E=ds(m.projectDir);if(as(E)&&cs(E).length>0)if(e.yes)S.log.info(`Directory ${E} is not empty. Merging (keeping existing files, overwriting conflicts).`);else{let O=await S.select({message:`Directory ${E} 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"}]});(S.isCancel(O)||O==="cancel")&&(S.cancel("Setup cancelled."),process.exit(0)),O==="overwrite"&&ls(E,{recursive:!0,force:!0})}let R={...m,projectDir:E,version:g,...e.demo?{docs:!1}:{}};o.start("Downloading template...");try{await ft(s,g,E),o.stop("Template downloaded.")}catch(y){o.stop("Download failed."),S.cancel(I(y)),process.exit(1)}let z;o.start("Generating project files...");try{if({dockerComposeGenerated:z}=await bt(R),!e.demo){let y=await De(E);await l(ps(E,dt),JSON.stringify(y,null," ")+`
1189
+ `),await wn(E,E)}await An(E,R.aiTools),await Sn(R,_),o.stop("Project files generated.")}catch(y){o.stop("Generation failed."),S.cancel(I(y)),process.exit(1)}await Tn(E);let D=await Pn(E);D&&R.demo!==!0&&e.dbMigration!==!1&&await _n(E),await Rn(E),D&&await On(E);let ae=Ee("docker"),b=Ot(R).map(y=>y.key).filter(y=>!R.credentials?.[y]);xn(R,{pnpmInstalled:D,dockerComposeGenerated:z,dockerAvailable:ae,skippedCredentials:b}),or(),S.log.info(us.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as $n}from"fs";import{readFile as Un}from"fs/promises";import{join as Je,resolve as Es}from"path";import*as P from"@clack/prompts";import Ne from"picocolors";import{mkdtemp as hs,rm as ys}from"fs/promises";import{tmpdir as vs}from"os";import{join as Ss}from"path";async function Zt(e,t,r,n){let o=await hs(Ss(vs(),"generatesaas-stage-"));try{await ft(e,t,o),await bt({...r,projectDir:o}),await Ht(o,n)}finally{await ys(o,{recursive:!0,force:!0})}}function Ln(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 At(e,t){let r=Ln(e),n=Ln(t);if(!r||!n)return 0;for(let o=0;o<3;o++)if(r[o]!==n[o])return r[o]-n[o];return 0}function jn(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=Es(t.cwd??process.cwd()),n=Je(r,re),o;try{o=JSON.parse(await Un(n,"utf-8"))}catch{P.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let i;try{i=await Ae()}catch(d){P.cancel(I(d)),process.exit(1)}let s=Z(i),a=P.spinner();try{a.start("Verifying access...");let d;try{d=await ie(s)}catch(b){throw b instanceof x&&b.status===401?new Error("Your saved API key was rejected. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):b}a.stop("Access verified."),he(i),a.start("Fetching latest skill files...");let g=await pr(s,d.latest);await Wt(r,g.skillMd,g.scripts,o.aiTools);let h=Jt(o.aiTools);if(a.stop("Skills updated."),P.log.success(`Skill files installed to ${Ne.cyan(h.length.toString())} locations.`),o.version===d.latest){P.log.info(`Already on the latest version (${o.version}).`);return}if(o.licenseToken)try{let b=await ur(s,{currentToken:o.licenseToken,newVersion:d.latest});o.licenseToken=b.token,b.licenseKeyHash&&(o.licenseKeyHash=b.licenseKeyHash),await l(n,JSON.stringify(o,null," ")+`
1190
+ `),P.log.success("License refreshed.")}catch(b){let y=mt(b);y&&(P.cancel(y.message),process.exit(1)),P.log.warn("License refresh skipped.")}let m=vn(o,r),_=Je(r,cr);a.start(`Staging v${d.latest} (shaped for your config)...`),await Zt(s,d.latest,m,_),a.stop("Template staged.");let{text:E,title:R}=await ws(s,d,o.version);E&&P.note(E,R);let z=Je(r,dt),D=Je(r,ut),ae=!$n(D),ee=!$n(z);if(ae){if(a.start("Building baseline template (one-time migration)..."),await Zt(s,o.version,m,D),ee){let b=await De(D);await l(z,JSON.stringify(b,null," ")+`
1191
+ `)}if(a.stop("Baseline template stored."),!ee){let b=await bs(z,D);b>0&&P.log.warn(`Rebuilt baseline differs from the original for ${b} 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(ee){a.start("Computing baseline template hashes...");let b=await De(D);await l(z,JSON.stringify(b,null," ")+`
1192
+ `),a.stop("Baseline hashes computed.")}if(await l(Je(r,lr),JSON.stringify({currentVersion:o.version,targetVersion:d.latest,changelog:E,stagedAt:new Date().toISOString()},null," ")+`
1193
+ `),P.log.info(`Update staged: ${Ne.cyan(o.version)} \u2192 ${Ne.cyan(d.latest)}`),o.aiTools&&o.aiTools.length>0){let b=o.aiTools[0],y=Ve[b].label;P.log.info(`Open your project in ${Ne.cyan(y)} and ask: ${Ne.cyan("'update my GenerateSaaS project'")}`)}else P.log.info(`Ask your AI coding assistant to ${Ne.cyan("'update my GenerateSaaS project'")}.`)}catch(d){a.stop("Failed."),P.cancel(`Update failed: ${I(d)}`),process.exit(1)}})}async function ws(e,t,r){let n=t.latest,o=t.versions.filter(s=>At(s.version,r)>0&&At(s.version,n)<=0).sort((s,a)=>At(s.version,a.version));if(o.length<=1)return{text:await xt(e,n),title:`Changelog v${n}`};let i=[];for(let s of o){let a=null;try{a=await xt(e,s.version)}catch{a=null}let d=s.date?` (${s.date.slice(0,10)})`:"",g=s.breaking?" [BREAKING]":"";i.push(`# v${s.version}${d}${g}
1194
+
1195
+ ${a??"_No changelog available for this release._"}`)}return{text:i.join(`
1196
+
1197
+ `),title:`Changelog v${r} \u2192 v${n}`}}async function bs(e,t){let r=JSON.parse(await Un(e,"utf-8")),n=await De(t),o=0;for(let[i,s]of Object.entries(r))Ie(i)||n[i]!==s&&o++;for(let i of Object.keys(n))i in r||o++;return o}import*as j from"@clack/prompts";import G from"picocolors";import{readFile as ks}from"fs/promises";import{join as As,resolve as Is}from"path";function Vn(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=Is(t.cwd??process.cwd()),n=As(r,re),o;try{o=JSON.parse(await ks(n,"utf-8"))}catch{j.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let i=[`Version: ${G.cyan(o.version)}`,`Frontend: ${G.cyan(o.frontend)}`,o.deploymentTarget?`Deploy target: ${G.cyan(o.deploymentTarget)}`:null,o.databaseProvider?`Database: ${G.cyan(o.databaseProvider)}`:null,o.cacheProvider?`Cache: ${G.cyan(o.cacheProvider)}`:null,o.aiTools&&o.aiTools.length>0?`AI tools: ${G.cyan(o.aiTools.join(", "))}`:null].filter(Boolean).join(`
1198
+ `);j.note(i,G.bold("Project Status"));let s=j.spinner();s.start("Checking for updates...");try{let a=await Ae(),d=Z(a),h=(await ie(d)).latest;o.version===h?(s.stop("Up to date."),j.log.success(`Already on the latest version (${G.green(h)})`)):(s.stop("Update available."),j.log.warning(`Update available: ${G.yellow(o.version)} \u2192 ${G.green(h)}`),j.log.info(`Open this project in your AI coding agent and ask it to ${G.cyan("update my GenerateSaaS project")} - it fetches and applies the update for you.`))}catch(a){s.stop("Check failed."),a instanceof x&&a.status===401?j.log.warning("Invalid API key. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):j.log.warning(`Could not check for updates: ${I(a)}`)}})}import{readFile as Ts}from"fs/promises";import*as A from"@clack/prompts";import k from"picocolors";function Ps(){return process.env.GENERATESAAS_API_KEY??Ke()}function _s(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 Rs(e){return{verdict:e.verdict??"unknown",ejectedAt:e.ejectedAt??null}}function Os(e){switch(e.verdict){case"licensed":return A.log.success(`${k.green("LICENSED")} - resolves to an account with an active${e.plan?` ${e.plan}`:""} license.`),!0;case"ejected":return A.log.success(`${k.green("EJECTED")} - a licensed buyer opted this install out of telemetry${e.ejectedAt?` on ${e.ejectedAt.slice(0,10)}`:""}. The site is legitimate.`),!0;case"revoked":return A.log.error(`${k.red("REVOKED")} - the owning account no longer holds a plan (refund or chargeback). This deployment is no longer licensed.`),!1;case"token_domain_mismatch":return A.log.error(`${k.red("LEAKED TOKEN")} - this license belongs to a different deployment${e.mismatchDomain?` (${k.cyan(e.mismatchDomain)})`:""}, not this site. The token was copied from a licensed project.`),!1;case"no_license_history":return A.log.error(`${k.red("NO LICENSE HISTORY")} - no license has ever been associated with this site. If it runs GenerateSaaS, treat it as unlicensed.`),!1;default:return A.log.warn(`${k.yellow("UNKNOWN")} - could not cross-reference the records right now. Try again shortly.`),!1}}async function Mn(e,t){let r=process.env.GENERATESAAS_API_URL??Ge,n=Ps();e.start("Cross-referencing license records...");try{let o=n?_s(await mr(r,n,{lkh:t.lkh,nid:t.nid,domain:t.domain})):Rs(await Ct(r,{token:t.token,domain:t.domain}));return e.stop(`${k.green("Checked")} - records cross-referenced`),Os(o)}catch(o){return e.stop(`${k.yellow("Skipped")} - ${I(o)}`),null}}function xs(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 Qt(e){return typeof e!="number"?"unknown":new Date(e*1e3).toISOString().split("T")[0]}function Fn(e){A.note([`License ID: ${k.cyan(String(e.lid??"unknown"))}`,`Version: ${k.cyan(String(e.ver??"unknown"))}`,`Init version: ${String(e.iver??"unknown")}`,`Frontend: ${String(e.fe??"unknown")}`,`Created: ${Qt(e.pat)}`,`Last updated: ${Qt(e.uat)}`,`Expires: ${Qt(e.exp)}`,`Install ID: ${String(e.nid??"unknown")}`].join(`
1199
+ `),k.yellow("License Details"))}function Ds(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 Bn(e){let t=A.spinner(),r=null,n="no candidates";for(let s of Ds(e)){t.start(`Checking ${s}...`);try{let a=await fetch(s);if(!a.ok){n=`${s} returned ${a.status}`,t.stop(`${k.yellow("Not here")} - ${n}`);continue}let d=(await a.text()).trim();if(!d||d.split(".").length!==3){n=`${s} did not return a JWT`,t.stop(`${k.yellow("Not here")} - ${n}`);continue}r=d,t.stop(`${k.green("Found")} - license endpoint responded`);break}catch(a){n=`${s}: ${I(a)}`,t.stop(`${k.yellow("Unreachable")} - ${n}`)}}if(r===null){A.log.warn(`No license endpoint found (last: ${n}). The site may be ejected, not a GenerateSaaS app, or serving its API elsewhere.`);let s=Gn(e);return s?await Mn(t,{domain:s})??!1:!1}let o;try{o=xs(r)}catch{return A.log.error("Could not decode JWT payload."),!1}t.start("Verifying signature...");try{let s=process.env.GENERATESAAS_API_URL??Ge,a=await Ct(s,{token:r});if(a.valid)t.stop(`${k.green("Valid")} - signature verified`);else return t.stop(`${k.red("Invalid")} - ${a.reason}`),!1}catch{return t.stop(`${k.yellow("Skipped")} - could not reach verification service`),A.log.warn("Signature not verified. Displaying unverified claims:"),Fn(o),!1}return Fn(o),await Mn(t,{token:r,lkh:typeof o.lkh=="string"?o.lkh:void 0,nid:typeof o.nid=="string"?o.nid:void 0,domain:Gn(e)})??!0}function Gn(e){try{return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`).hostname}catch{return}}function Kn(e){e.command("verify").description("Verify a GenerateSaaS license on a deployed site").argument("[url]","URL of the site to verify (e.g. https://example.com or https://example.com/api)").option("--file <path>","file with URLs to check, one per line").action(async(t,r)=>{if(!t&&!r.file&&(A.cancel("Provide a URL or --file <path>."),process.exit(1)),r.file){let o=(await Ts(r.file,"utf-8")).split(`
1200
+ `).map(s=>s.trim()).filter(s=>s&&!s.startsWith("#"));o.length===0&&(A.cancel("No URLs found in file."),process.exit(1));let i=0;for(let s of o)await Bn(s)&&i++,A.log.info("");A.log.success(`${i}/${o.length} sites verified.`)}else await Bn(t)||process.exit(1)})}import{existsSync as Cs,rmSync as Ns}from"fs";import*as K from"@clack/prompts";function zn(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){Cs(X)?(Ns(X),K.log.success("API key removed.")):K.log.info("No API key configured.");return}let r=Ke();r?K.log.info(`Current API key: ****${r.slice(-4)}`):K.log.info("No API key configured.");let n=await ze(),o=Z(n),i=K.spinner();i.start("Verifying API key...");try{await ie(o),i.stop("API key verified."),he(n),K.log.success("API key saved.")}catch(s){i.stop("Verification failed."),s instanceof x&&s.status===401?K.cancel("Invalid API key."):K.cancel(I(s)),process.exit(1)}})}import{existsSync as It,rmSync as Ls,readFileSync as tr,writeFileSync as Hn}from"fs";import{join as we}from"path";import*as C from"@clack/prompts";var $s=["packages/api/src/functions/maintenance/license-heartbeat.ts","packages/api/src/lib/cron-spread.ts","packages/api/src/lib/manifest.ts","packages/api/src/routes/internal/license.ts"],Us=[{file:"packages/api/src/routes/inngest.ts",removals:[`import { licenseHeartbeatFunction } from "../functions/maintenance/license-heartbeat";
1169
1201
  `,` licenseHeartbeatFunction,
1170
1202
  `]},{file:"packages/api/src/routes/internal/index.ts",removals:[`import licenseRoutes from "./license";
1171
1203
  `,` .route("/license", licenseRoutes)
1172
- `]}];function Os(e){return(e&&e.length>0?e.map(r=>He[r]):Object.values(He)).map(r=>Se(r,Kt))}function Xt(e){return kt(e)?(Ps(e,{recursive:!0}),!0):!1}function xs(e,t){if(!kt(e))return!1;let r=Zt(e,"utf-8"),n=r;for(let i of t)n=n.replace(i,"");return n===r?!1:(Fn(e,n,"utf-8"),!0)}function Ds(e){let t=Se(e,".gitignore");if(!kt(t))return!1;let r=Zt(t,"utf-8"),n=r.split(`
1173
- `).filter(i=>!i.includes(".generatesaas")).join(`
1174
- `);return n===r?!1:(Fn(t,n,"utf-8"),!0)}function Bn(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=Se(t,ee),n;try{n=JSON.parse(Zt(r,"utf-8"))}catch{O.cancel("No GenerateSaaS project found in this directory."),process.exit(1)}let i=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(i)&&(O.cancel("Eject cancelled."),process.exit(0)),n.licenseToken)try{await fetch("https://generatesaas.com/api/v1/heartbeat",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${n.licenseToken}`},body:JSON.stringify({event:"eject",version:n.version,frontend:n.frontend}),signal:AbortSignal.timeout(5e3)}),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 Os(n.aiTools))Xt(Se(t,a))&&o.push(a);for(let a of _s)Xt(Se(t,a))&&o.push(a);Xt(Se(t,U))&&o.push(U+"/");for(let a of Rs){let d=Se(t,a.file);xs(d,a.removals)?s.push(a.file):kt(d)&&O.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}Ds(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 Ee=new Cs().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("1.15.2").addHelpText("after",`
1204
+ `]}];function js(e){return(e&&e.length>0?e.map(r=>Ye[r]):Object.values(Ye)).map(r=>we(r,Yt))}function er(e){return It(e)?(Ls(e,{recursive:!0}),!0):!1}function Vs(e,t){if(!It(e))return!1;let r=tr(e,"utf-8"),n=r;for(let o of t)n=n.replace(o,"");return n===r?!1:(Hn(e,n,"utf-8"),!0)}function Ms(e){let t=we(e,".gitignore");if(!It(t))return!1;let r=tr(t,"utf-8"),n=r.split(`
1205
+ `).filter(o=>!o.includes(".generatesaas")).join(`
1206
+ `);return n===r?!1:(Hn(t,n,"utf-8"),!0)}function Yn(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=we(t,re),n;try{n=JSON.parse(tr(r,"utf-8"))}catch{C.cancel("No GenerateSaaS project found in this directory."),process.exit(1)}let o=await C.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(C.isCancel(o)&&(C.cancel("Eject cancelled."),process.exit(0)),n.licenseToken)try{await fetch("https://generatesaas.com/api/v1/heartbeat",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${n.licenseToken}`},body:JSON.stringify({event:"eject",version:n.version,frontend:n.frontend}),signal:AbortSignal.timeout(5e3)}),C.log.info("Recorded the opt-out with generatesaas.com (final event - nothing is sent after this).")}catch{C.log.warn("Could not reach generatesaas.com to record the opt-out. Ejecting anyway.")}let i=[],s=[];for(let a of js(n.aiTools))er(we(t,a))&&i.push(a);for(let a of $s)er(we(t,a))&&i.push(a);er(we(t,M))&&i.push(M+"/");for(let a of Us){let d=we(t,a.file);Vs(d,a.removals)?s.push(a.file):It(d)&&C.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}Ms(t)&&s.push(".gitignore");for(let a of i)C.log.info(`Deleted ${a}`);for(let a of s)C.log.info(`Modified ${a}`);C.log.success("Ejected successfully. This project is now fully standalone.")})}var be=new Fs().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("1.17.0").addHelpText("after",`
1175
1207
  Examples:
1176
1208
  $ generatesaas init Interactive setup
1177
1209
  $ generatesaas init -n my-app -y Quick setup with defaults
1178
1210
  $ generatesaas status Check for updates
1179
1211
  $ generatesaas auth Set or update API key
1180
- `);Rn(Ee);Cn(Ee);Nn(Ee);Vn(Ee);Mn(Ee);Bn(Ee);Ee.parseAsync().catch(e=>{Gn.cancel("An unexpected error occurred."),console.error(e),process.exit(1)});
1212
+ `);Nn(be);jn(be);Vn(be);Kn(be);zn(be);Yn(be);be.parseAsync().catch(e=>{Jn.cancel("An unexpected error occurred."),console.error(e),process.exit(1)});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "generatesaas",
3
- "version": "1.15.2",
3
+ "version": "1.17.0",
4
4
  "type": "module",
5
5
  "description": "CLI for scaffolding and managing GenerateSaaS projects",
6
6
  "license": "UNLICENSED",