create-start-kit-dev 0.1.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 +49 -0
- package/dist/index.mjs +811 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# create-start-kit-dev
|
|
2
|
+
|
|
3
|
+
CLI for scaffolding and configuring [Start Kit](https://github.com/CarlosZiegler/start-kit.dev) projects.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
### Create a new project
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bunx create-start-kit-dev create my-app
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This downloads the template, installs dependencies, and runs the interactive setup wizard.
|
|
14
|
+
|
|
15
|
+
### Initialize an existing project
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bunx create-start-kit-dev init
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Run a specific setup phase
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bunx create-start-kit-dev init --step database
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Setup Phases
|
|
28
|
+
|
|
29
|
+
The wizard guides you through these phases (resumable if interrupted):
|
|
30
|
+
|
|
31
|
+
| Phase | What it does |
|
|
32
|
+
|-------|-------------|
|
|
33
|
+
| **Branding** | App name, description, logo, colors |
|
|
34
|
+
| **Features** | Toggle AI, payments, storage, i18n |
|
|
35
|
+
| **Database** | PostgreSQL connection and schema setup |
|
|
36
|
+
| **Environment** | Generate `.env` with required variables |
|
|
37
|
+
| **Infrastructure** | Docker and deployment config |
|
|
38
|
+
|
|
39
|
+
## Development
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bun install
|
|
43
|
+
bun run build # Build with tsdown
|
|
44
|
+
bun run dev # Watch mode
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
MIT
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { confirm, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { downloadTemplate } from "giget";
|
|
5
|
+
|
|
6
|
+
//#region src/lib/state.ts
|
|
7
|
+
const STATE_FILE = ".setup-state.json";
|
|
8
|
+
const DEFAULT_STATE = {
|
|
9
|
+
version: 1,
|
|
10
|
+
completedPhases: []
|
|
11
|
+
};
|
|
12
|
+
function getStatePath() {
|
|
13
|
+
return `${process.cwd()}/${STATE_FILE}`;
|
|
14
|
+
}
|
|
15
|
+
function loadState() {
|
|
16
|
+
const path = getStatePath();
|
|
17
|
+
if (!existsSync(path)) return { ...DEFAULT_STATE };
|
|
18
|
+
const content = readFileSync(path, "utf-8");
|
|
19
|
+
return JSON.parse(content);
|
|
20
|
+
}
|
|
21
|
+
async function saveState(state) {
|
|
22
|
+
await Bun.write(getStatePath(), JSON.stringify(state, null, 2));
|
|
23
|
+
}
|
|
24
|
+
function isPhaseCompleted(state, phase) {
|
|
25
|
+
return state.completedPhases.includes(phase);
|
|
26
|
+
}
|
|
27
|
+
function markPhaseCompleted(state, phase) {
|
|
28
|
+
if (!state.completedPhases.includes(phase)) state.completedPhases.push(phase);
|
|
29
|
+
return state;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/lib/helpers.ts
|
|
34
|
+
const NAME_PATTERN = /name:\s*"[^"]*"/;
|
|
35
|
+
const DESCRIPTION_PATTERN = /description:\s*"[^"]*"/;
|
|
36
|
+
function updateJsonFile(path, updates) {
|
|
37
|
+
const content = readFileSync(path, "utf-8");
|
|
38
|
+
const json = JSON.parse(content);
|
|
39
|
+
for (const [key, value] of Object.entries(updates)) json[key] = value;
|
|
40
|
+
writeFileSync(path, `${JSON.stringify(json, null, 2)}\n`);
|
|
41
|
+
}
|
|
42
|
+
function updateAppConfig(path, name, description) {
|
|
43
|
+
let content = readFileSync(path, "utf-8");
|
|
44
|
+
content = content.replace(NAME_PATTERN, `name: "${name}"`);
|
|
45
|
+
content = content.replace(DESCRIPTION_PATTERN, `description: "${description}"`);
|
|
46
|
+
writeFileSync(path, content);
|
|
47
|
+
}
|
|
48
|
+
function writeEnvFile(path, vars, comments) {
|
|
49
|
+
const lines = [];
|
|
50
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
51
|
+
if (comments?.[key]) lines.push(`# ${comments[key]}`);
|
|
52
|
+
const formatted = value.includes(" ") ? `"${value}"` : value;
|
|
53
|
+
lines.push(`${key}=${formatted}`);
|
|
54
|
+
}
|
|
55
|
+
writeFileSync(path, `${lines.join("\n")}\n`);
|
|
56
|
+
}
|
|
57
|
+
function readEnvFile(path) {
|
|
58
|
+
try {
|
|
59
|
+
const content = readFileSync(path, "utf-8");
|
|
60
|
+
const result = {};
|
|
61
|
+
for (const line of content.split("\n")) {
|
|
62
|
+
const trimmed = line.trim();
|
|
63
|
+
if (trimmed && !trimmed.startsWith("#")) {
|
|
64
|
+
const eqIndex = trimmed.indexOf("=");
|
|
65
|
+
if (eqIndex > 0) {
|
|
66
|
+
const key = trimmed.slice(0, eqIndex);
|
|
67
|
+
let value = trimmed.slice(eqIndex + 1);
|
|
68
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
69
|
+
result[key] = value;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
} catch {
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function generateSecret(bytes = 32) {
|
|
79
|
+
const buffer = new Uint8Array(bytes);
|
|
80
|
+
crypto.getRandomValues(buffer);
|
|
81
|
+
return btoa(String.fromCharCode(...buffer));
|
|
82
|
+
}
|
|
83
|
+
async function exec(command) {
|
|
84
|
+
const proc = Bun.spawn([
|
|
85
|
+
"sh",
|
|
86
|
+
"-c",
|
|
87
|
+
command
|
|
88
|
+
], {
|
|
89
|
+
stdout: "pipe",
|
|
90
|
+
stderr: "pipe"
|
|
91
|
+
});
|
|
92
|
+
const stdout = await new Response(proc.stdout).text();
|
|
93
|
+
const stderr = await new Response(proc.stderr).text();
|
|
94
|
+
const exitCode = await proc.exited;
|
|
95
|
+
return {
|
|
96
|
+
stdout: stdout.trim(),
|
|
97
|
+
stderr: stderr.trim(),
|
|
98
|
+
exitCode
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
async function testDbConnection(url) {
|
|
102
|
+
try {
|
|
103
|
+
const { SQL } = await import("bun");
|
|
104
|
+
const sql = new SQL(url);
|
|
105
|
+
const result = sql.query("SELECT version()").get();
|
|
106
|
+
sql.close();
|
|
107
|
+
return {
|
|
108
|
+
ok: true,
|
|
109
|
+
version: result.version
|
|
110
|
+
};
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
error: error instanceof Error ? error.message : String(error)
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region src/lib/validators.ts
|
|
121
|
+
const DOMAIN_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i;
|
|
122
|
+
function isValidDomain(value) {
|
|
123
|
+
return DOMAIN_PATTERN.test(value);
|
|
124
|
+
}
|
|
125
|
+
function isValidPostgresUrl(value) {
|
|
126
|
+
return value.startsWith("postgresql://") || value.startsWith("postgres://");
|
|
127
|
+
}
|
|
128
|
+
function isValidAppName(value) {
|
|
129
|
+
return value.length >= 2 && value.length <= 50;
|
|
130
|
+
}
|
|
131
|
+
function toKebabCase(value) {
|
|
132
|
+
return value.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/phases/branding.ts
|
|
137
|
+
async function runBranding(state) {
|
|
138
|
+
const appName = await text({
|
|
139
|
+
message: "What's your app name?",
|
|
140
|
+
placeholder: "My SaaS App",
|
|
141
|
+
initialValue: state.branding?.appName,
|
|
142
|
+
validate: (v) => {
|
|
143
|
+
if (!isValidAppName(v)) return "Name must be 2-50 characters";
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
if (isCancel(appName)) process.exit(0);
|
|
147
|
+
const description = await text({
|
|
148
|
+
message: "Short description?",
|
|
149
|
+
placeholder: "A platform for managing widgets",
|
|
150
|
+
initialValue: state.branding?.description
|
|
151
|
+
});
|
|
152
|
+
if (isCancel(description)) process.exit(0);
|
|
153
|
+
const domain = await text({
|
|
154
|
+
message: "What's your domain? (for emails, auth origins)",
|
|
155
|
+
placeholder: "mysaasapp.com",
|
|
156
|
+
initialValue: state.branding?.domain,
|
|
157
|
+
validate: (v) => {
|
|
158
|
+
if (v && !isValidDomain(v)) return "Enter a valid domain (e.g. myapp.com)";
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
if (isCancel(domain)) process.exit(0);
|
|
162
|
+
const s = spinner();
|
|
163
|
+
s.start("Updating files...");
|
|
164
|
+
const kebabName = toKebabCase(appName);
|
|
165
|
+
updateJsonFile("package.json", { name: kebabName });
|
|
166
|
+
updateAppConfig("src/lib/config/app.config.ts", appName, description);
|
|
167
|
+
s.stop("Files updated");
|
|
168
|
+
log.success(`package.json → name: "${kebabName}"`);
|
|
169
|
+
log.success(`app.config.ts → name: "${appName}"`);
|
|
170
|
+
state.branding = {
|
|
171
|
+
appName,
|
|
172
|
+
description,
|
|
173
|
+
domain: domain || ""
|
|
174
|
+
};
|
|
175
|
+
markPhaseCompleted(state, "branding");
|
|
176
|
+
await saveState(state);
|
|
177
|
+
return state;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
//#endregion
|
|
181
|
+
//#region src/phases/database.ts
|
|
182
|
+
async function runDatabase(state) {
|
|
183
|
+
const dbChoice = await select({
|
|
184
|
+
message: "Do you already have a PostgreSQL database?",
|
|
185
|
+
options: [{
|
|
186
|
+
value: "own",
|
|
187
|
+
label: "Yes, I have a connection URL"
|
|
188
|
+
}, {
|
|
189
|
+
value: "instagres",
|
|
190
|
+
label: "No, create one instantly with Instagres (pg.new)",
|
|
191
|
+
hint: "free 72h, claim to keep"
|
|
192
|
+
}]
|
|
193
|
+
});
|
|
194
|
+
if (isCancel(dbChoice)) process.exit(0);
|
|
195
|
+
let databaseUrl;
|
|
196
|
+
if (dbChoice === "instagres") {
|
|
197
|
+
const s = spinner();
|
|
198
|
+
s.start("Creating instant Neon database via Instagres...");
|
|
199
|
+
const result = await exec("bunx get-db --yes --env .env --key DATABASE_URL");
|
|
200
|
+
if (result.exitCode !== 0) {
|
|
201
|
+
s.stop("Failed to create database");
|
|
202
|
+
log.error(`get-db failed: ${result.stderr}`);
|
|
203
|
+
log.info("You can try manually: npx get-db --yes");
|
|
204
|
+
log.info("Or provide your own DATABASE_URL and re-run: bunx create-start-kit-dev init --step database");
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
s.stop("Database created!");
|
|
208
|
+
databaseUrl = readEnvFile(".env").DATABASE_URL ?? "";
|
|
209
|
+
if (!databaseUrl) {
|
|
210
|
+
log.error("DATABASE_URL not found in .env after get-db. Check .env manually.");
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
log.success("DATABASE_URL written to .env");
|
|
214
|
+
log.warn("This database expires in 72 hours.");
|
|
215
|
+
log.info("Claim it at https://neon.tech or run: npx get-db claim");
|
|
216
|
+
} else {
|
|
217
|
+
const url = await text({
|
|
218
|
+
message: "Enter your DATABASE_URL:",
|
|
219
|
+
placeholder: "postgresql://user:pass@host:5432/mydb",
|
|
220
|
+
validate: (v) => {
|
|
221
|
+
if (!isValidPostgresUrl(v)) return "Must start with postgresql:// or postgres://";
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
if (isCancel(url)) process.exit(0);
|
|
225
|
+
databaseUrl = url;
|
|
226
|
+
}
|
|
227
|
+
const s = spinner();
|
|
228
|
+
s.start("Testing database connection...");
|
|
229
|
+
const test = await testDbConnection(databaseUrl);
|
|
230
|
+
if (!test.ok) {
|
|
231
|
+
s.stop("Connection failed");
|
|
232
|
+
log.error(`Could not connect: ${test.error}`);
|
|
233
|
+
log.info("Check your DATABASE_URL and try again.");
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
s.stop("Connected to PostgreSQL");
|
|
237
|
+
const shouldMigrate = await confirm({
|
|
238
|
+
message: "Run database migrations now?",
|
|
239
|
+
initialValue: true
|
|
240
|
+
});
|
|
241
|
+
if (isCancel(shouldMigrate)) process.exit(0);
|
|
242
|
+
if (shouldMigrate) {
|
|
243
|
+
const ms = spinner();
|
|
244
|
+
ms.start("Running migrations...");
|
|
245
|
+
const envVars = readEnvFile(".env");
|
|
246
|
+
if (!envVars.DATABASE_URL) {
|
|
247
|
+
envVars.DATABASE_URL = databaseUrl;
|
|
248
|
+
writeEnvFile(".env", envVars);
|
|
249
|
+
}
|
|
250
|
+
const migrateResult = await exec("bun run db:push");
|
|
251
|
+
if (migrateResult.exitCode !== 0) {
|
|
252
|
+
ms.stop("Migration failed");
|
|
253
|
+
log.error(migrateResult.stderr);
|
|
254
|
+
log.info("You can run migrations later with: bun run db:push");
|
|
255
|
+
} else ms.stop("Migrations applied!");
|
|
256
|
+
}
|
|
257
|
+
state.database = {
|
|
258
|
+
provider: dbChoice,
|
|
259
|
+
migrated: shouldMigrate === true
|
|
260
|
+
};
|
|
261
|
+
markPhaseCompleted(state, "database");
|
|
262
|
+
await saveState(state);
|
|
263
|
+
return state;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
//#endregion
|
|
267
|
+
//#region src/phases/env.ts
|
|
268
|
+
const DUMMY_VALUES = {
|
|
269
|
+
RESEND_API_KEY: "re_dummy_replace_me",
|
|
270
|
+
S3_ACCESS_KEY_ID: "dummy_replace_me",
|
|
271
|
+
S3_SECRET_ACCESS_KEY: "dummy_replace_me",
|
|
272
|
+
S3_BUCKET: "dummy-bucket"
|
|
273
|
+
};
|
|
274
|
+
function setupCoreVars(ctx) {
|
|
275
|
+
if (!ctx.envVars.BETTER_AUTH_SECRET || ctx.envVars.BETTER_AUTH_SECRET.includes("haha")) {
|
|
276
|
+
ctx.envVars.BETTER_AUTH_SECRET = generateSecret();
|
|
277
|
+
log.success("BETTER_AUTH_SECRET — auto-generated");
|
|
278
|
+
} else log.info("BETTER_AUTH_SECRET — keeping existing value");
|
|
279
|
+
ctx.envVars.BETTER_AUTH_BASE_URL = ctx.envVars.BETTER_AUTH_BASE_URL ?? "http://localhost:3000";
|
|
280
|
+
ctx.envVars.VITE_BETTER_AUTH_BASE_URL = ctx.envVars.VITE_BETTER_AUTH_BASE_URL ?? "http://localhost:3000";
|
|
281
|
+
}
|
|
282
|
+
async function setupEmail(ctx, enabled) {
|
|
283
|
+
if (enabled) {
|
|
284
|
+
const resendKey = await text({
|
|
285
|
+
message: "RESEND_API_KEY (get one at resend.com, or Enter for placeholder):",
|
|
286
|
+
placeholder: "re_xxxxxxxxxxxx",
|
|
287
|
+
initialValue: ctx.envVars.RESEND_API_KEY
|
|
288
|
+
});
|
|
289
|
+
if (isCancel(resendKey)) process.exit(0);
|
|
290
|
+
if (!resendKey || resendKey === DUMMY_VALUES.RESEND_API_KEY) {
|
|
291
|
+
ctx.envVars.RESEND_API_KEY = DUMMY_VALUES.RESEND_API_KEY;
|
|
292
|
+
ctx.placeholders.push("RESEND_API_KEY");
|
|
293
|
+
log.warn("Using placeholder — emails will fail until you add a real key");
|
|
294
|
+
} else ctx.envVars.RESEND_API_KEY = resendKey;
|
|
295
|
+
const domain = ctx.state.branding?.domain ?? "yourdomain.com";
|
|
296
|
+
const appName = ctx.state.branding?.appName ?? "App";
|
|
297
|
+
ctx.envVars.RESEND_FROM_EMAIL = ctx.envVars.RESEND_FROM_EMAIL ?? `${appName} <noreply@${domain}>`;
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
ctx.envVars.RESEND_API_KEY = ctx.envVars.RESEND_API_KEY ?? DUMMY_VALUES.RESEND_API_KEY;
|
|
301
|
+
if (ctx.envVars.RESEND_API_KEY === DUMMY_VALUES.RESEND_API_KEY) ctx.placeholders.push("RESEND_API_KEY");
|
|
302
|
+
}
|
|
303
|
+
async function setupStorageCredentials(ctx) {
|
|
304
|
+
const accessKey = await text({
|
|
305
|
+
message: "S3_ACCESS_KEY_ID:",
|
|
306
|
+
initialValue: ctx.envVars.S3_ACCESS_KEY_ID
|
|
307
|
+
});
|
|
308
|
+
if (isCancel(accessKey)) process.exit(0);
|
|
309
|
+
ctx.envVars.S3_ACCESS_KEY_ID = accessKey;
|
|
310
|
+
const secretKey = await text({
|
|
311
|
+
message: "S3_SECRET_ACCESS_KEY:",
|
|
312
|
+
initialValue: ctx.envVars.S3_SECRET_ACCESS_KEY
|
|
313
|
+
});
|
|
314
|
+
if (isCancel(secretKey)) process.exit(0);
|
|
315
|
+
ctx.envVars.S3_SECRET_ACCESS_KEY = secretKey;
|
|
316
|
+
const bucket = await text({
|
|
317
|
+
message: "S3_BUCKET:",
|
|
318
|
+
initialValue: ctx.envVars.S3_BUCKET ?? "app-assets"
|
|
319
|
+
});
|
|
320
|
+
if (isCancel(bucket)) process.exit(0);
|
|
321
|
+
ctx.envVars.S3_BUCKET = bucket;
|
|
322
|
+
const endpoint = await text({
|
|
323
|
+
message: "S3_ENDPOINT (optional, Enter to skip):",
|
|
324
|
+
initialValue: ctx.envVars.S3_ENDPOINT
|
|
325
|
+
});
|
|
326
|
+
if (isCancel(endpoint)) process.exit(0);
|
|
327
|
+
if (endpoint) ctx.envVars.S3_ENDPOINT = endpoint;
|
|
328
|
+
ctx.envVars.S3_REGION = ctx.envVars.S3_REGION ?? "us-east-1";
|
|
329
|
+
ctx.envVars.STORAGE_PROVIDER = ctx.envVars.STORAGE_PROVIDER ?? "s3";
|
|
330
|
+
}
|
|
331
|
+
function setSeaweedfsDefaults(ctx) {
|
|
332
|
+
ctx.envVars.STORAGE_PROVIDER = "seaweedfs";
|
|
333
|
+
ctx.envVars.S3_ENDPOINT = "http://localhost:8333";
|
|
334
|
+
ctx.envVars.S3_ACCESS_KEY_ID = "minioadmin";
|
|
335
|
+
ctx.envVars.S3_SECRET_ACCESS_KEY = "minioadmin";
|
|
336
|
+
ctx.envVars.S3_BUCKET = "app-assets";
|
|
337
|
+
ctx.envVars.S3_REGION = "us-east-1";
|
|
338
|
+
if (ctx.state.infra) ctx.state.infra.seaweedfs = true;
|
|
339
|
+
else ctx.state.infra = {
|
|
340
|
+
seaweedfs: true,
|
|
341
|
+
redis: false
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function setStorageDummies(ctx) {
|
|
345
|
+
ctx.envVars.STORAGE_PROVIDER = ctx.envVars.STORAGE_PROVIDER ?? "s3";
|
|
346
|
+
ctx.envVars.S3_ACCESS_KEY_ID = DUMMY_VALUES.S3_ACCESS_KEY_ID;
|
|
347
|
+
ctx.envVars.S3_SECRET_ACCESS_KEY = DUMMY_VALUES.S3_SECRET_ACCESS_KEY;
|
|
348
|
+
ctx.envVars.S3_BUCKET = DUMMY_VALUES.S3_BUCKET;
|
|
349
|
+
ctx.envVars.S3_REGION = "us-east-1";
|
|
350
|
+
ctx.placeholders.push("S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY", "S3_BUCKET");
|
|
351
|
+
log.warn("Using placeholders — file uploads will fail until configured");
|
|
352
|
+
}
|
|
353
|
+
async function setupStorage(ctx, enabled) {
|
|
354
|
+
if (!enabled) {
|
|
355
|
+
ctx.envVars.STORAGE_PROVIDER = ctx.envVars.STORAGE_PROVIDER ?? "s3";
|
|
356
|
+
ctx.envVars.S3_ACCESS_KEY_ID = ctx.envVars.S3_ACCESS_KEY_ID ?? DUMMY_VALUES.S3_ACCESS_KEY_ID;
|
|
357
|
+
ctx.envVars.S3_SECRET_ACCESS_KEY = ctx.envVars.S3_SECRET_ACCESS_KEY ?? DUMMY_VALUES.S3_SECRET_ACCESS_KEY;
|
|
358
|
+
ctx.envVars.S3_BUCKET = ctx.envVars.S3_BUCKET ?? DUMMY_VALUES.S3_BUCKET;
|
|
359
|
+
ctx.envVars.S3_REGION = ctx.envVars.S3_REGION ?? "us-east-1";
|
|
360
|
+
if (ctx.envVars.S3_ACCESS_KEY_ID === DUMMY_VALUES.S3_ACCESS_KEY_ID) ctx.placeholders.push("S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY", "S3_BUCKET");
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const storageChoice = await select({
|
|
364
|
+
message: "Do you have S3-compatible storage credentials?",
|
|
365
|
+
options: [
|
|
366
|
+
{
|
|
367
|
+
value: "dummy",
|
|
368
|
+
label: "No, use placeholder values",
|
|
369
|
+
hint: "storage calls will fail gracefully"
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
value: "credentials",
|
|
373
|
+
label: "Yes, I have credentials (S3, R2, Minio, etc.)"
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
value: "seaweedfs",
|
|
377
|
+
label: "Start SeaweedFS via Docker (local S3)"
|
|
378
|
+
}
|
|
379
|
+
]
|
|
380
|
+
});
|
|
381
|
+
if (isCancel(storageChoice)) process.exit(0);
|
|
382
|
+
if (storageChoice === "credentials") await setupStorageCredentials(ctx);
|
|
383
|
+
else if (storageChoice === "seaweedfs") setSeaweedfsDefaults(ctx);
|
|
384
|
+
else setStorageDummies(ctx);
|
|
385
|
+
}
|
|
386
|
+
async function setupStripe(ctx) {
|
|
387
|
+
const stripeSecret = await text({
|
|
388
|
+
message: "STRIPE_SECRET_KEY (Enter to skip):",
|
|
389
|
+
placeholder: "sk_test_xxxxx",
|
|
390
|
+
initialValue: ctx.envVars.STRIPE_SECRET_KEY
|
|
391
|
+
});
|
|
392
|
+
if (isCancel(stripeSecret)) process.exit(0);
|
|
393
|
+
if (!stripeSecret) {
|
|
394
|
+
log.info("Stripe skipped — payments feature will be disabled");
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
ctx.envVars.STRIPE_SECRET_KEY = stripeSecret;
|
|
398
|
+
const webhookSecret = await text({
|
|
399
|
+
message: "STRIPE_WEBHOOK_SECRET:",
|
|
400
|
+
placeholder: "whsec_xxxxx",
|
|
401
|
+
initialValue: ctx.envVars.STRIPE_WEBHOOK_SECRET
|
|
402
|
+
});
|
|
403
|
+
if (isCancel(webhookSecret)) process.exit(0);
|
|
404
|
+
if (webhookSecret) ctx.envVars.STRIPE_WEBHOOK_SECRET = webhookSecret;
|
|
405
|
+
const publishableKey = await text({
|
|
406
|
+
message: "STRIPE_PUBLISHABLE_KEY (for client):",
|
|
407
|
+
placeholder: "pk_test_xxxxx",
|
|
408
|
+
initialValue: ctx.envVars.STRIPE_PUBLISHABLE_KEY
|
|
409
|
+
});
|
|
410
|
+
if (isCancel(publishableKey)) process.exit(0);
|
|
411
|
+
if (publishableKey) {
|
|
412
|
+
ctx.envVars.STRIPE_PUBLISHABLE_KEY = publishableKey;
|
|
413
|
+
ctx.envVars.VITE_STRIPE_ENABLED = "true";
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
async function setupAI(ctx) {
|
|
417
|
+
const openai = await text({
|
|
418
|
+
message: "OpenAI API key (optional, Enter to skip):",
|
|
419
|
+
placeholder: "sk-proj-xxxxx",
|
|
420
|
+
initialValue: ctx.envVars.OPENAI_API_KEY
|
|
421
|
+
});
|
|
422
|
+
if (isCancel(openai)) process.exit(0);
|
|
423
|
+
if (openai) ctx.envVars.OPENAI_API_KEY = openai;
|
|
424
|
+
const anthropic = await text({
|
|
425
|
+
message: "Anthropic API key (optional, Enter to skip):",
|
|
426
|
+
initialValue: ctx.envVars.ANTHROPIC_API_KEY
|
|
427
|
+
});
|
|
428
|
+
if (isCancel(anthropic)) process.exit(0);
|
|
429
|
+
if (anthropic) ctx.envVars.ANTHROPIC_API_KEY = anthropic;
|
|
430
|
+
}
|
|
431
|
+
async function setupRedis(ctx) {
|
|
432
|
+
const redisChoice = await select({
|
|
433
|
+
message: "Do you have an external Redis?",
|
|
434
|
+
options: [
|
|
435
|
+
{
|
|
436
|
+
value: "skip",
|
|
437
|
+
label: "No, skip",
|
|
438
|
+
hint: "chat won't have resumable streams"
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
value: "url",
|
|
442
|
+
label: "Yes, I have a Redis URL"
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
value: "docker",
|
|
446
|
+
label: "Start Redis via Docker"
|
|
447
|
+
}
|
|
448
|
+
]
|
|
449
|
+
});
|
|
450
|
+
if (isCancel(redisChoice)) process.exit(0);
|
|
451
|
+
if (redisChoice === "url") {
|
|
452
|
+
const redisUrl = await text({
|
|
453
|
+
message: "REDIS_URL:",
|
|
454
|
+
placeholder: "redis://localhost:6379",
|
|
455
|
+
initialValue: ctx.envVars.REDIS_URL
|
|
456
|
+
});
|
|
457
|
+
if (isCancel(redisUrl)) process.exit(0);
|
|
458
|
+
if (redisUrl) ctx.envVars.REDIS_URL = redisUrl;
|
|
459
|
+
} else if (redisChoice === "docker") {
|
|
460
|
+
ctx.envVars.REDIS_URL = "redis://localhost:6379";
|
|
461
|
+
if (ctx.state.infra) ctx.state.infra.redis = true;
|
|
462
|
+
else ctx.state.infra = {
|
|
463
|
+
seaweedfs: false,
|
|
464
|
+
redis: true
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async function runEnv(state) {
|
|
469
|
+
const features = state.features ?? {
|
|
470
|
+
stripe: false,
|
|
471
|
+
ai: false,
|
|
472
|
+
storage: false,
|
|
473
|
+
redis: false,
|
|
474
|
+
email: false
|
|
475
|
+
};
|
|
476
|
+
const ctx = {
|
|
477
|
+
envVars: { ...readEnvFile(".env") },
|
|
478
|
+
placeholders: [],
|
|
479
|
+
state
|
|
480
|
+
};
|
|
481
|
+
setupCoreVars(ctx);
|
|
482
|
+
await setupEmail(ctx, features.email);
|
|
483
|
+
await setupStorage(ctx, features.storage);
|
|
484
|
+
if (features.stripe) await setupStripe(ctx);
|
|
485
|
+
if (features.ai) await setupAI(ctx);
|
|
486
|
+
if (features.redis) await setupRedis(ctx);
|
|
487
|
+
const s = spinner();
|
|
488
|
+
s.start("Writing .env file...");
|
|
489
|
+
writeEnvFile(".env", ctx.envVars);
|
|
490
|
+
s.stop(`.env written (${Object.keys(ctx.envVars).length} variables)`);
|
|
491
|
+
if (ctx.placeholders.length > 0) {
|
|
492
|
+
log.warn(`${ctx.placeholders.length} placeholders need real values later:`);
|
|
493
|
+
for (const p of ctx.placeholders) log.warn(` \u2022 ${p}`);
|
|
494
|
+
}
|
|
495
|
+
state.env = {
|
|
496
|
+
placeholders: ctx.placeholders,
|
|
497
|
+
written: true
|
|
498
|
+
};
|
|
499
|
+
markPhaseCompleted(state, "env");
|
|
500
|
+
await saveState(state);
|
|
501
|
+
return state;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
//#endregion
|
|
505
|
+
//#region src/phases/features.ts
|
|
506
|
+
const FEATURE_OPTIONS = [
|
|
507
|
+
{
|
|
508
|
+
value: "stripe",
|
|
509
|
+
label: "Stripe",
|
|
510
|
+
hint: "payments & subscriptions"
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
value: "ai",
|
|
514
|
+
label: "AI Chat",
|
|
515
|
+
hint: "OpenAI / Anthropic / Gemini"
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
value: "storage",
|
|
519
|
+
label: "Storage",
|
|
520
|
+
hint: "S3 / SeaweedFS / R2 / Minio"
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
value: "redis",
|
|
524
|
+
label: "Redis",
|
|
525
|
+
hint: "resumable chat streams"
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
value: "email",
|
|
529
|
+
label: "Email",
|
|
530
|
+
hint: "Resend — transactional emails"
|
|
531
|
+
}
|
|
532
|
+
];
|
|
533
|
+
async function runFeatures(state) {
|
|
534
|
+
const selected = await multiselect({
|
|
535
|
+
message: "Which features do you want to enable?",
|
|
536
|
+
options: FEATURE_OPTIONS.map((f) => ({
|
|
537
|
+
value: f.value,
|
|
538
|
+
label: `${f.label} (${f.hint})`
|
|
539
|
+
})),
|
|
540
|
+
initialValues: state.features ? Object.entries(state.features).filter(([, v]) => v).map(([k]) => k) : [],
|
|
541
|
+
required: false
|
|
542
|
+
});
|
|
543
|
+
if (isCancel(selected)) process.exit(0);
|
|
544
|
+
const features = {
|
|
545
|
+
stripe: selected.includes("stripe"),
|
|
546
|
+
ai: selected.includes("ai"),
|
|
547
|
+
storage: selected.includes("storage"),
|
|
548
|
+
redis: selected.includes("redis"),
|
|
549
|
+
email: selected.includes("email")
|
|
550
|
+
};
|
|
551
|
+
const enabled = Object.entries(features).filter(([, v]) => v).map(([k]) => k);
|
|
552
|
+
if (enabled.length > 0) log.success(`Enabled: ${enabled.join(", ")}`);
|
|
553
|
+
else log.info("No optional features selected — core app only");
|
|
554
|
+
state.features = features;
|
|
555
|
+
markPhaseCompleted(state, "features");
|
|
556
|
+
await saveState(state);
|
|
557
|
+
return state;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/phases/infra.ts
|
|
562
|
+
async function startDockerService(name, service, successMsg) {
|
|
563
|
+
const s = spinner();
|
|
564
|
+
s.start(`Starting ${name}...`);
|
|
565
|
+
const result = await exec(`docker compose up -d ${service}`);
|
|
566
|
+
if (result.exitCode !== 0) {
|
|
567
|
+
s.stop(`Failed to start ${name}`);
|
|
568
|
+
log.error(result.stderr);
|
|
569
|
+
log.info(`Make sure Docker is running, then try: docker compose up -d ${service}`);
|
|
570
|
+
} else s.stop(successMsg);
|
|
571
|
+
}
|
|
572
|
+
async function checkDatabase(envVars) {
|
|
573
|
+
if (!envVars.DATABASE_URL) return;
|
|
574
|
+
const db = await testDbConnection(envVars.DATABASE_URL);
|
|
575
|
+
return db.ok ? "Database — connected" : `Database — failed: ${db.error}`;
|
|
576
|
+
}
|
|
577
|
+
async function checkRedis(envVars) {
|
|
578
|
+
if (!envVars.REDIS_URL) return;
|
|
579
|
+
try {
|
|
580
|
+
const { RedisClient } = await import("bun");
|
|
581
|
+
const redis = new RedisClient(envVars.REDIS_URL);
|
|
582
|
+
await redis.connect();
|
|
583
|
+
await redis.disconnect();
|
|
584
|
+
return "Redis — connected";
|
|
585
|
+
} catch {
|
|
586
|
+
return "Redis — not reachable (will use fallback)";
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
async function runInfra(state) {
|
|
590
|
+
const needsSeaweedfs = state.infra?.seaweedfs ?? false;
|
|
591
|
+
const needsRedis = state.infra?.redis ?? false;
|
|
592
|
+
if (needsSeaweedfs || needsRedis) {
|
|
593
|
+
if (needsSeaweedfs) await startDockerService("SeaweedFS (local S3)", "seaweedfs", "SeaweedFS running on localhost:8333");
|
|
594
|
+
if (needsRedis) await startDockerService("Redis", "redis", "Redis running on localhost:6379");
|
|
595
|
+
} else {
|
|
596
|
+
log.info("No Docker services needed!");
|
|
597
|
+
log.info("Your setup uses external services or placeholders.");
|
|
598
|
+
}
|
|
599
|
+
const checks = spinner();
|
|
600
|
+
checks.start("Running final checks...");
|
|
601
|
+
const envVars = readEnvFile(".env");
|
|
602
|
+
const results = (await Promise.all([checkDatabase(envVars), checkRedis(envVars)])).filter((r) => r !== void 0);
|
|
603
|
+
checks.stop("Health checks complete");
|
|
604
|
+
for (const r of results) if (r.includes("failed") || r.includes("not reachable")) log.warn(r);
|
|
605
|
+
else log.success(r);
|
|
606
|
+
markPhaseCompleted(state, "infra");
|
|
607
|
+
await saveState(state);
|
|
608
|
+
return state;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
//#endregion
|
|
612
|
+
//#region src/phases/scaffold.ts
|
|
613
|
+
const TEMPLATE_URI = "gh:CarlosZiegler/start-kit.dev/apps/start-template#main";
|
|
614
|
+
async function fetchTemplate(targetDir) {
|
|
615
|
+
const s = spinner();
|
|
616
|
+
s.start("Downloading template...");
|
|
617
|
+
try {
|
|
618
|
+
await downloadTemplate(TEMPLATE_URI, {
|
|
619
|
+
dir: targetDir,
|
|
620
|
+
force: false
|
|
621
|
+
});
|
|
622
|
+
s.stop("Template downloaded");
|
|
623
|
+
} catch (error) {
|
|
624
|
+
s.stop("Download failed");
|
|
625
|
+
log.error(String(error));
|
|
626
|
+
process.exit(1);
|
|
627
|
+
}
|
|
628
|
+
await exec(`git -C "${targetDir}" init`);
|
|
629
|
+
}
|
|
630
|
+
async function installDeps(targetDir) {
|
|
631
|
+
const s = spinner();
|
|
632
|
+
s.start("Installing dependencies...");
|
|
633
|
+
const result = await exec(`cd "${targetDir}" && bun install`);
|
|
634
|
+
if (result.exitCode !== 0) {
|
|
635
|
+
s.stop("Install failed");
|
|
636
|
+
log.error(result.stderr);
|
|
637
|
+
log.info(`Try manually: cd ${targetDir} && bun install`);
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
s.stop("Dependencies installed");
|
|
641
|
+
}
|
|
642
|
+
async function runScaffold(projectNameArg) {
|
|
643
|
+
let projectName = projectNameArg;
|
|
644
|
+
if (!projectName) {
|
|
645
|
+
const name = await text({
|
|
646
|
+
message: "What's your project name?",
|
|
647
|
+
placeholder: "my-saas-app",
|
|
648
|
+
validate: (v = "") => {
|
|
649
|
+
if (!isValidAppName(v)) return "Name must be 2-50 characters";
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
if (isCancel(name)) process.exit(0);
|
|
653
|
+
projectName = name;
|
|
654
|
+
}
|
|
655
|
+
const dirName = toKebabCase(projectName);
|
|
656
|
+
const targetDir = `${process.cwd()}/${dirName}`;
|
|
657
|
+
if (existsSync(targetDir)) {
|
|
658
|
+
log.error(`Directory "${dirName}" already exists.`);
|
|
659
|
+
process.exit(1);
|
|
660
|
+
}
|
|
661
|
+
log.info(`Creating project in ./${dirName}`);
|
|
662
|
+
await fetchTemplate(targetDir);
|
|
663
|
+
await installDeps(targetDir);
|
|
664
|
+
await exec(`rm -f "${targetDir}/.setup-state.json"`);
|
|
665
|
+
log.success(`Project created in ./${dirName}`);
|
|
666
|
+
return targetDir;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
//#endregion
|
|
670
|
+
//#region src/index.ts
|
|
671
|
+
const PHASES = [
|
|
672
|
+
{
|
|
673
|
+
key: "branding",
|
|
674
|
+
label: "Branding",
|
|
675
|
+
run: runBranding
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
key: "features",
|
|
679
|
+
label: "Features",
|
|
680
|
+
run: runFeatures
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
key: "database",
|
|
684
|
+
label: "Database",
|
|
685
|
+
run: runDatabase
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
key: "env",
|
|
689
|
+
label: "Environment",
|
|
690
|
+
run: runEnv
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
key: "infra",
|
|
694
|
+
label: "Infrastructure",
|
|
695
|
+
run: runInfra
|
|
696
|
+
}
|
|
697
|
+
];
|
|
698
|
+
function showPhaseStatus(state) {
|
|
699
|
+
for (const phase of PHASES) {
|
|
700
|
+
const done = isPhaseCompleted(state, phase.key);
|
|
701
|
+
const icon = done ? "✓" : "○";
|
|
702
|
+
log.info(`${icon} ${phase.label}${done ? " (completed)" : ""}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
async function getResumeStartIndex(state) {
|
|
706
|
+
const firstIncomplete = PHASES.findIndex((p) => !isPhaseCompleted(state, p.key));
|
|
707
|
+
const options = [];
|
|
708
|
+
if (firstIncomplete >= 0) options.push({
|
|
709
|
+
value: "continue",
|
|
710
|
+
label: `Continue from ${PHASES[firstIncomplete].label}`
|
|
711
|
+
});
|
|
712
|
+
options.push({
|
|
713
|
+
value: "start-over",
|
|
714
|
+
label: "Start over"
|
|
715
|
+
}, {
|
|
716
|
+
value: "jump",
|
|
717
|
+
label: "Jump to a specific phase"
|
|
718
|
+
});
|
|
719
|
+
const choice = await select({
|
|
720
|
+
message: "What would you like to do?",
|
|
721
|
+
options
|
|
722
|
+
});
|
|
723
|
+
if (isCancel(choice)) process.exit(0);
|
|
724
|
+
if (choice === "continue") return firstIncomplete;
|
|
725
|
+
if (choice === "start-over") return -1;
|
|
726
|
+
return await selectPhase(state);
|
|
727
|
+
}
|
|
728
|
+
async function selectPhase(state) {
|
|
729
|
+
const jumpTo = await select({
|
|
730
|
+
message: "Which phase?",
|
|
731
|
+
options: PHASES.map((p, i) => ({
|
|
732
|
+
value: i,
|
|
733
|
+
label: p.label,
|
|
734
|
+
hint: isPhaseCompleted(state, p.key) ? "completed" : void 0
|
|
735
|
+
}))
|
|
736
|
+
});
|
|
737
|
+
if (isCancel(jumpTo)) process.exit(0);
|
|
738
|
+
return jumpTo;
|
|
739
|
+
}
|
|
740
|
+
async function runSinglePhase(targetStep, state) {
|
|
741
|
+
const phase = PHASES.find((p) => p.key === targetStep);
|
|
742
|
+
if (!phase) {
|
|
743
|
+
log.error(`Unknown phase: ${targetStep}`);
|
|
744
|
+
log.info(`Available: ${PHASES.map((p) => p.key).join(", ")}`);
|
|
745
|
+
process.exit(1);
|
|
746
|
+
}
|
|
747
|
+
await phase.run(state);
|
|
748
|
+
outro(`Phase "${phase.label}" complete!`);
|
|
749
|
+
}
|
|
750
|
+
async function runWizard(state) {
|
|
751
|
+
showPhaseStatus(state);
|
|
752
|
+
let startFrom = 0;
|
|
753
|
+
let currentState = state;
|
|
754
|
+
if (state.completedPhases.length > 0) {
|
|
755
|
+
const resumeIndex = await getResumeStartIndex(state);
|
|
756
|
+
if (resumeIndex === -1) currentState = {
|
|
757
|
+
version: 1,
|
|
758
|
+
completedPhases: []
|
|
759
|
+
};
|
|
760
|
+
else startFrom = resumeIndex;
|
|
761
|
+
}
|
|
762
|
+
for (let i = startFrom; i < PHASES.length; i++) {
|
|
763
|
+
const phase = PHASES[i];
|
|
764
|
+
log.step(`Phase ${i + 1}/${PHASES.length}: ${phase.label}`);
|
|
765
|
+
currentState = await phase.run(currentState);
|
|
766
|
+
}
|
|
767
|
+
outro("Setup complete! Run `bun dev` to start your app.");
|
|
768
|
+
}
|
|
769
|
+
function showUsage() {
|
|
770
|
+
console.log("Usage:");
|
|
771
|
+
console.log(" bunx create-start-kit-dev create [project-name] Create a new project");
|
|
772
|
+
console.log(" bunx create-start-kit-dev init [--step <phase>] Setup existing project");
|
|
773
|
+
console.log("");
|
|
774
|
+
console.log("Phases: branding, features, database, env, infra");
|
|
775
|
+
}
|
|
776
|
+
async function handleCreate(args) {
|
|
777
|
+
const projectName = args.at(1);
|
|
778
|
+
intro("Start Kit — Create New Project");
|
|
779
|
+
const targetDir = await runScaffold(projectName);
|
|
780
|
+
process.chdir(targetDir);
|
|
781
|
+
log.step("Now let's configure your project...");
|
|
782
|
+
await runWizard(loadState());
|
|
783
|
+
}
|
|
784
|
+
async function handleInit(args) {
|
|
785
|
+
const stepIndex = args.indexOf("--step");
|
|
786
|
+
const targetStep = stepIndex >= 0 ? args.at(stepIndex + 1) : void 0;
|
|
787
|
+
intro("Start Kit Setup");
|
|
788
|
+
const state = loadState();
|
|
789
|
+
if (targetStep) await runSinglePhase(targetStep, state);
|
|
790
|
+
else await runWizard(state);
|
|
791
|
+
}
|
|
792
|
+
async function main() {
|
|
793
|
+
const args = process.argv.slice(2);
|
|
794
|
+
const command = args.at(0);
|
|
795
|
+
if (command === "create") {
|
|
796
|
+
await handleCreate(args);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
if (command === "init") {
|
|
800
|
+
await handleInit(args);
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
showUsage();
|
|
804
|
+
}
|
|
805
|
+
main().catch((error) => {
|
|
806
|
+
console.error("Setup failed:", error);
|
|
807
|
+
process.exit(1);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
//#endregion
|
|
811
|
+
export { };
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-start-kit-dev",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for scaffolding and configuring Start Kit projects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Carlos Ziegler",
|
|
9
|
+
"url": "https://github.com/CarlosZiegler"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/CarlosZiegler/start-kit.dev",
|
|
14
|
+
"directory": "packages/cli"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/CarlosZiegler/start-kit.dev#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/CarlosZiegler/start-kit.dev/issues"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"cli",
|
|
22
|
+
"starter",
|
|
23
|
+
"template",
|
|
24
|
+
"tanstack",
|
|
25
|
+
"tanstack-start",
|
|
26
|
+
"bun",
|
|
27
|
+
"typescript",
|
|
28
|
+
"scaffold",
|
|
29
|
+
"create"
|
|
30
|
+
],
|
|
31
|
+
"bin": {
|
|
32
|
+
"create-start-kit-dev": "./dist/index.mjs"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsdown",
|
|
39
|
+
"dev": "tsdown --watch"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@clack/prompts": "^1.0.1",
|
|
43
|
+
"giget": "^3.1.2",
|
|
44
|
+
"picocolors": "^1.1.1"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/bun": "^1.3.9",
|
|
48
|
+
"tsdown": "^0.20.3",
|
|
49
|
+
"typescript": "^5.9.3"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"bun": ">=1.0.0"
|
|
53
|
+
}
|
|
54
|
+
}
|