spfn 0.2.0-beta.4 → 0.2.0-beta.40

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.40";
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;
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");
873
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;
@@ -958,10 +1042,10 @@ __export(function_migrations_exports, {
958
1042
  discoverFunctionMigrations: () => discoverFunctionMigrations,
959
1043
  executeFunctionMigrations: () => executeFunctionMigrations
960
1044
  });
961
- import chalk11 from "chalk";
1045
+ import chalk12 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");
@@ -989,7 +1073,7 @@ function discoverFunctionMigrations(cwd = process.cwd()) {
989
1073
  const migrationsDir = join15(packagePath, spfnConfig.migrations.dir);
990
1074
  if (!existsSync16(migrationsDir)) {
991
1075
  console.warn(
992
- chalk11.yellow(`\u26A0\uFE0F Package @spfn/${pkg} specifies migrations but directory not found: ${migrationsDir}`)
1076
+ chalk12.yellow(`\u26A0\uFE0F Package @spfn/${pkg} specifies migrations but directory not found: ${migrationsDir}`)
993
1077
  );
994
1078
  continue;
995
1079
  }
@@ -999,28 +1083,99 @@ function discoverFunctionMigrations(cwd = process.cwd()) {
999
1083
  packagePath
1000
1084
  });
1001
1085
  } catch (error) {
1002
- console.warn(chalk11.yellow(`\u26A0\uFE0F Failed to parse package.json for @spfn/${pkg}`));
1086
+ console.warn(chalk12.yellow(`\u26A0\uFE0F Failed to parse package.json for @spfn/${pkg}`));
1003
1087
  }
1004
1088
  }
1005
1089
  return functions;
1006
1090
  }
