generatesaas 3.9.0 → 3.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +3 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import{Command as qp}from"commander";import*as Us from"@clack/prompts";import{ex
|
|
|
5
5
|
`),x=[O!=="none"?` Payment: ${g.cyan(Ct[O].label)} (${Oe})`:` Payment: ${g.dim("none")}`,` Credits: ${ht?g.cyan("Yes"):g.dim("No")}`,` AI features: ${N?g.cyan("Yes"):g.dim("No")}`,...N?[` Runner: ${ue?g.cyan("Yes"):g.dim("No")}`]:[],...Ce?[]:[` Web AI: ${g.dim("No")} (app-first)`],` Email: ${g.cyan(Lt[q].label)}`,` Observability: ${J.length===0?g.dim("none"):g.cyan(J.map(dr=>$t[dr].label).join(", "))}`,` Multi-tenancy: ${xe?g.cyan("Yes"):g.dim("No")}`,` Projects: ${De?g.cyan("Yes"):g.dim("No")}`,` Docs app: ${Ne?g.cyan("Yes"):g.dim("No")}`,` Desktop app: ${v?g.cyan("Yes"):g.dim("No")}`,` Mobile app: ${G?g.cyan("Yes"):g.dim("No")}`,...se==="none"?[]:[` In-app purch.: ${g.cyan("RevenueCat")}`],` Rev. sharing: ${D?g.cyan("Yes"):g.dim("No")}`].join(`
|
|
6
6
|
`),Ae=[g.bold("Project"),l,"",g.bold("Infrastructure"),u,"",g.bold("Features"),x];if(Dt.length>0){let dr=Dt.map(tn=>{let Ms=ft[tn.key]?g.green("provided"):g.dim("skipped");return` ${tn.key}: ${Ms}`}).join(`
|
|
7
7
|
`);Ae.push("",g.bold("Credentials"),dr)}h.note(Ae.join(`
|
|
8
|
-
`),"Summary");let en=await h.confirm({message:"Proceed with these settings?"});(h.isCancel(en)||!en)&&(h.cancel("Setup cancelled."),process.exit(0))}return{projectName:n,appName:i,projectDir:o,architecture:I,deploymentTarget:a,databaseProvider:_,cacheProvider:f,paymentProvider:O,emailProvider:q,observability:J,multiTenancy:xe,projects:De,docs:Ne,desktop:v,mobile:G,mobilePurchases:se,ai:N,webAi:Ce,runner:ue,revenueSharing:D,credits:ht,defaultCurrency:Oe,...Object.keys(ft).length>0?{credentials:ft}:{},...e?.baseUrl!==void 0?{baseUrl:e.baseUrl}:{},...pr!==void 0?{demo:pr}:{}}}import{createReadStream as _n,existsSync as eo}from"fs";import{mkdir as to}from"fs/promises";import{join as ro}from"path";import{Readable as no}from"stream";import{pipeline as Ir}from"stream/promises";import{extract as io}from"tar";import{join as Fe}from"path";import{homedir as Hs}from"os";var wt=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",Q=".generatesaas",we=Fe(Q,"manifest.json"),wn="nextjs",br="nuxt",wr="2.4.1",vn=Fe(Q,"hashes.json"),Mt=Fe(Q,"template-hashes.json"),Ft=Fe(Q,"template"),kn=Fe(Q,"staging"),En=Fe(Q,"staging.json"),ve=Fe(Hs(),".generatesaas");var F=class extends Error{constructor(r,n,i){super(n);this.status=r;this.body=i}status;body;name="ApiError"};function ke(e){return{apiKey:e,baseUrl:wt}}async function Ie(e,t,r){let n=`${e.baseUrl}${t}`,i=await fetch(n,{...r,headers:{...r?.headers,Authorization:`Bearer ${e.apiKey}`,"User-Agent":"generatesaas-cli/3.
|
|
8
|
+
`),"Summary");let en=await h.confirm({message:"Proceed with these settings?"});(h.isCancel(en)||!en)&&(h.cancel("Setup cancelled."),process.exit(0))}return{projectName:n,appName:i,projectDir:o,architecture:I,deploymentTarget:a,databaseProvider:_,cacheProvider:f,paymentProvider:O,emailProvider:q,observability:J,multiTenancy:xe,projects:De,docs:Ne,desktop:v,mobile:G,mobilePurchases:se,ai:N,webAi:Ce,runner:ue,revenueSharing:D,credits:ht,defaultCurrency:Oe,...Object.keys(ft).length>0?{credentials:ft}:{},...e?.baseUrl!==void 0?{baseUrl:e.baseUrl}:{},...pr!==void 0?{demo:pr}:{}}}import{createReadStream as _n,existsSync as eo}from"fs";import{mkdir as to}from"fs/promises";import{join as ro}from"path";import{Readable as no}from"stream";import{pipeline as Ir}from"stream/promises";import{extract as io}from"tar";import{join as Fe}from"path";import{homedir as Hs}from"os";var wt=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",Q=".generatesaas",we=Fe(Q,"manifest.json"),wn="nextjs",br="nuxt",wr="2.4.1",vn=Fe(Q,"hashes.json"),Mt=Fe(Q,"template-hashes.json"),Ft=Fe(Q,"template"),kn=Fe(Q,"staging"),En=Fe(Q,"staging.json"),ve=Fe(Hs(),".generatesaas");var F=class extends Error{constructor(r,n,i){super(n);this.status=r;this.body=i}status;body;name="ApiError"};function ke(e){return{apiKey:e,baseUrl:wt}}async function Ie(e,t,r){let n=`${e.baseUrl}${t}`,i=await fetch(n,{...r,headers:{...r?.headers,Authorization:`Bearer ${e.apiKey}`,"User-Agent":"generatesaas-cli/3.10.0"}});if(!i.ok){let o,s;try{s=await i.json(),o=s.error??`API ${i.status}: ${t}`}catch{o=`API ${i.status}: ${t}`}throw new F(i.status,o,s)}return i}import{existsSync as Ys,mkdirSync as Ws,readFileSync as zs,writeFileSync as qs}from"fs";import{dirname as Js}from"path";import*as Re from"@clack/prompts";function vt(){if(!Ys(ve))return null;try{let e=JSON.parse(zs(ve,"utf-8"));return e.apiKey?e.apiKey:(e.token&&!e.apiKey&&Re.log.warning(`Found old GitHub token in ${ve}. Run 'generatesaas init' to set up your API key.`),null)}catch{return null}}function Ve(e){Ws(Js(ve),{recursive:!0}),qs(ve,`${JSON.stringify({apiKey:e},null," ")}
|
|
9
9
|
`,{mode:384})}async function it(e){if(e?.apiKey)return e.apiKey;let t=process.env.GENERATESAAS_API_KEY;if(t)return t;let r=vt();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 kt()}async function kt(){let e=await Re.text({message:"Enter your GenerateSaaS API key:",placeholder:"gs_live_...",validate:t=>{if(!t?.trim())return"API key is required."}});return Re.isCancel(e)&&(Re.cancel("Setup cancelled."),process.exit(0)),e.trim()}import{readFile as Xs}from"fs/promises";async function Te(e){let t=process.env.GENERATESAAS_VERSION_INDEX;return t?JSON.parse(await Xs(t,"utf-8")):process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{latest:"0.0.0-ci",versions:[{version:"0.0.0-ci",date:new Date().toISOString(),breaking:!1}],entitlements:null}:await(await Ie(e,"/versions")).json()}async function Sn(e,t){if(process.env.GENERATESAAS_OFFLINE_LICENSE==="1")return null;try{return await(await Ie(e,`/changelog/${encodeURIComponent(t)}`)).text()}catch(r){if(r instanceof F&&r.status===404)return null;throw r}}async function Pn(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{skills:{"generatesaas-update":{skillMd:`# Offline skill stub
|
|
10
10
|
`,scripts:{},references:{}}}}:await(await Ie(e,`/skills/${encodeURIComponent(t)}`)).json()}function Vt(e){if(!(e instanceof F)||e.status!==403)return null;let t=e.body;return!t||t.code!=="update_window_expired"?null:{message:e.message,lastAllowedVersion:t.lastAllowedVersion??null}}var vr="nextjs";function An(e){return{frontend:vr,architecture:e.architecture,deployTarget:e.deploymentTarget,database:e.databaseProvider,cache:e.cacheProvider,payment:e.paymentProvider,email:e.emailProvider,multiTenancy:e.multiTenancy,blog:!0,docs:e.docs,desktop:e.desktop,mobile:e.mobile,runner:e.runner,credits:e.credits,revenueSharing:e.revenueSharing,currency:e.defaultCurrency}}async function kr(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:"offline-test-token",licenseId:"offline-test-license-id",installId:t.installId}:await(await Ie(e,"/license/sign",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function In(e,t){return process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?{token:t.currentToken}:await(await Ie(e,"/license/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function Er(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 Rn(e,t,r){let n=await fetch(`${e}/license/inspect`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${t}`},body:JSON.stringify(r)});if(!n.ok){let i=await n.json().catch(()=>null);throw new Error(i?.error??`Inspect endpoint returned ${n.status}`)}return await n.json()}var Sr=new Set([".git","node_modules",".pnpm-store",".env",".env.test",".turbo",".data","dist",".next",".next-e2e-web",".next-e2e-desktop",".svelte-kit",".wrangler",".devcontainer","playwright-report","test-results",".drive-out",".mastra"]),Pr=new Set(["pnpm-lock.yaml"]);function st(e){if(Ar(e))return!0;for(let t of e.split("/"))if(Pr.has(t))return!0;return!1}var Zs=new Set(["data","mksaas","references","scripts",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","vitest.config.ts","README.md","TODO.md","OVERVIEW.md","HANDOFF.md"]),Tn=["scripts/docs-lint.ts"],Qs=["docs/superpowers","packages/cli","packages/cli-api","packages/config/tests/runner-brand-lockstep.test.ts","packages/database/tests/no-migration-history.test.ts","apps/runner","docs/agentrunner","infra/docker-compose.yml",".claude/commands",".claude/skills/web-port",".claude/skills/web-port-workspace",".claude/settings.local.json",".claude/worktrees"];function Ar(e){let t=e.split("/");for(let r of t)if(Sr.has(r))return!0;if(Tn.includes(e)||Tn.some(r=>r.startsWith(`${e}/`)))return!1;if(Zs.has(t[0]))return!0;for(let r of Qs)if(e===r||e.startsWith(`${r}/`))return!0;return!1}async function Bt(e,t,r){await to(r,{recursive:!0});let n=process.env.GENERATESAAS_TEMPLATE_TARBALL_DIR;if(n){let a=ro(n,`v${t}.tar.gz`);if(!eo(a))throw new Error(`GENERATESAAS_TEMPLATE_TARBALL_DIR is set but ${a} does not exist.`);await Ir(_n(a),Rr(r));return}let i=process.env.GENERATESAAS_TEMPLATE_TARBALL;if(i){await Ir(_n(i),Rr(r));return}let o=await Ie(e,`/template/${encodeURIComponent(t)}`);if(!o.body)throw new Error("Empty response body");let s=no.fromWeb(o.body);await Ir(s,Rr(r))}function Rr(e){return io({cwd:e,strip:1,filter:t=>{let r=t.replace(/^[^/]+\//,"");return r?!Ar(r):!0},sync:!1})}import{readdir as Gt,readFile as ot,rm as xn,rmdir as so,writeFile as Et}from"fs/promises";import{join as H}from"path";async function Dn(e){let t;try{t=await Gt(e,{withFileTypes:!0})}catch{return!1}let r=!1;for(let n of t){let i=H(e,n.name);if(n.isDirectory()){await Dn(i)&&(r=!0);continue}if(n.name.startsWith("_")){r=!0;continue}await xn(i,{force:!0})}return r||await so(e).catch(()=>{}),r}async function Nn(e){let t=H(e,"packages/content"),r=[];try{r=(await Gt(t,{withFileTypes:!0})).filter(s=>s.isDirectory()).map(s=>s.name)}catch{return}let n=new Set;for(let s of r){let a=H(t,s),d;try{d=(await Gt(a,{withFileTypes:!0})).filter(m=>m.isDirectory())}catch{continue}for(let m of d)n.add(m.name),await Dn(H(a,m.name))}let i=await lo(e),o=new Set([...n].filter(s=>i===void 0||i.has(s)));await Promise.all([...o].map(s=>xn(H(e,"apps/web/public/images",s),{recursive:!0,force:!0}))),await uo(e,o)}var oo="packages/config/src/index.ts",ao=/\bcontent:\s*\{[\s\S]*?\bsections:\s*\[([\s\S]*?)\]/,co=/\bslug:\s*"([^"]+)"/g;async function lo(e){let t;try{t=await ot(H(e,oo),"utf-8")}catch{return}let r=ao.exec(t)?.[1];if(r===void 0)return;let n=[...r.matchAll(co)].map(i=>i[1]).filter(i=>i!==void 0);return n.length>0?new Set(n):void 0}var po="packages/translate/.meta/mdx-translation-meta.json";async function uo(e,t){if(t.size===0)return;let r=H(e,po),n;try{n=JSON.parse(await ot(r,"utf-8"))}catch{return}if(typeof n!="object"||n===null)return;let i=n;if(typeof i.files!="object"||i.files===null)return;let o=i.files,s={};for(let[a,d]of Object.entries(o)){let m=a.split("/"),w=m.length>1?m[0]:void 0,b=m[m.length-1]?.startsWith("_")===!0;w!==void 0&&t.has(w)&&!b||(s[a]=d)}await Et(r,`${JSON.stringify({...i,files:s},null," ")}
|
|
11
11
|
`)}var mo="packages/config/src/blog.ts";async function Cn(e){let t=H(e,mo),r=await ot(t,"utf-8"),n=On(On(r,"blogCategories",t),"blogAuthors",t);await Et(t,n)}function On(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 Ln(e){let t=H(e,"packages/i18n/translations"),r;try{r=await Gt(t)}catch{return}for(let n of r){let i=H(t,n,"web.json"),o;try{o=await ot(i,"utf-8")}catch{continue}let s=JSON.parse(o);!s.content?.categories||s.content.categories.blog===void 0||(s.content.categories.blog={},await Et(i,`${JSON.stringify(s,null," ")}
|
|
@@ -1624,7 +1624,7 @@ function posthogConfigured(): boolean {
|
|
|
1624
1624
|
${b}`:s.message))}else i()})})}async function ps(e){if(!He("pnpm"))return U.log.warn("pnpm not found. Skipping lockfile regeneration."),!1;try{return await Ke("pnpm",["install","--lockfile-only","--no-frozen-lockfile","--config.minimumReleaseAge=0"],e),!0}catch(t){let r=t instanceof Error?t.message:String(t);return U.log.warn(`Lockfile regeneration failed: ${r}`),U.log.warn("Deploys using --frozen-lockfile may fail."),!1}}async function ds(e){if(!He("pnpm"))return U.log.warn("pnpm not found. Skipping dependency installation."),U.log.info("Install pnpm: https://pnpm.io/installation"),!1;let t=U.spinner();t.start("Installing dependencies (this may take a minute)...");try{return await Ke("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 U.log.warn(`pnpm install failed: ${n}`),U.log.warn("You can run it manually later."),!1}}async function us(e){if(!He("pnpm"))return!1;let t=U.spinner();t.start("Generating baseline database migration...");try{return await Ke("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 U.log.warn(`Could not generate baseline migration: ${n}`),U.log.warn("Run 'pnpm -F @repo/database generate' before your first deploy."),!1}}async function ms(e){try{return await ls(zr(e,".git")),U.log.info("Git repository already exists, skipping init."),!0}catch{}if(!He("git"))return U.log.warn("git not found. Skipping repository initialization."),!1;let t=U.spinner();t.start("Initializing git repository...");try{return await Ke("git",["init"],e),await Ke("git",["add","-A"],e),await Ke("git",["commit","--no-verify","-m","Initial commit from GenerateSaaS"],e),t.stop("Git repository initialized."),!0}catch{return t.stop("Git initialization failed."),U.log.warn("You can run git init manually later."),!1}}async function hs(e){if(!He("pnpm"))return!1;try{await ls(zr(e,".git"))}catch{return!1}try{let t=JSON.parse(await sp(zr(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 Ke("pnpm",["exec","simple-git-hooks"],e),!0}catch{return U.log.warn("Could not install git hooks. Run 'pnpm exec simple-git-hooks' manually."),!1}}import*as Ye from"@clack/prompts";import re from"picocolors";function fs(e,t){let r=Qe(e);t.dockerAvailable||Ye.log.warn("Docker not found. Install Docker to run local services: https://docs.docker.com/get-docker/"),t.baselineMigrationMissing&&Ye.log.warn("No database migration was generated (the install did not complete). Run `pnpm install` then `pnpm --filter @repo/database generate` and commit the result BEFORE deploying - otherwise your first deploy pushes the schema untracked.");let n=[];n.push(`cd ${e.projectDir}`),t.pnpmInstalled||n.push("pnpm install"),n.push(`pnpm infra ${re.dim(`# ${r.map(a=>sn[a].label).join(", ")}`)}`),n.push(`pnpm dev ${re.dim("# http://localhost:3000")}`),t.skippedCredentials.length>0&&(n.push(""),n.push(re.dim("Fill in remaining TODO values in .env"))),Ye.note(n.join(`
|
|
1625
1625
|
`),re.yellow("Start Development"));let i=[];i.push(`App ${re.cyan("http://localhost:3000")}`),e.architecture==="separate"&&i.push(`API ${re.cyan("http://localhost:3010")}`),r.includes("mailpit")&&i.push(`Mailpit ${re.cyan("http://localhost:8025")}`),r.includes("inngest")&&i.push(`Inngest ${re.cyan("http://localhost:8288")}`),Ye.note(i.join(`
|
|
1626
1626
|
`),re.yellow("Dev Tools"));let o=[],s=op(e);s.length>0&&o.push(`Set in production: ${re.dim(s.join(", "))}`),o.push("pnpm db:setup # Apply the database schema"),o.push(ap(e)),Ye.note(o.join(`
|
|
1627
|
-
`),re.yellow("Deployment"))}function op(e){let t=["DATABASE_URL","BETTER_AUTH_SECRET","INNGEST_APP_ID","INNGEST_EVENT_KEY","INNGEST_BASE_URL","INNGEST_SIGNING_KEY"];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 ap(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 ut(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 _e(e,t){let r=ut(e),n=ut(t);if(!r||!n)return 0;for(let i=0;i<3;i++)if(r[i]!==n[i])return r[i]-n[i];return 0}function or(e,t){return e.versions.find(r=>r.version===t)?.cli}function gs(e){return process.env.GENERATESAAS_TEMPLATE_TARBALL||process.env.GENERATESAAS_TEMPLATE_TARBALL_DIR||process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?!0:ut(e)===null}function ar(e,t,r="3.
|
|
1627
|
+
`),re.yellow("Deployment"))}function op(e){let t=["DATABASE_URL","BETTER_AUTH_SECRET","INNGEST_APP_ID","INNGEST_EVENT_KEY","INNGEST_BASE_URL","INNGEST_SIGNING_KEY"];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 ap(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 ut(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 _e(e,t){let r=ut(e),n=ut(t);if(!r||!n)return 0;for(let i=0;i<3;i++)if(r[i]!==n[i])return r[i]-n[i];return 0}function or(e,t){return e.versions.find(r=>r.version===t)?.cli}function gs(e){return process.env.GENERATESAAS_TEMPLATE_TARBALL||process.env.GENERATESAAS_TEMPLATE_TARBALL_DIR||process.env.GENERATESAAS_OFFLINE_LICENSE==="1"?!0:ut(e)===null}function ar(e,t,r="3.10.0"){if(gs(r))return{ok:!0};let n=or(e,e.latest);return!n||ut(n)===null?{ok:!0}:_e(r,n)>=0?{ok:!0}:{ok:!1,message:`Your installed generatesaas CLI (v${r}) is too old for template v${e.latest}, which needs CLI v${n} or newer. Run npx generatesaas@latest ${t} so npm fetches the current CLI. If the release happened minutes ago, npm may still be propagating it - retry shortly.`}}function ys(e,t,r="3.10.0"){if(gs(r))return{ok:!0};let n=or(e,t);return!n||ut(n)===null?{ok:!0}:_e(r,n)===0?{ok:!0}:{ok:!1,message:`Template v${t} must be scaffolded by CLI v${n} exactly (the CLI whose generators produced that release). Run npx generatesaas@${n} init.`}}import mp from"picocolors";var hp="a10a6fb9d7cadde32e37dad52059d17b5d2b916b08c76d8fbcc99982e9a3d87f";async function fp(e){let t=await crypto.subtle.digest("SHA-256",new TextEncoder().encode(`generatesaas-demo:${e}`)),r=[...new Uint8Array(t).subarray(0,16)].map(n=>n.toString(16).padStart(2,"0")).join("");return`${r.slice(0,8)}-${r.slice(8,12)}-${r.slice(12,16)}-${r.slice(16,20)}-${r.slice(20,32)}`}function bs(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 ne("--architecture <type>","fullstack or separate").choices([...et])).addOption(new ne("--payment <provider>","payment provider").choices([...tt])).addOption(new ne("--email <provider>","email provider").choices([...rt])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").option("--projects","enable projects (a switcher that scopes a user's work)").option("--no-projects","disable projects").option("--docs","include the docs app (apps/docs, Fumadocs)").option("--no-docs","exclude the docs app").option("--desktop","include the desktop app (apps/desktop, built with Electron)").option("--no-desktop","exclude the desktop app").option("--mobile","include the mobile app (apps/mobile, built with Expo)").option("--no-mobile","exclude the mobile app").addOption(new ne("--mobile-purchases <lane>","in-app purchases in the mobile app: revenuecat adds the store billing lane on top of your web payment provider, none keeps web billing only").choices([...Me])).option("--ai","include the AI features (chat, models, automations, integrations); unlocks the runner").option("--no-ai","exclude the AI features").option("--web-ai","show the AI features in the web app (the default)").option("--no-web-ai","desktop-first: keep the AI features out of the web app, which is reduced to account, login and billing - the desktop or mobile app is the AI product. Requires --desktop or --mobile, plus --ai, and cannot be combined with --runner").option("--runner","include the runner feature - your web users connect a machine that runs their own AI subscriptions for this app").option("--no-runner","exclude the backend runner feature").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)").addOption(new ne("--currency <code>","default currency for billing").choices([...fe])).addOption(new ne("--preset <preset>","infrastructure preset: serverless (Vercel + Neon + Upstash) or selfhost (Docker or Node + Postgres + Redis); --deploy/--database/--cache override it piecewise").choices([...Ue])).addOption(new ne("--deploy <target>","deployment target").choices([...ge])).addOption(new ne("--database <provider>","database provider").choices([...ye])).addOption(new ne("--cache <provider>","cache provider").choices([...be])).option("--observability <providers>",`comma-separated observability providers (${K.join(", ")}); anything unselected ships no SDK`).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 ne("--demo","first-party demo build: keep sample content, mark site non-indexable - requires CI API key").hideHelp()).addOption(new ne("--no-db-migration","skip generating the baseline DB migration (internal: demos/CI/playground)").hideHelp()).action(async(t,r)=>{await gp(t?{...r,apiKey:t}:r)})}async function gp(e){let t=performance.now();rn("3.10.0");let r;try{r=gn(e)}catch(v){k.cancel(j(v)),process.exit(1)}let n=k.spinner(),i;try{i=await it({apiKey:e.apiKey,prompt:!e.yes})}catch(v){k.cancel(j(v)),process.exit(1)}e.demo&&Br(i)!==hp&&(k.cancel("--demo is restricted to first-party demo deployments."),process.exit(1));let o=ke(i),s,a=async()=>{s=await Te(o);let v=ar(s,"init");if(!v.ok)throw new Error(v.message);return{latestVersion:s.latest,selectedVersion:s.latest,desktopAllowed:s.entitlements?.desktopAllowed??!0,runnerAllowed:s.entitlements?.runnerAllowed??!0,mobileAllowed:s.entitlements?.mobileAllowed??!0}};n.start("Verifying access...");let d,m,w=!0,b=!0,P=!0;try{({latestVersion:d,selectedVersion:m,desktopAllowed:w,runnerAllowed:b,mobileAllowed:P}=await a()),n.stop("Access verified."),Ve(i)}catch(v){if(n.stop("Access verification failed."),v instanceof F&&v.status===401){e.yes&&(k.cancel("Invalid API key. Cannot prompt in non-interactive mode."),process.exit(1)),k.log.warning("Invalid API key."),i=await kt(),o=ke(i),n.start("Verifying access...");try{({latestVersion:d,selectedVersion:m,desktopAllowed:w,runnerAllowed:b,mobileAllowed:P}=await a()),n.stop("Access verified."),Ve(i)}catch(M){n.stop("Access verification failed."),k.cancel(M instanceof F&&M.status===401?"Invalid API key.":j(M)),process.exit(1)}}else k.cancel(j(v)),process.exit(1)}k.log.success(`Latest version: ${d}`),r.desktop===!0&&!w&&(k.cancel("The desktop app is available on the Pro plan and up. Upgrade your plan at generatesaas.com."),process.exit(1)),r.runner===!0&&!b&&(k.cancel("The runner is available on the Pro plan and up. Upgrade your plan at generatesaas.com."),process.exit(1)),r.mobile===!0&&!P&&(k.cancel("The mobile app is available on the Pro plan and up. Upgrade your plan at generatesaas.com."),process.exit(1));let I;e.yes?I=yn({...r,...w?{}:{desktop:!1},...b?{}:{runner:!1},...P?{}:{mobile:!1}}):I=await bn(r,{desktopAllowed:w,runnerAllowed:b,mobileAllowed:P});let _;n.start("Activating license...");try{let v=e.demo&&I.baseUrl?await fp(I.baseUrl):crypto.randomUUID(),M=()=>({frontend:vr,version:m,installId:v,projectName:I.projectName,options:An(I)}),G;try{G=await kr(o,M())}catch(se){let N=Vt(se);if(!N?.lastAllowedVersion||!s)throw se;n.stop("License activation failed.");let de=ys(s,N.lastAllowedVersion);if(de.ok||(k.cancel(`${N.message} ${de.message}`),process.exit(1)),e.yes){let ue=or(s,N.lastAllowedVersion);k.cancel(`${N.message} Re-run ${ue?`npx generatesaas@${ue} init`:"init"} without --yes to continue with v${N.lastAllowedVersion} (the last version your license covers).`),process.exit(1)}let X=await k.confirm({message:`Your update window has ended. Continue with v${N.lastAllowedVersion} (the last version your license covers)?`});(k.isCancel(X)||!X)&&(k.cancel("Setup cancelled."),process.exit(0)),m=N.lastAllowedVersion,n.start(`Activating license for v${m}...`),G=await kr(o,M())}_={token:G.token,keyHash:Br(i),installId:G.installId??v},n.stop("License activated.")}catch(v){n.stop("License activation failed."),k.cancel(j(v)),process.exit(1)}let f=up(I.projectDir);if(cp(f)&&lp(f).length>0)if(e.yes)k.log.info(`Directory ${f} is not empty. Merging (keeping existing files, overwriting conflicts).`);else{let M=await k.select({message:`Directory ${f} 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"}]});(k.isCancel(M)||M==="cancel")&&(k.cancel("Setup cancelled."),process.exit(0)),M==="overwrite"&&pp(f,{recursive:!0,force:!0})}let O={...I,projectDir:f,version:m,...e.demo?{docs:!1}:{}};n.start("Downloading template...");try{await Bt(o,m,f),n.stop("Template downloaded.")}catch(v){n.stop("Download failed."),k.cancel(j(v)),process.exit(1)}n.start("Generating project files...");try{if(await ir(O),!e.demo){let v=await dt(f);await c(dp(f,Mt),`${JSON.stringify(v,null," ")}
|
|
1628
1628
|
`),await ss(f,f)}await cs(f),await ns(O,_),n.stop("Project files generated.")}catch(v){n.stop("Generation failed."),k.cancel(j(v)),process.exit(1)}!await ps(f)&&O.demo===!0&&(k.cancel("Lockfile regeneration failed on a demo build - refusing to ship a stale lockfile."),process.exit(1));let q=await ds(f),J=O.demo!==!0&&e.dbMigration!==!1,xe=!1;J&&q&&(await us(f),xe=!0),await ms(f),q&&await hs(f);let De=He("docker"),Je=yr(O).map(v=>v.key).filter(v=>!O.credentials?.[v]);fs(O,{pnpmInstalled:q,dockerAvailable:De,skippedCredentials:Je,baselineMigrationMissing:J&&!xe}),nn(),k.log.info(mp.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as cr}from"fs";import{readFile as Ps}from"fs/promises";import{join as We,resolve as kp}from"path";import*as y from"@clack/prompts";import C from"picocolors";import{mkdtemp as yp,rm as bp}from"fs/promises";import{tmpdir as wp}from"os";import{join as vp}from"path";async function qr(e,t,r,n){let i=await yp(vp(wp(),"generatesaas-stage-"));try{await Bt(e,t,i),await ir({...r,projectDir:i}),await Gr(i,n)}finally{await bp(i,{recursive:!0,force:!0})}}function ws(e){return e?"ON":"OFF"}var mt="packages/config/src/index.ts";function vs(e){let t=e.databaseProvider!==void 0&&e.databaseProvider!=="postgres",r=e.cacheProvider!==void 0&&e.cacheProvider!=="redis";return[...e.dockerServices??[]].filter(n=>!(n==="postgres"&&t)&&!(n==="redis"&&r)).sort()}function ks(e){return[...Qe({databaseProvider:e.databaseProvider??"postgres",cacheProvider:e.cacheProvider??"redis",emailProvider:e.emailProvider??"smtp"})].sort()}var Jr=[{key:"errorTracking",severity:"warn",normalize:e=>{e.errorTracking===void 0||e.observability!==void 0||(e.observability=e.errorTracking==="none"?[]:[e.errorTracking])},preserved:()=>!0,describe:()=>"Retired option `errorTracking`: it is now `observability`, an array, because a project may run more than one provider at once. Your recorded choice was migrated across automatically."},{key:"companion",severity:"warn",normalize:e=>{e.companion===void 0||e.runner!==void 0||(e.runner=e.companion)},preserved:()=>!0,describe:()=>"Retired option `companion`: it is now `runner`, because the daemon it names is AgentRunner. Your recorded choice was migrated across automatically - the feature itself is unchanged."},{key:"companionSource",severity:"warn",preserved:()=>!1,describe:e=>`Retired option \`companionSource\`: your project recorded a \`${String(e.companionSource)}\` daemon source. There is now one runner daemon - AgentRunner - which we publish and your users install from its own releases. Your project keeps the runner feature and its own pairing, dispatch and Runners page; it no longer holds or brands a daemon.`},{key:"companionName",severity:"warn",preserved:()=>!1,describe:e=>`Retired option \`companionName\`: your project recorded a branded daemon called ${e.companionName}. Buyers no longer brand or publish a daemon - your users install AgentRunner, which we maintain. Their existing install of your old daemon keeps running until they switch.`},{key:"companionRepoUrl",severity:"warn",preserved:()=>!1,describe:e=>`Retired option \`companionRepoUrl\`: your project pointed its install flow at ${e.companionRepoUrl}. It now points at AgentRunner, the one daemon we publish.`},{key:"companionLocalPath",severity:"warn",preserved:e=>(e.companionLocalPath??"").trim()==="",describe:e=>`Retired option \`companionLocalPath\`: your project recorded a local daemon checkout at ${e.companionLocalPath}. The option is gone, and so is the choice behind it - there is one runner daemon (AgentRunner) and we publish it, so nothing in your project is built from a local checkout any more.`},{key:"desktopAutoRelease",severity:"warn",applies:e=>e.desktop===!0,preserved:e=>e.desktopAutoRelease!==!0,describe:()=>"Retired option `desktopAutoRelease`: your project recorded automatic desktop releases, but the release trigger is no longer an option - cutting a release publishes signed installers from your repository and spends your Actions minutes, so it is always deliberate now. The staged `.github/workflows/desktop-release.yml` runs on `workflow_dispatch` only (the Actions tab). Add a `workflow_run` block to it if you want the CI-chained trigger back."},{key:"billingScope",severity:"ask",applies:e=>e.multiTenancy===!0,preserved:e=>e.billingScope!=="organization",describe:()=>"Retired option `billingScope`: your project recorded organization-scoped billing, and organization billing is GONE - not moved to config, removed. Money now lives only on a USER: an organization has no balance, no plan and no payment-provider customer of its own, and a workspace is funded by that organization's OWNER, whose personal balance every credit spent in it is drawn from. Payments already in flight are safe - a webhook arriving with old organization metadata is credited to that organization's owner rather than dropped - but a STORED balance is not: the `credits` and `plan` columns an organization holds them in are going away, and this update does NOT migrate them for you. MOVE every paying organization's credits and plan onto its owner BEFORE you take new checkouts, or that balance goes with the columns. How should this update proceed?"},{key:"socialProviders",severity:"warn",preserved:e=>(e.socialProviders??[]).length===0,describe:e=>`Retired option \`socialProviders\`: your project recorded ${(e.socialProviders??[]).join(", ")}, but it is now a plain config value that starts empty. Add those IDs back to \`config.auth.socialProviders\` in ${mt} or their sign-in buttons stop rendering.`},{key:"mcpServer",severity:"warn",preserved:e=>e.mcpServer!==!0,describe:()=>`Retired option \`mcpServer\`: your project recorded the outward MCP server ON, but its code now ships to every project behind a config gate generated OFF. Set \`config.mcpServer.enabled\` to \`true\` in ${mt} to keep your /mcp endpoint and its settings tab.`},{key:"emailTracking",severity:"ask",preserved:e=>e.emailTracking!==!1,describe:()=>`Retired option \`emailTracking\`: your project recorded email open/click tracking OFF, but it is now a plain config value generated ON. Applying this update as-is starts tracking opens and clicks for every user you mail. Put it back by setting \`config.email.tracking.enabled\` to \`false\` in ${mt}. How should this update proceed?`},{key:"aiBuiltin",severity:"warn",normalize:e=>{e.aiBuiltin===void 0&&e.aiByok!==void 0&&(e.aiBuiltin=e.aiByok===!1)},applies:e=>e.ai===!0,preserved:e=>e.aiBuiltin===oe(e),describe:e=>`Retired option \`aiBuiltin\`: your project recorded built-in AI models ${ws(e.aiBuiltin)}, but it is now a plain config value generated ${ws(oe(e))} for a project ${oe(e)?"with":"without"} a payment provider. Set \`config.ai.builtin\` to \`${String(e.aiBuiltin===!0)}\` in ${mt} to keep your choice.`},{key:"aiByok",severity:"warn",applies:e=>e.ai===!0,preserved:e=>e.aiByok!==!1,describe:()=>`Retired option \`aiByok\`: your project recorded bring-your-own-key AI OFF, but it is now a plain config value generated ON. Set \`config.ai.byok\` to \`false\` in ${mt} to keep it off.`},{key:"dockerServices",severity:"warn",preserved:e=>vs(e).join(",")===ks(e).join(","),describe:e=>`Retired option \`dockerServices\`: your project recorded ${vs(e).join(", ")||"no services"}, but the local Docker services are now DERIVED from your stack - postgres when you self-host Postgres, redis when you self-host Redis, mailpit on SMTP, inngest always - so an inconsistent selection is no longer possible. Your regenerated \`infra/docker-compose.yml\` will contain ${ks(e).join(", ")}. Add any extra service back by hand; the file is yours to edit.`},{key:"aiTools",severity:"warn",preserved:e=>(e.aiTools??[]).length===an.length,describe:e=>`Retired option \`aiTools\`: your project recorded ${(e.aiTools??[]).join(", ")||"no AI tools"}, but the \`generatesaas-update\` skill bundle now ships to every AI-tool root (${Ge.join(", ")}) so it is there whichever assistant you switch to. Delete the roots you do not use - they are plain text files with no runtime path.`},{key:"blog",severity:"warn",preserved:e=>e.blog!==!1,describe:()=>`Retired option \`blog\`: your project recorded the blog OFF, but content is no longer an option. This update adds the \`/blog\`, \`/alternatives\` and \`/compare\` route code to your project; every one of them 404s until you add a file under \`packages/content/{locale}/{section}/\`, and none of them appears in your navbar, footer, sitemap or llms.txt while it is empty. Delete an entry from \`config.content.sections\` in ${mt} to drop a section for good.`}],Jf=Jr.map(e=>e.key);function Es(e){for(let t of Jr)t.normalize?.(e)}function Ss(e){let t=[];for(let r of Jr){if(r.normalize?.(e),e[r.key]===void 0)continue;(!r.applies||r.applies(e))&&!r.preserved(e)&&t.push({key:r.key,message:r.describe(e),severity:r.severity}),delete e[r.key]}return t}function As(e){e.command("update").description("Update AI skill files and stage template updates").allowExcessArguments(!0).option("--cwd <path>","project directory (default: current directory)").option("--answer <id=choice>","answer a question that would otherwise prompt (repeatable), e.g. --answer desktop-migration=keep-ai",Ep,[]).action(async(t,r)=>{r.args.length>0&&y.log.warn("`auto` is now the default and the argument is ignored.");let n=Sp(t.answer),i=kp(t.cwd??process.cwd()),o=We(i,we),s;try{s=JSON.parse(await Ps(o,"utf-8"))}catch{y.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}s.frontend===br&&(y.cancel(`This project was generated on the Nuxt frontend (\`.generatesaas/manifest.json\` records \`frontend: "${br}"\`).
|
|
1629
1629
|
|
|
1630
1630
|
v${wr} is the last release that ships a Nuxt frontend, and this CLI cannot update or migrate a Nuxt project to Next.js.
|
|
@@ -1685,7 +1685,7 @@ ${b}`}).join(`
|
|
|
1685
1685
|
`]},{file:"packages/api/src/routes/public/v1/health.ts",removals:[`import { manifest } from "../../../lib/manifest";
|
|
1686
1686
|
`],replacements:[{from:'c.json({ status: "success" as const, version: manifest?.version })',to:'c.json({ status: "success" as const })'}]}];function Yp(){return Ge.flatMap(e=>Yr.map(t=>ze(e,t)))}function Zr(e){return lr(e)?(Gp(e,{recursive:!0}),!0):!1}function Wp(e,t,r=[]){if(!lr(e))return!1;let n=Qr(e,"utf-8"),i=n;for(let o of t)i=i.replace(o,"");for(let{from:o,to:s}of r)i=i.replace(o,s);return i===n?!1:($s(e,i,"utf-8"),!0)}function zp(e){let t=ze(e,".gitignore");if(!lr(t))return!1;let r=Qr(t,"utf-8"),n=r.split(`
|
|
1687
1687
|
`).filter(i=>!i.includes(".generatesaas")).join(`
|
|
1688
|
-
`);return n===r?!1:($s(t,n,"utf-8"),!0)}function js(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=ze(t,we),n;try{n=JSON.parse(Qr(r,"utf-8"))}catch{B.cancel("No GenerateSaaS project found in this directory."),process.exit(1)}let i=await B.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(B.isCancel(i)&&(B.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)}),B.log.info("Recorded the opt-out with generatesaas.com (final event - nothing is sent after this).")}catch{B.log.warn("Could not reach generatesaas.com to record the opt-out. Ejecting anyway.")}let o=[],s=[];for(let a of Yp())Zr(ze(t,a))&&o.push(a);for(let a of Kp)Zr(ze(t,a))&&o.push(a);Zr(ze(t,Q))&&o.push(`${Q}/`);for(let a of Hp){let d=ze(t,a.file);Wp(d,a.removals,a.replacements)?s.push(a.file):lr(d)&&B.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}zp(t)&&s.push(".gitignore");for(let a of o)B.log.info(`Deleted ${a}`);for(let a of s)B.log.info(`Modified ${a}`);B.log.success("Ejected successfully. This project is now fully standalone.")})}var qe=new qp().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("3.
|
|
1688
|
+
`);return n===r?!1:($s(t,n,"utf-8"),!0)}function js(e){e.command("eject").description("Remove all GenerateSaaS ties - manifest, license, heartbeat, skills").action(async()=>{let t=process.cwd(),r=ze(t,we),n;try{n=JSON.parse(Qr(r,"utf-8"))}catch{B.cancel("No GenerateSaaS project found in this directory."),process.exit(1)}let i=await B.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(B.isCancel(i)&&(B.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)}),B.log.info("Recorded the opt-out with generatesaas.com (final event - nothing is sent after this).")}catch{B.log.warn("Could not reach generatesaas.com to record the opt-out. Ejecting anyway.")}let o=[],s=[];for(let a of Yp())Zr(ze(t,a))&&o.push(a);for(let a of Kp)Zr(ze(t,a))&&o.push(a);Zr(ze(t,Q))&&o.push(`${Q}/`);for(let a of Hp){let d=ze(t,a.file);Wp(d,a.removals,a.replacements)?s.push(a.file):lr(d)&&B.log.warn(`Could not auto-modify ${a.file} - manually remove license/heartbeat references.`)}zp(t)&&s.push(".gitignore");for(let a of o)B.log.info(`Deleted ${a}`);for(let a of s)B.log.info(`Modified ${a}`);B.log.success("Ejected successfully. This project is now fully standalone.")})}var qe=new qp().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("3.10.0").addHelpText("after",`
|
|
1689
1689
|
Examples:
|
|
1690
1690
|
$ generatesaas init Interactive setup
|
|
1691
1691
|
$ generatesaas init -n my-app -y Quick setup with defaults
|