spfn 0.2.0-beta.46 → 0.2.0-beta.48

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.46";
792
+ return "0.2.0-beta.48";
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) {
@@ -2019,7 +2060,7 @@ async function initCodegen(options) {
2019
2060
  ]
2020
2061
  }
2021
2062
  };
2022
- writeFileSync8(rcPath, JSON.stringify(config, null, 2) + "\n");
2063
+ writeFileSync9(rcPath, JSON.stringify(config, null, 2) + "\n");
2023
2064
  console.log("\n" + chalk7.green.bold("\u2713 Created .spfnrc.ts\n"));
2024
2065
  console.log("Configuration:");
2025
2066
  console.log(chalk7.gray(JSON.stringify(config, null, 2)));
@@ -2234,7 +2275,7 @@ import { Command as Command9 } from "commander";
2234
2275
  import chalk10 from "chalk";
2235
2276
 
2236
2277
  // src/commands/db/utils/drizzle.ts
2237
- import { existsSync as existsSync15, writeFileSync as writeFileSync9, unlinkSync as unlinkSync2 } from "fs";
2278
+ import { existsSync as existsSync15, writeFileSync as writeFileSync10, unlinkSync as unlinkSync2 } from "fs";
2238
2279
  import { spawn } from "child_process";
2239
2280
  import { pathToFileURL } from "url";
2240
2281
  import chalk9 from "chalk";
@@ -2269,7 +2310,7 @@ async function runDrizzleCommand(command) {
2269
2310
  expandGlobs: true,
2270
2311
  autoDetectSchemas: true
2271
2312
  });
2272
- writeFileSync9(tempConfigPath, configContent);
2313
+ writeFileSync10(tempConfigPath, configContent);
2273
2314
  console.log(chalk9.dim("Using auto-generated Drizzle config\n"));
2274
2315
  }
2275
2316
  const args = command.split(" ");
@@ -2590,7 +2631,7 @@ async function applyFunctionMigrations() {
2590
2631
  }
2591
2632
 
2592
2633
  // src/commands/db/migrate.ts
2593
- import chalk17 from "chalk";
2634
+ import chalk18 from "chalk";
2594
2635
  import { join as join16 } from "path";
2595
2636
  import { existsSync as existsSync18 } from "fs";
2596
2637
 
@@ -2598,11 +2639,13 @@ import { existsSync as existsSync18 } from "fs";
2598
2639
  import { promises as fs3 } from "fs";
2599
2640
  import path3 from "path";
2600
2641
  import { spawn as spawn2 } from "child_process";
2601
- import chalk16 from "chalk";
2642
+ import chalk17 from "chalk";
2602
2643
  import ora7 from "ora";
2603
2644
 
2604
2645
  // src/commands/db/utils/database.ts
2605
2646
  import net from "net";
