spfn 0.2.0-beta.4 → 0.2.0-beta.41

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.41";
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,241 @@ 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 {
2442
+ }
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 patched = 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
+ const ensureSchemas = schemaFilter.filter((s) => s !== "public").map((s) => `CREATE SCHEMA IF NOT EXISTS "${s}";
2465
+ `);
2466
+ const statements = [...ensureSchemas, ...patched];
2467
+ if (patched.length === 0) {
2468
+ console.log(chalk13.green("\u2705 No changes detected \u2014 database is up to date\n"));
2469
+ await applyFunctionMigrations();
2470
+ return;
2471
+ }
2472
+ const result = classifyStatements(statements);
2473
+ if (options.dryRun) {
2474
+ displayDryRunSummary(result);
2475
+ return;
2476
+ }
2477
+ displayClassifiedStatements(result);
2478
+ if (options.force) {
2479
+ console.log(chalk13.dim("\n--force: applying all changes..."));
2480
+ for (const stmt of statements) {
2481
+ await db.execute(sql.raw(stmt));
2482
+ }
2483
+ displayApplySummary(statements.length, 0);
2484
+ } else if (result.destructive.length === 0) {
2485
+ for (const stmt of statements) {
2486
+ await db.execute(sql.raw(stmt));
2487
+ }
2488
+ displayApplySummary(statements.length, 0);
2489
+ } else {
2490
+ const safeCount = result.safe.length + result.warning.length;
2491
+ if (safeCount > 0) {
2492
+ for (const stmt of [...result.safe, ...result.warning]) {
2493
+ await db.execute(sql.raw(stmt.sql));
2494
+ }
2495
+ console.log(chalk13.green(`
2496
+ \u2705 Applied ${safeCount} safe statement(s)`));
2497
+ }
2498
+ console.log(chalk13.red(`
2499
+ \u274C ${result.destructive.length} destructive change(s) require confirmation:`));
2500
+ for (const stmt of result.destructive) {
2501
+ console.log(chalk13.red(` ${stmt.sql.replace(/\s+/g, " ").trim()}`));
2502
+ console.log(chalk13.dim(` \u2192 ${stmt.reason}`));
2503
+ }
2504
+ const { confirm } = await prompts3({
2505
+ type: "confirm",
2506
+ name: "confirm",
2507
+ message: "Apply destructive changes?",
2508
+ initial: false
2509
+ });
2510
+ if (confirm) {
2511
+ for (const stmt of result.destructive) {
2512
+ await db.execute(sql.raw(stmt.sql));
2513
+ }
2514
+ displayApplySummary(statements.length, 0);
2515
+ } else {
2516
+ displayApplySummary(safeCount, result.destructive.length);
2517
+ console.log(chalk13.dim("Tip: Use --force to apply all changes without prompting.\n"));
2518
+ }
2104
2519
  }
2520
+ await applyFunctionMigrations();
2521
+ } finally {
2522
+ await close();
2523
+ }
2524
+ }
2525
+ async function applyFunctionMigrations() {
2526
+ const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
2527
+ const functions = discoverFunctionMigrations2(process.cwd());
2528
+ if (functions.length === 0) {
2529
+ return;
2530
+ }
2531
+ console.log(chalk13.blue("\n\u{1F4E6} Applying function package migrations:"));
2532
+ functions.forEach((func) => {
2533
+ console.log(chalk13.dim(` - ${func.packageName}`));
2534
+ });
2535
+ try {
2536
+ await executeFunctionMigrations2(functions);
2537
+ console.log(chalk13.green("\n\u2705 All function migrations applied\n"));
2538
+ } catch (error) {
2539
+ console.error(chalk13.red("\n\u274C Failed to apply function migrations"));
2540
+ console.error(chalk13.red(error instanceof Error ? error.message : "Unknown error"));
2541
+ process.exit(1);
2105
2542
  }
2106
2543
  }
2107
2544
 
2108
2545
  // src/commands/db/migrate.ts
2109
- import chalk16 from "chalk";
2546
+ import chalk17 from "chalk";
2547
+ import { join as join16 } from "path";
2548
+ import { existsSync as existsSync18 } from "fs";
2110
2549
 
2111
2550
  // src/commands/db/backup.ts
2112
2551
  import { promises as fs3 } from "fs";
2113
2552
  import path3 from "path";
2114
2553
  import { spawn as spawn2 } from "child_process";
2115
- import chalk15 from "chalk";
2554
+ import chalk16 from "chalk";
2116
2555
  import ora7 from "ora";
2117
2556
 
2118
2557
  // src/commands/db/utils/database.ts
