scaffoldry 1.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.
@@ -0,0 +1,1952 @@
1
+ import {
2
+ checkEnvVar,
3
+ checkNodeVersion,
4
+ checkPnpmVersion,
5
+ checkStripeCli,
6
+ clearLicense,
7
+ createTemplateContext,
8
+ getStoredLicense,
9
+ isValidLicenseKeyFormat,
10
+ logger,
11
+ printBox,
12
+ printError,
13
+ printInfo,
14
+ printSuccess,
15
+ printWarning,
16
+ readEnvLocal,
17
+ runConfigurationChecks,
18
+ runEnvironmentChecks,
19
+ runServiceChecks,
20
+ saveProjectData,
21
+ storeLicense,
22
+ toKebabCase,
23
+ toPascalCase,
24
+ validateLicense,
25
+ writeEnvLocal
26
+ } from "./chunk-XIP7YNKZ.js";
27
+ import {
28
+ copyToClipboard,
29
+ getKillSignal,
30
+ getPlatformDisplayName,
31
+ getSpawnOptions,
32
+ getTermSignal,
33
+ setupShutdownHandlers
34
+ } from "./chunk-WOS3F5LR.js";
35
+
36
+ // src/templates/index.ts
37
+ function generateProjectFiles(context) {
38
+ const files = [];
39
+ files.push(generatePackageJson(context));
40
+ files.push(generateTsConfig(context));
41
+ files.push(generateEnvExample(context));
42
+ files.push(generateEnvLocal(context));
43
+ files.push(generateMainEntry(context));
44
+ files.push(generatePlatformConfig(context));
45
+ files.push(generateGitignore());
46
+ return files;
47
+ }
48
+ function generatePackageJson(context) {
49
+ const kebabName = toKebabCase(context.projectName);
50
+ const dependencies = {
51
+ "scaffoldry-platform": "^1.0.0",
52
+ "scaffoldry-db": "^1.0.0",
53
+ "scaffoldry-core": "^1.0.0",
54
+ "dotenv": "^16.5.0"
55
+ };
56
+ if (context.hasAuth) {
57
+ dependencies["scaffoldry-auth"] = "^1.0.0";
58
+ }
59
+ if (context.hasBilling) {
60
+ dependencies["scaffoldry-billing"] = "^1.0.0";
61
+ }
62
+ if (context.hasEmail) {
63
+ dependencies["scaffoldry-notify"] = "^1.0.0";
64
+ }
65
+ if (context.hasStorage) {
66
+ dependencies["scaffoldry-storage"] = "^1.0.0";
67
+ }
68
+ if (context.hasJobs) {
69
+ dependencies["scaffoldry-jobs"] = "^1.0.0";
70
+ }
71
+ if (context.hasWebhooks) {
72
+ dependencies["scaffoldry-webhooks"] = "^1.0.0";
73
+ }
74
+ const pkg = {
75
+ name: kebabName,
76
+ version: "0.1.0",
77
+ description: context.projectDescription,
78
+ type: "module",
79
+ scripts: {
80
+ dev: "tsx watch src/index.ts",
81
+ build: "tsc",
82
+ start: "node dist/index.js",
83
+ typecheck: "tsc --noEmit"
84
+ },
85
+ dependencies,
86
+ devDependencies: {
87
+ "@types/node": "^20.17.57",
88
+ tsx: "^4.20.3",
89
+ typescript: "^5.8.3"
90
+ }
91
+ };
92
+ return {
93
+ path: "package.json",
94
+ content: JSON.stringify(pkg, null, 2) + "\n"
95
+ };
96
+ }
97
+ function generateTsConfig(_context) {
98
+ const config = {
99
+ compilerOptions: {
100
+ target: "ES2022",
101
+ module: "ESNext",
102
+ moduleResolution: "bundler",
103
+ esModuleInterop: true,
104
+ strict: true,
105
+ skipLibCheck: true,
106
+ outDir: "./dist",
107
+ rootDir: "./src",
108
+ declaration: true
109
+ },
110
+ include: ["src/**/*"],
111
+ exclude: ["node_modules", "dist"]
112
+ };
113
+ return {
114
+ path: "tsconfig.json",
115
+ content: JSON.stringify(config, null, 2) + "\n"
116
+ };
117
+ }
118
+ function generateEnvExample(context) {
119
+ const lines = [
120
+ "# Database",
121
+ "DATABASE_URL=postgresql://localhost:5432/my_app",
122
+ ""
123
+ ];
124
+ if (context.hasBilling) {
125
+ lines.push(
126
+ "# Stripe",
127
+ "STRIPE_SECRET_KEY=sk_test_...",
128
+ "STRIPE_WEBHOOK_SECRET=whsec_...",
129
+ "STRIPE_PRICE_ID_STARTER=price_...",
130
+ "STRIPE_PRICE_ID_PRO=price_...",
131
+ ""
132
+ );
133
+ }
134
+ if (context.hasEmail) {
135
+ lines.push(
136
+ "# Resend",
137
+ "RESEND_API_KEY=re_...",
138
+ "RESEND_FROM_EMAIL=noreply@example.com",
139
+ ""
140
+ );
141
+ }
142
+ if (context.hasStorage) {
143
+ lines.push(
144
+ "# S3",
145
+ "S3_BUCKET=my-bucket",
146
+ "S3_REGION=us-east-1",
147
+ "S3_ACCESS_KEY_ID=AKIA...",
148
+ "S3_SECRET_ACCESS_KEY=...",
149
+ ""
150
+ );
151
+ }
152
+ return {
153
+ path: ".env.example",
154
+ content: lines.join("\n")
155
+ };
156
+ }
157
+ function generateEnvLocal(_context) {
158
+ return {
159
+ path: ".env.local",
160
+ content: "# Local environment overrides\n"
161
+ };
162
+ }
163
+ function generateMainEntry(context) {
164
+ const imports = ['import "dotenv/config";', 'import { createPlatform } from "./platform.js";'];
165
+ const content = `${imports.join("\n")}
166
+
167
+ async function main() {
168
+ const platform = createPlatform();
169
+
170
+ console.log("Platform initialized successfully!");
171
+ console.log("Available services:");
172
+ console.log(" - Database:", !!platform.db);
173
+ console.log(" - Logger:", !!platform.logger);
174
+ console.log(" - Audit:", !!platform.audit);
175
+ ${context.hasAuth ? ' console.log(" - Auth:", !!platform.auth);' : ""}
176
+ ${context.hasBilling ? ' console.log(" - Billing:", !!platform.billing);' : ""}
177
+ ${context.hasEmail ? ' console.log(" - Email:", !!platform.email);' : ""}
178
+ ${context.hasStorage ? ' console.log(" - Storage:", !!platform.storage);' : ""}
179
+ ${context.hasJobs ? ' console.log(" - Jobs:", !!platform.jobs);' : ""}
180
+ ${context.hasWebhooks ? ' console.log(" - Webhook Inbox:", !!platform.webhookInbox);' : ""}
181
+ }
182
+
183
+ main().catch(console.error);
184
+ `;
185
+ return {
186
+ path: "src/index.ts",
187
+ content
188
+ };
189
+ }
190
+ function generatePlatformConfig(context) {
191
+ const content = `import { createPlatform as createScaffoldryPlatform } from "@scaffoldry/platform";
192
+
193
+ export function createPlatform() {
194
+ return createScaffoldryPlatform({
195
+ database: {
196
+ url: process.env.DATABASE_URL!,
197
+ },
198
+ ${context.hasBilling ? ` stripe: {
199
+ secretKey: process.env.STRIPE_SECRET_KEY!,
200
+ webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
201
+ priceIdStarter: process.env.STRIPE_PRICE_ID_STARTER!,
202
+ priceIdPro: process.env.STRIPE_PRICE_ID_PRO!,
203
+ },` : ""}
204
+ ${context.hasEmail ? ` resend: {
205
+ apiKey: process.env.RESEND_API_KEY!,
206
+ fromEmail: process.env.RESEND_FROM_EMAIL!,
207
+ },` : ""}
208
+ ${context.hasStorage ? ` s3: {
209
+ bucket: process.env.S3_BUCKET!,
210
+ region: process.env.S3_REGION!,
211
+ accessKeyId: process.env.S3_ACCESS_KEY_ID!,
212
+ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
213
+ },` : ""}
214
+ });
215
+ }
216
+
217
+ export type Platform = ReturnType<typeof createPlatform>;
218
+ `;
219
+ return {
220
+ path: "src/platform.ts",
221
+ content
222
+ };
223
+ }
224
+ function generateGitignore() {
225
+ return {
226
+ path: ".gitignore",
227
+ content: `# Dependencies
228
+ node_modules/
229
+
230
+ # Build output
231
+ dist/
232
+
233
+ # Environment files
234
+ .env
235
+ .env.local
236
+ .env.*.local
237
+
238
+ # IDE
239
+ .vscode/
240
+ .idea/
241
+
242
+ # OS
243
+ .DS_Store
244
+ Thumbs.db
245
+
246
+ # Logs
247
+ *.log
248
+ npm-debug.log*
249
+
250
+ # Test coverage
251
+ coverage/
252
+ `
253
+ };
254
+ }
255
+
256
+ // src/commands/init.ts
257
+ import path from "path";
258
+ import fs from "fs-extra";
259
+ import prompts from "prompts";
260
+ import ora from "ora";
261
+ async function loadConfigFile(configPath) {
262
+ const absolutePath = path.resolve(process.cwd(), configPath);
263
+ if (!await fs.pathExists(absolutePath)) {
264
+ throw new Error(`Config file not found: ${absolutePath}`);
265
+ }
266
+ const content = await fs.readFile(absolutePath, "utf-8");
267
+ const config = JSON.parse(content);
268
+ if (!config.name || typeof config.name !== "string") {
269
+ throw new Error("Config file must contain a 'name' field");
270
+ }
271
+ return config;
272
+ }
273
+ async function initCommand(targetDir, options = {}) {
274
+ const { configFile, skipPrompts } = options;
275
+ let fileConfig = null;
276
+ if (configFile) {
277
+ try {
278
+ fileConfig = await loadConfigFile(configFile);
279
+ logger.log(`Using config file: ${configFile}`);
280
+ logger.newLine();
281
+ } catch (error) {
282
+ logger.error(error instanceof Error ? error.message : "Failed to load config file");
283
+ return;
284
+ }
285
+ }
286
+ if (!skipPrompts) {
287
+ logger.newLine();
288
+ printBox("Welcome to Scaffoldry!", [
289
+ "Let's build your SaaS in minutes.",
290
+ "",
291
+ "You're getting our production-ready stack:",
292
+ "\u2022 Neon (serverless PostgreSQL) + Drizzle ORM",
293
+ "\u2022 Password + Magic Link authentication",
294
+ "\u2022 Stripe billing",
295
+ "\u2022 Resend transactional email",
296
+ "\u2022 AWS S3 file storage"
297
+ ]);
298
+ logger.newLine();
299
+ }
300
+ const license = await ensureLicense(fileConfig?.licenseKey, skipPrompts);
301
+ if (!license) {
302
+ logger.error("A valid license is required to use Scaffoldry.");
303
+ logger.log("Purchase a license at: https://scaffoldry.com");
304
+ return;
305
+ }
306
+ let config;
307
+ if (fileConfig) {
308
+ config = {
309
+ name: fileConfig.name,
310
+ description: fileConfig.description || "A SaaS application built with Scaffoldry",
311
+ features: ["auth", "billing", "email", "storage", "jobs", "webhooks", "admin"],
312
+ database: "neon",
313
+ packageManager: "pnpm"
314
+ };
315
+ logger.log(`Project name: ${config.name}`);
316
+ logger.log(`Description: ${config.description}`);
317
+ logger.newLine();
318
+ } else {
319
+ config = await promptForConfig();
320
+ }
321
+ if (!config) {
322
+ logger.error("Project setup cancelled.");
323
+ return;
324
+ }
325
+ const projectDir = targetDir ? path.resolve(process.cwd(), targetDir) : path.resolve(process.cwd(), toKebabCase(config.name));
326
+ if (await fs.pathExists(projectDir)) {
327
+ if (skipPrompts) {
328
+ logger.log(`Removing existing directory: ${projectDir}`);
329
+ await fs.remove(projectDir);
330
+ } else {
331
+ const { overwrite } = await prompts({
332
+ type: "confirm",
333
+ name: "overwrite",
334
+ message: `Directory ${projectDir} already exists. Overwrite?`,
335
+ initial: false
336
+ });
337
+ if (!overwrite) {
338
+ logger.error("Project setup cancelled.");
339
+ return;
340
+ }
341
+ await fs.remove(projectDir);
342
+ }
343
+ }
344
+ const spinner = ora("Creating project files...").start();
345
+ try {
346
+ const context = createTemplateContext(config);
347
+ const files = generateProjectFiles(context);
348
+ for (const file of files) {
349
+ const filePath = path.join(projectDir, file.path);
350
+ await fs.ensureDir(path.dirname(filePath));
351
+ await fs.writeFile(filePath, file.content);
352
+ }
353
+ if (fileConfig?.env && Object.keys(fileConfig.env).length > 0) {
354
+ const envPath = path.join(projectDir, ".env.local");
355
+ const envContent = Object.entries(fileConfig.env).map(([key, value]) => `${key}=${value}`).join("\n");
356
+ await fs.writeFile(envPath, envContent + "\n");
357
+ }
358
+ spinner.succeed("Project files created!");
359
+ logger.newLine();
360
+ logger.success(`Project "${config.name}" created successfully!`);
361
+ let shouldRunSetup = false;
362
+ if (skipPrompts) {
363
+ shouldRunSetup = fileConfig?.runSetup ?? false;
364
+ if (!shouldRunSetup) {
365
+ logger.newLine();
366
+ logger.log("To configure your services:");
367
+ logger.log(` cd ${toKebabCase(config.name)}`);
368
+ logger.log(" pnpm install");
369
+ logger.log(" scaffoldry setup all");
370
+ }
371
+ } else {
372
+ logger.newLine();
373
+ printBox("Next: Configure Your Services", [
374
+ "Your project needs API keys for:",
375
+ "\u2022 Neon Database (required)",
376
+ "\u2022 Stripe Billing (required for billing)",
377
+ "\u2022 Resend Email (required for auth emails)",
378
+ "\u2022 AWS S3 Storage (optional)"
379
+ ]);
380
+ logger.newLine();
381
+ const response = await prompts({
382
+ type: "confirm",
383
+ name: "runSetup",
384
+ message: "Run setup wizard now?",
385
+ initial: true
386
+ });
387
+ shouldRunSetup = response.runSetup;
388
+ if (!shouldRunSetup) {
389
+ logger.newLine();
390
+ logger.log("To configure your services later:");
391
+ logger.log(` cd ${toKebabCase(config.name)}`);
392
+ logger.log(" pnpm install");
393
+ logger.log(" scaffoldry setup all");
394
+ logger.newLine();
395
+ logger.log("Or configure individually:");
396
+ logger.log(" scaffoldry setup database");
397
+ logger.log(" scaffoldry setup stripe");
398
+ logger.log(" scaffoldry setup email");
399
+ logger.log(" scaffoldry setup storage");
400
+ }
401
+ }
402
+ if (shouldRunSetup) {
403
+ process.chdir(projectDir);
404
+ const { setupAllCommand: setupAllCommand2 } = await import("./setup-L2PO5OVZ.js");
405
+ await setupAllCommand2();
406
+ }
407
+ logger.newLine();
408
+ } catch (error) {
409
+ spinner.fail("Failed to create project files.");
410
+ throw error;
411
+ }
412
+ }
413
+ async function promptForConfig() {
414
+ const response = await prompts([
415
+ {
416
+ type: "text",
417
+ name: "name",
418
+ message: "Project name:",
419
+ initial: "my-saas-app",
420
+ validate: (value) => value.length > 0 ? true : "Project name is required"
421
+ },
422
+ {
423
+ type: "text",
424
+ name: "description",
425
+ message: "What are you building? (one sentence)",
426
+ initial: "A SaaS application built with Scaffoldry"
427
+ }
428
+ ]);
429
+ if (!response.name) {
430
+ return null;
431
+ }
432
+ return {
433
+ name: response.name,
434
+ description: response.description,
435
+ // All features enabled by default in v1
436
+ features: ["auth", "billing", "email", "storage", "jobs", "webhooks", "admin"],
437
+ database: "neon",
438
+ // Neon is the v1 choice
439
+ packageManager: "pnpm"
440
+ // pnpm is the v1 choice
441
+ };
442
+ }
443
+ async function ensureLicense(providedKey, skipPrompts) {
444
+ if (providedKey) {
445
+ const normalizedKey = providedKey.trim().toUpperCase();
446
+ if (!isValidLicenseKeyFormat(normalizedKey)) {
447
+ logger.error("Invalid license key format in config. Expected: SCAF-XXXX-XXXX-XXXX-XXXX");
448
+ return null;
449
+ }
450
+ const spinner2 = ora("Validating license...").start();
451
+ const validation2 = await validateLicense(normalizedKey);
452
+ if (!validation2.valid) {
453
+ spinner2.fail(validation2.error || "License validation failed");
454
+ return null;
455
+ }
456
+ spinner2.succeed(`License validated for ${validation2.email || "licensed user"}`);
457
+ await storeLicense({
458
+ key: normalizedKey,
459
+ ...validation2.email && { email: validation2.email },
460
+ validatedAt: (/* @__PURE__ */ new Date()).toISOString()
461
+ });
462
+ logger.newLine();
463
+ return { key: normalizedKey, email: validation2.email };
464
+ }
465
+ const stored = await getStoredLicense();
466
+ if (stored) {
467
+ logger.log(`Authenticated as ${stored.email || "licensed user"}`);
468
+ logger.newLine();
469
+ const validation2 = await validateLicense(stored.key);
470
+ if (validation2.valid) {
471
+ return { key: stored.key, email: validation2.email || stored.email };
472
+ }
473
+ logger.warn("Your stored license is no longer valid.");
474
+ logger.newLine();
475
+ }
476
+ if (skipPrompts) {
477
+ logger.error("No valid license found. Provide licenseKey in config file or run 'scaffoldry login' first.");
478
+ return null;
479
+ }
480
+ logger.log("Please enter your Scaffoldry license key.");
481
+ logger.log("Purchase at: https://scaffoldry.com");
482
+ logger.newLine();
483
+ const { licenseKey } = await prompts({
484
+ type: "text",
485
+ name: "licenseKey",
486
+ message: "License key:",
487
+ validate: (value) => {
488
+ if (!value.trim()) {
489
+ return "License key is required";
490
+ }
491
+ if (!isValidLicenseKeyFormat(value.trim().toUpperCase())) {
492
+ return "Invalid license key format. Expected: SCAF-XXXX-XXXX-XXXX-XXXX";
493
+ }
494
+ return true;
495
+ },
496
+ format: (value) => value.trim().toUpperCase()
497
+ });
498
+ if (!licenseKey) {
499
+ return null;
500
+ }
501
+ const spinner = ora("Validating license...").start();
502
+ const validation = await validateLicense(licenseKey);
503
+ if (!validation.valid) {
504
+ spinner.fail(validation.error || "License validation failed");
505
+ return null;
506
+ }
507
+ spinner.succeed(`License validated for ${validation.email || "licensed user"}`);
508
+ await storeLicense({
509
+ key: licenseKey,
510
+ ...validation.email && { email: validation.email },
511
+ validatedAt: (/* @__PURE__ */ new Date()).toISOString()
512
+ });
513
+ logger.newLine();
514
+ return { key: licenseKey, email: validation.email };
515
+ }
516
+
517
+ // src/commands/login.ts
518
+ import prompts2 from "prompts";
519
+ import ora2 from "ora";
520
+ import fs2 from "fs/promises";
521
+ import path2 from "path";
522
+ import os from "os";
523
+ async function configureNpmrc(licenseKey) {
524
+ const npmrcPath = path2.join(os.homedir(), ".npmrc");
525
+ let content = "";
526
+ try {
527
+ content = await fs2.readFile(npmrcPath, "utf-8");
528
+ } catch {
529
+ }
530
+ const lines = content.split("\n");
531
+ const registryUrl = "https://scaffoldry.com/api/registry";
532
+ const registryKey = "@scaffoldry:registry";
533
+ const authKey = "//scaffoldry.com/api/registry/:_authToken";
534
+ let registryFound = false;
535
+ let authFound = false;
536
+ const newLines = lines.map((line) => {
537
+ if (line.trim().startsWith(registryKey)) {
538
+ registryFound = true;
539
+ return `${registryKey}=${registryUrl}`;
540
+ }
541
+ if (line.trim().startsWith(authKey)) {
542
+ authFound = true;
543
+ return `${authKey}=${licenseKey}`;
544
+ }
545
+ return line;
546
+ });
547
+ if (!registryFound) {
548
+ newLines.push(`${registryKey}=${registryUrl}`);
549
+ }
550
+ if (!authFound) {
551
+ newLines.push(`${authKey}=${licenseKey}`);
552
+ }
553
+ const cleanContent = newLines.join("\n").replace(/\n+$/, "") + "\n";
554
+ await fs2.writeFile(npmrcPath, cleanContent, "utf-8");
555
+ }
556
+ async function loginCommand() {
557
+ logger.log("");
558
+ logger.info("Scaffoldry Login");
559
+ logger.newLine();
560
+ const stored = await getStoredLicense();
561
+ if (stored) {
562
+ logger.log(`You are already logged in as ${stored.email || "licensed user"}`);
563
+ logger.newLine();
564
+ const { reauth } = await prompts2({
565
+ type: "confirm",
566
+ name: "reauth",
567
+ message: "Do you want to login with a different license key?",
568
+ initial: false
569
+ });
570
+ if (!reauth) {
571
+ return;
572
+ }
573
+ }
574
+ logger.log("Enter your Scaffoldry license key.");
575
+ logger.log("Purchase at: https://scaffoldry.com");
576
+ logger.newLine();
577
+ const { licenseKey } = await prompts2({
578
+ type: "text",
579
+ name: "licenseKey",
580
+ message: "License key:",
581
+ validate: (value) => {
582
+ if (!value.trim()) {
583
+ return "License key is required";
584
+ }
585
+ if (!isValidLicenseKeyFormat(value.trim().toUpperCase())) {
586
+ return "Invalid license key format. Expected: SCAF-XXXX-XXXX-XXXX-XXXX";
587
+ }
588
+ return true;
589
+ },
590
+ format: (value) => value.trim().toUpperCase()
591
+ });
592
+ if (!licenseKey) {
593
+ logger.error("Login cancelled.");
594
+ return;
595
+ }
596
+ const spinner = ora2("Validating license...").start();
597
+ const validation = await validateLicense(licenseKey);
598
+ if (!validation.valid) {
599
+ spinner.fail(validation.error || "License validation failed");
600
+ return;
601
+ }
602
+ spinner.succeed(`License validated!`);
603
+ await storeLicense({
604
+ key: licenseKey,
605
+ ...validation.email && { email: validation.email },
606
+ validatedAt: (/* @__PURE__ */ new Date()).toISOString()
607
+ });
608
+ await configureNpmrc(licenseKey);
609
+ logger.success("Configured .npmrc for private registry access.");
610
+ logger.newLine();
611
+ logger.success(`Logged in as ${validation.email || "licensed user"}`);
612
+ logger.newLine();
613
+ logger.log("You can now use all Scaffoldry features.");
614
+ logger.log("Run 'scaffoldry init' to create a new project.");
615
+ logger.newLine();
616
+ }
617
+
618
+ // src/commands/logout.ts
619
+ import prompts3 from "prompts";
620
+ async function logoutCommand() {
621
+ logger.log("");
622
+ logger.info("Scaffoldry Logout");
623
+ logger.newLine();
624
+ const stored = await getStoredLicense();
625
+ if (!stored) {
626
+ logger.log("You are not logged in.");
627
+ return;
628
+ }
629
+ const { confirm } = await prompts3({
630
+ type: "confirm",
631
+ name: "confirm",
632
+ message: `Log out from ${stored.email || "licensed user"}?`,
633
+ initial: true
634
+ });
635
+ if (!confirm) {
636
+ logger.log("Logout cancelled.");
637
+ return;
638
+ }
639
+ await clearLicense();
640
+ logger.success("Logged out successfully.");
641
+ logger.newLine();
642
+ }
643
+
644
+ // src/commands/rename.ts
645
+ import path3 from "path";
646
+ import fs3 from "fs-extra";
647
+ import { glob } from "glob";
648
+ import ora3 from "ora";
649
+ var SCAFFOLDRY_MARKERS = {
650
+ productName: ["Scaffoldry", "scaffoldry"],
651
+ productSlug: ["scaffoldry"],
652
+ npmScope: ["@scaffoldry"]
653
+ };
654
+ async function renameCommand(options) {
655
+ const {
656
+ productName,
657
+ productSlug = toKebabCase(productName),
658
+ primaryDomain = `${productSlug}.com`,
659
+ npmScope = `@${productSlug}`,
660
+ dryRun = false
661
+ } = options;
662
+ logger.info(`Renaming project to "${productName}"...`);
663
+ if (dryRun) {
664
+ logger.warn("DRY RUN - no files will be modified");
665
+ }
666
+ logger.newLine();
667
+ const projectDir = process.cwd();
668
+ const files = await glob("**/*.{ts,tsx,js,jsx,json,md,yml,yaml,env,env.*}", {
669
+ cwd: projectDir,
670
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**", "**/pnpm-lock.yaml"],
671
+ dot: true
672
+ });
673
+ const spinner = ora3("Scanning files...").start();
674
+ const replacements = [];
675
+ for (const file of files) {
676
+ const filePath = path3.join(projectDir, file);
677
+ const content = await fs3.readFile(filePath, "utf-8");
678
+ const changes = [];
679
+ if (SCAFFOLDRY_MARKERS.productName.some((m) => content.includes(m))) {
680
+ changes.push({ from: "Scaffoldry", to: toPascalCase(productName) });
681
+ changes.push({ from: "scaffoldry", to: productSlug });
682
+ }
683
+ if (content.includes("@scaffoldry/")) {
684
+ changes.push({ from: "@scaffoldry/", to: `${npmScope}/` });
685
+ }
686
+ if (content.includes('@scaffoldry"')) {
687
+ changes.push({ from: '@scaffoldry"', to: `${npmScope}"` });
688
+ }
689
+ if (content.includes("scaffoldry.com")) {
690
+ changes.push({ from: "scaffoldry.com", to: primaryDomain });
691
+ }
692
+ if (changes.length > 0) {
693
+ replacements.push({ file, changes });
694
+ }
695
+ }
696
+ spinner.succeed(`Found ${replacements.length} files to update`);
697
+ if (replacements.length === 0) {
698
+ logger.info("No Scaffoldry markers found. Is this a Scaffoldry project?");
699
+ return;
700
+ }
701
+ logger.newLine();
702
+ logger.log("Changes to apply:");
703
+ for (const { file, changes } of replacements) {
704
+ logger.log(` ${file}:`);
705
+ for (const { from, to } of changes) {
706
+ logger.log(` "${from}" -> "${to}"`);
707
+ }
708
+ }
709
+ logger.newLine();
710
+ if (dryRun) {
711
+ logger.warn("DRY RUN complete - no files were modified");
712
+ return;
713
+ }
714
+ const applySpinner = ora3("Applying changes...").start();
715
+ for (const { file, changes } of replacements) {
716
+ const filePath = path3.join(projectDir, file);
717
+ let content = await fs3.readFile(filePath, "utf-8");
718
+ for (const { from, to } of changes) {
719
+ content = content.split(from).join(to);
720
+ }
721
+ await fs3.writeFile(filePath, content);
722
+ }
723
+ applySpinner.succeed("Changes applied successfully");
724
+ logger.newLine();
725
+ logger.success(`Project renamed to "${productName}"!`);
726
+ logger.newLine();
727
+ logger.log("Next steps:");
728
+ logger.log(" 1. Review the changes");
729
+ logger.log(" 2. Run `pnpm install` to update dependencies");
730
+ logger.log(" 3. Commit the changes");
731
+ }
732
+
733
+ // src/commands/create-app.ts
734
+ import path4 from "path";
735
+ import fs4 from "fs-extra";
736
+ import ora4 from "ora";
737
+ async function createAppCommand(options) {
738
+ const { slug, name } = options;
739
+ const kebabSlug = toKebabCase(slug);
740
+ const pascalName = toPascalCase(name);
741
+ logger.info(`Creating new app "${name}" (${kebabSlug})...`);
742
+ logger.newLine();
743
+ const projectDir = process.cwd();
744
+ const appsDir = path4.join(projectDir, "apps");
745
+ const appDir = path4.join(appsDir, kebabSlug);
746
+ if (!await fs4.pathExists(appsDir)) {
747
+ logger.error("No 'apps' directory found. Are you in a Scaffoldry project root?");
748
+ return;
749
+ }
750
+ if (await fs4.pathExists(appDir)) {
751
+ logger.error(`App "${kebabSlug}" already exists at ${appDir}`);
752
+ return;
753
+ }
754
+ const spinner = ora4("Creating app files...").start();
755
+ try {
756
+ await fs4.ensureDir(appDir);
757
+ await fs4.writeJSON(
758
+ path4.join(appDir, "package.json"),
759
+ {
760
+ name: kebabSlug,
761
+ version: "0.0.0",
762
+ private: true,
763
+ scripts: {
764
+ dev: "next dev",
765
+ build: "next build",
766
+ start: "next start",
767
+ lint: "next lint",
768
+ typecheck: "tsc --noEmit"
769
+ },
770
+ dependencies: {
771
+ next: "^14.0.0",
772
+ react: "^18.2.0",
773
+ "react-dom": "^18.2.0",
774
+ "scaffoldry-platform": "workspace:*",
775
+ "scaffoldry-ui": "workspace:*"
776
+ },
777
+ devDependencies: {
778
+ "@types/node": "^20.10.0",
779
+ "@types/react": "^18.2.0",
780
+ "@types/react-dom": "^18.2.0",
781
+ typescript: "^5.3.0"
782
+ }
783
+ },
784
+ { spaces: 2 }
785
+ );
786
+ await fs4.writeJSON(
787
+ path4.join(appDir, "tsconfig.json"),
788
+ {
789
+ extends: "../../tsconfig.json",
790
+ compilerOptions: {
791
+ outDir: "./dist",
792
+ rootDir: "./src",
793
+ noEmit: false,
794
+ jsx: "preserve",
795
+ module: "ESNext",
796
+ moduleResolution: "bundler",
797
+ allowJs: true,
798
+ resolveJsonModule: true,
799
+ isolatedModules: true,
800
+ incremental: true,
801
+ plugins: [{ name: "next" }],
802
+ paths: {
803
+ "@/*": ["./src/*"]
804
+ }
805
+ },
806
+ include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
807
+ exclude: ["node_modules", "dist"]
808
+ },
809
+ { spaces: 2 }
810
+ );
811
+ await fs4.writeFile(
812
+ path4.join(appDir, "next.config.js"),
813
+ `/** @type {import('next').NextConfig} */
814
+ const nextConfig = {
815
+ transpilePackages: ["scaffoldry-platform", "scaffoldry-ui"],
816
+ };
817
+
818
+ module.exports = nextConfig;
819
+ `
820
+ );
821
+ await fs4.ensureDir(path4.join(appDir, "src", "app"));
822
+ await fs4.writeFile(
823
+ path4.join(appDir, "src", "app", "layout.tsx"),
824
+ `import type { Metadata } from "next";
825
+
826
+ export const metadata: Metadata = {
827
+ title: "${name}",
828
+ description: "Built with Scaffoldry",
829
+ };
830
+
831
+ export default function RootLayout({
832
+ children,
833
+ }: {
834
+ children: React.ReactNode;
835
+ }) {
836
+ return (
837
+ <html lang="en">
838
+ <body>{children}</body>
839
+ </html>
840
+ );
841
+ }
842
+ `
843
+ );
844
+ await fs4.writeFile(
845
+ path4.join(appDir, "src", "app", "page.tsx"),
846
+ `export default function Home() {
847
+ return (
848
+ <main className="flex min-h-screen flex-col items-center justify-center p-24">
849
+ <h1 className="text-4xl font-bold">${pascalName}</h1>
850
+ <p className="mt-4 text-lg text-gray-600">
851
+ Welcome to your new Scaffoldry app!
852
+ </p>
853
+ </main>
854
+ );
855
+ }
856
+ `
857
+ );
858
+ await fs4.writeFile(
859
+ path4.join(appDir, "next-env.d.ts"),
860
+ `/// <reference types="next" />
861
+ /// <reference types="next/image-types/global" />
862
+
863
+ // NOTE: This file should not be edited
864
+ // see https://nextjs.org/docs/basic-features/typescript for more information.
865
+ `
866
+ );
867
+ spinner.succeed("App files created!");
868
+ logger.newLine();
869
+ logger.success(`App "${name}" created successfully at apps/${kebabSlug}!`);
870
+ logger.newLine();
871
+ logger.log("Next steps:");
872
+ logger.log(` pnpm install`);
873
+ logger.log(` pnpm --filter ${kebabSlug} dev`);
874
+ logger.newLine();
875
+ } catch (error) {
876
+ spinner.fail("Failed to create app files.");
877
+ throw error;
878
+ }
879
+ }
880
+
881
+ // src/commands/upgrade.ts
882
+ import path5 from "path";
883
+ import fs5 from "fs-extra";
884
+ import ora5 from "ora";
885
+ var SCAFFOLDRY_PACKAGES = [
886
+ "scaffoldry-db",
887
+ "scaffoldry-core",
888
+ "scaffoldry-auth",
889
+ "scaffoldry-webhooks",
890
+ "scaffoldry-billing",
891
+ "scaffoldry-jobs",
892
+ "scaffoldry-notify",
893
+ "scaffoldry-storage",
894
+ "scaffoldry-platform",
895
+ "scaffoldry-ui",
896
+ "scaffoldry-admin",
897
+ "scaffoldry-cli"
898
+ ];
899
+ async function upgradeCommand(options) {
900
+ const { check = false, dryRun = false, breaking = false } = options;
901
+ logger.info("Checking for Scaffoldry updates...");
902
+ if (dryRun) {
903
+ logger.warn("DRY RUN - no packages will be modified");
904
+ }
905
+ logger.newLine();
906
+ const projectDir = process.cwd();
907
+ const packageJsonPath = path5.join(projectDir, "package.json");
908
+ if (!await fs5.pathExists(packageJsonPath)) {
909
+ logger.error("No package.json found. Are you in a project root?");
910
+ return;
911
+ }
912
+ const spinner = ora5("Fetching version information...").start();
913
+ try {
914
+ const packageJson = await fs5.readJSON(packageJsonPath);
915
+ const dependencies = {
916
+ ...packageJson.dependencies,
917
+ ...packageJson.devDependencies
918
+ };
919
+ const updates = [];
920
+ for (const pkgName of SCAFFOLDRY_PACKAGES) {
921
+ const currentVersion = dependencies[pkgName];
922
+ if (!currentVersion) continue;
923
+ const latestVersion = await getLatestVersion(pkgName);
924
+ if (latestVersion && currentVersion !== latestVersion) {
925
+ const isBreaking = isBreakingChange(currentVersion, latestVersion);
926
+ updates.push({
927
+ name: pkgName,
928
+ current: currentVersion,
929
+ latest: latestVersion,
930
+ isBreaking
931
+ });
932
+ }
933
+ }
934
+ spinner.succeed("Version check complete");
935
+ if (updates.length === 0) {
936
+ logger.success("All Scaffoldry packages are up to date!");
937
+ return;
938
+ }
939
+ const filteredUpdates = breaking ? updates : updates.filter((u) => !u.isBreaking);
940
+ logger.newLine();
941
+ logger.log("Available updates:");
942
+ logger.newLine();
943
+ for (const update of filteredUpdates) {
944
+ const breakingIndicator = update.isBreaking ? " (BREAKING)" : "";
945
+ logger.log(
946
+ ` ${update.name}: ${update.current} -> ${update.latest}${breakingIndicator}`
947
+ );
948
+ }
949
+ if (!breaking && updates.some((u) => u.isBreaking)) {
950
+ logger.newLine();
951
+ logger.warn(
952
+ `${updates.filter((u) => u.isBreaking).length} breaking updates available. Use --breaking to include them.`
953
+ );
954
+ }
955
+ if (check) {
956
+ logger.newLine();
957
+ logger.info("Run without --check to apply updates.");
958
+ return;
959
+ }
960
+ if (dryRun) {
961
+ logger.newLine();
962
+ logger.warn("DRY RUN complete - no packages were modified");
963
+ return;
964
+ }
965
+ logger.newLine();
966
+ const applySpinner = ora5("Applying updates...").start();
967
+ for (const update of filteredUpdates) {
968
+ if (packageJson.dependencies?.[update.name]) {
969
+ packageJson.dependencies[update.name] = update.latest;
970
+ }
971
+ if (packageJson.devDependencies?.[update.name]) {
972
+ packageJson.devDependencies[update.name] = update.latest;
973
+ }
974
+ }
975
+ await fs5.writeJSON(packageJsonPath, packageJson, { spaces: 2 });
976
+ applySpinner.succeed("Updates applied to package.json");
977
+ logger.newLine();
978
+ logger.success("Scaffoldry packages updated!");
979
+ logger.newLine();
980
+ logger.log("Next steps:");
981
+ logger.log(" 1. Run `pnpm install` to install updated packages");
982
+ logger.log(" 2. Run `scaffoldry migrate` to apply any new migrations");
983
+ logger.log(" 3. Review the CHANGELOG for breaking changes");
984
+ logger.newLine();
985
+ } catch (error) {
986
+ spinner.fail("Failed to check for updates");
987
+ throw error;
988
+ }
989
+ }
990
+ async function getLatestVersion(packageName) {
991
+ try {
992
+ const pkgPath = path5.join(
993
+ process.cwd(),
994
+ "packages",
995
+ packageName,
996
+ "package.json"
997
+ );
998
+ if (await fs5.pathExists(pkgPath)) {
999
+ const pkg = await fs5.readJSON(pkgPath);
1000
+ return pkg.version || "workspace:*";
1001
+ }
1002
+ } catch {
1003
+ }
1004
+ return null;
1005
+ }
1006
+ function isBreakingChange(current, latest) {
1007
+ const currentParts = current.replace(/[^\d.]/g, "").split(".");
1008
+ const latestParts = latest.replace(/[^\d.]/g, "").split(".");
1009
+ const currentMajor = parseInt(currentParts[0] ?? "0", 10);
1010
+ const latestMajor = parseInt(latestParts[0] ?? "0", 10);
1011
+ return !isNaN(currentMajor) && !isNaN(latestMajor) && latestMajor > currentMajor;
1012
+ }
1013
+
1014
+ // src/commands/migrate.ts
1015
+ import path6 from "path";
1016
+ import fs6 from "fs-extra";
1017
+ import { glob as glob2 } from "glob";
1018
+ import ora6 from "ora";
1019
+ import { neon } from "@neondatabase/serverless";
1020
+ import { drizzle } from "drizzle-orm/neon-http";
1021
+ import { migrate } from "drizzle-orm/neon-http/migrator";
1022
+ import postgres from "postgres";
1023
+ import { drizzle as drizzlePostgres } from "drizzle-orm/postgres-js";
1024
+ import { migrate as migratePostgres } from "drizzle-orm/postgres-js/migrator";
1025
+ async function migrateCommand(options = {}) {
1026
+ const { dryRun = false } = options;
1027
+ logger.info("Running migrations...");
1028
+ logger.newLine();
1029
+ const projectDir = process.cwd();
1030
+ const databaseUrl = process.env.DATABASE_URL;
1031
+ if (!databaseUrl) {
1032
+ logger.error("DATABASE_URL environment variable is not set.");
1033
+ logger.log("Set it in your .env file or export it in your shell.");
1034
+ return;
1035
+ }
1036
+ const migrationDirs = [
1037
+ path6.join(projectDir, "packages", "scaffoldry-db", "drizzle"),
1038
+ path6.join(projectDir, "drizzle")
1039
+ ];
1040
+ let migrationsDir = null;
1041
+ for (const dir of migrationDirs) {
1042
+ if (await fs6.pathExists(dir)) {
1043
+ migrationsDir = dir;
1044
+ break;
1045
+ }
1046
+ }
1047
+ if (!migrationsDir) {
1048
+ logger.error("No migrations directory found.");
1049
+ logger.log("Expected one of:");
1050
+ for (const dir of migrationDirs) {
1051
+ logger.log(` - ${dir}`);
1052
+ }
1053
+ logger.newLine();
1054
+ logger.log("Run 'pnpm --filter scaffoldry-db generate' to create migrations.");
1055
+ return;
1056
+ }
1057
+ const migrations = await glob2("*.sql", { cwd: migrationsDir });
1058
+ if (migrations.length === 0) {
1059
+ logger.warn("No migration files found.");
1060
+ logger.log("Run 'pnpm --filter scaffoldry-db generate' to create migrations.");
1061
+ return;
1062
+ }
1063
+ logger.info(`Found ${migrations.length} migration file(s) in ${migrationsDir}`);
1064
+ for (const migration of migrations.sort()) {
1065
+ logger.log(` - ${migration}`);
1066
+ }
1067
+ logger.newLine();
1068
+ if (dryRun) {
1069
+ logger.info("Dry run mode - no migrations will be applied.");
1070
+ return;
1071
+ }
1072
+ const spinner = ora6("Applying migrations...").start();
1073
+ try {
1074
+ const isNeon = databaseUrl.includes("neon.tech") || databaseUrl.includes("neon-");
1075
+ if (isNeon) {
1076
+ const sql = neon(databaseUrl);
1077
+ const db = drizzle(sql);
1078
+ await migrate(db, { migrationsFolder: migrationsDir });
1079
+ } else {
1080
+ const sql = postgres(databaseUrl, { max: 1 });
1081
+ const db = drizzlePostgres(sql);
1082
+ await migratePostgres(db, { migrationsFolder: migrationsDir });
1083
+ await sql.end();
1084
+ }
1085
+ spinner.succeed("Migrations applied successfully!");
1086
+ logger.newLine();
1087
+ logger.success(`Applied ${migrations.length} migration(s).`);
1088
+ logger.newLine();
1089
+ } catch (error) {
1090
+ spinner.fail("Migration failed");
1091
+ if (error instanceof Error) {
1092
+ logger.error(error.message);
1093
+ if (error.message.includes("already exists")) {
1094
+ logger.newLine();
1095
+ logger.log("This usually means the migration was partially applied.");
1096
+ logger.log("Check your database state and consider resetting if in development.");
1097
+ }
1098
+ }
1099
+ throw error;
1100
+ }
1101
+ }
1102
+ async function migrateCreateCommand(options) {
1103
+ const { name } = options;
1104
+ if (!/^[a-z][a-z0-9_]*$/.test(name)) {
1105
+ logger.error("Migration name must be snake_case (e.g., add_user_preferences)");
1106
+ return;
1107
+ }
1108
+ logger.info(`Creating migration "${name}"...`);
1109
+ logger.newLine();
1110
+ const projectDir = process.cwd();
1111
+ const migrationsDir = path6.join(projectDir, "drizzle");
1112
+ await fs6.ensureDir(migrationsDir);
1113
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T]/g, "").slice(0, 14);
1114
+ const fileName = `${timestamp}_${name}.sql`;
1115
+ const filePath = path6.join(migrationsDir, fileName);
1116
+ if (await fs6.pathExists(filePath)) {
1117
+ logger.error(`Migration file already exists: ${fileName}`);
1118
+ return;
1119
+ }
1120
+ const template = `-- Migration: ${name}
1121
+ -- Created at: ${(/* @__PURE__ */ new Date()).toISOString()}
1122
+
1123
+ -- Write your migration SQL here
1124
+
1125
+ -- Example:
1126
+ -- CREATE TABLE user_preferences (
1127
+ -- id TEXT PRIMARY KEY,
1128
+ -- user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
1129
+ -- theme TEXT NOT NULL DEFAULT 'light',
1130
+ -- notifications_enabled BOOLEAN NOT NULL DEFAULT true,
1131
+ -- created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1132
+ -- updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
1133
+ -- );
1134
+
1135
+ -- CREATE INDEX idx_user_preferences_user_id ON user_preferences(user_id);
1136
+ `;
1137
+ await fs6.writeFile(filePath, template);
1138
+ logger.success(`Migration created: ${fileName}`);
1139
+ logger.newLine();
1140
+ logger.log(`Edit the file at: ${filePath}`);
1141
+ logger.newLine();
1142
+ logger.log("After editing, run:");
1143
+ logger.log(" scaffoldry migrate");
1144
+ logger.newLine();
1145
+ }
1146
+
1147
+ // src/commands/dev.ts
1148
+ import { spawn } from "child_process";
1149
+ import chalk from "chalk";
1150
+ var colors = [
1151
+ chalk.cyan,
1152
+ chalk.magenta,
1153
+ chalk.yellow,
1154
+ chalk.green,
1155
+ chalk.blue
1156
+ ];
1157
+ function prefixLog(prefix, color, message) {
1158
+ const lines = message.split("\n").filter(Boolean);
1159
+ for (const line of lines) {
1160
+ console.log(color(`[${prefix}]`) + " " + line);
1161
+ }
1162
+ }
1163
+ async function checkPrerequisites(projectDir, options) {
1164
+ console.log();
1165
+ printBox("Starting Development Environment", [
1166
+ "Checking prerequisites..."
1167
+ ]);
1168
+ const checks = [];
1169
+ checks.push({ result: await checkNodeVersion(), required: true });
1170
+ checks.push({ result: await checkPnpmVersion(), required: true });
1171
+ const stripeCli = await checkStripeCli();
1172
+ const env = await readEnvLocal(projectDir);
1173
+ const hasStripeKey = Boolean(env.STRIPE_SECRET_KEY);
1174
+ checks.push({ result: stripeCli, required: !options.skipStripe && hasStripeKey });
1175
+ checks.push({ result: await checkEnvVar(projectDir, "DATABASE_URL"), required: true });
1176
+ if (!options.skipStripe) {
1177
+ checks.push({ result: await checkEnvVar(projectDir, "STRIPE_SECRET_KEY", { required: false }), required: false });
1178
+ }
1179
+ let allPassed = true;
1180
+ const stripeCliAvailable = stripeCli.status === "pass";
1181
+ for (const { result, required } of checks) {
1182
+ const icon = result.status === "pass" ? chalk.green("\u2713") : result.status === "warn" ? chalk.yellow("\u26A0") : chalk.red("\u2717");
1183
+ let line = ` ${icon} ${result.label}`;
1184
+ if (result.message) {
1185
+ line += chalk.gray(` (${result.message})`);
1186
+ }
1187
+ console.log(line);
1188
+ if (required && result.status === "fail") {
1189
+ allPassed = false;
1190
+ }
1191
+ }
1192
+ if (!allPassed) {
1193
+ console.log();
1194
+ printError("Some required prerequisites are missing");
1195
+ printInfo("Run `scaffoldry doctor` for more details");
1196
+ return { pass: false, stripeCli: stripeCliAvailable };
1197
+ }
1198
+ return { pass: true, stripeCli: stripeCliAvailable };
1199
+ }
1200
+ async function devCommand(options = {}) {
1201
+ const projectDir = process.cwd();
1202
+ const port = options.port || 3e3;
1203
+ const prereqs = await checkPrerequisites(projectDir, options);
1204
+ if (!prereqs.pass) {
1205
+ process.exit(1);
1206
+ }
1207
+ const processes = [];
1208
+ let colorIndex = 0;
1209
+ const getNextColor = () => {
1210
+ const color = colors[colorIndex++ % colors.length];
1211
+ return color ?? chalk.white;
1212
+ };
1213
+ console.log();
1214
+ console.log("Starting services...");
1215
+ const nextProcess = spawn("pnpm", ["run", "dev", "--", "--port", String(port)], {
1216
+ ...getSpawnOptions({
1217
+ cwd: projectDir,
1218
+ stdio: ["inherit", "pipe", "pipe"]
1219
+ })
1220
+ });
1221
+ const nextInfo = {
1222
+ name: "next",
1223
+ process: nextProcess,
1224
+ color: getNextColor()
1225
+ };
1226
+ processes.push(nextInfo);
1227
+ nextProcess.stdout?.on("data", (data) => {
1228
+ prefixLog("next", nextInfo.color, data.toString());
1229
+ });
1230
+ nextProcess.stderr?.on("data", (data) => {
1231
+ prefixLog("next", nextInfo.color, data.toString());
1232
+ });
1233
+ printSuccess(`Next.js dev server starting on http://localhost:${port}`);
1234
+ const env = await readEnvLocal(projectDir);
1235
+ if (!options.skipStripe && prereqs.stripeCli && env.STRIPE_SECRET_KEY) {
1236
+ const stripeProcess = spawn(
1237
+ "stripe",
1238
+ ["listen", "--forward-to", `localhost:${port}/api/webhooks/stripe`],
1239
+ {
1240
+ ...getSpawnOptions({
1241
+ cwd: projectDir,
1242
+ stdio: ["inherit", "pipe", "pipe"]
1243
+ })
1244
+ }
1245
+ );
1246
+ const stripeInfo = {
1247
+ name: "stripe",
1248
+ process: stripeProcess,
1249
+ color: getNextColor()
1250
+ };
1251
+ processes.push(stripeInfo);
1252
+ stripeProcess.stdout?.on("data", (data) => {
1253
+ const output = data.toString();
1254
+ prefixLog("stripe", stripeInfo.color, output);
1255
+ const secretMatch = output.match(/whsec_[A-Za-z0-9]+/);
1256
+ if (secretMatch) {
1257
+ console.log();
1258
+ printInfo(`Webhook secret: ${secretMatch[0]}`);
1259
+ printInfo("Update STRIPE_WEBHOOK_SECRET in .env.local if needed");
1260
+ }
1261
+ });
1262
+ stripeProcess.stderr?.on("data", (data) => {
1263
+ prefixLog("stripe", stripeInfo.color, data.toString());
1264
+ });
1265
+ printSuccess("Stripe CLI listening for webhooks");
1266
+ } else if (!options.skipStripe && !prereqs.stripeCli && env.STRIPE_SECRET_KEY) {
1267
+ printWarning("Stripe CLI not installed - webhook forwarding disabled");
1268
+ const { getInstallInstructions } = await import("./platform-Z35MB2P5.js");
1269
+ printInfo(getInstallInstructions("stripe-cli"));
1270
+ }
1271
+ console.log();
1272
+ printBox("Development Server Ready", [
1273
+ `App: http://localhost:${port}`,
1274
+ "",
1275
+ "Press Ctrl+C to stop all services."
1276
+ ]);
1277
+ console.log();
1278
+ const cleanup = () => {
1279
+ console.log();
1280
+ console.log("Shutting down...");
1281
+ const termSignal = getTermSignal();
1282
+ const killSignal = getKillSignal();
1283
+ for (const { name, process: proc, color } of processes) {
1284
+ prefixLog(name, color, "Stopping...");
1285
+ proc.kill(termSignal);
1286
+ }
1287
+ setTimeout(() => {
1288
+ for (const { process: proc } of processes) {
1289
+ if (!proc.killed) {
1290
+ proc.kill(killSignal);
1291
+ }
1292
+ }
1293
+ process.exit(0);
1294
+ }, 3e3);
1295
+ };
1296
+ setupShutdownHandlers(cleanup);
1297
+ for (const { name, process: proc, color } of processes) {
1298
+ proc.on("exit", (code) => {
1299
+ prefixLog(name, color, `Exited with code ${code}`);
1300
+ });
1301
+ proc.on("error", (error) => {
1302
+ prefixLog(name, color, `Error: ${error.message}`);
1303
+ });
1304
+ }
1305
+ }
1306
+
1307
+ // src/commands/doctor.ts
1308
+ import chalk2 from "chalk";
1309
+ import prompts4 from "prompts";
1310
+ function printCheckResults(results) {
1311
+ for (const result of results) {
1312
+ const icon = result.status === "pass" ? chalk2.green("\u2713") : result.status === "warn" ? chalk2.yellow("\u26A0") : chalk2.red("\u2717");
1313
+ let line = ` ${icon} ${result.label}`;
1314
+ if (result.message) {
1315
+ line += chalk2.gray(` (${result.message})`);
1316
+ }
1317
+ console.log(line);
1318
+ }
1319
+ }
1320
+ function collectIssues(results) {
1321
+ const issues = [];
1322
+ for (const result of results) {
1323
+ if (result.status === "fail" || result.status === "warn") {
1324
+ const issue = {
1325
+ label: result.label,
1326
+ message: result.message || "Issue detected"
1327
+ };
1328
+ if (result.fix) {
1329
+ issue.fix = result.fix;
1330
+ }
1331
+ issues.push(issue);
1332
+ }
1333
+ }
1334
+ return issues;
1335
+ }
1336
+ async function checkLicense() {
1337
+ const stored = await getStoredLicense();
1338
+ if (!stored) {
1339
+ return {
1340
+ status: "fail",
1341
+ label: "Scaffoldry license",
1342
+ message: "Not authenticated",
1343
+ fix: "Run: scaffoldry login"
1344
+ };
1345
+ }
1346
+ const validation = await validateLicense(stored.key);
1347
+ if (validation.valid) {
1348
+ return {
1349
+ status: "pass",
1350
+ label: "Scaffoldry license",
1351
+ message: stored.email || "Valid"
1352
+ };
1353
+ }
1354
+ return {
1355
+ status: "fail",
1356
+ label: "Scaffoldry license",
1357
+ message: validation.error || "Invalid",
1358
+ fix: "Run: scaffoldry login"
1359
+ };
1360
+ }
1361
+ async function doctorCommand() {
1362
+ const projectDir = process.cwd();
1363
+ console.log();
1364
+ printBox("Scaffoldry Doctor", [
1365
+ "Checking your project configuration..."
1366
+ ]);
1367
+ console.log();
1368
+ console.log(chalk2.bold("Platform:"), getPlatformDisplayName());
1369
+ console.log();
1370
+ console.log(chalk2.bold("Checking environment..."));
1371
+ const envResults = await runEnvironmentChecks();
1372
+ printCheckResults(envResults);
1373
+ console.log();
1374
+ console.log(chalk2.bold("Checking license..."));
1375
+ const licenseResult = await checkLicense();
1376
+ printCheckResults([licenseResult]);
1377
+ console.log();
1378
+ console.log(chalk2.bold("Checking configuration..."));
1379
+ const configResults = await runConfigurationChecks(projectDir);
1380
+ printCheckResults(configResults);
1381
+ console.log();
1382
+ console.log(chalk2.bold("Checking services..."));
1383
+ const serviceResults = await runServiceChecks(projectDir);
1384
+ printCheckResults(serviceResults);
1385
+ const licenseIssue = licenseResult.status !== "pass" ? {
1386
+ label: licenseResult.label,
1387
+ message: licenseResult.message || "Issue",
1388
+ ...licenseResult.fix ? { fix: licenseResult.fix } : {}
1389
+ } : null;
1390
+ const allIssues = [
1391
+ ...collectIssues(envResults),
1392
+ ...licenseIssue ? [licenseIssue] : [],
1393
+ ...collectIssues(configResults),
1394
+ ...collectIssues(serviceResults)
1395
+ ];
1396
+ const criticalIssues = allIssues.filter(
1397
+ (issue) => !issue.message.includes("optional") && !issue.message.includes("network error")
1398
+ );
1399
+ console.log();
1400
+ if (criticalIssues.length === 0) {
1401
+ console.log(chalk2.green.bold("\u2713 All checks passed!"));
1402
+ console.log();
1403
+ printInfo("Your project is configured correctly.");
1404
+ printInfo("Run `scaffoldry dev` to start development.");
1405
+ return;
1406
+ }
1407
+ const borderTop = "\u250C" + "\u2500".repeat(61) + "\u2510";
1408
+ const borderBottom = "\u2514" + "\u2500".repeat(61) + "\u2518";
1409
+ console.log(chalk2.yellow(borderTop));
1410
+ console.log(chalk2.yellow("\u2502") + " ".repeat(61) + chalk2.yellow("\u2502"));
1411
+ console.log(chalk2.yellow("\u2502") + chalk2.bold(` ${criticalIssues.length} issue${criticalIssues.length === 1 ? "" : "s"} need attention:`).padEnd(64) + chalk2.yellow("\u2502"));
1412
+ console.log(chalk2.yellow("\u2502") + " ".repeat(61) + chalk2.yellow("\u2502"));
1413
+ criticalIssues.forEach((issue, i) => {
1414
+ console.log(chalk2.yellow("\u2502") + ` ${i + 1}. ${issue.label}`.padEnd(61) + chalk2.yellow("\u2502"));
1415
+ console.log(chalk2.yellow("\u2502") + chalk2.gray(` ${issue.message}`).padEnd(70) + chalk2.yellow("\u2502"));
1416
+ if (issue.fix) {
1417
+ console.log(chalk2.yellow("\u2502") + chalk2.cyan(` \u2192 ${issue.fix}`).padEnd(70) + chalk2.yellow("\u2502"));
1418
+ }
1419
+ console.log(chalk2.yellow("\u2502") + " ".repeat(61) + chalk2.yellow("\u2502"));
1420
+ });
1421
+ console.log(chalk2.yellow(borderBottom));
1422
+ const fixableIssues = criticalIssues.filter((issue) => issue.fix?.startsWith("Run: scaffoldry setup"));
1423
+ if (fixableIssues.length > 0) {
1424
+ console.log();
1425
+ const { autoFix } = await prompts4({
1426
+ type: "confirm",
1427
+ name: "autoFix",
1428
+ message: "Run setup wizards to fix issues?",
1429
+ initial: true
1430
+ });
1431
+ if (autoFix) {
1432
+ const { setupCommand: setupCommand2 } = await import("./setup-L2PO5OVZ.js");
1433
+ const services = /* @__PURE__ */ new Set();
1434
+ for (const issue of fixableIssues) {
1435
+ if (issue.fix?.includes("setup stripe")) services.add("stripe");
1436
+ if (issue.fix?.includes("setup database")) services.add("database");
1437
+ if (issue.fix?.includes("setup email")) services.add("email");
1438
+ if (issue.fix?.includes("setup storage")) services.add("storage");
1439
+ if (issue.fix?.includes("setup all")) {
1440
+ services.add("database");
1441
+ services.add("stripe");
1442
+ services.add("email");
1443
+ }
1444
+ }
1445
+ for (const service of services) {
1446
+ await setupCommand2({ service });
1447
+ }
1448
+ console.log();
1449
+ printInfo("Run `scaffoldry doctor` again to verify fixes");
1450
+ }
1451
+ }
1452
+ }
1453
+
1454
+ // src/commands/secret.ts
1455
+ import crypto from "crypto";
1456
+ import prompts5 from "prompts";
1457
+ function generateSecureSecret(bytes = 32) {
1458
+ return crypto.randomBytes(bytes).toString("base64url");
1459
+ }
1460
+ async function secretSetCommand(options) {
1461
+ const projectDir = process.cwd();
1462
+ if (!options.key || !options.value) {
1463
+ printError("Both key and value are required");
1464
+ printInfo("Usage: scaffoldry secret set <KEY> <VALUE>");
1465
+ return;
1466
+ }
1467
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(options.key)) {
1468
+ printError("Invalid key format. Use UPPERCASE_WITH_UNDERSCORES");
1469
+ return;
1470
+ }
1471
+ await writeEnvLocal(projectDir, { [options.key]: options.value });
1472
+ logger.newLine();
1473
+ printSuccess(`Set ${options.key} in .env.local`);
1474
+ }
1475
+ async function secretRotateCommand(options) {
1476
+ const projectDir = process.cwd();
1477
+ switch (options.type) {
1478
+ case "auth":
1479
+ await rotateAuthSecret(projectDir);
1480
+ break;
1481
+ case "db":
1482
+ await rotateDatabaseCredentials(projectDir);
1483
+ break;
1484
+ default:
1485
+ printError(`Unknown secret type: ${options.type}`);
1486
+ printInfo("Supported types: auth, db");
1487
+ }
1488
+ }
1489
+ async function rotateAuthSecret(projectDir) {
1490
+ console.log();
1491
+ printBox("Rotating AUTH_SECRET", [
1492
+ "Per Section 32 rotation rules:",
1493
+ "1. Current AUTH_SECRET \u2192 AUTH_SECRET_PREVIOUS",
1494
+ "2. Generate new 32-byte random AUTH_SECRET",
1495
+ "3. Both keys valid during rotation window"
1496
+ ]);
1497
+ const env = await readEnvLocal(projectDir);
1498
+ const currentSecret = env.AUTH_SECRET;
1499
+ if (!currentSecret) {
1500
+ printWarning("No AUTH_SECRET found in .env.local");
1501
+ console.log();
1502
+ const { generate } = await prompts5({
1503
+ type: "confirm",
1504
+ name: "generate",
1505
+ message: "Generate a new AUTH_SECRET?",
1506
+ initial: true
1507
+ });
1508
+ if (generate) {
1509
+ const newSecret2 = generateSecureSecret(32);
1510
+ await writeEnvLocal(projectDir, { AUTH_SECRET: newSecret2 });
1511
+ printSuccess("Generated new AUTH_SECRET");
1512
+ }
1513
+ return;
1514
+ }
1515
+ console.log();
1516
+ const { proceed } = await prompts5({
1517
+ type: "confirm",
1518
+ name: "proceed",
1519
+ message: "Proceed with rotation?",
1520
+ initial: true
1521
+ });
1522
+ if (!proceed) {
1523
+ printInfo("Rotation cancelled");
1524
+ return;
1525
+ }
1526
+ console.log();
1527
+ console.log("Rotating...");
1528
+ const newSecret = generateSecureSecret(32);
1529
+ await writeEnvLocal(projectDir, {
1530
+ AUTH_SECRET: newSecret,
1531
+ AUTH_SECRET_PREVIOUS: currentSecret
1532
+ });
1533
+ printSuccess("Moved current key to AUTH_SECRET_PREVIOUS");
1534
+ printSuccess("Generated new AUTH_SECRET");
1535
+ printSuccess("Updated .env.local");
1536
+ console.log();
1537
+ printWarning("Important: Restart your server to apply changes.");
1538
+ printInfo("Sessions signed with old key will remain valid");
1539
+ printInfo("during the rotation window (AUTH_SECRET_PREVIOUS).");
1540
+ }
1541
+ async function rotateDatabaseCredentials(projectDir) {
1542
+ console.log();
1543
+ printBox("Database Credential Rotation", [
1544
+ "Database credentials require careful rotation to avoid downtime.",
1545
+ "",
1546
+ "Steps for Neon PostgreSQL:",
1547
+ "1. Create new role in Neon dashboard",
1548
+ "2. Grant same permissions as current role",
1549
+ "3. Update DATABASE_URL with new credentials",
1550
+ "4. Verify connection works",
1551
+ "5. Revoke old credentials"
1552
+ ]);
1553
+ const env = await readEnvLocal(projectDir);
1554
+ const currentUrl = env.DATABASE_URL;
1555
+ if (!currentUrl) {
1556
+ printError("No DATABASE_URL found in .env.local");
1557
+ printInfo("Run: scaffoldry setup database");
1558
+ return;
1559
+ }
1560
+ console.log();
1561
+ printInfo("Current database host:");
1562
+ try {
1563
+ const url = new URL(currentUrl);
1564
+ console.log(` ${url.hostname}`);
1565
+ } catch {
1566
+ console.log(" (unable to parse URL)");
1567
+ }
1568
+ console.log();
1569
+ printInfo("To rotate database credentials:");
1570
+ console.log(" 1. Go to https://console.neon.tech");
1571
+ console.log(" 2. Navigate to your project > Settings > Roles");
1572
+ console.log(" 3. Create a new role");
1573
+ console.log(" 4. Update the connection string");
1574
+ console.log(" 5. Run: scaffoldry setup database");
1575
+ console.log();
1576
+ const { openDocs } = await prompts5({
1577
+ type: "confirm",
1578
+ name: "openDocs",
1579
+ message: "Open Neon documentation for credential rotation?",
1580
+ initial: true
1581
+ });
1582
+ if (openDocs) {
1583
+ const { exec } = await import("child_process");
1584
+ const { promisify } = await import("util");
1585
+ const execAsync = promisify(exec);
1586
+ const os2 = await import("os");
1587
+ const url = "https://neon.tech/docs/manage/roles";
1588
+ const platform = os2.platform();
1589
+ try {
1590
+ if (platform === "darwin") {
1591
+ await execAsync(`open "${url}"`);
1592
+ } else if (platform === "win32") {
1593
+ await execAsync(`start "${url}"`);
1594
+ } else {
1595
+ await execAsync(`xdg-open "${url}"`);
1596
+ }
1597
+ printInfo("Opened Neon documentation");
1598
+ } catch {
1599
+ printInfo(`Visit: ${url}`);
1600
+ }
1601
+ }
1602
+ }
1603
+
1604
+ // src/commands/plan.ts
1605
+ import fs7 from "fs-extra";
1606
+ import path7 from "path";
1607
+ import prompts6 from "prompts";
1608
+ var MASTER_SPEC_INVARIANTS = `
1609
+ ## Scaffoldry v1 Invariants (MUST FOLLOW)
1610
+
1611
+ These are non-negotiable constraints that all generated code MUST follow:
1612
+
1613
+ ### Authentication
1614
+ - Use Argon2id for password hashing (NOT bcrypt)
1615
+ - Implement Pwned Passwords check on registration
1616
+ - Magic link tokens: 32-byte random, 15-minute expiry
1617
+ - Session cookies: HttpOnly, Secure, SameSite=Lax
1618
+
1619
+ ### Database
1620
+ - All tables MUST have tenant_id column for multi-tenancy
1621
+ - Use Row-Level Security (RLS) policies
1622
+ - Soft-delete with deleted_at timestamp (NOT hard delete)
1623
+ - All timestamps in UTC
1624
+
1625
+ ### API Design
1626
+ - All endpoints MUST be rate-limited
1627
+ - Input validation with Zod schemas
1628
+ - Return consistent error format: { error: string, code?: string }
1629
+ - Use POST for mutations, GET for queries
1630
+
1631
+ ### Security
1632
+ - No secrets in client-side code (NEXT_PUBLIC_ prefix rules)
1633
+ - CSRF protection on state-changing operations
1634
+ - Validate webhook signatures before processing
1635
+ - Log all authentication events to audit table
1636
+ `;
1637
+ var MASTER_SPEC_SECURITY = `
1638
+ ## Security Hardening Requirements
1639
+
1640
+ ### Password Security
1641
+ - Minimum 8 characters, check against Pwned Passwords API
1642
+ - Hash with Argon2id: memoryCost=65536, timeCost=3, parallelism=4
1643
+ - Never store plaintext passwords
1644
+ - Never log passwords or password hashes
1645
+
1646
+ ### Session Security
1647
+ - Rotate session ID on privilege escalation
1648
+ - 24-hour session expiry, refresh on activity
1649
+ - Store sessions in database with user_agent and ip_hash
1650
+ - Invalidate all sessions on password change
1651
+
1652
+ ### Rate Limiting
1653
+ - Login: 5 attempts per 15 minutes per IP
1654
+ - API: 100 requests per minute per user
1655
+ - Webhook: 1000 requests per minute per endpoint
1656
+
1657
+ ### Input Validation
1658
+ - Validate ALL user input server-side
1659
+ - Sanitize HTML output to prevent XSS
1660
+ - Use parameterized queries (Drizzle handles this)
1661
+ - Validate file uploads: type, size, content
1662
+
1663
+ ### Audit Logging
1664
+ - Log: login, logout, password_change, role_change, data_export
1665
+ - Include: user_id, action, ip_hash, timestamp, metadata
1666
+ - IP addresses MUST be hashed with salt (privacy compliance)
1667
+ `;
1668
+ var TECHNICAL_STACK = `
1669
+ ## Technical Stack (v1 - Locked)
1670
+
1671
+ This is the production-ready stack used by Scaffoldry:
1672
+
1673
+ - **Framework**: Next.js 14+ with App Router
1674
+ - **Language**: TypeScript (strict mode)
1675
+ - **Database**: PostgreSQL via Neon (serverless)
1676
+ - **ORM**: Drizzle ORM with migrations
1677
+ - **Auth**: Custom implementation (password + magic link)
1678
+ - **Payments**: Stripe (subscriptions + one-time)
1679
+ - **Email**: Resend (transactional emails)
1680
+ - **Storage**: AWS S3 (file uploads)
1681
+ - **Styling**: Tailwind CSS
1682
+ - **Validation**: Zod
1683
+ - **Testing**: Vitest
1684
+
1685
+ ### File Structure
1686
+ \`\`\`
1687
+ apps/web/
1688
+ \u251C\u2500\u2500 src/
1689
+ \u2502 \u251C\u2500\u2500 app/ # Next.js App Router pages
1690
+ \u2502 \u251C\u2500\u2500 components/ # React components
1691
+ \u2502 \u2514\u2500\u2500 lib/ # Utilities and helpers
1692
+ packages/
1693
+ \u251C\u2500\u2500 database/ # Drizzle schema and migrations
1694
+ \u251C\u2500\u2500 auth/ # Authentication logic
1695
+ \u251C\u2500\u2500 billing/ # Stripe integration
1696
+ \u251C\u2500\u2500 email/ # Email templates and sending
1697
+ \u2514\u2500\u2500 shared/ # Shared types and utilities
1698
+ \`\`\`
1699
+ `;
1700
+ function generateDevelopmentPlan(projectName, description, features) {
1701
+ return `# Project: ${projectName}
1702
+
1703
+ ## Executive Summary
1704
+ ${description}
1705
+
1706
+ ## Technical Stack
1707
+ - **Frontend**: Next.js 14 with App Router, TypeScript, Tailwind CSS
1708
+ - **Backend**: Next.js API Routes, PostgreSQL (Neon)
1709
+ - **Authentication**: Email/password + Magic links
1710
+ - **Payments**: Stripe subscriptions
1711
+ - **Email**: Transactional emails via Resend
1712
+ - **Database**: Neon (serverless PostgreSQL) + Drizzle ORM
1713
+
1714
+ ## Core Features
1715
+ ${features.map((f) => `- ${f}`).join("\n")}
1716
+
1717
+ ## Implementation Phases
1718
+
1719
+ ### Phase 1: Foundation
1720
+ - [ ] Project setup and configuration
1721
+ - [ ] Database schema design
1722
+ - [ ] User authentication flows
1723
+ - [ ] Basic CRUD operations
1724
+
1725
+ ### Phase 2: Core Features
1726
+ - [ ] Implement primary features
1727
+ - [ ] API endpoints
1728
+ - [ ] Frontend components
1729
+ - [ ] Email notifications
1730
+
1731
+ ### Phase 3: Polish
1732
+ - [ ] Error handling
1733
+ - [ ] Loading states
1734
+ - [ ] Testing
1735
+ - [ ] Documentation
1736
+
1737
+ ## Security Considerations
1738
+ - Row-level security for multi-tenancy
1739
+ - Rate limiting on all endpoints
1740
+ - Input validation with Zod
1741
+ - Audit logging for sensitive operations
1742
+
1743
+ ## Deployment
1744
+ - Vercel (recommended) or any Node.js host
1745
+ - Neon for database
1746
+ - Environment variables configured via \`scaffoldry setup\`
1747
+ `;
1748
+ }
1749
+ function generateAIPrompt(projectName, description, features) {
1750
+ return `# AI Implementation Prompt for ${projectName}
1751
+
1752
+ ## Project Description
1753
+ ${description}
1754
+
1755
+ ## Features to Implement
1756
+ ${features.map((f, i) => `${i + 1}. ${f}`).join("\n")}
1757
+
1758
+ ---
1759
+
1760
+ # CRITICAL: Scaffoldry Framework Context
1761
+
1762
+ You are implementing features for a Scaffoldry project. You MUST follow these constraints exactly.
1763
+
1764
+ ${MASTER_SPEC_INVARIANTS}
1765
+
1766
+ ${MASTER_SPEC_SECURITY}
1767
+
1768
+ ${TECHNICAL_STACK}
1769
+
1770
+ ---
1771
+
1772
+ ## Implementation Guidelines
1773
+
1774
+ When generating code:
1775
+
1776
+ 1. **Use existing patterns** - Check existing code in the codebase for patterns
1777
+ 2. **Follow the schema** - Database tables in packages/database/src/schema/
1778
+ 3. **Type everything** - No \`any\` types, use Zod for runtime validation
1779
+ 4. **Handle errors** - All API routes must have try/catch with proper error responses
1780
+ 5. **Add audit logs** - Log sensitive operations to the audit table
1781
+ 6. **Test critical paths** - Auth, payments, and data access MUST have tests
1782
+
1783
+ ## Example Patterns
1784
+
1785
+ ### API Route Pattern
1786
+ \`\`\`typescript
1787
+ // apps/web/src/app/api/example/route.ts
1788
+ import { NextRequest, NextResponse } from "next/server";
1789
+ import { z } from "zod";
1790
+ import { getSession } from "@scaffoldry/auth";
1791
+ import { db } from "@scaffoldry/database";
1792
+
1793
+ const schema = z.object({
1794
+ // Define your schema
1795
+ });
1796
+
1797
+ export async function POST(req: NextRequest) {
1798
+ try {
1799
+ const session = await getSession();
1800
+ if (!session) {
1801
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
1802
+ }
1803
+
1804
+ const body = await req.json();
1805
+ const data = schema.parse(body);
1806
+
1807
+ // Implementation here
1808
+
1809
+ return NextResponse.json({ success: true });
1810
+ } catch (error) {
1811
+ if (error instanceof z.ZodError) {
1812
+ return NextResponse.json({ error: "Invalid input" }, { status: 400 });
1813
+ }
1814
+ console.error("API Error:", error);
1815
+ return NextResponse.json({ error: "Internal error" }, { status: 500 });
1816
+ }
1817
+ }
1818
+ \`\`\`
1819
+
1820
+ ### Database Query Pattern
1821
+ \`\`\`typescript
1822
+ import { db, eq, and } from "@scaffoldry/database";
1823
+ import { users } from "@scaffoldry/database/schema";
1824
+
1825
+ // Always filter by tenant_id for multi-tenancy
1826
+ const user = await db.query.users.findFirst({
1827
+ where: and(
1828
+ eq(users.id, userId),
1829
+ eq(users.tenantId, session.tenantId)
1830
+ ),
1831
+ });
1832
+ \`\`\`
1833
+
1834
+ ---
1835
+
1836
+ Now implement the features described above, following all constraints and patterns.
1837
+ `;
1838
+ }
1839
+ async function planCommand() {
1840
+ console.log();
1841
+ printBox("Project Planner", [
1842
+ "Let's create a development plan for your SaaS.",
1843
+ "",
1844
+ "This will generate:",
1845
+ "\u2022 DEVELOPMENT_PLAN.md - Share with your team",
1846
+ "\u2022 AI_PROMPT.md - Paste into Claude, GPT, or Cursor"
1847
+ ]);
1848
+ const { description } = await prompts6({
1849
+ type: "text",
1850
+ name: "description",
1851
+ message: "Describe what you want to build:",
1852
+ validate: (value) => value.length > 10 ? true : "Please provide more detail"
1853
+ });
1854
+ if (!description) {
1855
+ printInfo("Cancelled");
1856
+ return;
1857
+ }
1858
+ console.log();
1859
+ printInfo("Now list the key features (one per line, empty line to finish):");
1860
+ console.log();
1861
+ const features = [];
1862
+ let collecting = true;
1863
+ while (collecting) {
1864
+ const { feature } = await prompts6({
1865
+ type: "text",
1866
+ name: "feature",
1867
+ message: features.length === 0 ? "Feature 1:" : `Feature ${features.length + 1}:`
1868
+ });
1869
+ if (!feature || feature.trim() === "") {
1870
+ if (features.length === 0) {
1871
+ printInfo("Please add at least one feature");
1872
+ continue;
1873
+ }
1874
+ collecting = false;
1875
+ } else {
1876
+ features.push(feature.trim());
1877
+ }
1878
+ }
1879
+ const projectName = description.split(" ").slice(0, 3).map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join("");
1880
+ console.log();
1881
+ console.log("\u250C" + "\u2500".repeat(61) + "\u2510");
1882
+ console.log("\u2502" + " ".repeat(61) + "\u2502");
1883
+ console.log("\u2502 " + `Project: ${projectName}`.padEnd(58) + "\u2502");
1884
+ console.log("\u2502" + " ".repeat(61) + "\u2502");
1885
+ console.log("\u2502" + " Features:".padEnd(58) + "\u2502");
1886
+ for (const feature of features) {
1887
+ console.log("\u2502" + ` \u2022 ${feature}`.slice(0, 58).padEnd(58) + "\u2502");
1888
+ }
1889
+ console.log("\u2502" + " ".repeat(61) + "\u2502");
1890
+ console.log("\u2514" + "\u2500".repeat(61) + "\u2518");
1891
+ const { confirm } = await prompts6({
1892
+ type: "confirm",
1893
+ name: "confirm",
1894
+ message: "Generate plans?",
1895
+ initial: true
1896
+ });
1897
+ if (!confirm) {
1898
+ printInfo("Cancelled");
1899
+ return;
1900
+ }
1901
+ console.log();
1902
+ console.log("Generating implementation plan...");
1903
+ const devPlan = generateDevelopmentPlan(projectName, description, features);
1904
+ const aiPrompt = generateAIPrompt(projectName, description, features);
1905
+ const projectDir = process.cwd();
1906
+ await fs7.writeFile(path7.join(projectDir, "DEVELOPMENT_PLAN.md"), devPlan);
1907
+ await fs7.writeFile(path7.join(projectDir, "AI_PROMPT.md"), aiPrompt);
1908
+ await saveProjectData(projectName, "plan.md", devPlan);
1909
+ await saveProjectData(projectName, "ai-prompt.md", aiPrompt);
1910
+ printSuccess("Plan saved to: ./DEVELOPMENT_PLAN.md");
1911
+ printSuccess("AI prompt saved to: ./AI_PROMPT.md");
1912
+ console.log();
1913
+ const { wantsCopy } = await prompts6({
1914
+ type: "confirm",
1915
+ name: "wantsCopy",
1916
+ message: "Copy AI prompt to clipboard?",
1917
+ initial: true
1918
+ });
1919
+ if (wantsCopy) {
1920
+ const copied = await copyToClipboard(aiPrompt);
1921
+ if (copied) {
1922
+ printSuccess("Copied to clipboard!");
1923
+ } else {
1924
+ printInfo("Could not copy to clipboard. Open AI_PROMPT.md manually.");
1925
+ }
1926
+ }
1927
+ console.log();
1928
+ printBox("Next Steps", [
1929
+ "1. Review DEVELOPMENT_PLAN.md with your team",
1930
+ "2. Use AI_PROMPT.md with Claude, GPT, or Cursor",
1931
+ "3. The AI prompt includes critical Scaffoldry",
1932
+ " constraints for compliant code generation"
1933
+ ]);
1934
+ }
1935
+
1936
+ export {
1937
+ generateProjectFiles,
1938
+ initCommand,
1939
+ loginCommand,
1940
+ logoutCommand,
1941
+ renameCommand,
1942
+ createAppCommand,
1943
+ upgradeCommand,
1944
+ migrateCommand,
1945
+ migrateCreateCommand,
1946
+ devCommand,
1947
+ doctorCommand,
1948
+ secretSetCommand,
1949
+ secretRotateCommand,
1950
+ planCommand
1951
+ };
1952
+ //# sourceMappingURL=chunk-AA2UXYNR.js.map