generatesaas 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{Command as Sn}from"commander";import*as Bt from"@clack/prompts";import{existsSync as Xr,readdirSync as Qr,rmSync as en}from"fs";import{join as tn,resolve as rn}from"path";import{Option as q}from"commander";import*as
|
|
3
|
-
`);p.note(s,"Unavailable on edge runtime")}p.log.info(
|
|
4
|
-
`),c=[a==="separate"?` Deploy target: ${
|
|
5
|
-
`),f=[g!=="none"?` Payment: ${
|
|
6
|
-
`),
|
|
7
|
-
`);
|
|
8
|
-
`),"Summary");let b=await p.confirm({message:"Proceed with these settings?"});(p.isCancel(b)||!b)&&(p.cancel("Setup cancelled."),process.exit(0))}return{projectName:r,appName:o,projectDir:n,frontend:i,architecture:a,deploymentTarget:l,databaseProvider:
|
|
9
|
-
|
|
2
|
+
import{Command as Sn}from"commander";import*as Bt from"@clack/prompts";import{existsSync as Xr,readdirSync as Qr,rmSync as en}from"fs";import{join as tn,resolve as rn}from"path";import{Option as q}from"commander";import*as v from"@clack/prompts";import*as he from"@clack/prompts";import Fe from"picocolors";function We(e){let t=e?` GenerateSaaS v${e} `:" GenerateSaaS ";he.intro(Fe.bgYellow(Fe.black(t)))}function Je(){he.outro(Fe.yellow("Happy building!"))}import*as p from"@clack/prompts";import u from"picocolors";var de={nuxt:{label:"Nuxt",hint:"Vue 3 + Nuxt 4"},nextjs:{label:"Next.js",hint:"coming soon"}},ve={fullstack:{label:"Fullstack",hint:"Nuxt handles API via server routes"},separate:{label:"Separate",hint:"Standalone Hono backend"}},Se={stripe:{label:"Stripe"},polar:{label:"Polar"},none:{label:"None",hint:"disable payments"}},Ee={smtp:{label:"SMTP",hint:"Mailpit for local dev"},ses:{label:"Amazon SES"},resend:{label:"Resend"}},Ie={user:{label:"Per user",hint:"each user has their own subscription"},organization:{label:"Per organization",hint:"org subscription shared by members"}},re={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}},me={"claude-code":{label:"Claude Code"},cursor:{label:"Cursor"},codex:{label:"Codex"},"gemini-cli":{label:"Gemini CLI"},windsurf:{label:"Windsurf"}};var W={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 J={node:{label:"Node.js / Docker",hint:"self-hosted with Dockerfile",edgeRuntime:!1},cloudflare:{label:"Cloudflare Workers",hint:"edge runtime, wrangler.toml",edgeRuntime:!0},vercel:{label:"Vercel",hint:"serverless, vercel.json",edgeRuntime:!0},netlify:{label:"Netlify",hint:"serverless, netlify.toml",edgeRuntime:!0}},U={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"}]}},M={redis:{label:"Redis (self-hosted)",hint:"local Docker, ioredis",managed:!1,envVars:[{key:"REDIS_URL",defaultValue:"redis://localhost:6379"}]},upstash:{label:"Upstash",hint:"serverless Redis",managed:!0,envVars:[{key:"UPSTASH_REDIS_REST_URL",comment:"# TODO: Add your Upstash REST URL"},{key:"UPSTASH_REDIS_REST_TOKEN",comment:"# TODO: Add your Upstash REST token"}]}},ue=[{target:"cloudflare",provider:"postgres",reason:"Cloudflare Workers cannot connect to self-hosted PostgreSQL. Use Neon or Supabase instead."},{target:"cloudflare",provider:"redis",reason:"Cloudflare Workers cannot connect to self-hosted Redis. Use Upstash instead."},{target:"vercel",provider:"redis",reason:"Vercel serverless cannot maintain persistent Redis connections. Consider Upstash."},{target:"netlify",provider:"redis",reason:"Netlify serverless cannot maintain persistent Redis connections. Consider Upstash."}],Ze=["Local file storage (sharp, geoip-lite)","SMTP email (use Resend or SES instead)","Content API git integration"];var Ae=["fullstack","separate"],we=["stripe","polar","none"],_e=["smtp","ses","resend"],be=["postgres","redis","inngest","mailpit"],Pe=["claude-code","cursor","codex","gemini-cli","windsurf"],Re=["user","organization"],Z=["USD","EUR","GBP","CAD","AUD","BRL","JPY"],X=["node","cloudflare","vercel","netlify"],Q=["postgres","neon","supabase"],ee=["redis","upstash"];function Te(e){return e.split("-").map(t=>t.charAt(0).toUpperCase()+t.slice(1)).join(" ")}function ke(e){return/^[a-z][a-z0-9-]*$/.test(e)}function E(e){return e instanceof Error?e.message:String(e)}function S(e){p.isCancel(e)&&(p.cancel("Setup cancelled."),process.exit(0))}function Ke(e){let t=[];return 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})),t}async function Xe(e){let t=!1;p.log.info(u.bold("Project"));let r=e?.projectName??await(async()=>{t=!0;let s=await p.text({message:"Project name:",placeholder:"my-saas",validate:c=>{if(!c?.trim())return"Project name is required.";if(!ke(c))return"Use lowercase letters, numbers, and hyphens only. Must start with a letter."}});return S(s),s})(),o=e?.appName??await(async()=>{t=!0;let s=await p.text({message:"App name:",initialValue:Te(r),validate:c=>{if(!c?.trim())return"App name is required."}});return S(s),s})(),n=e?.projectDir??await(async()=>{t=!0;let s=await p.text({message:"Project location:",initialValue:`./${r}`});return S(s),s==="."?process.cwd():s})();e?.frontend===void 0&&p.log.info("Next.js support is coming soon.");let i=e?.frontend??await(async()=>{t=!0;let s=Object.keys(de).filter(f=>f!=="nextjs"),c=await p.select({message:"Frontend framework:",options:s.map(f=>({value:f,label:de[f].label,hint:de[f].hint}))});return S(c),c})();p.log.info(u.bold("Infrastructure"));let a=e?.architecture??await(async()=>{t=!0;let s=await p.select({message:"Architecture:",options:Ae.map(c=>({value:c,label:ve[c].label,hint:ve[c].hint}))});return S(s),s})(),l=e?.deploymentTarget??"node";if(a==="separate"&&e?.deploymentTarget===void 0){t=!0;let s=await p.select({message:"Deployment target:",options:X.map(c=>({value:c,label:J[c].label,hint:J[c].hint}))});S(s),l=s}let m=e?.databaseProvider??await(async()=>{t=!0;let s=Q.filter(f=>!ue.some(T=>T.target===l&&T.provider===f));if(s.length===1){let f=s[0];return p.log.info(`Auto-selected ${U[f].label} (only compatible option for ${J[l].label}).`),f}let c=await p.select({message:"Database provider:",options:s.map(f=>({value:f,label:U[f].label,hint:U[f].hint}))});return S(c),c})(),y=e?.cacheProvider??await(async()=>{t=!0;let s=ee.filter(f=>!ue.some(T=>T.target===l&&T.provider===f));if(s.length===1){let f=s[0];return p.log.info(`Auto-selected ${M[f].label} (only compatible option for ${J[l].label}).`),f}let c=await p.select({message:"Cache provider:",options:s.map(f=>({value:f,label:M[f].label,hint:M[f].hint}))});return S(c),c})();if(a==="separate"&&J[l].edgeRuntime){let s=Ze.map(c=>` - ${c}`).join(`
|
|
3
|
+
`);p.note(s,"Unavailable on edge runtime")}p.log.info(u.bold("Features"));let g=e?.paymentProvider??await(async()=>{t=!0;let s=await p.select({message:"Payment provider:",options:we.map(c=>({value:c,label:Se[c].label,hint:Se[c].hint}))});return S(s),s})(),R=e?.defaultCurrency??await(async()=>{if(g==="none")return"USD";t=!0;let s=await p.select({message:"Default currency:",options:Z.map(c=>({value:c,label:c,hint:W[c].name}))});return S(s),s})(),N=e?.emailProvider??await(async()=>{t=!0;let s=await p.select({message:"Email provider:",options:_e.map(c=>({value:c,label:Ee[c].label,hint:Ee[c].hint}))});return S(s),s})(),G=e?.multiTenancy??await(async()=>{t=!0;let s=await p.confirm({message:"Enable multi-tenancy (organizations)?",initialValue:!1});return S(s),s})(),I=e?.billingScope??"user";if(G&&e?.billingScope===void 0){t=!0;let s=await p.select({message:"Billing scope:",options:Re.map(c=>({value:c,label:Ie[c].label,hint:Ie[c].hint}))});S(s),I=s}let H=e?.blog??await(async()=>{t=!0;let s=await p.confirm({message:"Enable blog?",initialValue:!1});return S(s),s})(),h=e?.revenueSharing??await(async()=>{t=!0;let s=await p.confirm({message:"Enable revenue sharing? (opt-in MRR leaderboard with dofollow backlinks)",initialValue:!1});return S(s),s})();p.log.info(u.bold("Tooling"));let _=e?.dockerServices??await(async()=>{t=!0;let s=[...be].filter(b=>b!=="mailpit");N==="smtp"&&s.push("mailpit");let c=s.map(b=>({value:b,label:re[b].label,hint:re[b].hint})),f=c.map(b=>b.value).filter(b=>!(b==="postgres"&&(m==="neon"||m==="supabase")||b==="redis"&&y==="upstash")),T=await p.multiselect({message:"Which services should we set up in Docker for you?",options:c,initialValues:f,required:!1});return S(T),T})(),Me=e?.aiTools??await(async()=>{t=!0;let s=Pe.map(f=>({value:f,label:me[f].label})),c=await p.multiselect({message:"Which AI coding tools do you use?",options:s,initialValues:[],required:!1});return S(c),c})(),ye=Ke({databaseProvider:m,cacheProvider:y,paymentProvider:g,emailProvider:N}),le={};if(ye.length>0&&t){p.log.info(u.bold("Credentials")+u.dim(" all optional \u2014 press Enter to skip, fill in .env later"));for(let s of ye)if(t=!0,s.secret){let c=await p.password({message:s.message,mask:"*"});S(c),typeof c=="string"&&c.trim()&&(le[s.key]=c.trim())}else{let c=await p.text({message:s.message,placeholder:s.placeholder});S(c),typeof c=="string"&&c.trim()&&(le[s.key]=c.trim())}}if(t){let s=[` Name: ${u.cyan(r)}`,` App name: ${u.cyan(o)}`,` Location: ${u.cyan(n)}`,` Frontend: ${u.cyan(de[i].label)}`,` Architecture: ${u.cyan(ve[a].label)}`].join(`
|
|
4
|
+
`),c=[a==="separate"?` Deploy target: ${u.cyan(J[l].label)}`:null,` Database: ${u.cyan(U[m].label)}`,` Cache: ${u.cyan(M[y].label)}`,_.length>0?` Docker: ${u.cyan(_.map(pe=>re[pe].label).join(", "))}`:` Docker: ${u.dim("none")}`].filter(Boolean).join(`
|
|
5
|
+
`),f=[g!=="none"?` Payment: ${u.cyan(Se[g].label)} (${R})`:` Payment: ${u.dim("none")}`,` Email: ${u.cyan(Ee[N].label)}`,` Multi-tenancy: ${G?u.cyan("Yes")+` (billing: ${Ie[I].label})`:u.dim("No")}`,` Blog: ${H?u.cyan("Yes"):u.dim("No")}`,` Rev. sharing: ${h?u.cyan("Yes"):u.dim("No")}`,Me.length>0?` AI tools: ${u.cyan(Me.map(pe=>me[pe].label).join(", "))}`:` AI tools: ${u.dim("none")}`].join(`
|
|
6
|
+
`),T=[u.bold("Project"),s,"",u.bold("Infrastructure"),c,"",u.bold("Features"),f];if(ye.length>0){let pe=ye.map(qe=>{let Gt=le[qe.key]?u.green("provided"):u.dim("skipped");return` ${qe.key}: ${Gt}`}).join(`
|
|
7
|
+
`);T.push("",u.bold("Credentials"),pe)}p.note(T.join(`
|
|
8
|
+
`),"Summary");let b=await p.confirm({message:"Proceed with these settings?"});(p.isCancel(b)||!b)&&(p.cancel("Setup cancelled."),process.exit(0))}return{projectName:r,appName:o,projectDir:n,frontend:i,architecture:a,deploymentTarget:l,databaseProvider:m,cacheProvider:y,paymentProvider:g,emailProvider:N,multiTenancy:G,billingScope:I,blog:H,revenueSharing:h,dockerServices:_,aiTools:Me,defaultCurrency:R,...Object.keys(le).length>0?{credentials:le}:{}}}import{rm as Ht}from"fs/promises";import{join as zt}from"path";var Yt=["apps/web-nuxt/content/en/blog","apps/web-nuxt/public/images/blog"];async function Qe(e){await Promise.all(Yt.map(t=>Ht(zt(e,t),{recursive:!0,force:!0})))}import{mkdir as nr}from"fs/promises";import{Readable as ir}from"stream";import{pipeline as or}from"stream/promises";import{extract as ar}from"tar";import{join as ne}from"path";import{homedir as qt}from"os";var De=process.env.GENERATESAAS_API_URL??"https://cli.generatesaas.com",te=".generatesaas",ie=ne(te,"manifest.json"),et=ne(te,"hashes.json"),Ce=ne(te,"template-hashes.json"),tt=ne(te,"staging"),rt=ne(te,"staging.json"),F=ne(qt(),".generatesaas");var w=class extends Error{constructor(r,o){super(o);this.status=r}name="ApiError"};function k(e){return{apiKey:e,baseUrl:De}}async function z(e,t,r){let o=`${e.baseUrl}${t}`,n=await fetch(o,{...r,headers:{...r?.headers,Authorization:`Bearer ${e.apiKey}`,"User-Agent":"generatesaas-cli"}});if(!n.ok){let i;try{i=(await n.json()).error??`API ${n.status}: ${t}`}catch{i=`API ${n.status}: ${t}`}throw new w(n.status,i)}return n}import{existsSync as Wt,readFileSync as Jt,writeFileSync as Zt,mkdirSync as Xt}from"fs";import{dirname as Qt}from"path";import*as Y from"@clack/prompts";function Be(){if(!Wt(F))return null;try{let e=JSON.parse(Jt(F,"utf-8"));return e.apiKey?e.apiKey:(e.token&&!e.apiKey&&Y.log.warning(`Found old GitHub token in ${F}. Run 'generatesaas init' to set up your API key.`),null)}catch{return null}}function K(e){Xt(Qt(F),{recursive:!0}),Zt(F,JSON.stringify({apiKey:e},null," ")+`
|
|
9
|
+
`,{mode:384})}async function oe(e){if(e?.apiKey)return e.apiKey;let t=process.env.GENERATESAAS_API_KEY;if(t)return t;let r=Be();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 B()}async function B(){let e=await Y.text({message:"Enter your GenerateSaaS API key:",placeholder:"gs_live_...",validate:t=>{if(!t?.trim())return"API key is required."}});return Y.isCancel(e)&&(Y.cancel("Setup cancelled."),process.exit(0)),e.trim()}async function D(e){return await(await z(e,"/versions")).json()}async function nt(e,t){try{return await(await z(e,`/changelog/${encodeURIComponent(t)}`)).text()}catch(r){if(r instanceof w&&r.status===404)return null;throw r}}async function it(e,t){return await(await z(e,`/skill/${encodeURIComponent(t)}`)).json()}async function ot(e,t){return await(await z(e,"/license/sign",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function at(e,t){return await(await z(e,"/license/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async function st(e,t){let r=await fetch(`${e}/license/verify`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({token:t})});if(!r.ok)throw new Error(`Verification service returned ${r.status}`);return await r.json()}var er=new Set([".git","node_modules",".pnpm-store",".env",".env.test",".turbo",".nuxt",".output",".data","dist",".next",".svelte-kit",".netlify",".wrangler",".devcontainer","playwright-report","test-results"]),tr=new Set(["data","mksaas",".claude",".cursor",".agents",".codex",".generatesaas",".vscode",".mcp.json","AGENTS.md","TODO.md","OVERVIEW.md","pnpm-lock.yaml"]),rr=["apps/cms","packages/blog","packages/cli","packages/cli-api","scripts/.env","scripts/.meta","infra/docker-compose.yml"];function xe(e){let t=e.split("/");for(let r of t)if(er.has(r))return!0;if(tr.has(t[0]))return!0;for(let r of rr)if(e===r||e.startsWith(r+"/"))return!0;return!1}async function fe(e,t,r){let o=await z(e,`/template/${encodeURIComponent(t)}`);if(!o.body)throw new Error("Empty response body");await nr(r,{recursive:!0});let n=ir.fromWeb(o.body);await or(n,ar({cwd:r,strip:1,filter:i=>{let a=i.replace(/^[^/]+\//,"");return a?!xe(a):!0}}))}import{mkdir as sr,writeFile as cr}from"fs/promises";import{dirname as lr}from"path";async function Oe(e){await sr(e,{recursive:!0})}async function d(e,t){await Oe(lr(e)),await cr(e,t,"utf-8")}async function ct(e){switch(e.cacheProvider){case"redis":await pr(e),await dr(e);break;case"upstash":await mr(e),await ur(e);break}}async function pr(e){await d(`${e.projectDir}/packages/api/src/services/redis.ts`,`import { Redis } from "ioredis";
|
|
10
10
|
import { RedisStore } from "rate-limit-redis";
|
|
11
11
|
import { env } from "../env.js";
|
|
12
12
|
|
|
@@ -63,7 +63,7 @@ export async function withMutex<T>(key: string, ttlMs: number, fn: () => Promise
|
|
|
63
63
|
if (current === lockValue) await redis.del(lockKey);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
`)}async function lt(e){let t=e.appName,r=e.paymentProvider!=="none",o=`import type { AppConfig } from "../types/index.js";
|
|
66
|
+
`)}async function lt(e){let t=e.appName.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),r=e.paymentProvider!=="none",o=`import type { AppConfig } from "../types/index.js";
|
|
67
67
|
|
|
68
68
|
export const config: AppConfig = {
|
|
69
69
|
siteName: "${t}",
|
|
@@ -261,6 +261,7 @@ export type { User, Account, Organization, Member } from "./db/auth.js";
|
|
|
261
261
|
POSTGRES_USER: postgres
|
|
262
262
|
POSTGRES_PASSWORD: postgres
|
|
263
263
|
POSTGRES_DB: saas
|
|
264
|
+
PGDATA: /var/lib/postgresql/data
|
|
264
265
|
volumes:
|
|
265
266
|
- postgres_data:/var/lib/postgresql/data
|
|
266
267
|
healthcheck:
|
|
@@ -325,10 +326,10 @@ export default handle(app);
|
|
|
325
326
|
import { app } from "@repo/api";
|
|
326
327
|
|
|
327
328
|
export default handle(app);
|
|
328
|
-
`}var Ir={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"}]},Ar={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
|
|
329
|
+
`}var Ir={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"}]},Ar={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 $e(e,t){return t?e.map(r=>{let o=t[r.key];return o?{...r,defaultValue:o,comment:void 0}:r}):e}function Le(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 wr(e){let t=crypto.randomUUID(),r=ut(32),o=ut(32),i=["# API Configuration",`API_URL=${e.architecture==="fullstack"?"http://localhost:3000/api":"http://localhost:3010"}`];i.push("","# Database"),Le($e(U[e.databaseProvider].envVars,e.credentials),i),i.push("","# Cache"),Le($e(M[e.cacheProvider].envVars,e.credentials),i),i.push("","# Authentication",`BETTER_AUTH_SECRET=${t}`,"","# Job Queue - Inngest","INNGEST_APP_ID=api",`INNGEST_EVENT_KEY=${r}`,`INNGEST_SIGNING_KEY=${o}`,"INNGEST_BASE_URL=http://127.0.0.1:8288"),e.architecture==="separate"&&i.push("","# API Port (standalone backend)","API_PORT=3010");let a=Ir[e.emailProvider];if(a&&(i.push("","# Email"),Le($e(a,e.credentials),i)),e.paymentProvider!=="none"){let l=Ar[e.paymentProvider];l&&(i.push("","# Payment"),Le($e(l,e.credentials),i))}return i.push(""),i.join(`
|
|
329
330
|
`)}function _r(e){return["# API Configuration",`NUXT_PUBLIC_API_URL=${e.architecture==="fullstack"?"http://localhost:3000/api":"http://localhost:3010"}`,""].join(`
|
|
330
331
|
`)}async function ft(e){let t=wr(e),r=_r(e);if(e.architecture==="fullstack"){let o=r+`
|
|
331
|
-
`+t;await d(`${e.projectDir}/apps/web-nuxt/.env`,o)}else await d(`${e.projectDir}/apps/web-nuxt/.env`,r),await d(`${e.projectDir}/apps/backend/.env`,t)}import{readdir as Pr}from"fs/promises";import{join as Ge,relative as ht}from"path";import{createHash as gt}from"crypto";import{readFile as br}from"fs/promises";async function
|
|
332
|
+
`+t;await d(`${e.projectDir}/apps/web-nuxt/.env`,o)}else await d(`${e.projectDir}/apps/web-nuxt/.env`,r),await d(`${e.projectDir}/apps/backend/.env`,t)}import{readdir as Pr}from"fs/promises";import{join as Ge,relative as ht}from"path";import{createHash as gt}from"crypto";import{readFile as br}from"fs/promises";async function Ne(e){let t=await br(e);return gt("sha256").update(t).digest("hex")}function yt(e){return gt("sha256").update(e).digest("hex")}var Rr=new Set([".git","node_modules",".pnpm-store",".env","data",te]);function Tr(e){let t=e.split("/");for(let r of t)if(Rr.has(r)||r.startsWith(".env")&&!r.includes("example"))return!0;return!1}async function vt(e,t){let r=[],o=await Pr(e,{withFileTypes:!0});for(let n of o){let i=Ge(e,n.name),a=ht(t,i);Tr(a)||(n.isDirectory()?r.push(...await vt(i,t)):n.isFile()&&r.push(i))}return r}async function St(e,t){let o=(await vt(e.projectDir,e.projectDir)).sort(),n=await Promise.all(o.map(async l=>[ht(e.projectDir,l),await Ne(l)])),i=Object.fromEntries(n),a={version:e.version,initialVersion:e.version,repo:"Duzbee/GenerateSaaS",frontend:e.frontend,aiTools:e.aiTools,deploymentTarget:e.deploymentTarget,databaseProvider:e.databaseProvider,cacheProvider:e.cacheProvider,revenueSharing:e.revenueSharing,...t&&{licenseToken:t.token,licenseKeyHash:t.keyHash,installId:t.installId}};await d(Ge(e.projectDir,ie),JSON.stringify(a,null," ")+`
|
|
332
333
|
`),await d(Ge(e.projectDir,et),JSON.stringify(i,null," ")+`
|
|
333
334
|
`)}async function Et(e){if(!(e.architecture!=="separate"||e.deploymentTarget==="node"))switch(e.deploymentTarget){case"cloudflare":await kr(e);break;case"vercel":await Dr(e);break;case"netlify":await Cr(e);break}}async function kr(e){let t=`name = "${e.projectName}-api"
|
|
334
335
|
main = "src/index.ts"
|
|
@@ -547,17 +548,17 @@ ${n}
|
|
|
547
548
|
}
|
|
548
549
|
};
|
|
549
550
|
`,l=`${e.projectDir}/packages/core/src/config/pricing.ts`;await d(l,a)}function Lr(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==="nuxt"?"Nuxt":e.frontend;if(t.push({type:"node-terminal",request:"launch",name:r,command:"pnpm dev",cwd:`\${workspaceFolder}/apps/web-${e.frontend}`,skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),t.push({type:"node-terminal",request:"launch",name:"Inngest",command:"pnpm dev:inngest",cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),t.push({type:"node-terminal",request:"launch",name:"Email",command:"pnpm dev:email",cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"],env:{NODE_ENV:"development"}}),e.paymentProvider==="stripe"){let o=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 ${o}`,cwd:"${workspaceFolder}",skipFiles:["<node_internals>/**"]})}return t}function Nr(e){let t=e.frontend==="nuxt"?"Nuxt":e.frontend,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 At(e){let t={version:"0.2.0",configurations:Lr(e),compounds:Nr(e)};await d(`${e.projectDir}/.vscode/launch.json`,JSON.stringify(t,null," ")+`
|
|
550
|
-
`)}import{readdir as jr}from"fs/promises";import{join as Vr,relative as wt}from"path";async function _t(e,t){let r=[],o=await jr(e,{withFileTypes:!0});for(let n of o){let i=Vr(e,n.name),a=wt(t,i);
|
|
551
|
-
`),
|
|
552
|
-
`),
|
|
553
|
-
`),
|
|
554
|
-
`),await Qe(y),o.stop("Template downloaded.")}catch(h){o.stop("Download failed."),
|
|
555
|
-
`),A.log.success("License refreshed.")}catch{A.log.warn("License refresh skipped.")}if(n.version===
|
|
556
|
-
`)}finally{await cn(
|
|
557
|
-
`),A.log.info(`Update staged: ${se.cyan(n.version)} \u2192 ${se.cyan(
|
|
558
|
-
`)
|
|
559
|
-
`),V.yellow("License Details"))}async function Mt(e){let t=P.spinner(),r=e.replace(/\/+$/,"");t.start(`Checking ${r}/license...`);let o;try{let i=await fetch(`${r}/license`);if(!i.ok)return t.stop(`${V.red("Not found")} \u2014 ${r}/license returned ${i.status}`),!1;if(o=await i.text(),!o||o.split(".").length!==3)return t.stop(`${V.red("Invalid")} \u2014 response is not a JWT`),!1;t.stop(`${V.green("Found")} \u2014 license endpoint responded`)}catch(i){return t.stop(`${V.red("Failed")} \u2014 ${
|
|
560
|
-
`).map(a=>a.trim()).filter(a=>a&&!a.startsWith("#"));n.length===0&&(P.cancel("No URLs found in file."),process.exit(1));let i=0;for(let a of n)await Mt(a)&&i++,P.log.info("");P.log.success(`${i}/${n.length} sites verified.`)}else await Mt(t)||process.exit(1)})}import{existsSync as hn,rmSync as vn}from"fs";import*as
|
|
551
|
+
`)}import{readdir as jr}from"fs/promises";import{join as Vr,relative as wt}from"path";async function _t(e,t){let r=[],o=await jr(e,{withFileTypes:!0});for(let n of o){let i=Vr(e,n.name),a=wt(t,i);xe(a)||(n.isDirectory()?r.push(...await _t(i,t)):n.isFile()&&r.push(i))}return r}async function je(e){let r=(await _t(e,e)).sort(),o=await Promise.all(r.map(async n=>[wt(e,n),await Ne(n)]));return Object.fromEntries(o)}import{existsSync as Ur}from"fs";import{readFile as bt,readdir as Mr}from"fs/promises";import{join as C,dirname as Fr}from"path";import{fileURLToPath as Kr}from"url";var Rt={"claude-code":".claude/skills",cursor:".cursor/skills",codex:".agents/skills","gemini-cli":".gemini/skills",windsurf:".windsurf/skills"},Br=Object.values(Rt),Gr="generatesaas-update",Pt=Fr(Kr(import.meta.url));function Hr(){let e=C(Pt,"skill","content");return Ur(e)?e:C(Pt,"content")}function He(e){return!e||e.length===0?Br:e.map(t=>Rt[t])}async function ze(e,t,r,o){let n=He(o);for(let i of n){let a=C(e,i,Gr),l=C(a,"scripts"),m=C(a,"references");await Oe(l),await Oe(m),await d(C(a,"SKILL.md"),t.replaceAll("__SKILL_ROOT__",i)),await d(C(m,".gitkeep"),"");for(let[y,g]of Object.entries(r))await d(C(l,y),g)}}async function Tt(e,t){let r=Hr(),o=await bt(C(r,"SKILL.md"),"utf-8"),n=C(r,"scripts"),i=await Mr(n),a={};for(let l of i)l!==".gitkeep"&&(a[l]=await bt(C(n,l),"utf-8"));await ze(e,o,a,t)}import{execFile as zr,execFileSync as Yr}from"child_process";import{access as qr}from"fs/promises";import{join as Wr}from"path";import*as j from"@clack/prompts";function Ue(e){try{let t=process.platform==="win32"?"where":"which";return Yr(t,[e],{stdio:"ignore"}),!0}catch{return!1}}function Ve(e,t,r,o=3e5){return new Promise((n,i)=>{zr(e,t,{cwd:r,timeout:o},a=>{a?i(a):n()})})}async function kt(e){if(!Ue("pnpm"))return j.log.warn("pnpm not found. Skipping dependency installation."),j.log.info("Install pnpm: https://pnpm.io/installation"),!1;let t=j.spinner();t.start("Installing dependencies (this may take a minute)...");try{return await Ve("pnpm",["install"],e),t.stop("Dependencies installed."),!0}catch{return t.stop("Dependency installation failed."),j.log.warn("pnpm install failed. You can run it manually later."),!1}}async function Dt(e){try{return await qr(Wr(e,".git")),j.log.info("Git repository already exists, skipping init."),!0}catch{}if(!Ue("git"))return j.log.warn("git not found. Skipping repository initialization."),!1;let t=j.spinner();t.start("Initializing git repository...");try{return await Ve("git",["init"],e),await Ve("git",["add","-A"],e),await Ve("git",["commit","-m","Initial commit from GenerateSaaS"],e),t.stop("Git repository initialized."),!0}catch{return t.stop("Git initialization failed."),j.log.warn("You can run git init manually later."),!1}}import*as ae from"@clack/prompts";import x from"picocolors";function Ct(e,t){t.dockerComposeGenerated&&!t.dockerAvailable&&ae.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(a=>re[a].label).join(", ");r.push(`pnpm infra ${x.dim(`# ${i}`)}`)}if(r.push(`pnpm dev ${x.dim("# http://localhost:3000")}`),t.skippedCredentials.length>0&&(r.push(""),r.push(x.dim("Fill in remaining TODO values in .env"))),ae.note(r.join(`
|
|
552
|
+
`),x.yellow("Start Development")),t.dockerComposeGenerated){let i=[];i.push(`App ${x.cyan("http://localhost:3000")}`),e.architecture==="separate"&&i.push(`API ${x.cyan("http://localhost:3010")}`),e.dockerServices.includes("mailpit")&&i.push(`Mailpit ${x.cyan("http://localhost:8025")}`),e.dockerServices.includes("inngest")&&i.push(`Inngest ${x.cyan("http://localhost:8288")}`),ae.note(i.join(`
|
|
553
|
+
`),x.yellow("Dev Tools"))}let o=[],n=Jr(e);n.length>0&&o.push(`Set in production: ${x.dim(n.join(", "))}`),o.push("pnpm db:push # Run database migrations"),o.push(Zr(e)),ae.note(o.join(`
|
|
554
|
+
`),x.yellow("Deployment"))}function Jr(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 Zr(e){switch(e.deploymentTarget){case"node":return"Deploy with Docker or your preferred Node.js host";case"cloudflare":return"wrangler deploy # Deploy to Cloudflare Workers";case"vercel":return"vercel deploy # Deploy to Vercel";case"netlify":return"netlify deploy # Deploy to Netlify"}}function $t(e){let t={};if(e.name!==void 0){if(!ke(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&&(t.frontend=e.frontend),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.revenueSharing!==void 0&&(t.revenueSharing=e.revenueSharing),e.docker!==void 0&&(t.dockerServices=Ot(e.docker,be,"docker service")),e.aiTools!==void 0&&(t.aiTools=Ot(e.aiTools,Pe,"AI tool")),e.currency!==void 0){if(!Z.includes(e.currency))throw new Error(`Invalid currency "${e.currency}". Valid values: ${Z.join(", ")}`);t.defaultCurrency=e.currency}if(e.deploy!==void 0){if(!X.includes(e.deploy))throw new Error(`Invalid deployment target "${e.deploy}". Valid values: ${X.join(", ")}`);t.deploymentTarget=e.deploy}if(e.database!==void 0){if(!Q.includes(e.database))throw new Error(`Invalid database provider "${e.database}". Valid values: ${Q.join(", ")}`);t.databaseProvider=e.database}if(e.cache!==void 0){if(!ee.includes(e.cache))throw new Error(`Invalid cache provider "${e.cache}". Valid values: ${ee.join(", ")}`);t.cacheProvider=e.cache}return t}var xt={projectName:"my-saas",frontend:"nuxt",architecture:"fullstack",paymentProvider:"stripe",emailProvider:"smtp",multiTenancy:!1,billingScope:"user",blog:!1,revenueSharing:!1,dockerServices:["postgres","redis","inngest"],aiTools:[],defaultCurrency:"USD",deploymentTarget:"node",databaseProvider:"postgres",cacheProvider:"redis"};function Lt(e){let t=e.projectName??xt.projectName,r=e.projectDir??`./${t}`,o=e.appName??Te(t),n={...xt,...e,projectName:t,appName:o,projectDir:r};for(let i of ue){if(n.deploymentTarget!==i.target)continue;let a=n.databaseProvider===i.provider?"database":"cache";if(n.databaseProvider===i.provider||n.cacheProvider===i.provider)throw new Error(`Incompatible: --deploy ${i.target} + --${a} ${i.provider}. ${i.reason}`)}return n}function Ot(e,t,r){if(e.trim()==="")return[];let o=e.split(",").map(i=>i.trim()).filter(Boolean),n=o.filter(i=>!t.includes(i));if(n.length>0)throw new Error(`Invalid ${r}(s): ${n.join(", ")}. Valid values: ${t.join(", ")}`);return o}import nn from"picocolors";function Nt(e){e.command("init").description("Scaffold a new GenerateSaaS project").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 q("--architecture <type>","fullstack or separate").choices([...Ae])).addOption(new q("--payment <provider>","payment provider").choices([...we])).addOption(new q("--email <provider>","email provider").choices([..._e])).option("--org","enable multi-tenancy (organizations)").option("--no-org","disable multi-tenancy").addOption(new q("--billing-scope <scope>","billing scope (requires --org)").choices([...Re])).option("--blog","enable blog").option("--no-blog","disable blog").option("--revenue-sharing","enable revenue sharing").option("--no-revenue-sharing","disable revenue sharing").option("--docker <services>","comma-separated: postgres,redis,inngest,mailpit").option("--ai-tools <tools>","comma-separated: claude-code,cursor,codex,gemini-cli,windsurf").addOption(new q("--currency <code>","default currency for billing").choices([...Z])).addOption(new q("--deploy <target>","deployment target").choices([...X])).addOption(new q("--database <provider>","database provider").choices([...Q])).addOption(new q("--cache <provider>","cache provider").choices([...ee])).option("--api-key <key>","API key (skips interactive prompt)").option("-y, --yes","accept defaults for unspecified options (non-interactive)").action(async t=>{await on(t)})}async function on(e){let t=performance.now();We("0.6.3");let r;try{r=$t(e)}catch(h){v.cancel(E(h)),process.exit(1)}let o=v.spinner(),n;try{n=await oe({apiKey:e.apiKey,prompt:!e.yes})}catch(h){v.cancel(E(h)),process.exit(1)}let i=k(n);o.start("Verifying access...");let a;try{a=(await D(i)).latest,o.stop("Access verified."),K(n)}catch(h){if(o.stop("Access verification failed."),h instanceof w&&h.status===401){v.log.warning("Invalid API key."),n=await B(),i=k(n),o.start("Verifying access...");try{a=(await D(i)).latest,o.stop("Access verified."),K(n)}catch(_){o.stop("Access verification failed."),v.cancel(_ instanceof w&&_.status===401?"Invalid API key.":E(_)),process.exit(1)}}else v.cancel(E(h)),process.exit(1)}v.log.success(`Latest version: ${a}`);let l;o.start("Activating license...");try{let h=crypto.randomUUID();l={token:(await ot(i,{frontend:"nuxt",version:a,installId:h})).token,keyHash:yt(n),installId:h},o.stop("License activated.")}catch(h){o.stop("License activation failed."),v.cancel(E(h)),process.exit(1)}let m;e.yes?m=Lt(r):m=await Xe(r);let y=rn(m.projectDir);if(Xr(y)&&Qr(y).length>0)if(e.yes)v.log.info(`Directory ${y} is not empty. Merging (keeping existing files, overwriting conflicts).`);else{let _=await v.select({message:`Directory ${y} 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"}]});(v.isCancel(_)||_==="cancel")&&(v.cancel("Setup cancelled."),process.exit(0)),_==="overwrite"&&en(y,{recursive:!0,force:!0})}let g={...m,projectDir:y,version:a};o.start("Downloading template...");try{await fe(i,a,y);let h=await je(y);await d(tn(y,Ce),JSON.stringify(h,null," ")+`
|
|
555
|
+
`),await Qe(y),o.stop("Template downloaded.")}catch(h){o.stop("Download failed."),v.cancel(E(h)),process.exit(1)}let R;o.start("Generating project files...");try{await lt(g),await It(g),await ft(g),R=await dt(g),await At(g),await mt(g),await pt(g),await ct(g),await Et(g),await Tt(y,g.aiTools),await St(g,l),o.stop("Project files generated.")}catch(h){o.stop("Generation failed."),v.cancel(E(h)),process.exit(1)}let N=await kt(y);await Dt(y);let G=Ue("docker"),H=Ke(g).map(h=>h.key).filter(h=>!g.credentials?.[h]);Ct(g,{pnpmInstalled:N,dockerComposeGenerated:R,dockerAvailable:G,skippedCredentials:H}),Je(),v.log.info(nn.dim(`Done in ${((performance.now()-t)/1e3).toFixed(1)}s`))}import{existsSync as an}from"fs";import{readFile as sn,rm as cn}from"fs/promises";import{join as ge,resolve as ln}from"path";import{mkdtemp as pn}from"fs/promises";import{tmpdir as dn}from"os";import*as A from"@clack/prompts";import se from"picocolors";function jt(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=ln(t.cwd??process.cwd()),o=ge(r,ie),n;try{n=JSON.parse(await sn(o,"utf-8"))}catch{A.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let i;try{i=await oe()}catch(m){A.cancel(E(m)),process.exit(1)}let a=k(i),l=A.spinner();try{l.start("Verifying access...");let m;try{m=await D(a)}catch(I){if(I instanceof w&&I.status===401)l.stop("Invalid API key."),i=await B(),a=k(i),l.start("Verifying access..."),m=await D(a);else throw I}l.stop("Access verified."),K(i),l.start("Fetching latest skill files...");let y=await it(a,m.latest);await ze(r,y.skillMd,y.scripts,n.aiTools);let g=He(n.aiTools);if(l.stop("Skills updated."),A.log.success(`Skill files installed to ${se.cyan(g.length.toString())} locations.`),n.licenseToken)try{let I=await at(a,{currentToken:n.licenseToken,newVersion:n.version});n.licenseToken=I.token,await d(o,JSON.stringify(n,null," ")+`
|
|
556
|
+
`),A.log.success("License refreshed.")}catch{A.log.warn("License refresh skipped.")}if(n.version===m.latest){A.log.info(`Already on the latest version (${n.version}).`);return}let R=ge(r,tt);l.start(`Downloading v${m.latest} template...`),await fe(a,m.latest,R),l.stop("Template staged.");let N=await nt(a,m.latest);N&&A.note(N,`Changelog v${m.latest}`);let G=ge(r,Ce);if(!an(G)){l.start("Computing baseline template hashes (one-time migration)...");let I=await pn(ge(dn(),"gs-update-"));try{await fe(a,n.version,I);let H=await je(I);await d(G,JSON.stringify(H,null," ")+`
|
|
557
|
+
`)}finally{await cn(I,{recursive:!0,force:!0})}l.stop("Baseline hashes computed.")}if(await d(ge(r,rt),JSON.stringify({currentVersion:n.version,targetVersion:m.latest,changelog:N,stagedAt:new Date().toISOString()},null," ")+`
|
|
558
|
+
`),A.log.info(`Update staged: ${se.cyan(n.version)} \u2192 ${se.cyan(m.latest)}`),n.aiTools&&n.aiTools.length>0){let I=n.aiTools[0],H=me[I].label;A.log.info(`Open your project in ${se.cyan(H)} and ask: ${se.cyan("'update my GenerateSaaS project'")}`)}else A.log.info(`Ask your AI coding assistant to ${se.cyan("'update my GenerateSaaS project'")}.`)}catch(m){l.stop("Failed."),A.cancel(`Update failed: ${E(m)}`),process.exit(1)}})}import*as $ from"@clack/prompts";import O from"picocolors";import{readFile as mn}from"fs/promises";import{join as un,resolve as fn}from"path";function Vt(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=fn(t.cwd??process.cwd()),o=un(r,ie),n;try{n=JSON.parse(await mn(o,"utf-8"))}catch{$.cancel(".generatesaas/manifest.json not found. Run this from a GenerateSaaS project."),process.exit(1)}let i=[`Version: ${O.cyan(n.version)}`,`Frontend: ${O.cyan(n.frontend)}`,n.deploymentTarget?`Deploy target: ${O.cyan(n.deploymentTarget)}`:null,n.databaseProvider?`Database: ${O.cyan(n.databaseProvider)}`:null,n.cacheProvider?`Cache: ${O.cyan(n.cacheProvider)}`:null,n.aiTools&&n.aiTools.length>0?`AI tools: ${O.cyan(n.aiTools.join(", "))}`:null].filter(Boolean).join(`
|
|
559
|
+
`);$.note(i,O.bold("Project Status"));let a=$.spinner();a.start("Checking for updates...");try{let l=await oe(),m=k(l),y;try{y=await D(m)}catch(R){if(R instanceof w&&R.status===401)a.stop("Invalid API key."),l=await B(),m=k(l),a.start("Checking for updates..."),y=await D(m),K(l);else throw R}let g=y.latest;n.version===g?(a.stop("Up to date."),$.log.success(`Already on the latest version (${O.green(g)})`)):(a.stop("Update available."),$.log.warning(`Update available: ${O.yellow(n.version)} \u2192 ${O.green(g)}`),$.log.info(`Run ${O.cyan("generatesaas update")} to update skill files, then ask your AI assistant to apply the update.`))}catch(l){a.stop("Check failed."),$.log.warning(`Could not check for updates: ${E(l)}`)}})}import{readFile as gn}from"fs/promises";import*as P from"@clack/prompts";import V from"picocolors";function yn(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 Ye(e){return typeof e!="number"?"unknown":new Date(e*1e3).toISOString().split("T")[0]}function Ut(e){P.note([`License ID: ${V.cyan(String(e.lid??"unknown"))}`,`Version: ${V.cyan(String(e.ver??"unknown"))}`,`Init version: ${String(e.iver??"unknown")}`,`Frontend: ${String(e.fe??"unknown")}`,`Created: ${Ye(e.pat)}`,`Last updated: ${Ye(e.uat)}`,`Expires: ${Ye(e.exp)}`,`Install ID: ${String(e.nid??"unknown")}`].join(`
|
|
560
|
+
`),V.yellow("License Details"))}async function Mt(e){let t=P.spinner(),r=e.replace(/\/+$/,"");t.start(`Checking ${r}/license...`);let o;try{let i=await fetch(`${r}/license`);if(!i.ok)return t.stop(`${V.red("Not found")} \u2014 ${r}/license returned ${i.status}`),!1;if(o=await i.text(),!o||o.split(".").length!==3)return t.stop(`${V.red("Invalid")} \u2014 response is not a JWT`),!1;t.stop(`${V.green("Found")} \u2014 license endpoint responded`)}catch(i){return t.stop(`${V.red("Failed")} \u2014 ${E(i)}`),!1}let n;try{n=yn(o)}catch{return P.log.error("Could not decode JWT payload."),!1}t.start("Verifying signature...");try{let i=process.env.GENERATESAAS_API_URL??De,a=await st(i,o);if(a.valid)t.stop(`${V.green("Valid")} \u2014 signature verified`);else return t.stop(`${V.red("Invalid")} \u2014 ${a.reason}`),!1}catch{return t.stop(`${V.yellow("Skipped")} \u2014 could not reach verification service`),P.log.warn("Signature not verified. Displaying unverified claims:"),Ut(n),!1}return Ut(n),!0}function Ft(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/api)").option("--file <path>","file with URLs to check, one per line").action(async(t,r)=>{if(!t&&!r.file&&(P.cancel("Provide a URL or --file <path>."),process.exit(1)),r.file){let n=(await gn(r.file,"utf-8")).split(`
|
|
561
|
+
`).map(a=>a.trim()).filter(a=>a&&!a.startsWith("#"));n.length===0&&(P.cancel("No URLs found in file."),process.exit(1));let i=0;for(let a of n)await Mt(a)&&i++,P.log.info("");P.log.success(`${i}/${n.length} sites verified.`)}else await Mt(t)||process.exit(1)})}import{existsSync as hn,rmSync as vn}from"fs";import*as L from"@clack/prompts";function Kt(e){e.command("auth").description("Set or update your GenerateSaaS API key").option("--clear","remove saved API key").action(async t=>{if(t.clear){hn(F)?(vn(F),L.log.success("API key removed.")):L.log.info("No API key configured.");return}let r=Be();r?L.log.info(`Current API key: ${r.slice(0,8)}...${r.slice(-4)}`):L.log.info("No API key configured.");let o=await B(),n=k(o),i=L.spinner();i.start("Verifying API key...");try{await D(n),i.stop("API key verified."),K(o),L.log.success("API key saved.")}catch(a){i.stop("Verification failed."),a instanceof w&&a.status===401?L.cancel("Invalid API key."):L.cancel(E(a)),process.exit(1)}})}var ce=new Sn().name("generatesaas").description("CLI for scaffolding and managing GenerateSaaS projects").version("0.6.3").addHelpText("after",`
|
|
561
562
|
Examples:
|
|
562
563
|
$ generatesaas init Interactive setup
|
|
563
564
|
$ generatesaas init -n my-app -y Quick setup with defaults
|
|
@@ -19,7 +19,7 @@ Update your project to the latest GenerateSaaS boilerplate version while preserv
|
|
|
19
19
|
### Step 1: Prepare Update Data
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
node
|
|
22
|
+
node __SKILL_ROOT__/generatesaas-update/scripts/prepare-update.js
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
Reads the staged update from `.generatesaas/staging/` and computes local diffs. Creates:
|
|
@@ -38,7 +38,7 @@ Read `references/changelog.md` to understand:
|
|
|
38
38
|
### Step 3: Classify Files
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
node
|
|
41
|
+
node __SKILL_ROOT__/generatesaas-update/scripts/classify-files.js
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Compares local file hashes (from `.generatesaas/hashes.json`) against current files to determine which the user has customized. Outputs `references/classification.json` with categories:
|
|
@@ -108,7 +108,7 @@ If the user wants to exclude items, move them to the **Skipped** section with th
|
|
|
108
108
|
### Step 5: Apply Auto-Updates
|
|
109
109
|
|
|
110
110
|
```bash
|
|
111
|
-
node
|
|
111
|
+
node __SKILL_ROOT__/generatesaas-update/scripts/apply-auto.js
|
|
112
112
|
```
|
|
113
113
|
|
|
114
114
|
Automatically updates `unmodified` files and creates `new` files. These are safe because:
|
|
@@ -121,7 +121,7 @@ After this completes, mark all auto-update and new file items as `[x]` in the pl
|
|
|
121
121
|
|
|
122
122
|
For each file in the `modified` category, work through the plan checklist one by one:
|
|
123
123
|
|
|
124
|
-
1. Read the corresponding diff at `references/diffs/<path>.diff`
|
|
124
|
+
1. Read the corresponding diff at `references/diffs/<path>.diff` (if no diff exists, the file was new upstream but already exists locally — read the staging copy at `.generatesaas/staging/<path>` instead)
|
|
125
125
|
2. Read the current local file
|
|
126
126
|
3. Understand what the user changed and why
|
|
127
127
|
4. Apply upstream changes that are compatible with the user's modifications
|
|
@@ -144,7 +144,7 @@ After all items are processed:
|
|
|
144
144
|
3. Run the completion script:
|
|
145
145
|
|
|
146
146
|
```bash
|
|
147
|
-
node
|
|
147
|
+
node __SKILL_ROOT__/generatesaas-update/scripts/complete-update.js
|
|
148
148
|
```
|
|
149
149
|
|
|
150
150
|
This regenerates `.generatesaas/hashes.json` with fresh file hashes and updates the version in `.generatesaas/manifest.json`. Cleans up the `references/` directory.
|
|
@@ -38,7 +38,16 @@ function hashFile(filePath) {
|
|
|
38
38
|
|
|
39
39
|
// ── File Walking ──
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
// Broader than strictly needed, but aligned with the CLI's exclusions.ts to ensure
|
|
42
|
+
// hash consistency across update cycles. The staging tarball is pre-filtered by
|
|
43
|
+
// exclusions.ts, so most of these only matter when walking the live project root.
|
|
44
|
+
const WALK_EXCLUSIONS = new Set([
|
|
45
|
+
".git", "node_modules", ".pnpm-store", ".env", ".env.test",
|
|
46
|
+
".turbo", ".nuxt", ".output", ".data", "dist",
|
|
47
|
+
".next", ".svelte-kit", ".netlify", ".wrangler",
|
|
48
|
+
".devcontainer", "playwright-report", "test-results",
|
|
49
|
+
INTERNAL_DIR,
|
|
50
|
+
]);
|
|
42
51
|
|
|
43
52
|
/** Check if a relative path should be excluded from walking. */
|
|
44
53
|
function shouldExcludeWalk(relativePath) {
|
|
@@ -95,7 +95,7 @@ function main() {
|
|
|
95
95
|
ensureDir(diffsDir);
|
|
96
96
|
let diffCount = 0;
|
|
97
97
|
|
|
98
|
-
for (const filePath of modified) {
|
|
98
|
+
for (const filePath of [...modified, ...added]) {
|
|
99
99
|
const userFile = path.join(root, filePath);
|
|
100
100
|
const stagingFile = path.join(stagingDir, filePath);
|
|
101
101
|
|