@biffo/cli 0.280.0 → 0.282.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command27 } from "commander";
4
+ import { Command as Command28 } from "commander";
5
5
 
6
6
  // src/commands/core.ts
7
7
  import { Command as Command4 } from "commander";
@@ -1082,6 +1082,23 @@ var GitAdapter = class {
1082
1082
  const { stdout } = await execa2("git", ["remote", "get-url", remote], { cwd });
1083
1083
  return stdout.trim();
1084
1084
  }
1085
+ /**
1086
+ * The commit SHA `HEAD` currently resolves to on `repoUrl`'s default
1087
+ * branch, without cloning anything — a single `git ls-remote` round trip.
1088
+ * Built for plugin provenance/staleness (#1547): recording or checking a
1089
+ * source commit should not need a full clone when the answer is one
1090
+ * network call. Returns `null` on any failure (network, auth, a repo that
1091
+ * does not exist) rather than throwing — both call sites treat an unknown
1092
+ * SHA as an honest "could not determine", never as license to invent one.
1093
+ */
1094
+ async resolveDefaultBranchSha(repoUrl) {
1095
+ const { stdout, exitCode } = await execa2("git", ["ls-remote", "--exit-code", repoUrl, "HEAD"], {
1096
+ reject: false
1097
+ });
1098
+ if (exitCode !== 0) return null;
1099
+ const sha = stdout.split(/\s+/)[0];
1100
+ return sha && /^[0-9a-f]{40}$/i.test(sha) ? sha : null;
1101
+ }
1085
1102
  /**
1086
1103
  * Prunes remote-tracking refs whose remote branch is gone, so `upstream:track`
1087
1104
  * reports `[gone]` for a merged-and-deleted branch (#758).
@@ -1596,7 +1613,7 @@ var GitHubAdapter = class {
1596
1613
  } catch (err) {
1597
1614
  if (err.status !== 404) throw err;
1598
1615
  }
1599
- await new Promise((resolve20) => setTimeout(resolve20, intervalMs));
1616
+ await new Promise((resolve21) => setTimeout(resolve21, intervalMs));
1600
1617
  }
1601
1618
  throw new Error(
1602
1619
  `Branch "${branch}" not found in ${org}/${repo} after ${timeoutMs / 1e3}s \u2014 GitHub template generation may have stalled. Check the repository and re-run biffo init.`
@@ -1611,7 +1628,7 @@ var GitHubAdapter = class {
1611
1628
  } catch (err) {
1612
1629
  if (err.status !== 404) throw err;
1613
1630
  }
1614
- await new Promise((resolve20) => setTimeout(resolve20, intervalMs));
1631
+ await new Promise((resolve21) => setTimeout(resolve21, intervalMs));
1615
1632
  }
1616
1633
  throw new Error(
1617
1634
  `Ref "${ref}" not found in ${org}/${repo} after ${timeoutMs / 1e3}s \u2014 GitHub template generation may have stalled. Check the repository and re-run biffo init.`
@@ -1845,7 +1862,7 @@ var GitHubAdapter = class {
1845
1862
  }
1846
1863
  if (status !== 404 || Date.now() >= deadline) throw err;
1847
1864
  log.info("Branch protection endpoint not yet ready, retrying...");
1848
- await new Promise((resolve20) => setTimeout(resolve20, protectionIntervalMs));
1865
+ await new Promise((resolve21) => setTimeout(resolve21, protectionIntervalMs));
1849
1866
  }
1850
1867
  }
1851
1868
  }
@@ -2018,7 +2035,7 @@ var GitHubAdapter = class {
2018
2035
  }
2019
2036
  if (status !== 404 || Date.now() >= deadline) throw err;
2020
2037
  log.info("Branch protection endpoint not yet ready, retrying...");
2021
- await new Promise((resolve20) => setTimeout(resolve20, protectionIntervalMs));
2038
+ await new Promise((resolve21) => setTimeout(resolve21, protectionIntervalMs));
2022
2039
  }
2023
2040
  }
2024
2041
  } catch (err) {
@@ -2291,7 +2308,7 @@ var GitHubAdapter = class {
2291
2308
  } catch (err) {
2292
2309
  if (err.status !== 404 || Date.now() >= deadline) throw err;
2293
2310
  log.info(`Workflow ${workflowId} not yet indexed by GitHub Actions, retrying...`);
2294
- await new Promise((resolve20) => setTimeout(resolve20, intervalMs));
2311
+ await new Promise((resolve21) => setTimeout(resolve21, intervalMs));
2295
2312
  }
2296
2313
  }
2297
2314
  }
@@ -2310,7 +2327,7 @@ var GitHubAdapter = class {
2310
2327
  } catch (err) {
2311
2328
  if (err.status !== 404 || Date.now() >= deadline) throw err;
2312
2329
  log.info(`Workflow ${workflowId} not yet indexed by GitHub Actions, retrying...`);
2313
- await new Promise((resolve20) => setTimeout(resolve20, intervalMs));
2330
+ await new Promise((resolve21) => setTimeout(resolve21, intervalMs));
2314
2331
  }
2315
2332
  }
2316
2333
  }
@@ -2334,7 +2351,7 @@ var GitHubAdapter = class {
2334
2351
  } else {
2335
2352
  log.info(" Waiting for run to be queued...");
2336
2353
  }
2337
- await new Promise((resolve20) => setTimeout(resolve20, intervalMs));
2354
+ await new Promise((resolve21) => setTimeout(resolve21, intervalMs));
2338
2355
  }
2339
2356
  throw new Error(
2340
2357
  `Workflow ${workflowId} did not complete within ${timeoutMs / 1e3 / 60} minutes`
@@ -4301,7 +4318,7 @@ var AwsAdapter = class {
4301
4318
  const code = err.Code;
4302
4319
  if (code === "OperationAborted" && attempt < maxAttempts) {
4303
4320
  log.info(` Waiting for S3 to release "${bucketName}"... (${attempt}/${maxAttempts})`);
4304
- await new Promise((resolve20) => setTimeout(resolve20, retryDelayMs));
4321
+ await new Promise((resolve21) => setTimeout(resolve21, retryDelayMs));
4305
4322
  } else if (code === "OperationAborted") {
4306
4323
  return false;
4307
4324
  } else {
@@ -7504,7 +7521,7 @@ async function promptForConfig(awsAccountId, awsRegion, awsProfile) {
7504
7521
  }
7505
7522
 
7506
7523
  // src/commands/plugin.ts
7507
- import { Command as Command20 } from "commander";
7524
+ import { Command as Command21 } from "commander";
7508
7525
 
7509
7526
  // src/commands/plugin-create.ts
7510
7527
  import { existsSync as existsSync26, readFileSync as readFileSync19, writeFileSync as writeFileSync9 } from "fs";
@@ -7739,6 +7756,13 @@ var ToolDeclarationSchema = z7.object({
7739
7756
  description: z7.string(),
7740
7757
  parameters: z7.record(z7.string(), z7.unknown()).default({})
7741
7758
  });
7759
+ var SeedDeclarationSchema = z7.object({
7760
+ dir: z7.string().regex(
7761
+ REL_DIR,
7762
+ "must be a plugin-relative path with no leading slash or traversal, e.g. db/seed"
7763
+ ),
7764
+ baseline_tables: z7.array(z7.string()).default([])
7765
+ }).strict();
7742
7766
  var ChatAgentDeclarationSchema = z7.object({
7743
7767
  key: z7.string().regex(/^[a-z][a-z0-9-]*$/, "must be a lowercase kebab-case slug"),
7744
7768
  agent_name: z7.string().optional(),
@@ -7777,7 +7801,11 @@ var PluginManifestSchema = z7.object({
7777
7801
  tools: z7.array(ToolDeclarationSchema).default([]),
7778
7802
  // Chat agents the plugin registers with Core (ADR-0017). Default empty — an
7779
7803
  // ordinary plugin declares none.
7780
- chat_agents: z7.array(ChatAgentDeclarationSchema).default([])
7804
+ chat_agents: z7.array(ChatAgentDeclarationSchema).default([]),
7805
+ // The plugin's tenant-scoped baseline-row seed (ADR-0005, biffo-template#1554).
7806
+ // Optional — a plugin with no baseline data omits this entirely, and
7807
+ // `biffo plugin install`/`upgrade` vendor nothing for it.
7808
+ seed: SeedDeclarationSchema.optional()
7781
7809
  }).superRefine((manifest, ctx) => {
7782
7810
  const tableNames = new Set(manifest.tables.map((t) => t.name));
7783
7811
  for (const route of manifest.api_routes) {
@@ -7788,6 +7816,16 @@ var PluginManifestSchema = z7.object({
7788
7816
  });
7789
7817
  }
7790
7818
  }
7819
+ if (manifest.seed) {
7820
+ for (const table of manifest.seed.baseline_tables) {
7821
+ if (!tableNames.has(table)) {
7822
+ ctx.addIssue({
7823
+ code: z7.ZodIssueCode.custom,
7824
+ message: `seed.baseline_tables references table '${table}', which is not declared in this manifest's 'tables' (${[...tableNames].sort().join(", ") || "none"})`
7825
+ });
7826
+ }
7827
+ }
7828
+ }
7791
7829
  });
7792
7830
  function validateManifest(raw) {
7793
7831
  const result = PluginManifestSchema.safeParse(raw);
@@ -8302,6 +8340,14 @@ var RegistryPluginEntrySchema = z8.object({
8302
8340
  description: z8.string().optional(),
8303
8341
  author: z8.string().optional(),
8304
8342
  tags: z8.array(z8.string()).optional(),
8343
+ // Summary-form mirror of the manifest's `seed.baseline_tables` (see
8344
+ // ../../lib/plugin-manifest.ts's SeedDeclarationSchema and
8345
+ // _skeletons/registry/registry-schema.json's `seed`, biffo-template#1554).
8346
+ // The registry entry only ever needs to know WHICH tables a plugin promises
8347
+ // baseline rows for, never the seed `dir` itself — that only matters to the
8348
+ // install/upgrade vendoring step, which reads it from the plugin's own
8349
+ // biffo.plugin.json after cloning, not from this summary.
8350
+ baseline_tables: z8.array(z8.string()).optional(),
8305
8351
  required_core_version: z8.string().optional(),
8306
8352
  infra_modules: z8.array(z8.string()).optional(),
8307
8353
  api_routes: z8.array(z8.string()).optional(),
@@ -8428,8 +8474,8 @@ function printEntry(entry) {
8428
8474
  }
8429
8475
 
8430
8476
  // src/commands/plugin-install.ts
8431
- import { cpSync as cpSync4, existsSync as existsSync28, mkdirSync as mkdirSync10, readFileSync as readFileSync21, statSync as statSync6 } from "fs";
8432
- import { join as join30, relative as relative3, resolve as resolve12 } from "path";
8477
+ import { cpSync as cpSync5, existsSync as existsSync30, mkdirSync as mkdirSync11, readFileSync as readFileSync22, statSync as statSync6 } from "fs";
8478
+ import { join as join32, relative as relative3, resolve as resolve12 } from "path";
8433
8479
  import chalk15 from "chalk";
8434
8480
  import { Command as Command15 } from "commander";
8435
8481
 
@@ -8475,10 +8521,129 @@ var PluginMigrationsAdapter = class {
8475
8521
  }
8476
8522
  };
8477
8523
 
8478
- // src/lib/plugin-source-copy.ts
8479
- import { copyFileSync as copyFileSync2, cpSync as cpSync3, mkdirSync as mkdirSync9 } from "fs";
8480
- import { basename, dirname as dirname9, join as join28 } from "path";
8524
+ // src/lib/plugin-provenance.ts
8525
+ import { existsSync as existsSync27, readFileSync as readFileSync20, writeFileSync as writeFileSync10 } from "fs";
8526
+ import { join as join28 } from "path";
8481
8527
  import { execa as execa5 } from "execa";
8528
+ var PLUGIN_PROVENANCE_FILENAME = ".biffo-plugin-provenance.json";
8529
+ function isPluginProvenance(value) {
8530
+ if (typeof value !== "object" || value === null) return false;
8531
+ const v = value;
8532
+ return typeof v["origin"] === "string" && (typeof v["ref"] === "string" || v["ref"] === null) && (typeof v["sha"] === "string" || v["sha"] === null) && typeof v["recordedAt"] === "string" && typeof v["inTree"] === "boolean";
8533
+ }
8534
+ function readProvenance(pluginDir2) {
8535
+ const path = join28(pluginDir2, PLUGIN_PROVENANCE_FILENAME);
8536
+ if (!existsSync27(path)) return { status: "absent" };
8537
+ let parsed;
8538
+ try {
8539
+ parsed = JSON.parse(readFileSync20(path, "utf8"));
8540
+ } catch (err) {
8541
+ return {
8542
+ status: "invalid",
8543
+ reason: `could not parse ${PLUGIN_PROVENANCE_FILENAME}: ${err.message}`
8544
+ };
8545
+ }
8546
+ if (!isPluginProvenance(parsed)) {
8547
+ return {
8548
+ status: "invalid",
8549
+ reason: `${PLUGIN_PROVENANCE_FILENAME} does not have the expected shape`
8550
+ };
8551
+ }
8552
+ return { status: "present", record: parsed };
8553
+ }
8554
+ function writePluginProvenance(pluginDir2, record) {
8555
+ writeFileSync10(join28(pluginDir2, PLUGIN_PROVENANCE_FILENAME), `${JSON.stringify(record, null, 2)}
8556
+ `);
8557
+ }
8558
+ function reconcileProvenance(previous, next) {
8559
+ if (previous.status === "present" && sameProvenance(previous.record, next)) return previous.record;
8560
+ return next;
8561
+ }
8562
+ function sameProvenance(a, b) {
8563
+ return a.origin === b.origin && a.ref === b.ref && a.sha === b.sha && a.inTree === b.inTree;
8564
+ }
8565
+ function inTreePluginProvenance(relTargetDir) {
8566
+ return {
8567
+ origin: relTargetDir,
8568
+ ref: null,
8569
+ sha: null,
8570
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
8571
+ inTree: true
8572
+ };
8573
+ }
8574
+ async function resolveLocalProvenance(sourceDir, origin) {
8575
+ const recordedAt = (/* @__PURE__ */ new Date()).toISOString();
8576
+ if (!await isGitWorkingTree(sourceDir)) {
8577
+ return { origin, ref: null, sha: null, recordedAt, inTree: false };
8578
+ }
8579
+ const sha = await tryGit(sourceDir, ["rev-parse", "HEAD"]);
8580
+ const rawRef = await tryGit(sourceDir, ["rev-parse", "--abbrev-ref", "HEAD"]);
8581
+ const ref = rawRef && rawRef !== "HEAD" ? rawRef : null;
8582
+ return { origin, ref, sha, recordedAt, inTree: false };
8583
+ }
8584
+ function resolveRegistryProvenance(repoUrl, sha) {
8585
+ return { origin: repoUrl, ref: null, sha, recordedAt: (/* @__PURE__ */ new Date()).toISOString(), inTree: false };
8586
+ }
8587
+ async function isGitWorkingTree(dir) {
8588
+ try {
8589
+ await execa5("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir });
8590
+ return true;
8591
+ } catch {
8592
+ return false;
8593
+ }
8594
+ }
8595
+ async function tryGit(cwd, args) {
8596
+ try {
8597
+ const { stdout } = await execa5("git", args, { cwd });
8598
+ return stdout.trim() || null;
8599
+ } catch {
8600
+ return null;
8601
+ }
8602
+ }
8603
+
8604
+ // src/lib/plugin-seed-vendor.ts
8605
+ import { cpSync as cpSync3, existsSync as existsSync28, mkdirSync as mkdirSync9, readdirSync as readdirSync12, rmSync as rmSync8 } from "fs";
8606
+ import { join as join29 } from "path";
8607
+ var VENDOR_PREFIX = "_plugin-";
8608
+ function pluginSeedImportDir(pluginName) {
8609
+ return `db/imports/${VENDOR_PREFIX}${pluginName}`;
8610
+ }
8611
+ function vendorPluginSeed(pluginSourceDir, manifest, cwd) {
8612
+ if (!manifest.seed) {
8613
+ return { vendored: false };
8614
+ }
8615
+ const sourceSeedDir = join29(pluginSourceDir, manifest.seed.dir);
8616
+ if (!existsSync28(sourceSeedDir)) {
8617
+ throw new Error(
8618
+ `${manifest.name}'s manifest declares seed.dir '${manifest.seed.dir}', but ${sourceSeedDir} does not exist in the plugin's source.`
8619
+ );
8620
+ }
8621
+ const sqlFiles = readdirSync12(sourceSeedDir).filter((f) => f.endsWith(".sql"));
8622
+ if (sqlFiles.length === 0) {
8623
+ throw new Error(
8624
+ `${manifest.name}'s manifest declares seed.dir '${manifest.seed.dir}', but ${sourceSeedDir} contains no *.sql files.`
8625
+ );
8626
+ }
8627
+ const relTargetDir = pluginSeedImportDir(manifest.name);
8628
+ const targetDir = join29(cwd, relTargetDir);
8629
+ rmSync8(targetDir, { recursive: true, force: true });
8630
+ mkdirSync9(targetDir, { recursive: true });
8631
+ for (const file of sqlFiles) {
8632
+ cpSync3(join29(sourceSeedDir, file), join29(targetDir, file));
8633
+ }
8634
+ log.success(
8635
+ `Vendored ${sqlFiles.length} seed file(s) to ${relTargetDir}/ (baseline_tables: ${manifest.seed.baseline_tables.join(", ") || "none declared"})`
8636
+ );
8637
+ log.info(
8638
+ `${relTargetDir}/*.sql are checksum-tracked once applied (ADR-0005 section 4) \u2014 a later version must ship a new, additively-numbered file for a changed seed, never edit one already released, or the next deploy fails loudly.`
8639
+ );
8640
+ return { vendored: true, stagedPath: relTargetDir };
8641
+ }
8642
+
8643
+ // src/lib/plugin-source-copy.ts
8644
+ import { copyFileSync as copyFileSync2, cpSync as cpSync4, mkdirSync as mkdirSync10 } from "fs";
8645
+ import { basename, dirname as dirname9, join as join30 } from "path";
8646
+ import { execa as execa6 } from "execa";
8482
8647
  var LOCAL_COPY_EXCLUDES = /* @__PURE__ */ new Set([
8483
8648
  ".git",
8484
8649
  ".venv",
@@ -8491,35 +8656,35 @@ var LOCAL_COPY_EXCLUDES = /* @__PURE__ */ new Set([
8491
8656
  ".terraform"
8492
8657
  ]);
8493
8658
  async function copyPluginSource(sourceDir, targetDir) {
8494
- if (await isGitWorkingTree(sourceDir)) {
8659
+ if (await isGitWorkingTree2(sourceDir)) {
8495
8660
  const files = await listGitFiles(sourceDir);
8496
8661
  for (const relPath of files) {
8497
- const destPath = join28(targetDir, relPath);
8498
- mkdirSync9(dirname9(destPath), { recursive: true });
8499
- copyFileSync2(join28(sourceDir, relPath), destPath);
8662
+ const destPath = join30(targetDir, relPath);
8663
+ mkdirSync10(dirname9(destPath), { recursive: true });
8664
+ copyFileSync2(join30(sourceDir, relPath), destPath);
8500
8665
  }
8501
8666
  return { usedGitIgnoreRules: true };
8502
8667
  }
8503
8668
  log.warn(
8504
8669
  `${sourceDir} is not a git working tree \u2014 cannot honour .gitignore. Falling back to a fixed exclude list (.git, .venv, node_modules, caches); anything else it does not know about (e.g. an unfamiliar cache directory) will be copied.`
8505
8670
  );
8506
- mkdirSync9(targetDir, { recursive: true });
8507
- cpSync3(sourceDir, targetDir, {
8671
+ mkdirSync10(targetDir, { recursive: true });
8672
+ cpSync4(sourceDir, targetDir, {
8508
8673
  recursive: true,
8509
8674
  filter: (src) => !LOCAL_COPY_EXCLUDES.has(basename(src))
8510
8675
  });
8511
8676
  return { usedGitIgnoreRules: false };
8512
8677
  }
8513
- async function isGitWorkingTree(dir) {
8678
+ async function isGitWorkingTree2(dir) {
8514
8679
  try {
8515
- await execa5("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir });
8680
+ await execa6("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir });
8516
8681
  return true;
8517
8682
  } catch {
8518
8683
  return false;
8519
8684
  }
8520
8685
  }
8521
8686
  async function listGitFiles(dir) {
8522
- const { stdout } = await execa5(
8687
+ const { stdout } = await execa6(
8523
8688
  "git",
8524
8689
  ["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
8525
8690
  { cwd: dir }
@@ -8528,8 +8693,8 @@ async function listGitFiles(dir) {
8528
8693
  }
8529
8694
 
8530
8695
  // src/lib/plugin-workspace-sources.ts
8531
- import { existsSync as existsSync27, readdirSync as readdirSync12, readFileSync as readFileSync20, writeFileSync as writeFileSync10 } from "fs";
8532
- import { join as join29 } from "path";
8696
+ import { existsSync as existsSync29, readdirSync as readdirSync13, readFileSync as readFileSync21, writeFileSync as writeFileSync11 } from "fs";
8697
+ import { join as join31 } from "path";
8533
8698
  function readTomlStringArray(text, key) {
8534
8699
  const open = new RegExp(`^${key}\\s*=\\s*\\[`, "m").exec(text);
8535
8700
  if (!open) return [];
@@ -8573,9 +8738,9 @@ function readDependencyNames(text) {
8573
8738
  return readTomlStringArray(text, "dependencies").map((dep) => /^\s*([A-Za-z0-9._-]+)/.exec(dep)?.[1] ?? "").filter(Boolean);
8574
8739
  }
8575
8740
  function workspaceMemberNames(instanceRoot) {
8576
- const rootPyproject = join29(instanceRoot, "pyproject.toml");
8577
- if (!existsSync27(rootPyproject)) return /* @__PURE__ */ new Set();
8578
- const text = readFileSync20(rootPyproject, "utf8");
8741
+ const rootPyproject = join31(instanceRoot, "pyproject.toml");
8742
+ if (!existsSync29(rootPyproject)) return /* @__PURE__ */ new Set();
8743
+ const text = readFileSync21(rootPyproject, "utf8");
8579
8744
  const members = readTomlStringArray(text, "members");
8580
8745
  const excluded = new Set(readTomlStringArray(text, "exclude"));
8581
8746
  const dirs = [];
@@ -8584,7 +8749,7 @@ function workspaceMemberNames(instanceRoot) {
8584
8749
  const base = member.slice(0, -2);
8585
8750
  let entries;
8586
8751
  try {
8587
- entries = readdirSync12(join29(instanceRoot, base), { withFileTypes: true });
8752
+ entries = readdirSync13(join31(instanceRoot, base), { withFileTypes: true });
8588
8753
  } catch {
8589
8754
  continue;
8590
8755
  }
@@ -8598,9 +8763,9 @@ function workspaceMemberNames(instanceRoot) {
8598
8763
  }
8599
8764
  const names = /* @__PURE__ */ new Set();
8600
8765
  for (const dir of dirs) {
8601
- const pp = join29(instanceRoot, dir, "pyproject.toml");
8602
- if (!existsSync27(pp)) continue;
8603
- const name = readProjectName(readFileSync20(pp, "utf8"));
8766
+ const pp = join31(instanceRoot, dir, "pyproject.toml");
8767
+ if (!existsSync29(pp)) continue;
8768
+ const name = readProjectName(readFileSync21(pp, "utf8"));
8604
8769
  if (name) names.add(name);
8605
8770
  }
8606
8771
  return names;
@@ -8611,8 +8776,8 @@ function existingWorkspaceSources(text) {
8611
8776
  );
8612
8777
  }
8613
8778
  function ensureWorkspaceSources(pluginPyprojectPath, memberNames) {
8614
- if (!existsSync27(pluginPyprojectPath) || memberNames.size === 0) return [];
8615
- const text = readFileSync20(pluginPyprojectPath, "utf8");
8779
+ if (!existsSync29(pluginPyprojectPath) || memberNames.size === 0) return [];
8780
+ const text = readFileSync21(pluginPyprojectPath, "utf8");
8616
8781
  const already = existingWorkspaceSources(text);
8617
8782
  const toAdd = readDependencyNames(text).filter((n) => memberNames.has(n) && !already.has(n));
8618
8783
  if (toAdd.length === 0) return [];
@@ -8632,12 +8797,12 @@ ${lines.join("\n")}${text.slice(insertAt)}`;
8632
8797
  ${lines.join("\n")}
8633
8798
  `;
8634
8799
  }
8635
- writeFileSync10(pluginPyprojectPath, updated);
8800
+ writeFileSync11(pluginPyprojectPath, updated);
8636
8801
  return toAdd;
8637
8802
  }
8638
8803
  function applyWorkspaceSources(targetDir, cwd, relTargetDir) {
8639
- const pluginPyproject = join29(targetDir, "pyproject.toml");
8640
- if (!existsSync27(pluginPyproject)) return;
8804
+ const pluginPyproject = join31(targetDir, "pyproject.toml");
8805
+ if (!existsSync29(pluginPyproject)) return;
8641
8806
  const sourced = ensureWorkspaceSources(pluginPyproject, workspaceMemberNames(cwd));
8642
8807
  if (sourced.length > 0) {
8643
8808
  log.info(
@@ -8677,14 +8842,14 @@ var pluginInstallCommand = new Command15("install").description(
8677
8842
  }
8678
8843
  );
8679
8844
  function resolveLocalPlugin(localPath) {
8680
- if (!existsSync28(localPath)) {
8845
+ if (!existsSync30(localPath)) {
8681
8846
  throw new Error(`--local path does not exist: ${localPath}`);
8682
8847
  }
8683
8848
  if (!statSync6(localPath).isDirectory()) {
8684
8849
  throw new Error(`--local path is not a directory: ${localPath}`);
8685
8850
  }
8686
- const manifestPath = join30(localPath, "biffo.plugin.json");
8687
- if (!existsSync28(manifestPath)) {
8851
+ const manifestPath = join32(localPath, "biffo.plugin.json");
8852
+ if (!existsSync30(manifestPath)) {
8688
8853
  throw new Error(
8689
8854
  `${localPath} does not contain a biffo.plugin.json manifest at its root \u2014 is it a plugin directory? (Scaffold one with \`biffo plugin create <name>\`.)`
8690
8855
  );
@@ -8710,8 +8875,8 @@ function parsePluginTarget(target) {
8710
8875
  async function cloneAndValidatePlugin(entry, git) {
8711
8876
  const tmpDir = await git.cloneToTemp(entry.repo, `biffo-plugin-${entry.name}`);
8712
8877
  try {
8713
- const manifestPath = join30(tmpDir, "biffo.plugin.json");
8714
- if (!existsSync28(manifestPath)) {
8878
+ const manifestPath = join32(tmpDir, "biffo.plugin.json");
8879
+ if (!existsSync30(manifestPath)) {
8715
8880
  throw new Error(
8716
8881
  `Plugin repo ${entry.repo} does not contain a biffo.plugin.json manifest at its root.`
8717
8882
  );
@@ -8739,8 +8904,8 @@ async function runPluginInstall(target, options, deps) {
8739
8904
  `Nothing to install. Pass a registry target (e.g. \`biffo plugin install acme-crm@1.0\`) or a local plugin directory (\`biffo plugin install --local services/acme-crm\`).`
8740
8905
  );
8741
8906
  }
8742
- const servicesDir = join30(options.cwd, "services");
8743
- if (!existsSync28(servicesDir)) {
8907
+ const servicesDir = join32(options.cwd, "services");
8908
+ if (!existsSync30(servicesDir)) {
8744
8909
  throw new Error(
8745
8910
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
8746
8911
  );
@@ -8758,10 +8923,10 @@ async function runPluginInstall(target, options, deps) {
8758
8923
  }
8759
8924
  const pluginName = entry ? entry.name : source.name;
8760
8925
  const relTargetDir = pluginDir(pluginName, "third-party");
8761
- const targetDir = join30(options.cwd, relTargetDir);
8762
- const modulesDir = join30(options.cwd, "modules", "plugins", pluginName);
8926
+ const targetDir = join32(options.cwd, relTargetDir);
8927
+ const modulesDir = join32(options.cwd, "modules", "plugins", pluginName);
8763
8928
  const inTreeSource = options.local !== void 0 && resolve12(options.local) === resolve12(targetDir);
8764
- if (existsSync28(targetDir) && !inTreeSource) {
8929
+ if (existsSync30(targetDir) && !inTreeSource) {
8765
8930
  throw new Error(
8766
8931
  `Plugin '${pluginName}' is already installed at ${relTargetDir}/. Remove it first, or wait for a future 'biffo plugin upgrade' command.`
8767
8932
  );
@@ -8796,16 +8961,19 @@ async function runPluginInstall(target, options, deps) {
8796
8961
  if (inTreeSource) {
8797
8962
  log.info(`${relTargetDir}/ is already in this checkout \u2014 installing in place.`);
8798
8963
  } else {
8799
- mkdirSync10(targetDir, { recursive: true });
8964
+ mkdirSync11(targetDir, { recursive: true });
8800
8965
  await copyPluginSource(source.sourceDir, targetDir);
8801
8966
  log.success(`Installed plugin source at ${relTargetDir}/`);
8802
8967
  }
8968
+ const previousProvenance = readProvenance(targetDir);
8969
+ const nextProvenance = inTreeSource ? inTreePluginProvenance(relTargetDir) : entry ? resolveRegistryProvenance(entry.repo, await deps.git.resolveDefaultBranchSha(entry.repo)) : await resolveLocalProvenance(source.sourceDir, source.origin);
8970
+ writePluginProvenance(targetDir, reconcileProvenance(previousProvenance, nextProvenance));
8803
8971
  applyWorkspaceSources(targetDir, options.cwd, relTargetDir);
8804
8972
  const stagePaths = [relTargetDir];
8805
- const tfSourceDir = join30(targetDir, "terraform");
8806
- if (existsSync28(tfSourceDir)) {
8807
- mkdirSync10(modulesDir, { recursive: true });
8808
- cpSync4(tfSourceDir, modulesDir, { recursive: true });
8973
+ const tfSourceDir = join32(targetDir, "terraform");
8974
+ if (existsSync30(tfSourceDir)) {
8975
+ mkdirSync11(modulesDir, { recursive: true });
8976
+ cpSync5(tfSourceDir, modulesDir, { recursive: true });
8809
8977
  stagePaths.push(`modules/plugins/${pluginName}`);
8810
8978
  log.success(`Copied Terraform module to modules/plugins/${pluginName}/`);
8811
8979
  const wiring = syncPluginTerraform(options.cwd);
@@ -8844,6 +9012,10 @@ async function runPluginInstall(target, options, deps) {
8844
9012
  } else {
8845
9013
  log.info(`${pluginName} declares no tables \u2014 nothing to migrate.`);
8846
9014
  }
9015
+ const seedResult = vendorPluginSeed(targetDir, manifest, options.cwd);
9016
+ if (seedResult.vendored) {
9017
+ stagePaths.push(seedResult.stagedPath);
9018
+ }
8847
9019
  const commitMessage = `feat(plugins): install ${pluginName}@${source.version}`;
8848
9020
  await deps.git.add(options.cwd, stagePaths);
8849
9021
  await deps.git.commit(options.cwd, commitMessage);
@@ -8860,7 +9032,7 @@ async function runPluginInstall(target, options, deps) {
8860
9032
  }
8861
9033
  function parseManifestFile(path) {
8862
9034
  try {
8863
- return JSON.parse(readFileSync21(path, "utf8"));
9035
+ return JSON.parse(readFileSync22(path, "utf8"));
8864
9036
  } catch (err) {
8865
9037
  throw new Error(`Could not parse ${path} as JSON: ${err.message}`);
8866
9038
  }
@@ -8892,13 +9064,18 @@ function printDryRun4(entry, source, relTargetDir, inTreeSource) {
8892
9064
  ` Would generate a migration for ${source.manifest.tables.length} table(s) into services/api/migrations/versions/`
8893
9065
  );
8894
9066
  }
9067
+ if (source && source.manifest.seed) {
9068
+ console.log(
9069
+ ` Would vendor seed DDL into: ${pluginSeedImportDir(name)}/ (baseline_tables: ${source.manifest.seed.baseline_tables.join(", ") || "none declared"})`
9070
+ );
9071
+ }
8895
9072
  console.log(` Would commit: feat(plugins): install ${name}@${version}
8896
9073
  `);
8897
9074
  }
8898
9075
 
8899
9076
  // src/commands/plugin-list.ts
8900
- import { existsSync as existsSync29, readFileSync as readFileSync22 } from "fs";
8901
- import { join as join31, resolve as resolve13 } from "path";
9077
+ import { existsSync as existsSync31, readFileSync as readFileSync23 } from "fs";
9078
+ import { join as join33, resolve as resolve13 } from "path";
8902
9079
  import chalk16 from "chalk";
8903
9080
  import { Command as Command16 } from "commander";
8904
9081
  var pluginListCommand = new Command16("list").description("List plugins installed in this project checkout").option("--cwd <path>", "Project root to scan (defaults to the current directory)").action(async (options) => {
@@ -8911,8 +9088,8 @@ var pluginListCommand = new Command16("list").description("List plugins installe
8911
9088
  }
8912
9089
  });
8913
9090
  async function runPluginList(options) {
8914
- const servicesDir = join31(options.cwd, "services");
8915
- if (!existsSync29(servicesDir)) {
9091
+ const servicesDir = join33(options.cwd, "services");
9092
+ if (!existsSync31(servicesDir)) {
8916
9093
  throw new Error(
8917
9094
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
8918
9095
  );
@@ -8920,7 +9097,7 @@ async function runPluginList(options) {
8920
9097
  const plugins = [];
8921
9098
  for (const location of findInstalledPlugins(options.cwd)) {
8922
9099
  try {
8923
- const manifest = validateManifest(JSON.parse(readFileSync22(location.manifestPath, "utf8")));
9100
+ const manifest = validateManifest(JSON.parse(readFileSync23(location.manifestPath, "utf8")));
8924
9101
  plugins.push({
8925
9102
  name: manifest.name,
8926
9103
  version: manifest.version,
@@ -8956,16 +9133,275 @@ async function runPluginList(options) {
8956
9133
  );
8957
9134
  }
8958
9135
 
9136
+ // src/commands/plugin-staleness.ts
9137
+ import { resolve as resolve14 } from "path";
9138
+ import { Command as Command17 } from "commander";
9139
+
9140
+ // src/lib/plugin-staleness.ts
9141
+ import { existsSync as existsSync32, readFileSync as readFileSync24, readdirSync as readdirSync14, statSync as statSync7 } from "fs";
9142
+ import { join as join34, relative as relative4 } from "path";
9143
+ function discoverVendoredPlugins(servicesDir) {
9144
+ if (!existsSync32(servicesDir)) return [];
9145
+ return readdirSync14(servicesDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("_") && e.name !== "api").map((e) => e.name).filter((name) => existsSync32(join34(servicesDir, name, "biffo.plugin.json"))).sort();
9146
+ }
9147
+ async function checkPluginStaleness(cwd, deps) {
9148
+ const servicesDir = join34(cwd, "services");
9149
+ const names = discoverVendoredPlugins(servicesDir);
9150
+ let registryRepoByName = null;
9151
+ const resolveRegistryRepo = async (name) => {
9152
+ if (registryRepoByName === null) {
9153
+ registryRepoByName = /* @__PURE__ */ new Map();
9154
+ try {
9155
+ const reg = await deps.registry.fetchRegistry();
9156
+ for (const entry of reg.plugins) registryRepoByName.set(entry.name, entry.repo);
9157
+ } catch {
9158
+ }
9159
+ }
9160
+ return registryRepoByName.get(name) ?? null;
9161
+ };
9162
+ const results = [];
9163
+ for (const name of names) {
9164
+ results.push(await checkOnePlugin(join34(servicesDir, name), name, resolveRegistryRepo, deps.git));
9165
+ }
9166
+ return results;
9167
+ }
9168
+ async function checkOnePlugin(pluginDir2, name, resolveRegistryRepo, git) {
9169
+ const provenance = readProvenance(pluginDir2);
9170
+ if (provenance.status === "invalid") {
9171
+ return {
9172
+ name,
9173
+ status: "cannot-tell",
9174
+ method: "unresolvable",
9175
+ detail: `provenance file is unreadable \u2014 ${provenance.reason}`
9176
+ };
9177
+ }
9178
+ const record = provenance.status === "present" ? provenance.record : null;
9179
+ if (record?.inTree) {
9180
+ return {
9181
+ name,
9182
+ status: "cannot-tell",
9183
+ method: "unresolvable",
9184
+ detail: `installed --local straight into services/${name}/ (in-tree) \u2014 there is no external plugin repo to compare against`
9185
+ };
9186
+ }
9187
+ if (record?.sha && isFetchableUrl(record.origin)) {
9188
+ return checkViaProvenance(name, record, record.origin, git);
9189
+ }
9190
+ const localOrigin = record && !isFetchableUrl(record.origin) && existsSync32(record.origin) ? record.origin : null;
9191
+ if (localOrigin) {
9192
+ return checkViaContentDiff(name, pluginDir2, localOrigin, { isLocalDir: true }, git);
9193
+ }
9194
+ const registryRepo = await resolveRegistryRepo(name);
9195
+ if (!registryRepo) {
9196
+ return {
9197
+ name,
9198
+ status: "cannot-tell",
9199
+ method: "unresolvable",
9200
+ detail: record ? `provenance records origin '${record.origin}', which is neither a reachable git URL nor a local directory that still exists, and '${name}' was not found in the plugin registry either` : `no provenance recorded (vendored before #1547, or an unreachable registry) and '${name}' was not found in the plugin registry \u2014 nothing to compare against`
9201
+ };
9202
+ }
9203
+ if (record?.sha) {
9204
+ return checkViaProvenance(name, record, registryRepo, git);
9205
+ }
9206
+ return checkViaContentDiff(name, pluginDir2, registryRepo, { isLocalDir: false }, git);
9207
+ }
9208
+ function isFetchableUrl(origin) {
9209
+ return /^(https?|git|ssh):\/\//.test(origin) || /^[^/\\]+@[^:]+:/.test(origin);
9210
+ }
9211
+ async function checkViaProvenance(name, record, repoUrl, git) {
9212
+ const remoteHeadSha = await git.resolveDefaultBranchSha(repoUrl);
9213
+ if (!remoteHeadSha) {
9214
+ return {
9215
+ name,
9216
+ status: "cannot-tell",
9217
+ method: "unresolvable",
9218
+ detail: `could not reach ${repoUrl} (network or authentication failure)`
9219
+ };
9220
+ }
9221
+ if (remoteHeadSha === record.sha) {
9222
+ return {
9223
+ name,
9224
+ status: "up-to-date",
9225
+ method: "provenance",
9226
+ detail: `matches ${repoUrl}'s default branch (${shortSha(remoteHeadSha)})`
9227
+ };
9228
+ }
9229
+ let clone;
9230
+ try {
9231
+ clone = await git.cloneForEditing(repoUrl, "biffo-plugin-staleness-full");
9232
+ } catch {
9233
+ return {
9234
+ name,
9235
+ status: "cannot-tell",
9236
+ method: "unresolvable",
9237
+ detail: `could not clone ${repoUrl} to count commits behind (network or authentication failure)`
9238
+ };
9239
+ }
9240
+ try {
9241
+ const commitsBehind = await git.countBehind(clone, record.sha, "HEAD");
9242
+ if (commitsBehind === null) {
9243
+ return {
9244
+ name,
9245
+ status: "cannot-tell",
9246
+ method: "unresolvable",
9247
+ detail: `recorded commit ${shortSha(record.sha)} was not found in ${repoUrl}'s history (rebased or force-pushed?) \u2014 cannot count commits behind`
9248
+ };
9249
+ }
9250
+ if (commitsBehind === 0) {
9251
+ return {
9252
+ name,
9253
+ status: "up-to-date",
9254
+ method: "provenance",
9255
+ detail: `matches ${repoUrl}'s default branch (${shortSha(remoteHeadSha)})`
9256
+ };
9257
+ }
9258
+ return {
9259
+ name,
9260
+ status: "behind",
9261
+ commitsBehind,
9262
+ method: "provenance",
9263
+ detail: `${commitsBehind} commit(s) behind ${repoUrl}'s default branch`
9264
+ };
9265
+ } finally {
9266
+ git.cleanup(clone);
9267
+ }
9268
+ }
9269
+ async function checkViaContentDiff(name, pluginDir2, sourceDir, opts, git) {
9270
+ let cloneDir = null;
9271
+ let effectiveSourceDir = sourceDir;
9272
+ if (!opts.isLocalDir) {
9273
+ try {
9274
+ cloneDir = await git.cloneToTemp(sourceDir, "biffo-plugin-staleness-content");
9275
+ } catch {
9276
+ return {
9277
+ name,
9278
+ status: "cannot-tell",
9279
+ method: "unresolvable",
9280
+ detail: `could not clone ${sourceDir} for a content comparison (network or authentication failure)`
9281
+ };
9282
+ }
9283
+ effectiveSourceDir = cloneDir;
9284
+ }
9285
+ try {
9286
+ const filesDiffering = await countDifferingFiles(effectiveSourceDir, pluginDir2);
9287
+ const originDescription = opts.isLocalDir ? effectiveSourceDir : sourceDir;
9288
+ if (filesDiffering === 0) {
9289
+ return {
9290
+ name,
9291
+ status: "up-to-date",
9292
+ method: "content-diff",
9293
+ filesDiffering: 0,
9294
+ detail: `byte-identical to ${originDescription} (no provenance recorded, so an exact commit could not be named)`
9295
+ };
9296
+ }
9297
+ return {
9298
+ name,
9299
+ status: "behind",
9300
+ filesDiffering,
9301
+ method: "content-diff",
9302
+ detail: `${filesDiffering} file(s) differ from ${originDescription} (no provenance recorded, so an exact commit count could not be determined)`
9303
+ };
9304
+ } finally {
9305
+ if (cloneDir) git.cleanup(cloneDir);
9306
+ }
9307
+ }
9308
+ function shortSha(sha) {
9309
+ return sha.slice(0, 7);
9310
+ }
9311
+ async function countDifferingFiles(sourceDir, pluginDir2) {
9312
+ const sourceFiles = await sourceFileList(sourceDir);
9313
+ const vendorFiles = vendorFileList(pluginDir2);
9314
+ const allPaths = /* @__PURE__ */ new Set([...sourceFiles, ...vendorFiles]);
9315
+ let differing = 0;
9316
+ for (const relPath of allPaths) {
9317
+ if (relPath === PLUGIN_PROVENANCE_FILENAME) continue;
9318
+ const inSource = sourceFiles.has(relPath);
9319
+ const inVendor = vendorFiles.has(relPath);
9320
+ if (!inSource || !inVendor) {
9321
+ differing++;
9322
+ continue;
9323
+ }
9324
+ const a = readFileSync24(join34(sourceDir, relPath));
9325
+ const b = readFileSync24(join34(pluginDir2, relPath));
9326
+ if (!a.equals(b)) differing++;
9327
+ }
9328
+ return differing;
9329
+ }
9330
+ async function sourceFileList(dir) {
9331
+ if (await isGitWorkingTree2(dir)) {
9332
+ return new Set(await listGitFiles(dir));
9333
+ }
9334
+ return new Set(walkExcluding(dir, dir, LOCAL_COPY_EXCLUDES));
9335
+ }
9336
+ function vendorFileList(dir) {
9337
+ return new Set(walkExcluding(dir, dir, LOCAL_COPY_EXCLUDES));
9338
+ }
9339
+ function walkExcluding(root, dir, excludes) {
9340
+ if (!existsSync32(dir)) return [];
9341
+ const out = [];
9342
+ for (const entry of readdirSync14(dir)) {
9343
+ if (excludes.has(entry) || entry === ".git") continue;
9344
+ const full = join34(dir, entry);
9345
+ const stat = statSync7(full);
9346
+ if (stat.isDirectory()) {
9347
+ out.push(...walkExcluding(root, full, excludes));
9348
+ } else {
9349
+ out.push(relative4(root, full));
9350
+ }
9351
+ }
9352
+ return out;
9353
+ }
9354
+ function exitCodeForStaleness(results) {
9355
+ if (results.some((r) => r.status === "cannot-tell")) return 2;
9356
+ if (results.some((r) => r.status === "behind")) return 1;
9357
+ return 0;
9358
+ }
9359
+ function formatStalenessReport(results) {
9360
+ if (results.length === 0) {
9361
+ return " No vendored plugins under services/ \u2014 nothing to check.";
9362
+ }
9363
+ const lines = [""];
9364
+ for (const r of results) {
9365
+ const icon = r.status === "up-to-date" ? "\u2713" : r.status === "behind" ? "\u26A0" : "?";
9366
+ lines.push(` ${icon} ${r.name}: ${labelFor(r.status)} \u2014 ${r.detail}`);
9367
+ }
9368
+ lines.push("");
9369
+ return lines.join("\n");
9370
+ }
9371
+ function labelFor(status) {
9372
+ switch (status) {
9373
+ case "up-to-date":
9374
+ return "up to date";
9375
+ case "behind":
9376
+ return "BEHIND";
9377
+ case "cannot-tell":
9378
+ return "CANNOT TELL";
9379
+ }
9380
+ }
9381
+
9382
+ // src/commands/plugin-staleness.ts
9383
+ var pluginStalenessCommand = new Command17("staleness").description(
9384
+ "Report how far each services/<name>/ vendored plugin has drifted from its source (#1547). Exits 0 up to date, 1 behind, 2 cannot tell \u2014 2 is never a pass."
9385
+ ).option("--cwd <path>", "Project root to check (defaults to the current directory)").action(async (options) => {
9386
+ const cwd = options.cwd ? resolve14(options.cwd) : process.cwd();
9387
+ const results = await checkPluginStaleness(cwd, {
9388
+ registry: new RegistryAdapter(),
9389
+ git: new GitAdapter()
9390
+ });
9391
+ console.log(formatStalenessReport(results));
9392
+ process.exit(exitCodeForStaleness(results));
9393
+ });
9394
+
8959
9395
  // src/commands/plugin-sync-migrations.ts
8960
- import { existsSync as existsSync30 } from "fs";
8961
- import { join as join32, relative as relative4, resolve as resolve14 } from "path";
9396
+ import { existsSync as existsSync33 } from "fs";
9397
+ import { join as join35, relative as relative5, resolve as resolve15 } from "path";
8962
9398
  import chalk17 from "chalk";
8963
- import { Command as Command17 } from "commander";
8964
- var pluginSyncMigrationsCommand = new Command17("sync-migrations").description(
9399
+ import { Command as Command18 } from "commander";
9400
+ var pluginSyncMigrationsCommand = new Command18("sync-migrations").description(
8965
9401
  "Generate real, committed migration file(s) for installed-but-not-yet-migrated plugin(s): biffo plugin sync-migrations [name]"
8966
9402
  ).argument("[name]", "Restrict to this installed plugin (default: every plugin under services/)").option("--dry-run", "Generate nothing; just report what would be generated").option("--no-commit", "Generate and stage the file(s) but do not commit").option("--cwd <path>", "Project root (defaults to the current directory)").action(
8967
9403
  async (name, options) => {
8968
- const cwd = options.cwd ? resolve14(options.cwd) : process.cwd();
9404
+ const cwd = options.cwd ? resolve15(options.cwd) : process.cwd();
8969
9405
  try {
8970
9406
  await runPluginSyncMigrations(
8971
9407
  name,
@@ -8979,11 +9415,11 @@ var pluginSyncMigrationsCommand = new Command17("sync-migrations").description(
8979
9415
  }
8980
9416
  );
8981
9417
  async function runPluginSyncMigrations(name, options, deps) {
8982
- const servicesDir = join32(options.cwd, "services");
8983
- if (!existsSync30(servicesDir)) {
9418
+ const servicesDir = join35(options.cwd, "services");
9419
+ if (!existsSync33(servicesDir)) {
8984
9420
  throw new Error(`${servicesDir} does not exist \u2014 is ${options.cwd} a Biffo project checkout?`);
8985
9421
  }
8986
- if (name && !existsSync30(join32(servicesDir, name, "biffo.plugin.json"))) {
9422
+ if (name && !existsSync33(join35(servicesDir, name, "biffo.plugin.json"))) {
8987
9423
  throw new Error(`Plugin '${name}' is not installed at services/${name}/.`);
8988
9424
  }
8989
9425
  if (options.dryRun) {
@@ -8999,7 +9435,7 @@ async function runPluginSyncMigrations(name, options, deps) {
8999
9435
  );
9000
9436
  return;
9001
9437
  }
9002
- const relativePaths = generated.map((p) => relative4(options.cwd, p));
9438
+ const relativePaths = generated.map((p) => relative5(options.cwd, p));
9003
9439
  for (const p of relativePaths) {
9004
9440
  log.success(`Generated ${p}`);
9005
9441
  }
@@ -9019,18 +9455,18 @@ async function runPluginSyncMigrations(name, options, deps) {
9019
9455
  }
9020
9456
 
9021
9457
  // src/commands/plugin-uninstall.ts
9022
- import { existsSync as existsSync31, readFileSync as readFileSync23, rmSync as rmSync8 } from "fs";
9023
- import { join as join33, resolve as resolve15 } from "path";
9458
+ import { existsSync as existsSync34, readFileSync as readFileSync25, rmSync as rmSync9 } from "fs";
9459
+ import { join as join36, resolve as resolve16 } from "path";
9024
9460
  import chalk18 from "chalk";
9025
- import { Command as Command18 } from "commander";
9461
+ import { Command as Command19 } from "commander";
9026
9462
  import inquirer6 from "inquirer";
9027
9463
  var NAME_PATTERN2 = /^[a-z][a-z0-9-]*$/;
9028
- var pluginUninstallCommand = new Command18("uninstall").description("Remove an installed plugin: biffo plugin uninstall <name>").argument("<name>", "Plugin name").option("--dry-run", "Print planned changes without modifying the repo").option("--force", "Skip the confirmation prompt").option(
9464
+ var pluginUninstallCommand = new Command19("uninstall").description("Remove an installed plugin: biffo plugin uninstall <name>").argument("<name>", "Plugin name").option("--dry-run", "Print planned changes without modifying the repo").option("--force", "Skip the confirmation prompt").option(
9029
9465
  "--keep-data",
9030
9466
  "No-op today (see notes) \u2014 the CLI never drops plugin data regardless of this flag"
9031
9467
  ).option("--cwd <path>", "Project root to uninstall from (defaults to the current directory)").action(
9032
9468
  async (name, options) => {
9033
- const cwd = options.cwd ? resolve15(options.cwd) : process.cwd();
9469
+ const cwd = options.cwd ? resolve16(options.cwd) : process.cwd();
9034
9470
  try {
9035
9471
  await runPluginUninstall(
9036
9472
  name,
@@ -9052,16 +9488,16 @@ async function runPluginUninstall(name, options, deps) {
9052
9488
  if (!NAME_PATTERN2.test(name)) {
9053
9489
  throw new Error(`Invalid plugin name '${name}'. Expected a lowercase kebab-case slug.`);
9054
9490
  }
9055
- const servicesDir = join33(options.cwd, "services");
9056
- if (!existsSync31(servicesDir)) {
9491
+ const servicesDir = join36(options.cwd, "services");
9492
+ if (!existsSync34(servicesDir)) {
9057
9493
  throw new Error(
9058
9494
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
9059
9495
  );
9060
9496
  }
9061
- const targetDir = join33(servicesDir, name);
9062
- if (!existsSync31(targetDir)) {
9063
- const firstParty = join33(servicesDir, FIRST_PARTY_PLUGINS_DIR, name);
9064
- if (existsSync31(firstParty)) {
9497
+ const targetDir = join36(servicesDir, name);
9498
+ if (!existsSync34(targetDir)) {
9499
+ const firstParty = join36(servicesDir, FIRST_PARTY_PLUGINS_DIR, name);
9500
+ if (existsSync34(firstParty)) {
9065
9501
  throw new Error(
9066
9502
  `Plugin '${name}' is a first-party plugin at ${pluginDir(name, "first-party")}/, which is template-owned \u2014 \`biffo core upgrade\` would restore it on the next upgrade. Disable it instead by removing '${name}' from \`enabled_plugins\` in infra/environments/<env>/main.tf and re-applying.`
9067
9503
  );
@@ -9069,9 +9505,9 @@ async function runPluginUninstall(name, options, deps) {
9069
9505
  throw new Error(`Plugin '${name}' is not installed at services/${name}/.`);
9070
9506
  }
9071
9507
  const version = readInstalledVersion(targetDir);
9072
- const modulesDir = join33(options.cwd, "modules", "plugins", name);
9508
+ const modulesDir = join36(options.cwd, "modules", "plugins", name);
9073
9509
  const stagePaths = [`services/${name}`];
9074
- if (existsSync31(modulesDir)) {
9510
+ if (existsSync34(modulesDir)) {
9075
9511
  stagePaths.push(`modules/plugins/${name}`);
9076
9512
  }
9077
9513
  if (options.dryRun) {
@@ -9091,10 +9527,10 @@ async function runPluginUninstall(name, options, deps) {
9091
9527
  `${options.cwd} is not a git repository \u2014 biffo plugin uninstall must be run from a Biffo project checkout.`
9092
9528
  );
9093
9529
  }
9094
- rmSync8(targetDir, { recursive: true, force: true });
9530
+ rmSync9(targetDir, { recursive: true, force: true });
9095
9531
  log.success(`Removed services/${name}/`);
9096
- if (existsSync31(modulesDir)) {
9097
- rmSync8(modulesDir, { recursive: true, force: true });
9532
+ if (existsSync34(modulesDir)) {
9533
+ rmSync9(modulesDir, { recursive: true, force: true });
9098
9534
  log.success(`Removed modules/plugins/${name}/`);
9099
9535
  const wiring = syncPluginTerraform(options.cwd);
9100
9536
  stagePaths.push(...wiring.changedPaths);
@@ -9128,12 +9564,17 @@ async function runPluginUninstall(name, options, deps) {
9128
9564
  "Any tables this plugin created remain in the database, and its migration file at services/api/migrations/versions/ is NOT removed (it is a permanent historical record \u2014 see notes). Dropping tables, if desired, requires a manual Alembic migration written against the Core API."
9129
9565
  );
9130
9566
  }
9567
+ if (existsSync34(join36(options.cwd, pluginSeedImportDir(name)))) {
9568
+ log.warn(
9569
+ `${pluginSeedImportDir(name)}/ (this plugin's vendored baseline-row seed, biffo-template#1554) was NOT removed either, for the same reason \u2014 see notes. Delete it by hand if you are certain the rows it applied should go too, but note nothing drops rows already applied to the database; that still needs a manual migration.`
9570
+ );
9571
+ }
9131
9572
  }
9132
9573
  function readInstalledVersion(targetDir) {
9133
- const manifestPath = join33(targetDir, "biffo.plugin.json");
9134
- if (!existsSync31(manifestPath)) return void 0;
9574
+ const manifestPath = join36(targetDir, "biffo.plugin.json");
9575
+ if (!existsSync34(manifestPath)) return void 0;
9135
9576
  try {
9136
- return validateManifest(JSON.parse(readFileSync23(manifestPath, "utf8"))).version;
9577
+ return validateManifest(JSON.parse(readFileSync25(manifestPath, "utf8"))).version;
9137
9578
  } catch {
9138
9579
  return void 0;
9139
9580
  }
@@ -9166,12 +9607,12 @@ function printDryRun5(name, version, stagePaths, keepData) {
9166
9607
  }
9167
9608
 
9168
9609
  // src/commands/plugin-upgrade.ts
9169
- import { cpSync as cpSync5, existsSync as existsSync32, mkdirSync as mkdirSync11, readFileSync as readFileSync24, rmSync as rmSync9 } from "fs";
9170
- import { join as join34, relative as relative5, resolve as resolve16 } from "path";
9610
+ import { cpSync as cpSync6, existsSync as existsSync35, mkdirSync as mkdirSync12, readFileSync as readFileSync26, rmSync as rmSync10 } from "fs";
9611
+ import { join as join37, relative as relative6, resolve as resolve17 } from "path";
9171
9612
  import chalk19 from "chalk";
9172
- import { Command as Command19 } from "commander";
9613
+ import { Command as Command20 } from "commander";
9173
9614
  import inquirer7 from "inquirer";
9174
- var pluginUpgradeCommand = new Command19("upgrade").description(
9615
+ var pluginUpgradeCommand = new Command20("upgrade").description(
9175
9616
  "Upgrade an installed plugin to a new minor version (biffo plugin upgrade <name>@<new-minor>) or refresh it in place from a local, unpublished checkout (biffo plugin upgrade --local <path>)"
9176
9617
  ).argument(
9177
9618
  "[target]",
@@ -9181,12 +9622,12 @@ var pluginUpgradeCommand = new Command19("upgrade").description(
9181
9622
  "Refresh the installed plugin from a local, unpublished checkout instead of the registry"
9182
9623
  ).option("--dry-run", "Resolve the new version and print planned changes without applying them").option("--force", "Skip the confirmation prompt").option("--cwd <path>", "Project root to upgrade in (defaults to the current directory)").action(
9183
9624
  async (target, options) => {
9184
- const cwd = options.cwd ? resolve16(options.cwd) : process.cwd();
9625
+ const cwd = options.cwd ? resolve17(options.cwd) : process.cwd();
9185
9626
  try {
9186
9627
  await runPluginUpgrade(
9187
9628
  target,
9188
9629
  {
9189
- ...options.local ? { local: resolve16(options.local) } : {},
9630
+ ...options.local ? { local: resolve17(options.local) } : {},
9190
9631
  dryRun: options.dryRun ?? false,
9191
9632
  force: options.force ?? false,
9192
9633
  cwd
@@ -9214,8 +9655,8 @@ async function runPluginUpgrade(target, options, deps) {
9214
9655
  `Nothing to upgrade. Pass a registry target (e.g. \`biffo plugin upgrade acme-crm@1.1\`) or a local checkout to refresh from (\`biffo plugin upgrade --local ../acme-crm\`).`
9215
9656
  );
9216
9657
  }
9217
- const servicesDir = join34(options.cwd, "services");
9218
- if (!existsSync32(servicesDir)) {
9658
+ const servicesDir = join37(options.cwd, "services");
9659
+ if (!existsSync35(servicesDir)) {
9219
9660
  throw new Error(
9220
9661
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
9221
9662
  );
@@ -9224,8 +9665,8 @@ async function runPluginUpgrade(target, options, deps) {
9224
9665
  return runLocalPluginRefresh(options.local, options, deps);
9225
9666
  }
9226
9667
  const { name, minor } = parsePluginTarget(target);
9227
- const targetDir = join34(servicesDir, name);
9228
- if (!existsSync32(targetDir)) {
9668
+ const targetDir = join37(servicesDir, name);
9669
+ if (!existsSync35(targetDir)) {
9229
9670
  throw new Error(
9230
9671
  `Plugin '${name}' is not installed at services/${name}/. Use 'biffo plugin install ${name}@${minor}' instead.`
9231
9672
  );
@@ -9239,7 +9680,7 @@ async function runPluginUpgrade(target, options, deps) {
9239
9680
  `Plugin declares required_core_version '${entry.required_core_version}'. The CLI cannot verify this against your deployment \u2014 the Core API exposes no version endpoint and services/api/pyproject.toml's version is a static placeholder, not a real release marker. Confirm compatibility yourself before deploying.`
9240
9681
  );
9241
9682
  }
9242
- const modulesDir = join34(options.cwd, "modules", "plugins", entry.name);
9683
+ const modulesDir = join37(options.cwd, "modules", "plugins", entry.name);
9243
9684
  if (options.dryRun) {
9244
9685
  printDryRun6(entry, currentVersion);
9245
9686
  return;
@@ -9267,19 +9708,25 @@ async function runPluginUpgrade(target, options, deps) {
9267
9708
  log.success(
9268
9709
  `Manifest valid \u2014 ${manifest.tables.length} table(s), ${manifest.api_routes.length} route(s)`
9269
9710
  );
9270
- rmSync9(targetDir, { recursive: true, force: true });
9271
- mkdirSync11(targetDir, { recursive: true });
9272
- cpSync5(tmpDir, targetDir, { recursive: true });
9711
+ const previousProvenance = readProvenance(targetDir);
9712
+ rmSync10(targetDir, { recursive: true, force: true });
9713
+ mkdirSync12(targetDir, { recursive: true });
9714
+ cpSync6(tmpDir, targetDir, { recursive: true });
9273
9715
  log.success(`Upgraded plugin source at services/${entry.name}/`);
9716
+ const nextProvenance = resolveRegistryProvenance(
9717
+ entry.repo,
9718
+ await deps.git.resolveDefaultBranchSha(entry.repo)
9719
+ );
9720
+ writePluginProvenance(targetDir, reconcileProvenance(previousProvenance, nextProvenance));
9274
9721
  applyWorkspaceSources(targetDir, options.cwd, `services/${entry.name}`);
9275
9722
  const stagePaths = [`services/${entry.name}`];
9276
- if (existsSync32(modulesDir)) {
9277
- rmSync9(modulesDir, { recursive: true, force: true });
9723
+ if (existsSync35(modulesDir)) {
9724
+ rmSync10(modulesDir, { recursive: true, force: true });
9278
9725
  }
9279
- const tfSourceDir = join34(targetDir, "terraform");
9280
- if (existsSync32(tfSourceDir)) {
9281
- mkdirSync11(modulesDir, { recursive: true });
9282
- cpSync5(tfSourceDir, modulesDir, { recursive: true });
9726
+ const tfSourceDir = join37(targetDir, "terraform");
9727
+ if (existsSync35(tfSourceDir)) {
9728
+ mkdirSync12(modulesDir, { recursive: true });
9729
+ cpSync6(tfSourceDir, modulesDir, { recursive: true });
9283
9730
  stagePaths.push(`modules/plugins/${entry.name}`);
9284
9731
  log.success(`Copied Terraform module to modules/plugins/${entry.name}/`);
9285
9732
  }
@@ -9289,16 +9736,20 @@ async function runPluginUpgrade(target, options, deps) {
9289
9736
  );
9290
9737
  const generatedPaths = await deps.migrations.generate(options.cwd, [entry.name]);
9291
9738
  for (const absPath of generatedPaths) {
9292
- stagePaths.push(relative5(options.cwd, absPath));
9739
+ stagePaths.push(relative6(options.cwd, absPath));
9293
9740
  }
9294
9741
  if (generatedPaths.length > 0) {
9295
- log.success(`Generated migration: ${relative5(options.cwd, generatedPaths[0])}`);
9742
+ log.success(`Generated migration: ${relative6(options.cwd, generatedPaths[0])}`);
9296
9743
  } else {
9297
9744
  log.info(
9298
9745
  `${entry.name}'s tables and columns already match the manifest \u2014 no migration needed.`
9299
9746
  );
9300
9747
  }
9301
9748
  }
9749
+ const seedResult = vendorPluginSeed(targetDir, manifest, options.cwd);
9750
+ if (seedResult.vendored) {
9751
+ stagePaths.push(seedResult.stagedPath);
9752
+ }
9302
9753
  const label = currentVersion ? `${entry.name} ${currentVersion} -> ${entry.version}` : `${entry.name} to ${entry.version}`;
9303
9754
  const commitMessage = `feat(plugins): upgrade ${label}`;
9304
9755
  await deps.git.add(options.cwd, stagePaths);
@@ -9317,16 +9768,16 @@ async function runPluginUpgrade(target, options, deps) {
9317
9768
  async function runLocalPluginRefresh(localPath, options, deps) {
9318
9769
  const source = resolveLocalPlugin(localPath);
9319
9770
  log.success(`Resolved ${source.name}@${source.version} from ${source.origin}`);
9320
- const servicesDir = join34(options.cwd, "services");
9321
- const targetDir = join34(servicesDir, source.name);
9322
- if (!existsSync32(targetDir)) {
9771
+ const servicesDir = join37(options.cwd, "services");
9772
+ const targetDir = join37(servicesDir, source.name);
9773
+ if (!existsSync35(targetDir)) {
9323
9774
  throw new Error(
9324
9775
  `Plugin '${source.name}' is not installed at services/${source.name}/. Use 'biffo plugin install --local ${localPath}' instead.`
9325
9776
  );
9326
9777
  }
9327
- const inTreeSource = resolve16(source.sourceDir) === resolve16(targetDir);
9778
+ const inTreeSource = resolve17(source.sourceDir) === resolve17(targetDir);
9328
9779
  const currentVersion = readInstalledVersion2(targetDir);
9329
- const modulesDir = join34(options.cwd, "modules", "plugins", source.name);
9780
+ const modulesDir = join37(options.cwd, "modules", "plugins", source.name);
9330
9781
  if (options.dryRun) {
9331
9782
  printLocalDryRun(source, currentVersion, inTreeSource);
9332
9783
  return;
@@ -9349,25 +9800,28 @@ async function runLocalPluginRefresh(localPath, options, deps) {
9349
9800
  log.success(
9350
9801
  `Manifest valid \u2014 ${manifest.tables.length} table(s), ${manifest.api_routes.length} route(s)`
9351
9802
  );
9803
+ const previousProvenance = readProvenance(targetDir);
9352
9804
  if (inTreeSource) {
9353
9805
  log.info(
9354
9806
  `services/${source.name}/ is already the local checkout \u2014 nothing to copy; re-syncing its Terraform module and checking for a migration.`
9355
9807
  );
9356
9808
  } else {
9357
- rmSync9(targetDir, { recursive: true, force: true });
9358
- mkdirSync11(targetDir, { recursive: true });
9809
+ rmSync10(targetDir, { recursive: true, force: true });
9810
+ mkdirSync12(targetDir, { recursive: true });
9359
9811
  await copyPluginSource(source.sourceDir, targetDir);
9360
9812
  log.success(`Refreshed plugin source at services/${source.name}/ from ${source.origin}`);
9361
9813
  }
9814
+ const nextProvenance = inTreeSource ? inTreePluginProvenance(`services/${source.name}`) : await resolveLocalProvenance(source.sourceDir, source.origin);
9815
+ writePluginProvenance(targetDir, reconcileProvenance(previousProvenance, nextProvenance));
9362
9816
  applyWorkspaceSources(targetDir, options.cwd, `services/${source.name}`);
9363
9817
  const stagePaths = [`services/${source.name}`];
9364
- if (existsSync32(modulesDir)) {
9365
- rmSync9(modulesDir, { recursive: true, force: true });
9818
+ if (existsSync35(modulesDir)) {
9819
+ rmSync10(modulesDir, { recursive: true, force: true });
9366
9820
  }
9367
- const tfSourceDir = join34(targetDir, "terraform");
9368
- if (existsSync32(tfSourceDir)) {
9369
- mkdirSync11(modulesDir, { recursive: true });
9370
- cpSync5(tfSourceDir, modulesDir, { recursive: true });
9821
+ const tfSourceDir = join37(targetDir, "terraform");
9822
+ if (existsSync35(tfSourceDir)) {
9823
+ mkdirSync12(modulesDir, { recursive: true });
9824
+ cpSync6(tfSourceDir, modulesDir, { recursive: true });
9371
9825
  stagePaths.push(`modules/plugins/${source.name}`);
9372
9826
  log.success(`Refreshed Terraform module at modules/plugins/${source.name}/`);
9373
9827
  }
@@ -9377,10 +9831,10 @@ async function runLocalPluginRefresh(localPath, options, deps) {
9377
9831
  );
9378
9832
  const generatedPaths = await deps.migrations.generate(options.cwd, [source.name]);
9379
9833
  for (const absPath of generatedPaths) {
9380
- stagePaths.push(relative5(options.cwd, absPath));
9834
+ stagePaths.push(relative6(options.cwd, absPath));
9381
9835
  }
9382
9836
  if (generatedPaths.length > 0) {
9383
- log.success(`Generated migration: ${relative5(options.cwd, generatedPaths[0])}`);
9837
+ log.success(`Generated migration: ${relative6(options.cwd, generatedPaths[0])}`);
9384
9838
  } else {
9385
9839
  log.info(
9386
9840
  `${source.name}'s tables and columns already match the manifest \u2014 no migration needed.`
@@ -9389,6 +9843,10 @@ async function runLocalPluginRefresh(localPath, options, deps) {
9389
9843
  } else {
9390
9844
  log.info(`${source.name} declares no tables \u2014 nothing to migrate.`);
9391
9845
  }
9846
+ const seedResult = vendorPluginSeed(targetDir, manifest, options.cwd);
9847
+ if (seedResult.vendored) {
9848
+ stagePaths.push(seedResult.stagedPath);
9849
+ }
9392
9850
  await deps.git.add(options.cwd, stagePaths);
9393
9851
  if (!await deps.git.hasUncommittedChanges(options.cwd)) {
9394
9852
  log.warn(`services/${source.name}/ already matches ${source.origin} \u2014 nothing to commit.`);
@@ -9408,10 +9866,10 @@ async function runLocalPluginRefresh(localPath, options, deps) {
9408
9866
  }
9409
9867
  }
9410
9868
  function readInstalledVersion2(targetDir) {
9411
- const manifestPath = join34(targetDir, "biffo.plugin.json");
9412
- if (!existsSync32(manifestPath)) return void 0;
9869
+ const manifestPath = join37(targetDir, "biffo.plugin.json");
9870
+ if (!existsSync35(manifestPath)) return void 0;
9413
9871
  try {
9414
- return validateManifest(JSON.parse(readFileSync24(manifestPath, "utf8"))).version;
9872
+ return validateManifest(JSON.parse(readFileSync26(manifestPath, "utf8"))).version;
9415
9873
  } catch {
9416
9874
  return void 0;
9417
9875
  }
@@ -9446,6 +9904,11 @@ function printDryRun6(entry, currentVersion) {
9446
9904
  ` Would replace Terraform module at: modules/plugins/${entry.name}/ (if the repo has one)`
9447
9905
  );
9448
9906
  }
9907
+ if (entry.baseline_tables && entry.baseline_tables.length > 0) {
9908
+ console.log(
9909
+ ` Would re-vendor seed DDL into: ${pluginSeedImportDir(entry.name)}/ (baseline_tables: ${entry.baseline_tables.join(", ")})`
9910
+ );
9911
+ }
9449
9912
  console.log(` Would commit: feat(plugins): upgrade ${entry.name} to ${entry.version}
9450
9913
  `);
9451
9914
  }
@@ -9465,12 +9928,17 @@ function printLocalDryRun(source, currentVersion, inTreeSource) {
9465
9928
  ` Would check for a migration for ${source.manifest.tables.length} table(s) (generated for a new table or an added column on an already-migrated table; a removed/retyped/nullability-changed column stops the refresh instead \u2014 #1539)`
9466
9929
  );
9467
9930
  }
9931
+ if (source.manifest.seed) {
9932
+ console.log(
9933
+ ` Would re-vendor seed DDL into: ${pluginSeedImportDir(source.name)}/ (baseline_tables: ${source.manifest.seed.baseline_tables.join(", ") || "none declared"})`
9934
+ );
9935
+ }
9468
9936
  console.log(` Would commit: chore(plugins): refresh ${source.name} from local checkout
9469
9937
  `);
9470
9938
  }
9471
9939
 
9472
9940
  // src/commands/plugin.ts
9473
- var pluginCommand = new Command20("plugin").description("Manage Biffo plugins");
9941
+ var pluginCommand = new Command21("plugin").description("Manage Biffo plugins");
9474
9942
  pluginCommand.addCommand(pluginCreateCommand);
9475
9943
  pluginCommand.addCommand(pluginListCommand);
9476
9944
  pluginCommand.addCommand(pluginInstallCommand);
@@ -9478,15 +9946,16 @@ pluginCommand.addCommand(pluginUninstallCommand);
9478
9946
  pluginCommand.addCommand(pluginUpgradeCommand);
9479
9947
  pluginCommand.addCommand(pluginSyncMigrationsCommand);
9480
9948
  pluginCommand.addCommand(pluginInfoCommand);
9949
+ pluginCommand.addCommand(pluginStalenessCommand);
9481
9950
 
9482
9951
  // src/commands/sibling.ts
9483
- import { Command as Command22 } from "commander";
9952
+ import { Command as Command23 } from "commander";
9484
9953
 
9485
9954
  // src/commands/sibling-check-identity.ts
9486
- import { existsSync as existsSync33, readFileSync as readFileSync25 } from "fs";
9487
- import { resolve as resolve17 } from "path";
9955
+ import { existsSync as existsSync36, readFileSync as readFileSync27 } from "fs";
9956
+ import { resolve as resolve18 } from "path";
9488
9957
  import chalk20 from "chalk";
9489
- import { Command as Command21 } from "commander";
9958
+ import { Command as Command22 } from "commander";
9490
9959
 
9491
9960
  // src/lib/sibling-identity-check.ts
9492
9961
  function checkSiblingIdentity(envs) {
@@ -9538,7 +10007,7 @@ function checkSiblingIdentity(envs) {
9538
10007
  // src/commands/sibling-check-identity.ts
9539
10008
  var VALID_ENVIRONMENTS2 = ["dev", "staging", "prod"];
9540
10009
  var SIBLING_CORE_POOL_VAR = "CORE_COGNITO_USER_POOL_ID";
9541
- var siblingCheckIdentityCommand = new Command21("check-identity").description(
10010
+ var siblingCheckIdentityCommand = new Command22("check-identity").description(
9542
10011
  "Detect when a core's Cognito pool has drifted from its published identity document or any sibling's baked-in CORE_COGNITO_USER_POOL_ID (#400). Run from the core repo; exits non-zero on drift so a scheduled/CI run goes red."
9543
10012
  ).option("--env <environment>", "Only check this environment (default: dev, staging, prod)").option("-p, --project <name>", "Project name (overrides biffo.config.json in current directory)").option("-c, --config <path>", "Path to biffo.config.json").action(async (options) => {
9544
10013
  if (options.env && !VALID_ENVIRONMENTS2.includes(options.env)) {
@@ -9677,7 +10146,7 @@ async function fetchPublishedIdentity(portalUrl) {
9677
10146
  }
9678
10147
  async function resolveConfig4(options) {
9679
10148
  if (options.config) {
9680
- const raw = JSON.parse(readFileSync25(resolve17(options.config), "utf8"));
10149
+ const raw = JSON.parse(readFileSync27(resolve18(options.config), "utf8"));
9681
10150
  const result = BiffoConfigSchema.safeParse(raw);
9682
10151
  if (!result.success) {
9683
10152
  log.error(`Invalid config at ${options.config}:`);
@@ -9696,9 +10165,9 @@ async function resolveConfig4(options) {
9696
10165
  }
9697
10166
  return cfg;
9698
10167
  }
9699
- const localConfigPath = resolve17(process.cwd(), "biffo.config.json");
9700
- if (existsSync33(localConfigPath)) {
9701
- const raw = JSON.parse(readFileSync25(localConfigPath, "utf8"));
10168
+ const localConfigPath = resolve18(process.cwd(), "biffo.config.json");
10169
+ if (existsSync36(localConfigPath)) {
10170
+ const raw = JSON.parse(readFileSync27(localConfigPath, "utf8"));
9702
10171
  const result = BiffoConfigSchema.safeParse(raw);
9703
10172
  if (result.success) return result.data;
9704
10173
  if (isTemplatePlaceholderConfig(raw)) {
@@ -9734,31 +10203,31 @@ async function resolveConfig4(options) {
9734
10203
  }
9735
10204
 
9736
10205
  // src/commands/sibling.ts
9737
- var siblingCommand = new Command22("sibling").description(
10206
+ var siblingCommand = new Command23("sibling").description(
9738
10207
  "Create and manage sibling apps that share a Biffo core project (ADR-0007)"
9739
10208
  );
9740
10209
  siblingCommand.addCommand(siblingCreateCommand);
9741
10210
  siblingCommand.addCommand(siblingCheckIdentityCommand);
9742
10211
 
9743
10212
  // src/commands/check.ts
9744
- import { Command as Command23 } from "commander";
10213
+ import { Command as Command24 } from "commander";
9745
10214
 
9746
10215
  // src/scripts/check-adr-numbering.ts
9747
- import { existsSync as existsSync35 } from "fs";
9748
- import { join as join36 } from "path";
9749
- import { execa as execa6 } from "execa";
10216
+ import { existsSync as existsSync38 } from "fs";
10217
+ import { join as join39 } from "path";
10218
+ import { execa as execa7 } from "execa";
9750
10219
 
9751
10220
  // src/lib/adr-numbering-guard.ts
9752
- import { existsSync as existsSync34, readdirSync as readdirSync13, readFileSync as readFileSync26 } from "fs";
9753
- import { join as join35 } from "path";
10221
+ import { existsSync as existsSync37, readdirSync as readdirSync15, readFileSync as readFileSync28 } from "fs";
10222
+ import { join as join38 } from "path";
9754
10223
  var ADR_FILENAME = /^(\d{4})-.+\.md$/;
9755
10224
  var ALLOWLIST_FILENAME = ".numbering-allowlist";
9756
10225
  var TEMPLATE_ADR_RESERVED_UPTO = "0099";
9757
10226
  function readAdrNumberingAllowlist(adrDir) {
9758
- const path = join35(adrDir, ALLOWLIST_FILENAME);
9759
- if (!existsSync34(path)) return /* @__PURE__ */ new Set();
10227
+ const path = join38(adrDir, ALLOWLIST_FILENAME);
10228
+ if (!existsSync37(path)) return /* @__PURE__ */ new Set();
9760
10229
  const numbers = /* @__PURE__ */ new Set();
9761
- for (const rawLine of readFileSync26(path, "utf8").split("\n")) {
10230
+ for (const rawLine of readFileSync28(path, "utf8").split("\n")) {
9762
10231
  const line = rawLine.split("#")[0].trim();
9763
10232
  if (line) numbers.add(line);
9764
10233
  }
@@ -9766,8 +10235,8 @@ function readAdrNumberingAllowlist(adrDir) {
9766
10235
  }
9767
10236
  function adrNumbersIn(adrDir) {
9768
10237
  const claims = /* @__PURE__ */ new Map();
9769
- if (!existsSync34(adrDir)) return claims;
9770
- for (const entry of readdirSync13(adrDir).sort()) {
10238
+ if (!existsSync37(adrDir)) return claims;
10239
+ for (const entry of readdirSync15(adrDir).sort()) {
9771
10240
  const match = ADR_FILENAME.exec(entry);
9772
10241
  if (!match) continue;
9773
10242
  const number = match[1];
@@ -9820,9 +10289,9 @@ function formatAdrReservedRangeViolations(violations, reservedUpTo = TEMPLATE_AD
9820
10289
 
9821
10290
  // src/scripts/check-adr-numbering.ts
9822
10291
  async function runAdrNumberingCheck() {
9823
- const root = (await execa6("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9824
- const adrDir = join36(root, "docs", "ADR");
9825
- if (!existsSync35(adrDir)) {
10292
+ const root = (await execa7("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10293
+ const adrDir = join39(root, "docs", "ADR");
10294
+ if (!existsSync38(adrDir)) {
9826
10295
  console.log("\u2713 ADR numbering guard: no docs/ADR/ directory \u2014 nothing to compare");
9827
10296
  return;
9828
10297
  }
@@ -9861,7 +10330,7 @@ Already accepted? List it in docs/ADR/${ALLOWLIST_FILENAME} instead of leaving t
9861
10330
 
9862
10331
  // src/scripts/check-branch-protection.ts
9863
10332
  import { Octokit as Octokit2 } from "@octokit/rest";
9864
- import { execa as execa7 } from "execa";
10333
+ import { execa as execa8 } from "execa";
9865
10334
 
9866
10335
  // src/lib/branch-protection-apply.ts
9867
10336
  var CONTEXT_CONSISTENCY_THRESHOLD = 2 / 3;
@@ -9988,7 +10457,7 @@ async function resolveRepo(explicit) {
9988
10457
  }
9989
10458
  return { owner, repo };
9990
10459
  }
9991
- const { stdout } = await execa7("git", ["remote", "get-url", "origin"]);
10460
+ const { stdout } = await execa8("git", ["remote", "get-url", "origin"]);
9992
10461
  const m = /github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/.exec(stdout.trim());
9993
10462
  if (!m?.[1] || !m[2]) {
9994
10463
  console.error(
@@ -10120,13 +10589,13 @@ async function runBranchProtectionCheck(explicitRepo, options = {}) {
10120
10589
  }
10121
10590
 
10122
10591
  // src/scripts/check-codeql-suppression.ts
10123
- import { existsSync as existsSync36 } from "fs";
10124
- import { join as join38, relative as relative6 } from "path";
10125
- import { execa as execa8 } from "execa";
10592
+ import { existsSync as existsSync39 } from "fs";
10593
+ import { join as join41, relative as relative7 } from "path";
10594
+ import { execa as execa9 } from "execa";
10126
10595
 
10127
10596
  // src/lib/codeql-suppression-guard.ts
10128
- import { readdirSync as readdirSync14, readFileSync as readFileSync27, statSync as statSync7 } from "fs";
10129
- import { join as join37 } from "path";
10597
+ import { readdirSync as readdirSync16, readFileSync as readFileSync29, statSync as statSync8 } from "fs";
10598
+ import { join as join40 } from "path";
10130
10599
  var SKIP_DIRS = /* @__PURE__ */ new Set([
10131
10600
  ".git",
10132
10601
  ".worktrees",
@@ -10152,15 +10621,15 @@ function walkSourceFiles(root) {
10152
10621
  const walk2 = (dir) => {
10153
10622
  let entries;
10154
10623
  try {
10155
- entries = readdirSync14(dir);
10624
+ entries = readdirSync16(dir);
10156
10625
  } catch {
10157
10626
  return;
10158
10627
  }
10159
10628
  for (const entry of entries) {
10160
- const p = join37(dir, entry);
10629
+ const p = join40(dir, entry);
10161
10630
  let st;
10162
10631
  try {
10163
- st = statSync7(p);
10632
+ st = statSync8(p);
10164
10633
  } catch {
10165
10634
  continue;
10166
10635
  }
@@ -10183,7 +10652,7 @@ function countSourceFiles(root) {
10183
10652
  function sweepCodeqlSuppressionComments(root) {
10184
10653
  const hits = [];
10185
10654
  for (const path of walkSourceFiles(root)) {
10186
- const text = readFileSync27(path, "utf8");
10655
+ const text = readFileSync29(path, "utf8");
10187
10656
  for (const line of findCodeqlSuppressionComments(text)) {
10188
10657
  hits.push({ path, line, text: text.split("\n")[line - 1] ?? "" });
10189
10658
  }
@@ -10193,9 +10662,9 @@ function sweepCodeqlSuppressionComments(root) {
10193
10662
 
10194
10663
  // src/scripts/check-codeql-suppression.ts
10195
10664
  async function runCodeqlSuppressionCheck() {
10196
- const root = (await execa8("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10197
- const scanRoot = join38(root, "cli", "src");
10198
- if (!existsSync36(scanRoot)) {
10665
+ const root = (await execa9("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10666
+ const scanRoot = join41(root, "cli", "src");
10667
+ if (!existsSync39(scanRoot)) {
10199
10668
  console.log(
10200
10669
  "\u2014 codeql-suppression guard: skipped \u2014 no cli/src in this repo, so there is no CLI source to scan."
10201
10670
  );
@@ -10207,7 +10676,7 @@ async function runCodeqlSuppressionCheck() {
10207
10676
  "\u2717 codeql-suppression guard: found a `codeql[...]`-shaped comment, which does not suppress anything in this repo (#1491) \u2014 dismiss the real alert instead (UI, or `PATCH .../code-scanning/alerts/<n>` with a recorded reason)\n"
10208
10677
  );
10209
10678
  for (const hit of hits) {
10210
- console.error(` ${relative6(root, hit.path)}:${hit.line} ${hit.text.trim()}`);
10679
+ console.error(` ${relative7(root, hit.path)}:${hit.line} ${hit.text.trim()}`);
10211
10680
  }
10212
10681
  process.exit(1);
10213
10682
  }
@@ -10219,11 +10688,11 @@ async function runCodeqlSuppressionCheck() {
10219
10688
  }
10220
10689
 
10221
10690
  // src/scripts/check-cognito-invite-template.ts
10222
- import { execa as execa9 } from "execa";
10691
+ import { execa as execa10 } from "execa";
10223
10692
 
10224
10693
  // src/lib/cognito-invite-template-guard.ts
10225
- import { readdirSync as readdirSync15, readFileSync as readFileSync28, statSync as statSync8 } from "fs";
10226
- import { join as join39 } from "path";
10694
+ import { readdirSync as readdirSync17, readFileSync as readFileSync30, statSync as statSync9 } from "fs";
10695
+ import { join as join42 } from "path";
10227
10696
  var REQUIRED_INVITE_MEMBERS = ["email_subject", "email_message", "sms_message"];
10228
10697
  var REQUIRED_INVITE_PLACEHOLDERS = ["{username}", "{####}"];
10229
10698
  var PLACEHOLDER_MEMBERS = ["email_message", "sms_message"];
@@ -10297,36 +10766,36 @@ function memberBody(blockBody, member) {
10297
10766
  }
10298
10767
  function findModuleTerraformFiles(repoRoot) {
10299
10768
  const found = [];
10300
- const walk2 = (dir, relative9) => {
10769
+ const walk2 = (dir, relative10) => {
10301
10770
  let entries;
10302
10771
  try {
10303
- entries = readdirSync15(dir);
10772
+ entries = readdirSync17(dir);
10304
10773
  } catch {
10305
10774
  return;
10306
10775
  }
10307
10776
  for (const entry of entries) {
10308
10777
  if (entry === "node_modules" || entry === ".git" || entry === ".worktrees") continue;
10309
- const full = join39(dir, entry);
10310
- const rel = `${relative9}/${entry}`;
10311
- if (statSync8(full).isDirectory()) {
10778
+ const full = join42(dir, entry);
10779
+ const rel = `${relative10}/${entry}`;
10780
+ if (statSync9(full).isDirectory()) {
10312
10781
  walk2(full, rel);
10313
10782
  } else if (entry.endsWith(".tf")) {
10314
10783
  found.push(rel);
10315
10784
  }
10316
10785
  }
10317
10786
  };
10318
- walk2(join39(repoRoot, "modules"), "modules");
10787
+ walk2(join42(repoRoot, "modules"), "modules");
10319
10788
  return found.sort();
10320
10789
  }
10321
10790
  function checkCognitoInviteTemplates(repoRoot) {
10322
10791
  return findModuleTerraformFiles(repoRoot).flatMap(
10323
- (file) => checkInviteTemplateSource(file, readFileSync28(join39(repoRoot, file), "utf8"))
10792
+ (file) => checkInviteTemplateSource(file, readFileSync30(join42(repoRoot, file), "utf8"))
10324
10793
  );
10325
10794
  }
10326
10795
 
10327
10796
  // src/scripts/check-cognito-invite-template.ts
10328
10797
  async function runCognitoInviteTemplateCheck() {
10329
- const root = (await execa9("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10798
+ const root = (await execa10("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10330
10799
  const files = findModuleTerraformFiles(root);
10331
10800
  console.log(`audited ${files.length} .tf file(s) under modules/ under ${root}`);
10332
10801
  if (files.length === 0) {
@@ -10348,12 +10817,12 @@ async function runCognitoInviteTemplateCheck() {
10348
10817
  }
10349
10818
 
10350
10819
  // src/scripts/check-core-direct-paths.ts
10351
- import { join as join41 } from "path";
10352
- import { execa as execa10 } from "execa";
10820
+ import { join as join44 } from "path";
10821
+ import { execa as execa11 } from "execa";
10353
10822
 
10354
10823
  // src/lib/core-direct-paths-audit.ts
10355
- import { existsSync as existsSync37, readFileSync as readFileSync29, readdirSync as readdirSync16, statSync as statSync9 } from "fs";
10356
- import { join as join40 } from "path";
10824
+ import { existsSync as existsSync40, readFileSync as readFileSync31, readdirSync as readdirSync18, statSync as statSync10 } from "fs";
10825
+ import { join as join43 } from "path";
10357
10826
  var EXTERNAL_BASE_IDENTIFIERS = ["CORE_API_URL"];
10358
10827
  var API_ROUTE_PREFIX = "/api/v1";
10359
10828
  var TEST_FILE_SUFFIXES = [".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx"];
@@ -10512,15 +10981,15 @@ function walkFiles(root, accept, skipDir) {
10512
10981
  const walk2 = (dir) => {
10513
10982
  let entries;
10514
10983
  try {
10515
- entries = readdirSync16(dir);
10984
+ entries = readdirSync18(dir);
10516
10985
  } catch {
10517
10986
  return;
10518
10987
  }
10519
10988
  for (const entry of entries) {
10520
- const p = join40(dir, entry);
10989
+ const p = join43(dir, entry);
10521
10990
  let st;
10522
10991
  try {
10523
- st = statSync9(p);
10992
+ st = statSync10(p);
10524
10993
  } catch {
10525
10994
  continue;
10526
10995
  }
@@ -10547,7 +11016,7 @@ function auditFrontendExtraction(frontendSrcDir, externalBases = EXTERNAL_BASE_I
10547
11016
  const extracted = [];
10548
11017
  let rawTotal = 0;
10549
11018
  for (const file of files) {
10550
- const text = readFileSync29(file, "utf8");
11019
+ const text = readFileSync31(file, "utf8");
10551
11020
  rawTotal += countRawExternalOccurrences(text, externalBases);
10552
11021
  extracted.push(...extractCoreDirectPaths(text, file, externalBases));
10553
11022
  }
@@ -10596,7 +11065,7 @@ function auditCoreRouteExtraction(apiSrcDir) {
10596
11065
  const prefixSet = /* @__PURE__ */ new Set();
10597
11066
  let rawApiRouterCount = 0;
10598
11067
  for (const file of files) {
10599
- const text = readFileSync29(file, "utf8");
11068
+ const text = readFileSync31(file, "utf8");
10600
11069
  const extraction = extractCoreRoutePrefixes(text);
10601
11070
  rawApiRouterCount += extraction.rawApiRouterCount;
10602
11071
  for (const p of extraction.prefixes) prefixSet.add(normalizePrefix(p));
@@ -10612,10 +11081,10 @@ function pathMatchesAnyCorePrefix(normalized, corePrefixes, apiRoutePrefix = API
10612
11081
  }
10613
11082
  function resolveSiblingCoreSrc(params) {
10614
11083
  const { estateDir, sibling } = params;
10615
- const configPath = join40(estateDir, sibling, "biffo.sibling.json");
11084
+ const configPath = join43(estateDir, sibling, "biffo.sibling.json");
10616
11085
  let raw;
10617
11086
  try {
10618
- raw = readFileSync29(configPath, "utf8");
11087
+ raw = readFileSync31(configPath, "utf8");
10619
11088
  } catch (err) {
10620
11089
  throw new Error(
10621
11090
  `cannot resolve ${sibling}'s core: ${configPath} does not exist or is unreadable (${err.message}) -- refusing to guess which core serves this sibling.`
@@ -10635,8 +11104,8 @@ function resolveSiblingCoreSrc(params) {
10635
11104
  `cannot resolve ${sibling}'s core: ${configPath} has no non-empty "core_project" field.`
10636
11105
  );
10637
11106
  }
10638
- const coreApiSrcDir = join40(estateDir, coreProject, "services", "api", "src");
10639
- if (!existsSync37(coreApiSrcDir)) {
11107
+ const coreApiSrcDir = join43(estateDir, coreProject, "services", "api", "src");
11108
+ if (!existsSync40(coreApiSrcDir)) {
10640
11109
  throw new Error(
10641
11110
  `cannot resolve ${sibling}'s core: biffo.sibling.json names core_project "${coreProject}", but ${coreApiSrcDir} does not exist -- the instance is missing from this estate checkout, not merely unmatched. Refusing to silently skip ${sibling} and shrink the audit's denominator.`
10642
11111
  );
@@ -10677,9 +11146,9 @@ function auditSiblingCoreDirectPaths(params) {
10677
11146
 
10678
11147
  // src/scripts/check-core-direct-paths.ts
10679
11148
  async function runCoreDirectPathsCheck(opts = {}) {
10680
- const root = (await execa10("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11149
+ const root = (await execa11("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10681
11150
  const sibling = opts.sibling ?? "sibling-template (self-check)";
10682
- const frontendSrcDir = opts.frontendSrc ?? join41(root, "_skeletons", "sibling-template", "apps", "frontend", "src");
11151
+ const frontendSrcDir = opts.frontendSrc ?? join44(root, "_skeletons", "sibling-template", "apps", "frontend", "src");
10683
11152
  let coreApiSrcDir;
10684
11153
  let coreProject = null;
10685
11154
  if (opts.coreSrc) {
@@ -10695,7 +11164,7 @@ async function runCoreDirectPathsCheck(opts = {}) {
10695
11164
  coreApiSrcDir = resolution.coreApiSrcDir;
10696
11165
  coreProject = resolution.coreProject;
10697
11166
  } else {
10698
- coreApiSrcDir = join41(root, "services", "api", "src");
11167
+ coreApiSrcDir = join44(root, "services", "api", "src");
10699
11168
  }
10700
11169
  const report = auditSiblingCoreDirectPaths({ sibling, frontendSrcDir, coreApiSrcDir });
10701
11170
  console.log(
@@ -10731,7 +11200,7 @@ async function runCoreDirectPathsCheck(opts = {}) {
10731
11200
  }
10732
11201
 
10733
11202
  // src/scripts/check-core-ownership.ts
10734
- import { execa as execa11 } from "execa";
11203
+ import { execa as execa12 } from "execa";
10735
11204
  var BOLD = "\x1B[1m";
10736
11205
  var DIM = "\x1B[2m";
10737
11206
  var RED = "\x1B[31m";
@@ -10742,7 +11211,7 @@ async function runOwnershipCheck(argv) {
10742
11211
  const stagedFlag = args.indexOf("--staged");
10743
11212
  const staged = stagedFlag !== -1;
10744
11213
  const messageFile = staged ? args[stagedFlag + 1] : void 0;
10745
- const root = (await execa11("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11214
+ const root = (await execa12("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10746
11215
  const ownership = classifyRepoOwnership(root);
10747
11216
  if (ownership === "template") {
10748
11217
  console.log("\u2713 core ownership guard: skipped \u2014 this is the template, which owns these paths.");
@@ -10758,11 +11227,11 @@ async function runOwnershipCheck(argv) {
10758
11227
  let deletedFiles = [];
10759
11228
  let commitMessage = "";
10760
11229
  if (staged) {
10761
- const { stdout } = await execa11("git", ["diff", "--cached", "--name-status"], { cwd: root });
11230
+ const { stdout } = await execa12("git", ["diff", "--cached", "--name-status"], { cwd: root });
10762
11231
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
10763
11232
  if (messageFile) {
10764
- const { readFileSync: readFileSync39, existsSync: existsSync46 } = await import("fs");
10765
- if (existsSync46(messageFile)) commitMessage = readFileSync39(messageFile, "utf8");
11233
+ const { readFileSync: readFileSync41, existsSync: existsSync49 } = await import("fs");
11234
+ if (existsSync49(messageFile)) commitMessage = readFileSync41(messageFile, "utf8");
10766
11235
  }
10767
11236
  } else {
10768
11237
  const base = process.env["GITHUB_BASE_REF"] ?? args[0];
@@ -10770,18 +11239,18 @@ async function runOwnershipCheck(argv) {
10770
11239
  console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
10771
11240
  process.exit(2);
10772
11241
  }
10773
- await execa11("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
10774
- const { stdout } = await execa11("git", ["diff", "--name-status", `origin/${base}...HEAD`], {
11242
+ await execa12("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
11243
+ const { stdout } = await execa12("git", ["diff", "--name-status", `origin/${base}...HEAD`], {
10775
11244
  cwd: root
10776
11245
  });
10777
11246
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
10778
- const { stdout: log2 } = await execa11("git", ["log", "--format=%B", `origin/${base}..HEAD`], {
11247
+ const { stdout: log2 } = await execa12("git", ["log", "--format=%B", `origin/${base}..HEAD`], {
10779
11248
  cwd: root,
10780
11249
  reject: false
10781
11250
  });
10782
11251
  commitMessage = log2;
10783
11252
  }
10784
- const { stdout: gitBranch } = await execa11("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
11253
+ const { stdout: gitBranch } = await execa12("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
10785
11254
  cwd: root,
10786
11255
  reject: false
10787
11256
  });
@@ -10863,11 +11332,11 @@ ${BOLD}If the divergence is deliberate${OFF}
10863
11332
  }
10864
11333
 
10865
11334
  // src/scripts/check-eventbridge-log-permissions.ts
10866
- import { execa as execa12 } from "execa";
11335
+ import { execa as execa13 } from "execa";
10867
11336
 
10868
11337
  // src/lib/eventbridge-log-permission-guard.ts
10869
- import { readFileSync as readFileSync30, readdirSync as readdirSync17, statSync as statSync10 } from "fs";
10870
- import { join as join42 } from "path";
11338
+ import { readFileSync as readFileSync32, readdirSync as readdirSync19, statSync as statSync11 } from "fs";
11339
+ import { join as join45 } from "path";
10871
11340
  var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", ".terraform", ".worktrees", "dist"]);
10872
11341
  var EVENT_TARGET_TYPE = "aws_cloudwatch_event_target";
10873
11342
  var LOG_RESOURCE_POLICY_TYPE = "aws_cloudwatch_log_resource_policy";
@@ -10939,15 +11408,15 @@ function walkTerraformFiles(root) {
10939
11408
  const walk2 = (dir) => {
10940
11409
  let entries;
10941
11410
  try {
10942
- entries = readdirSync17(dir);
11411
+ entries = readdirSync19(dir);
10943
11412
  } catch {
10944
11413
  return;
10945
11414
  }
10946
11415
  for (const entry of entries) {
10947
- const p = join42(dir, entry);
11416
+ const p = join45(dir, entry);
10948
11417
  let st;
10949
11418
  try {
10950
- st = statSync10(p);
11419
+ st = statSync11(p);
10951
11420
  } catch {
10952
11421
  continue;
10953
11422
  }
@@ -10987,7 +11456,7 @@ function auditEventBridgeLogPermissions(root) {
10987
11456
  let rawEventTargetCount = 0;
10988
11457
  let rawLogPolicyCount = 0;
10989
11458
  for (const file of files) {
10990
- const text = readFileSync30(file, "utf8");
11459
+ const text = readFileSync32(file, "utf8");
10991
11460
  rawEventTargetCount += countRawResourceDeclarations(text, EVENT_TARGET_TYPE);
10992
11461
  rawLogPolicyCount += countRawResourceDeclarations(text, LOG_RESOURCE_POLICY_TYPE);
10993
11462
  eventTargetBlocks.push(...findResourceBlocks(text, file, EVENT_TARGET_TYPE));
@@ -11040,7 +11509,7 @@ function auditEventBridgeLogPermissions(root) {
11040
11509
 
11041
11510
  // src/scripts/check-eventbridge-log-permissions.ts
11042
11511
  async function runEventBridgeLogPermissionCheck() {
11043
- const root = (await execa12("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11512
+ const root = (await execa13("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11044
11513
  let report;
11045
11514
  try {
11046
11515
  report = auditEventBridgeLogPermissions(root);
@@ -11077,15 +11546,15 @@ async function runEventBridgeLogPermissionCheck() {
11077
11546
  }
11078
11547
 
11079
11548
  // src/scripts/check-lambda-output.ts
11080
- import { execa as execa13 } from "execa";
11549
+ import { execa as execa14 } from "execa";
11081
11550
 
11082
11551
  // src/lib/lambda-output-guard.ts
11083
- import { readFileSync as readFileSync32 } from "fs";
11084
- import { join as join44 } from "path";
11552
+ import { readFileSync as readFileSync34 } from "fs";
11553
+ import { join as join47 } from "path";
11085
11554
 
11086
11555
  // src/lib/terraform-input-guard.ts
11087
- import { readdirSync as readdirSync18, readFileSync as readFileSync31, statSync as statSync11 } from "fs";
11088
- import { join as join43 } from "path";
11556
+ import { readdirSync as readdirSync20, readFileSync as readFileSync33, statSync as statSync12 } from "fs";
11557
+ import { join as join46 } from "path";
11089
11558
  var GUARDED_SUBCOMMANDS = [
11090
11559
  "init",
11091
11560
  "plan",
@@ -11100,20 +11569,20 @@ function stripComments2(source) {
11100
11569
  }
11101
11570
  function findWorkflowFiles(repoRoot) {
11102
11571
  const found = [];
11103
- const walk2 = (dir, relative9) => {
11572
+ const walk2 = (dir, relative10) => {
11104
11573
  let entries;
11105
11574
  try {
11106
- entries = readdirSync18(dir);
11575
+ entries = readdirSync20(dir);
11107
11576
  } catch {
11108
11577
  return;
11109
11578
  }
11110
11579
  for (const entry of entries) {
11111
11580
  if (entry === "node_modules" || entry === ".git" || entry === ".worktrees") continue;
11112
- const full = join43(dir, entry);
11113
- const rel = relative9 ? `${relative9}/${entry}` : entry;
11114
- if (statSync11(full).isDirectory()) {
11581
+ const full = join46(dir, entry);
11582
+ const rel = relative10 ? `${relative10}/${entry}` : entry;
11583
+ if (statSync12(full).isDirectory()) {
11115
11584
  walk2(full, rel);
11116
- } else if (/\.ya?ml$/.test(entry) && relative9.endsWith(".github/workflows")) {
11585
+ } else if (/\.ya?ml$/.test(entry) && relative10.endsWith(".github/workflows")) {
11117
11586
  found.push(rel);
11118
11587
  }
11119
11588
  }
@@ -11153,7 +11622,7 @@ function checkWorkflowSource(file, rawSource) {
11153
11622
  }
11154
11623
  function checkTerraformInput(repoRoot) {
11155
11624
  return findWorkflowFiles(repoRoot).flatMap(
11156
- (file) => checkWorkflowSource(file, readFileSync31(join43(repoRoot, file), "utf8"))
11625
+ (file) => checkWorkflowSource(file, readFileSync33(join46(repoRoot, file), "utf8"))
11157
11626
  );
11158
11627
  }
11159
11628
 
@@ -11211,13 +11680,13 @@ function checkWorkflowSource2(file, rawSource) {
11211
11680
  }
11212
11681
  function checkLambdaOutput(repoRoot) {
11213
11682
  return findWorkflowFiles(repoRoot).flatMap(
11214
- (file) => checkWorkflowSource2(file, readFileSync32(join44(repoRoot, file), "utf8"))
11683
+ (file) => checkWorkflowSource2(file, readFileSync34(join47(repoRoot, file), "utf8"))
11215
11684
  );
11216
11685
  }
11217
11686
 
11218
11687
  // src/scripts/check-lambda-output.ts
11219
11688
  async function runLambdaOutputCheck() {
11220
- const root = (await execa13("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11689
+ const root = (await execa14("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11221
11690
  const files = findWorkflowFiles(root);
11222
11691
  console.log(`audited ${files.length} workflow file(s) under ${root}`);
11223
11692
  if (files.length === 0) {
@@ -11239,9 +11708,9 @@ async function runLambdaOutputCheck() {
11239
11708
  }
11240
11709
 
11241
11710
  // src/scripts/check-pipe-trap.ts
11242
- import { readFileSync as readFileSync33, readdirSync as readdirSync19 } from "fs";
11243
- import { join as join45, relative as relative7 } from "path";
11244
- import { execa as execa14 } from "execa";
11711
+ import { readFileSync as readFileSync35, readdirSync as readdirSync21 } from "fs";
11712
+ import { join as join48, relative as relative8 } from "path";
11713
+ import { execa as execa15 } from "execa";
11245
11714
 
11246
11715
  // src/lib/pipe-trap-guard.ts
11247
11716
  var STATUS_BEARING = [
@@ -11337,23 +11806,23 @@ function findPipeTraps(source) {
11337
11806
  function shellFiles(root) {
11338
11807
  const out = [];
11339
11808
  for (const dir of ["scripts", ".githooks"]) {
11340
- const full = join45(root, dir);
11809
+ const full = join48(root, dir);
11341
11810
  let entries;
11342
11811
  try {
11343
- entries = readdirSync19(full, { withFileTypes: true });
11812
+ entries = readdirSync21(full, { withFileTypes: true });
11344
11813
  } catch {
11345
11814
  continue;
11346
11815
  }
11347
11816
  for (const entry of entries) {
11348
11817
  if (!entry.isFile()) continue;
11349
11818
  if (dir === "scripts" && !entry.name.endsWith(".sh")) continue;
11350
- out.push(join45(full, entry.name));
11819
+ out.push(join48(full, entry.name));
11351
11820
  }
11352
11821
  }
11353
11822
  return out;
11354
11823
  }
11355
11824
  async function runPipeTrapCheck() {
11356
- const root = (await execa14("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11825
+ const root = (await execa15("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11357
11826
  const files = shellFiles(root);
11358
11827
  console.log(`audited ${files.length} shell file(s) under scripts/ and .githooks/ under ${root}`);
11359
11828
  if (files.length === 0) {
@@ -11363,8 +11832,8 @@ async function runPipeTrapCheck() {
11363
11832
  process.exit(1);
11364
11833
  }
11365
11834
  const findings = files.flatMap(
11366
- (file) => findPipeTraps(readFileSync33(file, "utf8")).map(
11367
- (t) => `${relative7(root, file)}:${t.line} ${t.text}
11835
+ (file) => findPipeTraps(readFileSync35(file, "utf8")).map(
11836
+ (t) => `${relative8(root, file)}:${t.line} ${t.text}
11368
11837
  ${t.reason}`
11369
11838
  )
11370
11839
  );
@@ -11380,11 +11849,11 @@ async function runPipeTrapCheck() {
11380
11849
  }
11381
11850
 
11382
11851
  // src/scripts/check-plugin-allowlist-convention.ts
11383
- import { execa as execa15 } from "execa";
11852
+ import { execa as execa16 } from "execa";
11384
11853
 
11385
11854
  // src/lib/plugin-allowlist-convention.ts
11386
- import { readFileSync as readFileSync34 } from "fs";
11387
- import { join as join46 } from "path";
11855
+ import { readFileSync as readFileSync36 } from "fs";
11856
+ import { join as join49 } from "path";
11388
11857
  var COMPUTE_MAIN_TF = "modules/cloud/aws/compute/main.tf";
11389
11858
  var PLUGIN_TEMPLATE_MAIN_TF = "modules/plugins/_template/main.tf";
11390
11859
  var ALLOWLIST_MAIN_TF = "modules/cloud/aws/plugin-allowlist/main.tf";
@@ -11393,18 +11862,18 @@ var PROJECT = "<project>";
11393
11862
  var ENV = "<env>";
11394
11863
  var PLUGIN = "<plugin>";
11395
11864
  var ACCOUNT = "<account>";
11396
- function read(repoRoot, relative9) {
11865
+ function read(repoRoot, relative10) {
11397
11866
  try {
11398
- return readFileSync34(join46(repoRoot, relative9), "utf8");
11867
+ return readFileSync36(join49(repoRoot, relative10), "utf8");
11399
11868
  } catch {
11400
- throw new Error(`plugin-allowlist drift guard: cannot read ${relative9}`);
11869
+ throw new Error(`plugin-allowlist drift guard: cannot read ${relative10}`);
11401
11870
  }
11402
11871
  }
11403
11872
  function assignedString(source, name) {
11404
11873
  const match = new RegExp(`^\\s*${name}\\s*=\\s*"((?:[^"\\\\]|\\\\.)*)"\\s*$`, "m").exec(source);
11405
11874
  return match?.[1];
11406
11875
  }
11407
- function resolve18(expression, bindings) {
11876
+ function resolve19(expression, bindings) {
11408
11877
  let current = expression;
11409
11878
  for (let pass = 0; pass < 10; pass += 1) {
11410
11879
  const next = current.replace(/\$\{([^}]+)\}/g, (whole, ref) => {
@@ -11441,13 +11910,13 @@ function composeExpectedRoleName(repoRoot) {
11441
11910
  "var.project_name": PROJECT,
11442
11911
  "var.environment": ENV,
11443
11912
  "var.plugin_name": PLUGIN,
11444
- "var.function_name": resolve18(pluginFunctionName, {
11913
+ "var.function_name": resolve19(pluginFunctionName, {
11445
11914
  "var.plugin_name": PLUGIN
11446
11915
  })
11447
11916
  };
11448
- bindings["local.name_prefix"] = resolve18(namePrefix, bindings);
11449
- bindings["local.function_name"] = resolve18(functionName, bindings);
11450
- return resolve18(roleName, bindings);
11917
+ bindings["local.name_prefix"] = resolve19(namePrefix, bindings);
11918
+ bindings["local.function_name"] = resolve19(functionName, bindings);
11919
+ return resolve19(roleName, bindings);
11451
11920
  }
11452
11921
  function readAllowlistGlob(repoRoot) {
11453
11922
  const allowlist = read(repoRoot, ALLOWLIST_MAIN_TF);
@@ -11457,7 +11926,7 @@ function readAllowlistGlob(repoRoot) {
11457
11926
  `plugin-allowlist drift guard: could not find the "for name in var.enabled_plugins" glob in ${ALLOWLIST_MAIN_TF}.`
11458
11927
  );
11459
11928
  }
11460
- return resolve18(glob, {
11929
+ return resolve19(glob, {
11461
11930
  "data.aws_caller_identity.current.account_id": ACCOUNT,
11462
11931
  "var.project_name": PROJECT,
11463
11932
  "var.environment": ENV,
@@ -11491,7 +11960,7 @@ Plugins would be rejected by require_service_principal (ADR-0009). Fix the glob,
11491
11960
 
11492
11961
  // src/scripts/check-plugin-allowlist-convention.ts
11493
11962
  async function runPluginAllowlistConventionCheck() {
11494
- const root = (await execa15("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11963
+ const root = (await execa16("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11495
11964
  let violations;
11496
11965
  try {
11497
11966
  violations = checkAllowlistConvention(root);
@@ -11516,34 +11985,34 @@ async function runPluginAllowlistConventionCheck() {
11516
11985
  }
11517
11986
 
11518
11987
  // src/scripts/check-plugin-collisions.ts
11519
- import { existsSync as existsSync39 } from "fs";
11520
- import { join as join48 } from "path";
11521
- import { execa as execa16 } from "execa";
11988
+ import { existsSync as existsSync42 } from "fs";
11989
+ import { join as join51 } from "path";
11990
+ import { execa as execa17 } from "execa";
11522
11991
 
11523
11992
  // src/lib/plugin-collision-guard.ts
11524
- import { existsSync as existsSync38, readdirSync as readdirSync20, statSync as statSync12 } from "fs";
11525
- import { join as join47 } from "path";
11993
+ import { existsSync as existsSync41, readdirSync as readdirSync22, statSync as statSync13 } from "fs";
11994
+ import { join as join50 } from "path";
11526
11995
  var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
11527
11996
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
11528
11997
  function subdirectories(dir) {
11529
- if (!existsSync38(dir)) return [];
11530
- return readdirSync20(dir).filter((entry) => {
11998
+ if (!existsSync41(dir)) return [];
11999
+ return readdirSync22(dir).filter((entry) => {
11531
12000
  if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
11532
12001
  try {
11533
- return statSync12(join47(dir, entry)).isDirectory();
12002
+ return statSync13(join50(dir, entry)).isDirectory();
11534
12003
  } catch {
11535
12004
  return false;
11536
12005
  }
11537
12006
  });
11538
12007
  }
11539
12008
  function regularPackagesOf(pluginDir2) {
11540
- return subdirectories(pluginDir2).filter((name) => existsSync38(join47(pluginDir2, name, "__init__.py"))).sort();
12009
+ return subdirectories(pluginDir2).filter((name) => existsSync41(join50(pluginDir2, name, "__init__.py"))).sort();
11541
12010
  }
11542
12011
  function bareTestModulesOf(pluginDir2) {
11543
- const testsDir = join47(pluginDir2, "tests");
11544
- if (!existsSync38(testsDir)) return [];
11545
- if (existsSync38(join47(testsDir, "__init__.py"))) return [];
11546
- return readdirSync20(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
12012
+ const testsDir = join50(pluginDir2, "tests");
12013
+ if (!existsSync41(testsDir)) return [];
12014
+ if (existsSync41(join50(testsDir, "__init__.py"))) return [];
12015
+ return readdirSync22(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
11547
12016
  }
11548
12017
  function findCollisions(servicesDir, pluginDirs) {
11549
12018
  const plugins = (pluginDirs ?? subdirectories(servicesDir)).filter((name) => !name.startsWith("_")).filter((name) => name !== "api").sort();
@@ -11551,7 +12020,7 @@ function findCollisions(servicesDir, pluginDirs) {
11551
12020
  const gather = (kind, namesOf) => {
11552
12021
  const claims = /* @__PURE__ */ new Map();
11553
12022
  for (const plugin of plugins) {
11554
- for (const name of namesOf(join47(servicesDir, plugin))) {
12023
+ for (const name of namesOf(join50(servicesDir, plugin))) {
11555
12024
  claims.set(name, [...claims.get(name) ?? [], plugin]);
11556
12025
  }
11557
12026
  }
@@ -11588,9 +12057,9 @@ function formatCollisions(collisions) {
11588
12057
 
11589
12058
  // src/scripts/check-plugin-collisions.ts
11590
12059
  async function runPluginCollisionCheck() {
11591
- const root = (await execa16("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11592
- const servicesDir = join48(root, "services");
11593
- if (!existsSync39(servicesDir)) {
12060
+ const root = (await execa17("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12061
+ const servicesDir = join51(root, "services");
12062
+ if (!existsSync42(servicesDir)) {
11594
12063
  console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
11595
12064
  return;
11596
12065
  }
@@ -11607,11 +12076,11 @@ async function runPluginCollisionCheck() {
11607
12076
  }
11608
12077
 
11609
12078
  // src/scripts/check-plugin-terraform.ts
11610
- import { execa as execa17 } from "execa";
12079
+ import { execa as execa18 } from "execa";
11611
12080
 
11612
12081
  // src/lib/plugin-terraform-guard.ts
11613
- import { existsSync as existsSync40, readFileSync as readFileSync35, readdirSync as readdirSync21 } from "fs";
11614
- import { dirname as dirname10, join as join49, relative as relative8, sep as sep3 } from "path";
12082
+ import { existsSync as existsSync43, readFileSync as readFileSync37, readdirSync as readdirSync23 } from "fs";
12083
+ import { dirname as dirname10, join as join52, relative as relative9, sep as sep3 } from "path";
11615
12084
  var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
11616
12085
  var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
11617
12086
  function findPluginManifests(root) {
@@ -11619,16 +12088,16 @@ function findPluginManifests(root) {
11619
12088
  const walk2 = (dir) => {
11620
12089
  let entries;
11621
12090
  try {
11622
- entries = readdirSync21(dir, { withFileTypes: true });
12091
+ entries = readdirSync23(dir, { withFileTypes: true });
11623
12092
  } catch {
11624
12093
  return;
11625
12094
  }
11626
12095
  for (const entry of entries) {
11627
12096
  if (entry.isDirectory()) {
11628
12097
  if (SKIP_DIRS3.has(entry.name)) continue;
11629
- walk2(join49(dir, entry.name));
12098
+ walk2(join52(dir, entry.name));
11630
12099
  } else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
11631
- found.push(relative8(root, join49(dir, entry.name)).split(sep3).join("/"));
12100
+ found.push(relative9(root, join52(dir, entry.name)).split(sep3).join("/"));
11632
12101
  }
11633
12102
  }
11634
12103
  };
@@ -11638,7 +12107,7 @@ function findPluginManifests(root) {
11638
12107
  function readSubscriptions(absManifestPath) {
11639
12108
  let parsed;
11640
12109
  try {
11641
- parsed = JSON.parse(readFileSync35(absManifestPath, "utf8"));
12110
+ parsed = JSON.parse(readFileSync37(absManifestPath, "utf8"));
11642
12111
  } catch {
11643
12112
  return null;
11644
12113
  }
@@ -11653,15 +12122,15 @@ function readSubscriptions(absManifestPath) {
11653
12122
  }
11654
12123
  function checkPluginTerraform(root) {
11655
12124
  const violations = [];
11656
- const coreManifest = existsSync40(join49(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
12125
+ const coreManifest = existsSync43(join52(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
11657
12126
  for (const manifest of findPluginManifests(root)) {
11658
12127
  if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
11659
- const absManifest = join49(root, manifest);
12128
+ const absManifest = join52(root, manifest);
11660
12129
  const subscriptions = readSubscriptions(absManifest);
11661
12130
  if (subscriptions === null) continue;
11662
12131
  const pluginDir2 = dirname10(absManifest);
11663
- if (existsSync40(join49(pluginDir2, "terraform"))) continue;
11664
- const relPluginDir = relative8(root, pluginDir2).split(sep3).join("/");
12132
+ if (existsSync43(join52(pluginDir2, "terraform"))) continue;
12133
+ const relPluginDir = relative9(root, pluginDir2).split(sep3).join("/");
11665
12134
  violations.push({
11666
12135
  manifest,
11667
12136
  expectedTerraformDir: relPluginDir ? `${relPluginDir}/terraform` : "terraform",
@@ -11680,7 +12149,7 @@ function formatViolations(violations) {
11680
12149
 
11681
12150
  // src/scripts/check-plugin-terraform.ts
11682
12151
  async function runPluginTerraformCheck() {
11683
- const root = (await execa17("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12152
+ const root = (await execa18("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11684
12153
  const violations = checkPluginTerraform(root);
11685
12154
  if (violations.length > 0) {
11686
12155
  console.error("\u2717 plugin Terraform guard: event subscriptions with no infrastructure\n");
@@ -11691,13 +12160,13 @@ async function runPluginTerraformCheck() {
11691
12160
  }
11692
12161
 
11693
12162
  // src/scripts/check-plugin-tool-supply.ts
11694
- import { existsSync as existsSync42 } from "fs";
11695
- import { join as join51 } from "path";
11696
- import { execa as execa18 } from "execa";
12163
+ import { existsSync as existsSync45 } from "fs";
12164
+ import { join as join54 } from "path";
12165
+ import { execa as execa19 } from "execa";
11697
12166
 
11698
12167
  // src/lib/plugin-tool-supply-audit.ts
11699
- import { existsSync as existsSync41, readFileSync as readFileSync36, readdirSync as readdirSync22, statSync as statSync13 } from "fs";
11700
- import { join as join50 } from "path";
12168
+ import { existsSync as existsSync44, readFileSync as readFileSync38, readdirSync as readdirSync24, statSync as statSync14 } from "fs";
12169
+ import { join as join53 } from "path";
11701
12170
 
11702
12171
  // src/lib/openrouter-model-snapshot.ts
11703
12172
  var OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT = "2026-08-10T06:39:01Z";
@@ -12108,13 +12577,13 @@ var OPENROUTER_MODEL_IDS = [
12108
12577
  function listDirs(root) {
12109
12578
  let entries;
12110
12579
  try {
12111
- entries = readdirSync22(root);
12580
+ entries = readdirSync24(root);
12112
12581
  } catch {
12113
12582
  return [];
12114
12583
  }
12115
12584
  return entries.filter((e) => {
12116
12585
  try {
12117
- return statSync13(join50(root, e)).isDirectory();
12586
+ return statSync14(join53(root, e)).isDirectory();
12118
12587
  } catch {
12119
12588
  return false;
12120
12589
  }
@@ -12125,15 +12594,15 @@ function walkFiles2(root, accept, skipDir) {
12125
12594
  const walk2 = (dir) => {
12126
12595
  let entries;
12127
12596
  try {
12128
- entries = readdirSync22(dir);
12597
+ entries = readdirSync24(dir);
12129
12598
  } catch {
12130
12599
  return;
12131
12600
  }
12132
12601
  for (const entry of entries) {
12133
- const p = join50(dir, entry);
12602
+ const p = join53(dir, entry);
12134
12603
  let st;
12135
12604
  try {
12136
- st = statSync13(p);
12605
+ st = statSync14(p);
12137
12606
  } catch {
12138
12607
  continue;
12139
12608
  }
@@ -12156,14 +12625,14 @@ function pluginPythonFiles(pluginDir2) {
12156
12625
  );
12157
12626
  }
12158
12627
  function pluginTerraformFiles(pluginDir2) {
12159
- const tfDir = join50(pluginDir2, "terraform");
12628
+ const tfDir = join53(pluginDir2, "terraform");
12160
12629
  let entries;
12161
12630
  try {
12162
- entries = readdirSync22(tfDir);
12631
+ entries = readdirSync24(tfDir);
12163
12632
  } catch {
12164
12633
  return [];
12165
12634
  }
12166
- return entries.filter((e) => e.endsWith(".tf")).map((e) => join50(tfDir, e)).sort();
12635
+ return entries.filter((e) => e.endsWith(".tf")).map((e) => join53(tfDir, e)).sort();
12167
12636
  }
12168
12637
  function extractManifestTools(manifestText) {
12169
12638
  let parsed;
@@ -12415,8 +12884,8 @@ function isSnapshotStale(fetchedAt, now) {
12415
12884
  function normalizeModelId(id) {
12416
12885
  return id.endsWith(":online") ? id.slice(0, -":online".length) : id;
12417
12886
  }
12418
- var CONFIG_PY_PATH = join50("services", "api", "src", "api", "config.py");
12419
- var ORCHESTRATION_SCHEMA_PATH = join50(
12887
+ var CONFIG_PY_PATH = join53("services", "api", "src", "api", "config.py");
12888
+ var ORCHESTRATION_SCHEMA_PATH = join53(
12420
12889
  "services",
12421
12890
  "api",
12422
12891
  "src",
@@ -12428,10 +12897,10 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
12428
12897
  const knownModelIds = options.knownModelIds ?? OPENROUTER_MODEL_IDS;
12429
12898
  const snapshotFetchedAt = options.snapshotFetchedAt ?? OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT;
12430
12899
  const now = options.now ?? /* @__PURE__ */ new Date();
12431
- const configPath = join50(repoRoot, CONFIG_PY_PATH);
12432
- const orchestrationPath = join50(repoRoot, ORCHESTRATION_SCHEMA_PATH);
12433
- const configMissing = !existsSync41(configPath);
12434
- const orchestrationSchemaMissing = !existsSync41(orchestrationPath);
12900
+ const configPath = join53(repoRoot, CONFIG_PY_PATH);
12901
+ const orchestrationPath = join53(repoRoot, ORCHESTRATION_SCHEMA_PATH);
12902
+ const configMissing = !existsSync44(configPath);
12903
+ const orchestrationSchemaMissing = !existsSync44(orchestrationPath);
12435
12904
  const knownSet = new Set(knownModelIds);
12436
12905
  const snapshotEmpty = knownModelIds.length === 0;
12437
12906
  const snapshotStale = isSnapshotStale(snapshotFetchedAt, now);
@@ -12449,13 +12918,13 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
12449
12918
  };
12450
12919
  let settingsBlind = false;
12451
12920
  if (!configMissing) {
12452
- const settingsFields = extractSettingsModelFields(readFileSync36(configPath, "utf8"));
12921
+ const settingsFields = extractSettingsModelFields(readFileSync38(configPath, "utf8"));
12453
12922
  if (settingsFields.length === 0) settingsBlind = true;
12454
12923
  for (const { field, value } of settingsFields) record(`${CONFIG_PY_PATH}#${field}`, value);
12455
12924
  }
12456
12925
  let curatedFieldsBlind = false;
12457
12926
  if (!orchestrationSchemaMissing) {
12458
- const curated = extractCuratedModelFields(readFileSync36(orchestrationPath, "utf8"));
12927
+ const curated = extractCuratedModelFields(readFileSync38(orchestrationPath, "utf8"));
12459
12928
  if (curated.rawFieldCount > 0 && curated.fields.every((f) => f.defaultValue === null && f.optionValues.length === 0)) {
12460
12929
  curatedFieldsBlind = true;
12461
12930
  }
@@ -12502,7 +12971,7 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
12502
12971
  function discoverPluginDirs(pluginsRoot) {
12503
12972
  return listDirs(pluginsRoot).filter((name) => {
12504
12973
  try {
12505
- return statSync13(join50(pluginsRoot, name, "biffo.plugin.json")).isFile();
12974
+ return statSync14(join53(pluginsRoot, name, "biffo.plugin.json")).isFile();
12506
12975
  } catch {
12507
12976
  return false;
12508
12977
  }
@@ -12515,8 +12984,8 @@ function auditPluginToolSupply(pluginsRoot) {
12515
12984
  let terraformBlind = false;
12516
12985
  let totalDeclaredTools = 0;
12517
12986
  for (const name of pluginNames) {
12518
- const pluginDir2 = join50(pluginsRoot, name);
12519
- const manifestText = readFileSync36(join50(pluginDir2, "biffo.plugin.json"), "utf8");
12987
+ const pluginDir2 = join53(pluginsRoot, name);
12988
+ const manifestText = readFileSync38(join53(pluginDir2, "biffo.plugin.json"), "utf8");
12520
12989
  const manifest = extractManifestTools(manifestText);
12521
12990
  if (manifest.parseError) {
12522
12991
  findings.push({
@@ -12534,13 +13003,13 @@ function auditPluginToolSupply(pluginsRoot) {
12534
13003
  totalDeclaredTools += manifest.tools.length;
12535
13004
  const pySources = pluginPythonFiles(pluginDir2).map((f) => ({
12536
13005
  file: f,
12537
- text: readFileSync36(f, "utf8")
13006
+ text: readFileSync38(f, "utf8")
12538
13007
  }));
12539
13008
  const resolver = buildSymbolResolver(pySources);
12540
13009
  const registry = extractToolRegistryEntries(pySources, resolver);
12541
13010
  if (registry.rawToolDefinitionCount > 0 && registry.entries.length === 0) registryBlind = true;
12542
13011
  const tfFiles = pluginTerraformFiles(pluginDir2);
12543
- const tfText = tfFiles.map((f) => readFileSync36(f, "utf8")).join("\n");
13012
+ const tfText = tfFiles.map((f) => readFileSync38(f, "utf8")).join("\n");
12544
13013
  const terraform = extractTerraformEnvKeys(tfText);
12545
13014
  if (terraform.rawMarkerCount > 0 && terraform.resolvedBlockCount === 0) terraformBlind = true;
12546
13015
  for (const toolName of manifest.tools) {
@@ -12614,7 +13083,7 @@ function auditPluginToolSupply(pluginsRoot) {
12614
13083
  requiredEnvVars: envResult.envVars,
12615
13084
  missingEnvVars: anyWired ? [] : envResult.envVars,
12616
13085
  status: anyWired ? "ok" : "missing-env",
12617
- detail: anyWired ? `${entry.predicate}() is satisfiable: at least one of ${JSON.stringify(envResult.envVars)} is wired in Terraform` : `${entry.predicate}() reads ${JSON.stringify(envResult.envVars)} \u2014 NONE of these are wired by any environment_variables block under ${join50(pluginDir2, "terraform")}, so this deployment can never supply it`
13086
+ detail: anyWired ? `${entry.predicate}() is satisfiable: at least one of ${JSON.stringify(envResult.envVars)} is wired in Terraform` : `${entry.predicate}() reads ${JSON.stringify(envResult.envVars)} \u2014 NONE of these are wired by any environment_variables block under ${join53(pluginDir2, "terraform")}, so this deployment can never supply it`
12618
13087
  });
12619
13088
  }
12620
13089
  }
@@ -12645,10 +13114,10 @@ function auditPluginToolSupply(pluginsRoot) {
12645
13114
 
12646
13115
  // src/scripts/check-plugin-tool-supply.ts
12647
13116
  async function runPluginToolSupplyCheck() {
12648
- const root = (await execa18("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13117
+ const root = (await execa19("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12649
13118
  let allOk = true;
12650
- const pluginsRoot = join51(root, "services", "_plugins");
12651
- if (!existsSync42(pluginsRoot)) {
13119
+ const pluginsRoot = join54(root, "services", "_plugins");
13120
+ if (!existsSync45(pluginsRoot)) {
12652
13121
  console.log("\u2713 plugin tool-supply guard: no services/_plugins/ \u2014 nothing to audit");
12653
13122
  } else {
12654
13123
  const report = auditPluginToolSupply(pluginsRoot);
@@ -12678,8 +13147,8 @@ async function runPluginToolSupplyCheck() {
12678
13147
  console.log(`\u2713 plugin tool-supply guard: ${report.summary}`);
12679
13148
  }
12680
13149
  }
12681
- const servicesApiRoot = join51(root, "services", "api");
12682
- if (!existsSync42(servicesApiRoot)) {
13150
+ const servicesApiRoot = join54(root, "services", "api");
13151
+ if (!existsSync45(servicesApiRoot)) {
12683
13152
  console.log("\u2713 plugin model-id guard: no services/api/ \u2014 nothing to audit");
12684
13153
  } else {
12685
13154
  const modelReport = auditDeclaredModelIds(root);
@@ -12725,7 +13194,7 @@ async function runPluginToolSupplyCheck() {
12725
13194
  }
12726
13195
 
12727
13196
  // src/scripts/check-release-subject.ts
12728
- import { execa as execa19 } from "execa";
13197
+ import { execa as execa20 } from "execa";
12729
13198
 
12730
13199
  // src/lib/release-version.ts
12731
13200
  var MINOR_TYPES = /* @__PURE__ */ new Set(["feat"]);
@@ -12762,7 +13231,7 @@ async function fetchPrTitleViaGh({
12762
13231
  PR_NUMBER,
12763
13232
  GH_REPO
12764
13233
  }) {
12765
- const { stdout } = await execa19(
13234
+ const { stdout } = await execa20(
12766
13235
  "gh",
12767
13236
  ["pr", "view", PR_NUMBER, "--repo", GH_REPO, "--json", "title", "--jq", ".title"],
12768
13237
  { env: { ...process.env, GH_TOKEN } }
@@ -12798,7 +13267,7 @@ async function resolveReleaseSubject({
12798
13267
  );
12799
13268
  }
12800
13269
  }
12801
- return (await execa19("git", ["log", "-1", "--format=%s"], { cwd })).stdout.trim();
13270
+ return (await execa20("git", ["log", "-1", "--format=%s"], { cwd })).stdout.trim();
12802
13271
  }
12803
13272
  async function runReleaseSubjectCheck(argv) {
12804
13273
  const base = process.env["GITHUB_BASE_REF"] ?? argv[0];
@@ -12806,9 +13275,9 @@ async function runReleaseSubjectCheck(argv) {
12806
13275
  console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
12807
13276
  process.exit(2);
12808
13277
  }
12809
- const root = (await execa19("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12810
- await execa19("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
12811
- const { stdout } = await execa19("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
13278
+ const root = (await execa20("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13279
+ await execa20("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
13280
+ const { stdout } = await execa20("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
12812
13281
  cwd: root
12813
13282
  });
12814
13283
  const changedFiles = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
@@ -12856,13 +13325,13 @@ async function runReleaseSubjectCheck(argv) {
12856
13325
  }
12857
13326
 
12858
13327
  // src/scripts/check-skeleton-drift.ts
12859
- import { existsSync as existsSync43, readdirSync as readdirSync24 } from "fs";
12860
- import { join as join53 } from "path";
12861
- import { execa as execa20 } from "execa";
13328
+ import { existsSync as existsSync46, readdirSync as readdirSync26 } from "fs";
13329
+ import { join as join56 } from "path";
13330
+ import { execa as execa21 } from "execa";
12862
13331
 
12863
13332
  // src/lib/skeleton-drift-guard.ts
12864
- import { readFileSync as readFileSync37, readdirSync as readdirSync23, statSync as statSync14 } from "fs";
12865
- import { join as join52 } from "path";
13333
+ import { readFileSync as readFileSync39, readdirSync as readdirSync25, statSync as statSync15 } from "fs";
13334
+ import { join as join55 } from "path";
12866
13335
  var isWorkflow = (rel) => rel.startsWith(".github/workflows/") && (rel.endsWith(".yml") || rel.endsWith(".yaml"));
12867
13336
  var isRootLayout = (rel) => rel.endsWith("src/app/layout.tsx");
12868
13337
  var uncommented = (contents) => contents.split("\n").filter((line) => !/^\s*(\/\/|\/\*|\*)/.test(line)).join("\n");
@@ -12920,16 +13389,16 @@ function walk(dir, base = dir) {
12920
13389
  const out = [];
12921
13390
  let entries;
12922
13391
  try {
12923
- entries = readdirSync23(dir);
13392
+ entries = readdirSync25(dir);
12924
13393
  } catch {
12925
13394
  return out;
12926
13395
  }
12927
13396
  for (const entry of entries) {
12928
13397
  if (entry === ".venv" || entry === "node_modules" || entry === ".git") continue;
12929
- const abs = join52(dir, entry);
13398
+ const abs = join55(dir, entry);
12930
13399
  let isDir;
12931
13400
  try {
12932
- isDir = statSync14(abs).isDirectory();
13401
+ isDir = statSync15(abs).isDirectory();
12933
13402
  } catch {
12934
13403
  continue;
12935
13404
  }
@@ -12948,7 +13417,7 @@ function auditSkeleton(skeletonRoot, name, rules = SKELETON_RULES) {
12948
13417
  if (!rule.appliesTo(rel)) continue;
12949
13418
  let contents;
12950
13419
  try {
12951
- contents = readFileSync37(join52(skeletonRoot, rel), "utf8");
13420
+ contents = readFileSync39(join55(skeletonRoot, rel), "utf8");
12952
13421
  } catch {
12953
13422
  continue;
12954
13423
  }
@@ -12977,23 +13446,23 @@ function formatViolations2(violations) {
12977
13446
 
12978
13447
  // src/scripts/check-skeleton-drift.ts
12979
13448
  function discoverSkeletons(root) {
12980
- const skeletonsDir = join53(root, "_skeletons");
13449
+ const skeletonsDir = join56(root, "_skeletons");
12981
13450
  let entries;
12982
13451
  try {
12983
- entries = readdirSync24(skeletonsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
13452
+ entries = readdirSync26(skeletonsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
12984
13453
  } catch {
12985
13454
  return [];
12986
13455
  }
12987
- return entries.filter((name) => existsSync43(join53(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
13456
+ return entries.filter((name) => existsSync46(join56(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
12988
13457
  }
12989
13458
  async function runSkeletonDriftCheck() {
12990
- const root = (await execa20("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13459
+ const root = (await execa21("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12991
13460
  const skeletons = discoverSkeletons(root);
12992
13461
  let filesConsidered = 0;
12993
13462
  for (const name of skeletons) {
12994
- const skeletonRoot = join53(root, "_skeletons", name);
13463
+ const skeletonRoot = join56(root, "_skeletons", name);
12995
13464
  filesConsidered += findWorkflowFiles(skeletonRoot).length;
12996
- if (existsSync43(join53(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
13465
+ if (existsSync46(join56(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
12997
13466
  filesConsidered += 1;
12998
13467
  }
12999
13468
  }
@@ -13007,7 +13476,7 @@ async function runSkeletonDriftCheck() {
13007
13476
  process.exit(1);
13008
13477
  }
13009
13478
  const violations = skeletons.flatMap(
13010
- (name) => auditSkeleton(join53(root, "_skeletons", name), name)
13479
+ (name) => auditSkeleton(join56(root, "_skeletons", name), name)
13011
13480
  );
13012
13481
  if (violations.length > 0) {
13013
13482
  console.error("\u2717 Skeleton-drift guard: drift found between this repo and its scaffolding\n");
@@ -13019,9 +13488,9 @@ async function runSkeletonDriftCheck() {
13019
13488
  }
13020
13489
 
13021
13490
  // src/scripts/check-terraform-input.ts
13022
- import { execa as execa21 } from "execa";
13491
+ import { execa as execa22 } from "execa";
13023
13492
  async function runTerraformInputCheck() {
13024
- const root = (await execa21("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13493
+ const root = (await execa22("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13025
13494
  const files = findWorkflowFiles(root);
13026
13495
  console.log(`audited ${files.length} workflow file(s) under ${root}`);
13027
13496
  if (files.length === 0) {
@@ -13043,8 +13512,8 @@ async function runTerraformInputCheck() {
13043
13512
  }
13044
13513
 
13045
13514
  // src/commands/check.ts
13046
- var checkCommand = new Command23("check").description(
13047
- "Repo guards (ownership, release subject, plugin terraform, plugin collisions, eventbridge-log-permissions, plugin-tool-supply, core-direct-paths, cognito-invite-template, lambda-output, pipe-trap, codeql-suppression, skeleton-drift, terraform-input, plugin-allowlist-convention) run in CI and git hooks, plus out-of-band audits (branch protection)"
13515
+ var checkCommand = new Command24("check").description(
13516
+ "Repo guards (ownership, release subject, plugin terraform, plugin collisions, eventbridge-log-permissions, plugin-tool-supply, core-direct-paths, cognito-invite-template, lambda-output, pipe-trap, codeql-suppression, skeleton-drift, terraform-input, plugin-allowlist-convention) run in CI and git hooks, plus out-of-band audits (branch protection, plugin-staleness)"
13048
13517
  );
13049
13518
  checkCommand.command("ownership").description("Refuse changes to template-owned paths in an instance (#370)").argument("[base]", "Base branch to diff against; defaults to $GITHUB_BASE_REF").option("--staged <messageFile>", "Check staged changes instead of a branch diff (commit hook)").allowExcessArguments(true).action(async () => {
13050
13519
  await runOwnershipCheck(rawArgsAfter("ownership"));
@@ -13129,16 +13598,26 @@ checkCommand.command("branch-protection").description(
13129
13598
  ).action(async (opts) => {
13130
13599
  await runBranchProtectionCheck(opts.repo, { fix: opts.fix });
13131
13600
  });
13601
+ checkCommand.command("plugin-staleness").description(
13602
+ "Advisory: report how far each services/<name>/ vendored plugin has drifted from its source (#1547) \u2014 an instance can drift arbitrarily far behind a plugin repo while both sides' CI stays green, because neither has an opinion about the gap between them. Deliberately NEVER fails this check, unlike every other one in this group: an instance may legitimately pin a plugin version, and staleness moving is not a defect the way an ownership violation or a namespace collision is. Run `biffo plugin staleness` directly for the same measurement with a real 0/1/2 exit code, if you want to gate on it."
13603
+ ).action(async () => {
13604
+ const cwd = process.env["BIFFO_ORIGINAL_CWD"] || process.cwd();
13605
+ const results = await checkPluginStaleness(cwd, {
13606
+ registry: new RegistryAdapter(),
13607
+ git: new GitAdapter()
13608
+ });
13609
+ console.log(formatStalenessReport(results));
13610
+ });
13132
13611
  function rawArgsAfter(subcommand) {
13133
13612
  const at = process.argv.indexOf(subcommand);
13134
13613
  return at === -1 ? [] : process.argv.slice(at + 1);
13135
13614
  }
13136
13615
 
13137
13616
  // src/commands/doctor.ts
13138
- import { existsSync as existsSync44, readFileSync as readFileSync38 } from "fs";
13139
- import { join as join54, resolve as resolve19 } from "path";
13617
+ import { existsSync as existsSync47, readFileSync as readFileSync40 } from "fs";
13618
+ import { join as join57, resolve as resolve20 } from "path";
13140
13619
  import chalk21 from "chalk";
13141
- import { Command as Command24 } from "commander";
13620
+ import { Command as Command25 } from "commander";
13142
13621
 
13143
13622
  // src/lib/doctor.ts
13144
13623
  function checkCheckoutCurrency(facts) {
@@ -13261,10 +13740,10 @@ function runDoctorChecks(facts) {
13261
13740
 
13262
13741
  // src/commands/doctor.ts
13263
13742
  var INTEGRATION_BRANCH = "dev";
13264
- var doctorCommand = new Command24("doctor").description(
13743
+ var doctorCommand = new Command25("doctor").description(
13265
13744
  "Report repo-state conditions that make everything read from this checkout unreliable"
13266
13745
  ).option("--cwd <path>", "Repo root to inspect (defaults to the current directory)").option("--no-fetch", "Skip the fetch; report against refs as they already are locally").action(async (options) => {
13267
- const cwd = options.cwd ? resolve19(options.cwd) : process.cwd();
13746
+ const cwd = options.cwd ? resolve20(options.cwd) : process.cwd();
13268
13747
  try {
13269
13748
  const findings = await runDoctor({ cwd, fetch: options.fetch !== false });
13270
13749
  printFindings(findings);
@@ -13315,10 +13794,10 @@ async function runDoctor(options, deps = { git: new GitAdapter() }) {
13315
13794
  return runDoctorChecks(facts);
13316
13795
  }
13317
13796
  function readLocalCoreVersion(cwd) {
13318
- const path = join54(cwd, INSTANCE_CORE_FILE);
13319
- if (!existsSync44(path)) return null;
13797
+ const path = join57(cwd, INSTANCE_CORE_FILE);
13798
+ if (!existsSync47(path)) return null;
13320
13799
  try {
13321
- return extractVersionField(readFileSync38(path, "utf8"));
13800
+ return extractVersionField(readFileSync40(path, "utf8"));
13322
13801
  } catch {
13323
13802
  return null;
13324
13803
  }
@@ -13338,10 +13817,10 @@ function extractVersionField(contents) {
13338
13817
  return match?.[1] ?? null;
13339
13818
  }
13340
13819
  function readFossil(cwd) {
13341
- const path = join54(cwd, CORE_VERSION_FILE);
13342
- if (!existsSync44(path)) return null;
13820
+ const path = join57(cwd, CORE_VERSION_FILE);
13821
+ if (!existsSync47(path)) return null;
13343
13822
  try {
13344
- const value = readFileSync38(path, "utf8").trim();
13823
+ const value = readFileSync40(path, "utf8").trim();
13345
13824
  return value === "" ? null : value;
13346
13825
  } catch {
13347
13826
  return null;
@@ -13374,9 +13853,9 @@ function printFindings(findings) {
13374
13853
  import { execSync as execSync7 } from "child_process";
13375
13854
  import { GetCallerIdentityCommand as GetCallerIdentityCommand3, STSClient as STSClient3 } from "@aws-sdk/client-sts";
13376
13855
  import chalk22 from "chalk";
13377
- import { Command as Command25 } from "commander";
13856
+ import { Command as Command26 } from "commander";
13378
13857
  import inquirer8 from "inquirer";
13379
- var teardownCommand = new Command25("teardown").description(
13858
+ var teardownCommand = new Command26("teardown").description(
13380
13859
  "Destroy all infrastructure then remove the repo, IAM role, and state bucket \u2014 single command"
13381
13860
  ).option("--project <name>", "Project name to tear down (reads session if omitted)").option("--skip-destroy", "Skip terraform destroy (only use if infrastructure is already gone)").option(
13382
13861
  "--confirm <name>",
@@ -13787,16 +14266,16 @@ function resolveGithubToken4() {
13787
14266
  import { spawnSync } from "child_process";
13788
14267
  import { dirname as dirname12 } from "path";
13789
14268
  import { fileURLToPath as fileURLToPath6 } from "url";
13790
- import { Command as Command26 } from "commander";
14269
+ import { Command as Command27 } from "commander";
13791
14270
 
13792
14271
  // src/lib/packaged-scripts.ts
13793
- import { existsSync as existsSync45 } from "fs";
13794
- import { dirname as dirname11, join as join55 } from "path";
14272
+ import { existsSync as existsSync48 } from "fs";
14273
+ import { dirname as dirname11, join as join58 } from "path";
13795
14274
  function findPackagedScript(startDir, relativePath) {
13796
14275
  let dir = startDir;
13797
14276
  for (; ; ) {
13798
- const candidate = join55(dir, relativePath);
13799
- if (existsSync45(candidate)) return candidate;
14277
+ const candidate = join58(dir, relativePath);
14278
+ if (existsSync48(candidate)) return candidate;
13800
14279
  const parent = dirname11(dir);
13801
14280
  if (parent === dir) return null;
13802
14281
  dir = parent;
@@ -13816,7 +14295,7 @@ function runPackagedScript(script, args, cwd) {
13816
14295
  return result.status === null ? 2 : result.status;
13817
14296
  }
13818
14297
  function packagedScriptCommand(spec) {
13819
- const command = new Command26(spec.name).description(spec.description).allowExcessArguments(true).allowUnknownOption(true);
14298
+ const command = new Command27(spec.name).description(spec.description).allowExcessArguments(true).allowUnknownOption(true);
13820
14299
  if (spec.argument) command.argument(`<${spec.argument.name}>`, spec.argument.description);
13821
14300
  return command.action(() => {
13822
14301
  const here = dirname12(fileURLToPath6(import.meta.url));
@@ -13905,7 +14384,7 @@ var runnerDropForensicsCommand = packagedScriptCommand({
13905
14384
  });
13906
14385
 
13907
14386
  // src/index.ts
13908
- var program = new Command27();
14387
+ var program = new Command28();
13909
14388
  function cliVersion() {
13910
14389
  try {
13911
14390
  return getLatestCoreVersion();