spfn 0.2.0-beta.52 → 0.2.0-beta.56

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
@@ -69,12 +69,24 @@ function detectPackageManager(cwd) {
69
69
  }
70
70
  return "npm";
71
71
  }
72
+ function getRunCommand(pm) {
73
+ return pm === "npm" ? "npm run" : `${pm} run`;
74
+ }
72
75
  var init_package_manager = __esm({
73
76
  "src/utils/package-manager.ts"() {
74
77
  "use strict";
75
78
  }
76
79
  });
77
80
 
81
+ // src/utils/messages.ts
82
+ var ENV_FILES_HINT;
83
+ var init_messages = __esm({
84
+ "src/utils/messages.ts"() {
85
+ "use strict";
86
+ ENV_FILES_HINT = "server secrets belong in .env.server \u2014 Next.js never loads it";
87
+ }
88
+ });
89
+
78
90
  // src/commands/setup.ts
79
91
  var setup_exports = {};
80
92
  __export(setup_exports, {
@@ -572,10 +584,78 @@ var init_api_proxy = __esm({
572
584
  }
573
585
  });
574
586
 
575
- // src/commands/init/steps/docker.ts
587
+ // src/commands/init/steps/next-config.ts
576
588
  import { existsSync as existsSync7, readFileSync as readFileSync2 } from "fs";
577
589
  import { join as join7 } from "path";
590
+ import chalk3 from "chalk";
578
591
  import fse4 from "fs-extra";
592
+ function printManualInstructions() {
593
+ logger.warn("Could not update next.config automatically.");
594
+ logger.info("@spfn/auth OAuth requires this rewrite in your next.config:\n");
595
+ console.log(chalk3.gray(AUTH_REWRITES_BLOCK + "\n"));
596
+ }
597
+ async function setupNextConfig(cwd, includeAuth) {
598
+ if (!includeAuth) {
599
+ return;
600
+ }
601
+ const configPath = ["next.config.ts", "next.config.js", "next.config.mjs"].map((name) => join7(cwd, name)).find(existsSync7);
602
+ if (!configPath) {
603
+ writeFileSync3(join7(cwd, "next.config.ts"), NEW_CONFIG_TEMPLATE);
604
+ logger.step("Created next.config.ts with the /_auth/* \u2192 SPFN API rewrite");
605
+ return;
606
+ }
607
+ const content = readFileSync2(configPath, "utf-8");
608
+ if (content.includes("/_auth/:path*")) {
609
+ logger.warn("next.config already forwards /_auth/*, skipping");
610
+ return;
611
+ }
612
+ if (content.includes("rewrites")) {
613
+ printManualInstructions();
614
+ return;
615
+ }
616
+ const patched = content.replace(
617
+ /(const\s+nextConfig[^=]*=\s*\{)(\s*\})?/,
618
+ (_match, opening, closing) => `${opening}
619
+ ${AUTH_REWRITES_BLOCK}${closing ? "\n}" : ""}`
620
+ );
621
+ if (patched === content) {
622
+ printManualInstructions();
623
+ return;
624
+ }
625
+ writeFileSync3(configPath, patched);
626
+ logger.step(`Added the /_auth/* \u2192 SPFN API rewrite to ${configPath.split("/").pop()}`);
627
+ }
628
+ var writeFileSync3, AUTH_REWRITES_BLOCK, NEW_CONFIG_TEMPLATE;
629
+ var init_next_config = __esm({
630
+ "src/commands/init/steps/next-config.ts"() {
631
+ "use strict";
632
+ init_logger();
633
+ ({ writeFileSync: writeFileSync3 } = fse4);
634
+ AUTH_REWRITES_BLOCK = ` async rewrites() {
635
+ // @spfn/auth: OAuth provider callbacks must return to this app's origin \u2014
636
+ // the oauth_csrf cookie is set here. Forward /_auth/* to the SPFN API.
637
+ return [
638
+ {
639
+ source: '/_auth/:path*',
640
+ destination: \`\${process.env.SPFN_API_URL || 'http://localhost:8790'}/_auth/:path*\`,
641
+ },
642
+ ];
643
+ },`;
644
+ NEW_CONFIG_TEMPLATE = `import type { NextConfig } from "next";
645
+
646
+ const nextConfig: NextConfig = {
647
+ ${AUTH_REWRITES_BLOCK}
648
+ };
649
+
650
+ export default nextConfig;
651
+ `;
652
+ }
653
+ });
654
+
655
+ // src/commands/init/steps/docker.ts
656
+ import { existsSync as existsSync8, readFileSync as readFileSync3 } from "fs";
657
+ import { join as join8 } from "path";
658
+ import fse5 from "fs-extra";
579
659
  function adaptDockerfileForPm(content, pm) {
580
660
  const adaptations = PM_DOCKERFILE_ADAPTATIONS[pm];
581
661
  if (!adaptations) {
@@ -585,11 +665,11 @@ function adaptDockerfileForPm(content, pm) {
585
665
  }
586
666
  async function setupDockerFiles(cwd, pm = "pnpm") {
587
667
  const templatesDir = findTemplatesPath();
588
- const dockerComposePath = join7(cwd, "docker-compose.yml");
589
- if (!existsSync7(dockerComposePath)) {
668
+ const dockerComposePath = join8(cwd, "docker-compose.yml");
669
+ if (!existsSync8(dockerComposePath)) {
590
670
  try {
591
- const dockerComposeTemplate = join7(templatesDir, "docker-compose.yml");
592
- if (existsSync7(dockerComposeTemplate)) {
671
+ const dockerComposeTemplate = join8(templatesDir, "docker-compose.yml");
672
+ if (existsSync8(dockerComposeTemplate)) {
593
673
  copySync2(dockerComposeTemplate, dockerComposePath);
594
674
  logger.success("Created docker-compose.yml (PostgreSQL + Redis)");
595
675
  }
@@ -598,27 +678,27 @@ async function setupDockerFiles(cwd, pm = "pnpm") {
598
678
  }
599
679
  }
600
680
  try {
601
- const dockerfilePath = join7(cwd, "Dockerfile");
602
- if (!existsSync7(dockerfilePath)) {
603
- const dockerfileTemplate = join7(templatesDir, "Dockerfile");
604
- if (existsSync7(dockerfileTemplate)) {
605
- const content = adaptDockerfileForPm(readFileSync2(dockerfileTemplate, "utf-8"), pm);
606
- writeFileSync3(dockerfilePath, content);
681
+ const dockerfilePath = join8(cwd, "Dockerfile");
682
+ if (!existsSync8(dockerfilePath)) {
683
+ const dockerfileTemplate = join8(templatesDir, "Dockerfile");
684
+ if (existsSync8(dockerfileTemplate)) {
685
+ const content = adaptDockerfileForPm(readFileSync3(dockerfileTemplate, "utf-8"), pm);
686
+ writeFileSync4(dockerfilePath, content);
607
687
  logger.success(`Created Dockerfile (${pm})`);
608
688
  }
609
689
  }
610
- const dockerignorePath = join7(cwd, ".dockerignore");
611
- if (!existsSync7(dockerignorePath)) {
612
- const dockerignoreTemplate = join7(templatesDir, ".dockerignore");
613
- if (existsSync7(dockerignoreTemplate)) {
690
+ const dockerignorePath = join8(cwd, ".dockerignore");
691
+ if (!existsSync8(dockerignorePath)) {
692
+ const dockerignoreTemplate = join8(templatesDir, ".dockerignore");
693
+ if (existsSync8(dockerignoreTemplate)) {
614
694
  copySync2(dockerignoreTemplate, dockerignorePath);
615
695
  logger.success("Created .dockerignore");
616
696
  }
617
697
  }
618
- const dockerComposeProdPath = join7(cwd, "docker-compose.production.yml");
619
- if (!existsSync7(dockerComposeProdPath)) {
620
- const dockerComposeProdTemplate = join7(templatesDir, "docker-compose.production.yml");
621
- if (existsSync7(dockerComposeProdTemplate)) {
698
+ const dockerComposeProdPath = join8(cwd, "docker-compose.production.yml");
699
+ if (!existsSync8(dockerComposeProdPath)) {
700
+ const dockerComposeProdTemplate = join8(templatesDir, "docker-compose.production.yml");
701
+ if (existsSync8(dockerComposeProdTemplate)) {
622
702
  copySync2(dockerComposeProdTemplate, dockerComposeProdPath);
623
703
  logger.success("Created docker-compose.production.yml");
624
704
  }
@@ -627,13 +707,13 @@ async function setupDockerFiles(cwd, pm = "pnpm") {
627
707
  logger.warn("Could not copy Docker files (you can create them manually)");
628
708
  }
629
709
  }
630
- var copySync2, writeFileSync3, PM_DOCKERFILE_ADAPTATIONS;
710
+ var copySync2, writeFileSync4, PM_DOCKERFILE_ADAPTATIONS;
631
711
  var init_docker = __esm({
632
712
  "src/commands/init/steps/docker.ts"() {
633
713
  "use strict";
634
714
  init_logger();
635
715
  init_templates();
636
- ({ copySync: copySync2, writeFileSync: writeFileSync3 } = fse4);
716
+ ({ copySync: copySync2, writeFileSync: writeFileSync4 } = fse5);
637
717
  PM_DOCKERFILE_ADAPTATIONS = {
638
718
  npm: [
639
719
  ["# Install pnpm\nRUN corepack enable pnpm\n\n", ""],
@@ -664,12 +744,12 @@ var init_docker = __esm({
664
744
  });
665
745
 
666
746
  // src/commands/init/steps/deployment-config.ts
667
- import { existsSync as existsSync8 } from "fs";
668
- import { join as join8 } from "path";
669
- import fse5 from "fs-extra";
747
+ import { existsSync as existsSync9 } from "fs";
748
+ import { join as join9 } from "path";
749
+ import fse6 from "fs-extra";
670
750
  async function setupDeploymentConfig(cwd, packageJson, packageManager) {
671
- const deploymentConfigPath = join8(cwd, "spfn.config.js");
672
- if (!existsSync8(deploymentConfigPath)) {
751
+ const deploymentConfigPath = join9(cwd, "spfn.config.js");
752
+ if (!existsSync9(deploymentConfigPath)) {
673
753
  try {
674
754
  const projectName = packageJson.name?.replace(/[@\/]/g, "-").toLowerCase() || cwd.split("/").pop()?.toLowerCase() || "my-app";
675
755
  const configContent = `/**
@@ -771,25 +851,25 @@ export default {
771
851
  }
772
852
  }
773
853
  `;
774
- writeFileSync4(deploymentConfigPath, configContent);
854
+ writeFileSync5(deploymentConfigPath, configContent);
775
855
  logger.success(`Created spfn.config.js (subdomain: ${projectName}.spfn.app)`);
776
856
  } catch (error) {
777
857
  logger.warn("Could not create spfn.config.js");
778
858
  }
779
859
  }
780
860
  }
781
- var writeFileSync4;
861
+ var writeFileSync5;
782
862
  var init_deployment_config = __esm({
783
863
  "src/commands/init/steps/deployment-config.ts"() {
784
864
  "use strict";
785
865
  init_logger();
786
- ({ writeFileSync: writeFileSync4 } = fse5);
866
+ ({ writeFileSync: writeFileSync5 } = fse6);
787
867
  }
788
868
  });
789
869
 
790
870
  // src/utils/version.ts
791
871
  function getCliVersion() {
792
- return "0.2.0-beta.52";
872
+ return "0.2.0-beta.56";
793
873
  }
794
874
  function getTagFromVersion(version) {
795
875
  const match = version.match(/-([a-z]+)\./i);
@@ -807,7 +887,7 @@ var init_version = __esm({
807
887
  // src/commands/init/steps/package.ts
808
888
  import ora3 from "ora";
809
889
  import { execa as execa2 } from "execa";
810
- import fse6 from "fs-extra";
890
+ import fse7 from "fs-extra";
811
891
  async function setupPackageJson(cwd, packageJsonPath, packageJson, packageManager, includeAuth) {
812
892
  const spinner = ora3("Updating package.json...").start();
813
893
  packageJson.dependencies = packageJson.dependencies || {};
@@ -842,7 +922,7 @@ async function setupPackageJson(cwd, packageJsonPath, packageJson, packageManage
842
922
  packageJson.scripts["spfn:start"] = "spfn start";
843
923
  packageJson.scripts["spfn:build"] = "spfn build";
844
924
  packageJson.scripts["codegen"] = "spfn codegen run";
845
- writeFileSync5(packageJsonPath, JSON.stringify(packageJson, null, 2));
925
+ writeFileSync6(packageJsonPath, JSON.stringify(packageJson, null, 2));
846
926
  spinner.succeed("package.json updated");
847
927
  spinner.start("Installing dependencies...");
848
928
  try {
@@ -855,24 +935,122 @@ async function setupPackageJson(cwd, packageJsonPath, packageJson, packageManage
855
935
  process.exit(1);
856
936
  }
857
937
  }
858
- var writeFileSync5;
938
+ var writeFileSync6;
859
939
  var init_package = __esm({
860
940
  "src/commands/init/steps/package.ts"() {
861
941
  "use strict";
862
942
  init_logger();
863
943
  init_version();
864
- ({ writeFileSync: writeFileSync5 } = fse6);
944
+ ({ writeFileSync: writeFileSync6 } = fse7);
945
+ }
946
+ });
947
+
948
+ // src/utils/env-file.ts
949
+ import { existsSync as existsSync10, readFileSync as readFileSync4, writeFileSync as writeFileSync7, chmodSync } from "fs";
950
+ import { join as join10 } from "path";
951
+ import { parse } from "dotenv";
952
+ function restrictEnvFilePerms(filePath) {
953
+ try {
954
+ chmodSync(filePath, 384);
955
+ } catch {
956
+ }
957
+ }
958
+ function envKeyOf(line) {
959
+ const match = line.match(/^\s*(?:export\s+)?([A-Z_][A-Z0-9_]*)\s*=/);
960
+ return match ? match[1] : null;
961
+ }
962
+ function collectDeclaredKeys(content) {
963
+ const keys = /* @__PURE__ */ new Set();
964
+ for (const line of content.split("\n")) {
965
+ const key = envKeyOf(line.replace(/^\s*#\s*/, ""));
966
+ if (key !== null) {
967
+ keys.add(key);
968
+ }
969
+ }
970
+ return keys;
971
+ }
972
+ function parseEnvFile(filePath) {
973
+ if (!existsSync10(filePath)) {
974
+ return {};
975
+ }
976
+ return parse(readFileSync4(filePath, "utf-8"));
977
+ }
978
+ function upsertEnvVar(filePath, key, value) {
979
+ const existing = existsSync10(filePath) ? readFileSync4(filePath, "utf-8") : "";
980
+ const line = `${key}=${value}`;
981
+ let replaced = false;
982
+ const next = existing.split("\n").map((current) => {
983
+ if (!replaced && envKeyOf(current) === key) {
984
+ replaced = true;
985
+ return line;
986
+ }
987
+ return current;
988
+ });
989
+ if (replaced) {
990
+ writeFileSync7(filePath, next.join("\n"));
991
+ return "updated";
992
+ }
993
+ const base = existing.length > 0 && !existing.endsWith("\n") ? existing + "\n" : existing;
994
+ writeFileSync7(filePath, base + line + "\n");
995
+ return "added";
996
+ }
997
+ function gitignoreCovers(lines, target) {
998
+ const normalized = stripSlashes(target);
999
+ let covered = false;
1000
+ for (const raw of lines) {
1001
+ const line = raw.trim();
1002
+ if (line === "" || line.startsWith("#")) {
1003
+ continue;
1004
+ }
1005
+ const negation = line.startsWith("!");
1006
+ const rule = stripSlashes(negation ? line.slice(1) : line);
1007
+ if (rule === normalized || rule.endsWith("*") && normalized.startsWith(rule.slice(0, -1))) {
1008
+ covered = !negation;
1009
+ }
1010
+ }
1011
+ return covered;
1012
+ }
1013
+ function stripSlashes(value) {
1014
+ return value.replace(/^\//, "").replace(/\/$/, "");
1015
+ }
1016
+ function ensureGitignored(cwd, entries) {
1017
+ const gitignorePath = join10(cwd, ".gitignore");
1018
+ const content = existsSync10(gitignorePath) ? readFileSync4(gitignorePath, "utf-8") : "";
1019
+ const lines = content.split("\n");
1020
+ const added = [];
1021
+ let appended = "";
1022
+ for (const { pattern, comment } of entries) {
1023
+ if (!gitignoreCovers(lines, pattern)) {
1024
+ appended += `
1025
+ # ${comment}
1026
+ ${pattern}
1027
+ `;
1028
+ added.push(pattern);
1029
+ }
1030
+ }
1031
+ if (appended.length > 0) {
1032
+ writeFileSync7(gitignorePath, content + appended);
1033
+ }
1034
+ return added;
1035
+ }
1036
+ var init_env_file = __esm({
1037
+ "src/utils/env-file.ts"() {
1038
+ "use strict";
865
1039
  }
866
1040
  });
867
1041
 
868
1042
  // src/commands/init/steps/config-files.ts
869
- import { existsSync as existsSync9, readFileSync as readFileSync3 } from "fs";
870
- import { join as join9 } from "path";
871
- import fse7 from "fs-extra";
1043
+ import { existsSync as existsSync11, readFileSync as readFileSync5 } from "fs";
1044
+ import { join as join11 } from "path";
1045
+ import fse8 from "fs-extra";
1046
+ function withPlaceholderCreds(text) {
1047
+ return text.replace(/(postgresql:\/\/)[^@\s/]+@/g, "$1user:password@");
1048
+ }
872
1049
  async function setupConfigFiles(cwd) {
873
- generateEnvExamples(cwd);
874
- const spfnrcPath = join9(cwd, ".spfnrc.ts");
875
- if (!existsSync9(spfnrcPath)) {
1050
+ updateGitignore(cwd);
1051
+ generateEnvFiles(cwd);
1052
+ const spfnrcPath = join11(cwd, ".spfnrc.ts");
1053
+ if (!existsSync11(spfnrcPath)) {
876
1054
  const spfnrcContent = `import { defineConfig, defineGenerator } from '@spfn/core/codegen';
877
1055
 
878
1056
  /**
@@ -893,136 +1071,207 @@ export default defineConfig({
893
1071
  ]
894
1072
  });
895
1073
  `;
896
- writeFileSync6(spfnrcPath, spfnrcContent);
1074
+ writeFileSync8(spfnrcPath, spfnrcContent);
897
1075
  logger.success("Created .spfnrc.ts (codegen configuration)");
898
1076
  }
899
- updateGitignore(cwd);
900
1077
  updateTsconfig(cwd);
901
1078
  }
902
- function generateEnvExamples(cwd) {
903
- writeEnvExample(cwd, ".env.example", `# Shared defaults (committed)
904
- # These values are shared across all environments.
905
-
906
- # Environment
907
- NODE_ENV=local
908
-
909
- # Logging
910
- SPFN_LOG_LEVEL=info
911
-
912
- # Server
913
- PORT=4000
914
-
915
- # SPFN API Server URL (for API Route Proxy and SSR)
916
- SPFN_API_URL=http://localhost:8790
917
- NEXT_PUBLIC_SPFN_API_URL=http://localhost:8790
918
- `);
919
- writeEnvExample(cwd, ".env.local.example", `# Local overrides (gitignored)
920
- # Developer-specific values that should NOT be committed.
921
-
922
- # Database (matches docker-compose.yml)
923
- DATABASE_URL=postgresql://spfn:spfn@localhost:5432/spfn_dev
924
-
925
- # Cache - Redis/Valkey (optional)
926
- CACHE_URL=redis://localhost:6379
927
-
928
- # SPFN App URL (optional, for CORS and redirects)
929
- # SPFN_APP_URL=http://localhost:3790
930
- `);
931
- writeEnvExample(cwd, ".env.server.example", `# Server-only environment (template)
932
- # Copy to .env.server (gitignored) and fill in real values.
933
- # These values are only loaded by the SPFN server, not by Next.js.
934
-
935
- # Database pool
936
- DB_POOL_MAX=10
937
- DB_POOL_IDLE_TIMEOUT=30
938
-
939
- # Server timeouts
940
- SERVER_TIMEOUT=120000
941
- SHUTDOWN_TIMEOUT=30000
942
-
943
- # --- Secrets (never commit .env.server) ---
944
-
945
- # Database write/read URLs (master-replica pattern, optional)
946
- # DATABASE_WRITE_URL=postgresql://user:password@master:5432/dbname
947
- # DATABASE_READ_URL=postgresql://user:password@replica:5432/dbname
948
-
949
- # Cache password (optional)
950
- # CACHE_PASSWORD=your-redis-password
951
- `);
1079
+ function generateEnvFiles(cwd) {
1080
+ writeEnvFile(cwd, ".env.local", ENV_LOCAL_TEMPLATE);
1081
+ writeServerEnv(cwd);
1082
+ writeExampleEnv(cwd, ENV_EXAMPLE_TEMPLATE);
952
1083
  }
953
- function writeEnvExample(cwd, filename, content) {
954
- const filePath = join9(cwd, filename);
955
- if (existsSync9(filePath)) {
1084
+ function writeServerEnv(cwd) {
1085
+ const filePath = join11(cwd, ".env.server");
1086
+ if (!existsSync11(filePath)) {
1087
+ writeFileSync8(filePath, ENV_SERVER_TEMPLATE);
1088
+ restrictEnvFilePerms(filePath);
1089
+ logger.success("Created .env.server");
956
1090
  return;
957
1091
  }
958
- writeFileSync6(filePath, content);
959
- logger.success(`Created ${filename}`);
1092
+ writeFileSync8(join11(cwd, ".env.server.example"), withPlaceholderCreds(ENV_SERVER_TEMPLATE));
1093
+ logger.warn(".env.server already exists \u2014 left it untouched; wrote SPFN's reference to .env.server.example, add any missing keys manually");
960
1094
  }
961
- function updateGitignore(cwd) {
962
- const gitignorePath = join9(cwd, ".gitignore");
963
- if (!existsSync9(gitignorePath)) {
1095
+ function writeExampleEnv(cwd, content) {
1096
+ const filePath = join11(cwd, ".env.example");
1097
+ if (existsSync11(filePath)) {
964
1098
  return;
965
1099
  }
966
- try {
967
- const content = readFileSync3(gitignorePath, "utf-8");
968
- let updated = content;
969
- let changed = false;
970
- if (!content.includes(".spfn")) {
971
- updated = updated.replace(
972
- /# production\n\/build/,
973
- "# production\n/build\n\n# spfn\n/.spfn/"
974
- );
975
- changed = true;
976
- }
977
- if (!content.includes(".env.local") && !content.includes(".env.*.local")) {
978
- updated += `
979
- # environment secrets (local overrides)
980
- .env.local
981
- .env.*.local
982
- `;
983
- changed = true;
984
- }
985
- if (!content.includes(".env.server")) {
986
- updated += `
987
- # spfn server env (secrets)
988
- .env.server
1100
+ writeFileSync8(filePath, content);
1101
+ logger.success("Created .env.example (committed reference)");
1102
+ }
1103
+ function writeEnvFile(cwd, filename, content) {
1104
+ const filePath = join11(cwd, filename);
1105
+ if (!existsSync11(filePath)) {
1106
+ writeFileSync8(filePath, content);
1107
+ logger.success(`Created ${filename}`);
1108
+ return;
1109
+ }
1110
+ mergeMissingEnvKeys(filePath, filename, content);
1111
+ }
1112
+ function mergeMissingEnvKeys(filePath, filename, template) {
1113
+ const existing = readFileSync5(filePath, "utf-8");
1114
+ const existingKeys = collectDeclaredKeys(existing);
1115
+ const missing = template.split("\n").filter((line) => {
1116
+ const key = envKeyOf(line);
1117
+ return key !== null && !existingKeys.has(key);
1118
+ });
1119
+ if (missing.length === 0) {
1120
+ return;
1121
+ }
1122
+ const block = `
1123
+ # Added by spfn init
1124
+ ${missing.join("\n")}
989
1125
  `;
990
- changed = true;
991
- }
992
- if (changed) {
993
- writeFileSync6(gitignorePath, updated);
994
- logger.success("Updated .gitignore with .spfn directory and env patterns");
1126
+ writeFileSync8(filePath, existing.replace(/\n*$/, "\n") + block);
1127
+ logger.success(`Updated ${filename} (added ${missing.length} SPFN key(s))`);
1128
+ }
1129
+ function updateGitignore(cwd) {
1130
+ const gitignorePath = join11(cwd, ".gitignore");
1131
+ const existed = existsSync11(gitignorePath);
1132
+ try {
1133
+ const content = existed ? readFileSync5(gitignorePath, "utf-8") : "";
1134
+ const additions = pendingIgnoreRules(content);
1135
+ if (additions.length === 0) {
1136
+ return;
995
1137
  }
996
- } catch (error) {
997
- logger.warn("Could not update .gitignore (you can add patterns manually)");
1138
+ writeFileSync8(gitignorePath, content + additions.join(""));
1139
+ logger.success(existed ? "Updated .gitignore with .spfn directory and env patterns" : "Created .gitignore with .spfn directory and env patterns");
1140
+ } catch {
1141
+ logger.warn("Could not update .gitignore \u2014 add .env.local and .env.server manually before committing, so the generated .env.server secrets are not tracked");
1142
+ }
1143
+ }
1144
+ function pendingIgnoreRules(content) {
1145
+ const lines = content.split("\n");
1146
+ const rules = [];
1147
+ if (!gitignoreCovers(lines, ".spfn")) {
1148
+ rules.push("\n# spfn\n/.spfn/\n");
1149
+ }
1150
+ if (!gitignoreCovers(lines, ".env.local")) {
1151
+ rules.push("\n# environment secrets (local overrides)\n.env.local\n");
1152
+ }
1153
+ if (!gitignoreCovers(lines, ".env.*.local")) {
1154
+ rules.push("\n# environment secrets (env-specific local overrides)\n.env.*.local\n");
1155
+ }
1156
+ if (!gitignoreCovers(lines, ".env.server")) {
1157
+ rules.push("\n# spfn server env (secrets)\n.env.server\n");
998
1158
  }
1159
+ if (!lines.some((line) => line.trim() === "!.env.example")) {
1160
+ rules.push("\n# spfn env reference (committed)\n!.env.example\n");
1161
+ }
1162
+ return rules;
999
1163
  }
1000
1164
  function updateTsconfig(cwd) {
1001
- const tsconfigPath = join9(cwd, "tsconfig.json");
1002
- if (!existsSync9(tsconfigPath)) {
1165
+ const tsconfigPath = join11(cwd, "tsconfig.json");
1166
+ if (!existsSync11(tsconfigPath)) {
1003
1167
  return;
1004
1168
  }
1005
1169
  try {
1006
- const tsconfigContent = readFileSync3(tsconfigPath, "utf-8");
1170
+ const tsconfigContent = readFileSync5(tsconfigPath, "utf-8");
1007
1171
  const tsconfig = JSON.parse(tsconfigContent);
1008
1172
  if (!tsconfig.exclude) {
1009
1173
  tsconfig.exclude = [];
1010
1174
  }
1011
1175
  if (!tsconfig.exclude.includes("src/server")) {
1012
1176
  tsconfig.exclude.push("src/server");
1013
- writeFileSync6(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
1177
+ writeFileSync8(tsconfigPath, JSON.stringify(tsconfig, null, 2) + "\n");
1014
1178
  logger.success("Updated tsconfig.json (excluded src/server for Vercel compatibility)");
1015
1179
  }
1016
- } catch (error) {
1180
+ } catch {
1017
1181
  logger.warn('Could not update tsconfig.json (you can add "src/server" to exclude manually)');
1018
1182
  }
1019
1183
  }
1020
- var writeFileSync6;
1184
+ var writeFileSync8, ENV_LOCAL_TEMPLATE, ENV_SERVER_TEMPLATE, ENV_EXAMPLE_TEMPLATE;
1021
1185
  var init_config_files = __esm({
1022
1186
  "src/commands/init/steps/config-files.ts"() {
1023
1187
  "use strict";
1024
1188
  init_logger();
1025
- ({ writeFileSync: writeFileSync6 } = fse7);
1189
+ init_env_file();
1190
+ ({ writeFileSync: writeFileSync8 } = fse8);
1191
+ ENV_LOCAL_TEMPLATE = `# Next.js environment
1192
+ # Loaded by Next.js (and the SPFN backend). Only non-secret, Next.js-facing
1193
+ # values belong here \u2014 server secrets go in .env.server.
1194
+
1195
+ # SPFN API endpoint \u2014 browser + Next.js SSR/proxy target
1196
+ SPFN_API_URL=http://localhost:8790
1197
+ NEXT_PUBLIC_SPFN_API_URL=http://localhost:8790
1198
+
1199
+ # Next.js app URL (used by the SPFN server for CORS/redirects)
1200
+ SPFN_APP_URL=http://localhost:3790
1201
+ `;
1202
+ ENV_SERVER_TEMPLATE = `# SPFN backend environment
1203
+ # Loaded ONLY by the SPFN server, never by Next.js. Keep all server-only config
1204
+ # and secrets here so they never reach the Next.js process.
1205
+
1206
+ # Environment
1207
+ NODE_ENV=local
1208
+
1209
+ # Logging
1210
+ SPFN_LOG_LEVEL=info
1211
+
1212
+ # Database (matches docker-compose.yml) \u2014 secret, server-only
1213
+ DATABASE_URL=postgresql://spfn:spfn@localhost:5432/spfn_dev
1214
+
1215
+ # Cache \u2014 Redis/Valkey (optional)
1216
+ CACHE_URL=redis://localhost:6379
1217
+
1218
+ # Database pool
1219
+ DB_POOL_MAX=10
1220
+ DB_POOL_IDLE_TIMEOUT=30
1221
+
1222
+ # --- Optional secrets (uncomment and set as needed) ---
1223
+ # DATABASE_WRITE_URL=postgresql://user:password@master:5432/dbname
1224
+ # DATABASE_READ_URL=postgresql://user:password@replica:5432/dbname
1225
+ # CACHE_PASSWORD=your-redis-password
1226
+ `;
1227
+ ENV_EXAMPLE_TEMPLATE = withPlaceholderCreds(`# Example environment \u2014 committed reference for the variables SPFN uses.
1228
+ # Real values live in .env.local (Next.js) and .env.server (backend secrets),
1229
+ # both gitignored. This file documents the keys; it is not loaded by anything.
1230
+
1231
+ ${ENV_LOCAL_TEMPLATE}
1232
+ ${ENV_SERVER_TEMPLATE}`);
1233
+ }
1234
+ });
1235
+
1236
+ // src/commands/init/steps/readme.ts
1237
+ import { existsSync as existsSync12, readFileSync as readFileSync6 } from "fs";
1238
+ import { basename, join as join12 } from "path";
1239
+ import fse9 from "fs-extra";
1240
+ async function setupReadme(cwd, ctx) {
1241
+ const readmePath = join12(cwd, "README.md");
1242
+ if (existsSync12(readmePath) && !ctx.overwrite) {
1243
+ logger.info("README.md already exists \u2014 keeping it");
1244
+ return;
1245
+ }
1246
+ const templatePath = join12(findTemplatesPath(), "README.md");
1247
+ let template;
1248
+ try {
1249
+ template = readFileSync6(templatePath, "utf-8");
1250
+ } catch {
1251
+ logger.warn("README template not found \u2014 skipping README generation");
1252
+ return;
1253
+ }
1254
+ const content = renderReadme(template, {
1255
+ projectName: ctx.packageJson.name || basename(cwd) || "my-app",
1256
+ pm: ctx.pm,
1257
+ pmRun: getRunCommand(ctx.pm),
1258
+ includeAuth: ctx.includeAuth
1259
+ });
1260
+ writeFileSync9(readmePath, content);
1261
+ logger.success("Created README.md");
1262
+ }
1263
+ function renderReadme(template, vars) {
1264
+ const authBlock = /\r?\n?<!-- \{\{#auth\}\} -->\r?\n([\s\S]*?)\r?\n<!-- \{\{\/auth\}\} -->\r?\n/;
1265
+ return template.replace(authBlock, vars.includeAuth ? "\n$1\n" : "\n").replaceAll("{{projectName}}", () => vars.projectName).replaceAll("{{pmRun}}", () => vars.pmRun).replaceAll("{{pm}}", () => vars.pm);
1266
+ }
1267
+ var writeFileSync9;
1268
+ var init_readme = __esm({
1269
+ "src/commands/init/steps/readme.ts"() {
1270
+ "use strict";
1271
+ init_logger();
1272
+ init_package_manager();
1273
+ init_templates();
1274
+ ({ writeFileSync: writeFileSync9 } = fse9);
1026
1275
  }
1027
1276
  });
1028
1277
 
@@ -1033,7 +1282,7 @@ __export(init_exports, {
1033
1282
  initializeSpfn: () => initializeSpfn
1034
1283
  });
1035
1284
  import { Command as Command2 } from "commander";
1036
- import chalk3 from "chalk";
1285
+ import chalk4 from "chalk";
1037
1286
  async function initializeSpfn(options = {}) {
1038
1287
  const cwd = process.cwd();
1039
1288
  const { packageJson, packageJsonPath, includeAuth } = await validateProject(cwd, options.yes || false);
@@ -1041,26 +1290,28 @@ async function initializeSpfn(options = {}) {
1041
1290
  logger.step(`Detected package manager: ${pm}`);
1042
1291
  await setupServerStructure(cwd);
1043
1292
  await setupApiProxy(cwd, includeAuth);
1293
+ await setupNextConfig(cwd, includeAuth);
1044
1294
  await setupDockerFiles(cwd, pm);
1045
1295
  await setupDeploymentConfig(cwd, packageJson, pm);
1046
1296
  await setupPackageJson(cwd, packageJsonPath, packageJson, pm, includeAuth);
1047
1297
  await setupConfigFiles(cwd);
1048
- console.log("\n" + chalk3.green.bold("\u2713 SPFN initialized successfully!\n"));
1298
+ await setupReadme(cwd, { pm, packageJson, includeAuth, overwrite: options.overwriteReadme ?? false });
1299
+ console.log("\n" + chalk4.green.bold("\u2713 SPFN initialized successfully!\n"));
1049
1300
  console.log("Next steps:");
1050
1301
  console.log(" 1. Start PostgreSQL & Redis (if not installed locally):");
1051
- console.log(" " + chalk3.cyan("docker compose up -d"));
1052
- console.log(" 2. Copy .env.local.example to .env.local");
1053
- console.log(" " + chalk3.cyan("cp .env.local.example .env.local"));
1054
- console.log(" 3. Run: " + chalk3.cyan(pm === "npm" ? "npm run spfn:dev" : `${pm} run spfn:dev`));
1302
+ console.log(" " + chalk4.cyan("docker compose up -d"));
1303
+ console.log(" 2. Review the generated env files (.env.local, .env.server)");
1304
+ console.log(" " + chalk4.dim(ENV_FILES_HINT));
1305
+ console.log(" 3. Run: " + chalk4.cyan(`${getRunCommand(pm)} spfn:dev`));
1055
1306
  console.log(" 4. Visit:");
1056
- console.log(" - Next.js: " + chalk3.cyan("http://localhost:3790"));
1057
- console.log(" - API: " + chalk3.cyan("http://localhost:8790/health"));
1307
+ console.log(" - Next.js: " + chalk4.cyan("http://localhost:3790"));
1308
+ console.log(" - API: " + chalk4.cyan("http://localhost:8790/health"));
1058
1309
  console.log("\nAvailable commands:");
1059
- console.log(" \u2022 " + chalk3.cyan(pm === "npm" ? "npm run spfn:dev" : `${pm} spfn:dev`) + " - Start SPFN + Next.js");
1060
- console.log(" \u2022 " + chalk3.cyan("spfn env:validate") + " - Validate environment variables");
1061
- console.log(" \u2022 " + chalk3.cyan("spfn env:docs") + " - Generate env documentation");
1062
- console.log(" \u2022 " + chalk3.cyan("spfn env:check") + " - Check environment status");
1063
- console.log("\n" + chalk3.blue("\u{1F4A1} Tip:") + " Edit " + chalk3.cyan("src/server/config/env.config.ts") + " to manage environment variables");
1310
+ console.log(" \u2022 " + chalk4.cyan(`${getRunCommand(pm)} spfn:dev`) + " - Start SPFN + Next.js");
1311
+ console.log(" \u2022 " + chalk4.cyan("spfn env validate") + " - Validate environment variables");
1312
+ console.log(" \u2022 " + chalk4.cyan("spfn env list") + " - List env vars from the schema");
1313
+ console.log(" \u2022 " + chalk4.cyan("spfn secret set") + " - Store a secret (keychain / SOPS)");
1314
+ console.log("\n" + chalk4.blue("\u{1F4A1} Tip:") + " Edit " + chalk4.cyan("src/server/config/env.config.ts") + " to manage environment variables");
1064
1315
  }
1065
1316
  var initCommand;
1066
1317
  var init_init = __esm({
@@ -1068,13 +1319,16 @@ var init_init = __esm({
1068
1319
  "use strict";
1069
1320
  init_package_manager();
1070
1321
  init_logger();
1322
+ init_messages();
1071
1323
  init_validate();
1072
1324
  init_server_structure();
1073
1325
  init_api_proxy();
1326
+ init_next_config();
1074
1327
  init_docker();
1075
1328
  init_deployment_config();
1076
1329
  init_package();
1077
1330
  init_config_files();
1331
+ init_readme();
1078
1332
  initCommand = new Command2("init").description("Initialize SPFN in your Next.js project").option("-y, --yes", "Skip prompts and use defaults").action(initializeSpfn);
1079
1333
  }
1080
1334
  });
@@ -1085,38 +1339,38 @@ __export(function_migrations_exports, {
1085
1339
  discoverFunctionMigrations: () => discoverFunctionMigrations,
1086
1340
  executeFunctionMigrations: () => executeFunctionMigrations
1087
1341
  });
1088
- import chalk12 from "chalk";
1089
- import { join as join15 } from "path";
1342
+ import chalk13 from "chalk";
1343
+ import { join as join19 } from "path";
1090
1344
  import { env as env2 } from "@spfn/core/config";
1091
1345
  import { loadEnv as loadEnv2 } from "@spfn/core/server";
1092
- import { existsSync as existsSync16, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
1346
+ import { existsSync as existsSync19, readdirSync as readdirSync2, readFileSync as readFileSync9 } from "fs";
1093
1347
  function discoverFunctionMigrations(cwd = process.cwd()) {
1094
- const nodeModulesPath = join15(cwd, "node_modules");
1095
- if (!existsSync16(nodeModulesPath)) {
1348
+ const nodeModulesPath = join19(cwd, "node_modules");
1349
+ if (!existsSync19(nodeModulesPath)) {
1096
1350
  return [];
1097
1351
  }
1098
1352
  const functions = [];
1099
- const spfnDir = join15(nodeModulesPath, "@spfn");
1100
- if (!existsSync16(spfnDir)) {
1353
+ const spfnDir = join19(nodeModulesPath, "@spfn");
1354
+ if (!existsSync19(spfnDir)) {
1101
1355
  return [];
1102
1356
  }
1103
1357
  const packages = readdirSync2(spfnDir);
1104
1358
  for (const pkg of packages) {
1105
- const packagePath = join15(spfnDir, pkg);
1106
- const packageJsonPath = join15(packagePath, "package.json");
1107
- if (!existsSync16(packageJsonPath)) {
1359
+ const packagePath = join19(spfnDir, pkg);
1360
+ const packageJsonPath = join19(packagePath, "package.json");
1361
+ if (!existsSync19(packageJsonPath)) {
1108
1362
  continue;
1109
1363
  }
1110
1364
  try {
1111
- const packageJson = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
1365
+ const packageJson = JSON.parse(readFileSync9(packageJsonPath, "utf-8"));
1112
1366
  const spfnConfig = packageJson.spfn;
1113
1367
  if (!spfnConfig?.migrations) {
1114
1368
  continue;
1115
1369
  }
1116
- const migrationsDir = join15(packagePath, spfnConfig.migrations.dir);
1117
- if (!existsSync16(migrationsDir)) {
1370
+ const migrationsDir = join19(packagePath, spfnConfig.migrations.dir);
1371
+ if (!existsSync19(migrationsDir)) {
1118
1372
  console.warn(
1119
- chalk12.yellow(`\u26A0\uFE0F Package @spfn/${pkg} specifies migrations but directory not found: ${migrationsDir}`)
1373
+ chalk13.yellow(`\u26A0\uFE0F Package @spfn/${pkg} specifies migrations but directory not found: ${migrationsDir}`)
1120
1374
  );
1121
1375
  continue;
1122
1376
  }
@@ -1126,7 +1380,7 @@ function discoverFunctionMigrations(cwd = process.cwd()) {
1126
1380
  packagePath
1127
1381
  });
1128
1382
  } catch (error) {
1129
- console.warn(chalk12.yellow(`\u26A0\uFE0F Failed to parse package.json for @spfn/${pkg}`));
1383
+ console.warn(chalk13.yellow(`\u26A0\uFE0F Failed to parse package.json for @spfn/${pkg}`));
1130
1384
  }
1131
1385
  }
1132
1386
  return functions;
@@ -1141,7 +1395,7 @@ async function migrateLegacyTable(db, functionMigrations) {
1141
1395
  if (!legacyCheck[0]?.exists) {
1142
1396
  return;
1143
1397
  }
1144
- console.log(chalk12.dim("\n Migrating legacy shared migration table to per-package tables..."));
1398
+ console.log(chalk13.dim("\n Migrating legacy shared migration table to per-package tables..."));
1145
1399
  const legacyRows = await db.execute(
1146
1400
  `SELECT hash, created_at FROM drizzle."__spfn_fn_migrations" ORDER BY id`
1147
1401
  );
@@ -1150,11 +1404,11 @@ async function migrateLegacyTable(db, functionMigrations) {
1150
1404
  return;
1151
1405
  }
1152
1406
  for (const func of functionMigrations) {
1153
- const journalPath = join15(func.migrationsDir, "meta", "_journal.json");
1154
- if (!existsSync16(journalPath)) {
1407
+ const journalPath = join19(func.migrationsDir, "meta", "_journal.json");
1408
+ if (!existsSync19(journalPath)) {
1155
1409
  continue;
1156
1410
  }
1157
- const journal = JSON.parse(readFileSync6(journalPath, "utf-8"));
1411
+ const journal = JSON.parse(readFileSync9(journalPath, "utf-8"));
1158
1412
  const entries = journal.entries || [];
1159
1413
  const tableName = `__spfn_fn_${func.packageName.replace("@spfn/", "")}_migrations`;
1160
1414
  await db.execute(
@@ -1176,11 +1430,11 @@ async function migrateLegacyTable(db, functionMigrations) {
1176
1430
  const { createHash } = await import("crypto");
1177
1431
  let copied = 0;
1178
1432
  for (const entry of entries) {
1179
- const sqlPath = join15(func.migrationsDir, `${entry.tag}.sql`);
1180
- if (!existsSync16(sqlPath)) {
1433
+ const sqlPath = join19(func.migrationsDir, `${entry.tag}.sql`);
1434
+ if (!existsSync19(sqlPath)) {
1181
1435
  continue;
1182
1436
  }
1183
- const sqlContent = readFileSync6(sqlPath, "utf-8");
1437
+ const sqlContent = readFileSync9(sqlPath, "utf-8");
1184
1438
  const hash = createHash("sha256").update(sqlContent).digest("hex");
1185
1439
  const found = legacyRows.find((r) => r.hash === hash);
1186
1440
  if (found) {
@@ -1191,11 +1445,11 @@ async function migrateLegacyTable(db, functionMigrations) {
1191
1445
  }
1192
1446
  }
1193
1447
  if (copied > 0) {
1194
- console.log(chalk12.dim(` \u2713 ${func.packageName}: copied ${copied} migration record(s)`));
1448
+ console.log(chalk13.dim(` \u2713 ${func.packageName}: copied ${copied} migration record(s)`));
1195
1449
  }
1196
1450
  }
1197
1451
  await db.execute(`DROP TABLE drizzle."__spfn_fn_migrations"`);
1198
- console.log(chalk12.dim(" \u2713 Legacy migration table removed\n"));
1452
+ console.log(chalk13.dim(" \u2713 Legacy migration table removed\n"));
1199
1453
  }
1200
1454
  async function executeFunctionMigrations(functionMigrations) {
1201
1455
  let executedCount = 0;
@@ -1211,14 +1465,14 @@ async function executeFunctionMigrations(functionMigrations) {
1211
1465
  try {
1212
1466
  await migrateLegacyTable(db, functionMigrations);
1213
1467
  for (const func of functionMigrations) {
1214
- console.log(chalk12.blue(`
1468
+ console.log(chalk13.blue(`
1215
1469
  \u{1F4E6} Running ${func.packageName} migrations...`));
1216
1470
  const tableName = `__spfn_fn_${func.packageName.replace("@spfn/", "")}_migrations`;
1217
1471
  await migrate(db, {
1218
1472
  migrationsFolder: func.migrationsDir,
1219
1473
  migrationsTable: tableName
1220
1474
  });
1221
- console.log(chalk12.green(` \u2713 ${func.packageName} migrations applied`));
1475
+ console.log(chalk13.green(` \u2713 ${func.packageName} migrations applied`));
1222
1476
  executedCount++;
1223
1477
  }
1224
1478
  } finally {
@@ -1233,22 +1487,23 @@ var init_function_migrations = __esm({
1233
1487
  });
1234
1488
 
1235
1489
  // src/index.ts
1236
- import { Command as Command12 } from "commander";
1490
+ import { Command as Command13 } from "commander";
1237
1491
 
1238
1492
  // src/commands/create.ts
1239
1493
  init_logger();
1240
1494
  init_package_manager();
1495
+ init_messages();
1241
1496
  import { Command as Command3 } from "commander";
1242
- import { existsSync as existsSync10 } from "fs";
1243
- import { join as join10, resolve, dirname as dirname2 } from "path";
1497
+ import { existsSync as existsSync13 } from "fs";
1498
+ import { join as join13, resolve, dirname as dirname2 } from "path";
1244
1499
  import prompts2 from "prompts";
1245
1500
  import ora4 from "ora";
1246
1501
  import { execa as execa3 } from "execa";
1247
- import chalk4 from "chalk";
1502
+ import chalk5 from "chalk";
1248
1503
  function findPnpmWorkspaceRoot(startDir) {
1249
1504
  let dir = resolve(startDir);
1250
1505
  while (true) {
1251
- if (existsSync10(join10(dir, "pnpm-workspace.yaml"))) {
1506
+ if (existsSync13(join13(dir, "pnpm-workspace.yaml"))) {
1252
1507
  return dir;
1253
1508
  }
1254
1509
  const parent = dirname2(dir);
@@ -1259,12 +1514,12 @@ function findPnpmWorkspaceRoot(startDir) {
1259
1514
  }
1260
1515
  async function createProject(projectName, options) {
1261
1516
  const cwd = process.cwd();
1262
- const projectPath = join10(cwd, projectName);
1263
- if (existsSync10(projectPath)) {
1517
+ const projectPath = join13(cwd, projectName);
1518
+ if (existsSync13(projectPath)) {
1264
1519
  logger.error(`Directory ${projectName} already exists.`);
1265
1520
  process.exit(1);
1266
1521
  }
1267
- console.log(chalk4.blue.bold("\n\u{1F680} Creating Next.js project with SPFN...\n"));
1522
+ console.log(chalk5.blue.bold("\n\u{1F680} Creating Next.js project with SPFN...\n"));
1268
1523
  let pm = options.pm || detectPackageManager(cwd);
1269
1524
  if (!options.yes && !options.pm) {
1270
1525
  const { selectedPm } = await prompts2({
@@ -1383,33 +1638,33 @@ async function createProject(projectName, options) {
1383
1638
  const initSpinner = ora4("Initializing SPFN...").start();
1384
1639
  try {
1385
1640
  const { initializeSpfn: initializeSpfn2 } = await Promise.resolve().then(() => (init_init(), init_exports));
1386
- await initializeSpfn2({ yes: true });
1641
+ await initializeSpfn2({ yes: true, overwriteReadme: true });
1387
1642
  initSpinner.succeed("SPFN initialized");
1388
1643
  } catch (error) {
1389
1644
  initSpinner.fail("Failed to initialize SPFN");
1390
1645
  logger.error(String(error));
1391
1646
  process.exit(1);
1392
1647
  }
1393
- console.log("\n" + chalk4.green.bold("\u2713 Project created successfully!\n"));
1394
- console.log(chalk4.bold("Next steps:\n"));
1395
- console.log(` ${chalk4.cyan("cd")} ${projectName}`);
1396
- console.log(` ${chalk4.cyan("docker compose up -d")} ${chalk4.gray("# Start PostgreSQL & Redis")}`);
1397
- console.log(` ${chalk4.cyan("cp .env.local.example .env.local")} ${chalk4.gray("# Configure environment")}`);
1398
- console.log(` ${chalk4.cyan(`${pm === "npm" ? "npm run" : pm + " run"} spfn:dev`)} ${chalk4.gray("# Start dev server")}
1648
+ console.log("\n" + chalk5.green.bold("\u2713 Project created successfully!\n"));
1649
+ console.log(chalk5.bold("Next steps:\n"));
1650
+ console.log(` ${chalk5.cyan("cd")} ${projectName}`);
1651
+ console.log(` ${chalk5.cyan("docker compose up -d")} ${chalk5.gray("# Start PostgreSQL & Redis")}`);
1652
+ console.log(` ${chalk5.gray(`# .env.local & .env.server are generated \u2014 ${ENV_FILES_HINT}`)}`);
1653
+ console.log(` ${chalk5.cyan(`${getRunCommand(pm)} spfn:dev`)} ${chalk5.gray("# Start dev server")}
1399
1654
  `);
1400
- console.log(chalk4.bold("Your app will be available at:\n"));
1401
- console.log(` ${chalk4.cyan("http://localhost:3790")} ${chalk4.gray("(Next.js)")}`);
1402
- console.log(` ${chalk4.cyan("http://localhost:8790")} ${chalk4.gray("(SPFN API)")}
1655
+ console.log(chalk5.bold("Your app will be available at:\n"));
1656
+ console.log(` ${chalk5.cyan("http://localhost:3790")} ${chalk5.gray("(Next.js)")}`);
1657
+ console.log(` ${chalk5.cyan("http://localhost:8790")} ${chalk5.gray("(SPFN API)")}
1403
1658
  `);
1404
- console.log(chalk4.bold("\u{1F680} Ready for production?\n"));
1405
- console.log(" " + chalk4.cyan("Build for production:"));
1406
- console.log(` ${chalk4.cyan(pm === "npm" ? "npm run" : pm + " run")} spfn:build`);
1407
- console.log(` ${chalk4.cyan(pm === "npm" ? "npm run" : pm + " run")} spfn:start
1659
+ console.log(chalk5.bold("\u{1F680} Ready for production?\n"));
1660
+ console.log(" " + chalk5.cyan("Build for production:"));
1661
+ console.log(` ${chalk5.cyan(getRunCommand(pm))} spfn:build`);
1662
+ console.log(` ${chalk5.cyan(getRunCommand(pm))} spfn:start
1408
1663
  `);
1409
- console.log(" " + chalk4.cyan("Or deploy with Docker:"));
1410
- console.log(` ${chalk4.cyan("docker compose -f docker-compose.production.yml up --build -d")}
1664
+ console.log(" " + chalk5.cyan("Or deploy with Docker:"));
1665
+ console.log(` ${chalk5.cyan("docker compose -f docker-compose.production.yml up --build -d")}
1411
1666
  `);
1412
- console.log(chalk4.dim(" \u{1F310} Documentation: https://github.com/spfn/spfn\n"));
1667
+ console.log(chalk5.dim(" \u{1F310} Documentation: https://github.com/spfn/spfn\n"));
1413
1668
  }
1414
1669
  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) => {
1415
1670
  await createProject(projectName, options);
@@ -1422,75 +1677,278 @@ init_init();
1422
1677
  init_logger();
1423
1678
  init_package_manager();
1424
1679
  import { Command as Command4 } from "commander";
1425
- import { existsSync as existsSync11, readFileSync as readFileSync4, writeFileSync as writeFileSync7, mkdirSync, unlinkSync, rmSync, watch } from "fs";
1426
- import { join as join11 } from "path";
1427
- import { execa as execa4 } from "execa";
1680
+ import { existsSync as existsSync14, readFileSync as readFileSync7, writeFileSync as writeFileSync10, mkdirSync, unlinkSync, rmSync, watch } from "fs";
1681
+ import { join as join15, relative, sep } from "path";
1682
+ import { execa as execa6 } from "execa";
1428
1683
  import chokidar from "chokidar";
1429
- function waitForReadyFile(filePath, timeoutMs = 3e4) {
1430
- return new Promise((resolve3, reject) => {
1431
- if (existsSync11(filePath)) {
1432
- unlinkSync(filePath);
1684
+
1685
+ // src/utils/secret-store/index.ts
1686
+ init_env_file();
1687
+ import { join as join14 } from "path";
1688
+
1689
+ // src/utils/secret-store/macos.ts
1690
+ import { execa as execa4 } from "execa";
1691
+ var MacosKeychainStore = class {
1692
+ id = "macos-keychain";
1693
+ label = "macOS Keychain";
1694
+ async isAvailable() {
1695
+ if (process.platform !== "darwin") {
1696
+ return false;
1697
+ }
1698
+ try {
1699
+ await execa4("security", ["help"]);
1700
+ return true;
1701
+ } catch {
1702
+ return false;
1433
1703
  }
1434
- const timer = setTimeout(() => {
1435
- watcher.close();
1436
- reject(new Error(`Server did not become ready within ${timeoutMs / 1e3}s`));
1437
- }, timeoutMs);
1438
- const dir = join11(filePath, "..");
1439
- const fileName = filePath.split("/").pop();
1440
- const watcher = watch(dir, (_event, name) => {
1441
- if (name === fileName && existsSync11(filePath)) {
1442
- watcher.close();
1443
- clearTimeout(timer);
1444
- resolve3(readFileSync4(filePath, "utf-8").trim());
1445
- }
1446
- });
1447
- });
1448
- }
1449
- var devCommand = new Command4("dev").description("Start SPFN development server (detects and runs Next.js + Hono)").option("-p, --port <port>", "Server port").option("-H, --host <host>", "Server host").option("--routes <path>", "Routes directory path").option("--server-only", "Run only Hono server (skip Next.js)").option("--watch", "Enable hot reload (watch mode)").action(async (options) => {
1450
- process.setMaxListeners(20);
1451
- if (!process.env.NODE_ENV) {
1452
- process.env.NODE_ENV = "development";
1453
- }
1454
- const cwd = process.cwd();
1455
- const serverDir = join11(cwd, "src", "server");
1456
- if (!existsSync11(serverDir)) {
1457
- logger.error("src/server directory not found.");
1458
- logger.info('Run "spfn init" first to initialize SPFN in your project.');
1459
- process.exit(1);
1460
1704
  }
1461
- const packageJsonPath = join11(cwd, "package.json");
1462
- let hasNext = false;
1463
- if (existsSync11(packageJsonPath)) {
1464
- const packageJson = JSON.parse(readFileSync4(packageJsonPath, "utf-8"));
1465
- hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
1705
+ async get(name) {
1706
+ try {
1707
+ const { stdout } = await execa4("security", [
1708
+ "find-generic-password",
1709
+ "-s",
1710
+ KEYCHAIN_SERVICE,
1711
+ "-a",
1712
+ name,
1713
+ "-w"
1714
+ ]);
1715
+ return stdout;
1716
+ } catch {
1717
+ return null;
1718
+ }
1466
1719
  }
1467
- const tempDir = join11(cwd, ".spfn");
1468
- const serverEntry = join11(tempDir, "server.mjs");
1469
- const watcherEntry = join11(tempDir, "watcher.mjs");
1470
- mkdirSync(tempDir, { recursive: true });
1471
- const serverBuildDir = join11(tempDir, "server");
1472
- if (existsSync11(serverBuildDir)) {
1473
- rmSync(serverBuildDir, { recursive: true, force: true });
1474
- logger.info("[SPFN] Cleaned stale build cache (.spfn/server)");
1720
+ async set(name, value) {
1721
+ await execa4("security", [
1722
+ "add-generic-password",
1723
+ "-U",
1724
+ "-s",
1725
+ KEYCHAIN_SERVICE,
1726
+ "-a",
1727
+ name,
1728
+ "-w",
1729
+ value
1730
+ ]);
1475
1731
  }
1476
- const readySignal = join11(tempDir, "server-ready");
1477
- if (existsSync11(readySignal)) {
1478
- unlinkSync(readySignal);
1732
+ async delete(name) {
1733
+ try {
1734
+ await execa4("security", [
1735
+ "delete-generic-password",
1736
+ "-s",
1737
+ KEYCHAIN_SERVICE,
1738
+ "-a",
1739
+ name
1740
+ ]);
1741
+ } catch {
1742
+ }
1479
1743
  }
1480
- const configParts = [];
1481
- if (options.port) configParts.push(`port: ${options.port}`);
1482
- if (options.host) configParts.push(`host: '${options.host}'`);
1483
- if (options.routes) configParts.push(`routesPath: '${options.routes}'`);
1484
- configParts.push("debug: true");
1485
- const readyFile = join11(tempDir, "server-ready");
1486
- writeFileSync7(serverEntry, `
1487
- import { writeFileSync } from 'fs';
1488
-
1489
- // Load environment variables FIRST (before any imports that depend on them)
1490
- await import('@spfn/core/config');
1744
+ };
1491
1745
 
1492
- // Import and start server
1493
- const { startServer } = await import('@spfn/core/server');
1746
+ // src/utils/secret-store/windows.ts
1747
+ var INSTALL_HINT = "Windows keychain support needs the optional native dependency: install `@napi-rs/keyring`.";
1748
+ async function loadKeyring() {
1749
+ try {
1750
+ return await import("@napi-rs/keyring");
1751
+ } catch {
1752
+ return null;
1753
+ }
1754
+ }
1755
+ var WindowsCredentialStore = class {
1756
+ id = "windows-credential-manager";
1757
+ label = "Windows Credential Manager";
1758
+ async isAvailable() {
1759
+ if (process.platform !== "win32") {
1760
+ return false;
1761
+ }
1762
+ return await loadKeyring() !== null;
1763
+ }
1764
+ async entry(name) {
1765
+ const keyring = await loadKeyring();
1766
+ if (!keyring) {
1767
+ throw new Error(INSTALL_HINT);
1768
+ }
1769
+ return new keyring.Entry(KEYCHAIN_SERVICE, name);
1770
+ }
1771
+ async get(name) {
1772
+ try {
1773
+ return (await this.entry(name)).getPassword();
1774
+ } catch {
1775
+ return null;
1776
+ }
1777
+ }
1778
+ async set(name, value) {
1779
+ (await this.entry(name)).setPassword(value);
1780
+ }
1781
+ async delete(name) {
1782
+ try {
1783
+ (await this.entry(name)).deletePassword();
1784
+ } catch {
1785
+ }
1786
+ }
1787
+ };
1788
+
1789
+ // src/utils/secret-store/linux.ts
1790
+ import { execa as execa5 } from "execa";
1791
+ var ATTRS = (name) => ["service", KEYCHAIN_SERVICE, "account", name];
1792
+ var LinuxSecretToolStore = class {
1793
+ id = "linux-secret-tool";
1794
+ label = "libsecret (secret-tool)";
1795
+ async isAvailable() {
1796
+ if (process.platform !== "linux") {
1797
+ return false;
1798
+ }
1799
+ try {
1800
+ await execa5("secret-tool", ["--version"]);
1801
+ return true;
1802
+ } catch {
1803
+ return false;
1804
+ }
1805
+ }
1806
+ async get(name) {
1807
+ try {
1808
+ const { stdout } = await execa5("secret-tool", ["lookup", ...ATTRS(name)]);
1809
+ return stdout;
1810
+ } catch {
1811
+ return null;
1812
+ }
1813
+ }
1814
+ async set(name, value) {
1815
+ await execa5(
1816
+ "secret-tool",
1817
+ ["store", "--label", `spfn ${name}`, ...ATTRS(name)],
1818
+ { input: value }
1819
+ );
1820
+ }
1821
+ async delete(name) {
1822
+ try {
1823
+ await execa5("secret-tool", ["clear", ...ATTRS(name)]);
1824
+ } catch {
1825
+ }
1826
+ }
1827
+ };
1828
+
1829
+ // src/utils/secret-store/index.ts
1830
+ var KEYCHAIN_REF_PREFIX = "secret:keychain:";
1831
+ var KEYCHAIN_SERVICE = "spfn";
1832
+ function detectStore() {
1833
+ switch (process.platform) {
1834
+ case "darwin":
1835
+ return new MacosKeychainStore();
1836
+ case "win32":
1837
+ return new WindowsCredentialStore();
1838
+ case "linux":
1839
+ return new LinuxSecretToolStore();
1840
+ default:
1841
+ return new MacosKeychainStore();
1842
+ }
1843
+ }
1844
+ function keychainName(key) {
1845
+ return `${KEYCHAIN_SERVICE}_${key}`;
1846
+ }
1847
+ function keychainRef(key) {
1848
+ return `${KEYCHAIN_REF_PREFIX}${keychainName(key)}`;
1849
+ }
1850
+ async function resolveKeychainEnv(cwd) {
1851
+ const parsed = parseEnvFile(join14(cwd, ".env.server"));
1852
+ const refs = Object.entries(parsed).filter(([, value]) => value.startsWith(KEYCHAIN_REF_PREFIX));
1853
+ if (refs.length === 0) {
1854
+ return { env: {}, missing: [] };
1855
+ }
1856
+ const store = detectStore();
1857
+ const env8 = {};
1858
+ const missing = [];
1859
+ if (!await store.isAvailable()) {
1860
+ return { env: env8, missing: refs.map(([key]) => key) };
1861
+ }
1862
+ for (const [key, value] of refs) {
1863
+ const name = value.slice(KEYCHAIN_REF_PREFIX.length);
1864
+ const resolved = await store.get(name);
1865
+ if (resolved === null) {
1866
+ missing.push(key);
1867
+ } else {
1868
+ env8[key] = resolved;
1869
+ }
1870
+ }
1871
+ return { env: env8, missing };
1872
+ }
1873
+
1874
+ // src/commands/dev.ts
1875
+ function ignoreDotfilesUnder(root) {
1876
+ return (watchedPath) => relative(root, watchedPath).split(sep).some((segment) => segment.startsWith(".") && segment !== "." && segment !== "..");
1877
+ }
1878
+ function waitForReadyFile(filePath, timeoutMs = 3e4) {
1879
+ return new Promise((resolve3, reject) => {
1880
+ if (existsSync14(filePath)) {
1881
+ unlinkSync(filePath);
1882
+ }
1883
+ const timer = setTimeout(() => {
1884
+ watcher.close();
1885
+ reject(new Error(`Server did not become ready within ${timeoutMs / 1e3}s`));
1886
+ }, timeoutMs);
1887
+ const dir = join15(filePath, "..");
1888
+ const fileName = filePath.split("/").pop();
1889
+ const watcher = watch(dir, (_event, name) => {
1890
+ if (name === fileName && existsSync14(filePath)) {
1891
+ watcher.close();
1892
+ clearTimeout(timer);
1893
+ resolve3(readFileSync7(filePath, "utf-8").trim());
1894
+ }
1895
+ });
1896
+ });
1897
+ }
1898
+ var devCommand = new Command4("dev").description("Start SPFN development server (detects and runs Next.js + Hono)").option("-p, --port <port>", "Server port").option("-H, --host <host>", "Server host").option("--routes <path>", "Routes directory path").option("--server-only", "Run only Hono server (skip Next.js)").option("--watch", "Enable hot reload (watch mode)").action(async (options) => {
1899
+ process.setMaxListeners(20);
1900
+ if (!process.env.NODE_ENV) {
1901
+ process.env.NODE_ENV = "development";
1902
+ }
1903
+ const cwd = process.cwd();
1904
+ const serverDir = join15(cwd, "src", "server");
1905
+ if (!existsSync14(serverDir)) {
1906
+ logger.error("src/server directory not found.");
1907
+ logger.info('Run "spfn init" first to initialize SPFN in your project.');
1908
+ process.exit(1);
1909
+ }
1910
+ const { env: keychainEnv, missing: keychainMissing } = await resolveKeychainEnv(cwd);
1911
+ const injectedCount = Object.keys(keychainEnv).length;
1912
+ if (injectedCount > 0) {
1913
+ logger.info(`[SPFN] Injecting ${injectedCount} secret(s) from the keychain`);
1914
+ }
1915
+ if (keychainMissing.length > 0) {
1916
+ logger.warn(`[SPFN] Could not resolve keychain secret(s): ${keychainMissing.join(", ")} \u2014 run \`spfn secret set <KEY>\``);
1917
+ }
1918
+ const serverEnv = { ...process.env, ...keychainEnv };
1919
+ const packageJsonPath = join15(cwd, "package.json");
1920
+ let hasNext = false;
1921
+ if (existsSync14(packageJsonPath)) {
1922
+ const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf-8"));
1923
+ hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
1924
+ }
1925
+ const tempDir = join15(cwd, ".spfn");
1926
+ const serverEntry = join15(tempDir, "server.mjs");
1927
+ const watcherEntry = join15(tempDir, "watcher.mjs");
1928
+ mkdirSync(tempDir, { recursive: true });
1929
+ const serverBuildDir = join15(tempDir, "server");
1930
+ if (existsSync14(serverBuildDir)) {
1931
+ rmSync(serverBuildDir, { recursive: true, force: true });
1932
+ logger.info("[SPFN] Cleaned stale build cache (.spfn/server)");
1933
+ }
1934
+ const readySignal = join15(tempDir, "server-ready");
1935
+ if (existsSync14(readySignal)) {
1936
+ unlinkSync(readySignal);
1937
+ }
1938
+ const configParts = [];
1939
+ if (options.port) configParts.push(`port: ${options.port}`);
1940
+ if (options.host) configParts.push(`host: '${options.host}'`);
1941
+ if (options.routes) configParts.push(`routesPath: '${options.routes}'`);
1942
+ configParts.push("debug: true");
1943
+ const readyFile = join15(tempDir, "server-ready");
1944
+ writeFileSync10(serverEntry, `
1945
+ import { writeFileSync } from 'fs';
1946
+
1947
+ // Load environment variables FIRST (before any imports that depend on them)
1948
+ await import('@spfn/core/config');
1949
+
1950
+ // Import and start server
1951
+ const { startServer } = await import('@spfn/core/server');
1494
1952
 
1495
1953
  const instance = await startServer({
1496
1954
  ${configParts.join(",\n ")}
@@ -1499,7 +1957,7 @@ const instance = await startServer({
1499
1957
  // Signal ready with actual port
1500
1958
  writeFileSync(${JSON.stringify(readyFile)}, String(instance.config.port));
1501
1959
  `);
1502
- writeFileSync7(watcherEntry, `
1960
+ writeFileSync10(watcherEntry, `
1503
1961
  // Load environment variables
1504
1962
  // const { loadEnvFiles } = await import('@spfn/core/server');
1505
1963
  // loadEnvFiles();
@@ -1564,7 +2022,7 @@ catch (error)
1564
2022
  const startWatcher2 = () => {
1565
2023
  const watcherCmd = pm === "npm" ? "npx" : pm;
1566
2024
  const watcherArgs = pm === "npm" ? ["tsx", watcherEntry] : ["exec", "tsx", watcherEntry];
1567
- watcherProcess2 = execa4(watcherCmd, watcherArgs, {
2025
+ watcherProcess2 = execa6(watcherCmd, watcherArgs, {
1568
2026
  cwd,
1569
2027
  stdio: "inherit",
1570
2028
  reject: false
@@ -1572,17 +2030,18 @@ catch (error)
1572
2030
  watcherProcess2.then((result) => {
1573
2031
  if (result.exitCode !== 0 && result.exitCode !== 130) {
1574
2032
  logger.error(`Codegen watcher exited with code ${result.exitCode}`);
1575
- process.exit(1);
2033
+ logger.warn("[SPFN] Codegen watch is disabled for this session \u2014 the server keeps running. Run codegen manually after route changes.");
1576
2034
  }
1577
2035
  });
1578
2036
  };
1579
2037
  const startServer2 = () => {
1580
2038
  const serverCmd = pm === "npm" ? "npx" : pm;
1581
2039
  const serverArgs = pm === "npm" ? ["tsx", serverEntry] : ["exec", "tsx", serverEntry];
1582
- serverProcess2 = execa4(serverCmd, serverArgs, {
2040
+ serverProcess2 = execa6(serverCmd, serverArgs, {
1583
2041
  cwd,
1584
2042
  stdio: "inherit",
1585
- reject: false
2043
+ reject: false,
2044
+ env: serverEnv
1586
2045
  });
1587
2046
  };
1588
2047
  const restartServer2 = async () => {
@@ -1603,8 +2062,7 @@ catch (error)
1603
2062
  };
1604
2063
  if (watchMode2) {
1605
2064
  const watcher = chokidar.watch(serverDir, {
1606
- ignored: /(^|[\/\\])\../,
1607
- // ignore dotfiles
2065
+ ignored: ignoreDotfilesUnder(serverDir),
1608
2066
  persistent: true,
1609
2067
  ignoreInitial: true,
1610
2068
  awaitWriteFinish: {
@@ -1658,7 +2116,7 @@ catch (error)
1658
2116
  const startWatcher = () => {
1659
2117
  const watcherCmd = pm === "npm" ? "npx" : pm;
1660
2118
  const watcherArgs = pm === "npm" ? ["tsx", watcherEntry] : ["exec", "tsx", watcherEntry];
1661
- watcherProcess = execa4(watcherCmd, watcherArgs, {
2119
+ watcherProcess = execa6(watcherCmd, watcherArgs, {
1662
2120
  cwd,
1663
2121
  stdio: "inherit",
1664
2122
  reject: false
@@ -1666,14 +2124,14 @@ catch (error)
1666
2124
  watcherProcess.then((result) => {
1667
2125
  if (result.exitCode !== 0 && result.exitCode !== 130) {
1668
2126
  logger.error(`Codegen watcher exited with code ${result.exitCode}`);
1669
- process.exit(1);
2127
+ logger.warn("[SPFN] Codegen watch is disabled for this session \u2014 the server keeps running. Run codegen manually after route changes.");
1670
2128
  }
1671
2129
  });
1672
2130
  };
1673
2131
  const startNext = () => {
1674
2132
  const nextCmd = pm === "npm" ? "npm" : pm;
1675
2133
  const nextArgs = pm === "npm" ? ["run", "spfn:next"] : ["run", "spfn:next"];
1676
- nextProcess = execa4(nextCmd, nextArgs, {
2134
+ nextProcess = execa6(nextCmd, nextArgs, {
1677
2135
  cwd,
1678
2136
  stdio: "inherit",
1679
2137
  reject: false
@@ -1688,10 +2146,11 @@ catch (error)
1688
2146
  const startServer = () => {
1689
2147
  const serverCmd = pm === "npm" ? "npx" : pm;
1690
2148
  const serverArgs = pm === "npm" ? ["tsx", serverEntry] : ["exec", "tsx", serverEntry];
1691
- serverProcess = execa4(serverCmd, serverArgs, {
2149
+ serverProcess = execa6(serverCmd, serverArgs, {
1692
2150
  cwd,
1693
2151
  stdio: "inherit",
1694
- reject: false
2152
+ reject: false,
2153
+ env: serverEnv
1695
2154
  });
1696
2155
  };
1697
2156
  const restartServer = async () => {
@@ -1712,8 +2171,7 @@ catch (error)
1712
2171
  };
1713
2172
  if (watchMode) {
1714
2173
  const watcher = chokidar.watch(serverDir, {
1715
- ignored: /(^|[\/\\])\../,
1716
- // ignore dotfiles
2174
+ ignored: ignoreDotfilesUnder(serverDir),
1717
2175
  persistent: true,
1718
2176
  ignoreInitial: true,
1719
2177
  awaitWriteFinish: {
@@ -1772,11 +2230,11 @@ catch (error)
1772
2230
  init_logger();
1773
2231
  init_package_manager();
1774
2232
  import { Command as Command5 } from "commander";
1775
- import { existsSync as existsSync12, writeFileSync as writeFileSync8, mkdirSync as mkdirSync2, readdirSync } from "fs";
1776
- import { join as join12 } from "path";
1777
- import { execa as execa5 } from "execa";
2233
+ import { existsSync as existsSync15, writeFileSync as writeFileSync11, mkdirSync as mkdirSync2, readdirSync } from "fs";
2234
+ import { join as join16 } from "path";
2235
+ import { execa as execa7 } from "execa";
1778
2236
  import ora5 from "ora";
1779
- import chalk5 from "chalk";
2237
+ import chalk6 from "chalk";
1780
2238
  import { build } from "tsup";
1781
2239
  async function buildProject(options) {
1782
2240
  if (!process.env.NODE_ENV) {
@@ -1787,17 +2245,17 @@ async function buildProject(options) {
1787
2245
  }
1788
2246
  const cwd = process.cwd();
1789
2247
  const pm = detectPackageManager(cwd);
1790
- const packageJsonPath = join12(cwd, "package.json");
2248
+ const packageJsonPath = join16(cwd, "package.json");
1791
2249
  let hasNext = false;
1792
- if (existsSync12(packageJsonPath)) {
2250
+ if (existsSync15(packageJsonPath)) {
1793
2251
  const packageJson = JSON.parse(await import("fs").then(
1794
2252
  (fs5) => fs5.promises.readFile(packageJsonPath, "utf-8")
1795
2253
  ));
1796
2254
  hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
1797
2255
  }
1798
- const serverDir = join12(cwd, "src", "server");
1799
- const hasServer = existsSync12(serverDir);
1800
- console.log(chalk5.blue.bold("\n\u{1F3D7}\uFE0F Building SPFN project for production...\n"));
2256
+ const serverDir = join16(cwd, "src", "server");
2257
+ const hasServer = existsSync15(serverDir);
2258
+ console.log(chalk6.blue.bold("\n\u{1F3D7}\uFE0F Building SPFN project for production...\n"));
1801
2259
  if (hasServer) {
1802
2260
  const spinner = ora5("Generating API client...").start();
1803
2261
  try {
@@ -1819,7 +2277,7 @@ async function buildProject(options) {
1819
2277
  if (hasNext && !options.serverOnly) {
1820
2278
  const spinner = ora5("Building Next.js...").start();
1821
2279
  try {
1822
- await execa5(pm, ["run", "build"], {
2280
+ await execa7(pm, ["run", "build"], {
1823
2281
  cwd,
1824
2282
  stdio: "inherit"
1825
2283
  });
@@ -1833,10 +2291,10 @@ async function buildProject(options) {
1833
2291
  if (hasServer && !options.nextOnly) {
1834
2292
  const spinner = ora5("Building SPFN server...").start();
1835
2293
  try {
1836
- const outputDir = join12(cwd, ".spfn", "server");
2294
+ const outputDir = join16(cwd, ".spfn", "server");
1837
2295
  mkdirSync2(outputDir, { recursive: true });
1838
- const serverDir2 = join12(cwd, "src", "server");
1839
- if (!existsSync12(serverDir2)) {
2296
+ const serverDir2 = join16(cwd, "src", "server");
2297
+ if (!existsSync15(serverDir2)) {
1840
2298
  spinner.fail("SPFN server build failed");
1841
2299
  logger.error("src/server/ directory not found");
1842
2300
  logger.error('Please run "spfn init" to initialize the project.');
@@ -1864,7 +2322,7 @@ async function buildProject(options) {
1864
2322
  onSuccess: async () => {
1865
2323
  }
1866
2324
  });
1867
- const prodServerPath = join12(cwd, ".spfn", "prod-server.mjs");
2325
+ const prodServerPath = join16(cwd, ".spfn", "prod-server.mjs");
1868
2326
  const prodServerContent = `// Load environment variables FIRST (before any imports that depend on them)
1869
2327
  // Use centralized environment loader for standard dotenv priority
1870
2328
  const { env } = await import('@spfn/core/config');
@@ -1888,12 +2346,12 @@ await startServer({
1888
2346
  debug: false
1889
2347
  });
1890
2348
  `;
1891
- writeFileSync8(prodServerPath, prodServerContent);
2349
+ writeFileSync11(prodServerPath, prodServerContent);
1892
2350
  spinner.succeed(`SPFN server build completed \u2192 .spfn/server`);
1893
- const routesDir = join12(cwd, ".spfn", "server", "routes");
1894
- if (existsSync12(routesDir)) {
2351
+ const routesDir = join16(cwd, ".spfn", "server", "routes");
2352
+ if (existsSync15(routesDir)) {
1895
2353
  console.log();
1896
- console.log(chalk5.bold("Route (api)"));
2354
+ console.log(chalk6.bold("Route (api)"));
1897
2355
  try {
1898
2356
  const routes = readdirSync(routesDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name).sort();
1899
2357
  if (routes.length > 0) {
@@ -1902,13 +2360,13 @@ await startServer({
1902
2360
  const prefix = isLast ? "\u2514" : "\u251C";
1903
2361
  const routePath = `/api/${route}`;
1904
2362
  if (route === "health") {
1905
- console.log(`${prefix} ${chalk5.green("GET")} ${routePath}`);
2363
+ console.log(`${prefix} ${chalk6.green("GET")} ${routePath}`);
1906
2364
  } else {
1907
- console.log(`${prefix} ${chalk5.cyan("*")} ${routePath}`);
2365
+ console.log(`${prefix} ${chalk6.cyan("*")} ${routePath}`);
1908
2366
  }
1909
2367
  });
1910
2368
  console.log();
1911
- console.log(chalk5.cyan("*") + " (Hono) multiple methods supported");
2369
+ console.log(chalk6.cyan("*") + " (Hono) multiple methods supported");
1912
2370
  }
1913
2371
  } catch (error) {
1914
2372
  }
@@ -1916,9 +2374,9 @@ await startServer({
1916
2374
  } catch (error) {
1917
2375
  spinner.fail("SPFN server build failed");
1918
2376
  if (error instanceof Error) {
1919
- console.error("\n" + chalk5.red(error.message));
2377
+ console.error("\n" + chalk6.red(error.message));
1920
2378
  if (error.stack) {
1921
- console.error(chalk5.dim("\n" + error.stack));
2379
+ console.error(chalk6.dim("\n" + error.stack));
1922
2380
  }
1923
2381
  } else {
1924
2382
  logger.error(String(error));
@@ -1930,13 +2388,13 @@ await startServer({
1930
2388
  logger.error("No Next.js or SPFN server found in this project.");
1931
2389
  process.exit(1);
1932
2390
  }
1933
- console.log("\n" + chalk5.green.bold("\u2713 Build completed successfully!\n"));
1934
- console.log(chalk5.bold("Next steps:\n"));
1935
- console.log(" " + chalk5.cyan("Start production server:"));
1936
- console.log(` ${chalk5.cyan(pm === "npm" ? "npm run" : pm + " run")} spfn:start ${chalk5.gray("# Start SPFN + Next.js")}
2391
+ console.log("\n" + chalk6.green.bold("\u2713 Build completed successfully!\n"));
2392
+ console.log(chalk6.bold("Next steps:\n"));
2393
+ console.log(" " + chalk6.cyan("Start production server:"));
2394
+ console.log(` ${chalk6.cyan(getRunCommand(pm))} spfn:start ${chalk6.gray("# Start SPFN + Next.js")}
1937
2395
  `);
1938
- console.log(" " + chalk5.cyan("Or deploy with Docker:"));
1939
- console.log(` ${chalk5.cyan("docker compose -f docker-compose.production.yml up --build -d")}
2396
+ console.log(" " + chalk6.cyan("Or deploy with Docker:"));
2397
+ console.log(` ${chalk6.cyan("docker compose -f docker-compose.production.yml up --build -d")}
1940
2398
  `);
1941
2399
  }
1942
2400
  var buildCommand = new Command5("build").description("Build SPFN project for production (Next.js + Server)").option("--server-only", "Build only SPFN server (skip Next.js)").option("--next-only", "Build only Next.js (skip SPFN server)").option("--turbo", "Use Turbopack for Next.js build").action(async (options) => {
@@ -1947,25 +2405,25 @@ var buildCommand = new Command5("build").description("Build SPFN project for pro
1947
2405
  init_logger();
1948
2406
  init_package_manager();
1949
2407
  import { Command as Command6 } from "commander";
1950
- import { existsSync as existsSync13, readFileSync as readFileSync5 } from "fs";
1951
- import { join as join13 } from "path";
1952
- import { execa as execa6 } from "execa";
1953
- import chalk6 from "chalk";
2408
+ import { existsSync as existsSync16, readFileSync as readFileSync8 } from "fs";
2409
+ import { join as join17 } from "path";
2410
+ import { execa as execa8 } from "execa";
2411
+ import chalk7 from "chalk";
1954
2412
  var startCommand = new Command6("start").description("Start SPFN production server (Next.js + Hono)").option("--server-only", "Run only SPFN server (skip Next.js)").option("--next-only", "Run only Next.js (skip SPFN server)").option("-p, --port <port>", "Server port", "8790").option("-h, --host <host>", "Server host", "0.0.0.0").action(async (options) => {
1955
2413
  if (!process.env.NODE_ENV) {
1956
2414
  process.env.NODE_ENV = "production";
1957
2415
  }
1958
2416
  const cwd = process.cwd();
1959
- const packageJsonPath = join13(cwd, "package.json");
2417
+ const packageJsonPath = join17(cwd, "package.json");
1960
2418
  let hasNext = false;
1961
- if (existsSync13(packageJsonPath)) {
1962
- const packageJson = JSON.parse(readFileSync5(packageJsonPath, "utf-8"));
2419
+ if (existsSync16(packageJsonPath)) {
2420
+ const packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf-8"));
1963
2421
  hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
1964
2422
  }
1965
- const builtServerDir = join13(cwd, ".spfn", "server");
1966
- const hasBuiltServer = existsSync13(builtServerDir);
1967
- const nextBuildDir = join13(cwd, ".next");
1968
- const hasNextBuild = existsSync13(nextBuildDir);
2423
+ const builtServerDir = join17(cwd, ".spfn", "server");
2424
+ const hasBuiltServer = existsSync16(builtServerDir);
2425
+ const nextBuildDir = join17(cwd, ".next");
2426
+ const hasNextBuild = existsSync16(nextBuildDir);
1969
2427
  if (!options.nextOnly && !hasBuiltServer) {
1970
2428
  logger.error('.spfn/server directory not found. Please run "spfn build" first.');
1971
2429
  process.exit(1);
@@ -1975,8 +2433,8 @@ var startCommand = new Command6("start").description("Start SPFN production serv
1975
2433
  process.exit(1);
1976
2434
  }
1977
2435
  const pm = detectPackageManager(cwd);
1978
- const serverEntry = join13(cwd, ".spfn", "prod-server.mjs");
1979
- if (!existsSync13(serverEntry)) {
2436
+ const serverEntry = join17(cwd, ".spfn", "prod-server.mjs");
2437
+ if (!existsSync16(serverEntry)) {
1980
2438
  logger.error('.spfn/prod-server.mjs not found. Please run "spfn build" first.');
1981
2439
  process.exit(1);
1982
2440
  }
@@ -1986,7 +2444,7 @@ var startCommand = new Command6("start").description("Start SPFN production serv
1986
2444
  logger.info(`Starting SPFN Server (production) on http://${options.host}:${options.port}
1987
2445
  `);
1988
2446
  try {
1989
- await execa6("node", [serverEntry], {
2447
+ await execa8("node", [serverEntry], {
1990
2448
  stdio: "inherit",
1991
2449
  cwd,
1992
2450
  env: { ...process.env }
@@ -2000,7 +2458,7 @@ var startCommand = new Command6("start").description("Start SPFN production serv
2000
2458
  if (options.nextOnly) {
2001
2459
  logger.info("Starting Next.js (production) on http://0.0.0.0:3790\n");
2002
2460
  try {
2003
- await execa6("npx", ["next", "start", "-H", "0.0.0.0", "-p", "3790"], {
2461
+ await execa8("npx", ["next", "start", "-H", "0.0.0.0", "-p", "3790"], {
2004
2462
  stdio: "inherit",
2005
2463
  cwd
2006
2464
  });
@@ -2012,12 +2470,12 @@ var startCommand = new Command6("start").description("Start SPFN production serv
2012
2470
  }
2013
2471
  const nextCmd = "next start -H 0.0.0.0 -p 3790";
2014
2472
  const serverCmd = `node "${serverEntry}"`;
2015
- console.log(chalk6.blue.bold("\n\u{1F680} Starting SPFN production server...\n"));
2473
+ console.log(chalk7.blue.bold("\n\u{1F680} Starting SPFN production server...\n"));
2016
2474
  logger.info("Next.js: http://0.0.0.0:3790");
2017
2475
  logger.info(`SPFN API: http://${options.host}:${options.port}
2018
2476
  `);
2019
2477
  try {
2020
- await execa6(
2478
+ await execa8(
2021
2479
  pm === "npm" ? "npx" : pm,
2022
2480
  pm === "npm" ? ["concurrently", "--raw", "--kill-others", nextCmd, serverCmd] : ["exec", "concurrently", "--raw", "--kill-others", nextCmd, serverCmd],
2023
2481
  {
@@ -2039,13 +2497,13 @@ var startCommand = new Command6("start").description("Start SPFN production serv
2039
2497
  // src/commands/codegen.ts
2040
2498
  init_logger();
2041
2499
  import { Command as Command7 } from "commander";
2042
- import { existsSync as existsSync14, writeFileSync as writeFileSync9 } from "fs";
2043
- import { join as join14 } from "path";
2044
- import chalk7 from "chalk";
2500
+ import { existsSync as existsSync17, writeFileSync as writeFileSync12 } from "fs";
2501
+ import { join as join18 } from "path";
2502
+ import chalk8 from "chalk";
2045
2503
  async function initCodegen(options) {
2046
2504
  const cwd = process.cwd();
2047
- const rcPath = join14(cwd, ".spfnrc.ts");
2048
- if (existsSync14(rcPath)) {
2505
+ const rcPath = join18(cwd, ".spfnrc.ts");
2506
+ if (existsSync17(rcPath)) {
2049
2507
  logger.warn(".spfnrc.ts already exists");
2050
2508
  logger.info("Edit manually to add custom generators");
2051
2509
  process.exit(0);
@@ -2070,14 +2528,14 @@ export default defineConfig({
2070
2528
  ],
2071
2529
  });
2072
2530
  `;
2073
- writeFileSync9(rcPath, content);
2074
- console.log("\n" + chalk7.green.bold("\u2713 Created .spfnrc.ts\n"));
2075
- console.log(chalk7.gray("Configured the @spfn/core:route-map generator."));
2531
+ writeFileSync12(rcPath, content);
2532
+ console.log("\n" + chalk8.green.bold("\u2713 Created .spfnrc.ts\n"));
2533
+ console.log(chalk8.gray("Configured the @spfn/core:route-map generator."));
2076
2534
  if (options.withExample) {
2077
- console.log("\n" + chalk7.yellow("\u{1F4DD} To add custom generators:"));
2535
+ console.log("\n" + chalk8.yellow("\u{1F4DD} To add custom generators:"));
2078
2536
  console.log(" 1. Create your generator file (e.g., src/generators/my-generator.ts)");
2079
2537
  console.log(" 2. Add to .spfnrc.ts:");
2080
- console.log(chalk7.gray(`
2538
+ console.log(chalk8.gray(`
2081
2539
  {
2082
2540
  "codegen": {
2083
2541
  "generators": [
@@ -2087,11 +2545,11 @@ export default defineConfig({
2087
2545
  }
2088
2546
  }
2089
2547
  `));
2090
- console.log(" 3. Run: " + chalk7.cyan("spfn dev") + " (generators run automatically)");
2548
+ console.log(" 3. Run: " + chalk8.cyan("spfn dev") + " (generators run automatically)");
2091
2549
  } else {
2092
- console.log("\n" + chalk7.yellow("\u{1F4DD} Next steps:"));
2550
+ console.log("\n" + chalk8.yellow("\u{1F4DD} Next steps:"));
2093
2551
  console.log(" \u2022 Add custom generators to .spfnrc.ts");
2094
- console.log(" \u2022 Run: " + chalk7.cyan("spfn dev") + " to start development with code generation");
2552
+ console.log(" \u2022 Run: " + chalk8.cyan("spfn dev") + " to start development with code generation");
2095
2553
  }
2096
2554
  }
2097
2555
  async function listGenerators() {
@@ -2104,10 +2562,10 @@ async function listGenerators() {
2104
2562
  logger.info('Run "spfn codegen init" to initialize configuration');
2105
2563
  return;
2106
2564
  }
2107
- console.log("\n" + chalk7.bold("Registered Generators:"));
2565
+ console.log("\n" + chalk8.bold("Registered Generators:"));
2108
2566
  generators.forEach((gen, index) => {
2109
- console.log(` ${index + 1}. ${chalk7.cyan(gen.name)}`);
2110
- console.log(` Patterns: ${chalk7.gray(gen.watchPatterns.join(", "))}`);
2567
+ console.log(` ${index + 1}. ${chalk8.cyan(gen.name)}`);
2568
+ console.log(` Patterns: ${chalk8.gray(gen.watchPatterns.join(", "))}`);
2111
2569
  });
2112
2570
  console.log("");
2113
2571
  }
@@ -2128,7 +2586,7 @@ async function runGenerators() {
2128
2586
  debug: true
2129
2587
  });
2130
2588
  await orchestrator.generateAll();
2131
- console.log("\n" + chalk7.green.bold("\u2713 Code generation completed"));
2589
+ console.log("\n" + chalk8.green.bold("\u2713 Code generation completed"));
2132
2590
  }
2133
2591
  var codegenCommand = new Command7("codegen").description("Code generation management");
2134
2592
  codegenCommand.command("init").description("Initialize .spfnrc.ts with codegen configuration").option("--with-example", "Show example custom generator usage").action(initCodegen);
@@ -2138,9 +2596,30 @@ codegenCommand.command("run").description("Run code generators once (no watch mo
2138
2596
  // src/commands/key.ts
2139
2597
  init_logger();
2140
2598
  import { Command as Command8 } from "commander";
2141
- import { randomBytes } from "crypto";
2142
2599
  import { execSync } from "child_process";
2143
- import chalk8 from "chalk";
2600
+ import chalk9 from "chalk";
2601
+
2602
+ // src/utils/secret-gen.ts
2603
+ import { randomBytes, randomUUID } from "crypto";
2604
+ function randomBase64Url(bytes) {
2605
+ return randomBytes(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
2606
+ }
2607
+ function generateSecretValue(spec) {
2608
+ switch (spec) {
2609
+ case "hex32":
2610
+ return randomBytes(32).toString("hex");
2611
+ case "hex64":
2612
+ return randomBytes(64).toString("hex");
2613
+ case "base64url32":
2614
+ return randomBase64Url(32);
2615
+ case "uuid":
2616
+ return randomUUID();
2617
+ default:
2618
+ throw new Error(`Unknown generate strategy: ${spec}`);
2619
+ }
2620
+ }
2621
+
2622
+ // src/commands/key.ts
2144
2623
  var PRESETS = {
2145
2624
  "auth-encryption": {
2146
2625
  bytes: 32,
@@ -2191,43 +2670,43 @@ function copyToClipboard(text) {
2191
2670
  }
2192
2671
  }
2193
2672
  function generateSecret(bytes, preset, envVarName, copy) {
2194
- const key = randomBytes(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
2673
+ const key = randomBase64Url(bytes);
2195
2674
  const config = preset ? PRESETS[preset] : null;
2196
- console.log("\n" + chalk8.green.bold("\u2713 Generated secret key:"));
2675
+ console.log("\n" + chalk9.green.bold("\u2713 Generated secret key:"));
2197
2676
  if (config) {
2198
- console.log(chalk8.dim(` ${config.description} (${bytes * 8} bits)`));
2677
+ console.log(chalk9.dim(` ${config.description} (${bytes * 8} bits)`));
2199
2678
  } else {
2200
- console.log(chalk8.dim(` ${bytes * 8}-bit secret`));
2679
+ console.log(chalk9.dim(` ${bytes * 8}-bit secret`));
2201
2680
  }
2202
- console.log("\n" + chalk8.cyan(key) + "\n");
2681
+ console.log("\n" + chalk9.cyan(key) + "\n");
2203
2682
  const varName = envVarName || config?.envVar || "SECRET_KEY";
2204
- console.log(chalk8.dim("Add to your .env file:"));
2205
- console.log(chalk8.yellow(`${varName}=${key}
2683
+ console.log(chalk9.dim("Add to your .env file:"));
2684
+ console.log(chalk9.yellow(`${varName}=${key}
2206
2685
  `));
2207
2686
  if (config?.usage) {
2208
- console.log(chalk8.dim("Usage:"));
2209
- console.log(chalk8.gray(` ${config.usage}
2687
+ console.log(chalk9.dim("Usage:"));
2688
+ console.log(chalk9.gray(` ${config.usage}
2210
2689
  `));
2211
2690
  }
2212
2691
  if (copy) {
2213
2692
  if (copyToClipboard(key)) {
2214
- console.log(chalk8.green("\u2713 Copied to clipboard!\n"));
2693
+ console.log(chalk9.green("\u2713 Copied to clipboard!\n"));
2215
2694
  } else {
2216
2695
  logger.warn("Could not copy to clipboard");
2217
2696
  }
2218
2697
  }
2219
2698
  }
2220
2699
  function listPresets() {
2221
- console.log("\n" + chalk8.bold("Available presets:"));
2700
+ console.log("\n" + chalk9.bold("Available presets:"));
2222
2701
  console.log();
2223
2702
  Object.entries(PRESETS).forEach(([name, config]) => {
2224
- console.log(` ${chalk8.cyan(name.padEnd(20))} ${chalk8.dim(config.description)}`);
2225
- console.log(` ${" ".repeat(20)} ${chalk8.gray(`\u2192 ${config.envVar} (${config.bytes * 8} bits)`)}`);
2703
+ console.log(` ${chalk9.cyan(name.padEnd(20))} ${chalk9.dim(config.description)}`);
2704
+ console.log(` ${" ".repeat(20)} ${chalk9.gray(`\u2192 ${config.envVar} (${config.bytes * 8} bits)`)}`);
2226
2705
  console.log();
2227
2706
  });
2228
- console.log(chalk8.dim("Usage:"));
2229
- console.log(chalk8.gray(" spfn key <preset>"));
2230
- console.log(chalk8.gray(" spfn key auth-encryption --copy"));
2707
+ console.log(chalk9.dim("Usage:"));
2708
+ console.log(chalk9.gray(" spfn key <preset>"));
2709
+ console.log(chalk9.gray(" spfn key auth-encryption --copy"));
2231
2710
  console.log();
2232
2711
  }
2233
2712
  var generateValueCommand = new Command8("generate").alias("gen").description("Generate random value (simple output, no metadata)").option("-b, --bytes <number>", "Number of random bytes", "32").option("-c, --copy", "Copy to clipboard").action((options) => {
@@ -2236,13 +2715,13 @@ var generateValueCommand = new Command8("generate").alias("gen").description("Ge
2236
2715
  logger.error("Invalid bytes value. Must be between 1 and 128.");
2237
2716
  process.exit(1);
2238
2717
  }
2239
- const value = randomBytes(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
2718
+ const value = randomBase64Url(bytes);
2240
2719
  console.log(value);
2241
2720
  if (options.copy) {
2242
2721
  if (copyToClipboard(value)) {
2243
- console.error(chalk8.green("\u2713 Copied to clipboard"));
2722
+ console.error(chalk9.green("\u2713 Copied to clipboard"));
2244
2723
  } else {
2245
- console.error(chalk8.yellow("\u26A0 Could not copy to clipboard"));
2724
+ console.error(chalk9.yellow("\u26A0 Could not copy to clipboard"));
2246
2725
  }
2247
2726
  }
2248
2727
  });
@@ -2260,9 +2739,9 @@ var keyCommand = new Command8("key").alias("k").description("Generate secure ran
2260
2739
  logger.error(`Unknown preset: ${preset}`);
2261
2740
  console.log("\nAvailable presets:");
2262
2741
  Object.entries(PRESETS).forEach(([name, config]) => {
2263
- console.log(` ${chalk8.cyan(name)}: ${config.description}`);
2742
+ console.log(` ${chalk9.cyan(name)}: ${config.description}`);
2264
2743
  });
2265
- console.log("\nUse " + chalk8.cyan("--list") + " to see detailed information");
2744
+ console.log("\nUse " + chalk9.cyan("--list") + " to see detailed information");
2266
2745
  process.exit(1);
2267
2746
  }
2268
2747
  generateSecret(
@@ -2281,13 +2760,13 @@ init_setup();
2281
2760
  import { Command as Command9 } from "commander";
2282
2761
 
2283
2762
  // src/commands/db/generate.ts
2284
- import chalk10 from "chalk";
2763
+ import chalk11 from "chalk";
2285
2764
 
2286
2765
  // src/commands/db/utils/drizzle.ts
2287
- import { existsSync as existsSync15, writeFileSync as writeFileSync10, unlinkSync as unlinkSync2 } from "fs";
2766
+ import { existsSync as existsSync18, writeFileSync as writeFileSync13, unlinkSync as unlinkSync2 } from "fs";
2288
2767
  import { spawn } from "child_process";
2289
2768
  import { pathToFileURL } from "url";
2290
- import chalk9 from "chalk";
2769
+ import chalk10 from "chalk";
2291
2770
  import ora6 from "ora";
2292
2771
  import { env } from "@spfn/core/config";
2293
2772
  import { loadEnv } from "@spfn/core/server";
@@ -2308,20 +2787,20 @@ function shouldRelaxDbTls(databaseUrl) {
2308
2787
  function validateDatabasePrerequisites() {
2309
2788
  loadEnv();
2310
2789
  if (!env.DATABASE_URL) {
2311
- console.error(chalk9.red("\u274C DATABASE_URL not found in environment"));
2312
- console.log(chalk9.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2790
+ console.error(chalk10.red("\u274C DATABASE_URL not found in environment"));
2791
+ console.log(chalk10.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2313
2792
  throw new Error("DATABASE_URL is required for database operations");
2314
2793
  }
2315
2794
  }
2316
2795
  async function runDrizzleCommand(command) {
2317
- const hasUserConfig = existsSync15("./drizzle.config.ts");
2796
+ const hasUserConfig = existsSync18("./drizzle.config.ts");
2318
2797
  const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
2319
2798
  const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
2320
2799
  if (!hasUserConfig) {
2321
2800
  loadEnv();
2322
2801
  if (!env.DATABASE_URL) {
2323
- console.error(chalk9.red("\u274C DATABASE_URL not found in environment"));
2324
- console.log(chalk9.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2802
+ console.error(chalk10.red("\u274C DATABASE_URL not found in environment"));
2803
+ console.log(chalk10.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2325
2804
  process.exit(1);
2326
2805
  }
2327
2806
  const { generateDrizzleConfigFile } = await import("@spfn/core/db");
@@ -2333,8 +2812,8 @@ async function runDrizzleCommand(command) {
2333
2812
  expandGlobs: true,
2334
2813
  autoDetectSchemas: true
2335
2814
  });
2336
- writeFileSync10(tempConfigPath, configContent);
2337
- console.log(chalk9.dim("Using auto-generated Drizzle config\n"));
2815
+ writeFileSync13(tempConfigPath, configContent);
2816
+ console.log(chalk10.dim("Using auto-generated Drizzle config\n"));
2338
2817
  }
2339
2818
  const args = command.split(" ");
2340
2819
  args.push(`--config=${configPath}`);
@@ -2346,7 +2825,7 @@ async function runDrizzleCommand(command) {
2346
2825
  env: shouldRelaxDbTls(env.DATABASE_URL) ? { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" } : { ...process.env }
2347
2826
  });
2348
2827
  const cleanup = () => {
2349
- if (!hasUserConfig && existsSync15(tempConfigPath)) {
2828
+ if (!hasUserConfig && existsSync18(tempConfigPath)) {
2350
2829
  unlinkSync2(tempConfigPath);
2351
2830
  }
2352
2831
  };
@@ -2369,10 +2848,10 @@ async function runWithSpinner(spinnerText, command, successMessage, failMessage)
2369
2848
  try {
2370
2849
  spinner.stop();
2371
2850
  await runDrizzleCommand(command);
2372
- console.log(chalk9.green(`\u2705 ${successMessage}`));
2851
+ console.log(chalk10.green(`\u2705 ${successMessage}`));
2373
2852
  } catch (error) {
2374
2853
  spinner.fail(failMessage);
2375
- console.error(chalk9.red(error instanceof Error ? error.message : "Unknown error"));
2854
+ console.error(chalk10.red(error instanceof Error ? error.message : "Unknown error"));
2376
2855
  process.exit(1);
2377
2856
  }
2378
2857
  }
@@ -2418,16 +2897,16 @@ async function createPushConnection() {
2418
2897
  async function dbGenerate() {
2419
2898
  try {
2420
2899
  await runDrizzleCommand("generate");
2421
- console.log(chalk10.green("\n\u2705 Migrations generated successfully"));
2900
+ console.log(chalk11.green("\n\u2705 Migrations generated successfully"));
2422
2901
  } catch (error) {
2423
- console.error(chalk10.red("\n\u274C Failed to generate migrations"));
2424
- console.error(chalk10.red(error instanceof Error ? error.message : "Unknown error"));
2902
+ console.error(chalk11.red("\n\u274C Failed to generate migrations"));
2903
+ console.error(chalk11.red(error instanceof Error ? error.message : "Unknown error"));
2425
2904
  process.exit(1);
2426
2905
  }
2427
2906
  }
2428
2907
 
2429
2908
  // src/commands/db/push.ts
2430
- import chalk13 from "chalk";
2909
+ import chalk14 from "chalk";
2431
2910
  import prompts3 from "prompts";
2432
2911
  import "@spfn/core/config";
2433
2912
  import { loadEnv as loadEnv3 } from "@spfn/core/server";
@@ -2474,7 +2953,7 @@ function classifyStatements(statements) {
2474
2953
  }
2475
2954
 
2476
2955
  // src/commands/db/utils/push-display.ts
2477
- import chalk11 from "chalk";
2956
+ import chalk12 from "chalk";
2478
2957
  function formatSql(sql2) {
2479
2958
  return sql2.replace(/\s+/g, " ").trim();
2480
2959
  }
@@ -2482,30 +2961,30 @@ function printStatements(statements, color) {
2482
2961
  for (const stmt of statements) {
2483
2962
  console.log(color(` ${formatSql(stmt.sql)}`));
2484
2963
  if (stmt.reason) {
2485
- console.log(chalk11.dim(` \u2192 ${stmt.reason}`));
2964
+ console.log(chalk12.dim(` \u2192 ${stmt.reason}`));
2486
2965
  }
2487
2966
  }
2488
2967
  }
2489
2968
  function displayClassifiedStatements(result) {
2490
2969
  if (result.safe.length > 0) {
2491
- console.log(chalk11.green(`
2970
+ console.log(chalk12.green(`
2492
2971
  \u2705 Safe changes (${result.safe.length}):`));
2493
- printStatements(result.safe, chalk11.green);
2972
+ printStatements(result.safe, chalk12.green);
2494
2973
  }
2495
2974
  if (result.warning.length > 0) {
2496
- console.log(chalk11.yellow(`
2975
+ console.log(chalk12.yellow(`
2497
2976
  \u26A0\uFE0F Warnings (${result.warning.length}):`));
2498
- printStatements(result.warning, chalk11.yellow);
2977
+ printStatements(result.warning, chalk12.yellow);
2499
2978
  }
2500
2979
  if (result.destructive.length > 0) {
2501
- console.log(chalk11.red(`
2980
+ console.log(chalk12.red(`
2502
2981
  \u274C Destructive changes (${result.destructive.length}):`));
2503
- printStatements(result.destructive, chalk11.red);
2982
+ printStatements(result.destructive, chalk12.red);
2504
2983
  }
2505
2984
  }
2506
2985
  function displayDryRunSummary(result) {
2507
2986
  const total = result.safe.length + result.warning.length + result.destructive.length;
2508
- console.log(chalk11.cyan(`
2987
+ console.log(chalk12.cyan(`
2509
2988
  \u{1F50D} Dry-run: ${total} statement(s) detected
2510
2989
  `));
2511
2990
  displayClassifiedStatements(result);
@@ -2513,11 +2992,11 @@ function displayDryRunSummary(result) {
2513
2992
  }
2514
2993
  function displayApplySummary(applied, skipped) {
2515
2994
  if (applied > 0) {
2516
- console.log(chalk11.green(`
2995
+ console.log(chalk12.green(`
2517
2996
  \u2705 Applied ${applied} statement(s)`));
2518
2997
  }
2519
2998
  if (skipped > 0) {
2520
- console.log(chalk11.yellow(`\u23ED\uFE0F Skipped ${skipped} destructive statement(s)`));
2999
+ console.log(chalk12.yellow(`\u23ED\uFE0F Skipped ${skipped} destructive statement(s)`));
2521
3000
  }
2522
3001
  console.log("");
2523
3002
  }
@@ -2535,10 +3014,10 @@ async function dbPush(options = {}) {
2535
3014
  });
2536
3015
  const schemaFiles = Array.isArray(config.schema) ? config.schema : [config.schema];
2537
3016
  if (schemaFiles.length === 0) {
2538
- console.log(chalk13.yellow("No schema files found."));
3017
+ console.log(chalk14.yellow("No schema files found."));
2539
3018
  return;
2540
3019
  }
2541
- console.log(chalk13.dim(`Found ${schemaFiles.length} schema file(s)
3020
+ console.log(chalk14.dim(`Found ${schemaFiles.length} schema file(s)
2542
3021
  `));
2543
3022
  const imports = await loadSchemaImports(schemaFiles);
2544
3023
  const detectedSchemas = new Set(config.schemaFilter ?? ["public"]);
@@ -2564,7 +3043,7 @@ async function dbPush(options = {}) {
2564
3043
  const patched = statementsToExecute.filter((s) => {
2565
3044
  const dropMatch = s.match(/^\s*DROP\s+SCHEMA\s+"?([^"\s;]+)"?/i);
2566
3045
  if (dropMatch && managedSchemaSet.has(dropMatch[1])) {
2567
- console.log(chalk13.dim(` [skip] DROP SCHEMA "${dropMatch[1]}" \u2014 managed schema, ignoring`));
3046
+ console.log(chalk14.dim(` [skip] DROP SCHEMA "${dropMatch[1]}" \u2014 managed schema, ignoring`));
2568
3047
  return false;
2569
3048
  }
2570
3049
  return true;
@@ -2575,7 +3054,7 @@ async function dbPush(options = {}) {
2575
3054
  `);
2576
3055
  const statements = [...ensureSchemas, ...patched];
2577
3056
  if (patched.length === 0) {
2578
- console.log(chalk13.green("\u2705 No changes detected \u2014 database is up to date\n"));
3057
+ console.log(chalk14.green("\u2705 No changes detected \u2014 database is up to date\n"));
2579
3058
  await applyFunctionMigrations();
2580
3059
  return;
2581
3060
  }
@@ -2586,7 +3065,7 @@ async function dbPush(options = {}) {
2586
3065
  }
2587
3066
  displayClassifiedStatements(result);
2588
3067
  if (options.force) {
2589
- console.log(chalk13.dim("\n--force: applying all changes..."));
3068
+ console.log(chalk14.dim("\n--force: applying all changes..."));
2590
3069
  for (const stmt of statements) {
2591
3070
  await db.execute(sql.raw(stmt));
2592
3071
  }
@@ -2603,14 +3082,14 @@ async function dbPush(options = {}) {
2603
3082
  for (const stmt of nonDestructive) {
2604
3083
  await db.execute(sql.raw(stmt));
2605
3084
  }
2606
- console.log(chalk13.green(`
3085
+ console.log(chalk14.green(`
2607
3086
  \u2705 Applied ${nonDestructive.length} safe statement(s)`));
2608
3087
  }
2609
- console.log(chalk13.red(`
3088
+ console.log(chalk14.red(`
2610
3089
  \u274C ${result.destructive.length} destructive change(s) require confirmation:`));
2611
3090
  for (const stmt of result.destructive) {
2612
- console.log(chalk13.red(` ${stmt.sql.replace(/\s+/g, " ").trim()}`));
2613
- console.log(chalk13.dim(` \u2192 ${stmt.reason}`));
3091
+ console.log(chalk14.red(` ${stmt.sql.replace(/\s+/g, " ").trim()}`));
3092
+ console.log(chalk14.dim(` \u2192 ${stmt.reason}`));
2614
3093
  }
2615
3094
  const { confirm } = await prompts3({
2616
3095
  type: "confirm",
@@ -2625,7 +3104,7 @@ async function dbPush(options = {}) {
2625
3104
  displayApplySummary(statements.length, 0);
2626
3105
  } else {
2627
3106
  displayApplySummary(nonDestructive.length, result.destructive.length);
2628
- console.log(chalk13.dim("Tip: Use --force to apply all changes without prompting.\n"));
3107
+ console.log(chalk14.dim("Tip: Use --force to apply all changes without prompting.\n"));
2629
3108
  }
2630
3109
  }
2631
3110
  await applyFunctionMigrations();
@@ -2639,35 +3118,35 @@ async function applyFunctionMigrations() {
2639
3118
  if (functions.length === 0) {
2640
3119
  return;
2641
3120
  }
2642
- console.log(chalk13.blue("\n\u{1F4E6} Applying function package migrations:"));
3121
+ console.log(chalk14.blue("\n\u{1F4E6} Applying function package migrations:"));
2643
3122
  functions.forEach((func) => {
2644
- console.log(chalk13.dim(` - ${func.packageName}`));
3123
+ console.log(chalk14.dim(` - ${func.packageName}`));
2645
3124
  });
2646
3125
  try {
2647
3126
  await executeFunctionMigrations2(functions);
2648
- console.log(chalk13.green("\n\u2705 All function migrations applied\n"));
3127
+ console.log(chalk14.green("\n\u2705 All function migrations applied\n"));
2649
3128
  } catch (error) {
2650
- console.error(chalk13.red("\n\u274C Failed to apply function migrations"));
2651
- console.error(chalk13.red(error instanceof Error ? error.message : "Unknown error"));
3129
+ console.error(chalk14.red("\n\u274C Failed to apply function migrations"));
3130
+ console.error(chalk14.red(error instanceof Error ? error.message : "Unknown error"));
2652
3131
  process.exit(1);
2653
3132
  }
2654
3133
  }
2655
3134
 
2656
3135
  // src/commands/db/migrate.ts
2657
- import chalk18 from "chalk";
2658
- import { join as join16 } from "path";
2659
- import { existsSync as existsSync18 } from "fs";
3136
+ import chalk19 from "chalk";
3137
+ import { join as join20 } from "path";
3138
+ import { existsSync as existsSync21 } from "fs";
2660
3139
 
2661
3140
  // src/commands/db/backup.ts
2662
3141
  import { promises as fs3 } from "fs";
2663
3142
  import path3 from "path";
2664
3143
  import { spawn as spawn2 } from "child_process";
2665
- import chalk17 from "chalk";
3144
+ import chalk18 from "chalk";
2666
3145
  import ora7 from "ora";
2667
3146
 
2668
3147
  // src/commands/db/utils/database.ts
2669
3148
  import net from "net";
2670
- import chalk14 from "chalk";
3149
+ import chalk15 from "chalk";
2671
3150
  import prompts4 from "prompts";
2672
3151
  function parseDatabaseUrl(dbUrl) {
2673
3152
  try {
@@ -2698,7 +3177,7 @@ async function confirmDangerousTarget(dbInfo) {
2698
3177
  message: `Remote/production database detected. Type the database name "${dbInfo.database}" to continue:`
2699
3178
  });
2700
3179
  if (typed !== dbInfo.database) {
2701
- console.log(chalk14.gray("Cancelled (database name mismatch)."));
3180
+ console.log(chalk15.gray("Cancelled (database name mismatch)."));
2702
3181
  process.exit(0);
2703
3182
  }
2704
3183
  }
@@ -2749,16 +3228,16 @@ function formatTimestamp() {
2749
3228
 
2750
3229
  // src/commands/db/utils/backup-files.ts
2751
3230
  import { promises as fs2 } from "fs";
2752
- import { existsSync as existsSync17 } from "fs";
3231
+ import { existsSync as existsSync20 } from "fs";
2753
3232
  import path2 from "path";
2754
- import chalk16 from "chalk";
3233
+ import chalk17 from "chalk";
2755
3234
 
2756
3235
  // src/commands/db/utils/metadata.ts
2757
3236
  import { promises as fs } from "fs";
2758
3237
  import path from "path";
2759
3238
  import { promisify } from "util";
2760
3239
  import { exec } from "child_process";
2761
- import chalk15 from "chalk";
3240
+ import chalk16 from "chalk";
2762
3241
  var execAsync = promisify(exec);
2763
3242
  async function collectGitInfo() {
2764
3243
  try {
@@ -2821,7 +3300,7 @@ async function collectMigrationInfo(dbUrl) {
2821
3300
  await pool.end();
2822
3301
  }
2823
3302
  } catch (error) {
2824
- console.log(chalk15.dim("\u26A0\uFE0F Could not fetch migration info"));
3303
+ console.log(chalk16.dim("\u26A0\uFE0F Could not fetch migration info"));
2825
3304
  return void 0;
2826
3305
  }
2827
3306
  }
@@ -2829,9 +3308,9 @@ async function saveBackupMetadata(metadata, backupFilename) {
2829
3308
  const metadataPath = backupFilename.replace(/\.(sql|dump)$/, ".meta.json");
2830
3309
  try {
2831
3310
  await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
2832
- console.log(chalk15.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
3311
+ console.log(chalk16.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
2833
3312
  } catch (error) {
2834
- console.log(chalk15.dim("\u26A0\uFE0F Could not save metadata"));
3313
+ console.log(chalk16.dim("\u26A0\uFE0F Could not save metadata"));
2835
3314
  }
2836
3315
  }
2837
3316
  async function loadBackupMetadata(backupFilename) {
@@ -2849,7 +3328,7 @@ async function ensureBackupInGitignore() {
2849
3328
  const gitignorePath = path2.join(process.cwd(), ".gitignore");
2850
3329
  try {
2851
3330
  let content = "";
2852
- let exists = existsSync17(gitignorePath);
3331
+ let exists = existsSync20(gitignorePath);
2853
3332
  if (exists) {
2854
3333
  content = await fs2.readFile(gitignorePath, "utf-8");
2855
3334
  }
@@ -2860,10 +3339,10 @@ async function ensureBackupInGitignore() {
2860
3339
  if (!hasBackupsIgnore) {
2861
3340
  const entry = exists && content && !content.endsWith("\n") ? "\n\n# Database backups\nbackups/\n" : "# Database backups\nbackups/\n";
2862
3341
  await fs2.appendFile(gitignorePath, entry);
2863
- console.log(chalk16.dim("\u2713 Added backups/ to .gitignore"));
3342
+ console.log(chalk17.dim("\u2713 Added backups/ to .gitignore"));
2864
3343
  }
2865
3344
  } catch (error) {
2866
- console.log(chalk16.dim("\u26A0\uFE0F Could not update .gitignore"));
3345
+ console.log(chalk17.dim("\u26A0\uFE0F Could not update .gitignore"));
2867
3346
  }
2868
3347
  }
2869
3348
  async function ensureBackupDir() {
@@ -2871,7 +3350,7 @@ async function ensureBackupDir() {
2871
3350
  try {
2872
3351
  await fs2.mkdir(backupDir, { recursive: true });
2873
3352
  const gitignorePath = path2.join(backupDir, ".gitignore");
2874
- const gitignoreExists = existsSync17(gitignorePath);
3353
+ const gitignoreExists = existsSync20(gitignorePath);
2875
3354
  if (!gitignoreExists) {
2876
3355
  await fs2.writeFile(gitignorePath, "# Ignore all backup files\n*.sql\n*.dump\n*.meta.json\n");
2877
3356
  }
@@ -2913,12 +3392,12 @@ async function listBackupFiles() {
2913
3392
  import { env as env3 } from "@spfn/core/config";
2914
3393
  import { loadEnv as loadEnv4 } from "@spfn/core/server";
2915
3394
  async function dbBackup(options) {
2916
- console.log(chalk17.blue("\u{1F4BE} Creating database backup...\n"));
3395
+ console.log(chalk18.blue("\u{1F4BE} Creating database backup...\n"));
2917
3396
  loadEnv4();
2918
3397
  const dbUrl = env3.DATABASE_URL;
2919
3398
  if (!dbUrl) {
2920
- console.error(chalk17.red("\u274C DATABASE_URL not found in environment"));
2921
- console.log(chalk17.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
3399
+ console.error(chalk18.red("\u274C DATABASE_URL not found in environment"));
3400
+ console.log(chalk18.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
2922
3401
  process.exit(1);
2923
3402
  }
2924
3403
  const dbInfo = parseDatabaseUrl(dbUrl);
@@ -2928,7 +3407,7 @@ async function dbBackup(options) {
2928
3407
  const ext = format === "sql" ? "sql" : "dump";
2929
3408
  const filename = options.output || path3.join(backupDir, `${dbInfo.database}_${timestamp}.${ext}`);
2930
3409
  if (options.dataOnly && options.schemaOnly) {
2931
- console.error(chalk17.red("\u274C Cannot use --data-only and --schema-only together"));
3410
+ console.error(chalk18.red("\u274C Cannot use --data-only and --schema-only together"));
2932
3411
  process.exit(1);
2933
3412
  }
2934
3413
  const args = [
@@ -2974,11 +3453,11 @@ async function dbBackup(options) {
2974
3453
  const stats = await fs3.stat(filename);
2975
3454
  const size = formatBytes(stats.size);
2976
3455
  spinner.succeed("Backup created");
2977
- console.log(chalk17.green(`
3456
+ console.log(chalk18.green(`
2978
3457
  \u2705 Backup created successfully`));
2979
- console.log(chalk17.gray(` File: ${filename}`));
2980
- console.log(chalk17.gray(` Size: ${size}`));
2981
- console.log(chalk17.dim("\n\u{1F4CB} Collecting metadata..."));
3458
+ console.log(chalk18.gray(` File: ${filename}`));
3459
+ console.log(chalk18.gray(` Size: ${size}`));
3460
+ console.log(chalk18.dim("\n\u{1F4CB} Collecting metadata..."));
2982
3461
  const [gitInfo, migrationInfo] = await Promise.all([
2983
3462
  collectGitInfo(),
2984
3463
  collectMigrationInfo(dbUrl)
@@ -3018,11 +3497,11 @@ async function dbBackup(options) {
3018
3497
  reject(error);
3019
3498
  });
3020
3499
  }).catch((error) => {
3021
- console.error(chalk17.red("\n\u274C Failed to create backup"));
3500
+ console.error(chalk18.red("\n\u274C Failed to create backup"));
3022
3501
  if (errorOutput.includes("pg_dump: command not found") || errorOutput.includes("not found")) {
3023
- console.error(chalk17.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
3502
+ console.error(chalk18.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
3024
3503
  } else {
3025
- console.error(chalk17.red(error instanceof Error ? error.message : "Unknown error"));
3504
+ console.error(chalk18.red(error instanceof Error ? error.message : "Unknown error"));
3026
3505
  }
3027
3506
  process.exit(1);
3028
3507
  });
@@ -3039,7 +3518,7 @@ async function dbMigrate(options = {}) {
3039
3518
  process.exit(1);
3040
3519
  }
3041
3520
  if (options.withBackup) {
3042
- console.log(chalk18.blue("\u{1F4E6} Creating pre-migration backup...\n"));
3521
+ console.log(chalk19.blue("\u{1F4E6} Creating pre-migration backup...\n"));
3043
3522
  await dbBackup({
3044
3523
  format: "custom",
3045
3524
  tag: "pre-migration",
@@ -3052,66 +3531,66 @@ async function dbMigrate(options = {}) {
3052
3531
  const postgres = await import("postgres");
3053
3532
  loadEnv5();
3054
3533
  if (!env4.DATABASE_URL) {
3055
- console.error(chalk18.red("\u274C DATABASE_URL not found in environment"));
3534
+ console.error(chalk19.red("\u274C DATABASE_URL not found in environment"));
3056
3535
  process.exit(1);
3057
3536
  }
3058
3537
  const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
3059
3538
  const functions = discoverFunctionMigrations2(process.cwd());
3060
3539
  if (functions.length > 0) {
3061
- console.log(chalk18.blue("\u{1F4E6} Applying function package migrations:"));
3540
+ console.log(chalk19.blue("\u{1F4E6} Applying function package migrations:"));
3062
3541
  functions.forEach((func) => {
3063
- console.log(chalk18.dim(` - ${func.packageName}`));
3542
+ console.log(chalk19.dim(` - ${func.packageName}`));
3064
3543
  });
3065
3544
  await executeFunctionMigrations2(functions);
3066
- console.log(chalk18.green("\u2705 Function migrations applied\n"));
3545
+ console.log(chalk19.green("\u2705 Function migrations applied\n"));
3067
3546
  }
3068
- const projectMigrationsDir = join16(process.cwd(), "src/server/drizzle");
3069
- if (existsSync18(projectMigrationsDir)) {
3547
+ const projectMigrationsDir = join20(process.cwd(), "src/server/drizzle");
3548
+ if (existsSync21(projectMigrationsDir)) {
3070
3549
  const projConn = postgres.default(env4.DATABASE_URL, { max: 1 });
3071
3550
  const projDb = drizzle(projConn);
3072
3551
  try {
3073
- console.log(chalk18.blue("\u{1F4E6} Running project migrations..."));
3552
+ console.log(chalk19.blue("\u{1F4E6} Running project migrations..."));
3074
3553
  await migrate(projDb, {
3075
3554
  migrationsFolder: projectMigrationsDir,
3076
3555
  migrationsTable: PROJECT_MIGRATIONS_TABLE
3077
3556
  });
3078
- console.log(chalk18.green("\u2705 Project migrations applied successfully"));
3557
+ console.log(chalk19.green("\u2705 Project migrations applied successfully"));
3079
3558
  } finally {
3080
3559
  await projConn.end();
3081
3560
  }
3082
3561
  } else {
3083
- console.log(chalk18.dim("No project migrations found (src/server/drizzle)"));
3562
+ console.log(chalk19.dim("No project migrations found (src/server/drizzle)"));
3084
3563
  }
3085
3564
  }
3086
3565
 
3087
3566
  // src/commands/db/studio.ts
3088
- import chalk19 from "chalk";
3089
- import { existsSync as existsSync19, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3 } from "fs";
3567
+ import chalk20 from "chalk";
3568
+ import { existsSync as existsSync22, writeFileSync as writeFileSync14, unlinkSync as unlinkSync3 } from "fs";
3090
3569
  import { spawn as spawn3 } from "child_process";
3091
3570
  import { env as env5 } from "@spfn/core/config";
3092
3571
  async function dbStudio(requestedPort) {
3093
- console.log(chalk19.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
3572
+ console.log(chalk20.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
3094
3573
  const defaultPort = 4983;
3095
3574
  const startPort = requestedPort || defaultPort;
3096
3575
  let port;
3097
3576
  try {
3098
3577
  port = await findAvailablePort(startPort);
3099
3578
  if (port !== startPort) {
3100
- console.log(chalk19.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
3579
+ console.log(chalk20.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
3101
3580
  `));
3102
3581
  }
3103
3582
  } catch (error) {
3104
- console.error(chalk19.red(error instanceof Error ? error.message : "Failed to find available port"));
3583
+ console.error(chalk20.red(error instanceof Error ? error.message : "Failed to find available port"));
3105
3584
  process.exit(1);
3106
3585
  }
3107
- const hasUserConfig = existsSync19("./drizzle.config.ts");
3586
+ const hasUserConfig = existsSync22("./drizzle.config.ts");
3108
3587
  const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
3109
3588
  try {
3110
3589
  const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
3111
3590
  if (!hasUserConfig) {
3112
3591
  if (!env5.DATABASE_URL) {
3113
- console.error(chalk19.red("\u274C DATABASE_URL not found in environment"));
3114
- console.log(chalk19.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
3592
+ console.error(chalk20.red("\u274C DATABASE_URL not found in environment"));
3593
+ console.log(chalk20.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
3115
3594
  process.exit(1);
3116
3595
  }
3117
3596
  const { generateDrizzleConfigFile } = await import("@spfn/core/db");
@@ -3121,8 +3600,8 @@ async function dbStudio(requestedPort) {
3121
3600
  expandGlobs: true
3122
3601
  // Expand glob patterns for Studio compatibility
3123
3602
  });
3124
- writeFileSync11(tempConfigPath, configContent);
3125
- console.log(chalk19.dim("Using auto-generated Drizzle config\n"));
3603
+ writeFileSync14(tempConfigPath, configContent);
3604
+ console.log(chalk20.dim("Using auto-generated Drizzle config\n"));
3126
3605
  }
3127
3606
  const studioProcess = spawn3("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
3128
3607
  stdio: "inherit",
@@ -3130,26 +3609,26 @@ async function dbStudio(requestedPort) {
3130
3609
  env: shouldRelaxDbTls(env5.DATABASE_URL) ? { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" } : { ...process.env }
3131
3610
  });
3132
3611
  const cleanup = () => {
3133
- if (!hasUserConfig && existsSync19(tempConfigPath)) {
3612
+ if (!hasUserConfig && existsSync22(tempConfigPath)) {
3134
3613
  unlinkSync3(tempConfigPath);
3135
3614
  }
3136
3615
  };
3137
3616
  studioProcess.on("exit", (code) => {
3138
3617
  cleanup();
3139
3618
  if (code !== 0 && code !== null) {
3140
- console.error(chalk19.red(`
3619
+ console.error(chalk20.red(`
3141
3620
  \u274C Drizzle Studio exited with code ${code}`));
3142
3621
  process.exit(code);
3143
3622
  }
3144
3623
  });
3145
3624
  studioProcess.on("error", (error) => {
3146
3625
  cleanup();
3147
- console.error(chalk19.red("\u274C Failed to start Drizzle Studio"));
3148
- console.error(chalk19.red(error.message));
3626
+ console.error(chalk20.red("\u274C Failed to start Drizzle Studio"));
3627
+ console.error(chalk20.red(error.message));
3149
3628
  process.exit(1);
3150
3629
  });
3151
3630
  process.on("SIGINT", () => {
3152
- console.log(chalk19.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
3631
+ console.log(chalk20.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
3153
3632
  studioProcess.kill("SIGTERM");
3154
3633
  cleanup();
3155
3634
  process.exit(0);
@@ -3160,30 +3639,30 @@ async function dbStudio(requestedPort) {
3160
3639
  process.exit(0);
3161
3640
  });
3162
3641
  } catch (error) {
3163
- if (!hasUserConfig && existsSync19(tempConfigPath)) {
3642
+ if (!hasUserConfig && existsSync22(tempConfigPath)) {
3164
3643
  unlinkSync3(tempConfigPath);
3165
3644
  }
3166
- console.error(chalk19.red("\u274C Failed to start Drizzle Studio"));
3167
- console.error(chalk19.red(error instanceof Error ? error.message : "Unknown error"));
3645
+ console.error(chalk20.red("\u274C Failed to start Drizzle Studio"));
3646
+ console.error(chalk20.red(error instanceof Error ? error.message : "Unknown error"));
3168
3647
  process.exit(1);
3169
3648
  }
3170
3649
  }
3171
3650
 
3172
3651
  // src/commands/db/drop.ts
3173
- import chalk20 from "chalk";
3652
+ import chalk21 from "chalk";
3174
3653
  import prompts5 from "prompts";
3175
3654
  import { env as env6 } from "@spfn/core/config";
3176
3655
  import { loadEnv as loadEnv6 } from "@spfn/core/server";
3177
3656
  async function dbDrop() {
3178
3657
  loadEnv6();
3179
3658
  if (!env6.DATABASE_URL) {
3180
- console.error(chalk20.red("\u274C DATABASE_URL not found in environment"));
3181
- console.log(chalk20.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
3659
+ console.error(chalk21.red("\u274C DATABASE_URL not found in environment"));
3660
+ console.log(chalk21.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
3182
3661
  process.exit(1);
3183
3662
  }
3184
3663
  const dbInfo = parseDatabaseUrl(env6.DATABASE_URL);
3185
- console.log(chalk20.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
3186
- console.log(chalk20.dim(` Target: ${dbInfo.database} @ ${dbInfo.host}:${dbInfo.port}
3664
+ console.log(chalk21.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
3665
+ console.log(chalk21.dim(` Target: ${dbInfo.database} @ ${dbInfo.host}:${dbInfo.port}
3187
3666
  `));
3188
3667
  const { confirm } = await prompts5({
3189
3668
  type: "confirm",
@@ -3192,7 +3671,7 @@ async function dbDrop() {
3192
3671
  initial: false
3193
3672
  });
3194
3673
  if (!confirm) {
3195
- console.log(chalk20.gray("Cancelled."));
3674
+ console.log(chalk21.gray("Cancelled."));
3196
3675
  process.exit(0);
3197
3676
  }
3198
3677
  await confirmDangerousTarget(dbInfo);
@@ -3205,7 +3684,7 @@ async function dbDrop() {
3205
3684
  }
3206
3685
 
3207
3686
  // src/commands/db/check.ts
3208
- import chalk21 from "chalk";
3687
+ import chalk22 from "chalk";
3209
3688
  import ora8 from "ora";
3210
3689
  async function dbCheck() {
3211
3690
  const spinner = ora8("Checking database connection...").start();
@@ -3214,7 +3693,7 @@ async function dbCheck() {
3214
3693
  spinner.succeed("Database connection OK");
3215
3694
  } catch (error) {
3216
3695
  spinner.fail("Database connection failed");
3217
- console.error(chalk21.red(error instanceof Error ? error.message : "Unknown error"));
3696
+ console.error(chalk22.red(error instanceof Error ? error.message : "Unknown error"));
3218
3697
  process.exit(1);
3219
3698
  }
3220
3699
  }
@@ -3222,25 +3701,25 @@ async function dbCheck() {
3222
3701
  // src/commands/db/restore.ts
3223
3702
  import path4 from "path";
3224
3703
  import { spawn as spawn4 } from "child_process";
3225
- import chalk22 from "chalk";
3704
+ import chalk23 from "chalk";
3226
3705
  import ora9 from "ora";
3227
3706
  import prompts6 from "prompts";
3228
3707
  import { env as env7 } from "@spfn/core/config";
3229
3708
  import { loadEnv as loadEnv7 } from "@spfn/core/server";
3230
3709
  async function dbRestore(backupFile, options = {}) {
3231
- console.log(chalk22.blue("\u267B\uFE0F Restoring database from backup...\n"));
3710
+ console.log(chalk23.blue("\u267B\uFE0F Restoring database from backup...\n"));
3232
3711
  loadEnv7();
3233
3712
  const dbUrl = env7.DATABASE_URL;
3234
3713
  if (!dbUrl) {
3235
- console.error(chalk22.red("\u274C DATABASE_URL not found in environment"));
3236
- console.log(chalk22.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
3714
+ console.error(chalk23.red("\u274C DATABASE_URL not found in environment"));
3715
+ console.log(chalk23.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
3237
3716
  process.exit(1);
3238
3717
  }
3239
3718
  let file = backupFile;
3240
3719
  if (!file) {
3241
3720
  const backups = await listBackupFiles();
3242
3721
  if (backups.length === 0) {
3243
- console.log(chalk22.yellow("No backups found in ./backups directory"));
3722
+ console.log(chalk23.yellow("No backups found in ./backups directory"));
3244
3723
  process.exit(0);
3245
3724
  }
3246
3725
  const { selected } = await prompts6({
@@ -3253,31 +3732,31 @@ async function dbRestore(backupFile, options = {}) {
3253
3732
  }))
3254
3733
  });
3255
3734
  if (!selected) {
3256
- console.log(chalk22.gray("Cancelled"));
3735
+ console.log(chalk23.gray("Cancelled"));
3257
3736
  process.exit(0);
3258
3737
  }
3259
3738
  file = selected;
3260
3739
  }
3261
3740
  if (!file) {
3262
- console.error(chalk22.red("\u274C No backup file selected"));
3741
+ console.error(chalk23.red("\u274C No backup file selected"));
3263
3742
  process.exit(1);
3264
3743
  }
3265
3744
  const metadata = await loadBackupMetadata(file);
3266
3745
  if (metadata) {
3267
- console.log(chalk22.blue("\n\u{1F4CB} Backup Information:\n"));
3268
- console.log(chalk22.dim(` Database: ${metadata.database}`));
3269
- console.log(chalk22.dim(` Created: ${new Date(metadata.timestamp).toLocaleString()}`));
3746
+ console.log(chalk23.blue("\n\u{1F4CB} Backup Information:\n"));
3747
+ console.log(chalk23.dim(` Database: ${metadata.database}`));
3748
+ console.log(chalk23.dim(` Created: ${new Date(metadata.timestamp).toLocaleString()}`));
3270
3749
  if (metadata.environment) {
3271
- console.log(chalk22.dim(` Environment: ${metadata.environment}`));
3750
+ console.log(chalk23.dim(` Environment: ${metadata.environment}`));
3272
3751
  }
3273
3752
  if (metadata.tags && metadata.tags.length > 0) {
3274
- console.log(chalk22.dim(` Tags: ${metadata.tags.join(", ")}`));
3753
+ console.log(chalk23.dim(` Tags: ${metadata.tags.join(", ")}`));
3275
3754
  }
3276
3755
  if (metadata.backup.dataOnly) {
3277
- console.log(chalk22.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
3756
+ console.log(chalk23.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
3278
3757
  }
3279
3758
  if (metadata.backup.schemaOnly) {
3280
- console.log(chalk22.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
3759
+ console.log(chalk23.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
3281
3760
  }
3282
3761
  const warnings2 = [];
3283
3762
  const [currentGitInfo, currentMigrationInfo] = await Promise.all([
@@ -3300,8 +3779,8 @@ async function dbRestore(backupFile, options = {}) {
3300
3779
  }
3301
3780
  }
3302
3781
  if (warnings2.length > 0) {
3303
- console.log(chalk22.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
3304
- warnings2.forEach((warning) => console.log(chalk22.yellow(` - ${warning}`)));
3782
+ console.log(chalk23.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
3783
+ warnings2.forEach((warning) => console.log(chalk23.yellow(` - ${warning}`)));
3305
3784
  console.log("");
3306
3785
  }
3307
3786
  }
@@ -3309,16 +3788,16 @@ async function dbRestore(backupFile, options = {}) {
3309
3788
  const { confirm } = await prompts6({
3310
3789
  type: "confirm",
3311
3790
  name: "confirm",
3312
- message: chalk22.yellow(`\u26A0\uFE0F This will replace all data in "${dbInfo.database}" @ ${dbInfo.host}:${dbInfo.port}. Continue?`),
3791
+ message: chalk23.yellow(`\u26A0\uFE0F This will replace all data in "${dbInfo.database}" @ ${dbInfo.host}:${dbInfo.port}. Continue?`),
3313
3792
  initial: false
3314
3793
  });
3315
3794
  if (!confirm) {
3316
- console.log(chalk22.gray("Cancelled"));
3795
+ console.log(chalk23.gray("Cancelled"));
3317
3796
  process.exit(0);
3318
3797
  }
3319
3798
  await confirmDangerousTarget(dbInfo);
3320
3799
  if (options.dataOnly && options.schemaOnly) {
3321
- console.error(chalk22.red("\u274C Cannot use --data-only and --schema-only together"));
3800
+ console.error(chalk23.red("\u274C Cannot use --data-only and --schema-only together"));
3322
3801
  process.exit(1);
3323
3802
  }
3324
3803
  const ext = path4.extname(file);
@@ -3346,11 +3825,11 @@ async function dbRestore(backupFile, options = {}) {
3346
3825
  args.push(file);
3347
3826
  } else {
3348
3827
  if (options.drop) {
3349
- console.log(chalk22.yellow("\u26A0\uFE0F Note: --drop only works with custom format backups (.dump) and was ignored."));
3828
+ console.log(chalk23.yellow("\u26A0\uFE0F Note: --drop only works with custom format backups (.dump) and was ignored."));
3350
3829
  }
3351
3830
  if (options.dataOnly || options.schemaOnly) {
3352
- console.log(chalk22.yellow("\u26A0\uFE0F Note: --data-only and --schema-only options only work with custom format backups (.dump)"));
3353
- console.log(chalk22.yellow(" For SQL files, the backup must have been created with the desired option.\n"));
3831
+ console.log(chalk23.yellow("\u26A0\uFE0F Note: --data-only and --schema-only options only work with custom format backups (.dump)"));
3832
+ console.log(chalk23.yellow(" For SQL files, the backup must have been created with the desired option.\n"));
3354
3833
  }
3355
3834
  args.push("-h", dbInfo.host);
3356
3835
  args.push("-p", dbInfo.port);
@@ -3393,7 +3872,7 @@ async function dbRestore(backupFile, options = {}) {
3393
3872
  }
3394
3873
  if (verbose) {
3395
3874
  spinner.stop();
3396
- console.log(chalk22.dim(` ${line.trim()}`));
3875
+ console.log(chalk23.dim(` ${line.trim()}`));
3397
3876
  spinner.start();
3398
3877
  }
3399
3878
  }
@@ -3401,7 +3880,7 @@ async function dbRestore(backupFile, options = {}) {
3401
3880
  restoreProcess.stdout?.on("data", (data) => {
3402
3881
  if (verbose) {
3403
3882
  spinner.stop();
3404
- console.log(chalk22.dim(` ${data.toString().trim()}`));
3883
+ console.log(chalk23.dim(` ${data.toString().trim()}`));
3405
3884
  spinner.start();
3406
3885
  }
3407
3886
  });
@@ -3411,31 +3890,31 @@ async function dbRestore(backupFile, options = {}) {
3411
3890
  const summary = objectCount > 0 ? ` (${objectCount} objects)` : "";
3412
3891
  spinner.succeed(`Restore completed${summary}`);
3413
3892
  if (warnings.length > 0) {
3414
- console.log(chalk22.yellow(`
3893
+ console.log(chalk23.yellow(`
3415
3894
  \u26A0\uFE0F Warnings during restore (${warnings.length}):
3416
3895
  `));
3417
3896
  for (const w of warnings) {
3418
- console.log(chalk22.yellow(` - ${w}`));
3897
+ console.log(chalk23.yellow(` - ${w}`));
3419
3898
  }
3420
3899
  }
3421
- console.log(chalk22.green("\n\u2705 Database restored successfully"));
3900
+ console.log(chalk23.green("\n\u2705 Database restored successfully"));
3422
3901
  resolve3();
3423
3902
  } else {
3424
3903
  spinner.fail("Restore failed");
3425
3904
  if (errors.length > 0) {
3426
- console.error(chalk22.red(`
3905
+ console.error(chalk23.red(`
3427
3906
  \u274C Errors (${errors.length}):
3428
3907
  `));
3429
3908
  for (const e of errors) {
3430
- console.error(chalk22.red(` - ${e}`));
3909
+ console.error(chalk23.red(` - ${e}`));
3431
3910
  }
3432
3911
  }
3433
3912
  if (warnings.length > 0) {
3434
- console.log(chalk22.yellow(`
3913
+ console.log(chalk23.yellow(`
3435
3914
  \u26A0\uFE0F Warnings (${warnings.length}):
3436
3915
  `));
3437
3916
  for (const w of warnings) {
3438
- console.log(chalk22.yellow(` - ${w}`));
3917
+ console.log(chalk23.yellow(` - ${w}`));
3439
3918
  }
3440
3919
  }
3441
3920
  const fallback = errors.length === 0 && warnings.length === 0 ? "Restore failed with no output" : "";
@@ -3449,7 +3928,7 @@ async function dbRestore(backupFile, options = {}) {
3449
3928
  }).catch((error) => {
3450
3929
  const msg = error instanceof Error ? error.message : "Unknown error";
3451
3930
  if (msg) {
3452
- console.error(chalk22.red(`
3931
+ console.error(chalk23.red(`
3453
3932
  \u274C ${msg}`));
3454
3933
  }
3455
3934
  process.exit(1);
@@ -3457,17 +3936,17 @@ async function dbRestore(backupFile, options = {}) {
3457
3936
  }
3458
3937
 
3459
3938
  // src/commands/db/list.ts
3460
- import chalk23 from "chalk";
3939
+ import chalk24 from "chalk";
3461
3940
  async function dbBackupList() {
3462
- console.log(chalk23.blue("\u{1F4CB} Database backups:\n"));
3941
+ console.log(chalk24.blue("\u{1F4CB} Database backups:\n"));
3463
3942
  const backups = await listBackupFiles();
3464
3943
  if (backups.length === 0) {
3465
- console.log(chalk23.yellow("No backups found in ./backups directory"));
3466
- console.log(chalk23.gray("\n\u{1F4A1} Create a backup with: pnpm spfn db backup\n"));
3944
+ console.log(chalk24.yellow("No backups found in ./backups directory"));
3945
+ console.log(chalk24.gray("\n\u{1F4A1} Create a backup with: pnpm spfn db backup\n"));
3467
3946
  return;
3468
3947
  }
3469
- console.log(chalk23.bold(" Date Size File"));
3470
- 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"));
3948
+ console.log(chalk24.bold(" Date Size File"));
3949
+ console.log(chalk24.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"));
3471
3950
  backups.forEach((backup) => {
3472
3951
  const date = backup.date.toLocaleString("en-US", {
3473
3952
  year: "numeric",
@@ -3478,23 +3957,23 @@ async function dbBackupList() {
3478
3957
  second: "2-digit"
3479
3958
  });
3480
3959
  const sizeStr = backup.size.padEnd(10);
3481
- console.log(chalk23.white(` ${date} ${sizeStr} ${backup.name}`));
3960
+ console.log(chalk24.white(` ${date} ${sizeStr} ${backup.name}`));
3482
3961
  });
3483
- console.log(chalk23.gray(`
3962
+ console.log(chalk24.gray(`
3484
3963
  Total: ${backups.length} backup(s)
3485
3964
  `));
3486
3965
  }
3487
3966
 
3488
3967
  // src/commands/db/clean.ts
3489
3968
  import { promises as fs4 } from "fs";
3490
- import chalk24 from "chalk";
3969
+ import chalk25 from "chalk";
3491
3970
  import ora10 from "ora";
3492
3971
  import prompts7 from "prompts";
3493
3972
  async function dbBackupClean(options) {
3494
- console.log(chalk24.blue("\u{1F9F9} Cleaning old backups...\n"));
3973
+ console.log(chalk25.blue("\u{1F9F9} Cleaning old backups...\n"));
3495
3974
  const backups = await listBackupFiles();
3496
3975
  if (backups.length === 0) {
3497
- console.log(chalk24.yellow("No backups found"));
3976
+ console.log(chalk25.yellow("No backups found"));
3498
3977
  return;
3499
3978
  }
3500
3979
  let toDelete = [];
@@ -3511,13 +3990,13 @@ async function dbBackupClean(options) {
3511
3990
  toDelete = backups.slice(defaultKeep);
3512
3991
  }
3513
3992
  if (toDelete.length === 0) {
3514
- console.log(chalk24.green("\u2705 No backups to clean"));
3993
+ console.log(chalk25.green("\u2705 No backups to clean"));
3515
3994
  return;
3516
3995
  }
3517
- console.log(chalk24.yellow(`The following ${toDelete.length} backup(s) will be deleted:
3996
+ console.log(chalk25.yellow(`The following ${toDelete.length} backup(s) will be deleted:
3518
3997
  `));
3519
3998
  toDelete.forEach((backup) => {
3520
- console.log(chalk24.gray(` - ${backup.name} (${backup.size})`));
3999
+ console.log(chalk25.gray(` - ${backup.name} (${backup.size})`));
3521
4000
  });
3522
4001
  const { confirm } = await prompts7({
3523
4002
  type: "confirm",
@@ -3526,7 +4005,7 @@ async function dbBackupClean(options) {
3526
4005
  initial: false
3527
4006
  });
3528
4007
  if (!confirm) {
3529
- console.log(chalk24.gray("Cancelled"));
4008
+ console.log(chalk25.gray("Cancelled"));
3530
4009
  return;
3531
4010
  }
3532
4011
  const spinner = ora10("Deleting backups...").start();
@@ -3537,19 +4016,19 @@ async function dbBackupClean(options) {
3537
4016
  fs4.rm(backup.path.replace(/\.(sql|dump)$/, ".meta.json"), { force: true })
3538
4017
  ]));
3539
4018
  spinner.succeed("Backups deleted");
3540
- console.log(chalk24.green(`
4019
+ console.log(chalk25.green(`
3541
4020
  \u2705 Deleted ${toDelete.length} backup(s)`));
3542
4021
  } catch (error) {
3543
4022
  spinner.fail("Failed to delete backups");
3544
- console.error(chalk24.red(error instanceof Error ? error.message : "Unknown error"));
4023
+ console.error(chalk25.red(error instanceof Error ? error.message : "Unknown error"));
3545
4024
  process.exit(1);
3546
4025
  }
3547
4026
  }
3548
4027
 
3549
4028
  // src/commands/db/reindex.ts
3550
- import { existsSync as existsSync20, readFileSync as readFileSync7, writeFileSync as writeFileSync12, renameSync, copyFileSync } from "fs";
3551
- import { join as join17 } from "path";
3552
- import chalk25 from "chalk";
4029
+ import { existsSync as existsSync23, readFileSync as readFileSync10, writeFileSync as writeFileSync15, renameSync, copyFileSync } from "fs";
4030
+ import { join as join21 } from "path";
4031
+ import chalk26 from "chalk";
3553
4032
  import { loadEnv as loadEnv8 } from "@spfn/core/server";
3554
4033
  function isTimestampPrefix(tag) {
3555
4034
  const prefix = tag.split("_")[0];
@@ -3570,15 +4049,15 @@ async function dbReindex(options = {}) {
3570
4049
  const { getDrizzleConfig } = await import("@spfn/core/db");
3571
4050
  const config = getDrizzleConfig({ disablePackageDiscovery: true });
3572
4051
  const outDir = config.out;
3573
- const journalPath = join17(outDir, "meta", "_journal.json");
3574
- if (!existsSync20(journalPath)) {
3575
- console.error(chalk25.red("\u274C No _journal.json found at:"), journalPath);
3576
- console.log(chalk25.yellow("\u{1F4A1} Run `spfn db generate` first to create migrations"));
4052
+ const journalPath = join21(outDir, "meta", "_journal.json");
4053
+ if (!existsSync23(journalPath)) {
4054
+ console.error(chalk26.red("\u274C No _journal.json found at:"), journalPath);
4055
+ console.log(chalk26.yellow("\u{1F4A1} Run `spfn db generate` first to create migrations"));
3577
4056
  process.exit(1);
3578
4057
  }
3579
- const journal = JSON.parse(readFileSync7(journalPath, "utf-8"));
4058
+ const journal = JSON.parse(readFileSync10(journalPath, "utf-8"));
3580
4059
  if (journal.entries.length === 0) {
3581
- console.log(chalk25.yellow("No migration entries found \u2014 nothing to reindex."));
4060
+ console.log(chalk26.yellow("No migration entries found \u2014 nothing to reindex."));
3582
4061
  return;
3583
4062
  }
3584
4063
  const renames = [];
@@ -3592,46 +4071,46 @@ async function dbReindex(options = {}) {
3592
4071
  const { prefix: oldPrefix, suffix } = parseTag(entry.tag);
3593
4072
  const newPrefix = String(entry.when);
3594
4073
  const newTag = suffix ? `${newPrefix}_${suffix}` : newPrefix;
3595
- const oldSql = join17(outDir, `${entry.tag}.sql`);
3596
- const newSql = join17(outDir, `${newTag}.sql`);
3597
- if (existsSync20(oldSql)) {
4074
+ const oldSql = join21(outDir, `${entry.tag}.sql`);
4075
+ const newSql = join21(outDir, `${newTag}.sql`);
4076
+ if (existsSync23(oldSql)) {
3598
4077
  renames.push({ type: "sql", from: oldSql, to: newSql });
3599
4078
  }
3600
- const oldSnapshot = join17(outDir, "meta", `${oldPrefix}_snapshot.json`);
3601
- const newSnapshot = join17(outDir, "meta", `${newPrefix}_snapshot.json`);
3602
- if (existsSync20(oldSnapshot)) {
4079
+ const oldSnapshot = join21(outDir, "meta", `${oldPrefix}_snapshot.json`);
4080
+ const newSnapshot = join21(outDir, "meta", `${newPrefix}_snapshot.json`);
4081
+ if (existsSync23(oldSnapshot)) {
3603
4082
  renames.push({ type: "snapshot", from: oldSnapshot, to: newSnapshot });
3604
4083
  }
3605
4084
  tagUpdates.push({ idx: entry.idx, oldTag: entry.tag, newTag });
3606
4085
  }
3607
4086
  if (tagUpdates.length === 0) {
3608
- console.log(chalk25.green("\u2705 All migrations already use timestamp prefix \u2014 nothing to do."));
4087
+ console.log(chalk26.green("\u2705 All migrations already use timestamp prefix \u2014 nothing to do."));
3609
4088
  if (skipped > 0) {
3610
- console.log(chalk25.dim(` (${skipped} entries already timestamp-prefixed)`));
4089
+ console.log(chalk26.dim(` (${skipped} entries already timestamp-prefixed)`));
3611
4090
  }
3612
4091
  return;
3613
4092
  }
3614
- console.log(chalk25.bold("\n\u{1F4CB} Reindex plan:\n"));
4093
+ console.log(chalk26.bold("\n\u{1F4CB} Reindex plan:\n"));
3615
4094
  for (const update of tagUpdates) {
3616
4095
  console.log(
3617
- chalk25.dim(` [${update.idx}]`),
3618
- chalk25.red(update.oldTag),
3619
- chalk25.dim("\u2192"),
3620
- chalk25.green(update.newTag)
4096
+ chalk26.dim(` [${update.idx}]`),
4097
+ chalk26.red(update.oldTag),
4098
+ chalk26.dim("\u2192"),
4099
+ chalk26.green(update.newTag)
3621
4100
  );
3622
4101
  }
3623
- console.log(chalk25.dim(`
4102
+ console.log(chalk26.dim(`
3624
4103
  ${renames.length} file(s) to rename, ${tagUpdates.length} journal tag(s) to update`));
3625
4104
  if (skipped > 0) {
3626
- console.log(chalk25.dim(` ${skipped} entry/entries already timestamp-prefixed (skipped)`));
4105
+ console.log(chalk26.dim(` ${skipped} entry/entries already timestamp-prefixed (skipped)`));
3627
4106
  }
3628
4107
  if (options.dryRun) {
3629
- console.log(chalk25.yellow("\n\u{1F50D} Dry run \u2014 no changes applied."));
4108
+ console.log(chalk26.yellow("\n\u{1F50D} Dry run \u2014 no changes applied."));
3630
4109
  return;
3631
4110
  }
3632
4111
  const backupPath = journalPath + ".bak";
3633
4112
  copyFileSync(journalPath, backupPath);
3634
- console.log(chalk25.dim(`
4113
+ console.log(chalk26.dim(`
3635
4114
  Backed up journal \u2192 ${backupPath}`));
3636
4115
  for (const rename of renames) {
3637
4116
  renameSync(rename.from, rename.to);
@@ -3642,8 +4121,8 @@ async function dbReindex(options = {}) {
3642
4121
  entry.tag = update.newTag;
3643
4122
  }
3644
4123
  }
3645
- writeFileSync12(journalPath, JSON.stringify(journal, null, 2) + "\n");
3646
- console.log(chalk25.green(`
4124
+ writeFileSync15(journalPath, JSON.stringify(journal, null, 2) + "\n");
4125
+ console.log(chalk26.green(`
3647
4126
  \u2705 Reindex complete \u2014 ${tagUpdates.length} migration(s) converted to timestamp prefix.`));
3648
4127
  }
3649
4128
 
@@ -3664,52 +4143,52 @@ dbCommand.command("reindex").description("Convert migration files from sequentia
3664
4143
  // src/commands/add.ts
3665
4144
  init_package_manager();
3666
4145
  import { Command as Command10 } from "commander";
3667
- import { existsSync as existsSync21, readFileSync as readFileSync8 } from "fs";
3668
- import { join as join18 } from "path";
3669
- import { execa as execa7 } from "execa";
3670
- import chalk26 from "chalk";
4146
+ import { existsSync as existsSync24, readFileSync as readFileSync11 } from "fs";
4147
+ import { join as join22 } from "path";
4148
+ import { execa as execa9 } from "execa";
4149
+ import chalk27 from "chalk";
3671
4150
  import ora11 from "ora";
3672
4151
  async function addPackage(packageName) {
3673
4152
  if (!packageName.includes("/")) {
3674
- console.error(chalk26.red("\u274C Please specify full package name"));
3675
- console.log(chalk26.yellow("\n\u{1F4A1} Examples:"));
3676
- console.log(chalk26.gray(" pnpm spfn add @spfn/cms"));
3677
- console.log(chalk26.gray(" pnpm spfn add @mycompany/spfn-analytics"));
4153
+ console.error(chalk27.red("\u274C Please specify full package name"));
4154
+ console.log(chalk27.yellow("\n\u{1F4A1} Examples:"));
4155
+ console.log(chalk27.gray(" pnpm spfn add @spfn/cms"));
4156
+ console.log(chalk27.gray(" pnpm spfn add @mycompany/spfn-analytics"));
3678
4157
  process.exit(1);
3679
4158
  }
3680
- console.log(chalk26.blue(`
4159
+ console.log(chalk27.blue(`
3681
4160
  \u{1F4E6} Setting up ${packageName}...
3682
4161
  `));
3683
- const pkgPath = join18(process.cwd(), "node_modules", ...packageName.split("/"));
3684
- const pkgJsonPath = join18(pkgPath, "package.json");
3685
- if (!existsSync21(pkgJsonPath)) {
4162
+ const pkgPath = join22(process.cwd(), "node_modules", ...packageName.split("/"));
4163
+ const pkgJsonPath = join22(pkgPath, "package.json");
4164
+ if (!existsSync24(pkgJsonPath)) {
3686
4165
  const pm = detectPackageManager(process.cwd());
3687
4166
  const installSpinner = ora11("Installing package...").start();
3688
4167
  try {
3689
- await execa7(pm, ["add", packageName]);
4168
+ await execa9(pm, ["add", packageName]);
3690
4169
  installSpinner.succeed("Package installed");
3691
4170
  } catch (error) {
3692
4171
  installSpinner.fail("Failed to install package");
3693
- console.error(chalk26.red(error instanceof Error ? error.message : "Unknown error"));
4172
+ console.error(chalk27.red(error instanceof Error ? error.message : "Unknown error"));
3694
4173
  process.exit(1);
3695
4174
  }
3696
4175
  } else {
3697
- console.log(chalk26.gray("\u2713 Package already installed (using local version)\n"));
4176
+ console.log(chalk27.gray("\u2713 Package already installed (using local version)\n"));
3698
4177
  }
3699
- if (!existsSync21(pkgJsonPath)) {
3700
- console.error(chalk26.red(`\u274C Package ${packageName} not found after installation`));
4178
+ if (!existsSync24(pkgJsonPath)) {
4179
+ console.error(chalk27.red(`\u274C Package ${packageName} not found after installation`));
3701
4180
  process.exit(1);
3702
4181
  }
3703
- const pkgJson = JSON.parse(readFileSync8(pkgJsonPath, "utf-8"));
4182
+ const pkgJson = JSON.parse(readFileSync11(pkgJsonPath, "utf-8"));
3704
4183
  if (pkgJson.spfn?.migrations) {
3705
- console.log(chalk26.blue(`
4184
+ console.log(chalk27.blue(`
3706
4185
  \u{1F5C4}\uFE0F Setting up database for ${packageName}...
3707
4186
  `));
3708
4187
  const { env: env8 } = await import("@spfn/core/config");
3709
4188
  if (!env8.DATABASE_URL) {
3710
- console.log(chalk26.yellow("\u26A0\uFE0F DATABASE_URL not found"));
3711
- console.log(chalk26.gray("Skipping database setup. Run migrations manually when ready:\n"));
3712
- console.log(chalk26.gray(" pnpm spfn db push\n"));
4189
+ console.log(chalk27.yellow("\u26A0\uFE0F DATABASE_URL not found"));
4190
+ console.log(chalk27.gray("Skipping database setup. Run migrations manually when ready:\n"));
4191
+ console.log(chalk27.gray(" pnpm spfn db push\n"));
3713
4192
  } else {
3714
4193
  const { discoverFunctionMigrations: discoverFunctionMigrations2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
3715
4194
  const functions = discoverFunctionMigrations2(process.cwd());
@@ -3721,21 +4200,21 @@ async function addPackage(packageName) {
3721
4200
  spinner.succeed("Migrations applied");
3722
4201
  } catch (error) {
3723
4202
  spinner.fail("Failed to apply migrations");
3724
- console.error(chalk26.red(error instanceof Error ? error.message : "Unknown error"));
4203
+ console.error(chalk27.red(error instanceof Error ? error.message : "Unknown error"));
3725
4204
  process.exit(1);
3726
4205
  }
3727
4206
  } else {
3728
- console.log(chalk26.gray("\u2139\uFE0F No migrations found for this package"));
4207
+ console.log(chalk27.gray("\u2139\uFE0F No migrations found for this package"));
3729
4208
  }
3730
4209
  }
3731
4210
  } else {
3732
- console.log(chalk26.gray("\n\u2139\uFE0F No database migrations to apply"));
4211
+ console.log(chalk27.gray("\n\u2139\uFE0F No database migrations to apply"));
3733
4212
  }
3734
- console.log(chalk26.green(`
4213
+ console.log(chalk27.green(`
3735
4214
  \u2705 ${packageName} installed successfully!
3736
4215
  `));
3737
4216
  if (pkgJson.spfn?.setupMessage) {
3738
- console.log(chalk26.cyan("\u{1F4DA} Setup Guide:"));
4217
+ console.log(chalk27.cyan("\u{1F4DA} Setup Guide:"));
3739
4218
  console.log(pkgJson.spfn.setupMessage);
3740
4219
  console.log();
3741
4220
  }
@@ -3744,11 +4223,43 @@ var addCommand = new Command10("add").description("Install and set up SPFN ecosy
3744
4223
 
3745
4224
  // src/commands/env.ts
3746
4225
  import { Command as Command11 } from "commander";
3747
- import chalk27 from "chalk";
3748
- import { existsSync as existsSync22, readFileSync as readFileSync9, writeFileSync as writeFileSync13 } from "fs";
4226
+ import chalk28 from "chalk";
4227
+ import { existsSync as existsSync25, readFileSync as readFileSync12, writeFileSync as writeFileSync16 } from "fs";
3749
4228
  import { resolve as resolve2 } from "path";
3750
- import { parse } from "dotenv";
4229
+ import { parse as parse2 } from "dotenv";
4230
+
4231
+ // src/utils/env-schema.ts
3751
4232
  var VALID_ENVS = ["local", "development", "staging", "production", "test"];
4233
+ async function loadEnvSchema(packageName) {
4234
+ try {
4235
+ const module = await import(`${packageName}/config`);
4236
+ if (!module.envSchema) {
4237
+ throw new Error(`Package ${packageName} does not export envSchema from config`);
4238
+ }
4239
+ return module.envSchema;
4240
+ } catch (error) {
4241
+ if (error instanceof Error && error.message.includes("does not export envSchema")) {
4242
+ throw error;
4243
+ }
4244
+ const message = error instanceof Error ? error.message : String(error);
4245
+ throw new Error(`Failed to load package ${packageName}: ${message}`);
4246
+ }
4247
+ }
4248
+ function getTargetFile(schema) {
4249
+ const isNextjs = schema.nextjs ?? schema.key?.startsWith("NEXT_PUBLIC_");
4250
+ if (isNextjs) {
4251
+ return schema.sensitive ? ".env.local" : ".env";
4252
+ }
4253
+ return ".env.server";
4254
+ }
4255
+ function secretEntries(schema) {
4256
+ return Object.values(schema).filter((entry) => entry.sensitive);
4257
+ }
4258
+ function generatableSecrets(schema) {
4259
+ return secretEntries(schema).filter((entry) => !!entry.generate);
4260
+ }
4261
+
4262
+ // src/commands/env.ts
3752
4263
  var BASE_ENV_FILES = {
3753
4264
  nextjs: [".env", ".env.local"],
3754
4265
  server: [".env.server"]
@@ -3767,51 +4278,28 @@ function getEnvFilesForEnvironment(nodeEnv) {
3767
4278
  files.push(".env.server");
3768
4279
  return files;
3769
4280
  }
3770
- function getTargetFile(schema) {
3771
- const isNextjs = schema.nextjs ?? schema.key?.startsWith("NEXT_PUBLIC_");
3772
- if (isNextjs) {
3773
- return schema.sensitive ? ".env.local" : ".env";
3774
- }
3775
- return ".env.server";
3776
- }
3777
- async function loadEnvSchema(packageName) {
3778
- try {
3779
- const schemaPath = `${packageName}/config`;
3780
- const module = await import(schemaPath);
3781
- if (!module.envSchema) {
3782
- throw new Error(`Package ${packageName} does not export envSchema from config`);
3783
- }
3784
- return module.envSchema;
3785
- } catch (error) {
3786
- if (error instanceof Error && error.message.includes("does not export envSchema")) {
3787
- throw error;
3788
- }
3789
- const errorMessage = error instanceof Error ? error.message : String(error);
3790
- throw new Error(`Failed to load package ${packageName}: ${errorMessage}`);
3791
- }
3792
- }
3793
4281
  function formatType(type) {
3794
4282
  const typeColors = {
3795
- string: chalk27.green,
3796
- number: chalk27.blue,
3797
- boolean: chalk27.yellow,
3798
- url: chalk27.cyan,
3799
- enum: chalk27.magenta,
3800
- json: chalk27.red
4283
+ string: chalk28.green,
4284
+ number: chalk28.blue,
4285
+ boolean: chalk28.yellow,
4286
+ url: chalk28.cyan,
4287
+ enum: chalk28.magenta,
4288
+ json: chalk28.red
3801
4289
  };
3802
- return (typeColors[type] || chalk27.white)(type);
4290
+ return (typeColors[type] || chalk28.white)(type);
3803
4291
  }
3804
4292
  function formatDefault(value, type) {
3805
4293
  if (value === void 0) {
3806
- return chalk27.dim("(none)");
4294
+ return chalk28.dim("(none)");
3807
4295
  }
3808
4296
  if (type === "string" || type === "url") {
3809
- return chalk27.green(`"${value}"`);
4297
+ return chalk28.green(`"${value}"`);
3810
4298
  }
3811
4299
  if (type === "boolean") {
3812
- return value ? chalk27.green("true") : chalk27.red("false");
4300
+ return value ? chalk28.green("true") : chalk28.red("false");
3813
4301
  }
3814
- return chalk27.cyan(String(value));
4302
+ return chalk28.cyan(String(value));
3815
4303
  }
3816
4304
  async function listEnvVars(options) {
3817
4305
  const packageName = options.package || "@spfn/core";
@@ -3825,28 +4313,28 @@ async function listEnvVars(options) {
3825
4313
  acc[target].push([key, schema]);
3826
4314
  return acc;
3827
4315
  }, {});
3828
- console.log(chalk27.blue.bold(`
4316
+ console.log(chalk28.blue.bold(`
3829
4317
  \u{1F4CB} Environment Variables by File (${packageName})
3830
4318
  `));
3831
4319
  for (const [file, vars] of Object.entries(grouped)) {
3832
- console.log(chalk27.bold.magenta(`
4320
+ console.log(chalk28.bold.magenta(`
3833
4321
  ${file}`));
3834
- console.log(chalk27.dim("\u2500".repeat(50)));
4322
+ console.log(chalk28.dim("\u2500".repeat(50)));
3835
4323
  for (const [key, schema] of vars) {
3836
4324
  printEnvVar(key, schema);
3837
4325
  }
3838
4326
  }
3839
4327
  } else {
3840
- console.log(chalk27.blue.bold(`
4328
+ console.log(chalk28.blue.bold(`
3841
4329
  \u{1F4CB} Environment Variables (${packageName})
3842
4330
  `));
3843
4331
  for (const [key, schema] of allVars) {
3844
4332
  printEnvVar(key, schema, true);
3845
4333
  }
3846
4334
  }
3847
- console.log(chalk27.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
4335
+ console.log(chalk28.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
3848
4336
  } catch (error) {
3849
- console.error(chalk27.red(`
4337
+ console.error(chalk28.red(`
3850
4338
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
3851
4339
  `));
3852
4340
  process.exit(1);
@@ -3854,17 +4342,17 @@ ${file}`));
3854
4342
  }
3855
4343
  function printEnvVar(key, schema, showFile = false) {
3856
4344
  const typeStr = formatType(schema.type);
3857
- const requiredStr = schema.required || schema.default !== void 0 ? chalk27.red("[required]") : chalk27.dim("[optional]");
3858
- const sensitiveStr = schema.sensitive ? chalk27.yellow(" [sensitive]") : "";
3859
- const fileStr = showFile ? chalk27.dim(` \u2192 ${getTargetFile(schema)}`) : "";
3860
- console.log(`${chalk27.bold.cyan(key)} ${chalk27.dim("(")}${typeStr}${chalk27.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
3861
- console.log(` ${chalk27.dim(schema.description)}`);
4345
+ const requiredStr = schema.required || schema.default !== void 0 ? chalk28.red("[required]") : chalk28.dim("[optional]");
4346
+ const sensitiveStr = schema.sensitive ? chalk28.yellow(" [sensitive]") : "";
4347
+ const fileStr = showFile ? chalk28.dim(` \u2192 ${getTargetFile(schema)}`) : "";
4348
+ console.log(`${chalk28.bold.cyan(key)} ${chalk28.dim("(")}${typeStr}${chalk28.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
4349
+ console.log(` ${chalk28.dim(schema.description)}`);
3862
4350
  if (schema.default !== void 0) {
3863
- console.log(` ${chalk27.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
4351
+ console.log(` ${chalk28.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
3864
4352
  }
3865
4353
  if (schema.examples && schema.examples.length > 0) {
3866
4354
  const exampleStr = schema.examples.map((ex) => formatDefault(ex, schema.type)).join(", ");
3867
- console.log(` ${chalk27.dim("Examples:")} ${exampleStr}`);
4355
+ console.log(` ${chalk28.dim("Examples:")} ${exampleStr}`);
3868
4356
  }
3869
4357
  console.log();
3870
4358
  }
@@ -3872,7 +4360,7 @@ async function showEnvStats(options) {
3872
4360
  const packageName = options.package || "@spfn/core";
3873
4361
  try {
3874
4362
  const envSchema = await loadEnvSchema(packageName);
3875
- console.log(chalk27.blue.bold(`
4363
+ console.log(chalk28.blue.bold(`
3876
4364
  \u{1F4CA} Environment Variable Statistics (${packageName})
3877
4365
  `));
3878
4366
  const allVars = Object.entries(envSchema);
@@ -3894,24 +4382,24 @@ async function showEnvStats(options) {
3894
4382
  acc[file] = (acc[file] || 0) + 1;
3895
4383
  return acc;
3896
4384
  }, {});
3897
- console.log(`${chalk27.bold("Total variables:")} ${chalk27.cyan(allVars.length)}`);
3898
- console.log(`${chalk27.bold("Required:")} ${chalk27.red(required.length)}`);
3899
- console.log(`${chalk27.bold("Optional:")} ${chalk27.dim(optional.length)}`);
3900
- console.log(`${chalk27.bold("Sensitive:")} ${chalk27.yellow(sensitive.length)}`);
3901
- console.log(chalk27.bold("\nBy Target:"));
3902
- console.log(` ${chalk27.blue("Next.js accessible:")} ${chalk27.cyan(nextjsVars.length)}`);
3903
- console.log(` ${chalk27.magenta("SPFN server only:")} ${chalk27.cyan(serverOnlyVars.length)}`);
3904
- console.log(chalk27.bold("\nBy File:"));
4385
+ console.log(`${chalk28.bold("Total variables:")} ${chalk28.cyan(allVars.length)}`);
4386
+ console.log(`${chalk28.bold("Required:")} ${chalk28.red(required.length)}`);
4387
+ console.log(`${chalk28.bold("Optional:")} ${chalk28.dim(optional.length)}`);
4388
+ console.log(`${chalk28.bold("Sensitive:")} ${chalk28.yellow(sensitive.length)}`);
4389
+ console.log(chalk28.bold("\nBy Target:"));
4390
+ console.log(` ${chalk28.blue("Next.js accessible:")} ${chalk28.cyan(nextjsVars.length)}`);
4391
+ console.log(` ${chalk28.magenta("SPFN server only:")} ${chalk28.cyan(serverOnlyVars.length)}`);
4392
+ console.log(chalk28.bold("\nBy File:"));
3905
4393
  for (const [file, count] of Object.entries(fileCount)) {
3906
- console.log(` ${chalk27.dim(file)}: ${chalk27.cyan(count)}`);
4394
+ console.log(` ${chalk28.dim(file)}: ${chalk28.cyan(count)}`);
3907
4395
  }
3908
- console.log(chalk27.bold("\nBy Type:"));
4396
+ console.log(chalk28.bold("\nBy Type:"));
3909
4397
  for (const [type, count] of Object.entries(typeCount)) {
3910
- console.log(` ${formatType(type)}: ${chalk27.cyan(count)}`);
4398
+ console.log(` ${formatType(type)}: ${chalk28.cyan(count)}`);
3911
4399
  }
3912
4400
  console.log();
3913
4401
  } catch (error) {
3914
- console.error(chalk27.red(`
4402
+ console.error(chalk28.red(`
3915
4403
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
3916
4404
  `));
3917
4405
  process.exit(1);
@@ -3931,26 +4419,26 @@ async function searchEnvVars(query, options) {
3931
4419
  }
3932
4420
  }
3933
4421
  if (results.length === 0) {
3934
- console.log(chalk27.yellow(`
4422
+ console.log(chalk28.yellow(`
3935
4423
  \u26A0\uFE0F No environment variables found matching "${query}"
3936
4424
  `));
3937
4425
  return;
3938
4426
  }
3939
- console.log(chalk27.blue.bold(`
4427
+ console.log(chalk28.blue.bold(`
3940
4428
  \u{1F50D} Found ${results.length} environment variable(s) matching "${query}"
3941
4429
  `));
3942
4430
  for (const [key, schema] of results) {
3943
4431
  const typeStr = formatType(schema.type);
3944
- const requiredStr = schema.required || schema.default !== void 0 ? chalk27.red("[required]") : chalk27.dim("[optional]");
3945
- console.log(`${chalk27.bold.cyan(key)} ${chalk27.dim("(")}${typeStr}${chalk27.dim(")")} ${requiredStr}`);
3946
- console.log(` ${chalk27.dim(schema.description)}`);
4432
+ const requiredStr = schema.required || schema.default !== void 0 ? chalk28.red("[required]") : chalk28.dim("[optional]");
4433
+ console.log(`${chalk28.bold.cyan(key)} ${chalk28.dim("(")}${typeStr}${chalk28.dim(")")} ${requiredStr}`);
4434
+ console.log(` ${chalk28.dim(schema.description)}`);
3947
4435
  if (schema.default !== void 0) {
3948
- console.log(` ${chalk27.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
4436
+ console.log(` ${chalk28.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
3949
4437
  }
3950
4438
  console.log();
3951
4439
  }
3952
4440
  } catch (error) {
3953
- console.error(chalk27.red(`
4441
+ console.error(chalk28.red(`
3954
4442
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
3955
4443
  `));
3956
4444
  process.exit(1);
@@ -3962,9 +4450,9 @@ envCommand.command("stats").description("Show environment variable statistics").
3962
4450
  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);
3963
4451
  function validateEnvOption(envValue) {
3964
4452
  if (!VALID_ENVS.includes(envValue)) {
3965
- console.error(chalk27.red(`
4453
+ console.error(chalk28.red(`
3966
4454
  \u274C Invalid environment: "${envValue}"`));
3967
- console.log(chalk27.dim(` Valid values: ${VALID_ENVS.join(", ")}
4455
+ console.log(chalk28.dim(` Valid values: ${VALID_ENVS.join(", ")}
3968
4456
  `));
3969
4457
  process.exit(1);
3970
4458
  }
@@ -3985,8 +4473,8 @@ async function initEnvFiles(options) {
3985
4473
  return acc;
3986
4474
  }, {});
3987
4475
  if (targetEnv) {
3988
- console.log(chalk27.blue.bold(`
3989
- \u{1F680} Generating .env template files for ${chalk27.cyan(targetEnv)} environment
4476
+ console.log(chalk28.blue.bold(`
4477
+ \u{1F680} Generating .env template files for ${chalk28.cyan(targetEnv)} environment
3990
4478
  `));
3991
4479
  const envSpecificFiles = {};
3992
4480
  const committedVars = allVars.filter(([_, schema]) => !schema.sensitive);
@@ -4002,24 +4490,24 @@ async function initEnvFiles(options) {
4002
4490
  writeEnvTemplate(cwd, file, vars, options.force ?? false);
4003
4491
  }
4004
4492
  } else {
4005
- console.log(chalk27.blue.bold(`
4493
+ console.log(chalk28.blue.bold(`
4006
4494
  \u{1F680} Generating .env template files
4007
4495
  `));
4008
4496
  for (const [file, vars] of Object.entries(grouped)) {
4009
4497
  writeEnvTemplate(cwd, file, vars, options.force ?? false);
4010
4498
  }
4011
4499
  }
4012
- console.log(chalk27.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
4013
- console.log(chalk27.dim(" cp .env.example .env"));
4014
- console.log(chalk27.dim(" cp .env.local.example .env.local"));
4015
- console.log(chalk27.dim(" cp .env.server.example .env.server"));
4500
+ console.log(chalk28.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
4501
+ console.log(chalk28.dim(" cp .env.example .env"));
4502
+ console.log(chalk28.dim(" cp .env.local.example .env.local"));
4503
+ console.log(chalk28.dim(" cp .env.server.example .env.server"));
4016
4504
  if (targetEnv) {
4017
- console.log(chalk27.dim(` cp .env.${targetEnv}.example .env.${targetEnv}`));
4018
- console.log(chalk27.dim(` cp .env.${targetEnv}.local.example .env.${targetEnv}.local`));
4505
+ console.log(chalk28.dim(` cp .env.${targetEnv}.example .env.${targetEnv}`));
4506
+ console.log(chalk28.dim(` cp .env.${targetEnv}.local.example .env.${targetEnv}.local`));
4019
4507
  }
4020
4508
  console.log("");
4021
4509
  } catch (error) {
4022
- console.error(chalk27.red(`
4510
+ console.error(chalk28.red(`
4023
4511
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
4024
4512
  `));
4025
4513
  process.exit(1);
@@ -4027,12 +4515,12 @@ async function initEnvFiles(options) {
4027
4515
  }
4028
4516
  function writeEnvTemplate(cwd, file, vars, force) {
4029
4517
  const filePath = resolve2(cwd, file);
4030
- if (existsSync22(filePath) && !force) {
4031
- console.log(chalk27.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
4518
+ if (existsSync25(filePath) && !force) {
4519
+ console.log(chalk28.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
4032
4520
  return;
4033
4521
  }
4034
- writeFileSync13(filePath, generateEnvFileContent(vars), "utf-8");
4035
- console.log(chalk27.green(` \u2705 ${file} (${vars.length} variables)`));
4522
+ writeFileSync16(filePath, generateEnvFileContent(vars), "utf-8");
4523
+ console.log(chalk28.green(` \u2705 ${file} (${vars.length} variables)`));
4036
4524
  }
4037
4525
  function generateEnvFileContent(vars) {
4038
4526
  const lines = [
@@ -4067,7 +4555,7 @@ async function checkEnvFiles(options) {
4067
4555
  const envSchema = await loadEnvSchema(packageName);
4068
4556
  const allVars = Object.entries(envSchema);
4069
4557
  const envLabel = targetEnv ? ` (${targetEnv})` : "";
4070
- console.log(chalk27.blue.bold(`
4558
+ console.log(chalk28.blue.bold(`
4071
4559
  \u{1F50D} Checking .env files against schema${envLabel}
4072
4560
  `));
4073
4561
  const filesToCheck = targetEnv ? getEnvFilesForEnvironment(targetEnv) : [...BASE_ENV_FILES.nextjs, ...BASE_ENV_FILES.server];
@@ -4076,15 +4564,15 @@ async function checkEnvFiles(options) {
4076
4564
  const warnings = [];
4077
4565
  for (const file of filesToCheck) {
4078
4566
  const filePath = resolve2(cwd, file);
4079
- if (!existsSync22(filePath)) {
4567
+ if (!existsSync25(filePath)) {
4080
4568
  continue;
4081
4569
  }
4082
- const content = readFileSync9(filePath, "utf-8");
4083
- const parsed = parse(content);
4570
+ const content = readFileSync12(filePath, "utf-8");
4571
+ const parsed = parse2(content);
4084
4572
  for (const [key, value] of Object.entries(parsed)) {
4085
4573
  loadedEnv[key] = { value: value || "", file };
4086
4574
  }
4087
- console.log(chalk27.dim(` \u{1F4C4} ${file} loaded`));
4575
+ console.log(chalk28.dim(` \u{1F4C4} ${file} loaded`));
4088
4576
  }
4089
4577
  console.log("");
4090
4578
  for (const [key, schema] of allVars) {
@@ -4092,7 +4580,7 @@ async function checkEnvFiles(options) {
4092
4580
  const found = loadedEnv[key];
4093
4581
  if (!found) {
4094
4582
  if (schema.required && schema.default === void 0) {
4095
- issues.push(`${chalk27.red("\u2717")} ${chalk27.cyan(key)} is required but not found in any .env file`);
4583
+ issues.push(`${chalk28.red("\u2717")} ${chalk28.cyan(key)} is required but not found in any .env file`);
4096
4584
  }
4097
4585
  continue;
4098
4586
  }
@@ -4102,11 +4590,11 @@ async function checkEnvFiles(options) {
4102
4590
  if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
4103
4591
  if (schema.sensitive) {
4104
4592
  issues.push(
4105
- `${chalk27.red("\u2717")} ${chalk27.cyan(key)} is sensitive and should be in ${chalk27.magenta(expectedFile)}, but found in ${chalk27.yellow(found.file)} (security risk!)`
4593
+ `${chalk28.red("\u2717")} ${chalk28.cyan(key)} is sensitive and should be in ${chalk28.magenta(expectedFile)}, but found in ${chalk28.yellow(found.file)} (security risk!)`
4106
4594
  );
4107
4595
  } else {
4108
4596
  warnings.push(
4109
- `${chalk27.yellow("\u26A0")} ${chalk27.cyan(key)} should be in ${chalk27.magenta(expectedFile)}, but found in ${chalk27.dim(found.file)}`
4597
+ `${chalk28.yellow("\u26A0")} ${chalk28.cyan(key)} should be in ${chalk28.magenta(expectedFile)}, but found in ${chalk28.dim(found.file)}`
4110
4598
  );
4111
4599
  }
4112
4600
  }
@@ -4114,34 +4602,34 @@ async function checkEnvFiles(options) {
4114
4602
  for (const [key, { file }] of Object.entries(loadedEnv)) {
4115
4603
  const inSchema = allVars.some(([k]) => k === key);
4116
4604
  if (!inSchema) {
4117
- warnings.push(`${chalk27.yellow("\u26A0")} ${chalk27.cyan(key)} in ${chalk27.dim(file)} is not in schema`);
4605
+ warnings.push(`${chalk28.yellow("\u26A0")} ${chalk28.cyan(key)} in ${chalk28.dim(file)} is not in schema`);
4118
4606
  }
4119
4607
  }
4120
4608
  if (issues.length > 0) {
4121
- console.log(chalk27.red.bold("Issues:"));
4609
+ console.log(chalk28.red.bold("Issues:"));
4122
4610
  for (const issue of issues) {
4123
4611
  console.log(` ${issue}`);
4124
4612
  }
4125
4613
  console.log("");
4126
4614
  }
4127
4615
  if (warnings.length > 0) {
4128
- console.log(chalk27.yellow.bold("Warnings:"));
4616
+ console.log(chalk28.yellow.bold("Warnings:"));
4129
4617
  for (const warning of warnings) {
4130
4618
  console.log(` ${warning}`);
4131
4619
  }
4132
4620
  console.log("");
4133
4621
  }
4134
4622
  if (issues.length === 0 && warnings.length === 0) {
4135
- console.log(chalk27.green("\u2705 All environment variables are correctly configured!\n"));
4623
+ console.log(chalk28.green("\u2705 All environment variables are correctly configured!\n"));
4136
4624
  } else {
4137
- console.log(chalk27.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
4625
+ console.log(chalk28.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
4138
4626
  `));
4139
4627
  if (issues.length > 0) {
4140
4628
  process.exit(1);
4141
4629
  }
4142
4630
  }
4143
4631
  } catch (error) {
4144
- console.error(chalk27.red(`
4632
+ console.error(chalk28.red(`
4145
4633
  \u274C ${error instanceof Error ? error.message : "Unknown error"}
4146
4634
  `));
4147
4635
  process.exit(1);
@@ -4155,15 +4643,15 @@ async function validateEnvVars(options) {
4155
4643
  if (targetEnv) {
4156
4644
  const { loadEnv: loadEnv9 } = await import("@spfn/core/env/loader");
4157
4645
  const result = loadEnv9({ nodeEnv: targetEnv });
4158
- console.log(chalk27.blue.bold(`
4159
- \u{1F50D} Validating environment variables for ${chalk27.cyan(targetEnv)}
4646
+ console.log(chalk28.blue.bold(`
4647
+ \u{1F50D} Validating environment variables for ${chalk28.cyan(targetEnv)}
4160
4648
  `));
4161
4649
  if (result.loadedFiles.length > 0) {
4162
- console.log(chalk27.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
4650
+ console.log(chalk28.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
4163
4651
  }
4164
4652
  console.log("");
4165
4653
  } else {
4166
- console.log(chalk27.blue.bold(`
4654
+ console.log(chalk28.blue.bold(`
4167
4655
  \u{1F50D} Validating environment variables
4168
4656
  `));
4169
4657
  }
@@ -4171,7 +4659,7 @@ async function validateEnvVars(options) {
4171
4659
  const allWarnings = [];
4172
4660
  for (const packageName of packages) {
4173
4661
  try {
4174
- console.log(chalk27.dim(` \u{1F4E6} ${packageName}`));
4662
+ console.log(chalk28.dim(` \u{1F4E6} ${packageName}`));
4175
4663
  const envSchema = await loadEnvSchema(packageName);
4176
4664
  const { createEnvRegistry } = await import("@spfn/core/env");
4177
4665
  const registry = createEnvRegistry(envSchema);
@@ -4184,10 +4672,10 @@ async function validateEnvVars(options) {
4184
4672
  }
4185
4673
  } catch (error) {
4186
4674
  if (error instanceof Error && error.message.includes("does not export envSchema")) {
4187
- console.log(chalk27.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
4675
+ console.log(chalk28.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
4188
4676
  continue;
4189
4677
  }
4190
- console.error(chalk27.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
4678
+ console.error(chalk28.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
4191
4679
  if (options.strict) {
4192
4680
  process.exit(1);
4193
4681
  }
@@ -4195,32 +4683,32 @@ async function validateEnvVars(options) {
4195
4683
  }
4196
4684
  console.log("");
4197
4685
  if (allErrors.length > 0) {
4198
- console.log(chalk27.red.bold(`\u274C Validation Errors (${allErrors.length}):
4686
+ console.log(chalk28.red.bold(`\u274C Validation Errors (${allErrors.length}):
4199
4687
  `));
4200
4688
  for (const error of allErrors) {
4201
- console.log(` ${chalk27.red("\u2717")} ${chalk27.cyan(error.key)}`);
4202
- console.log(` ${chalk27.dim(error.message)}`);
4203
- console.log(` ${chalk27.dim(`from ${error.package}`)}`);
4689
+ console.log(` ${chalk28.red("\u2717")} ${chalk28.cyan(error.key)}`);
4690
+ console.log(` ${chalk28.dim(error.message)}`);
4691
+ console.log(` ${chalk28.dim(`from ${error.package}`)}`);
4204
4692
  console.log("");
4205
4693
  }
4206
4694
  }
4207
4695
  if (allWarnings.length > 0) {
4208
- console.log(chalk27.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
4696
+ console.log(chalk28.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
4209
4697
  `));
4210
4698
  for (const warning of allWarnings) {
4211
- console.log(` ${chalk27.yellow("\u26A0")} ${chalk27.cyan(warning.key)}`);
4212
- console.log(` ${chalk27.dim(warning.message)}`);
4699
+ console.log(` ${chalk28.yellow("\u26A0")} ${chalk28.cyan(warning.key)}`);
4700
+ console.log(` ${chalk28.dim(warning.message)}`);
4213
4701
  console.log("");
4214
4702
  }
4215
4703
  }
4216
4704
  if (allErrors.length === 0 && allWarnings.length === 0) {
4217
- console.log(chalk27.green.bold("\u2705 All environment variables are valid!\n"));
4705
+ console.log(chalk28.green.bold("\u2705 All environment variables are valid!\n"));
4218
4706
  } else if (allErrors.length === 0) {
4219
- console.log(chalk27.green("\u2705 No errors found."));
4220
- console.log(chalk27.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
4707
+ console.log(chalk28.green("\u2705 No errors found."));
4708
+ console.log(chalk28.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
4221
4709
  `));
4222
4710
  } else {
4223
- console.log(chalk27.red(`
4711
+ console.log(chalk28.red(`
4224
4712
  \u274C Validation failed with ${allErrors.length} error(s)
4225
4713
  `));
4226
4714
  process.exit(1);
@@ -4228,9 +4716,591 @@ async function validateEnvVars(options) {
4228
4716
  }
4229
4717
  envCommand.command("validate").description("Validate environment variables against schema (for CI/CD)").option("-p, --packages <packages...>", "Packages to validate", ["@spfn/core"]).option("-e, --env <environment>", "Load env files for specific environment before validating").option("-s, --strict", "Exit on any error (including load failures)").action(validateEnvVars);
4230
4718
 
4719
+ // src/commands/secret/index.ts
4720
+ import { Command as Command12 } from "commander";
4721
+
4722
+ // src/commands/secret/set.ts
4723
+ init_logger();
4724
+ import prompts8 from "prompts";
4725
+ import chalk29 from "chalk";
4726
+
4727
+ // src/commands/secret/options.ts
4728
+ init_logger();
4729
+ function resolveEnv(env8) {
4730
+ const value = env8 ?? "local";
4731
+ if (!VALID_ENVS.includes(value)) {
4732
+ logger.error(`Invalid environment: "${value}". Valid values: ${VALID_ENVS.join(", ")}`);
4733
+ process.exit(1);
4734
+ }
4735
+ return value;
4736
+ }
4737
+
4738
+ // src/commands/secret/store-value.ts
4739
+ init_logger();
4740
+ import { join as join24 } from "path";
4741
+ init_env_file();
4742
+
4743
+ // src/utils/sops.ts
4744
+ import { execa as execa10 } from "execa";
4745
+ import { existsSync as existsSync26, mkdirSync as mkdirSync3, writeFileSync as writeFileSync17 } from "fs";
4746
+ import { dirname as dirname3 } from "path";
4747
+ async function ensureSopsInstalled() {
4748
+ try {
4749
+ await execa10("sops", ["--version"]);
4750
+ } catch {
4751
+ throw new Error("`sops` not found on PATH. Install it: https://github.com/getsops/sops");
4752
+ }
4753
+ }
4754
+ async function sopsDecrypt(absFile) {
4755
+ if (!existsSync26(absFile)) {
4756
+ return {};
4757
+ }
4758
+ const { stdout } = await execa10("sops", ["--decrypt", "--output-type", "json", absFile]);
4759
+ return JSON.parse(stdout);
4760
+ }
4761
+ async function sopsSetValue(absFile, relFile, key, value) {
4762
+ if (existsSync26(absFile)) {
4763
+ await execa10("sops", ["set", absFile, `["${key}"]`, JSON.stringify(value)]);
4764
+ return;
4765
+ }
4766
+ mkdirSync3(dirname3(absFile), { recursive: true });
4767
+ const { stdout } = await execa10(
4768
+ "sops",
4769
+ [
4770
+ "--encrypt",
4771
+ "--input-type",
4772
+ "json",
4773
+ "--output-type",
4774
+ "json",
4775
+ "--filename-override",
4776
+ relFile,
4777
+ "/dev/stdin"
4778
+ ],
4779
+ { input: JSON.stringify({ [key]: value }) }
4780
+ );
4781
+ writeFileSync17(absFile, stdout);
4782
+ }
4783
+ async function sopsUpdateKeys(absFile) {
4784
+ await execa10("sops", ["updatekeys", "-y", absFile]);
4785
+ }
4786
+
4787
+ // src/utils/secret-config.ts
4788
+ import { existsSync as existsSync27, readdirSync as readdirSync3 } from "fs";
4789
+ import { join as join23 } from "path";
4790
+ var SECRETS_DIR = "secrets";
4791
+ function getSopsFile(cwd, env8) {
4792
+ const relFile = `${SECRETS_DIR}/${env8}.enc.json`;
4793
+ return { absFile: join23(cwd, relFile), relFile };
4794
+ }
4795
+ function findUp(cwd, filename, maxDepth = 6) {
4796
+ let dir = cwd;
4797
+ for (let depth = 0; depth < maxDepth; depth++) {
4798
+ if (existsSync27(join23(dir, filename))) {
4799
+ return dir;
4800
+ }
4801
+ const parent = join23(dir, "..");
4802
+ if (parent === dir) {
4803
+ break;
4804
+ }
4805
+ dir = parent;
4806
+ }
4807
+ return null;
4808
+ }
4809
+ function findSopsConfig(cwd) {
4810
+ const dir = findUp(cwd, ".sops.yaml");
4811
+ return dir ? join23(dir, ".sops.yaml") : null;
4812
+ }
4813
+ function hasSopsConfig(cwd) {
4814
+ return findSopsConfig(cwd) !== null;
4815
+ }
4816
+ function listSopsFiles(cwd) {
4817
+ const dir = join23(cwd, SECRETS_DIR);
4818
+ if (!existsSync27(dir)) {
4819
+ return [];
4820
+ }
4821
+ return readdirSync3(dir).filter((name) => name.endsWith(".enc.json")).map((name) => join23(dir, name));
4822
+ }
4823
+
4824
+ // src/commands/secret/store-value.ts
4825
+ function isLocalEnv(env8) {
4826
+ return env8 === "local";
4827
+ }
4828
+ var KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
4829
+ function assertValidKey(key) {
4830
+ if (!KEY_PATTERN.test(key)) {
4831
+ throw new Error(`Invalid secret key "${key}". Use an env-var name: letters, digits, and underscores, not starting with a digit.`);
4832
+ }
4833
+ }
4834
+ async function describeTarget(env8) {
4835
+ if (isLocalEnv(env8)) {
4836
+ return detectStore().label;
4837
+ }
4838
+ return `SOPS (${getSopsFile(process.cwd(), env8).relFile})`;
4839
+ }
4840
+ async function storeSecret(cwd, env8, key, value) {
4841
+ assertValidKey(key);
4842
+ if (isLocalEnv(env8)) {
4843
+ const store = detectStore();
4844
+ if (!await store.isAvailable()) {
4845
+ throw new Error(
4846
+ `${store.label} is unavailable on this machine. On Windows install \`@napi-rs/keyring\`; on headless Linux no keyring daemon is present.`
4847
+ );
4848
+ }
4849
+ await store.set(keychainName(key), value);
4850
+ const serverEnvPath = join24(cwd, ".env.server");
4851
+ const result = upsertEnvVar(serverEnvPath, key, keychainRef(key));
4852
+ restrictEnvFilePerms(serverEnvPath);
4853
+ ensureGitignored(cwd, [{ pattern: ".env.server", comment: "spfn server env (secrets)" }]);
4854
+ logger.success(`Stored ${key} in ${store.label}; .env.server ${result} the reference.`);
4855
+ return;
4856
+ }
4857
+ await ensureSopsInstalled();
4858
+ if (!hasSopsConfig(cwd)) {
4859
+ logger.warn(
4860
+ ".sops.yaml not found \u2014 add a backend (run `spfn secret keygen` for age, or a KMS rule) so the file can be decrypted on deploy."
4861
+ );
4862
+ }
4863
+ const { absFile, relFile } = getSopsFile(cwd, env8);
4864
+ await sopsSetValue(absFile, relFile, key, value);
4865
+ logger.success(`Set ${key} in ${relFile} (encrypted). Commit and deploy to apply.`);
4866
+ }
4867
+
4868
+ // src/commands/secret/set.ts
4869
+ async function secretSet(key, options) {
4870
+ const env8 = resolveEnv(options.env);
4871
+ const pkg = options.package ?? "@spfn/core";
4872
+ const resolvedKey = key ?? await pickSecretKey(pkg);
4873
+ if (!resolvedKey) {
4874
+ logger.error("No secret key provided.");
4875
+ process.exit(1);
4876
+ }
4877
+ await warnIfNotSecret(pkg, resolvedKey);
4878
+ const { value } = await prompts8({
4879
+ type: "password",
4880
+ name: "value",
4881
+ message: `Value for ${chalk29.cyan(resolvedKey)} (${env8})`
4882
+ });
4883
+ if (!value) {
4884
+ logger.warn("Cancelled \u2014 no value entered.");
4885
+ process.exit(0);
4886
+ }
4887
+ logger.step(`Storing ${resolvedKey} \u2192 ${await describeTarget(env8)}`);
4888
+ try {
4889
+ await storeSecret(process.cwd(), env8, resolvedKey, value);
4890
+ } catch (error) {
4891
+ logger.error(error instanceof Error ? error.message : String(error));
4892
+ process.exit(1);
4893
+ }
4894
+ }
4895
+ async function pickSecretKey(pkg) {
4896
+ let entries;
4897
+ try {
4898
+ entries = secretEntries(await loadEnvSchema(pkg));
4899
+ } catch {
4900
+ return void 0;
4901
+ }
4902
+ if (entries.length === 0) {
4903
+ return void 0;
4904
+ }
4905
+ const { key } = await prompts8({
4906
+ type: "select",
4907
+ name: "key",
4908
+ message: "Which secret?",
4909
+ choices: entries.map((entry) => ({
4910
+ title: entry.key,
4911
+ description: entry.description,
4912
+ value: entry.key
4913
+ }))
4914
+ });
4915
+ return key;
4916
+ }
4917
+ async function warnIfNotSecret(pkg, key) {
4918
+ try {
4919
+ const schema = await loadEnvSchema(pkg);
4920
+ const entry = schema[key];
4921
+ if (entry && !entry.sensitive) {
4922
+ logger.warn(`${key} is not marked as a secret in ${pkg}. Consider declaring it with envSecret().`);
4923
+ }
4924
+ } catch {
4925
+ }
4926
+ }
4927
+
4928
+ // src/commands/secret/list.ts
4929
+ init_logger();
4930
+ import chalk30 from "chalk";
4931
+ async function secretList(options) {
4932
+ const env8 = resolveEnv(options.env);
4933
+ const pkg = options.package ?? "@spfn/core";
4934
+ let entries;
4935
+ try {
4936
+ entries = secretEntries(await loadEnvSchema(pkg));
4937
+ } catch (error) {
4938
+ logger.error(error instanceof Error ? error.message : String(error));
4939
+ process.exit(1);
4940
+ }
4941
+ if (entries.length === 0) {
4942
+ logger.info(`No secrets declared in ${pkg}.`);
4943
+ return;
4944
+ }
4945
+ const present = await loadPresence(env8, entries);
4946
+ console.log(chalk30.blue.bold(`
4947
+ \u{1F511} Secrets (${pkg}) \u2014 ${env8}
4948
+ `));
4949
+ for (const entry of entries) {
4950
+ const status = statusOf(entry, present.has(entry.key));
4951
+ console.log(` ${badge(status)} ${chalk30.cyan(entry.key)}${entry.generate ? chalk30.dim(" (generatable)") : ""}`);
4952
+ }
4953
+ console.log();
4954
+ }
4955
+ async function loadPresence(env8, entries) {
4956
+ if (isLocalEnv(env8)) {
4957
+ const store = detectStore();
4958
+ if (!await store.isAvailable()) {
4959
+ logger.warn(`${store.label} unavailable \u2014 status shown as missing.`);
4960
+ return /* @__PURE__ */ new Set();
4961
+ }
4962
+ const present = /* @__PURE__ */ new Set();
4963
+ for (const entry of entries) {
4964
+ if (await store.get(keychainName(entry.key)) !== null) {
4965
+ present.add(entry.key);
4966
+ }
4967
+ }
4968
+ return present;
4969
+ }
4970
+ await ensureSopsInstalled();
4971
+ const { absFile } = getSopsFile(process.cwd(), env8);
4972
+ const decrypted = await sopsDecrypt(absFile);
4973
+ return new Set(Object.keys(decrypted));
4974
+ }
4975
+ function statusOf(entry, present) {
4976
+ if (present) {
4977
+ return "set";
4978
+ }
4979
+ return entry.generate ? "missing" : "awaiting-input";
4980
+ }
4981
+ function badge(status) {
4982
+ switch (status) {
4983
+ case "set":
4984
+ return chalk30.green("\u25CF");
4985
+ case "missing":
4986
+ return chalk30.yellow("\u25CB");
4987
+ case "awaiting-input":
4988
+ return chalk30.red("\u25CB");
4989
+ }
4990
+ }
4991
+
4992
+ // src/commands/secret/generate.ts
4993
+ init_logger();
4994
+ import chalk31 from "chalk";
4995
+ async function secretGenerate(key, options) {
4996
+ const env8 = resolveEnv(options.env);
4997
+ const pkg = options.package ?? "@spfn/core";
4998
+ let schema;
4999
+ try {
5000
+ schema = await loadEnvSchema(pkg);
5001
+ } catch (error) {
5002
+ logger.error(error instanceof Error ? error.message : String(error));
5003
+ process.exit(1);
5004
+ }
5005
+ const targets = options.all ? generatableSecrets(schema) : [requireGeneratable(schema, key)];
5006
+ if (targets.length === 0) {
5007
+ logger.warn("No generatable secrets found (none declare a `generate` strategy).");
5008
+ return;
5009
+ }
5010
+ logger.step(`Generating ${targets.length} secret(s) \u2192 ${await describeTarget(env8)}`);
5011
+ for (const entry of targets) {
5012
+ const value = generateSecretValue(entry.generate);
5013
+ try {
5014
+ await storeSecret(process.cwd(), env8, entry.key, value);
5015
+ } catch (error) {
5016
+ logger.error(`${entry.key}: ${error instanceof Error ? error.message : String(error)}`);
5017
+ process.exit(1);
5018
+ }
5019
+ }
5020
+ }
5021
+ function requireGeneratable(schema, key) {
5022
+ if (!key) {
5023
+ logger.error("Provide a KEY, or use --all to generate every generatable secret.");
5024
+ process.exit(1);
5025
+ }
5026
+ const entry = schema[key];
5027
+ if (!entry) {
5028
+ logger.error(`${key} is not declared in the schema.`);
5029
+ process.exit(1);
5030
+ }
5031
+ if (!entry.generate) {
5032
+ logger.error(`${chalk31.cyan(key)} has no generate strategy \u2014 it's an external value. Use \`spfn secret set ${key}\`.`);
5033
+ process.exit(1);
5034
+ }
5035
+ return entry;
5036
+ }
5037
+
5038
+ // src/commands/secret/rotate.ts
5039
+ init_logger();
5040
+ import chalk32 from "chalk";
5041
+ async function secretRotate(key, options) {
5042
+ const env8 = resolveEnv(options.env);
5043
+ const pkg = options.package ?? "@spfn/core";
5044
+ let schema;
5045
+ try {
5046
+ schema = await loadEnvSchema(pkg);
5047
+ } catch (error) {
5048
+ logger.error(error instanceof Error ? error.message : String(error));
5049
+ process.exit(1);
5050
+ }
5051
+ const targets = options.all ? secretEntries(schema) : [requireEntry(schema, key)];
5052
+ const generatable = targets.filter((entry) => entry.generate);
5053
+ const external = targets.filter((entry) => !entry.generate);
5054
+ if (generatable.length > 0) {
5055
+ logger.step(`Rotating ${generatable.length} secret(s) \u2192 ${await describeTarget(env8)}`);
5056
+ for (const entry of generatable) {
5057
+ try {
5058
+ await storeSecret(process.cwd(), env8, entry.key, generateSecretValue(entry.generate));
5059
+ } catch (error) {
5060
+ logger.error(`${entry.key}: ${error instanceof Error ? error.message : String(error)}`);
5061
+ process.exit(1);
5062
+ }
5063
+ }
5064
+ }
5065
+ for (const entry of external) {
5066
+ logger.warn(`${chalk32.cyan(entry.key)} is external \u2014 reissue it at the provider, then \`spfn secret set ${entry.key} --env ${env8}\`.`);
5067
+ }
5068
+ if (generatable.length > 0) {
5069
+ logger.info("Rotation updates the source only \u2014 commit and deploy to apply.");
5070
+ }
5071
+ }
5072
+ function requireEntry(schema, key) {
5073
+ if (!key) {
5074
+ logger.error("Provide a KEY, or use --all to rotate every secret.");
5075
+ process.exit(1);
5076
+ }
5077
+ const entry = schema[key];
5078
+ if (!entry) {
5079
+ logger.error(`${key} is not declared in the schema.`);
5080
+ process.exit(1);
5081
+ }
5082
+ return entry;
5083
+ }
5084
+
5085
+ // src/commands/secret/keygen.ts
5086
+ init_logger();
5087
+ import { execa as execa11 } from "execa";
5088
+ import { existsSync as existsSync28, mkdirSync as mkdirSync4 } from "fs";
5089
+ import { homedir } from "os";
5090
+ import { dirname as dirname4, join as join25 } from "path";
5091
+ import chalk33 from "chalk";
5092
+ function ageKeyFile() {
5093
+ return process.env.SOPS_AGE_KEY_FILE ?? join25(homedir(), ".config", "sops", "age", "keys.txt");
5094
+ }
5095
+ async function ensureAgeInstalled() {
5096
+ try {
5097
+ await execa11("age-keygen", ["--version"]);
5098
+ } catch {
5099
+ throw new Error("`age-keygen` not found on PATH. Install age: https://github.com/FiloSottile/age");
5100
+ }
5101
+ }
5102
+ function publicKeys(text) {
5103
+ return text.match(/age1[0-9a-z]+/g) ?? [];
5104
+ }
5105
+ async function secretKeygen() {
5106
+ try {
5107
+ await ensureAgeInstalled();
5108
+ } catch (error) {
5109
+ logger.error(error instanceof Error ? error.message : String(error));
5110
+ process.exit(1);
5111
+ }
5112
+ const keyFile = ageKeyFile();
5113
+ if (existsSync28(keyFile)) {
5114
+ const { stdout } = await execa11("age-keygen", ["-y", keyFile]);
5115
+ logger.warn(`age key file already exists: ${keyFile}`);
5116
+ printPublicKeys(publicKeys(stdout));
5117
+ return;
5118
+ }
5119
+ mkdirSync4(dirname4(keyFile), { recursive: true });
5120
+ const { stderr } = await execa11("age-keygen", ["-o", keyFile]);
5121
+ logger.success(`Created age key: ${keyFile}`);
5122
+ printPublicKeys(publicKeys(stderr));
5123
+ }
5124
+ function printPublicKeys(keys) {
5125
+ if (keys.length === 0) {
5126
+ logger.warn("Could not read the public key \u2014 run `age-keygen -y <file>` manually.");
5127
+ return;
5128
+ }
5129
+ console.log(chalk33.bold("\nPublic key(s):"));
5130
+ for (const key of keys) {
5131
+ console.log(` ${chalk33.cyan(key)}`);
5132
+ }
5133
+ console.log(chalk33.dim("\nRegister it as a recipient:"));
5134
+ console.log(chalk33.dim(` spfn secret recipients add ${keys[0]}
5135
+ `));
5136
+ }
5137
+
5138
+ // src/commands/secret/recipients.ts
5139
+ init_logger();
5140
+ import { existsSync as existsSync29, readFileSync as readFileSync13, writeFileSync as writeFileSync18 } from "fs";
5141
+ import { join as join26 } from "path";
5142
+ import { parse as parse3, stringify } from "yaml";
5143
+ import chalk34 from "chalk";
5144
+ var SOPS_CONFIG = ".sops.yaml";
5145
+ var DEFAULT_PATH_REGEX = "secrets/.*\\.enc\\.json$";
5146
+ var AGE_RECIPIENT = /^age1[0-9a-z]+$/;
5147
+ async function secretRecipients(action, key, _options) {
5148
+ const cwd = process.cwd();
5149
+ const configPath = join26(cwd, SOPS_CONFIG);
5150
+ if (action === "list") {
5151
+ listRecipients(configPath);
5152
+ return;
5153
+ }
5154
+ if (action !== "add" && action !== "remove") {
5155
+ logger.error(`Unknown action "${action}". Use add | remove | list.`);
5156
+ process.exit(1);
5157
+ }
5158
+ if (!key) {
5159
+ logger.error(`A recipient (age1...) is required for \`recipients ${action}\`.`);
5160
+ process.exit(1);
5161
+ }
5162
+ const config = loadConfig(configPath);
5163
+ const rule = ensureRule(config);
5164
+ const recipients = parseRecipients(rule.age);
5165
+ if (action === "add") {
5166
+ if (!AGE_RECIPIENT.test(key)) {
5167
+ logger.error(`"${key}" is not an age recipient (expected age1\u2026). Run \`spfn secret keygen\` to mint one.`);
5168
+ process.exit(1);
5169
+ }
5170
+ recipients.add(key);
5171
+ } else {
5172
+ recipients.delete(key);
5173
+ logger.warn("Removing a recipient does not revoke values they already decrypted \u2014 rotate those values.");
5174
+ }
5175
+ rule.age = [...recipients].join(",");
5176
+ writeFileSync18(configPath, stringify(config));
5177
+ logger.success(`${action === "add" ? "Added" : "Removed"} recipient; updated ${SOPS_CONFIG}.`);
5178
+ await reencrypt(cwd);
5179
+ }
5180
+ function listRecipients(configPath) {
5181
+ if (!existsSync29(configPath)) {
5182
+ logger.info(`No ${SOPS_CONFIG} found.`);
5183
+ return;
5184
+ }
5185
+ const config = loadConfig(configPath);
5186
+ const rules = config.creation_rules ?? [];
5187
+ console.log(chalk34.blue.bold(`
5188
+ \u{1F465} Recipients (${SOPS_CONFIG})
5189
+ `));
5190
+ for (const rule of rules) {
5191
+ console.log(chalk34.dim(` ${rule.path_regex ?? "(any path)"}`));
5192
+ for (const recipient of parseRecipients(rule.age)) {
5193
+ console.log(` ${chalk34.cyan(recipient)}`);
5194
+ }
5195
+ }
5196
+ console.log();
5197
+ }
5198
+ function loadConfig(configPath) {
5199
+ if (!existsSync29(configPath)) {
5200
+ return { creation_rules: [] };
5201
+ }
5202
+ return parse3(readFileSync13(configPath, "utf-8")) ?? { creation_rules: [] };
5203
+ }
5204
+ function ensureRule(config) {
5205
+ if (!config.creation_rules) {
5206
+ config.creation_rules = [];
5207
+ }
5208
+ let rule = config.creation_rules.find((r) => r.path_regex === DEFAULT_PATH_REGEX);
5209
+ if (!rule) {
5210
+ rule = { path_regex: DEFAULT_PATH_REGEX, age: "" };
5211
+ config.creation_rules.push(rule);
5212
+ }
5213
+ return rule;
5214
+ }
5215
+ function parseRecipients(age) {
5216
+ return new Set(
5217
+ (age ?? "").split(",").map((value) => value.trim()).filter((value) => value.length > 0)
5218
+ );
5219
+ }
5220
+ async function reencrypt(cwd) {
5221
+ const files = listSopsFiles(cwd);
5222
+ if (files.length === 0) {
5223
+ return;
5224
+ }
5225
+ await ensureSopsInstalled();
5226
+ logger.step(`Re-encrypting ${files.length} secret file(s) for the new recipient set\u2026`);
5227
+ for (const file of files) {
5228
+ await sopsUpdateKeys(file);
5229
+ }
5230
+ }
5231
+
5232
+ // src/commands/secret/check.ts
5233
+ init_logger();
5234
+ import { join as join27 } from "path";
5235
+ import chalk35 from "chalk";
5236
+ init_env_file();
5237
+ var COMMITTED_FILES = [".env", ".env.example"];
5238
+ var PLACEHOLDER = /(your-|changeme|placeholder|example|<.*>)/i;
5239
+ async function secretCheck(options) {
5240
+ const cwd = process.cwd();
5241
+ const pkg = options.package ?? "@spfn/core";
5242
+ let entries;
5243
+ try {
5244
+ entries = secretEntries(await loadEnvSchema(pkg));
5245
+ } catch (error) {
5246
+ logger.error(error instanceof Error ? error.message : String(error));
5247
+ process.exit(1);
5248
+ }
5249
+ const secretKeys = new Set(entries.map((entry) => entry.key));
5250
+ const issues = [];
5251
+ const warnings = [];
5252
+ for (const file of COMMITTED_FILES) {
5253
+ const parsed = parseEnvFile(join27(cwd, file));
5254
+ for (const [key, value] of Object.entries(parsed)) {
5255
+ if (secretKeys.has(key) && value.length > 0 && !PLACEHOLDER.test(value)) {
5256
+ issues.push(`${chalk35.cyan(key)} has a real value in committed ${chalk35.yellow(file)} \u2014 move it to the keychain/SOPS.`);
5257
+ }
5258
+ }
5259
+ }
5260
+ const serverEnv = parseEnvFile(join27(cwd, ".env.server"));
5261
+ for (const key of secretKeys) {
5262
+ const value = serverEnv[key];
5263
+ if (value && !value.startsWith(KEYCHAIN_REF_PREFIX)) {
5264
+ warnings.push(`${chalk35.cyan(key)} is plaintext in .env.server \u2014 run \`spfn secret set ${key}\` to move it to the keychain.`);
5265
+ }
5266
+ }
5267
+ report(issues, warnings, cwd);
5268
+ }
5269
+ function report(issues, warnings, cwd) {
5270
+ console.log(chalk35.blue.bold("\n\u{1F50D} Secret hygiene check\n"));
5271
+ for (const issue of issues) {
5272
+ console.log(` ${chalk35.red("\u2717")} ${issue}`);
5273
+ }
5274
+ for (const warning of warnings) {
5275
+ console.log(` ${chalk35.yellow("\u26A0")} ${warning}`);
5276
+ }
5277
+ if (!hasSopsConfig(cwd)) {
5278
+ console.log(` ${chalk35.dim("\u2139 no .sops.yaml \u2014 prod secrets need a backend (spfn secret keygen, or add a KMS rule)")}`);
5279
+ }
5280
+ if (issues.length === 0 && warnings.length === 0) {
5281
+ console.log(chalk35.green(" \u2713 No plaintext secret leaks found."));
5282
+ }
5283
+ console.log();
5284
+ if (issues.length > 0) {
5285
+ process.exit(1);
5286
+ }
5287
+ }
5288
+
5289
+ // src/commands/secret/index.ts
5290
+ var ENV_OPTION = ["-e, --env <env>", "Target environment (local | development | staging | production)", "local"];
5291
+ var PKG_OPTION = ["-p, --package <package>", "Package whose env schema to read", "@spfn/core"];
5292
+ var secretCommand = new Command12("secret").description("Manage secrets: keychain locally, SOPS (age / GCP KMS / AWS KMS) for deployed environments");
5293
+ secretCommand.command("set [key]").description("Store a secret value (prompts for the value, masked)").option(...ENV_OPTION).option(...PKG_OPTION).action(secretSet);
5294
+ secretCommand.command("list").description("List declared secrets and their status (never prints values)").option(...ENV_OPTION).option(...PKG_OPTION).action(secretList);
5295
+ secretCommand.command("generate [key]").description("Generate value(s) for schema secrets that declare a generate strategy").option("-a, --all", "Generate every generatable secret").option(...ENV_OPTION).option(...PKG_OPTION).action(secretGenerate);
5296
+ secretCommand.command("rotate [key]").description("Rotate secret value(s); external secrets are flagged for manual reissue").option("-a, --all", "Rotate every secret").option(...ENV_OPTION).option(...PKG_OPTION).action(secretRotate);
5297
+ secretCommand.command("keygen").description("Generate an age key pair for SOPS (no-cloud backend)").action(secretKeygen);
5298
+ secretCommand.command("recipients <action> [key]").description("Manage .sops.yaml age recipients: add | remove | list").action(secretRecipients);
5299
+ secretCommand.command("check").description("Static hygiene lint \u2014 flag plaintext secret leaks").option(...PKG_OPTION).action(secretCheck);
5300
+
4231
5301
  // src/index.ts
4232
5302
  init_version();
4233
- var program = new Command12();
5303
+ var program = new Command13();
4234
5304
  program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(getCliVersion());
4235
5305
  program.addCommand(createCommand);
4236
5306
  program.addCommand(initCommand);
@@ -4243,6 +5313,7 @@ program.addCommand(keyCommand);
4243
5313
  program.addCommand(setupCommand);
4244
5314
  program.addCommand(dbCommand);
4245
5315
  program.addCommand(envCommand);
5316
+ program.addCommand(secretCommand);
4246
5317
  async function run() {
4247
5318
  await program.parseAsync(process.argv);
4248
5319
  }