@biffo/cli 0.280.0 → 0.281.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.
Files changed (2) hide show
  1. package/dist/index.js +713 -334
  2. package/package.json +1 -1
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";
@@ -8428,8 +8445,8 @@ function printEntry(entry) {
8428
8445
  }
8429
8446
 
8430
8447
  // 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";
8448
+ import { cpSync as cpSync4, existsSync as existsSync29, mkdirSync as mkdirSync10, readFileSync as readFileSync22, statSync as statSync6 } from "fs";
8449
+ import { join as join31, relative as relative3, resolve as resolve12 } from "path";
8433
8450
  import chalk15 from "chalk";
8434
8451
  import { Command as Command15 } from "commander";
8435
8452
 
@@ -8475,10 +8492,90 @@ var PluginMigrationsAdapter = class {
8475
8492
  }
8476
8493
  };
8477
8494
 
8495
+ // src/lib/plugin-provenance.ts
8496
+ import { existsSync as existsSync27, readFileSync as readFileSync20, writeFileSync as writeFileSync10 } from "fs";
8497
+ import { join as join28 } from "path";
8498
+ import { execa as execa5 } from "execa";
8499
+ var PLUGIN_PROVENANCE_FILENAME = ".biffo-plugin-provenance.json";
8500
+ function isPluginProvenance(value) {
8501
+ if (typeof value !== "object" || value === null) return false;
8502
+ const v = value;
8503
+ 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";
8504
+ }
8505
+ function readProvenance(pluginDir2) {
8506
+ const path = join28(pluginDir2, PLUGIN_PROVENANCE_FILENAME);
8507
+ if (!existsSync27(path)) return { status: "absent" };
8508
+ let parsed;
8509
+ try {
8510
+ parsed = JSON.parse(readFileSync20(path, "utf8"));
8511
+ } catch (err) {
8512
+ return {
8513
+ status: "invalid",
8514
+ reason: `could not parse ${PLUGIN_PROVENANCE_FILENAME}: ${err.message}`
8515
+ };
8516
+ }
8517
+ if (!isPluginProvenance(parsed)) {
8518
+ return {
8519
+ status: "invalid",
8520
+ reason: `${PLUGIN_PROVENANCE_FILENAME} does not have the expected shape`
8521
+ };
8522
+ }
8523
+ return { status: "present", record: parsed };
8524
+ }
8525
+ function writePluginProvenance(pluginDir2, record) {
8526
+ writeFileSync10(join28(pluginDir2, PLUGIN_PROVENANCE_FILENAME), `${JSON.stringify(record, null, 2)}
8527
+ `);
8528
+ }
8529
+ function reconcileProvenance(previous, next) {
8530
+ if (previous.status === "present" && sameProvenance(previous.record, next)) return previous.record;
8531
+ return next;
8532
+ }
8533
+ function sameProvenance(a, b) {
8534
+ return a.origin === b.origin && a.ref === b.ref && a.sha === b.sha && a.inTree === b.inTree;
8535
+ }
8536
+ function inTreePluginProvenance(relTargetDir) {
8537
+ return {
8538
+ origin: relTargetDir,
8539
+ ref: null,
8540
+ sha: null,
8541
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
8542
+ inTree: true
8543
+ };
8544
+ }
8545
+ async function resolveLocalProvenance(sourceDir, origin) {
8546
+ const recordedAt = (/* @__PURE__ */ new Date()).toISOString();
8547
+ if (!await isGitWorkingTree(sourceDir)) {
8548
+ return { origin, ref: null, sha: null, recordedAt, inTree: false };
8549
+ }
8550
+ const sha = await tryGit(sourceDir, ["rev-parse", "HEAD"]);
8551
+ const rawRef = await tryGit(sourceDir, ["rev-parse", "--abbrev-ref", "HEAD"]);
8552
+ const ref = rawRef && rawRef !== "HEAD" ? rawRef : null;
8553
+ return { origin, ref, sha, recordedAt, inTree: false };
8554
+ }
8555
+ function resolveRegistryProvenance(repoUrl, sha) {
8556
+ return { origin: repoUrl, ref: null, sha, recordedAt: (/* @__PURE__ */ new Date()).toISOString(), inTree: false };
8557
+ }
8558
+ async function isGitWorkingTree(dir) {
8559
+ try {
8560
+ await execa5("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir });
8561
+ return true;
8562
+ } catch {
8563
+ return false;
8564
+ }
8565
+ }
8566
+ async function tryGit(cwd, args) {
8567
+ try {
8568
+ const { stdout } = await execa5("git", args, { cwd });
8569
+ return stdout.trim() || null;
8570
+ } catch {
8571
+ return null;
8572
+ }
8573
+ }
8574
+
8478
8575
  // src/lib/plugin-source-copy.ts
8479
8576
  import { copyFileSync as copyFileSync2, cpSync as cpSync3, mkdirSync as mkdirSync9 } from "fs";
8480
- import { basename, dirname as dirname9, join as join28 } from "path";
8481
- import { execa as execa5 } from "execa";
8577
+ import { basename, dirname as dirname9, join as join29 } from "path";
8578
+ import { execa as execa6 } from "execa";
8482
8579
  var LOCAL_COPY_EXCLUDES = /* @__PURE__ */ new Set([
8483
8580
  ".git",
8484
8581
  ".venv",
@@ -8491,12 +8588,12 @@ var LOCAL_COPY_EXCLUDES = /* @__PURE__ */ new Set([
8491
8588
  ".terraform"
8492
8589
  ]);
8493
8590
  async function copyPluginSource(sourceDir, targetDir) {
8494
- if (await isGitWorkingTree(sourceDir)) {
8591
+ if (await isGitWorkingTree2(sourceDir)) {
8495
8592
  const files = await listGitFiles(sourceDir);
8496
8593
  for (const relPath of files) {
8497
- const destPath = join28(targetDir, relPath);
8594
+ const destPath = join29(targetDir, relPath);
8498
8595
  mkdirSync9(dirname9(destPath), { recursive: true });
8499
- copyFileSync2(join28(sourceDir, relPath), destPath);
8596
+ copyFileSync2(join29(sourceDir, relPath), destPath);
8500
8597
  }
8501
8598
  return { usedGitIgnoreRules: true };
8502
8599
  }
@@ -8510,16 +8607,16 @@ async function copyPluginSource(sourceDir, targetDir) {
8510
8607
  });
8511
8608
  return { usedGitIgnoreRules: false };
8512
8609
  }
8513
- async function isGitWorkingTree(dir) {
8610
+ async function isGitWorkingTree2(dir) {
8514
8611
  try {
8515
- await execa5("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir });
8612
+ await execa6("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir });
8516
8613
  return true;
8517
8614
  } catch {
8518
8615
  return false;
8519
8616
  }
8520
8617
  }
8521
8618
  async function listGitFiles(dir) {
8522
- const { stdout } = await execa5(
8619
+ const { stdout } = await execa6(
8523
8620
  "git",
8524
8621
  ["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
8525
8622
  { cwd: dir }
@@ -8528,8 +8625,8 @@ async function listGitFiles(dir) {
8528
8625
  }
8529
8626
 
8530
8627
  // 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";
8628
+ import { existsSync as existsSync28, readdirSync as readdirSync12, readFileSync as readFileSync21, writeFileSync as writeFileSync11 } from "fs";
8629
+ import { join as join30 } from "path";
8533
8630
  function readTomlStringArray(text, key) {
8534
8631
  const open = new RegExp(`^${key}\\s*=\\s*\\[`, "m").exec(text);
8535
8632
  if (!open) return [];
@@ -8573,9 +8670,9 @@ function readDependencyNames(text) {
8573
8670
  return readTomlStringArray(text, "dependencies").map((dep) => /^\s*([A-Za-z0-9._-]+)/.exec(dep)?.[1] ?? "").filter(Boolean);
8574
8671
  }
8575
8672
  function workspaceMemberNames(instanceRoot) {
8576
- const rootPyproject = join29(instanceRoot, "pyproject.toml");
8577
- if (!existsSync27(rootPyproject)) return /* @__PURE__ */ new Set();
8578
- const text = readFileSync20(rootPyproject, "utf8");
8673
+ const rootPyproject = join30(instanceRoot, "pyproject.toml");
8674
+ if (!existsSync28(rootPyproject)) return /* @__PURE__ */ new Set();
8675
+ const text = readFileSync21(rootPyproject, "utf8");
8579
8676
  const members = readTomlStringArray(text, "members");
8580
8677
  const excluded = new Set(readTomlStringArray(text, "exclude"));
8581
8678
  const dirs = [];
@@ -8584,7 +8681,7 @@ function workspaceMemberNames(instanceRoot) {
8584
8681
  const base = member.slice(0, -2);
8585
8682
  let entries;
8586
8683
  try {
8587
- entries = readdirSync12(join29(instanceRoot, base), { withFileTypes: true });
8684
+ entries = readdirSync12(join30(instanceRoot, base), { withFileTypes: true });
8588
8685
  } catch {
8589
8686
  continue;
8590
8687
  }
@@ -8598,9 +8695,9 @@ function workspaceMemberNames(instanceRoot) {
8598
8695
  }
8599
8696
  const names = /* @__PURE__ */ new Set();
8600
8697
  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"));
8698
+ const pp = join30(instanceRoot, dir, "pyproject.toml");
8699
+ if (!existsSync28(pp)) continue;
8700
+ const name = readProjectName(readFileSync21(pp, "utf8"));
8604
8701
  if (name) names.add(name);
8605
8702
  }
8606
8703
  return names;
@@ -8611,8 +8708,8 @@ function existingWorkspaceSources(text) {
8611
8708
  );
8612
8709
  }
8613
8710
  function ensureWorkspaceSources(pluginPyprojectPath, memberNames) {
8614
- if (!existsSync27(pluginPyprojectPath) || memberNames.size === 0) return [];
8615
- const text = readFileSync20(pluginPyprojectPath, "utf8");
8711
+ if (!existsSync28(pluginPyprojectPath) || memberNames.size === 0) return [];
8712
+ const text = readFileSync21(pluginPyprojectPath, "utf8");
8616
8713
  const already = existingWorkspaceSources(text);
8617
8714
  const toAdd = readDependencyNames(text).filter((n) => memberNames.has(n) && !already.has(n));
8618
8715
  if (toAdd.length === 0) return [];
@@ -8632,12 +8729,12 @@ ${lines.join("\n")}${text.slice(insertAt)}`;
8632
8729
  ${lines.join("\n")}
8633
8730
  `;
8634
8731
  }
8635
- writeFileSync10(pluginPyprojectPath, updated);
8732
+ writeFileSync11(pluginPyprojectPath, updated);
8636
8733
  return toAdd;
8637
8734
  }
8638
8735
  function applyWorkspaceSources(targetDir, cwd, relTargetDir) {
8639
- const pluginPyproject = join29(targetDir, "pyproject.toml");
8640
- if (!existsSync27(pluginPyproject)) return;
8736
+ const pluginPyproject = join30(targetDir, "pyproject.toml");
8737
+ if (!existsSync28(pluginPyproject)) return;
8641
8738
  const sourced = ensureWorkspaceSources(pluginPyproject, workspaceMemberNames(cwd));
8642
8739
  if (sourced.length > 0) {
8643
8740
  log.info(
@@ -8677,14 +8774,14 @@ var pluginInstallCommand = new Command15("install").description(
8677
8774
  }
8678
8775
  );
8679
8776
  function resolveLocalPlugin(localPath) {
8680
- if (!existsSync28(localPath)) {
8777
+ if (!existsSync29(localPath)) {
8681
8778
  throw new Error(`--local path does not exist: ${localPath}`);
8682
8779
  }
8683
8780
  if (!statSync6(localPath).isDirectory()) {
8684
8781
  throw new Error(`--local path is not a directory: ${localPath}`);
8685
8782
  }
8686
- const manifestPath = join30(localPath, "biffo.plugin.json");
8687
- if (!existsSync28(manifestPath)) {
8783
+ const manifestPath = join31(localPath, "biffo.plugin.json");
8784
+ if (!existsSync29(manifestPath)) {
8688
8785
  throw new Error(
8689
8786
  `${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
8787
  );
@@ -8710,8 +8807,8 @@ function parsePluginTarget(target) {
8710
8807
  async function cloneAndValidatePlugin(entry, git) {
8711
8808
  const tmpDir = await git.cloneToTemp(entry.repo, `biffo-plugin-${entry.name}`);
8712
8809
  try {
8713
- const manifestPath = join30(tmpDir, "biffo.plugin.json");
8714
- if (!existsSync28(manifestPath)) {
8810
+ const manifestPath = join31(tmpDir, "biffo.plugin.json");
8811
+ if (!existsSync29(manifestPath)) {
8715
8812
  throw new Error(
8716
8813
  `Plugin repo ${entry.repo} does not contain a biffo.plugin.json manifest at its root.`
8717
8814
  );
@@ -8739,8 +8836,8 @@ async function runPluginInstall(target, options, deps) {
8739
8836
  `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
8837
  );
8741
8838
  }
8742
- const servicesDir = join30(options.cwd, "services");
8743
- if (!existsSync28(servicesDir)) {
8839
+ const servicesDir = join31(options.cwd, "services");
8840
+ if (!existsSync29(servicesDir)) {
8744
8841
  throw new Error(
8745
8842
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
8746
8843
  );
@@ -8758,10 +8855,10 @@ async function runPluginInstall(target, options, deps) {
8758
8855
  }
8759
8856
  const pluginName = entry ? entry.name : source.name;
8760
8857
  const relTargetDir = pluginDir(pluginName, "third-party");
8761
- const targetDir = join30(options.cwd, relTargetDir);
8762
- const modulesDir = join30(options.cwd, "modules", "plugins", pluginName);
8858
+ const targetDir = join31(options.cwd, relTargetDir);
8859
+ const modulesDir = join31(options.cwd, "modules", "plugins", pluginName);
8763
8860
  const inTreeSource = options.local !== void 0 && resolve12(options.local) === resolve12(targetDir);
8764
- if (existsSync28(targetDir) && !inTreeSource) {
8861
+ if (existsSync29(targetDir) && !inTreeSource) {
8765
8862
  throw new Error(
8766
8863
  `Plugin '${pluginName}' is already installed at ${relTargetDir}/. Remove it first, or wait for a future 'biffo plugin upgrade' command.`
8767
8864
  );
@@ -8800,10 +8897,13 @@ async function runPluginInstall(target, options, deps) {
8800
8897
  await copyPluginSource(source.sourceDir, targetDir);
8801
8898
  log.success(`Installed plugin source at ${relTargetDir}/`);
8802
8899
  }
8900
+ const previousProvenance = readProvenance(targetDir);
8901
+ const nextProvenance = inTreeSource ? inTreePluginProvenance(relTargetDir) : entry ? resolveRegistryProvenance(entry.repo, await deps.git.resolveDefaultBranchSha(entry.repo)) : await resolveLocalProvenance(source.sourceDir, source.origin);
8902
+ writePluginProvenance(targetDir, reconcileProvenance(previousProvenance, nextProvenance));
8803
8903
  applyWorkspaceSources(targetDir, options.cwd, relTargetDir);
8804
8904
  const stagePaths = [relTargetDir];
8805
- const tfSourceDir = join30(targetDir, "terraform");
8806
- if (existsSync28(tfSourceDir)) {
8905
+ const tfSourceDir = join31(targetDir, "terraform");
8906
+ if (existsSync29(tfSourceDir)) {
8807
8907
  mkdirSync10(modulesDir, { recursive: true });
8808
8908
  cpSync4(tfSourceDir, modulesDir, { recursive: true });
8809
8909
  stagePaths.push(`modules/plugins/${pluginName}`);
@@ -8860,7 +8960,7 @@ async function runPluginInstall(target, options, deps) {
8860
8960
  }
8861
8961
  function parseManifestFile(path) {
8862
8962
  try {
8863
- return JSON.parse(readFileSync21(path, "utf8"));
8963
+ return JSON.parse(readFileSync22(path, "utf8"));
8864
8964
  } catch (err) {
8865
8965
  throw new Error(`Could not parse ${path} as JSON: ${err.message}`);
8866
8966
  }
@@ -8897,8 +8997,8 @@ function printDryRun4(entry, source, relTargetDir, inTreeSource) {
8897
8997
  }
8898
8998
 
8899
8999
  // 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";
9000
+ import { existsSync as existsSync30, readFileSync as readFileSync23 } from "fs";
9001
+ import { join as join32, resolve as resolve13 } from "path";
8902
9002
  import chalk16 from "chalk";
8903
9003
  import { Command as Command16 } from "commander";
8904
9004
  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 +9011,8 @@ var pluginListCommand = new Command16("list").description("List plugins installe
8911
9011
  }
8912
9012
  });
8913
9013
  async function runPluginList(options) {
8914
- const servicesDir = join31(options.cwd, "services");
8915
- if (!existsSync29(servicesDir)) {
9014
+ const servicesDir = join32(options.cwd, "services");
9015
+ if (!existsSync30(servicesDir)) {
8916
9016
  throw new Error(
8917
9017
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
8918
9018
  );
@@ -8920,7 +9020,7 @@ async function runPluginList(options) {
8920
9020
  const plugins = [];
8921
9021
  for (const location of findInstalledPlugins(options.cwd)) {
8922
9022
  try {
8923
- const manifest = validateManifest(JSON.parse(readFileSync22(location.manifestPath, "utf8")));
9023
+ const manifest = validateManifest(JSON.parse(readFileSync23(location.manifestPath, "utf8")));
8924
9024
  plugins.push({
8925
9025
  name: manifest.name,
8926
9026
  version: manifest.version,
@@ -8956,16 +9056,275 @@ async function runPluginList(options) {
8956
9056
  );
8957
9057
  }
8958
9058
 
9059
+ // src/commands/plugin-staleness.ts
9060
+ import { resolve as resolve14 } from "path";
9061
+ import { Command as Command17 } from "commander";
9062
+
9063
+ // src/lib/plugin-staleness.ts
9064
+ import { existsSync as existsSync31, readFileSync as readFileSync24, readdirSync as readdirSync13, statSync as statSync7 } from "fs";
9065
+ import { join as join33, relative as relative4 } from "path";
9066
+ function discoverVendoredPlugins(servicesDir) {
9067
+ if (!existsSync31(servicesDir)) return [];
9068
+ return readdirSync13(servicesDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("_") && e.name !== "api").map((e) => e.name).filter((name) => existsSync31(join33(servicesDir, name, "biffo.plugin.json"))).sort();
9069
+ }
9070
+ async function checkPluginStaleness(cwd, deps) {
9071
+ const servicesDir = join33(cwd, "services");
9072
+ const names = discoverVendoredPlugins(servicesDir);
9073
+ let registryRepoByName = null;
9074
+ const resolveRegistryRepo = async (name) => {
9075
+ if (registryRepoByName === null) {
9076
+ registryRepoByName = /* @__PURE__ */ new Map();
9077
+ try {
9078
+ const reg = await deps.registry.fetchRegistry();
9079
+ for (const entry of reg.plugins) registryRepoByName.set(entry.name, entry.repo);
9080
+ } catch {
9081
+ }
9082
+ }
9083
+ return registryRepoByName.get(name) ?? null;
9084
+ };
9085
+ const results = [];
9086
+ for (const name of names) {
9087
+ results.push(await checkOnePlugin(join33(servicesDir, name), name, resolveRegistryRepo, deps.git));
9088
+ }
9089
+ return results;
9090
+ }
9091
+ async function checkOnePlugin(pluginDir2, name, resolveRegistryRepo, git) {
9092
+ const provenance = readProvenance(pluginDir2);
9093
+ if (provenance.status === "invalid") {
9094
+ return {
9095
+ name,
9096
+ status: "cannot-tell",
9097
+ method: "unresolvable",
9098
+ detail: `provenance file is unreadable \u2014 ${provenance.reason}`
9099
+ };
9100
+ }
9101
+ const record = provenance.status === "present" ? provenance.record : null;
9102
+ if (record?.inTree) {
9103
+ return {
9104
+ name,
9105
+ status: "cannot-tell",
9106
+ method: "unresolvable",
9107
+ detail: `installed --local straight into services/${name}/ (in-tree) \u2014 there is no external plugin repo to compare against`
9108
+ };
9109
+ }
9110
+ if (record?.sha && isFetchableUrl(record.origin)) {
9111
+ return checkViaProvenance(name, record, record.origin, git);
9112
+ }
9113
+ const localOrigin = record && !isFetchableUrl(record.origin) && existsSync31(record.origin) ? record.origin : null;
9114
+ if (localOrigin) {
9115
+ return checkViaContentDiff(name, pluginDir2, localOrigin, { isLocalDir: true }, git);
9116
+ }
9117
+ const registryRepo = await resolveRegistryRepo(name);
9118
+ if (!registryRepo) {
9119
+ return {
9120
+ name,
9121
+ status: "cannot-tell",
9122
+ method: "unresolvable",
9123
+ 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`
9124
+ };
9125
+ }
9126
+ if (record?.sha) {
9127
+ return checkViaProvenance(name, record, registryRepo, git);
9128
+ }
9129
+ return checkViaContentDiff(name, pluginDir2, registryRepo, { isLocalDir: false }, git);
9130
+ }
9131
+ function isFetchableUrl(origin) {
9132
+ return /^(https?|git|ssh):\/\//.test(origin) || /^[^/\\]+@[^:]+:/.test(origin);
9133
+ }
9134
+ async function checkViaProvenance(name, record, repoUrl, git) {
9135
+ const remoteHeadSha = await git.resolveDefaultBranchSha(repoUrl);
9136
+ if (!remoteHeadSha) {
9137
+ return {
9138
+ name,
9139
+ status: "cannot-tell",
9140
+ method: "unresolvable",
9141
+ detail: `could not reach ${repoUrl} (network or authentication failure)`
9142
+ };
9143
+ }
9144
+ if (remoteHeadSha === record.sha) {
9145
+ return {
9146
+ name,
9147
+ status: "up-to-date",
9148
+ method: "provenance",
9149
+ detail: `matches ${repoUrl}'s default branch (${shortSha(remoteHeadSha)})`
9150
+ };
9151
+ }
9152
+ let clone;
9153
+ try {
9154
+ clone = await git.cloneForEditing(repoUrl, "biffo-plugin-staleness-full");
9155
+ } catch {
9156
+ return {
9157
+ name,
9158
+ status: "cannot-tell",
9159
+ method: "unresolvable",
9160
+ detail: `could not clone ${repoUrl} to count commits behind (network or authentication failure)`
9161
+ };
9162
+ }
9163
+ try {
9164
+ const commitsBehind = await git.countBehind(clone, record.sha, "HEAD");
9165
+ if (commitsBehind === null) {
9166
+ return {
9167
+ name,
9168
+ status: "cannot-tell",
9169
+ method: "unresolvable",
9170
+ detail: `recorded commit ${shortSha(record.sha)} was not found in ${repoUrl}'s history (rebased or force-pushed?) \u2014 cannot count commits behind`
9171
+ };
9172
+ }
9173
+ if (commitsBehind === 0) {
9174
+ return {
9175
+ name,
9176
+ status: "up-to-date",
9177
+ method: "provenance",
9178
+ detail: `matches ${repoUrl}'s default branch (${shortSha(remoteHeadSha)})`
9179
+ };
9180
+ }
9181
+ return {
9182
+ name,
9183
+ status: "behind",
9184
+ commitsBehind,
9185
+ method: "provenance",
9186
+ detail: `${commitsBehind} commit(s) behind ${repoUrl}'s default branch`
9187
+ };
9188
+ } finally {
9189
+ git.cleanup(clone);
9190
+ }
9191
+ }
9192
+ async function checkViaContentDiff(name, pluginDir2, sourceDir, opts, git) {
9193
+ let cloneDir = null;
9194
+ let effectiveSourceDir = sourceDir;
9195
+ if (!opts.isLocalDir) {
9196
+ try {
9197
+ cloneDir = await git.cloneToTemp(sourceDir, "biffo-plugin-staleness-content");
9198
+ } catch {
9199
+ return {
9200
+ name,
9201
+ status: "cannot-tell",
9202
+ method: "unresolvable",
9203
+ detail: `could not clone ${sourceDir} for a content comparison (network or authentication failure)`
9204
+ };
9205
+ }
9206
+ effectiveSourceDir = cloneDir;
9207
+ }
9208
+ try {
9209
+ const filesDiffering = await countDifferingFiles(effectiveSourceDir, pluginDir2);
9210
+ const originDescription = opts.isLocalDir ? effectiveSourceDir : sourceDir;
9211
+ if (filesDiffering === 0) {
9212
+ return {
9213
+ name,
9214
+ status: "up-to-date",
9215
+ method: "content-diff",
9216
+ filesDiffering: 0,
9217
+ detail: `byte-identical to ${originDescription} (no provenance recorded, so an exact commit could not be named)`
9218
+ };
9219
+ }
9220
+ return {
9221
+ name,
9222
+ status: "behind",
9223
+ filesDiffering,
9224
+ method: "content-diff",
9225
+ detail: `${filesDiffering} file(s) differ from ${originDescription} (no provenance recorded, so an exact commit count could not be determined)`
9226
+ };
9227
+ } finally {
9228
+ if (cloneDir) git.cleanup(cloneDir);
9229
+ }
9230
+ }
9231
+ function shortSha(sha) {
9232
+ return sha.slice(0, 7);
9233
+ }
9234
+ async function countDifferingFiles(sourceDir, pluginDir2) {
9235
+ const sourceFiles = await sourceFileList(sourceDir);
9236
+ const vendorFiles = vendorFileList(pluginDir2);
9237
+ const allPaths = /* @__PURE__ */ new Set([...sourceFiles, ...vendorFiles]);
9238
+ let differing = 0;
9239
+ for (const relPath of allPaths) {
9240
+ if (relPath === PLUGIN_PROVENANCE_FILENAME) continue;
9241
+ const inSource = sourceFiles.has(relPath);
9242
+ const inVendor = vendorFiles.has(relPath);
9243
+ if (!inSource || !inVendor) {
9244
+ differing++;
9245
+ continue;
9246
+ }
9247
+ const a = readFileSync24(join33(sourceDir, relPath));
9248
+ const b = readFileSync24(join33(pluginDir2, relPath));
9249
+ if (!a.equals(b)) differing++;
9250
+ }
9251
+ return differing;
9252
+ }
9253
+ async function sourceFileList(dir) {
9254
+ if (await isGitWorkingTree2(dir)) {
9255
+ return new Set(await listGitFiles(dir));
9256
+ }
9257
+ return new Set(walkExcluding(dir, dir, LOCAL_COPY_EXCLUDES));
9258
+ }
9259
+ function vendorFileList(dir) {
9260
+ return new Set(walkExcluding(dir, dir, LOCAL_COPY_EXCLUDES));
9261
+ }
9262
+ function walkExcluding(root, dir, excludes) {
9263
+ if (!existsSync31(dir)) return [];
9264
+ const out = [];
9265
+ for (const entry of readdirSync13(dir)) {
9266
+ if (excludes.has(entry) || entry === ".git") continue;
9267
+ const full = join33(dir, entry);
9268
+ const stat = statSync7(full);
9269
+ if (stat.isDirectory()) {
9270
+ out.push(...walkExcluding(root, full, excludes));
9271
+ } else {
9272
+ out.push(relative4(root, full));
9273
+ }
9274
+ }
9275
+ return out;
9276
+ }
9277
+ function exitCodeForStaleness(results) {
9278
+ if (results.some((r) => r.status === "cannot-tell")) return 2;
9279
+ if (results.some((r) => r.status === "behind")) return 1;
9280
+ return 0;
9281
+ }
9282
+ function formatStalenessReport(results) {
9283
+ if (results.length === 0) {
9284
+ return " No vendored plugins under services/ \u2014 nothing to check.";
9285
+ }
9286
+ const lines = [""];
9287
+ for (const r of results) {
9288
+ const icon = r.status === "up-to-date" ? "\u2713" : r.status === "behind" ? "\u26A0" : "?";
9289
+ lines.push(` ${icon} ${r.name}: ${labelFor(r.status)} \u2014 ${r.detail}`);
9290
+ }
9291
+ lines.push("");
9292
+ return lines.join("\n");
9293
+ }
9294
+ function labelFor(status) {
9295
+ switch (status) {
9296
+ case "up-to-date":
9297
+ return "up to date";
9298
+ case "behind":
9299
+ return "BEHIND";
9300
+ case "cannot-tell":
9301
+ return "CANNOT TELL";
9302
+ }
9303
+ }
9304
+
9305
+ // src/commands/plugin-staleness.ts
9306
+ var pluginStalenessCommand = new Command17("staleness").description(
9307
+ "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."
9308
+ ).option("--cwd <path>", "Project root to check (defaults to the current directory)").action(async (options) => {
9309
+ const cwd = options.cwd ? resolve14(options.cwd) : process.cwd();
9310
+ const results = await checkPluginStaleness(cwd, {
9311
+ registry: new RegistryAdapter(),
9312
+ git: new GitAdapter()
9313
+ });
9314
+ console.log(formatStalenessReport(results));
9315
+ process.exit(exitCodeForStaleness(results));
9316
+ });
9317
+
8959
9318
  // 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";
9319
+ import { existsSync as existsSync32 } from "fs";
9320
+ import { join as join34, relative as relative5, resolve as resolve15 } from "path";
8962
9321
  import chalk17 from "chalk";
8963
- import { Command as Command17 } from "commander";
8964
- var pluginSyncMigrationsCommand = new Command17("sync-migrations").description(
9322
+ import { Command as Command18 } from "commander";
9323
+ var pluginSyncMigrationsCommand = new Command18("sync-migrations").description(
8965
9324
  "Generate real, committed migration file(s) for installed-but-not-yet-migrated plugin(s): biffo plugin sync-migrations [name]"
8966
9325
  ).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
9326
  async (name, options) => {
8968
- const cwd = options.cwd ? resolve14(options.cwd) : process.cwd();
9327
+ const cwd = options.cwd ? resolve15(options.cwd) : process.cwd();
8969
9328
  try {
8970
9329
  await runPluginSyncMigrations(
8971
9330
  name,
@@ -8979,11 +9338,11 @@ var pluginSyncMigrationsCommand = new Command17("sync-migrations").description(
8979
9338
  }
8980
9339
  );
8981
9340
  async function runPluginSyncMigrations(name, options, deps) {
8982
- const servicesDir = join32(options.cwd, "services");
8983
- if (!existsSync30(servicesDir)) {
9341
+ const servicesDir = join34(options.cwd, "services");
9342
+ if (!existsSync32(servicesDir)) {
8984
9343
  throw new Error(`${servicesDir} does not exist \u2014 is ${options.cwd} a Biffo project checkout?`);
8985
9344
  }
8986
- if (name && !existsSync30(join32(servicesDir, name, "biffo.plugin.json"))) {
9345
+ if (name && !existsSync32(join34(servicesDir, name, "biffo.plugin.json"))) {
8987
9346
  throw new Error(`Plugin '${name}' is not installed at services/${name}/.`);
8988
9347
  }
8989
9348
  if (options.dryRun) {
@@ -8999,7 +9358,7 @@ async function runPluginSyncMigrations(name, options, deps) {
8999
9358
  );
9000
9359
  return;
9001
9360
  }
9002
- const relativePaths = generated.map((p) => relative4(options.cwd, p));
9361
+ const relativePaths = generated.map((p) => relative5(options.cwd, p));
9003
9362
  for (const p of relativePaths) {
9004
9363
  log.success(`Generated ${p}`);
9005
9364
  }
@@ -9019,18 +9378,18 @@ async function runPluginSyncMigrations(name, options, deps) {
9019
9378
  }
9020
9379
 
9021
9380
  // 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";
9381
+ import { existsSync as existsSync33, readFileSync as readFileSync25, rmSync as rmSync8 } from "fs";
9382
+ import { join as join35, resolve as resolve16 } from "path";
9024
9383
  import chalk18 from "chalk";
9025
- import { Command as Command18 } from "commander";
9384
+ import { Command as Command19 } from "commander";
9026
9385
  import inquirer6 from "inquirer";
9027
9386
  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(
9387
+ 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
9388
  "--keep-data",
9030
9389
  "No-op today (see notes) \u2014 the CLI never drops plugin data regardless of this flag"
9031
9390
  ).option("--cwd <path>", "Project root to uninstall from (defaults to the current directory)").action(
9032
9391
  async (name, options) => {
9033
- const cwd = options.cwd ? resolve15(options.cwd) : process.cwd();
9392
+ const cwd = options.cwd ? resolve16(options.cwd) : process.cwd();
9034
9393
  try {
9035
9394
  await runPluginUninstall(
9036
9395
  name,
@@ -9052,16 +9411,16 @@ async function runPluginUninstall(name, options, deps) {
9052
9411
  if (!NAME_PATTERN2.test(name)) {
9053
9412
  throw new Error(`Invalid plugin name '${name}'. Expected a lowercase kebab-case slug.`);
9054
9413
  }
9055
- const servicesDir = join33(options.cwd, "services");
9056
- if (!existsSync31(servicesDir)) {
9414
+ const servicesDir = join35(options.cwd, "services");
9415
+ if (!existsSync33(servicesDir)) {
9057
9416
  throw new Error(
9058
9417
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
9059
9418
  );
9060
9419
  }
9061
- const targetDir = join33(servicesDir, name);
9062
- if (!existsSync31(targetDir)) {
9063
- const firstParty = join33(servicesDir, FIRST_PARTY_PLUGINS_DIR, name);
9064
- if (existsSync31(firstParty)) {
9420
+ const targetDir = join35(servicesDir, name);
9421
+ if (!existsSync33(targetDir)) {
9422
+ const firstParty = join35(servicesDir, FIRST_PARTY_PLUGINS_DIR, name);
9423
+ if (existsSync33(firstParty)) {
9065
9424
  throw new Error(
9066
9425
  `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
9426
  );
@@ -9069,9 +9428,9 @@ async function runPluginUninstall(name, options, deps) {
9069
9428
  throw new Error(`Plugin '${name}' is not installed at services/${name}/.`);
9070
9429
  }
9071
9430
  const version = readInstalledVersion(targetDir);
9072
- const modulesDir = join33(options.cwd, "modules", "plugins", name);
9431
+ const modulesDir = join35(options.cwd, "modules", "plugins", name);
9073
9432
  const stagePaths = [`services/${name}`];
9074
- if (existsSync31(modulesDir)) {
9433
+ if (existsSync33(modulesDir)) {
9075
9434
  stagePaths.push(`modules/plugins/${name}`);
9076
9435
  }
9077
9436
  if (options.dryRun) {
@@ -9093,7 +9452,7 @@ async function runPluginUninstall(name, options, deps) {
9093
9452
  }
9094
9453
  rmSync8(targetDir, { recursive: true, force: true });
9095
9454
  log.success(`Removed services/${name}/`);
9096
- if (existsSync31(modulesDir)) {
9455
+ if (existsSync33(modulesDir)) {
9097
9456
  rmSync8(modulesDir, { recursive: true, force: true });
9098
9457
  log.success(`Removed modules/plugins/${name}/`);
9099
9458
  const wiring = syncPluginTerraform(options.cwd);
@@ -9130,10 +9489,10 @@ async function runPluginUninstall(name, options, deps) {
9130
9489
  }
9131
9490
  }
9132
9491
  function readInstalledVersion(targetDir) {
9133
- const manifestPath = join33(targetDir, "biffo.plugin.json");
9134
- if (!existsSync31(manifestPath)) return void 0;
9492
+ const manifestPath = join35(targetDir, "biffo.plugin.json");
9493
+ if (!existsSync33(manifestPath)) return void 0;
9135
9494
  try {
9136
- return validateManifest(JSON.parse(readFileSync23(manifestPath, "utf8"))).version;
9495
+ return validateManifest(JSON.parse(readFileSync25(manifestPath, "utf8"))).version;
9137
9496
  } catch {
9138
9497
  return void 0;
9139
9498
  }
@@ -9166,12 +9525,12 @@ function printDryRun5(name, version, stagePaths, keepData) {
9166
9525
  }
9167
9526
 
9168
9527
  // 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";
9528
+ import { cpSync as cpSync5, existsSync as existsSync34, mkdirSync as mkdirSync11, readFileSync as readFileSync26, rmSync as rmSync9 } from "fs";
9529
+ import { join as join36, relative as relative6, resolve as resolve17 } from "path";
9171
9530
  import chalk19 from "chalk";
9172
- import { Command as Command19 } from "commander";
9531
+ import { Command as Command20 } from "commander";
9173
9532
  import inquirer7 from "inquirer";
9174
- var pluginUpgradeCommand = new Command19("upgrade").description(
9533
+ var pluginUpgradeCommand = new Command20("upgrade").description(
9175
9534
  "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
9535
  ).argument(
9177
9536
  "[target]",
@@ -9181,12 +9540,12 @@ var pluginUpgradeCommand = new Command19("upgrade").description(
9181
9540
  "Refresh the installed plugin from a local, unpublished checkout instead of the registry"
9182
9541
  ).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
9542
  async (target, options) => {
9184
- const cwd = options.cwd ? resolve16(options.cwd) : process.cwd();
9543
+ const cwd = options.cwd ? resolve17(options.cwd) : process.cwd();
9185
9544
  try {
9186
9545
  await runPluginUpgrade(
9187
9546
  target,
9188
9547
  {
9189
- ...options.local ? { local: resolve16(options.local) } : {},
9548
+ ...options.local ? { local: resolve17(options.local) } : {},
9190
9549
  dryRun: options.dryRun ?? false,
9191
9550
  force: options.force ?? false,
9192
9551
  cwd
@@ -9214,8 +9573,8 @@ async function runPluginUpgrade(target, options, deps) {
9214
9573
  `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
9574
  );
9216
9575
  }
9217
- const servicesDir = join34(options.cwd, "services");
9218
- if (!existsSync32(servicesDir)) {
9576
+ const servicesDir = join36(options.cwd, "services");
9577
+ if (!existsSync34(servicesDir)) {
9219
9578
  throw new Error(
9220
9579
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
9221
9580
  );
@@ -9224,8 +9583,8 @@ async function runPluginUpgrade(target, options, deps) {
9224
9583
  return runLocalPluginRefresh(options.local, options, deps);
9225
9584
  }
9226
9585
  const { name, minor } = parsePluginTarget(target);
9227
- const targetDir = join34(servicesDir, name);
9228
- if (!existsSync32(targetDir)) {
9586
+ const targetDir = join36(servicesDir, name);
9587
+ if (!existsSync34(targetDir)) {
9229
9588
  throw new Error(
9230
9589
  `Plugin '${name}' is not installed at services/${name}/. Use 'biffo plugin install ${name}@${minor}' instead.`
9231
9590
  );
@@ -9239,7 +9598,7 @@ async function runPluginUpgrade(target, options, deps) {
9239
9598
  `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
9599
  );
9241
9600
  }
9242
- const modulesDir = join34(options.cwd, "modules", "plugins", entry.name);
9601
+ const modulesDir = join36(options.cwd, "modules", "plugins", entry.name);
9243
9602
  if (options.dryRun) {
9244
9603
  printDryRun6(entry, currentVersion);
9245
9604
  return;
@@ -9267,17 +9626,23 @@ async function runPluginUpgrade(target, options, deps) {
9267
9626
  log.success(
9268
9627
  `Manifest valid \u2014 ${manifest.tables.length} table(s), ${manifest.api_routes.length} route(s)`
9269
9628
  );
9629
+ const previousProvenance = readProvenance(targetDir);
9270
9630
  rmSync9(targetDir, { recursive: true, force: true });
9271
9631
  mkdirSync11(targetDir, { recursive: true });
9272
9632
  cpSync5(tmpDir, targetDir, { recursive: true });
9273
9633
  log.success(`Upgraded plugin source at services/${entry.name}/`);
9634
+ const nextProvenance = resolveRegistryProvenance(
9635
+ entry.repo,
9636
+ await deps.git.resolveDefaultBranchSha(entry.repo)
9637
+ );
9638
+ writePluginProvenance(targetDir, reconcileProvenance(previousProvenance, nextProvenance));
9274
9639
  applyWorkspaceSources(targetDir, options.cwd, `services/${entry.name}`);
9275
9640
  const stagePaths = [`services/${entry.name}`];
9276
- if (existsSync32(modulesDir)) {
9641
+ if (existsSync34(modulesDir)) {
9277
9642
  rmSync9(modulesDir, { recursive: true, force: true });
9278
9643
  }
9279
- const tfSourceDir = join34(targetDir, "terraform");
9280
- if (existsSync32(tfSourceDir)) {
9644
+ const tfSourceDir = join36(targetDir, "terraform");
9645
+ if (existsSync34(tfSourceDir)) {
9281
9646
  mkdirSync11(modulesDir, { recursive: true });
9282
9647
  cpSync5(tfSourceDir, modulesDir, { recursive: true });
9283
9648
  stagePaths.push(`modules/plugins/${entry.name}`);
@@ -9289,10 +9654,10 @@ async function runPluginUpgrade(target, options, deps) {
9289
9654
  );
9290
9655
  const generatedPaths = await deps.migrations.generate(options.cwd, [entry.name]);
9291
9656
  for (const absPath of generatedPaths) {
9292
- stagePaths.push(relative5(options.cwd, absPath));
9657
+ stagePaths.push(relative6(options.cwd, absPath));
9293
9658
  }
9294
9659
  if (generatedPaths.length > 0) {
9295
- log.success(`Generated migration: ${relative5(options.cwd, generatedPaths[0])}`);
9660
+ log.success(`Generated migration: ${relative6(options.cwd, generatedPaths[0])}`);
9296
9661
  } else {
9297
9662
  log.info(
9298
9663
  `${entry.name}'s tables and columns already match the manifest \u2014 no migration needed.`
@@ -9317,16 +9682,16 @@ async function runPluginUpgrade(target, options, deps) {
9317
9682
  async function runLocalPluginRefresh(localPath, options, deps) {
9318
9683
  const source = resolveLocalPlugin(localPath);
9319
9684
  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)) {
9685
+ const servicesDir = join36(options.cwd, "services");
9686
+ const targetDir = join36(servicesDir, source.name);
9687
+ if (!existsSync34(targetDir)) {
9323
9688
  throw new Error(
9324
9689
  `Plugin '${source.name}' is not installed at services/${source.name}/. Use 'biffo plugin install --local ${localPath}' instead.`
9325
9690
  );
9326
9691
  }
9327
- const inTreeSource = resolve16(source.sourceDir) === resolve16(targetDir);
9692
+ const inTreeSource = resolve17(source.sourceDir) === resolve17(targetDir);
9328
9693
  const currentVersion = readInstalledVersion2(targetDir);
9329
- const modulesDir = join34(options.cwd, "modules", "plugins", source.name);
9694
+ const modulesDir = join36(options.cwd, "modules", "plugins", source.name);
9330
9695
  if (options.dryRun) {
9331
9696
  printLocalDryRun(source, currentVersion, inTreeSource);
9332
9697
  return;
@@ -9349,6 +9714,7 @@ async function runLocalPluginRefresh(localPath, options, deps) {
9349
9714
  log.success(
9350
9715
  `Manifest valid \u2014 ${manifest.tables.length} table(s), ${manifest.api_routes.length} route(s)`
9351
9716
  );
9717
+ const previousProvenance = readProvenance(targetDir);
9352
9718
  if (inTreeSource) {
9353
9719
  log.info(
9354
9720
  `services/${source.name}/ is already the local checkout \u2014 nothing to copy; re-syncing its Terraform module and checking for a migration.`
@@ -9359,13 +9725,15 @@ async function runLocalPluginRefresh(localPath, options, deps) {
9359
9725
  await copyPluginSource(source.sourceDir, targetDir);
9360
9726
  log.success(`Refreshed plugin source at services/${source.name}/ from ${source.origin}`);
9361
9727
  }
9728
+ const nextProvenance = inTreeSource ? inTreePluginProvenance(`services/${source.name}`) : await resolveLocalProvenance(source.sourceDir, source.origin);
9729
+ writePluginProvenance(targetDir, reconcileProvenance(previousProvenance, nextProvenance));
9362
9730
  applyWorkspaceSources(targetDir, options.cwd, `services/${source.name}`);
9363
9731
  const stagePaths = [`services/${source.name}`];
9364
- if (existsSync32(modulesDir)) {
9732
+ if (existsSync34(modulesDir)) {
9365
9733
  rmSync9(modulesDir, { recursive: true, force: true });
9366
9734
  }
9367
- const tfSourceDir = join34(targetDir, "terraform");
9368
- if (existsSync32(tfSourceDir)) {
9735
+ const tfSourceDir = join36(targetDir, "terraform");
9736
+ if (existsSync34(tfSourceDir)) {
9369
9737
  mkdirSync11(modulesDir, { recursive: true });
9370
9738
  cpSync5(tfSourceDir, modulesDir, { recursive: true });
9371
9739
  stagePaths.push(`modules/plugins/${source.name}`);
@@ -9377,10 +9745,10 @@ async function runLocalPluginRefresh(localPath, options, deps) {
9377
9745
  );
9378
9746
  const generatedPaths = await deps.migrations.generate(options.cwd, [source.name]);
9379
9747
  for (const absPath of generatedPaths) {
9380
- stagePaths.push(relative5(options.cwd, absPath));
9748
+ stagePaths.push(relative6(options.cwd, absPath));
9381
9749
  }
9382
9750
  if (generatedPaths.length > 0) {
9383
- log.success(`Generated migration: ${relative5(options.cwd, generatedPaths[0])}`);
9751
+ log.success(`Generated migration: ${relative6(options.cwd, generatedPaths[0])}`);
9384
9752
  } else {
9385
9753
  log.info(
9386
9754
  `${source.name}'s tables and columns already match the manifest \u2014 no migration needed.`
@@ -9408,10 +9776,10 @@ async function runLocalPluginRefresh(localPath, options, deps) {
9408
9776
  }
9409
9777
  }
9410
9778
  function readInstalledVersion2(targetDir) {
9411
- const manifestPath = join34(targetDir, "biffo.plugin.json");
9412
- if (!existsSync32(manifestPath)) return void 0;
9779
+ const manifestPath = join36(targetDir, "biffo.plugin.json");
9780
+ if (!existsSync34(manifestPath)) return void 0;
9413
9781
  try {
9414
- return validateManifest(JSON.parse(readFileSync24(manifestPath, "utf8"))).version;
9782
+ return validateManifest(JSON.parse(readFileSync26(manifestPath, "utf8"))).version;
9415
9783
  } catch {
9416
9784
  return void 0;
9417
9785
  }
@@ -9470,7 +9838,7 @@ function printLocalDryRun(source, currentVersion, inTreeSource) {
9470
9838
  }
9471
9839
 
9472
9840
  // src/commands/plugin.ts
9473
- var pluginCommand = new Command20("plugin").description("Manage Biffo plugins");
9841
+ var pluginCommand = new Command21("plugin").description("Manage Biffo plugins");
9474
9842
  pluginCommand.addCommand(pluginCreateCommand);
9475
9843
  pluginCommand.addCommand(pluginListCommand);
9476
9844
  pluginCommand.addCommand(pluginInstallCommand);
@@ -9478,15 +9846,16 @@ pluginCommand.addCommand(pluginUninstallCommand);
9478
9846
  pluginCommand.addCommand(pluginUpgradeCommand);
9479
9847
  pluginCommand.addCommand(pluginSyncMigrationsCommand);
9480
9848
  pluginCommand.addCommand(pluginInfoCommand);
9849
+ pluginCommand.addCommand(pluginStalenessCommand);
9481
9850
 
9482
9851
  // src/commands/sibling.ts
9483
- import { Command as Command22 } from "commander";
9852
+ import { Command as Command23 } from "commander";
9484
9853
 
9485
9854
  // src/commands/sibling-check-identity.ts
9486
- import { existsSync as existsSync33, readFileSync as readFileSync25 } from "fs";
9487
- import { resolve as resolve17 } from "path";
9855
+ import { existsSync as existsSync35, readFileSync as readFileSync27 } from "fs";
9856
+ import { resolve as resolve18 } from "path";
9488
9857
  import chalk20 from "chalk";
9489
- import { Command as Command21 } from "commander";
9858
+ import { Command as Command22 } from "commander";
9490
9859
 
9491
9860
  // src/lib/sibling-identity-check.ts
9492
9861
  function checkSiblingIdentity(envs) {
@@ -9538,7 +9907,7 @@ function checkSiblingIdentity(envs) {
9538
9907
  // src/commands/sibling-check-identity.ts
9539
9908
  var VALID_ENVIRONMENTS2 = ["dev", "staging", "prod"];
9540
9909
  var SIBLING_CORE_POOL_VAR = "CORE_COGNITO_USER_POOL_ID";
9541
- var siblingCheckIdentityCommand = new Command21("check-identity").description(
9910
+ var siblingCheckIdentityCommand = new Command22("check-identity").description(
9542
9911
  "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
9912
  ).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
9913
  if (options.env && !VALID_ENVIRONMENTS2.includes(options.env)) {
@@ -9677,7 +10046,7 @@ async function fetchPublishedIdentity(portalUrl) {
9677
10046
  }
9678
10047
  async function resolveConfig4(options) {
9679
10048
  if (options.config) {
9680
- const raw = JSON.parse(readFileSync25(resolve17(options.config), "utf8"));
10049
+ const raw = JSON.parse(readFileSync27(resolve18(options.config), "utf8"));
9681
10050
  const result = BiffoConfigSchema.safeParse(raw);
9682
10051
  if (!result.success) {
9683
10052
  log.error(`Invalid config at ${options.config}:`);
@@ -9696,9 +10065,9 @@ async function resolveConfig4(options) {
9696
10065
  }
9697
10066
  return cfg;
9698
10067
  }
9699
- const localConfigPath = resolve17(process.cwd(), "biffo.config.json");
9700
- if (existsSync33(localConfigPath)) {
9701
- const raw = JSON.parse(readFileSync25(localConfigPath, "utf8"));
10068
+ const localConfigPath = resolve18(process.cwd(), "biffo.config.json");
10069
+ if (existsSync35(localConfigPath)) {
10070
+ const raw = JSON.parse(readFileSync27(localConfigPath, "utf8"));
9702
10071
  const result = BiffoConfigSchema.safeParse(raw);
9703
10072
  if (result.success) return result.data;
9704
10073
  if (isTemplatePlaceholderConfig(raw)) {
@@ -9734,31 +10103,31 @@ async function resolveConfig4(options) {
9734
10103
  }
9735
10104
 
9736
10105
  // src/commands/sibling.ts
9737
- var siblingCommand = new Command22("sibling").description(
10106
+ var siblingCommand = new Command23("sibling").description(
9738
10107
  "Create and manage sibling apps that share a Biffo core project (ADR-0007)"
9739
10108
  );
9740
10109
  siblingCommand.addCommand(siblingCreateCommand);
9741
10110
  siblingCommand.addCommand(siblingCheckIdentityCommand);
9742
10111
 
9743
10112
  // src/commands/check.ts
9744
- import { Command as Command23 } from "commander";
10113
+ import { Command as Command24 } from "commander";
9745
10114
 
9746
10115
  // 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";
10116
+ import { existsSync as existsSync37 } from "fs";
10117
+ import { join as join38 } from "path";
10118
+ import { execa as execa7 } from "execa";
9750
10119
 
9751
10120
  // 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";
10121
+ import { existsSync as existsSync36, readdirSync as readdirSync14, readFileSync as readFileSync28 } from "fs";
10122
+ import { join as join37 } from "path";
9754
10123
  var ADR_FILENAME = /^(\d{4})-.+\.md$/;
9755
10124
  var ALLOWLIST_FILENAME = ".numbering-allowlist";
9756
10125
  var TEMPLATE_ADR_RESERVED_UPTO = "0099";
9757
10126
  function readAdrNumberingAllowlist(adrDir) {
9758
- const path = join35(adrDir, ALLOWLIST_FILENAME);
9759
- if (!existsSync34(path)) return /* @__PURE__ */ new Set();
10127
+ const path = join37(adrDir, ALLOWLIST_FILENAME);
10128
+ if (!existsSync36(path)) return /* @__PURE__ */ new Set();
9760
10129
  const numbers = /* @__PURE__ */ new Set();
9761
- for (const rawLine of readFileSync26(path, "utf8").split("\n")) {
10130
+ for (const rawLine of readFileSync28(path, "utf8").split("\n")) {
9762
10131
  const line = rawLine.split("#")[0].trim();
9763
10132
  if (line) numbers.add(line);
9764
10133
  }
@@ -9766,8 +10135,8 @@ function readAdrNumberingAllowlist(adrDir) {
9766
10135
  }
9767
10136
  function adrNumbersIn(adrDir) {
9768
10137
  const claims = /* @__PURE__ */ new Map();
9769
- if (!existsSync34(adrDir)) return claims;
9770
- for (const entry of readdirSync13(adrDir).sort()) {
10138
+ if (!existsSync36(adrDir)) return claims;
10139
+ for (const entry of readdirSync14(adrDir).sort()) {
9771
10140
  const match = ADR_FILENAME.exec(entry);
9772
10141
  if (!match) continue;
9773
10142
  const number = match[1];
@@ -9820,9 +10189,9 @@ function formatAdrReservedRangeViolations(violations, reservedUpTo = TEMPLATE_AD
9820
10189
 
9821
10190
  // src/scripts/check-adr-numbering.ts
9822
10191
  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)) {
10192
+ const root = (await execa7("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10193
+ const adrDir = join38(root, "docs", "ADR");
10194
+ if (!existsSync37(adrDir)) {
9826
10195
  console.log("\u2713 ADR numbering guard: no docs/ADR/ directory \u2014 nothing to compare");
9827
10196
  return;
9828
10197
  }
@@ -9861,7 +10230,7 @@ Already accepted? List it in docs/ADR/${ALLOWLIST_FILENAME} instead of leaving t
9861
10230
 
9862
10231
  // src/scripts/check-branch-protection.ts
9863
10232
  import { Octokit as Octokit2 } from "@octokit/rest";
9864
- import { execa as execa7 } from "execa";
10233
+ import { execa as execa8 } from "execa";
9865
10234
 
9866
10235
  // src/lib/branch-protection-apply.ts
9867
10236
  var CONTEXT_CONSISTENCY_THRESHOLD = 2 / 3;
@@ -9988,7 +10357,7 @@ async function resolveRepo(explicit) {
9988
10357
  }
9989
10358
  return { owner, repo };
9990
10359
  }
9991
- const { stdout } = await execa7("git", ["remote", "get-url", "origin"]);
10360
+ const { stdout } = await execa8("git", ["remote", "get-url", "origin"]);
9992
10361
  const m = /github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/.exec(stdout.trim());
9993
10362
  if (!m?.[1] || !m[2]) {
9994
10363
  console.error(
@@ -10120,13 +10489,13 @@ async function runBranchProtectionCheck(explicitRepo, options = {}) {
10120
10489
  }
10121
10490
 
10122
10491
  // 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";
10492
+ import { existsSync as existsSync38 } from "fs";
10493
+ import { join as join40, relative as relative7 } from "path";
10494
+ import { execa as execa9 } from "execa";
10126
10495
 
10127
10496
  // 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";
10497
+ import { readdirSync as readdirSync15, readFileSync as readFileSync29, statSync as statSync8 } from "fs";
10498
+ import { join as join39 } from "path";
10130
10499
  var SKIP_DIRS = /* @__PURE__ */ new Set([
10131
10500
  ".git",
10132
10501
  ".worktrees",
@@ -10152,15 +10521,15 @@ function walkSourceFiles(root) {
10152
10521
  const walk2 = (dir) => {
10153
10522
  let entries;
10154
10523
  try {
10155
- entries = readdirSync14(dir);
10524
+ entries = readdirSync15(dir);
10156
10525
  } catch {
10157
10526
  return;
10158
10527
  }
10159
10528
  for (const entry of entries) {
10160
- const p = join37(dir, entry);
10529
+ const p = join39(dir, entry);
10161
10530
  let st;
10162
10531
  try {
10163
- st = statSync7(p);
10532
+ st = statSync8(p);
10164
10533
  } catch {
10165
10534
  continue;
10166
10535
  }
@@ -10183,7 +10552,7 @@ function countSourceFiles(root) {
10183
10552
  function sweepCodeqlSuppressionComments(root) {
10184
10553
  const hits = [];
10185
10554
  for (const path of walkSourceFiles(root)) {
10186
- const text = readFileSync27(path, "utf8");
10555
+ const text = readFileSync29(path, "utf8");
10187
10556
  for (const line of findCodeqlSuppressionComments(text)) {
10188
10557
  hits.push({ path, line, text: text.split("\n")[line - 1] ?? "" });
10189
10558
  }
@@ -10193,9 +10562,9 @@ function sweepCodeqlSuppressionComments(root) {
10193
10562
 
10194
10563
  // src/scripts/check-codeql-suppression.ts
10195
10564
  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)) {
10565
+ const root = (await execa9("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10566
+ const scanRoot = join40(root, "cli", "src");
10567
+ if (!existsSync38(scanRoot)) {
10199
10568
  console.log(
10200
10569
  "\u2014 codeql-suppression guard: skipped \u2014 no cli/src in this repo, so there is no CLI source to scan."
10201
10570
  );
@@ -10207,7 +10576,7 @@ async function runCodeqlSuppressionCheck() {
10207
10576
  "\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
10577
  );
10209
10578
  for (const hit of hits) {
10210
- console.error(` ${relative6(root, hit.path)}:${hit.line} ${hit.text.trim()}`);
10579
+ console.error(` ${relative7(root, hit.path)}:${hit.line} ${hit.text.trim()}`);
10211
10580
  }
10212
10581
  process.exit(1);
10213
10582
  }
@@ -10219,11 +10588,11 @@ async function runCodeqlSuppressionCheck() {
10219
10588
  }
10220
10589
 
10221
10590
  // src/scripts/check-cognito-invite-template.ts
10222
- import { execa as execa9 } from "execa";
10591
+ import { execa as execa10 } from "execa";
10223
10592
 
10224
10593
  // 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";
10594
+ import { readdirSync as readdirSync16, readFileSync as readFileSync30, statSync as statSync9 } from "fs";
10595
+ import { join as join41 } from "path";
10227
10596
  var REQUIRED_INVITE_MEMBERS = ["email_subject", "email_message", "sms_message"];
10228
10597
  var REQUIRED_INVITE_PLACEHOLDERS = ["{username}", "{####}"];
10229
10598
  var PLACEHOLDER_MEMBERS = ["email_message", "sms_message"];
@@ -10297,36 +10666,36 @@ function memberBody(blockBody, member) {
10297
10666
  }
10298
10667
  function findModuleTerraformFiles(repoRoot) {
10299
10668
  const found = [];
10300
- const walk2 = (dir, relative9) => {
10669
+ const walk2 = (dir, relative10) => {
10301
10670
  let entries;
10302
10671
  try {
10303
- entries = readdirSync15(dir);
10672
+ entries = readdirSync16(dir);
10304
10673
  } catch {
10305
10674
  return;
10306
10675
  }
10307
10676
  for (const entry of entries) {
10308
10677
  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()) {
10678
+ const full = join41(dir, entry);
10679
+ const rel = `${relative10}/${entry}`;
10680
+ if (statSync9(full).isDirectory()) {
10312
10681
  walk2(full, rel);
10313
10682
  } else if (entry.endsWith(".tf")) {
10314
10683
  found.push(rel);
10315
10684
  }
10316
10685
  }
10317
10686
  };
10318
- walk2(join39(repoRoot, "modules"), "modules");
10687
+ walk2(join41(repoRoot, "modules"), "modules");
10319
10688
  return found.sort();
10320
10689
  }
10321
10690
  function checkCognitoInviteTemplates(repoRoot) {
10322
10691
  return findModuleTerraformFiles(repoRoot).flatMap(
10323
- (file) => checkInviteTemplateSource(file, readFileSync28(join39(repoRoot, file), "utf8"))
10692
+ (file) => checkInviteTemplateSource(file, readFileSync30(join41(repoRoot, file), "utf8"))
10324
10693
  );
10325
10694
  }
10326
10695
 
10327
10696
  // src/scripts/check-cognito-invite-template.ts
10328
10697
  async function runCognitoInviteTemplateCheck() {
10329
- const root = (await execa9("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10698
+ const root = (await execa10("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10330
10699
  const files = findModuleTerraformFiles(root);
10331
10700
  console.log(`audited ${files.length} .tf file(s) under modules/ under ${root}`);
10332
10701
  if (files.length === 0) {
@@ -10348,12 +10717,12 @@ async function runCognitoInviteTemplateCheck() {
10348
10717
  }
10349
10718
 
10350
10719
  // src/scripts/check-core-direct-paths.ts
10351
- import { join as join41 } from "path";
10352
- import { execa as execa10 } from "execa";
10720
+ import { join as join43 } from "path";
10721
+ import { execa as execa11 } from "execa";
10353
10722
 
10354
10723
  // 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";
10724
+ import { existsSync as existsSync39, readFileSync as readFileSync31, readdirSync as readdirSync17, statSync as statSync10 } from "fs";
10725
+ import { join as join42 } from "path";
10357
10726
  var EXTERNAL_BASE_IDENTIFIERS = ["CORE_API_URL"];
10358
10727
  var API_ROUTE_PREFIX = "/api/v1";
10359
10728
  var TEST_FILE_SUFFIXES = [".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx"];
@@ -10512,15 +10881,15 @@ function walkFiles(root, accept, skipDir) {
10512
10881
  const walk2 = (dir) => {
10513
10882
  let entries;
10514
10883
  try {
10515
- entries = readdirSync16(dir);
10884
+ entries = readdirSync17(dir);
10516
10885
  } catch {
10517
10886
  return;
10518
10887
  }
10519
10888
  for (const entry of entries) {
10520
- const p = join40(dir, entry);
10889
+ const p = join42(dir, entry);
10521
10890
  let st;
10522
10891
  try {
10523
- st = statSync9(p);
10892
+ st = statSync10(p);
10524
10893
  } catch {
10525
10894
  continue;
10526
10895
  }
@@ -10547,7 +10916,7 @@ function auditFrontendExtraction(frontendSrcDir, externalBases = EXTERNAL_BASE_I
10547
10916
  const extracted = [];
10548
10917
  let rawTotal = 0;
10549
10918
  for (const file of files) {
10550
- const text = readFileSync29(file, "utf8");
10919
+ const text = readFileSync31(file, "utf8");
10551
10920
  rawTotal += countRawExternalOccurrences(text, externalBases);
10552
10921
  extracted.push(...extractCoreDirectPaths(text, file, externalBases));
10553
10922
  }
@@ -10596,7 +10965,7 @@ function auditCoreRouteExtraction(apiSrcDir) {
10596
10965
  const prefixSet = /* @__PURE__ */ new Set();
10597
10966
  let rawApiRouterCount = 0;
10598
10967
  for (const file of files) {
10599
- const text = readFileSync29(file, "utf8");
10968
+ const text = readFileSync31(file, "utf8");
10600
10969
  const extraction = extractCoreRoutePrefixes(text);
10601
10970
  rawApiRouterCount += extraction.rawApiRouterCount;
10602
10971
  for (const p of extraction.prefixes) prefixSet.add(normalizePrefix(p));
@@ -10612,10 +10981,10 @@ function pathMatchesAnyCorePrefix(normalized, corePrefixes, apiRoutePrefix = API
10612
10981
  }
10613
10982
  function resolveSiblingCoreSrc(params) {
10614
10983
  const { estateDir, sibling } = params;
10615
- const configPath = join40(estateDir, sibling, "biffo.sibling.json");
10984
+ const configPath = join42(estateDir, sibling, "biffo.sibling.json");
10616
10985
  let raw;
10617
10986
  try {
10618
- raw = readFileSync29(configPath, "utf8");
10987
+ raw = readFileSync31(configPath, "utf8");
10619
10988
  } catch (err) {
10620
10989
  throw new Error(
10621
10990
  `cannot resolve ${sibling}'s core: ${configPath} does not exist or is unreadable (${err.message}) -- refusing to guess which core serves this sibling.`
@@ -10635,8 +11004,8 @@ function resolveSiblingCoreSrc(params) {
10635
11004
  `cannot resolve ${sibling}'s core: ${configPath} has no non-empty "core_project" field.`
10636
11005
  );
10637
11006
  }
10638
- const coreApiSrcDir = join40(estateDir, coreProject, "services", "api", "src");
10639
- if (!existsSync37(coreApiSrcDir)) {
11007
+ const coreApiSrcDir = join42(estateDir, coreProject, "services", "api", "src");
11008
+ if (!existsSync39(coreApiSrcDir)) {
10640
11009
  throw new Error(
10641
11010
  `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
11011
  );
@@ -10677,9 +11046,9 @@ function auditSiblingCoreDirectPaths(params) {
10677
11046
 
10678
11047
  // src/scripts/check-core-direct-paths.ts
10679
11048
  async function runCoreDirectPathsCheck(opts = {}) {
10680
- const root = (await execa10("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11049
+ const root = (await execa11("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10681
11050
  const sibling = opts.sibling ?? "sibling-template (self-check)";
10682
- const frontendSrcDir = opts.frontendSrc ?? join41(root, "_skeletons", "sibling-template", "apps", "frontend", "src");
11051
+ const frontendSrcDir = opts.frontendSrc ?? join43(root, "_skeletons", "sibling-template", "apps", "frontend", "src");
10683
11052
  let coreApiSrcDir;
10684
11053
  let coreProject = null;
10685
11054
  if (opts.coreSrc) {
@@ -10695,7 +11064,7 @@ async function runCoreDirectPathsCheck(opts = {}) {
10695
11064
  coreApiSrcDir = resolution.coreApiSrcDir;
10696
11065
  coreProject = resolution.coreProject;
10697
11066
  } else {
10698
- coreApiSrcDir = join41(root, "services", "api", "src");
11067
+ coreApiSrcDir = join43(root, "services", "api", "src");
10699
11068
  }
10700
11069
  const report = auditSiblingCoreDirectPaths({ sibling, frontendSrcDir, coreApiSrcDir });
10701
11070
  console.log(
@@ -10731,7 +11100,7 @@ async function runCoreDirectPathsCheck(opts = {}) {
10731
11100
  }
10732
11101
 
10733
11102
  // src/scripts/check-core-ownership.ts
10734
- import { execa as execa11 } from "execa";
11103
+ import { execa as execa12 } from "execa";
10735
11104
  var BOLD = "\x1B[1m";
10736
11105
  var DIM = "\x1B[2m";
10737
11106
  var RED = "\x1B[31m";
@@ -10742,7 +11111,7 @@ async function runOwnershipCheck(argv) {
10742
11111
  const stagedFlag = args.indexOf("--staged");
10743
11112
  const staged = stagedFlag !== -1;
10744
11113
  const messageFile = staged ? args[stagedFlag + 1] : void 0;
10745
- const root = (await execa11("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11114
+ const root = (await execa12("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10746
11115
  const ownership = classifyRepoOwnership(root);
10747
11116
  if (ownership === "template") {
10748
11117
  console.log("\u2713 core ownership guard: skipped \u2014 this is the template, which owns these paths.");
@@ -10758,11 +11127,11 @@ async function runOwnershipCheck(argv) {
10758
11127
  let deletedFiles = [];
10759
11128
  let commitMessage = "";
10760
11129
  if (staged) {
10761
- const { stdout } = await execa11("git", ["diff", "--cached", "--name-status"], { cwd: root });
11130
+ const { stdout } = await execa12("git", ["diff", "--cached", "--name-status"], { cwd: root });
10762
11131
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
10763
11132
  if (messageFile) {
10764
- const { readFileSync: readFileSync39, existsSync: existsSync46 } = await import("fs");
10765
- if (existsSync46(messageFile)) commitMessage = readFileSync39(messageFile, "utf8");
11133
+ const { readFileSync: readFileSync41, existsSync: existsSync48 } = await import("fs");
11134
+ if (existsSync48(messageFile)) commitMessage = readFileSync41(messageFile, "utf8");
10766
11135
  }
10767
11136
  } else {
10768
11137
  const base = process.env["GITHUB_BASE_REF"] ?? args[0];
@@ -10770,18 +11139,18 @@ async function runOwnershipCheck(argv) {
10770
11139
  console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
10771
11140
  process.exit(2);
10772
11141
  }
10773
- await execa11("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
10774
- const { stdout } = await execa11("git", ["diff", "--name-status", `origin/${base}...HEAD`], {
11142
+ await execa12("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
11143
+ const { stdout } = await execa12("git", ["diff", "--name-status", `origin/${base}...HEAD`], {
10775
11144
  cwd: root
10776
11145
  });
10777
11146
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
10778
- const { stdout: log2 } = await execa11("git", ["log", "--format=%B", `origin/${base}..HEAD`], {
11147
+ const { stdout: log2 } = await execa12("git", ["log", "--format=%B", `origin/${base}..HEAD`], {
10779
11148
  cwd: root,
10780
11149
  reject: false
10781
11150
  });
10782
11151
  commitMessage = log2;
10783
11152
  }
10784
- const { stdout: gitBranch } = await execa11("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
11153
+ const { stdout: gitBranch } = await execa12("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
10785
11154
  cwd: root,
10786
11155
  reject: false
10787
11156
  });
@@ -10863,11 +11232,11 @@ ${BOLD}If the divergence is deliberate${OFF}
10863
11232
  }
10864
11233
 
10865
11234
  // src/scripts/check-eventbridge-log-permissions.ts
10866
- import { execa as execa12 } from "execa";
11235
+ import { execa as execa13 } from "execa";
10867
11236
 
10868
11237
  // 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";
11238
+ import { readFileSync as readFileSync32, readdirSync as readdirSync18, statSync as statSync11 } from "fs";
11239
+ import { join as join44 } from "path";
10871
11240
  var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", ".terraform", ".worktrees", "dist"]);
10872
11241
  var EVENT_TARGET_TYPE = "aws_cloudwatch_event_target";
10873
11242
  var LOG_RESOURCE_POLICY_TYPE = "aws_cloudwatch_log_resource_policy";
@@ -10939,15 +11308,15 @@ function walkTerraformFiles(root) {
10939
11308
  const walk2 = (dir) => {
10940
11309
  let entries;
10941
11310
  try {
10942
- entries = readdirSync17(dir);
11311
+ entries = readdirSync18(dir);
10943
11312
  } catch {
10944
11313
  return;
10945
11314
  }
10946
11315
  for (const entry of entries) {
10947
- const p = join42(dir, entry);
11316
+ const p = join44(dir, entry);
10948
11317
  let st;
10949
11318
  try {
10950
- st = statSync10(p);
11319
+ st = statSync11(p);
10951
11320
  } catch {
10952
11321
  continue;
10953
11322
  }
@@ -10987,7 +11356,7 @@ function auditEventBridgeLogPermissions(root) {
10987
11356
  let rawEventTargetCount = 0;
10988
11357
  let rawLogPolicyCount = 0;
10989
11358
  for (const file of files) {
10990
- const text = readFileSync30(file, "utf8");
11359
+ const text = readFileSync32(file, "utf8");
10991
11360
  rawEventTargetCount += countRawResourceDeclarations(text, EVENT_TARGET_TYPE);
10992
11361
  rawLogPolicyCount += countRawResourceDeclarations(text, LOG_RESOURCE_POLICY_TYPE);
10993
11362
  eventTargetBlocks.push(...findResourceBlocks(text, file, EVENT_TARGET_TYPE));
@@ -11040,7 +11409,7 @@ function auditEventBridgeLogPermissions(root) {
11040
11409
 
11041
11410
  // src/scripts/check-eventbridge-log-permissions.ts
11042
11411
  async function runEventBridgeLogPermissionCheck() {
11043
- const root = (await execa12("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11412
+ const root = (await execa13("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11044
11413
  let report;
11045
11414
  try {
11046
11415
  report = auditEventBridgeLogPermissions(root);
@@ -11077,15 +11446,15 @@ async function runEventBridgeLogPermissionCheck() {
11077
11446
  }
11078
11447
 
11079
11448
  // src/scripts/check-lambda-output.ts
11080
- import { execa as execa13 } from "execa";
11449
+ import { execa as execa14 } from "execa";
11081
11450
 
11082
11451
  // src/lib/lambda-output-guard.ts
11083
- import { readFileSync as readFileSync32 } from "fs";
11084
- import { join as join44 } from "path";
11452
+ import { readFileSync as readFileSync34 } from "fs";
11453
+ import { join as join46 } from "path";
11085
11454
 
11086
11455
  // 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";
11456
+ import { readdirSync as readdirSync19, readFileSync as readFileSync33, statSync as statSync12 } from "fs";
11457
+ import { join as join45 } from "path";
11089
11458
  var GUARDED_SUBCOMMANDS = [
11090
11459
  "init",
11091
11460
  "plan",
@@ -11100,20 +11469,20 @@ function stripComments2(source) {
11100
11469
  }
11101
11470
  function findWorkflowFiles(repoRoot) {
11102
11471
  const found = [];
11103
- const walk2 = (dir, relative9) => {
11472
+ const walk2 = (dir, relative10) => {
11104
11473
  let entries;
11105
11474
  try {
11106
- entries = readdirSync18(dir);
11475
+ entries = readdirSync19(dir);
11107
11476
  } catch {
11108
11477
  return;
11109
11478
  }
11110
11479
  for (const entry of entries) {
11111
11480
  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()) {
11481
+ const full = join45(dir, entry);
11482
+ const rel = relative10 ? `${relative10}/${entry}` : entry;
11483
+ if (statSync12(full).isDirectory()) {
11115
11484
  walk2(full, rel);
11116
- } else if (/\.ya?ml$/.test(entry) && relative9.endsWith(".github/workflows")) {
11485
+ } else if (/\.ya?ml$/.test(entry) && relative10.endsWith(".github/workflows")) {
11117
11486
  found.push(rel);
11118
11487
  }
11119
11488
  }
@@ -11153,7 +11522,7 @@ function checkWorkflowSource(file, rawSource) {
11153
11522
  }
11154
11523
  function checkTerraformInput(repoRoot) {
11155
11524
  return findWorkflowFiles(repoRoot).flatMap(
11156
- (file) => checkWorkflowSource(file, readFileSync31(join43(repoRoot, file), "utf8"))
11525
+ (file) => checkWorkflowSource(file, readFileSync33(join45(repoRoot, file), "utf8"))
11157
11526
  );
11158
11527
  }
11159
11528
 
@@ -11211,13 +11580,13 @@ function checkWorkflowSource2(file, rawSource) {
11211
11580
  }
11212
11581
  function checkLambdaOutput(repoRoot) {
11213
11582
  return findWorkflowFiles(repoRoot).flatMap(
11214
- (file) => checkWorkflowSource2(file, readFileSync32(join44(repoRoot, file), "utf8"))
11583
+ (file) => checkWorkflowSource2(file, readFileSync34(join46(repoRoot, file), "utf8"))
11215
11584
  );
11216
11585
  }
11217
11586
 
11218
11587
  // src/scripts/check-lambda-output.ts
11219
11588
  async function runLambdaOutputCheck() {
11220
- const root = (await execa13("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11589
+ const root = (await execa14("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11221
11590
  const files = findWorkflowFiles(root);
11222
11591
  console.log(`audited ${files.length} workflow file(s) under ${root}`);
11223
11592
  if (files.length === 0) {
@@ -11239,9 +11608,9 @@ async function runLambdaOutputCheck() {
11239
11608
  }
11240
11609
 
11241
11610
  // 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";
11611
+ import { readFileSync as readFileSync35, readdirSync as readdirSync20 } from "fs";
11612
+ import { join as join47, relative as relative8 } from "path";
11613
+ import { execa as execa15 } from "execa";
11245
11614
 
11246
11615
  // src/lib/pipe-trap-guard.ts
11247
11616
  var STATUS_BEARING = [
@@ -11337,23 +11706,23 @@ function findPipeTraps(source) {
11337
11706
  function shellFiles(root) {
11338
11707
  const out = [];
11339
11708
  for (const dir of ["scripts", ".githooks"]) {
11340
- const full = join45(root, dir);
11709
+ const full = join47(root, dir);
11341
11710
  let entries;
11342
11711
  try {
11343
- entries = readdirSync19(full, { withFileTypes: true });
11712
+ entries = readdirSync20(full, { withFileTypes: true });
11344
11713
  } catch {
11345
11714
  continue;
11346
11715
  }
11347
11716
  for (const entry of entries) {
11348
11717
  if (!entry.isFile()) continue;
11349
11718
  if (dir === "scripts" && !entry.name.endsWith(".sh")) continue;
11350
- out.push(join45(full, entry.name));
11719
+ out.push(join47(full, entry.name));
11351
11720
  }
11352
11721
  }
11353
11722
  return out;
11354
11723
  }
11355
11724
  async function runPipeTrapCheck() {
11356
- const root = (await execa14("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11725
+ const root = (await execa15("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11357
11726
  const files = shellFiles(root);
11358
11727
  console.log(`audited ${files.length} shell file(s) under scripts/ and .githooks/ under ${root}`);
11359
11728
  if (files.length === 0) {
@@ -11363,8 +11732,8 @@ async function runPipeTrapCheck() {
11363
11732
  process.exit(1);
11364
11733
  }
11365
11734
  const findings = files.flatMap(
11366
- (file) => findPipeTraps(readFileSync33(file, "utf8")).map(
11367
- (t) => `${relative7(root, file)}:${t.line} ${t.text}
11735
+ (file) => findPipeTraps(readFileSync35(file, "utf8")).map(
11736
+ (t) => `${relative8(root, file)}:${t.line} ${t.text}
11368
11737
  ${t.reason}`
11369
11738
  )
11370
11739
  );
@@ -11380,11 +11749,11 @@ async function runPipeTrapCheck() {
11380
11749
  }
11381
11750
 
11382
11751
  // src/scripts/check-plugin-allowlist-convention.ts
11383
- import { execa as execa15 } from "execa";
11752
+ import { execa as execa16 } from "execa";
11384
11753
 
11385
11754
  // src/lib/plugin-allowlist-convention.ts
11386
- import { readFileSync as readFileSync34 } from "fs";
11387
- import { join as join46 } from "path";
11755
+ import { readFileSync as readFileSync36 } from "fs";
11756
+ import { join as join48 } from "path";
11388
11757
  var COMPUTE_MAIN_TF = "modules/cloud/aws/compute/main.tf";
11389
11758
  var PLUGIN_TEMPLATE_MAIN_TF = "modules/plugins/_template/main.tf";
11390
11759
  var ALLOWLIST_MAIN_TF = "modules/cloud/aws/plugin-allowlist/main.tf";
@@ -11393,18 +11762,18 @@ var PROJECT = "<project>";
11393
11762
  var ENV = "<env>";
11394
11763
  var PLUGIN = "<plugin>";
11395
11764
  var ACCOUNT = "<account>";
11396
- function read(repoRoot, relative9) {
11765
+ function read(repoRoot, relative10) {
11397
11766
  try {
11398
- return readFileSync34(join46(repoRoot, relative9), "utf8");
11767
+ return readFileSync36(join48(repoRoot, relative10), "utf8");
11399
11768
  } catch {
11400
- throw new Error(`plugin-allowlist drift guard: cannot read ${relative9}`);
11769
+ throw new Error(`plugin-allowlist drift guard: cannot read ${relative10}`);
11401
11770
  }
11402
11771
  }
11403
11772
  function assignedString(source, name) {
11404
11773
  const match = new RegExp(`^\\s*${name}\\s*=\\s*"((?:[^"\\\\]|\\\\.)*)"\\s*$`, "m").exec(source);
11405
11774
  return match?.[1];
11406
11775
  }
11407
- function resolve18(expression, bindings) {
11776
+ function resolve19(expression, bindings) {
11408
11777
  let current = expression;
11409
11778
  for (let pass = 0; pass < 10; pass += 1) {
11410
11779
  const next = current.replace(/\$\{([^}]+)\}/g, (whole, ref) => {
@@ -11441,13 +11810,13 @@ function composeExpectedRoleName(repoRoot) {
11441
11810
  "var.project_name": PROJECT,
11442
11811
  "var.environment": ENV,
11443
11812
  "var.plugin_name": PLUGIN,
11444
- "var.function_name": resolve18(pluginFunctionName, {
11813
+ "var.function_name": resolve19(pluginFunctionName, {
11445
11814
  "var.plugin_name": PLUGIN
11446
11815
  })
11447
11816
  };
11448
- bindings["local.name_prefix"] = resolve18(namePrefix, bindings);
11449
- bindings["local.function_name"] = resolve18(functionName, bindings);
11450
- return resolve18(roleName, bindings);
11817
+ bindings["local.name_prefix"] = resolve19(namePrefix, bindings);
11818
+ bindings["local.function_name"] = resolve19(functionName, bindings);
11819
+ return resolve19(roleName, bindings);
11451
11820
  }
11452
11821
  function readAllowlistGlob(repoRoot) {
11453
11822
  const allowlist = read(repoRoot, ALLOWLIST_MAIN_TF);
@@ -11457,7 +11826,7 @@ function readAllowlistGlob(repoRoot) {
11457
11826
  `plugin-allowlist drift guard: could not find the "for name in var.enabled_plugins" glob in ${ALLOWLIST_MAIN_TF}.`
11458
11827
  );
11459
11828
  }
11460
- return resolve18(glob, {
11829
+ return resolve19(glob, {
11461
11830
  "data.aws_caller_identity.current.account_id": ACCOUNT,
11462
11831
  "var.project_name": PROJECT,
11463
11832
  "var.environment": ENV,
@@ -11491,7 +11860,7 @@ Plugins would be rejected by require_service_principal (ADR-0009). Fix the glob,
11491
11860
 
11492
11861
  // src/scripts/check-plugin-allowlist-convention.ts
11493
11862
  async function runPluginAllowlistConventionCheck() {
11494
- const root = (await execa15("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11863
+ const root = (await execa16("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11495
11864
  let violations;
11496
11865
  try {
11497
11866
  violations = checkAllowlistConvention(root);
@@ -11516,34 +11885,34 @@ async function runPluginAllowlistConventionCheck() {
11516
11885
  }
11517
11886
 
11518
11887
  // 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";
11888
+ import { existsSync as existsSync41 } from "fs";
11889
+ import { join as join50 } from "path";
11890
+ import { execa as execa17 } from "execa";
11522
11891
 
11523
11892
  // 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";
11893
+ import { existsSync as existsSync40, readdirSync as readdirSync21, statSync as statSync13 } from "fs";
11894
+ import { join as join49 } from "path";
11526
11895
  var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
11527
11896
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
11528
11897
  function subdirectories(dir) {
11529
- if (!existsSync38(dir)) return [];
11530
- return readdirSync20(dir).filter((entry) => {
11898
+ if (!existsSync40(dir)) return [];
11899
+ return readdirSync21(dir).filter((entry) => {
11531
11900
  if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
11532
11901
  try {
11533
- return statSync12(join47(dir, entry)).isDirectory();
11902
+ return statSync13(join49(dir, entry)).isDirectory();
11534
11903
  } catch {
11535
11904
  return false;
11536
11905
  }
11537
11906
  });
11538
11907
  }
11539
11908
  function regularPackagesOf(pluginDir2) {
11540
- return subdirectories(pluginDir2).filter((name) => existsSync38(join47(pluginDir2, name, "__init__.py"))).sort();
11909
+ return subdirectories(pluginDir2).filter((name) => existsSync40(join49(pluginDir2, name, "__init__.py"))).sort();
11541
11910
  }
11542
11911
  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();
11912
+ const testsDir = join49(pluginDir2, "tests");
11913
+ if (!existsSync40(testsDir)) return [];
11914
+ if (existsSync40(join49(testsDir, "__init__.py"))) return [];
11915
+ return readdirSync21(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
11547
11916
  }
11548
11917
  function findCollisions(servicesDir, pluginDirs) {
11549
11918
  const plugins = (pluginDirs ?? subdirectories(servicesDir)).filter((name) => !name.startsWith("_")).filter((name) => name !== "api").sort();
@@ -11551,7 +11920,7 @@ function findCollisions(servicesDir, pluginDirs) {
11551
11920
  const gather = (kind, namesOf) => {
11552
11921
  const claims = /* @__PURE__ */ new Map();
11553
11922
  for (const plugin of plugins) {
11554
- for (const name of namesOf(join47(servicesDir, plugin))) {
11923
+ for (const name of namesOf(join49(servicesDir, plugin))) {
11555
11924
  claims.set(name, [...claims.get(name) ?? [], plugin]);
11556
11925
  }
11557
11926
  }
@@ -11588,9 +11957,9 @@ function formatCollisions(collisions) {
11588
11957
 
11589
11958
  // src/scripts/check-plugin-collisions.ts
11590
11959
  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)) {
11960
+ const root = (await execa17("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11961
+ const servicesDir = join50(root, "services");
11962
+ if (!existsSync41(servicesDir)) {
11594
11963
  console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
11595
11964
  return;
11596
11965
  }
@@ -11607,11 +11976,11 @@ async function runPluginCollisionCheck() {
11607
11976
  }
11608
11977
 
11609
11978
  // src/scripts/check-plugin-terraform.ts
11610
- import { execa as execa17 } from "execa";
11979
+ import { execa as execa18 } from "execa";
11611
11980
 
11612
11981
  // 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";
11982
+ import { existsSync as existsSync42, readFileSync as readFileSync37, readdirSync as readdirSync22 } from "fs";
11983
+ import { dirname as dirname10, join as join51, relative as relative9, sep as sep3 } from "path";
11615
11984
  var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
11616
11985
  var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
11617
11986
  function findPluginManifests(root) {
@@ -11619,16 +11988,16 @@ function findPluginManifests(root) {
11619
11988
  const walk2 = (dir) => {
11620
11989
  let entries;
11621
11990
  try {
11622
- entries = readdirSync21(dir, { withFileTypes: true });
11991
+ entries = readdirSync22(dir, { withFileTypes: true });
11623
11992
  } catch {
11624
11993
  return;
11625
11994
  }
11626
11995
  for (const entry of entries) {
11627
11996
  if (entry.isDirectory()) {
11628
11997
  if (SKIP_DIRS3.has(entry.name)) continue;
11629
- walk2(join49(dir, entry.name));
11998
+ walk2(join51(dir, entry.name));
11630
11999
  } else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
11631
- found.push(relative8(root, join49(dir, entry.name)).split(sep3).join("/"));
12000
+ found.push(relative9(root, join51(dir, entry.name)).split(sep3).join("/"));
11632
12001
  }
11633
12002
  }
11634
12003
  };
@@ -11638,7 +12007,7 @@ function findPluginManifests(root) {
11638
12007
  function readSubscriptions(absManifestPath) {
11639
12008
  let parsed;
11640
12009
  try {
11641
- parsed = JSON.parse(readFileSync35(absManifestPath, "utf8"));
12010
+ parsed = JSON.parse(readFileSync37(absManifestPath, "utf8"));
11642
12011
  } catch {
11643
12012
  return null;
11644
12013
  }
@@ -11653,15 +12022,15 @@ function readSubscriptions(absManifestPath) {
11653
12022
  }
11654
12023
  function checkPluginTerraform(root) {
11655
12024
  const violations = [];
11656
- const coreManifest = existsSync40(join49(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
12025
+ const coreManifest = existsSync42(join51(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
11657
12026
  for (const manifest of findPluginManifests(root)) {
11658
12027
  if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
11659
- const absManifest = join49(root, manifest);
12028
+ const absManifest = join51(root, manifest);
11660
12029
  const subscriptions = readSubscriptions(absManifest);
11661
12030
  if (subscriptions === null) continue;
11662
12031
  const pluginDir2 = dirname10(absManifest);
11663
- if (existsSync40(join49(pluginDir2, "terraform"))) continue;
11664
- const relPluginDir = relative8(root, pluginDir2).split(sep3).join("/");
12032
+ if (existsSync42(join51(pluginDir2, "terraform"))) continue;
12033
+ const relPluginDir = relative9(root, pluginDir2).split(sep3).join("/");
11665
12034
  violations.push({
11666
12035
  manifest,
11667
12036
  expectedTerraformDir: relPluginDir ? `${relPluginDir}/terraform` : "terraform",
@@ -11680,7 +12049,7 @@ function formatViolations(violations) {
11680
12049
 
11681
12050
  // src/scripts/check-plugin-terraform.ts
11682
12051
  async function runPluginTerraformCheck() {
11683
- const root = (await execa17("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12052
+ const root = (await execa18("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11684
12053
  const violations = checkPluginTerraform(root);
11685
12054
  if (violations.length > 0) {
11686
12055
  console.error("\u2717 plugin Terraform guard: event subscriptions with no infrastructure\n");
@@ -11691,13 +12060,13 @@ async function runPluginTerraformCheck() {
11691
12060
  }
11692
12061
 
11693
12062
  // 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";
12063
+ import { existsSync as existsSync44 } from "fs";
12064
+ import { join as join53 } from "path";
12065
+ import { execa as execa19 } from "execa";
11697
12066
 
11698
12067
  // 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";
12068
+ import { existsSync as existsSync43, readFileSync as readFileSync38, readdirSync as readdirSync23, statSync as statSync14 } from "fs";
12069
+ import { join as join52 } from "path";
11701
12070
 
11702
12071
  // src/lib/openrouter-model-snapshot.ts
11703
12072
  var OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT = "2026-08-10T06:39:01Z";
@@ -12108,13 +12477,13 @@ var OPENROUTER_MODEL_IDS = [
12108
12477
  function listDirs(root) {
12109
12478
  let entries;
12110
12479
  try {
12111
- entries = readdirSync22(root);
12480
+ entries = readdirSync23(root);
12112
12481
  } catch {
12113
12482
  return [];
12114
12483
  }
12115
12484
  return entries.filter((e) => {
12116
12485
  try {
12117
- return statSync13(join50(root, e)).isDirectory();
12486
+ return statSync14(join52(root, e)).isDirectory();
12118
12487
  } catch {
12119
12488
  return false;
12120
12489
  }
@@ -12125,15 +12494,15 @@ function walkFiles2(root, accept, skipDir) {
12125
12494
  const walk2 = (dir) => {
12126
12495
  let entries;
12127
12496
  try {
12128
- entries = readdirSync22(dir);
12497
+ entries = readdirSync23(dir);
12129
12498
  } catch {
12130
12499
  return;
12131
12500
  }
12132
12501
  for (const entry of entries) {
12133
- const p = join50(dir, entry);
12502
+ const p = join52(dir, entry);
12134
12503
  let st;
12135
12504
  try {
12136
- st = statSync13(p);
12505
+ st = statSync14(p);
12137
12506
  } catch {
12138
12507
  continue;
12139
12508
  }
@@ -12156,14 +12525,14 @@ function pluginPythonFiles(pluginDir2) {
12156
12525
  );
12157
12526
  }
12158
12527
  function pluginTerraformFiles(pluginDir2) {
12159
- const tfDir = join50(pluginDir2, "terraform");
12528
+ const tfDir = join52(pluginDir2, "terraform");
12160
12529
  let entries;
12161
12530
  try {
12162
- entries = readdirSync22(tfDir);
12531
+ entries = readdirSync23(tfDir);
12163
12532
  } catch {
12164
12533
  return [];
12165
12534
  }
12166
- return entries.filter((e) => e.endsWith(".tf")).map((e) => join50(tfDir, e)).sort();
12535
+ return entries.filter((e) => e.endsWith(".tf")).map((e) => join52(tfDir, e)).sort();
12167
12536
  }
12168
12537
  function extractManifestTools(manifestText) {
12169
12538
  let parsed;
@@ -12415,8 +12784,8 @@ function isSnapshotStale(fetchedAt, now) {
12415
12784
  function normalizeModelId(id) {
12416
12785
  return id.endsWith(":online") ? id.slice(0, -":online".length) : id;
12417
12786
  }
12418
- var CONFIG_PY_PATH = join50("services", "api", "src", "api", "config.py");
12419
- var ORCHESTRATION_SCHEMA_PATH = join50(
12787
+ var CONFIG_PY_PATH = join52("services", "api", "src", "api", "config.py");
12788
+ var ORCHESTRATION_SCHEMA_PATH = join52(
12420
12789
  "services",
12421
12790
  "api",
12422
12791
  "src",
@@ -12428,10 +12797,10 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
12428
12797
  const knownModelIds = options.knownModelIds ?? OPENROUTER_MODEL_IDS;
12429
12798
  const snapshotFetchedAt = options.snapshotFetchedAt ?? OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT;
12430
12799
  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);
12800
+ const configPath = join52(repoRoot, CONFIG_PY_PATH);
12801
+ const orchestrationPath = join52(repoRoot, ORCHESTRATION_SCHEMA_PATH);
12802
+ const configMissing = !existsSync43(configPath);
12803
+ const orchestrationSchemaMissing = !existsSync43(orchestrationPath);
12435
12804
  const knownSet = new Set(knownModelIds);
12436
12805
  const snapshotEmpty = knownModelIds.length === 0;
12437
12806
  const snapshotStale = isSnapshotStale(snapshotFetchedAt, now);
@@ -12449,13 +12818,13 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
12449
12818
  };
12450
12819
  let settingsBlind = false;
12451
12820
  if (!configMissing) {
12452
- const settingsFields = extractSettingsModelFields(readFileSync36(configPath, "utf8"));
12821
+ const settingsFields = extractSettingsModelFields(readFileSync38(configPath, "utf8"));
12453
12822
  if (settingsFields.length === 0) settingsBlind = true;
12454
12823
  for (const { field, value } of settingsFields) record(`${CONFIG_PY_PATH}#${field}`, value);
12455
12824
  }
12456
12825
  let curatedFieldsBlind = false;
12457
12826
  if (!orchestrationSchemaMissing) {
12458
- const curated = extractCuratedModelFields(readFileSync36(orchestrationPath, "utf8"));
12827
+ const curated = extractCuratedModelFields(readFileSync38(orchestrationPath, "utf8"));
12459
12828
  if (curated.rawFieldCount > 0 && curated.fields.every((f) => f.defaultValue === null && f.optionValues.length === 0)) {
12460
12829
  curatedFieldsBlind = true;
12461
12830
  }
@@ -12502,7 +12871,7 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
12502
12871
  function discoverPluginDirs(pluginsRoot) {
12503
12872
  return listDirs(pluginsRoot).filter((name) => {
12504
12873
  try {
12505
- return statSync13(join50(pluginsRoot, name, "biffo.plugin.json")).isFile();
12874
+ return statSync14(join52(pluginsRoot, name, "biffo.plugin.json")).isFile();
12506
12875
  } catch {
12507
12876
  return false;
12508
12877
  }
@@ -12515,8 +12884,8 @@ function auditPluginToolSupply(pluginsRoot) {
12515
12884
  let terraformBlind = false;
12516
12885
  let totalDeclaredTools = 0;
12517
12886
  for (const name of pluginNames) {
12518
- const pluginDir2 = join50(pluginsRoot, name);
12519
- const manifestText = readFileSync36(join50(pluginDir2, "biffo.plugin.json"), "utf8");
12887
+ const pluginDir2 = join52(pluginsRoot, name);
12888
+ const manifestText = readFileSync38(join52(pluginDir2, "biffo.plugin.json"), "utf8");
12520
12889
  const manifest = extractManifestTools(manifestText);
12521
12890
  if (manifest.parseError) {
12522
12891
  findings.push({
@@ -12534,13 +12903,13 @@ function auditPluginToolSupply(pluginsRoot) {
12534
12903
  totalDeclaredTools += manifest.tools.length;
12535
12904
  const pySources = pluginPythonFiles(pluginDir2).map((f) => ({
12536
12905
  file: f,
12537
- text: readFileSync36(f, "utf8")
12906
+ text: readFileSync38(f, "utf8")
12538
12907
  }));
12539
12908
  const resolver = buildSymbolResolver(pySources);
12540
12909
  const registry = extractToolRegistryEntries(pySources, resolver);
12541
12910
  if (registry.rawToolDefinitionCount > 0 && registry.entries.length === 0) registryBlind = true;
12542
12911
  const tfFiles = pluginTerraformFiles(pluginDir2);
12543
- const tfText = tfFiles.map((f) => readFileSync36(f, "utf8")).join("\n");
12912
+ const tfText = tfFiles.map((f) => readFileSync38(f, "utf8")).join("\n");
12544
12913
  const terraform = extractTerraformEnvKeys(tfText);
12545
12914
  if (terraform.rawMarkerCount > 0 && terraform.resolvedBlockCount === 0) terraformBlind = true;
12546
12915
  for (const toolName of manifest.tools) {
@@ -12614,7 +12983,7 @@ function auditPluginToolSupply(pluginsRoot) {
12614
12983
  requiredEnvVars: envResult.envVars,
12615
12984
  missingEnvVars: anyWired ? [] : envResult.envVars,
12616
12985
  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`
12986
+ 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 ${join52(pluginDir2, "terraform")}, so this deployment can never supply it`
12618
12987
  });
12619
12988
  }
12620
12989
  }
@@ -12645,10 +13014,10 @@ function auditPluginToolSupply(pluginsRoot) {
12645
13014
 
12646
13015
  // src/scripts/check-plugin-tool-supply.ts
12647
13016
  async function runPluginToolSupplyCheck() {
12648
- const root = (await execa18("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13017
+ const root = (await execa19("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12649
13018
  let allOk = true;
12650
- const pluginsRoot = join51(root, "services", "_plugins");
12651
- if (!existsSync42(pluginsRoot)) {
13019
+ const pluginsRoot = join53(root, "services", "_plugins");
13020
+ if (!existsSync44(pluginsRoot)) {
12652
13021
  console.log("\u2713 plugin tool-supply guard: no services/_plugins/ \u2014 nothing to audit");
12653
13022
  } else {
12654
13023
  const report = auditPluginToolSupply(pluginsRoot);
@@ -12678,8 +13047,8 @@ async function runPluginToolSupplyCheck() {
12678
13047
  console.log(`\u2713 plugin tool-supply guard: ${report.summary}`);
12679
13048
  }
12680
13049
  }
12681
- const servicesApiRoot = join51(root, "services", "api");
12682
- if (!existsSync42(servicesApiRoot)) {
13050
+ const servicesApiRoot = join53(root, "services", "api");
13051
+ if (!existsSync44(servicesApiRoot)) {
12683
13052
  console.log("\u2713 plugin model-id guard: no services/api/ \u2014 nothing to audit");
12684
13053
  } else {
12685
13054
  const modelReport = auditDeclaredModelIds(root);
@@ -12725,7 +13094,7 @@ async function runPluginToolSupplyCheck() {
12725
13094
  }
12726
13095
 
12727
13096
  // src/scripts/check-release-subject.ts
12728
- import { execa as execa19 } from "execa";
13097
+ import { execa as execa20 } from "execa";
12729
13098
 
12730
13099
  // src/lib/release-version.ts
12731
13100
  var MINOR_TYPES = /* @__PURE__ */ new Set(["feat"]);
@@ -12762,7 +13131,7 @@ async function fetchPrTitleViaGh({
12762
13131
  PR_NUMBER,
12763
13132
  GH_REPO
12764
13133
  }) {
12765
- const { stdout } = await execa19(
13134
+ const { stdout } = await execa20(
12766
13135
  "gh",
12767
13136
  ["pr", "view", PR_NUMBER, "--repo", GH_REPO, "--json", "title", "--jq", ".title"],
12768
13137
  { env: { ...process.env, GH_TOKEN } }
@@ -12798,7 +13167,7 @@ async function resolveReleaseSubject({
12798
13167
  );
12799
13168
  }
12800
13169
  }
12801
- return (await execa19("git", ["log", "-1", "--format=%s"], { cwd })).stdout.trim();
13170
+ return (await execa20("git", ["log", "-1", "--format=%s"], { cwd })).stdout.trim();
12802
13171
  }
12803
13172
  async function runReleaseSubjectCheck(argv) {
12804
13173
  const base = process.env["GITHUB_BASE_REF"] ?? argv[0];
@@ -12806,9 +13175,9 @@ async function runReleaseSubjectCheck(argv) {
12806
13175
  console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
12807
13176
  process.exit(2);
12808
13177
  }
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`], {
13178
+ const root = (await execa20("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13179
+ await execa20("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
13180
+ const { stdout } = await execa20("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
12812
13181
  cwd: root
12813
13182
  });
12814
13183
  const changedFiles = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
@@ -12856,13 +13225,13 @@ async function runReleaseSubjectCheck(argv) {
12856
13225
  }
12857
13226
 
12858
13227
  // 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";
13228
+ import { existsSync as existsSync45, readdirSync as readdirSync25 } from "fs";
13229
+ import { join as join55 } from "path";
13230
+ import { execa as execa21 } from "execa";
12862
13231
 
12863
13232
  // 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";
13233
+ import { readFileSync as readFileSync39, readdirSync as readdirSync24, statSync as statSync15 } from "fs";
13234
+ import { join as join54 } from "path";
12866
13235
  var isWorkflow = (rel) => rel.startsWith(".github/workflows/") && (rel.endsWith(".yml") || rel.endsWith(".yaml"));
12867
13236
  var isRootLayout = (rel) => rel.endsWith("src/app/layout.tsx");
12868
13237
  var uncommented = (contents) => contents.split("\n").filter((line) => !/^\s*(\/\/|\/\*|\*)/.test(line)).join("\n");
@@ -12920,16 +13289,16 @@ function walk(dir, base = dir) {
12920
13289
  const out = [];
12921
13290
  let entries;
12922
13291
  try {
12923
- entries = readdirSync23(dir);
13292
+ entries = readdirSync24(dir);
12924
13293
  } catch {
12925
13294
  return out;
12926
13295
  }
12927
13296
  for (const entry of entries) {
12928
13297
  if (entry === ".venv" || entry === "node_modules" || entry === ".git") continue;
12929
- const abs = join52(dir, entry);
13298
+ const abs = join54(dir, entry);
12930
13299
  let isDir;
12931
13300
  try {
12932
- isDir = statSync14(abs).isDirectory();
13301
+ isDir = statSync15(abs).isDirectory();
12933
13302
  } catch {
12934
13303
  continue;
12935
13304
  }
@@ -12948,7 +13317,7 @@ function auditSkeleton(skeletonRoot, name, rules = SKELETON_RULES) {
12948
13317
  if (!rule.appliesTo(rel)) continue;
12949
13318
  let contents;
12950
13319
  try {
12951
- contents = readFileSync37(join52(skeletonRoot, rel), "utf8");
13320
+ contents = readFileSync39(join54(skeletonRoot, rel), "utf8");
12952
13321
  } catch {
12953
13322
  continue;
12954
13323
  }
@@ -12977,23 +13346,23 @@ function formatViolations2(violations) {
12977
13346
 
12978
13347
  // src/scripts/check-skeleton-drift.ts
12979
13348
  function discoverSkeletons(root) {
12980
- const skeletonsDir = join53(root, "_skeletons");
13349
+ const skeletonsDir = join55(root, "_skeletons");
12981
13350
  let entries;
12982
13351
  try {
12983
- entries = readdirSync24(skeletonsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
13352
+ entries = readdirSync25(skeletonsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
12984
13353
  } catch {
12985
13354
  return [];
12986
13355
  }
12987
- return entries.filter((name) => existsSync43(join53(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
13356
+ return entries.filter((name) => existsSync45(join55(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
12988
13357
  }
12989
13358
  async function runSkeletonDriftCheck() {
12990
- const root = (await execa20("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13359
+ const root = (await execa21("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12991
13360
  const skeletons = discoverSkeletons(root);
12992
13361
  let filesConsidered = 0;
12993
13362
  for (const name of skeletons) {
12994
- const skeletonRoot = join53(root, "_skeletons", name);
13363
+ const skeletonRoot = join55(root, "_skeletons", name);
12995
13364
  filesConsidered += findWorkflowFiles(skeletonRoot).length;
12996
- if (existsSync43(join53(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
13365
+ if (existsSync45(join55(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
12997
13366
  filesConsidered += 1;
12998
13367
  }
12999
13368
  }
@@ -13007,7 +13376,7 @@ async function runSkeletonDriftCheck() {
13007
13376
  process.exit(1);
13008
13377
  }
13009
13378
  const violations = skeletons.flatMap(
13010
- (name) => auditSkeleton(join53(root, "_skeletons", name), name)
13379
+ (name) => auditSkeleton(join55(root, "_skeletons", name), name)
13011
13380
  );
13012
13381
  if (violations.length > 0) {
13013
13382
  console.error("\u2717 Skeleton-drift guard: drift found between this repo and its scaffolding\n");
@@ -13019,9 +13388,9 @@ async function runSkeletonDriftCheck() {
13019
13388
  }
13020
13389
 
13021
13390
  // src/scripts/check-terraform-input.ts
13022
- import { execa as execa21 } from "execa";
13391
+ import { execa as execa22 } from "execa";
13023
13392
  async function runTerraformInputCheck() {
13024
- const root = (await execa21("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13393
+ const root = (await execa22("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13025
13394
  const files = findWorkflowFiles(root);
13026
13395
  console.log(`audited ${files.length} workflow file(s) under ${root}`);
13027
13396
  if (files.length === 0) {
@@ -13043,8 +13412,8 @@ async function runTerraformInputCheck() {
13043
13412
  }
13044
13413
 
13045
13414
  // 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)"
13415
+ var checkCommand = new Command24("check").description(
13416
+ "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
13417
  );
13049
13418
  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
13419
  await runOwnershipCheck(rawArgsAfter("ownership"));
@@ -13129,16 +13498,26 @@ checkCommand.command("branch-protection").description(
13129
13498
  ).action(async (opts) => {
13130
13499
  await runBranchProtectionCheck(opts.repo, { fix: opts.fix });
13131
13500
  });
13501
+ checkCommand.command("plugin-staleness").description(
13502
+ "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."
13503
+ ).action(async () => {
13504
+ const cwd = process.env["BIFFO_ORIGINAL_CWD"] || process.cwd();
13505
+ const results = await checkPluginStaleness(cwd, {
13506
+ registry: new RegistryAdapter(),
13507
+ git: new GitAdapter()
13508
+ });
13509
+ console.log(formatStalenessReport(results));
13510
+ });
13132
13511
  function rawArgsAfter(subcommand) {
13133
13512
  const at = process.argv.indexOf(subcommand);
13134
13513
  return at === -1 ? [] : process.argv.slice(at + 1);
13135
13514
  }
13136
13515
 
13137
13516
  // src/commands/doctor.ts
13138
- import { existsSync as existsSync44, readFileSync as readFileSync38 } from "fs";
13139
- import { join as join54, resolve as resolve19 } from "path";
13517
+ import { existsSync as existsSync46, readFileSync as readFileSync40 } from "fs";
13518
+ import { join as join56, resolve as resolve20 } from "path";
13140
13519
  import chalk21 from "chalk";
13141
- import { Command as Command24 } from "commander";
13520
+ import { Command as Command25 } from "commander";
13142
13521
 
13143
13522
  // src/lib/doctor.ts
13144
13523
  function checkCheckoutCurrency(facts) {
@@ -13261,10 +13640,10 @@ function runDoctorChecks(facts) {
13261
13640
 
13262
13641
  // src/commands/doctor.ts
13263
13642
  var INTEGRATION_BRANCH = "dev";
13264
- var doctorCommand = new Command24("doctor").description(
13643
+ var doctorCommand = new Command25("doctor").description(
13265
13644
  "Report repo-state conditions that make everything read from this checkout unreliable"
13266
13645
  ).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();
13646
+ const cwd = options.cwd ? resolve20(options.cwd) : process.cwd();
13268
13647
  try {
13269
13648
  const findings = await runDoctor({ cwd, fetch: options.fetch !== false });
13270
13649
  printFindings(findings);
@@ -13315,10 +13694,10 @@ async function runDoctor(options, deps = { git: new GitAdapter() }) {
13315
13694
  return runDoctorChecks(facts);
13316
13695
  }
13317
13696
  function readLocalCoreVersion(cwd) {
13318
- const path = join54(cwd, INSTANCE_CORE_FILE);
13319
- if (!existsSync44(path)) return null;
13697
+ const path = join56(cwd, INSTANCE_CORE_FILE);
13698
+ if (!existsSync46(path)) return null;
13320
13699
  try {
13321
- return extractVersionField(readFileSync38(path, "utf8"));
13700
+ return extractVersionField(readFileSync40(path, "utf8"));
13322
13701
  } catch {
13323
13702
  return null;
13324
13703
  }
@@ -13338,10 +13717,10 @@ function extractVersionField(contents) {
13338
13717
  return match?.[1] ?? null;
13339
13718
  }
13340
13719
  function readFossil(cwd) {
13341
- const path = join54(cwd, CORE_VERSION_FILE);
13342
- if (!existsSync44(path)) return null;
13720
+ const path = join56(cwd, CORE_VERSION_FILE);
13721
+ if (!existsSync46(path)) return null;
13343
13722
  try {
13344
- const value = readFileSync38(path, "utf8").trim();
13723
+ const value = readFileSync40(path, "utf8").trim();
13345
13724
  return value === "" ? null : value;
13346
13725
  } catch {
13347
13726
  return null;
@@ -13374,9 +13753,9 @@ function printFindings(findings) {
13374
13753
  import { execSync as execSync7 } from "child_process";
13375
13754
  import { GetCallerIdentityCommand as GetCallerIdentityCommand3, STSClient as STSClient3 } from "@aws-sdk/client-sts";
13376
13755
  import chalk22 from "chalk";
13377
- import { Command as Command25 } from "commander";
13756
+ import { Command as Command26 } from "commander";
13378
13757
  import inquirer8 from "inquirer";
13379
- var teardownCommand = new Command25("teardown").description(
13758
+ var teardownCommand = new Command26("teardown").description(
13380
13759
  "Destroy all infrastructure then remove the repo, IAM role, and state bucket \u2014 single command"
13381
13760
  ).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
13761
  "--confirm <name>",
@@ -13787,16 +14166,16 @@ function resolveGithubToken4() {
13787
14166
  import { spawnSync } from "child_process";
13788
14167
  import { dirname as dirname12 } from "path";
13789
14168
  import { fileURLToPath as fileURLToPath6 } from "url";
13790
- import { Command as Command26 } from "commander";
14169
+ import { Command as Command27 } from "commander";
13791
14170
 
13792
14171
  // src/lib/packaged-scripts.ts
13793
- import { existsSync as existsSync45 } from "fs";
13794
- import { dirname as dirname11, join as join55 } from "path";
14172
+ import { existsSync as existsSync47 } from "fs";
14173
+ import { dirname as dirname11, join as join57 } from "path";
13795
14174
  function findPackagedScript(startDir, relativePath) {
13796
14175
  let dir = startDir;
13797
14176
  for (; ; ) {
13798
- const candidate = join55(dir, relativePath);
13799
- if (existsSync45(candidate)) return candidate;
14177
+ const candidate = join57(dir, relativePath);
14178
+ if (existsSync47(candidate)) return candidate;
13800
14179
  const parent = dirname11(dir);
13801
14180
  if (parent === dir) return null;
13802
14181
  dir = parent;
@@ -13816,7 +14195,7 @@ function runPackagedScript(script, args, cwd) {
13816
14195
  return result.status === null ? 2 : result.status;
13817
14196
  }
13818
14197
  function packagedScriptCommand(spec) {
13819
- const command = new Command26(spec.name).description(spec.description).allowExcessArguments(true).allowUnknownOption(true);
14198
+ const command = new Command27(spec.name).description(spec.description).allowExcessArguments(true).allowUnknownOption(true);
13820
14199
  if (spec.argument) command.argument(`<${spec.argument.name}>`, spec.argument.description);
13821
14200
  return command.action(() => {
13822
14201
  const here = dirname12(fileURLToPath6(import.meta.url));
@@ -13905,7 +14284,7 @@ var runnerDropForensicsCommand = packagedScriptCommand({
13905
14284
  });
13906
14285
 
13907
14286
  // src/index.ts
13908
- var program = new Command27();
14287
+ var program = new Command28();
13909
14288
  function cliVersion() {
13910
14289
  try {
13911
14290
  return getLatestCoreVersion();