1091
+ async function migrateLegacyTable(db, functionMigrations) {
1092
+ const legacyCheck = await db.execute(
1093
+ `SELECT EXISTS (
1094
+ SELECT 1 FROM information_schema.tables
1095
+ WHERE table_schema = 'drizzle' AND table_name = '__spfn_fn_migrations'
1096
+ ) AS "exists"`
1097
+ );
1098
+ if (!legacyCheck[0]?.exists) {
1099
+ return;
1100
+ }
1101
+ console.log(chalk12.dim("\n Migrating legacy shared migration table to per-package tables..."));
1102
+ const legacyRows = await db.execute(
1103
+ `SELECT hash, created_at FROM drizzle."__spfn_fn_migrations" ORDER BY id`
1104
+ );
1105
+ if (legacyRows.length === 0) {
1106
+ await db.execute(`DROP TABLE drizzle."__spfn_fn_migrations"`);
1107
+ return;
1108
+ }
1109
+ for (const func of functionMigrations) {
1110
+ const journalPath = join15(func.migrationsDir, "meta", "_journal.json");
1111
+ if (!existsSync16(journalPath)) {
1112
+ continue;
1113
+ }
1114
+ const journal = JSON.parse(readFileSync5(journalPath, "utf-8"));
1115
+ const entries = journal.entries || [];
1116
+ const tableName = `__spfn_fn_${func.packageName.replace("@spfn/", "")}_migrations`;
1117
+ await db.execute(
1118
+ `CREATE SCHEMA IF NOT EXISTS drizzle`
1119
+ );
1120
+ await db.execute(
1121
+ `CREATE TABLE IF NOT EXISTS drizzle."${tableName}" (
1122
+ id serial PRIMARY KEY,
1123
+ hash text NOT NULL,
1124
+ created_at bigint
1125
+ )`
1126
+ );
1127
+ const existing = await db.execute(
1128
+ `SELECT COUNT(*) AS "count" FROM drizzle."${tableName}"`
1129
+ );
1130
+ if (Number(existing[0]?.count) > 0) {
1131
+ continue;
1132
+ }
1133
+ const { createHash } = await import("crypto");
1134
+ let copied = 0;
1135
+ for (const entry of entries) {
1136
+ const sqlPath = join15(func.migrationsDir, `${entry.tag}.sql`);
1137
+ if (!existsSync16(sqlPath)) {
1138
+ continue;
1139
+ }
1140
+ const sqlContent = readFileSync5(sqlPath, "utf-8");
1141
+ const hash = createHash("sha256").update(sqlContent).digest("hex");
1142
+ const found = legacyRows.find((r) => r.hash === hash);
1143
+ if (found) {
1144
+ await db.execute(
1145
+ `INSERT INTO drizzle."${tableName}" (hash, created_at) VALUES ('${hash}', ${entry.when})`
1146
+ );
1147
+ copied++;
1148
+ }
1149
+ }
1150
+ if (copied > 0) {
1151
+ console.log(chalk12.dim(` \u2713 ${func.packageName}: copied ${copied} migration record(s)`));
1152
+ }
1153
+ }
1154
+ await db.execute(`DROP TABLE drizzle."__spfn_fn_migrations"`);
1155
+ console.log(chalk12.dim(" \u2713 Legacy migration table removed\n"));
1156
+ }
1007
1157
  async function executeFunctionMigrations(functionMigrations) {
1008
1158
  let executedCount = 0;
1009
1159
  const { drizzle } = await import("drizzle-orm/postgres-js");
1010
1160
  const { migrate } = await import("drizzle-orm/postgres-js/migrator");
1011
1161
  const postgres = await import("postgres");
1012
- loadEnvFiles2();
1162
+ loadEnv2();
1013
1163
  if (!env2.DATABASE_URL) {
1014
1164
  throw new Error("DATABASE_URL not found in environment");
1015
1165
  }
1016
1166
  const connection = postgres.default(env2.DATABASE_URL, { max: 1 });
1017
1167
  const db = drizzle(connection);
1018
1168
  try {
1169
+ await migrateLegacyTable(db, functionMigrations);
1019
1170
  for (const func of functionMigrations) {
1020
- console.log(chalk11.blue(`
1171
+ console.log(chalk12.blue(`
1021
1172
  \u{1F4E6} Running ${func.packageName} migrations...`));
1022
- await migrate(db, { migrationsFolder: func.migrationsDir });
1023
- console.log(chalk11.green(` \u2713 ${func.packageName} migrations applied`));
1173
+ const tableName = `__spfn_fn_${func.packageName.replace("@spfn/", "")}_migrations`;
1174
+ await migrate(db, {
1175
+ migrationsFolder: func.migrationsDir,
1176
+ migrationsTable: tableName
1177
+ });
1178
+ console.log(chalk12.green(` \u2713 ${func.packageName} migrations applied`));
1024
1179
  executedCount++;
1025
1180
  }
1026
1181
  } finally {
@@ -1179,10 +1334,30 @@ init_init();
1179
1334
  init_logger();
1180
1335
  init_package_manager();
1181
1336
  import { Command as Command4 } from "commander";
1182
- import { existsSync as existsSync11, readFileSync as readFileSync3, writeFileSync as writeFileSync6, mkdirSync } from "fs";
1337
+ import { existsSync as existsSync11, readFileSync as readFileSync3, writeFileSync as writeFileSync6, mkdirSync, unlinkSync, rmSync, watch } from "fs";
1183
1338
  import { join as join11 } from "path";
1184
1339
  import { execa as execa4 } from "execa";
1185
1340
  import chokidar from "chokidar";
1341
+ function waitForReadyFile(filePath, timeoutMs = 3e4) {
1342
+ return new Promise((resolve2, reject) => {
1343
+ if (existsSync11(filePath)) {
1344
+ unlinkSync(filePath);
1345
+ }
1346
+ const timer = setTimeout(() => {
1347
+ watcher.close();
1348
+ reject(new Error(`Server did not become ready within ${timeoutMs / 1e3}s`));
1349
+ }, timeoutMs);
1350
+ const dir = join11(filePath, "..");
1351
+ const fileName = filePath.split("/").pop();
1352
+ const watcher = watch(dir, (event, name) => {
1353
+ if (name === fileName && existsSync11(filePath)) {
1354
+ watcher.close();
1355
+ clearTimeout(timer);
1356
+ resolve2(readFileSync3(filePath, "utf-8").trim());
1357
+ }
1358
+ });
1359
+ });
1360
+ }
1186
1361
  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
1362
  process.setMaxListeners(20);
1188
1363
  if (!process.env.NODE_ENV) {
@@ -1205,22 +1380,36 @@ var devCommand = new Command4("dev").description("Start SPFN development server
1205
1380
  const serverEntry = join11(tempDir, "server.mjs");
1206
1381
  const watcherEntry = join11(tempDir, "watcher.mjs");
1207
1382
  mkdirSync(tempDir, { recursive: true });
1383
+ const serverBuildDir = join11(tempDir, "server");
1384
+ if (existsSync11(serverBuildDir)) {
1385
+ rmSync(serverBuildDir, { recursive: true, force: true });
1386
+ logger.info("[SPFN] Cleaned stale build cache (.spfn/server)");
1387
+ }
1388
+ const readySignal = join11(tempDir, "server-ready");
1389
+ if (existsSync11(readySignal)) {
1390
+ unlinkSync(readySignal);
1391
+ }
1208
1392
  const configParts = [];
1209
1393
  if (options.port) configParts.push(`port: ${options.port}`);
1210
1394
  if (options.host) configParts.push(`host: '${options.host}'`);
1211
1395
  if (options.routes) configParts.push(`routesPath: '${options.routes}'`);
1212
1396
  configParts.push("debug: true");
1397
+ const readyFile = join11(tempDir, "server-ready");
1213
1398
  writeFileSync6(serverEntry, `
1399
+ import { writeFileSync } from 'fs';
1400
+
1214
1401
  // Load environment variables FIRST (before any imports that depend on them)
1215
- // Use centralized environment loader for standard dotenv priority
1216
1402
  await import('@spfn/core/config');
1217
1403
 
1218
1404
  // Import and start server
1219
1405
  const { startServer } = await import('@spfn/core/server');
1220
1406
 
1221
- await startServer({
1407
+ const instance = await startServer({
1222
1408
  ${configParts.join(",\n ")}
1223
1409
  });
1410
+
1411
+ // Signal ready with actual port
1412
+ writeFileSync(${JSON.stringify(readyFile)}, String(instance.config.port));
1224
1413
  `);
1225
1414
  writeFileSync6(watcherEntry, `
1226
1415
  // Load environment variables
@@ -1277,7 +1466,9 @@ catch (error)
1277
1466
  const pm = detectPackageManager(cwd);
1278
1467
  if (options.serverOnly || !hasNext) {
1279
1468
  const watchMode2 = options.watch === true;
1280
- logger.info(`Starting SPFN Server on http://${options.host}:${options.port}${watchMode2 ? " (watch mode)" : ""}
1469
+ const host = options.host ?? process.env.HOST ?? "localhost";
1470
+ const port = options.port ?? process.env.PORT ?? "4000";
1471
+ logger.info(`Starting SPFN Server on http://${host}:${port}${watchMode2 ? " (watch mode)" : ""}
1281
1472
  `);
1282
1473
  let serverProcess2 = null;
1283
1474
  let watcherProcess2 = null;
@@ -1471,7 +1662,13 @@ catch (error)
1471
1662
  process.on("SIGTERM", cleanup);
1472
1663
  startWatcher();
1473
1664
  startServer();
1474
- await new Promise((resolve2) => setTimeout(resolve2, 2e3));
1665
+ try {
1666
+ const port = await waitForReadyFile(readyFile);
1667
+ logger.info(`[SPFN] Server ready on port ${port}, starting Next.js...
1668
+ `);
1669
+ } catch (error) {
1670
+ logger.warn(`[SPFN] Server readiness check timed out, starting Next.js anyway...`);
1671
+ }
1475
1672
  startNext();
1476
1673
  await new Promise((resolve2) => {
1477
1674
  const keepAlive = setInterval(() => {
@@ -1522,7 +1719,7 @@ async function buildProject(options) {
1522
1719
  const orchestrator = new CodegenOrchestrator({
1523
1720
  generators,
1524
1721
  cwd,
1525
- debug: false
1722
+ debug: true
1526
1723
  });
1527
1724
  await orchestrator.generateAll();
1528
1725
  spinner.succeed("API client generated");
@@ -1832,7 +2029,7 @@ async function runGenerators() {
1832
2029
  const orchestrator = new CodegenOrchestrator({
1833
2030
  generators,
1834
2031
  cwd,
1835
- debug: false
2032
+ debug: true
1836
2033
  });
1837
2034
  await orchestrator.generateAll();
1838
2035
  console.log("\n" + chalk7.green.bold("\u2713 Code generation completed"));
@@ -1991,14 +2188,15 @@ import { Command as Command9 } from "commander";
1991
2188
  import chalk10 from "chalk";
1992
2189
 
1993
2190
  // src/commands/db/utils/drizzle.ts
1994
- import { existsSync as existsSync15, writeFileSync as writeFileSync9, unlinkSync } from "fs";
2191
+ import { existsSync as existsSync15, writeFileSync as writeFileSync9, unlinkSync as unlinkSync2 } from "fs";
1995
2192
  import { spawn } from "child_process";
2193
+ import { pathToFileURL } from "url";
1996
2194
  import chalk9 from "chalk";
1997
2195
  import ora6 from "ora";
1998
2196
  import { env } from "@spfn/core/config";
1999
- import { loadEnvFiles } from "@spfn/core/server";
2197
+ import { loadEnv } from "@spfn/core/server";
2000
2198
  function validateDatabasePrerequisites() {
2001
- loadEnvFiles();
2199
+ loadEnv();
2002
2200
  if (!env.DATABASE_URL) {
2003
2201
  console.error(chalk9.red("\u274C DATABASE_URL not found in environment"));
2004
2202
  console.log(chalk9.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
@@ -2010,7 +2208,7 @@ async function runDrizzleCommand(command) {
2010
2208
  const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
2011
2209
  const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
2012
2210
  if (!hasUserConfig) {
2013
- loadEnvFiles();
2211
+ loadEnv();
2014
2212
  if (!env.DATABASE_URL) {
2015
2213
  console.error(chalk9.red("\u274C DATABASE_URL not found in environment"));
2016
2214
  console.log(chalk9.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
@@ -2020,7 +2218,10 @@ async function runDrizzleCommand(command) {
2020
2218
  const configContent = generateDrizzleConfigFile({
2021
2219
  cwd: process.cwd(),
2022
2220
  // Exclude package schemas to avoid .ts/.js mixing (packages use migrations instead)
2023
- disablePackageDiscovery: true
2221
+ disablePackageDiscovery: true,
2222
+ // Expand globs and auto-detect PostgreSQL schemas for push/generate compatibility
2223
+ expandGlobs: true,
2224
+ autoDetectSchemas: true
2024
2225
  });
2025
2226
  writeFileSync9(tempConfigPath, configContent);
2026
2227
  console.log(chalk9.dim("Using auto-generated Drizzle config\n"));
@@ -2031,11 +2232,12 @@ async function runDrizzleCommand(command) {
2031
2232
  const drizzleProcess = spawn("drizzle-kit", args, {
2032
2233
  stdio: "inherit",
2033
2234
  // Allow interactive input
2034
- shell: true
2235
+ shell: true,
2236
+ env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" }
2035
2237
  });
2036
2238
  const cleanup = () => {
2037
2239
  if (!hasUserConfig && existsSync15(tempConfigPath)) {
2038
- unlinkSync(tempConfigPath);
2240
+ unlinkSync2(tempConfigPath);
2039
2241
  }
2040
2242
  };
2041
2243
  drizzleProcess.on("close", (code) => {
@@ -2064,6 +2266,43 @@ async function runWithSpinner(spinnerText, command, successMessage, failMessage)
2064
2266
  process.exit(1);
2065
2267
  }
2066
2268
  }
2269
+ async function ensureTsxLoader() {
2270
+ }
2271
+ async function loadSchemaImports(schemaFiles) {
2272
+ const hasTsFiles = schemaFiles.some((f) => f.endsWith(".ts"));
2273
+ if (hasTsFiles) {
2274
+ await ensureTsxLoader();
2275
+ }
2276
+ const imports = {};
2277
+ for (const file of schemaFiles) {
2278
+ const moduleUrl = pathToFileURL(file).href;
2279
+ const mod = await import(moduleUrl);
2280
+ for (const [key, value] of Object.entries(mod)) {
2281
+ if (key !== "default") {
2282
+ imports[key] = value;
2283
+ }
2284
+ }
2285
+ }
2286
+ return imports;
2287
+ }
2288
+ async function createPushConnection() {
2289
+ loadEnv();
2290
+ if (!env.DATABASE_URL) {
2291
+ throw new Error("DATABASE_URL is required");
2292
+ }
2293
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
2294
+ const pg = await import("pg");
2295
+ const { drizzle } = await import("drizzle-orm/node-postgres");
2296
+ const pool = new pg.default.Pool({
2297
+ connectionString: env.DATABASE_URL,
2298
+ max: 1
2299
+ });
2300
+ const db = drizzle(pool);
2301
+ return {
2302
+ db,
2303
+ close: () => pool.end()
2304
+ };
2305
+ }
2067
2306
 
2068
2307
  // src/commands/db/generate.ts
2069
2308
  async function dbGenerate() {
@@ -2078,41 +2317,238 @@ async function dbGenerate() {
2078
2317
  }
2079
2318
 
2080
2319
  // src/commands/db/push.ts
2081
- import chalk12 from "chalk";
2320
+ import chalk13 from "chalk";
2321
+ import prompts3 from "prompts";
2082
2322
  import "@spfn/core/config";
2083
- async function dbPush() {
2084
- await runWithSpinner(
2085
- "Pushing schema changes to database...",
2086
- "push",
2087
- "Schema pushed successfully",
2088
- "Failed to push schema"
2089
- );
2090
- const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
2091
- const functions = discoverFunctionMigrations2(process.cwd());
2092
- if (functions.length > 0) {
2093
- console.log(chalk12.blue("\n\u{1F4E6} Applying function package migrations:"));
2094
- functions.forEach((func) => {
2095
- console.log(chalk12.dim(` - ${func.packageName}`));
2096
- });
2323
+ import { loadEnv as loadEnv3 } from "@spfn/core/server";
2324
+ import { sql } from "drizzle-orm";
2325
+ import { getTableConfig } from "drizzle-orm/pg-core";
2326
+
2327
+ // src/commands/db/utils/sql-classifier.ts
2328
+ var DESTRUCTIVE_PATTERNS = [
2329
+ { pattern: /^\s*DROP\s+TABLE/i, reason: "Drops entire table" },
2330
+ { pattern: /^\s*DROP\s+INDEX/i, reason: "Drops index" },
2331
+ { pattern: /^\s*DROP\s+SCHEMA/i, reason: "Drops schema" },
2332
+ { pattern: /^\s*DROP\s+TYPE/i, reason: "Drops custom type" },
2333
+ { pattern: /ALTER\s+TABLE\s+.*\bDROP\s+COLUMN\b/i, reason: "Drops column" },
2334
+ { pattern: /ALTER\s+TABLE\s+.*\bDROP\s+CONSTRAINT\b/i, reason: "Drops constraint" },
2335
+ { pattern: /ALTER\s+TABLE\s+.*ALTER\s+COLUMN\s+.*\bTYPE\b/i, reason: "Changes column type (potential data loss)" },
2336
+ { pattern: /ALTER\s+TYPE\s+.*\bRENAME\s+VALUE\b/i, reason: "Renames enum value" },
2337
+ { pattern: /^\s*TRUNCATE\b/i, reason: "Truncates table data" },
2338
+ { pattern: /^\s*DELETE\s+FROM\b/i, reason: "Deletes table data" }
2339
+ ];
2340
+ var WARNING_PATTERNS = [
2341
+ { pattern: /ALTER\s+TABLE\s+.*ALTER\s+COLUMN\s+.*\bSET\s+NOT\s+NULL\b/i, reason: "Adds NOT NULL (fails if NULLs exist)" },
2342
+ { pattern: /ALTER\s+TABLE\s+.*\bRENAME\s+COLUMN\b/i, reason: "Renames column" }
2343
+ ];
2344
+ function classifyStatement(sql2) {
2345
+ for (const { pattern, reason } of DESTRUCTIVE_PATTERNS) {
2346
+ if (pattern.test(sql2)) {
2347
+ return { sql: sql2, category: "destructive", reason };
2348
+ }
2349
+ }
2350
+ for (const { pattern, reason } of WARNING_PATTERNS) {
2351
+ if (pattern.test(sql2)) {
2352
+ return { sql: sql2, category: "warning", reason };
2353
+ }
2354
+ }
2355
+ return { sql: sql2, category: "safe", reason: "" };
2356
+ }
2357
+ function classifyStatements(statements) {
2358
+ const result = { safe: [], destructive: [], warning: [] };
2359
+ for (const sql2 of statements) {
2360
+ const classified = classifyStatement(sql2);
2361
+ result[classified.category].push(classified);
2362
+ }
2363
+ return result;
2364
+ }
2365
+
2366
+ // src/commands/db/utils/push-display.ts
2367
+ import chalk11 from "chalk";
2368
+ function formatSql(sql2) {
2369
+ return sql2.replace(/\s+/g, " ").trim();
2370
+ }
2371
+ function printStatements(statements, color) {
2372
+ for (const stmt of statements) {
2373
+ console.log(color(` ${formatSql(stmt.sql)}`));
2374
+ if (stmt.reason) {
2375
+ console.log(chalk11.dim(` \u2192 ${stmt.reason}`));
2376
+ }
2377
+ }
2378
+ }
2379
+ function displayClassifiedStatements(result) {
2380
+ if (result.safe.length > 0) {
2381
+ console.log(chalk11.green(`
2382
+ \u2705 Safe changes (${result.safe.length}):`));
2383
+ printStatements(result.safe, chalk11.green);
2384
+ }
2385
+ if (result.warning.length > 0) {
2386
+ console.log(chalk11.yellow(`
2387
+ \u26A0\uFE0F Warnings (${result.warning.length}):`));
2388
+ printStatements(result.warning, chalk11.yellow);
2389
+ }
2390
+ if (result.destructive.length > 0) {
2391
+ console.log(chalk11.red(`
2392
+ \u274C Destructive changes (${result.destructive.length}):`));
2393
+ printStatements(result.destructive, chalk11.red);
2394
+ }
2395
+ }
2396
+ function displayDryRunSummary(result) {
2397
+ const total = result.safe.length + result.warning.length + result.destructive.length;
2398
+ console.log(chalk11.cyan(`
2399
+ \u{1F50D} Dry-run: ${total} statement(s) detected
2400
+ `));
2401
+ displayClassifiedStatements(result);
2402
+ console.log("");
2403
+ }
2404
+ function displayApplySummary(applied, skipped) {
2405
+ if (applied > 0) {
2406
+ console.log(chalk11.green(`
2407
+ \u2705 Applied ${applied} statement(s)`));
2408
+ }
2409
+ if (skipped > 0) {
2410
+ console.log(chalk11.yellow(`\u23ED\uFE0F Skipped ${skipped} destructive statement(s)`));
2411
+ }
2412
+ console.log("");
2413
+ }
2414
+
2415
+ // src/commands/db/push.ts
2416
+ async function dbPush(options = {}) {
2417
+ validateDatabasePrerequisites();
2418
+ loadEnv3();
2419
+ const { getDrizzleConfig } = await import("@spfn/core/db");
2420
+ const config = getDrizzleConfig({
2421
+ cwd: process.cwd(),
2422
+ expandGlobs: true,
2423
+ autoDetectSchemas: true,
2424
+ disablePackageDiscovery: true
2425
+ });
2426
+ const schemaFiles = Array.isArray(config.schema) ? config.schema : [config.schema];
2427
+ if (schemaFiles.length === 0) {
2428
+ console.log(chalk13.yellow("No schema files found."));
2429
+ return;
2430
+ }
2431
+ console.log(chalk13.dim(`Found ${schemaFiles.length} schema file(s)
2432
+ `));
2433
+ const imports = await loadSchemaImports(schemaFiles);
2434
+ const detectedSchemas = new Set(config.schemaFilter ?? ["public"]);
2435
+ for (const value of Object.values(imports)) {
2097
2436
  try {
2098
- await executeFunctionMigrations2(functions);
2099
- console.log(chalk12.green("\n\u2705 All function migrations applied\n"));
2100
- } catch (error) {
2101
- console.error(chalk12.red("\n\u274C Failed to apply function migrations"));
2102
- console.error(chalk12.red(error instanceof Error ? error.message : "Unknown error"));
2103
- process.exit(1);
2437
+ const cfg = getTableConfig(value);
2438
+ if (cfg.schema) {
2439
+ detectedSchemas.add(cfg.schema);
2440
+ }
2441
+ } catch {
2104
2442
  }
2105
2443
  }
2444
+ const schemaFilter = Array.from(detectedSchemas);
2445
+ const { db, close } = await createPushConnection();
2446
+ try {
2447
+ const { pushSchema } = await import("drizzle-kit/api");
2448
+ const { statementsToExecute } = await pushSchema(
2449
+ imports,
2450
+ db,
2451
+ schemaFilter
2452
+ );
2453
+ const managedSchemaSet = new Set(schemaFilter);
2454
+ const statements = statementsToExecute.filter((s) => {
2455
+ const dropMatch = s.match(/^\s*DROP\s+SCHEMA\s+"?([^"\s;]+)"?/i);
2456
+ if (dropMatch && managedSchemaSet.has(dropMatch[1])) {
2457
+ console.log(chalk13.dim(` [skip] DROP SCHEMA "${dropMatch[1]}" \u2014 managed schema, ignoring`));
2458
+ return false;
2459
+ }
2460
+ return true;
2461
+ }).map(
2462
+ (s) => s.replace(/^CREATE SCHEMA(?!\s+IF\s+NOT\s+EXISTS)/i, "CREATE SCHEMA IF NOT EXISTS")
2463
+ );
2464
+ if (statements.length === 0) {
2465
+ console.log(chalk13.green("\u2705 No changes detected \u2014 database is up to date\n"));
2466
+ await applyFunctionMigrations();
2467
+ return;
2468
+ }
2469
+ const result = classifyStatements(statements);
2470
+ if (options.dryRun) {
2471
+ displayDryRunSummary(result);
2472
+ return;
2473
+ }
2474
+ displayClassifiedStatements(result);
2475
+ if (options.force) {
2476
+ console.log(chalk13.dim("\n--force: applying all changes..."));
2477
+ for (const stmt of statements) {
2478
+ await db.execute(sql.raw(stmt));
2479
+ }
2480
+ displayApplySummary(statements.length, 0);
2481
+ } else if (result.destructive.length === 0) {
2482
+ for (const stmt of statements) {
2483
+ await db.execute(sql.raw(stmt));
2484
+ }
2485
+ displayApplySummary(statements.length, 0);
2486
+ } else {
2487
+ const safeCount = result.safe.length + result.warning.length;
2488
+ if (safeCount > 0) {
2489
+ for (const stmt of [...result.safe, ...result.warning]) {
2490
+ await db.execute(sql.raw(stmt.sql));
2491
+ }
2492
+ console.log(chalk13.green(`
2493
+ \u2705 Applied ${safeCount} safe statement(s)`));
2494
+ }
2495
+ console.log(chalk13.red(`
2496
+ \u274C ${result.destructive.length} destructive change(s) require confirmation:`));
2497
+ for (const stmt of result.destructive) {
2498
+ console.log(chalk13.red(` ${stmt.sql.replace(/\s+/g, " ").trim()}`));
2499
+ console.log(chalk13.dim(` \u2192 ${stmt.reason}`));
2500
+ }
2501
+ const { confirm } = await prompts3({
2502
+ type: "confirm",
2503
+ name: "confirm",
2504
+ message: "Apply destructive changes?",
2505
+ initial: false
2506
+ });
2507
+ if (confirm) {
2508
+ for (const stmt of result.destructive) {
2509
+ await db.execute(sql.raw(stmt.sql));
2510
+ }
2511
+ displayApplySummary(statements.length, 0);
2512
+ } else {
2513
+ displayApplySummary(safeCount, result.destructive.length);
2514
+ console.log(chalk13.dim("Tip: Use --force to apply all changes without prompting.\n"));
2515
+ }
2516
+ }
2517
+ await applyFunctionMigrations();
2518
+ } finally {
2519
+ await close();
2520
+ }
2521
+ }
2522
+ async function applyFunctionMigrations() {
2523
+ const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
2524
+ const functions = discoverFunctionMigrations2(process.cwd());
2525
+ if (functions.length === 0) {
2526
+ return;
2527
+ }
2528
+ console.log(chalk13.blue("\n\u{1F4E6} Applying function package migrations:"));
2529
+ functions.forEach((func) => {
2530
+ console.log(chalk13.dim(` - ${func.packageName}`));
2531
+ });
2532
+ try {
2533
+ await executeFunctionMigrations2(functions);
2534
+ console.log(chalk13.green("\n\u2705 All function migrations applied\n"));
2535
+ } catch (error) {
2536
+ console.error(chalk13.red("\n\u274C Failed to apply function migrations"));
2537
+ console.error(chalk13.red(error instanceof Error ? error.message : "Unknown error"));
2538
+ process.exit(1);
2539
+ }
2106
2540
  }
2107
2541
 
2108
2542
  // src/commands/db/migrate.ts
2109
- import chalk16 from "chalk";
2543
+ import chalk17 from "chalk";
2544
+ import { join as join16 } from "path";
2545
+ import { existsSync as existsSync18 } from "fs";
2110
2546
 
2111
2547
  // src/commands/db/backup.ts
2112
2548
  import { promises as fs3 } from "fs";
2113
2549
  import path3 from "path";
2114
2550
  import { spawn as spawn2 } from "child_process";
2115
- import chalk15 from "chalk";
2551
+ import chalk16 from "chalk";
2116
2552
  import ora7 from "ora";
2117
2553
 
2118
2554
  // src/commands/db/utils/database.ts
@@ -2181,14 +2617,14 @@ function formatTimestamp() {
2181
2617
  import { promises as fs2 } from "fs";
2182
2618
  import { existsSync as existsSync17 } from "fs";
2183
2619
  import path2 from "path";
2184
- import chalk14 from "chalk";
2620
+ import chalk15 from "chalk";
2185
2621
 
2186
2622
  // src/commands/db/utils/metadata.ts
2187
2623
  import { promises as fs } from "fs";
2188
2624
  import path from "path";
2189
2625
  import { promisify } from "util";
2190
2626
  import { exec } from "child_process";
2191
- import chalk13 from "chalk";
2627
+ import chalk14 from "chalk";
2192
2628
  var execAsync = promisify(exec);
2193
2629
  async function collectGitInfo() {
2194
2630
  try {
@@ -2251,7 +2687,7 @@ async function collectMigrationInfo(dbUrl) {
2251
2687
  await pool.end();
2252
2688
  }
2253
2689
  } catch (error) {
2254
- console.log(chalk13.dim("\u26A0\uFE0F Could not fetch migration info"));
2690
+ console.log(chalk14.dim("\u26A0\uFE0F Could not fetch migration info"));
2255
2691
  return void 0;
2256
2692
  }
2257
2693
  }
@@ -2259,9 +2695,9 @@ async function saveBackupMetadata(metadata, backupFilename) {
2259
2695
  const metadataPath = backupFilename.replace(/\.(sql|dump)$/, ".meta.json");
2260
2696
  try {
2261
2697
  await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
2262
- console.log(chalk13.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
2698
+ console.log(chalk14.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
2263
2699
  } catch (error) {
2264
- console.log(chalk13.dim("\u26A0\uFE0F Could not save metadata"));
2700
+ console.log(chalk14.dim("\u26A0\uFE0F Could not save metadata"));
2265
2701
  }
2266
2702
  }
2267
2703
  async function loadBackupMetadata(backupFilename) {
@@ -2290,10 +2726,10 @@ async function ensureBackupInGitignore() {
2290
2726
  if (!hasBackupsIgnore) {
2291
2727
  const entry = exists && content && !content.endsWith("\n") ? "\n\n# Database backups\nbackups/\n" : "# Database backups\nbackups/\n";
2292
2728
  await fs2.appendFile(gitignorePath, entry);
2293
- console.log(chalk14.dim("\u2713 Added backups/ to .gitignore"));
2729
+ console.log(chalk15.dim("\u2713 Added backups/ to .gitignore"));
2294
2730
  }
2295
2731
  } catch (error) {
2296
- console.log(chalk14.dim("\u26A0\uFE0F Could not update .gitignore"));
2732
+ console.log(chalk15.dim("\u26A0\uFE0F Could not update .gitignore"));
2297
2733
  }
2298
2734
  }
2299
2735
  async function ensureBackupDir() {
@@ -2341,12 +2777,14 @@ async function listBackupFiles() {
2341
2777
 
2342
2778
  // src/commands/db/backup.ts
2343
2779
  import { env as env3 } from "@spfn/core/config";
2780
+ import { loadEnv as loadEnv4 } from "@spfn/core/server";
2344
2781
  async function dbBackup(options) {
2345
- console.log(chalk15.blue("\u{1F4BE} Creating database backup...\n"));
2782
+ console.log(chalk16.blue("\u{1F4BE} Creating database backup...\n"));
2783
+ loadEnv4();
2346
2784
  const dbUrl = env3.DATABASE_URL;
2347
2785
  if (!dbUrl) {
2348
- console.error(chalk15.red("\u274C DATABASE_URL not found in environment"));
2349
- console.log(chalk15.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2786
+ console.error(chalk16.red("\u274C DATABASE_URL not found in environment"));
2787
+ console.log(chalk16.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2350
2788
  process.exit(1);
2351
2789
  }
2352
2790
  const dbInfo = parseDatabaseUrl(dbUrl);
@@ -2356,7 +2794,7 @@ async function dbBackup(options) {
2356
2794
  const ext = format === "sql" ? "sql" : "dump";
2357
2795
  const filename = options.output || path3.join(backupDir, `${dbInfo.database}_${timestamp}.${ext}`);
2358
2796
  if (options.dataOnly && options.schemaOnly) {
2359
- console.error(chalk15.red("\u274C Cannot use --data-only and --schema-only together"));
2797
+ console.error(chalk16.red("\u274C Cannot use --data-only and --schema-only together"));
2360
2798
  process.exit(1);
2361
2799
  }
2362
2800
  const args = [
@@ -2402,11 +2840,11 @@ async function dbBackup(options) {
2402
2840
  const stats = await fs3.stat(filename);
2403
2841
  const size = formatBytes(stats.size);
2404
2842
  spinner.succeed("Backup created");
2405
- console.log(chalk15.green(`
2843
+ console.log(chalk16.green(`
2406
2844
  \u2705 Backup created successfully`));
2407
- console.log(chalk15.gray(` File: ${filename}`));
2408
- console.log(chalk15.gray(` Size: ${size}`));
2409
- console.log(chalk15.dim("\n\u{1F4CB} Collecting metadata..."));
2845
+ console.log(chalk16.gray(` File: ${filename}`));
2846
+ console.log(chalk16.gray(` Size: ${size}`));
2847
+ console.log(chalk16.dim("\n\u{1F4CB} Collecting metadata..."));
2410
2848
  const [gitInfo, migrationInfo] = await Promise.all([
2411
2849
  collectGitInfo(),
2412
2850
  collectMigrationInfo(dbUrl)
@@ -2446,18 +2884,20 @@ async function dbBackup(options) {
2446
2884
  reject(error);
2447
2885
  });
2448
2886
  }).catch((error) => {
2449
- console.error(chalk15.red("\n\u274C Failed to create backup"));
2887
+ console.error(chalk16.red("\n\u274C Failed to create backup"));
2450
2888
  if (errorOutput.includes("pg_dump: command not found") || errorOutput.includes("not found")) {
2451
- console.error(chalk15.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
2889
+ console.error(chalk16.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
2452
2890
  } else {
2453
- console.error(chalk15.red(error instanceof Error ? error.message : "Unknown error"));
2891
+ console.error(chalk16.red(error instanceof Error ? error.message : "Unknown error"));
2454
2892
  }
2455
2893
  process.exit(1);
2456
2894
  });
2457
2895
  }
2458
2896
 
2459
2897
  // src/commands/db/migrate.ts
2460
- import "@spfn/core/config";
2898
+ import { env as env4 } from "@spfn/core/config";
2899
+ import { loadEnv as loadEnv5 } from "@spfn/core/server";
2900
+ var PROJECT_MIGRATIONS_TABLE = "__drizzle_migrations";
2461
2901
  async function dbMigrate(options = {}) {
2462
2902
  try {
2463
2903
  validateDatabasePrerequisites();
@@ -2465,7 +2905,7 @@ async function dbMigrate(options = {}) {
2465
2905
  process.exit(1);
2466
2906
  }
2467
2907
  if (options.withBackup) {
2468
- console.log(chalk16.blue("\u{1F4E6} Creating pre-migration backup...\n"));
2908
+ console.log(chalk17.blue("\u{1F4E6} Creating pre-migration backup...\n"));
2469
2909
  await dbBackup({
2470
2910
  format: "custom",
2471
2911
  tag: "pre-migration",
@@ -2473,59 +2913,72 @@ async function dbMigrate(options = {}) {
2473
2913
  });
2474
2914
  console.log("");
2475
2915
  }
2916
+ const { drizzle } = await import("drizzle-orm/postgres-js");
2917
+ const { migrate } = await import("drizzle-orm/postgres-js/migrator");
2918
+ const postgres = await import("postgres");
2919
+ loadEnv5();
2920
+ if (!env4.DATABASE_URL) {
2921
+ console.error(chalk17.red("\u274C DATABASE_URL not found in environment"));
2922
+ process.exit(1);
2923
+ }
2476
2924
  const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
2477
2925
  const functions = discoverFunctionMigrations2(process.cwd());
2478
2926
  if (functions.length > 0) {
2479
- console.log(chalk16.blue("\u{1F4E6} Applying function package migrations:"));
2927
+ console.log(chalk17.blue("\u{1F4E6} Applying function package migrations:"));
2480
2928
  functions.forEach((func) => {
2481
- console.log(chalk16.dim(` - ${func.packageName}`));
2929
+ console.log(chalk17.dim(` - ${func.packageName}`));
2482
2930
  });
2931
+ await executeFunctionMigrations2(functions);
2932
+ console.log(chalk17.green("\u2705 Function migrations applied\n"));
2933
+ }
2934
+ const projectMigrationsDir = join16(process.cwd(), "src/server/drizzle");
2935
+ if (existsSync18(projectMigrationsDir)) {
2936
+ const projConn = postgres.default(env4.DATABASE_URL, { max: 1 });
2937
+ const projDb = drizzle(projConn);
2483
2938
  try {
2484
- await executeFunctionMigrations2(functions);
2485
- 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);
2939
+ console.log(chalk17.blue("\u{1F4E6} Running project migrations..."));
2940
+ await migrate(projDb, {
2941
+ migrationsFolder: projectMigrationsDir,
2942
+ migrationsTable: PROJECT_MIGRATIONS_TABLE
2943
+ });
2944
+ console.log(chalk17.green("\u2705 Project migrations applied successfully"));
2945
+ } finally {
2946
+ await projConn.end();
2490
2947
  }
2948
+ } else {
2949
+ console.log(chalk17.dim("No project migrations found (src/server/drizzle)"));
2491
2950
  }
2492
- await runWithSpinner(
2493
- "Running project migrations...",
2494
- "migrate",
2495
- "Project migrations applied successfully",
2496
- "Failed to run project migrations"
2497
- );
2498
2951
  }
2499
2952
 
2500
2953
  // src/commands/db/studio.ts
2501
- import chalk17 from "chalk";
2502
- import { existsSync as existsSync18, writeFileSync as writeFileSync10, unlinkSync as unlinkSync2 } from "fs";
2954
+ import chalk18 from "chalk";
2955
+ import { existsSync as existsSync19, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
2503
2956
  import { spawn as spawn3 } from "child_process";
2504
- import { env as env4 } from "@spfn/core/config";
2957
+ import { env as env5 } from "@spfn/core/config";
2505
2958
  import "@spfn/core/config";
2506
2959
  async function dbStudio(requestedPort) {
2507
- console.log(chalk17.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
2960
+ console.log(chalk18.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
2508
2961
  const defaultPort = 4983;
2509
2962
  const startPort = requestedPort || defaultPort;
2510
2963
  let port;
2511
2964
  try {
2512
2965
  port = await findAvailablePort(startPort);
2513
2966
  if (port !== startPort) {
2514
- console.log(chalk17.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
2967
+ console.log(chalk18.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
2515
2968
  `));
2516
2969
  }
2517
2970
  } catch (error) {
2518
- console.error(chalk17.red(error instanceof Error ? error.message : "Failed to find available port"));
2971
+ console.error(chalk18.red(error instanceof Error ? error.message : "Failed to find available port"));
2519
2972
  process.exit(1);
2520
2973
  }
2521
- const hasUserConfig = existsSync18("./drizzle.config.ts");
2974
+ const hasUserConfig = existsSync19("./drizzle.config.ts");
2522
2975
  const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
2523
2976
  try {
2524
2977
  const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
2525
2978
  if (!hasUserConfig) {
2526
- if (!env4.DATABASE_URL) {
2527
- console.error(chalk17.red("\u274C DATABASE_URL not found in environment"));
2528
- console.log(chalk17.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2979
+ if (!env5.DATABASE_URL) {
2980
+ console.error(chalk18.red("\u274C DATABASE_URL not found in environment"));
2981
+ console.log(chalk18.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2529
2982
  process.exit(1);
2530
2983
  }
2531
2984
  const { generateDrizzleConfigFile } = await import("@spfn/core/db");
@@ -2536,33 +2989,34 @@ async function dbStudio(requestedPort) {
2536
2989
  // Expand glob patterns for Studio compatibility
2537
2990
  });
2538
2991
  writeFileSync10(tempConfigPath, configContent);
2539
- console.log(chalk17.dim("Using auto-generated Drizzle config\n"));
2992
+ console.log(chalk18.dim("Using auto-generated Drizzle config\n"));
2540
2993
  }
2541
2994
  const studioProcess = spawn3("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
2542
2995
  stdio: "inherit",
2543
- shell: true
2996
+ shell: true,
2997
+ env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" }
2544
2998
  });
2545
2999
  const cleanup = () => {
2546
- if (!hasUserConfig && existsSync18(tempConfigPath)) {
2547
- unlinkSync2(tempConfigPath);
3000
+ if (!hasUserConfig && existsSync19(tempConfigPath)) {
3001
+ unlinkSync3(tempConfigPath);
2548
3002
  }
2549
3003
  };
2550
3004
  studioProcess.on("exit", (code) => {
2551
3005
  cleanup();
2552
3006
  if (code !== 0 && code !== null) {
2553
- console.error(chalk17.red(`
3007
+ console.error(chalk18.red(`
2554
3008
  \u274C Drizzle Studio exited with code ${code}`));
2555
3009
  process.exit(code);
2556
3010
  }
2557
3011
  });
2558
3012
  studioProcess.on("error", (error) => {
2559
3013
  cleanup();
2560
- console.error(chalk17.red("\u274C Failed to start Drizzle Studio"));
2561
- console.error(chalk17.red(error.message));
3014
+ console.error(chalk18.red("\u274C Failed to start Drizzle Studio"));
3015
+ console.error(chalk18.red(error.message));
2562
3016
  process.exit(1);
2563
3017
  });
2564
3018
  process.on("SIGINT", () => {
2565
- console.log(chalk17.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
3019
+ console.log(chalk18.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
2566
3020
  studioProcess.kill("SIGTERM");
2567
3021
  cleanup();
2568
3022
  process.exit(0);
@@ -2573,28 +3027,28 @@ async function dbStudio(requestedPort) {
2573
3027
  process.exit(0);
2574
3028
  });
2575
3029
  } catch (error) {
2576
- if (!hasUserConfig && existsSync18(tempConfigPath)) {
2577
- unlinkSync2(tempConfigPath);
3030
+ if (!hasUserConfig && existsSync19(tempConfigPath)) {
3031
+ unlinkSync3(tempConfigPath);
2578
3032
  }
2579
- console.error(chalk17.red("\u274C Failed to start Drizzle Studio"));
2580
- console.error(chalk17.red(error instanceof Error ? error.message : "Unknown error"));
3033
+ console.error(chalk18.red("\u274C Failed to start Drizzle Studio"));
3034
+ console.error(chalk18.red(error instanceof Error ? error.message : "Unknown error"));
2581
3035
  process.exit(1);
2582
3036
  }
2583
3037
  }
2584
3038
 
2585
3039
  // src/commands/db/drop.ts
2586
- import chalk18 from "chalk";
2587
- import prompts3 from "prompts";
3040
+ import chalk19 from "chalk";
3041
+ import prompts4 from "prompts";
2588
3042
  async function dbDrop() {
2589
- console.log(chalk18.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
2590
- const { confirm } = await prompts3({
3043
+ console.log(chalk19.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
3044
+ const { confirm } = await prompts4({
2591
3045
  type: "confirm",
2592
3046
  name: "confirm",
2593
3047
  message: "Are you sure you want to drop all tables?",
2594
3048
  initial: false
2595
3049
  });
2596
3050
  if (!confirm) {
2597
- console.log(chalk18.gray("Cancelled."));
3051
+ console.log(chalk19.gray("Cancelled."));
2598
3052
  process.exit(0);
2599
3053
  }
2600
3054
  await runWithSpinner(
@@ -2606,7 +3060,7 @@ async function dbDrop() {
2606
3060
  }
2607
3061
 
2608
3062
  // src/commands/db/check.ts
2609
- import chalk19 from "chalk";
3063
+ import chalk20 from "chalk";
2610
3064
  import ora8 from "ora";
2611
3065
  async function dbCheck() {
2612
3066
  const spinner = ora8("Checking database connection...").start();
@@ -2615,7 +3069,7 @@ async function dbCheck() {
2615
3069
  spinner.succeed("Database connection OK");
2616
3070
  } catch (error) {
2617
3071
  spinner.fail("Database connection failed");
2618
- console.error(chalk19.red(error instanceof Error ? error.message : "Unknown error"));
3072
+ console.error(chalk20.red(error instanceof Error ? error.message : "Unknown error"));
2619
3073
  process.exit(1);
2620
3074
  }
2621
3075
  }
@@ -2623,26 +3077,28 @@ async function dbCheck() {
2623
3077
  // src/commands/db/restore.ts
2624
3078
  import path4 from "path";
2625
3079
  import { spawn as spawn4 } from "child_process";
2626
- import chalk20 from "chalk";
3080
+ import chalk21 from "chalk";
2627
3081
  import ora9 from "ora";
2628
- import prompts4 from "prompts";
2629
- import { env as env5 } from "@spfn/core/config";
3082
+ import prompts5 from "prompts";
3083
+ import { env as env6 } from "@spfn/core/config";
3084
+ import { loadEnv as loadEnv6 } from "@spfn/core/server";
2630
3085
  async function dbRestore(backupFile, options = {}) {
2631
- console.log(chalk20.blue("\u267B\uFE0F Restoring database from backup...\n"));
2632
- const dbUrl = env5.DATABASE_URL;
3086
+ console.log(chalk21.blue("\u267B\uFE0F Restoring database from backup...\n"));
3087
+ loadEnv6();
3088
+ const dbUrl = env6.DATABASE_URL;
2633
3089
  if (!dbUrl) {
2634
- console.error(chalk20.red("\u274C DATABASE_URL not found in environment"));
2635
- console.log(chalk20.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
3090
+ console.error(chalk21.red("\u274C DATABASE_URL not found in environment"));
3091
+ console.log(chalk21.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2636
3092
  process.exit(1);
2637
3093
  }
2638
3094
  let file = backupFile;
2639
3095
  if (!file) {
2640
3096
  const backups = await listBackupFiles();
2641
3097
  if (backups.length === 0) {
2642
- console.log(chalk20.yellow("No backups found in ./backups directory"));
3098
+ console.log(chalk21.yellow("No backups found in ./backups directory"));
2643
3099
  process.exit(0);
2644
3100
  }
2645
- const { selected } = await prompts4({
3101
+ const { selected } = await prompts5({
2646
3102
  type: "select",
2647
3103
  name: "selected",
2648
3104
  message: "Select backup to restore:",
@@ -2652,70 +3108,70 @@ async function dbRestore(backupFile, options = {}) {
2652
3108
  }))
2653
3109
  });
2654
3110
  if (!selected) {
2655
- console.log(chalk20.gray("Cancelled"));
3111
+ console.log(chalk21.gray("Cancelled"));
2656
3112
  process.exit(0);
2657
3113
  }
2658
3114
  file = selected;
2659
3115
  }
2660
3116
  if (!file) {
2661
- console.error(chalk20.red("\u274C No backup file selected"));
3117
+ console.error(chalk21.red("\u274C No backup file selected"));
2662
3118
  process.exit(1);
2663
3119
  }
2664
3120
  const metadata = await loadBackupMetadata(file);
2665
3121
  if (metadata) {
2666
- console.log(chalk20.blue("\n\u{1F4CB} Backup Information:\n"));
2667
- console.log(chalk20.dim(` Database: ${metadata.database}`));
2668
- console.log(chalk20.dim(` Created: ${new Date(metadata.timestamp).toLocaleString()}`));
3122
+ console.log(chalk21.blue("\n\u{1F4CB} Backup Information:\n"));
3123
+ console.log(chalk21.dim(` Database: ${metadata.database}`));
3124
+ console.log(chalk21.dim(` Created: ${new Date(metadata.timestamp).toLocaleString()}`));
2669
3125
  if (metadata.environment) {
2670
- console.log(chalk20.dim(` Environment: ${metadata.environment}`));
3126
+ console.log(chalk21.dim(` Environment: ${metadata.environment}`));
2671
3127
  }
2672
3128
  if (metadata.tags && metadata.tags.length > 0) {
2673
- console.log(chalk20.dim(` Tags: ${metadata.tags.join(", ")}`));
3129
+ console.log(chalk21.dim(` Tags: ${metadata.tags.join(", ")}`));
2674
3130
  }
2675
3131
  if (metadata.backup.dataOnly) {
2676
- console.log(chalk20.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
3132
+ console.log(chalk21.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
2677
3133
  }
2678
3134
  if (metadata.backup.schemaOnly) {
2679
- console.log(chalk20.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
3135
+ console.log(chalk21.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
2680
3136
  }
2681
- const warnings = [];
3137
+ const warnings2 = [];
2682
3138
  const [currentGitInfo, currentMigrationInfo] = await Promise.all([
2683
3139
  collectGitInfo(),
2684
3140
  collectMigrationInfo(dbUrl)
2685
3141
  ]);
2686
3142
  if (metadata.git && currentGitInfo) {
2687
3143
  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)}`);
3144
+ warnings2.push(`Git commit mismatch: backup from ${metadata.git.commit.substring(0, 7)}, current is ${currentGitInfo.commit.substring(0, 7)}`);
2689
3145
  }
2690
3146
  if (metadata.git.branch !== currentGitInfo.branch) {
2691
- warnings.push(`Git branch mismatch: backup from '${metadata.git.branch}', current is '${currentGitInfo.branch}'`);
3147
+ warnings2.push(`Git branch mismatch: backup from '${metadata.git.branch}', current is '${currentGitInfo.branch}'`);
2692
3148
  }
2693
3149
  }
2694
3150
  if (metadata.migrations && currentMigrationInfo) {
2695
3151
  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}`);
3152
+ warnings2.push(`Migration version mismatch: backup has ${metadata.migrations.count} migrations, current has ${currentMigrationInfo.count}`);
3153
+ warnings2.push(` Last migration in backup: ${metadata.migrations.hash}`);
3154
+ warnings2.push(` Current last migration: ${currentMigrationInfo.hash}`);
2699
3155
  }
2700
3156
  }
2701
- if (warnings.length > 0) {
2702
- console.log(chalk20.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
2703
- warnings.forEach((warning) => console.log(chalk20.yellow(` - ${warning}`)));
3157
+ if (warnings2.length > 0) {
3158
+ console.log(chalk21.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
3159
+ warnings2.forEach((warning) => console.log(chalk21.yellow(` - ${warning}`)));
2704
3160
  console.log("");
2705
3161
  }
2706
3162
  }
2707
- const { confirm } = await prompts4({
3163
+ const { confirm } = await prompts5({
2708
3164
  type: "confirm",
2709
3165
  name: "confirm",
2710
- message: chalk20.yellow("\u26A0\uFE0F This will replace all data in the database. Continue?"),
3166
+ message: chalk21.yellow("\u26A0\uFE0F This will replace all data in the database. Continue?"),
2711
3167
  initial: false
2712
3168
  });
2713
3169
  if (!confirm) {
2714
- console.log(chalk20.gray("Cancelled"));
3170
+ console.log(chalk21.gray("Cancelled"));
2715
3171
  process.exit(0);
2716
3172
  }
2717
3173
  if (options.dataOnly && options.schemaOnly) {
2718
- console.error(chalk20.red("\u274C Cannot use --data-only and --schema-only together"));
3174
+ console.error(chalk21.red("\u274C Cannot use --data-only and --schema-only together"));
2719
3175
  process.exit(1);
2720
3176
  }
2721
3177
  const dbInfo = parseDatabaseUrl(dbUrl);
@@ -2728,6 +3184,7 @@ async function dbRestore(backupFile, options = {}) {
2728
3184
  args.push("-p", dbInfo.port);
2729
3185
  args.push("-U", dbInfo.user);
2730
3186
  args.push("-d", dbInfo.database);
3187
+ args.push("--verbose");
2731
3188
  if (options.drop) {
2732
3189
  args.push("--clean");
2733
3190
  }
@@ -2743,15 +3200,17 @@ async function dbRestore(backupFile, options = {}) {
2743
3200
  args.push(file);
2744
3201
  } else {
2745
3202
  if (options.dataOnly || options.schemaOnly) {
2746
- console.log(chalk20.yellow("\u26A0\uFE0F Note: --data-only and --schema-only options only work with custom format backups (.dump)"));
2747
- console.log(chalk20.yellow(" For SQL files, the backup must have been created with the desired option.\n"));
3203
+ console.log(chalk21.yellow("\u26A0\uFE0F Note: --data-only and --schema-only options only work with custom format backups (.dump)"));
3204
+ console.log(chalk21.yellow(" For SQL files, the backup must have been created with the desired option.\n"));
2748
3205
  }
2749
3206
  args.push("-h", dbInfo.host);
2750
3207
  args.push("-p", dbInfo.port);
2751
3208
  args.push("-U", dbInfo.user);
2752
3209
  args.push("-d", dbInfo.database);
3210
+ args.push("-v", "ON_ERROR_STOP=1");
2753
3211
  args.push("-f", file);
2754
3212
  }
3213
+ const verbose = options.verbose ?? false;
2755
3214
  const spinner = ora9("Restoring backup...").start();
2756
3215
  const restoreProcess = spawn4(command, args, {
2757
3216
  stdio: ["ignore", "pipe", "pipe"],
@@ -2760,19 +3219,81 @@ async function dbRestore(backupFile, options = {}) {
2760
3219
  PGPASSWORD: dbInfo.password
2761
3220
  }
2762
3221
  });
2763
- let errorOutput = "";
3222
+ const warnings = [];
3223
+ const errors = [];
3224
+ let objectCount = 0;
3225
+ let lastObject = "";
2764
3226
  restoreProcess.stderr?.on("data", (data) => {
2765
- errorOutput += data.toString();
3227
+ const lines = data.toString().split("\n").filter((l) => l.trim());
3228
+ for (const line of lines) {
3229
+ if (/^pg_restore:.*warning:/i.test(line) || /^WARNING:/i.test(line)) {
3230
+ warnings.push(line.trim());
3231
+ } else if (/^pg_restore:.*error:/i.test(line) || /^ERROR:/i.test(line) || /^psql:.*ERROR/i.test(line)) {
3232
+ errors.push(line.trim());
3233
+ }
3234
+ const objectMatch = line.match(/processing item (\d+)\/(\d+)/);
3235
+ if (objectMatch) {
3236
+ objectCount = Number(objectMatch[2]);
3237
+ const current = Number(objectMatch[1]);
3238
+ const desc = line.replace(/^pg_restore:\s*/, "").trim();
3239
+ lastObject = desc;
3240
+ spinner.text = `Restoring backup... [${current}/${objectCount}] ${desc}`;
3241
+ } else if (isCustomFormat) {
3242
+ const desc = line.replace(/^pg_restore:\s*/, "").trim();
3243
+ if (desc && !/warning:|error:/i.test(desc)) {
3244
+ lastObject = desc;
3245
+ spinner.text = `Restoring backup... ${desc}`;
3246
+ }
3247
+ }
3248
+ if (verbose) {
3249
+ spinner.stop();
3250
+ console.log(chalk21.dim(` ${line.trim()}`));
3251
+ spinner.start();
3252
+ }
3253
+ }
3254
+ });
3255
+ restoreProcess.stdout?.on("data", (data) => {
3256
+ if (verbose) {
3257
+ spinner.stop();
3258
+ console.log(chalk21.dim(` ${data.toString().trim()}`));
3259
+ spinner.start();
3260
+ }
2766
3261
  });
2767
3262
  await new Promise((resolve2, reject) => {
2768
3263
  restoreProcess.on("close", (code) => {
2769
3264
  if (code === 0) {
2770
- spinner.succeed("Restore completed");
2771
- console.log(chalk20.green("\n\u2705 Database restored successfully"));
3265
+ const summary = objectCount > 0 ? ` (${objectCount} objects)` : "";
3266
+ spinner.succeed(`Restore completed${summary}`);
3267
+ if (warnings.length > 0) {
3268
+ console.log(chalk21.yellow(`
3269
+ \u26A0\uFE0F Warnings during restore (${warnings.length}):
3270
+ `));
3271
+ for (const w of warnings) {
3272
+ console.log(chalk21.yellow(` - ${w}`));
3273
+ }
3274
+ }
3275
+ console.log(chalk21.green("\n\u2705 Database restored successfully"));
2772
3276
  resolve2();
2773
3277
  } else {
2774
3278
  spinner.fail("Restore failed");
2775
- reject(new Error(errorOutput || "Restore failed"));
3279
+ if (errors.length > 0) {
3280
+ console.error(chalk21.red(`
3281
+ \u274C Errors (${errors.length}):
3282
+ `));
3283
+ for (const e of errors) {
3284
+ console.error(chalk21.red(` - ${e}`));
3285
+ }
3286
+ }
3287
+ if (warnings.length > 0) {
3288
+ console.log(chalk21.yellow(`
3289
+ \u26A0\uFE0F Warnings (${warnings.length}):
3290
+ `));
3291
+ for (const w of warnings) {
3292
+ console.log(chalk21.yellow(` - ${w}`));
3293
+ }
3294
+ }
3295
+ const fallback = errors.length === 0 && warnings.length === 0 ? "Restore failed with no output" : "";
3296
+ reject(new Error(fallback));
2776
3297
  }
2777
3298
  });
2778
3299
  restoreProcess.on("error", (error) => {
@@ -2780,24 +3301,27 @@ async function dbRestore(backupFile, options = {}) {
2780
3301
  reject(error);
2781
3302
  });
2782
3303
  }).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"));
3304
+ const msg = error instanceof Error ? error.message : "Unknown error";
3305
+ if (msg) {
3306
+ console.error(chalk21.red(`
3307
+ \u274C ${msg}`));
3308
+ }
2785
3309
  process.exit(1);
2786
3310
  });
2787
3311
  }
2788
3312
 
2789
3313
  // src/commands/db/list.ts
2790
- import chalk21 from "chalk";
3314
+ import chalk22 from "chalk";
2791
3315
  async function dbBackupList() {
2792
- console.log(chalk21.blue("\u{1F4CB} Database backups:\n"));
3316
+ console.log(chalk22.blue("\u{1F4CB} Database backups:\n"));
2793
3317
  const backups = await listBackupFiles();
2794
3318
  if (backups.length === 0) {
2795
- console.log(chalk21.yellow("No backups found in ./backups directory"));
2796
- console.log(chalk21.gray("\n\u{1F4A1} Create a backup with: pnpm spfn db backup\n"));
3319
+ console.log(chalk22.yellow("No backups found in ./backups directory"));
3320
+ console.log(chalk22.gray("\n\u{1F4A1} Create a backup with: pnpm spfn db backup\n"));
2797
3321
  return;
2798
3322
  }
2799
- console.log(chalk21.bold(" Date Size File"));
2800
- 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"));
3323
+ console.log(chalk22.bold(" Date Size File"));
3324
+ console.log(chalk22.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"));
2801
3325
  backups.forEach((backup) => {
2802
3326
  const date = backup.date.toLocaleString("en-US", {
2803
3327
  year: "numeric",
@@ -2808,23 +3332,23 @@ async function dbBackupList() {
2808
3332
  second: "2-digit"
2809
3333
  });
2810
3334
  const sizeStr = backup.size.padEnd(10);
2811
- console.log(chalk21.white(` ${date} ${sizeStr} ${backup.name}`));
3335
+ console.log(chalk22.white(` ${date} ${sizeStr} ${backup.name}`));
2812
3336
  });
2813
- console.log(chalk21.gray(`
3337
+ console.log(chalk22.gray(`
2814
3338
  Total: ${backups.length} backup(s)
2815
3339
  `));
2816
3340
  }
2817
3341
 
2818
3342
  // src/commands/db/clean.ts
2819
3343
  import { promises as fs4 } from "fs";
2820
- import chalk22 from "chalk";
3344
+ import chalk23 from "chalk";
2821
3345
  import ora10 from "ora";
2822
- import prompts5 from "prompts";
3346
+ import prompts6 from "prompts";
2823
3347
  async function dbBackupClean(options) {
2824
- console.log(chalk22.blue("\u{1F9F9} Cleaning old backups...\n"));
3348
+ console.log(chalk23.blue("\u{1F9F9} Cleaning old backups...\n"));
2825
3349
  const backups = await listBackupFiles();
2826
3350
  if (backups.length === 0) {
2827
- console.log(chalk22.yellow("No backups found"));
3351
+ console.log(chalk23.yellow("No backups found"));
2828
3352
  return;
2829
3353
  }
2830
3354
  let toDelete = [];
@@ -2841,74 +3365,176 @@ async function dbBackupClean(options) {
2841
3365
  toDelete = backups.slice(defaultKeep);
2842
3366
  }
2843
3367
  if (toDelete.length === 0) {
2844
- console.log(chalk22.green("\u2705 No backups to clean"));
3368
+ console.log(chalk23.green("\u2705 No backups to clean"));
2845
3369
  return;
2846
3370
  }
2847
- console.log(chalk22.yellow(`The following ${toDelete.length} backup(s) will be deleted:
3371
+ console.log(chalk23.yellow(`The following ${toDelete.length} backup(s) will be deleted:
2848
3372
  `));
2849
3373
  toDelete.forEach((backup) => {
2850
- console.log(chalk22.gray(` - ${backup.name} (${backup.size})`));
3374
+ console.log(chalk23.gray(` - ${backup.name} (${backup.size})`));
2851
3375
  });
2852
- const { confirm } = await prompts5({
3376
+ const { confirm } = await prompts6({
2853
3377
  type: "confirm",
2854
3378
  name: "confirm",
2855
3379
  message: "\nProceed with deletion?",
2856
3380
  initial: false
2857
3381
  });
2858
3382
  if (!confirm) {
2859
- console.log(chalk22.gray("Cancelled"));
3383
+ console.log(chalk23.gray("Cancelled"));
2860
3384
  return;
2861
3385
  }
2862
3386
  const spinner = ora10("Deleting backups...").start();
2863
3387
  try {
2864
3388
  await Promise.all(toDelete.map((backup) => fs4.unlink(backup.path)));
2865
3389
  spinner.succeed("Backups deleted");
2866
- console.log(chalk22.green(`
3390
+ console.log(chalk23.green(`
2867
3391
  \u2705 Deleted ${toDelete.length} backup(s)`));
2868
3392
  } catch (error) {
2869
3393
  spinner.fail("Failed to delete backups");
2870
- console.error(chalk22.red(error instanceof Error ? error.message : "Unknown error"));
3394
+ console.error(chalk23.red(error instanceof Error ? error.message : "Unknown error"));
2871
3395
  process.exit(1);
2872
3396
  }
2873
3397
  }
2874
3398
 
3399
+ // src/commands/db/reindex.ts
3400
+ import { existsSync as existsSync20, readFileSync as readFileSync6, writeFileSync as writeFileSync11, renameSync, copyFileSync } from "fs";
3401
+ import { join as join17 } from "path";
3402
+ import chalk24 from "chalk";
3403
+ import { loadEnv as loadEnv7 } from "@spfn/core/server";
3404
+ function isTimestampPrefix(tag) {
3405
+ const prefix = tag.split("_")[0];
3406
+ return /^\d{5,}$/.test(prefix);
3407
+ }
3408
+ function parseTag(tag) {
3409
+ const underscoreIdx = tag.indexOf("_");
3410
+ if (underscoreIdx === -1) {
3411
+ return { prefix: tag, suffix: "" };
3412
+ }
3413
+ return {
3414
+ prefix: tag.substring(0, underscoreIdx),
3415
+ suffix: tag.substring(underscoreIdx + 1)
3416
+ };
3417
+ }
3418
+ async function dbReindex(options = {}) {
3419
+ loadEnv7();
3420
+ const { getDrizzleConfig } = await import("@spfn/core/db");
3421
+ const config = getDrizzleConfig({ disablePackageDiscovery: true });
3422
+ const outDir = config.out;
3423
+ const journalPath = join17(outDir, "meta", "_journal.json");
3424
+ if (!existsSync20(journalPath)) {
3425
+ console.error(chalk24.red("\u274C No _journal.json found at:"), journalPath);
3426
+ console.log(chalk24.yellow("\u{1F4A1} Run `spfn db generate` first to create migrations"));
3427
+ process.exit(1);
3428
+ }
3429
+ const journal = JSON.parse(readFileSync6(journalPath, "utf-8"));
3430
+ if (journal.entries.length === 0) {
3431
+ console.log(chalk24.yellow("No migration entries found \u2014 nothing to reindex."));
3432
+ return;
3433
+ }
3434
+ const renames = [];
3435
+ const tagUpdates = [];
3436
+ let skipped = 0;
3437
+ for (const entry of journal.entries) {
3438
+ if (isTimestampPrefix(entry.tag)) {
3439
+ skipped++;
3440
+ continue;
3441
+ }
3442
+ const { prefix: oldPrefix, suffix } = parseTag(entry.tag);
3443
+ const newPrefix = String(entry.when);
3444
+ const newTag = suffix ? `${newPrefix}_${suffix}` : newPrefix;
3445
+ const oldSql = join17(outDir, `${entry.tag}.sql`);
3446
+ const newSql = join17(outDir, `${newTag}.sql`);
3447
+ if (existsSync20(oldSql)) {
3448
+ renames.push({ type: "sql", from: oldSql, to: newSql });
3449
+ }
3450
+ const oldSnapshot = join17(outDir, "meta", `${oldPrefix}_snapshot.json`);
3451
+ const newSnapshot = join17(outDir, "meta", `${newPrefix}_snapshot.json`);
3452
+ if (existsSync20(oldSnapshot)) {
3453
+ renames.push({ type: "snapshot", from: oldSnapshot, to: newSnapshot });
3454
+ }
3455
+ tagUpdates.push({ idx: entry.idx, oldTag: entry.tag, newTag });
3456
+ }
3457
+ if (tagUpdates.length === 0) {
3458
+ console.log(chalk24.green("\u2705 All migrations already use timestamp prefix \u2014 nothing to do."));
3459
+ if (skipped > 0) {
3460
+ console.log(chalk24.dim(` (${skipped} entries already timestamp-prefixed)`));
3461
+ }
3462
+ return;
3463
+ }
3464
+ console.log(chalk24.bold("\n\u{1F4CB} Reindex plan:\n"));
3465
+ for (const update of tagUpdates) {
3466
+ console.log(
3467
+ chalk24.dim(` [${update.idx}]`),
3468
+ chalk24.red(update.oldTag),
3469
+ chalk24.dim("\u2192"),
3470
+ chalk24.green(update.newTag)
3471
+ );
3472
+ }
3473
+ console.log(chalk24.dim(`
3474
+ ${renames.length} file(s) to rename, ${tagUpdates.length} journal tag(s) to update`));
3475
+ if (skipped > 0) {
3476
+ console.log(chalk24.dim(` ${skipped} entry/entries already timestamp-prefixed (skipped)`));
3477
+ }
3478
+ if (options.dryRun) {
3479
+ console.log(chalk24.yellow("\n\u{1F50D} Dry run \u2014 no changes applied."));
3480
+ return;
3481
+ }
3482
+ const backupPath = journalPath + ".bak";
3483
+ copyFileSync(journalPath, backupPath);
3484
+ console.log(chalk24.dim(`
3485
+ Backed up journal \u2192 ${backupPath}`));
3486
+ for (const rename of renames) {
3487
+ renameSync(rename.from, rename.to);
3488
+ }
3489
+ for (const update of tagUpdates) {
3490
+ const entry = journal.entries.find((e) => e.idx === update.idx);
3491
+ if (entry) {
3492
+ entry.tag = update.newTag;
3493
+ }
3494
+ }
3495
+ writeFileSync11(journalPath, JSON.stringify(journal, null, 2) + "\n");
3496
+ console.log(chalk24.green(`
3497
+ \u2705 Reindex complete \u2014 ${tagUpdates.length} migration(s) converted to timestamp prefix.`));
3498
+ }
3499
+
2875
3500
  // src/commands/db/index.ts
2876
3501
  var dbCommand = new Command9("db").description("Database management commands (wraps Drizzle Kit)");
2877
3502
  dbCommand.command("generate").alias("g").description("Generate database migrations from schema changes").action(dbGenerate);
2878
- dbCommand.command("push").description("Push schema changes directly to database (no migrations)").action(dbPush);
3503
+ dbCommand.command("push").description("Push schema changes to database (safe mode by default)").option("--force", "Apply all changes including destructive ones").option("--dry-run", "Show changes without applying").action((options) => dbPush(options));
2879
3504
  dbCommand.command("migrate").alias("m").description("Run pending migrations").option("--with-backup", "Create backup before running migrations").action((options) => dbMigrate(options));
2880
3505
  dbCommand.command("studio").description("Open Drizzle Studio (database GUI)").option("-p, --port <port>", "Studio port (auto-finds if in use)").action((options) => dbStudio(options.port ? Number(options.port) : void 0));
2881
3506
  dbCommand.command("drop").description("Drop all database tables (\u26A0\uFE0F dangerous!)").action(dbDrop);
2882
3507
  dbCommand.command("check").description("Check database connection").action(dbCheck);
2883
3508
  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));
3509
+ 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
3510
  dbCommand.command("backup:list").description("List all database backups").action(dbBackupList);
2886
3511
  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));
3512
+ dbCommand.command("reindex").description("Convert migration files from sequential to timestamp prefix").option("--dry-run", "Show changes without applying").action((options) => dbReindex(options));
2887
3513
 
2888
3514
  // src/commands/add.ts
2889
3515
  import { Command as Command10 } from "commander";
2890
- import { existsSync as existsSync19, readFileSync as readFileSync6 } from "fs";
2891
- import { join as join16 } from "path";
3516
+ import { existsSync as existsSync21, readFileSync as readFileSync7 } from "fs";
3517
+ import { join as join18 } from "path";
2892
3518
  import { exec as exec2 } from "child_process";
2893
3519
  import { promisify as promisify2 } from "util";
2894
- import chalk23 from "chalk";
3520
+ import chalk25 from "chalk";
2895
3521
  import ora11 from "ora";
2896
3522
  var execAsync2 = promisify2(exec2);
2897
3523
  async function addPackage(packageName) {
2898
3524
  if (!packageName.includes("/")) {
2899
- console.error(chalk23.red("\u274C Please specify full package name"));
2900
- console.log(chalk23.yellow("\n\u{1F4A1} Examples:"));
2901
- console.log(chalk23.gray(" pnpm spfn add @spfn/cms"));
2902
- console.log(chalk23.gray(" pnpm spfn add @mycompany/spfn-analytics"));
3525
+ console.error(chalk25.red("\u274C Please specify full package name"));
3526
+ console.log(chalk25.yellow("\n\u{1F4A1} Examples:"));
3527
+ console.log(chalk25.gray(" pnpm spfn add @spfn/cms"));
3528
+ console.log(chalk25.gray(" pnpm spfn add @mycompany/spfn-analytics"));
2903
3529
  process.exit(1);
2904
3530
  }
2905
- console.log(chalk23.blue(`
3531
+ console.log(chalk25.blue(`
2906
3532
  \u{1F4E6} Setting up ${packageName}...
2907
3533
  `));
2908
3534
  try {
2909
- const pkgPath = join16(process.cwd(), "node_modules", ...packageName.split("/"));
2910
- const pkgJsonPath = join16(pkgPath, "package.json");
2911
- if (!existsSync19(pkgJsonPath)) {
3535
+ const pkgPath = join18(process.cwd(), "node_modules", ...packageName.split("/"));
3536
+ const pkgJsonPath = join18(pkgPath, "package.json");
3537
+ if (!existsSync21(pkgJsonPath)) {
2912
3538
  const installSpinner = ora11("Installing package...").start();
2913
3539
  try {
2914
3540
  await execAsync2(`pnpm add ${packageName}`);
@@ -2918,21 +3544,21 @@ async function addPackage(packageName) {
2918
3544
  throw error;
2919
3545
  }
2920
3546
  } else {
2921
- console.log(chalk23.gray("\u2713 Package already installed (using local version)\n"));
3547
+ console.log(chalk25.gray("\u2713 Package already installed (using local version)\n"));
2922
3548
  }
2923
- if (!existsSync19(pkgJsonPath)) {
3549
+ if (!existsSync21(pkgJsonPath)) {
2924
3550
  throw new Error(`Package ${packageName} not found after installation`);
2925
3551
  }
2926
- const pkgJson = JSON.parse(readFileSync6(pkgJsonPath, "utf-8"));
3552
+ const pkgJson = JSON.parse(readFileSync7(pkgJsonPath, "utf-8"));
2927
3553
  if (pkgJson.spfn?.migrations) {
2928
- console.log(chalk23.blue(`
3554
+ console.log(chalk25.blue(`
2929
3555
  \u{1F5C4}\uFE0F Setting up database for ${packageName}...
2930
3556
  `));
2931
- const { env: env6 } = await import("@spfn/core/config");
2932
- if (!env6.DATABASE_URL) {
2933
- console.log(chalk23.yellow("\u26A0\uFE0F DATABASE_URL not found"));
2934
- console.log(chalk23.gray("Skipping database setup. Run migrations manually when ready:\n"));
2935
- console.log(chalk23.gray(` pnpm spfn db push
3557
+ const { env: env7 } = await import("@spfn/core/config");
3558
+ if (!env7.DATABASE_URL) {
3559
+ console.log(chalk25.yellow("\u26A0\uFE0F DATABASE_URL not found"));
3560
+ console.log(chalk25.gray("Skipping database setup. Run migrations manually when ready:\n"));
3561
+ console.log(chalk25.gray(` pnpm spfn db push
2936
3562
  `));
2937
3563
  } else {
2938
3564
  const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
@@ -2948,25 +3574,25 @@ async function addPackage(packageName) {
2948
3574
  throw error;
2949
3575
  }
2950
3576
  } else {
2951
- console.log(chalk23.gray("\u2139\uFE0F No migrations found for this package"));
3577
+ console.log(chalk25.gray("\u2139\uFE0F No migrations found for this package"));
2952
3578
  }
2953
3579
  }
2954
3580
  } else {
2955
- console.log(chalk23.gray("\n\u2139\uFE0F No database migrations to apply"));
3581
+ console.log(chalk25.gray("\n\u2139\uFE0F No database migrations to apply"));
2956
3582
  }
2957
- console.log(chalk23.green(`
3583
+ console.log(chalk25.green(`
2958
3584
  \u2705 ${packageName} installed successfully!
2959
3585
  `));
2960
3586
  if (pkgJson.spfn?.setupMessage) {
2961
- console.log(chalk23.cyan("\u{1F4DA} Setup Guide:"));
3587
+ console.log(chalk25.cyan("\u{1F4DA} Setup Guide:"));
2962
3588
  console.log(pkgJson.spfn.setupMessage);
2963
3589
  console.log();
2964
3590
  }
2965
3591
  } catch (error) {
2966
- console.error(chalk23.red(`
3592
+ console.error(chalk25.red(`
2967
3593
  \u274C Failed to install ${packageName}
2968
3594
  `));
2969
- console.error(chalk23.red(error instanceof Error ? error.message : "Unknown error"));
3595
+ console.error(chalk25.red(error instanceof Error ? error.message : "Unknown error"));
2970
3596
  process.exit(1);
2971
3597
  }
2972
3598
  }
@@ -2976,16 +3602,16 @@ var addCommand = new Command10("add").description("Install and set up SPFN ecosy
2976
3602
  init_logger();
2977
3603
  import { Command as Command11 } from "commander";
2978
3604
  import ora12 from "ora";
2979
- import { join as join25 } from "path";
2980
- import { existsSync as existsSync22 } from "fs";
2981
- import chalk25 from "chalk";
3605
+ import { join as join27 } from "path";
3606
+ import { existsSync as existsSync24 } from "fs";
3607
+ import chalk27 from "chalk";
2982
3608
 
2983
3609
  // src/commands/generate/prompts.ts
2984
3610
  init_logger();
2985
- import prompts6 from "prompts";
2986
- import chalk24 from "chalk";
3611
+ import prompts7 from "prompts";
3612
+ import chalk26 from "chalk";
2987
3613
  async function promptScope() {
2988
- const response = await prompts6({
3614
+ const response = await prompts7({
2989
3615
  type: "text",
2990
3616
  name: "scope",
2991
3617
  message: "NPM scope (e.g., @mycompany, @username):",
@@ -3007,7 +3633,7 @@ async function promptScope() {
3007
3633
  return response.scope;
3008
3634
  }
3009
3635
  async function promptFunctionName() {
3010
- const response = await prompts6({
3636
+ const response = await prompts7({
3011
3637
  type: "text",
3012
3638
  name: "fnName",
3013
3639
  message: "Function name:",
@@ -3028,7 +3654,7 @@ async function promptFunctionName() {
3028
3654
  return response.fnName;
3029
3655
  }
3030
3656
  async function promptDescription(fnName) {
3031
- const response = await prompts6({
3657
+ const response = await prompts7({
3032
3658
  type: "text",
3033
3659
  name: "description",
3034
3660
  message: "Function description:",
@@ -3037,7 +3663,7 @@ async function promptDescription(fnName) {
3037
3663
  return response.description || "A description of what this module does";
3038
3664
  }
3039
3665
  async function promptEntities() {
3040
- const response = await prompts6({
3666
+ const response = await prompts7({
3041
3667
  type: "list",
3042
3668
  name: "entities",
3043
3669
  message: "Entity names (comma-separated, press enter to skip):",
@@ -3049,14 +3675,14 @@ async function promptEntities() {
3049
3675
  async function confirmConfiguration(config) {
3050
3676
  const { scope, fnName, description, entities, enableCache, enableRoutes } = config;
3051
3677
  console.log("");
3052
- logger.info(chalk24.bold("\u26A1 Function Configuration:"));
3053
- console.log(` ${chalk24.gray("Package:")} ${chalk24.cyan(`${scope}/${fnName}`)}`);
3054
- console.log(` ${chalk24.gray("Description:")} ${description}`);
3055
- console.log(` ${chalk24.gray("Entities:")} ${entities.length > 0 ? entities.join(", ") : chalk24.gray("none")}`);
3056
- console.log(` ${chalk24.gray("Cache:")} ${enableCache ? chalk24.green("yes") : chalk24.gray("no")}`);
3057
- console.log(` ${chalk24.gray("Routes:")} ${enableRoutes ? chalk24.green("yes") : chalk24.gray("no")}`);
3678
+ logger.info(chalk26.bold("\u26A1 Function Configuration:"));
3679
+ console.log(` ${chalk26.gray("Package:")} ${chalk26.cyan(`${scope}/${fnName}`)}`);
3680
+ console.log(` ${chalk26.gray("Description:")} ${description}`);
3681
+ console.log(` ${chalk26.gray("Entities:")} ${entities.length > 0 ? entities.join(", ") : chalk26.gray("none")}`);
3682
+ console.log(` ${chalk26.gray("Cache:")} ${enableCache ? chalk26.green("yes") : chalk26.gray("no")}`);
3683
+ console.log(` ${chalk26.gray("Routes:")} ${enableRoutes ? chalk26.green("yes") : chalk26.gray("no")}`);
3058
3684
  console.log("");
3059
- const { confirmed } = await prompts6({
3685
+ const { confirmed } = await prompts7({
3060
3686
  type: "confirm",
3061
3687
  name: "confirmed",
3062
3688
  message: "Create function?",
@@ -3070,12 +3696,12 @@ async function confirmConfiguration(config) {
3070
3696
  }
3071
3697
 
3072
3698
  // src/commands/generate/generators/structure.ts
3073
- import { join as join24 } from "path";
3074
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync17 } from "fs";
3699
+ import { join as join26 } from "path";
3700
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync18 } from "fs";
3075
3701
 
3076
3702
  // src/commands/generate/generators/config.ts
3077
- import { join as join18 } from "path";
3078
- import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync3 } from "fs";
3703
+ import { join as join20 } from "path";
3704
+ import { writeFileSync as writeFileSync12, mkdirSync as mkdirSync3 } from "fs";
3079
3705
 
3080
3706
  // src/commands/generate/string-utils.ts
3081
3707
  function toPascalCase(str) {
@@ -3104,30 +3730,30 @@ function toSafeSchemaName(str) {
3104
3730
  }
3105
3731
 
3106
3732
  // 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";
3733
+ import { readFileSync as readFileSync8, existsSync as existsSync22 } from "fs";
3734
+ import { join as join19, dirname as dirname2 } from "path";
3109
3735
  import { fileURLToPath as fileURLToPath2 } from "url";
3110
3736
  function findTemplatesPath2() {
3111
3737
  const __filename = fileURLToPath2(import.meta.url);
3112
3738
  const __dirname2 = dirname2(__filename);
3113
- const distPath = join17(__dirname2, "commands", "generate", "templates");
3114
- if (existsSync20(distPath)) {
3739
+ const distPath = join19(__dirname2, "commands", "generate", "templates");
3740
+ if (existsSync22(distPath)) {
3115
3741
  return distPath;
3116
3742
  }
3117
- const sameDirPath = join17(__dirname2, "templates");
3118
- if (existsSync20(sameDirPath)) {
3743
+ const sameDirPath = join19(__dirname2, "templates");
3744
+ if (existsSync22(sameDirPath)) {
3119
3745
  return sameDirPath;
3120
3746
  }
3121
- const srcPath = join17(__dirname2, "..", "..", "src", "commands", "generate", "templates");
3122
- if (existsSync20(srcPath)) {
3747
+ const srcPath = join19(__dirname2, "..", "..", "src", "commands", "generate", "templates");
3748
+ if (existsSync22(srcPath)) {
3123
3749
  return srcPath;
3124
3750
  }
3125
3751
  throw new Error(`Templates directory not found. Tried: ${distPath}, ${sameDirPath}, ${srcPath}`);
3126
3752
  }
3127
3753
  function loadTemplate(templateName, variables) {
3128
3754
  const templatesDir = findTemplatesPath2();
3129
- const templatePath = join17(templatesDir, `${templateName}.template`);
3130
- let content = readFileSync7(templatePath, "utf-8");
3755
+ const templatePath = join19(templatesDir, `${templateName}.template`);
3756
+ let content = readFileSync8(templatePath, "utf-8");
3131
3757
  for (const [key, value] of Object.entries(variables)) {
3132
3758
  const regex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
3133
3759
  content = content.replace(regex, value);
@@ -3229,8 +3855,8 @@ function generatePackageJson(fnDir, scope, fnName, description) {
3229
3855
  vitest: "^4.0.6"
3230
3856
  }
3231
3857
  };
3232
- writeFileSync11(
3233
- join18(fnDir, "package.json"),
3858
+ writeFileSync12(
3859
+ join20(fnDir, "package.json"),
3234
3860
  JSON.stringify(content, null, 4) + "\n"
3235
3861
  );
3236
3862
  }
@@ -3263,8 +3889,8 @@ function generateTsConfig(fnDir) {
3263
3889
  include: ["src/**/*"],
3264
3890
  exclude: ["node_modules", "dist", "**/*.test.ts", "**/__tests__/**"]
3265
3891
  };
3266
- writeFileSync11(
3267
- join18(fnDir, "tsconfig.json"),
3892
+ writeFileSync12(
3893
+ join20(fnDir, "tsconfig.json"),
3268
3894
  JSON.stringify(content, null, 4) + "\n"
3269
3895
  );
3270
3896
  }
@@ -3340,7 +3966,7 @@ export default defineConfig({
3340
3966
  ],
3341
3967
  });
3342
3968
  `;
3343
- writeFileSync11(join18(fnDir, "tsup.config.ts"), content);
3969
+ writeFileSync12(join20(fnDir, "tsup.config.ts"), content);
3344
3970
  }
3345
3971
  function generateDrizzleConfig(fnDir, scope, fnName) {
3346
3972
  const schemaName = `spfn_${toSnakeCase(fnName)}`;
@@ -3360,7 +3986,7 @@ export default defineConfig({
3360
3986
  schemaFilter: ['${schemaName}'], // Only generate for ${fnName} schema
3361
3987
  });
3362
3988
  `;
3363
- writeFileSync11(join18(fnDir, "drizzle.config.ts"), content);
3989
+ writeFileSync12(join20(fnDir, "drizzle.config.ts"), content);
3364
3990
  }
3365
3991
  function generateExampleGenerator(fnDir, scope, fnName) {
3366
3992
  const pascalName = toPascalCase(fnName);
@@ -3429,10 +4055,10 @@ export const moduleName = '${fnName}';
3429
4055
  };
3430
4056
  }
3431
4057
  `;
3432
- const generatorsDir = join18(fnDir, "src/server/generators");
4058
+ const generatorsDir = join20(fnDir, "src/server/generators");
3433
4059
  mkdirSync3(generatorsDir, { recursive: true });
3434
- writeFileSync11(
3435
- join18(generatorsDir, "example-generator.ts"),
4060
+ writeFileSync12(
4061
+ join20(generatorsDir, "example-generator.ts"),
3436
4062
  content
3437
4063
  );
3438
4064
  const indexContent = `/**
@@ -3446,8 +4072,8 @@ export const moduleName = '${fnName}';
3446
4072
 
3447
4073
  export { create${pascalName}ExampleGenerator } from './example-generator.js';
3448
4074
  `;
3449
- writeFileSync11(
3450
- join18(generatorsDir, "index.ts"),
4075
+ writeFileSync12(
4076
+ join20(generatorsDir, "index.ts"),
3451
4077
  indexContent
3452
4078
  );
3453
4079
  }
@@ -3936,15 +4562,15 @@ Contributions are welcome! Please follow the development workflow above.
3936
4562
 
3937
4563
  MIT
3938
4564
  `;
3939
- writeFileSync11(join18(fnDir, "README.md"), content);
4565
+ writeFileSync12(join20(fnDir, "README.md"), content);
3940
4566
  }
3941
4567
 
3942
4568
  // src/commands/generate/generators/entity.ts
3943
- import { join as join19 } from "path";
3944
- import { writeFileSync as writeFileSync12, existsSync as existsSync21 } from "fs";
4569
+ import { join as join21 } from "path";
4570
+ import { writeFileSync as writeFileSync13, existsSync as existsSync23 } from "fs";
3945
4571
  function generateSchema(fnDir, scope, fnName) {
3946
- const schemaFilePath = join19(fnDir, "src/server/entities/schema.ts");
3947
- if (existsSync21(schemaFilePath)) {
4572
+ const schemaFilePath = join21(fnDir, "src/server/entities/schema.ts");
4573
+ if (existsSync23(schemaFilePath)) {
3948
4574
  return;
3949
4575
  }
3950
4576
  const packageName = `${scope}/${fnName}`;
@@ -3955,7 +4581,7 @@ function generateSchema(fnDir, scope, fnName) {
3955
4581
  PACKAGE_NAME: packageName,
3956
4582
  SCHEMA_VAR_NAME: schemaVarName
3957
4583
  });
3958
- writeFileSync12(schemaFilePath, content);
4584
+ writeFileSync13(schemaFilePath, content);
3959
4585
  }
3960
4586
  function generateEntity(fnDir, scope, fnName, entityName) {
3961
4587
  const safeScope = toSafeSchemaName(scope);
@@ -3974,8 +4600,8 @@ function generateEntity(fnDir, scope, fnName, entityName) {
3974
4600
  SCHEMA_VAR_NAME: schemaVarName,
3975
4601
  SCHEMA_FILE_NAME: schemaFileName
3976
4602
  });
3977
- writeFileSync12(
3978
- join19(fnDir, `src/server/entities/${toKebabCase(entityName)}.ts`),
4603
+ writeFileSync13(
4604
+ join21(fnDir, `src/server/entities/${toKebabCase(entityName)}.ts`),
3979
4605
  content
3980
4606
  );
3981
4607
  }
@@ -3983,12 +4609,12 @@ function generateEntitiesIndex(fnDir, entities) {
3983
4609
  const schemaExport = `export * from './schema';`;
3984
4610
  const entityExports = entities.map((entity) => `export * from './${toKebabCase(entity)}';`).join("\n");
3985
4611
  const content = [schemaExport, entityExports].filter(Boolean).join("\n");
3986
- writeFileSync12(join19(fnDir, "src/server/entities/index.ts"), content + "\n");
4612
+ writeFileSync13(join21(fnDir, "src/server/entities/index.ts"), content + "\n");
3987
4613
  }
3988
4614
 
3989
4615
  // src/commands/generate/generators/repository.ts
3990
- import { join as join20 } from "path";
3991
- import { writeFileSync as writeFileSync13 } from "fs";
4616
+ import { join as join22 } from "path";
4617
+ import { writeFileSync as writeFileSync14 } from "fs";
3992
4618
  function generateRepository(fnDir, entityName) {
3993
4619
  const pascalName = toPascalCase(entityName);
3994
4620
  const repoName = `${entityName}Repository`;
@@ -3997,19 +4623,19 @@ function generateRepository(fnDir, entityName) {
3997
4623
  ENTITY_NAME: entityName,
3998
4624
  REPO_NAME: repoName
3999
4625
  });
4000
- writeFileSync13(
4001
- join20(fnDir, `src/server/repositories/${toKebabCase(entityName)}.repository.ts`),
4626
+ writeFileSync14(
4627
+ join22(fnDir, `src/server/repositories/${toKebabCase(entityName)}.repository.ts`),
4002
4628
  content
4003
4629
  );
4004
4630
  }
4005
4631
  function generateRepositoriesIndex(fnDir, entities) {
4006
4632
  const exports = entities.map((entity) => `export * from './${toKebabCase(entity)}.repository';`).join("\n");
4007
- writeFileSync13(join20(fnDir, "src/server/repositories/index.ts"), exports + "\n");
4633
+ writeFileSync14(join22(fnDir, "src/server/repositories/index.ts"), exports + "\n");
4008
4634
  }
4009
4635
 
4010
4636
  // src/commands/generate/generators/route.ts
4011
- import { join as join21 } from "path";
4012
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync14 } from "fs";
4637
+ import { join as join23 } from "path";
4638
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync15 } from "fs";
4013
4639
  function generateRoute(fnDir, entityName) {
4014
4640
  const pascalName = toPascalCase(entityName);
4015
4641
  const repoName = `${entityName}Repository`;
@@ -4020,29 +4646,29 @@ function generateRoute(fnDir, entityName) {
4020
4646
  REPO_NAME: repoName,
4021
4647
  KEBAB_NAME: kebabName
4022
4648
  });
4023
- const routeDir = join21(fnDir, `src/server/routes/${kebabName}`);
4649
+ const routeDir = join23(fnDir, `src/server/routes/${kebabName}`);
4024
4650
  mkdirSync4(routeDir, { recursive: true });
4025
- writeFileSync14(join21(routeDir, "index.ts"), content);
4651
+ writeFileSync15(join23(routeDir, "index.ts"), content);
4026
4652
  }
4027
4653
 
4028
4654
  // src/commands/generate/generators/contract.ts
4029
- import { join as join22 } from "path";
4030
- import { writeFileSync as writeFileSync15 } from "fs";
4655
+ import { join as join24 } from "path";
4656
+ import { writeFileSync as writeFileSync16 } from "fs";
4031
4657
  function generateContract(fnDir, entityName) {
4032
4658
  const pascalName = toPascalCase(entityName);
4033
4659
  const content = loadTemplate("contract", {
4034
4660
  PASCAL_NAME: pascalName,
4035
4661
  ENTITY_NAME: entityName
4036
4662
  });
4037
- writeFileSync15(
4038
- join22(fnDir, `src/lib/contracts/${toKebabCase(entityName)}.ts`),
4663
+ writeFileSync16(
4664
+ join24(fnDir, `src/lib/contracts/${toKebabCase(entityName)}.ts`),
4039
4665
  content
4040
4666
  );
4041
4667
  }
4042
4668
 
4043
4669
  // src/commands/generate/generators/index-files.ts
4044
- import { join as join23 } from "path";
4045
- import { writeFileSync as writeFileSync16 } from "fs";
4670
+ import { join as join25 } from "path";
4671
+ import { writeFileSync as writeFileSync17 } from "fs";
4046
4672
  function generateMainIndex(fnDir, fnName) {
4047
4673
  const content = `/**
4048
4674
  * @spfn/${fnName}
@@ -4068,7 +4694,7 @@ export * from '@/lib/types/index';
4068
4694
 
4069
4695
  export * from '@/server/entities/index';
4070
4696
  `;
4071
- writeFileSync16(join23(fnDir, "src/index.ts"), content);
4697
+ writeFileSync17(join25(fnDir, "src/index.ts"), content);
4072
4698
  }
4073
4699
  function generateServerIndex(fnDir) {
4074
4700
  const content = `/**
@@ -4103,7 +4729,7 @@ export * from '@/server/repositories/index';
4103
4729
 
4104
4730
  // TODO: Export helpers here
4105
4731
  `;
4106
- writeFileSync16(join23(fnDir, "src/server.ts"), content);
4732
+ writeFileSync17(join25(fnDir, "src/server.ts"), content);
4107
4733
  }
4108
4734
  function generateClientIndex(fnDir) {
4109
4735
  const content = `/**
@@ -4136,7 +4762,7 @@ export * from './client/store';
4136
4762
 
4137
4763
  export * from './client/components';
4138
4764
  `;
4139
- writeFileSync16(join23(fnDir, "src/client.ts"), content);
4765
+ writeFileSync17(join25(fnDir, "src/client.ts"), content);
4140
4766
  }
4141
4767
  function generateTypesFile(fnDir, fnName) {
4142
4768
  const content = `/**
@@ -4148,7 +4774,7 @@ function generateTypesFile(fnDir, fnName) {
4148
4774
 
4149
4775
  export * from '@/lib/types/index';
4150
4776
  `;
4151
- writeFileSync16(join23(fnDir, "src/types.ts"), content);
4777
+ writeFileSync17(join25(fnDir, "src/types.ts"), content);
4152
4778
  }
4153
4779
 
4154
4780
  // src/commands/generate/generators/structure.ts
@@ -4170,7 +4796,7 @@ async function generateFunctionStructure(options) {
4170
4796
  "src/client/store",
4171
4797
  "src/client/components"
4172
4798
  ];
4173
- dirs.forEach((dir) => mkdirSync5(join24(fnDir, dir), { recursive: true }));
4799
+ dirs.forEach((dir) => mkdirSync5(join26(fnDir, dir), { recursive: true }));
4174
4800
  generatePackageJson(fnDir, scope, fnName, description);
4175
4801
  generateTsConfig(fnDir);
4176
4802
  generateTsupConfig(fnDir);
@@ -4190,15 +4816,15 @@ async function generateFunctionStructure(options) {
4190
4816
  generateEntitiesIndex(fnDir, entities);
4191
4817
  generateRepositoriesIndex(fnDir, entities);
4192
4818
  } 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");
4819
+ writeFileSync18(join26(fnDir, "src/server/entities/index.ts"), "// Export your entities here\nexport {}\n");
4820
+ writeFileSync18(join26(fnDir, "src/server/repositories/index.ts"), "// Export your repositories here\nexport {}\n");
4821
+ }
4822
+ writeFileSync18(join26(fnDir, "src/client/hooks/index.ts"), "/**\n * Client Hooks\n */\n\n// TODO: Add hooks (e.g., useAuth, useData, etc.)\nexport {}\n");
4823
+ writeFileSync18(join26(fnDir, "src/client/store/index.ts"), "/**\n * Client Store\n */\n\n// TODO: Add Zustand store if needed\nexport {}\n");
4824
+ writeFileSync18(join26(fnDir, "src/client/components/index.ts"), "/**\n * Client Components\n */\n\n// TODO: Add React components\nexport {}\n");
4825
+ writeFileSync18(join26(fnDir, "src/client/index.ts"), "/**\n * Client Module Entry\n */\n\nexport * from './hooks';\nexport * from './store';\nexport * from './components';\n");
4826
+ writeFileSync18(join26(fnDir, "src/lib/types/index.ts"), "/**\n * Shared Type Definitions\n */\n\n// Add your shared types here\nexport {}\n");
4827
+ writeFileSync18(join26(fnDir, "src/lib/contracts/index.ts"), "/**\n * API Contracts\n */\n\n// Export your contracts here\nexport {}\n");
4202
4828
  generateMainIndex(fnDir, fnName);
4203
4829
  generateServerIndex(fnDir);
4204
4830
  generateClientIndex(fnDir);
@@ -4222,8 +4848,8 @@ async function generateFunction(name, options) {
4222
4848
  logger.error("Function name is required");
4223
4849
  process.exit(1);
4224
4850
  }
4225
- const fnDir = join25(cwd, fnName);
4226
- if (existsSync22(fnDir)) {
4851
+ const fnDir = join27(cwd, fnName);
4852
+ if (existsSync24(fnDir)) {
4227
4853
  logger.error(`Directory ${fnName} already exists at ${fnDir}`);
4228
4854
  process.exit(1);
4229
4855
  }
@@ -4268,13 +4894,13 @@ async function generateFunction(name, options) {
4268
4894
  });
4269
4895
  spinner.succeed("Function structure generated");
4270
4896
  console.log("");
4271
- logger.success(`\u2728 Package ${chalk25.cyan(`${scope}/${fnName}`)} created successfully!
4897
+ logger.success(`\u2728 Package ${chalk27.cyan(`${scope}/${fnName}`)} created successfully!
4272
4898
  `);
4273
- logger.info(chalk25.bold("\u{1F4DA} Next steps:"));
4274
- console.log(` ${chalk25.gray("1.")} cd ${fnName}`);
4275
- console.log(` ${chalk25.gray("2.")} pnpm install ${chalk25.dim("(in monorepo root)")}`);
4276
- console.log(` ${chalk25.gray("3.")} pnpm build`);
4277
- console.log(` ${chalk25.gray("4.")} ${chalk25.dim("Use the package in your app")}`);
4899
+ logger.info(chalk27.bold("\u{1F4DA} Next steps:"));
4900
+ console.log(` ${chalk27.gray("1.")} cd ${fnName}`);
4901
+ console.log(` ${chalk27.gray("2.")} pnpm install ${chalk27.dim("(in monorepo root)")}`);
4902
+ console.log(` ${chalk27.gray("3.")} pnpm build`);
4903
+ console.log(` ${chalk27.gray("4.")} ${chalk27.dim("Use the package in your app")}`);
4278
4904
  console.log("");
4279
4905
  } catch (error) {
4280
4906
  spinner.fail("Failed to generate function");
@@ -4287,14 +4913,30 @@ generateCommand.command("fn").description("Generate a new SPFN function module")
4287
4913
 
4288
4914
  // src/commands/env.ts
4289
4915
  import { Command as Command12 } from "commander";
4290
- import chalk26 from "chalk";
4291
- import { existsSync as existsSync23, readFileSync as readFileSync8, writeFileSync as writeFileSync18 } from "fs";
4916
+ import chalk28 from "chalk";
4917
+ import { existsSync as existsSync25, readFileSync as readFileSync9, writeFileSync as writeFileSync19 } from "fs";
4292
4918
  import { resolve } from "path";
4293
4919
  import { parse } from "dotenv";
4294
- var ENV_FILES = {
4920
+ var VALID_ENVS = ["local", "development", "staging", "production", "test"];
4921
+ var BASE_ENV_FILES = {
4295
4922
  nextjs: [".env", ".env.local"],
4296
4923
  server: [".env.server", ".env.server.local"]
4297
4924
  };
4925
+ function getEnvFilesForEnvironment(nodeEnv) {
4926
+ const files = [".env"];
4927
+ if (nodeEnv) {
4928
+ files.push(`.env.${nodeEnv}`);
4929
+ }
4930
+ if (nodeEnv !== "test") {
4931
+ files.push(".env.local");
4932
+ }
4933
+ if (nodeEnv) {
4934
+ files.push(`.env.${nodeEnv}.local`);
4935
+ }
4936
+ files.push(".env.server");
4937
+ files.push(".env.server.local");
4938
+ return files;
4939
+ }
4298
4940
  function getTargetFile(schema) {
4299
4941
  const isNextjs = schema.nextjs ?? schema.key?.startsWith("NEXT_PUBLIC_");
4300
4942
  if (isNextjs) {
@@ -4320,27 +4962,26 @@ async function loadEnvSchema(packageName) {
4320
4962
  }
4321
4963
  function formatType(type) {
4322
4964
  const typeColors = {
4323
- string: chalk26.green,
4324
- number: chalk26.blue,
4325
- boolean: chalk26.yellow,
4326
- url: chalk26.cyan,
4327
- enum: chalk26.magenta,
4328
- json: chalk26.red
4965
+ string: chalk28.green,
4966
+ number: chalk28.blue,
4967
+ boolean: chalk28.yellow,
4968
+ url: chalk28.cyan,
4969
+ enum: chalk28.magenta,
4970
+ json: chalk28.red
4329
4971
  };
4330
- const colorFn = typeColors[type] || chalk26.white;
4331
- return colorFn(type);
4972
+ return (typeColors[type] || chalk28.white)(type);
4332
4973
  }
4333
4974
  function formatDefault(value, type) {
4334
4975
  if (value === void 0) {
4335
- return chalk26.dim("(none)");
4976
+ return chalk28.dim("(none)");
4336
4977
  }
4337
4978
  if (type === "string" || type === "url") {
4338
- return chalk26.green(`"${value}"`);
4979
+ return chalk28.green(`"${value}"`);
4339
4980
  }
4340
4981
  if (type === "boolean") {
4341
- return value ? chalk26.green("true") : chalk26.red("false");
4982
+ return value ? chalk28.green("true") : chalk28.red("false");
4342
4983
  }
4343
- return chalk26.cyan(String(value));
4984
+ return chalk28.cyan(String(value));
4344
4985
  }
4345
4986
  async function listEnvVars(options) {
4346
4987
  const packageName = options.package || "@spfn/core";
@@ -4354,28 +4995,28 @@ async function listEnvVars(options) {
4354
4995
  acc[target].push([key, schema]);
4355
4996
  return acc;
4356
4997
  }, {});
4357
- console.log(chalk26.blue.bold(`
4998
+ console.log(chalk28.blue.bold(`
4358
4999
  \u{1F4CB} Environment Variables by File (${packageName})
4359
5000
  `));
4360
5001
  for (const [file, vars] of Object.entries(grouped)) {
4361
- console.log(chalk26.bold.magenta(`
5002
+ console.log(chalk28.bold.magenta(`
4362
5003
  ${file}`));
4363
- console.log(chalk26.dim("\u2500".repeat(50)));
5004
+ console.log(chalk28.dim("\u2500".repeat(50)));
4364
5005
  for (const [key, schema] of vars) {
4365
5006
  printEnvVar(key, schema);
4366
5007
  }
4367
5008
  }
4368
5009
  } else {
4369
- console.log(chalk26.blue.bold(`
5010
+ console.log(chalk28.blue.bold(`
4370
5011
  \u{1F4CB} Environment Variables (${packageName})
4371
5012
  `));
4372
5013
  for (const [key, schema] of allVars) {
4373
5014
  printEnvVar(key, schema, true);
4374
5015
  }
4375
5016
  }
4376
- console.log(chalk26.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
5017
+ console.log(chalk28.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
4377
5018
  } catch (error) {
4378
- console.error(chalk26.red(`
5019
+ console.error(chalk28.red(`
4379
5020
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
4380
5021
  `));
4381
5022
  process.exit(1);
@@ -4383,17 +5024,17 @@ ${file}`));
4383
5024
  }
4384
5025
  function printEnvVar(key, schema, showFile = false) {
4385
5026
  const typeStr = formatType(schema.type);
4386
- const requiredStr = schema.required || schema.default !== void 0 ? chalk26.red("[required]") : chalk26.dim("[optional]");
4387
- const sensitiveStr = schema.sensitive ? chalk26.yellow(" [sensitive]") : "";
4388
- const fileStr = showFile ? chalk26.dim(` \u2192 ${getTargetFile(schema)}`) : "";
4389
- console.log(`${chalk26.bold.cyan(key)} ${chalk26.dim("(")}${typeStr}${chalk26.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
4390
- console.log(` ${chalk26.dim(schema.description)}`);
5027
+ const requiredStr = schema.required || schema.default !== void 0 ? chalk28.red("[required]") : chalk28.dim("[optional]");
5028
+ const sensitiveStr = schema.sensitive ? chalk28.yellow(" [sensitive]") : "";
5029
+ const fileStr = showFile ? chalk28.dim(` \u2192 ${getTargetFile(schema)}`) : "";
5030
+ console.log(`${chalk28.bold.cyan(key)} ${chalk28.dim("(")}${typeStr}${chalk28.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
5031
+ console.log(` ${chalk28.dim(schema.description)}`);
4391
5032
  if (schema.default !== void 0) {
4392
- console.log(` ${chalk26.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
5033
+ console.log(` ${chalk28.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
4393
5034
  }
4394
5035
  if (schema.examples && schema.examples.length > 0) {
4395
5036
  const exampleStr = schema.examples.map((ex) => formatDefault(ex, schema.type)).join(", ");
4396
- console.log(` ${chalk26.dim("Examples:")} ${exampleStr}`);
5037
+ console.log(` ${chalk28.dim("Examples:")} ${exampleStr}`);
4397
5038
  }
4398
5039
  console.log();
4399
5040
  }
@@ -4401,7 +5042,7 @@ async function showEnvStats(options) {
4401
5042
  const packageName = options.package || "@spfn/core";
4402
5043
  try {
4403
5044
  const envSchema = await loadEnvSchema(packageName);
4404
- console.log(chalk26.blue.bold(`
5045
+ console.log(chalk28.blue.bold(`
4405
5046
  \u{1F4CA} Environment Variable Statistics (${packageName})
4406
5047
  `));
4407
5048
  const allVars = Object.entries(envSchema);
@@ -4423,24 +5064,24 @@ async function showEnvStats(options) {
4423
5064
  acc[file] = (acc[file] || 0) + 1;
4424
5065
  return acc;
4425
5066
  }, {});
4426
- console.log(`${chalk26.bold("Total variables:")} ${chalk26.cyan(allVars.length)}`);
4427
- console.log(`${chalk26.bold("Required:")} ${chalk26.red(required.length)}`);
4428
- console.log(`${chalk26.bold("Optional:")} ${chalk26.dim(optional.length)}`);
4429
- console.log(`${chalk26.bold("Sensitive:")} ${chalk26.yellow(sensitive.length)}`);
4430
- console.log(chalk26.bold("\nBy Target:"));
4431
- console.log(` ${chalk26.blue("Next.js accessible:")} ${chalk26.cyan(nextjsVars.length)}`);
4432
- console.log(` ${chalk26.magenta("SPFN server only:")} ${chalk26.cyan(serverOnlyVars.length)}`);
4433
- console.log(chalk26.bold("\nBy File:"));
5067
+ console.log(`${chalk28.bold("Total variables:")} ${chalk28.cyan(allVars.length)}`);
5068
+ console.log(`${chalk28.bold("Required:")} ${chalk28.red(required.length)}`);
5069
+ console.log(`${chalk28.bold("Optional:")} ${chalk28.dim(optional.length)}`);
5070
+ console.log(`${chalk28.bold("Sensitive:")} ${chalk28.yellow(sensitive.length)}`);
5071
+ console.log(chalk28.bold("\nBy Target:"));
5072
+ console.log(` ${chalk28.blue("Next.js accessible:")} ${chalk28.cyan(nextjsVars.length)}`);
5073
+ console.log(` ${chalk28.magenta("SPFN server only:")} ${chalk28.cyan(serverOnlyVars.length)}`);
5074
+ console.log(chalk28.bold("\nBy File:"));
4434
5075
  for (const [file, count] of Object.entries(fileCount)) {
4435
- console.log(` ${chalk26.dim(file)}: ${chalk26.cyan(count)}`);
5076
+ console.log(` ${chalk28.dim(file)}: ${chalk28.cyan(count)}`);
4436
5077
  }
4437
- console.log(chalk26.bold("\nBy Type:"));
5078
+ console.log(chalk28.bold("\nBy Type:"));
4438
5079
  for (const [type, count] of Object.entries(typeCount)) {
4439
- console.log(` ${formatType(type)}: ${chalk26.cyan(count)}`);
5080
+ console.log(` ${formatType(type)}: ${chalk28.cyan(count)}`);
4440
5081
  }
4441
5082
  console.log();
4442
5083
  } catch (error) {
4443
- console.error(chalk26.red(`
5084
+ console.error(chalk28.red(`
4444
5085
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
4445
5086
  `));
4446
5087
  process.exit(1);
@@ -4460,26 +5101,26 @@ async function searchEnvVars(query, options) {
4460
5101
  }
4461
5102
  }
4462
5103
  if (results.length === 0) {
4463
- console.log(chalk26.yellow(`
5104
+ console.log(chalk28.yellow(`
4464
5105
  \u26A0\uFE0F No environment variables found matching "${query}"
4465
5106
  `));
4466
5107
  return;
4467
5108
  }
4468
- console.log(chalk26.blue.bold(`
5109
+ console.log(chalk28.blue.bold(`
4469
5110
  \u{1F50D} Found ${results.length} environment variable(s) matching "${query}"
4470
5111
  `));
4471
5112
  for (const [key, schema] of results) {
4472
5113
  const typeStr = formatType(schema.type);
4473
- const requiredStr = schema.required || schema.default !== void 0 ? chalk26.red("[required]") : chalk26.dim("[optional]");
4474
- console.log(`${chalk26.bold.cyan(key)} ${chalk26.dim("(")}${typeStr}${chalk26.dim(")")} ${requiredStr}`);
4475
- console.log(` ${chalk26.dim(schema.description)}`);
5114
+ const requiredStr = schema.required || schema.default !== void 0 ? chalk28.red("[required]") : chalk28.dim("[optional]");
5115
+ console.log(`${chalk28.bold.cyan(key)} ${chalk28.dim("(")}${typeStr}${chalk28.dim(")")} ${requiredStr}`);
5116
+ console.log(` ${chalk28.dim(schema.description)}`);
4476
5117
  if (schema.default !== void 0) {
4477
- console.log(` ${chalk26.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
5118
+ console.log(` ${chalk28.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
4478
5119
  }
4479
5120
  console.log();
4480
5121
  }
4481
5122
  } catch (error) {
4482
- console.error(chalk26.red(`
5123
+ console.error(chalk28.red(`
4483
5124
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
4484
5125
  `));
4485
5126
  process.exit(1);
@@ -4489,8 +5130,19 @@ var envCommand = new Command12("env").description("Manage environment variables"
4489
5130
  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
5131
  envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(showEnvStats);
4491
5132
  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);
5133
+ function validateEnvOption(envValue) {
5134
+ if (!VALID_ENVS.includes(envValue)) {
5135
+ console.error(chalk28.red(`
5136
+ \u274C Invalid environment: "${envValue}"`));
5137
+ console.log(chalk28.dim(` Valid values: ${VALID_ENVS.join(", ")}
5138
+ `));
5139
+ process.exit(1);
5140
+ }
5141
+ return envValue;
5142
+ }
4492
5143
  async function initEnvFiles(options) {
4493
5144
  const packageName = options.package || "@spfn/core";
5145
+ const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
4494
5146
  const cwd = process.cwd();
4495
5147
  try {
4496
5148
  const envSchema = await loadEnvSchema(packageName);
@@ -4502,31 +5154,57 @@ async function initEnvFiles(options) {
4502
5154
  acc[exampleFile].push([key, schema]);
4503
5155
  return acc;
4504
5156
  }, {});
4505
- console.log(chalk26.blue.bold(`
5157
+ if (targetEnv) {
5158
+ console.log(chalk28.blue.bold(`
5159
+ \u{1F680} Generating .env template files for ${chalk28.cyan(targetEnv)} environment
5160
+ `));
5161
+ const envSpecificFiles = {};
5162
+ const committedVars = allVars.filter(([_, schema]) => !schema.sensitive);
5163
+ if (committedVars.length > 0) {
5164
+ envSpecificFiles[`.env.${targetEnv}.example`] = committedVars;
5165
+ }
5166
+ const sensitiveVars = allVars.filter(([_, schema]) => schema.sensitive);
5167
+ if (sensitiveVars.length > 0) {
5168
+ envSpecificFiles[`.env.${targetEnv}.local.example`] = sensitiveVars;
5169
+ }
5170
+ const allGrouped = { ...grouped, ...envSpecificFiles };
5171
+ for (const [file, vars] of Object.entries(allGrouped)) {
5172
+ writeEnvTemplate(cwd, file, vars, options.force ?? false);
5173
+ }
5174
+ } else {
5175
+ console.log(chalk28.blue.bold(`
4506
5176
  \u{1F680} Generating .env template files
4507
5177
  `));
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;
5178
+ for (const [file, vars] of Object.entries(grouped)) {
5179
+ writeEnvTemplate(cwd, file, vars, options.force ?? false);
4513
5180
  }
4514
- const content = generateEnvFileContent(vars);
4515
- writeFileSync18(filePath, content, "utf-8");
4516
- console.log(chalk26.green(` \u2705 ${file} (${vars.length} variables)`));
4517
- }
4518
- console.log(chalk26.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
4519
- console.log(chalk26.dim(" cp .env.example .env"));
4520
- console.log(chalk26.dim(" cp .env.local.example .env.local"));
4521
- console.log(chalk26.dim(" cp .env.server.example .env.server"));
4522
- console.log(chalk26.dim(" cp .env.server.local.example .env.server.local\n"));
5181
+ }
5182
+ console.log(chalk28.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
5183
+ console.log(chalk28.dim(" cp .env.example .env"));
5184
+ console.log(chalk28.dim(" cp .env.local.example .env.local"));
5185
+ console.log(chalk28.dim(" cp .env.server.example .env.server"));
5186
+ console.log(chalk28.dim(" cp .env.server.local.example .env.server.local"));
5187
+ if (targetEnv) {
5188
+ console.log(chalk28.dim(` cp .env.${targetEnv}.example .env.${targetEnv}`));
5189
+ console.log(chalk28.dim(` cp .env.${targetEnv}.local.example .env.${targetEnv}.local`));
5190
+ }
5191
+ console.log("");
4523
5192
  } catch (error) {
4524
- console.error(chalk26.red(`
5193
+ console.error(chalk28.red(`
4525
5194
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
4526
5195
  `));
4527
5196
  process.exit(1);
4528
5197
  }
4529
5198
  }
5199
+ function writeEnvTemplate(cwd, file, vars, force) {
5200
+ const filePath = resolve(cwd, file);
5201
+ if (existsSync25(filePath) && !force) {
5202
+ console.log(chalk28.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
5203
+ return;
5204
+ }
5205
+ writeFileSync19(filePath, generateEnvFileContent(vars), "utf-8");
5206
+ console.log(chalk28.green(` \u2705 ${file} (${vars.length} variables)`));
5207
+ }
4530
5208
  function generateEnvFileContent(vars) {
4531
5209
  const lines = [
4532
5210
  "# Auto-generated by spfn env init",
@@ -4554,28 +5232,30 @@ function generateEnvFileContent(vars) {
4554
5232
  }
4555
5233
  async function checkEnvFiles(options) {
4556
5234
  const packageName = options.package || "@spfn/core";
5235
+ const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
4557
5236
  const cwd = process.cwd();
4558
5237
  try {
4559
5238
  const envSchema = await loadEnvSchema(packageName);
4560
5239
  const allVars = Object.entries(envSchema);
4561
- console.log(chalk26.blue.bold(`
4562
- \u{1F50D} Checking .env files against schema
5240
+ const envLabel = targetEnv ? ` (${targetEnv})` : "";
5241
+ console.log(chalk28.blue.bold(`
5242
+ \u{1F50D} Checking .env files against schema${envLabel}
4563
5243
  `));
4564
- const allFiles = [...ENV_FILES.nextjs, ...ENV_FILES.server];
5244
+ const filesToCheck = targetEnv ? getEnvFilesForEnvironment(targetEnv) : [...BASE_ENV_FILES.nextjs, ...BASE_ENV_FILES.server];
4565
5245
  const loadedEnv = {};
4566
5246
  const issues = [];
4567
5247
  const warnings = [];
4568
- for (const file of allFiles) {
5248
+ for (const file of filesToCheck) {
4569
5249
  const filePath = resolve(cwd, file);
4570
- if (!existsSync23(filePath)) {
5250
+ if (!existsSync25(filePath)) {
4571
5251
  continue;
4572
5252
  }
4573
- const content = readFileSync8(filePath, "utf-8");
5253
+ const content = readFileSync9(filePath, "utf-8");
4574
5254
  const parsed = parse(content);
4575
5255
  for (const [key, value] of Object.entries(parsed)) {
4576
5256
  loadedEnv[key] = { value: value || "", file };
4577
5257
  }
4578
- console.log(chalk26.dim(` \u{1F4C4} ${file} loaded`));
5258
+ console.log(chalk28.dim(` \u{1F4C4} ${file} loaded`));
4579
5259
  }
4580
5260
  console.log("");
4581
5261
  for (const [key, schema] of allVars) {
@@ -4583,21 +5263,21 @@ async function checkEnvFiles(options) {
4583
5263
  const found = loadedEnv[key];
4584
5264
  if (!found) {
4585
5265
  if (schema.required && schema.default === void 0) {
4586
- issues.push(`${chalk26.red("\u2717")} ${chalk26.cyan(key)} is required but not found in any .env file`);
5266
+ issues.push(`${chalk28.red("\u2717")} ${chalk28.cyan(key)} is required but not found in any .env file`);
4587
5267
  }
4588
5268
  continue;
4589
5269
  }
4590
- const isNextjsFile = ENV_FILES.nextjs.includes(found.file);
4591
- const isServerFile = ENV_FILES.server.includes(found.file);
5270
+ const isNextjsFile = BASE_ENV_FILES.nextjs.includes(found.file);
5271
+ const isServerFile = BASE_ENV_FILES.server.includes(found.file);
4592
5272
  const shouldBeNextjs = schema.nextjs ?? key.startsWith("NEXT_PUBLIC_");
4593
5273
  if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
4594
5274
  if (schema.sensitive) {
4595
5275
  issues.push(
4596
- `${chalk26.red("\u2717")} ${chalk26.cyan(key)} is sensitive and should be in ${chalk26.magenta(expectedFile)}, but found in ${chalk26.yellow(found.file)} (security risk!)`
5276
+ `${chalk28.red("\u2717")} ${chalk28.cyan(key)} is sensitive and should be in ${chalk28.magenta(expectedFile)}, but found in ${chalk28.yellow(found.file)} (security risk!)`
4597
5277
  );
4598
5278
  } else {
4599
5279
  warnings.push(
4600
- `${chalk26.yellow("\u26A0")} ${chalk26.cyan(key)} should be in ${chalk26.magenta(expectedFile)}, but found in ${chalk26.dim(found.file)}`
5280
+ `${chalk28.yellow("\u26A0")} ${chalk28.cyan(key)} should be in ${chalk28.magenta(expectedFile)}, but found in ${chalk28.dim(found.file)}`
4601
5281
  );
4602
5282
  }
4603
5283
  }
@@ -4605,51 +5285,64 @@ async function checkEnvFiles(options) {
4605
5285
  for (const [key, { file }] of Object.entries(loadedEnv)) {
4606
5286
  const inSchema = allVars.some(([k]) => k === key);
4607
5287
  if (!inSchema) {
4608
- warnings.push(`${chalk26.yellow("\u26A0")} ${chalk26.cyan(key)} in ${chalk26.dim(file)} is not in schema`);
5288
+ warnings.push(`${chalk28.yellow("\u26A0")} ${chalk28.cyan(key)} in ${chalk28.dim(file)} is not in schema`);
4609
5289
  }
4610
5290
  }
4611
5291
  if (issues.length > 0) {
4612
- console.log(chalk26.red.bold("Issues:"));
5292
+ console.log(chalk28.red.bold("Issues:"));
4613
5293
  for (const issue of issues) {
4614
5294
  console.log(` ${issue}`);
4615
5295
  }
4616
5296
  console.log("");
4617
5297
  }
4618
5298
  if (warnings.length > 0) {
4619
- console.log(chalk26.yellow.bold("Warnings:"));
5299
+ console.log(chalk28.yellow.bold("Warnings:"));
4620
5300
  for (const warning of warnings) {
4621
5301
  console.log(` ${warning}`);
4622
5302
  }
4623
5303
  console.log("");
4624
5304
  }
4625
5305
  if (issues.length === 0 && warnings.length === 0) {
4626
- console.log(chalk26.green("\u2705 All environment variables are correctly configured!\n"));
5306
+ console.log(chalk28.green("\u2705 All environment variables are correctly configured!\n"));
4627
5307
  } else {
4628
- console.log(chalk26.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
5308
+ console.log(chalk28.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
4629
5309
  `));
4630
5310
  if (issues.length > 0) {
4631
5311
  process.exit(1);
4632
5312
  }
4633
5313
  }
4634
5314
  } catch (error) {
4635
- console.error(chalk26.red(`
5315
+ console.error(chalk28.red(`
4636
5316
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
4637
5317
  `));
4638
5318
  process.exit(1);
4639
5319
  }
4640
5320
  }
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);
5321
+ 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);
5322
+ 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);
4643
5323
  async function validateEnvVars(options) {
4644
5324
  const packages = options.packages || ["@spfn/core"];
4645
- console.log(chalk26.blue.bold(`
5325
+ const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
5326
+ if (targetEnv) {
5327
+ const { loadEnv: loadEnv8 } = await import("@spfn/core/env/loader");
5328
+ const result = loadEnv8({ nodeEnv: targetEnv });
5329
+ console.log(chalk28.blue.bold(`
5330
+ \u{1F50D} Validating environment variables for ${chalk28.cyan(targetEnv)}
5331
+ `));
5332
+ if (result.loadedFiles.length > 0) {
5333
+ console.log(chalk28.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
5334
+ }
5335
+ console.log("");
5336
+ } else {
5337
+ console.log(chalk28.blue.bold(`
4646
5338
  \u{1F50D} Validating environment variables
4647
5339
  `));
5340
+ }
4648
5341
  const allErrors = [];
4649
5342
  const allWarnings = [];
4650
5343
  for (const packageName of packages) {
4651
5344
  try {
4652
- console.log(chalk26.dim(` \u{1F4E6} ${packageName}`));
5345
+ console.log(chalk28.dim(` \u{1F4E6} ${packageName}`));
4653
5346
  const envSchema = await loadEnvSchema(packageName);
4654
5347
  const { createEnvRegistry } = await import("@spfn/core/env");
4655
5348
  const registry = createEnvRegistry(envSchema);
@@ -4662,10 +5355,10 @@ async function validateEnvVars(options) {
4662
5355
  }
4663
5356
  } catch (error) {
4664
5357
  if (error instanceof Error && error.message.includes("does not export envSchema")) {
4665
- console.log(chalk26.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
5358
+ console.log(chalk28.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
4666
5359
  continue;
4667
5360
  }
4668
- console.error(chalk26.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
5361
+ console.error(chalk28.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
4669
5362
  if (options.strict) {
4670
5363
  process.exit(1);
4671
5364
  }
@@ -4673,42 +5366,43 @@ async function validateEnvVars(options) {
4673
5366
  }
4674
5367
  console.log("");
4675
5368
  if (allErrors.length > 0) {
4676
- console.log(chalk26.red.bold(`\u274C Validation Errors (${allErrors.length}):
5369
+ console.log(chalk28.red.bold(`\u274C Validation Errors (${allErrors.length}):
4677
5370
  `));
4678
5371
  for (const error of allErrors) {
4679
- console.log(` ${chalk26.red("\u2717")} ${chalk26.cyan(error.key)}`);
4680
- console.log(` ${chalk26.dim(error.message)}`);
4681
- console.log(` ${chalk26.dim(`from ${error.package}`)}`);
5372
+ console.log(` ${chalk28.red("\u2717")} ${chalk28.cyan(error.key)}`);
5373
+ console.log(` ${chalk28.dim(error.message)}`);
5374
+ console.log(` ${chalk28.dim(`from ${error.package}`)}`);
4682
5375
  console.log("");
4683
5376
  }
4684
5377
  }
4685
5378
  if (allWarnings.length > 0) {
4686
- console.log(chalk26.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
5379
+ console.log(chalk28.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
4687
5380
  `));
4688
5381
  for (const warning of allWarnings) {
4689
- console.log(` ${chalk26.yellow("\u26A0")} ${chalk26.cyan(warning.key)}`);
4690
- console.log(` ${chalk26.dim(warning.message)}`);
5382
+ console.log(` ${chalk28.yellow("\u26A0")} ${chalk28.cyan(warning.key)}`);
5383
+ console.log(` ${chalk28.dim(warning.message)}`);
4691
5384
  console.log("");
4692
5385
  }
4693
5386
  }
4694
5387
  if (allErrors.length === 0 && allWarnings.length === 0) {
4695
- console.log(chalk26.green.bold("\u2705 All environment variables are valid!\n"));
5388
+ console.log(chalk28.green.bold("\u2705 All environment variables are valid!\n"));
4696
5389
  } else if (allErrors.length === 0) {
4697
- console.log(chalk26.green("\u2705 No errors found."));
4698
- console.log(chalk26.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
5390
+ console.log(chalk28.green("\u2705 No errors found."));
5391
+ console.log(chalk28.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
4699
5392
  `));
4700
5393
  } else {
4701
- console.log(chalk26.red(`
5394
+ console.log(chalk28.red(`
4702
5395
  \u274C Validation failed with ${allErrors.length} error(s)
4703
5396
  `));
4704
5397
  process.exit(1);
4705
5398
  }
4706
5399
  }
4707
- 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);
5400
+ 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);
4708
5401
 
4709
5402
  // src/index.ts
5403
+ init_version();
4710
5404
  var program = new Command13();
4711
- program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version("0.1.0");
5405
+ program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(getCliVersion());
4712
5406
  program.addCommand(createCommand);
4713
5407
  program.addCommand(initCommand);
4714
5408
  program.addCommand(addCommand);