@superbuilds/create-supersaas-app 0.0.0-stage → 2.0.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/README.md CHANGED
@@ -1,3 +1,114 @@
1
- # Temporary Holding Version
1
+ # create-supersaas-app
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > Scaffold production-ready SaaS apps in seconds
4
+
5
+ The fastest way to get your indie SaaS off the ground. One command, full source code, ready to ship.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ npx create-supersaas-app my-saas
11
+ cd my-saas
12
+ pnpm dev
13
+ # Open http://localhost:3000/setup to configure
14
+ ```
15
+
16
+ ## What You Get
17
+
18
+ - **Next.js 15** App Router + React 19 + TypeScript (strict mode)
19
+ - **Authentication** — Better Auth: email/password, magic links, 2FA, Google/GitHub/Twitter OAuth
20
+ - **4 Payment Providers** — Stripe, Polar, LemonSqueezy, DodoPayments (switch with one env var)
21
+ - **5 Email Providers** — Resend, Postmark, SendGrid, Mailgun, Nodemailer/SMTP
22
+ - **Database** — Drizzle ORM on PostgreSQL (Neon, Supabase, or local Docker)
23
+ - **Teams** — multi-tenant orgs with invites, owner/admin/member roles
24
+ - **Admin Dashboard** — user management, analytics, subscriptions, leads, feature flags
25
+ - **13 Themes** — complete UI vibes, switch with one line in globals.css
26
+ - **Blog + Changelog** — MDX with syntax highlighting, RSS, reading time
27
+ - **Rate Limiting** — Upstash Redis middleware
28
+ - **i18n** — locale detection, server + client translations
29
+ - **Setup Wizard** — browser-based `/setup` generates your .env in 2 minutes
30
+ - **AI-Ready** — ships with CLAUDE.md + AGENTS.md for Claude Code, Cursor, Copilot, Windsurf
31
+
32
+ ## Usage
33
+
34
+ ### Interactive Mode
35
+
36
+ ```bash
37
+ npx create-supersaas-app
38
+ ```
39
+
40
+ You'll be prompted for:
41
+ 1. License key (from your purchase email)
42
+ 2. App name
43
+ 3. Database provider (Neon, Supabase, or local Docker Postgres)
44
+ 4. Optional features (blog, admin dashboard)
45
+ 5. Install dependencies (yes/no)
46
+
47
+ ### Non-Interactive Mode
48
+
49
+ For CI/CD, automation, or AI agents:
50
+
51
+ ```bash
52
+ npx create-supersaas-app my-saas \
53
+ --key YOUR_LICENSE_KEY \
54
+ --db neon \
55
+ --features blog,admin \
56
+ --install \
57
+ --yes
58
+ ```
59
+
60
+ ### Dev Mode
61
+
62
+ For local development without a license:
63
+
64
+ ```bash
65
+ npx create-supersaas-app my-saas --key dev
66
+ ```
67
+
68
+ ## CLI Options
69
+
70
+ | Flag | Description | Values |
71
+ |------|-------------|--------|
72
+ | `--key <key>` | License key | Your key from purchase email |
73
+ | `--db <provider>` | Database provider | `neon`, `supabase`, `local` |
74
+ | `--features <list>` | Optional features | `blog`, `admin` (comma-separated) |
75
+ | `--install` | Install dependencies after scaffolding | — |
76
+ | `--no-git` | Skip git repository initialization | — |
77
+ | `--yes` / `--defaults` | Skip prompts, use defaults | — |
78
+
79
+ ## Deactivate License
80
+
81
+ Free up a machine activation slot:
82
+
83
+ ```bash
84
+ npx create-supersaas-app deactivate
85
+ ```
86
+
87
+ ## After Scaffolding
88
+
89
+ ```bash
90
+ cd my-saas
91
+ pnpm dev # deps already installed if you said yes during setup
92
+ # Open http://localhost:3000/setup — the setup wizard walks you through everything
93
+ ```
94
+
95
+ The setup wizard configures:
96
+ - Database connection
97
+ - Auth secrets
98
+ - Payment provider
99
+ - Email provider
100
+ - App identity
101
+
102
+ After setup, manage runtime config at `/admin/settings`.
103
+
104
+ ## License
105
+
106
+ Requires a valid SuperSaaS Kit license. One purchase ($129), unlimited projects, 3 machine activations.
107
+
108
+ Get yours at [supersaas-kit.superbuilds.fun](https://supersaas-kit.superbuilds.fun)
109
+
110
+ ## Support
111
+
112
+ - **Docs:** [supersaas-kit.superbuilds.fun/docs](https://supersaas-kit.superbuilds.fun/docs)
113
+ - **Discord:** [discord.gg/bM3w3RZYYH](https://discord.gg/bM3w3RZYYH)
114
+ - **Email:** hi@superbuilds.fun
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,562 @@
1
+ #!/usr/bin/env node
2
+ import { program } from 'commander';
3
+ import * as p3 from '@clack/prompts';
4
+ import chalk from 'chalk';
5
+ import path from 'path';
6
+ import fs from 'fs-extra';
7
+ import ora from 'ora';
8
+ import { execSync } from 'child_process';
9
+ import os2 from 'os';
10
+ import { createRequire } from 'module';
11
+
12
+ var API_BASE = process.env.SUPERSAAS_API_URL || "https://supersaas-kit.superbuilds.fun";
13
+ var SUBSTITUTIONS = {
14
+ __APP_NAME__: (c) => c.name
15
+ };
16
+ var FEATURE_PATHS = {
17
+ blog: [
18
+ "src/app/(marketing)/blog",
19
+ "src/app/blog",
20
+ "src/lib/blog.ts",
21
+ "content/blog",
22
+ "src/components/blog"
23
+ ],
24
+ admin: [
25
+ "src/app/(app)/admin",
26
+ "src/lib/admin"
27
+ ]
28
+ };
29
+ var DATABASE_CONFIGS = {
30
+ neon: { hint: "# Get your connection string from https://console.neon.tech" },
31
+ supabase: { hint: "# Get your connection string from Supabase Dashboard > Settings > Database" },
32
+ local: { hint: "# Local Docker Postgres: docker compose up -d" }
33
+ };
34
+ async function downloadTemplate(licenseKey, targetDir) {
35
+ const spinner = ora("Downloading template...").start();
36
+ try {
37
+ const res = await fetch(`${API_BASE}/api/download`, {
38
+ method: "POST",
39
+ headers: { "Content-Type": "application/json" },
40
+ body: JSON.stringify({
41
+ license_key: licenseKey
42
+ })
43
+ });
44
+ if (!res.ok) {
45
+ spinner.fail(chalk.red("Download failed"));
46
+ const errorBody = await res.json().catch(() => ({}));
47
+ const msg = errorBody.error || `HTTP ${res.status}`;
48
+ p3.log.error(chalk.red(msg));
49
+ if (res.status === 403) {
50
+ p3.log.info(chalk.dim("Your license may be invalid. Check https://supersaas-kit.superbuilds.fun"));
51
+ }
52
+ if (res.status === 429) {
53
+ p3.log.info(chalk.dim("Too many downloads. Wait an hour and try again."));
54
+ }
55
+ return false;
56
+ }
57
+ const tmpDir = path.join(os2.tmpdir(), `supersaas-kit-${Date.now()}`);
58
+ await fs.ensureDir(tmpDir);
59
+ const archivePath = path.join(tmpDir, "template.tar.gz");
60
+ const arrayBuffer = await res.arrayBuffer();
61
+ await fs.writeFile(archivePath, Buffer.from(arrayBuffer));
62
+ spinner.text = "Extracting template...";
63
+ await fs.ensureDir(targetDir);
64
+ try {
65
+ execSync(`tar -xzf "${archivePath}" -C "${targetDir}"`, {
66
+ stdio: "pipe"
67
+ });
68
+ } catch (tarErr) {
69
+ spinner.fail(chalk.red("Extraction failed"));
70
+ const isWindows = process.platform === "win32";
71
+ if (isWindows) {
72
+ p3.log.error(chalk.red("tar is not available on this system."));
73
+ p3.log.info(chalk.dim("Install tar via Windows Build Tools, or use WSL."));
74
+ } else {
75
+ p3.log.error(chalk.red(`tar extraction failed: ${tarErr.message}`));
76
+ }
77
+ await fs.remove(tmpDir);
78
+ return false;
79
+ }
80
+ await fs.remove(tmpDir);
81
+ spinner.succeed(chalk.green("Template downloaded and extracted"));
82
+ return true;
83
+ } catch (err) {
84
+ spinner.fail(chalk.red("Download failed"));
85
+ p3.log.error(
86
+ chalk.red(
87
+ `Could not download template: ${err.message}`
88
+ )
89
+ );
90
+ p3.log.info(chalk.dim("Check your internet connection and try again."));
91
+ return false;
92
+ }
93
+ }
94
+ async function createApp(config) {
95
+ const targetDir = path.resolve(process.cwd(), config.name);
96
+ if (await fs.pathExists(targetDir)) {
97
+ p3.log.error(chalk.red(`Directory "${config.name}" already exists.`));
98
+ process.exit(1);
99
+ }
100
+ const downloaded = await downloadTemplate(config.licenseKey, targetDir);
101
+ if (!downloaded) {
102
+ if (await fs.pathExists(targetDir)) {
103
+ await fs.remove(targetDir);
104
+ }
105
+ process.exit(1);
106
+ }
107
+ const excludePaths = /* @__PURE__ */ new Set();
108
+ for (const [feature, paths] of Object.entries(FEATURE_PATHS)) {
109
+ if (!config.features.includes(feature)) {
110
+ for (const p4 of paths) {
111
+ excludePaths.add(p4);
112
+ }
113
+ }
114
+ }
115
+ for (const excludePath of excludePaths) {
116
+ const fullPath = path.join(targetDir, excludePath);
117
+ if (await fs.pathExists(fullPath)) {
118
+ await fs.remove(fullPath);
119
+ }
120
+ }
121
+ const pkgTemplatePath = path.join(targetDir, "package.json.template");
122
+ if (await fs.pathExists(pkgTemplatePath)) {
123
+ await fs.move(pkgTemplatePath, path.join(targetDir, "package.json"));
124
+ }
125
+ await substituteVars(targetDir, config);
126
+ await configureDatabase(targetDir, config);
127
+ await updateManifest(targetDir, config);
128
+ const envExample = path.join(targetDir, ".env.example");
129
+ if (await fs.pathExists(envExample)) {
130
+ await fs.copy(envExample, path.join(targetDir, ".env"));
131
+ p3.log.info(chalk.dim("Created .env from template \u2014 configure via /setup after first run"));
132
+ }
133
+ await cleanupFeatureImports(targetDir, config);
134
+ const activeFeatures = ["auth", "payments", "email", "seo", ...config.features];
135
+ p3.log.info(chalk.dim(`Database: ${config.database}`));
136
+ p3.log.info(chalk.dim(`Features: ${activeFeatures.join(", ")}`));
137
+ if (config.install) {
138
+ const pm = detectPackageManager();
139
+ const spinner = ora(`Installing dependencies with ${pm}...`).start();
140
+ try {
141
+ execSync(`${pm} install`, { cwd: targetDir, stdio: "pipe" });
142
+ spinner.succeed(chalk.green("Dependencies installed"));
143
+ } catch {
144
+ spinner.fail(chalk.yellow("Install failed \u2014 run it manually after setup"));
145
+ }
146
+ }
147
+ if (!config.skipGit && hasGit()) {
148
+ try {
149
+ execSync("git init", { cwd: targetDir, stdio: "pipe" });
150
+ execSync("git add -A", { cwd: targetDir, stdio: "pipe" });
151
+ execSync('git commit -m "Initial commit from create-supersaas-app"', {
152
+ cwd: targetDir,
153
+ stdio: "pipe"
154
+ });
155
+ p3.log.info(chalk.dim("Initialized git repository"));
156
+ } catch {
157
+ }
158
+ }
159
+ }
160
+ function detectPackageManager() {
161
+ const userAgent = process.env.npm_config_user_agent || "";
162
+ if (userAgent.startsWith("pnpm")) return "pnpm";
163
+ if (userAgent.startsWith("yarn")) return "yarn";
164
+ if (userAgent.startsWith("bun")) return "bun";
165
+ return "pnpm";
166
+ }
167
+ function hasGit() {
168
+ try {
169
+ execSync("git --version", { stdio: "pipe" });
170
+ return true;
171
+ } catch {
172
+ return false;
173
+ }
174
+ }
175
+ async function configureDatabase(targetDir, config) {
176
+ const dbConfig = DATABASE_CONFIGS[config.database];
177
+ if (!dbConfig) return;
178
+ const envPath = path.join(targetDir, ".env.example");
179
+ if (await fs.pathExists(envPath)) {
180
+ let content = await fs.readFile(envPath, "utf-8");
181
+ const hint = dbConfig.hint;
182
+ if (hint && !content.includes(hint)) {
183
+ content = content.replace(/DATABASE_URL=/, `${hint}
184
+ DATABASE_URL=`);
185
+ await fs.writeFile(envPath, content);
186
+ }
187
+ }
188
+ }
189
+ async function updateManifest(targetDir, config) {
190
+ const manifestPath = path.join(targetDir, ".supersaas", "manifest.json");
191
+ if (await fs.pathExists(manifestPath)) {
192
+ const manifest = await fs.readJson(manifestPath);
193
+ manifest.database = config.database;
194
+ manifest.features = config.features;
195
+ manifest.createdAt = (/* @__PURE__ */ new Date()).toISOString();
196
+ if (manifest.providers) {
197
+ if (manifest.providers.db) manifest.providers.db = "postgres";
198
+ }
199
+ await fs.writeJson(manifestPath, manifest, { spaces: 2 });
200
+ }
201
+ }
202
+ async function cleanupFeatureImports(targetDir, config) {
203
+ if (!config.features.includes("blog")) {
204
+ const sitemapPath = path.join(targetDir, "src/app/sitemap.ts");
205
+ if (await fs.pathExists(sitemapPath)) {
206
+ await fs.writeFile(sitemapPath, `import { MetadataRoute } from "next";
207
+ import { appConfig } from "@/config/app";
208
+
209
+ const baseUrl = appConfig.url;
210
+
211
+ export default function sitemap(): MetadataRoute.Sitemap {
212
+ const now = new Date();
213
+
214
+ return [
215
+ { url: baseUrl, lastModified: now, changeFrequency: "weekly", priority: 1.0 },
216
+ { url: \`\${baseUrl}/sign-up\`, lastModified: now, changeFrequency: "yearly", priority: 0.5 },
217
+ ];
218
+ }
219
+ `);
220
+ }
221
+ const configPath = path.join(targetDir, "src/config/app.ts");
222
+ if (await fs.pathExists(configPath)) {
223
+ let content = await fs.readFile(configPath, "utf-8");
224
+ content = content.replace(/\s*\{\s*label:\s*["']Blog["'],\s*href:\s*["']\/blog["']\s*\},?\n?/g, "");
225
+ await fs.writeFile(configPath, content);
226
+ }
227
+ }
228
+ if (!config.features.includes("admin")) {
229
+ const sidebarPath = path.join(targetDir, "src/components/layout/sidebar.tsx");
230
+ if (await fs.pathExists(sidebarPath)) {
231
+ let content = await fs.readFile(sidebarPath, "utf-8");
232
+ content = content.replace(/\s*\{\s*name:\s*["']Admin["'].*?\},?\n?/gs, "");
233
+ await fs.writeFile(sidebarPath, content);
234
+ }
235
+ }
236
+ }
237
+ async function substituteVars(dir, config) {
238
+ const textExtensions = [
239
+ ".ts",
240
+ ".tsx",
241
+ ".js",
242
+ ".jsx",
243
+ ".json",
244
+ ".md",
245
+ ".mdx",
246
+ ".css",
247
+ ".env",
248
+ ".example"
249
+ ];
250
+ const files = await getAllFiles(dir);
251
+ for (const file of files) {
252
+ const ext = path.extname(file);
253
+ const basename = path.basename(file);
254
+ if (!textExtensions.includes(ext) && !basename.startsWith(".env")) continue;
255
+ let content = await fs.readFile(file, "utf-8");
256
+ let changed = false;
257
+ for (const [placeholder, resolver] of Object.entries(SUBSTITUTIONS)) {
258
+ if (content.includes(placeholder)) {
259
+ content = content.replaceAll(placeholder, resolver(config));
260
+ changed = true;
261
+ }
262
+ }
263
+ if (changed) {
264
+ await fs.writeFile(file, content);
265
+ }
266
+ }
267
+ }
268
+ async function getAllFiles(dir) {
269
+ const entries = await fs.readdir(dir, { withFileTypes: true });
270
+ const files = [];
271
+ for (const entry of entries) {
272
+ const full = path.join(dir, entry.name);
273
+ if (entry.isDirectory()) {
274
+ if (entry.name === "node_modules" || entry.name === ".next") continue;
275
+ files.push(...await getAllFiles(full));
276
+ } else {
277
+ files.push(full);
278
+ }
279
+ }
280
+ return files;
281
+ }
282
+ var SUPERSAAS_DIR = path.join(os2.homedir(), ".supersaas");
283
+ var LICENSE_PATH = path.join(SUPERSAAS_DIR, "license.json");
284
+ var DODO_BASE_URL = process.env.SUPERSAAS_DODO_URL || "https://test.dodopayments.com";
285
+ async function loadLocalLicense() {
286
+ try {
287
+ if (await fs.pathExists(LICENSE_PATH)) {
288
+ const data = await fs.readJson(LICENSE_PATH);
289
+ if (data.key && data.instance_id) {
290
+ return data;
291
+ }
292
+ }
293
+ } catch {
294
+ }
295
+ return null;
296
+ }
297
+ async function validateKey(key) {
298
+ try {
299
+ const res = await fetch(`${DODO_BASE_URL}/licenses/validate`, {
300
+ method: "POST",
301
+ headers: { "Content-Type": "application/json" },
302
+ body: JSON.stringify({ license_key: key })
303
+ });
304
+ if (!res.ok) {
305
+ return { valid: false };
306
+ }
307
+ const data = await res.json();
308
+ return { valid: data.valid !== false, data };
309
+ } catch {
310
+ return { valid: false };
311
+ }
312
+ }
313
+ async function activateOnline(key) {
314
+ const machineName = `${os2.hostname()} (${os2.platform()})`;
315
+ try {
316
+ const validation = await validateKey(key);
317
+ if (!validation.valid) {
318
+ p3.log.error(chalk.red("Invalid or expired license key."));
319
+ return null;
320
+ }
321
+ const res = await fetch(`${DODO_BASE_URL}/licenses/activate`, {
322
+ method: "POST",
323
+ headers: { "Content-Type": "application/json" },
324
+ body: JSON.stringify({
325
+ license_key: key,
326
+ name: machineName
327
+ })
328
+ });
329
+ if (!res.ok) {
330
+ const body = await res.json().catch(() => ({}));
331
+ const msg = body.message || body.error || `HTTP ${res.status}`;
332
+ p3.log.error(chalk.red(`Activation failed: ${msg}`));
333
+ return null;
334
+ }
335
+ const result = await res.json();
336
+ const license = {
337
+ key,
338
+ instance_id: result.id || result.license_key_instance_id || "",
339
+ activated_at: (/* @__PURE__ */ new Date()).toISOString(),
340
+ valid: true
341
+ };
342
+ await fs.ensureDir(SUPERSAAS_DIR);
343
+ await fs.writeJson(LICENSE_PATH, license, { spaces: 2 });
344
+ return license;
345
+ } catch {
346
+ p3.log.error(
347
+ chalk.red("Could not reach license server. Check your connection.")
348
+ );
349
+ return null;
350
+ }
351
+ }
352
+ async function activate(keyArg, nonInteractive) {
353
+ const existing = await loadLocalLicense();
354
+ if (existing) {
355
+ const check = await validateKey(existing.key).catch(() => ({ valid: true }));
356
+ if (check.valid) {
357
+ p3.log.success(
358
+ chalk.dim(`Licensed \u2014 ${existing.key.slice(0, 12)}...`)
359
+ );
360
+ return existing;
361
+ }
362
+ await fs.remove(LICENSE_PATH);
363
+ if (!nonInteractive) {
364
+ p3.log.warning(chalk.yellow("License expired or revoked. Please re-activate."));
365
+ }
366
+ }
367
+ if (!keyArg && nonInteractive) {
368
+ return null;
369
+ }
370
+ const key = keyArg || await p3.text({
371
+ message: "Enter your license key:",
372
+ placeholder: "Paste your license key from the purchase email",
373
+ validate: (v) => {
374
+ if (!v) return "License key is required";
375
+ if (v.length < 8) return "Invalid key format";
376
+ }
377
+ });
378
+ if (p3.isCancel(key)) return null;
379
+ p3.log.step("Activating license...");
380
+ const license = await activateOnline(key);
381
+ if (license) {
382
+ p3.log.success(chalk.green("License activated!"));
383
+ return license;
384
+ }
385
+ return null;
386
+ }
387
+ async function deactivate() {
388
+ const existing = await loadLocalLicense();
389
+ if (!existing) {
390
+ p3.log.warning(chalk.yellow("No active license found on this machine."));
391
+ return false;
392
+ }
393
+ try {
394
+ const res = await fetch(`${DODO_BASE_URL}/licenses/deactivate`, {
395
+ method: "POST",
396
+ headers: { "Content-Type": "application/json" },
397
+ body: JSON.stringify({
398
+ license_key: existing.key,
399
+ license_key_instance_id: existing.instance_id
400
+ })
401
+ });
402
+ if (res.ok) {
403
+ await fs.remove(LICENSE_PATH);
404
+ p3.log.success(chalk.green("License deactivated. Machine slot freed."));
405
+ return true;
406
+ }
407
+ const body = await res.json().catch(() => ({}));
408
+ p3.log.error(chalk.red(`Deactivation failed: ${body.message || "Unknown error"}`));
409
+ return false;
410
+ } catch {
411
+ p3.log.error(chalk.red("Could not reach license server."));
412
+ return false;
413
+ }
414
+ }
415
+ var require2 = createRequire(import.meta.url);
416
+ var { version } = require2("../package.json");
417
+ var VALID_DBS = ["neon", "supabase", "local"];
418
+ var VALID_FEATURES = ["blog", "admin"];
419
+ program.name("create-supersaas-app").description("Scaffold production-ready SaaS apps in seconds").version(version).argument("[name]", "Name of your app").option("--key <license-key>", "License key for activation").option("--defaults", "Skip prompts, use default settings (for agents)").option("--yes", "Alias for --defaults (non-interactive mode)").option("--db <provider>", "Database provider (neon|supabase|local)").option("--features <list>", "Comma-separated features (blog,admin)").option("--install", "Install dependencies after scaffolding").option("--no-git", "Skip git repository initialization").action(async (name, options) => {
420
+ const nonInteractive = options.defaults || options.yes || !process.stdin.isTTY;
421
+ if (!nonInteractive) {
422
+ p3.intro(
423
+ `${chalk.bold("supersaas-kit")} ${chalk.dim(`v${version}`)}
424
+ ` + chalk.dim(" Production-ready SaaS in one command")
425
+ );
426
+ }
427
+ let licenseResult = await activate(options.key, nonInteractive);
428
+ if (!licenseResult) {
429
+ if (!nonInteractive) p3.cancel("License activation failed.");
430
+ else console.error("License activation failed.");
431
+ process.exit(1);
432
+ }
433
+ let appName = name;
434
+ if (!appName && nonInteractive) {
435
+ console.error("Error: app name is required in non-interactive mode. Pass it as the first argument: create-supersaas-app <name>");
436
+ process.exit(1);
437
+ }
438
+ if (!appName) {
439
+ appName = await p3.text({
440
+ message: "What's your app called?",
441
+ placeholder: "my-saas",
442
+ validate: (v) => {
443
+ if (!v) return "App name is required";
444
+ if (!/^[a-z0-9-]+$/.test(v))
445
+ return "Use lowercase letters, numbers, and hyphens only";
446
+ }
447
+ });
448
+ if (p3.isCancel(appName)) {
449
+ p3.cancel("Cancelled.");
450
+ process.exit(0);
451
+ }
452
+ }
453
+ let dbChoice = options.db || (nonInteractive ? "neon" : null);
454
+ if (dbChoice && !VALID_DBS.includes(dbChoice)) {
455
+ const msg = `Invalid --db value "${dbChoice}". Valid options: ${VALID_DBS.join(", ")}`;
456
+ if (nonInteractive) {
457
+ console.error(`Error: ${msg}`);
458
+ process.exit(1);
459
+ }
460
+ p3.log.warn(chalk.yellow(msg));
461
+ dbChoice = null;
462
+ }
463
+ if (!dbChoice) {
464
+ dbChoice = await p3.select({
465
+ message: "Database provider?",
466
+ options: [
467
+ { value: "neon", label: "Neon (Postgres)", hint: "serverless, recommended" },
468
+ { value: "supabase", label: "Supabase (Postgres)" },
469
+ { value: "local", label: "Local Docker Postgres", hint: "docker compose up -d" }
470
+ ]
471
+ });
472
+ if (p3.isCancel(dbChoice)) {
473
+ p3.cancel("Cancelled.");
474
+ process.exit(0);
475
+ }
476
+ }
477
+ let features = options.features ? options.features.split(",").map((f) => f.trim()).filter(Boolean) : [];
478
+ if (features.length > 0) {
479
+ const invalid = features.filter((f) => !VALID_FEATURES.includes(f));
480
+ if (invalid.length > 0) {
481
+ const msg = `Unknown features: ${invalid.join(", ")}. Valid options: ${VALID_FEATURES.join(", ")}`;
482
+ if (nonInteractive) {
483
+ console.error(`Error: ${msg}`);
484
+ process.exit(1);
485
+ }
486
+ p3.log.warn(chalk.yellow(msg));
487
+ features = features.filter((f) => VALID_FEATURES.includes(f));
488
+ }
489
+ }
490
+ if (!nonInteractive && !options.features) {
491
+ features = await p3.multiselect({
492
+ message: "Include optional modules?",
493
+ options: [
494
+ { value: "blog", label: "Blog (MDX)", hint: "SEO-ready with RSS feed" },
495
+ { value: "admin", label: "Admin Dashboard", hint: "user management, analytics" }
496
+ ],
497
+ required: false
498
+ });
499
+ if (p3.isCancel(features)) {
500
+ p3.cancel("Cancelled.");
501
+ process.exit(0);
502
+ }
503
+ }
504
+ let shouldInstall = options.install || false;
505
+ if (!nonInteractive && !options.install) {
506
+ shouldInstall = await p3.confirm({
507
+ message: "Install dependencies?",
508
+ initialValue: true
509
+ });
510
+ if (p3.isCancel(shouldInstall)) {
511
+ p3.cancel("Cancelled.");
512
+ process.exit(0);
513
+ }
514
+ }
515
+ await createApp({
516
+ name: appName,
517
+ database: dbChoice,
518
+ features: features || [],
519
+ licenseKey: licenseResult.key,
520
+ install: shouldInstall,
521
+ skipGit: options.git === false
522
+ });
523
+ if (!nonInteractive) {
524
+ const steps = [
525
+ chalk.green("Your app is ready!"),
526
+ "",
527
+ ` ${chalk.cyan("cd")} ${appName}`
528
+ ];
529
+ if (!shouldInstall) {
530
+ steps.push(` ${chalk.cyan("pnpm install")}`);
531
+ }
532
+ steps.push(
533
+ ` ${chalk.cyan("pnpm dev")}`,
534
+ "",
535
+ chalk.dim(" Then visit /setup to configure your services."),
536
+ chalk.dim(" Docs: https://supersaas-kit.superbuilds.fun/docs")
537
+ );
538
+ p3.outro(steps.join("\n"));
539
+ } else {
540
+ const installCmd = shouldInstall ? "" : "pnpm install && ";
541
+ console.log(`\u2714 Created ${appName}. Run: cd ${appName} && ${installCmd}pnpm dev`);
542
+ }
543
+ });
544
+ program.command("deactivate").description("Deactivate your license on this machine (frees a machine slot)").action(async () => {
545
+ p3.intro(chalk.bold("supersaas-kit \u2014 deactivate"));
546
+ const confirmed = await p3.confirm({
547
+ message: "This will free one of your 3 machine activation slots. Continue?",
548
+ initialValue: false
549
+ });
550
+ if (p3.isCancel(confirmed) || !confirmed) {
551
+ p3.outro(chalk.dim("Deactivation cancelled."));
552
+ return;
553
+ }
554
+ const success = await deactivate();
555
+ if (success) {
556
+ p3.outro(chalk.green("License deactivated. You can activate on another machine now."));
557
+ } else {
558
+ p3.outro(chalk.red("Deactivation failed. Run again or contact hi@superbuilds.fun"));
559
+ process.exit(1);
560
+ }
561
+ });
562
+ program.parse();
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "@superbuilds/create-supersaas-app",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "2.0.1",
4
+ "description": "Scaffold production-ready SaaS apps in seconds — SuperSaaS Kit CLI",
5
+ "author": "superbuilds",
6
+ "license": "MIT",
7
+ "homepage": "https://supersaas-kit.superbuilds.fun",
8
+ "bugs": {
9
+ "url": "https://github.com/bixblues/supersaas-kit/issues"
10
+ },
11
+ "bin": {
12
+ "create-supersaas-app": "./dist/index.js"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format esm --dts --treeshake --define.__DEV_MODE__=false",
20
+ "build:dev": "tsup src/index.ts --format esm --dts --define.__DEV_MODE__=true",
21
+ "dev": "tsup src/index.ts --format esm --watch --define.__DEV_MODE__=true"
22
+ },
23
+ "dependencies": {
24
+ "@clack/prompts": "^0.9.1",
25
+ "chalk": "^5.4.1",
26
+ "commander": "^13.1.0",
27
+ "fs-extra": "^11.3.0",
28
+ "ora": "^8.2.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/fs-extra": "^11.0.4",
32
+ "@types/node": "^22.15.0",
33
+ "tsup": "^8.4.0",
34
+ "typescript": "^5.8.0"
35
+ },
36
+ "type": "module",
37
+ "engines": {
38
+ "node": ">=18.17.0"
39
+ },
40
+ "keywords": [
41
+ "supersaas-kit",
42
+ "superbuilds",
43
+ "saas",
44
+ "saas-boilerplate",
45
+ "nextjs",
46
+ "nextjs-starter",
47
+ "indie-hacker",
48
+ "create-app",
49
+ "starter",
50
+ "boilerplate"
51
+ ],
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "https://github.com/bixblues/supersaas-kit",
58
+ "directory": "packages/create-supersaas-app"
59
+ }
60
+ }