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,1216 @@
1
+ import {
2
+ getInstallInstructions,
3
+ openBrowser
4
+ } from "./chunk-WOS3F5LR.js";
5
+
6
+ // src/utils/logger.ts
7
+ import chalk from "chalk";
8
+ var logger = {
9
+ info: (message) => {
10
+ console.log(chalk.blue("info"), message);
11
+ },
12
+ success: (message) => {
13
+ console.log(chalk.green("success"), message);
14
+ },
15
+ warn: (message) => {
16
+ console.log(chalk.yellow("warn"), message);
17
+ },
18
+ error: (message) => {
19
+ console.log(chalk.red("error"), message);
20
+ },
21
+ log: (message) => {
22
+ console.log(message);
23
+ },
24
+ newLine: () => {
25
+ console.log();
26
+ }
27
+ };
28
+
29
+ // src/utils/template.ts
30
+ function createTemplateContext(config) {
31
+ return {
32
+ projectName: config.name,
33
+ projectDescription: config.description,
34
+ features: config.features,
35
+ databaseProvider: config.database,
36
+ packageManager: config.packageManager,
37
+ hasAuth: config.features.includes("auth"),
38
+ hasBilling: config.features.includes("billing"),
39
+ hasEmail: config.features.includes("email"),
40
+ hasStorage: config.features.includes("storage"),
41
+ hasJobs: config.features.includes("jobs"),
42
+ hasWebhooks: config.features.includes("webhooks"),
43
+ hasAdmin: config.features.includes("admin")
44
+ };
45
+ }
46
+ function toKebabCase(str) {
47
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
48
+ }
49
+ function toPascalCase(str) {
50
+ return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
51
+ }
52
+ function toCamelCase(str) {
53
+ const pascal = toPascalCase(str);
54
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
55
+ }
56
+ function replaceTemplateVars(content, context) {
57
+ return content.replace(/\{\{projectName\}\}/g, context.projectName).replace(/\{\{projectDescription\}\}/g, context.projectDescription).replace(/\{\{pascalName\}\}/g, toPascalCase(context.projectName)).replace(/\{\{camelName\}\}/g, toCamelCase(context.projectName)).replace(/\{\{kebabName\}\}/g, toKebabCase(context.projectName));
58
+ }
59
+
60
+ // src/utils/license.ts
61
+ import fs from "fs-extra";
62
+ import path from "path";
63
+ import os from "os";
64
+ var CONFIG_DIR = path.join(os.homedir(), ".scaffoldry");
65
+ var LICENSE_FILE = path.join(CONFIG_DIR, "license.json");
66
+ var API_BASE_URL = process.env.SCAFFOLDRY_API_URL || "https://scaffoldry.com";
67
+ async function getStoredLicense() {
68
+ try {
69
+ if (await fs.pathExists(LICENSE_FILE)) {
70
+ const config = await fs.readJson(LICENSE_FILE);
71
+ return config;
72
+ }
73
+ } catch {
74
+ }
75
+ return null;
76
+ }
77
+ async function storeLicense(config) {
78
+ await fs.ensureDir(CONFIG_DIR);
79
+ await fs.writeJson(LICENSE_FILE, config, { spaces: 2 });
80
+ }
81
+ async function clearLicense() {
82
+ try {
83
+ await fs.remove(LICENSE_FILE);
84
+ } catch {
85
+ }
86
+ }
87
+ function isValidLicenseKeyFormat(key) {
88
+ const pattern = /^SCAF-[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}$/;
89
+ return pattern.test(key.toUpperCase());
90
+ }
91
+ async function validateLicense(licenseKey) {
92
+ const machineId = await getMachineId();
93
+ try {
94
+ const response = await fetch(`${API_BASE_URL}/api/license/validate`, {
95
+ method: "POST",
96
+ headers: {
97
+ "Content-Type": "application/json",
98
+ "User-Agent": "scaffoldry-cli/1.0.0"
99
+ },
100
+ body: JSON.stringify({
101
+ licenseKey,
102
+ machineId
103
+ })
104
+ });
105
+ const data = await response.json();
106
+ return data;
107
+ } catch {
108
+ const stored = await getStoredLicense();
109
+ if (stored && stored.key === licenseKey) {
110
+ return {
111
+ valid: true,
112
+ email: stored.email
113
+ };
114
+ }
115
+ return {
116
+ valid: false,
117
+ email: void 0,
118
+ error: "Unable to validate license. Please check your internet connection."
119
+ };
120
+ }
121
+ }
122
+ async function getMachineId() {
123
+ const hostname = os.hostname();
124
+ const username = os.userInfo().username;
125
+ const platform = os.platform();
126
+ const str = `${hostname}:${username}:${platform}`;
127
+ let hash = 0;
128
+ for (let i = 0; i < str.length; i++) {
129
+ const char = str.charCodeAt(i);
130
+ hash = (hash << 5) - hash + char;
131
+ hash = hash & hash;
132
+ }
133
+ return `machine-${Math.abs(hash).toString(16)}`;
134
+ }
135
+
136
+ // src/utils/env.ts
137
+ import fs2 from "fs-extra";
138
+ import path2 from "path";
139
+ function parseEnvFile(content) {
140
+ const env = {};
141
+ const lines = content.split("\n");
142
+ for (const line of lines) {
143
+ const trimmed = line.trim();
144
+ if (!trimmed || trimmed.startsWith("#")) {
145
+ continue;
146
+ }
147
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
148
+ if (match && match[1] && match[2] !== void 0) {
149
+ const key = match[1];
150
+ let value = match[2];
151
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
152
+ value = value.slice(1, -1);
153
+ }
154
+ env[key] = value;
155
+ }
156
+ }
157
+ return env;
158
+ }
159
+ function serializeEnvFile(env, originalContent) {
160
+ if (!originalContent) {
161
+ return Object.entries(env).map(([key, value]) => {
162
+ if (value.includes(" ") || value.includes("#") || value.includes("=")) {
163
+ return `${key}="${value}"`;
164
+ }
165
+ return `${key}=${value}`;
166
+ }).join("\n") + "\n";
167
+ }
168
+ const lines = originalContent.split("\n");
169
+ const result = [];
170
+ const writtenKeys = /* @__PURE__ */ new Set();
171
+ for (const line of lines) {
172
+ const trimmed = line.trim();
173
+ if (!trimmed || trimmed.startsWith("#")) {
174
+ result.push(line);
175
+ continue;
176
+ }
177
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=/);
178
+ if (match && match[1]) {
179
+ const key = match[1];
180
+ const value = env[key];
181
+ if (value !== void 0) {
182
+ if (value.includes(" ") || value.includes("#") || value.includes("=")) {
183
+ result.push(`${key}="${value}"`);
184
+ } else {
185
+ result.push(`${key}=${value}`);
186
+ }
187
+ writtenKeys.add(key);
188
+ } else {
189
+ result.push(line);
190
+ }
191
+ } else {
192
+ result.push(line);
193
+ }
194
+ }
195
+ for (const [key, value] of Object.entries(env)) {
196
+ if (!writtenKeys.has(key)) {
197
+ if (value.includes(" ") || value.includes("#") || value.includes("=")) {
198
+ result.push(`${key}="${value}"`);
199
+ } else {
200
+ result.push(`${key}=${value}`);
201
+ }
202
+ }
203
+ }
204
+ return result.join("\n");
205
+ }
206
+ async function readEnvLocal(projectDir) {
207
+ const envPath = path2.join(projectDir, ".env.local");
208
+ if (!await fs2.pathExists(envPath)) {
209
+ return {};
210
+ }
211
+ const content = await fs2.readFile(envPath, "utf-8");
212
+ return parseEnvFile(content);
213
+ }
214
+ async function writeEnvLocal(projectDir, updates) {
215
+ const envPath = path2.join(projectDir, ".env.local");
216
+ let originalContent;
217
+ if (await fs2.pathExists(envPath)) {
218
+ originalContent = await fs2.readFile(envPath, "utf-8");
219
+ const existing = parseEnvFile(originalContent);
220
+ updates = { ...existing, ...updates };
221
+ }
222
+ const newContent = serializeEnvFile(updates, originalContent);
223
+ await fs2.writeFile(envPath, newContent);
224
+ }
225
+
226
+ // src/utils/config.ts
227
+ import fs3 from "fs-extra";
228
+ import path3 from "path";
229
+ import os2 from "os";
230
+ var CONFIG_DIR2 = path3.join(os2.homedir(), ".scaffoldry");
231
+ var CONFIG_FILE = path3.join(CONFIG_DIR2, "config.json");
232
+ async function getUserConfig() {
233
+ try {
234
+ if (await fs3.pathExists(CONFIG_FILE)) {
235
+ const content = await fs3.readFile(CONFIG_FILE, "utf-8");
236
+ return JSON.parse(content);
237
+ }
238
+ } catch {
239
+ }
240
+ return {};
241
+ }
242
+ async function setUserConfig(config) {
243
+ await fs3.ensureDir(CONFIG_DIR2);
244
+ await fs3.writeJson(CONFIG_FILE, config, { spaces: 2 });
245
+ }
246
+ async function updateUserConfig(updates) {
247
+ const current = await getUserConfig();
248
+ await setUserConfig({ ...current, ...updates });
249
+ }
250
+ async function getConfigValue(key) {
251
+ const config = await getUserConfig();
252
+ return config[key];
253
+ }
254
+ async function setConfigValue(key, value) {
255
+ await updateUserConfig({ [key]: value });
256
+ }
257
+ function getProjectDataDir(projectName) {
258
+ return path3.join(CONFIG_DIR2, "projects", projectName);
259
+ }
260
+ async function saveProjectData(projectName, filename, data) {
261
+ const projectDir = getProjectDataDir(projectName);
262
+ await fs3.ensureDir(projectDir);
263
+ await fs3.writeFile(path3.join(projectDir, filename), data);
264
+ }
265
+
266
+ // src/utils/diagnostics.ts
267
+ import { exec } from "child_process";
268
+ import { promisify } from "util";
269
+ import fs4 from "fs-extra";
270
+ import path4 from "path";
271
+ var execAsync = promisify(exec);
272
+ async function checkNodeVersion() {
273
+ try {
274
+ const { stdout } = await execAsync("node --version");
275
+ const version = stdout.trim().replace("v", "");
276
+ const major = parseInt(version.split(".")[0] || "0", 10);
277
+ if (major >= 20) {
278
+ return { status: "pass", label: `Node.js ${version}` };
279
+ }
280
+ if (major >= 18) {
281
+ return {
282
+ status: "warn",
283
+ label: `Node.js ${version}`,
284
+ message: "Node.js 20+ recommended",
285
+ fix: getInstallInstructions("node")
286
+ };
287
+ }
288
+ return {
289
+ status: "fail",
290
+ label: `Node.js ${version}`,
291
+ message: "Node.js 18+ required",
292
+ fix: getInstallInstructions("node")
293
+ };
294
+ } catch {
295
+ return {
296
+ status: "fail",
297
+ label: "Node.js",
298
+ message: "Not installed",
299
+ fix: getInstallInstructions("node")
300
+ };
301
+ }
302
+ }
303
+ async function checkPnpmVersion() {
304
+ try {
305
+ const { stdout } = await execAsync("pnpm --version");
306
+ const version = stdout.trim();
307
+ const major = parseInt(version.split(".")[0] || "0", 10);
308
+ if (major >= 9) {
309
+ return { status: "pass", label: `pnpm ${version}` };
310
+ }
311
+ if (major >= 8) {
312
+ return {
313
+ status: "warn",
314
+ label: `pnpm ${version}`,
315
+ message: "pnpm 9+ recommended",
316
+ fix: getInstallInstructions("pnpm")
317
+ };
318
+ }
319
+ return {
320
+ status: "fail",
321
+ label: `pnpm ${version}`,
322
+ message: "pnpm 8+ required",
323
+ fix: getInstallInstructions("pnpm")
324
+ };
325
+ } catch {
326
+ return {
327
+ status: "fail",
328
+ label: "pnpm",
329
+ message: "Not installed",
330
+ fix: getInstallInstructions("pnpm")
331
+ };
332
+ }
333
+ }
334
+ async function checkGitInstalled() {
335
+ try {
336
+ const { stdout } = await execAsync("git --version");
337
+ const match = stdout.match(/git version ([\d.]+)/);
338
+ const version = match ? match[1] : "unknown";
339
+ return { status: "pass", label: `Git ${version}` };
340
+ } catch {
341
+ return {
342
+ status: "fail",
343
+ label: "Git",
344
+ message: "Not installed",
345
+ fix: getInstallInstructions("git")
346
+ };
347
+ }
348
+ }
349
+ async function checkStripeCli() {
350
+ try {
351
+ const { stdout } = await execAsync("stripe --version");
352
+ const match = stdout.match(/stripe version ([\d.]+)/i) || stdout.match(/([\d.]+)/);
353
+ const version = match ? match[1] : "unknown";
354
+ return { status: "pass", label: `Stripe CLI ${version}` };
355
+ } catch {
356
+ return {
357
+ status: "warn",
358
+ label: "Stripe CLI",
359
+ message: "Not installed (required for local webhook testing)",
360
+ fix: getInstallInstructions("stripe-cli")
361
+ };
362
+ }
363
+ }
364
+ async function checkEnvVar(projectDir, key, options) {
365
+ const env = await readEnvLocal(projectDir);
366
+ const value = env[key];
367
+ if (!value) {
368
+ if (options?.required === false) {
369
+ return { status: "pass", label: key, message: "Not configured (optional)" };
370
+ }
371
+ return {
372
+ status: "fail",
373
+ label: key,
374
+ message: "Not configured",
375
+ fix: `Run: scaffoldry setup ${getSetupCommandForEnvVar(key)}`
376
+ };
377
+ }
378
+ if (options?.pattern && !options.pattern.test(value)) {
379
+ return {
380
+ status: "warn",
381
+ label: key,
382
+ message: "Invalid format",
383
+ fix: `Run: scaffoldry setup ${getSetupCommandForEnvVar(key)}`
384
+ };
385
+ }
386
+ const masked = value.length > 8 ? `${value.slice(0, 4)}${"\u2022".repeat(Math.min(12, value.length - 8))}${value.slice(-4)}` : "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
387
+ return { status: "pass", label: key, message: masked };
388
+ }
389
+ function getSetupCommandForEnvVar(key) {
390
+ if (key.includes("STRIPE")) return "stripe";
391
+ if (key.includes("DATABASE") || key.includes("DB_")) return "database";
392
+ if (key.includes("RESEND") || key.includes("EMAIL")) return "email";
393
+ if (key.includes("S3") || key.includes("AWS") || key.includes("STORAGE")) return "storage";
394
+ return "all";
395
+ }
396
+ async function checkDatabaseConnection(projectDir) {
397
+ const env = await readEnvLocal(projectDir);
398
+ const dbUrl = env.DATABASE_URL;
399
+ if (!dbUrl) {
400
+ return {
401
+ status: "fail",
402
+ label: "Database connection",
403
+ message: "DATABASE_URL not configured",
404
+ fix: "Run: scaffoldry setup database"
405
+ };
406
+ }
407
+ try {
408
+ const { neon } = await import("@neondatabase/serverless");
409
+ const sql = neon(dbUrl);
410
+ await sql`SELECT 1`;
411
+ return { status: "pass", label: "Database connection" };
412
+ } catch (error) {
413
+ const message = error instanceof Error ? error.message : "Connection failed";
414
+ return {
415
+ status: "fail",
416
+ label: "Database connection",
417
+ message: message.slice(0, 50),
418
+ fix: "Check your DATABASE_URL and network connection"
419
+ };
420
+ }
421
+ }
422
+ async function checkStripeApiKey(projectDir) {
423
+ const env = await readEnvLocal(projectDir);
424
+ const apiKey = env.STRIPE_SECRET_KEY;
425
+ if (!apiKey) {
426
+ return {
427
+ status: "fail",
428
+ label: "Stripe API",
429
+ message: "STRIPE_SECRET_KEY not configured",
430
+ fix: "Run: scaffoldry setup stripe"
431
+ };
432
+ }
433
+ try {
434
+ const response = await fetch("https://api.stripe.com/v1/balance", {
435
+ headers: {
436
+ Authorization: `Bearer ${apiKey}`
437
+ }
438
+ });
439
+ if (response.ok) {
440
+ return { status: "pass", label: "Stripe API" };
441
+ }
442
+ const data = await response.json();
443
+ return {
444
+ status: "fail",
445
+ label: "Stripe API",
446
+ message: data.error?.message || "Invalid API key",
447
+ fix: "Run: scaffoldry setup stripe"
448
+ };
449
+ } catch {
450
+ return {
451
+ status: "warn",
452
+ label: "Stripe API",
453
+ message: "Could not verify (network error)"
454
+ };
455
+ }
456
+ }
457
+ async function checkResendApiKey(projectDir) {
458
+ const env = await readEnvLocal(projectDir);
459
+ const apiKey = env.RESEND_API_KEY;
460
+ if (!apiKey) {
461
+ return {
462
+ status: "fail",
463
+ label: "Resend API",
464
+ message: "RESEND_API_KEY not configured",
465
+ fix: "Run: scaffoldry setup email"
466
+ };
467
+ }
468
+ try {
469
+ const response = await fetch("https://api.resend.com/domains", {
470
+ headers: {
471
+ Authorization: `Bearer ${apiKey}`
472
+ }
473
+ });
474
+ if (response.ok) {
475
+ return { status: "pass", label: "Resend API" };
476
+ }
477
+ if (response.status === 401) {
478
+ return {
479
+ status: "fail",
480
+ label: "Resend API",
481
+ message: "Invalid API key (401 Unauthorized)",
482
+ fix: "Run: scaffoldry setup email"
483
+ };
484
+ }
485
+ return {
486
+ status: "warn",
487
+ label: "Resend API",
488
+ message: `API returned ${response.status}`
489
+ };
490
+ } catch {
491
+ return {
492
+ status: "warn",
493
+ label: "Resend API",
494
+ message: "Could not verify (network error)"
495
+ };
496
+ }
497
+ }
498
+ async function checkEnvLocalExists(projectDir) {
499
+ const envPath = path4.join(projectDir, ".env.local");
500
+ if (await fs4.pathExists(envPath)) {
501
+ return { status: "pass", label: ".env.local file" };
502
+ }
503
+ return {
504
+ status: "fail",
505
+ label: ".env.local file",
506
+ message: "Not found",
507
+ fix: "Run: scaffoldry setup all"
508
+ };
509
+ }
510
+ async function runEnvironmentChecks() {
511
+ const results = await Promise.all([
512
+ checkNodeVersion(),
513
+ checkPnpmVersion(),
514
+ checkGitInstalled(),
515
+ checkStripeCli()
516
+ ]);
517
+ return results;
518
+ }
519
+ async function runConfigurationChecks(projectDir) {
520
+ const results = [];
521
+ results.push(await checkEnvLocalExists(projectDir));
522
+ results.push(await checkEnvVar(projectDir, "DATABASE_URL"));
523
+ results.push(await checkEnvVar(projectDir, "STRIPE_SECRET_KEY"));
524
+ results.push(await checkEnvVar(projectDir, "STRIPE_WEBHOOK_SECRET"));
525
+ results.push(await checkEnvVar(projectDir, "RESEND_API_KEY"));
526
+ results.push(await checkEnvVar(projectDir, "AUTH_SECRET"));
527
+ return results;
528
+ }
529
+ async function runServiceChecks(projectDir) {
530
+ const results = await Promise.all([
531
+ checkDatabaseConnection(projectDir),
532
+ checkStripeApiKey(projectDir),
533
+ checkResendApiKey(projectDir)
534
+ ]);
535
+ return results;
536
+ }
537
+
538
+ // src/wizards/stripe.ts
539
+ import ora from "ora";
540
+
541
+ // src/wizards/base.ts
542
+ import prompts from "prompts";
543
+ import chalk2 from "chalk";
544
+ function printBox(title, content) {
545
+ const width = 61;
546
+ const topBorder = "\u250C" + "\u2500".repeat(width) + "\u2510";
547
+ const bottomBorder = "\u2514" + "\u2500".repeat(width) + "\u2518";
548
+ console.log(chalk2.cyan(topBorder));
549
+ console.log(chalk2.cyan("\u2502") + " ".repeat(width) + chalk2.cyan("\u2502"));
550
+ console.log(chalk2.cyan("\u2502") + " " + chalk2.bold(title).padEnd(width - 3) + chalk2.cyan("\u2502"));
551
+ console.log(chalk2.cyan("\u2502") + " ".repeat(width) + chalk2.cyan("\u2502"));
552
+ for (const line of content) {
553
+ const paddedLine = " " + line;
554
+ console.log(chalk2.cyan("\u2502") + paddedLine.padEnd(width) + chalk2.cyan("\u2502"));
555
+ }
556
+ console.log(chalk2.cyan("\u2502") + " ".repeat(width) + chalk2.cyan("\u2502"));
557
+ console.log(chalk2.cyan(bottomBorder));
558
+ }
559
+ function printStep(stepNumber, title) {
560
+ console.log();
561
+ console.log(chalk2.bold(` STEP ${stepNumber}: ${title}`));
562
+ console.log(" " + "\u2500".repeat(53));
563
+ }
564
+ function printSuccess(message) {
565
+ console.log(chalk2.green(" \u2713 " + message));
566
+ }
567
+ function printInfo(message) {
568
+ console.log(chalk2.blue(" \u2192 " + message));
569
+ }
570
+ function printWarning(message) {
571
+ console.log(chalk2.yellow(" \u26A0 " + message));
572
+ }
573
+ function printError(message) {
574
+ console.log(chalk2.red(" \u2717 " + message));
575
+ }
576
+ async function maybeOpenBrowser(url, description) {
577
+ const autoOpen = await getConfigValue("openBrowserAutomatically");
578
+ if (autoOpen === true) {
579
+ await openBrowser(url);
580
+ printInfo(`Opened: ${url}`);
581
+ return;
582
+ }
583
+ if (autoOpen === false) {
584
+ printInfo(`URL: ${url}`);
585
+ return;
586
+ }
587
+ const { openIt, remember } = await prompts([
588
+ {
589
+ type: "confirm",
590
+ name: "openIt",
591
+ message: `Open ${description} in browser?`,
592
+ initial: true
593
+ },
594
+ {
595
+ type: "confirm",
596
+ name: "remember",
597
+ message: "Remember this preference?",
598
+ initial: true
599
+ }
600
+ ]);
601
+ if (remember) {
602
+ await setConfigValue("openBrowserAutomatically", openIt);
603
+ }
604
+ if (openIt) {
605
+ await openBrowser(url);
606
+ printInfo(`Opened: ${url}`);
607
+ } else {
608
+ printInfo(`URL: ${url}`);
609
+ }
610
+ }
611
+ async function promptSecret(message) {
612
+ const { value } = await prompts({
613
+ type: "password",
614
+ name: "value",
615
+ message
616
+ });
617
+ return value;
618
+ }
619
+ async function promptText(message, options) {
620
+ const { value } = await prompts({
621
+ type: "text",
622
+ name: "value",
623
+ message,
624
+ initial: options?.initial,
625
+ validate: options?.validate
626
+ });
627
+ return value;
628
+ }
629
+ async function promptConfirm(message, initial = true) {
630
+ const { value } = await prompts({
631
+ type: "confirm",
632
+ name: "value",
633
+ message,
634
+ initial
635
+ });
636
+ return value ?? false;
637
+ }
638
+ async function saveWizardResults(projectDir, envVars) {
639
+ await writeEnvLocal(projectDir, envVars);
640
+ logger.newLine();
641
+ printSuccess("Environment variables saved to .env.local");
642
+ }
643
+
644
+ // src/wizards/stripe.ts
645
+ var STRIPE_DASHBOARD_URL = "https://dashboard.stripe.com/apikeys";
646
+ var STRIPE_WEBHOOK_URL = "https://dashboard.stripe.com/webhooks";
647
+ async function verifyStripeKey(apiKey) {
648
+ try {
649
+ const response = await fetch("https://api.stripe.com/v1/account", {
650
+ headers: {
651
+ Authorization: `Bearer ${apiKey}`
652
+ }
653
+ });
654
+ if (response.ok) {
655
+ const data = await response.json();
656
+ const account = { id: data.id };
657
+ if (data.business_profile?.name) {
658
+ account.businessName = data.business_profile.name;
659
+ }
660
+ if (data.email) {
661
+ account.email = data.email;
662
+ }
663
+ return { valid: true, account };
664
+ }
665
+ const errorData = await response.json();
666
+ return {
667
+ valid: false,
668
+ error: errorData.error?.message || `API returned ${response.status}`
669
+ };
670
+ } catch (error) {
671
+ return {
672
+ valid: false,
673
+ error: error instanceof Error ? error.message : "Network error"
674
+ };
675
+ }
676
+ }
677
+ function isValidStripeSecretKey(key) {
678
+ return /^sk_(test|live)_[A-Za-z0-9]+$/.test(key);
679
+ }
680
+ function isValidWebhookSecret(secret) {
681
+ return /^whsec_[A-Za-z0-9]+$/.test(secret);
682
+ }
683
+ async function stripeWizard(projectDir) {
684
+ console.log();
685
+ printBox("Stripe Setup", [
686
+ "Let's configure Stripe for your billing system.",
687
+ "",
688
+ "You'll need:",
689
+ "\u2022 Stripe Secret Key (sk_test_... or sk_live_...)",
690
+ "\u2022 Webhook Signing Secret (whsec_...)"
691
+ ]);
692
+ printStep(1, "Get your API keys");
693
+ console.log(" 1. Go to: https://dashboard.stripe.com/apikeys");
694
+ console.log(' 2. Copy your "Secret key" (starts with sk_test_ or sk_)');
695
+ console.log();
696
+ await maybeOpenBrowser(STRIPE_DASHBOARD_URL, "Stripe dashboard");
697
+ console.log();
698
+ const secretKey = await promptSecret("Paste your Stripe Secret Key:");
699
+ if (!secretKey) {
700
+ printError("Stripe secret key is required");
701
+ return { success: false, message: "Cancelled" };
702
+ }
703
+ if (!isValidStripeSecretKey(secretKey)) {
704
+ printError("Invalid key format. Expected: sk_test_... or sk_live_...");
705
+ return { success: false, message: "Invalid key format" };
706
+ }
707
+ const spinner = ora("Verifying with Stripe API...").start();
708
+ printInfo("Calling stripe.account.retrieve()...");
709
+ const verification = await verifyStripeKey(secretKey);
710
+ if (!verification.valid) {
711
+ spinner.fail("Key verification failed");
712
+ printError(verification.error || "Unknown error");
713
+ return { success: false, message: verification.error || "Verification failed" };
714
+ }
715
+ const accountName = verification.account?.businessName || verification.account?.email || verification.account?.id;
716
+ spinner.succeed(`Key verified! Account: ${accountName}`);
717
+ printStep(2, "Webhook endpoint");
718
+ console.log(" For local development, we'll use Stripe CLI.");
719
+ console.log(" For production, add: https://your-domain.com/api/webhooks/stripe");
720
+ console.log();
721
+ console.log(" To get your webhook signing secret:");
722
+ console.log(" \u2022 Local dev: Run `stripe listen` to get whsec_...");
723
+ console.log(" \u2022 Production: Copy from Stripe Dashboard > Webhooks");
724
+ console.log();
725
+ const { setupWebhook } = { setupWebhook: await promptConfirm("Do you have a webhook signing secret?", false) };
726
+ let webhookSecret;
727
+ if (setupWebhook) {
728
+ await maybeOpenBrowser(STRIPE_WEBHOOK_URL, "Stripe webhooks");
729
+ console.log();
730
+ webhookSecret = await promptSecret("Paste your Webhook Signing Secret:");
731
+ if (webhookSecret && !isValidWebhookSecret(webhookSecret)) {
732
+ printError("Invalid webhook secret format. Expected: whsec_...");
733
+ webhookSecret = void 0;
734
+ }
735
+ }
736
+ if (!webhookSecret) {
737
+ printInfo("Skipping webhook secret for now.");
738
+ printInfo("For local dev, run: stripe listen --forward-to localhost:3000/api/webhooks/stripe");
739
+ webhookSecret = "whsec_PLACEHOLDER_RUN_STRIPE_LISTEN";
740
+ }
741
+ const envVars = {
742
+ STRIPE_SECRET_KEY: secretKey,
743
+ STRIPE_WEBHOOK_SECRET: webhookSecret
744
+ };
745
+ const isTestMode = secretKey.startsWith("sk_test_");
746
+ if (isTestMode) {
747
+ console.log();
748
+ printInfo("Tip: Also add your publishable key for client-side Stripe.js");
749
+ const publishableKey = await promptSecret("Paste your Stripe Publishable Key (optional):");
750
+ if (publishableKey && publishableKey.startsWith("pk_test_")) {
751
+ envVars.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY = publishableKey;
752
+ }
753
+ }
754
+ await saveWizardResults(projectDir, envVars);
755
+ console.log();
756
+ printSuccess("Stripe setup complete!");
757
+ if (webhookSecret === "whsec_PLACEHOLDER_RUN_STRIPE_LISTEN") {
758
+ console.log();
759
+ printInfo("Next: Run `scaffoldry dev` to start with Stripe webhook forwarding");
760
+ }
761
+ return {
762
+ success: true,
763
+ envVars
764
+ };
765
+ }
766
+
767
+ // src/wizards/database.ts
768
+ import ora2 from "ora";
769
+ var NEON_DASHBOARD_URL = "https://console.neon.tech";
770
+ async function verifyDatabaseConnection(connectionString) {
771
+ try {
772
+ const { neon } = await import("@neondatabase/serverless");
773
+ const sql = neon(connectionString);
774
+ await sql`SELECT 1 as test`;
775
+ return { valid: true };
776
+ } catch (error) {
777
+ return {
778
+ valid: false,
779
+ error: error instanceof Error ? error.message : "Connection failed"
780
+ };
781
+ }
782
+ }
783
+ function isValidConnectionString(url) {
784
+ try {
785
+ const parsed = new URL(url);
786
+ return (parsed.protocol === "postgres:" || parsed.protocol === "postgresql:") && parsed.hostname.length > 0;
787
+ } catch {
788
+ return false;
789
+ }
790
+ }
791
+ function extractHost(url) {
792
+ try {
793
+ const parsed = new URL(url);
794
+ return parsed.hostname;
795
+ } catch {
796
+ return "unknown";
797
+ }
798
+ }
799
+ async function databaseWizard(projectDir) {
800
+ console.log();
801
+ printBox("Database Setup", [
802
+ "Let's configure your Neon PostgreSQL database.",
803
+ "",
804
+ "Scaffoldry uses Neon for serverless PostgreSQL.",
805
+ "It's free to start and scales automatically."
806
+ ]);
807
+ printStep(1, "Get your Neon connection string");
808
+ console.log(" 1. Sign in at: https://console.neon.tech");
809
+ console.log(" 2. Create a project (or select existing)");
810
+ console.log(" 3. Copy the connection string from the Dashboard");
811
+ console.log(" (Format: postgresql://user:pass@host/dbname)");
812
+ console.log();
813
+ await maybeOpenBrowser(NEON_DASHBOARD_URL, "Neon console");
814
+ console.log();
815
+ const connectionString = await promptSecret("Paste your DATABASE_URL:");
816
+ if (!connectionString) {
817
+ printError("Database connection string is required");
818
+ return { success: false, message: "Cancelled" };
819
+ }
820
+ if (!isValidConnectionString(connectionString)) {
821
+ printError("Invalid connection string format");
822
+ printInfo("Expected: postgresql://user:password@host/database");
823
+ return { success: false, message: "Invalid format" };
824
+ }
825
+ const spinner = ora2("Verifying database connection...").start();
826
+ printInfo("Executing: SELECT 1");
827
+ const verification = await verifyDatabaseConnection(connectionString);
828
+ if (!verification.valid) {
829
+ spinner.fail("Connection failed");
830
+ printError(verification.error || "Unknown error");
831
+ console.log();
832
+ printInfo("Troubleshooting tips:");
833
+ console.log(" \u2022 Check that your connection string is correct");
834
+ console.log(" \u2022 Ensure your IP is allowed (Neon > Settings > IP Allow)");
835
+ console.log(" \u2022 For Neon, make sure SSL is enabled (add ?sslmode=require)");
836
+ return { success: false, message: verification.error || "Connection failed" };
837
+ }
838
+ const host = extractHost(connectionString);
839
+ spinner.succeed(`Connected to ${host}!`);
840
+ const envVars = {
841
+ DATABASE_URL: connectionString
842
+ };
843
+ await saveWizardResults(projectDir, envVars);
844
+ console.log();
845
+ printSuccess("Database setup complete!");
846
+ console.log();
847
+ printInfo("Next: Run `scaffoldry migrate` to apply database migrations");
848
+ return {
849
+ success: true,
850
+ envVars
851
+ };
852
+ }
853
+
854
+ // src/wizards/email.ts
855
+ import ora3 from "ora";
856
+ var RESEND_DASHBOARD_URL = "https://resend.com/api-keys";
857
+ async function verifyResendKey(apiKey) {
858
+ try {
859
+ const response = await fetch("https://api.resend.com/domains", {
860
+ headers: {
861
+ Authorization: `Bearer ${apiKey}`
862
+ }
863
+ });
864
+ if (response.ok) {
865
+ const data = await response.json();
866
+ return {
867
+ valid: true,
868
+ domains: data.data || []
869
+ };
870
+ }
871
+ if (response.status === 401) {
872
+ return {
873
+ valid: false,
874
+ error: "Invalid API key (401 Unauthorized)"
875
+ };
876
+ }
877
+ return {
878
+ valid: false,
879
+ error: `API returned ${response.status}`
880
+ };
881
+ } catch (error) {
882
+ return {
883
+ valid: false,
884
+ error: error instanceof Error ? error.message : "Network error"
885
+ };
886
+ }
887
+ }
888
+ function isValidResendKey(key) {
889
+ return /^re_[A-Za-z0-9]+$/.test(key);
890
+ }
891
+ async function emailWizard(projectDir) {
892
+ console.log();
893
+ printBox("Email Setup", [
894
+ "Let's configure Resend for transactional emails.",
895
+ "",
896
+ "Resend handles:",
897
+ "\u2022 Password reset emails",
898
+ "\u2022 Magic link authentication",
899
+ "\u2022 Welcome emails and notifications"
900
+ ]);
901
+ printStep(1, "Get your Resend API key");
902
+ console.log(" 1. Sign in at: https://resend.com");
903
+ console.log(" 2. Go to API Keys in the dashboard");
904
+ console.log(" 3. Create a new API key (or use existing)");
905
+ console.log(" (Format: re_...)");
906
+ console.log();
907
+ await maybeOpenBrowser(RESEND_DASHBOARD_URL, "Resend dashboard");
908
+ console.log();
909
+ const apiKey = await promptSecret("Paste your Resend API Key:");
910
+ if (!apiKey) {
911
+ printError("Resend API key is required");
912
+ return { success: false, message: "Cancelled" };
913
+ }
914
+ if (!isValidResendKey(apiKey)) {
915
+ printError("Invalid API key format. Expected: re_...");
916
+ return { success: false, message: "Invalid format" };
917
+ }
918
+ const spinner = ora3("Verifying with Resend API...").start();
919
+ printInfo("Calling resend.domains.list()...");
920
+ const verification = await verifyResendKey(apiKey);
921
+ if (!verification.valid) {
922
+ spinner.fail("API key verification failed");
923
+ printError(verification.error || "Unknown error");
924
+ return { success: false, message: verification.error || "Verification failed" };
925
+ }
926
+ spinner.succeed("API key verified!");
927
+ if (verification.domains && verification.domains.length > 0) {
928
+ console.log();
929
+ printInfo("Verified domains:");
930
+ for (const domain of verification.domains) {
931
+ const status = domain.status === "verified" ? "\u2713" : "\u26A0";
932
+ console.log(` ${status} ${domain.name} (${domain.status})`);
933
+ }
934
+ } else {
935
+ console.log();
936
+ printInfo("No domains configured yet.");
937
+ printInfo("For production, add your domain at https://resend.com/domains");
938
+ }
939
+ printStep(2, "Configure sender address");
940
+ console.log(" For testing, you can use: onboarding@resend.dev");
941
+ console.log(" For production, use: noreply@your-verified-domain.com");
942
+ console.log();
943
+ let defaultFrom = "onboarding@resend.dev";
944
+ if (verification.domains && verification.domains.length > 0) {
945
+ const verifiedDomain = verification.domains.find((d) => d.status === "verified");
946
+ if (verifiedDomain) {
947
+ defaultFrom = `noreply@${verifiedDomain.name}`;
948
+ }
949
+ }
950
+ const emailFrom = await promptText("Default sender email:", { initial: defaultFrom });
951
+ const envVars = {
952
+ RESEND_API_KEY: apiKey
953
+ };
954
+ if (emailFrom) {
955
+ envVars.EMAIL_FROM = emailFrom;
956
+ }
957
+ await saveWizardResults(projectDir, envVars);
958
+ console.log();
959
+ printSuccess("Email setup complete!");
960
+ if (!verification.domains || verification.domains.length === 0) {
961
+ console.log();
962
+ printInfo("Important: Add a verified domain for production use");
963
+ printInfo("Visit: https://resend.com/domains");
964
+ }
965
+ return {
966
+ success: true,
967
+ envVars
968
+ };
969
+ }
970
+
971
+ // src/wizards/storage.ts
972
+ import ora4 from "ora";
973
+ var AWS_CONSOLE_URL = "https://console.aws.amazon.com/s3";
974
+ var AWS_IAM_URL = "https://console.aws.amazon.com/iam";
975
+ async function verifyS3Access(bucket, region, accessKeyId, secretAccessKey) {
976
+ try {
977
+ if (!bucket || !region || !accessKeyId || !secretAccessKey) {
978
+ return { valid: false, error: "Missing required configuration" };
979
+ }
980
+ if (!/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(bucket)) {
981
+ return { valid: false, error: "Invalid bucket name format" };
982
+ }
983
+ return { valid: true };
984
+ } catch (error) {
985
+ return {
986
+ valid: false,
987
+ error: error instanceof Error ? error.message : "Verification failed"
988
+ };
989
+ }
990
+ }
991
+ async function storageWizard(projectDir) {
992
+ console.log();
993
+ printBox("Storage Setup (AWS S3)", [
994
+ "Let's configure AWS S3 for file storage.",
995
+ "",
996
+ "S3 is used for:",
997
+ "\u2022 User file uploads",
998
+ "\u2022 Profile images and avatars",
999
+ "\u2022 Document storage",
1000
+ "",
1001
+ "Note: This feature is optional."
1002
+ ]);
1003
+ const proceed = await promptConfirm("Do you want to configure S3 storage?", true);
1004
+ if (!proceed) {
1005
+ printInfo("Skipping S3 storage setup.");
1006
+ return { success: true, message: "Skipped" };
1007
+ }
1008
+ printStep(1, "Create or select an S3 bucket");
1009
+ console.log(" 1. Sign in to AWS Console");
1010
+ console.log(" 2. Go to S3 and create a bucket (or use existing)");
1011
+ console.log(" 3. Note the bucket name and region");
1012
+ console.log();
1013
+ console.log(" Recommended settings:");
1014
+ console.log(" \u2022 Block all public access: ON");
1015
+ console.log(" \u2022 Versioning: Optional");
1016
+ console.log(" \u2022 Default encryption: Enabled");
1017
+ console.log();
1018
+ await maybeOpenBrowser(AWS_CONSOLE_URL, "AWS S3 console");
1019
+ console.log();
1020
+ const bucketName = await promptText("S3 Bucket name:", {
1021
+ validate: (value) => {
1022
+ if (!value) return "Bucket name is required";
1023
+ if (!/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(value)) {
1024
+ return "Invalid bucket name format";
1025
+ }
1026
+ return true;
1027
+ }
1028
+ });
1029
+ if (!bucketName) {
1030
+ printError("Bucket name is required");
1031
+ return { success: false, message: "Cancelled" };
1032
+ }
1033
+ const region = await promptText("AWS Region:", {
1034
+ initial: "us-east-1"
1035
+ });
1036
+ if (!region) {
1037
+ printError("Region is required");
1038
+ return { success: false, message: "Cancelled" };
1039
+ }
1040
+ printStep(2, "Create IAM credentials");
1041
+ console.log(" 1. Go to IAM in AWS Console");
1042
+ console.log(" 2. Create a new user or use existing");
1043
+ console.log(" 3. Attach S3 access policy (or use AmazonS3FullAccess)");
1044
+ console.log(" 4. Create Access Key and copy credentials");
1045
+ console.log();
1046
+ await maybeOpenBrowser(AWS_IAM_URL, "AWS IAM console");
1047
+ console.log();
1048
+ const accessKeyId = await promptText("AWS Access Key ID:");
1049
+ if (!accessKeyId) {
1050
+ printError("Access Key ID is required");
1051
+ return { success: false, message: "Cancelled" };
1052
+ }
1053
+ const secretAccessKey = await promptSecret("AWS Secret Access Key:");
1054
+ if (!secretAccessKey) {
1055
+ printError("Secret Access Key is required");
1056
+ return { success: false, message: "Cancelled" };
1057
+ }
1058
+ const spinner = ora4("Validating S3 configuration...").start();
1059
+ const verification = await verifyS3Access(bucketName, region, accessKeyId, secretAccessKey);
1060
+ if (!verification.valid) {
1061
+ spinner.fail("Configuration validation failed");
1062
+ printError(verification.error || "Unknown error");
1063
+ const continueAnyway = await promptConfirm("Save configuration anyway?", false);
1064
+ if (!continueAnyway) {
1065
+ return { success: false, message: verification.error || "Verification failed" };
1066
+ }
1067
+ printWarning("Saving unverified configuration");
1068
+ } else {
1069
+ spinner.succeed("Configuration validated!");
1070
+ }
1071
+ const envVars = {
1072
+ S3_BUCKET_NAME: bucketName,
1073
+ S3_REGION: region,
1074
+ AWS_ACCESS_KEY_ID: accessKeyId,
1075
+ AWS_SECRET_ACCESS_KEY: secretAccessKey
1076
+ };
1077
+ await saveWizardResults(projectDir, envVars);
1078
+ console.log();
1079
+ printSuccess("Storage setup complete!");
1080
+ console.log();
1081
+ printInfo("Your files will be stored in:");
1082
+ console.log(` s3://${bucketName} (${region})`);
1083
+ return {
1084
+ success: true,
1085
+ envVars
1086
+ };
1087
+ }
1088
+
1089
+ // src/commands/setup.ts
1090
+ function getProjectDir() {
1091
+ return process.cwd();
1092
+ }
1093
+ async function runWizard(service, projectDir) {
1094
+ switch (service) {
1095
+ case "stripe":
1096
+ return stripeWizard(projectDir);
1097
+ case "database":
1098
+ return databaseWizard(projectDir);
1099
+ case "email":
1100
+ return emailWizard(projectDir);
1101
+ case "storage":
1102
+ return storageWizard(projectDir);
1103
+ }
1104
+ }
1105
+ async function setupCommand(options) {
1106
+ const projectDir = getProjectDir();
1107
+ if (options.service === "all") {
1108
+ logger.newLine();
1109
+ printBox("Scaffoldry Setup", [
1110
+ "Let's configure all your services.",
1111
+ "",
1112
+ "We'll set up:",
1113
+ "\u2022 Database (Neon PostgreSQL)",
1114
+ "\u2022 Stripe (Payments)",
1115
+ "\u2022 Email (Resend)",
1116
+ "\u2022 Storage (AWS S3) - optional"
1117
+ ]);
1118
+ const services = ["database", "stripe", "email", "storage"];
1119
+ const results = [];
1120
+ for (const service of services) {
1121
+ const result = await runWizard(service, projectDir);
1122
+ results.push({ service, success: result.success });
1123
+ if (!result.success && result.message !== "Skipped") {
1124
+ logger.newLine();
1125
+ printError(`${service} setup failed: ${result.message}`);
1126
+ const { continueSetup } = await import("prompts").then(
1127
+ (m) => m.default({
1128
+ type: "confirm",
1129
+ name: "continueSetup",
1130
+ message: "Continue with other services?",
1131
+ initial: true
1132
+ })
1133
+ );
1134
+ if (!continueSetup) {
1135
+ break;
1136
+ }
1137
+ }
1138
+ }
1139
+ logger.newLine();
1140
+ printBox("Setup Complete", [
1141
+ "Here's a summary of your configuration:",
1142
+ "",
1143
+ ...results.map(
1144
+ ({ service, success }) => `${success ? "\u2713" : "\u2717"} ${service.charAt(0).toUpperCase() + service.slice(1)}`
1145
+ )
1146
+ ]);
1147
+ const successCount = results.filter((r) => r.success).length;
1148
+ if (successCount === results.length) {
1149
+ logger.newLine();
1150
+ printSuccess("All services configured!");
1151
+ printInfo("Run `scaffoldry doctor` to verify your configuration");
1152
+ printInfo("Run `scaffoldry dev` to start development");
1153
+ } else {
1154
+ logger.newLine();
1155
+ printInfo(`${successCount}/${results.length} services configured`);
1156
+ printInfo("Run `scaffoldry setup <service>` to configure remaining services");
1157
+ }
1158
+ } else {
1159
+ const result = await runWizard(options.service, projectDir);
1160
+ if (result.success) {
1161
+ logger.newLine();
1162
+ printInfo("Run `scaffoldry doctor` to verify your full configuration");
1163
+ }
1164
+ }
1165
+ }
1166
+ async function setupStripeCommand() {
1167
+ await setupCommand({ service: "stripe" });
1168
+ }
1169
+ async function setupDatabaseCommand() {
1170
+ await setupCommand({ service: "database" });
1171
+ }
1172
+ async function setupEmailCommand() {
1173
+ await setupCommand({ service: "email" });
1174
+ }
1175
+ async function setupStorageCommand() {
1176
+ await setupCommand({ service: "storage" });
1177
+ }
1178
+ async function setupAllCommand() {
1179
+ await setupCommand({ service: "all" });
1180
+ }
1181
+
1182
+ export {
1183
+ logger,
1184
+ createTemplateContext,
1185
+ toKebabCase,
1186
+ toPascalCase,
1187
+ toCamelCase,
1188
+ replaceTemplateVars,
1189
+ getStoredLicense,
1190
+ storeLicense,
1191
+ clearLicense,
1192
+ isValidLicenseKeyFormat,
1193
+ validateLicense,
1194
+ readEnvLocal,
1195
+ writeEnvLocal,
1196
+ saveProjectData,
1197
+ checkNodeVersion,
1198
+ checkPnpmVersion,
1199
+ checkStripeCli,
1200
+ checkEnvVar,
1201
+ runEnvironmentChecks,
1202
+ runConfigurationChecks,
1203
+ runServiceChecks,
1204
+ printBox,
1205
+ printSuccess,
1206
+ printInfo,
1207
+ printWarning,
1208
+ printError,
1209
+ setupCommand,
1210
+ setupStripeCommand,
1211
+ setupDatabaseCommand,
1212
+ setupEmailCommand,
1213
+ setupStorageCommand,
1214
+ setupAllCommand
1215
+ };
1216
+ //# sourceMappingURL=chunk-XIP7YNKZ.js.map