generatesaas 1.15.0 → 1.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +95 -99
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as
|
|
3
|
-
`);m.note(c,"Unavailable on edge runtime")}m.log.info(g.bold("Features"));let
|
|
4
|
-
`),p=[` Deploy target: ${g.cyan(
|
|
5
|
-
`),v=[
|
|
6
|
-
`),
|
|
7
|
-
`);
|
|
8
|
-
`),"Summary");let
|
|
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
|
|
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
|
|
11
|
-
`)}function
|
|
12
|
-
- **Payments:** ${
|
|
2
|
+
import{Command as Ds}from"commander";import*as Bn from"@clack/prompts";import{existsSync as Zi,readdirSync as Qi,rmSync as es}from"fs";import{join as ts,resolve as rs}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"],ot=["claude-code","cursor","codex","gemini-cli","windsurf"],it=["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 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 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})(),o=e?.projectDir??await(async()=>{t=!0;let c=await m.text({message:"Project location:",initialValue:`./${r}`});return w(c),c==="."?process.cwd():c})(),i=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})(),ie=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})(),$=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($&&e?.billingScope===void 0){t=!0;let c=await m.select({message:"Billing scope:",options:it.map(p=>({value:p,label:Ze[p].label,hint:Ze[p].hint}))});w(c),we=c}let Z=e?.blog??await(async()=>{t=!0;let c=await m.confirm({message:"Enable blog?",initialValue:!0});return w(c),c})(),u=e?.docs??await(async()=>{t=!0;let c=await m.confirm({message:"Include docs app? (self-hosted Fumadocs documentation site)",initialValue:!1});return w(c),c})(),b=e?.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=ot.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(o)}`,` Frontend: ${g.cyan($e[i].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)} (${ie})`:` Payment: ${g.dim("none")}`,` Credits: ${se?g.cyan("Yes"):g.dim("No")}`,` Email: ${g.cyan(Xe[x].label)}`,` Multi-tenancy: ${$?g.cyan("Yes")+` (billing: ${Ze[we].label})`:g.dim("No")}`,` Blog: ${Z?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 Gn=Le[Qt.key]?g.green("provided"):g.dim("skipped");return` ${Qt.key}: ${Gn}`}).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:o,frontend:i,architecture:y,deploymentTarget:s,databaseProvider:f,cacheProvider:S,paymentProvider:T,emailProvider:x,multiTenancy:$,billingScope:we,blog:Z,docs:u,desktop:b,revenueSharing:J,credits:se,dockerServices:be,aiTools:At,socialProviders:G,defaultCurrency:ie,...Object.keys(Le).length>0?{credentials:Le}:{},...e?.baseUrl!==void 0?{baseUrl:e.baseUrl}:{},...Tt!==void 0?{demo:Tt}:{}}}import{createReadStream as Zn}from"fs";import{mkdir as Qn}from"fs/promises";import{Readable as eo}from"stream";import{pipeline as dr}from"stream/promises";import{extract as to}from"tar";import{join as me}from"path";import{homedir as Kn}from"os";var Be=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",j=".generatesaas",Q=me(j,"manifest.json"),or=me(j,"hashes.json"),lt=me(j,"template-hashes.json"),pt=me(j,"template"),ir=me(j,"staging"),sr=me(j,"staging.json"),q=me(Kn(),".generatesaas");var R=class extends Error{constructor(r,n,o){super(n);this.status=r;this.body=o}status;body;name="ApiError"};function X(e){return{apiKey:e,baseUrl:Be}}async function ee(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 R(o.status,i,s)}return o}import{existsSync as zn,readFileSync as Hn,writeFileSync as Yn,mkdirSync as Jn}from"fs";import{dirname as Wn}from"path";import*as te from"@clack/prompts";function Ge(){if(!zn(q))return null;try{let e=JSON.parse(Hn(q,"utf-8"));return e.apiKey?e.apiKey:(e.token&&!e.apiKey&&te.log.warning(`Found old GitHub token in ${q}. Run 'generatesaas init' to set up your API key.`),null)}catch{return null}}function fe(e){Jn(Wn(q),{recursive:!0}),Yn(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 te.text({message:"Enter your GenerateSaaS API key:",placeholder:"gs_live_...",validate:t=>{if(!t?.trim())return"API key is required."}});return te.isCancel(e)&&(te.cancel("Setup cancelled."),process.exit(0)),e.trim()}async function re(e){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{latest:"0.0.0-ci",versions:[{version:"0.0.0-ci",date:new Date().toISOString(),breaking:!1}]}:await(await ee(e,"/versions")).json()}async function _t(e,t){try{return await(await ee(e,`/changelog/${encodeURIComponent(t)}`)).text()}catch(r){if(r instanceof R&&r.status===404)return null;throw r}}async function ar(e,t){return await(await ee(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 ee(e,"/license/sign",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function lr(e,t){return await(await ee(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 o=await n.json().catch(()=>null);throw new Error(o?.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 qn=new Set(["data","mksaas","references","scripts",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","README.md","TODO.md","OVERVIEW.md"]),Xn=["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(qn.has(t[0]))return!0;for(let r of Xn)if(e===r||e.startsWith(r+"/"))return!0;return!1}async function ut(e,t,r){await Qn(r,{recursive:!0});let n=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(n){await dr(Zn(n),ur(r));return}let o=await ee(e,`/template/${encodeURIComponent(t)}`);if(!o.body)throw new Error("Empty response body");let i=eo.fromWeb(o.body);await dr(i,ur(r))}function ur(e){return to({cwd:e,strip:1,filter:t=>{let r=t.replace(/^[^/]+\//,"");return r?!Ct(r):!0},sync:!1})}import{readdir as ro,readFile as Nt,rm as fr,writeFile as Lt}from"fs/promises";import{join as Ie}from"path";var no=["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(no.map(t=>fr(Ie(e,t),{recursive:!0,force:!0})))}var oo="packages/config/src/blog.ts";async function hr(e){let t=Ie(e,oo),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 ro(t)}catch{return}for(let n of r){let o=Ie(t,n,"web.json"),i;try{i=await Nt(o,"utf-8")}catch{continue}let s=JSON.parse(i);!s.blog||s.blog.categories===void 0||(s.blog.categories={},await Lt(o,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 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=>io(i,t))),await Lt(r,JSON.stringify(o,null," ")+`
|
|
11
|
+
`)}function io(e,t){return!(e.startsWith("mcp__")||t!=="nuxt"&&e.includes("nuxt"))}import{join as Er}from"path";import{mkdir as so,readdir as ao,rm as co,rmdir as lo,writeFile as po}from"fs/promises";import{dirname as mt,join as uo,relative as mo,sep as fo}from"path";function ge(e){return e.split(fo).join("/")}async function ft(e){await so(e,{recursive:!0})}async function l(e,t){await ft(mt(e)),await po(e,t,"utf-8")}async function $t(e,t){await co(e,{force:!0});let r=mt(e);for(;r!==t&&r!==mt(r);){try{await lo(r)}catch{return}r=mt(r)}}async function he(e,t,r){let n=[],o=await ao(e,{withFileTypes:!0});for(let i of o){let s=uo(e,i.name),a=ge(mo(t,s));r(a)||(i.isDirectory()?n.push(...await he(s,t,r)):i.isFile()&&n.push(s))}return n}var go={postgres:"Postgres",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"},ho={resend:"Resend",ses:"Amazon SES",smtp:"SMTP"},yo={redis:"Redis",upstash:"Upstash Redis"},vo={node:"Node.js / Docker",vercel:"Vercel"},So={stripe:"Stripe",polar:"Polar"};function Eo(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=yo[e.cacheProvider],h=vo[e.deploymentTarget],y=e.paymentProvider==="none"?"":`
|
|
12
|
+
- **Payments:** ${So[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
|
|
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,10 +37,10 @@ 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 + ${
|
|
40
|
+
- **Database:** Drizzle ORM + ${go[e.databaseProvider]}
|
|
41
41
|
- **Cache + jobs:** ${d} + Inngest
|
|
42
42
|
- **Auth:** Better Auth${y}
|
|
43
|
-
- **Email:** ${
|
|
43
|
+
- **Email:** ${ho[e.emailProvider]}
|
|
44
44
|
- **Deploy:** ${h}
|
|
45
45
|
|
|
46
46
|
## Where things live (extend these - don't reinvent)
|
|
@@ -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 (${
|
|
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
|
|
@@ -81,8 +81,8 @@ 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"),
|
|
85
|
-
`)}import{join as
|
|
84
|
+
`}async function wr(e){await l(Er(e.projectDir,"AGENTS.md"),Eo(e)),await l(Er(e.projectDir,"CLAUDE.md"),`@AGENTS.md
|
|
85
|
+
`)}import{join as wo}from"path";var bo={postgres:"Postgres (self-hosted)",neon:"Neon (managed Postgres)",supabase:"Supabase (managed Postgres)"};function ko(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=bo[e.databaseProvider],s=Qe(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
88
|
- Inngest dev server: http://127.0.0.1:8288`,h=s?`pnpm infra # optional: starts local Docker services (Postgres / Redis / Inngest / Mailpit)
|
|
@@ -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 + ${
|
|
100
|
+
- **ORM / DB:** Drizzle + ${i}
|
|
101
101
|
- **Background jobs:** Inngest
|
|
102
102
|
- **i18n:** ${r?"@nuxtjs/i18n":"next-intl"}
|
|
103
103
|
|
|
@@ -143,11 +143,11 @@ pnpm dlx generatesaas eject
|
|
|
143
143
|
## Updates
|
|
144
144
|
|
|
145
145
|
${f}
|
|
146
|
-
`}async function br(e){await l(
|
|
146
|
+
`}async function br(e){await l(wo(e.projectDir,"README.md"),ko(e))}import{join as gt}from"path";function Ao(e){let t=e.split(".");return t.length>=3?t.slice(1).join("."):e}function To(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"app"}function Io(e,t){let r=To(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",o=e.baseUrl?new URL(e.baseUrl).hostname:"example.com",i=Ao(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=
|
|
150
|
+
},`:"",d=e.frontend==="nextjs"&&e.architecture==="fullstack"?"false":"true",h=Io(t,i),y=`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: "${
|
|
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@${
|
|
186
|
+
email: "noreply@${i}",
|
|
187
187
|
senderName: "${t}"
|
|
188
188
|
},
|
|
189
189
|
marketing: {
|
|
190
|
-
email: "hello@${
|
|
190
|
+
email: "hello@${i}",
|
|
191
191
|
senderName: "${t}",
|
|
192
192
|
signatureName: "Alex"
|
|
193
193
|
},
|
|
194
194
|
support: {
|
|
195
|
-
email: "support@${
|
|
195
|
+
email: "support@${i}",
|
|
196
196
|
senderName: "${t} Support"
|
|
197
197
|
}
|
|
198
198
|
},
|
|
@@ -321,7 +321,7 @@ export const desktopConfig = {
|
|
|
321
321
|
productName: "${h.productName}",
|
|
322
322
|
protocol: "${h.protocol}",
|
|
323
323
|
baseUrl: "${n}",
|
|
324
|
-
autoUpdate: { provider: "generic", url: "https://cdn.${
|
|
324
|
+
autoUpdate: { provider: "generic", url: "https://cdn.${i}/desktop" }
|
|
325
325
|
};
|
|
326
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
|
|
327
327
|
// TypeScript config index reads it directly (the Electron renderer). Mirrors the
|
|
@@ -333,10 +333,10 @@ export const tenancyConfig = ${e.multiTenancy?`{
|
|
|
333
333
|
billingScope: "${e.billingScope}"
|
|
334
334
|
}`:"{ multiTenant: false }"};
|
|
335
335
|
`),e.desktop&&await l(gt(e.projectDir,"apps/desktop/dev-app-update.yml"),`provider: generic
|
|
336
|
-
url: https://cdn.${
|
|
336
|
+
url: https://cdn.${i}/desktop
|
|
337
337
|
updaterCacheDirName: ${h.protocol}-updater
|
|
338
|
-
`)}import{join as
|
|
339
|
-
`)}async function Ar(e){let t=
|
|
338
|
+
`)}import{join as Po}from"path";function _o(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=Po(e.projectDir,"packages/config/src/pricing.ts"),r=e.defaultCurrency;if(e.paymentProvider==="none"){let f=`import type { PricingConfig } from "@repo/config/types";
|
|
340
340
|
|
|
341
341
|
export const pricingConfig: PricingConfig = {
|
|
342
342
|
defaultPlan: "free",
|
|
@@ -365,7 +365,7 @@ export const pricingConfig: PricingConfig = {
|
|
|
365
365
|
credits: { enabled: false },
|
|
366
366
|
products: { enabled: false, items: [] }
|
|
367
367
|
};
|
|
368
|
-
`;await l(t,f);return}let n=e.paymentProvider,
|
|
368
|
+
`;await l(t,f);return}let n=e.paymentProvider,o=_o(n),i=e.credits,s=jt({credits:5,rateLimit:100,withCredits:i}),a=jt({credits:10,rateLimit:1e3,withCredits:i}),d=jt({credits:50,rateLimit:5e3,withCredits:i}),y=`import type { PricingConfig } from "@repo/config/types";
|
|
369
369
|
|
|
370
370
|
export const pricingConfig: PricingConfig = {
|
|
371
371
|
defaultPlan: "free",
|
|
@@ -404,12 +404,12 @@ ${s}
|
|
|
404
404
|
],
|
|
405
405
|
prices: [
|
|
406
406
|
{
|
|
407
|
-
${
|
|
407
|
+
${o}
|
|
408
408
|
interval: "month",
|
|
409
409
|
amounts: { ${r}: 9 }
|
|
410
410
|
},
|
|
411
411
|
{
|
|
412
|
-
${
|
|
412
|
+
${o}
|
|
413
413
|
interval: "year",
|
|
414
414
|
amounts: { ${r}: 90 },
|
|
415
415
|
anchorAmounts: { ${r}: 109 }
|
|
@@ -433,14 +433,14 @@ ${a}
|
|
|
433
433
|
],
|
|
434
434
|
prices: [
|
|
435
435
|
{
|
|
436
|
-
${
|
|
436
|
+
${o}
|
|
437
437
|
interval: "month",
|
|
438
438
|
amounts: { ${r}: 29 },
|
|
439
439
|
anchorAmounts: { ${r}: 39 },
|
|
440
440
|
featured: true
|
|
441
441
|
},
|
|
442
442
|
{
|
|
443
|
-
${
|
|
443
|
+
${o}
|
|
444
444
|
interval: "year",
|
|
445
445
|
amounts: { ${r}: 290 },
|
|
446
446
|
anchorAmounts: { ${r}: 349 }
|
|
@@ -449,17 +449,17 @@ ${i}
|
|
|
449
449
|
${d}
|
|
450
450
|
}
|
|
451
451
|
],
|
|
452
|
-
${
|
|
452
|
+
${i?" credits: { enabled: true }":" credits: { enabled: false }"},
|
|
453
453
|
products: {
|
|
454
454
|
enabled: false,
|
|
455
455
|
items: []
|
|
456
456
|
}
|
|
457
457
|
};
|
|
458
|
-
`;await l(t,y)}var
|
|
459
|
-
`)}function
|
|
460
|
-
`)}async function Pr(e){let t=
|
|
458
|
+
`;await l(t,y)}var Ro={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 Oo(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 xo={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 Do(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:o}=Ir(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(N[e.databaseProvider].envVars,r),i),i.push("","# Cache"),_e(Pe(z[e.cacheProvider].envVars,r),i),i.push("","# Authentication"),t==="example"&&i.push("# Generate a strong secret, e.g. `openssl rand -hex 32`"),ht(i,"BETTER_AUTH_SECRET",crypto.randomUUID(),t),i.push("","# Content API (random secret; required when contentApi feature is enabled in @repo/config)"),ht(i,"CONTENT_API_KEY",Ut(32),t),i.push("","# Job Queue - Inngest","INNGEST_APP_ID=api"),ht(i,"INNGEST_EVENT_KEY",Ut(32),t),ht(i,"INNGEST_SIGNING_KEY",Ut(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=Ro[e.emailProvider];if(s&&(i.push("","# Email"),_e(Pe(s,r),i)),e.paymentProvider!=="none"){let a=xo[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(Oo(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(""),i.join(`
|
|
459
|
+
`)}function Co(e){let{apiUrl:t}=Ir(e),r=e.frontend==="nextjs"?"NEXT_PUBLIC_API_URL":"NUXT_PUBLIC_API_URL",n=["# API Configuration",`${r}=${t}`],o=Do(e);return o&&e.architecture==="separate"&&n.push("","# Production (uncomment and replace with your deployed hostnames):",`# ${r}=${o.backend}`),n.push(""),n.join(`
|
|
460
|
+
`)}async function Pr(e){let t=Co(e);await l(`${e.projectDir}/.env`,t+`
|
|
461
461
|
`+Tr(e,"env")),await l(`${e.projectDir}/.env.example`,t+`
|
|
462
|
-
`+Tr(e,"example"))}import{join as
|
|
462
|
+
`+Tr(e,"example"))}import{join as No}from"path";async function _r(e){let t=[...e.dockerServices];if(N[e.databaseProvider].managed&&(t=t.filter(i=>i!=="postgres")),z[e.cacheProvider].managed&&(t=t.filter(i=>i!=="redis")),t.length===0)return!1;let r=[],n=[];t.includes("postgres")&&(r.push(` postgres:
|
|
463
463
|
image: postgres:18-alpine
|
|
464
464
|
ports:
|
|
465
465
|
- "\${POSTGRES_PORT:-5432}:5432"
|
|
@@ -495,16 +495,16 @@ ${o?" credits: { enabled: true }":" credits: { enabled: false }"},
|
|
|
495
495
|
image: inngest/inngest:v1.17.4
|
|
496
496
|
ports:
|
|
497
497
|
- "\${INNGEST_PORT:-8288}:8288"
|
|
498
|
-
command: inngest dev`);let
|
|
498
|
+
command: inngest dev`);let o=`services:
|
|
499
499
|
${r.join(`
|
|
500
500
|
|
|
501
501
|
`)}
|
|
502
|
-
`;return n.length>0&&(
|
|
502
|
+
`;return n.length>0&&(o+=`
|
|
503
503
|
volumes:
|
|
504
504
|
${n.join(`
|
|
505
505
|
`)}
|
|
506
|
-
`),await l(
|
|
507
|
-
`)}import{join as
|
|
506
|
+
`),await l(No(e.projectDir,"infra/docker-compose.yml"),o),!0}import{join as Lo}from"path";function $o(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 jo(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:$o(e),compounds:jo(e)};await l(Lo(e.projectDir,".vscode/launch.json"),JSON.stringify(t,null," ")+`
|
|
507
|
+
`)}import{join as Uo}from"path";async function Or(e){if(e.architecture!=="separate")return;await l(Uo(e.projectDir,"apps/backend/src/index.ts"),`import { serve } from "@hono/node-server";
|
|
508
508
|
import app from "@repo/api";
|
|
509
509
|
import { closeRedis, env, logger } from "@repo/runtime";
|
|
510
510
|
|
|
@@ -535,9 +535,9 @@ bootstrap().catch((error) => {
|
|
|
535
535
|
logger.error("[Backend] Fatal error", error);
|
|
536
536
|
process.exit(1);
|
|
537
537
|
});
|
|
538
|
-
`)}import{readFile as
|
|
538
|
+
`)}import{readFile as Vo}from"fs/promises";import{join as Mo}from"path";var Fo=`export * from "./db/auth";
|
|
539
539
|
export * from "./db/schema";
|
|
540
|
-
export type { User, Account, Organization, Member } from "./db/auth";`;async function
|
|
540
|
+
export type { User, Account, Organization, Member } from "./db/auth";`;async function xr(e){let t=Mo(e.projectDir,"packages/database/src/index.ts"),r=Fo;try{let o=await Vo(t,"utf-8"),i=Bo(o);i&&(r=i)}catch{}let n;switch(e.databaseProvider){case"postgres":n=Go(r);break;case"neon":n=Ko(r);break;case"supabase":n=zo(r);break}await l(t,n)}function Bo(e){return e.split(`
|
|
541
541
|
`).filter(n=>n.startsWith("export type ")||n.startsWith("export * from")).join(`
|
|
542
542
|
`)}var Vt=`
|
|
543
543
|
/** Extract affected-row count from a delete/update result (works for pg + postgres-js). */
|
|
@@ -546,7 +546,7 @@ export function affectedRowCount(result: unknown): number {
|
|
|
546
546
|
const r = result as { rowCount?: number; count?: number };
|
|
547
547
|
return r.rowCount ?? r.count ?? 0;
|
|
548
548
|
}
|
|
549
|
-
`;function
|
|
549
|
+
`;function Go(e){return`import { drizzle } from "drizzle-orm/node-postgres";
|
|
550
550
|
import { z } from "zod";
|
|
551
551
|
import * as authSchema from "./db/auth";
|
|
552
552
|
import * as appSchema from "./db/schema";
|
|
@@ -561,7 +561,7 @@ export const db = drizzle(parsed.data.DATABASE_URL, { schema });
|
|
|
561
561
|
export const pool = db.$client;
|
|
562
562
|
|
|
563
563
|
${e}
|
|
564
|
-
${Vt}`}function
|
|
564
|
+
${Vt}`}function Ko(e){return`import { neon } from "@neondatabase/serverless";
|
|
565
565
|
import { drizzle } from "drizzle-orm/neon-http";
|
|
566
566
|
import { z } from "zod";
|
|
567
567
|
import * as authSchema from "./db/auth";
|
|
@@ -577,7 +577,7 @@ const sql = neon(parsed.data.DATABASE_URL);
|
|
|
577
577
|
export const db = drizzle(sql, { schema });
|
|
578
578
|
|
|
579
579
|
${e}
|
|
580
|
-
${Vt}`}function
|
|
580
|
+
${Vt}`}function zo(e){return`import { drizzle } from "drizzle-orm/postgres-js";
|
|
581
581
|
import postgres from "postgres";
|
|
582
582
|
import { z } from "zod";
|
|
583
583
|
import * as authSchema from "./db/auth";
|
|
@@ -593,7 +593,7 @@ const client = postgres(parsed.data.DATABASE_URL);
|
|
|
593
593
|
export const db = drizzle(client, { schema });
|
|
594
594
|
|
|
595
595
|
${e}
|
|
596
|
-
${Vt}`}import{join as yt}from"path";async function Dr(e){switch(e.cacheProvider){case"redis":await
|
|
596
|
+
${Vt}`}import{join as yt}from"path";async function Dr(e){switch(e.cacheProvider){case"redis":await Ho(e),await Yo(e);break;case"upstash":await Jo(e),await Wo(e);break}}async function Ho(e){await l(yt(e.projectDir,"packages/runtime/src/redis.ts"),`import type { Store } from "hono-rate-limiter";
|
|
597
597
|
import { Redis } from "ioredis";
|
|
598
598
|
import { RedisStore, type RedisReply } from "rate-limit-redis";
|
|
599
599
|
import { env } from "./env";
|
|
@@ -717,7 +717,7 @@ export async function closeRedis() {
|
|
|
717
717
|
closed = true;
|
|
718
718
|
}
|
|
719
719
|
}
|
|
720
|
-
`)}async function
|
|
720
|
+
`)}async function Yo(e){await l(yt(e.projectDir,"packages/runtime/src/mutex.ts"),`import { Mutex } from "redis-semaphore";
|
|
721
721
|
import { redis } from "./redis";
|
|
722
722
|
|
|
723
723
|
export class MutexTimeoutError extends Error {
|
|
@@ -754,7 +754,7 @@ export async function withMutex<T>(
|
|
|
754
754
|
await mutex.release();
|
|
755
755
|
}
|
|
756
756
|
}
|
|
757
|
-
`)}async function
|
|
757
|
+
`)}async function Jo(e){await l(yt(e.projectDir,"packages/runtime/src/redis.ts"),`import { Redis } from "@upstash/redis";
|
|
758
758
|
import type { Store } from "hono-rate-limiter";
|
|
759
759
|
import { env } from "./env";
|
|
760
760
|
|
|
@@ -867,7 +867,7 @@ export const limiterStore: Store = createLimiterStore();
|
|
|
867
867
|
|
|
868
868
|
/** No persistent connection to close with Upstash REST. */
|
|
869
869
|
export async function closeRedis(): Promise<void> {}
|
|
870
|
-
`)}async function
|
|
870
|
+
`)}async function Wo(e){await l(yt(e.projectDir,"packages/runtime/src/mutex.ts"),`import { Lock } from "@upstash/lock";
|
|
871
871
|
import { redis } from "./redis";
|
|
872
872
|
|
|
873
873
|
export class MutexTimeoutError extends Error {
|
|
@@ -912,7 +912,7 @@ export async function withMutex<T>(
|
|
|
912
912
|
await lock.release();
|
|
913
913
|
}
|
|
914
914
|
}
|
|
915
|
-
`)}async function Cr(e){await l(`${e.projectDir}/packages/runtime/src/env.ts`,
|
|
915
|
+
`)}async function Cr(e){await l(`${e.projectDir}/packages/runtime/src/env.ts`,qo(e))}function qo(e){return`import { z } from "zod";
|
|
916
916
|
|
|
917
917
|
const EnvSchema = z.object({
|
|
918
918
|
NODE_ENV: z.enum(["development", "production"]).default("development"),
|
|
@@ -985,10 +985,6 @@ ${e.cacheProvider==="upstash"?` UPSTASH_REDIS_REST_URL: z.string(),
|
|
|
985
985
|
STORAGE_PUBLIC_BUCKET: z.string().optional(),
|
|
986
986
|
STORAGE_PRIVATE_BUCKET: z.string().optional(),
|
|
987
987
|
STORAGE_PUBLIC_URL: z.string().optional(),
|
|
988
|
-
STORAGE_FORCE_PATH_STYLE: z
|
|
989
|
-
.string()
|
|
990
|
-
.optional()
|
|
991
|
-
.transform((val) => val === "true"),
|
|
992
988
|
|
|
993
989
|
STORAGE_LOCAL_PATH: z.string().optional(),
|
|
994
990
|
STORAGE_LOCAL_PUBLIC_URL: z.string().optional(),
|
|
@@ -1031,13 +1027,13 @@ export const env = (() => {
|
|
|
1031
1027
|
const parsedApiUrl = new URL(env.API_URL);
|
|
1032
1028
|
export const apiBasePath = parsedApiUrl.pathname === "/" ? "" : parsedApiUrl.pathname;
|
|
1033
1029
|
export const apiOrigin = parsedApiUrl.origin;
|
|
1034
|
-
`}import{readFile as
|
|
1035
|
-
`)}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
|
|
1036
|
-
`)}var
|
|
1037
|
-
`))}}import{readFile as
|
|
1038
|
-
`);let
|
|
1030
|
+
`}import{readFile as Xo}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 Xo(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 o=n?"devDependencies":"dependencies";e[o]||(e[o]={}),e[o][t]=r}async function Nr(e){await Zo(e),await Qo(e),await ei(e),await ti(e),e.frontend==="nextjs"?await ni(e):await ri(e)}async function Zo(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 Qo(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 ei(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 ti(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 ri(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),o=n.dependencies?.["@repo/api"];o&&(delete n.dependencies?.["@repo/api"],Re(n,"@repo/api",o,!0)),await xe(r,n)}async function ni(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),o=n.dependencies?.["@repo/api"];o&&(delete n.dependencies?.["@repo/api"],Re(n,"@repo/api",o,!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),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(h=>h!==i);d.length!==o.env.length&&(o.env=d,a=!0)}if(Array.isArray(o.outputs)){let d=o.outputs.filter(h=>!s.has(h));d.length!==o.outputs.length&&(o.outputs=d,a=!0)}a&&await l(t,JSON.stringify(n,null," ")+`
|
|
1032
|
+
`)}var oi=["base.json","node.json","next.json"],Lr="GenerateSaaS ";async function Vr(e){if(!e.demo)for(let t of oi){let r=jr(e.projectDir,"tooling/typescript",t),n;try{n=await $r(r,"utf-8")}catch{continue}let o=JSON.parse(n);typeof o.display!="string"||!o.display.startsWith(Lr)||(o.display=o.display.slice(Lr.length),await l(r,JSON.stringify(o,null," ")+`
|
|
1033
|
+
`))}}import{readFile as ii,rm as si}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 ii(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," ")+`
|
|
1034
|
+
`);let i=Fr(e.projectDir,"packages/i18n/nuxt");if(n&&!Mr(i))throw new Error(`pruneI18nNuxt: packages/i18n declares a Nuxt export surface but ${i} is missing - did the i18n Nuxt module move?`);await si(i,{recursive:!0,force:!0})}import{readFile as Gr,rm as ai}from"fs/promises";import{join as Mt}from"path";async function Kr(e){e.cacheProvider==="upstash"&&await Promise.all([ci(e.projectDir),li(e.projectDir),ai(Mt(e.projectDir,"packages/runtime/tests/redis.test.ts"),{force:!0})])}async function ci(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",
|
|
1039
1035
|
UPSTASH_REDIS_REST_TOKEN: "test-token",
|
|
1040
|
-
`);n!==r&&await l(t,n)}async function
|
|
1036
|
+
`);n!==r&&await l(t,n)}async function li(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",
|
|
1041
1037
|
UPSTASH_REDIS_REST_TOKEN: "test-token",
|
|
1042
1038
|
`),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",
|
|
1043
1039
|
UPSTASH_REDIS_REST_TOKEN: "test-token",
|
|
@@ -1058,8 +1054,8 @@ vi.mock("@upstash/lock", () => {
|
|
|
1058
1054
|
return { Lock };
|
|
1059
1055
|
});
|
|
1060
1056
|
|
|
1061
|
-
$1`),n!==r&&await l(t,n)}import{readdir as zr,readFile as
|
|
1062
|
-
`)}async function
|
|
1057
|
+
$1`),n!==r&&await l(t,n)}import{readdir as zr,readFile as pi,rm as ne}from"fs/promises";import{join as U}from"path";var di=new Set(["ci.yml","desktop-release.yml"]),ui=["cli","cli:clean","demo:bench","playground:regen","playground:test","playground:test:units","playground:test:build"];async function Hr(e){let t=U(e.projectDir,".github/workflows"),r=await zr(t).catch(()=>[]);for(let n of r)di.has(n)||await ne(U(t,n),{recursive:!0,force:!0})}async function Yr(e){let t=U(e.projectDir,"package.json"),r=await pi(t,"utf-8"),n=JSON.parse(r),o=!1;if(n.scripts){for(let i of ui)i in n.scripts&&(delete n.scripts[i],o=!0);if(!Qe(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," ")+`
|
|
1058
|
+
`)}async function Jr(e){let t=e.frontend==="nextjs"?"apps/web-nuxt":"apps/web-next";await ne(U(e.projectDir,t),{recursive:!0,force:!0})}async function Wr(e){e.docs||await ne(U(e.projectDir,"apps/docs"),{recursive:!0})}async function qr(e){let t=e.frontend==="nextjs"?"docs/nuxt":"docs/next";await ne(U(e.projectDir,t),{recursive:!0,force:!0}),await ne(U(e.projectDir,"docs/index.mdx"))}async function Xr(e){if(e.desktop)return;await ne(U(e.projectDir,"apps/desktop"),{recursive:!0});let t=U(e.projectDir,"packages/i18n/translations");for(let r of await zr(t,{withFileTypes:!0}))r.isDirectory()&&await ne(U(t,r.name,"desktop.json"),{force:!0});await ne(U(e.projectDir,".github/workflows/desktop-release.yml"))}import{join as mi}from"path";async function Zr(e){let t=fi(e);await l(mi(e.projectDir,".github/workflows/ci.yml"),t)}function fi(e){let t=N[e.databaseProvider].managed,r=e.cacheProvider==="upstash",n=e.frontend==="nuxt",o=t?"":` services:
|
|
1063
1059
|
postgres:
|
|
1064
1060
|
image: postgres:18-alpine
|
|
1065
1061
|
env:
|
|
@@ -1073,13 +1069,13 @@ $1`),n!==r&&await l(t,n)}import{readdir as zr,readFile as Hr,rm as O}from"fs/pro
|
|
|
1073
1069
|
--health-interval 10s
|
|
1074
1070
|
--health-timeout 5s
|
|
1075
1071
|
--health-retries 5
|
|
1076
|
-
`,
|
|
1072
|
+
`,i=t?"postgres://test:test@localhost:5432/saas_test":"postgres://postgres:postgres@localhost:5432/saas",s=r?`
|
|
1077
1073
|
UPSTASH_REDIS_REST_URL: https://test.upstash.io
|
|
1078
1074
|
UPSTASH_REDIS_REST_TOKEN: test-token`:"",a=(()=>{switch(e.paymentProvider){case"stripe":return`
|
|
1079
1075
|
STRIPE_SECRET_KEY: test
|
|
1080
1076
|
STRIPE_WEBHOOK_SECRET: test`;case"polar":return`
|
|
1081
1077
|
POLAR_ACCESS_TOKEN: test
|
|
1082
|
-
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=>
|
|
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=>`
|
|
1083
1079
|
${S.name}: test`).join("")).join("");return`# Deployment is handled by the hosting platform (Vercel, Coolify, etc.)
|
|
1084
1080
|
# which auto-deploys on push. CI runs in parallel as a quality gate.
|
|
1085
1081
|
# For PR-based workflows, enable GitHub branch protection to require CI before merging.
|
|
@@ -1124,7 +1120,7 @@ ${n?` # vue-tsc on web-nuxt OOMs on the GitHub runner's default heap once
|
|
|
1124
1120
|
name: Test
|
|
1125
1121
|
runs-on: ubuntu-latest
|
|
1126
1122
|
timeout-minutes: 10
|
|
1127
|
-
${
|
|
1123
|
+
${o} env:
|
|
1128
1124
|
CONTENT_API_KEY: test-contentapi-key-16chars${s}${a}${d}
|
|
1129
1125
|
STORAGE_REGION: test
|
|
1130
1126
|
STORAGE_ENDPOINT: http://test
|
|
@@ -1137,48 +1133,48 @@ ${i} env:
|
|
|
1137
1133
|
- uses: ./.github/actions/setup
|
|
1138
1134
|
|
|
1139
1135
|
- name: Write root env
|
|
1140
|
-
run: echo "DATABASE_URL=${
|
|
1136
|
+
run: echo "DATABASE_URL=${i}" > .env
|
|
1141
1137
|
|
|
1142
1138
|
- run: pnpm test
|
|
1143
|
-
`}import{readFile as
|
|
1144
|
-
`))}async function
|
|
1145
|
-
`))}async function
|
|
1139
|
+
`}import{readFile as Qr}from"fs/promises";import{existsSync as gi}from"fs";import{join as Ft}from"path";var en="@repo/database";function hi(e){return e?"pnpm -F @repo/database reset && pnpm -F @repo/database push --force":"pnpm -F @repo/database migrate"}function yi(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 vi(e,t,r){let n=await Qr(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(en)||(o.scripts={...o.scripts,[t]:`${r} && ${i}`},await l(e,JSON.stringify(o,null," ")+`
|
|
1140
|
+
`))}async function Si(e,t){let r=gi(e)?JSON.parse(await Qr(e,"utf-8")):{$schema:"https://openapi.vercel.sh/vercel.json"},n=r.buildCommand?.trim()||"pnpm build";n.includes(en)||(r.buildCommand=`${t} && ${n}`,await l(e,JSON.stringify(r,null," ")+`
|
|
1141
|
+
`))}async function tn(e){let t=hi(e.demo===!0),r=yi(e.architecture,e.frontend),n=Ft(e.projectDir,"apps",r);switch(e.deploymentTarget){case"node":await vi(Ft(n,"package.json"),"start",t);return;case"vercel":await Si(Ft(n,"vercel.json"),t);return;default:{let o=e.deploymentTarget;throw new Error(`generateDeployScripts: unhandled deployment target "${String(o)}"`)}}}import{readFile as Ei}from"fs/promises";import{existsSync as wi}from"fs";import{join as St}from"path";var bi=["stripe","polar"];async function rn(e){let t=St(e.projectDir,"packages/payments/src"),r=e.paymentProvider,n=`// Active payment provider. To switch, change the path below to
|
|
1146
1142
|
// "./polar/index" or "./none/index" (other folders are kept in place).
|
|
1147
1143
|
export { ops } from "./${r}/index";
|
|
1148
|
-
`;await l(St(t,"providers/index.ts"),n),await l(St(t,"index.ts"),await
|
|
1149
|
-
`).filter(s=>!
|
|
1150
|
-
`)}import{readdir as
|
|
1151
|
-
`).filter(
|
|
1152
|
-
`).length&&await l(e,
|
|
1153
|
-
`))}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
|
|
1154
|
-
`),await l(
|
|
1155
|
-
`)}import{relative as
|
|
1156
|
-
`);
|
|
1157
|
-
${f}`:s.message))}else
|
|
1158
|
-
`),
|
|
1159
|
-
`),
|
|
1160
|
-
`),M.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=z[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 F("--frontend <type>","frontend framework").choices([...Fe])).addOption(new F("--architecture <type>","fullstack or separate").choices([...et])).addOption(new F("--payment <provider>","payment provider").choices([...tt])).addOption(new F("--email <provider>","email provider").choices([...rt])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").addOption(new F("--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 F("--currency <code>","default currency for billing").choices([...le])).addOption(new F("--deploy <target>","deployment target").choices([...pe])).addOption(new F("--database <provider>","database provider").choices([...de])).addOption(new F("--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 F("--demo","first-party demo build: keep sample content, mark site non-indexable - requires CI API key").hideHelp()).addOption(new F("--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.0");let r,n;try{r=Pn(e),n=ss(e.templateVersion)}catch(u){E.cancel(P(u)),process.exit(1)}let i=E.spinner(),o;try{o=await Ae({apiKey:e.apiKey,prompt:!e.yes})}catch(u){E.cancel(P(u)),process.exit(1)}e.demo&&Bt(o)!==os&&(E.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let s=Z(o),a=async()=>{let u=await ne(s),b=u.latest,W=n??b;if(n&&!u.versions.some(se=>se.version===W))throw new Error(`Template version "${n}" is not available.`);return{latestVersion:b,selectedVersion:W}};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 x&&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=Z(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 x&&b.status===401?"Invalid API key.":P(b)),process.exit(1)}}else E.cancel(P(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)}),W;try{W=await Rt(s,b())}catch(se){let K=dt(se);if(!K?.lastAllowedVersion)throw se;i.stop("License activation failed."),e.yes&&(E.cancel(`${K.message} Re-run with --template-version ${K.lastAllowedVersion}.`),process.exit(1));let be=await E.confirm({message:`Your update window has ended. Continue with v${K.lastAllowedVersion} (the last version your license covers)?`});(E.isCancel(be)||!be)&&(E.cancel("Setup cancelled."),process.exit(0)),h=K.lastAllowedVersion,i.start(`Activating license for v${h}...`),W=await Rt(s,b())}f={token:W.token,keyHash:Bt(o),installId:u},i.stop("License activated.")}catch(u){i.stop("License activation failed."),E.cancel(P(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 I={...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(P(u)),process.exit(1)}let oe;i.start("Generating project files...");try{if({dockerComposeGenerated:oe}=await Et(I),!e.demo){let u=await De(S);await l(rs(S,lt),JSON.stringify(u,null," ")+`
|
|
1161
|
-
`),await
|
|
1162
|
-
`),
|
|
1163
|
-
`)}if(a.stop("Baseline template stored."),!
|
|
1164
|
-
`),a.stop("Baseline hashes computed.")}if(await l(Ye(r,sr),JSON.stringify({currentVersion:
|
|
1165
|
-
`),
|
|
1166
|
-
|
|
1167
|
-
${a??"_No changelog available for this release._"}`)}return{text:
|
|
1168
|
-
|
|
1169
|
-
`),title:`Changelog v${r} \u2192 v${n}`}}async function
|
|
1170
|
-
`);
|
|
1171
|
-
`),
|
|
1172
|
-
`).map(s=>s.trim()).filter(s=>s&&!s.startsWith("#"));
|
|
1144
|
+
`;await l(St(t,"providers/index.ts"),n),await l(St(t,"index.ts"),await ki(t,r))}async function ki(e,t){let r=St(e,"index.ts");if(!wi(r))throw new Error(`generatePaymentBarrel: expected ${r} to exist - did packages/payments move?`);let n=await Ei(r,"utf-8"),o=bi.filter(s=>s!==t);return n.split(`
|
|
1145
|
+
`).filter(s=>!o.some(a=>s.includes(`./providers/${a}/`))).join(`
|
|
1146
|
+
`)}import{readdir as Ai,readFile as Ti}from"fs/promises";import{join as nn}from"path";async function on(e){if(e.demo)return;let t=e.appName.trim()||e.projectName,r=JSON.stringify(t).slice(1,-1),n=nn(e.projectDir,"packages/i18n/translations"),o;try{o=await Ai(n)}catch{return}for(let i of o){let s=nn(n,i,"web.json"),a;try{a=await Ti(s,"utf-8")}catch{continue}let d=a.replaceAll("GenerateSaaS",r);d!==a&&await l(s,d)}}import{readFile as Ii}from"fs/promises";import{join as sn}from"path";var Pi=[".nuxt/",".nuxt",".nitro/",".nitro",".output/",".output","_locales/"],_i=[".next/",".next",".svelte-kit/",".svelte-kit",".wrangler/",".wrangler",".dev.vars"];async function cn(e){let r=e.frontend==="nuxt"?_i:Pi;await an(sn(e.projectDir,".gitignore"),r),await an(sn(e.projectDir,".dockerignore"),r)}async function an(e,t){let r;try{r=await Ii(e,"utf-8")}catch{return}let n=new Set(t),o=r.split(`
|
|
1147
|
+
`).filter(i=>!n.has(i.trim()));o.length!==r.split(`
|
|
1148
|
+
`).length&&await l(e,o.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 Zr(e),await Yr(e),await Jr(e),await Wr(e),await qr(e),await Xr(e),await tn(e),await rn(e),await on(e),await cn(e),{dockerComposeGenerated:r}}import{basename as pn,join as dn,relative as Oi}from"path";import{createHash as ln}from"crypto";import{readFile as Ri}from"fs/promises";async function wt(e){let t=await Ri(e);return ln("sha256").update(t).digest("hex")}function Bt(e){return ln("sha256").update(e).digest("hex")}var xi=new Set(["data",j]);function Di(e){let t=e.split("/");for(let r of t)if(xt.has(r)||Dt.has(r)||xi.has(r)||r.startsWith(".env")&&!r.includes("example"))return!0;return!1}function un(e,t){return{projectName:e.projectName??pn(t),appName:e.appName??e.projectName??pn(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 mn(e,t){let n=(await he(e.projectDir,e.projectDir,Di)).sort(),o=await Promise.all(n.map(async a=>[ge(Oi(e.projectDir,a)),await wt(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,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(dn(e.projectDir,Q),JSON.stringify(s,null," ")+`
|
|
1150
|
+
`),await l(dn(e.projectDir,or),JSON.stringify(i,null," ")+`
|
|
1151
|
+
`)}import{relative as Ci}from"path";async function De(e){let r=(await he(e,e,Te)).sort(),n=await Promise.all(r.map(async o=>[ge(Ci(e,o)),await wt(o)]));return Object.fromEntries(n)}import{copyFile as Ni,mkdir as Li,rm as $i}from"fs/promises";import{dirname as ji,join as fn,relative as Ui}from"path";import{existsSync as Vi}from"fs";async function Gt(e,t){Vi(t)&&await $i(t,{recursive:!0,force:!0});let r=await he(e,e,Te);for(let n of r){let o=ge(Ui(e,n)),i=fn(t,o);await Li(ji(i),{recursive:!0}),await Ni(n,i)}}async function gn(e,t){await Gt(e,fn(t,pt))}import{existsSync as Mi}from"fs";import{readFile as hn,readdir as Fi}from"fs/promises";import{join as Y,dirname as Bi,resolve as Gi,sep as Ki}from"path";import{fileURLToPath as zi}from"url";var He={"claude-code":".claude/skills",cursor:".cursor/skills",codex:".agents/skills","gemini-cli":".gemini/skills",windsurf:".windsurf/skills"},Jl=Object.values(He),Kt="generatesaas-update",yn=Bi(zi(import.meta.url));function Hi(){let e=Y(yn,"skill","content");return Mi(e)?e:Y(yn,"content")}function zt(e){return!e||e.length===0?[]:e.map(t=>He[t])}async function Ht(e,t,r,n){let o=zt(n);for(let i of o){let s=Y(e,i,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__",i)),await l(Y(d,".gitkeep"),"");for(let[h,y]of Object.entries(r)){let f=Gi(a,h);f.startsWith(a+Ki)&&await l(f,y)}}}async function vn(e,t){let r=Hi(),n=await hn(Y(r,"SKILL.md"),"utf-8"),o=Y(r,"scripts"),i=await Fi(o),s={};for(let a of i)a!==".gitkeep"&&(s[a]=await hn(Y(o,a),"utf-8"));await Ht(e,n,s,t)}import{execFile as Yi,execFileSync as Ji}from"child_process";import{access as Sn,readFile as Wi}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 Ji(t,[e],{stdio:"ignore"}),!0}catch{return!1}}function ye(e,t,r,n=3e5){return new Promise((o,i)=>{Yi(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
|
+
`);i(new Error(f?`${s.message}
|
|
1153
|
+
${f}`:s.message))}else o()})})}async function En(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 wn(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 bn(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 kn(e){try{return await Sn(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 An(e){if(!ve("pnpm"))return!1;try{await Sn(Yt(e,".git"))}catch{return!1}try{let t=JSON.parse(await Wi(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 Tn(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 ${V.dim(`# ${i}`)}`)}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 i=[];i.push(`App ${V.cyan("http://localhost:3000")}`),e.architecture==="separate"&&i.push(`API ${V.cyan("http://localhost:3010")}`),e.dockerServices.includes("mailpit")&&i.push(`Mailpit ${V.cyan("http://localhost:8025")}`),e.dockerServices.includes("inngest")&&i.push(`Inngest ${V.cyan("http://localhost:8288")}`),Ce.note(i.join(`
|
|
1155
|
+
`),V.yellow("Dev Tools"))}let n=[],o=qi(e);o.length>0&&n.push(`Set in production: ${V.dim(o.join(", "))}`),n.push("pnpm db:push # Run database migrations"),n.push(Xi(e)),Ce.note(n.join(`
|
|
1156
|
+
`),V.yellow("Deployment"))}function qi(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 Xi(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 In(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,ot,"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 oe={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 Pn(e){let t=e.projectName??oe.projectName,r=e.projectDir??`./${t}`,n=e.appName??at(t),o=e.deploymentTarget??oe.deploymentTarget,i=K[o]?.edgeRuntime??!1,s=e.databaseProvider??(i?"neon":oe.databaseProvider),a=e.cacheProvider??(i?"upstash":oe.cacheProvider),d=e.emailProvider??(i?"resend":oe.emailProvider),h=e.dockerServices??(i?oe.dockerServices.filter(f=>f!=="postgres"&&f!=="redis"):oe.dockerServices),y={...oe,...e,projectName:t,appName:n,projectDir:r,deploymentTarget:o,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(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 ns from"picocolors";var os="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";function is(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 _n(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([...it])).option("--blog","enable blog").option("--no-blog","disable blog").option("--docs","include the docs app (apps/docs, Fumadocs)").option("--no-docs","exclude the docs app").option("--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 ss(t?{...r,apiKey:t}:r)})}async function ss(e){let t=performance.now();er("1.15.1");let r,n;try{r=In(e),n=is(e.templateVersion)}catch(u){E.cancel(I(u)),process.exit(1)}let o=E.spinner(),i;try{i=await Ae({apiKey:e.apiKey,prompt:!e.yes})}catch(u){E.cancel(I(u)),process.exit(1)}e.demo&&Bt(i)!==os&&(E.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let s=X(i),a=async()=>{let u=await re(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}};o.start("Verifying access...");let d,h;try{({latestVersion:d,selectedVersion:h}=await a()),o.stop("Access verified."),fe(i)}catch(u){if(o.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."),i=await Ke(),s=X(i),o.start("Verifying access...");try{({latestVersion:d,selectedVersion:h}=await a()),o.stop("Access verified."),fe(i)}catch(b){o.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=Pn(r):y=await nr(r);let f;o.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;o.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,o.start(`Activating license for v${h}...`),J=await Rt(s,b())}f={token:J.token,keyHash:Bt(i),installId:u},o.stop("License activated.")}catch(u){o.stop("License activation failed."),E.cancel(I(u)),process.exit(1)}let S=rs(y.projectDir);if(Zi(S)&&Qi(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"&&es(S,{recursive:!0,force:!0})}let T={...y,projectDir:S,version:h,...e.demo?{docs:!1}:{}};o.start("Downloading template...");try{await ut(s,h,S),o.stop("Template downloaded.")}catch(u){o.stop("Download failed."),E.cancel(I(u)),process.exit(1)}let ie;o.start("Generating project files...");try{if({dockerComposeGenerated:ie}=await Et(T),!e.demo){let u=await De(S);await l(ts(S,lt),JSON.stringify(u,null," ")+`
|
|
1157
|
+
`),await gn(S,S)}await vn(S,T.aiTools),await mn(T,f),o.stop("Project files generated.")}catch(u){o.stop("Generation failed."),E.cancel(I(u)),process.exit(1)}await En(S);let x=await wn(S);x&&T.demo!==!0&&e.dbMigration!==!1&&await bn(S),await kn(S),x&&await An(S);let $=ve("docker"),Z=Pt(T).map(u=>u.key).filter(u=>!T.credentials?.[u]);Tn(T,{pnpmInstalled:x,dockerComposeGenerated:ie,dockerAvailable:$,skippedCredentials:Z}),tr(),E.log.info(ns.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as On}from"fs";import{readFile as xn}from"fs/promises";import{join as Ye,resolve as ds}from"path";import*as _ from"@clack/prompts";import Ne from"picocolors";import{mkdtemp as as,rm as cs}from"fs/promises";import{tmpdir as ls}from"os";import{join as ps}from"path";async function Wt(e,t,r,n){let o=await as(ps(ls(),"generatesaas-stage-"));try{await ut(e,t,o),await Et({...r,projectDir:o}),await Gt(o,n)}finally{await cs(o,{recursive:!0,force:!0})}}function Rn(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=Rn(e),n=Rn(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 Dn(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=ds(t.cwd??process.cwd()),n=Ye(r,Q),o;try{o=JSON.parse(await xn(n,"utf-8"))}catch{_.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let i;try{i=await Ae()}catch(d){_.cancel(I(d)),process.exit(1)}let s=X(i),a=_.spinner();try{a.start("Verifying access...");let d;try{d=await re(s)}catch(u){throw u instanceof R&&u.status===401?new Error("Your saved API key was rejected. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):u}a.stop("Access verified."),fe(i),a.start("Fetching latest skill files...");let h=await ar(s,d.latest);await Ht(r,h.skillMd,h.scripts,o.aiTools);let y=zt(o.aiTools);if(a.stop("Skills updated."),_.log.success(`Skill files installed to ${Ne.cyan(y.length.toString())} locations.`),o.version===d.latest){_.log.info(`Already on the latest version (${o.version}).`);return}if(o.licenseToken)try{let u=await lr(s,{currentToken:o.licenseToken,newVersion:d.latest});o.licenseToken=u.token,u.licenseKeyHash&&(o.licenseKeyHash=u.licenseKeyHash),await l(n,JSON.stringify(o,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=un(o,r),S=Ye(r,ir);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:ie}=await us(s,d,o.version);T&&_.note(T,ie);let x=Ye(r,lt),$=Ye(r,pt),we=!On($),Z=!On(x);if(we){if(a.start("Building baseline template (one-time migration)..."),await Wt(s,o.version,f,$),Z){let u=await De($);await l(x,JSON.stringify(u,null," ")+`
|
|
1159
|
+
`)}if(a.stop("Baseline template stored."),!Z){let u=await ms(x,$);u>0&&_.log.warn(`Rebuilt baseline differs from the original for ${u} file(s) (the CLI's shaping evolved since this project was scaffolded). Classification still follows the committed template-hashes.json; upstream diffs for those files may include unrelated noise.`)}}else if(Z){a.start("Computing baseline template hashes...");let u=await De($);await l(x,JSON.stringify(u,null," ")+`
|
|
1160
|
+
`),a.stop("Baseline hashes computed.")}if(await l(Ye(r,sr),JSON.stringify({currentVersion:o.version,targetVersion:d.latest,changelog:T,stagedAt:new Date().toISOString()},null," ")+`
|
|
1161
|
+
`),_.log.info(`Update staged: ${Ne.cyan(o.version)} \u2192 ${Ne.cyan(d.latest)}`),o.aiTools&&o.aiTools.length>0){let u=o.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 us(e,t,r){let n=t.latest,o=t.versions.filter(s=>bt(s.version,r)>0&&bt(s.version,n)<=0).sort((s,a)=>bt(s.version,a.version));if(o.length<=1)return{text:await _t(e,n),title:`Changelog v${n}`};let i=[];for(let s of o){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]":"";i.push(`# v${s.version}${d}${h}
|
|
1162
|
+
|
|
1163
|
+
${a??"_No changelog available for this release._"}`)}return{text:i.join(`
|
|
1164
|
+
|
|
1165
|
+
`),title:`Changelog v${r} \u2192 v${n}`}}async function ms(e,t){let r=JSON.parse(await xn(e,"utf-8")),n=await De(t),o=0;for(let[i,s]of Object.entries(r))Te(i)||n[i]!==s&&o++;for(let i of Object.keys(n))i in r||o++;return o}import*as L from"@clack/prompts";import F from"picocolors";import{readFile as fs}from"fs/promises";import{join as gs,resolve as hs}from"path";function Cn(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=hs(t.cwd??process.cwd()),n=gs(r,Q),o;try{o=JSON.parse(await fs(n,"utf-8"))}catch{L.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let i=[`Version: ${F.cyan(o.version)}`,`Frontend: ${F.cyan(o.frontend)}`,o.deploymentTarget?`Deploy target: ${F.cyan(o.deploymentTarget)}`:null,o.databaseProvider?`Database: ${F.cyan(o.databaseProvider)}`:null,o.cacheProvider?`Cache: ${F.cyan(o.cacheProvider)}`:null,o.aiTools&&o.aiTools.length>0?`AI tools: ${F.cyan(o.aiTools.join(", "))}`:null].filter(Boolean).join(`
|
|
1166
|
+
`);L.note(i,F.bold("Project Status"));let s=L.spinner();s.start("Checking for updates...");try{let a=await Ae(),d=X(a),y=(await re(d)).latest;o.version===y?(s.stop("Up to date."),L.log.success(`Already on the latest version (${F.green(y)})`)):(s.stop("Update available."),L.log.warning(`Update available: ${F.yellow(o.version)} \u2192 ${F.green(y)}`),L.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?L.log.warning("Invalid API key. Run `generatesaas auth` to update it, or set GENERATESAAS_API_KEY."):L.log.warning(`Could not check for updates: ${I(a)}`)}})}import{readFile as ys}from"fs/promises";import*as A from"@clack/prompts";import k from"picocolors";function vs(){return process.env.GENERATESAAS_API_KEY??Ge()}function Ss(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 Es(e){return{verdict:e.verdict??"unknown",ejectedAt:e.ejectedAt??null}}function ws(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 Nn(e,t){let r=process.env.GENERATESAAS_API_URL??Be,n=vs();e.start("Cross-referencing license records...");try{let o=n?Ss(await pr(r,n,{lkh:t.lkh,nid:t.nid,domain:t.domain})):Es(await Ot(r,{token:t.token,domain:t.domain}));return e.stop(`${k.green("Checked")} - records cross-referenced`),ws(o)}catch(o){return e.stop(`${k.yellow("Skipped")} - ${I(o)}`),null}}function bs(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 Ln(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 ks(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 $n(e){let t=A.spinner(),r=null,n="no candidates";for(let s of ks(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=jn(e);return s?await Nn(t,{domain:s})??!1:!1}let o;try{o=bs(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:"),Ln(o),!1}return Ln(o),await Nn(t,{token:r,lkh:typeof o.lkh=="string"?o.lkh:void 0,nid:typeof o.nid=="string"?o.nid:void 0,domain:jn(e)})??!0}function jn(e){try{return new URL(/^https?:\/\//i.test(e)?e:`https://${e}`).hostname}catch{return}}function Un(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 ys(r.file,"utf-8")).split(`
|
|
1168
|
+
`).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 $n(s)&&i++,A.log.info("");A.log.success(`${i}/${o.length} sites verified.`)}else await $n(t)||process.exit(1)})}import{existsSync as As,rmSync as Ts}from"fs";import*as B from"@clack/prompts";function Vn(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){As(q)?(Ts(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(),o=X(n),i=B.spinner();i.start("Verifying API key...");try{await re(o),i.stop("API key verified."),fe(n),B.log.success("API key saved.")}catch(s){i.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 Is,readFileSync as Zt,writeFileSync as Mn}from"fs";import{join as Se}from"path";import*as O from"@clack/prompts";var Ps=["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"],_s=[{file:"packages/api/src/routes/inngest.ts",removals:[`import { licenseHeartbeatFunction } from "../functions/maintenance/license-heartbeat";
|
|
1173
1169
|
`,` licenseHeartbeatFunction,
|
|
1174
1170
|
`]},{file:"packages/api/src/routes/internal/index.ts",removals:[`import licenseRoutes from "./license";
|
|
1175
1171
|
`,` .route("/license", licenseRoutes)
|
|
1176
|
-
`]}];function
|
|
1177
|
-
`).filter(
|
|
1178
|
-
`);return n===r?!1:(
|
|
1172
|
+
`]}];function Rs(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)?(Is(e,{recursive:!0}),!0):!1}function Os(e,t){if(!kt(e))return!1;let r=Zt(e,"utf-8"),n=r;for(let o of t)n=n.replace(o,"");return n===r?!1:(Mn(e,n,"utf-8"),!0)}function xs(e){let t=Se(e,".gitignore");if(!kt(t))return!1;let r=Zt(t,"utf-8"),n=r.split(`
|
|
1173
|
+
`).filter(o=>!o.includes(".generatesaas")).join(`
|
|
1174
|
+
`);return n===r?!1:(Mn(t,n,"utf-8"),!0)}function Fn(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=Se(t,Q),n;try{n=JSON.parse(Zt(r,"utf-8"))}catch{O.cancel("No GenerateSaaS project found in this directory."),process.exit(1)}let o=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(o)&&(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 i=[],s=[];for(let a of Rs(n.aiTools))Xt(Se(t,a))&&i.push(a);for(let a of Ps)Xt(Se(t,a))&&i.push(a);Xt(Se(t,j))&&i.push(j+"/");for(let a of _s){let d=Se(t,a.file);Os(d,a.removals)?s.push(a.file):kt(d)&&O.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}xs(t)&&s.push(".gitignore");for(let a of i)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 Ds().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("1.15.1").addHelpText("after",`
|
|
1179
1175
|
Examples:
|
|
1180
1176
|
$ generatesaas init Interactive setup
|
|
1181
1177
|
$ generatesaas init -n my-app -y Quick setup with defaults
|
|
1182
1178
|
$ generatesaas status Check for updates
|
|
1183
1179
|
$ generatesaas auth Set or update API key
|
|
1184
|
-
`);
|
|
1180
|
+
`);_n(Ee);Dn(Ee);Cn(Ee);Un(Ee);Vn(Ee);Fn(Ee);Ee.parseAsync().catch(e=>{Bn.cancel("An unexpected error occurred."),console.error(e),process.exit(1)});
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "generatesaas",
|
|
3
|
-
"version": "1.15.
|
|
3
|
+
"version": "1.15.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI for scaffolding and managing GenerateSaaS projects",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"author": "GenerateSaaS",
|
|
8
|
+
"homepage": "https://generatesaas.com",
|
|
6
9
|
"bin": {
|
|
7
10
|
"generatesaas": "./dist/index.js"
|
|
8
11
|
},
|