@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 +113 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +562 -0
- package/package.json +58 -4
package/README.md
CHANGED
|
@@ -1,3 +1,114 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-supersaas-app
|
|
2
2
|
|
|
3
|
-
|
|
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
|
package/dist/index.d.ts
ADDED
|
@@ -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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|