spfn 0.2.0-beta.60 → 0.2.0-beta.63
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 +57 -17
- package/dist/index.js +970 -631
- package/dist/templates/README.md +24 -11
- package/dist/templates/modes/full/src/app/auth/callback/page.tsx +1 -0
- package/dist/templates/modes/full/src/app/login/page.tsx +68 -0
- package/dist/templates/modes/full/src/i18n/catalogs.ts +16 -0
- package/dist/templates/modes/full/src/i18n/server.ts +9 -0
- package/dist/templates/modes/full/src/server/config/env.config.ts +23 -0
- package/dist/templates/modes/full/src/server/mcp.ts +75 -0
- package/dist/templates/modes/full/src/server/router.ts +32 -0
- package/dist/templates/modes/full/src/server/routes/examples.ts +94 -0
- package/dist/templates/modes/full/src/server/routes/health.ts +9 -0
- package/dist/templates/modes/full/src/server/routes/root.ts +15 -0
- package/dist/templates/modes/full/src/server/server.config.ts +14 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -87,6 +87,44 @@ var init_messages = __esm({
|
|
|
87
87
|
}
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
// src/commands/init/mode.ts
|
|
91
|
+
import prompts from "prompts";
|
|
92
|
+
async function selectScaffoldMode(options) {
|
|
93
|
+
if (options.mode) {
|
|
94
|
+
return options.mode;
|
|
95
|
+
}
|
|
96
|
+
if (options.yes) {
|
|
97
|
+
return "bare";
|
|
98
|
+
}
|
|
99
|
+
const { selectedMode } = await prompts({
|
|
100
|
+
type: "select",
|
|
101
|
+
name: "selectedMode",
|
|
102
|
+
message: "Which SPFN scaffold do you want?",
|
|
103
|
+
choices: [
|
|
104
|
+
{
|
|
105
|
+
title: "full (recommended)",
|
|
106
|
+
description: "Core, auth, i18n, and MCP \u2014 ready for Prototype to Production",
|
|
107
|
+
value: "full"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
title: "bare",
|
|
111
|
+
description: "Core-only full-stack skeleton",
|
|
112
|
+
value: "bare"
|
|
113
|
+
}
|
|
114
|
+
],
|
|
115
|
+
initial: 0
|
|
116
|
+
});
|
|
117
|
+
if (!selectedMode) {
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
return selectedMode;
|
|
121
|
+
}
|
|
122
|
+
var init_mode = __esm({
|
|
123
|
+
"src/commands/init/mode.ts"() {
|
|
124
|
+
"use strict";
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
90
128
|
// src/commands/setup.ts
|
|
91
129
|
var setup_exports = {};
|
|
92
130
|
__export(setup_exports, {
|
|
@@ -387,7 +425,7 @@ var init_setup = __esm({
|
|
|
387
425
|
// src/commands/init/steps/validate.ts
|
|
388
426
|
import { existsSync as existsSync3 } from "fs";
|
|
389
427
|
import { join as join3 } from "path";
|
|
390
|
-
import
|
|
428
|
+
import prompts2 from "prompts";
|
|
391
429
|
async function validateProject(cwd, skipPrompts) {
|
|
392
430
|
const packageJsonPath = join3(cwd, "package.json");
|
|
393
431
|
if (!existsSync3(packageJsonPath)) {
|
|
@@ -401,7 +439,7 @@ async function validateProject(cwd, skipPrompts) {
|
|
|
401
439
|
if (!hasNext) {
|
|
402
440
|
logger.warn("Next.js not detected in dependencies.");
|
|
403
441
|
if (!skipPrompts) {
|
|
404
|
-
const { proceed } = await
|
|
442
|
+
const { proceed } = await prompts2(
|
|
405
443
|
{
|
|
406
444
|
type: "confirm",
|
|
407
445
|
name: "proceed",
|
|
@@ -418,7 +456,7 @@ async function validateProject(cwd, skipPrompts) {
|
|
|
418
456
|
if (existsSync3(join3(cwd, "src", "server"))) {
|
|
419
457
|
logger.warn("src/server directory already exists.");
|
|
420
458
|
if (!skipPrompts) {
|
|
421
|
-
const { overwrite } = await
|
|
459
|
+
const { overwrite } = await prompts2(
|
|
422
460
|
{
|
|
423
461
|
type: "confirm",
|
|
424
462
|
name: "overwrite",
|
|
@@ -432,19 +470,7 @@ async function validateProject(cwd, skipPrompts) {
|
|
|
432
470
|
}
|
|
433
471
|
}
|
|
434
472
|
}
|
|
435
|
-
|
|
436
|
-
if (!skipPrompts) {
|
|
437
|
-
const { auth } = await prompts(
|
|
438
|
-
{
|
|
439
|
-
type: "confirm",
|
|
440
|
-
name: "auth",
|
|
441
|
-
message: "Include authentication (@spfn/auth)?",
|
|
442
|
-
initial: true
|
|
443
|
-
}
|
|
444
|
-
);
|
|
445
|
-
includeAuth = auth;
|
|
446
|
-
}
|
|
447
|
-
return { packageJson, packageJsonPath, includeAuth };
|
|
473
|
+
return { packageJson, packageJsonPath };
|
|
448
474
|
}
|
|
449
475
|
var init_validate = __esm({
|
|
450
476
|
"src/commands/init/steps/validate.ts"() {
|
|
@@ -470,6 +496,10 @@ function findTemplatesPath() {
|
|
|
470
496
|
if (existsSync4(devPath)) {
|
|
471
497
|
return devPath;
|
|
472
498
|
}
|
|
499
|
+
const sourcePath = join4(__dirname, "..", "..", "..", "..", "templates");
|
|
500
|
+
if (existsSync4(sourcePath)) {
|
|
501
|
+
return sourcePath;
|
|
502
|
+
}
|
|
473
503
|
throw new Error("Templates directory not found. Please rebuild the package.");
|
|
474
504
|
}
|
|
475
505
|
var __dirname;
|
|
@@ -482,10 +512,10 @@ var init_templates = __esm({
|
|
|
482
512
|
|
|
483
513
|
// src/commands/init/steps/server-structure.ts
|
|
484
514
|
import { existsSync as existsSync5 } from "fs";
|
|
485
|
-
import { join as join5 } from "path";
|
|
515
|
+
import { join as join5, sep } from "path";
|
|
486
516
|
import ora2 from "ora";
|
|
487
517
|
import fse2 from "fs-extra";
|
|
488
|
-
async function setupServerStructure(cwd) {
|
|
518
|
+
async function setupServerStructure(cwd, mode) {
|
|
489
519
|
const spinner = ora2("Setting up server structure...").start();
|
|
490
520
|
try {
|
|
491
521
|
const templatesDir = findTemplatesPath();
|
|
@@ -511,6 +541,27 @@ async function setupServerStructure(cwd) {
|
|
|
511
541
|
copySync(envConfigTemplate, envConfigTarget);
|
|
512
542
|
logger.success("Created src/server/config/env.config.ts (environment management)");
|
|
513
543
|
}
|
|
544
|
+
if (mode === "full") {
|
|
545
|
+
const fullTemplateDir = join5(templatesDir, "modes", "full");
|
|
546
|
+
if (!existsSync5(fullTemplateDir)) {
|
|
547
|
+
throw new Error(`Full scaffold templates not found at: ${fullTemplateDir}`);
|
|
548
|
+
}
|
|
549
|
+
const fullAppTemplateDir = join5(fullTemplateDir, "src", "app");
|
|
550
|
+
const fullI18nTemplateDir = join5(fullTemplateDir, "src", "i18n");
|
|
551
|
+
const mergeOnlyDirs = [fullAppTemplateDir, fullI18nTemplateDir];
|
|
552
|
+
copySync(fullTemplateDir, cwd, {
|
|
553
|
+
filter: (source) => !mergeOnlyDirs.some((directory) => source === directory || source.startsWith(`${directory}${sep}`))
|
|
554
|
+
});
|
|
555
|
+
const appTargetDir = existsSync5(join5(cwd, "src", "app")) ? join5(cwd, "src", "app") : join5(cwd, "app");
|
|
556
|
+
copySync(fullAppTemplateDir, appTargetDir, {
|
|
557
|
+
overwrite: false,
|
|
558
|
+
errorOnExist: false
|
|
559
|
+
});
|
|
560
|
+
copySync(fullI18nTemplateDir, join5(cwd, "src", "i18n"), {
|
|
561
|
+
overwrite: false,
|
|
562
|
+
errorOnExist: false
|
|
563
|
+
});
|
|
564
|
+
}
|
|
514
565
|
spinner.succeed("Server structure created");
|
|
515
566
|
} catch (error) {
|
|
516
567
|
spinner.fail("Failed to create server structure");
|
|
@@ -551,8 +602,10 @@ async function setupApiProxy(cwd, includeAuth) {
|
|
|
551
602
|
return;
|
|
552
603
|
}
|
|
553
604
|
ensureDirSync3(rpcDir);
|
|
554
|
-
const
|
|
605
|
+
const authImports = includeAuth ? `import '@spfn/auth/nextjs/api';
|
|
606
|
+
import { authRouteMap } from '@spfn/auth';
|
|
555
607
|
` : "";
|
|
608
|
+
const proxyRouteMap = includeAuth ? "{ ...routeMap, ...authRouteMap }" : "routeMap";
|
|
556
609
|
const routeContent = `/**
|
|
557
610
|
* SPFN RPC Proxy
|
|
558
611
|
*
|
|
@@ -566,10 +619,10 @@ async function setupApiProxy(cwd, includeAuth) {
|
|
|
566
619
|
* Run \`spfn codegen run\` if route-map.ts is missing.
|
|
567
620
|
*/
|
|
568
621
|
|
|
569
|
-
${
|
|
622
|
+
${authImports}import { routeMap } from '@/generated/route-map';
|
|
570
623
|
import { createRpcProxy } from '@spfn/core/nextjs/server';
|
|
571
624
|
|
|
572
|
-
export const { GET, POST } = createRpcProxy({ routeMap });
|
|
625
|
+
export const { GET, POST } = createRpcProxy({ routeMap: ${proxyRouteMap} });
|
|
573
626
|
`;
|
|
574
627
|
writeFileSync2(rpcRoutePath, routeContent);
|
|
575
628
|
const relativePath = rpcRoutePath.replace(cwd + "/", "");
|
|
@@ -869,7 +922,7 @@ var init_deployment_config = __esm({
|
|
|
869
922
|
|
|
870
923
|
// src/utils/version.ts
|
|
871
924
|
function getCliVersion() {
|
|
872
|
-
return "0.2.0-beta.
|
|
925
|
+
return "0.2.0-beta.63";
|
|
873
926
|
}
|
|
874
927
|
function getTagFromVersion(version) {
|
|
875
928
|
const match = version.match(/-([a-z]+)\./i);
|
|
@@ -888,7 +941,7 @@ var init_version = __esm({
|
|
|
888
941
|
import ora3 from "ora";
|
|
889
942
|
import { execa as execa2 } from "execa";
|
|
890
943
|
import fse7 from "fs-extra";
|
|
891
|
-
async function setupPackageJson(cwd, packageJsonPath, packageJson, packageManager,
|
|
944
|
+
async function setupPackageJson(cwd, packageJsonPath, packageJson, packageManager, mode) {
|
|
892
945
|
const spinner = ora3("Updating package.json...").start();
|
|
893
946
|
packageJson.dependencies = packageJson.dependencies || {};
|
|
894
947
|
packageJson.devDependencies = packageJson.devDependencies || {};
|
|
@@ -902,8 +955,19 @@ async function setupPackageJson(cwd, packageJsonPath, packageJson, packageManage
|
|
|
902
955
|
packageJson.dependencies["pg"] = "^8.16.3";
|
|
903
956
|
packageJson.dependencies["spfn"] = spfnTag;
|
|
904
957
|
packageJson.dependencies["concurrently"] = "^9.2.1";
|
|
905
|
-
if (
|
|
958
|
+
if (mode === "full") {
|
|
906
959
|
packageJson.dependencies["@spfn/auth"] = spfnTag;
|
|
960
|
+
packageJson.dependencies["@spfn/i18n"] = spfnTag;
|
|
961
|
+
packageJson.dependencies["@spfn/mcp"] = spfnTag;
|
|
962
|
+
packageJson.dependencies["@spfn/notification"] = spfnTag;
|
|
963
|
+
packageJson.engines = packageJson.engines || {};
|
|
964
|
+
const existingNodeRange = packageJson.engines.node;
|
|
965
|
+
if (!existingNodeRange || !requiresNode20OrNewer(existingNodeRange)) {
|
|
966
|
+
packageJson.engines.node = ">=20.0.0";
|
|
967
|
+
if (existingNodeRange) {
|
|
968
|
+
logger.warn(`Updated engines.node from "${existingNodeRange}" to ">=20.0.0" because full mode includes @spfn/mcp`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
907
971
|
}
|
|
908
972
|
packageJson.devDependencies["@types/node"] = "^20.11.0";
|
|
909
973
|
packageJson.devDependencies["tsx"] = "^4.20.6";
|
|
@@ -935,6 +999,16 @@ async function setupPackageJson(cwd, packageJsonPath, packageJson, packageManage
|
|
|
935
999
|
process.exit(1);
|
|
936
1000
|
}
|
|
937
1001
|
}
|
|
1002
|
+
function requiresNode20OrNewer(range) {
|
|
1003
|
+
return range.split("||").map((alternative) => alternative.trim()).every((alternative) => {
|
|
1004
|
+
const lowerBound = alternative.match(/^(?:>=|>|\^|~|=)?\s*v?(\d+)/);
|
|
1005
|
+
if (!lowerBound) {
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
const major = Number(lowerBound[1]);
|
|
1009
|
+
return major >= 20;
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
938
1012
|
var writeFileSync6;
|
|
939
1013
|
var init_package = __esm({
|
|
940
1014
|
"src/commands/init/steps/package.ts"() {
|
|
@@ -1041,14 +1115,70 @@ var init_env_file = __esm({
|
|
|
1041
1115
|
|
|
1042
1116
|
// src/commands/init/steps/config-files.ts
|
|
1043
1117
|
import { existsSync as existsSync11, readFileSync as readFileSync5 } from "fs";
|
|
1118
|
+
import { randomBytes } from "crypto";
|
|
1119
|
+
import { spawnSync } from "child_process";
|
|
1044
1120
|
import { join as join11 } from "path";
|
|
1045
1121
|
import fse8 from "fs-extra";
|
|
1122
|
+
function envExampleTemplate(mode) {
|
|
1123
|
+
return withPlaceholderCreds(`# Example environment \u2014 committed reference for the variables SPFN uses.
|
|
1124
|
+
# Real values live in .env.local (Next.js) and .env.server (backend secrets),
|
|
1125
|
+
# both gitignored. This file documents the keys; it is not loaded by anything.
|
|
1126
|
+
|
|
1127
|
+
${ENV_LOCAL_TEMPLATE}${mode === "full" ? FULL_ENV_LOCAL_EXAMPLE : ""}
|
|
1128
|
+
${ENV_SERVER_TEMPLATE}${mode === "full" ? FULL_ENV_SERVER_EXAMPLE : ""}`);
|
|
1129
|
+
}
|
|
1130
|
+
function envServerExampleTemplate(mode) {
|
|
1131
|
+
return withPlaceholderCreds(
|
|
1132
|
+
`${ENV_SERVER_TEMPLATE}${mode === "full" ? FULL_ENV_SERVER_EXAMPLE : ""}`
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
function envLocalTemplate(mode) {
|
|
1136
|
+
if (mode === "bare") {
|
|
1137
|
+
return ENV_LOCAL_TEMPLATE;
|
|
1138
|
+
}
|
|
1139
|
+
return `${ENV_LOCAL_TEMPLATE}
|
|
1140
|
+
# Full scaffold: auth session encryption (Next.js server only; never NEXT_PUBLIC_*)
|
|
1141
|
+
SPFN_AUTH_SESSION_SECRET=${randomSecret("base64url")}
|
|
1142
|
+
SPFN_AUTH_SESSION_TTL=7d
|
|
1143
|
+
NEXT_PUBLIC_SPFN_APP_URL=http://localhost:3790
|
|
1144
|
+
`;
|
|
1145
|
+
}
|
|
1146
|
+
function envLocalExampleTemplate(mode) {
|
|
1147
|
+
return `${ENV_LOCAL_TEMPLATE}${mode === "full" ? FULL_ENV_LOCAL_EXAMPLE : ""}`;
|
|
1148
|
+
}
|
|
1149
|
+
function envServerTemplate(mode) {
|
|
1150
|
+
if (mode === "bare") {
|
|
1151
|
+
return ENV_SERVER_TEMPLATE;
|
|
1152
|
+
}
|
|
1153
|
+
return `${ENV_SERVER_TEMPLATE}
|
|
1154
|
+
# Full scaffold: authentication
|
|
1155
|
+
SPFN_AUTH_VERIFICATION_TOKEN_SECRET=${randomSecret("base64url")}
|
|
1156
|
+
SPFN_AUTH_TOKEN_ENCRYPTION_KEYS=v1:${randomSecret("base64")}
|
|
1157
|
+
|
|
1158
|
+
# Enable the social providers you need.
|
|
1159
|
+
# SPFN_AUTH_GOOGLE_CLIENT_ID=your-google-client-id
|
|
1160
|
+
# SPFN_AUTH_GOOGLE_CLIENT_SECRET=your-google-client-secret
|
|
1161
|
+
# SPFN_AUTH_GITHUB_CLIENT_ID=your-github-client-id
|
|
1162
|
+
# SPFN_AUTH_GITHUB_CLIENT_SECRET=your-github-client-secret
|
|
1163
|
+
# SPFN_AUTH_KAKAO_CLIENT_ID=your-kakao-rest-api-key
|
|
1164
|
+
# SPFN_AUTH_KAKAO_CLIENT_SECRET=your-kakao-client-secret
|
|
1165
|
+
# SPFN_AUTH_NAVER_CLIENT_ID=your-naver-client-id
|
|
1166
|
+
# SPFN_AUTH_NAVER_CLIENT_SECRET=your-naver-client-secret
|
|
1167
|
+
|
|
1168
|
+
# Full scaffold: MCP operator endpoint
|
|
1169
|
+
SPFN_MCP_URL=http://localhost:8790
|
|
1170
|
+
SPFN_MCP_API_KEY=${randomSecret("base64url")}
|
|
1171
|
+
`;
|
|
1172
|
+
}
|
|
1173
|
+
function randomSecret(encoding) {
|
|
1174
|
+
return randomBytes(32).toString(encoding);
|
|
1175
|
+
}
|
|
1046
1176
|
function withPlaceholderCreds(text) {
|
|
1047
1177
|
return text.replace(/(postgresql:\/\/)[^@\s/]+@/g, "$1user:password@");
|
|
1048
1178
|
}
|
|
1049
|
-
async function setupConfigFiles(cwd) {
|
|
1179
|
+
async function setupConfigFiles(cwd, mode) {
|
|
1050
1180
|
updateGitignore(cwd);
|
|
1051
|
-
generateEnvFiles(cwd);
|
|
1181
|
+
generateEnvFiles(cwd, mode);
|
|
1052
1182
|
const spfnrcPath = join11(cwd, ".spfnrc.ts");
|
|
1053
1183
|
if (!existsSync11(spfnrcPath)) {
|
|
1054
1184
|
const spfnrcContent = `import { defineConfig, defineGenerator } from '@spfn/core/codegen';
|
|
@@ -1076,20 +1206,42 @@ export default defineConfig({
|
|
|
1076
1206
|
}
|
|
1077
1207
|
updateTsconfig(cwd);
|
|
1078
1208
|
}
|
|
1079
|
-
function generateEnvFiles(cwd) {
|
|
1080
|
-
|
|
1081
|
-
writeServerEnv(cwd);
|
|
1082
|
-
writeExampleEnv(cwd,
|
|
1209
|
+
function generateEnvFiles(cwd, mode) {
|
|
1210
|
+
writeLocalEnv(cwd, envLocalTemplate(mode), envLocalExampleTemplate(mode));
|
|
1211
|
+
writeServerEnv(cwd, envServerTemplate(mode), envServerExampleTemplate(mode));
|
|
1212
|
+
writeExampleEnv(cwd, envExampleTemplate(mode));
|
|
1213
|
+
}
|
|
1214
|
+
function writeLocalEnv(cwd, template, exampleTemplate) {
|
|
1215
|
+
const filename = ".env.local";
|
|
1216
|
+
const filePath = join11(cwd, filename);
|
|
1217
|
+
if (isGitTracked(cwd, filename)) {
|
|
1218
|
+
const referenceName = ".env.local.spfn.example";
|
|
1219
|
+
const referencePath = join11(cwd, referenceName);
|
|
1220
|
+
if (!existsSync11(referencePath)) {
|
|
1221
|
+
writeFileSync8(referencePath, exampleTemplate);
|
|
1222
|
+
}
|
|
1223
|
+
logger.warn(`${filename} is tracked by Git \u2014 left it untouched; wrote ${referenceName}. Untrack ${filename}, then add the missing keys with freshly generated values`);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
writeEnvFile(cwd, filename, template);
|
|
1227
|
+
restrictEnvFilePerms(filePath);
|
|
1228
|
+
}
|
|
1229
|
+
function isGitTracked(cwd, filename) {
|
|
1230
|
+
const result = spawnSync("git", ["ls-files", "--error-unmatch", "--", filename], {
|
|
1231
|
+
cwd,
|
|
1232
|
+
stdio: "ignore"
|
|
1233
|
+
});
|
|
1234
|
+
return result.status === 0;
|
|
1083
1235
|
}
|
|
1084
|
-
function writeServerEnv(cwd) {
|
|
1236
|
+
function writeServerEnv(cwd, template, exampleTemplate) {
|
|
1085
1237
|
const filePath = join11(cwd, ".env.server");
|
|
1086
1238
|
if (!existsSync11(filePath)) {
|
|
1087
|
-
writeFileSync8(filePath,
|
|
1239
|
+
writeFileSync8(filePath, template);
|
|
1088
1240
|
restrictEnvFilePerms(filePath);
|
|
1089
1241
|
logger.success("Created .env.server");
|
|
1090
1242
|
return;
|
|
1091
1243
|
}
|
|
1092
|
-
writeFileSync8(join11(cwd, ".env.server.example"),
|
|
1244
|
+
writeFileSync8(join11(cwd, ".env.server.example"), exampleTemplate);
|
|
1093
1245
|
logger.warn(".env.server already exists \u2014 left it untouched; wrote SPFN's reference to .env.server.example, add any missing keys manually");
|
|
1094
1246
|
}
|
|
1095
1247
|
function writeExampleEnv(cwd, content) {
|
|
@@ -1181,7 +1333,7 @@ function updateTsconfig(cwd) {
|
|
|
1181
1333
|
logger.warn('Could not update tsconfig.json (you can add "src/server" to exclude manually)');
|
|
1182
1334
|
}
|
|
1183
1335
|
}
|
|
1184
|
-
var writeFileSync8, ENV_LOCAL_TEMPLATE, ENV_SERVER_TEMPLATE,
|
|
1336
|
+
var writeFileSync8, ENV_LOCAL_TEMPLATE, FULL_ENV_LOCAL_EXAMPLE, ENV_SERVER_TEMPLATE, FULL_ENV_SERVER_EXAMPLE;
|
|
1185
1337
|
var init_config_files = __esm({
|
|
1186
1338
|
"src/commands/init/steps/config-files.ts"() {
|
|
1187
1339
|
"use strict";
|
|
@@ -1189,8 +1341,8 @@ var init_config_files = __esm({
|
|
|
1189
1341
|
init_env_file();
|
|
1190
1342
|
({ writeFileSync: writeFileSync8 } = fse8);
|
|
1191
1343
|
ENV_LOCAL_TEMPLATE = `# Next.js environment
|
|
1192
|
-
# Loaded by Next.js (and the SPFN backend).
|
|
1193
|
-
#
|
|
1344
|
+
# Loaded by Next.js (and the SPFN backend). Never prefix secrets with
|
|
1345
|
+
# NEXT_PUBLIC_. Backend-only secrets belong in .env.server.
|
|
1194
1346
|
|
|
1195
1347
|
# SPFN API endpoint \u2014 browser + Next.js SSR/proxy target
|
|
1196
1348
|
SPFN_API_URL=http://localhost:8790
|
|
@@ -1198,6 +1350,12 @@ NEXT_PUBLIC_SPFN_API_URL=http://localhost:8790
|
|
|
1198
1350
|
|
|
1199
1351
|
# Next.js app URL (used by the SPFN server for CORS/redirects)
|
|
1200
1352
|
SPFN_APP_URL=http://localhost:3790
|
|
1353
|
+
`;
|
|
1354
|
+
FULL_ENV_LOCAL_EXAMPLE = `
|
|
1355
|
+
# Full scaffold: auth session encryption (Next.js server only; never NEXT_PUBLIC_*)
|
|
1356
|
+
SPFN_AUTH_SESSION_SECRET=replace-with-a-random-secret-at-least-32-characters
|
|
1357
|
+
SPFN_AUTH_SESSION_TTL=7d
|
|
1358
|
+
NEXT_PUBLIC_SPFN_APP_URL=http://localhost:3790
|
|
1201
1359
|
`;
|
|
1202
1360
|
ENV_SERVER_TEMPLATE = `# SPFN backend environment
|
|
1203
1361
|
# Loaded ONLY by the SPFN server, never by Next.js. Keep all server-only config
|
|
@@ -1224,12 +1382,25 @@ DB_POOL_IDLE_TIMEOUT=30
|
|
|
1224
1382
|
# DATABASE_READ_URL=postgresql://user:password@replica:5432/dbname
|
|
1225
1383
|
# CACHE_PASSWORD=your-redis-password
|
|
1226
1384
|
`;
|
|
1227
|
-
|
|
1228
|
-
#
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1385
|
+
FULL_ENV_SERVER_EXAMPLE = `
|
|
1386
|
+
# Full scaffold: authentication
|
|
1387
|
+
SPFN_AUTH_VERIFICATION_TOKEN_SECRET=replace-with-a-random-secret-at-least-32-characters
|
|
1388
|
+
SPFN_AUTH_TOKEN_ENCRYPTION_KEYS=v1:replace-with-a-base64-encoded-32-byte-key
|
|
1389
|
+
|
|
1390
|
+
# Enable the social providers you need.
|
|
1391
|
+
# SPFN_AUTH_GOOGLE_CLIENT_ID=your-google-client-id
|
|
1392
|
+
# SPFN_AUTH_GOOGLE_CLIENT_SECRET=your-google-client-secret
|
|
1393
|
+
# SPFN_AUTH_GITHUB_CLIENT_ID=your-github-client-id
|
|
1394
|
+
# SPFN_AUTH_GITHUB_CLIENT_SECRET=your-github-client-secret
|
|
1395
|
+
# SPFN_AUTH_KAKAO_CLIENT_ID=your-kakao-rest-api-key
|
|
1396
|
+
# SPFN_AUTH_KAKAO_CLIENT_SECRET=your-kakao-client-secret
|
|
1397
|
+
# SPFN_AUTH_NAVER_CLIENT_ID=your-naver-client-id
|
|
1398
|
+
# SPFN_AUTH_NAVER_CLIENT_SECRET=your-naver-client-secret
|
|
1399
|
+
|
|
1400
|
+
# Full scaffold: MCP operator endpoint
|
|
1401
|
+
SPFN_MCP_URL=http://localhost:8790
|
|
1402
|
+
SPFN_MCP_API_KEY=replace-with-a-random-operator-key
|
|
1403
|
+
`;
|
|
1233
1404
|
}
|
|
1234
1405
|
});
|
|
1235
1406
|
|
|
@@ -1254,15 +1425,16 @@ async function setupReadme(cwd, ctx) {
|
|
|
1254
1425
|
const content = renderReadme(template, {
|
|
1255
1426
|
projectName: ctx.packageJson.name || basename(cwd) || "my-app",
|
|
1256
1427
|
pm: ctx.pm,
|
|
1428
|
+
pmExec: ctx.pm === "npm" ? "npx" : ctx.pm,
|
|
1257
1429
|
pmRun: getRunCommand(ctx.pm),
|
|
1258
|
-
|
|
1430
|
+
mode: ctx.mode
|
|
1259
1431
|
});
|
|
1260
1432
|
writeFileSync9(readmePath, content);
|
|
1261
1433
|
logger.success("Created README.md");
|
|
1262
1434
|
}
|
|
1263
1435
|
function renderReadme(template, vars) {
|
|
1264
1436
|
const authBlock = /\r?\n?<!-- \{\{#auth\}\} -->\r?\n([\s\S]*?)\r?\n<!-- \{\{\/auth\}\} -->\r?\n/;
|
|
1265
|
-
return template.replace(authBlock, vars.
|
|
1437
|
+
return template.replace(authBlock, vars.mode === "full" ? "\n$1\n" : "\n").replaceAll("{{mode}}", () => vars.mode).replaceAll("{{projectName}}", () => vars.projectName).replaceAll("{{pmRun}}", () => vars.pmRun).replaceAll("{{pmExec}}", () => vars.pmExec).replaceAll("{{pm}}", () => vars.pm);
|
|
1266
1438
|
}
|
|
1267
1439
|
var writeFileSync9;
|
|
1268
1440
|
var init_readme = __esm({
|
|
@@ -1281,31 +1453,40 @@ __export(init_exports, {
|
|
|
1281
1453
|
initCommand: () => initCommand,
|
|
1282
1454
|
initializeSpfn: () => initializeSpfn
|
|
1283
1455
|
});
|
|
1284
|
-
import { Command as Command2 } from "commander";
|
|
1456
|
+
import { Command as Command2, Option } from "commander";
|
|
1285
1457
|
import chalk4 from "chalk";
|
|
1286
1458
|
async function initializeSpfn(options = {}) {
|
|
1287
1459
|
const cwd = process.cwd();
|
|
1288
|
-
const { packageJson, packageJsonPath
|
|
1460
|
+
const { packageJson, packageJsonPath } = await validateProject(cwd, options.yes || false);
|
|
1461
|
+
const mode = await selectScaffoldMode(options);
|
|
1462
|
+
const includeAuth = mode === "full";
|
|
1289
1463
|
const pm = detectPackageManager(cwd);
|
|
1290
1464
|
logger.step(`Detected package manager: ${pm}`);
|
|
1291
|
-
await setupServerStructure(cwd);
|
|
1465
|
+
await setupServerStructure(cwd, mode);
|
|
1292
1466
|
await setupApiProxy(cwd, includeAuth);
|
|
1293
1467
|
await setupNextConfig(cwd, includeAuth);
|
|
1294
1468
|
await setupDockerFiles(cwd, pm);
|
|
1295
1469
|
await setupDeploymentConfig(cwd, packageJson, pm);
|
|
1296
|
-
await setupPackageJson(cwd, packageJsonPath, packageJson, pm,
|
|
1297
|
-
await setupConfigFiles(cwd);
|
|
1298
|
-
await setupReadme(cwd, { pm, packageJson,
|
|
1470
|
+
await setupPackageJson(cwd, packageJsonPath, packageJson, pm, mode);
|
|
1471
|
+
await setupConfigFiles(cwd, mode);
|
|
1472
|
+
await setupReadme(cwd, { pm, packageJson, mode, overwrite: options.overwriteReadme ?? false });
|
|
1299
1473
|
console.log("\n" + chalk4.green.bold("\u2713 SPFN initialized successfully!\n"));
|
|
1300
1474
|
console.log("Next steps:");
|
|
1301
1475
|
console.log(" 1. Start PostgreSQL & Redis (if not installed locally):");
|
|
1302
1476
|
console.log(" " + chalk4.cyan("docker compose up -d"));
|
|
1303
1477
|
console.log(" 2. Review the generated env files (.env.local, .env.server)");
|
|
1304
1478
|
console.log(" " + chalk4.dim(ENV_FILES_HINT));
|
|
1305
|
-
|
|
1306
|
-
|
|
1479
|
+
if (mode === "full") {
|
|
1480
|
+
const spfnCommand = pm === "npm" ? "npx spfn" : `${pm} spfn`;
|
|
1481
|
+
console.log(" 3. Apply the auth migrations: " + chalk4.cyan(`${spfnCommand} db migrate`));
|
|
1482
|
+
}
|
|
1483
|
+
console.log(` ${mode === "full" ? "4" : "3"}. Run: ` + chalk4.cyan(`${getRunCommand(pm)} spfn:dev`));
|
|
1484
|
+
console.log(` ${mode === "full" ? "5" : "4"}. Visit:`);
|
|
1307
1485
|
console.log(" - Next.js: " + chalk4.cyan("http://localhost:3790"));
|
|
1308
1486
|
console.log(" - API: " + chalk4.cyan("http://localhost:8790/health"));
|
|
1487
|
+
if (mode === "full") {
|
|
1488
|
+
console.log(" - MCP: " + chalk4.cyan("http://localhost:8790/mcp"));
|
|
1489
|
+
}
|
|
1309
1490
|
console.log("\nAvailable commands:");
|
|
1310
1491
|
console.log(" \u2022 " + chalk4.cyan(`${getRunCommand(pm)} spfn:dev`) + " - Start SPFN + Next.js");
|
|
1311
1492
|
console.log(" \u2022 " + chalk4.cyan("spfn env validate") + " - Validate environment variables");
|
|
@@ -1329,7 +1510,8 @@ var init_init = __esm({
|
|
|
1329
1510
|
init_package();
|
|
1330
1511
|
init_config_files();
|
|
1331
1512
|
init_readme();
|
|
1332
|
-
|
|
1513
|
+
init_mode();
|
|
1514
|
+
initCommand = new Command2("init").description("Initialize SPFN in your Next.js project").addOption(new Option("--mode <mode>", "Scaffold mode: bare (core) or full (core, auth, i18n, MCP)").choices(["bare", "full"])).option("-y, --yes", "Skip prompts and use defaults").action(initializeSpfn);
|
|
1333
1515
|
}
|
|
1334
1516
|
});
|
|
1335
1517
|
|
|
@@ -1342,39 +1524,39 @@ __export(function_migrations_exports, {
|
|
|
1342
1524
|
loadFunctionMigrationPlans: () => loadFunctionMigrationPlans,
|
|
1343
1525
|
readMigrationEntries: () => readMigrationEntries
|
|
1344
1526
|
});
|
|
1345
|
-
import
|
|
1527
|
+
import chalk6 from "chalk";
|
|
1346
1528
|
import { createHash } from "crypto";
|
|
1347
|
-
import { join as
|
|
1348
|
-
import { env
|
|
1349
|
-
import { loadEnv
|
|
1350
|
-
import { existsSync as
|
|
1529
|
+
import { join as join15 } from "path";
|
|
1530
|
+
import { env } from "@spfn/core/config";
|
|
1531
|
+
import { loadEnv } from "@spfn/core/server";
|
|
1532
|
+
import { existsSync as existsSync14, readdirSync, readFileSync as readFileSync7 } from "fs";
|
|
1351
1533
|
function discoverFunctionMigrations(cwd = process.cwd()) {
|
|
1352
|
-
const nodeModulesPath =
|
|
1353
|
-
if (!
|
|
1534
|
+
const nodeModulesPath = join15(cwd, "node_modules");
|
|
1535
|
+
if (!existsSync14(nodeModulesPath)) {
|
|
1354
1536
|
return [];
|
|
1355
1537
|
}
|
|
1356
1538
|
const functions = [];
|
|
1357
|
-
const spfnDir =
|
|
1358
|
-
if (!
|
|
1539
|
+
const spfnDir = join15(nodeModulesPath, "@spfn");
|
|
1540
|
+
if (!existsSync14(spfnDir)) {
|
|
1359
1541
|
return [];
|
|
1360
1542
|
}
|
|
1361
|
-
const packages =
|
|
1543
|
+
const packages = readdirSync(spfnDir);
|
|
1362
1544
|
for (const pkg of packages) {
|
|
1363
|
-
const packagePath =
|
|
1364
|
-
const packageJsonPath =
|
|
1365
|
-
if (!
|
|
1545
|
+
const packagePath = join15(spfnDir, pkg);
|
|
1546
|
+
const packageJsonPath = join15(packagePath, "package.json");
|
|
1547
|
+
if (!existsSync14(packageJsonPath)) {
|
|
1366
1548
|
continue;
|
|
1367
1549
|
}
|
|
1368
1550
|
try {
|
|
1369
|
-
const packageJson = JSON.parse(
|
|
1551
|
+
const packageJson = JSON.parse(readFileSync7(packageJsonPath, "utf-8"));
|
|
1370
1552
|
const spfnConfig = packageJson.spfn;
|
|
1371
1553
|
if (!spfnConfig?.migrations) {
|
|
1372
1554
|
continue;
|
|
1373
1555
|
}
|
|
1374
|
-
const migrationsDir =
|
|
1375
|
-
if (!
|
|
1556
|
+
const migrationsDir = join15(packagePath, spfnConfig.migrations.dir);
|
|
1557
|
+
if (!existsSync14(migrationsDir)) {
|
|
1376
1558
|
console.warn(
|
|
1377
|
-
|
|
1559
|
+
chalk6.yellow(`\u26A0\uFE0F Package @spfn/${pkg} specifies migrations but directory not found: ${migrationsDir}`)
|
|
1378
1560
|
);
|
|
1379
1561
|
continue;
|
|
1380
1562
|
}
|
|
@@ -1384,14 +1566,14 @@ function discoverFunctionMigrations(cwd = process.cwd()) {
|
|
|
1384
1566
|
packagePath
|
|
1385
1567
|
});
|
|
1386
1568
|
} catch (error) {
|
|
1387
|
-
console.warn(
|
|
1569
|
+
console.warn(chalk6.yellow(`\u26A0\uFE0F Failed to parse package.json for @spfn/${pkg}`));
|
|
1388
1570
|
}
|
|
1389
1571
|
}
|
|
1390
1572
|
return functions;
|
|
1391
1573
|
}
|
|
1392
1574
|
function readMigrationEntries(migrationsDir, packageName) {
|
|
1393
|
-
const journalPath =
|
|
1394
|
-
return
|
|
1575
|
+
const journalPath = join15(migrationsDir, "meta", "_journal.json");
|
|
1576
|
+
return existsSync14(journalPath) ? readJournalEntries(migrationsDir, journalPath, packageName) : readFolderEntries(migrationsDir, packageName);
|
|
1395
1577
|
}
|
|
1396
1578
|
function loadFunctionMigrationPlans(functionMigrations) {
|
|
1397
1579
|
return functionMigrations.map((func) => ({
|
|
@@ -1402,7 +1584,7 @@ function loadFunctionMigrationPlans(functionMigrations) {
|
|
|
1402
1584
|
function readJournalEntries(migrationsDir, journalPath, packageName) {
|
|
1403
1585
|
let journal;
|
|
1404
1586
|
try {
|
|
1405
|
-
journal = JSON.parse(
|
|
1587
|
+
journal = JSON.parse(readFileSync7(journalPath, "utf-8"));
|
|
1406
1588
|
} catch {
|
|
1407
1589
|
journal = {};
|
|
1408
1590
|
}
|
|
@@ -1415,18 +1597,18 @@ function readJournalEntries(migrationsDir, journalPath, packageName) {
|
|
|
1415
1597
|
if (typeof entry?.tag !== "string" || typeof entry?.when !== "number") {
|
|
1416
1598
|
throw new Error(`${packageName}: invalid journal entry in ${journalPath}`);
|
|
1417
1599
|
}
|
|
1418
|
-
const sqlPath =
|
|
1419
|
-
if (!
|
|
1600
|
+
const sqlPath = join15(migrationsDir, `${entry.tag}.sql`);
|
|
1601
|
+
if (!existsSync14(sqlPath)) {
|
|
1420
1602
|
throw new Error(`${packageName}: migration file not found: ${entry.tag}.sql`);
|
|
1421
1603
|
}
|
|
1422
|
-
return toEntry(entry.tag,
|
|
1604
|
+
return toEntry(entry.tag, readFileSync7(sqlPath, "utf-8"), entry.when);
|
|
1423
1605
|
});
|
|
1424
1606
|
}
|
|
1425
1607
|
function readFolderEntries(migrationsDir, packageName) {
|
|
1426
|
-
const folders =
|
|
1608
|
+
const folders = readdirSync(migrationsDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name).filter((name) => existsSync14(join15(migrationsDir, name, "migration.sql"))).sort((a, b) => a.localeCompare(b));
|
|
1427
1609
|
return folders.map((name) => toEntry(
|
|
1428
1610
|
name,
|
|
1429
|
-
|
|
1611
|
+
readFileSync7(join15(migrationsDir, name, "migration.sql"), "utf-8"),
|
|
1430
1612
|
folderTimestampMillis(name, packageName)
|
|
1431
1613
|
));
|
|
1432
1614
|
}
|
|
@@ -1466,7 +1648,7 @@ async function migrateLegacyTable(db, plans) {
|
|
|
1466
1648
|
if (!legacyCheck[0]?.exists) {
|
|
1467
1649
|
return;
|
|
1468
1650
|
}
|
|
1469
|
-
console.log(
|
|
1651
|
+
console.log(chalk6.dim("\n Migrating legacy shared migration table to per-package tables..."));
|
|
1470
1652
|
const legacyRows = await db.query(
|
|
1471
1653
|
`SELECT hash, created_at FROM drizzle."__spfn_fn_migrations" ORDER BY id`
|
|
1472
1654
|
);
|
|
@@ -1496,11 +1678,11 @@ async function migrateLegacyTable(db, plans) {
|
|
|
1496
1678
|
copied++;
|
|
1497
1679
|
}
|
|
1498
1680
|
if (copied > 0) {
|
|
1499
|
-
console.log(
|
|
1681
|
+
console.log(chalk6.dim(` \u2713 ${plan.packageName}: copied ${copied} migration record(s)`));
|
|
1500
1682
|
}
|
|
1501
1683
|
}
|
|
1502
1684
|
await db.query(`DROP TABLE drizzle."__spfn_fn_migrations"`);
|
|
1503
|
-
console.log(
|
|
1685
|
+
console.log(chalk6.dim(" \u2713 Legacy migration table removed\n"));
|
|
1504
1686
|
}
|
|
1505
1687
|
async function ensureMigrationsTable(db, tableName) {
|
|
1506
1688
|
await db.query(`CREATE SCHEMA IF NOT EXISTS drizzle`);
|
|
@@ -1544,20 +1726,20 @@ function createPostgresJsMigrationDb(client) {
|
|
|
1544
1726
|
}
|
|
1545
1727
|
async function executeFunctionMigrations(plans) {
|
|
1546
1728
|
const postgres = await import("postgres");
|
|
1547
|
-
|
|
1548
|
-
if (!
|
|
1729
|
+
loadEnv();
|
|
1730
|
+
if (!env.DATABASE_URL) {
|
|
1549
1731
|
throw new Error("DATABASE_URL not found in environment");
|
|
1550
1732
|
}
|
|
1551
|
-
const connection = postgres.default(
|
|
1733
|
+
const connection = postgres.default(env.DATABASE_URL, { max: 1 });
|
|
1552
1734
|
const db = createPostgresJsMigrationDb(connection);
|
|
1553
1735
|
let appliedCount = 0;
|
|
1554
1736
|
try {
|
|
1555
1737
|
await migrateLegacyTable(db, plans);
|
|
1556
1738
|
for (const plan of plans) {
|
|
1557
|
-
console.log(
|
|
1739
|
+
console.log(chalk6.blue(`
|
|
1558
1740
|
\u{1F4E6} Running ${plan.packageName} migrations...`));
|
|
1559
1741
|
const applied = await applyFunctionMigrationPlan(db, plan);
|
|
1560
|
-
console.log(applied > 0 ?
|
|
1742
|
+
console.log(applied > 0 ? chalk6.green(` \u2713 ${plan.packageName}: ${applied} migration(s) applied`) : chalk6.dim(` \u2013 ${plan.packageName}: up to date`));
|
|
1561
1743
|
appliedCount += applied;
|
|
1562
1744
|
}
|
|
1563
1745
|
} finally {
|
|
@@ -1571,6 +1753,112 @@ var init_function_migrations = __esm({
|
|
|
1571
1753
|
}
|
|
1572
1754
|
});
|
|
1573
1755
|
|
|
1756
|
+
// src/utils/migration-status.ts
|
|
1757
|
+
var migration_status_exports = {};
|
|
1758
|
+
__export(migration_status_exports, {
|
|
1759
|
+
filterPendingEntries: () => filterPendingEntries,
|
|
1760
|
+
functionMigrationsTable: () => functionMigrationsTable2,
|
|
1761
|
+
getMigrationStatus: () => getMigrationStatus,
|
|
1762
|
+
warnPendingMigrations: () => warnPendingMigrations
|
|
1763
|
+
});
|
|
1764
|
+
import { join as join16 } from "path";
|
|
1765
|
+
import { existsSync as existsSync15 } from "fs";
|
|
1766
|
+
import chalk7 from "chalk";
|
|
1767
|
+
import { env as env2 } from "@spfn/core/config";
|
|
1768
|
+
import { loadEnv as loadEnv2 } from "@spfn/core/server";
|
|
1769
|
+
function functionMigrationsTable2(packageName) {
|
|
1770
|
+
return `__spfn_fn_${packageName.replace("@spfn/", "")}_migrations`;
|
|
1771
|
+
}
|
|
1772
|
+
function filterPendingEntries(entries, lastAppliedMillis, appliedNames) {
|
|
1773
|
+
return entries.filter((entry) => entry.millis > lastAppliedMillis && !appliedNames.has(entry.name));
|
|
1774
|
+
}
|
|
1775
|
+
async function readAppliedNames(sql2, tableName) {
|
|
1776
|
+
const columnCheck = await sql2`
|
|
1777
|
+
SELECT EXISTS (
|
|
1778
|
+
SELECT 1 FROM information_schema.columns
|
|
1779
|
+
WHERE table_schema = 'drizzle' AND table_name = ${tableName} AND column_name = 'name'
|
|
1780
|
+
) AS "exists"`;
|
|
1781
|
+
if (!columnCheck[0]?.exists) {
|
|
1782
|
+
return /* @__PURE__ */ new Set();
|
|
1783
|
+
}
|
|
1784
|
+
const rows = await sql2`
|
|
1785
|
+
SELECT name FROM drizzle.${sql2(tableName)} WHERE name IS NOT NULL`;
|
|
1786
|
+
return new Set(rows.map((row) => row.name));
|
|
1787
|
+
}
|
|
1788
|
+
async function collectTargetStatus(sql2, name, migrationsDir, tableName) {
|
|
1789
|
+
const entries = readMigrationEntries(migrationsDir, name);
|
|
1790
|
+
const tableCheck = await sql2`
|
|
1791
|
+
SELECT EXISTS (
|
|
1792
|
+
SELECT 1 FROM information_schema.tables
|
|
1793
|
+
WHERE table_schema = 'drizzle' AND table_name = ${tableName}
|
|
1794
|
+
) AS "exists"`;
|
|
1795
|
+
let lastApplied = 0;
|
|
1796
|
+
let appliedNames = /* @__PURE__ */ new Set();
|
|
1797
|
+
if (tableCheck[0]?.exists) {
|
|
1798
|
+
const rows = await sql2`
|
|
1799
|
+
SELECT created_at FROM drizzle.${sql2(tableName)}
|
|
1800
|
+
ORDER BY created_at DESC LIMIT 1`;
|
|
1801
|
+
lastApplied = rows[0]?.created_at ? Number(rows[0].created_at) : 0;
|
|
1802
|
+
appliedNames = await readAppliedNames(sql2, tableName);
|
|
1803
|
+
}
|
|
1804
|
+
const pendingEntries = filterPendingEntries(entries, lastApplied, appliedNames);
|
|
1805
|
+
return {
|
|
1806
|
+
name,
|
|
1807
|
+
total: entries.length,
|
|
1808
|
+
applied: entries.length - pendingEntries.length,
|
|
1809
|
+
pending: pendingEntries.length,
|
|
1810
|
+
pendingTags: pendingEntries.map((e) => e.name)
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
async function getMigrationStatus(cwd = process.cwd(), databaseUrl) {
|
|
1814
|
+
loadEnv2();
|
|
1815
|
+
const url = databaseUrl ?? env2.DATABASE_URL;
|
|
1816
|
+
if (!url) {
|
|
1817
|
+
throw new Error("DATABASE_URL not found in environment");
|
|
1818
|
+
}
|
|
1819
|
+
const postgres = await import("postgres");
|
|
1820
|
+
const sql2 = postgres.default(url, { max: 1, connect_timeout: 5 });
|
|
1821
|
+
try {
|
|
1822
|
+
const packages = [];
|
|
1823
|
+
for (const func of discoverFunctionMigrations(cwd)) {
|
|
1824
|
+
packages.push(await collectTargetStatus(
|
|
1825
|
+
sql2,
|
|
1826
|
+
func.packageName,
|
|
1827
|
+
func.migrationsDir,
|
|
1828
|
+
functionMigrationsTable2(func.packageName)
|
|
1829
|
+
));
|
|
1830
|
+
}
|
|
1831
|
+
const projectDir = join16(cwd, "src", "server", "drizzle");
|
|
1832
|
+
const project = existsSync15(projectDir) ? await collectTargetStatus(sql2, "project (src/server/drizzle)", projectDir, "__drizzle_migrations") : null;
|
|
1833
|
+
return { packages, project: project && project.total > 0 ? project : null };
|
|
1834
|
+
} finally {
|
|
1835
|
+
await sql2.end();
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
async function warnPendingMigrations(cwd, databaseUrl) {
|
|
1839
|
+
let status;
|
|
1840
|
+
try {
|
|
1841
|
+
status = await getMigrationStatus(cwd, databaseUrl);
|
|
1842
|
+
} catch {
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
const targets = [...status.packages, ...status.project ? [status.project] : []].filter((t) => t.pending > 0);
|
|
1846
|
+
if (targets.length === 0) {
|
|
1847
|
+
return;
|
|
1848
|
+
}
|
|
1849
|
+
console.warn("");
|
|
1850
|
+
for (const target of targets) {
|
|
1851
|
+
console.warn(chalk7.yellow(`\u26A0\uFE0F ${target.name}: ${target.pending} pending migration(s)`));
|
|
1852
|
+
}
|
|
1853
|
+
console.warn(chalk7.yellow(" Missing tables will fail at request time \u2014 run: pnpm spfn db migrate\n"));
|
|
1854
|
+
}
|
|
1855
|
+
var init_migration_status = __esm({
|
|
1856
|
+
"src/utils/migration-status.ts"() {
|
|
1857
|
+
"use strict";
|
|
1858
|
+
init_function_migrations();
|
|
1859
|
+
}
|
|
1860
|
+
});
|
|
1861
|
+
|
|
1574
1862
|
// src/index.ts
|
|
1575
1863
|
import { Command as Command13 } from "commander";
|
|
1576
1864
|
|
|
@@ -1578,10 +1866,11 @@ import { Command as Command13 } from "commander";
|
|
|
1578
1866
|
init_logger();
|
|
1579
1867
|
init_package_manager();
|
|
1580
1868
|
init_messages();
|
|
1581
|
-
|
|
1869
|
+
init_mode();
|
|
1870
|
+
import { Command as Command3, Option as Option2 } from "commander";
|
|
1582
1871
|
import { existsSync as existsSync13 } from "fs";
|
|
1583
1872
|
import { join as join13, resolve, dirname as dirname2 } from "path";
|
|
1584
|
-
import
|
|
1873
|
+
import prompts3 from "prompts";
|
|
1585
1874
|
import ora4 from "ora";
|
|
1586
1875
|
import { execa as execa3 } from "execa";
|
|
1587
1876
|
import chalk5 from "chalk";
|
|
@@ -1607,7 +1896,7 @@ async function createProject(projectName, options) {
|
|
|
1607
1896
|
console.log(chalk5.blue.bold("\n\u{1F680} Creating Next.js project with SPFN...\n"));
|
|
1608
1897
|
let pm = options.pm || detectPackageManager(cwd);
|
|
1609
1898
|
if (!options.yes && !options.pm) {
|
|
1610
|
-
const { selectedPm } = await
|
|
1899
|
+
const { selectedPm } = await prompts3({
|
|
1611
1900
|
type: "select",
|
|
1612
1901
|
name: "selectedPm",
|
|
1613
1902
|
message: "Which package manager do you want to use?",
|
|
@@ -1625,6 +1914,8 @@ async function createProject(projectName, options) {
|
|
|
1625
1914
|
pm = selectedPm;
|
|
1626
1915
|
}
|
|
1627
1916
|
logger.step(`Using package manager: ${pm}`);
|
|
1917
|
+
const mode = await selectScaffoldMode(options);
|
|
1918
|
+
logger.step(`Using scaffold mode: ${mode}`);
|
|
1628
1919
|
const workspaceRoot = pm === "pnpm" ? findPnpmWorkspaceRoot(cwd) : null;
|
|
1629
1920
|
const isInWorkspace = workspaceRoot !== null;
|
|
1630
1921
|
if (isInWorkspace) {
|
|
@@ -1723,7 +2014,7 @@ async function createProject(projectName, options) {
|
|
|
1723
2014
|
const initSpinner = ora4("Initializing SPFN...").start();
|
|
1724
2015
|
try {
|
|
1725
2016
|
const { initializeSpfn: initializeSpfn2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
1726
|
-
await initializeSpfn2({ yes: true, overwriteReadme: true });
|
|
2017
|
+
await initializeSpfn2({ yes: true, overwriteReadme: true, mode });
|
|
1727
2018
|
initSpinner.succeed("SPFN initialized");
|
|
1728
2019
|
} catch (error) {
|
|
1729
2020
|
initSpinner.fail("Failed to initialize SPFN");
|
|
@@ -1735,6 +2026,10 @@ async function createProject(projectName, options) {
|
|
|
1735
2026
|
console.log(` ${chalk5.cyan("cd")} ${projectName}`);
|
|
1736
2027
|
console.log(` ${chalk5.cyan("docker compose up -d")} ${chalk5.gray("# Start PostgreSQL & Redis")}`);
|
|
1737
2028
|
console.log(` ${chalk5.gray(`# .env.local & .env.server are generated \u2014 ${ENV_FILES_HINT}`)}`);
|
|
2029
|
+
if (mode === "full") {
|
|
2030
|
+
const spfnCommand = pm === "npm" ? "npx spfn" : `${pm} spfn`;
|
|
2031
|
+
console.log(` ${chalk5.cyan(`${spfnCommand} db migrate`)} ${chalk5.gray("# Apply auth migrations")}`);
|
|
2032
|
+
}
|
|
1738
2033
|
console.log(` ${chalk5.cyan(`${getRunCommand(pm)} spfn:dev`)} ${chalk5.gray("# Start dev server")}
|
|
1739
2034
|
`);
|
|
1740
2035
|
console.log(chalk5.bold("Your app will be available at:\n"));
|
|
@@ -1751,7 +2046,7 @@ async function createProject(projectName, options) {
|
|
|
1751
2046
|
`);
|
|
1752
2047
|
console.log(chalk5.dim(" \u{1F310} Documentation: https://github.com/spfn/spfn\n"));
|
|
1753
2048
|
}
|
|
1754
|
-
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) => {
|
|
2049
|
+
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)").addOption(new Option2("--mode <mode>", "Scaffold mode: bare (core) or full (core, auth, i18n, MCP)").choices(["bare", "full"])).option("-y, --yes", "Skip prompts and use defaults").action(async (projectName, options) => {
|
|
1755
2050
|
await createProject(projectName, options);
|
|
1756
2051
|
});
|
|
1757
2052
|
|
|
@@ -1762,8 +2057,8 @@ init_init();
|
|
|
1762
2057
|
init_logger();
|
|
1763
2058
|
init_package_manager();
|
|
1764
2059
|
import { Command as Command4 } from "commander";
|
|
1765
|
-
import { existsSync as
|
|
1766
|
-
import { join as
|
|
2060
|
+
import { existsSync as existsSync16, readFileSync as readFileSync8, writeFileSync as writeFileSync10, mkdirSync, unlinkSync, rmSync, watch } from "fs";
|
|
2061
|
+
import { join as join17, relative, sep as sep2 } from "path";
|
|
1767
2062
|
import { execa as execa6 } from "execa";
|
|
1768
2063
|
import chokidar from "chokidar";
|
|
1769
2064
|
|
|
@@ -1939,10 +2234,10 @@ async function resolveKeychainEnv(cwd) {
|
|
|
1939
2234
|
return { env: {}, missing: [] };
|
|
1940
2235
|
}
|
|
1941
2236
|
const store = detectStore();
|
|
1942
|
-
const
|
|
2237
|
+
const env9 = {};
|
|
1943
2238
|
const missing = [];
|
|
1944
2239
|
if (!await store.isAvailable()) {
|
|
1945
|
-
return { env:
|
|
2240
|
+
return { env: env9, missing: refs.map(([key]) => key) };
|
|
1946
2241
|
}
|
|
1947
2242
|
for (const [key, value] of refs) {
|
|
1948
2243
|
const name = value.slice(KEYCHAIN_REF_PREFIX.length);
|
|
@@ -1950,32 +2245,32 @@ async function resolveKeychainEnv(cwd) {
|
|
|
1950
2245
|
if (resolved === null) {
|
|
1951
2246
|
missing.push(key);
|
|
1952
2247
|
} else {
|
|
1953
|
-
|
|
2248
|
+
env9[key] = resolved;
|
|
1954
2249
|
}
|
|
1955
2250
|
}
|
|
1956
|
-
return { env:
|
|
2251
|
+
return { env: env9, missing };
|
|
1957
2252
|
}
|
|
1958
2253
|
|
|
1959
2254
|
// src/commands/dev.ts
|
|
1960
2255
|
function ignoreDotfilesUnder(root) {
|
|
1961
|
-
return (watchedPath) => relative(root, watchedPath).split(
|
|
2256
|
+
return (watchedPath) => relative(root, watchedPath).split(sep2).some((segment) => segment.startsWith(".") && segment !== "." && segment !== "..");
|
|
1962
2257
|
}
|
|
1963
2258
|
function waitForReadyFile(filePath, timeoutMs = 3e4) {
|
|
1964
2259
|
return new Promise((resolve3, reject) => {
|
|
1965
|
-
if (
|
|
2260
|
+
if (existsSync16(filePath)) {
|
|
1966
2261
|
unlinkSync(filePath);
|
|
1967
2262
|
}
|
|
1968
2263
|
const timer = setTimeout(() => {
|
|
1969
2264
|
watcher.close();
|
|
1970
2265
|
reject(new Error(`Server did not become ready within ${timeoutMs / 1e3}s`));
|
|
1971
2266
|
}, timeoutMs);
|
|
1972
|
-
const dir =
|
|
2267
|
+
const dir = join17(filePath, "..");
|
|
1973
2268
|
const fileName = filePath.split("/").pop();
|
|
1974
2269
|
const watcher = watch(dir, (_event, name) => {
|
|
1975
|
-
if (name === fileName &&
|
|
2270
|
+
if (name === fileName && existsSync16(filePath)) {
|
|
1976
2271
|
watcher.close();
|
|
1977
2272
|
clearTimeout(timer);
|
|
1978
|
-
resolve3(
|
|
2273
|
+
resolve3(readFileSync8(filePath, "utf-8").trim());
|
|
1979
2274
|
}
|
|
1980
2275
|
});
|
|
1981
2276
|
});
|
|
@@ -1986,8 +2281,8 @@ var devCommand = new Command4("dev").description("Start SPFN development server
|
|
|
1986
2281
|
process.env.NODE_ENV = "development";
|
|
1987
2282
|
}
|
|
1988
2283
|
const cwd = process.cwd();
|
|
1989
|
-
const serverDir =
|
|
1990
|
-
if (!
|
|
2284
|
+
const serverDir = join17(cwd, "src", "server");
|
|
2285
|
+
if (!existsSync16(serverDir)) {
|
|
1991
2286
|
logger.error("src/server directory not found.");
|
|
1992
2287
|
logger.info('Run "spfn init" first to initialize SPFN in your project.');
|
|
1993
2288
|
process.exit(1);
|
|
@@ -2001,23 +2296,25 @@ var devCommand = new Command4("dev").description("Start SPFN development server
|
|
|
2001
2296
|
logger.warn(`[SPFN] Could not resolve keychain secret(s): ${keychainMissing.join(", ")} \u2014 run \`spfn secret set <KEY>\``);
|
|
2002
2297
|
}
|
|
2003
2298
|
const serverEnv = { ...process.env, ...keychainEnv };
|
|
2004
|
-
const
|
|
2299
|
+
const { warnPendingMigrations: warnPendingMigrations2 } = await Promise.resolve().then(() => (init_migration_status(), migration_status_exports));
|
|
2300
|
+
await warnPendingMigrations2(cwd, serverEnv.DATABASE_URL);
|
|
2301
|
+
const packageJsonPath = join17(cwd, "package.json");
|
|
2005
2302
|
let hasNext = false;
|
|
2006
|
-
if (
|
|
2007
|
-
const packageJson = JSON.parse(
|
|
2303
|
+
if (existsSync16(packageJsonPath)) {
|
|
2304
|
+
const packageJson = JSON.parse(readFileSync8(packageJsonPath, "utf-8"));
|
|
2008
2305
|
hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
|
|
2009
2306
|
}
|
|
2010
|
-
const tempDir =
|
|
2011
|
-
const serverEntry =
|
|
2012
|
-
const watcherEntry =
|
|
2307
|
+
const tempDir = join17(cwd, ".spfn");
|
|
2308
|
+
const serverEntry = join17(tempDir, "server.mjs");
|
|
2309
|
+
const watcherEntry = join17(tempDir, "watcher.mjs");
|
|
2013
2310
|
mkdirSync(tempDir, { recursive: true });
|
|
2014
|
-
const serverBuildDir =
|
|
2015
|
-
if (
|
|
2311
|
+
const serverBuildDir = join17(tempDir, "server");
|
|
2312
|
+
if (existsSync16(serverBuildDir)) {
|
|
2016
2313
|
rmSync(serverBuildDir, { recursive: true, force: true });
|
|
2017
2314
|
logger.info("[SPFN] Cleaned stale build cache (.spfn/server)");
|
|
2018
2315
|
}
|
|
2019
|
-
const readySignal =
|
|
2020
|
-
if (
|
|
2316
|
+
const readySignal = join17(tempDir, "server-ready");
|
|
2317
|
+
if (existsSync16(readySignal)) {
|
|
2021
2318
|
unlinkSync(readySignal);
|
|
2022
2319
|
}
|
|
2023
2320
|
const configParts = [];
|
|
@@ -2025,7 +2322,7 @@ var devCommand = new Command4("dev").description("Start SPFN development server
|
|
|
2025
2322
|
if (options.host) configParts.push(`host: '${options.host}'`);
|
|
2026
2323
|
if (options.routes) configParts.push(`routesPath: '${options.routes}'`);
|
|
2027
2324
|
configParts.push("debug: true");
|
|
2028
|
-
const readyFile =
|
|
2325
|
+
const readyFile = join17(tempDir, "server-ready");
|
|
2029
2326
|
writeFileSync10(serverEntry, `
|
|
2030
2327
|
import { writeFileSync } from 'fs';
|
|
2031
2328
|
|
|
@@ -2315,11 +2612,11 @@ catch (error)
|
|
|
2315
2612
|
init_logger();
|
|
2316
2613
|
init_package_manager();
|
|
2317
2614
|
import { Command as Command5 } from "commander";
|
|
2318
|
-
import { existsSync as
|
|
2319
|
-
import { join as
|
|
2615
|
+
import { existsSync as existsSync17, writeFileSync as writeFileSync11, mkdirSync as mkdirSync2, readdirSync as readdirSync2 } from "fs";
|
|
2616
|
+
import { join as join18 } from "path";
|
|
2320
2617
|
import { execa as execa7 } from "execa";
|
|
2321
2618
|
import ora5 from "ora";
|
|
2322
|
-
import
|
|
2619
|
+
import chalk8 from "chalk";
|
|
2323
2620
|
import { build } from "tsup";
|
|
2324
2621
|
async function buildProject(options) {
|
|
2325
2622
|
if (!process.env.NODE_ENV) {
|
|
@@ -2330,17 +2627,17 @@ async function buildProject(options) {
|
|
|
2330
2627
|
}
|
|
2331
2628
|
const cwd = process.cwd();
|
|
2332
2629
|
const pm = detectPackageManager(cwd);
|
|
2333
|
-
const packageJsonPath =
|
|
2630
|
+
const packageJsonPath = join18(cwd, "package.json");
|
|
2334
2631
|
let hasNext = false;
|
|
2335
|
-
if (
|
|
2632
|
+
if (existsSync17(packageJsonPath)) {
|
|
2336
2633
|
const packageJson = JSON.parse(await import("fs").then(
|
|
2337
2634
|
(fs5) => fs5.promises.readFile(packageJsonPath, "utf-8")
|
|
2338
2635
|
));
|
|
2339
2636
|
hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
|
|
2340
2637
|
}
|
|
2341
|
-
const serverDir =
|
|
2342
|
-
const hasServer =
|
|
2343
|
-
console.log(
|
|
2638
|
+
const serverDir = join18(cwd, "src", "server");
|
|
2639
|
+
const hasServer = existsSync17(serverDir);
|
|
2640
|
+
console.log(chalk8.blue.bold("\n\u{1F3D7}\uFE0F Building SPFN project for production...\n"));
|
|
2344
2641
|
if (hasServer) {
|
|
2345
2642
|
const spinner = ora5("Generating API client...").start();
|
|
2346
2643
|
try {
|
|
@@ -2376,10 +2673,10 @@ async function buildProject(options) {
|
|
|
2376
2673
|
if (hasServer && !options.nextOnly) {
|
|
2377
2674
|
const spinner = ora5("Building SPFN server...").start();
|
|
2378
2675
|
try {
|
|
2379
|
-
const outputDir =
|
|
2676
|
+
const outputDir = join18(cwd, ".spfn", "server");
|
|
2380
2677
|
mkdirSync2(outputDir, { recursive: true });
|
|
2381
|
-
const serverDir2 =
|
|
2382
|
-
if (!
|
|
2678
|
+
const serverDir2 = join18(cwd, "src", "server");
|
|
2679
|
+
if (!existsSync17(serverDir2)) {
|
|
2383
2680
|
spinner.fail("SPFN server build failed");
|
|
2384
2681
|
logger.error("src/server/ directory not found");
|
|
2385
2682
|
logger.error('Please run "spfn init" to initialize the project.');
|
|
@@ -2407,7 +2704,7 @@ async function buildProject(options) {
|
|
|
2407
2704
|
onSuccess: async () => {
|
|
2408
2705
|
}
|
|
2409
2706
|
});
|
|
2410
|
-
const prodServerPath =
|
|
2707
|
+
const prodServerPath = join18(cwd, ".spfn", "prod-server.mjs");
|
|
2411
2708
|
const prodServerContent = `// Load environment variables FIRST (before any imports that depend on them)
|
|
2412
2709
|
// Use centralized environment loader for standard dotenv priority
|
|
2413
2710
|
const { env } = await import('@spfn/core/config');
|
|
@@ -2433,25 +2730,25 @@ await startServer({
|
|
|
2433
2730
|
`;
|
|
2434
2731
|
writeFileSync11(prodServerPath, prodServerContent);
|
|
2435
2732
|
spinner.succeed(`SPFN server build completed \u2192 .spfn/server`);
|
|
2436
|
-
const routesDir =
|
|
2437
|
-
if (
|
|
2733
|
+
const routesDir = join18(cwd, ".spfn", "server", "routes");
|
|
2734
|
+
if (existsSync17(routesDir)) {
|
|
2438
2735
|
console.log();
|
|
2439
|
-
console.log(
|
|
2736
|
+
console.log(chalk8.bold("Route (api)"));
|
|
2440
2737
|
try {
|
|
2441
|
-
const routes =
|
|
2738
|
+
const routes = readdirSync2(routesDir, { withFileTypes: true }).filter((dirent) => dirent.isDirectory()).map((dirent) => dirent.name).sort();
|
|
2442
2739
|
if (routes.length > 0) {
|
|
2443
2740
|
routes.forEach((route, index) => {
|
|
2444
2741
|
const isLast = index === routes.length - 1;
|
|
2445
2742
|
const prefix = isLast ? "\u2514" : "\u251C";
|
|
2446
2743
|
const routePath = `/api/${route}`;
|
|
2447
2744
|
if (route === "health") {
|
|
2448
|
-
console.log(`${prefix} ${
|
|
2745
|
+
console.log(`${prefix} ${chalk8.green("GET")} ${routePath}`);
|
|
2449
2746
|
} else {
|
|
2450
|
-
console.log(`${prefix} ${
|
|
2747
|
+
console.log(`${prefix} ${chalk8.cyan("*")} ${routePath}`);
|
|
2451
2748
|
}
|
|
2452
2749
|
});
|
|
2453
2750
|
console.log();
|
|
2454
|
-
console.log(
|
|
2751
|
+
console.log(chalk8.cyan("*") + " (Hono) multiple methods supported");
|
|
2455
2752
|
}
|
|
2456
2753
|
} catch (error) {
|
|
2457
2754
|
}
|
|
@@ -2459,9 +2756,9 @@ await startServer({
|
|
|
2459
2756
|
} catch (error) {
|
|
2460
2757
|
spinner.fail("SPFN server build failed");
|
|
2461
2758
|
if (error instanceof Error) {
|
|
2462
|
-
console.error("\n" +
|
|
2759
|
+
console.error("\n" + chalk8.red(error.message));
|
|
2463
2760
|
if (error.stack) {
|
|
2464
|
-
console.error(
|
|
2761
|
+
console.error(chalk8.dim("\n" + error.stack));
|
|
2465
2762
|
}
|
|
2466
2763
|
} else {
|
|
2467
2764
|
logger.error(String(error));
|
|
@@ -2473,13 +2770,13 @@ await startServer({
|
|
|
2473
2770
|
logger.error("No Next.js or SPFN server found in this project.");
|
|
2474
2771
|
process.exit(1);
|
|
2475
2772
|
}
|
|
2476
|
-
console.log("\n" +
|
|
2477
|
-
console.log(
|
|
2478
|
-
console.log(" " +
|
|
2479
|
-
console.log(` ${
|
|
2773
|
+
console.log("\n" + chalk8.green.bold("\u2713 Build completed successfully!\n"));
|
|
2774
|
+
console.log(chalk8.bold("Next steps:\n"));
|
|
2775
|
+
console.log(" " + chalk8.cyan("Start production server:"));
|
|
2776
|
+
console.log(` ${chalk8.cyan(getRunCommand(pm))} spfn:start ${chalk8.gray("# Start SPFN + Next.js")}
|
|
2480
2777
|
`);
|
|
2481
|
-
console.log(" " +
|
|
2482
|
-
console.log(` ${
|
|
2778
|
+
console.log(" " + chalk8.cyan("Or deploy with Docker:"));
|
|
2779
|
+
console.log(` ${chalk8.cyan("docker compose -f docker-compose.production.yml up --build -d")}
|
|
2483
2780
|
`);
|
|
2484
2781
|
}
|
|
2485
2782
|
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) => {
|
|
@@ -2490,25 +2787,25 @@ var buildCommand = new Command5("build").description("Build SPFN project for pro
|
|
|
2490
2787
|
init_logger();
|
|
2491
2788
|
init_package_manager();
|
|
2492
2789
|
import { Command as Command6 } from "commander";
|
|
2493
|
-
import { existsSync as
|
|
2494
|
-
import { join as
|
|
2790
|
+
import { existsSync as existsSync18, readFileSync as readFileSync9 } from "fs";
|
|
2791
|
+
import { join as join19 } from "path";
|
|
2495
2792
|
import { execa as execa8 } from "execa";
|
|
2496
|
-
import
|
|
2793
|
+
import chalk9 from "chalk";
|
|
2497
2794
|
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) => {
|
|
2498
2795
|
if (!process.env.NODE_ENV) {
|
|
2499
2796
|
process.env.NODE_ENV = "production";
|
|
2500
2797
|
}
|
|
2501
2798
|
const cwd = process.cwd();
|
|
2502
|
-
const packageJsonPath =
|
|
2799
|
+
const packageJsonPath = join19(cwd, "package.json");
|
|
2503
2800
|
let hasNext = false;
|
|
2504
|
-
if (
|
|
2505
|
-
const packageJson = JSON.parse(
|
|
2801
|
+
if (existsSync18(packageJsonPath)) {
|
|
2802
|
+
const packageJson = JSON.parse(readFileSync9(packageJsonPath, "utf-8"));
|
|
2506
2803
|
hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);
|
|
2507
2804
|
}
|
|
2508
|
-
const builtServerDir =
|
|
2509
|
-
const hasBuiltServer =
|
|
2510
|
-
const nextBuildDir =
|
|
2511
|
-
const hasNextBuild =
|
|
2805
|
+
const builtServerDir = join19(cwd, ".spfn", "server");
|
|
2806
|
+
const hasBuiltServer = existsSync18(builtServerDir);
|
|
2807
|
+
const nextBuildDir = join19(cwd, ".next");
|
|
2808
|
+
const hasNextBuild = existsSync18(nextBuildDir);
|
|
2512
2809
|
if (!options.nextOnly && !hasBuiltServer) {
|
|
2513
2810
|
logger.error('.spfn/server directory not found. Please run "spfn build" first.');
|
|
2514
2811
|
process.exit(1);
|
|
@@ -2518,8 +2815,8 @@ var startCommand = new Command6("start").description("Start SPFN production serv
|
|
|
2518
2815
|
process.exit(1);
|
|
2519
2816
|
}
|
|
2520
2817
|
const pm = detectPackageManager(cwd);
|
|
2521
|
-
const serverEntry =
|
|
2522
|
-
if (!
|
|
2818
|
+
const serverEntry = join19(cwd, ".spfn", "prod-server.mjs");
|
|
2819
|
+
if (!existsSync18(serverEntry)) {
|
|
2523
2820
|
logger.error('.spfn/prod-server.mjs not found. Please run "spfn build" first.');
|
|
2524
2821
|
process.exit(1);
|
|
2525
2822
|
}
|
|
@@ -2555,7 +2852,7 @@ var startCommand = new Command6("start").description("Start SPFN production serv
|
|
|
2555
2852
|
}
|
|
2556
2853
|
const nextCmd = "next start -H 0.0.0.0 -p 3790";
|
|
2557
2854
|
const serverCmd = `node "${serverEntry}"`;
|
|
2558
|
-
console.log(
|
|
2855
|
+
console.log(chalk9.blue.bold("\n\u{1F680} Starting SPFN production server...\n"));
|
|
2559
2856
|
logger.info("Next.js: http://0.0.0.0:3790");
|
|
2560
2857
|
logger.info(`SPFN API: http://${options.host}:${options.port}
|
|
2561
2858
|
`);
|
|
@@ -2582,13 +2879,13 @@ var startCommand = new Command6("start").description("Start SPFN production serv
|
|
|
2582
2879
|
// src/commands/codegen.ts
|
|
2583
2880
|
init_logger();
|
|
2584
2881
|
import { Command as Command7 } from "commander";
|
|
2585
|
-
import { existsSync as
|
|
2586
|
-
import { join as
|
|
2587
|
-
import
|
|
2882
|
+
import { existsSync as existsSync19, writeFileSync as writeFileSync12 } from "fs";
|
|
2883
|
+
import { join as join20 } from "path";
|
|
2884
|
+
import chalk10 from "chalk";
|
|
2588
2885
|
async function initCodegen(options) {
|
|
2589
2886
|
const cwd = process.cwd();
|
|
2590
|
-
const rcPath =
|
|
2591
|
-
if (
|
|
2887
|
+
const rcPath = join20(cwd, ".spfnrc.ts");
|
|
2888
|
+
if (existsSync19(rcPath)) {
|
|
2592
2889
|
logger.warn(".spfnrc.ts already exists");
|
|
2593
2890
|
logger.info("Edit manually to add custom generators");
|
|
2594
2891
|
process.exit(0);
|
|
@@ -2614,13 +2911,13 @@ export default defineConfig({
|
|
|
2614
2911
|
});
|
|
2615
2912
|
`;
|
|
2616
2913
|
writeFileSync12(rcPath, content);
|
|
2617
|
-
console.log("\n" +
|
|
2618
|
-
console.log(
|
|
2914
|
+
console.log("\n" + chalk10.green.bold("\u2713 Created .spfnrc.ts\n"));
|
|
2915
|
+
console.log(chalk10.gray("Configured the @spfn/core:route-map generator."));
|
|
2619
2916
|
if (options.withExample) {
|
|
2620
|
-
console.log("\n" +
|
|
2917
|
+
console.log("\n" + chalk10.yellow("\u{1F4DD} To add custom generators:"));
|
|
2621
2918
|
console.log(" 1. Create your generator file (e.g., src/generators/my-generator.ts)");
|
|
2622
2919
|
console.log(" 2. Add to .spfnrc.ts:");
|
|
2623
|
-
console.log(
|
|
2920
|
+
console.log(chalk10.gray(`
|
|
2624
2921
|
{
|
|
2625
2922
|
"codegen": {
|
|
2626
2923
|
"generators": [
|
|
@@ -2630,11 +2927,11 @@ export default defineConfig({
|
|
|
2630
2927
|
}
|
|
2631
2928
|
}
|
|
2632
2929
|
`));
|
|
2633
|
-
console.log(" 3. Run: " +
|
|
2930
|
+
console.log(" 3. Run: " + chalk10.cyan("spfn dev") + " (generators run automatically)");
|
|
2634
2931
|
} else {
|
|
2635
|
-
console.log("\n" +
|
|
2932
|
+
console.log("\n" + chalk10.yellow("\u{1F4DD} Next steps:"));
|
|
2636
2933
|
console.log(" \u2022 Add custom generators to .spfnrc.ts");
|
|
2637
|
-
console.log(" \u2022 Run: " +
|
|
2934
|
+
console.log(" \u2022 Run: " + chalk10.cyan("spfn dev") + " to start development with code generation");
|
|
2638
2935
|
}
|
|
2639
2936
|
}
|
|
2640
2937
|
async function listGenerators() {
|
|
@@ -2647,10 +2944,10 @@ async function listGenerators() {
|
|
|
2647
2944
|
logger.info('Run "spfn codegen init" to initialize configuration');
|
|
2648
2945
|
return;
|
|
2649
2946
|
}
|
|
2650
|
-
console.log("\n" +
|
|
2947
|
+
console.log("\n" + chalk10.bold("Registered Generators:"));
|
|
2651
2948
|
generators.forEach((gen, index) => {
|
|
2652
|
-
console.log(` ${index + 1}. ${
|
|
2653
|
-
console.log(` Patterns: ${
|
|
2949
|
+
console.log(` ${index + 1}. ${chalk10.cyan(gen.name)}`);
|
|
2950
|
+
console.log(` Patterns: ${chalk10.gray(gen.watchPatterns.join(", "))}`);
|
|
2654
2951
|
});
|
|
2655
2952
|
console.log("");
|
|
2656
2953
|
}
|
|
@@ -2671,7 +2968,7 @@ async function runGenerators() {
|
|
|
2671
2968
|
debug: true
|
|
2672
2969
|
});
|
|
2673
2970
|
await orchestrator.generateAll();
|
|
2674
|
-
console.log("\n" +
|
|
2971
|
+
console.log("\n" + chalk10.green.bold("\u2713 Code generation completed"));
|
|
2675
2972
|
}
|
|
2676
2973
|
var codegenCommand = new Command7("codegen").description("Code generation management");
|
|
2677
2974
|
codegenCommand.command("init").description("Initialize .spfnrc.ts with codegen configuration").option("--with-example", "Show example custom generator usage").action(initCodegen);
|
|
@@ -2682,19 +2979,19 @@ codegenCommand.command("run").description("Run code generators once (no watch mo
|
|
|
2682
2979
|
init_logger();
|
|
2683
2980
|
import { Command as Command8 } from "commander";
|
|
2684
2981
|
import { execSync } from "child_process";
|
|
2685
|
-
import
|
|
2982
|
+
import chalk11 from "chalk";
|
|
2686
2983
|
|
|
2687
2984
|
// src/utils/secret-gen.ts
|
|
2688
|
-
import { randomBytes, randomUUID } from "crypto";
|
|
2985
|
+
import { randomBytes as randomBytes2, randomUUID } from "crypto";
|
|
2689
2986
|
function randomBase64Url(bytes) {
|
|
2690
|
-
return
|
|
2987
|
+
return randomBytes2(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
2691
2988
|
}
|
|
2692
2989
|
function generateSecretValue(spec) {
|
|
2693
2990
|
switch (spec) {
|
|
2694
2991
|
case "hex32":
|
|
2695
|
-
return
|
|
2992
|
+
return randomBytes2(32).toString("hex");
|
|
2696
2993
|
case "hex64":
|
|
2697
|
-
return
|
|
2994
|
+
return randomBytes2(64).toString("hex");
|
|
2698
2995
|
case "base64url32":
|
|
2699
2996
|
return randomBase64Url(32);
|
|
2700
2997
|
case "uuid":
|
|
@@ -2757,41 +3054,41 @@ function copyToClipboard(text) {
|
|
|
2757
3054
|
function generateSecret(bytes, preset, envVarName, copy) {
|
|
2758
3055
|
const key = randomBase64Url(bytes);
|
|
2759
3056
|
const config = preset ? PRESETS[preset] : null;
|
|
2760
|
-
console.log("\n" +
|
|
3057
|
+
console.log("\n" + chalk11.green.bold("\u2713 Generated secret key:"));
|
|
2761
3058
|
if (config) {
|
|
2762
|
-
console.log(
|
|
3059
|
+
console.log(chalk11.dim(` ${config.description} (${bytes * 8} bits)`));
|
|
2763
3060
|
} else {
|
|
2764
|
-
console.log(
|
|
3061
|
+
console.log(chalk11.dim(` ${bytes * 8}-bit secret`));
|
|
2765
3062
|
}
|
|
2766
|
-
console.log("\n" +
|
|
3063
|
+
console.log("\n" + chalk11.cyan(key) + "\n");
|
|
2767
3064
|
const varName = envVarName || config?.envVar || "SECRET_KEY";
|
|
2768
|
-
console.log(
|
|
2769
|
-
console.log(
|
|
3065
|
+
console.log(chalk11.dim("Add to your .env file:"));
|
|
3066
|
+
console.log(chalk11.yellow(`${varName}=${key}
|
|
2770
3067
|
`));
|
|
2771
3068
|
if (config?.usage) {
|
|
2772
|
-
console.log(
|
|
2773
|
-
console.log(
|
|
3069
|
+
console.log(chalk11.dim("Usage:"));
|
|
3070
|
+
console.log(chalk11.gray(` ${config.usage}
|
|
2774
3071
|
`));
|
|
2775
3072
|
}
|
|
2776
3073
|
if (copy) {
|
|
2777
3074
|
if (copyToClipboard(key)) {
|
|
2778
|
-
console.log(
|
|
3075
|
+
console.log(chalk11.green("\u2713 Copied to clipboard!\n"));
|
|
2779
3076
|
} else {
|
|
2780
3077
|
logger.warn("Could not copy to clipboard");
|
|
2781
3078
|
}
|
|
2782
3079
|
}
|
|
2783
3080
|
}
|
|
2784
3081
|
function listPresets() {
|
|
2785
|
-
console.log("\n" +
|
|
3082
|
+
console.log("\n" + chalk11.bold("Available presets:"));
|
|
2786
3083
|
console.log();
|
|
2787
3084
|
Object.entries(PRESETS).forEach(([name, config]) => {
|
|
2788
|
-
console.log(` ${
|
|
2789
|
-
console.log(` ${" ".repeat(20)} ${
|
|
3085
|
+
console.log(` ${chalk11.cyan(name.padEnd(20))} ${chalk11.dim(config.description)}`);
|
|
3086
|
+
console.log(` ${" ".repeat(20)} ${chalk11.gray(`\u2192 ${config.envVar} (${config.bytes * 8} bits)`)}`);
|
|
2790
3087
|
console.log();
|
|
2791
3088
|
});
|
|
2792
|
-
console.log(
|
|
2793
|
-
console.log(
|
|
2794
|
-
console.log(
|
|
3089
|
+
console.log(chalk11.dim("Usage:"));
|
|
3090
|
+
console.log(chalk11.gray(" spfn key <preset>"));
|
|
3091
|
+
console.log(chalk11.gray(" spfn key auth-encryption --copy"));
|
|
2795
3092
|
console.log();
|
|
2796
3093
|
}
|
|
2797
3094
|
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) => {
|
|
@@ -2804,9 +3101,9 @@ var generateValueCommand = new Command8("generate").alias("gen").description("Ge
|
|
|
2804
3101
|
console.log(value);
|
|
2805
3102
|
if (options.copy) {
|
|
2806
3103
|
if (copyToClipboard(value)) {
|
|
2807
|
-
console.error(
|
|
3104
|
+
console.error(chalk11.green("\u2713 Copied to clipboard"));
|
|
2808
3105
|
} else {
|
|
2809
|
-
console.error(
|
|
3106
|
+
console.error(chalk11.yellow("\u26A0 Could not copy to clipboard"));
|
|
2810
3107
|
}
|
|
2811
3108
|
}
|
|
2812
3109
|
});
|
|
@@ -2824,9 +3121,9 @@ var keyCommand = new Command8("key").alias("k").description("Generate secure ran
|
|
|
2824
3121
|
logger.error(`Unknown preset: ${preset}`);
|
|
2825
3122
|
console.log("\nAvailable presets:");
|
|
2826
3123
|
Object.entries(PRESETS).forEach(([name, config]) => {
|
|
2827
|
-
console.log(` ${
|
|
3124
|
+
console.log(` ${chalk11.cyan(name)}: ${config.description}`);
|
|
2828
3125
|
});
|
|
2829
|
-
console.log("\nUse " +
|
|
3126
|
+
console.log("\nUse " + chalk11.cyan("--list") + " to see detailed information");
|
|
2830
3127
|
process.exit(1);
|
|
2831
3128
|
}
|
|
2832
3129
|
generateSecret(
|
|
@@ -2845,16 +3142,16 @@ init_setup();
|
|
|
2845
3142
|
import { Command as Command9 } from "commander";
|
|
2846
3143
|
|
|
2847
3144
|
// src/commands/db/generate.ts
|
|
2848
|
-
import
|
|
3145
|
+
import chalk13 from "chalk";
|
|
2849
3146
|
|
|
2850
3147
|
// src/commands/db/utils/drizzle.ts
|
|
2851
|
-
import { existsSync as
|
|
3148
|
+
import { existsSync as existsSync20, writeFileSync as writeFileSync13, unlinkSync as unlinkSync2 } from "fs";
|
|
2852
3149
|
import { spawn } from "child_process";
|
|
2853
3150
|
import { pathToFileURL } from "url";
|
|
2854
|
-
import
|
|
3151
|
+
import chalk12 from "chalk";
|
|
2855
3152
|
import ora6 from "ora";
|
|
2856
|
-
import { env } from "@spfn/core/config";
|
|
2857
|
-
import { loadEnv } from "@spfn/core/server";
|
|
3153
|
+
import { env as env3 } from "@spfn/core/config";
|
|
3154
|
+
import { loadEnv as loadEnv3 } from "@spfn/core/server";
|
|
2858
3155
|
var TLS_SSL_MODES = /* @__PURE__ */ new Set(["no-verify", "prefer", "require", "verify-ca", "verify-full"]);
|
|
2859
3156
|
function parseDatabaseUrl(databaseUrl) {
|
|
2860
3157
|
if (!databaseUrl) {
|
|
@@ -2907,22 +3204,22 @@ function resolvePushConnectionConfig(databaseUrl) {
|
|
|
2907
3204
|
return { connectionString: parsedUrl.toString() };
|
|
2908
3205
|
}
|
|
2909
3206
|
function validateDatabasePrerequisites() {
|
|
2910
|
-
|
|
2911
|
-
if (!
|
|
2912
|
-
console.error(
|
|
2913
|
-
console.log(
|
|
3207
|
+
loadEnv3();
|
|
3208
|
+
if (!env3.DATABASE_URL) {
|
|
3209
|
+
console.error(chalk12.red("\u274C DATABASE_URL not found in environment"));
|
|
3210
|
+
console.log(chalk12.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
2914
3211
|
throw new Error("DATABASE_URL is required for database operations");
|
|
2915
3212
|
}
|
|
2916
3213
|
}
|
|
2917
3214
|
async function runDrizzleCommand(command) {
|
|
2918
|
-
const hasUserConfig =
|
|
3215
|
+
const hasUserConfig = existsSync20("./drizzle.config.ts");
|
|
2919
3216
|
const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
|
|
2920
3217
|
const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
|
|
2921
3218
|
if (!hasUserConfig) {
|
|
2922
|
-
|
|
2923
|
-
if (!
|
|
2924
|
-
console.error(
|
|
2925
|
-
console.log(
|
|
3219
|
+
loadEnv3();
|
|
3220
|
+
if (!env3.DATABASE_URL) {
|
|
3221
|
+
console.error(chalk12.red("\u274C DATABASE_URL not found in environment"));
|
|
3222
|
+
console.log(chalk12.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
2926
3223
|
process.exit(1);
|
|
2927
3224
|
}
|
|
2928
3225
|
const { generateDrizzleConfigFile } = await import("@spfn/core/db");
|
|
@@ -2935,7 +3232,7 @@ async function runDrizzleCommand(command) {
|
|
|
2935
3232
|
autoDetectSchemas: true
|
|
2936
3233
|
});
|
|
2937
3234
|
writeFileSync13(tempConfigPath, configContent);
|
|
2938
|
-
console.log(
|
|
3235
|
+
console.log(chalk12.dim("Using auto-generated Drizzle config\n"));
|
|
2939
3236
|
}
|
|
2940
3237
|
const args = command.split(" ");
|
|
2941
3238
|
args.push(`--config=${configPath}`);
|
|
@@ -2944,10 +3241,10 @@ async function runDrizzleCommand(command) {
|
|
|
2944
3241
|
stdio: "inherit",
|
|
2945
3242
|
// Allow interactive input
|
|
2946
3243
|
shell: true,
|
|
2947
|
-
env: shouldRelaxDbTls(
|
|
3244
|
+
env: shouldRelaxDbTls(env3.DATABASE_URL) ? { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" } : { ...process.env }
|
|
2948
3245
|
});
|
|
2949
3246
|
const cleanup = () => {
|
|
2950
|
-
if (!hasUserConfig &&
|
|
3247
|
+
if (!hasUserConfig && existsSync20(tempConfigPath)) {
|
|
2951
3248
|
unlinkSync2(tempConfigPath);
|
|
2952
3249
|
}
|
|
2953
3250
|
};
|
|
@@ -2970,10 +3267,10 @@ async function runWithSpinner(spinnerText, command, successMessage, failMessage)
|
|
|
2970
3267
|
try {
|
|
2971
3268
|
spinner.stop();
|
|
2972
3269
|
await runDrizzleCommand(command);
|
|
2973
|
-
console.log(
|
|
3270
|
+
console.log(chalk12.green(`\u2705 ${successMessage}`));
|
|
2974
3271
|
} catch (error) {
|
|
2975
3272
|
spinner.fail(failMessage);
|
|
2976
|
-
console.error(
|
|
3273
|
+
console.error(chalk12.red(error instanceof Error ? error.message : "Unknown error"));
|
|
2977
3274
|
process.exit(1);
|
|
2978
3275
|
}
|
|
2979
3276
|
}
|
|
@@ -2997,13 +3294,13 @@ async function loadSchemaImports(schemaFiles) {
|
|
|
2997
3294
|
return imports;
|
|
2998
3295
|
}
|
|
2999
3296
|
async function createPushConnection() {
|
|
3000
|
-
|
|
3001
|
-
if (!
|
|
3297
|
+
loadEnv3();
|
|
3298
|
+
if (!env3.DATABASE_URL) {
|
|
3002
3299
|
throw new Error("DATABASE_URL is required");
|
|
3003
3300
|
}
|
|
3004
3301
|
const pg = await import("pg");
|
|
3005
3302
|
const { drizzle } = await import("drizzle-orm/node-postgres");
|
|
3006
|
-
const connectionConfig = resolvePushConnectionConfig(
|
|
3303
|
+
const connectionConfig = resolvePushConnectionConfig(env3.DATABASE_URL);
|
|
3007
3304
|
const pool = new pg.default.Pool({
|
|
3008
3305
|
...connectionConfig,
|
|
3009
3306
|
max: 1
|
|
@@ -3019,19 +3316,19 @@ async function createPushConnection() {
|
|
|
3019
3316
|
async function dbGenerate() {
|
|
3020
3317
|
try {
|
|
3021
3318
|
await runDrizzleCommand("generate");
|
|
3022
|
-
console.log(
|
|
3319
|
+
console.log(chalk13.green("\n\u2705 Migrations generated successfully"));
|
|
3023
3320
|
} catch (error) {
|
|
3024
|
-
console.error(
|
|
3025
|
-
console.error(
|
|
3321
|
+
console.error(chalk13.red("\n\u274C Failed to generate migrations"));
|
|
3322
|
+
console.error(chalk13.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3026
3323
|
process.exit(1);
|
|
3027
3324
|
}
|
|
3028
3325
|
}
|
|
3029
3326
|
|
|
3030
3327
|
// src/commands/db/push.ts
|
|
3031
|
-
import
|
|
3032
|
-
import
|
|
3328
|
+
import chalk15 from "chalk";
|
|
3329
|
+
import prompts4 from "prompts";
|
|
3033
3330
|
import "@spfn/core/config";
|
|
3034
|
-
import { loadEnv as
|
|
3331
|
+
import { loadEnv as loadEnv4 } from "@spfn/core/server";
|
|
3035
3332
|
import { sql } from "drizzle-orm";
|
|
3036
3333
|
import { getTableConfig } from "drizzle-orm/pg-core";
|
|
3037
3334
|
|
|
@@ -3075,7 +3372,7 @@ function classifyStatements(statements) {
|
|
|
3075
3372
|
}
|
|
3076
3373
|
|
|
3077
3374
|
// src/commands/db/utils/push-display.ts
|
|
3078
|
-
import
|
|
3375
|
+
import chalk14 from "chalk";
|
|
3079
3376
|
function formatSql(sql2) {
|
|
3080
3377
|
return sql2.replace(/\s+/g, " ").trim();
|
|
3081
3378
|
}
|
|
@@ -3083,30 +3380,30 @@ function printStatements(statements, color) {
|
|
|
3083
3380
|
for (const stmt of statements) {
|
|
3084
3381
|
console.log(color(` ${formatSql(stmt.sql)}`));
|
|
3085
3382
|
if (stmt.reason) {
|
|
3086
|
-
console.log(
|
|
3383
|
+
console.log(chalk14.dim(` \u2192 ${stmt.reason}`));
|
|
3087
3384
|
}
|
|
3088
3385
|
}
|
|
3089
3386
|
}
|
|
3090
3387
|
function displayClassifiedStatements(result) {
|
|
3091
3388
|
if (result.safe.length > 0) {
|
|
3092
|
-
console.log(
|
|
3389
|
+
console.log(chalk14.green(`
|
|
3093
3390
|
\u2705 Safe changes (${result.safe.length}):`));
|
|
3094
|
-
printStatements(result.safe,
|
|
3391
|
+
printStatements(result.safe, chalk14.green);
|
|
3095
3392
|
}
|
|
3096
3393
|
if (result.warning.length > 0) {
|
|
3097
|
-
console.log(
|
|
3394
|
+
console.log(chalk14.yellow(`
|
|
3098
3395
|
\u26A0\uFE0F Warnings (${result.warning.length}):`));
|
|
3099
|
-
printStatements(result.warning,
|
|
3396
|
+
printStatements(result.warning, chalk14.yellow);
|
|
3100
3397
|
}
|
|
3101
3398
|
if (result.destructive.length > 0) {
|
|
3102
|
-
console.log(
|
|
3399
|
+
console.log(chalk14.red(`
|
|
3103
3400
|
\u274C Destructive changes (${result.destructive.length}):`));
|
|
3104
|
-
printStatements(result.destructive,
|
|
3401
|
+
printStatements(result.destructive, chalk14.red);
|
|
3105
3402
|
}
|
|
3106
3403
|
}
|
|
3107
3404
|
function displayDryRunSummary(result) {
|
|
3108
3405
|
const total = result.safe.length + result.warning.length + result.destructive.length;
|
|
3109
|
-
console.log(
|
|
3406
|
+
console.log(chalk14.cyan(`
|
|
3110
3407
|
\u{1F50D} Dry-run: ${total} statement(s) detected
|
|
3111
3408
|
`));
|
|
3112
3409
|
displayClassifiedStatements(result);
|
|
@@ -3114,11 +3411,11 @@ function displayDryRunSummary(result) {
|
|
|
3114
3411
|
}
|
|
3115
3412
|
function displayApplySummary(applied, skipped) {
|
|
3116
3413
|
if (applied > 0) {
|
|
3117
|
-
console.log(
|
|
3414
|
+
console.log(chalk14.green(`
|
|
3118
3415
|
\u2705 Applied ${applied} statement(s)`));
|
|
3119
3416
|
}
|
|
3120
3417
|
if (skipped > 0) {
|
|
3121
|
-
console.log(
|
|
3418
|
+
console.log(chalk14.yellow(`\u23ED\uFE0F Skipped ${skipped} destructive statement(s)`));
|
|
3122
3419
|
}
|
|
3123
3420
|
console.log("");
|
|
3124
3421
|
}
|
|
@@ -3153,7 +3450,7 @@ async function applyStatements(db, statements) {
|
|
|
3153
3450
|
}
|
|
3154
3451
|
async function dbPush(options = {}) {
|
|
3155
3452
|
validateDatabasePrerequisites();
|
|
3156
|
-
|
|
3453
|
+
loadEnv4();
|
|
3157
3454
|
const { getDrizzleConfig } = await import("@spfn/core/db");
|
|
3158
3455
|
const config = getDrizzleConfig({
|
|
3159
3456
|
cwd: process.cwd(),
|
|
@@ -3163,10 +3460,10 @@ async function dbPush(options = {}) {
|
|
|
3163
3460
|
});
|
|
3164
3461
|
const schemaFiles = Array.isArray(config.schema) ? config.schema : [config.schema];
|
|
3165
3462
|
if (schemaFiles.length === 0) {
|
|
3166
|
-
console.log(
|
|
3463
|
+
console.log(chalk15.yellow("No schema files found."));
|
|
3167
3464
|
return;
|
|
3168
3465
|
}
|
|
3169
|
-
console.log(
|
|
3466
|
+
console.log(chalk15.dim(`Found ${schemaFiles.length} schema file(s)
|
|
3170
3467
|
`));
|
|
3171
3468
|
const imports = await loadSchemaImports(schemaFiles);
|
|
3172
3469
|
const detectedSchemas = new Set(config.schemaFilter ?? ["public"]);
|
|
@@ -3185,10 +3482,10 @@ async function dbPush(options = {}) {
|
|
|
3185
3482
|
try {
|
|
3186
3483
|
const { statements, hints } = await resolvePushPlan(imports, db, schemaFilter);
|
|
3187
3484
|
for (const hint of hints) {
|
|
3188
|
-
console.log(
|
|
3485
|
+
console.log(chalk15.yellow(`\u26A0\uFE0F ${hint.hint}`));
|
|
3189
3486
|
}
|
|
3190
3487
|
if (statements.length === 0) {
|
|
3191
|
-
console.log(
|
|
3488
|
+
console.log(chalk15.green("\u2705 No changes detected \u2014 database is up to date\n"));
|
|
3192
3489
|
await applyFunctionMigrations(functionPlans);
|
|
3193
3490
|
return;
|
|
3194
3491
|
}
|
|
@@ -3199,7 +3496,7 @@ async function dbPush(options = {}) {
|
|
|
3199
3496
|
}
|
|
3200
3497
|
displayClassifiedStatements(result);
|
|
3201
3498
|
if (options.force) {
|
|
3202
|
-
console.log(
|
|
3499
|
+
console.log(chalk15.dim("\n--force: applying all changes..."));
|
|
3203
3500
|
await applyStatements(db, statements);
|
|
3204
3501
|
displayApplySummary(statements.length, 0);
|
|
3205
3502
|
} else if (result.destructive.length === 0) {
|
|
@@ -3208,13 +3505,13 @@ async function dbPush(options = {}) {
|
|
|
3208
3505
|
} else {
|
|
3209
3506
|
const destructiveSet = new Set(result.destructive.map((s) => s.sql));
|
|
3210
3507
|
const nonDestructive = statements.filter((s) => !destructiveSet.has(s));
|
|
3211
|
-
console.log(
|
|
3508
|
+
console.log(chalk15.red(`
|
|
3212
3509
|
\u274C ${result.destructive.length} destructive change(s) require confirmation:`));
|
|
3213
3510
|
for (const stmt of result.destructive) {
|
|
3214
|
-
console.log(
|
|
3215
|
-
console.log(
|
|
3511
|
+
console.log(chalk15.red(` ${stmt.sql.replace(/\s+/g, " ").trim()}`));
|
|
3512
|
+
console.log(chalk15.dim(` \u2192 ${stmt.reason}`));
|
|
3216
3513
|
}
|
|
3217
|
-
const { confirm } = await
|
|
3514
|
+
const { confirm } = await prompts4({
|
|
3218
3515
|
type: "confirm",
|
|
3219
3516
|
name: "confirm",
|
|
3220
3517
|
message: "Apply destructive changes?",
|
|
@@ -3228,7 +3525,7 @@ async function dbPush(options = {}) {
|
|
|
3228
3525
|
await applyStatements(db, nonDestructive);
|
|
3229
3526
|
}
|
|
3230
3527
|
displayApplySummary(nonDestructive.length, result.destructive.length);
|
|
3231
|
-
console.log(
|
|
3528
|
+
console.log(chalk15.dim("Tip: Use --force to apply all changes without prompting.\n"));
|
|
3232
3529
|
}
|
|
3233
3530
|
}
|
|
3234
3531
|
await applyFunctionMigrations(functionPlans);
|
|
@@ -3241,8 +3538,8 @@ function loadFunctionPlansOrExit() {
|
|
|
3241
3538
|
try {
|
|
3242
3539
|
return loadFunctionMigrationPlans(functions);
|
|
3243
3540
|
} catch (error) {
|
|
3244
|
-
console.error(
|
|
3245
|
-
console.error(
|
|
3541
|
+
console.error(chalk15.red("\n\u274C Invalid function package migrations \u2014 nothing was applied"));
|
|
3542
|
+
console.error(chalk15.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3246
3543
|
process.exit(1);
|
|
3247
3544
|
}
|
|
3248
3545
|
}
|
|
@@ -3250,37 +3547,37 @@ async function applyFunctionMigrations(plans) {
|
|
|
3250
3547
|
if (plans.length === 0) {
|
|
3251
3548
|
return;
|
|
3252
3549
|
}
|
|
3253
|
-
console.log(
|
|
3550
|
+
console.log(chalk15.blue("\n\u{1F4E6} Applying function package migrations:"));
|
|
3254
3551
|
plans.forEach((plan) => {
|
|
3255
|
-
console.log(
|
|
3552
|
+
console.log(chalk15.dim(` - ${plan.packageName}`));
|
|
3256
3553
|
});
|
|
3257
3554
|
try {
|
|
3258
3555
|
await executeFunctionMigrations(plans);
|
|
3259
|
-
console.log(
|
|
3556
|
+
console.log(chalk15.green("\n\u2705 All function migrations applied\n"));
|
|
3260
3557
|
} catch (error) {
|
|
3261
|
-
console.error(
|
|
3262
|
-
console.error(
|
|
3263
|
-
console.error(
|
|
3558
|
+
console.error(chalk15.red("\n\u274C Failed to apply function package migrations"));
|
|
3559
|
+
console.error(chalk15.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3560
|
+
console.error(chalk15.yellow("Project schema changes (if any) were already applied \u2014 only function package migrations failed."));
|
|
3264
3561
|
process.exit(1);
|
|
3265
3562
|
}
|
|
3266
3563
|
}
|
|
3267
3564
|
|
|
3268
3565
|
// src/commands/db/migrate.ts
|
|
3269
|
-
import
|
|
3270
|
-
import { join as
|
|
3271
|
-
import { existsSync as
|
|
3566
|
+
import chalk20 from "chalk";
|
|
3567
|
+
import { join as join21 } from "path";
|
|
3568
|
+
import { existsSync as existsSync22 } from "fs";
|
|
3272
3569
|
|
|
3273
3570
|
// src/commands/db/backup.ts
|
|
3274
3571
|
import { promises as fs3 } from "fs";
|
|
3275
3572
|
import path3 from "path";
|
|
3276
3573
|
import { spawn as spawn2 } from "child_process";
|
|
3277
|
-
import
|
|
3574
|
+
import chalk19 from "chalk";
|
|
3278
3575
|
import ora7 from "ora";
|
|
3279
3576
|
|
|
3280
3577
|
// src/commands/db/utils/database.ts
|
|
3281
3578
|
import net from "net";
|
|
3282
|
-
import
|
|
3283
|
-
import
|
|
3579
|
+
import chalk16 from "chalk";
|
|
3580
|
+
import prompts5 from "prompts";
|
|
3284
3581
|
function parseDatabaseUrl2(dbUrl) {
|
|
3285
3582
|
try {
|
|
3286
3583
|
const url = new URL(dbUrl);
|
|
@@ -3304,13 +3601,13 @@ async function confirmDangerousTarget(dbInfo) {
|
|
|
3304
3601
|
if (!isRemoteHost(dbInfo.host) && process.env.NODE_ENV !== "production") {
|
|
3305
3602
|
return;
|
|
3306
3603
|
}
|
|
3307
|
-
const { typed } = await
|
|
3604
|
+
const { typed } = await prompts5({
|
|
3308
3605
|
type: "text",
|
|
3309
3606
|
name: "typed",
|
|
3310
3607
|
message: `Remote/production database detected. Type the database name "${dbInfo.database}" to continue:`
|
|
3311
3608
|
});
|
|
3312
3609
|
if (typed !== dbInfo.database) {
|
|
3313
|
-
console.log(
|
|
3610
|
+
console.log(chalk16.gray("Cancelled (database name mismatch)."));
|
|
3314
3611
|
process.exit(0);
|
|
3315
3612
|
}
|
|
3316
3613
|
}
|
|
@@ -3361,16 +3658,16 @@ function formatTimestamp() {
|
|
|
3361
3658
|
|
|
3362
3659
|
// src/commands/db/utils/backup-files.ts
|
|
3363
3660
|
import { promises as fs2 } from "fs";
|
|
3364
|
-
import { existsSync as
|
|
3661
|
+
import { existsSync as existsSync21 } from "fs";
|
|
3365
3662
|
import path2 from "path";
|
|
3366
|
-
import
|
|
3663
|
+
import chalk18 from "chalk";
|
|
3367
3664
|
|
|
3368
3665
|
// src/commands/db/utils/metadata.ts
|
|
3369
3666
|
import { promises as fs } from "fs";
|
|
3370
3667
|
import path from "path";
|
|
3371
3668
|
import { promisify } from "util";
|
|
3372
3669
|
import { exec } from "child_process";
|
|
3373
|
-
import
|
|
3670
|
+
import chalk17 from "chalk";
|
|
3374
3671
|
var execAsync = promisify(exec);
|
|
3375
3672
|
async function collectGitInfo() {
|
|
3376
3673
|
try {
|
|
@@ -3433,7 +3730,7 @@ async function collectMigrationInfo(dbUrl) {
|
|
|
3433
3730
|
await pool.end();
|
|
3434
3731
|
}
|
|
3435
3732
|
} catch (error) {
|
|
3436
|
-
console.log(
|
|
3733
|
+
console.log(chalk17.dim("\u26A0\uFE0F Could not fetch migration info"));
|
|
3437
3734
|
return void 0;
|
|
3438
3735
|
}
|
|
3439
3736
|
}
|
|
@@ -3441,9 +3738,9 @@ async function saveBackupMetadata(metadata, backupFilename) {
|
|
|
3441
3738
|
const metadataPath = backupFilename.replace(/\.(sql|dump)$/, ".meta.json");
|
|
3442
3739
|
try {
|
|
3443
3740
|
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
3444
|
-
console.log(
|
|
3741
|
+
console.log(chalk17.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
|
|
3445
3742
|
} catch (error) {
|
|
3446
|
-
console.log(
|
|
3743
|
+
console.log(chalk17.dim("\u26A0\uFE0F Could not save metadata"));
|
|
3447
3744
|
}
|
|
3448
3745
|
}
|
|
3449
3746
|
async function loadBackupMetadata(backupFilename) {
|
|
@@ -3461,7 +3758,7 @@ async function ensureBackupInGitignore() {
|
|
|
3461
3758
|
const gitignorePath = path2.join(process.cwd(), ".gitignore");
|
|
3462
3759
|
try {
|
|
3463
3760
|
let content = "";
|
|
3464
|
-
let exists =
|
|
3761
|
+
let exists = existsSync21(gitignorePath);
|
|
3465
3762
|
if (exists) {
|
|
3466
3763
|
content = await fs2.readFile(gitignorePath, "utf-8");
|
|
3467
3764
|
}
|
|
@@ -3472,10 +3769,10 @@ async function ensureBackupInGitignore() {
|
|
|
3472
3769
|
if (!hasBackupsIgnore) {
|
|
3473
3770
|
const entry = exists && content && !content.endsWith("\n") ? "\n\n# Database backups\nbackups/\n" : "# Database backups\nbackups/\n";
|
|
3474
3771
|
await fs2.appendFile(gitignorePath, entry);
|
|
3475
|
-
console.log(
|
|
3772
|
+
console.log(chalk18.dim("\u2713 Added backups/ to .gitignore"));
|
|
3476
3773
|
}
|
|
3477
3774
|
} catch (error) {
|
|
3478
|
-
console.log(
|
|
3775
|
+
console.log(chalk18.dim("\u26A0\uFE0F Could not update .gitignore"));
|
|
3479
3776
|
}
|
|
3480
3777
|
}
|
|
3481
3778
|
async function ensureBackupDir() {
|
|
@@ -3483,7 +3780,7 @@ async function ensureBackupDir() {
|
|
|
3483
3780
|
try {
|
|
3484
3781
|
await fs2.mkdir(backupDir, { recursive: true });
|
|
3485
3782
|
const gitignorePath = path2.join(backupDir, ".gitignore");
|
|
3486
|
-
const gitignoreExists =
|
|
3783
|
+
const gitignoreExists = existsSync21(gitignorePath);
|
|
3487
3784
|
if (!gitignoreExists) {
|
|
3488
3785
|
await fs2.writeFile(gitignorePath, "# Ignore all backup files\n*.sql\n*.dump\n*.meta.json\n");
|
|
3489
3786
|
}
|
|
@@ -3522,15 +3819,15 @@ async function listBackupFiles() {
|
|
|
3522
3819
|
}
|
|
3523
3820
|
|
|
3524
3821
|
// src/commands/db/backup.ts
|
|
3525
|
-
import { env as
|
|
3526
|
-
import { loadEnv as
|
|
3822
|
+
import { env as env4 } from "@spfn/core/config";
|
|
3823
|
+
import { loadEnv as loadEnv5 } from "@spfn/core/server";
|
|
3527
3824
|
async function dbBackup(options) {
|
|
3528
|
-
console.log(
|
|
3529
|
-
|
|
3530
|
-
const dbUrl =
|
|
3825
|
+
console.log(chalk19.blue("\u{1F4BE} Creating database backup...\n"));
|
|
3826
|
+
loadEnv5();
|
|
3827
|
+
const dbUrl = env4.DATABASE_URL;
|
|
3531
3828
|
if (!dbUrl) {
|
|
3532
|
-
console.error(
|
|
3533
|
-
console.log(
|
|
3829
|
+
console.error(chalk19.red("\u274C DATABASE_URL not found in environment"));
|
|
3830
|
+
console.log(chalk19.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
3534
3831
|
process.exit(1);
|
|
3535
3832
|
}
|
|
3536
3833
|
const dbInfo = parseDatabaseUrl2(dbUrl);
|
|
@@ -3540,7 +3837,7 @@ async function dbBackup(options) {
|
|
|
3540
3837
|
const ext = format === "sql" ? "sql" : "dump";
|
|
3541
3838
|
const filename = options.output || path3.join(backupDir, `${dbInfo.database}_${timestamp}.${ext}`);
|
|
3542
3839
|
if (options.dataOnly && options.schemaOnly) {
|
|
3543
|
-
console.error(
|
|
3840
|
+
console.error(chalk19.red("\u274C Cannot use --data-only and --schema-only together"));
|
|
3544
3841
|
process.exit(1);
|
|
3545
3842
|
}
|
|
3546
3843
|
const args = [
|
|
@@ -3586,11 +3883,11 @@ async function dbBackup(options) {
|
|
|
3586
3883
|
const stats = await fs3.stat(filename);
|
|
3587
3884
|
const size = formatBytes(stats.size);
|
|
3588
3885
|
spinner.succeed("Backup created");
|
|
3589
|
-
console.log(
|
|
3886
|
+
console.log(chalk19.green(`
|
|
3590
3887
|
\u2705 Backup created successfully`));
|
|
3591
|
-
console.log(
|
|
3592
|
-
console.log(
|
|
3593
|
-
console.log(
|
|
3888
|
+
console.log(chalk19.gray(` File: ${filename}`));
|
|
3889
|
+
console.log(chalk19.gray(` Size: ${size}`));
|
|
3890
|
+
console.log(chalk19.dim("\n\u{1F4CB} Collecting metadata..."));
|
|
3594
3891
|
const [gitInfo, migrationInfo] = await Promise.all([
|
|
3595
3892
|
collectGitInfo(),
|
|
3596
3893
|
collectMigrationInfo(dbUrl)
|
|
@@ -3630,19 +3927,19 @@ async function dbBackup(options) {
|
|
|
3630
3927
|
reject(error);
|
|
3631
3928
|
});
|
|
3632
3929
|
}).catch((error) => {
|
|
3633
|
-
console.error(
|
|
3930
|
+
console.error(chalk19.red("\n\u274C Failed to create backup"));
|
|
3634
3931
|
if (errorOutput.includes("pg_dump: command not found") || errorOutput.includes("not found")) {
|
|
3635
|
-
console.error(
|
|
3932
|
+
console.error(chalk19.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
|
|
3636
3933
|
} else {
|
|
3637
|
-
console.error(
|
|
3934
|
+
console.error(chalk19.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3638
3935
|
}
|
|
3639
3936
|
process.exit(1);
|
|
3640
3937
|
});
|
|
3641
3938
|
}
|
|
3642
3939
|
|
|
3643
3940
|
// src/commands/db/migrate.ts
|
|
3644
|
-
import { env as
|
|
3645
|
-
import { loadEnv as
|
|
3941
|
+
import { env as env5 } from "@spfn/core/config";
|
|
3942
|
+
import { loadEnv as loadEnv6 } from "@spfn/core/server";
|
|
3646
3943
|
var PROJECT_MIGRATIONS_TABLE = "__drizzle_migrations";
|
|
3647
3944
|
async function dbMigrate(options = {}) {
|
|
3648
3945
|
try {
|
|
@@ -3651,7 +3948,7 @@ async function dbMigrate(options = {}) {
|
|
|
3651
3948
|
process.exit(1);
|
|
3652
3949
|
}
|
|
3653
3950
|
if (options.withBackup) {
|
|
3654
|
-
console.log(
|
|
3951
|
+
console.log(chalk20.blue("\u{1F4E6} Creating pre-migration backup...\n"));
|
|
3655
3952
|
await dbBackup({
|
|
3656
3953
|
format: "custom",
|
|
3657
3954
|
tag: "pre-migration",
|
|
@@ -3662,68 +3959,108 @@ async function dbMigrate(options = {}) {
|
|
|
3662
3959
|
const { drizzle } = await import("drizzle-orm/postgres-js");
|
|
3663
3960
|
const { migrate } = await import("drizzle-orm/postgres-js/migrator");
|
|
3664
3961
|
const postgres = await import("postgres");
|
|
3665
|
-
|
|
3666
|
-
if (!
|
|
3667
|
-
console.error(
|
|
3962
|
+
loadEnv6();
|
|
3963
|
+
if (!env5.DATABASE_URL) {
|
|
3964
|
+
console.error(chalk20.red("\u274C DATABASE_URL not found in environment"));
|
|
3668
3965
|
process.exit(1);
|
|
3669
3966
|
}
|
|
3670
3967
|
const { discoverFunctionMigrations: discoverFunctionMigrations2, loadFunctionMigrationPlans: loadFunctionMigrationPlans2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
3671
3968
|
const functions = discoverFunctionMigrations2(process.cwd());
|
|
3672
3969
|
if (functions.length > 0) {
|
|
3673
|
-
console.log(
|
|
3970
|
+
console.log(chalk20.blue("\u{1F4E6} Applying function package migrations:"));
|
|
3674
3971
|
functions.forEach((func) => {
|
|
3675
|
-
console.log(
|
|
3972
|
+
console.log(chalk20.dim(` - ${func.packageName}`));
|
|
3676
3973
|
});
|
|
3677
3974
|
await executeFunctionMigrations2(loadFunctionMigrationPlans2(functions));
|
|
3678
|
-
console.log(
|
|
3975
|
+
console.log(chalk20.green("\u2705 Function migrations applied\n"));
|
|
3679
3976
|
}
|
|
3680
|
-
const projectMigrationsDir =
|
|
3681
|
-
if (
|
|
3682
|
-
const projConn = postgres.default(
|
|
3977
|
+
const projectMigrationsDir = join21(process.cwd(), "src/server/drizzle");
|
|
3978
|
+
if (existsSync22(projectMigrationsDir)) {
|
|
3979
|
+
const projConn = postgres.default(env5.DATABASE_URL, { max: 1 });
|
|
3683
3980
|
const projDb = drizzle({ client: projConn });
|
|
3684
3981
|
try {
|
|
3685
|
-
console.log(
|
|
3982
|
+
console.log(chalk20.blue("\u{1F4E6} Running project migrations..."));
|
|
3686
3983
|
await migrate(projDb, {
|
|
3687
3984
|
migrationsFolder: projectMigrationsDir,
|
|
3688
3985
|
migrationsTable: PROJECT_MIGRATIONS_TABLE
|
|
3689
3986
|
});
|
|
3690
|
-
console.log(
|
|
3987
|
+
console.log(chalk20.green("\u2705 Project migrations applied successfully"));
|
|
3691
3988
|
} finally {
|
|
3692
3989
|
await projConn.end();
|
|
3693
3990
|
}
|
|
3694
3991
|
} else {
|
|
3695
|
-
console.log(
|
|
3992
|
+
console.log(chalk20.dim("No project migrations found (src/server/drizzle)"));
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
// src/commands/db/status.ts
|
|
3997
|
+
import chalk21 from "chalk";
|
|
3998
|
+
init_migration_status();
|
|
3999
|
+
async function dbStatus() {
|
|
4000
|
+
try {
|
|
4001
|
+
validateDatabasePrerequisites();
|
|
4002
|
+
} catch (error) {
|
|
4003
|
+
process.exit(1);
|
|
4004
|
+
}
|
|
4005
|
+
let status;
|
|
4006
|
+
try {
|
|
4007
|
+
status = await getMigrationStatus(process.cwd());
|
|
4008
|
+
} catch (error) {
|
|
4009
|
+
console.error(chalk21.red("\u274C Failed to read migration status"));
|
|
4010
|
+
console.error(chalk21.red(error instanceof Error ? error.message : "Unknown error"));
|
|
4011
|
+
process.exit(1);
|
|
4012
|
+
}
|
|
4013
|
+
const targets = [...status.packages, ...status.project ? [status.project] : []];
|
|
4014
|
+
if (targets.length === 0) {
|
|
4015
|
+
console.log(chalk21.dim("No migrations found (no @spfn packages with migrations, no src/server/drizzle)."));
|
|
4016
|
+
return;
|
|
4017
|
+
}
|
|
4018
|
+
console.log(chalk21.blue("\u{1F4E6} Migration status:\n"));
|
|
4019
|
+
for (const target of targets) {
|
|
4020
|
+
const state = target.pending === 0 ? chalk21.green(`${target.applied}/${target.total} applied`) : chalk21.yellow(`${target.applied}/${target.total} applied, ${target.pending} pending`);
|
|
4021
|
+
console.log(` ${target.name.padEnd(32)} ${state}`);
|
|
4022
|
+
for (const tag of target.pendingTags) {
|
|
4023
|
+
console.log(chalk21.dim(` - ${tag}`));
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
const pendingTotal = targets.reduce((sum, t) => sum + t.pending, 0);
|
|
4027
|
+
if (pendingTotal === 0) {
|
|
4028
|
+
console.log(chalk21.green("\n\u2705 Database is up to date\n"));
|
|
4029
|
+
} else {
|
|
4030
|
+
console.log(chalk21.yellow(`
|
|
4031
|
+
\u26A0\uFE0F ${pendingTotal} pending migration(s) \u2014 run: pnpm spfn db migrate
|
|
4032
|
+
`));
|
|
3696
4033
|
}
|
|
3697
4034
|
}
|
|
3698
4035
|
|
|
3699
4036
|
// src/commands/db/studio.ts
|
|
3700
|
-
import
|
|
3701
|
-
import { existsSync as
|
|
4037
|
+
import chalk22 from "chalk";
|
|
4038
|
+
import { existsSync as existsSync23, writeFileSync as writeFileSync14, unlinkSync as unlinkSync3 } from "fs";
|
|
3702
4039
|
import { spawn as spawn3 } from "child_process";
|
|
3703
|
-
import { env as
|
|
4040
|
+
import { env as env6 } from "@spfn/core/config";
|
|
3704
4041
|
async function dbStudio(requestedPort) {
|
|
3705
|
-
console.log(
|
|
4042
|
+
console.log(chalk22.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
|
|
3706
4043
|
const defaultPort = 4983;
|
|
3707
4044
|
const startPort = requestedPort || defaultPort;
|
|
3708
4045
|
let port;
|
|
3709
4046
|
try {
|
|
3710
4047
|
port = await findAvailablePort(startPort);
|
|
3711
4048
|
if (port !== startPort) {
|
|
3712
|
-
console.log(
|
|
4049
|
+
console.log(chalk22.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
|
|
3713
4050
|
`));
|
|
3714
4051
|
}
|
|
3715
4052
|
} catch (error) {
|
|
3716
|
-
console.error(
|
|
4053
|
+
console.error(chalk22.red(error instanceof Error ? error.message : "Failed to find available port"));
|
|
3717
4054
|
process.exit(1);
|
|
3718
4055
|
}
|
|
3719
|
-
const hasUserConfig =
|
|
4056
|
+
const hasUserConfig = existsSync23("./drizzle.config.ts");
|
|
3720
4057
|
const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
|
|
3721
4058
|
try {
|
|
3722
4059
|
const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
|
|
3723
4060
|
if (!hasUserConfig) {
|
|
3724
|
-
if (!
|
|
3725
|
-
console.error(
|
|
3726
|
-
console.log(
|
|
4061
|
+
if (!env6.DATABASE_URL) {
|
|
4062
|
+
console.error(chalk22.red("\u274C DATABASE_URL not found in environment"));
|
|
4063
|
+
console.log(chalk22.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
3727
4064
|
process.exit(1);
|
|
3728
4065
|
}
|
|
3729
4066
|
const { generateDrizzleConfigFile } = await import("@spfn/core/db");
|
|
@@ -3734,34 +4071,34 @@ async function dbStudio(requestedPort) {
|
|
|
3734
4071
|
// Expand glob patterns for Studio compatibility
|
|
3735
4072
|
});
|
|
3736
4073
|
writeFileSync14(tempConfigPath, configContent);
|
|
3737
|
-
console.log(
|
|
4074
|
+
console.log(chalk22.dim("Using auto-generated Drizzle config\n"));
|
|
3738
4075
|
}
|
|
3739
4076
|
const studioProcess = spawn3("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
|
|
3740
4077
|
stdio: "inherit",
|
|
3741
4078
|
shell: true,
|
|
3742
|
-
env: shouldRelaxDbTls(
|
|
4079
|
+
env: shouldRelaxDbTls(env6.DATABASE_URL) ? { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" } : { ...process.env }
|
|
3743
4080
|
});
|
|
3744
4081
|
const cleanup = () => {
|
|
3745
|
-
if (!hasUserConfig &&
|
|
4082
|
+
if (!hasUserConfig && existsSync23(tempConfigPath)) {
|
|
3746
4083
|
unlinkSync3(tempConfigPath);
|
|
3747
4084
|
}
|
|
3748
4085
|
};
|
|
3749
4086
|
studioProcess.on("exit", (code) => {
|
|
3750
4087
|
cleanup();
|
|
3751
4088
|
if (code !== 0 && code !== null) {
|
|
3752
|
-
console.error(
|
|
4089
|
+
console.error(chalk22.red(`
|
|
3753
4090
|
\u274C Drizzle Studio exited with code ${code}`));
|
|
3754
4091
|
process.exit(code);
|
|
3755
4092
|
}
|
|
3756
4093
|
});
|
|
3757
4094
|
studioProcess.on("error", (error) => {
|
|
3758
4095
|
cleanup();
|
|
3759
|
-
console.error(
|
|
3760
|
-
console.error(
|
|
4096
|
+
console.error(chalk22.red("\u274C Failed to start Drizzle Studio"));
|
|
4097
|
+
console.error(chalk22.red(error.message));
|
|
3761
4098
|
process.exit(1);
|
|
3762
4099
|
});
|
|
3763
4100
|
process.on("SIGINT", () => {
|
|
3764
|
-
console.log(
|
|
4101
|
+
console.log(chalk22.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
|
|
3765
4102
|
studioProcess.kill("SIGTERM");
|
|
3766
4103
|
cleanup();
|
|
3767
4104
|
process.exit(0);
|
|
@@ -3772,39 +4109,39 @@ async function dbStudio(requestedPort) {
|
|
|
3772
4109
|
process.exit(0);
|
|
3773
4110
|
});
|
|
3774
4111
|
} catch (error) {
|
|
3775
|
-
if (!hasUserConfig &&
|
|
4112
|
+
if (!hasUserConfig && existsSync23(tempConfigPath)) {
|
|
3776
4113
|
unlinkSync3(tempConfigPath);
|
|
3777
4114
|
}
|
|
3778
|
-
console.error(
|
|
3779
|
-
console.error(
|
|
4115
|
+
console.error(chalk22.red("\u274C Failed to start Drizzle Studio"));
|
|
4116
|
+
console.error(chalk22.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3780
4117
|
process.exit(1);
|
|
3781
4118
|
}
|
|
3782
4119
|
}
|
|
3783
4120
|
|
|
3784
4121
|
// src/commands/db/drop.ts
|
|
3785
|
-
import
|
|
3786
|
-
import
|
|
3787
|
-
import { env as
|
|
3788
|
-
import { loadEnv as
|
|
4122
|
+
import chalk23 from "chalk";
|
|
4123
|
+
import prompts6 from "prompts";
|
|
4124
|
+
import { env as env7 } from "@spfn/core/config";
|
|
4125
|
+
import { loadEnv as loadEnv7 } from "@spfn/core/server";
|
|
3789
4126
|
async function dbDrop() {
|
|
3790
|
-
|
|
3791
|
-
if (!
|
|
3792
|
-
console.error(
|
|
3793
|
-
console.log(
|
|
4127
|
+
loadEnv7();
|
|
4128
|
+
if (!env7.DATABASE_URL) {
|
|
4129
|
+
console.error(chalk23.red("\u274C DATABASE_URL not found in environment"));
|
|
4130
|
+
console.log(chalk23.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
3794
4131
|
process.exit(1);
|
|
3795
4132
|
}
|
|
3796
|
-
const dbInfo = parseDatabaseUrl2(
|
|
3797
|
-
console.log(
|
|
3798
|
-
console.log(
|
|
4133
|
+
const dbInfo = parseDatabaseUrl2(env7.DATABASE_URL);
|
|
4134
|
+
console.log(chalk23.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
|
|
4135
|
+
console.log(chalk23.dim(` Target: ${dbInfo.database} @ ${dbInfo.host}:${dbInfo.port}
|
|
3799
4136
|
`));
|
|
3800
|
-
const { confirm } = await
|
|
4137
|
+
const { confirm } = await prompts6({
|
|
3801
4138
|
type: "confirm",
|
|
3802
4139
|
name: "confirm",
|
|
3803
4140
|
message: "Are you sure you want to drop all tables?",
|
|
3804
4141
|
initial: false
|
|
3805
4142
|
});
|
|
3806
4143
|
if (!confirm) {
|
|
3807
|
-
console.log(
|
|
4144
|
+
console.log(chalk23.gray("Cancelled."));
|
|
3808
4145
|
process.exit(0);
|
|
3809
4146
|
}
|
|
3810
4147
|
await confirmDangerousTarget(dbInfo);
|
|
@@ -3817,16 +4154,16 @@ async function dbDrop() {
|
|
|
3817
4154
|
}
|
|
3818
4155
|
|
|
3819
4156
|
// src/commands/db/check.ts
|
|
3820
|
-
import
|
|
4157
|
+
import chalk24 from "chalk";
|
|
3821
4158
|
import ora8 from "ora";
|
|
3822
4159
|
async function dbCheck() {
|
|
3823
|
-
const spinner = ora8("Checking
|
|
4160
|
+
const spinner = ora8("Checking migration files...").start();
|
|
3824
4161
|
try {
|
|
3825
4162
|
await runDrizzleCommand("check");
|
|
3826
|
-
spinner.succeed("
|
|
4163
|
+
spinner.succeed("Migration files consistent");
|
|
3827
4164
|
} catch (error) {
|
|
3828
|
-
spinner.fail("
|
|
3829
|
-
console.error(
|
|
4165
|
+
spinner.fail("Migration file check failed");
|
|
4166
|
+
console.error(chalk24.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3830
4167
|
process.exit(1);
|
|
3831
4168
|
}
|
|
3832
4169
|
}
|
|
@@ -3834,28 +4171,28 @@ async function dbCheck() {
|
|
|
3834
4171
|
// src/commands/db/restore.ts
|
|
3835
4172
|
import path4 from "path";
|
|
3836
4173
|
import { spawn as spawn4 } from "child_process";
|
|
3837
|
-
import
|
|
4174
|
+
import chalk25 from "chalk";
|
|
3838
4175
|
import ora9 from "ora";
|
|
3839
|
-
import
|
|
3840
|
-
import { env as
|
|
3841
|
-
import { loadEnv as
|
|
4176
|
+
import prompts7 from "prompts";
|
|
4177
|
+
import { env as env8 } from "@spfn/core/config";
|
|
4178
|
+
import { loadEnv as loadEnv8 } from "@spfn/core/server";
|
|
3842
4179
|
async function dbRestore(backupFile, options = {}) {
|
|
3843
|
-
console.log(
|
|
3844
|
-
|
|
3845
|
-
const dbUrl =
|
|
4180
|
+
console.log(chalk25.blue("\u267B\uFE0F Restoring database from backup...\n"));
|
|
4181
|
+
loadEnv8();
|
|
4182
|
+
const dbUrl = env8.DATABASE_URL;
|
|
3846
4183
|
if (!dbUrl) {
|
|
3847
|
-
console.error(
|
|
3848
|
-
console.log(
|
|
4184
|
+
console.error(chalk25.red("\u274C DATABASE_URL not found in environment"));
|
|
4185
|
+
console.log(chalk25.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
3849
4186
|
process.exit(1);
|
|
3850
4187
|
}
|
|
3851
4188
|
let file = backupFile;
|
|
3852
4189
|
if (!file) {
|
|
3853
4190
|
const backups = await listBackupFiles();
|
|
3854
4191
|
if (backups.length === 0) {
|
|
3855
|
-
console.log(
|
|
4192
|
+
console.log(chalk25.yellow("No backups found in ./backups directory"));
|
|
3856
4193
|
process.exit(0);
|
|
3857
4194
|
}
|
|
3858
|
-
const { selected } = await
|
|
4195
|
+
const { selected } = await prompts7({
|
|
3859
4196
|
type: "select",
|
|
3860
4197
|
name: "selected",
|
|
3861
4198
|
message: "Select backup to restore:",
|
|
@@ -3865,31 +4202,31 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3865
4202
|
}))
|
|
3866
4203
|
});
|
|
3867
4204
|
if (!selected) {
|
|
3868
|
-
console.log(
|
|
4205
|
+
console.log(chalk25.gray("Cancelled"));
|
|
3869
4206
|
process.exit(0);
|
|
3870
4207
|
}
|
|
3871
4208
|
file = selected;
|
|
3872
4209
|
}
|
|
3873
4210
|
if (!file) {
|
|
3874
|
-
console.error(
|
|
4211
|
+
console.error(chalk25.red("\u274C No backup file selected"));
|
|
3875
4212
|
process.exit(1);
|
|
3876
4213
|
}
|
|
3877
4214
|
const metadata = await loadBackupMetadata(file);
|
|
3878
4215
|
if (metadata) {
|
|
3879
|
-
console.log(
|
|
3880
|
-
console.log(
|
|
3881
|
-
console.log(
|
|
4216
|
+
console.log(chalk25.blue("\n\u{1F4CB} Backup Information:\n"));
|
|
4217
|
+
console.log(chalk25.dim(` Database: ${metadata.database}`));
|
|
4218
|
+
console.log(chalk25.dim(` Created: ${new Date(metadata.timestamp).toLocaleString()}`));
|
|
3882
4219
|
if (metadata.environment) {
|
|
3883
|
-
console.log(
|
|
4220
|
+
console.log(chalk25.dim(` Environment: ${metadata.environment}`));
|
|
3884
4221
|
}
|
|
3885
4222
|
if (metadata.tags && metadata.tags.length > 0) {
|
|
3886
|
-
console.log(
|
|
4223
|
+
console.log(chalk25.dim(` Tags: ${metadata.tags.join(", ")}`));
|
|
3887
4224
|
}
|
|
3888
4225
|
if (metadata.backup.dataOnly) {
|
|
3889
|
-
console.log(
|
|
4226
|
+
console.log(chalk25.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
|
|
3890
4227
|
}
|
|
3891
4228
|
if (metadata.backup.schemaOnly) {
|
|
3892
|
-
console.log(
|
|
4229
|
+
console.log(chalk25.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
|
|
3893
4230
|
}
|
|
3894
4231
|
const warnings2 = [];
|
|
3895
4232
|
const [currentGitInfo, currentMigrationInfo] = await Promise.all([
|
|
@@ -3912,25 +4249,25 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3912
4249
|
}
|
|
3913
4250
|
}
|
|
3914
4251
|
if (warnings2.length > 0) {
|
|
3915
|
-
console.log(
|
|
3916
|
-
warnings2.forEach((warning) => console.log(
|
|
4252
|
+
console.log(chalk25.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
|
|
4253
|
+
warnings2.forEach((warning) => console.log(chalk25.yellow(` - ${warning}`)));
|
|
3917
4254
|
console.log("");
|
|
3918
4255
|
}
|
|
3919
4256
|
}
|
|
3920
4257
|
const dbInfo = parseDatabaseUrl2(dbUrl);
|
|
3921
|
-
const { confirm } = await
|
|
4258
|
+
const { confirm } = await prompts7({
|
|
3922
4259
|
type: "confirm",
|
|
3923
4260
|
name: "confirm",
|
|
3924
|
-
message:
|
|
4261
|
+
message: chalk25.yellow(`\u26A0\uFE0F This will replace all data in "${dbInfo.database}" @ ${dbInfo.host}:${dbInfo.port}. Continue?`),
|
|
3925
4262
|
initial: false
|
|
3926
4263
|
});
|
|
3927
4264
|
if (!confirm) {
|
|
3928
|
-
console.log(
|
|
4265
|
+
console.log(chalk25.gray("Cancelled"));
|
|
3929
4266
|
process.exit(0);
|
|
3930
4267
|
}
|
|
3931
4268
|
await confirmDangerousTarget(dbInfo);
|
|
3932
4269
|
if (options.dataOnly && options.schemaOnly) {
|
|
3933
|
-
console.error(
|
|
4270
|
+
console.error(chalk25.red("\u274C Cannot use --data-only and --schema-only together"));
|
|
3934
4271
|
process.exit(1);
|
|
3935
4272
|
}
|
|
3936
4273
|
const ext = path4.extname(file);
|
|
@@ -3958,11 +4295,11 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
3958
4295
|
args.push(file);
|
|
3959
4296
|
} else {
|
|
3960
4297
|
if (options.drop) {
|
|
3961
|
-
console.log(
|
|
4298
|
+
console.log(chalk25.yellow("\u26A0\uFE0F Note: --drop only works with custom format backups (.dump) and was ignored."));
|
|
3962
4299
|
}
|
|
3963
4300
|
if (options.dataOnly || options.schemaOnly) {
|
|
3964
|
-
console.log(
|
|
3965
|
-
console.log(
|
|
4301
|
+
console.log(chalk25.yellow("\u26A0\uFE0F Note: --data-only and --schema-only options only work with custom format backups (.dump)"));
|
|
4302
|
+
console.log(chalk25.yellow(" For SQL files, the backup must have been created with the desired option.\n"));
|
|
3966
4303
|
}
|
|
3967
4304
|
args.push("-h", dbInfo.host);
|
|
3968
4305
|
args.push("-p", dbInfo.port);
|
|
@@ -4005,7 +4342,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4005
4342
|
}
|
|
4006
4343
|
if (verbose) {
|
|
4007
4344
|
spinner.stop();
|
|
4008
|
-
console.log(
|
|
4345
|
+
console.log(chalk25.dim(` ${line.trim()}`));
|
|
4009
4346
|
spinner.start();
|
|
4010
4347
|
}
|
|
4011
4348
|
}
|
|
@@ -4013,7 +4350,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4013
4350
|
restoreProcess.stdout?.on("data", (data) => {
|
|
4014
4351
|
if (verbose) {
|
|
4015
4352
|
spinner.stop();
|
|
4016
|
-
console.log(
|
|
4353
|
+
console.log(chalk25.dim(` ${data.toString().trim()}`));
|
|
4017
4354
|
spinner.start();
|
|
4018
4355
|
}
|
|
4019
4356
|
});
|
|
@@ -4023,31 +4360,31 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4023
4360
|
const summary = objectCount > 0 ? ` (${objectCount} objects)` : "";
|
|
4024
4361
|
spinner.succeed(`Restore completed${summary}`);
|
|
4025
4362
|
if (warnings.length > 0) {
|
|
4026
|
-
console.log(
|
|
4363
|
+
console.log(chalk25.yellow(`
|
|
4027
4364
|
\u26A0\uFE0F Warnings during restore (${warnings.length}):
|
|
4028
4365
|
`));
|
|
4029
4366
|
for (const w of warnings) {
|
|
4030
|
-
console.log(
|
|
4367
|
+
console.log(chalk25.yellow(` - ${w}`));
|
|
4031
4368
|
}
|
|
4032
4369
|
}
|
|
4033
|
-
console.log(
|
|
4370
|
+
console.log(chalk25.green("\n\u2705 Database restored successfully"));
|
|
4034
4371
|
resolve3();
|
|
4035
4372
|
} else {
|
|
4036
4373
|
spinner.fail("Restore failed");
|
|
4037
4374
|
if (errors.length > 0) {
|
|
4038
|
-
console.error(
|
|
4375
|
+
console.error(chalk25.red(`
|
|
4039
4376
|
\u274C Errors (${errors.length}):
|
|
4040
4377
|
`));
|
|
4041
4378
|
for (const e of errors) {
|
|
4042
|
-
console.error(
|
|
4379
|
+
console.error(chalk25.red(` - ${e}`));
|
|
4043
4380
|
}
|
|
4044
4381
|
}
|
|
4045
4382
|
if (warnings.length > 0) {
|
|
4046
|
-
console.log(
|
|
4383
|
+
console.log(chalk25.yellow(`
|
|
4047
4384
|
\u26A0\uFE0F Warnings (${warnings.length}):
|
|
4048
4385
|
`));
|
|
4049
4386
|
for (const w of warnings) {
|
|
4050
|
-
console.log(
|
|
4387
|
+
console.log(chalk25.yellow(` - ${w}`));
|
|
4051
4388
|
}
|
|
4052
4389
|
}
|
|
4053
4390
|
const fallback = errors.length === 0 && warnings.length === 0 ? "Restore failed with no output" : "";
|
|
@@ -4061,7 +4398,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4061
4398
|
}).catch((error) => {
|
|
4062
4399
|
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
4063
4400
|
if (msg) {
|
|
4064
|
-
console.error(
|
|
4401
|
+
console.error(chalk25.red(`
|
|
4065
4402
|
\u274C ${msg}`));
|
|
4066
4403
|
}
|
|
4067
4404
|
process.exit(1);
|
|
@@ -4069,17 +4406,17 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4069
4406
|
}
|
|
4070
4407
|
|
|
4071
4408
|
// src/commands/db/list.ts
|
|
4072
|
-
import
|
|
4409
|
+
import chalk26 from "chalk";
|
|
4073
4410
|
async function dbBackupList() {
|
|
4074
|
-
console.log(
|
|
4411
|
+
console.log(chalk26.blue("\u{1F4CB} Database backups:\n"));
|
|
4075
4412
|
const backups = await listBackupFiles();
|
|
4076
4413
|
if (backups.length === 0) {
|
|
4077
|
-
console.log(
|
|
4078
|
-
console.log(
|
|
4414
|
+
console.log(chalk26.yellow("No backups found in ./backups directory"));
|
|
4415
|
+
console.log(chalk26.gray("\n\u{1F4A1} Create a backup with: pnpm spfn db backup\n"));
|
|
4079
4416
|
return;
|
|
4080
4417
|
}
|
|
4081
|
-
console.log(
|
|
4082
|
-
console.log(
|
|
4418
|
+
console.log(chalk26.bold(" Date Size File"));
|
|
4419
|
+
console.log(chalk26.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"));
|
|
4083
4420
|
backups.forEach((backup) => {
|
|
4084
4421
|
const date = backup.date.toLocaleString("en-US", {
|
|
4085
4422
|
year: "numeric",
|
|
@@ -4090,23 +4427,23 @@ async function dbBackupList() {
|
|
|
4090
4427
|
second: "2-digit"
|
|
4091
4428
|
});
|
|
4092
4429
|
const sizeStr = backup.size.padEnd(10);
|
|
4093
|
-
console.log(
|
|
4430
|
+
console.log(chalk26.white(` ${date} ${sizeStr} ${backup.name}`));
|
|
4094
4431
|
});
|
|
4095
|
-
console.log(
|
|
4432
|
+
console.log(chalk26.gray(`
|
|
4096
4433
|
Total: ${backups.length} backup(s)
|
|
4097
4434
|
`));
|
|
4098
4435
|
}
|
|
4099
4436
|
|
|
4100
4437
|
// src/commands/db/clean.ts
|
|
4101
4438
|
import { promises as fs4 } from "fs";
|
|
4102
|
-
import
|
|
4439
|
+
import chalk27 from "chalk";
|
|
4103
4440
|
import ora10 from "ora";
|
|
4104
|
-
import
|
|
4441
|
+
import prompts8 from "prompts";
|
|
4105
4442
|
async function dbBackupClean(options) {
|
|
4106
|
-
console.log(
|
|
4443
|
+
console.log(chalk27.blue("\u{1F9F9} Cleaning old backups...\n"));
|
|
4107
4444
|
const backups = await listBackupFiles();
|
|
4108
4445
|
if (backups.length === 0) {
|
|
4109
|
-
console.log(
|
|
4446
|
+
console.log(chalk27.yellow("No backups found"));
|
|
4110
4447
|
return;
|
|
4111
4448
|
}
|
|
4112
4449
|
let toDelete = [];
|
|
@@ -4123,22 +4460,22 @@ async function dbBackupClean(options) {
|
|
|
4123
4460
|
toDelete = backups.slice(defaultKeep);
|
|
4124
4461
|
}
|
|
4125
4462
|
if (toDelete.length === 0) {
|
|
4126
|
-
console.log(
|
|
4463
|
+
console.log(chalk27.green("\u2705 No backups to clean"));
|
|
4127
4464
|
return;
|
|
4128
4465
|
}
|
|
4129
|
-
console.log(
|
|
4466
|
+
console.log(chalk27.yellow(`The following ${toDelete.length} backup(s) will be deleted:
|
|
4130
4467
|
`));
|
|
4131
4468
|
toDelete.forEach((backup) => {
|
|
4132
|
-
console.log(
|
|
4469
|
+
console.log(chalk27.gray(` - ${backup.name} (${backup.size})`));
|
|
4133
4470
|
});
|
|
4134
|
-
const { confirm } = await
|
|
4471
|
+
const { confirm } = await prompts8({
|
|
4135
4472
|
type: "confirm",
|
|
4136
4473
|
name: "confirm",
|
|
4137
4474
|
message: "\nProceed with deletion?",
|
|
4138
4475
|
initial: false
|
|
4139
4476
|
});
|
|
4140
4477
|
if (!confirm) {
|
|
4141
|
-
console.log(
|
|
4478
|
+
console.log(chalk27.gray("Cancelled"));
|
|
4142
4479
|
return;
|
|
4143
4480
|
}
|
|
4144
4481
|
const spinner = ora10("Deleting backups...").start();
|
|
@@ -4149,20 +4486,20 @@ async function dbBackupClean(options) {
|
|
|
4149
4486
|
fs4.rm(backup.path.replace(/\.(sql|dump)$/, ".meta.json"), { force: true })
|
|
4150
4487
|
]));
|
|
4151
4488
|
spinner.succeed("Backups deleted");
|
|
4152
|
-
console.log(
|
|
4489
|
+
console.log(chalk27.green(`
|
|
4153
4490
|
\u2705 Deleted ${toDelete.length} backup(s)`));
|
|
4154
4491
|
} catch (error) {
|
|
4155
4492
|
spinner.fail("Failed to delete backups");
|
|
4156
|
-
console.error(
|
|
4493
|
+
console.error(chalk27.red(error instanceof Error ? error.message : "Unknown error"));
|
|
4157
4494
|
process.exit(1);
|
|
4158
4495
|
}
|
|
4159
4496
|
}
|
|
4160
4497
|
|
|
4161
4498
|
// src/commands/db/reindex.ts
|
|
4162
|
-
import { existsSync as
|
|
4163
|
-
import { join as
|
|
4164
|
-
import
|
|
4165
|
-
import { loadEnv as
|
|
4499
|
+
import { existsSync as existsSync24, readFileSync as readFileSync10, writeFileSync as writeFileSync15, renameSync, copyFileSync } from "fs";
|
|
4500
|
+
import { join as join22 } from "path";
|
|
4501
|
+
import chalk28 from "chalk";
|
|
4502
|
+
import { loadEnv as loadEnv9 } from "@spfn/core/server";
|
|
4166
4503
|
function isTimestampPrefix(tag) {
|
|
4167
4504
|
const prefix = tag.split("_")[0];
|
|
4168
4505
|
return /^\d{5,}$/.test(prefix);
|
|
@@ -4178,19 +4515,19 @@ function parseTag(tag) {
|
|
|
4178
4515
|
};
|
|
4179
4516
|
}
|
|
4180
4517
|
async function dbReindex(options = {}) {
|
|
4181
|
-
|
|
4518
|
+
loadEnv9();
|
|
4182
4519
|
const { getDrizzleConfig } = await import("@spfn/core/db");
|
|
4183
4520
|
const config = getDrizzleConfig({ disablePackageDiscovery: true });
|
|
4184
4521
|
const outDir = config.out;
|
|
4185
|
-
const journalPath =
|
|
4186
|
-
if (!
|
|
4187
|
-
console.error(
|
|
4188
|
-
console.log(
|
|
4522
|
+
const journalPath = join22(outDir, "meta", "_journal.json");
|
|
4523
|
+
if (!existsSync24(journalPath)) {
|
|
4524
|
+
console.error(chalk28.red("\u274C No _journal.json found at:"), journalPath);
|
|
4525
|
+
console.log(chalk28.yellow("\u{1F4A1} Run `spfn db generate` first to create migrations"));
|
|
4189
4526
|
process.exit(1);
|
|
4190
4527
|
}
|
|
4191
4528
|
const journal = JSON.parse(readFileSync10(journalPath, "utf-8"));
|
|
4192
4529
|
if (journal.entries.length === 0) {
|
|
4193
|
-
console.log(
|
|
4530
|
+
console.log(chalk28.yellow("No migration entries found \u2014 nothing to reindex."));
|
|
4194
4531
|
return;
|
|
4195
4532
|
}
|
|
4196
4533
|
const renames = [];
|
|
@@ -4204,46 +4541,46 @@ async function dbReindex(options = {}) {
|
|
|
4204
4541
|
const { prefix: oldPrefix, suffix } = parseTag(entry.tag);
|
|
4205
4542
|
const newPrefix = String(entry.when);
|
|
4206
4543
|
const newTag = suffix ? `${newPrefix}_${suffix}` : newPrefix;
|
|
4207
|
-
const oldSql =
|
|
4208
|
-
const newSql =
|
|
4209
|
-
if (
|
|
4544
|
+
const oldSql = join22(outDir, `${entry.tag}.sql`);
|
|
4545
|
+
const newSql = join22(outDir, `${newTag}.sql`);
|
|
4546
|
+
if (existsSync24(oldSql)) {
|
|
4210
4547
|
renames.push({ type: "sql", from: oldSql, to: newSql });
|
|
4211
4548
|
}
|
|
4212
|
-
const oldSnapshot =
|
|
4213
|
-
const newSnapshot =
|
|
4214
|
-
if (
|
|
4549
|
+
const oldSnapshot = join22(outDir, "meta", `${oldPrefix}_snapshot.json`);
|
|
4550
|
+
const newSnapshot = join22(outDir, "meta", `${newPrefix}_snapshot.json`);
|
|
4551
|
+
if (existsSync24(oldSnapshot)) {
|
|
4215
4552
|
renames.push({ type: "snapshot", from: oldSnapshot, to: newSnapshot });
|
|
4216
4553
|
}
|
|
4217
4554
|
tagUpdates.push({ idx: entry.idx, oldTag: entry.tag, newTag });
|
|
4218
4555
|
}
|
|
4219
4556
|
if (tagUpdates.length === 0) {
|
|
4220
|
-
console.log(
|
|
4557
|
+
console.log(chalk28.green("\u2705 All migrations already use timestamp prefix \u2014 nothing to do."));
|
|
4221
4558
|
if (skipped > 0) {
|
|
4222
|
-
console.log(
|
|
4559
|
+
console.log(chalk28.dim(` (${skipped} entries already timestamp-prefixed)`));
|
|
4223
4560
|
}
|
|
4224
4561
|
return;
|
|
4225
4562
|
}
|
|
4226
|
-
console.log(
|
|
4563
|
+
console.log(chalk28.bold("\n\u{1F4CB} Reindex plan:\n"));
|
|
4227
4564
|
for (const update of tagUpdates) {
|
|
4228
4565
|
console.log(
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4566
|
+
chalk28.dim(` [${update.idx}]`),
|
|
4567
|
+
chalk28.red(update.oldTag),
|
|
4568
|
+
chalk28.dim("\u2192"),
|
|
4569
|
+
chalk28.green(update.newTag)
|
|
4233
4570
|
);
|
|
4234
4571
|
}
|
|
4235
|
-
console.log(
|
|
4572
|
+
console.log(chalk28.dim(`
|
|
4236
4573
|
${renames.length} file(s) to rename, ${tagUpdates.length} journal tag(s) to update`));
|
|
4237
4574
|
if (skipped > 0) {
|
|
4238
|
-
console.log(
|
|
4575
|
+
console.log(chalk28.dim(` ${skipped} entry/entries already timestamp-prefixed (skipped)`));
|
|
4239
4576
|
}
|
|
4240
4577
|
if (options.dryRun) {
|
|
4241
|
-
console.log(
|
|
4578
|
+
console.log(chalk28.yellow("\n\u{1F50D} Dry run \u2014 no changes applied."));
|
|
4242
4579
|
return;
|
|
4243
4580
|
}
|
|
4244
4581
|
const backupPath = journalPath + ".bak";
|
|
4245
4582
|
copyFileSync(journalPath, backupPath);
|
|
4246
|
-
console.log(
|
|
4583
|
+
console.log(chalk28.dim(`
|
|
4247
4584
|
Backed up journal \u2192 ${backupPath}`));
|
|
4248
4585
|
for (const rename of renames) {
|
|
4249
4586
|
renameSync(rename.from, rename.to);
|
|
@@ -4255,7 +4592,7 @@ async function dbReindex(options = {}) {
|
|
|
4255
4592
|
}
|
|
4256
4593
|
}
|
|
4257
4594
|
writeFileSync15(journalPath, JSON.stringify(journal, null, 2) + "\n");
|
|
4258
|
-
console.log(
|
|
4595
|
+
console.log(chalk28.green(`
|
|
4259
4596
|
\u2705 Reindex complete \u2014 ${tagUpdates.length} migration(s) converted to timestamp prefix.`));
|
|
4260
4597
|
}
|
|
4261
4598
|
|
|
@@ -4264,9 +4601,10 @@ var dbCommand = new Command9("db").description("Database management commands (wr
|
|
|
4264
4601
|
dbCommand.command("generate").alias("g").description("Generate database migrations from schema changes").action(dbGenerate);
|
|
4265
4602
|
dbCommand.command("push").description("Push schema changes to database (safe mode by default)").option("--force", "Apply all changes including destructive ones").option("--dry-run", "Show changes without applying").action((options) => dbPush(options));
|
|
4266
4603
|
dbCommand.command("migrate").alias("m").description("Run pending migrations").option("--with-backup", "Create backup before running migrations").action((options) => dbMigrate(options));
|
|
4604
|
+
dbCommand.command("status").description("Show applied/pending migration status (function packages + project)").action(dbStatus);
|
|
4267
4605
|
dbCommand.command("studio").description("Open Drizzle Studio (database GUI)").option("-p, --port <port>", "Studio port (auto-finds if in use)").action((options) => dbStudio(options.port ? Number(options.port) : void 0));
|
|
4268
4606
|
dbCommand.command("drop").description("Drop all database tables (\u26A0\uFE0F dangerous!)").action(dbDrop);
|
|
4269
|
-
dbCommand.command("check").description("Check
|
|
4607
|
+
dbCommand.command("check").description("Check migration file consistency (drizzle-kit check)").action(dbCheck);
|
|
4270
4608
|
dbCommand.command("backup").description("Create a database backup").option("-f, --format <format>", "Backup format (sql or custom)", "sql").option("-o, --output <path>", "Custom output path").option("-s, --schema <name>", "Backup specific schema only").option("--data-only", "Backup data only (no schema)").option("--schema-only", "Backup schema only (no data)").option("--tag <tags>", "Comma-separated tags for this backup").option("--env <environment>", "Environment label (e.g., production, staging)").action((options) => dbBackup(options));
|
|
4271
4609
|
dbCommand.command("restore [file]").description("Restore database from backup").option("--drop", "Drop existing tables before restore").option("-s, --schema <name>", "Restore specific schema only").option("--data-only", "Restore data only (requires custom format .dump file)").option("--schema-only", "Restore schema only (requires custom format .dump file)").option("-v, --verbose", "Show detailed restore progress").action((file, options) => dbRestore(file, options));
|
|
4272
4610
|
dbCommand.command("backup:list").description("List all database backups").action(dbBackupList);
|
|
@@ -4276,25 +4614,25 @@ dbCommand.command("reindex").description("Convert migration files from sequentia
|
|
|
4276
4614
|
// src/commands/add.ts
|
|
4277
4615
|
init_package_manager();
|
|
4278
4616
|
import { Command as Command10 } from "commander";
|
|
4279
|
-
import { existsSync as
|
|
4280
|
-
import { join as
|
|
4617
|
+
import { existsSync as existsSync25, readFileSync as readFileSync11 } from "fs";
|
|
4618
|
+
import { join as join23 } from "path";
|
|
4281
4619
|
import { execa as execa9 } from "execa";
|
|
4282
|
-
import
|
|
4620
|
+
import chalk29 from "chalk";
|
|
4283
4621
|
import ora11 from "ora";
|
|
4284
4622
|
async function addPackage(packageName) {
|
|
4285
4623
|
if (!packageName.includes("/")) {
|
|
4286
|
-
console.error(
|
|
4287
|
-
console.log(
|
|
4288
|
-
console.log(
|
|
4289
|
-
console.log(
|
|
4624
|
+
console.error(chalk29.red("\u274C Please specify full package name"));
|
|
4625
|
+
console.log(chalk29.yellow("\n\u{1F4A1} Examples:"));
|
|
4626
|
+
console.log(chalk29.gray(" pnpm spfn add @spfn/cms"));
|
|
4627
|
+
console.log(chalk29.gray(" pnpm spfn add @mycompany/spfn-analytics"));
|
|
4290
4628
|
process.exit(1);
|
|
4291
4629
|
}
|
|
4292
|
-
console.log(
|
|
4630
|
+
console.log(chalk29.blue(`
|
|
4293
4631
|
\u{1F4E6} Setting up ${packageName}...
|
|
4294
4632
|
`));
|
|
4295
|
-
const pkgPath =
|
|
4296
|
-
const pkgJsonPath =
|
|
4297
|
-
if (!
|
|
4633
|
+
const pkgPath = join23(process.cwd(), "node_modules", ...packageName.split("/"));
|
|
4634
|
+
const pkgJsonPath = join23(pkgPath, "package.json");
|
|
4635
|
+
if (!existsSync25(pkgJsonPath)) {
|
|
4298
4636
|
const pm = detectPackageManager(process.cwd());
|
|
4299
4637
|
const installSpinner = ora11("Installing package...").start();
|
|
4300
4638
|
try {
|
|
@@ -4302,26 +4640,27 @@ async function addPackage(packageName) {
|
|
|
4302
4640
|
installSpinner.succeed("Package installed");
|
|
4303
4641
|
} catch (error) {
|
|
4304
4642
|
installSpinner.fail("Failed to install package");
|
|
4305
|
-
console.error(
|
|
4643
|
+
console.error(chalk29.red(error instanceof Error ? error.message : "Unknown error"));
|
|
4306
4644
|
process.exit(1);
|
|
4307
4645
|
}
|
|
4308
4646
|
} else {
|
|
4309
|
-
console.log(
|
|
4647
|
+
console.log(chalk29.gray("\u2713 Package already installed (using local version)\n"));
|
|
4310
4648
|
}
|
|
4311
|
-
if (!
|
|
4312
|
-
console.error(
|
|
4649
|
+
if (!existsSync25(pkgJsonPath)) {
|
|
4650
|
+
console.error(chalk29.red(`\u274C Package ${packageName} not found after installation`));
|
|
4313
4651
|
process.exit(1);
|
|
4314
4652
|
}
|
|
4315
4653
|
const pkgJson = JSON.parse(readFileSync11(pkgJsonPath, "utf-8"));
|
|
4316
4654
|
if (pkgJson.spfn?.migrations) {
|
|
4317
|
-
console.log(
|
|
4655
|
+
console.log(chalk29.blue(`
|
|
4318
4656
|
\u{1F5C4}\uFE0F Setting up database for ${packageName}...
|
|
4319
4657
|
`));
|
|
4320
|
-
const { env:
|
|
4321
|
-
if (!
|
|
4322
|
-
console.log(
|
|
4323
|
-
console.log(
|
|
4324
|
-
console.log(
|
|
4658
|
+
const { env: env9 } = await import("@spfn/core/config");
|
|
4659
|
+
if (!env9.DATABASE_URL) {
|
|
4660
|
+
console.log(chalk29.yellow("\u26A0\uFE0F DATABASE_URL not found \u2014 skipping database setup."));
|
|
4661
|
+
console.log(chalk29.yellow(` ${packageName} tables are created by its bundled migrations, not by schema push.`));
|
|
4662
|
+
console.log(chalk29.cyan(" Once DATABASE_URL is set, run: pnpm spfn db migrate"));
|
|
4663
|
+
console.log(chalk29.gray(" (check state anytime with: pnpm spfn db status)\n"));
|
|
4325
4664
|
} else {
|
|
4326
4665
|
const { discoverFunctionMigrations: discoverFunctionMigrations2, loadFunctionMigrationPlans: loadFunctionMigrationPlans2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
4327
4666
|
const functions = discoverFunctionMigrations2(process.cwd());
|
|
@@ -4333,21 +4672,21 @@ async function addPackage(packageName) {
|
|
|
4333
4672
|
spinner.succeed("Migrations applied");
|
|
4334
4673
|
} catch (error) {
|
|
4335
4674
|
spinner.fail("Failed to apply migrations");
|
|
4336
|
-
console.error(
|
|
4675
|
+
console.error(chalk29.red(error instanceof Error ? error.message : "Unknown error"));
|
|
4337
4676
|
process.exit(1);
|
|
4338
4677
|
}
|
|
4339
4678
|
} else {
|
|
4340
|
-
console.log(
|
|
4679
|
+
console.log(chalk29.gray("\u2139\uFE0F No migrations found for this package"));
|
|
4341
4680
|
}
|
|
4342
4681
|
}
|
|
4343
4682
|
} else {
|
|
4344
|
-
console.log(
|
|
4683
|
+
console.log(chalk29.gray("\n\u2139\uFE0F No database migrations to apply"));
|
|
4345
4684
|
}
|
|
4346
|
-
console.log(
|
|
4685
|
+
console.log(chalk29.green(`
|
|
4347
4686
|
\u2705 ${packageName} installed successfully!
|
|
4348
4687
|
`));
|
|
4349
4688
|
if (pkgJson.spfn?.setupMessage) {
|
|
4350
|
-
console.log(
|
|
4689
|
+
console.log(chalk29.cyan("\u{1F4DA} Setup Guide:"));
|
|
4351
4690
|
console.log(pkgJson.spfn.setupMessage);
|
|
4352
4691
|
console.log();
|
|
4353
4692
|
}
|
|
@@ -4356,8 +4695,8 @@ var addCommand = new Command10("add").description("Install and set up SPFN ecosy
|
|
|
4356
4695
|
|
|
4357
4696
|
// src/commands/env.ts
|
|
4358
4697
|
import { Command as Command11 } from "commander";
|
|
4359
|
-
import
|
|
4360
|
-
import { existsSync as
|
|
4698
|
+
import chalk30 from "chalk";
|
|
4699
|
+
import { existsSync as existsSync26, readFileSync as readFileSync12, writeFileSync as writeFileSync16 } from "fs";
|
|
4361
4700
|
import { resolve as resolve2 } from "path";
|
|
4362
4701
|
import { parse as parse2 } from "dotenv";
|
|
4363
4702
|
|
|
@@ -4413,26 +4752,26 @@ function getEnvFilesForEnvironment(nodeEnv) {
|
|
|
4413
4752
|
}
|
|
4414
4753
|
function formatType(type) {
|
|
4415
4754
|
const typeColors = {
|
|
4416
|
-
string:
|
|
4417
|
-
number:
|
|
4418
|
-
boolean:
|
|
4419
|
-
url:
|
|
4420
|
-
enum:
|
|
4421
|
-
json:
|
|
4755
|
+
string: chalk30.green,
|
|
4756
|
+
number: chalk30.blue,
|
|
4757
|
+
boolean: chalk30.yellow,
|
|
4758
|
+
url: chalk30.cyan,
|
|
4759
|
+
enum: chalk30.magenta,
|
|
4760
|
+
json: chalk30.red
|
|
4422
4761
|
};
|
|
4423
|
-
return (typeColors[type] ||
|
|
4762
|
+
return (typeColors[type] || chalk30.white)(type);
|
|
4424
4763
|
}
|
|
4425
4764
|
function formatDefault(value, type) {
|
|
4426
4765
|
if (value === void 0) {
|
|
4427
|
-
return
|
|
4766
|
+
return chalk30.dim("(none)");
|
|
4428
4767
|
}
|
|
4429
4768
|
if (type === "string" || type === "url") {
|
|
4430
|
-
return
|
|
4769
|
+
return chalk30.green(`"${value}"`);
|
|
4431
4770
|
}
|
|
4432
4771
|
if (type === "boolean") {
|
|
4433
|
-
return value ?
|
|
4772
|
+
return value ? chalk30.green("true") : chalk30.red("false");
|
|
4434
4773
|
}
|
|
4435
|
-
return
|
|
4774
|
+
return chalk30.cyan(String(value));
|
|
4436
4775
|
}
|
|
4437
4776
|
async function listEnvVars(options) {
|
|
4438
4777
|
const packageName = options.package || "@spfn/core";
|
|
@@ -4446,28 +4785,28 @@ async function listEnvVars(options) {
|
|
|
4446
4785
|
acc[target].push([key, schema]);
|
|
4447
4786
|
return acc;
|
|
4448
4787
|
}, {});
|
|
4449
|
-
console.log(
|
|
4788
|
+
console.log(chalk30.blue.bold(`
|
|
4450
4789
|
\u{1F4CB} Environment Variables by File (${packageName})
|
|
4451
4790
|
`));
|
|
4452
4791
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
4453
|
-
console.log(
|
|
4792
|
+
console.log(chalk30.bold.magenta(`
|
|
4454
4793
|
${file}`));
|
|
4455
|
-
console.log(
|
|
4794
|
+
console.log(chalk30.dim("\u2500".repeat(50)));
|
|
4456
4795
|
for (const [key, schema] of vars) {
|
|
4457
4796
|
printEnvVar(key, schema);
|
|
4458
4797
|
}
|
|
4459
4798
|
}
|
|
4460
4799
|
} else {
|
|
4461
|
-
console.log(
|
|
4800
|
+
console.log(chalk30.blue.bold(`
|
|
4462
4801
|
\u{1F4CB} Environment Variables (${packageName})
|
|
4463
4802
|
`));
|
|
4464
4803
|
for (const [key, schema] of allVars) {
|
|
4465
4804
|
printEnvVar(key, schema, true);
|
|
4466
4805
|
}
|
|
4467
4806
|
}
|
|
4468
|
-
console.log(
|
|
4807
|
+
console.log(chalk30.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
|
|
4469
4808
|
} catch (error) {
|
|
4470
|
-
console.error(
|
|
4809
|
+
console.error(chalk30.red(`
|
|
4471
4810
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4472
4811
|
`));
|
|
4473
4812
|
process.exit(1);
|
|
@@ -4475,17 +4814,17 @@ ${file}`));
|
|
|
4475
4814
|
}
|
|
4476
4815
|
function printEnvVar(key, schema, showFile = false) {
|
|
4477
4816
|
const typeStr = formatType(schema.type);
|
|
4478
|
-
const requiredStr = schema.required || schema.default !== void 0 ?
|
|
4479
|
-
const sensitiveStr = schema.sensitive ?
|
|
4480
|
-
const fileStr = showFile ?
|
|
4481
|
-
console.log(`${
|
|
4482
|
-
console.log(` ${
|
|
4817
|
+
const requiredStr = schema.required || schema.default !== void 0 ? chalk30.red("[required]") : chalk30.dim("[optional]");
|
|
4818
|
+
const sensitiveStr = schema.sensitive ? chalk30.yellow(" [sensitive]") : "";
|
|
4819
|
+
const fileStr = showFile ? chalk30.dim(` \u2192 ${getTargetFile(schema)}`) : "";
|
|
4820
|
+
console.log(`${chalk30.bold.cyan(key)} ${chalk30.dim("(")}${typeStr}${chalk30.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
|
|
4821
|
+
console.log(` ${chalk30.dim(schema.description)}`);
|
|
4483
4822
|
if (schema.default !== void 0) {
|
|
4484
|
-
console.log(` ${
|
|
4823
|
+
console.log(` ${chalk30.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
4485
4824
|
}
|
|
4486
4825
|
if (schema.examples && schema.examples.length > 0) {
|
|
4487
4826
|
const exampleStr = schema.examples.map((ex) => formatDefault(ex, schema.type)).join(", ");
|
|
4488
|
-
console.log(` ${
|
|
4827
|
+
console.log(` ${chalk30.dim("Examples:")} ${exampleStr}`);
|
|
4489
4828
|
}
|
|
4490
4829
|
console.log();
|
|
4491
4830
|
}
|
|
@@ -4493,7 +4832,7 @@ async function showEnvStats(options) {
|
|
|
4493
4832
|
const packageName = options.package || "@spfn/core";
|
|
4494
4833
|
try {
|
|
4495
4834
|
const envSchema = await loadEnvSchema(packageName);
|
|
4496
|
-
console.log(
|
|
4835
|
+
console.log(chalk30.blue.bold(`
|
|
4497
4836
|
\u{1F4CA} Environment Variable Statistics (${packageName})
|
|
4498
4837
|
`));
|
|
4499
4838
|
const allVars = Object.entries(envSchema);
|
|
@@ -4515,24 +4854,24 @@ async function showEnvStats(options) {
|
|
|
4515
4854
|
acc[file] = (acc[file] || 0) + 1;
|
|
4516
4855
|
return acc;
|
|
4517
4856
|
}, {});
|
|
4518
|
-
console.log(`${
|
|
4519
|
-
console.log(`${
|
|
4520
|
-
console.log(`${
|
|
4521
|
-
console.log(`${
|
|
4522
|
-
console.log(
|
|
4523
|
-
console.log(` ${
|
|
4524
|
-
console.log(` ${
|
|
4525
|
-
console.log(
|
|
4857
|
+
console.log(`${chalk30.bold("Total variables:")} ${chalk30.cyan(allVars.length)}`);
|
|
4858
|
+
console.log(`${chalk30.bold("Required:")} ${chalk30.red(required.length)}`);
|
|
4859
|
+
console.log(`${chalk30.bold("Optional:")} ${chalk30.dim(optional.length)}`);
|
|
4860
|
+
console.log(`${chalk30.bold("Sensitive:")} ${chalk30.yellow(sensitive.length)}`);
|
|
4861
|
+
console.log(chalk30.bold("\nBy Target:"));
|
|
4862
|
+
console.log(` ${chalk30.blue("Next.js accessible:")} ${chalk30.cyan(nextjsVars.length)}`);
|
|
4863
|
+
console.log(` ${chalk30.magenta("SPFN server only:")} ${chalk30.cyan(serverOnlyVars.length)}`);
|
|
4864
|
+
console.log(chalk30.bold("\nBy File:"));
|
|
4526
4865
|
for (const [file, count] of Object.entries(fileCount)) {
|
|
4527
|
-
console.log(` ${
|
|
4866
|
+
console.log(` ${chalk30.dim(file)}: ${chalk30.cyan(count)}`);
|
|
4528
4867
|
}
|
|
4529
|
-
console.log(
|
|
4868
|
+
console.log(chalk30.bold("\nBy Type:"));
|
|
4530
4869
|
for (const [type, count] of Object.entries(typeCount)) {
|
|
4531
|
-
console.log(` ${formatType(type)}: ${
|
|
4870
|
+
console.log(` ${formatType(type)}: ${chalk30.cyan(count)}`);
|
|
4532
4871
|
}
|
|
4533
4872
|
console.log();
|
|
4534
4873
|
} catch (error) {
|
|
4535
|
-
console.error(
|
|
4874
|
+
console.error(chalk30.red(`
|
|
4536
4875
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4537
4876
|
`));
|
|
4538
4877
|
process.exit(1);
|
|
@@ -4552,26 +4891,26 @@ async function searchEnvVars(query, options) {
|
|
|
4552
4891
|
}
|
|
4553
4892
|
}
|
|
4554
4893
|
if (results.length === 0) {
|
|
4555
|
-
console.log(
|
|
4894
|
+
console.log(chalk30.yellow(`
|
|
4556
4895
|
\u26A0\uFE0F No environment variables found matching "${query}"
|
|
4557
4896
|
`));
|
|
4558
4897
|
return;
|
|
4559
4898
|
}
|
|
4560
|
-
console.log(
|
|
4899
|
+
console.log(chalk30.blue.bold(`
|
|
4561
4900
|
\u{1F50D} Found ${results.length} environment variable(s) matching "${query}"
|
|
4562
4901
|
`));
|
|
4563
4902
|
for (const [key, schema] of results) {
|
|
4564
4903
|
const typeStr = formatType(schema.type);
|
|
4565
|
-
const requiredStr = schema.required || schema.default !== void 0 ?
|
|
4566
|
-
console.log(`${
|
|
4567
|
-
console.log(` ${
|
|
4904
|
+
const requiredStr = schema.required || schema.default !== void 0 ? chalk30.red("[required]") : chalk30.dim("[optional]");
|
|
4905
|
+
console.log(`${chalk30.bold.cyan(key)} ${chalk30.dim("(")}${typeStr}${chalk30.dim(")")} ${requiredStr}`);
|
|
4906
|
+
console.log(` ${chalk30.dim(schema.description)}`);
|
|
4568
4907
|
if (schema.default !== void 0) {
|
|
4569
|
-
console.log(` ${
|
|
4908
|
+
console.log(` ${chalk30.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
4570
4909
|
}
|
|
4571
4910
|
console.log();
|
|
4572
4911
|
}
|
|
4573
4912
|
} catch (error) {
|
|
4574
|
-
console.error(
|
|
4913
|
+
console.error(chalk30.red(`
|
|
4575
4914
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4576
4915
|
`));
|
|
4577
4916
|
process.exit(1);
|
|
@@ -4583,9 +4922,9 @@ envCommand.command("stats").description("Show environment variable statistics").
|
|
|
4583
4922
|
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);
|
|
4584
4923
|
function validateEnvOption(envValue) {
|
|
4585
4924
|
if (!VALID_ENVS.includes(envValue)) {
|
|
4586
|
-
console.error(
|
|
4925
|
+
console.error(chalk30.red(`
|
|
4587
4926
|
\u274C Invalid environment: "${envValue}"`));
|
|
4588
|
-
console.log(
|
|
4927
|
+
console.log(chalk30.dim(` Valid values: ${VALID_ENVS.join(", ")}
|
|
4589
4928
|
`));
|
|
4590
4929
|
process.exit(1);
|
|
4591
4930
|
}
|
|
@@ -4606,8 +4945,8 @@ async function initEnvFiles(options) {
|
|
|
4606
4945
|
return acc;
|
|
4607
4946
|
}, {});
|
|
4608
4947
|
if (targetEnv) {
|
|
4609
|
-
console.log(
|
|
4610
|
-
\u{1F680} Generating .env template files for ${
|
|
4948
|
+
console.log(chalk30.blue.bold(`
|
|
4949
|
+
\u{1F680} Generating .env template files for ${chalk30.cyan(targetEnv)} environment
|
|
4611
4950
|
`));
|
|
4612
4951
|
const envSpecificFiles = {};
|
|
4613
4952
|
const committedVars = allVars.filter(([_, schema]) => !schema.sensitive);
|
|
@@ -4623,24 +4962,24 @@ async function initEnvFiles(options) {
|
|
|
4623
4962
|
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
4624
4963
|
}
|
|
4625
4964
|
} else {
|
|
4626
|
-
console.log(
|
|
4965
|
+
console.log(chalk30.blue.bold(`
|
|
4627
4966
|
\u{1F680} Generating .env template files
|
|
4628
4967
|
`));
|
|
4629
4968
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
4630
4969
|
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
4631
4970
|
}
|
|
4632
4971
|
}
|
|
4633
|
-
console.log(
|
|
4634
|
-
console.log(
|
|
4635
|
-
console.log(
|
|
4636
|
-
console.log(
|
|
4972
|
+
console.log(chalk30.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
|
|
4973
|
+
console.log(chalk30.dim(" cp .env.example .env"));
|
|
4974
|
+
console.log(chalk30.dim(" cp .env.local.example .env.local"));
|
|
4975
|
+
console.log(chalk30.dim(" cp .env.server.example .env.server"));
|
|
4637
4976
|
if (targetEnv) {
|
|
4638
|
-
console.log(
|
|
4639
|
-
console.log(
|
|
4977
|
+
console.log(chalk30.dim(` cp .env.${targetEnv}.example .env.${targetEnv}`));
|
|
4978
|
+
console.log(chalk30.dim(` cp .env.${targetEnv}.local.example .env.${targetEnv}.local`));
|
|
4640
4979
|
}
|
|
4641
4980
|
console.log("");
|
|
4642
4981
|
} catch (error) {
|
|
4643
|
-
console.error(
|
|
4982
|
+
console.error(chalk30.red(`
|
|
4644
4983
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4645
4984
|
`));
|
|
4646
4985
|
process.exit(1);
|
|
@@ -4648,12 +4987,12 @@ async function initEnvFiles(options) {
|
|
|
4648
4987
|
}
|
|
4649
4988
|
function writeEnvTemplate(cwd, file, vars, force) {
|
|
4650
4989
|
const filePath = resolve2(cwd, file);
|
|
4651
|
-
if (
|
|
4652
|
-
console.log(
|
|
4990
|
+
if (existsSync26(filePath) && !force) {
|
|
4991
|
+
console.log(chalk30.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
|
|
4653
4992
|
return;
|
|
4654
4993
|
}
|
|
4655
4994
|
writeFileSync16(filePath, generateEnvFileContent(vars), "utf-8");
|
|
4656
|
-
console.log(
|
|
4995
|
+
console.log(chalk30.green(` \u2705 ${file} (${vars.length} variables)`));
|
|
4657
4996
|
}
|
|
4658
4997
|
function generateEnvFileContent(vars) {
|
|
4659
4998
|
const lines = [
|
|
@@ -4688,7 +5027,7 @@ async function checkEnvFiles(options) {
|
|
|
4688
5027
|
const envSchema = await loadEnvSchema(packageName);
|
|
4689
5028
|
const allVars = Object.entries(envSchema);
|
|
4690
5029
|
const envLabel = targetEnv ? ` (${targetEnv})` : "";
|
|
4691
|
-
console.log(
|
|
5030
|
+
console.log(chalk30.blue.bold(`
|
|
4692
5031
|
\u{1F50D} Checking .env files against schema${envLabel}
|
|
4693
5032
|
`));
|
|
4694
5033
|
const filesToCheck = targetEnv ? getEnvFilesForEnvironment(targetEnv) : [...BASE_ENV_FILES.nextjs, ...BASE_ENV_FILES.server];
|
|
@@ -4697,7 +5036,7 @@ async function checkEnvFiles(options) {
|
|
|
4697
5036
|
const warnings = [];
|
|
4698
5037
|
for (const file of filesToCheck) {
|
|
4699
5038
|
const filePath = resolve2(cwd, file);
|
|
4700
|
-
if (!
|
|
5039
|
+
if (!existsSync26(filePath)) {
|
|
4701
5040
|
continue;
|
|
4702
5041
|
}
|
|
4703
5042
|
const content = readFileSync12(filePath, "utf-8");
|
|
@@ -4705,7 +5044,7 @@ async function checkEnvFiles(options) {
|
|
|
4705
5044
|
for (const [key, value] of Object.entries(parsed)) {
|
|
4706
5045
|
loadedEnv[key] = { value: value || "", file };
|
|
4707
5046
|
}
|
|
4708
|
-
console.log(
|
|
5047
|
+
console.log(chalk30.dim(` \u{1F4C4} ${file} loaded`));
|
|
4709
5048
|
}
|
|
4710
5049
|
console.log("");
|
|
4711
5050
|
for (const [key, schema] of allVars) {
|
|
@@ -4713,7 +5052,7 @@ async function checkEnvFiles(options) {
|
|
|
4713
5052
|
const found = loadedEnv[key];
|
|
4714
5053
|
if (!found) {
|
|
4715
5054
|
if (schema.required && schema.default === void 0) {
|
|
4716
|
-
issues.push(`${
|
|
5055
|
+
issues.push(`${chalk30.red("\u2717")} ${chalk30.cyan(key)} is required but not found in any .env file`);
|
|
4717
5056
|
}
|
|
4718
5057
|
continue;
|
|
4719
5058
|
}
|
|
@@ -4723,11 +5062,11 @@ async function checkEnvFiles(options) {
|
|
|
4723
5062
|
if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
|
|
4724
5063
|
if (schema.sensitive) {
|
|
4725
5064
|
issues.push(
|
|
4726
|
-
`${
|
|
5065
|
+
`${chalk30.red("\u2717")} ${chalk30.cyan(key)} is sensitive and should be in ${chalk30.magenta(expectedFile)}, but found in ${chalk30.yellow(found.file)} (security risk!)`
|
|
4727
5066
|
);
|
|
4728
5067
|
} else {
|
|
4729
5068
|
warnings.push(
|
|
4730
|
-
`${
|
|
5069
|
+
`${chalk30.yellow("\u26A0")} ${chalk30.cyan(key)} should be in ${chalk30.magenta(expectedFile)}, but found in ${chalk30.dim(found.file)}`
|
|
4731
5070
|
);
|
|
4732
5071
|
}
|
|
4733
5072
|
}
|
|
@@ -4735,34 +5074,34 @@ async function checkEnvFiles(options) {
|
|
|
4735
5074
|
for (const [key, { file }] of Object.entries(loadedEnv)) {
|
|
4736
5075
|
const inSchema = allVars.some(([k]) => k === key);
|
|
4737
5076
|
if (!inSchema) {
|
|
4738
|
-
warnings.push(`${
|
|
5077
|
+
warnings.push(`${chalk30.yellow("\u26A0")} ${chalk30.cyan(key)} in ${chalk30.dim(file)} is not in schema`);
|
|
4739
5078
|
}
|
|
4740
5079
|
}
|
|
4741
5080
|
if (issues.length > 0) {
|
|
4742
|
-
console.log(
|
|
5081
|
+
console.log(chalk30.red.bold("Issues:"));
|
|
4743
5082
|
for (const issue of issues) {
|
|
4744
5083
|
console.log(` ${issue}`);
|
|
4745
5084
|
}
|
|
4746
5085
|
console.log("");
|
|
4747
5086
|
}
|
|
4748
5087
|
if (warnings.length > 0) {
|
|
4749
|
-
console.log(
|
|
5088
|
+
console.log(chalk30.yellow.bold("Warnings:"));
|
|
4750
5089
|
for (const warning of warnings) {
|
|
4751
5090
|
console.log(` ${warning}`);
|
|
4752
5091
|
}
|
|
4753
5092
|
console.log("");
|
|
4754
5093
|
}
|
|
4755
5094
|
if (issues.length === 0 && warnings.length === 0) {
|
|
4756
|
-
console.log(
|
|
5095
|
+
console.log(chalk30.green("\u2705 All environment variables are correctly configured!\n"));
|
|
4757
5096
|
} else {
|
|
4758
|
-
console.log(
|
|
5097
|
+
console.log(chalk30.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
|
|
4759
5098
|
`));
|
|
4760
5099
|
if (issues.length > 0) {
|
|
4761
5100
|
process.exit(1);
|
|
4762
5101
|
}
|
|
4763
5102
|
}
|
|
4764
5103
|
} catch (error) {
|
|
4765
|
-
console.error(
|
|
5104
|
+
console.error(chalk30.red(`
|
|
4766
5105
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4767
5106
|
`));
|
|
4768
5107
|
process.exit(1);
|
|
@@ -4774,17 +5113,17 @@ async function validateEnvVars(options) {
|
|
|
4774
5113
|
const packages = options.packages || ["@spfn/core"];
|
|
4775
5114
|
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
4776
5115
|
if (targetEnv) {
|
|
4777
|
-
const { loadEnv:
|
|
4778
|
-
const result =
|
|
4779
|
-
console.log(
|
|
4780
|
-
\u{1F50D} Validating environment variables for ${
|
|
5116
|
+
const { loadEnv: loadEnv10 } = await import("@spfn/core/env/loader");
|
|
5117
|
+
const result = loadEnv10({ nodeEnv: targetEnv });
|
|
5118
|
+
console.log(chalk30.blue.bold(`
|
|
5119
|
+
\u{1F50D} Validating environment variables for ${chalk30.cyan(targetEnv)}
|
|
4781
5120
|
`));
|
|
4782
5121
|
if (result.loadedFiles.length > 0) {
|
|
4783
|
-
console.log(
|
|
5122
|
+
console.log(chalk30.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
|
|
4784
5123
|
}
|
|
4785
5124
|
console.log("");
|
|
4786
5125
|
} else {
|
|
4787
|
-
console.log(
|
|
5126
|
+
console.log(chalk30.blue.bold(`
|
|
4788
5127
|
\u{1F50D} Validating environment variables
|
|
4789
5128
|
`));
|
|
4790
5129
|
}
|
|
@@ -4792,7 +5131,7 @@ async function validateEnvVars(options) {
|
|
|
4792
5131
|
const allWarnings = [];
|
|
4793
5132
|
for (const packageName of packages) {
|
|
4794
5133
|
try {
|
|
4795
|
-
console.log(
|
|
5134
|
+
console.log(chalk30.dim(` \u{1F4E6} ${packageName}`));
|
|
4796
5135
|
const envSchema = await loadEnvSchema(packageName);
|
|
4797
5136
|
const { createEnvRegistry } = await import("@spfn/core/env");
|
|
4798
5137
|
const registry = createEnvRegistry(envSchema);
|
|
@@ -4805,10 +5144,10 @@ async function validateEnvVars(options) {
|
|
|
4805
5144
|
}
|
|
4806
5145
|
} catch (error) {
|
|
4807
5146
|
if (error instanceof Error && error.message.includes("does not export envSchema")) {
|
|
4808
|
-
console.log(
|
|
5147
|
+
console.log(chalk30.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
|
|
4809
5148
|
continue;
|
|
4810
5149
|
}
|
|
4811
|
-
console.error(
|
|
5150
|
+
console.error(chalk30.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
|
|
4812
5151
|
if (options.strict) {
|
|
4813
5152
|
process.exit(1);
|
|
4814
5153
|
}
|
|
@@ -4816,32 +5155,32 @@ async function validateEnvVars(options) {
|
|
|
4816
5155
|
}
|
|
4817
5156
|
console.log("");
|
|
4818
5157
|
if (allErrors.length > 0) {
|
|
4819
|
-
console.log(
|
|
5158
|
+
console.log(chalk30.red.bold(`\u274C Validation Errors (${allErrors.length}):
|
|
4820
5159
|
`));
|
|
4821
5160
|
for (const error of allErrors) {
|
|
4822
|
-
console.log(` ${
|
|
4823
|
-
console.log(` ${
|
|
4824
|
-
console.log(` ${
|
|
5161
|
+
console.log(` ${chalk30.red("\u2717")} ${chalk30.cyan(error.key)}`);
|
|
5162
|
+
console.log(` ${chalk30.dim(error.message)}`);
|
|
5163
|
+
console.log(` ${chalk30.dim(`from ${error.package}`)}`);
|
|
4825
5164
|
console.log("");
|
|
4826
5165
|
}
|
|
4827
5166
|
}
|
|
4828
5167
|
if (allWarnings.length > 0) {
|
|
4829
|
-
console.log(
|
|
5168
|
+
console.log(chalk30.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
|
|
4830
5169
|
`));
|
|
4831
5170
|
for (const warning of allWarnings) {
|
|
4832
|
-
console.log(` ${
|
|
4833
|
-
console.log(` ${
|
|
5171
|
+
console.log(` ${chalk30.yellow("\u26A0")} ${chalk30.cyan(warning.key)}`);
|
|
5172
|
+
console.log(` ${chalk30.dim(warning.message)}`);
|
|
4834
5173
|
console.log("");
|
|
4835
5174
|
}
|
|
4836
5175
|
}
|
|
4837
5176
|
if (allErrors.length === 0 && allWarnings.length === 0) {
|
|
4838
|
-
console.log(
|
|
5177
|
+
console.log(chalk30.green.bold("\u2705 All environment variables are valid!\n"));
|
|
4839
5178
|
} else if (allErrors.length === 0) {
|
|
4840
|
-
console.log(
|
|
4841
|
-
console.log(
|
|
5179
|
+
console.log(chalk30.green("\u2705 No errors found."));
|
|
5180
|
+
console.log(chalk30.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
|
|
4842
5181
|
`));
|
|
4843
5182
|
} else {
|
|
4844
|
-
console.log(
|
|
5183
|
+
console.log(chalk30.red(`
|
|
4845
5184
|
\u274C Validation failed with ${allErrors.length} error(s)
|
|
4846
5185
|
`));
|
|
4847
5186
|
process.exit(1);
|
|
@@ -4854,13 +5193,13 @@ import { Command as Command12 } from "commander";
|
|
|
4854
5193
|
|
|
4855
5194
|
// src/commands/secret/set.ts
|
|
4856
5195
|
init_logger();
|
|
4857
|
-
import
|
|
4858
|
-
import
|
|
5196
|
+
import prompts9 from "prompts";
|
|
5197
|
+
import chalk31 from "chalk";
|
|
4859
5198
|
|
|
4860
5199
|
// src/commands/secret/options.ts
|
|
4861
5200
|
init_logger();
|
|
4862
|
-
function resolveEnv(
|
|
4863
|
-
const value =
|
|
5201
|
+
function resolveEnv(env9) {
|
|
5202
|
+
const value = env9 ?? "local";
|
|
4864
5203
|
if (!VALID_ENVS.includes(value)) {
|
|
4865
5204
|
logger.error(`Invalid environment: "${value}". Valid values: ${VALID_ENVS.join(", ")}`);
|
|
4866
5205
|
process.exit(1);
|
|
@@ -4870,12 +5209,12 @@ function resolveEnv(env8) {
|
|
|
4870
5209
|
|
|
4871
5210
|
// src/commands/secret/store-value.ts
|
|
4872
5211
|
init_logger();
|
|
4873
|
-
import { join as
|
|
5212
|
+
import { join as join25 } from "path";
|
|
4874
5213
|
init_env_file();
|
|
4875
5214
|
|
|
4876
5215
|
// src/utils/sops.ts
|
|
4877
5216
|
import { execa as execa10 } from "execa";
|
|
4878
|
-
import { existsSync as
|
|
5217
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync3, writeFileSync as writeFileSync17 } from "fs";
|
|
4879
5218
|
import { dirname as dirname3 } from "path";
|
|
4880
5219
|
async function ensureSopsInstalled() {
|
|
4881
5220
|
try {
|
|
@@ -4885,14 +5224,14 @@ async function ensureSopsInstalled() {
|
|
|
4885
5224
|
}
|
|
4886
5225
|
}
|
|
4887
5226
|
async function sopsDecrypt(absFile) {
|
|
4888
|
-
if (!
|
|
5227
|
+
if (!existsSync27(absFile)) {
|
|
4889
5228
|
return {};
|
|
4890
5229
|
}
|
|
4891
5230
|
const { stdout } = await execa10("sops", ["--decrypt", "--output-type", "json", absFile]);
|
|
4892
5231
|
return JSON.parse(stdout);
|
|
4893
5232
|
}
|
|
4894
5233
|
async function sopsSetValue(absFile, relFile, key, value) {
|
|
4895
|
-
if (
|
|
5234
|
+
if (existsSync27(absFile)) {
|
|
4896
5235
|
await execa10("sops", ["set", absFile, `["${key}"]`, JSON.stringify(value)]);
|
|
4897
5236
|
return;
|
|
4898
5237
|
}
|
|
@@ -4918,20 +5257,20 @@ async function sopsUpdateKeys(absFile) {
|
|
|
4918
5257
|
}
|
|
4919
5258
|
|
|
4920
5259
|
// src/utils/secret-config.ts
|
|
4921
|
-
import { existsSync as
|
|
4922
|
-
import { join as
|
|
5260
|
+
import { existsSync as existsSync28, readdirSync as readdirSync3 } from "fs";
|
|
5261
|
+
import { join as join24 } from "path";
|
|
4923
5262
|
var SECRETS_DIR = "secrets";
|
|
4924
|
-
function getSopsFile(cwd,
|
|
4925
|
-
const relFile = `${SECRETS_DIR}/${
|
|
4926
|
-
return { absFile:
|
|
5263
|
+
function getSopsFile(cwd, env9) {
|
|
5264
|
+
const relFile = `${SECRETS_DIR}/${env9}.enc.json`;
|
|
5265
|
+
return { absFile: join24(cwd, relFile), relFile };
|
|
4927
5266
|
}
|
|
4928
5267
|
function findUp(cwd, filename, maxDepth = 6) {
|
|
4929
5268
|
let dir = cwd;
|
|
4930
5269
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
4931
|
-
if (
|
|
5270
|
+
if (existsSync28(join24(dir, filename))) {
|
|
4932
5271
|
return dir;
|
|
4933
5272
|
}
|
|
4934
|
-
const parent =
|
|
5273
|
+
const parent = join24(dir, "..");
|
|
4935
5274
|
if (parent === dir) {
|
|
4936
5275
|
break;
|
|
4937
5276
|
}
|
|
@@ -4941,22 +5280,22 @@ function findUp(cwd, filename, maxDepth = 6) {
|
|
|
4941
5280
|
}
|
|
4942
5281
|
function findSopsConfig(cwd) {
|
|
4943
5282
|
const dir = findUp(cwd, ".sops.yaml");
|
|
4944
|
-
return dir ?
|
|
5283
|
+
return dir ? join24(dir, ".sops.yaml") : null;
|
|
4945
5284
|
}
|
|
4946
5285
|
function hasSopsConfig(cwd) {
|
|
4947
5286
|
return findSopsConfig(cwd) !== null;
|
|
4948
5287
|
}
|
|
4949
5288
|
function listSopsFiles(cwd) {
|
|
4950
|
-
const dir =
|
|
4951
|
-
if (!
|
|
5289
|
+
const dir = join24(cwd, SECRETS_DIR);
|
|
5290
|
+
if (!existsSync28(dir)) {
|
|
4952
5291
|
return [];
|
|
4953
5292
|
}
|
|
4954
|
-
return readdirSync3(dir).filter((name) => name.endsWith(".enc.json")).map((name) =>
|
|
5293
|
+
return readdirSync3(dir).filter((name) => name.endsWith(".enc.json")).map((name) => join24(dir, name));
|
|
4955
5294
|
}
|
|
4956
5295
|
|
|
4957
5296
|
// src/commands/secret/store-value.ts
|
|
4958
|
-
function isLocalEnv(
|
|
4959
|
-
return
|
|
5297
|
+
function isLocalEnv(env9) {
|
|
5298
|
+
return env9 === "local";
|
|
4960
5299
|
}
|
|
4961
5300
|
var KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
4962
5301
|
function assertValidKey(key) {
|
|
@@ -4964,15 +5303,15 @@ function assertValidKey(key) {
|
|
|
4964
5303
|
throw new Error(`Invalid secret key "${key}". Use an env-var name: letters, digits, and underscores, not starting with a digit.`);
|
|
4965
5304
|
}
|
|
4966
5305
|
}
|
|
4967
|
-
async function describeTarget(
|
|
4968
|
-
if (isLocalEnv(
|
|
5306
|
+
async function describeTarget(env9) {
|
|
5307
|
+
if (isLocalEnv(env9)) {
|
|
4969
5308
|
return detectStore().label;
|
|
4970
5309
|
}
|
|
4971
|
-
return `SOPS (${getSopsFile(process.cwd(),
|
|
5310
|
+
return `SOPS (${getSopsFile(process.cwd(), env9).relFile})`;
|
|
4972
5311
|
}
|
|
4973
|
-
async function storeSecret(cwd,
|
|
5312
|
+
async function storeSecret(cwd, env9, key, value) {
|
|
4974
5313
|
assertValidKey(key);
|
|
4975
|
-
if (isLocalEnv(
|
|
5314
|
+
if (isLocalEnv(env9)) {
|
|
4976
5315
|
const store = detectStore();
|
|
4977
5316
|
if (!await store.isAvailable()) {
|
|
4978
5317
|
throw new Error(
|
|
@@ -4980,7 +5319,7 @@ async function storeSecret(cwd, env8, key, value) {
|
|
|
4980
5319
|
);
|
|
4981
5320
|
}
|
|
4982
5321
|
await store.set(keychainName(key), value);
|
|
4983
|
-
const serverEnvPath =
|
|
5322
|
+
const serverEnvPath = join25(cwd, ".env.server");
|
|
4984
5323
|
const result = upsertEnvVar(serverEnvPath, key, keychainRef(key));
|
|
4985
5324
|
restrictEnvFilePerms(serverEnvPath);
|
|
4986
5325
|
ensureGitignored(cwd, [{ pattern: ".env.server", comment: "spfn server env (secrets)" }]);
|
|
@@ -4993,14 +5332,14 @@ async function storeSecret(cwd, env8, key, value) {
|
|
|
4993
5332
|
".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."
|
|
4994
5333
|
);
|
|
4995
5334
|
}
|
|
4996
|
-
const { absFile, relFile } = getSopsFile(cwd,
|
|
5335
|
+
const { absFile, relFile } = getSopsFile(cwd, env9);
|
|
4997
5336
|
await sopsSetValue(absFile, relFile, key, value);
|
|
4998
5337
|
logger.success(`Set ${key} in ${relFile} (encrypted). Commit and deploy to apply.`);
|
|
4999
5338
|
}
|
|
5000
5339
|
|
|
5001
5340
|
// src/commands/secret/set.ts
|
|
5002
5341
|
async function secretSet(key, options) {
|
|
5003
|
-
const
|
|
5342
|
+
const env9 = resolveEnv(options.env);
|
|
5004
5343
|
const pkg = options.package ?? "@spfn/core";
|
|
5005
5344
|
const resolvedKey = key ?? await pickSecretKey(pkg);
|
|
5006
5345
|
if (!resolvedKey) {
|
|
@@ -5008,18 +5347,18 @@ async function secretSet(key, options) {
|
|
|
5008
5347
|
process.exit(1);
|
|
5009
5348
|
}
|
|
5010
5349
|
await warnIfNotSecret(pkg, resolvedKey);
|
|
5011
|
-
const { value } = await
|
|
5350
|
+
const { value } = await prompts9({
|
|
5012
5351
|
type: "password",
|
|
5013
5352
|
name: "value",
|
|
5014
|
-
message: `Value for ${
|
|
5353
|
+
message: `Value for ${chalk31.cyan(resolvedKey)} (${env9})`
|
|
5015
5354
|
});
|
|
5016
5355
|
if (!value) {
|
|
5017
5356
|
logger.warn("Cancelled \u2014 no value entered.");
|
|
5018
5357
|
process.exit(0);
|
|
5019
5358
|
}
|
|
5020
|
-
logger.step(`Storing ${resolvedKey} \u2192 ${await describeTarget(
|
|
5359
|
+
logger.step(`Storing ${resolvedKey} \u2192 ${await describeTarget(env9)}`);
|
|
5021
5360
|
try {
|
|
5022
|
-
await storeSecret(process.cwd(),
|
|
5361
|
+
await storeSecret(process.cwd(), env9, resolvedKey, value);
|
|
5023
5362
|
} catch (error) {
|
|
5024
5363
|
logger.error(error instanceof Error ? error.message : String(error));
|
|
5025
5364
|
process.exit(1);
|
|
@@ -5035,7 +5374,7 @@ async function pickSecretKey(pkg) {
|
|
|
5035
5374
|
if (entries.length === 0) {
|
|
5036
5375
|
return void 0;
|
|
5037
5376
|
}
|
|
5038
|
-
const { key } = await
|
|
5377
|
+
const { key } = await prompts9({
|
|
5039
5378
|
type: "select",
|
|
5040
5379
|
name: "key",
|
|
5041
5380
|
message: "Which secret?",
|
|
@@ -5060,9 +5399,9 @@ async function warnIfNotSecret(pkg, key) {
|
|
|
5060
5399
|
|
|
5061
5400
|
// src/commands/secret/list.ts
|
|
5062
5401
|
init_logger();
|
|
5063
|
-
import
|
|
5402
|
+
import chalk32 from "chalk";
|
|
5064
5403
|
async function secretList(options) {
|
|
5065
|
-
const
|
|
5404
|
+
const env9 = resolveEnv(options.env);
|
|
5066
5405
|
const pkg = options.package ?? "@spfn/core";
|
|
5067
5406
|
let entries;
|
|
5068
5407
|
try {
|
|
@@ -5075,18 +5414,18 @@ async function secretList(options) {
|
|
|
5075
5414
|
logger.info(`No secrets declared in ${pkg}.`);
|
|
5076
5415
|
return;
|
|
5077
5416
|
}
|
|
5078
|
-
const present = await loadPresence(
|
|
5079
|
-
console.log(
|
|
5080
|
-
\u{1F511} Secrets (${pkg}) \u2014 ${
|
|
5417
|
+
const present = await loadPresence(env9, entries);
|
|
5418
|
+
console.log(chalk32.blue.bold(`
|
|
5419
|
+
\u{1F511} Secrets (${pkg}) \u2014 ${env9}
|
|
5081
5420
|
`));
|
|
5082
5421
|
for (const entry of entries) {
|
|
5083
5422
|
const status = statusOf(entry, present.has(entry.key));
|
|
5084
|
-
console.log(` ${badge(status)} ${
|
|
5423
|
+
console.log(` ${badge(status)} ${chalk32.cyan(entry.key)}${entry.generate ? chalk32.dim(" (generatable)") : ""}`);
|
|
5085
5424
|
}
|
|
5086
5425
|
console.log();
|
|
5087
5426
|
}
|
|
5088
|
-
async function loadPresence(
|
|
5089
|
-
if (isLocalEnv(
|
|
5427
|
+
async function loadPresence(env9, entries) {
|
|
5428
|
+
if (isLocalEnv(env9)) {
|
|
5090
5429
|
const store = detectStore();
|
|
5091
5430
|
if (!await store.isAvailable()) {
|
|
5092
5431
|
logger.warn(`${store.label} unavailable \u2014 status shown as missing.`);
|
|
@@ -5101,7 +5440,7 @@ async function loadPresence(env8, entries) {
|
|
|
5101
5440
|
return present;
|
|
5102
5441
|
}
|
|
5103
5442
|
await ensureSopsInstalled();
|
|
5104
|
-
const { absFile } = getSopsFile(process.cwd(),
|
|
5443
|
+
const { absFile } = getSopsFile(process.cwd(), env9);
|
|
5105
5444
|
const decrypted = await sopsDecrypt(absFile);
|
|
5106
5445
|
return new Set(Object.keys(decrypted));
|
|
5107
5446
|
}
|
|
@@ -5114,19 +5453,19 @@ function statusOf(entry, present) {
|
|
|
5114
5453
|
function badge(status) {
|
|
5115
5454
|
switch (status) {
|
|
5116
5455
|
case "set":
|
|
5117
|
-
return
|
|
5456
|
+
return chalk32.green("\u25CF");
|
|
5118
5457
|
case "missing":
|
|
5119
|
-
return
|
|
5458
|
+
return chalk32.yellow("\u25CB");
|
|
5120
5459
|
case "awaiting-input":
|
|
5121
|
-
return
|
|
5460
|
+
return chalk32.red("\u25CB");
|
|
5122
5461
|
}
|
|
5123
5462
|
}
|
|
5124
5463
|
|
|
5125
5464
|
// src/commands/secret/generate.ts
|
|
5126
5465
|
init_logger();
|
|
5127
|
-
import
|
|
5466
|
+
import chalk33 from "chalk";
|
|
5128
5467
|
async function secretGenerate(key, options) {
|
|
5129
|
-
const
|
|
5468
|
+
const env9 = resolveEnv(options.env);
|
|
5130
5469
|
const pkg = options.package ?? "@spfn/core";
|
|
5131
5470
|
let schema;
|
|
5132
5471
|
try {
|
|
@@ -5140,11 +5479,11 @@ async function secretGenerate(key, options) {
|
|
|
5140
5479
|
logger.warn("No generatable secrets found (none declare a `generate` strategy).");
|
|
5141
5480
|
return;
|
|
5142
5481
|
}
|
|
5143
|
-
logger.step(`Generating ${targets.length} secret(s) \u2192 ${await describeTarget(
|
|
5482
|
+
logger.step(`Generating ${targets.length} secret(s) \u2192 ${await describeTarget(env9)}`);
|
|
5144
5483
|
for (const entry of targets) {
|
|
5145
5484
|
const value = generateSecretValue(entry.generate);
|
|
5146
5485
|
try {
|
|
5147
|
-
await storeSecret(process.cwd(),
|
|
5486
|
+
await storeSecret(process.cwd(), env9, entry.key, value);
|
|
5148
5487
|
} catch (error) {
|
|
5149
5488
|
logger.error(`${entry.key}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5150
5489
|
process.exit(1);
|
|
@@ -5162,7 +5501,7 @@ function requireGeneratable(schema, key) {
|
|
|
5162
5501
|
process.exit(1);
|
|
5163
5502
|
}
|
|
5164
5503
|
if (!entry.generate) {
|
|
5165
|
-
logger.error(`${
|
|
5504
|
+
logger.error(`${chalk33.cyan(key)} has no generate strategy \u2014 it's an external value. Use \`spfn secret set ${key}\`.`);
|
|
5166
5505
|
process.exit(1);
|
|
5167
5506
|
}
|
|
5168
5507
|
return entry;
|
|
@@ -5170,9 +5509,9 @@ function requireGeneratable(schema, key) {
|
|
|
5170
5509
|
|
|
5171
5510
|
// src/commands/secret/rotate.ts
|
|
5172
5511
|
init_logger();
|
|
5173
|
-
import
|
|
5512
|
+
import chalk34 from "chalk";
|
|
5174
5513
|
async function secretRotate(key, options) {
|
|
5175
|
-
const
|
|
5514
|
+
const env9 = resolveEnv(options.env);
|
|
5176
5515
|
const pkg = options.package ?? "@spfn/core";
|
|
5177
5516
|
let schema;
|
|
5178
5517
|
try {
|
|
@@ -5185,10 +5524,10 @@ async function secretRotate(key, options) {
|
|
|
5185
5524
|
const generatable = targets.filter((entry) => entry.generate);
|
|
5186
5525
|
const external = targets.filter((entry) => !entry.generate);
|
|
5187
5526
|
if (generatable.length > 0) {
|
|
5188
|
-
logger.step(`Rotating ${generatable.length} secret(s) \u2192 ${await describeTarget(
|
|
5527
|
+
logger.step(`Rotating ${generatable.length} secret(s) \u2192 ${await describeTarget(env9)}`);
|
|
5189
5528
|
for (const entry of generatable) {
|
|
5190
5529
|
try {
|
|
5191
|
-
await storeSecret(process.cwd(),
|
|
5530
|
+
await storeSecret(process.cwd(), env9, entry.key, generateSecretValue(entry.generate));
|
|
5192
5531
|
} catch (error) {
|
|
5193
5532
|
logger.error(`${entry.key}: ${error instanceof Error ? error.message : String(error)}`);
|
|
5194
5533
|
process.exit(1);
|
|
@@ -5196,7 +5535,7 @@ async function secretRotate(key, options) {
|
|
|
5196
5535
|
}
|
|
5197
5536
|
}
|
|
5198
5537
|
for (const entry of external) {
|
|
5199
|
-
logger.warn(`${
|
|
5538
|
+
logger.warn(`${chalk34.cyan(entry.key)} is external \u2014 reissue it at the provider, then \`spfn secret set ${entry.key} --env ${env9}\`.`);
|
|
5200
5539
|
}
|
|
5201
5540
|
if (generatable.length > 0) {
|
|
5202
5541
|
logger.info("Rotation updates the source only \u2014 commit and deploy to apply.");
|
|
@@ -5218,12 +5557,12 @@ function requireEntry(schema, key) {
|
|
|
5218
5557
|
// src/commands/secret/keygen.ts
|
|
5219
5558
|
init_logger();
|
|
5220
5559
|
import { execa as execa11 } from "execa";
|
|
5221
|
-
import { existsSync as
|
|
5560
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync4 } from "fs";
|
|
5222
5561
|
import { homedir } from "os";
|
|
5223
|
-
import { dirname as dirname4, join as
|
|
5224
|
-
import
|
|
5562
|
+
import { dirname as dirname4, join as join26 } from "path";
|
|
5563
|
+
import chalk35 from "chalk";
|
|
5225
5564
|
function ageKeyFile() {
|
|
5226
|
-
return process.env.SOPS_AGE_KEY_FILE ??
|
|
5565
|
+
return process.env.SOPS_AGE_KEY_FILE ?? join26(homedir(), ".config", "sops", "age", "keys.txt");
|
|
5227
5566
|
}
|
|
5228
5567
|
async function ensureAgeInstalled() {
|
|
5229
5568
|
try {
|
|
@@ -5243,7 +5582,7 @@ async function secretKeygen() {
|
|
|
5243
5582
|
process.exit(1);
|
|
5244
5583
|
}
|
|
5245
5584
|
const keyFile = ageKeyFile();
|
|
5246
|
-
if (
|
|
5585
|
+
if (existsSync29(keyFile)) {
|
|
5247
5586
|
const { stdout } = await execa11("age-keygen", ["-y", keyFile]);
|
|
5248
5587
|
logger.warn(`age key file already exists: ${keyFile}`);
|
|
5249
5588
|
printPublicKeys(publicKeys(stdout));
|
|
@@ -5259,27 +5598,27 @@ function printPublicKeys(keys) {
|
|
|
5259
5598
|
logger.warn("Could not read the public key \u2014 run `age-keygen -y <file>` manually.");
|
|
5260
5599
|
return;
|
|
5261
5600
|
}
|
|
5262
|
-
console.log(
|
|
5601
|
+
console.log(chalk35.bold("\nPublic key(s):"));
|
|
5263
5602
|
for (const key of keys) {
|
|
5264
|
-
console.log(` ${
|
|
5603
|
+
console.log(` ${chalk35.cyan(key)}`);
|
|
5265
5604
|
}
|
|
5266
|
-
console.log(
|
|
5267
|
-
console.log(
|
|
5605
|
+
console.log(chalk35.dim("\nRegister it as a recipient:"));
|
|
5606
|
+
console.log(chalk35.dim(` spfn secret recipients add ${keys[0]}
|
|
5268
5607
|
`));
|
|
5269
5608
|
}
|
|
5270
5609
|
|
|
5271
5610
|
// src/commands/secret/recipients.ts
|
|
5272
5611
|
init_logger();
|
|
5273
|
-
import { existsSync as
|
|
5274
|
-
import { join as
|
|
5612
|
+
import { existsSync as existsSync30, readFileSync as readFileSync13, writeFileSync as writeFileSync18 } from "fs";
|
|
5613
|
+
import { join as join27 } from "path";
|
|
5275
5614
|
import { parse as parse3, stringify } from "yaml";
|
|
5276
|
-
import
|
|
5615
|
+
import chalk36 from "chalk";
|
|
5277
5616
|
var SOPS_CONFIG = ".sops.yaml";
|
|
5278
5617
|
var DEFAULT_PATH_REGEX = "secrets/.*\\.enc\\.json$";
|
|
5279
5618
|
var AGE_RECIPIENT = /^age1[0-9a-z]+$/;
|
|
5280
5619
|
async function secretRecipients(action, key, _options) {
|
|
5281
5620
|
const cwd = process.cwd();
|
|
5282
|
-
const configPath =
|
|
5621
|
+
const configPath = join27(cwd, SOPS_CONFIG);
|
|
5283
5622
|
if (action === "list") {
|
|
5284
5623
|
listRecipients(configPath);
|
|
5285
5624
|
return;
|
|
@@ -5311,25 +5650,25 @@ async function secretRecipients(action, key, _options) {
|
|
|
5311
5650
|
await reencrypt(cwd);
|
|
5312
5651
|
}
|
|
5313
5652
|
function listRecipients(configPath) {
|
|
5314
|
-
if (!
|
|
5653
|
+
if (!existsSync30(configPath)) {
|
|
5315
5654
|
logger.info(`No ${SOPS_CONFIG} found.`);
|
|
5316
5655
|
return;
|
|
5317
5656
|
}
|
|
5318
5657
|
const config = loadConfig(configPath);
|
|
5319
5658
|
const rules = config.creation_rules ?? [];
|
|
5320
|
-
console.log(
|
|
5659
|
+
console.log(chalk36.blue.bold(`
|
|
5321
5660
|
\u{1F465} Recipients (${SOPS_CONFIG})
|
|
5322
5661
|
`));
|
|
5323
5662
|
for (const rule of rules) {
|
|
5324
|
-
console.log(
|
|
5663
|
+
console.log(chalk36.dim(` ${rule.path_regex ?? "(any path)"}`));
|
|
5325
5664
|
for (const recipient of parseRecipients(rule.age)) {
|
|
5326
|
-
console.log(` ${
|
|
5665
|
+
console.log(` ${chalk36.cyan(recipient)}`);
|
|
5327
5666
|
}
|
|
5328
5667
|
}
|
|
5329
5668
|
console.log();
|
|
5330
5669
|
}
|
|
5331
5670
|
function loadConfig(configPath) {
|
|
5332
|
-
if (!
|
|
5671
|
+
if (!existsSync30(configPath)) {
|
|
5333
5672
|
return { creation_rules: [] };
|
|
5334
5673
|
}
|
|
5335
5674
|
return parse3(readFileSync13(configPath, "utf-8")) ?? { creation_rules: [] };
|
|
@@ -5364,8 +5703,8 @@ async function reencrypt(cwd) {
|
|
|
5364
5703
|
|
|
5365
5704
|
// src/commands/secret/check.ts
|
|
5366
5705
|
init_logger();
|
|
5367
|
-
import { join as
|
|
5368
|
-
import
|
|
5706
|
+
import { join as join28 } from "path";
|
|
5707
|
+
import chalk37 from "chalk";
|
|
5369
5708
|
init_env_file();
|
|
5370
5709
|
var COMMITTED_FILES = [".env", ".env.example"];
|
|
5371
5710
|
var PLACEHOLDER = /(your-|changeme|placeholder|example|<.*>)/i;
|
|
@@ -5383,35 +5722,35 @@ async function secretCheck(options) {
|
|
|
5383
5722
|
const issues = [];
|
|
5384
5723
|
const warnings = [];
|
|
5385
5724
|
for (const file of COMMITTED_FILES) {
|
|
5386
|
-
const parsed = parseEnvFile(
|
|
5725
|
+
const parsed = parseEnvFile(join28(cwd, file));
|
|
5387
5726
|
for (const [key, value] of Object.entries(parsed)) {
|
|
5388
5727
|
if (secretKeys.has(key) && value.length > 0 && !PLACEHOLDER.test(value)) {
|
|
5389
|
-
issues.push(`${
|
|
5728
|
+
issues.push(`${chalk37.cyan(key)} has a real value in committed ${chalk37.yellow(file)} \u2014 move it to the keychain/SOPS.`);
|
|
5390
5729
|
}
|
|
5391
5730
|
}
|
|
5392
5731
|
}
|
|
5393
|
-
const serverEnv = parseEnvFile(
|
|
5732
|
+
const serverEnv = parseEnvFile(join28(cwd, ".env.server"));
|
|
5394
5733
|
for (const key of secretKeys) {
|
|
5395
5734
|
const value = serverEnv[key];
|
|
5396
5735
|
if (value && !value.startsWith(KEYCHAIN_REF_PREFIX)) {
|
|
5397
|
-
warnings.push(`${
|
|
5736
|
+
warnings.push(`${chalk37.cyan(key)} is plaintext in .env.server \u2014 run \`spfn secret set ${key}\` to move it to the keychain.`);
|
|
5398
5737
|
}
|
|
5399
5738
|
}
|
|
5400
5739
|
report(issues, warnings, cwd);
|
|
5401
5740
|
}
|
|
5402
5741
|
function report(issues, warnings, cwd) {
|
|
5403
|
-
console.log(
|
|
5742
|
+
console.log(chalk37.blue.bold("\n\u{1F50D} Secret hygiene check\n"));
|
|
5404
5743
|
for (const issue of issues) {
|
|
5405
|
-
console.log(` ${
|
|
5744
|
+
console.log(` ${chalk37.red("\u2717")} ${issue}`);
|
|
5406
5745
|
}
|
|
5407
5746
|
for (const warning of warnings) {
|
|
5408
|
-
console.log(` ${
|
|
5747
|
+
console.log(` ${chalk37.yellow("\u26A0")} ${warning}`);
|
|
5409
5748
|
}
|
|
5410
5749
|
if (!hasSopsConfig(cwd)) {
|
|
5411
|
-
console.log(` ${
|
|
5750
|
+
console.log(` ${chalk37.dim("\u2139 no .sops.yaml \u2014 prod secrets need a backend (spfn secret keygen, or add a KMS rule)")}`);
|
|
5412
5751
|
}
|
|
5413
5752
|
if (issues.length === 0 && warnings.length === 0) {
|
|
5414
|
-
console.log(
|
|
5753
|
+
console.log(chalk37.green(" \u2713 No plaintext secret leaks found."));
|
|
5415
5754
|
}
|
|
5416
5755
|
console.log();
|
|
5417
5756
|
if (issues.length > 0) {
|