spfn 0.1.0-alpha.88 → 0.2.0-beta.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,36 +1,1085 @@
1
- import {
2
- setupCommand
3
- } from "./chunk-K5K5BTB7.js";
4
- import {
5
- initCommand
6
- } from "./chunk-526QKBO7.js";
7
- import {
8
- detectPackageManager,
9
- logger
10
- } from "./chunk-QH74KQEW.js";
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
11
10
 
12
- // src/index.ts
13
- import { Command as Command10 } from "commander";
11
+ // src/utils/logger.ts
12
+ import chalk from "chalk";
13
+ var logger;
14
+ var init_logger = __esm({
15
+ "src/utils/logger.ts"() {
16
+ "use strict";
17
+ logger = {
18
+ info: (message) => {
19
+ console.log(chalk.blue("\u2139"), message);
20
+ },
21
+ success: (message) => {
22
+ console.log(chalk.green("\u2713"), message);
23
+ },
24
+ warn: (message) => {
25
+ console.log(chalk.yellow("\u26A0"), message);
26
+ },
27
+ error: (message) => {
28
+ console.log(chalk.red("\u2717"), message);
29
+ },
30
+ step: (message) => {
31
+ console.log(chalk.cyan("\u25B8"), message);
32
+ }
33
+ };
34
+ }
35
+ });
14
36
 
15
- // src/commands/create.ts
16
- import { Command } from "commander";
37
+ // src/utils/package-manager.ts
17
38
  import { existsSync } from "fs";
18
39
  import { join } from "path";
19
- import prompts from "prompts";
40
+ function detectPackageManager(cwd) {
41
+ if (existsSync(join(cwd, "bun.lockb"))) {
42
+ return "bun";
43
+ }
44
+ if (existsSync(join(cwd, "pnpm-lock.yaml"))) {
45
+ return "pnpm";
46
+ }
47
+ if (existsSync(join(cwd, "yarn.lock"))) {
48
+ return "yarn";
49
+ }
50
+ let currentDir = cwd;
51
+ let depth = 0;
52
+ const maxDepth = 5;
53
+ while (depth < maxDepth) {
54
+ const parentDir = join(currentDir, "..");
55
+ if (parentDir === currentDir) {
56
+ break;
57
+ }
58
+ if (existsSync(join(parentDir, "pnpm-lock.yaml"))) {
59
+ return "pnpm";
60
+ }
61
+ if (existsSync(join(parentDir, "yarn.lock"))) {
62
+ return "yarn";
63
+ }
64
+ if (existsSync(join(parentDir, "bun.lockb"))) {
65
+ return "bun";
66
+ }
67
+ currentDir = parentDir;
68
+ depth++;
69
+ }
70
+ return "npm";
71
+ }
72
+ var init_package_manager = __esm({
73
+ "src/utils/package-manager.ts"() {
74
+ "use strict";
75
+ }
76
+ });
77
+
78
+ // src/commands/setup.ts
79
+ var setup_exports = {};
80
+ __export(setup_exports, {
81
+ setupCommand: () => setupCommand,
82
+ setupIcons: () => setupIcons
83
+ });
84
+ import { Command } from "commander";
85
+ import { existsSync as existsSync2 } from "fs";
86
+ import { join as join2 } from "path";
20
87
  import ora from "ora";
21
88
  import { execa } from "execa";
