spfn 0.2.0-beta.61 → 0.2.0-beta.64
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 +644 -315
- 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/dist/templates/vercel/npmrc +4 -0
- package/dist/templates/vercel/route.ts +53 -0
- package/dist/templates/vercel/vercel.json +6 -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.64";
|
|
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
|
|
|
@@ -1678,16 +1860,17 @@ var init_migration_status = __esm({
|
|
|
1678
1860
|
});
|
|
1679
1861
|
|
|
1680
1862
|
// src/index.ts
|
|
1681
|
-
import { Command as
|
|
1863
|
+
import { Command as Command14 } from "commander";
|
|
1682
1864
|
|
|
1683
1865
|
// src/commands/create.ts
|
|
1684
1866
|
init_logger();
|
|
1685
1867
|
init_package_manager();
|
|
1686
1868
|
init_messages();
|
|
1687
|
-
|
|
1869
|
+
init_mode();
|
|
1870
|
+
import { Command as Command3, Option as Option2 } from "commander";
|
|
1688
1871
|
import { existsSync as existsSync13 } from "fs";
|
|
1689
1872
|
import { join as join13, resolve, dirname as dirname2 } from "path";
|
|
1690
|
-
import
|
|
1873
|
+
import prompts3 from "prompts";
|
|
1691
1874
|
import ora4 from "ora";
|
|
1692
1875
|
import { execa as execa3 } from "execa";
|
|
1693
1876
|
import chalk5 from "chalk";
|
|
@@ -1713,7 +1896,7 @@ async function createProject(projectName, options) {
|
|
|
1713
1896
|
console.log(chalk5.blue.bold("\n\u{1F680} Creating Next.js project with SPFN...\n"));
|
|
1714
1897
|
let pm = options.pm || detectPackageManager(cwd);
|
|
1715
1898
|
if (!options.yes && !options.pm) {
|
|
1716
|
-
const { selectedPm } = await
|
|
1899
|
+
const { selectedPm } = await prompts3({
|
|
1717
1900
|
type: "select",
|
|
1718
1901
|
name: "selectedPm",
|
|
1719
1902
|
message: "Which package manager do you want to use?",
|
|
@@ -1731,6 +1914,8 @@ async function createProject(projectName, options) {
|
|
|
1731
1914
|
pm = selectedPm;
|
|
1732
1915
|
}
|
|
1733
1916
|
logger.step(`Using package manager: ${pm}`);
|
|
1917
|
+
const mode = await selectScaffoldMode(options);
|
|
1918
|
+
logger.step(`Using scaffold mode: ${mode}`);
|
|
1734
1919
|
const workspaceRoot = pm === "pnpm" ? findPnpmWorkspaceRoot(cwd) : null;
|
|
1735
1920
|
const isInWorkspace = workspaceRoot !== null;
|
|
1736
1921
|
if (isInWorkspace) {
|
|
@@ -1829,7 +2014,7 @@ async function createProject(projectName, options) {
|
|
|
1829
2014
|
const initSpinner = ora4("Initializing SPFN...").start();
|
|
1830
2015
|
try {
|
|
1831
2016
|
const { initializeSpfn: initializeSpfn2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
1832
|
-
await initializeSpfn2({ yes: true, overwriteReadme: true });
|
|
2017
|
+
await initializeSpfn2({ yes: true, overwriteReadme: true, mode });
|
|
1833
2018
|
initSpinner.succeed("SPFN initialized");
|
|
1834
2019
|
} catch (error) {
|
|
1835
2020
|
initSpinner.fail("Failed to initialize SPFN");
|
|
@@ -1841,6 +2026,10 @@ async function createProject(projectName, options) {
|
|
|
1841
2026
|
console.log(` ${chalk5.cyan("cd")} ${projectName}`);
|
|
1842
2027
|
console.log(` ${chalk5.cyan("docker compose up -d")} ${chalk5.gray("# Start PostgreSQL & Redis")}`);
|
|
1843
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
|
+
}
|
|
1844
2033
|
console.log(` ${chalk5.cyan(`${getRunCommand(pm)} spfn:dev`)} ${chalk5.gray("# Start dev server")}
|
|
1845
2034
|
`);
|
|
1846
2035
|
console.log(chalk5.bold("Your app will be available at:\n"));
|
|
@@ -1857,7 +2046,7 @@ async function createProject(projectName, options) {
|
|
|
1857
2046
|
`);
|
|
1858
2047
|
console.log(chalk5.dim(" \u{1F310} Documentation: https://github.com/spfn/spfn\n"));
|
|
1859
2048
|
}
|
|
1860
|
-
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) => {
|
|
1861
2050
|
await createProject(projectName, options);
|
|
1862
2051
|
});
|
|
1863
2052
|
|
|
@@ -1869,7 +2058,7 @@ init_logger();
|
|
|
1869
2058
|
init_package_manager();
|
|
1870
2059
|
import { Command as Command4 } from "commander";
|
|
1871
2060
|
import { existsSync as existsSync16, readFileSync as readFileSync8, writeFileSync as writeFileSync10, mkdirSync, unlinkSync, rmSync, watch } from "fs";
|
|
1872
|
-
import { join as join17, relative, sep } from "path";
|
|
2061
|
+
import { join as join17, relative, sep as sep2 } from "path";
|
|
1873
2062
|
import { execa as execa6 } from "execa";
|
|
1874
2063
|
import chokidar from "chokidar";
|
|
1875
2064
|
|
|
@@ -2064,7 +2253,7 @@ async function resolveKeychainEnv(cwd) {
|
|
|
2064
2253
|
|
|
2065
2254
|
// src/commands/dev.ts
|
|
2066
2255
|
function ignoreDotfilesUnder(root) {
|
|
2067
|
-
return (watchedPath) => relative(root, watchedPath).split(
|
|
2256
|
+
return (watchedPath) => relative(root, watchedPath).split(sep2).some((segment) => segment.startsWith(".") && segment !== "." && segment !== "..");
|
|
2068
2257
|
}
|
|
2069
2258
|
function waitForReadyFile(filePath, timeoutMs = 3e4) {
|
|
2070
2259
|
return new Promise((resolve3, reject) => {
|
|
@@ -2687,16 +2876,95 @@ var startCommand = new Command6("start").description("Start SPFN production serv
|
|
|
2687
2876
|
}
|
|
2688
2877
|
});
|
|
2689
2878
|
|
|
2690
|
-
// src/commands/
|
|
2879
|
+
// src/commands/provision.ts
|
|
2691
2880
|
init_logger();
|
|
2881
|
+
init_package_manager();
|
|
2692
2882
|
import { Command as Command7 } from "commander";
|
|
2693
|
-
import { existsSync as existsSync19, writeFileSync as writeFileSync12 } from "fs";
|
|
2883
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync3, writeFileSync as writeFileSync12 } from "fs";
|
|
2694
2884
|
import { join as join20 } from "path";
|
|
2885
|
+
import { pathToFileURL } from "url";
|
|
2886
|
+
import { execa as execa9 } from "execa";
|
|
2887
|
+
var CONFIG_FILE_PATHS = [
|
|
2888
|
+
".spfn/server/server.config.mjs",
|
|
2889
|
+
".spfn/server/server.config.js",
|
|
2890
|
+
"src/server/server.config.ts",
|
|
2891
|
+
"src/server/server.config.js"
|
|
2892
|
+
];
|
|
2893
|
+
function findServerConfig(cwd) {
|
|
2894
|
+
return CONFIG_FILE_PATHS.map((path5) => join20(cwd, path5)).find(existsSync19);
|
|
2895
|
+
}
|
|
2896
|
+
var provisionCommand = new Command7("provision").description("Run deploy-time provisioning (seed/RBAC lifecycle hooks) \u2014 once per deploy, not per cold start").action(async () => {
|
|
2897
|
+
if (!process.env.NODE_ENV) {
|
|
2898
|
+
process.env.NODE_ENV = "production";
|
|
2899
|
+
}
|
|
2900
|
+
const cwd = process.cwd();
|
|
2901
|
+
const configPath = findServerConfig(cwd);
|
|
2902
|
+
if (!configPath) {
|
|
2903
|
+
logger.error("server.config not found.");
|
|
2904
|
+
logger.info(`Looked for: ${CONFIG_FILE_PATHS.join(", ")}`);
|
|
2905
|
+
logger.info('Run "spfn init" first, or run this command from the app root.');
|
|
2906
|
+
process.exit(1);
|
|
2907
|
+
}
|
|
2908
|
+
const { env: keychainEnv, missing: keychainMissing } = await resolveKeychainEnv(cwd);
|
|
2909
|
+
if (Object.keys(keychainEnv).length > 0) {
|
|
2910
|
+
logger.info(`[SPFN] Injecting ${Object.keys(keychainEnv).length} secret(s) from the keychain`);
|
|
2911
|
+
}
|
|
2912
|
+
if (keychainMissing.length > 0) {
|
|
2913
|
+
logger.warn(`[SPFN] Could not resolve keychain secret(s): ${keychainMissing.join(", ")} \u2014 run \`spfn secret set <KEY>\``);
|
|
2914
|
+
}
|
|
2915
|
+
const tempDir = join20(cwd, ".spfn");
|
|
2916
|
+
const runnerEntry = join20(tempDir, "provision.mjs");
|
|
2917
|
+
mkdirSync3(tempDir, { recursive: true });
|
|
2918
|
+
writeFileSync12(runnerEntry, `
|
|
2919
|
+
if (!process.env.NODE_ENV)
|
|
2920
|
+
{
|
|
2921
|
+
process.env.NODE_ENV = 'production';
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
// The Vercel Supabase integration injects POSTGRES_URL but not DATABASE_URL,
|
|
2925
|
+
// which is what SPFN reads. Map it so build-step provisioning connects too.
|
|
2926
|
+
process.env.DATABASE_URL ??= process.env.POSTGRES_URL;
|
|
2927
|
+
|
|
2928
|
+
// Load environment variables FIRST (before any imports that depend on them)
|
|
2929
|
+
await import('@spfn/core/config');
|
|
2930
|
+
|
|
2931
|
+
const { provisionInfrastructure } = await import('@spfn/core/server');
|
|
2932
|
+
|
|
2933
|
+
const config = (await import(${JSON.stringify(pathToFileURL(configPath).href)})).default;
|
|
2934
|
+
|
|
2935
|
+
await provisionInfrastructure(config);
|
|
2936
|
+
|
|
2937
|
+
// provisionInfrastructure leaves the DB pool open \u2014 exit explicitly.
|
|
2938
|
+
process.exit(0);
|
|
2939
|
+
`);
|
|
2940
|
+
logger.info(`[SPFN] Provisioning with config: ${configPath.replace(cwd + "/", "")}
|
|
2941
|
+
`);
|
|
2942
|
+
const pm = detectPackageManager(cwd);
|
|
2943
|
+
const runnerCmd = pm === "npm" ? "npx" : pm;
|
|
2944
|
+
const runnerArgs = pm === "npm" ? ["tsx", runnerEntry] : ["exec", "tsx", runnerEntry];
|
|
2945
|
+
const result = await execa9(runnerCmd, runnerArgs, {
|
|
2946
|
+
cwd,
|
|
2947
|
+
stdio: "inherit",
|
|
2948
|
+
reject: false,
|
|
2949
|
+
env: { ...process.env, ...keychainEnv }
|
|
2950
|
+
});
|
|
2951
|
+
if (result.exitCode !== 0) {
|
|
2952
|
+
logger.error(`Provisioning failed (exit code ${result.exitCode})`);
|
|
2953
|
+
process.exit(result.exitCode ?? 1);
|
|
2954
|
+
}
|
|
2955
|
+
logger.info("[SPFN] Provisioning complete");
|
|
2956
|
+
});
|
|
2957
|
+
|
|
2958
|
+
// src/commands/codegen.ts
|
|
2959
|
+
init_logger();
|
|
2960
|
+
import { Command as Command8 } from "commander";
|
|
2961
|
+
import { existsSync as existsSync20, writeFileSync as writeFileSync13 } from "fs";
|
|
2962
|
+
import { join as join21 } from "path";
|
|
2695
2963
|
import chalk10 from "chalk";
|
|
2696
2964
|
async function initCodegen(options) {
|
|
2697
2965
|
const cwd = process.cwd();
|
|
2698
|
-
const rcPath =
|
|
2699
|
-
if (
|
|
2966
|
+
const rcPath = join21(cwd, ".spfnrc.ts");
|
|
2967
|
+
if (existsSync20(rcPath)) {
|
|
2700
2968
|
logger.warn(".spfnrc.ts already exists");
|
|
2701
2969
|
logger.info("Edit manually to add custom generators");
|
|
2702
2970
|
process.exit(0);
|
|
@@ -2721,7 +2989,7 @@ export default defineConfig({
|
|
|
2721
2989
|
],
|
|
2722
2990
|
});
|
|
2723
2991
|
`;
|
|
2724
|
-
|
|
2992
|
+
writeFileSync13(rcPath, content);
|
|
2725
2993
|
console.log("\n" + chalk10.green.bold("\u2713 Created .spfnrc.ts\n"));
|
|
2726
2994
|
console.log(chalk10.gray("Configured the @spfn/core:route-map generator."));
|
|
2727
2995
|
if (options.withExample) {
|
|
@@ -2781,28 +3049,28 @@ async function runGenerators() {
|
|
|
2781
3049
|
await orchestrator.generateAll();
|
|
2782
3050
|
console.log("\n" + chalk10.green.bold("\u2713 Code generation completed"));
|
|
2783
3051
|
}
|
|
2784
|
-
var codegenCommand = new
|
|
3052
|
+
var codegenCommand = new Command8("codegen").description("Code generation management");
|
|
2785
3053
|
codegenCommand.command("init").description("Initialize .spfnrc.ts with codegen configuration").option("--with-example", "Show example custom generator usage").action(initCodegen);
|
|
2786
3054
|
codegenCommand.command("list").alias("ls").description("List registered code generators").action(listGenerators);
|
|
2787
3055
|
codegenCommand.command("run").description("Run code generators once (no watch mode)").action(runGenerators);
|
|
2788
3056
|
|
|
2789
3057
|
// src/commands/key.ts
|
|
2790
3058
|
init_logger();
|
|
2791
|
-
import { Command as
|
|
3059
|
+
import { Command as Command9 } from "commander";
|
|
2792
3060
|
import { execSync } from "child_process";
|
|
2793
3061
|
import chalk11 from "chalk";
|
|
2794
3062
|
|
|
2795
3063
|
// src/utils/secret-gen.ts
|
|
2796
|
-
import { randomBytes, randomUUID } from "crypto";
|
|
3064
|
+
import { randomBytes as randomBytes2, randomUUID } from "crypto";
|
|
2797
3065
|
function randomBase64Url(bytes) {
|
|
2798
|
-
return
|
|
3066
|
+
return randomBytes2(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
2799
3067
|
}
|
|
2800
3068
|
function generateSecretValue(spec) {
|
|
2801
3069
|
switch (spec) {
|
|
2802
3070
|
case "hex32":
|
|
2803
|
-
return
|
|
3071
|
+
return randomBytes2(32).toString("hex");
|
|
2804
3072
|
case "hex64":
|
|
2805
|
-
return
|
|
3073
|
+
return randomBytes2(64).toString("hex");
|
|
2806
3074
|
case "base64url32":
|
|
2807
3075
|
return randomBase64Url(32);
|
|
2808
3076
|
case "uuid":
|
|
@@ -2902,7 +3170,7 @@ function listPresets() {
|
|
|
2902
3170
|
console.log(chalk11.gray(" spfn key auth-encryption --copy"));
|
|
2903
3171
|
console.log();
|
|
2904
3172
|
}
|
|
2905
|
-
var generateValueCommand = new
|
|
3173
|
+
var generateValueCommand = new Command9("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) => {
|
|
2906
3174
|
const bytes = parseInt(options.bytes, 10);
|
|
2907
3175
|
if (isNaN(bytes) || bytes < 1 || bytes > 128) {
|
|
2908
3176
|
logger.error("Invalid bytes value. Must be between 1 and 128.");
|
|
@@ -2918,7 +3186,7 @@ var generateValueCommand = new Command8("generate").alias("gen").description("Ge
|
|
|
2918
3186
|
}
|
|
2919
3187
|
}
|
|
2920
3188
|
});
|
|
2921
|
-
var keyCommand = new
|
|
3189
|
+
var keyCommand = new Command9("key").alias("k").description("Generate secure random keys and secrets").argument("[preset]", `Preset type (use --list to see all)`).option("-l, --list", "List all available presets").option("-b, --bytes <number>", "Number of random bytes to generate", "32").option("-e, --env <name>", "Environment variable name").option("-c, --copy", "Copy to clipboard").action((preset, options) => {
|
|
2922
3190
|
if (options.list) {
|
|
2923
3191
|
listPresets();
|
|
2924
3192
|
return;
|
|
@@ -2950,15 +3218,15 @@ keyCommand.addCommand(generateValueCommand);
|
|
|
2950
3218
|
init_setup();
|
|
2951
3219
|
|
|
2952
3220
|
// src/commands/db/index.ts
|
|
2953
|
-
import { Command as
|
|
3221
|
+
import { Command as Command10 } from "commander";
|
|
2954
3222
|
|
|
2955
3223
|
// src/commands/db/generate.ts
|
|
2956
3224
|
import chalk13 from "chalk";
|
|
2957
3225
|
|
|
2958
3226
|
// src/commands/db/utils/drizzle.ts
|
|
2959
|
-
import { existsSync as
|
|
3227
|
+
import { existsSync as existsSync21, writeFileSync as writeFileSync14, unlinkSync as unlinkSync2 } from "fs";
|
|
2960
3228
|
import { spawn } from "child_process";
|
|
2961
|
-
import { pathToFileURL } from "url";
|
|
3229
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
2962
3230
|
import chalk12 from "chalk";
|
|
2963
3231
|
import ora6 from "ora";
|
|
2964
3232
|
import { env as env3 } from "@spfn/core/config";
|
|
@@ -3023,7 +3291,7 @@ function validateDatabasePrerequisites() {
|
|
|
3023
3291
|
}
|
|
3024
3292
|
}
|
|
3025
3293
|
async function runDrizzleCommand(command) {
|
|
3026
|
-
const hasUserConfig =
|
|
3294
|
+
const hasUserConfig = existsSync21("./drizzle.config.ts");
|
|
3027
3295
|
const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
|
|
3028
3296
|
const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
|
|
3029
3297
|
if (!hasUserConfig) {
|
|
@@ -3042,7 +3310,7 @@ async function runDrizzleCommand(command) {
|
|
|
3042
3310
|
expandGlobs: true,
|
|
3043
3311
|
autoDetectSchemas: true
|
|
3044
3312
|
});
|
|
3045
|
-
|
|
3313
|
+
writeFileSync14(tempConfigPath, configContent);
|
|
3046
3314
|
console.log(chalk12.dim("Using auto-generated Drizzle config\n"));
|
|
3047
3315
|
}
|
|
3048
3316
|
const args = command.split(" ");
|
|
@@ -3055,7 +3323,7 @@ async function runDrizzleCommand(command) {
|
|
|
3055
3323
|
env: shouldRelaxDbTls(env3.DATABASE_URL) ? { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" } : { ...process.env }
|
|
3056
3324
|
});
|
|
3057
3325
|
const cleanup = () => {
|
|
3058
|
-
if (!hasUserConfig &&
|
|
3326
|
+
if (!hasUserConfig && existsSync21(tempConfigPath)) {
|
|
3059
3327
|
unlinkSync2(tempConfigPath);
|
|
3060
3328
|
}
|
|
3061
3329
|
};
|
|
@@ -3094,7 +3362,7 @@ async function loadSchemaImports(schemaFiles) {
|
|
|
3094
3362
|
}
|
|
3095
3363
|
const imports = {};
|
|
3096
3364
|
for (const file of schemaFiles) {
|
|
3097
|
-
const moduleUrl =
|
|
3365
|
+
const moduleUrl = pathToFileURL2(file).href;
|
|
3098
3366
|
const mod = await import(moduleUrl);
|
|
3099
3367
|
for (const [key, value] of Object.entries(mod)) {
|
|
3100
3368
|
if (key !== "default") {
|
|
@@ -3137,7 +3405,7 @@ async function dbGenerate() {
|
|
|
3137
3405
|
|
|
3138
3406
|
// src/commands/db/push.ts
|
|
3139
3407
|
import chalk15 from "chalk";
|
|
3140
|
-
import
|
|
3408
|
+
import prompts4 from "prompts";
|
|
3141
3409
|
import "@spfn/core/config";
|
|
3142
3410
|
import { loadEnv as loadEnv4 } from "@spfn/core/server";
|
|
3143
3411
|
import { sql } from "drizzle-orm";
|
|
@@ -3322,7 +3590,7 @@ async function dbPush(options = {}) {
|
|
|
3322
3590
|
console.log(chalk15.red(` ${stmt.sql.replace(/\s+/g, " ").trim()}`));
|
|
3323
3591
|
console.log(chalk15.dim(` \u2192 ${stmt.reason}`));
|
|
3324
3592
|
}
|
|
3325
|
-
const { confirm } = await
|
|
3593
|
+
const { confirm } = await prompts4({
|
|
3326
3594
|
type: "confirm",
|
|
3327
3595
|
name: "confirm",
|
|
3328
3596
|
message: "Apply destructive changes?",
|
|
@@ -3375,8 +3643,8 @@ async function applyFunctionMigrations(plans) {
|
|
|
3375
3643
|
|
|
3376
3644
|
// src/commands/db/migrate.ts
|
|
3377
3645
|
import chalk20 from "chalk";
|
|
3378
|
-
import { join as
|
|
3379
|
-
import { existsSync as
|
|
3646
|
+
import { join as join22 } from "path";
|
|
3647
|
+
import { existsSync as existsSync23 } from "fs";
|
|
3380
3648
|
|
|
3381
3649
|
// src/commands/db/backup.ts
|
|
3382
3650
|
import { promises as fs3 } from "fs";
|
|
@@ -3388,7 +3656,7 @@ import ora7 from "ora";
|
|
|
3388
3656
|
// src/commands/db/utils/database.ts
|
|
3389
3657
|
import net from "net";
|
|
3390
3658
|
import chalk16 from "chalk";
|
|
3391
|
-
import
|
|
3659
|
+
import prompts5 from "prompts";
|
|
3392
3660
|
function parseDatabaseUrl2(dbUrl) {
|
|
3393
3661
|
try {
|
|
3394
3662
|
const url = new URL(dbUrl);
|
|
@@ -3412,7 +3680,7 @@ async function confirmDangerousTarget(dbInfo) {
|
|
|
3412
3680
|
if (!isRemoteHost(dbInfo.host) && process.env.NODE_ENV !== "production") {
|
|
3413
3681
|
return;
|
|
3414
3682
|
}
|
|
3415
|
-
const { typed } = await
|
|
3683
|
+
const { typed } = await prompts5({
|
|
3416
3684
|
type: "text",
|
|
3417
3685
|
name: "typed",
|
|
3418
3686
|
message: `Remote/production database detected. Type the database name "${dbInfo.database}" to continue:`
|
|
@@ -3469,7 +3737,7 @@ function formatTimestamp() {
|
|
|
3469
3737
|
|
|
3470
3738
|
// src/commands/db/utils/backup-files.ts
|
|
3471
3739
|
import { promises as fs2 } from "fs";
|
|
3472
|
-
import { existsSync as
|
|
3740
|
+
import { existsSync as existsSync22 } from "fs";
|
|
3473
3741
|
import path2 from "path";
|
|
3474
3742
|
import chalk18 from "chalk";
|
|
3475
3743
|
|
|
@@ -3569,7 +3837,7 @@ async function ensureBackupInGitignore() {
|
|
|
3569
3837
|
const gitignorePath = path2.join(process.cwd(), ".gitignore");
|
|
3570
3838
|
try {
|
|
3571
3839
|
let content = "";
|
|
3572
|
-
let exists =
|
|
3840
|
+
let exists = existsSync22(gitignorePath);
|
|
3573
3841
|
if (exists) {
|
|
3574
3842
|
content = await fs2.readFile(gitignorePath, "utf-8");
|
|
3575
3843
|
}
|
|
@@ -3591,7 +3859,7 @@ async function ensureBackupDir() {
|
|
|
3591
3859
|
try {
|
|
3592
3860
|
await fs2.mkdir(backupDir, { recursive: true });
|
|
3593
3861
|
const gitignorePath = path2.join(backupDir, ".gitignore");
|
|
3594
|
-
const gitignoreExists =
|
|
3862
|
+
const gitignoreExists = existsSync22(gitignorePath);
|
|
3595
3863
|
if (!gitignoreExists) {
|
|
3596
3864
|
await fs2.writeFile(gitignorePath, "# Ignore all backup files\n*.sql\n*.dump\n*.meta.json\n");
|
|
3597
3865
|
}
|
|
@@ -3785,8 +4053,8 @@ async function dbMigrate(options = {}) {
|
|
|
3785
4053
|
await executeFunctionMigrations2(loadFunctionMigrationPlans2(functions));
|
|
3786
4054
|
console.log(chalk20.green("\u2705 Function migrations applied\n"));
|
|
3787
4055
|
}
|
|
3788
|
-
const projectMigrationsDir =
|
|
3789
|
-
if (
|
|
4056
|
+
const projectMigrationsDir = join22(process.cwd(), "src/server/drizzle");
|
|
4057
|
+
if (existsSync23(projectMigrationsDir)) {
|
|
3790
4058
|
const projConn = postgres.default(env5.DATABASE_URL, { max: 1 });
|
|
3791
4059
|
const projDb = drizzle({ client: projConn });
|
|
3792
4060
|
try {
|
|
@@ -3846,7 +4114,7 @@ async function dbStatus() {
|
|
|
3846
4114
|
|
|
3847
4115
|
// src/commands/db/studio.ts
|
|
3848
4116
|
import chalk22 from "chalk";
|
|
3849
|
-
import { existsSync as
|
|
4117
|
+
import { existsSync as existsSync24, writeFileSync as writeFileSync15, unlinkSync as unlinkSync3 } from "fs";
|
|
3850
4118
|
import { spawn as spawn3 } from "child_process";
|
|
3851
4119
|
import { env as env6 } from "@spfn/core/config";
|
|
3852
4120
|
async function dbStudio(requestedPort) {
|
|
@@ -3864,7 +4132,7 @@ async function dbStudio(requestedPort) {
|
|
|
3864
4132
|
console.error(chalk22.red(error instanceof Error ? error.message : "Failed to find available port"));
|
|
3865
4133
|
process.exit(1);
|
|
3866
4134
|
}
|
|
3867
|
-
const hasUserConfig =
|
|
4135
|
+
const hasUserConfig = existsSync24("./drizzle.config.ts");
|
|
3868
4136
|
const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
|
|
3869
4137
|
try {
|
|
3870
4138
|
const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
|
|
@@ -3881,7 +4149,7 @@ async function dbStudio(requestedPort) {
|
|
|
3881
4149
|
expandGlobs: true
|
|
3882
4150
|
// Expand glob patterns for Studio compatibility
|
|
3883
4151
|
});
|
|
3884
|
-
|
|
4152
|
+
writeFileSync15(tempConfigPath, configContent);
|
|
3885
4153
|
console.log(chalk22.dim("Using auto-generated Drizzle config\n"));
|
|
3886
4154
|
}
|
|
3887
4155
|
const studioProcess = spawn3("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
|
|
@@ -3890,7 +4158,7 @@ async function dbStudio(requestedPort) {
|
|
|
3890
4158
|
env: shouldRelaxDbTls(env6.DATABASE_URL) ? { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" } : { ...process.env }
|
|
3891
4159
|
});
|
|
3892
4160
|
const cleanup = () => {
|
|
3893
|
-
if (!hasUserConfig &&
|
|
4161
|
+
if (!hasUserConfig && existsSync24(tempConfigPath)) {
|
|
3894
4162
|
unlinkSync3(tempConfigPath);
|
|
3895
4163
|
}
|
|
3896
4164
|
};
|
|
@@ -3920,7 +4188,7 @@ async function dbStudio(requestedPort) {
|
|
|
3920
4188
|
process.exit(0);
|
|
3921
4189
|
});
|
|
3922
4190
|
} catch (error) {
|
|
3923
|
-
if (!hasUserConfig &&
|
|
4191
|
+
if (!hasUserConfig && existsSync24(tempConfigPath)) {
|
|
3924
4192
|
unlinkSync3(tempConfigPath);
|
|
3925
4193
|
}
|
|
3926
4194
|
console.error(chalk22.red("\u274C Failed to start Drizzle Studio"));
|
|
@@ -3931,7 +4199,7 @@ async function dbStudio(requestedPort) {
|
|
|
3931
4199
|
|
|
3932
4200
|
// src/commands/db/drop.ts
|
|
3933
4201
|
import chalk23 from "chalk";
|
|
3934
|
-
import
|
|
4202
|
+
import prompts6 from "prompts";
|
|
3935
4203
|
import { env as env7 } from "@spfn/core/config";
|
|
3936
4204
|
import { loadEnv as loadEnv7 } from "@spfn/core/server";
|
|
3937
4205
|
async function dbDrop() {
|
|
@@ -3945,7 +4213,7 @@ async function dbDrop() {
|
|
|
3945
4213
|
console.log(chalk23.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
|
|
3946
4214
|
console.log(chalk23.dim(` Target: ${dbInfo.database} @ ${dbInfo.host}:${dbInfo.port}
|
|
3947
4215
|
`));
|
|
3948
|
-
const { confirm } = await
|
|
4216
|
+
const { confirm } = await prompts6({
|
|
3949
4217
|
type: "confirm",
|
|
3950
4218
|
name: "confirm",
|
|
3951
4219
|
message: "Are you sure you want to drop all tables?",
|
|
@@ -3984,7 +4252,7 @@ import path4 from "path";
|
|
|
3984
4252
|
import { spawn as spawn4 } from "child_process";
|
|
3985
4253
|
import chalk25 from "chalk";
|
|
3986
4254
|
import ora9 from "ora";
|
|
3987
|
-
import
|
|
4255
|
+
import prompts7 from "prompts";
|
|
3988
4256
|
import { env as env8 } from "@spfn/core/config";
|
|
3989
4257
|
import { loadEnv as loadEnv8 } from "@spfn/core/server";
|
|
3990
4258
|
async function dbRestore(backupFile, options = {}) {
|
|
@@ -4003,7 +4271,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4003
4271
|
console.log(chalk25.yellow("No backups found in ./backups directory"));
|
|
4004
4272
|
process.exit(0);
|
|
4005
4273
|
}
|
|
4006
|
-
const { selected } = await
|
|
4274
|
+
const { selected } = await prompts7({
|
|
4007
4275
|
type: "select",
|
|
4008
4276
|
name: "selected",
|
|
4009
4277
|
message: "Select backup to restore:",
|
|
@@ -4066,7 +4334,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4066
4334
|
}
|
|
4067
4335
|
}
|
|
4068
4336
|
const dbInfo = parseDatabaseUrl2(dbUrl);
|
|
4069
|
-
const { confirm } = await
|
|
4337
|
+
const { confirm } = await prompts7({
|
|
4070
4338
|
type: "confirm",
|
|
4071
4339
|
name: "confirm",
|
|
4072
4340
|
message: chalk25.yellow(`\u26A0\uFE0F This will replace all data in "${dbInfo.database}" @ ${dbInfo.host}:${dbInfo.port}. Continue?`),
|
|
@@ -4249,7 +4517,7 @@ async function dbBackupList() {
|
|
|
4249
4517
|
import { promises as fs4 } from "fs";
|
|
4250
4518
|
import chalk27 from "chalk";
|
|
4251
4519
|
import ora10 from "ora";
|
|
4252
|
-
import
|
|
4520
|
+
import prompts8 from "prompts";
|
|
4253
4521
|
async function dbBackupClean(options) {
|
|
4254
4522
|
console.log(chalk27.blue("\u{1F9F9} Cleaning old backups...\n"));
|
|
4255
4523
|
const backups = await listBackupFiles();
|
|
@@ -4279,7 +4547,7 @@ async function dbBackupClean(options) {
|
|
|
4279
4547
|
toDelete.forEach((backup) => {
|
|
4280
4548
|
console.log(chalk27.gray(` - ${backup.name} (${backup.size})`));
|
|
4281
4549
|
});
|
|
4282
|
-
const { confirm } = await
|
|
4550
|
+
const { confirm } = await prompts8({
|
|
4283
4551
|
type: "confirm",
|
|
4284
4552
|
name: "confirm",
|
|
4285
4553
|
message: "\nProceed with deletion?",
|
|
@@ -4307,8 +4575,8 @@ async function dbBackupClean(options) {
|
|
|
4307
4575
|
}
|
|
4308
4576
|
|
|
4309
4577
|
// src/commands/db/reindex.ts
|
|
4310
|
-
import { existsSync as
|
|
4311
|
-
import { join as
|
|
4578
|
+
import { existsSync as existsSync25, readFileSync as readFileSync10, writeFileSync as writeFileSync16, renameSync, copyFileSync } from "fs";
|
|
4579
|
+
import { join as join23 } from "path";
|
|
4312
4580
|
import chalk28 from "chalk";
|
|
4313
4581
|
import { loadEnv as loadEnv9 } from "@spfn/core/server";
|
|
4314
4582
|
function isTimestampPrefix(tag) {
|
|
@@ -4330,8 +4598,8 @@ async function dbReindex(options = {}) {
|
|
|
4330
4598
|
const { getDrizzleConfig } = await import("@spfn/core/db");
|
|
4331
4599
|
const config = getDrizzleConfig({ disablePackageDiscovery: true });
|
|
4332
4600
|
const outDir = config.out;
|
|
4333
|
-
const journalPath =
|
|
4334
|
-
if (!
|
|
4601
|
+
const journalPath = join23(outDir, "meta", "_journal.json");
|
|
4602
|
+
if (!existsSync25(journalPath)) {
|
|
4335
4603
|
console.error(chalk28.red("\u274C No _journal.json found at:"), journalPath);
|
|
4336
4604
|
console.log(chalk28.yellow("\u{1F4A1} Run `spfn db generate` first to create migrations"));
|
|
4337
4605
|
process.exit(1);
|
|
@@ -4352,14 +4620,14 @@ async function dbReindex(options = {}) {
|
|
|
4352
4620
|
const { prefix: oldPrefix, suffix } = parseTag(entry.tag);
|
|
4353
4621
|
const newPrefix = String(entry.when);
|
|
4354
4622
|
const newTag = suffix ? `${newPrefix}_${suffix}` : newPrefix;
|
|
4355
|
-
const oldSql =
|
|
4356
|
-
const newSql =
|
|
4357
|
-
if (
|
|
4623
|
+
const oldSql = join23(outDir, `${entry.tag}.sql`);
|
|
4624
|
+
const newSql = join23(outDir, `${newTag}.sql`);
|
|
4625
|
+
if (existsSync25(oldSql)) {
|
|
4358
4626
|
renames.push({ type: "sql", from: oldSql, to: newSql });
|
|
4359
4627
|
}
|
|
4360
|
-
const oldSnapshot =
|
|
4361
|
-
const newSnapshot =
|
|
4362
|
-
if (
|
|
4628
|
+
const oldSnapshot = join23(outDir, "meta", `${oldPrefix}_snapshot.json`);
|
|
4629
|
+
const newSnapshot = join23(outDir, "meta", `${newPrefix}_snapshot.json`);
|
|
4630
|
+
if (existsSync25(oldSnapshot)) {
|
|
4363
4631
|
renames.push({ type: "snapshot", from: oldSnapshot, to: newSnapshot });
|
|
4364
4632
|
}
|
|
4365
4633
|
tagUpdates.push({ idx: entry.idx, oldTag: entry.tag, newTag });
|
|
@@ -4402,13 +4670,13 @@ async function dbReindex(options = {}) {
|
|
|
4402
4670
|
entry.tag = update.newTag;
|
|
4403
4671
|
}
|
|
4404
4672
|
}
|
|
4405
|
-
|
|
4673
|
+
writeFileSync16(journalPath, JSON.stringify(journal, null, 2) + "\n");
|
|
4406
4674
|
console.log(chalk28.green(`
|
|
4407
4675
|
\u2705 Reindex complete \u2014 ${tagUpdates.length} migration(s) converted to timestamp prefix.`));
|
|
4408
4676
|
}
|
|
4409
4677
|
|
|
4410
4678
|
// src/commands/db/index.ts
|
|
4411
|
-
var dbCommand = new
|
|
4679
|
+
var dbCommand = new Command10("db").description("Database management commands (wraps Drizzle Kit)");
|
|
4412
4680
|
dbCommand.command("generate").alias("g").description("Generate database migrations from schema changes").action(dbGenerate);
|
|
4413
4681
|
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));
|
|
4414
4682
|
dbCommand.command("migrate").alias("m").description("Run pending migrations").option("--with-backup", "Create backup before running migrations").action((options) => dbMigrate(options));
|
|
@@ -4424,54 +4692,114 @@ dbCommand.command("reindex").description("Convert migration files from sequentia
|
|
|
4424
4692
|
|
|
4425
4693
|
// src/commands/add.ts
|
|
4426
4694
|
init_package_manager();
|
|
4427
|
-
import { Command as
|
|
4428
|
-
import { existsSync as
|
|
4429
|
-
import { join as
|
|
4430
|
-
import { execa as
|
|
4431
|
-
import
|
|
4695
|
+
import { Command as Command11 } from "commander";
|
|
4696
|
+
import { existsSync as existsSync27, readFileSync as readFileSync11 } from "fs";
|
|
4697
|
+
import { join as join25 } from "path";
|
|
4698
|
+
import { execa as execa10 } from "execa";
|
|
4699
|
+
import chalk30 from "chalk";
|
|
4432
4700
|
import ora11 from "ora";
|
|
4701
|
+
|
|
4702
|
+
// src/commands/add-vercel.ts
|
|
4703
|
+
init_templates();
|
|
4704
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync26, mkdirSync as mkdirSync4 } from "fs";
|
|
4705
|
+
import { dirname as dirname3, join as join24 } from "path";
|
|
4706
|
+
import chalk29 from "chalk";
|
|
4707
|
+
var FILES = [
|
|
4708
|
+
{
|
|
4709
|
+
template: "route.ts",
|
|
4710
|
+
target: join24("src", "app", "api", "backend", "[[...route]]", "route.ts"),
|
|
4711
|
+
description: "hono/vercel adapter (mounts the SPFN app under /api/backend)"
|
|
4712
|
+
},
|
|
4713
|
+
{
|
|
4714
|
+
template: "vercel.json",
|
|
4715
|
+
target: "vercel.json",
|
|
4716
|
+
description: "Vercel build config (pnpm spfn:build)"
|
|
4717
|
+
},
|
|
4718
|
+
{
|
|
4719
|
+
template: "npmrc",
|
|
4720
|
+
target: ".npmrc",
|
|
4721
|
+
description: "@spfn registry auth (reads GITEA_NPM_TOKEN from env)"
|
|
4722
|
+
}
|
|
4723
|
+
];
|
|
4724
|
+
async function addVercel() {
|
|
4725
|
+
console.log(chalk29.blue("\n\u{1F4E6} Setting up the Vercel serverless target...\n"));
|
|
4726
|
+
const cwd = process.cwd();
|
|
4727
|
+
const templatesDir = join24(findTemplatesPath(), "vercel");
|
|
4728
|
+
for (const file of FILES) {
|
|
4729
|
+
const targetPath = join24(cwd, file.target);
|
|
4730
|
+
if (existsSync26(targetPath)) {
|
|
4731
|
+
console.log(chalk29.yellow(`\u23ED\uFE0F ${file.target} already exists \u2014 skipped (not overwritten)`));
|
|
4732
|
+
continue;
|
|
4733
|
+
}
|
|
4734
|
+
mkdirSync4(dirname3(targetPath), { recursive: true });
|
|
4735
|
+
copyFileSync2(join24(templatesDir, file.template), targetPath);
|
|
4736
|
+
console.log(chalk29.green(`\u2705 ${file.target}`) + chalk29.gray(` \u2014 ${file.description}`));
|
|
4737
|
+
}
|
|
4738
|
+
console.log(chalk29.green("\n\u2705 Vercel target ready!\n"));
|
|
4739
|
+
console.log(chalk29.cyan("\u{1F4DA} Next steps:"));
|
|
4740
|
+
console.log(chalk29.gray(" 1. Make sure `hono` is a direct dependency (hono/vercel must resolve from the app):"));
|
|
4741
|
+
console.log(chalk29.gray(" pnpm add hono"));
|
|
4742
|
+
console.log(chalk29.gray(" 2. Set Vercel project env vars:"));
|
|
4743
|
+
console.log(chalk29.gray(" GITEA_NPM_TOKEN \u2014 @spfn registry token (install step)"));
|
|
4744
|
+
console.log(chalk29.gray(" SPFN_AUTH_* \u2014 auth secrets (same as local .env.server)"));
|
|
4745
|
+
console.log(chalk29.gray(" SPFN_API_URL \u2014 https://<your-domain>/api/backend (RPC proxy origin)"));
|
|
4746
|
+
console.log(chalk29.gray(" DATABASE_URL \u2014 set automatically from POSTGRES_URL if you use the"));
|
|
4747
|
+
console.log(chalk29.gray(" Vercel Supabase integration (mapped in route.ts)"));
|
|
4748
|
+
console.log(chalk29.gray(" 3. Run migrations against the DIRECT (non-pooler) connection before deploying:"));
|
|
4749
|
+
console.log(chalk29.gray(" DATABASE_URL=<direct-url> pnpm spfn db migrate"));
|
|
4750
|
+
console.log(chalk29.gray(" 4. Seed/RBAC provisioning runs once per deploy, not per cold start:"));
|
|
4751
|
+
console.log(chalk29.gray(" pnpm spfn provision (e.g. from the build step or a deploy hook)"));
|
|
4752
|
+
console.log(chalk29.gray(" 5. Jobs (config.jobs) are NOT processed on serverless \u2014 drain the queue from a"));
|
|
4753
|
+
console.log(chalk29.gray(" scheduled endpoint (Vercel Cron) or run workers on an always-on target.\n"));
|
|
4754
|
+
}
|
|
4755
|
+
|
|
4756
|
+
// src/commands/add.ts
|
|
4433
4757
|
async function addPackage(packageName) {
|
|
4758
|
+
if (packageName === "vercel") {
|
|
4759
|
+
await addVercel();
|
|
4760
|
+
return;
|
|
4761
|
+
}
|
|
4434
4762
|
if (!packageName.includes("/")) {
|
|
4435
|
-
console.error(
|
|
4436
|
-
console.log(
|
|
4437
|
-
console.log(
|
|
4438
|
-
console.log(
|
|
4763
|
+
console.error(chalk30.red("\u274C Please specify full package name"));
|
|
4764
|
+
console.log(chalk30.yellow("\n\u{1F4A1} Examples:"));
|
|
4765
|
+
console.log(chalk30.gray(" pnpm spfn add @spfn/cms"));
|
|
4766
|
+
console.log(chalk30.gray(" pnpm spfn add @mycompany/spfn-analytics"));
|
|
4439
4767
|
process.exit(1);
|
|
4440
4768
|
}
|
|
4441
|
-
console.log(
|
|
4769
|
+
console.log(chalk30.blue(`
|
|
4442
4770
|
\u{1F4E6} Setting up ${packageName}...
|
|
4443
4771
|
`));
|
|
4444
|
-
const pkgPath =
|
|
4445
|
-
const pkgJsonPath =
|
|
4446
|
-
if (!
|
|
4772
|
+
const pkgPath = join25(process.cwd(), "node_modules", ...packageName.split("/"));
|
|
4773
|
+
const pkgJsonPath = join25(pkgPath, "package.json");
|
|
4774
|
+
if (!existsSync27(pkgJsonPath)) {
|
|
4447
4775
|
const pm = detectPackageManager(process.cwd());
|
|
4448
4776
|
const installSpinner = ora11("Installing package...").start();
|
|
4449
4777
|
try {
|
|
4450
|
-
await
|
|
4778
|
+
await execa10(pm, ["add", packageName]);
|
|
4451
4779
|
installSpinner.succeed("Package installed");
|
|
4452
4780
|
} catch (error) {
|
|
4453
4781
|
installSpinner.fail("Failed to install package");
|
|
4454
|
-
console.error(
|
|
4782
|
+
console.error(chalk30.red(error instanceof Error ? error.message : "Unknown error"));
|
|
4455
4783
|
process.exit(1);
|
|
4456
4784
|
}
|
|
4457
4785
|
} else {
|
|
4458
|
-
console.log(
|
|
4786
|
+
console.log(chalk30.gray("\u2713 Package already installed (using local version)\n"));
|
|
4459
4787
|
}
|
|
4460
|
-
if (!
|
|
4461
|
-
console.error(
|
|
4788
|
+
if (!existsSync27(pkgJsonPath)) {
|
|
4789
|
+
console.error(chalk30.red(`\u274C Package ${packageName} not found after installation`));
|
|
4462
4790
|
process.exit(1);
|
|
4463
4791
|
}
|
|
4464
4792
|
const pkgJson = JSON.parse(readFileSync11(pkgJsonPath, "utf-8"));
|
|
4465
4793
|
if (pkgJson.spfn?.migrations) {
|
|
4466
|
-
console.log(
|
|
4794
|
+
console.log(chalk30.blue(`
|
|
4467
4795
|
\u{1F5C4}\uFE0F Setting up database for ${packageName}...
|
|
4468
4796
|
`));
|
|
4469
4797
|
const { env: env9 } = await import("@spfn/core/config");
|
|
4470
4798
|
if (!env9.DATABASE_URL) {
|
|
4471
|
-
console.log(
|
|
4472
|
-
console.log(
|
|
4473
|
-
console.log(
|
|
4474
|
-
console.log(
|
|
4799
|
+
console.log(chalk30.yellow("\u26A0\uFE0F DATABASE_URL not found \u2014 skipping database setup."));
|
|
4800
|
+
console.log(chalk30.yellow(` ${packageName} tables are created by its bundled migrations, not by schema push.`));
|
|
4801
|
+
console.log(chalk30.cyan(" Once DATABASE_URL is set, run: pnpm spfn db migrate"));
|
|
4802
|
+
console.log(chalk30.gray(" (check state anytime with: pnpm spfn db status)\n"));
|
|
4475
4803
|
} else {
|
|
4476
4804
|
const { discoverFunctionMigrations: discoverFunctionMigrations2, loadFunctionMigrationPlans: loadFunctionMigrationPlans2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
4477
4805
|
const functions = discoverFunctionMigrations2(process.cwd());
|
|
@@ -4483,31 +4811,31 @@ async function addPackage(packageName) {
|
|
|
4483
4811
|
spinner.succeed("Migrations applied");
|
|
4484
4812
|
} catch (error) {
|
|
4485
4813
|
spinner.fail("Failed to apply migrations");
|
|
4486
|
-
console.error(
|
|
4814
|
+
console.error(chalk30.red(error instanceof Error ? error.message : "Unknown error"));
|
|
4487
4815
|
process.exit(1);
|
|
4488
4816
|
}
|
|
4489
4817
|
} else {
|
|
4490
|
-
console.log(
|
|
4818
|
+
console.log(chalk30.gray("\u2139\uFE0F No migrations found for this package"));
|
|
4491
4819
|
}
|
|
4492
4820
|
}
|
|
4493
4821
|
} else {
|
|
4494
|
-
console.log(
|
|
4822
|
+
console.log(chalk30.gray("\n\u2139\uFE0F No database migrations to apply"));
|
|
4495
4823
|
}
|
|
4496
|
-
console.log(
|
|
4824
|
+
console.log(chalk30.green(`
|
|
4497
4825
|
\u2705 ${packageName} installed successfully!
|
|
4498
4826
|
`));
|
|
4499
4827
|
if (pkgJson.spfn?.setupMessage) {
|
|
4500
|
-
console.log(
|
|
4828
|
+
console.log(chalk30.cyan("\u{1F4DA} Setup Guide:"));
|
|
4501
4829
|
console.log(pkgJson.spfn.setupMessage);
|
|
4502
4830
|
console.log();
|
|
4503
4831
|
}
|
|
4504
4832
|
}
|
|
4505
|
-
var addCommand = new
|
|
4833
|
+
var addCommand = new Command11("add").description("Install and set up SPFN ecosystem packages, or scaffold a deploy target (vercel)").argument("<package>", 'Package name (e.g., @spfn/cms, @mycompany/spfn-analytics) or "vercel"').action(addPackage);
|
|
4506
4834
|
|
|
4507
4835
|
// src/commands/env.ts
|
|
4508
|
-
import { Command as
|
|
4509
|
-
import
|
|
4510
|
-
import { existsSync as
|
|
4836
|
+
import { Command as Command12 } from "commander";
|
|
4837
|
+
import chalk31 from "chalk";
|
|
4838
|
+
import { existsSync as existsSync28, readFileSync as readFileSync12, writeFileSync as writeFileSync17 } from "fs";
|
|
4511
4839
|
import { resolve as resolve2 } from "path";
|
|
4512
4840
|
import { parse as parse2 } from "dotenv";
|
|
4513
4841
|
|
|
@@ -4563,26 +4891,26 @@ function getEnvFilesForEnvironment(nodeEnv) {
|
|
|
4563
4891
|
}
|
|
4564
4892
|
function formatType(type) {
|
|
4565
4893
|
const typeColors = {
|
|
4566
|
-
string:
|
|
4567
|
-
number:
|
|
4568
|
-
boolean:
|
|
4569
|
-
url:
|
|
4570
|
-
enum:
|
|
4571
|
-
json:
|
|
4894
|
+
string: chalk31.green,
|
|
4895
|
+
number: chalk31.blue,
|
|
4896
|
+
boolean: chalk31.yellow,
|
|
4897
|
+
url: chalk31.cyan,
|
|
4898
|
+
enum: chalk31.magenta,
|
|
4899
|
+
json: chalk31.red
|
|
4572
4900
|
};
|
|
4573
|
-
return (typeColors[type] ||
|
|
4901
|
+
return (typeColors[type] || chalk31.white)(type);
|
|
4574
4902
|
}
|
|
4575
4903
|
function formatDefault(value, type) {
|
|
4576
4904
|
if (value === void 0) {
|
|
4577
|
-
return
|
|
4905
|
+
return chalk31.dim("(none)");
|
|
4578
4906
|
}
|
|
4579
4907
|
if (type === "string" || type === "url") {
|
|
4580
|
-
return
|
|
4908
|
+
return chalk31.green(`"${value}"`);
|
|
4581
4909
|
}
|
|
4582
4910
|
if (type === "boolean") {
|
|
4583
|
-
return value ?
|
|
4911
|
+
return value ? chalk31.green("true") : chalk31.red("false");
|
|
4584
4912
|
}
|
|
4585
|
-
return
|
|
4913
|
+
return chalk31.cyan(String(value));
|
|
4586
4914
|
}
|
|
4587
4915
|
async function listEnvVars(options) {
|
|
4588
4916
|
const packageName = options.package || "@spfn/core";
|
|
@@ -4596,28 +4924,28 @@ async function listEnvVars(options) {
|
|
|
4596
4924
|
acc[target].push([key, schema]);
|
|
4597
4925
|
return acc;
|
|
4598
4926
|
}, {});
|
|
4599
|
-
console.log(
|
|
4927
|
+
console.log(chalk31.blue.bold(`
|
|
4600
4928
|
\u{1F4CB} Environment Variables by File (${packageName})
|
|
4601
4929
|
`));
|
|
4602
4930
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
4603
|
-
console.log(
|
|
4931
|
+
console.log(chalk31.bold.magenta(`
|
|
4604
4932
|
${file}`));
|
|
4605
|
-
console.log(
|
|
4933
|
+
console.log(chalk31.dim("\u2500".repeat(50)));
|
|
4606
4934
|
for (const [key, schema] of vars) {
|
|
4607
4935
|
printEnvVar(key, schema);
|
|
4608
4936
|
}
|
|
4609
4937
|
}
|
|
4610
4938
|
} else {
|
|
4611
|
-
console.log(
|
|
4939
|
+
console.log(chalk31.blue.bold(`
|
|
4612
4940
|
\u{1F4CB} Environment Variables (${packageName})
|
|
4613
4941
|
`));
|
|
4614
4942
|
for (const [key, schema] of allVars) {
|
|
4615
4943
|
printEnvVar(key, schema, true);
|
|
4616
4944
|
}
|
|
4617
4945
|
}
|
|
4618
|
-
console.log(
|
|
4946
|
+
console.log(chalk31.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
|
|
4619
4947
|
} catch (error) {
|
|
4620
|
-
console.error(
|
|
4948
|
+
console.error(chalk31.red(`
|
|
4621
4949
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4622
4950
|
`));
|
|
4623
4951
|
process.exit(1);
|
|
@@ -4625,17 +4953,17 @@ ${file}`));
|
|
|
4625
4953
|
}
|
|
4626
4954
|
function printEnvVar(key, schema, showFile = false) {
|
|
4627
4955
|
const typeStr = formatType(schema.type);
|
|
4628
|
-
const requiredStr = schema.required || schema.default !== void 0 ?
|
|
4629
|
-
const sensitiveStr = schema.sensitive ?
|
|
4630
|
-
const fileStr = showFile ?
|
|
4631
|
-
console.log(`${
|
|
4632
|
-
console.log(` ${
|
|
4956
|
+
const requiredStr = schema.required || schema.default !== void 0 ? chalk31.red("[required]") : chalk31.dim("[optional]");
|
|
4957
|
+
const sensitiveStr = schema.sensitive ? chalk31.yellow(" [sensitive]") : "";
|
|
4958
|
+
const fileStr = showFile ? chalk31.dim(` \u2192 ${getTargetFile(schema)}`) : "";
|
|
4959
|
+
console.log(`${chalk31.bold.cyan(key)} ${chalk31.dim("(")}${typeStr}${chalk31.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
|
|
4960
|
+
console.log(` ${chalk31.dim(schema.description)}`);
|
|
4633
4961
|
if (schema.default !== void 0) {
|
|
4634
|
-
console.log(` ${
|
|
4962
|
+
console.log(` ${chalk31.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
4635
4963
|
}
|
|
4636
4964
|
if (schema.examples && schema.examples.length > 0) {
|
|
4637
4965
|
const exampleStr = schema.examples.map((ex) => formatDefault(ex, schema.type)).join(", ");
|
|
4638
|
-
console.log(` ${
|
|
4966
|
+
console.log(` ${chalk31.dim("Examples:")} ${exampleStr}`);
|
|
4639
4967
|
}
|
|
4640
4968
|
console.log();
|
|
4641
4969
|
}
|
|
@@ -4643,7 +4971,7 @@ async function showEnvStats(options) {
|
|
|
4643
4971
|
const packageName = options.package || "@spfn/core";
|
|
4644
4972
|
try {
|
|
4645
4973
|
const envSchema = await loadEnvSchema(packageName);
|
|
4646
|
-
console.log(
|
|
4974
|
+
console.log(chalk31.blue.bold(`
|
|
4647
4975
|
\u{1F4CA} Environment Variable Statistics (${packageName})
|
|
4648
4976
|
`));
|
|
4649
4977
|
const allVars = Object.entries(envSchema);
|
|
@@ -4665,24 +4993,24 @@ async function showEnvStats(options) {
|
|
|
4665
4993
|
acc[file] = (acc[file] || 0) + 1;
|
|
4666
4994
|
return acc;
|
|
4667
4995
|
}, {});
|
|
4668
|
-
console.log(`${
|
|
4669
|
-
console.log(`${
|
|
4670
|
-
console.log(`${
|
|
4671
|
-
console.log(`${
|
|
4672
|
-
console.log(
|
|
4673
|
-
console.log(` ${
|
|
4674
|
-
console.log(` ${
|
|
4675
|
-
console.log(
|
|
4996
|
+
console.log(`${chalk31.bold("Total variables:")} ${chalk31.cyan(allVars.length)}`);
|
|
4997
|
+
console.log(`${chalk31.bold("Required:")} ${chalk31.red(required.length)}`);
|
|
4998
|
+
console.log(`${chalk31.bold("Optional:")} ${chalk31.dim(optional.length)}`);
|
|
4999
|
+
console.log(`${chalk31.bold("Sensitive:")} ${chalk31.yellow(sensitive.length)}`);
|
|
5000
|
+
console.log(chalk31.bold("\nBy Target:"));
|
|
5001
|
+
console.log(` ${chalk31.blue("Next.js accessible:")} ${chalk31.cyan(nextjsVars.length)}`);
|
|
5002
|
+
console.log(` ${chalk31.magenta("SPFN server only:")} ${chalk31.cyan(serverOnlyVars.length)}`);
|
|
5003
|
+
console.log(chalk31.bold("\nBy File:"));
|
|
4676
5004
|
for (const [file, count] of Object.entries(fileCount)) {
|
|
4677
|
-
console.log(` ${
|
|
5005
|
+
console.log(` ${chalk31.dim(file)}: ${chalk31.cyan(count)}`);
|
|
4678
5006
|
}
|
|
4679
|
-
console.log(
|
|
5007
|
+
console.log(chalk31.bold("\nBy Type:"));
|
|
4680
5008
|
for (const [type, count] of Object.entries(typeCount)) {
|
|
4681
|
-
console.log(` ${formatType(type)}: ${
|
|
5009
|
+
console.log(` ${formatType(type)}: ${chalk31.cyan(count)}`);
|
|
4682
5010
|
}
|
|
4683
5011
|
console.log();
|
|
4684
5012
|
} catch (error) {
|
|
4685
|
-
console.error(
|
|
5013
|
+
console.error(chalk31.red(`
|
|
4686
5014
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4687
5015
|
`));
|
|
4688
5016
|
process.exit(1);
|
|
@@ -4702,40 +5030,40 @@ async function searchEnvVars(query, options) {
|
|
|
4702
5030
|
}
|
|
4703
5031
|
}
|
|
4704
5032
|
if (results.length === 0) {
|
|
4705
|
-
console.log(
|
|
5033
|
+
console.log(chalk31.yellow(`
|
|
4706
5034
|
\u26A0\uFE0F No environment variables found matching "${query}"
|
|
4707
5035
|
`));
|
|
4708
5036
|
return;
|
|
4709
5037
|
}
|
|
4710
|
-
console.log(
|
|
5038
|
+
console.log(chalk31.blue.bold(`
|
|
4711
5039
|
\u{1F50D} Found ${results.length} environment variable(s) matching "${query}"
|
|
4712
5040
|
`));
|
|
4713
5041
|
for (const [key, schema] of results) {
|
|
4714
5042
|
const typeStr = formatType(schema.type);
|
|
4715
|
-
const requiredStr = schema.required || schema.default !== void 0 ?
|
|
4716
|
-
console.log(`${
|
|
4717
|
-
console.log(` ${
|
|
5043
|
+
const requiredStr = schema.required || schema.default !== void 0 ? chalk31.red("[required]") : chalk31.dim("[optional]");
|
|
5044
|
+
console.log(`${chalk31.bold.cyan(key)} ${chalk31.dim("(")}${typeStr}${chalk31.dim(")")} ${requiredStr}`);
|
|
5045
|
+
console.log(` ${chalk31.dim(schema.description)}`);
|
|
4718
5046
|
if (schema.default !== void 0) {
|
|
4719
|
-
console.log(` ${
|
|
5047
|
+
console.log(` ${chalk31.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
4720
5048
|
}
|
|
4721
5049
|
console.log();
|
|
4722
5050
|
}
|
|
4723
5051
|
} catch (error) {
|
|
4724
|
-
console.error(
|
|
5052
|
+
console.error(chalk31.red(`
|
|
4725
5053
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4726
5054
|
`));
|
|
4727
5055
|
process.exit(1);
|
|
4728
5056
|
}
|
|
4729
5057
|
}
|
|
4730
|
-
var envCommand = new
|
|
5058
|
+
var envCommand = new Command12("env").description("Manage environment variables");
|
|
4731
5059
|
envCommand.command("list").description("List all environment variables from schema").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").option("-g, --group", "Group variables by target file").action(listEnvVars);
|
|
4732
5060
|
envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(showEnvStats);
|
|
4733
5061
|
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);
|
|
4734
5062
|
function validateEnvOption(envValue) {
|
|
4735
5063
|
if (!VALID_ENVS.includes(envValue)) {
|
|
4736
|
-
console.error(
|
|
5064
|
+
console.error(chalk31.red(`
|
|
4737
5065
|
\u274C Invalid environment: "${envValue}"`));
|
|
4738
|
-
console.log(
|
|
5066
|
+
console.log(chalk31.dim(` Valid values: ${VALID_ENVS.join(", ")}
|
|
4739
5067
|
`));
|
|
4740
5068
|
process.exit(1);
|
|
4741
5069
|
}
|
|
@@ -4756,8 +5084,8 @@ async function initEnvFiles(options) {
|
|
|
4756
5084
|
return acc;
|
|
4757
5085
|
}, {});
|
|
4758
5086
|
if (targetEnv) {
|
|
4759
|
-
console.log(
|
|
4760
|
-
\u{1F680} Generating .env template files for ${
|
|
5087
|
+
console.log(chalk31.blue.bold(`
|
|
5088
|
+
\u{1F680} Generating .env template files for ${chalk31.cyan(targetEnv)} environment
|
|
4761
5089
|
`));
|
|
4762
5090
|
const envSpecificFiles = {};
|
|
4763
5091
|
const committedVars = allVars.filter(([_, schema]) => !schema.sensitive);
|
|
@@ -4773,24 +5101,24 @@ async function initEnvFiles(options) {
|
|
|
4773
5101
|
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
4774
5102
|
}
|
|
4775
5103
|
} else {
|
|
4776
|
-
console.log(
|
|
5104
|
+
console.log(chalk31.blue.bold(`
|
|
4777
5105
|
\u{1F680} Generating .env template files
|
|
4778
5106
|
`));
|
|
4779
5107
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
4780
5108
|
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
4781
5109
|
}
|
|
4782
5110
|
}
|
|
4783
|
-
console.log(
|
|
4784
|
-
console.log(
|
|
4785
|
-
console.log(
|
|
4786
|
-
console.log(
|
|
5111
|
+
console.log(chalk31.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
|
|
5112
|
+
console.log(chalk31.dim(" cp .env.example .env"));
|
|
5113
|
+
console.log(chalk31.dim(" cp .env.local.example .env.local"));
|
|
5114
|
+
console.log(chalk31.dim(" cp .env.server.example .env.server"));
|
|
4787
5115
|
if (targetEnv) {
|
|
4788
|
-
console.log(
|
|
4789
|
-
console.log(
|
|
5116
|
+
console.log(chalk31.dim(` cp .env.${targetEnv}.example .env.${targetEnv}`));
|
|
5117
|
+
console.log(chalk31.dim(` cp .env.${targetEnv}.local.example .env.${targetEnv}.local`));
|
|
4790
5118
|
}
|
|
4791
5119
|
console.log("");
|
|
4792
5120
|
} catch (error) {
|
|
4793
|
-
console.error(
|
|
5121
|
+
console.error(chalk31.red(`
|
|
4794
5122
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4795
5123
|
`));
|
|
4796
5124
|
process.exit(1);
|
|
@@ -4798,12 +5126,12 @@ async function initEnvFiles(options) {
|
|
|
4798
5126
|
}
|
|
4799
5127
|
function writeEnvTemplate(cwd, file, vars, force) {
|
|
4800
5128
|
const filePath = resolve2(cwd, file);
|
|
4801
|
-
if (
|
|
4802
|
-
console.log(
|
|
5129
|
+
if (existsSync28(filePath) && !force) {
|
|
5130
|
+
console.log(chalk31.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
|
|
4803
5131
|
return;
|
|
4804
5132
|
}
|
|
4805
|
-
|
|
4806
|
-
console.log(
|
|
5133
|
+
writeFileSync17(filePath, generateEnvFileContent(vars), "utf-8");
|
|
5134
|
+
console.log(chalk31.green(` \u2705 ${file} (${vars.length} variables)`));
|
|
4807
5135
|
}
|
|
4808
5136
|
function generateEnvFileContent(vars) {
|
|
4809
5137
|
const lines = [
|
|
@@ -4838,7 +5166,7 @@ async function checkEnvFiles(options) {
|
|
|
4838
5166
|
const envSchema = await loadEnvSchema(packageName);
|
|
4839
5167
|
const allVars = Object.entries(envSchema);
|
|
4840
5168
|
const envLabel = targetEnv ? ` (${targetEnv})` : "";
|
|
4841
|
-
console.log(
|
|
5169
|
+
console.log(chalk31.blue.bold(`
|
|
4842
5170
|
\u{1F50D} Checking .env files against schema${envLabel}
|
|
4843
5171
|
`));
|
|
4844
5172
|
const filesToCheck = targetEnv ? getEnvFilesForEnvironment(targetEnv) : [...BASE_ENV_FILES.nextjs, ...BASE_ENV_FILES.server];
|
|
@@ -4847,7 +5175,7 @@ async function checkEnvFiles(options) {
|
|
|
4847
5175
|
const warnings = [];
|
|
4848
5176
|
for (const file of filesToCheck) {
|
|
4849
5177
|
const filePath = resolve2(cwd, file);
|
|
4850
|
-
if (!
|
|
5178
|
+
if (!existsSync28(filePath)) {
|
|
4851
5179
|
continue;
|
|
4852
5180
|
}
|
|
4853
5181
|
const content = readFileSync12(filePath, "utf-8");
|
|
@@ -4855,7 +5183,7 @@ async function checkEnvFiles(options) {
|
|
|
4855
5183
|
for (const [key, value] of Object.entries(parsed)) {
|
|
4856
5184
|
loadedEnv[key] = { value: value || "", file };
|
|
4857
5185
|
}
|
|
4858
|
-
console.log(
|
|
5186
|
+
console.log(chalk31.dim(` \u{1F4C4} ${file} loaded`));
|
|
4859
5187
|
}
|
|
4860
5188
|
console.log("");
|
|
4861
5189
|
for (const [key, schema] of allVars) {
|
|
@@ -4863,7 +5191,7 @@ async function checkEnvFiles(options) {
|
|
|
4863
5191
|
const found = loadedEnv[key];
|
|
4864
5192
|
if (!found) {
|
|
4865
5193
|
if (schema.required && schema.default === void 0) {
|
|
4866
|
-
issues.push(`${
|
|
5194
|
+
issues.push(`${chalk31.red("\u2717")} ${chalk31.cyan(key)} is required but not found in any .env file`);
|
|
4867
5195
|
}
|
|
4868
5196
|
continue;
|
|
4869
5197
|
}
|
|
@@ -4873,11 +5201,11 @@ async function checkEnvFiles(options) {
|
|
|
4873
5201
|
if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
|
|
4874
5202
|
if (schema.sensitive) {
|
|
4875
5203
|
issues.push(
|
|
4876
|
-
`${
|
|
5204
|
+
`${chalk31.red("\u2717")} ${chalk31.cyan(key)} is sensitive and should be in ${chalk31.magenta(expectedFile)}, but found in ${chalk31.yellow(found.file)} (security risk!)`
|
|
4877
5205
|
);
|
|
4878
5206
|
} else {
|
|
4879
5207
|
warnings.push(
|
|
4880
|
-
`${
|
|
5208
|
+
`${chalk31.yellow("\u26A0")} ${chalk31.cyan(key)} should be in ${chalk31.magenta(expectedFile)}, but found in ${chalk31.dim(found.file)}`
|
|
4881
5209
|
);
|
|
4882
5210
|
}
|
|
4883
5211
|
}
|
|
@@ -4885,34 +5213,34 @@ async function checkEnvFiles(options) {
|
|
|
4885
5213
|
for (const [key, { file }] of Object.entries(loadedEnv)) {
|
|
4886
5214
|
const inSchema = allVars.some(([k]) => k === key);
|
|
4887
5215
|
if (!inSchema) {
|
|
4888
|
-
warnings.push(`${
|
|
5216
|
+
warnings.push(`${chalk31.yellow("\u26A0")} ${chalk31.cyan(key)} in ${chalk31.dim(file)} is not in schema`);
|
|
4889
5217
|
}
|
|
4890
5218
|
}
|
|
4891
5219
|
if (issues.length > 0) {
|
|
4892
|
-
console.log(
|
|
5220
|
+
console.log(chalk31.red.bold("Issues:"));
|
|
4893
5221
|
for (const issue of issues) {
|
|
4894
5222
|
console.log(` ${issue}`);
|
|
4895
5223
|
}
|
|
4896
5224
|
console.log("");
|
|
4897
5225
|
}
|
|
4898
5226
|
if (warnings.length > 0) {
|
|
4899
|
-
console.log(
|
|
5227
|
+
console.log(chalk31.yellow.bold("Warnings:"));
|
|
4900
5228
|
for (const warning of warnings) {
|
|
4901
5229
|
console.log(` ${warning}`);
|
|
4902
5230
|
}
|
|
4903
5231
|
console.log("");
|
|
4904
5232
|
}
|
|
4905
5233
|
if (issues.length === 0 && warnings.length === 0) {
|
|
4906
|
-
console.log(
|
|
5234
|
+
console.log(chalk31.green("\u2705 All environment variables are correctly configured!\n"));
|
|
4907
5235
|
} else {
|
|
4908
|
-
console.log(
|
|
5236
|
+
console.log(chalk31.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
|
|
4909
5237
|
`));
|
|
4910
5238
|
if (issues.length > 0) {
|
|
4911
5239
|
process.exit(1);
|
|
4912
5240
|
}
|
|
4913
5241
|
}
|
|
4914
5242
|
} catch (error) {
|
|
4915
|
-
console.error(
|
|
5243
|
+
console.error(chalk31.red(`
|
|
4916
5244
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4917
5245
|
`));
|
|
4918
5246
|
process.exit(1);
|
|
@@ -4926,15 +5254,15 @@ async function validateEnvVars(options) {
|
|
|
4926
5254
|
if (targetEnv) {
|
|
4927
5255
|
const { loadEnv: loadEnv10 } = await import("@spfn/core/env/loader");
|
|
4928
5256
|
const result = loadEnv10({ nodeEnv: targetEnv });
|
|
4929
|
-
console.log(
|
|
4930
|
-
\u{1F50D} Validating environment variables for ${
|
|
5257
|
+
console.log(chalk31.blue.bold(`
|
|
5258
|
+
\u{1F50D} Validating environment variables for ${chalk31.cyan(targetEnv)}
|
|
4931
5259
|
`));
|
|
4932
5260
|
if (result.loadedFiles.length > 0) {
|
|
4933
|
-
console.log(
|
|
5261
|
+
console.log(chalk31.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
|
|
4934
5262
|
}
|
|
4935
5263
|
console.log("");
|
|
4936
5264
|
} else {
|
|
4937
|
-
console.log(
|
|
5265
|
+
console.log(chalk31.blue.bold(`
|
|
4938
5266
|
\u{1F50D} Validating environment variables
|
|
4939
5267
|
`));
|
|
4940
5268
|
}
|
|
@@ -4942,7 +5270,7 @@ async function validateEnvVars(options) {
|
|
|
4942
5270
|
const allWarnings = [];
|
|
4943
5271
|
for (const packageName of packages) {
|
|
4944
5272
|
try {
|
|
4945
|
-
console.log(
|
|
5273
|
+
console.log(chalk31.dim(` \u{1F4E6} ${packageName}`));
|
|
4946
5274
|
const envSchema = await loadEnvSchema(packageName);
|
|
4947
5275
|
const { createEnvRegistry } = await import("@spfn/core/env");
|
|
4948
5276
|
const registry = createEnvRegistry(envSchema);
|
|
@@ -4955,10 +5283,10 @@ async function validateEnvVars(options) {
|
|
|
4955
5283
|
}
|
|
4956
5284
|
} catch (error) {
|
|
4957
5285
|
if (error instanceof Error && error.message.includes("does not export envSchema")) {
|
|
4958
|
-
console.log(
|
|
5286
|
+
console.log(chalk31.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
|
|
4959
5287
|
continue;
|
|
4960
5288
|
}
|
|
4961
|
-
console.error(
|
|
5289
|
+
console.error(chalk31.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
|
|
4962
5290
|
if (options.strict) {
|
|
4963
5291
|
process.exit(1);
|
|
4964
5292
|
}
|
|
@@ -4966,32 +5294,32 @@ async function validateEnvVars(options) {
|
|
|
4966
5294
|
}
|
|
4967
5295
|
console.log("");
|
|
4968
5296
|
if (allErrors.length > 0) {
|
|
4969
|
-
console.log(
|
|
5297
|
+
console.log(chalk31.red.bold(`\u274C Validation Errors (${allErrors.length}):
|
|
4970
5298
|
`));
|
|
4971
5299
|
for (const error of allErrors) {
|
|
4972
|
-
console.log(` ${
|
|
4973
|
-
console.log(` ${
|
|
4974
|
-
console.log(` ${
|
|
5300
|
+
console.log(` ${chalk31.red("\u2717")} ${chalk31.cyan(error.key)}`);
|
|
5301
|
+
console.log(` ${chalk31.dim(error.message)}`);
|
|
5302
|
+
console.log(` ${chalk31.dim(`from ${error.package}`)}`);
|
|
4975
5303
|
console.log("");
|
|
4976
5304
|
}
|
|
4977
5305
|
}
|
|
4978
5306
|
if (allWarnings.length > 0) {
|
|
4979
|
-
console.log(
|
|
5307
|
+
console.log(chalk31.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
|
|
4980
5308
|
`));
|
|
4981
5309
|
for (const warning of allWarnings) {
|
|
4982
|
-
console.log(` ${
|
|
4983
|
-
console.log(` ${
|
|
5310
|
+
console.log(` ${chalk31.yellow("\u26A0")} ${chalk31.cyan(warning.key)}`);
|
|
5311
|
+
console.log(` ${chalk31.dim(warning.message)}`);
|
|
4984
5312
|
console.log("");
|
|
4985
5313
|
}
|
|
4986
5314
|
}
|
|
4987
5315
|
if (allErrors.length === 0 && allWarnings.length === 0) {
|
|
4988
|
-
console.log(
|
|
5316
|
+
console.log(chalk31.green.bold("\u2705 All environment variables are valid!\n"));
|
|
4989
5317
|
} else if (allErrors.length === 0) {
|
|
4990
|
-
console.log(
|
|
4991
|
-
console.log(
|
|
5318
|
+
console.log(chalk31.green("\u2705 No errors found."));
|
|
5319
|
+
console.log(chalk31.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
|
|
4992
5320
|
`));
|
|
4993
5321
|
} else {
|
|
4994
|
-
console.log(
|
|
5322
|
+
console.log(chalk31.red(`
|
|
4995
5323
|
\u274C Validation failed with ${allErrors.length} error(s)
|
|
4996
5324
|
`));
|
|
4997
5325
|
process.exit(1);
|
|
@@ -5000,12 +5328,12 @@ async function validateEnvVars(options) {
|
|
|
5000
5328
|
envCommand.command("validate").description("Validate environment variables against schema (for CI/CD)").option("-p, --packages <packages...>", "Packages to validate", ["@spfn/core"]).option("-e, --env <environment>", "Load env files for specific environment before validating").option("-s, --strict", "Exit on any error (including load failures)").action(validateEnvVars);
|
|
5001
5329
|
|
|
5002
5330
|
// src/commands/secret/index.ts
|
|
5003
|
-
import { Command as
|
|
5331
|
+
import { Command as Command13 } from "commander";
|
|
5004
5332
|
|
|
5005
5333
|
// src/commands/secret/set.ts
|
|
5006
5334
|
init_logger();
|
|
5007
|
-
import
|
|
5008
|
-
import
|
|
5335
|
+
import prompts9 from "prompts";
|
|
5336
|
+
import chalk32 from "chalk";
|
|
5009
5337
|
|
|
5010
5338
|
// src/commands/secret/options.ts
|
|
5011
5339
|
init_logger();
|
|
@@ -5020,34 +5348,34 @@ function resolveEnv(env9) {
|
|
|
5020
5348
|
|
|
5021
5349
|
// src/commands/secret/store-value.ts
|
|
5022
5350
|
init_logger();
|
|
5023
|
-
import { join as
|
|
5351
|
+
import { join as join27 } from "path";
|
|
5024
5352
|
init_env_file();
|
|
5025
5353
|
|
|
5026
5354
|
// src/utils/sops.ts
|
|
5027
|
-
import { execa as
|
|
5028
|
-
import { existsSync as
|
|
5029
|
-
import { dirname as
|
|
5355
|
+
import { execa as execa11 } from "execa";
|
|
5356
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync5, writeFileSync as writeFileSync18 } from "fs";
|
|
5357
|
+
import { dirname as dirname4 } from "path";
|
|
5030
5358
|
async function ensureSopsInstalled() {
|
|
5031
5359
|
try {
|
|
5032
|
-
await
|
|
5360
|
+
await execa11("sops", ["--version"]);
|
|
5033
5361
|
} catch {
|
|
5034
5362
|
throw new Error("`sops` not found on PATH. Install it: https://github.com/getsops/sops");
|
|
5035
5363
|
}
|
|
5036
5364
|
}
|
|
5037
5365
|
async function sopsDecrypt(absFile) {
|
|
5038
|
-
if (!
|
|
5366
|
+
if (!existsSync29(absFile)) {
|
|
5039
5367
|
return {};
|
|
5040
5368
|
}
|
|
5041
|
-
const { stdout } = await
|
|
5369
|
+
const { stdout } = await execa11("sops", ["--decrypt", "--output-type", "json", absFile]);
|
|
5042
5370
|
return JSON.parse(stdout);
|
|
5043
5371
|
}
|
|
5044
5372
|
async function sopsSetValue(absFile, relFile, key, value) {
|
|
5045
|
-
if (
|
|
5046
|
-
await
|
|
5373
|
+
if (existsSync29(absFile)) {
|
|
5374
|
+
await execa11("sops", ["set", absFile, `["${key}"]`, JSON.stringify(value)]);
|
|
5047
5375
|
return;
|
|
5048
5376
|
}
|
|
5049
|
-
|
|
5050
|
-
const { stdout } = await
|
|
5377
|
+
mkdirSync5(dirname4(absFile), { recursive: true });
|
|
5378
|
+
const { stdout } = await execa11(
|
|
5051
5379
|
"sops",
|
|
5052
5380
|
[
|
|
5053
5381
|
"--encrypt",
|
|
@@ -5061,27 +5389,27 @@ async function sopsSetValue(absFile, relFile, key, value) {
|
|
|
5061
5389
|
],
|
|
5062
5390
|
{ input: JSON.stringify({ [key]: value }) }
|
|
5063
5391
|
);
|
|
5064
|
-
|
|
5392
|
+
writeFileSync18(absFile, stdout);
|
|
5065
5393
|
}
|
|
5066
5394
|
async function sopsUpdateKeys(absFile) {
|
|
5067
|
-
await
|
|
5395
|
+
await execa11("sops", ["updatekeys", "-y", absFile]);
|
|
5068
5396
|
}
|
|
5069
5397
|
|
|
5070
5398
|
// src/utils/secret-config.ts
|
|
5071
|
-
import { existsSync as
|
|
5072
|
-
import { join as
|
|
5399
|
+
import { existsSync as existsSync30, readdirSync as readdirSync3 } from "fs";
|
|
5400
|
+
import { join as join26 } from "path";
|
|
5073
5401
|
var SECRETS_DIR = "secrets";
|
|
5074
5402
|
function getSopsFile(cwd, env9) {
|
|
5075
5403
|
const relFile = `${SECRETS_DIR}/${env9}.enc.json`;
|
|
5076
|
-
return { absFile:
|
|
5404
|
+
return { absFile: join26(cwd, relFile), relFile };
|
|
5077
5405
|
}
|
|
5078
5406
|
function findUp(cwd, filename, maxDepth = 6) {
|
|
5079
5407
|
let dir = cwd;
|
|
5080
5408
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
5081
|
-
if (
|
|
5409
|
+
if (existsSync30(join26(dir, filename))) {
|
|
5082
5410
|
return dir;
|
|
5083
5411
|
}
|
|
5084
|
-
const parent =
|
|
5412
|
+
const parent = join26(dir, "..");
|
|
5085
5413
|
if (parent === dir) {
|
|
5086
5414
|
break;
|
|
5087
5415
|
}
|
|
@@ -5091,17 +5419,17 @@ function findUp(cwd, filename, maxDepth = 6) {
|
|
|
5091
5419
|
}
|
|
5092
5420
|
function findSopsConfig(cwd) {
|
|
5093
5421
|
const dir = findUp(cwd, ".sops.yaml");
|
|
5094
|
-
return dir ?
|
|
5422
|
+
return dir ? join26(dir, ".sops.yaml") : null;
|
|
5095
5423
|
}
|
|
5096
5424
|
function hasSopsConfig(cwd) {
|
|
5097
5425
|
return findSopsConfig(cwd) !== null;
|
|
5098
5426
|
}
|
|
5099
5427
|
function listSopsFiles(cwd) {
|
|
5100
|
-
const dir =
|
|
5101
|
-
if (!
|
|
5428
|
+
const dir = join26(cwd, SECRETS_DIR);
|
|
5429
|
+
if (!existsSync30(dir)) {
|
|
5102
5430
|
return [];
|
|
5103
5431
|
}
|
|
5104
|
-
return readdirSync3(dir).filter((name) => name.endsWith(".enc.json")).map((name) =>
|
|
5432
|
+
return readdirSync3(dir).filter((name) => name.endsWith(".enc.json")).map((name) => join26(dir, name));
|
|
5105
5433
|
}
|
|
5106
5434
|
|
|
5107
5435
|
// src/commands/secret/store-value.ts
|
|
@@ -5130,7 +5458,7 @@ async function storeSecret(cwd, env9, key, value) {
|
|
|
5130
5458
|
);
|
|
5131
5459
|
}
|
|
5132
5460
|
await store.set(keychainName(key), value);
|
|
5133
|
-
const serverEnvPath =
|
|
5461
|
+
const serverEnvPath = join27(cwd, ".env.server");
|
|
5134
5462
|
const result = upsertEnvVar(serverEnvPath, key, keychainRef(key));
|
|
5135
5463
|
restrictEnvFilePerms(serverEnvPath);
|
|
5136
5464
|
ensureGitignored(cwd, [{ pattern: ".env.server", comment: "spfn server env (secrets)" }]);
|
|
@@ -5158,10 +5486,10 @@ async function secretSet(key, options) {
|
|
|
5158
5486
|
process.exit(1);
|
|
5159
5487
|
}
|
|
5160
5488
|
await warnIfNotSecret(pkg, resolvedKey);
|
|
5161
|
-
const { value } = await
|
|
5489
|
+
const { value } = await prompts9({
|
|
5162
5490
|
type: "password",
|
|
5163
5491
|
name: "value",
|
|
5164
|
-
message: `Value for ${
|
|
5492
|
+
message: `Value for ${chalk32.cyan(resolvedKey)} (${env9})`
|
|
5165
5493
|
});
|
|
5166
5494
|
if (!value) {
|
|
5167
5495
|
logger.warn("Cancelled \u2014 no value entered.");
|
|
@@ -5185,7 +5513,7 @@ async function pickSecretKey(pkg) {
|
|
|
5185
5513
|
if (entries.length === 0) {
|
|
5186
5514
|
return void 0;
|
|
5187
5515
|
}
|
|
5188
|
-
const { key } = await
|
|
5516
|
+
const { key } = await prompts9({
|
|
5189
5517
|
type: "select",
|
|
5190
5518
|
name: "key",
|
|
5191
5519
|
message: "Which secret?",
|
|
@@ -5210,7 +5538,7 @@ async function warnIfNotSecret(pkg, key) {
|
|
|
5210
5538
|
|
|
5211
5539
|
// src/commands/secret/list.ts
|
|
5212
5540
|
init_logger();
|
|
5213
|
-
import
|
|
5541
|
+
import chalk33 from "chalk";
|
|
5214
5542
|
async function secretList(options) {
|
|
5215
5543
|
const env9 = resolveEnv(options.env);
|
|
5216
5544
|
const pkg = options.package ?? "@spfn/core";
|
|
@@ -5226,12 +5554,12 @@ async function secretList(options) {
|
|
|
5226
5554
|
return;
|
|
5227
5555
|
}
|
|
5228
5556
|
const present = await loadPresence(env9, entries);
|
|
5229
|
-
console.log(
|
|
5557
|
+
console.log(chalk33.blue.bold(`
|
|
5230
5558
|
\u{1F511} Secrets (${pkg}) \u2014 ${env9}
|
|
5231
5559
|
`));
|
|
5232
5560
|
for (const entry of entries) {
|
|
5233
5561
|
const status = statusOf(entry, present.has(entry.key));
|
|
5234
|
-
console.log(` ${badge(status)} ${
|
|
5562
|
+
console.log(` ${badge(status)} ${chalk33.cyan(entry.key)}${entry.generate ? chalk33.dim(" (generatable)") : ""}`);
|
|
5235
5563
|
}
|
|
5236
5564
|
console.log();
|
|
5237
5565
|
}
|
|
@@ -5264,17 +5592,17 @@ function statusOf(entry, present) {
|
|
|
5264
5592
|
function badge(status) {
|
|
5265
5593
|
switch (status) {
|
|
5266
5594
|
case "set":
|
|
5267
|
-
return
|
|
5595
|
+
return chalk33.green("\u25CF");
|
|
5268
5596
|
case "missing":
|
|
5269
|
-
return
|
|
5597
|
+
return chalk33.yellow("\u25CB");
|
|
5270
5598
|
case "awaiting-input":
|
|
5271
|
-
return
|
|
5599
|
+
return chalk33.red("\u25CB");
|
|
5272
5600
|
}
|
|
5273
5601
|
}
|
|
5274
5602
|
|
|
5275
5603
|
// src/commands/secret/generate.ts
|
|
5276
5604
|
init_logger();
|
|
5277
|
-
import
|
|
5605
|
+
import chalk34 from "chalk";
|
|
5278
5606
|
async function secretGenerate(key, options) {
|
|
5279
5607
|
const env9 = resolveEnv(options.env);
|
|
5280
5608
|
const pkg = options.package ?? "@spfn/core";
|
|
@@ -5312,7 +5640,7 @@ function requireGeneratable(schema, key) {
|
|
|
5312
5640
|
process.exit(1);
|
|
5313
5641
|
}
|
|
5314
5642
|
if (!entry.generate) {
|
|
5315
|
-
logger.error(`${
|
|
5643
|
+
logger.error(`${chalk34.cyan(key)} has no generate strategy \u2014 it's an external value. Use \`spfn secret set ${key}\`.`);
|
|
5316
5644
|
process.exit(1);
|
|
5317
5645
|
}
|
|
5318
5646
|
return entry;
|
|
@@ -5320,7 +5648,7 @@ function requireGeneratable(schema, key) {
|
|
|
5320
5648
|
|
|
5321
5649
|
// src/commands/secret/rotate.ts
|
|
5322
5650
|
init_logger();
|
|
5323
|
-
import
|
|
5651
|
+
import chalk35 from "chalk";
|
|
5324
5652
|
async function secretRotate(key, options) {
|
|
5325
5653
|
const env9 = resolveEnv(options.env);
|
|
5326
5654
|
const pkg = options.package ?? "@spfn/core";
|
|
@@ -5346,7 +5674,7 @@ async function secretRotate(key, options) {
|
|
|
5346
5674
|
}
|
|
5347
5675
|
}
|
|
5348
5676
|
for (const entry of external) {
|
|
5349
|
-
logger.warn(`${
|
|
5677
|
+
logger.warn(`${chalk35.cyan(entry.key)} is external \u2014 reissue it at the provider, then \`spfn secret set ${entry.key} --env ${env9}\`.`);
|
|
5350
5678
|
}
|
|
5351
5679
|
if (generatable.length > 0) {
|
|
5352
5680
|
logger.info("Rotation updates the source only \u2014 commit and deploy to apply.");
|
|
@@ -5367,17 +5695,17 @@ function requireEntry(schema, key) {
|
|
|
5367
5695
|
|
|
5368
5696
|
// src/commands/secret/keygen.ts
|
|
5369
5697
|
init_logger();
|
|
5370
|
-
import { execa as
|
|
5371
|
-
import { existsSync as
|
|
5698
|
+
import { execa as execa12 } from "execa";
|
|
5699
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync6 } from "fs";
|
|
5372
5700
|
import { homedir } from "os";
|
|
5373
|
-
import { dirname as
|
|
5374
|
-
import
|
|
5701
|
+
import { dirname as dirname5, join as join28 } from "path";
|
|
5702
|
+
import chalk36 from "chalk";
|
|
5375
5703
|
function ageKeyFile() {
|
|
5376
|
-
return process.env.SOPS_AGE_KEY_FILE ??
|
|
5704
|
+
return process.env.SOPS_AGE_KEY_FILE ?? join28(homedir(), ".config", "sops", "age", "keys.txt");
|
|
5377
5705
|
}
|
|
5378
5706
|
async function ensureAgeInstalled() {
|
|
5379
5707
|
try {
|
|
5380
|
-
await
|
|
5708
|
+
await execa12("age-keygen", ["--version"]);
|
|
5381
5709
|
} catch {
|
|
5382
5710
|
throw new Error("`age-keygen` not found on PATH. Install age: https://github.com/FiloSottile/age");
|
|
5383
5711
|
}
|
|
@@ -5393,14 +5721,14 @@ async function secretKeygen() {
|
|
|
5393
5721
|
process.exit(1);
|
|
5394
5722
|
}
|
|
5395
5723
|
const keyFile = ageKeyFile();
|
|
5396
|
-
if (
|
|
5397
|
-
const { stdout } = await
|
|
5724
|
+
if (existsSync31(keyFile)) {
|
|
5725
|
+
const { stdout } = await execa12("age-keygen", ["-y", keyFile]);
|
|
5398
5726
|
logger.warn(`age key file already exists: ${keyFile}`);
|
|
5399
5727
|
printPublicKeys(publicKeys(stdout));
|
|
5400
5728
|
return;
|
|
5401
5729
|
}
|
|
5402
|
-
|
|
5403
|
-
const { stderr } = await
|
|
5730
|
+
mkdirSync6(dirname5(keyFile), { recursive: true });
|
|
5731
|
+
const { stderr } = await execa12("age-keygen", ["-o", keyFile]);
|
|
5404
5732
|
logger.success(`Created age key: ${keyFile}`);
|
|
5405
5733
|
printPublicKeys(publicKeys(stderr));
|
|
5406
5734
|
}
|
|
@@ -5409,27 +5737,27 @@ function printPublicKeys(keys) {
|
|
|
5409
5737
|
logger.warn("Could not read the public key \u2014 run `age-keygen -y <file>` manually.");
|
|
5410
5738
|
return;
|
|
5411
5739
|
}
|
|
5412
|
-
console.log(
|
|
5740
|
+
console.log(chalk36.bold("\nPublic key(s):"));
|
|
5413
5741
|
for (const key of keys) {
|
|
5414
|
-
console.log(` ${
|
|
5742
|
+
console.log(` ${chalk36.cyan(key)}`);
|
|
5415
5743
|
}
|
|
5416
|
-
console.log(
|
|
5417
|
-
console.log(
|
|
5744
|
+
console.log(chalk36.dim("\nRegister it as a recipient:"));
|
|
5745
|
+
console.log(chalk36.dim(` spfn secret recipients add ${keys[0]}
|
|
5418
5746
|
`));
|
|
5419
5747
|
}
|
|
5420
5748
|
|
|
5421
5749
|
// src/commands/secret/recipients.ts
|
|
5422
5750
|
init_logger();
|
|
5423
|
-
import { existsSync as
|
|
5424
|
-
import { join as
|
|
5751
|
+
import { existsSync as existsSync32, readFileSync as readFileSync13, writeFileSync as writeFileSync19 } from "fs";
|
|
5752
|
+
import { join as join29 } from "path";
|
|
5425
5753
|
import { parse as parse3, stringify } from "yaml";
|
|
5426
|
-
import
|
|
5754
|
+
import chalk37 from "chalk";
|
|
5427
5755
|
var SOPS_CONFIG = ".sops.yaml";
|
|
5428
5756
|
var DEFAULT_PATH_REGEX = "secrets/.*\\.enc\\.json$";
|
|
5429
5757
|
var AGE_RECIPIENT = /^age1[0-9a-z]+$/;
|
|
5430
5758
|
async function secretRecipients(action, key, _options) {
|
|
5431
5759
|
const cwd = process.cwd();
|
|
5432
|
-
const configPath =
|
|
5760
|
+
const configPath = join29(cwd, SOPS_CONFIG);
|
|
5433
5761
|
if (action === "list") {
|
|
5434
5762
|
listRecipients(configPath);
|
|
5435
5763
|
return;
|
|
@@ -5456,30 +5784,30 @@ async function secretRecipients(action, key, _options) {
|
|
|
5456
5784
|
logger.warn("Removing a recipient does not revoke values they already decrypted \u2014 rotate those values.");
|
|
5457
5785
|
}
|
|
5458
5786
|
rule.age = [...recipients].join(",");
|
|
5459
|
-
|
|
5787
|
+
writeFileSync19(configPath, stringify(config));
|
|
5460
5788
|
logger.success(`${action === "add" ? "Added" : "Removed"} recipient; updated ${SOPS_CONFIG}.`);
|
|
5461
5789
|
await reencrypt(cwd);
|
|
5462
5790
|
}
|
|
5463
5791
|
function listRecipients(configPath) {
|
|
5464
|
-
if (!
|
|
5792
|
+
if (!existsSync32(configPath)) {
|
|
5465
5793
|
logger.info(`No ${SOPS_CONFIG} found.`);
|
|
5466
5794
|
return;
|
|
5467
5795
|
}
|
|
5468
5796
|
const config = loadConfig(configPath);
|
|
5469
5797
|
const rules = config.creation_rules ?? [];
|
|
5470
|
-
console.log(
|
|
5798
|
+
console.log(chalk37.blue.bold(`
|
|
5471
5799
|
\u{1F465} Recipients (${SOPS_CONFIG})
|
|
5472
5800
|
`));
|
|
5473
5801
|
for (const rule of rules) {
|
|
5474
|
-
console.log(
|
|
5802
|
+
console.log(chalk37.dim(` ${rule.path_regex ?? "(any path)"}`));
|
|
5475
5803
|
for (const recipient of parseRecipients(rule.age)) {
|
|
5476
|
-
console.log(` ${
|
|
5804
|
+
console.log(` ${chalk37.cyan(recipient)}`);
|
|
5477
5805
|
}
|
|
5478
5806
|
}
|
|
5479
5807
|
console.log();
|
|
5480
5808
|
}
|
|
5481
5809
|
function loadConfig(configPath) {
|
|
5482
|
-
if (!
|
|
5810
|
+
if (!existsSync32(configPath)) {
|
|
5483
5811
|
return { creation_rules: [] };
|
|
5484
5812
|
}
|
|
5485
5813
|
return parse3(readFileSync13(configPath, "utf-8")) ?? { creation_rules: [] };
|
|
@@ -5514,8 +5842,8 @@ async function reencrypt(cwd) {
|
|
|
5514
5842
|
|
|
5515
5843
|
// src/commands/secret/check.ts
|
|
5516
5844
|
init_logger();
|
|
5517
|
-
import { join as
|
|
5518
|
-
import
|
|
5845
|
+
import { join as join30 } from "path";
|
|
5846
|
+
import chalk38 from "chalk";
|
|
5519
5847
|
init_env_file();
|
|
5520
5848
|
var COMMITTED_FILES = [".env", ".env.example"];
|
|
5521
5849
|
var PLACEHOLDER = /(your-|changeme|placeholder|example|<.*>)/i;
|
|
@@ -5533,35 +5861,35 @@ async function secretCheck(options) {
|
|
|
5533
5861
|
const issues = [];
|
|
5534
5862
|
const warnings = [];
|
|
5535
5863
|
for (const file of COMMITTED_FILES) {
|
|
5536
|
-
const parsed = parseEnvFile(
|
|
5864
|
+
const parsed = parseEnvFile(join30(cwd, file));
|
|
5537
5865
|
for (const [key, value] of Object.entries(parsed)) {
|
|
5538
5866
|
if (secretKeys.has(key) && value.length > 0 && !PLACEHOLDER.test(value)) {
|
|
5539
|
-
issues.push(`${
|
|
5867
|
+
issues.push(`${chalk38.cyan(key)} has a real value in committed ${chalk38.yellow(file)} \u2014 move it to the keychain/SOPS.`);
|
|
5540
5868
|
}
|
|
5541
5869
|
}
|
|
5542
5870
|
}
|
|
5543
|
-
const serverEnv = parseEnvFile(
|
|
5871
|
+
const serverEnv = parseEnvFile(join30(cwd, ".env.server"));
|
|
5544
5872
|
for (const key of secretKeys) {
|
|
5545
5873
|
const value = serverEnv[key];
|
|
5546
5874
|
if (value && !value.startsWith(KEYCHAIN_REF_PREFIX)) {
|
|
5547
|
-
warnings.push(`${
|
|
5875
|
+
warnings.push(`${chalk38.cyan(key)} is plaintext in .env.server \u2014 run \`spfn secret set ${key}\` to move it to the keychain.`);
|
|
5548
5876
|
}
|
|
5549
5877
|
}
|
|
5550
5878
|
report(issues, warnings, cwd);
|
|
5551
5879
|
}
|
|
5552
5880
|
function report(issues, warnings, cwd) {
|
|
5553
|
-
console.log(
|
|
5881
|
+
console.log(chalk38.blue.bold("\n\u{1F50D} Secret hygiene check\n"));
|
|
5554
5882
|
for (const issue of issues) {
|
|
5555
|
-
console.log(` ${
|
|
5883
|
+
console.log(` ${chalk38.red("\u2717")} ${issue}`);
|
|
5556
5884
|
}
|
|
5557
5885
|
for (const warning of warnings) {
|
|
5558
|
-
console.log(` ${
|
|
5886
|
+
console.log(` ${chalk38.yellow("\u26A0")} ${warning}`);
|
|
5559
5887
|
}
|
|
5560
5888
|
if (!hasSopsConfig(cwd)) {
|
|
5561
|
-
console.log(` ${
|
|
5889
|
+
console.log(` ${chalk38.dim("\u2139 no .sops.yaml \u2014 prod secrets need a backend (spfn secret keygen, or add a KMS rule)")}`);
|
|
5562
5890
|
}
|
|
5563
5891
|
if (issues.length === 0 && warnings.length === 0) {
|
|
5564
|
-
console.log(
|
|
5892
|
+
console.log(chalk38.green(" \u2713 No plaintext secret leaks found."));
|
|
5565
5893
|
}
|
|
5566
5894
|
console.log();
|
|
5567
5895
|
if (issues.length > 0) {
|
|
@@ -5572,7 +5900,7 @@ function report(issues, warnings, cwd) {
|
|
|
5572
5900
|
// src/commands/secret/index.ts
|
|
5573
5901
|
var ENV_OPTION = ["-e, --env <env>", "Target environment (local | development | staging | production)", "local"];
|
|
5574
5902
|
var PKG_OPTION = ["-p, --package <package>", "Package whose env schema to read", "@spfn/core"];
|
|
5575
|
-
var secretCommand = new
|
|
5903
|
+
var secretCommand = new Command13("secret").description("Manage secrets: keychain locally, SOPS (age / GCP KMS / AWS KMS) for deployed environments");
|
|
5576
5904
|
secretCommand.command("set [key]").description("Store a secret value (prompts for the value, masked)").option(...ENV_OPTION).option(...PKG_OPTION).action(secretSet);
|
|
5577
5905
|
secretCommand.command("list").description("List declared secrets and their status (never prints values)").option(...ENV_OPTION).option(...PKG_OPTION).action(secretList);
|
|
5578
5906
|
secretCommand.command("generate [key]").description("Generate value(s) for schema secrets that declare a generate strategy").option("-a, --all", "Generate every generatable secret").option(...ENV_OPTION).option(...PKG_OPTION).action(secretGenerate);
|
|
@@ -5583,7 +5911,7 @@ secretCommand.command("check").description("Static hygiene lint \u2014 flag plai
|
|
|
5583
5911
|
|
|
5584
5912
|
// src/index.ts
|
|
5585
5913
|
init_version();
|
|
5586
|
-
var program = new
|
|
5914
|
+
var program = new Command14();
|
|
5587
5915
|
program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(getCliVersion());
|
|
5588
5916
|
program.addCommand(createCommand);
|
|
5589
5917
|
program.addCommand(initCommand);
|
|
@@ -5591,6 +5919,7 @@ program.addCommand(addCommand);
|
|
|
5591
5919
|
program.addCommand(devCommand);
|
|
5592
5920
|
program.addCommand(buildCommand);
|
|
5593
5921
|
program.addCommand(startCommand);
|
|
5922
|
+
program.addCommand(provisionCommand);
|
|
5594
5923
|
program.addCommand(codegenCommand);
|
|
5595
5924
|
program.addCommand(keyCommand);
|
|
5596
5925
|
program.addCommand(setupCommand);
|