@superbuilds/create-supersaas-app 0.0.0-stage → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +446 -0
- package/package.json +58 -4
package/README.md
CHANGED
|
@@ -1,3 +1,139 @@
|
|
|
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. Built for serious builders who ship.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx create-supersaas-app my-saas --key YOUR_LICENSE_KEY
|
|
11
|
+
cd my-saas
|
|
12
|
+
npm run dev
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## What You Get
|
|
16
|
+
|
|
17
|
+
- **Next.js 15** + React 19 + TypeScript
|
|
18
|
+
- **Authentication** (Better Auth) - email, OAuth, magic links
|
|
19
|
+
- **Payments** (Stripe) - subscriptions, one-time, webhooks
|
|
20
|
+
- **Database** (Drizzle ORM) - type-safe, migrations included
|
|
21
|
+
- **Email** (Resend) - transactional + newsletter
|
|
22
|
+
- **Teams** - multi-tenant with invites
|
|
23
|
+
- **Admin Dashboard** - user management, analytics
|
|
24
|
+
- **Beautiful UI** - Tailwind CSS v4, dark mode, responsive
|
|
25
|
+
- **API Routes** - rate limiting, validation, error handling
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Interactive Mode (Recommended)
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx create-supersaas-app
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
You'll be prompted for:
|
|
36
|
+
- App name
|
|
37
|
+
- Database provider (Neon, Supabase, PlanetScale, SQLite)
|
|
38
|
+
- Optional features (blog, AI routes, admin, waitlist)
|
|
39
|
+
|
|
40
|
+
### Non-Interactive Mode
|
|
41
|
+
|
|
42
|
+
Perfect for CI/CD or automation:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx create-supersaas-app my-saas \
|
|
46
|
+
--key YOUR_LICENSE_KEY \
|
|
47
|
+
--db neon \
|
|
48
|
+
--features blog,ai,admin \
|
|
49
|
+
--yes
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Options
|
|
53
|
+
|
|
54
|
+
| Flag | Description | Example |
|
|
55
|
+
|------|-------------|---------|
|
|
56
|
+
| `--key` | License key (required) | `--key lic_abc123` |
|
|
57
|
+
| `--db` | Database provider | `--db neon` |
|
|
58
|
+
| `--features` | Comma-separated features | `--features blog,ai` |
|
|
59
|
+
| `--yes` | Skip prompts, use defaults | `--yes` |
|
|
60
|
+
|
|
61
|
+
## Features
|
|
62
|
+
|
|
63
|
+
### Core (All Tiers)
|
|
64
|
+
|
|
65
|
+
- Authentication (email + OAuth)
|
|
66
|
+
- Database with migrations
|
|
67
|
+
- Payment processing
|
|
68
|
+
- Email delivery
|
|
69
|
+
- Team management
|
|
70
|
+
- API routes
|
|
71
|
+
|
|
72
|
+
### Pro Tier
|
|
73
|
+
|
|
74
|
+
- AI routes (OpenAI/Anthropic)
|
|
75
|
+
- Admin dashboard
|
|
76
|
+
- Advanced themes
|
|
77
|
+
- Priority support
|
|
78
|
+
|
|
79
|
+
### Bundle Tier
|
|
80
|
+
|
|
81
|
+
- Everything in Pro
|
|
82
|
+
- Lifetime updates
|
|
83
|
+
- Team licenses
|
|
84
|
+
- Early access to new features
|
|
85
|
+
|
|
86
|
+
## Database Options
|
|
87
|
+
|
|
88
|
+
- **Neon** (Postgres) - Recommended, generous free tier
|
|
89
|
+
- **Supabase** (Postgres) - Auth + DB + Storage
|
|
90
|
+
- **PlanetScale** (MySQL) - Serverless MySQL
|
|
91
|
+
- **SQLite** - Local development
|
|
92
|
+
|
|
93
|
+
## After Installation
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
cd your-app
|
|
97
|
+
|
|
98
|
+
# 1. Set up environment variables
|
|
99
|
+
cp .env.example .env.local
|
|
100
|
+
# Edit .env.local with your keys
|
|
101
|
+
|
|
102
|
+
# 2. Run migrations
|
|
103
|
+
npm run db:push
|
|
104
|
+
|
|
105
|
+
# 3. Start dev server
|
|
106
|
+
npm run dev
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Visit `http://localhost:3000`
|
|
110
|
+
|
|
111
|
+
## CLI Commands (built-in)
|
|
112
|
+
|
|
113
|
+
Once your app is created, use the built-in CLI from your project directory:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
npx supersaas setup stripe
|
|
117
|
+
npx supersaas add blog
|
|
118
|
+
npx supersaas deploy
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
You need a valid supersaas-kit license to use this. Get yours at [supersaas-kit.superbuilds.fun](https://supersaas-kit.superbuilds.fun)
|
|
124
|
+
|
|
125
|
+
## Support
|
|
126
|
+
|
|
127
|
+
- **Docs:** https://supersaas-kit.superbuilds.fun/docs
|
|
128
|
+
- **Issues:** https://github.com/bixblues/supersaas-kit/issues
|
|
129
|
+
- **Email:** hi@superbuilds.fun
|
|
130
|
+
|
|
131
|
+
## What's Next?
|
|
132
|
+
|
|
133
|
+
1. Read the docs: https://supersaas-kit.superbuilds.fun/docs
|
|
134
|
+
2. Join Discord: https://supersaas-kit.superbuilds.fun/discord
|
|
135
|
+
3. Follow updates: https://x.com/superbuilds
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
Built with ❤️ by indie hackers, for indie hackers.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
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 not include this tier. 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
|
+
execSync(`tar -xzf "${archivePath}" -C "${targetDir}"`, {
|
|
65
|
+
stdio: "pipe"
|
|
66
|
+
});
|
|
67
|
+
await fs.remove(tmpDir);
|
|
68
|
+
spinner.succeed(chalk.green("Template downloaded and extracted"));
|
|
69
|
+
return true;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
spinner.fail(chalk.red("Download failed"));
|
|
72
|
+
p3.log.error(
|
|
73
|
+
chalk.red(
|
|
74
|
+
`Could not download template: ${err.message}`
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
p3.log.info(chalk.dim("Check your internet connection and try again."));
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function createApp(config) {
|
|
82
|
+
const targetDir = path.resolve(process.cwd(), config.name);
|
|
83
|
+
if (await fs.pathExists(targetDir)) {
|
|
84
|
+
p3.log.error(chalk.red(`Directory "${config.name}" already exists.`));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
const downloaded = await downloadTemplate(config.licenseKey, targetDir);
|
|
88
|
+
if (!downloaded) {
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
const excludePaths = /* @__PURE__ */ new Set();
|
|
92
|
+
for (const [feature, paths] of Object.entries(FEATURE_PATHS)) {
|
|
93
|
+
if (!config.features.includes(feature)) {
|
|
94
|
+
for (const p4 of paths) {
|
|
95
|
+
excludePaths.add(p4);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const excludePath of excludePaths) {
|
|
100
|
+
const fullPath = path.join(targetDir, excludePath);
|
|
101
|
+
if (await fs.pathExists(fullPath)) {
|
|
102
|
+
await fs.remove(fullPath);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const pkgTemplatePath = path.join(targetDir, "package.json.template");
|
|
106
|
+
if (await fs.pathExists(pkgTemplatePath)) {
|
|
107
|
+
await fs.move(pkgTemplatePath, path.join(targetDir, "package.json"));
|
|
108
|
+
}
|
|
109
|
+
await substituteVars(targetDir, config);
|
|
110
|
+
await configureDatabase(targetDir, config);
|
|
111
|
+
await updateManifest(targetDir, config);
|
|
112
|
+
const envExample = path.join(targetDir, ".env.example");
|
|
113
|
+
if (await fs.pathExists(envExample)) {
|
|
114
|
+
await fs.copy(envExample, path.join(targetDir, ".env"));
|
|
115
|
+
}
|
|
116
|
+
await cleanupFeatureImports(targetDir, config);
|
|
117
|
+
const activeFeatures = ["auth", "payments", "email", "seo", ...config.features];
|
|
118
|
+
p3.log.info(chalk.dim(`Database: ${config.database}`));
|
|
119
|
+
p3.log.info(chalk.dim(`Features: ${activeFeatures.join(", ")}`));
|
|
120
|
+
}
|
|
121
|
+
async function configureDatabase(targetDir, config) {
|
|
122
|
+
const dbConfig = DATABASE_CONFIGS[config.database];
|
|
123
|
+
if (!dbConfig) return;
|
|
124
|
+
const envPath = path.join(targetDir, ".env.example");
|
|
125
|
+
if (await fs.pathExists(envPath)) {
|
|
126
|
+
let content = await fs.readFile(envPath, "utf-8");
|
|
127
|
+
const hint = dbConfig.hint;
|
|
128
|
+
if (hint && !content.includes(hint)) {
|
|
129
|
+
content = content.replace(/DATABASE_URL=/, `${hint}
|
|
130
|
+
DATABASE_URL=`);
|
|
131
|
+
await fs.writeFile(envPath, content);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (config.database === "local") {
|
|
135
|
+
const envPath2 = path.join(targetDir, ".env.example");
|
|
136
|
+
if (await fs.pathExists(envPath2)) {
|
|
137
|
+
let content = await fs.readFile(envPath2, "utf-8");
|
|
138
|
+
if (content.includes("DATABASE_URL=postgres://postgres:postgres@localhost:5432/app")) ;
|
|
139
|
+
await fs.writeFile(envPath2, content);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function updateManifest(targetDir, config) {
|
|
144
|
+
const manifestPath = path.join(targetDir, ".supersaas", "manifest.json");
|
|
145
|
+
if (await fs.pathExists(manifestPath)) {
|
|
146
|
+
const manifest = await fs.readJson(manifestPath);
|
|
147
|
+
manifest.database = config.database;
|
|
148
|
+
manifest.features = config.features;
|
|
149
|
+
manifest.tier = config.tier;
|
|
150
|
+
manifest.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
151
|
+
if (manifest.providers) {
|
|
152
|
+
if (manifest.providers.db) manifest.providers.db = "postgres";
|
|
153
|
+
}
|
|
154
|
+
await fs.writeJson(manifestPath, manifest, { spaces: 2 });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function cleanupFeatureImports(targetDir, config) {
|
|
158
|
+
if (!config.features.includes("blog")) {
|
|
159
|
+
const sitemapPath = path.join(targetDir, "src/app/sitemap.ts");
|
|
160
|
+
if (await fs.pathExists(sitemapPath)) {
|
|
161
|
+
await fs.writeFile(sitemapPath, `import { MetadataRoute } from "next";
|
|
162
|
+
import { appConfig } from "@/config/app";
|
|
163
|
+
|
|
164
|
+
const baseUrl = appConfig.url;
|
|
165
|
+
|
|
166
|
+
export default function sitemap(): MetadataRoute.Sitemap {
|
|
167
|
+
const now = new Date();
|
|
168
|
+
|
|
169
|
+
return [
|
|
170
|
+
{ url: baseUrl, lastModified: now, changeFrequency: "weekly", priority: 1.0 },
|
|
171
|
+
{ url: \`\${baseUrl}/sign-up\`, lastModified: now, changeFrequency: "yearly", priority: 0.5 },
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
`);
|
|
175
|
+
}
|
|
176
|
+
const configPath = path.join(targetDir, "src/config/app.ts");
|
|
177
|
+
if (await fs.pathExists(configPath)) {
|
|
178
|
+
let content = await fs.readFile(configPath, "utf-8");
|
|
179
|
+
content = content.replace(/\s*\{\s*label:\s*["']Blog["'],\s*href:\s*["']\/blog["']\s*\},?\n?/g, "");
|
|
180
|
+
await fs.writeFile(configPath, content);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!config.features.includes("admin")) {
|
|
184
|
+
const sidebarPath = path.join(targetDir, "src/components/layout/sidebar.tsx");
|
|
185
|
+
if (await fs.pathExists(sidebarPath)) {
|
|
186
|
+
let content = await fs.readFile(sidebarPath, "utf-8");
|
|
187
|
+
content = content.replace(/\s*\{\s*label:\s*["']Admin["'].*?\},?\n?/gs, "");
|
|
188
|
+
await fs.writeFile(sidebarPath, content);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function substituteVars(dir, config) {
|
|
193
|
+
const textExtensions = [
|
|
194
|
+
".ts",
|
|
195
|
+
".tsx",
|
|
196
|
+
".js",
|
|
197
|
+
".jsx",
|
|
198
|
+
".json",
|
|
199
|
+
".md",
|
|
200
|
+
".mdx",
|
|
201
|
+
".css",
|
|
202
|
+
".env",
|
|
203
|
+
".example"
|
|
204
|
+
];
|
|
205
|
+
const files = await getAllFiles(dir);
|
|
206
|
+
for (const file of files) {
|
|
207
|
+
const ext = path.extname(file);
|
|
208
|
+
const basename = path.basename(file);
|
|
209
|
+
if (!textExtensions.includes(ext) && !basename.startsWith(".env")) continue;
|
|
210
|
+
let content = await fs.readFile(file, "utf-8");
|
|
211
|
+
let changed = false;
|
|
212
|
+
for (const [placeholder, resolver] of Object.entries(SUBSTITUTIONS)) {
|
|
213
|
+
if (content.includes(placeholder)) {
|
|
214
|
+
content = content.replaceAll(placeholder, resolver(config));
|
|
215
|
+
changed = true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (changed) {
|
|
219
|
+
await fs.writeFile(file, content);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async function getAllFiles(dir) {
|
|
224
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
225
|
+
const files = [];
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
const full = path.join(dir, entry.name);
|
|
228
|
+
if (entry.isDirectory()) {
|
|
229
|
+
if (entry.name === "node_modules" || entry.name === ".next") continue;
|
|
230
|
+
files.push(...await getAllFiles(full));
|
|
231
|
+
} else {
|
|
232
|
+
files.push(full);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return files;
|
|
236
|
+
}
|
|
237
|
+
var SUPERSAAS_DIR = path.join(os2.homedir(), ".supersaas");
|
|
238
|
+
var LICENSE_PATH = path.join(SUPERSAAS_DIR, "license.json");
|
|
239
|
+
var DODO_BASE_URL = process.env.SUPERSAAS_DODO_URL || "https://test.dodopayments.com";
|
|
240
|
+
function inferTierFromActivation(_data) {
|
|
241
|
+
return "bundle";
|
|
242
|
+
}
|
|
243
|
+
async function loadLocalLicense() {
|
|
244
|
+
try {
|
|
245
|
+
if (await fs.pathExists(LICENSE_PATH)) {
|
|
246
|
+
const data = await fs.readJson(LICENSE_PATH);
|
|
247
|
+
if (data.key && data.tier && data.instance_id) {
|
|
248
|
+
return data;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
async function validateKey(key) {
|
|
256
|
+
try {
|
|
257
|
+
const res = await fetch(`${DODO_BASE_URL}/licenses/validate`, {
|
|
258
|
+
method: "POST",
|
|
259
|
+
headers: { "Content-Type": "application/json" },
|
|
260
|
+
body: JSON.stringify({ license_key: key })
|
|
261
|
+
});
|
|
262
|
+
if (!res.ok) {
|
|
263
|
+
return { valid: false };
|
|
264
|
+
}
|
|
265
|
+
const data = await res.json();
|
|
266
|
+
return { valid: data.valid !== false, data };
|
|
267
|
+
} catch {
|
|
268
|
+
return { valid: false };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async function activateOnline(key) {
|
|
272
|
+
const machineName = `${os2.hostname()} (${os2.platform()})`;
|
|
273
|
+
try {
|
|
274
|
+
const validation = await validateKey(key);
|
|
275
|
+
if (!validation.valid) {
|
|
276
|
+
p3.log.error(chalk.red("Invalid or expired license key."));
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
const res = await fetch(`${DODO_BASE_URL}/licenses/activate`, {
|
|
280
|
+
method: "POST",
|
|
281
|
+
headers: { "Content-Type": "application/json" },
|
|
282
|
+
body: JSON.stringify({
|
|
283
|
+
license_key: key,
|
|
284
|
+
name: machineName
|
|
285
|
+
})
|
|
286
|
+
});
|
|
287
|
+
if (!res.ok) {
|
|
288
|
+
const body = await res.json().catch(() => ({}));
|
|
289
|
+
const msg = body.message || body.error || `HTTP ${res.status}`;
|
|
290
|
+
p3.log.error(chalk.red(`Activation failed: ${msg}`));
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
const result = await res.json();
|
|
294
|
+
const tier = inferTierFromActivation(result);
|
|
295
|
+
const license = {
|
|
296
|
+
key,
|
|
297
|
+
tier,
|
|
298
|
+
instance_id: result.id || result.license_key_instance_id || "",
|
|
299
|
+
activated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
300
|
+
valid: true
|
|
301
|
+
};
|
|
302
|
+
await fs.ensureDir(SUPERSAAS_DIR);
|
|
303
|
+
await fs.writeJson(LICENSE_PATH, license, { spaces: 2 });
|
|
304
|
+
return license;
|
|
305
|
+
} catch {
|
|
306
|
+
p3.log.error(
|
|
307
|
+
chalk.red("Could not reach license server. Check your connection.")
|
|
308
|
+
);
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
async function activate(keyArg, nonInteractive) {
|
|
313
|
+
const existing = await loadLocalLicense();
|
|
314
|
+
if (existing) {
|
|
315
|
+
const check = await validateKey(existing.key).catch(() => ({ valid: true }));
|
|
316
|
+
if (check.valid) {
|
|
317
|
+
p3.log.success(
|
|
318
|
+
chalk.dim(
|
|
319
|
+
`Licensed (${existing.tier}) - ${existing.key.slice(0, 12)}...`
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
return existing;
|
|
323
|
+
}
|
|
324
|
+
await fs.remove(LICENSE_PATH);
|
|
325
|
+
p3.log.warning(chalk.yellow("License expired or revoked. Please re-activate."));
|
|
326
|
+
}
|
|
327
|
+
if (!keyArg && nonInteractive) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
const key = keyArg || await p3.text({
|
|
331
|
+
message: "Enter your license key:",
|
|
332
|
+
placeholder: "Paste your license key from the purchase email",
|
|
333
|
+
validate: (v) => {
|
|
334
|
+
if (!v) return "License key is required";
|
|
335
|
+
if (v.length < 8) return "Invalid key format";
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
if (p3.isCancel(key)) return null;
|
|
339
|
+
p3.log.step("Activating license...");
|
|
340
|
+
const license = await activateOnline(key);
|
|
341
|
+
if (license) {
|
|
342
|
+
p3.log.success(chalk.green(`Activated! Tier: ${license.tier}`));
|
|
343
|
+
return license;
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
var require2 = createRequire(import.meta.url);
|
|
348
|
+
var { version } = require2("../package.json");
|
|
349
|
+
var VALID_DBS = ["neon", "supabase", "local"];
|
|
350
|
+
var VALID_FEATURES = ["blog", "admin"];
|
|
351
|
+
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)").action(async (name, options) => {
|
|
352
|
+
const nonInteractive = options.defaults || options.yes || !process.stdin.isTTY;
|
|
353
|
+
if (!nonInteractive) p3.intro(chalk.bold("supersaas-kit"));
|
|
354
|
+
let licenseResult = await activate(options.key, nonInteractive);
|
|
355
|
+
if (!licenseResult) {
|
|
356
|
+
if (!nonInteractive) p3.cancel("License activation failed.");
|
|
357
|
+
else console.error("License activation failed.");
|
|
358
|
+
process.exit(1);
|
|
359
|
+
}
|
|
360
|
+
let appName = name;
|
|
361
|
+
if (!appName && nonInteractive) {
|
|
362
|
+
console.error("Error: app name is required in non-interactive mode. Pass it as the first argument: create-supersaas-app <name>");
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
if (!appName) {
|
|
366
|
+
appName = await p3.text({
|
|
367
|
+
message: "What's your app called?",
|
|
368
|
+
placeholder: "my-saas",
|
|
369
|
+
validate: (v) => {
|
|
370
|
+
if (!v) return "App name is required";
|
|
371
|
+
if (!/^[a-z0-9-]+$/.test(v))
|
|
372
|
+
return "Use lowercase letters, numbers, and hyphens only";
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
if (p3.isCancel(appName)) {
|
|
376
|
+
p3.cancel("Cancelled.");
|
|
377
|
+
process.exit(0);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
let dbChoice = options.db || (nonInteractive ? "neon" : null);
|
|
381
|
+
if (dbChoice && !VALID_DBS.includes(dbChoice)) {
|
|
382
|
+
const msg = `Invalid --db value "${dbChoice}". Valid options: ${VALID_DBS.join(", ")}`;
|
|
383
|
+
if (nonInteractive) {
|
|
384
|
+
console.error(`Error: ${msg}`);
|
|
385
|
+
process.exit(1);
|
|
386
|
+
}
|
|
387
|
+
p3.log.warn(chalk.yellow(msg));
|
|
388
|
+
dbChoice = null;
|
|
389
|
+
}
|
|
390
|
+
if (!dbChoice) {
|
|
391
|
+
dbChoice = await p3.select({
|
|
392
|
+
message: "Database provider?",
|
|
393
|
+
options: [
|
|
394
|
+
{ value: "neon", label: "Neon (Postgres)", hint: "serverless, recommended" },
|
|
395
|
+
{ value: "supabase", label: "Supabase (Postgres)" },
|
|
396
|
+
{ value: "local", label: "Local Docker Postgres", hint: "docker compose up -d" }
|
|
397
|
+
]
|
|
398
|
+
});
|
|
399
|
+
if (p3.isCancel(dbChoice)) {
|
|
400
|
+
p3.cancel("Cancelled.");
|
|
401
|
+
process.exit(0);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
let features = options.features ? options.features.split(",").map((f) => f.trim()).filter(Boolean) : [];
|
|
405
|
+
if (features.length > 0) {
|
|
406
|
+
const invalid = features.filter((f) => !VALID_FEATURES.includes(f));
|
|
407
|
+
if (invalid.length > 0) {
|
|
408
|
+
const msg = `Unknown features: ${invalid.join(", ")}. Valid options: ${VALID_FEATURES.join(", ")}`;
|
|
409
|
+
if (nonInteractive) {
|
|
410
|
+
console.error(`Error: ${msg}`);
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
p3.log.warn(chalk.yellow(msg));
|
|
414
|
+
features = features.filter((f) => VALID_FEATURES.includes(f));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (!nonInteractive && !options.features) {
|
|
418
|
+
features = await p3.multiselect({
|
|
419
|
+
message: "Include optional modules?",
|
|
420
|
+
options: [
|
|
421
|
+
{ value: "blog", label: "Blog (MDX)", hint: "SEO boost" },
|
|
422
|
+
{ value: "admin", label: "Admin Dashboard", hint: "Pro" }
|
|
423
|
+
],
|
|
424
|
+
required: false
|
|
425
|
+
});
|
|
426
|
+
if (p3.isCancel(features)) {
|
|
427
|
+
p3.cancel("Cancelled.");
|
|
428
|
+
process.exit(0);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
await createApp({
|
|
432
|
+
name: appName,
|
|
433
|
+
database: dbChoice,
|
|
434
|
+
features: features || [],
|
|
435
|
+
tier: licenseResult.tier,
|
|
436
|
+
licenseKey: licenseResult.key
|
|
437
|
+
});
|
|
438
|
+
if (!nonInteractive) {
|
|
439
|
+
p3.outro(
|
|
440
|
+
chalk.green("Done!") + ` Run ${chalk.cyan(`cd ${appName} && npm install && npm run dev`)} to start.`
|
|
441
|
+
);
|
|
442
|
+
} else {
|
|
443
|
+
console.log(`\u2714 Created ${appName}. Run: cd ${appName} && npm install && npm run dev`);
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
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.0",
|
|
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
|
+
}
|