spfn 0.2.0-beta.47 → 0.2.0-beta.49

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
@@ -535,8 +535,8 @@ async function setupApiProxy(cwd, includeAuth) {
535
535
  const rpcDir = join6(appDir, "api", "rpc", "[routeName]");
536
536
  const rpcRoutePath = join6(rpcDir, "route.ts");
537
537
  if (existsSync6(rpcRoutePath)) {
538
- logger.error(`RPC proxy route already exists: ${rpcRoutePath.replace(cwd + "/", "")}`);
539
- process.exit(1);
538
+ logger.warn(`RPC proxy route already exists, skipping: ${rpcRoutePath.replace(cwd + "/", "")}`);
539
+ return;
540
540
  }
541
541
  ensureDirSync3(rpcDir);
542
542
  const authImport = includeAuth ? `import '@spfn/auth/nextjs/api';
@@ -573,10 +573,17 @@ var init_api_proxy = __esm({
573
573
  });
574
574
 
575
575
  // src/commands/init/steps/docker.ts
576
- import { existsSync as existsSync7 } from "fs";
576
+ import { existsSync as existsSync7, readFileSync as readFileSync2 } from "fs";
577
577
  import { join as join7 } from "path";
578
578
  import fse4 from "fs-extra";
579
- async function setupDockerFiles(cwd) {
579
+ function adaptDockerfileForPm(content, pm) {
580
+ const adaptations = PM_DOCKERFILE_ADAPTATIONS[pm];
581
+ if (!adaptations) {
582
+ return content;
583
+ }
584
+ return adaptations.reduce((acc, [from, to]) => acc.replace(from, to), content);
585
+ }
586
+ async function setupDockerFiles(cwd, pm = "pnpm") {
580
587
  const templatesDir = findTemplatesPath();
581
588
  const dockerComposePath = join7(cwd, "docker-compose.yml");
582
589
  if (!existsSync7(dockerComposePath)) {
@@ -595,8 +602,9 @@ async function setupDockerFiles(cwd) {
595
602
  if (!existsSync7(dockerfilePath)) {
596
603
  const dockerfileTemplate = join7(templatesDir, "Dockerfile");
597
604
  if (existsSync7(dockerfileTemplate)) {
598
- copySync2(dockerfileTemplate, dockerfilePath);
599
- logger.success("Created Dockerfile");
605
+ const content = adaptDockerfileForPm(readFileSync2(dockerfileTemplate, "utf-8"), pm);
606
+ writeFileSync3(dockerfilePath, content);
607
+ logger.success(`Created Dockerfile (${pm})`);
600
608
  }
601
609
  }
602
610
  const dockerignorePath = join7(cwd, ".dockerignore");
@@ -619,13 +627,39 @@ async function setupDockerFiles(cwd) {
619
627
  logger.warn("Could not copy Docker files (you can create them manually)");
620
628
  }
621
629
  }
622
- var copySync2;
630
+ var copySync2, writeFileSync3, PM_DOCKERFILE_ADAPTATIONS;
623
631
  var init_docker = __esm({
624
632
  "src/commands/init/steps/docker.ts"() {
625
633
  "use strict";
626
634
  init_logger();
627
635
  init_templates();
628
- ({ copySync: copySync2 } = fse4);
636
+ ({ copySync: copySync2, writeFileSync: writeFileSync3 } = fse4);
637
+ PM_DOCKERFILE_ADAPTATIONS = {
638
+ npm: [
639
+ ["# Install pnpm\nRUN corepack enable pnpm\n\n", ""],
640
+ ["pnpm-lock.yaml*", "package-lock.json*"],
641
+ ["RUN pnpm install --frozen-lockfile --prod=false", "RUN npm ci --include=dev"],
642
+ ["RUN pnpm run spfn:build", "RUN npm run spfn:build"],
643
+ ["RUN pnpm prune --prod", "RUN npm prune --omit=dev"],
644
+ ['CMD ["pnpm", "run", "spfn:start"]', 'CMD ["npm", "run", "spfn:start"]']
645
+ ],
646
+ yarn: [
647
+ ["RUN corepack enable pnpm", "RUN corepack enable yarn"],
648
+ ["pnpm-lock.yaml*", "yarn.lock*"],
649
+ ["RUN pnpm install --frozen-lockfile --prod=false", "RUN yarn install --frozen-lockfile"],
650
+ ["RUN pnpm run spfn:build", "RUN yarn run spfn:build"],
651
+ ["# Remove dev dependencies (optional, reduces image size)\nRUN pnpm prune --prod\n\n", ""],
652
+ ['CMD ["pnpm", "run", "spfn:start"]', 'CMD ["yarn", "run", "spfn:start"]']
653
+ ],
654
+ bun: [
655
+ ["# Install pnpm\nRUN corepack enable pnpm", "# Install bun\nRUN npm install -g bun"],
656
+ ["pnpm-lock.yaml*", "bun.lockb*"],
657
+ ["RUN pnpm install --frozen-lockfile --prod=false", "RUN bun install --frozen-lockfile"],
658
+ ["RUN pnpm run spfn:build", "RUN bun run spfn:build"],
659
+ ["# Remove dev dependencies (optional, reduces image size)\nRUN pnpm prune --prod\n\n", ""],
660
+ ['CMD ["pnpm", "run", "spfn:start"]', 'CMD ["bun", "run", "spfn:start"]']
661
+ ]
662
+ };
629
663
  }
630
664
  });
631
665
 
@@ -737,25 +771,25 @@ export default {
737
771
  }
738
772
  }
739
773
  `;
740
- writeFileSync3(deploymentConfigPath, configContent);
774
+ writeFileSync4(deploymentConfigPath, configContent);
741
775
  logger.success(`Created spfn.config.js (subdomain: ${projectName}.spfn.app)`);
742
776
  } catch (error) {
743
777
  logger.warn("Could not create spfn.config.js");
744
778
  }
745
779
  }
746
780
  }
747
- var writeFileSync3;
781
+ var writeFileSync4;
748
782
  var init_deployment_config = __esm({
749
783
  "src/commands/init/steps/deployment-config.ts"() {
750
784
  "use strict";
751
785
  init_logger();
752
- ({ writeFileSync: writeFileSync3 } = fse5);
786
+ ({ writeFileSync: writeFileSync4 } = fse5);
753
787
  }
754
788
  });
755
789
 
756
790
  // src/utils/version.ts
757
791
  function getCliVersion() {
758
- return "0.2.0-beta.47";
792
+ return "0.2.0-beta.49";
759
793
  }