2647
+ import chalk14 from "chalk";
2648
+ import prompts4 from "prompts";
2606
2649
  function parseDatabaseUrl(dbUrl) {
2607
2650
  try {
2608
2651
  const url = new URL(dbUrl);
@@ -2618,6 +2661,24 @@ function parseDatabaseUrl(dbUrl) {
2618
2661
  throw new Error(`Invalid DATABASE_URL format: ${error instanceof Error ? error.message : "Unknown error"}`);
2619
2662
  }
2620
2663
  }
2664
+ var LOCAL_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "::1"]);
2665
+ function isRemoteHost(host) {
2666
+ return !LOCAL_HOSTS.has(host);
2667
+ }
2668
+ async function confirmDangerousTarget(dbInfo) {
2669
+ if (!isRemoteHost(dbInfo.host) && process.env.NODE_ENV !== "production") {
2670
+ return;
2671
+ }
2672
+ const { typed } = await prompts4({
2673
+ type: "text",
2674
+ name: "typed",
2675
+ message: `Remote/production database detected. Type the database name "${dbInfo.database}" to continue:`
2676
+ });
2677
+ if (typed !== dbInfo.database) {
2678
+ console.log(chalk14.gray("Cancelled (database name mismatch)."));
2679
+ process.exit(0);
2680
+ }
2681
+ }
2621
2682
  async function isPortAvailable(port) {
2622
2683
  return new Promise((resolve3) => {
2623
2684
  const server = net.createServer();
@@ -2667,14 +2728,14 @@ function formatTimestamp() {
2667
2728
  import { promises as fs2 } from "fs";
2668
2729
  import { existsSync as existsSync17 } from "fs";
2669
2730
  import path2 from "path";
2670
- import chalk15 from "chalk";
2731
+ import chalk16 from "chalk";
2671
2732
 
2672
2733
  // src/commands/db/utils/metadata.ts
2673
2734
  import { promises as fs } from "fs";
2674
2735
  import path from "path";
2675
2736
  import { promisify } from "util";
2676
2737
  import { exec } from "child_process";
2677
- import chalk14 from "chalk";
2738
+ import chalk15 from "chalk";
2678
2739
  var execAsync = promisify(exec);
2679
2740
  async function collectGitInfo() {
2680
2741
  try {
@@ -2737,7 +2798,7 @@ async function collectMigrationInfo(dbUrl) {
2737
2798
  await pool.end();
2738
2799
  }
2739
2800
  } catch (error) {
2740
- console.log(chalk14.dim("\u26A0\uFE0F Could not fetch migration info"));
2801
+ console.log(chalk15.dim("\u26A0\uFE0F Could not fetch migration info"));
2741
2802
  return void 0;
2742
2803
  }
2743
2804
  }
@@ -2745,9 +2806,9 @@ async function saveBackupMetadata(metadata, backupFilename) {
2745
2806
  const metadataPath = backupFilename.replace(/\.(sql|dump)$/, ".meta.json");
2746
2807
  try {
2747
2808
  await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
2748
- console.log(chalk14.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
2809
+ console.log(chalk15.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
2749
2810
  } catch (error) {
2750
- console.log(chalk14.dim("\u26A0\uFE0F Could not save metadata"));
2811
+ console.log(chalk15.dim("\u26A0\uFE0F Could not save metadata"));
2751
2812
  }
2752
2813
  }
2753
2814
  async function loadBackupMetadata(backupFilename) {
@@ -2776,10 +2837,10 @@ async function ensureBackupInGitignore() {
2776
2837
  if (!hasBackupsIgnore) {
2777
2838
  const entry = exists && content && !content.endsWith("\n") ? "\n\n# Database backups\nbackups/\n" : "# Database backups\nbackups/\n";
2778
2839
  await fs2.appendFile(gitignorePath, entry);
2779
- console.log(chalk15.dim("\u2713 Added backups/ to .gitignore"));
2840
+ console.log(chalk16.dim("\u2713 Added backups/ to .gitignore"));
2780
2841
  }
2781
2842
  } catch (error) {
2782
- console.log(chalk15.dim("\u26A0\uFE0F Could not update .gitignore"));
2843
+ console.log(chalk16.dim("\u26A0\uFE0F Could not update .gitignore"));
2783
2844
  }
2784
2845
  }
2785
2846
  async function ensureBackupDir() {
@@ -2829,12 +2890,12 @@ async function listBackupFiles() {
2829
2890
  import { env as env3 } from "@spfn/core/config";
2830
2891
  import { loadEnv as loadEnv4 } from "@spfn/core/server";
2831
2892
  async function dbBackup(options) {
2832
- console.log(chalk16.blue("\u{1F4BE} Creating database backup...\n"));
2893
+ console.log(chalk17.blue("\u{1F4BE} Creating database backup...\n"));
2833
2894
  loadEnv4();
2834
2895
  const dbUrl = env3.DATABASE_URL;
2835
2896
  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"));
2897
+ console.error(chalk17.red("\u274C DATABASE_URL not found in environment"));
2898
+ console.log(chalk17.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2838
2899
  process.exit(1);
2839
2900
  }
2840
2901
  const dbInfo = parseDatabaseUrl(dbUrl);
@@ -2844,7 +2905,7 @@ async function dbBackup(options) {
2844
2905
  const ext = format === "sql" ? "sql" : "dump";
2845
2906
  const filename = options.output || path3.join(backupDir, `${dbInfo.database}_${timestamp}.${ext}`);
2846
2907
  if (options.dataOnly && options.schemaOnly) {
2847
- console.error(chalk16.red("\u274C Cannot use --data-only and --schema-only together"));
2908
+ console.error(chalk17.red("\u274C Cannot use --data-only and --schema-only together"));
2848
2909
  process.exit(1);
2849
2910
  }
2850
2911
  const args = [
@@ -2890,11 +2951,11 @@ async function dbBackup(options) {
2890
2951
  const stats = await fs3.stat(filename);
2891
2952
  const size = formatBytes(stats.size);
2892
2953
  spinner.succeed("Backup created");
2893
- console.log(chalk16.green(`
2954
+ console.log(chalk17.green(`
2894
2955
  \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..."));
2956
+ console.log(chalk17.gray(` File: ${filename}`));
2957
+ console.log(chalk17.gray(` Size: ${size}`));
2958
+ console.log(chalk17.dim("\n\u{1F4CB} Collecting metadata..."));
2898
2959
  const [gitInfo, migrationInfo] = await Promise.all([
2899
2960
  collectGitInfo(),
2900
2961
  collectMigrationInfo(dbUrl)
@@ -2934,11 +2995,11 @@ async function dbBackup(options) {
2934
2995
  reject(error);
2935
2996
  });
2936
2997
  }).catch((error) => {
2937
- console.error(chalk16.red("\n\u274C Failed to create backup"));
2998
+ console.error(chalk17.red("\n\u274C Failed to create backup"));
2938
2999
  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."));
3000
+ console.error(chalk17.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
2940
3001
  } else {
2941
- console.error(chalk16.red(error instanceof Error ? error.message : "Unknown error"));
3002
+ console.error(chalk17.red(error instanceof Error ? error.message : "Unknown error"));
2942
3003
  }
2943
3004
  process.exit(1);
2944
3005
  });
@@ -2955,7 +3016,7 @@ async function dbMigrate(options = {}) {
2955
3016
  process.exit(1);
2956
3017
  }
2957
3018
  if (options.withBackup) {
2958
- console.log(chalk17.blue("\u{1F4E6} Creating pre-migration backup...\n"));
3019
+ console.log(chalk18.blue("\u{1F4E6} Creating pre-migration backup...\n"));
2959
3020
  await dbBackup({
2960
3021
  format: "custom",
2961
3022
  tag: "pre-migration",
@@ -2968,57 +3029,56 @@ async function dbMigrate(options = {}) {
2968
3029
  const postgres = await import("postgres");
2969
3030
  loadEnv5();
2970
3031
  if (!env4.DATABASE_URL) {
2971
- console.error(chalk17.red("\u274C DATABASE_URL not found in environment"));
3032
+ console.error(chalk18.red("\u274C DATABASE_URL not found in environment"));
2972
3033
  process.exit(1);
2973
3034
  }
2974
3035
  const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
2975
3036
  const functions = discoverFunctionMigrations2(process.cwd());
2976
3037
  if (functions.length > 0) {
2977
- console.log(chalk17.blue("\u{1F4E6} Applying function package migrations:"));
3038
+ console.log(chalk18.blue("\u{1F4E6} Applying function package migrations:"));
2978
3039
  functions.forEach((func) => {
2979
- console.log(chalk17.dim(` - ${func.packageName}`));
3040
+ console.log(chalk18.dim(` - ${func.packageName}`));
2980
3041
  });
2981
3042
  await executeFunctionMigrations2(functions);
2982
- console.log(chalk17.green("\u2705 Function migrations applied\n"));
3043
+ console.log(chalk18.green("\u2705 Function migrations applied\n"));
2983
3044
  }
2984
3045
  const projectMigrationsDir = join16(process.cwd(), "src/server/drizzle");
2985
3046
  if (existsSync18(projectMigrationsDir)) {
2986
3047
  const projConn = postgres.default(env4.DATABASE_URL, { max: 1 });
2987
3048
  const projDb = drizzle(projConn);
2988
3049
  try {
2989
- console.log(chalk17.blue("\u{1F4E6} Running project migrations..."));
3050
+ console.log(chalk18.blue("\u{1F4E6} Running project migrations..."));
2990
3051
  await migrate(projDb, {
2991
3052
  migrationsFolder: projectMigrationsDir,
2992
3053
  migrationsTable: PROJECT_MIGRATIONS_TABLE
2993
3054
  });
2994
- console.log(chalk17.green("\u2705 Project migrations applied successfully"));
3055
+ console.log(chalk18.green("\u2705 Project migrations applied successfully"));
2995
3056
  } finally {
2996
3057
  await projConn.end();
2997
3058
  }
2998
3059
  } else {
2999
- console.log(chalk17.dim("No project migrations found (src/server/drizzle)"));
3060
+ console.log(chalk18.dim("No project migrations found (src/server/drizzle)"));
3000
3061
  }
3001
3062
  }
3002
3063
 
3003
3064
  // src/commands/db/studio.ts
3004
- import chalk18 from "chalk";
3005
- import { existsSync as existsSync19, writeFileSync as writeFileSync10, unlinkSync as unlinkSync3 } from "fs";
3065
+ import chalk19 from "chalk";
3066
+ import { existsSync as existsSync19, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3 } from "fs";
3006
3067
  import { spawn as spawn3 } from "child_process";
3007
3068
  import { env as env5 } from "@spfn/core/config";
3008
- import "@spfn/core/config";
3009
3069
  async function dbStudio(requestedPort) {
3010
- console.log(chalk18.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
3070
+ console.log(chalk19.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
3011
3071
  const defaultPort = 4983;
3012
3072
  const startPort = requestedPort || defaultPort;
3013
3073
  let port;
3014
3074
  try {
3015
3075
  port = await findAvailablePort(startPort);
3016
3076
  if (port !== startPort) {
3017
- console.log(chalk18.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
3077
+ console.log(chalk19.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
3018
3078
  `));
3019
3079
  }
3020
3080
  } catch (error) {
3021
- console.error(chalk18.red(error instanceof Error ? error.message : "Failed to find available port"));
3081
+ console.error(chalk19.red(error instanceof Error ? error.message : "Failed to find available port"));
3022
3082
  process.exit(1);
3023
3083
  }
3024
3084
  const hasUserConfig = existsSync19("./drizzle.config.ts");
@@ -3027,8 +3087,8 @@ async function dbStudio(requestedPort) {
3027
3087
  const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
3028
3088
  if (!hasUserConfig) {
3029
3089
  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"));
3090
+ console.error(chalk19.red("\u274C DATABASE_URL not found in environment"));
3091
+ console.log(chalk19.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
3032
3092
  process.exit(1);
3033
3093
  }
3034
3094
  const { generateDrizzleConfigFile } = await import("@spfn/core/db");
@@ -3038,8 +3098,8 @@ async function dbStudio(requestedPort) {
3038
3098
  expandGlobs: true
3039
3099
  // Expand glob patterns for Studio compatibility
3040
3100
  });
3041
- writeFileSync10(tempConfigPath, configContent);
3042
- console.log(chalk18.dim("Using auto-generated Drizzle config\n"));
3101
+ writeFileSync11(tempConfigPath, configContent);
3102
+ console.log(chalk19.dim("Using auto-generated Drizzle config\n"));
3043
3103
  }
3044
3104
  const studioProcess = spawn3("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
3045
3105
  stdio: "inherit",
@@ -3054,19 +3114,19 @@ async function dbStudio(requestedPort) {
3054
3114
  studioProcess.on("exit", (code) => {
3055
3115
  cleanup();
3056
3116
  if (code !== 0 && code !== null) {
3057
- console.error(chalk18.red(`
3117
+ console.error(chalk19.red(`
3058
3118
  \u274C Drizzle Studio exited with code ${code}`));
3059
3119
  process.exit(code);
3060
3120
  }
3061
3121
  });
3062
3122
  studioProcess.on("error", (error) => {
3063
3123
  cleanup();
3064
- console.error(chalk18.red("\u274C Failed to start Drizzle Studio"));
3065
- console.error(chalk18.red(error.message));
3124
+ console.error(chalk19.red("\u274C Failed to start Drizzle Studio"));
3125
+ console.error(chalk19.red(error.message));
3066
3126
  process.exit(1);
3067
3127
  });
3068
3128
  process.on("SIGINT", () => {
3069
- console.log(chalk18.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
3129
+ console.log(chalk19.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
3070
3130
  studioProcess.kill("SIGTERM");
3071
3131
  cleanup();
3072
3132
  process.exit(0);
@@ -3080,27 +3140,39 @@ async function dbStudio(requestedPort) {
3080
3140
  if (!hasUserConfig && existsSync19(tempConfigPath)) {
3081
3141
  unlinkSync3(tempConfigPath);
3082
3142
  }
3083
- console.error(chalk18.red("\u274C Failed to start Drizzle Studio"));
3084
- console.error(chalk18.red(error instanceof Error ? error.message : "Unknown error"));
3143
+ console.error(chalk19.red("\u274C Failed to start Drizzle Studio"));
3144
+ console.error(chalk19.red(error instanceof Error ? error.message : "Unknown error"));
3085
3145
  process.exit(1);
3086
3146
  }
3087
3147
  }
3088
3148
 
3089
3149
  // src/commands/db/drop.ts
3090
- import chalk19 from "chalk";
3091
- import prompts4 from "prompts";
3150
+ import chalk20 from "chalk";
3151
+ import prompts5 from "prompts";
3152
+ import { env as env6 } from "@spfn/core/config";
3153
+ import { loadEnv as loadEnv6 } from "@spfn/core/server";
3092
3154
  async function dbDrop() {
3093
- console.log(chalk19.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
3094
- const { confirm } = await prompts4({
3155
+ loadEnv6();
3156
+ if (!env6.DATABASE_URL) {
3157
+ console.error(chalk20.red("\u274C DATABASE_URL not found in environment"));
3158
+ console.log(chalk20.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
3159
+ process.exit(1);
3160
+ }
3161
+ const dbInfo = parseDatabaseUrl(env6.DATABASE_URL);
3162
+ console.log(chalk20.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
3163
+ console.log(chalk20.dim(` Target: ${dbInfo.database} @ ${dbInfo.host}:${dbInfo.port}
3164
+ `));
3165
+ const { confirm } = await prompts5({
3095
3166
  type: "confirm",
3096
3167
  name: "confirm",
3097
3168
  message: "Are you sure you want to drop all tables?",
3098
3169
  initial: false
3099
3170
  });
3100
3171
  if (!confirm) {
3101
- console.log(chalk19.gray("Cancelled."));
3172
+ console.log(chalk20.gray("Cancelled."));
3102
3173
  process.exit(0);
3103
3174
  }
3175
+ await confirmDangerousTarget(dbInfo);
3104
3176
  await runWithSpinner(
3105
3177
  "Dropping all tables...",
3106
3178
  "drop",
@@ -3110,7 +3182,7 @@ async function dbDrop() {
3110
3182
  }
3111
3183
 
3112
3184
  // src/commands/db/check.ts
3113
- import chalk20 from "chalk";
3185
+ import chalk21 from "chalk";
3114
3186
  import ora8 from "ora";
3115
3187
  async function dbCheck() {
3116
3188
  const spinner = ora8("Checking database connection...").start();
@@ -3119,7 +3191,7 @@ async function dbCheck() {
3119
3191
  spinner.succeed("Database connection OK");
3120
3192
  } catch (error) {
3121
3193
  spinner.fail("Database connection failed");
3122
- console.error(chalk20.red(error instanceof Error ? error.message : "Unknown error"));
3194
+ console.error(chalk21.red(error instanceof Error ? error.message : "Unknown error"));
3123
3195
  process.exit(1);
3124
3196
  }
3125
3197
  }
@@ -3127,28 +3199,28 @@ async function dbCheck() {
3127
3199
  // src/commands/db/restore.ts
3128
3200
  import path4 from "path";
3129
3201
  import { spawn as spawn4 } from "child_process";
3130
- import chalk21 from "chalk";
3202
+ import chalk22 from "chalk";
3131
3203
  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";
3204
+ import prompts6 from "prompts";
3205
+ import { env as env7 } from "@spfn/core/config";
3206
+ import { loadEnv as loadEnv7 } from "@spfn/core/server";
3135
3207
  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;
3208
+ console.log(chalk22.blue("\u267B\uFE0F Restoring database from backup...\n"));
3209
+ loadEnv7();
3210
+ const dbUrl = env7.DATABASE_URL;
3139
3211
  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"));
3212
+ console.error(chalk22.red("\u274C DATABASE_URL not found in environment"));
3213
+ console.log(chalk22.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
3142
3214
  process.exit(1);
3143
3215
  }
3144
3216
  let file = backupFile;
3145
3217
  if (!file) {
3146
3218
  const backups = await listBackupFiles();
3147
3219
  if (backups.length === 0) {
3148
- console.log(chalk21.yellow("No backups found in ./backups directory"));
3220
+ console.log(chalk22.yellow("No backups found in ./backups directory"));
3149
3221
  process.exit(0);
3150
3222
  }
3151
- const { selected } = await prompts5({
3223
+ const { selected } = await prompts6({
3152
3224
  type: "select",
3153
3225
  name: "selected",
3154
3226
  message: "Select backup to restore:",
@@ -3158,31 +3230,31 @@ async function dbRestore(backupFile, options = {}) {
3158
3230
  }))
3159
3231
  });
3160
3232
  if (!selected) {
3161
- console.log(chalk21.gray("Cancelled"));
3233
+ console.log(chalk22.gray("Cancelled"));
3162
3234
  process.exit(0);
3163
3235
  }
3164
3236
  file = selected;
3165
3237
  }
3166
3238
  if (!file) {
3167
- console.error(chalk21.red("\u274C No backup file selected"));
3239
+ console.error(chalk22.red("\u274C No backup file selected"));
3168
3240
  process.exit(1);
3169
3241
  }
3170
3242
  const metadata = await loadBackupMetadata(file);
3171
3243
  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()}`));
3244
+ console.log(chalk22.blue("\n\u{1F4CB} Backup Information:\n"));
3245
+ console.log(chalk22.dim(` Database: ${metadata.database}`));
3246
+ console.log(chalk22.dim(` Created: ${new Date(metadata.timestamp).toLocaleString()}`));
3175
3247
  if (metadata.environment) {
3176
- console.log(chalk21.dim(` Environment: ${metadata.environment}`));
3248
+ console.log(chalk22.dim(` Environment: ${metadata.environment}`));
3177
3249
  }
3178
3250
  if (metadata.tags && metadata.tags.length > 0) {
3179
- console.log(chalk21.dim(` Tags: ${metadata.tags.join(", ")}`));
3251
+ console.log(chalk22.dim(` Tags: ${metadata.tags.join(", ")}`));
3180
3252
  }
3181
3253
  if (metadata.backup.dataOnly) {
3182
- console.log(chalk21.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
3254
+ console.log(chalk22.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
3183
3255
  }
3184
3256
  if (metadata.backup.schemaOnly) {
3185
- console.log(chalk21.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
3257
+ console.log(chalk22.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
3186
3258
  }
3187
3259
  const warnings2 = [];
3188
3260
  const [currentGitInfo, currentMigrationInfo] = await Promise.all([
@@ -3205,26 +3277,27 @@ async function dbRestore(backupFile, options = {}) {
3205
3277
  }
3206
3278
  }
3207
3279
  if (warnings2.length > 0) {
3208
- console.log(chalk21.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
3209
- warnings2.forEach((warning) => console.log(chalk21.yellow(` - ${warning}`)));
3280
+ console.log(chalk22.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
3281
+ warnings2.forEach((warning) => console.log(chalk22.yellow(` - ${warning}`)));
3210
3282
  console.log("");
3211
3283
  }
3212
3284
  }
3213
- const { confirm } = await prompts5({
3285
+ const dbInfo = parseDatabaseUrl(dbUrl);
3286
+ const { confirm } = await prompts6({
3214
3287
  type: "confirm",
3215
3288
  name: "confirm",
3216
- message: chalk21.yellow("\u26A0\uFE0F This will replace all data in the database. Continue?"),
3289
+ message: chalk22.yellow(`\u26A0\uFE0F This will replace all data in "${dbInfo.database}" @ ${dbInfo.host}:${dbInfo.port}. Continue?`),
3217
3290
  initial: false
3218
3291
  });
3219
3292
  if (!confirm) {
3220
- console.log(chalk21.gray("Cancelled"));
3293
+ console.log(chalk22.gray("Cancelled"));
3221
3294
  process.exit(0);
3222
3295
  }
3296
+ await confirmDangerousTarget(dbInfo);
3223
3297
  if (options.dataOnly && options.schemaOnly) {
3224
- console.error(chalk21.red("\u274C Cannot use --data-only and --schema-only together"));
3298
+ console.error(chalk22.red("\u274C Cannot use --data-only and --schema-only together"));
3225
3299
  process.exit(1);
3226
3300
  }
3227
- const dbInfo = parseDatabaseUrl(dbUrl);
3228
3301
  const ext = path4.extname(file);
3229
3302
  const isCustomFormat = ext === ".dump";
3230
3303
  const command = isCustomFormat ? "pg_restore" : "psql";
@@ -3236,7 +3309,7 @@ async function dbRestore(backupFile, options = {}) {
3236
3309
  args.push("-d", dbInfo.database);
3237
3310
  args.push("--verbose");
3238
3311
  if (options.drop) {
3239
- args.push("--clean");
3312
+ args.push("--clean", "--if-exists");
3240
3313
  }
3241
3314
  if (options.schema) {
3242
3315
  args.push("-n", options.schema);
@@ -3249,9 +3322,12 @@ async function dbRestore(backupFile, options = {}) {
3249
3322
  }
3250
3323
  args.push(file);
3251
3324
  } else {
3325
+ if (options.drop) {
3326
+ console.log(chalk22.yellow("\u26A0\uFE0F Note: --drop only works with custom format backups (.dump) and was ignored."));
3327
+ }
3252
3328
  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"));
3329
+ console.log(chalk22.yellow("\u26A0\uFE0F Note: --data-only and --schema-only options only work with custom format backups (.dump)"));
3330
+ console.log(chalk22.yellow(" For SQL files, the backup must have been created with the desired option.\n"));
3255
3331
  }
3256
3332
  args.push("-h", dbInfo.host);
3257
3333
  args.push("-p", dbInfo.port);
@@ -3272,7 +3348,6 @@ async function dbRestore(backupFile, options = {}) {
3272
3348
  const warnings = [];
3273
3349
  const errors = [];
3274
3350
  let objectCount = 0;
3275
- let lastObject = "";
3276
3351
  restoreProcess.stderr?.on("data", (data) => {
3277
3352
  const lines = data.toString().split("\n").filter((l) => l.trim());
3278
3353
  for (const line of lines) {
@@ -3286,18 +3361,16 @@ async function dbRestore(backupFile, options = {}) {
3286
3361
  objectCount = Number(objectMatch[2]);
3287
3362
  const current = Number(objectMatch[1]);
3288
3363
  const desc = line.replace(/^pg_restore:\s*/, "").trim();
3289
- lastObject = desc;
3290
3364
  spinner.text = `Restoring backup... [${current}/${objectCount}] ${desc}`;
3291
3365
  } else if (isCustomFormat) {
3292
3366
  const desc = line.replace(/^pg_restore:\s*/, "").trim();
3293
3367
  if (desc && !/warning:|error:/i.test(desc)) {
3294
- lastObject = desc;
3295
3368
  spinner.text = `Restoring backup... ${desc}`;
3296
3369
  }
3297
3370
  }
3298
3371
  if (verbose) {
3299
3372
  spinner.stop();
3300
- console.log(chalk21.dim(` ${line.trim()}`));
3373
+ console.log(chalk22.dim(` ${line.trim()}`));
3301
3374
  spinner.start();
3302
3375
  }
3303
3376
  }
@@ -3305,7 +3378,7 @@ async function dbRestore(backupFile, options = {}) {
3305
3378
  restoreProcess.stdout?.on("data", (data) => {
3306
3379
  if (verbose) {
3307
3380
  spinner.stop();
3308
- console.log(chalk21.dim(` ${data.toString().trim()}`));
3381
+ console.log(chalk22.dim(` ${data.toString().trim()}`));
3309
3382
  spinner.start();
3310
3383
  }
3311
3384
  });
@@ -3315,31 +3388,31 @@ async function dbRestore(backupFile, options = {}) {
3315
3388
  const summary = objectCount > 0 ? ` (${objectCount} objects)` : "";
3316
3389
  spinner.succeed(`Restore completed${summary}`);
3317
3390
  if (warnings.length > 0) {
3318
- console.log(chalk21.yellow(`
3391
+ console.log(chalk22.yellow(`
3319
3392
  \u26A0\uFE0F Warnings during restore (${warnings.length}):
3320
3393
  `));
3321
3394
  for (const w of warnings) {
3322
- console.log(chalk21.yellow(` - ${w}`));
3395
+ console.log(chalk22.yellow(` - ${w}`));
3323
3396
  }
3324
3397
  }
3325
- console.log(chalk21.green("\n\u2705 Database restored successfully"));
3398
+ console.log(chalk22.green("\n\u2705 Database restored successfully"));
3326
3399
  resolve3();
3327
3400
  } else {
3328
3401
  spinner.fail("Restore failed");
3329
3402
  if (errors.length > 0) {
3330
- console.error(chalk21.red(`
3403
+ console.error(chalk22.red(`
3331
3404
  \u274C Errors (${errors.length}):
3332
3405
  `));
3333
3406
  for (const e of errors) {
3334
- console.error(chalk21.red(` - ${e}`));
3407
+ console.error(chalk22.red(` - ${e}`));
3335
3408
  }
3336
3409
  }
3337
3410
  if (warnings.length > 0) {
3338
- console.log(chalk21.yellow(`
3411
+ console.log(chalk22.yellow(`
3339
3412
  \u26A0\uFE0F Warnings (${warnings.length}):
3340
3413
  `));
3341
3414
  for (const w of warnings) {
3342
- console.log(chalk21.yellow(` - ${w}`));
3415
+ console.log(chalk22.yellow(` - ${w}`));
3343
3416
  }
3344
3417
  }
3345
3418
  const fallback = errors.length === 0 && warnings.length === 0 ? "Restore failed with no output" : "";
@@ -3353,7 +3426,7 @@ async function dbRestore(backupFile, options = {}) {
3353
3426
  }).catch((error) => {
3354
3427
  const msg = error instanceof Error ? error.message : "Unknown error";
3355
3428
  if (msg) {
3356
- console.error(chalk21.red(`
3429
+ console.error(chalk22.red(`
3357
3430
  \u274C ${msg}`));
3358
3431
  }
3359
3432
  process.exit(1);
@@ -3361,17 +3434,17 @@ async function dbRestore(backupFile, options = {}) {
3361
3434
  }
3362
3435
 
3363
3436
  // src/commands/db/list.ts
3364
- import chalk22 from "chalk";
3437
+ import chalk23 from "chalk";
3365
3438
  async function dbBackupList() {
3366
- console.log(chalk22.blue("\u{1F4CB} Database backups:\n"));
3439
+ console.log(chalk23.blue("\u{1F4CB} Database backups:\n"));
3367
3440
  const backups = await listBackupFiles();
3368
3441
  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"));
3442
+ console.log(chalk23.yellow("No backups found in ./backups directory"));
3443
+ console.log(chalk23.gray("\n\u{1F4A1} Create a backup with: pnpm spfn db backup\n"));
3371
3444
  return;
3372
3445
  }
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"));
3446
+ console.log(chalk23.bold(" Date Size File"));
3447
+ 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
3448
  backups.forEach((backup) => {
3376
3449
  const date = backup.date.toLocaleString("en-US", {
3377
3450
  year: "numeric",
@@ -3382,23 +3455,23 @@ async function dbBackupList() {
3382
3455
  second: "2-digit"
3383
3456
  });
3384
3457
  const sizeStr = backup.size.padEnd(10);
3385
- console.log(chalk22.white(` ${date} ${sizeStr} ${backup.name}`));
3458
+ console.log(chalk23.white(` ${date} ${sizeStr} ${backup.name}`));
3386
3459
  });
3387
- console.log(chalk22.gray(`
3460
+ console.log(chalk23.gray(`
3388
3461
  Total: ${backups.length} backup(s)
3389
3462
  `));
3390
3463
  }
3391
3464
 
3392
3465
  // src/commands/db/clean.ts
3393
3466
  import { promises as fs4 } from "fs";
3394
- import chalk23 from "chalk";
3467
+ import chalk24 from "chalk";
3395
3468
  import ora10 from "ora";
3396
- import prompts6 from "prompts";
3469
+ import prompts7 from "prompts";
3397
3470
  async function dbBackupClean(options) {
3398
- console.log(chalk23.blue("\u{1F9F9} Cleaning old backups...\n"));
3471
+ console.log(chalk24.blue("\u{1F9F9} Cleaning old backups...\n"));
3399
3472
  const backups = await listBackupFiles();
3400
3473
  if (backups.length === 0) {
3401
- console.log(chalk23.yellow("No backups found"));
3474
+ console.log(chalk24.yellow("No backups found"));
3402
3475
  return;
3403
3476
  }
3404
3477
  let toDelete = [];
@@ -3415,42 +3488,46 @@ async function dbBackupClean(options) {
3415
3488
  toDelete = backups.slice(defaultKeep);
3416
3489
  }
3417
3490
  if (toDelete.length === 0) {
3418
- console.log(chalk23.green("\u2705 No backups to clean"));
3491
+ console.log(chalk24.green("\u2705 No backups to clean"));
3419
3492
  return;
3420
3493
  }
3421
- console.log(chalk23.yellow(`The following ${toDelete.length} backup(s) will be deleted:
3494
+ console.log(chalk24.yellow(`The following ${toDelete.length} backup(s) will be deleted:
3422
3495
  `));
3423
3496
  toDelete.forEach((backup) => {
3424
- console.log(chalk23.gray(` - ${backup.name} (${backup.size})`));
3497
+ console.log(chalk24.gray(` - ${backup.name} (${backup.size})`));
3425
3498
  });
3426
- const { confirm } = await prompts6({
3499
+ const { confirm } = await prompts7({
3427
3500
  type: "confirm",
3428
3501
  name: "confirm",
3429
3502
  message: "\nProceed with deletion?",
3430
3503
  initial: false
3431
3504
  });
3432
3505
  if (!confirm) {
3433
- console.log(chalk23.gray("Cancelled"));
3506
+ console.log(chalk24.gray("Cancelled"));
3434
3507
  return;
3435
3508
  }
3436
3509
  const spinner = ora10("Deleting backups...").start();
3437
3510
  try {
3438
- await Promise.all(toDelete.map((backup) => fs4.unlink(backup.path)));
3511
+ await Promise.all(toDelete.flatMap((backup) => [
3512
+ fs4.unlink(backup.path),
3513
+ // Remove the .meta.json sidecar too (force: ok if it doesn't exist)
3514
+ fs4.rm(backup.path.replace(/\.(sql|dump)$/, ".meta.json"), { force: true })
3515
+ ]));
3439
3516
  spinner.succeed("Backups deleted");
3440
- console.log(chalk23.green(`
3517
+ console.log(chalk24.green(`
3441
3518
  \u2705 Deleted ${toDelete.length} backup(s)`));
3442
3519
  } catch (error) {
3443
3520
  spinner.fail("Failed to delete backups");
3444
- console.error(chalk23.red(error instanceof Error ? error.message : "Unknown error"));
3521
+ console.error(chalk24.red(error instanceof Error ? error.message : "Unknown error"));
3445
3522
  process.exit(1);
3446
3523
  }
3447
3524
  }
3448
3525
 
3449
3526
  // src/commands/db/reindex.ts
3450
- import { existsSync as existsSync20, readFileSync as readFileSync6, writeFileSync as writeFileSync11, renameSync, copyFileSync } from "fs";
3527
+ import { existsSync as existsSync20, readFileSync as readFileSync7, writeFileSync as writeFileSync12, renameSync, copyFileSync } from "fs";
3451
3528
  import { join as join17 } from "path";
3452
- import chalk24 from "chalk";
3453
- import { loadEnv as loadEnv7 } from "@spfn/core/server";
3529
+ import chalk25 from "chalk";
3530
+ import { loadEnv as loadEnv8 } from "@spfn/core/server";
3454
3531
  function isTimestampPrefix(tag) {
3455
3532
  const prefix = tag.split("_")[0];
3456
3533
  return /^\d{5,}$/.test(prefix);
@@ -3466,19 +3543,19 @@ function parseTag(tag) {
3466
3543
  };
3467
3544
  }
3468
3545
  async function dbReindex(options = {}) {
3469
- loadEnv7();
3546
+ loadEnv8();
3470
3547
  const { getDrizzleConfig } = await import("@spfn/core/db");
3471
3548
  const config = getDrizzleConfig({ disablePackageDiscovery: true });
3472
3549
  const outDir = config.out;
3473
3550
  const journalPath = join17(outDir, "meta", "_journal.json");
3474
3551
  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"));
3552
+ console.error(chalk25.red("\u274C No _journal.json found at:"), journalPath);
3553
+ console.log(chalk25.yellow("\u{1F4A1} Run `spfn db generate` first to create migrations"));
3477
3554
  process.exit(1);
3478
3555
  }
3479
- const journal = JSON.parse(readFileSync6(journalPath, "utf-8"));
3556
+ const journal = JSON.parse(readFileSync7(journalPath, "utf-8"));
3480
3557
  if (journal.entries.length === 0) {
3481
- console.log(chalk24.yellow("No migration entries found \u2014 nothing to reindex."));
3558
+ console.log(chalk25.yellow("No migration entries found \u2014 nothing to reindex."));
3482
3559
  return;
3483
3560
  }
3484
3561
  const renames = [];
@@ -3505,33 +3582,33 @@ async function dbReindex(options = {}) {
3505
3582
  tagUpdates.push({ idx: entry.idx, oldTag: entry.tag, newTag });
3506
3583
  }
3507
3584
  if (tagUpdates.length === 0) {
3508
- console.log(chalk24.green("\u2705 All migrations already use timestamp prefix \u2014 nothing to do."));
3585
+ console.log(chalk25.green("\u2705 All migrations already use timestamp prefix \u2014 nothing to do."));
3509
3586
  if (skipped > 0) {
3510
- console.log(chalk24.dim(` (${skipped} entries already timestamp-prefixed)`));
3587
+ console.log(chalk25.dim(` (${skipped} entries already timestamp-prefixed)`));
3511
3588
  }
3512
3589
  return;
3513
3590
  }
3514
- console.log(chalk24.bold("\n\u{1F4CB} Reindex plan:\n"));
3591
+ console.log(chalk25.bold("\n\u{1F4CB} Reindex plan:\n"));
3515
3592
  for (const update of tagUpdates) {
3516
3593
  console.log(
3517
- chalk24.dim(` [${update.idx}]`),
3518
- chalk24.red(update.oldTag),
3519
- chalk24.dim("\u2192"),
3520
- chalk24.green(update.newTag)
3594
+ chalk25.dim(` [${update.idx}]`),
3595
+ chalk25.red(update.oldTag),
3596
+ chalk25.dim("\u2192"),
3597
+ chalk25.green(update.newTag)
3521
3598
  );
3522
3599
  }
3523
- console.log(chalk24.dim(`
3600
+ console.log(chalk25.dim(`
3524
3601
  ${renames.length} file(s) to rename, ${tagUpdates.length} journal tag(s) to update`));
3525
3602
  if (skipped > 0) {
3526
- console.log(chalk24.dim(` ${skipped} entry/entries already timestamp-prefixed (skipped)`));
3603
+ console.log(chalk25.dim(` ${skipped} entry/entries already timestamp-prefixed (skipped)`));
3527
3604
  }
3528
3605
  if (options.dryRun) {
3529
- console.log(chalk24.yellow("\n\u{1F50D} Dry run \u2014 no changes applied."));
3606
+ console.log(chalk25.yellow("\n\u{1F50D} Dry run \u2014 no changes applied."));
3530
3607
  return;
3531
3608
  }
3532
3609
  const backupPath = journalPath + ".bak";
3533
3610
  copyFileSync(journalPath, backupPath);
3534
- console.log(chalk24.dim(`
3611
+ console.log(chalk25.dim(`
3535
3612
  Backed up journal \u2192 ${backupPath}`));
3536
3613
  for (const rename of renames) {
3537
3614
  renameSync(rename.from, rename.to);
@@ -3542,8 +3619,8 @@ async function dbReindex(options = {}) {
3542
3619
  entry.tag = update.newTag;
3543
3620
  }
3544
3621
  }
3545
- writeFileSync11(journalPath, JSON.stringify(journal, null, 2) + "\n");
3546
- console.log(chalk24.green(`
3622
+ writeFileSync12(journalPath, JSON.stringify(journal, null, 2) + "\n");
3623
+ console.log(chalk25.green(`
3547
3624
  \u2705 Reindex complete \u2014 ${tagUpdates.length} migration(s) converted to timestamp prefix.`));
3548
3625
  }
3549
3626
 
@@ -3562,1415 +3639,96 @@ dbCommand.command("backup:clean").description("Clean old database backups").opti
3562
3639
  dbCommand.command("reindex").description("Convert migration files from sequential to timestamp prefix").option("--dry-run", "Show changes without applying").action((options) => dbReindex(options));
3563
3640
 
3564
3641
  // src/commands/add.ts
3642
+ init_package_manager();
3565
3643
  import { Command as Command10 } from "commander";
3566
- import { existsSync as existsSync21, readFileSync as readFileSync7 } from "fs";
3644
+ import { existsSync as existsSync21, readFileSync as readFileSync8 } from "fs";
3567
3645
  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";
3646
+ import { execa as execa7 } from "execa";
3647
+ import chalk26 from "chalk";
3571
3648
  import ora11 from "ora";
3572
- var execAsync2 = promisify2(exec2);
3573
3649
  async function addPackage(packageName) {
3574
3650
  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"));
3651
+ console.error(chalk26.red("\u274C Please specify full package name"));
3652
+ console.log(chalk26.yellow("\n\u{1F4A1} Examples:"));
3653
+ console.log(chalk26.gray(" pnpm spfn add @spfn/cms"));
3654
+ console.log(chalk26.gray(" pnpm spfn add @mycompany/spfn-analytics"));
3579
3655
  process.exit(1);
3580
3656
  }
3581
- console.log(chalk25.blue(`
3657
+ console.log(chalk26.blue(`
3582
3658
  \u{1F4E6} Setting up ${packageName}...
3583
3659
  `));
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`);
3660
+ const pkgPath = join18(process.cwd(), "node_modules", ...packageName.split("/"));
3661
+ const pkgJsonPath = join18(pkgPath, "package.json");
3662
+ if (!existsSync21(pkgJsonPath)) {
3663
+ const pm = detectPackageManager(process.cwd());
3664
+ const installSpinner = ora11("Installing package...").start();
3665
+ try {
3666
+ await execa7(pm, ["add", packageName]);
3667
+ installSpinner.succeed("Package installed");
3668
+ } catch (error) {
3669
+ installSpinner.fail("Failed to install package");
3670
+ console.error(chalk26.red(error instanceof Error ? error.message : "Unknown error"));
3671
+ process.exit(1);
3601
3672
  }
3602
- const pkgJson = JSON.parse(readFileSync7(pkgJsonPath, "utf-8"));
3603
- if (pkgJson.spfn?.migrations) {
3604
- console.log(chalk25.blue(`
3673
+ } else {
3674
+ console.log(chalk26.gray("\u2713 Package already installed (using local version)\n"));
3675
+ }
3676
+ if (!existsSync21(pkgJsonPath)) {
3677
+ console.error(chalk26.red(`\u274C Package ${packageName} not found after installation`));
3678
+ process.exit(1);
3679
+ }
3680
+ const pkgJson = JSON.parse(readFileSync8(pkgJsonPath, "utf-8"));
3681
+ if (pkgJson.spfn?.migrations) {
3682
+ console.log(chalk26.blue(`
3605
3683
  \u{1F5C4}\uFE0F Setting up database for ${packageName}...
3606
3684
  `));
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"));
3685
+ const { env: env8 } = await import("@spfn/core/config");
3686
+ if (!env8.DATABASE_URL) {
3687
+ console.log(chalk26.yellow("\u26A0\uFE0F DATABASE_URL not found"));
3688
+ console.log(chalk26.gray("Skipping database setup. Run migrations manually when ready:\n"));
3689
+ console.log(chalk26.gray(" pnpm spfn db push\n"));
3690
+ } else {
3691
+ const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
3692
+ const functions = discoverFunctionMigrations2(process.cwd());
3693
+ const targetFunction = functions.find((f) => f.packageName === packageName);
3694
+ if (targetFunction) {
3695
+ const spinner = ora11("Applying migrations...").start();
3696
+ try {
3697
+ await executeFunctionMigrations2([targetFunction]);
3698
+ spinner.succeed("Migrations applied");
3699
+ } catch (error) {
3700
+ spinner.fail("Failed to apply migrations");
3701
+ console.error(chalk26.red(error instanceof Error ? error.message : "Unknown error"));
3702
+ process.exit(1);
3628
3703
  }
3704
+ } else {
3705
+ console.log(chalk26.gray("\u2139\uFE0F No migrations found for this package"));
3629
3706
  }
3630
- } else {
3631
- console.log(chalk25.gray("\n\u2139\uFE0F No database migrations to apply"));
3632
3707
  }
3633
- console.log(chalk25.green(`
3708
+ } else {
3709
+ console.log(chalk26.gray("\n\u2139\uFE0F No database migrations to apply"));
3710
+ }
3711
+ console.log(chalk26.green(`
3634
3712
  \u2705 ${packageName} installed successfully!
3635
3713
  `));
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);
3714
+ if (pkgJson.spfn?.setupMessage) {
3715
+ console.log(chalk26.cyan("\u{1F4DA} Setup Guide:"));
3716
+ console.log(pkgJson.spfn.setupMessage);
3717
+ console.log();
3647
3718
  }
3648
3719
  }
3649
3720
  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
3721
 
3651
- // src/commands/generate.ts
3652
- init_logger();
3722
+ // src/commands/env.ts
3653
3723
  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
3724
  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";
3725
+ import { existsSync as existsSync22, readFileSync as readFileSync9, writeFileSync as writeFileSync13 } from "fs";
4968
3726
  import { resolve as resolve2 } from "path";
4969
3727
  import { parse } from "dotenv";
4970
3728
  var VALID_ENVS = ["local", "development", "staging", "production", "test"];
4971
3729
  var BASE_ENV_FILES = {
4972
3730
  nextjs: [".env", ".env.local"],
4973
- server: [".env.server", ".env.server.local"]
3731
+ server: [".env.server"]
4974
3732
  };
4975
3733
  function getEnvFilesForEnvironment(nodeEnv) {
4976
3734
  const files = [".env"];
@@ -4984,7 +3742,6 @@ function getEnvFilesForEnvironment(nodeEnv) {
4984
3742
  files.push(`.env.${nodeEnv}.local`);
4985
3743
  }
4986
3744
  files.push(".env.server");
4987
- files.push(".env.server.local");
4988
3745
  return files;
4989
3746
  }
4990
3747
  function getTargetFile(schema) {
@@ -4992,7 +3749,7 @@ function getTargetFile(schema) {
4992
3749
  if (isNextjs) {
4993
3750
  return schema.sensitive ? ".env.local" : ".env";
4994
3751
  }
4995
- return schema.sensitive ? ".env.server.local" : ".env.server";
3752
+ return ".env.server";
4996
3753
  }
4997
3754
  async function loadEnvSchema(packageName) {
4998
3755
  try {
@@ -5012,26 +3769,26 @@ async function loadEnvSchema(packageName) {
5012
3769
  }
5013
3770
  function formatType(type) {
5014
3771
  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
3772
+ string: chalk27.green,
3773
+ number: chalk27.blue,
3774
+ boolean: chalk27.yellow,
3775
+ url: chalk27.cyan,
3776
+ enum: chalk27.magenta,
3777
+ json: chalk27.red
5021
3778
  };
5022
- return (typeColors[type] || chalk28.white)(type);
3779
+ return (typeColors[type] || chalk27.white)(type);
5023
3780
  }
5024
3781
  function formatDefault(value, type) {
5025
3782
  if (value === void 0) {
5026
- return chalk28.dim("(none)");
3783
+ return chalk27.dim("(none)");
5027
3784
  }
5028
3785
  if (type === "string" || type === "url") {
5029
- return chalk28.green(`"${value}"`);
3786
+ return chalk27.green(`"${value}"`);
5030
3787
  }
5031
3788
  if (type === "boolean") {
5032
- return value ? chalk28.green("true") : chalk28.red("false");
3789
+ return value ? chalk27.green("true") : chalk27.red("false");
5033
3790
  }
5034
- return chalk28.cyan(String(value));
3791
+ return chalk27.cyan(String(value));
5035
3792
  }
5036
3793
  async function listEnvVars(options) {
5037
3794
  const packageName = options.package || "@spfn/core";
@@ -5045,28 +3802,28 @@ async function listEnvVars(options) {
5045
3802
  acc[target].push([key, schema]);
5046
3803
  return acc;
5047
3804
  }, {});
5048
- console.log(chalk28.blue.bold(`
3805
+ console.log(chalk27.blue.bold(`
5049
3806
  \u{1F4CB} Environment Variables by File (${packageName})
5050
3807
  `));
5051
3808
  for (const [file, vars] of Object.entries(grouped)) {
5052
- console.log(chalk28.bold.magenta(`
3809
+ console.log(chalk27.bold.magenta(`
5053
3810
  ${file}`));
5054
- console.log(chalk28.dim("\u2500".repeat(50)));
3811
+ console.log(chalk27.dim("\u2500".repeat(50)));
5055
3812
  for (const [key, schema] of vars) {
5056
3813
  printEnvVar(key, schema);
5057
3814
  }
5058
3815
  }
5059
3816
  } else {
5060
- console.log(chalk28.blue.bold(`
3817
+ console.log(chalk27.blue.bold(`
5061
3818
  \u{1F4CB} Environment Variables (${packageName})
5062
3819
  `));
5063
3820
  for (const [key, schema] of allVars) {
5064
3821
  printEnvVar(key, schema, true);
5065
3822
  }
5066
3823
  }
5067
- console.log(chalk28.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
3824
+ console.log(chalk27.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
5068
3825
  } catch (error) {
5069
- console.error(chalk28.red(`
3826
+ console.error(chalk27.red(`
5070
3827
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
5071
3828
  `));
5072
3829
  process.exit(1);
@@ -5074,17 +3831,17 @@ ${file}`));
5074
3831
  }
5075
3832
  function printEnvVar(key, schema, showFile = false) {
5076
3833
  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)}`);
3834
+ const requiredStr = schema.required || schema.default !== void 0 ? chalk27.red("[required]") : chalk27.dim("[optional]");
3835
+ const sensitiveStr = schema.sensitive ? chalk27.yellow(" [sensitive]") : "";
3836
+ const fileStr = showFile ? chalk27.dim(` \u2192 ${getTargetFile(schema)}`) : "";
3837
+ console.log(`${chalk27.bold.cyan(key)} ${chalk27.dim("(")}${typeStr}${chalk27.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
3838
+ console.log(` ${chalk27.dim(schema.description)}`);
5082
3839
  if (schema.default !== void 0) {
5083
- console.log(` ${chalk28.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
3840
+ console.log(` ${chalk27.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
5084
3841
  }
5085
3842
  if (schema.examples && schema.examples.length > 0) {
5086
3843
  const exampleStr = schema.examples.map((ex) => formatDefault(ex, schema.type)).join(", ");
5087
- console.log(` ${chalk28.dim("Examples:")} ${exampleStr}`);
3844
+ console.log(` ${chalk27.dim("Examples:")} ${exampleStr}`);
5088
3845
  }
5089
3846
  console.log();
5090
3847
  }
@@ -5092,7 +3849,7 @@ async function showEnvStats(options) {
5092
3849
  const packageName = options.package || "@spfn/core";
5093
3850
  try {
5094
3851
  const envSchema = await loadEnvSchema(packageName);
5095
- console.log(chalk28.blue.bold(`
3852
+ console.log(chalk27.blue.bold(`
5096
3853
  \u{1F4CA} Environment Variable Statistics (${packageName})
5097
3854
  `));
5098
3855
  const allVars = Object.entries(envSchema);
@@ -5114,24 +3871,24 @@ async function showEnvStats(options) {
5114
3871
  acc[file] = (acc[file] || 0) + 1;
5115
3872
  return acc;
5116
3873
  }, {});
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:"));
3874
+ console.log(`${chalk27.bold("Total variables:")} ${chalk27.cyan(allVars.length)}`);
3875
+ console.log(`${chalk27.bold("Required:")} ${chalk27.red(required.length)}`);
3876
+ console.log(`${chalk27.bold("Optional:")} ${chalk27.dim(optional.length)}`);
3877
+ console.log(`${chalk27.bold("Sensitive:")} ${chalk27.yellow(sensitive.length)}`);
3878
+ console.log(chalk27.bold("\nBy Target:"));
3879
+ console.log(` ${chalk27.blue("Next.js accessible:")} ${chalk27.cyan(nextjsVars.length)}`);
3880
+ console.log(` ${chalk27.magenta("SPFN server only:")} ${chalk27.cyan(serverOnlyVars.length)}`);
3881
+ console.log(chalk27.bold("\nBy File:"));
5125
3882
  for (const [file, count] of Object.entries(fileCount)) {
5126
- console.log(` ${chalk28.dim(file)}: ${chalk28.cyan(count)}`);
3883
+ console.log(` ${chalk27.dim(file)}: ${chalk27.cyan(count)}`);
5127
3884
  }
5128
- console.log(chalk28.bold("\nBy Type:"));
3885
+ console.log(chalk27.bold("\nBy Type:"));
5129
3886
  for (const [type, count] of Object.entries(typeCount)) {
5130
- console.log(` ${formatType(type)}: ${chalk28.cyan(count)}`);
3887
+ console.log(` ${formatType(type)}: ${chalk27.cyan(count)}`);
5131
3888
  }
5132
3889
  console.log();
5133
3890
  } catch (error) {
5134
- console.error(chalk28.red(`
3891
+ console.error(chalk27.red(`
5135
3892
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
5136
3893
  `));
5137
3894
  process.exit(1);
@@ -5151,40 +3908,40 @@ async function searchEnvVars(query, options) {
5151
3908
  }
5152
3909
  }
5153
3910
  if (results.length === 0) {
5154
- console.log(chalk28.yellow(`
3911
+ console.log(chalk27.yellow(`
5155
3912
  \u26A0\uFE0F No environment variables found matching "${query}"
5156
3913
  `));
5157
3914
  return;
5158
3915
  }
5159
- console.log(chalk28.blue.bold(`
3916
+ console.log(chalk27.blue.bold(`
5160
3917
  \u{1F50D} Found ${results.length} environment variable(s) matching "${query}"
5161
3918
  `));
5162
3919
  for (const [key, schema] of results) {
5163
3920
  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)}`);
3921
+ const requiredStr = schema.required || schema.default !== void 0 ? chalk27.red("[required]") : chalk27.dim("[optional]");
3922
+ console.log(`${chalk27.bold.cyan(key)} ${chalk27.dim("(")}${typeStr}${chalk27.dim(")")} ${requiredStr}`);
3923
+ console.log(` ${chalk27.dim(schema.description)}`);
5167
3924
  if (schema.default !== void 0) {
5168
- console.log(` ${chalk28.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
3925
+ console.log(` ${chalk27.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
5169
3926
  }
5170
3927
  console.log();
5171
3928
  }
5172
3929
  } catch (error) {
5173
- console.error(chalk28.red(`
3930
+ console.error(chalk27.red(`
5174
3931
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
5175
3932
  `));
5176
3933
  process.exit(1);
5177
3934
  }
5178
3935
  }
5179
- var envCommand = new Command12("env").description("Manage environment variables");
3936
+ var envCommand = new Command11("env").description("Manage environment variables");
5180
3937
  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
3938
  envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(showEnvStats);
5182
3939
  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
3940
  function validateEnvOption(envValue) {
5184
3941
  if (!VALID_ENVS.includes(envValue)) {
5185
- console.error(chalk28.red(`
3942
+ console.error(chalk27.red(`
5186
3943
  \u274C Invalid environment: "${envValue}"`));
5187
- console.log(chalk28.dim(` Valid values: ${VALID_ENVS.join(", ")}
3944
+ console.log(chalk27.dim(` Valid values: ${VALID_ENVS.join(", ")}
5188
3945
  `));
5189
3946
  process.exit(1);
5190
3947
  }
@@ -5205,8 +3962,8 @@ async function initEnvFiles(options) {
5205
3962
  return acc;
5206
3963
  }, {});
5207
3964
  if (targetEnv) {
5208
- console.log(chalk28.blue.bold(`
5209
- \u{1F680} Generating .env template files for ${chalk28.cyan(targetEnv)} environment
3965
+ console.log(chalk27.blue.bold(`
3966
+ \u{1F680} Generating .env template files for ${chalk27.cyan(targetEnv)} environment
5210
3967
  `));
5211
3968
  const envSpecificFiles = {};
5212
3969
  const committedVars = allVars.filter(([_, schema]) => !schema.sensitive);
@@ -5222,25 +3979,24 @@ async function initEnvFiles(options) {
5222
3979
  writeEnvTemplate(cwd, file, vars, options.force ?? false);
5223
3980
  }
5224
3981
  } else {
5225
- console.log(chalk28.blue.bold(`
3982
+ console.log(chalk27.blue.bold(`
5226
3983
  \u{1F680} Generating .env template files
5227
3984
  `));
5228
3985
  for (const [file, vars] of Object.entries(grouped)) {
5229
3986
  writeEnvTemplate(cwd, file, vars, options.force ?? false);
5230
3987
  }
5231
3988
  }
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"));
3989
+ console.log(chalk27.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
3990
+ console.log(chalk27.dim(" cp .env.example .env"));
3991
+ console.log(chalk27.dim(" cp .env.local.example .env.local"));
3992
+ console.log(chalk27.dim(" cp .env.server.example .env.server"));
5237
3993
  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`));
3994
+ console.log(chalk27.dim(` cp .env.${targetEnv}.example .env.${targetEnv}`));
3995
+ console.log(chalk27.dim(` cp .env.${targetEnv}.local.example .env.${targetEnv}.local`));
5240
3996
  }
5241
3997
  console.log("");
5242
3998
  } catch (error) {
5243
- console.error(chalk28.red(`
3999
+ console.error(chalk27.red(`
5244
4000
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
5245
4001
  `));
5246
4002
  process.exit(1);
@@ -5248,12 +4004,12 @@ async function initEnvFiles(options) {
5248
4004
  }
5249
4005
  function writeEnvTemplate(cwd, file, vars, force) {
5250
4006
  const filePath = resolve2(cwd, file);
5251
- if (existsSync25(filePath) && !force) {
5252
- console.log(chalk28.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
4007
+ if (existsSync22(filePath) && !force) {
4008
+ console.log(chalk27.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
5253
4009
  return;
5254
4010
  }
5255
- writeFileSync19(filePath, generateEnvFileContent(vars), "utf-8");
5256
- console.log(chalk28.green(` \u2705 ${file} (${vars.length} variables)`));
4011
+ writeFileSync13(filePath, generateEnvFileContent(vars), "utf-8");
4012
+ console.log(chalk27.green(` \u2705 ${file} (${vars.length} variables)`));
5257
4013
  }
5258
4014
  function generateEnvFileContent(vars) {
5259
4015
  const lines = [
@@ -5288,7 +4044,7 @@ async function checkEnvFiles(options) {
5288
4044
  const envSchema = await loadEnvSchema(packageName);
5289
4045
  const allVars = Object.entries(envSchema);
5290
4046
  const envLabel = targetEnv ? ` (${targetEnv})` : "";
5291
- console.log(chalk28.blue.bold(`
4047
+ console.log(chalk27.blue.bold(`
5292
4048
  \u{1F50D} Checking .env files against schema${envLabel}
5293
4049
  `));
5294
4050
  const filesToCheck = targetEnv ? getEnvFilesForEnvironment(targetEnv) : [...BASE_ENV_FILES.nextjs, ...BASE_ENV_FILES.server];
@@ -5297,7 +4053,7 @@ async function checkEnvFiles(options) {
5297
4053
  const warnings = [];
5298
4054
  for (const file of filesToCheck) {
5299
4055
  const filePath = resolve2(cwd, file);
5300
- if (!existsSync25(filePath)) {
4056
+ if (!existsSync22(filePath)) {
5301
4057
  continue;
5302
4058
  }
5303
4059
  const content = readFileSync9(filePath, "utf-8");
@@ -5305,7 +4061,7 @@ async function checkEnvFiles(options) {
5305
4061
  for (const [key, value] of Object.entries(parsed)) {
5306
4062
  loadedEnv[key] = { value: value || "", file };
5307
4063
  }
5308
- console.log(chalk28.dim(` \u{1F4C4} ${file} loaded`));
4064
+ console.log(chalk27.dim(` \u{1F4C4} ${file} loaded`));
5309
4065
  }
5310
4066
  console.log("");
5311
4067
  for (const [key, schema] of allVars) {
@@ -5313,7 +4069,7 @@ async function checkEnvFiles(options) {
5313
4069
  const found = loadedEnv[key];
5314
4070
  if (!found) {
5315
4071
  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`);
4072
+ issues.push(`${chalk27.red("\u2717")} ${chalk27.cyan(key)} is required but not found in any .env file`);
5317
4073
  }
5318
4074
  continue;
5319
4075
  }
@@ -5323,11 +4079,11 @@ async function checkEnvFiles(options) {
5323
4079
  if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
5324
4080
  if (schema.sensitive) {
5325
4081
  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!)`
4082
+ `${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
4083
  );
5328
4084
  } else {
5329
4085
  warnings.push(
5330
- `${chalk28.yellow("\u26A0")} ${chalk28.cyan(key)} should be in ${chalk28.magenta(expectedFile)}, but found in ${chalk28.dim(found.file)}`
4086
+ `${chalk27.yellow("\u26A0")} ${chalk27.cyan(key)} should be in ${chalk27.magenta(expectedFile)}, but found in ${chalk27.dim(found.file)}`
5331
4087
  );
5332
4088
  }
5333
4089
  }
@@ -5335,34 +4091,34 @@ async function checkEnvFiles(options) {
5335
4091
  for (const [key, { file }] of Object.entries(loadedEnv)) {
5336
4092
  const inSchema = allVars.some(([k]) => k === key);
5337
4093
  if (!inSchema) {
5338
- warnings.push(`${chalk28.yellow("\u26A0")} ${chalk28.cyan(key)} in ${chalk28.dim(file)} is not in schema`);
4094
+ warnings.push(`${chalk27.yellow("\u26A0")} ${chalk27.cyan(key)} in ${chalk27.dim(file)} is not in schema`);
5339
4095
  }
5340
4096
  }
5341
4097
  if (issues.length > 0) {
5342
- console.log(chalk28.red.bold("Issues:"));
4098
+ console.log(chalk27.red.bold("Issues:"));
5343
4099
  for (const issue of issues) {
5344
4100
  console.log(` ${issue}`);
5345
4101
  }
5346
4102
  console.log("");
5347
4103
  }
5348
4104
  if (warnings.length > 0) {
5349
- console.log(chalk28.yellow.bold("Warnings:"));
4105
+ console.log(chalk27.yellow.bold("Warnings:"));
5350
4106
  for (const warning of warnings) {
5351
4107
  console.log(` ${warning}`);
5352
4108
  }
5353
4109
  console.log("");
5354
4110
  }
5355
4111
  if (issues.length === 0 && warnings.length === 0) {
5356
- console.log(chalk28.green("\u2705 All environment variables are correctly configured!\n"));
4112
+ console.log(chalk27.green("\u2705 All environment variables are correctly configured!\n"));
5357
4113
  } else {
5358
- console.log(chalk28.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
4114
+ console.log(chalk27.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
5359
4115
  `));
5360
4116
  if (issues.length > 0) {
5361
4117
  process.exit(1);
5362
4118
  }
5363
4119
  }
5364
4120
  } catch (error) {
5365
- console.error(chalk28.red(`
4121
+ console.error(chalk27.red(`
5366
4122
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
5367
4123
  `));
5368
4124
  process.exit(1);
@@ -5374,17 +4130,17 @@ async function validateEnvVars(options) {
5374
4130
  const packages = options.packages || ["@spfn/core"];
5375
4131
  const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
5376
4132
  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)}
4133
+ const { loadEnv: loadEnv9 } = await import("@spfn/core/env/loader");
4134
+ const result = loadEnv9({ nodeEnv: targetEnv });
4135
+ console.log(chalk27.blue.bold(`
4136
+ \u{1F50D} Validating environment variables for ${chalk27.cyan(targetEnv)}
5381
4137
  `));
5382
4138
  if (result.loadedFiles.length > 0) {
5383
- console.log(chalk28.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
4139
+ console.log(chalk27.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
5384
4140
  }
5385
4141
  console.log("");
5386
4142
  } else {
5387
- console.log(chalk28.blue.bold(`
4143
+ console.log(chalk27.blue.bold(`
5388
4144
  \u{1F50D} Validating environment variables
5389
4145
  `));
5390
4146
  }
@@ -5392,7 +4148,7 @@ async function validateEnvVars(options) {
5392
4148
  const allWarnings = [];
5393
4149
  for (const packageName of packages) {
5394
4150
  try {
5395
- console.log(chalk28.dim(` \u{1F4E6} ${packageName}`));
4151
+ console.log(chalk27.dim(` \u{1F4E6} ${packageName}`));
5396
4152
  const envSchema = await loadEnvSchema(packageName);
5397
4153
  const { createEnvRegistry } = await import("@spfn/core/env");
5398
4154
  const registry = createEnvRegistry(envSchema);
@@ -5405,10 +4161,10 @@ async function validateEnvVars(options) {
5405
4161
  }
5406
4162
  } catch (error) {
5407
4163
  if (error instanceof Error && error.message.includes("does not export envSchema")) {
5408
- console.log(chalk28.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
4164
+ console.log(chalk27.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
5409
4165
  continue;
5410
4166
  }
5411
- console.error(chalk28.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
4167
+ console.error(chalk27.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
5412
4168
  if (options.strict) {
5413
4169
  process.exit(1);
5414
4170
  }
@@ -5416,32 +4172,32 @@ async function validateEnvVars(options) {
5416
4172
  }
5417
4173
  console.log("");
5418
4174
  if (allErrors.length > 0) {
5419
- console.log(chalk28.red.bold(`\u274C Validation Errors (${allErrors.length}):
4175
+ console.log(chalk27.red.bold(`\u274C Validation Errors (${allErrors.length}):
5420
4176
  `));
5421
4177
  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}`)}`);
4178
+ console.log(` ${chalk27.red("\u2717")} ${chalk27.cyan(error.key)}`);
4179
+ console.log(` ${chalk27.dim(error.message)}`);
4180
+ console.log(` ${chalk27.dim(`from ${error.package}`)}`);
5425
4181
  console.log("");
5426
4182
  }
5427
4183
  }
5428
4184
  if (allWarnings.length > 0) {
5429
- console.log(chalk28.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
4185
+ console.log(chalk27.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
5430
4186
  `));
5431
4187
  for (const warning of allWarnings) {
5432
- console.log(` ${chalk28.yellow("\u26A0")} ${chalk28.cyan(warning.key)}`);
5433
- console.log(` ${chalk28.dim(warning.message)}`);
4188
+ console.log(` ${chalk27.yellow("\u26A0")} ${chalk27.cyan(warning.key)}`);
4189
+ console.log(` ${chalk27.dim(warning.message)}`);
5434
4190
  console.log("");
5435
4191
  }
5436
4192
  }
5437
4193
  if (allErrors.length === 0 && allWarnings.length === 0) {
5438
- console.log(chalk28.green.bold("\u2705 All environment variables are valid!\n"));
4194
+ console.log(chalk27.green.bold("\u2705 All environment variables are valid!\n"));
5439
4195
  } 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.
4196
+ console.log(chalk27.green("\u2705 No errors found."));
4197
+ console.log(chalk27.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
5442
4198
  `));
5443
4199
  } else {
5444
- console.log(chalk28.red(`
4200
+ console.log(chalk27.red(`
5445
4201
  \u274C Validation failed with ${allErrors.length} error(s)
5446
4202
  `));
5447
4203
  process.exit(1);
@@ -5451,12 +4207,11 @@ envCommand.command("validate").description("Validate environment variables again
5451
4207
 
5452
4208
  // src/index.ts
5453
4209
  init_version();
5454
- var program = new Command13();
4210
+ var program = new Command12();
5455
4211
  program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(getCliVersion());
5456
4212
  program.addCommand(createCommand);
5457
4213
  program.addCommand(initCommand);
5458
4214
  program.addCommand(addCommand);
5459
- program.addCommand(generateCommand);
5460
4215
  program.addCommand(devCommand);
5461
4216
  program.addCommand(buildCommand);
5462
4217
  program.addCommand(startCommand);