@@ -2181,14 +2620,14 @@ function formatTimestamp() {
2181
2620
  import { promises as fs2 } from "fs";
2182
2621
  import { existsSync as existsSync17 } from "fs";
2183
2622
  import path2 from "path";
2184
- import chalk14 from "chalk";
2623
+ import chalk15 from "chalk";
2185
2624
 
2186
2625
  // src/commands/db/utils/metadata.ts
2187
2626
  import { promises as fs } from "fs";
2188
2627
  import path from "path";
2189
2628
  import { promisify } from "util";
2190
2629
  import { exec } from "child_process";
2191
- import chalk13 from "chalk";
2630
+ import chalk14 from "chalk";
2192
2631
  var execAsync = promisify(exec);
2193
2632
  async function collectGitInfo() {
2194
2633
  try {
@@ -2251,7 +2690,7 @@ async function collectMigrationInfo(dbUrl) {
2251
2690
  await pool.end();
2252
2691
  }
2253
2692
  } catch (error) {
2254
- console.log(chalk13.dim("\u26A0\uFE0F Could not fetch migration info"));
2693
+ console.log(chalk14.dim("\u26A0\uFE0F Could not fetch migration info"));
2255
2694
  return void 0;
2256
2695
  }
2257
2696
  }
@@ -2259,9 +2698,9 @@ async function saveBackupMetadata(metadata, backupFilename) {
2259
2698
  const metadataPath = backupFilename.replace(/\.(sql|dump)$/, ".meta.json");
2260
2699
  try {
2261
2700
  await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
2262
- console.log(chalk13.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
2701
+ console.log(chalk14.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
2263
2702
  } catch (error) {
2264
- console.log(chalk13.dim("\u26A0\uFE0F Could not save metadata"));
2703
+ console.log(chalk14.dim("\u26A0\uFE0F Could not save metadata"));
2265
2704
  }
2266
2705
  }
2267
2706
  async function loadBackupMetadata(backupFilename) {
@@ -2290,10 +2729,10 @@ async function ensureBackupInGitignore() {
2290
2729
  if (!hasBackupsIgnore) {
2291
2730
  const entry = exists && content && !content.endsWith("\n") ? "\n\n# Database backups\nbackups/\n" : "# Database backups\nbackups/\n";
2292
2731
  await fs2.appendFile(gitignorePath, entry);
2293
- console.log(chalk14.dim("\u2713 Added backups/ to .gitignore"));
2732
+ console.log(chalk15.dim("\u2713 Added backups/ to .gitignore"));
2294
2733
  }
2295
2734
  } catch (error) {
2296
- console.log(chalk14.dim("\u26A0\uFE0F Could not update .gitignore"));
2735
+ console.log(chalk15.dim("\u26A0\uFE0F Could not update .gitignore"));
2297
2736
  }
2298
2737
  }
2299
2738
  async function ensureBackupDir() {
@@ -2341,12 +2780,14 @@ async function listBackupFiles() {
2341
2780
 
2342
2781
  // src/commands/db/backup.ts
2343
2782
  import { env as env3 } from "@spfn/core/config";
2783
+ import { loadEnv as loadEnv4 } from "@spfn/core/server";
2344
2784
  async function dbBackup(options) {
2345
- console.log(chalk15.blue("\u{1F4BE} Creating database backup...\n"));
2785
+ console.log(chalk16.blue("\u{1F4BE} Creating database backup...\n"));
2786
+ loadEnv4();
2346
2787
  const dbUrl = env3.DATABASE_URL;
2347
2788
  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"));
2789
+ console.error(chalk16.red("\u274C DATABASE_URL not found in environment"));
2790
+ console.log(chalk16.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2350
2791
  process.exit(1);
2351
2792
  }
2352
2793
  const dbInfo = parseDatabaseUrl(dbUrl);
@@ -2356,7 +2797,7 @@ async function dbBackup(options) {
2356
2797
  const ext = format === "sql" ? "sql" : "dump";
2357
2798
  const filename = options.output || path3.join(backupDir, `${dbInfo.database}_${timestamp}.${ext}`);
2358
2799
  if (options.dataOnly && options.schemaOnly) {
2359
- console.error(chalk15.red("\u274C Cannot use --data-only and --schema-only together"));
2800
+ console.error(chalk16.red("\u274C Cannot use --data-only and --schema-only together"));
2360
2801
  process.exit(1);
2361
2802
  }
2362
2803
  const args = [
@@ -2402,11 +2843,11 @@ async function dbBackup(options) {
2402
2843
  const stats = await fs3.stat(filename);
2403
2844
  const size = formatBytes(stats.size);
2404
2845
  spinner.succeed("Backup created");
2405
- console.log(chalk15.green(`
2846
+ console.log(chalk16.green(`
2406
2847
  \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..."));
2848
+ console.log(chalk16.gray(` File: ${filename}`));
2849
+ console.log(chalk16.gray(` Size: ${size}`));
2850
+ console.log(chalk16.dim("\n\u{1F4CB} Collecting metadata..."));
2410
2851
  const [gitInfo, migrationInfo] = await Promise.all([
2411
2852
  collectGitInfo(),
2412
2853
  collectMigrationInfo(dbUrl)
@@ -2446,18 +2887,20 @@ async function dbBackup(options) {
2446
2887
  reject(error);
2447
2888
  });
2448
2889
  }).catch((error) => {
2449
- console.error(chalk15.red("\n\u274C Failed to create backup"));
2890
+ console.error(chalk16.red("\n\u274C Failed to create backup"));
2450
2891
  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."));
2892
+ console.error(chalk16.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
2452
2893
  } else {
2453
- console.error(chalk15.red(error instanceof Error ? error.message : "Unknown error"));
2894
+ console.error(chalk16.red(error instanceof Error ? error.message : "Unknown error"));
2454
2895
  }
2455
2896
  process.exit(1);
2456
2897
  });
2457
2898
  }
2458
2899
 
2459
2900
  // src/commands/db/migrate.ts
2460
- import "@spfn/core/config";
2901
+ import { env as env4 } from "@spfn/core/config";
2902
+ import { loadEnv as loadEnv5 } from "@spfn/core/server";
2903
+ var PROJECT_MIGRATIONS_TABLE = "__drizzle_migrations";
2461
2904
  async function dbMigrate(options = {}) {
2462
2905
  try {
2463
2906
  validateDatabasePrerequisites();
@@ -2465,7 +2908,7 @@ async function dbMigrate(options = {}) {
2465
2908
  process.exit(1);
2466
2909
  }
2467
2910
  if (options.withBackup) {
2468
- console.log(chalk16.blue("\u{1F4E6} Creating pre-migration backup...\n"));
2911
+ console.log(chalk17.blue("\u{1F4E6} Creating pre-migration backup...\n"));
2469
2912
  await dbBackup({
2470
2913
  format: "custom",
2471
2914
  tag: "pre-migration",
@@ -2473,59 +2916,72 @@ async function dbMigrate(options = {}) {
2473
2916
  });
2474
2917
  console.log("");
2475
2918
  }
2919
+ const { drizzle } = await import("drizzle-orm/postgres-js");
2920
+ const { migrate } = await import("drizzle-orm/postgres-js/migrator");
2921
+ const postgres = await import("postgres");
2922
+ loadEnv5();
2923
+ if (!env4.DATABASE_URL) {
2924
+ console.error(chalk17.red("\u274C DATABASE_URL not found in environment"));
2925
+ process.exit(1);
2926
+ }
2476
2927
  const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
2477
2928
  const functions = discoverFunctionMigrations2(process.cwd());
2478
2929
  if (functions.length > 0) {
2479
- console.log(chalk16.blue("\u{1F4E6} Applying function package migrations:"));
2930
+ console.log(chalk17.blue("\u{1F4E6} Applying function package migrations:"));
2480
2931
  functions.forEach((func) => {
2481
- console.log(chalk16.dim(` - ${func.packageName}`));
2932
+ console.log(chalk17.dim(` - ${func.packageName}`));
2482
2933
  });
2934
+ await executeFunctionMigrations2(functions);
2935
+ console.log(chalk17.green("\u2705 Function migrations applied\n"));
2936
+ }
2937
+ const projectMigrationsDir = join16(process.cwd(), "src/server/drizzle");
2938
+ if (existsSync18(projectMigrationsDir)) {
2939
+ const projConn = postgres.default(env4.DATABASE_URL, { max: 1 });
2940
+ const projDb = drizzle(projConn);
2483
2941
  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);
2942
+ console.log(chalk17.blue("\u{1F4E6} Running project migrations..."));
2943
+ await migrate(projDb, {
2944
+ migrationsFolder: projectMigrationsDir,
2945
+ migrationsTable: PROJECT_MIGRATIONS_TABLE
2946
+ });
2947
+ console.log(chalk17.green("\u2705 Project migrations applied successfully"));
2948
+ } finally {
2949
+ await projConn.end();
2490
2950
  }
2951
+ } else {
2952
+ console.log(chalk17.dim("No project migrations found (src/server/drizzle)"));
2491
2953
  }
2492
- await runWithSpinner(
2493
- "Running project migrations...",
2494
- "migrate",
2495
- "Project migrations applied successfully",
2496
- "Failed to run project migrations"
2497
- );
2498
2954
  }
2499
2955
 
2500
2956
  // src/commands/db/studio.ts
2501
- import chalk17 from "chalk";
2502
- import { existsSync as existsSync18, writeFileSync as writeFileSync10, unlinkSync as unlinkSync2 } from "fs";
2957
+ import chalk18 from "chalk";
2958
+ import { existsSync as existsSync19, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
2503
2959
  import { spawn as spawn3 } from "child_process";
2504
- import { env as env4 } from "@spfn/core/config";
2960
+ import { env as env5 } from "@spfn/core/config";
2505
2961
  import "@spfn/core/config";
2506
2962
  async function dbStudio(requestedPort) {
2507
- console.log(chalk17.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
2963
+ console.log(chalk18.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
2508
2964
  const defaultPort = 4983;
2509
2965
  const startPort = requestedPort || defaultPort;
2510
2966
  let port;
2511
2967
  try {
2512
2968
  port = await findAvailablePort(startPort);
2513
2969
  if (port !== startPort) {
2514
- console.log(chalk17.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
2970
+ console.log(chalk18.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
2515
2971
  `));
2516
2972
  }
2517
2973
  } catch (error) {
2518
- console.error(chalk17.red(error instanceof Error ? error.message : "Failed to find available port"));
2974
+ console.error(chalk18.red(error instanceof Error ? error.message : "Failed to find available port"));
2519
2975
  process.exit(1);
2520
2976
  }
2521
- const hasUserConfig = existsSync18("./drizzle.config.ts");
2977
+ const hasUserConfig = existsSync19("./drizzle.config.ts");
2522
2978
  const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
2523
2979
  try {
2524
2980
  const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
2525
2981
  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"));
2982
+ if (!env5.DATABASE_URL) {
2983
+ console.error(chalk18.red("\u274C DATABASE_URL not found in environment"));
2984
+ console.log(chalk18.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2529
2985
  process.exit(1);
2530
2986
  }
2531
2987
  const { generateDrizzleConfigFile } = await import("@spfn/core/db");
@@ -2536,33 +2992,34 @@ async function dbStudio(requestedPort) {
2536
2992
  // Expand glob patterns for Studio compatibility
2537
2993
  });
2538
2994
  writeFileSync10(tempConfigPath, configContent);
2539
- console.log(chalk17.dim("Using auto-generated Drizzle config\n"));
2995
+ console.log(chalk18.dim("Using auto-generated Drizzle config\n"));
2540
2996
  }
2541
2997
  const studioProcess = spawn3("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
2542
2998
  stdio: "inherit",
2543
- shell: true
2999
+ shell: true,
3000
+ env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" }
2544
3001
  });
2545
3002
  const cleanup = () => {
2546
- if (!hasUserConfig && existsSync18(tempConfigPath)) {
2547
- unlinkSync2(tempConfigPath);
3003
+ if (!hasUserConfig && existsSync19(tempConfigPath)) {
3004
+ unlinkSync3(tempConfigPath);
2548
3005
  }
2549
3006
  };
2550
3007
  studioProcess.on("exit", (code) => {
2551
3008
  cleanup();
2552
3009
  if (code !== 0 && code !== null) {
2553
- console.error(chalk17.red(`
3010
+ console.error(chalk18.red(`
2554
3011
  \u274C Drizzle Studio exited with code ${code}`));
2555
3012
  process.exit(code);
2556
3013
  }
2557
3014
  });
2558
3015
  studioProcess.on("error", (error) => {
2559
3016
  cleanup();
2560
- console.error(chalk17.red("\u274C Failed to start Drizzle Studio"));
2561
- console.error(chalk17.red(error.message));
3017
+ console.error(chalk18.red("\u274C Failed to start Drizzle Studio"));
3018
+ console.error(chalk18.red(error.message));
2562
3019
  process.exit(1);
2563
3020
  });
2564
3021
  process.on("SIGINT", () => {
2565
- console.log(chalk17.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
3022
+ console.log(chalk18.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
2566
3023
  studioProcess.kill("SIGTERM");
2567
3024
  cleanup();
2568
3025
  process.exit(0);
@@ -2573,28 +3030,28 @@ async function dbStudio(requestedPort) {
2573
3030
  process.exit(0);
2574
3031
  });
2575
3032
  } catch (error) {
2576
- if (!hasUserConfig && existsSync18(tempConfigPath)) {
2577
- unlinkSync2(tempConfigPath);
3033
+ if (!hasUserConfig && existsSync19(tempConfigPath)) {
3034
+ unlinkSync3(tempConfigPath);
2578
3035
  }
2579
- console.error(chalk17.red("\u274C Failed to start Drizzle Studio"));
2580
- console.error(chalk17.red(error instanceof Error ? error.message : "Unknown error"));
3036
+ console.error(chalk18.red("\u274C Failed to start Drizzle Studio"));
3037
+ console.error(chalk18.red(error instanceof Error ? error.message : "Unknown error"));
2581
3038
  process.exit(1);
2582
3039
  }
2583
3040
  }
2584
3041
 
2585
3042
  // src/commands/db/drop.ts
2586
- import chalk18 from "chalk";
2587
- import prompts3 from "prompts";
3043
+ import chalk19 from "chalk";
3044
+ import prompts4 from "prompts";
2588
3045
  async function dbDrop() {
2589
- console.log(chalk18.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
2590
- const { confirm } = await prompts3({
3046
+ console.log(chalk19.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
3047
+ const { confirm } = await prompts4({
2591
3048
  type: "confirm",
2592
3049
  name: "confirm",
2593
3050
  message: "Are you sure you want to drop all tables?",
2594
3051
  initial: false
2595
3052
  });
2596
3053
  if (!confirm) {
2597
- console.log(chalk18.gray("Cancelled."));
3054
+ console.log(chalk19.gray("Cancelled."));
2598
3055
  process.exit(0);
2599
3056
  }
2600
3057
  await runWithSpinner(
@@ -2606,7 +3063,7 @@ async function dbDrop() {
2606
3063
  }
2607
3064
 
2608
3065
  // src/commands/db/check.ts
2609
- import chalk19 from "chalk";
3066
+ import chalk20 from "chalk";
2610
3067
  import ora8 from "ora";
2611
3068
  async function dbCheck() {
2612
3069
  const spinner = ora8("Checking database connection...").start();
@@ -2615,7 +3072,7 @@ async function dbCheck() {
2615
3072
  spinner.succeed("Database connection OK");
2616
3073
  } catch (error) {
2617
3074
  spinner.fail("Database connection failed");
2618
- console.error(chalk19.red(error instanceof Error ? error.message : "Unknown error"));
3075
+ console.error(chalk20.red(error instanceof Error ? error.message : "Unknown error"));
2619
3076
  process.exit(1);
2620
3077
  }
2621
3078
  }
@@ -2623,26 +3080,28 @@ async function dbCheck() {
2623
3080
  // src/commands/db/restore.ts
2624
3081
  import path4 from "path";
2625
3082
  import { spawn as spawn4 } from "child_process";
2626
- import chalk20 from "chalk";
3083
+ import chalk21 from "chalk";
2627
3084
  import ora9 from "ora";
2628
- import prompts4 from "prompts";
2629
- import { env as env5 } from "@spfn/core/config";
3085
+ import prompts5 from "prompts";
3086
+ import { env as env6 } from "@spfn/core/config";
3087
+ import { loadEnv as loadEnv6 } from "@spfn/core/server";
2630
3088
  async function dbRestore(backupFile, options = {}) {
2631
- console.log(chalk20.blue("\u267B\uFE0F Restoring database from backup...\n"));
2632
- const dbUrl = env5.DATABASE_URL;
3089
+ console.log(chalk21.blue("\u267B\uFE0F Restoring database from backup...\n"));
3090
+ loadEnv6();
3091
+ const dbUrl = env6.DATABASE_URL;
2633
3092
  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"));
3093
+ console.error(chalk21.red("\u274C DATABASE_URL not found in environment"));
3094
+ console.log(chalk21.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2636
3095
  process.exit(1);
2637
3096
  }
2638
3097
  let file = backupFile;
2639
3098
  if (!file) {
2640
3099
  const backups = await listBackupFiles();
2641
3100
  if (backups.length === 0) {
2642
- console.log(chalk20.yellow("No backups found in ./backups directory"));
3101
+ console.log(chalk21.yellow("No backups found in ./backups directory"));
2643
3102
  process.exit(0);
2644
3103
  }
2645
- const { selected } = await prompts4({
3104
+ const { selected } = await prompts5({
2646
3105
  type: "select",
2647
3106
  name: "selected",
2648
3107
  message: "Select backup to restore:",
@@ -2652,70 +3111,70 @@ async function dbRestore(backupFile, options = {}) {
2652
3111
  }))
2653
3112
  });
2654
3113
  if (!selected) {
2655
- console.log(chalk20.gray("Cancelled"));
3114
+ console.log(chalk21.gray("Cancelled"));
2656
3115
  process.exit(0);
2657
3116
  }
2658
3117
  file = selected;
2659
3118
  }
2660
3119
  if (!file) {
2661
- console.error(chalk20.red("\u274C No backup file selected"));
3120
+ console.error(chalk21.red("\u274C No backup file selected"));
2662
3121
  process.exit(1);
2663
3122
  }
2664
3123
  const metadata = await loadBackupMetadata(file);
2665
3124
  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()}`));
3125
+ console.log(chalk21.blue("\n\u{1F4CB} Backup Information:\n"));
3126
+ console.log(chalk21.dim(` Database: ${metadata.database}`));
3127
+ console.log(chalk21.dim(` Created: ${new Date(metadata.timestamp).toLocaleString()}`));
2669
3128
  if (metadata.environment) {
2670
- console.log(chalk20.dim(` Environment: ${metadata.environment}`));
3129
+ console.log(chalk21.dim(` Environment: ${metadata.environment}`));
2671
3130
  }
2672
3131
  if (metadata.tags && metadata.tags.length > 0) {
2673
- console.log(chalk20.dim(` Tags: ${metadata.tags.join(", ")}`));
3132
+ console.log(chalk21.dim(` Tags: ${metadata.tags.join(", ")}`));
2674
3133
  }
2675
3134
  if (metadata.backup.dataOnly) {
2676
- console.log(chalk20.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
3135
+ console.log(chalk21.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
2677
3136
  }
2678
3137
  if (metadata.backup.schemaOnly) {
2679
- console.log(chalk20.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
3138
+ console.log(chalk21.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
2680
3139
  }
2681
- const warnings = [];
3140
+ const warnings2 = [];
2682
3141
  const [currentGitInfo, currentMigrationInfo] = await Promise.all([
2683
3142
  collectGitInfo(),
2684
3143
  collectMigrationInfo(dbUrl)
2685
3144
  ]);
2686
3145
  if (metadata.git && currentGitInfo) {
2687
3146
  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)}`);
3147
+ warnings2.push(`Git commit mismatch: backup from ${metadata.git.commit.substring(0, 7)}, current is ${currentGitInfo.commit.substring(0, 7)}`);
2689
3148
  }
2690
3149
  if (metadata.git.branch !== currentGitInfo.branch) {
2691
- warnings.push(`Git branch mismatch: backup from '${metadata.git.branch}', current is '${currentGitInfo.branch}'`);
3150
+ warnings2.push(`Git branch mismatch: backup from '${metadata.git.branch}', current is '${currentGitInfo.branch}'`);
2692
3151
  }
2693
3152
  }
2694
3153
  if (metadata.migrations && currentMigrationInfo) {
2695
3154
  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}`);
3155
+ warnings2.push(`Migration version mismatch: backup has ${metadata.migrations.count} migrations, current has ${currentMigrationInfo.count}`);
3156
+ warnings2.push(` Last migration in backup: ${metadata.migrations.hash}`);
3157
+ warnings2.push(` Current last migration: ${currentMigrationInfo.hash}`);
2699
3158
  }
2700
3159
  }
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}`)));
3160
+ if (warnings2.length > 0) {
3161
+ console.log(chalk21.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
3162
+ warnings2.forEach((warning) => console.log(chalk21.yellow(` - ${warning}`)));
2704
3163
  console.log("");
2705
3164
  }
2706
3165
  }
2707
- const { confirm } = await prompts4({
3166
+ const { confirm } = await prompts5({
2708
3167
  type: "confirm",
2709
3168
  name: "confirm",
2710
- message: chalk20.yellow("\u26A0\uFE0F This will replace all data in the database. Continue?"),
3169
+ message: chalk21.yellow("\u26A0\uFE0F This will replace all data in the database. Continue?"),
2711
3170
  initial: false
2712
3171
  });
2713
3172
  if (!confirm) {
2714
- console.log(chalk20.gray("Cancelled"));
3173
+ console.log(chalk21.gray("Cancelled"));
2715
3174
  process.exit(0);
2716
3175
  }
2717
3176
  if (options.dataOnly && options.schemaOnly) {
2718
- console.error(chalk20.red("\u274C Cannot use --data-only and --schema-only together"));
3177
+ console.error(chalk21.red("\u274C Cannot use --data-only and --schema-only together"));
2719
3178
  process.exit(1);
2720
3179
  }
2721
3180
  const dbInfo = parseDatabaseUrl(dbUrl);
@@ -2728,6 +3187,7 @@ async function dbRestore(backupFile, options = {}) {
2728
3187
  args.push("-p", dbInfo.port);
2729
3188
  args.push("-U", dbInfo.user);
2730
3189
  args.push("-d", dbInfo.database);
3190
+ args.push("--verbose");
2731
3191
  if (options.drop) {
2732
3192
  args.push("--clean");
2733
3193
  }
@@ -2743,15 +3203,17 @@ async function dbRestore(backupFile, options = {}) {
2743
3203
  args.push(file);
2744
3204
  } else {
2745
3205
  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"));
3206
+ console.log(chalk21.yellow("\u26A0\uFE0F Note: --data-only and --schema-only options only work with custom format backups (.dump)"));
3207
+ console.log(chalk21.yellow(" For SQL files, the backup must have been created with the desired option.\n"));
2748
3208
  }
2749
3209
  args.push("-h", dbInfo.host);
2750
3210
  args.push("-p", dbInfo.port);
2751
3211
  args.push("-U", dbInfo.user);
2752
3212
  args.push("-d", dbInfo.database);
3213
+ args.push("-v", "ON_ERROR_STOP=1");
2753
3214
  args.push("-f", file);
2754
3215
  }
3216
+ const verbose = options.verbose ?? false;
2755
3217
  const spinner = ora9("Restoring backup...").start();
2756
3218
  const restoreProcess = spawn4(command, args, {
2757
3219
  stdio: ["ignore", "pipe", "pipe"],
@@ -2760,19 +3222,81 @@ async function dbRestore(backupFile, options = {}) {
2760
3222
  PGPASSWORD: dbInfo.password
2761
3223
  }
2762
3224
  });
2763
- let errorOutput = "";
3225
+ const warnings = [];
3226
+ const errors = [];
3227
+ let objectCount = 0;
3228
+ let lastObject = "";
2764
3229
  restoreProcess.stderr?.on("data", (data) => {
2765
- errorOutput += data.toString();
3230
+ const lines = data.toString().split("\n").filter((l) => l.trim());
3231
+ for (const line of lines) {
3232
+ if (/^pg_restore:.*warning:/i.test(line) || /^WARNING:/i.test(line)) {
3233
+ warnings.push(line.trim());
3234
+ } else if (/^pg_restore:.*error:/i.test(line) || /^ERROR:/i.test(line) || /^psql:.*ERROR/i.test(line)) {
3235
+ errors.push(line.trim());
3236
+ }
3237
+ const objectMatch = line.match(/processing item (\d+)\/(\d+)/);
3238
+ if (objectMatch) {
3239
+ objectCount = Number(objectMatch[2]);
3240
+ const current = Number(objectMatch[1]);
3241
+ const desc = line.replace(/^pg_restore:\s*/, "").trim();
3242
+ lastObject = desc;
3243
+ spinner.text = `Restoring backup... [${current}/${objectCount}] ${desc}`;
3244
+ } else if (isCustomFormat) {
3245
+ const desc = line.replace(/^pg_restore:\s*/, "").trim();
3246
+ if (desc && !/warning:|error:/i.test(desc)) {
3247
+ lastObject = desc;
3248
+ spinner.text = `Restoring backup... ${desc}`;
3249
+ }
3250
+ }
3251
+ if (verbose) {
3252
+ spinner.stop();
3253
+ console.log(chalk21.dim(` ${line.trim()}`));
3254
+ spinner.start();
3255
+ }
3256
+ }
3257
+ });
3258
+ restoreProcess.stdout?.on("data", (data) => {
3259
+ if (verbose) {
3260
+ spinner.stop();
3261
+ console.log(chalk21.dim(` ${data.toString().trim()}`));
3262
+ spinner.start();
3263
+ }
2766
3264
  });
2767
3265
  await new Promise((resolve2, reject) => {
2768
3266
  restoreProcess.on("close", (code) => {
2769
3267
  if (code === 0) {
2770
- spinner.succeed("Restore completed");
2771
- console.log(chalk20.green("\n\u2705 Database restored successfully"));
3268
+ const summary = objectCount > 0 ? ` (${objectCount} objects)` : "";
3269
+ spinner.succeed(`Restore completed${summary}`);
3270
+ if (warnings.length > 0) {
3271
+ console.log(chalk21.yellow(`
3272
+ \u26A0\uFE0F Warnings during restore (${warnings.length}):
3273
+ `));
3274
+ for (const w of warnings) {
3275
+ console.log(chalk21.yellow(` - ${w}`));
3276
+ }
3277
+ }
3278
+ console.log(chalk21.green("\n\u2705 Database restored successfully"));
2772
3279
  resolve2();
2773
3280
  } else {
2774
3281
  spinner.fail("Restore failed");
2775
- reject(new Error(errorOutput || "Restore failed"));
3282
+ if (errors.length > 0) {
3283
+ console.error(chalk21.red(`
3284
+ \u274C Errors (${errors.length}):
3285
+ `));
3286
+ for (const e of errors) {
3287
+ console.error(chalk21.red(` - ${e}`));
3288
+ }
3289
+ }
3290
+ if (warnings.length > 0) {
3291
+ console.log(chalk21.yellow(`
3292
+ \u26A0\uFE0F Warnings (${warnings.length}):
3293
+ `));
3294
+ for (const w of warnings) {
3295
+ console.log(chalk21.yellow(` - ${w}`));
3296
+ }
3297
+ }
3298
+ const fallback = errors.length === 0 && warnings.length === 0 ? "Restore failed with no output" : "";
3299
+ reject(new Error(fallback));
2776
3300
  }
2777
3301
  });
2778
3302
  restoreProcess.on("error", (error) => {
@@ -2780,24 +3304,27 @@ async function dbRestore(backupFile, options = {}) {
2780
3304
  reject(error);
2781
3305
  });
2782
3306
  }).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"));
3307
+ const msg = error instanceof Error ? error.message : "Unknown error";
3308
+ if (msg) {
3309
+ console.error(chalk21.red(`
3310
+ \u274C ${msg}`));
3311
+ }
2785
3312
  process.exit(1);
2786
3313
  });
2787
3314
  }
2788
3315
 
2789
3316
  // src/commands/db/list.ts
2790
- import chalk21 from "chalk";
3317
+ import chalk22 from "chalk";
2791
3318
  async function dbBackupList() {
2792
- console.log(chalk21.blue("\u{1F4CB} Database backups:\n"));
3319
+ console.log(chalk22.blue("\u{1F4CB} Database backups:\n"));
2793
3320
  const backups = await listBackupFiles();
2794
3321
  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"));
3322
+ console.log(chalk22.yellow("No backups found in ./backups directory"));
3323
+ console.log(chalk22.gray("\n\u{1F4A1} Create a backup with: pnpm spfn db backup\n"));
2797
3324
  return;
2798
3325
  }
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"));
3326
+ console.log(chalk22.bold(" Date Size File"));
3327
+ 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
3328
  backups.forEach((backup) => {
2802
3329
  const date = backup.date.toLocaleString("en-US", {
2803
3330
  year: "numeric",
@@ -2808,23 +3335,23 @@ async function dbBackupList() {
2808
3335
  second: "2-digit"
2809
3336
  });
2810
3337
  const sizeStr = backup.size.padEnd(10);
2811
- console.log(chalk21.white(` ${date} ${sizeStr} ${backup.name}`));
3338
+ console.log(chalk22.white(` ${date} ${sizeStr} ${backup.name}`));
2812
3339
  });
2813
- console.log(chalk21.gray(`
3340
+ console.log(chalk22.gray(`
2814
3341
  Total: ${backups.length} backup(s)
2815
3342
  `));
2816
3343
  }
2817
3344
 
2818
3345
  // src/commands/db/clean.ts
2819
3346
  import { promises as fs4 } from "fs";
2820
- import chalk22 from "chalk";
3347
+ import chalk23 from "chalk";
2821
3348
  import ora10 from "ora";
2822
- import prompts5 from "prompts";
3349
+ import prompts6 from "prompts";
2823
3350
  async function dbBackupClean(options) {
2824
- console.log(chalk22.blue("\u{1F9F9} Cleaning old backups...\n"));
3351
+ console.log(chalk23.blue("\u{1F9F9} Cleaning old backups...\n"));
2825
3352
  const backups = await listBackupFiles();
2826
3353
  if (backups.length === 0) {
2827
- console.log(chalk22.yellow("No backups found"));
3354
+ console.log(chalk23.yellow("No backups found"));
2828
3355
  return;
2829
3356
  }
2830
3357
  let toDelete = [];
@@ -2841,74 +3368,176 @@ async function dbBackupClean(options) {
2841
3368
  toDelete = backups.slice(defaultKeep);
2842
3369
  }
2843
3370
  if (toDelete.length === 0) {
2844
- console.log(chalk22.green("\u2705 No backups to clean"));
3371
+ console.log(chalk23.green("\u2705 No backups to clean"));
2845
3372
  return;
2846
3373
  }
2847
- console.log(chalk22.yellow(`The following ${toDelete.length} backup(s) will be deleted:
3374
+ console.log(chalk23.yellow(`The following ${toDelete.length} backup(s) will be deleted:
2848
3375
  `));
2849
3376
  toDelete.forEach((backup) => {
2850
- console.log(chalk22.gray(` - ${backup.name} (${backup.size})`));
3377
+ console.log(chalk23.gray(` - ${backup.name} (${backup.size})`));
2851
3378
  });
2852
- const { confirm } = await prompts5({
3379
+ const { confirm } = await prompts6({
2853
3380
  type: "confirm",
2854
3381
  name: "confirm",
2855
3382
  message: "\nProceed with deletion?",
2856
3383
  initial: false
2857
3384
  });
2858
3385
  if (!confirm) {
2859
- console.log(chalk22.gray("Cancelled"));
3386
+ console.log(chalk23.gray("Cancelled"));
2860
3387
  return;
2861
3388
  }
2862
3389
  const spinner = ora10("Deleting backups...").start();
2863
3390
  try {
2864
3391
  await Promise.all(toDelete.map((backup) => fs4.unlink(backup.path)));
2865
3392
  spinner.succeed("Backups deleted");
2866
- console.log(chalk22.green(`
3393
+ console.log(chalk23.green(`
2867
3394
  \u2705 Deleted ${toDelete.length} backup(s)`));
2868
3395
  } catch (error) {
2869
3396
  spinner.fail("Failed to delete backups");
2870
- console.error(chalk22.red(error instanceof Error ? error.message : "Unknown error"));
3397
+ console.error(chalk23.red(error instanceof Error ? error.message : "Unknown error"));
2871
3398
  process.exit(1);
2872
3399
  }
2873
3400
  }
2874
3401
 
3402
+ // src/commands/db/reindex.ts
3403
+ import { existsSync as existsSync20, readFileSync as readFileSync6, writeFileSync as writeFileSync11, renameSync, copyFileSync } from "fs";
3404
+ import { join as join17 } from "path";
3405
+ import chalk24 from "chalk";
3406
+ import { loadEnv as loadEnv7 } from "@spfn/core/server";
3407
+ function isTimestampPrefix(tag) {
3408
+ const prefix = tag.split("_")[0];
3409
+ return /^\d{5,}$/.test(prefix);
3410
+ }
3411
+ function parseTag(tag) {
3412
+ const underscoreIdx = tag.indexOf("_");
3413
+ if (underscoreIdx === -1) {
3414
+ return { prefix: tag, suffix: "" };
3415
+ }
3416
+ return {
3417
+ prefix: tag.substring(0, underscoreIdx),
3418
+ suffix: tag.substring(underscoreIdx + 1)
3419
+ };
3420
+ }
3421
+ async function dbReindex(options = {}) {
3422
+ loadEnv7();
3423
+ const { getDrizzleConfig } = await import("@spfn/core/db");
3424
+ const config = getDrizzleConfig({ disablePackageDiscovery: true });
3425
+ const outDir = config.out;
3426
+ const journalPath = join17(outDir, "meta", "_journal.json");
3427
+ if (!existsSync20(journalPath)) {
3428
+ console.error(chalk24.red("\u274C No _journal.json found at:"), journalPath);
3429
+ console.log(chalk24.yellow("\u{1F4A1} Run `spfn db generate` first to create migrations"));
3430
+ process.exit(1);
3431
+ }
3432
+ const journal = JSON.parse(readFileSync6(journalPath, "utf-8"));
3433
+ if (journal.entries.length === 0) {
3434
+ console.log(chalk24.yellow("No migration entries found \u2014 nothing to reindex."));
3435
+ return;
3436
+ }
3437
+ const renames = [];
3438
+ const tagUpdates = [];
3439
+ let skipped = 0;
3440
+ for (const entry of journal.entries) {
3441
+ if (isTimestampPrefix(entry.tag)) {
3442
+ skipped++;
3443
+ continue;
3444
+ }
3445
+ const { prefix: oldPrefix, suffix } = parseTag(entry.tag);
3446
+ const newPrefix = String(entry.when);
3447
+ const newTag = suffix ? `${newPrefix}_${suffix}` : newPrefix;
3448
+ const oldSql = join17(outDir, `${entry.tag}.sql`);
3449
+ const newSql = join17(outDir, `${newTag}.sql`);
3450
+ if (existsSync20(oldSql)) {
3451
+ renames.push({ type: "sql", from: oldSql, to: newSql });
3452
+ }
3453
+ const oldSnapshot = join17(outDir, "meta", `${oldPrefix}_snapshot.json`);
3454
+ const newSnapshot = join17(outDir, "meta", `${newPrefix}_snapshot.json`);
3455
+ if (existsSync20(oldSnapshot)) {
3456
+ renames.push({ type: "snapshot", from: oldSnapshot, to: newSnapshot });
3457
+ }
3458
+ tagUpdates.push({ idx: entry.idx, oldTag: entry.tag, newTag });
3459
+ }
3460
+ if (tagUpdates.length === 0) {
3461
+ console.log(chalk24.green("\u2705 All migrations already use timestamp prefix \u2014 nothing to do."));
3462
+ if (skipped > 0) {
3463
+ console.log(chalk24.dim(` (${skipped} entries already timestamp-prefixed)`));
3464
+ }
3465
+ return;
3466
+ }
3467
+ console.log(chalk24.bold("\n\u{1F4CB} Reindex plan:\n"));
3468
+ for (const update of tagUpdates) {
3469
+ console.log(
3470
+ chalk24.dim(` [${update.idx}]`),
3471
+ chalk24.red(update.oldTag),
3472
+ chalk24.dim("\u2192"),
3473
+ chalk24.green(update.newTag)
3474
+ );
3475
+ }
3476
+ console.log(chalk24.dim(`
3477
+ ${renames.length} file(s) to rename, ${tagUpdates.length} journal tag(s) to update`));
3478
+ if (skipped > 0) {
3479
+ console.log(chalk24.dim(` ${skipped} entry/entries already timestamp-prefixed (skipped)`));
3480
+ }
3481
+ if (options.dryRun) {
3482
+ console.log(chalk24.yellow("\n\u{1F50D} Dry run \u2014 no changes applied."));
3483
+ return;
3484
+ }
3485
+ const backupPath = journalPath + ".bak";
3486
+ copyFileSync(journalPath, backupPath);
3487
+ console.log(chalk24.dim(`
3488
+ Backed up journal \u2192 ${backupPath}`));
3489
+ for (const rename of renames) {
3490
+ renameSync(rename.from, rename.to);
3491
+ }
3492
+ for (const update of tagUpdates) {
3493
+ const entry = journal.entries.find((e) => e.idx === update.idx);
3494
+ if (entry) {
3495
+ entry.tag = update.newTag;
3496
+ }
3497
+ }
3498
+ writeFileSync11(journalPath, JSON.stringify(journal, null, 2) + "\n");
3499
+ console.log(chalk24.green(`
3500
+ \u2705 Reindex complete \u2014 ${tagUpdates.length} migration(s) converted to timestamp prefix.`));
3501
+ }
3502
+
2875
3503
  // src/commands/db/index.ts
2876
3504
  var dbCommand = new Command9("db").description("Database management commands (wraps Drizzle Kit)");
2877
3505
  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);
3506
+ 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
3507
  dbCommand.command("migrate").alias("m").description("Run pending migrations").option("--with-backup", "Create backup before running migrations").action((options) => dbMigrate(options));
2880
3508
  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
3509
  dbCommand.command("drop").description("Drop all database tables (\u26A0\uFE0F dangerous!)").action(dbDrop);
2882
3510
  dbCommand.command("check").description("Check database connection").action(dbCheck);
2883
3511
  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));
3512
+ 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
3513
  dbCommand.command("backup:list").description("List all database backups").action(dbBackupList);
2886
3514
  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));
3515
+ dbCommand.command("reindex").description("Convert migration files from sequential to timestamp prefix").option("--dry-run", "Show changes without applying").action((options) => dbReindex(options));
2887
3516
 
2888
3517
  // src/commands/add.ts
2889
3518
  import { Command as Command10 } from "commander";
2890
- import { existsSync as existsSync19, readFileSync as readFileSync6 } from "fs";
2891
- import { join as join16 } from "path";
3519
+ import { existsSync as existsSync21, readFileSync as readFileSync7 } from "fs";
3520
+ import { join as join18 } from "path";
2892
3521
  import { exec as exec2 } from "child_process";
2893
3522
  import { promisify as promisify2 } from "util";
2894
- import chalk23 from "chalk";
3523
+ import chalk25 from "chalk";
2895
3524
  import ora11 from "ora";
2896
3525
  var execAsync2 = promisify2(exec2);
2897
3526
  async function addPackage(packageName) {
2898
3527
  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"));
3528
+ console.error(chalk25.red("\u274C Please specify full package name"));
3529
+ console.log(chalk25.yellow("\n\u{1F4A1} Examples:"));
3530
+ console.log(chalk25.gray(" pnpm spfn add @spfn/cms"));
3531
+ console.log(chalk25.gray(" pnpm spfn add @mycompany/spfn-analytics"));
2903
3532
  process.exit(1);
2904
3533
  }
2905
- console.log(chalk23.blue(`
3534
+ console.log(chalk25.blue(`
2906
3535
  \u{1F4E6} Setting up ${packageName}...
2907
3536
  `));
2908
3537
  try {
2909
- const pkgPath = join16(process.cwd(), "node_modules", ...packageName.split("/"));
2910
- const pkgJsonPath = join16(pkgPath, "package.json");
2911
- if (!existsSync19(pkgJsonPath)) {
3538
+ const pkgPath = join18(process.cwd(), "node_modules", ...packageName.split("/"));
3539
+ const pkgJsonPath = join18(pkgPath, "package.json");
3540
+ if (!existsSync21(pkgJsonPath)) {
2912
3541
  const installSpinner = ora11("Installing package...").start();
2913
3542
  try {
2914
3543
  await execAsync2(`pnpm add ${packageName}`);
@@ -2918,21 +3547,21 @@ async function addPackage(packageName) {
2918
3547
  throw error;
2919
3548
  }
2920
3549
  } else {
2921
- console.log(chalk23.gray("\u2713 Package already installed (using local version)\n"));
3550
+ console.log(chalk25.gray("\u2713 Package already installed (using local version)\n"));
2922
3551
  }
2923
- if (!existsSync19(pkgJsonPath)) {
3552
+ if (!existsSync21(pkgJsonPath)) {
2924
3553
  throw new Error(`Package ${packageName} not found after installation`);
2925
3554
  }
2926
- const pkgJson = JSON.parse(readFileSync6(pkgJsonPath, "utf-8"));
3555
+ const pkgJson = JSON.parse(readFileSync7(pkgJsonPath, "utf-8"));
2927
3556
  if (pkgJson.spfn?.migrations) {
2928
- console.log(chalk23.blue(`
3557
+ console.log(chalk25.blue(`
2929
3558
  \u{1F5C4}\uFE0F Setting up database for ${packageName}...
2930
3559
  `));
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
3560
+ const { env: env7 } = await import("@spfn/core/config");
3561
+ if (!env7.DATABASE_URL) {
3562
+ console.log(chalk25.yellow("\u26A0\uFE0F DATABASE_URL not found"));
3563
+ console.log(chalk25.gray("Skipping database setup. Run migrations manually when ready:\n"));
3564
+ console.log(chalk25.gray(` pnpm spfn db push
2936
3565
  `));
2937
3566
  } else {
2938
3567
  const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
@@ -2948,25 +3577,25 @@ async function addPackage(packageName) {
2948
3577
  throw error;
2949
3578
  }
2950
3579
  } else {
2951
- console.log(chalk23.gray("\u2139\uFE0F No migrations found for this package"));
3580
+ console.log(chalk25.gray("\u2139\uFE0F No migrations found for this package"));
2952
3581
  }
2953
3582
  }
2954
3583
  } else {
2955
- console.log(chalk23.gray("\n\u2139\uFE0F No database migrations to apply"));
3584
+ console.log(chalk25.gray("\n\u2139\uFE0F No database migrations to apply"));
2956
3585
  }
2957
- console.log(chalk23.green(`
3586
+ console.log(chalk25.green(`
2958
3587
  \u2705 ${packageName} installed successfully!
2959
3588
  `));
2960
3589
  if (pkgJson.spfn?.setupMessage) {
2961
- console.log(chalk23.cyan("\u{1F4DA} Setup Guide:"));
3590
+ console.log(chalk25.cyan("\u{1F4DA} Setup Guide:"));
2962
3591
  console.log(pkgJson.spfn.setupMessage);
2963
3592
  console.log();
2964
3593
  }
2965
3594
  } catch (error) {
2966
- console.error(chalk23.red(`
3595
+ console.error(chalk25.red(`
2967
3596
  \u274C Failed to install ${packageName}
2968
3597
  `));
2969
- console.error(chalk23.red(error instanceof Error ? error.message : "Unknown error"));
3598
+ console.error(chalk25.red(error instanceof Error ? error.message : "Unknown error"));
2970
3599
  process.exit(1);
2971
3600
  }
2972
3601
  }
@@ -2976,16 +3605,16 @@ var addCommand = new Command10("add").description("Install and set up SPFN ecosy
2976
3605
  init_logger();
2977
3606
  import { Command as Command11 } from "commander";
2978
3607
  import ora12 from "ora";
2979
- import { join as join25 } from "path";
2980
- import { existsSync as existsSync22 } from "fs";
2981
- import chalk25 from "chalk";
3608
+ import { join as join27 } from "path";
3609
+ import { existsSync as existsSync24 } from "fs";
3610
+ import chalk27 from "chalk";
2982
3611
 
2983
3612
  // src/commands/generate/prompts.ts
2984
3613
  init_logger();
2985
- import prompts6 from "prompts";
2986
- import chalk24 from "chalk";
3614
+ import prompts7 from "prompts";
3615
+ import chalk26 from "chalk";
2987
3616
  async function promptScope() {
2988
- const response = await prompts6({
3617
+ const response = await prompts7({
2989
3618
  type: "text",
2990
3619
  name: "scope",
2991
3620
  message: "NPM scope (e.g., @mycompany, @username):",
@@ -3007,7 +3636,7 @@ async function promptScope() {
3007
3636
  return response.scope;
3008
3637
  }
3009
3638
  async function promptFunctionName() {
3010
- const response = await prompts6({
3639
+ const response = await prompts7({
3011
3640
  type: "text",
3012
3641
  name: "fnName",
3013
3642
  message: "Function name:",
@@ -3028,7 +3657,7 @@ async function promptFunctionName() {
3028
3657
  return response.fnName;
3029
3658
  }
3030
3659
  async function promptDescription(fnName) {
3031
- const response = await prompts6({
3660
+ const response = await prompts7({
3032
3661
  type: "text",
3033
3662
  name: "description",
3034
3663
  message: "Function description:",
@@ -3037,7 +3666,7 @@ async function promptDescription(fnName) {
3037
3666
  return response.description || "A description of what this module does";
3038
3667
  }
3039
3668
  async function promptEntities() {
3040
- const response = await prompts6({
3669
+ const response = await prompts7({
3041
3670
  type: "list",
3042
3671
  name: "entities",
3043
3672
  message: "Entity names (comma-separated, press enter to skip):",
@@ -3049,14 +3678,14 @@ async function promptEntities() {
3049
3678
  async function confirmConfiguration(config) {
3050
3679
  const { scope, fnName, description, entities, enableCache, enableRoutes } = config;
3051
3680
  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")}`);
3681
+ logger.info(chalk26.bold("\u26A1 Function Configuration:"));
3682
+ console.log(` ${chalk26.gray("Package:")} ${chalk26.cyan(`${scope}/${fnName}`)}`);
3683
+ console.log(` ${chalk26.gray("Description:")} ${description}`);
3684
+ console.log(` ${chalk26.gray("Entities:")} ${entities.length > 0 ? entities.join(", ") : chalk26.gray("none")}`);
3685
+ console.log(` ${chalk26.gray("Cache:")} ${enableCache ? chalk26.green("yes") : chalk26.gray("no")}`);
3686
+ console.log(` ${chalk26.gray("Routes:")} ${enableRoutes ? chalk26.green("yes") : chalk26.gray("no")}`);
3058
3687
  console.log("");
3059
- const { confirmed } = await prompts6({
3688
+ const { confirmed } = await prompts7({
3060
3689
  type: "confirm",
3061
3690
  name: "confirmed",
3062
3691
  message: "Create function?",
@@ -3070,12 +3699,12 @@ async function confirmConfiguration(config) {
3070
3699
  }
3071
3700
 
3072
3701
  // src/commands/generate/generators/structure.ts
3073
- import { join as join24 } from "path";
3074
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync17 } from "fs";
3702
+ import { join as join26 } from "path";
3703
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync18 } from "fs";
3075
3704
 
3076
3705
  // src/commands/generate/generators/config.ts
3077
- import { join as join18 } from "path";
3078
- import { writeFileSync as writeFileSync11, mkdirSync as mkdirSync3 } from "fs";
3706
+ import { join as join20 } from "path";
3707
+ import { writeFileSync as writeFileSync12, mkdirSync as mkdirSync3 } from "fs";
3079
3708
 
3080
3709
  // src/commands/generate/string-utils.ts
3081
3710
  function toPascalCase(str) {
@@ -3104,30 +3733,30 @@ function toSafeSchemaName(str) {
3104
3733
  }
3105
3734
 
3106
3735
  // 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";
3736
+ import { readFileSync as readFileSync8, existsSync as existsSync22 } from "fs";
3737
+ import { join as join19, dirname as dirname2 } from "path";
3109
3738
  import { fileURLToPath as fileURLToPath2 } from "url";
3110
3739
  function findTemplatesPath2() {
3111
3740
  const __filename = fileURLToPath2(import.meta.url);
3112
3741
  const __dirname2 = dirname2(__filename);
3113
- const distPath = join17(__dirname2, "commands", "generate", "templates");
3114
- if (existsSync20(distPath)) {
3742
+ const distPath = join19(__dirname2, "commands", "generate", "templates");
3743
+ if (existsSync22(distPath)) {
3115
3744
  return distPath;
3116
3745
  }
3117
- const sameDirPath = join17(__dirname2, "templates");
3118
- if (existsSync20(sameDirPath)) {
3746
+ const sameDirPath = join19(__dirname2, "templates");
3747
+ if (existsSync22(sameDirPath)) {
3119
3748
  return sameDirPath;
3120
3749
  }
3121
- const srcPath = join17(__dirname2, "..", "..", "src", "commands", "generate", "templates");
3122
- if (existsSync20(srcPath)) {
3750
+ const srcPath = join19(__dirname2, "..", "..", "src", "commands", "generate", "templates");
3751
+ if (existsSync22(srcPath)) {
3123
3752
  return srcPath;
3124
3753
  }
3125
3754
  throw new Error(`Templates directory not found. Tried: ${distPath}, ${sameDirPath}, ${srcPath}`);
3126
3755
  }
3127
3756
  function loadTemplate(templateName, variables) {
3128
3757
  const templatesDir = findTemplatesPath2();
3129
- const templatePath = join17(templatesDir, `${templateName}.template`);
3130
- let content = readFileSync7(templatePath, "utf-8");
3758
+ const templatePath = join19(templatesDir, `${templateName}.template`);
3759
+ let content = readFileSync8(templatePath, "utf-8");
3131
3760
  for (const [key, value] of Object.entries(variables)) {
3132
3761
  const regex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
3133
3762
  content = content.replace(regex, value);
@@ -3229,8 +3858,8 @@ function generatePackageJson(fnDir, scope, fnName, description) {
3229
3858
  vitest: "^4.0.6"
3230
3859
  }
3231
3860
  };
3232
- writeFileSync11(
3233
- join18(fnDir, "package.json"),
3861
+ writeFileSync12(
3862
+ join20(fnDir, "package.json"),
3234
3863
  JSON.stringify(content, null, 4) + "\n"
3235
3864
  );
3236
3865
  }
@@ -3263,8 +3892,8 @@ function generateTsConfig(fnDir) {
3263
3892
  include: ["src/**/*"],
3264
3893
  exclude: ["node_modules", "dist", "**/*.test.ts", "**/__tests__/**"]
3265
3894
  };
3266
- writeFileSync11(
3267
- join18(fnDir, "tsconfig.json"),
3895
+ writeFileSync12(
3896
+ join20(fnDir, "tsconfig.json"),
3268
3897
  JSON.stringify(content, null, 4) + "\n"
3269
3898
  );
3270
3899
  }
@@ -3340,7 +3969,7 @@ export default defineConfig({
3340
3969
  ],
3341
3970
  });
3342
3971
  `;
3343
- writeFileSync11(join18(fnDir, "tsup.config.ts"), content);
3972
+ writeFileSync12(join20(fnDir, "tsup.config.ts"), content);
3344
3973
  }
3345
3974
  function generateDrizzleConfig(fnDir, scope, fnName) {
3346
3975
  const schemaName = `spfn_${toSnakeCase(fnName)}`;
@@ -3360,7 +3989,7 @@ export default defineConfig({
3360
3989
  schemaFilter: ['${schemaName}'], // Only generate for ${fnName} schema
3361
3990
  });
3362
3991
  `;
3363
- writeFileSync11(join18(fnDir, "drizzle.config.ts"), content);
3992
+ writeFileSync12(join20(fnDir, "drizzle.config.ts"), content);
3364
3993
  }
3365
3994
  function generateExampleGenerator(fnDir, scope, fnName) {
3366
3995
  const pascalName = toPascalCase(fnName);
@@ -3429,10 +4058,10 @@ export const moduleName = '${fnName}';
3429
4058
  };
3430
4059
  }
3431
4060
  `;
3432
- const generatorsDir = join18(fnDir, "src/server/generators");
4061
+ const generatorsDir = join20(fnDir, "src/server/generators");
3433
4062
  mkdirSync3(generatorsDir, { recursive: true });
3434
- writeFileSync11(
3435
- join18(generatorsDir, "example-generator.ts"),
4063
+ writeFileSync12(
4064
+ join20(generatorsDir, "example-generator.ts"),
3436
4065
  content
3437
4066
  );
3438
4067
  const indexContent = `/**
@@ -3446,8 +4075,8 @@ export const moduleName = '${fnName}';
3446
4075
 
3447
4076
  export { create${pascalName}ExampleGenerator } from './example-generator.js';
3448
4077
  `;
3449
- writeFileSync11(
3450
- join18(generatorsDir, "index.ts"),
4078
+ writeFileSync12(
4079
+ join20(generatorsDir, "index.ts"),
3451
4080
  indexContent
3452
4081
  );
3453
4082
  }
@@ -3936,15 +4565,15 @@ Contributions are welcome! Please follow the development workflow above.
3936
4565
 
3937
4566
  MIT
3938
4567
  `;
3939
- writeFileSync11(join18(fnDir, "README.md"), content);
4568
+ writeFileSync12(join20(fnDir, "README.md"), content);
3940
4569
  }
3941
4570
 
3942
4571
  // src/commands/generate/generators/entity.ts
3943
- import { join as join19 } from "path";
3944
- import { writeFileSync as writeFileSync12, existsSync as existsSync21 } from "fs";
4572
+ import { join as join21 } from "path";
4573
+ import { writeFileSync as writeFileSync13, existsSync as existsSync23 } from "fs";
3945
4574
  function generateSchema(fnDir, scope, fnName) {
3946
- const schemaFilePath = join19(fnDir, "src/server/entities/schema.ts");
3947
- if (existsSync21(schemaFilePath)) {
4575
+ const schemaFilePath = join21(fnDir, "src/server/entities/schema.ts");
4576
+ if (existsSync23(schemaFilePath)) {
3948
4577
  return;
3949
4578
  }
3950
4579
  const packageName = `${scope}/${fnName}`;
@@ -3955,7 +4584,7 @@ function generateSchema(fnDir, scope, fnName) {
3955
4584
  PACKAGE_NAME: packageName,
3956
4585
  SCHEMA_VAR_NAME: schemaVarName
3957
4586
  });
3958
- writeFileSync12(schemaFilePath, content);
4587
+ writeFileSync13(schemaFilePath, content);
3959
4588
  }
3960
4589
  function generateEntity(fnDir, scope, fnName, entityName) {
3961
4590
  const safeScope = toSafeSchemaName(scope);
@@ -3974,8 +4603,8 @@ function generateEntity(fnDir, scope, fnName, entityName) {
3974
4603
  SCHEMA_VAR_NAME: schemaVarName,
3975
4604
  SCHEMA_FILE_NAME: schemaFileName
3976
4605
  });
3977
- writeFileSync12(
3978
- join19(fnDir, `src/server/entities/${toKebabCase(entityName)}.ts`),
4606
+ writeFileSync13(
4607
+ join21(fnDir, `src/server/entities/${toKebabCase(entityName)}.ts`),
3979
4608
  content
3980
4609
  );
3981
4610
  }
@@ -3983,12 +4612,12 @@ function generateEntitiesIndex(fnDir, entities) {
3983
4612
  const schemaExport = `export * from './schema';`;
3984
4613
  const entityExports = entities.map((entity) => `export * from './${toKebabCase(entity)}';`).join("\n");
3985
4614
  const content = [schemaExport, entityExports].filter(Boolean).join("\n");
3986
- writeFileSync12(join19(fnDir, "src/server/entities/index.ts"), content + "\n");
4615
+ writeFileSync13(join21(fnDir, "src/server/entities/index.ts"), content + "\n");
3987
4616
  }
3988
4617
 
3989
4618
  // src/commands/generate/generators/repository.ts
3990
- import { join as join20 } from "path";
3991
- import { writeFileSync as writeFileSync13 } from "fs";
4619
+ import { join as join22 } from "path";
4620
+ import { writeFileSync as writeFileSync14 } from "fs";
3992
4621
  function generateRepository(fnDir, entityName) {
3993
4622
  const pascalName = toPascalCase(entityName);
3994
4623
  const repoName = `${entityName}Repository`;
@@ -3997,19 +4626,19 @@ function generateRepository(fnDir, entityName) {
3997
4626
  ENTITY_NAME: entityName,
3998
4627
  REPO_NAME: repoName
3999
4628
  });
4000
- writeFileSync13(
4001
- join20(fnDir, `src/server/repositories/${toKebabCase(entityName)}.repository.ts`),
4629
+ writeFileSync14(
4630
+ join22(fnDir, `src/server/repositories/${toKebabCase(entityName)}.repository.ts`),
4002
4631
  content
4003
4632
  );
4004
4633
  }
4005
4634
  function generateRepositoriesIndex(fnDir, entities) {
4006
4635
  const exports = entities.map((entity) => `export * from './${toKebabCase(entity)}.repository';`).join("\n");
4007
- writeFileSync13(join20(fnDir, "src/server/repositories/index.ts"), exports + "\n");
4636
+ writeFileSync14(join22(fnDir, "src/server/repositories/index.ts"), exports + "\n");
4008
4637
  }
4009
4638
 
4010
4639
  // src/commands/generate/generators/route.ts
4011
- import { join as join21 } from "path";
4012
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync14 } from "fs";
4640
+ import { join as join23 } from "path";
4641
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync15 } from "fs";
4013
4642
  function generateRoute(fnDir, entityName) {
4014
4643
  const pascalName = toPascalCase(entityName);
4015
4644
  const repoName = `${entityName}Repository`;
@@ -4020,29 +4649,29 @@ function generateRoute(fnDir, entityName) {
4020
4649
  REPO_NAME: repoName,
4021
4650
  KEBAB_NAME: kebabName
4022
4651
  });
4023
- const routeDir = join21(fnDir, `src/server/routes/${kebabName}`);
4652
+ const routeDir = join23(fnDir, `src/server/routes/${kebabName}`);
4024
4653
  mkdirSync4(routeDir, { recursive: true });
4025
- writeFileSync14(join21(routeDir, "index.ts"), content);
4654
+ writeFileSync15(join23(routeDir, "index.ts"), content);
4026
4655
  }
4027
4656
 
4028
4657
  // src/commands/generate/generators/contract.ts
4029
- import { join as join22 } from "path";
4030
- import { writeFileSync as writeFileSync15 } from "fs";
4658
+ import { join as join24 } from "path";
4659
+ import { writeFileSync as writeFileSync16 } from "fs";
4031
4660
  function generateContract(fnDir, entityName) {
4032
4661
  const pascalName = toPascalCase(entityName);
4033
4662
  const content = loadTemplate("contract", {
4034
4663
  PASCAL_NAME: pascalName,
4035
4664
  ENTITY_NAME: entityName
4036
4665
  });
4037
- writeFileSync15(
4038
- join22(fnDir, `src/lib/contracts/${toKebabCase(entityName)}.ts`),
4666
+ writeFileSync16(
4667
+ join24(fnDir, `src/lib/contracts/${toKebabCase(entityName)}.ts`),
4039
4668
  content
4040
4669
  );
4041
4670
  }
4042
4671
 
4043
4672
  // src/commands/generate/generators/index-files.ts
4044
- import { join as join23 } from "path";
4045
- import { writeFileSync as writeFileSync16 } from "fs";
4673
+ import { join as join25 } from "path";
4674
+ import { writeFileSync as writeFileSync17 } from "fs";
4046
4675
  function generateMainIndex(fnDir, fnName) {
4047
4676
  const content = `/**
4048
4677
  * @spfn/${fnName}
@@ -4068,7 +4697,7 @@ export * from '@/lib/types/index';
4068
4697
 
4069
4698
  export * from '@/server/entities/index';
4070
4699
  `;
4071
- writeFileSync16(join23(fnDir, "src/index.ts"), content);
4700
+ writeFileSync17(join25(fnDir, "src/index.ts"), content);
4072
4701
  }
4073
4702
  function generateServerIndex(fnDir) {
4074
4703
  const content = `/**
@@ -4103,7 +4732,7 @@ export * from '@/server/repositories/index';
4103
4732
 
4104
4733
  // TODO: Export helpers here
4105
4734
  `;
4106
- writeFileSync16(join23(fnDir, "src/server.ts"), content);
4735
+ writeFileSync17(join25(fnDir, "src/server.ts"), content);
4107
4736
  }
4108
4737
  function generateClientIndex(fnDir) {
4109
4738
  const content = `/**
@@ -4136,7 +4765,7 @@ export * from './client/store';
4136
4765
 
4137
4766
  export * from './client/components';
4138
4767
  `;
4139
- writeFileSync16(join23(fnDir, "src/client.ts"), content);
4768
+ writeFileSync17(join25(fnDir, "src/client.ts"), content);
4140
4769
  }
4141
4770
  function generateTypesFile(fnDir, fnName) {
4142
4771
  const content = `/**
@@ -4148,7 +4777,7 @@ function generateTypesFile(fnDir, fnName) {
4148
4777
 
4149
4778
  export * from '@/lib/types/index';
4150
4779
  `;
4151
- writeFileSync16(join23(fnDir, "src/types.ts"), content);
4780
+ writeFileSync17(join25(fnDir, "src/types.ts"), content);
4152
4781
  }
4153
4782
 
4154
4783
  // src/commands/generate/generators/structure.ts
@@ -4170,7 +4799,7 @@ async function generateFunctionStructure(options) {
4170
4799
  "src/client/store",
4171
4800
  "src/client/components"
4172
4801
  ];
4173
- dirs.forEach((dir) => mkdirSync5(join24(fnDir, dir), { recursive: true }));
4802
+ dirs.forEach((dir) => mkdirSync5(join26(fnDir, dir), { recursive: true }));
4174
4803
  generatePackageJson(fnDir, scope, fnName, description);
4175
4804
  generateTsConfig(fnDir);
4176
4805
  generateTsupConfig(fnDir);
@@ -4190,15 +4819,15 @@ async function generateFunctionStructure(options) {
4190
4819
  generateEntitiesIndex(fnDir, entities);
4191
4820
  generateRepositoriesIndex(fnDir, entities);
4192
4821
  } 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");
4822
+ writeFileSync18(join26(fnDir, "src/server/entities/index.ts"), "// Export your entities here\nexport {}\n");
4823
+ writeFileSync18(join26(fnDir, "src/server/repositories/index.ts"), "// Export your repositories here\nexport {}\n");
4824
+ }
4825
+ writeFileSync18(join26(fnDir, "src/client/hooks/index.ts"), "/**\n * Client Hooks\n */\n\n// TODO: Add hooks (e.g., useAuth, useData, etc.)\nexport {}\n");
4826
+ writeFileSync18(join26(fnDir, "src/client/store/index.ts"), "/**\n * Client Store\n */\n\n// TODO: Add Zustand store if needed\nexport {}\n");
4827
+ writeFileSync18(join26(fnDir, "src/client/components/index.ts"), "/**\n * Client Components\n */\n\n// TODO: Add React components\nexport {}\n");
4828
+ writeFileSync18(join26(fnDir, "src/client/index.ts"), "/**\n * Client Module Entry\n */\n\nexport * from './hooks';\nexport * from './store';\nexport * from './components';\n");
4829
+ writeFileSync18(join26(fnDir, "src/lib/types/index.ts"), "/**\n * Shared Type Definitions\n */\n\n// Add your shared types here\nexport {}\n");
4830
+ writeFileSync18(join26(fnDir, "src/lib/contracts/index.ts"), "/**\n * API Contracts\n */\n\n// Export your contracts here\nexport {}\n");
4202
4831
  generateMainIndex(fnDir, fnName);
4203
4832
  generateServerIndex(fnDir);
4204
4833
  generateClientIndex(fnDir);
@@ -4222,8 +4851,8 @@ async function generateFunction(name, options) {
4222
4851
  logger.error("Function name is required");
4223
4852
  process.exit(1);
4224
4853
  }
4225
- const fnDir = join25(cwd, fnName);
4226
- if (existsSync22(fnDir)) {
4854
+ const fnDir = join27(cwd, fnName);
4855
+ if (existsSync24(fnDir)) {
4227
4856
  logger.error(`Directory ${fnName} already exists at ${fnDir}`);
4228
4857
  process.exit(1);
4229
4858
  }
@@ -4268,13 +4897,13 @@ async function generateFunction(name, options) {
4268
4897
  });
4269
4898
  spinner.succeed("Function structure generated");
4270
4899
  console.log("");
4271
- logger.success(`\u2728 Package ${chalk25.cyan(`${scope}/${fnName}`)} created successfully!
4900
+ logger.success(`\u2728 Package ${chalk27.cyan(`${scope}/${fnName}`)} created successfully!
4272
4901
  `);
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")}`);
4902
+ logger.info(chalk27.bold("\u{1F4DA} Next steps:"));
4903
+ console.log(` ${chalk27.gray("1.")} cd ${fnName}`);
4904
+ console.log(` ${chalk27.gray("2.")} pnpm install ${chalk27.dim("(in monorepo root)")}`);
4905
+ console.log(` ${chalk27.gray("3.")} pnpm build`);
4906
+ console.log(` ${chalk27.gray("4.")} ${chalk27.dim("Use the package in your app")}`);
4278
4907
  console.log("");
4279
4908
  } catch (error) {
4280
4909
  spinner.fail("Failed to generate function");
@@ -4287,14 +4916,30 @@ generateCommand.command("fn").description("Generate a new SPFN function module")
4287
4916
 
4288
4917
  // src/commands/env.ts
4289
4918
  import { Command as Command12 } from "commander";
4290
- import chalk26 from "chalk";
4291
- import { existsSync as existsSync23, readFileSync as readFileSync8, writeFileSync as writeFileSync18 } from "fs";
4919
+ import chalk28 from "chalk";
4920
+ import { existsSync as existsSync25, readFileSync as readFileSync9, writeFileSync as writeFileSync19 } from "fs";
4292
4921
  import { resolve } from "path";
4293
4922
  import { parse } from "dotenv";
4294
- var ENV_FILES = {
4923
+ var VALID_ENVS = ["local", "development", "staging", "production", "test"];
4924
+ var BASE_ENV_FILES = {
4295
4925
  nextjs: [".env", ".env.local"],
4296
4926
  server: [".env.server", ".env.server.local"]
4297
4927
  };
4928
+ function getEnvFilesForEnvironment(nodeEnv) {
4929
+ const files = [".env"];
4930
+ if (nodeEnv) {
4931
+ files.push(`.env.${nodeEnv}`);
4932
+ }
4933
+ if (nodeEnv !== "test") {
4934
+ files.push(".env.local");
4935
+ }
4936
+ if (nodeEnv) {
4937
+ files.push(`.env.${nodeEnv}.local`);
4938
+ }
4939
+ files.push(".env.server");
4940
+ files.push(".env.server.local");
4941
+ return files;
4942
+ }
4298
4943
  function getTargetFile(schema) {
4299
4944
  const isNextjs = schema.nextjs ?? schema.key?.startsWith("NEXT_PUBLIC_");
4300
4945
  if (isNextjs) {
@@ -4320,27 +4965,26 @@ async function loadEnvSchema(packageName) {
4320
4965
  }
4321
4966
  function formatType(type) {
4322
4967
  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
4968
+ string: chalk28.green,
4969
+ number: chalk28.blue,
4970
+ boolean: chalk28.yellow,
4971
+ url: chalk28.cyan,
4972
+ enum: chalk28.magenta,
4973
+ json: chalk28.red
4329
4974
  };
4330
- const colorFn = typeColors[type] || chalk26.white;
4331
- return colorFn(type);
4975
+ return (typeColors[type] || chalk28.white)(type);
4332
4976
  }
4333
4977
  function formatDefault(value, type) {
4334
4978
  if (value === void 0) {
4335
- return chalk26.dim("(none)");
4979
+ return chalk28.dim("(none)");
4336
4980
  }
4337
4981
  if (type === "string" || type === "url") {
4338
- return chalk26.green(`"${value}"`);
4982
+ return chalk28.green(`"${value}"`);
4339
4983
  }
4340
4984
  if (type === "boolean") {
4341
- return value ? chalk26.green("true") : chalk26.red("false");
4985
+ return value ? chalk28.green("true") : chalk28.red("false");
4342
4986
  }
4343
- return chalk26.cyan(String(value));
4987
+ return chalk28.cyan(String(value));
4344
4988
  }
4345
4989
  async function listEnvVars(options) {
4346
4990
  const packageName = options.package || "@spfn/core";
@@ -4354,28 +4998,28 @@ async function listEnvVars(options) {
4354
4998
  acc[target].push([key, schema]);
4355
4999
  return acc;
4356
5000
  }, {});
4357
- console.log(chalk26.blue.bold(`
5001
+ console.log(chalk28.blue.bold(`
4358
5002
  \u{1F4CB} Environment Variables by File (${packageName})
4359
5003
  `));
4360
5004
  for (const [file, vars] of Object.entries(grouped)) {
4361
- console.log(chalk26.bold.magenta(`
5005
+ console.log(chalk28.bold.magenta(`
4362
5006
  ${file}`));
4363
- console.log(chalk26.dim("\u2500".repeat(50)));
5007
+ console.log(chalk28.dim("\u2500".repeat(50)));
4364
5008
  for (const [key, schema] of vars) {
4365
5009
  printEnvVar(key, schema);
4366
5010
  }
4367
5011
  }
4368
5012
  } else {
4369
- console.log(chalk26.blue.bold(`
5013
+ console.log(chalk28.blue.bold(`
4370
5014
  \u{1F4CB} Environment Variables (${packageName})
4371
5015
  `));
4372
5016
  for (const [key, schema] of allVars) {
4373
5017
  printEnvVar(key, schema, true);
4374
5018
  }
4375
5019
  }
4376
- console.log(chalk26.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
5020
+ console.log(chalk28.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
4377
5021
  } catch (error) {
4378
- console.error(chalk26.red(`
5022
+ console.error(chalk28.red(`
4379
5023
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
4380
5024
  `));
4381
5025
  process.exit(1);
@@ -4383,17 +5027,17 @@ ${file}`));
4383
5027
  }
4384
5028
  function printEnvVar(key, schema, showFile = false) {
4385
5029
  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)}`);
5030
+ const requiredStr = schema.required || schema.default !== void 0 ? chalk28.red("[required]") : chalk28.dim("[optional]");
5031
+ const sensitiveStr = schema.sensitive ? chalk28.yellow(" [sensitive]") : "";
5032
+ const fileStr = showFile ? chalk28.dim(` \u2192 ${getTargetFile(schema)}`) : "";
5033
+ console.log(`${chalk28.bold.cyan(key)} ${chalk28.dim("(")}${typeStr}${chalk28.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
5034
+ console.log(` ${chalk28.dim(schema.description)}`);
4391
5035
  if (schema.default !== void 0) {
4392
- console.log(` ${chalk26.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
5036
+ console.log(` ${chalk28.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
4393
5037
  }
4394
5038
  if (schema.examples && schema.examples.length > 0) {
4395
5039
  const exampleStr = schema.examples.map((ex) => formatDefault(ex, schema.type)).join(", ");
4396
- console.log(` ${chalk26.dim("Examples:")} ${exampleStr}`);
5040
+ console.log(` ${chalk28.dim("Examples:")} ${exampleStr}`);
4397
5041
  }
4398
5042
  console.log();
4399
5043
  }
@@ -4401,7 +5045,7 @@ async function showEnvStats(options) {
4401
5045
  const packageName = options.package || "@spfn/core";
4402
5046
  try {
4403
5047
  const envSchema = await loadEnvSchema(packageName);
4404
- console.log(chalk26.blue.bold(`
5048
+ console.log(chalk28.blue.bold(`
4405
5049
  \u{1F4CA} Environment Variable Statistics (${packageName})
4406
5050
  `));
4407
5051
  const allVars = Object.entries(envSchema);
@@ -4423,24 +5067,24 @@ async function showEnvStats(options) {
4423
5067
  acc[file] = (acc[file] || 0) + 1;
4424
5068
  return acc;
4425
5069
  }, {});
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:"));
5070
+ console.log(`${chalk28.bold("Total variables:")} ${chalk28.cyan(allVars.length)}`);
5071
+ console.log(`${chalk28.bold("Required:")} ${chalk28.red(required.length)}`);
5072
+ console.log(`${chalk28.bold("Optional:")} ${chalk28.dim(optional.length)}`);
5073
+ console.log(`${chalk28.bold("Sensitive:")} ${chalk28.yellow(sensitive.length)}`);
5074
+ console.log(chalk28.bold("\nBy Target:"));
5075
+ console.log(` ${chalk28.blue("Next.js accessible:")} ${chalk28.cyan(nextjsVars.length)}`);
5076
+ console.log(` ${chalk28.magenta("SPFN server only:")} ${chalk28.cyan(serverOnlyVars.length)}`);
5077
+ console.log(chalk28.bold("\nBy File:"));
4434
5078
  for (const [file, count] of Object.entries(fileCount)) {
4435
- console.log(` ${chalk26.dim(file)}: ${chalk26.cyan(count)}`);
5079
+ console.log(` ${chalk28.dim(file)}: ${chalk28.cyan(count)}`);
4436
5080
  }
4437
- console.log(chalk26.bold("\nBy Type:"));
5081
+ console.log(chalk28.bold("\nBy Type:"));
4438
5082
  for (const [type, count] of Object.entries(typeCount)) {
4439
- console.log(` ${formatType(type)}: ${chalk26.cyan(count)}`);
5083
+ console.log(` ${formatType(type)}: ${chalk28.cyan(count)}`);
4440
5084
  }
4441
5085
  console.log();
4442
5086
  } catch (error) {
4443
- console.error(chalk26.red(`
5087
+ console.error(chalk28.red(`
4444
5088
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
4445
5089
  `));
4446
5090
  process.exit(1);
@@ -4460,26 +5104,26 @@ async function searchEnvVars(query, options) {
4460
5104
  }
4461
5105
  }
4462
5106
  if (results.length === 0) {
4463
- console.log(chalk26.yellow(`
5107
+ console.log(chalk28.yellow(`
4464
5108
  \u26A0\uFE0F No environment variables found matching "${query}"
4465
5109
  `));
4466
5110
  return;
4467
5111
  }
4468
- console.log(chalk26.blue.bold(`
5112
+ console.log(chalk28.blue.bold(`
4469
5113
  \u{1F50D} Found ${results.length} environment variable(s) matching "${query}"
4470
5114
  `));
4471
5115
  for (const [key, schema] of results) {
4472
5116
  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)}`);
5117
+ const requiredStr = schema.required || schema.default !== void 0 ? chalk28.red("[required]") : chalk28.dim("[optional]");
5118
+ console.log(`${chalk28.bold.cyan(key)} ${chalk28.dim("(")}${typeStr}${chalk28.dim(")")} ${requiredStr}`);
5119
+ console.log(` ${chalk28.dim(schema.description)}`);
4476
5120
  if (schema.default !== void 0) {
4477
- console.log(` ${chalk26.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
5121
+ console.log(` ${chalk28.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
4478
5122
  }
4479
5123
  console.log();
4480
5124
  }
4481
5125
  } catch (error) {
4482
- console.error(chalk26.red(`
5126
+ console.error(chalk28.red(`
4483
5127
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
4484
5128
  `));
4485
5129
  process.exit(1);
@@ -4489,8 +5133,19 @@ var envCommand = new Command12("env").description("Manage environment variables"
4489
5133
  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
5134
  envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(showEnvStats);
4491
5135
  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);
5136
+ function validateEnvOption(envValue) {
5137
+ if (!VALID_ENVS.includes(envValue)) {
5138
+ console.error(chalk28.red(`
5139
+ \u274C Invalid environment: "${envValue}"`));
5140
+ console.log(chalk28.dim(` Valid values: ${VALID_ENVS.join(", ")}
5141
+ `));
5142
+ process.exit(1);
5143
+ }
5144
+ return envValue;
5145
+ }
4492
5146
  async function initEnvFiles(options) {
4493
5147
  const packageName = options.package || "@spfn/core";
5148
+ const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
4494
5149
  const cwd = process.cwd();
4495
5150
  try {
4496
5151
  const envSchema = await loadEnvSchema(packageName);
@@ -4502,31 +5157,57 @@ async function initEnvFiles(options) {
4502
5157
  acc[exampleFile].push([key, schema]);
4503
5158
  return acc;
4504
5159
  }, {});
4505
- console.log(chalk26.blue.bold(`
5160
+ if (targetEnv) {
5161
+ console.log(chalk28.blue.bold(`
5162
+ \u{1F680} Generating .env template files for ${chalk28.cyan(targetEnv)} environment
5163
+ `));
5164
+ const envSpecificFiles = {};
5165
+ const committedVars = allVars.filter(([_, schema]) => !schema.sensitive);
5166
+ if (committedVars.length > 0) {
5167
+ envSpecificFiles[`.env.${targetEnv}.example`] = committedVars;
5168
+ }
5169
+ const sensitiveVars = allVars.filter(([_, schema]) => schema.sensitive);
5170
+ if (sensitiveVars.length > 0) {
5171
+ envSpecificFiles[`.env.${targetEnv}.local.example`] = sensitiveVars;
5172
+ }
5173
+ const allGrouped = { ...grouped, ...envSpecificFiles };
5174
+ for (const [file, vars] of Object.entries(allGrouped)) {
5175
+ writeEnvTemplate(cwd, file, vars, options.force ?? false);
5176
+ }
5177
+ } else {
5178
+ console.log(chalk28.blue.bold(`
4506
5179
  \u{1F680} Generating .env template files
4507
5180
  `));
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;
5181
+ for (const [file, vars] of Object.entries(grouped)) {
5182
+ writeEnvTemplate(cwd, file, vars, options.force ?? false);
4513
5183
  }
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"));
5184
+ }
5185
+ console.log(chalk28.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
5186
+ console.log(chalk28.dim(" cp .env.example .env"));
5187
+ console.log(chalk28.dim(" cp .env.local.example .env.local"));
5188
+ console.log(chalk28.dim(" cp .env.server.example .env.server"));
5189
+ console.log(chalk28.dim(" cp .env.server.local.example .env.server.local"));
5190
+ if (targetEnv) {
5191
+ console.log(chalk28.dim(` cp .env.${targetEnv}.example .env.${targetEnv}`));
5192
+ console.log(chalk28.dim(` cp .env.${targetEnv}.local.example .env.${targetEnv}.local`));
5193
+ }
5194
+ console.log("");
4523
5195
  } catch (error) {
4524
- console.error(chalk26.red(`
5196
+ console.error(chalk28.red(`
4525
5197
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
4526
5198
  `));
4527
5199
  process.exit(1);
4528
5200
  }
4529
5201
  }
5202
+ function writeEnvTemplate(cwd, file, vars, force) {
5203
+ const filePath = resolve(cwd, file);
5204
+ if (existsSync25(filePath) && !force) {
5205
+ console.log(chalk28.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
5206
+ return;
5207
+ }
5208
+ writeFileSync19(filePath, generateEnvFileContent(vars), "utf-8");
5209
+ console.log(chalk28.green(` \u2705 ${file} (${vars.length} variables)`));
5210
+ }
4530
5211
  function generateEnvFileContent(vars) {
4531
5212
  const lines = [
4532
5213
  "# Auto-generated by spfn env init",
@@ -4554,28 +5235,30 @@ function generateEnvFileContent(vars) {
4554
5235
  }
4555
5236
  async function checkEnvFiles(options) {
4556
5237
  const packageName = options.package || "@spfn/core";
5238
+ const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
4557
5239
  const cwd = process.cwd();
4558
5240
  try {
4559
5241
  const envSchema = await loadEnvSchema(packageName);
4560
5242
  const allVars = Object.entries(envSchema);
4561
- console.log(chalk26.blue.bold(`
4562
- \u{1F50D} Checking .env files against schema
5243
+ const envLabel = targetEnv ? ` (${targetEnv})` : "";
5244
+ console.log(chalk28.blue.bold(`
5245
+ \u{1F50D} Checking .env files against schema${envLabel}
4563
5246
  `));
4564
- const allFiles = [...ENV_FILES.nextjs, ...ENV_FILES.server];
5247
+ const filesToCheck = targetEnv ? getEnvFilesForEnvironment(targetEnv) : [...BASE_ENV_FILES.nextjs, ...BASE_ENV_FILES.server];
4565
5248
  const loadedEnv = {};
4566
5249
  const issues = [];
4567
5250
  const warnings = [];
4568
- for (const file of allFiles) {
5251
+ for (const file of filesToCheck) {
4569
5252
  const filePath = resolve(cwd, file);
4570
- if (!existsSync23(filePath)) {
5253
+ if (!existsSync25(filePath)) {
4571
5254
  continue;
4572
5255
  }
4573
- const content = readFileSync8(filePath, "utf-8");
5256
+ const content = readFileSync9(filePath, "utf-8");
4574
5257
  const parsed = parse(content);
4575
5258
  for (const [key, value] of Object.entries(parsed)) {
4576
5259
  loadedEnv[key] = { value: value || "", file };
4577
5260
  }
4578
- console.log(chalk26.dim(` \u{1F4C4} ${file} loaded`));
5261
+ console.log(chalk28.dim(` \u{1F4C4} ${file} loaded`));
4579
5262
  }
4580
5263
  console.log("");
4581
5264
  for (const [key, schema] of allVars) {
@@ -4583,21 +5266,21 @@ async function checkEnvFiles(options) {
4583
5266
  const found = loadedEnv[key];
4584
5267
  if (!found) {
4585
5268
  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`);
5269
+ issues.push(`${chalk28.red("\u2717")} ${chalk28.cyan(key)} is required but not found in any .env file`);
4587
5270
  }
4588
5271
  continue;
4589
5272
  }
4590
- const isNextjsFile = ENV_FILES.nextjs.includes(found.file);
4591
- const isServerFile = ENV_FILES.server.includes(found.file);
5273
+ const isNextjsFile = BASE_ENV_FILES.nextjs.includes(found.file);
5274
+ const isServerFile = BASE_ENV_FILES.server.includes(found.file);
4592
5275
  const shouldBeNextjs = schema.nextjs ?? key.startsWith("NEXT_PUBLIC_");
4593
5276
  if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
4594
5277
  if (schema.sensitive) {
4595
5278
  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!)`
5279
+ `${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
5280
  );
4598
5281
  } else {
4599
5282
  warnings.push(
4600
- `${chalk26.yellow("\u26A0")} ${chalk26.cyan(key)} should be in ${chalk26.magenta(expectedFile)}, but found in ${chalk26.dim(found.file)}`
5283
+ `${chalk28.yellow("\u26A0")} ${chalk28.cyan(key)} should be in ${chalk28.magenta(expectedFile)}, but found in ${chalk28.dim(found.file)}`
4601
5284
  );
4602
5285
  }
4603
5286
  }
@@ -4605,51 +5288,64 @@ async function checkEnvFiles(options) {
4605
5288
  for (const [key, { file }] of Object.entries(loadedEnv)) {
4606
5289
  const inSchema = allVars.some(([k]) => k === key);
4607
5290
  if (!inSchema) {
4608
- warnings.push(`${chalk26.yellow("\u26A0")} ${chalk26.cyan(key)} in ${chalk26.dim(file)} is not in schema`);
5291
+ warnings.push(`${chalk28.yellow("\u26A0")} ${chalk28.cyan(key)} in ${chalk28.dim(file)} is not in schema`);
4609
5292
  }
4610
5293
  }
4611
5294
  if (issues.length > 0) {
4612
- console.log(chalk26.red.bold("Issues:"));
5295
+ console.log(chalk28.red.bold("Issues:"));
4613
5296
  for (const issue of issues) {
4614
5297
  console.log(` ${issue}`);
4615
5298
  }
4616
5299
  console.log("");
4617
5300
  }
4618
5301
  if (warnings.length > 0) {
4619
- console.log(chalk26.yellow.bold("Warnings:"));
5302
+ console.log(chalk28.yellow.bold("Warnings:"));
4620
5303
  for (const warning of warnings) {
4621
5304
  console.log(` ${warning}`);
4622
5305
  }
4623
5306
  console.log("");
4624
5307
  }
4625
5308
  if (issues.length === 0 && warnings.length === 0) {
4626
- console.log(chalk26.green("\u2705 All environment variables are correctly configured!\n"));
5309
+ console.log(chalk28.green("\u2705 All environment variables are correctly configured!\n"));
4627
5310
  } else {
4628
- console.log(chalk26.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
5311
+ console.log(chalk28.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
4629
5312
  `));
4630
5313
  if (issues.length > 0) {
4631
5314
  process.exit(1);
4632
5315
  }
4633
5316
  }
4634
5317
  } catch (error) {
4635
- console.error(chalk26.red(`
5318
+ console.error(chalk28.red(`
4636
5319
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
4637
5320
  `));
4638
5321
  process.exit(1);
4639
5322
  }
4640
5323
  }
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);
5324
+ 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);
5325
+ 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
5326
  async function validateEnvVars(options) {
4644
5327
  const packages = options.packages || ["@spfn/core"];
4645
- console.log(chalk26.blue.bold(`
5328
+ const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
5329
+ if (targetEnv) {
5330
+ const { loadEnv: loadEnv8 } = await import("@spfn/core/env/loader");
5331
+ const result = loadEnv8({ nodeEnv: targetEnv });
5332
+ console.log(chalk28.blue.bold(`
5333
+ \u{1F50D} Validating environment variables for ${chalk28.cyan(targetEnv)}
5334
+ `));
5335
+ if (result.loadedFiles.length > 0) {
5336
+ console.log(chalk28.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
5337
+ }
5338
+ console.log("");
5339
+ } else {
5340
+ console.log(chalk28.blue.bold(`
4646
5341
  \u{1F50D} Validating environment variables
4647
5342
  `));
5343
+ }
4648
5344
  const allErrors = [];
4649
5345
  const allWarnings = [];
4650
5346
  for (const packageName of packages) {
4651
5347
  try {
4652
- console.log(chalk26.dim(` \u{1F4E6} ${packageName}`));
5348
+ console.log(chalk28.dim(` \u{1F4E6} ${packageName}`));
4653
5349
  const envSchema = await loadEnvSchema(packageName);
4654
5350
  const { createEnvRegistry } = await import("@spfn/core/env");
4655
5351
  const registry = createEnvRegistry(envSchema);
@@ -4662,10 +5358,10 @@ async function validateEnvVars(options) {
4662
5358
  }
4663
5359
  } catch (error) {
4664
5360
  if (error instanceof Error && error.message.includes("does not export envSchema")) {
4665
- console.log(chalk26.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
5361
+ console.log(chalk28.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
4666
5362
  continue;
4667
5363
  }
4668
- console.error(chalk26.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
5364
+ console.error(chalk28.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
4669
5365
  if (options.strict) {
4670
5366
  process.exit(1);
4671
5367
  }
@@ -4673,42 +5369,43 @@ async function validateEnvVars(options) {
4673
5369
  }
4674
5370
  console.log("");
4675
5371
  if (allErrors.length > 0) {
4676
- console.log(chalk26.red.bold(`\u274C Validation Errors (${allErrors.length}):
5372
+ console.log(chalk28.red.bold(`\u274C Validation Errors (${allErrors.length}):
4677
5373
  `));
4678
5374
  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}`)}`);
5375
+ console.log(` ${chalk28.red("\u2717")} ${chalk28.cyan(error.key)}`);
5376
+ console.log(` ${chalk28.dim(error.message)}`);
5377
+ console.log(` ${chalk28.dim(`from ${error.package}`)}`);
4682
5378
  console.log("");
4683
5379
  }
4684
5380
  }
4685
5381
  if (allWarnings.length > 0) {
4686
- console.log(chalk26.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
5382
+ console.log(chalk28.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
4687
5383
  `));
4688
5384
  for (const warning of allWarnings) {
4689
- console.log(` ${chalk26.yellow("\u26A0")} ${chalk26.cyan(warning.key)}`);
4690
- console.log(` ${chalk26.dim(warning.message)}`);
5385
+ console.log(` ${chalk28.yellow("\u26A0")} ${chalk28.cyan(warning.key)}`);
5386
+ console.log(` ${chalk28.dim(warning.message)}`);
4691
5387
  console.log("");
4692
5388
  }
4693
5389
  }
4694
5390
  if (allErrors.length === 0 && allWarnings.length === 0) {
4695
- console.log(chalk26.green.bold("\u2705 All environment variables are valid!\n"));
5391
+ console.log(chalk28.green.bold("\u2705 All environment variables are valid!\n"));
4696
5392
  } 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.
5393
+ console.log(chalk28.green("\u2705 No errors found."));
5394
+ console.log(chalk28.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
4699
5395
  `));
4700
5396
  } else {
4701
- console.log(chalk26.red(`
5397
+ console.log(chalk28.red(`
4702
5398
  \u274C Validation failed with ${allErrors.length} error(s)
4703
5399
  `));
4704
5400
  process.exit(1);
4705
5401
  }
4706
5402
  }
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);
5403
+ 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
5404
 
4709
5405
  // src/index.ts
5406
+ init_version();
4710
5407
  var program = new Command13();
4711
- program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version("0.1.0");
5408
+ program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(getCliVersion());
4712
5409
  program.addCommand(createCommand);
4713
5410
  program.addCommand(initCommand);
4714
5411
  program.addCommand(addCommand);