22
- import chalk from "chalk";
89
+ import fse from "fs-extra";
90
+ import chalk2 from "chalk";
91
+ async function setupIcons() {
92
+ const cwd = process.cwd();
93
+ logger.info("Setting up SVGR for SVG icon management...\n");
94
+ const packageJsonPath = join2(cwd, "package.json");
95
+ if (!existsSync2(packageJsonPath)) {
96
+ logger.error("No package.json found. Please run this in a Next.js project.");
97
+ process.exit(1);
98
+ }
99
+ const packageJson = JSON.parse(
100
+ readFileSync(packageJsonPath, "utf-8")
101
+ );
102
+ const hasNext = packageJson.dependencies?.next || packageJson.devDependencies?.next;
103
+ if (!hasNext) {
104
+ logger.error("Next.js not detected in dependencies. This setup is for Next.js projects only.");
105
+ process.exit(1);
106
+ }
107
+ const hasSvgr = packageJson.devDependencies?.["@svgr/webpack"];
108
+ if (hasSvgr) {
109
+ logger.warn("@svgr/webpack is already installed.");
110
+ logger.info("Skipping installation, but will create directory structure...\n");
111
+ }
112
+ if (!hasSvgr) {
113
+ const pm = detectPackageManager(cwd);
114
+ logger.step(`Detected package manager: ${pm}`);
115
+ const spinner2 = ora("Installing @svgr/webpack...").start();
116
+ try {
117
+ await execa(
118
+ pm,
119
+ pm === "npm" ? ["install", "--save-dev", "@svgr/webpack"] : ["add", "-D", "@svgr/webpack"],
120
+ { cwd }
121
+ );
122
+ spinner2.succeed("@svgr/webpack installed");
123
+ } catch (error) {
124
+ spinner2.fail("Failed to install @svgr/webpack");
125
+ logger.error(String(error));
126
+ process.exit(1);
127
+ }
128
+ }
129
+ const spinner = ora("Updating next.config...").start();
130
+ try {
131
+ const possibleConfigs = [
132
+ "next.config.ts",
133
+ "next.config.js",
134
+ "next.config.mjs"
135
+ ];
136
+ let configPath = null;
137
+ for (const config of possibleConfigs) {
138
+ const path5 = join2(cwd, config);
139
+ if (existsSync2(path5)) {
140
+ configPath = path5;
141
+ break;
142
+ }
143
+ }
144
+ if (!configPath) {
145
+ spinner.warn("next.config not found, creating next.config.ts...");
146
+ configPath = join2(cwd, "next.config.ts");
147
+ const newConfig = `import type { NextConfig } from "next";
148
+
149
+ const nextConfig: NextConfig = {
150
+ webpack(config) {
151
+ // SVGR: Import SVG as React components
152
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
153
+ const fileLoaderRule = (config.module.rules as any[])
154
+ .find((rule: any) => Array.isArray(rule.oneOf))
155
+ ?.oneOf.find((rule: any) => rule.test?.test?.('.svg'));
156
+
157
+ if (fileLoaderRule) {
158
+ fileLoaderRule.exclude = /\\.svg$/i;
159
+ }
160
+
161
+ config.module.rules.unshift({
162
+ test: /\\.svg$/i,
163
+ use: ['@svgr/webpack'],
164
+ });
165
+
166
+ return config;
167
+ },
168
+ turbopack: {
169
+ rules: {
170
+ '*.svg': {
171
+ loaders: ['@svgr/webpack'],
172
+ as: '*.js',
173
+ },
174
+ },
175
+ },
176
+ };
177
+
178
+ export default nextConfig;
179
+ `;
180
+ writeFileSync(configPath, newConfig);
181
+ spinner.succeed("Created next.config.ts with SVGR support");
182
+ } else {
183
+ let configContent = readFileSync(configPath, "utf-8");
184
+ if (configContent.includes("@svgr/webpack")) {
185
+ spinner.warn("SVGR already configured in next.config");
186
+ } else {
187
+ const hasWebpack = configContent.includes("webpack(");
188
+ const hasTurbopack = configContent.includes("turbopack:");
189
+ if (hasWebpack || hasTurbopack) {
190
+ spinner.info("Manual update required for next.config");
191
+ logger.warn("\nYou need to manually add SVGR configuration to your next.config file.");
192
+ logger.info("See: https://react-svgr.com/docs/next/");
193
+ logger.info("\nAdd this to your next.config:\n");
194
+ console.log(chalk2.gray(`
195
+ webpack(config) {
196
+ const fileLoaderRule = config.module.rules
197
+ .find(rule => rule.oneOf)
198
+ ?.oneOf.find(rule => rule.test?.test?.('.svg'));
199
+
200
+ if (fileLoaderRule) {
201
+ fileLoaderRule.exclude = /\\.svg$/i;
202
+ }
203
+
204
+ config.module.rules.unshift({
205
+ test: /\\.svg$/i,
206
+ use: ['@svgr/webpack'],
207
+ });
208
+
209
+ return config;
210
+ },
211
+ turbopack: {
212
+ rules: {
213
+ '*.svg': {
214
+ loaders: ['@svgr/webpack'],
215
+ as: '*.js',
216
+ },
217
+ },
218
+ },
219
+ `));
220
+ } else {
221
+ const webpackConfig = ` webpack(config) {
222
+ // SVGR: Import SVG as React components
223
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
224
+ const fileLoaderRule = (config.module.rules as any[])
225
+ .find((rule: any) => Array.isArray(rule.oneOf))
226
+ ?.oneOf.find((rule: any) => rule.test?.test?.('.svg'));
227
+
228
+ if (fileLoaderRule) {
229
+ fileLoaderRule.exclude = /\\.svg$/i;
230
+ }
231
+
232
+ config.module.rules.unshift({
233
+ test: /\\.svg$/i,
234
+ use: ['@svgr/webpack'],
235
+ });
236
+
237
+ return config;
238
+ },`;
239
+ const turbopackConfig = ` turbopack: {
240
+ rules: {
241
+ '*.svg': {
242
+ loaders: ['@svgr/webpack'],
243
+ as: '*.js',
244
+ },
245
+ },
246
+ },`;
247
+ const emptyConfigPattern = /const\s+\w+:\s*NextConfig\s*=\s*\{\s*\};/;
248
+ if (emptyConfigPattern.test(configContent)) {
249
+ configContent = configContent.replace(
250
+ emptyConfigPattern,
251
+ `const nextConfig: NextConfig = {
252
+ ${webpackConfig}
253
+ ${turbopackConfig}
254
+ };`
255
+ );
256
+ } else {
257
+ const configObjectPattern = /(const\s+\w+:\s*NextConfig\s*=\s*\{)([^}]*?)(\};)/s;
258
+ if (configObjectPattern.test(configContent)) {
259
+ configContent = configContent.replace(
260
+ configObjectPattern,
261
+ (_match, opening, content, closing) => {
262
+ const trimmedContent = content.trim();
263
+ if (trimmedContent) {
264
+ return `${opening}${content}
265
+ ${webpackConfig}
266
+ ${turbopackConfig}
267
+ ${closing}`;
268
+ } else {
269
+ return `${opening}
270
+ ${webpackConfig}
271
+ ${turbopackConfig}
272
+ ${closing}`;
273
+ }
274
+ }
275
+ );
276
+ }
277
+ }
278
+ writeFileSync(configPath, configContent);
279
+ spinner.succeed("Added SVGR configuration to next.config");
280
+ }
281
+ }
282
+ }
283
+ } catch (error) {
284
+ spinner.fail("Failed to update next.config");
285
+ logger.error(String(error));
286
+ }
287
+ const iconsSpinner = ora("Creating src/assets/icons/ directory...").start();
288
+ try {
289
+ const iconsDir = join2(cwd, "src", "assets", "icons");
290
+ ensureDirSync(iconsDir);
291
+ const readmePath = join2(iconsDir, "README.md");
292
+ const readmeContent = `# Icons
293
+
294
+ This directory manages SVG icons for the project.
295
+
296
+ ## Usage
297
+
298
+ Import SVG files as React components using SVGR:
299
+
300
+ \`\`\`tsx
301
+ import Logo from '@/assets/icons/logo.svg';
302
+
303
+ function MyComponent() {
304
+ return (
305
+ <Logo className="size-8 text-gray-900 dark:text-white" />
306
+ );
307
+ }
308
+ \`\`\`
309
+
310
+ ## Color Control
311
+
312
+ Use \`fill="currentColor"\` in your SVG files to control colors via Tailwind CSS:
313
+
314
+ \`\`\`tsx
315
+ // Light mode: gray-900, Dark mode: white
316
+ <Logo className="size-8 text-gray-900 dark:text-white" />
317
+
318
+ // Custom colors
319
+ <Icon className="size-6 text-blue-600" />
320
+ \`\`\`
321
+
322
+ ## Adding New Icons
323
+
324
+ 1. Add SVG file to this directory
325
+ 2. Set \`fill="currentColor"\` for color control (optional)
326
+ 3. Remove \`width\` and \`height\` attributes for flexible sizing
327
+ 4. Import and use as React component
328
+
329
+ \`\`\`tsx
330
+ import NewIcon from '@/assets/icons/new-icon.svg';
331
+ \`\`\`
332
+
333
+ ## Configuration
334
+
335
+ - **next.config.ts**: SVGR webpack loader configuration
336
+ - **Turbopack**: SVG loader rules for fast refresh
337
+
338
+ ## Example SVG Structure
339
+
340
+ \`\`\`xml
341
+ <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
342
+ <path d="..." fill="currentColor"/>
343
+ </svg>
344
+ \`\`\`
345
+
346
+ Note: Remove \`width\` and \`height\` for flexible sizing with Tailwind utilities.
347
+ `;
348
+ writeFileSync(readmePath, readmeContent);
349
+ iconsSpinner.succeed("Created src/assets/icons/ directory with README.md");
350
+ } catch (error) {
351
+ iconsSpinner.fail("Failed to create directory structure");
352
+ logger.error(String(error));
353
+ }
354
+ console.log("\n" + chalk2.green.bold("\u2713 SVGR setup completed!\n"));
355
+ console.log("Next steps:");
356
+ console.log(" 1. Add SVG files to " + chalk2.cyan("src/assets/icons/"));
357
+ console.log(" 2. Import them as React components:");
358
+ console.log(" " + chalk2.gray("import Logo from '@/assets/icons/logo.svg';"));
359
+ console.log(" 3. Use with Tailwind classes:");
360
+ console.log(" " + chalk2.gray('<Logo className="size-8 text-gray-900 dark:text-white" />'));
361
+ console.log("\nDocumentation: " + chalk2.cyan("src/assets/icons/README.md"));
362
+ }
363
+ var ensureDirSync, writeFileSync, readFileSync, setupCommand;
364
+ var init_setup = __esm({
365
+ "src/commands/setup.ts"() {
366
+ "use strict";
367
+ init_logger();
368
+ init_package_manager();
369
+ ({ ensureDirSync, writeFileSync, readFileSync } = fse);
370
+ setupCommand = new Command("setup").description("Setup additional features for your project");
371
+ setupCommand.command("icons").description("Setup SVGR for SVG icon management").action(setupIcons);
372
+ }
373
+ });
374
+
375
+ // src/commands/init/steps/validate.ts
376
+ import { existsSync as existsSync3 } from "fs";
377
+ import { join as join3 } from "path";
378
+ import prompts from "prompts";
379
+ async function validateProject(cwd, skipPrompts) {
380
+ const packageJsonPath = join3(cwd, "package.json");
381
+ if (!existsSync3(packageJsonPath)) {
382
+ logger.error("No package.json found. Please run this in a Next.js project.");
383
+ process.exit(1);
384
+ }
385
+ const packageJson = JSON.parse(await import("fs").then(
386
+ (fs5) => fs5.promises.readFile(packageJsonPath, "utf-8")
387
+ ));
388
+ const hasNext = packageJson.dependencies?.next || packageJson.devDependencies?.next;
389
+ if (!hasNext) {
390
+ logger.warn("Next.js not detected in dependencies.");
391
+ if (!skipPrompts) {
392
+ const { proceed } = await prompts(
393
+ {
394
+ type: "confirm",
395
+ name: "proceed",
396
+ message: "Continue anyway?",
397
+ initial: false
398
+ }
399
+ );
400
+ if (!proceed) {
401
+ process.exit(0);
402
+ }
403
+ }
404
+ }
405
+ logger.info("Initializing SPFN in your Next.js project...\n");
406
+ if (existsSync3(join3(cwd, "src", "server"))) {
407
+ logger.warn("src/server directory already exists.");
408
+ if (!skipPrompts) {
409
+ const { overwrite } = await prompts(
410
+ {
411
+ type: "confirm",
412
+ name: "overwrite",
413
+ message: "Overwrite existing files?",
414
+ initial: false
415
+ }
416
+ );
417
+ if (!overwrite) {
418
+ logger.info("Cancelled.");
419
+ process.exit(0);
420
+ }
421
+ }
422
+ }
423
+ let includeAuth = false;
424
+ if (!skipPrompts) {
425
+ const { auth } = await prompts(
426
+ {
427
+ type: "confirm",
428
+ name: "auth",
429
+ message: "Include authentication (@spfn/auth)?",
430
+ initial: true
431
+ }
432
+ );
433
+ includeAuth = auth;
434
+ }
435
+ return { packageJson, packageJsonPath, includeAuth };
436
+ }
437
+ var init_validate = __esm({
438
+ "src/commands/init/steps/validate.ts"() {
439
+ "use strict";
440
+ init_logger();
441
+ }
442
+ });
443
+
444
+ // src/commands/init/utils/templates.ts
445
+ import { existsSync as existsSync4 } from "fs";
446
+ import { join as join4, dirname } from "path";
447
+ import { fileURLToPath } from "url";
448
+ function findTemplatesPath() {
449
+ const bundledPath = join4(__dirname, "templates");
450
+ if (existsSync4(bundledPath)) {
451
+ return bundledPath;
452
+ }
453
+ const npmPath = join4(__dirname, "..", "..", "templates");
454
+ if (existsSync4(npmPath)) {
455
+ return npmPath;
456
+ }
457
+ const devPath = join4(__dirname, "..", "..", "..", "templates");
458
+ if (existsSync4(devPath)) {
459
+ return devPath;
460
+ }
461
+ throw new Error("Templates directory not found. Please rebuild the package.");
462
+ }
463
+ var __dirname;
464
+ var init_templates = __esm({
465
+ "src/commands/init/utils/templates.ts"() {
466
+ "use strict";
467
+ __dirname = dirname(fileURLToPath(import.meta.url));
468
+ }
469
+ });
470
+
471
+ // src/commands/init/steps/server-structure.ts
472
+ import { existsSync as existsSync5 } from "fs";
473
+ import { join as join5 } from "path";
474
+ import ora2 from "ora";
475
+ import fse2 from "fs-extra";
476
+ async function setupServerStructure(cwd) {
477
+ const spinner = ora2("Setting up server structure...").start();
478
+ try {
479
+ const templatesDir = findTemplatesPath();
480
+ const serverTemplateDir = join5(templatesDir, "server");
481
+ const targetDir = join5(cwd, "src", "server");
482
+ if (!existsSync5(serverTemplateDir)) {
483
+ spinner.fail("Failed to create server structure");
484
+ logger.error(`Server templates not found at: ${serverTemplateDir}`);
485
+ process.exit(1);
486
+ }
487
+ ensureDirSync2(targetDir);
488
+ copySync(serverTemplateDir, targetDir);
489
+ const libTemplateDir = join5(templatesDir, "lib");
490
+ const libTargetDir = join5(cwd, "src", "lib");
491
+ if (existsSync5(libTemplateDir)) {
492
+ ensureDirSync2(libTargetDir);
493
+ copySync(libTemplateDir, libTargetDir);
494
+ }
495
+ const envConfigTemplate = join5(serverTemplateDir, "config", "env.config.ts");
496
+ const envConfigTarget = join5(targetDir, "config", "env.config.ts");
497
+ if (existsSync5(envConfigTemplate)) {
498
+ ensureDirSync2(join5(targetDir, "config"));
499
+ copySync(envConfigTemplate, envConfigTarget);
500
+ logger.success("Created src/server/config/env.config.ts (environment management)");
501
+ }
502
+ spinner.succeed("Server structure created");
503
+ } catch (error) {
504
+ spinner.fail("Failed to create server structure");
505
+ logger.error(String(error));
506
+ process.exit(1);
507
+ }
508
+ }
509
+ var copySync, ensureDirSync2;
510
+ var init_server_structure = __esm({
511
+ "src/commands/init/steps/server-structure.ts"() {
512
+ "use strict";
513
+ init_logger();
514
+ init_templates();
515
+ ({ copySync, ensureDirSync: ensureDirSync2 } = fse2);
516
+ }
517
+ });
518
+
519
+ // src/commands/init/steps/api-proxy.ts
520
+ import { existsSync as existsSync6 } from "fs";
521
+ import { join as join6 } from "path";
522
+ import fse3 from "fs-extra";
523
+ async function setupApiProxy(cwd, includeAuth) {
524
+ const srcAppDir = join6(cwd, "src", "app");
525
+ const rootAppDir = join6(cwd, "app");
526
+ let appDir;
527
+ if (existsSync6(srcAppDir)) {
528
+ appDir = srcAppDir;
529
+ } else if (existsSync6(rootAppDir)) {
530
+ appDir = rootAppDir;
531
+ } else {
532
+ logger.error("Next.js app directory not found. Expected src/app or app directory.");
533
+ process.exit(1);
534
+ }
535
+ const rpcDir = join6(appDir, "api", "rpc", "[routeName]");
536
+ const rpcRoutePath = join6(rpcDir, "route.ts");
537
+ if (existsSync6(rpcRoutePath)) {
538
+ logger.error(`RPC proxy route already exists: ${rpcRoutePath.replace(cwd + "/", "")}`);
539
+ process.exit(1);
540
+ }
541
+ ensureDirSync3(rpcDir);
542
+ const authImport = includeAuth ? `import '@spfn/auth/nextjs/api';
543
+ ` : "";
544
+ const routeContent = `/**
545
+ * SPFN RPC Proxy
546
+ *
547
+ * Resolves routeName to actual HTTP method and path from routeMap,
548
+ * then forwards requests to SPFN API server with automatic:
549
+ * - Cookie forwarding
550
+ * - Interceptor execution
551
+ * - Header manipulation
552
+ *
553
+ * Note: Uses generated route-map to avoid loading server code in Next.js process.
554
+ * Run \`spfn codegen run\` if route-map.ts is missing.
555
+ */
556
+
557
+ ${authImport}import { routeMap } from '@/generated/route-map';
558
+ import { createRpcProxy } from '@spfn/core/nextjs/server';
559
+
560
+ export const { GET, POST } = createRpcProxy({ routeMap });
561
+ `;
562
+ writeFileSync2(rpcRoutePath, routeContent);
563
+ const relativePath = rpcRoutePath.replace(cwd + "/", "");
564
+ logger.success(`Created ${relativePath} (RPC proxy)`);
565
+ }
566
+ var ensureDirSync3, writeFileSync2;
567
+ var init_api_proxy = __esm({
568
+ "src/commands/init/steps/api-proxy.ts"() {
569
+ "use strict";
570
+ init_logger();
571
+ ({ ensureDirSync: ensureDirSync3, writeFileSync: writeFileSync2 } = fse3);
572
+ }
573
+ });
574
+
575
+ // src/commands/init/steps/docker.ts
576
+ import { existsSync as existsSync7 } from "fs";
577
+ import { join as join7 } from "path";
578
+ import fse4 from "fs-extra";
579
+ async function setupDockerFiles(cwd) {
580
+ const templatesDir = findTemplatesPath();
581
+ const dockerComposePath = join7(cwd, "docker-compose.yml");
582
+ if (!existsSync7(dockerComposePath)) {
583
+ try {
584
+ const dockerComposeTemplate = join7(templatesDir, "docker-compose.yml");
585
+ if (existsSync7(dockerComposeTemplate)) {
586
+ copySync2(dockerComposeTemplate, dockerComposePath);
587
+ logger.success("Created docker-compose.yml (PostgreSQL + Redis)");
588
+ }
589
+ } catch (error) {
590
+ logger.warn("Could not copy docker-compose.yml");
591
+ }
592
+ }
593
+ try {
594
+ const dockerfilePath = join7(cwd, "Dockerfile");
595
+ if (!existsSync7(dockerfilePath)) {
596
+ const dockerfileTemplate = join7(templatesDir, "Dockerfile");
597
+ if (existsSync7(dockerfileTemplate)) {
598
+ copySync2(dockerfileTemplate, dockerfilePath);
599
+ logger.success("Created Dockerfile");
600
+ }
601
+ }
602
+ const dockerignorePath = join7(cwd, ".dockerignore");
603
+ if (!existsSync7(dockerignorePath)) {
604
+ const dockerignoreTemplate = join7(templatesDir, ".dockerignore");
605
+ if (existsSync7(dockerignoreTemplate)) {
606
+ copySync2(dockerignoreTemplate, dockerignorePath);
607
+ logger.success("Created .dockerignore");
608
+ }
609
+ }
610
+ const dockerComposeProdPath = join7(cwd, "docker-compose.production.yml");
611
+ if (!existsSync7(dockerComposeProdPath)) {
612
+ const dockerComposeProdTemplate = join7(templatesDir, "docker-compose.production.yml");
613
+ if (existsSync7(dockerComposeProdTemplate)) {
614
+ copySync2(dockerComposeProdTemplate, dockerComposeProdPath);
615
+ logger.success("Created docker-compose.production.yml");
616
+ }
617
+ }
618
+ } catch (error) {
619
+ logger.warn("Could not copy Docker files (you can create them manually)");
620
+ }
621
+ }
622
+ var copySync2;
623
+ var init_docker = __esm({
624
+ "src/commands/init/steps/docker.ts"() {
625
+ "use strict";
626
+ init_logger();
627
+ init_templates();
628
+ ({ copySync: copySync2 } = fse4);
629
+ }
630
+ });
631
+
632
+ // src/commands/init/steps/deployment-config.ts
633
+ import { existsSync as existsSync8 } from "fs";
634
+ import { join as join8 } from "path";
635
+ import fse5 from "fs-extra";
636
+ async function setupDeploymentConfig(cwd, packageJson, packageManager) {
637
+ const deploymentConfigPath = join8(cwd, "spfn.config.js");
638
+ if (!existsSync8(deploymentConfigPath)) {
639
+ try {
640
+ const projectName = packageJson.name?.replace(/[@\/]/g, "-").toLowerCase() || cwd.split("/").pop()?.toLowerCase() || "my-app";
641
+ const configContent = `/**
642
+ * SPFN Configuration
643
+ *
644
+ * This file configures your SPFN application deployment settings.
645
+ *
646
+ * @type {import('spfn').SpfnConfig}
647
+ */
648
+ export default {
649
+ /**
650
+ * Package manager to use for dependency installation
651
+ * Options: 'npm' | 'yarn' | 'pnpm' | 'bun'
652
+ */
653
+ packageManager: '${packageManager}',
654
+
655
+ /**
656
+ * Deployment configuration for SPFN cloud platform
657
+ */
658
+ deployment: {
659
+ /**
660
+ * Your app's subdomain on spfn.app
661
+ *
662
+ * This will automatically create region-specific domains:
663
+ * - {subdomain}.{region}.spfn.app \u2192 Next.js frontend (port 3790)
664
+ * - api-{subdomain}.{region}.spfn.app \u2192 SPFN backend (port 8790)
665
+ *
666
+ * Example: subdomain: '${projectName}', region: 'us' creates:
667
+ * - ${projectName}.us.spfn.app
668
+ * - api-${projectName}.us.spfn.app
669
+ */
670
+ subdomain: '${projectName}',
671
+
672
+ /**
673
+ * Deployment region (optional, defaults to 'us')
674
+ *
675
+ * Available regions:
676
+ * - 'us': Virginia, USA (default)
677
+ * - 'kr': Seoul, South Korea
678
+ * - 'jp': Tokyo, Japan [Coming soon]
679
+ * - 'sg': Singapore [Coming soon]
680
+ * - 'eu': Frankfurt, Germany [Coming soon]
681
+ */
682
+ region: 'us',
683
+
684
+ /**
685
+ * Custom domains (optional)
686
+ *
687
+ * Add your own custom domains here. Make sure to configure DNS:
688
+ * - CNAME record pointing to spfn.app
689
+ *
690
+ * Example:
691
+ * customDomains: {
692
+ * nextjs: ['www.example.com', 'example.com'],
693
+ * spfn: ['api.example.com']
694
+ * }
695
+ */
696
+ customDomains: {
697
+ /**
698
+ * Custom domains for Next.js frontend
699
+ */
700
+ nextjs: [],
701
+
702
+ /**
703
+ * Custom domains for SPFN backend API
704
+ */
705
+ spfn: []
706
+ },
707
+
708
+ /**
709
+ * Environment variables (optional)
710
+ *
711
+ * Most environment variables are auto-generated by the CI/CD pipeline.
712
+ * Only add custom values if you need to override defaults.
713
+ *
714
+ * \u{1F527} Auto-generated variables (leave env empty for defaults):
715
+ * - NEXT_PUBLIC_API_URL: https://api-{subdomain}.{region}.spfn.app
716
+ * (Used by browser/client-side code)
717
+ * - API_URL: http://localhost:8790
718
+ * (Used by Next.js SSR/API Routes - same container, internal)
719
+ *
720
+ * \u{1F4CB} When to add custom env:
721
+ * - Using custom API domain (not *.spfn.app)
722
+ * - Additional environment variables for your app
723
+ *
724
+ * \u26A0\uFE0F SECURITY WARNING:
725
+ * - These values are committed to Git
726
+ * - Do NOT put sensitive credentials here (DB passwords, API keys, etc.)
727
+ * - For production secrets, use your CI/CD secrets management
728
+ *
729
+ * Example (custom API domain):
730
+ * env: {
731
+ * NEXT_PUBLIC_API_URL: 'https://api.custom.com',
732
+ * API_URL: 'https://api.custom.com',
733
+ * NEXT_PUBLIC_FEATURE_FLAG: 'true'
734
+ * }
735
+ */
736
+ env: {}
737
+ }
738
+ }
739
+ `;
740
+ writeFileSync3(deploymentConfigPath, configContent);
741
+ logger.success(`Created spfn.config.js (subdomain: ${projectName}.spfn.app)`);
742
+ } catch (error) {
743
+ logger.warn("Could not create spfn.config.js");
744
+ }
745
+ }
746
+ }
747
+ var writeFileSync3;
748
+ var init_deployment_config = __esm({
749
+ "src/commands/init/steps/deployment-config.ts"() {
750
+ "use strict";
751
+ init_logger();
752
+ ({ writeFileSync: writeFileSync3 } = fse5);
753
+ }
754
+ });
755
+
756
+ // src/utils/version.ts
757
+ function getCliVersion() {
758
+ return "0.2.0-beta.10";
759
+ }
760
+ function getTagFromVersion(version) {
761
+ const match = version.match(/-([a-z]+)\./i);
762
+ return match ? match[1] : "latest";
763
+ }
764
+ function getSpfnTag() {
765
+ return getTagFromVersion(getCliVersion());
766
+ }
767
+ var init_version = __esm({
768
+ "src/utils/version.ts"() {
769
+ "use strict";
770
+ }
771
+ });
772
+
773
+ // src/commands/init/steps/package.ts
774
+ import ora3 from "ora";
775
+ import { execa as execa2 } from "execa";
776
+ import fse6 from "fs-extra";
777
+ async function setupPackageJson(cwd, packageJsonPath, packageJson, packageManager, includeAuth) {
778
+ const spinner = ora3("Updating package.json...").start();
779
+ packageJson.dependencies = packageJson.dependencies || {};
780
+ packageJson.devDependencies = packageJson.devDependencies || {};
781
+ packageJson.scripts = packageJson.scripts || {};
782
+ const spfnTag = getSpfnTag();
783
+ packageJson.dependencies["@spfn/core"] = spfnTag;
784
+ packageJson.dependencies["@sinclair/typebox"] = "^0.34.0";
785
+ packageJson.dependencies["drizzle-typebox"] = "^0.1.0";
786
+ packageJson.dependencies["spfn"] = spfnTag;
787
+ packageJson.dependencies["concurrently"] = "^9.2.1";
788
+ if (includeAuth) {
789
+ packageJson.dependencies["@spfn/auth"] = spfnTag;
790
+ }
791
+ packageJson.devDependencies["@types/node"] = "^20.11.0";
792
+ packageJson.devDependencies["tsx"] = "^4.20.6";
793
+ packageJson.devDependencies["tsup"] = "^8.5.0";
794
+ packageJson.devDependencies["drizzle-kit"] = "^0.31.5";
795
+ packageJson.devDependencies["dotenv"] = "^17.2.3";
796
+ if (!packageJson.scripts["build"]) {
797
+ packageJson.scripts["build"] = "next build --turbopack";
798
+ }
799
+ if (!packageJson.scripts["start"]) {
800
+ packageJson.scripts["start"] = "next start";
801
+ }
802
+ packageJson.scripts["spfn:dev"] = "spfn dev";
803
+ packageJson.scripts["spfn:server"] = "spfn dev --server-only";
804
+ packageJson.scripts["spfn:next"] = "next dev --turbo --port 3790";
805
+ packageJson.scripts["spfn:start"] = "spfn start";
806
+ packageJson.scripts["spfn:build"] = "spfn build";
807
+ packageJson.scripts["codegen"] = "spfn codegen run";
808
+ writeFileSync4(packageJsonPath, JSON.stringify(packageJson, null, 2));
809
+ spinner.succeed("package.json updated");
810
+ spinner.start("Installing dependencies...");
811
+ try {
812
+ const installArgs = packageManager === "npm" ? ["install", "--legacy-peer-deps"] : ["install"];
813
+ await execa2(packageManager, installArgs, { cwd });
814
+ spinner.succeed("Dependencies installed");
815
+ } catch (error) {
816
+ spinner.fail("Failed to install dependencies");
817
+ logger.error(String(error));
818
+ process.exit(1);
819
+ }
820
+ }
821
+ var writeFileSync4;
822
+ var init_package = __esm({
823
+ "src/commands/init/steps/package.ts"() {
824
+ "use strict";
825
+ init_logger();
826
+ init_version();
827
+ ({ writeFileSync: writeFileSync4 } = fse6);
828
+ }
829
+ });
830
+
831
+ // src/commands/init/steps/config-files.ts
832
+ import { existsSync as existsSync9, readFileSync as readFileSync2 } from "fs";
833
+ import { join as join9 } from "path";
834
+ import fse7 from "fs-extra";
835
+ async function setupConfigFiles(cwd) {
836
+ const envExamplePath = join9(cwd, ".env.local.example");
837
+ if (!existsSync9(envExamplePath)) {
838
+ const envExampleContent = `# Environment
839
+ NODE_ENV=local
840
+
841
+ # Logging
842
+ SPFN_LOG_LEVEL=info
843
+
844
+ # Database (matches docker-compose.yml)
845
+ DATABASE_URL=postgresql://spfn:spfn@localhost:5432/spfn_dev
846
+
847
+ # Cache - Redis/Valkey (optional)
848
+ CACHE_URL=redis://localhost:6379
849
+
850
+ # SPFN API Server URL (for API Route Proxy and SSR)
851
+ SPFN_API_URL=http://localhost:8790
852
+ `;
853
+ writeFileSync5(envExamplePath, envExampleContent);
854
+ logger.success("Created .env.local.example");
855
+ }
856
+ const spfnrcPath = join9(cwd, ".spfnrc.ts");
857
+ if (!existsSync9(spfnrcPath)) {
858
+ const spfnrcContent = `import { defineConfig, defineGenerator } from '@spfn/core/codegen';
859
+
860
+ /**
861
+ * SPFN Codegen Configuration
862
+ *
863
+ * Configure code generators here. Generators run during \`spfn dev\` and \`spfn codegen run\`.
864
+ */
865
+
866
+ export default defineConfig({
867
+ generators: [
868
+ // Route map generator - generates routeName \u2192 {method, path} mappings
869
+ // Used by RPC proxy to resolve routes without importing server code
870
+ defineGenerator({
871
+ name: '@spfn/core:route-map',
872
+ routerPath: './src/server/router.ts',
873
+ outputPath: './src/generated/route-map.ts',
874
+ }),
875
+ ]
876
+ });
877
+ `;
878
+ writeFileSync5(spfnrcPath, spfnrcContent);
879
+ logger.success("Created .spfnrc.ts (codegen configuration)");
880
+ }
881
+ const gitignorePath = join9(cwd, ".gitignore");
882
+ if (existsSync9(gitignorePath)) {
883
+ try {
884
+ const gitignoreContent = readFileSync2(gitignorePath, "utf-8");
885
+ if (!gitignoreContent.includes(".spfn")) {
886
+ const updatedContent = gitignoreContent.replace(
887
+ /# production\n\/build/,
888
+ "# production\n/build\n\n# spfn\n/.spfn/"
889
+ );
890
+ writeFileSync5(gitignorePath, updatedContent);
891
+ logger.success("Updated .gitignore with .spfn directory");
892
+ }
893
+ } catch (error) {
894
+ logger.warn("Could not update .gitignore (you can add .spfn manually)");
895
+ }
896
+ }
897
+ const tsconfigPath = join9(cwd, "tsconfig.json");
898
+ if (existsSync9(tsconfigPath)) {
899
+ try {
900
+ const tsconfigContent = readFileSync2(tsconfigPath, "utf-8");
901
+ const tsconfig = JSON.parse(tsconfigContent);
902
+ if (!tsconfig.exclude) {
903
+ tsconfig.exclude = [];
904
+ }
905
+ if (!tsconfig.exclude.includes("src/server")) {
906
+ tsconfig.exclude.push("src/server");
907
+ writeFileSync5(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
908
+ logger.success("Updated tsconfig.json (excluded src/server for Vercel compatibility)");
909
+ }
910
+ } catch (error) {
911
+ logger.warn('Could not update tsconfig.json (you can add "src/server" to exclude manually)');
912
+ }
913
+ }
914
+ }
915
+ var writeFileSync5;
916
+ var init_config_files = __esm({
917
+ "src/commands/init/steps/config-files.ts"() {
918
+ "use strict";
919
+ init_logger();
920
+ ({ writeFileSync: writeFileSync5 } = fse7);
921
+ }
922
+ });
923
+
924
+ // src/commands/init/index.ts
925
+ var init_exports = {};
926
+ __export(init_exports, {
927
+ initCommand: () => initCommand,
928
+ initializeSpfn: () => initializeSpfn
929
+ });
930
+ import { Command as Command2 } from "commander";
931
+ import chalk3 from "chalk";
932
+ async function initializeSpfn(options = {}) {
933
+ const cwd = process.cwd();
934
+ const { packageJson, packageJsonPath, includeAuth } = await validateProject(cwd, options.yes || false);
935
+ const pm = detectPackageManager(cwd);
936
+ logger.step(`Detected package manager: ${pm}`);
937
+ await setupServerStructure(cwd);
938
+ await setupApiProxy(cwd, includeAuth);
939
+ await setupDockerFiles(cwd);
940
+ await setupDeploymentConfig(cwd, packageJson, pm);
941
+ await setupPackageJson(cwd, packageJsonPath, packageJson, pm, includeAuth);
942
+ await setupConfigFiles(cwd);
943
+ console.log("\n" + chalk3.green.bold("\u2713 SPFN initialized successfully!\n"));
944
+ console.log("Next steps:");
945
+ console.log(" 1. Start PostgreSQL & Redis (if not installed locally):");
946
+ console.log(" " + chalk3.cyan("docker compose up -d"));
947
+ console.log(" 2. Copy .env.local.example to .env.local");
948
+ console.log(" " + chalk3.cyan("cp .env.local.example .env.local"));
949
+ console.log(" 3. Run: " + chalk3.cyan(pm === "npm" ? "npm run spfn:dev" : `${pm} run spfn:dev`));
950
+ console.log(" 4. Visit:");
951
+ console.log(" - Next.js: " + chalk3.cyan("http://localhost:3790"));
952
+ console.log(" - API: " + chalk3.cyan("http://localhost:8790/health"));
953
+ console.log("\nAvailable commands:");
954
+ console.log(" \u2022 " + chalk3.cyan(pm === "npm" ? "npm run spfn:dev" : `${pm} spfn:dev`) + " - Start SPFN + Next.js");
955
+ console.log(" \u2022 " + chalk3.cyan("spfn env:validate") + " - Validate environment variables");
956
+ console.log(" \u2022 " + chalk3.cyan("spfn env:docs") + " - Generate env documentation");
957
+ console.log(" \u2022 " + chalk3.cyan("spfn env:check") + " - Check environment status");
958
+ console.log("\n" + chalk3.blue("\u{1F4A1} Tip:") + " Edit " + chalk3.cyan("src/server/config/env.config.ts") + " to manage environment variables");
959
+ }
960
+ var initCommand;
961
+ var init_init = __esm({
962
+ "src/commands/init/index.ts"() {
963
+ "use strict";
964
+ init_package_manager();
965
+ init_logger();
966
+ init_validate();
967
+ init_server_structure();
968
+ init_api_proxy();
969
+ init_docker();
970
+ init_deployment_config();
971
+ init_package();
972
+ init_config_files();
973
+ initCommand = new Command2("init").description("Initialize SPFN in your Next.js project").option("-y, --yes", "Skip prompts and use defaults").action(initializeSpfn);
974
+ }
975
+ });
976
+
977
+ // src/utils/function-migrations.ts
978
+ var function_migrations_exports = {};
979
+ __export(function_migrations_exports, {
980
+ discoverFunctionMigrations: () => discoverFunctionMigrations,
981
+ executeFunctionMigrations: () => executeFunctionMigrations
982
+ });
983
+ import chalk11 from "chalk";
984
+ import { join as join15 } from "path";
985
+ import { env as env2 } from "@spfn/core/config";
986
+ import { loadEnvFiles as loadEnvFiles2 } from "@spfn/core/server";
987
+ import { existsSync as existsSync16, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
988
+ function discoverFunctionMigrations(cwd = process.cwd()) {
989
+ const nodeModulesPath = join15(cwd, "node_modules");
990
+ if (!existsSync16(nodeModulesPath)) {
991
+ return [];
992
+ }
993
+ const functions = [];
994
+ const spfnDir = join15(nodeModulesPath, "@spfn");
995
+ if (!existsSync16(spfnDir)) {
996
+ return [];
997
+ }
998
+ const packages = readdirSync2(spfnDir);
999
+ for (const pkg of packages) {
1000
+ const packagePath = join15(spfnDir, pkg);
1001
+ const packageJsonPath = join15(packagePath, "package.json");
1002
+ if (!existsSync16(packageJsonPath)) {
1003
+ continue;
1004
+ }
1005
+ try {
1006
+ const packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
1007
+ const spfnConfig = packageJson.spfn;
1008
+ if (!spfnConfig?.migrations) {
1009
+ continue;
1010
+ }
1011
+ const migrationsDir = join15(packagePath, spfnConfig.migrations.dir);
1012
+ if (!existsSync16(migrationsDir)) {
1013
+ console.warn(
1014
+ chalk11.yellow(`\u26A0\uFE0F Package @spfn/${pkg} specifies migrations but directory not found: ${migrationsDir}`)
1015
+ );
1016
+ continue;
1017
+ }
1018
+ functions.push({
1019
+ packageName: `@spfn/${pkg}`,
1020
+ migrationsDir,
1021
+ packagePath
1022
+ });
1023
+ } catch (error) {
1024
+ console.warn(chalk11.yellow(`\u26A0\uFE0F Failed to parse package.json for @spfn/${pkg}`));
1025
+ }
1026
+ }
1027
+ return functions;
1028
+ }
1029
+ async function executeFunctionMigrations(functionMigrations) {
1030
+ let executedCount = 0;
1031
+ const { drizzle } = await import("drizzle-orm/postgres-js");
1032
+ const { migrate } = await import("drizzle-orm/postgres-js/migrator");
1033
+ const postgres = await import("postgres");
1034
+ loadEnvFiles2();
1035
+ if (!env2.DATABASE_URL) {
1036
+ throw new Error("DATABASE_URL not found in environment");
1037
+ }
1038
+ const connection = postgres.default(env2.DATABASE_URL, { max: 1 });
1039
+ const db = drizzle(connection);
1040
+ try {
1041
+ for (const func of functionMigrations) {
1042
+ console.log(chalk11.blue(`
1043
+ \u{1F4E6} Running ${func.packageName} migrations...`));
1044
+ await migrate(db, { migrationsFolder: func.migrationsDir });
1045
+ console.log(chalk11.green(` \u2713 ${func.packageName} migrations applied`));
1046
+ executedCount++;
1047
+ }
1048
+ } finally {
1049
+ await connection.end();
1050
+ }
1051
+ return executedCount;
1052
+ }
1053
+ var init_function_migrations = __esm({
1054
+ "src/utils/function-migrations.ts"() {
1055
+ "use strict";
1056
+ }
1057
+ });
1058
+
1059
+ // src/index.ts
1060
+ import { Command as Command13 } from "commander";
1061
+
1062
+ // src/commands/create.ts
1063
+ init_logger();
1064
+ init_package_manager();
1065
+ import { Command as Command3 } from "commander";
1066
+ import { existsSync as existsSync10 } from "fs";
1067
+ import { join as join10 } from "path";
1068
+ import prompts2 from "prompts";
1069
+ import ora4 from "ora";
1070
+ import { execa as execa3 } from "execa";
1071
+ import chalk4 from "chalk";
23
1072
  async function createProject(projectName, options) {
24
1073
  const cwd = process.cwd();
25
- const projectPath = join(cwd, projectName);
26
- if (existsSync(projectPath)) {
1074
+ const projectPath = join10(cwd, projectName);
1075
+ if (existsSync10(projectPath)) {
27
1076
  logger.error(`Directory ${projectName} already exists.`);
28
1077
  process.exit(1);
29
1078
  }
30
- console.log(chalk.blue.bold("\n\u{1F680} Creating Next.js project with SPFN...\n"));
1079
+ console.log(chalk4.blue.bold("\n\u{1F680} Creating Next.js project with SPFN...\n"));
31
1080
  let pm = options.pm || detectPackageManager(cwd);
32
1081
  if (!options.yes && !options.pm) {
33
- const { selectedPm } = await prompts({
1082
+ const { selectedPm } = await prompts2({
34
1083
  type: "select",
35
1084
  name: "selectedPm",
36
1085
  message: "Which package manager do you want to use?",
@@ -48,7 +1097,7 @@ async function createProject(projectName, options) {
48
1097
  pm = selectedPm;
49
1098
  }
50
1099
  logger.step(`Using package manager: ${pm}`);
51
- const spinner = ora("Creating Next.js project...").start();
1100
+ const spinner = ora4("Creating Next.js project...").start();
52
1101
  try {
53
1102
  const createNextAppArgs = [
54
1103
  "create-next-app@latest",
@@ -71,7 +1120,7 @@ async function createProject(projectName, options) {
71
1120
  }
72
1121
  const createCommand2 = pm === "npm" ? "npx" : pm === "yarn" ? "yarn" : pm === "pnpm" ? "pnpm" : "bunx";
73
1122
  const createArgs = createCommand2 === "npx" ? createNextAppArgs : ["dlx", ...createNextAppArgs];
74
- await execa(createCommand2, createArgs, {
1123
+ await execa3(createCommand2, createArgs, {
75
1124
  cwd,
76
1125
  stdio: "inherit"
77
1126
  });
@@ -85,22 +1134,22 @@ async function createProject(projectName, options) {
85
1134
  logger.info(`
86
1135
  \u{1F4C2} Changed directory to ${projectName}
87
1136
  `);
88
- const iconsSpinner = ora("Setting up SVGR for icon management...").start();
1137
+ const iconsSpinner = ora4("Setting up SVGR for icon management...").start();
89
1138
  try {
90
1139
  const installArgs = pm === "npm" ? ["install", "--save-dev", "@svgr/webpack"] : pm === "yarn" ? ["add", "-D", "@svgr/webpack"] : pm === "pnpm" ? ["add", "-D", "@svgr/webpack"] : ["add", "-d", "@svgr/webpack"];
91
- await execa(pm, installArgs, { cwd: projectPath });
92
- const { setupIcons } = await import("./setup-JA2ADEQ6.js");
93
- await setupIcons();
1140
+ await execa3(pm, installArgs, { cwd: projectPath });
1141
+ const { setupIcons: setupIcons2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
1142
+ await setupIcons2();
94
1143
  iconsSpinner.succeed("SVGR setup completed");
95
1144
  } catch (error) {
96
1145
  iconsSpinner.warn("Failed to setup SVGR (you can run `spfn setup icons` later)");
97
1146
  }
98
1147
  if (options.shadcn) {
99
- const shadcnSpinner = ora("Setting up shadcn/ui...").start();
1148
+ const shadcnSpinner = ora4("Setting up shadcn/ui...").start();
100
1149
  try {
101
1150
  const shadcnCommand = pm === "npm" ? "npx" : pm === "pnpm" ? "pnpx" : pm === "yarn" ? "yarn dlx" : "bunx";
102
1151
  const shadcnArgs = pm === "yarn" ? ["shadcn@latest", "init", "--yes", "--defaults"] : ["shadcn@latest", "init", "--yes", "--defaults"];
103
- await execa(shadcnCommand, shadcnArgs, {
1152
+ await execa3(shadcnCommand, shadcnArgs, {
104
1153
  cwd: projectPath,
105
1154
  stdio: "inherit"
106
1155
  });
@@ -109,95 +1158,103 @@ async function createProject(projectName, options) {
109
1158
  shadcnSpinner.warn("Failed to initialize shadcn/ui (you can run `npx shadcn@latest init` later)");
110
1159
  }
111
1160
  }
112
- const initSpinner = ora("Initializing SPFN...").start();
1161
+ const initSpinner = ora4("Initializing SPFN...").start();
113
1162
  try {
114
- const { initializeSpfn } = await import("./init-EKWKKNUL.js");
115
- await initializeSpfn({ yes: true });
1163
+ const { initializeSpfn: initializeSpfn2 } = await Promise.resolve().then(() => (init_init(), init_exports));
1164
+ await initializeSpfn2({ yes: true });
116
1165
  initSpinner.succeed("SPFN initialized");
117
1166
  } catch (error) {
118
1167
  initSpinner.fail("Failed to initialize SPFN");
119
1168
  logger.error(String(error));
120
1169
  process.exit(1);
121
1170
  }
122
- console.log("\n" + chalk.green.bold("\u2713 Project created successfully!\n"));
123
- console.log(chalk.bold("Next steps:\n"));
124
- console.log(` ${chalk.cyan("cd")} ${projectName}`);
125
- console.log(` ${chalk.cyan("docker compose up -d")} ${chalk.gray("# Start PostgreSQL & Redis")}`);
126
- console.log(` ${chalk.cyan("cp .env.local.example .env.local")} ${chalk.gray("# Configure environment")}`);
127
- console.log(` ${chalk.cyan(`${pm === "npm" ? "npm run" : pm + " run"} spfn:dev`)} ${chalk.gray("# Start dev server")}
1171
+ console.log("\n" + chalk4.green.bold("\u2713 Project created successfully!\n"));
1172
+ console.log(chalk4.bold("Next steps:\n"));
1173
+ console.log(` ${chalk4.cyan("cd")} ${projectName}`);
1174
+ console.log(` ${chalk4.cyan("docker compose up -d")} ${chalk4.gray("# Start PostgreSQL & Redis")}`);
1175
+ console.log(` ${chalk4.cyan("cp .env.local.example .env.local")} ${chalk4.gray("# Configure environment")}`);
1176
+ console.log(` ${chalk4.cyan(`${pm === "npm" ? "npm run" : pm + " run"} spfn:dev`)} ${chalk4.gray("# Start dev server")}
128
1177
  `);
129
- console.log(chalk.bold("Your app will be available at:\n"));
130
- console.log(` ${chalk.cyan("http://localhost:3790")} ${chalk.gray("(Next.js)")}`);
131
- console.log(` ${chalk.cyan("http://localhost:8790")} ${chalk.gray("(SPFN API)")}
1178
+ console.log(chalk4.bold("Your app will be available at:\n"));
1179
+ console.log(` ${chalk4.cyan("http://localhost:3790")} ${chalk4.gray("(Next.js)")}`);
1180
+ console.log(` ${chalk4.cyan("http://localhost:8790")} ${chalk4.gray("(SPFN API)")}
132
1181
  `);
133
- console.log(chalk.bold("\u{1F680} Ready for production?\n"));
134
- console.log(" " + chalk.cyan("Build for production:"));
135
- console.log(` ${chalk.cyan(pm === "npm" ? "npm run" : pm + " run")} spfn:build`);
136
- console.log(` ${chalk.cyan(pm === "npm" ? "npm run" : pm + " run")} spfn:start
1182
+ console.log(chalk4.bold("\u{1F680} Ready for production?\n"));
1183
+ console.log(" " + chalk4.cyan("Build for production:"));
1184
+ console.log(` ${chalk4.cyan(pm === "npm" ? "npm run" : pm + " run")} spfn:build`);
1185
+ console.log(` ${chalk4.cyan(pm === "npm" ? "npm run" : pm + " run")} spfn:start
137
1186
  `);
138
- console.log(" " + chalk.cyan("Or deploy with Docker:"));
139
- console.log(` ${chalk.cyan("docker compose -f docker-compose.production.yml up --build -d")}
1187
+ console.log(" " + chalk4.cyan("Or deploy with Docker:"));
1188
+ console.log(` ${chalk4.cyan("docker compose -f docker-compose.production.yml up --build -d")}
140
1189
  `);
141
- console.log(chalk.dim(" \u{1F4D6} See .guide/deployment.md for complete deployment guide"));
142
- console.log(chalk.dim(" \u{1F310} Documentation: https://github.com/spfn/spfn\n"));
1190
+ console.log(chalk4.dim(" \u{1F4D6} See .guide/deployment.md for complete deployment guide"));
1191
+ console.log(chalk4.dim(" \u{1F310} Documentation: https://github.com/spfn/spfn\n"));
143
1192
  }
144
- var createCommand = new Command("create").description("Create a new Next.js project with SPFN").argument("<project-name>", "Name of the project directory").option("--skip-install", "Skip installing dependencies").option("--skip-git", "Skip initializing a git repository").option("--pm <manager>", "Package manager to use (npm, pnpm, yarn, bun)").option("--shadcn", "Setup shadcn/ui (component library)").option("-y, --yes", "Skip prompts and use defaults").action(async (projectName, options) => {
1193
+ var createCommand = new Command3("create").description("Create a new Next.js project with SPFN").argument("<project-name>", "Name of the project directory").option("--skip-install", "Skip installing dependencies").option("--skip-git", "Skip initializing a git repository").option("--pm <manager>", "Package manager to use (npm, pnpm, yarn, bun)").option("--shadcn", "Setup shadcn/ui (component library)").option("-y, --yes", "Skip prompts and use defaults").action(async (projectName, options) => {
145
1194
  await createProject(projectName, options);
146
1195
  });
147
1196
 
1197
+ // src/index.ts
1198
+ init_init();
1199
+
148
1200
  // src/commands/dev.ts
149
- import { Command as Command2 } from "commander";
150
- import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync } from "fs";
151
- import { join as join2 } from "path";
152
- import { execa as execa2 } from "execa";
1201
+ init_logger();
1202
+ init_package_manager();
1203
+ import { Command as Command4 } from "commander";
1204
+ import { existsSync as existsSync11, readFileSync as readFileSync3, writeFileSync as writeFileSync6, mkdirSync } from "fs";
1205
+ import { join as join11 } from "path";
1206
+ import { execa as execa4 } from "execa";
153
1207
  import chokidar from "chokidar";
154
- var devCommand = new Command2("dev").description("Start SPFN development server (detects and runs Next.js + Hono)").option("-p, --port <port>", "Server port", "8790").option("-h, --host <host>", "Server host", "localhost").option("--routes <path>", "Routes directory path").option("--server-only", "Run only Hono server (skip Next.js)").option("--no-watch", "Disable hot reload (watch mode)").action(async (options) => {
1208
+ var devCommand = new Command4("dev").description("Start SPFN development server (detects and runs Next.js + Hono)").option("-p, --port <port>", "Server port").option("-H, --host <host>", "Server host").option("--routes <path>", "Routes directory path").option("--server-only", "Run only Hono server (skip Next.js)").option("--watch", "Enable hot reload (watch mode)").action(async (options) => {
155
1209
  process.setMaxListeners(20);
156
1210
  if (!process.env.NODE_ENV) {
157
1211
  process.env.NODE_ENV = "development";
158
1212
  }
159
1213
  const cwd = process.cwd();
160
- const serverDir = join2(cwd, "src", "server");
161
- if (!existsSync2(serverDir)) {
1214
+ const serverDir = join11(cwd, "src", "server");
1215
+ if (!existsSync11(serverDir)) {
162
1216
  logger.error("src/server directory not found.");
163
1217
  logger.info('Run "spfn init" first to initialize SPFN in your project.');
164
1218
  process.exit(1);
165
1219
  }
166
- const packageJsonPath = join2(cwd, "package.json");
1220
+ const packageJsonPath = join11(cwd, "package.json");
167
1221
  let hasNext = false;
168
- if (existsSync2(packageJsonPath)) {
169
- const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
1222
+ if (existsSync11(packageJsonPath)) {
1223
+ const packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
170
1224
  hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
171
1225
  }
172
- const tempDir = join2(cwd, "node_modules", ".spfn");
173
- const serverEntry = join2(tempDir, "server.mjs");
174
- const watcherEntry = join2(tempDir, "watcher.mjs");
1226
+ const tempDir = join11(cwd, ".spfn");
1227
+ const serverEntry = join11(tempDir, "server.mjs");
1228
+ const watcherEntry = join11(tempDir, "watcher.mjs");
175
1229
  mkdirSync(tempDir, { recursive: true });
176
- writeFileSync(serverEntry, `
1230
+ const configParts = [];
1231
+ if (options.port) configParts.push(`port: ${options.port}`);
1232
+ if (options.host) configParts.push(`host: '${options.host}'`);
1233
+ if (options.routes) configParts.push(`routesPath: '${options.routes}'`);
1234
+ configParts.push("debug: true");
1235
+ writeFileSync6(serverEntry, `
177
1236
  // Load environment variables FIRST (before any imports that depend on them)
178
1237
  // Use centralized environment loader for standard dotenv priority
179
- const { loadEnvironment } = await import('@spfn/core/env');
180
- loadEnvironment({ debug: true });
1238
+ await import('@spfn/core/config');
181
1239
 
182
1240
  // Import and start server
183
1241
  const { startServer } = await import('@spfn/core/server');
184
1242
 
185
1243
  await startServer({
186
- port: ${options.port},
187
- host: '${options.host}',
188
- ${options.routes ? `routesPath: '${options.routes}',` : ""}debug: true
1244
+ ${configParts.join(",\n ")}
189
1245
  });
190
1246
  `);
191
- writeFileSync(watcherEntry, `
1247
+ writeFileSync6(watcherEntry, `
192
1248
  // Load environment variables
193
- const { loadEnvironment } = await import('@spfn/core/env');
194
- loadEnvironment({ debug: false });
195
-
196
- // Initialize database for generators that need it
197
- const { initDatabase, closeDatabase } = await import('@spfn/core/db');
198
- await initDatabase({
199
- pool: { max: 3 } // Watcher needs fewer connections than server
200
- });
1249
+ // const { loadEnvFiles } = await import('@spfn/core/server');
1250
+ // loadEnvFiles();
1251
+ // await import('@spfn/core/config');
1252
+ //
1253
+ // // Initialize database for generators that need it
1254
+ // const { initDatabase, closeDatabase } = await import('@spfn/core/db');
1255
+ // await initDatabase({
1256
+ // pool: { max: 3 } // Watcher needs fewer connections than server
1257
+ // });
201
1258
 
202
1259
  import { CodegenOrchestrator, loadCodegenConfig, createGeneratorsFromConfig } from '@spfn/core/codegen';
203
1260
 
@@ -215,7 +1272,6 @@ const orchestrator = new CodegenOrchestrator({
215
1272
  const cleanup = async () =>
216
1273
  {
217
1274
  await orchestrator.close();
218
- await closeDatabase();
219
1275
  };
220
1276
 
221
1277
  process.on('SIGTERM', async () =>
@@ -229,15 +1285,23 @@ process.on('SIGINT', async () =>
229
1285
  process.exit(0);
230
1286
  });
231
1287
 
232
- await orchestrator.watch();
233
-
234
- // Keep process alive
235
- await new Promise(() => {});
1288
+ // Start watching - this will run indefinitely until the watcher is closed
1289
+ try
1290
+ {
1291
+ await orchestrator.watch();
1292
+ }
1293
+ catch (error)
1294
+ {
1295
+ console.error('[SPFN] Codegen watcher error:', error);
1296
+ process.exit(1);
1297
+ }
236
1298
  `);
237
1299
  const pm = detectPackageManager(cwd);
238
1300
  if (options.serverOnly || !hasNext) {
239
- const watchMode2 = options.watch !== false;
240
- logger.info(`Starting SPFN Server on http://${options.host}:${options.port}${watchMode2 ? " (watch mode)" : ""}
1301
+ const watchMode2 = options.watch === true;
1302
+ const host = options.host ?? process.env.HOST ?? "localhost";
1303
+ const port = options.port ?? process.env.PORT ?? "4000";
1304
+ logger.info(`Starting SPFN Server on http://${host}:${port}${watchMode2 ? " (watch mode)" : ""}
241
1305
  `);
242
1306
  let serverProcess2 = null;
243
1307
  let watcherProcess2 = null;
@@ -245,7 +1309,7 @@ await new Promise(() => {});
245
1309
  const startWatcher2 = () => {
246
1310
  const watcherCmd = pm === "npm" ? "npx" : pm;
247
1311
  const watcherArgs = pm === "npm" ? ["tsx", watcherEntry] : ["exec", "tsx", watcherEntry];
248
- watcherProcess2 = execa2(watcherCmd, watcherArgs, {
1312
+ watcherProcess2 = execa4(watcherCmd, watcherArgs, {
249
1313
  cwd,
250
1314
  stdio: "inherit",
251
1315
  reject: false
@@ -260,7 +1324,7 @@ await new Promise(() => {});
260
1324
  const startServer2 = () => {
261
1325
  const serverCmd = pm === "npm" ? "npx" : pm;
262
1326
  const serverArgs = pm === "npm" ? ["tsx", serverEntry] : ["exec", "tsx", serverEntry];
263
- serverProcess2 = execa2(serverCmd, serverArgs, {
1327
+ serverProcess2 = execa4(serverCmd, serverArgs, {
264
1328
  cwd,
265
1329
  stdio: "inherit",
266
1330
  reject: false
@@ -275,7 +1339,7 @@ await new Promise(() => {});
275
1339
  serverProcess2.kill("SIGTERM");
276
1340
  await serverProcess2.catch(() => {
277
1341
  });
278
- await new Promise((resolve) => setTimeout(resolve, 500));
1342
+ await new Promise((resolve2) => setTimeout(resolve2, 500));
279
1343
  } catch (error) {
280
1344
  }
281
1345
  }
@@ -293,16 +1357,16 @@ await new Promise(() => {});
293
1357
  pollInterval: 50
294
1358
  }
295
1359
  });
296
- watcher.on("change", (path2) => {
297
- logger.info(`[SPFN] Changed: ${path2.replace(cwd + "/", "")}`);
1360
+ watcher.on("change", (path5) => {
1361
+ logger.info(`[SPFN] Changed: ${path5.replace(cwd + "/", "")}`);
298
1362
  restartServer2();
299
1363
  });
300
- watcher.on("add", (path2) => {
301
- logger.info(`[SPFN] Added: ${path2.replace(cwd + "/", "")}`);
1364
+ watcher.on("add", (path5) => {
1365
+ logger.info(`[SPFN] Added: ${path5.replace(cwd + "/", "")}`);
302
1366
  restartServer2();
303
1367
  });
304
- watcher.on("unlink", (path2) => {
305
- logger.info(`[SPFN] Removed: ${path2.replace(cwd + "/", "")}`);
1368
+ watcher.on("unlink", (path5) => {
1369
+ logger.info(`[SPFN] Removed: ${path5.replace(cwd + "/", "")}`);
306
1370
  restartServer2();
307
1371
  });
308
1372
  }
@@ -319,11 +1383,17 @@ await new Promise(() => {});
319
1383
  process.on("SIGTERM", cleanup2);
320
1384
  startWatcher2();
321
1385
  startServer2();
322
- await new Promise(() => {
1386
+ await new Promise((resolve2) => {
1387
+ const keepAlive = setInterval(() => {
1388
+ }, 1e6);
1389
+ process.once("beforeExit", () => {
1390
+ clearInterval(keepAlive);
1391
+ resolve2();
1392
+ });
323
1393
  });
324
1394
  return;
325
1395
  }
326
- const watchMode = options.watch !== false;
1396
+ const watchMode = options.watch === true;
327
1397
  logger.info(`Starting SPFN server + Next.js (Turbopack)${watchMode ? " (watch mode)" : ""}...
328
1398
  `);
329
1399
  let serverProcess = null;
@@ -333,7 +1403,7 @@ await new Promise(() => {});
333
1403
  const startWatcher = () => {
334
1404
  const watcherCmd = pm === "npm" ? "npx" : pm;
335
1405
  const watcherArgs = pm === "npm" ? ["tsx", watcherEntry] : ["exec", "tsx", watcherEntry];
336
- watcherProcess = execa2(watcherCmd, watcherArgs, {
1406
+ watcherProcess = execa4(watcherCmd, watcherArgs, {
337
1407
  cwd,
338
1408
  stdio: "inherit",
339
1409
  reject: false
@@ -348,7 +1418,7 @@ await new Promise(() => {});
348
1418
  const startNext = () => {
349
1419
  const nextCmd = pm === "npm" ? "npm" : pm;
350
1420
  const nextArgs = pm === "npm" ? ["run", "spfn:next"] : ["run", "spfn:next"];
351
- nextProcess = execa2(nextCmd, nextArgs, {
1421
+ nextProcess = execa4(nextCmd, nextArgs, {
352
1422
  cwd,
353
1423
  stdio: "inherit",
354
1424
  reject: false
@@ -363,7 +1433,7 @@ await new Promise(() => {});
363
1433
  const startServer = () => {
364
1434
  const serverCmd = pm === "npm" ? "npx" : pm;
365
1435
  const serverArgs = pm === "npm" ? ["tsx", serverEntry] : ["exec", "tsx", serverEntry];
366
- serverProcess = execa2(serverCmd, serverArgs, {
1436
+ serverProcess = execa4(serverCmd, serverArgs, {
367
1437
  cwd,
368
1438
  stdio: "inherit",
369
1439
  reject: false
@@ -378,7 +1448,7 @@ await new Promise(() => {});
378
1448
  serverProcess.kill("SIGTERM");
379
1449
  await serverProcess.catch(() => {
380
1450
  });
381
- await new Promise((resolve) => setTimeout(resolve, 500));
1451
+ await new Promise((resolve2) => setTimeout(resolve2, 500));
382
1452
  } catch (error) {
383
1453
  }
384
1454
  }
@@ -396,16 +1466,16 @@ await new Promise(() => {});
396
1466
  pollInterval: 50
397
1467
  }
398
1468
  });
399
- watcher.on("change", (path2) => {
400
- logger.info(`[SPFN] Changed: ${path2.replace(cwd + "/", "")}`);
1469
+ watcher.on("change", (path5) => {
1470
+ logger.info(`[SPFN] Changed: ${path5.replace(cwd + "/", "")}`);
401
1471
  restartServer();
402
1472
  });
403
- watcher.on("add", (path2) => {
404
- logger.info(`[SPFN] Added: ${path2.replace(cwd + "/", "")}`);
1473
+ watcher.on("add", (path5) => {
1474
+ logger.info(`[SPFN] Added: ${path5.replace(cwd + "/", "")}`);
405
1475
  restartServer();
406
1476
  });
407
- watcher.on("unlink", (path2) => {
408
- logger.info(`[SPFN] Removed: ${path2.replace(cwd + "/", "")}`);
1477
+ watcher.on("unlink", (path5) => {
1478
+ logger.info(`[SPFN] Removed: ${path5.replace(cwd + "/", "")}`);
409
1479
  restartServer();
410
1480
  });
411
1481
  }
@@ -425,39 +1495,50 @@ await new Promise(() => {});
425
1495
  process.on("SIGTERM", cleanup);
426
1496
  startWatcher();
427
1497
  startServer();
428
- await new Promise((resolve) => setTimeout(resolve, 2e3));
1498
+ await new Promise((resolve2) => setTimeout(resolve2, 2e3));
429
1499
  startNext();
430
- await new Promise(() => {
1500
+ await new Promise((resolve2) => {
1501
+ const keepAlive = setInterval(() => {
1502
+ }, 1e6);
1503
+ process.once("beforeExit", () => {
1504
+ clearInterval(keepAlive);
1505
+ resolve2();
1506
+ });
431
1507
  });
432
1508
  });
433
1509
 
434
1510
  // src/commands/build.ts
435
- import { Command as Command3 } from "commander";
436
- import { existsSync as existsSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
437
- import { join as join3 } from "path";
438
- import { execa as execa3 } from "execa";
439
- import ora2 from "ora";
440
- import chalk2 from "chalk";
1511
+ init_logger();
1512
+ init_package_manager();
1513
+ import { Command as Command5 } from "commander";
1514
+ import { existsSync as existsSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync2, readdirSync } from "fs";
1515
+ import { join as join12 } from "path";
1516
+ import { execa as execa5 } from "execa";
1517
+ import ora5 from "ora";
1518
+ import chalk5 from "chalk";
441
1519
  import { build } from "tsup";
442
1520
  async function buildProject(options) {
443
1521
  if (!process.env.NODE_ENV) {
444
1522
  process.env.NODE_ENV = "production";
445
1523
  }
1524
+ if (!process.env.LOG_LEVEL) {
1525
+ process.env.LOG_LEVEL = "warn";
1526
+ }
446
1527
  const cwd = process.cwd();
447
1528
  const pm = detectPackageManager(cwd);
448
- const packageJsonPath = join3(cwd, "package.json");
1529
+ const packageJsonPath = join12(cwd, "package.json");
449
1530
  let hasNext = false;
450
- if (existsSync3(packageJsonPath)) {
1531
+ if (existsSync12(packageJsonPath)) {
451
1532
  const packageJson = JSON.parse(await import("fs").then(
452
- (fs2) => fs2.promises.readFile(packageJsonPath, "utf-8")
1533
+ (fs5) => fs5.promises.readFile(packageJsonPath, "utf-8")
453
1534
  ));
454
1535
  hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
455
1536
  }
456
- const serverDir = join3(cwd, "src", "server");
457
- const hasServer = existsSync3(serverDir);
458
- console.log(chalk2.blue.bold("\n\u{1F3D7}\uFE0F Building SPFN project for production...\n"));
1537
+ const serverDir = join12(cwd, "src", "server");
1538
+ const hasServer = existsSync12(serverDir);
1539
+ console.log(chalk5.blue.bold("\n\u{1F3D7}\uFE0F Building SPFN project for production...\n"));
459
1540
  if (hasServer) {
460
- const spinner = ora2("Generating API client...").start();
1541
+ const spinner = ora5("Generating API client...").start();
461
1542
  try {
462
1543
  const { CodegenOrchestrator, loadCodegenConfig, createGeneratorsFromConfig } = await import("@spfn/core/codegen");
463
1544
  const config = loadCodegenConfig(cwd);
@@ -465,7 +1546,7 @@ async function buildProject(options) {
465
1546
  const orchestrator = new CodegenOrchestrator({
466
1547
  generators,
467
1548
  cwd,
468
- debug: false
1549
+ debug: true
469
1550
  });
470
1551
  await orchestrator.generateAll();
471
1552
  spinner.succeed("API client generated");
@@ -475,9 +1556,9 @@ async function buildProject(options) {
475
1556
  }
476
1557
  }
477
1558
  if (hasNext && !options.serverOnly) {
478
- const spinner = ora2("Building Next.js...").start();
1559
+ const spinner = ora5("Building Next.js...").start();
479
1560
  try {
480
- await execa3(pm, ["run", "build"], {
1561
+ await execa5(pm, ["run", "build"], {
481
1562
  cwd,
482
1563
  stdio: "inherit"
483
1564
  });
@@ -489,12 +1570,12 @@ async function buildProject(options) {
489
1570
  }
490
1571
  }
491
1572
  if (hasServer && !options.nextOnly) {
492
- const spinner = ora2("Building SPFN server...").start();
1573
+ const spinner = ora5("Building SPFN server...").start();
493
1574
  try {
494
- const outputDir = join3(cwd, ".spfn", "server");
1575
+ const outputDir = join12(cwd, ".spfn", "server");
495
1576
  mkdirSync2(outputDir, { recursive: true });
496
- const serverDir2 = join3(cwd, "src", "server");
497
- if (!existsSync3(serverDir2)) {
1577
+ const serverDir2 = join12(cwd, "src", "server");
1578
+ if (!existsSync12(serverDir2)) {
498
1579
  spinner.fail("SPFN server build failed");
499
1580
  logger.error("src/server/ directory not found");
500
1581
  logger.error('Please run "spfn init" to initialize the project.');
@@ -518,15 +1599,14 @@ async function buildProject(options) {
518
1599
  "@sinclair/typebox",
519
1600
  "@spfn/core"
520
1601
  ],
521
- silent: false,
1602
+ silent: true,
522
1603
  onSuccess: async () => {
523
1604
  }
524
1605
  });
525
- const prodServerPath = join3(cwd, ".spfn", "prod-server.mjs");
1606
+ const prodServerPath = join12(cwd, ".spfn", "prod-server.mjs");
526
1607
  const prodServerContent = `// Load environment variables FIRST (before any imports that depend on them)
527
1608
  // Use centralized environment loader for standard dotenv priority
528
- const { loadEnvironment } = await import('@spfn/core/env');
529
- loadEnvironment({ debug: false });
1609
+ const { env } = await import('@spfn/core/config');
530
1610
 
531
1611
  // Now import server (logger singleton will be created with correct NODE_ENV)
532
1612
  const { startServer } = await import('@spfn/core/server');
@@ -537,8 +1617,8 @@ import { dirname } from 'path';
537
1617
  const __dirname = dirname(fileURLToPath(import.meta.url));
538
1618
 
539
1619
  // Environment variables: from .env files OR injected by container/kubernetes
540
- const port = process.env.SPFN_PORT || process.env.PORT || '8790';
541
- const host = process.env.SPFN_HOST || process.env.HOST || '0.0.0.0';
1620
+ const port = env.SPFN_PORT || '8790';
1621
+ const host = env.SPFN_HOST || '0.0.0.0';
542
1622
 
543
1623
  await startServer({
544
1624
  port: Number(port),
@@ -547,11 +1627,41 @@ await startServer({
547
1627
  debug: false
548
1628
  });
549
1629
  `;
550
- writeFileSync2(prodServerPath, prodServerContent);
1630
+ writeFileSync7(prodServerPath, prodServerContent);
551
1631
  spinner.succeed(`SPFN server build completed \u2192 .spfn/server`);
1632
+ const routesDir = join12(cwd, ".spfn", "server", "routes");
1633
+ if (existsSync12(routesDir)) {
1634
+ console.log();
1635
+ console.log(chalk5.bold("Route (api)"));
1636
+ try {
1637
+ const routes = readdirSync(routesDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name).sort();
1638
+ if (routes.length > 0) {
1639
+ routes.forEach((route, index) => {
1640
+ const isLast = index === routes.length - 1;
1641
+ const prefix = isLast ? "\u2514" : "\u251C";
1642
+ const routePath = `/api/${route}`;
1643
+ if (route === "health") {
1644
+ console.log(`${prefix} ${chalk5.green("GET")} ${routePath}`);
1645
+ } else {
1646
+ console.log(`${prefix} ${chalk5.cyan("*")} ${routePath}`);
1647
+ }
1648
+ });
1649
+ console.log();
1650
+ console.log(chalk5.cyan("*") + " (Hono) multiple methods supported");
1651
+ }
1652
+ } catch (error) {
1653
+ }
1654
+ }
552
1655
  } catch (error) {
553
1656
  spinner.fail("SPFN server build failed");
554
- logger.error(String(error));
1657
+ if (error instanceof Error) {
1658
+ console.error("\n" + chalk5.red(error.message));
1659
+ if (error.stack) {
1660
+ console.error(chalk5.dim("\n" + error.stack));
1661
+ }
1662
+ } else {
1663
+ logger.error(String(error));
1664
+ }
555
1665
  process.exit(1);
556
1666
  }
557
1667
  }
@@ -559,41 +1669,42 @@ await startServer({
559
1669
  logger.error("No Next.js or SPFN server found in this project.");
560
1670
  process.exit(1);
561
1671
  }
562
- console.log("\n" + chalk2.green.bold("\u2713 Build completed successfully!\n"));
563
- console.log(chalk2.bold("Next steps:\n"));
564
- console.log(" " + chalk2.cyan("Start production server:"));
565
- console.log(` ${chalk2.cyan(pm === "npm" ? "npm run" : pm + " run")} spfn:start ${chalk2.gray("# Start SPFN + Next.js")}
1672
+ console.log("\n" + chalk5.green.bold("\u2713 Build completed successfully!\n"));
1673
+ console.log(chalk5.bold("Next steps:\n"));
1674
+ console.log(" " + chalk5.cyan("Start production server:"));
1675
+ console.log(` ${chalk5.cyan(pm === "npm" ? "npm run" : pm + " run")} spfn:start ${chalk5.gray("# Start SPFN + Next.js")}
566
1676
  `);
567
- console.log(" " + chalk2.cyan("Or deploy with Docker:"));
568
- console.log(` ${chalk2.cyan("docker compose -f docker-compose.production.yml up --build -d")}
1677
+ console.log(" " + chalk5.cyan("Or deploy with Docker:"));
1678
+ console.log(` ${chalk5.cyan("docker compose -f docker-compose.production.yml up --build -d")}
569
1679
  `);
570
- console.log(chalk2.dim(" \u{1F4D6} See .guide/deployment.md for complete deployment guide\n"));
571
1680
  }
572
- var buildCommand = new Command3("build").description("Build SPFN project for production (Next.js + Server)").option("--server-only", "Build only SPFN server (skip Next.js)").option("--next-only", "Build only Next.js (skip SPFN server)").option("--turbo", "Use Turbopack for Next.js build").action(async (options) => {
1681
+ var buildCommand = new Command5("build").description("Build SPFN project for production (Next.js + Server)").option("--server-only", "Build only SPFN server (skip Next.js)").option("--next-only", "Build only Next.js (skip SPFN server)").option("--turbo", "Use Turbopack for Next.js build").action(async (options) => {
573
1682
  await buildProject(options);
574
1683
  });
575
1684
 
576
1685
  // src/commands/start.ts
577
- import { Command as Command4 } from "commander";
578
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
579
- import { join as join4 } from "path";
580
- import { execa as execa4 } from "execa";
581
- import chalk3 from "chalk";
582
- var startCommand = new Command4("start").description("Start SPFN production server (Next.js + Hono)").option("--server-only", "Run only SPFN server (skip Next.js)").option("--next-only", "Run only Next.js (skip SPFN server)").option("-p, --port <port>", "Server port", "8790").option("-h, --host <host>", "Server host", "0.0.0.0").action(async (options) => {
1686
+ init_logger();
1687
+ init_package_manager();
1688
+ import { Command as Command6 } from "commander";
1689
+ import { existsSync as existsSync13, readFileSync as readFileSync4 } from "fs";
1690
+ import { join as join13 } from "path";
1691
+ import { execa as execa6 } from "execa";
1692
+ import chalk6 from "chalk";
1693
+ var startCommand = new Command6("start").description("Start SPFN production server (Next.js + Hono)").option("--server-only", "Run only SPFN server (skip Next.js)").option("--next-only", "Run only Next.js (skip SPFN server)").option("-p, --port <port>", "Server port", "8790").option("-h, --host <host>", "Server host", "0.0.0.0").action(async (options) => {
583
1694
  if (!process.env.NODE_ENV) {
584
1695
  process.env.NODE_ENV = "production";
585
1696
  }
586
1697
  const cwd = process.cwd();
587
- const packageJsonPath = join4(cwd, "package.json");
1698
+ const packageJsonPath = join13(cwd, "package.json");
588
1699
  let hasNext = false;
589
- if (existsSync4(packageJsonPath)) {
590
- const packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
1700
+ if (existsSync13(packageJsonPath)) {
1701
+ const packageJson = JSON.parse(readFileSync4(packageJsonPath, "utf-8"));
591
1702
  hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
592
1703
  }
593
- const builtServerDir = join4(cwd, ".spfn", "server");
594
- const hasBuiltServer = existsSync4(builtServerDir);
595
- const nextBuildDir = join4(cwd, ".next");
596
- const hasNextBuild = existsSync4(nextBuildDir);
1704
+ const builtServerDir = join13(cwd, ".spfn", "server");
1705
+ const hasBuiltServer = existsSync13(builtServerDir);
1706
+ const nextBuildDir = join13(cwd, ".next");
1707
+ const hasNextBuild = existsSync13(nextBuildDir);
597
1708
  if (!options.nextOnly && !hasBuiltServer) {
598
1709
  logger.error('.spfn/server directory not found. Please run "spfn build" first.');
599
1710
  process.exit(1);
@@ -603,8 +1714,8 @@ var startCommand = new Command4("start").description("Start SPFN production serv
603
1714
  process.exit(1);
604
1715
  }
605
1716
  const pm = detectPackageManager(cwd);
606
- const serverEntry = join4(cwd, ".spfn", "prod-server.mjs");
607
- if (!existsSync4(serverEntry)) {
1717
+ const serverEntry = join13(cwd, ".spfn", "prod-server.mjs");
1718
+ if (!existsSync13(serverEntry)) {
608
1719
  logger.error('.spfn/prod-server.mjs not found. Please run "spfn build" first.');
609
1720
  process.exit(1);
610
1721
  }
@@ -614,7 +1725,7 @@ var startCommand = new Command4("start").description("Start SPFN production serv
614
1725
  logger.info(`Starting SPFN Server (production) on http://${options.host}:${options.port}
615
1726
  `);
616
1727
  try {
617
- await execa4("node", [serverEntry], {
1728
+ await execa6("node", [serverEntry], {
618
1729
  stdio: "inherit",
619
1730
  cwd,
620
1731
  env: { ...process.env }
@@ -628,7 +1739,7 @@ var startCommand = new Command4("start").description("Start SPFN production serv
628
1739
  if (options.nextOnly) {
629
1740
  logger.info("Starting Next.js (production) on http://0.0.0.0:3790\n");
630
1741
  try {
631
- await execa4("npx", ["next", "start", "-H", "0.0.0.0", "-p", "3790"], {
1742
+ await execa6("npx", ["next", "start", "-H", "0.0.0.0", "-p", "3790"], {
632
1743
  stdio: "inherit",
633
1744
  cwd
634
1745
  });
@@ -640,12 +1751,12 @@ var startCommand = new Command4("start").description("Start SPFN production serv
640
1751
  }
641
1752
  const nextCmd = "next start -H 0.0.0.0 -p 3790";
642
1753
  const serverCmd = `node ${serverEntry}`;
643
- console.log(chalk3.blue.bold("\n\u{1F680} Starting SPFN production server...\n"));
1754
+ console.log(chalk6.blue.bold("\n\u{1F680} Starting SPFN production server...\n"));
644
1755
  logger.info("Next.js: http://0.0.0.0:3790");
645
1756
  logger.info(`SPFN API: http://${options.host}:${options.port}
646
1757
  `);
647
1758
  try {
648
- await execa4(
1759
+ await execa6(
649
1760
  pm === "npm" ? "npx" : pm,
650
1761
  pm === "npm" ? ["concurrently", "--raw", "--kill-others", `"${nextCmd}"`, `"${serverCmd}"`] : ["exec", "concurrently", "--raw", "--kill-others", `"${nextCmd}"`, `"${serverCmd}"`],
651
1762
  {
@@ -666,15 +1777,16 @@ var startCommand = new Command4("start").description("Start SPFN production serv
666
1777
  });
667
1778
 
668
1779
  // src/commands/codegen.ts
669
- import { Command as Command5 } from "commander";
670
- import { existsSync as existsSync5, writeFileSync as writeFileSync3 } from "fs";
671
- import { join as join5 } from "path";
672
- import chalk4 from "chalk";
1780
+ init_logger();
1781
+ import { Command as Command7 } from "commander";
1782
+ import { existsSync as existsSync14, writeFileSync as writeFileSync8 } from "fs";
1783
+ import { join as join14 } from "path";
1784
+ import chalk7 from "chalk";
673
1785
  async function initCodegen(options) {
674
1786
  const cwd = process.cwd();
675
- const rcPath = join5(cwd, ".spfnrc.json");
676
- if (existsSync5(rcPath)) {
677
- logger.warn(".spfnrc.json already exists");
1787
+ const rcPath = join14(cwd, ".spfnrc.ts");
1788
+ if (existsSync14(rcPath)) {
1789
+ logger.warn(".spfnrc.ts already exists");
678
1790
  logger.info("Edit manually to add custom generators");
679
1791
  process.exit(0);
680
1792
  }
@@ -688,15 +1800,15 @@ async function initCodegen(options) {
688
1800
  ]
689
1801
  }
690
1802
  };
691
- writeFileSync3(rcPath, JSON.stringify(config, null, 2) + "\n");
692
- console.log("\n" + chalk4.green.bold("\u2713 Created .spfnrc.json\n"));
1803
+ writeFileSync8(rcPath, JSON.stringify(config, null, 2) + "\n");
1804
+ console.log("\n" + chalk7.green.bold("\u2713 Created .spfnrc.ts\n"));
693
1805
  console.log("Configuration:");
694
- console.log(chalk4.gray(JSON.stringify(config, null, 2)));
1806
+ console.log(chalk7.gray(JSON.stringify(config, null, 2)));
695
1807
  if (options.withExample) {
696
- console.log("\n" + chalk4.yellow("\u{1F4DD} To add custom generators:"));
1808
+ console.log("\n" + chalk7.yellow("\u{1F4DD} To add custom generators:"));
697
1809
  console.log(" 1. Create your generator file (e.g., src/generators/my-generator.ts)");
698
- console.log(" 2. Add to .spfnrc.json:");
699
- console.log(chalk4.gray(`
1810
+ console.log(" 2. Add to .spfnrc.ts:");
1811
+ console.log(chalk7.gray(`
700
1812
  {
701
1813
  "codegen": {
702
1814
  "generators": [
@@ -706,11 +1818,11 @@ async function initCodegen(options) {
706
1818
  }
707
1819
  }
708
1820
  `));
709
- console.log(" 3. Run: " + chalk4.cyan("spfn dev") + " (generators run automatically)");
1821
+ console.log(" 3. Run: " + chalk7.cyan("spfn dev") + " (generators run automatically)");
710
1822
  } else {
711
- console.log("\n" + chalk4.yellow("\u{1F4DD} Next steps:"));
712
- console.log(" \u2022 Add custom generators to .spfnrc.json");
713
- console.log(" \u2022 Run: " + chalk4.cyan("spfn dev") + " to start development with code generation");
1823
+ console.log("\n" + chalk7.yellow("\u{1F4DD} Next steps:"));
1824
+ console.log(" \u2022 Add custom generators to .spfnrc.ts");
1825
+ console.log(" \u2022 Run: " + chalk7.cyan("spfn dev") + " to start development with code generation");
714
1826
  }
715
1827
  }
716
1828
  async function listGenerators() {
@@ -723,10 +1835,10 @@ async function listGenerators() {
723
1835
  logger.info('Run "spfn codegen init" to initialize configuration');
724
1836
  return;
725
1837
  }
726
- console.log("\n" + chalk4.bold("Registered Generators:"));
1838
+ console.log("\n" + chalk7.bold("Registered Generators:"));
727
1839
  generators.forEach((gen, index) => {
728
- console.log(` ${index + 1}. ${chalk4.cyan(gen.name)}`);
729
- console.log(` Patterns: ${chalk4.gray(gen.watchPatterns.join(", "))}`);
1840
+ console.log(` ${index + 1}. ${chalk7.cyan(gen.name)}`);
1841
+ console.log(` Patterns: ${chalk7.gray(gen.watchPatterns.join(", "))}`);
730
1842
  });
731
1843
  console.log("");
732
1844
  }
@@ -744,21 +1856,22 @@ async function runGenerators() {
744
1856
  const orchestrator = new CodegenOrchestrator({
745
1857
  generators,
746
1858
  cwd,
747
- debug: false
1859
+ debug: true
748
1860
  });
749
1861
  await orchestrator.generateAll();
750
- console.log("\n" + chalk4.green.bold("\u2713 Code generation completed"));
1862
+ console.log("\n" + chalk7.green.bold("\u2713 Code generation completed"));
751
1863
  }
752
- var codegenCommand = new Command5("codegen").description("Code generation management");
753
- codegenCommand.command("init").description("Initialize .spfnrc.json with codegen configuration").option("--with-example", "Show example custom generator usage").action(initCodegen);
1864
+ var codegenCommand = new Command7("codegen").description("Code generation management");
1865
+ codegenCommand.command("init").description("Initialize .spfnrc.ts with codegen configuration").option("--with-example", "Show example custom generator usage").action(initCodegen);
754
1866
  codegenCommand.command("list").alias("ls").description("List registered code generators").action(listGenerators);
755
1867
  codegenCommand.command("run").description("Run code generators once (no watch mode)").action(runGenerators);
756
1868
 
757
1869
  // src/commands/key.ts
758
- import { Command as Command6 } from "commander";
1870
+ init_logger();
1871
+ import { Command as Command8 } from "commander";
759
1872
  import { randomBytes } from "crypto";
760
1873
  import { execSync } from "child_process";
761
- import chalk5 from "chalk";
1874
+ import chalk8 from "chalk";
762
1875
  var PRESETS = {
763
1876
  "auth-encryption": {
764
1877
  bytes: 32,
@@ -809,62 +1922,62 @@ function copyToClipboard(text) {
809
1922
  }
810
1923
  }
811
1924
  function generateSecret(bytes, preset, envVarName, copy) {
812
- const key = randomBytes(bytes).toString("hex");
1925
+ const key = randomBytes(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
813
1926
  const config = preset ? PRESETS[preset] : null;
814
- console.log("\n" + chalk5.green.bold("\u2713 Generated secret key:"));
1927
+ console.log("\n" + chalk8.green.bold("\u2713 Generated secret key:"));
815
1928
  if (config) {
816
- console.log(chalk5.dim(` ${config.description} (${bytes * 8} bits)`));
1929
+ console.log(chalk8.dim(` ${config.description} (${bytes * 8} bits)`));
817
1930
  } else {
818
- console.log(chalk5.dim(` ${bytes * 8}-bit secret`));
1931
+ console.log(chalk8.dim(` ${bytes * 8}-bit secret`));
819
1932
  }
820
- console.log("\n" + chalk5.cyan(key) + "\n");
1933
+ console.log("\n" + chalk8.cyan(key) + "\n");
821
1934
  const varName = envVarName || config?.envVar || "SECRET_KEY";
822
- console.log(chalk5.dim("Add to your .env file:"));
823
- console.log(chalk5.yellow(`${varName}=${key}
1935
+ console.log(chalk8.dim("Add to your .env file:"));
1936
+ console.log(chalk8.yellow(`${varName}=${key}
824
1937
  `));
825
1938
  if (config?.usage) {
826
- console.log(chalk5.dim("Usage:"));
827
- console.log(chalk5.gray(` ${config.usage}
1939
+ console.log(chalk8.dim("Usage:"));
1940
+ console.log(chalk8.gray(` ${config.usage}
828
1941
  `));
829
1942
  }
830
1943
  if (copy) {
831
1944
  if (copyToClipboard(key)) {
832
- console.log(chalk5.green("\u2713 Copied to clipboard!\n"));
1945
+ console.log(chalk8.green("\u2713 Copied to clipboard!\n"));
833
1946
  } else {
834
1947
  logger.warn("Could not copy to clipboard");
835
1948
  }
836
1949
  }
837
1950
  }
838
1951
  function listPresets() {
839
- console.log("\n" + chalk5.bold("Available presets:"));
1952
+ console.log("\n" + chalk8.bold("Available presets:"));
840
1953
  console.log();
841
1954
  Object.entries(PRESETS).forEach(([name, config]) => {
842
- console.log(` ${chalk5.cyan(name.padEnd(20))} ${chalk5.dim(config.description)}`);
843
- console.log(` ${" ".repeat(20)} ${chalk5.gray(`\u2192 ${config.envVar} (${config.bytes * 8} bits)`)}`);
1955
+ console.log(` ${chalk8.cyan(name.padEnd(20))} ${chalk8.dim(config.description)}`);
1956
+ console.log(` ${" ".repeat(20)} ${chalk8.gray(`\u2192 ${config.envVar} (${config.bytes * 8} bits)`)}`);
844
1957
  console.log();
845
1958
  });
846
- console.log(chalk5.dim("Usage:"));
847
- console.log(chalk5.gray(" spfn key <preset>"));
848
- console.log(chalk5.gray(" spfn key auth-encryption --copy"));
1959
+ console.log(chalk8.dim("Usage:"));
1960
+ console.log(chalk8.gray(" spfn key <preset>"));
1961
+ console.log(chalk8.gray(" spfn key auth-encryption --copy"));
849
1962
  console.log();
850
1963
  }
851
- var generateValueCommand = new Command6("generate").alias("gen").description("Generate random value (simple output, no metadata)").option("-b, --bytes <number>", "Number of random bytes", "32").option("-c, --copy", "Copy to clipboard").action((options) => {
1964
+ var generateValueCommand = new Command8("generate").alias("gen").description("Generate random value (simple output, no metadata)").option("-b, --bytes <number>", "Number of random bytes", "32").option("-c, --copy", "Copy to clipboard").action((options) => {
852
1965
  const bytes = parseInt(options.bytes, 10);
853
1966
  if (isNaN(bytes) || bytes < 1 || bytes > 128) {
854
1967
  logger.error("Invalid bytes value. Must be between 1 and 128.");
855
1968
  process.exit(1);
856
1969
  }
857
- const value = randomBytes(bytes).toString("hex");
1970
+ const value = randomBytes(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
858
1971
  console.log(value);
859
1972
  if (options.copy) {
860
1973
  if (copyToClipboard(value)) {
861
- console.error(chalk5.green("\u2713 Copied to clipboard"));
1974
+ console.error(chalk8.green("\u2713 Copied to clipboard"));
862
1975
  } else {
863
- console.error(chalk5.yellow("\u26A0 Could not copy to clipboard"));
1976
+ console.error(chalk8.yellow("\u26A0 Could not copy to clipboard"));
864
1977
  }
865
1978
  }
866
1979
  });
867
- var keyCommand = new Command6("key").alias("k").description("Generate secure random keys and secrets").argument("[preset]", `Preset type (use --list to see all)`).option("-l, --list", "List all available presets").option("-b, --bytes <number>", "Number of random bytes to generate", "32").option("-e, --env <name>", "Environment variable name").option("-c, --copy", "Copy to clipboard").action((preset, options) => {
1980
+ var keyCommand = new Command8("key").alias("k").description("Generate secure random keys and secrets").argument("[preset]", `Preset type (use --list to see all)`).option("-l, --list", "List all available presets").option("-b, --bytes <number>", "Number of random bytes to generate", "32").option("-e, --env <name>", "Environment variable name").option("-c, --copy", "Copy to clipboard").action((preset, options) => {
868
1981
  if (options.list) {
869
1982
  listPresets();
870
1983
  return;
@@ -878,9 +1991,9 @@ var keyCommand = new Command6("key").alias("k").description("Generate secure ran
878
1991
  logger.error(`Unknown preset: ${preset}`);
879
1992
  console.log("\nAvailable presets:");
880
1993
  Object.entries(PRESETS).forEach(([name, config]) => {
881
- console.log(` ${chalk5.cyan(name)}: ${config.description}`);
1994
+ console.log(` ${chalk8.cyan(name)}: ${config.description}`);
882
1995
  });
883
- console.log("\nUse " + chalk5.cyan("--list") + " to see detailed information");
1996
+ console.log("\nUse " + chalk8.cyan("--list") + " to see detailed information");
884
1997
  process.exit(1);
885
1998
  }
886
1999
  generateSecret(
@@ -892,41 +2005,145 @@ var keyCommand = new Command6("key").alias("k").description("Generate secure ran
892
2005
  });
893
2006
  keyCommand.addCommand(generateValueCommand);
894
2007
 
895
- // src/commands/db.ts
896
- import { Command as Command7 } from "commander";
897
- import { existsSync as existsSync6, writeFileSync as writeFileSync4, unlinkSync } from "fs";
898
- import { promises as fs } from "fs";
899
- import path from "path";
900
- import { exec, spawn } from "child_process";
901
- import { promisify } from "util";
902
- import chalk6 from "chalk";
903
- import ora3 from "ora";
904
- import prompts2 from "prompts";
905
- import net from "net";
906
- var execAsync = promisify(exec);
907
- async function isPortAvailable(port) {
908
- return new Promise((resolve) => {
909
- const server = net.createServer();
910
- server.once("error", () => {
911
- server.close();
912
- resolve(false);
2008
+ // src/index.ts
2009
+ init_setup();
2010
+
2011
+ // src/commands/db/index.ts
2012
+ import { Command as Command9 } from "commander";
2013
+
2014
+ // src/commands/db/generate.ts
2015
+ import chalk10 from "chalk";
2016
+
2017
+ // src/commands/db/utils/drizzle.ts
2018
+ import { existsSync as existsSync15, writeFileSync as writeFileSync9, unlinkSync } from "fs";
2019
+ import { spawn } from "child_process";
2020
+ import chalk9 from "chalk";
2021
+ import ora6 from "ora";
2022
+ import { env } from "@spfn/core/config";
2023
+ import { loadEnvFiles } from "@spfn/core/server";
2024
+ function validateDatabasePrerequisites() {
2025
+ loadEnvFiles();
2026
+ if (!env.DATABASE_URL) {
2027
+ console.error(chalk9.red("\u274C DATABASE_URL not found in environment"));
2028
+ console.log(chalk9.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2029
+ throw new Error("DATABASE_URL is required for database operations");
2030
+ }
2031
+ }
2032
+ async function runDrizzleCommand(command) {
2033
+ const hasUserConfig = existsSync15("./drizzle.config.ts");
2034
+ const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
2035
+ const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
2036
+ if (!hasUserConfig) {
2037
+ loadEnvFiles();
2038
+ if (!env.DATABASE_URL) {
2039
+ console.error(chalk9.red("\u274C DATABASE_URL not found in environment"));
2040
+ console.log(chalk9.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2041
+ process.exit(1);
2042
+ }
2043
+ const { generateDrizzleConfigFile } = await import("@spfn/core/db");
2044
+ const configContent = generateDrizzleConfigFile({
2045
+ cwd: process.cwd(),
2046
+ // Exclude package schemas to avoid .ts/.js mixing (packages use migrations instead)
2047
+ disablePackageDiscovery: true,
2048
+ // Expand globs and auto-detect PostgreSQL schemas for push/generate compatibility
2049
+ expandGlobs: true,
2050
+ autoDetectSchemas: true
913
2051
  });
914
- server.once("listening", () => {
915
- server.close();
916
- resolve(true);
2052
+ writeFileSync9(tempConfigPath, configContent);
2053
+ console.log(chalk9.dim("Using auto-generated Drizzle config\n"));
2054
+ }
2055
+ const args = command.split(" ");
2056
+ args.push(`--config=${configPath}`);
2057
+ return new Promise((resolve2, reject) => {
2058
+ const drizzleProcess = spawn("drizzle-kit", args, {
2059
+ stdio: "inherit",
2060
+ // Allow interactive input
2061
+ shell: true
2062
+ });
2063
+ const cleanup = () => {
2064
+ if (!hasUserConfig && existsSync15(tempConfigPath)) {
2065
+ unlinkSync(tempConfigPath);
2066
+ }
2067
+ };
2068
+ drizzleProcess.on("close", (code) => {
2069
+ cleanup();
2070
+ if (code === 0) {
2071
+ resolve2();
2072
+ } else {
2073
+ reject(new Error(`drizzle-kit ${command} exited with code ${code}`));
2074
+ }
2075
+ });
2076
+ drizzleProcess.on("error", (error) => {
2077
+ cleanup();
2078
+ reject(error);
917
2079
  });
918
- server.listen(port, "127.0.0.1");
919
2080
  });
920
2081
  }
921
- async function findAvailablePort(startPort, maxAttempts = 10) {
922
- for (let i = 0; i < maxAttempts; i++) {
923
- const port = startPort + i;
924
- if (await isPortAvailable(port)) {
925
- return port;
2082
+ async function runWithSpinner(spinnerText, command, successMessage, failMessage) {
2083
+ const spinner = ora6(spinnerText).start();
2084
+ try {
2085
+ spinner.stop();
2086
+ await runDrizzleCommand(command);
2087
+ console.log(chalk9.green(`\u2705 ${successMessage}`));
2088
+ } catch (error) {
2089
+ spinner.fail(failMessage);
2090
+ console.error(chalk9.red(error instanceof Error ? error.message : "Unknown error"));
2091
+ process.exit(1);
2092
+ }
2093
+ }
2094
+
2095
+ // src/commands/db/generate.ts
2096
+ async function dbGenerate() {
2097
+ try {
2098
+ await runDrizzleCommand("generate");
2099
+ console.log(chalk10.green("\n\u2705 Migrations generated successfully"));
2100
+ } catch (error) {
2101
+ console.error(chalk10.red("\n\u274C Failed to generate migrations"));
2102
+ console.error(chalk10.red(error instanceof Error ? error.message : "Unknown error"));
2103
+ process.exit(1);
2104
+ }
2105
+ }
2106
+
2107
+ // src/commands/db/push.ts
2108
+ import chalk12 from "chalk";
2109
+ import "@spfn/core/config";
2110
+ async function dbPush() {
2111
+ await runWithSpinner(
2112
+ "Pushing schema changes to database...",
2113
+ "push",
2114
+ "Schema pushed successfully",
2115
+ "Failed to push schema"
2116
+ );
2117
+ const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
2118
+ const functions = discoverFunctionMigrations2(process.cwd());
2119
+ if (functions.length > 0) {
2120
+ console.log(chalk12.blue("\n\u{1F4E6} Applying function package migrations:"));
2121
+ functions.forEach((func) => {
2122
+ console.log(chalk12.dim(` - ${func.packageName}`));
2123
+ });
2124
+ try {
2125
+ await executeFunctionMigrations2(functions);
2126
+ console.log(chalk12.green("\n\u2705 All function migrations applied\n"));
2127
+ } catch (error) {
2128
+ console.error(chalk12.red("\n\u274C Failed to apply function migrations"));
2129
+ console.error(chalk12.red(error instanceof Error ? error.message : "Unknown error"));
2130
+ process.exit(1);
926
2131
  }
927
2132
  }
928
- throw new Error(`No available ports found between ${startPort} and ${startPort + maxAttempts - 1}`);
929
2133
  }
2134
+
2135
+ // src/commands/db/migrate.ts
2136
+ import chalk16 from "chalk";
2137
+
2138
+ // src/commands/db/backup.ts
2139
+ import { promises as fs3 } from "fs";
2140
+ import path3 from "path";
2141
+ import { spawn as spawn2 } from "child_process";
2142
+ import chalk15 from "chalk";
2143
+ import ora7 from "ora";
2144
+
2145
+ // src/commands/db/utils/database.ts
2146
+ import net from "net";
930
2147
  function parseDatabaseUrl(dbUrl) {
931
2148
  try {
932
2149
  const url = new URL(dbUrl);
@@ -942,61 +2159,64 @@ function parseDatabaseUrl(dbUrl) {
942
2159
  throw new Error(`Invalid DATABASE_URL format: ${error instanceof Error ? error.message : "Unknown error"}`);
943
2160
  }
944
2161
  }
945
- function formatBytes(bytes) {
946
- if (bytes === 0) {
947
- return "0 B";
948
- }
949
- const k = 1024;
950
- const sizes = ["B", "KB", "MB", "GB"];
951
- const i = Math.floor(Math.log(bytes) / Math.log(k));
952
- return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
953
- }
954
- function formatTimestamp() {
955
- const now = /* @__PURE__ */ new Date();
956
- const year = now.getFullYear();
957
- const month = String(now.getMonth() + 1).padStart(2, "0");
958
- const day = String(now.getDate()).padStart(2, "0");
959
- const hours = String(now.getHours()).padStart(2, "0");
960
- const minutes = String(now.getMinutes()).padStart(2, "0");
961
- const seconds = String(now.getSeconds()).padStart(2, "0");
962
- return `${year}-${month}-${day}_${hours}${minutes}${seconds}`;
2162
+ async function isPortAvailable(port) {
2163
+ return new Promise((resolve2) => {
2164
+ const server = net.createServer();
2165
+ server.once("error", () => {
2166
+ server.close();
2167
+ resolve2(false);
2168
+ });
2169
+ server.once("listening", () => {
2170
+ server.close();
2171
+ resolve2(true);
2172
+ });
2173
+ server.listen(port, "127.0.0.1");
2174
+ });
963
2175
  }
964
- async function ensureBackupInGitignore() {
965
- const gitignorePath = path.join(process.cwd(), ".gitignore");
966
- try {
967
- let content = "";
968
- let exists = existsSync6(gitignorePath);
969
- if (exists) {
970
- content = await fs.readFile(gitignorePath, "utf-8");
971
- }
972
- const lines = content.split("\n");
973
- const hasBackupsIgnore = lines.some(
974
- (line) => line.trim() === "backups/" || line.trim() === "/backups/" || line.trim() === "backups"
975
- );
976
- if (!hasBackupsIgnore) {
977
- const entry = exists && content && !content.endsWith("\n") ? "\n\n# Database backups\nbackups/\n" : "# Database backups\nbackups/\n";
978
- await fs.appendFile(gitignorePath, entry);
979
- console.log(chalk6.dim("\u2713 Added backups/ to .gitignore"));
2176
+ async function findAvailablePort(startPort, maxAttempts = 10) {
2177
+ for (let i = 0; i < maxAttempts; i++) {
2178
+ const port = startPort + i;
2179
+ if (await isPortAvailable(port)) {
2180
+ return port;
980
2181
  }
981
- } catch (error) {
982
- console.log(chalk6.dim("\u26A0\uFE0F Could not update .gitignore"));
983
2182
  }
2183
+ throw new Error(`No available ports found between ${startPort} and ${startPort + maxAttempts - 1}`);
2184
+ }
2185
+
2186
+ // src/commands/db/utils/formatters.ts
2187
+ function formatBytes(bytes) {
2188
+ if (bytes === 0) {
2189
+ return "0 B";
2190
+ }
2191
+ const k = 1024;
2192
+ const sizes = ["B", "KB", "MB", "GB"];
2193
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
2194
+ return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
984
2195
  }
985
- async function ensureBackupDir() {
986
- const backupDir = path.join(process.cwd(), "backups");
987
- try {
988
- await fs.mkdir(backupDir, { recursive: true });
989
- const gitignorePath = path.join(backupDir, ".gitignore");
990
- const gitignoreExists = existsSync6(gitignorePath);
991
- if (!gitignoreExists) {
992
- await fs.writeFile(gitignorePath, "# Ignore all backup files\n*.sql\n*.dump\n*.meta.json\n");
993
- }
994
- await ensureBackupInGitignore();
995
- return backupDir;
996
- } catch (error) {
997
- throw new Error(`Failed to create backup directory: ${error instanceof Error ? error.message : "Unknown error"}`);
998
- }
2196
+ function formatTimestamp() {
2197
+ const now = /* @__PURE__ */ new Date();
2198
+ const year = now.getFullYear();
2199
+ const month = String(now.getMonth() + 1).padStart(2, "0");
2200
+ const day = String(now.getDate()).padStart(2, "0");
2201
+ const hours = String(now.getHours()).padStart(2, "0");
2202
+ const minutes = String(now.getMinutes()).padStart(2, "0");
2203
+ const seconds = String(now.getSeconds()).padStart(2, "0");
2204
+ return `${year}-${month}-${day}_${hours}${minutes}${seconds}`;
999
2205
  }
2206
+
2207
+ // src/commands/db/utils/backup-files.ts
2208
+ import { promises as fs2 } from "fs";
2209
+ import { existsSync as existsSync17 } from "fs";
2210
+ import path2 from "path";
2211
+ import chalk14 from "chalk";
2212
+
2213
+ // src/commands/db/utils/metadata.ts
2214
+ import { promises as fs } from "fs";
2215
+ import path from "path";
2216
+ import { promisify } from "util";
2217
+ import { exec } from "child_process";
2218
+ import chalk13 from "chalk";
2219
+ var execAsync = promisify(exec);
1000
2220
  async function collectGitInfo() {
1001
2221
  try {
1002
2222
  const { stdout: isRepo } = await execAsync('git rev-parse --is-inside-work-tree 2>/dev/null || echo "false"');
@@ -1058,7 +2278,7 @@ async function collectMigrationInfo(dbUrl) {
1058
2278
  await pool.end();
1059
2279
  }
1060
2280
  } catch (error) {
1061
- console.log(chalk6.dim("\u26A0\uFE0F Could not fetch migration info"));
2281
+ console.log(chalk13.dim("\u26A0\uFE0F Could not fetch migration info"));
1062
2282
  return void 0;
1063
2283
  }
1064
2284
  }
@@ -1066,9 +2286,9 @@ async function saveBackupMetadata(metadata, backupFilename) {
1066
2286
  const metadataPath = backupFilename.replace(/\.(sql|dump)$/, ".meta.json");
1067
2287
  try {
1068
2288
  await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
1069
- console.log(chalk6.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
2289
+ console.log(chalk13.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
1070
2290
  } catch (error) {
1071
- console.log(chalk6.dim("\u26A0\uFE0F Could not save metadata"));
2291
+ console.log(chalk13.dim("\u26A0\uFE0F Could not save metadata"));
1072
2292
  }
1073
2293
  }
1074
2294
  async function loadBackupMetadata(backupFilename) {
@@ -1080,14 +2300,52 @@ async function loadBackupMetadata(backupFilename) {
1080
2300
  return void 0;
1081
2301
  }
1082
2302
  }
2303
+
2304
+ // src/commands/db/utils/backup-files.ts
2305
+ async function ensureBackupInGitignore() {
2306
+ const gitignorePath = path2.join(process.cwd(), ".gitignore");
2307
+ try {
2308
+ let content = "";
2309
+ let exists = existsSync17(gitignorePath);
2310
+ if (exists) {
2311
+ content = await fs2.readFile(gitignorePath, "utf-8");
2312
+ }
2313
+ const lines = content.split("\n");
2314
+ const hasBackupsIgnore = lines.some(
2315
+ (line) => line.trim() === "backups/" || line.trim() === "/backups/" || line.trim() === "backups"
2316
+ );
2317
+ if (!hasBackupsIgnore) {
2318
+ const entry = exists && content && !content.endsWith("\n") ? "\n\n# Database backups\nbackups/\n" : "# Database backups\nbackups/\n";
2319
+ await fs2.appendFile(gitignorePath, entry);
2320
+ console.log(chalk14.dim("\u2713 Added backups/ to .gitignore"));
2321
+ }
2322
+ } catch (error) {
2323
+ console.log(chalk14.dim("\u26A0\uFE0F Could not update .gitignore"));
2324
+ }
2325
+ }
2326
+ async function ensureBackupDir() {
2327
+ const backupDir = path2.join(process.cwd(), "backups");
2328
+ try {
2329
+ await fs2.mkdir(backupDir, { recursive: true });
2330
+ const gitignorePath = path2.join(backupDir, ".gitignore");
2331
+ const gitignoreExists = existsSync17(gitignorePath);
2332
+ if (!gitignoreExists) {
2333
+ await fs2.writeFile(gitignorePath, "# Ignore all backup files\n*.sql\n*.dump\n*.meta.json\n");
2334
+ }
2335
+ await ensureBackupInGitignore();
2336
+ return backupDir;
2337
+ } catch (error) {
2338
+ throw new Error(`Failed to create backup directory: ${error instanceof Error ? error.message : "Unknown error"}`);
2339
+ }
2340
+ }
1083
2341
  async function listBackupFiles() {
1084
- const backupDir = path.join(process.cwd(), "backups");
2342
+ const backupDir = path2.join(process.cwd(), "backups");
1085
2343
  try {
1086
- const files = await fs.readdir(backupDir);
2344
+ const files = await fs2.readdir(backupDir);
1087
2345
  const backups = await Promise.all(
1088
2346
  files.filter((f) => f.endsWith(".sql") || f.endsWith(".dump")).map(async (f) => {
1089
- const filepath = path.join(backupDir, f);
1090
- const stats = await fs.stat(filepath);
2347
+ const filepath = path2.join(backupDir, f);
2348
+ const stats = await fs2.stat(filepath);
1091
2349
  const metadata = await loadBackupMetadata(filepath);
1092
2350
  return {
1093
2351
  name: f,
@@ -1107,93 +2365,136 @@ async function listBackupFiles() {
1107
2365
  throw error;
1108
2366
  }
1109
2367
  }
1110
- async function runDrizzleCommand(command) {
1111
- const { loadEnvironment } = await import("@spfn/core/env");
1112
- loadEnvironment({ debug: false });
1113
- const hasUserConfig = existsSync6("./drizzle.config.ts");
1114
- const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
1115
- try {
1116
- const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
1117
- if (!hasUserConfig) {
1118
- if (!process.env.DATABASE_URL) {
1119
- console.error(chalk6.red("\u274C DATABASE_URL not found in environment"));
1120
- console.log(chalk6.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
1121
- process.exit(1);
1122
- }
1123
- const { generateDrizzleConfigFile } = await import("@spfn/core/db");
1124
- const configContent = generateDrizzleConfigFile({
1125
- cwd: process.cwd(),
1126
- // Exclude package schemas to avoid .ts/.js mixing (packages use migrations instead)
1127
- disablePackageDiscovery: true
1128
- });
1129
- writeFileSync4(tempConfigPath, configContent);
1130
- console.log(chalk6.dim("Using auto-generated Drizzle config\n"));
1131
- }
1132
- const fullCommand = `drizzle-kit ${command} --config=${configPath}`;
1133
- const { stdout, stderr } = await execAsync(fullCommand);
1134
- if (stdout) {
1135
- console.log(stdout);
1136
- }
1137
- if (stderr) {
1138
- console.error(stderr);
1139
- }
1140
- } finally {
1141
- if (!hasUserConfig && existsSync6(tempConfigPath)) {
1142
- unlinkSync(tempConfigPath);
1143
- }
2368
+
2369
+ // src/commands/db/backup.ts
2370
+ import { env as env3 } from "@spfn/core/config";
2371
+ import { loadEnvFiles as loadEnvFiles3 } from "@spfn/core/server";
2372
+ async function dbBackup(options) {
2373
+ console.log(chalk15.blue("\u{1F4BE} Creating database backup...\n"));
2374
+ loadEnvFiles3();
2375
+ const dbUrl = env3.DATABASE_URL;
2376
+ if (!dbUrl) {
2377
+ console.error(chalk15.red("\u274C DATABASE_URL not found in environment"));
2378
+ console.log(chalk15.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2379
+ process.exit(1);
1144
2380
  }
1145
- }
1146
- async function runWithSpinner(spinnerText, command, successMessage, failMessage) {
1147
- const spinner = ora3(spinnerText).start();
1148
- try {
1149
- spinner.stop();
1150
- await runDrizzleCommand(command);
1151
- console.log(chalk6.green(`\u2705 ${successMessage}`));
1152
- } catch (error) {
1153
- spinner.fail(failMessage);
1154
- console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
2381
+ const dbInfo = parseDatabaseUrl(dbUrl);
2382
+ const backupDir = await ensureBackupDir();
2383
+ const timestamp = formatTimestamp();
2384
+ const format = options.format || "sql";
2385
+ const ext = format === "sql" ? "sql" : "dump";
2386
+ const filename = options.output || path3.join(backupDir, `${dbInfo.database}_${timestamp}.${ext}`);
2387
+ if (options.dataOnly && options.schemaOnly) {
2388
+ console.error(chalk15.red("\u274C Cannot use --data-only and --schema-only together"));
1155
2389
  process.exit(1);
1156
2390
  }
1157
- }
1158
- async function dbGenerate() {
1159
- await runWithSpinner(
1160
- "Generating database migrations...",
1161
- "generate",
1162
- "Migrations generated successfully",
1163
- "Failed to generate migrations"
1164
- );
1165
- }
1166
- async function dbPush() {
1167
- const { loadEnvironment } = await import("@spfn/core/env");
1168
- loadEnvironment({ debug: false });
1169
- await runWithSpinner(
1170
- "Pushing schema changes to database...",
1171
- "push",
1172
- "Schema pushed successfully",
1173
- "Failed to push schema"
1174
- );
1175
- const { discoverFunctionMigrations, executeFunctionMigrations } = await import("./function-migrations-AXX6HWXL.js");
1176
- const functions = discoverFunctionMigrations(process.cwd());
1177
- if (functions.length > 0) {
1178
- console.log(chalk6.blue("\n\u{1F4E6} Applying function package migrations:"));
1179
- functions.forEach((func) => {
1180
- console.log(chalk6.dim(` - ${func.packageName}`));
2391
+ const args = [
2392
+ "-h",
2393
+ dbInfo.host,
2394
+ "-p",
2395
+ dbInfo.port,
2396
+ "-U",
2397
+ dbInfo.user,
2398
+ "-d",
2399
+ dbInfo.database,
2400
+ "-f",
2401
+ filename
2402
+ ];
2403
+ if (format === "custom") {
2404
+ args.push("-Fc");
2405
+ }
2406
+ if (options.schema) {
2407
+ args.push("-n", options.schema);
2408
+ }
2409
+ if (options.dataOnly) {
2410
+ args.push("--data-only");
2411
+ }
2412
+ if (options.schemaOnly) {
2413
+ args.push("--schema-only");
2414
+ }
2415
+ const spinner = ora7("Creating backup...").start();
2416
+ const pgDump = spawn2("pg_dump", args, {
2417
+ stdio: ["ignore", "pipe", "pipe"],
2418
+ env: {
2419
+ ...process.env,
2420
+ PGPASSWORD: dbInfo.password
2421
+ }
2422
+ });
2423
+ let errorOutput = "";
2424
+ pgDump.stderr?.on("data", (data) => {
2425
+ errorOutput += data.toString();
2426
+ });
2427
+ await new Promise((resolve2, reject) => {
2428
+ pgDump.on("close", async (code) => {
2429
+ if (code === 0) {
2430
+ try {
2431
+ const stats = await fs3.stat(filename);
2432
+ const size = formatBytes(stats.size);
2433
+ spinner.succeed("Backup created");
2434
+ console.log(chalk15.green(`
2435
+ \u2705 Backup created successfully`));
2436
+ console.log(chalk15.gray(` File: ${filename}`));
2437
+ console.log(chalk15.gray(` Size: ${size}`));
2438
+ console.log(chalk15.dim("\n\u{1F4CB} Collecting metadata..."));
2439
+ const [gitInfo, migrationInfo] = await Promise.all([
2440
+ collectGitInfo(),
2441
+ collectMigrationInfo(dbUrl)
2442
+ ]);
2443
+ const tags = [];
2444
+ if (options.tag) {
2445
+ tags.push(...options.tag.split(",").map((t) => t.trim()));
2446
+ }
2447
+ const metadata = {
2448
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2449
+ database: dbInfo.database,
2450
+ environment: options.env || process.env.NODE_ENV,
2451
+ git: gitInfo,
2452
+ migrations: migrationInfo,
2453
+ backup: {
2454
+ filename: path3.basename(filename),
2455
+ format,
2456
+ sizeBytes: stats.size,
2457
+ schema: options.schema,
2458
+ dataOnly: options.dataOnly,
2459
+ schemaOnly: options.schemaOnly
2460
+ },
2461
+ tags: tags.length > 0 ? tags : void 0
2462
+ };
2463
+ await saveBackupMetadata(metadata, filename);
2464
+ resolve2();
2465
+ } catch (error) {
2466
+ reject(error);
2467
+ }
2468
+ } else {
2469
+ spinner.fail("Backup failed");
2470
+ reject(new Error(errorOutput || "pg_dump failed"));
2471
+ }
1181
2472
  });
1182
- try {
1183
- await executeFunctionMigrations(functions);
1184
- console.log(chalk6.green("\n\u2705 All function migrations applied\n"));
1185
- } catch (error) {
1186
- console.error(chalk6.red("\n\u274C Failed to apply function migrations"));
1187
- console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1188
- process.exit(1);
2473
+ pgDump.on("error", (error) => {
2474
+ spinner.fail("Backup failed");
2475
+ reject(error);
2476
+ });
2477
+ }).catch((error) => {
2478
+ console.error(chalk15.red("\n\u274C Failed to create backup"));
2479
+ if (errorOutput.includes("pg_dump: command not found") || errorOutput.includes("not found")) {
2480
+ console.error(chalk15.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
2481
+ } else {
2482
+ console.error(chalk15.red(error instanceof Error ? error.message : "Unknown error"));
1189
2483
  }
1190
- }
2484
+ process.exit(1);
2485
+ });
1191
2486
  }
2487
+
2488
+ // src/commands/db/migrate.ts
2489
+ import "@spfn/core/config";
1192
2490
  async function dbMigrate(options = {}) {
1193
- const { loadEnvironment } = await import("@spfn/core/env");
1194
- loadEnvironment({ debug: false });
2491
+ try {
2492
+ validateDatabasePrerequisites();
2493
+ } catch (error) {
2494
+ process.exit(1);
2495
+ }
1195
2496
  if (options.withBackup) {
1196
- console.log(chalk6.blue("\u{1F4E6} Creating pre-migration backup...\n"));
2497
+ console.log(chalk16.blue("\u{1F4E6} Creating pre-migration backup...\n"));
1197
2498
  await dbBackup({
1198
2499
  format: "custom",
1199
2500
  tag: "pre-migration",
@@ -1201,19 +2502,19 @@ async function dbMigrate(options = {}) {
1201
2502
  });
1202
2503
  console.log("");
1203
2504
  }
1204
- const { discoverFunctionMigrations, executeFunctionMigrations } = await import("./function-migrations-AXX6HWXL.js");
1205
- const functions = discoverFunctionMigrations(process.cwd());
2505
+ const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
2506
+ const functions = discoverFunctionMigrations2(process.cwd());
1206
2507
  if (functions.length > 0) {
1207
- console.log(chalk6.blue("\u{1F4E6} Applying function package migrations:"));
2508
+ console.log(chalk16.blue("\u{1F4E6} Applying function package migrations:"));
1208
2509
  functions.forEach((func) => {
1209
- console.log(chalk6.dim(` - ${func.packageName}`));
2510
+ console.log(chalk16.dim(` - ${func.packageName}`));
1210
2511
  });
1211
2512
  try {
1212
- await executeFunctionMigrations(functions);
1213
- console.log(chalk6.green("\u2705 Function migrations applied\n"));
2513
+ await executeFunctionMigrations2(functions);
2514
+ console.log(chalk16.green("\u2705 Function migrations applied\n"));
1214
2515
  } catch (error) {
1215
- console.error(chalk6.red("\n\u274C Failed to apply function migrations"));
1216
- console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
2516
+ console.error(chalk16.red("\n\u274C Failed to apply function migrations"));
2517
+ console.error(chalk16.red(error instanceof Error ? error.message : "Unknown error"));
1217
2518
  process.exit(1);
1218
2519
  }
1219
2520
  }
@@ -1224,66 +2525,73 @@ async function dbMigrate(options = {}) {
1224
2525
  "Failed to run project migrations"
1225
2526
  );
1226
2527
  }
2528
+
2529
+ // src/commands/db/studio.ts
2530
+ import chalk17 from "chalk";
2531
+ import { existsSync as existsSync18, writeFileSync as writeFileSync10, unlinkSync as unlinkSync2 } from "fs";
2532
+ import { spawn as spawn3 } from "child_process";
2533
+ import { env as env4 } from "@spfn/core/config";
2534
+ import "@spfn/core/config";
1227
2535
  async function dbStudio(requestedPort) {
1228
- console.log(chalk6.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
1229
- const { loadEnvironment } = await import("@spfn/core/env");
1230
- loadEnvironment({ debug: false });
2536
+ console.log(chalk17.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
1231
2537
  const defaultPort = 4983;
1232
2538
  const startPort = requestedPort || defaultPort;
1233
2539
  let port;
1234
2540
  try {
1235
2541
  port = await findAvailablePort(startPort);
1236
2542
  if (port !== startPort) {
1237
- console.log(chalk6.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
2543
+ console.log(chalk17.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
1238
2544
  `));
1239
2545
  }
1240
2546
  } catch (error) {
1241
- console.error(chalk6.red(error instanceof Error ? error.message : "Failed to find available port"));
2547
+ console.error(chalk17.red(error instanceof Error ? error.message : "Failed to find available port"));
1242
2548
  process.exit(1);
1243
2549
  }
1244
- const hasUserConfig = existsSync6("./drizzle.config.ts");
2550
+ const hasUserConfig = existsSync18("./drizzle.config.ts");
1245
2551
  const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
1246
2552
  try {
1247
2553
  const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
1248
2554
  if (!hasUserConfig) {
1249
- if (!process.env.DATABASE_URL) {
1250
- console.error(chalk6.red("\u274C DATABASE_URL not found in environment"));
1251
- console.log(chalk6.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2555
+ if (!env4.DATABASE_URL) {
2556
+ console.error(chalk17.red("\u274C DATABASE_URL not found in environment"));
2557
+ console.log(chalk17.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
1252
2558
  process.exit(1);
1253
2559
  }
1254
2560
  const { generateDrizzleConfigFile } = await import("@spfn/core/db");
1255
2561
  const configContent = generateDrizzleConfigFile({
1256
2562
  cwd: process.cwd(),
1257
- disablePackageDiscovery: true
2563
+ disablePackageDiscovery: true,
2564
+ expandGlobs: true
2565
+ // Expand glob patterns for Studio compatibility
1258
2566
  });
1259
- writeFileSync4(tempConfigPath, configContent);
1260
- console.log(chalk6.dim("Using auto-generated Drizzle config\n"));
2567
+ writeFileSync10(tempConfigPath, configContent);
2568
+ console.log(chalk17.dim("Using auto-generated Drizzle config\n"));
1261
2569
  }
1262
- const studioProcess = spawn("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
2570
+ const studioProcess = spawn3("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
1263
2571
  stdio: "inherit",
1264
2572
  shell: true
1265
2573
  });
1266
2574
  const cleanup = () => {
1267
- if (!hasUserConfig && existsSync6(tempConfigPath)) {
1268
- unlinkSync(tempConfigPath);
2575
+ if (!hasUserConfig && existsSync18(tempConfigPath)) {
2576
+ unlinkSync2(tempConfigPath);
1269
2577
  }
1270
2578
  };
1271
2579
  studioProcess.on("exit", (code) => {
1272
2580
  cleanup();
1273
2581
  if (code !== 0 && code !== null) {
1274
- console.error(chalk6.red(`
2582
+ console.error(chalk17.red(`
1275
2583
  \u274C Drizzle Studio exited with code ${code}`));
1276
2584
  process.exit(code);
1277
2585
  }
1278
2586
  });
1279
2587
  studioProcess.on("error", (error) => {
1280
2588
  cleanup();
1281
- console.error(chalk6.red("\u274C Failed to start Drizzle Studio"));
1282
- console.error(chalk6.red(error.message));
2589
+ console.error(chalk17.red("\u274C Failed to start Drizzle Studio"));
2590
+ console.error(chalk17.red(error.message));
1283
2591
  process.exit(1);
1284
2592
  });
1285
2593
  process.on("SIGINT", () => {
1286
- console.log(chalk6.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
2594
+ console.log(chalk17.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
1287
2595
  studioProcess.kill("SIGTERM");
1288
2596
  cleanup();
1289
2597
  process.exit(0);
@@ -1294,24 +2602,28 @@ async function dbStudio(requestedPort) {
1294
2602
  process.exit(0);
1295
2603
  });
1296
2604
  } catch (error) {
1297
- if (!hasUserConfig && existsSync6(tempConfigPath)) {
1298
- unlinkSync(tempConfigPath);
2605
+ if (!hasUserConfig && existsSync18(tempConfigPath)) {
2606
+ unlinkSync2(tempConfigPath);
1299
2607
  }
1300
- console.error(chalk6.red("\u274C Failed to start Drizzle Studio"));
1301
- console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
2608
+ console.error(chalk17.red("\u274C Failed to start Drizzle Studio"));
2609
+ console.error(chalk17.red(error instanceof Error ? error.message : "Unknown error"));
1302
2610
  process.exit(1);
1303
2611
  }
1304
2612
  }
2613
+
2614
+ // src/commands/db/drop.ts
2615
+ import chalk18 from "chalk";
2616
+ import prompts3 from "prompts";
1305
2617
  async function dbDrop() {
1306
- console.log(chalk6.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
1307
- const { confirm } = await prompts2({
2618
+ console.log(chalk18.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
2619
+ const { confirm } = await prompts3({
1308
2620
  type: "confirm",
1309
2621
  name: "confirm",
1310
2622
  message: "Are you sure you want to drop all tables?",
1311
2623
  initial: false
1312
2624
  });
1313
2625
  if (!confirm) {
1314
- console.log(chalk6.gray("Cancelled."));
2626
+ console.log(chalk18.gray("Cancelled."));
1315
2627
  process.exit(0);
1316
2628
  }
1317
2629
  await runWithSpinner(
@@ -1321,144 +2633,47 @@ async function dbDrop() {
1321
2633
  "Failed to drop tables"
1322
2634
  );
1323
2635
  }
2636
+
2637
+ // src/commands/db/check.ts
2638
+ import chalk19 from "chalk";
2639
+ import ora8 from "ora";
1324
2640
  async function dbCheck() {
1325
- const spinner = ora3("Checking database connection...").start();
2641
+ const spinner = ora8("Checking database connection...").start();
1326
2642
  try {
1327
2643
  await runDrizzleCommand("check");
1328
2644
  spinner.succeed("Database connection OK");
1329
2645
  } catch (error) {
1330
2646
  spinner.fail("Database connection failed");
1331
- console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1332
- process.exit(1);
1333
- }
1334
- }
1335
- async function dbBackup(options) {
1336
- console.log(chalk6.blue("\u{1F4BE} Creating database backup...\n"));
1337
- const { loadEnvironment } = await import("@spfn/core/env");
1338
- loadEnvironment({ debug: false });
1339
- const dbUrl = process.env.DATABASE_URL;
1340
- if (!dbUrl) {
1341
- console.error(chalk6.red("\u274C DATABASE_URL not found in environment"));
1342
- console.log(chalk6.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
1343
- process.exit(1);
1344
- }
1345
- const dbInfo = parseDatabaseUrl(dbUrl);
1346
- const backupDir = await ensureBackupDir();
1347
- const timestamp = formatTimestamp();
1348
- const format = options.format || "sql";
1349
- const ext = format === "sql" ? "sql" : "dump";
1350
- const filename = options.output || path.join(backupDir, `${dbInfo.database}_${timestamp}.${ext}`);
1351
- if (options.dataOnly && options.schemaOnly) {
1352
- console.error(chalk6.red("\u274C Cannot use --data-only and --schema-only together"));
2647
+ console.error(chalk19.red(error instanceof Error ? error.message : "Unknown error"));
1353
2648
  process.exit(1);
1354
2649
  }
1355
- const args = [
1356
- "-h",
1357
- dbInfo.host,
1358
- "-p",
1359
- dbInfo.port,
1360
- "-U",
1361
- dbInfo.user,
1362
- "-d",
1363
- dbInfo.database,
1364
- "-f",
1365
- filename
1366
- ];
1367
- if (format === "custom") {
1368
- args.push("-Fc");
1369
- }
1370
- if (options.schema) {
1371
- args.push("-n", options.schema);
1372
- }
1373
- if (options.dataOnly) {
1374
- args.push("--data-only");
1375
- }
1376
- if (options.schemaOnly) {
1377
- args.push("--schema-only");
1378
- }
1379
- const spinner = ora3("Creating backup...").start();
1380
- const pgDump = spawn("pg_dump", args, {
1381
- stdio: ["ignore", "pipe", "pipe"],
1382
- env: {
1383
- ...process.env,
1384
- PGPASSWORD: dbInfo.password
1385
- }
1386
- });
1387
- let errorOutput = "";
1388
- pgDump.stderr?.on("data", (data) => {
1389
- errorOutput += data.toString();
1390
- });
1391
- pgDump.on("close", async (code) => {
1392
- if (code === 0) {
1393
- const stats = await fs.stat(filename);
1394
- const size = formatBytes(stats.size);
1395
- spinner.succeed("Backup created");
1396
- console.log(chalk6.green(`
1397
- \u2705 Backup created successfully`));
1398
- console.log(chalk6.gray(` File: ${filename}`));
1399
- console.log(chalk6.gray(` Size: ${size}`));
1400
- console.log(chalk6.dim("\n\u{1F4CB} Collecting metadata..."));
1401
- const [gitInfo, migrationInfo] = await Promise.all([
1402
- collectGitInfo(),
1403
- collectMigrationInfo(dbUrl)
1404
- ]);
1405
- const tags = [];
1406
- if (options.tag) {
1407
- tags.push(...options.tag.split(",").map((t) => t.trim()));
1408
- }
1409
- const metadata = {
1410
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1411
- database: dbInfo.database,
1412
- environment: options.env || process.env.NODE_ENV,
1413
- git: gitInfo,
1414
- migrations: migrationInfo,
1415
- backup: {
1416
- filename: path.basename(filename),
1417
- format,
1418
- sizeBytes: stats.size,
1419
- schema: options.schema,
1420
- dataOnly: options.dataOnly,
1421
- schemaOnly: options.schemaOnly
1422
- },
1423
- tags: tags.length > 0 ? tags : void 0
1424
- };
1425
- await saveBackupMetadata(metadata, filename);
1426
- } else {
1427
- spinner.fail("Backup failed");
1428
- console.error(chalk6.red("\n\u274C Failed to create backup"));
1429
- if (errorOutput.includes("pg_dump: command not found") || errorOutput.includes("not found")) {
1430
- console.error(chalk6.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
1431
- } else if (errorOutput) {
1432
- console.error(chalk6.red(errorOutput));
1433
- }
1434
- process.exit(1);
1435
- }
1436
- });
1437
- pgDump.on("error", (error) => {
1438
- spinner.fail("Backup failed");
1439
- console.error(chalk6.red("\n\u274C Failed to start pg_dump"));
1440
- console.error(chalk6.red(error.message));
1441
- process.exit(1);
1442
- });
1443
2650
  }
2651
+
2652
+ // src/commands/db/restore.ts
2653
+ import path4 from "path";
2654
+ import { spawn as spawn4 } from "child_process";
2655
+ import chalk20 from "chalk";
2656
+ import ora9 from "ora";
2657
+ import prompts4 from "prompts";
2658
+ import { env as env5 } from "@spfn/core/config";
2659
+ import { loadEnvFiles as loadEnvFiles4 } from "@spfn/core/server";
1444
2660
  async function dbRestore(backupFile, options = {}) {
1445
- console.log(chalk6.blue("\u267B\uFE0F Restoring database from backup...\n"));
1446
- const { loadEnvironment } = await import("@spfn/core/env");
1447
- loadEnvironment({ debug: false });
1448
- const dbUrl = process.env.DATABASE_URL;
2661
+ console.log(chalk20.blue("\u267B\uFE0F Restoring database from backup...\n"));
2662
+ loadEnvFiles4();
2663
+ const dbUrl = env5.DATABASE_URL;
1449
2664
  if (!dbUrl) {
1450
- console.error(chalk6.red("\u274C DATABASE_URL not found in environment"));
1451
- console.log(chalk6.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2665
+ console.error(chalk20.red("\u274C DATABASE_URL not found in environment"));
2666
+ console.log(chalk20.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
1452
2667
  process.exit(1);
1453
2668
  }
1454
2669
  let file = backupFile;
1455
2670
  if (!file) {
1456
2671
  const backups = await listBackupFiles();
1457
2672
  if (backups.length === 0) {
1458
- console.log(chalk6.yellow("No backups found in ./backups directory"));
2673
+ console.log(chalk20.yellow("No backups found in ./backups directory"));
1459
2674
  process.exit(0);
1460
2675
  }
1461
- const { selected } = await prompts2({
2676
+ const { selected } = await prompts4({
1462
2677
  type: "select",
1463
2678
  name: "selected",
1464
2679
  message: "Select backup to restore:",
@@ -1468,74 +2683,74 @@ async function dbRestore(backupFile, options = {}) {
1468
2683
  }))
1469
2684
  });
1470
2685
  if (!selected) {
1471
- console.log(chalk6.gray("Cancelled"));
2686
+ console.log(chalk20.gray("Cancelled"));
1472
2687
  process.exit(0);
1473
2688
  }
1474
2689
  file = selected;
1475
2690
  }
1476
2691
  if (!file) {
1477
- console.error(chalk6.red("\u274C No backup file selected"));
2692
+ console.error(chalk20.red("\u274C No backup file selected"));
1478
2693
  process.exit(1);
1479
2694
  }
1480
2695
  const metadata = await loadBackupMetadata(file);
1481
2696
  if (metadata) {
1482
- console.log(chalk6.blue("\n\u{1F4CB} Backup Information:\n"));
1483
- console.log(chalk6.dim(` Database: ${metadata.database}`));
1484
- console.log(chalk6.dim(` Created: ${new Date(metadata.timestamp).toLocaleString()}`));
2697
+ console.log(chalk20.blue("\n\u{1F4CB} Backup Information:\n"));
2698
+ console.log(chalk20.dim(` Database: ${metadata.database}`));
2699
+ console.log(chalk20.dim(` Created: ${new Date(metadata.timestamp).toLocaleString()}`));
1485
2700
  if (metadata.environment) {
1486
- console.log(chalk6.dim(` Environment: ${metadata.environment}`));
2701
+ console.log(chalk20.dim(` Environment: ${metadata.environment}`));
1487
2702
  }
1488
2703
  if (metadata.tags && metadata.tags.length > 0) {
1489
- console.log(chalk6.dim(` Tags: ${metadata.tags.join(", ")}`));
2704
+ console.log(chalk20.dim(` Tags: ${metadata.tags.join(", ")}`));
1490
2705
  }
1491
2706
  if (metadata.backup.dataOnly) {
1492
- console.log(chalk6.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
2707
+ console.log(chalk20.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
1493
2708
  }
1494
2709
  if (metadata.backup.schemaOnly) {
1495
- console.log(chalk6.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
2710
+ console.log(chalk20.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
1496
2711
  }
1497
- const warnings = [];
2712
+ const warnings2 = [];
1498
2713
  const [currentGitInfo, currentMigrationInfo] = await Promise.all([
1499
2714
  collectGitInfo(),
1500
2715
  collectMigrationInfo(dbUrl)
1501
2716
  ]);
1502
2717
  if (metadata.git && currentGitInfo) {
1503
2718
  if (metadata.git.commit !== currentGitInfo.commit) {
1504
- warnings.push(`Git commit mismatch: backup from ${metadata.git.commit.substring(0, 7)}, current is ${currentGitInfo.commit.substring(0, 7)}`);
2719
+ warnings2.push(`Git commit mismatch: backup from ${metadata.git.commit.substring(0, 7)}, current is ${currentGitInfo.commit.substring(0, 7)}`);
1505
2720
  }
1506
2721
  if (metadata.git.branch !== currentGitInfo.branch) {
1507
- warnings.push(`Git branch mismatch: backup from '${metadata.git.branch}', current is '${currentGitInfo.branch}'`);
2722
+ warnings2.push(`Git branch mismatch: backup from '${metadata.git.branch}', current is '${currentGitInfo.branch}'`);
1508
2723
  }
1509
2724
  }
1510
2725
  if (metadata.migrations && currentMigrationInfo) {
1511
2726
  if (metadata.migrations.hash !== currentMigrationInfo.hash) {
1512
- warnings.push(`Migration version mismatch: backup has ${metadata.migrations.count} migrations, current has ${currentMigrationInfo.count}`);
1513
- warnings.push(` Last migration in backup: ${metadata.migrations.hash}`);
1514
- warnings.push(` Current last migration: ${currentMigrationInfo.hash}`);
2727
+ warnings2.push(`Migration version mismatch: backup has ${metadata.migrations.count} migrations, current has ${currentMigrationInfo.count}`);
2728
+ warnings2.push(` Last migration in backup: ${metadata.migrations.hash}`);
2729
+ warnings2.push(` Current last migration: ${currentMigrationInfo.hash}`);
1515
2730
  }
1516
2731
  }
1517
- if (warnings.length > 0) {
1518
- console.log(chalk6.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
1519
- warnings.forEach((warning) => console.log(chalk6.yellow(` - ${warning}`)));
2732
+ if (warnings2.length > 0) {
2733
+ console.log(chalk20.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
2734
+ warnings2.forEach((warning) => console.log(chalk20.yellow(` - ${warning}`)));
1520
2735
  console.log("");
1521
2736
  }
1522
2737
  }
1523
- const { confirm } = await prompts2({
2738
+ const { confirm } = await prompts4({
1524
2739
  type: "confirm",
1525
2740
  name: "confirm",
1526
- message: chalk6.yellow("\u26A0\uFE0F This will replace all data in the database. Continue?"),
2741
+ message: chalk20.yellow("\u26A0\uFE0F This will replace all data in the database. Continue?"),
1527
2742
  initial: false
1528
2743
  });
1529
2744
  if (!confirm) {
1530
- console.log(chalk6.gray("Cancelled"));
2745
+ console.log(chalk20.gray("Cancelled"));
1531
2746
  process.exit(0);
1532
2747
  }
1533
2748
  if (options.dataOnly && options.schemaOnly) {
1534
- console.error(chalk6.red("\u274C Cannot use --data-only and --schema-only together"));
2749
+ console.error(chalk20.red("\u274C Cannot use --data-only and --schema-only together"));
1535
2750
  process.exit(1);
1536
2751
  }
1537
2752
  const dbInfo = parseDatabaseUrl(dbUrl);
1538
- const ext = path.extname(file);
2753
+ const ext = path4.extname(file);
1539
2754
  const isCustomFormat = ext === ".dump";
1540
2755
  const command = isCustomFormat ? "pg_restore" : "psql";
1541
2756
  const args = [];
@@ -1544,6 +2759,7 @@ async function dbRestore(backupFile, options = {}) {
1544
2759
  args.push("-p", dbInfo.port);
1545
2760
  args.push("-U", dbInfo.user);
1546
2761
  args.push("-d", dbInfo.database);
2762
+ args.push("--verbose");
1547
2763
  if (options.drop) {
1548
2764
  args.push("--clean");
1549
2765
  }
@@ -1559,58 +2775,128 @@ async function dbRestore(backupFile, options = {}) {
1559
2775
  args.push(file);
1560
2776
  } else {
1561
2777
  if (options.dataOnly || options.schemaOnly) {
1562
- console.log(chalk6.yellow("\u26A0\uFE0F Note: --data-only and --schema-only options only work with custom format backups (.dump)"));
1563
- console.log(chalk6.yellow(" For SQL files, the backup must have been created with the desired option.\n"));
2778
+ console.log(chalk20.yellow("\u26A0\uFE0F Note: --data-only and --schema-only options only work with custom format backups (.dump)"));
2779
+ console.log(chalk20.yellow(" For SQL files, the backup must have been created with the desired option.\n"));
1564
2780
  }
1565
2781
  args.push("-h", dbInfo.host);
1566
2782
  args.push("-p", dbInfo.port);
1567
2783
  args.push("-U", dbInfo.user);
1568
2784
  args.push("-d", dbInfo.database);
2785
+ args.push("-v", "ON_ERROR_STOP=1");
1569
2786
  args.push("-f", file);
1570
2787
  }
1571
- const spinner = ora3("Restoring backup...").start();
1572
- const restoreProcess = spawn(command, args, {
2788
+ const verbose = options.verbose ?? false;
2789
+ const spinner = ora9("Restoring backup...").start();
2790
+ const restoreProcess = spawn4(command, args, {
1573
2791
  stdio: ["ignore", "pipe", "pipe"],
1574
2792
  env: {
1575
2793
  ...process.env,
1576
2794
  PGPASSWORD: dbInfo.password
1577
2795
  }
1578
2796
  });
1579
- let errorOutput = "";
2797
+ const warnings = [];
2798
+ const errors = [];
2799
+ let objectCount = 0;
2800
+ let lastObject = "";
1580
2801
  restoreProcess.stderr?.on("data", (data) => {
1581
- errorOutput += data.toString();
2802
+ const lines = data.toString().split("\n").filter((l) => l.trim());
2803
+ for (const line of lines) {
2804
+ if (/^pg_restore:.*warning:/i.test(line) || /^WARNING:/i.test(line)) {
2805
+ warnings.push(line.trim());
2806
+ } else if (/^pg_restore:.*error:/i.test(line) || /^ERROR:/i.test(line) || /^psql:.*ERROR/i.test(line)) {
2807
+ errors.push(line.trim());
2808
+ }
2809
+ const objectMatch = line.match(/processing item (\d+)\/(\d+)/);
2810
+ if (objectMatch) {
2811
+ objectCount = Number(objectMatch[2]);
2812
+ const current = Number(objectMatch[1]);
2813
+ const desc = line.replace(/^pg_restore:\s*/, "").trim();
2814
+ lastObject = desc;
2815
+ spinner.text = `Restoring backup... [${current}/${objectCount}] ${desc}`;
2816
+ } else if (isCustomFormat) {
2817
+ const desc = line.replace(/^pg_restore:\s*/, "").trim();
2818
+ if (desc && !/warning:|error:/i.test(desc)) {
2819
+ lastObject = desc;
2820
+ spinner.text = `Restoring backup... ${desc}`;
2821
+ }
2822
+ }
2823
+ if (verbose) {
2824
+ spinner.stop();
2825
+ console.log(chalk20.dim(` ${line.trim()}`));
2826
+ spinner.start();
2827
+ }
2828
+ }
1582
2829
  });
1583
- restoreProcess.on("close", (code) => {
1584
- if (code === 0) {
1585
- spinner.succeed("Restore completed");
1586
- console.log(chalk6.green("\n\u2705 Database restored successfully"));
1587
- } else {
1588
- spinner.fail("Restore failed");
1589
- console.error(chalk6.red("\n\u274C Failed to restore database"));
1590
- if (errorOutput) {
1591
- console.error(chalk6.red(errorOutput));
2830
+ restoreProcess.stdout?.on("data", (data) => {
2831
+ if (verbose) {
2832
+ spinner.stop();
2833
+ console.log(chalk20.dim(` ${data.toString().trim()}`));
2834
+ spinner.start();
2835
+ }
2836
+ });
2837
+ await new Promise((resolve2, reject) => {
2838
+ restoreProcess.on("close", (code) => {
2839
+ if (code === 0) {
2840
+ const summary = objectCount > 0 ? ` (${objectCount} objects)` : "";
2841
+ spinner.succeed(`Restore completed${summary}`);
2842
+ if (warnings.length > 0) {
2843
+ console.log(chalk20.yellow(`
2844
+ \u26A0\uFE0F Warnings during restore (${warnings.length}):
2845
+ `));
2846
+ for (const w of warnings) {
2847
+ console.log(chalk20.yellow(` - ${w}`));
2848
+ }
2849
+ }
2850
+ console.log(chalk20.green("\n\u2705 Database restored successfully"));
2851
+ resolve2();
2852
+ } else {
2853
+ spinner.fail("Restore failed");
2854
+ if (errors.length > 0) {
2855
+ console.error(chalk20.red(`
2856
+ \u274C Errors (${errors.length}):
2857
+ `));
2858
+ for (const e of errors) {
2859
+ console.error(chalk20.red(` - ${e}`));
2860
+ }
2861
+ }
2862
+ if (warnings.length > 0) {
2863
+ console.log(chalk20.yellow(`
2864
+ \u26A0\uFE0F Warnings (${warnings.length}):
2865
+ `));
2866
+ for (const w of warnings) {
2867
+ console.log(chalk20.yellow(` - ${w}`));
2868
+ }
2869
+ }
2870
+ const fallback = errors.length === 0 && warnings.length === 0 ? "Restore failed with no output" : "";
2871
+ reject(new Error(fallback));
1592
2872
  }
1593
- process.exit(1);
2873
+ });
2874
+ restoreProcess.on("error", (error) => {
2875
+ spinner.fail("Restore failed");
2876
+ reject(error);
2877
+ });
2878
+ }).catch((error) => {
2879
+ const msg = error instanceof Error ? error.message : "Unknown error";
2880
+ if (msg) {
2881
+ console.error(chalk20.red(`
2882
+ \u274C ${msg}`));
1594
2883
  }
1595
- });
1596
- restoreProcess.on("error", (error) => {
1597
- spinner.fail("Restore failed");
1598
- console.error(chalk6.red(`
1599
- \u274C Failed to start ${command}`));
1600
- console.error(chalk6.red(error.message));
1601
2884
  process.exit(1);
1602
2885
  });
1603
2886
  }
2887
+
2888
+ // src/commands/db/list.ts
2889
+ import chalk21 from "chalk";
1604
2890
  async function dbBackupList() {
1605
- console.log(chalk6.blue("\u{1F4CB} Database backups:\n"));
2891
+ console.log(chalk21.blue("\u{1F4CB} Database backups:\n"));
1606
2892
  const backups = await listBackupFiles();
1607
2893
  if (backups.length === 0) {
1608
- console.log(chalk6.yellow("No backups found in ./backups directory"));
1609
- console.log(chalk6.gray("\n\u{1F4A1} Create a backup with: pnpm spfn db backup\n"));
2894
+ console.log(chalk21.yellow("No backups found in ./backups directory"));
2895
+ console.log(chalk21.gray("\n\u{1F4A1} Create a backup with: pnpm spfn db backup\n"));
1610
2896
  return;
1611
2897
  }
1612
- console.log(chalk6.bold(" Date Size File"));
1613
- console.log(chalk6.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2898
+ console.log(chalk21.bold(" Date Size File"));
2899
+ console.log(chalk21.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1614
2900
  backups.forEach((backup) => {
1615
2901
  const date = backup.date.toLocaleString("en-US", {
1616
2902
  year: "numeric",
@@ -1621,17 +2907,23 @@ async function dbBackupList() {
1621
2907
  second: "2-digit"
1622
2908
  });
1623
2909
  const sizeStr = backup.size.padEnd(10);
1624
- console.log(chalk6.white(` ${date} ${sizeStr} ${backup.name}`));
2910
+ console.log(chalk21.white(` ${date} ${sizeStr} ${backup.name}`));
1625
2911
  });
1626
- console.log(chalk6.gray(`
2912
+ console.log(chalk21.gray(`
1627
2913
  Total: ${backups.length} backup(s)
1628
2914
  `));
1629
2915
  }
2916
+
2917
+ // src/commands/db/clean.ts
2918
+ import { promises as fs4 } from "fs";
2919
+ import chalk22 from "chalk";
2920
+ import ora10 from "ora";
2921
+ import prompts5 from "prompts";
1630
2922
  async function dbBackupClean(options) {
1631
- console.log(chalk6.blue("\u{1F9F9} Cleaning old backups...\n"));
2923
+ console.log(chalk22.blue("\u{1F9F9} Cleaning old backups...\n"));
1632
2924
  const backups = await listBackupFiles();
1633
2925
  if (backups.length === 0) {
1634
- console.log(chalk6.yellow("No backups found"));
2926
+ console.log(chalk22.yellow("No backups found"));
1635
2927
  return;
1636
2928
  }
1637
2929
  let toDelete = [];
@@ -1648,277 +2940,39 @@ async function dbBackupClean(options) {
1648
2940
  toDelete = backups.slice(defaultKeep);
1649
2941
  }
1650
2942
  if (toDelete.length === 0) {
1651
- console.log(chalk6.green("\u2705 No backups to clean"));
2943
+ console.log(chalk22.green("\u2705 No backups to clean"));
1652
2944
  return;
1653
2945
  }
1654
- console.log(chalk6.yellow(`The following ${toDelete.length} backup(s) will be deleted:
2946
+ console.log(chalk22.yellow(`The following ${toDelete.length} backup(s) will be deleted:
1655
2947
  `));
1656
2948
  toDelete.forEach((backup) => {
1657
- console.log(chalk6.gray(` - ${backup.name} (${backup.size})`));
2949
+ console.log(chalk22.gray(` - ${backup.name} (${backup.size})`));
1658
2950
  });
1659
- const { confirm } = await prompts2({
2951
+ const { confirm } = await prompts5({
1660
2952
  type: "confirm",
1661
2953
  name: "confirm",
1662
2954
  message: "\nProceed with deletion?",
1663
2955
  initial: false
1664
2956
  });
1665
2957
  if (!confirm) {
1666
- console.log(chalk6.gray("Cancelled"));
2958
+ console.log(chalk22.gray("Cancelled"));
1667
2959
  return;
1668
2960
  }
1669
- const spinner = ora3("Deleting backups...").start();
2961
+ const spinner = ora10("Deleting backups...").start();
1670
2962
  try {
1671
- await Promise.all(toDelete.map((backup) => fs.unlink(backup.path)));
2963
+ await Promise.all(toDelete.map((backup) => fs4.unlink(backup.path)));
1672
2964
  spinner.succeed("Backups deleted");
1673
- console.log(chalk6.green(`
2965
+ console.log(chalk22.green(`
1674
2966
  \u2705 Deleted ${toDelete.length} backup(s)`));
1675
2967
  } catch (error) {
1676
2968
  spinner.fail("Failed to delete backups");
1677
- console.error(chalk6.red(error instanceof Error ? error.message : "Unknown error"));
1678
- process.exit(1);
1679
- }
1680
- }
1681
- async function dbSync(target, options) {
1682
- console.log(chalk6.blue("\u{1F504} Database sync\n"));
1683
- const { loadEnvironment } = await import("@spfn/core/env");
1684
- loadEnvironment({ debug: false });
1685
- const {
1686
- validateSyncEnvironments,
1687
- testDatabaseConnection,
1688
- getDatabaseInfo,
1689
- isProductionLike,
1690
- getAvailableSyncTargets
1691
- } = await import("./db-sync-NBLWUZMH.js");
1692
- const sourceName = options.pull ? target : "local";
1693
- const targetName = options.pull ? "local" : target;
1694
- let source, targetEnv;
1695
- try {
1696
- const envs = await validateSyncEnvironments(sourceName, targetName);
1697
- source = envs.source;
1698
- targetEnv = envs.target;
1699
- } catch (error) {
1700
- console.error(chalk6.red(`\u274C ${error instanceof Error ? error.message : "Environment validation failed"}`));
1701
- const available = getAvailableSyncTargets();
1702
- if (available.length > 0) {
1703
- console.log(chalk6.yellow(`
1704
- \u{1F4A1} Available sync targets: ${available.join(", ")}`));
1705
- console.log(chalk6.dim(` Configure in .env: SPFN_DB_${target.toUpperCase()}=postgresql://...`));
1706
- } else {
1707
- console.log(chalk6.yellow("\n\u{1F4A1} No sync targets configured"));
1708
- console.log(chalk6.dim(" Add to .env: SPFN_DB_DEV=postgresql://..."));
1709
- }
1710
- process.exit(1);
1711
- }
1712
- if (isProductionLike(targetEnv.name) && !options.force) {
1713
- console.error(chalk6.red(`\u274C Cannot sync to production-like environment '${targetEnv.name}' without --force flag`));
1714
- console.log(chalk6.yellow(" This is a safety measure to prevent accidental data loss"));
1715
- console.log(chalk6.dim(" Use --force if you really want to do this"));
1716
- process.exit(1);
1717
- }
1718
- console.log(chalk6.dim("Testing database connections..."));
1719
- const spinner = ora3("Connecting to source database...").start();
1720
- const sourceConnected = await testDatabaseConnection(source);
1721
- if (!sourceConnected) {
1722
- spinner.fail("Failed to connect to source database");
1723
- console.error(chalk6.red(`\u274C Cannot connect to ${source.name} database`));
1724
- process.exit(1);
1725
- }
1726
- spinner.text = "Connecting to target database...";
1727
- const targetConnected = await testDatabaseConnection(targetEnv);
1728
- if (!targetConnected) {
1729
- spinner.fail("Failed to connect to target database");
1730
- console.error(chalk6.red(`\u274C Cannot connect to ${targetEnv.name} database`));
1731
- process.exit(1);
1732
- }
1733
- spinner.succeed("Database connections OK");
1734
- console.log(chalk6.dim("\nCollecting database information..."));
1735
- const [sourceInfo, targetInfo] = await Promise.all([
1736
- getDatabaseInfo(source),
1737
- getDatabaseInfo(targetEnv)
1738
- ]);
1739
- console.log(chalk6.blue("\n\u{1F4CB} Sync Plan:\n"));
1740
- console.log(chalk6.white(` Source: ${chalk6.cyan(source.name)} (${source.connection.database})`));
1741
- console.log(chalk6.dim(` ${sourceInfo.tableCount} tables, ${sourceInfo.size}`));
1742
- console.log("");
1743
- console.log(chalk6.white(` Target: ${chalk6.yellow(targetEnv.name)} (${targetEnv.connection.database})`));
1744
- console.log(chalk6.dim(` ${targetInfo.tableCount} tables, ${targetInfo.size}`));
1745
- console.log("");
1746
- if (options.tables) {
1747
- console.log(chalk6.dim(` Tables: ${options.tables}`));
1748
- }
1749
- if (options.excludeTables) {
1750
- console.log(chalk6.dim(` Exclude: ${options.excludeTables}`));
1751
- }
1752
- if (options.dataOnly) {
1753
- console.log(chalk6.yellow(" \u26A0\uFE0F Data only (schema will not be changed)"));
1754
- }
1755
- if (options.schemaOnly) {
1756
- console.log(chalk6.yellow(" \u26A0\uFE0F Schema only (data will not be changed)"));
1757
- }
1758
- console.log(chalk6.yellow("\n \u26A0\uFE0F Target database will be completely replaced!"));
1759
- console.log(chalk6.dim(" \u2139\uFE0F Target will be backed up before sync\n"));
1760
- if (options.dryRun) {
1761
- console.log(chalk6.green("\u2705 Dry run complete (no changes made)"));
1762
- return;
1763
- }
1764
- if (!options.yes) {
1765
- const { confirm } = await prompts2({
1766
- type: "confirm",
1767
- name: "confirm",
1768
- message: chalk6.yellow(`Sync ${source.name} \u2192 ${targetEnv.name}?`),
1769
- initial: false
1770
- });
1771
- if (!confirm) {
1772
- console.log(chalk6.gray("Cancelled"));
1773
- process.exit(0);
1774
- }
1775
- }
1776
- console.log(chalk6.blue("\n\u{1F4E6} Step 1/4: Creating target backup...\n"));
1777
- const originalDatabaseUrl = process.env.DATABASE_URL;
1778
- process.env.DATABASE_URL = targetEnv.url;
1779
- try {
1780
- await dbBackup({
1781
- format: "custom",
1782
- tag: `pre-sync-from-${source.name}`,
1783
- env: targetEnv.name
1784
- });
1785
- } finally {
1786
- process.env.DATABASE_URL = originalDatabaseUrl;
1787
- }
1788
- console.log(chalk6.blue("\n\u{1F4E4} Step 2/4: Dumping source database..."));
1789
- const tempDir = path.join(process.cwd(), "backups");
1790
- const timestamp = formatTimestamp();
1791
- const tempDumpFile = path.join(tempDir, `_temp_sync_${timestamp}.dump`);
1792
- const dumpSpinner = ora3("Creating source dump...").start();
1793
- const dumpArgs = [
1794
- "-h",
1795
- source.connection.host,
1796
- "-p",
1797
- source.connection.port,
1798
- "-U",
1799
- source.connection.user,
1800
- "-d",
1801
- source.connection.database,
1802
- "-f",
1803
- tempDumpFile,
1804
- "-Fc"
1805
- // Custom format
1806
- ];
1807
- if (options.tables) {
1808
- const tables = options.tables.split(",").map((t) => t.trim());
1809
- tables.forEach((table) => {
1810
- dumpArgs.push("-t", table);
1811
- });
1812
- }
1813
- if (options.excludeTables) {
1814
- const tables = options.excludeTables.split(",").map((t) => t.trim());
1815
- tables.forEach((table) => {
1816
- dumpArgs.push("-T", table);
1817
- });
1818
- }
1819
- if (options.dataOnly) {
1820
- dumpArgs.push("--data-only");
1821
- }
1822
- if (options.schemaOnly) {
1823
- dumpArgs.push("--schema-only");
1824
- }
1825
- const dumpProcess = spawn("pg_dump", dumpArgs, {
1826
- stdio: ["ignore", "pipe", "pipe"],
1827
- env: {
1828
- ...process.env,
1829
- PGPASSWORD: source.connection.password
1830
- }
1831
- });
1832
- let dumpError = "";
1833
- dumpProcess.stderr?.on("data", (data) => {
1834
- dumpError += data.toString();
1835
- });
1836
- await new Promise((resolve, reject) => {
1837
- dumpProcess.on("close", (code) => {
1838
- if (code === 0) {
1839
- dumpSpinner.succeed("Source dump created");
1840
- resolve();
1841
- } else {
1842
- dumpSpinner.fail("Source dump failed");
1843
- reject(new Error(dumpError || "pg_dump failed"));
1844
- }
1845
- });
1846
- dumpProcess.on("error", (error) => {
1847
- dumpSpinner.fail("Source dump failed");
1848
- reject(error);
1849
- });
1850
- }).catch((error) => {
1851
- console.error(chalk6.red(`
1852
- \u274C ${error instanceof Error ? error.message : "Failed to dump source"}`));
1853
- process.exit(1);
1854
- });
1855
- console.log(chalk6.blue("\n\u{1F4E5} Step 3/4: Restoring to target database..."));
1856
- const restoreSpinner = ora3("Restoring to target...").start();
1857
- const restoreArgs = [
1858
- "-h",
1859
- targetEnv.connection.host,
1860
- "-p",
1861
- targetEnv.connection.port,
1862
- "-U",
1863
- targetEnv.connection.user,
1864
- "-d",
1865
- targetEnv.connection.database,
1866
- "--clean",
1867
- // Drop existing objects
1868
- "--if-exists"
1869
- // Don't error if objects don't exist
1870
- ];
1871
- if (options.dataOnly) {
1872
- restoreArgs.push("--data-only");
1873
- }
1874
- if (options.schemaOnly) {
1875
- restoreArgs.push("--schema-only");
1876
- }
1877
- restoreArgs.push(tempDumpFile);
1878
- const restoreProcess = spawn("pg_restore", restoreArgs, {
1879
- stdio: ["ignore", "pipe", "pipe"],
1880
- env: {
1881
- ...process.env,
1882
- PGPASSWORD: targetEnv.connection.password
1883
- }
1884
- });
1885
- let restoreError = "";
1886
- restoreProcess.stderr?.on("data", (data) => {
1887
- restoreError += data.toString();
1888
- });
1889
- await new Promise((resolve, reject) => {
1890
- restoreProcess.on("close", (code) => {
1891
- if (code === 0) {
1892
- restoreSpinner.succeed("Target restored");
1893
- resolve();
1894
- } else {
1895
- restoreSpinner.fail("Target restore failed");
1896
- reject(new Error(restoreError || "pg_restore failed"));
1897
- }
1898
- });
1899
- restoreProcess.on("error", (error) => {
1900
- restoreSpinner.fail("Target restore failed");
1901
- reject(error);
1902
- });
1903
- }).catch((error) => {
1904
- console.error(chalk6.red(`
1905
- \u274C ${error instanceof Error ? error.message : "Failed to restore target"}`));
1906
- console.log(chalk6.yellow("\n\u{1F4A1} You can restore the target from the backup created in Step 1"));
2969
+ console.error(chalk22.red(error instanceof Error ? error.message : "Unknown error"));
1907
2970
  process.exit(1);
1908
- });
1909
- console.log(chalk6.blue("\n\u{1F9F9} Step 4/4: Cleaning up..."));
1910
- try {
1911
- await fs.unlink(tempDumpFile);
1912
- console.log(chalk6.dim("\u2713 Temporary files deleted"));
1913
- } catch (error) {
1914
- console.log(chalk6.dim("\u26A0\uFE0F Could not delete temporary dump file"));
1915
2971
  }
1916
- console.log(chalk6.green(`
1917
- \u2705 Sync completed successfully!`));
1918
- console.log(chalk6.dim(` ${source.name} \u2192 ${targetEnv.name}`));
1919
- console.log("");
1920
2972
  }
1921
- var dbCommand = new Command7("db").description("Database management commands (wraps Drizzle Kit)");
2973
+
2974
+ // src/commands/db/index.ts
2975
+ var dbCommand = new Command9("db").description("Database management commands (wraps Drizzle Kit)");
1922
2976
  dbCommand.command("generate").alias("g").description("Generate database migrations from schema changes").action(dbGenerate);
1923
2977
  dbCommand.command("push").description("Push schema changes directly to database (no migrations)").action(dbPush);
1924
2978
  dbCommand.command("migrate").alias("m").description("Run pending migrations").option("--with-backup", "Create backup before running migrations").action((options) => dbMigrate(options));
@@ -1926,36 +2980,35 @@ dbCommand.command("studio").description("Open Drizzle Studio (database GUI)").op
1926
2980
  dbCommand.command("drop").description("Drop all database tables (\u26A0\uFE0F dangerous!)").action(dbDrop);
1927
2981
  dbCommand.command("check").description("Check database connection").action(dbCheck);
1928
2982
  dbCommand.command("backup").description("Create a database backup").option("-f, --format <format>", "Backup format (sql or custom)", "sql").option("-o, --output <path>", "Custom output path").option("-s, --schema <name>", "Backup specific schema only").option("--data-only", "Backup data only (no schema)").option("--schema-only", "Backup schema only (no data)").option("--tag <tags>", "Comma-separated tags for this backup").option("--env <environment>", "Environment label (e.g., production, staging)").action((options) => dbBackup(options));
1929
- dbCommand.command("restore [file]").description("Restore database from backup").option("--drop", "Drop existing tables before restore").option("-s, --schema <name>", "Restore specific schema only").option("--data-only", "Restore data only (requires custom format .dump file)").option("--schema-only", "Restore schema only (requires custom format .dump file)").action((file, options) => dbRestore(file, options));
2983
+ dbCommand.command("restore [file]").description("Restore database from backup").option("--drop", "Drop existing tables before restore").option("-s, --schema <name>", "Restore specific schema only").option("--data-only", "Restore data only (requires custom format .dump file)").option("--schema-only", "Restore schema only (requires custom format .dump file)").option("-v, --verbose", "Show detailed restore progress").action((file, options) => dbRestore(file, options));
1930
2984
  dbCommand.command("backup:list").description("List all database backups").action(dbBackupList);
1931
2985
  dbCommand.command("backup:clean").description("Clean old database backups").option("-k, --keep <number>", "Keep N most recent backups", parseInt).option("-o, --older-than <days>", "Delete backups older than N days", parseInt).action((options) => dbBackupClean(options));
1932
- dbCommand.command("sync <target>").description("Sync database between environments").option("--pull", "Pull from target to local (reverse direction)").option("--tables <tables>", "Sync only specific tables (comma-separated)").option("--exclude-tables <tables>", "Exclude specific tables (comma-separated)").option("--data-only", "Sync data only (schema unchanged)").option("--schema-only", "Sync schema only (data unchanged)").option("--force", "Allow syncing to production-like environments").option("--dry-run", "Show sync plan without making changes").option("-y, --yes", "Skip confirmation prompt").action((target, options) => dbSync(target, options));
1933
2986
 
1934
2987
  // src/commands/add.ts
1935
- import { Command as Command8 } from "commander";
1936
- import { existsSync as existsSync7, readFileSync as readFileSync3 } from "fs";
1937
- import { join as join6 } from "path";
2988
+ import { Command as Command10 } from "commander";
2989
+ import { existsSync as existsSync19, readFileSync as readFileSync6 } from "fs";
2990
+ import { join as join16 } from "path";
1938
2991
  import { exec as exec2 } from "child_process";
1939
2992
  import { promisify as promisify2 } from "util";
1940
- import chalk7 from "chalk";
1941
- import ora4 from "ora";
2993
+ import chalk23 from "chalk";
2994
+ import ora11 from "ora";
1942
2995
  var execAsync2 = promisify2(exec2);
1943
2996
  async function addPackage(packageName) {
1944
2997
  if (!packageName.includes("/")) {
1945
- console.error(chalk7.red("\u274C Please specify full package name"));
1946
- console.log(chalk7.yellow("\n\u{1F4A1} Examples:"));
1947
- console.log(chalk7.gray(" pnpm spfn add @spfn/cms"));
1948
- console.log(chalk7.gray(" pnpm spfn add @mycompany/spfn-analytics"));
2998
+ console.error(chalk23.red("\u274C Please specify full package name"));
2999
+ console.log(chalk23.yellow("\n\u{1F4A1} Examples:"));
3000
+ console.log(chalk23.gray(" pnpm spfn add @spfn/cms"));
3001
+ console.log(chalk23.gray(" pnpm spfn add @mycompany/spfn-analytics"));
1949
3002
  process.exit(1);
1950
3003
  }
1951
- console.log(chalk7.blue(`
3004
+ console.log(chalk23.blue(`
1952
3005
  \u{1F4E6} Setting up ${packageName}...
1953
3006
  `));
1954
3007
  try {
1955
- const pkgPath = join6(process.cwd(), "node_modules", ...packageName.split("/"));
1956
- const pkgJsonPath = join6(pkgPath, "package.json");
1957
- if (!existsSync7(pkgJsonPath)) {
1958
- const installSpinner = ora4("Installing package...").start();
3008
+ const pkgPath = join16(process.cwd(), "node_modules", ...packageName.split("/"));
3009
+ const pkgJsonPath = join16(pkgPath, "package.json");
3010
+ if (!existsSync19(pkgJsonPath)) {
3011
+ const installSpinner = ora11("Installing package...").start();
1959
3012
  try {
1960
3013
  await execAsync2(`pnpm add ${packageName}`);
1961
3014
  installSpinner.succeed("Package installed");
@@ -1964,73 +3017,74 @@ async function addPackage(packageName) {
1964
3017
  throw error;
1965
3018
  }
1966
3019
  } else {
1967
- console.log(chalk7.gray("\u2713 Package already installed (using local version)\n"));
3020
+ console.log(chalk23.gray("\u2713 Package already installed (using local version)\n"));
1968
3021
  }
1969
- if (!existsSync7(pkgJsonPath)) {
3022
+ if (!existsSync19(pkgJsonPath)) {
1970
3023
  throw new Error(`Package ${packageName} not found after installation`);
1971
3024
  }
1972
- const pkgJson = JSON.parse(readFileSync3(pkgJsonPath, "utf-8"));
3025
+ const pkgJson = JSON.parse(readFileSync6(pkgJsonPath, "utf-8"));
1973
3026
  if (pkgJson.spfn?.migrations) {
1974
- console.log(chalk7.blue(`
3027
+ console.log(chalk23.blue(`
1975
3028
  \u{1F5C4}\uFE0F Setting up database for ${packageName}...
1976
3029
  `));
1977
- const { loadEnvironment } = await import("@spfn/core/env");
1978
- loadEnvironment({ debug: false });
1979
- if (!process.env.DATABASE_URL) {
1980
- console.log(chalk7.yellow("\u26A0\uFE0F DATABASE_URL not found"));
1981
- console.log(chalk7.gray("Skipping database setup. Run migrations manually when ready:\n"));
1982
- console.log(chalk7.gray(` pnpm spfn db push
3030
+ const { env: env6 } = await import("@spfn/core/config");
3031
+ if (!env6.DATABASE_URL) {
3032
+ console.log(chalk23.yellow("\u26A0\uFE0F DATABASE_URL not found"));
3033
+ console.log(chalk23.gray("Skipping database setup. Run migrations manually when ready:\n"));
3034
+ console.log(chalk23.gray(` pnpm spfn db push
1983
3035
  `));
1984
3036
  } else {
1985
- const { discoverFunctionMigrations, executeFunctionMigrations } = await import("./function-migrations-AXX6HWXL.js");
1986
- const functions = discoverFunctionMigrations(process.cwd());
3037
+ const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
3038
+ const functions = discoverFunctionMigrations2(process.cwd());
1987
3039
  const targetFunction = functions.find((f) => f.packageName === packageName);
1988
3040
  if (targetFunction) {
1989
- const spinner = ora4("Applying migrations...").start();
3041
+ const spinner = ora11("Applying migrations...").start();
1990
3042
  try {
1991
- await executeFunctionMigrations([targetFunction]);
3043
+ await executeFunctionMigrations2([targetFunction]);
1992
3044
  spinner.succeed("Migrations applied");
1993
3045
  } catch (error) {
1994
3046
  spinner.fail("Failed to apply migrations");
1995
3047
  throw error;
1996
3048
  }
1997
3049
  } else {
1998
- console.log(chalk7.gray("\u2139\uFE0F No migrations found for this package"));
3050
+ console.log(chalk23.gray("\u2139\uFE0F No migrations found for this package"));
1999
3051
  }
2000
3052
  }
2001
3053
  } else {
2002
- console.log(chalk7.gray("\n\u2139\uFE0F No database migrations to apply"));
3054
+ console.log(chalk23.gray("\n\u2139\uFE0F No database migrations to apply"));
2003
3055
  }
2004
- console.log(chalk7.green(`
3056
+ console.log(chalk23.green(`
2005
3057
  \u2705 ${packageName} installed successfully!
2006
3058
  `));
2007
3059
  if (pkgJson.spfn?.setupMessage) {
2008
- console.log(chalk7.cyan("\u{1F4DA} Setup Guide:"));
3060
+ console.log(chalk23.cyan("\u{1F4DA} Setup Guide:"));
2009
3061
  console.log(pkgJson.spfn.setupMessage);
2010
3062
  console.log();
2011
3063
  }
2012
3064
  } catch (error) {
2013
- console.error(chalk7.red(`
3065
+ console.error(chalk23.red(`
2014
3066
  \u274C Failed to install ${packageName}
2015
3067
  `));
2016
- console.error(chalk7.red(error instanceof Error ? error.message : "Unknown error"));
3068
+ console.error(chalk23.red(error instanceof Error ? error.message : "Unknown error"));
2017
3069
  process.exit(1);
2018
3070
  }
2019
3071
  }
2020
- var addCommand = new Command8("add").description("Install and set up SPFN ecosystem packages").argument("<package>", "Package name (e.g., @spfn/cms, @mycompany/spfn-analytics)").action(addPackage);
3072
+ var addCommand = new Command10("add").description("Install and set up SPFN ecosystem packages").argument("<package>", "Package name (e.g., @spfn/cms, @mycompany/spfn-analytics)").action(addPackage);
2021
3073
 
2022
3074
  // src/commands/generate.ts
2023
- import { Command as Command9 } from "commander";
2024
- import ora5 from "ora";
2025
- import { join as join15 } from "path";
2026
- import { existsSync as existsSync10 } from "fs";
2027
- import chalk9 from "chalk";
3075
+ init_logger();
3076
+ import { Command as Command11 } from "commander";
3077
+ import ora12 from "ora";
3078
+ import { join as join25 } from "path";
3079
+ import { existsSync as existsSync22 } from "fs";
3080
+ import chalk25 from "chalk";
2028
3081
 
2029
3082
  // src/commands/generate/prompts.ts
2030
- import prompts3 from "prompts";
2031
- import chalk8 from "chalk";
3083
+ init_logger();
3084
+ import prompts6 from "prompts";
3085
+ import chalk24 from "chalk";
2032
3086
  async function promptScope() {
2033
- const response = await prompts3({
3087
+ const response = await prompts6({
2034
3088
  type: "text",
2035
3089
  name: "scope",
2036
3090
  message: "NPM scope (e.g., @mycompany, @username):",
@@ -2052,7 +3106,7 @@ async function promptScope() {
2052
3106
  return response.scope;
2053
3107
  }
2054
3108
  async function promptFunctionName() {
2055
- const response = await prompts3({
3109
+ const response = await prompts6({
2056
3110
  type: "text",
2057
3111
  name: "fnName",
2058
3112
  message: "Function name:",
@@ -2073,7 +3127,7 @@ async function promptFunctionName() {
2073
3127
  return response.fnName;
2074
3128
  }
2075
3129
  async function promptDescription(fnName) {
2076
- const response = await prompts3({
3130
+ const response = await prompts6({
2077
3131
  type: "text",
2078
3132
  name: "description",
2079
3133
  message: "Function description:",
@@ -2082,7 +3136,7 @@ async function promptDescription(fnName) {
2082
3136
  return response.description || "A description of what this module does";
2083
3137
  }
2084
3138
  async function promptEntities() {
2085
- const response = await prompts3({
3139
+ const response = await prompts6({
2086
3140
  type: "list",
2087
3141
  name: "entities",
2088
3142
  message: "Entity names (comma-separated, press enter to skip):",
@@ -2094,14 +3148,14 @@ async function promptEntities() {
2094
3148
  async function confirmConfiguration(config) {
2095
3149
  const { scope, fnName, description, entities, enableCache, enableRoutes } = config;
2096
3150
  console.log("");
2097
- logger.info(chalk8.bold("\u26A1 Function Configuration:"));
2098
- console.log(` ${chalk8.gray("Package:")} ${chalk8.cyan(`${scope}/${fnName}`)}`);
2099
- console.log(` ${chalk8.gray("Description:")} ${description}`);
2100
- console.log(` ${chalk8.gray("Entities:")} ${entities.length > 0 ? entities.join(", ") : chalk8.gray("none")}`);
2101
- console.log(` ${chalk8.gray("Cache:")} ${enableCache ? chalk8.green("yes") : chalk8.gray("no")}`);
2102
- console.log(` ${chalk8.gray("Routes:")} ${enableRoutes ? chalk8.green("yes") : chalk8.gray("no")}`);
3151
+ logger.info(chalk24.bold("\u26A1 Function Configuration:"));
3152
+ console.log(` ${chalk24.gray("Package:")} ${chalk24.cyan(`${scope}/${fnName}`)}`);
3153
+ console.log(` ${chalk24.gray("Description:")} ${description}`);
3154
+ console.log(` ${chalk24.gray("Entities:")} ${entities.length > 0 ? entities.join(", ") : chalk24.gray("none")}`);
3155
+ console.log(` ${chalk24.gray("Cache:")} ${enableCache ? chalk24.green("yes") : chalk24.gray("no")}`);
3156
+ console.log(` ${chalk24.gray("Routes:")} ${enableRoutes ? chalk24.green("yes") : chalk24.gray("no")}`);
2103
3157
  console.log("");
2104
- const { confirmed } = await prompts3({
3158
+ const { confirmed } = await prompts6({
2105
3159
  type: "confirm",
2106
3160
  name: "confirmed",
2107
3161
  message: "Create function?",
@@ -2115,12 +3169,12 @@ async function confirmConfiguration(config) {
2115
3169
  }
2116
3170
 
2117
3171
  // src/commands/generate/generators/structure.ts
2118
- import { join as join14 } from "path";
2119
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync12 } from "fs";
3172
+ import { join as join24 } from "path";
3173
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync17 } from "fs";
2120
3174
 
2121
3175
  // src/commands/generate/generators/config.ts
2122
- import { join as join8 } from "path";
2123
- import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync3 } from "fs";
3176
+ import { join as join18 } from "path";
3177
+ import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync3 } from "fs";
2124
3178
 
2125
3179
  // src/commands/generate/string-utils.ts
2126
3180
  function toPascalCase(str) {
@@ -2149,30 +3203,30 @@ function toSafeSchemaName(str) {
2149
3203
  }
2150
3204
 
2151
3205
  // src/commands/generate/template-loader.ts
2152
- import { readFileSync as readFileSync4, existsSync as existsSync8 } from "fs";
2153
- import { join as join7, dirname } from "path";
2154
- import { fileURLToPath } from "url";
2155
- function findTemplatesPath() {
2156
- const __filename = fileURLToPath(import.meta.url);
2157
- const __dirname = dirname(__filename);
2158
- const distPath = join7(__dirname, "commands", "generate", "templates");
2159
- if (existsSync8(distPath)) {
3206
+ import { readFileSync as readFileSync7, existsSync as existsSync20 } from "fs";
3207
+ import { join as join17, dirname as dirname2 } from "path";
3208
+ import { fileURLToPath as fileURLToPath2 } from "url";
3209
+ function findTemplatesPath2() {
3210
+ const __filename = fileURLToPath2(import.meta.url);
3211
+ const __dirname2 = dirname2(__filename);
3212
+ const distPath = join17(__dirname2, "commands", "generate", "templates");
3213
+ if (existsSync20(distPath)) {
2160
3214
  return distPath;
2161
3215
  }
2162
- const sameDirPath = join7(__dirname, "templates");
2163
- if (existsSync8(sameDirPath)) {
3216
+ const sameDirPath = join17(__dirname2, "templates");
3217
+ if (existsSync20(sameDirPath)) {
2164
3218
  return sameDirPath;
2165
3219
  }
2166
- const srcPath = join7(__dirname, "..", "..", "src", "commands", "generate", "templates");
2167
- if (existsSync8(srcPath)) {
3220
+ const srcPath = join17(__dirname2, "..", "..", "src", "commands", "generate", "templates");
3221
+ if (existsSync20(srcPath)) {
2168
3222
  return srcPath;
2169
3223
  }
2170
3224
  throw new Error(`Templates directory not found. Tried: ${distPath}, ${sameDirPath}, ${srcPath}`);
2171
3225
  }
2172
3226
  function loadTemplate(templateName, variables) {
2173
- const templatesDir = findTemplatesPath();
2174
- const templatePath = join7(templatesDir, `${templateName}.template`);
2175
- let content = readFileSync4(templatePath, "utf-8");
3227
+ const templatesDir = findTemplatesPath2();
3228
+ const templatePath = join17(templatesDir, `${templateName}.template`);
3229
+ let content = readFileSync7(templatePath, "utf-8");
2176
3230
  for (const [key, value] of Object.entries(variables)) {
2177
3231
  const regex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
2178
3232
  content = content.replace(regex, value);
@@ -2274,8 +3328,8 @@ function generatePackageJson(fnDir, scope, fnName, description) {
2274
3328
  vitest: "^4.0.6"
2275
3329
  }
2276
3330
  };
2277
- writeFileSync6(
2278
- join8(fnDir, "package.json"),
3331
+ writeFileSync11(
3332
+ join18(fnDir, "package.json"),
2279
3333
  JSON.stringify(content, null, 4) + "\n"
2280
3334
  );
2281
3335
  }
@@ -2308,8 +3362,8 @@ function generateTsConfig(fnDir) {
2308
3362
  include: ["src/**/*"],
2309
3363
  exclude: ["node_modules", "dist", "**/*.test.ts", "**/__tests__/**"]
2310
3364
  };
2311
- writeFileSync6(
2312
- join8(fnDir, "tsconfig.json"),
3365
+ writeFileSync11(
3366
+ join18(fnDir, "tsconfig.json"),
2313
3367
  JSON.stringify(content, null, 4) + "\n"
2314
3368
  );
2315
3369
  }
@@ -2385,7 +3439,7 @@ export default defineConfig({
2385
3439
  ],
2386
3440
  });
2387
3441
  `;
2388
- writeFileSync6(join8(fnDir, "tsup.config.ts"), content);
3442
+ writeFileSync11(join18(fnDir, "tsup.config.ts"), content);
2389
3443
  }
2390
3444
  function generateDrizzleConfig(fnDir, scope, fnName) {
2391
3445
  const schemaName = `spfn_${toSnakeCase(fnName)}`;
@@ -2405,7 +3459,7 @@ export default defineConfig({
2405
3459
  schemaFilter: ['${schemaName}'], // Only generate for ${fnName} schema
2406
3460
  });
2407
3461
  `;
2408
- writeFileSync6(join8(fnDir, "drizzle.config.ts"), content);
3462
+ writeFileSync11(join18(fnDir, "drizzle.config.ts"), content);
2409
3463
  }
2410
3464
  function generateExampleGenerator(fnDir, scope, fnName) {
2411
3465
  const pascalName = toPascalCase(fnName);
@@ -2474,10 +3528,10 @@ export const moduleName = '${fnName}';
2474
3528
  };
2475
3529
  }
2476
3530
  `;
2477
- const generatorsDir = join8(fnDir, "src/server/generators");
3531
+ const generatorsDir = join18(fnDir, "src/server/generators");
2478
3532
  mkdirSync3(generatorsDir, { recursive: true });
2479
- writeFileSync6(
2480
- join8(generatorsDir, "example-generator.ts"),
3533
+ writeFileSync11(
3534
+ join18(generatorsDir, "example-generator.ts"),
2481
3535
  content
2482
3536
  );
2483
3537
  const indexContent = `/**
@@ -2491,8 +3545,8 @@ export const moduleName = '${fnName}';
2491
3545
 
2492
3546
  export { create${pascalName}ExampleGenerator } from './example-generator.js';
2493
3547
  `;
2494
- writeFileSync6(
2495
- join8(generatorsDir, "index.ts"),
3548
+ writeFileSync11(
3549
+ join18(generatorsDir, "index.ts"),
2496
3550
  indexContent
2497
3551
  );
2498
3552
  }
@@ -2981,15 +4035,15 @@ Contributions are welcome! Please follow the development workflow above.
2981
4035
 
2982
4036
  MIT
2983
4037
  `;
2984
- writeFileSync6(join8(fnDir, "README.md"), content);
4038
+ writeFileSync11(join18(fnDir, "README.md"), content);
2985
4039
  }
2986
4040
 
2987
4041
  // src/commands/generate/generators/entity.ts
2988
- import { join as join9 } from "path";
2989
- import { writeFileSync as writeFileSync7, existsSync as existsSync9 } from "fs";
4042
+ import { join as join19 } from "path";
4043
+ import { writeFileSync as writeFileSync12, existsSync as existsSync21 } from "fs";
2990
4044
  function generateSchema(fnDir, scope, fnName) {
2991
- const schemaFilePath = join9(fnDir, "src/server/entities/schema.ts");
2992
- if (existsSync9(schemaFilePath)) {
4045
+ const schemaFilePath = join19(fnDir, "src/server/entities/schema.ts");
4046
+ if (existsSync21(schemaFilePath)) {
2993
4047
  return;
2994
4048
  }
2995
4049
  const packageName = `${scope}/${fnName}`;
@@ -3000,7 +4054,7 @@ function generateSchema(fnDir, scope, fnName) {
3000
4054
  PACKAGE_NAME: packageName,
3001
4055
  SCHEMA_VAR_NAME: schemaVarName
3002
4056
  });
3003
- writeFileSync7(schemaFilePath, content);
4057
+ writeFileSync12(schemaFilePath, content);
3004
4058
  }
3005
4059
  function generateEntity(fnDir, scope, fnName, entityName) {
3006
4060
  const safeScope = toSafeSchemaName(scope);
@@ -3019,8 +4073,8 @@ function generateEntity(fnDir, scope, fnName, entityName) {
3019
4073
  SCHEMA_VAR_NAME: schemaVarName,
3020
4074
  SCHEMA_FILE_NAME: schemaFileName
3021
4075
  });
3022
- writeFileSync7(
3023
- join9(fnDir, `src/server/entities/${toKebabCase(entityName)}.ts`),
4076
+ writeFileSync12(
4077
+ join19(fnDir, `src/server/entities/${toKebabCase(entityName)}.ts`),
3024
4078
  content
3025
4079
  );
3026
4080
  }
@@ -3028,12 +4082,12 @@ function generateEntitiesIndex(fnDir, entities) {
3028
4082
  const schemaExport = `export * from './schema';`;
3029
4083
  const entityExports = entities.map((entity) => `export * from './${toKebabCase(entity)}';`).join("\n");
3030
4084
  const content = [schemaExport, entityExports].filter(Boolean).join("\n");
3031
- writeFileSync7(join9(fnDir, "src/server/entities/index.ts"), content + "\n");
4085
+ writeFileSync12(join19(fnDir, "src/server/entities/index.ts"), content + "\n");
3032
4086
  }
3033
4087
 
3034
4088
  // src/commands/generate/generators/repository.ts
3035
- import { join as join10 } from "path";
3036
- import { writeFileSync as writeFileSync8 } from "fs";
4089
+ import { join as join20 } from "path";
4090
+ import { writeFileSync as writeFileSync13 } from "fs";
3037
4091
  function generateRepository(fnDir, entityName) {
3038
4092
  const pascalName = toPascalCase(entityName);
3039
4093
  const repoName = `${entityName}Repository`;
@@ -3042,19 +4096,19 @@ function generateRepository(fnDir, entityName) {
3042
4096
  ENTITY_NAME: entityName,
3043
4097
  REPO_NAME: repoName
3044
4098
  });
3045
- writeFileSync8(
3046
- join10(fnDir, `src/server/repositories/${toKebabCase(entityName)}.repository.ts`),
4099
+ writeFileSync13(
4100
+ join20(fnDir, `src/server/repositories/${toKebabCase(entityName)}.repository.ts`),
3047
4101
  content
3048
4102
  );
3049
4103
  }
3050
4104
  function generateRepositoriesIndex(fnDir, entities) {
3051
4105
  const exports = entities.map((entity) => `export * from './${toKebabCase(entity)}.repository';`).join("\n");
3052
- writeFileSync8(join10(fnDir, "src/server/repositories/index.ts"), exports + "\n");
4106
+ writeFileSync13(join20(fnDir, "src/server/repositories/index.ts"), exports + "\n");
3053
4107
  }
3054
4108
 
3055
4109
  // src/commands/generate/generators/route.ts
3056
- import { join as join11 } from "path";
3057
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync9 } from "fs";
4110
+ import { join as join21 } from "path";
4111
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync14 } from "fs";
3058
4112
  function generateRoute(fnDir, entityName) {
3059
4113
  const pascalName = toPascalCase(entityName);
3060
4114
  const repoName = `${entityName}Repository`;
@@ -3065,29 +4119,29 @@ function generateRoute(fnDir, entityName) {
3065
4119
  REPO_NAME: repoName,
3066
4120
  KEBAB_NAME: kebabName
3067
4121
  });
3068
- const routeDir = join11(fnDir, `src/server/routes/${kebabName}`);
4122
+ const routeDir = join21(fnDir, `src/server/routes/${kebabName}`);
3069
4123
  mkdirSync4(routeDir, { recursive: true });
3070
- writeFileSync9(join11(routeDir, "index.ts"), content);
4124
+ writeFileSync14(join21(routeDir, "index.ts"), content);
3071
4125
  }
3072
4126
 
3073
4127
  // src/commands/generate/generators/contract.ts
3074
- import { join as join12 } from "path";
3075
- import { writeFileSync as writeFileSync10 } from "fs";
4128
+ import { join as join22 } from "path";
4129
+ import { writeFileSync as writeFileSync15 } from "fs";
3076
4130
  function generateContract(fnDir, entityName) {
3077
4131
  const pascalName = toPascalCase(entityName);
3078
4132
  const content = loadTemplate("contract", {
3079
4133
  PASCAL_NAME: pascalName,
3080
4134
  ENTITY_NAME: entityName
3081
4135
  });
3082
- writeFileSync10(
3083
- join12(fnDir, `src/lib/contracts/${toKebabCase(entityName)}.ts`),
4136
+ writeFileSync15(
4137
+ join22(fnDir, `src/lib/contracts/${toKebabCase(entityName)}.ts`),
3084
4138
  content
3085
4139
  );
3086
4140
  }
3087
4141
 
3088
4142
  // src/commands/generate/generators/index-files.ts
3089
- import { join as join13 } from "path";
3090
- import { writeFileSync as writeFileSync11 } from "fs";
4143
+ import { join as join23 } from "path";
4144
+ import { writeFileSync as writeFileSync16 } from "fs";
3091
4145
  function generateMainIndex(fnDir, fnName) {
3092
4146
  const content = `/**
3093
4147
  * @spfn/${fnName}
@@ -3113,7 +4167,7 @@ export * from '@/lib/types/index';
3113
4167
 
3114
4168
  export * from '@/server/entities/index';
3115
4169
  `;
3116
- writeFileSync11(join13(fnDir, "src/index.ts"), content);
4170
+ writeFileSync16(join23(fnDir, "src/index.ts"), content);
3117
4171
  }
3118
4172
  function generateServerIndex(fnDir) {
3119
4173
  const content = `/**
@@ -3148,7 +4202,7 @@ export * from '@/server/repositories/index';
3148
4202
 
3149
4203
  // TODO: Export helpers here
3150
4204
  `;
3151
- writeFileSync11(join13(fnDir, "src/server.ts"), content);
4205
+ writeFileSync16(join23(fnDir, "src/server.ts"), content);
3152
4206
  }
3153
4207
  function generateClientIndex(fnDir) {
3154
4208
  const content = `/**
@@ -3181,7 +4235,7 @@ export * from './client/store';
3181
4235
 
3182
4236
  export * from './client/components';
3183
4237
  `;
3184
- writeFileSync11(join13(fnDir, "src/client.ts"), content);
4238
+ writeFileSync16(join23(fnDir, "src/client.ts"), content);
3185
4239
  }
3186
4240
  function generateTypesFile(fnDir, fnName) {
3187
4241
  const content = `/**
@@ -3193,7 +4247,7 @@ function generateTypesFile(fnDir, fnName) {
3193
4247
 
3194
4248
  export * from '@/lib/types/index';
3195
4249
  `;
3196
- writeFileSync11(join13(fnDir, "src/types.ts"), content);
4250
+ writeFileSync16(join23(fnDir, "src/types.ts"), content);
3197
4251
  }
3198
4252
 
3199
4253
  // src/commands/generate/generators/structure.ts
@@ -3215,7 +4269,7 @@ async function generateFunctionStructure(options) {
3215
4269
  "src/client/store",
3216
4270
  "src/client/components"
3217
4271
  ];
3218
- dirs.forEach((dir) => mkdirSync5(join14(fnDir, dir), { recursive: true }));
4272
+ dirs.forEach((dir) => mkdirSync5(join24(fnDir, dir), { recursive: true }));
3219
4273
  generatePackageJson(fnDir, scope, fnName, description);
3220
4274
  generateTsConfig(fnDir);
3221
4275
  generateTsupConfig(fnDir);
@@ -3235,15 +4289,15 @@ async function generateFunctionStructure(options) {
3235
4289
  generateEntitiesIndex(fnDir, entities);
3236
4290
  generateRepositoriesIndex(fnDir, entities);
3237
4291
  } else {
3238
- writeFileSync12(join14(fnDir, "src/server/entities/index.ts"), "// Export your entities here\nexport {}\n");
3239
- writeFileSync12(join14(fnDir, "src/server/repositories/index.ts"), "// Export your repositories here\nexport {}\n");
3240
- }
3241
- writeFileSync12(join14(fnDir, "src/client/hooks/index.ts"), "/**\n * Client Hooks\n */\n\n// TODO: Add hooks (e.g., useAuth, useData, etc.)\nexport {}\n");
3242
- writeFileSync12(join14(fnDir, "src/client/store/index.ts"), "/**\n * Client Store\n */\n\n// TODO: Add Zustand store if needed\nexport {}\n");
3243
- writeFileSync12(join14(fnDir, "src/client/components/index.ts"), "/**\n * Client Components\n */\n\n// TODO: Add React components\nexport {}\n");
3244
- writeFileSync12(join14(fnDir, "src/client/index.ts"), "/**\n * Client Module Entry\n */\n\nexport * from './hooks';\nexport * from './store';\nexport * from './components';\n");
3245
- writeFileSync12(join14(fnDir, "src/lib/types/index.ts"), "/**\n * Shared Type Definitions\n */\n\n// Add your shared types here\nexport {}\n");
3246
- writeFileSync12(join14(fnDir, "src/lib/contracts/index.ts"), "/**\n * API Contracts\n */\n\n// Export your contracts here\nexport {}\n");
4292
+ writeFileSync17(join24(fnDir, "src/server/entities/index.ts"), "// Export your entities here\nexport {}\n");
4293
+ writeFileSync17(join24(fnDir, "src/server/repositories/index.ts"), "// Export your repositories here\nexport {}\n");
4294
+ }
4295
+ writeFileSync17(join24(fnDir, "src/client/hooks/index.ts"), "/**\n * Client Hooks\n */\n\n// TODO: Add hooks (e.g., useAuth, useData, etc.)\nexport {}\n");
4296
+ writeFileSync17(join24(fnDir, "src/client/store/index.ts"), "/**\n * Client Store\n */\n\n// TODO: Add Zustand store if needed\nexport {}\n");
4297
+ writeFileSync17(join24(fnDir, "src/client/components/index.ts"), "/**\n * Client Components\n */\n\n// TODO: Add React components\nexport {}\n");
4298
+ writeFileSync17(join24(fnDir, "src/client/index.ts"), "/**\n * Client Module Entry\n */\n\nexport * from './hooks';\nexport * from './store';\nexport * from './components';\n");
4299
+ writeFileSync17(join24(fnDir, "src/lib/types/index.ts"), "/**\n * Shared Type Definitions\n */\n\n// Add your shared types here\nexport {}\n");
4300
+ writeFileSync17(join24(fnDir, "src/lib/contracts/index.ts"), "/**\n * API Contracts\n */\n\n// Export your contracts here\nexport {}\n");
3247
4301
  generateMainIndex(fnDir, fnName);
3248
4302
  generateServerIndex(fnDir);
3249
4303
  generateClientIndex(fnDir);
@@ -3267,8 +4321,8 @@ async function generateFunction(name, options) {
3267
4321
  logger.error("Function name is required");
3268
4322
  process.exit(1);
3269
4323
  }
3270
- const fnDir = join15(cwd, fnName);
3271
- if (existsSync10(fnDir)) {
4324
+ const fnDir = join25(cwd, fnName);
4325
+ if (existsSync22(fnDir)) {
3272
4326
  logger.error(`Directory ${fnName} already exists at ${fnDir}`);
3273
4327
  process.exit(1);
3274
4328
  }
@@ -3300,7 +4354,7 @@ async function generateFunction(name, options) {
3300
4354
  process.exit(0);
3301
4355
  }
3302
4356
  }
3303
- const spinner = ora5("Generating function structure...").start();
4357
+ const spinner = ora12("Generating function structure...").start();
3304
4358
  try {
3305
4359
  await generateFunctionStructure({
3306
4360
  fnDir,
@@ -3313,13 +4367,13 @@ async function generateFunction(name, options) {
3313
4367
  });
3314
4368
  spinner.succeed("Function structure generated");
3315
4369
  console.log("");
3316
- logger.success(`\u2728 Package ${chalk9.cyan(`${scope}/${fnName}`)} created successfully!
4370
+ logger.success(`\u2728 Package ${chalk25.cyan(`${scope}/${fnName}`)} created successfully!
3317
4371
  `);
3318
- logger.info(chalk9.bold("\u{1F4DA} Next steps:"));
3319
- console.log(` ${chalk9.gray("1.")} cd ${fnName}`);
3320
- console.log(` ${chalk9.gray("2.")} pnpm install ${chalk9.dim("(in monorepo root)")}`);
3321
- console.log(` ${chalk9.gray("3.")} pnpm build`);
3322
- console.log(` ${chalk9.gray("4.")} ${chalk9.dim("Use the package in your app")}`);
4372
+ logger.info(chalk25.bold("\u{1F4DA} Next steps:"));
4373
+ console.log(` ${chalk25.gray("1.")} cd ${fnName}`);
4374
+ console.log(` ${chalk25.gray("2.")} pnpm install ${chalk25.dim("(in monorepo root)")}`);
4375
+ console.log(` ${chalk25.gray("3.")} pnpm build`);
4376
+ console.log(` ${chalk25.gray("4.")} ${chalk25.dim("Use the package in your app")}`);
3323
4377
  console.log("");
3324
4378
  } catch (error) {
3325
4379
  spinner.fail("Failed to generate function");
@@ -3327,12 +4381,434 @@ async function generateFunction(name, options) {
3327
4381
  process.exit(1);
3328
4382
  }
3329
4383
  }
3330
- var generateCommand = new Command9("generate").alias("g").description("Generate SPFN resources");
4384
+ var generateCommand = new Command11("generate").alias("g").description("Generate SPFN resources");
3331
4385
  generateCommand.command("fn").description("Generate a new SPFN function module").argument("[name]", "Function name").option("-s, --scope <scope>", "NPM scope (e.g., @spfn, @mycompany)").option("-d, --description <text>", "Function description").option("-e, --entities <list>", "Comma-separated entity names").option("--skip-cache", "Skip cache generation").option("--skip-routes", "Skip route generation").option("-y, --yes", "Skip all prompts").action(generateFunction);
3332
4386
 
4387
+ // src/commands/env.ts
4388
+ import { Command as Command12 } from "commander";
4389
+ import chalk26 from "chalk";
4390
+ import { existsSync as existsSync23, readFileSync as readFileSync8, writeFileSync as writeFileSync18 } from "fs";
4391
+ import { resolve } from "path";
4392
+ import { parse } from "dotenv";
4393
+ var ENV_FILES = {
4394
+ nextjs: [".env", ".env.local"],
4395
+ server: [".env.server", ".env.server.local"]
4396
+ };
4397
+ function getTargetFile(schema) {
4398
+ const isNextjs = schema.nextjs ?? schema.key?.startsWith("NEXT_PUBLIC_");
4399
+ if (isNextjs) {
4400
+ return schema.sensitive ? ".env.local" : ".env";
4401
+ }
4402
+ return schema.sensitive ? ".env.server.local" : ".env.server";
4403
+ }
4404
+ async function loadEnvSchema(packageName) {
4405
+ try {
4406
+ const schemaPath = `${packageName}/config`;
4407
+ const module = await import(schemaPath);
4408
+ if (!module.envSchema) {
4409
+ throw new Error(`Package ${packageName} does not export envSchema from config`);
4410
+ }
4411
+ return module.envSchema;
4412
+ } catch (error) {
4413
+ if (error instanceof Error && error.message.includes("does not export envSchema")) {
4414
+ throw error;
4415
+ }
4416
+ const errorMessage = error instanceof Error ? error.message : String(error);
4417
+ throw new Error(`Failed to load package ${packageName}: ${errorMessage}`);
4418
+ }
4419
+ }
4420
+ function formatType(type) {
4421
+ const typeColors = {
4422
+ string: chalk26.green,
4423
+ number: chalk26.blue,
4424
+ boolean: chalk26.yellow,
4425
+ url: chalk26.cyan,
4426
+ enum: chalk26.magenta,
4427
+ json: chalk26.red
4428
+ };
4429
+ const colorFn = typeColors[type] || chalk26.white;
4430
+ return colorFn(type);
4431
+ }
4432
+ function formatDefault(value, type) {
4433
+ if (value === void 0) {
4434
+ return chalk26.dim("(none)");
4435
+ }
4436
+ if (type === "string" || type === "url") {
4437
+ return chalk26.green(`"${value}"`);
4438
+ }
4439
+ if (type === "boolean") {
4440
+ return value ? chalk26.green("true") : chalk26.red("false");
4441
+ }
4442
+ return chalk26.cyan(String(value));
4443
+ }
4444
+ async function listEnvVars(options) {
4445
+ const packageName = options.package || "@spfn/core";
4446
+ try {
4447
+ const envSchema = await loadEnvSchema(packageName);
4448
+ const allVars = Object.entries(envSchema);
4449
+ if (options.group) {
4450
+ const grouped = allVars.reduce((acc, [key, schema]) => {
4451
+ const target = getTargetFile(schema);
4452
+ if (!acc[target]) acc[target] = [];
4453
+ acc[target].push([key, schema]);
4454
+ return acc;
4455
+ }, {});
4456
+ console.log(chalk26.blue.bold(`
4457
+ \u{1F4CB} Environment Variables by File (${packageName})
4458
+ `));
4459
+ for (const [file, vars] of Object.entries(grouped)) {
4460
+ console.log(chalk26.bold.magenta(`
4461
+ ${file}`));
4462
+ console.log(chalk26.dim("\u2500".repeat(50)));
4463
+ for (const [key, schema] of vars) {
4464
+ printEnvVar(key, schema);
4465
+ }
4466
+ }
4467
+ } else {
4468
+ console.log(chalk26.blue.bold(`
4469
+ \u{1F4CB} Environment Variables (${packageName})
4470
+ `));
4471
+ for (const [key, schema] of allVars) {
4472
+ printEnvVar(key, schema, true);
4473
+ }
4474
+ }
4475
+ console.log(chalk26.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
4476
+ } catch (error) {
4477
+ console.error(chalk26.red(`
4478
+ \u274C ${error instanceof Error ? error.message : "Unknown error"}
4479
+ `));
4480
+ process.exit(1);
4481
+ }
4482
+ }
4483
+ function printEnvVar(key, schema, showFile = false) {
4484
+ const typeStr = formatType(schema.type);
4485
+ const requiredStr = schema.required || schema.default !== void 0 ? chalk26.red("[required]") : chalk26.dim("[optional]");
4486
+ const sensitiveStr = schema.sensitive ? chalk26.yellow(" [sensitive]") : "";
4487
+ const fileStr = showFile ? chalk26.dim(` \u2192 ${getTargetFile(schema)}`) : "";
4488
+ console.log(`${chalk26.bold.cyan(key)} ${chalk26.dim("(")}${typeStr}${chalk26.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
4489
+ console.log(` ${chalk26.dim(schema.description)}`);
4490
+ if (schema.default !== void 0) {
4491
+ console.log(` ${chalk26.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
4492
+ }
4493
+ if (schema.examples && schema.examples.length > 0) {
4494
+ const exampleStr = schema.examples.map((ex) => formatDefault(ex, schema.type)).join(", ");
4495
+ console.log(` ${chalk26.dim("Examples:")} ${exampleStr}`);
4496
+ }
4497
+ console.log();
4498
+ }
4499
+ async function showEnvStats(options) {
4500
+ const packageName = options.package || "@spfn/core";
4501
+ try {
4502
+ const envSchema = await loadEnvSchema(packageName);
4503
+ console.log(chalk26.blue.bold(`
4504
+ \u{1F4CA} Environment Variable Statistics (${packageName})
4505
+ `));
4506
+ const allVars = Object.entries(envSchema);
4507
+ const required = allVars.filter(([_, schema]) => schema.required || schema.default !== void 0);
4508
+ const optional = allVars.filter(([_, schema]) => !schema.required && schema.default === void 0);
4509
+ const sensitive = allVars.filter(([_, schema]) => schema.sensitive);
4510
+ const nextjsVars = allVars.filter(
4511
+ ([_, schema]) => schema.nextjs ?? schema.key?.startsWith("NEXT_PUBLIC_")
4512
+ );
4513
+ const serverOnlyVars = allVars.filter(
4514
+ ([_, schema]) => !(schema.nextjs ?? schema.key?.startsWith("NEXT_PUBLIC_"))
4515
+ );
4516
+ const typeCount = allVars.reduce((acc, [_, schema]) => {
4517
+ acc[schema.type] = (acc[schema.type] || 0) + 1;
4518
+ return acc;
4519
+ }, {});
4520
+ const fileCount = allVars.reduce((acc, [_, schema]) => {
4521
+ const file = getTargetFile(schema);
4522
+ acc[file] = (acc[file] || 0) + 1;
4523
+ return acc;
4524
+ }, {});
4525
+ console.log(`${chalk26.bold("Total variables:")} ${chalk26.cyan(allVars.length)}`);
4526
+ console.log(`${chalk26.bold("Required:")} ${chalk26.red(required.length)}`);
4527
+ console.log(`${chalk26.bold("Optional:")} ${chalk26.dim(optional.length)}`);
4528
+ console.log(`${chalk26.bold("Sensitive:")} ${chalk26.yellow(sensitive.length)}`);
4529
+ console.log(chalk26.bold("\nBy Target:"));
4530
+ console.log(` ${chalk26.blue("Next.js accessible:")} ${chalk26.cyan(nextjsVars.length)}`);
4531
+ console.log(` ${chalk26.magenta("SPFN server only:")} ${chalk26.cyan(serverOnlyVars.length)}`);
4532
+ console.log(chalk26.bold("\nBy File:"));
4533
+ for (const [file, count] of Object.entries(fileCount)) {
4534
+ console.log(` ${chalk26.dim(file)}: ${chalk26.cyan(count)}`);
4535
+ }
4536
+ console.log(chalk26.bold("\nBy Type:"));
4537
+ for (const [type, count] of Object.entries(typeCount)) {
4538
+ console.log(` ${formatType(type)}: ${chalk26.cyan(count)}`);
4539
+ }
4540
+ console.log();
4541
+ } catch (error) {
4542
+ console.error(chalk26.red(`
4543
+ \u274C ${error instanceof Error ? error.message : "Unknown error"}
4544
+ `));
4545
+ process.exit(1);
4546
+ }
4547
+ }
4548
+ async function searchEnvVars(query, options) {
4549
+ const packageName = options.package || "@spfn/core";
4550
+ try {
4551
+ const envSchema = await loadEnvSchema(packageName);
4552
+ const normalizedQuery = query.toLowerCase();
4553
+ const results = [];
4554
+ for (const [key, schema] of Object.entries(envSchema)) {
4555
+ const matchesKey = key.toLowerCase().includes(normalizedQuery);
4556
+ const matchesDescription = schema.description.toLowerCase().includes(normalizedQuery);
4557
+ if (matchesKey || matchesDescription) {
4558
+ results.push([key, schema]);
4559
+ }
4560
+ }
4561
+ if (results.length === 0) {
4562
+ console.log(chalk26.yellow(`
4563
+ \u26A0\uFE0F No environment variables found matching "${query}"
4564
+ `));
4565
+ return;
4566
+ }
4567
+ console.log(chalk26.blue.bold(`
4568
+ \u{1F50D} Found ${results.length} environment variable(s) matching "${query}"
4569
+ `));
4570
+ for (const [key, schema] of results) {
4571
+ const typeStr = formatType(schema.type);
4572
+ const requiredStr = schema.required || schema.default !== void 0 ? chalk26.red("[required]") : chalk26.dim("[optional]");
4573
+ console.log(`${chalk26.bold.cyan(key)} ${chalk26.dim("(")}${typeStr}${chalk26.dim(")")} ${requiredStr}`);
4574
+ console.log(` ${chalk26.dim(schema.description)}`);
4575
+ if (schema.default !== void 0) {
4576
+ console.log(` ${chalk26.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
4577
+ }
4578
+ console.log();
4579
+ }
4580
+ } catch (error) {
4581
+ console.error(chalk26.red(`
4582
+ \u274C ${error instanceof Error ? error.message : "Unknown error"}
4583
+ `));
4584
+ process.exit(1);
4585
+ }
4586
+ }
4587
+ var envCommand = new Command12("env").description("Manage environment variables");
4588
+ envCommand.command("list").description("List all environment variables from schema").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").option("-g, --group", "Group variables by target file").action(listEnvVars);
4589
+ envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(showEnvStats);
4590
+ envCommand.command("search").description("Search environment variables").argument("<query>", "Search query (matches key or description)").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(searchEnvVars);
4591
+ async function initEnvFiles(options) {
4592
+ const packageName = options.package || "@spfn/core";
4593
+ const cwd = process.cwd();
4594
+ try {
4595
+ const envSchema = await loadEnvSchema(packageName);
4596
+ const allVars = Object.entries(envSchema);
4597
+ const grouped = allVars.reduce((acc, [key, schema]) => {
4598
+ const target = getTargetFile(schema);
4599
+ const exampleFile = target + ".example";
4600
+ if (!acc[exampleFile]) acc[exampleFile] = [];
4601
+ acc[exampleFile].push([key, schema]);
4602
+ return acc;
4603
+ }, {});
4604
+ console.log(chalk26.blue.bold(`
4605
+ \u{1F680} Generating .env template files
4606
+ `));
4607
+ for (const [file, vars] of Object.entries(grouped)) {
4608
+ const filePath = resolve(cwd, file);
4609
+ if (existsSync23(filePath) && !options.force) {
4610
+ console.log(chalk26.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
4611
+ continue;
4612
+ }
4613
+ const content = generateEnvFileContent(vars);
4614
+ writeFileSync18(filePath, content, "utf-8");
4615
+ console.log(chalk26.green(` \u2705 ${file} (${vars.length} variables)`));
4616
+ }
4617
+ console.log(chalk26.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
4618
+ console.log(chalk26.dim(" cp .env.example .env"));
4619
+ console.log(chalk26.dim(" cp .env.local.example .env.local"));
4620
+ console.log(chalk26.dim(" cp .env.server.example .env.server"));
4621
+ console.log(chalk26.dim(" cp .env.server.local.example .env.server.local\n"));
4622
+ } catch (error) {
4623
+ console.error(chalk26.red(`
4624
+ \u274C ${error instanceof Error ? error.message : "Unknown error"}
4625
+ `));
4626
+ process.exit(1);
4627
+ }
4628
+ }
4629
+ function generateEnvFileContent(vars) {
4630
+ const lines = [
4631
+ "# Auto-generated by spfn env init",
4632
+ "# Copy this file and fill in the values",
4633
+ ""
4634
+ ];
4635
+ for (const [key, schema] of vars) {
4636
+ lines.push(`# ${schema.description}`);
4637
+ if (schema.required) {
4638
+ lines.push(`# [required]`);
4639
+ }
4640
+ if (schema.sensitive) {
4641
+ lines.push(`# [sensitive] - Do not commit this value!`);
4642
+ }
4643
+ let value = "";
4644
+ if (schema.default !== void 0) {
4645
+ value = String(schema.default);
4646
+ } else if (schema.examples && schema.examples.length > 0) {
4647
+ value = String(schema.examples[0]);
4648
+ }
4649
+ lines.push(`${key}=${value}`);
4650
+ lines.push("");
4651
+ }
4652
+ return lines.join("\n");
4653
+ }
4654
+ async function checkEnvFiles(options) {
4655
+ const packageName = options.package || "@spfn/core";
4656
+ const cwd = process.cwd();
4657
+ try {
4658
+ const envSchema = await loadEnvSchema(packageName);
4659
+ const allVars = Object.entries(envSchema);
4660
+ console.log(chalk26.blue.bold(`
4661
+ \u{1F50D} Checking .env files against schema
4662
+ `));
4663
+ const allFiles = [...ENV_FILES.nextjs, ...ENV_FILES.server];
4664
+ const loadedEnv = {};
4665
+ const issues = [];
4666
+ const warnings = [];
4667
+ for (const file of allFiles) {
4668
+ const filePath = resolve(cwd, file);
4669
+ if (!existsSync23(filePath)) {
4670
+ continue;
4671
+ }
4672
+ const content = readFileSync8(filePath, "utf-8");
4673
+ const parsed = parse(content);
4674
+ for (const [key, value] of Object.entries(parsed)) {
4675
+ loadedEnv[key] = { value: value || "", file };
4676
+ }
4677
+ console.log(chalk26.dim(` \u{1F4C4} ${file} loaded`));
4678
+ }
4679
+ console.log("");
4680
+ for (const [key, schema] of allVars) {
4681
+ const expectedFile = getTargetFile(schema);
4682
+ const found = loadedEnv[key];
4683
+ if (!found) {
4684
+ if (schema.required && schema.default === void 0) {
4685
+ issues.push(`${chalk26.red("\u2717")} ${chalk26.cyan(key)} is required but not found in any .env file`);
4686
+ }
4687
+ continue;
4688
+ }
4689
+ const isNextjsFile = ENV_FILES.nextjs.includes(found.file);
4690
+ const isServerFile = ENV_FILES.server.includes(found.file);
4691
+ const shouldBeNextjs = schema.nextjs ?? key.startsWith("NEXT_PUBLIC_");
4692
+ if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
4693
+ if (schema.sensitive) {
4694
+ issues.push(
4695
+ `${chalk26.red("\u2717")} ${chalk26.cyan(key)} is sensitive and should be in ${chalk26.magenta(expectedFile)}, but found in ${chalk26.yellow(found.file)} (security risk!)`
4696
+ );
4697
+ } else {
4698
+ warnings.push(
4699
+ `${chalk26.yellow("\u26A0")} ${chalk26.cyan(key)} should be in ${chalk26.magenta(expectedFile)}, but found in ${chalk26.dim(found.file)}`
4700
+ );
4701
+ }
4702
+ }
4703
+ }
4704
+ for (const [key, { file }] of Object.entries(loadedEnv)) {
4705
+ const inSchema = allVars.some(([k]) => k === key);
4706
+ if (!inSchema) {
4707
+ warnings.push(`${chalk26.yellow("\u26A0")} ${chalk26.cyan(key)} in ${chalk26.dim(file)} is not in schema`);
4708
+ }
4709
+ }
4710
+ if (issues.length > 0) {
4711
+ console.log(chalk26.red.bold("Issues:"));
4712
+ for (const issue of issues) {
4713
+ console.log(` ${issue}`);
4714
+ }
4715
+ console.log("");
4716
+ }
4717
+ if (warnings.length > 0) {
4718
+ console.log(chalk26.yellow.bold("Warnings:"));
4719
+ for (const warning of warnings) {
4720
+ console.log(` ${warning}`);
4721
+ }
4722
+ console.log("");
4723
+ }
4724
+ if (issues.length === 0 && warnings.length === 0) {
4725
+ console.log(chalk26.green("\u2705 All environment variables are correctly configured!\n"));
4726
+ } else {
4727
+ console.log(chalk26.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
4728
+ `));
4729
+ if (issues.length > 0) {
4730
+ process.exit(1);
4731
+ }
4732
+ }
4733
+ } catch (error) {
4734
+ console.error(chalk26.red(`
4735
+ \u274C ${error instanceof Error ? error.message : "Unknown error"}
4736
+ `));
4737
+ process.exit(1);
4738
+ }
4739
+ }
4740
+ envCommand.command("init").description("Generate .env template files from schema").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").option("-f, --force", "Overwrite existing files").action(initEnvFiles);
4741
+ envCommand.command("check").description("Check .env files against schema").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(checkEnvFiles);
4742
+ async function validateEnvVars(options) {
4743
+ const packages = options.packages || ["@spfn/core"];
4744
+ console.log(chalk26.blue.bold(`
4745
+ \u{1F50D} Validating environment variables
4746
+ `));
4747
+ const allErrors = [];
4748
+ const allWarnings = [];
4749
+ for (const packageName of packages) {
4750
+ try {
4751
+ console.log(chalk26.dim(` \u{1F4E6} ${packageName}`));
4752
+ const envSchema = await loadEnvSchema(packageName);
4753
+ const { createEnvRegistry } = await import("@spfn/core/env");
4754
+ const registry = createEnvRegistry(envSchema);
4755
+ const result = registry.validateAll();
4756
+ for (const error of result.errors) {
4757
+ allErrors.push({ ...error, package: packageName });
4758
+ }
4759
+ for (const warning of result.warnings) {
4760
+ allWarnings.push({ ...warning, package: packageName });
4761
+ }
4762
+ } catch (error) {
4763
+ if (error instanceof Error && error.message.includes("does not export envSchema")) {
4764
+ console.log(chalk26.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
4765
+ continue;
4766
+ }
4767
+ console.error(chalk26.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
4768
+ if (options.strict) {
4769
+ process.exit(1);
4770
+ }
4771
+ }
4772
+ }
4773
+ console.log("");
4774
+ if (allErrors.length > 0) {
4775
+ console.log(chalk26.red.bold(`\u274C Validation Errors (${allErrors.length}):
4776
+ `));
4777
+ for (const error of allErrors) {
4778
+ console.log(` ${chalk26.red("\u2717")} ${chalk26.cyan(error.key)}`);
4779
+ console.log(` ${chalk26.dim(error.message)}`);
4780
+ console.log(` ${chalk26.dim(`from ${error.package}`)}`);
4781
+ console.log("");
4782
+ }
4783
+ }
4784
+ if (allWarnings.length > 0) {
4785
+ console.log(chalk26.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
4786
+ `));
4787
+ for (const warning of allWarnings) {
4788
+ console.log(` ${chalk26.yellow("\u26A0")} ${chalk26.cyan(warning.key)}`);
4789
+ console.log(` ${chalk26.dim(warning.message)}`);
4790
+ console.log("");
4791
+ }
4792
+ }
4793
+ if (allErrors.length === 0 && allWarnings.length === 0) {
4794
+ console.log(chalk26.green.bold("\u2705 All environment variables are valid!\n"));
4795
+ } else if (allErrors.length === 0) {
4796
+ console.log(chalk26.green("\u2705 No errors found."));
4797
+ console.log(chalk26.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
4798
+ `));
4799
+ } else {
4800
+ console.log(chalk26.red(`
4801
+ \u274C Validation failed with ${allErrors.length} error(s)
4802
+ `));
4803
+ process.exit(1);
4804
+ }
4805
+ }
4806
+ envCommand.command("validate").description("Validate environment variables against schema (for CI/CD)").option("-p, --packages <packages...>", "Packages to validate", ["@spfn/core"]).option("-s, --strict", "Exit on any error (including load failures)").action(validateEnvVars);
4807
+
3333
4808
  // src/index.ts
3334
- var program = new Command10();
3335
- program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version("0.1.0");
4809
+ init_version();
4810
+ var program = new Command13();
4811
+ program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(getCliVersion());
3336
4812
  program.addCommand(createCommand);
3337
4813
  program.addCommand(initCommand);
3338
4814
  program.addCommand(addCommand);
@@ -3344,6 +4820,7 @@ program.addCommand(codegenCommand);
3344
4820
  program.addCommand(keyCommand);
3345
4821
  program.addCommand(setupCommand);
3346
4822
  program.addCommand(dbCommand);
4823
+ program.addCommand(envCommand);
3347
4824
  async function run() {
3348
4825
  await program.parseAsync(process.argv);
3349
4826
  }