spfn 0.3.0-beta.11 → 0.3.0-beta.13
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 +102 -9
- package/dist/index.js +1360 -523
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -606,9 +606,7 @@ async function setupApiProxy(cwd, includeAuth) {
|
|
|
606
606
|
}
|
|
607
607
|
ensureDirSync3(rpcDir);
|
|
608
608
|
const authImports = includeAuth ? `import '@spfn/auth/nextjs/api';
|
|
609
|
-
import { authRouteMap } from '@spfn/auth';
|
|
610
609
|
` : "";
|
|
611
|
-
const proxyRouteMap = includeAuth ? "{ ...routeMap, ...authRouteMap }" : "routeMap";
|
|
612
610
|
const routeContent = `/**
|
|
613
611
|
* SPFN RPC Proxy
|
|
614
612
|
*
|
|
@@ -625,7 +623,7 @@ import { authRouteMap } from '@spfn/auth';
|
|
|
625
623
|
${authImports}import { routeMap } from '@/generated/route-map';
|
|
626
624
|
import { createRpcProxy } from '@spfn/core/nextjs/server';
|
|
627
625
|
|
|
628
|
-
export const { GET, POST } = createRpcProxy({ routeMap
|
|
626
|
+
export const { GET, POST } = createRpcProxy({ routeMap });
|
|
629
627
|
`;
|
|
630
628
|
writeFileSync2(rpcRoutePath, routeContent);
|
|
631
629
|
const relativePath = rpcRoutePath.replace(cwd + "/", "");
|
|
@@ -951,7 +949,7 @@ import { dirname as dirname2, join as join10 } from "path";
|
|
|
951
949
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
952
950
|
function getCliVersion() {
|
|
953
951
|
if (true) {
|
|
954
|
-
return "0.3.0-beta.
|
|
952
|
+
return "0.3.0-beta.13";
|
|
955
953
|
}
|
|
956
954
|
return fromPackageJson ?? (fromPackageJson = readPackageVersion());
|
|
957
955
|
}
|
|
@@ -2383,7 +2381,7 @@ function writeAgentInstructions(cwd, agentFiles) {
|
|
|
2383
2381
|
}
|
|
2384
2382
|
}
|
|
2385
2383
|
function waitForReadyFile(filePath, timeoutMs = 3e4) {
|
|
2386
|
-
return new Promise((
|
|
2384
|
+
return new Promise((resolve7, reject) => {
|
|
2387
2385
|
if (existsSync16(filePath)) {
|
|
2388
2386
|
unlinkSync(filePath);
|
|
2389
2387
|
}
|
|
@@ -2397,7 +2395,7 @@ function waitForReadyFile(filePath, timeoutMs = 3e4) {
|
|
|
2397
2395
|
if (name === fileName && existsSync16(filePath)) {
|
|
2398
2396
|
watcher.close();
|
|
2399
2397
|
clearTimeout(timer);
|
|
2400
|
-
|
|
2398
|
+
resolve7(readFileSync9(filePath, "utf-8").trim());
|
|
2401
2399
|
}
|
|
2402
2400
|
});
|
|
2403
2401
|
});
|
|
@@ -2576,7 +2574,7 @@ catch (error)
|
|
|
2576
2574
|
serverProcess2.kill("SIGTERM");
|
|
2577
2575
|
await serverProcess2.catch(() => {
|
|
2578
2576
|
});
|
|
2579
|
-
await new Promise((
|
|
2577
|
+
await new Promise((resolve7) => setTimeout(resolve7, 500));
|
|
2580
2578
|
} catch (error) {
|
|
2581
2579
|
}
|
|
2582
2580
|
}
|
|
@@ -2619,12 +2617,12 @@ catch (error)
|
|
|
2619
2617
|
process.on("SIGTERM", cleanup2);
|
|
2620
2618
|
startWatcher2();
|
|
2621
2619
|
startServer2();
|
|
2622
|
-
await new Promise((
|
|
2620
|
+
await new Promise((resolve7) => {
|
|
2623
2621
|
const keepAlive = setInterval(() => {
|
|
2624
2622
|
}, 1e6);
|
|
2625
2623
|
process.once("beforeExit", () => {
|
|
2626
2624
|
clearInterval(keepAlive);
|
|
2627
|
-
|
|
2625
|
+
resolve7();
|
|
2628
2626
|
});
|
|
2629
2627
|
});
|
|
2630
2628
|
return;
|
|
@@ -2685,7 +2683,7 @@ catch (error)
|
|
|
2685
2683
|
serverProcess.kill("SIGTERM");
|
|
2686
2684
|
await serverProcess.catch(() => {
|
|
2687
2685
|
});
|
|
2688
|
-
await new Promise((
|
|
2686
|
+
await new Promise((resolve7) => setTimeout(resolve7, 500));
|
|
2689
2687
|
} catch (error) {
|
|
2690
2688
|
}
|
|
2691
2689
|
}
|
|
@@ -2739,12 +2737,12 @@ catch (error)
|
|
|
2739
2737
|
logger.warn(`[SPFN] Server readiness check timed out, starting Next.js anyway...`);
|
|
2740
2738
|
}
|
|
2741
2739
|
startNext();
|
|
2742
|
-
await new Promise((
|
|
2740
|
+
await new Promise((resolve7) => {
|
|
2743
2741
|
const keepAlive = setInterval(() => {
|
|
2744
2742
|
}, 1e6);
|
|
2745
2743
|
process.once("beforeExit", () => {
|
|
2746
2744
|
clearInterval(keepAlive);
|
|
2747
|
-
|
|
2745
|
+
resolve7();
|
|
2748
2746
|
});
|
|
2749
2747
|
});
|
|
2750
2748
|
});
|
|
@@ -3206,9 +3204,20 @@ async function runGenerators() {
|
|
|
3206
3204
|
const orchestrator = new CodegenOrchestrator({
|
|
3207
3205
|
generators,
|
|
3208
3206
|
cwd,
|
|
3209
|
-
debug: true
|
|
3207
|
+
debug: true,
|
|
3208
|
+
// A generator that refuses — a router that will not load, a duplicate
|
|
3209
|
+
// route name — has to reach the exit code. Printing a green check over
|
|
3210
|
+
// a logged failure leaves the stale output on disk and tells the
|
|
3211
|
+
// developer who just added a route that the map now names it.
|
|
3212
|
+
throwOnError: true
|
|
3210
3213
|
});
|
|
3211
|
-
|
|
3214
|
+
try {
|
|
3215
|
+
await orchestrator.generateAll();
|
|
3216
|
+
} catch (error) {
|
|
3217
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
3218
|
+
console.log("\n" + chalk9.red.bold("\u2717 Code generation failed"));
|
|
3219
|
+
process.exit(1);
|
|
3220
|
+
}
|
|
3212
3221
|
console.log("\n" + chalk9.green.bold("\u2713 Code generation completed"));
|
|
3213
3222
|
}
|
|
3214
3223
|
var codegenCommand = new Command8("codegen").description("Code generation management");
|
|
@@ -3593,7 +3602,7 @@ async function runDrizzleCommand(command) {
|
|
|
3593
3602
|
}
|
|
3594
3603
|
const args = command.split(" ");
|
|
3595
3604
|
args.push(`--config=${configPath}`);
|
|
3596
|
-
return new Promise((
|
|
3605
|
+
return new Promise((resolve7, reject) => {
|
|
3597
3606
|
const drizzleProcess = spawn("drizzle-kit", args, {
|
|
3598
3607
|
stdio: "inherit",
|
|
3599
3608
|
// Allow interactive input
|
|
@@ -3608,7 +3617,7 @@ async function runDrizzleCommand(command) {
|
|
|
3608
3617
|
drizzleProcess.on("close", (code) => {
|
|
3609
3618
|
cleanup();
|
|
3610
3619
|
if (code === 0) {
|
|
3611
|
-
|
|
3620
|
+
resolve7();
|
|
3612
3621
|
} else {
|
|
3613
3622
|
reject(new Error(`drizzle-kit ${command} exited with code ${code}`));
|
|
3614
3623
|
}
|
|
@@ -4124,15 +4133,15 @@ async function confirmDangerousTarget(dbInfo) {
|
|
|
4124
4133
|
}
|
|
4125
4134
|
}
|
|
4126
4135
|
async function isPortAvailable(port) {
|
|
4127
|
-
return new Promise((
|
|
4136
|
+
return new Promise((resolve7) => {
|
|
4128
4137
|
const server = net.createServer();
|
|
4129
4138
|
server.once("error", () => {
|
|
4130
4139
|
server.close();
|
|
4131
|
-
|
|
4140
|
+
resolve7(false);
|
|
4132
4141
|
});
|
|
4133
4142
|
server.once("listening", () => {
|
|
4134
4143
|
server.close();
|
|
4135
|
-
|
|
4144
|
+
resolve7(true);
|
|
4136
4145
|
});
|
|
4137
4146
|
server.listen(port, "127.0.0.1");
|
|
4138
4147
|
});
|
|
@@ -4388,7 +4397,7 @@ async function dbBackup(options) {
|
|
|
4388
4397
|
pgDump.stderr?.on("data", (data) => {
|
|
4389
4398
|
errorOutput += data.toString();
|
|
4390
4399
|
});
|
|
4391
|
-
await new Promise((
|
|
4400
|
+
await new Promise((resolve7, reject) => {
|
|
4392
4401
|
pgDump.on("close", async (code) => {
|
|
4393
4402
|
if (code === 0) {
|
|
4394
4403
|
try {
|
|
@@ -4425,7 +4434,7 @@ async function dbBackup(options) {
|
|
|
4425
4434
|
tags: tags.length > 0 ? tags : void 0
|
|
4426
4435
|
};
|
|
4427
4436
|
await saveBackupMetadata(metadata, filename);
|
|
4428
|
-
|
|
4437
|
+
resolve7();
|
|
4429
4438
|
} catch (error) {
|
|
4430
4439
|
reject(error);
|
|
4431
4440
|
}
|
|
@@ -4904,7 +4913,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4904
4913
|
spinner.start();
|
|
4905
4914
|
}
|
|
4906
4915
|
});
|
|
4907
|
-
await new Promise((
|
|
4916
|
+
await new Promise((resolve7, reject) => {
|
|
4908
4917
|
restoreProcess.on("close", (code) => {
|
|
4909
4918
|
if (code === 0) {
|
|
4910
4919
|
const summary = objectCount > 0 ? ` (${objectCount} objects)` : "";
|
|
@@ -4918,7 +4927,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4918
4927
|
}
|
|
4919
4928
|
}
|
|
4920
4929
|
console.log(chalk26.green("\n\u2705 Database restored successfully"));
|
|
4921
|
-
|
|
4930
|
+
resolve7();
|
|
4922
4931
|
} else {
|
|
4923
4932
|
spinner.fail("Restore failed");
|
|
4924
4933
|
if (errors.length > 0) {
|
|
@@ -5325,25 +5334,32 @@ var addCommand = new Command12("add").description("Add a capability to this proj
|
|
|
5325
5334
|
// src/commands/env.ts
|
|
5326
5335
|
import { Command as Command13 } from "commander";
|
|
5327
5336
|
import chalk32 from "chalk";
|
|
5328
|
-
import { existsSync as
|
|
5329
|
-
import { resolve as
|
|
5337
|
+
import { existsSync as existsSync31, readFileSync as readFileSync16, writeFileSync as writeFileSync18 } from "fs";
|
|
5338
|
+
import { resolve as resolve4 } from "path";
|
|
5330
5339
|
import { parse as parse2 } from "dotenv";
|
|
5331
5340
|
|
|
5332
5341
|
// src/utils/env-schema.ts
|
|
5342
|
+
import { createRequire } from "module";
|
|
5343
|
+
import { join as join28 } from "path";
|
|
5344
|
+
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
5333
5345
|
var VALID_ENVS = ["local", "development", "staging", "production", "test"];
|
|
5334
|
-
async function loadEnvSchema(packageName) {
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
throw new Error(`Package ${packageName} does not export envSchema from config`);
|
|
5339
|
-
}
|
|
5340
|
-
return module.envSchema;
|
|
5341
|
-
} catch (error) {
|
|
5342
|
-
if (error instanceof Error && error.message.includes("does not export envSchema")) {
|
|
5343
|
-
throw error;
|
|
5344
|
-
}
|
|
5346
|
+
async function loadEnvSchema(packageName, cwd = process.cwd()) {
|
|
5347
|
+
const specifier = `${packageName}/config`;
|
|
5348
|
+
const path5 = resolveFrom(join28(cwd, "noop.js"), specifier);
|
|
5349
|
+
const module = await (path5 ? import(pathToFileURL3(path5).href) : import(specifier)).catch((error) => {
|
|
5345
5350
|
const message = error instanceof Error ? error.message : String(error);
|
|
5346
|
-
|
|
5351
|
+
return Promise.reject(new Error(`Failed to load package ${packageName}: ${message}`));
|
|
5352
|
+
});
|
|
5353
|
+
if (!module.envSchema) {
|
|
5354
|
+
throw new Error(`Package ${packageName} does not export envSchema from config`);
|
|
5355
|
+
}
|
|
5356
|
+
return module.envSchema;
|
|
5357
|
+
}
|
|
5358
|
+
function resolveFrom(parent, specifier) {
|
|
5359
|
+
try {
|
|
5360
|
+
return createRequire(parent).resolve(specifier);
|
|
5361
|
+
} catch {
|
|
5362
|
+
return void 0;
|
|
5347
5363
|
}
|
|
5348
5364
|
}
|
|
5349
5365
|
function getTargetFile(schema) {
|
|
@@ -5359,6 +5375,416 @@ function secretEntries(schema) {
|
|
|
5359
5375
|
function generatableSecrets(schema) {
|
|
5360
5376
|
return secretEntries(schema).filter((entry) => !!entry.generate);
|
|
5361
5377
|
}
|
|
5378
|
+
function layerOf(entry) {
|
|
5379
|
+
return entry.layer ?? "environment";
|
|
5380
|
+
}
|
|
5381
|
+
|
|
5382
|
+
// src/utils/env-list.ts
|
|
5383
|
+
import { existsSync as existsSync28, readFileSync as readFileSync14 } from "fs";
|
|
5384
|
+
import { join as join29, resolve as resolve2 } from "path";
|
|
5385
|
+
import { pathToFileURL as pathToFileURL4 } from "url";
|
|
5386
|
+
import { loadAppConfig as loadAppConfig3 } from "@spfn/core/app-config";
|
|
5387
|
+
var CORE_PACKAGE = "@spfn/core";
|
|
5388
|
+
var TYPESCRIPT_FILE = /\.[cm]?tsx?$/;
|
|
5389
|
+
async function loadEnvList(options, cwd = process.cwd()) {
|
|
5390
|
+
if (options.package) {
|
|
5391
|
+
return mergeEnvSources([{ name: options.package, schema: await loadEnvSchema(options.package, cwd) }]);
|
|
5392
|
+
}
|
|
5393
|
+
return mergeEnvSources([
|
|
5394
|
+
...await loadAppSchemas(cwd),
|
|
5395
|
+
...await loadPackageSchemas(cwd)
|
|
5396
|
+
]);
|
|
5397
|
+
}
|
|
5398
|
+
function mergeEnvSources(sources) {
|
|
5399
|
+
const schema = {};
|
|
5400
|
+
const declaredBy = {};
|
|
5401
|
+
const conflicts = [];
|
|
5402
|
+
for (const source of sources) {
|
|
5403
|
+
for (const [key, entry] of Object.entries(source.schema)) {
|
|
5404
|
+
const existing = schema[key];
|
|
5405
|
+
const owners = declaredBy[key] ?? [];
|
|
5406
|
+
declaredBy[key] = [...owners, source.name];
|
|
5407
|
+
if (!existing) {
|
|
5408
|
+
schema[key] = { ...entry, key };
|
|
5409
|
+
continue;
|
|
5410
|
+
}
|
|
5411
|
+
if (describeRules(existing) !== describeRules(entry)) {
|
|
5412
|
+
conflicts.push(`${key}: ${owners.join(", ")} declares ${describeRules(existing)}, ${source.name} declares ${describeRules(entry)}`);
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
5415
|
+
}
|
|
5416
|
+
if (conflicts.length > 0) {
|
|
5417
|
+
throw new Error(`Env schemas declare a variable differently:
|
|
5418
|
+
- ${conflicts.join("\n - ")}`);
|
|
5419
|
+
}
|
|
5420
|
+
return { sources, schema, declaredBy };
|
|
5421
|
+
}
|
|
5422
|
+
function groupBySource(list, entries = Object.values(list.schema)) {
|
|
5423
|
+
return list.sources.map((source) => ({
|
|
5424
|
+
source: source.name,
|
|
5425
|
+
entries: entries.filter((entry) => list.declaredBy[entry.key]?.[0] === source.name)
|
|
5426
|
+
})).filter((group) => group.entries.length > 0);
|
|
5427
|
+
}
|
|
5428
|
+
function alsoDeclaredBy(list, key) {
|
|
5429
|
+
return (list.declaredBy[key] ?? []).slice(1);
|
|
5430
|
+
}
|
|
5431
|
+
function describeEnvList(options) {
|
|
5432
|
+
return options.package ?? "whole app";
|
|
5433
|
+
}
|
|
5434
|
+
function describeRules(entry) {
|
|
5435
|
+
return `"${entry.type}" (${entry.required ? "required" : "optional"}${entry.sensitive ? ", sensitive" : ""}${layerOf(entry) === "instance" ? ", instance layer" : ""})`;
|
|
5436
|
+
}
|
|
5437
|
+
async function loadAppSchemas(cwd) {
|
|
5438
|
+
const paths = (await loadAppConfig3(cwd)).env?.schemas ?? [];
|
|
5439
|
+
if (!Array.isArray(paths) || paths.some((path5) => typeof path5 !== "string")) {
|
|
5440
|
+
throw new Error("spfn.config.js: env.schemas must be an array of paths relative to the project root.");
|
|
5441
|
+
}
|
|
5442
|
+
const sources = [];
|
|
5443
|
+
for (const path5 of paths) {
|
|
5444
|
+
sources.push({ name: path5, schema: await loadSchemaModule(cwd, path5) });
|
|
5445
|
+
}
|
|
5446
|
+
return sources;
|
|
5447
|
+
}
|
|
5448
|
+
async function loadSchemaModule(cwd, path5) {
|
|
5449
|
+
const fullPath = resolve2(cwd, path5);
|
|
5450
|
+
if (!existsSync28(fullPath)) {
|
|
5451
|
+
throw new Error(`spfn.config.js env.schemas: ${path5} does not exist.`);
|
|
5452
|
+
}
|
|
5453
|
+
const module = await importAppModule(cwd, fullPath).catch((error) => {
|
|
5454
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5455
|
+
return Promise.reject(new Error(`spfn.config.js env.schemas: ${path5} could not be loaded: ${message}`));
|
|
5456
|
+
});
|
|
5457
|
+
if (!module.envSchema) {
|
|
5458
|
+
throw new Error(`spfn.config.js env.schemas: ${path5} does not export envSchema.`);
|
|
5459
|
+
}
|
|
5460
|
+
return module.envSchema;
|
|
5461
|
+
}
|
|
5462
|
+
async function importAppModule(cwd, fullPath) {
|
|
5463
|
+
const url = pathToFileURL4(fullPath).href;
|
|
5464
|
+
const tsx = await loadTsx(cwd);
|
|
5465
|
+
if (tsx) {
|
|
5466
|
+
const tsconfig = join29(cwd, "tsconfig.json");
|
|
5467
|
+
return await tsx.tsImport(url, {
|
|
5468
|
+
parentURL: pathToFileURL4(join29(cwd, "noop.js")).href,
|
|
5469
|
+
tsconfig: existsSync28(tsconfig) ? tsconfig : false
|
|
5470
|
+
});
|
|
5471
|
+
}
|
|
5472
|
+
if (TYPESCRIPT_FILE.test(fullPath)) {
|
|
5473
|
+
throw new Error("tsx is not installed in this project, and it is what loads TypeScript. Add it: pnpm add -D tsx");
|
|
5474
|
+
}
|
|
5475
|
+
return await import(url);
|
|
5476
|
+
}
|
|
5477
|
+
async function loadTsx(cwd) {
|
|
5478
|
+
const path5 = resolveFrom(join29(cwd, "noop.js"), "tsx/esm/api") ?? resolveFrom(import.meta.url, "tsx/esm/api");
|
|
5479
|
+
return path5 ? await import(pathToFileURL4(path5).href) : void 0;
|
|
5480
|
+
}
|
|
5481
|
+
async function loadPackageSchemas(cwd) {
|
|
5482
|
+
const sources = [];
|
|
5483
|
+
for (const name of readSpfnDependencies(cwd)) {
|
|
5484
|
+
const path5 = resolveFrom(join29(cwd, "noop.js"), `${name}/config`);
|
|
5485
|
+
const module = path5 ? await importPackageConfig(name, path5) : void 0;
|
|
5486
|
+
if (module?.envSchema) {
|
|
5487
|
+
sources.push({ name, schema: module.envSchema });
|
|
5488
|
+
}
|
|
5489
|
+
}
|
|
5490
|
+
return sources;
|
|
5491
|
+
}
|
|
5492
|
+
async function importPackageConfig(name, path5) {
|
|
5493
|
+
return await import(pathToFileURL4(path5).href).catch((error) => {
|
|
5494
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5495
|
+
return Promise.reject(new Error(`Failed to load package ${name}: ${message}`));
|
|
5496
|
+
});
|
|
5497
|
+
}
|
|
5498
|
+
function readSpfnDependencies(cwd) {
|
|
5499
|
+
const packageJsonPath = join29(cwd, "package.json");
|
|
5500
|
+
if (!existsSync28(packageJsonPath)) {
|
|
5501
|
+
return [];
|
|
5502
|
+
}
|
|
5503
|
+
const packageJson = JSON.parse(readFileSync14(packageJsonPath, "utf-8"));
|
|
5504
|
+
const names = Object.keys({ ...packageJson.dependencies, ...packageJson.devDependencies }).filter((name) => name.startsWith("@spfn/")).sort();
|
|
5505
|
+
return names.includes(CORE_PACKAGE) ? [CORE_PACKAGE, ...names.filter((name) => name !== CORE_PACKAGE)] : names;
|
|
5506
|
+
}
|
|
5507
|
+
|
|
5508
|
+
// src/utils/env-audit/index.ts
|
|
5509
|
+
import { existsSync as existsSync30, readFileSync as readFileSync15 } from "fs";
|
|
5510
|
+
import { join as join32, relative as relative2, resolve as resolve3 } from "path";
|
|
5511
|
+
import { loadAppConfig as loadAppConfig4 } from "@spfn/core/app-config";
|
|
5512
|
+
|
|
5513
|
+
// src/utils/env-audit/files.ts
|
|
5514
|
+
import { existsSync as existsSync29, lstatSync, readdirSync as readdirSync2 } from "fs";
|
|
5515
|
+
import { join as join30 } from "path";
|
|
5516
|
+
var DEFAULT_INCLUDE = ["src"];
|
|
5517
|
+
var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
5518
|
+
"node_modules",
|
|
5519
|
+
".git",
|
|
5520
|
+
"dist",
|
|
5521
|
+
"build",
|
|
5522
|
+
"out",
|
|
5523
|
+
"coverage",
|
|
5524
|
+
".next",
|
|
5525
|
+
".turbo",
|
|
5526
|
+
".vercel",
|
|
5527
|
+
".output",
|
|
5528
|
+
".spfn"
|
|
5529
|
+
]);
|
|
5530
|
+
var SOURCE_FILE = /\.(?:[cm]?[jt]s|[jt]sx)$/;
|
|
5531
|
+
var DECLARATION_FILE = /\.d\.[cm]?ts$/;
|
|
5532
|
+
var GLOB_CHARACTER = /[*?]/;
|
|
5533
|
+
function listAuditFiles(cwd, scope) {
|
|
5534
|
+
const include = scope.include.map(globToRegExp);
|
|
5535
|
+
const ignore = scope.ignore.map(globToRegExp);
|
|
5536
|
+
const files = /* @__PURE__ */ new Set();
|
|
5537
|
+
for (const root of new Set(scope.include.map(staticPrefix))) {
|
|
5538
|
+
walk(cwd, root, ignore, files);
|
|
5539
|
+
}
|
|
5540
|
+
return [...files].filter((file) => matchesAny(file, include)).sort();
|
|
5541
|
+
}
|
|
5542
|
+
function matchesAny(path5, globs) {
|
|
5543
|
+
const segments = path5.split("/");
|
|
5544
|
+
return segments.some((_, index) => {
|
|
5545
|
+
const prefix = segments.slice(0, index + 1).join("/");
|
|
5546
|
+
return globs.some((glob) => glob.test(prefix));
|
|
5547
|
+
});
|
|
5548
|
+
}
|
|
5549
|
+
function globToRegExp(glob) {
|
|
5550
|
+
const body = normalizeGlob(glob).split(/(\*\*\/|\*\*|\*|\?)/).map((part) => {
|
|
5551
|
+
switch (part) {
|
|
5552
|
+
case "**/":
|
|
5553
|
+
return "(?:.*/)?";
|
|
5554
|
+
case "**":
|
|
5555
|
+
return ".*";
|
|
5556
|
+
case "*":
|
|
5557
|
+
return "[^/]*";
|
|
5558
|
+
case "?":
|
|
5559
|
+
return "[^/]";
|
|
5560
|
+
default:
|
|
5561
|
+
return part.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
5562
|
+
}
|
|
5563
|
+
}).join("");
|
|
5564
|
+
return new RegExp(`^${body}$`);
|
|
5565
|
+
}
|
|
5566
|
+
function normalizeGlob(glob) {
|
|
5567
|
+
const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "").replace(/\/\*\*$/, "");
|
|
5568
|
+
if (normalized.startsWith("/") || normalized.split("/").includes("..")) {
|
|
5569
|
+
throw new Error(`spfn.config.js env.audit: "${glob}" must be relative to the project root and stay inside it.`);
|
|
5570
|
+
}
|
|
5571
|
+
return normalized === "." ? "**" : normalized;
|
|
5572
|
+
}
|
|
5573
|
+
function staticPrefix(glob) {
|
|
5574
|
+
const segments = normalizeGlob(glob).split("/");
|
|
5575
|
+
const firstWildcard = segments.findIndex((segment) => GLOB_CHARACTER.test(segment));
|
|
5576
|
+
return (firstWildcard === -1 ? segments : segments.slice(0, firstWildcard)).join("/");
|
|
5577
|
+
}
|
|
5578
|
+
function walk(cwd, relPath, ignore, files) {
|
|
5579
|
+
const fullPath = join30(cwd, relPath);
|
|
5580
|
+
if (!existsSync29(fullPath) || relPath && matchesAny(relPath, ignore)) {
|
|
5581
|
+
return;
|
|
5582
|
+
}
|
|
5583
|
+
const stats = lstatSync(fullPath);
|
|
5584
|
+
if (stats.isFile()) {
|
|
5585
|
+
addIfSource(relPath, files);
|
|
5586
|
+
}
|
|
5587
|
+
if (!stats.isDirectory()) {
|
|
5588
|
+
return;
|
|
5589
|
+
}
|
|
5590
|
+
for (const entry of readdirSync2(fullPath, { withFileTypes: true })) {
|
|
5591
|
+
const child = relPath ? `${relPath}/${entry.name}` : entry.name;
|
|
5592
|
+
if (entry.isDirectory() && !SKIPPED_DIRECTORIES.has(entry.name)) {
|
|
5593
|
+
walk(cwd, child, ignore, files);
|
|
5594
|
+
} else if (entry.isFile() && !matchesAny(child, ignore)) {
|
|
5595
|
+
addIfSource(child, files);
|
|
5596
|
+
}
|
|
5597
|
+
}
|
|
5598
|
+
}
|
|
5599
|
+
function addIfSource(relPath, files) {
|
|
5600
|
+
if (SOURCE_FILE.test(relPath) && !DECLARATION_FILE.test(relPath)) {
|
|
5601
|
+
files.add(relPath);
|
|
5602
|
+
}
|
|
5603
|
+
}
|
|
5604
|
+
|
|
5605
|
+
// src/utils/env-audit/parse.ts
|
|
5606
|
+
import { createRequire as createRequire2 } from "module";
|
|
5607
|
+
import { join as join31 } from "path";
|
|
5608
|
+
var ALWAYS_ALLOWED = "NODE_ENV";
|
|
5609
|
+
var MAX_KEY_TEXT = 40;
|
|
5610
|
+
var compiler;
|
|
5611
|
+
function loadTypeScript(cwd) {
|
|
5612
|
+
if (compiler) {
|
|
5613
|
+
return compiler;
|
|
5614
|
+
}
|
|
5615
|
+
const path5 = resolveFrom(join31(cwd, "noop.js"), "typescript") ?? resolveFrom(import.meta.url, "typescript") ?? resolveThroughCore();
|
|
5616
|
+
if (!path5) {
|
|
5617
|
+
throw new Error("spfn env audit parses sources with TypeScript, which is not installed. Add it: pnpm add -D typescript");
|
|
5618
|
+
}
|
|
5619
|
+
compiler = createRequire2(import.meta.url)(path5);
|
|
5620
|
+
return compiler;
|
|
5621
|
+
}
|
|
5622
|
+
function resolveThroughCore() {
|
|
5623
|
+
const core = resolveFrom(import.meta.url, "@spfn/core/app-config");
|
|
5624
|
+
return core ? resolveFrom(core, "typescript") : void 0;
|
|
5625
|
+
}
|
|
5626
|
+
function readSourceFacts(ts, fileName, text) {
|
|
5627
|
+
const source = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true, scriptKindOf(ts, fileName));
|
|
5628
|
+
const facts = { directReads: [], names: /* @__PURE__ */ new Set() };
|
|
5629
|
+
const visit = (node) => {
|
|
5630
|
+
collectName(ts, node, facts.names);
|
|
5631
|
+
collectDirectRead(ts, source, node, facts.directReads);
|
|
5632
|
+
ts.forEachChild(node, visit);
|
|
5633
|
+
};
|
|
5634
|
+
visit(source);
|
|
5635
|
+
facts.directReads = facts.directReads.filter((read) => read.name !== ALWAYS_ALLOWED);
|
|
5636
|
+
return facts;
|
|
5637
|
+
}
|
|
5638
|
+
function readPropertyLines(ts, fileName, text) {
|
|
5639
|
+
const source = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true, scriptKindOf(ts, fileName));
|
|
5640
|
+
const lines = /* @__PURE__ */ new Map();
|
|
5641
|
+
const visit = (node) => {
|
|
5642
|
+
const name = ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node) ? propertyNameText(ts, node.name) : void 0;
|
|
5643
|
+
if (name && !lines.has(name)) {
|
|
5644
|
+
lines.set(name, lineOf(source, node));
|
|
5645
|
+
}
|
|
5646
|
+
ts.forEachChild(node, visit);
|
|
5647
|
+
};
|
|
5648
|
+
visit(source);
|
|
5649
|
+
return lines;
|
|
5650
|
+
}
|
|
5651
|
+
function scriptKindOf(ts, fileName) {
|
|
5652
|
+
if (fileName.endsWith(".tsx")) {
|
|
5653
|
+
return ts.ScriptKind.TSX;
|
|
5654
|
+
}
|
|
5655
|
+
return /\.[cm]?tsx?$/.test(fileName) ? ts.ScriptKind.TS : ts.ScriptKind.JSX;
|
|
5656
|
+
}
|
|
5657
|
+
function collectName(ts, node, names) {
|
|
5658
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
5659
|
+
names.add(node.name.text);
|
|
5660
|
+
} else if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
5661
|
+
names.add(node.text);
|
|
5662
|
+
} else if (ts.isBindingElement(node) && ts.isObjectBindingPattern(node.parent)) {
|
|
5663
|
+
const name = propertyNameText(ts, node.propertyName ?? node.name);
|
|
5664
|
+
if (name) {
|
|
5665
|
+
names.add(name);
|
|
5666
|
+
}
|
|
5667
|
+
}
|
|
5668
|
+
}
|
|
5669
|
+
function collectDirectRead(ts, source, node, reads) {
|
|
5670
|
+
if (ts.isPropertyAccessExpression(node) && isProcessEnv(ts, node.expression)) {
|
|
5671
|
+
reads.push({ line: lineOf(source, node), name: node.name.text, reason: "read from process.env" });
|
|
5672
|
+
} else if (ts.isElementAccessExpression(node) && isProcessEnv(ts, node.expression)) {
|
|
5673
|
+
reads.push({ line: lineOf(source, node), ...describeElementKey(ts, source, node.argumentExpression) });
|
|
5674
|
+
} else if (ts.isVariableDeclaration(node) && node.initializer && isProcessEnv(ts, node.initializer) && ts.isObjectBindingPattern(node.name)) {
|
|
5675
|
+
reads.push(...node.name.elements.map((element) => destructuredRead(ts, source, element)));
|
|
5676
|
+
}
|
|
5677
|
+
}
|
|
5678
|
+
function describeElementKey(ts, source, key) {
|
|
5679
|
+
if (ts.isStringLiteralLike(key)) {
|
|
5680
|
+
return { name: key.text, reason: "read from process.env" };
|
|
5681
|
+
}
|
|
5682
|
+
const text = key.getText(source).replace(/\s+/g, " ");
|
|
5683
|
+
return {
|
|
5684
|
+
name: `[${text.length > MAX_KEY_TEXT ? `${text.slice(0, MAX_KEY_TEXT)}\u2026` : text}]`,
|
|
5685
|
+
reason: "read from process.env under a computed key"
|
|
5686
|
+
};
|
|
5687
|
+
}
|
|
5688
|
+
function destructuredRead(ts, source, element) {
|
|
5689
|
+
const name = element.dotDotDotToken ? `...${element.name.getText(source)}` : propertyNameText(ts, element.propertyName ?? element.name) ?? `[${(element.propertyName ?? element.name).getText(source)}]`;
|
|
5690
|
+
return { line: lineOf(source, element), name, reason: "destructured from process.env" };
|
|
5691
|
+
}
|
|
5692
|
+
function isProcessEnv(ts, node) {
|
|
5693
|
+
const expression = skipWrappers(ts, node);
|
|
5694
|
+
return ts.isPropertyAccessExpression(expression) && expression.name.text === "env" && isProcess(ts, skipWrappers(ts, expression.expression));
|
|
5695
|
+
}
|
|
5696
|
+
function isProcess(ts, node) {
|
|
5697
|
+
if (ts.isIdentifier(node)) {
|
|
5698
|
+
return node.text === "process";
|
|
5699
|
+
}
|
|
5700
|
+
return ts.isPropertyAccessExpression(node) && node.name.text === "process" && ts.isIdentifier(node.expression) && ["globalThis", "global", "window", "self"].includes(node.expression.text);
|
|
5701
|
+
}
|
|
5702
|
+
function skipWrappers(ts, node) {
|
|
5703
|
+
let current = node;
|
|
5704
|
+
while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isNonNullExpression(current) || ts.isSatisfiesExpression(current)) {
|
|
5705
|
+
current = current.expression;
|
|
5706
|
+
}
|
|
5707
|
+
return current;
|
|
5708
|
+
}
|
|
5709
|
+
function propertyNameText(ts, name) {
|
|
5710
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name) || ts.isPrivateIdentifier(name)) {
|
|
5711
|
+
return name.text;
|
|
5712
|
+
}
|
|
5713
|
+
return void 0;
|
|
5714
|
+
}
|
|
5715
|
+
function lineOf(source, node) {
|
|
5716
|
+
return source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
|
|
5717
|
+
}
|
|
5718
|
+
|
|
5719
|
+
// src/utils/env-audit/index.ts
|
|
5720
|
+
var ROOT_CONFIG_FILE = /^[^/]+\.config\.(?:js|cjs|mjs|ts)$/;
|
|
5721
|
+
async function runEnvAudit(cwd = process.cwd()) {
|
|
5722
|
+
const scope = readScope((await loadAppConfig4(cwd)).env?.audit);
|
|
5723
|
+
const appSources = await loadAppSchemas(cwd);
|
|
5724
|
+
const list = mergeEnvSources([...appSources, ...await loadPackageSchemas(cwd)]);
|
|
5725
|
+
const ts = loadTypeScript(cwd);
|
|
5726
|
+
const files = listAuditFiles(cwd, scope);
|
|
5727
|
+
const context = {
|
|
5728
|
+
cwd,
|
|
5729
|
+
ts,
|
|
5730
|
+
list,
|
|
5731
|
+
appSources,
|
|
5732
|
+
schemaFiles: new Set(appSources.map((source) => toProjectPath(cwd, source.name))),
|
|
5733
|
+
facts: new Map(files.map((file) => [file, readSourceFacts(ts, file, readFileSync15(join32(cwd, file), "utf-8"))]))
|
|
5734
|
+
};
|
|
5735
|
+
return {
|
|
5736
|
+
fileCount: files.length,
|
|
5737
|
+
directReads: findDirectReads(context),
|
|
5738
|
+
declarations: appSources.length > 0 ? findUnusedDeclarations(context) : [],
|
|
5739
|
+
notices: appSources.length > 0 ? [] : ["No env.schemas in spfn.config.js \u2014 the unused-declaration check is skipped."]
|
|
5740
|
+
};
|
|
5741
|
+
}
|
|
5742
|
+
function readScope(audit) {
|
|
5743
|
+
const include = audit?.include ?? DEFAULT_INCLUDE;
|
|
5744
|
+
const ignore = audit?.ignore ?? [];
|
|
5745
|
+
for (const [field, value] of [["include", include], ["ignore", ignore]]) {
|
|
5746
|
+
if (!Array.isArray(value) || value.some((glob) => typeof glob !== "string")) {
|
|
5747
|
+
throw new Error(`spfn.config.js: env.audit.${field} must be an array of globs relative to the project root.`);
|
|
5748
|
+
}
|
|
5749
|
+
}
|
|
5750
|
+
return { include, ignore };
|
|
5751
|
+
}
|
|
5752
|
+
function findDirectReads({ facts, schemaFiles }) {
|
|
5753
|
+
return [...facts].filter(([file]) => !schemaFiles.has(file) && !ROOT_CONFIG_FILE.test(file)).flatMap(([file, { directReads }]) => directReads.map((read) => ({ file, ...read })));
|
|
5754
|
+
}
|
|
5755
|
+
function findUnusedDeclarations(context) {
|
|
5756
|
+
const readNames = new Set([...context.facts].filter(([file]) => !context.schemaFiles.has(file)).flatMap(([, { names }]) => [...names]));
|
|
5757
|
+
return context.appSources.flatMap((source) => {
|
|
5758
|
+
const file = toProjectPath(context.cwd, source.name);
|
|
5759
|
+
const lines = readPropertyLines(context.ts, file, readFileSync15(join32(context.cwd, file), "utf-8"));
|
|
5760
|
+
return Object.entries(source.schema).filter(([name]) => !isPackageName(context, name)).flatMap(([name, entry]) => checkEntry(context, readNames, name, entry).map((reason) => ({ file, line: lines.get(name) ?? 1, name, reason })));
|
|
5761
|
+
});
|
|
5762
|
+
}
|
|
5763
|
+
function checkEntry(context, readNames, name, entry) {
|
|
5764
|
+
if (entry.readBy === void 0) {
|
|
5765
|
+
return readNames.has(name) ? [] : ["declared but read nowhere in scope (unused)"];
|
|
5766
|
+
}
|
|
5767
|
+
if (!Array.isArray(entry.readBy) || entry.readBy.some((path5) => typeof path5 !== "string")) {
|
|
5768
|
+
return ["readBy must be an array of paths relative to the project root"];
|
|
5769
|
+
}
|
|
5770
|
+
return checkReadBy(context, entry.readBy);
|
|
5771
|
+
}
|
|
5772
|
+
function isPackageName({ list, appSources }, name) {
|
|
5773
|
+
const appNames = new Set(appSources.map((source) => source.name));
|
|
5774
|
+
return (list.declaredBy[name] ?? []).some((owner) => !appNames.has(owner));
|
|
5775
|
+
}
|
|
5776
|
+
function checkReadBy({ cwd, facts }, readBy) {
|
|
5777
|
+
return readBy.flatMap((path5) => {
|
|
5778
|
+
const file = toProjectPath(cwd, path5);
|
|
5779
|
+
if (!existsSync30(join32(cwd, file))) {
|
|
5780
|
+
return [`readBy file ${path5} does not exist`];
|
|
5781
|
+
}
|
|
5782
|
+
return facts.has(file) ? [] : [`readBy file ${path5} is not scanned (see env.audit)`];
|
|
5783
|
+
});
|
|
5784
|
+
}
|
|
5785
|
+
function toProjectPath(cwd, path5) {
|
|
5786
|
+
return relative2(cwd, resolve3(cwd, path5)).split("\\").join("/");
|
|
5787
|
+
}
|
|
5362
5788
|
|
|
5363
5789
|
// src/commands/env.ts
|
|
5364
5790
|
var BASE_ENV_FILES = {
|
|
@@ -5403,10 +5829,10 @@ function formatDefault(value, type) {
|
|
|
5403
5829
|
return chalk32.cyan(String(value));
|
|
5404
5830
|
}
|
|
5405
5831
|
async function listEnvVars(options) {
|
|
5406
|
-
const
|
|
5832
|
+
const label = describeEnvList(options);
|
|
5407
5833
|
try {
|
|
5408
|
-
const
|
|
5409
|
-
const allVars =
|
|
5834
|
+
const list = await loadEnvList(options);
|
|
5835
|
+
const allVars = orderedVars(list);
|
|
5410
5836
|
if (options.group) {
|
|
5411
5837
|
const grouped = allVars.reduce((acc, [key, schema]) => {
|
|
5412
5838
|
const target = getTargetFile(schema);
|
|
@@ -5415,22 +5841,27 @@ async function listEnvVars(options) {
|
|
|
5415
5841
|
return acc;
|
|
5416
5842
|
}, {});
|
|
5417
5843
|
console.log(chalk32.blue.bold(`
|
|
5418
|
-
\u{1F4CB} Environment Variables by File (${
|
|
5844
|
+
\u{1F4CB} Environment Variables by File (${label})
|
|
5419
5845
|
`));
|
|
5420
5846
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
5421
5847
|
console.log(chalk32.bold.magenta(`
|
|
5422
5848
|
${file}`));
|
|
5423
5849
|
console.log(chalk32.dim("\u2500".repeat(50)));
|
|
5424
5850
|
for (const [key, schema] of vars) {
|
|
5425
|
-
printEnvVar(key, schema);
|
|
5851
|
+
printEnvVar(key, schema, false, list.declaredBy[key]);
|
|
5426
5852
|
}
|
|
5427
5853
|
}
|
|
5428
5854
|
} else {
|
|
5429
5855
|
console.log(chalk32.blue.bold(`
|
|
5430
|
-
\u{1F4CB} Environment Variables (${
|
|
5856
|
+
\u{1F4CB} Environment Variables (${label})
|
|
5431
5857
|
`));
|
|
5432
|
-
for (const
|
|
5433
|
-
|
|
5858
|
+
for (const group of groupBySource(list)) {
|
|
5859
|
+
console.log(chalk32.bold.magenta(`
|
|
5860
|
+
${group.source}`));
|
|
5861
|
+
console.log(chalk32.dim("\u2500".repeat(50)));
|
|
5862
|
+
for (const entry of group.entries) {
|
|
5863
|
+
printEnvVar(entry.key, entry, true, alsoDeclaredBy(list, entry.key));
|
|
5864
|
+
}
|
|
5434
5865
|
}
|
|
5435
5866
|
}
|
|
5436
5867
|
console.log(chalk32.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
|
|
@@ -5441,13 +5872,19 @@ ${file}`));
|
|
|
5441
5872
|
process.exit(1);
|
|
5442
5873
|
}
|
|
5443
5874
|
}
|
|
5444
|
-
function
|
|
5875
|
+
function orderedVars(list) {
|
|
5876
|
+
return groupBySource(list).flatMap((group) => group.entries.map((entry) => [entry.key, entry]));
|
|
5877
|
+
}
|
|
5878
|
+
function printEnvVar(key, schema, showFile = false, sources = []) {
|
|
5445
5879
|
const typeStr = formatType(schema.type);
|
|
5446
5880
|
const requiredStr = schema.required || schema.default !== void 0 ? chalk32.red("[required]") : chalk32.dim("[optional]");
|
|
5447
5881
|
const sensitiveStr = schema.sensitive ? chalk32.yellow(" [sensitive]") : "";
|
|
5448
5882
|
const fileStr = showFile ? chalk32.dim(` \u2192 ${getTargetFile(schema)}`) : "";
|
|
5449
5883
|
console.log(`${chalk32.bold.cyan(key)} ${chalk32.dim("(")}${typeStr}${chalk32.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
|
|
5450
5884
|
console.log(` ${chalk32.dim(schema.description)}`);
|
|
5885
|
+
if (sources.length > 0) {
|
|
5886
|
+
console.log(` ${chalk32.dim(showFile ? "Also declared by:" : "Declared by:")} ${chalk32.dim(sources.join(", "))}`);
|
|
5887
|
+
}
|
|
5451
5888
|
if (schema.default !== void 0) {
|
|
5452
5889
|
console.log(` ${chalk32.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
5453
5890
|
}
|
|
@@ -5458,13 +5895,12 @@ function printEnvVar(key, schema, showFile = false) {
|
|
|
5458
5895
|
console.log();
|
|
5459
5896
|
}
|
|
5460
5897
|
async function showEnvStats(options) {
|
|
5461
|
-
const packageName = options.package || "@spfn/core";
|
|
5462
5898
|
try {
|
|
5463
|
-
const
|
|
5899
|
+
const list = await loadEnvList(options);
|
|
5464
5900
|
console.log(chalk32.blue.bold(`
|
|
5465
|
-
\u{1F4CA} Environment Variable Statistics (${
|
|
5901
|
+
\u{1F4CA} Environment Variable Statistics (${describeEnvList(options)})
|
|
5466
5902
|
`));
|
|
5467
|
-
const allVars =
|
|
5903
|
+
const allVars = orderedVars(list);
|
|
5468
5904
|
const required = allVars.filter(([_, schema]) => schema.required || schema.default !== void 0);
|
|
5469
5905
|
const optional = allVars.filter(([_, schema]) => !schema.required && schema.default === void 0);
|
|
5470
5906
|
const sensitive = allVars.filter(([_, schema]) => schema.sensitive);
|
|
@@ -5507,12 +5943,11 @@ async function showEnvStats(options) {
|
|
|
5507
5943
|
}
|
|
5508
5944
|
}
|
|
5509
5945
|
async function searchEnvVars(query, options) {
|
|
5510
|
-
const packageName = options.package || "@spfn/core";
|
|
5511
5946
|
try {
|
|
5512
|
-
const
|
|
5947
|
+
const list = await loadEnvList(options);
|
|
5513
5948
|
const normalizedQuery = query.toLowerCase();
|
|
5514
5949
|
const results = [];
|
|
5515
|
-
for (const [key, schema] of
|
|
5950
|
+
for (const [key, schema] of orderedVars(list)) {
|
|
5516
5951
|
const matchesKey = key.toLowerCase().includes(normalizedQuery);
|
|
5517
5952
|
const matchesDescription = schema.description.toLowerCase().includes(normalizedQuery);
|
|
5518
5953
|
if (matchesKey || matchesDescription) {
|
|
@@ -5545,10 +5980,11 @@ async function searchEnvVars(query, options) {
|
|
|
5545
5980
|
process.exit(1);
|
|
5546
5981
|
}
|
|
5547
5982
|
}
|
|
5983
|
+
var PACKAGE_OPTION_HELP = "Read only this package's env schema (default: the whole app \u2014 spfn.config.js env.schemas + installed @spfn/* packages)";
|
|
5548
5984
|
var envCommand = new Command13("env").description("Manage environment variables");
|
|
5549
|
-
envCommand.command("list").description("List all environment variables from schema").option("-p, --package <package>",
|
|
5550
|
-
envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>",
|
|
5551
|
-
envCommand.command("search").description("Search environment variables").argument("<query>", "Search query (matches key or description)").option("-p, --package <package>",
|
|
5985
|
+
envCommand.command("list").description("List all environment variables from schema").option("-p, --package <package>", PACKAGE_OPTION_HELP).option("-g, --group", "Group variables by target file").action(listEnvVars);
|
|
5986
|
+
envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>", PACKAGE_OPTION_HELP).action(showEnvStats);
|
|
5987
|
+
envCommand.command("search").description("Search environment variables").argument("<query>", "Search query (matches key or description)").option("-p, --package <package>", PACKAGE_OPTION_HELP).action(searchEnvVars);
|
|
5552
5988
|
function validateEnvOption(envValue) {
|
|
5553
5989
|
if (!VALID_ENVS.includes(envValue)) {
|
|
5554
5990
|
console.error(chalk32.red(`
|
|
@@ -5560,12 +5996,11 @@ function validateEnvOption(envValue) {
|
|
|
5560
5996
|
return envValue;
|
|
5561
5997
|
}
|
|
5562
5998
|
async function initEnvFiles(options) {
|
|
5563
|
-
const packageName = options.package || "@spfn/core";
|
|
5564
5999
|
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
5565
6000
|
const cwd = process.cwd();
|
|
5566
6001
|
try {
|
|
5567
|
-
const
|
|
5568
|
-
const allVars =
|
|
6002
|
+
const list = await loadEnvList(options);
|
|
6003
|
+
const allVars = orderedVars(list);
|
|
5569
6004
|
const grouped = allVars.reduce((acc, [key, schema]) => {
|
|
5570
6005
|
const target = getTargetFile(schema);
|
|
5571
6006
|
const exampleFile = target + ".example";
|
|
@@ -5588,14 +6023,14 @@ async function initEnvFiles(options) {
|
|
|
5588
6023
|
}
|
|
5589
6024
|
const allGrouped = { ...grouped, ...envSpecificFiles };
|
|
5590
6025
|
for (const [file, vars] of Object.entries(allGrouped)) {
|
|
5591
|
-
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
6026
|
+
writeEnvTemplate(cwd, file, vars, options.force ?? false, list);
|
|
5592
6027
|
}
|
|
5593
6028
|
} else {
|
|
5594
6029
|
console.log(chalk32.blue.bold(`
|
|
5595
6030
|
\u{1F680} Generating .env template files
|
|
5596
6031
|
`));
|
|
5597
6032
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
5598
|
-
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
6033
|
+
writeEnvTemplate(cwd, file, vars, options.force ?? false, list);
|
|
5599
6034
|
}
|
|
5600
6035
|
}
|
|
5601
6036
|
console.log(chalk32.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
|
|
@@ -5614,22 +6049,28 @@ async function initEnvFiles(options) {
|
|
|
5614
6049
|
process.exit(1);
|
|
5615
6050
|
}
|
|
5616
6051
|
}
|
|
5617
|
-
function writeEnvTemplate(cwd, file, vars, force) {
|
|
5618
|
-
const filePath =
|
|
5619
|
-
if (
|
|
6052
|
+
function writeEnvTemplate(cwd, file, vars, force, list) {
|
|
6053
|
+
const filePath = resolve4(cwd, file);
|
|
6054
|
+
if (existsSync31(filePath) && !force) {
|
|
5620
6055
|
console.log(chalk32.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
|
|
5621
6056
|
return;
|
|
5622
6057
|
}
|
|
5623
|
-
writeFileSync18(filePath, generateEnvFileContent(vars), "utf-8");
|
|
6058
|
+
writeFileSync18(filePath, generateEnvFileContent(vars, list), "utf-8");
|
|
5624
6059
|
console.log(chalk32.green(` \u2705 ${file} (${vars.length} variables)`));
|
|
5625
6060
|
}
|
|
5626
|
-
function generateEnvFileContent(vars) {
|
|
6061
|
+
function generateEnvFileContent(vars, list) {
|
|
5627
6062
|
const lines = [
|
|
5628
6063
|
"# Auto-generated by spfn env init",
|
|
5629
6064
|
"# Copy this file and fill in the values",
|
|
5630
6065
|
""
|
|
5631
6066
|
];
|
|
6067
|
+
let section;
|
|
5632
6068
|
for (const [key, schema] of vars) {
|
|
6069
|
+
const source = list.declaredBy[key]?.[0];
|
|
6070
|
+
if (list.sources.length > 1 && source !== section) {
|
|
6071
|
+
section = source;
|
|
6072
|
+
lines.push(`# \u2500\u2500 ${source} \u2500\u2500`, "");
|
|
6073
|
+
}
|
|
5633
6074
|
lines.push(`# ${schema.description}`);
|
|
5634
6075
|
if (schema.required) {
|
|
5635
6076
|
lines.push(`# [required]`);
|
|
@@ -5649,12 +6090,11 @@ function generateEnvFileContent(vars) {
|
|
|
5649
6090
|
return lines.join("\n");
|
|
5650
6091
|
}
|
|
5651
6092
|
async function checkEnvFiles(options) {
|
|
5652
|
-
const packageName = options.package || "@spfn/core";
|
|
5653
6093
|
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
5654
6094
|
const cwd = process.cwd();
|
|
5655
6095
|
try {
|
|
5656
|
-
const
|
|
5657
|
-
const allVars =
|
|
6096
|
+
const list = await loadEnvList(options);
|
|
6097
|
+
const allVars = orderedVars(list);
|
|
5658
6098
|
const envLabel = targetEnv ? ` (${targetEnv})` : "";
|
|
5659
6099
|
console.log(chalk32.blue.bold(`
|
|
5660
6100
|
\u{1F50D} Checking .env files against schema${envLabel}
|
|
@@ -5664,11 +6104,11 @@ async function checkEnvFiles(options) {
|
|
|
5664
6104
|
const issues = [];
|
|
5665
6105
|
const warnings = [];
|
|
5666
6106
|
for (const file of filesToCheck) {
|
|
5667
|
-
const filePath =
|
|
5668
|
-
if (!
|
|
6107
|
+
const filePath = resolve4(cwd, file);
|
|
6108
|
+
if (!existsSync31(filePath)) {
|
|
5669
6109
|
continue;
|
|
5670
6110
|
}
|
|
5671
|
-
const content =
|
|
6111
|
+
const content = readFileSync16(filePath, "utf-8");
|
|
5672
6112
|
const parsed = parse2(content);
|
|
5673
6113
|
for (const [key, value] of Object.entries(parsed)) {
|
|
5674
6114
|
loadedEnv[key] = { value: value || "", file };
|
|
@@ -5681,7 +6121,7 @@ async function checkEnvFiles(options) {
|
|
|
5681
6121
|
const found = loadedEnv[key];
|
|
5682
6122
|
if (!found) {
|
|
5683
6123
|
if (schema.required && schema.default === void 0) {
|
|
5684
|
-
issues.push(`${chalk32.red("\u2717")} ${chalk32.cyan(key)} is required but not found in any .env file`);
|
|
6124
|
+
issues.push(sourced(list, key, `${chalk32.red("\u2717")} ${chalk32.cyan(key)} is required but not found in any .env file`));
|
|
5685
6125
|
}
|
|
5686
6126
|
continue;
|
|
5687
6127
|
}
|
|
@@ -5690,34 +6130,33 @@ async function checkEnvFiles(options) {
|
|
|
5690
6130
|
const shouldBeNextjs = schema.nextjs ?? key.startsWith("NEXT_PUBLIC_");
|
|
5691
6131
|
if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
|
|
5692
6132
|
if (schema.sensitive) {
|
|
5693
|
-
issues.push(
|
|
6133
|
+
issues.push(sourced(
|
|
6134
|
+
list,
|
|
6135
|
+
key,
|
|
5694
6136
|
`${chalk32.red("\u2717")} ${chalk32.cyan(key)} is sensitive and should be in ${chalk32.magenta(expectedFile)}, but found in ${chalk32.yellow(found.file)} (security risk!)`
|
|
5695
|
-
);
|
|
6137
|
+
));
|
|
5696
6138
|
} else {
|
|
5697
|
-
warnings.push(
|
|
6139
|
+
warnings.push(sourced(
|
|
6140
|
+
list,
|
|
6141
|
+
key,
|
|
5698
6142
|
`${chalk32.yellow("\u26A0")} ${chalk32.cyan(key)} should be in ${chalk32.magenta(expectedFile)}, but found in ${chalk32.dim(found.file)}`
|
|
5699
|
-
);
|
|
6143
|
+
));
|
|
5700
6144
|
}
|
|
5701
6145
|
}
|
|
5702
6146
|
}
|
|
5703
6147
|
for (const [key, { file }] of Object.entries(loadedEnv)) {
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
warnings.push(`${chalk32.yellow("\u26A0")} ${chalk32.cyan(key)} in ${chalk32.dim(file)} is not in schema`);
|
|
6148
|
+
if (!list.schema[key]) {
|
|
6149
|
+
warnings.push({ source: UNDECLARED, text: `${chalk32.yellow("\u26A0")} ${chalk32.cyan(key)} in ${chalk32.dim(file)} is not in schema` });
|
|
5707
6150
|
}
|
|
5708
6151
|
}
|
|
5709
6152
|
if (issues.length > 0) {
|
|
5710
6153
|
console.log(chalk32.red.bold("Issues:"));
|
|
5711
|
-
|
|
5712
|
-
console.log(` ${issue}`);
|
|
5713
|
-
}
|
|
6154
|
+
printBySource(issues);
|
|
5714
6155
|
console.log("");
|
|
5715
6156
|
}
|
|
5716
6157
|
if (warnings.length > 0) {
|
|
5717
6158
|
console.log(chalk32.yellow.bold("Warnings:"));
|
|
5718
|
-
|
|
5719
|
-
console.log(` ${warning}`);
|
|
5720
|
-
}
|
|
6159
|
+
printBySource(warnings);
|
|
5721
6160
|
console.log("");
|
|
5722
6161
|
}
|
|
5723
6162
|
if (issues.length === 0 && warnings.length === 0) {
|
|
@@ -5736,10 +6175,22 @@ async function checkEnvFiles(options) {
|
|
|
5736
6175
|
process.exit(1);
|
|
5737
6176
|
}
|
|
5738
6177
|
}
|
|
5739
|
-
|
|
5740
|
-
|
|
6178
|
+
var UNDECLARED = "(not declared by any schema)";
|
|
6179
|
+
function sourced(list, key, text) {
|
|
6180
|
+
return { source: list.declaredBy[key]?.[0] ?? UNDECLARED, text };
|
|
6181
|
+
}
|
|
6182
|
+
function printBySource(lines) {
|
|
6183
|
+
const sources = [...new Set(lines.map((line) => line.source))];
|
|
6184
|
+
for (const source of sources) {
|
|
6185
|
+
console.log(` ${chalk32.bold.magenta(source)}`);
|
|
6186
|
+
for (const line of lines.filter((candidate) => candidate.source === source)) {
|
|
6187
|
+
console.log(` ${line.text}`);
|
|
6188
|
+
}
|
|
6189
|
+
}
|
|
6190
|
+
}
|
|
6191
|
+
envCommand.command("init").description("Generate .env template files from schema").option("-p, --package <package>", PACKAGE_OPTION_HELP).option("-e, --env <environment>", "Generate environment-specific templates (e.g. production, staging)").option("-f, --force", "Overwrite existing files").action(initEnvFiles);
|
|
6192
|
+
envCommand.command("check").description("Check .env files against schema").option("-p, --package <package>", PACKAGE_OPTION_HELP).option("-e, --env <environment>", "Check files for a specific environment (e.g. production)").action(checkEnvFiles);
|
|
5741
6193
|
async function validateEnvVars(options) {
|
|
5742
|
-
const packages = options.packages || ["@spfn/core"];
|
|
5743
6194
|
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
5744
6195
|
if (targetEnv) {
|
|
5745
6196
|
const { loadEnv: loadEnv10 } = await import("@spfn/core/env/loader");
|
|
@@ -5756,66 +6207,133 @@ async function validateEnvVars(options) {
|
|
|
5756
6207
|
\u{1F50D} Validating environment variables
|
|
5757
6208
|
`));
|
|
5758
6209
|
}
|
|
5759
|
-
const
|
|
5760
|
-
const
|
|
5761
|
-
for (const packageName of packages) {
|
|
5762
|
-
try {
|
|
5763
|
-
console.log(chalk32.dim(` \u{1F4E6} ${packageName}`));
|
|
5764
|
-
const envSchema = await loadEnvSchema(packageName);
|
|
5765
|
-
const { createEnvRegistry } = await import("@spfn/core/env");
|
|
5766
|
-
const registry = createEnvRegistry(envSchema);
|
|
5767
|
-
const result = registry.validateAll();
|
|
5768
|
-
for (const error of result.errors) {
|
|
5769
|
-
allErrors.push({ ...error, package: packageName });
|
|
5770
|
-
}
|
|
5771
|
-
for (const warning of result.warnings) {
|
|
5772
|
-
allWarnings.push({ ...warning, package: packageName });
|
|
5773
|
-
}
|
|
5774
|
-
} catch (error) {
|
|
5775
|
-
if (error instanceof Error && error.message.includes("does not export envSchema")) {
|
|
5776
|
-
console.log(chalk32.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
|
|
5777
|
-
continue;
|
|
5778
|
-
}
|
|
5779
|
-
console.error(chalk32.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
|
|
5780
|
-
if (options.strict) {
|
|
5781
|
-
process.exit(1);
|
|
5782
|
-
}
|
|
5783
|
-
}
|
|
5784
|
-
}
|
|
6210
|
+
const sources = options.packages ? await loadPackagesToValidate(options.packages, options.strict ?? false) : await loadWholeListToValidate();
|
|
6211
|
+
const { errors, warnings } = await validateSources(sources);
|
|
5785
6212
|
console.log("");
|
|
5786
|
-
if (
|
|
5787
|
-
console.log(chalk32.red.bold(`\u274C Validation Errors (${
|
|
6213
|
+
if (errors.length > 0) {
|
|
6214
|
+
console.log(chalk32.red.bold(`\u274C Validation Errors (${errors.length}):
|
|
5788
6215
|
`));
|
|
5789
|
-
|
|
5790
|
-
console.log(` ${chalk32.red("\u2717")} ${chalk32.cyan(error.key)}`);
|
|
5791
|
-
console.log(` ${chalk32.dim(error.message)}`);
|
|
5792
|
-
console.log(` ${chalk32.dim(`from ${error.package}`)}`);
|
|
5793
|
-
console.log("");
|
|
5794
|
-
}
|
|
6216
|
+
printFindingsBySource(errors, chalk32.red("\u2717"));
|
|
5795
6217
|
}
|
|
5796
|
-
if (
|
|
5797
|
-
console.log(chalk32.yellow.bold(`\u26A0\uFE0F Warnings (${
|
|
6218
|
+
if (warnings.length > 0) {
|
|
6219
|
+
console.log(chalk32.yellow.bold(`\u26A0\uFE0F Warnings (${warnings.length}):
|
|
5798
6220
|
`));
|
|
5799
|
-
|
|
5800
|
-
console.log(` ${chalk32.yellow("\u26A0")} ${chalk32.cyan(warning.key)}`);
|
|
5801
|
-
console.log(` ${chalk32.dim(warning.message)}`);
|
|
5802
|
-
console.log("");
|
|
5803
|
-
}
|
|
6221
|
+
printFindingsBySource(warnings, chalk32.yellow("\u26A0"));
|
|
5804
6222
|
}
|
|
5805
|
-
if (
|
|
6223
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
5806
6224
|
console.log(chalk32.green.bold("\u2705 All environment variables are valid!\n"));
|
|
5807
|
-
} else if (
|
|
6225
|
+
} else if (errors.length === 0) {
|
|
5808
6226
|
console.log(chalk32.green("\u2705 No errors found."));
|
|
5809
|
-
console.log(chalk32.yellow(`\u26A0\uFE0F ${
|
|
6227
|
+
console.log(chalk32.yellow(`\u26A0\uFE0F ${warnings.length} warning(s) found.
|
|
5810
6228
|
`));
|
|
5811
6229
|
} else {
|
|
5812
6230
|
console.log(chalk32.red(`
|
|
5813
|
-
\u274C Validation failed with ${
|
|
6231
|
+
\u274C Validation failed with ${errors.length} error(s)
|
|
5814
6232
|
`));
|
|
5815
6233
|
process.exit(1);
|
|
5816
6234
|
}
|
|
5817
6235
|
}
|
|
5818
|
-
|
|
6236
|
+
async function loadPackagesToValidate(packages, strict) {
|
|
6237
|
+
const sources = [];
|
|
6238
|
+
for (const packageName of packages) {
|
|
6239
|
+
console.log(chalk32.dim(` \u{1F4E6} ${packageName}`));
|
|
6240
|
+
const schema = await loadEnvSchema(packageName).catch((error) => {
|
|
6241
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6242
|
+
if (message.includes("does not export envSchema")) {
|
|
6243
|
+
console.log(chalk32.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
|
|
6244
|
+
} else {
|
|
6245
|
+
console.error(chalk32.red(` \u274C Failed to load: ${message}`));
|
|
6246
|
+
exitIf(strict);
|
|
6247
|
+
}
|
|
6248
|
+
return void 0;
|
|
6249
|
+
});
|
|
6250
|
+
if (schema) {
|
|
6251
|
+
sources.push({ name: packageName, schema });
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
return sources;
|
|
6255
|
+
}
|
|
6256
|
+
async function loadWholeListToValidate() {
|
|
6257
|
+
const list = await loadEnvList({}).catch((error) => {
|
|
6258
|
+
console.error(chalk32.red(` \u274C ${error instanceof Error ? error.message : String(error)}`));
|
|
6259
|
+
return process.exit(1);
|
|
6260
|
+
});
|
|
6261
|
+
for (const source of list.sources) {
|
|
6262
|
+
console.log(chalk32.dim(` \u{1F4E6} ${source.name}`));
|
|
6263
|
+
}
|
|
6264
|
+
return list.sources;
|
|
6265
|
+
}
|
|
6266
|
+
function exitIf(condition) {
|
|
6267
|
+
if (condition) {
|
|
6268
|
+
process.exit(1);
|
|
6269
|
+
}
|
|
6270
|
+
}
|
|
6271
|
+
async function validateSources(sources) {
|
|
6272
|
+
const { createEnvRegistry } = await import("@spfn/core/env");
|
|
6273
|
+
const errors = [];
|
|
6274
|
+
const warnings = [];
|
|
6275
|
+
for (const source of sources) {
|
|
6276
|
+
const result = createEnvRegistry(source.schema).validateAll();
|
|
6277
|
+
errors.push(...result.errors.map((error) => ({ ...error, source: source.name })));
|
|
6278
|
+
warnings.push(...result.warnings.map((warning) => ({ ...warning, source: source.name })));
|
|
6279
|
+
}
|
|
6280
|
+
return { errors: uniqueFindings(errors), warnings: uniqueFindings(warnings) };
|
|
6281
|
+
}
|
|
6282
|
+
function uniqueFindings(findings) {
|
|
6283
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6284
|
+
return findings.filter((finding) => {
|
|
6285
|
+
const id = `${finding.key}
|
|
6286
|
+
${finding.message}`;
|
|
6287
|
+
const isNew = !seen.has(id);
|
|
6288
|
+
seen.add(id);
|
|
6289
|
+
return isNew;
|
|
6290
|
+
});
|
|
6291
|
+
}
|
|
6292
|
+
function printFindingsBySource(findings, mark) {
|
|
6293
|
+
printBySource(findings.map((finding) => ({
|
|
6294
|
+
source: finding.source,
|
|
6295
|
+
text: `${mark} ${chalk32.cyan(finding.key)}
|
|
6296
|
+
${chalk32.dim(finding.message)}`
|
|
6297
|
+
})));
|
|
6298
|
+
console.log("");
|
|
6299
|
+
}
|
|
6300
|
+
envCommand.command("validate").description("Validate environment variables against schema (for CI/CD)").option("-p, --packages <packages...>", "Validate only these packages (default: the whole app \u2014 spfn.config.js env.schemas + installed @spfn/* packages)").option("-e, --env <environment>", "Load env files for specific environment before validating").option("-s, --strict", "Exit on any error (including load failures)").action(validateEnvVars);
|
|
6301
|
+
async function auditEnv() {
|
|
6302
|
+
const report2 = await runEnvAudit().catch((error) => {
|
|
6303
|
+
console.error(chalk32.red(`
|
|
6304
|
+
\u274C ${error instanceof Error ? error.message : String(error)}
|
|
6305
|
+
`));
|
|
6306
|
+
return process.exit(1);
|
|
6307
|
+
});
|
|
6308
|
+
console.log(chalk32.blue.bold(`
|
|
6309
|
+
\u{1F50D} Env audit \u2014 ${report2.fileCount} file(s) scanned
|
|
6310
|
+
`));
|
|
6311
|
+
printAuditFindings("Direct reads of process.env", report2.directReads);
|
|
6312
|
+
printAuditFindings("Declarations", report2.declarations);
|
|
6313
|
+
for (const notice of report2.notices) {
|
|
6314
|
+
console.log(chalk32.dim(` \u2139 ${notice}
|
|
6315
|
+
`));
|
|
6316
|
+
}
|
|
6317
|
+
const count = report2.directReads.length + report2.declarations.length;
|
|
6318
|
+
if (count === 0) {
|
|
6319
|
+
console.log(chalk32.green("\u2705 The code reads the env list, and every app variable is read.\n"));
|
|
6320
|
+
return;
|
|
6321
|
+
}
|
|
6322
|
+
console.log(chalk32.dim(`Found ${count} finding(s)
|
|
6323
|
+
`));
|
|
6324
|
+
process.exit(1);
|
|
6325
|
+
}
|
|
6326
|
+
function printAuditFindings(heading, findings) {
|
|
6327
|
+
if (findings.length === 0) {
|
|
6328
|
+
return;
|
|
6329
|
+
}
|
|
6330
|
+
console.log(chalk32.bold(`${heading} (${findings.length}):`));
|
|
6331
|
+
for (const finding of [...findings].sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line)) {
|
|
6332
|
+
console.log(` ${chalk32.yellow(`${finding.file}:${finding.line}`)} ${chalk32.cyan(finding.name)} ${chalk32.dim(finding.reason)}`);
|
|
6333
|
+
}
|
|
6334
|
+
console.log("");
|
|
6335
|
+
}
|
|
6336
|
+
envCommand.command("audit").description("Find direct process.env reads and app env variables nothing reads (spfn.config.js env.audit)").action(auditEnv);
|
|
5819
6337
|
|
|
5820
6338
|
// src/commands/ops/index.ts
|
|
5821
6339
|
import { Command as Command15 } from "commander";
|
|
@@ -6238,14 +6756,14 @@ function assertBatchSafe(label, value) {
|
|
|
6238
6756
|
function storeOpsToken(account, token) {
|
|
6239
6757
|
assertBatchSafe("Keychain account", account);
|
|
6240
6758
|
assertBatchSafe("Token", token);
|
|
6241
|
-
return new Promise((
|
|
6759
|
+
return new Promise((resolve7, reject) => {
|
|
6242
6760
|
const child = spawn5("security", ["-i"], { stdio: ["pipe", "ignore", "pipe"] });
|
|
6243
6761
|
let stderr = "";
|
|
6244
6762
|
child.stderr.on("data", (chunk) => stderr += String(chunk));
|
|
6245
6763
|
child.on("error", reject);
|
|
6246
6764
|
child.on("close", (code) => {
|
|
6247
6765
|
if (code === 0) {
|
|
6248
|
-
|
|
6766
|
+
resolve7();
|
|
6249
6767
|
return;
|
|
6250
6768
|
}
|
|
6251
6769
|
reject(new Error(`security exited with code ${code}: ${stderr.trim()}`));
|
|
@@ -6257,27 +6775,27 @@ function storeOpsToken(account, token) {
|
|
|
6257
6775
|
}
|
|
6258
6776
|
function loadOpsToken(account) {
|
|
6259
6777
|
assertBatchSafe("Keychain account", account);
|
|
6260
|
-
return new Promise((
|
|
6778
|
+
return new Promise((resolve7, reject) => {
|
|
6261
6779
|
execFile(
|
|
6262
6780
|
"security",
|
|
6263
6781
|
["find-generic-password", "-s", KEYCHAIN_SERVICE2, "-a", account, "-w"],
|
|
6264
6782
|
(error, stdout) => {
|
|
6265
6783
|
if (error) {
|
|
6266
6784
|
if (typeof error.code === "number" && error.code === 44) {
|
|
6267
|
-
|
|
6785
|
+
resolve7(null);
|
|
6268
6786
|
return;
|
|
6269
6787
|
}
|
|
6270
6788
|
reject(error);
|
|
6271
6789
|
return;
|
|
6272
6790
|
}
|
|
6273
|
-
|
|
6791
|
+
resolve7(stdout.trim() || null);
|
|
6274
6792
|
}
|
|
6275
6793
|
);
|
|
6276
6794
|
});
|
|
6277
6795
|
}
|
|
6278
6796
|
function deleteOpsToken(account) {
|
|
6279
6797
|
assertBatchSafe("Keychain account", account);
|
|
6280
|
-
return new Promise((
|
|
6798
|
+
return new Promise((resolve7, reject) => {
|
|
6281
6799
|
execFile(
|
|
6282
6800
|
"security",
|
|
6283
6801
|
["delete-generic-password", "-s", KEYCHAIN_SERVICE2, "-a", account],
|
|
@@ -6286,7 +6804,7 @@ function deleteOpsToken(account) {
|
|
|
6286
6804
|
reject(error);
|
|
6287
6805
|
return;
|
|
6288
6806
|
}
|
|
6289
|
-
|
|
6807
|
+
resolve7();
|
|
6290
6808
|
}
|
|
6291
6809
|
);
|
|
6292
6810
|
});
|
|
@@ -6374,14 +6892,14 @@ import prompts9 from "prompts";
|
|
|
6374
6892
|
import chalk34 from "chalk";
|
|
6375
6893
|
|
|
6376
6894
|
// src/utils/ops/auth-crypto.ts
|
|
6377
|
-
import { createRequire } from "module";
|
|
6378
|
-
import { join as
|
|
6379
|
-
import { pathToFileURL as
|
|
6895
|
+
import { createRequire as createRequire3 } from "module";
|
|
6896
|
+
import { join as join33 } from "path";
|
|
6897
|
+
import { pathToFileURL as pathToFileURL5 } from "url";
|
|
6380
6898
|
var CRYPTO_ENTRY_SINCE = "0.3.0-beta.2";
|
|
6381
6899
|
var CRYPTO_ENTRY = "@spfn/auth/crypto";
|
|
6382
6900
|
async function importFromApp(specifier) {
|
|
6383
|
-
const requireFromApp =
|
|
6384
|
-
return await import(
|
|
6901
|
+
const requireFromApp = createRequire3(join33(process.cwd(), "noop.js"));
|
|
6902
|
+
return await import(pathToFileURL5(requireFromApp.resolve(specifier)).href);
|
|
6385
6903
|
}
|
|
6386
6904
|
async function loadAuthCrypto() {
|
|
6387
6905
|
try {
|
|
@@ -6786,23 +7304,15 @@ import chalk37 from "chalk";
|
|
|
6786
7304
|
|
|
6787
7305
|
// src/commands/secret/options.ts
|
|
6788
7306
|
init_logger();
|
|
6789
|
-
function resolveEnv(env9) {
|
|
6790
|
-
const value = env9 ?? "local";
|
|
6791
|
-
if (!VALID_ENVS.includes(value)) {
|
|
6792
|
-
logger.error(`Invalid environment: "${value}". Valid values: ${VALID_ENVS.join(", ")}`);
|
|
6793
|
-
process.exit(1);
|
|
6794
|
-
}
|
|
6795
|
-
return value;
|
|
6796
|
-
}
|
|
6797
7307
|
|
|
6798
7308
|
// src/commands/secret/store-value.ts
|
|
6799
7309
|
init_logger();
|
|
6800
|
-
import { join as
|
|
7310
|
+
import { join as join35 } from "path";
|
|
6801
7311
|
init_env_file();
|
|
6802
7312
|
|
|
6803
7313
|
// src/utils/sops.ts
|
|
6804
7314
|
import { execa as execa11 } from "execa";
|
|
6805
|
-
import { existsSync as
|
|
7315
|
+
import { existsSync as existsSync32, mkdirSync as mkdirSync5, readFileSync as readFileSync17, writeFileSync as writeFileSync19 } from "fs";
|
|
6806
7316
|
import { dirname as dirname5 } from "path";
|
|
6807
7317
|
async function ensureSopsInstalled() {
|
|
6808
7318
|
try {
|
|
@@ -6812,53 +7322,89 @@ async function ensureSopsInstalled() {
|
|
|
6812
7322
|
}
|
|
6813
7323
|
}
|
|
6814
7324
|
async function sopsDecrypt(absFile) {
|
|
6815
|
-
if (!
|
|
7325
|
+
if (!existsSync32(absFile)) {
|
|
6816
7326
|
return {};
|
|
6817
7327
|
}
|
|
6818
|
-
const { stdout } = await execa11("sops", ["--decrypt", "--output-type", "json", absFile])
|
|
6819
|
-
|
|
7328
|
+
const { stdout } = await execa11("sops", ["--decrypt", "--output-type", "json", absFile]).catch((error) => {
|
|
7329
|
+
const exitCode = error.exitCode ?? "unknown";
|
|
7330
|
+
return Promise.reject(new Error(
|
|
7331
|
+
`sops could not decrypt ${absFile} (exit code ${exitCode}). Check that this machine has key access for the backend .sops.yaml selects; \`sops --decrypt ${absFile} > /dev/null\` shows sops' reason.`
|
|
7332
|
+
));
|
|
7333
|
+
});
|
|
7334
|
+
return toStringRecord(absFile, parseJsonObject(absFile, stdout));
|
|
7335
|
+
}
|
|
7336
|
+
function sopsKeyNames(absFile) {
|
|
7337
|
+
if (!existsSync32(absFile)) {
|
|
7338
|
+
return [];
|
|
7339
|
+
}
|
|
7340
|
+
return Object.keys(parseJsonObject(absFile, readFileSync17(absFile, "utf-8"))).filter((key) => key !== "sops");
|
|
7341
|
+
}
|
|
7342
|
+
function parseJsonObject(absFile, text) {
|
|
7343
|
+
let parsed;
|
|
7344
|
+
try {
|
|
7345
|
+
parsed = JSON.parse(text);
|
|
7346
|
+
} catch {
|
|
7347
|
+
parsed = void 0;
|
|
7348
|
+
}
|
|
7349
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
7350
|
+
throw new Error(`${absFile} is not a flat JSON object of secrets.`);
|
|
7351
|
+
}
|
|
7352
|
+
return parsed;
|
|
7353
|
+
}
|
|
7354
|
+
function toStringRecord(absFile, parsed) {
|
|
7355
|
+
const record = {};
|
|
7356
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
7357
|
+
if (typeof value === "object" && value !== null) {
|
|
7358
|
+
throw new Error(`${absFile}: ${key} holds a nested value; secret files are flat KEY \u2192 string maps.`);
|
|
7359
|
+
}
|
|
7360
|
+
record[key] = String(value);
|
|
7361
|
+
}
|
|
7362
|
+
return record;
|
|
6820
7363
|
}
|
|
6821
7364
|
async function sopsSetValue(absFile, relFile, key, value) {
|
|
6822
|
-
if (
|
|
6823
|
-
await execa11("sops", ["set", absFile, `["${key}"]
|
|
7365
|
+
if (existsSync32(absFile)) {
|
|
7366
|
+
await execa11("sops", ["set", "--value-stdin", absFile, `["${key}"]`], { input: JSON.stringify(value) }).catch((error) => Promise.reject(sopsWriteError(relFile, key, error)));
|
|
6824
7367
|
return;
|
|
6825
7368
|
}
|
|
6826
7369
|
mkdirSync5(dirname5(absFile), { recursive: true });
|
|
6827
7370
|
const { stdout } = await execa11(
|
|
6828
7371
|
"sops",
|
|
6829
7372
|
[
|
|
6830
|
-
"
|
|
7373
|
+
"encrypt",
|
|
6831
7374
|
"--input-type",
|
|
6832
7375
|
"json",
|
|
6833
7376
|
"--output-type",
|
|
6834
7377
|
"json",
|
|
6835
7378
|
"--filename-override",
|
|
6836
|
-
relFile
|
|
6837
|
-
"/dev/stdin"
|
|
7379
|
+
relFile
|
|
6838
7380
|
],
|
|
6839
7381
|
{ input: JSON.stringify({ [key]: value }) }
|
|
6840
|
-
);
|
|
7382
|
+
).catch((error) => Promise.reject(sopsWriteError(relFile, key, error)));
|
|
6841
7383
|
writeFileSync19(absFile, stdout);
|
|
6842
7384
|
}
|
|
7385
|
+
function sopsWriteError(relFile, key, error) {
|
|
7386
|
+
const exitCode = error.exitCode ?? "unknown";
|
|
7387
|
+
return new Error(`sops could not write ${key} to ${relFile} (exit code ${exitCode}). Check .sops.yaml has a creation rule for the file and this machine has key access.`);
|
|
7388
|
+
}
|
|
6843
7389
|
async function sopsUpdateKeys(absFile) {
|
|
6844
7390
|
await execa11("sops", ["updatekeys", "-y", absFile]);
|
|
6845
7391
|
}
|
|
6846
7392
|
|
|
6847
7393
|
// src/utils/secret-config.ts
|
|
6848
|
-
import { existsSync as
|
|
6849
|
-
import { join as
|
|
7394
|
+
import { existsSync as existsSync33, readdirSync as readdirSync3 } from "fs";
|
|
7395
|
+
import { join as join34 } from "path";
|
|
6850
7396
|
var SECRETS_DIR = "secrets";
|
|
6851
|
-
function getSopsFile(cwd, env9) {
|
|
6852
|
-
const relFile = `${SECRETS_DIR}/${env9}.enc.json`;
|
|
6853
|
-
return { absFile:
|
|
7397
|
+
function getSopsFile(cwd, env9, instance) {
|
|
7398
|
+
const relFile = `${SECRETS_DIR}/${instance ? `${env9}.${instance}` : env9}.enc.json`;
|
|
7399
|
+
return { absFile: join34(cwd, relFile), relFile };
|
|
6854
7400
|
}
|
|
6855
7401
|
function findUp(cwd, filename, maxDepth = 6) {
|
|
6856
7402
|
let dir = cwd;
|
|
6857
7403
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
6858
|
-
if (
|
|
7404
|
+
if (existsSync33(join34(dir, filename))) {
|
|
6859
7405
|
return dir;
|
|
6860
7406
|
}
|
|
6861
|
-
const parent =
|
|
7407
|
+
const parent = join34(dir, "..");
|
|
6862
7408
|
if (parent === dir) {
|
|
6863
7409
|
break;
|
|
6864
7410
|
}
|
|
@@ -6868,17 +7414,17 @@ function findUp(cwd, filename, maxDepth = 6) {
|
|
|
6868
7414
|
}
|
|
6869
7415
|
function findSopsConfig(cwd) {
|
|
6870
7416
|
const dir = findUp(cwd, ".sops.yaml");
|
|
6871
|
-
return dir ?
|
|
7417
|
+
return dir ? join34(dir, ".sops.yaml") : null;
|
|
6872
7418
|
}
|
|
6873
7419
|
function hasSopsConfig(cwd) {
|
|
6874
7420
|
return findSopsConfig(cwd) !== null;
|
|
6875
7421
|
}
|
|
6876
7422
|
function listSopsFiles(cwd) {
|
|
6877
|
-
const dir =
|
|
6878
|
-
if (!
|
|
7423
|
+
const dir = join34(cwd, SECRETS_DIR);
|
|
7424
|
+
if (!existsSync33(dir)) {
|
|
6879
7425
|
return [];
|
|
6880
7426
|
}
|
|
6881
|
-
return
|
|
7427
|
+
return readdirSync3(dir).filter((name) => name.endsWith(".enc.json")).map((name) => join34(dir, name));
|
|
6882
7428
|
}
|
|
6883
7429
|
|
|
6884
7430
|
// src/commands/secret/store-value.ts
|
|
@@ -6891,15 +7437,15 @@ function assertValidKey(key) {
|
|
|
6891
7437
|
throw new Error(`Invalid secret key "${key}". Use an env-var name: letters, digits, and underscores, not starting with a digit.`);
|
|
6892
7438
|
}
|
|
6893
7439
|
}
|
|
6894
|
-
async function describeTarget(
|
|
6895
|
-
if (isLocalEnv(
|
|
7440
|
+
async function describeTarget(target) {
|
|
7441
|
+
if (isLocalEnv(target.env)) {
|
|
6896
7442
|
return detectStore().label;
|
|
6897
7443
|
}
|
|
6898
|
-
return `SOPS (${getSopsFile(process.cwd(),
|
|
7444
|
+
return `SOPS (${getSopsFile(process.cwd(), target.env, target.instance).relFile})`;
|
|
6899
7445
|
}
|
|
6900
|
-
async function storeSecret(cwd,
|
|
7446
|
+
async function storeSecret(cwd, target, key, value) {
|
|
6901
7447
|
assertValidKey(key);
|
|
6902
|
-
if (isLocalEnv(
|
|
7448
|
+
if (isLocalEnv(target.env)) {
|
|
6903
7449
|
const store = detectStore();
|
|
6904
7450
|
if (!await store.isAvailable()) {
|
|
6905
7451
|
throw new Error(
|
|
@@ -6907,7 +7453,7 @@ async function storeSecret(cwd, env9, key, value) {
|
|
|
6907
7453
|
);
|
|
6908
7454
|
}
|
|
6909
7455
|
await store.set(keychainName(key), value);
|
|
6910
|
-
const serverEnvPath =
|
|
7456
|
+
const serverEnvPath = join35(cwd, ".env.server");
|
|
6911
7457
|
const result = upsertEnvVar(serverEnvPath, key, keychainRef(key));
|
|
6912
7458
|
restrictEnvFilePerms(serverEnvPath);
|
|
6913
7459
|
ensureGitignored(cwd, [{ pattern: ".env.server", comment: "spfn server env (secrets)" }]);
|
|
@@ -6920,45 +7466,124 @@ async function storeSecret(cwd, env9, key, value) {
|
|
|
6920
7466
|
".sops.yaml not found \u2014 add a backend (run `spfn secret keygen` for age, or a KMS rule) so the file can be decrypted on deploy."
|
|
6921
7467
|
);
|
|
6922
7468
|
}
|
|
6923
|
-
const { absFile, relFile } = getSopsFile(cwd,
|
|
7469
|
+
const { absFile, relFile } = getSopsFile(cwd, target.env, target.instance);
|
|
6924
7470
|
await sopsSetValue(absFile, relFile, key, value);
|
|
6925
7471
|
logger.success(`Set ${key} in ${relFile} (encrypted). Commit and deploy to apply.`);
|
|
6926
7472
|
}
|
|
6927
7473
|
|
|
7474
|
+
// src/commands/secret/options.ts
|
|
7475
|
+
var INSTANCE_PATTERN = /^[a-z0-9-]+$/;
|
|
7476
|
+
async function loadSecretList(options) {
|
|
7477
|
+
return await loadEnvList(options).catch((error) => {
|
|
7478
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
7479
|
+
return process.exit(1);
|
|
7480
|
+
});
|
|
7481
|
+
}
|
|
7482
|
+
function resolveEnv(env9) {
|
|
7483
|
+
const value = env9 ?? "local";
|
|
7484
|
+
if (!VALID_ENVS.includes(value)) {
|
|
7485
|
+
logger.error(`Invalid environment: "${value}". Valid values: ${VALID_ENVS.join(", ")}`);
|
|
7486
|
+
process.exit(1);
|
|
7487
|
+
}
|
|
7488
|
+
return value;
|
|
7489
|
+
}
|
|
7490
|
+
function isValidInstance(instance) {
|
|
7491
|
+
return INSTANCE_PATTERN.test(instance);
|
|
7492
|
+
}
|
|
7493
|
+
function resolveTarget(options) {
|
|
7494
|
+
const env9 = resolveEnv(options.env);
|
|
7495
|
+
const instance = options.instance;
|
|
7496
|
+
if (instance === void 0) {
|
|
7497
|
+
return { env: env9 };
|
|
7498
|
+
}
|
|
7499
|
+
if (!isValidInstance(instance)) {
|
|
7500
|
+
logger.error(`Invalid instance: "${instance}". Use lowercase letters, digits and dashes.`);
|
|
7501
|
+
process.exit(1);
|
|
7502
|
+
}
|
|
7503
|
+
if (isLocalEnv(env9)) {
|
|
7504
|
+
logger.error("--instance applies to a deployed environment; pass --env too.");
|
|
7505
|
+
process.exit(1);
|
|
7506
|
+
}
|
|
7507
|
+
return { env: env9, instance };
|
|
7508
|
+
}
|
|
7509
|
+
function targetLayer(target) {
|
|
7510
|
+
if (isLocalEnv(target.env)) {
|
|
7511
|
+
return void 0;
|
|
7512
|
+
}
|
|
7513
|
+
return target.instance ? "instance" : "environment";
|
|
7514
|
+
}
|
|
7515
|
+
function entriesForTarget(entries, target) {
|
|
7516
|
+
const layer = targetLayer(target);
|
|
7517
|
+
return layer ? entries.filter((entry) => layerOf(entry) === layer) : entries;
|
|
7518
|
+
}
|
|
7519
|
+
function assertTargetLayer(entry, target) {
|
|
7520
|
+
const layer = targetLayer(target);
|
|
7521
|
+
if (!entry || !layer || layerOf(entry) === layer) {
|
|
7522
|
+
return;
|
|
7523
|
+
}
|
|
7524
|
+
const hint = layer === "environment" ? "pass --instance <name>" : "drop --instance";
|
|
7525
|
+
logger.error(`${entry.key} is declared with layer "${layerOf(entry)}" \u2014 ${hint}.`);
|
|
7526
|
+
process.exit(1);
|
|
7527
|
+
}
|
|
7528
|
+
|
|
6928
7529
|
// src/commands/secret/set.ts
|
|
6929
7530
|
async function secretSet(key, options) {
|
|
6930
|
-
const
|
|
6931
|
-
|
|
6932
|
-
|
|
7531
|
+
const target = resolveTarget(options);
|
|
7532
|
+
if (options.stdin && !key) {
|
|
7533
|
+
logger.error("--stdin needs the KEY argument.");
|
|
7534
|
+
process.exit(1);
|
|
7535
|
+
}
|
|
7536
|
+
const resolvedKey = key ?? await pickSecretKey(options);
|
|
6933
7537
|
if (!resolvedKey) {
|
|
6934
7538
|
logger.error("No secret key provided.");
|
|
6935
7539
|
process.exit(1);
|
|
6936
7540
|
}
|
|
6937
|
-
await
|
|
7541
|
+
const entry = await findEntry(options, resolvedKey);
|
|
7542
|
+
warnIfNotSecret(options, resolvedKey, entry);
|
|
7543
|
+
assertTargetLayer(entry, target);
|
|
7544
|
+
const value = options.stdin ? await readValueFromStdin(process.stdin) : await promptValue(resolvedKey, target);
|
|
7545
|
+
logger.step(`Storing ${resolvedKey} \u2192 ${await describeTarget(target)}`);
|
|
7546
|
+
try {
|
|
7547
|
+
await storeSecret(process.cwd(), target, resolvedKey, value);
|
|
7548
|
+
} catch (error) {
|
|
7549
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
7550
|
+
process.exit(1);
|
|
7551
|
+
}
|
|
7552
|
+
}
|
|
7553
|
+
async function promptValue(key, target) {
|
|
6938
7554
|
const { value } = await prompts11({
|
|
6939
7555
|
type: "password",
|
|
6940
7556
|
name: "value",
|
|
6941
|
-
message: `Value for ${chalk37.cyan(
|
|
7557
|
+
message: `Value for ${chalk37.cyan(key)} (${target.instance ? `${target.env}.${target.instance}` : target.env})`
|
|
6942
7558
|
});
|
|
6943
7559
|
if (!value) {
|
|
6944
7560
|
logger.warn("Cancelled \u2014 no value entered.");
|
|
6945
7561
|
process.exit(0);
|
|
6946
7562
|
}
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
logger.error(
|
|
7563
|
+
return value;
|
|
7564
|
+
}
|
|
7565
|
+
async function readValueFromStdin(stdin) {
|
|
7566
|
+
if ("isTTY" in stdin && stdin.isTTY) {
|
|
7567
|
+
logger.error("--stdin reads a piped value; stdin is a terminal. Pipe the value in, or drop --stdin to be prompted.");
|
|
6952
7568
|
process.exit(1);
|
|
6953
7569
|
}
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
7570
|
+
const chunks = [];
|
|
7571
|
+
for await (const chunk of stdin) {
|
|
7572
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
7573
|
+
}
|
|
7574
|
+
const value = stripTrailingNewline(Buffer.concat(chunks).toString("utf-8"));
|
|
7575
|
+
if (value.length === 0) {
|
|
7576
|
+
logger.error("--stdin received an empty value.");
|
|
7577
|
+
process.exit(1);
|
|
6961
7578
|
}
|
|
7579
|
+
return value;
|
|
7580
|
+
}
|
|
7581
|
+
function stripTrailingNewline(value) {
|
|
7582
|
+
return value.replace(/\r?\n$/, "");
|
|
7583
|
+
}
|
|
7584
|
+
async function pickSecretKey(options) {
|
|
7585
|
+
const list = await loadEnvList(options).catch(() => void 0);
|
|
7586
|
+
const entries = list ? secretEntries(list.schema) : [];
|
|
6962
7587
|
if (entries.length === 0) {
|
|
6963
7588
|
return void 0;
|
|
6964
7589
|
}
|
|
@@ -6974,14 +7599,13 @@ async function pickSecretKey(pkg) {
|
|
|
6974
7599
|
});
|
|
6975
7600
|
return key;
|
|
6976
7601
|
}
|
|
6977
|
-
async function
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
}
|
|
6984
|
-
} catch {
|
|
7602
|
+
async function findEntry(options, key) {
|
|
7603
|
+
const list = await loadEnvList(options).catch(() => void 0);
|
|
7604
|
+
return list?.schema[key];
|
|
7605
|
+
}
|
|
7606
|
+
function warnIfNotSecret(options, key, entry) {
|
|
7607
|
+
if (entry && !entry.sensitive) {
|
|
7608
|
+
logger.warn(`${key} is not marked as a secret in ${describeEnvList(options)}. Consider declaring it with envSecret().`);
|
|
6985
7609
|
}
|
|
6986
7610
|
}
|
|
6987
7611
|
|
|
@@ -6989,31 +7613,43 @@ async function warnIfNotSecret(pkg, key) {
|
|
|
6989
7613
|
init_logger();
|
|
6990
7614
|
import chalk38 from "chalk";
|
|
6991
7615
|
async function secretList(options) {
|
|
6992
|
-
const
|
|
6993
|
-
const
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
}
|
|
7001
|
-
if (entries.length === 0) {
|
|
7002
|
-
logger.info(`No secrets declared in ${pkg}.`);
|
|
7616
|
+
const target = resolveTarget(options);
|
|
7617
|
+
const label = describeEnvList(options);
|
|
7618
|
+
const list = await loadSecretList(options);
|
|
7619
|
+
const entries = entriesForTarget(secretEntries(list.schema), target);
|
|
7620
|
+
const present = await loadPresence(target, entries);
|
|
7621
|
+
const unlisted = options.package ? [] : unlistedNames(list, present);
|
|
7622
|
+
if (entries.length === 0 && unlisted.length === 0) {
|
|
7623
|
+
logger.info(`No secrets declared in ${label}.`);
|
|
7003
7624
|
return;
|
|
7004
7625
|
}
|
|
7005
|
-
const present = await loadPresence(env9, entries);
|
|
7006
7626
|
console.log(chalk38.blue.bold(`
|
|
7007
|
-
\u{1F511} Secrets (${
|
|
7627
|
+
\u{1F511} Secrets (${label}) \u2014 ${target.instance ? `${target.env}.${target.instance}` : target.env}
|
|
7008
7628
|
`));
|
|
7009
|
-
for (const
|
|
7010
|
-
|
|
7011
|
-
|
|
7629
|
+
for (const group of groupBySource(list, entries)) {
|
|
7630
|
+
console.log(` ${chalk38.bold.magenta(group.source)}`);
|
|
7631
|
+
for (const entry of group.entries) {
|
|
7632
|
+
const status = statusOf(entry, present.has(entry.key));
|
|
7633
|
+
console.log(` ${badge(status)} ${chalk38.cyan(entry.key)}${entry.generate ? chalk38.dim(" (generatable)") : ""}`);
|
|
7634
|
+
}
|
|
7012
7635
|
}
|
|
7636
|
+
printUnlisted(unlisted);
|
|
7013
7637
|
console.log();
|
|
7014
7638
|
}
|
|
7015
|
-
|
|
7016
|
-
|
|
7639
|
+
function unlistedNames(list, present) {
|
|
7640
|
+
return [...present].filter((name) => !Object.hasOwn(list.schema, name)).sort();
|
|
7641
|
+
}
|
|
7642
|
+
function printUnlisted(names) {
|
|
7643
|
+
if (names.length === 0) {
|
|
7644
|
+
return;
|
|
7645
|
+
}
|
|
7646
|
+
console.log(` ${chalk38.bold.magenta("(not in list)")}`);
|
|
7647
|
+
for (const name of names) {
|
|
7648
|
+
console.log(` ${chalk38.yellow("?")} ${chalk38.cyan(name)} ${chalk38.dim("not in list")}`);
|
|
7649
|
+
}
|
|
7650
|
+
}
|
|
7651
|
+
async function loadPresence(target, entries) {
|
|
7652
|
+
if (isLocalEnv(target.env)) {
|
|
7017
7653
|
const store = detectStore();
|
|
7018
7654
|
if (!await store.isAvailable()) {
|
|
7019
7655
|
logger.warn(`${store.label} unavailable \u2014 status shown as missing.`);
|
|
@@ -7027,10 +7663,7 @@ async function loadPresence(env9, entries) {
|
|
|
7027
7663
|
}
|
|
7028
7664
|
return present;
|
|
7029
7665
|
}
|
|
7030
|
-
|
|
7031
|
-
const { absFile } = getSopsFile(process.cwd(), env9);
|
|
7032
|
-
const decrypted = await sopsDecrypt(absFile);
|
|
7033
|
-
return new Set(Object.keys(decrypted));
|
|
7666
|
+
return new Set(sopsKeyNames(getSopsFile(process.cwd(), target.env, target.instance).absFile));
|
|
7034
7667
|
}
|
|
7035
7668
|
function statusOf(entry, present) {
|
|
7036
7669
|
if (present) {
|
|
@@ -7053,25 +7686,19 @@ function badge(status) {
|
|
|
7053
7686
|
init_logger();
|
|
7054
7687
|
import chalk39 from "chalk";
|
|
7055
7688
|
async function secretGenerate(key, options) {
|
|
7056
|
-
const
|
|
7057
|
-
const
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
schema = await loadEnvSchema(pkg);
|
|
7061
|
-
} catch (error) {
|
|
7062
|
-
logger.error(error instanceof Error ? error.message : String(error));
|
|
7063
|
-
process.exit(1);
|
|
7064
|
-
}
|
|
7065
|
-
const targets = options.all ? generatableSecrets(schema) : [requireGeneratable(schema, key)];
|
|
7689
|
+
const target = resolveTarget(options);
|
|
7690
|
+
const schema = (await loadSecretList(options)).schema;
|
|
7691
|
+
const targets = options.all ? entriesForTarget(generatableSecrets(schema), target) : [requireGeneratable(schema, key)];
|
|
7692
|
+
targets.forEach((entry) => assertTargetLayer(entry, target));
|
|
7066
7693
|
if (targets.length === 0) {
|
|
7067
7694
|
logger.warn("No generatable secrets found (none declare a `generate` strategy).");
|
|
7068
7695
|
return;
|
|
7069
7696
|
}
|
|
7070
|
-
logger.step(`Generating ${targets.length} secret(s) \u2192 ${await describeTarget(
|
|
7697
|
+
logger.step(`Generating ${targets.length} secret(s) \u2192 ${await describeTarget(target)}`);
|
|
7071
7698
|
for (const entry of targets) {
|
|
7072
7699
|
const value = generateSecretValue(entry.generate);
|
|
7073
7700
|
try {
|
|
7074
|
-
await storeSecret(process.cwd(),
|
|
7701
|
+
await storeSecret(process.cwd(), target, entry.key, value);
|
|
7075
7702
|
} catch (error) {
|
|
7076
7703
|
logger.error(`${entry.key}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7077
7704
|
process.exit(1);
|
|
@@ -7099,23 +7726,17 @@ function requireGeneratable(schema, key) {
|
|
|
7099
7726
|
init_logger();
|
|
7100
7727
|
import chalk40 from "chalk";
|
|
7101
7728
|
async function secretRotate(key, options) {
|
|
7102
|
-
const
|
|
7103
|
-
const
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
schema = await loadEnvSchema(pkg);
|
|
7107
|
-
} catch (error) {
|
|
7108
|
-
logger.error(error instanceof Error ? error.message : String(error));
|
|
7109
|
-
process.exit(1);
|
|
7110
|
-
}
|
|
7111
|
-
const targets = options.all ? secretEntries(schema) : [requireEntry(schema, key)];
|
|
7729
|
+
const target = resolveTarget(options);
|
|
7730
|
+
const schema = (await loadSecretList(options)).schema;
|
|
7731
|
+
const targets = options.all ? entriesForTarget(secretEntries(schema), target) : [requireEntry(schema, key)];
|
|
7732
|
+
targets.forEach((entry) => assertTargetLayer(entry, target));
|
|
7112
7733
|
const generatable = targets.filter((entry) => entry.generate);
|
|
7113
7734
|
const external = targets.filter((entry) => !entry.generate);
|
|
7114
7735
|
if (generatable.length > 0) {
|
|
7115
|
-
logger.step(`Rotating ${generatable.length} secret(s) \u2192 ${await describeTarget(
|
|
7736
|
+
logger.step(`Rotating ${generatable.length} secret(s) \u2192 ${await describeTarget(target)}`);
|
|
7116
7737
|
for (const entry of generatable) {
|
|
7117
7738
|
try {
|
|
7118
|
-
await storeSecret(process.cwd(),
|
|
7739
|
+
await storeSecret(process.cwd(), target, entry.key, generateSecretValue(entry.generate));
|
|
7119
7740
|
} catch (error) {
|
|
7120
7741
|
logger.error(`${entry.key}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7121
7742
|
process.exit(1);
|
|
@@ -7123,7 +7744,7 @@ async function secretRotate(key, options) {
|
|
|
7123
7744
|
}
|
|
7124
7745
|
}
|
|
7125
7746
|
for (const entry of external) {
|
|
7126
|
-
logger.warn(`${chalk40.cyan(entry.key)} is external \u2014 reissue it at the provider, then \`spfn secret set ${entry.key} --env ${
|
|
7747
|
+
logger.warn(`${chalk40.cyan(entry.key)} is external \u2014 reissue it at the provider, then \`spfn secret set ${entry.key} --env ${target.env}${target.instance ? ` --instance ${target.instance}` : ""}\`.`);
|
|
7127
7748
|
}
|
|
7128
7749
|
if (generatable.length > 0) {
|
|
7129
7750
|
logger.info("Rotation updates the source only \u2014 commit and deploy to apply.");
|
|
@@ -7145,12 +7766,12 @@ function requireEntry(schema, key) {
|
|
|
7145
7766
|
// src/commands/secret/keygen.ts
|
|
7146
7767
|
init_logger();
|
|
7147
7768
|
import { execa as execa12 } from "execa";
|
|
7148
|
-
import { existsSync as
|
|
7769
|
+
import { existsSync as existsSync34, mkdirSync as mkdirSync6 } from "fs";
|
|
7149
7770
|
import { homedir } from "os";
|
|
7150
|
-
import { dirname as dirname6, join as
|
|
7771
|
+
import { dirname as dirname6, join as join36 } from "path";
|
|
7151
7772
|
import chalk41 from "chalk";
|
|
7152
7773
|
function ageKeyFile() {
|
|
7153
|
-
return process.env.SOPS_AGE_KEY_FILE ??
|
|
7774
|
+
return process.env.SOPS_AGE_KEY_FILE ?? join36(homedir(), ".config", "sops", "age", "keys.txt");
|
|
7154
7775
|
}
|
|
7155
7776
|
async function ensureAgeInstalled() {
|
|
7156
7777
|
try {
|
|
@@ -7170,7 +7791,7 @@ async function secretKeygen() {
|
|
|
7170
7791
|
process.exit(1);
|
|
7171
7792
|
}
|
|
7172
7793
|
const keyFile = ageKeyFile();
|
|
7173
|
-
if (
|
|
7794
|
+
if (existsSync34(keyFile)) {
|
|
7174
7795
|
const { stdout } = await execa12("age-keygen", ["-y", keyFile]);
|
|
7175
7796
|
logger.warn(`age key file already exists: ${keyFile}`);
|
|
7176
7797
|
printPublicKeys(publicKeys(stdout));
|
|
@@ -7197,8 +7818,8 @@ function printPublicKeys(keys) {
|
|
|
7197
7818
|
|
|
7198
7819
|
// src/commands/secret/recipients.ts
|
|
7199
7820
|
init_logger();
|
|
7200
|
-
import { existsSync as
|
|
7201
|
-
import { join as
|
|
7821
|
+
import { existsSync as existsSync35, readFileSync as readFileSync18, writeFileSync as writeFileSync20 } from "fs";
|
|
7822
|
+
import { join as join37 } from "path";
|
|
7202
7823
|
import { parse as parse3, stringify } from "yaml";
|
|
7203
7824
|
import chalk42 from "chalk";
|
|
7204
7825
|
var SOPS_CONFIG = ".sops.yaml";
|
|
@@ -7206,7 +7827,7 @@ var DEFAULT_PATH_REGEX = "secrets/.*\\.enc\\.json$";
|
|
|
7206
7827
|
var AGE_RECIPIENT = /^age1[0-9a-z]+$/;
|
|
7207
7828
|
async function secretRecipients(action, key, _options) {
|
|
7208
7829
|
const cwd = process.cwd();
|
|
7209
|
-
const configPath =
|
|
7830
|
+
const configPath = join37(cwd, SOPS_CONFIG);
|
|
7210
7831
|
if (action === "list") {
|
|
7211
7832
|
listRecipients(configPath);
|
|
7212
7833
|
return;
|
|
@@ -7238,7 +7859,7 @@ async function secretRecipients(action, key, _options) {
|
|
|
7238
7859
|
await reencrypt(cwd);
|
|
7239
7860
|
}
|
|
7240
7861
|
function listRecipients(configPath) {
|
|
7241
|
-
if (!
|
|
7862
|
+
if (!existsSync35(configPath)) {
|
|
7242
7863
|
logger.info(`No ${SOPS_CONFIG} found.`);
|
|
7243
7864
|
return;
|
|
7244
7865
|
}
|
|
@@ -7256,10 +7877,10 @@ function listRecipients(configPath) {
|
|
|
7256
7877
|
console.log();
|
|
7257
7878
|
}
|
|
7258
7879
|
function loadConfig(configPath) {
|
|
7259
|
-
if (!
|
|
7880
|
+
if (!existsSync35(configPath)) {
|
|
7260
7881
|
return { creation_rules: [] };
|
|
7261
7882
|
}
|
|
7262
|
-
return parse3(
|
|
7883
|
+
return parse3(readFileSync18(configPath, "utf-8")) ?? { creation_rules: [] };
|
|
7263
7884
|
}
|
|
7264
7885
|
function ensureRule(config) {
|
|
7265
7886
|
if (!config.creation_rules) {
|
|
@@ -7290,42 +7911,55 @@ async function reencrypt(cwd) {
|
|
|
7290
7911
|
}
|
|
7291
7912
|
|
|
7292
7913
|
// src/commands/secret/check.ts
|
|
7293
|
-
|
|
7294
|
-
import { join as join33 } from "path";
|
|
7914
|
+
import { join as join38 } from "path";
|
|
7295
7915
|
import chalk43 from "chalk";
|
|
7296
7916
|
init_env_file();
|
|
7297
7917
|
var COMMITTED_FILES = [".env", ".env.example", ".env.local.example", ".env.server.example"];
|
|
7298
7918
|
var PLACEHOLDER = /(your-|changeme|placeholder|example|<.*>)/i;
|
|
7299
7919
|
async function secretCheck(options) {
|
|
7300
7920
|
const cwd = process.cwd();
|
|
7301
|
-
const
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
entries = secretEntries(await loadEnvSchema(pkg));
|
|
7305
|
-
} catch (error) {
|
|
7306
|
-
logger.error(error instanceof Error ? error.message : String(error));
|
|
7307
|
-
process.exit(1);
|
|
7308
|
-
}
|
|
7921
|
+
const target = resolveTarget(options);
|
|
7922
|
+
const schema = (await loadSecretList(options)).schema;
|
|
7923
|
+
const entries = secretEntries(schema);
|
|
7309
7924
|
const secretKeys = new Set(entries.map((entry) => entry.key));
|
|
7310
7925
|
const issues = [];
|
|
7311
7926
|
const warnings = [];
|
|
7312
7927
|
for (const file of COMMITTED_FILES) {
|
|
7313
|
-
const parsed = parseEnvFile(
|
|
7928
|
+
const parsed = parseEnvFile(join38(cwd, file));
|
|
7314
7929
|
for (const [key, value] of Object.entries(parsed)) {
|
|
7315
7930
|
if (secretKeys.has(key) && value.length > 0 && !PLACEHOLDER.test(value)) {
|
|
7316
7931
|
issues.push(`${chalk43.cyan(key)} has a real value in committed ${chalk43.yellow(file)} \u2014 move it to the keychain/SOPS.`);
|
|
7317
7932
|
}
|
|
7318
7933
|
}
|
|
7319
7934
|
}
|
|
7320
|
-
const serverEnv = parseEnvFile(
|
|
7935
|
+
const serverEnv = parseEnvFile(join38(cwd, ".env.server"));
|
|
7321
7936
|
for (const key of secretKeys) {
|
|
7322
7937
|
const value = serverEnv[key];
|
|
7323
7938
|
if (value && !value.startsWith(KEYCHAIN_REF_PREFIX)) {
|
|
7324
7939
|
warnings.push(`${chalk43.cyan(key)} is plaintext in .env.server \u2014 run \`spfn secret set ${key}\` to move it to the keychain.`);
|
|
7325
7940
|
}
|
|
7326
7941
|
}
|
|
7942
|
+
if (!isLocalEnv(target.env)) {
|
|
7943
|
+
checkPlacement(cwd, schema, target, issues, warnings);
|
|
7944
|
+
}
|
|
7327
7945
|
report(issues, warnings, cwd);
|
|
7328
7946
|
}
|
|
7947
|
+
function checkPlacement(cwd, schema, target, issues, warnings) {
|
|
7948
|
+
const files = [{ file: getSopsFile(cwd, target.env), layer: "environment" }];
|
|
7949
|
+
if (target.instance) {
|
|
7950
|
+
files.push({ file: getSopsFile(cwd, target.env, target.instance), layer: "instance" });
|
|
7951
|
+
}
|
|
7952
|
+
for (const { file, layer } of files) {
|
|
7953
|
+
for (const name of sopsKeyNames(file.absFile)) {
|
|
7954
|
+
const entry = Object.hasOwn(schema, name) ? schema[name] : void 0;
|
|
7955
|
+
if (!entry) {
|
|
7956
|
+
warnings.push(`${chalk43.cyan(name)} in ${chalk43.yellow(file.relFile)} is not in the env list \u2014 export skips it.`);
|
|
7957
|
+
} else if (layerOf(entry) !== layer) {
|
|
7958
|
+
issues.push(`${chalk43.cyan(name)} is declared with layer "${layerOf(entry)}" but is in ${chalk43.yellow(file.relFile)}.`);
|
|
7959
|
+
}
|
|
7960
|
+
}
|
|
7961
|
+
}
|
|
7962
|
+
}
|
|
7329
7963
|
function report(issues, warnings, cwd) {
|
|
7330
7964
|
console.log(chalk43.blue.bold("\n\u{1F50D} Secret hygiene check\n"));
|
|
7331
7965
|
for (const issue of issues) {
|
|
@@ -7346,26 +7980,229 @@ function report(issues, warnings, cwd) {
|
|
|
7346
7980
|
}
|
|
7347
7981
|
}
|
|
7348
7982
|
|
|
7983
|
+
// src/commands/secret/export.ts
|
|
7984
|
+
init_logger();
|
|
7985
|
+
init_env_file();
|
|
7986
|
+
import { existsSync as existsSync36, statSync } from "fs";
|
|
7987
|
+
import { dirname as dirname8, resolve as resolve5 } from "path";
|
|
7988
|
+
import chalk44 from "chalk";
|
|
7989
|
+
|
|
7990
|
+
// src/utils/secret-layers.ts
|
|
7991
|
+
function mergeLayers(schema, sources) {
|
|
7992
|
+
const merge = { values: {}, origins: {}, errors: [], warnings: [] };
|
|
7993
|
+
for (const [name, found] of collectListed(schema, sources, merge.warnings)) {
|
|
7994
|
+
const reason = placementError(schema[name], found) ?? valueError(schema[name], found[0].values[name]);
|
|
7995
|
+
if (reason) {
|
|
7996
|
+
merge.errors.push(`${name}: ${reason}`);
|
|
7997
|
+
continue;
|
|
7998
|
+
}
|
|
7999
|
+
merge.values[name] = found[0].values[name];
|
|
8000
|
+
merge.origins[name] = found[0].label;
|
|
8001
|
+
}
|
|
8002
|
+
for (const entry of Object.values(schema)) {
|
|
8003
|
+
if (isRequired(entry) && !sources.some((source) => Object.hasOwn(source.values, entry.key))) {
|
|
8004
|
+
merge.errors.push(`${entry.key}: required, and in none of the layers`);
|
|
8005
|
+
}
|
|
8006
|
+
}
|
|
8007
|
+
return merge;
|
|
8008
|
+
}
|
|
8009
|
+
function collectListed(schema, sources, warnings) {
|
|
8010
|
+
const found = /* @__PURE__ */ new Map();
|
|
8011
|
+
for (const source of sources) {
|
|
8012
|
+
for (const name of Object.keys(source.values)) {
|
|
8013
|
+
if (!Object.hasOwn(schema, name)) {
|
|
8014
|
+
warnings.push(`${name} (in ${source.label}) is not in the env list \u2014 not written`);
|
|
8015
|
+
continue;
|
|
8016
|
+
}
|
|
8017
|
+
found.set(name, [...found.get(name) ?? [], source]);
|
|
8018
|
+
}
|
|
8019
|
+
}
|
|
8020
|
+
return found;
|
|
8021
|
+
}
|
|
8022
|
+
function placementError(entry, found) {
|
|
8023
|
+
if (found.length > 1) {
|
|
8024
|
+
return `in more than one layer: ${found.map((source) => source.label).join(" and ")}`;
|
|
8025
|
+
}
|
|
8026
|
+
if (found[0].kind !== layerOf(entry)) {
|
|
8027
|
+
return `declared with layer "${layerOf(entry)}", but found in ${found[0].label}`;
|
|
8028
|
+
}
|
|
8029
|
+
return void 0;
|
|
8030
|
+
}
|
|
8031
|
+
function valueError(entry, value) {
|
|
8032
|
+
if (entry.minLength !== void 0 && value.length < entry.minLength) {
|
|
8033
|
+
return `must be at least ${entry.minLength} characters long`;
|
|
8034
|
+
}
|
|
8035
|
+
if (!entry.validator) {
|
|
8036
|
+
return void 0;
|
|
8037
|
+
}
|
|
8038
|
+
try {
|
|
8039
|
+
entry.validator(value);
|
|
8040
|
+
return void 0;
|
|
8041
|
+
} catch (error) {
|
|
8042
|
+
return withoutValue(error instanceof Error ? error.message : String(error), value);
|
|
8043
|
+
}
|
|
8044
|
+
}
|
|
8045
|
+
function withoutValue(reason, value) {
|
|
8046
|
+
if (value.length > 0 && reason.includes(value)) {
|
|
8047
|
+
return "rejected by its validator (the message is withheld because it contains the value)";
|
|
8048
|
+
}
|
|
8049
|
+
return reason;
|
|
8050
|
+
}
|
|
8051
|
+
function isRequired(entry) {
|
|
8052
|
+
return entry.required === true && entry.default === void 0;
|
|
8053
|
+
}
|
|
8054
|
+
|
|
8055
|
+
// src/utils/dotenv-out.ts
|
|
8056
|
+
import { closeSync, fsyncSync, openSync, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync21 } from "fs";
|
|
8057
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
8058
|
+
import { basename as basename2, dirname as dirname7, join as join39 } from "path";
|
|
8059
|
+
import { parse as parse4 } from "dotenv";
|
|
8060
|
+
var QUOTES = ["'", "`", '"'];
|
|
8061
|
+
function formatDotenv(values) {
|
|
8062
|
+
const unrepresentable = [];
|
|
8063
|
+
const lines = Object.entries(values).map(([name, value]) => {
|
|
8064
|
+
const line = dotenvLine(name, value);
|
|
8065
|
+
if (!line) {
|
|
8066
|
+
unrepresentable.push(name);
|
|
8067
|
+
}
|
|
8068
|
+
return line;
|
|
8069
|
+
});
|
|
8070
|
+
const content = lines.join("\n") + "\n";
|
|
8071
|
+
const mismatched = Object.keys(values).filter((name) => parse4(content)[name] !== values[name]);
|
|
8072
|
+
const failed2 = [.../* @__PURE__ */ new Set([...unrepresentable, ...mismatched])];
|
|
8073
|
+
if (failed2.length > 0) {
|
|
8074
|
+
throw new Error(`Cannot write as dotenv without changing the value: ${failed2.join(", ")}`);
|
|
8075
|
+
}
|
|
8076
|
+
return content;
|
|
8077
|
+
}
|
|
8078
|
+
function dotenvLine(name, value) {
|
|
8079
|
+
const quoted = value.endsWith("\\") ? [] : QUOTES.map((quote) => `${quote}${value}${quote}`);
|
|
8080
|
+
return [...quoted, value].map((form) => `${name}=${form}`).find((line) => parse4(line)[name] === value);
|
|
8081
|
+
}
|
|
8082
|
+
function writePrivateFileAtomic(path5, content) {
|
|
8083
|
+
const temp = join39(dirname7(path5), `.${basename2(path5)}.${randomBytes3(6).toString("hex")}.tmp`);
|
|
8084
|
+
const fd = openSync(temp, "wx", 384);
|
|
8085
|
+
try {
|
|
8086
|
+
writeAndClose(fd, content);
|
|
8087
|
+
renameSync2(temp, path5);
|
|
8088
|
+
} finally {
|
|
8089
|
+
rmSync2(temp, { force: true });
|
|
8090
|
+
}
|
|
8091
|
+
}
|
|
8092
|
+
function writeAndClose(fd, content) {
|
|
8093
|
+
try {
|
|
8094
|
+
writeFileSync21(fd, content);
|
|
8095
|
+
fsyncSync(fd);
|
|
8096
|
+
} finally {
|
|
8097
|
+
closeSync(fd);
|
|
8098
|
+
}
|
|
8099
|
+
}
|
|
8100
|
+
|
|
8101
|
+
// src/commands/secret/export.ts
|
|
8102
|
+
async function secretExport(options) {
|
|
8103
|
+
const target = resolveTarget(options);
|
|
8104
|
+
if (!options.out) {
|
|
8105
|
+
logger.error("--out <file> is required: export writes values to a file, never to the terminal.");
|
|
8106
|
+
process.exit(1);
|
|
8107
|
+
}
|
|
8108
|
+
if (isLocalEnv(target.env)) {
|
|
8109
|
+
logger.error("export reads the encrypted files of a deployed environment; pass --env <env>.");
|
|
8110
|
+
process.exit(1);
|
|
8111
|
+
}
|
|
8112
|
+
await ensureSopsInstalled();
|
|
8113
|
+
const request2 = {
|
|
8114
|
+
cwd: process.cwd(),
|
|
8115
|
+
target,
|
|
8116
|
+
schema: (await loadSecretList({})).schema,
|
|
8117
|
+
withFiles: options.with ?? [],
|
|
8118
|
+
out: options.out
|
|
8119
|
+
};
|
|
8120
|
+
const report2 = await exportEnvFile(request2).catch((error) => {
|
|
8121
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
8122
|
+
return process.exit(1);
|
|
8123
|
+
});
|
|
8124
|
+
printReport2(report2, request2.out);
|
|
8125
|
+
}
|
|
8126
|
+
async function exportEnvFile(request2) {
|
|
8127
|
+
const out = resolve5(request2.cwd, request2.out);
|
|
8128
|
+
assertDirectory(dirname8(out), "--out");
|
|
8129
|
+
const merge = mergeLayers(request2.schema, await readLayers(request2));
|
|
8130
|
+
const report2 = { written: false, origins: merge.origins, errors: merge.errors, warnings: merge.warnings };
|
|
8131
|
+
if (merge.errors.length > 0) {
|
|
8132
|
+
return report2;
|
|
8133
|
+
}
|
|
8134
|
+
writePrivateFileAtomic(out, formatDotenv(merge.values));
|
|
8135
|
+
return { ...report2, written: true };
|
|
8136
|
+
}
|
|
8137
|
+
async function readLayers(request2) {
|
|
8138
|
+
const { cwd, target } = request2;
|
|
8139
|
+
const withFiles = request2.withFiles.map((file) => readWithFile(cwd, file));
|
|
8140
|
+
const sources = [{
|
|
8141
|
+
label: "environment",
|
|
8142
|
+
kind: "environment",
|
|
8143
|
+
values: await sopsDecrypt(getSopsFile(cwd, target.env).absFile)
|
|
8144
|
+
}];
|
|
8145
|
+
if (target.instance) {
|
|
8146
|
+
sources.push({
|
|
8147
|
+
label: "instance",
|
|
8148
|
+
kind: "instance",
|
|
8149
|
+
values: await sopsDecrypt(getSopsFile(cwd, target.env, target.instance).absFile)
|
|
8150
|
+
});
|
|
8151
|
+
}
|
|
8152
|
+
return [...sources, ...withFiles];
|
|
8153
|
+
}
|
|
8154
|
+
function readWithFile(cwd, file) {
|
|
8155
|
+
const path5 = resolve5(cwd, file);
|
|
8156
|
+
if (!existsSync36(path5)) {
|
|
8157
|
+
throw new Error(`--with file not found: ${path5}`);
|
|
8158
|
+
}
|
|
8159
|
+
return { label: `--with ${file}`, kind: "instance", values: parseEnvFile(path5) };
|
|
8160
|
+
}
|
|
8161
|
+
function assertDirectory(path5, option) {
|
|
8162
|
+
if (!existsSync36(path5) || !statSync(path5).isDirectory()) {
|
|
8163
|
+
throw new Error(`${option}: directory ${path5} does not exist. Create it first.`);
|
|
8164
|
+
}
|
|
8165
|
+
}
|
|
8166
|
+
function printReport2(report2, out) {
|
|
8167
|
+
for (const warning of report2.warnings) {
|
|
8168
|
+
logger.warn(warning);
|
|
8169
|
+
}
|
|
8170
|
+
if (!report2.written) {
|
|
8171
|
+
logger.error(`Nothing written \u2014 ${report2.errors.length} problem(s):`);
|
|
8172
|
+
report2.errors.forEach((error) => console.log(` ${chalk44.red("\u2717")} ${error}`));
|
|
8173
|
+
process.exit(1);
|
|
8174
|
+
}
|
|
8175
|
+
for (const [name, layer] of Object.entries(report2.origins)) {
|
|
8176
|
+
console.log(` ${chalk44.cyan(name)} ${chalk44.dim(`\u2190 ${layer}`)}`);
|
|
8177
|
+
}
|
|
8178
|
+
logger.success(`Wrote ${Object.keys(report2.origins).length} name(s) to ${out} (mode 0600).`);
|
|
8179
|
+
}
|
|
8180
|
+
|
|
7349
8181
|
// src/commands/secret/index.ts
|
|
7350
8182
|
var ENV_OPTION = ["-e, --env <env>", "Target environment (local | development | staging | production)", "local"];
|
|
7351
|
-
var
|
|
8183
|
+
var INSTANCE_OPTION = ["-i, --instance <name>", "Target one instance of the environment: secrets/<env>.<instance>.enc.json (lowercase letters, digits, dashes)"];
|
|
8184
|
+
var PKG_OPTION = ["-p, --package <package>", "Read only this package's env schema (default: the whole app \u2014 spfn.config.js env.schemas + installed @spfn/* packages)"];
|
|
7352
8185
|
var secretCommand = new Command16("secret").description("Manage secrets: keychain locally, SOPS (age / GCP KMS / AWS KMS) for deployed environments");
|
|
7353
|
-
secretCommand.command("set [key]").description("Store a secret value (prompts for the value, masked)").option(...ENV_OPTION).option(...PKG_OPTION).action(secretSet);
|
|
7354
|
-
secretCommand.command("list").description("List declared secrets and their status (never prints values)").option(...ENV_OPTION).option(...PKG_OPTION).action(secretList);
|
|
7355
|
-
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);
|
|
7356
|
-
secretCommand.command("rotate [key]").description("Rotate secret value(s); external secrets are flagged for manual reissue").option("-a, --all", "Rotate every secret").option(...ENV_OPTION).option(...PKG_OPTION).action(secretRotate);
|
|
8186
|
+
secretCommand.command("set [key]").description("Store a secret value (prompts for the value, masked; --stdin reads it from a pipe)").option(...ENV_OPTION).option(...INSTANCE_OPTION).option("--stdin", "Read the value from stdin (one trailing newline dropped); never prompts").option(...PKG_OPTION).action(secretSet);
|
|
8187
|
+
secretCommand.command("list").description("List declared secrets and their status (never prints values)").option(...ENV_OPTION).option(...INSTANCE_OPTION).option(...PKG_OPTION).action(secretList);
|
|
8188
|
+
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(...INSTANCE_OPTION).option(...PKG_OPTION).action(secretGenerate);
|
|
8189
|
+
secretCommand.command("rotate [key]").description("Rotate secret value(s); external secrets are flagged for manual reissue").option("-a, --all", "Rotate every secret").option(...ENV_OPTION).option(...INSTANCE_OPTION).option(...PKG_OPTION).action(secretRotate);
|
|
7357
8190
|
secretCommand.command("keygen").description("Generate an age key pair for SOPS (no-cloud backend)").action(secretKeygen);
|
|
7358
8191
|
secretCommand.command("recipients <action> [key]").description("Manage .sops.yaml age recipients: add | remove | list").action(secretRecipients);
|
|
7359
|
-
secretCommand.command("check").description("Static hygiene lint \u2014 flag plaintext secret leaks").option(...PKG_OPTION).action(secretCheck);
|
|
8192
|
+
secretCommand.command("check").description("Static hygiene lint \u2014 flag plaintext secret leaks; with a deployed --env, names in the wrong layer").option(...ENV_OPTION).option(...INSTANCE_OPTION).option(...PKG_OPTION).action(secretCheck);
|
|
8193
|
+
secretCommand.command("export").description("Decrypt and merge a deployment's layers into a 0600 dotenv file (prints names only)").requiredOption("-e, --env <env>", "Deployed environment (development | staging | production | test)").option(...INSTANCE_OPTION).option("-w, --with <file>", "Plaintext dotenv file of computed instance values (repeatable)", collect, []).option("-o, --out <file>", "Dotenv file to write (required; its directory must exist)").action(secretExport);
|
|
8194
|
+
function collect(value, previous) {
|
|
8195
|
+
return [...previous, value];
|
|
8196
|
+
}
|
|
7360
8197
|
|
|
7361
8198
|
// src/commands/cloud/index.ts
|
|
7362
8199
|
import { Command as Command17 } from "commander";
|
|
7363
8200
|
|
|
7364
8201
|
// src/commands/cloud/link.ts
|
|
7365
8202
|
init_logger();
|
|
7366
|
-
import { basename as
|
|
8203
|
+
import { basename as basename3 } from "path";
|
|
7367
8204
|
import prompts12 from "prompts";
|
|
7368
|
-
import
|
|
8205
|
+
import chalk45 from "chalk";
|
|
7369
8206
|
|
|
7370
8207
|
// src/utils/cloud/tokens.ts
|
|
7371
8208
|
var TOKEN_KEYS = {
|
|
@@ -7403,20 +8240,20 @@ async function deleteCloudToken(provider) {
|
|
|
7403
8240
|
|
|
7404
8241
|
// src/utils/cloud/config.ts
|
|
7405
8242
|
init_env_file();
|
|
7406
|
-
import { existsSync as
|
|
7407
|
-
import { dirname as
|
|
7408
|
-
var CONFIG_REL_PATH =
|
|
8243
|
+
import { existsSync as existsSync37, mkdirSync as mkdirSync7, readFileSync as readFileSync19, writeFileSync as writeFileSync22 } from "fs";
|
|
8244
|
+
import { dirname as dirname9, join as join40 } from "path";
|
|
8245
|
+
var CONFIG_REL_PATH = join40(".spfn", "cloud.json");
|
|
7409
8246
|
function cloudConfigPath(cwd) {
|
|
7410
|
-
return
|
|
8247
|
+
return join40(cwd, CONFIG_REL_PATH);
|
|
7411
8248
|
}
|
|
7412
8249
|
function readCloudConfig(cwd) {
|
|
7413
8250
|
const path5 = cloudConfigPath(cwd);
|
|
7414
|
-
if (!
|
|
8251
|
+
if (!existsSync37(path5)) {
|
|
7415
8252
|
return {};
|
|
7416
8253
|
}
|
|
7417
8254
|
let parsed;
|
|
7418
8255
|
try {
|
|
7419
|
-
parsed = JSON.parse(
|
|
8256
|
+
parsed = JSON.parse(readFileSync19(path5, "utf-8"));
|
|
7420
8257
|
} catch {
|
|
7421
8258
|
throw new Error(`${path5} is not valid JSON \u2014 delete it and run \`spfn cloud link\` again.`);
|
|
7422
8259
|
}
|
|
@@ -7427,8 +8264,8 @@ function readCloudConfig(cwd) {
|
|
|
7427
8264
|
}
|
|
7428
8265
|
function writeCloudConfig(cwd, config) {
|
|
7429
8266
|
const path5 = cloudConfigPath(cwd);
|
|
7430
|
-
mkdirSync7(
|
|
7431
|
-
|
|
8267
|
+
mkdirSync7(dirname9(path5), { recursive: true });
|
|
8268
|
+
writeFileSync22(path5, JSON.stringify(config, null, 4) + "\n", "utf-8");
|
|
7432
8269
|
ensureGitignored(cwd, [{ pattern: ".spfn/cloud.json", comment: "spfn cloud link state (per-user accounts)" }]);
|
|
7433
8270
|
}
|
|
7434
8271
|
function requireLinked(config, provider) {
|
|
@@ -7657,7 +8494,7 @@ async function cloudLink(options) {
|
|
|
7657
8494
|
logger.error(error instanceof Error ? error.message : String(error));
|
|
7658
8495
|
process.exit(1);
|
|
7659
8496
|
}
|
|
7660
|
-
logger.success(`Link state saved to ${
|
|
8497
|
+
logger.success(`Link state saved to ${chalk45.cyan(cloudConfigPath(cwd))} (identifiers only \u2014 tokens stay in the keychain).`);
|
|
7661
8498
|
logger.info("Next: `spfn cloud status` for limits vs usage, `spfn cloud env pull` for project keys.");
|
|
7662
8499
|
}
|
|
7663
8500
|
async function linkVercel(config, cwd, options) {
|
|
@@ -7677,7 +8514,7 @@ async function linkVercel(config, cwd, options) {
|
|
|
7677
8514
|
throw new Error(`No Vercel project named "${options.vercelProject}" \u2014 available: ${projects.map((p) => p.name).join(", ")}.`);
|
|
7678
8515
|
}
|
|
7679
8516
|
} else {
|
|
7680
|
-
const defaultIndex = Math.max(0, projects.findIndex((p) => p.name ===
|
|
8517
|
+
const defaultIndex = Math.max(0, projects.findIndex((p) => p.name === basename3(cwd)));
|
|
7681
8518
|
({ projectId } = await prompts12({
|
|
7682
8519
|
type: "select",
|
|
7683
8520
|
name: "projectId",
|
|
@@ -7749,7 +8586,7 @@ async function obtainToken(provider, label, envVar, createUrl, replace) {
|
|
|
7749
8586
|
return fromKeychain;
|
|
7750
8587
|
}
|
|
7751
8588
|
}
|
|
7752
|
-
logger.info(`Create one at ${
|
|
8589
|
+
logger.info(`Create one at ${chalk45.cyan(createUrl)} if you don't have it yet.`);
|
|
7753
8590
|
const { token } = await prompts12({
|
|
7754
8591
|
type: "password",
|
|
7755
8592
|
name: "token",
|
|
@@ -7762,7 +8599,7 @@ async function obtainToken(provider, label, envVar, createUrl, replace) {
|
|
|
7762
8599
|
}
|
|
7763
8600
|
|
|
7764
8601
|
// src/commands/cloud/limits.ts
|
|
7765
|
-
import
|
|
8602
|
+
import chalk47 from "chalk";
|
|
7766
8603
|
|
|
7767
8604
|
// src/utils/cloud/limits-data.ts
|
|
7768
8605
|
var VERCEL_HOBBY_LIMITS = [
|
|
@@ -7801,21 +8638,21 @@ var SUPABASE_FREE_NOTES = [
|
|
|
7801
8638
|
var LIMITS_VERIFIED_ON = "2026-08-13";
|
|
7802
8639
|
|
|
7803
8640
|
// src/utils/cloud/format.ts
|
|
7804
|
-
import
|
|
8641
|
+
import chalk46 from "chalk";
|
|
7805
8642
|
var MIGRATION_PROMPT_THRESHOLD = 0.8;
|
|
7806
8643
|
function formatPercent(used, limit) {
|
|
7807
8644
|
if (limit <= 0) {
|
|
7808
|
-
return
|
|
8645
|
+
return chalk46.dim("n/a");
|
|
7809
8646
|
}
|
|
7810
8647
|
const ratio = used / limit;
|
|
7811
8648
|
const text = `${(ratio * 100).toFixed(1)}%`;
|
|
7812
8649
|
if (ratio >= 1) {
|
|
7813
|
-
return
|
|
8650
|
+
return chalk46.red.bold(text);
|
|
7814
8651
|
}
|
|
7815
8652
|
if (ratio >= MIGRATION_PROMPT_THRESHOLD) {
|
|
7816
|
-
return
|
|
8653
|
+
return chalk46.yellow.bold(text);
|
|
7817
8654
|
}
|
|
7818
|
-
return
|
|
8655
|
+
return chalk46.green(text);
|
|
7819
8656
|
}
|
|
7820
8657
|
function isNearLimit(used, limit) {
|
|
7821
8658
|
return limit > 0 && used / limit >= MIGRATION_PROMPT_THRESHOLD;
|
|
@@ -7852,25 +8689,25 @@ var PER_LABEL = {
|
|
|
7852
8689
|
async function cloudLimits() {
|
|
7853
8690
|
printPlan("Vercel Hobby", VERCEL_HOBBY_LIMITS, VERCEL_HOBBY_NOTES);
|
|
7854
8691
|
printPlan("Supabase Free", SUPABASE_FREE_LIMITS, SUPABASE_FREE_NOTES);
|
|
7855
|
-
console.log(
|
|
7856
|
-
console.log(
|
|
8692
|
+
console.log(chalk47.dim(`Numbers verified against the official docs on ${LIMITS_VERIFIED_ON}.`));
|
|
8693
|
+
console.log(chalk47.dim("Live usage against these limits: `spfn cloud status`.\n"));
|
|
7857
8694
|
}
|
|
7858
8695
|
function printPlan(title, limits, notes) {
|
|
7859
|
-
console.log(
|
|
8696
|
+
console.log(chalk47.bold(`
|
|
7860
8697
|
${title}`));
|
|
7861
8698
|
for (const limit of limits) {
|
|
7862
8699
|
const amount = `${formatCount(limit.limit)} ${limit.unit}`;
|
|
7863
|
-
console.log(` ${limit.label.padEnd(34)} ${amount.padEnd(26)} ${
|
|
8700
|
+
console.log(` ${limit.label.padEnd(34)} ${amount.padEnd(26)} ${chalk47.dim(PER_LABEL[limit.per])}`);
|
|
7864
8701
|
}
|
|
7865
8702
|
for (const note of notes) {
|
|
7866
|
-
console.log(
|
|
8703
|
+
console.log(chalk47.dim(` \u2022 ${note}`));
|
|
7867
8704
|
}
|
|
7868
8705
|
console.log("");
|
|
7869
8706
|
}
|
|
7870
8707
|
|
|
7871
8708
|
// src/commands/cloud/usage.ts
|
|
7872
8709
|
init_logger();
|
|
7873
|
-
import
|
|
8710
|
+
import chalk48 from "chalk";
|
|
7874
8711
|
|
|
7875
8712
|
// src/commands/cloud/collect.ts
|
|
7876
8713
|
async function collectSnapshot(cwd) {
|
|
@@ -7963,10 +8800,10 @@ function printVercel(snapshot) {
|
|
|
7963
8800
|
if (!snapshot.vercel) {
|
|
7964
8801
|
return;
|
|
7965
8802
|
}
|
|
7966
|
-
console.log(
|
|
8803
|
+
console.log(chalk48.bold(`
|
|
7967
8804
|
Vercel \u2014 account-wide usage, rolling 30 days (linked project: ${snapshot.vercel.projectName})`));
|
|
7968
8805
|
if (snapshot.vercel.services.length === 0) {
|
|
7969
|
-
console.log(
|
|
8806
|
+
console.log(chalk48.dim(" No usage recorded in the billing feed yet."));
|
|
7970
8807
|
}
|
|
7971
8808
|
for (const service of snapshot.vercel.services) {
|
|
7972
8809
|
const amount = `${formatCount(service.consumed)} ${service.unit}`.trim();
|
|
@@ -7978,12 +8815,12 @@ function printSupabase(snapshot) {
|
|
|
7978
8815
|
return;
|
|
7979
8816
|
}
|
|
7980
8817
|
const { projectName, status, dbSizeBytes, dailyApiCount } = snapshot.supabase;
|
|
7981
|
-
console.log(
|
|
8818
|
+
console.log(chalk48.bold(`
|
|
7982
8819
|
Supabase \u2014 ${projectName}`));
|
|
7983
8820
|
console.log(` ${"Project status".padEnd(34)} ${status}`);
|
|
7984
|
-
console.log(` ${"Database size".padEnd(34)} ${dbSizeBytes === null ?
|
|
7985
|
-
console.log(` ${"API requests (last 24h)".padEnd(34)} ${dailyApiCount === null ?
|
|
7986
|
-
console.log(
|
|
8821
|
+
console.log(` ${"Database size".padEnd(34)} ${dbSizeBytes === null ? chalk48.dim("unavailable") : formatBytes2(dbSizeBytes)}`);
|
|
8822
|
+
console.log(` ${"API requests (last 24h)".padEnd(34)} ${dailyApiCount === null ? chalk48.dim("unavailable") : formatCount(dailyApiCount)}`);
|
|
8823
|
+
console.log(chalk48.dim(" Org totals (egress, MAU) have no public API \u2014 exact numbers: supabase.com/dashboard \u2192 Usage."));
|
|
7987
8824
|
}
|
|
7988
8825
|
function printProblems(snapshot) {
|
|
7989
8826
|
console.log("");
|
|
@@ -7997,7 +8834,7 @@ function printProblems(snapshot) {
|
|
|
7997
8834
|
|
|
7998
8835
|
// src/commands/cloud/status.ts
|
|
7999
8836
|
init_logger();
|
|
8000
|
-
import
|
|
8837
|
+
import chalk49 from "chalk";
|
|
8001
8838
|
async function cloudStatus() {
|
|
8002
8839
|
const snapshot = await collectSnapshot(process.cwd());
|
|
8003
8840
|
const nearLimit = [];
|
|
@@ -8016,10 +8853,10 @@ function printVercel2(snapshot, nearLimit) {
|
|
|
8016
8853
|
if (!snapshot.vercel) {
|
|
8017
8854
|
return;
|
|
8018
8855
|
}
|
|
8019
|
-
console.log(
|
|
8856
|
+
console.log(chalk49.bold(`
|
|
8020
8857
|
Vercel Hobby \u2014 account-wide usage, rolling 30 days (linked project: ${snapshot.vercel.projectName}; limits as of ${LIMITS_VERIFIED_ON})`));
|
|
8021
8858
|
if (snapshot.vercel.services.length === 0) {
|
|
8022
|
-
console.log(
|
|
8859
|
+
console.log(chalk49.dim(" No usage recorded in the billing feed yet."));
|
|
8023
8860
|
}
|
|
8024
8861
|
for (const service of snapshot.vercel.services) {
|
|
8025
8862
|
const limit = matchVercelLimit(service.serviceName);
|
|
@@ -8038,12 +8875,12 @@ function printSupabase2(snapshot, nearLimit) {
|
|
|
8038
8875
|
return;
|
|
8039
8876
|
}
|
|
8040
8877
|
const { projectName, status, dbSizeBytes, dailyApiCount } = snapshot.supabase;
|
|
8041
|
-
console.log(
|
|
8878
|
+
console.log(chalk49.bold(`
|
|
8042
8879
|
Supabase Free \u2014 ${projectName}`));
|
|
8043
8880
|
if (isSupabasePaused(status)) {
|
|
8044
|
-
console.log(` ${"Project status".padEnd(34)} ${
|
|
8881
|
+
console.log(` ${"Project status".padEnd(34)} ${chalk49.red.bold(status)} \u2014 run \`spfn cloud keepalive\` after restoring it`);
|
|
8045
8882
|
} else {
|
|
8046
|
-
console.log(` ${"Project status".padEnd(34)} ${
|
|
8883
|
+
console.log(` ${"Project status".padEnd(34)} ${chalk49.green(status)}`);
|
|
8047
8884
|
}
|
|
8048
8885
|
if (dbSizeBytes !== null) {
|
|
8049
8886
|
const dbLimit = SUPABASE_FREE_LIMITS.find((l) => l.key === "db-size");
|
|
@@ -8053,25 +8890,25 @@ Supabase Free \u2014 ${projectName}`));
|
|
|
8053
8890
|
nearLimit.push("Supabase database size");
|
|
8054
8891
|
}
|
|
8055
8892
|
} else {
|
|
8056
|
-
console.log(` ${"Database size".padEnd(34)} ${
|
|
8893
|
+
console.log(` ${"Database size".padEnd(34)} ${chalk49.dim("unavailable")}`);
|
|
8057
8894
|
}
|
|
8058
|
-
console.log(` ${"API requests (last 24h)".padEnd(34)} ${dailyApiCount === null ?
|
|
8059
|
-
console.log(
|
|
8895
|
+
console.log(` ${"API requests (last 24h)".padEnd(34)} ${dailyApiCount === null ? chalk49.dim("unavailable") : formatCount(dailyApiCount)}`);
|
|
8896
|
+
console.log(chalk49.dim(` DB size ${dbSizeBytes === null ? "unknown" : formatBytes2(dbSizeBytes)} counts per project; egress/MAU are org totals \u2014 exact numbers on the dashboard.`));
|
|
8060
8897
|
}
|
|
8061
8898
|
function printMigrationPrompt(nearLimit) {
|
|
8062
8899
|
if (nearLimit.length === 0) {
|
|
8063
8900
|
return;
|
|
8064
8901
|
}
|
|
8065
|
-
console.log(
|
|
8066
|
-
console.log(
|
|
8067
|
-
console.log(
|
|
8902
|
+
console.log(chalk49.yellow.bold(`\u26A0 Approaching free-tier limits: ${nearLimit.join(", ")}.`));
|
|
8903
|
+
console.log(chalk49.yellow(" When a Vercel Hobby limit is hit, that capability pauses; Supabase over quota restricts the whole org."));
|
|
8904
|
+
console.log(chalk49.yellow(" Migrating to your own cloud removes these ceilings \u2014 `spfn cloud migrate` (coming soon).\n"));
|
|
8068
8905
|
}
|
|
8069
8906
|
|
|
8070
8907
|
// src/commands/cloud/keepalive.ts
|
|
8071
8908
|
init_logger();
|
|
8072
|
-
import { existsSync as
|
|
8073
|
-
import { dirname as
|
|
8074
|
-
import
|
|
8909
|
+
import { existsSync as existsSync38, mkdirSync as mkdirSync8, readFileSync as readFileSync20, writeFileSync as writeFileSync23 } from "fs";
|
|
8910
|
+
import { dirname as dirname10, join as join41 } from "path";
|
|
8911
|
+
import chalk50 from "chalk";
|
|
8075
8912
|
var DEFAULT_HEALTH_PATH = "/api/backend/_core/health?detailed=true";
|
|
8076
8913
|
var DEFAULT_SCHEDULE = "0 3 * * *";
|
|
8077
8914
|
async function cloudKeepalive(options) {
|
|
@@ -8096,12 +8933,12 @@ async function cloudKeepalive(options) {
|
|
|
8096
8933
|
logger.info("The DB ping needs detailed health checks: keep `healthCheck: { detailed: true }` in your server config.");
|
|
8097
8934
|
}
|
|
8098
8935
|
function setupVercelCron(cwd, healthPath) {
|
|
8099
|
-
const vercelJsonPath =
|
|
8936
|
+
const vercelJsonPath = join41(cwd, "vercel.json");
|
|
8100
8937
|
let vercelJson = {};
|
|
8101
|
-
if (
|
|
8938
|
+
if (existsSync38(vercelJsonPath)) {
|
|
8102
8939
|
let parsed;
|
|
8103
8940
|
try {
|
|
8104
|
-
parsed = JSON.parse(
|
|
8941
|
+
parsed = JSON.parse(readFileSync20(vercelJsonPath, "utf-8"));
|
|
8105
8942
|
} catch {
|
|
8106
8943
|
logger.error("vercel.json is not valid JSON (comments and trailing commas are not allowed) \u2014 fix it and re-run.");
|
|
8107
8944
|
process.exit(1);
|
|
@@ -8119,10 +8956,10 @@ function setupVercelCron(cwd, healthPath) {
|
|
|
8119
8956
|
}
|
|
8120
8957
|
if (result === "occupied") {
|
|
8121
8958
|
logger.error("vercel.json already has a cron job, and Vercel Hobby allows only one.");
|
|
8122
|
-
logger.info(`Point the existing cron at ${
|
|
8959
|
+
logger.info(`Point the existing cron at ${chalk50.cyan(healthPath)} yourself, or use \`--github-actions --url <deployed-url>\`.`);
|
|
8123
8960
|
process.exit(1);
|
|
8124
8961
|
}
|
|
8125
|
-
|
|
8962
|
+
writeFileSync23(vercelJsonPath, JSON.stringify(vercelJson, null, 4) + "\n", "utf-8");
|
|
8126
8963
|
logger.success(`vercel.json: daily cron added (${DEFAULT_SCHEDULE} UTC \u2192 ${healthPath}). Deploy to activate.`);
|
|
8127
8964
|
warnIfBackendRouteMissing(cwd);
|
|
8128
8965
|
}
|
|
@@ -8142,8 +8979,8 @@ function setupGithubActions(cwd, healthPath, url) {
|
|
|
8142
8979
|
logger.error("`--github-actions` needs `--url <deployed-url>` \u2014 the workflow runs outside your app and must know where it lives.");
|
|
8143
8980
|
process.exit(1);
|
|
8144
8981
|
}
|
|
8145
|
-
const workflowPath =
|
|
8146
|
-
if (
|
|
8982
|
+
const workflowPath = join41(cwd, ".github", "workflows", "spfn-keepalive.yml");
|
|
8983
|
+
if (existsSync38(workflowPath)) {
|
|
8147
8984
|
logger.success(`${workflowPath} already exists \u2014 nothing to do.`);
|
|
8148
8985
|
return;
|
|
8149
8986
|
}
|
|
@@ -8152,8 +8989,8 @@ function setupGithubActions(cwd, healthPath, url) {
|
|
|
8152
8989
|
logger.error(`--url "${url}" is not a valid URL. Pass the deployed app origin, e.g. https://myapp.vercel.app.`);
|
|
8153
8990
|
process.exit(1);
|
|
8154
8991
|
}
|
|
8155
|
-
mkdirSync8(
|
|
8156
|
-
|
|
8992
|
+
mkdirSync8(dirname10(workflowPath), { recursive: true });
|
|
8993
|
+
writeFileSync23(workflowPath, githubWorkflow(new URL(healthPath, base).toString()), "utf-8");
|
|
8157
8994
|
logger.success(".github/workflows/spfn-keepalive.yml: daily keep-alive workflow added. Commit and push to activate.");
|
|
8158
8995
|
}
|
|
8159
8996
|
function normalizeDeployUrl(url) {
|
|
@@ -8190,8 +9027,8 @@ jobs:
|
|
|
8190
9027
|
`;
|
|
8191
9028
|
}
|
|
8192
9029
|
function warnIfBackendRouteMissing(cwd) {
|
|
8193
|
-
const routePath =
|
|
8194
|
-
if (!
|
|
9030
|
+
const routePath = join41(cwd, "src", "app", "api", "backend", "[[...route]]", "route.ts");
|
|
9031
|
+
if (!existsSync38(routePath)) {
|
|
8195
9032
|
logger.warn("src/app/api/backend/[[...route]]/route.ts not found \u2014 the cron path answers only after `spfn add vercel`.");
|
|
8196
9033
|
}
|
|
8197
9034
|
}
|
|
@@ -8199,9 +9036,9 @@ function warnIfBackendRouteMissing(cwd) {
|
|
|
8199
9036
|
// src/commands/cloud/env.ts
|
|
8200
9037
|
init_logger();
|
|
8201
9038
|
init_env_file();
|
|
8202
|
-
import { join as
|
|
9039
|
+
import { join as join42 } from "path";
|
|
8203
9040
|
import prompts13 from "prompts";
|
|
8204
|
-
import
|
|
9041
|
+
import chalk51 from "chalk";
|
|
8205
9042
|
async function cloudEnvPull(options) {
|
|
8206
9043
|
const cwd = process.cwd();
|
|
8207
9044
|
try {
|
|
@@ -8220,7 +9057,7 @@ async function cloudEnvPull(options) {
|
|
|
8220
9057
|
logger.info("Values were written to files/keychain only \u2014 none were displayed.");
|
|
8221
9058
|
}
|
|
8222
9059
|
function writePublicValues(cwd, projectRef2, keys) {
|
|
8223
|
-
const envLocalPath =
|
|
9060
|
+
const envLocalPath = join42(cwd, ".env.local");
|
|
8224
9061
|
ensureGitignored(cwd, [{ pattern: ".env.local", comment: "local env (per-machine values)" }]);
|
|
8225
9062
|
upsertEnvVar(envLocalPath, "NEXT_PUBLIC_SUPABASE_URL", `https://${projectRef2}.supabase.co`);
|
|
8226
9063
|
logger.success("NEXT_PUBLIC_SUPABASE_URL \u2192 .env.local");
|
|
@@ -8238,7 +9075,7 @@ async function storeServiceKey(cwd, keys) {
|
|
|
8238
9075
|
logger.warn("No service_role/secret key found on the project \u2014 skipped SUPABASE_SERVICE_ROLE_KEY.");
|
|
8239
9076
|
return;
|
|
8240
9077
|
}
|
|
8241
|
-
await storeSecret(cwd, "local", "SUPABASE_SERVICE_ROLE_KEY", service.api_key);
|
|
9078
|
+
await storeSecret(cwd, { env: "local" }, "SUPABASE_SERVICE_ROLE_KEY", service.api_key);
|
|
8242
9079
|
}
|
|
8243
9080
|
function findKey(keys, candidates) {
|
|
8244
9081
|
return keys.find((key) => candidates.includes(key.name)) ?? keys.find((key) => key.type !== void 0 && candidates.includes(key.type));
|
|
@@ -8259,7 +9096,7 @@ async function storeDatabaseUrl(cwd, token, projectRef2) {
|
|
|
8259
9096
|
return;
|
|
8260
9097
|
}
|
|
8261
9098
|
const url = sessionPoolerUrl(template, password);
|
|
8262
|
-
await storeSecret(cwd, "local", "DATABASE_URL", url);
|
|
9099
|
+
await storeSecret(cwd, { env: "local" }, "DATABASE_URL", url);
|
|
8263
9100
|
logger.info("This is the session-mode pooler connection (IPv4-safe, works for migrations). Serverless runtime can use the transaction pooler (port 6543) instead.");
|
|
8264
9101
|
}
|
|
8265
9102
|
async function poolerTemplate(token, projectRef2) {
|
|
@@ -8303,7 +9140,7 @@ async function cloudEnvPush(keys) {
|
|
|
8303
9140
|
logger.error(`${failure.key}: rejected by Vercel \u2014 ${failure.message}`);
|
|
8304
9141
|
}
|
|
8305
9142
|
for (const envVar of vars.filter((v) => !failedKeys.has(v.key))) {
|
|
8306
|
-
logger.success(`${envVar.key} \u2192 Vercel ${
|
|
9143
|
+
logger.success(`${envVar.key} \u2192 Vercel ${chalk51.cyan(linked.projectName)} (${envVar.type}, ${envVar.target.join("+")})`);
|
|
8307
9144
|
}
|
|
8308
9145
|
if (result.failed.length > 0) {
|
|
8309
9146
|
process.exit(1);
|
|
@@ -8314,8 +9151,8 @@ async function cloudEnvPush(keys) {
|
|
|
8314
9151
|
}
|
|
8315
9152
|
}
|
|
8316
9153
|
async function resolveLocalValues(cwd, keys) {
|
|
8317
|
-
const publicEnv = { ...parseEnvFile(
|
|
8318
|
-
const serverEnv = parseEnvFile(
|
|
9154
|
+
const publicEnv = { ...parseEnvFile(join42(cwd, ".env")), ...parseEnvFile(join42(cwd, ".env.local")) };
|
|
9155
|
+
const serverEnv = parseEnvFile(join42(cwd, ".env.server"));
|
|
8319
9156
|
const keychain = await resolveKeychainEnv(cwd);
|
|
8320
9157
|
const vars = [];
|
|
8321
9158
|
const missing = [];
|
|
@@ -8353,9 +9190,9 @@ envGroup.command("push").description("Push local env values to the Vercel projec
|
|
|
8353
9190
|
|
|
8354
9191
|
// src/commands/kit/index.ts
|
|
8355
9192
|
import { Command as Command18 } from "commander";
|
|
8356
|
-
import
|
|
9193
|
+
import chalk53 from "chalk";
|
|
8357
9194
|
import prompts14 from "prompts";
|
|
8358
|
-
import { resolve as
|
|
9195
|
+
import { resolve as resolve6 } from "path";
|
|
8359
9196
|
|
|
8360
9197
|
// src/kit/errors.ts
|
|
8361
9198
|
var KIT_EXIT = {
|
|
@@ -8471,11 +9308,11 @@ async function resolveKitAdapters(request2) {
|
|
|
8471
9308
|
// src/kit/operations/install.ts
|
|
8472
9309
|
import { createHash as createHash2 } from "crypto";
|
|
8473
9310
|
import { hostname as hostname2, tmpdir } from "os";
|
|
8474
|
-
import { mkdtempSync, existsSync as
|
|
8475
|
-
import { join as
|
|
9311
|
+
import { mkdtempSync, existsSync as existsSync50 } from "fs";
|
|
9312
|
+
import { join as join52 } from "path";
|
|
8476
9313
|
|
|
8477
9314
|
// src/kit/events.ts
|
|
8478
|
-
import
|
|
9315
|
+
import chalk52 from "chalk";
|
|
8479
9316
|
|
|
8480
9317
|
// src/kit/secret-scan.ts
|
|
8481
9318
|
var SECRET_SHAPES = [
|
|
@@ -8527,30 +9364,30 @@ function redactEvent(event, knownSecrets) {
|
|
|
8527
9364
|
function humanLine(event) {
|
|
8528
9365
|
switch (event.status) {
|
|
8529
9366
|
case "completed":
|
|
8530
|
-
return `${
|
|
9367
|
+
return `${chalk52.green("\u2713")} ${event.summary}`;
|
|
8531
9368
|
case "waiting":
|
|
8532
|
-
return `${
|
|
9369
|
+
return `${chalk52.yellow("\u2026")} ${event.summary}${nextLine(event)}`;
|
|
8533
9370
|
case "failed":
|
|
8534
|
-
return `${
|
|
8535
|
-
${
|
|
9371
|
+
return `${chalk52.red("\u2717")} ${event.summary}
|
|
9372
|
+
${chalk52.gray(event.code)}${nextLine(event)}`;
|
|
8536
9373
|
case "started":
|
|
8537
|
-
return
|
|
9374
|
+
return chalk52.gray(`\u2192 ${event.summary}`);
|
|
8538
9375
|
default:
|
|
8539
|
-
return
|
|
9376
|
+
return chalk52.gray(` ${event.summary}`);
|
|
8540
9377
|
}
|
|
8541
9378
|
}
|
|
8542
9379
|
function nextLine(event) {
|
|
8543
9380
|
if (!event.next) {
|
|
8544
9381
|
return "";
|
|
8545
9382
|
}
|
|
8546
|
-
const approval = event.next.requiresHumanApproval ?
|
|
9383
|
+
const approval = event.next.requiresHumanApproval ? chalk52.yellow(" (needs your approval)") : "";
|
|
8547
9384
|
return `
|
|
8548
|
-
${
|
|
9385
|
+
${chalk52.gray("next:")} ${event.next.command}${approval}`;
|
|
8549
9386
|
}
|
|
8550
9387
|
|
|
8551
9388
|
// src/kit/journal.ts
|
|
8552
|
-
import { mkdirSync as mkdirSync10, readdirSync as
|
|
8553
|
-
import { dirname as
|
|
9389
|
+
import { mkdirSync as mkdirSync10, readdirSync as readdirSync4, readFileSync as readFileSync21, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync25, existsSync as existsSync40, statSync as statSync2 } from "fs";
|
|
9390
|
+
import { dirname as dirname11, join as join44 } from "path";
|
|
8554
9391
|
|
|
8555
9392
|
// src/kit/digest.ts
|
|
8556
9393
|
import { createHash } from "crypto";
|
|
@@ -8591,8 +9428,8 @@ function digestOfJson(value) {
|
|
|
8591
9428
|
}
|
|
8592
9429
|
|
|
8593
9430
|
// src/kit/paths.ts
|
|
8594
|
-
import { mkdirSync as mkdirSync9, writeFileSync as
|
|
8595
|
-
import { join as
|
|
9431
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync24, existsSync as existsSync39 } from "fs";
|
|
9432
|
+
import { join as join43 } from "path";
|
|
8596
9433
|
var KIT_STATE_DIR = ".spfn";
|
|
8597
9434
|
var OPERATIONS_IGNORE = [
|
|
8598
9435
|
"# Written by `spfn kit`. One machine's run is not part of the project.",
|
|
@@ -8600,27 +9437,27 @@ var OPERATIONS_IGNORE = [
|
|
|
8600
9437
|
""
|
|
8601
9438
|
].join("\n");
|
|
8602
9439
|
function kitPaths(root) {
|
|
8603
|
-
const stateDir =
|
|
8604
|
-
const operationsDir =
|
|
8605
|
-
const lockDir =
|
|
9440
|
+
const stateDir = join43(root, KIT_STATE_DIR);
|
|
9441
|
+
const operationsDir = join43(stateDir, "operations");
|
|
9442
|
+
const lockDir = join43(operationsDir, "operation.lock");
|
|
8606
9443
|
return {
|
|
8607
9444
|
root,
|
|
8608
9445
|
stateDir,
|
|
8609
|
-
licenseFile:
|
|
8610
|
-
lockFile:
|
|
9446
|
+
licenseFile: join43(stateDir, "license.json"),
|
|
9447
|
+
lockFile: join43(stateDir, "kit-lock.json"),
|
|
8611
9448
|
operationsDir,
|
|
8612
|
-
activeJournal:
|
|
8613
|
-
historyDir:
|
|
9449
|
+
activeJournal: join43(operationsDir, "active.json"),
|
|
9450
|
+
historyDir: join43(operationsDir, "history"),
|
|
8614
9451
|
lockDir,
|
|
8615
|
-
lockOwnerFile:
|
|
9452
|
+
lockOwnerFile: join43(lockDir, "owner.json")
|
|
8616
9453
|
};
|
|
8617
9454
|
}
|
|
8618
9455
|
function ensureOperationsDir(root) {
|
|
8619
9456
|
const paths = kitPaths(root);
|
|
8620
|
-
const ignoreFile =
|
|
9457
|
+
const ignoreFile = join43(paths.stateDir, ".gitignore");
|
|
8621
9458
|
mkdirSync9(paths.stateDir, { recursive: true });
|
|
8622
|
-
if (!
|
|
8623
|
-
|
|
9459
|
+
if (!existsSync39(ignoreFile)) {
|
|
9460
|
+
writeFileSync24(ignoreFile, OPERATIONS_IGNORE, "utf8");
|
|
8624
9461
|
}
|
|
8625
9462
|
mkdirSync9(paths.operationsDir, { recursive: true });
|
|
8626
9463
|
return paths.operationsDir;
|
|
@@ -8907,14 +9744,14 @@ var JournalStore = class {
|
|
|
8907
9744
|
return readJournalFile(this.paths.activeJournal);
|
|
8908
9745
|
}
|
|
8909
9746
|
readHistory(operationId) {
|
|
8910
|
-
return readJournalFile(
|
|
9747
|
+
return readJournalFile(join44(this.paths.historyDir, `${operationId}.json`));
|
|
8911
9748
|
}
|
|
8912
9749
|
/** Every journal this project still has, newest update first. */
|
|
8913
9750
|
listHistory() {
|
|
8914
|
-
if (!
|
|
9751
|
+
if (!existsSync40(this.paths.historyDir)) {
|
|
8915
9752
|
return [];
|
|
8916
9753
|
}
|
|
8917
|
-
return
|
|
9754
|
+
return readdirSync4(this.paths.historyDir).filter((name) => name.endsWith(".json")).map((name) => readJournalFile(join44(this.paths.historyDir, name))).filter((journal) => journal !== null).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
8918
9755
|
}
|
|
8919
9756
|
create(init) {
|
|
8920
9757
|
const timestamp = this.now();
|
|
@@ -8973,17 +9810,17 @@ var JournalStore = class {
|
|
|
8973
9810
|
}
|
|
8974
9811
|
/** Move a finished operation into history and clear the active slot. */
|
|
8975
9812
|
archive(journal) {
|
|
8976
|
-
writeJsonAtomically(
|
|
8977
|
-
|
|
9813
|
+
writeJsonAtomically(join44(this.paths.historyDir, `${journal.operationId}.json`), journal);
|
|
9814
|
+
rmSync3(this.paths.activeJournal, { force: true });
|
|
8978
9815
|
this.pruneHistory();
|
|
8979
9816
|
}
|
|
8980
9817
|
pruneHistory() {
|
|
8981
|
-
if (!
|
|
9818
|
+
if (!existsSync40(this.paths.historyDir)) {
|
|
8982
9819
|
return;
|
|
8983
9820
|
}
|
|
8984
|
-
const files =
|
|
9821
|
+
const files = readdirSync4(this.paths.historyDir).filter((name) => name.endsWith(".json")).map((name) => join44(this.paths.historyDir, name)).sort((left, right) => statSync2(right).mtimeMs - statSync2(left).mtimeMs);
|
|
8985
9822
|
for (const file of files.slice(this.historyLimit)) {
|
|
8986
|
-
|
|
9823
|
+
rmSync3(file, { force: true });
|
|
8987
9824
|
}
|
|
8988
9825
|
}
|
|
8989
9826
|
};
|
|
@@ -9003,26 +9840,26 @@ function upsertCheckpoint(checkpoints, entry) {
|
|
|
9003
9840
|
return next;
|
|
9004
9841
|
}
|
|
9005
9842
|
function readJournalFile(file) {
|
|
9006
|
-
if (!
|
|
9843
|
+
if (!existsSync40(file)) {
|
|
9007
9844
|
return null;
|
|
9008
9845
|
}
|
|
9009
9846
|
try {
|
|
9010
|
-
const parsed = JSON.parse(
|
|
9847
|
+
const parsed = JSON.parse(readFileSync21(file, "utf8"));
|
|
9011
9848
|
return validateOperationJournal(parsed).valid ? parsed : null;
|
|
9012
9849
|
} catch {
|
|
9013
9850
|
return null;
|
|
9014
9851
|
}
|
|
9015
9852
|
}
|
|
9016
9853
|
function writeJsonAtomically(file, value) {
|
|
9017
|
-
mkdirSync10(
|
|
9854
|
+
mkdirSync10(dirname11(file), { recursive: true });
|
|
9018
9855
|
const temporary = `${file}.tmp`;
|
|
9019
|
-
|
|
9856
|
+
writeFileSync25(temporary, `${JSON.stringify(value, null, 4)}
|
|
9020
9857
|
`, { encoding: "utf8", mode: 384 });
|
|
9021
|
-
|
|
9858
|
+
renameSync3(temporary, file);
|
|
9022
9859
|
}
|
|
9023
9860
|
|
|
9024
9861
|
// src/kit/lock.ts
|
|
9025
|
-
import { mkdirSync as mkdirSync11, readFileSync as
|
|
9862
|
+
import { mkdirSync as mkdirSync11, readFileSync as readFileSync22, rmSync as rmSync4, writeFileSync as writeFileSync26, existsSync as existsSync41 } from "fs";
|
|
9026
9863
|
import { hostname } from "os";
|
|
9027
9864
|
function defaultIsProcessAlive(pid) {
|
|
9028
9865
|
try {
|
|
@@ -9046,7 +9883,7 @@ function acquireOperationLock(options) {
|
|
|
9046
9883
|
return handle(paths, owner, false);
|
|
9047
9884
|
}
|
|
9048
9885
|
reconcileStaleLock(options);
|
|
9049
|
-
|
|
9886
|
+
rmSync4(paths.lockDir, { recursive: true, force: true });
|
|
9050
9887
|
if (!tryCreateLockDir(paths)) {
|
|
9051
9888
|
throw new KitError("KIT_OPERATION_ACTIVE", "Another spfn kit operation took the project lock first.", {
|
|
9052
9889
|
evidence: { lockDir: paths.lockDir },
|
|
@@ -9058,11 +9895,11 @@ function acquireOperationLock(options) {
|
|
|
9058
9895
|
}
|
|
9059
9896
|
function readLockOwner(root) {
|
|
9060
9897
|
const paths = kitPaths(root);
|
|
9061
|
-
if (!
|
|
9898
|
+
if (!existsSync41(paths.lockOwnerFile)) {
|
|
9062
9899
|
return null;
|
|
9063
9900
|
}
|
|
9064
9901
|
try {
|
|
9065
|
-
const parsed = JSON.parse(
|
|
9902
|
+
const parsed = JSON.parse(readFileSync22(paths.lockOwnerFile, "utf8"));
|
|
9066
9903
|
return typeof parsed?.pid === "number" && typeof parsed?.operationId === "string" ? parsed : null;
|
|
9067
9904
|
} catch {
|
|
9068
9905
|
return null;
|
|
@@ -9131,7 +9968,7 @@ function tryCreateLockDir(paths) {
|
|
|
9131
9968
|
}
|
|
9132
9969
|
}
|
|
9133
9970
|
function writeOwner(paths, owner) {
|
|
9134
|
-
|
|
9971
|
+
writeFileSync26(paths.lockOwnerFile, `${JSON.stringify(owner, null, 4)}
|
|
9135
9972
|
`, "utf8");
|
|
9136
9973
|
}
|
|
9137
9974
|
function handle(paths, owner, reclaimed) {
|
|
@@ -9139,24 +9976,24 @@ function handle(paths, owner, reclaimed) {
|
|
|
9139
9976
|
owner,
|
|
9140
9977
|
reclaimed,
|
|
9141
9978
|
release() {
|
|
9142
|
-
|
|
9979
|
+
rmSync4(paths.lockDir, { recursive: true, force: true });
|
|
9143
9980
|
}
|
|
9144
9981
|
};
|
|
9145
9982
|
}
|
|
9146
9983
|
|
|
9147
9984
|
// src/kit/operation-context.ts
|
|
9148
|
-
import { mkdirSync as mkdirSync12, readFileSync as
|
|
9149
|
-
import { dirname as
|
|
9985
|
+
import { mkdirSync as mkdirSync12, readFileSync as readFileSync23, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync27, existsSync as existsSync42 } from "fs";
|
|
9986
|
+
import { dirname as dirname12, join as join45 } from "path";
|
|
9150
9987
|
function contextFile(root) {
|
|
9151
|
-
return
|
|
9988
|
+
return join45(kitPaths(root).operationsDir, "context.json");
|
|
9152
9989
|
}
|
|
9153
9990
|
function readOperationContext(root) {
|
|
9154
9991
|
const file = contextFile(root);
|
|
9155
|
-
if (!
|
|
9992
|
+
if (!existsSync42(file)) {
|
|
9156
9993
|
return null;
|
|
9157
9994
|
}
|
|
9158
9995
|
try {
|
|
9159
|
-
const parsed = JSON.parse(
|
|
9996
|
+
const parsed = JSON.parse(readFileSync23(file, "utf8"));
|
|
9160
9997
|
return parsed?.schemaVersion === 1 && typeof parsed.operationId === "string" ? parsed : null;
|
|
9161
9998
|
} catch {
|
|
9162
9999
|
return null;
|
|
@@ -9164,14 +10001,14 @@ function readOperationContext(root) {
|
|
|
9164
10001
|
}
|
|
9165
10002
|
function writeOperationContext(root, context) {
|
|
9166
10003
|
const file = contextFile(root);
|
|
9167
|
-
mkdirSync12(
|
|
10004
|
+
mkdirSync12(dirname12(file), { recursive: true });
|
|
9168
10005
|
const temporary = `${file}.tmp`;
|
|
9169
|
-
|
|
10006
|
+
writeFileSync27(temporary, `${JSON.stringify(context, null, 4)}
|
|
9170
10007
|
`, "utf8");
|
|
9171
|
-
|
|
10008
|
+
renameSync4(temporary, file);
|
|
9172
10009
|
}
|
|
9173
10010
|
function clearOperationContext(root) {
|
|
9174
|
-
|
|
10011
|
+
rmSync5(contextFile(root), { force: true });
|
|
9175
10012
|
}
|
|
9176
10013
|
|
|
9177
10014
|
// src/kit/plan.ts
|
|
@@ -9716,7 +10553,7 @@ async function fetchThroughRedirects(start, fetcher, allowlist) {
|
|
|
9716
10553
|
}
|
|
9717
10554
|
|
|
9718
10555
|
// src/kit/credentials.ts
|
|
9719
|
-
import { randomBytes as
|
|
10556
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
9720
10557
|
var KIT_KEYCHAIN_SERVICE = "superfunction.spfn.kit";
|
|
9721
10558
|
var KIT_KEYCHAIN_NAMESPACE_ENV = "SPFN_KIT_KEYCHAIN_NAMESPACE";
|
|
9722
10559
|
var NAMESPACE_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
@@ -9739,7 +10576,7 @@ function pendingAccount(kitId, installationId, localClientId) {
|
|
|
9739
10576
|
return `${kitId}:pending:${installationId}:${localClientId}`;
|
|
9740
10577
|
}
|
|
9741
10578
|
function newCandidateCredential() {
|
|
9742
|
-
return `spfnlc_${
|
|
10579
|
+
return `spfnlc_${randomBytes4(8).toString("hex")}.${randomBytes4(32).toString("base64url")}`;
|
|
9743
10580
|
}
|
|
9744
10581
|
function credentialPublicId(credential) {
|
|
9745
10582
|
const match = /^spfn[a-z]*_([0-9a-f]{16})\./.exec(credential);
|
|
@@ -9801,8 +10638,8 @@ var KeychainKitCredentialStore = class {
|
|
|
9801
10638
|
};
|
|
9802
10639
|
|
|
9803
10640
|
// src/kit/tooling.ts
|
|
9804
|
-
import { cpSync, mkdirSync as mkdirSync13, readdirSync as
|
|
9805
|
-
import { join as
|
|
10641
|
+
import { cpSync, mkdirSync as mkdirSync13, readdirSync as readdirSync5, readFileSync as readFileSync24, rmSync as rmSync6, statSync as statSync3, existsSync as existsSync43 } from "fs";
|
|
10642
|
+
import { join as join46, relative as relative3, sep as sep3 } from "path";
|
|
9806
10643
|
|
|
9807
10644
|
// src/kit/manifest.ts
|
|
9808
10645
|
var DEFAULT_AGENT_PACK_ROOT = ".spfn/agent-pack";
|
|
@@ -10108,27 +10945,27 @@ function assertSafeRelativePath(path5, refuse) {
|
|
|
10108
10945
|
}
|
|
10109
10946
|
function snapshotTree(root, ignore = ["node_modules", ".git"]) {
|
|
10110
10947
|
const files = /* @__PURE__ */ new Map();
|
|
10111
|
-
const
|
|
10112
|
-
for (const entry of
|
|
10948
|
+
const walk2 = (directory) => {
|
|
10949
|
+
for (const entry of readdirSync5(directory, { withFileTypes: true })) {
|
|
10113
10950
|
if (ignore.includes(entry.name)) {
|
|
10114
10951
|
continue;
|
|
10115
10952
|
}
|
|
10116
|
-
const absolute =
|
|
10953
|
+
const absolute = join46(directory, entry.name);
|
|
10117
10954
|
if (entry.isDirectory()) {
|
|
10118
|
-
|
|
10955
|
+
walk2(absolute);
|
|
10119
10956
|
continue;
|
|
10120
10957
|
}
|
|
10121
10958
|
if (!entry.isFile()) {
|
|
10122
10959
|
continue;
|
|
10123
10960
|
}
|
|
10124
10961
|
files.set(
|
|
10125
|
-
|
|
10126
|
-
sha256Digest(
|
|
10962
|
+
relative3(root, absolute).split(sep3).join("/"),
|
|
10963
|
+
sha256Digest(readFileSync24(absolute))
|
|
10127
10964
|
);
|
|
10128
10965
|
}
|
|
10129
10966
|
};
|
|
10130
|
-
if (
|
|
10131
|
-
|
|
10967
|
+
if (existsSync43(root) && statSync3(root).isDirectory()) {
|
|
10968
|
+
walk2(root);
|
|
10132
10969
|
}
|
|
10133
10970
|
return { files };
|
|
10134
10971
|
}
|
|
@@ -10154,7 +10991,7 @@ function diffTrees(before, after) {
|
|
|
10154
10991
|
}
|
|
10155
10992
|
var COPY_EXCLUDED = ["node_modules", ".git", ".spfn"];
|
|
10156
10993
|
async function runIsolated(projectDir2, stagingDir, run2) {
|
|
10157
|
-
|
|
10994
|
+
rmSync6(stagingDir, { recursive: true, force: true });
|
|
10158
10995
|
mkdirSync13(stagingDir, { recursive: true });
|
|
10159
10996
|
cpSync(projectDir2, stagingDir, {
|
|
10160
10997
|
recursive: true,
|
|
@@ -10166,7 +11003,7 @@ async function runIsolated(projectDir2, stagingDir, run2) {
|
|
|
10166
11003
|
const diff = diffTrees(before, snapshotTree(stagingDir));
|
|
10167
11004
|
return { value, diff };
|
|
10168
11005
|
} finally {
|
|
10169
|
-
|
|
11006
|
+
rmSync6(stagingDir, { recursive: true, force: true });
|
|
10170
11007
|
}
|
|
10171
11008
|
}
|
|
10172
11009
|
function assertWritesWithinAllowlist(diff, allowlist, context) {
|
|
@@ -10184,22 +11021,22 @@ function assertWritesWithinAllowlist(diff, allowlist, context) {
|
|
|
10184
11021
|
}
|
|
10185
11022
|
|
|
10186
11023
|
// src/kit/agent-pack.ts
|
|
10187
|
-
import { existsSync as
|
|
10188
|
-
import { dirname as
|
|
11024
|
+
import { existsSync as existsSync44, mkdirSync as mkdirSync14, readFileSync as readFileSync25, writeFileSync as writeFileSync28 } from "fs";
|
|
11025
|
+
import { dirname as dirname13, join as join47 } from "path";
|
|
10189
11026
|
var AGENT_PACK_RECORD_PATH = ".spfn/agent-pack.json";
|
|
10190
11027
|
function writeAgentPackRecord(projectDir2, record) {
|
|
10191
|
-
const file =
|
|
10192
|
-
mkdirSync14(
|
|
10193
|
-
|
|
11028
|
+
const file = join47(projectDir2, AGENT_PACK_RECORD_PATH);
|
|
11029
|
+
mkdirSync14(dirname13(file), { recursive: true });
|
|
11030
|
+
writeFileSync28(file, `${JSON.stringify(record, null, 4)}
|
|
10194
11031
|
`, "utf8");
|
|
10195
11032
|
}
|
|
10196
11033
|
function readAgentPackRecord(projectDir2) {
|
|
10197
|
-
const file =
|
|
10198
|
-
if (!
|
|
11034
|
+
const file = join47(projectDir2, AGENT_PACK_RECORD_PATH);
|
|
11035
|
+
if (!existsSync44(file)) {
|
|
10199
11036
|
return null;
|
|
10200
11037
|
}
|
|
10201
11038
|
try {
|
|
10202
|
-
const parsed = JSON.parse(
|
|
11039
|
+
const parsed = JSON.parse(readFileSync25(file, "utf8"));
|
|
10203
11040
|
return parsed.schemaVersion === 1 && typeof parsed.files === "object" && parsed.files !== null ? parsed : null;
|
|
10204
11041
|
} catch {
|
|
10205
11042
|
return null;
|
|
@@ -10207,8 +11044,8 @@ function readAgentPackRecord(projectDir2) {
|
|
|
10207
11044
|
}
|
|
10208
11045
|
|
|
10209
11046
|
// src/kit/installed-state.ts
|
|
10210
|
-
import { mkdirSync as mkdirSync15, readFileSync as
|
|
10211
|
-
import { dirname as
|
|
11047
|
+
import { mkdirSync as mkdirSync15, readFileSync as readFileSync26, renameSync as renameSync5, writeFileSync as writeFileSync29, existsSync as existsSync45 } from "fs";
|
|
11048
|
+
import { dirname as dirname14 } from "path";
|
|
10212
11049
|
function readLicenseFile(file) {
|
|
10213
11050
|
const parsed = readJson(file);
|
|
10214
11051
|
if (parsed === null) {
|
|
@@ -10250,7 +11087,7 @@ function requireInstalledLock(file) {
|
|
|
10250
11087
|
const lock = readInstalledLock(file);
|
|
10251
11088
|
if (lock === null) {
|
|
10252
11089
|
throw new KitError("KIT_LOCK_INVALID", "This project has no readable .spfn/kit-lock.json.", {
|
|
10253
|
-
evidence: { file, exists:
|
|
11090
|
+
evidence: { file, exists: existsSync45(file) },
|
|
10254
11091
|
next: { command: "spfn kit recover --json", requiresHumanApproval: false }
|
|
10255
11092
|
});
|
|
10256
11093
|
}
|
|
@@ -10285,29 +11122,29 @@ function lockFromManifest(manifest, context) {
|
|
|
10285
11122
|
};
|
|
10286
11123
|
}
|
|
10287
11124
|
function readJson(file) {
|
|
10288
|
-
if (!
|
|
11125
|
+
if (!existsSync45(file)) {
|
|
10289
11126
|
return null;
|
|
10290
11127
|
}
|
|
10291
11128
|
try {
|
|
10292
|
-
return JSON.parse(
|
|
11129
|
+
return JSON.parse(readFileSync26(file, "utf8"));
|
|
10293
11130
|
} catch {
|
|
10294
11131
|
return null;
|
|
10295
11132
|
}
|
|
10296
11133
|
}
|
|
10297
11134
|
function writeJsonAtomically2(file, value) {
|
|
10298
|
-
mkdirSync15(
|
|
11135
|
+
mkdirSync15(dirname14(file), { recursive: true });
|
|
10299
11136
|
const temporary = `${file}.tmp`;
|
|
10300
|
-
|
|
11137
|
+
writeFileSync29(temporary, `${JSON.stringify(value, null, 4)}
|
|
10301
11138
|
`, "utf8");
|
|
10302
|
-
|
|
11139
|
+
renameSync5(temporary, file);
|
|
10303
11140
|
}
|
|
10304
11141
|
|
|
10305
11142
|
// src/kit/drift.ts
|
|
10306
|
-
import { readFileSync as
|
|
10307
|
-
import { join as
|
|
11143
|
+
import { readFileSync as readFileSync27, existsSync as existsSync46 } from "fs";
|
|
11144
|
+
import { join as join48 } from "path";
|
|
10308
11145
|
function fileDigest(root, relativePath) {
|
|
10309
|
-
const file =
|
|
10310
|
-
return
|
|
11146
|
+
const file = join48(root, relativePath);
|
|
11147
|
+
return existsSync46(file) ? sha256Digest(readFileSync27(file)) : null;
|
|
10311
11148
|
}
|
|
10312
11149
|
function agentPackTargets(root, lock) {
|
|
10313
11150
|
if (lock.agentPack.root === void 0) {
|
|
@@ -10338,12 +11175,12 @@ function managedDigests(root, lock) {
|
|
|
10338
11175
|
}
|
|
10339
11176
|
|
|
10340
11177
|
// src/kit/operations/shared.ts
|
|
10341
|
-
import { mkdirSync as mkdirSync17, readdirSync as
|
|
10342
|
-
import { dirname as
|
|
11178
|
+
import { mkdirSync as mkdirSync17, readdirSync as readdirSync6, readFileSync as readFileSync30, writeFileSync as writeFileSync32, existsSync as existsSync49 } from "fs";
|
|
11179
|
+
import { dirname as dirname16, join as join51 } from "path";
|
|
10343
11180
|
|
|
10344
11181
|
// src/kit/expand.ts
|
|
10345
|
-
import { mkdirSync as mkdirSync16, existsSync as
|
|
10346
|
-
import { dirname as
|
|
11182
|
+
import { mkdirSync as mkdirSync16, existsSync as existsSync47, readFileSync as readFileSync28, writeFileSync as writeFileSync30 } from "fs";
|
|
11183
|
+
import { dirname as dirname15, join as join49 } from "path";
|
|
10347
11184
|
|
|
10348
11185
|
// src/kit/tar.ts
|
|
10349
11186
|
var BLOCK = 512;
|
|
@@ -10444,32 +11281,32 @@ function expandArchive(bytes, request2) {
|
|
|
10444
11281
|
let written = 0;
|
|
10445
11282
|
let matched = 0;
|
|
10446
11283
|
for (const entry of entries) {
|
|
10447
|
-
const
|
|
10448
|
-
const destination =
|
|
11284
|
+
const relative4 = request2.root === void 0 || request2.root.length === 0 ? entry.path : `${request2.root.replace(/\/+$/, "")}/${entry.path}`;
|
|
11285
|
+
const destination = join49(request2.targetDir, relative4);
|
|
10449
11286
|
if (entry.kind === "directory") {
|
|
10450
11287
|
mkdirSync16(destination, { recursive: true });
|
|
10451
11288
|
continue;
|
|
10452
11289
|
}
|
|
10453
11290
|
const digest = sha256Digest(entry.bytes);
|
|
10454
|
-
files[
|
|
10455
|
-
if (
|
|
10456
|
-
assertSameBytes(destination,
|
|
11291
|
+
files[relative4] = digest;
|
|
11292
|
+
if (existsSync47(destination) && request2.existing !== "replace") {
|
|
11293
|
+
assertSameBytes(destination, relative4, digest);
|
|
10457
11294
|
matched += 1;
|
|
10458
11295
|
continue;
|
|
10459
11296
|
}
|
|
10460
|
-
mkdirSync16(
|
|
10461
|
-
|
|
11297
|
+
mkdirSync16(dirname15(destination), { recursive: true });
|
|
11298
|
+
writeFileSync30(destination, entry.bytes, { mode: entry.mode === 0 ? 420 : entry.mode });
|
|
10462
11299
|
written += 1;
|
|
10463
11300
|
}
|
|
10464
11301
|
return { files, written, matched };
|
|
10465
11302
|
}
|
|
10466
|
-
function assertSameBytes(destination,
|
|
10467
|
-
const actual = sha256Digest(
|
|
11303
|
+
function assertSameBytes(destination, relative4, expected) {
|
|
11304
|
+
const actual = sha256Digest(readFileSync28(destination));
|
|
10468
11305
|
if (actual === expected) {
|
|
10469
11306
|
return;
|
|
10470
11307
|
}
|
|
10471
11308
|
throw new KitError("KIT_TARGET_NOT_EMPTY", "A file the release would write is already there with different contents.", {
|
|
10472
|
-
evidence: { path:
|
|
11309
|
+
evidence: { path: relative4, expected, actual },
|
|
10473
11310
|
next: { command: "spfn kit status --json", requiresHumanApproval: false }
|
|
10474
11311
|
});
|
|
10475
11312
|
}
|
|
@@ -10572,15 +11409,15 @@ function registryNpmrc(scopes, registryUrl) {
|
|
|
10572
11409
|
}
|
|
10573
11410
|
|
|
10574
11411
|
// src/kit/package-graph.ts
|
|
10575
|
-
import { existsSync as
|
|
10576
|
-
import { join as
|
|
11412
|
+
import { existsSync as existsSync48, readFileSync as readFileSync29, writeFileSync as writeFileSync31 } from "fs";
|
|
11413
|
+
import { join as join50 } from "path";
|
|
10577
11414
|
var DEPENDENCY_BLOCKS = ["dependencies", "devDependencies", "optionalDependencies"];
|
|
10578
11415
|
function applyManifestVersions(projectDir2, manifest) {
|
|
10579
|
-
const file =
|
|
10580
|
-
if (!
|
|
11416
|
+
const file = join50(projectDir2, "package.json");
|
|
11417
|
+
if (!existsSync48(file)) {
|
|
10581
11418
|
return [];
|
|
10582
11419
|
}
|
|
10583
|
-
const raw =
|
|
11420
|
+
const raw = readFileSync29(file, "utf8");
|
|
10584
11421
|
const document = JSON.parse(raw);
|
|
10585
11422
|
const wanted = new Map(manifest.packages.map((entry) => [entry.name, entry.version]));
|
|
10586
11423
|
const changes = [];
|
|
@@ -10599,7 +11436,7 @@ function applyManifestVersions(projectDir2, manifest) {
|
|
|
10599
11436
|
}
|
|
10600
11437
|
}
|
|
10601
11438
|
if (changes.length > 0) {
|
|
10602
|
-
|
|
11439
|
+
writeFileSync31(file, `${JSON.stringify(document, null, indentationOf(raw))}${trailingNewline(raw)}`);
|
|
10603
11440
|
}
|
|
10604
11441
|
return changes;
|
|
10605
11442
|
}
|
|
@@ -10627,12 +11464,12 @@ function installedGraphMismatches(projectDir2, manifest, skip = () => false) {
|
|
|
10627
11464
|
return mismatches;
|
|
10628
11465
|
}
|
|
10629
11466
|
function installedVersion(projectDir2, name) {
|
|
10630
|
-
const file =
|
|
10631
|
-
if (!
|
|
11467
|
+
const file = join50(projectDir2, "node_modules", ...name.split("/"), "package.json");
|
|
11468
|
+
if (!existsSync48(file)) {
|
|
10632
11469
|
return null;
|
|
10633
11470
|
}
|
|
10634
11471
|
try {
|
|
10635
|
-
const version = JSON.parse(
|
|
11472
|
+
const version = JSON.parse(readFileSync29(file, "utf8")).version;
|
|
10636
11473
|
return typeof version === "string" ? version : null;
|
|
10637
11474
|
} catch {
|
|
10638
11475
|
return null;
|
|
@@ -10719,10 +11556,10 @@ function latestActiveRelease(catalog) {
|
|
|
10719
11556
|
return active[0];
|
|
10720
11557
|
}
|
|
10721
11558
|
function assertEmptyTarget(targetDir) {
|
|
10722
|
-
if (!
|
|
11559
|
+
if (!existsSync49(targetDir)) {
|
|
10723
11560
|
return;
|
|
10724
11561
|
}
|
|
10725
|
-
const entries =
|
|
11562
|
+
const entries = readdirSync6(targetDir).filter((name) => name !== ".DS_Store");
|
|
10726
11563
|
if (entries.length > 0) {
|
|
10727
11564
|
throw new KitError("KIT_TARGET_NOT_EMPTY", "A Kit install needs a new or empty directory.", {
|
|
10728
11565
|
evidence: { targetDir, entries: entries.length }
|
|
@@ -10750,13 +11587,13 @@ async function materializeTargets(adapters, projectDir2, targets, options = {})
|
|
|
10750
11587
|
continue;
|
|
10751
11588
|
}
|
|
10752
11589
|
written[target.path] = digest;
|
|
10753
|
-
writeManagedFile(
|
|
11590
|
+
writeManagedFile(join51(projectDir2, target.path), target.path, bytes, digest, options.existing);
|
|
10754
11591
|
}
|
|
10755
11592
|
return written;
|
|
10756
11593
|
}
|
|
10757
11594
|
function writeManagedFile(file, path5, bytes, digest, existing = "verify") {
|
|
10758
|
-
if (
|
|
10759
|
-
const actual = sha256Digest(
|
|
11595
|
+
if (existsSync49(file) && existing !== "replace") {
|
|
11596
|
+
const actual = sha256Digest(readFileSync30(file));
|
|
10760
11597
|
if (actual === digest) {
|
|
10761
11598
|
return;
|
|
10762
11599
|
}
|
|
@@ -10765,8 +11602,8 @@ function writeManagedFile(file, path5, bytes, digest, existing = "verify") {
|
|
|
10765
11602
|
next: { command: "spfn kit status --json", requiresHumanApproval: false }
|
|
10766
11603
|
});
|
|
10767
11604
|
}
|
|
10768
|
-
mkdirSync17(
|
|
10769
|
-
|
|
11605
|
+
mkdirSync17(dirname16(file), { recursive: true });
|
|
11606
|
+
writeFileSync32(file, bytes);
|
|
10770
11607
|
}
|
|
10771
11608
|
async function installFrozenGraph(adapters, options) {
|
|
10772
11609
|
const maxAttempts = options.allowSessionRetry === false ? 1 : 2;
|
|
@@ -10828,7 +11665,7 @@ function scopesOf(manifest) {
|
|
|
10828
11665
|
return scopes.length === 0 ? ["@superfunction"] : [...new Set(scopes)];
|
|
10829
11666
|
}
|
|
10830
11667
|
function writeRegistryNpmrc(projectDir2, manifest, registryUrl) {
|
|
10831
|
-
|
|
11668
|
+
writeFileSync32(join51(projectDir2, ".npmrc"), registryNpmrc(scopesOf(manifest), registryUrl), "utf8");
|
|
10832
11669
|
}
|
|
10833
11670
|
function assertGraphInstalled(options) {
|
|
10834
11671
|
if (options.expect === void 0) {
|
|
@@ -11076,10 +11913,10 @@ function installSteps(options) {
|
|
|
11076
11913
|
return { kind: "done", evidence: { attempts: evidence.attempts, tooling: tooling.specifier } };
|
|
11077
11914
|
},
|
|
11078
11915
|
async verify() {
|
|
11079
|
-
if (!
|
|
11916
|
+
if (!existsSync50(join52(projectDir2, "node_modules"))) {
|
|
11080
11917
|
return { ok: false, reason: "dependencies-missing" };
|
|
11081
11918
|
}
|
|
11082
|
-
if (!
|
|
11919
|
+
if (!existsSync50(paths.lockFile)) {
|
|
11083
11920
|
writeLock(options, projectDir2, adapters.clock.now());
|
|
11084
11921
|
}
|
|
11085
11922
|
return { ok: true };
|
|
@@ -11178,7 +12015,7 @@ async function verifyToolingPlan(options, projectDir2, operationId) {
|
|
|
11178
12015
|
});
|
|
11179
12016
|
const isolated = await runIsolated(
|
|
11180
12017
|
projectDir2,
|
|
11181
|
-
|
|
12018
|
+
join52(mkdtempSync(join52(tmpdir(), "spfn-kit-")), operationId),
|
|
11182
12019
|
(workingCopy) => discovered.tooling.planInstall({
|
|
11183
12020
|
projectDir: workingCopy,
|
|
11184
12021
|
release: options.manifest.version
|
|
@@ -11486,8 +12323,8 @@ async function startRecovery(sink, adapters, activationId) {
|
|
|
11486
12323
|
}
|
|
11487
12324
|
|
|
11488
12325
|
// src/kit/customer-source.ts
|
|
11489
|
-
import { existsSync as
|
|
11490
|
-
import { dirname as
|
|
12326
|
+
import { existsSync as existsSync51, mkdirSync as mkdirSync18, readFileSync as readFileSync31, writeFileSync as writeFileSync33 } from "fs";
|
|
12327
|
+
import { dirname as dirname17, join as join53 } from "path";
|
|
11491
12328
|
var IGNORED_DIRECTORIES = ["node_modules", ".git", ".next", ".turbo", ".vercel", "dist", "coverage"];
|
|
11492
12329
|
var KIT_OWNED_FILES = ["pnpm-lock.yaml", "package.json", ".npmrc", "tsconfig.tsbuildinfo"];
|
|
11493
12330
|
function declaredPaths(declaration) {
|
|
@@ -11532,21 +12369,21 @@ function compareCustomerSource(before, after) {
|
|
|
11532
12369
|
return paths.filter((path5) => before[path5] !== after[path5]).map((path5) => ({ path: path5, before: before[path5] ?? null, after: after[path5] ?? null }));
|
|
11533
12370
|
}
|
|
11534
12371
|
function customerBaselinePath(projectDir2, operationId) {
|
|
11535
|
-
return
|
|
12372
|
+
return join53(projectDir2, ".spfn", "operations", `${operationId}.customer-source.json`);
|
|
11536
12373
|
}
|
|
11537
12374
|
function writeCustomerBaseline(projectDir2, baseline) {
|
|
11538
12375
|
const path5 = customerBaselinePath(projectDir2, baseline.operationId);
|
|
11539
|
-
mkdirSync18(
|
|
11540
|
-
|
|
12376
|
+
mkdirSync18(dirname17(path5), { recursive: true });
|
|
12377
|
+
writeFileSync33(path5, `${JSON.stringify(baseline, null, 4)}
|
|
11541
12378
|
`);
|
|
11542
12379
|
}
|
|
11543
12380
|
function readCustomerBaseline(projectDir2, operationId) {
|
|
11544
12381
|
const path5 = customerBaselinePath(projectDir2, operationId);
|
|
11545
|
-
if (!
|
|
12382
|
+
if (!existsSync51(path5)) {
|
|
11546
12383
|
return null;
|
|
11547
12384
|
}
|
|
11548
12385
|
try {
|
|
11549
|
-
const parsed = JSON.parse(
|
|
12386
|
+
const parsed = JSON.parse(readFileSync31(path5, "utf8"));
|
|
11550
12387
|
return parsed?.schemaVersion === 1 && typeof parsed.files === "object" ? parsed : null;
|
|
11551
12388
|
} catch {
|
|
11552
12389
|
return null;
|
|
@@ -11948,8 +12785,8 @@ function planResult(sink, plan, digest) {
|
|
|
11948
12785
|
}
|
|
11949
12786
|
|
|
11950
12787
|
// src/kit/operations/inspect.ts
|
|
11951
|
-
import { existsSync as
|
|
11952
|
-
import { join as
|
|
12788
|
+
import { existsSync as existsSync52 } from "fs";
|
|
12789
|
+
import { join as join54 } from "path";
|
|
11953
12790
|
async function runStatus(request2, adapters) {
|
|
11954
12791
|
const paths = kitPaths(request2.projectDir);
|
|
11955
12792
|
const lock = readInstalledLock(paths.lockFile);
|
|
@@ -12062,7 +12899,7 @@ async function runCheck(request2, adapters) {
|
|
|
12062
12899
|
fixCommand: `spfn kit resume ${journal.operationId} --json`
|
|
12063
12900
|
});
|
|
12064
12901
|
}
|
|
12065
|
-
if (!
|
|
12902
|
+
if (!existsSync52(join54(request2.projectDir, "node_modules"))) {
|
|
12066
12903
|
diagnostics.push({
|
|
12067
12904
|
code: "KIT_GATE_FAILED",
|
|
12068
12905
|
severity: "warning",
|
|
@@ -12210,7 +13047,7 @@ async function runAbandon(request2, adapters) {
|
|
|
12210
13047
|
|
|
12211
13048
|
// src/commands/kit/index.ts
|
|
12212
13049
|
function projectDir(options) {
|
|
12213
|
-
return
|
|
13050
|
+
return resolve6(options.dir ?? process.cwd());
|
|
12214
13051
|
}
|
|
12215
13052
|
function licenseKeyReader(options) {
|
|
12216
13053
|
return async () => {
|
|
@@ -12304,10 +13141,10 @@ function reportFailure(error, json) {
|
|
|
12304
13141
|
if (json) {
|
|
12305
13142
|
console.log(JSON.stringify(event));
|
|
12306
13143
|
} else {
|
|
12307
|
-
console.error(`${
|
|
12308
|
-
console.error(` ${
|
|
13144
|
+
console.error(`${chalk53.red("\u2717")} ${error.message}`);
|
|
13145
|
+
console.error(` ${chalk53.gray(error.code)}`);
|
|
12309
13146
|
if (error.next) {
|
|
12310
|
-
console.error(` ${
|
|
13147
|
+
console.error(` ${chalk53.gray("next:")} ${error.next.command}`);
|
|
12311
13148
|
}
|
|
12312
13149
|
}
|
|
12313
13150
|
process.exitCode = error.exitCode;
|
|
@@ -12331,7 +13168,7 @@ async function readOnlyAdapters(projectDir2) {
|
|
|
12331
13168
|
}
|
|
12332
13169
|
var kitCommand = new Command18("kit").description("Install, verify and update a Superfunction Kit in this project");
|
|
12333
13170
|
kitCommand.command("install <setup-url> <directory>").description("Install a Kit into a new, empty directory").option("--json", "newline-delimited JSON events and a machine-readable result").option("--license-key-stdin", "read the license key from stdin instead of prompting").action(async (setupUrl, directory, options) => {
|
|
12334
|
-
const targetDir =
|
|
13171
|
+
const targetDir = resolve6(directory);
|
|
12335
13172
|
await withResult(options.json === true, async () => runInstall({
|
|
12336
13173
|
setupUrl,
|
|
12337
13174
|
targetDir,
|
|
@@ -12359,7 +13196,7 @@ kitCommand.command("status").description("Report what is installed, activated an
|
|
|
12359
13196
|
try {
|
|
12360
13197
|
const report2 = await runStatus({ projectDir: dir }, await readOnlyAdapters(dir));
|
|
12361
13198
|
printJson(options.json === true, report2, () => {
|
|
12362
|
-
console.log(
|
|
13199
|
+
console.log(chalk53.bold(report2.installed ? `${report2.kitId} ${report2.release}` : "No Kit installed here"));
|
|
12363
13200
|
console.log(` activation: ${report2.activationId ?? "-"} credential: ${report2.credential}`);
|
|
12364
13201
|
console.log(` managed drift: ${report2.managedDrift} update available: ${report2.updateAvailable ?? "none"}`);
|
|
12365
13202
|
if (report2.operation) {
|
|
@@ -12376,15 +13213,15 @@ kitCommand.command("check").description("Check the installed release against its
|
|
|
12376
13213
|
const report2 = await runCheck({ projectDir: dir }, await readOnlyAdapters(dir));
|
|
12377
13214
|
printJson(options.json === true, report2, () => {
|
|
12378
13215
|
if (report2.diagnostics.length === 0) {
|
|
12379
|
-
console.log(`${
|
|
13216
|
+
console.log(`${chalk53.green("\u2713")} No problems found.`);
|
|
12380
13217
|
return;
|
|
12381
13218
|
}
|
|
12382
13219
|
for (const diagnostic of report2.diagnostics) {
|
|
12383
|
-
const mark = diagnostic.severity === "error" ?
|
|
13220
|
+
const mark = diagnostic.severity === "error" ? chalk53.red("\u2717") : chalk53.yellow("!");
|
|
12384
13221
|
console.log(`${mark} ${diagnostic.summary}`);
|
|
12385
|
-
console.log(` ${
|
|
13222
|
+
console.log(` ${chalk53.gray(diagnostic.code)}${diagnostic.path ? ` ${diagnostic.path}` : ""}`);
|
|
12386
13223
|
if (diagnostic.fixCommand) {
|
|
12387
|
-
console.log(` ${
|
|
13224
|
+
console.log(` ${chalk53.gray("next:")} ${diagnostic.fixCommand}`);
|
|
12388
13225
|
}
|
|
12389
13226
|
}
|
|
12390
13227
|
});
|
|
@@ -12474,8 +13311,8 @@ function parseKeys(raw) {
|
|
|
12474
13311
|
}
|
|
12475
13312
|
|
|
12476
13313
|
// src/kit/scaffold.ts
|
|
12477
|
-
import { mkdirSync as mkdirSync19, existsSync as
|
|
12478
|
-
import { dirname as
|
|
13314
|
+
import { mkdirSync as mkdirSync19, existsSync as existsSync53, readFileSync as readFileSync32, writeFileSync as writeFileSync34 } from "fs";
|
|
13315
|
+
import { dirname as dirname18, join as join55 } from "path";
|
|
12479
13316
|
|
|
12480
13317
|
// src/kit/http/registry.ts
|
|
12481
13318
|
import { createHash as createHash3 } from "crypto";
|
|
@@ -12663,9 +13500,9 @@ var KitRegistryProxyClient = class {
|
|
|
12663
13500
|
return response.bytes;
|
|
12664
13501
|
}
|
|
12665
13502
|
};
|
|
12666
|
-
function registryEntitlementProbe(client,
|
|
13503
|
+
function registryEntitlementProbe(client, resolve7) {
|
|
12667
13504
|
return async (request2) => {
|
|
12668
|
-
const target = await
|
|
13505
|
+
const target = await resolve7(request2);
|
|
12669
13506
|
if (target === null) {
|
|
12670
13507
|
return { entitled: false, reason: "unavailable" };
|
|
12671
13508
|
}
|
|
@@ -12821,20 +13658,20 @@ var ArtifactScaffoldPort = class {
|
|
|
12821
13658
|
}
|
|
12822
13659
|
};
|
|
12823
13660
|
function seedProjectName(targetDir, name) {
|
|
12824
|
-
const file =
|
|
12825
|
-
if (!
|
|
13661
|
+
const file = join55(targetDir, "package.json");
|
|
13662
|
+
if (!existsSync53(file)) {
|
|
12826
13663
|
return;
|
|
12827
13664
|
}
|
|
12828
13665
|
let parsed;
|
|
12829
13666
|
try {
|
|
12830
|
-
parsed = JSON.parse(
|
|
13667
|
+
parsed = JSON.parse(readFileSync32(file, "utf8"));
|
|
12831
13668
|
} catch {
|
|
12832
13669
|
throw new KitError("KIT_MANIFEST_INVALID", "The release scaffold's package.json is not readable JSON.", {
|
|
12833
13670
|
evidence: { path: "package.json" }
|
|
12834
13671
|
});
|
|
12835
13672
|
}
|
|
12836
13673
|
parsed.name = packageNameOf(name);
|
|
12837
|
-
|
|
13674
|
+
writeFileSync34(file, `${JSON.stringify(parsed, null, 4)}
|
|
12838
13675
|
`, "utf8");
|
|
12839
13676
|
}
|
|
12840
13677
|
function packageNameOf(name) {
|
|
@@ -12842,9 +13679,9 @@ function packageNameOf(name) {
|
|
|
12842
13679
|
return cleaned.length === 0 ? "kit-project" : cleaned;
|
|
12843
13680
|
}
|
|
12844
13681
|
function writeScaffoldRecord(targetDir, record) {
|
|
12845
|
-
const file =
|
|
12846
|
-
mkdirSync19(
|
|
12847
|
-
|
|
13682
|
+
const file = join55(targetDir, SCAFFOLD_RECORD_PATH);
|
|
13683
|
+
mkdirSync19(dirname18(file), { recursive: true });
|
|
13684
|
+
writeFileSync34(file, `${JSON.stringify(record, null, 4)}
|
|
12848
13685
|
`, "utf8");
|
|
12849
13686
|
}
|
|
12850
13687
|
|
|
@@ -13223,9 +14060,9 @@ function requireCatalogUrl(projectDir2) {
|
|
|
13223
14060
|
}
|
|
13224
14061
|
|
|
13225
14062
|
// src/kit/local/index.ts
|
|
13226
|
-
import { createRequire as
|
|
13227
|
-
import { join as
|
|
13228
|
-
import { pathToFileURL as
|
|
14063
|
+
import { createRequire as createRequire4 } from "module";
|
|
14064
|
+
import { join as join59 } from "path";
|
|
14065
|
+
import { pathToFileURL as pathToFileURL6 } from "url";
|
|
13229
14066
|
|
|
13230
14067
|
// src/kit/local/process.ts
|
|
13231
14068
|
import { execa as execa13 } from "execa";
|
|
@@ -13373,8 +14210,8 @@ ${result.stdout}`;
|
|
|
13373
14210
|
}
|
|
13374
14211
|
|
|
13375
14212
|
// src/kit/local/database.ts
|
|
13376
|
-
import { existsSync as
|
|
13377
|
-
import { dirname as
|
|
14213
|
+
import { existsSync as existsSync54 } from "fs";
|
|
14214
|
+
import { dirname as dirname19, join as join56 } from "path";
|
|
13378
14215
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
13379
14216
|
var DEFAULT_DATABASE_TIMEOUT_MS = 9e5;
|
|
13380
14217
|
var SpfnDatabasePort = class {
|
|
@@ -13457,13 +14294,13 @@ function spfnCommand() {
|
|
|
13457
14294
|
return bin === null ? { file: "spfn", args: [] } : { file: process.execPath, args: [bin] };
|
|
13458
14295
|
}
|
|
13459
14296
|
function resolveSpfnBin(from = fileURLToPath3(import.meta.url)) {
|
|
13460
|
-
let directory =
|
|
14297
|
+
let directory = dirname19(from);
|
|
13461
14298
|
for (let depth = 0; depth < 8; depth += 1) {
|
|
13462
|
-
const candidate =
|
|
13463
|
-
if (
|
|
14299
|
+
const candidate = join56(directory, "bin", "spfn.js");
|
|
14300
|
+
if (existsSync54(candidate)) {
|
|
13464
14301
|
return candidate;
|
|
13465
14302
|
}
|
|
13466
|
-
const parent =
|
|
14303
|
+
const parent = dirname19(directory);
|
|
13467
14304
|
if (parent === directory) {
|
|
13468
14305
|
break;
|
|
13469
14306
|
}
|
|
@@ -13473,8 +14310,8 @@ function resolveSpfnBin(from = fileURLToPath3(import.meta.url)) {
|
|
|
13473
14310
|
}
|
|
13474
14311
|
|
|
13475
14312
|
// src/kit/local/gates.ts
|
|
13476
|
-
import { existsSync as
|
|
13477
|
-
import { join as
|
|
14313
|
+
import { existsSync as existsSync55, readFileSync as readFileSync33 } from "fs";
|
|
14314
|
+
import { join as join57 } from "path";
|
|
13478
14315
|
var DEFAULT_GATE_TIMEOUT_MS = 9e5;
|
|
13479
14316
|
var GATE_PLANS = {
|
|
13480
14317
|
"kit-check": { scripts: [], fallback: spfnFallback(["kit", "check", "--json"]) },
|
|
@@ -13525,12 +14362,12 @@ var CommandGatePort = class {
|
|
|
13525
14362
|
}
|
|
13526
14363
|
};
|
|
13527
14364
|
function projectScripts(cwd) {
|
|
13528
|
-
const file =
|
|
13529
|
-
if (!
|
|
14365
|
+
const file = join57(cwd, "package.json");
|
|
14366
|
+
if (!existsSync55(file)) {
|
|
13530
14367
|
return {};
|
|
13531
14368
|
}
|
|
13532
14369
|
try {
|
|
13533
|
-
const parsed = JSON.parse(
|
|
14370
|
+
const parsed = JSON.parse(readFileSync33(file, "utf8"));
|
|
13534
14371
|
return parsed.scripts ?? {};
|
|
13535
14372
|
} catch {
|
|
13536
14373
|
return {};
|
|
@@ -13542,8 +14379,8 @@ function spfnFallback(args) {
|
|
|
13542
14379
|
}
|
|
13543
14380
|
|
|
13544
14381
|
// src/kit/local/git.ts
|
|
13545
|
-
import { existsSync as
|
|
13546
|
-
import { join as
|
|
14382
|
+
import { existsSync as existsSync56 } from "fs";
|
|
14383
|
+
import { join as join58 } from "path";
|
|
13547
14384
|
var FALLBACK_AUTHOR_NAME = "spfn kit";
|
|
13548
14385
|
var FALLBACK_AUTHOR_EMAIL = "kit@superfunction.xyz";
|
|
13549
14386
|
var DEFAULT_GIT_TIMEOUT_MS = 12e4;
|
|
@@ -13554,7 +14391,7 @@ var SystemGitPort = class {
|
|
|
13554
14391
|
}
|
|
13555
14392
|
/** Start a repository, or leave the one that is already there alone. */
|
|
13556
14393
|
async init(request2) {
|
|
13557
|
-
if (
|
|
14394
|
+
if (existsSync56(join58(request2.cwd, ".git"))) {
|
|
13558
14395
|
return;
|
|
13559
14396
|
}
|
|
13560
14397
|
const result = await this.git(request2.cwd, ["init"]);
|
|
@@ -13671,8 +14508,8 @@ function createKitLocalPorts(options = {}) {
|
|
|
13671
14508
|
}
|
|
13672
14509
|
function createProjectModuleLoader() {
|
|
13673
14510
|
return async (specifier, projectDir2) => {
|
|
13674
|
-
const
|
|
13675
|
-
return import(
|
|
14511
|
+
const resolve7 = createRequire4(join59(projectDir2, "package.json"));
|
|
14512
|
+
return import(pathToFileURL6(resolve7.resolve(specifier)).href);
|
|
13676
14513
|
};
|
|
13677
14514
|
}
|
|
13678
14515
|
|