spfn 0.2.0-beta.2 → 0.2.0-beta.20

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
@@ -521,35 +521,47 @@ import { existsSync as existsSync6 } from "fs";
521
521
  import { join as join6 } from "path";
522
522
  import fse3 from "fs-extra";
523
523
  async function setupApiProxy(cwd, includeAuth) {
524
- const appDir = existsSync6(join6(cwd, "src", "app")) ? join6(cwd, "src", "app") : join6(cwd, "app");
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
+ }
525
535
  const rpcDir = join6(appDir, "api", "rpc", "[routeName]");
526
536
  const rpcRoutePath = join6(rpcDir, "route.ts");
527
- if (!existsSync6(rpcRoutePath)) {
528
- ensureDirSync3(rpcDir);
529
- const authImport = includeAuth ? `import '@spfn/auth/nextjs/api';
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';
530
543
  ` : "";
531
- const routeContent = `/**
544
+ const routeContent = `/**
532
545
  * SPFN RPC Proxy
533
546
  *
534
- * Resolves routeName to actual HTTP method and path from router,
547
+ * Resolves routeName to actual HTTP method and path from routeMap,
535
548
  * then forwards requests to SPFN API server with automatic:
536
549
  * - Cookie forwarding
537
550
  * - Interceptor execution
538
551
  * - Header manipulation
539
552
  *
540
- * Note: Imports from '@spfn/core/nextjs/server' (server-only)
541
- * Uses next/headers internally - do not import in Client Components
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.
542
555
  */
543
556
 
544
- ${authImport}import { appRouter } from '@/server/router';
557
+ ${authImport}import { routeMap } from '@/generated/route-map';
545
558
  import { createRpcProxy } from '@spfn/core/nextjs/server';
546
559
 
547
- export const { GET, POST } = createRpcProxy({ router: appRouter });
560
+ export const { GET, POST } = createRpcProxy({ routeMap });
548
561
  `;
549
- writeFileSync2(rpcRoutePath, routeContent);
550
- const relativePath = rpcRoutePath.replace(cwd + "/", "");
551
- logger.success(`Created ${relativePath} (RPC proxy)`);
552
- }
562
+ writeFileSync2(rpcRoutePath, routeContent);
563
+ const relativePath = rpcRoutePath.replace(cwd + "/", "");
564
+ logger.success(`Created ${relativePath} (RPC proxy)`);
553
565
  }
554
566
  var ensureDirSync3, writeFileSync2;
555
567
  var init_api_proxy = __esm({
@@ -741,6 +753,23 @@ var init_deployment_config = __esm({
741
753
  }
742
754
  });
743
755
 
756
+ // src/utils/version.ts
757
+ function getCliVersion() {
758
+ return "0.2.0-beta.20";
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
+
744
773
  // src/commands/init/steps/package.ts
745
774
  import ora3 from "ora";
746
775
  import { execa as execa2 } from "execa";
@@ -750,13 +779,15 @@ async function setupPackageJson(cwd, packageJsonPath, packageJson, packageManage
750
779
  packageJson.dependencies = packageJson.dependencies || {};
751
780
  packageJson.devDependencies = packageJson.devDependencies || {};
752
781
  packageJson.scripts = packageJson.scripts || {};
753
- packageJson.dependencies["@spfn/core"] = "alpha";
782
+ const spfnTag = getSpfnTag();
783
+ packageJson.dependencies["@spfn/core"] = spfnTag;
754
784
  packageJson.dependencies["@sinclair/typebox"] = "^0.34.0";
785
+ packageJson.dependencies["drizzle-orm"] = "^0.45.0";
755
786
  packageJson.dependencies["drizzle-typebox"] = "^0.1.0";
756
- packageJson.dependencies["spfn"] = "alpha";
787
+ packageJson.dependencies["spfn"] = spfnTag;
757
788
  packageJson.dependencies["concurrently"] = "^9.2.1";
758
789
  if (includeAuth) {
759
- packageJson.dependencies["@spfn/auth"] = "alpha";
790
+ packageJson.dependencies["@spfn/auth"] = spfnTag;
760
791
  }
761
792
  packageJson.devDependencies["@types/node"] = "^20.11.0";
762
793
  packageJson.devDependencies["tsx"] = "^4.20.6";
@@ -793,6 +824,7 @@ var init_package = __esm({
793
824
  "src/commands/init/steps/package.ts"() {
794
825
  "use strict";
795
826
  init_logger();
827
+ init_version();
796
828
  ({ writeFileSync: writeFileSync4 } = fse6);
797
829
  }
798
830
  });
@@ -802,26 +834,7 @@ import { existsSync as existsSync9, readFileSync as readFileSync2 } from "fs";
802
834
  import { join as join9 } from "path";
803
835
  import fse7 from "fs-extra";
804
836
  async function setupConfigFiles(cwd) {
805
- const envExamplePath = join9(cwd, ".env.local.example");
806
- if (!existsSync9(envExamplePath)) {
807
- const envExampleContent = `# Environment
808
- NODE_ENV=local
809
-
810
- # Logging
811
- SPFN_LOG_LEVEL=info
812
-
813
- # Database (matches docker-compose.yml)
814
- DATABASE_URL=postgresql://spfn:spfn@localhost:5432/spfn_dev
815
-
816
- # Cache - Redis/Valkey (optional)
817
- CACHE_URL=redis://localhost:6379
818
-
819
- # SPFN API Server URL (for API Route Proxy and SSR)
820
- SPFN_API_URL=http://localhost:8790
821
- `;
822
- writeFileSync5(envExamplePath, envExampleContent);
823
- logger.success("Created .env.local.example");
824
- }
837
+ generateEnvExamples(cwd);
825
838
  const spfnrcPath = join9(cwd, ".spfnrc.ts");
826
839
  if (!existsSync9(spfnrcPath)) {
827
840
  const spfnrcContent = `import { defineConfig, defineGenerator } from '@spfn/core/codegen';
@@ -830,64 +843,135 @@ SPFN_API_URL=http://localhost:8790
830
843
  * SPFN Codegen Configuration
831
844
  *
832
845
  * Configure code generators here. Generators run during \`spfn dev\` and \`spfn codegen run\`.
833
- *
834
- * Example: Custom generator
835
- * @example
836
- * const myGenerator = defineGenerator({
837
- * name: 'my-package:generator',
838
- * enabled: true,
839
- * // ... generator-specific options
840
- * });
841
- *
842
- * Example: File-based generator
843
- * @example
844
- * const customGen = defineGenerator({
845
- * path: './src/generators/my-generator.ts',
846
- * });
847
846
  */
848
847
 
849
848
  export default defineConfig({
850
849
  generators: [
851
- // Add your generators here
852
- // myGenerator,
850
+ // Route map generator - generates routeName \u2192 {method, path} mappings
851
+ // Used by RPC proxy to resolve routes without importing server code
852
+ defineGenerator({
853
+ name: '@spfn/core:route-map',
854
+ routerPath: './src/server/router.ts',
855
+ outputPath: './src/generated/route-map.ts',
856
+ }),
853
857
  ]
854
858
  });
855
859
  `;
856
860
  writeFileSync5(spfnrcPath, spfnrcContent);
857
861
  logger.success("Created .spfnrc.ts (codegen configuration)");
858
862
  }