760
794
  function getTagFromVersion(version) {
761
795
  const match = version.match(/-([a-z]+)\./i);
@@ -784,6 +818,8 @@ async function setupPackageJson(cwd, packageJsonPath, packageJson, packageManage
784
818
  packageJson.dependencies["@sinclair/typebox"] = "^0.34.0";
785
819
  packageJson.dependencies["drizzle-orm"] = "^0.45.0";
786
820
  packageJson.dependencies["drizzle-typebox"] = "^0.1.0";
821
+ packageJson.dependencies["postgres"] = "^3.4.0";
822
+ packageJson.dependencies["pg"] = "^8.16.3";
787
823
  packageJson.dependencies["spfn"] = spfnTag;
788
824
  packageJson.dependencies["concurrently"] = "^9.2.1";
789
825
  if (includeAuth) {
@@ -806,7 +842,7 @@ async function setupPackageJson(cwd, packageJsonPath, packageJson, packageManage
806
842
  packageJson.scripts["spfn:start"] = "spfn start";
807
843
  packageJson.scripts["spfn:build"] = "spfn build";
808
844
  packageJson.scripts["codegen"] = "spfn codegen run";
809
- writeFileSync4(packageJsonPath, JSON.stringify(packageJson, null, 2));
845
+ writeFileSync5(packageJsonPath, JSON.stringify(packageJson, null, 2));
810
846
  spinner.succeed("package.json updated");
811
847
  spinner.start("Installing dependencies...");
812
848
  try {
@@ -819,18 +855,18 @@ async function setupPackageJson(cwd, packageJsonPath, packageJson, packageManage
819
855
  process.exit(1);
820
856
  }
821
857
  }
822
- var writeFileSync4;
858
+ var writeFileSync5;
823
859
  var init_package = __esm({
824
860
  "src/commands/init/steps/package.ts"() {
825
861
  "use strict";
826
862
  init_logger();
827
863
  init_version();
828
- ({ writeFileSync: writeFileSync4 } = fse6);
864
+ ({ writeFileSync: writeFileSync5 } = fse6);
829
865
  }
830
866
  });
831
867
 
832
868
  // src/commands/init/steps/config-files.ts
833
- import { existsSync as existsSync9, readFileSync as readFileSync2 } from "fs";
869
+ import { existsSync as existsSync9, readFileSync as readFileSync3 } from "fs";
834
870
  import { join as join9 } from "path";
835
871
  import fse7 from "fs-extra";
836
872
  async function setupConfigFiles(cwd) {
@@ -857,7 +893,7 @@ export default defineConfig({
857
893
  ]
858
894
  });
859
895
  `;
860
- writeFileSync5(spfnrcPath, spfnrcContent);
896
+ writeFileSync6(spfnrcPath, spfnrcContent);
861
897
  logger.success("Created .spfnrc.ts (codegen configuration)");
862
898
  }
863
899
  updateGitignore(cwd);
@@ -892,7 +928,8 @@ CACHE_URL=redis://localhost:6379
892
928
  # SPFN App URL (optional, for CORS and redirects)
893
929
  # SPFN_APP_URL=http://localhost:3790
894
930
  `);
895
- writeEnvExample(cwd, ".env.server.example", `# Server-only defaults (committed)
931
+ writeEnvExample(cwd, ".env.server.example", `# Server-only environment (template)
932
+ # Copy to .env.server (gitignored) and fill in real values.
896
933
  # These values are only loaded by the SPFN server, not by Next.js.
897
934
 
898
935
  # Database pool
@@ -902,9 +939,8 @@ DB_POOL_IDLE_TIMEOUT=30
902
939
  # Server timeouts
903
940
  SERVER_TIMEOUT=120000
904
941
  SHUTDOWN_TIMEOUT=30000
905
- `);
906
- writeEnvExample(cwd, ".env.server.local.example", `# Server secrets (gitignored)
907
- # Server-only sensitive values. Never commit this file.
942
+
943
+ # --- Secrets (never commit .env.server) ---
908
944
 
909
945
  # Database write/read URLs (master-replica pattern, optional)
910
946
  # DATABASE_WRITE_URL=postgresql://user:password@master:5432/dbname
@@ -919,7 +955,7 @@ function writeEnvExample(cwd, filename, content) {
919
955
  if (existsSync9(filePath)) {
920
956
  return;
921
957
  }
922
- writeFileSync5(filePath, content);
958
+ writeFileSync6(filePath, content);
923
959
  logger.success(`Created ${filename}`);
924
960
  }
925
961
  function updateGitignore(cwd) {
@@ -928,7 +964,7 @@ function updateGitignore(cwd) {
928
964
  return;
929
965
  }
930
966
  try {
931
- const content = readFileSync2(gitignorePath, "utf-8");
967
+ const content = readFileSync3(gitignorePath, "utf-8");
932
968
  let updated = content;
933
969
  let changed = false;
934
970
  if (!content.includes(".spfn")) {
@@ -943,11 +979,18 @@ function updateGitignore(cwd) {
943
979
  # environment secrets (local overrides)
944
980
  .env.local
945
981
  .env.*.local
982
+ `;
983
+ changed = true;
984
+ }
985
+ if (!content.includes(".env.server")) {
986
+ updated += `
987
+ # spfn server env (secrets)
988
+ .env.server
946
989
  `;
947
990
  changed = true;
948
991
  }
949
992
  if (changed) {
950
- writeFileSync5(gitignorePath, updated);
993
+ writeFileSync6(gitignorePath, updated);
951
994
  logger.success("Updated .gitignore with .spfn directory and env patterns");
952
995
  }
953
996
  } catch (error) {
@@ -960,26 +1003,26 @@ function updateTsconfig(cwd) {
960
1003
  return;
961
1004
  }
962
1005
  try {
963
- const tsconfigContent = readFileSync2(tsconfigPath, "utf-8");
1006
+ const tsconfigContent = readFileSync3(tsconfigPath, "utf-8");
964
1007
  const tsconfig = JSON.parse(tsconfigContent);
965
1008
  if (!tsconfig.exclude) {
966
1009
  tsconfig.exclude = [];
967
1010
  }
968
1011
  if (!tsconfig.exclude.includes("src/server")) {
969
1012
  tsconfig.exclude.push("src/server");
970
- writeFileSync5(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
1013
+ writeFileSync6(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
971
1014
  logger.success("Updated tsconfig.json (excluded src/server for Vercel compatibility)");
972
1015
  }
973
1016
  } catch (error) {
974
1017
  logger.warn('Could not update tsconfig.json (you can add "src/server" to exclude manually)');
975
1018
  }
976
1019
  }
977
- var writeFileSync5;
1020
+ var writeFileSync6;
978
1021
  var init_config_files = __esm({
979
1022
  "src/commands/init/steps/config-files.ts"() {
980
1023
  "use strict";
981
1024
  init_logger();
982
- ({ writeFileSync: writeFileSync5 } = fse7);
1025
+ ({ writeFileSync: writeFileSync6 } = fse7);
983
1026
  }
984
1027
  });
985
1028
 
@@ -998,7 +1041,7 @@ async function initializeSpfn(options = {}) {
998
1041
  logger.step(`Detected package manager: ${pm}`);
999
1042
  await setupServerStructure(cwd);
1000
1043
  await setupApiProxy(cwd, includeAuth);
1001
- await setupDockerFiles(cwd);
1044
+ await setupDockerFiles(cwd, pm);
1002
1045
  await setupDeploymentConfig(cwd, packageJson, pm);
1003
1046
  await setupPackageJson(cwd, packageJsonPath, packageJson, pm, includeAuth);
1004
1047
  await setupConfigFiles(cwd);
@@ -1046,7 +1089,7 @@ import chalk12 from "chalk";
1046
1089
  import { join as join15 } from "path";
1047
1090
  import { env as env2 } from "@spfn/core/config";
1048
1091
  import { loadEnv as loadEnv2 } from "@spfn/core/server";
1049
- import { existsSync as existsSync16, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
1092
+ import { existsSync as existsSync16, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
1050
1093
  function discoverFunctionMigrations(cwd = process.cwd()) {
1051
1094
  const nodeModulesPath = join15(cwd, "node_modules");
1052
1095
  if (!existsSync16(nodeModulesPath)) {
@@ -1065,7 +1108,7 @@ function discoverFunctionMigrations(cwd = process.cwd()) {
1065
1108
  continue;
1066
1109
  }
1067
1110
  try {
1068
- const packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
1111
+ const packageJson = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
1069
1112
  const spfnConfig = packageJson.spfn;
1070
1113
  if (!spfnConfig?.migrations) {
1071
1114
  continue;
@@ -1111,7 +1154,7 @@ async function migrateLegacyTable(db, functionMigrations) {
1111
1154
  if (!existsSync16(journalPath)) {
1112
1155
  continue;
1113
1156
  }
1114
- const journal = JSON.parse(readFileSync5(journalPath, "utf-8"));
1157
+ const journal = JSON.parse(readFileSync6(journalPath, "utf-8"));
1115
1158
  const entries = journal.entries || [];
1116
1159
  const tableName = `__spfn_fn_${func.packageName.replace("@spfn/", "")}_migrations`;
1117
1160
  await db.execute(
@@ -1137,7 +1180,7 @@ async function migrateLegacyTable(db, functionMigrations) {
1137
1180
  if (!existsSync16(sqlPath)) {
1138
1181
  continue;
1139
1182
  }
1140
- const sqlContent = readFileSync5(sqlPath, "utf-8");
1183
+ const sqlContent = readFileSync6(sqlPath, "utf-8");
1141
1184
  const hash = createHash("sha256").update(sqlContent).digest("hex");
1142
1185
  const found = legacyRows.find((r) => r.hash === hash);
1143
1186
  if (found) {
@@ -1190,7 +1233,7 @@ var init_function_migrations = __esm({
1190
1233
  });
1191
1234
 
1192
1235
  // src/index.ts
1193
- import { Command as Command13 } from "commander";
1236
+ import { Command as Command12 } from "commander";
1194
1237
 
1195
1238
  // src/commands/create.ts
1196
1239
  init_logger();
@@ -1366,7 +1409,6 @@ async function createProject(projectName, options) {
1366
1409
  console.log(" " + chalk4.cyan("Or deploy with Docker:"));
1367
1410
  console.log(` ${chalk4.cyan("docker compose -f docker-compose.production.yml up --build -d")}
1368
1411
  `);
1369
- console.log(chalk4.dim(" \u{1F4D6} See .guide/deployment.md for complete deployment guide"));
1370
1412
  console.log(chalk4.dim(" \u{1F310} Documentation: https://github.com/spfn/spfn\n"));
1371
1413
  }
1372
1414
  var createCommand = new Command3("create").description("Create a new Next.js project with SPFN").argument("<project-name>", "Name of the project directory").option("--skip-install", "Skip installing dependencies").option("--skip-git", "Skip initializing a git repository").option("--pm <manager>", "Package manager to use (npm, pnpm, yarn, bun)").option("--shadcn", "Setup shadcn/ui (component library)").option("-y, --yes", "Skip prompts and use defaults").action(async (projectName, options) => {
@@ -1380,7 +1422,7 @@ init_init();
1380
1422
  init_logger();
1381
1423
  init_package_manager();
1382
1424
  import { Command as Command4 } from "commander";
1383
- import { existsSync as existsSync11, readFileSync as readFileSync3, writeFileSync as writeFileSync6, mkdirSync, unlinkSync, rmSync, watch } from "fs";
1425
+ import { existsSync as existsSync11, readFileSync as readFileSync4, writeFileSync as writeFileSync7, mkdirSync, unlinkSync, rmSync, watch } from "fs";
1384
1426
  import { join as join11 } from "path";
1385
1427
  import { execa as execa4 } from "execa";
1386
1428
  import chokidar from "chokidar";
@@ -1395,11 +1437,11 @@ function waitForReadyFile(filePath, timeoutMs = 3e4) {
1395
1437
  }, timeoutMs);
1396
1438
  const dir = join11(filePath, "..");
1397
1439
  const fileName = filePath.split("/").pop();
1398
- const watcher = watch(dir, (event, name) => {
1440
+ const watcher = watch(dir, (_event, name) => {
1399
1441
  if (name === fileName && existsSync11(filePath)) {
1400
1442
  watcher.close();
1401
1443
  clearTimeout(timer);
1402
- resolve3(readFileSync3(filePath, "utf-8").trim());
1444
+ resolve3(readFileSync4(filePath, "utf-8").trim());
1403
1445
  }
1404
1446
  });
1405
1447
  });
@@ -1419,7 +1461,7 @@ var devCommand = new Command4("dev").description("Start SPFN development server
1419
1461
  const packageJsonPath = join11(cwd, "package.json");
1420
1462
  let hasNext = false;
1421
1463
  if (existsSync11(packageJsonPath)) {
1422
- const packageJson = JSON.parse(readFileSync3(packageJsonPath, "utf-8"));
1464
+ const packageJson = JSON.parse(readFileSync4(packageJsonPath, "utf-8"));
1423
1465
  hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
1424
1466
  }
1425
1467
  const tempDir = join11(cwd, ".spfn");
@@ -1441,7 +1483,7 @@ var devCommand = new Command4("dev").description("Start SPFN development server
1441
1483
  if (options.routes) configParts.push(`routesPath: '${options.routes}'`);
1442
1484
  configParts.push("debug: true");
1443
1485
  const readyFile = join11(tempDir, "server-ready");
1444
- writeFileSync6(serverEntry, `
1486
+ writeFileSync7(serverEntry, `
1445
1487
  import { writeFileSync } from 'fs';
1446
1488
 
1447
1489
  // Load environment variables FIRST (before any imports that depend on them)
@@ -1457,7 +1499,7 @@ const instance = await startServer({
1457
1499
  // Signal ready with actual port
1458
1500
  writeFileSync(${JSON.stringify(readyFile)}, String(instance.config.port));
1459
1501
  `);
1460
- writeFileSync6(watcherEntry, `
1502
+ writeFileSync7(watcherEntry, `
1461
1503
  // Load environment variables
1462
1504
  // const { loadEnvFiles } = await import('@spfn/core/server');
1463
1505
  // loadEnvFiles();
@@ -1730,7 +1772,7 @@ catch (error)
1730
1772
  init_logger();
1731
1773
  init_package_manager();
1732
1774
  import { Command as Command5 } from "commander";
1733
- import { existsSync as existsSync12, writeFileSync as writeFileSync7, mkdirSync as mkdirSync2, readdirSync } from "fs";
1775
+ import { existsSync as existsSync12, writeFileSync as writeFileSync8, mkdirSync as mkdirSync2, readdirSync } from "fs";
1734
1776
  import { join as join12 } from "path";
1735
1777
  import { execa as execa5 } from "execa";
1736
1778
  import ora5 from "ora";
@@ -1846,7 +1888,7 @@ await startServer({
1846
1888
  debug: false
1847
1889
  });
1848
1890
  `;
1849
- writeFileSync7(prodServerPath, prodServerContent);
1891
+ writeFileSync8(prodServerPath, prodServerContent);
1850
1892
  spinner.succeed(`SPFN server build completed \u2192 .spfn/server`);
1851
1893
  const routesDir = join12(cwd, ".spfn", "server", "routes");
1852
1894
  if (existsSync12(routesDir)) {
@@ -1905,7 +1947,7 @@ var buildCommand = new Command5("build").description("Build SPFN project for pro
1905
1947
  init_logger();
1906
1948
  init_package_manager();
1907
1949
  import { Command as Command6 } from "commander";
1908
- import { existsSync as existsSync13, readFileSync as readFileSync4 } from "fs";
1950
+ import { existsSync as existsSync13, readFileSync as readFileSync5 } from "fs";
1909
1951
  import { join as join13 } from "path";
1910
1952
  import { execa as execa6 } from "execa";
1911
1953
  import chalk6 from "chalk";
@@ -1917,7 +1959,7 @@ var startCommand = new Command6("start").description("Start SPFN production serv
1917
1959
  const packageJsonPath = join13(cwd, "package.json");
1918
1960
  let hasNext = false;
1919
1961
  if (existsSync13(packageJsonPath)) {
1920
- const packageJson = JSON.parse(readFileSync4(packageJsonPath, "utf-8"));
1962
+ const packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
1921
1963
  hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
1922
1964
  }
1923
1965
  const builtServerDir = join13(cwd, ".spfn", "server");
@@ -1969,7 +2011,7 @@ var startCommand = new Command6("start").description("Start SPFN production serv
1969
2011
  return;
1970
2012
  }
1971
2013
  const nextCmd = "next start -H 0.0.0.0 -p 3790";
1972
- const serverCmd = `node ${serverEntry}`;
2014
+ const serverCmd = `node "${serverEntry}"`;
1973
2015
  console.log(chalk6.blue.bold("\n\u{1F680} Starting SPFN production server...\n"));
1974
2016
  logger.info("Next.js: http://0.0.0.0:3790");
1975
2017
  logger.info(`SPFN API: http://${options.host}:${options.port}
@@ -1977,11 +2019,10 @@ var startCommand = new Command6("start").description("Start SPFN production serv
1977
2019
  try {
1978
2020
  await execa6(
1979
2021
  pm === "npm" ? "npx" : pm,
1980
- pm === "npm" ? ["concurrently", "--raw", "--kill-others", `"${nextCmd}"`, `"${serverCmd}"`] : ["exec", "concurrently", "--raw", "--kill-others", `"${nextCmd}"`, `"${serverCmd}"`],
2022
+ pm === "npm" ? ["concurrently", "--raw", "--kill-others", nextCmd, serverCmd] : ["exec", "concurrently", "--raw", "--kill-others", nextCmd, serverCmd],
1981
2023
  {
1982
2024
  stdio: "inherit",
1983
2025
  cwd,
1984
- shell: true,
1985
2026
  env: { ...process.env }
1986
2027
  }
1987
2028
  );
@@ -1998,7 +2039,7 @@ var startCommand = new Command6("start").description("Start SPFN production serv
1998
2039
  // src/commands/codegen.ts
1999
2040
  init_logger();
2000
2041
  import { Command as Command7 } from "commander";
2001
- import { existsSync as existsSync14, writeFileSync as writeFileSync8 } from "fs";
2042
+ import { existsSync as existsSync14, writeFileSync as writeFileSync9 } from "fs";
2002
2043
  import { join as join14 } from "path";
2003
2044
  import chalk7 from "chalk";
2004
2045
  async function initCodegen(options) {
@@ -2009,20 +2050,29 @@ async function initCodegen(options) {
2009
2050
  logger.info("Edit manually to add custom generators");
2010
2051
  process.exit(0);
2011
2052
  }
2012
- const config = {
2013
- codegen: {
2014
- generators: [
2015
- {
2016
- name: "@spfn/core:contract",
2017
- enabled: true
2018
- }
2019
- ]
2020
- }
2021
- };
2022
- writeFileSync8(rcPath, JSON.stringify(config, null, 2) + "\n");
2053
+ const content = `import { defineConfig, defineGenerator } from '@spfn/core/codegen';
2054
+
2055
+ /**
2056
+ * SPFN Codegen Configuration
2057
+ *
2058
+ * Configure code generators here. Generators run during \`spfn dev\` and \`spfn codegen run\`.
2059
+ */
2060
+
2061
+ export default defineConfig({
2062
+ generators: [
2063
+ // Route map generator - generates routeName \u2192 {method, path} mappings
2064
+ // Used by RPC proxy to resolve routes without importing server code
2065
+ defineGenerator({
2066
+ name: '@spfn/core:route-map',
2067
+ routerPath: './src/server/router.ts',
2068
+ outputPath: './src/generated/route-map.ts',
2069
+ }),
2070
+ ],
2071
+ });
2072
+ `;
2073
+ writeFileSync9(rcPath, content);
2023
2074
  console.log("\n" + chalk7.green.bold("\u2713 Created .spfnrc.ts\n"));
2024
- console.log("Configuration:");
2025
- console.log(chalk7.gray(JSON.stringify(config, null, 2)));
2075
+ console.log(chalk7.gray("Configured the @spfn/core:route-map generator."));
2026
2076
  if (options.withExample) {
2027
2077
  console.log("\n" + chalk7.yellow("\u{1F4DD} To add custom generators:"));
2028
2078
  console.log(" 1. Create your generator file (e.g., src/generators/my-generator.ts)");
@@ -2234,7 +2284,7 @@ import { Command as Command9 } from "commander";
2234
2284
  import chalk10 from "chalk";
2235
2285
 
2236
2286
  // src/commands/db/utils/drizzle.ts
2237
- import { existsSync as existsSync15, writeFileSync as writeFileSync9, unlinkSync as unlinkSync2 } from "fs";
2287
+ import { existsSync as existsSync15, writeFileSync as writeFileSync10, unlinkSync as unlinkSync2 } from "fs";
2238
2288
  import { spawn } from "child_process";
2239
2289
  import { pathToFileURL } from "url";
2240
2290
  import chalk9 from "chalk";
@@ -2269,7 +2319,7 @@ async function runDrizzleCommand(command) {
2269
2319
  expandGlobs: true,
2270
2320
  autoDetectSchemas: true
2271
2321
  });
2272
- writeFileSync9(tempConfigPath, configContent);
2322
+ writeFileSync10(tempConfigPath, configContent);
2273
2323
  console.log(chalk9.dim("Using auto-generated Drizzle config\n"));
2274
2324
  }
2275
2325
  const args = command.split(" ");
@@ -2590,7 +2640,7 @@ async function applyFunctionMigrations() {
2590
2640
  }
2591
2641
 
2592
2642
  // src/commands/db/migrate.ts
2593
- import chalk17 from "chalk";
2643
+ import chalk18 from "chalk";
2594
2644
  import { join as join16 } from "path";
2595
2645
  import { existsSync as existsSync18 } from "fs";
2596
2646
 
@@ -2598,11 +2648,13 @@ import { existsSync as existsSync18 } from "fs";
2598
2648
  import { promises as fs3 } from "fs";
2599
2649
  import path3 from "path";
2600
2650
  import { spawn as spawn2 } from "child_process";
2601
- import chalk16 from "chalk";
2651
+ import chalk17 from "chalk";
2602
2652
  import ora7 from "ora";
2603
2653
 
2604
2654
  // src/commands/db/utils/database.ts
2605
2655
  import net from "net";
2656
+ import chalk14 from "chalk";
2657
+ import prompts4 from "prompts";
2606
2658
  function parseDatabaseUrl(dbUrl) {
2607
2659
  try {
2608
2660
  const url = new URL(dbUrl);
@@ -2618,6 +2670,24 @@ function parseDatabaseUrl(dbUrl) {
2618
2670
  throw new Error(`Invalid DATABASE_URL format: ${error instanceof Error ? error.message : "Unknown error"}`);
2619
2671
  }
2620
2672
  }
2673
+ var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
2674
+ function isRemoteHost(host) {
2675
+ return !LOCAL_HOSTS.has(host);
2676
+ }
2677
+ async function confirmDangerousTarget(dbInfo) {
2678
+ if (!isRemoteHost(dbInfo.host) && process.env.NODE_ENV !== "production") {
2679
+ return;
2680
+ }
2681
+ const { typed } = await prompts4({
2682
+ type: "text",
2683
+ name: "typed",
2684
+ message: `Remote/production database detected. Type the database name "${dbInfo.database}" to continue:`
2685
+ });
2686
+ if (typed !== dbInfo.database) {
2687
+ console.log(chalk14.gray("Cancelled (database name mismatch)."));
2688
+ process.exit(0);
2689
+ }
2690
+ }
2621
2691
  async function isPortAvailable(port) {
2622
2692
  return new Promise((resolve3) => {
2623
2693
  const server = net.createServer();
@@ -2667,14 +2737,14 @@ function formatTimestamp() {
2667
2737
  import { promises as fs2 } from "fs";
2668
2738
  import { existsSync as existsSync17 } from "fs";
2669
2739
  import path2 from "path";
2670
- import chalk15 from "chalk";
2740
+ import chalk16 from "chalk";
2671
2741
 
2672
2742
  // src/commands/db/utils/metadata.ts
2673
2743
  import { promises as fs } from "fs";
2674
2744
  import path from "path";
2675
2745
  import { promisify } from "util";
2676
2746
  import { exec } from "child_process";
2677
- import chalk14 from "chalk";
2747
+ import chalk15 from "chalk";
2678
2748
  var execAsync = promisify(exec);
2679
2749
  async function collectGitInfo() {
2680
2750
  try {
@@ -2737,7 +2807,7 @@ async function collectMigrationInfo(dbUrl) {
2737
2807
  await pool.end();
2738
2808
  }
2739
2809
  } catch (error) {
2740
- console.log(chalk14.dim("\u26A0\uFE0F Could not fetch migration info"));
2810
+ console.log(chalk15.dim("\u26A0\uFE0F Could not fetch migration info"));
2741
2811
  return void 0;
2742
2812
  }
2743
2813
  }
@@ -2745,9 +2815,9 @@ async function saveBackupMetadata(metadata, backupFilename) {
2745
2815
  const metadataPath = backupFilename.replace(/\.(sql|dump)$/, ".meta.json");
2746
2816
  try {
2747
2817
  await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
2748
- console.log(chalk14.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
2818
+ console.log(chalk15.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
2749
2819
  } catch (error) {
2750
- console.log(chalk14.dim("\u26A0\uFE0F Could not save metadata"));
2820
+ console.log(chalk15.dim("\u26A0\uFE0F Could not save metadata"));
2751
2821
  }
2752
2822
  }
2753
2823
  async function loadBackupMetadata(backupFilename) {
@@ -2776,10 +2846,10 @@ async function ensureBackupInGitignore() {
2776
2846
  if (!hasBackupsIgnore) {
2777
2847
  const entry = exists && content && !content.endsWith("\n") ? "\n\n# Database backups\nbackups/\n" : "# Database backups\nbackups/\n";
2778
2848
  await fs2.appendFile(gitignorePath, entry);
2779
- console.log(chalk15.dim("\u2713 Added backups/ to .gitignore"));
2849
+ console.log(chalk16.dim("\u2713 Added backups/ to .gitignore"));
2780
2850
  }
2781
2851
  } catch (error) {
2782
- console.log(chalk15.dim("\u26A0\uFE0F Could not update .gitignore"));
2852
+ console.log(chalk16.dim("\u26A0\uFE0F Could not update .gitignore"));
2783
2853
  }
2784
2854
  }
2785
2855
  async function ensureBackupDir() {
@@ -2829,12 +2899,12 @@ async function listBackupFiles() {
2829
2899
  import { env as env3 } from "@spfn/core/config";
2830
2900
  import { loadEnv as loadEnv4 } from "@spfn/core/server";
2831
2901
  async function dbBackup(options) {
2832
- console.log(chalk16.blue("\u{1F4BE} Creating database backup...\n"));
2902
+ console.log(chalk17.blue("\u{1F4BE} Creating database backup...\n"));
2833
2903
  loadEnv4();
2834
2904
  const dbUrl = env3.DATABASE_URL;
2835
2905
  if (!dbUrl) {
2836
- console.error(chalk16.red("\u274C DATABASE_URL not found in environment"));
2837
- console.log(chalk16.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2906
+ console.error(chalk17.red("\u274C DATABASE_URL not found in environment"));
2907
+ console.log(chalk17.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2838
2908
  process.exit(1);
2839
2909
  }
2840
2910
  const dbInfo = parseDatabaseUrl(dbUrl);
@@ -2844,7 +2914,7 @@ async function dbBackup(options) {
2844
2914
  const ext = format === "sql" ? "sql" : "dump";
2845
2915
  const filename = options.output || path3.join(backupDir, `${dbInfo.database}_${timestamp}.${ext}`);
2846
2916
  if (options.dataOnly && options.schemaOnly) {
2847
- console.error(chalk16.red("\u274C Cannot use --data-only and --schema-only together"));
2917
+ console.error(chalk17.red("\u274C Cannot use --data-only and --schema-only together"));
2848
2918
  process.exit(1);
2849
2919
  }
2850
2920
  const args = [
@@ -2890,11 +2960,11 @@ async function dbBackup(options) {
2890
2960
  const stats = await fs3.stat(filename);
2891
2961
  const size = formatBytes(stats.size);
2892
2962
  spinner.succeed("Backup created");
2893
- console.log(chalk16.green(`
2963
+ console.log(chalk17.green(`
2894
2964
  \u2705 Backup created successfully`));
2895
- console.log(chalk16.gray(` File: ${filename}`));
2896
- console.log(chalk16.gray(` Size: ${size}`));
2897
- console.log(chalk16.dim("\n\u{1F4CB} Collecting metadata..."));
2965
+ console.log(chalk17.gray(` File: ${filename}`));
2966
+ console.log(chalk17.gray(` Size: ${size}`));
2967
+ console.log(chalk17.dim("\n\u{1F4CB} Collecting metadata..."));
2898
2968
  const [gitInfo, migrationInfo] = await Promise.all([
2899
2969
  collectGitInfo(),
2900
2970
  collectMigrationInfo(dbUrl)
@@ -2934,11 +3004,11 @@ async function dbBackup(options) {
2934
3004
  reject(error);
2935
3005
  });
2936
3006
  }).catch((error) => {
2937
- console.error(chalk16.red("\n\u274C Failed to create backup"));
3007
+ console.error(chalk17.red("\n\u274C Failed to create backup"));
2938
3008
  if (errorOutput.includes("pg_dump: command not found") || errorOutput.includes("not found")) {
2939
- console.error(chalk16.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
3009
+ console.error(chalk17.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
2940
3010
  } else {
2941
- console.error(chalk16.red(error instanceof Error ? error.message : "Unknown error"));
3011
+ console.error(chalk17.red(error instanceof Error ? error.message : "Unknown error"));
2942
3012
  }
2943
3013
  process.exit(1);
2944
3014
  });
@@ -2955,7 +3025,7 @@ async function dbMigrate(options = {}) {
2955
3025
  process.exit(1);
2956
3026
  }
2957
3027
  if (options.withBackup) {
2958
- console.log(chalk17.blue("\u{1F4E6} Creating pre-migration backup...\n"));
3028
+ console.log(chalk18.blue("\u{1F4E6} Creating pre-migration backup...\n"));
2959
3029
  await dbBackup({
2960
3030
  format: "custom",
2961
3031
  tag: "pre-migration",
@@ -2968,57 +3038,56 @@ async function dbMigrate(options = {}) {
2968
3038
  const postgres = await import("postgres");
2969
3039
  loadEnv5();
2970
3040
  if (!env4.DATABASE_URL) {
2971
- console.error(chalk17.red("\u274C DATABASE_URL not found in environment"));
3041
+ console.error(chalk18.red("\u274C DATABASE_URL not found in environment"));
2972
3042
  process.exit(1);
2973
3043
  }
2974
3044
  const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
2975
3045
  const functions = discoverFunctionMigrations2(process.cwd());
2976
3046
  if (functions.length > 0) {
2977
- console.log(chalk17.blue("\u{1F4E6} Applying function package migrations:"));
3047
+ console.log(chalk18.blue("\u{1F4E6} Applying function package migrations:"));
2978
3048
  functions.forEach((func) => {
2979
- console.log(chalk17.dim(` - ${func.packageName}`));
3049
+ console.log(chalk18.dim(` - ${func.packageName}`));
2980
3050
  });
2981
3051
  await executeFunctionMigrations2(functions);
2982
- console.log(chalk17.green("\u2705 Function migrations applied\n"));
3052
+ console.log(chalk18.green("\u2705 Function migrations applied\n"));
2983
3053
  }
2984
3054
  const projectMigrationsDir = join16(process.cwd(), "src/server/drizzle");
2985
3055
  if (existsSync18(projectMigrationsDir)) {
2986
3056
  const projConn = postgres.default(env4.DATABASE_URL, { max: 1 });
2987
3057
  const projDb = drizzle(projConn);
2988
3058
  try {
2989
- console.log(chalk17.blue("\u{1F4E6} Running project migrations..."));
3059
+ console.log(chalk18.blue("\u{1F4E6} Running project migrations..."));
2990
3060
  await migrate(projDb, {
2991
3061
  migrationsFolder: projectMigrationsDir,
2992
3062
  migrationsTable: PROJECT_MIGRATIONS_TABLE
2993
3063
  });
2994
- console.log(chalk17.green("\u2705 Project migrations applied successfully"));
3064
+ console.log(chalk18.green("\u2705 Project migrations applied successfully"));
2995
3065
  } finally {
2996
3066
  await projConn.end();
2997
3067
  }
2998
3068
  } else {
2999
- console.log(chalk17.dim("No project migrations found (src/server/drizzle)"));
3069
+ console.log(chalk18.dim("No project migrations found (src/server/drizzle)"));
3000
3070
  }
3001
3071
  }
3002
3072
 
3003
3073
  // src/commands/db/studio.ts
3004
- import chalk18 from "chalk";
3005
- import { existsSync as existsSync19, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
3074
+ import chalk19 from "chalk";
3075
+ import { existsSync as existsSync19, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3 } from "fs";
3006
3076
  import { spawn as spawn3 } from "child_process";
3007
3077
  import { env as env5 } from "@spfn/core/config";
3008
- import "@spfn/core/config";
3009
3078
  async function dbStudio(requestedPort) {
3010
- console.log(chalk18.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
3079
+ console.log(chalk19.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
3011
3080
  const defaultPort = 4983;
3012
3081
  const startPort = requestedPort || defaultPort;
3013
3082
  let port;
3014
3083
  try {
3015
3084
  port = await findAvailablePort(startPort);
3016
3085
  if (port !== startPort) {
3017
- console.log(chalk18.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
3086
+ console.log(chalk19.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
3018
3087
  `));
3019
3088
  }
3020
3089
  } catch (error) {
3021
- console.error(chalk18.red(error instanceof Error ? error.message : "Failed to find available port"));
3090
+ console.error(chalk19.red(error instanceof Error ? error.message : "Failed to find available port"));
3022
3091
  process.exit(1);
3023
3092
  }
3024
3093
  const hasUserConfig = existsSync19("./drizzle.config.ts");
@@ -3027,8 +3096,8 @@ async function dbStudio(requestedPort) {
3027
3096
  const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
3028
3097
  if (!hasUserConfig) {
3029
3098
  if (!env5.DATABASE_URL) {
3030
- console.error(chalk18.red("\u274C DATABASE_URL not found in environment"));
3031
- console.log(chalk18.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
3099
+ console.error(chalk19.red("\u274C DATABASE_URL not found in environment"));
3100
+ console.log(chalk19.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
3032
3101
  process.exit(1);
3033
3102
  }
3034
3103
  const { generateDrizzleConfigFile } = await import("@spfn/core/db");
@@ -3038,8 +3107,8 @@ async function dbStudio(requestedPort) {
3038
3107
  expandGlobs: true
3039
3108
  // Expand glob patterns for Studio compatibility
3040
3109
  });
3041
- writeFileSync10(tempConfigPath, configContent);
3042
- console.log(chalk18.dim("Using auto-generated Drizzle config\n"));
3110
+ writeFileSync11(tempConfigPath, configContent);
3111
+ console.log(chalk19.dim("Using auto-generated Drizzle config\n"));
3043
3112
  }
3044
3113
  const studioProcess = spawn3("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
3045
3114
  stdio: "inherit",
@@ -3054,19 +3123,19 @@ async function dbStudio(requestedPort) {
3054
3123
  studioProcess.on("exit", (code) => {
3055
3124
  cleanup();
3056
3125
  if (code !== 0 && code !== null) {
3057
- console.error(chalk18.red(`
3126
+ console.error(chalk19.red(`
3058
3127
  \u274C Drizzle Studio exited with code ${code}`));
3059
3128
  process.exit(code);
3060
3129
  }
3061
3130
  });
3062
3131
  studioProcess.on("error", (error) => {
3063
3132
  cleanup();
3064
- console.error(chalk18.red("\u274C Failed to start Drizzle Studio"));
3065
- console.error(chalk18.red(error.message));
3133
+ console.error(chalk19.red("\u274C Failed to start Drizzle Studio"));
3134
+ console.error(chalk19.red(error.message));
3066
3135
  process.exit(1);
3067
3136
  });
3068
3137
  process.on("SIGINT", () => {
3069
- console.log(chalk18.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
3138
+ console.log(chalk19.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
3070
3139
  studioProcess.kill("SIGTERM");
3071
3140
  cleanup();
3072
3141
  process.exit(0);
@@ -3080,27 +3149,39 @@ async function dbStudio(requestedPort) {
3080
3149
  if (!hasUserConfig && existsSync19(tempConfigPath)) {
3081
3150
  unlinkSync3(tempConfigPath);
3082
3151
  }
3083
- console.error(chalk18.red("\u274C Failed to start Drizzle Studio"));
3084
- console.error(chalk18.red(error instanceof Error ? error.message : "Unknown error"));
3152
+ console.error(chalk19.red("\u274C Failed to start Drizzle Studio"));
3153
+ console.error(chalk19.red(error instanceof Error ? error.message : "Unknown error"));
3085
3154
  process.exit(1);
3086
3155
  }
3087
3156
  }
3088
3157
 
3089
3158
  // src/commands/db/drop.ts
3090
- import chalk19 from "chalk";
3091
- import prompts4 from "prompts";
3159
+ import chalk20 from "chalk";
3160
+ import prompts5 from "prompts";
3161
+ import { env as env6 } from "@spfn/core/config";
3162
+ import { loadEnv as loadEnv6 } from "@spfn/core/server";
3092
3163
  async function dbDrop() {
3093
- console.log(chalk19.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
3094
- const { confirm } = await prompts4({
3164
+ loadEnv6();
3165
+ if (!env6.DATABASE_URL) {
3166
+ console.error(chalk20.red("\u274C DATABASE_URL not found in environment"));
3167
+ console.log(chalk20.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
3168
+ process.exit(1);
3169
+ }
3170
+ const dbInfo = parseDatabaseUrl(env6.DATABASE_URL);
3171
+ console.log(chalk20.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
3172
+ console.log(chalk20.dim(` Target: ${dbInfo.database} @ ${dbInfo.host}:${dbInfo.port}
3173
+ `));
3174
+ const { confirm } = await prompts5({
3095
3175
  type: "confirm",
3096
3176
  name: "confirm",
3097
3177
  message: "Are you sure you want to drop all tables?",
3098
3178
  initial: false
3099
3179
  });
3100
3180
  if (!confirm) {
3101
- console.log(chalk19.gray("Cancelled."));
3181
+ console.log(chalk20.gray("Cancelled."));
3102
3182
  process.exit(0);
3103
3183
  }
3184
+ await confirmDangerousTarget(dbInfo);
3104
3185
  await runWithSpinner(
3105
3186
  "Dropping all tables...",
3106
3187
  "drop",
@@ -3110,7 +3191,7 @@ async function dbDrop() {
3110
3191
  }
3111
3192
 
3112
3193
  // src/commands/db/check.ts
3113
- import chalk20 from "chalk";
3194
+ import chalk21 from "chalk";
3114
3195
  import ora8 from "ora";
3115
3196
  async function dbCheck() {
3116
3197
  const spinner = ora8("Checking database connection...").start();
@@ -3119,7 +3200,7 @@ async function dbCheck() {
3119
3200
  spinner.succeed("Database connection OK");
3120
3201
  } catch (error) {
3121
3202
  spinner.fail("Database connection failed");
3122
- console.error(chalk20.red(error instanceof Error ? error.message : "Unknown error"));
3203
+ console.error(chalk21.red(error instanceof Error ? error.message : "Unknown error"));
3123
3204
  process.exit(1);
3124
3205
  }
3125
3206
  }
@@ -3127,28 +3208,28 @@ async function dbCheck() {
3127
3208
  // src/commands/db/restore.ts
3128
3209
  import path4 from "path";
3129
3210
  import { spawn as spawn4 } from "child_process";
3130
- import chalk21 from "chalk";
3211
+ import chalk22 from "chalk";
3131
3212
  import ora9 from "ora";
3132
- import prompts5 from "prompts";
3133
- import { env as env6 } from "@spfn/core/config";
3134
- import { loadEnv as loadEnv6 } from "@spfn/core/server";
3213
+ import prompts6 from "prompts";
3214
+ import { env as env7 } from "@spfn/core/config";
3215
+ import { loadEnv as loadEnv7 } from "@spfn/core/server";
3135
3216
  async function dbRestore(backupFile, options = {}) {
3136
- console.log(chalk21.blue("\u267B\uFE0F Restoring database from backup...\n"));
3137
- loadEnv6();
3138
- const dbUrl = env6.DATABASE_URL;
3217
+ console.log(chalk22.blue("\u267B\uFE0F Restoring database from backup...\n"));
3218
+ loadEnv7();
3219
+ const dbUrl = env7.DATABASE_URL;
3139
3220
  if (!dbUrl) {
3140
- console.error(chalk21.red("\u274C DATABASE_URL not found in environment"));
3141
- console.log(chalk21.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
3221
+ console.error(chalk22.red("\u274C DATABASE_URL not found in environment"));
3222
+ console.log(chalk22.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
3142
3223
  process.exit(1);
3143
3224
  }
3144
3225
  let file = backupFile;
3145
3226
  if (!file) {
3146
3227
  const backups = await listBackupFiles();
3147
3228
  if (backups.length === 0) {
3148
- console.log(chalk21.yellow("No backups found in ./backups directory"));
3229
+ console.log(chalk22.yellow("No backups found in ./backups directory"));
3149
3230
  process.exit(0);
3150
3231
  }
3151
- const { selected } = await prompts5({
3232
+ const { selected } = await prompts6({
3152
3233
  type: "select",
3153
3234
  name: "selected",
3154
3235
  message: "Select backup to restore:",
@@ -3158,31 +3239,31 @@ async function dbRestore(backupFile, options = {}) {
3158
3239
  }))
3159
3240
  });
3160
3241
  if (!selected) {
3161
- console.log(chalk21.gray("Cancelled"));
3242
+ console.log(chalk22.gray("Cancelled"));
3162
3243
  process.exit(0);
3163
3244
  }
3164
3245
  file = selected;
3165
3246
  }
3166
3247
  if (!file) {
3167
- console.error(chalk21.red("\u274C No backup file selected"));
3248
+ console.error(chalk22.red("\u274C No backup file selected"));
3168
3249
  process.exit(1);
3169
3250
  }
3170
3251
  const metadata = await loadBackupMetadata(file);
3171
3252
  if (metadata) {
3172
- console.log(chalk21.blue("\n\u{1F4CB} Backup Information:\n"));
3173
- console.log(chalk21.dim(` Database: ${metadata.database}`));
3174
- console.log(chalk21.dim(` Created: ${new Date(metadata.timestamp).toLocaleString()}`));
3253
+ console.log(chalk22.blue("\n\u{1F4CB} Backup Information:\n"));
3254
+ console.log(chalk22.dim(` Database: ${metadata.database}`));
3255
+ console.log(chalk22.dim(` Created: ${new Date(metadata.timestamp).toLocaleString()}`));
3175
3256
  if (metadata.environment) {
3176
- console.log(chalk21.dim(` Environment: ${metadata.environment}`));
3257
+ console.log(chalk22.dim(` Environment: ${metadata.environment}`));
3177
3258
  }
3178
3259
  if (metadata.tags && metadata.tags.length > 0) {
3179
- console.log(chalk21.dim(` Tags: ${metadata.tags.join(", ")}`));
3260
+ console.log(chalk22.dim(` Tags: ${metadata.tags.join(", ")}`));
3180
3261
  }
3181
3262
  if (metadata.backup.dataOnly) {
3182
- console.log(chalk21.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
3263
+ console.log(chalk22.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
3183
3264
  }
3184
3265
  if (metadata.backup.schemaOnly) {
3185
- console.log(chalk21.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
3266
+ console.log(chalk22.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
3186
3267
  }
3187
3268
  const warnings2 = [];
3188
3269
  const [currentGitInfo, currentMigrationInfo] = await Promise.all([
@@ -3205,26 +3286,27 @@ async function dbRestore(backupFile, options = {}) {
3205
3286
  }
3206
3287
  }
3207
3288
  if (warnings2.length > 0) {
3208
- console.log(chalk21.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
3209
- warnings2.forEach((warning) => console.log(chalk21.yellow(` - ${warning}`)));
3289
+ console.log(chalk22.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
3290
+ warnings2.forEach((warning) => console.log(chalk22.yellow(` - ${warning}`)));
3210
3291
  console.log("");
3211
3292
  }
3212
3293
  }
3213
- const { confirm } = await prompts5({
3294
+ const dbInfo = parseDatabaseUrl(dbUrl);
3295
+ const { confirm } = await prompts6({
3214
3296
  type: "confirm",
3215
3297
  name: "confirm",
3216
- message: chalk21.yellow("\u26A0\uFE0F This will replace all data in the database. Continue?"),
3298
+ message: chalk22.yellow(`\u26A0\uFE0F This will replace all data in "${dbInfo.database}" @ ${dbInfo.host}:${dbInfo.port}. Continue?`),
3217
3299
  initial: false
3218
3300
  });
3219
3301
  if (!confirm) {
3220
- console.log(chalk21.gray("Cancelled"));
3302
+ console.log(chalk22.gray("Cancelled"));
3221
3303
  process.exit(0);
3222
3304
  }
3305
+ await confirmDangerousTarget(dbInfo);
3223
3306
  if (options.dataOnly && options.schemaOnly) {
3224
- console.error(chalk21.red("\u274C Cannot use --data-only and --schema-only together"));
3307
+ console.error(chalk22.red("\u274C Cannot use --data-only and --schema-only together"));
3225
3308
  process.exit(1);
3226
3309
  }
3227
- const dbInfo = parseDatabaseUrl(dbUrl);
3228
3310
  const ext = path4.extname(file);
3229
3311
  const isCustomFormat = ext === ".dump";
3230
3312
  const command = isCustomFormat ? "pg_restore" : "psql";
@@ -3236,7 +3318,7 @@ async function dbRestore(backupFile, options = {}) {
3236
3318
  args.push("-d", dbInfo.database);
3237
3319
  args.push("--verbose");
3238
3320
  if (options.drop) {
3239
- args.push("--clean");
3321
+ args.push("--clean", "--if-exists");
3240
3322
  }
3241
3323
  if (options.schema) {
3242
3324
  args.push("-n", options.schema);
@@ -3249,9 +3331,12 @@ async function dbRestore(backupFile, options = {}) {
3249
3331
  }
3250
3332
  args.push(file);
3251
3333
  } else {
3334
+ if (options.drop) {
3335
+ console.log(chalk22.yellow("\u26A0\uFE0F Note: --drop only works with custom format backups (.dump) and was ignored."));
3336
+ }
3252
3337
  if (options.dataOnly || options.schemaOnly) {
3253
- console.log(chalk21.yellow("\u26A0\uFE0F Note: --data-only and --schema-only options only work with custom format backups (.dump)"));
3254
- console.log(chalk21.yellow(" For SQL files, the backup must have been created with the desired option.\n"));
3338
+ console.log(chalk22.yellow("\u26A0\uFE0F Note: --data-only and --schema-only options only work with custom format backups (.dump)"));
3339
+ console.log(chalk22.yellow(" For SQL files, the backup must have been created with the desired option.\n"));
3255
3340
  }
3256
3341
  args.push("-h", dbInfo.host);
3257
3342
  args.push("-p", dbInfo.port);
@@ -3272,7 +3357,6 @@ async function dbRestore(backupFile, options = {}) {
3272
3357
  const warnings = [];
3273
3358
  const errors = [];
3274
3359
  let objectCount = 0;
3275
- let lastObject = "";
3276
3360
  restoreProcess.stderr?.on("data", (data) => {
3277
3361
  const lines = data.toString().split("\n").filter((l) => l.trim());
3278
3362
  for (const line of lines) {
@@ -3286,18 +3370,16 @@ async function dbRestore(backupFile, options = {}) {
3286
3370
  objectCount = Number(objectMatch[2]);
3287
3371
  const current = Number(objectMatch[1]);
3288
3372
  const desc = line.replace(/^pg_restore:\s*/, "").trim();
3289
- lastObject = desc;
3290
3373
  spinner.text = `Restoring backup... [${current}/${objectCount}] ${desc}`;
3291
3374
  } else if (isCustomFormat) {
3292
3375
  const desc = line.replace(/^pg_restore:\s*/, "").trim();
3293
3376
  if (desc && !/warning:|error:/i.test(desc)) {
3294
- lastObject = desc;
3295
3377
  spinner.text = `Restoring backup... ${desc}`;
3296
3378
  }
3297
3379
  }
3298
3380
  if (verbose) {
3299
3381
  spinner.stop();
3300
- console.log(chalk21.dim(` ${line.trim()}`));
3382
+ console.log(chalk22.dim(` ${line.trim()}`));
3301
3383
  spinner.start();
3302
3384
  }
3303
3385
  }
@@ -3305,7 +3387,7 @@ async function dbRestore(backupFile, options = {}) {
3305
3387
  restoreProcess.stdout?.on("data", (data) => {
3306
3388
  if (verbose) {
3307
3389
  spinner.stop();
3308
- console.log(chalk21.dim(` ${data.toString().trim()}`));
3390
+ console.log(chalk22.dim(` ${data.toString().trim()}`));
3309
3391
  spinner.start();
3310
3392
  }
3311
3393
  });
@@ -3315,31 +3397,31 @@ async function dbRestore(backupFile, options = {}) {
3315
3397
  const summary = objectCount > 0 ? ` (${objectCount} objects)` : "";
3316
3398
  spinner.succeed(`Restore completed${summary}`);
3317
3399
  if (warnings.length > 0) {
3318
- console.log(chalk21.yellow(`
3400
+ console.log(chalk22.yellow(`
3319
3401
  \u26A0\uFE0F Warnings during restore (${warnings.length}):
3320
3402
  `));
3321
3403
  for (const w of warnings) {
3322
- console.log(chalk21.yellow(` - ${w}`));
3404
+ console.log(chalk22.yellow(` - ${w}`));
3323
3405
  }
3324
3406
  }
3325
- console.log(chalk21.green("\n\u2705 Database restored successfully"));
3407
+ console.log(chalk22.green("\n\u2705 Database restored successfully"));
3326
3408
  resolve3();
3327
3409
  } else {
3328
3410
  spinner.fail("Restore failed");
3329
3411
  if (errors.length > 0) {
3330
- console.error(chalk21.red(`
3412
+ console.error(chalk22.red(`
3331
3413
  \u274C Errors (${errors.length}):
3332
3414
  `));
3333
3415
  for (const e of errors) {
3334
- console.error(chalk21.red(` - ${e}`));
3416
+ console.error(chalk22.red(` - ${e}`));
3335
3417
  }
3336
3418
  }
3337
3419
  if (warnings.length > 0) {
3338
- console.log(chalk21.yellow(`
3420
+ console.log(chalk22.yellow(`
3339
3421
  \u26A0\uFE0F Warnings (${warnings.length}):
3340
3422
  `));
3341
3423
  for (const w of warnings) {
3342
- console.log(chalk21.yellow(` - ${w}`));
3424
+ console.log(chalk22.yellow(` - ${w}`));
3343
3425
  }
3344
3426
  }
3345
3427
  const fallback = errors.length === 0 && warnings.length === 0 ? "Restore failed with no output" : "";
@@ -3353,7 +3435,7 @@ async function dbRestore(backupFile, options = {}) {
3353
3435
  }).catch((error) => {
3354
3436
  const msg = error instanceof Error ? error.message : "Unknown error";
3355
3437
  if (msg) {
3356
- console.error(chalk21.red(`
3438
+ console.error(chalk22.red(`
3357
3439
  \u274C ${msg}`));
3358
3440
  }
3359
3441
  process.exit(1);
@@ -3361,17 +3443,17 @@ async function dbRestore(backupFile, options = {}) {
3361
3443
  }
3362
3444
 
3363
3445
  // src/commands/db/list.ts
3364
- import chalk22 from "chalk";
3446
+ import chalk23 from "chalk";
3365
3447
  async function dbBackupList() {
3366
- console.log(chalk22.blue("\u{1F4CB} Database backups:\n"));
3448
+ console.log(chalk23.blue("\u{1F4CB} Database backups:\n"));
3367
3449
  const backups = await listBackupFiles();
3368
3450
  if (backups.length === 0) {
3369
- console.log(chalk22.yellow("No backups found in ./backups directory"));
3370
- console.log(chalk22.gray("\n\u{1F4A1} Create a backup with: pnpm spfn db backup\n"));
3451
+ console.log(chalk23.yellow("No backups found in ./backups directory"));
3452
+ console.log(chalk23.gray("\n\u{1F4A1} Create a backup with: pnpm spfn db backup\n"));
3371
3453
  return;
3372
3454
  }
3373
- console.log(chalk22.bold(" Date Size File"));
3374
- 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"));
3455
+ console.log(chalk23.bold(" Date Size File"));
3456
+ console.log(chalk23.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"));
3375
3457
  backups.forEach((backup) => {
3376
3458
  const date = backup.date.toLocaleString("en-US", {
3377
3459
  year: "numeric",
@@ -3382,23 +3464,23 @@ async function dbBackupList() {
3382
3464
  second: "2-digit"
3383
3465
  });
3384
3466
  const sizeStr = backup.size.padEnd(10);
3385
- console.log(chalk22.white(` ${date} ${sizeStr} ${backup.name}`));
3467
+ console.log(chalk23.white(` ${date} ${sizeStr} ${backup.name}`));
3386
3468
  });
3387
- console.log(chalk22.gray(`
3469
+ console.log(chalk23.gray(`
3388
3470
  Total: ${backups.length} backup(s)
3389
3471
  `));
3390
3472
  }
3391
3473
 
3392
3474
  // src/commands/db/clean.ts
3393
3475
  import { promises as fs4 } from "fs";
3394
- import chalk23 from "chalk";
3476
+ import chalk24 from "chalk";
3395
3477
  import ora10 from "ora";
3396
- import prompts6 from "prompts";
3478
+ import prompts7 from "prompts";
3397
3479
  async function dbBackupClean(options) {
3398
- console.log(chalk23.blue("\u{1F9F9} Cleaning old backups...\n"));
3480
+ console.log(chalk24.blue("\u{1F9F9} Cleaning old backups...\n"));
3399
3481
  const backups = await listBackupFiles();
3400
3482
  if (backups.length === 0) {
3401
- console.log(chalk23.yellow("No backups found"));
3483
+ console.log(chalk24.yellow("No backups found"));
3402
3484
  return;
3403
3485
  }
3404
3486
  let toDelete = [];
@@ -3415,42 +3497,46 @@ async function dbBackupClean(options) {
3415
3497
  toDelete = backups.slice(defaultKeep);
3416
3498
  }
3417
3499
  if (toDelete.length === 0) {
3418
- console.log(chalk23.green("\u2705 No backups to clean"));
3500
+ console.log(chalk24.green("\u2705 No backups to clean"));
3419
3501
  return;
3420
3502
  }
3421
- console.log(chalk23.yellow(`The following ${toDelete.length} backup(s) will be deleted:
3503
+ console.log(chalk24.yellow(`The following ${toDelete.length} backup(s) will be deleted:
3422
3504
  `));
3423
3505
  toDelete.forEach((backup) => {
3424
- console.log(chalk23.gray(` - ${backup.name} (${backup.size})`));
3506
+ console.log(chalk24.gray(` - ${backup.name} (${backup.size})`));
3425
3507
  });
3426
- const { confirm } = await prompts6({
3508
+ const { confirm } = await prompts7({
3427
3509
  type: "confirm",
3428
3510
  name: "confirm",
3429
3511
  message: "\nProceed with deletion?",
3430
3512
  initial: false
3431
3513
  });
3432
3514
  if (!confirm) {
3433
- console.log(chalk23.gray("Cancelled"));
3515
+ console.log(chalk24.gray("Cancelled"));
3434
3516
  return;
3435
3517
  }
3436
3518
  const spinner = ora10("Deleting backups...").start();
3437
3519
  try {
3438
- await Promise.all(toDelete.map((backup) => fs4.unlink(backup.path)));
3520
+ await Promise.all(toDelete.flatMap((backup) => [
3521
+ fs4.unlink(backup.path),
3522
+ // Remove the .meta.json sidecar too (force: ok if it doesn't exist)
3523
+ fs4.rm(backup.path.replace(/\.(sql|dump)$/, ".meta.json"), { force: true })
3524
+ ]));
3439
3525
  spinner.succeed("Backups deleted");
3440
- console.log(chalk23.green(`
3526
+ console.log(chalk24.green(`
3441
3527
  \u2705 Deleted ${toDelete.length} backup(s)`));
3442
3528
  } catch (error) {
3443
3529
  spinner.fail("Failed to delete backups");
3444
- console.error(chalk23.red(error instanceof Error ? error.message : "Unknown error"));
3530
+ console.error(chalk24.red(error instanceof Error ? error.message : "Unknown error"));
3445
3531
  process.exit(1);
3446
3532
  }
3447
3533
  }
3448
3534
 
3449
3535
  // src/commands/db/reindex.ts
3450
- import { existsSync as existsSync20, readFileSync as readFileSync6, writeFileSync as writeFileSync11, renameSync, copyFileSync } from "fs";
3536
+ import { existsSync as existsSync20, readFileSync as readFileSync7, writeFileSync as writeFileSync12, renameSync, copyFileSync } from "fs";
3451
3537
  import { join as join17 } from "path";
3452
- import chalk24 from "chalk";
3453
- import { loadEnv as loadEnv7 } from "@spfn/core/server";
3538
+ import chalk25 from "chalk";
3539
+ import { loadEnv as loadEnv8 } from "@spfn/core/server";
3454
3540
  function isTimestampPrefix(tag) {
3455
3541
  const prefix = tag.split("_")[0];
3456
3542
  return /^\d{5,}$/.test(prefix);
@@ -3466,19 +3552,19 @@ function parseTag(tag) {
3466
3552
  };
3467
3553
  }
3468
3554
  async function dbReindex(options = {}) {
3469
- loadEnv7();
3555
+ loadEnv8();
3470
3556
  const { getDrizzleConfig } = await import("@spfn/core/db");
3471
3557
  const config = getDrizzleConfig({ disablePackageDiscovery: true });
3472
3558
  const outDir = config.out;
3473
3559
  const journalPath = join17(outDir, "meta", "_journal.json");
3474
3560
  if (!existsSync20(journalPath)) {
3475
- console.error(chalk24.red("\u274C No _journal.json found at:"), journalPath);
3476
- console.log(chalk24.yellow("\u{1F4A1} Run `spfn db generate` first to create migrations"));
3561
+ console.error(chalk25.red("\u274C No _journal.json found at:"), journalPath);
3562
+ console.log(chalk25.yellow("\u{1F4A1} Run `spfn db generate` first to create migrations"));
3477
3563
  process.exit(1);
3478
3564
  }
3479
- const journal = JSON.parse(readFileSync6(journalPath, "utf-8"));
3565
+ const journal = JSON.parse(readFileSync7(journalPath, "utf-8"));
3480
3566
  if (journal.entries.length === 0) {
3481
- console.log(chalk24.yellow("No migration entries found \u2014 nothing to reindex."));
3567
+ console.log(chalk25.yellow("No migration entries found \u2014 nothing to reindex."));
3482
3568
  return;
3483
3569
  }
3484
3570
  const renames = [];
@@ -3505,33 +3591,33 @@ async function dbReindex(options = {}) {
3505
3591
  tagUpdates.push({ idx: entry.idx, oldTag: entry.tag, newTag });
3506
3592
  }
3507
3593
  if (tagUpdates.length === 0) {
3508
- console.log(chalk24.green("\u2705 All migrations already use timestamp prefix \u2014 nothing to do."));
3594
+ console.log(chalk25.green("\u2705 All migrations already use timestamp prefix \u2014 nothing to do."));
3509
3595
  if (skipped > 0) {
3510
- console.log(chalk24.dim(` (${skipped} entries already timestamp-prefixed)`));
3596
+ console.log(chalk25.dim(` (${skipped} entries already timestamp-prefixed)`));
3511
3597
  }
3512
3598
  return;
3513
3599
  }
3514
- console.log(chalk24.bold("\n\u{1F4CB} Reindex plan:\n"));
3600
+ console.log(chalk25.bold("\n\u{1F4CB} Reindex plan:\n"));
3515
3601
  for (const update of tagUpdates) {
3516
3602
  console.log(
3517
- chalk24.dim(` [${update.idx}]`),
3518
- chalk24.red(update.oldTag),
3519
- chalk24.dim("\u2192"),
3520
- chalk24.green(update.newTag)
3603
+ chalk25.dim(` [${update.idx}]`),
3604
+ chalk25.red(update.oldTag),
3605
+ chalk25.dim("\u2192"),
3606
+ chalk25.green(update.newTag)
3521
3607
  );
3522
3608
  }
3523
- console.log(chalk24.dim(`
3609
+ console.log(chalk25.dim(`
3524
3610
  ${renames.length} file(s) to rename, ${tagUpdates.length} journal tag(s) to update`));
3525
3611
  if (skipped > 0) {
3526
- console.log(chalk24.dim(` ${skipped} entry/entries already timestamp-prefixed (skipped)`));
3612
+ console.log(chalk25.dim(` ${skipped} entry/entries already timestamp-prefixed (skipped)`));
3527
3613
  }
3528
3614
  if (options.dryRun) {
3529
- console.log(chalk24.yellow("\n\u{1F50D} Dry run \u2014 no changes applied."));
3615
+ console.log(chalk25.yellow("\n\u{1F50D} Dry run \u2014 no changes applied."));
3530
3616
  return;
3531
3617
  }
3532
3618
  const backupPath = journalPath + ".bak";
3533
3619
  copyFileSync(journalPath, backupPath);
3534
- console.log(chalk24.dim(`
3620
+ console.log(chalk25.dim(`
3535
3621
  Backed up journal \u2192 ${backupPath}`));
3536
3622
  for (const rename of renames) {
3537
3623
  renameSync(rename.from, rename.to);
@@ -3542,8 +3628,8 @@ async function dbReindex(options = {}) {
3542
3628
  entry.tag = update.newTag;
3543
3629
  }
3544
3630
  }
3545
- writeFileSync11(journalPath, JSON.stringify(journal, null, 2) + "\n");
3546
- console.log(chalk24.green(`
3631
+ writeFileSync12(journalPath, JSON.stringify(journal, null, 2) + "\n");
3632
+ console.log(chalk25.green(`
3547
3633
  \u2705 Reindex complete \u2014 ${tagUpdates.length} migration(s) converted to timestamp prefix.`));
3548
3634
  }
3549
3635
 
@@ -3562,1415 +3648,96 @@ dbCommand.command("backup:clean").description("Clean old database backups").opti
3562
3648
  dbCommand.command("reindex").description("Convert migration files from sequential to timestamp prefix").option("--dry-run", "Show changes without applying").action((options) => dbReindex(options));
3563
3649
 
3564
3650
  // src/commands/add.ts
3651
+ init_package_manager();
3565
3652
  import { Command as Command10 } from "commander";
3566
- import { existsSync as existsSync21, readFileSync as readFileSync7 } from "fs";
3653
+ import { existsSync as existsSync21, readFileSync as readFileSync8 } from "fs";
3567
3654
  import { join as join18 } from "path";
3568
- import { exec as exec2 } from "child_process";
3569
- import { promisify as promisify2 } from "util";
3570
- import chalk25 from "chalk";
3655
+ import { execa as execa7 } from "execa";
3656
+ import chalk26 from "chalk";
3571
3657
  import ora11 from "ora";
3572
- var execAsync2 = promisify2(exec2);
3573
3658
  async function addPackage(packageName) {
3574
3659
  if (!packageName.includes("/")) {
3575
- console.error(chalk25.red("\u274C Please specify full package name"));
3576
- console.log(chalk25.yellow("\n\u{1F4A1} Examples:"));
3577
- console.log(chalk25.gray(" pnpm spfn add @spfn/cms"));
3578
- console.log(chalk25.gray(" pnpm spfn add @mycompany/spfn-analytics"));
3660
+ console.error(chalk26.red("\u274C Please specify full package name"));
3661
+ console.log(chalk26.yellow("\n\u{1F4A1} Examples:"));
3662
+ console.log(chalk26.gray(" pnpm spfn add @spfn/cms"));
3663
+ console.log(chalk26.gray(" pnpm spfn add @mycompany/spfn-analytics"));
3579
3664
  process.exit(1);
3580
3665
  }
3581
- console.log(chalk25.blue(`
3666
+ console.log(chalk26.blue(`
3582
3667
  \u{1F4E6} Setting up ${packageName}...
3583
3668
  `));
3584
- try {
3585
- const pkgPath = join18(process.cwd(), "node_modules", ...packageName.split("/"));
3586
- const pkgJsonPath = join18(pkgPath, "package.json");
3587
- if (!existsSync21(pkgJsonPath)) {
3588
- const installSpinner = ora11("Installing package...").start();
3589
- try {
3590
- await execAsync2(`pnpm add ${packageName}`);
3591
- installSpinner.succeed("Package installed");
3592
- } catch (error) {
3593
- installSpinner.fail("Failed to install package");
3594
- throw error;
3595
- }
3596
- } else {
3597
- console.log(chalk25.gray("\u2713 Package already installed (using local version)\n"));
3598
- }
3599
- if (!existsSync21(pkgJsonPath)) {
3600
- throw new Error(`Package ${packageName} not found after installation`);
3669
+ const pkgPath = join18(process.cwd(), "node_modules", ...packageName.split("/"));
3670
+ const pkgJsonPath = join18(pkgPath, "package.json");
3671
+ if (!existsSync21(pkgJsonPath)) {
3672
+ const pm = detectPackageManager(process.cwd());
3673
+ const installSpinner = ora11("Installing package...").start();
3674
+ try {
3675
+ await execa7(pm, ["add", packageName]);
3676
+ installSpinner.succeed("Package installed");
3677
+ } catch (error) {
3678
+ installSpinner.fail("Failed to install package");
3679
+ console.error(chalk26.red(error instanceof Error ? error.message : "Unknown error"));
3680
+ process.exit(1);
3601
3681
  }
3602
- const pkgJson = JSON.parse(readFileSync7(pkgJsonPath, "utf-8"));
3603
- if (pkgJson.spfn?.migrations) {
3604
- console.log(chalk25.blue(`
3682
+ } else {
3683
+ console.log(chalk26.gray("\u2713 Package already installed (using local version)\n"));
3684
+ }
3685
+ if (!existsSync21(pkgJsonPath)) {
3686
+ console.error(chalk26.red(`\u274C Package ${packageName} not found after installation`));
3687
+ process.exit(1);
3688
+ }
3689
+ const pkgJson = JSON.parse(readFileSync8(pkgJsonPath, "utf-8"));
3690
+ if (pkgJson.spfn?.migrations) {
3691
+ console.log(chalk26.blue(`
3605
3692
  \u{1F5C4}\uFE0F Setting up database for ${packageName}...
3606
3693
  `));
3607
- const { env: env7 } = await import("@spfn/core/config");
3608
- if (!env7.DATABASE_URL) {
3609
- console.log(chalk25.yellow("\u26A0\uFE0F DATABASE_URL not found"));
3610
- console.log(chalk25.gray("Skipping database setup. Run migrations manually when ready:\n"));
3611
- console.log(chalk25.gray(` pnpm spfn db push
3612
- `));
3613
- } else {
3614
- const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
3615
- const functions = discoverFunctionMigrations2(process.cwd());
3616
- const targetFunction = functions.find((f) => f.packageName === packageName);
3617
- if (targetFunction) {
3618
- const spinner = ora11("Applying migrations...").start();
3619
- try {
3620
- await executeFunctionMigrations2([targetFunction]);
3621
- spinner.succeed("Migrations applied");
3622
- } catch (error) {
3623
- spinner.fail("Failed to apply migrations");
3624
- throw error;
3625
- }
3626
- } else {
3627
- console.log(chalk25.gray("\u2139\uFE0F No migrations found for this package"));
3694
+ const { env: env8 } = await import("@spfn/core/config");
3695
+ if (!env8.DATABASE_URL) {
3696
+ console.log(chalk26.yellow("\u26A0\uFE0F DATABASE_URL not found"));
3697
+ console.log(chalk26.gray("Skipping database setup. Run migrations manually when ready:\n"));
3698
+ console.log(chalk26.gray(" pnpm spfn db push\n"));
3699
+ } else {
3700
+ const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
3701
+ const functions = discoverFunctionMigrations2(process.cwd());
3702
+ const targetFunction = functions.find((f) => f.packageName === packageName);
3703
+ if (targetFunction) {
3704
+ const spinner = ora11("Applying migrations...").start();
3705
+ try {
3706
+ await executeFunctionMigrations2([targetFunction]);
3707
+ spinner.succeed("Migrations applied");
3708
+ } catch (error) {
3709
+ spinner.fail("Failed to apply migrations");
3710
+ console.error(chalk26.red(error instanceof Error ? error.message : "Unknown error"));
3711
+ process.exit(1);
3628
3712
  }
3713
+ } else {
3714
+ console.log(chalk26.gray("\u2139\uFE0F No migrations found for this package"));
3629
3715
  }
3630
- } else {
3631
- console.log(chalk25.gray("\n\u2139\uFE0F No database migrations to apply"));
3632
3716
  }
3633
- console.log(chalk25.green(`
3717
+ } else {
3718
+ console.log(chalk26.gray("\n\u2139\uFE0F No database migrations to apply"));
3719
+ }
3720
+ console.log(chalk26.green(`
3634
3721
  \u2705 ${packageName} installed successfully!
3635
3722
  `));
3636
- if (pkgJson.spfn?.setupMessage) {
3637
- console.log(chalk25.cyan("\u{1F4DA} Setup Guide:"));
3638
- console.log(pkgJson.spfn.setupMessage);
3639
- console.log();
3640
- }
3641
- } catch (error) {
3642
- console.error(chalk25.red(`
3643
- \u274C Failed to install ${packageName}
3644
- `));
3645
- console.error(chalk25.red(error instanceof Error ? error.message : "Unknown error"));
3646
- process.exit(1);
3723
+ if (pkgJson.spfn?.setupMessage) {
3724
+ console.log(chalk26.cyan("\u{1F4DA} Setup Guide:"));
3725
+ console.log(pkgJson.spfn.setupMessage);
3726
+ console.log();
3647
3727
  }
3648
3728
  }
3649
3729
  var addCommand = new Command10("add").description("Install and set up SPFN ecosystem packages").argument("<package>", "Package name (e.g., @spfn/cms, @mycompany/spfn-analytics)").action(addPackage);
3650
3730
 
3651
- // src/commands/generate.ts
3652
- init_logger();
3731
+ // src/commands/env.ts
3653
3732
  import { Command as Command11 } from "commander";
3654
- import ora12 from "ora";
3655
- import { join as join27 } from "path";
3656
- import { existsSync as existsSync24 } from "fs";
3657
3733
  import chalk27 from "chalk";
3658
-
3659
- // src/commands/generate/prompts.ts
3660
- init_logger();
3661
- import prompts7 from "prompts";
3662
- import chalk26 from "chalk";
3663
- async function promptScope() {
3664
- const response = await prompts7({
3665
- type: "text",
3666
- name: "scope",
3667
- message: "NPM scope (e.g., @mycompany, @username):",
3668
- initial: "@spfn",
3669
- validate: (value) => {
3670
- if (!value || value.length === 0) {
3671
- return "Scope is required";
3672
- }
3673
- if (!/^@[a-z0-9-]+$/.test(value)) {
3674
- return "Scope must start with @ and contain lowercase alphanumeric with hyphens";
3675
- }
3676
- return true;
3677
- }
3678
- });
3679
- if (!response.scope) {
3680
- logger.info("Cancelled");
3681
- process.exit(0);
3682
- }
3683
- return response.scope;
3684
- }
3685
- async function promptFunctionName() {
3686
- const response = await prompts7({
3687
- type: "text",
3688
- name: "fnName",
3689
- message: "Function name:",
3690
- validate: (value) => {
3691
- if (!value || value.length === 0) {
3692
- return "Function name is required";
3693
- }
3694
- if (!/^[a-z][a-z0-9-]*$/.test(value)) {
3695
- return "Function name must be lowercase alphanumeric with hyphens";
3696
- }
3697
- return true;
3698
- }
3699
- });
3700
- if (!response.fnName) {
3701
- logger.info("Cancelled");
3702
- process.exit(0);
3703
- }
3704
- return response.fnName;
3705
- }
3706
- async function promptDescription(fnName) {
3707
- const response = await prompts7({
3708
- type: "text",
3709
- name: "description",
3710
- message: "Function description:",
3711
- initial: "A description of what this module does"
3712
- });
3713
- return response.description || "A description of what this module does";
3714
- }
3715
- async function promptEntities() {
3716
- const response = await prompts7({
3717
- type: "list",
3718
- name: "entities",
3719
- message: "Entity names (comma-separated, press enter to skip):",
3720
- separator: ",",
3721
- initial: ""
3722
- });
3723
- return response.entities || [];
3724
- }
3725
- async function confirmConfiguration(config) {
3726
- const { scope, fnName, description, entities, enableCache, enableRoutes } = config;
3727
- console.log("");
3728
- logger.info(chalk26.bold("\u26A1 Function Configuration:"));
3729
- console.log(` ${chalk26.gray("Package:")} ${chalk26.cyan(`${scope}/${fnName}`)}`);
3730
- console.log(` ${chalk26.gray("Description:")} ${description}`);
3731
- console.log(` ${chalk26.gray("Entities:")} ${entities.length > 0 ? entities.join(", ") : chalk26.gray("none")}`);
3732
- console.log(` ${chalk26.gray("Cache:")} ${enableCache ? chalk26.green("yes") : chalk26.gray("no")}`);
3733
- console.log(` ${chalk26.gray("Routes:")} ${enableRoutes ? chalk26.green("yes") : chalk26.gray("no")}`);
3734
- console.log("");
3735
- const { confirmed } = await prompts7({
3736
- type: "confirm",
3737
- name: "confirmed",
3738
- message: "Create function?",
3739
- initial: true
3740
- });
3741
- if (!confirmed) {
3742
- logger.info("Cancelled");
3743
- return false;
3744
- }
3745
- return true;
3746
- }
3747
-
3748
- // src/commands/generate/generators/structure.ts
3749
- import { join as join26 } from "path";
3750
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync18 } from "fs";
3751
-
3752
- // src/commands/generate/generators/config.ts
3753
- import { join as join20 } from "path";
3754
- import { writeFileSync as writeFileSync12, mkdirSync as mkdirSync3 } from "fs";
3755
-
3756
- // src/commands/generate/string-utils.ts
3757
- function toPascalCase(str) {
3758
- return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
3759
- }
3760
- function toKebabCase(str) {
3761
- return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
3762
- }
3763
- function toSnakeCase(str) {
3764
- return str.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase().replace(/-/g, "_");
3765
- }
3766
- function toCamelCase(str) {
3767
- return str.split(/[-_]/).map(
3768
- (word, index) => index === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
3769
- ).join("");
3770
- }
3771
- function toSafeSchemaName(str) {
3772
- let result = str.toLowerCase().replace(/[@]/g, "").replace(/[^a-z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
3773
- if (/^[0-9]/.test(result)) {
3774
- result = "_" + result;
3775
- }
3776
- if (result.length > 63) {
3777
- result = result.substring(0, 63);
3778
- }
3779
- return result;
3780
- }
3781
-
3782
- // src/commands/generate/template-loader.ts
3783
- import { readFileSync as readFileSync8, existsSync as existsSync22 } from "fs";
3784
- import { join as join19, dirname as dirname3 } from "path";
3785
- import { fileURLToPath as fileURLToPath2 } from "url";
3786
- function findTemplatesPath2() {
3787
- const __filename = fileURLToPath2(import.meta.url);
3788
- const __dirname2 = dirname3(__filename);
3789
- const distPath = join19(__dirname2, "commands", "generate", "templates");
3790
- if (existsSync22(distPath)) {
3791
- return distPath;
3792
- }
3793
- const sameDirPath = join19(__dirname2, "templates");
3794
- if (existsSync22(sameDirPath)) {
3795
- return sameDirPath;
3796
- }
3797
- const srcPath = join19(__dirname2, "..", "..", "src", "commands", "generate", "templates");
3798
- if (existsSync22(srcPath)) {
3799
- return srcPath;
3800
- }
3801
- throw new Error(`Templates directory not found. Tried: ${distPath}, ${sameDirPath}, ${srcPath}`);
3802
- }
3803
- function loadTemplate(templateName, variables) {
3804
- const templatesDir = findTemplatesPath2();
3805
- const templatePath = join19(templatesDir, `${templateName}.template`);
3806
- let content = readFileSync8(templatePath, "utf-8");
3807
- for (const [key, value] of Object.entries(variables)) {
3808
- const regex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
3809
- content = content.replace(regex, value);
3810
- }
3811
- return content;
3812
- }
3813
-
3814
- // src/commands/generate/generators/config.ts
3815
- function generatePackageJson(fnDir, scope, fnName, description) {
3816
- const content = {
3817
- name: `${scope}/${fnName}`,
3818
- version: "0.1.0-alpha.1",
3819
- description,
3820
- type: "module",
3821
- main: "./dist/index.js",
3822
- types: "./dist/index.d.ts",
3823
- exports: {
3824
- ".": {
3825
- types: "./dist/index.d.ts",
3826
- import: "./dist/index.js",
3827
- require: "./dist/index.js"
3828
- },
3829
- "./server": {
3830
- types: "./dist/server.d.ts",
3831
- import: "./dist/server.js",
3832
- require: "./dist/server.js"
3833
- },
3834
- "./client": {
3835
- types: "./dist/client.d.ts",
3836
- import: "./dist/client.js",
3837
- require: "./dist/client.js"
3838
- }
3839
- },
3840
- files: ["dist", "migrations", "README.md"],
3841
- scripts: {
3842
- build: "npm run db:generate && tsup",
3843
- watch: "tsup --watch",
3844
- dev: "tsup --watch",
3845
- "type-check": "tsc --noEmit",
3846
- clean: "rm -rf dist migrations",
3847
- "db:generate": "drizzle-kit generate",
3848
- codegen: "spfn codegen run",
3849
- test: "vitest",
3850
- "test:watch": "vitest --watch",
3851
- "test:coverage": "vitest run --coverage",
3852
- "test:routes": "vitest src/server/routes",
3853
- "docker:test:up": "docker compose -f docker-compose.test.yml up -d",
3854
- "docker:test:down": "docker compose -f docker-compose.test.yml down",
3855
- "docker:test:logs": "docker compose -f docker-compose.test.yml logs -f"
3856
- },
3857
- keywords: ["spfn", fnName, "nextjs", "typescript"],
3858
- author: "SPFN Team",
3859
- license: "MIT",
3860
- spfn: {
3861
- prefix: `/_${fnName}`,
3862
- schemas: ["./dist/server/entities/*.js"],
3863
- routes: { dir: "./dist/server/routes" },
3864
- migrations: { dir: "./migrations" },
3865
- codegen: {
3866
- generators: [
3867
- {
3868
- name: "@spfn/core:contract",
3869
- contractsDir: "src/lib/contracts",
3870
- outputPath: "src/api",
3871
- runOn: ["build", "manual"]
3872
- },
3873
- {
3874
- name: "${scope}/${fnName}:example",
3875
- runOn: ["manual"]
3876
- }
3877
- ]
3878
- },
3879
- setupMessage: ` \u{1F4DA} Next steps:
3880
- 1. Import from '${scope}/${fnName}'
3881
- 2. View data: pnpm spfn db studio
3882
- 3. API at: http://localhost:8790/_${fnName}`
3883
- },
3884
- peerDependencies: {
3885
- "@spfn/core": "workspace:*",
3886
- "drizzle-orm": "^0.44.0",
3887
- next: "^15.0.0",
3888
- react: "^18.0.0 || ^19.0.0"
3889
- },
3890
- dependencies: {
3891
- "@sinclair/typebox": "^0.34.0"
3892
- },
3893
- devDependencies: {
3894
- "@types/node": "^20.11.0",
3895
- "@types/react": "^19",
3896
- "@vitest/coverage-v8": "^4.0.6",
3897
- "drizzle-kit": "^0.31.6",
3898
- "drizzle-typebox": "^0.1.0",
3899
- glob: "^11.0.3",
3900
- postgres: "^3.4.0",
3901
- spfn: "workspace:*",
3902
- tsup: "^8.3.5",
3903
- tsx: "^4.19.2",
3904
- typescript: "^5",
3905
- vitest: "^4.0.6"
3906
- }
3907
- };
3908
- writeFileSync12(
3909
- join20(fnDir, "package.json"),
3910
- JSON.stringify(content, null, 4) + "\n"
3911
- );
3912
- }
3913
- function generateTsConfig(fnDir) {
3914
- const content = {
3915
- compilerOptions: {
3916
- target: "ES2022",
3917
- module: "ESNext",
3918
- lib: ["ES2022"],
3919
- moduleResolution: "bundler",
3920
- esModuleInterop: true,
3921
- skipLibCheck: true,
3922
- strict: true,
3923
- noUncheckedIndexedAccess: true,
3924
- declaration: true,
3925
- declarationMap: true,
3926
- sourceMap: true,
3927
- outDir: "./dist",
3928
- rootDir: "./src",
3929
- composite: false,
3930
- isolatedModules: true,
3931
- resolveJsonModule: true,
3932
- allowSyntheticDefaultImports: true,
3933
- forceConsistentCasingInFileNames: true,
3934
- baseUrl: ".",
3935
- paths: {
3936
- "@/*": ["./src/*"]
3937
- }
3938
- },
3939
- include: ["src/**/*"],
3940
- exclude: ["node_modules", "dist", "**/*.test.ts", "**/__tests__/**"]
3941
- };
3942
- writeFileSync12(
3943
- join20(fnDir, "tsconfig.json"),
3944
- JSON.stringify(content, null, 4) + "\n"
3945
- );
3946
- }
3947
- function generateTsupConfig(fnDir) {
3948
- const content = `import { defineConfig } from 'tsup';
3949
- import { glob } from 'glob';
3950
- import * as path from 'path';
3951
-
3952
- // Find all route handler files
3953
- const routeFiles = glob.sync('src/server/routes/**/index.ts');
3954
- const routeEntries = Object.fromEntries(
3955
- routeFiles.map((file) =>
3956
- {
3957
- const key = file
3958
- .replace('src/', '')
3959
- .replace('/index.ts', '/index')
3960
- .replace('.ts', '');
3961
- return [key, file];
3962
- })
3963
- );
3964
-
3965
- // Find all entity files
3966
- const entityFiles = glob.sync('src/server/entities/**/*.ts');
3967
- const entityEntries = Object.fromEntries(
3968
- entityFiles.map((file) =>
3969
- {
3970
- const key = file.replace('src/', '').replace('.ts', '');
3971
- return [key, file];
3972
- })
3973
- );
3974
-
3975
- // Find all contract files
3976
- const contractFiles = glob.sync('src/lib/contracts/**/*.ts');
3977
- const contractEntries = Object.fromEntries(
3978
- contractFiles.map((file) =>
3979
- {
3980
- const key = file.replace('src/', '').replace('.ts', '');
3981
- return [key, file];
3982
- })
3983
- );
3984
-
3985
- export default defineConfig({
3986
- entry: {
3987
- index: 'src/index.ts',
3988
- server: 'src/server.ts',
3989
- client: 'src/client.ts',
3990
- ...entityEntries,
3991
- ...contractEntries,
3992
- ...routeEntries,
3993
- },
3994
- format: ['esm'],
3995
- dts: true,
3996
- sourcemap: true,
3997
- clean: true,
3998
- splitting: false,
3999
- esbuildOptions(options)
4000
- {
4001
- // Add path alias support
4002
- options.alias = {
4003
- '@': path.resolve(__dirname, './src'),
4004
- };
4005
- },
4006
- external: [
4007
- '@spfn/core',
4008
- 'drizzle-orm',
4009
- 'next',
4010
- 'react',
4011
- '@sinclair/typebox',
4012
- 'jiti',
4013
- 'zustand',
4014
- 'server-only',
4015
- 'react-dom',
4016
- ],
4017
- });
4018
- `;
4019
- writeFileSync12(join20(fnDir, "tsup.config.ts"), content);
4020
- }
4021
- function generateDrizzleConfig(fnDir, scope, fnName) {
4022
- const schemaName = `spfn_${toSnakeCase(fnName)}`;
4023
- const content = `import { defineConfig } from 'drizzle-kit';
4024
-
4025
- /**
4026
- * Drizzle Config for ${scope}/${fnName} package
4027
- *
4028
- * This generates migrations for the ${fnName} schema that will be
4029
- * bundled with the package and applied automatically when
4030
- * users run \`spfn db push\` or \`spfn db migrate\`
4031
- */
4032
- export default defineConfig({
4033
- schema: './src/server/entities/*.ts',
4034
- out: './migrations',
4035
- dialect: 'postgresql',
4036
- schemaFilter: ['${schemaName}'], // Only generate for ${fnName} schema
4037
- });
4038
- `;
4039
- writeFileSync12(join20(fnDir, "drizzle.config.ts"), content);
4040
- }
4041
- function generateExampleGenerator(fnDir, scope, fnName) {
4042
- const pascalName = toPascalCase(fnName);
4043
- const content = `/**
4044
- * Example Custom Generator for ${fnName}
4045
- *
4046
- * This is a template for creating custom code generators.
4047
- * Uncomment and modify to create your own generator.
4048
- */
4049
-
4050
- import type { Generator, GeneratorOptions } from '@spfn/core/codegen';
4051
- import { writeFileSync, mkdirSync } from 'fs';
4052
- import { join } from 'path';
4053
-
4054
- /**
4055
- * Example: Auto-generate index files for types
4056
- */
4057
- export function create${pascalName}ExampleGenerator(): Generator
4058
- {
4059
- return {
4060
- name: '${fnName}:example',
4061
-
4062
- // Define when this generator should run
4063
- // Options: 'build', 'manual', 'watch', 'start'
4064
- runOn: ['build', 'manual'],
4065
-
4066
- // Optional: watch for file changes
4067
- watchPatterns: [
4068
- 'src/lib/types/**/*.ts',
4069
- ],
4070
-
4071
- async generate(options: GeneratorOptions): Promise<void>
4072
- {
4073
- const { cwd, debug } = options;
4074
-
4075
- if (debug)
4076
- {
4077
- console.log('Running ${fnName} example generator...');
4078
- }
4079
-
4080
- // Your generation logic here
4081
- const outputDir = join(cwd, 'src/generated');
4082
- const outputPath = join(outputDir, 'example.ts');
4083
-
4084
- // Ensure output directory exists
4085
- mkdirSync(outputDir, { recursive: true });
4086
-
4087
- // Generate content
4088
- const content = \`/**
4089
- * Auto-generated file
4090
- *
4091
- * @generated \${new Date().toISOString()}
4092
- */
4093
-
4094
- export const generatedAt = '\${new Date().toISOString()}';
4095
- export const moduleName = '${fnName}';
4096
- \`;
4097
-
4098
- writeFileSync(outputPath, content, 'utf-8');
4099
-
4100
- if (debug)
4101
- {
4102
- console.log('\u2713 Generated:', outputPath);
4103
- }
4104
- },
4105
- };
4106
- }
4107
- `;
4108
- const generatorsDir = join20(fnDir, "src/server/generators");
4109
- mkdirSync3(generatorsDir, { recursive: true });
4110
- writeFileSync12(
4111
- join20(generatorsDir, "example-generator.ts"),
4112
- content
4113
- );
4114
- const indexContent = `/**
4115
- * Custom Generators
4116
- *
4117
- * Export your custom generators here.
4118
- *
4119
- * The example generator is already registered in package.json.
4120
- * Comment it out if you don't want to use it.
4121
- */
4122
-
4123
- export { create${pascalName}ExampleGenerator } from './example-generator.js';
4124
- `;
4125
- writeFileSync12(
4126
- join20(generatorsDir, "index.ts"),
4127
- indexContent
4128
- );
4129
- }
4130
- function generateReadme(fnDir, scope, fnName, description) {
4131
- const content = `# ${scope}/${fnName}
4132
-
4133
- ${description}
4134
-
4135
- ## \u{1F4E6} Installation
4136
-
4137
- For users of your module:
4138
-
4139
- \`\`\`bash
4140
- spfn add ${scope}/${fnName}
4141
- \`\`\`
4142
-
4143
- ## \u{1F680} Usage
4144
-
4145
- \`\`\`typescript
4146
- // Server-side
4147
- import { /* your entities, repositories */ } from '${scope}/${fnName}/server';
4148
-
4149
- // Client-side
4150
- import { /* your hooks, components */ } from '${scope}/${fnName}/client';
4151
-
4152
- // Generated API client
4153
- import { ${fnName}Api } from '${scope}/${fnName}/api';
4154
-
4155
- // Example: Using the generated API client
4156
- const data = await ${fnName}Api.list();
4157
- \`\`\`
4158
-
4159
- ## \u{1F4C1} Project Structure
4160
-
4161
- \`\`\`
4162
- src/
4163
- \u251C\u2500\u2500 lib/
4164
- \u2502 \u251C\u2500\u2500 contracts/ # API contracts (shared types)
4165
- \u2502 \u2514\u2500\u2500 types/ # Shared TypeScript types
4166
- \u251C\u2500\u2500 server/
4167
- \u2502 \u251C\u2500\u2500 entities/ # Drizzle ORM entities
4168
- \u2502 \u251C\u2500\u2500 repositories/ # Data access layer
4169
- \u2502 \u251C\u2500\u2500 routes/ # API route handlers
4170
- \u2502 \u251C\u2500\u2500 helpers/ # Server utilities
4171
- \u2502 \u2514\u2500\u2500 generators/ # Code generators (optional)
4172
- \u251C\u2500\u2500 client/
4173
- \u2502 \u251C\u2500\u2500 hooks/ # React hooks
4174
- \u2502 \u251C\u2500\u2500 store/ # Zustand stores
4175
- \u2502 \u2514\u2500\u2500 components/ # React components
4176
- \u251C\u2500\u2500 api/ # Auto-generated API client (do not edit manually)
4177
- \u251C\u2500\u2500 index.ts # Main entry point
4178
- \u251C\u2500\u2500 server.ts # Server exports
4179
- \u2514\u2500\u2500 client.ts # Client exports
4180
- \`\`\`
4181
-
4182
- ## \u{1F6E0}\uFE0F Development Guide
4183
-
4184
- ### Available Scripts
4185
-
4186
- \`\`\`bash
4187
- # Development
4188
- pnpm dev # Watch mode - auto rebuild on changes
4189
- pnpm build # Build for production
4190
- pnpm type-check # TypeScript type checking
4191
-
4192
- # Database
4193
- pnpm db:generate # Generate migrations from entities
4194
- pnpm clean # Clean dist and migrations
4195
-
4196
- # Code Generation
4197
- pnpm codegen # Generate API client from contracts
4198
-
4199
- # Testing
4200
- pnpm test # Run tests
4201
- pnpm test:watch # Watch mode
4202
- pnpm test:coverage # Generate coverage report
4203
- pnpm test:routes # Test only routes
4204
-
4205
- # Docker (for testing)
4206
- pnpm docker:test:up # Start test database
4207
- pnpm docker:test:down # Stop test database
4208
- pnpm docker:test:logs # View database logs
4209
- \`\`\`
4210
-
4211
- ### Development Workflow
4212
-
4213
- 1. **Define Entities** (\`src/server/entities/\`)
4214
- \`\`\`typescript
4215
- // src/server/entities/${fnName}-example.ts
4216
- import { pgTable, uuid, text, timestamp } from 'drizzle-orm/pg-core';
4217
-
4218
- export const ${fnName}Examples = pgTable('${fnName}_examples', {
4219
- id: uuid('id').primaryKey().defaultRandom(),
4220
- name: text('name').notNull(),
4221
- createdAt: timestamp('created_at').defaultNow().notNull(),
4222
- });
4223
- \`\`\`
4224
-
4225
- 2. **Generate Migrations**
4226
- \`\`\`bash
4227
- pnpm db:generate
4228
- \`\`\`
4229
-
4230
- 3. **Create Repositories** (\`src/server/repositories/\`)
4231
- \`\`\`typescript
4232
- // src/server/repositories/example-repository.ts
4233
- import { db } from '@spfn/core/db';
4234
- import { ${fnName}Examples } from '../entities/${fnName}-example';
4235
-
4236
- export class ExampleRepository {
4237
- static async findAll() {
4238
- return db.select().from(${fnName}Examples);
4239
- }
4240
- }
4241
- \`\`\`
4242
-
4243
- 4. **Define Contracts** (\`src/lib/contracts/\`)
4244
- \`\`\`typescript
4245
- // src/lib/contracts/example.ts
4246
- import { contract } from '@spfn/core';
4247
- import { Type as t } from '@sinclair/typebox';
4248
-
4249
- export const getExamplesContract = contract({
4250
- method: 'GET',
4251
- path: '/_${fnName}/examples',
4252
- responses: {
4253
- 200: t.Array(t.Object({
4254
- id: t.String(),
4255
- name: t.String(),
4256
- createdAt: t.String(),
4257
- })),
4258
- },
4259
- });
4260
- \`\`\`
4261
-
4262
- 5. **Create Routes** (\`src/server/routes/\`)
4263
- \`\`\`typescript
4264
- // src/server/routes/examples/index.ts
4265
- import { route } from '@spfn/core';
4266
- import { getExamplesContract } from '@/lib/contracts/example';
4267
- import { ExampleRepository } from '@/server/repositories/example-repository';
4268
-
4269
- export const GET = route(getExamplesContract, async () => {
4270
- const examples = await ExampleRepository.findAll();
4271
- return { status: 200, body: examples };
4272
- });
4273
- \`\`\`
4274
-
4275
- 6. **Generate API Client**
4276
- \`\`\`bash
4277
- pnpm codegen
4278
- \`\`\`
4279
-
4280
- This generates type-safe API client in \`src/api/\`:
4281
- \`\`\`typescript
4282
- // Auto-generated
4283
- export const ${fnName}Api = {
4284
- examples: Examples,
4285
- // ... other resources
4286
- };
4287
- \`\`\`
4288
-
4289
- 7. **Write Tests** (\`src/server/routes/__tests__/\`)
4290
- \`\`\`typescript
4291
- import { describe, it, expect, beforeAll } from 'vitest';
4292
- import { GET } from '../examples/index';
4293
-
4294
- describe('GET /_${fnName}/examples', () => {
4295
- it('should return all examples', async () => {
4296
- const response = await GET();
4297
- expect(response.status).toBe(200);
4298
- expect(Array.isArray(response.body)).toBe(true);
4299
- });
4300
- });
4301
- \`\`\`
4302
-
4303
- 8. **Create Client Hooks** (optional)
4304
- \`\`\`typescript
4305
- // src/client/hooks/use-examples.ts
4306
- 'use client';
4307
- import { useState, useEffect } from 'react';
4308
- import { ${fnName}Api } from '@/api';
4309
-
4310
- export function useExamples() {
4311
- const [examples, setExamples] = useState([]);
4312
- const [loading, setLoading] = useState(true);
4313
-
4314
- useEffect(() => {
4315
- ${fnName}Api.examples.list()
4316
- .then(setExamples)
4317
- .finally(() => setLoading(false));
4318
- }, []);
4319
-
4320
- return { examples, loading };
4321
- }
4322
- \`\`\`
4323
-
4324
- ### Configuration
4325
-
4326
- The module is configured via \`package.json\`:
4327
-
4328
- \`\`\`json
4329
- {
4330
- "spfn": {
4331
- "prefix": "/_${fnName}",
4332
- "schemas": ["./dist/server/entities/*.js"],
4333
- "routes": { "dir": "./dist/server/routes" },
4334
- "migrations": { "dir": "./migrations" },
4335
- "codegen": {
4336
- "generators": [{
4337
- "name": "@spfn/core:contract",
4338
- "contractsDir": "src/lib/contracts",
4339
- "outputPath": "src/api"
4340
- }]
4341
- }
4342
- }
4343
- }
4344
- \`\`\`
4345
-
4346
- #### Configuration Options
4347
-
4348
- | Field | Type | Description |
4349
- |-------|------|-------------|
4350
- | \`prefix\` | \`string\` | API route prefix for this module. Used for namespacing routes and generating API client name. Example: \`"/_${fnName}"\` becomes \`${fnName}Api\` |
4351
- | \`schemas\` | \`string[]\` | Glob patterns for Drizzle entity files. These are used by \`spfn db push\` to apply migrations. |
4352
- | \`routes.dir\` | \`string\` | Directory containing compiled route handlers. Used by \`spfn start\` to load routes. |
4353
- | \`migrations.dir\` | \`string\` | Directory containing database migration files generated by \`drizzle-kit generate\`. |
4354
- | \`codegen.generators\` | \`Generator[]\` | Array of code generators to run. Built-in: \`@spfn/core:contract\` for API client generation. |
4355
- | \`setupMessage\` | \`string\` | (Optional) Message shown after \`spfn add ${scope}/${fnName}\` to guide users. |
4356
-
4357
- #### Built-in Generator: @spfn/core:contract
4358
-
4359
- The contract generator automatically creates type-safe API clients from your route contracts.
4360
-
4361
- \`\`\`json
4362
- {
4363
- "name": "@spfn/core:contract",
4364
- "contractsDir": "src/lib/contracts", // Where to scan for contracts
4365
- "outputPath": "src/api" // Where to generate API client
4366
- }
4367
- \`\`\`
4368
-
4369
- **How it works:**
4370
- 1. Scans \`contractsDir\` for contract exports
4371
- 2. Groups routes by resource (based on file name)
4372
- 3. Generates:
4373
- - Resource files (\`src/api/Examples.ts\`)
4374
- - Type exports for params, query, body, response
4375
- - Main index file with \`${fnName}Api\` object
4376
- 4. Uses \`prefix\` to generate unique API name (avoids naming conflicts)
4377
-
4378
- **Generated API structure:**
4379
- \`\`\`typescript
4380
- // src/api/index.ts
4381
- export const ${fnName}Api = {
4382
- examples: Examples, // Camel case keys
4383
- exampleById: ExampleById,
4384
- // ...
4385
- };
4386
- \`\`\`
4387
-
4388
- ## \u{1F3A8} Custom Generators
4389
-
4390
- You can create custom code generators for your module. Generators run automatically during development and build.
4391
-
4392
- ### Creating a Custom Generator
4393
-
4394
- 1. **Create generator file** (\`src/server/generators/\`)
4395
-
4396
- \`\`\`typescript
4397
- // src/server/generators/sync-generator.ts
4398
- import type { Generator, GeneratorOptions } from '@spfn/core/codegen';
4399
- import { writeFileSync } from 'fs';
4400
- import { join } from 'path';
4401
-
4402
- export function createSyncGenerator(): Generator {
4403
- return {
4404
- name: '${fnName}-sync',
4405
-
4406
- // Define when this generator should run
4407
- runOn: ['build', 'manual'], // 'build', 'manual', 'watch', 'start'
4408
-
4409
- // Optional: watch for file changes
4410
- watchPatterns: [
4411
- 'src/server/entities/**/*.ts',
4412
- 'src/lib/types/**/*.ts',
4413
- ],
4414
-
4415
- async generate(options: GeneratorOptions): Promise<void> {
4416
- const { cwd, debug } = options;
4417
-
4418
- if (debug) {
4419
- console.log('Running ${fnName} sync generator...');
4420
- }
4421
-
4422
- // Your generation logic here
4423
- const outputPath = join(cwd, 'src/generated/sync.ts');
4424
- const content = \`// Auto-generated sync file
4425
- export const generatedAt = '\${new Date().toISOString()}';
4426
- \`;
4427
-
4428
- writeFileSync(outputPath, content, 'utf-8');
4429
-
4430
- if (debug) {
4431
- console.log('\u2713 Generated:', outputPath);
4432
- }
4433
- },
4434
- };
4435
- }
4436
- \`\`\`
4437
-
4438
- 2. **Export generator** (\`src/server/generators/index.ts\`)
4439
-
4440
- \`\`\`typescript
4441
- export { createSyncGenerator } from './sync-generator.js';
4442
- \`\`\`
4443
-
4444
- 3. **Register in package.json**
4445
-
4446
- \`\`\`json
4447
- {
4448
- "spfn": {
4449
- "codegen": {
4450
- "generators": [
4451
- {
4452
- "name": "@spfn/core:contract",
4453
- "contractsDir": "src/lib/contracts",
4454
- "outputPath": "src/api"
4455
- },
4456
- {
4457
- "name": "${scope}/${fnName}:sync", // Custom generator
4458
- "watchPatterns": ["src/server/entities/**/*.ts"]
4459
- }
4460
- ]
4461
- }
4462
- }
4463
- }
4464
- \`\`\`
4465
-
4466
- ### Generator Options
4467
-
4468
- \`\`\`typescript
4469
- interface Generator {
4470
- name: string; // Generator name
4471
- runOn?: GeneratorTrigger[]; // When to run: 'build' | 'manual' | 'watch' | 'start'
4472
- watchPatterns?: string[]; // Glob patterns to watch
4473
- generate: (options: GeneratorOptions) => Promise<void>;
4474
- }
4475
-
4476
- interface GeneratorOptions {
4477
- cwd: string; // Current working directory
4478
- debug?: boolean; // Debug mode flag
4479
- trigger?: { // Trigger information (for watch mode)
4480
- type: 'watch' | 'manual' | 'build' | 'start';
4481
- changedFile?: {
4482
- path: string;
4483
- event: 'add' | 'change' | 'unlink';
4484
- };
4485
- };
4486
- }
4487
- \`\`\`
4488
-
4489
- ### Real-World Example: Type Sync Generator
4490
-
4491
- \`\`\`typescript
4492
- // src/server/generators/type-sync-generator.ts
4493
- import type { Generator, GeneratorOptions } from '@spfn/core/codegen';
4494
- import { readdirSync, readFileSync, writeFileSync } from 'fs';
4495
- import { join } from 'path';
4496
-
4497
- /**
4498
- * Generates a type index that auto-exports all types
4499
- */
4500
- export function createTypeSyncGenerator(): Generator {
4501
- return {
4502
- name: '${fnName}-type-sync',
4503
- runOn: ['build', 'watch', 'manual'],
4504
- watchPatterns: ['src/lib/types/**/*.ts'],
4505
-
4506
- async generate(options: GeneratorOptions): Promise<void> {
4507
- const { cwd, debug } = options;
4508
- const typesDir = join(cwd, 'src/lib/types');
4509
- const outputFile = join(typesDir, 'index.ts');
4510
-
4511
- // Read all type files
4512
- const files = readdirSync(typesDir)
4513
- .filter(f => f.endsWith('.ts') && f !== 'index.ts')
4514
- .map(f => f.replace('.ts', ''));
4515
-
4516
- // Generate exports
4517
- const content = \`/**
4518
- * Auto-generated type index
4519
- *
4520
- * @generated \${new Date().toISOString()}
4521
- */
4522
-
4523
- \${files.map(f => \`export * from './\${f}.js';\`).join('\\n')}
4524
- \`;
4525
-
4526
- writeFileSync(outputFile, content, 'utf-8');
4527
-
4528
- if (debug) {
4529
- console.log(\`\u2713 Synced \${files.length} type files\`);
4530
- }
4531
- },
4532
- };
4533
- }
4534
- \`\`\`
4535
-
4536
- ### Running Generators
4537
-
4538
- \`\`\`bash
4539
- # Run all generators
4540
- pnpm codegen
4541
-
4542
- # Run specific generator
4543
- pnpm spfn codegen run --generator ${scope}/${fnName}:sync
4544
-
4545
- # Watch mode (auto-runs on file changes)
4546
- pnpm spfn codegen watch
4547
- \`\`\`
4548
-
4549
- ## \u{1F9EA} Testing
4550
-
4551
- ### Setup Test Database
4552
-
4553
- \`\`\`bash
4554
- # Start PostgreSQL in Docker
4555
- pnpm docker:test:up
4556
-
4557
- # Run tests
4558
- pnpm test
4559
-
4560
- # Stop database
4561
- pnpm docker:test:down
4562
- \`\`\`
4563
-
4564
- ### Test Structure
4565
-
4566
- \`\`\`
4567
- src/server/routes/
4568
- \u251C\u2500\u2500 examples/
4569
- \u2502 \u251C\u2500\u2500 index.ts
4570
- \u2502 \u2514\u2500\u2500 __tests__/
4571
- \u2502 \u2514\u2500\u2500 index.test.ts
4572
- \`\`\`
4573
-
4574
- ## \u{1F4DA} API Documentation
4575
-
4576
- ### Entities
4577
-
4578
- Document your database entities here.
4579
-
4580
- ### Routes
4581
-
4582
- - \`GET /_${fnName}/...\` - Description
4583
- - \`POST /_${fnName}/...\` - Description
4584
- - \`GET /_${fnName}/.../:id\` - Description
4585
- - \`PATCH /_${fnName}/.../:id\` - Description
4586
- - \`DELETE /_${fnName}/.../:id\` - Description
4587
-
4588
- ### Client API
4589
-
4590
- The generated \`${fnName}Api\` provides type-safe methods for all routes.
4591
-
4592
- ## \u{1F4E6} Publishing
4593
-
4594
- 1. Update version in \`package.json\`
4595
- 2. Build the package: \`pnpm build\`
4596
- 3. Test in a real project
4597
- 4. Publish: \`npm publish\` (or via your CI/CD)
4598
-
4599
- ## \u{1F3D7}\uFE0F Architecture
4600
-
4601
- This module follows SPFN's 3-layer architecture:
4602
-
4603
- - **Shared Layer** (\`lib/\`): Contracts and types shared between client and server
4604
- - **Server Layer** (\`server/\`): Database entities, repositories, business logic, API routes
4605
- - **Client Layer** (\`client/\`): React hooks, components, state management
4606
-
4607
- ## \u{1F91D} Contributing
4608
-
4609
- Contributions are welcome! Please follow the development workflow above.
4610
-
4611
- ## \u{1F4C4} License
4612
-
4613
- MIT
4614
- `;
4615
- writeFileSync12(join20(fnDir, "README.md"), content);
4616
- }
4617
-
4618
- // src/commands/generate/generators/entity.ts
4619
- import { join as join21 } from "path";
4620
- import { writeFileSync as writeFileSync13, existsSync as existsSync23 } from "fs";
4621
- function generateSchema(fnDir, scope, fnName) {
4622
- const schemaFilePath = join21(fnDir, "src/server/entities/schema.ts");
4623
- if (existsSync23(schemaFilePath)) {
4624
- return;
4625
- }
4626
- const packageName = `${scope}/${fnName}`;
4627
- const fnNamePascal = toPascalCase(fnName);
4628
- const schemaVarName = `${toCamelCase(fnName)}Schema`;
4629
- const content = loadTemplate("schema", {
4630
- FN_NAME_PASCAL: fnNamePascal,
4631
- PACKAGE_NAME: packageName,
4632
- SCHEMA_VAR_NAME: schemaVarName
4633
- });
4634
- writeFileSync13(schemaFilePath, content);
4635
- }
4636
- function generateEntity(fnDir, scope, fnName, entityName) {
4637
- const safeScope = toSafeSchemaName(scope);
4638
- const safeFnName = toSafeSchemaName(fnName);
4639
- const schemaName = `${safeScope}_${safeFnName}`;
4640
- const tableName = toKebabCase(entityName);
4641
- const pascalName = toPascalCase(entityName);
4642
- const schemaVarName = `${toCamelCase(fnName)}Schema`;
4643
- const schemaFileName = "schema";
4644
- const content = loadTemplate("entity", {
4645
- PASCAL_NAME: pascalName,
4646
- ENTITY_NAME: entityName,
4647
- TABLE_NAME: tableName,
4648
- SCHEMA_NAME: schemaName,
4649
- FN_NAME: fnName,
4650
- SCHEMA_VAR_NAME: schemaVarName,
4651
- SCHEMA_FILE_NAME: schemaFileName
4652
- });
4653
- writeFileSync13(
4654
- join21(fnDir, `src/server/entities/${toKebabCase(entityName)}.ts`),
4655
- content
4656
- );
4657
- }
4658
- function generateEntitiesIndex(fnDir, entities) {
4659
- const schemaExport = `export * from './schema';`;
4660
- const entityExports = entities.map((entity) => `export * from './${toKebabCase(entity)}';`).join("\n");
4661
- const content = [schemaExport, entityExports].filter(Boolean).join("\n");
4662
- writeFileSync13(join21(fnDir, "src/server/entities/index.ts"), content + "\n");
4663
- }
4664
-
4665
- // src/commands/generate/generators/repository.ts
4666
- import { join as join22 } from "path";
4667
- import { writeFileSync as writeFileSync14 } from "fs";
4668
- function generateRepository(fnDir, entityName) {
4669
- const pascalName = toPascalCase(entityName);
4670
- const repoName = `${entityName}Repository`;
4671
- const content = loadTemplate("repository", {
4672
- PASCAL_NAME: pascalName,
4673
- ENTITY_NAME: entityName,
4674
- REPO_NAME: repoName
4675
- });
4676
- writeFileSync14(
4677
- join22(fnDir, `src/server/repositories/${toKebabCase(entityName)}.repository.ts`),
4678
- content
4679
- );
4680
- }
4681
- function generateRepositoriesIndex(fnDir, entities) {
4682
- const exports = entities.map((entity) => `export * from './${toKebabCase(entity)}.repository';`).join("\n");
4683
- writeFileSync14(join22(fnDir, "src/server/repositories/index.ts"), exports + "\n");
4684
- }
4685
-
4686
- // src/commands/generate/generators/route.ts
4687
- import { join as join23 } from "path";
4688
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync15 } from "fs";
4689
- function generateRoute(fnDir, entityName) {
4690
- const pascalName = toPascalCase(entityName);
4691
- const repoName = `${entityName}Repository`;
4692
- const kebabName = toKebabCase(entityName);
4693
- const content = loadTemplate("route", {
4694
- PASCAL_NAME: pascalName,
4695
- ENTITY_NAME: entityName,
4696
- REPO_NAME: repoName,
4697
- KEBAB_NAME: kebabName
4698
- });
4699
- const routeDir = join23(fnDir, `src/server/routes/${kebabName}`);
4700
- mkdirSync4(routeDir, { recursive: true });
4701
- writeFileSync15(join23(routeDir, "index.ts"), content);
4702
- }
4703
-
4704
- // src/commands/generate/generators/contract.ts
4705
- import { join as join24 } from "path";
4706
- import { writeFileSync as writeFileSync16 } from "fs";
4707
- function generateContract(fnDir, entityName) {
4708
- const pascalName = toPascalCase(entityName);
4709
- const content = loadTemplate("contract", {
4710
- PASCAL_NAME: pascalName,
4711
- ENTITY_NAME: entityName
4712
- });
4713
- writeFileSync16(
4714
- join24(fnDir, `src/lib/contracts/${toKebabCase(entityName)}.ts`),
4715
- content
4716
- );
4717
- }
4718
-
4719
- // src/commands/generate/generators/index-files.ts
4720
- import { join as join25 } from "path";
4721
- import { writeFileSync as writeFileSync17 } from "fs";
4722
- function generateMainIndex(fnDir, fnName) {
4723
- const content = `/**
4724
- * @spfn/${fnName}
4725
- *
4726
- * Common Module (Types, Entities)
4727
- * \uACF5\uD1B5 \uBAA8\uB4C8 (\uD0C0\uC785, \uC5D4\uD2F0\uD2F0)
4728
- *
4729
- * Import structure:
4730
- * - @spfn/${fnName} - Common (types, entities)
4731
- * - @spfn/${fnName}/server - Server-side (routes, repositories, helpers)
4732
- * - @spfn/${fnName}/client - Client-side (hooks, store, components)
4733
- */
4734
-
4735
- // ============================================================================
4736
- // Types
4737
- // ============================================================================
4738
-
4739
- export * from '@/lib/types/index';
4740
-
4741
- // ============================================================================
4742
- // Entities (for type reference)
4743
- // ============================================================================
4744
-
4745
- export * from '@/server/entities/index';
4746
- `;
4747
- writeFileSync17(join25(fnDir, "src/index.ts"), content);
4748
- }
4749
- function generateServerIndex(fnDir) {
4750
- const content = `/**
4751
- * @spfn/[name]/server
4752
- *
4753
- * Server-side Only Module
4754
- * \uC11C\uBC84 \uC804\uC6A9 \uBAA8\uB4C8 (\uC11C\uBC84 \uCEF4\uD3EC\uB10C\uD2B8 + \uBC31\uC5D4\uB4DC)
4755
- *
4756
- * Includes:
4757
- * - Routes
4758
- * - Repositories (DB access)
4759
- * - Helpers
4760
- *
4761
- * @note This module should only be imported in server-side code
4762
- */
4763
-
4764
- // ============================================================================
4765
- // Routes
4766
- // ============================================================================
4767
-
4768
- // TODO: Export routes here
4769
-
4770
- // ============================================================================
4771
- // Repositories (DB access)
4772
- // ============================================================================
4773
-
4774
- export * from '@/server/repositories/index';
4775
-
4776
- // ============================================================================
4777
- // Helpers
4778
- // ============================================================================
4779
-
4780
- // TODO: Export helpers here
4781
- `;
4782
- writeFileSync17(join25(fnDir, "src/server.ts"), content);
4783
- }
4784
- function generateClientIndex(fnDir) {
4785
- const content = `/**
4786
- * @spfn/[name]/client
4787
- *
4788
- * Client Components Only
4789
- * \uD074\uB77C\uC774\uC5B8\uD2B8 \uCEF4\uD3EC\uB10C\uD2B8 \uC804\uC6A9 (\uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC2E4\uD589)
4790
- *
4791
- * Includes:
4792
- * - Hooks
4793
- * - Store (Zustand)
4794
- * - Components
4795
- */
4796
-
4797
- // ============================================================================
4798
- // Client-side Hooks
4799
- // ============================================================================
4800
-
4801
- export * from './client/hooks';
4802
-
4803
- // ============================================================================
4804
- // Client-side Store
4805
- // ============================================================================
4806
-
4807
- export * from './client/store';
4808
-
4809
- // ============================================================================
4810
- // Client-side Components
4811
- // ============================================================================
4812
-
4813
- export * from './client/components';
4814
- `;
4815
- writeFileSync17(join25(fnDir, "src/client.ts"), content);
4816
- }
4817
- function generateTypesFile(fnDir, fnName) {
4818
- const content = `/**
4819
- * @spfn/${fnName} Types
4820
- *
4821
- * @deprecated This file is kept for backwards compatibility.
4822
- * Types are now in src/lib/types/index.ts
4823
- */
4824
-
4825
- export * from '@/lib/types/index';
4826
- `;
4827
- writeFileSync17(join25(fnDir, "src/types.ts"), content);
4828
- }
4829
-
4830
- // src/commands/generate/generators/structure.ts
4831
- async function generateFunctionStructure(options) {
4832
- const { fnDir, scope, fnName, description, enableRoutes } = options;
4833
- const entities = options.entities.filter((e) => e && e.trim().length > 0);
4834
- const dirs = [
4835
- // Shared layer
4836
- "src/lib/contracts",
4837
- "src/lib/types",
4838
- // Server layer
4839
- "src/server/entities",
4840
- "src/server/routes",
4841
- "src/server/repositories",
4842
- "src/server/helpers",
4843
- "src/server/generators",
4844
- // Client layer
4845
- "src/client/hooks",
4846
- "src/client/store",
4847
- "src/client/components"
4848
- ];
4849
- dirs.forEach((dir) => mkdirSync5(join26(fnDir, dir), { recursive: true }));
4850
- generatePackageJson(fnDir, scope, fnName, description);
4851
- generateTsConfig(fnDir);
4852
- generateTsupConfig(fnDir);
4853
- generateDrizzleConfig(fnDir, scope, fnName);
4854
- generateExampleGenerator(fnDir, scope, fnName);
4855
- generateReadme(fnDir, scope, fnName, description);
4856
- if (entities.length > 0) {
4857
- generateSchema(fnDir, scope, fnName);
4858
- for (const entity of entities) {
4859
- generateEntity(fnDir, scope, fnName, entity);
4860
- generateRepository(fnDir, entity);
4861
- if (enableRoutes) {
4862
- generateRoute(fnDir, entity);
4863
- generateContract(fnDir, entity);
4864
- }
4865
- }
4866
- generateEntitiesIndex(fnDir, entities);
4867
- generateRepositoriesIndex(fnDir, entities);
4868
- } else {
4869
- writeFileSync18(join26(fnDir, "src/server/entities/index.ts"), "// Export your entities here\nexport {}\n");
4870
- writeFileSync18(join26(fnDir, "src/server/repositories/index.ts"), "// Export your repositories here\nexport {}\n");
4871
- }
4872
- writeFileSync18(join26(fnDir, "src/client/hooks/index.ts"), "/**\n * Client Hooks\n */\n\n// TODO: Add hooks (e.g., useAuth, useData, etc.)\nexport {}\n");
4873
- writeFileSync18(join26(fnDir, "src/client/store/index.ts"), "/**\n * Client Store\n */\n\n// TODO: Add Zustand store if needed\nexport {}\n");
4874
- writeFileSync18(join26(fnDir, "src/client/components/index.ts"), "/**\n * Client Components\n */\n\n// TODO: Add React components\nexport {}\n");
4875
- writeFileSync18(join26(fnDir, "src/client/index.ts"), "/**\n * Client Module Entry\n */\n\nexport * from './hooks';\nexport * from './store';\nexport * from './components';\n");
4876
- writeFileSync18(join26(fnDir, "src/lib/types/index.ts"), "/**\n * Shared Type Definitions\n */\n\n// Add your shared types here\nexport {}\n");
4877
- writeFileSync18(join26(fnDir, "src/lib/contracts/index.ts"), "/**\n * API Contracts\n */\n\n// Export your contracts here\nexport {}\n");
4878
- generateMainIndex(fnDir, fnName);
4879
- generateServerIndex(fnDir);
4880
- generateClientIndex(fnDir);
4881
- generateTypesFile(fnDir, fnName);
4882
- }
4883
-
4884
- // src/commands/generate.ts
4885
- async function generateFunction(name, options) {
4886
- const cwd = process.cwd();
4887
- let scope = options.scope;
4888
- if (!scope && !options.yes) {
4889
- scope = await promptScope();
4890
- } else if (!scope) {
4891
- scope = "@spfn";
4892
- }
4893
- let fnName = name;
4894
- if (!fnName && !options.yes) {
4895
- fnName = await promptFunctionName();
4896
- }
4897
- if (!fnName) {
4898
- logger.error("Function name is required");
4899
- process.exit(1);
4900
- }
4901
- const fnDir = join27(cwd, fnName);
4902
- if (existsSync24(fnDir)) {
4903
- logger.error(`Directory ${fnName} already exists at ${fnDir}`);
4904
- process.exit(1);
4905
- }
4906
- let description = options.description;
4907
- if (!description && !options.yes) {
4908
- description = await promptDescription(fnName);
4909
- } else if (!description) {
4910
- description = `SPFN ${fnName} function`;
4911
- }
4912
- let entities = [];
4913
- if (options.entities) {
4914
- entities = options.entities.split(",").map((e) => e.trim()).filter(Boolean);
4915
- } else if (!options.yes) {
4916
- entities = await promptEntities();
4917
- }
4918
- const enableCache = !options.skipCache;
4919
- const enableRoutes = !options.skipRoutes;
4920
- const config = {
4921
- scope,
4922
- fnName,
4923
- description,
4924
- entities,
4925
- enableCache,
4926
- enableRoutes
4927
- };
4928
- if (!options.yes) {
4929
- const confirmed = await confirmConfiguration(config);
4930
- if (!confirmed) {
4931
- process.exit(0);
4932
- }
4933
- }
4934
- const spinner = ora12("Generating function structure...").start();
4935
- try {
4936
- await generateFunctionStructure({
4937
- fnDir,
4938
- scope,
4939
- fnName,
4940
- description,
4941
- entities,
4942
- enableCache,
4943
- enableRoutes
4944
- });
4945
- spinner.succeed("Function structure generated");
4946
- console.log("");
4947
- logger.success(`\u2728 Package ${chalk27.cyan(`${scope}/${fnName}`)} created successfully!
4948
- `);
4949
- logger.info(chalk27.bold("\u{1F4DA} Next steps:"));
4950
- console.log(` ${chalk27.gray("1.")} cd ${fnName}`);
4951
- console.log(` ${chalk27.gray("2.")} pnpm install ${chalk27.dim("(in monorepo root)")}`);
4952
- console.log(` ${chalk27.gray("3.")} pnpm build`);
4953
- console.log(` ${chalk27.gray("4.")} ${chalk27.dim("Use the package in your app")}`);
4954
- console.log("");
4955
- } catch (error) {
4956
- spinner.fail("Failed to generate function");
4957
- logger.error(String(error));
4958
- process.exit(1);
4959
- }
4960
- }
4961
- var generateCommand = new Command11("generate").alias("g").description("Generate SPFN resources");
4962
- generateCommand.command("fn").description("Generate a new SPFN function module").argument("[name]", "Function name").option("-s, --scope <scope>", "NPM scope (e.g., @spfn, @mycompany)").option("-d, --description <text>", "Function description").option("-e, --entities <list>", "Comma-separated entity names").option("--skip-cache", "Skip cache generation").option("--skip-routes", "Skip route generation").option("-y, --yes", "Skip all prompts").action(generateFunction);
4963
-
4964
- // src/commands/env.ts
4965
- import { Command as Command12 } from "commander";
4966
- import chalk28 from "chalk";
4967
- import { existsSync as existsSync25, readFileSync as readFileSync9, writeFileSync as writeFileSync19 } from "fs";
3734
+ import { existsSync as existsSync22, readFileSync as readFileSync9, writeFileSync as writeFileSync13 } from "fs";
4968
3735
  import { resolve as resolve2 } from "path";
4969
3736
  import { parse } from "dotenv";
4970
3737
  var VALID_ENVS = ["local", "development", "staging", "production", "test"];
4971
3738
  var BASE_ENV_FILES = {
4972
3739
  nextjs: [".env", ".env.local"],
4973
- server: [".env.server", ".env.server.local"]
3740
+ server: [".env.server"]
4974
3741
  };
4975
3742
  function getEnvFilesForEnvironment(nodeEnv) {
4976
3743
  const files = [".env"];
@@ -4984,7 +3751,6 @@ function getEnvFilesForEnvironment(nodeEnv) {
4984
3751
  files.push(`.env.${nodeEnv}.local`);
4985
3752
  }
4986
3753
  files.push(".env.server");
4987
- files.push(".env.server.local");
4988
3754
  return files;
4989
3755
  }
4990
3756
  function getTargetFile(schema) {
@@ -4992,7 +3758,7 @@ function getTargetFile(schema) {
4992
3758
  if (isNextjs) {
4993
3759
  return schema.sensitive ? ".env.local" : ".env";
4994
3760
  }
4995
- return schema.sensitive ? ".env.server.local" : ".env.server";
3761
+ return ".env.server";
4996
3762
  }
4997
3763
  async function loadEnvSchema(packageName) {
4998
3764
  try {
@@ -5012,26 +3778,26 @@ async function loadEnvSchema(packageName) {
5012
3778
  }
5013
3779
  function formatType(type) {
5014
3780
  const typeColors = {
5015
- string: chalk28.green,
5016
- number: chalk28.blue,
5017
- boolean: chalk28.yellow,
5018
- url: chalk28.cyan,
5019
- enum: chalk28.magenta,
5020
- json: chalk28.red
3781
+ string: chalk27.green,
3782
+ number: chalk27.blue,
3783
+ boolean: chalk27.yellow,
3784
+ url: chalk27.cyan,
3785
+ enum: chalk27.magenta,
3786
+ json: chalk27.red
5021
3787
  };
5022
- return (typeColors[type] || chalk28.white)(type);
3788
+ return (typeColors[type] || chalk27.white)(type);
5023
3789
  }
5024
3790
  function formatDefault(value, type) {
5025
3791
  if (value === void 0) {
5026
- return chalk28.dim("(none)");
3792
+ return chalk27.dim("(none)");
5027
3793
  }
5028
3794
  if (type === "string" || type === "url") {
5029
- return chalk28.green(`"${value}"`);
3795
+ return chalk27.green(`"${value}"`);
5030
3796
  }
5031
3797
  if (type === "boolean") {
5032
- return value ? chalk28.green("true") : chalk28.red("false");
3798
+ return value ? chalk27.green("true") : chalk27.red("false");
5033
3799
  }
5034
- return chalk28.cyan(String(value));
3800
+ return chalk27.cyan(String(value));
5035
3801
  }
5036
3802
  async function listEnvVars(options) {
5037
3803
  const packageName = options.package || "@spfn/core";
@@ -5045,28 +3811,28 @@ async function listEnvVars(options) {
5045
3811
  acc[target].push([key, schema]);
5046
3812
  return acc;
5047
3813
  }, {});
5048
- console.log(chalk28.blue.bold(`
3814
+ console.log(chalk27.blue.bold(`
5049
3815
  \u{1F4CB} Environment Variables by File (${packageName})
5050
3816
  `));
5051
3817
  for (const [file, vars] of Object.entries(grouped)) {
5052
- console.log(chalk28.bold.magenta(`
3818
+ console.log(chalk27.bold.magenta(`
5053
3819
  ${file}`));
5054
- console.log(chalk28.dim("\u2500".repeat(50)));
3820
+ console.log(chalk27.dim("\u2500".repeat(50)));
5055
3821
  for (const [key, schema] of vars) {
5056
3822
  printEnvVar(key, schema);
5057
3823
  }
5058
3824
  }
5059
3825
  } else {
5060
- console.log(chalk28.blue.bold(`
3826
+ console.log(chalk27.blue.bold(`
5061
3827
  \u{1F4CB} Environment Variables (${packageName})
5062
3828
  `));
5063
3829
  for (const [key, schema] of allVars) {
5064
3830
  printEnvVar(key, schema, true);
5065
3831
  }
5066
3832
  }
5067
- console.log(chalk28.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
3833
+ console.log(chalk27.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
5068
3834
  } catch (error) {
5069
- console.error(chalk28.red(`
3835
+ console.error(chalk27.red(`
5070
3836
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
5071
3837
  `));
5072
3838
  process.exit(1);
@@ -5074,17 +3840,17 @@ ${file}`));
5074
3840
  }
5075
3841
  function printEnvVar(key, schema, showFile = false) {
5076
3842
  const typeStr = formatType(schema.type);
5077
- const requiredStr = schema.required || schema.default !== void 0 ? chalk28.red("[required]") : chalk28.dim("[optional]");
5078
- const sensitiveStr = schema.sensitive ? chalk28.yellow(" [sensitive]") : "";
5079
- const fileStr = showFile ? chalk28.dim(` \u2192 ${getTargetFile(schema)}`) : "";
5080
- console.log(`${chalk28.bold.cyan(key)} ${chalk28.dim("(")}${typeStr}${chalk28.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
5081
- console.log(` ${chalk28.dim(schema.description)}`);
3843
+ const requiredStr = schema.required || schema.default !== void 0 ? chalk27.red("[required]") : chalk27.dim("[optional]");
3844
+ const sensitiveStr = schema.sensitive ? chalk27.yellow(" [sensitive]") : "";
3845
+ const fileStr = showFile ? chalk27.dim(` \u2192 ${getTargetFile(schema)}`) : "";
3846
+ console.log(`${chalk27.bold.cyan(key)} ${chalk27.dim("(")}${typeStr}${chalk27.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
3847
+ console.log(` ${chalk27.dim(schema.description)}`);
5082
3848
  if (schema.default !== void 0) {
5083
- console.log(` ${chalk28.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
3849
+ console.log(` ${chalk27.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
5084
3850
  }
5085
3851
  if (schema.examples && schema.examples.length > 0) {
5086
3852
  const exampleStr = schema.examples.map((ex) => formatDefault(ex, schema.type)).join(", ");
5087
- console.log(` ${chalk28.dim("Examples:")} ${exampleStr}`);
3853
+ console.log(` ${chalk27.dim("Examples:")} ${exampleStr}`);
5088
3854
  }
5089
3855
  console.log();
5090
3856
  }
@@ -5092,7 +3858,7 @@ async function showEnvStats(options) {
5092
3858
  const packageName = options.package || "@spfn/core";
5093
3859
  try {
5094
3860
  const envSchema = await loadEnvSchema(packageName);
5095
- console.log(chalk28.blue.bold(`
3861
+ console.log(chalk27.blue.bold(`
5096
3862
  \u{1F4CA} Environment Variable Statistics (${packageName})
5097
3863
  `));
5098
3864
  const allVars = Object.entries(envSchema);
@@ -5114,24 +3880,24 @@ async function showEnvStats(options) {
5114
3880
  acc[file] = (acc[file] || 0) + 1;
5115
3881
  return acc;
5116
3882
  }, {});
5117
- console.log(`${chalk28.bold("Total variables:")} ${chalk28.cyan(allVars.length)}`);
5118
- console.log(`${chalk28.bold("Required:")} ${chalk28.red(required.length)}`);
5119
- console.log(`${chalk28.bold("Optional:")} ${chalk28.dim(optional.length)}`);
5120
- console.log(`${chalk28.bold("Sensitive:")} ${chalk28.yellow(sensitive.length)}`);
5121
- console.log(chalk28.bold("\nBy Target:"));
5122
- console.log(` ${chalk28.blue("Next.js accessible:")} ${chalk28.cyan(nextjsVars.length)}`);
5123
- console.log(` ${chalk28.magenta("SPFN server only:")} ${chalk28.cyan(serverOnlyVars.length)}`);
5124
- console.log(chalk28.bold("\nBy File:"));
3883
+ console.log(`${chalk27.bold("Total variables:")} ${chalk27.cyan(allVars.length)}`);
3884
+ console.log(`${chalk27.bold("Required:")} ${chalk27.red(required.length)}`);
3885
+ console.log(`${chalk27.bold("Optional:")} ${chalk27.dim(optional.length)}`);
3886
+ console.log(`${chalk27.bold("Sensitive:")} ${chalk27.yellow(sensitive.length)}`);
3887
+ console.log(chalk27.bold("\nBy Target:"));
3888
+ console.log(` ${chalk27.blue("Next.js accessible:")} ${chalk27.cyan(nextjsVars.length)}`);
3889
+ console.log(` ${chalk27.magenta("SPFN server only:")} ${chalk27.cyan(serverOnlyVars.length)}`);
3890
+ console.log(chalk27.bold("\nBy File:"));
5125
3891
  for (const [file, count] of Object.entries(fileCount)) {
5126
- console.log(` ${chalk28.dim(file)}: ${chalk28.cyan(count)}`);
3892
+ console.log(` ${chalk27.dim(file)}: ${chalk27.cyan(count)}`);
5127
3893
  }
5128
- console.log(chalk28.bold("\nBy Type:"));
3894
+ console.log(chalk27.bold("\nBy Type:"));
5129
3895
  for (const [type, count] of Object.entries(typeCount)) {
5130
- console.log(` ${formatType(type)}: ${chalk28.cyan(count)}`);
3896
+ console.log(` ${formatType(type)}: ${chalk27.cyan(count)}`);
5131
3897
  }
5132
3898
  console.log();
5133
3899
  } catch (error) {
5134
- console.error(chalk28.red(`
3900
+ console.error(chalk27.red(`
5135
3901
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
5136
3902
  `));
5137
3903
  process.exit(1);
@@ -5151,40 +3917,40 @@ async function searchEnvVars(query, options) {
5151
3917
  }
5152
3918
  }
5153
3919
  if (results.length === 0) {
5154
- console.log(chalk28.yellow(`
3920
+ console.log(chalk27.yellow(`
5155
3921
  \u26A0\uFE0F No environment variables found matching "${query}"
5156
3922
  `));
5157
3923
  return;
5158
3924
  }
5159
- console.log(chalk28.blue.bold(`
3925
+ console.log(chalk27.blue.bold(`
5160
3926
  \u{1F50D} Found ${results.length} environment variable(s) matching "${query}"
5161
3927
  `));
5162
3928
  for (const [key, schema] of results) {
5163
3929
  const typeStr = formatType(schema.type);
5164
- const requiredStr = schema.required || schema.default !== void 0 ? chalk28.red("[required]") : chalk28.dim("[optional]");
5165
- console.log(`${chalk28.bold.cyan(key)} ${chalk28.dim("(")}${typeStr}${chalk28.dim(")")} ${requiredStr}`);
5166
- console.log(` ${chalk28.dim(schema.description)}`);
3930
+ const requiredStr = schema.required || schema.default !== void 0 ? chalk27.red("[required]") : chalk27.dim("[optional]");
3931
+ console.log(`${chalk27.bold.cyan(key)} ${chalk27.dim("(")}${typeStr}${chalk27.dim(")")} ${requiredStr}`);
3932
+ console.log(` ${chalk27.dim(schema.description)}`);
5167
3933
  if (schema.default !== void 0) {
5168
- console.log(` ${chalk28.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
3934
+ console.log(` ${chalk27.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
5169
3935
  }
5170
3936
  console.log();
5171
3937
  }
5172
3938
  } catch (error) {
5173
- console.error(chalk28.red(`
3939
+ console.error(chalk27.red(`
5174
3940
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
5175
3941
  `));
5176
3942
  process.exit(1);
5177
3943
  }
5178
3944
  }
5179
- var envCommand = new Command12("env").description("Manage environment variables");
3945
+ var envCommand = new Command11("env").description("Manage environment variables");
5180
3946
  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);
5181
3947
  envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(showEnvStats);
5182
3948
  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);
5183
3949
  function validateEnvOption(envValue) {
5184
3950
  if (!VALID_ENVS.includes(envValue)) {
5185
- console.error(chalk28.red(`
3951
+ console.error(chalk27.red(`
5186
3952
  \u274C Invalid environment: "${envValue}"`));
5187
- console.log(chalk28.dim(` Valid values: ${VALID_ENVS.join(", ")}
3953
+ console.log(chalk27.dim(` Valid values: ${VALID_ENVS.join(", ")}
5188
3954
  `));
5189
3955
  process.exit(1);
5190
3956
  }
@@ -5205,8 +3971,8 @@ async function initEnvFiles(options) {
5205
3971
  return acc;
5206
3972
  }, {});
5207
3973
  if (targetEnv) {
5208
- console.log(chalk28.blue.bold(`
5209
- \u{1F680} Generating .env template files for ${chalk28.cyan(targetEnv)} environment
3974
+ console.log(chalk27.blue.bold(`
3975
+ \u{1F680} Generating .env template files for ${chalk27.cyan(targetEnv)} environment
5210
3976
  `));
5211
3977
  const envSpecificFiles = {};
5212
3978
  const committedVars = allVars.filter(([_, schema]) => !schema.sensitive);
@@ -5222,25 +3988,24 @@ async function initEnvFiles(options) {
5222
3988
  writeEnvTemplate(cwd, file, vars, options.force ?? false);
5223
3989
  }
5224
3990
  } else {
5225
- console.log(chalk28.blue.bold(`
3991
+ console.log(chalk27.blue.bold(`
5226
3992
  \u{1F680} Generating .env template files
5227
3993
  `));
5228
3994
  for (const [file, vars] of Object.entries(grouped)) {
5229
3995
  writeEnvTemplate(cwd, file, vars, options.force ?? false);
5230
3996
  }
5231
3997
  }
5232
- console.log(chalk28.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
5233
- console.log(chalk28.dim(" cp .env.example .env"));
5234
- console.log(chalk28.dim(" cp .env.local.example .env.local"));
5235
- console.log(chalk28.dim(" cp .env.server.example .env.server"));
5236
- console.log(chalk28.dim(" cp .env.server.local.example .env.server.local"));
3998
+ console.log(chalk27.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
3999
+ console.log(chalk27.dim(" cp .env.example .env"));
4000
+ console.log(chalk27.dim(" cp .env.local.example .env.local"));
4001
+ console.log(chalk27.dim(" cp .env.server.example .env.server"));
5237
4002
  if (targetEnv) {
5238
- console.log(chalk28.dim(` cp .env.${targetEnv}.example .env.${targetEnv}`));
5239
- console.log(chalk28.dim(` cp .env.${targetEnv}.local.example .env.${targetEnv}.local`));
4003
+ console.log(chalk27.dim(` cp .env.${targetEnv}.example .env.${targetEnv}`));
4004
+ console.log(chalk27.dim(` cp .env.${targetEnv}.local.example .env.${targetEnv}.local`));
5240
4005
  }
5241
4006
  console.log("");
5242
4007
  } catch (error) {
5243
- console.error(chalk28.red(`
4008
+ console.error(chalk27.red(`
5244
4009
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
5245
4010
  `));
5246
4011
  process.exit(1);
@@ -5248,12 +4013,12 @@ async function initEnvFiles(options) {
5248
4013
  }
5249
4014
  function writeEnvTemplate(cwd, file, vars, force) {
5250
4015
  const filePath = resolve2(cwd, file);
5251
- if (existsSync25(filePath) && !force) {
5252
- console.log(chalk28.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
4016
+ if (existsSync22(filePath) && !force) {
4017
+ console.log(chalk27.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
5253
4018
  return;
5254
4019
  }
5255
- writeFileSync19(filePath, generateEnvFileContent(vars), "utf-8");
5256
- console.log(chalk28.green(` \u2705 ${file} (${vars.length} variables)`));
4020
+ writeFileSync13(filePath, generateEnvFileContent(vars), "utf-8");
4021
+ console.log(chalk27.green(` \u2705 ${file} (${vars.length} variables)`));
5257
4022
  }
5258
4023
  function generateEnvFileContent(vars) {
5259
4024
  const lines = [
@@ -5288,7 +4053,7 @@ async function checkEnvFiles(options) {
5288
4053
  const envSchema = await loadEnvSchema(packageName);
5289
4054
  const allVars = Object.entries(envSchema);
5290
4055
  const envLabel = targetEnv ? ` (${targetEnv})` : "";
5291
- console.log(chalk28.blue.bold(`
4056
+ console.log(chalk27.blue.bold(`
5292
4057
  \u{1F50D} Checking .env files against schema${envLabel}
5293
4058
  `));
5294
4059
  const filesToCheck = targetEnv ? getEnvFilesForEnvironment(targetEnv) : [...BASE_ENV_FILES.nextjs, ...BASE_ENV_FILES.server];
@@ -5297,7 +4062,7 @@ async function checkEnvFiles(options) {
5297
4062
  const warnings = [];
5298
4063
  for (const file of filesToCheck) {
5299
4064
  const filePath = resolve2(cwd, file);
5300
- if (!existsSync25(filePath)) {
4065
+ if (!existsSync22(filePath)) {
5301
4066
  continue;
5302
4067
  }
5303
4068
  const content = readFileSync9(filePath, "utf-8");
@@ -5305,7 +4070,7 @@ async function checkEnvFiles(options) {
5305
4070
  for (const [key, value] of Object.entries(parsed)) {
5306
4071
  loadedEnv[key] = { value: value || "", file };
5307
4072
  }
5308
- console.log(chalk28.dim(` \u{1F4C4} ${file} loaded`));
4073
+ console.log(chalk27.dim(` \u{1F4C4} ${file} loaded`));
5309
4074
  }
5310
4075
  console.log("");
5311
4076
  for (const [key, schema] of allVars) {
@@ -5313,7 +4078,7 @@ async function checkEnvFiles(options) {
5313
4078
  const found = loadedEnv[key];
5314
4079
  if (!found) {
5315
4080
  if (schema.required && schema.default === void 0) {
5316
- issues.push(`${chalk28.red("\u2717")} ${chalk28.cyan(key)} is required but not found in any .env file`);
4081
+ issues.push(`${chalk27.red("\u2717")} ${chalk27.cyan(key)} is required but not found in any .env file`);
5317
4082
  }
5318
4083
  continue;
5319
4084
  }
@@ -5323,11 +4088,11 @@ async function checkEnvFiles(options) {
5323
4088
  if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
5324
4089
  if (schema.sensitive) {
5325
4090
  issues.push(
5326
- `${chalk28.red("\u2717")} ${chalk28.cyan(key)} is sensitive and should be in ${chalk28.magenta(expectedFile)}, but found in ${chalk28.yellow(found.file)} (security risk!)`
4091
+ `${chalk27.red("\u2717")} ${chalk27.cyan(key)} is sensitive and should be in ${chalk27.magenta(expectedFile)}, but found in ${chalk27.yellow(found.file)} (security risk!)`
5327
4092
  );
5328
4093
  } else {
5329
4094
  warnings.push(
5330
- `${chalk28.yellow("\u26A0")} ${chalk28.cyan(key)} should be in ${chalk28.magenta(expectedFile)}, but found in ${chalk28.dim(found.file)}`
4095
+ `${chalk27.yellow("\u26A0")} ${chalk27.cyan(key)} should be in ${chalk27.magenta(expectedFile)}, but found in ${chalk27.dim(found.file)}`
5331
4096
  );
5332
4097
  }
5333
4098
  }
@@ -5335,34 +4100,34 @@ async function checkEnvFiles(options) {
5335
4100
  for (const [key, { file }] of Object.entries(loadedEnv)) {
5336
4101
  const inSchema = allVars.some(([k]) => k === key);
5337
4102
  if (!inSchema) {
5338
- warnings.push(`${chalk28.yellow("\u26A0")} ${chalk28.cyan(key)} in ${chalk28.dim(file)} is not in schema`);
4103
+ warnings.push(`${chalk27.yellow("\u26A0")} ${chalk27.cyan(key)} in ${chalk27.dim(file)} is not in schema`);
5339
4104
  }
5340
4105
  }
5341
4106
  if (issues.length > 0) {
5342
- console.log(chalk28.red.bold("Issues:"));
4107
+ console.log(chalk27.red.bold("Issues:"));
5343
4108
  for (const issue of issues) {
5344
4109
  console.log(` ${issue}`);
5345
4110
  }
5346
4111
  console.log("");
5347
4112
  }
5348
4113
  if (warnings.length > 0) {
5349
- console.log(chalk28.yellow.bold("Warnings:"));
4114
+ console.log(chalk27.yellow.bold("Warnings:"));
5350
4115
  for (const warning of warnings) {
5351
4116
  console.log(` ${warning}`);
5352
4117
  }
5353
4118
  console.log("");
5354
4119
  }
5355
4120
  if (issues.length === 0 && warnings.length === 0) {
5356
- console.log(chalk28.green("\u2705 All environment variables are correctly configured!\n"));
4121
+ console.log(chalk27.green("\u2705 All environment variables are correctly configured!\n"));
5357
4122
  } else {
5358
- console.log(chalk28.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
4123
+ console.log(chalk27.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
5359
4124
  `));
5360
4125
  if (issues.length > 0) {
5361
4126
  process.exit(1);
5362
4127
  }
5363
4128
  }
5364
4129
  } catch (error) {
5365
- console.error(chalk28.red(`
4130
+ console.error(chalk27.red(`
5366
4131
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
5367
4132
  `));
5368
4133
  process.exit(1);
@@ -5374,17 +4139,17 @@ async function validateEnvVars(options) {
5374
4139
  const packages = options.packages || ["@spfn/core"];
5375
4140
  const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
5376
4141
  if (targetEnv) {
5377
- const { loadEnv: loadEnv8 } = await import("@spfn/core/env/loader");
5378
- const result = loadEnv8({ nodeEnv: targetEnv });
5379
- console.log(chalk28.blue.bold(`
5380
- \u{1F50D} Validating environment variables for ${chalk28.cyan(targetEnv)}
4142
+ const { loadEnv: loadEnv9 } = await import("@spfn/core/env/loader");
4143
+ const result = loadEnv9({ nodeEnv: targetEnv });
4144
+ console.log(chalk27.blue.bold(`
4145
+ \u{1F50D} Validating environment variables for ${chalk27.cyan(targetEnv)}
5381
4146
  `));
5382
4147
  if (result.loadedFiles.length > 0) {
5383
- console.log(chalk28.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
4148
+ console.log(chalk27.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
5384
4149
  }
5385
4150
  console.log("");
5386
4151
  } else {
5387
- console.log(chalk28.blue.bold(`
4152
+ console.log(chalk27.blue.bold(`
5388
4153
  \u{1F50D} Validating environment variables
5389
4154
  `));
5390
4155
  }
@@ -5392,7 +4157,7 @@ async function validateEnvVars(options) {
5392
4157
  const allWarnings = [];
5393
4158
  for (const packageName of packages) {
5394
4159
  try {
5395
- console.log(chalk28.dim(` \u{1F4E6} ${packageName}`));
4160
+ console.log(chalk27.dim(` \u{1F4E6} ${packageName}`));
5396
4161
  const envSchema = await loadEnvSchema(packageName);
5397
4162
  const { createEnvRegistry } = await import("@spfn/core/env");
5398
4163
  const registry = createEnvRegistry(envSchema);
@@ -5405,10 +4170,10 @@ async function validateEnvVars(options) {
5405
4170
  }
5406
4171
  } catch (error) {
5407
4172
  if (error instanceof Error && error.message.includes("does not export envSchema")) {
5408
- console.log(chalk28.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
4173
+ console.log(chalk27.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
5409
4174
  continue;
5410
4175
  }
5411
- console.error(chalk28.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
4176
+ console.error(chalk27.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
5412
4177
  if (options.strict) {
5413
4178
  process.exit(1);
5414
4179
  }
@@ -5416,32 +4181,32 @@ async function validateEnvVars(options) {
5416
4181
  }
5417
4182
  console.log("");
5418
4183
  if (allErrors.length > 0) {
5419
- console.log(chalk28.red.bold(`\u274C Validation Errors (${allErrors.length}):
4184
+ console.log(chalk27.red.bold(`\u274C Validation Errors (${allErrors.length}):
5420
4185
  `));
5421
4186
  for (const error of allErrors) {
5422
- console.log(` ${chalk28.red("\u2717")} ${chalk28.cyan(error.key)}`);
5423
- console.log(` ${chalk28.dim(error.message)}`);
5424
- console.log(` ${chalk28.dim(`from ${error.package}`)}`);
4187
+ console.log(` ${chalk27.red("\u2717")} ${chalk27.cyan(error.key)}`);
4188
+ console.log(` ${chalk27.dim(error.message)}`);
4189
+ console.log(` ${chalk27.dim(`from ${error.package}`)}`);
5425
4190
  console.log("");
5426
4191
  }
5427
4192
  }
5428
4193
  if (allWarnings.length > 0) {
5429
- console.log(chalk28.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
4194
+ console.log(chalk27.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
5430
4195
  `));
5431
4196
  for (const warning of allWarnings) {
5432
- console.log(` ${chalk28.yellow("\u26A0")} ${chalk28.cyan(warning.key)}`);
5433
- console.log(` ${chalk28.dim(warning.message)}`);
4197
+ console.log(` ${chalk27.yellow("\u26A0")} ${chalk27.cyan(warning.key)}`);
4198
+ console.log(` ${chalk27.dim(warning.message)}`);
5434
4199
  console.log("");
5435
4200
  }
5436
4201
  }
5437
4202
  if (allErrors.length === 0 && allWarnings.length === 0) {
5438
- console.log(chalk28.green.bold("\u2705 All environment variables are valid!\n"));
4203
+ console.log(chalk27.green.bold("\u2705 All environment variables are valid!\n"));
5439
4204
  } else if (allErrors.length === 0) {
5440
- console.log(chalk28.green("\u2705 No errors found."));
5441
- console.log(chalk28.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
4205
+ console.log(chalk27.green("\u2705 No errors found."));
4206
+ console.log(chalk27.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
5442
4207
  `));
5443
4208
  } else {
5444
- console.log(chalk28.red(`
4209
+ console.log(chalk27.red(`
5445
4210
  \u274C Validation failed with ${allErrors.length} error(s)
5446
4211
  `));
5447
4212
  process.exit(1);
@@ -5451,12 +4216,11 @@ envCommand.command("validate").description("Validate environment variables again
5451
4216
 
5452
4217
  // src/index.ts
5453
4218
  init_version();
5454
- var program = new Command13();
4219
+ var program = new Command12();
5455
4220
  program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(getCliVersion());
5456
4221
  program.addCommand(createCommand);
5457
4222
  program.addCommand(initCommand);
5458
4223
  program.addCommand(addCommand);
5459
- program.addCommand(generateCommand);
5460
4224
  program.addCommand(devCommand);
5461
4225
  program.addCommand(buildCommand);
5462
4226
  program.addCommand(startCommand);