spfn 0.3.0-beta.12 → 0.3.0-beta.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +100 -8
  2. package/dist/index.js +1346 -518
  3. 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.12";
952
+ return "0.3.0-beta.13";
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((resolve4, reject) => {
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
- resolve4(readFileSync9(filePath, "utf-8").trim());
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((resolve4) => setTimeout(resolve4, 500));
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((resolve4) => {
2620
+ await new Promise((resolve7) => {
2621
2621
  const keepAlive = setInterval(() => {
2622
2622
  }, 1e6);
2623
2623
  process.once("beforeExit", () => {
2624
2624
  clearInterval(keepAlive);
2625
- resolve4();
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((resolve4) => setTimeout(resolve4, 500));
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((resolve4) => {
2740
+ await new Promise((resolve7) => {
2741
2741
  const keepAlive = setInterval(() => {
2742
2742
  }, 1e6);
2743
2743
  process.once("beforeExit", () => {
2744
2744
  clearInterval(keepAlive);
2745
- resolve4();
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((resolve4, reject) => {
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
- resolve4();
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((resolve4) => {
4136
+ return new Promise((resolve7) => {
4137
4137
  const server = net.createServer();
4138
4138
  server.once("error", () => {
4139
4139
  server.close();
4140
- resolve4(false);
4140
+ resolve7(false);
4141
4141
  });
4142
4142
  server.once("listening", () => {
4143
4143
  server.close();
4144
- resolve4(true);
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((resolve4, reject) => {
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
- resolve4();
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((resolve4, reject) => {
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
- resolve4();
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 existsSync28, readFileSync as readFileSync14, writeFileSync as writeFileSync18 } from "fs";
5338
- import { resolve as resolve2 } from "path";
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
- try {
5345
- const module = await import(`${packageName}/config`);
5346
- if (!module.envSchema) {
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
- throw new Error(`Failed to load package ${packageName}: ${message}`);
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 layerOf(entry) {
5379
+ return entry.layer ?? "environment";
5380
+ }
5381
+
5382
+ // src/utils/env-list.ts
5383
+ import { existsSync as existsSync28, readFileSync as readFileSync14 } from "fs";
5384
+ import { join as join29, resolve as resolve2 } from "path";
5385
+ import { pathToFileURL as pathToFileURL4 } from "url";
5386
+ import { loadAppConfig as loadAppConfig3 } from "@spfn/core/app-config";
5387
+ var CORE_PACKAGE = "@spfn/core";
5388
+ var TYPESCRIPT_FILE = /\.[cm]?tsx?$/;
5389
+ async function loadEnvList(options, cwd = process.cwd()) {
5390
+ if (options.package) {
5391
+ return mergeEnvSources([{ name: options.package, schema: await loadEnvSchema(options.package, cwd) }]);
5392
+ }
5393
+ return mergeEnvSources([
5394
+ ...await loadAppSchemas(cwd),
5395
+ ...await loadPackageSchemas(cwd)
5396
+ ]);
5397
+ }
5398
+ function mergeEnvSources(sources) {
5399
+ const schema = {};
5400
+ const declaredBy = {};
5401
+ const conflicts = [];
5402
+ for (const source of sources) {
5403
+ for (const [key, entry] of Object.entries(source.schema)) {
5404
+ const existing = schema[key];
5405
+ const owners = declaredBy[key] ?? [];
5406
+ declaredBy[key] = [...owners, source.name];
5407
+ if (!existing) {
5408
+ schema[key] = { ...entry, key };
5409
+ continue;
5410
+ }
5411
+ if (describeRules(existing) !== describeRules(entry)) {
5412
+ conflicts.push(`${key}: ${owners.join(", ")} declares ${describeRules(existing)}, ${source.name} declares ${describeRules(entry)}`);
5413
+ }
5414
+ }
5415
+ }
5416
+ if (conflicts.length > 0) {
5417
+ throw new Error(`Env schemas declare a variable differently:
5418
+ - ${conflicts.join("\n - ")}`);
5419
+ }
5420
+ return { sources, schema, declaredBy };
5421
+ }
5422
+ function groupBySource(list, entries = Object.values(list.schema)) {
5423
+ return list.sources.map((source) => ({
5424
+ source: source.name,
5425
+ entries: entries.filter((entry) => list.declaredBy[entry.key]?.[0] === source.name)
5426
+ })).filter((group) => group.entries.length > 0);
5427
+ }
5428
+ function alsoDeclaredBy(list, key) {
5429
+ return (list.declaredBy[key] ?? []).slice(1);
5430
+ }
5431
+ function describeEnvList(options) {
5432
+ return options.package ?? "whole app";
5433
+ }
5434
+ function describeRules(entry) {
5435
+ return `"${entry.type}" (${entry.required ? "required" : "optional"}${entry.sensitive ? ", sensitive" : ""}${layerOf(entry) === "instance" ? ", instance layer" : ""})`;
5436
+ }
5437
+ async function loadAppSchemas(cwd) {
5438
+ const paths = (await loadAppConfig3(cwd)).env?.schemas ?? [];
5439
+ if (!Array.isArray(paths) || paths.some((path5) => typeof path5 !== "string")) {
5440
+ throw new Error("spfn.config.js: env.schemas must be an array of paths relative to the project root.");
5441
+ }
5442
+ const sources = [];
5443
+ for (const path5 of paths) {
5444
+ sources.push({ name: path5, schema: await loadSchemaModule(cwd, path5) });
5445
+ }
5446
+ return sources;
5447
+ }
5448
+ async function loadSchemaModule(cwd, path5) {
5449
+ const fullPath = resolve2(cwd, path5);
5450
+ if (!existsSync28(fullPath)) {
5451
+ throw new Error(`spfn.config.js env.schemas: ${path5} does not exist.`);
5452
+ }
5453
+ const module = await importAppModule(cwd, fullPath).catch((error) => {
5454
+ const message = error instanceof Error ? error.message : String(error);
5455
+ return Promise.reject(new Error(`spfn.config.js env.schemas: ${path5} could not be loaded: ${message}`));
5456
+ });
5457
+ if (!module.envSchema) {
5458
+ throw new Error(`spfn.config.js env.schemas: ${path5} does not export envSchema.`);
5459
+ }
5460
+ return module.envSchema;
5461
+ }
5462
+ async function importAppModule(cwd, fullPath) {
5463
+ const url = pathToFileURL4(fullPath).href;
5464
+ const tsx = await loadTsx(cwd);
5465
+ if (tsx) {
5466
+ const tsconfig = join29(cwd, "tsconfig.json");
5467
+ return await tsx.tsImport(url, {
5468
+ parentURL: pathToFileURL4(join29(cwd, "noop.js")).href,
5469
+ tsconfig: existsSync28(tsconfig) ? tsconfig : false
5470
+ });
5471
+ }
5472
+ if (TYPESCRIPT_FILE.test(fullPath)) {
5473
+ throw new Error("tsx is not installed in this project, and it is what loads TypeScript. Add it: pnpm add -D tsx");
5474
+ }
5475
+ return await import(url);
5476
+ }
5477
+ async function loadTsx(cwd) {
5478
+ const path5 = resolveFrom(join29(cwd, "noop.js"), "tsx/esm/api") ?? resolveFrom(import.meta.url, "tsx/esm/api");
5479
+ return path5 ? await import(pathToFileURL4(path5).href) : void 0;
5480
+ }
5481
+ async function loadPackageSchemas(cwd) {
5482
+ const sources = [];
5483
+ for (const name of readSpfnDependencies(cwd)) {
5484
+ const path5 = resolveFrom(join29(cwd, "noop.js"), `${name}/config`);
5485
+ const module = path5 ? await importPackageConfig(name, path5) : void 0;
5486
+ if (module?.envSchema) {
5487
+ sources.push({ name, schema: module.envSchema });
5488
+ }
5489
+ }
5490
+ return sources;
5491
+ }
5492
+ async function importPackageConfig(name, path5) {
5493
+ return await import(pathToFileURL4(path5).href).catch((error) => {
5494
+ const message = error instanceof Error ? error.message : String(error);
5495
+ return Promise.reject(new Error(`Failed to load package ${name}: ${message}`));
5496
+ });
5497
+ }
5498
+ function readSpfnDependencies(cwd) {
5499
+ const packageJsonPath = join29(cwd, "package.json");
5500
+ if (!existsSync28(packageJsonPath)) {
5501
+ return [];
5502
+ }
5503
+ const packageJson = JSON.parse(readFileSync14(packageJsonPath, "utf-8"));
5504
+ const names = Object.keys({ ...packageJson.dependencies, ...packageJson.devDependencies }).filter((name) => name.startsWith("@spfn/")).sort();
5505
+ return names.includes(CORE_PACKAGE) ? [CORE_PACKAGE, ...names.filter((name) => name !== CORE_PACKAGE)] : names;
5506
+ }
5507
+
5508
+ // src/utils/env-audit/index.ts
5509
+ import { existsSync as existsSync30, readFileSync as readFileSync15 } from "fs";
5510
+ import { join as join32, relative as relative2, resolve as resolve3 } from "path";
5511
+ import { loadAppConfig as loadAppConfig4 } from "@spfn/core/app-config";
5512
+
5513
+ // src/utils/env-audit/files.ts
5514
+ import { existsSync as existsSync29, lstatSync, readdirSync as readdirSync2 } from "fs";
5515
+ import { join as join30 } from "path";
5516
+ var DEFAULT_INCLUDE = ["src"];
5517
+ var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
5518
+ "node_modules",
5519
+ ".git",
5520
+ "dist",
5521
+ "build",
5522
+ "out",
5523
+ "coverage",
5524
+ ".next",
5525
+ ".turbo",
5526
+ ".vercel",
5527
+ ".output",
5528
+ ".spfn"
5529
+ ]);
5530
+ var SOURCE_FILE = /\.(?:[cm]?[jt]s|[jt]sx)$/;
5531
+ var DECLARATION_FILE = /\.d\.[cm]?ts$/;
5532
+ var GLOB_CHARACTER = /[*?]/;
5533
+ function listAuditFiles(cwd, scope) {
5534
+ const include = scope.include.map(globToRegExp);
5535
+ const ignore = scope.ignore.map(globToRegExp);
5536
+ const files = /* @__PURE__ */ new Set();
5537
+ for (const root of new Set(scope.include.map(staticPrefix))) {
5538
+ walk(cwd, root, ignore, files);
5539
+ }
5540
+ return [...files].filter((file) => matchesAny(file, include)).sort();
5541
+ }
5542
+ function matchesAny(path5, globs) {
5543
+ const segments = path5.split("/");
5544
+ return segments.some((_, index) => {
5545
+ const prefix = segments.slice(0, index + 1).join("/");
5546
+ return globs.some((glob) => glob.test(prefix));
5547
+ });
5548
+ }
5549
+ function globToRegExp(glob) {
5550
+ const body = normalizeGlob(glob).split(/(\*\*\/|\*\*|\*|\?)/).map((part) => {
5551
+ switch (part) {
5552
+ case "**/":
5553
+ return "(?:.*/)?";
5554
+ case "**":
5555
+ return ".*";
5556
+ case "*":
5557
+ return "[^/]*";
5558
+ case "?":
5559
+ return "[^/]";
5560
+ default:
5561
+ return part.replace(/[.+^${}()|[\]\\]/g, "\\$&");
5562
+ }
5563
+ }).join("");
5564
+ return new RegExp(`^${body}$`);
5565
+ }
5566
+ function normalizeGlob(glob) {
5567
+ const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "").replace(/\/\*\*$/, "");
5568
+ if (normalized.startsWith("/") || normalized.split("/").includes("..")) {
5569
+ throw new Error(`spfn.config.js env.audit: "${glob}" must be relative to the project root and stay inside it.`);
5570
+ }
5571
+ return normalized === "." ? "**" : normalized;
5572
+ }
5573
+ function staticPrefix(glob) {
5574
+ const segments = normalizeGlob(glob).split("/");
5575
+ const firstWildcard = segments.findIndex((segment) => GLOB_CHARACTER.test(segment));
5576
+ return (firstWildcard === -1 ? segments : segments.slice(0, firstWildcard)).join("/");
5577
+ }
5578
+ function walk(cwd, relPath, ignore, files) {
5579
+ const fullPath = join30(cwd, relPath);
5580
+ if (!existsSync29(fullPath) || relPath && matchesAny(relPath, ignore)) {
5581
+ return;
5582
+ }
5583
+ const stats = lstatSync(fullPath);
5584
+ if (stats.isFile()) {
5585
+ addIfSource(relPath, files);
5586
+ }
5587
+ if (!stats.isDirectory()) {
5588
+ return;
5589
+ }
5590
+ for (const entry of readdirSync2(fullPath, { withFileTypes: true })) {
5591
+ const child = relPath ? `${relPath}/${entry.name}` : entry.name;
5592
+ if (entry.isDirectory() && !SKIPPED_DIRECTORIES.has(entry.name)) {
5593
+ walk(cwd, child, ignore, files);
5594
+ } else if (entry.isFile() && !matchesAny(child, ignore)) {
5595
+ addIfSource(child, files);
5596
+ }
5597
+ }
5598
+ }
5599
+ function addIfSource(relPath, files) {
5600
+ if (SOURCE_FILE.test(relPath) && !DECLARATION_FILE.test(relPath)) {
5601
+ files.add(relPath);
5602
+ }
5603
+ }
5604
+
5605
+ // src/utils/env-audit/parse.ts
5606
+ import { createRequire as createRequire2 } from "module";
5607
+ import { join as join31 } from "path";
5608
+ var ALWAYS_ALLOWED = "NODE_ENV";
5609
+ var MAX_KEY_TEXT = 40;
5610
+ var compiler;
5611
+ function loadTypeScript(cwd) {
5612
+ if (compiler) {
5613
+ return compiler;
5614
+ }
5615
+ const path5 = resolveFrom(join31(cwd, "noop.js"), "typescript") ?? resolveFrom(import.meta.url, "typescript") ?? resolveThroughCore();
5616
+ if (!path5) {
5617
+ throw new Error("spfn env audit parses sources with TypeScript, which is not installed. Add it: pnpm add -D typescript");
5618
+ }
5619
+ compiler = createRequire2(import.meta.url)(path5);
5620
+ return compiler;
5621
+ }
5622
+ function resolveThroughCore() {
5623
+ const core = resolveFrom(import.meta.url, "@spfn/core/app-config");
5624
+ return core ? resolveFrom(core, "typescript") : void 0;
5625
+ }
5626
+ function readSourceFacts(ts, fileName, text) {
5627
+ const source = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true, scriptKindOf(ts, fileName));
5628
+ const facts = { directReads: [], names: /* @__PURE__ */ new Set() };
5629
+ const visit = (node) => {
5630
+ collectName(ts, node, facts.names);
5631
+ collectDirectRead(ts, source, node, facts.directReads);
5632
+ ts.forEachChild(node, visit);
5633
+ };
5634
+ visit(source);
5635
+ facts.directReads = facts.directReads.filter((read) => read.name !== ALWAYS_ALLOWED);
5636
+ return facts;
5637
+ }
5638
+ function readPropertyLines(ts, fileName, text) {
5639
+ const source = ts.createSourceFile(fileName, text, ts.ScriptTarget.Latest, true, scriptKindOf(ts, fileName));
5640
+ const lines = /* @__PURE__ */ new Map();
5641
+ const visit = (node) => {
5642
+ const name = ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node) ? propertyNameText(ts, node.name) : void 0;
5643
+ if (name && !lines.has(name)) {
5644
+ lines.set(name, lineOf(source, node));
5645
+ }
5646
+ ts.forEachChild(node, visit);
5647
+ };
5648
+ visit(source);
5649
+ return lines;
5650
+ }
5651
+ function scriptKindOf(ts, fileName) {
5652
+ if (fileName.endsWith(".tsx")) {
5653
+ return ts.ScriptKind.TSX;
5654
+ }
5655
+ return /\.[cm]?tsx?$/.test(fileName) ? ts.ScriptKind.TS : ts.ScriptKind.JSX;
5656
+ }
5657
+ function collectName(ts, node, names) {
5658
+ if (ts.isPropertyAccessExpression(node)) {
5659
+ names.add(node.name.text);
5660
+ } else if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
5661
+ names.add(node.text);
5662
+ } else if (ts.isBindingElement(node) && ts.isObjectBindingPattern(node.parent)) {
5663
+ const name = propertyNameText(ts, node.propertyName ?? node.name);
5664
+ if (name) {
5665
+ names.add(name);
5666
+ }
5667
+ }
5668
+ }
5669
+ function collectDirectRead(ts, source, node, reads) {
5670
+ if (ts.isPropertyAccessExpression(node) && isProcessEnv(ts, node.expression)) {
5671
+ reads.push({ line: lineOf(source, node), name: node.name.text, reason: "read from process.env" });
5672
+ } else if (ts.isElementAccessExpression(node) && isProcessEnv(ts, node.expression)) {
5673
+ reads.push({ line: lineOf(source, node), ...describeElementKey(ts, source, node.argumentExpression) });
5674
+ } else if (ts.isVariableDeclaration(node) && node.initializer && isProcessEnv(ts, node.initializer) && ts.isObjectBindingPattern(node.name)) {
5675
+ reads.push(...node.name.elements.map((element) => destructuredRead(ts, source, element)));
5676
+ }
5677
+ }
5678
+ function describeElementKey(ts, source, key) {
5679
+ if (ts.isStringLiteralLike(key)) {
5680
+ return { name: key.text, reason: "read from process.env" };
5681
+ }
5682
+ const text = key.getText(source).replace(/\s+/g, " ");
5683
+ return {
5684
+ name: `[${text.length > MAX_KEY_TEXT ? `${text.slice(0, MAX_KEY_TEXT)}\u2026` : text}]`,
5685
+ reason: "read from process.env under a computed key"
5686
+ };
5687
+ }
5688
+ function destructuredRead(ts, source, element) {
5689
+ const name = element.dotDotDotToken ? `...${element.name.getText(source)}` : propertyNameText(ts, element.propertyName ?? element.name) ?? `[${(element.propertyName ?? element.name).getText(source)}]`;
5690
+ return { line: lineOf(source, element), name, reason: "destructured from process.env" };
5691
+ }
5692
+ function isProcessEnv(ts, node) {
5693
+ const expression = skipWrappers(ts, node);
5694
+ return ts.isPropertyAccessExpression(expression) && expression.name.text === "env" && isProcess(ts, skipWrappers(ts, expression.expression));
5695
+ }
5696
+ function isProcess(ts, node) {
5697
+ if (ts.isIdentifier(node)) {
5698
+ return node.text === "process";
5699
+ }
5700
+ return ts.isPropertyAccessExpression(node) && node.name.text === "process" && ts.isIdentifier(node.expression) && ["globalThis", "global", "window", "self"].includes(node.expression.text);
5701
+ }
5702
+ function skipWrappers(ts, node) {
5703
+ let current = node;
5704
+ while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isNonNullExpression(current) || ts.isSatisfiesExpression(current)) {
5705
+ current = current.expression;
5706
+ }
5707
+ return current;
5708
+ }
5709
+ function propertyNameText(ts, name) {
5710
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name) || ts.isPrivateIdentifier(name)) {
5711
+ return name.text;
5712
+ }
5713
+ return void 0;
5714
+ }
5715
+ function lineOf(source, node) {
5716
+ return source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
5717
+ }
5718
+
5719
+ // src/utils/env-audit/index.ts
5720
+ var ROOT_CONFIG_FILE = /^[^/]+\.config\.(?:js|cjs|mjs|ts)$/;
5721
+ async function runEnvAudit(cwd = process.cwd()) {
5722
+ const scope = readScope((await loadAppConfig4(cwd)).env?.audit);
5723
+ const appSources = await loadAppSchemas(cwd);
5724
+ const list = mergeEnvSources([...appSources, ...await loadPackageSchemas(cwd)]);
5725
+ const ts = loadTypeScript(cwd);
5726
+ const files = listAuditFiles(cwd, scope);
5727
+ const context = {
5728
+ cwd,
5729
+ ts,
5730
+ list,
5731
+ appSources,
5732
+ schemaFiles: new Set(appSources.map((source) => toProjectPath(cwd, source.name))),
5733
+ facts: new Map(files.map((file) => [file, readSourceFacts(ts, file, readFileSync15(join32(cwd, file), "utf-8"))]))
5734
+ };
5735
+ return {
5736
+ fileCount: files.length,
5737
+ directReads: findDirectReads(context),
5738
+ declarations: appSources.length > 0 ? findUnusedDeclarations(context) : [],
5739
+ notices: appSources.length > 0 ? [] : ["No env.schemas in spfn.config.js \u2014 the unused-declaration check is skipped."]
5740
+ };
5741
+ }
5742
+ function readScope(audit) {
5743
+ const include = audit?.include ?? DEFAULT_INCLUDE;
5744
+ const ignore = audit?.ignore ?? [];
5745
+ for (const [field, value] of [["include", include], ["ignore", ignore]]) {
5746
+ if (!Array.isArray(value) || value.some((glob) => typeof glob !== "string")) {
5747
+ throw new Error(`spfn.config.js: env.audit.${field} must be an array of globs relative to the project root.`);
5748
+ }
5749
+ }
5750
+ return { include, ignore };
5751
+ }
5752
+ function findDirectReads({ facts, schemaFiles }) {
5753
+ return [...facts].filter(([file]) => !schemaFiles.has(file) && !ROOT_CONFIG_FILE.test(file)).flatMap(([file, { directReads }]) => directReads.map((read) => ({ file, ...read })));
5754
+ }
5755
+ function findUnusedDeclarations(context) {
5756
+ const readNames = new Set([...context.facts].filter(([file]) => !context.schemaFiles.has(file)).flatMap(([, { names }]) => [...names]));
5757
+ return context.appSources.flatMap((source) => {
5758
+ const file = toProjectPath(context.cwd, source.name);
5759
+ const lines = readPropertyLines(context.ts, file, readFileSync15(join32(context.cwd, file), "utf-8"));
5760
+ return Object.entries(source.schema).filter(([name]) => !isPackageName(context, name)).flatMap(([name, entry]) => checkEntry(context, readNames, name, entry).map((reason) => ({ file, line: lines.get(name) ?? 1, name, reason })));
5761
+ });
5762
+ }
5763
+ function checkEntry(context, readNames, name, entry) {
5764
+ if (entry.readBy === void 0) {
5765
+ return readNames.has(name) ? [] : ["declared but read nowhere in scope (unused)"];
5766
+ }
5767
+ if (!Array.isArray(entry.readBy) || entry.readBy.some((path5) => typeof path5 !== "string")) {
5768
+ return ["readBy must be an array of paths relative to the project root"];
5769
+ }
5770
+ return checkReadBy(context, entry.readBy);
5771
+ }
5772
+ function isPackageName({ list, appSources }, name) {
5773
+ const appNames = new Set(appSources.map((source) => source.name));
5774
+ return (list.declaredBy[name] ?? []).some((owner) => !appNames.has(owner));
5775
+ }
5776
+ function checkReadBy({ cwd, facts }, readBy) {
5777
+ return readBy.flatMap((path5) => {
5778
+ const file = toProjectPath(cwd, path5);
5779
+ if (!existsSync30(join32(cwd, file))) {
5780
+ return [`readBy file ${path5} does not exist`];
5781
+ }
5782
+ return facts.has(file) ? [] : [`readBy file ${path5} is not scanned (see env.audit)`];
5783
+ });
5784
+ }
5785
+ function toProjectPath(cwd, path5) {
5786
+ return relative2(cwd, resolve3(cwd, path5)).split("\\").join("/");
5787
+ }
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 packageName = options.package || "@spfn/core";
5832
+ const label = describeEnvList(options);
5416
5833
  try {
5417
- const envSchema = await loadEnvSchema(packageName);
5418
- const allVars = Object.entries(envSchema);
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 (${packageName})
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 (${packageName})
5856
+ \u{1F4CB} Environment Variables (${label})
5440
5857
  `));
5441
- for (const [key, schema] of allVars) {
5442
- printEnvVar(key, schema, true);
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 printEnvVar(key, schema, showFile = false) {
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 envSchema = await loadEnvSchema(packageName);
5899
+ const list = await loadEnvList(options);
5473
5900
  console.log(chalk32.blue.bold(`
5474
- \u{1F4CA} Environment Variable Statistics (${packageName})
5901
+ \u{1F4CA} Environment Variable Statistics (${describeEnvList(options)})
5475
5902
  `));
5476
- const allVars = Object.entries(envSchema);
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 envSchema = await loadEnvSchema(packageName);
5947
+ const list = await loadEnvList(options);
5522
5948
  const normalizedQuery = query.toLowerCase();
5523
5949
  const results = [];
5524
- for (const [key, schema] of Object.entries(envSchema)) {
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>", "Package name to read env schema from", "@spfn/core").option("-g, --group", "Group variables by target file").action(listEnvVars);
5559
- envCommand.command("stats").description("Show environment variable statistics").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(showEnvStats);
5560
- envCommand.command("search").description("Search environment variables").argument("<query>", "Search query (matches key or description)").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").action(searchEnvVars);
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 envSchema = await loadEnvSchema(packageName);
5577
- const allVars = Object.entries(envSchema);
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 = resolve2(cwd, file);
5628
- if (existsSync28(filePath) && !force) {
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 envSchema = await loadEnvSchema(packageName);
5666
- const allVars = Object.entries(envSchema);
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 = resolve2(cwd, file);
5677
- if (!existsSync28(filePath)) {
6107
+ const filePath = resolve4(cwd, file);
6108
+ if (!existsSync31(filePath)) {
5678
6109
  continue;
5679
6110
  }
5680
- const content = readFileSync14(filePath, "utf-8");
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
- const inSchema = allVars.some(([k]) => k === key);
5714
- if (!inSchema) {
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
- for (const issue of issues) {
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
- for (const warning of warnings) {
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
- envCommand.command("init").description("Generate .env template files from schema").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").option("-e, --env <environment>", "Generate environment-specific templates (e.g. production, staging)").option("-f, --force", "Overwrite existing files").action(initEnvFiles);
5749
- envCommand.command("check").description("Check .env files against schema").option("-p, --package <package>", "Package name to read env schema from", "@spfn/core").option("-e, --env <environment>", "Check files for a specific environment (e.g. production)").action(checkEnvFiles);
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 allErrors = [];
5769
- const allWarnings = [];
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 (allErrors.length > 0) {
5796
- console.log(chalk32.red.bold(`\u274C Validation Errors (${allErrors.length}):
6213
+ if (errors.length > 0) {
6214
+ console.log(chalk32.red.bold(`\u274C Validation Errors (${errors.length}):
5797
6215
  `));
5798
- for (const error of allErrors) {
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 (allWarnings.length > 0) {
5806
- console.log(chalk32.yellow.bold(`\u26A0\uFE0F Warnings (${allWarnings.length}):
6218
+ if (warnings.length > 0) {
6219
+ console.log(chalk32.yellow.bold(`\u26A0\uFE0F Warnings (${warnings.length}):
5807
6220
  `));
5808
- for (const warning of allWarnings) {
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 (allErrors.length === 0 && allWarnings.length === 0) {
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 (allErrors.length === 0) {
6225
+ } else if (errors.length === 0) {
5817
6226
  console.log(chalk32.green("\u2705 No errors found."));
5818
- console.log(chalk32.yellow(`\u26A0\uFE0F ${allWarnings.length} warning(s) found.
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 ${allErrors.length} error(s)
6231
+ \u274C Validation failed with ${errors.length} error(s)
5823
6232
  `));
5824
6233
  process.exit(1);
5825
6234
  }
5826
6235
  }
5827
- envCommand.command("validate").description("Validate environment variables against schema (for CI/CD)").option("-p, --packages <packages...>", "Packages to validate", ["@spfn/core"]).option("-e, --env <environment>", "Load env files for specific environment before validating").option("-s, --strict", "Exit on any error (including load failures)").action(validateEnvVars);
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((resolve4, reject) => {
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
- resolve4();
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((resolve4, reject) => {
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
- resolve4(null);
6785
+ resolve7(null);
6277
6786
  return;
6278
6787
  }
6279
6788
  reject(error);
6280
6789
  return;
6281
6790
  }
6282
- resolve4(stdout.trim() || null);
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((resolve4, reject) => {
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
- resolve4();
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 join28 } from "path";
6388
- import { pathToFileURL as pathToFileURL3 } from "url";
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 = createRequire(join28(process.cwd(), "noop.js"));
6393
- return await import(pathToFileURL3(requireFromApp.resolve(specifier)).href);
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 join30 } from "path";
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 existsSync29, mkdirSync as mkdirSync5, writeFileSync as writeFileSync19 } from "fs";
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 (!existsSync29(absFile)) {
7325
+ if (!existsSync32(absFile)) {
6825
7326
  return {};
6826
7327
  }
6827
- const { stdout } = await execa11("sops", ["--decrypt", "--output-type", "json", absFile]);
6828
- return JSON.parse(stdout);
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 (existsSync29(absFile)) {
6832
- await execa11("sops", ["set", absFile, `["${key}"]`, JSON.stringify(value)]);
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
- "--encrypt",
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 existsSync30, readdirSync as readdirSync2 } from "fs";
6858
- import { join as join29 } from "path";
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: join29(cwd, relFile), relFile };
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 (existsSync30(join29(dir, filename))) {
7404
+ if (existsSync33(join34(dir, filename))) {
6868
7405
  return dir;
6869
7406
  }
6870
- const parent = join29(dir, "..");
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 ? join29(dir, ".sops.yaml") : null;
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 = join29(cwd, SECRETS_DIR);
6887
- if (!existsSync30(dir)) {
7423
+ const dir = join34(cwd, SECRETS_DIR);
7424
+ if (!existsSync33(dir)) {
6888
7425
  return [];
6889
7426
  }
6890
- return readdirSync2(dir).filter((name) => name.endsWith(".enc.json")).map((name) => join29(dir, name));
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(env9) {
6904
- if (isLocalEnv(env9)) {
7440
+ async function describeTarget(target) {
7441
+ if (isLocalEnv(target.env)) {
6905
7442
  return detectStore().label;
6906
7443
  }
6907
- return `SOPS (${getSopsFile(process.cwd(), env9).relFile})`;
7444
+ return `SOPS (${getSopsFile(process.cwd(), target.env, target.instance).relFile})`;
6908
7445
  }
6909
- async function storeSecret(cwd, env9, key, value) {
7446
+ async function storeSecret(cwd, target, key, value) {
6910
7447
  assertValidKey(key);
6911
- if (isLocalEnv(env9)) {
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 = join30(cwd, ".env.server");
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, env9);
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) => layerOf(entry) === layer) : entries;
7518
+ }
7519
+ function assertTargetLayer(entry, target) {
7520
+ const layer = targetLayer(target);
7521
+ if (!entry || !layer || layerOf(entry) === layer) {
7522
+ return;
7523
+ }
7524
+ const hint = layer === "environment" ? "pass --instance <name>" : "drop --instance";
7525
+ logger.error(`${entry.key} is declared with layer "${layerOf(entry)}" \u2014 ${hint}.`);
7526
+ process.exit(1);
7527
+ }
7528
+
6937
7529
  // src/commands/secret/set.ts
6938
7530
  async function secretSet(key, options) {
6939
- const env9 = resolveEnv(options.env);
6940
- const pkg = options.package ?? "@spfn/core";
6941
- const resolvedKey = key ?? await pickSecretKey(pkg);
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 warnIfNotSecret(pkg, resolvedKey);
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(resolvedKey)} (${env9})`
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
- logger.step(`Storing ${resolvedKey} \u2192 ${await describeTarget(env9)}`);
6957
- try {
6958
- await storeSecret(process.cwd(), env9, resolvedKey, value);
6959
- } catch (error) {
6960
- logger.error(error instanceof Error ? error.message : String(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
- async function pickSecretKey(pkg) {
6965
- let entries;
6966
- try {
6967
- entries = secretEntries(await loadEnvSchema(pkg));
6968
- } catch {
6969
- return void 0;
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 warnIfNotSecret(pkg, key) {
6987
- try {
6988
- const schema = await loadEnvSchema(pkg);
6989
- const entry = schema[key];
6990
- if (entry && !entry.sensitive) {
6991
- logger.warn(`${key} is not marked as a secret in ${pkg}. Consider declaring it with envSecret().`);
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 env9 = resolveEnv(options.env);
7002
- const pkg = options.package ?? "@spfn/core";
7003
- let entries;
7004
- try {
7005
- entries = secretEntries(await loadEnvSchema(pkg));
7006
- } catch (error) {
7007
- logger.error(error instanceof Error ? error.message : String(error));
7008
- process.exit(1);
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 (${pkg}) \u2014 ${env9}
7627
+ \u{1F511} Secrets (${label}) \u2014 ${target.instance ? `${target.env}.${target.instance}` : target.env}
7017
7628
  `));
7018
- for (const entry of entries) {
7019
- const status = statusOf(entry, present.has(entry.key));
7020
- console.log(` ${badge(status)} ${chalk38.cyan(entry.key)}${entry.generate ? chalk38.dim(" (generatable)") : ""}`);
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
- async function loadPresence(env9, entries) {
7025
- if (isLocalEnv(env9)) {
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
- await ensureSopsInstalled();
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 env9 = resolveEnv(options.env);
7066
- const pkg = options.package ?? "@spfn/core";
7067
- let schema;
7068
- try {
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(env9)}`);
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(), env9, entry.key, value);
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 env9 = resolveEnv(options.env);
7112
- const pkg = options.package ?? "@spfn/core";
7113
- let schema;
7114
- try {
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(env9)}`);
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(), env9, entry.key, generateSecretValue(entry.generate));
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 ${env9}\`.`);
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 existsSync31, mkdirSync as mkdirSync6 } from "fs";
7769
+ import { existsSync as existsSync34, mkdirSync as mkdirSync6 } from "fs";
7158
7770
  import { homedir } from "os";
7159
- import { dirname as dirname6, join as join31 } from "path";
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 ?? join31(homedir(), ".config", "sops", "age", "keys.txt");
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 (existsSync31(keyFile)) {
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 existsSync32, readFileSync as readFileSync15, writeFileSync as writeFileSync20 } from "fs";
7210
- import { join as join32 } from "path";
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 = join32(cwd, SOPS_CONFIG);
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 (!existsSync32(configPath)) {
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 (!existsSync32(configPath)) {
7880
+ if (!existsSync35(configPath)) {
7269
7881
  return { creation_rules: [] };
7270
7882
  }
7271
- return parse3(readFileSync15(configPath, "utf-8")) ?? { creation_rules: [] };
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,55 @@ async function reencrypt(cwd) {
7299
7911
  }
7300
7912
 
7301
7913
  // src/commands/secret/check.ts
7302
- init_logger();
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 pkg = options.package ?? "@spfn/core";
7311
- let entries;
7312
- try {
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(join33(cwd, file));
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(join33(cwd, ".env.server"));
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
+ if (target.instance) {
7950
+ files.push({ file: getSopsFile(cwd, target.env, target.instance), layer: "instance" });
7951
+ }
7952
+ for (const { file, layer } of files) {
7953
+ for (const name of sopsKeyNames(file.absFile)) {
7954
+ const entry = Object.hasOwn(schema, name) ? schema[name] : void 0;
7955
+ if (!entry) {
7956
+ warnings.push(`${chalk43.cyan(name)} in ${chalk43.yellow(file.relFile)} is not in the env list \u2014 export skips it.`);
7957
+ } else if (layerOf(entry) !== layer) {
7958
+ issues.push(`${chalk43.cyan(name)} is declared with layer "${layerOf(entry)}" but is in ${chalk43.yellow(file.relFile)}.`);
7959
+ }
7960
+ }
7961
+ }
7962
+ }
7338
7963
  function report(issues, warnings, cwd) {
7339
7964
  console.log(chalk43.blue.bold("\n\u{1F50D} Secret hygiene check\n"));
7340
7965
  for (const issue of issues) {
@@ -7355,26 +7980,229 @@ function report(issues, warnings, cwd) {
7355
7980
  }
7356
7981
  }
7357
7982
 
7983
+ // src/commands/secret/export.ts
7984
+ init_logger();
7985
+ init_env_file();
7986
+ import { existsSync as existsSync36, statSync } from "fs";
7987
+ import { dirname as dirname8, resolve as resolve5 } from "path";
7988
+ import chalk44 from "chalk";
7989
+
7990
+ // src/utils/secret-layers.ts
7991
+ function mergeLayers(schema, sources) {
7992
+ const merge = { values: {}, origins: {}, errors: [], warnings: [] };
7993
+ for (const [name, found] of collectListed(schema, sources, merge.warnings)) {
7994
+ const reason = placementError(schema[name], found) ?? valueError(schema[name], found[0].values[name]);
7995
+ if (reason) {
7996
+ merge.errors.push(`${name}: ${reason}`);
7997
+ continue;
7998
+ }
7999
+ merge.values[name] = found[0].values[name];
8000
+ merge.origins[name] = found[0].label;
8001
+ }
8002
+ for (const entry of Object.values(schema)) {
8003
+ if (isRequired(entry) && !sources.some((source) => Object.hasOwn(source.values, entry.key))) {
8004
+ merge.errors.push(`${entry.key}: required, and in none of the layers`);
8005
+ }
8006
+ }
8007
+ return merge;
8008
+ }
8009
+ function collectListed(schema, sources, warnings) {
8010
+ const found = /* @__PURE__ */ new Map();
8011
+ for (const source of sources) {
8012
+ for (const name of Object.keys(source.values)) {
8013
+ if (!Object.hasOwn(schema, name)) {
8014
+ warnings.push(`${name} (in ${source.label}) is not in the env list \u2014 not written`);
8015
+ continue;
8016
+ }
8017
+ found.set(name, [...found.get(name) ?? [], source]);
8018
+ }
8019
+ }
8020
+ return found;
8021
+ }
8022
+ function placementError(entry, found) {
8023
+ if (found.length > 1) {
8024
+ return `in more than one layer: ${found.map((source) => source.label).join(" and ")}`;
8025
+ }
8026
+ if (found[0].kind !== layerOf(entry)) {
8027
+ return `declared with layer "${layerOf(entry)}", but found in ${found[0].label}`;
8028
+ }
8029
+ return void 0;
8030
+ }
8031
+ function valueError(entry, value) {
8032
+ if (entry.minLength !== void 0 && value.length < entry.minLength) {
8033
+ return `must be at least ${entry.minLength} characters long`;
8034
+ }
8035
+ if (!entry.validator) {
8036
+ return void 0;
8037
+ }
8038
+ try {
8039
+ entry.validator(value);
8040
+ return void 0;
8041
+ } catch (error) {
8042
+ return withoutValue(error instanceof Error ? error.message : String(error), value);
8043
+ }
8044
+ }
8045
+ function withoutValue(reason, value) {
8046
+ if (value.length > 0 && reason.includes(value)) {
8047
+ return "rejected by its validator (the message is withheld because it contains the value)";
8048
+ }
8049
+ return reason;
8050
+ }
8051
+ function isRequired(entry) {
8052
+ return entry.required === true && entry.default === void 0;
8053
+ }
8054
+
8055
+ // src/utils/dotenv-out.ts
8056
+ import { closeSync, fsyncSync, openSync, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync21 } from "fs";
8057
+ import { randomBytes as randomBytes3 } from "crypto";
8058
+ import { basename as basename2, dirname as dirname7, join as join39 } from "path";
8059
+ import { parse as parse4 } from "dotenv";
8060
+ var QUOTES = ["'", "`", '"'];
8061
+ function formatDotenv(values) {
8062
+ const unrepresentable = [];
8063
+ const lines = Object.entries(values).map(([name, value]) => {
8064
+ const line = dotenvLine(name, value);
8065
+ if (!line) {
8066
+ unrepresentable.push(name);
8067
+ }
8068
+ return line;
8069
+ });
8070
+ const content = lines.join("\n") + "\n";
8071
+ const mismatched = Object.keys(values).filter((name) => parse4(content)[name] !== values[name]);
8072
+ const failed2 = [.../* @__PURE__ */ new Set([...unrepresentable, ...mismatched])];
8073
+ if (failed2.length > 0) {
8074
+ throw new Error(`Cannot write as dotenv without changing the value: ${failed2.join(", ")}`);
8075
+ }
8076
+ return content;
8077
+ }
8078
+ function dotenvLine(name, value) {
8079
+ const quoted = value.endsWith("\\") ? [] : QUOTES.map((quote) => `${quote}${value}${quote}`);
8080
+ return [...quoted, value].map((form) => `${name}=${form}`).find((line) => parse4(line)[name] === value);
8081
+ }
8082
+ function writePrivateFileAtomic(path5, content) {
8083
+ const temp = join39(dirname7(path5), `.${basename2(path5)}.${randomBytes3(6).toString("hex")}.tmp`);
8084
+ const fd = openSync(temp, "wx", 384);
8085
+ try {
8086
+ writeAndClose(fd, content);
8087
+ renameSync2(temp, path5);
8088
+ } finally {
8089
+ rmSync2(temp, { force: true });
8090
+ }
8091
+ }
8092
+ function writeAndClose(fd, content) {
8093
+ try {
8094
+ writeFileSync21(fd, content);
8095
+ fsyncSync(fd);
8096
+ } finally {
8097
+ closeSync(fd);
8098
+ }
8099
+ }
8100
+
8101
+ // src/commands/secret/export.ts
8102
+ async function secretExport(options) {
8103
+ const target = resolveTarget(options);
8104
+ if (!options.out) {
8105
+ logger.error("--out <file> is required: export writes values to a file, never to the terminal.");
8106
+ process.exit(1);
8107
+ }
8108
+ if (isLocalEnv(target.env)) {
8109
+ logger.error("export reads the encrypted files of a deployed environment; pass --env <env>.");
8110
+ process.exit(1);
8111
+ }
8112
+ await ensureSopsInstalled();
8113
+ const request2 = {
8114
+ cwd: process.cwd(),
8115
+ target,
8116
+ schema: (await loadSecretList({})).schema,
8117
+ withFiles: options.with ?? [],
8118
+ out: options.out
8119
+ };
8120
+ const report2 = await exportEnvFile(request2).catch((error) => {
8121
+ logger.error(error instanceof Error ? error.message : String(error));
8122
+ return process.exit(1);
8123
+ });
8124
+ printReport2(report2, request2.out);
8125
+ }
8126
+ async function exportEnvFile(request2) {
8127
+ const out = resolve5(request2.cwd, request2.out);
8128
+ assertDirectory(dirname8(out), "--out");
8129
+ const merge = mergeLayers(request2.schema, await readLayers(request2));
8130
+ const report2 = { written: false, origins: merge.origins, errors: merge.errors, warnings: merge.warnings };
8131
+ if (merge.errors.length > 0) {
8132
+ return report2;
8133
+ }
8134
+ writePrivateFileAtomic(out, formatDotenv(merge.values));
8135
+ return { ...report2, written: true };
8136
+ }
8137
+ async function readLayers(request2) {
8138
+ const { cwd, target } = request2;
8139
+ const withFiles = request2.withFiles.map((file) => readWithFile(cwd, file));
8140
+ const sources = [{
8141
+ label: "environment",
8142
+ kind: "environment",
8143
+ values: await sopsDecrypt(getSopsFile(cwd, target.env).absFile)
8144
+ }];
8145
+ if (target.instance) {
8146
+ sources.push({
8147
+ label: "instance",
8148
+ kind: "instance",
8149
+ values: await sopsDecrypt(getSopsFile(cwd, target.env, target.instance).absFile)
8150
+ });
8151
+ }
8152
+ return [...sources, ...withFiles];
8153
+ }
8154
+ function readWithFile(cwd, file) {
8155
+ const path5 = resolve5(cwd, file);
8156
+ if (!existsSync36(path5)) {
8157
+ throw new Error(`--with file not found: ${path5}`);
8158
+ }
8159
+ return { label: `--with ${file}`, kind: "instance", values: parseEnvFile(path5) };
8160
+ }
8161
+ function assertDirectory(path5, option) {
8162
+ if (!existsSync36(path5) || !statSync(path5).isDirectory()) {
8163
+ throw new Error(`${option}: directory ${path5} does not exist. Create it first.`);
8164
+ }
8165
+ }
8166
+ function printReport2(report2, out) {
8167
+ for (const warning of report2.warnings) {
8168
+ logger.warn(warning);
8169
+ }
8170
+ if (!report2.written) {
8171
+ logger.error(`Nothing written \u2014 ${report2.errors.length} problem(s):`);
8172
+ report2.errors.forEach((error) => console.log(` ${chalk44.red("\u2717")} ${error}`));
8173
+ process.exit(1);
8174
+ }
8175
+ for (const [name, layer] of Object.entries(report2.origins)) {
8176
+ console.log(` ${chalk44.cyan(name)} ${chalk44.dim(`\u2190 ${layer}`)}`);
8177
+ }
8178
+ logger.success(`Wrote ${Object.keys(report2.origins).length} name(s) to ${out} (mode 0600).`);
8179
+ }
8180
+
7358
8181
  // src/commands/secret/index.ts
7359
8182
  var ENV_OPTION = ["-e, --env <env>", "Target environment (local | development | staging | production)", "local"];
7360
- var PKG_OPTION = ["-p, --package <package>", "Package whose env schema to read", "@spfn/core"];
8183
+ var INSTANCE_OPTION = ["-i, --instance <name>", "Target one instance of the environment: secrets/<env>.<instance>.enc.json (lowercase letters, digits, dashes)"];
8184
+ var PKG_OPTION = ["-p, --package <package>", "Read only this package's env schema (default: the whole app \u2014 spfn.config.js env.schemas + installed @spfn/* packages)"];
7361
8185
  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);
8186
+ secretCommand.command("set [key]").description("Store a secret value (prompts for the value, masked; --stdin reads it from a pipe)").option(...ENV_OPTION).option(...INSTANCE_OPTION).option("--stdin", "Read the value from stdin (one trailing newline dropped); never prompts").option(...PKG_OPTION).action(secretSet);
8187
+ secretCommand.command("list").description("List declared secrets and their status (never prints values)").option(...ENV_OPTION).option(...INSTANCE_OPTION).option(...PKG_OPTION).action(secretList);
8188
+ secretCommand.command("generate [key]").description("Generate value(s) for schema secrets that declare a generate strategy").option("-a, --all", "Generate every generatable secret").option(...ENV_OPTION).option(...INSTANCE_OPTION).option(...PKG_OPTION).action(secretGenerate);
8189
+ secretCommand.command("rotate [key]").description("Rotate secret value(s); external secrets are flagged for manual reissue").option("-a, --all", "Rotate every secret").option(...ENV_OPTION).option(...INSTANCE_OPTION).option(...PKG_OPTION).action(secretRotate);
7366
8190
  secretCommand.command("keygen").description("Generate an age key pair for SOPS (no-cloud backend)").action(secretKeygen);
7367
8191
  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);
8192
+ secretCommand.command("check").description("Static hygiene lint \u2014 flag plaintext secret leaks; with a deployed --env, names in the wrong layer").option(...ENV_OPTION).option(...INSTANCE_OPTION).option(...PKG_OPTION).action(secretCheck);
8193
+ secretCommand.command("export").description("Decrypt and merge a deployment's layers into a 0600 dotenv file (prints names only)").requiredOption("-e, --env <env>", "Deployed environment (development | staging | production | test)").option(...INSTANCE_OPTION).option("-w, --with <file>", "Plaintext dotenv file of computed instance values (repeatable)", collect, []).option("-o, --out <file>", "Dotenv file to write (required; its directory must exist)").action(secretExport);
8194
+ function collect(value, previous) {
8195
+ return [...previous, value];
8196
+ }
7369
8197
 
7370
8198
  // src/commands/cloud/index.ts
7371
8199
  import { Command as Command17 } from "commander";
7372
8200
 
7373
8201
  // src/commands/cloud/link.ts
7374
8202
  init_logger();
7375
- import { basename as basename2 } from "path";
8203
+ import { basename as basename3 } from "path";
7376
8204
  import prompts12 from "prompts";
7377
- import chalk44 from "chalk";
8205
+ import chalk45 from "chalk";
7378
8206
 
7379
8207
  // src/utils/cloud/tokens.ts
7380
8208
  var TOKEN_KEYS = {
@@ -7412,20 +8240,20 @@ async function deleteCloudToken(provider) {
7412
8240
 
7413
8241
  // src/utils/cloud/config.ts
7414
8242
  init_env_file();
7415
- import { existsSync as existsSync33, mkdirSync as mkdirSync7, readFileSync as readFileSync16, writeFileSync as writeFileSync21 } from "fs";
7416
- import { dirname as dirname7, join as join34 } from "path";
7417
- var CONFIG_REL_PATH = join34(".spfn", "cloud.json");
8243
+ import { existsSync as existsSync37, mkdirSync as mkdirSync7, readFileSync as readFileSync19, writeFileSync as writeFileSync22 } from "fs";
8244
+ import { dirname as dirname9, join as join40 } from "path";
8245
+ var CONFIG_REL_PATH = join40(".spfn", "cloud.json");
7418
8246
  function cloudConfigPath(cwd) {
7419
- return join34(cwd, CONFIG_REL_PATH);
8247
+ return join40(cwd, CONFIG_REL_PATH);
7420
8248
  }
7421
8249
  function readCloudConfig(cwd) {
7422
8250
  const path5 = cloudConfigPath(cwd);
7423
- if (!existsSync33(path5)) {
8251
+ if (!existsSync37(path5)) {
7424
8252
  return {};
7425
8253
  }
7426
8254
  let parsed;
7427
8255
  try {
7428
- parsed = JSON.parse(readFileSync16(path5, "utf-8"));
8256
+ parsed = JSON.parse(readFileSync19(path5, "utf-8"));
7429
8257
  } catch {
7430
8258
  throw new Error(`${path5} is not valid JSON \u2014 delete it and run \`spfn cloud link\` again.`);
7431
8259
  }
@@ -7436,8 +8264,8 @@ function readCloudConfig(cwd) {
7436
8264
  }
7437
8265
  function writeCloudConfig(cwd, config) {
7438
8266
  const path5 = cloudConfigPath(cwd);
7439
- mkdirSync7(dirname7(path5), { recursive: true });
7440
- writeFileSync21(path5, JSON.stringify(config, null, 4) + "\n", "utf-8");
8267
+ mkdirSync7(dirname9(path5), { recursive: true });
8268
+ writeFileSync22(path5, JSON.stringify(config, null, 4) + "\n", "utf-8");
7441
8269
  ensureGitignored(cwd, [{ pattern: ".spfn/cloud.json", comment: "spfn cloud link state (per-user accounts)" }]);
7442
8270
  }
7443
8271
  function requireLinked(config, provider) {
@@ -7666,7 +8494,7 @@ async function cloudLink(options) {
7666
8494
  logger.error(error instanceof Error ? error.message : String(error));
7667
8495
  process.exit(1);
7668
8496
  }
7669
- logger.success(`Link state saved to ${chalk44.cyan(cloudConfigPath(cwd))} (identifiers only \u2014 tokens stay in the keychain).`);
8497
+ logger.success(`Link state saved to ${chalk45.cyan(cloudConfigPath(cwd))} (identifiers only \u2014 tokens stay in the keychain).`);
7670
8498
  logger.info("Next: `spfn cloud status` for limits vs usage, `spfn cloud env pull` for project keys.");
7671
8499
  }
7672
8500
  async function linkVercel(config, cwd, options) {
@@ -7686,7 +8514,7 @@ async function linkVercel(config, cwd, options) {
7686
8514
  throw new Error(`No Vercel project named "${options.vercelProject}" \u2014 available: ${projects.map((p) => p.name).join(", ")}.`);
7687
8515
  }
7688
8516
  } else {
7689
- const defaultIndex = Math.max(0, projects.findIndex((p) => p.name === basename2(cwd)));
8517
+ const defaultIndex = Math.max(0, projects.findIndex((p) => p.name === basename3(cwd)));
7690
8518
  ({ projectId } = await prompts12({
7691
8519
  type: "select",
7692
8520
  name: "projectId",
@@ -7758,7 +8586,7 @@ async function obtainToken(provider, label, envVar, createUrl, replace) {
7758
8586
  return fromKeychain;
7759
8587
  }
7760
8588
  }
7761
- logger.info(`Create one at ${chalk44.cyan(createUrl)} if you don't have it yet.`);
8589
+ logger.info(`Create one at ${chalk45.cyan(createUrl)} if you don't have it yet.`);
7762
8590
  const { token } = await prompts12({
7763
8591
  type: "password",
7764
8592
  name: "token",
@@ -7771,7 +8599,7 @@ async function obtainToken(provider, label, envVar, createUrl, replace) {
7771
8599
  }
7772
8600
 
7773
8601
  // src/commands/cloud/limits.ts
7774
- import chalk46 from "chalk";
8602
+ import chalk47 from "chalk";
7775
8603
 
7776
8604
  // src/utils/cloud/limits-data.ts
7777
8605
  var VERCEL_HOBBY_LIMITS = [
@@ -7810,21 +8638,21 @@ var SUPABASE_FREE_NOTES = [
7810
8638
  var LIMITS_VERIFIED_ON = "2026-08-13";
7811
8639
 
7812
8640
  // src/utils/cloud/format.ts
7813
- import chalk45 from "chalk";
8641
+ import chalk46 from "chalk";
7814
8642
  var MIGRATION_PROMPT_THRESHOLD = 0.8;
7815
8643
  function formatPercent(used, limit) {
7816
8644
  if (limit <= 0) {
7817
- return chalk45.dim("n/a");
8645
+ return chalk46.dim("n/a");
7818
8646
  }
7819
8647
  const ratio = used / limit;
7820
8648
  const text = `${(ratio * 100).toFixed(1)}%`;
7821
8649
  if (ratio >= 1) {
7822
- return chalk45.red.bold(text);
8650
+ return chalk46.red.bold(text);
7823
8651
  }
7824
8652
  if (ratio >= MIGRATION_PROMPT_THRESHOLD) {
7825
- return chalk45.yellow.bold(text);
8653
+ return chalk46.yellow.bold(text);
7826
8654
  }
7827
- return chalk45.green(text);
8655
+ return chalk46.green(text);
7828
8656
  }
7829
8657
  function isNearLimit(used, limit) {
7830
8658
  return limit > 0 && used / limit >= MIGRATION_PROMPT_THRESHOLD;
@@ -7861,25 +8689,25 @@ var PER_LABEL = {
7861
8689
  async function cloudLimits() {
7862
8690
  printPlan("Vercel Hobby", VERCEL_HOBBY_LIMITS, VERCEL_HOBBY_NOTES);
7863
8691
  printPlan("Supabase Free", SUPABASE_FREE_LIMITS, SUPABASE_FREE_NOTES);
7864
- console.log(chalk46.dim(`Numbers verified against the official docs on ${LIMITS_VERIFIED_ON}.`));
7865
- console.log(chalk46.dim("Live usage against these limits: `spfn cloud status`.\n"));
8692
+ console.log(chalk47.dim(`Numbers verified against the official docs on ${LIMITS_VERIFIED_ON}.`));
8693
+ console.log(chalk47.dim("Live usage against these limits: `spfn cloud status`.\n"));
7866
8694
  }
7867
8695
  function printPlan(title, limits, notes) {
7868
- console.log(chalk46.bold(`
8696
+ console.log(chalk47.bold(`
7869
8697
  ${title}`));
7870
8698
  for (const limit of limits) {
7871
8699
  const amount = `${formatCount(limit.limit)} ${limit.unit}`;
7872
- console.log(` ${limit.label.padEnd(34)} ${amount.padEnd(26)} ${chalk46.dim(PER_LABEL[limit.per])}`);
8700
+ console.log(` ${limit.label.padEnd(34)} ${amount.padEnd(26)} ${chalk47.dim(PER_LABEL[limit.per])}`);
7873
8701
  }
7874
8702
  for (const note of notes) {
7875
- console.log(chalk46.dim(` \u2022 ${note}`));
8703
+ console.log(chalk47.dim(` \u2022 ${note}`));
7876
8704
  }
7877
8705
  console.log("");
7878
8706
  }
7879
8707
 
7880
8708
  // src/commands/cloud/usage.ts
7881
8709
  init_logger();
7882
- import chalk47 from "chalk";
8710
+ import chalk48 from "chalk";
7883
8711
 
7884
8712
  // src/commands/cloud/collect.ts
7885
8713
  async function collectSnapshot(cwd) {
@@ -7972,10 +8800,10 @@ function printVercel(snapshot) {
7972
8800
  if (!snapshot.vercel) {
7973
8801
  return;
7974
8802
  }
7975
- console.log(chalk47.bold(`
8803
+ console.log(chalk48.bold(`
7976
8804
  Vercel \u2014 account-wide usage, rolling 30 days (linked project: ${snapshot.vercel.projectName})`));
7977
8805
  if (snapshot.vercel.services.length === 0) {
7978
- console.log(chalk47.dim(" No usage recorded in the billing feed yet."));
8806
+ console.log(chalk48.dim(" No usage recorded in the billing feed yet."));
7979
8807
  }
7980
8808
  for (const service of snapshot.vercel.services) {
7981
8809
  const amount = `${formatCount(service.consumed)} ${service.unit}`.trim();
@@ -7987,12 +8815,12 @@ function printSupabase(snapshot) {
7987
8815
  return;
7988
8816
  }
7989
8817
  const { projectName, status, dbSizeBytes, dailyApiCount } = snapshot.supabase;
7990
- console.log(chalk47.bold(`
8818
+ console.log(chalk48.bold(`
7991
8819
  Supabase \u2014 ${projectName}`));
7992
8820
  console.log(` ${"Project status".padEnd(34)} ${status}`);
7993
- console.log(` ${"Database size".padEnd(34)} ${dbSizeBytes === null ? chalk47.dim("unavailable") : formatBytes2(dbSizeBytes)}`);
7994
- console.log(` ${"API requests (last 24h)".padEnd(34)} ${dailyApiCount === null ? chalk47.dim("unavailable") : formatCount(dailyApiCount)}`);
7995
- console.log(chalk47.dim(" Org totals (egress, MAU) have no public API \u2014 exact numbers: supabase.com/dashboard \u2192 Usage."));
8821
+ console.log(` ${"Database size".padEnd(34)} ${dbSizeBytes === null ? chalk48.dim("unavailable") : formatBytes2(dbSizeBytes)}`);
8822
+ console.log(` ${"API requests (last 24h)".padEnd(34)} ${dailyApiCount === null ? chalk48.dim("unavailable") : formatCount(dailyApiCount)}`);
8823
+ console.log(chalk48.dim(" Org totals (egress, MAU) have no public API \u2014 exact numbers: supabase.com/dashboard \u2192 Usage."));
7996
8824
  }
7997
8825
  function printProblems(snapshot) {
7998
8826
  console.log("");
@@ -8006,7 +8834,7 @@ function printProblems(snapshot) {
8006
8834
 
8007
8835
  // src/commands/cloud/status.ts
8008
8836
  init_logger();
8009
- import chalk48 from "chalk";
8837
+ import chalk49 from "chalk";
8010
8838
  async function cloudStatus() {
8011
8839
  const snapshot = await collectSnapshot(process.cwd());
8012
8840
  const nearLimit = [];
@@ -8025,10 +8853,10 @@ function printVercel2(snapshot, nearLimit) {
8025
8853
  if (!snapshot.vercel) {
8026
8854
  return;
8027
8855
  }
8028
- console.log(chalk48.bold(`
8856
+ console.log(chalk49.bold(`
8029
8857
  Vercel Hobby \u2014 account-wide usage, rolling 30 days (linked project: ${snapshot.vercel.projectName}; limits as of ${LIMITS_VERIFIED_ON})`));
8030
8858
  if (snapshot.vercel.services.length === 0) {
8031
- console.log(chalk48.dim(" No usage recorded in the billing feed yet."));
8859
+ console.log(chalk49.dim(" No usage recorded in the billing feed yet."));
8032
8860
  }
8033
8861
  for (const service of snapshot.vercel.services) {
8034
8862
  const limit = matchVercelLimit(service.serviceName);
@@ -8047,12 +8875,12 @@ function printSupabase2(snapshot, nearLimit) {
8047
8875
  return;
8048
8876
  }
8049
8877
  const { projectName, status, dbSizeBytes, dailyApiCount } = snapshot.supabase;
8050
- console.log(chalk48.bold(`
8878
+ console.log(chalk49.bold(`
8051
8879
  Supabase Free \u2014 ${projectName}`));
8052
8880
  if (isSupabasePaused(status)) {
8053
- console.log(` ${"Project status".padEnd(34)} ${chalk48.red.bold(status)} \u2014 run \`spfn cloud keepalive\` after restoring it`);
8881
+ console.log(` ${"Project status".padEnd(34)} ${chalk49.red.bold(status)} \u2014 run \`spfn cloud keepalive\` after restoring it`);
8054
8882
  } else {
8055
- console.log(` ${"Project status".padEnd(34)} ${chalk48.green(status)}`);
8883
+ console.log(` ${"Project status".padEnd(34)} ${chalk49.green(status)}`);
8056
8884
  }
8057
8885
  if (dbSizeBytes !== null) {
8058
8886
  const dbLimit = SUPABASE_FREE_LIMITS.find((l) => l.key === "db-size");
@@ -8062,25 +8890,25 @@ Supabase Free \u2014 ${projectName}`));
8062
8890
  nearLimit.push("Supabase database size");
8063
8891
  }
8064
8892
  } else {
8065
- console.log(` ${"Database size".padEnd(34)} ${chalk48.dim("unavailable")}`);
8893
+ console.log(` ${"Database size".padEnd(34)} ${chalk49.dim("unavailable")}`);
8066
8894
  }
8067
- console.log(` ${"API requests (last 24h)".padEnd(34)} ${dailyApiCount === null ? chalk48.dim("unavailable") : formatCount(dailyApiCount)}`);
8068
- console.log(chalk48.dim(` DB size ${dbSizeBytes === null ? "unknown" : formatBytes2(dbSizeBytes)} counts per project; egress/MAU are org totals \u2014 exact numbers on the dashboard.`));
8895
+ console.log(` ${"API requests (last 24h)".padEnd(34)} ${dailyApiCount === null ? chalk49.dim("unavailable") : formatCount(dailyApiCount)}`);
8896
+ console.log(chalk49.dim(` DB size ${dbSizeBytes === null ? "unknown" : formatBytes2(dbSizeBytes)} counts per project; egress/MAU are org totals \u2014 exact numbers on the dashboard.`));
8069
8897
  }
8070
8898
  function printMigrationPrompt(nearLimit) {
8071
8899
  if (nearLimit.length === 0) {
8072
8900
  return;
8073
8901
  }
8074
- console.log(chalk48.yellow.bold(`\u26A0 Approaching free-tier limits: ${nearLimit.join(", ")}.`));
8075
- console.log(chalk48.yellow(" When a Vercel Hobby limit is hit, that capability pauses; Supabase over quota restricts the whole org."));
8076
- console.log(chalk48.yellow(" Migrating to your own cloud removes these ceilings \u2014 `spfn cloud migrate` (coming soon).\n"));
8902
+ console.log(chalk49.yellow.bold(`\u26A0 Approaching free-tier limits: ${nearLimit.join(", ")}.`));
8903
+ console.log(chalk49.yellow(" When a Vercel Hobby limit is hit, that capability pauses; Supabase over quota restricts the whole org."));
8904
+ console.log(chalk49.yellow(" Migrating to your own cloud removes these ceilings \u2014 `spfn cloud migrate` (coming soon).\n"));
8077
8905
  }
8078
8906
 
8079
8907
  // src/commands/cloud/keepalive.ts
8080
8908
  init_logger();
8081
- import { existsSync as existsSync34, mkdirSync as mkdirSync8, readFileSync as readFileSync17, writeFileSync as writeFileSync22 } from "fs";
8082
- import { dirname as dirname8, join as join35 } from "path";
8083
- import chalk49 from "chalk";
8909
+ import { existsSync as existsSync38, mkdirSync as mkdirSync8, readFileSync as readFileSync20, writeFileSync as writeFileSync23 } from "fs";
8910
+ import { dirname as dirname10, join as join41 } from "path";
8911
+ import chalk50 from "chalk";
8084
8912
  var DEFAULT_HEALTH_PATH = "/api/backend/_core/health?detailed=true";
8085
8913
  var DEFAULT_SCHEDULE = "0 3 * * *";
8086
8914
  async function cloudKeepalive(options) {
@@ -8105,12 +8933,12 @@ async function cloudKeepalive(options) {
8105
8933
  logger.info("The DB ping needs detailed health checks: keep `healthCheck: { detailed: true }` in your server config.");
8106
8934
  }
8107
8935
  function setupVercelCron(cwd, healthPath) {
8108
- const vercelJsonPath = join35(cwd, "vercel.json");
8936
+ const vercelJsonPath = join41(cwd, "vercel.json");
8109
8937
  let vercelJson = {};
8110
- if (existsSync34(vercelJsonPath)) {
8938
+ if (existsSync38(vercelJsonPath)) {
8111
8939
  let parsed;
8112
8940
  try {
8113
- parsed = JSON.parse(readFileSync17(vercelJsonPath, "utf-8"));
8941
+ parsed = JSON.parse(readFileSync20(vercelJsonPath, "utf-8"));
8114
8942
  } catch {
8115
8943
  logger.error("vercel.json is not valid JSON (comments and trailing commas are not allowed) \u2014 fix it and re-run.");
8116
8944
  process.exit(1);
@@ -8128,10 +8956,10 @@ function setupVercelCron(cwd, healthPath) {
8128
8956
  }
8129
8957
  if (result === "occupied") {
8130
8958
  logger.error("vercel.json already has a cron job, and Vercel Hobby allows only one.");
8131
- logger.info(`Point the existing cron at ${chalk49.cyan(healthPath)} yourself, or use \`--github-actions --url <deployed-url>\`.`);
8959
+ logger.info(`Point the existing cron at ${chalk50.cyan(healthPath)} yourself, or use \`--github-actions --url <deployed-url>\`.`);
8132
8960
  process.exit(1);
8133
8961
  }
8134
- writeFileSync22(vercelJsonPath, JSON.stringify(vercelJson, null, 4) + "\n", "utf-8");
8962
+ writeFileSync23(vercelJsonPath, JSON.stringify(vercelJson, null, 4) + "\n", "utf-8");
8135
8963
  logger.success(`vercel.json: daily cron added (${DEFAULT_SCHEDULE} UTC \u2192 ${healthPath}). Deploy to activate.`);
8136
8964
  warnIfBackendRouteMissing(cwd);
8137
8965
  }
@@ -8151,8 +8979,8 @@ function setupGithubActions(cwd, healthPath, url) {
8151
8979
  logger.error("`--github-actions` needs `--url <deployed-url>` \u2014 the workflow runs outside your app and must know where it lives.");
8152
8980
  process.exit(1);
8153
8981
  }
8154
- const workflowPath = join35(cwd, ".github", "workflows", "spfn-keepalive.yml");
8155
- if (existsSync34(workflowPath)) {
8982
+ const workflowPath = join41(cwd, ".github", "workflows", "spfn-keepalive.yml");
8983
+ if (existsSync38(workflowPath)) {
8156
8984
  logger.success(`${workflowPath} already exists \u2014 nothing to do.`);
8157
8985
  return;
8158
8986
  }
@@ -8161,8 +8989,8 @@ function setupGithubActions(cwd, healthPath, url) {
8161
8989
  logger.error(`--url "${url}" is not a valid URL. Pass the deployed app origin, e.g. https://myapp.vercel.app.`);
8162
8990
  process.exit(1);
8163
8991
  }
8164
- mkdirSync8(dirname8(workflowPath), { recursive: true });
8165
- writeFileSync22(workflowPath, githubWorkflow(new URL(healthPath, base).toString()), "utf-8");
8992
+ mkdirSync8(dirname10(workflowPath), { recursive: true });
8993
+ writeFileSync23(workflowPath, githubWorkflow(new URL(healthPath, base).toString()), "utf-8");
8166
8994
  logger.success(".github/workflows/spfn-keepalive.yml: daily keep-alive workflow added. Commit and push to activate.");
8167
8995
  }
8168
8996
  function normalizeDeployUrl(url) {
@@ -8199,8 +9027,8 @@ jobs:
8199
9027
  `;
8200
9028
  }
8201
9029
  function warnIfBackendRouteMissing(cwd) {
8202
- const routePath = join35(cwd, "src", "app", "api", "backend", "[[...route]]", "route.ts");
8203
- if (!existsSync34(routePath)) {
9030
+ const routePath = join41(cwd, "src", "app", "api", "backend", "[[...route]]", "route.ts");
9031
+ if (!existsSync38(routePath)) {
8204
9032
  logger.warn("src/app/api/backend/[[...route]]/route.ts not found \u2014 the cron path answers only after `spfn add vercel`.");
8205
9033
  }
8206
9034
  }
@@ -8208,9 +9036,9 @@ function warnIfBackendRouteMissing(cwd) {
8208
9036
  // src/commands/cloud/env.ts
8209
9037
  init_logger();
8210
9038
  init_env_file();
8211
- import { join as join36 } from "path";
9039
+ import { join as join42 } from "path";
8212
9040
  import prompts13 from "prompts";
8213
- import chalk50 from "chalk";
9041
+ import chalk51 from "chalk";
8214
9042
  async function cloudEnvPull(options) {
8215
9043
  const cwd = process.cwd();
8216
9044
  try {
@@ -8229,7 +9057,7 @@ async function cloudEnvPull(options) {
8229
9057
  logger.info("Values were written to files/keychain only \u2014 none were displayed.");
8230
9058
  }
8231
9059
  function writePublicValues(cwd, projectRef2, keys) {
8232
- const envLocalPath = join36(cwd, ".env.local");
9060
+ const envLocalPath = join42(cwd, ".env.local");
8233
9061
  ensureGitignored(cwd, [{ pattern: ".env.local", comment: "local env (per-machine values)" }]);
8234
9062
  upsertEnvVar(envLocalPath, "NEXT_PUBLIC_SUPABASE_URL", `https://${projectRef2}.supabase.co`);
8235
9063
  logger.success("NEXT_PUBLIC_SUPABASE_URL \u2192 .env.local");
@@ -8247,7 +9075,7 @@ async function storeServiceKey(cwd, keys) {
8247
9075
  logger.warn("No service_role/secret key found on the project \u2014 skipped SUPABASE_SERVICE_ROLE_KEY.");
8248
9076
  return;
8249
9077
  }
8250
- await storeSecret(cwd, "local", "SUPABASE_SERVICE_ROLE_KEY", service.api_key);
9078
+ await storeSecret(cwd, { env: "local" }, "SUPABASE_SERVICE_ROLE_KEY", service.api_key);
8251
9079
  }
8252
9080
  function findKey(keys, candidates) {
8253
9081
  return keys.find((key) => candidates.includes(key.name)) ?? keys.find((key) => key.type !== void 0 && candidates.includes(key.type));
@@ -8268,7 +9096,7 @@ async function storeDatabaseUrl(cwd, token, projectRef2) {
8268
9096
  return;
8269
9097
  }
8270
9098
  const url = sessionPoolerUrl(template, password);
8271
- await storeSecret(cwd, "local", "DATABASE_URL", url);
9099
+ await storeSecret(cwd, { env: "local" }, "DATABASE_URL", url);
8272
9100
  logger.info("This is the session-mode pooler connection (IPv4-safe, works for migrations). Serverless runtime can use the transaction pooler (port 6543) instead.");
8273
9101
  }
8274
9102
  async function poolerTemplate(token, projectRef2) {
@@ -8312,7 +9140,7 @@ async function cloudEnvPush(keys) {
8312
9140
  logger.error(`${failure.key}: rejected by Vercel \u2014 ${failure.message}`);
8313
9141
  }
8314
9142
  for (const envVar of vars.filter((v) => !failedKeys.has(v.key))) {
8315
- logger.success(`${envVar.key} \u2192 Vercel ${chalk50.cyan(linked.projectName)} (${envVar.type}, ${envVar.target.join("+")})`);
9143
+ logger.success(`${envVar.key} \u2192 Vercel ${chalk51.cyan(linked.projectName)} (${envVar.type}, ${envVar.target.join("+")})`);
8316
9144
  }
8317
9145
  if (result.failed.length > 0) {
8318
9146
  process.exit(1);
@@ -8323,8 +9151,8 @@ async function cloudEnvPush(keys) {
8323
9151
  }
8324
9152
  }
8325
9153
  async function resolveLocalValues(cwd, keys) {
8326
- const publicEnv = { ...parseEnvFile(join36(cwd, ".env")), ...parseEnvFile(join36(cwd, ".env.local")) };
8327
- const serverEnv = parseEnvFile(join36(cwd, ".env.server"));
9154
+ const publicEnv = { ...parseEnvFile(join42(cwd, ".env")), ...parseEnvFile(join42(cwd, ".env.local")) };
9155
+ const serverEnv = parseEnvFile(join42(cwd, ".env.server"));
8328
9156
  const keychain = await resolveKeychainEnv(cwd);
8329
9157
  const vars = [];
8330
9158
  const missing = [];
@@ -8362,9 +9190,9 @@ envGroup.command("push").description("Push local env values to the Vercel projec
8362
9190
 
8363
9191
  // src/commands/kit/index.ts
8364
9192
  import { Command as Command18 } from "commander";
8365
- import chalk52 from "chalk";
9193
+ import chalk53 from "chalk";
8366
9194
  import prompts14 from "prompts";
8367
- import { resolve as resolve3 } from "path";
9195
+ import { resolve as resolve6 } from "path";
8368
9196
 
8369
9197
  // src/kit/errors.ts
8370
9198
  var KIT_EXIT = {
@@ -8480,11 +9308,11 @@ async function resolveKitAdapters(request2) {
8480
9308
  // src/kit/operations/install.ts
8481
9309
  import { createHash as createHash2 } from "crypto";
8482
9310
  import { hostname as hostname2, tmpdir } from "os";
8483
- import { mkdtempSync, existsSync as existsSync46 } from "fs";
8484
- import { join as join46 } from "path";
9311
+ import { mkdtempSync, existsSync as existsSync50 } from "fs";
9312
+ import { join as join52 } from "path";
8485
9313
 
8486
9314
  // src/kit/events.ts
8487
- import chalk51 from "chalk";
9315
+ import chalk52 from "chalk";
8488
9316
 
8489
9317
  // src/kit/secret-scan.ts
8490
9318
  var SECRET_SHAPES = [
@@ -8536,30 +9364,30 @@ function redactEvent(event, knownSecrets) {
8536
9364
  function humanLine(event) {
8537
9365
  switch (event.status) {
8538
9366
  case "completed":
8539
- return `${chalk51.green("\u2713")} ${event.summary}`;
9367
+ return `${chalk52.green("\u2713")} ${event.summary}`;
8540
9368
  case "waiting":
8541
- return `${chalk51.yellow("\u2026")} ${event.summary}${nextLine(event)}`;
9369
+ return `${chalk52.yellow("\u2026")} ${event.summary}${nextLine(event)}`;
8542
9370
  case "failed":
8543
- return `${chalk51.red("\u2717")} ${event.summary}
8544
- ${chalk51.gray(event.code)}${nextLine(event)}`;
9371
+ return `${chalk52.red("\u2717")} ${event.summary}
9372
+ ${chalk52.gray(event.code)}${nextLine(event)}`;
8545
9373
  case "started":
8546
- return chalk51.gray(`\u2192 ${event.summary}`);
9374
+ return chalk52.gray(`\u2192 ${event.summary}`);
8547
9375
  default:
8548
- return chalk51.gray(` ${event.summary}`);
9376
+ return chalk52.gray(` ${event.summary}`);
8549
9377
  }
8550
9378
  }
8551
9379
  function nextLine(event) {
8552
9380
  if (!event.next) {
8553
9381
  return "";
8554
9382
  }
8555
- const approval = event.next.requiresHumanApproval ? chalk51.yellow(" (needs your approval)") : "";
9383
+ const approval = event.next.requiresHumanApproval ? chalk52.yellow(" (needs your approval)") : "";
8556
9384
  return `
8557
- ${chalk51.gray("next:")} ${event.next.command}${approval}`;
9385
+ ${chalk52.gray("next:")} ${event.next.command}${approval}`;
8558
9386
  }
8559
9387
 
8560
9388
  // src/kit/journal.ts
8561
- import { mkdirSync as mkdirSync10, readdirSync as readdirSync3, readFileSync as readFileSync18, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync24, existsSync as existsSync36, statSync } from "fs";
8562
- import { dirname as dirname9, join as join38 } from "path";
9389
+ import { mkdirSync as mkdirSync10, readdirSync as readdirSync4, readFileSync as readFileSync21, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync25, existsSync as existsSync40, statSync as statSync2 } from "fs";
9390
+ import { dirname as dirname11, join as join44 } from "path";
8563
9391
 
8564
9392
  // src/kit/digest.ts
8565
9393
  import { createHash } from "crypto";
@@ -8600,8 +9428,8 @@ function digestOfJson(value) {
8600
9428
  }
8601
9429
 
8602
9430
  // src/kit/paths.ts
8603
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync23, existsSync as existsSync35 } from "fs";
8604
- import { join as join37 } from "path";
9431
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync24, existsSync as existsSync39 } from "fs";
9432
+ import { join as join43 } from "path";
8605
9433
  var KIT_STATE_DIR = ".spfn";
8606
9434
  var OPERATIONS_IGNORE = [
8607
9435
  "# Written by `spfn kit`. One machine's run is not part of the project.",
@@ -8609,27 +9437,27 @@ var OPERATIONS_IGNORE = [
8609
9437
  ""
8610
9438
  ].join("\n");
8611
9439
  function kitPaths(root) {
8612
- const stateDir = join37(root, KIT_STATE_DIR);
8613
- const operationsDir = join37(stateDir, "operations");
8614
- const lockDir = join37(operationsDir, "operation.lock");
9440
+ const stateDir = join43(root, KIT_STATE_DIR);
9441
+ const operationsDir = join43(stateDir, "operations");
9442
+ const lockDir = join43(operationsDir, "operation.lock");
8615
9443
  return {
8616
9444
  root,
8617
9445
  stateDir,
8618
- licenseFile: join37(stateDir, "license.json"),
8619
- lockFile: join37(stateDir, "kit-lock.json"),
9446
+ licenseFile: join43(stateDir, "license.json"),
9447
+ lockFile: join43(stateDir, "kit-lock.json"),
8620
9448
  operationsDir,
8621
- activeJournal: join37(operationsDir, "active.json"),
8622
- historyDir: join37(operationsDir, "history"),
9449
+ activeJournal: join43(operationsDir, "active.json"),
9450
+ historyDir: join43(operationsDir, "history"),
8623
9451
  lockDir,
8624
- lockOwnerFile: join37(lockDir, "owner.json")
9452
+ lockOwnerFile: join43(lockDir, "owner.json")
8625
9453
  };
8626
9454
  }
8627
9455
  function ensureOperationsDir(root) {
8628
9456
  const paths = kitPaths(root);
8629
- const ignoreFile = join37(paths.stateDir, ".gitignore");
9457
+ const ignoreFile = join43(paths.stateDir, ".gitignore");
8630
9458
  mkdirSync9(paths.stateDir, { recursive: true });
8631
- if (!existsSync35(ignoreFile)) {
8632
- writeFileSync23(ignoreFile, OPERATIONS_IGNORE, "utf8");
9459
+ if (!existsSync39(ignoreFile)) {
9460
+ writeFileSync24(ignoreFile, OPERATIONS_IGNORE, "utf8");
8633
9461
  }
8634
9462
  mkdirSync9(paths.operationsDir, { recursive: true });
8635
9463
  return paths.operationsDir;
@@ -8916,14 +9744,14 @@ var JournalStore = class {
8916
9744
  return readJournalFile(this.paths.activeJournal);
8917
9745
  }
8918
9746
  readHistory(operationId) {
8919
- return readJournalFile(join38(this.paths.historyDir, `${operationId}.json`));
9747
+ return readJournalFile(join44(this.paths.historyDir, `${operationId}.json`));
8920
9748
  }
8921
9749
  /** Every journal this project still has, newest update first. */
8922
9750
  listHistory() {
8923
- if (!existsSync36(this.paths.historyDir)) {
9751
+ if (!existsSync40(this.paths.historyDir)) {
8924
9752
  return [];
8925
9753
  }
8926
- return readdirSync3(this.paths.historyDir).filter((name) => name.endsWith(".json")).map((name) => readJournalFile(join38(this.paths.historyDir, name))).filter((journal) => journal !== null).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
9754
+ return readdirSync4(this.paths.historyDir).filter((name) => name.endsWith(".json")).map((name) => readJournalFile(join44(this.paths.historyDir, name))).filter((journal) => journal !== null).sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
8927
9755
  }
8928
9756
  create(init) {
8929
9757
  const timestamp = this.now();
@@ -8982,17 +9810,17 @@ var JournalStore = class {
8982
9810
  }
8983
9811
  /** Move a finished operation into history and clear the active slot. */
8984
9812
  archive(journal) {
8985
- writeJsonAtomically(join38(this.paths.historyDir, `${journal.operationId}.json`), journal);
8986
- rmSync2(this.paths.activeJournal, { force: true });
9813
+ writeJsonAtomically(join44(this.paths.historyDir, `${journal.operationId}.json`), journal);
9814
+ rmSync3(this.paths.activeJournal, { force: true });
8987
9815
  this.pruneHistory();
8988
9816
  }
8989
9817
  pruneHistory() {
8990
- if (!existsSync36(this.paths.historyDir)) {
9818
+ if (!existsSync40(this.paths.historyDir)) {
8991
9819
  return;
8992
9820
  }
8993
- const files = readdirSync3(this.paths.historyDir).filter((name) => name.endsWith(".json")).map((name) => join38(this.paths.historyDir, name)).sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs);
9821
+ const files = readdirSync4(this.paths.historyDir).filter((name) => name.endsWith(".json")).map((name) => join44(this.paths.historyDir, name)).sort((left, right) => statSync2(right).mtimeMs - statSync2(left).mtimeMs);
8994
9822
  for (const file of files.slice(this.historyLimit)) {
8995
- rmSync2(file, { force: true });
9823
+ rmSync3(file, { force: true });
8996
9824
  }
8997
9825
  }
8998
9826
  };
@@ -9012,26 +9840,26 @@ function upsertCheckpoint(checkpoints, entry) {
9012
9840
  return next;
9013
9841
  }
9014
9842
  function readJournalFile(file) {
9015
- if (!existsSync36(file)) {
9843
+ if (!existsSync40(file)) {
9016
9844
  return null;
9017
9845
  }
9018
9846
  try {
9019
- const parsed = JSON.parse(readFileSync18(file, "utf8"));
9847
+ const parsed = JSON.parse(readFileSync21(file, "utf8"));
9020
9848
  return validateOperationJournal(parsed).valid ? parsed : null;
9021
9849
  } catch {
9022
9850
  return null;
9023
9851
  }
9024
9852
  }
9025
9853
  function writeJsonAtomically(file, value) {
9026
- mkdirSync10(dirname9(file), { recursive: true });
9854
+ mkdirSync10(dirname11(file), { recursive: true });
9027
9855
  const temporary = `${file}.tmp`;
9028
- writeFileSync24(temporary, `${JSON.stringify(value, null, 4)}
9856
+ writeFileSync25(temporary, `${JSON.stringify(value, null, 4)}
9029
9857
  `, { encoding: "utf8", mode: 384 });
9030
- renameSync2(temporary, file);
9858
+ renameSync3(temporary, file);
9031
9859
  }
9032
9860
 
9033
9861
  // src/kit/lock.ts
9034
- import { mkdirSync as mkdirSync11, readFileSync as readFileSync19, rmSync as rmSync3, writeFileSync as writeFileSync25, existsSync as existsSync37 } from "fs";
9862
+ import { mkdirSync as mkdirSync11, readFileSync as readFileSync22, rmSync as rmSync4, writeFileSync as writeFileSync26, existsSync as existsSync41 } from "fs";
9035
9863
  import { hostname } from "os";
9036
9864
  function defaultIsProcessAlive(pid) {
9037
9865
  try {
@@ -9055,7 +9883,7 @@ function acquireOperationLock(options) {
9055
9883
  return handle(paths, owner, false);
9056
9884
  }
9057
9885
  reconcileStaleLock(options);
9058
- rmSync3(paths.lockDir, { recursive: true, force: true });
9886
+ rmSync4(paths.lockDir, { recursive: true, force: true });
9059
9887
  if (!tryCreateLockDir(paths)) {
9060
9888
  throw new KitError("KIT_OPERATION_ACTIVE", "Another spfn kit operation took the project lock first.", {
9061
9889
  evidence: { lockDir: paths.lockDir },
@@ -9067,11 +9895,11 @@ function acquireOperationLock(options) {
9067
9895
  }
9068
9896
  function readLockOwner(root) {
9069
9897
  const paths = kitPaths(root);
9070
- if (!existsSync37(paths.lockOwnerFile)) {
9898
+ if (!existsSync41(paths.lockOwnerFile)) {
9071
9899
  return null;
9072
9900
  }
9073
9901
  try {
9074
- const parsed = JSON.parse(readFileSync19(paths.lockOwnerFile, "utf8"));
9902
+ const parsed = JSON.parse(readFileSync22(paths.lockOwnerFile, "utf8"));
9075
9903
  return typeof parsed?.pid === "number" && typeof parsed?.operationId === "string" ? parsed : null;
9076
9904
  } catch {
9077
9905
  return null;
@@ -9140,7 +9968,7 @@ function tryCreateLockDir(paths) {
9140
9968
  }
9141
9969
  }
9142
9970
  function writeOwner(paths, owner) {
9143
- writeFileSync25(paths.lockOwnerFile, `${JSON.stringify(owner, null, 4)}
9971
+ writeFileSync26(paths.lockOwnerFile, `${JSON.stringify(owner, null, 4)}
9144
9972
  `, "utf8");
9145
9973
  }
9146
9974
  function handle(paths, owner, reclaimed) {
@@ -9148,24 +9976,24 @@ function handle(paths, owner, reclaimed) {
9148
9976
  owner,
9149
9977
  reclaimed,
9150
9978
  release() {
9151
- rmSync3(paths.lockDir, { recursive: true, force: true });
9979
+ rmSync4(paths.lockDir, { recursive: true, force: true });
9152
9980
  }
9153
9981
  };
9154
9982
  }
9155
9983
 
9156
9984
  // src/kit/operation-context.ts
9157
- import { mkdirSync as mkdirSync12, readFileSync as readFileSync20, renameSync as renameSync3, rmSync as rmSync4, writeFileSync as writeFileSync26, existsSync as existsSync38 } from "fs";
9158
- import { dirname as dirname10, join as join39 } from "path";
9985
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync23, renameSync as renameSync4, rmSync as rmSync5, writeFileSync as writeFileSync27, existsSync as existsSync42 } from "fs";
9986
+ import { dirname as dirname12, join as join45 } from "path";
9159
9987
  function contextFile(root) {
9160
- return join39(kitPaths(root).operationsDir, "context.json");
9988
+ return join45(kitPaths(root).operationsDir, "context.json");
9161
9989
  }
9162
9990
  function readOperationContext(root) {
9163
9991
  const file = contextFile(root);
9164
- if (!existsSync38(file)) {
9992
+ if (!existsSync42(file)) {
9165
9993
  return null;
9166
9994
  }
9167
9995
  try {
9168
- const parsed = JSON.parse(readFileSync20(file, "utf8"));
9996
+ const parsed = JSON.parse(readFileSync23(file, "utf8"));
9169
9997
  return parsed?.schemaVersion === 1 && typeof parsed.operationId === "string" ? parsed : null;
9170
9998
  } catch {
9171
9999
  return null;
@@ -9173,14 +10001,14 @@ function readOperationContext(root) {
9173
10001
  }
9174
10002
  function writeOperationContext(root, context) {
9175
10003
  const file = contextFile(root);
9176
- mkdirSync12(dirname10(file), { recursive: true });
10004
+ mkdirSync12(dirname12(file), { recursive: true });
9177
10005
  const temporary = `${file}.tmp`;
9178
- writeFileSync26(temporary, `${JSON.stringify(context, null, 4)}
10006
+ writeFileSync27(temporary, `${JSON.stringify(context, null, 4)}
9179
10007
  `, "utf8");
9180
- renameSync3(temporary, file);
10008
+ renameSync4(temporary, file);
9181
10009
  }
9182
10010
  function clearOperationContext(root) {
9183
- rmSync4(contextFile(root), { force: true });
10011
+ rmSync5(contextFile(root), { force: true });
9184
10012
  }
9185
10013
 
9186
10014
  // src/kit/plan.ts
@@ -9725,7 +10553,7 @@ async function fetchThroughRedirects(start, fetcher, allowlist) {
9725
10553
  }
9726
10554
 
9727
10555
  // src/kit/credentials.ts
9728
- import { randomBytes as randomBytes3 } from "crypto";
10556
+ import { randomBytes as randomBytes4 } from "crypto";
9729
10557
  var KIT_KEYCHAIN_SERVICE = "superfunction.spfn.kit";
9730
10558
  var KIT_KEYCHAIN_NAMESPACE_ENV = "SPFN_KIT_KEYCHAIN_NAMESPACE";
9731
10559
  var NAMESPACE_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
@@ -9748,7 +10576,7 @@ function pendingAccount(kitId, installationId, localClientId) {
9748
10576
  return `${kitId}:pending:${installationId}:${localClientId}`;
9749
10577
  }
9750
10578
  function newCandidateCredential() {
9751
- return `spfnlc_${randomBytes3(8).toString("hex")}.${randomBytes3(32).toString("base64url")}`;
10579
+ return `spfnlc_${randomBytes4(8).toString("hex")}.${randomBytes4(32).toString("base64url")}`;
9752
10580
  }
9753
10581
  function credentialPublicId(credential) {
9754
10582
  const match = /^spfn[a-z]*_([0-9a-f]{16})\./.exec(credential);
@@ -9810,8 +10638,8 @@ var KeychainKitCredentialStore = class {
9810
10638
  };
9811
10639
 
9812
10640
  // src/kit/tooling.ts
9813
- import { cpSync, mkdirSync as mkdirSync13, readdirSync as readdirSync4, readFileSync as readFileSync21, rmSync as rmSync5, statSync as statSync2, existsSync as existsSync39 } from "fs";
9814
- import { join as join40, relative as relative2, sep as sep3 } from "path";
10641
+ import { cpSync, mkdirSync as mkdirSync13, readdirSync as readdirSync5, readFileSync as readFileSync24, rmSync as rmSync6, statSync as statSync3, existsSync as existsSync43 } from "fs";
10642
+ import { join as join46, relative as relative3, sep as sep3 } from "path";
9815
10643
 
9816
10644
  // src/kit/manifest.ts
9817
10645
  var DEFAULT_AGENT_PACK_ROOT = ".spfn/agent-pack";
@@ -10117,27 +10945,27 @@ function assertSafeRelativePath(path5, refuse) {
10117
10945
  }
10118
10946
  function snapshotTree(root, ignore = ["node_modules", ".git"]) {
10119
10947
  const files = /* @__PURE__ */ new Map();
10120
- const walk = (directory) => {
10121
- for (const entry of readdirSync4(directory, { withFileTypes: true })) {
10948
+ const walk2 = (directory) => {
10949
+ for (const entry of readdirSync5(directory, { withFileTypes: true })) {
10122
10950
  if (ignore.includes(entry.name)) {
10123
10951
  continue;
10124
10952
  }
10125
- const absolute = join40(directory, entry.name);
10953
+ const absolute = join46(directory, entry.name);
10126
10954
  if (entry.isDirectory()) {
10127
- walk(absolute);
10955
+ walk2(absolute);
10128
10956
  continue;
10129
10957
  }
10130
10958
  if (!entry.isFile()) {
10131
10959
  continue;
10132
10960
  }
10133
10961
  files.set(
10134
- relative2(root, absolute).split(sep3).join("/"),
10135
- sha256Digest(readFileSync21(absolute))
10962
+ relative3(root, absolute).split(sep3).join("/"),
10963
+ sha256Digest(readFileSync24(absolute))
10136
10964
  );
10137
10965
  }
10138
10966
  };
10139
- if (existsSync39(root) && statSync2(root).isDirectory()) {
10140
- walk(root);
10967
+ if (existsSync43(root) && statSync3(root).isDirectory()) {
10968
+ walk2(root);
10141
10969
  }
10142
10970
  return { files };
10143
10971
  }
@@ -10163,7 +10991,7 @@ function diffTrees(before, after) {
10163
10991
  }
10164
10992
  var COPY_EXCLUDED = ["node_modules", ".git", ".spfn"];
10165
10993
  async function runIsolated(projectDir2, stagingDir, run2) {
10166
- rmSync5(stagingDir, { recursive: true, force: true });
10994
+ rmSync6(stagingDir, { recursive: true, force: true });
10167
10995
  mkdirSync13(stagingDir, { recursive: true });
10168
10996
  cpSync(projectDir2, stagingDir, {
10169
10997
  recursive: true,
@@ -10175,7 +11003,7 @@ async function runIsolated(projectDir2, stagingDir, run2) {
10175
11003
  const diff = diffTrees(before, snapshotTree(stagingDir));
10176
11004
  return { value, diff };
10177
11005
  } finally {
10178
- rmSync5(stagingDir, { recursive: true, force: true });
11006
+ rmSync6(stagingDir, { recursive: true, force: true });
10179
11007
  }
10180
11008
  }
10181
11009
  function assertWritesWithinAllowlist(diff, allowlist, context) {
@@ -10193,22 +11021,22 @@ function assertWritesWithinAllowlist(diff, allowlist, context) {
10193
11021
  }
10194
11022
 
10195
11023
  // src/kit/agent-pack.ts
10196
- import { existsSync as existsSync40, mkdirSync as mkdirSync14, readFileSync as readFileSync22, writeFileSync as writeFileSync27 } from "fs";
10197
- import { dirname as dirname11, join as join41 } from "path";
11024
+ import { existsSync as existsSync44, mkdirSync as mkdirSync14, readFileSync as readFileSync25, writeFileSync as writeFileSync28 } from "fs";
11025
+ import { dirname as dirname13, join as join47 } from "path";
10198
11026
  var AGENT_PACK_RECORD_PATH = ".spfn/agent-pack.json";
10199
11027
  function writeAgentPackRecord(projectDir2, record) {
10200
- const file = join41(projectDir2, AGENT_PACK_RECORD_PATH);
10201
- mkdirSync14(dirname11(file), { recursive: true });
10202
- writeFileSync27(file, `${JSON.stringify(record, null, 4)}
11028
+ const file = join47(projectDir2, AGENT_PACK_RECORD_PATH);
11029
+ mkdirSync14(dirname13(file), { recursive: true });
11030
+ writeFileSync28(file, `${JSON.stringify(record, null, 4)}
10203
11031
  `, "utf8");
10204
11032
  }
10205
11033
  function readAgentPackRecord(projectDir2) {
10206
- const file = join41(projectDir2, AGENT_PACK_RECORD_PATH);
10207
- if (!existsSync40(file)) {
11034
+ const file = join47(projectDir2, AGENT_PACK_RECORD_PATH);
11035
+ if (!existsSync44(file)) {
10208
11036
  return null;
10209
11037
  }
10210
11038
  try {
10211
- const parsed = JSON.parse(readFileSync22(file, "utf8"));
11039
+ const parsed = JSON.parse(readFileSync25(file, "utf8"));
10212
11040
  return parsed.schemaVersion === 1 && typeof parsed.files === "object" && parsed.files !== null ? parsed : null;
10213
11041
  } catch {
10214
11042
  return null;
@@ -10216,8 +11044,8 @@ function readAgentPackRecord(projectDir2) {
10216
11044
  }
10217
11045
 
10218
11046
  // src/kit/installed-state.ts
10219
- import { mkdirSync as mkdirSync15, readFileSync as readFileSync23, renameSync as renameSync4, writeFileSync as writeFileSync28, existsSync as existsSync41 } from "fs";
10220
- import { dirname as dirname12 } from "path";
11047
+ import { mkdirSync as mkdirSync15, readFileSync as readFileSync26, renameSync as renameSync5, writeFileSync as writeFileSync29, existsSync as existsSync45 } from "fs";
11048
+ import { dirname as dirname14 } from "path";
10221
11049
  function readLicenseFile(file) {
10222
11050
  const parsed = readJson(file);
10223
11051
  if (parsed === null) {
@@ -10259,7 +11087,7 @@ function requireInstalledLock(file) {
10259
11087
  const lock = readInstalledLock(file);
10260
11088
  if (lock === null) {
10261
11089
  throw new KitError("KIT_LOCK_INVALID", "This project has no readable .spfn/kit-lock.json.", {
10262
- evidence: { file, exists: existsSync41(file) },
11090
+ evidence: { file, exists: existsSync45(file) },
10263
11091
  next: { command: "spfn kit recover --json", requiresHumanApproval: false }
10264
11092
  });
10265
11093
  }
@@ -10294,29 +11122,29 @@ function lockFromManifest(manifest, context) {
10294
11122
  };
10295
11123
  }
10296
11124
  function readJson(file) {
10297
- if (!existsSync41(file)) {
11125
+ if (!existsSync45(file)) {
10298
11126
  return null;
10299
11127
  }
10300
11128
  try {
10301
- return JSON.parse(readFileSync23(file, "utf8"));
11129
+ return JSON.parse(readFileSync26(file, "utf8"));
10302
11130
  } catch {
10303
11131
  return null;
10304
11132
  }
10305
11133
  }
10306
11134
  function writeJsonAtomically2(file, value) {
10307
- mkdirSync15(dirname12(file), { recursive: true });
11135
+ mkdirSync15(dirname14(file), { recursive: true });
10308
11136
  const temporary = `${file}.tmp`;
10309
- writeFileSync28(temporary, `${JSON.stringify(value, null, 4)}
11137
+ writeFileSync29(temporary, `${JSON.stringify(value, null, 4)}
10310
11138
  `, "utf8");
10311
- renameSync4(temporary, file);
11139
+ renameSync5(temporary, file);
10312
11140
  }
10313
11141
 
10314
11142
  // src/kit/drift.ts
10315
- import { readFileSync as readFileSync24, existsSync as existsSync42 } from "fs";
10316
- import { join as join42 } from "path";
11143
+ import { readFileSync as readFileSync27, existsSync as existsSync46 } from "fs";
11144
+ import { join as join48 } from "path";
10317
11145
  function fileDigest(root, relativePath) {
10318
- const file = join42(root, relativePath);
10319
- return existsSync42(file) ? sha256Digest(readFileSync24(file)) : null;
11146
+ const file = join48(root, relativePath);
11147
+ return existsSync46(file) ? sha256Digest(readFileSync27(file)) : null;
10320
11148
  }
10321
11149
  function agentPackTargets(root, lock) {
10322
11150
  if (lock.agentPack.root === void 0) {
@@ -10347,12 +11175,12 @@ function managedDigests(root, lock) {
10347
11175
  }
10348
11176
 
10349
11177
  // src/kit/operations/shared.ts
10350
- import { mkdirSync as mkdirSync17, readdirSync as readdirSync5, readFileSync as readFileSync27, writeFileSync as writeFileSync31, existsSync as existsSync45 } from "fs";
10351
- import { dirname as dirname14, join as join45 } from "path";
11178
+ import { mkdirSync as mkdirSync17, readdirSync as readdirSync6, readFileSync as readFileSync30, writeFileSync as writeFileSync32, existsSync as existsSync49 } from "fs";
11179
+ import { dirname as dirname16, join as join51 } from "path";
10352
11180
 
10353
11181
  // src/kit/expand.ts
10354
- import { mkdirSync as mkdirSync16, existsSync as existsSync43, readFileSync as readFileSync25, writeFileSync as writeFileSync29 } from "fs";
10355
- import { dirname as dirname13, join as join43 } from "path";
11182
+ import { mkdirSync as mkdirSync16, existsSync as existsSync47, readFileSync as readFileSync28, writeFileSync as writeFileSync30 } from "fs";
11183
+ import { dirname as dirname15, join as join49 } from "path";
10356
11184
 
10357
11185
  // src/kit/tar.ts
10358
11186
  var BLOCK = 512;
@@ -10453,32 +11281,32 @@ function expandArchive(bytes, request2) {
10453
11281
  let written = 0;
10454
11282
  let matched = 0;
10455
11283
  for (const entry of entries) {
10456
- const relative3 = request2.root === void 0 || request2.root.length === 0 ? entry.path : `${request2.root.replace(/\/+$/, "")}/${entry.path}`;
10457
- const destination = join43(request2.targetDir, relative3);
11284
+ const relative4 = request2.root === void 0 || request2.root.length === 0 ? entry.path : `${request2.root.replace(/\/+$/, "")}/${entry.path}`;
11285
+ const destination = join49(request2.targetDir, relative4);
10458
11286
  if (entry.kind === "directory") {
10459
11287
  mkdirSync16(destination, { recursive: true });
10460
11288
  continue;
10461
11289
  }
10462
11290
  const digest = sha256Digest(entry.bytes);
10463
- files[relative3] = digest;
10464
- if (existsSync43(destination) && request2.existing !== "replace") {
10465
- assertSameBytes(destination, relative3, digest);
11291
+ files[relative4] = digest;
11292
+ if (existsSync47(destination) && request2.existing !== "replace") {
11293
+ assertSameBytes(destination, relative4, digest);
10466
11294
  matched += 1;
10467
11295
  continue;
10468
11296
  }
10469
- mkdirSync16(dirname13(destination), { recursive: true });
10470
- writeFileSync29(destination, entry.bytes, { mode: entry.mode === 0 ? 420 : entry.mode });
11297
+ mkdirSync16(dirname15(destination), { recursive: true });
11298
+ writeFileSync30(destination, entry.bytes, { mode: entry.mode === 0 ? 420 : entry.mode });
10471
11299
  written += 1;
10472
11300
  }
10473
11301
  return { files, written, matched };
10474
11302
  }
10475
- function assertSameBytes(destination, relative3, expected) {
10476
- const actual = sha256Digest(readFileSync25(destination));
11303
+ function assertSameBytes(destination, relative4, expected) {
11304
+ const actual = sha256Digest(readFileSync28(destination));
10477
11305
  if (actual === expected) {
10478
11306
  return;
10479
11307
  }
10480
11308
  throw new KitError("KIT_TARGET_NOT_EMPTY", "A file the release would write is already there with different contents.", {
10481
- evidence: { path: relative3, expected, actual },
11309
+ evidence: { path: relative4, expected, actual },
10482
11310
  next: { command: "spfn kit status --json", requiresHumanApproval: false }
10483
11311
  });
10484
11312
  }
@@ -10581,15 +11409,15 @@ function registryNpmrc(scopes, registryUrl) {
10581
11409
  }
10582
11410
 
10583
11411
  // src/kit/package-graph.ts
10584
- import { existsSync as existsSync44, readFileSync as readFileSync26, writeFileSync as writeFileSync30 } from "fs";
10585
- import { join as join44 } from "path";
11412
+ import { existsSync as existsSync48, readFileSync as readFileSync29, writeFileSync as writeFileSync31 } from "fs";
11413
+ import { join as join50 } from "path";
10586
11414
  var DEPENDENCY_BLOCKS = ["dependencies", "devDependencies", "optionalDependencies"];
10587
11415
  function applyManifestVersions(projectDir2, manifest) {
10588
- const file = join44(projectDir2, "package.json");
10589
- if (!existsSync44(file)) {
11416
+ const file = join50(projectDir2, "package.json");
11417
+ if (!existsSync48(file)) {
10590
11418
  return [];
10591
11419
  }
10592
- const raw = readFileSync26(file, "utf8");
11420
+ const raw = readFileSync29(file, "utf8");
10593
11421
  const document = JSON.parse(raw);
10594
11422
  const wanted = new Map(manifest.packages.map((entry) => [entry.name, entry.version]));
10595
11423
  const changes = [];
@@ -10608,7 +11436,7 @@ function applyManifestVersions(projectDir2, manifest) {
10608
11436
  }
10609
11437
  }
10610
11438
  if (changes.length > 0) {
10611
- writeFileSync30(file, `${JSON.stringify(document, null, indentationOf(raw))}${trailingNewline(raw)}`);
11439
+ writeFileSync31(file, `${JSON.stringify(document, null, indentationOf(raw))}${trailingNewline(raw)}`);
10612
11440
  }
10613
11441
  return changes;
10614
11442
  }
@@ -10636,12 +11464,12 @@ function installedGraphMismatches(projectDir2, manifest, skip = () => false) {
10636
11464
  return mismatches;
10637
11465
  }
10638
11466
  function installedVersion(projectDir2, name) {
10639
- const file = join44(projectDir2, "node_modules", ...name.split("/"), "package.json");
10640
- if (!existsSync44(file)) {
11467
+ const file = join50(projectDir2, "node_modules", ...name.split("/"), "package.json");
11468
+ if (!existsSync48(file)) {
10641
11469
  return null;
10642
11470
  }
10643
11471
  try {
10644
- const version = JSON.parse(readFileSync26(file, "utf8")).version;
11472
+ const version = JSON.parse(readFileSync29(file, "utf8")).version;
10645
11473
  return typeof version === "string" ? version : null;
10646
11474
  } catch {
10647
11475
  return null;
@@ -10728,10 +11556,10 @@ function latestActiveRelease(catalog) {
10728
11556
  return active[0];
10729
11557
  }
10730
11558
  function assertEmptyTarget(targetDir) {
10731
- if (!existsSync45(targetDir)) {
11559
+ if (!existsSync49(targetDir)) {
10732
11560
  return;
10733
11561
  }
10734
- const entries = readdirSync5(targetDir).filter((name) => name !== ".DS_Store");
11562
+ const entries = readdirSync6(targetDir).filter((name) => name !== ".DS_Store");
10735
11563
  if (entries.length > 0) {
10736
11564
  throw new KitError("KIT_TARGET_NOT_EMPTY", "A Kit install needs a new or empty directory.", {
10737
11565
  evidence: { targetDir, entries: entries.length }
@@ -10759,13 +11587,13 @@ async function materializeTargets(adapters, projectDir2, targets, options = {})
10759
11587
  continue;
10760
11588
  }
10761
11589
  written[target.path] = digest;
10762
- writeManagedFile(join45(projectDir2, target.path), target.path, bytes, digest, options.existing);
11590
+ writeManagedFile(join51(projectDir2, target.path), target.path, bytes, digest, options.existing);
10763
11591
  }
10764
11592
  return written;
10765
11593
  }
10766
11594
  function writeManagedFile(file, path5, bytes, digest, existing = "verify") {
10767
- if (existsSync45(file) && existing !== "replace") {
10768
- const actual = sha256Digest(readFileSync27(file));
11595
+ if (existsSync49(file) && existing !== "replace") {
11596
+ const actual = sha256Digest(readFileSync30(file));
10769
11597
  if (actual === digest) {
10770
11598
  return;
10771
11599
  }
@@ -10774,8 +11602,8 @@ function writeManagedFile(file, path5, bytes, digest, existing = "verify") {
10774
11602
  next: { command: "spfn kit status --json", requiresHumanApproval: false }
10775
11603
  });
10776
11604
  }
10777
- mkdirSync17(dirname14(file), { recursive: true });
10778
- writeFileSync31(file, bytes);
11605
+ mkdirSync17(dirname16(file), { recursive: true });
11606
+ writeFileSync32(file, bytes);
10779
11607
  }
10780
11608
  async function installFrozenGraph(adapters, options) {
10781
11609
  const maxAttempts = options.allowSessionRetry === false ? 1 : 2;
@@ -10837,7 +11665,7 @@ function scopesOf(manifest) {
10837
11665
  return scopes.length === 0 ? ["@superfunction"] : [...new Set(scopes)];
10838
11666
  }
10839
11667
  function writeRegistryNpmrc(projectDir2, manifest, registryUrl) {
10840
- writeFileSync31(join45(projectDir2, ".npmrc"), registryNpmrc(scopesOf(manifest), registryUrl), "utf8");
11668
+ writeFileSync32(join51(projectDir2, ".npmrc"), registryNpmrc(scopesOf(manifest), registryUrl), "utf8");
10841
11669
  }
10842
11670
  function assertGraphInstalled(options) {
10843
11671
  if (options.expect === void 0) {
@@ -11085,10 +11913,10 @@ function installSteps(options) {
11085
11913
  return { kind: "done", evidence: { attempts: evidence.attempts, tooling: tooling.specifier } };
11086
11914
  },
11087
11915
  async verify() {
11088
- if (!existsSync46(join46(projectDir2, "node_modules"))) {
11916
+ if (!existsSync50(join52(projectDir2, "node_modules"))) {
11089
11917
  return { ok: false, reason: "dependencies-missing" };
11090
11918
  }
11091
- if (!existsSync46(paths.lockFile)) {
11919
+ if (!existsSync50(paths.lockFile)) {
11092
11920
  writeLock(options, projectDir2, adapters.clock.now());
11093
11921
  }
11094
11922
  return { ok: true };
@@ -11187,7 +12015,7 @@ async function verifyToolingPlan(options, projectDir2, operationId) {
11187
12015
  });
11188
12016
  const isolated = await runIsolated(
11189
12017
  projectDir2,
11190
- join46(mkdtempSync(join46(tmpdir(), "spfn-kit-")), operationId),
12018
+ join52(mkdtempSync(join52(tmpdir(), "spfn-kit-")), operationId),
11191
12019
  (workingCopy) => discovered.tooling.planInstall({
11192
12020
  projectDir: workingCopy,
11193
12021
  release: options.manifest.version
@@ -11495,8 +12323,8 @@ async function startRecovery(sink, adapters, activationId) {
11495
12323
  }
11496
12324
 
11497
12325
  // src/kit/customer-source.ts
11498
- import { existsSync as existsSync47, mkdirSync as mkdirSync18, readFileSync as readFileSync28, writeFileSync as writeFileSync32 } from "fs";
11499
- import { dirname as dirname15, join as join47 } from "path";
12326
+ import { existsSync as existsSync51, mkdirSync as mkdirSync18, readFileSync as readFileSync31, writeFileSync as writeFileSync33 } from "fs";
12327
+ import { dirname as dirname17, join as join53 } from "path";
11500
12328
  var IGNORED_DIRECTORIES = ["node_modules", ".git", ".next", ".turbo", ".vercel", "dist", "coverage"];
11501
12329
  var KIT_OWNED_FILES = ["pnpm-lock.yaml", "package.json", ".npmrc", "tsconfig.tsbuildinfo"];
11502
12330
  function declaredPaths(declaration) {
@@ -11541,21 +12369,21 @@ function compareCustomerSource(before, after) {
11541
12369
  return paths.filter((path5) => before[path5] !== after[path5]).map((path5) => ({ path: path5, before: before[path5] ?? null, after: after[path5] ?? null }));
11542
12370
  }
11543
12371
  function customerBaselinePath(projectDir2, operationId) {
11544
- return join47(projectDir2, ".spfn", "operations", `${operationId}.customer-source.json`);
12372
+ return join53(projectDir2, ".spfn", "operations", `${operationId}.customer-source.json`);
11545
12373
  }
11546
12374
  function writeCustomerBaseline(projectDir2, baseline) {
11547
12375
  const path5 = customerBaselinePath(projectDir2, baseline.operationId);
11548
- mkdirSync18(dirname15(path5), { recursive: true });
11549
- writeFileSync32(path5, `${JSON.stringify(baseline, null, 4)}
12376
+ mkdirSync18(dirname17(path5), { recursive: true });
12377
+ writeFileSync33(path5, `${JSON.stringify(baseline, null, 4)}
11550
12378
  `);
11551
12379
  }
11552
12380
  function readCustomerBaseline(projectDir2, operationId) {
11553
12381
  const path5 = customerBaselinePath(projectDir2, operationId);
11554
- if (!existsSync47(path5)) {
12382
+ if (!existsSync51(path5)) {
11555
12383
  return null;
11556
12384
  }
11557
12385
  try {
11558
- const parsed = JSON.parse(readFileSync28(path5, "utf8"));
12386
+ const parsed = JSON.parse(readFileSync31(path5, "utf8"));
11559
12387
  return parsed?.schemaVersion === 1 && typeof parsed.files === "object" ? parsed : null;
11560
12388
  } catch {
11561
12389
  return null;
@@ -11957,8 +12785,8 @@ function planResult(sink, plan, digest) {
11957
12785
  }
11958
12786
 
11959
12787
  // src/kit/operations/inspect.ts
11960
- import { existsSync as existsSync48 } from "fs";
11961
- import { join as join48 } from "path";
12788
+ import { existsSync as existsSync52 } from "fs";
12789
+ import { join as join54 } from "path";
11962
12790
  async function runStatus(request2, adapters) {
11963
12791
  const paths = kitPaths(request2.projectDir);
11964
12792
  const lock = readInstalledLock(paths.lockFile);
@@ -12071,7 +12899,7 @@ async function runCheck(request2, adapters) {
12071
12899
  fixCommand: `spfn kit resume ${journal.operationId} --json`
12072
12900
  });
12073
12901
  }
12074
- if (!existsSync48(join48(request2.projectDir, "node_modules"))) {
12902
+ if (!existsSync52(join54(request2.projectDir, "node_modules"))) {
12075
12903
  diagnostics.push({
12076
12904
  code: "KIT_GATE_FAILED",
12077
12905
  severity: "warning",
@@ -12219,7 +13047,7 @@ async function runAbandon(request2, adapters) {
12219
13047
 
12220
13048
  // src/commands/kit/index.ts
12221
13049
  function projectDir(options) {
12222
- return resolve3(options.dir ?? process.cwd());
13050
+ return resolve6(options.dir ?? process.cwd());
12223
13051
  }
12224
13052
  function licenseKeyReader(options) {
12225
13053
  return async () => {
@@ -12313,10 +13141,10 @@ function reportFailure(error, json) {
12313
13141
  if (json) {
12314
13142
  console.log(JSON.stringify(event));
12315
13143
  } else {
12316
- console.error(`${chalk52.red("\u2717")} ${error.message}`);
12317
- console.error(` ${chalk52.gray(error.code)}`);
13144
+ console.error(`${chalk53.red("\u2717")} ${error.message}`);
13145
+ console.error(` ${chalk53.gray(error.code)}`);
12318
13146
  if (error.next) {
12319
- console.error(` ${chalk52.gray("next:")} ${error.next.command}`);
13147
+ console.error(` ${chalk53.gray("next:")} ${error.next.command}`);
12320
13148
  }
12321
13149
  }
12322
13150
  process.exitCode = error.exitCode;
@@ -12340,7 +13168,7 @@ async function readOnlyAdapters(projectDir2) {
12340
13168
  }
12341
13169
  var kitCommand = new Command18("kit").description("Install, verify and update a Superfunction Kit in this project");
12342
13170
  kitCommand.command("install <setup-url> <directory>").description("Install a Kit into a new, empty directory").option("--json", "newline-delimited JSON events and a machine-readable result").option("--license-key-stdin", "read the license key from stdin instead of prompting").action(async (setupUrl, directory, options) => {
12343
- const targetDir = resolve3(directory);
13171
+ const targetDir = resolve6(directory);
12344
13172
  await withResult(options.json === true, async () => runInstall({
12345
13173
  setupUrl,
12346
13174
  targetDir,
@@ -12368,7 +13196,7 @@ kitCommand.command("status").description("Report what is installed, activated an
12368
13196
  try {
12369
13197
  const report2 = await runStatus({ projectDir: dir }, await readOnlyAdapters(dir));
12370
13198
  printJson(options.json === true, report2, () => {
12371
- console.log(chalk52.bold(report2.installed ? `${report2.kitId} ${report2.release}` : "No Kit installed here"));
13199
+ console.log(chalk53.bold(report2.installed ? `${report2.kitId} ${report2.release}` : "No Kit installed here"));
12372
13200
  console.log(` activation: ${report2.activationId ?? "-"} credential: ${report2.credential}`);
12373
13201
  console.log(` managed drift: ${report2.managedDrift} update available: ${report2.updateAvailable ?? "none"}`);
12374
13202
  if (report2.operation) {
@@ -12385,15 +13213,15 @@ kitCommand.command("check").description("Check the installed release against its
12385
13213
  const report2 = await runCheck({ projectDir: dir }, await readOnlyAdapters(dir));
12386
13214
  printJson(options.json === true, report2, () => {
12387
13215
  if (report2.diagnostics.length === 0) {
12388
- console.log(`${chalk52.green("\u2713")} No problems found.`);
13216
+ console.log(`${chalk53.green("\u2713")} No problems found.`);
12389
13217
  return;
12390
13218
  }
12391
13219
  for (const diagnostic of report2.diagnostics) {
12392
- const mark = diagnostic.severity === "error" ? chalk52.red("\u2717") : chalk52.yellow("!");
13220
+ const mark = diagnostic.severity === "error" ? chalk53.red("\u2717") : chalk53.yellow("!");
12393
13221
  console.log(`${mark} ${diagnostic.summary}`);
12394
- console.log(` ${chalk52.gray(diagnostic.code)}${diagnostic.path ? ` ${diagnostic.path}` : ""}`);
13222
+ console.log(` ${chalk53.gray(diagnostic.code)}${diagnostic.path ? ` ${diagnostic.path}` : ""}`);
12395
13223
  if (diagnostic.fixCommand) {
12396
- console.log(` ${chalk52.gray("next:")} ${diagnostic.fixCommand}`);
13224
+ console.log(` ${chalk53.gray("next:")} ${diagnostic.fixCommand}`);
12397
13225
  }
12398
13226
  }
12399
13227
  });
@@ -12483,8 +13311,8 @@ function parseKeys(raw) {
12483
13311
  }
12484
13312
 
12485
13313
  // src/kit/scaffold.ts
12486
- import { mkdirSync as mkdirSync19, existsSync as existsSync49, readFileSync as readFileSync29, writeFileSync as writeFileSync33 } from "fs";
12487
- import { dirname as dirname16, join as join49 } from "path";
13314
+ import { mkdirSync as mkdirSync19, existsSync as existsSync53, readFileSync as readFileSync32, writeFileSync as writeFileSync34 } from "fs";
13315
+ import { dirname as dirname18, join as join55 } from "path";
12488
13316
 
12489
13317
  // src/kit/http/registry.ts
12490
13318
  import { createHash as createHash3 } from "crypto";
@@ -12672,9 +13500,9 @@ var KitRegistryProxyClient = class {
12672
13500
  return response.bytes;
12673
13501
  }
12674
13502
  };
12675
- function registryEntitlementProbe(client, resolve4) {
13503
+ function registryEntitlementProbe(client, resolve7) {
12676
13504
  return async (request2) => {
12677
- const target = await resolve4(request2);
13505
+ const target = await resolve7(request2);
12678
13506
  if (target === null) {
12679
13507
  return { entitled: false, reason: "unavailable" };
12680
13508
  }
@@ -12830,20 +13658,20 @@ var ArtifactScaffoldPort = class {
12830
13658
  }
12831
13659
  };
12832
13660
  function seedProjectName(targetDir, name) {
12833
- const file = join49(targetDir, "package.json");
12834
- if (!existsSync49(file)) {
13661
+ const file = join55(targetDir, "package.json");
13662
+ if (!existsSync53(file)) {
12835
13663
  return;
12836
13664
  }
12837
13665
  let parsed;
12838
13666
  try {
12839
- parsed = JSON.parse(readFileSync29(file, "utf8"));
13667
+ parsed = JSON.parse(readFileSync32(file, "utf8"));
12840
13668
  } catch {
12841
13669
  throw new KitError("KIT_MANIFEST_INVALID", "The release scaffold's package.json is not readable JSON.", {
12842
13670
  evidence: { path: "package.json" }
12843
13671
  });
12844
13672
  }
12845
13673
  parsed.name = packageNameOf(name);
12846
- writeFileSync33(file, `${JSON.stringify(parsed, null, 4)}
13674
+ writeFileSync34(file, `${JSON.stringify(parsed, null, 4)}
12847
13675
  `, "utf8");
12848
13676
  }
12849
13677
  function packageNameOf(name) {
@@ -12851,9 +13679,9 @@ function packageNameOf(name) {
12851
13679
  return cleaned.length === 0 ? "kit-project" : cleaned;
12852
13680
  }
12853
13681
  function writeScaffoldRecord(targetDir, record) {
12854
- const file = join49(targetDir, SCAFFOLD_RECORD_PATH);
12855
- mkdirSync19(dirname16(file), { recursive: true });
12856
- writeFileSync33(file, `${JSON.stringify(record, null, 4)}
13682
+ const file = join55(targetDir, SCAFFOLD_RECORD_PATH);
13683
+ mkdirSync19(dirname18(file), { recursive: true });
13684
+ writeFileSync34(file, `${JSON.stringify(record, null, 4)}
12857
13685
  `, "utf8");
12858
13686
  }
12859
13687
 
@@ -13232,9 +14060,9 @@ function requireCatalogUrl(projectDir2) {
13232
14060
  }
13233
14061
 
13234
14062
  // src/kit/local/index.ts
13235
- import { createRequire as createRequire2 } from "module";
13236
- import { join as join53 } from "path";
13237
- import { pathToFileURL as pathToFileURL4 } from "url";
14063
+ import { createRequire as createRequire4 } from "module";
14064
+ import { join as join59 } from "path";
14065
+ import { pathToFileURL as pathToFileURL6 } from "url";
13238
14066
 
13239
14067
  // src/kit/local/process.ts
13240
14068
  import { execa as execa13 } from "execa";
@@ -13382,8 +14210,8 @@ ${result.stdout}`;
13382
14210
  }
13383
14211
 
13384
14212
  // src/kit/local/database.ts
13385
- import { existsSync as existsSync50 } from "fs";
13386
- import { dirname as dirname17, join as join50 } from "path";
14213
+ import { existsSync as existsSync54 } from "fs";
14214
+ import { dirname as dirname19, join as join56 } from "path";
13387
14215
  import { fileURLToPath as fileURLToPath3 } from "url";
13388
14216
  var DEFAULT_DATABASE_TIMEOUT_MS = 9e5;
13389
14217
  var SpfnDatabasePort = class {
@@ -13466,13 +14294,13 @@ function spfnCommand() {
13466
14294
  return bin === null ? { file: "spfn", args: [] } : { file: process.execPath, args: [bin] };
13467
14295
  }
13468
14296
  function resolveSpfnBin(from = fileURLToPath3(import.meta.url)) {
13469
- let directory = dirname17(from);
14297
+ let directory = dirname19(from);
13470
14298
  for (let depth = 0; depth < 8; depth += 1) {
13471
- const candidate = join50(directory, "bin", "spfn.js");
13472
- if (existsSync50(candidate)) {
14299
+ const candidate = join56(directory, "bin", "spfn.js");
14300
+ if (existsSync54(candidate)) {
13473
14301
  return candidate;
13474
14302
  }
13475
- const parent = dirname17(directory);
14303
+ const parent = dirname19(directory);
13476
14304
  if (parent === directory) {
13477
14305
  break;
13478
14306
  }
@@ -13482,8 +14310,8 @@ function resolveSpfnBin(from = fileURLToPath3(import.meta.url)) {
13482
14310
  }
13483
14311
 
13484
14312
  // src/kit/local/gates.ts
13485
- import { existsSync as existsSync51, readFileSync as readFileSync30 } from "fs";
13486
- import { join as join51 } from "path";
14313
+ import { existsSync as existsSync55, readFileSync as readFileSync33 } from "fs";
14314
+ import { join as join57 } from "path";
13487
14315
  var DEFAULT_GATE_TIMEOUT_MS = 9e5;
13488
14316
  var GATE_PLANS = {
13489
14317
  "kit-check": { scripts: [], fallback: spfnFallback(["kit", "check", "--json"]) },
@@ -13534,12 +14362,12 @@ var CommandGatePort = class {
13534
14362
  }
13535
14363
  };
13536
14364
  function projectScripts(cwd) {
13537
- const file = join51(cwd, "package.json");
13538
- if (!existsSync51(file)) {
14365
+ const file = join57(cwd, "package.json");
14366
+ if (!existsSync55(file)) {
13539
14367
  return {};
13540
14368
  }
13541
14369
  try {
13542
- const parsed = JSON.parse(readFileSync30(file, "utf8"));
14370
+ const parsed = JSON.parse(readFileSync33(file, "utf8"));
13543
14371
  return parsed.scripts ?? {};
13544
14372
  } catch {
13545
14373
  return {};
@@ -13551,8 +14379,8 @@ function spfnFallback(args) {
13551
14379
  }
13552
14380
 
13553
14381
  // src/kit/local/git.ts
13554
- import { existsSync as existsSync52 } from "fs";
13555
- import { join as join52 } from "path";
14382
+ import { existsSync as existsSync56 } from "fs";
14383
+ import { join as join58 } from "path";
13556
14384
  var FALLBACK_AUTHOR_NAME = "spfn kit";
13557
14385
  var FALLBACK_AUTHOR_EMAIL = "kit@superfunction.xyz";
13558
14386
  var DEFAULT_GIT_TIMEOUT_MS = 12e4;
@@ -13563,7 +14391,7 @@ var SystemGitPort = class {
13563
14391
  }
13564
14392
  /** Start a repository, or leave the one that is already there alone. */
13565
14393
  async init(request2) {
13566
- if (existsSync52(join52(request2.cwd, ".git"))) {
14394
+ if (existsSync56(join58(request2.cwd, ".git"))) {
13567
14395
  return;
13568
14396
  }
13569
14397
  const result = await this.git(request2.cwd, ["init"]);
@@ -13680,8 +14508,8 @@ function createKitLocalPorts(options = {}) {
13680
14508
  }
13681
14509
  function createProjectModuleLoader() {
13682
14510
  return async (specifier, projectDir2) => {
13683
- const resolve4 = createRequire2(join53(projectDir2, "package.json"));
13684
- return import(pathToFileURL4(resolve4.resolve(specifier)).href);
14511
+ const resolve7 = createRequire4(join59(projectDir2, "package.json"));
14512
+ return import(pathToFileURL6(resolve7.resolve(specifier)).href);
13685
14513
  };
13686
14514
  }
13687
14515