863
+ updateGitignore(cwd);
864
+ updateTsconfig(cwd);
865
+ }
866
+ function generateEnvExamples(cwd) {
867
+ writeEnvExample(cwd, ".env.example", `# Shared defaults (committed)
868
+ # These values are shared across all environments.
869
+
870
+ # Environment
871
+ NODE_ENV=local
872
+
873
+ # Logging
874
+ SPFN_LOG_LEVEL=info
875
+
876
+ # Server
877
+ PORT=4000
878
+
879
+ # SPFN API Server URL (for API Route Proxy and SSR)
880
+ SPFN_API_URL=http://localhost:8790
881
+ NEXT_PUBLIC_SPFN_API_URL=http://localhost:8790
882
+ `);
883
+ writeEnvExample(cwd, ".env.local.example", `# Local overrides (gitignored)
884
+ # Developer-specific values that should NOT be committed.
885
+
886
+ # Database (matches docker-compose.yml)
887
+ DATABASE_URL=postgresql://spfn:spfn@localhost:5432/spfn_dev
888
+
889
+ # Cache - Redis/Valkey (optional)
890
+ CACHE_URL=redis://localhost:6379
891
+
892
+ # SPFN App URL (optional, for CORS and redirects)
893
+ # SPFN_APP_URL=http://localhost:3790
894
+ `);
895
+ writeEnvExample(cwd, ".env.server.example", `# Server-only defaults (committed)
896
+ # These values are only loaded by the SPFN server, not by Next.js.
897
+
898
+ # Database pool
899
+ DB_POOL_MAX=10
900
+ DB_POOL_IDLE_TIMEOUT=30
901
+
902
+ # Server timeouts
903
+ SERVER_TIMEOUT=120000
904
+ SHUTDOWN_TIMEOUT=30000
905
+ `);
906
+ writeEnvExample(cwd, ".env.server.local.example", `# Server secrets (gitignored)
907
+ # Server-only sensitive values. Never commit this file.
908
+
909
+ # Database write/read URLs (master-replica pattern, optional)
910
+ # DATABASE_WRITE_URL=postgresql://user:password@master:5432/dbname
911
+ # DATABASE_READ_URL=postgresql://user:password@replica:5432/dbname
912
+
913
+ # Cache password (optional)
914
+ # CACHE_PASSWORD=your-redis-password
915
+ `);
916
+ }
917
+ function writeEnvExample(cwd, filename, content) {
918
+ const filePath = join9(cwd, filename);
919
+ if (existsSync9(filePath)) {
920
+ return;
921
+ }
922
+ writeFileSync5(filePath, content);
923
+ logger.success(`Created ${filename}`);
924
+ }
925
+ function updateGitignore(cwd) {
859
926
  const gitignorePath = join9(cwd, ".gitignore");
860
- if (existsSync9(gitignorePath)) {
861
- try {
862
- const gitignoreContent = readFileSync2(gitignorePath, "utf-8");
863
- if (!gitignoreContent.includes(".spfn")) {
864
- const updatedContent = gitignoreContent.replace(
865
- /# production\n\/build/,
866
- "# production\n/build\n\n# spfn\n/.spfn/"
867
- );
868
- writeFileSync5(gitignorePath, updatedContent);
869
- logger.success("Updated .gitignore with .spfn directory");
870
- }
871
- } catch (error) {
872
- logger.warn("Could not update .gitignore (you can add .spfn manually)");
927
+ if (!existsSync9(gitignorePath)) {
928
+ return;
929
+ }
930
+ try {
931
+ const content = readFileSync2(gitignorePath, "utf-8");
932
+ let updated = content;
933
+ let changed = false;
934
+ if (!content.includes(".spfn")) {
935
+ updated = updated.replace(
936
+ /# production\n\/build/,
937
+ "# production\n/build\n\n# spfn\n/.spfn/"
938
+ );
939
+ changed = true;
873
940
  }
941
+ if (!content.includes(".env.local") && !content.includes(".env.*.local")) {
942
+ updated += `
943
+ # environment secrets (local overrides)
944
+ .env.local
945
+ .env.*.local
946
+ `;
947
+ changed = true;
948
+ }
949
+ if (changed) {
950
+ writeFileSync5(gitignorePath, updated);
951
+ logger.success("Updated .gitignore with .spfn directory and env patterns");
952
+ }
953
+ } catch (error) {
954
+ logger.warn("Could not update .gitignore (you can add patterns manually)");
874
955
  }
956
+ }
957
+ function updateTsconfig(cwd) {
875
958
  const tsconfigPath = join9(cwd, "tsconfig.json");
876
- if (existsSync9(tsconfigPath)) {
877
- try {
878
- const tsconfigContent = readFileSync2(tsconfigPath, "utf-8");
879
- const tsconfig = JSON.parse(tsconfigContent);
880
- if (!tsconfig.exclude) {
881
- tsconfig.exclude = [];
882
- }
883
- if (!tsconfig.exclude.includes("src/server")) {
884
- tsconfig.exclude.push("src/server");
885
- writeFileSync5(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
886
- logger.success("Updated tsconfig.json (excluded src/server for Vercel compatibility)");
887
- }
888
- } catch (error) {
889
- logger.warn('Could not update tsconfig.json (you can add "src/server" to exclude manually)');
959
+ if (!existsSync9(tsconfigPath)) {
960
+ return;
961
+ }
962
+ try {
963
+ const tsconfigContent = readFileSync2(tsconfigPath, "utf-8");
964
+ const tsconfig = JSON.parse(tsconfigContent);
965
+ if (!tsconfig.exclude) {
966
+ tsconfig.exclude = [];
967
+ }
968
+ if (!tsconfig.exclude.includes("src/server")) {
969
+ tsconfig.exclude.push("src/server");
970
+ writeFileSync5(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
971
+ logger.success("Updated tsconfig.json (excluded src/server for Vercel compatibility)");
890
972
  }
973
+ } catch (error) {
974
+ logger.warn('Could not update tsconfig.json (you can add "src/server" to exclude manually)');
891
975
  }
892
976
  }
893
977
  var writeFileSync5;
@@ -961,7 +1045,7 @@ __export(function_migrations_exports, {
961
1045
  import chalk11 from "chalk";
962
1046
  import { join as join15 } from "path";
963
1047
  import { env as env2 } from "@spfn/core/config";
964
- import { loadEnvFiles as loadEnvFiles2 } from "@spfn/core/server";
1048
+ import { loadEnv as loadEnv2 } from "@spfn/core/server";
965
1049
  import { existsSync as existsSync16, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
966
1050
  function discoverFunctionMigrations(cwd = process.cwd()) {
967
1051
  const nodeModulesPath = join15(cwd, "node_modules");
@@ -1009,7 +1093,7 @@ async function executeFunctionMigrations(functionMigrations) {
1009
1093
  const { drizzle } = await import("drizzle-orm/postgres-js");
1010
1094
  const { migrate } = await import("drizzle-orm/postgres-js/migrator");
1011
1095
  const postgres = await import("postgres");
1012
- loadEnvFiles2();
1096
+ loadEnv2();
1013
1097
  if (!env2.DATABASE_URL) {
1014
1098
  throw new Error("DATABASE_URL not found in environment");
1015
1099
  }
@@ -1019,7 +1103,10 @@ async function executeFunctionMigrations(functionMigrations) {
1019
1103
  for (const func of functionMigrations) {
1020
1104
  console.log(chalk11.blue(`
1021
1105
  \u{1F4E6} Running ${func.packageName} migrations...`));
1022
- await migrate(db, { migrationsFolder: func.migrationsDir });
1106
+ await migrate(db, {
1107
+ migrationsFolder: func.migrationsDir,
1108
+ migrationsTable: "__spfn_fn_migrations"
1109
+ });
1023
1110
  console.log(chalk11.green(` \u2713 ${func.packageName} migrations applied`));
1024
1111
  executedCount++;
1025
1112
  }
@@ -1179,10 +1266,30 @@ init_init();
1179
1266
  init_logger();
1180
1267
  init_package_manager();
1181
1268
  import { Command as Command4 } from "commander";
1182
- import { existsSync as existsSync11, readFileSync as readFileSync3, writeFileSync as writeFileSync6, mkdirSync } from "fs";
1269
+ import { existsSync as existsSync11, readFileSync as readFileSync3, writeFileSync as writeFileSync6, mkdirSync, unlinkSync, watch } from "fs";
1183
1270
  import { join as join11 } from "path";
1184
1271
  import { execa as execa4 } from "execa";
1185
1272
  import chokidar from "chokidar";
1273
+ function waitForReadyFile(filePath, timeoutMs = 3e4) {
1274
+ return new Promise((resolve2, reject) => {
1275
+ if (existsSync11(filePath)) {
1276
+ unlinkSync(filePath);
1277
+ }
1278
+ const timer = setTimeout(() => {
1279
+ watcher.close();
1280
+ reject(new Error(`Server did not become ready within ${timeoutMs / 1e3}s`));
1281
+ }, timeoutMs);
1282
+ const dir = join11(filePath, "..");
1283
+ const fileName = filePath.split("/").pop();
1284
+ const watcher = watch(dir, (event, name) => {
1285
+ if (name === fileName && existsSync11(filePath)) {
1286
+ watcher.close();
1287
+ clearTimeout(timer);
1288
+ resolve2(readFileSync3(filePath, "utf-8").trim());
1289
+ }
1290
+ });
1291
+ });
1292
+ }
1186
1293
  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) => {
1187
1294
  process.setMaxListeners(20);
1188
1295
  if (!process.env.NODE_ENV) {
@@ -1210,17 +1317,22 @@ var devCommand = new Command4("dev").description("Start SPFN development server
1210
1317
  if (options.host) configParts.push(`host: '${options.host}'`);
1211
1318
  if (options.routes) configParts.push(`routesPath: '${options.routes}'`);
1212
1319
  configParts.push("debug: true");
1320
+ const readyFile = join11(tempDir, "server-ready");
1213
1321
  writeFileSync6(serverEntry, `
1322
+ import { writeFileSync } from 'fs';
1323
+
1214
1324
  // Load environment variables FIRST (before any imports that depend on them)
1215
- // Use centralized environment loader for standard dotenv priority
1216
1325
  await import('@spfn/core/config');
1217
1326
 
1218
1327
  // Import and start server
1219
1328
  const { startServer } = await import('@spfn/core/server');
1220
1329
 
1221
- await startServer({
1330
+ const instance = await startServer({
1222
1331
  ${configParts.join(",\n ")}
1223
1332
  });
1333
+
1334
+ // Signal ready with actual port
1335
+ writeFileSync(${JSON.stringify(readyFile)}, String(instance.config.port));
1224
1336
  `);
1225
1337
  writeFileSync6(watcherEntry, `
1226
1338
  // Load environment variables
@@ -1277,7 +1389,9 @@ catch (error)
1277
1389
  const pm = detectPackageManager(cwd);
1278
1390
  if (options.serverOnly || !hasNext) {
1279
1391
  const watchMode2 = options.watch === true;
1280
- logger.info(`Starting SPFN Server on http://${options.host}:${options.port}${watchMode2 ? " (watch mode)" : ""}
1392
+ const host = options.host ?? process.env.HOST ?? "localhost";
1393
+ const port = options.port ?? process.env.PORT ?? "4000";
1394
+ logger.info(`Starting SPFN Server on http://${host}:${port}${watchMode2 ? " (watch mode)" : ""}
1281
1395
  `);
1282
1396
  let serverProcess2 = null;
1283
1397
  let watcherProcess2 = null;
@@ -1471,7 +1585,13 @@ catch (error)
1471
1585
  process.on("SIGTERM", cleanup);
1472
1586
  startWatcher();
1473
1587
  startServer();
1474
- await new Promise((resolve2) => setTimeout(resolve2, 2e3));
1588
+ try {
1589
+ const port = await waitForReadyFile(readyFile);
1590
+ logger.info(`[SPFN] Server ready on port ${port}, starting Next.js...
1591
+ `);
1592
+ } catch (error) {
1593
+ logger.warn(`[SPFN] Server readiness check timed out, starting Next.js anyway...`);
1594
+ }
1475
1595
  startNext();
1476
1596
  await new Promise((resolve2) => {
1477
1597
  const keepAlive = setInterval(() => {
@@ -1522,7 +1642,7 @@ async function buildProject(options) {
1522
1642
  const orchestrator = new CodegenOrchestrator({
1523
1643
  generators,
1524
1644
  cwd,
1525
- debug: false
1645
+ debug: true
1526
1646
  });
1527
1647
  await orchestrator.generateAll();
1528
1648
  spinner.succeed("API client generated");
@@ -1832,7 +1952,7 @@ async function runGenerators() {
1832
1952
  const orchestrator = new CodegenOrchestrator({
1833
1953
  generators,
1834
1954
  cwd,
1835
- debug: false
1955
+ debug: true
1836
1956
  });
1837
1957
  await orchestrator.generateAll();
1838
1958
  console.log("\n" + chalk7.green.bold("\u2713 Code generation completed"));
@@ -1991,14 +2111,14 @@ import { Command as Command9 } from "commander";
1991
2111
  import chalk10 from "chalk";
1992
2112
 
1993
2113
  // src/commands/db/utils/drizzle.ts
1994
- import { existsSync as existsSync15, writeFileSync as writeFileSync9, unlinkSync } from "fs";
2114
+ import { existsSync as existsSync15, writeFileSync as writeFileSync9, unlinkSync as unlinkSync2 } from "fs";
1995
2115
  import { spawn } from "child_process";
1996
2116
  import chalk9 from "chalk";
1997
2117
  import ora6 from "ora";
1998
2118
  import { env } from "@spfn/core/config";
1999
- import { loadEnvFiles } from "@spfn/core/server";
2119
+ import { loadEnv } from "@spfn/core/server";
2000
2120
  function validateDatabasePrerequisites() {
2001
- loadEnvFiles();
2121
+ loadEnv();
2002
2122
  if (!env.DATABASE_URL) {
2003
2123
  console.error(chalk9.red("\u274C DATABASE_URL not found in environment"));
2004
2124
  console.log(chalk9.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
@@ -2010,7 +2130,7 @@ async function runDrizzleCommand(command) {
2010
2130
  const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
2011
2131
  const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
2012
2132
  if (!hasUserConfig) {
2013
- loadEnvFiles();
2133
+ loadEnv();
2014
2134
  if (!env.DATABASE_URL) {
2015
2135
  console.error(chalk9.red("\u274C DATABASE_URL not found in environment"));
2016
2136
  console.log(chalk9.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
@@ -2020,7 +2140,10 @@ async function runDrizzleCommand(command) {
2020
2140
  const configContent = generateDrizzleConfigFile({
2021
2141
  cwd: process.cwd(),
2022
2142
  // Exclude package schemas to avoid .ts/.js mixing (packages use migrations instead)
2023
- disablePackageDiscovery: true
2143
+ disablePackageDiscovery: true,
2144
+ // Expand globs and auto-detect PostgreSQL schemas for push/generate compatibility
2145
+ expandGlobs: true,
2146
+ autoDetectSchemas: true
2024
2147
  });
2025
2148
  writeFileSync9(tempConfigPath, configContent);
2026
2149
  console.log(chalk9.dim("Using auto-generated Drizzle config\n"));
@@ -2031,11 +2154,12 @@ async function runDrizzleCommand(command) {
2031
2154
  const drizzleProcess = spawn("drizzle-kit", args, {
2032
2155
  stdio: "inherit",
2033
2156
  // Allow interactive input
2034
- shell: true
2157
+ shell: true,
2158
+ env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" }
2035
2159
  });
2036
2160
  const cleanup = () => {
2037
2161
  if (!hasUserConfig && existsSync15(tempConfigPath)) {
2038
- unlinkSync(tempConfigPath);
2162
+ unlinkSync2(tempConfigPath);
2039
2163
  }
2040
2164
  };
2041
2165
  drizzleProcess.on("close", (code) => {
@@ -2107,6 +2231,8 @@ async function dbPush() {
2107
2231
 
2108
2232
  // src/commands/db/migrate.ts
2109
2233
  import chalk16 from "chalk";
2234
+ import { join as join16 } from "path";
2235
+ import { existsSync as existsSync18 } from "fs";
2110
2236
 
2111
2237
  // src/commands/db/backup.ts
2112
2238
  import { promises as fs3 } from "fs";
@@ -2341,8 +2467,10 @@ async function listBackupFiles() {
2341
2467
 
2342
2468
  // src/commands/db/backup.ts
2343
2469
  import { env as env3 } from "@spfn/core/config";
2470
+ import { loadEnv as loadEnv3 } from "@spfn/core/server";
2344
2471
  async function dbBackup(options) {
2345
2472
  console.log(chalk15.blue("\u{1F4BE} Creating database backup...\n"));
2473
+ loadEnv3();
2346
2474
  const dbUrl = env3.DATABASE_URL;
2347
2475
  if (!dbUrl) {
2348
2476
  console.error(chalk15.red("\u274C DATABASE_URL not found in environment"));
@@ -2457,7 +2585,10 @@ async function dbBackup(options) {
2457
2585
  }
2458
2586
 
2459
2587
  // src/commands/db/migrate.ts
2460
- import "@spfn/core/config";
2588
+ import { env as env4 } from "@spfn/core/config";
2589
+ import { loadEnv as loadEnv4 } from "@spfn/core/server";
2590
+ var FUNCTION_MIGRATIONS_TABLE = "__spfn_fn_migrations";
2591
+ var PROJECT_MIGRATIONS_TABLE = "__drizzle_migrations";
2461
2592
  async function dbMigrate(options = {}) {
2462
2593
  try {
2463
2594
  validateDatabasePrerequisites();
@@ -2473,35 +2604,62 @@ async function dbMigrate(options = {}) {
2473
2604
  });
2474
2605
  console.log("");
2475
2606
  }
2476
- const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
2607
+ const { drizzle } = await import("drizzle-orm/postgres-js");
2608
+ const { migrate } = await import("drizzle-orm/postgres-js/migrator");
2609
+ const postgres = await import("postgres");
2610
+ loadEnv4();
2611
+ if (!env4.DATABASE_URL) {
2612
+ console.error(chalk16.red("\u274C DATABASE_URL not found in environment"));
2613
+ process.exit(1);
2614
+ }
2615
+ const { discoverFunctionMigrations: discoverFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
2477
2616
  const functions = discoverFunctionMigrations2(process.cwd());
2478
2617
  if (functions.length > 0) {
2479
- console.log(chalk16.blue("\u{1F4E6} Applying function package migrations:"));
2480
- functions.forEach((func) => {
2481
- console.log(chalk16.dim(` - ${func.packageName}`));
2482
- });
2618
+ const fnConn = postgres.default(env4.DATABASE_URL, { max: 1 });
2619
+ const fnDb = drizzle(fnConn);
2483
2620
  try {
2484
- await executeFunctionMigrations2(functions);
2621
+ console.log(chalk16.blue("\u{1F4E6} Applying function package migrations:"));
2622
+ functions.forEach((func) => {
2623
+ console.log(chalk16.dim(` - ${func.packageName}`));
2624
+ });
2625
+ for (const func of functions) {
2626
+ console.log(chalk16.blue(`
2627
+ \u{1F4E6} Running ${func.packageName} migrations...`));
2628
+ await migrate(fnDb, {
2629
+ migrationsFolder: func.migrationsDir,
2630
+ migrationsTable: FUNCTION_MIGRATIONS_TABLE
2631
+ });
2632
+ console.log(chalk16.green(` \u2713 ${func.packageName} migrations applied`));
2633
+ }
2485
2634
  console.log(chalk16.green("\u2705 Function migrations applied\n"));
2486
- } catch (error) {
2487
- console.error(chalk16.red("\n\u274C Failed to apply function migrations"));
2488
- console.error(chalk16.red(error instanceof Error ? error.message : "Unknown error"));
2489
- process.exit(1);
2635
+ } finally {
2636
+ await fnConn.end();
2490
2637
  }
2491
2638
  }
2492
- await runWithSpinner(
2493
- "Running project migrations...",
2494
- "migrate",
2495
- "Project migrations applied successfully",
2496
- "Failed to run project migrations"
2497
- );
2639
+ const projectMigrationsDir = join16(process.cwd(), "src/server/drizzle");
2640
+ if (existsSync18(projectMigrationsDir)) {
2641
+ const projConn = postgres.default(env4.DATABASE_URL, { max: 1 });
2642
+ const projDb = drizzle(projConn);
2643
+ try {
2644
+ console.log(chalk16.blue("\u{1F4E6} Running project migrations..."));
2645
+ await migrate(projDb, {
2646
+ migrationsFolder: projectMigrationsDir,
2647
+ migrationsTable: PROJECT_MIGRATIONS_TABLE
2648
+ });
2649
+ console.log(chalk16.green("\u2705 Project migrations applied successfully"));
2650
+ } finally {
2651
+ await projConn.end();
2652
+ }
2653
+ } else {
2654
+ console.log(chalk16.dim("No project migrations found (src/server/drizzle)"));
2655
+ }
2498
2656
  }
2499
2657
 
2500
2658
  // src/commands/db/studio.ts
2501
2659
  import chalk17 from "chalk";
2502
- import { existsSync as existsSync18, writeFileSync as writeFileSync10, unlinkSync as unlinkSync2 } from "fs";
2660
+ import { existsSync as existsSync19, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
2503
2661
  import { spawn as spawn3 } from "child_process";
2504
- import { env as env4 } from "@spfn/core/config";
2662
+ import { env as env5 } from "@spfn/core/config";
2505
2663
  import "@spfn/core/config";
2506
2664
  async function dbStudio(requestedPort) {
2507
2665
  console.log(chalk17.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
@@ -2518,12 +2676,12 @@ async function dbStudio(requestedPort) {
2518
2676
  console.error(chalk17.red(error instanceof Error ? error.message : "Failed to find available port"));
2519
2677
  process.exit(1);
2520
2678
  }
2521
- const hasUserConfig = existsSync18("./drizzle.config.ts");
2679
+ const hasUserConfig = existsSync19("./drizzle.config.ts");
2522
2680
  const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
2523
2681
  try {
2524
2682
  const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
2525
2683
  if (!hasUserConfig) {
2526
- if (!env4.DATABASE_URL) {
2684
+ if (!env5.DATABASE_URL) {
2527
2685
  console.error(chalk17.red("\u274C DATABASE_URL not found in environment"));
2528
2686
  console.log(chalk17.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2529
2687
  process.exit(1);
@@ -2540,11 +2698,12 @@ async function dbStudio(requestedPort) {
2540
2698
  }
2541
2699
  const studioProcess = spawn3("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
2542
2700
  stdio: "inherit",
2543
- shell: true
2701
+ shell: true,
2702
+ env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" }
2544
2703
  });
2545
2704
  const cleanup = () => {
2546
- if (!hasUserConfig && existsSync18(tempConfigPath)) {
2547
- unlinkSync2(tempConfigPath);
2705
+ if (!hasUserConfig && existsSync19(tempConfigPath)) {
2706
+ unlinkSync3(tempConfigPath);
2548
2707
  }
2549
2708
  };
2550
2709
  studioProcess.on("exit", (code) => {
@@ -2573,8 +2732,8 @@ async function dbStudio(requestedPort) {
2573
2732
  process.exit(0);
2574
2733
  });
2575
2734
  } catch (error) {
2576
- if (!hasUserConfig && existsSync18(tempConfigPath)) {
2577
- unlinkSync2(tempConfigPath);
2735
+ if (!hasUserConfig && existsSync19(tempConfigPath)) {
2736
+ unlinkSync3(tempConfigPath);
2578
2737
  }
2579
2738
  console.error(chalk17.red("\u274C Failed to start Drizzle Studio"));
2580
2739
  console.error(chalk17.red(error instanceof Error ? error.message : "Unknown error"));
@@ -2626,10 +2785,12 @@ import { spawn as spawn4 } from "child_process";
2626
2785
  import chalk20 from "chalk";
2627
2786
  import ora9 from "ora";
2628
2787
  import prompts4 from "prompts";
2629
- import { env as env5 } from "@spfn/core/config";
2788
+ import { env as env6 } from "@spfn/core/config";
2789
+ import { loadEnv as loadEnv5 } from "@spfn/core/server";
2630
2790
  async function dbRestore(backupFile, options = {}) {
2631
2791
  console.log(chalk20.blue("\u267B\uFE0F Restoring database from backup...\n"));
2632
- const dbUrl = env5.DATABASE_URL;
2792
+ loadEnv5();
2793
+ const dbUrl = env6.DATABASE_URL;
2633
2794
  if (!dbUrl) {
2634
2795
  console.error(chalk20.red("\u274C DATABASE_URL not found in environment"));
2635
2796
  console.log(chalk20.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
@@ -2678,29 +2839,29 @@ async function dbRestore(backupFile, options = {}) {
2678
2839
  if (metadata.backup.schemaOnly) {
2679
2840
  console.log(chalk20.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
2680
2841
  }
2681
- const warnings = [];
2842
+ const warnings2 = [];
2682
2843
  const [currentGitInfo, currentMigrationInfo] = await Promise.all([
2683
2844
  collectGitInfo(),
2684
2845
  collectMigrationInfo(dbUrl)
2685
2846
  ]);
2686
2847
  if (metadata.git && currentGitInfo) {
2687
2848
  if (metadata.git.commit !== currentGitInfo.commit) {
2688
- warnings.push(`Git commit mismatch: backup from ${metadata.git.commit.substring(0, 7)}, current is ${currentGitInfo.commit.substring(0, 7)}`);
2849
+ warnings2.push(`Git commit mismatch: backup from ${metadata.git.commit.substring(0, 7)}, current is ${currentGitInfo.commit.substring(0, 7)}`);
2689
2850
  }
2690
2851
  if (metadata.git.branch !== currentGitInfo.branch) {
2691
- warnings.push(`Git branch mismatch: backup from '${metadata.git.branch}', current is '${currentGitInfo.branch}'`);
2852
+ warnings2.push(`Git branch mismatch: backup from '${metadata.git.branch}', current is '${currentGitInfo.branch}'`);
2692
2853
  }
2693
2854
  }
2694
2855
  if (metadata.migrations && currentMigrationInfo) {
2695
2856
  if (metadata.migrations.hash !== currentMigrationInfo.hash) {
2696
- warnings.push(`Migration version mismatch: backup has ${metadata.migrations.count} migrations, current has ${currentMigrationInfo.count}`);
2697
- warnings.push(` Last migration in backup: ${metadata.migrations.hash}`);
2698
- warnings.push(` Current last migration: ${currentMigrationInfo.hash}`);
2857
+ warnings2.push(`Migration version mismatch: backup has ${metadata.migrations.count} migrations, current has ${currentMigrationInfo.count}`);
2858
+ warnings2.push(` Last migration in backup: ${metadata.migrations.hash}`);
2859
+ warnings2.push(` Current last migration: ${currentMigrationInfo.hash}`);
2699
2860
  }
2700
2861
  }
2701
- if (warnings.length > 0) {
2862
+ if (warnings2.length > 0) {
2702
2863
  console.log(chalk20.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
2703
- warnings.forEach((warning) => console.log(chalk20.yellow(` - ${warning}`)));
2864
+ warnings2.forEach((warning) => console.log(chalk20.yellow(` - ${warning}`)));
2704
2865
  console.log("");
2705
2866
  }
2706
2867
  }
@@ -2728,6 +2889,7 @@ async function dbRestore(backupFile, options = {}) {
2728
2889
  args.push("-p", dbInfo.port);
2729
2890
  args.push("-U", dbInfo.user);
2730
2891
  args.push("-d", dbInfo.database);
2892
+ args.push("--verbose");
2731
2893
  if (options.drop) {
2732
2894
  args.push("--clean");
2733
2895
  }
@@ -2750,8 +2912,10 @@ async function dbRestore(backupFile, options = {}) {
2750
2912
  args.push("-p", dbInfo.port);
2751
2913
  args.push("-U", dbInfo.user);
2752
2914
  args.push("-d", dbInfo.database);
2915
+ args.push("-v", "ON_ERROR_STOP=1");
2753
2916
  args.push("-f", file);
2754
2917
  }
2918
+ const verbose = options.verbose ?? false;
2755
2919
  const spinner = ora9("Restoring backup...").start();
2756
2920
  const restoreProcess = spawn4(command, args, {
2757
2921
  stdio: ["ignore", "pipe", "pipe"],
@@ -2760,19 +2924,81 @@ async function dbRestore(backupFile, options = {}) {
2760
2924
  PGPASSWORD: dbInfo.password
2761
2925
  }
2762
2926
  });
2763
- let errorOutput = "";
2927
+ const warnings = [];
2928
+ const errors = [];
2929
+ let objectCount = 0;
2930
+ let lastObject = "";
2764
2931
  restoreProcess.stderr?.on("data", (data) => {
2765
- errorOutput += data.toString();
2932
+ const lines = data.toString().split("\n").filter((l) => l.trim());
2933
+ for (const line of lines) {
2934
+ if (/^pg_restore:.*warning:/i.test(line) || /^WARNING:/i.test(line)) {
2935
+ warnings.push(line.trim());
2936
+ } else if (/^pg_restore:.*error:/i.test(line) || /^ERROR:/i.test(line) || /^psql:.*ERROR/i.test(line)) {
2937
+ errors.push(line.trim());
2938
+ }
2939
+ const objectMatch = line.match(/processing item (\d+)\/(\d+)/);
2940
+ if (objectMatch) {
2941
+ objectCount = Number(objectMatch[2]);
2942
+ const current = Number(objectMatch[1]);
2943
+ const desc = line.replace(/^pg_restore:\s*/, "").trim();
2944
+ lastObject = desc;
2945
+ spinner.text = `Restoring backup... [${current}/${objectCount}] ${desc}`;
2946
+ } else if (isCustomFormat) {
2947
+ const desc = line.replace(/^pg_restore:\s*/, "").trim();
2948
+ if (desc && !/warning:|error:/i.test(desc)) {
2949
+ lastObject = desc;
2950
+ spinner.text = `Restoring backup... ${desc}`;
2951
+ }
2952
+ }
2953
+ if (verbose) {
2954
+ spinner.stop();
2955
+ console.log(chalk20.dim(` ${line.trim()}`));
2956
+ spinner.start();
2957
+ }
2958
+ }
2959
+ });
2960
+ restoreProcess.stdout?.on("data", (data) => {
2961
+ if (verbose) {
2962
+ spinner.stop();
2963
+ console.log(chalk20.dim(` ${data.toString().trim()}`));
2964
+ spinner.start();
2965
+ }
2766
2966
  });
2767
2967
  await new Promise((resolve2, reject) => {
2768
2968
  restoreProcess.on("close", (code) => {
2769
2969
  if (code === 0) {
2770
- spinner.succeed("Restore completed");
2970
+ const summary = objectCount > 0 ? ` (${objectCount} objects)` : "";
2971
+ spinner.succeed(`Restore completed${summary}`);
2972
+ if (warnings.length > 0) {
2973
+ console.log(chalk20.yellow(`
2974
+ \u26A0\uFE0F Warnings during restore (${warnings.length}):
2975
+ `));
2976
+ for (const w of warnings) {
2977
+ console.log(chalk20.yellow(` - ${w}`));
2978
+ }
2979
+ }
2771
2980
  console.log(chalk20.green("\n\u2705 Database restored successfully"));
2772
2981
  resolve2();
2773
2982
  } else {
2774
2983
  spinner.fail("Restore failed");
2775
- reject(new Error(errorOutput || "Restore failed"));
2984
+ if (errors.length > 0) {
2985
+ console.error(chalk20.red(`
2986
+ \u274C Errors (${errors.length}):
2987
+ `));
2988
+ for (const e of errors) {
2989
+ console.error(chalk20.red(` - ${e}`));
2990
+ }
2991
+ }
2992
+ if (warnings.length > 0) {
2993
+ console.log(chalk20.yellow(`
2994
+ \u26A0\uFE0F Warnings (${warnings.length}):
2995
+ `));
2996
+ for (const w of warnings) {
2997
+ console.log(chalk20.yellow(` - ${w}`));
2998
+ }
2999
+ }
3000
+ const fallback = errors.length === 0 && warnings.length === 0 ? "Restore failed with no output" : "";
3001
+ reject(new Error(fallback));
2776
3002
  }
2777
3003
  });
2778
3004
  restoreProcess.on("error", (error) => {
@@ -2780,8 +3006,11 @@ async function dbRestore(backupFile, options = {}) {
2780
3006
  reject(error);
2781
3007
  });
2782
3008
  }).catch((error) => {
2783
- console.error(chalk20.red("\n\u274C Failed to restore database"));
2784
- console.error(chalk20.red(error instanceof Error ? error.message : "Unknown error"));
3009
+ const msg = error instanceof Error ? error.message : "Unknown error";
3010
+ if (msg) {
3011
+ console.error(chalk20.red(`
3012
+ \u274C ${msg}`));
3013
+ }
2785
3014
  process.exit(1);
2786
3015
  });
2787
3016
  }
@@ -2881,14 +3110,14 @@ dbCommand.command("studio").description("Open Drizzle Studio (database GUI)").op
2881
3110
  dbCommand.command("drop").description("Drop all database tables (\u26A0\uFE0F dangerous!)").action(dbDrop);
2882
3111
  dbCommand.command("check").description("Check database connection").action(dbCheck);
2883
3112
  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));
2884
- 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));
3113
+ 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));
2885
3114
  dbCommand.command("backup:list").description("List all database backups").action(dbBackupList);
2886
3115
  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));
2887
3116
 
2888
3117
  // src/commands/add.ts
2889
3118
  import { Command as Command10 } from "commander";
2890
- import { existsSync as existsSync19, readFileSync as readFileSync6 } from "fs";
2891
- import { join as join16 } from "path";
3119
+ import { existsSync as existsSync20, readFileSync as readFileSync6 } from "fs";
3120
+ import { join as join17 } from "path";
2892
3121
  import { exec as exec2 } from "child_process";
2893
3122
  import { promisify as promisify2 } from "util";
2894
3123
  import chalk23 from "chalk";
@@ -2906,9 +3135,9 @@ async function addPackage(packageName) {
2906
3135
  \u{1F4E6} Setting up ${packageName}...
2907
3136
  `));
2908
3137
  try {
2909
- const pkgPath = join16(process.cwd(), "node_modules", ...packageName.split("/"));
2910
- const pkgJsonPath = join16(pkgPath, "package.json");
2911
- if (!existsSync19(pkgJsonPath)) {
3138
+ const pkgPath = join17(process.cwd(), "node_modules", ...packageName.split("/"));
3139
+ const pkgJsonPath = join17(pkgPath, "package.json");
3140
+ if (!existsSync20(pkgJsonPath)) {
2912
3141
  const installSpinner = ora11("Installing package...").start();
2913
3142
  try {
2914
3143
  await execAsync2(`pnpm add ${packageName}`);
@@ -2920,7 +3149,7 @@ async function addPackage(packageName) {
2920
3149
  } else {
2921
3150
  console.log(chalk23.gray("\u2713 Package already installed (using local version)\n"));
2922
3151
  }
2923
- if (!existsSync19(pkgJsonPath)) {
3152
+ if (!existsSync20(pkgJsonPath)) {
2924
3153
  throw new Error(`Package ${packageName} not found after installation`);
2925
3154
  }
2926
3155
  const pkgJson = JSON.parse(readFileSync6(pkgJsonPath, "utf-8"));
@@ -2928,8 +3157,8 @@ async function addPackage(packageName) {
2928
3157
  console.log(chalk23.blue(`
2929
3158
  \u{1F5C4}\uFE0F Setting up database for ${packageName}...
2930
3159
  `));
2931
- const { env: env6 } = await import("@spfn/core/config");
2932
- if (!env6.DATABASE_URL) {
3160
+ const { env: env7 } = await import("@spfn/core/config");
3161
+ if (!env7.DATABASE_URL) {
2933
3162
  console.log(chalk23.yellow("\u26A0\uFE0F DATABASE_URL not found"));
2934
3163
  console.log(chalk23.gray("Skipping database setup. Run migrations manually when ready:\n"));
2935
3164
  console.log(chalk23.gray(` pnpm spfn db push
@@ -2976,8 +3205,8 @@ var addCommand = new Command10("add").description("Install and set up SPFN ecosy
2976
3205
  init_logger();
2977
3206
  import { Command as Command11 } from "commander";
2978
3207
  import ora12 from "ora";
2979
- import { join as join25 } from "path";
2980
- import { existsSync as existsSync22 } from "fs";
3208
+ import { join as join26 } from "path";
3209
+ import { existsSync as existsSync23 } from "fs";
2981
3210
  import chalk25 from "chalk";
2982
3211
 
2983
3212
  // src/commands/generate/prompts.ts
@@ -3070,11 +3299,11 @@ async function confirmConfiguration(config) {
3070
3299
  }
3071
3300
 
3072
3301
  // src/commands/generate/generators/structure.ts
3073
- import { join as join24 } from "path";
3302
+ import { join as join25 } from "path";
3074
3303
  import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync17 } from "fs";
3075
3304
 
3076
3305
  // src/commands/generate/generators/config.ts
3077
- import { join as join18 } from "path";
3306
+ import { join as join19 } from "path";
3078
3307
  import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync3 } from "fs";
3079
3308
 
3080
3309
  // src/commands/generate/string-utils.ts
@@ -3104,29 +3333,29 @@ function toSafeSchemaName(str) {
3104
3333
  }
3105
3334
 
3106
3335
  // src/commands/generate/template-loader.ts
3107
- import { readFileSync as readFileSync7, existsSync as existsSync20 } from "fs";
3108
- import { join as join17, dirname as dirname2 } from "path";
3336
+ import { readFileSync as readFileSync7, existsSync as existsSync21 } from "fs";
3337
+ import { join as join18, dirname as dirname2 } from "path";
3109
3338
  import { fileURLToPath as fileURLToPath2 } from "url";
3110
3339
  function findTemplatesPath2() {
3111
3340
  const __filename = fileURLToPath2(import.meta.url);
3112
3341
  const __dirname2 = dirname2(__filename);
3113
- const distPath = join17(__dirname2, "commands", "generate", "templates");
3114
- if (existsSync20(distPath)) {
3342
+ const distPath = join18(__dirname2, "commands", "generate", "templates");
3343
+ if (existsSync21(distPath)) {
3115
3344
  return distPath;
3116
3345
  }
3117
- const sameDirPath = join17(__dirname2, "templates");
3118
- if (existsSync20(sameDirPath)) {
3346
+ const sameDirPath = join18(__dirname2, "templates");
3347
+ if (existsSync21(sameDirPath)) {
3119
3348
  return sameDirPath;
3120
3349
  }
3121
- const srcPath = join17(__dirname2, "..", "..", "src", "commands", "generate", "templates");
3122
- if (existsSync20(srcPath)) {
3350
+ const srcPath = join18(__dirname2, "..", "..", "src", "commands", "generate", "templates");
3351
+ if (existsSync21(srcPath)) {
3123
3352
  return srcPath;
3124
3353
  }
3125
3354
  throw new Error(`Templates directory not found. Tried: ${distPath}, ${sameDirPath}, ${srcPath}`);
3126
3355
  }
3127
3356
  function loadTemplate(templateName, variables) {
3128
3357
  const templatesDir = findTemplatesPath2();
3129
- const templatePath = join17(templatesDir, `${templateName}.template`);
3358
+ const templatePath = join18(templatesDir, `${templateName}.template`);
3130
3359
  let content = readFileSync7(templatePath, "utf-8");
3131
3360
  for (const [key, value] of Object.entries(variables)) {
3132
3361
  const regex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
@@ -3230,7 +3459,7 @@ function generatePackageJson(fnDir, scope, fnName, description) {
3230
3459
  }
3231
3460
  };
3232
3461
  writeFileSync11(
3233
- join18(fnDir, "package.json"),
3462
+ join19(fnDir, "package.json"),
3234
3463
  JSON.stringify(content, null, 4) + "\n"
3235
3464
  );
3236
3465
  }
@@ -3264,7 +3493,7 @@ function generateTsConfig(fnDir) {
3264
3493
  exclude: ["node_modules", "dist", "**/*.test.ts", "**/__tests__/**"]
3265
3494
  };
3266
3495
  writeFileSync11(
3267
- join18(fnDir, "tsconfig.json"),
3496
+ join19(fnDir, "tsconfig.json"),
3268
3497
  JSON.stringify(content, null, 4) + "\n"
3269
3498
  );
3270
3499
  }
@@ -3340,7 +3569,7 @@ export default defineConfig({
3340
3569
  ],
3341
3570
  });
3342
3571
  `;
3343
- writeFileSync11(join18(fnDir, "tsup.config.ts"), content);
3572
+ writeFileSync11(join19(fnDir, "tsup.config.ts"), content);
3344
3573
  }
3345
3574
  function generateDrizzleConfig(fnDir, scope, fnName) {
3346
3575
  const schemaName = `spfn_${toSnakeCase(fnName)}`;
@@ -3360,7 +3589,7 @@ export default defineConfig({
3360
3589
  schemaFilter: ['${schemaName}'], // Only generate for ${fnName} schema
3361
3590
  });
3362
3591
  `;
3363
- writeFileSync11(join18(fnDir, "drizzle.config.ts"), content);
3592
+ writeFileSync11(join19(fnDir, "drizzle.config.ts"), content);
3364
3593
  }
3365
3594
  function generateExampleGenerator(fnDir, scope, fnName) {
3366
3595
  const pascalName = toPascalCase(fnName);
@@ -3429,10 +3658,10 @@ export const moduleName = '${fnName}';
3429
3658
  };
3430
3659
  }
3431
3660
  `;
3432
- const generatorsDir = join18(fnDir, "src/server/generators");
3661
+ const generatorsDir = join19(fnDir, "src/server/generators");
3433
3662
  mkdirSync3(generatorsDir, { recursive: true });
3434
3663
  writeFileSync11(
3435
- join18(generatorsDir, "example-generator.ts"),
3664
+ join19(generatorsDir, "example-generator.ts"),
3436
3665
  content
3437
3666
  );
3438
3667
  const indexContent = `/**
@@ -3447,7 +3676,7 @@ export const moduleName = '${fnName}';
3447
3676
  export { create${pascalName}ExampleGenerator } from './example-generator.js';
3448
3677
  `;
3449
3678
  writeFileSync11(
3450
- join18(generatorsDir, "index.ts"),
3679
+ join19(generatorsDir, "index.ts"),
3451
3680
  indexContent
3452
3681
  );
3453
3682
  }
@@ -3936,15 +4165,15 @@ Contributions are welcome! Please follow the development workflow above.
3936
4165
 
3937
4166
  MIT
3938
4167
  `;
3939
- writeFileSync11(join18(fnDir, "README.md"), content);
4168
+ writeFileSync11(join19(fnDir, "README.md"), content);
3940
4169
  }
3941
4170
 
3942
4171
  // src/commands/generate/generators/entity.ts
3943
- import { join as join19 } from "path";
3944
- import { writeFileSync as writeFileSync12, existsSync as existsSync21 } from "fs";
4172
+ import { join as join20 } from "path";
4173
+ import { writeFileSync as writeFileSync12, existsSync as existsSync22 } from "fs";
3945
4174
  function generateSchema(fnDir, scope, fnName) {
3946
- const schemaFilePath = join19(fnDir, "src/server/entities/schema.ts");
3947
- if (existsSync21(schemaFilePath)) {
4175
+ const schemaFilePath = join20(fnDir, "src/server/entities/schema.ts");
4176
+ if (existsSync22(schemaFilePath)) {
3948
4177
  return;
3949
4178
  }
3950
4179
  const packageName = `${scope}/${fnName}`;
@@ -3975,7 +4204,7 @@ function generateEntity(fnDir, scope, fnName, entityName) {
3975
4204
  SCHEMA_FILE_NAME: schemaFileName
3976
4205
  });
3977
4206
  writeFileSync12(
3978
- join19(fnDir, `src/server/entities/${toKebabCase(entityName)}.ts`),
4207
+ join20(fnDir, `src/server/entities/${toKebabCase(entityName)}.ts`),
3979
4208
  content
3980
4209
  );
3981
4210
  }
@@ -3983,11 +4212,11 @@ function generateEntitiesIndex(fnDir, entities) {
3983
4212
  const schemaExport = `export * from './schema';`;
3984
4213
  const entityExports = entities.map((entity) => `export * from './${toKebabCase(entity)}';`).join("\n");
3985
4214
  const content = [schemaExport, entityExports].filter(Boolean).join("\n");
3986
- writeFileSync12(join19(fnDir, "src/server/entities/index.ts"), content + "\n");
4215
+ writeFileSync12(join20(fnDir, "src/server/entities/index.ts"), content + "\n");
3987
4216
  }
3988
4217
 
3989
4218
  // src/commands/generate/generators/repository.ts
3990
- import { join as join20 } from "path";
4219
+ import { join as join21 } from "path";
3991
4220
  import { writeFileSync as writeFileSync13 } from "fs";
3992
4221
  function generateRepository(fnDir, entityName) {
3993
4222
  const pascalName = toPascalCase(entityName);
@@ -3998,17 +4227,17 @@ function generateRepository(fnDir, entityName) {
3998
4227
  REPO_NAME: repoName
3999
4228
  });
4000
4229
  writeFileSync13(
4001
- join20(fnDir, `src/server/repositories/${toKebabCase(entityName)}.repository.ts`),
4230
+ join21(fnDir, `src/server/repositories/${toKebabCase(entityName)}.repository.ts`),
4002
4231
  content
4003
4232
  );
4004
4233
  }
4005
4234
  function generateRepositoriesIndex(fnDir, entities) {
4006
4235
  const exports = entities.map((entity) => `export * from './${toKebabCase(entity)}.repository';`).join("\n");
4007
- writeFileSync13(join20(fnDir, "src/server/repositories/index.ts"), exports + "\n");
4236
+ writeFileSync13(join21(fnDir, "src/server/repositories/index.ts"), exports + "\n");
4008
4237
  }
4009
4238
 
4010
4239
  // src/commands/generate/generators/route.ts
4011
- import { join as join21 } from "path";
4240
+ import { join as join22 } from "path";
4012
4241
  import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync14 } from "fs";
4013
4242
  function generateRoute(fnDir, entityName) {
4014
4243
  const pascalName = toPascalCase(entityName);
@@ -4020,13 +4249,13 @@ function generateRoute(fnDir, entityName) {
4020
4249
  REPO_NAME: repoName,
4021
4250
  KEBAB_NAME: kebabName
4022
4251
  });
4023
- const routeDir = join21(fnDir, `src/server/routes/${kebabName}`);
4252
+ const routeDir = join22(fnDir, `src/server/routes/${kebabName}`);
4024
4253
  mkdirSync4(routeDir, { recursive: true });
4025
- writeFileSync14(join21(routeDir, "index.ts"), content);
4254
+ writeFileSync14(join22(routeDir, "index.ts"), content);
4026
4255
  }
4027
4256
 
4028
4257
  // src/commands/generate/generators/contract.ts
4029
- import { join as join22 } from "path";
4258
+ import { join as join23 } from "path";
4030
4259
  import { writeFileSync as writeFileSync15 } from "fs";
4031
4260
  function generateContract(fnDir, entityName) {
4032
4261
  const pascalName = toPascalCase(entityName);
@@ -4035,13 +4264,13 @@ function generateContract(fnDir, entityName) {
4035
4264
  ENTITY_NAME: entityName
4036
4265
  });
4037
4266
  writeFileSync15(
4038
- join22(fnDir, `src/lib/contracts/${toKebabCase(entityName)}.ts`),
4267
+ join23(fnDir, `src/lib/contracts/${toKebabCase(entityName)}.ts`),
4039
4268
  content
4040
4269
  );
4041
4270
  }
4042
4271
 
4043
4272
  // src/commands/generate/generators/index-files.ts
4044
- import { join as join23 } from "path";
4273
+ import { join as join24 } from "path";
4045
4274
  import { writeFileSync as writeFileSync16 } from "fs";
4046
4275
  function generateMainIndex(fnDir, fnName) {
4047
4276
  const content = `/**
@@ -4068,7 +4297,7 @@ export * from '@/lib/types/index';
4068
4297
 
4069
4298
  export * from '@/server/entities/index';
4070
4299
  `;
4071
- writeFileSync16(join23(fnDir, "src/index.ts"), content);
4300
+ writeFileSync16(join24(fnDir, "src/index.ts"), content);
4072
4301
  }
4073
4302
  function generateServerIndex(fnDir) {
4074
4303
  const content = `/**
@@ -4103,7 +4332,7 @@ export * from '@/server/repositories/index';
4103
4332
 
4104
4333
  // TODO: Export helpers here
4105
4334
  `;
4106
- writeFileSync16(join23(fnDir, "src/server.ts"), content);
4335
+ writeFileSync16(join24(fnDir, "src/server.ts"), content);
4107
4336
  }
4108
4337
  function generateClientIndex(fnDir) {
4109
4338
  const content = `/**
@@ -4136,7 +4365,7 @@ export * from './client/store';
4136
4365
 
4137
4366
  export * from './client/components';
4138
4367
  `;
4139
- writeFileSync16(join23(fnDir, "src/client.ts"), content);
4368
+ writeFileSync16(join24(fnDir, "src/client.ts"), content);
4140
4369
  }
4141
4370
  function generateTypesFile(fnDir, fnName) {
4142
4371
  const content = `/**
@@ -4148,7 +4377,7 @@ function generateTypesFile(fnDir, fnName) {
4148
4377
 
4149
4378
  export * from '@/lib/types/index';
4150
4379
  `;
4151
- writeFileSync16(join23(fnDir, "src/types.ts"), content);
4380
+ writeFileSync16(join24(fnDir, "src/types.ts"), content);
4152
4381
  }
4153
4382
 
4154
4383
  // src/commands/generate/generators/structure.ts
@@ -4170,7 +4399,7 @@ async function generateFunctionStructure(options) {
4170
4399
  "src/client/store",
4171
4400
  "src/client/components"
4172
4401
  ];
4173
- dirs.forEach((dir) => mkdirSync5(join24(fnDir, dir), { recursive: true }));
4402
+ dirs.forEach((dir) => mkdirSync5(join25(fnDir, dir), { recursive: true }));
4174
4403
  generatePackageJson(fnDir, scope, fnName, description);
4175
4404
  generateTsConfig(fnDir);
4176
4405
  generateTsupConfig(fnDir);
@@ -4190,15 +4419,15 @@ async function generateFunctionStructure(options) {
4190
4419
  generateEntitiesIndex(fnDir, entities);
4191
4420
  generateRepositoriesIndex(fnDir, entities);
4192
4421
  } else {
4193
- writeFileSync17(join24(fnDir, "src/server/entities/index.ts"), "// Export your entities here\nexport {}\n");
4194
- writeFileSync17(join24(fnDir, "src/server/repositories/index.ts"), "// Export your repositories here\nexport {}\n");
4195
- }
4196
- writeFileSync17(join24(fnDir, "src/client/hooks/index.ts"), "/**\n * Client Hooks\n */\n\n// TODO: Add hooks (e.g., useAuth, useData, etc.)\nexport {}\n");
4197
- writeFileSync17(join24(fnDir, "src/client/store/index.ts"), "/**\n * Client Store\n */\n\n// TODO: Add Zustand store if needed\nexport {}\n");
4198
- writeFileSync17(join24(fnDir, "src/client/components/index.ts"), "/**\n * Client Components\n */\n\n// TODO: Add React components\nexport {}\n");
4199
- writeFileSync17(join24(fnDir, "src/client/index.ts"), "/**\n * Client Module Entry\n */\n\nexport * from './hooks';\nexport * from './store';\nexport * from './components';\n");
4200
- writeFileSync17(join24(fnDir, "src/lib/types/index.ts"), "/**\n * Shared Type Definitions\n */\n\n// Add your shared types here\nexport {}\n");
4201
- writeFileSync17(join24(fnDir, "src/lib/contracts/index.ts"), "/**\n * API Contracts\n */\n\n// Export your contracts here\nexport {}\n");
4422
+ writeFileSync17(join25(fnDir, "src/server/entities/index.ts"), "// Export your entities here\nexport {}\n");
4423
+ writeFileSync17(join25(fnDir, "src/server/repositories/index.ts"), "// Export your repositories here\nexport {}\n");
4424
+ }
4425
+ writeFileSync17(join25(fnDir, "src/client/hooks/index.ts"), "/**\n * Client Hooks\n */\n\n// TODO: Add hooks (e.g., useAuth, useData, etc.)\nexport {}\n");
4426
+ writeFileSync17(join25(fnDir, "src/client/store/index.ts"), "/**\n * Client Store\n */\n\n// TODO: Add Zustand store if needed\nexport {}\n");
4427
+ writeFileSync17(join25(fnDir, "src/client/components/index.ts"), "/**\n * Client Components\n */\n\n// TODO: Add React components\nexport {}\n");
4428
+ writeFileSync17(join25(fnDir, "src/client/index.ts"), "/**\n * Client Module Entry\n */\n\nexport * from './hooks';\nexport * from './store';\nexport * from './components';\n");
4429
+ writeFileSync17(join25(fnDir, "src/lib/types/index.ts"), "/**\n * Shared Type Definitions\n */\n\n// Add your shared types here\nexport {}\n");
4430
+ writeFileSync17(join25(fnDir, "src/lib/contracts/index.ts"), "/**\n * API Contracts\n */\n\n// Export your contracts here\nexport {}\n");
4202
4431
  generateMainIndex(fnDir, fnName);
4203
4432
  generateServerIndex(fnDir);
4204
4433
  generateClientIndex(fnDir);
@@ -4222,8 +4451,8 @@ async function generateFunction(name, options) {
4222
4451
  logger.error("Function name is required");
4223
4452
  process.exit(1);
4224
4453
  }
4225
- const fnDir = join25(cwd, fnName);
4226
- if (existsSync22(fnDir)) {
4454
+ const fnDir = join26(cwd, fnName);
4455
+ if (existsSync23(fnDir)) {
4227
4456
  logger.error(`Directory ${fnName} already exists at ${fnDir}`);
4228
4457
  process.exit(1);
4229
4458
  }
@@ -4288,13 +4517,29 @@ generateCommand.command("fn").description("Generate a new SPFN function module")
4288
4517
  // src/commands/env.ts
4289
4518
  import { Command as Command12 } from "commander";
4290
4519
  import chalk26 from "chalk";
4291
- import { existsSync as existsSync23, readFileSync as readFileSync8, writeFileSync as writeFileSync18 } from "fs";
4520
+ import { existsSync as existsSync24, readFileSync as readFileSync8, writeFileSync as writeFileSync18 } from "fs";
4292
4521
  import { resolve } from "path";
4293
4522
  import { parse } from "dotenv";
4294
- var ENV_FILES = {
4523
+ var VALID_ENVS = ["local", "development", "staging", "production", "test"];
4524
+ var BASE_ENV_FILES = {
4295
4525
  nextjs: [".env", ".env.local"],
4296
4526
  server: [".env.server", ".env.server.local"]
4297
4527
  };
4528
+ function getEnvFilesForEnvironment(nodeEnv) {
4529
+ const files = [".env"];
4530
+ if (nodeEnv) {
4531
+ files.push(`.env.${nodeEnv}`);
4532
+ }
4533
+ if (nodeEnv !== "test") {
4534
+ files.push(".env.local");
4535
+ }
4536
+ if (nodeEnv) {
4537
+ files.push(`.env.${nodeEnv}.local`);
4538
+ }
4539
+ files.push(".env.server");
4540
+ files.push(".env.server.local");
4541
+ return files;
4542
+ }
4298
4543
  function getTargetFile(schema) {
4299
4544
  const isNextjs = schema.nextjs ?? schema.key?.startsWith("NEXT_PUBLIC_");
4300
4545
  if (isNextjs) {
@@ -4327,8 +4572,7 @@ function formatType(type) {
4327
4572
  enum: chalk26.magenta,
4328
4573
  json: chalk26.red
4329
4574
  };
4330
- const colorFn = typeColors[type] || chalk26.white;
4331
- return colorFn(type);
4575
+ return (typeColors[type] || chalk26.white)(type);
4332
4576
  }
4333
4577
  function formatDefault(value, type) {
4334
4578
  if (value === void 0) {
@@ -4489,8 +4733,19 @@ var envCommand = new Command12("env").description("Manage environment variables"
4489
4733
  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);
4490
4734
  envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(showEnvStats);
4491
4735
  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);
4736
+ function validateEnvOption(envValue) {
4737
+ if (!VALID_ENVS.includes(envValue)) {
4738
+ console.error(chalk26.red(`
4739
+ \u274C Invalid environment: "${envValue}"`));
4740
+ console.log(chalk26.dim(` Valid values: ${VALID_ENVS.join(", ")}
4741
+ `));
4742
+ process.exit(1);
4743
+ }
4744
+ return envValue;
4745
+ }
4492
4746
  async function initEnvFiles(options) {
4493
4747
  const packageName = options.package || "@spfn/core";
4748
+ const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
4494
4749
  const cwd = process.cwd();
4495
4750
  try {
4496
4751
  const envSchema = await loadEnvSchema(packageName);
@@ -4502,24 +4757,41 @@ async function initEnvFiles(options) {
4502
4757
  acc[exampleFile].push([key, schema]);
4503
4758
  return acc;
4504
4759
  }, {});
4505
- console.log(chalk26.blue.bold(`
4760
+ if (targetEnv) {
4761
+ console.log(chalk26.blue.bold(`
4762
+ \u{1F680} Generating .env template files for ${chalk26.cyan(targetEnv)} environment
4763
+ `));
4764
+ const envSpecificFiles = {};
4765
+ const committedVars = allVars.filter(([_, schema]) => !schema.sensitive);
4766
+ if (committedVars.length > 0) {
4767
+ envSpecificFiles[`.env.${targetEnv}.example`] = committedVars;
4768
+ }
4769
+ const sensitiveVars = allVars.filter(([_, schema]) => schema.sensitive);
4770
+ if (sensitiveVars.length > 0) {
4771
+ envSpecificFiles[`.env.${targetEnv}.local.example`] = sensitiveVars;
4772
+ }
4773
+ const allGrouped = { ...grouped, ...envSpecificFiles };
4774
+ for (const [file, vars] of Object.entries(allGrouped)) {
4775
+ writeEnvTemplate(cwd, file, vars, options.force ?? false);
4776
+ }
4777
+ } else {
4778
+ console.log(chalk26.blue.bold(`
4506
4779
  \u{1F680} Generating .env template files
4507
4780
  `));
4508
- for (const [file, vars] of Object.entries(grouped)) {
4509
- const filePath = resolve(cwd, file);
4510
- if (existsSync23(filePath) && !options.force) {
4511
- console.log(chalk26.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
4512
- continue;
4781
+ for (const [file, vars] of Object.entries(grouped)) {
4782
+ writeEnvTemplate(cwd, file, vars, options.force ?? false);
4513
4783
  }
4514
- const content = generateEnvFileContent(vars);
4515
- writeFileSync18(filePath, content, "utf-8");
4516
- console.log(chalk26.green(` \u2705 ${file} (${vars.length} variables)`));
4517
4784
  }
4518
4785
  console.log(chalk26.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
4519
4786
  console.log(chalk26.dim(" cp .env.example .env"));
4520
4787
  console.log(chalk26.dim(" cp .env.local.example .env.local"));
4521
4788
  console.log(chalk26.dim(" cp .env.server.example .env.server"));
4522
- console.log(chalk26.dim(" cp .env.server.local.example .env.server.local\n"));
4789
+ console.log(chalk26.dim(" cp .env.server.local.example .env.server.local"));
4790
+ if (targetEnv) {
4791
+ console.log(chalk26.dim(` cp .env.${targetEnv}.example .env.${targetEnv}`));
4792
+ console.log(chalk26.dim(` cp .env.${targetEnv}.local.example .env.${targetEnv}.local`));
4793
+ }
4794
+ console.log("");
4523
4795
  } catch (error) {
4524
4796
  console.error(chalk26.red(`
4525
4797
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
@@ -4527,6 +4799,15 @@ async function initEnvFiles(options) {
4527
4799
  process.exit(1);
4528
4800
  }
4529
4801
  }
4802
+ function writeEnvTemplate(cwd, file, vars, force) {
4803
+ const filePath = resolve(cwd, file);
4804
+ if (existsSync24(filePath) && !force) {
4805
+ console.log(chalk26.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
4806
+ return;
4807
+ }
4808
+ writeFileSync18(filePath, generateEnvFileContent(vars), "utf-8");
4809
+ console.log(chalk26.green(` \u2705 ${file} (${vars.length} variables)`));
4810
+ }
4530
4811
  function generateEnvFileContent(vars) {
4531
4812
  const lines = [
4532
4813
  "# Auto-generated by spfn env init",
@@ -4554,20 +4835,22 @@ function generateEnvFileContent(vars) {
4554
4835
  }
4555
4836
  async function checkEnvFiles(options) {
4556
4837
  const packageName = options.package || "@spfn/core";
4838
+ const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
4557
4839
  const cwd = process.cwd();
4558
4840
  try {
4559
4841
  const envSchema = await loadEnvSchema(packageName);
4560
4842
  const allVars = Object.entries(envSchema);
4843
+ const envLabel = targetEnv ? ` (${targetEnv})` : "";
4561
4844
  console.log(chalk26.blue.bold(`
4562
- \u{1F50D} Checking .env files against schema
4845
+ \u{1F50D} Checking .env files against schema${envLabel}
4563
4846
  `));
4564
- const allFiles = [...ENV_FILES.nextjs, ...ENV_FILES.server];
4847
+ const filesToCheck = targetEnv ? getEnvFilesForEnvironment(targetEnv) : [...BASE_ENV_FILES.nextjs, ...BASE_ENV_FILES.server];
4565
4848
  const loadedEnv = {};
4566
4849
  const issues = [];
4567
4850
  const warnings = [];
4568
- for (const file of allFiles) {
4851
+ for (const file of filesToCheck) {
4569
4852
  const filePath = resolve(cwd, file);
4570
- if (!existsSync23(filePath)) {
4853
+ if (!existsSync24(filePath)) {
4571
4854
  continue;
4572
4855
  }
4573
4856
  const content = readFileSync8(filePath, "utf-8");
@@ -4587,8 +4870,8 @@ async function checkEnvFiles(options) {
4587
4870
  }
4588
4871
  continue;
4589
4872
  }
4590
- const isNextjsFile = ENV_FILES.nextjs.includes(found.file);
4591
- const isServerFile = ENV_FILES.server.includes(found.file);
4873
+ const isNextjsFile = BASE_ENV_FILES.nextjs.includes(found.file);
4874
+ const isServerFile = BASE_ENV_FILES.server.includes(found.file);
4592
4875
  const shouldBeNextjs = schema.nextjs ?? key.startsWith("NEXT_PUBLIC_");
4593
4876
  if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
4594
4877
  if (schema.sensitive) {
@@ -4638,12 +4921,91 @@ async function checkEnvFiles(options) {
4638
4921
  process.exit(1);
4639
4922
  }
4640
4923
  }
4641
- 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);
4642
- envCommand.command("check").description("Check .env files against schema").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(checkEnvFiles);
4924
+ envCommand.command("init").description("Generate .env template files from schema").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").option("-e, --env <environment>", "Generate environment-specific templates (e.g. production, staging)").option("-f, --force", "Overwrite existing files").action(initEnvFiles);
4925
+ envCommand.command("check").description("Check .env files against schema").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").option("-e, --env <environment>", "Check files for a specific environment (e.g. production)").action(checkEnvFiles);
4926
+ async function validateEnvVars(options) {
4927
+ const packages = options.packages || ["@spfn/core"];
4928
+ const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
4929
+ if (targetEnv) {
4930
+ const { loadEnv: loadEnv6 } = await import("@spfn/core/env/loader");
4931
+ const result = loadEnv6({ nodeEnv: targetEnv });
4932
+ console.log(chalk26.blue.bold(`
4933
+ \u{1F50D} Validating environment variables for ${chalk26.cyan(targetEnv)}
4934
+ `));
4935
+ if (result.loadedFiles.length > 0) {
4936
+ console.log(chalk26.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
4937
+ }
4938
+ console.log("");
4939
+ } else {
4940
+ console.log(chalk26.blue.bold(`
4941
+ \u{1F50D} Validating environment variables
4942
+ `));
4943
+ }
4944
+ const allErrors = [];
4945
+ const allWarnings = [];
4946
+ for (const packageName of packages) {
4947
+ try {
4948
+ console.log(chalk26.dim(` \u{1F4E6} ${packageName}`));
4949
+ const envSchema = await loadEnvSchema(packageName);
4950
+ const { createEnvRegistry } = await import("@spfn/core/env");
4951
+ const registry = createEnvRegistry(envSchema);
4952
+ const result = registry.validateAll();
4953
+ for (const error of result.errors) {
4954
+ allErrors.push({ ...error, package: packageName });
4955
+ }
4956
+ for (const warning of result.warnings) {
4957
+ allWarnings.push({ ...warning, package: packageName });
4958
+ }
4959
+ } catch (error) {
4960
+ if (error instanceof Error && error.message.includes("does not export envSchema")) {
4961
+ console.log(chalk26.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
4962
+ continue;
4963
+ }
4964
+ console.error(chalk26.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
4965
+ if (options.strict) {
4966
+ process.exit(1);
4967
+ }
4968
+ }
4969
+ }
4970
+ console.log("");
4971
+ if (allErrors.length > 0) {
4972
+ console.log(chalk26.red.bold(`\u274C Validation Errors (${allErrors.length}):
4973
+ `));
4974
+ for (const error of allErrors) {
4975
+ console.log(` ${chalk26.red("\u2717")} ${chalk26.cyan(error.key)}`);
4976
+ console.log(` ${chalk26.dim(error.message)}`);
4977
+ console.log(` ${chalk26.dim(`from ${error.package}`)}`);
4978
+ console.log("");
4979
+ }
4980
+ }
4981
+ if (allWarnings.length > 0) {
4982
+ console.log(chalk26.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
4983
+ `));
4984
+ for (const warning of allWarnings) {
4985
+ console.log(` ${chalk26.yellow("\u26A0")} ${chalk26.cyan(warning.key)}`);
4986
+ console.log(` ${chalk26.dim(warning.message)}`);
4987
+ console.log("");
4988
+ }
4989
+ }
4990
+ if (allErrors.length === 0 && allWarnings.length === 0) {
4991
+ console.log(chalk26.green.bold("\u2705 All environment variables are valid!\n"));
4992
+ } else if (allErrors.length === 0) {
4993
+ console.log(chalk26.green("\u2705 No errors found."));
4994
+ console.log(chalk26.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
4995
+ `));
4996
+ } else {
4997
+ console.log(chalk26.red(`
4998
+ \u274C Validation failed with ${allErrors.length} error(s)
4999
+ `));
5000
+ process.exit(1);
5001
+ }
5002
+ }
5003
+ envCommand.command("validate").description("Validate environment variables against schema (for CI/CD)").option("-p, --packages <packages...>", "Packages to validate", ["@spfn/core"]).option("-e, --env <environment>", "Load env files for specific environment before validating").option("-s, --strict", "Exit on any error (including load failures)").action(validateEnvVars);
4643
5004
 
4644
5005
  // src/index.ts
5006
+ init_version();
4645
5007
  var program = new Command13();
4646
- program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version("0.1.0");
5008
+ program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(getCliVersion());
4647
5009
  program.addCommand(createCommand);
4648
5010
  program.addCommand(initCommand);
4649
5011
  program.addCommand(addCommand);