spfn 0.2.0-beta.63 → 0.2.0-beta.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +378 -238
- 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 +4 -4
package/dist/index.js
CHANGED
|
@@ -922,7 +922,7 @@ var init_deployment_config = __esm({
|
|
|
922
922
|
|
|
923
923
|
// src/utils/version.ts
|
|
924
924
|
function getCliVersion() {
|
|
925
|
-
return "0.2.0-beta.
|
|
925
|
+
return "0.2.0-beta.65";
|
|
926
926
|
}
|
|
927
927
|
function getTagFromVersion(version) {
|
|
928
928
|
const match = version.match(/-([a-z]+)\./i);
|
|
@@ -1860,7 +1860,7 @@ var init_migration_status = __esm({
|
|
|
1860
1860
|
});
|
|
1861
1861
|
|
|
1862
1862
|
// src/index.ts
|
|
1863
|
-
import { Command as
|
|
1863
|
+
import { Command as Command14 } from "commander";
|
|
1864
1864
|
|
|
1865
1865
|
// src/commands/create.ts
|
|
1866
1866
|
init_logger();
|
|
@@ -2876,16 +2876,95 @@ var startCommand = new Command6("start").description("Start SPFN production serv
|
|
|
2876
2876
|
}
|
|
2877
2877
|
});
|
|
2878
2878
|
|
|
2879
|
-
// src/commands/
|
|
2879
|
+
// src/commands/provision.ts
|
|
2880
2880
|
init_logger();
|
|
2881
|
+
init_package_manager();
|
|
2881
2882
|
import { Command as Command7 } from "commander";
|
|
2882
|
-
import { existsSync as existsSync19, writeFileSync as writeFileSync12 } from "fs";
|
|
2883
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync3, writeFileSync as writeFileSync12 } from "fs";
|
|
2883
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";
|
|
2884
2963
|
import chalk10 from "chalk";
|
|
2885
2964
|
async function initCodegen(options) {
|
|
2886
2965
|
const cwd = process.cwd();
|
|
2887
|
-
const rcPath =
|
|
2888
|
-
if (
|
|
2966
|
+
const rcPath = join21(cwd, ".spfnrc.ts");
|
|
2967
|
+
if (existsSync20(rcPath)) {
|
|
2889
2968
|
logger.warn(".spfnrc.ts already exists");
|
|
2890
2969
|
logger.info("Edit manually to add custom generators");
|
|
2891
2970
|
process.exit(0);
|
|
@@ -2910,7 +2989,7 @@ export default defineConfig({
|
|
|
2910
2989
|
],
|
|
2911
2990
|
});
|
|
2912
2991
|
`;
|
|
2913
|
-
|
|
2992
|
+
writeFileSync13(rcPath, content);
|
|
2914
2993
|
console.log("\n" + chalk10.green.bold("\u2713 Created .spfnrc.ts\n"));
|
|
2915
2994
|
console.log(chalk10.gray("Configured the @spfn/core:route-map generator."));
|
|
2916
2995
|
if (options.withExample) {
|
|
@@ -2970,14 +3049,14 @@ async function runGenerators() {
|
|
|
2970
3049
|
await orchestrator.generateAll();
|
|
2971
3050
|
console.log("\n" + chalk10.green.bold("\u2713 Code generation completed"));
|
|
2972
3051
|
}
|
|
2973
|
-
var codegenCommand = new
|
|
3052
|
+
var codegenCommand = new Command8("codegen").description("Code generation management");
|
|
2974
3053
|
codegenCommand.command("init").description("Initialize .spfnrc.ts with codegen configuration").option("--with-example", "Show example custom generator usage").action(initCodegen);
|
|
2975
3054
|
codegenCommand.command("list").alias("ls").description("List registered code generators").action(listGenerators);
|
|
2976
3055
|
codegenCommand.command("run").description("Run code generators once (no watch mode)").action(runGenerators);
|
|
2977
3056
|
|
|
2978
3057
|
// src/commands/key.ts
|
|
2979
3058
|
init_logger();
|
|
2980
|
-
import { Command as
|
|
3059
|
+
import { Command as Command9 } from "commander";
|
|
2981
3060
|
import { execSync } from "child_process";
|
|
2982
3061
|
import chalk11 from "chalk";
|
|
2983
3062
|
|
|
@@ -3091,7 +3170,7 @@ function listPresets() {
|
|
|
3091
3170
|
console.log(chalk11.gray(" spfn key auth-encryption --copy"));
|
|
3092
3171
|
console.log();
|
|
3093
3172
|
}
|
|
3094
|
-
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) => {
|
|
3095
3174
|
const bytes = parseInt(options.bytes, 10);
|
|
3096
3175
|
if (isNaN(bytes) || bytes < 1 || bytes > 128) {
|
|
3097
3176
|
logger.error("Invalid bytes value. Must be between 1 and 128.");
|
|
@@ -3107,7 +3186,7 @@ var generateValueCommand = new Command8("generate").alias("gen").description("Ge
|
|
|
3107
3186
|
}
|
|
3108
3187
|
}
|
|
3109
3188
|
});
|
|
3110
|
-
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) => {
|
|
3111
3190
|
if (options.list) {
|
|
3112
3191
|
listPresets();
|
|
3113
3192
|
return;
|
|
@@ -3139,15 +3218,15 @@ keyCommand.addCommand(generateValueCommand);
|
|
|
3139
3218
|
init_setup();
|
|
3140
3219
|
|
|
3141
3220
|
// src/commands/db/index.ts
|
|
3142
|
-
import { Command as
|
|
3221
|
+
import { Command as Command10 } from "commander";
|
|
3143
3222
|
|
|
3144
3223
|
// src/commands/db/generate.ts
|
|
3145
3224
|
import chalk13 from "chalk";
|
|
3146
3225
|
|
|
3147
3226
|
// src/commands/db/utils/drizzle.ts
|
|
3148
|
-
import { existsSync as
|
|
3227
|
+
import { existsSync as existsSync21, writeFileSync as writeFileSync14, unlinkSync as unlinkSync2 } from "fs";
|
|
3149
3228
|
import { spawn } from "child_process";
|
|
3150
|
-
import { pathToFileURL } from "url";
|
|
3229
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
3151
3230
|
import chalk12 from "chalk";
|
|
3152
3231
|
import ora6 from "ora";
|
|
3153
3232
|
import { env as env3 } from "@spfn/core/config";
|
|
@@ -3212,7 +3291,7 @@ function validateDatabasePrerequisites() {
|
|
|
3212
3291
|
}
|
|
3213
3292
|
}
|
|
3214
3293
|
async function runDrizzleCommand(command) {
|
|
3215
|
-
const hasUserConfig =
|
|
3294
|
+
const hasUserConfig = existsSync21("./drizzle.config.ts");
|
|
3216
3295
|
const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
|
|
3217
3296
|
const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
|
|
3218
3297
|
if (!hasUserConfig) {
|
|
@@ -3231,7 +3310,7 @@ async function runDrizzleCommand(command) {
|
|
|
3231
3310
|
expandGlobs: true,
|
|
3232
3311
|
autoDetectSchemas: true
|
|
3233
3312
|
});
|
|
3234
|
-
|
|
3313
|
+
writeFileSync14(tempConfigPath, configContent);
|
|
3235
3314
|
console.log(chalk12.dim("Using auto-generated Drizzle config\n"));
|
|
3236
3315
|
}
|
|
3237
3316
|
const args = command.split(" ");
|
|
@@ -3244,7 +3323,7 @@ async function runDrizzleCommand(command) {
|
|
|
3244
3323
|
env: shouldRelaxDbTls(env3.DATABASE_URL) ? { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" } : { ...process.env }
|
|
3245
3324
|
});
|
|
3246
3325
|
const cleanup = () => {
|
|
3247
|
-
if (!hasUserConfig &&
|
|
3326
|
+
if (!hasUserConfig && existsSync21(tempConfigPath)) {
|
|
3248
3327
|
unlinkSync2(tempConfigPath);
|
|
3249
3328
|
}
|
|
3250
3329
|
};
|
|
@@ -3283,7 +3362,7 @@ async function loadSchemaImports(schemaFiles) {
|
|
|
3283
3362
|
}
|
|
3284
3363
|
const imports = {};
|
|
3285
3364
|
for (const file of schemaFiles) {
|
|
3286
|
-
const moduleUrl =
|
|
3365
|
+
const moduleUrl = pathToFileURL2(file).href;
|
|
3287
3366
|
const mod = await import(moduleUrl);
|
|
3288
3367
|
for (const [key, value] of Object.entries(mod)) {
|
|
3289
3368
|
if (key !== "default") {
|
|
@@ -3564,8 +3643,8 @@ async function applyFunctionMigrations(plans) {
|
|
|
3564
3643
|
|
|
3565
3644
|
// src/commands/db/migrate.ts
|
|
3566
3645
|
import chalk20 from "chalk";
|
|
3567
|
-
import { join as
|
|
3568
|
-
import { existsSync as
|
|
3646
|
+
import { join as join22 } from "path";
|
|
3647
|
+
import { existsSync as existsSync23 } from "fs";
|
|
3569
3648
|
|
|
3570
3649
|
// src/commands/db/backup.ts
|
|
3571
3650
|
import { promises as fs3 } from "fs";
|
|
@@ -3658,7 +3737,7 @@ function formatTimestamp() {
|
|
|
3658
3737
|
|
|
3659
3738
|
// src/commands/db/utils/backup-files.ts
|
|
3660
3739
|
import { promises as fs2 } from "fs";
|
|
3661
|
-
import { existsSync as
|
|
3740
|
+
import { existsSync as existsSync22 } from "fs";
|
|
3662
3741
|
import path2 from "path";
|
|
3663
3742
|
import chalk18 from "chalk";
|
|
3664
3743
|
|
|
@@ -3758,7 +3837,7 @@ async function ensureBackupInGitignore() {
|
|
|
3758
3837
|
const gitignorePath = path2.join(process.cwd(), ".gitignore");
|
|
3759
3838
|
try {
|
|
3760
3839
|
let content = "";
|
|
3761
|
-
let exists =
|
|
3840
|
+
let exists = existsSync22(gitignorePath);
|
|
3762
3841
|
if (exists) {
|
|
3763
3842
|
content = await fs2.readFile(gitignorePath, "utf-8");
|
|
3764
3843
|
}
|
|
@@ -3780,7 +3859,7 @@ async function ensureBackupDir() {
|
|
|
3780
3859
|
try {
|
|
3781
3860
|
await fs2.mkdir(backupDir, { recursive: true });
|
|
3782
3861
|
const gitignorePath = path2.join(backupDir, ".gitignore");
|
|
3783
|
-
const gitignoreExists =
|
|
3862
|
+
const gitignoreExists = existsSync22(gitignorePath);
|
|
3784
3863
|
if (!gitignoreExists) {
|
|
3785
3864
|
await fs2.writeFile(gitignorePath, "# Ignore all backup files\n*.sql\n*.dump\n*.meta.json\n");
|
|
3786
3865
|
}
|
|
@@ -3974,8 +4053,8 @@ async function dbMigrate(options = {}) {
|
|
|
3974
4053
|
await executeFunctionMigrations2(loadFunctionMigrationPlans2(functions));
|
|
3975
4054
|
console.log(chalk20.green("\u2705 Function migrations applied\n"));
|
|
3976
4055
|
}
|
|
3977
|
-
const projectMigrationsDir =
|
|
3978
|
-
if (
|
|
4056
|
+
const projectMigrationsDir = join22(process.cwd(), "src/server/drizzle");
|
|
4057
|
+
if (existsSync23(projectMigrationsDir)) {
|
|
3979
4058
|
const projConn = postgres.default(env5.DATABASE_URL, { max: 1 });
|
|
3980
4059
|
const projDb = drizzle({ client: projConn });
|
|
3981
4060
|
try {
|
|
@@ -4035,7 +4114,7 @@ async function dbStatus() {
|
|
|
4035
4114
|
|
|
4036
4115
|
// src/commands/db/studio.ts
|
|
4037
4116
|
import chalk22 from "chalk";
|
|
4038
|
-
import { existsSync as
|
|
4117
|
+
import { existsSync as existsSync24, writeFileSync as writeFileSync15, unlinkSync as unlinkSync3 } from "fs";
|
|
4039
4118
|
import { spawn as spawn3 } from "child_process";
|
|
4040
4119
|
import { env as env6 } from "@spfn/core/config";
|
|
4041
4120
|
async function dbStudio(requestedPort) {
|
|
@@ -4053,7 +4132,7 @@ async function dbStudio(requestedPort) {
|
|
|
4053
4132
|
console.error(chalk22.red(error instanceof Error ? error.message : "Failed to find available port"));
|
|
4054
4133
|
process.exit(1);
|
|
4055
4134
|
}
|
|
4056
|
-
const hasUserConfig =
|
|
4135
|
+
const hasUserConfig = existsSync24("./drizzle.config.ts");
|
|
4057
4136
|
const tempConfigPath = `./drizzle.config.${process.pid}.${Date.now()}.temp.ts`;
|
|
4058
4137
|
try {
|
|
4059
4138
|
const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
|
|
@@ -4070,7 +4149,7 @@ async function dbStudio(requestedPort) {
|
|
|
4070
4149
|
expandGlobs: true
|
|
4071
4150
|
// Expand glob patterns for Studio compatibility
|
|
4072
4151
|
});
|
|
4073
|
-
|
|
4152
|
+
writeFileSync15(tempConfigPath, configContent);
|
|
4074
4153
|
console.log(chalk22.dim("Using auto-generated Drizzle config\n"));
|
|
4075
4154
|
}
|
|
4076
4155
|
const studioProcess = spawn3("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
|
|
@@ -4079,7 +4158,7 @@ async function dbStudio(requestedPort) {
|
|
|
4079
4158
|
env: shouldRelaxDbTls(env6.DATABASE_URL) ? { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED: "0" } : { ...process.env }
|
|
4080
4159
|
});
|
|
4081
4160
|
const cleanup = () => {
|
|
4082
|
-
if (!hasUserConfig &&
|
|
4161
|
+
if (!hasUserConfig && existsSync24(tempConfigPath)) {
|
|
4083
4162
|
unlinkSync3(tempConfigPath);
|
|
4084
4163
|
}
|
|
4085
4164
|
};
|
|
@@ -4109,7 +4188,7 @@ async function dbStudio(requestedPort) {
|
|
|
4109
4188
|
process.exit(0);
|
|
4110
4189
|
});
|
|
4111
4190
|
} catch (error) {
|
|
4112
|
-
if (!hasUserConfig &&
|
|
4191
|
+
if (!hasUserConfig && existsSync24(tempConfigPath)) {
|
|
4113
4192
|
unlinkSync3(tempConfigPath);
|
|
4114
4193
|
}
|
|
4115
4194
|
console.error(chalk22.red("\u274C Failed to start Drizzle Studio"));
|
|
@@ -4496,8 +4575,8 @@ async function dbBackupClean(options) {
|
|
|
4496
4575
|
}
|
|
4497
4576
|
|
|
4498
4577
|
// src/commands/db/reindex.ts
|
|
4499
|
-
import { existsSync as
|
|
4500
|
-
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";
|
|
4501
4580
|
import chalk28 from "chalk";
|
|
4502
4581
|
import { loadEnv as loadEnv9 } from "@spfn/core/server";
|
|
4503
4582
|
function isTimestampPrefix(tag) {
|
|
@@ -4519,8 +4598,8 @@ async function dbReindex(options = {}) {
|
|
|
4519
4598
|
const { getDrizzleConfig } = await import("@spfn/core/db");
|
|
4520
4599
|
const config = getDrizzleConfig({ disablePackageDiscovery: true });
|
|
4521
4600
|
const outDir = config.out;
|
|
4522
|
-
const journalPath =
|
|
4523
|
-
if (!
|
|
4601
|
+
const journalPath = join23(outDir, "meta", "_journal.json");
|
|
4602
|
+
if (!existsSync25(journalPath)) {
|
|
4524
4603
|
console.error(chalk28.red("\u274C No _journal.json found at:"), journalPath);
|
|
4525
4604
|
console.log(chalk28.yellow("\u{1F4A1} Run `spfn db generate` first to create migrations"));
|
|
4526
4605
|
process.exit(1);
|
|
@@ -4541,14 +4620,14 @@ async function dbReindex(options = {}) {
|
|
|
4541
4620
|
const { prefix: oldPrefix, suffix } = parseTag(entry.tag);
|
|
4542
4621
|
const newPrefix = String(entry.when);
|
|
4543
4622
|
const newTag = suffix ? `${newPrefix}_${suffix}` : newPrefix;
|
|
4544
|
-
const oldSql =
|
|
4545
|
-
const newSql =
|
|
4546
|
-
if (
|
|
4623
|
+
const oldSql = join23(outDir, `${entry.tag}.sql`);
|
|
4624
|
+
const newSql = join23(outDir, `${newTag}.sql`);
|
|
4625
|
+
if (existsSync25(oldSql)) {
|
|
4547
4626
|
renames.push({ type: "sql", from: oldSql, to: newSql });
|
|
4548
4627
|
}
|
|
4549
|
-
const oldSnapshot =
|
|
4550
|
-
const newSnapshot =
|
|
4551
|
-
if (
|
|
4628
|
+
const oldSnapshot = join23(outDir, "meta", `${oldPrefix}_snapshot.json`);
|
|
4629
|
+
const newSnapshot = join23(outDir, "meta", `${newPrefix}_snapshot.json`);
|
|
4630
|
+
if (existsSync25(oldSnapshot)) {
|
|
4552
4631
|
renames.push({ type: "snapshot", from: oldSnapshot, to: newSnapshot });
|
|
4553
4632
|
}
|
|
4554
4633
|
tagUpdates.push({ idx: entry.idx, oldTag: entry.tag, newTag });
|
|
@@ -4591,13 +4670,13 @@ async function dbReindex(options = {}) {
|
|
|
4591
4670
|
entry.tag = update.newTag;
|
|
4592
4671
|
}
|
|
4593
4672
|
}
|
|
4594
|
-
|
|
4673
|
+
writeFileSync16(journalPath, JSON.stringify(journal, null, 2) + "\n");
|
|
4595
4674
|
console.log(chalk28.green(`
|
|
4596
4675
|
\u2705 Reindex complete \u2014 ${tagUpdates.length} migration(s) converted to timestamp prefix.`));
|
|
4597
4676
|
}
|
|
4598
4677
|
|
|
4599
4678
|
// src/commands/db/index.ts
|
|
4600
|
-
var dbCommand = new
|
|
4679
|
+
var dbCommand = new Command10("db").description("Database management commands (wraps Drizzle Kit)");
|
|
4601
4680
|
dbCommand.command("generate").alias("g").description("Generate database migrations from schema changes").action(dbGenerate);
|
|
4602
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));
|
|
4603
4682
|
dbCommand.command("migrate").alias("m").description("Run pending migrations").option("--with-backup", "Create backup before running migrations").action((options) => dbMigrate(options));
|
|
@@ -4613,54 +4692,114 @@ dbCommand.command("reindex").description("Convert migration files from sequentia
|
|
|
4613
4692
|
|
|
4614
4693
|
// src/commands/add.ts
|
|
4615
4694
|
init_package_manager();
|
|
4616
|
-
import { Command as
|
|
4617
|
-
import { existsSync as
|
|
4618
|
-
import { join as
|
|
4619
|
-
import { execa as
|
|
4620
|
-
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";
|
|
4621
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
|
|
4622
4757
|
async function addPackage(packageName) {
|
|
4758
|
+
if (packageName === "vercel") {
|
|
4759
|
+
await addVercel();
|
|
4760
|
+
return;
|
|
4761
|
+
}
|
|
4623
4762
|
if (!packageName.includes("/")) {
|
|
4624
|
-
console.error(
|
|
4625
|
-
console.log(
|
|
4626
|
-
console.log(
|
|
4627
|
-
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"));
|
|
4628
4767
|
process.exit(1);
|
|
4629
4768
|
}
|
|
4630
|
-
console.log(
|
|
4769
|
+
console.log(chalk30.blue(`
|
|
4631
4770
|
\u{1F4E6} Setting up ${packageName}...
|
|
4632
4771
|
`));
|
|
4633
|
-
const pkgPath =
|
|
4634
|
-
const pkgJsonPath =
|
|
4635
|
-
if (!
|
|
4772
|
+
const pkgPath = join25(process.cwd(), "node_modules", ...packageName.split("/"));
|
|
4773
|
+
const pkgJsonPath = join25(pkgPath, "package.json");
|
|
4774
|
+
if (!existsSync27(pkgJsonPath)) {
|
|
4636
4775
|
const pm = detectPackageManager(process.cwd());
|
|
4637
4776
|
const installSpinner = ora11("Installing package...").start();
|
|
4638
4777
|
try {
|
|
4639
|
-
await
|
|
4778
|
+
await execa10(pm, ["add", packageName]);
|
|
4640
4779
|
installSpinner.succeed("Package installed");
|
|
4641
4780
|
} catch (error) {
|
|
4642
4781
|
installSpinner.fail("Failed to install package");
|
|
4643
|
-
console.error(
|
|
4782
|
+
console.error(chalk30.red(error instanceof Error ? error.message : "Unknown error"));
|
|
4644
4783
|
process.exit(1);
|
|
4645
4784
|
}
|
|
4646
4785
|
} else {
|
|
4647
|
-
console.log(
|
|
4786
|
+
console.log(chalk30.gray("\u2713 Package already installed (using local version)\n"));
|
|
4648
4787
|
}
|
|
4649
|
-
if (!
|
|
4650
|
-
console.error(
|
|
4788
|
+
if (!existsSync27(pkgJsonPath)) {
|
|
4789
|
+
console.error(chalk30.red(`\u274C Package ${packageName} not found after installation`));
|
|
4651
4790
|
process.exit(1);
|
|
4652
4791
|
}
|
|
4653
4792
|
const pkgJson = JSON.parse(readFileSync11(pkgJsonPath, "utf-8"));
|
|
4654
4793
|
if (pkgJson.spfn?.migrations) {
|
|
4655
|
-
console.log(
|
|
4794
|
+
console.log(chalk30.blue(`
|
|
4656
4795
|
\u{1F5C4}\uFE0F Setting up database for ${packageName}...
|
|
4657
4796
|
`));
|
|
4658
4797
|
const { env: env9 } = await import("@spfn/core/config");
|
|
4659
4798
|
if (!env9.DATABASE_URL) {
|
|
4660
|
-
console.log(
|
|
4661
|
-
console.log(
|
|
4662
|
-
console.log(
|
|
4663
|
-
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"));
|
|
4664
4803
|
} else {
|
|
4665
4804
|
const { discoverFunctionMigrations: discoverFunctionMigrations2, loadFunctionMigrationPlans: loadFunctionMigrationPlans2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
4666
4805
|
const functions = discoverFunctionMigrations2(process.cwd());
|
|
@@ -4672,31 +4811,31 @@ async function addPackage(packageName) {
|
|
|
4672
4811
|
spinner.succeed("Migrations applied");
|
|
4673
4812
|
} catch (error) {
|
|
4674
4813
|
spinner.fail("Failed to apply migrations");
|
|
4675
|
-
console.error(
|
|
4814
|
+
console.error(chalk30.red(error instanceof Error ? error.message : "Unknown error"));
|
|
4676
4815
|
process.exit(1);
|
|
4677
4816
|
}
|
|
4678
4817
|
} else {
|
|
4679
|
-
console.log(
|
|
4818
|
+
console.log(chalk30.gray("\u2139\uFE0F No migrations found for this package"));
|
|
4680
4819
|
}
|
|
4681
4820
|
}
|
|
4682
4821
|
} else {
|
|
4683
|
-
console.log(
|
|
4822
|
+
console.log(chalk30.gray("\n\u2139\uFE0F No database migrations to apply"));
|
|
4684
4823
|
}
|
|
4685
|
-
console.log(
|
|
4824
|
+
console.log(chalk30.green(`
|
|
4686
4825
|
\u2705 ${packageName} installed successfully!
|
|
4687
4826
|
`));
|
|
4688
4827
|
if (pkgJson.spfn?.setupMessage) {
|
|
4689
|
-
console.log(
|
|
4828
|
+
console.log(chalk30.cyan("\u{1F4DA} Setup Guide:"));
|
|
4690
4829
|
console.log(pkgJson.spfn.setupMessage);
|
|
4691
4830
|
console.log();
|
|
4692
4831
|
}
|
|
4693
4832
|
}
|
|
4694
|
-
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);
|
|
4695
4834
|
|
|
4696
4835
|
// src/commands/env.ts
|
|
4697
|
-
import { Command as
|
|
4698
|
-
import
|
|
4699
|
-
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";
|
|
4700
4839
|
import { resolve as resolve2 } from "path";
|
|
4701
4840
|
import { parse as parse2 } from "dotenv";
|
|
4702
4841
|
|
|
@@ -4752,26 +4891,26 @@ function getEnvFilesForEnvironment(nodeEnv) {
|
|
|
4752
4891
|
}
|
|
4753
4892
|
function formatType(type) {
|
|
4754
4893
|
const typeColors = {
|
|
4755
|
-
string:
|
|
4756
|
-
number:
|
|
4757
|
-
boolean:
|
|
4758
|
-
url:
|
|
4759
|
-
enum:
|
|
4760
|
-
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
|
|
4761
4900
|
};
|
|
4762
|
-
return (typeColors[type] ||
|
|
4901
|
+
return (typeColors[type] || chalk31.white)(type);
|
|
4763
4902
|
}
|
|
4764
4903
|
function formatDefault(value, type) {
|
|
4765
4904
|
if (value === void 0) {
|
|
4766
|
-
return
|
|
4905
|
+
return chalk31.dim("(none)");
|
|
4767
4906
|
}
|
|
4768
4907
|
if (type === "string" || type === "url") {
|
|
4769
|
-
return
|
|
4908
|
+
return chalk31.green(`"${value}"`);
|
|
4770
4909
|
}
|
|
4771
4910
|
if (type === "boolean") {
|
|
4772
|
-
return value ?
|
|
4911
|
+
return value ? chalk31.green("true") : chalk31.red("false");
|
|
4773
4912
|
}
|
|
4774
|
-
return
|
|
4913
|
+
return chalk31.cyan(String(value));
|
|
4775
4914
|
}
|
|
4776
4915
|
async function listEnvVars(options) {
|
|
4777
4916
|
const packageName = options.package || "@spfn/core";
|
|
@@ -4785,28 +4924,28 @@ async function listEnvVars(options) {
|
|
|
4785
4924
|
acc[target].push([key, schema]);
|
|
4786
4925
|
return acc;
|
|
4787
4926
|
}, {});
|
|
4788
|
-
console.log(
|
|
4927
|
+
console.log(chalk31.blue.bold(`
|
|
4789
4928
|
\u{1F4CB} Environment Variables by File (${packageName})
|
|
4790
4929
|
`));
|
|
4791
4930
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
4792
|
-
console.log(
|
|
4931
|
+
console.log(chalk31.bold.magenta(`
|
|
4793
4932
|
${file}`));
|
|
4794
|
-
console.log(
|
|
4933
|
+
console.log(chalk31.dim("\u2500".repeat(50)));
|
|
4795
4934
|
for (const [key, schema] of vars) {
|
|
4796
4935
|
printEnvVar(key, schema);
|
|
4797
4936
|
}
|
|
4798
4937
|
}
|
|
4799
4938
|
} else {
|
|
4800
|
-
console.log(
|
|
4939
|
+
console.log(chalk31.blue.bold(`
|
|
4801
4940
|
\u{1F4CB} Environment Variables (${packageName})
|
|
4802
4941
|
`));
|
|
4803
4942
|
for (const [key, schema] of allVars) {
|
|
4804
4943
|
printEnvVar(key, schema, true);
|
|
4805
4944
|
}
|
|
4806
4945
|
}
|
|
4807
|
-
console.log(
|
|
4946
|
+
console.log(chalk31.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
|
|
4808
4947
|
} catch (error) {
|
|
4809
|
-
console.error(
|
|
4948
|
+
console.error(chalk31.red(`
|
|
4810
4949
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4811
4950
|
`));
|
|
4812
4951
|
process.exit(1);
|
|
@@ -4814,17 +4953,17 @@ ${file}`));
|
|
|
4814
4953
|
}
|
|
4815
4954
|
function printEnvVar(key, schema, showFile = false) {
|
|
4816
4955
|
const typeStr = formatType(schema.type);
|
|
4817
|
-
const requiredStr = schema.required || schema.default !== void 0 ?
|
|
4818
|
-
const sensitiveStr = schema.sensitive ?
|
|
4819
|
-
const fileStr = showFile ?
|
|
4820
|
-
console.log(`${
|
|
4821
|
-
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)}`);
|
|
4822
4961
|
if (schema.default !== void 0) {
|
|
4823
|
-
console.log(` ${
|
|
4962
|
+
console.log(` ${chalk31.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
4824
4963
|
}
|
|
4825
4964
|
if (schema.examples && schema.examples.length > 0) {
|
|
4826
4965
|
const exampleStr = schema.examples.map((ex) => formatDefault(ex, schema.type)).join(", ");
|
|
4827
|
-
console.log(` ${
|
|
4966
|
+
console.log(` ${chalk31.dim("Examples:")} ${exampleStr}`);
|
|
4828
4967
|
}
|
|
4829
4968
|
console.log();
|
|
4830
4969
|
}
|
|
@@ -4832,7 +4971,7 @@ async function showEnvStats(options) {
|
|
|
4832
4971
|
const packageName = options.package || "@spfn/core";
|
|
4833
4972
|
try {
|
|
4834
4973
|
const envSchema = await loadEnvSchema(packageName);
|
|
4835
|
-
console.log(
|
|
4974
|
+
console.log(chalk31.blue.bold(`
|
|
4836
4975
|
\u{1F4CA} Environment Variable Statistics (${packageName})
|
|
4837
4976
|
`));
|
|
4838
4977
|
const allVars = Object.entries(envSchema);
|
|
@@ -4854,24 +4993,24 @@ async function showEnvStats(options) {
|
|
|
4854
4993
|
acc[file] = (acc[file] || 0) + 1;
|
|
4855
4994
|
return acc;
|
|
4856
4995
|
}, {});
|
|
4857
|
-
console.log(`${
|
|
4858
|
-
console.log(`${
|
|
4859
|
-
console.log(`${
|
|
4860
|
-
console.log(`${
|
|
4861
|
-
console.log(
|
|
4862
|
-
console.log(` ${
|
|
4863
|
-
console.log(` ${
|
|
4864
|
-
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:"));
|
|
4865
5004
|
for (const [file, count] of Object.entries(fileCount)) {
|
|
4866
|
-
console.log(` ${
|
|
5005
|
+
console.log(` ${chalk31.dim(file)}: ${chalk31.cyan(count)}`);
|
|
4867
5006
|
}
|
|
4868
|
-
console.log(
|
|
5007
|
+
console.log(chalk31.bold("\nBy Type:"));
|
|
4869
5008
|
for (const [type, count] of Object.entries(typeCount)) {
|
|
4870
|
-
console.log(` ${formatType(type)}: ${
|
|
5009
|
+
console.log(` ${formatType(type)}: ${chalk31.cyan(count)}`);
|
|
4871
5010
|
}
|
|
4872
5011
|
console.log();
|
|
4873
5012
|
} catch (error) {
|
|
4874
|
-
console.error(
|
|
5013
|
+
console.error(chalk31.red(`
|
|
4875
5014
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4876
5015
|
`));
|
|
4877
5016
|
process.exit(1);
|
|
@@ -4891,40 +5030,40 @@ async function searchEnvVars(query, options) {
|
|
|
4891
5030
|
}
|
|
4892
5031
|
}
|
|
4893
5032
|
if (results.length === 0) {
|
|
4894
|
-
console.log(
|
|
5033
|
+
console.log(chalk31.yellow(`
|
|
4895
5034
|
\u26A0\uFE0F No environment variables found matching "${query}"
|
|
4896
5035
|
`));
|
|
4897
5036
|
return;
|
|
4898
5037
|
}
|
|
4899
|
-
console.log(
|
|
5038
|
+
console.log(chalk31.blue.bold(`
|
|
4900
5039
|
\u{1F50D} Found ${results.length} environment variable(s) matching "${query}"
|
|
4901
5040
|
`));
|
|
4902
5041
|
for (const [key, schema] of results) {
|
|
4903
5042
|
const typeStr = formatType(schema.type);
|
|
4904
|
-
const requiredStr = schema.required || schema.default !== void 0 ?
|
|
4905
|
-
console.log(`${
|
|
4906
|
-
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)}`);
|
|
4907
5046
|
if (schema.default !== void 0) {
|
|
4908
|
-
console.log(` ${
|
|
5047
|
+
console.log(` ${chalk31.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
4909
5048
|
}
|
|
4910
5049
|
console.log();
|
|
4911
5050
|
}
|
|
4912
5051
|
} catch (error) {
|
|
4913
|
-
console.error(
|
|
5052
|
+
console.error(chalk31.red(`
|
|
4914
5053
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4915
5054
|
`));
|
|
4916
5055
|
process.exit(1);
|
|
4917
5056
|
}
|
|
4918
5057
|
}
|
|
4919
|
-
var envCommand = new
|
|
5058
|
+
var envCommand = new Command12("env").description("Manage environment variables");
|
|
4920
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);
|
|
4921
5060
|
envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(showEnvStats);
|
|
4922
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);
|
|
4923
5062
|
function validateEnvOption(envValue) {
|
|
4924
5063
|
if (!VALID_ENVS.includes(envValue)) {
|
|
4925
|
-
console.error(
|
|
5064
|
+
console.error(chalk31.red(`
|
|
4926
5065
|
\u274C Invalid environment: "${envValue}"`));
|
|
4927
|
-
console.log(
|
|
5066
|
+
console.log(chalk31.dim(` Valid values: ${VALID_ENVS.join(", ")}
|
|
4928
5067
|
`));
|
|
4929
5068
|
process.exit(1);
|
|
4930
5069
|
}
|
|
@@ -4945,8 +5084,8 @@ async function initEnvFiles(options) {
|
|
|
4945
5084
|
return acc;
|
|
4946
5085
|
}, {});
|
|
4947
5086
|
if (targetEnv) {
|
|
4948
|
-
console.log(
|
|
4949
|
-
\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
|
|
4950
5089
|
`));
|
|
4951
5090
|
const envSpecificFiles = {};
|
|
4952
5091
|
const committedVars = allVars.filter(([_, schema]) => !schema.sensitive);
|
|
@@ -4962,24 +5101,24 @@ async function initEnvFiles(options) {
|
|
|
4962
5101
|
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
4963
5102
|
}
|
|
4964
5103
|
} else {
|
|
4965
|
-
console.log(
|
|
5104
|
+
console.log(chalk31.blue.bold(`
|
|
4966
5105
|
\u{1F680} Generating .env template files
|
|
4967
5106
|
`));
|
|
4968
5107
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
4969
5108
|
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
4970
5109
|
}
|
|
4971
5110
|
}
|
|
4972
|
-
console.log(
|
|
4973
|
-
console.log(
|
|
4974
|
-
console.log(
|
|
4975
|
-
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"));
|
|
4976
5115
|
if (targetEnv) {
|
|
4977
|
-
console.log(
|
|
4978
|
-
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`));
|
|
4979
5118
|
}
|
|
4980
5119
|
console.log("");
|
|
4981
5120
|
} catch (error) {
|
|
4982
|
-
console.error(
|
|
5121
|
+
console.error(chalk31.red(`
|
|
4983
5122
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4984
5123
|
`));
|
|
4985
5124
|
process.exit(1);
|
|
@@ -4987,12 +5126,12 @@ async function initEnvFiles(options) {
|
|
|
4987
5126
|
}
|
|
4988
5127
|
function writeEnvTemplate(cwd, file, vars, force) {
|
|
4989
5128
|
const filePath = resolve2(cwd, file);
|
|
4990
|
-
if (
|
|
4991
|
-
console.log(
|
|
5129
|
+
if (existsSync28(filePath) && !force) {
|
|
5130
|
+
console.log(chalk31.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
|
|
4992
5131
|
return;
|
|
4993
5132
|
}
|
|
4994
|
-
|
|
4995
|
-
console.log(
|
|
5133
|
+
writeFileSync17(filePath, generateEnvFileContent(vars), "utf-8");
|
|
5134
|
+
console.log(chalk31.green(` \u2705 ${file} (${vars.length} variables)`));
|
|
4996
5135
|
}
|
|
4997
5136
|
function generateEnvFileContent(vars) {
|
|
4998
5137
|
const lines = [
|
|
@@ -5027,7 +5166,7 @@ async function checkEnvFiles(options) {
|
|
|
5027
5166
|
const envSchema = await loadEnvSchema(packageName);
|
|
5028
5167
|
const allVars = Object.entries(envSchema);
|
|
5029
5168
|
const envLabel = targetEnv ? ` (${targetEnv})` : "";
|
|
5030
|
-
console.log(
|
|
5169
|
+
console.log(chalk31.blue.bold(`
|
|
5031
5170
|
\u{1F50D} Checking .env files against schema${envLabel}
|
|
5032
5171
|
`));
|
|
5033
5172
|
const filesToCheck = targetEnv ? getEnvFilesForEnvironment(targetEnv) : [...BASE_ENV_FILES.nextjs, ...BASE_ENV_FILES.server];
|
|
@@ -5036,7 +5175,7 @@ async function checkEnvFiles(options) {
|
|
|
5036
5175
|
const warnings = [];
|
|
5037
5176
|
for (const file of filesToCheck) {
|
|
5038
5177
|
const filePath = resolve2(cwd, file);
|
|
5039
|
-
if (!
|
|
5178
|
+
if (!existsSync28(filePath)) {
|
|
5040
5179
|
continue;
|
|
5041
5180
|
}
|
|
5042
5181
|
const content = readFileSync12(filePath, "utf-8");
|
|
@@ -5044,7 +5183,7 @@ async function checkEnvFiles(options) {
|
|
|
5044
5183
|
for (const [key, value] of Object.entries(parsed)) {
|
|
5045
5184
|
loadedEnv[key] = { value: value || "", file };
|
|
5046
5185
|
}
|
|
5047
|
-
console.log(
|
|
5186
|
+
console.log(chalk31.dim(` \u{1F4C4} ${file} loaded`));
|
|
5048
5187
|
}
|
|
5049
5188
|
console.log("");
|
|
5050
5189
|
for (const [key, schema] of allVars) {
|
|
@@ -5052,7 +5191,7 @@ async function checkEnvFiles(options) {
|
|
|
5052
5191
|
const found = loadedEnv[key];
|
|
5053
5192
|
if (!found) {
|
|
5054
5193
|
if (schema.required && schema.default === void 0) {
|
|
5055
|
-
issues.push(`${
|
|
5194
|
+
issues.push(`${chalk31.red("\u2717")} ${chalk31.cyan(key)} is required but not found in any .env file`);
|
|
5056
5195
|
}
|
|
5057
5196
|
continue;
|
|
5058
5197
|
}
|
|
@@ -5062,11 +5201,11 @@ async function checkEnvFiles(options) {
|
|
|
5062
5201
|
if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
|
|
5063
5202
|
if (schema.sensitive) {
|
|
5064
5203
|
issues.push(
|
|
5065
|
-
`${
|
|
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!)`
|
|
5066
5205
|
);
|
|
5067
5206
|
} else {
|
|
5068
5207
|
warnings.push(
|
|
5069
|
-
`${
|
|
5208
|
+
`${chalk31.yellow("\u26A0")} ${chalk31.cyan(key)} should be in ${chalk31.magenta(expectedFile)}, but found in ${chalk31.dim(found.file)}`
|
|
5070
5209
|
);
|
|
5071
5210
|
}
|
|
5072
5211
|
}
|
|
@@ -5074,34 +5213,34 @@ async function checkEnvFiles(options) {
|
|
|
5074
5213
|
for (const [key, { file }] of Object.entries(loadedEnv)) {
|
|
5075
5214
|
const inSchema = allVars.some(([k]) => k === key);
|
|
5076
5215
|
if (!inSchema) {
|
|
5077
|
-
warnings.push(`${
|
|
5216
|
+
warnings.push(`${chalk31.yellow("\u26A0")} ${chalk31.cyan(key)} in ${chalk31.dim(file)} is not in schema`);
|
|
5078
5217
|
}
|
|
5079
5218
|
}
|
|
5080
5219
|
if (issues.length > 0) {
|
|
5081
|
-
console.log(
|
|
5220
|
+
console.log(chalk31.red.bold("Issues:"));
|
|
5082
5221
|
for (const issue of issues) {
|
|
5083
5222
|
console.log(` ${issue}`);
|
|
5084
5223
|
}
|
|
5085
5224
|
console.log("");
|
|
5086
5225
|
}
|
|
5087
5226
|
if (warnings.length > 0) {
|
|
5088
|
-
console.log(
|
|
5227
|
+
console.log(chalk31.yellow.bold("Warnings:"));
|
|
5089
5228
|
for (const warning of warnings) {
|
|
5090
5229
|
console.log(` ${warning}`);
|
|
5091
5230
|
}
|
|
5092
5231
|
console.log("");
|
|
5093
5232
|
}
|
|
5094
5233
|
if (issues.length === 0 && warnings.length === 0) {
|
|
5095
|
-
console.log(
|
|
5234
|
+
console.log(chalk31.green("\u2705 All environment variables are correctly configured!\n"));
|
|
5096
5235
|
} else {
|
|
5097
|
-
console.log(
|
|
5236
|
+
console.log(chalk31.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
|
|
5098
5237
|
`));
|
|
5099
5238
|
if (issues.length > 0) {
|
|
5100
5239
|
process.exit(1);
|
|
5101
5240
|
}
|
|
5102
5241
|
}
|
|
5103
5242
|
} catch (error) {
|
|
5104
|
-
console.error(
|
|
5243
|
+
console.error(chalk31.red(`
|
|
5105
5244
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
5106
5245
|
`));
|
|
5107
5246
|
process.exit(1);
|
|
@@ -5115,15 +5254,15 @@ async function validateEnvVars(options) {
|
|
|
5115
5254
|
if (targetEnv) {
|
|
5116
5255
|
const { loadEnv: loadEnv10 } = await import("@spfn/core/env/loader");
|
|
5117
5256
|
const result = loadEnv10({ nodeEnv: targetEnv });
|
|
5118
|
-
console.log(
|
|
5119
|
-
\u{1F50D} Validating environment variables for ${
|
|
5257
|
+
console.log(chalk31.blue.bold(`
|
|
5258
|
+
\u{1F50D} Validating environment variables for ${chalk31.cyan(targetEnv)}
|
|
5120
5259
|
`));
|
|
5121
5260
|
if (result.loadedFiles.length > 0) {
|
|
5122
|
-
console.log(
|
|
5261
|
+
console.log(chalk31.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
|
|
5123
5262
|
}
|
|
5124
5263
|
console.log("");
|
|
5125
5264
|
} else {
|
|
5126
|
-
console.log(
|
|
5265
|
+
console.log(chalk31.blue.bold(`
|
|
5127
5266
|
\u{1F50D} Validating environment variables
|
|
5128
5267
|
`));
|
|
5129
5268
|
}
|
|
@@ -5131,7 +5270,7 @@ async function validateEnvVars(options) {
|
|
|
5131
5270
|
const allWarnings = [];
|
|
5132
5271
|
for (const packageName of packages) {
|
|
5133
5272
|
try {
|
|
5134
|
-
console.log(
|
|
5273
|
+
console.log(chalk31.dim(` \u{1F4E6} ${packageName}`));
|
|
5135
5274
|
const envSchema = await loadEnvSchema(packageName);
|
|
5136
5275
|
const { createEnvRegistry } = await import("@spfn/core/env");
|
|
5137
5276
|
const registry = createEnvRegistry(envSchema);
|
|
@@ -5144,10 +5283,10 @@ async function validateEnvVars(options) {
|
|
|
5144
5283
|
}
|
|
5145
5284
|
} catch (error) {
|
|
5146
5285
|
if (error instanceof Error && error.message.includes("does not export envSchema")) {
|
|
5147
|
-
console.log(
|
|
5286
|
+
console.log(chalk31.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
|
|
5148
5287
|
continue;
|
|
5149
5288
|
}
|
|
5150
|
-
console.error(
|
|
5289
|
+
console.error(chalk31.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
|
|
5151
5290
|
if (options.strict) {
|
|
5152
5291
|
process.exit(1);
|
|
5153
5292
|
}
|
|
@@ -5155,32 +5294,32 @@ async function validateEnvVars(options) {
|
|
|
5155
5294
|
}
|
|
5156
5295
|
console.log("");
|
|
5157
5296
|
if (allErrors.length > 0) {
|
|
5158
|
-
console.log(
|
|
5297
|
+
console.log(chalk31.red.bold(`\u274C Validation Errors (${allErrors.length}):
|
|
5159
5298
|
`));
|
|
5160
5299
|
for (const error of allErrors) {
|
|
5161
|
-
console.log(` ${
|
|
5162
|
-
console.log(` ${
|
|
5163
|
-
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}`)}`);
|
|
5164
5303
|
console.log("");
|
|
5165
5304
|
}
|
|
5166
5305
|
}
|
|
5167
5306
|
if (allWarnings.length > 0) {
|
|
5168
|
-
console.log(
|
|
5307
|
+
console.log(chalk31.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
|
|
5169
5308
|
`));
|
|
5170
5309
|
for (const warning of allWarnings) {
|
|
5171
|
-
console.log(` ${
|
|
5172
|
-
console.log(` ${
|
|
5310
|
+
console.log(` ${chalk31.yellow("\u26A0")} ${chalk31.cyan(warning.key)}`);
|
|
5311
|
+
console.log(` ${chalk31.dim(warning.message)}`);
|
|
5173
5312
|
console.log("");
|
|
5174
5313
|
}
|
|
5175
5314
|
}
|
|
5176
5315
|
if (allErrors.length === 0 && allWarnings.length === 0) {
|
|
5177
|
-
console.log(
|
|
5316
|
+
console.log(chalk31.green.bold("\u2705 All environment variables are valid!\n"));
|
|
5178
5317
|
} else if (allErrors.length === 0) {
|
|
5179
|
-
console.log(
|
|
5180
|
-
console.log(
|
|
5318
|
+
console.log(chalk31.green("\u2705 No errors found."));
|
|
5319
|
+
console.log(chalk31.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
|
|
5181
5320
|
`));
|
|
5182
5321
|
} else {
|
|
5183
|
-
console.log(
|
|
5322
|
+
console.log(chalk31.red(`
|
|
5184
5323
|
\u274C Validation failed with ${allErrors.length} error(s)
|
|
5185
5324
|
`));
|
|
5186
5325
|
process.exit(1);
|
|
@@ -5189,12 +5328,12 @@ async function validateEnvVars(options) {
|
|
|
5189
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);
|
|
5190
5329
|
|
|
5191
5330
|
// src/commands/secret/index.ts
|
|
5192
|
-
import { Command as
|
|
5331
|
+
import { Command as Command13 } from "commander";
|
|
5193
5332
|
|
|
5194
5333
|
// src/commands/secret/set.ts
|
|
5195
5334
|
init_logger();
|
|
5196
5335
|
import prompts9 from "prompts";
|
|
5197
|
-
import
|
|
5336
|
+
import chalk32 from "chalk";
|
|
5198
5337
|
|
|
5199
5338
|
// src/commands/secret/options.ts
|
|
5200
5339
|
init_logger();
|
|
@@ -5209,34 +5348,34 @@ function resolveEnv(env9) {
|
|
|
5209
5348
|
|
|
5210
5349
|
// src/commands/secret/store-value.ts
|
|
5211
5350
|
init_logger();
|
|
5212
|
-
import { join as
|
|
5351
|
+
import { join as join27 } from "path";
|
|
5213
5352
|
init_env_file();
|
|
5214
5353
|
|
|
5215
5354
|
// src/utils/sops.ts
|
|
5216
|
-
import { execa as
|
|
5217
|
-
import { existsSync as
|
|
5218
|
-
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";
|
|
5219
5358
|
async function ensureSopsInstalled() {
|
|
5220
5359
|
try {
|
|
5221
|
-
await
|
|
5360
|
+
await execa11("sops", ["--version"]);
|
|
5222
5361
|
} catch {
|
|
5223
5362
|
throw new Error("`sops` not found on PATH. Install it: https://github.com/getsops/sops");
|
|
5224
5363
|
}
|
|
5225
5364
|
}
|
|
5226
5365
|
async function sopsDecrypt(absFile) {
|
|
5227
|
-
if (!
|
|
5366
|
+
if (!existsSync29(absFile)) {
|
|
5228
5367
|
return {};
|
|
5229
5368
|
}
|
|
5230
|
-
const { stdout } = await
|
|
5369
|
+
const { stdout } = await execa11("sops", ["--decrypt", "--output-type", "json", absFile]);
|
|
5231
5370
|
return JSON.parse(stdout);
|
|
5232
5371
|
}
|
|
5233
5372
|
async function sopsSetValue(absFile, relFile, key, value) {
|
|
5234
|
-
if (
|
|
5235
|
-
await
|
|
5373
|
+
if (existsSync29(absFile)) {
|
|
5374
|
+
await execa11("sops", ["set", absFile, `["${key}"]`, JSON.stringify(value)]);
|
|
5236
5375
|
return;
|
|
5237
5376
|
}
|
|
5238
|
-
|
|
5239
|
-
const { stdout } = await
|
|
5377
|
+
mkdirSync5(dirname4(absFile), { recursive: true });
|
|
5378
|
+
const { stdout } = await execa11(
|
|
5240
5379
|
"sops",
|
|
5241
5380
|
[
|
|
5242
5381
|
"--encrypt",
|
|
@@ -5250,27 +5389,27 @@ async function sopsSetValue(absFile, relFile, key, value) {
|
|
|
5250
5389
|
],
|
|
5251
5390
|
{ input: JSON.stringify({ [key]: value }) }
|
|
5252
5391
|
);
|
|
5253
|
-
|
|
5392
|
+
writeFileSync18(absFile, stdout);
|
|
5254
5393
|
}
|
|
5255
5394
|
async function sopsUpdateKeys(absFile) {
|
|
5256
|
-
await
|
|
5395
|
+
await execa11("sops", ["updatekeys", "-y", absFile]);
|
|
5257
5396
|
}
|
|
5258
5397
|
|
|
5259
5398
|
// src/utils/secret-config.ts
|
|
5260
|
-
import { existsSync as
|
|
5261
|
-
import { join as
|
|
5399
|
+
import { existsSync as existsSync30, readdirSync as readdirSync3 } from "fs";
|
|
5400
|
+
import { join as join26 } from "path";
|
|
5262
5401
|
var SECRETS_DIR = "secrets";
|
|
5263
5402
|
function getSopsFile(cwd, env9) {
|
|
5264
5403
|
const relFile = `${SECRETS_DIR}/${env9}.enc.json`;
|
|
5265
|
-
return { absFile:
|
|
5404
|
+
return { absFile: join26(cwd, relFile), relFile };
|
|
5266
5405
|
}
|
|
5267
5406
|
function findUp(cwd, filename, maxDepth = 6) {
|
|
5268
5407
|
let dir = cwd;
|
|
5269
5408
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
5270
|
-
if (
|
|
5409
|
+
if (existsSync30(join26(dir, filename))) {
|
|
5271
5410
|
return dir;
|
|
5272
5411
|
}
|
|
5273
|
-
const parent =
|
|
5412
|
+
const parent = join26(dir, "..");
|
|
5274
5413
|
if (parent === dir) {
|
|
5275
5414
|
break;
|
|
5276
5415
|
}
|
|
@@ -5280,17 +5419,17 @@ function findUp(cwd, filename, maxDepth = 6) {
|
|
|
5280
5419
|
}
|
|
5281
5420
|
function findSopsConfig(cwd) {
|
|
5282
5421
|
const dir = findUp(cwd, ".sops.yaml");
|
|
5283
|
-
return dir ?
|
|
5422
|
+
return dir ? join26(dir, ".sops.yaml") : null;
|
|
5284
5423
|
}
|
|
5285
5424
|
function hasSopsConfig(cwd) {
|
|
5286
5425
|
return findSopsConfig(cwd) !== null;
|
|
5287
5426
|
}
|
|
5288
5427
|
function listSopsFiles(cwd) {
|
|
5289
|
-
const dir =
|
|
5290
|
-
if (!
|
|
5428
|
+
const dir = join26(cwd, SECRETS_DIR);
|
|
5429
|
+
if (!existsSync30(dir)) {
|
|
5291
5430
|
return [];
|
|
5292
5431
|
}
|
|
5293
|
-
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));
|
|
5294
5433
|
}
|
|
5295
5434
|
|
|
5296
5435
|
// src/commands/secret/store-value.ts
|
|
@@ -5319,7 +5458,7 @@ async function storeSecret(cwd, env9, key, value) {
|
|
|
5319
5458
|
);
|
|
5320
5459
|
}
|
|
5321
5460
|
await store.set(keychainName(key), value);
|
|
5322
|
-
const serverEnvPath =
|
|
5461
|
+
const serverEnvPath = join27(cwd, ".env.server");
|
|
5323
5462
|
const result = upsertEnvVar(serverEnvPath, key, keychainRef(key));
|
|
5324
5463
|
restrictEnvFilePerms(serverEnvPath);
|
|
5325
5464
|
ensureGitignored(cwd, [{ pattern: ".env.server", comment: "spfn server env (secrets)" }]);
|
|
@@ -5350,7 +5489,7 @@ async function secretSet(key, options) {
|
|
|
5350
5489
|
const { value } = await prompts9({
|
|
5351
5490
|
type: "password",
|
|
5352
5491
|
name: "value",
|
|
5353
|
-
message: `Value for ${
|
|
5492
|
+
message: `Value for ${chalk32.cyan(resolvedKey)} (${env9})`
|
|
5354
5493
|
});
|
|
5355
5494
|
if (!value) {
|
|
5356
5495
|
logger.warn("Cancelled \u2014 no value entered.");
|
|
@@ -5399,7 +5538,7 @@ async function warnIfNotSecret(pkg, key) {
|
|
|
5399
5538
|
|
|
5400
5539
|
// src/commands/secret/list.ts
|
|
5401
5540
|
init_logger();
|
|
5402
|
-
import
|
|
5541
|
+
import chalk33 from "chalk";
|
|
5403
5542
|
async function secretList(options) {
|
|
5404
5543
|
const env9 = resolveEnv(options.env);
|
|
5405
5544
|
const pkg = options.package ?? "@spfn/core";
|
|
@@ -5415,12 +5554,12 @@ async function secretList(options) {
|
|
|
5415
5554
|
return;
|
|
5416
5555
|
}
|
|
5417
5556
|
const present = await loadPresence(env9, entries);
|
|
5418
|
-
console.log(
|
|
5557
|
+
console.log(chalk33.blue.bold(`
|
|
5419
5558
|
\u{1F511} Secrets (${pkg}) \u2014 ${env9}
|
|
5420
5559
|
`));
|
|
5421
5560
|
for (const entry of entries) {
|
|
5422
5561
|
const status = statusOf(entry, present.has(entry.key));
|
|
5423
|
-
console.log(` ${badge(status)} ${
|
|
5562
|
+
console.log(` ${badge(status)} ${chalk33.cyan(entry.key)}${entry.generate ? chalk33.dim(" (generatable)") : ""}`);
|
|
5424
5563
|
}
|
|
5425
5564
|
console.log();
|
|
5426
5565
|
}
|
|
@@ -5453,17 +5592,17 @@ function statusOf(entry, present) {
|
|
|
5453
5592
|
function badge(status) {
|
|
5454
5593
|
switch (status) {
|
|
5455
5594
|
case "set":
|
|
5456
|
-
return
|
|
5595
|
+
return chalk33.green("\u25CF");
|
|
5457
5596
|
case "missing":
|
|
5458
|
-
return
|
|
5597
|
+
return chalk33.yellow("\u25CB");
|
|
5459
5598
|
case "awaiting-input":
|
|
5460
|
-
return
|
|
5599
|
+
return chalk33.red("\u25CB");
|
|
5461
5600
|
}
|
|
5462
5601
|
}
|
|
5463
5602
|
|
|
5464
5603
|
// src/commands/secret/generate.ts
|
|
5465
5604
|
init_logger();
|
|
5466
|
-
import
|
|
5605
|
+
import chalk34 from "chalk";
|
|
5467
5606
|
async function secretGenerate(key, options) {
|
|
5468
5607
|
const env9 = resolveEnv(options.env);
|
|
5469
5608
|
const pkg = options.package ?? "@spfn/core";
|
|
@@ -5501,7 +5640,7 @@ function requireGeneratable(schema, key) {
|
|
|
5501
5640
|
process.exit(1);
|
|
5502
5641
|
}
|
|
5503
5642
|
if (!entry.generate) {
|
|
5504
|
-
logger.error(`${
|
|
5643
|
+
logger.error(`${chalk34.cyan(key)} has no generate strategy \u2014 it's an external value. Use \`spfn secret set ${key}\`.`);
|
|
5505
5644
|
process.exit(1);
|
|
5506
5645
|
}
|
|
5507
5646
|
return entry;
|
|
@@ -5509,7 +5648,7 @@ function requireGeneratable(schema, key) {
|
|
|
5509
5648
|
|
|
5510
5649
|
// src/commands/secret/rotate.ts
|
|
5511
5650
|
init_logger();
|
|
5512
|
-
import
|
|
5651
|
+
import chalk35 from "chalk";
|
|
5513
5652
|
async function secretRotate(key, options) {
|
|
5514
5653
|
const env9 = resolveEnv(options.env);
|
|
5515
5654
|
const pkg = options.package ?? "@spfn/core";
|
|
@@ -5535,7 +5674,7 @@ async function secretRotate(key, options) {
|
|
|
5535
5674
|
}
|
|
5536
5675
|
}
|
|
5537
5676
|
for (const entry of external) {
|
|
5538
|
-
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}\`.`);
|
|
5539
5678
|
}
|
|
5540
5679
|
if (generatable.length > 0) {
|
|
5541
5680
|
logger.info("Rotation updates the source only \u2014 commit and deploy to apply.");
|
|
@@ -5556,17 +5695,17 @@ function requireEntry(schema, key) {
|
|
|
5556
5695
|
|
|
5557
5696
|
// src/commands/secret/keygen.ts
|
|
5558
5697
|
init_logger();
|
|
5559
|
-
import { execa as
|
|
5560
|
-
import { existsSync as
|
|
5698
|
+
import { execa as execa12 } from "execa";
|
|
5699
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync6 } from "fs";
|
|
5561
5700
|
import { homedir } from "os";
|
|
5562
|
-
import { dirname as
|
|
5563
|
-
import
|
|
5701
|
+
import { dirname as dirname5, join as join28 } from "path";
|
|
5702
|
+
import chalk36 from "chalk";
|
|
5564
5703
|
function ageKeyFile() {
|
|
5565
|
-
return process.env.SOPS_AGE_KEY_FILE ??
|
|
5704
|
+
return process.env.SOPS_AGE_KEY_FILE ?? join28(homedir(), ".config", "sops", "age", "keys.txt");
|
|
5566
5705
|
}
|
|
5567
5706
|
async function ensureAgeInstalled() {
|
|
5568
5707
|
try {
|
|
5569
|
-
await
|
|
5708
|
+
await execa12("age-keygen", ["--version"]);
|
|
5570
5709
|
} catch {
|
|
5571
5710
|
throw new Error("`age-keygen` not found on PATH. Install age: https://github.com/FiloSottile/age");
|
|
5572
5711
|
}
|
|
@@ -5582,14 +5721,14 @@ async function secretKeygen() {
|
|
|
5582
5721
|
process.exit(1);
|
|
5583
5722
|
}
|
|
5584
5723
|
const keyFile = ageKeyFile();
|
|
5585
|
-
if (
|
|
5586
|
-
const { stdout } = await
|
|
5724
|
+
if (existsSync31(keyFile)) {
|
|
5725
|
+
const { stdout } = await execa12("age-keygen", ["-y", keyFile]);
|
|
5587
5726
|
logger.warn(`age key file already exists: ${keyFile}`);
|
|
5588
5727
|
printPublicKeys(publicKeys(stdout));
|
|
5589
5728
|
return;
|
|
5590
5729
|
}
|
|
5591
|
-
|
|
5592
|
-
const { stderr } = await
|
|
5730
|
+
mkdirSync6(dirname5(keyFile), { recursive: true });
|
|
5731
|
+
const { stderr } = await execa12("age-keygen", ["-o", keyFile]);
|
|
5593
5732
|
logger.success(`Created age key: ${keyFile}`);
|
|
5594
5733
|
printPublicKeys(publicKeys(stderr));
|
|
5595
5734
|
}
|
|
@@ -5598,27 +5737,27 @@ function printPublicKeys(keys) {
|
|
|
5598
5737
|
logger.warn("Could not read the public key \u2014 run `age-keygen -y <file>` manually.");
|
|
5599
5738
|
return;
|
|
5600
5739
|
}
|
|
5601
|
-
console.log(
|
|
5740
|
+
console.log(chalk36.bold("\nPublic key(s):"));
|
|
5602
5741
|
for (const key of keys) {
|
|
5603
|
-
console.log(` ${
|
|
5742
|
+
console.log(` ${chalk36.cyan(key)}`);
|
|
5604
5743
|
}
|
|
5605
|
-
console.log(
|
|
5606
|
-
console.log(
|
|
5744
|
+
console.log(chalk36.dim("\nRegister it as a recipient:"));
|
|
5745
|
+
console.log(chalk36.dim(` spfn secret recipients add ${keys[0]}
|
|
5607
5746
|
`));
|
|
5608
5747
|
}
|
|
5609
5748
|
|
|
5610
5749
|
// src/commands/secret/recipients.ts
|
|
5611
5750
|
init_logger();
|
|
5612
|
-
import { existsSync as
|
|
5613
|
-
import { join as
|
|
5751
|
+
import { existsSync as existsSync32, readFileSync as readFileSync13, writeFileSync as writeFileSync19 } from "fs";
|
|
5752
|
+
import { join as join29 } from "path";
|
|
5614
5753
|
import { parse as parse3, stringify } from "yaml";
|
|
5615
|
-
import
|
|
5754
|
+
import chalk37 from "chalk";
|
|
5616
5755
|
var SOPS_CONFIG = ".sops.yaml";
|
|
5617
5756
|
var DEFAULT_PATH_REGEX = "secrets/.*\\.enc\\.json$";
|
|
5618
5757
|
var AGE_RECIPIENT = /^age1[0-9a-z]+$/;
|
|
5619
5758
|
async function secretRecipients(action, key, _options) {
|
|
5620
5759
|
const cwd = process.cwd();
|
|
5621
|
-
const configPath =
|
|
5760
|
+
const configPath = join29(cwd, SOPS_CONFIG);
|
|
5622
5761
|
if (action === "list") {
|
|
5623
5762
|
listRecipients(configPath);
|
|
5624
5763
|
return;
|
|
@@ -5645,30 +5784,30 @@ async function secretRecipients(action, key, _options) {
|
|
|
5645
5784
|
logger.warn("Removing a recipient does not revoke values they already decrypted \u2014 rotate those values.");
|
|
5646
5785
|
}
|
|
5647
5786
|
rule.age = [...recipients].join(",");
|
|
5648
|
-
|
|
5787
|
+
writeFileSync19(configPath, stringify(config));
|
|
5649
5788
|
logger.success(`${action === "add" ? "Added" : "Removed"} recipient; updated ${SOPS_CONFIG}.`);
|
|
5650
5789
|
await reencrypt(cwd);
|
|
5651
5790
|
}
|
|
5652
5791
|
function listRecipients(configPath) {
|
|
5653
|
-
if (!
|
|
5792
|
+
if (!existsSync32(configPath)) {
|
|
5654
5793
|
logger.info(`No ${SOPS_CONFIG} found.`);
|
|
5655
5794
|
return;
|
|
5656
5795
|
}
|
|
5657
5796
|
const config = loadConfig(configPath);
|
|
5658
5797
|
const rules = config.creation_rules ?? [];
|
|
5659
|
-
console.log(
|
|
5798
|
+
console.log(chalk37.blue.bold(`
|
|
5660
5799
|
\u{1F465} Recipients (${SOPS_CONFIG})
|
|
5661
5800
|
`));
|
|
5662
5801
|
for (const rule of rules) {
|
|
5663
|
-
console.log(
|
|
5802
|
+
console.log(chalk37.dim(` ${rule.path_regex ?? "(any path)"}`));
|
|
5664
5803
|
for (const recipient of parseRecipients(rule.age)) {
|
|
5665
|
-
console.log(` ${
|
|
5804
|
+
console.log(` ${chalk37.cyan(recipient)}`);
|
|
5666
5805
|
}
|
|
5667
5806
|
}
|
|
5668
5807
|
console.log();
|
|
5669
5808
|
}
|
|
5670
5809
|
function loadConfig(configPath) {
|
|
5671
|
-
if (!
|
|
5810
|
+
if (!existsSync32(configPath)) {
|
|
5672
5811
|
return { creation_rules: [] };
|
|
5673
5812
|
}
|
|
5674
5813
|
return parse3(readFileSync13(configPath, "utf-8")) ?? { creation_rules: [] };
|
|
@@ -5703,8 +5842,8 @@ async function reencrypt(cwd) {
|
|
|
5703
5842
|
|
|
5704
5843
|
// src/commands/secret/check.ts
|
|
5705
5844
|
init_logger();
|
|
5706
|
-
import { join as
|
|
5707
|
-
import
|
|
5845
|
+
import { join as join30 } from "path";
|
|
5846
|
+
import chalk38 from "chalk";
|
|
5708
5847
|
init_env_file();
|
|
5709
5848
|
var COMMITTED_FILES = [".env", ".env.example"];
|
|
5710
5849
|
var PLACEHOLDER = /(your-|changeme|placeholder|example|<.*>)/i;
|
|
@@ -5722,35 +5861,35 @@ async function secretCheck(options) {
|
|
|
5722
5861
|
const issues = [];
|
|
5723
5862
|
const warnings = [];
|
|
5724
5863
|
for (const file of COMMITTED_FILES) {
|
|
5725
|
-
const parsed = parseEnvFile(
|
|
5864
|
+
const parsed = parseEnvFile(join30(cwd, file));
|
|
5726
5865
|
for (const [key, value] of Object.entries(parsed)) {
|
|
5727
5866
|
if (secretKeys.has(key) && value.length > 0 && !PLACEHOLDER.test(value)) {
|
|
5728
|
-
issues.push(`${
|
|
5867
|
+
issues.push(`${chalk38.cyan(key)} has a real value in committed ${chalk38.yellow(file)} \u2014 move it to the keychain/SOPS.`);
|
|
5729
5868
|
}
|
|
5730
5869
|
}
|
|
5731
5870
|
}
|
|
5732
|
-
const serverEnv = parseEnvFile(
|
|
5871
|
+
const serverEnv = parseEnvFile(join30(cwd, ".env.server"));
|
|
5733
5872
|
for (const key of secretKeys) {
|
|
5734
5873
|
const value = serverEnv[key];
|
|
5735
5874
|
if (value && !value.startsWith(KEYCHAIN_REF_PREFIX)) {
|
|
5736
|
-
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.`);
|
|
5737
5876
|
}
|
|
5738
5877
|
}
|
|
5739
5878
|
report(issues, warnings, cwd);
|
|
5740
5879
|
}
|
|
5741
5880
|
function report(issues, warnings, cwd) {
|
|
5742
|
-
console.log(
|
|
5881
|
+
console.log(chalk38.blue.bold("\n\u{1F50D} Secret hygiene check\n"));
|
|
5743
5882
|
for (const issue of issues) {
|
|
5744
|
-
console.log(` ${
|
|
5883
|
+
console.log(` ${chalk38.red("\u2717")} ${issue}`);
|
|
5745
5884
|
}
|
|
5746
5885
|
for (const warning of warnings) {
|
|
5747
|
-
console.log(` ${
|
|
5886
|
+
console.log(` ${chalk38.yellow("\u26A0")} ${warning}`);
|
|
5748
5887
|
}
|
|
5749
5888
|
if (!hasSopsConfig(cwd)) {
|
|
5750
|
-
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)")}`);
|
|
5751
5890
|
}
|
|
5752
5891
|
if (issues.length === 0 && warnings.length === 0) {
|
|
5753
|
-
console.log(
|
|
5892
|
+
console.log(chalk38.green(" \u2713 No plaintext secret leaks found."));
|
|
5754
5893
|
}
|
|
5755
5894
|
console.log();
|
|
5756
5895
|
if (issues.length > 0) {
|
|
@@ -5761,7 +5900,7 @@ function report(issues, warnings, cwd) {
|
|
|
5761
5900
|
// src/commands/secret/index.ts
|
|
5762
5901
|
var ENV_OPTION = ["-e, --env <env>", "Target environment (local | development | staging | production)", "local"];
|
|
5763
5902
|
var PKG_OPTION = ["-p, --package <package>", "Package whose env schema to read", "@spfn/core"];
|
|
5764
|
-
var secretCommand = new
|
|
5903
|
+
var secretCommand = new Command13("secret").description("Manage secrets: keychain locally, SOPS (age / GCP KMS / AWS KMS) for deployed environments");
|
|
5765
5904
|
secretCommand.command("set [key]").description("Store a secret value (prompts for the value, masked)").option(...ENV_OPTION).option(...PKG_OPTION).action(secretSet);
|
|
5766
5905
|
secretCommand.command("list").description("List declared secrets and their status (never prints values)").option(...ENV_OPTION).option(...PKG_OPTION).action(secretList);
|
|
5767
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);
|
|
@@ -5772,7 +5911,7 @@ secretCommand.command("check").description("Static hygiene lint \u2014 flag plai
|
|
|
5772
5911
|
|
|
5773
5912
|
// src/index.ts
|
|
5774
5913
|
init_version();
|
|
5775
|
-
var program = new
|
|
5914
|
+
var program = new Command14();
|
|
5776
5915
|
program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(getCliVersion());
|
|
5777
5916
|
program.addCommand(createCommand);
|
|
5778
5917
|
program.addCommand(initCommand);
|
|
@@ -5780,6 +5919,7 @@ program.addCommand(addCommand);
|
|
|
5780
5919
|
program.addCommand(devCommand);
|
|
5781
5920
|
program.addCommand(buildCommand);
|
|
5782
5921
|
program.addCommand(startCommand);
|
|
5922
|
+
program.addCommand(provisionCommand);
|
|
5783
5923
|
program.addCommand(codegenCommand);
|
|
5784
5924
|
program.addCommand(keyCommand);
|
|
5785
5925
|
program.addCommand(setupCommand);
|