spfn 0.3.0-beta.12 → 0.3.0-beta.14
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 +104 -8
- package/dist/index.js +1350 -518
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -949,7 +949,7 @@ import { dirname as dirname2, join as join10 } from "path";
|
|
|
949
949
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
950
950
|
function getCliVersion() {
|
|
951
951
|
if (true) {
|
|
952
|
-
return "0.3.0-beta.
|
|
952
|
+
return "0.3.0-beta.14";
|
|
953
953
|
}
|
|
954
954
|
return fromPackageJson ?? (fromPackageJson = readPackageVersion());
|
|
955
955
|
}
|
|
@@ -2381,7 +2381,7 @@ function writeAgentInstructions(cwd, agentFiles) {
|
|
|
2381
2381
|
}
|
|
2382
2382
|
}
|
|
2383
2383
|
function waitForReadyFile(filePath, timeoutMs = 3e4) {
|
|
2384
|
-
return new Promise((
|
|
2384
|
+
return new Promise((resolve7, reject) => {
|
|
2385
2385
|
if (existsSync16(filePath)) {
|
|
2386
2386
|
unlinkSync(filePath);
|
|
2387
2387
|
}
|
|
@@ -2395,7 +2395,7 @@ function waitForReadyFile(filePath, timeoutMs = 3e4) {
|
|
|
2395
2395
|
if (name === fileName && existsSync16(filePath)) {
|
|
2396
2396
|
watcher.close();
|
|
2397
2397
|
clearTimeout(timer);
|
|
2398
|
-
|
|
2398
|
+
resolve7(readFileSync9(filePath, "utf-8").trim());
|
|
2399
2399
|
}
|
|
2400
2400
|
});
|
|
2401
2401
|
});
|
|
@@ -2574,7 +2574,7 @@ catch (error)
|
|
|
2574
2574
|
serverProcess2.kill("SIGTERM");
|
|
2575
2575
|
await serverProcess2.catch(() => {
|
|
2576
2576
|
});
|
|
2577
|
-
await new Promise((
|
|
2577
|
+
await new Promise((resolve7) => setTimeout(resolve7, 500));
|
|
2578
2578
|
} catch (error) {
|
|
2579
2579
|
}
|
|
2580
2580
|
}
|
|
@@ -2617,12 +2617,12 @@ catch (error)
|
|
|
2617
2617
|
process.on("SIGTERM", cleanup2);
|
|
2618
2618
|
startWatcher2();
|
|
2619
2619
|
startServer2();
|
|
2620
|
-
await new Promise((
|
|
2620
|
+
await new Promise((resolve7) => {
|
|
2621
2621
|
const keepAlive = setInterval(() => {
|
|
2622
2622
|
}, 1e6);
|
|
2623
2623
|
process.once("beforeExit", () => {
|
|
2624
2624
|
clearInterval(keepAlive);
|
|
2625
|
-
|
|
2625
|
+
resolve7();
|
|
2626
2626
|
});
|
|
2627
2627
|
});
|
|
2628
2628
|
return;
|
|
@@ -2683,7 +2683,7 @@ catch (error)
|
|
|
2683
2683
|
serverProcess.kill("SIGTERM");
|
|
2684
2684
|
await serverProcess.catch(() => {
|
|
2685
2685
|
});
|
|
2686
|
-
await new Promise((
|
|
2686
|
+
await new Promise((resolve7) => setTimeout(resolve7, 500));
|
|
2687
2687
|
} catch (error) {
|
|
2688
2688
|
}
|
|
2689
2689
|
}
|
|
@@ -2737,12 +2737,12 @@ catch (error)
|
|
|
2737
2737
|
logger.warn(`[SPFN] Server readiness check timed out, starting Next.js anyway...`);
|
|
2738
2738
|
}
|
|
2739
2739
|
startNext();
|
|
2740
|
-
await new Promise((
|
|
2740
|
+
await new Promise((resolve7) => {
|
|
2741
2741
|
const keepAlive = setInterval(() => {
|
|
2742
2742
|
}, 1e6);
|
|
2743
2743
|
process.once("beforeExit", () => {
|
|
2744
2744
|
clearInterval(keepAlive);
|
|
2745
|
-
|
|
2745
|
+
resolve7();
|
|
2746
2746
|
});
|
|
2747
2747
|
});
|
|
2748
2748
|
});
|
|
@@ -3602,7 +3602,7 @@ async function runDrizzleCommand(command) {
|
|
|
3602
3602
|
}
|
|
3603
3603
|
const args = command.split(" ");
|
|
3604
3604
|
args.push(`--config=${configPath}`);
|
|
3605
|
-
return new Promise((
|
|
3605
|
+
return new Promise((resolve7, reject) => {
|
|
3606
3606
|
const drizzleProcess = spawn("drizzle-kit", args, {
|
|
3607
3607
|
stdio: "inherit",
|
|
3608
3608
|
// Allow interactive input
|
|
@@ -3617,7 +3617,7 @@ async function runDrizzleCommand(command) {
|
|
|
3617
3617
|
drizzleProcess.on("close", (code) => {
|
|
3618
3618
|
cleanup();
|
|
3619
3619
|
if (code === 0) {
|
|
3620
|
-
|
|
3620
|
+
resolve7();
|
|
3621
3621
|
} else {
|
|
3622
3622
|
reject(new Error(`drizzle-kit ${command} exited with code ${code}`));
|
|
3623
3623
|
}
|
|
@@ -4133,15 +4133,15 @@ async function confirmDangerousTarget(dbInfo) {
|
|
|
4133
4133
|
}
|
|
4134
4134
|
}
|
|
4135
4135
|
async function isPortAvailable(port) {
|
|
4136
|
-
return new Promise((
|
|
4136
|
+
return new Promise((resolve7) => {
|
|
4137
4137
|
const server = net.createServer();
|
|
4138
4138
|
server.once("error", () => {
|
|
4139
4139
|
server.close();
|
|
4140
|
-
|
|
4140
|
+
resolve7(false);
|
|
4141
4141
|
});
|
|
4142
4142
|
server.once("listening", () => {
|
|
4143
4143
|
server.close();
|
|
4144
|
-
|
|
4144
|
+
resolve7(true);
|
|
4145
4145
|
});
|
|
4146
4146
|
server.listen(port, "127.0.0.1");
|
|
4147
4147
|
});
|
|
@@ -4397,7 +4397,7 @@ async function dbBackup(options) {
|
|
|
4397
4397
|
pgDump.stderr?.on("data", (data) => {
|
|
4398
4398
|
errorOutput += data.toString();
|
|
4399
4399
|
});
|
|
4400
|
-
await new Promise((
|
|
4400
|
+
await new Promise((resolve7, reject) => {
|
|
4401
4401
|
pgDump.on("close", async (code) => {
|
|
4402
4402
|
if (code === 0) {
|
|
4403
4403
|
try {
|
|
@@ -4434,7 +4434,7 @@ async function dbBackup(options) {
|
|
|
4434
4434
|
tags: tags.length > 0 ? tags : void 0
|
|
4435
4435
|
};
|
|
4436
4436
|
await saveBackupMetadata(metadata, filename);
|
|
4437
|
-
|
|
4437
|
+
resolve7();
|
|
4438
4438
|
} catch (error) {
|
|
4439
4439
|
reject(error);
|
|
4440
4440
|
}
|
|
@@ -4913,7 +4913,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4913
4913
|
spinner.start();
|
|
4914
4914
|
}
|
|
4915
4915
|
});
|
|
4916
|
-
await new Promise((
|
|
4916
|
+
await new Promise((resolve7, reject) => {
|
|
4917
4917
|
restoreProcess.on("close", (code) => {
|
|
4918
4918
|
if (code === 0) {
|
|
4919
4919
|
const summary = objectCount > 0 ? ` (${objectCount} objects)` : "";
|
|
@@ -4927,7 +4927,7 @@ async function dbRestore(backupFile, options = {}) {
|
|
|
4927
4927
|
}
|
|
4928
4928
|
}
|
|
4929
4929
|
console.log(chalk26.green("\n\u2705 Database restored successfully"));
|
|
4930
|
-
|
|
4930
|
+
resolve7();
|
|
4931
4931
|
} else {
|
|
4932
4932
|
spinner.fail("Restore failed");
|
|
4933
4933
|
if (errors.length > 0) {
|
|
@@ -5334,25 +5334,32 @@ var addCommand = new Command12("add").description("Add a capability to this proj
|
|
|
5334
5334
|
// src/commands/env.ts
|
|
5335
5335
|
import { Command as Command13 } from "commander";
|
|
5336
5336
|
import chalk32 from "chalk";
|
|
5337
|
-
import { existsSync as
|
|
5338
|
-
import { resolve as
|
|
5337
|
+
import { existsSync as existsSync31, readFileSync as readFileSync16, writeFileSync as writeFileSync18 } from "fs";
|
|
5338
|
+
import { resolve as resolve4 } from "path";
|
|
5339
5339
|
import { parse as parse2 } from "dotenv";
|
|
5340
5340
|
|
|
5341
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";
|
|
5342
5345
|
var VALID_ENVS = ["local", "development", "staging", "production", "test"];
|
|
5343
|
-
async function loadEnvSchema(packageName) {
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
throw new Error(`Package ${packageName} does not export envSchema from config`);
|
|
5348
|
-
}
|
|
5349
|
-
return module.envSchema;
|
|
5350
|
-
} catch (error) {
|
|
5351
|
-
if (error instanceof Error && error.message.includes("does not export envSchema")) {
|
|
5352
|
-
throw error;
|
|
5353
|
-
}
|
|
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) => {
|
|
5354
5350
|
const message = error instanceof Error ? error.message : String(error);
|
|
5355
|
-
|
|
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;
|
|
5356
5363
|
}
|
|
5357
5364
|
}
|
|
5358
5365
|
function getTargetFile(schema) {
|
|
@@ -5368,6 +5375,416 @@ function secretEntries(schema) {
|
|
|
5368
5375
|
function generatableSecrets(schema) {
|
|
5369
5376
|
return secretEntries(schema).filter((entry) => !!entry.generate);
|
|
5370
5377
|
}
|
|
5378
|
+
function allowsLayer(entry, layer) {
|
|
5379
|
+
return entry.layer === void 0 || entry.layer === layer;
|
|
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" : ""}${entry.layer ? `, ${entry.layer} 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
|
+
}
|
|
5371
5788
|
|
|
5372
5789
|
// src/commands/env.ts
|
|
5373
5790
|
var BASE_ENV_FILES = {
|
|
@@ -5412,10 +5829,10 @@ function formatDefault(value, type) {
|
|
|
5412
5829
|
return chalk32.cyan(String(value));
|
|
5413
5830
|
}
|
|
5414
5831
|
async function listEnvVars(options) {
|
|
5415
|
-
const
|
|
5832
|
+
const label = describeEnvList(options);
|
|
5416
5833
|
try {
|
|
5417
|
-
const
|
|
5418
|
-
const allVars =
|
|
5834
|
+
const list = await loadEnvList(options);
|
|
5835
|
+
const allVars = orderedVars(list);
|
|
5419
5836
|
if (options.group) {
|
|
5420
5837
|
const grouped = allVars.reduce((acc, [key, schema]) => {
|
|
5421
5838
|
const target = getTargetFile(schema);
|
|
@@ -5424,22 +5841,27 @@ async function listEnvVars(options) {
|
|
|
5424
5841
|
return acc;
|
|
5425
5842
|
}, {});
|
|
5426
5843
|
console.log(chalk32.blue.bold(`
|
|
5427
|
-
\u{1F4CB} Environment Variables by File (${
|
|
5844
|
+
\u{1F4CB} Environment Variables by File (${label})
|
|
5428
5845
|
`));
|
|
5429
5846
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
5430
5847
|
console.log(chalk32.bold.magenta(`
|
|
5431
5848
|
${file}`));
|
|
5432
5849
|
console.log(chalk32.dim("\u2500".repeat(50)));
|
|
5433
5850
|
for (const [key, schema] of vars) {
|
|
5434
|
-
printEnvVar(key, schema);
|
|
5851
|
+
printEnvVar(key, schema, false, list.declaredBy[key]);
|
|
5435
5852
|
}
|
|
5436
5853
|
}
|
|
5437
5854
|
} else {
|
|
5438
5855
|
console.log(chalk32.blue.bold(`
|
|
5439
|
-
\u{1F4CB} Environment Variables (${
|
|
5856
|
+
\u{1F4CB} Environment Variables (${label})
|
|
5440
5857
|
`));
|
|
5441
|
-
for (const
|
|
5442
|
-
|
|
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
|
+
}
|
|
5443
5865
|
}
|
|
5444
5866
|
}
|
|
5445
5867
|
console.log(chalk32.dim("\n\u{1F4A1} Tip: Use `spfn env init` to generate .env template files\n"));
|
|
@@ -5450,13 +5872,19 @@ ${file}`));
|
|
|
5450
5872
|
process.exit(1);
|
|
5451
5873
|
}
|
|
5452
5874
|
}
|
|
5453
|
-
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 = []) {
|
|
5454
5879
|
const typeStr = formatType(schema.type);
|
|
5455
5880
|
const requiredStr = schema.required || schema.default !== void 0 ? chalk32.red("[required]") : chalk32.dim("[optional]");
|
|
5456
5881
|
const sensitiveStr = schema.sensitive ? chalk32.yellow(" [sensitive]") : "";
|
|
5457
5882
|
const fileStr = showFile ? chalk32.dim(` \u2192 ${getTargetFile(schema)}`) : "";
|
|
5458
5883
|
console.log(`${chalk32.bold.cyan(key)} ${chalk32.dim("(")}${typeStr}${chalk32.dim(")")} ${requiredStr}${sensitiveStr}${fileStr}`);
|
|
5459
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
|
+
}
|
|
5460
5888
|
if (schema.default !== void 0) {
|
|
5461
5889
|
console.log(` ${chalk32.dim("Default:")} ${formatDefault(schema.default, schema.type)}`);
|
|
5462
5890
|
}
|
|
@@ -5467,13 +5895,12 @@ function printEnvVar(key, schema, showFile = false) {
|
|
|
5467
5895
|
console.log();
|
|
5468
5896
|
}
|
|
5469
5897
|
async function showEnvStats(options) {
|
|
5470
|
-
const packageName = options.package || "@spfn/core";
|
|
5471
5898
|
try {
|
|
5472
|
-
const
|
|
5899
|
+
const list = await loadEnvList(options);
|
|
5473
5900
|
console.log(chalk32.blue.bold(`
|
|
5474
|
-
\u{1F4CA} Environment Variable Statistics (${
|
|
5901
|
+
\u{1F4CA} Environment Variable Statistics (${describeEnvList(options)})
|
|
5475
5902
|
`));
|
|
5476
|
-
const allVars =
|
|
5903
|
+
const allVars = orderedVars(list);
|
|
5477
5904
|
const required = allVars.filter(([_, schema]) => schema.required || schema.default !== void 0);
|
|
5478
5905
|
const optional = allVars.filter(([_, schema]) => !schema.required && schema.default === void 0);
|
|
5479
5906
|
const sensitive = allVars.filter(([_, schema]) => schema.sensitive);
|
|
@@ -5516,12 +5943,11 @@ async function showEnvStats(options) {
|
|
|
5516
5943
|
}
|
|
5517
5944
|
}
|
|
5518
5945
|
async function searchEnvVars(query, options) {
|
|
5519
|
-
const packageName = options.package || "@spfn/core";
|
|
5520
5946
|
try {
|
|
5521
|
-
const
|
|
5947
|
+
const list = await loadEnvList(options);
|
|
5522
5948
|
const normalizedQuery = query.toLowerCase();
|
|
5523
5949
|
const results = [];
|
|
5524
|
-
for (const [key, schema] of
|
|
5950
|
+
for (const [key, schema] of orderedVars(list)) {
|
|
5525
5951
|
const matchesKey = key.toLowerCase().includes(normalizedQuery);
|
|
5526
5952
|
const matchesDescription = schema.description.toLowerCase().includes(normalizedQuery);
|
|
5527
5953
|
if (matchesKey || matchesDescription) {
|
|
@@ -5554,10 +5980,11 @@ async function searchEnvVars(query, options) {
|
|
|
5554
5980
|
process.exit(1);
|
|
5555
5981
|
}
|
|
5556
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)";
|
|
5557
5984
|
var envCommand = new Command13("env").description("Manage environment variables");
|
|
5558
|
-
envCommand.command("list").description("List all environment variables from schema").option("-p, --package <package>",
|
|
5559
|
-
envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>",
|
|
5560
|
-
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);
|
|
5561
5988
|
function validateEnvOption(envValue) {
|
|
5562
5989
|
if (!VALID_ENVS.includes(envValue)) {
|
|
5563
5990
|
console.error(chalk32.red(`
|
|
@@ -5569,12 +5996,11 @@ function validateEnvOption(envValue) {
|
|
|
5569
5996
|
return envValue;
|
|
5570
5997
|
}
|
|
5571
5998
|
async function initEnvFiles(options) {
|
|
5572
|
-
const packageName = options.package || "@spfn/core";
|
|
5573
5999
|
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
5574
6000
|
const cwd = process.cwd();
|
|
5575
6001
|
try {
|
|
5576
|
-
const
|
|
5577
|
-
const allVars =
|
|
6002
|
+
const list = await loadEnvList(options);
|
|
6003
|
+
const allVars = orderedVars(list);
|
|
5578
6004
|
const grouped = allVars.reduce((acc, [key, schema]) => {
|
|
5579
6005
|
const target = getTargetFile(schema);
|
|
5580
6006
|
const exampleFile = target + ".example";
|
|
@@ -5597,14 +6023,14 @@ async function initEnvFiles(options) {
|
|
|
5597
6023
|
}
|
|
5598
6024
|
const allGrouped = { ...grouped, ...envSpecificFiles };
|
|
5599
6025
|
for (const [file, vars] of Object.entries(allGrouped)) {
|
|
5600
|
-
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
6026
|
+
writeEnvTemplate(cwd, file, vars, options.force ?? false, list);
|
|
5601
6027
|
}
|
|
5602
6028
|
} else {
|
|
5603
6029
|
console.log(chalk32.blue.bold(`
|
|
5604
6030
|
\u{1F680} Generating .env template files
|
|
5605
6031
|
`));
|
|
5606
6032
|
for (const [file, vars] of Object.entries(grouped)) {
|
|
5607
|
-
writeEnvTemplate(cwd, file, vars, options.force ?? false);
|
|
6033
|
+
writeEnvTemplate(cwd, file, vars, options.force ?? false, list);
|
|
5608
6034
|
}
|
|
5609
6035
|
}
|
|
5610
6036
|
console.log(chalk32.dim("\n\u{1F4A1} Copy .example files to create your actual .env files:"));
|
|
@@ -5623,22 +6049,28 @@ async function initEnvFiles(options) {
|
|
|
5623
6049
|
process.exit(1);
|
|
5624
6050
|
}
|
|
5625
6051
|
}
|
|
5626
|
-
function writeEnvTemplate(cwd, file, vars, force) {
|
|
5627
|
-
const filePath =
|
|
5628
|
-
if (
|
|
6052
|
+
function writeEnvTemplate(cwd, file, vars, force, list) {
|
|
6053
|
+
const filePath = resolve4(cwd, file);
|
|
6054
|
+
if (existsSync31(filePath) && !force) {
|
|
5629
6055
|
console.log(chalk32.yellow(` \u23ED\uFE0F ${file} already exists (use --force to overwrite)`));
|
|
5630
6056
|
return;
|
|
5631
6057
|
}
|
|
5632
|
-
writeFileSync18(filePath, generateEnvFileContent(vars), "utf-8");
|
|
6058
|
+
writeFileSync18(filePath, generateEnvFileContent(vars, list), "utf-8");
|
|
5633
6059
|
console.log(chalk32.green(` \u2705 ${file} (${vars.length} variables)`));
|
|
5634
6060
|
}
|
|
5635
|
-
function generateEnvFileContent(vars) {
|
|
6061
|
+
function generateEnvFileContent(vars, list) {
|
|
5636
6062
|
const lines = [
|
|
5637
6063
|
"# Auto-generated by spfn env init",
|
|
5638
6064
|
"# Copy this file and fill in the values",
|
|
5639
6065
|
""
|
|
5640
6066
|
];
|
|
6067
|
+
let section;
|
|
5641
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
|
+
}
|
|
5642
6074
|
lines.push(`# ${schema.description}`);
|
|
5643
6075
|
if (schema.required) {
|
|
5644
6076
|
lines.push(`# [required]`);
|
|
@@ -5658,12 +6090,11 @@ function generateEnvFileContent(vars) {
|
|
|
5658
6090
|
return lines.join("\n");
|
|
5659
6091
|
}
|
|
5660
6092
|
async function checkEnvFiles(options) {
|
|
5661
|
-
const packageName = options.package || "@spfn/core";
|
|
5662
6093
|
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
5663
6094
|
const cwd = process.cwd();
|
|
5664
6095
|
try {
|
|
5665
|
-
const
|
|
5666
|
-
const allVars =
|
|
6096
|
+
const list = await loadEnvList(options);
|
|
6097
|
+
const allVars = orderedVars(list);
|
|
5667
6098
|
const envLabel = targetEnv ? ` (${targetEnv})` : "";
|
|
5668
6099
|
console.log(chalk32.blue.bold(`
|
|
5669
6100
|
\u{1F50D} Checking .env files against schema${envLabel}
|
|
@@ -5673,11 +6104,11 @@ async function checkEnvFiles(options) {
|
|
|
5673
6104
|
const issues = [];
|
|
5674
6105
|
const warnings = [];
|
|
5675
6106
|
for (const file of filesToCheck) {
|
|
5676
|
-
const filePath =
|
|
5677
|
-
if (!
|
|
6107
|
+
const filePath = resolve4(cwd, file);
|
|
6108
|
+
if (!existsSync31(filePath)) {
|
|
5678
6109
|
continue;
|
|
5679
6110
|
}
|
|
5680
|
-
const content =
|
|
6111
|
+
const content = readFileSync16(filePath, "utf-8");
|
|
5681
6112
|
const parsed = parse2(content);
|
|
5682
6113
|
for (const [key, value] of Object.entries(parsed)) {
|
|
5683
6114
|
loadedEnv[key] = { value: value || "", file };
|
|
@@ -5690,7 +6121,7 @@ async function checkEnvFiles(options) {
|
|
|
5690
6121
|
const found = loadedEnv[key];
|
|
5691
6122
|
if (!found) {
|
|
5692
6123
|
if (schema.required && schema.default === void 0) {
|
|
5693
|
-
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`));
|
|
5694
6125
|
}
|
|
5695
6126
|
continue;
|
|
5696
6127
|
}
|
|
@@ -5699,34 +6130,33 @@ async function checkEnvFiles(options) {
|
|
|
5699
6130
|
const shouldBeNextjs = schema.nextjs ?? key.startsWith("NEXT_PUBLIC_");
|
|
5700
6131
|
if (!shouldBeNextjs && isNextjsFile && !isServerFile) {
|
|
5701
6132
|
if (schema.sensitive) {
|
|
5702
|
-
issues.push(
|
|
6133
|
+
issues.push(sourced(
|
|
6134
|
+
list,
|
|
6135
|
+
key,
|
|
5703
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!)`
|
|
5704
|
-
);
|
|
6137
|
+
));
|
|
5705
6138
|
} else {
|
|
5706
|
-
warnings.push(
|
|
6139
|
+
warnings.push(sourced(
|
|
6140
|
+
list,
|
|
6141
|
+
key,
|
|
5707
6142
|
`${chalk32.yellow("\u26A0")} ${chalk32.cyan(key)} should be in ${chalk32.magenta(expectedFile)}, but found in ${chalk32.dim(found.file)}`
|
|
5708
|
-
);
|
|
6143
|
+
));
|
|
5709
6144
|
}
|
|
5710
6145
|
}
|
|
5711
6146
|
}
|
|
5712
6147
|
for (const [key, { file }] of Object.entries(loadedEnv)) {
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
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` });
|
|
5716
6150
|
}
|
|
5717
6151
|
}
|
|
5718
6152
|
if (issues.length > 0) {
|
|
5719
6153
|
console.log(chalk32.red.bold("Issues:"));
|
|
5720
|
-
|
|
5721
|
-
console.log(` ${issue}`);
|
|
5722
|
-
}
|
|
6154
|
+
printBySource(issues);
|
|
5723
6155
|
console.log("");
|
|
5724
6156
|
}
|
|
5725
6157
|
if (warnings.length > 0) {
|
|
5726
6158
|
console.log(chalk32.yellow.bold("Warnings:"));
|
|
5727
|
-
|
|
5728
|
-
console.log(` ${warning}`);
|
|
5729
|
-
}
|
|
6159
|
+
printBySource(warnings);
|
|
5730
6160
|
console.log("");
|
|
5731
6161
|
}
|
|
5732
6162
|
if (issues.length === 0 && warnings.length === 0) {
|
|
@@ -5745,10 +6175,22 @@ async function checkEnvFiles(options) {
|
|
|
5745
6175
|
process.exit(1);
|
|
5746
6176
|
}
|
|
5747
6177
|
}
|
|
5748
|
-
|
|
5749
|
-
|
|
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);
|
|
5750
6193
|
async function validateEnvVars(options) {
|
|
5751
|
-
const packages = options.packages || ["@spfn/core"];
|
|
5752
6194
|
const targetEnv = options.env ? validateEnvOption(options.env) : void 0;
|
|
5753
6195
|
if (targetEnv) {
|
|
5754
6196
|
const { loadEnv: loadEnv10 } = await import("@spfn/core/env/loader");
|
|
@@ -5765,66 +6207,133 @@ async function validateEnvVars(options) {
|
|
|
5765
6207
|
\u{1F50D} Validating environment variables
|
|
5766
6208
|
`));
|
|
5767
6209
|
}
|
|
5768
|
-
const
|
|
5769
|
-
const
|
|
5770
|
-
for (const packageName of packages) {
|
|
5771
|
-
try {
|
|
5772
|
-
console.log(chalk32.dim(` \u{1F4E6} ${packageName}`));
|
|
5773
|
-
const envSchema = await loadEnvSchema(packageName);
|
|
5774
|
-
const { createEnvRegistry } = await import("@spfn/core/env");
|
|
5775
|
-
const registry = createEnvRegistry(envSchema);
|
|
5776
|
-
const result = registry.validateAll();
|
|
5777
|
-
for (const error of result.errors) {
|
|
5778
|
-
allErrors.push({ ...error, package: packageName });
|
|
5779
|
-
}
|
|
5780
|
-
for (const warning of result.warnings) {
|
|
5781
|
-
allWarnings.push({ ...warning, package: packageName });
|
|
5782
|
-
}
|
|
5783
|
-
} catch (error) {
|
|
5784
|
-
if (error instanceof Error && error.message.includes("does not export envSchema")) {
|
|
5785
|
-
console.log(chalk32.dim(` \u23ED\uFE0F No envSchema exported, skipping`));
|
|
5786
|
-
continue;
|
|
5787
|
-
}
|
|
5788
|
-
console.error(chalk32.red(` \u274C Failed to load: ${error instanceof Error ? error.message : String(error)}`));
|
|
5789
|
-
if (options.strict) {
|
|
5790
|
-
process.exit(1);
|
|
5791
|
-
}
|
|
5792
|
-
}
|
|
5793
|
-
}
|
|
6210
|
+
const sources = options.packages ? await loadPackagesToValidate(options.packages, options.strict ?? false) : await loadWholeListToValidate();
|
|
6211
|
+
const { errors, warnings } = await validateSources(sources);
|
|
5794
6212
|
console.log("");
|
|
5795
|
-
if (
|
|
5796
|
-
console.log(chalk32.red.bold(`\u274C Validation Errors (${
|
|
6213
|
+
if (errors.length > 0) {
|
|
6214
|
+
console.log(chalk32.red.bold(`\u274C Validation Errors (${errors.length}):
|
|
5797
6215
|
`));
|
|
5798
|
-
|
|
5799
|
-
console.log(` ${chalk32.red("\u2717")} ${chalk32.cyan(error.key)}`);
|
|
5800
|
-
console.log(` ${chalk32.dim(error.message)}`);
|
|
5801
|
-
console.log(` ${chalk32.dim(`from ${error.package}`)}`);
|
|
5802
|
-
console.log("");
|
|
5803
|
-
}
|
|
6216
|
+
printFindingsBySource(errors, chalk32.red("\u2717"));
|
|
5804
6217
|
}
|
|
5805
|
-
if (
|
|
5806
|
-
console.log(chalk32.yellow.bold(`\u26A0\uFE0F Warnings (${
|
|
6218
|
+
if (warnings.length > 0) {
|
|
6219
|
+
console.log(chalk32.yellow.bold(`\u26A0\uFE0F Warnings (${warnings.length}):
|
|
5807
6220
|
`));
|
|
5808
|
-
|
|
5809
|
-
console.log(` ${chalk32.yellow("\u26A0")} ${chalk32.cyan(warning.key)}`);
|
|
5810
|
-
console.log(` ${chalk32.dim(warning.message)}`);
|
|
5811
|
-
console.log("");
|
|
5812
|
-
}
|
|
6221
|
+
printFindingsBySource(warnings, chalk32.yellow("\u26A0"));
|
|
5813
6222
|
}
|
|
5814
|
-
if (
|
|
6223
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
5815
6224
|
console.log(chalk32.green.bold("\u2705 All environment variables are valid!\n"));
|
|
5816
|
-
} else if (
|
|
6225
|
+
} else if (errors.length === 0) {
|
|
5817
6226
|
console.log(chalk32.green("\u2705 No errors found."));
|
|
5818
|
-
console.log(chalk32.yellow(`\u26A0\uFE0F ${
|
|
6227
|
+
console.log(chalk32.yellow(`\u26A0\uFE0F ${warnings.length} warning(s) found.
|
|
5819
6228
|
`));
|
|
5820
6229
|
} else {
|
|
5821
6230
|
console.log(chalk32.red(`
|
|
5822
|
-
\u274C Validation failed with ${
|
|
6231
|
+
\u274C Validation failed with ${errors.length} error(s)
|
|
5823
6232
|
`));
|
|
5824
6233
|
process.exit(1);
|
|
5825
6234
|
}
|
|
5826
6235
|
}
|
|
5827
|
-
|
|
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);
|
|
5828
6337
|
|
|
5829
6338
|
// src/commands/ops/index.ts
|
|
5830
6339
|
import { Command as Command15 } from "commander";
|
|
@@ -6247,14 +6756,14 @@ function assertBatchSafe(label, value) {
|
|
|
6247
6756
|
function storeOpsToken(account, token) {
|
|
6248
6757
|
assertBatchSafe("Keychain account", account);
|
|
6249
6758
|
assertBatchSafe("Token", token);
|
|
6250
|
-
return new Promise((
|
|
6759
|
+
return new Promise((resolve7, reject) => {
|
|
6251
6760
|
const child = spawn5("security", ["-i"], { stdio: ["pipe", "ignore", "pipe"] });
|
|
6252
6761
|
let stderr = "";
|
|
6253
6762
|
child.stderr.on("data", (chunk) => stderr += String(chunk));
|
|
6254
6763
|
child.on("error", reject);
|
|
6255
6764
|
child.on("close", (code) => {
|
|
6256
6765
|
if (code === 0) {
|
|
6257
|
-
|
|
6766
|
+
resolve7();
|
|
6258
6767
|
return;
|
|
6259
6768
|
}
|
|
6260
6769
|
reject(new Error(`security exited with code ${code}: ${stderr.trim()}`));
|
|
@@ -6266,27 +6775,27 @@ function storeOpsToken(account, token) {
|
|
|
6266
6775
|
}
|
|
6267
6776
|
function loadOpsToken(account) {
|
|
6268
6777
|
assertBatchSafe("Keychain account", account);
|
|
6269
|
-
return new Promise((
|
|
6778
|
+
return new Promise((resolve7, reject) => {
|
|
6270
6779
|
execFile(
|
|
6271
6780
|
"security",
|
|
6272
6781
|
["find-generic-password", "-s", KEYCHAIN_SERVICE2, "-a", account, "-w"],
|
|
6273
6782
|
(error, stdout) => {
|
|
6274
6783
|
if (error) {
|
|
6275
6784
|
if (typeof error.code === "number" && error.code === 44) {
|
|
6276
|
-
|
|
6785
|
+
resolve7(null);
|
|
6277
6786
|
return;
|
|
6278
6787
|
}
|
|
6279
6788
|
reject(error);
|
|
6280
6789
|
return;
|
|
6281
6790
|
}
|
|
6282
|
-
|
|
6791
|
+
resolve7(stdout.trim() || null);
|
|
6283
6792
|
}
|
|
6284
6793
|
);
|
|
6285
6794
|
});
|
|
6286
6795
|
}
|
|
6287
6796
|
function deleteOpsToken(account) {
|
|
6288
6797
|
assertBatchSafe("Keychain account", account);
|
|
6289
|
-
return new Promise((
|
|
6798
|
+
return new Promise((resolve7, reject) => {
|
|
6290
6799
|
execFile(
|
|
6291
6800
|
"security",
|
|
6292
6801
|
["delete-generic-password", "-s", KEYCHAIN_SERVICE2, "-a", account],
|
|
@@ -6295,7 +6804,7 @@ function deleteOpsToken(account) {
|
|
|
6295
6804
|
reject(error);
|
|
6296
6805
|
return;
|
|
6297
6806
|
}
|
|
6298
|
-
|
|
6807
|
+
resolve7();
|
|
6299
6808
|
}
|
|
6300
6809
|
);
|
|
6301
6810
|
});
|
|
@@ -6383,14 +6892,14 @@ import prompts9 from "prompts";
|
|
|
6383
6892
|
import chalk34 from "chalk";
|
|
6384
6893
|
|
|
6385
6894
|
// src/utils/ops/auth-crypto.ts
|
|
6386
|
-
import { createRequire } from "module";
|
|
6387
|
-
import { join as
|
|
6388
|
-
import { pathToFileURL as
|
|
6895
|
+
import { createRequire as createRequire3 } from "module";
|
|
6896
|
+
import { join as join33 } from "path";
|
|
6897
|
+
import { pathToFileURL as pathToFileURL5 } from "url";
|
|
6389
6898
|
var CRYPTO_ENTRY_SINCE = "0.3.0-beta.2";
|
|
6390
6899
|
var CRYPTO_ENTRY = "@spfn/auth/crypto";
|
|
6391
6900
|
async function importFromApp(specifier) {
|
|
6392
|
-
const requireFromApp =
|
|
6393
|
-
return await import(
|
|
6901
|
+
const requireFromApp = createRequire3(join33(process.cwd(), "noop.js"));
|
|
6902
|
+
return await import(pathToFileURL5(requireFromApp.resolve(specifier)).href);
|
|
6394
6903
|
}
|
|
6395
6904
|
async function loadAuthCrypto() {
|
|
6396
6905
|
try {
|
|
@@ -6795,23 +7304,15 @@ import chalk37 from "chalk";
|
|
|
6795
7304
|
|
|
6796
7305
|
// src/commands/secret/options.ts
|
|
6797
7306
|
init_logger();
|
|
6798
|
-
function resolveEnv(env9) {
|
|
6799
|
-
const value = env9 ?? "local";
|
|
6800
|
-
if (!VALID_ENVS.includes(value)) {
|
|
6801
|
-
logger.error(`Invalid environment: "${value}". Valid values: ${VALID_ENVS.join(", ")}`);
|
|
6802
|
-
process.exit(1);
|
|
6803
|
-
}
|
|
6804
|
-
return value;
|
|
6805
|
-
}
|
|
6806
7307
|
|
|
6807
7308
|
// src/commands/secret/store-value.ts
|
|
6808
7309
|
init_logger();
|
|
6809
|
-
import { join as
|
|
7310
|
+
import { join as join35 } from "path";
|
|
6810
7311
|
init_env_file();
|
|
6811
7312
|
|
|
6812
7313
|
// src/utils/sops.ts
|
|
6813
7314
|
import { execa as execa11 } from "execa";
|
|
6814
|
-
import { existsSync as
|
|
7315
|
+
import { existsSync as existsSync32, mkdirSync as mkdirSync5, readFileSync as readFileSync17, writeFileSync as writeFileSync19 } from "fs";
|
|
6815
7316
|
import { dirname as dirname5 } from "path";
|
|
6816
7317
|
async function ensureSopsInstalled() {
|
|
6817
7318
|
try {
|
|
@@ -6821,53 +7322,89 @@ async function ensureSopsInstalled() {
|
|
|
6821
7322
|
}
|
|
6822
7323
|
}
|
|
6823
7324
|
async function sopsDecrypt(absFile) {
|
|
6824
|
-
if (!
|
|
7325
|
+
if (!existsSync32(absFile)) {
|
|
6825
7326
|
return {};
|
|
6826
7327
|
}
|
|
6827
|
-
const { stdout } = await execa11("sops", ["--decrypt", "--output-type", "json", absFile])
|
|
6828
|
-
|
|
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;
|
|
6829
7363
|
}
|
|
6830
7364
|
async function sopsSetValue(absFile, relFile, key, value) {
|
|
6831
|
-
if (
|
|
6832
|
-
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)));
|
|
6833
7367
|
return;
|
|
6834
7368
|
}
|
|
6835
7369
|
mkdirSync5(dirname5(absFile), { recursive: true });
|
|
6836
7370
|
const { stdout } = await execa11(
|
|
6837
7371
|
"sops",
|
|
6838
7372
|
[
|
|
6839
|
-
"
|
|
7373
|
+
"encrypt",
|
|
6840
7374
|
"--input-type",
|
|
6841
7375
|
"json",
|
|
6842
7376
|
"--output-type",
|
|
6843
7377
|
"json",
|
|
6844
7378
|
"--filename-override",
|
|
6845
|
-
relFile
|
|
6846
|
-
"/dev/stdin"
|
|
7379
|
+
relFile
|
|
6847
7380
|
],
|
|
6848
7381
|
{ input: JSON.stringify({ [key]: value }) }
|
|
6849
|
-
);
|
|
7382
|
+
).catch((error) => Promise.reject(sopsWriteError(relFile, key, error)));
|
|
6850
7383
|
writeFileSync19(absFile, stdout);
|
|
6851
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
|
+
}
|
|
6852
7389
|
async function sopsUpdateKeys(absFile) {
|
|
6853
7390
|
await execa11("sops", ["updatekeys", "-y", absFile]);
|
|
6854
7391
|
}
|
|
6855
7392
|
|
|
6856
7393
|
// src/utils/secret-config.ts
|
|
6857
|
-
import { existsSync as
|
|
6858
|
-
import { join as
|
|
7394
|
+
import { existsSync as existsSync33, readdirSync as readdirSync3 } from "fs";
|
|
7395
|
+
import { join as join34 } from "path";
|
|
6859
7396
|
var SECRETS_DIR = "secrets";
|
|
6860
|
-
function getSopsFile(cwd, env9) {
|
|
6861
|
-
const relFile = `${SECRETS_DIR}/${env9}.enc.json`;
|
|
6862
|
-
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 };
|
|
6863
7400
|
}
|
|
6864
7401
|
function findUp(cwd, filename, maxDepth = 6) {
|
|
6865
7402
|
let dir = cwd;
|
|
6866
7403
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
6867
|
-
if (
|
|
7404
|
+
if (existsSync33(join34(dir, filename))) {
|
|
6868
7405
|
return dir;
|
|
6869
7406
|
}
|
|
6870
|
-
const parent =
|
|
7407
|
+
const parent = join34(dir, "..");
|
|
6871
7408
|
if (parent === dir) {
|
|
6872
7409
|
break;
|
|
6873
7410
|
}
|
|
@@ -6877,17 +7414,17 @@ function findUp(cwd, filename, maxDepth = 6) {
|
|
|
6877
7414
|
}
|
|
6878
7415
|
function findSopsConfig(cwd) {
|
|
6879
7416
|
const dir = findUp(cwd, ".sops.yaml");
|
|
6880
|
-
return dir ?
|
|
7417
|
+
return dir ? join34(dir, ".sops.yaml") : null;
|
|
6881
7418
|
}
|
|
6882
7419
|
function hasSopsConfig(cwd) {
|
|
6883
7420
|
return findSopsConfig(cwd) !== null;
|
|
6884
7421
|
}
|
|
6885
7422
|
function listSopsFiles(cwd) {
|
|
6886
|
-
const dir =
|
|
6887
|
-
if (!
|
|
7423
|
+
const dir = join34(cwd, SECRETS_DIR);
|
|
7424
|
+
if (!existsSync33(dir)) {
|
|
6888
7425
|
return [];
|
|
6889
7426
|
}
|
|
6890
|
-
return
|
|
7427
|
+
return readdirSync3(dir).filter((name) => name.endsWith(".enc.json")).map((name) => join34(dir, name));
|
|
6891
7428
|
}
|
|
6892
7429
|
|
|
6893
7430
|
// src/commands/secret/store-value.ts
|
|
@@ -6900,15 +7437,15 @@ function assertValidKey(key) {
|
|
|
6900
7437
|
throw new Error(`Invalid secret key "${key}". Use an env-var name: letters, digits, and underscores, not starting with a digit.`);
|
|
6901
7438
|
}
|
|
6902
7439
|
}
|
|
6903
|
-
async function describeTarget(
|
|
6904
|
-
if (isLocalEnv(
|
|
7440
|
+
async function describeTarget(target) {
|
|
7441
|
+
if (isLocalEnv(target.env)) {
|
|
6905
7442
|
return detectStore().label;
|
|
6906
7443
|
}
|
|
6907
|
-
return `SOPS (${getSopsFile(process.cwd(),
|
|
7444
|
+
return `SOPS (${getSopsFile(process.cwd(), target.env, target.instance).relFile})`;
|
|
6908
7445
|
}
|
|
6909
|
-
async function storeSecret(cwd,
|
|
7446
|
+
async function storeSecret(cwd, target, key, value) {
|
|
6910
7447
|
assertValidKey(key);
|
|
6911
|
-
if (isLocalEnv(
|
|
7448
|
+
if (isLocalEnv(target.env)) {
|
|
6912
7449
|
const store = detectStore();
|
|
6913
7450
|
if (!await store.isAvailable()) {
|
|
6914
7451
|
throw new Error(
|
|
@@ -6916,7 +7453,7 @@ async function storeSecret(cwd, env9, key, value) {
|
|
|
6916
7453
|
);
|
|
6917
7454
|
}
|
|
6918
7455
|
await store.set(keychainName(key), value);
|
|
6919
|
-
const serverEnvPath =
|
|
7456
|
+
const serverEnvPath = join35(cwd, ".env.server");
|
|
6920
7457
|
const result = upsertEnvVar(serverEnvPath, key, keychainRef(key));
|
|
6921
7458
|
restrictEnvFilePerms(serverEnvPath);
|
|
6922
7459
|
ensureGitignored(cwd, [{ pattern: ".env.server", comment: "spfn server env (secrets)" }]);
|
|
@@ -6929,45 +7466,124 @@ async function storeSecret(cwd, env9, key, value) {
|
|
|
6929
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."
|
|
6930
7467
|
);
|
|
6931
7468
|
}
|
|
6932
|
-
const { absFile, relFile } = getSopsFile(cwd,
|
|
7469
|
+
const { absFile, relFile } = getSopsFile(cwd, target.env, target.instance);
|
|
6933
7470
|
await sopsSetValue(absFile, relFile, key, value);
|
|
6934
7471
|
logger.success(`Set ${key} in ${relFile} (encrypted). Commit and deploy to apply.`);
|
|
6935
7472
|
}
|
|
6936
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) => (entry.layer ?? "environment") === layer) : entries;
|
|
7518
|
+
}
|
|
7519
|
+
function assertTargetLayer(entry, target) {
|
|
7520
|
+
const layer = targetLayer(target);
|
|
7521
|
+
if (!entry || !layer || allowsLayer(entry, layer)) {
|
|
7522
|
+
return;
|
|
7523
|
+
}
|
|
7524
|
+
const hint = layer === "environment" ? "pass --instance <name>" : "drop --instance";
|
|
7525
|
+
logger.error(`${entry.key} is declared with layer "${entry.layer}" \u2014 ${hint}.`);
|
|
7526
|
+
process.exit(1);
|
|
7527
|
+
}
|
|
7528
|
+
|
|
6937
7529
|
// src/commands/secret/set.ts
|
|
6938
7530
|
async function secretSet(key, options) {
|
|
6939
|
-
const
|
|
6940
|
-
|
|
6941
|
-
|
|
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);
|
|
6942
7537
|
if (!resolvedKey) {
|
|
6943
7538
|
logger.error("No secret key provided.");
|
|
6944
7539
|
process.exit(1);
|
|
6945
7540
|
}
|
|
6946
|
-
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) {
|
|
6947
7554
|
const { value } = await prompts11({
|
|
6948
7555
|
type: "password",
|
|
6949
7556
|
name: "value",
|
|
6950
|
-
message: `Value for ${chalk37.cyan(
|
|
7557
|
+
message: `Value for ${chalk37.cyan(key)} (${target.instance ? `${target.env}.${target.instance}` : target.env})`
|
|
6951
7558
|
});
|
|
6952
7559
|
if (!value) {
|
|
6953
7560
|
logger.warn("Cancelled \u2014 no value entered.");
|
|
6954
7561
|
process.exit(0);
|
|
6955
7562
|
}
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
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.");
|
|
6961
7568
|
process.exit(1);
|
|
6962
7569
|
}
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
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);
|
|
6970
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) : [];
|
|
6971
7587
|
if (entries.length === 0) {
|
|
6972
7588
|
return void 0;
|
|
6973
7589
|
}
|
|
@@ -6983,14 +7599,13 @@ async function pickSecretKey(pkg) {
|
|
|
6983
7599
|
});
|
|
6984
7600
|
return key;
|
|
6985
7601
|
}
|
|
6986
|
-
async function
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
}
|
|
6993
|
-
} 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().`);
|
|
6994
7609
|
}
|
|
6995
7610
|
}
|
|
6996
7611
|
|
|
@@ -6998,31 +7613,43 @@ async function warnIfNotSecret(pkg, key) {
|
|
|
6998
7613
|
init_logger();
|
|
6999
7614
|
import chalk38 from "chalk";
|
|
7000
7615
|
async function secretList(options) {
|
|
7001
|
-
const
|
|
7002
|
-
const
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
}
|
|
7010
|
-
if (entries.length === 0) {
|
|
7011
|
-
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}.`);
|
|
7012
7624
|
return;
|
|
7013
7625
|
}
|
|
7014
|
-
const present = await loadPresence(env9, entries);
|
|
7015
7626
|
console.log(chalk38.blue.bold(`
|
|
7016
|
-
\u{1F511} Secrets (${
|
|
7627
|
+
\u{1F511} Secrets (${label}) \u2014 ${target.instance ? `${target.env}.${target.instance}` : target.env}
|
|
7017
7628
|
`));
|
|
7018
|
-
for (const
|
|
7019
|
-
|
|
7020
|
-
|
|
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
|
+
}
|
|
7021
7635
|
}
|
|
7636
|
+
printUnlisted(unlisted);
|
|
7022
7637
|
console.log();
|
|
7023
7638
|
}
|
|
7024
|
-
|
|
7025
|
-
|
|
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)) {
|
|
7026
7653
|
const store = detectStore();
|
|
7027
7654
|
if (!await store.isAvailable()) {
|
|
7028
7655
|
logger.warn(`${store.label} unavailable \u2014 status shown as missing.`);
|
|
@@ -7036,10 +7663,7 @@ async function loadPresence(env9, entries) {
|
|
|
7036
7663
|
}
|
|
7037
7664
|
return present;
|
|
7038
7665
|
}
|
|
7039
|
-
|
|
7040
|
-
const { absFile } = getSopsFile(process.cwd(), env9);
|
|
7041
|
-
const decrypted = await sopsDecrypt(absFile);
|
|
7042
|
-
return new Set(Object.keys(decrypted));
|
|
7666
|
+
return new Set(sopsKeyNames(getSopsFile(process.cwd(), target.env, target.instance).absFile));
|
|
7043
7667
|
}
|
|
7044
7668
|
function statusOf(entry, present) {
|
|
7045
7669
|
if (present) {
|
|
@@ -7062,25 +7686,19 @@ function badge(status) {
|
|
|
7062
7686
|
init_logger();
|
|
7063
7687
|
import chalk39 from "chalk";
|
|
7064
7688
|
async function secretGenerate(key, options) {
|
|
7065
|
-
const
|
|
7066
|
-
const
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
schema = await loadEnvSchema(pkg);
|
|
7070
|
-
} catch (error) {
|
|
7071
|
-
logger.error(error instanceof Error ? error.message : String(error));
|
|
7072
|
-
process.exit(1);
|
|
7073
|
-
}
|
|
7074
|
-
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));
|
|
7075
7693
|
if (targets.length === 0) {
|
|
7076
7694
|
logger.warn("No generatable secrets found (none declare a `generate` strategy).");
|
|
7077
7695
|
return;
|
|
7078
7696
|
}
|
|
7079
|
-
logger.step(`Generating ${targets.length} secret(s) \u2192 ${await describeTarget(
|
|
7697
|
+
logger.step(`Generating ${targets.length} secret(s) \u2192 ${await describeTarget(target)}`);
|
|
7080
7698
|
for (const entry of targets) {
|
|
7081
7699
|
const value = generateSecretValue(entry.generate);
|
|
7082
7700
|
try {
|
|
7083
|
-
await storeSecret(process.cwd(),
|
|
7701
|
+
await storeSecret(process.cwd(), target, entry.key, value);
|
|
7084
7702
|
} catch (error) {
|
|
7085
7703
|
logger.error(`${entry.key}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7086
7704
|
process.exit(1);
|
|
@@ -7108,23 +7726,17 @@ function requireGeneratable(schema, key) {
|
|
|
7108
7726
|
init_logger();
|
|
7109
7727
|
import chalk40 from "chalk";
|
|
7110
7728
|
async function secretRotate(key, options) {
|
|
7111
|
-
const
|
|
7112
|
-
const
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
schema = await loadEnvSchema(pkg);
|
|
7116
|
-
} catch (error) {
|
|
7117
|
-
logger.error(error instanceof Error ? error.message : String(error));
|
|
7118
|
-
process.exit(1);
|
|
7119
|
-
}
|
|
7120
|
-
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));
|
|
7121
7733
|
const generatable = targets.filter((entry) => entry.generate);
|
|
7122
7734
|
const external = targets.filter((entry) => !entry.generate);
|
|
7123
7735
|
if (generatable.length > 0) {
|
|
7124
|
-
logger.step(`Rotating ${generatable.length} secret(s) \u2192 ${await describeTarget(
|
|
7736
|
+
logger.step(`Rotating ${generatable.length} secret(s) \u2192 ${await describeTarget(target)}`);
|
|
7125
7737
|
for (const entry of generatable) {
|
|
7126
7738
|
try {
|
|
7127
|
-
await storeSecret(process.cwd(),
|
|
7739
|
+
await storeSecret(process.cwd(), target, entry.key, generateSecretValue(entry.generate));
|
|
7128
7740
|
} catch (error) {
|
|
7129
7741
|
logger.error(`${entry.key}: ${error instanceof Error ? error.message : String(error)}`);
|
|
7130
7742
|
process.exit(1);
|
|
@@ -7132,7 +7744,7 @@ async function secretRotate(key, options) {
|
|
|
7132
7744
|
}
|
|
7133
7745
|
}
|
|
7134
7746
|
for (const entry of external) {
|
|
7135
|
-
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}` : ""}\`.`);
|
|
7136
7748
|
}
|
|
7137
7749
|
if (generatable.length > 0) {
|
|
7138
7750
|
logger.info("Rotation updates the source only \u2014 commit and deploy to apply.");
|
|
@@ -7154,12 +7766,12 @@ function requireEntry(schema, key) {
|
|
|
7154
7766
|
// src/commands/secret/keygen.ts
|
|
7155
7767
|
init_logger();
|
|
7156
7768
|
import { execa as execa12 } from "execa";
|
|
7157
|
-
import { existsSync as
|
|
7769
|
+
import { existsSync as existsSync34, mkdirSync as mkdirSync6 } from "fs";
|
|
7158
7770
|
import { homedir } from "os";
|
|
7159
|
-
import { dirname as dirname6, join as
|
|
7771
|
+
import { dirname as dirname6, join as join36 } from "path";
|
|
7160
7772
|
import chalk41 from "chalk";
|
|
7161
7773
|
function ageKeyFile() {
|
|
7162
|
-
return process.env.SOPS_AGE_KEY_FILE ??
|
|
7774
|
+
return process.env.SOPS_AGE_KEY_FILE ?? join36(homedir(), ".config", "sops", "age", "keys.txt");
|
|
7163
7775
|
}
|
|
7164
7776
|
async function ensureAgeInstalled() {
|
|
7165
7777
|
try {
|
|
@@ -7179,7 +7791,7 @@ async function secretKeygen() {
|
|
|
7179
7791
|
process.exit(1);
|
|
7180
7792
|
}
|
|
7181
7793
|
const keyFile = ageKeyFile();
|
|
7182
|
-
if (
|
|
7794
|
+
if (existsSync34(keyFile)) {
|
|
7183
7795
|
const { stdout } = await execa12("age-keygen", ["-y", keyFile]);
|
|
7184
7796
|
logger.warn(`age key file already exists: ${keyFile}`);
|
|
7185
7797
|
printPublicKeys(publicKeys(stdout));
|
|
@@ -7206,8 +7818,8 @@ function printPublicKeys(keys) {
|
|
|
7206
7818
|
|
|
7207
7819
|
// src/commands/secret/recipients.ts
|
|
7208
7820
|
init_logger();
|
|
7209
|
-
import { existsSync as
|
|
7210
|
-
import { join as
|
|
7821
|
+
import { existsSync as existsSync35, readFileSync as readFileSync18, writeFileSync as writeFileSync20 } from "fs";
|
|
7822
|
+
import { join as join37 } from "path";
|
|
7211
7823
|
import { parse as parse3, stringify } from "yaml";
|
|
7212
7824
|
import chalk42 from "chalk";
|
|
7213
7825
|
var SOPS_CONFIG = ".sops.yaml";
|
|
@@ -7215,7 +7827,7 @@ var DEFAULT_PATH_REGEX = "secrets/.*\\.enc\\.json$";
|
|
|
7215
7827
|
var AGE_RECIPIENT = /^age1[0-9a-z]+$/;
|
|
7216
7828
|
async function secretRecipients(action, key, _options) {
|
|
7217
7829
|
const cwd = process.cwd();
|
|
7218
|
-
const configPath =
|
|
7830
|
+
const configPath = join37(cwd, SOPS_CONFIG);
|
|
7219
7831
|
if (action === "list") {
|
|
7220
7832
|
listRecipients(configPath);
|
|
7221
7833
|
return;
|
|
@@ -7247,7 +7859,7 @@ async function secretRecipients(action, key, _options) {
|
|
|
7247
7859
|
await reencrypt(cwd);
|
|
7248
7860
|
}
|
|
7249
7861
|
function listRecipients(configPath) {
|
|
7250
|
-
if (!
|
|
7862
|
+
if (!existsSync35(configPath)) {
|
|
7251
7863
|
logger.info(`No ${SOPS_CONFIG} found.`);
|
|
7252
7864
|
return;
|
|
7253
7865
|
}
|
|
@@ -7265,10 +7877,10 @@ function listRecipients(configPath) {
|
|
|
7265
7877
|
console.log();
|
|
7266
7878
|
}
|
|
7267
7879
|
function loadConfig(configPath) {
|
|
7268
|
-
if (!
|
|
7880
|
+
if (!existsSync35(configPath)) {
|
|
7269
7881
|
return { creation_rules: [] };
|
|
7270
7882
|
}
|
|
7271
|
-
return parse3(
|
|
7883
|
+
return parse3(readFileSync18(configPath, "utf-8")) ?? { creation_rules: [] };
|
|
7272
7884
|
}
|
|
7273
7885
|
function ensureRule(config) {
|
|
7274
7886
|
if (!config.creation_rules) {
|
|
@@ -7299,42 +7911,59 @@ async function reencrypt(cwd) {
|
|
|
7299
7911
|
}
|
|
7300
7912
|
|
|
7301
7913
|
// src/commands/secret/check.ts
|
|
7302
|
-
|
|
7303
|
-
import { join as join33 } from "path";
|
|
7914
|
+
import { join as join38 } from "path";
|
|
7304
7915
|
import chalk43 from "chalk";
|
|
7305
7916
|
init_env_file();
|
|
7306
7917
|
var COMMITTED_FILES = [".env", ".env.example", ".env.local.example", ".env.server.example"];
|
|
7307
7918
|
var PLACEHOLDER = /(your-|changeme|placeholder|example|<.*>)/i;
|
|
7308
7919
|
async function secretCheck(options) {
|
|
7309
7920
|
const cwd = process.cwd();
|
|
7310
|
-
const
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
entries = secretEntries(await loadEnvSchema(pkg));
|
|
7314
|
-
} catch (error) {
|
|
7315
|
-
logger.error(error instanceof Error ? error.message : String(error));
|
|
7316
|
-
process.exit(1);
|
|
7317
|
-
}
|
|
7921
|
+
const target = resolveTarget(options);
|
|
7922
|
+
const schema = (await loadSecretList(options)).schema;
|
|
7923
|
+
const entries = secretEntries(schema);
|
|
7318
7924
|
const secretKeys = new Set(entries.map((entry) => entry.key));
|
|
7319
7925
|
const issues = [];
|
|
7320
7926
|
const warnings = [];
|
|
7321
7927
|
for (const file of COMMITTED_FILES) {
|
|
7322
|
-
const parsed = parseEnvFile(
|
|
7928
|
+
const parsed = parseEnvFile(join38(cwd, file));
|
|
7323
7929
|
for (const [key, value] of Object.entries(parsed)) {
|
|
7324
7930
|
if (secretKeys.has(key) && value.length > 0 && !PLACEHOLDER.test(value)) {
|
|
7325
7931
|
issues.push(`${chalk43.cyan(key)} has a real value in committed ${chalk43.yellow(file)} \u2014 move it to the keychain/SOPS.`);
|
|
7326
7932
|
}
|
|
7327
7933
|
}
|
|
7328
7934
|
}
|
|
7329
|
-
const serverEnv = parseEnvFile(
|
|
7935
|
+
const serverEnv = parseEnvFile(join38(cwd, ".env.server"));
|
|
7330
7936
|
for (const key of secretKeys) {
|
|
7331
7937
|
const value = serverEnv[key];
|
|
7332
7938
|
if (value && !value.startsWith(KEYCHAIN_REF_PREFIX)) {
|
|
7333
7939
|
warnings.push(`${chalk43.cyan(key)} is plaintext in .env.server \u2014 run \`spfn secret set ${key}\` to move it to the keychain.`);
|
|
7334
7940
|
}
|
|
7335
7941
|
}
|
|
7942
|
+
if (!isLocalEnv(target.env)) {
|
|
7943
|
+
checkPlacement(cwd, schema, target, issues, warnings);
|
|
7944
|
+
}
|
|
7336
7945
|
report(issues, warnings, cwd);
|
|
7337
7946
|
}
|
|
7947
|
+
function checkPlacement(cwd, schema, target, issues, warnings) {
|
|
7948
|
+
const files = [{ file: getSopsFile(cwd, target.env), layer: "environment" }];
|
|
7949
|
+
const seenIn = /* @__PURE__ */ new Map();
|
|
7950
|
+
if (target.instance) {
|
|
7951
|
+
files.push({ file: getSopsFile(cwd, target.env, target.instance), layer: "instance" });
|
|
7952
|
+
}
|
|
7953
|
+
for (const { file, layer } of files) {
|
|
7954
|
+
for (const name of sopsKeyNames(file.absFile)) {
|
|
7955
|
+
const entry = Object.hasOwn(schema, name) ? schema[name] : void 0;
|
|
7956
|
+
if (!entry) {
|
|
7957
|
+
warnings.push(`${chalk43.cyan(name)} in ${chalk43.yellow(file.relFile)} is not in the env list \u2014 export skips it.`);
|
|
7958
|
+
} else if (!allowsLayer(entry, layer)) {
|
|
7959
|
+
issues.push(`${chalk43.cyan(name)} is declared with layer "${entry.layer}" but is in ${chalk43.yellow(file.relFile)}.`);
|
|
7960
|
+
} else if (seenIn.has(name)) {
|
|
7961
|
+
issues.push(`${chalk43.cyan(name)} is in both ${chalk43.yellow(seenIn.get(name))} and ${chalk43.yellow(file.relFile)}.`);
|
|
7962
|
+
}
|
|
7963
|
+
seenIn.set(name, file.relFile);
|
|
7964
|
+
}
|
|
7965
|
+
}
|
|
7966
|
+
}
|
|
7338
7967
|
function report(issues, warnings, cwd) {
|
|
7339
7968
|
console.log(chalk43.blue.bold("\n\u{1F50D} Secret hygiene check\n"));
|
|
7340
7969
|
for (const issue of issues) {
|
|
@@ -7355,26 +7984,229 @@ function report(issues, warnings, cwd) {
|
|
|
7355
7984
|
}
|
|
7356
7985
|
}
|
|
7357
7986
|
|
|
7987
|
+
// src/commands/secret/export.ts
|
|
7988
|
+
init_logger();
|
|
7989
|
+
init_env_file();
|
|
7990
|
+
import { existsSync as existsSync36, statSync } from "fs";
|
|
7991
|
+
import { dirname as dirname8, resolve as resolve5 } from "path";
|
|
7992
|
+
import chalk44 from "chalk";
|
|
7993
|
+
|
|
7994
|
+
// src/utils/secret-layers.ts
|
|
7995
|
+
function mergeLayers(schema, sources) {
|
|
7996
|
+
const merge = { values: {}, origins: {}, errors: [], warnings: [] };
|
|
7997
|
+
for (const [name, found] of collectListed(schema, sources, merge.warnings)) {
|
|
7998
|
+
const reason = placementError(schema[name], found) ?? valueError(schema[name], found[0].values[name]);
|
|
7999
|
+
if (reason) {
|
|
8000
|
+
merge.errors.push(`${name}: ${reason}`);
|
|
8001
|
+
continue;
|
|
8002
|
+
}
|
|
8003
|
+
merge.values[name] = found[0].values[name];
|
|
8004
|
+
merge.origins[name] = found[0].label;
|
|
8005
|
+
}
|
|
8006
|
+
for (const entry of Object.values(schema)) {
|
|
8007
|
+
if (isRequired(entry) && !sources.some((source) => Object.hasOwn(source.values, entry.key))) {
|
|
8008
|
+
merge.errors.push(`${entry.key}: required, and in none of the layers`);
|
|
8009
|
+
}
|
|
8010
|
+
}
|
|
8011
|
+
return merge;
|
|
8012
|
+
}
|
|
8013
|
+
function collectListed(schema, sources, warnings) {
|
|
8014
|
+
const found = /* @__PURE__ */ new Map();
|
|
8015
|
+
for (const source of sources) {
|
|
8016
|
+
for (const name of Object.keys(source.values)) {
|
|
8017
|
+
if (!Object.hasOwn(schema, name)) {
|
|
8018
|
+
warnings.push(`${name} (in ${source.label}) is not in the env list \u2014 not written`);
|
|
8019
|
+
continue;
|
|
8020
|
+
}
|
|
8021
|
+
found.set(name, [...found.get(name) ?? [], source]);
|
|
8022
|
+
}
|
|
8023
|
+
}
|
|
8024
|
+
return found;
|
|
8025
|
+
}
|
|
8026
|
+
function placementError(entry, found) {
|
|
8027
|
+
if (found.length > 1) {
|
|
8028
|
+
return `in more than one layer: ${found.map((source) => source.label).join(" and ")}`;
|
|
8029
|
+
}
|
|
8030
|
+
if (!allowsLayer(entry, found[0].kind)) {
|
|
8031
|
+
return `declared with layer "${entry.layer}", but found in ${found[0].label}`;
|
|
8032
|
+
}
|
|
8033
|
+
return void 0;
|
|
8034
|
+
}
|
|
8035
|
+
function valueError(entry, value) {
|
|
8036
|
+
if (entry.minLength !== void 0 && value.length < entry.minLength) {
|
|
8037
|
+
return `must be at least ${entry.minLength} characters long`;
|
|
8038
|
+
}
|
|
8039
|
+
if (!entry.validator) {
|
|
8040
|
+
return void 0;
|
|
8041
|
+
}
|
|
8042
|
+
try {
|
|
8043
|
+
entry.validator(value);
|
|
8044
|
+
return void 0;
|
|
8045
|
+
} catch (error) {
|
|
8046
|
+
return withoutValue(error instanceof Error ? error.message : String(error), value);
|
|
8047
|
+
}
|
|
8048
|
+
}
|
|
8049
|
+
function withoutValue(reason, value) {
|
|
8050
|
+
if (value.length > 0 && reason.includes(value)) {
|
|
8051
|
+
return "rejected by its validator (the message is withheld because it contains the value)";
|
|
8052
|
+
}
|
|
8053
|
+
return reason;
|
|
8054
|
+
}
|
|
8055
|
+
function isRequired(entry) {
|
|
8056
|
+
return entry.required === true && entry.default === void 0;
|
|
8057
|
+
}
|
|
8058
|
+
|
|
8059
|
+
// src/utils/dotenv-out.ts
|
|
8060
|
+
import { closeSync, fsyncSync, openSync, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync21 } from "fs";
|
|
8061
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
8062
|
+
import { basename as basename2, dirname as dirname7, join as join39 } from "path";
|
|
8063
|
+
import { parse as parse4 } from "dotenv";
|
|
8064
|
+
var QUOTES = ["'", "`", '"'];
|
|
8065
|
+
function formatDotenv(values) {
|
|
8066
|
+
const unrepresentable = [];
|
|
8067
|
+
const lines = Object.entries(values).map(([name, value]) => {
|
|
8068
|
+
const line = dotenvLine(name, value);
|
|
8069
|
+
if (!line) {
|
|
8070
|
+
unrepresentable.push(name);
|
|
8071
|
+
}
|
|
8072
|
+
return line;
|
|
8073
|
+
});
|
|
8074
|
+
const content = lines.join("\n") + "\n";
|
|
8075
|
+
const mismatched = Object.keys(values).filter((name) => parse4(content)[name] !== values[name]);
|
|
8076
|
+
const failed2 = [.../* @__PURE__ */ new Set([...unrepresentable, ...mismatched])];
|
|
8077
|
+
if (failed2.length > 0) {
|
|
8078
|
+
throw new Error(`Cannot write as dotenv without changing the value: ${failed2.join(", ")}`);
|
|
8079
|
+
}
|
|
8080
|
+
return content;
|
|
8081
|
+
}
|
|
8082
|
+
function dotenvLine(name, value) {
|
|
8083
|
+
const quoted = value.endsWith("\\") ? [] : QUOTES.map((quote) => `${quote}${value}${quote}`);
|
|
8084
|
+
return [...quoted, value].map((form) => `${name}=${form}`).find((line) => parse4(line)[name] === value);
|
|
8085
|
+
}
|
|
8086
|
+
function writePrivateFileAtomic(path5, content) {
|
|
8087
|
+
const temp = join39(dirname7(path5), `.${basename2(path5)}.${randomBytes3(6).toString("hex")}.tmp`);
|
|
8088
|
+
const fd = openSync(temp, "wx", 384);
|
|
8089
|
+
try {
|
|
8090
|
+
writeAndClose(fd, content);
|
|
8091
|
+
renameSync2(temp, path5);
|
|
8092
|
+
} finally {
|
|
8093
|
+
rmSync2(temp, { force: true });
|
|
8094
|
+
}
|
|
8095
|
+
}
|
|
8096
|
+
function writeAndClose(fd, content) {
|
|
8097
|
+
try {
|
|
8098
|
+
writeFileSync21(fd, content);
|
|
8099
|
+
fsyncSync(fd);
|
|
8100
|
+
} finally {
|
|
8101
|
+
closeSync(fd);
|
|
8102
|
+
}
|
|
8103
|
+
}
|
|
8104
|
+
|
|
8105
|
+
// src/commands/secret/export.ts
|
|
8106
|
+
async function secretExport(options) {
|
|
8107
|
+
const target = resolveTarget(options);
|
|
8108
|
+
if (!options.out) {
|
|
8109
|
+
logger.error("--out <file> is required: export writes values to a file, never to the terminal.");
|
|
8110
|
+
process.exit(1);
|
|
8111
|
+
}
|
|
8112
|
+
if (isLocalEnv(target.env)) {
|
|
8113
|
+
logger.error("export reads the encrypted files of a deployed environment; pass --env <env>.");
|
|
8114
|
+
process.exit(1);
|
|
8115
|
+
}
|
|
8116
|
+
await ensureSopsInstalled();
|
|
8117
|
+
const request2 = {
|
|
8118
|
+
cwd: process.cwd(),
|
|
8119
|
+
target,
|
|
8120
|
+
schema: (await loadSecretList({})).schema,
|
|
8121
|
+
withFiles: options.with ?? [],
|
|
8122
|
+
out: options.out
|
|
8123
|
+
};
|
|
8124
|
+
const report2 = await exportEnvFile(request2).catch((error) => {
|
|
8125
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
8126
|
+
return process.exit(1);
|
|
8127
|
+
});
|
|
8128
|
+
printReport2(report2, request2.out);
|
|
8129
|
+
}
|
|
8130
|
+
async function exportEnvFile(request2) {
|
|
8131
|
+
const out = resolve5(request2.cwd, request2.out);
|
|
8132
|
+
assertDirectory(dirname8(out), "--out");
|
|
8133
|
+
const merge = mergeLayers(request2.schema, await readLayers(request2));
|
|
8134
|
+
const report2 = { written: false, origins: merge.origins, errors: merge.errors, warnings: merge.warnings };
|
|
8135
|
+
if (merge.errors.length > 0) {
|
|
8136
|
+
return report2;
|
|
8137
|
+
}
|
|
8138
|
+
writePrivateFileAtomic(out, formatDotenv(merge.values));
|
|
8139
|
+
return { ...report2, written: true };
|
|
8140
|
+
}
|
|
8141
|
+
async function readLayers(request2) {
|
|
8142
|
+
const { cwd, target } = request2;
|
|
8143
|
+
const withFiles = request2.withFiles.map((file) => readWithFile(cwd, file));
|
|
8144
|
+
const sources = [{
|
|
8145
|
+
label: "environment",
|
|
8146
|
+
kind: "environment",
|
|
8147
|
+
values: await sopsDecrypt(getSopsFile(cwd, target.env).absFile)
|
|
8148
|
+
}];
|
|
8149
|
+
if (target.instance) {
|
|
8150
|
+
sources.push({
|
|
8151
|
+
label: "instance",
|
|
8152
|
+
kind: "instance",
|
|
8153
|
+
values: await sopsDecrypt(getSopsFile(cwd, target.env, target.instance).absFile)
|
|
8154
|
+
});
|
|
8155
|
+
}
|
|
8156
|
+
return [...sources, ...withFiles];
|
|
8157
|
+
}
|
|
8158
|
+
function readWithFile(cwd, file) {
|
|
8159
|
+
const path5 = resolve5(cwd, file);
|
|
8160
|
+
if (!existsSync36(path5)) {
|
|
8161
|
+
throw new Error(`--with file not found: ${path5}`);
|
|
8162
|
+
}
|
|
8163
|
+
return { label: `--with ${file}`, kind: "instance", values: parseEnvFile(path5) };
|
|
8164
|
+
}
|
|
8165
|
+
function assertDirectory(path5, option) {
|
|
8166
|
+
if (!existsSync36(path5) || !statSync(path5).isDirectory()) {
|
|
8167
|
+
throw new Error(`${option}: directory ${path5} does not exist. Create it first.`);
|
|
8168
|
+
}
|
|
8169
|
+
}
|
|
8170
|
+
function printReport2(report2, out) {
|
|
8171
|
+
for (const warning of report2.warnings) {
|
|
8172
|
+
logger.warn(warning);
|
|
8173
|
+
}
|
|
8174
|
+
if (!report2.written) {
|
|
8175
|
+
logger.error(`Nothing written \u2014 ${report2.errors.length} problem(s):`);
|
|
8176
|
+
report2.errors.forEach((error) => console.log(` ${chalk44.red("\u2717")} ${error}`));
|
|
8177
|
+
process.exit(1);
|
|
8178
|
+
}
|
|
8179
|
+
for (const [name, layer] of Object.entries(report2.origins)) {
|
|
8180
|
+
console.log(` ${chalk44.cyan(name)} ${chalk44.dim(`\u2190 ${layer}`)}`);
|
|
8181
|
+
}
|
|
8182
|
+
logger.success(`Wrote ${Object.keys(report2.origins).length} name(s) to ${out} (mode 0600).`);
|
|
8183
|
+
}
|
|
8184
|
+
|
|
7358
8185
|
// src/commands/secret/index.ts
|
|
7359
8186
|
var ENV_OPTION = ["-e, --env <env>", "Target environment (local | development | staging | production)", "local"];
|
|
7360
|
-
var
|
|
8187
|
+
var INSTANCE_OPTION = ["-i, --instance <name>", "Target one instance of the environment: secrets/<env>.<instance>.enc.json (lowercase letters, digits, dashes)"];
|
|
8188
|
+
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)"];
|
|
7361
8189
|
var secretCommand = new Command16("secret").description("Manage secrets: keychain locally, SOPS (age / GCP KMS / AWS KMS) for deployed environments");
|
|
7362
|
-
secretCommand.command("set [key]").description("Store a secret value (prompts for the value, masked)").option(...ENV_OPTION).option(...PKG_OPTION).action(secretSet);
|
|
7363
|
-
secretCommand.command("list").description("List declared secrets and their status (never prints values)").option(...ENV_OPTION).option(...PKG_OPTION).action(secretList);
|
|
7364
|
-
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);
|
|
7365
|
-
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);
|
|
8190
|
+
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);
|
|
8191
|
+
secretCommand.command("list").description("List declared secrets and their status (never prints values)").option(...ENV_OPTION).option(...INSTANCE_OPTION).option(...PKG_OPTION).action(secretList);
|
|
8192
|
+
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);
|
|
8193
|
+
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);
|
|
7366
8194
|
secretCommand.command("keygen").description("Generate an age key pair for SOPS (no-cloud backend)").action(secretKeygen);
|
|
7367
8195
|
secretCommand.command("recipients <action> [key]").description("Manage .sops.yaml age recipients: add | remove | list").action(secretRecipients);
|
|
7368
|
-
secretCommand.command("check").description("Static hygiene lint \u2014 flag plaintext secret leaks").option(...PKG_OPTION).action(secretCheck);
|
|
8196
|
+
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);
|
|
8197
|
+
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);
|
|
8198
|
+
function collect(value, previous) {
|
|
8199
|
+
return [...previous, value];
|
|
8200
|
+
}
|
|
7369
8201
|
|
|
7370
8202
|
// src/commands/cloud/index.ts
|
|
7371
8203
|
import { Command as Command17 } from "commander";
|
|
7372
8204
|
|
|
7373
8205
|
// src/commands/cloud/link.ts
|
|
7374
8206
|
init_logger();
|
|
7375
|
-
import { basename as
|
|
8207
|
+
import { basename as basename3 } from "path";
|
|
7376
8208
|
import prompts12 from "prompts";
|
|
7377
|
-
import
|
|
8209
|
+
import chalk45 from "chalk";
|
|
7378
8210
|
|
|
7379
8211
|
// src/utils/cloud/tokens.ts
|
|
7380
8212
|
var TOKEN_KEYS = {
|
|
@@ -7412,20 +8244,20 @@ async function deleteCloudToken(provider) {
|
|
|
7412
8244
|
|
|
7413
8245
|
// src/utils/cloud/config.ts
|
|
7414
8246
|
init_env_file();
|
|
7415
|
-
import { existsSync as
|
|
7416
|
-
import { dirname as
|
|
7417
|
-
var CONFIG_REL_PATH =
|
|
8247
|
+
import { existsSync as existsSync37, mkdirSync as mkdirSync7, readFileSync as readFileSync19, writeFileSync as writeFileSync22 } from "fs";
|
|
8248
|
+
import { dirname as dirname9, join as join40 } from "path";
|
|
8249
|
+
var CONFIG_REL_PATH = join40(".spfn", "cloud.json");
|
|
7418
8250
|
function cloudConfigPath(cwd) {
|
|
7419
|
-
return
|
|
8251
|
+
return join40(cwd, CONFIG_REL_PATH);
|
|
7420
8252
|
}
|
|
7421
8253
|
function readCloudConfig(cwd) {
|
|
7422
8254
|
const path5 = cloudConfigPath(cwd);
|
|
7423
|
-
if (!
|
|
8255
|
+
if (!existsSync37(path5)) {
|
|
7424
8256
|
return {};
|
|
7425
8257
|
}
|
|
7426
8258
|
let parsed;
|
|
7427
8259
|
try {
|
|
7428
|
-
parsed = JSON.parse(
|
|
8260
|
+
parsed = JSON.parse(readFileSync19(path5, "utf-8"));
|
|
7429
8261
|
} catch {
|
|
7430
8262
|
throw new Error(`${path5} is not valid JSON \u2014 delete it and run \`spfn cloud link\` again.`);
|
|
7431
8263
|
}
|
|
@@ -7436,8 +8268,8 @@ function readCloudConfig(cwd) {
|
|
|
7436
8268
|
}
|
|
7437
8269
|
function writeCloudConfig(cwd, config) {
|
|
7438
8270
|
const path5 = cloudConfigPath(cwd);
|
|
7439
|
-
mkdirSync7(
|
|
7440
|
-
|
|
8271
|
+
mkdirSync7(dirname9(path5), { recursive: true });
|
|
8272
|
+
writeFileSync22(path5, JSON.stringify(config, null, 4) + "\n", "utf-8");
|
|
7441
8273
|
ensureGitignored(cwd, [{ pattern: ".spfn/cloud.json", comment: "spfn cloud link state (per-user accounts)" }]);
|
|
7442
8274
|
}
|
|
7443
8275
|
function requireLinked(config, provider) {
|
|
@@ -7666,7 +8498,7 @@ async function cloudLink(options) {
|
|
|
7666
8498
|
logger.error(error instanceof Error ? error.message : String(error));
|
|
7667
8499
|
process.exit(1);
|
|
7668
8500
|
}
|
|
7669
|
-
logger.success(`Link state saved to ${
|
|
8501
|
+
logger.success(`Link state saved to ${chalk45.cyan(cloudConfigPath(cwd))} (identifiers only \u2014 tokens stay in the keychain).`);
|
|
7670
8502
|
logger.info("Next: `spfn cloud status` for limits vs usage, `spfn cloud env pull` for project keys.");
|
|
7671
8503
|
}
|
|
7672
8504
|
async function linkVercel(config, cwd, options) {
|
|
@@ -7686,7 +8518,7 @@ async function linkVercel(config, cwd, options) {
|
|
|
7686
8518
|
throw new Error(`No Vercel project named "${options.vercelProject}" \u2014 available: ${projects.map((p) => p.name).join(", ")}.`);
|
|
7687
8519
|
}
|
|
7688
8520
|
} else {
|
|
7689
|
-
const defaultIndex = Math.max(0, projects.findIndex((p) => p.name ===
|
|
8521
|
+
const defaultIndex = Math.max(0, projects.findIndex((p) => p.name === basename3(cwd)));
|
|
7690
8522
|
({ projectId } = await prompts12({
|
|
7691
8523
|
type: "select",
|
|
7692
8524
|
name: "projectId",
|
|
@@ -7758,7 +8590,7 @@ async function obtainToken(provider, label, envVar, createUrl, replace) {
|
|
|
7758
8590
|
return fromKeychain;
|
|
7759
8591
|
}
|
|
7760
8592
|
}
|
|
7761
|
-
logger.info(`Create one at ${
|
|
8593
|
+
logger.info(`Create one at ${chalk45.cyan(createUrl)} if you don't have it yet.`);
|
|
7762
8594
|
const { token } = await prompts12({
|
|
7763
8595
|
type: "password",
|
|
7764
8596
|
name: "token",
|
|
@@ -7771,7 +8603,7 @@ async function obtainToken(provider, label, envVar, createUrl, replace) {
|
|
|
7771
8603
|
}
|
|
7772
8604
|
|
|
7773
8605
|
// src/commands/cloud/limits.ts
|
|
7774
|
-
import
|
|
8606
|
+
import chalk47 from "chalk";
|
|
7775
8607
|
|
|
7776
8608
|
// src/utils/cloud/limits-data.ts
|
|
7777
8609
|
var VERCEL_HOBBY_LIMITS = [
|
|
@@ -7810,21 +8642,21 @@ var SUPABASE_FREE_NOTES = [
|
|
|
7810
8642
|
var LIMITS_VERIFIED_ON = "2026-08-13";
|
|
7811
8643
|
|
|
7812
8644
|
// src/utils/cloud/format.ts
|
|
7813
|
-
import
|
|
8645
|
+
import chalk46 from "chalk";
|
|
7814
8646
|
var MIGRATION_PROMPT_THRESHOLD = 0.8;
|
|
7815
8647
|
function formatPercent(used, limit) {
|
|
7816
8648
|
if (limit <= 0) {
|
|
7817
|
-
return
|
|
8649
|
+
return chalk46.dim("n/a");
|
|
7818
8650
|
}
|
|
7819
8651
|
const ratio = used / limit;
|
|
7820
8652
|
const text = `${(ratio * 100).toFixed(1)}%`;
|
|
7821
8653
|
if (ratio >= 1) {
|
|
7822
|
-
return
|
|
8654
|
+
return chalk46.red.bold(text);
|
|
7823
8655
|
}
|
|
7824
8656
|
if (ratio >= MIGRATION_PROMPT_THRESHOLD) {
|
|
7825
|
-
return
|
|
8657
|
+
return chalk46.yellow.bold(text);
|
|
7826
8658
|
}
|
|
7827
|
-
return
|
|
8659
|
+
return chalk46.green(text);
|
|
7828
8660
|
}
|
|
7829
8661
|
function isNearLimit(used, limit) {
|
|
7830
8662
|
return limit > 0 && used / limit >= MIGRATION_PROMPT_THRESHOLD;
|
|
@@ -7861,25 +8693,25 @@ var PER_LABEL = {
|
|
|
7861
8693
|
async function cloudLimits() {
|
|
7862
8694
|
printPlan("Vercel Hobby", VERCEL_HOBBY_LIMITS, VERCEL_HOBBY_NOTES);
|
|
7863
8695
|
printPlan("Supabase Free", SUPABASE_FREE_LIMITS, SUPABASE_FREE_NOTES);
|
|
7864
|
-
console.log(
|
|
7865
|
-
console.log(
|
|
8696
|
+
console.log(chalk47.dim(`Numbers verified against the official docs on ${LIMITS_VERIFIED_ON}.`));
|
|
8697
|
+
console.log(chalk47.dim("Live usage against these limits: `spfn cloud status`.\n"));
|
|
7866
8698
|
}
|
|
7867
8699
|
function printPlan(title, limits, notes) {
|
|
7868
|
-
console.log(
|
|
8700
|
+
console.log(chalk47.bold(`
|
|
7869
8701
|
${title}`));
|
|
7870
8702
|
for (const limit of limits) {
|
|
7871
8703
|
const amount = `${formatCount(limit.limit)} ${limit.unit}`;
|
|
7872
|
-
console.log(` ${limit.label.padEnd(34)} ${amount.padEnd(26)} ${
|
|
8704
|
+
console.log(` ${limit.label.padEnd(34)} ${amount.padEnd(26)} ${chalk47.dim(PER_LABEL[limit.per])}`);
|
|
7873
8705
|
}
|
|
7874
8706
|
for (const note of notes) {
|
|
7875
|
-
console.log(
|
|
8707
|
+
console.log(chalk47.dim(` \u2022 ${note}`));
|
|
7876
8708
|
}
|
|
7877
8709
|
console.log("");
|
|
7878
8710
|
}
|
|
7879
8711
|
|
|
7880
8712
|
// src/commands/cloud/usage.ts
|
|
7881
8713
|
init_logger();
|
|
7882
|
-
import
|
|
8714
|
+
import chalk48 from "chalk";
|
|
7883
8715
|
|
|
7884
8716
|
// src/commands/cloud/collect.ts
|
|
7885
8717
|
async function collectSnapshot(cwd) {
|
|
@@ -7972,10 +8804,10 @@ function printVercel(snapshot) {
|
|
|
7972
8804
|
if (!snapshot.vercel) {
|
|
7973
8805
|
return;
|
|
7974
8806
|
}
|
|
7975
|
-
console.log(
|
|
8807
|
+
console.log(chalk48.bold(`
|
|
7976
8808
|
Vercel \u2014 account-wide usage, rolling 30 days (linked project: ${snapshot.vercel.projectName})`));
|
|
7977
8809
|
if (snapshot.vercel.services.length === 0) {
|
|
7978
|
-
console.log(
|
|
8810
|
+
console.log(chalk48.dim(" No usage recorded in the billing feed yet."));
|
|
7979
8811
|
}
|
|
7980
8812
|
for (const service of snapshot.vercel.services) {
|
|
7981
8813
|
const amount = `${formatCount(service.consumed)} ${service.unit}`.trim();
|
|
@@ -7987,12 +8819,12 @@ function printSupabase(snapshot) {
|
|
|
7987
8819
|
return;
|
|
7988
8820
|
}
|
|
7989
8821
|
const { projectName, status, dbSizeBytes, dailyApiCount } = snapshot.supabase;
|
|
7990
|
-
console.log(
|
|
8822
|
+
console.log(chalk48.bold(`
|
|
7991
8823
|
Supabase \u2014 ${projectName}`));
|
|
7992
8824
|
console.log(` ${"Project status".padEnd(34)} ${status}`);
|
|
7993
|
-
console.log(` ${"Database size".padEnd(34)} ${dbSizeBytes === null ?
|
|
7994
|
-
console.log(` ${"API requests (last 24h)".padEnd(34)} ${dailyApiCount === null ?
|
|
7995
|
-
console.log(
|
|
8825
|
+
console.log(` ${"Database size".padEnd(34)} ${dbSizeBytes === null ? chalk48.dim("unavailable") : formatBytes2(dbSizeBytes)}`);
|
|
8826
|
+
console.log(` ${"API requests (last 24h)".padEnd(34)} ${dailyApiCount === null ? chalk48.dim("unavailable") : formatCount(dailyApiCount)}`);
|
|
8827
|
+
console.log(chalk48.dim(" Org totals (egress, MAU) have no public API \u2014 exact numbers: supabase.com/dashboard \u2192 Usage."));
|
|
7996
8828
|
}
|
|
7997
8829
|
function printProblems(snapshot) {
|
|
7998
8830
|
console.log("");
|
|
@@ -8006,7 +8838,7 @@ function printProblems(snapshot) {
|
|
|
8006
8838
|
|
|
8007
8839
|
// src/commands/cloud/status.ts
|
|
8008
8840
|
init_logger();
|
|
8009
|
-
import
|
|
8841
|
+
import chalk49 from "chalk";
|
|
8010
8842
|
async function cloudStatus() {
|
|
8011
8843
|
const snapshot = await collectSnapshot(process.cwd());
|
|
8012
8844
|
const nearLimit = [];
|
|
@@ -8025,10 +8857,10 @@ function printVercel2(snapshot, nearLimit) {
|
|
|
8025
8857
|
if (!snapshot.vercel) {
|
|
8026
8858
|
return;
|
|
8027
8859
|
}
|
|
8028
|
-
console.log(
|
|
8860
|
+
console.log(chalk49.bold(`
|
|
8029
8861
|
Vercel Hobby \u2014 account-wide usage, rolling 30 days (linked project: ${snapshot.vercel.projectName}; limits as of ${LIMITS_VERIFIED_ON})`));
|
|
8030
8862
|
if (snapshot.vercel.services.length === 0) {
|
|
8031
|
-
console.log(
|
|
8863
|
+
console.log(chalk49.dim(" No usage recorded in the billing feed yet."));
|
|
8032
8864
|
}
|
|
8033
8865
|
for (const service of snapshot.vercel.services) {
|
|
8034
8866
|
const limit = matchVercelLimit(service.serviceName);
|
|
@@ -8047,12 +8879,12 @@ function printSupabase2(snapshot, nearLimit) {
|
|
|
8047
8879
|
return;
|
|
8048
8880
|
}
|
|
8049
8881
|
const { projectName, status, dbSizeBytes, dailyApiCount } = snapshot.supabase;
|
|
8050
|
-
console.log(
|
|
8882
|
+
console.log(chalk49.bold(`
|
|
8051
8883
|
Supabase Free \u2014 ${projectName}`));
|
|
8052
8884
|
if (isSupabasePaused(status)) {
|
|
8053
|
-
console.log(` ${"Project status".padEnd(34)} ${
|
|
8885
|
+
console.log(` ${"Project status".padEnd(34)} ${chalk49.red.bold(status)} \u2014 run \`spfn cloud keepalive\` after restoring it`);
|
|
8054
8886
|
} else {
|
|
8055
|
-
console.log(` ${"Project status".padEnd(34)} ${
|
|
8887
|
+
console.log(` ${"Project status".padEnd(34)} ${chalk49.green(status)}`);
|
|
8056
8888
|
}
|
|
8057
8889
|
if (dbSizeBytes !== null) {
|
|
8058
8890
|
const dbLimit = SUPABASE_FREE_LIMITS.find((l) => l.key === "db-size");
|
|
@@ -8062,25 +8894,25 @@ Supabase Free \u2014 ${projectName}`));
|
|
|
8062
8894
|
nearLimit.push("Supabase database size");
|
|
8063
8895
|
}
|
|
8064
8896
|
} else {
|
|
8065
|
-
console.log(` ${"Database size".padEnd(34)} ${
|
|
8897
|
+
console.log(` ${"Database size".padEnd(34)} ${chalk49.dim("unavailable")}`);
|
|
8066
8898
|
}
|
|
8067
|
-
console.log(` ${"API requests (last 24h)".padEnd(34)} ${dailyApiCount === null ?
|
|
8068
|
-
console.log(
|
|
8899
|
+
console.log(` ${"API requests (last 24h)".padEnd(34)} ${dailyApiCount === null ? chalk49.dim("unavailable") : formatCount(dailyApiCount)}`);
|
|
8900
|
+
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.`));
|
|
8069
8901
|
}
|
|
8070
8902
|
function printMigrationPrompt(nearLimit) {
|
|
8071
8903
|
if (nearLimit.length === 0) {
|
|
8072
8904
|
return;
|
|
8073
8905
|
}
|
|
8074
|
-
console.log(
|
|
8075
|
-
console.log(
|
|
8076
|
-
console.log(
|
|
8906
|
+
console.log(chalk49.yellow.bold(`\u26A0 Approaching free-tier limits: ${nearLimit.join(", ")}.`));
|
|
8907
|
+
console.log(chalk49.yellow(" When a Vercel Hobby limit is hit, that capability pauses; Supabase over quota restricts the whole org."));
|
|
8908
|
+
console.log(chalk49.yellow(" Migrating to your own cloud removes these ceilings \u2014 `spfn cloud migrate` (coming soon).\n"));
|
|
8077
8909
|
}
|
|
8078
8910
|
|
|
8079
8911
|
// src/commands/cloud/keepalive.ts
|
|
8080
8912
|
init_logger();
|
|
8081
|
-
import { existsSync as
|
|
8082
|
-
import { dirname as
|
|
8083
|
-
import
|
|
8913
|
+
import { existsSync as existsSync38, mkdirSync as mkdirSync8, readFileSync as readFileSync20, writeFileSync as writeFileSync23 } from "fs";
|
|
8914
|
+
import { dirname as dirname10, join as join41 } from "path";
|
|
8915
|
+
import chalk50 from "chalk";
|
|
8084
8916
|
var DEFAULT_HEALTH_PATH = "/api/backend/_core/health?detailed=true";
|
|
8085
8917
|
var DEFAULT_SCHEDULE = "0 3 * * *";
|
|
8086
8918
|
async function cloudKeepalive(options) {
|
|
@@ -8105,12 +8937,12 @@ async function cloudKeepalive(options) {
|
|
|
8105
8937
|
logger.info("The DB ping needs detailed health checks: keep `healthCheck: { detailed: true }` in your server config.");
|
|
8106
8938
|
}
|
|
8107
8939
|
function setupVercelCron(cwd, healthPath) {
|
|
8108
|
-
const vercelJsonPath =
|
|
8940
|
+
const vercelJsonPath = join41(cwd, "vercel.json");
|
|
8109
8941
|
let vercelJson = {};
|
|
8110
|
-
if (
|
|
8942
|
+
if (existsSync38(vercelJsonPath)) {
|
|
8111
8943
|
let parsed;
|
|
8112
8944
|
try {
|
|
8113
|
-
parsed = JSON.parse(
|
|
8945
|
+
parsed = JSON.parse(readFileSync20(vercelJsonPath, "utf-8"));
|
|
8114
8946
|
} catch {
|
|
8115
8947
|
logger.error("vercel.json is not valid JSON (comments and trailing commas are not allowed) \u2014 fix it and re-run.");
|
|
8116
8948
|
process.exit(1);
|
|
@@ -8128,10 +8960,10 @@ function setupVercelCron(cwd, healthPath) {
|
|
|
8128
8960
|
}
|
|
8129
8961
|
if (result === "occupied") {
|
|
8130
8962
|
logger.error("vercel.json already has a cron job, and Vercel Hobby allows only one.");
|
|
8131
|
-
logger.info(`Point the existing cron at ${
|
|
8963
|
+
logger.info(`Point the existing cron at ${chalk50.cyan(healthPath)} yourself, or use \`--github-actions --url <deployed-url>\`.`);
|
|
8132
8964
|
process.exit(1);
|
|
8133
8965
|
}
|
|
8134
|
-
|
|
8966
|
+
writeFileSync23(vercelJsonPath, JSON.stringify(vercelJson, null, 4) + "\n", "utf-8");
|
|
8135
8967
|
logger.success(`vercel.json: daily cron added (${DEFAULT_SCHEDULE} UTC \u2192 ${healthPath}). Deploy to activate.`);
|
|
8136
8968
|
warnIfBackendRouteMissing(cwd);
|
|
8137
8969
|
}
|
|
@@ -8151,8 +8983,8 @@ function setupGithubActions(cwd, healthPath, url) {
|
|
|
8151
8983
|
logger.error("`--github-actions` needs `--url <deployed-url>` \u2014 the workflow runs outside your app and must know where it lives.");
|
|
8152
8984
|
process.exit(1);
|
|
8153
8985
|
}
|
|
8154
|
-
const workflowPath =
|
|
8155
|
-
if (
|
|
8986
|
+
const workflowPath = join41(cwd, ".github", "workflows", "spfn-keepalive.yml");
|
|
8987
|
+
if (existsSync38(workflowPath)) {
|
|
8156
8988
|
logger.success(`${workflowPath} already exists \u2014 nothing to do.`);
|
|
8157
8989
|
return;
|
|
8158
8990
|
}
|
|
@@ -8161,8 +8993,8 @@ function setupGithubActions(cwd, healthPath, url) {
|
|
|
8161
8993
|
logger.error(`--url "${url}" is not a valid URL. Pass the deployed app origin, e.g. https://myapp.vercel.app.`);
|
|
8162
8994
|
process.exit(1);
|
|
8163
8995
|
}
|
|
8164
|
-
mkdirSync8(
|
|
8165
|
-
|
|
8996
|
+
mkdirSync8(dirname10(workflowPath), { recursive: true });
|
|
8997
|
+
writeFileSync23(workflowPath, githubWorkflow(new URL(healthPath, base).toString()), "utf-8");
|
|
8166
8998
|
logger.success(".github/workflows/spfn-keepalive.yml: daily keep-alive workflow added. Commit and push to activate.");
|
|
8167
8999
|
}
|
|
8168
9000
|
function normalizeDeployUrl(url) {
|
|
@@ -8199,8 +9031,8 @@ jobs:
|
|
|
8199
9031
|
`;
|
|
8200
9032
|
}
|
|
8201
9033
|
function warnIfBackendRouteMissing(cwd) {
|
|
8202
|
-
const routePath =
|
|
8203
|
-
if (!
|
|
9034
|
+
const routePath = join41(cwd, "src", "app", "api", "backend", "[[...route]]", "route.ts");
|
|
9035
|
+
if (!existsSync38(routePath)) {
|
|
8204
9036
|
logger.warn("src/app/api/backend/[[...route]]/route.ts not found \u2014 the cron path answers only after `spfn add vercel`.");
|
|
8205
9037
|
}
|
|
8206
9038
|
}
|
|
@@ -8208,9 +9040,9 @@ function warnIfBackendRouteMissing(cwd) {
|
|
|
8208
9040
|
// src/commands/cloud/env.ts
|
|
8209
9041
|
init_logger();
|
|
8210
9042
|
init_env_file();
|
|
8211
|
-
import { join as
|
|
9043
|
+
import { join as join42 } from "path";
|
|
8212
9044
|
import prompts13 from "prompts";
|
|
8213
|
-
import
|
|
9045
|
+
import chalk51 from "chalk";
|
|
8214
9046
|
async function cloudEnvPull(options) {
|
|
8215
9047
|
const cwd = process.cwd();
|
|
8216
9048
|
try {
|
|
@@ -8229,7 +9061,7 @@ async function cloudEnvPull(options) {
|
|
|
8229
9061
|
logger.info("Values were written to files/keychain only \u2014 none were displayed.");
|
|
8230
9062
|
}
|
|
8231
9063
|
function writePublicValues(cwd, projectRef2, keys) {
|
|
8232
|
-
const envLocalPath =
|
|
9064
|
+
const envLocalPath = join42(cwd, ".env.local");
|
|
8233
9065
|
ensureGitignored(cwd, [{ pattern: ".env.local", comment: "local env (per-machine values)" }]);
|
|
8234
9066
|
upsertEnvVar(envLocalPath, "NEXT_PUBLIC_SUPABASE_URL", `https://${projectRef2}.supabase.co`);
|
|
8235
9067
|
logger.success("NEXT_PUBLIC_SUPABASE_URL \u2192 .env.local");
|
|
@@ -8247,7 +9079,7 @@ async function storeServiceKey(cwd, keys) {
|
|
|
8247
9079
|
logger.warn("No service_role/secret key found on the project \u2014 skipped SUPABASE_SERVICE_ROLE_KEY.");
|
|
8248
9080
|
return;
|
|
8249
9081
|
}
|
|
8250
|
-
await storeSecret(cwd, "local", "SUPABASE_SERVICE_ROLE_KEY", service.api_key);
|
|
9082
|
+
await storeSecret(cwd, { env: "local" }, "SUPABASE_SERVICE_ROLE_KEY", service.api_key);
|
|
8251
9083
|
}
|
|
8252
9084
|
function findKey(keys, candidates) {
|
|
8253
9085
|
return keys.find((key) => candidates.includes(key.name)) ?? keys.find((key) => key.type !== void 0 && candidates.includes(key.type));
|
|
@@ -8268,7 +9100,7 @@ async function storeDatabaseUrl(cwd, token, projectRef2) {
|
|
|
8268
9100
|
return;
|
|
8269
9101
|
}
|
|
8270
9102
|
const url = sessionPoolerUrl(template, password);
|
|
8271
|
-
await storeSecret(cwd, "local", "DATABASE_URL", url);
|
|
9103
|
+
await storeSecret(cwd, { env: "local" }, "DATABASE_URL", url);
|
|
8272
9104
|
logger.info("This is the session-mode pooler connection (IPv4-safe, works for migrations). Serverless runtime can use the transaction pooler (port 6543) instead.");
|
|
8273
9105
|
}
|
|
8274
9106
|
async function poolerTemplate(token, projectRef2) {
|
|
@@ -8312,7 +9144,7 @@ async function cloudEnvPush(keys) {
|
|
|
8312
9144
|
logger.error(`${failure.key}: rejected by Vercel \u2014 ${failure.message}`);
|
|
8313
9145
|
}
|
|
8314
9146
|
for (const envVar of vars.filter((v) => !failedKeys.has(v.key))) {
|
|
8315
|
-
logger.success(`${envVar.key} \u2192 Vercel ${
|
|
9147
|
+
logger.success(`${envVar.key} \u2192 Vercel ${chalk51.cyan(linked.projectName)} (${envVar.type}, ${envVar.target.join("+")})`);
|
|
8316
9148
|
}
|
|
8317
9149
|
if (result.failed.length > 0) {
|
|
8318
9150
|
process.exit(1);
|
|
@@ -8323,8 +9155,8 @@ async function cloudEnvPush(keys) {
|
|
|
8323
9155
|
}
|
|
8324
9156
|
}
|
|
8325
9157
|
async function resolveLocalValues(cwd, keys) {
|
|
8326
|
-
const publicEnv = { ...parseEnvFile(
|
|
8327
|
-
const serverEnv = parseEnvFile(
|
|
9158
|
+
const publicEnv = { ...parseEnvFile(join42(cwd, ".env")), ...parseEnvFile(join42(cwd, ".env.local")) };
|
|
9159
|
+
const serverEnv = parseEnvFile(join42(cwd, ".env.server"));
|
|
8328
9160
|
const keychain = await resolveKeychainEnv(cwd);
|
|
8329
9161
|
const vars = [];
|
|
8330
9162
|
const missing = [];
|
|
@@ -8362,9 +9194,9 @@ envGroup.command("push").description("Push local env values to the Vercel projec
|
|
|
8362
9194
|
|
|
8363
9195
|
// src/commands/kit/index.ts
|
|
8364
9196
|
import { Command as Command18 } from "commander";
|
|
8365
|
-
import
|
|
9197
|
+
import chalk53 from "chalk";
|
|
8366
9198
|
import prompts14 from "prompts";
|
|
8367
|
-
import { resolve as
|
|
9199
|
+
import { resolve as resolve6 } from "path";
|
|
8368
9200
|
|
|
8369
9201
|
// src/kit/errors.ts
|
|
8370
9202
|
var KIT_EXIT = {
|
|
@@ -8480,11 +9312,11 @@ async function resolveKitAdapters(request2) {
|
|
|
8480
9312
|
// src/kit/operations/install.ts
|
|
8481
9313
|
import { createHash as createHash2 } from "crypto";
|
|
8482
9314
|
import { hostname as hostname2, tmpdir } from "os";
|
|
8483
|
-
import { mkdtempSync, existsSync as
|
|
8484
|
-
import { join as
|
|
9315
|
+
import { mkdtempSync, existsSync as existsSync50 } from "fs";
|
|
9316
|
+
import { join as join52 } from "path";
|
|
8485
9317
|
|
|
8486
9318
|
// src/kit/events.ts
|
|
8487
|
-
import
|
|
9319
|
+
import chalk52 from "chalk";
|
|
8488
9320
|
|
|
8489
9321
|
// src/kit/secret-scan.ts
|
|
8490
9322
|
var SECRET_SHAPES = [
|
|
@@ -8536,30 +9368,30 @@ function redactEvent(event, knownSecrets) {
|
|
|
8536
9368
|
function humanLine(event) {
|
|
8537
9369
|
switch (event.status) {
|
|
8538
9370
|
case "completed":
|
|
8539
|
-
return `${
|
|
9371
|
+
return `${chalk52.green("\u2713")} ${event.summary}`;
|
|
8540
9372
|
case "waiting":
|
|
8541
|
-
return `${
|
|
9373
|
+
return `${chalk52.yellow("\u2026")} ${event.summary}${nextLine(event)}`;
|
|
8542
9374
|
case "failed":
|
|
8543
|
-
return `${
|
|
8544
|
-
${
|
|
9375
|
+
return `${chalk52.red("\u2717")} ${event.summary}
|
|
9376
|
+
${chalk52.gray(event.code)}${nextLine(event)}`;
|
|
8545
9377
|
case "started":
|
|
8546
|
-
return
|
|
9378
|
+
return chalk52.gray(`\u2192 ${event.summary}`);
|
|
8547
9379
|
default:
|
|
8548
|
-
return
|
|
9380
|
+
return chalk52.gray(` ${event.summary}`);
|
|
8549
9381
|
}
|
|
8550
9382
|
}
|
|
8551
9383
|
function nextLine(event) {
|
|
8552
9384
|
if (!event.next) {
|
|
8553
9385
|
return "";
|
|
8554
9386
|
}
|
|
8555
|
-
const approval = event.next.requiresHumanApproval ?
|
|
9387
|
+
const approval = event.next.requiresHumanApproval ? chalk52.yellow(" (needs your approval)") : "";
|
|
8556
9388
|
return `
|
|
8557
|
-
${
|
|
9389
|
+
${chalk52.gray("next:")} ${event.next.command}${approval}`;
|
|
8558
9390
|
}
|
|
8559
9391
|
|
|
8560
9392
|
// src/kit/journal.ts
|
|
8561
|
-
import { mkdirSync as mkdirSync10, readdirSync as
|
|
8562
|
-
import { dirname as
|
|
9393
|
+
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";
|
|
9394
|
+
import { dirname as dirname11, join as join44 } from "path";
|
|
8563
9395
|
|
|
8564
9396
|
// src/kit/digest.ts
|
|
8565
9397
|
import { createHash } from "crypto";
|
|
@@ -8600,8 +9432,8 @@ function digestOfJson(value) {
|
|
|
8600
9432
|
}
|
|
8601
9433
|
|
|
8602
9434
|
// src/kit/paths.ts
|
|
8603
|
-
import { mkdirSync as mkdirSync9, writeFileSync as
|
|
8604
|
-
import { join as
|
|
9435
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync24, existsSync as existsSync39 } from "fs";
|
|
9436
|
+
import { join as join43 } from "path";
|
|
8605
9437
|
var KIT_STATE_DIR = ".spfn";
|
|
8606
9438
|
var OPERATIONS_IGNORE = [
|
|
8607
9439
|
"# Written by `spfn kit`. One machine's run is not part of the project.",
|
|
@@ -8609,27 +9441,27 @@ var OPERATIONS_IGNORE = [
|
|
|
8609
9441
|
""
|
|
8610
9442
|
].join("\n");
|
|
8611
9443
|
function kitPaths(root) {
|
|
8612
|
-
const stateDir =
|
|
8613
|
-
const operationsDir =
|
|
8614
|
-
const lockDir =
|
|
9444
|
+
const stateDir = join43(root, KIT_STATE_DIR);
|
|
9445
|
+
const operationsDir = join43(stateDir, "operations");
|
|
9446
|
+
const lockDir = join43(operationsDir, "operation.lock");
|
|
8615
9447
|
return {
|
|
8616
9448
|
root,
|
|
8617
9449
|
stateDir,
|
|
8618
|
-
licenseFile:
|
|
8619
|
-
lockFile:
|
|
9450
|
+
licenseFile: join43(stateDir, "license.json"),
|
|
9451
|
+
lockFile: join43(stateDir, "kit-lock.json"),
|
|
8620
9452
|
operationsDir,
|
|
8621
|
-
activeJournal:
|
|
8622
|
-
historyDir:
|
|
9453
|
+
activeJournal: join43(operationsDir, "active.json"),
|
|
9454
|
+
historyDir: join43(operationsDir, "history"),
|
|
8623
9455
|
lockDir,
|
|
8624
|
-
lockOwnerFile:
|
|
9456
|
+
lockOwnerFile: join43(lockDir, "owner.json")
|
|
8625
9457
|
};
|
|
8626
9458
|
}
|
|
8627
9459
|
function ensureOperationsDir(root) {
|
|
8628
9460
|
const paths = kitPaths(root);
|
|
8629
|
-
const ignoreFile =
|
|
9461
|
+
const ignoreFile = join43(paths.stateDir, ".gitignore");
|
|
8630
9462
|
mkdirSync9(paths.stateDir, { recursive: true });
|
|
8631
|
-
if (!
|
|
8632
|
-
|
|
9463
|
+
if (!existsSync39(ignoreFile)) {
|
|
9464
|
+
writeFileSync24(ignoreFile, OPERATIONS_IGNORE, "utf8");
|
|
8633
9465
|
}
|
|
8634
9466
|
mkdirSync9(paths.operationsDir, { recursive: true });
|
|
8635
9467
|
return paths.operationsDir;
|
|
@@ -8916,14 +9748,14 @@ var JournalStore = class {
|
|
|
8916
9748
|
return readJournalFile(this.paths.activeJournal);
|
|
8917
9749
|
}
|
|
8918
9750
|
readHistory(operationId) {
|
|
8919
|
-
return readJournalFile(
|
|
9751
|
+
return readJournalFile(join44(this.paths.historyDir, `${operationId}.json`));
|
|
8920
9752
|
}
|
|
8921
9753
|
/** Every journal this project still has, newest update first. */
|
|
8922
9754
|
listHistory() {
|
|
8923
|
-
if (!
|
|
9755
|
+
if (!existsSync40(this.paths.historyDir)) {
|
|
8924
9756
|
return [];
|
|
8925
9757
|
}
|
|
8926
|
-
return
|
|
9758
|
+
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));
|
|
8927
9759
|
}
|
|
8928
9760
|
create(init) {
|
|
8929
9761
|
const timestamp = this.now();
|
|
@@ -8982,17 +9814,17 @@ var JournalStore = class {
|
|
|
8982
9814
|
}
|
|
8983
9815
|
/** Move a finished operation into history and clear the active slot. */
|
|
8984
9816
|
archive(journal) {
|
|
8985
|
-
writeJsonAtomically(
|
|
8986
|
-
|
|
9817
|
+
writeJsonAtomically(join44(this.paths.historyDir, `${journal.operationId}.json`), journal);
|
|
9818
|
+
rmSync3(this.paths.activeJournal, { force: true });
|
|
8987
9819
|
this.pruneHistory();
|
|
8988
9820
|
}
|
|
8989
9821
|
pruneHistory() {
|
|
8990
|
-
if (!
|
|
9822
|
+
if (!existsSync40(this.paths.historyDir)) {
|
|
8991
9823
|
return;
|
|
8992
9824
|
}
|
|
8993
|
-
const files =
|
|
9825
|
+
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);
|
|
8994
9826
|
for (const file of files.slice(this.historyLimit)) {
|
|
8995
|
-
|
|
9827
|
+
rmSync3(file, { force: true });
|
|
8996
9828
|
}
|
|
8997
9829
|
}
|
|
8998
9830
|
};
|
|
@@ -9012,26 +9844,26 @@ function upsertCheckpoint(checkpoints, entry) {
|
|
|
9012
9844
|
return next;
|
|
9013
9845
|
}
|
|
9014
9846
|
function readJournalFile(file) {
|
|
9015
|
-
if (!
|
|
9847
|
+
if (!existsSync40(file)) {
|
|
9016
9848
|
return null;
|
|
9017
9849
|
}
|
|
9018
9850
|
try {
|
|
9019
|
-
const parsed = JSON.parse(
|
|
9851
|
+
const parsed = JSON.parse(readFileSync21(file, "utf8"));
|
|
9020
9852
|
return validateOperationJournal(parsed).valid ? parsed : null;
|
|
9021
9853
|
} catch {
|
|
9022
9854
|
return null;
|
|
9023
9855
|
}
|
|
9024
9856
|
}
|
|
9025
9857
|
function writeJsonAtomically(file, value) {
|
|
9026
|
-
mkdirSync10(
|
|
9858
|
+
mkdirSync10(dirname11(file), { recursive: true });
|
|
9027
9859
|
const temporary = `${file}.tmp`;
|
|
9028
|
-
|
|
9860
|
+
writeFileSync25(temporary, `${JSON.stringify(value, null, 4)}
|
|
9029
9861
|
`, { encoding: "utf8", mode: 384 });
|
|
9030
|
-
|
|
9862
|
+
renameSync3(temporary, file);
|
|
9031
9863
|
}
|
|
9032
9864
|
|
|
9033
9865
|
// src/kit/lock.ts
|
|
9034
|
-
import { mkdirSync as mkdirSync11, readFileSync as
|
|
9866
|
+
import { mkdirSync as mkdirSync11, readFileSync as readFileSync22, rmSync as rmSync4, writeFileSync as writeFileSync26, existsSync as existsSync41 } from "fs";
|
|
9035
9867
|
import { hostname } from "os";
|
|
9036
9868
|
function defaultIsProcessAlive(pid) {
|
|
9037
9869
|
try {
|
|
@@ -9055,7 +9887,7 @@ function acquireOperationLock(options) {
|
|
|
9055
9887
|
return handle(paths, owner, false);
|
|
9056
9888
|
}
|
|
9057
9889
|
reconcileStaleLock(options);
|
|
9058
|
-
|
|
9890
|
+
rmSync4(paths.lockDir, { recursive: true, force: true });
|
|
9059
9891
|
if (!tryCreateLockDir(paths)) {
|
|
9060
9892
|
throw new KitError("KIT_OPERATION_ACTIVE", "Another spfn kit operation took the project lock first.", {
|
|
9061
9893
|
evidence: { lockDir: paths.lockDir },
|
|
@@ -9067,11 +9899,11 @@ function acquireOperationLock(options) {
|
|
|
9067
9899
|
}
|
|
9068
9900
|
function readLockOwner(root) {
|
|
9069
9901
|
const paths = kitPaths(root);
|
|
9070
|
-
if (!
|
|
9902
|
+
if (!existsSync41(paths.lockOwnerFile)) {
|
|
9071
9903
|
return null;
|
|
9072
9904
|
}
|
|
9073
9905
|
try {
|
|
9074
|
-
const parsed = JSON.parse(
|
|
9906
|
+
const parsed = JSON.parse(readFileSync22(paths.lockOwnerFile, "utf8"));
|
|
9075
9907
|
return typeof parsed?.pid === "number" && typeof parsed?.operationId === "string" ? parsed : null;
|
|
9076
9908
|
} catch {
|
|
9077
9909
|
return null;
|
|
@@ -9140,7 +9972,7 @@ function tryCreateLockDir(paths) {
|
|
|
9140
9972
|
}
|
|
9141
9973
|
}
|
|
9142
9974
|
function writeOwner(paths, owner) {
|
|
9143
|
-
|
|
9975
|
+
writeFileSync26(paths.lockOwnerFile, `${JSON.stringify(owner, null, 4)}
|
|
9144
9976
|
`, "utf8");
|
|
9145
9977
|
}
|
|
9146
9978
|
function handle(paths, owner, reclaimed) {
|
|
@@ -9148,24 +9980,24 @@ function handle(paths, owner, reclaimed) {
|
|
|
9148
9980
|
owner,
|
|
9149
9981
|
reclaimed,
|
|
9150
9982
|
release() {
|
|
9151
|
-
|
|
9983
|
+
rmSync4(paths.lockDir, { recursive: true, force: true });
|
|
9152
9984
|
}
|
|
9153
9985
|
};
|
|
9154
9986
|
}
|
|
9155
9987
|
|
|
9156
9988
|
// src/kit/operation-context.ts
|
|
9157
|
-
import { mkdirSync as mkdirSync12, readFileSync as
|
|
9158
|
-
import { dirname as
|
|
9989
|
+
import { mkdirSync as mkdirSync12, readFileSync as readFileSync23, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync27, existsSync as existsSync42 } from "fs";
|
|
9990
|
+
import { dirname as dirname12, join as join45 } from "path";
|
|
9159
9991
|
function contextFile(root) {
|
|
9160
|
-
return
|
|
9992
|
+
return join45(kitPaths(root).operationsDir, "context.json");
|
|
9161
9993
|
}
|
|
9162
9994
|
function readOperationContext(root) {
|
|
9163
9995
|
const file = contextFile(root);
|
|
9164
|
-
if (!
|
|
9996
|
+
if (!existsSync42(file)) {
|
|
9165
9997
|
return null;
|
|
9166
9998
|
}
|
|
9167
9999
|
try {
|
|
9168
|
-
const parsed = JSON.parse(
|
|
10000
|
+
const parsed = JSON.parse(readFileSync23(file, "utf8"));
|
|
9169
10001
|
return parsed?.schemaVersion === 1 && typeof parsed.operationId === "string" ? parsed : null;
|
|
9170
10002
|
} catch {
|
|
9171
10003
|
return null;
|
|
@@ -9173,14 +10005,14 @@ function readOperationContext(root) {
|
|
|
9173
10005
|
}
|
|
9174
10006
|
function writeOperationContext(root, context) {
|
|
9175
10007
|
const file = contextFile(root);
|
|
9176
|
-
mkdirSync12(
|
|
10008
|
+
mkdirSync12(dirname12(file), { recursive: true });
|
|
9177
10009
|
const temporary = `${file}.tmp`;
|
|
9178
|
-
|
|
10010
|
+
writeFileSync27(temporary, `${JSON.stringify(context, null, 4)}
|
|
9179
10011
|
`, "utf8");
|
|
9180
|
-
|
|
10012
|
+
renameSync4(temporary, file);
|
|
9181
10013
|
}
|
|
9182
10014
|
function clearOperationContext(root) {
|
|
9183
|
-
|
|
10015
|
+
rmSync5(contextFile(root), { force: true });
|
|
9184
10016
|
}
|
|
9185
10017
|
|
|
9186
10018
|
// src/kit/plan.ts
|
|
@@ -9725,7 +10557,7 @@ async function fetchThroughRedirects(start, fetcher, allowlist) {
|
|
|
9725
10557
|
}
|
|
9726
10558
|
|
|
9727
10559
|
// src/kit/credentials.ts
|
|
9728
|
-
import { randomBytes as
|
|
10560
|
+
import { randomBytes as randomBytes4 } from "crypto";
|
|
9729
10561
|
var KIT_KEYCHAIN_SERVICE = "superfunction.spfn.kit";
|
|
9730
10562
|
var KIT_KEYCHAIN_NAMESPACE_ENV = "SPFN_KIT_KEYCHAIN_NAMESPACE";
|
|
9731
10563
|
var NAMESPACE_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
@@ -9748,7 +10580,7 @@ function pendingAccount(kitId, installationId, localClientId) {
|
|
|
9748
10580
|
return `${kitId}:pending:${installationId}:${localClientId}`;
|
|
9749
10581
|
}
|
|
9750
10582
|
function newCandidateCredential() {
|
|
9751
|
-
return `spfnlc_${
|
|
10583
|
+
return `spfnlc_${randomBytes4(8).toString("hex")}.${randomBytes4(32).toString("base64url")}`;
|
|
9752
10584
|
}
|
|
9753
10585
|
function credentialPublicId(credential) {
|
|
9754
10586
|
const match = /^spfn[a-z]*_([0-9a-f]{16})\./.exec(credential);
|
|
@@ -9810,8 +10642,8 @@ var KeychainKitCredentialStore = class {
|
|
|
9810
10642
|
};
|
|
9811
10643
|
|
|
9812
10644
|
// src/kit/tooling.ts
|
|
9813
|
-
import { cpSync, mkdirSync as mkdirSync13, readdirSync as
|
|
9814
|
-
import { join as
|
|
10645
|
+
import { cpSync, mkdirSync as mkdirSync13, readdirSync as readdirSync5, readFileSync as readFileSync24, rmSync as rmSync6, statSync as statSync3, existsSync as existsSync43 } from "fs";
|
|
10646
|
+
import { join as join46, relative as relative3, sep as sep3 } from "path";
|
|
9815
10647
|
|
|
9816
10648
|
// src/kit/manifest.ts
|
|
9817
10649
|
var DEFAULT_AGENT_PACK_ROOT = ".spfn/agent-pack";
|
|
@@ -10117,27 +10949,27 @@ function assertSafeRelativePath(path5, refuse) {
|
|
|
10117
10949
|
}
|
|
10118
10950
|
function snapshotTree(root, ignore = ["node_modules", ".git"]) {
|
|
10119
10951
|
const files = /* @__PURE__ */ new Map();
|
|
10120
|
-
const
|
|
10121
|
-
for (const entry of
|
|
10952
|
+
const walk2 = (directory) => {
|
|
10953
|
+
for (const entry of readdirSync5(directory, { withFileTypes: true })) {
|
|
10122
10954
|
if (ignore.includes(entry.name)) {
|
|
10123
10955
|
continue;
|
|
10124
10956
|
}
|
|
10125
|
-
const absolute =
|
|
10957
|
+
const absolute = join46(directory, entry.name);
|
|
10126
10958
|
if (entry.isDirectory()) {
|
|
10127
|
-
|
|
10959
|
+
walk2(absolute);
|
|
10128
10960
|
continue;
|
|
10129
10961
|
}
|
|
10130
10962
|
if (!entry.isFile()) {
|
|
10131
10963
|
continue;
|
|
10132
10964
|
}
|
|
10133
10965
|
files.set(
|
|
10134
|
-
|
|
10135
|
-
sha256Digest(
|
|
10966
|
+
relative3(root, absolute).split(sep3).join("/"),
|
|
10967
|
+
sha256Digest(readFileSync24(absolute))
|
|
10136
10968
|
);
|
|
10137
10969
|
}
|
|
10138
10970
|
};
|
|
10139
|
-
if (
|
|
10140
|
-
|
|
10971
|
+
if (existsSync43(root) && statSync3(root).isDirectory()) {
|
|
10972
|
+
walk2(root);
|
|
10141
10973
|
}
|
|
10142
10974
|
return { files };
|
|
10143
10975
|
}
|
|
@@ -10163,7 +10995,7 @@ function diffTrees(before, after) {
|
|
|
10163
10995
|
}
|
|
10164
10996
|
var COPY_EXCLUDED = ["node_modules", ".git", ".spfn"];
|
|
10165
10997
|
async function runIsolated(projectDir2, stagingDir, run2) {
|
|
10166
|
-
|
|
10998
|
+
rmSync6(stagingDir, { recursive: true, force: true });
|
|
10167
10999
|
mkdirSync13(stagingDir, { recursive: true });
|
|
10168
11000
|
cpSync(projectDir2, stagingDir, {
|
|
10169
11001
|
recursive: true,
|
|
@@ -10175,7 +11007,7 @@ async function runIsolated(projectDir2, stagingDir, run2) {
|
|
|
10175
11007
|
const diff = diffTrees(before, snapshotTree(stagingDir));
|
|
10176
11008
|
return { value, diff };
|
|
10177
11009
|
} finally {
|
|
10178
|
-
|
|
11010
|
+
rmSync6(stagingDir, { recursive: true, force: true });
|
|
10179
11011
|
}
|
|
10180
11012
|
}
|
|
10181
11013
|
function assertWritesWithinAllowlist(diff, allowlist, context) {
|
|
@@ -10193,22 +11025,22 @@ function assertWritesWithinAllowlist(diff, allowlist, context) {
|
|
|
10193
11025
|
}
|
|
10194
11026
|
|
|
10195
11027
|
// src/kit/agent-pack.ts
|
|
10196
|
-
import { existsSync as
|
|
10197
|
-
import { dirname as
|
|
11028
|
+
import { existsSync as existsSync44, mkdirSync as mkdirSync14, readFileSync as readFileSync25, writeFileSync as writeFileSync28 } from "fs";
|
|
11029
|
+
import { dirname as dirname13, join as join47 } from "path";
|
|
10198
11030
|
var AGENT_PACK_RECORD_PATH = ".spfn/agent-pack.json";
|
|
10199
11031
|
function writeAgentPackRecord(projectDir2, record) {
|
|
10200
|
-
const file =
|
|
10201
|
-
mkdirSync14(
|
|
10202
|
-
|
|
11032
|
+
const file = join47(projectDir2, AGENT_PACK_RECORD_PATH);
|
|
11033
|
+
mkdirSync14(dirname13(file), { recursive: true });
|
|
11034
|
+
writeFileSync28(file, `${JSON.stringify(record, null, 4)}
|
|
10203
11035
|
`, "utf8");
|
|
10204
11036
|
}
|
|
10205
11037
|
function readAgentPackRecord(projectDir2) {
|
|
10206
|
-
const file =
|
|
10207
|
-
if (!
|
|
11038
|
+
const file = join47(projectDir2, AGENT_PACK_RECORD_PATH);
|
|
11039
|
+
if (!existsSync44(file)) {
|
|
10208
11040
|
return null;
|
|
10209
11041
|
}
|
|
10210
11042
|
try {
|
|
10211
|
-
const parsed = JSON.parse(
|
|
11043
|
+
const parsed = JSON.parse(readFileSync25(file, "utf8"));
|
|
10212
11044
|
return parsed.schemaVersion === 1 && typeof parsed.files === "object" && parsed.files !== null ? parsed : null;
|
|
10213
11045
|
} catch {
|
|
10214
11046
|
return null;
|
|
@@ -10216,8 +11048,8 @@ function readAgentPackRecord(projectDir2) {
|
|
|
10216
11048
|
}
|
|
10217
11049
|
|
|
10218
11050
|
// src/kit/installed-state.ts
|
|
10219
|
-
import { mkdirSync as mkdirSync15, readFileSync as
|
|
10220
|
-
import { dirname as
|
|
11051
|
+
import { mkdirSync as mkdirSync15, readFileSync as readFileSync26, renameSync as renameSync5, writeFileSync as writeFileSync29, existsSync as existsSync45 } from "fs";
|
|
11052
|
+
import { dirname as dirname14 } from "path";
|
|
10221
11053
|
function readLicenseFile(file) {
|
|
10222
11054
|
const parsed = readJson(file);
|
|
10223
11055
|
if (parsed === null) {
|
|
@@ -10259,7 +11091,7 @@ function requireInstalledLock(file) {
|
|
|
10259
11091
|
const lock = readInstalledLock(file);
|
|
10260
11092
|
if (lock === null) {
|
|
10261
11093
|
throw new KitError("KIT_LOCK_INVALID", "This project has no readable .spfn/kit-lock.json.", {
|
|
10262
|
-
evidence: { file, exists:
|
|
11094
|
+
evidence: { file, exists: existsSync45(file) },
|
|
10263
11095
|
next: { command: "spfn kit recover --json", requiresHumanApproval: false }
|
|
10264
11096
|
});
|
|
10265
11097
|
}
|
|
@@ -10294,29 +11126,29 @@ function lockFromManifest(manifest, context) {
|
|
|
10294
11126
|
};
|
|
10295
11127
|
}
|
|
10296
11128
|
function readJson(file) {
|
|
10297
|
-
if (!
|
|
11129
|
+
if (!existsSync45(file)) {
|
|
10298
11130
|
return null;
|
|
10299
11131
|
}
|
|
10300
11132
|
try {
|
|
10301
|
-
return JSON.parse(
|
|
11133
|
+
return JSON.parse(readFileSync26(file, "utf8"));
|
|
10302
11134
|
} catch {
|
|
10303
11135
|
return null;
|
|
10304
11136
|
}
|
|
10305
11137
|
}
|
|
10306
11138
|
function writeJsonAtomically2(file, value) {
|
|
10307
|
-
mkdirSync15(
|
|
11139
|
+
mkdirSync15(dirname14(file), { recursive: true });
|
|
10308
11140
|
const temporary = `${file}.tmp`;
|
|
10309
|
-
|
|
11141
|
+
writeFileSync29(temporary, `${JSON.stringify(value, null, 4)}
|
|
10310
11142
|
`, "utf8");
|
|
10311
|
-
|
|
11143
|
+
renameSync5(temporary, file);
|
|
10312
11144
|
}
|
|
10313
11145
|
|
|
10314
11146
|
// src/kit/drift.ts
|
|
10315
|
-
import { readFileSync as
|
|
10316
|
-
import { join as
|
|
11147
|
+
import { readFileSync as readFileSync27, existsSync as existsSync46 } from "fs";
|
|
11148
|
+
import { join as join48 } from "path";
|
|
10317
11149
|
function fileDigest(root, relativePath) {
|
|
10318
|
-
const file =
|
|
10319
|
-
return
|
|
11150
|
+
const file = join48(root, relativePath);
|
|
11151
|
+
return existsSync46(file) ? sha256Digest(readFileSync27(file)) : null;
|
|
10320
11152
|
}
|
|
10321
11153
|
function agentPackTargets(root, lock) {
|
|
10322
11154
|
if (lock.agentPack.root === void 0) {
|
|
@@ -10347,12 +11179,12 @@ function managedDigests(root, lock) {
|
|
|
10347
11179
|
}
|
|
10348
11180
|
|
|
10349
11181
|
// src/kit/operations/shared.ts
|
|
10350
|
-
import { mkdirSync as mkdirSync17, readdirSync as
|
|
10351
|
-
import { dirname as
|
|
11182
|
+
import { mkdirSync as mkdirSync17, readdirSync as readdirSync6, readFileSync as readFileSync30, writeFileSync as writeFileSync32, existsSync as existsSync49 } from "fs";
|
|
11183
|
+
import { dirname as dirname16, join as join51 } from "path";
|
|
10352
11184
|
|
|
10353
11185
|
// src/kit/expand.ts
|
|
10354
|
-
import { mkdirSync as mkdirSync16, existsSync as
|
|
10355
|
-
import { dirname as
|
|
11186
|
+
import { mkdirSync as mkdirSync16, existsSync as existsSync47, readFileSync as readFileSync28, writeFileSync as writeFileSync30 } from "fs";
|
|
11187
|
+
import { dirname as dirname15, join as join49 } from "path";
|
|
10356
11188
|
|
|
10357
11189
|
// src/kit/tar.ts
|
|
10358
11190
|
var BLOCK = 512;
|
|
@@ -10453,32 +11285,32 @@ function expandArchive(bytes, request2) {
|
|
|
10453
11285
|
let written = 0;
|
|
10454
11286
|
let matched = 0;
|
|
10455
11287
|
for (const entry of entries) {
|
|
10456
|
-
const
|
|
10457
|
-
const destination =
|
|
11288
|
+
const relative4 = request2.root === void 0 || request2.root.length === 0 ? entry.path : `${request2.root.replace(/\/+$/, "")}/${entry.path}`;
|
|
11289
|
+
const destination = join49(request2.targetDir, relative4);
|
|
10458
11290
|
if (entry.kind === "directory") {
|
|
10459
11291
|
mkdirSync16(destination, { recursive: true });
|
|
10460
11292
|
continue;
|
|
10461
11293
|
}
|
|
10462
11294
|
const digest = sha256Digest(entry.bytes);
|
|
10463
|
-
files[
|
|
10464
|
-
if (
|
|
10465
|
-
assertSameBytes(destination,
|
|
11295
|
+
files[relative4] = digest;
|
|
11296
|
+
if (existsSync47(destination) && request2.existing !== "replace") {
|
|
11297
|
+
assertSameBytes(destination, relative4, digest);
|
|
10466
11298
|
matched += 1;
|
|
10467
11299
|
continue;
|
|
10468
11300
|
}
|
|
10469
|
-
mkdirSync16(
|
|
10470
|
-
|
|
11301
|
+
mkdirSync16(dirname15(destination), { recursive: true });
|
|
11302
|
+
writeFileSync30(destination, entry.bytes, { mode: entry.mode === 0 ? 420 : entry.mode });
|
|
10471
11303
|
written += 1;
|
|
10472
11304
|
}
|
|
10473
11305
|
return { files, written, matched };
|
|
10474
11306
|
}
|
|
10475
|
-
function assertSameBytes(destination,
|
|
10476
|
-
const actual = sha256Digest(
|
|
11307
|
+
function assertSameBytes(destination, relative4, expected) {
|
|
11308
|
+
const actual = sha256Digest(readFileSync28(destination));
|
|
10477
11309
|
if (actual === expected) {
|
|
10478
11310
|
return;
|
|
10479
11311
|
}
|
|
10480
11312
|
throw new KitError("KIT_TARGET_NOT_EMPTY", "A file the release would write is already there with different contents.", {
|
|
10481
|
-
evidence: { path:
|
|
11313
|
+
evidence: { path: relative4, expected, actual },
|
|
10482
11314
|
next: { command: "spfn kit status --json", requiresHumanApproval: false }
|
|
10483
11315
|
});
|
|
10484
11316
|
}
|
|
@@ -10581,15 +11413,15 @@ function registryNpmrc(scopes, registryUrl) {
|
|
|
10581
11413
|
}
|
|
10582
11414
|
|
|
10583
11415
|
// src/kit/package-graph.ts
|
|
10584
|
-
import { existsSync as
|
|
10585
|
-
import { join as
|
|
11416
|
+
import { existsSync as existsSync48, readFileSync as readFileSync29, writeFileSync as writeFileSync31 } from "fs";
|
|
11417
|
+
import { join as join50 } from "path";
|
|
10586
11418
|
var DEPENDENCY_BLOCKS = ["dependencies", "devDependencies", "optionalDependencies"];
|
|
10587
11419
|
function applyManifestVersions(projectDir2, manifest) {
|
|
10588
|
-
const file =
|
|
10589
|
-
if (!
|
|
11420
|
+
const file = join50(projectDir2, "package.json");
|
|
11421
|
+
if (!existsSync48(file)) {
|
|
10590
11422
|
return [];
|
|
10591
11423
|
}
|
|
10592
|
-
const raw =
|
|
11424
|
+
const raw = readFileSync29(file, "utf8");
|
|
10593
11425
|
const document = JSON.parse(raw);
|
|
10594
11426
|
const wanted = new Map(manifest.packages.map((entry) => [entry.name, entry.version]));
|
|
10595
11427
|
const changes = [];
|
|
@@ -10608,7 +11440,7 @@ function applyManifestVersions(projectDir2, manifest) {
|
|
|
10608
11440
|
}
|
|
10609
11441
|
}
|
|
10610
11442
|
if (changes.length > 0) {
|
|
10611
|
-
|
|
11443
|
+
writeFileSync31(file, `${JSON.stringify(document, null, indentationOf(raw))}${trailingNewline(raw)}`);
|
|
10612
11444
|
}
|
|
10613
11445
|
return changes;
|
|
10614
11446
|
}
|
|
@@ -10636,12 +11468,12 @@ function installedGraphMismatches(projectDir2, manifest, skip = () => false) {
|
|
|
10636
11468
|
return mismatches;
|
|
10637
11469
|
}
|
|
10638
11470
|
function installedVersion(projectDir2, name) {
|
|
10639
|
-
const file =
|
|
10640
|
-
if (!
|
|
11471
|
+
const file = join50(projectDir2, "node_modules", ...name.split("/"), "package.json");
|
|
11472
|
+
if (!existsSync48(file)) {
|
|
10641
11473
|
return null;
|
|
10642
11474
|
}
|
|
10643
11475
|
try {
|
|
10644
|
-
const version = JSON.parse(
|
|
11476
|
+
const version = JSON.parse(readFileSync29(file, "utf8")).version;
|
|
10645
11477
|
return typeof version === "string" ? version : null;
|
|
10646
11478
|
} catch {
|
|
10647
11479
|
return null;
|
|
@@ -10728,10 +11560,10 @@ function latestActiveRelease(catalog) {
|
|
|
10728
11560
|
return active[0];
|
|
10729
11561
|
}
|
|
10730
11562
|
function assertEmptyTarget(targetDir) {
|
|
10731
|
-
if (!
|
|
11563
|
+
if (!existsSync49(targetDir)) {
|
|
10732
11564
|
return;
|
|
10733
11565
|
}
|
|
10734
|
-
const entries =
|
|
11566
|
+
const entries = readdirSync6(targetDir).filter((name) => name !== ".DS_Store");
|
|
10735
11567
|
if (entries.length > 0) {
|
|
10736
11568
|
throw new KitError("KIT_TARGET_NOT_EMPTY", "A Kit install needs a new or empty directory.", {
|
|
10737
11569
|
evidence: { targetDir, entries: entries.length }
|
|
@@ -10759,13 +11591,13 @@ async function materializeTargets(adapters, projectDir2, targets, options = {})
|
|
|
10759
11591
|
continue;
|
|
10760
11592
|
}
|
|
10761
11593
|
written[target.path] = digest;
|
|
10762
|
-
writeManagedFile(
|
|
11594
|
+
writeManagedFile(join51(projectDir2, target.path), target.path, bytes, digest, options.existing);
|
|
10763
11595
|
}
|
|
10764
11596
|
return written;
|
|
10765
11597
|
}
|
|
10766
11598
|
function writeManagedFile(file, path5, bytes, digest, existing = "verify") {
|
|
10767
|
-
if (
|
|
10768
|
-
const actual = sha256Digest(
|
|
11599
|
+
if (existsSync49(file) && existing !== "replace") {
|
|
11600
|
+
const actual = sha256Digest(readFileSync30(file));
|
|
10769
11601
|
if (actual === digest) {
|
|
10770
11602
|
return;
|
|
10771
11603
|
}
|
|
@@ -10774,8 +11606,8 @@ function writeManagedFile(file, path5, bytes, digest, existing = "verify") {
|
|
|
10774
11606
|
next: { command: "spfn kit status --json", requiresHumanApproval: false }
|
|
10775
11607
|
});
|
|
10776
11608
|
}
|
|
10777
|
-
mkdirSync17(
|
|
10778
|
-
|
|
11609
|
+
mkdirSync17(dirname16(file), { recursive: true });
|
|
11610
|
+
writeFileSync32(file, bytes);
|
|
10779
11611
|
}
|
|
10780
11612
|
async function installFrozenGraph(adapters, options) {
|
|
10781
11613
|
const maxAttempts = options.allowSessionRetry === false ? 1 : 2;
|
|
@@ -10837,7 +11669,7 @@ function scopesOf(manifest) {
|
|
|
10837
11669
|
return scopes.length === 0 ? ["@superfunction"] : [...new Set(scopes)];
|
|
10838
11670
|
}
|
|
10839
11671
|
function writeRegistryNpmrc(projectDir2, manifest, registryUrl) {
|
|
10840
|
-
|
|
11672
|
+
writeFileSync32(join51(projectDir2, ".npmrc"), registryNpmrc(scopesOf(manifest), registryUrl), "utf8");
|
|
10841
11673
|
}
|
|
10842
11674
|
function assertGraphInstalled(options) {
|
|
10843
11675
|
if (options.expect === void 0) {
|
|
@@ -11085,10 +11917,10 @@ function installSteps(options) {
|
|
|
11085
11917
|
return { kind: "done", evidence: { attempts: evidence.attempts, tooling: tooling.specifier } };
|
|
11086
11918
|
},
|
|
11087
11919
|
async verify() {
|
|
11088
|
-
if (!
|
|
11920
|
+
if (!existsSync50(join52(projectDir2, "node_modules"))) {
|
|
11089
11921
|
return { ok: false, reason: "dependencies-missing" };
|
|
11090
11922
|
}
|
|
11091
|
-
if (!
|
|
11923
|
+
if (!existsSync50(paths.lockFile)) {
|
|
11092
11924
|
writeLock(options, projectDir2, adapters.clock.now());
|
|
11093
11925
|
}
|
|
11094
11926
|
return { ok: true };
|
|
@@ -11187,7 +12019,7 @@ async function verifyToolingPlan(options, projectDir2, operationId) {
|
|
|
11187
12019
|
});
|
|
11188
12020
|
const isolated = await runIsolated(
|
|
11189
12021
|
projectDir2,
|
|
11190
|
-
|
|
12022
|
+
join52(mkdtempSync(join52(tmpdir(), "spfn-kit-")), operationId),
|
|
11191
12023
|
(workingCopy) => discovered.tooling.planInstall({
|
|
11192
12024
|
projectDir: workingCopy,
|
|
11193
12025
|
release: options.manifest.version
|
|
@@ -11495,8 +12327,8 @@ async function startRecovery(sink, adapters, activationId) {
|
|
|
11495
12327
|
}
|
|
11496
12328
|
|
|
11497
12329
|
// src/kit/customer-source.ts
|
|
11498
|
-
import { existsSync as
|
|
11499
|
-
import { dirname as
|
|
12330
|
+
import { existsSync as existsSync51, mkdirSync as mkdirSync18, readFileSync as readFileSync31, writeFileSync as writeFileSync33 } from "fs";
|
|
12331
|
+
import { dirname as dirname17, join as join53 } from "path";
|
|
11500
12332
|
var IGNORED_DIRECTORIES = ["node_modules", ".git", ".next", ".turbo", ".vercel", "dist", "coverage"];
|
|
11501
12333
|
var KIT_OWNED_FILES = ["pnpm-lock.yaml", "package.json", ".npmrc", "tsconfig.tsbuildinfo"];
|
|
11502
12334
|
function declaredPaths(declaration) {
|
|
@@ -11541,21 +12373,21 @@ function compareCustomerSource(before, after) {
|
|
|
11541
12373
|
return paths.filter((path5) => before[path5] !== after[path5]).map((path5) => ({ path: path5, before: before[path5] ?? null, after: after[path5] ?? null }));
|
|
11542
12374
|
}
|
|
11543
12375
|
function customerBaselinePath(projectDir2, operationId) {
|
|
11544
|
-
return
|
|
12376
|
+
return join53(projectDir2, ".spfn", "operations", `${operationId}.customer-source.json`);
|
|
11545
12377
|
}
|
|
11546
12378
|
function writeCustomerBaseline(projectDir2, baseline) {
|
|
11547
12379
|
const path5 = customerBaselinePath(projectDir2, baseline.operationId);
|
|
11548
|
-
mkdirSync18(
|
|
11549
|
-
|
|
12380
|
+
mkdirSync18(dirname17(path5), { recursive: true });
|
|
12381
|
+
writeFileSync33(path5, `${JSON.stringify(baseline, null, 4)}
|
|
11550
12382
|
`);
|
|
11551
12383
|
}
|
|
11552
12384
|
function readCustomerBaseline(projectDir2, operationId) {
|
|
11553
12385
|
const path5 = customerBaselinePath(projectDir2, operationId);
|
|
11554
|
-
if (!
|
|
12386
|
+
if (!existsSync51(path5)) {
|
|
11555
12387
|
return null;
|
|
11556
12388
|
}
|
|
11557
12389
|
try {
|
|
11558
|
-
const parsed = JSON.parse(
|
|
12390
|
+
const parsed = JSON.parse(readFileSync31(path5, "utf8"));
|
|
11559
12391
|
return parsed?.schemaVersion === 1 && typeof parsed.files === "object" ? parsed : null;
|
|
11560
12392
|
} catch {
|
|
11561
12393
|
return null;
|
|
@@ -11957,8 +12789,8 @@ function planResult(sink, plan, digest) {
|
|
|
11957
12789
|
}
|
|
11958
12790
|
|
|
11959
12791
|
// src/kit/operations/inspect.ts
|
|
11960
|
-
import { existsSync as
|
|
11961
|
-
import { join as
|
|
12792
|
+
import { existsSync as existsSync52 } from "fs";
|
|
12793
|
+
import { join as join54 } from "path";
|
|
11962
12794
|
async function runStatus(request2, adapters) {
|
|
11963
12795
|
const paths = kitPaths(request2.projectDir);
|
|
11964
12796
|
const lock = readInstalledLock(paths.lockFile);
|
|
@@ -12071,7 +12903,7 @@ async function runCheck(request2, adapters) {
|
|
|
12071
12903
|
fixCommand: `spfn kit resume ${journal.operationId} --json`
|
|
12072
12904
|
});
|
|
12073
12905
|
}
|
|
12074
|
-
if (!
|
|
12906
|
+
if (!existsSync52(join54(request2.projectDir, "node_modules"))) {
|
|
12075
12907
|
diagnostics.push({
|
|
12076
12908
|
code: "KIT_GATE_FAILED",
|
|
12077
12909
|
severity: "warning",
|
|
@@ -12219,7 +13051,7 @@ async function runAbandon(request2, adapters) {
|
|
|
12219
13051
|
|
|
12220
13052
|
// src/commands/kit/index.ts
|
|
12221
13053
|
function projectDir(options) {
|
|
12222
|
-
return
|
|
13054
|
+
return resolve6(options.dir ?? process.cwd());
|
|
12223
13055
|
}
|
|
12224
13056
|
function licenseKeyReader(options) {
|
|
12225
13057
|
return async () => {
|
|
@@ -12313,10 +13145,10 @@ function reportFailure(error, json) {
|
|
|
12313
13145
|
if (json) {
|
|
12314
13146
|
console.log(JSON.stringify(event));
|
|
12315
13147
|
} else {
|
|
12316
|
-
console.error(`${
|
|
12317
|
-
console.error(` ${
|
|
13148
|
+
console.error(`${chalk53.red("\u2717")} ${error.message}`);
|
|
13149
|
+
console.error(` ${chalk53.gray(error.code)}`);
|
|
12318
13150
|
if (error.next) {
|
|
12319
|
-
console.error(` ${
|
|
13151
|
+
console.error(` ${chalk53.gray("next:")} ${error.next.command}`);
|
|
12320
13152
|
}
|
|
12321
13153
|
}
|
|
12322
13154
|
process.exitCode = error.exitCode;
|
|
@@ -12340,7 +13172,7 @@ async function readOnlyAdapters(projectDir2) {
|
|
|
12340
13172
|
}
|
|
12341
13173
|
var kitCommand = new Command18("kit").description("Install, verify and update a Superfunction Kit in this project");
|
|
12342
13174
|
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) => {
|
|
12343
|
-
const targetDir =
|
|
13175
|
+
const targetDir = resolve6(directory);
|
|
12344
13176
|
await withResult(options.json === true, async () => runInstall({
|
|
12345
13177
|
setupUrl,
|
|
12346
13178
|
targetDir,
|
|
@@ -12368,7 +13200,7 @@ kitCommand.command("status").description("Report what is installed, activated an
|
|
|
12368
13200
|
try {
|
|
12369
13201
|
const report2 = await runStatus({ projectDir: dir }, await readOnlyAdapters(dir));
|
|
12370
13202
|
printJson(options.json === true, report2, () => {
|
|
12371
|
-
console.log(
|
|
13203
|
+
console.log(chalk53.bold(report2.installed ? `${report2.kitId} ${report2.release}` : "No Kit installed here"));
|
|
12372
13204
|
console.log(` activation: ${report2.activationId ?? "-"} credential: ${report2.credential}`);
|
|
12373
13205
|
console.log(` managed drift: ${report2.managedDrift} update available: ${report2.updateAvailable ?? "none"}`);
|
|
12374
13206
|
if (report2.operation) {
|
|
@@ -12385,15 +13217,15 @@ kitCommand.command("check").description("Check the installed release against its
|
|
|
12385
13217
|
const report2 = await runCheck({ projectDir: dir }, await readOnlyAdapters(dir));
|
|
12386
13218
|
printJson(options.json === true, report2, () => {
|
|
12387
13219
|
if (report2.diagnostics.length === 0) {
|
|
12388
|
-
console.log(`${
|
|
13220
|
+
console.log(`${chalk53.green("\u2713")} No problems found.`);
|
|
12389
13221
|
return;
|
|
12390
13222
|
}
|
|
12391
13223
|
for (const diagnostic of report2.diagnostics) {
|
|
12392
|
-
const mark = diagnostic.severity === "error" ?
|
|
13224
|
+
const mark = diagnostic.severity === "error" ? chalk53.red("\u2717") : chalk53.yellow("!");
|
|
12393
13225
|
console.log(`${mark} ${diagnostic.summary}`);
|
|
12394
|
-
console.log(` ${
|
|
13226
|
+
console.log(` ${chalk53.gray(diagnostic.code)}${diagnostic.path ? ` ${diagnostic.path}` : ""}`);
|
|
12395
13227
|
if (diagnostic.fixCommand) {
|
|
12396
|
-
console.log(` ${
|
|
13228
|
+
console.log(` ${chalk53.gray("next:")} ${diagnostic.fixCommand}`);
|
|
12397
13229
|
}
|
|
12398
13230
|
}
|
|
12399
13231
|
});
|
|
@@ -12483,8 +13315,8 @@ function parseKeys(raw) {
|
|
|
12483
13315
|
}
|
|
12484
13316
|
|
|
12485
13317
|
// src/kit/scaffold.ts
|
|
12486
|
-
import { mkdirSync as mkdirSync19, existsSync as
|
|
12487
|
-
import { dirname as
|
|
13318
|
+
import { mkdirSync as mkdirSync19, existsSync as existsSync53, readFileSync as readFileSync32, writeFileSync as writeFileSync34 } from "fs";
|
|
13319
|
+
import { dirname as dirname18, join as join55 } from "path";
|
|
12488
13320
|
|
|
12489
13321
|
// src/kit/http/registry.ts
|
|
12490
13322
|
import { createHash as createHash3 } from "crypto";
|
|
@@ -12672,9 +13504,9 @@ var KitRegistryProxyClient = class {
|
|
|
12672
13504
|
return response.bytes;
|
|
12673
13505
|
}
|
|
12674
13506
|
};
|
|
12675
|
-
function registryEntitlementProbe(client,
|
|
13507
|
+
function registryEntitlementProbe(client, resolve7) {
|
|
12676
13508
|
return async (request2) => {
|
|
12677
|
-
const target = await
|
|
13509
|
+
const target = await resolve7(request2);
|
|
12678
13510
|
if (target === null) {
|
|
12679
13511
|
return { entitled: false, reason: "unavailable" };
|
|
12680
13512
|
}
|
|
@@ -12830,20 +13662,20 @@ var ArtifactScaffoldPort = class {
|
|
|
12830
13662
|
}
|
|
12831
13663
|
};
|
|
12832
13664
|
function seedProjectName(targetDir, name) {
|
|
12833
|
-
const file =
|
|
12834
|
-
if (!
|
|
13665
|
+
const file = join55(targetDir, "package.json");
|
|
13666
|
+
if (!existsSync53(file)) {
|
|
12835
13667
|
return;
|
|
12836
13668
|
}
|
|
12837
13669
|
let parsed;
|
|
12838
13670
|
try {
|
|
12839
|
-
parsed = JSON.parse(
|
|
13671
|
+
parsed = JSON.parse(readFileSync32(file, "utf8"));
|
|
12840
13672
|
} catch {
|
|
12841
13673
|
throw new KitError("KIT_MANIFEST_INVALID", "The release scaffold's package.json is not readable JSON.", {
|
|
12842
13674
|
evidence: { path: "package.json" }
|
|
12843
13675
|
});
|
|
12844
13676
|
}
|
|
12845
13677
|
parsed.name = packageNameOf(name);
|
|
12846
|
-
|
|
13678
|
+
writeFileSync34(file, `${JSON.stringify(parsed, null, 4)}
|
|
12847
13679
|
`, "utf8");
|
|
12848
13680
|
}
|
|
12849
13681
|
function packageNameOf(name) {
|
|
@@ -12851,9 +13683,9 @@ function packageNameOf(name) {
|
|
|
12851
13683
|
return cleaned.length === 0 ? "kit-project" : cleaned;
|
|
12852
13684
|
}
|
|
12853
13685
|
function writeScaffoldRecord(targetDir, record) {
|
|
12854
|
-
const file =
|
|
12855
|
-
mkdirSync19(
|
|
12856
|
-
|
|
13686
|
+
const file = join55(targetDir, SCAFFOLD_RECORD_PATH);
|
|
13687
|
+
mkdirSync19(dirname18(file), { recursive: true });
|
|
13688
|
+
writeFileSync34(file, `${JSON.stringify(record, null, 4)}
|
|
12857
13689
|
`, "utf8");
|
|
12858
13690
|
}
|
|
12859
13691
|
|
|
@@ -13232,9 +14064,9 @@ function requireCatalogUrl(projectDir2) {
|
|
|
13232
14064
|
}
|
|
13233
14065
|
|
|
13234
14066
|
// src/kit/local/index.ts
|
|
13235
|
-
import { createRequire as
|
|
13236
|
-
import { join as
|
|
13237
|
-
import { pathToFileURL as
|
|
14067
|
+
import { createRequire as createRequire4 } from "module";
|
|
14068
|
+
import { join as join59 } from "path";
|
|
14069
|
+
import { pathToFileURL as pathToFileURL6 } from "url";
|
|
13238
14070
|
|
|
13239
14071
|
// src/kit/local/process.ts
|
|
13240
14072
|
import { execa as execa13 } from "execa";
|
|
@@ -13382,8 +14214,8 @@ ${result.stdout}`;
|
|
|
13382
14214
|
}
|
|
13383
14215
|
|
|
13384
14216
|
// src/kit/local/database.ts
|
|
13385
|
-
import { existsSync as
|
|
13386
|
-
import { dirname as
|
|
14217
|
+
import { existsSync as existsSync54 } from "fs";
|
|
14218
|
+
import { dirname as dirname19, join as join56 } from "path";
|
|
13387
14219
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
13388
14220
|
var DEFAULT_DATABASE_TIMEOUT_MS = 9e5;
|
|
13389
14221
|
var SpfnDatabasePort = class {
|
|
@@ -13466,13 +14298,13 @@ function spfnCommand() {
|
|
|
13466
14298
|
return bin === null ? { file: "spfn", args: [] } : { file: process.execPath, args: [bin] };
|
|
13467
14299
|
}
|
|
13468
14300
|
function resolveSpfnBin(from = fileURLToPath3(import.meta.url)) {
|
|
13469
|
-
let directory =
|
|
14301
|
+
let directory = dirname19(from);
|
|
13470
14302
|
for (let depth = 0; depth < 8; depth += 1) {
|
|
13471
|
-
const candidate =
|
|
13472
|
-
if (
|
|
14303
|
+
const candidate = join56(directory, "bin", "spfn.js");
|
|
14304
|
+
if (existsSync54(candidate)) {
|
|
13473
14305
|
return candidate;
|
|
13474
14306
|
}
|
|
13475
|
-
const parent =
|
|
14307
|
+
const parent = dirname19(directory);
|
|
13476
14308
|
if (parent === directory) {
|
|
13477
14309
|
break;
|
|
13478
14310
|
}
|
|
@@ -13482,8 +14314,8 @@ function resolveSpfnBin(from = fileURLToPath3(import.meta.url)) {
|
|
|
13482
14314
|
}
|
|
13483
14315
|
|
|
13484
14316
|
// src/kit/local/gates.ts
|
|
13485
|
-
import { existsSync as
|
|
13486
|
-
import { join as
|
|
14317
|
+
import { existsSync as existsSync55, readFileSync as readFileSync33 } from "fs";
|
|
14318
|
+
import { join as join57 } from "path";
|
|
13487
14319
|
var DEFAULT_GATE_TIMEOUT_MS = 9e5;
|
|
13488
14320
|
var GATE_PLANS = {
|
|
13489
14321
|
"kit-check": { scripts: [], fallback: spfnFallback(["kit", "check", "--json"]) },
|
|
@@ -13534,12 +14366,12 @@ var CommandGatePort = class {
|
|
|
13534
14366
|
}
|
|
13535
14367
|
};
|
|
13536
14368
|
function projectScripts(cwd) {
|
|
13537
|
-
const file =
|
|
13538
|
-
if (!
|
|
14369
|
+
const file = join57(cwd, "package.json");
|
|
14370
|
+
if (!existsSync55(file)) {
|
|
13539
14371
|
return {};
|
|
13540
14372
|
}
|
|
13541
14373
|
try {
|
|
13542
|
-
const parsed = JSON.parse(
|
|
14374
|
+
const parsed = JSON.parse(readFileSync33(file, "utf8"));
|
|
13543
14375
|
return parsed.scripts ?? {};
|
|
13544
14376
|
} catch {
|
|
13545
14377
|
return {};
|
|
@@ -13551,8 +14383,8 @@ function spfnFallback(args) {
|
|
|
13551
14383
|
}
|
|
13552
14384
|
|
|
13553
14385
|
// src/kit/local/git.ts
|
|
13554
|
-
import { existsSync as
|
|
13555
|
-
import { join as
|
|
14386
|
+
import { existsSync as existsSync56 } from "fs";
|
|
14387
|
+
import { join as join58 } from "path";
|
|
13556
14388
|
var FALLBACK_AUTHOR_NAME = "spfn kit";
|
|
13557
14389
|
var FALLBACK_AUTHOR_EMAIL = "kit@superfunction.xyz";
|
|
13558
14390
|
var DEFAULT_GIT_TIMEOUT_MS = 12e4;
|
|
@@ -13563,7 +14395,7 @@ var SystemGitPort = class {
|
|
|
13563
14395
|
}
|
|
13564
14396
|
/** Start a repository, or leave the one that is already there alone. */
|
|
13565
14397
|
async init(request2) {
|
|
13566
|
-
if (
|
|
14398
|
+
if (existsSync56(join58(request2.cwd, ".git"))) {
|
|
13567
14399
|
return;
|
|
13568
14400
|
}
|
|
13569
14401
|
const result = await this.git(request2.cwd, ["init"]);
|
|
@@ -13680,8 +14512,8 @@ function createKitLocalPorts(options = {}) {
|
|
|
13680
14512
|
}
|
|
13681
14513
|
function createProjectModuleLoader() {
|
|
13682
14514
|
return async (specifier, projectDir2) => {
|
|
13683
|
-
const
|
|
13684
|
-
return import(
|
|
14515
|
+
const resolve7 = createRequire4(join59(projectDir2, "package.json"));
|
|
14516
|
+
return import(pathToFileURL6(resolve7.resolve(specifier)).href);
|
|
13685
14517
|
};
|
|
13686
14518
|
}
|
|
13687
14519
|
|