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/README.md +49 -7
- package/bin/spfn.js +10 -3
- package/dist/index.js +1763 -692
- package/dist/templates/README.md +81 -0
- package/package.json +19 -12
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/
|
|
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 =
|
|
589
|
-
if (!
|
|
668
|
+
const dockerComposePath = join8(cwd, "docker-compose.yml");
|
|
669
|
+
if (!existsSync8(dockerComposePath)) {
|
|
590
670
|
try {
|
|
591
|
-
const dockerComposeTemplate =
|
|
592
|
-
if (
|
|
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 =
|
|
602
|
-
if (!
|
|
603
|
-
const dockerfileTemplate =
|
|
604
|
-
if (
|
|
605
|
-
const content = adaptDockerfileForPm(
|
|
606
|
-
|
|
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 =
|
|
611
|
-
if (!
|
|
612
|
-
const dockerignoreTemplate =
|
|
613
|
-
if (
|
|
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 =
|
|
619
|
-
if (!
|
|
620
|
-
const dockerComposeProdTemplate =
|
|
621
|
-
if (
|
|
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,
|
|
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:
|
|
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
|
|
668
|
-
import { join as
|
|
669
|
-
import
|
|
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 =
|
|
672
|
-
if (!
|
|
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
|
-
|
|
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
|
|
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:
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
870
|
-
import { join as
|
|
871
|
-
import
|
|
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
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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
|
-
|
|
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
|
|
903
|
-
|
|
904
|
-
|
|
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
|
|
954
|
-
const filePath =
|
|
955
|
-
if (
|
|
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
|
-
|
|
959
|
-
logger.
|
|
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
|
|
962
|
-
const
|
|
963
|
-
if (
|
|
1095
|
+
function writeExampleEnv(cwd, content) {
|
|
1096
|
+
const filePath = join11(cwd, ".env.example");
|
|
1097
|
+
if (existsSync11(filePath)) {
|
|
964
1098
|
return;
|
|
965
1099
|
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
.
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
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
|
-
|
|
997
|
-
logger.
|
|
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 =
|
|
1002
|
-
if (!
|
|
1165
|
+
const tsconfigPath = join11(cwd, "tsconfig.json");
|
|
1166
|
+
if (!existsSync11(tsconfigPath)) {
|
|
1003
1167
|
return;
|
|
1004
1168
|
}
|
|
1005
1169
|
try {
|
|
1006
|
-
const tsconfigContent =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
(
|
|
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
|
|
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
|
-
|
|
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(" " +
|
|
1052
|
-
console.log(" 2.
|
|
1053
|
-
console.log(" " +
|
|
1054
|
-
console.log(" 3. Run: " +
|
|
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: " +
|
|
1057
|
-
console.log(" - API: " +
|
|
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 " +
|
|
1060
|
-
console.log(" \u2022 " +
|
|
1061
|
-
console.log(" \u2022 " +
|
|
1062
|
-
console.log(" \u2022 " +
|
|
1063
|
-
console.log("\n" +
|
|
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
|
|
1089
|
-
import { join as
|
|
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
|
|
1346
|
+
import { existsSync as existsSync19, readdirSync as readdirSync2, readFileSync as readFileSync9 } from "fs";
|
|
1093
1347
|
function discoverFunctionMigrations(cwd = process.cwd()) {
|
|
1094
|
-
const nodeModulesPath =
|
|
1095
|
-
if (!
|
|
1348
|
+
const nodeModulesPath = join19(cwd, "node_modules");
|
|
1349
|
+
if (!existsSync19(nodeModulesPath)) {
|
|
1096
1350
|
return [];
|
|
1097
1351
|
}
|
|
1098
1352
|
const functions = [];
|
|
1099
|
-
const spfnDir =
|
|
1100
|
-
if (!
|
|
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 =
|
|
1106
|
-
const packageJsonPath =
|
|
1107
|
-
if (!
|
|
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(
|
|
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 =
|
|
1117
|
-
if (!
|
|
1370
|
+
const migrationsDir = join19(packagePath, spfnConfig.migrations.dir);
|
|
1371
|
+
if (!existsSync19(migrationsDir)) {
|
|
1118
1372
|
console.warn(
|
|
1119
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
1154
|
-
if (!
|
|
1407
|
+
const journalPath = join19(func.migrationsDir, "meta", "_journal.json");
|
|
1408
|
+
if (!existsSync19(journalPath)) {
|
|
1155
1409
|
continue;
|
|
1156
1410
|
}
|
|
1157
|
-
const journal = JSON.parse(
|
|
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 =
|
|
1180
|
-
if (!
|
|
1433
|
+
const sqlPath = join19(func.migrationsDir, `${entry.tag}.sql`);
|
|
1434
|
+
if (!existsSync19(sqlPath)) {
|
|
1181
1435
|
continue;
|
|
1182
1436
|
}
|
|
1183
|
-
const sqlContent =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
1243
|
-
import { join as
|
|
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
|
|
1502
|
+
import chalk5 from "chalk";
|
|
1248
1503
|
function findPnpmWorkspaceRoot(startDir) {
|
|
1249
1504
|
let dir = resolve(startDir);
|
|
1250
1505
|
while (true) {
|
|
1251
|
-
if (
|
|
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 =
|
|
1263
|
-
if (
|
|
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(
|
|
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" +
|
|
1394
|
-
console.log(
|
|
1395
|
-
console.log(` ${
|
|
1396
|
-
console.log(` ${
|
|
1397
|
-
console.log(` ${
|
|
1398
|
-
console.log(` ${
|
|
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(
|
|
1401
|
-
console.log(` ${
|
|
1402
|
-
console.log(` ${
|
|
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(
|
|
1405
|
-
console.log(" " +
|
|
1406
|
-
console.log(` ${
|
|
1407
|
-
console.log(` ${
|
|
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(" " +
|
|
1410
|
-
console.log(` ${
|
|
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(
|
|
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
|
|
1426
|
-
import { join as
|
|
1427
|
-
import { execa as
|
|
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
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
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
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
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
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1493
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
1776
|
-
import { join as
|
|
1777
|
-
import { execa as
|
|
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
|
|
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 =
|
|
2248
|
+
const packageJsonPath = join16(cwd, "package.json");
|
|
1791
2249
|
let hasNext = false;
|
|
1792
|
-
if (
|
|
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 =
|
|
1799
|
-
const hasServer =
|
|
1800
|
-
console.log(
|
|
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
|
|
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 =
|
|
2294
|
+
const outputDir = join16(cwd, ".spfn", "server");
|
|
1837
2295
|
mkdirSync2(outputDir, { recursive: true });
|
|
1838
|
-
const serverDir2 =
|
|
1839
|
-
if (!
|
|
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 =
|
|
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
|
-
|
|
2349
|
+
writeFileSync11(prodServerPath, prodServerContent);
|
|
1892
2350
|
spinner.succeed(`SPFN server build completed \u2192 .spfn/server`);
|
|
1893
|
-
const routesDir =
|
|
1894
|
-
if (
|
|
2351
|
+
const routesDir = join16(cwd, ".spfn", "server", "routes");
|
|
2352
|
+
if (existsSync15(routesDir)) {
|
|
1895
2353
|
console.log();
|
|
1896
|
-
console.log(
|
|
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} ${
|
|
2363
|
+
console.log(`${prefix} ${chalk6.green("GET")} ${routePath}`);
|
|
1906
2364
|
} else {
|
|
1907
|
-
console.log(`${prefix} ${
|
|
2365
|
+
console.log(`${prefix} ${chalk6.cyan("*")} ${routePath}`);
|
|
1908
2366
|
}
|
|
1909
2367
|
});
|
|
1910
2368
|
console.log();
|
|
1911
|
-
console.log(
|
|
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" +
|
|
2377
|
+
console.error("\n" + chalk6.red(error.message));
|
|
1920
2378
|
if (error.stack) {
|
|
1921
|
-
console.error(
|
|
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" +
|
|
1934
|
-
console.log(
|
|
1935
|
-
console.log(" " +
|
|
1936
|
-
console.log(` ${
|
|
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(" " +
|
|
1939
|
-
console.log(` ${
|
|
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
|
|
1951
|
-
import { join as
|
|
1952
|
-
import { execa as
|
|
1953
|
-
import
|
|
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 =
|
|
2417
|
+
const packageJsonPath = join17(cwd, "package.json");
|
|
1960
2418
|
let hasNext = false;
|
|
1961
|
-
if (
|
|
1962
|
-
const packageJson = JSON.parse(
|
|
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 =
|
|
1966
|
-
const hasBuiltServer =
|
|
1967
|
-
const nextBuildDir =
|
|
1968
|
-
const hasNextBuild =
|
|
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 =
|
|
1979
|
-
if (!
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
2043
|
-
import { join as
|
|
2044
|
-
import
|
|
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 =
|
|
2048
|
-
if (
|
|
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
|
-
|
|
2074
|
-
console.log("\n" +
|
|
2075
|
-
console.log(
|
|
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" +
|
|
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(
|
|
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: " +
|
|
2548
|
+
console.log(" 3. Run: " + chalk8.cyan("spfn dev") + " (generators run automatically)");
|
|
2091
2549
|
} else {
|
|
2092
|
-
console.log("\n" +
|
|
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: " +
|
|
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" +
|
|
2565
|
+
console.log("\n" + chalk8.bold("Registered Generators:"));
|
|
2108
2566
|
generators.forEach((gen, index) => {
|
|
2109
|
-
console.log(` ${index + 1}. ${
|
|
2110
|
-
console.log(` Patterns: ${
|
|
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" +
|
|
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
|
|
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 =
|
|
2673
|
+
const key = randomBase64Url(bytes);
|
|
2195
2674
|
const config = preset ? PRESETS[preset] : null;
|
|
2196
|
-
console.log("\n" +
|
|
2675
|
+
console.log("\n" + chalk9.green.bold("\u2713 Generated secret key:"));
|
|
2197
2676
|
if (config) {
|
|
2198
|
-
console.log(
|
|
2677
|
+
console.log(chalk9.dim(` ${config.description} (${bytes * 8} bits)`));
|
|
2199
2678
|
} else {
|
|
2200
|
-
console.log(
|
|
2679
|
+
console.log(chalk9.dim(` ${bytes * 8}-bit secret`));
|
|
2201
2680
|
}
|
|
2202
|
-
console.log("\n" +
|
|
2681
|
+
console.log("\n" + chalk9.cyan(key) + "\n");
|
|
2203
2682
|
const varName = envVarName || config?.envVar || "SECRET_KEY";
|
|
2204
|
-
console.log(
|
|
2205
|
-
console.log(
|
|
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(
|
|
2209
|
-
console.log(
|
|
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(
|
|
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" +
|
|
2700
|
+
console.log("\n" + chalk9.bold("Available presets:"));
|
|
2222
2701
|
console.log();
|
|
2223
2702
|
Object.entries(PRESETS).forEach(([name, config]) => {
|
|
2224
|
-
console.log(` ${
|
|
2225
|
-
console.log(` ${" ".repeat(20)} ${
|
|
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(
|
|
2229
|
-
console.log(
|
|
2230
|
-
console.log(
|
|
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 =
|
|
2718
|
+
const value = randomBase64Url(bytes);
|
|
2240
2719
|
console.log(value);
|
|
2241
2720
|
if (options.copy) {
|
|
2242
2721
|
if (copyToClipboard(value)) {
|
|
2243
|
-
console.error(
|
|
2722
|
+
console.error(chalk9.green("\u2713 Copied to clipboard"));
|
|
2244
2723
|
} else {
|
|
2245
|
-
console.error(
|
|
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(` ${
|
|
2742
|
+
console.log(` ${chalk9.cyan(name)}: ${config.description}`);
|
|
2264
2743
|
});
|
|
2265
|
-
console.log("\nUse " +
|
|
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
|
|
2763
|
+
import chalk11 from "chalk";
|
|
2285
2764
|
|
|
2286
2765
|
// src/commands/db/utils/drizzle.ts
|
|
2287
|
-
import { existsSync as
|
|
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
|
|
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(
|
|
2312
|
-
console.log(
|
|
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 =
|
|
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(
|
|
2324
|
-
console.log(
|
|
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
|
-
|
|
2337
|
-
console.log(
|
|
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 &&
|
|
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(
|
|
2851
|
+
console.log(chalk10.green(`\u2705 ${successMessage}`));
|
|
2373
2852
|
} catch (error) {
|
|
2374
2853
|
spinner.fail(failMessage);
|
|
2375
|
-
console.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(
|
|
2900
|
+
console.log(chalk11.green("\n\u2705 Migrations generated successfully"));
|
|
2422
2901
|
} catch (error) {
|
|
2423
|
-
console.error(
|
|
2424
|
-
console.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
|
|
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
|
|
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(
|
|
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(
|
|
2970
|
+
console.log(chalk12.green(`
|
|
2492
2971
|
\u2705 Safe changes (${result.safe.length}):`));
|
|
2493
|
-
printStatements(result.safe,
|
|
2972
|
+
printStatements(result.safe, chalk12.green);
|
|
2494
2973
|
}
|
|
2495
2974
|
if (result.warning.length > 0) {
|
|
2496
|
-
console.log(
|
|
2975
|
+
console.log(chalk12.yellow(`
|
|
2497
2976
|
\u26A0\uFE0F Warnings (${result.warning.length}):`));
|
|
2498
|
-
printStatements(result.warning,
|
|
2977
|
+
printStatements(result.warning, chalk12.yellow);
|
|
2499
2978
|
}
|
|
2500
2979
|
if (result.destructive.length > 0) {
|
|
2501
|
-
console.log(
|
|
2980
|
+
console.log(chalk12.red(`
|
|
2502
2981
|
\u274C Destructive changes (${result.destructive.length}):`));
|
|
2503
|
-
printStatements(result.destructive,
|
|
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(
|
|
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(
|
|
2995
|
+
console.log(chalk12.green(`
|
|
2517
2996
|
\u2705 Applied ${applied} statement(s)`));
|
|
2518
2997
|
}
|
|
2519
2998
|
if (skipped > 0) {
|
|
2520
|
-
console.log(
|
|
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(
|
|
3017
|
+
console.log(chalk14.yellow("No schema files found."));
|
|
2539
3018
|
return;
|
|
2540
3019
|
}
|
|
2541
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
3085
|
+
console.log(chalk14.green(`
|
|
2607
3086
|
\u2705 Applied ${nonDestructive.length} safe statement(s)`));
|
|
2608
3087
|
}
|
|
2609
|
-
console.log(
|
|
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(
|
|
2613
|
-
console.log(
|
|
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(
|
|
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(
|
|
3121
|
+
console.log(chalk14.blue("\n\u{1F4E6} Applying function package migrations:"));
|
|
2643
3122
|
functions.forEach((func) => {
|
|
2644
|
-
console.log(
|
|
3123
|
+
console.log(chalk14.dim(` - ${func.packageName}`));
|
|
2645
3124
|
});
|
|
2646
3125
|
try {
|
|
2647
3126
|
await executeFunctionMigrations2(functions);
|
|
2648
|
-
console.log(
|
|
3127
|
+
console.log(chalk14.green("\n\u2705 All function migrations applied\n"));
|
|
2649
3128
|
} catch (error) {
|
|
2650
|
-
console.error(
|
|
2651
|
-
console.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
|
|
2658
|
-
import { join as
|
|
2659
|
-
import { existsSync as
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
3231
|
+
import { existsSync as existsSync20 } from "fs";
|
|
2753
3232
|
import path2 from "path";
|
|
2754
|
-
import
|
|
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
|
|
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(
|
|
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(
|
|
3311
|
+
console.log(chalk16.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
|
|
2833
3312
|
} catch (error) {
|
|
2834
|
-
console.log(
|
|
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 =
|
|
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(
|
|
3342
|
+
console.log(chalk17.dim("\u2713 Added backups/ to .gitignore"));
|
|
2864
3343
|
}
|
|
2865
3344
|
} catch (error) {
|
|
2866
|
-
console.log(
|
|
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 =
|
|
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(
|
|
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(
|
|
2921
|
-
console.log(
|
|
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(
|
|
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(
|
|
3456
|
+
console.log(chalk18.green(`
|
|
2978
3457
|
\u2705 Backup created successfully`));
|
|
2979
|
-
console.log(
|
|
2980
|
-
console.log(
|
|
2981
|
-
console.log(
|
|
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(
|
|
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(
|
|
3502
|
+
console.error(chalk18.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
|
|
3024
3503
|
} else {
|
|
3025
|
-
console.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(
|
|
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(
|
|
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(
|
|
3540
|
+
console.log(chalk19.blue("\u{1F4E6} Applying function package migrations:"));
|
|
3062
3541
|
functions.forEach((func) => {
|
|
3063
|
-
console.log(
|
|
3542
|
+
console.log(chalk19.dim(` - ${func.packageName}`));
|
|
3064
3543
|
});
|
|
3065
3544
|
await executeFunctionMigrations2(functions);
|
|
3066
|
-
console.log(
|
|
3545
|
+
console.log(chalk19.green("\u2705 Function migrations applied\n"));
|
|
3067
3546
|
}
|
|
3068
|
-
const projectMigrationsDir =
|
|
3069
|
-
if (
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
3089
|
-
import { existsSync as
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
3114
|
-
console.log(
|
|
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
|
-
|
|
3125
|
-
console.log(
|
|
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 &&
|
|
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(
|
|
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(
|
|
3148
|
-
console.error(
|
|
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(
|
|
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 &&
|
|
3642
|
+
if (!hasUserConfig && existsSync22(tempConfigPath)) {
|
|
3164
3643
|
unlinkSync3(tempConfigPath);
|
|
3165
3644
|
}
|
|
3166
|
-
console.error(
|
|
3167
|
-
console.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
|
|
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(
|
|
3181
|
-
console.log(
|
|
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(
|
|
3186
|
-
console.log(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
3236
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
3268
|
-
console.log(
|
|
3269
|
-
console.log(
|
|
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(
|
|
3750
|
+
console.log(chalk23.dim(` Environment: ${metadata.environment}`));
|
|
3272
3751
|
}
|
|
3273
3752
|
if (metadata.tags && metadata.tags.length > 0) {
|
|
3274
|
-
console.log(
|
|
3753
|
+
console.log(chalk23.dim(` Tags: ${metadata.tags.join(", ")}`));
|
|
3275
3754
|
}
|
|
3276
3755
|
if (metadata.backup.dataOnly) {
|
|
3277
|
-
console.log(
|
|
3756
|
+
console.log(chalk23.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
|
|
3278
3757
|
}
|
|
3279
3758
|
if (metadata.backup.schemaOnly) {
|
|
3280
|
-
console.log(
|
|
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(
|
|
3304
|
-
warnings2.forEach((warning) => console.log(
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
3353
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
3897
|
+
console.log(chalk23.yellow(` - ${w}`));
|
|
3419
3898
|
}
|
|
3420
3899
|
}
|
|
3421
|
-
console.log(
|
|
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(
|
|
3905
|
+
console.error(chalk23.red(`
|
|
3427
3906
|
\u274C Errors (${errors.length}):
|
|
3428
3907
|
`));
|
|
3429
3908
|
for (const e of errors) {
|
|
3430
|
-
console.error(
|
|
3909
|
+
console.error(chalk23.red(` - ${e}`));
|
|
3431
3910
|
}
|
|
3432
3911
|
}
|
|
3433
3912
|
if (warnings.length > 0) {
|
|
3434
|
-
console.log(
|
|
3913
|
+
console.log(chalk23.yellow(`
|
|
3435
3914
|
\u26A0\uFE0F Warnings (${warnings.length}):
|
|
3436
3915
|
`));
|
|
3437
3916
|
for (const w of warnings) {
|
|
3438
|
-
console.log(
|
|
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(
|
|
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
|
|
3939
|
+
import chalk24 from "chalk";
|
|
3461
3940
|
async function dbBackupList() {
|
|
3462
|
-
console.log(
|
|
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(
|
|
3466
|
-
console.log(
|
|
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(
|
|
3470
|
-
console.log(
|
|
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(
|
|
3960
|
+
console.log(chalk24.white(` ${date} ${sizeStr} ${backup.name}`));
|
|
3482
3961
|
});
|
|
3483
|
-
console.log(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
3993
|
+
console.log(chalk25.green("\u2705 No backups to clean"));
|
|
3515
3994
|
return;
|
|
3516
3995
|
}
|
|
3517
|
-
console.log(
|
|
3996
|
+
console.log(chalk25.yellow(`The following ${toDelete.length} backup(s) will be deleted:
|
|
3518
3997
|
`));
|
|
3519
3998
|
toDelete.forEach((backup) => {
|
|
3520
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
3551
|
-
import { join as
|
|
3552
|
-
import
|
|
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 =
|
|
3574
|
-
if (!
|
|
3575
|
-
console.error(
|
|
3576
|
-
console.log(
|
|
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(
|
|
4058
|
+
const journal = JSON.parse(readFileSync10(journalPath, "utf-8"));
|
|
3580
4059
|
if (journal.entries.length === 0) {
|
|
3581
|
-
console.log(
|
|
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 =
|
|
3596
|
-
const newSql =
|
|
3597
|
-
if (
|
|
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 =
|
|
3601
|
-
const newSnapshot =
|
|
3602
|
-
if (
|
|
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(
|
|
4087
|
+
console.log(chalk26.green("\u2705 All migrations already use timestamp prefix \u2014 nothing to do."));
|
|
3609
4088
|
if (skipped > 0) {
|
|
3610
|
-
console.log(
|
|
4089
|
+
console.log(chalk26.dim(` (${skipped} entries already timestamp-prefixed)`));
|
|
3611
4090
|
}
|
|
3612
4091
|
return;
|
|
3613
4092
|
}
|
|
3614
|
-
console.log(
|
|
4093
|
+
console.log(chalk26.bold("\n\u{1F4CB} Reindex plan:\n"));
|
|
3615
4094
|
for (const update of tagUpdates) {
|
|
3616
4095
|
console.log(
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
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(
|
|
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(
|
|
4105
|
+
console.log(chalk26.dim(` ${skipped} entry/entries already timestamp-prefixed (skipped)`));
|
|
3627
4106
|
}
|
|
3628
4107
|
if (options.dryRun) {
|
|
3629
|
-
console.log(
|
|
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(
|
|
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
|
-
|
|
3646
|
-
console.log(
|
|
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
|
|
3668
|
-
import { join as
|
|
3669
|
-
import { execa as
|
|
3670
|
-
import
|
|
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(
|
|
3675
|
-
console.log(
|
|
3676
|
-
console.log(
|
|
3677
|
-
console.log(
|
|
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(
|
|
4159
|
+
console.log(chalk27.blue(`
|
|
3681
4160
|
\u{1F4E6} Setting up ${packageName}...
|
|
3682
4161
|
`));
|
|
3683
|
-
const pkgPath =
|
|
3684
|
-
const pkgJsonPath =
|
|
3685
|
-
if (!
|
|
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
|
|
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(
|
|
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(
|
|
4176
|
+
console.log(chalk27.gray("\u2713 Package already installed (using local version)\n"));
|
|
3698
4177
|
}
|
|
3699
|
-
if (!
|
|
3700
|
-
console.error(
|
|
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(
|
|
4182
|
+
const pkgJson = JSON.parse(readFileSync11(pkgJsonPath, "utf-8"));
|
|
3704
4183
|
if (pkgJson.spfn?.migrations) {
|
|
3705
|
-
console.log(
|
|
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(
|
|
3711
|
-
console.log(
|
|
3712
|
-
console.log(
|
|
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(
|
|
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(
|
|
4207
|
+
console.log(chalk27.gray("\u2139\uFE0F No migrations found for this package"));
|
|
3729
4208
|
}
|
|
3730
4209
|
}
|
|
3731
4210
|
} else {
|
|
3732
|
-
console.log(
|
|
4211
|
+
console.log(chalk27.gray("\n\u2139\uFE0F No database migrations to apply"));
|
|
3733
4212
|
}
|
|
3734
|
-
console.log(
|
|
4213
|
+
console.log(chalk27.green(`
|
|
3735
4214
|
\u2705 ${packageName} installed successfully!
|
|
3736
4215
|
`));
|
|
3737
4216
|
if (pkgJson.spfn?.setupMessage) {
|
|
3738
|
-
console.log(
|
|
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
|
|
3748
|
-
import { existsSync as
|
|
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:
|
|
3796
|
-
number:
|
|
3797
|
-
boolean:
|
|
3798
|
-
url:
|
|
3799
|
-
enum:
|
|
3800
|
-
json:
|
|
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] ||
|
|
4290
|
+
return (typeColors[type] || chalk28.white)(type);
|
|
3803
4291
|
}
|
|
3804
4292
|
function formatDefault(value, type) {
|
|
3805
4293
|
if (value === void 0) {
|
|
3806
|
-
return
|
|
4294
|
+
return chalk28.dim("(none)");
|
|
3807
4295
|
}
|
|
3808
4296
|
if (type === "string" || type === "url") {
|
|
3809
|
-
return
|
|
4297
|
+
return chalk28.green(`"${value}"`);
|
|
3810
4298
|
}
|
|
3811
4299
|
if (type === "boolean") {
|
|
3812
|
-
return value ?
|
|
4300
|
+
return value ? chalk28.green("true") : chalk28.red("false");
|
|
3813
4301
|
}
|
|
3814
|
-
return
|
|
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(
|
|
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(
|
|
4320
|
+
console.log(chalk28.bold.magenta(`
|
|
3833
4321
|
${file}`));
|
|
3834
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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 ?
|
|
3858
|
-
const sensitiveStr = schema.sensitive ?
|
|
3859
|
-
const fileStr = showFile ?
|
|
3860
|
-
console.log(`${
|
|
3861
|
-
console.log(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(
|
|
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(`${
|
|
3898
|
-
console.log(`${
|
|
3899
|
-
console.log(`${
|
|
3900
|
-
console.log(`${
|
|
3901
|
-
console.log(
|
|
3902
|
-
console.log(` ${
|
|
3903
|
-
console.log(` ${
|
|
3904
|
-
console.log(
|
|
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(` ${
|
|
4394
|
+
console.log(` ${chalk28.dim(file)}: ${chalk28.cyan(count)}`);
|
|
3907
4395
|
}
|
|
3908
|
-
console.log(
|
|
4396
|
+
console.log(chalk28.bold("\nBy Type:"));
|
|
3909
4397
|
for (const [type, count] of Object.entries(typeCount)) {
|
|
3910
|
-
console.log(` ${formatType(type)}: ${
|
|
4398
|
+
console.log(` ${formatType(type)}: ${chalk28.cyan(count)}`);
|
|
3911
4399
|
}
|
|
3912
4400
|
console.log();
|
|
3913
4401
|
} catch (error) {
|
|
3914
|
-
console.error(
|
|
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(
|
|
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(
|
|
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 ?
|
|
3945
|
-
console.log(`${
|
|
3946
|
-
console.log(` ${
|
|
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(` ${
|
|
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(
|
|
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(
|
|
4453
|
+
console.error(chalk28.red(`
|
|
3966
4454
|
\u274C Invalid environment: "${envValue}"`));
|
|
3967
|
-
console.log(
|
|
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(
|
|
3989
|
-
\u{1F680} Generating .env template files for ${
|
|
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(
|
|
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(
|
|
4013
|
-
console.log(
|
|
4014
|
-
console.log(
|
|
4015
|
-
console.log(
|
|
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(
|
|
4018
|
-
console.log(
|
|
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(
|
|
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 (
|
|
4031
|
-
console.log(
|
|
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
|
-
|
|
4035
|
-
console.log(
|
|
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(
|
|
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 (!
|
|
4567
|
+
if (!existsSync25(filePath)) {
|
|
4080
4568
|
continue;
|
|
4081
4569
|
}
|
|
4082
|
-
const content =
|
|
4083
|
-
const parsed =
|
|
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(
|
|
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(`${
|
|
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
|
-
`${
|
|
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
|
-
`${
|
|
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(`${
|
|
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(
|
|
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(
|
|
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(
|
|
4623
|
+
console.log(chalk28.green("\u2705 All environment variables are correctly configured!\n"));
|
|
4136
4624
|
} else {
|
|
4137
|
-
console.log(
|
|
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(
|
|
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(
|
|
4159
|
-
\u{1F50D} Validating environment variables for ${
|
|
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(
|
|
4650
|
+
console.log(chalk28.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
|
|
4163
4651
|
}
|
|
4164
4652
|
console.log("");
|
|
4165
4653
|
} else {
|
|
4166
|
-
console.log(
|
|
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(
|
|
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(
|
|
4675
|
+
console.log(chalk28.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
|
|
4188
4676
|
continue;
|
|
4189
4677
|
}
|
|
4190
|
-
console.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(
|
|
4686
|
+
console.log(chalk28.red.bold(`\u274C Validation Errors (${allErrors.length}):
|
|
4199
4687
|
`));
|
|
4200
4688
|
for (const error of allErrors) {
|
|
4201
|
-
console.log(` ${
|
|
4202
|
-
console.log(` ${
|
|
4203
|
-
console.log(` ${
|
|
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(
|
|
4696
|
+
console.log(chalk28.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
|
|
4209
4697
|
`));
|
|
4210
4698
|
for (const warning of allWarnings) {
|
|
4211
|
-
console.log(` ${
|
|
4212
|
-
console.log(` ${
|
|
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(
|
|
4705
|
+
console.log(chalk28.green.bold("\u2705 All environment variables are valid!\n"));
|
|
4218
4706
|
} else if (allErrors.length === 0) {
|
|
4219
|
-
console.log(
|
|
4220
|
-
console.log(
|
|
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(
|
|
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
|
|
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
|
}
|