spfn 0.2.0-beta.65 → 0.2.0-beta.67
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 +114 -5
- package/dist/index.js +513 -393
- package/package.json +6 -5
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.67";
|
|
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 Command15 } from "commander";
|
|
1864
1864
|
|
|
1865
1865
|
// src/commands/create.ts
|
|
1866
1866
|
init_logger();
|
|
@@ -2647,13 +2647,18 @@ async function buildProject(options) {
|
|
|
2647
2647
|
const orchestrator = new CodegenOrchestrator({
|
|
2648
2648
|
generators,
|
|
2649
2649
|
cwd,
|
|
2650
|
-
debug: true
|
|
2650
|
+
debug: true,
|
|
2651
|
+
// A generator that refuses at build time — a broken route contract,
|
|
2652
|
+
// a router that will not load — must reach the exit code rather
|
|
2653
|
+
// than scroll past while the build ships anyway.
|
|
2654
|
+
throwOnError: true
|
|
2651
2655
|
});
|
|
2652
|
-
await orchestrator.generateAll();
|
|
2656
|
+
await orchestrator.generateAll("build");
|
|
2653
2657
|
spinner.succeed("API client generated");
|
|
2654
2658
|
} catch (error) {
|
|
2655
|
-
spinner.
|
|
2656
|
-
logger.
|
|
2659
|
+
spinner.fail("Code generation failed");
|
|
2660
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
2661
|
+
process.exit(1);
|
|
2657
2662
|
}
|
|
2658
2663
|
}
|
|
2659
2664
|
if (hasNext && !options.serverOnly) {
|
|
@@ -3054,11 +3059,123 @@ codegenCommand.command("init").description("Initialize .spfnrc.ts with codegen c
|
|
|
3054
3059
|
codegenCommand.command("list").alias("ls").description("List registered code generators").action(listGenerators);
|
|
3055
3060
|
codegenCommand.command("run").description("Run code generators once (no watch mode)").action(runGenerators);
|
|
3056
3061
|
|
|
3057
|
-
// src/commands/
|
|
3062
|
+
// src/commands/contract.ts
|
|
3058
3063
|
init_logger();
|
|
3059
3064
|
import { Command as Command9 } from "commander";
|
|
3060
|
-
import {
|
|
3065
|
+
import { join as join22 } from "path";
|
|
3061
3066
|
import chalk11 from "chalk";
|
|
3067
|
+
async function resolveContractsDir(cwd, override) {
|
|
3068
|
+
if (override) {
|
|
3069
|
+
return join22(cwd, override);
|
|
3070
|
+
}
|
|
3071
|
+
const { loadCodegenConfig } = await import("@spfn/core/codegen");
|
|
3072
|
+
const config = loadCodegenConfig(cwd);
|
|
3073
|
+
const generator = config.generators?.find(
|
|
3074
|
+
(entry) => "name" in entry && entry.name === "@spfn/core:contract"
|
|
3075
|
+
);
|
|
3076
|
+
return join22(cwd, generator?.outputDir ?? "./contracts");
|
|
3077
|
+
}
|
|
3078
|
+
async function regenerate(cwd) {
|
|
3079
|
+
const { loadCodegenConfig, createGeneratorsFromConfig, CodegenOrchestrator } = await import("@spfn/core/codegen");
|
|
3080
|
+
const config = loadCodegenConfig(cwd);
|
|
3081
|
+
const generators = (await createGeneratorsFromConfig(config, cwd)).filter((generator) => generator.name === "@spfn/core:contract");
|
|
3082
|
+
if (generators.length === 0) {
|
|
3083
|
+
throw new Error(
|
|
3084
|
+
"No @spfn/core:contract generator is configured in .spfnrc.ts.\nAdd one with the router path before using contract commands."
|
|
3085
|
+
);
|
|
3086
|
+
}
|
|
3087
|
+
const orchestrator = new CodegenOrchestrator({ generators, cwd, throwOnError: true });
|
|
3088
|
+
await orchestrator.generateAll("manual");
|
|
3089
|
+
}
|
|
3090
|
+
async function checkContractCommand(options) {
|
|
3091
|
+
const cwd = process.cwd();
|
|
3092
|
+
try {
|
|
3093
|
+
await regenerate(cwd);
|
|
3094
|
+
const { checkContract, formatViolations, readCurrentDocument } = await import("@spfn/core/contract");
|
|
3095
|
+
const contractsDir = await resolveContractsDir(cwd, options.dir);
|
|
3096
|
+
const result = checkContract(contractsDir, readCurrentDocument(contractsDir));
|
|
3097
|
+
for (const warning of result.warnings) {
|
|
3098
|
+
logger.warn(warning);
|
|
3099
|
+
}
|
|
3100
|
+
if (result.violations.length > 0) {
|
|
3101
|
+
console.log("\n" + chalk11.red.bold(`\u2717 Breaks the contract released as ${result.baselineVersion}
|
|
3102
|
+
`));
|
|
3103
|
+
console.log(formatViolations(result.violations));
|
|
3104
|
+
console.log("");
|
|
3105
|
+
process.exit(1);
|
|
3106
|
+
}
|
|
3107
|
+
const against = result.baselineVersion ? `backward compatible with released ${result.baselineVersion}` : "generated (nothing released to compare against yet)";
|
|
3108
|
+
console.log("\n" + chalk11.green.bold(`\u2713 Contract ${against}
|
|
3109
|
+
`));
|
|
3110
|
+
} catch (error) {
|
|
3111
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
3112
|
+
process.exit(1);
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
async function releaseContractCommand(version, options) {
|
|
3116
|
+
const cwd = process.cwd();
|
|
3117
|
+
try {
|
|
3118
|
+
await regenerate(cwd);
|
|
3119
|
+
const {
|
|
3120
|
+
checkContract,
|
|
3121
|
+
formatViolations,
|
|
3122
|
+
readCurrentDocument,
|
|
3123
|
+
writeSnapshot
|
|
3124
|
+
} = await import("@spfn/core/contract");
|
|
3125
|
+
const contractsDir = await resolveContractsDir(cwd, options.dir);
|
|
3126
|
+
const document = readCurrentDocument(contractsDir);
|
|
3127
|
+
const result = checkContract(contractsDir, document);
|
|
3128
|
+
if (result.violations.length > 0) {
|
|
3129
|
+
console.log("\n" + chalk11.red.bold(`\u2717 Cannot release: breaks the contract released as ${result.baselineVersion}
|
|
3130
|
+
`));
|
|
3131
|
+
console.log(formatViolations(result.violations));
|
|
3132
|
+
console.log("");
|
|
3133
|
+
process.exit(1);
|
|
3134
|
+
}
|
|
3135
|
+
for (const warning of result.warnings) {
|
|
3136
|
+
logger.warn(warning);
|
|
3137
|
+
}
|
|
3138
|
+
const file = writeSnapshot(contractsDir, version, document);
|
|
3139
|
+
console.log("\n" + chalk11.green.bold(`\u2713 Released contract ${version}
|
|
3140
|
+
`));
|
|
3141
|
+
console.log(chalk11.gray(` ${file}`));
|
|
3142
|
+
console.log(chalk11.gray(` ${document.operations.length} operation(s)`));
|
|
3143
|
+
console.log(chalk11.yellow("\n Commit this snapshot. The gate compares every later build against it.\n"));
|
|
3144
|
+
} catch (error) {
|
|
3145
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
3146
|
+
process.exit(1);
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
async function listContractCommand(options) {
|
|
3150
|
+
const cwd = process.cwd();
|
|
3151
|
+
try {
|
|
3152
|
+
const { listSnapshots } = await import("@spfn/core/contract");
|
|
3153
|
+
const contractsDir = await resolveContractsDir(cwd, options.dir);
|
|
3154
|
+
const snapshots = listSnapshots(contractsDir);
|
|
3155
|
+
if (snapshots.length === 0) {
|
|
3156
|
+
logger.info("No released contract snapshot yet");
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
console.log("\n" + chalk11.bold("Released contracts:"));
|
|
3160
|
+
for (const snapshot of snapshots) {
|
|
3161
|
+
console.log(` ${chalk11.cyan(snapshot.version)} ${chalk11.gray(snapshot.file)}`);
|
|
3162
|
+
}
|
|
3163
|
+
console.log("");
|
|
3164
|
+
} catch (error) {
|
|
3165
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
3166
|
+
process.exit(1);
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
var contractCommand = new Command9("contract").description("Route contract management (what separately deployed clients are promised)");
|
|
3170
|
+
contractCommand.command("check").description("Regenerate the contract and compare it against the newest released snapshot").option("--dir <path>", "Contracts directory (default: from .spfnrc.ts)").action(checkContractCommand);
|
|
3171
|
+
contractCommand.command("release <version>").description("Write contracts/released/<version>.json \u2014 required for every release").option("--dir <path>", "Contracts directory (default: from .spfnrc.ts)").action(releaseContractCommand);
|
|
3172
|
+
contractCommand.command("list").alias("ls").description("List released contract snapshots").option("--dir <path>", "Contracts directory (default: from .spfnrc.ts)").action(listContractCommand);
|
|
3173
|
+
|
|
3174
|
+
// src/commands/key.ts
|
|
3175
|
+
init_logger();
|
|
3176
|
+
import { Command as Command10 } from "commander";
|
|
3177
|
+
import { execSync } from "child_process";
|
|
3178
|
+
import chalk12 from "chalk";
|
|
3062
3179
|
|
|
3063
3180
|
// src/utils/secret-gen.ts
|
|
3064
3181
|
import { randomBytes as randomBytes2, randomUUID } from "crypto";
|
|
@@ -3133,44 +3250,44 @@ function copyToClipboard(text) {
|
|
|
3133
3250
|
function generateSecret(bytes, preset, envVarName, copy) {
|
|
3134
3251
|
const key = randomBase64Url(bytes);
|
|
3135
3252
|
const config = preset ? PRESETS[preset] : null;
|
|
3136
|
-
console.log("\n" +
|
|
3253
|
+
console.log("\n" + chalk12.green.bold("\u2713 Generated secret key:"));
|
|
3137
3254
|
if (config) {
|
|
3138
|
-
console.log(
|
|
3255
|
+
console.log(chalk12.dim(` ${config.description} (${bytes * 8} bits)`));
|
|
3139
3256
|
} else {
|
|
3140
|
-
console.log(
|
|
3257
|
+
console.log(chalk12.dim(` ${bytes * 8}-bit secret`));
|
|
3141
3258
|
}
|
|
3142
|
-
console.log("\n" +
|
|
3259
|
+
console.log("\n" + chalk12.cyan(key) + "\n");
|
|
3143
3260
|
const varName = envVarName || config?.envVar || "SECRET_KEY";
|
|
3144
|
-
console.log(
|
|
3145
|
-
console.log(
|
|
3261
|
+
console.log(chalk12.dim("Add to your .env file:"));
|
|
3262
|
+
console.log(chalk12.yellow(`${varName}=${key}
|
|
3146
3263
|
`));
|
|
3147
3264
|
if (config?.usage) {
|
|
3148
|
-
console.log(
|
|
3149
|
-
console.log(
|
|
3265
|
+
console.log(chalk12.dim("Usage:"));
|
|
3266
|
+
console.log(chalk12.gray(` ${config.usage}
|
|
3150
3267
|
`));
|
|
3151
3268
|
}
|
|
3152
3269
|
if (copy) {
|
|
3153
3270
|
if (copyToClipboard(key)) {
|
|
3154
|
-
console.log(
|
|
3271
|
+
console.log(chalk12.green("\u2713 Copied to clipboard!\n"));
|
|
3155
3272
|
} else {
|
|
3156
3273
|
logger.warn("Could not copy to clipboard");
|
|
3157
3274
|
}
|
|
3158
3275
|
}
|
|
3159
3276
|
}
|
|
3160
3277
|
function listPresets() {
|
|
3161
|
-
console.log("\n" +
|
|
3278
|
+
console.log("\n" + chalk12.bold("Available presets:"));
|
|
3162
3279
|
console.log();
|
|
3163
3280
|
Object.entries(PRESETS).forEach(([name, config]) => {
|
|
3164
|
-
console.log(` ${
|
|
3165
|
-
console.log(` ${" ".repeat(20)} ${
|
|
3281
|
+
console.log(` ${chalk12.cyan(name.padEnd(20))} ${chalk12.dim(config.description)}`);
|
|
3282
|
+
console.log(` ${" ".repeat(20)} ${chalk12.gray(`\u2192 ${config.envVar} (${config.bytes * 8} bits)`)}`);
|
|
3166
3283
|
console.log();
|
|
3167
3284
|
});
|
|
3168
|
-
console.log(
|
|
3169
|
-
console.log(
|
|
3170
|
-
console.log(
|
|
3285
|
+
console.log(chalk12.dim("Usage:"));
|
|
3286
|
+
console.log(chalk12.gray(" spfn key <preset>"));
|
|
3287
|
+
console.log(chalk12.gray(" spfn key auth-encryption --copy"));
|
|
3171
3288
|
console.log();
|
|
3172
3289
|
}
|
|
3173
|
-
var generateValueCommand = new
|
|
3290
|
+
var generateValueCommand = new Command10("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) => {
|
|
3174
3291
|
const bytes = parseInt(options.bytes, 10);
|
|
3175
3292
|
if (isNaN(bytes) || bytes < 1 || bytes > 128) {
|
|
3176
3293
|
logger.error("Invalid bytes value. Must be between 1 and 128.");
|
|
@@ -3180,13 +3297,13 @@ var generateValueCommand = new Command9("generate").alias("gen").description("Ge
|
|
|
3180
3297
|
console.log(value);
|
|
3181
3298
|
if (options.copy) {
|
|
3182
3299
|
if (copyToClipboard(value)) {
|
|
3183
|
-
console.error(
|
|
3300
|
+
console.error(chalk12.green("\u2713 Copied to clipboard"));
|
|
3184
3301
|
} else {
|
|
3185
|
-
console.error(
|
|
3302
|
+
console.error(chalk12.yellow("\u26A0 Could not copy to clipboard"));
|
|
3186
3303
|
}
|
|
3187
3304
|
}
|
|
3188
3305
|
});
|
|
3189
|
-
var keyCommand = new
|
|
3306
|
+
var keyCommand = new Command10("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) => {
|
|
3190
3307
|
if (options.list) {
|
|
3191
3308
|
listPresets();
|
|
3192
3309
|
return;
|
|
@@ -3200,9 +3317,9 @@ var keyCommand = new Command9("key").alias("k").description("Generate secure ran
|
|
|
3200
3317
|
logger.error(`Unknown preset: ${preset}`);
|
|
3201
3318
|
console.log("\nAvailable presets:");
|
|
3202
3319
|
Object.entries(PRESETS).forEach(([name, config]) => {
|
|
3203
|
-
console.log(` ${
|
|
3320
|
+
console.log(` ${chalk12.cyan(name)}: ${config.description}`);
|
|
3204
3321
|
});
|
|
3205
|
-
console.log("\nUse " +
|
|
3322
|
+
console.log("\nUse " + chalk12.cyan("--list") + " to see detailed information");
|
|
3206
3323
|
process.exit(1);
|
|
3207
3324
|
}
|
|
3208
3325
|
generateSecret(
|
|
@@ -3218,16 +3335,16 @@ keyCommand.addCommand(generateValueCommand);
|
|
|
3218
3335
|
init_setup();
|
|
3219
3336
|
|
|
3220
3337
|
// src/commands/db/index.ts
|
|
3221
|
-
import { Command as
|
|
3338
|
+
import { Command as Command11 } from "commander";
|
|
3222
3339
|
|
|
3223
3340
|
// src/commands/db/generate.ts
|
|
3224
|
-
import
|
|
3341
|
+
import chalk14 from "chalk";
|
|
3225
3342
|
|
|
3226
3343
|
// src/commands/db/utils/drizzle.ts
|
|
3227
3344
|
import { existsSync as existsSync21, writeFileSync as writeFileSync14, unlinkSync as unlinkSync2 } from "fs";
|
|
3228
3345
|
import { spawn } from "child_process";
|
|
3229
3346
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
3230
|
-
import
|
|
3347
|
+
import chalk13 from "chalk";
|
|
3231
3348
|
import ora6 from "ora";
|
|
3232
3349
|
import { env as env3 } from "@spfn/core/config";
|
|
3233
3350
|
import { loadEnv as loadEnv3 } from "@spfn/core/server";
|
|
@@ -3285,8 +3402,8 @@ function resolvePushConnectionConfig(databaseUrl) {
|
|
|
3285
3402
|
function validateDatabasePrerequisites() {
|
|
3286
3403
|
loadEnv3();
|
|
3287
3404
|
if (!env3.DATABASE_URL) {
|
|
3288
|
-
console.error(
|
|
3289
|
-
console.log(
|
|
3405
|
+
console.error(chalk13.red("\u274C DATABASE_URL not found in environment"));
|
|
3406
|
+
console.log(chalk13.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
3290
3407
|
throw new Error("DATABASE_URL is required for database operations");
|
|
3291
3408
|
}
|
|
3292
3409
|
}
|
|
@@ -3297,8 +3414,8 @@ async function runDrizzleCommand(command) {
|
|
|
3297
3414
|
if (!hasUserConfig) {
|
|
3298
3415
|
loadEnv3();
|
|
3299
3416
|
if (!env3.DATABASE_URL) {
|
|
3300
|
-
console.error(
|
|
3301
|
-
console.log(
|
|
3417
|
+
console.error(chalk13.red("\u274C DATABASE_URL not found in environment"));
|
|
3418
|
+
console.log(chalk13.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
3302
3419
|
process.exit(1);
|
|
3303
3420
|
}
|
|
3304
3421
|
const { generateDrizzleConfigFile } = await import("@spfn/core/db");
|
|
@@ -3311,7 +3428,7 @@ async function runDrizzleCommand(command) {
|
|
|
3311
3428
|
autoDetectSchemas: true
|
|
3312
3429
|
});
|
|
3313
3430
|
writeFileSync14(tempConfigPath, configContent);
|
|
3314
|
-
console.log(
|
|
3431
|
+
console.log(chalk13.dim("Using auto-generated Drizzle config\n"));
|
|
3315
3432
|
}
|
|
3316
3433
|
const args = command.split(" ");
|
|
3317
3434
|
args.push(`--config=${configPath}`);
|
|
@@ -3346,10 +3463,10 @@ async function runWithSpinner(spinnerText, command, successMessage, failMessage)
|
|
|
3346
3463
|
try {
|
|
3347
3464
|
spinner.stop();
|
|
3348
3465
|
await runDrizzleCommand(command);
|
|
3349
|
-
console.log(
|
|
3466
|
+
console.log(chalk13.green(`\u2705 ${successMessage}`));
|
|
3350
3467
|
} catch (error) {
|
|
3351
3468
|
spinner.fail(failMessage);
|
|
3352
|
-
console.error(
|
|
3469
|
+
console.error(chalk13.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3353
3470
|
process.exit(1);
|
|
3354
3471
|
}
|
|
3355
3472
|
}
|
|
@@ -3395,16 +3512,16 @@ async function createPushConnection() {
|
|
|
3395
3512
|
async function dbGenerate() {
|
|
3396
3513
|
try {
|
|
3397
3514
|
await runDrizzleCommand("generate");
|
|
3398
|
-
console.log(
|
|
3515
|
+
console.log(chalk14.green("\n\u2705 Migrations generated successfully"));
|
|
3399
3516
|
} catch (error) {
|
|
3400
|
-
console.error(
|
|
3401
|
-
console.error(
|
|
3517
|
+
console.error(chalk14.red("\n\u274C Failed to generate migrations"));
|
|
3518
|
+
console.error(chalk14.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3402
3519
|
process.exit(1);
|
|
3403
3520
|
}
|
|
3404
3521
|
}
|
|
3405
3522
|
|
|
3406
3523
|
// src/commands/db/push.ts
|
|
3407
|
-
import
|
|
3524
|
+
import chalk16 from "chalk";
|
|
3408
3525
|
import prompts4 from "prompts";
|
|
3409
3526
|
import "@spfn/core/config";
|
|
3410
3527
|
import { loadEnv as loadEnv4 } from "@spfn/core/server";
|
|
@@ -3451,7 +3568,7 @@ function classifyStatements(statements) {
|
|
|
3451
3568
|
}
|
|
3452
3569
|
|
|
3453
3570
|
// src/commands/db/utils/push-display.ts
|
|
3454
|
-
import
|
|
3571
|
+
import chalk15 from "chalk";
|
|
3455
3572
|
function formatSql(sql2) {
|
|
3456
3573
|
return sql2.replace(/\s+/g, " ").trim();
|
|
3457
3574
|
}
|
|
@@ -3459,30 +3576,30 @@ function printStatements(statements, color) {
|
|
|
3459
3576
|
for (const stmt of statements) {
|
|
3460
3577
|
console.log(color(` ${formatSql(stmt.sql)}`));
|
|
3461
3578
|
if (stmt.reason) {
|
|
3462
|
-
console.log(
|
|
3579
|
+
console.log(chalk15.dim(` \u2192 ${stmt.reason}`));
|
|
3463
3580
|
}
|
|
3464
3581
|
}
|
|
3465
3582
|
}
|
|
3466
3583
|
function displayClassifiedStatements(result) {
|
|
3467
3584
|
if (result.safe.length > 0) {
|
|
3468
|
-
console.log(
|
|
3585
|
+
console.log(chalk15.green(`
|
|
3469
3586
|
\u2705 Safe changes (${result.safe.length}):`));
|
|
3470
|
-
printStatements(result.safe,
|
|
3587
|
+
printStatements(result.safe, chalk15.green);
|
|
3471
3588
|
}
|
|
3472
3589
|
if (result.warning.length > 0) {
|
|
3473
|
-
console.log(
|
|
3590
|
+
console.log(chalk15.yellow(`
|
|
3474
3591
|
\u26A0\uFE0F Warnings (${result.warning.length}):`));
|
|
3475
|
-
printStatements(result.warning,
|
|
3592
|
+
printStatements(result.warning, chalk15.yellow);
|
|
3476
3593
|
}
|
|
3477
3594
|
if (result.destructive.length > 0) {
|
|
3478
|
-
console.log(
|
|
3595
|
+
console.log(chalk15.red(`
|
|
3479
3596
|
\u274C Destructive changes (${result.destructive.length}):`));
|
|
3480
|
-
printStatements(result.destructive,
|
|
3597
|
+
printStatements(result.destructive, chalk15.red);
|
|
3481
3598
|
}
|
|
3482
3599
|
}
|
|
3483
3600
|
function displayDryRunSummary(result) {
|
|
3484
3601
|
const total = result.safe.length + result.warning.length + result.destructive.length;
|
|
3485
|
-
console.log(
|
|
3602
|
+
console.log(chalk15.cyan(`
|
|
3486
3603
|
\u{1F50D} Dry-run: ${total} statement(s) detected
|
|
3487
3604
|
`));
|
|
3488
3605
|
displayClassifiedStatements(result);
|
|
@@ -3490,11 +3607,11 @@ function displayDryRunSummary(result) {
|
|
|
3490
3607
|
}
|
|
3491
3608
|
function displayApplySummary(applied, skipped) {
|
|
3492
3609
|
if (applied > 0) {
|
|
3493
|
-
console.log(
|
|
3610
|
+
console.log(chalk15.green(`
|
|
3494
3611
|
\u2705 Applied ${applied} statement(s)`));
|
|
3495
3612
|
}
|
|
3496
3613
|
if (skipped > 0) {
|
|
3497
|
-
console.log(
|
|
3614
|
+
console.log(chalk15.yellow(`\u23ED\uFE0F Skipped ${skipped} destructive statement(s)`));
|
|
3498
3615
|
}
|
|
3499
3616
|
console.log("");
|
|
3500
3617
|
}
|
|
@@ -3539,10 +3656,10 @@ async function dbPush(options = {}) {
|
|
|
3539
3656
|
});
|
|
3540
3657
|
const schemaFiles = Array.isArray(config.schema) ? config.schema : [config.schema];
|
|
3541
3658
|
if (schemaFiles.length === 0) {
|
|
3542
|
-
console.log(
|
|
3659
|
+
console.log(chalk16.yellow("No schema files found."));
|
|
3543
3660
|
return;
|
|
3544
3661
|
}
|
|
3545
|
-
console.log(
|
|
3662
|
+
console.log(chalk16.dim(`Found ${schemaFiles.length} schema file(s)
|
|
3546
3663
|
`));
|
|
3547
3664
|
const imports = await loadSchemaImports(schemaFiles);
|
|
3548
3665
|
const detectedSchemas = new Set(config.schemaFilter ?? ["public"]);
|
|
@@ -3561,10 +3678,10 @@ async function dbPush(options = {}) {
|
|
|
3561
3678
|
try {
|
|
3562
3679
|
const { statements, hints } = await resolvePushPlan(imports, db, schemaFilter);
|
|
3563
3680
|
for (const hint of hints) {
|
|
3564
|
-
console.log(
|
|
3681
|
+
console.log(chalk16.yellow(`\u26A0\uFE0F ${hint.hint}`));
|
|
3565
3682
|
}
|
|
3566
3683
|
if (statements.length === 0) {
|
|
3567
|
-
console.log(
|
|
3684
|
+
console.log(chalk16.green("\u2705 No changes detected \u2014 database is up to date\n"));
|
|
3568
3685
|
await applyFunctionMigrations(functionPlans);
|
|
3569
3686
|
return;
|
|
3570
3687
|
}
|
|
@@ -3575,7 +3692,7 @@ async function dbPush(options = {}) {
|
|
|
3575
3692
|
}
|
|
3576
3693
|
displayClassifiedStatements(result);
|
|
3577
3694
|
if (options.force) {
|
|
3578
|
-
console.log(
|
|
3695
|
+
console.log(chalk16.dim("\n--force: applying all changes..."));
|
|
3579
3696
|
await applyStatements(db, statements);
|
|
3580
3697
|
displayApplySummary(statements.length, 0);
|
|
3581
3698
|
} else if (result.destructive.length === 0) {
|
|
@@ -3584,11 +3701,11 @@ async function dbPush(options = {}) {
|
|
|
3584
3701
|
} else {
|
|
3585
3702
|
const destructiveSet = new Set(result.destructive.map((s) => s.sql));
|
|
3586
3703
|
const nonDestructive = statements.filter((s) => !destructiveSet.has(s));
|
|
3587
|
-
console.log(
|
|
3704
|
+
console.log(chalk16.red(`
|
|
3588
3705
|
\u274C ${result.destructive.length} destructive change(s) require confirmation:`));
|
|
3589
3706
|
for (const stmt of result.destructive) {
|
|
3590
|
-
console.log(
|
|
3591
|
-
console.log(
|
|
3707
|
+
console.log(chalk16.red(` ${stmt.sql.replace(/\s+/g, " ").trim()}`));
|
|
3708
|
+
console.log(chalk16.dim(` \u2192 ${stmt.reason}`));
|
|
3592
3709
|
}
|
|
3593
3710
|
const { confirm } = await prompts4({
|
|
3594
3711
|
type: "confirm",
|
|
@@ -3604,7 +3721,7 @@ async function dbPush(options = {}) {
|
|
|
3604
3721
|
await applyStatements(db, nonDestructive);
|
|
3605
3722
|
}
|
|
3606
3723
|
displayApplySummary(nonDestructive.length, result.destructive.length);
|
|
3607
|
-
console.log(
|
|
3724
|
+
console.log(chalk16.dim("Tip: Use --force to apply all changes without prompting.\n"));
|
|
3608
3725
|
}
|
|
3609
3726
|
}
|
|
3610
3727
|
await applyFunctionMigrations(functionPlans);
|
|
@@ -3617,8 +3734,8 @@ function loadFunctionPlansOrExit() {
|
|
|
3617
3734
|
try {
|
|
3618
3735
|
return loadFunctionMigrationPlans(functions);
|
|
3619
3736
|
} catch (error) {
|
|
3620
|
-
console.error(
|
|
3621
|
-
console.error(
|
|
3737
|
+
console.error(chalk16.red("\n\u274C Invalid function package migrations \u2014 nothing was applied"));
|
|
3738
|
+
console.error(chalk16.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3622
3739
|
process.exit(1);
|
|
3623
3740
|
}
|
|
3624
3741
|
}
|
|
@@ -3626,36 +3743,36 @@ async function applyFunctionMigrations(plans) {
|
|
|
3626
3743
|
if (plans.length === 0) {
|
|
3627
3744
|
return;
|
|
3628
3745
|
}
|
|
3629
|
-
console.log(
|
|
3746
|
+
console.log(chalk16.blue("\n\u{1F4E6} Applying function package migrations:"));
|
|
3630
3747
|
plans.forEach((plan) => {
|
|
3631
|
-
console.log(
|
|
3748
|
+
console.log(chalk16.dim(` - ${plan.packageName}`));
|
|
3632
3749
|
});
|
|
3633
3750
|
try {
|
|
3634
3751
|
await executeFunctionMigrations(plans);
|
|
3635
|
-
console.log(
|
|
3752
|
+
console.log(chalk16.green("\n\u2705 All function migrations applied\n"));
|
|
3636
3753
|
} catch (error) {
|
|
3637
|
-
console.error(
|
|
3638
|
-
console.error(
|
|
3639
|
-
console.error(
|
|
3754
|
+
console.error(chalk16.red("\n\u274C Failed to apply function package migrations"));
|
|
3755
|
+
console.error(chalk16.red(error instanceof Error ? error.message : "Unknown error"));
|
|
3756
|
+
console.error(chalk16.yellow("Project schema changes (if any) were already applied \u2014 only function package migrations failed."));
|
|
3640
3757
|
process.exit(1);
|
|
3641
3758
|
}
|
|
3642
3759
|
}
|
|
3643
3760
|
|
|
3644
3761
|
// src/commands/db/migrate.ts
|
|
3645
|
-
import
|
|
3646
|
-
import { join as
|
|
3762
|
+
import chalk21 from "chalk";
|
|
3763
|
+
import { join as join23 } from "path";
|
|
3647
3764
|
import { existsSync as existsSync23 } from "fs";
|
|
3648
3765
|
|
|
3649
3766
|
// src/commands/db/backup.ts
|
|
3650
3767
|
import { promises as fs3 } from "fs";
|
|
3651
3768
|
import path3 from "path";
|
|
3652
3769
|
import { spawn as spawn2 } from "child_process";
|
|
3653
|
-
import
|
|
3770
|
+
import chalk20 from "chalk";
|
|
3654
3771
|
import ora7 from "ora";
|
|
3655
3772
|
|
|
3656
3773
|
// src/commands/db/utils/database.ts
|
|
3657
3774
|
import net from "net";
|
|
3658
|
-
import
|
|
3775
|
+
import chalk17 from "chalk";
|
|
3659
3776
|
import prompts5 from "prompts";
|
|
3660
3777
|
function parseDatabaseUrl2(dbUrl) {
|
|
3661
3778
|
try {
|
|
@@ -3686,7 +3803,7 @@ async function confirmDangerousTarget(dbInfo) {
|
|
|
3686
3803
|
message: `Remote/production database detected. Type the database name "${dbInfo.database}" to continue:`
|
|
3687
3804
|
});
|
|
3688
3805
|
if (typed !== dbInfo.database) {
|
|
3689
|
-
console.log(
|
|
3806
|
+
console.log(chalk17.gray("Cancelled (database name mismatch)."));
|
|
3690
3807
|
process.exit(0);
|
|
3691
3808
|
}
|
|
3692
3809
|
}
|
|
@@ -3739,14 +3856,14 @@ function formatTimestamp() {
|
|
|
3739
3856
|
import { promises as fs2 } from "fs";
|
|
3740
3857
|
import { existsSync as existsSync22 } from "fs";
|
|
3741
3858
|
import path2 from "path";
|
|
3742
|
-
import
|
|
3859
|
+
import chalk19 from "chalk";
|
|
3743
3860
|
|
|
3744
3861
|
// src/commands/db/utils/metadata.ts
|
|
3745
3862
|
import { promises as fs } from "fs";
|
|
3746
3863
|
import path from "path";
|
|
3747
3864
|
import { promisify } from "util";
|
|
3748
3865
|
import { exec } from "child_process";
|
|
3749
|
-
import
|
|
3866
|
+
import chalk18 from "chalk";
|
|
3750
3867
|
var execAsync = promisify(exec);
|
|
3751
3868
|
async function collectGitInfo() {
|
|
3752
3869
|
try {
|
|
@@ -3809,7 +3926,7 @@ async function collectMigrationInfo(dbUrl) {
|
|
|
3809
3926
|
await pool.end();
|
|
3810
3927
|
}
|
|
3811
3928
|
} catch (error) {
|
|
3812
|
-
console.log(
|
|
3929
|
+
console.log(chalk18.dim("\u26A0\uFE0F Could not fetch migration info"));
|
|
3813
3930
|
return void 0;
|
|
3814
3931
|
}
|
|
3815
3932
|
}
|
|
@@ -3817,9 +3934,9 @@ async function saveBackupMetadata(metadata, backupFilename) {
|
|
|
3817
3934
|
const metadataPath = backupFilename.replace(/\.(sql|dump)$/, ".meta.json");
|
|
3818
3935
|
try {
|
|
3819
3936
|
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
3820
|
-
console.log(
|
|
3937
|
+
console.log(chalk18.dim(`\u2713 Metadata saved: ${path.basename(metadataPath)}`));
|
|
3821
3938
|
} catch (error) {
|
|
3822
|
-
console.log(
|
|
3939
|
+
console.log(chalk18.dim("\u26A0\uFE0F Could not save metadata"));
|
|
3823
3940
|
}
|
|
3824
3941
|
}
|
|
3825
3942
|
async function loadBackupMetadata(backupFilename) {
|
|
@@ -3848,10 +3965,10 @@ async function ensureBackupInGitignore() {
|
|
|
3848
3965
|
if (!hasBackupsIgnore) {
|
|
3849
3966
|
const entry = exists && content && !content.endsWith("\n") ? "\n\n# Database backups\nbackups/\n" : "# Database backups\nbackups/\n";
|
|
3850
3967
|
await fs2.appendFile(gitignorePath, entry);
|
|
3851
|
-
console.log(
|
|
3968
|
+
console.log(chalk19.dim("\u2713 Added backups/ to .gitignore"));
|
|
3852
3969
|
}
|
|
3853
3970
|
} catch (error) {
|
|
3854
|
-
console.log(
|
|
3971
|
+
console.log(chalk19.dim("\u26A0\uFE0F Could not update .gitignore"));
|
|
3855
3972
|
}
|
|
3856
3973
|
}
|
|
3857
3974
|
async function ensureBackupDir() {
|
|
@@ -3901,12 +4018,12 @@ async function listBackupFiles() {
|
|
|
3901
4018
|
import { env as env4 } from "@spfn/core/config";
|
|
3902
4019
|
import { loadEnv as loadEnv5 } from "@spfn/core/server";
|
|
3903
4020
|
async function dbBackup(options) {
|
|
3904
|
-
console.log(
|
|
4021
|
+
console.log(chalk20.blue("\u{1F4BE} Creating database backup...\n"));
|
|
3905
4022
|
loadEnv5();
|
|
3906
4023
|
const dbUrl = env4.DATABASE_URL;
|
|
3907
4024
|
if (!dbUrl) {
|
|
3908
|
-
console.error(
|
|
3909
|
-
console.log(
|
|
4025
|
+
console.error(chalk20.red("\u274C DATABASE_URL not found in environment"));
|
|
4026
|
+
console.log(chalk20.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
3910
4027
|
process.exit(1);
|
|
3911
4028
|
}
|
|
3912
4029
|
const dbInfo = parseDatabaseUrl2(dbUrl);
|
|
@@ -3916,7 +4033,7 @@ async function dbBackup(options) {
|
|
|
3916
4033
|
const ext = format === "sql" ? "sql" : "dump";
|
|
3917
4034
|
const filename = options.output || path3.join(backupDir, `${dbInfo.database}_${timestamp}.${ext}`);
|
|
3918
4035
|
if (options.dataOnly && options.schemaOnly) {
|
|
3919
|
-
console.error(
|
|
4036
|
+
console.error(chalk20.red("\u274C Cannot use --data-only and --schema-only together"));
|
|
3920
4037
|
process.exit(1);
|
|
3921
4038
|
}
|
|
3922
4039
|
const args = [
|
|
@@ -3962,11 +4079,11 @@ async function dbBackup(options) {
|
|
|
3962
4079
|
const stats = await fs3.stat(filename);
|
|
3963
4080
|
const size = formatBytes(stats.size);
|
|
3964
4081
|
spinner.succeed("Backup created");
|
|
3965
|
-
console.log(
|
|
4082
|
+
console.log(chalk20.green(`
|
|
3966
4083
|
\u2705 Backup created successfully`));
|
|
3967
|
-
console.log(
|
|
3968
|
-
console.log(
|
|
3969
|
-
console.log(
|
|
4084
|
+
console.log(chalk20.gray(` File: ${filename}`));
|
|
4085
|
+
console.log(chalk20.gray(` Size: ${size}`));
|
|
4086
|
+
console.log(chalk20.dim("\n\u{1F4CB} Collecting metadata..."));
|
|
3970
4087
|
const [gitInfo, migrationInfo] = await Promise.all([
|
|
3971
4088
|
collectGitInfo(),
|
|
3972
4089
|
collectMigrationInfo(dbUrl)
|
|
@@ -4006,11 +4123,11 @@ async function dbBackup(options) {
|
|
|
4006
4123
|
reject(error);
|
|
4007
4124
|
});
|
|
4008
4125
|
}).catch((error) => {
|
|
4009
|
-
console.error(
|
|
4126
|
+
console.error(chalk20.red("\n\u274C Failed to create backup"));
|
|
4010
4127
|
if (errorOutput.includes("pg_dump: command not found") || errorOutput.includes("not found")) {
|
|
4011
|
-
console.error(
|
|
4128
|
+
console.error(chalk20.yellow("\n\u{1F4A1} pg_dump is not installed. Please install PostgreSQL client tools."));
|
|
4012
4129
|
} else {
|
|
4013
|
-
console.error(
|
|
4130
|
+
console.error(chalk20.red(error instanceof Error ? error.message : "Unknown error"));
|
|
4014
4131
|
}
|
|
4015
4132
|
process.exit(1);
|
|
4016
4133
|
});
|
|
@@ -4027,7 +4144,7 @@ async function dbMigrate(options = {}) {
|
|
|
4027
4144
|
process.exit(1);
|
|
4028
4145
|
}
|
|
4029
4146
|
if (options.withBackup) {
|
|
4030
|
-
console.log(
|
|
4147
|
+
console.log(chalk21.blue("\u{1F4E6} Creating pre-migration backup...\n"));
|
|
4031
4148
|
await dbBackup({
|
|
4032
4149
|
format: "custom",
|
|
4033
4150
|
tag: "pre-migration",
|
|
@@ -4040,40 +4157,40 @@ async function dbMigrate(options = {}) {
|
|
|
4040
4157
|
const postgres = await import("postgres");
|
|
4041
4158
|
loadEnv6();
|
|
4042
4159
|
if (!env5.DATABASE_URL) {
|
|
4043
|
-
console.error(
|
|
4160
|
+
console.error(chalk21.red("\u274C DATABASE_URL not found in environment"));
|
|
4044
4161
|
process.exit(1);
|
|
4045
4162
|
}
|
|
4046
4163
|
const { discoverFunctionMigrations: discoverFunctionMigrations2, loadFunctionMigrationPlans: loadFunctionMigrationPlans2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
4047
4164
|
const functions = discoverFunctionMigrations2(process.cwd());
|
|
4048
4165
|
if (functions.length > 0) {
|
|
4049
|
-
console.log(
|
|
4166
|
+
console.log(chalk21.blue("\u{1F4E6} Applying function package migrations:"));
|
|
4050
4167
|
functions.forEach((func) => {
|
|
4051
|
-
console.log(
|
|
4168
|
+
console.log(chalk21.dim(` - ${func.packageName}`));
|
|
4052
4169
|
});
|
|
4053
4170
|
await executeFunctionMigrations2(loadFunctionMigrationPlans2(functions));
|
|
4054
|
-
console.log(
|
|
4171
|
+
console.log(chalk21.green("\u2705 Function migrations applied\n"));
|
|
4055
4172
|
}
|
|
4056
|
-
const projectMigrationsDir =
|
|
4173
|
+
const projectMigrationsDir = join23(process.cwd(), "src/server/drizzle");
|
|
4057
4174
|
if (existsSync23(projectMigrationsDir)) {
|
|
4058
4175
|
const projConn = postgres.default(env5.DATABASE_URL, { max: 1 });
|
|
4059
4176
|
const projDb = drizzle({ client: projConn });
|
|
4060
4177
|
try {
|
|
4061
|
-
console.log(
|
|
4178
|
+
console.log(chalk21.blue("\u{1F4E6} Running project migrations..."));
|
|
4062
4179
|
await migrate(projDb, {
|
|
4063
4180
|
migrationsFolder: projectMigrationsDir,
|
|
4064
4181
|
migrationsTable: PROJECT_MIGRATIONS_TABLE
|
|
4065
4182
|
});
|
|
4066
|
-
console.log(
|
|
4183
|
+
console.log(chalk21.green("\u2705 Project migrations applied successfully"));
|
|
4067
4184
|
} finally {
|
|
4068
4185
|
await projConn.end();
|
|
4069
4186
|
}
|
|
4070
4187
|
} else {
|
|
4071
|
-
console.log(
|
|
4188
|
+
console.log(chalk21.dim("No project migrations found (src/server/drizzle)"));
|
|
4072
4189
|
}
|
|
4073
4190
|
}
|
|
4074
4191
|
|
|
4075
4192
|
// src/commands/db/status.ts
|
|
4076
|
-
import
|
|
4193
|
+
import chalk22 from "chalk";
|
|
4077
4194
|
init_migration_status();
|
|
4078
4195
|
async function dbStatus() {
|
|
4079
4196
|
try {
|
|
@@ -4085,51 +4202,51 @@ async function dbStatus() {
|
|
|
4085
4202
|
try {
|
|
4086
4203
|
status = await getMigrationStatus(process.cwd());
|
|
4087
4204
|
} catch (error) {
|
|
4088
|
-
console.error(
|
|
4089
|
-
console.error(
|
|
4205
|
+
console.error(chalk22.red("\u274C Failed to read migration status"));
|
|
4206
|
+
console.error(chalk22.red(error instanceof Error ? error.message : "Unknown error"));
|
|
4090
4207
|
process.exit(1);
|
|
4091
4208
|
}
|
|
4092
4209
|
const targets = [...status.packages, ...status.project ? [status.project] : []];
|
|
4093
4210
|
if (targets.length === 0) {
|
|
4094
|
-
console.log(
|
|
4211
|
+
console.log(chalk22.dim("No migrations found (no @spfn packages with migrations, no src/server/drizzle)."));
|
|
4095
4212
|
return;
|
|
4096
4213
|
}
|
|
4097
|
-
console.log(
|
|
4214
|
+
console.log(chalk22.blue("\u{1F4E6} Migration status:\n"));
|
|
4098
4215
|
for (const target of targets) {
|
|
4099
|
-
const state = target.pending === 0 ?
|
|
4216
|
+
const state = target.pending === 0 ? chalk22.green(`${target.applied}/${target.total} applied`) : chalk22.yellow(`${target.applied}/${target.total} applied, ${target.pending} pending`);
|
|
4100
4217
|
console.log(` ${target.name.padEnd(32)} ${state}`);
|
|
4101
4218
|
for (const tag of target.pendingTags) {
|
|
4102
|
-
console.log(
|
|
4219
|
+
console.log(chalk22.dim(` - ${tag}`));
|
|
4103
4220
|
}
|
|
4104
4221
|
}
|
|
4105
4222
|
const pendingTotal = targets.reduce((sum, t) => sum + t.pending, 0);
|
|
4106
4223
|
if (pendingTotal === 0) {
|
|
4107
|
-
console.log(
|
|
4224
|
+
console.log(chalk22.green("\n\u2705 Database is up to date\n"));
|
|
4108
4225
|
} else {
|
|
4109
|
-
console.log(
|
|
4226
|
+
console.log(chalk22.yellow(`
|
|
4110
4227
|
\u26A0\uFE0F ${pendingTotal} pending migration(s) \u2014 run: pnpm spfn db migrate
|
|
4111
4228
|
`));
|
|
4112
4229
|
}
|
|
4113
4230
|
}
|
|
4114
4231
|
|
|
4115
4232
|
// src/commands/db/studio.ts
|
|
4116
|
-
import
|
|
4233
|
+
import chalk23 from "chalk";
|
|
4117
4234
|
import { existsSync as existsSync24, writeFileSync as writeFileSync15, unlinkSync as unlinkSync3 } from "fs";
|
|
4118
4235
|
import { spawn as spawn3 } from "child_process";
|
|
4119
4236
|
import { env as env6 } from "@spfn/core/config";
|
|
4120
4237
|
async function dbStudio(requestedPort) {
|
|
4121
|
-
console.log(
|
|
4238
|
+
console.log(chalk23.blue("\u{1F3A8} Opening Drizzle Studio...\n"));
|
|
4122
4239
|
const defaultPort = 4983;
|
|
4123
4240
|
const startPort = requestedPort || defaultPort;
|
|
4124
4241
|
let port;
|
|
4125
4242
|
try {
|
|
4126
4243
|
port = await findAvailablePort(startPort);
|
|
4127
4244
|
if (port !== startPort) {
|
|
4128
|
-
console.log(
|
|
4245
|
+
console.log(chalk23.yellow(`\u26A0\uFE0F Port ${startPort} is in use, using port ${port} instead
|
|
4129
4246
|
`));
|
|
4130
4247
|
}
|
|
4131
4248
|
} catch (error) {
|
|
4132
|
-
console.error(
|
|
4249
|
+
console.error(chalk23.red(error instanceof Error ? error.message : "Failed to find available port"));
|
|
4133
4250
|
process.exit(1);
|
|
4134
4251
|
}
|
|
4135
4252
|
const hasUserConfig = existsSync24("./drizzle.config.ts");
|
|
@@ -4138,8 +4255,8 @@ async function dbStudio(requestedPort) {
|
|
|
4138
4255
|
const configPath = hasUserConfig ? "./drizzle.config.ts" : tempConfigPath;
|
|
4139
4256
|
if (!hasUserConfig) {
|
|
4140
4257
|
if (!env6.DATABASE_URL) {
|
|
4141
|
-
console.error(
|
|
4142
|
-
console.log(
|
|
4258
|
+
console.error(chalk23.red("\u274C DATABASE_URL not found in environment"));
|
|
4259
|
+
console.log(chalk23.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
4143
4260
|
process.exit(1);
|
|
4144
4261
|
}
|
|
4145
4262
|
const { generateDrizzleConfigFile } = await import("@spfn/core/db");
|
|
@@ -4150,7 +4267,7 @@ async function dbStudio(requestedPort) {
|
|
|
4150
4267
|
// Expand glob patterns for Studio compatibility
|
|
4151
4268
|
});
|
|
4152
4269
|
writeFileSync15(tempConfigPath, configContent);
|
|
4153
|
-
console.log(
|
|
4270
|
+
console.log(chalk23.dim("Using auto-generated Drizzle config\n"));
|
|
4154
4271
|
}
|
|
4155
4272
|
const studioProcess = spawn3("drizzle-kit", ["studio", `--port=${port}`, `--config=${configPath}`], {
|
|
4156
4273
|
stdio: "inherit",
|
|
@@ -4165,19 +4282,19 @@ async function dbStudio(requestedPort) {
|
|
|
4165
4282
|
studioProcess.on("exit", (code) => {
|
|
4166
4283
|
cleanup();
|
|
4167
4284
|
if (code !== 0 && code !== null) {
|
|
4168
|
-
console.error(
|
|
4285
|
+
console.error(chalk23.red(`
|
|
4169
4286
|
\u274C Drizzle Studio exited with code ${code}`));
|
|
4170
4287
|
process.exit(code);
|
|
4171
4288
|
}
|
|
4172
4289
|
});
|
|
4173
4290
|
studioProcess.on("error", (error) => {
|
|
4174
4291
|
cleanup();
|
|
4175
|
-
console.error(
|
|
4176
|
-
console.error(
|
|
4292
|
+
console.error(chalk23.red("\u274C Failed to start Drizzle Studio"));
|
|
4293
|
+
console.error(chalk23.red(error.message));
|
|
4177
4294
|
process.exit(1);
|
|
4178
4295
|
});
|
|
4179
4296
|
process.on("SIGINT", () => {
|
|
4180
|
-
console.log(
|
|
4297
|
+
console.log(chalk23.yellow("\n\n\u{1F44B} Shutting down Drizzle Studio..."));
|
|
4181
4298
|
studioProcess.kill("SIGTERM");
|
|
4182
4299
|
cleanup();
|
|
4183
4300
|
process.exit(0);
|
|
@@ -4191,27 +4308,27 @@ async function dbStudio(requestedPort) {
|
|
|
4191
4308
|
if (!hasUserConfig && existsSync24(tempConfigPath)) {
|
|
4192
4309
|
unlinkSync3(tempConfigPath);
|
|
4193
4310
|
}
|
|
4194
|
-
console.error(
|
|
4195
|
-
console.error(
|
|
4311
|
+
console.error(chalk23.red("\u274C Failed to start Drizzle Studio"));
|
|
4312
|
+
console.error(chalk23.red(error instanceof Error ? error.message : "Unknown error"));
|
|
4196
4313
|
process.exit(1);
|
|
4197
4314
|
}
|
|
4198
4315
|
}
|
|
4199
4316
|
|
|
4200
4317
|
// src/commands/db/drop.ts
|
|
4201
|
-
import
|
|
4318
|
+
import chalk24 from "chalk";
|
|
4202
4319
|
import prompts6 from "prompts";
|
|
4203
4320
|
import { env as env7 } from "@spfn/core/config";
|
|
4204
4321
|
import { loadEnv as loadEnv7 } from "@spfn/core/server";
|
|
4205
4322
|
async function dbDrop() {
|
|
4206
4323
|
loadEnv7();
|
|
4207
4324
|
if (!env7.DATABASE_URL) {
|
|
4208
|
-
console.error(
|
|
4209
|
-
console.log(
|
|
4325
|
+
console.error(chalk24.red("\u274C DATABASE_URL not found in environment"));
|
|
4326
|
+
console.log(chalk24.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
4210
4327
|
process.exit(1);
|
|
4211
4328
|
}
|
|
4212
4329
|
const dbInfo = parseDatabaseUrl2(env7.DATABASE_URL);
|
|
4213
|
-
console.log(
|
|
4214
|
-
console.log(
|
|
4330
|
+
console.log(chalk24.yellow("\u26A0\uFE0F WARNING: This will drop all tables in your database!"));
|
|
4331
|
+
console.log(chalk24.dim(` Target: ${dbInfo.database} @ ${dbInfo.host}:${dbInfo.port}
|
|
4215
4332
|
`));
|
|
4216
4333
|
const { confirm } = await prompts6({
|
|
4217
4334
|
type: "confirm",
|
|
@@ -4220,7 +4337,7 @@ async function dbDrop() {
|
|
|
4220
4337
|
initial: false
|
|
4221
4338
|
});
|
|
4222
4339
|
if (!confirm) {
|
|
4223
|
-
console.log(
|
|
4340
|
+
console.log(chalk24.gray("Cancelled."));
|
|
4224
4341
|
process.exit(0);
|
|
4225
4342
|
}
|
|
4226
4343
|
await confirmDangerousTarget(dbInfo);
|
|
@@ -4233,7 +4350,7 @@ async function dbDrop() {
|
|
|
4233
4350
|
}
|
|
4234
4351
|
|
|
4235
4352
|
// src/commands/db/check.ts
|
|
4236
|
-
import
|
|
4353
|
+
import chalk25 from "chalk";
|
|
4237
4354
|
import ora8 from "ora";
|
|
4238
4355
|
async function dbCheck() {
|
|
4239
4356
|
const spinner = ora8("Checking migration files...").start();
|
|
@@ -4242,7 +4359,7 @@ async function dbCheck() {
|
|
|
4242
4359
|
spinner.succeed("Migration files consistent");
|
|
4243
4360
|
} catch (error) {
|
|
4244
4361
|
spinner.fail("Migration file check failed");
|
|
4245
|
-
console.error(
|
|
4362
|
+
console.error(chalk25.red(error instanceof Error ? error.message : "Unknown error"));
|
|
4246
4363
|
process.exit(1);
|
|
4247
4364
|
}
|
|
4248
4365
|
}
|
|
@@ -4250,25 +4367,25 @@ async function dbCheck() {
|
|
|
4250
4367
|
// src/commands/db/restore.ts
|
|
4251
4368
|
import path4 from "path";
|
|
4252
4369
|
import { spawn as spawn4 } from "child_process";
|
|
4253
|
-
import
|
|
4370
|
+
import chalk26 from "chalk";
|
|
4254
4371
|
import ora9 from "ora";
|
|
4255
4372
|
import prompts7 from "prompts";
|
|
4256
4373
|
import { env as env8 } from "@spfn/core/config";
|
|
4257
4374
|
import { loadEnv as loadEnv8 } from "@spfn/core/server";
|
|
4258
4375
|
async function dbRestore(backupFile, options = {}) {
|
|
4259
|
-
console.log(
|
|
4376
|
+
console.log(chalk26.blue("\u267B\uFE0F Restoring database from backup...\n"));
|
|
4260
4377
|
loadEnv8();
|
|
4261
4378
|
const dbUrl = env8.DATABASE_URL;
|
|
4262
4379
|
if (!dbUrl) {
|
|
4263
|
-
console.error(
|
|
4264
|
-
console.log(
|
|
4380
|
+
console.error(chalk26.red("\u274C DATABASE_URL not found in environment"));
|
|
4381
|
+
console.log(chalk26.yellow("\n\u{1F4A1} Tip: Add DATABASE_URL to your .env file"));
|
|
4265
4382
|
process.exit(1);
|
|
4266
4383
|
}
|
|
4267
4384
|
let file = backupFile;
|
|
4268
4385
|
if (!file) {
|
|
4269
4386
|
const backups = await listBackupFiles();
|
|
4270
4387
|
if (backups.length === 0) {
|
|
4271
|
-
console.log(
|
|
4388
|
+
console.log(chalk26.yellow("No backups found in ./backups directory"));
|
|
4272
4389
|
process.exit(0);
|
|
4273
4390
|
}
|
|
4274
4391
|
const { selected } = await prompts7({
|
|
@@ -4281,31 +4398,31 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4281
4398
|
}))
|
|
4282
4399
|
});
|
|
4283
4400
|
if (!selected) {
|
|
4284
|
-
console.log(
|
|
4401
|
+
console.log(chalk26.gray("Cancelled"));
|
|
4285
4402
|
process.exit(0);
|
|
4286
4403
|
}
|
|
4287
4404
|
file = selected;
|
|
4288
4405
|
}
|
|
4289
4406
|
if (!file) {
|
|
4290
|
-
console.error(
|
|
4407
|
+
console.error(chalk26.red("\u274C No backup file selected"));
|
|
4291
4408
|
process.exit(1);
|
|
4292
4409
|
}
|
|
4293
4410
|
const metadata = await loadBackupMetadata(file);
|
|
4294
4411
|
if (metadata) {
|
|
4295
|
-
console.log(
|
|
4296
|
-
console.log(
|
|
4297
|
-
console.log(
|
|
4412
|
+
console.log(chalk26.blue("\n\u{1F4CB} Backup Information:\n"));
|
|
4413
|
+
console.log(chalk26.dim(` Database: ${metadata.database}`));
|
|
4414
|
+
console.log(chalk26.dim(` Created: ${new Date(metadata.timestamp).toLocaleString()}`));
|
|
4298
4415
|
if (metadata.environment) {
|
|
4299
|
-
console.log(
|
|
4416
|
+
console.log(chalk26.dim(` Environment: ${metadata.environment}`));
|
|
4300
4417
|
}
|
|
4301
4418
|
if (metadata.tags && metadata.tags.length > 0) {
|
|
4302
|
-
console.log(
|
|
4419
|
+
console.log(chalk26.dim(` Tags: ${metadata.tags.join(", ")}`));
|
|
4303
4420
|
}
|
|
4304
4421
|
if (metadata.backup.dataOnly) {
|
|
4305
|
-
console.log(
|
|
4422
|
+
console.log(chalk26.yellow(" \u26A0\uFE0F Data-only backup (no schema)"));
|
|
4306
4423
|
}
|
|
4307
4424
|
if (metadata.backup.schemaOnly) {
|
|
4308
|
-
console.log(
|
|
4425
|
+
console.log(chalk26.yellow(" \u26A0\uFE0F Schema-only backup (no data)"));
|
|
4309
4426
|
}
|
|
4310
4427
|
const warnings2 = [];
|
|
4311
4428
|
const [currentGitInfo, currentMigrationInfo] = await Promise.all([
|
|
@@ -4328,8 +4445,8 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4328
4445
|
}
|
|
4329
4446
|
}
|
|
4330
4447
|
if (warnings2.length > 0) {
|
|
4331
|
-
console.log(
|
|
4332
|
-
warnings2.forEach((warning) => console.log(
|
|
4448
|
+
console.log(chalk26.yellow("\n\u26A0\uFE0F Version Warnings:\n"));
|
|
4449
|
+
warnings2.forEach((warning) => console.log(chalk26.yellow(` - ${warning}`)));
|
|
4333
4450
|
console.log("");
|
|
4334
4451
|
}
|
|
4335
4452
|
}
|
|
@@ -4337,16 +4454,16 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4337
4454
|
const { confirm } = await prompts7({
|
|
4338
4455
|
type: "confirm",
|
|
4339
4456
|
name: "confirm",
|
|
4340
|
-
message:
|
|
4457
|
+
message: chalk26.yellow(`\u26A0\uFE0F This will replace all data in "${dbInfo.database}" @ ${dbInfo.host}:${dbInfo.port}. Continue?`),
|
|
4341
4458
|
initial: false
|
|
4342
4459
|
});
|
|
4343
4460
|
if (!confirm) {
|
|
4344
|
-
console.log(
|
|
4461
|
+
console.log(chalk26.gray("Cancelled"));
|
|
4345
4462
|
process.exit(0);
|
|
4346
4463
|
}
|
|
4347
4464
|
await confirmDangerousTarget(dbInfo);
|
|
4348
4465
|
if (options.dataOnly && options.schemaOnly) {
|
|
4349
|
-
console.error(
|
|
4466
|
+
console.error(chalk26.red("\u274C Cannot use --data-only and --schema-only together"));
|
|
4350
4467
|
process.exit(1);
|
|
4351
4468
|
}
|
|
4352
4469
|
const ext = path4.extname(file);
|
|
@@ -4374,11 +4491,11 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4374
4491
|
args.push(file);
|
|
4375
4492
|
} else {
|
|
4376
4493
|
if (options.drop) {
|
|
4377
|
-
console.log(
|
|
4494
|
+
console.log(chalk26.yellow("\u26A0\uFE0F Note: --drop only works with custom format backups (.dump) and was ignored."));
|
|
4378
4495
|
}
|
|
4379
4496
|
if (options.dataOnly || options.schemaOnly) {
|
|
4380
|
-
console.log(
|
|
4381
|
-
console.log(
|
|
4497
|
+
console.log(chalk26.yellow("\u26A0\uFE0F Note: --data-only and --schema-only options only work with custom format backups (.dump)"));
|
|
4498
|
+
console.log(chalk26.yellow(" For SQL files, the backup must have been created with the desired option.\n"));
|
|
4382
4499
|
}
|
|
4383
4500
|
args.push("-h", dbInfo.host);
|
|
4384
4501
|
args.push("-p", dbInfo.port);
|
|
@@ -4421,7 +4538,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4421
4538
|
}
|
|
4422
4539
|
if (verbose) {
|
|
4423
4540
|
spinner.stop();
|
|
4424
|
-
console.log(
|
|
4541
|
+
console.log(chalk26.dim(` ${line.trim()}`));
|
|
4425
4542
|
spinner.start();
|
|
4426
4543
|
}
|
|
4427
4544
|
}
|
|
@@ -4429,7 +4546,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4429
4546
|
restoreProcess.stdout?.on("data", (data) => {
|
|
4430
4547
|
if (verbose) {
|
|
4431
4548
|
spinner.stop();
|
|
4432
|
-
console.log(
|
|
4549
|
+
console.log(chalk26.dim(` ${data.toString().trim()}`));
|
|
4433
4550
|
spinner.start();
|
|
4434
4551
|
}
|
|
4435
4552
|
});
|
|
@@ -4439,31 +4556,31 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4439
4556
|
const summary = objectCount > 0 ? ` (${objectCount} objects)` : "";
|
|
4440
4557
|
spinner.succeed(`Restore completed${summary}`);
|
|
4441
4558
|
if (warnings.length > 0) {
|
|
4442
|
-
console.log(
|
|
4559
|
+
console.log(chalk26.yellow(`
|
|
4443
4560
|
\u26A0\uFE0F Warnings during restore (${warnings.length}):
|
|
4444
4561
|
`));
|
|
4445
4562
|
for (const w of warnings) {
|
|
4446
|
-
console.log(
|
|
4563
|
+
console.log(chalk26.yellow(` - ${w}`));
|
|
4447
4564
|
}
|
|
4448
4565
|
}
|
|
4449
|
-
console.log(
|
|
4566
|
+
console.log(chalk26.green("\n\u2705 Database restored successfully"));
|
|
4450
4567
|
resolve3();
|
|
4451
4568
|
} else {
|
|
4452
4569
|
spinner.fail("Restore failed");
|
|
4453
4570
|
if (errors.length > 0) {
|
|
4454
|
-
console.error(
|
|
4571
|
+
console.error(chalk26.red(`
|
|
4455
4572
|
\u274C Errors (${errors.length}):
|
|
4456
4573
|
`));
|
|
4457
4574
|
for (const e of errors) {
|
|
4458
|
-
console.error(
|
|
4575
|
+
console.error(chalk26.red(` - ${e}`));
|
|
4459
4576
|
}
|
|
4460
4577
|
}
|
|
4461
4578
|
if (warnings.length > 0) {
|
|
4462
|
-
console.log(
|
|
4579
|
+
console.log(chalk26.yellow(`
|
|
4463
4580
|
\u26A0\uFE0F Warnings (${warnings.length}):
|
|
4464
4581
|
`));
|
|
4465
4582
|
for (const w of warnings) {
|
|
4466
|
-
console.log(
|
|
4583
|
+
console.log(chalk26.yellow(` - ${w}`));
|
|
4467
4584
|
}
|
|
4468
4585
|
}
|
|
4469
4586
|
const fallback = errors.length === 0 && warnings.length === 0 ? "Restore failed with no output" : "";
|
|
@@ -4477,7 +4594,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4477
4594
|
}).catch((error) => {
|
|
4478
4595
|
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
4479
4596
|
if (msg) {
|
|
4480
|
-
console.error(
|
|
4597
|
+
console.error(chalk26.red(`
|
|
4481
4598
|
\u274C ${msg}`));
|
|
4482
4599
|
}
|
|
4483
4600
|
process.exit(1);
|
|
@@ -4485,17 +4602,17 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4485
4602
|
}
|
|
4486
4603
|
|
|
4487
4604
|
// src/commands/db/list.ts
|
|
4488
|
-
import
|
|
4605
|
+
import chalk27 from "chalk";
|
|
4489
4606
|
async function dbBackupList() {
|
|
4490
|
-
console.log(
|
|
4607
|
+
console.log(chalk27.blue("\u{1F4CB} Database backups:\n"));
|
|
4491
4608
|
const backups = await listBackupFiles();
|
|
4492
4609
|
if (backups.length === 0) {
|
|
4493
|
-
console.log(
|
|
4494
|
-
console.log(
|
|
4610
|
+
console.log(chalk27.yellow("No backups found in ./backups directory"));
|
|
4611
|
+
console.log(chalk27.gray("\n\u{1F4A1} Create a backup with: pnpm spfn db backup\n"));
|
|
4495
4612
|
return;
|
|
4496
4613
|
}
|
|
4497
|
-
console.log(
|
|
4498
|
-
console.log(
|
|
4614
|
+
console.log(chalk27.bold(" Date Size File"));
|
|
4615
|
+
console.log(chalk27.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
4499
4616
|
backups.forEach((backup) => {
|
|
4500
4617
|
const date = backup.date.toLocaleString("en-US", {
|
|
4501
4618
|
year: "numeric",
|
|
@@ -4506,23 +4623,23 @@ async function dbBackupList() {
|
|
|
4506
4623
|
second: "2-digit"
|
|
4507
4624
|
});
|
|
4508
4625
|
const sizeStr = backup.size.padEnd(10);
|
|
4509
|
-
console.log(
|
|
4626
|
+
console.log(chalk27.white(` ${date} ${sizeStr} ${backup.name}`));
|
|
4510
4627
|
});
|
|
4511
|
-
console.log(
|
|
4628
|
+
console.log(chalk27.gray(`
|
|
4512
4629
|
Total: ${backups.length} backup(s)
|
|
4513
4630
|
`));
|
|
4514
4631
|
}
|
|
4515
4632
|
|
|
4516
4633
|
// src/commands/db/clean.ts
|
|
4517
4634
|
import { promises as fs4 } from "fs";
|
|
4518
|
-
import
|
|
4635
|
+
import chalk28 from "chalk";
|
|
4519
4636
|
import ora10 from "ora";
|
|
4520
4637
|
import prompts8 from "prompts";
|
|
4521
4638
|
async function dbBackupClean(options) {
|
|
4522
|
-
console.log(
|
|
4639
|
+
console.log(chalk28.blue("\u{1F9F9} Cleaning old backups...\n"));
|
|
4523
4640
|
const backups = await listBackupFiles();
|
|
4524
4641
|
if (backups.length === 0) {
|
|
4525
|
-
console.log(
|
|
4642
|
+
console.log(chalk28.yellow("No backups found"));
|
|
4526
4643
|
return;
|
|
4527
4644
|
}
|
|
4528
4645
|
let toDelete = [];
|
|
@@ -4539,13 +4656,13 @@ async function dbBackupClean(options) {
|
|
|
4539
4656
|
toDelete = backups.slice(defaultKeep);
|
|
4540
4657
|
}
|
|
4541
4658
|
if (toDelete.length === 0) {
|
|
4542
|
-
console.log(
|
|
4659
|
+
console.log(chalk28.green("\u2705 No backups to clean"));
|
|
4543
4660
|
return;
|
|
4544
4661
|
}
|
|
4545
|
-
console.log(
|
|
4662
|
+
console.log(chalk28.yellow(`The following ${toDelete.length} backup(s) will be deleted:
|
|
4546
4663
|
`));
|
|
4547
4664
|
toDelete.forEach((backup) => {
|
|
4548
|
-
console.log(
|
|
4665
|
+
console.log(chalk28.gray(` - ${backup.name} (${backup.size})`));
|
|
4549
4666
|
});
|
|
4550
4667
|
const { confirm } = await prompts8({
|
|
4551
4668
|
type: "confirm",
|
|
@@ -4554,7 +4671,7 @@ async function dbBackupClean(options) {
|
|
|
4554
4671
|
initial: false
|
|
4555
4672
|
});
|
|
4556
4673
|
if (!confirm) {
|
|
4557
|
-
console.log(
|
|
4674
|
+
console.log(chalk28.gray("Cancelled"));
|
|
4558
4675
|
return;
|
|
4559
4676
|
}
|
|
4560
4677
|
const spinner = ora10("Deleting backups...").start();
|
|
@@ -4565,19 +4682,19 @@ async function dbBackupClean(options) {
|
|
|
4565
4682
|
fs4.rm(backup.path.replace(/\.(sql|dump)$/, ".meta.json"), { force: true })
|
|
4566
4683
|
]));
|
|
4567
4684
|
spinner.succeed("Backups deleted");
|
|
4568
|
-
console.log(
|
|
4685
|
+
console.log(chalk28.green(`
|
|
4569
4686
|
\u2705 Deleted ${toDelete.length} backup(s)`));
|
|
4570
4687
|
} catch (error) {
|
|
4571
4688
|
spinner.fail("Failed to delete backups");
|
|
4572
|
-
console.error(
|
|
4689
|
+
console.error(chalk28.red(error instanceof Error ? error.message : "Unknown error"));
|
|
4573
4690
|
process.exit(1);
|
|
4574
4691
|
}
|
|
4575
4692
|
}
|
|
4576
4693
|
|
|
4577
4694
|
// src/commands/db/reindex.ts
|
|
4578
4695
|
import { existsSync as existsSync25, readFileSync as readFileSync10, writeFileSync as writeFileSync16, renameSync, copyFileSync } from "fs";
|
|
4579
|
-
import { join as
|
|
4580
|
-
import
|
|
4696
|
+
import { join as join24 } from "path";
|
|
4697
|
+
import chalk29 from "chalk";
|
|
4581
4698
|
import { loadEnv as loadEnv9 } from "@spfn/core/server";
|
|
4582
4699
|
function isTimestampPrefix(tag) {
|
|
4583
4700
|
const prefix = tag.split("_")[0];
|
|
@@ -4598,15 +4715,15 @@ async function dbReindex(options = {}) {
|
|
|
4598
4715
|
const { getDrizzleConfig } = await import("@spfn/core/db");
|
|
4599
4716
|
const config = getDrizzleConfig({ disablePackageDiscovery: true });
|
|
4600
4717
|
const outDir = config.out;
|
|
4601
|
-
const journalPath =
|
|
4718
|
+
const journalPath = join24(outDir, "meta", "_journal.json");
|
|
4602
4719
|
if (!existsSync25(journalPath)) {
|
|
4603
|
-
console.error(
|
|
4604
|
-
console.log(
|
|
4720
|
+
console.error(chalk29.red("\u274C No _journal.json found at:"), journalPath);
|
|
4721
|
+
console.log(chalk29.yellow("\u{1F4A1} Run `spfn db generate` first to create migrations"));
|
|
4605
4722
|
process.exit(1);
|
|
4606
4723
|
}
|
|
4607
4724
|
const journal = JSON.parse(readFileSync10(journalPath, "utf-8"));
|
|
4608
4725
|
if (journal.entries.length === 0) {
|
|
4609
|
-
console.log(
|
|
4726
|
+
console.log(chalk29.yellow("No migration entries found \u2014 nothing to reindex."));
|
|
4610
4727
|
return;
|
|
4611
4728
|
}
|
|
4612
4729
|
const renames = [];
|
|
@@ -4620,46 +4737,46 @@ async function dbReindex(options = {}) {
|
|
|
4620
4737
|
const { prefix: oldPrefix, suffix } = parseTag(entry.tag);
|
|
4621
4738
|
const newPrefix = String(entry.when);
|
|
4622
4739
|
const newTag = suffix ? `${newPrefix}_${suffix}` : newPrefix;
|
|
4623
|
-
const oldSql =
|
|
4624
|
-
const newSql =
|
|
4740
|
+
const oldSql = join24(outDir, `${entry.tag}.sql`);
|
|
4741
|
+
const newSql = join24(outDir, `${newTag}.sql`);
|
|
4625
4742
|
if (existsSync25(oldSql)) {
|
|
4626
4743
|
renames.push({ type: "sql", from: oldSql, to: newSql });
|
|
4627
4744
|
}
|
|
4628
|
-
const oldSnapshot =
|
|
4629
|
-
const newSnapshot =
|
|
4745
|
+
const oldSnapshot = join24(outDir, "meta", `${oldPrefix}_snapshot.json`);
|
|
4746
|
+
const newSnapshot = join24(outDir, "meta", `${newPrefix}_snapshot.json`);
|
|
4630
4747
|
if (existsSync25(oldSnapshot)) {
|
|
4631
4748
|
renames.push({ type: "snapshot", from: oldSnapshot, to: newSnapshot });
|
|
4632
4749
|
}
|
|
4633
4750
|
tagUpdates.push({ idx: entry.idx, oldTag: entry.tag, newTag });
|
|
4634
4751
|
}
|
|
4635
4752
|
if (tagUpdates.length === 0) {
|
|
4636
|
-
console.log(
|
|
4753
|
+
console.log(chalk29.green("\u2705 All migrations already use timestamp prefix \u2014 nothing to do."));
|
|
4637
4754
|
if (skipped > 0) {
|
|
4638
|
-
console.log(
|
|
4755
|
+
console.log(chalk29.dim(` (${skipped} entries already timestamp-prefixed)`));
|
|
4639
4756
|
}
|
|
4640
4757
|
return;
|
|
4641
4758
|
}
|
|
4642
|
-
console.log(
|
|
4759
|
+
console.log(chalk29.bold("\n\u{1F4CB} Reindex plan:\n"));
|
|
4643
4760
|
for (const update of tagUpdates) {
|
|
4644
4761
|
console.log(
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4762
|
+
chalk29.dim(` [${update.idx}]`),
|
|
4763
|
+
chalk29.red(update.oldTag),
|
|
4764
|
+
chalk29.dim("\u2192"),
|
|
4765
|
+
chalk29.green(update.newTag)
|
|
4649
4766
|
);
|
|
4650
4767
|
}
|
|
4651
|
-
console.log(
|
|
4768
|
+
console.log(chalk29.dim(`
|
|
4652
4769
|
${renames.length} file(s) to rename, ${tagUpdates.length} journal tag(s) to update`));
|
|
4653
4770
|
if (skipped > 0) {
|
|
4654
|
-
console.log(
|
|
4771
|
+
console.log(chalk29.dim(` ${skipped} entry/entries already timestamp-prefixed (skipped)`));
|
|
4655
4772
|
}
|
|
4656
4773
|
if (options.dryRun) {
|
|
4657
|
-
console.log(
|
|
4774
|
+
console.log(chalk29.yellow("\n\u{1F50D} Dry run \u2014 no changes applied."));
|
|
4658
4775
|
return;
|
|
4659
4776
|
}
|
|
4660
4777
|
const backupPath = journalPath + ".bak";
|
|
4661
4778
|
copyFileSync(journalPath, backupPath);
|
|
4662
|
-
console.log(
|
|
4779
|
+
console.log(chalk29.dim(`
|
|
4663
4780
|
Backed up journal \u2192 ${backupPath}`));
|
|
4664
4781
|
for (const rename of renames) {
|
|
4665
4782
|
renameSync(rename.from, rename.to);
|
|
@@ -4671,12 +4788,12 @@ async function dbReindex(options = {}) {
|
|
|
4671
4788
|
}
|
|
4672
4789
|
}
|
|
4673
4790
|
writeFileSync16(journalPath, JSON.stringify(journal, null, 2) + "\n");
|
|
4674
|
-
console.log(
|
|
4791
|
+
console.log(chalk29.green(`
|
|
4675
4792
|
\u2705 Reindex complete \u2014 ${tagUpdates.length} migration(s) converted to timestamp prefix.`));
|
|
4676
4793
|
}
|
|
4677
4794
|
|
|
4678
4795
|
// src/commands/db/index.ts
|
|
4679
|
-
var dbCommand = new
|
|
4796
|
+
var dbCommand = new Command11("db").description("Database management commands (wraps Drizzle Kit)");
|
|
4680
4797
|
dbCommand.command("generate").alias("g").description("Generate database migrations from schema changes").action(dbGenerate);
|
|
4681
4798
|
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));
|
|
4682
4799
|
dbCommand.command("migrate").alias("m").description("Run pending migrations").option("--with-backup", "Create backup before running migrations").action((options) => dbMigrate(options));
|
|
@@ -4692,22 +4809,22 @@ dbCommand.command("reindex").description("Convert migration files from sequentia
|
|
|
4692
4809
|
|
|
4693
4810
|
// src/commands/add.ts
|
|
4694
4811
|
init_package_manager();
|
|
4695
|
-
import { Command as
|
|
4812
|
+
import { Command as Command12 } from "commander";
|
|
4696
4813
|
import { existsSync as existsSync27, readFileSync as readFileSync11 } from "fs";
|
|
4697
|
-
import { join as
|
|
4814
|
+
import { join as join26 } from "path";
|
|
4698
4815
|
import { execa as execa10 } from "execa";
|
|
4699
|
-
import
|
|
4816
|
+
import chalk31 from "chalk";
|
|
4700
4817
|
import ora11 from "ora";
|
|
4701
4818
|
|
|
4702
4819
|
// src/commands/add-vercel.ts
|
|
4703
4820
|
init_templates();
|
|
4704
4821
|
import { copyFileSync as copyFileSync2, existsSync as existsSync26, mkdirSync as mkdirSync4 } from "fs";
|
|
4705
|
-
import { dirname as dirname3, join as
|
|
4706
|
-
import
|
|
4822
|
+
import { dirname as dirname3, join as join25 } from "path";
|
|
4823
|
+
import chalk30 from "chalk";
|
|
4707
4824
|
var FILES = [
|
|
4708
4825
|
{
|
|
4709
4826
|
template: "route.ts",
|
|
4710
|
-
target:
|
|
4827
|
+
target: join25("src", "app", "api", "backend", "[[...route]]", "route.ts"),
|
|
4711
4828
|
description: "hono/vercel adapter (mounts the SPFN app under /api/backend)"
|
|
4712
4829
|
},
|
|
4713
4830
|
{
|
|
@@ -4722,35 +4839,35 @@ var FILES = [
|
|
|
4722
4839
|
}
|
|
4723
4840
|
];
|
|
4724
4841
|
async function addVercel() {
|
|
4725
|
-
console.log(
|
|
4842
|
+
console.log(chalk30.blue("\n\u{1F4E6} Setting up the Vercel serverless target...\n"));
|
|
4726
4843
|
const cwd = process.cwd();
|
|
4727
|
-
const templatesDir =
|
|
4844
|
+
const templatesDir = join25(findTemplatesPath(), "vercel");
|
|
4728
4845
|
for (const file of FILES) {
|
|
4729
|
-
const targetPath =
|
|
4846
|
+
const targetPath = join25(cwd, file.target);
|
|
4730
4847
|
if (existsSync26(targetPath)) {
|
|
4731
|
-
console.log(
|
|
4848
|
+
console.log(chalk30.yellow(`\u23ED\uFE0F ${file.target} already exists \u2014 skipped (not overwritten)`));
|
|
4732
4849
|
continue;
|
|
4733
4850
|
}
|
|
4734
4851
|
mkdirSync4(dirname3(targetPath), { recursive: true });
|
|
4735
|
-
copyFileSync2(
|
|
4736
|
-
console.log(
|
|
4737
|
-
}
|
|
4738
|
-
console.log(
|
|
4739
|
-
console.log(
|
|
4740
|
-
console.log(
|
|
4741
|
-
console.log(
|
|
4742
|
-
console.log(
|
|
4743
|
-
console.log(
|
|
4744
|
-
console.log(
|
|
4745
|
-
console.log(
|
|
4746
|
-
console.log(
|
|
4747
|
-
console.log(
|
|
4748
|
-
console.log(
|
|
4749
|
-
console.log(
|
|
4750
|
-
console.log(
|
|
4751
|
-
console.log(
|
|
4752
|
-
console.log(
|
|
4753
|
-
console.log(
|
|
4852
|
+
copyFileSync2(join25(templatesDir, file.template), targetPath);
|
|
4853
|
+
console.log(chalk30.green(`\u2705 ${file.target}`) + chalk30.gray(` \u2014 ${file.description}`));
|
|
4854
|
+
}
|
|
4855
|
+
console.log(chalk30.green("\n\u2705 Vercel target ready!\n"));
|
|
4856
|
+
console.log(chalk30.cyan("\u{1F4DA} Next steps:"));
|
|
4857
|
+
console.log(chalk30.gray(" 1. Make sure `hono` is a direct dependency (hono/vercel must resolve from the app):"));
|
|
4858
|
+
console.log(chalk30.gray(" pnpm add hono"));
|
|
4859
|
+
console.log(chalk30.gray(" 2. Set Vercel project env vars:"));
|
|
4860
|
+
console.log(chalk30.gray(" GITEA_NPM_TOKEN \u2014 @spfn registry token (install step)"));
|
|
4861
|
+
console.log(chalk30.gray(" SPFN_AUTH_* \u2014 auth secrets (same as local .env.server)"));
|
|
4862
|
+
console.log(chalk30.gray(" SPFN_API_URL \u2014 https://<your-domain>/api/backend (RPC proxy origin)"));
|
|
4863
|
+
console.log(chalk30.gray(" DATABASE_URL \u2014 set automatically from POSTGRES_URL if you use the"));
|
|
4864
|
+
console.log(chalk30.gray(" Vercel Supabase integration (mapped in route.ts)"));
|
|
4865
|
+
console.log(chalk30.gray(" 3. Run migrations against the DIRECT (non-pooler) connection before deploying:"));
|
|
4866
|
+
console.log(chalk30.gray(" DATABASE_URL=<direct-url> pnpm spfn db migrate"));
|
|
4867
|
+
console.log(chalk30.gray(" 4. Seed/RBAC provisioning runs once per deploy, not per cold start:"));
|
|
4868
|
+
console.log(chalk30.gray(" pnpm spfn provision (e.g. from the build step or a deploy hook)"));
|
|
4869
|
+
console.log(chalk30.gray(" 5. Jobs (config.jobs) are NOT processed on serverless \u2014 drain the queue from a"));
|
|
4870
|
+
console.log(chalk30.gray(" scheduled endpoint (Vercel Cron) or run workers on an always-on target.\n"));
|
|
4754
4871
|
}
|
|
4755
4872
|
|
|
4756
4873
|
// src/commands/add.ts
|
|
@@ -4760,17 +4877,19 @@ async function addPackage(packageName) {
|
|
|
4760
4877
|
return;
|
|
4761
4878
|
}
|
|
4762
4879
|
if (!packageName.includes("/")) {
|
|
4763
|
-
console.error(
|
|
4764
|
-
console.log(
|
|
4765
|
-
console.log(
|
|
4766
|
-
console.log(
|
|
4880
|
+
console.error(chalk31.red("\u274C Please specify full package name"));
|
|
4881
|
+
console.log(chalk31.yellow("\n\u{1F4A1} Examples:"));
|
|
4882
|
+
console.log(chalk31.gray(" pnpm spfn add @spfn/cms"));
|
|
4883
|
+
console.log(chalk31.gray(" pnpm spfn add @mycompany/spfn-analytics"));
|
|
4884
|
+
console.log(chalk31.gray("\n Deploy targets are unscoped. The only one today is:"));
|
|
4885
|
+
console.log(chalk31.gray(" pnpm spfn add vercel"));
|
|
4767
4886
|
process.exit(1);
|
|
4768
4887
|
}
|
|
4769
|
-
console.log(
|
|
4888
|
+
console.log(chalk31.blue(`
|
|
4770
4889
|
\u{1F4E6} Setting up ${packageName}...
|
|
4771
4890
|
`));
|
|
4772
|
-
const pkgPath =
|
|
4773
|
-
const pkgJsonPath =
|
|
4891
|
+
const pkgPath = join26(process.cwd(), "node_modules", ...packageName.split("/"));
|
|
4892
|
+
const pkgJsonPath = join26(pkgPath, "package.json");
|
|
4774
4893
|
if (!existsSync27(pkgJsonPath)) {
|
|
4775
4894
|
const pm = detectPackageManager(process.cwd());
|
|
4776
4895
|
const installSpinner = ora11("Installing package...").start();
|
|
@@ -4779,27 +4898,27 @@ async function addPackage(packageName) {
|
|
|
4779
4898
|
installSpinner.succeed("Package installed");
|
|
4780
4899
|
} catch (error) {
|
|
4781
4900
|
installSpinner.fail("Failed to install package");
|
|
4782
|
-
console.error(
|
|
4901
|
+
console.error(chalk31.red(error instanceof Error ? error.message : "Unknown error"));
|
|
4783
4902
|
process.exit(1);
|
|
4784
4903
|
}
|
|
4785
4904
|
} else {
|
|
4786
|
-
console.log(
|
|
4905
|
+
console.log(chalk31.gray("\u2713 Package already installed (using local version)\n"));
|
|
4787
4906
|
}
|
|
4788
4907
|
if (!existsSync27(pkgJsonPath)) {
|
|
4789
|
-
console.error(
|
|
4908
|
+
console.error(chalk31.red(`\u274C Package ${packageName} not found after installation`));
|
|
4790
4909
|
process.exit(1);
|
|
4791
4910
|
}
|
|
4792
4911
|
const pkgJson = JSON.parse(readFileSync11(pkgJsonPath, "utf-8"));
|
|
4793
4912
|
if (pkgJson.spfn?.migrations) {
|
|
4794
|
-
console.log(
|
|
4913
|
+
console.log(chalk31.blue(`
|
|
4795
4914
|
\u{1F5C4}\uFE0F Setting up database for ${packageName}...
|
|
4796
4915
|
`));
|
|
4797
4916
|
const { env: env9 } = await import("@spfn/core/config");
|
|
4798
4917
|
if (!env9.DATABASE_URL) {
|
|
4799
|
-
console.log(
|
|
4800
|
-
console.log(
|
|
4801
|
-
console.log(
|
|
4802
|
-
console.log(
|
|
4918
|
+
console.log(chalk31.yellow("\u26A0\uFE0F DATABASE_URL not found \u2014 skipping database setup."));
|
|
4919
|
+
console.log(chalk31.yellow(` ${packageName} tables are created by its bundled migrations, not by schema push.`));
|
|
4920
|
+
console.log(chalk31.cyan(" Once DATABASE_URL is set, run: pnpm spfn db migrate"));
|
|
4921
|
+
console.log(chalk31.gray(" (check state anytime with: pnpm spfn db status)\n"));
|
|
4803
4922
|
} else {
|
|
4804
4923
|
const { discoverFunctionMigrations: discoverFunctionMigrations2, loadFunctionMigrationPlans: loadFunctionMigrationPlans2, executeFunctionMigrations: executeFunctionMigrations2 } = await Promise.resolve().then(() => (init_function_migrations(), function_migrations_exports));
|
|
4805
4924
|
const functions = discoverFunctionMigrations2(process.cwd());
|
|
@@ -4811,30 +4930,30 @@ async function addPackage(packageName) {
|
|
|
4811
4930
|
spinner.succeed("Migrations applied");
|
|
4812
4931
|
} catch (error) {
|
|
4813
4932
|
spinner.fail("Failed to apply migrations");
|
|
4814
|
-
console.error(
|
|
4933
|
+
console.error(chalk31.red(error instanceof Error ? error.message : "Unknown error"));
|
|
4815
4934
|
process.exit(1);
|
|
4816
4935
|
}
|
|
4817
4936
|
} else {
|
|
4818
|
-
console.log(
|
|
4937
|
+
console.log(chalk31.gray("\u2139\uFE0F No migrations found for this package"));
|
|
4819
4938
|
}
|
|
4820
4939
|
}
|
|
4821
4940
|
} else {
|
|
4822
|
-
console.log(
|
|
4941
|
+
console.log(chalk31.gray("\n\u2139\uFE0F No database migrations to apply"));
|
|
4823
4942
|
}
|
|
4824
|
-
console.log(
|
|
4943
|
+
console.log(chalk31.green(`
|
|
4825
4944
|
\u2705 ${packageName} installed successfully!
|
|
4826
4945
|
`));
|
|
4827
4946
|
if (pkgJson.spfn?.setupMessage) {
|
|
4828
|
-
console.log(
|
|
4947
|
+
console.log(chalk31.cyan("\u{1F4DA} Setup Guide:"));
|
|
4829
4948
|
console.log(pkgJson.spfn.setupMessage);
|
|
4830
4949
|
console.log();
|
|
4831
4950
|
}
|
|
4832
4951
|
}
|
|
4833
|
-
var addCommand = new
|
|
4952
|
+
var addCommand = new Command12("add").description("Add a capability to this project: an SPFN ecosystem package, or a deploy target").argument("<package|target>", "Scoped package name (e.g., @spfn/cms, @mycompany/spfn-analytics), or a deploy target: vercel").action(addPackage);
|
|
4834
4953
|
|
|
4835
4954
|
// src/commands/env.ts
|
|
4836
|
-
import { Command as
|
|
4837
|
-
import
|
|
4955
|
+
import { Command as Command13 } from "commander";
|
|
4956
|
+
import chalk32 from "chalk";
|
|
4838
4957
|
import { existsSync as existsSync28, readFileSync as readFileSync12, writeFileSync as writeFileSync17 } from "fs";
|
|
4839
4958
|
import { resolve as resolve2 } from "path";
|
|
4840
4959
|
import { parse as parse2 } from "dotenv";
|
|
@@ -4891,26 +5010,26 @@ function getEnvFilesForEnvironment(nodeEnv) {
|
|
|
4891
5010
|
}
|
|
4892
5011
|
function formatType(type) {
|
|
4893
5012
|
const typeColors = {
|
|
4894
|
-
string:
|
|
4895
|
-
number:
|
|
4896
|
-
boolean:
|
|
4897
|
-
url:
|
|
4898
|
-
enum:
|
|
4899
|
-
json:
|
|
5013
|
+
string: chalk32.green,
|
|
5014
|
+
number: chalk32.blue,
|
|
5015
|
+
boolean: chalk32.yellow,
|
|
5016
|
+
url: chalk32.cyan,
|
|
5017
|
+
enum: chalk32.magenta,
|
|
5018
|
+
json: chalk32.red
|
|
4900
5019
|
};
|
|
4901
|
-
return (typeColors[type] ||
|
|
5020
|
+
return (typeColors[type] || chalk32.white)(type);
|
|
4902
5021
|
}
|
|
4903
5022
|
function formatDefault(value, type) {
|
|
4904
5023
|
if (value === void 0) {
|
|
4905
|
-
return
|
|
5024
|
+
return chalk32.dim("(none)");
|
|
4906
5025
|
}
|
|
4907
5026
|
if (type === "string" || type === "url") {
|
|
4908
|
-
return
|
|
5027
|
+
return chalk32.green(`"${value}"`);
|
|
4909
5028
|
}
|
|
4910
5029
|
if (type === "boolean") {
|
|
4911
|
-
return value ?
|
|
5030
|
+
return value ? chalk32.green("true") : chalk32.red("false");
|
|
4912
5031
|
}
|
|
4913
|
-
return
|
|
5032
|
+
return chalk32.cyan(String(value));
|
|
4914
5033
|
}
|
|
4915
5034
|
async function listEnvVars(options) {
|
|
4916
5035
|
const packageName = options.package || "@spfn/core";
|
|
@@ -4924,28 +5043,28 @@ async function listEnvVars(options) {
|
|
|
4924
5043
|
acc[target].push([key, schema]);
|
|
4925
5044
|
return acc;
|
|
4926
5045
|
}, {});
|
|
4927
|
-
console.log(
|
|
5046
|
+
console.log(chalk32.blue.bold(`
|
|
4928
5047
|
\u{1F4CB} Environment Variables by File (${packageName})
|
|
4929
5048
|
`));
|
|
4930
5049
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
4931
|
-
console.log(
|
|
5050
|
+
console.log(chalk32.bold.magenta(`
|
|
4932
5051
|
${file}`));
|
|
4933
|
-
console.log(
|
|
5052
|
+
console.log(chalk32.dim("\u2500".repeat(50)));
|
|
4934
5053
|
for (const [key, schema] of vars) {
|
|
4935
5054
|
printEnvVar(key, schema);
|
|
4936
5055
|
}
|
|
4937
5056
|
}
|
|
4938
5057
|
} else {
|
|
4939
|
-
console.log(
|
|
5058
|
+
console.log(chalk32.blue.bold(`
|
|
4940
5059
|
\u{1F4CB} Environment Variables (${packageName})
|
|
4941
5060
|
`));
|
|
4942
5061
|
for (const [key, schema] of allVars) {
|
|
4943
5062
|
printEnvVar(key, schema, true);
|
|
4944
5063
|
}
|
|
4945
5064
|
}
|
|
4946
|
-
console.log(
|
|
5065
|
+
console.log(chalk32.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
|
|
4947
5066
|
} catch (error) {
|
|
4948
|
-
console.error(
|
|
5067
|
+
console.error(chalk32.red(`
|
|
4949
5068
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
4950
5069
|
`));
|
|
4951
5070
|
process.exit(1);
|
|
@@ -4953,17 +5072,17 @@ ${file}`));
|
|
|
4953
5072
|
}
|
|
4954
5073
|
function printEnvVar(key, schema, showFile = false) {
|
|
4955
5074
|
const typeStr = formatType(schema.type);
|
|
4956
|
-
const requiredStr = schema.required || schema.default !== void 0 ?
|
|
4957
|
-
const sensitiveStr = schema.sensitive ?
|
|
4958
|
-
const fileStr = showFile ?
|
|
4959
|
-
console.log(`${
|
|
4960
|
-
console.log(` ${
|
|
5075
|
+
const requiredStr = schema.required || schema.default !== void 0 ? chalk32.red("[required]") : chalk32.dim("[optional]");
|
|
5076
|
+
const sensitiveStr = schema.sensitive ? chalk32.yellow(" [sensitive]") : "";
|
|
5077
|
+
const fileStr = showFile ? chalk32.dim(` \u2192 ${getTargetFile(schema)}`) : "";
|
|
5078
|
+
console.log(`${chalk32.bold.cyan(key)} ${chalk32.dim("(")}${typeStr}${chalk32.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
|
|
5079
|
+
console.log(` ${chalk32.dim(schema.description)}`);
|
|
4961
5080
|
if (schema.default !== void 0) {
|
|
4962
|
-
console.log(` ${
|
|
5081
|
+
console.log(` ${chalk32.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
4963
5082
|
}
|
|
4964
5083
|
if (schema.examples && schema.examples.length > 0) {
|
|
4965
5084
|
const exampleStr = schema.examples.map((ex) => formatDefault(ex, schema.type)).join(", ");
|
|
4966
|
-
console.log(` ${
|
|
5085
|
+
console.log(` ${chalk32.dim("Examples:")} ${exampleStr}`);
|
|
4967
5086
|
}
|
|
4968
5087
|
console.log();
|
|
4969
5088
|
}
|
|
@@ -4971,7 +5090,7 @@ async function showEnvStats(options) {
|
|
|
4971
5090
|
const packageName = options.package || "@spfn/core";
|
|
4972
5091
|
try {
|
|
4973
5092
|
const envSchema = await loadEnvSchema(packageName);
|
|
4974
|
-
console.log(
|
|
5093
|
+
console.log(chalk32.blue.bold(`
|
|
4975
5094
|
\u{1F4CA} Environment Variable Statistics (${packageName})
|
|
4976
5095
|
`));
|
|
4977
5096
|
const allVars = Object.entries(envSchema);
|
|
@@ -4993,24 +5112,24 @@ async function showEnvStats(options) {
|
|
|
4993
5112
|
acc[file] = (acc[file] || 0) + 1;
|
|
4994
5113
|
return acc;
|
|
4995
5114
|
}, {});
|
|
4996
|
-
console.log(`${
|
|
4997
|
-
console.log(`${
|
|
4998
|
-
console.log(`${
|
|
4999
|
-
console.log(`${
|
|
5000
|
-
console.log(
|
|
5001
|
-
console.log(` ${
|
|
5002
|
-
console.log(` ${
|
|
5003
|
-
console.log(
|
|
5115
|
+
console.log(`${chalk32.bold("Total variables:")} ${chalk32.cyan(allVars.length)}`);
|
|
5116
|
+
console.log(`${chalk32.bold("Required:")} ${chalk32.red(required.length)}`);
|
|
5117
|
+
console.log(`${chalk32.bold("Optional:")} ${chalk32.dim(optional.length)}`);
|
|
5118
|
+
console.log(`${chalk32.bold("Sensitive:")} ${chalk32.yellow(sensitive.length)}`);
|
|
5119
|
+
console.log(chalk32.bold("\nBy Target:"));
|
|
5120
|
+
console.log(` ${chalk32.blue("Next.js accessible:")} ${chalk32.cyan(nextjsVars.length)}`);
|
|
5121
|
+
console.log(` ${chalk32.magenta("SPFN server only:")} ${chalk32.cyan(serverOnlyVars.length)}`);
|
|
5122
|
+
console.log(chalk32.bold("\nBy File:"));
|
|
5004
5123
|
for (const [file, count] of Object.entries(fileCount)) {
|
|
5005
|
-
console.log(` ${
|
|
5124
|
+
console.log(` ${chalk32.dim(file)}: ${chalk32.cyan(count)}`);
|
|
5006
5125
|
}
|
|
5007
|
-
console.log(
|
|
5126
|
+
console.log(chalk32.bold("\nBy Type:"));
|
|
5008
5127
|
for (const [type, count] of Object.entries(typeCount)) {
|
|
5009
|
-
console.log(` ${formatType(type)}: ${
|
|
5128
|
+
console.log(` ${formatType(type)}: ${chalk32.cyan(count)}`);
|
|
5010
5129
|
}
|
|
5011
5130
|
console.log();
|
|
5012
5131
|
} catch (error) {
|
|
5013
|
-
console.error(
|
|
5132
|
+
console.error(chalk32.red(`
|
|
5014
5133
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
5015
5134
|
`));
|
|
5016
5135
|
process.exit(1);
|
|
@@ -5030,40 +5149,40 @@ async function searchEnvVars(query, options) {
|
|
|
5030
5149
|
}
|
|
5031
5150
|
}
|
|
5032
5151
|
if (results.length === 0) {
|
|
5033
|
-
console.log(
|
|
5152
|
+
console.log(chalk32.yellow(`
|
|
5034
5153
|
\u26A0\uFE0F No environment variables found matching "${query}"
|
|
5035
5154
|
`));
|
|
5036
5155
|
return;
|
|
5037
5156
|
}
|
|
5038
|
-
console.log(
|
|
5157
|
+
console.log(chalk32.blue.bold(`
|
|
5039
5158
|
\u{1F50D} Found ${results.length} environment variable(s) matching "${query}"
|
|
5040
5159
|
`));
|
|
5041
5160
|
for (const [key, schema] of results) {
|
|
5042
5161
|
const typeStr = formatType(schema.type);
|
|
5043
|
-
const requiredStr = schema.required || schema.default !== void 0 ?
|
|
5044
|
-
console.log(`${
|
|
5045
|
-
console.log(` ${
|
|
5162
|
+
const requiredStr = schema.required || schema.default !== void 0 ? chalk32.red("[required]") : chalk32.dim("[optional]");
|
|
5163
|
+
console.log(`${chalk32.bold.cyan(key)} ${chalk32.dim("(")}${typeStr}${chalk32.dim(")")} ${requiredStr}`);
|
|
5164
|
+
console.log(` ${chalk32.dim(schema.description)}`);
|
|
5046
5165
|
if (schema.default !== void 0) {
|
|
5047
|
-
console.log(` ${
|
|
5166
|
+
console.log(` ${chalk32.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
5048
5167
|
}
|
|
5049
5168
|
console.log();
|
|
5050
5169
|
}
|
|
5051
5170
|
} catch (error) {
|
|
5052
|
-
console.error(
|
|
5171
|
+
console.error(chalk32.red(`
|
|
5053
5172
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
5054
5173
|
`));
|
|
5055
5174
|
process.exit(1);
|
|
5056
5175
|
}
|
|
5057
5176
|
}
|
|
5058
|
-
var envCommand = new
|
|
5177
|
+
var envCommand = new Command13("env").description("Manage environment variables");
|
|
5059
5178
|
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);
|
|
5060
5179
|
envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(showEnvStats);
|
|
5061
5180
|
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);
|
|
5062
5181
|
function validateEnvOption(envValue) {
|
|
5063
5182
|
if (!VALID_ENVS.includes(envValue)) {
|
|
5064
|
-
console.error(
|
|
5183
|
+
console.error(chalk32.red(`
|
|
5065
5184
|
\u274C Invalid environment: "${envValue}"`));
|
|
5066
|
-
console.log(
|
|
5185
|
+
console.log(chalk32.dim(` Valid values: ${VALID_ENVS.join(", ")}
|
|
5067
5186
|
`));
|
|
5068
5187
|
process.exit(1);
|
|
5069
5188
|
}
|
|
@@ -5084,8 +5203,8 @@ async function initEnvFiles(options) {
|
|
|
5084
5203
|
return acc;
|
|
5085
5204
|
}, {});
|
|
5086
5205
|
if (targetEnv) {
|
|
5087
|
-
console.log(
|
|
5088
|
-
\u{1F680} Generating .env template files for ${
|
|
5206
|
+
console.log(chalk32.blue.bold(`
|
|
5207
|
+
\u{1F680} Generating .env template files for ${chalk32.cyan(targetEnv)} environment
|
|
5089
5208
|
`));
|
|
5090
5209
|
const envSpecificFiles = {};
|
|
5091
5210
|
const committedVars = allVars.filter(([_, schema]) => !schema.sensitive);
|
|
@@ -5101,24 +5220,24 @@ async function initEnvFiles(options) {
|
|
|
5101
5220
|
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
5102
5221
|
}
|
|
5103
5222
|
} else {
|
|
5104
|
-
console.log(
|
|
5223
|
+
console.log(chalk32.blue.bold(`
|
|
5105
5224
|
\u{1F680} Generating .env template files
|
|
5106
5225
|
`));
|
|
5107
5226
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
5108
5227
|
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
5109
5228
|
}
|
|
5110
5229
|
}
|
|
5111
|
-
console.log(
|
|
5112
|
-
console.log(
|
|
5113
|
-
console.log(
|
|
5114
|
-
console.log(
|
|
5230
|
+
console.log(chalk32.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
|
|
5231
|
+
console.log(chalk32.dim(" cp .env.example .env"));
|
|
5232
|
+
console.log(chalk32.dim(" cp .env.local.example .env.local"));
|
|
5233
|
+
console.log(chalk32.dim(" cp .env.server.example .env.server"));
|
|
5115
5234
|
if (targetEnv) {
|
|
5116
|
-
console.log(
|
|
5117
|
-
console.log(
|
|
5235
|
+
console.log(chalk32.dim(` cp .env.${targetEnv}.example .env.${targetEnv}`));
|
|
5236
|
+
console.log(chalk32.dim(` cp .env.${targetEnv}.local.example .env.${targetEnv}.local`));
|
|
5118
5237
|
}
|
|
5119
5238
|
console.log("");
|
|
5120
5239
|
} catch (error) {
|
|
5121
|
-
console.error(
|
|
5240
|
+
console.error(chalk32.red(`
|
|
5122
5241
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
5123
5242
|
`));
|
|
5124
5243
|
process.exit(1);
|
|
@@ -5127,11 +5246,11 @@ async function initEnvFiles(options) {
|
|
|
5127
5246
|
function writeEnvTemplate(cwd, file, vars, force) {
|
|
5128
5247
|
const filePath = resolve2(cwd, file);
|
|
5129
5248
|
if (existsSync28(filePath) && !force) {
|
|
5130
|
-
console.log(
|
|
5249
|
+
console.log(chalk32.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
|
|
5131
5250
|
return;
|
|
5132
5251
|
}
|
|
5133
5252
|
writeFileSync17(filePath, generateEnvFileContent(vars), "utf-8");
|
|
5134
|
-
console.log(
|
|
5253
|
+
console.log(chalk32.green(` \u2705 ${file} (${vars.length} variables)`));
|
|
5135
5254
|
}
|
|
5136
5255
|
function generateEnvFileContent(vars) {
|
|
5137
5256
|
const lines = [
|
|
@@ -5166,7 +5285,7 @@ async function checkEnvFiles(options) {
|
|
|
5166
5285
|
const envSchema = await loadEnvSchema(packageName);
|
|
5167
5286
|
const allVars = Object.entries(envSchema);
|
|
5168
5287
|
const envLabel = targetEnv ? ` (${targetEnv})` : "";
|
|
5169
|
-
console.log(
|
|
5288
|
+
console.log(chalk32.blue.bold(`
|
|
5170
5289
|
\u{1F50D} Checking .env files against schema${envLabel}
|
|
5171
5290
|
`));
|
|
5172
5291
|
const filesToCheck = targetEnv ? getEnvFilesForEnvironment(targetEnv) : [...BASE_ENV_FILES.nextjs, ...BASE_ENV_FILES.server];
|
|
@@ -5183,7 +5302,7 @@ async function checkEnvFiles(options) {
|
|
|
5183
5302
|
for (const [key, value] of Object.entries(parsed)) {
|
|
5184
5303
|
loadedEnv[key] = { value: value || "", file };
|
|
5185
5304
|
}
|
|
5186
|
-
console.log(
|
|
5305
|
+
console.log(chalk32.dim(` \u{1F4C4} ${file} loaded`));
|
|
5187
5306
|
}
|
|
5188
5307
|
console.log("");
|
|
5189
5308
|
for (const [key, schema] of allVars) {
|
|
@@ -5191,7 +5310,7 @@ async function checkEnvFiles(options) {
|
|
|
5191
5310
|
const found = loadedEnv[key];
|
|
5192
5311
|
if (!found) {
|
|
5193
5312
|
if (schema.required && schema.default === void 0) {
|
|
5194
|
-
issues.push(`${
|
|
5313
|
+
issues.push(`${chalk32.red("\u2717")} ${chalk32.cyan(key)} is required but not found in any .env file`);
|
|
5195
5314
|
}
|
|
5196
5315
|
continue;
|
|
5197
5316
|
}
|
|
@@ -5201,11 +5320,11 @@ async function checkEnvFiles(options) {
|
|
|
5201
5320
|
if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
|
|
5202
5321
|
if (schema.sensitive) {
|
|
5203
5322
|
issues.push(
|
|
5204
|
-
`${
|
|
5323
|
+
`${chalk32.red("\u2717")} ${chalk32.cyan(key)} is sensitive and should be in ${chalk32.magenta(expectedFile)}, but found in ${chalk32.yellow(found.file)} (security risk!)`
|
|
5205
5324
|
);
|
|
5206
5325
|
} else {
|
|
5207
5326
|
warnings.push(
|
|
5208
|
-
`${
|
|
5327
|
+
`${chalk32.yellow("\u26A0")} ${chalk32.cyan(key)} should be in ${chalk32.magenta(expectedFile)}, but found in ${chalk32.dim(found.file)}`
|
|
5209
5328
|
);
|
|
5210
5329
|
}
|
|
5211
5330
|
}
|
|
@@ -5213,34 +5332,34 @@ async function checkEnvFiles(options) {
|
|
|
5213
5332
|
for (const [key, { file }] of Object.entries(loadedEnv)) {
|
|
5214
5333
|
const inSchema = allVars.some(([k]) => k === key);
|
|
5215
5334
|
if (!inSchema) {
|
|
5216
|
-
warnings.push(`${
|
|
5335
|
+
warnings.push(`${chalk32.yellow("\u26A0")} ${chalk32.cyan(key)} in ${chalk32.dim(file)} is not in schema`);
|
|
5217
5336
|
}
|
|
5218
5337
|
}
|
|
5219
5338
|
if (issues.length > 0) {
|
|
5220
|
-
console.log(
|
|
5339
|
+
console.log(chalk32.red.bold("Issues:"));
|
|
5221
5340
|
for (const issue of issues) {
|
|
5222
5341
|
console.log(` ${issue}`);
|
|
5223
5342
|
}
|
|
5224
5343
|
console.log("");
|
|
5225
5344
|
}
|
|
5226
5345
|
if (warnings.length > 0) {
|
|
5227
|
-
console.log(
|
|
5346
|
+
console.log(chalk32.yellow.bold("Warnings:"));
|
|
5228
5347
|
for (const warning of warnings) {
|
|
5229
5348
|
console.log(` ${warning}`);
|
|
5230
5349
|
}
|
|
5231
5350
|
console.log("");
|
|
5232
5351
|
}
|
|
5233
5352
|
if (issues.length === 0 && warnings.length === 0) {
|
|
5234
|
-
console.log(
|
|
5353
|
+
console.log(chalk32.green("\u2705 All environment variables are correctly configured!\n"));
|
|
5235
5354
|
} else {
|
|
5236
|
-
console.log(
|
|
5355
|
+
console.log(chalk32.dim(`Found ${issues.length} issue(s) and ${warnings.length} warning(s)
|
|
5237
5356
|
`));
|
|
5238
5357
|
if (issues.length > 0) {
|
|
5239
5358
|
process.exit(1);
|
|
5240
5359
|
}
|
|
5241
5360
|
}
|
|
5242
5361
|
} catch (error) {
|
|
5243
|
-
console.error(
|
|
5362
|
+
console.error(chalk32.red(`
|
|
5244
5363
|
\u274C ${error instanceof Error ? error.message : "Unknown error"}
|
|
5245
5364
|
`));
|
|
5246
5365
|
process.exit(1);
|
|
@@ -5254,15 +5373,15 @@ async function validateEnvVars(options) {
|
|
|
5254
5373
|
if (targetEnv) {
|
|
5255
5374
|
const { loadEnv: loadEnv10 } = await import("@spfn/core/env/loader");
|
|
5256
5375
|
const result = loadEnv10({ nodeEnv: targetEnv });
|
|
5257
|
-
console.log(
|
|
5258
|
-
\u{1F50D} Validating environment variables for ${
|
|
5376
|
+
console.log(chalk32.blue.bold(`
|
|
5377
|
+
\u{1F50D} Validating environment variables for ${chalk32.cyan(targetEnv)}
|
|
5259
5378
|
`));
|
|
5260
5379
|
if (result.loadedFiles.length > 0) {
|
|
5261
|
-
console.log(
|
|
5380
|
+
console.log(chalk32.dim(` Loaded: ${result.loadedFiles.join(", ")}`));
|
|
5262
5381
|
}
|
|
5263
5382
|
console.log("");
|
|
5264
5383
|
} else {
|
|
5265
|
-
console.log(
|
|
5384
|
+
console.log(chalk32.blue.bold(`
|
|
5266
5385
|
\u{1F50D} Validating environment variables
|
|
5267
5386
|
`));
|
|
5268
5387
|
}
|
|
@@ -5270,7 +5389,7 @@ async function validateEnvVars(options) {
|
|
|
5270
5389
|
const allWarnings = [];
|
|
5271
5390
|
for (const packageName of packages) {
|
|
5272
5391
|
try {
|
|
5273
|
-
console.log(
|
|
5392
|
+
console.log(chalk32.dim(` \u{1F4E6} ${packageName}`));
|
|
5274
5393
|
const envSchema = await loadEnvSchema(packageName);
|
|
5275
5394
|
const { createEnvRegistry } = await import("@spfn/core/env");
|
|
5276
5395
|
const registry = createEnvRegistry(envSchema);
|
|
@@ -5283,10 +5402,10 @@ async function validateEnvVars(options) {
|
|
|
5283
5402
|
}
|
|
5284
5403
|
} catch (error) {
|
|
5285
5404
|
if (error instanceof Error && error.message.includes("does not export envSchema")) {
|
|
5286
|
-
console.log(
|
|
5405
|
+
console.log(chalk32.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
|
|
5287
5406
|
continue;
|
|
5288
5407
|
}
|
|
5289
|
-
console.error(
|
|
5408
|
+
console.error(chalk32.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
|
|
5290
5409
|
if (options.strict) {
|
|
5291
5410
|
process.exit(1);
|
|
5292
5411
|
}
|
|
@@ -5294,32 +5413,32 @@ async function validateEnvVars(options) {
|
|
|
5294
5413
|
}
|
|
5295
5414
|
console.log("");
|
|
5296
5415
|
if (allErrors.length > 0) {
|
|
5297
|
-
console.log(
|
|
5416
|
+
console.log(chalk32.red.bold(`\u274C Validation Errors (${allErrors.length}):
|
|
5298
5417
|
`));
|
|
5299
5418
|
for (const error of allErrors) {
|
|
5300
|
-
console.log(` ${
|
|
5301
|
-
console.log(` ${
|
|
5302
|
-
console.log(` ${
|
|
5419
|
+
console.log(` ${chalk32.red("\u2717")} ${chalk32.cyan(error.key)}`);
|
|
5420
|
+
console.log(` ${chalk32.dim(error.message)}`);
|
|
5421
|
+
console.log(` ${chalk32.dim(`from ${error.package}`)}`);
|
|
5303
5422
|
console.log("");
|
|
5304
5423
|
}
|
|
5305
5424
|
}
|
|
5306
5425
|
if (allWarnings.length > 0) {
|
|
5307
|
-
console.log(
|
|
5426
|
+
console.log(chalk32.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
|
|
5308
5427
|
`));
|
|
5309
5428
|
for (const warning of allWarnings) {
|
|
5310
|
-
console.log(` ${
|
|
5311
|
-
console.log(` ${
|
|
5429
|
+
console.log(` ${chalk32.yellow("\u26A0")} ${chalk32.cyan(warning.key)}`);
|
|
5430
|
+
console.log(` ${chalk32.dim(warning.message)}`);
|
|
5312
5431
|
console.log("");
|
|
5313
5432
|
}
|
|
5314
5433
|
}
|
|
5315
5434
|
if (allErrors.length === 0 && allWarnings.length === 0) {
|
|
5316
|
-
console.log(
|
|
5435
|
+
console.log(chalk32.green.bold("\u2705 All environment variables are valid!\n"));
|
|
5317
5436
|
} else if (allErrors.length === 0) {
|
|
5318
|
-
console.log(
|
|
5319
|
-
console.log(
|
|
5437
|
+
console.log(chalk32.green("\u2705 No errors found."));
|
|
5438
|
+
console.log(chalk32.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
|
|
5320
5439
|
`));
|
|
5321
5440
|
} else {
|
|
5322
|
-
console.log(
|
|
5441
|
+
console.log(chalk32.red(`
|
|
5323
5442
|
\u274C Validation failed with ${allErrors.length} error(s)
|
|
5324
5443
|
`));
|
|
5325
5444
|
process.exit(1);
|
|
@@ -5328,12 +5447,12 @@ async function validateEnvVars(options) {
|
|
|
5328
5447
|
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);
|
|
5329
5448
|
|
|
5330
5449
|
// src/commands/secret/index.ts
|
|
5331
|
-
import { Command as
|
|
5450
|
+
import { Command as Command14 } from "commander";
|
|
5332
5451
|
|
|
5333
5452
|
// src/commands/secret/set.ts
|
|
5334
5453
|
init_logger();
|
|
5335
5454
|
import prompts9 from "prompts";
|
|
5336
|
-
import
|
|
5455
|
+
import chalk33 from "chalk";
|
|
5337
5456
|
|
|
5338
5457
|
// src/commands/secret/options.ts
|
|
5339
5458
|
init_logger();
|
|
@@ -5348,7 +5467,7 @@ function resolveEnv(env9) {
|
|
|
5348
5467
|
|
|
5349
5468
|
// src/commands/secret/store-value.ts
|
|
5350
5469
|
init_logger();
|
|
5351
|
-
import { join as
|
|
5470
|
+
import { join as join28 } from "path";
|
|
5352
5471
|
init_env_file();
|
|
5353
5472
|
|
|
5354
5473
|
// src/utils/sops.ts
|
|
@@ -5397,19 +5516,19 @@ async function sopsUpdateKeys(absFile) {
|
|
|
5397
5516
|
|
|
5398
5517
|
// src/utils/secret-config.ts
|
|
5399
5518
|
import { existsSync as existsSync30, readdirSync as readdirSync3 } from "fs";
|
|
5400
|
-
import { join as
|
|
5519
|
+
import { join as join27 } from "path";
|
|
5401
5520
|
var SECRETS_DIR = "secrets";
|
|
5402
5521
|
function getSopsFile(cwd, env9) {
|
|
5403
5522
|
const relFile = `${SECRETS_DIR}/${env9}.enc.json`;
|
|
5404
|
-
return { absFile:
|
|
5523
|
+
return { absFile: join27(cwd, relFile), relFile };
|
|
5405
5524
|
}
|
|
5406
5525
|
function findUp(cwd, filename, maxDepth = 6) {
|
|
5407
5526
|
let dir = cwd;
|
|
5408
5527
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
5409
|
-
if (existsSync30(
|
|
5528
|
+
if (existsSync30(join27(dir, filename))) {
|
|
5410
5529
|
return dir;
|
|
5411
5530
|
}
|
|
5412
|
-
const parent =
|
|
5531
|
+
const parent = join27(dir, "..");
|
|
5413
5532
|
if (parent === dir) {
|
|
5414
5533
|
break;
|
|
5415
5534
|
}
|
|
@@ -5419,17 +5538,17 @@ function findUp(cwd, filename, maxDepth = 6) {
|
|
|
5419
5538
|
}
|
|
5420
5539
|
function findSopsConfig(cwd) {
|
|
5421
5540
|
const dir = findUp(cwd, ".sops.yaml");
|
|
5422
|
-
return dir ?
|
|
5541
|
+
return dir ? join27(dir, ".sops.yaml") : null;
|
|
5423
5542
|
}
|
|
5424
5543
|
function hasSopsConfig(cwd) {
|
|
5425
5544
|
return findSopsConfig(cwd) !== null;
|
|
5426
5545
|
}
|
|
5427
5546
|
function listSopsFiles(cwd) {
|
|
5428
|
-
const dir =
|
|
5547
|
+
const dir = join27(cwd, SECRETS_DIR);
|
|
5429
5548
|
if (!existsSync30(dir)) {
|
|
5430
5549
|
return [];
|
|
5431
5550
|
}
|
|
5432
|
-
return readdirSync3(dir).filter((name) => name.endsWith(".enc.json")).map((name) =>
|
|
5551
|
+
return readdirSync3(dir).filter((name) => name.endsWith(".enc.json")).map((name) => join27(dir, name));
|
|
5433
5552
|
}
|
|
5434
5553
|
|
|
5435
5554
|
// src/commands/secret/store-value.ts
|
|
@@ -5458,7 +5577,7 @@ async function storeSecret(cwd, env9, key, value) {
|
|
|
5458
5577
|
);
|
|
5459
5578
|
}
|
|
5460
5579
|
await store.set(keychainName(key), value);
|
|
5461
|
-
const serverEnvPath =
|
|
5580
|
+
const serverEnvPath = join28(cwd, ".env.server");
|
|
5462
5581
|
const result = upsertEnvVar(serverEnvPath, key, keychainRef(key));
|
|
5463
5582
|
restrictEnvFilePerms(serverEnvPath);
|
|
5464
5583
|
ensureGitignored(cwd, [{ pattern: ".env.server", comment: "spfn server env (secrets)" }]);
|
|
@@ -5489,7 +5608,7 @@ async function secretSet(key, options) {
|
|
|
5489
5608
|
const { value } = await prompts9({
|
|
5490
5609
|
type: "password",
|
|
5491
5610
|
name: "value",
|
|
5492
|
-
message: `Value for ${
|
|
5611
|
+
message: `Value for ${chalk33.cyan(resolvedKey)} (${env9})`
|
|
5493
5612
|
});
|
|
5494
5613
|
if (!value) {
|
|
5495
5614
|
logger.warn("Cancelled \u2014 no value entered.");
|
|
@@ -5538,7 +5657,7 @@ async function warnIfNotSecret(pkg, key) {
|
|
|
5538
5657
|
|
|
5539
5658
|
// src/commands/secret/list.ts
|
|
5540
5659
|
init_logger();
|
|
5541
|
-
import
|
|
5660
|
+
import chalk34 from "chalk";
|
|
5542
5661
|
async function secretList(options) {
|
|
5543
5662
|
const env9 = resolveEnv(options.env);
|
|
5544
5663
|
const pkg = options.package ?? "@spfn/core";
|
|
@@ -5554,12 +5673,12 @@ async function secretList(options) {
|
|
|
5554
5673
|
return;
|
|
5555
5674
|
}
|
|
5556
5675
|
const present = await loadPresence(env9, entries);
|
|
5557
|
-
console.log(
|
|
5676
|
+
console.log(chalk34.blue.bold(`
|
|
5558
5677
|
\u{1F511} Secrets (${pkg}) \u2014 ${env9}
|
|
5559
5678
|
`));
|
|
5560
5679
|
for (const entry of entries) {
|
|
5561
5680
|
const status = statusOf(entry, present.has(entry.key));
|
|
5562
|
-
console.log(` ${badge(status)} ${
|
|
5681
|
+
console.log(` ${badge(status)} ${chalk34.cyan(entry.key)}${entry.generate ? chalk34.dim(" (generatable)") : ""}`);
|
|
5563
5682
|
}
|
|
5564
5683
|
console.log();
|
|
5565
5684
|
}
|
|
@@ -5592,17 +5711,17 @@ function statusOf(entry, present) {
|
|
|
5592
5711
|
function badge(status) {
|
|
5593
5712
|
switch (status) {
|
|
5594
5713
|
case "set":
|
|
5595
|
-
return
|
|
5714
|
+
return chalk34.green("\u25CF");
|
|
5596
5715
|
case "missing":
|
|
5597
|
-
return
|
|
5716
|
+
return chalk34.yellow("\u25CB");
|
|
5598
5717
|
case "awaiting-input":
|
|
5599
|
-
return
|
|
5718
|
+
return chalk34.red("\u25CB");
|
|
5600
5719
|
}
|
|
5601
5720
|
}
|
|
5602
5721
|
|
|
5603
5722
|
// src/commands/secret/generate.ts
|
|
5604
5723
|
init_logger();
|
|
5605
|
-
import
|
|
5724
|
+
import chalk35 from "chalk";
|
|
5606
5725
|
async function secretGenerate(key, options) {
|
|
5607
5726
|
const env9 = resolveEnv(options.env);
|
|
5608
5727
|
const pkg = options.package ?? "@spfn/core";
|
|
@@ -5640,7 +5759,7 @@ function requireGeneratable(schema, key) {
|
|
|
5640
5759
|
process.exit(1);
|
|
5641
5760
|
}
|
|
5642
5761
|
if (!entry.generate) {
|
|
5643
|
-
logger.error(`${
|
|
5762
|
+
logger.error(`${chalk35.cyan(key)} has no generate strategy \u2014 it's an external value. Use \`spfn secret set ${key}\`.`);
|
|
5644
5763
|
process.exit(1);
|
|
5645
5764
|
}
|
|
5646
5765
|
return entry;
|
|
@@ -5648,7 +5767,7 @@ function requireGeneratable(schema, key) {
|
|
|
5648
5767
|
|
|
5649
5768
|
// src/commands/secret/rotate.ts
|
|
5650
5769
|
init_logger();
|
|
5651
|
-
import
|
|
5770
|
+
import chalk36 from "chalk";
|
|
5652
5771
|
async function secretRotate(key, options) {
|
|
5653
5772
|
const env9 = resolveEnv(options.env);
|
|
5654
5773
|
const pkg = options.package ?? "@spfn/core";
|
|
@@ -5674,7 +5793,7 @@ async function secretRotate(key, options) {
|
|
|
5674
5793
|
}
|
|
5675
5794
|
}
|
|
5676
5795
|
for (const entry of external) {
|
|
5677
|
-
logger.warn(`${
|
|
5796
|
+
logger.warn(`${chalk36.cyan(entry.key)} is external \u2014 reissue it at the provider, then \`spfn secret set ${entry.key} --env ${env9}\`.`);
|
|
5678
5797
|
}
|
|
5679
5798
|
if (generatable.length > 0) {
|
|
5680
5799
|
logger.info("Rotation updates the source only \u2014 commit and deploy to apply.");
|
|
@@ -5698,10 +5817,10 @@ init_logger();
|
|
|
5698
5817
|
import { execa as execa12 } from "execa";
|
|
5699
5818
|
import { existsSync as existsSync31, mkdirSync as mkdirSync6 } from "fs";
|
|
5700
5819
|
import { homedir } from "os";
|
|
5701
|
-
import { dirname as dirname5, join as
|
|
5702
|
-
import
|
|
5820
|
+
import { dirname as dirname5, join as join29 } from "path";
|
|
5821
|
+
import chalk37 from "chalk";
|
|
5703
5822
|
function ageKeyFile() {
|
|
5704
|
-
return process.env.SOPS_AGE_KEY_FILE ??
|
|
5823
|
+
return process.env.SOPS_AGE_KEY_FILE ?? join29(homedir(), ".config", "sops", "age", "keys.txt");
|
|
5705
5824
|
}
|
|
5706
5825
|
async function ensureAgeInstalled() {
|
|
5707
5826
|
try {
|
|
@@ -5737,27 +5856,27 @@ function printPublicKeys(keys) {
|
|
|
5737
5856
|
logger.warn("Could not read the public key \u2014 run `age-keygen -y <file>` manually.");
|
|
5738
5857
|
return;
|
|
5739
5858
|
}
|
|
5740
|
-
console.log(
|
|
5859
|
+
console.log(chalk37.bold("\nPublic key(s):"));
|
|
5741
5860
|
for (const key of keys) {
|
|
5742
|
-
console.log(` ${
|
|
5861
|
+
console.log(` ${chalk37.cyan(key)}`);
|
|
5743
5862
|
}
|
|
5744
|
-
console.log(
|
|
5745
|
-
console.log(
|
|
5863
|
+
console.log(chalk37.dim("\nRegister it as a recipient:"));
|
|
5864
|
+
console.log(chalk37.dim(` spfn secret recipients add ${keys[0]}
|
|
5746
5865
|
`));
|
|
5747
5866
|
}
|
|
5748
5867
|
|
|
5749
5868
|
// src/commands/secret/recipients.ts
|
|
5750
5869
|
init_logger();
|
|
5751
5870
|
import { existsSync as existsSync32, readFileSync as readFileSync13, writeFileSync as writeFileSync19 } from "fs";
|
|
5752
|
-
import { join as
|
|
5871
|
+
import { join as join30 } from "path";
|
|
5753
5872
|
import { parse as parse3, stringify } from "yaml";
|
|
5754
|
-
import
|
|
5873
|
+
import chalk38 from "chalk";
|
|
5755
5874
|
var SOPS_CONFIG = ".sops.yaml";
|
|
5756
5875
|
var DEFAULT_PATH_REGEX = "secrets/.*\\.enc\\.json$";
|
|
5757
5876
|
var AGE_RECIPIENT = /^age1[0-9a-z]+$/;
|
|
5758
5877
|
async function secretRecipients(action, key, _options) {
|
|
5759
5878
|
const cwd = process.cwd();
|
|
5760
|
-
const configPath =
|
|
5879
|
+
const configPath = join30(cwd, SOPS_CONFIG);
|
|
5761
5880
|
if (action === "list") {
|
|
5762
5881
|
listRecipients(configPath);
|
|
5763
5882
|
return;
|
|
@@ -5795,13 +5914,13 @@ function listRecipients(configPath) {
|
|
|
5795
5914
|
}
|
|
5796
5915
|
const config = loadConfig(configPath);
|
|
5797
5916
|
const rules = config.creation_rules ?? [];
|
|
5798
|
-
console.log(
|
|
5917
|
+
console.log(chalk38.blue.bold(`
|
|
5799
5918
|
\u{1F465} Recipients (${SOPS_CONFIG})
|
|
5800
5919
|
`));
|
|
5801
5920
|
for (const rule of rules) {
|
|
5802
|
-
console.log(
|
|
5921
|
+
console.log(chalk38.dim(` ${rule.path_regex ?? "(any path)"}`));
|
|
5803
5922
|
for (const recipient of parseRecipients(rule.age)) {
|
|
5804
|
-
console.log(` ${
|
|
5923
|
+
console.log(` ${chalk38.cyan(recipient)}`);
|
|
5805
5924
|
}
|
|
5806
5925
|
}
|
|
5807
5926
|
console.log();
|
|
@@ -5842,8 +5961,8 @@ async function reencrypt(cwd) {
|
|
|
5842
5961
|
|
|
5843
5962
|
// src/commands/secret/check.ts
|
|
5844
5963
|
init_logger();
|
|
5845
|
-
import { join as
|
|
5846
|
-
import
|
|
5964
|
+
import { join as join31 } from "path";
|
|
5965
|
+
import chalk39 from "chalk";
|
|
5847
5966
|
init_env_file();
|
|
5848
5967
|
var COMMITTED_FILES = [".env", ".env.example"];
|
|
5849
5968
|
var PLACEHOLDER = /(your-|changeme|placeholder|example|<.*>)/i;
|
|
@@ -5861,35 +5980,35 @@ async function secretCheck(options) {
|
|
|
5861
5980
|
const issues = [];
|
|
5862
5981
|
const warnings = [];
|
|
5863
5982
|
for (const file of COMMITTED_FILES) {
|
|
5864
|
-
const parsed = parseEnvFile(
|
|
5983
|
+
const parsed = parseEnvFile(join31(cwd, file));
|
|
5865
5984
|
for (const [key, value] of Object.entries(parsed)) {
|
|
5866
5985
|
if (secretKeys.has(key) && value.length > 0 && !PLACEHOLDER.test(value)) {
|
|
5867
|
-
issues.push(`${
|
|
5986
|
+
issues.push(`${chalk39.cyan(key)} has a real value in committed ${chalk39.yellow(file)} \u2014 move it to the keychain/SOPS.`);
|
|
5868
5987
|
}
|
|
5869
5988
|
}
|
|
5870
5989
|
}
|
|
5871
|
-
const serverEnv = parseEnvFile(
|
|
5990
|
+
const serverEnv = parseEnvFile(join31(cwd, ".env.server"));
|
|
5872
5991
|
for (const key of secretKeys) {
|
|
5873
5992
|
const value = serverEnv[key];
|
|
5874
5993
|
if (value && !value.startsWith(KEYCHAIN_REF_PREFIX)) {
|
|
5875
|
-
warnings.push(`${
|
|
5994
|
+
warnings.push(`${chalk39.cyan(key)} is plaintext in .env.server \u2014 run \`spfn secret set ${key}\` to move it to the keychain.`);
|
|
5876
5995
|
}
|
|
5877
5996
|
}
|
|
5878
5997
|
report(issues, warnings, cwd);
|
|
5879
5998
|
}
|
|
5880
5999
|
function report(issues, warnings, cwd) {
|
|
5881
|
-
console.log(
|
|
6000
|
+
console.log(chalk39.blue.bold("\n\u{1F50D} Secret hygiene check\n"));
|
|
5882
6001
|
for (const issue of issues) {
|
|
5883
|
-
console.log(` ${
|
|
6002
|
+
console.log(` ${chalk39.red("\u2717")} ${issue}`);
|
|
5884
6003
|
}
|
|
5885
6004
|
for (const warning of warnings) {
|
|
5886
|
-
console.log(` ${
|
|
6005
|
+
console.log(` ${chalk39.yellow("\u26A0")} ${warning}`);
|
|
5887
6006
|
}
|
|
5888
6007
|
if (!hasSopsConfig(cwd)) {
|
|
5889
|
-
console.log(` ${
|
|
6008
|
+
console.log(` ${chalk39.dim("\u2139 no .sops.yaml \u2014 prod secrets need a backend (spfn secret keygen, or add a KMS rule)")}`);
|
|
5890
6009
|
}
|
|
5891
6010
|
if (issues.length === 0 && warnings.length === 0) {
|
|
5892
|
-
console.log(
|
|
6011
|
+
console.log(chalk39.green(" \u2713 No plaintext secret leaks found."));
|
|
5893
6012
|
}
|
|
5894
6013
|
console.log();
|
|
5895
6014
|
if (issues.length > 0) {
|
|
@@ -5900,7 +6019,7 @@ function report(issues, warnings, cwd) {
|
|
|
5900
6019
|
// src/commands/secret/index.ts
|
|
5901
6020
|
var ENV_OPTION = ["-e, --env <env>", "Target environment (local | development | staging | production)", "local"];
|
|
5902
6021
|
var PKG_OPTION = ["-p, --package <package>", "Package whose env schema to read", "@spfn/core"];
|
|
5903
|
-
var secretCommand = new
|
|
6022
|
+
var secretCommand = new Command14("secret").description("Manage secrets: keychain locally, SOPS (age / GCP KMS / AWS KMS) for deployed environments");
|
|
5904
6023
|
secretCommand.command("set [key]").description("Store a secret value (prompts for the value, masked)").option(...ENV_OPTION).option(...PKG_OPTION).action(secretSet);
|
|
5905
6024
|
secretCommand.command("list").description("List declared secrets and their status (never prints values)").option(...ENV_OPTION).option(...PKG_OPTION).action(secretList);
|
|
5906
6025
|
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);
|
|
@@ -5911,7 +6030,7 @@ secretCommand.command("check").description("Static hygiene lint \u2014 flag plai
|
|
|
5911
6030
|
|
|
5912
6031
|
// src/index.ts
|
|
5913
6032
|
init_version();
|
|
5914
|
-
var program = new
|
|
6033
|
+
var program = new Command15();
|
|
5915
6034
|
program.name("spfn").description("SPFN CLI - The Missing Backend for Next.js").version(getCliVersion());
|
|
5916
6035
|
program.addCommand(createCommand);
|
|
5917
6036
|
program.addCommand(initCommand);
|
|
@@ -5921,6 +6040,7 @@ program.addCommand(buildCommand);
|
|
|
5921
6040
|
program.addCommand(startCommand);
|
|
5922
6041
|
program.addCommand(provisionCommand);
|
|
5923
6042
|
program.addCommand(codegenCommand);
|
|
6043
|
+
program.addCommand(contractCommand);
|
|
5924
6044
|
program.addCommand(keyCommand);
|
|
5925
6045
|
program.addCommand(setupCommand);
|
|
5926
6046
|
program.addCommand(dbCommand);
|