@biffo/cli 0.279.2 → 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 +723 -336
  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";
@@ -8287,6 +8304,13 @@ import { Command as Command14 } from "commander";
8287
8304
 
8288
8305
  // src/adapters/registry/index.ts
8289
8306
  import { z as z8 } from "zod";
8307
+ var UiComponentEntrySchema = z8.object({
8308
+ type: z8.enum(["nav-link", "page", "dashboard-widget", "modal", "dialog"]),
8309
+ label: z8.string(),
8310
+ path: z8.string(),
8311
+ icon: z8.string().optional(),
8312
+ requires_auth: z8.boolean().optional()
8313
+ });
8290
8314
  var RegistryPluginEntrySchema = z8.object({
8291
8315
  name: z8.string().regex(/^[a-z][a-z0-9-]*$/),
8292
8316
  version: z8.string().regex(/^\d+\.\d+\.\d+$/),
@@ -8298,7 +8322,7 @@ var RegistryPluginEntrySchema = z8.object({
8298
8322
  required_core_version: z8.string().optional(),
8299
8323
  infra_modules: z8.array(z8.string()).optional(),
8300
8324
  api_routes: z8.array(z8.string()).optional(),
8301
- ui_components: z8.array(z8.string()).optional(),
8325
+ ui_components: z8.array(UiComponentEntrySchema).optional(),
8302
8326
  status: z8.enum(["active", "disabled"])
8303
8327
  });
8304
8328
  var PluginRegistrySchema = z8.object({
@@ -8414,14 +8438,15 @@ function printEntry(entry) {
8414
8438
  console.log(` API routes: ${entry.api_routes.join(", ")}`);
8415
8439
  }
8416
8440
  if (entry.ui_components?.length) {
8417
- console.log(` UI components: ${entry.ui_components.join(", ")}`);
8441
+ const summary = entry.ui_components.map((c) => `${c.label} (${c.type})`).join(", ");
8442
+ console.log(` UI components: ${summary}`);
8418
8443
  }
8419
8444
  console.log("");
8420
8445
  }
8421
8446
 
8422
8447
  // src/commands/plugin-install.ts
8423
- import { cpSync as cpSync4, existsSync as existsSync28, mkdirSync as mkdirSync10, readFileSync as readFileSync21, statSync as statSync6 } from "fs";
8424
- 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";
8425
8450
  import chalk15 from "chalk";
8426
8451
  import { Command as Command15 } from "commander";
8427
8452
 
@@ -8467,10 +8492,90 @@ var PluginMigrationsAdapter = class {
8467
8492
  }
8468
8493
  };
8469
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
+
8470
8575
  // src/lib/plugin-source-copy.ts
8471
8576
  import { copyFileSync as copyFileSync2, cpSync as cpSync3, mkdirSync as mkdirSync9 } from "fs";
8472
- import { basename, dirname as dirname9, join as join28 } from "path";
8473
- import { execa as execa5 } from "execa";
8577
+ import { basename, dirname as dirname9, join as join29 } from "path";
8578
+ import { execa as execa6 } from "execa";
8474
8579
  var LOCAL_COPY_EXCLUDES = /* @__PURE__ */ new Set([
8475
8580
  ".git",
8476
8581
  ".venv",
@@ -8483,12 +8588,12 @@ var LOCAL_COPY_EXCLUDES = /* @__PURE__ */ new Set([
8483
8588
  ".terraform"
8484
8589
  ]);
8485
8590
  async function copyPluginSource(sourceDir, targetDir) {
8486
- if (await isGitWorkingTree(sourceDir)) {
8591
+ if (await isGitWorkingTree2(sourceDir)) {
8487
8592
  const files = await listGitFiles(sourceDir);
8488
8593
  for (const relPath of files) {
8489
- const destPath = join28(targetDir, relPath);
8594
+ const destPath = join29(targetDir, relPath);
8490
8595
  mkdirSync9(dirname9(destPath), { recursive: true });
8491
- copyFileSync2(join28(sourceDir, relPath), destPath);
8596
+ copyFileSync2(join29(sourceDir, relPath), destPath);
8492
8597
  }
8493
8598
  return { usedGitIgnoreRules: true };
8494
8599
  }
@@ -8502,16 +8607,16 @@ async function copyPluginSource(sourceDir, targetDir) {
8502
8607
  });
8503
8608
  return { usedGitIgnoreRules: false };
8504
8609
  }
8505
- async function isGitWorkingTree(dir) {
8610
+ async function isGitWorkingTree2(dir) {
8506
8611
  try {
8507
- await execa5("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir });
8612
+ await execa6("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir });
8508
8613
  return true;
8509
8614
  } catch {
8510
8615
  return false;
8511
8616
  }
8512
8617
  }
8513
8618
  async function listGitFiles(dir) {
8514
- const { stdout } = await execa5(
8619
+ const { stdout } = await execa6(
8515
8620
  "git",
8516
8621
  ["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
8517
8622
  { cwd: dir }
@@ -8520,8 +8625,8 @@ async function listGitFiles(dir) {
8520
8625
  }
8521
8626
 
8522
8627
  // src/lib/plugin-workspace-sources.ts
8523
- import { existsSync as existsSync27, readdirSync as readdirSync12, readFileSync as readFileSync20, writeFileSync as writeFileSync10 } from "fs";
8524
- 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";
8525
8630
  function readTomlStringArray(text, key) {
8526
8631
  const open = new RegExp(`^${key}\\s*=\\s*\\[`, "m").exec(text);
8527
8632
  if (!open) return [];
@@ -8565,9 +8670,9 @@ function readDependencyNames(text) {
8565
8670
  return readTomlStringArray(text, "dependencies").map((dep) => /^\s*([A-Za-z0-9._-]+)/.exec(dep)?.[1] ?? "").filter(Boolean);
8566
8671
  }
8567
8672
  function workspaceMemberNames(instanceRoot) {
8568
- const rootPyproject = join29(instanceRoot, "pyproject.toml");
8569
- if (!existsSync27(rootPyproject)) return /* @__PURE__ */ new Set();
8570
- 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");
8571
8676
  const members = readTomlStringArray(text, "members");
8572
8677
  const excluded = new Set(readTomlStringArray(text, "exclude"));
8573
8678
  const dirs = [];
@@ -8576,7 +8681,7 @@ function workspaceMemberNames(instanceRoot) {
8576
8681
  const base = member.slice(0, -2);
8577
8682
  let entries;
8578
8683
  try {
8579
- entries = readdirSync12(join29(instanceRoot, base), { withFileTypes: true });
8684
+ entries = readdirSync12(join30(instanceRoot, base), { withFileTypes: true });
8580
8685
  } catch {
8581
8686
  continue;
8582
8687
  }
@@ -8590,9 +8695,9 @@ function workspaceMemberNames(instanceRoot) {
8590
8695
  }
8591
8696
  const names = /* @__PURE__ */ new Set();
8592
8697
  for (const dir of dirs) {
8593
- const pp = join29(instanceRoot, dir, "pyproject.toml");
8594
- if (!existsSync27(pp)) continue;
8595
- 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"));
8596
8701
  if (name) names.add(name);
8597
8702
  }
8598
8703
  return names;
@@ -8603,8 +8708,8 @@ function existingWorkspaceSources(text) {
8603
8708
  );
8604
8709
  }
8605
8710
  function ensureWorkspaceSources(pluginPyprojectPath, memberNames) {
8606
- if (!existsSync27(pluginPyprojectPath) || memberNames.size === 0) return [];
8607
- const text = readFileSync20(pluginPyprojectPath, "utf8");
8711
+ if (!existsSync28(pluginPyprojectPath) || memberNames.size === 0) return [];
8712
+ const text = readFileSync21(pluginPyprojectPath, "utf8");
8608
8713
  const already = existingWorkspaceSources(text);
8609
8714
  const toAdd = readDependencyNames(text).filter((n) => memberNames.has(n) && !already.has(n));
8610
8715
  if (toAdd.length === 0) return [];
@@ -8624,12 +8729,12 @@ ${lines.join("\n")}${text.slice(insertAt)}`;
8624
8729
  ${lines.join("\n")}
8625
8730
  `;
8626
8731
  }
8627
- writeFileSync10(pluginPyprojectPath, updated);
8732
+ writeFileSync11(pluginPyprojectPath, updated);
8628
8733
  return toAdd;
8629
8734
  }
8630
8735
  function applyWorkspaceSources(targetDir, cwd, relTargetDir) {
8631
- const pluginPyproject = join29(targetDir, "pyproject.toml");
8632
- if (!existsSync27(pluginPyproject)) return;
8736
+ const pluginPyproject = join30(targetDir, "pyproject.toml");
8737
+ if (!existsSync28(pluginPyproject)) return;
8633
8738
  const sourced = ensureWorkspaceSources(pluginPyproject, workspaceMemberNames(cwd));
8634
8739
  if (sourced.length > 0) {
8635
8740
  log.info(
@@ -8669,14 +8774,14 @@ var pluginInstallCommand = new Command15("install").description(
8669
8774
  }
8670
8775
  );
8671
8776
  function resolveLocalPlugin(localPath) {
8672
- if (!existsSync28(localPath)) {
8777
+ if (!existsSync29(localPath)) {
8673
8778
  throw new Error(`--local path does not exist: ${localPath}`);
8674
8779
  }
8675
8780
  if (!statSync6(localPath).isDirectory()) {
8676
8781
  throw new Error(`--local path is not a directory: ${localPath}`);
8677
8782
  }
8678
- const manifestPath = join30(localPath, "biffo.plugin.json");
8679
- if (!existsSync28(manifestPath)) {
8783
+ const manifestPath = join31(localPath, "biffo.plugin.json");
8784
+ if (!existsSync29(manifestPath)) {
8680
8785
  throw new Error(
8681
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>\`.)`
8682
8787
  );
@@ -8702,8 +8807,8 @@ function parsePluginTarget(target) {
8702
8807
  async function cloneAndValidatePlugin(entry, git) {
8703
8808
  const tmpDir = await git.cloneToTemp(entry.repo, `biffo-plugin-${entry.name}`);
8704
8809
  try {
8705
- const manifestPath = join30(tmpDir, "biffo.plugin.json");
8706
- if (!existsSync28(manifestPath)) {
8810
+ const manifestPath = join31(tmpDir, "biffo.plugin.json");
8811
+ if (!existsSync29(manifestPath)) {
8707
8812
  throw new Error(
8708
8813
  `Plugin repo ${entry.repo} does not contain a biffo.plugin.json manifest at its root.`
8709
8814
  );
@@ -8731,8 +8836,8 @@ async function runPluginInstall(target, options, deps) {
8731
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\`).`
8732
8837
  );
8733
8838
  }
8734
- const servicesDir = join30(options.cwd, "services");
8735
- if (!existsSync28(servicesDir)) {
8839
+ const servicesDir = join31(options.cwd, "services");
8840
+ if (!existsSync29(servicesDir)) {
8736
8841
  throw new Error(
8737
8842
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
8738
8843
  );
@@ -8750,10 +8855,10 @@ async function runPluginInstall(target, options, deps) {
8750
8855
  }
8751
8856
  const pluginName = entry ? entry.name : source.name;
8752
8857
  const relTargetDir = pluginDir(pluginName, "third-party");
8753
- const targetDir = join30(options.cwd, relTargetDir);
8754
- const modulesDir = join30(options.cwd, "modules", "plugins", pluginName);
8858
+ const targetDir = join31(options.cwd, relTargetDir);
8859
+ const modulesDir = join31(options.cwd, "modules", "plugins", pluginName);
8755
8860
  const inTreeSource = options.local !== void 0 && resolve12(options.local) === resolve12(targetDir);
8756
- if (existsSync28(targetDir) && !inTreeSource) {
8861
+ if (existsSync29(targetDir) && !inTreeSource) {
8757
8862
  throw new Error(
8758
8863
  `Plugin '${pluginName}' is already installed at ${relTargetDir}/. Remove it first, or wait for a future 'biffo plugin upgrade' command.`
8759
8864
  );
@@ -8792,10 +8897,13 @@ async function runPluginInstall(target, options, deps) {
8792
8897
  await copyPluginSource(source.sourceDir, targetDir);
8793
8898
  log.success(`Installed plugin source at ${relTargetDir}/`);
8794
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));
8795
8903
  applyWorkspaceSources(targetDir, options.cwd, relTargetDir);
8796
8904
  const stagePaths = [relTargetDir];
8797
- const tfSourceDir = join30(targetDir, "terraform");
8798
- if (existsSync28(tfSourceDir)) {
8905
+ const tfSourceDir = join31(targetDir, "terraform");
8906
+ if (existsSync29(tfSourceDir)) {
8799
8907
  mkdirSync10(modulesDir, { recursive: true });
8800
8908
  cpSync4(tfSourceDir, modulesDir, { recursive: true });
8801
8909
  stagePaths.push(`modules/plugins/${pluginName}`);
@@ -8852,7 +8960,7 @@ async function runPluginInstall(target, options, deps) {
8852
8960
  }
8853
8961
  function parseManifestFile(path) {
8854
8962
  try {
8855
- return JSON.parse(readFileSync21(path, "utf8"));
8963
+ return JSON.parse(readFileSync22(path, "utf8"));
8856
8964
  } catch (err) {
8857
8965
  throw new Error(`Could not parse ${path} as JSON: ${err.message}`);
8858
8966
  }
@@ -8889,8 +8997,8 @@ function printDryRun4(entry, source, relTargetDir, inTreeSource) {
8889
8997
  }
8890
8998
 
8891
8999
  // src/commands/plugin-list.ts
8892
- import { existsSync as existsSync29, readFileSync as readFileSync22 } from "fs";
8893
- 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";
8894
9002
  import chalk16 from "chalk";
8895
9003
  import { Command as Command16 } from "commander";
8896
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) => {
@@ -8903,8 +9011,8 @@ var pluginListCommand = new Command16("list").description("List plugins installe
8903
9011
  }
8904
9012
  });
8905
9013
  async function runPluginList(options) {
8906
- const servicesDir = join31(options.cwd, "services");
8907
- if (!existsSync29(servicesDir)) {
9014
+ const servicesDir = join32(options.cwd, "services");
9015
+ if (!existsSync30(servicesDir)) {
8908
9016
  throw new Error(
8909
9017
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
8910
9018
  );
@@ -8912,7 +9020,7 @@ async function runPluginList(options) {
8912
9020
  const plugins = [];
8913
9021
  for (const location of findInstalledPlugins(options.cwd)) {
8914
9022
  try {
8915
- const manifest = validateManifest(JSON.parse(readFileSync22(location.manifestPath, "utf8")));
9023
+ const manifest = validateManifest(JSON.parse(readFileSync23(location.manifestPath, "utf8")));
8916
9024
  plugins.push({
8917
9025
  name: manifest.name,
8918
9026
  version: manifest.version,
@@ -8948,16 +9056,275 @@ async function runPluginList(options) {
8948
9056
  );
8949
9057
  }
8950
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
+
8951
9318
  // src/commands/plugin-sync-migrations.ts
8952
- import { existsSync as existsSync30 } from "fs";
8953
- 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";
8954
9321
  import chalk17 from "chalk";
8955
- import { Command as Command17 } from "commander";
8956
- var pluginSyncMigrationsCommand = new Command17("sync-migrations").description(
9322
+ import { Command as Command18 } from "commander";
9323
+ var pluginSyncMigrationsCommand = new Command18("sync-migrations").description(
8957
9324
  "Generate real, committed migration file(s) for installed-but-not-yet-migrated plugin(s): biffo plugin sync-migrations [name]"
8958
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(
8959
9326
  async (name, options) => {
8960
- const cwd = options.cwd ? resolve14(options.cwd) : process.cwd();
9327
+ const cwd = options.cwd ? resolve15(options.cwd) : process.cwd();
8961
9328
  try {
8962
9329
  await runPluginSyncMigrations(
8963
9330
  name,
@@ -8971,11 +9338,11 @@ var pluginSyncMigrationsCommand = new Command17("sync-migrations").description(
8971
9338
  }
8972
9339
  );
8973
9340
  async function runPluginSyncMigrations(name, options, deps) {
8974
- const servicesDir = join32(options.cwd, "services");
8975
- if (!existsSync30(servicesDir)) {
9341
+ const servicesDir = join34(options.cwd, "services");
9342
+ if (!existsSync32(servicesDir)) {
8976
9343
  throw new Error(`${servicesDir} does not exist \u2014 is ${options.cwd} a Biffo project checkout?`);
8977
9344
  }
8978
- if (name && !existsSync30(join32(servicesDir, name, "biffo.plugin.json"))) {
9345
+ if (name && !existsSync32(join34(servicesDir, name, "biffo.plugin.json"))) {
8979
9346
  throw new Error(`Plugin '${name}' is not installed at services/${name}/.`);
8980
9347
  }
8981
9348
  if (options.dryRun) {
@@ -8991,7 +9358,7 @@ async function runPluginSyncMigrations(name, options, deps) {
8991
9358
  );
8992
9359
  return;
8993
9360
  }
8994
- const relativePaths = generated.map((p) => relative4(options.cwd, p));
9361
+ const relativePaths = generated.map((p) => relative5(options.cwd, p));
8995
9362
  for (const p of relativePaths) {
8996
9363
  log.success(`Generated ${p}`);
8997
9364
  }
@@ -9011,18 +9378,18 @@ async function runPluginSyncMigrations(name, options, deps) {
9011
9378
  }
9012
9379
 
9013
9380
  // src/commands/plugin-uninstall.ts
9014
- import { existsSync as existsSync31, readFileSync as readFileSync23, rmSync as rmSync8 } from "fs";
9015
- 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";
9016
9383
  import chalk18 from "chalk";
9017
- import { Command as Command18 } from "commander";
9384
+ import { Command as Command19 } from "commander";
9018
9385
  import inquirer6 from "inquirer";
9019
9386
  var NAME_PATTERN2 = /^[a-z][a-z0-9-]*$/;
9020
- 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(
9021
9388
  "--keep-data",
9022
9389
  "No-op today (see notes) \u2014 the CLI never drops plugin data regardless of this flag"
9023
9390
  ).option("--cwd <path>", "Project root to uninstall from (defaults to the current directory)").action(
9024
9391
  async (name, options) => {
9025
- const cwd = options.cwd ? resolve15(options.cwd) : process.cwd();
9392
+ const cwd = options.cwd ? resolve16(options.cwd) : process.cwd();
9026
9393
  try {
9027
9394
  await runPluginUninstall(
9028
9395
  name,
@@ -9044,16 +9411,16 @@ async function runPluginUninstall(name, options, deps) {
9044
9411
  if (!NAME_PATTERN2.test(name)) {
9045
9412
  throw new Error(`Invalid plugin name '${name}'. Expected a lowercase kebab-case slug.`);
9046
9413
  }
9047
- const servicesDir = join33(options.cwd, "services");
9048
- if (!existsSync31(servicesDir)) {
9414
+ const servicesDir = join35(options.cwd, "services");
9415
+ if (!existsSync33(servicesDir)) {
9049
9416
  throw new Error(
9050
9417
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
9051
9418
  );
9052
9419
  }
9053
- const targetDir = join33(servicesDir, name);
9054
- if (!existsSync31(targetDir)) {
9055
- const firstParty = join33(servicesDir, FIRST_PARTY_PLUGINS_DIR, name);
9056
- 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)) {
9057
9424
  throw new Error(
9058
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.`
9059
9426
  );
@@ -9061,9 +9428,9 @@ async function runPluginUninstall(name, options, deps) {
9061
9428
  throw new Error(`Plugin '${name}' is not installed at services/${name}/.`);
9062
9429
  }
9063
9430
  const version = readInstalledVersion(targetDir);
9064
- const modulesDir = join33(options.cwd, "modules", "plugins", name);
9431
+ const modulesDir = join35(options.cwd, "modules", "plugins", name);
9065
9432
  const stagePaths = [`services/${name}`];
9066
- if (existsSync31(modulesDir)) {
9433
+ if (existsSync33(modulesDir)) {
9067
9434
  stagePaths.push(`modules/plugins/${name}`);
9068
9435
  }
9069
9436
  if (options.dryRun) {
@@ -9085,7 +9452,7 @@ async function runPluginUninstall(name, options, deps) {
9085
9452
  }
9086
9453
  rmSync8(targetDir, { recursive: true, force: true });
9087
9454
  log.success(`Removed services/${name}/`);
9088
- if (existsSync31(modulesDir)) {
9455
+ if (existsSync33(modulesDir)) {
9089
9456
  rmSync8(modulesDir, { recursive: true, force: true });
9090
9457
  log.success(`Removed modules/plugins/${name}/`);
9091
9458
  const wiring = syncPluginTerraform(options.cwd);
@@ -9122,10 +9489,10 @@ async function runPluginUninstall(name, options, deps) {
9122
9489
  }
9123
9490
  }
9124
9491
  function readInstalledVersion(targetDir) {
9125
- const manifestPath = join33(targetDir, "biffo.plugin.json");
9126
- if (!existsSync31(manifestPath)) return void 0;
9492
+ const manifestPath = join35(targetDir, "biffo.plugin.json");
9493
+ if (!existsSync33(manifestPath)) return void 0;
9127
9494
  try {
9128
- return validateManifest(JSON.parse(readFileSync23(manifestPath, "utf8"))).version;
9495
+ return validateManifest(JSON.parse(readFileSync25(manifestPath, "utf8"))).version;
9129
9496
  } catch {
9130
9497
  return void 0;
9131
9498
  }
@@ -9158,12 +9525,12 @@ function printDryRun5(name, version, stagePaths, keepData) {
9158
9525
  }
9159
9526
 
9160
9527
  // src/commands/plugin-upgrade.ts
9161
- import { cpSync as cpSync5, existsSync as existsSync32, mkdirSync as mkdirSync11, readFileSync as readFileSync24, rmSync as rmSync9 } from "fs";
9162
- 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";
9163
9530
  import chalk19 from "chalk";
9164
- import { Command as Command19 } from "commander";
9531
+ import { Command as Command20 } from "commander";
9165
9532
  import inquirer7 from "inquirer";
9166
- var pluginUpgradeCommand = new Command19("upgrade").description(
9533
+ var pluginUpgradeCommand = new Command20("upgrade").description(
9167
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>)"
9168
9535
  ).argument(
9169
9536
  "[target]",
@@ -9173,12 +9540,12 @@ var pluginUpgradeCommand = new Command19("upgrade").description(
9173
9540
  "Refresh the installed plugin from a local, unpublished checkout instead of the registry"
9174
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(
9175
9542
  async (target, options) => {
9176
- const cwd = options.cwd ? resolve16(options.cwd) : process.cwd();
9543
+ const cwd = options.cwd ? resolve17(options.cwd) : process.cwd();
9177
9544
  try {
9178
9545
  await runPluginUpgrade(
9179
9546
  target,
9180
9547
  {
9181
- ...options.local ? { local: resolve16(options.local) } : {},
9548
+ ...options.local ? { local: resolve17(options.local) } : {},
9182
9549
  dryRun: options.dryRun ?? false,
9183
9550
  force: options.force ?? false,
9184
9551
  cwd
@@ -9206,8 +9573,8 @@ async function runPluginUpgrade(target, options, deps) {
9206
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\`).`
9207
9574
  );
9208
9575
  }
9209
- const servicesDir = join34(options.cwd, "services");
9210
- if (!existsSync32(servicesDir)) {
9576
+ const servicesDir = join36(options.cwd, "services");
9577
+ if (!existsSync34(servicesDir)) {
9211
9578
  throw new Error(
9212
9579
  `${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
9213
9580
  );
@@ -9216,8 +9583,8 @@ async function runPluginUpgrade(target, options, deps) {
9216
9583
  return runLocalPluginRefresh(options.local, options, deps);
9217
9584
  }
9218
9585
  const { name, minor } = parsePluginTarget(target);
9219
- const targetDir = join34(servicesDir, name);
9220
- if (!existsSync32(targetDir)) {
9586
+ const targetDir = join36(servicesDir, name);
9587
+ if (!existsSync34(targetDir)) {
9221
9588
  throw new Error(
9222
9589
  `Plugin '${name}' is not installed at services/${name}/. Use 'biffo plugin install ${name}@${minor}' instead.`
9223
9590
  );
@@ -9231,7 +9598,7 @@ async function runPluginUpgrade(target, options, deps) {
9231
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.`
9232
9599
  );
9233
9600
  }
9234
- const modulesDir = join34(options.cwd, "modules", "plugins", entry.name);
9601
+ const modulesDir = join36(options.cwd, "modules", "plugins", entry.name);
9235
9602
  if (options.dryRun) {
9236
9603
  printDryRun6(entry, currentVersion);
9237
9604
  return;
@@ -9259,17 +9626,23 @@ async function runPluginUpgrade(target, options, deps) {
9259
9626
  log.success(
9260
9627
  `Manifest valid \u2014 ${manifest.tables.length} table(s), ${manifest.api_routes.length} route(s)`
9261
9628
  );
9629
+ const previousProvenance = readProvenance(targetDir);
9262
9630
  rmSync9(targetDir, { recursive: true, force: true });
9263
9631
  mkdirSync11(targetDir, { recursive: true });
9264
9632
  cpSync5(tmpDir, targetDir, { recursive: true });
9265
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));
9266
9639
  applyWorkspaceSources(targetDir, options.cwd, `services/${entry.name}`);
9267
9640
  const stagePaths = [`services/${entry.name}`];
9268
- if (existsSync32(modulesDir)) {
9641
+ if (existsSync34(modulesDir)) {
9269
9642
  rmSync9(modulesDir, { recursive: true, force: true });
9270
9643
  }
9271
- const tfSourceDir = join34(targetDir, "terraform");
9272
- if (existsSync32(tfSourceDir)) {
9644
+ const tfSourceDir = join36(targetDir, "terraform");
9645
+ if (existsSync34(tfSourceDir)) {
9273
9646
  mkdirSync11(modulesDir, { recursive: true });
9274
9647
  cpSync5(tfSourceDir, modulesDir, { recursive: true });
9275
9648
  stagePaths.push(`modules/plugins/${entry.name}`);
@@ -9281,10 +9654,10 @@ async function runPluginUpgrade(target, options, deps) {
9281
9654
  );
9282
9655
  const generatedPaths = await deps.migrations.generate(options.cwd, [entry.name]);
9283
9656
  for (const absPath of generatedPaths) {
9284
- stagePaths.push(relative5(options.cwd, absPath));
9657
+ stagePaths.push(relative6(options.cwd, absPath));
9285
9658
  }
9286
9659
  if (generatedPaths.length > 0) {
9287
- log.success(`Generated migration: ${relative5(options.cwd, generatedPaths[0])}`);
9660
+ log.success(`Generated migration: ${relative6(options.cwd, generatedPaths[0])}`);
9288
9661
  } else {
9289
9662
  log.info(
9290
9663
  `${entry.name}'s tables and columns already match the manifest \u2014 no migration needed.`
@@ -9309,16 +9682,16 @@ async function runPluginUpgrade(target, options, deps) {
9309
9682
  async function runLocalPluginRefresh(localPath, options, deps) {
9310
9683
  const source = resolveLocalPlugin(localPath);
9311
9684
  log.success(`Resolved ${source.name}@${source.version} from ${source.origin}`);
9312
- const servicesDir = join34(options.cwd, "services");
9313
- const targetDir = join34(servicesDir, source.name);
9314
- if (!existsSync32(targetDir)) {
9685
+ const servicesDir = join36(options.cwd, "services");
9686
+ const targetDir = join36(servicesDir, source.name);
9687
+ if (!existsSync34(targetDir)) {
9315
9688
  throw new Error(
9316
9689
  `Plugin '${source.name}' is not installed at services/${source.name}/. Use 'biffo plugin install --local ${localPath}' instead.`
9317
9690
  );
9318
9691
  }
9319
- const inTreeSource = resolve16(source.sourceDir) === resolve16(targetDir);
9692
+ const inTreeSource = resolve17(source.sourceDir) === resolve17(targetDir);
9320
9693
  const currentVersion = readInstalledVersion2(targetDir);
9321
- const modulesDir = join34(options.cwd, "modules", "plugins", source.name);
9694
+ const modulesDir = join36(options.cwd, "modules", "plugins", source.name);
9322
9695
  if (options.dryRun) {
9323
9696
  printLocalDryRun(source, currentVersion, inTreeSource);
9324
9697
  return;
@@ -9341,6 +9714,7 @@ async function runLocalPluginRefresh(localPath, options, deps) {
9341
9714
  log.success(
9342
9715
  `Manifest valid \u2014 ${manifest.tables.length} table(s), ${manifest.api_routes.length} route(s)`
9343
9716
  );
9717
+ const previousProvenance = readProvenance(targetDir);
9344
9718
  if (inTreeSource) {
9345
9719
  log.info(
9346
9720
  `services/${source.name}/ is already the local checkout \u2014 nothing to copy; re-syncing its Terraform module and checking for a migration.`
@@ -9351,13 +9725,15 @@ async function runLocalPluginRefresh(localPath, options, deps) {
9351
9725
  await copyPluginSource(source.sourceDir, targetDir);
9352
9726
  log.success(`Refreshed plugin source at services/${source.name}/ from ${source.origin}`);
9353
9727
  }
9728
+ const nextProvenance = inTreeSource ? inTreePluginProvenance(`services/${source.name}`) : await resolveLocalProvenance(source.sourceDir, source.origin);
9729
+ writePluginProvenance(targetDir, reconcileProvenance(previousProvenance, nextProvenance));
9354
9730
  applyWorkspaceSources(targetDir, options.cwd, `services/${source.name}`);
9355
9731
  const stagePaths = [`services/${source.name}`];
9356
- if (existsSync32(modulesDir)) {
9732
+ if (existsSync34(modulesDir)) {
9357
9733
  rmSync9(modulesDir, { recursive: true, force: true });
9358
9734
  }
9359
- const tfSourceDir = join34(targetDir, "terraform");
9360
- if (existsSync32(tfSourceDir)) {
9735
+ const tfSourceDir = join36(targetDir, "terraform");
9736
+ if (existsSync34(tfSourceDir)) {
9361
9737
  mkdirSync11(modulesDir, { recursive: true });
9362
9738
  cpSync5(tfSourceDir, modulesDir, { recursive: true });
9363
9739
  stagePaths.push(`modules/plugins/${source.name}`);
@@ -9369,10 +9745,10 @@ async function runLocalPluginRefresh(localPath, options, deps) {
9369
9745
  );
9370
9746
  const generatedPaths = await deps.migrations.generate(options.cwd, [source.name]);
9371
9747
  for (const absPath of generatedPaths) {
9372
- stagePaths.push(relative5(options.cwd, absPath));
9748
+ stagePaths.push(relative6(options.cwd, absPath));
9373
9749
  }
9374
9750
  if (generatedPaths.length > 0) {
9375
- log.success(`Generated migration: ${relative5(options.cwd, generatedPaths[0])}`);
9751
+ log.success(`Generated migration: ${relative6(options.cwd, generatedPaths[0])}`);
9376
9752
  } else {
9377
9753
  log.info(
9378
9754
  `${source.name}'s tables and columns already match the manifest \u2014 no migration needed.`
@@ -9400,10 +9776,10 @@ async function runLocalPluginRefresh(localPath, options, deps) {
9400
9776
  }
9401
9777
  }
9402
9778
  function readInstalledVersion2(targetDir) {
9403
- const manifestPath = join34(targetDir, "biffo.plugin.json");
9404
- if (!existsSync32(manifestPath)) return void 0;
9779
+ const manifestPath = join36(targetDir, "biffo.plugin.json");
9780
+ if (!existsSync34(manifestPath)) return void 0;
9405
9781
  try {
9406
- return validateManifest(JSON.parse(readFileSync24(manifestPath, "utf8"))).version;
9782
+ return validateManifest(JSON.parse(readFileSync26(manifestPath, "utf8"))).version;
9407
9783
  } catch {
9408
9784
  return void 0;
9409
9785
  }
@@ -9462,7 +9838,7 @@ function printLocalDryRun(source, currentVersion, inTreeSource) {
9462
9838
  }
9463
9839
 
9464
9840
  // src/commands/plugin.ts
9465
- var pluginCommand = new Command20("plugin").description("Manage Biffo plugins");
9841
+ var pluginCommand = new Command21("plugin").description("Manage Biffo plugins");
9466
9842
  pluginCommand.addCommand(pluginCreateCommand);
9467
9843
  pluginCommand.addCommand(pluginListCommand);
9468
9844
  pluginCommand.addCommand(pluginInstallCommand);
@@ -9470,15 +9846,16 @@ pluginCommand.addCommand(pluginUninstallCommand);
9470
9846
  pluginCommand.addCommand(pluginUpgradeCommand);
9471
9847
  pluginCommand.addCommand(pluginSyncMigrationsCommand);
9472
9848
  pluginCommand.addCommand(pluginInfoCommand);
9849
+ pluginCommand.addCommand(pluginStalenessCommand);
9473
9850
 
9474
9851
  // src/commands/sibling.ts
9475
- import { Command as Command22 } from "commander";
9852
+ import { Command as Command23 } from "commander";
9476
9853
 
9477
9854
  // src/commands/sibling-check-identity.ts
9478
- import { existsSync as existsSync33, readFileSync as readFileSync25 } from "fs";
9479
- import { resolve as resolve17 } from "path";
9855
+ import { existsSync as existsSync35, readFileSync as readFileSync27 } from "fs";
9856
+ import { resolve as resolve18 } from "path";
9480
9857
  import chalk20 from "chalk";
9481
- import { Command as Command21 } from "commander";
9858
+ import { Command as Command22 } from "commander";
9482
9859
 
9483
9860
  // src/lib/sibling-identity-check.ts
9484
9861
  function checkSiblingIdentity(envs) {
@@ -9530,7 +9907,7 @@ function checkSiblingIdentity(envs) {
9530
9907
  // src/commands/sibling-check-identity.ts
9531
9908
  var VALID_ENVIRONMENTS2 = ["dev", "staging", "prod"];
9532
9909
  var SIBLING_CORE_POOL_VAR = "CORE_COGNITO_USER_POOL_ID";
9533
- var siblingCheckIdentityCommand = new Command21("check-identity").description(
9910
+ var siblingCheckIdentityCommand = new Command22("check-identity").description(
9534
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."
9535
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) => {
9536
9913
  if (options.env && !VALID_ENVIRONMENTS2.includes(options.env)) {
@@ -9669,7 +10046,7 @@ async function fetchPublishedIdentity(portalUrl) {
9669
10046
  }
9670
10047
  async function resolveConfig4(options) {
9671
10048
  if (options.config) {
9672
- const raw = JSON.parse(readFileSync25(resolve17(options.config), "utf8"));
10049
+ const raw = JSON.parse(readFileSync27(resolve18(options.config), "utf8"));
9673
10050
  const result = BiffoConfigSchema.safeParse(raw);
9674
10051
  if (!result.success) {
9675
10052
  log.error(`Invalid config at ${options.config}:`);
@@ -9688,9 +10065,9 @@ async function resolveConfig4(options) {
9688
10065
  }
9689
10066
  return cfg;
9690
10067
  }
9691
- const localConfigPath = resolve17(process.cwd(), "biffo.config.json");
9692
- if (existsSync33(localConfigPath)) {
9693
- 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"));
9694
10071
  const result = BiffoConfigSchema.safeParse(raw);
9695
10072
  if (result.success) return result.data;
9696
10073
  if (isTemplatePlaceholderConfig(raw)) {
@@ -9726,31 +10103,31 @@ async function resolveConfig4(options) {
9726
10103
  }
9727
10104
 
9728
10105
  // src/commands/sibling.ts
9729
- var siblingCommand = new Command22("sibling").description(
10106
+ var siblingCommand = new Command23("sibling").description(
9730
10107
  "Create and manage sibling apps that share a Biffo core project (ADR-0007)"
9731
10108
  );
9732
10109
  siblingCommand.addCommand(siblingCreateCommand);
9733
10110
  siblingCommand.addCommand(siblingCheckIdentityCommand);
9734
10111
 
9735
10112
  // src/commands/check.ts
9736
- import { Command as Command23 } from "commander";
10113
+ import { Command as Command24 } from "commander";
9737
10114
 
9738
10115
  // src/scripts/check-adr-numbering.ts
9739
- import { existsSync as existsSync35 } from "fs";
9740
- import { join as join36 } from "path";
9741
- 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";
9742
10119
 
9743
10120
  // src/lib/adr-numbering-guard.ts
9744
- import { existsSync as existsSync34, readdirSync as readdirSync13, readFileSync as readFileSync26 } from "fs";
9745
- 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";
9746
10123
  var ADR_FILENAME = /^(\d{4})-.+\.md$/;
9747
10124
  var ALLOWLIST_FILENAME = ".numbering-allowlist";
9748
10125
  var TEMPLATE_ADR_RESERVED_UPTO = "0099";
9749
10126
  function readAdrNumberingAllowlist(adrDir) {
9750
- const path = join35(adrDir, ALLOWLIST_FILENAME);
9751
- if (!existsSync34(path)) return /* @__PURE__ */ new Set();
10127
+ const path = join37(adrDir, ALLOWLIST_FILENAME);
10128
+ if (!existsSync36(path)) return /* @__PURE__ */ new Set();
9752
10129
  const numbers = /* @__PURE__ */ new Set();
9753
- for (const rawLine of readFileSync26(path, "utf8").split("\n")) {
10130
+ for (const rawLine of readFileSync28(path, "utf8").split("\n")) {
9754
10131
  const line = rawLine.split("#")[0].trim();
9755
10132
  if (line) numbers.add(line);
9756
10133
  }
@@ -9758,8 +10135,8 @@ function readAdrNumberingAllowlist(adrDir) {
9758
10135
  }
9759
10136
  function adrNumbersIn(adrDir) {
9760
10137
  const claims = /* @__PURE__ */ new Map();
9761
- if (!existsSync34(adrDir)) return claims;
9762
- for (const entry of readdirSync13(adrDir).sort()) {
10138
+ if (!existsSync36(adrDir)) return claims;
10139
+ for (const entry of readdirSync14(adrDir).sort()) {
9763
10140
  const match = ADR_FILENAME.exec(entry);
9764
10141
  if (!match) continue;
9765
10142
  const number = match[1];
@@ -9812,9 +10189,9 @@ function formatAdrReservedRangeViolations(violations, reservedUpTo = TEMPLATE_AD
9812
10189
 
9813
10190
  // src/scripts/check-adr-numbering.ts
9814
10191
  async function runAdrNumberingCheck() {
9815
- const root = (await execa6("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9816
- const adrDir = join36(root, "docs", "ADR");
9817
- 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)) {
9818
10195
  console.log("\u2713 ADR numbering guard: no docs/ADR/ directory \u2014 nothing to compare");
9819
10196
  return;
9820
10197
  }
@@ -9853,7 +10230,7 @@ Already accepted? List it in docs/ADR/${ALLOWLIST_FILENAME} instead of leaving t
9853
10230
 
9854
10231
  // src/scripts/check-branch-protection.ts
9855
10232
  import { Octokit as Octokit2 } from "@octokit/rest";
9856
- import { execa as execa7 } from "execa";
10233
+ import { execa as execa8 } from "execa";
9857
10234
 
9858
10235
  // src/lib/branch-protection-apply.ts
9859
10236
  var CONTEXT_CONSISTENCY_THRESHOLD = 2 / 3;
@@ -9980,7 +10357,7 @@ async function resolveRepo(explicit) {
9980
10357
  }
9981
10358
  return { owner, repo };
9982
10359
  }
9983
- const { stdout } = await execa7("git", ["remote", "get-url", "origin"]);
10360
+ const { stdout } = await execa8("git", ["remote", "get-url", "origin"]);
9984
10361
  const m = /github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/.exec(stdout.trim());
9985
10362
  if (!m?.[1] || !m[2]) {
9986
10363
  console.error(
@@ -10112,13 +10489,13 @@ async function runBranchProtectionCheck(explicitRepo, options = {}) {
10112
10489
  }
10113
10490
 
10114
10491
  // src/scripts/check-codeql-suppression.ts
10115
- import { existsSync as existsSync36 } from "fs";
10116
- import { join as join38, relative as relative6 } from "path";
10117
- 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";
10118
10495
 
10119
10496
  // src/lib/codeql-suppression-guard.ts
10120
- import { readdirSync as readdirSync14, readFileSync as readFileSync27, statSync as statSync7 } from "fs";
10121
- 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";
10122
10499
  var SKIP_DIRS = /* @__PURE__ */ new Set([
10123
10500
  ".git",
10124
10501
  ".worktrees",
@@ -10144,15 +10521,15 @@ function walkSourceFiles(root) {
10144
10521
  const walk2 = (dir) => {
10145
10522
  let entries;
10146
10523
  try {
10147
- entries = readdirSync14(dir);
10524
+ entries = readdirSync15(dir);
10148
10525
  } catch {
10149
10526
  return;
10150
10527
  }
10151
10528
  for (const entry of entries) {
10152
- const p = join37(dir, entry);
10529
+ const p = join39(dir, entry);
10153
10530
  let st;
10154
10531
  try {
10155
- st = statSync7(p);
10532
+ st = statSync8(p);
10156
10533
  } catch {
10157
10534
  continue;
10158
10535
  }
@@ -10175,7 +10552,7 @@ function countSourceFiles(root) {
10175
10552
  function sweepCodeqlSuppressionComments(root) {
10176
10553
  const hits = [];
10177
10554
  for (const path of walkSourceFiles(root)) {
10178
- const text = readFileSync27(path, "utf8");
10555
+ const text = readFileSync29(path, "utf8");
10179
10556
  for (const line of findCodeqlSuppressionComments(text)) {
10180
10557
  hits.push({ path, line, text: text.split("\n")[line - 1] ?? "" });
10181
10558
  }
@@ -10185,9 +10562,9 @@ function sweepCodeqlSuppressionComments(root) {
10185
10562
 
10186
10563
  // src/scripts/check-codeql-suppression.ts
10187
10564
  async function runCodeqlSuppressionCheck() {
10188
- const root = (await execa8("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10189
- const scanRoot = join38(root, "cli", "src");
10190
- 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)) {
10191
10568
  console.log(
10192
10569
  "\u2014 codeql-suppression guard: skipped \u2014 no cli/src in this repo, so there is no CLI source to scan."
10193
10570
  );
@@ -10199,7 +10576,7 @@ async function runCodeqlSuppressionCheck() {
10199
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"
10200
10577
  );
10201
10578
  for (const hit of hits) {
10202
- console.error(` ${relative6(root, hit.path)}:${hit.line} ${hit.text.trim()}`);
10579
+ console.error(` ${relative7(root, hit.path)}:${hit.line} ${hit.text.trim()}`);
10203
10580
  }
10204
10581
  process.exit(1);
10205
10582
  }
@@ -10211,11 +10588,11 @@ async function runCodeqlSuppressionCheck() {
10211
10588
  }
10212
10589
 
10213
10590
  // src/scripts/check-cognito-invite-template.ts
10214
- import { execa as execa9 } from "execa";
10591
+ import { execa as execa10 } from "execa";
10215
10592
 
10216
10593
  // src/lib/cognito-invite-template-guard.ts
10217
- import { readdirSync as readdirSync15, readFileSync as readFileSync28, statSync as statSync8 } from "fs";
10218
- 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";
10219
10596
  var REQUIRED_INVITE_MEMBERS = ["email_subject", "email_message", "sms_message"];
10220
10597
  var REQUIRED_INVITE_PLACEHOLDERS = ["{username}", "{####}"];
10221
10598
  var PLACEHOLDER_MEMBERS = ["email_message", "sms_message"];
@@ -10289,36 +10666,36 @@ function memberBody(blockBody, member) {
10289
10666
  }
10290
10667
  function findModuleTerraformFiles(repoRoot) {
10291
10668
  const found = [];
10292
- const walk2 = (dir, relative9) => {
10669
+ const walk2 = (dir, relative10) => {
10293
10670
  let entries;
10294
10671
  try {
10295
- entries = readdirSync15(dir);
10672
+ entries = readdirSync16(dir);
10296
10673
  } catch {
10297
10674
  return;
10298
10675
  }
10299
10676
  for (const entry of entries) {
10300
10677
  if (entry === "node_modules" || entry === ".git" || entry === ".worktrees") continue;
10301
- const full = join39(dir, entry);
10302
- const rel = `${relative9}/${entry}`;
10303
- if (statSync8(full).isDirectory()) {
10678
+ const full = join41(dir, entry);
10679
+ const rel = `${relative10}/${entry}`;
10680
+ if (statSync9(full).isDirectory()) {
10304
10681
  walk2(full, rel);
10305
10682
  } else if (entry.endsWith(".tf")) {
10306
10683
  found.push(rel);
10307
10684
  }
10308
10685
  }
10309
10686
  };
10310
- walk2(join39(repoRoot, "modules"), "modules");
10687
+ walk2(join41(repoRoot, "modules"), "modules");
10311
10688
  return found.sort();
10312
10689
  }
10313
10690
  function checkCognitoInviteTemplates(repoRoot) {
10314
10691
  return findModuleTerraformFiles(repoRoot).flatMap(
10315
- (file) => checkInviteTemplateSource(file, readFileSync28(join39(repoRoot, file), "utf8"))
10692
+ (file) => checkInviteTemplateSource(file, readFileSync30(join41(repoRoot, file), "utf8"))
10316
10693
  );
10317
10694
  }
10318
10695
 
10319
10696
  // src/scripts/check-cognito-invite-template.ts
10320
10697
  async function runCognitoInviteTemplateCheck() {
10321
- const root = (await execa9("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10698
+ const root = (await execa10("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10322
10699
  const files = findModuleTerraformFiles(root);
10323
10700
  console.log(`audited ${files.length} .tf file(s) under modules/ under ${root}`);
10324
10701
  if (files.length === 0) {
@@ -10340,12 +10717,12 @@ async function runCognitoInviteTemplateCheck() {
10340
10717
  }
10341
10718
 
10342
10719
  // src/scripts/check-core-direct-paths.ts
10343
- import { join as join41 } from "path";
10344
- import { execa as execa10 } from "execa";
10720
+ import { join as join43 } from "path";
10721
+ import { execa as execa11 } from "execa";
10345
10722
 
10346
10723
  // src/lib/core-direct-paths-audit.ts
10347
- import { existsSync as existsSync37, readFileSync as readFileSync29, readdirSync as readdirSync16, statSync as statSync9 } from "fs";
10348
- 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";
10349
10726
  var EXTERNAL_BASE_IDENTIFIERS = ["CORE_API_URL"];
10350
10727
  var API_ROUTE_PREFIX = "/api/v1";
10351
10728
  var TEST_FILE_SUFFIXES = [".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx"];
@@ -10504,15 +10881,15 @@ function walkFiles(root, accept, skipDir) {
10504
10881
  const walk2 = (dir) => {
10505
10882
  let entries;
10506
10883
  try {
10507
- entries = readdirSync16(dir);
10884
+ entries = readdirSync17(dir);
10508
10885
  } catch {
10509
10886
  return;
10510
10887
  }
10511
10888
  for (const entry of entries) {
10512
- const p = join40(dir, entry);
10889
+ const p = join42(dir, entry);
10513
10890
  let st;
10514
10891
  try {
10515
- st = statSync9(p);
10892
+ st = statSync10(p);
10516
10893
  } catch {
10517
10894
  continue;
10518
10895
  }
@@ -10539,7 +10916,7 @@ function auditFrontendExtraction(frontendSrcDir, externalBases = EXTERNAL_BASE_I
10539
10916
  const extracted = [];
10540
10917
  let rawTotal = 0;
10541
10918
  for (const file of files) {
10542
- const text = readFileSync29(file, "utf8");
10919
+ const text = readFileSync31(file, "utf8");
10543
10920
  rawTotal += countRawExternalOccurrences(text, externalBases);
10544
10921
  extracted.push(...extractCoreDirectPaths(text, file, externalBases));
10545
10922
  }
@@ -10588,7 +10965,7 @@ function auditCoreRouteExtraction(apiSrcDir) {
10588
10965
  const prefixSet = /* @__PURE__ */ new Set();
10589
10966
  let rawApiRouterCount = 0;
10590
10967
  for (const file of files) {
10591
- const text = readFileSync29(file, "utf8");
10968
+ const text = readFileSync31(file, "utf8");
10592
10969
  const extraction = extractCoreRoutePrefixes(text);
10593
10970
  rawApiRouterCount += extraction.rawApiRouterCount;
10594
10971
  for (const p of extraction.prefixes) prefixSet.add(normalizePrefix(p));
@@ -10604,10 +10981,10 @@ function pathMatchesAnyCorePrefix(normalized, corePrefixes, apiRoutePrefix = API
10604
10981
  }
10605
10982
  function resolveSiblingCoreSrc(params) {
10606
10983
  const { estateDir, sibling } = params;
10607
- const configPath = join40(estateDir, sibling, "biffo.sibling.json");
10984
+ const configPath = join42(estateDir, sibling, "biffo.sibling.json");
10608
10985
  let raw;
10609
10986
  try {
10610
- raw = readFileSync29(configPath, "utf8");
10987
+ raw = readFileSync31(configPath, "utf8");
10611
10988
  } catch (err) {
10612
10989
  throw new Error(
10613
10990
  `cannot resolve ${sibling}'s core: ${configPath} does not exist or is unreadable (${err.message}) -- refusing to guess which core serves this sibling.`
@@ -10627,8 +11004,8 @@ function resolveSiblingCoreSrc(params) {
10627
11004
  `cannot resolve ${sibling}'s core: ${configPath} has no non-empty "core_project" field.`
10628
11005
  );
10629
11006
  }
10630
- const coreApiSrcDir = join40(estateDir, coreProject, "services", "api", "src");
10631
- if (!existsSync37(coreApiSrcDir)) {
11007
+ const coreApiSrcDir = join42(estateDir, coreProject, "services", "api", "src");
11008
+ if (!existsSync39(coreApiSrcDir)) {
10632
11009
  throw new Error(
10633
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.`
10634
11011
  );
@@ -10669,9 +11046,9 @@ function auditSiblingCoreDirectPaths(params) {
10669
11046
 
10670
11047
  // src/scripts/check-core-direct-paths.ts
10671
11048
  async function runCoreDirectPathsCheck(opts = {}) {
10672
- const root = (await execa10("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11049
+ const root = (await execa11("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10673
11050
  const sibling = opts.sibling ?? "sibling-template (self-check)";
10674
- 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");
10675
11052
  let coreApiSrcDir;
10676
11053
  let coreProject = null;
10677
11054
  if (opts.coreSrc) {
@@ -10687,7 +11064,7 @@ async function runCoreDirectPathsCheck(opts = {}) {
10687
11064
  coreApiSrcDir = resolution.coreApiSrcDir;
10688
11065
  coreProject = resolution.coreProject;
10689
11066
  } else {
10690
- coreApiSrcDir = join41(root, "services", "api", "src");
11067
+ coreApiSrcDir = join43(root, "services", "api", "src");
10691
11068
  }
10692
11069
  const report = auditSiblingCoreDirectPaths({ sibling, frontendSrcDir, coreApiSrcDir });
10693
11070
  console.log(
@@ -10723,7 +11100,7 @@ async function runCoreDirectPathsCheck(opts = {}) {
10723
11100
  }
10724
11101
 
10725
11102
  // src/scripts/check-core-ownership.ts
10726
- import { execa as execa11 } from "execa";
11103
+ import { execa as execa12 } from "execa";
10727
11104
  var BOLD = "\x1B[1m";
10728
11105
  var DIM = "\x1B[2m";
10729
11106
  var RED = "\x1B[31m";
@@ -10734,7 +11111,7 @@ async function runOwnershipCheck(argv) {
10734
11111
  const stagedFlag = args.indexOf("--staged");
10735
11112
  const staged = stagedFlag !== -1;
10736
11113
  const messageFile = staged ? args[stagedFlag + 1] : void 0;
10737
- const root = (await execa11("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11114
+ const root = (await execa12("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
10738
11115
  const ownership = classifyRepoOwnership(root);
10739
11116
  if (ownership === "template") {
10740
11117
  console.log("\u2713 core ownership guard: skipped \u2014 this is the template, which owns these paths.");
@@ -10750,11 +11127,11 @@ async function runOwnershipCheck(argv) {
10750
11127
  let deletedFiles = [];
10751
11128
  let commitMessage = "";
10752
11129
  if (staged) {
10753
- const { stdout } = await execa11("git", ["diff", "--cached", "--name-status"], { cwd: root });
11130
+ const { stdout } = await execa12("git", ["diff", "--cached", "--name-status"], { cwd: root });
10754
11131
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
10755
11132
  if (messageFile) {
10756
- const { readFileSync: readFileSync39, existsSync: existsSync46 } = await import("fs");
10757
- if (existsSync46(messageFile)) commitMessage = readFileSync39(messageFile, "utf8");
11133
+ const { readFileSync: readFileSync41, existsSync: existsSync48 } = await import("fs");
11134
+ if (existsSync48(messageFile)) commitMessage = readFileSync41(messageFile, "utf8");
10758
11135
  }
10759
11136
  } else {
10760
11137
  const base = process.env["GITHUB_BASE_REF"] ?? args[0];
@@ -10762,18 +11139,18 @@ async function runOwnershipCheck(argv) {
10762
11139
  console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
10763
11140
  process.exit(2);
10764
11141
  }
10765
- await execa11("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
10766
- 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`], {
10767
11144
  cwd: root
10768
11145
  });
10769
11146
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
10770
- 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`], {
10771
11148
  cwd: root,
10772
11149
  reject: false
10773
11150
  });
10774
11151
  commitMessage = log2;
10775
11152
  }
10776
- const { stdout: gitBranch } = await execa11("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
11153
+ const { stdout: gitBranch } = await execa12("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
10777
11154
  cwd: root,
10778
11155
  reject: false
10779
11156
  });
@@ -10855,11 +11232,11 @@ ${BOLD}If the divergence is deliberate${OFF}
10855
11232
  }
10856
11233
 
10857
11234
  // src/scripts/check-eventbridge-log-permissions.ts
10858
- import { execa as execa12 } from "execa";
11235
+ import { execa as execa13 } from "execa";
10859
11236
 
10860
11237
  // src/lib/eventbridge-log-permission-guard.ts
10861
- import { readFileSync as readFileSync30, readdirSync as readdirSync17, statSync as statSync10 } from "fs";
10862
- 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";
10863
11240
  var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", ".terraform", ".worktrees", "dist"]);
10864
11241
  var EVENT_TARGET_TYPE = "aws_cloudwatch_event_target";
10865
11242
  var LOG_RESOURCE_POLICY_TYPE = "aws_cloudwatch_log_resource_policy";
@@ -10931,15 +11308,15 @@ function walkTerraformFiles(root) {
10931
11308
  const walk2 = (dir) => {
10932
11309
  let entries;
10933
11310
  try {
10934
- entries = readdirSync17(dir);
11311
+ entries = readdirSync18(dir);
10935
11312
  } catch {
10936
11313
  return;
10937
11314
  }
10938
11315
  for (const entry of entries) {
10939
- const p = join42(dir, entry);
11316
+ const p = join44(dir, entry);
10940
11317
  let st;
10941
11318
  try {
10942
- st = statSync10(p);
11319
+ st = statSync11(p);
10943
11320
  } catch {
10944
11321
  continue;
10945
11322
  }
@@ -10979,7 +11356,7 @@ function auditEventBridgeLogPermissions(root) {
10979
11356
  let rawEventTargetCount = 0;
10980
11357
  let rawLogPolicyCount = 0;
10981
11358
  for (const file of files) {
10982
- const text = readFileSync30(file, "utf8");
11359
+ const text = readFileSync32(file, "utf8");
10983
11360
  rawEventTargetCount += countRawResourceDeclarations(text, EVENT_TARGET_TYPE);
10984
11361
  rawLogPolicyCount += countRawResourceDeclarations(text, LOG_RESOURCE_POLICY_TYPE);
10985
11362
  eventTargetBlocks.push(...findResourceBlocks(text, file, EVENT_TARGET_TYPE));
@@ -11032,7 +11409,7 @@ function auditEventBridgeLogPermissions(root) {
11032
11409
 
11033
11410
  // src/scripts/check-eventbridge-log-permissions.ts
11034
11411
  async function runEventBridgeLogPermissionCheck() {
11035
- const root = (await execa12("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11412
+ const root = (await execa13("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11036
11413
  let report;
11037
11414
  try {
11038
11415
  report = auditEventBridgeLogPermissions(root);
@@ -11069,15 +11446,15 @@ async function runEventBridgeLogPermissionCheck() {
11069
11446
  }
11070
11447
 
11071
11448
  // src/scripts/check-lambda-output.ts
11072
- import { execa as execa13 } from "execa";
11449
+ import { execa as execa14 } from "execa";
11073
11450
 
11074
11451
  // src/lib/lambda-output-guard.ts
11075
- import { readFileSync as readFileSync32 } from "fs";
11076
- import { join as join44 } from "path";
11452
+ import { readFileSync as readFileSync34 } from "fs";
11453
+ import { join as join46 } from "path";
11077
11454
 
11078
11455
  // src/lib/terraform-input-guard.ts
11079
- import { readdirSync as readdirSync18, readFileSync as readFileSync31, statSync as statSync11 } from "fs";
11080
- 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";
11081
11458
  var GUARDED_SUBCOMMANDS = [
11082
11459
  "init",
11083
11460
  "plan",
@@ -11092,20 +11469,20 @@ function stripComments2(source) {
11092
11469
  }
11093
11470
  function findWorkflowFiles(repoRoot) {
11094
11471
  const found = [];
11095
- const walk2 = (dir, relative9) => {
11472
+ const walk2 = (dir, relative10) => {
11096
11473
  let entries;
11097
11474
  try {
11098
- entries = readdirSync18(dir);
11475
+ entries = readdirSync19(dir);
11099
11476
  } catch {
11100
11477
  return;
11101
11478
  }
11102
11479
  for (const entry of entries) {
11103
11480
  if (entry === "node_modules" || entry === ".git" || entry === ".worktrees") continue;
11104
- const full = join43(dir, entry);
11105
- const rel = relative9 ? `${relative9}/${entry}` : entry;
11106
- if (statSync11(full).isDirectory()) {
11481
+ const full = join45(dir, entry);
11482
+ const rel = relative10 ? `${relative10}/${entry}` : entry;
11483
+ if (statSync12(full).isDirectory()) {
11107
11484
  walk2(full, rel);
11108
- } else if (/\.ya?ml$/.test(entry) && relative9.endsWith(".github/workflows")) {
11485
+ } else if (/\.ya?ml$/.test(entry) && relative10.endsWith(".github/workflows")) {
11109
11486
  found.push(rel);
11110
11487
  }
11111
11488
  }
@@ -11145,7 +11522,7 @@ function checkWorkflowSource(file, rawSource) {
11145
11522
  }
11146
11523
  function checkTerraformInput(repoRoot) {
11147
11524
  return findWorkflowFiles(repoRoot).flatMap(
11148
- (file) => checkWorkflowSource(file, readFileSync31(join43(repoRoot, file), "utf8"))
11525
+ (file) => checkWorkflowSource(file, readFileSync33(join45(repoRoot, file), "utf8"))
11149
11526
  );
11150
11527
  }
11151
11528
 
@@ -11203,13 +11580,13 @@ function checkWorkflowSource2(file, rawSource) {
11203
11580
  }
11204
11581
  function checkLambdaOutput(repoRoot) {
11205
11582
  return findWorkflowFiles(repoRoot).flatMap(
11206
- (file) => checkWorkflowSource2(file, readFileSync32(join44(repoRoot, file), "utf8"))
11583
+ (file) => checkWorkflowSource2(file, readFileSync34(join46(repoRoot, file), "utf8"))
11207
11584
  );
11208
11585
  }
11209
11586
 
11210
11587
  // src/scripts/check-lambda-output.ts
11211
11588
  async function runLambdaOutputCheck() {
11212
- const root = (await execa13("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11589
+ const root = (await execa14("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11213
11590
  const files = findWorkflowFiles(root);
11214
11591
  console.log(`audited ${files.length} workflow file(s) under ${root}`);
11215
11592
  if (files.length === 0) {
@@ -11231,9 +11608,9 @@ async function runLambdaOutputCheck() {
11231
11608
  }
11232
11609
 
11233
11610
  // src/scripts/check-pipe-trap.ts
11234
- import { readFileSync as readFileSync33, readdirSync as readdirSync19 } from "fs";
11235
- import { join as join45, relative as relative7 } from "path";
11236
- 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";
11237
11614
 
11238
11615
  // src/lib/pipe-trap-guard.ts
11239
11616
  var STATUS_BEARING = [
@@ -11329,23 +11706,23 @@ function findPipeTraps(source) {
11329
11706
  function shellFiles(root) {
11330
11707
  const out = [];
11331
11708
  for (const dir of ["scripts", ".githooks"]) {
11332
- const full = join45(root, dir);
11709
+ const full = join47(root, dir);
11333
11710
  let entries;
11334
11711
  try {
11335
- entries = readdirSync19(full, { withFileTypes: true });
11712
+ entries = readdirSync20(full, { withFileTypes: true });
11336
11713
  } catch {
11337
11714
  continue;
11338
11715
  }
11339
11716
  for (const entry of entries) {
11340
11717
  if (!entry.isFile()) continue;
11341
11718
  if (dir === "scripts" && !entry.name.endsWith(".sh")) continue;
11342
- out.push(join45(full, entry.name));
11719
+ out.push(join47(full, entry.name));
11343
11720
  }
11344
11721
  }
11345
11722
  return out;
11346
11723
  }
11347
11724
  async function runPipeTrapCheck() {
11348
- const root = (await execa14("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11725
+ const root = (await execa15("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11349
11726
  const files = shellFiles(root);
11350
11727
  console.log(`audited ${files.length} shell file(s) under scripts/ and .githooks/ under ${root}`);
11351
11728
  if (files.length === 0) {
@@ -11355,8 +11732,8 @@ async function runPipeTrapCheck() {
11355
11732
  process.exit(1);
11356
11733
  }
11357
11734
  const findings = files.flatMap(
11358
- (file) => findPipeTraps(readFileSync33(file, "utf8")).map(
11359
- (t) => `${relative7(root, file)}:${t.line} ${t.text}
11735
+ (file) => findPipeTraps(readFileSync35(file, "utf8")).map(
11736
+ (t) => `${relative8(root, file)}:${t.line} ${t.text}
11360
11737
  ${t.reason}`
11361
11738
  )
11362
11739
  );
@@ -11372,11 +11749,11 @@ async function runPipeTrapCheck() {
11372
11749
  }
11373
11750
 
11374
11751
  // src/scripts/check-plugin-allowlist-convention.ts
11375
- import { execa as execa15 } from "execa";
11752
+ import { execa as execa16 } from "execa";
11376
11753
 
11377
11754
  // src/lib/plugin-allowlist-convention.ts
11378
- import { readFileSync as readFileSync34 } from "fs";
11379
- import { join as join46 } from "path";
11755
+ import { readFileSync as readFileSync36 } from "fs";
11756
+ import { join as join48 } from "path";
11380
11757
  var COMPUTE_MAIN_TF = "modules/cloud/aws/compute/main.tf";
11381
11758
  var PLUGIN_TEMPLATE_MAIN_TF = "modules/plugins/_template/main.tf";
11382
11759
  var ALLOWLIST_MAIN_TF = "modules/cloud/aws/plugin-allowlist/main.tf";
@@ -11385,18 +11762,18 @@ var PROJECT = "<project>";
11385
11762
  var ENV = "<env>";
11386
11763
  var PLUGIN = "<plugin>";
11387
11764
  var ACCOUNT = "<account>";
11388
- function read(repoRoot, relative9) {
11765
+ function read(repoRoot, relative10) {
11389
11766
  try {
11390
- return readFileSync34(join46(repoRoot, relative9), "utf8");
11767
+ return readFileSync36(join48(repoRoot, relative10), "utf8");
11391
11768
  } catch {
11392
- throw new Error(`plugin-allowlist drift guard: cannot read ${relative9}`);
11769
+ throw new Error(`plugin-allowlist drift guard: cannot read ${relative10}`);
11393
11770
  }
11394
11771
  }
11395
11772
  function assignedString(source, name) {
11396
11773
  const match = new RegExp(`^\\s*${name}\\s*=\\s*"((?:[^"\\\\]|\\\\.)*)"\\s*$`, "m").exec(source);
11397
11774
  return match?.[1];
11398
11775
  }
11399
- function resolve18(expression, bindings) {
11776
+ function resolve19(expression, bindings) {
11400
11777
  let current = expression;
11401
11778
  for (let pass = 0; pass < 10; pass += 1) {
11402
11779
  const next = current.replace(/\$\{([^}]+)\}/g, (whole, ref) => {
@@ -11433,13 +11810,13 @@ function composeExpectedRoleName(repoRoot) {
11433
11810
  "var.project_name": PROJECT,
11434
11811
  "var.environment": ENV,
11435
11812
  "var.plugin_name": PLUGIN,
11436
- "var.function_name": resolve18(pluginFunctionName, {
11813
+ "var.function_name": resolve19(pluginFunctionName, {
11437
11814
  "var.plugin_name": PLUGIN
11438
11815
  })
11439
11816
  };
11440
- bindings["local.name_prefix"] = resolve18(namePrefix, bindings);
11441
- bindings["local.function_name"] = resolve18(functionName, bindings);
11442
- 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);
11443
11820
  }
11444
11821
  function readAllowlistGlob(repoRoot) {
11445
11822
  const allowlist = read(repoRoot, ALLOWLIST_MAIN_TF);
@@ -11449,7 +11826,7 @@ function readAllowlistGlob(repoRoot) {
11449
11826
  `plugin-allowlist drift guard: could not find the "for name in var.enabled_plugins" glob in ${ALLOWLIST_MAIN_TF}.`
11450
11827
  );
11451
11828
  }
11452
- return resolve18(glob, {
11829
+ return resolve19(glob, {
11453
11830
  "data.aws_caller_identity.current.account_id": ACCOUNT,
11454
11831
  "var.project_name": PROJECT,
11455
11832
  "var.environment": ENV,
@@ -11483,7 +11860,7 @@ Plugins would be rejected by require_service_principal (ADR-0009). Fix the glob,
11483
11860
 
11484
11861
  // src/scripts/check-plugin-allowlist-convention.ts
11485
11862
  async function runPluginAllowlistConventionCheck() {
11486
- const root = (await execa15("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11863
+ const root = (await execa16("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11487
11864
  let violations;
11488
11865
  try {
11489
11866
  violations = checkAllowlistConvention(root);
@@ -11508,34 +11885,34 @@ async function runPluginAllowlistConventionCheck() {
11508
11885
  }
11509
11886
 
11510
11887
  // src/scripts/check-plugin-collisions.ts
11511
- import { existsSync as existsSync39 } from "fs";
11512
- import { join as join48 } from "path";
11513
- 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";
11514
11891
 
11515
11892
  // src/lib/plugin-collision-guard.ts
11516
- import { existsSync as existsSync38, readdirSync as readdirSync20, statSync as statSync12 } from "fs";
11517
- 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";
11518
11895
  var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
11519
11896
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
11520
11897
  function subdirectories(dir) {
11521
- if (!existsSync38(dir)) return [];
11522
- return readdirSync20(dir).filter((entry) => {
11898
+ if (!existsSync40(dir)) return [];
11899
+ return readdirSync21(dir).filter((entry) => {
11523
11900
  if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
11524
11901
  try {
11525
- return statSync12(join47(dir, entry)).isDirectory();
11902
+ return statSync13(join49(dir, entry)).isDirectory();
11526
11903
  } catch {
11527
11904
  return false;
11528
11905
  }
11529
11906
  });
11530
11907
  }
11531
11908
  function regularPackagesOf(pluginDir2) {
11532
- 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();
11533
11910
  }
11534
11911
  function bareTestModulesOf(pluginDir2) {
11535
- const testsDir = join47(pluginDir2, "tests");
11536
- if (!existsSync38(testsDir)) return [];
11537
- if (existsSync38(join47(testsDir, "__init__.py"))) return [];
11538
- 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();
11539
11916
  }
11540
11917
  function findCollisions(servicesDir, pluginDirs) {
11541
11918
  const plugins = (pluginDirs ?? subdirectories(servicesDir)).filter((name) => !name.startsWith("_")).filter((name) => name !== "api").sort();
@@ -11543,7 +11920,7 @@ function findCollisions(servicesDir, pluginDirs) {
11543
11920
  const gather = (kind, namesOf) => {
11544
11921
  const claims = /* @__PURE__ */ new Map();
11545
11922
  for (const plugin of plugins) {
11546
- for (const name of namesOf(join47(servicesDir, plugin))) {
11923
+ for (const name of namesOf(join49(servicesDir, plugin))) {
11547
11924
  claims.set(name, [...claims.get(name) ?? [], plugin]);
11548
11925
  }
11549
11926
  }
@@ -11580,9 +11957,9 @@ function formatCollisions(collisions) {
11580
11957
 
11581
11958
  // src/scripts/check-plugin-collisions.ts
11582
11959
  async function runPluginCollisionCheck() {
11583
- const root = (await execa16("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11584
- const servicesDir = join48(root, "services");
11585
- 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)) {
11586
11963
  console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
11587
11964
  return;
11588
11965
  }
@@ -11599,11 +11976,11 @@ async function runPluginCollisionCheck() {
11599
11976
  }
11600
11977
 
11601
11978
  // src/scripts/check-plugin-terraform.ts
11602
- import { execa as execa17 } from "execa";
11979
+ import { execa as execa18 } from "execa";
11603
11980
 
11604
11981
  // src/lib/plugin-terraform-guard.ts
11605
- import { existsSync as existsSync40, readFileSync as readFileSync35, readdirSync as readdirSync21 } from "fs";
11606
- 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";
11607
11984
  var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
11608
11985
  var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
11609
11986
  function findPluginManifests(root) {
@@ -11611,16 +11988,16 @@ function findPluginManifests(root) {
11611
11988
  const walk2 = (dir) => {
11612
11989
  let entries;
11613
11990
  try {
11614
- entries = readdirSync21(dir, { withFileTypes: true });
11991
+ entries = readdirSync22(dir, { withFileTypes: true });
11615
11992
  } catch {
11616
11993
  return;
11617
11994
  }
11618
11995
  for (const entry of entries) {
11619
11996
  if (entry.isDirectory()) {
11620
11997
  if (SKIP_DIRS3.has(entry.name)) continue;
11621
- walk2(join49(dir, entry.name));
11998
+ walk2(join51(dir, entry.name));
11622
11999
  } else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
11623
- found.push(relative8(root, join49(dir, entry.name)).split(sep3).join("/"));
12000
+ found.push(relative9(root, join51(dir, entry.name)).split(sep3).join("/"));
11624
12001
  }
11625
12002
  }
11626
12003
  };
@@ -11630,7 +12007,7 @@ function findPluginManifests(root) {
11630
12007
  function readSubscriptions(absManifestPath) {
11631
12008
  let parsed;
11632
12009
  try {
11633
- parsed = JSON.parse(readFileSync35(absManifestPath, "utf8"));
12010
+ parsed = JSON.parse(readFileSync37(absManifestPath, "utf8"));
11634
12011
  } catch {
11635
12012
  return null;
11636
12013
  }
@@ -11645,15 +12022,15 @@ function readSubscriptions(absManifestPath) {
11645
12022
  }
11646
12023
  function checkPluginTerraform(root) {
11647
12024
  const violations = [];
11648
- const coreManifest = existsSync40(join49(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
12025
+ const coreManifest = existsSync42(join51(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
11649
12026
  for (const manifest of findPluginManifests(root)) {
11650
12027
  if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
11651
- const absManifest = join49(root, manifest);
12028
+ const absManifest = join51(root, manifest);
11652
12029
  const subscriptions = readSubscriptions(absManifest);
11653
12030
  if (subscriptions === null) continue;
11654
12031
  const pluginDir2 = dirname10(absManifest);
11655
- if (existsSync40(join49(pluginDir2, "terraform"))) continue;
11656
- const relPluginDir = relative8(root, pluginDir2).split(sep3).join("/");
12032
+ if (existsSync42(join51(pluginDir2, "terraform"))) continue;
12033
+ const relPluginDir = relative9(root, pluginDir2).split(sep3).join("/");
11657
12034
  violations.push({
11658
12035
  manifest,
11659
12036
  expectedTerraformDir: relPluginDir ? `${relPluginDir}/terraform` : "terraform",
@@ -11672,7 +12049,7 @@ function formatViolations(violations) {
11672
12049
 
11673
12050
  // src/scripts/check-plugin-terraform.ts
11674
12051
  async function runPluginTerraformCheck() {
11675
- const root = (await execa17("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12052
+ const root = (await execa18("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
11676
12053
  const violations = checkPluginTerraform(root);
11677
12054
  if (violations.length > 0) {
11678
12055
  console.error("\u2717 plugin Terraform guard: event subscriptions with no infrastructure\n");
@@ -11683,13 +12060,13 @@ async function runPluginTerraformCheck() {
11683
12060
  }
11684
12061
 
11685
12062
  // src/scripts/check-plugin-tool-supply.ts
11686
- import { existsSync as existsSync42 } from "fs";
11687
- import { join as join51 } from "path";
11688
- 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";
11689
12066
 
11690
12067
  // src/lib/plugin-tool-supply-audit.ts
11691
- import { existsSync as existsSync41, readFileSync as readFileSync36, readdirSync as readdirSync22, statSync as statSync13 } from "fs";
11692
- 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";
11693
12070
 
11694
12071
  // src/lib/openrouter-model-snapshot.ts
11695
12072
  var OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT = "2026-08-10T06:39:01Z";
@@ -12100,13 +12477,13 @@ var OPENROUTER_MODEL_IDS = [
12100
12477
  function listDirs(root) {
12101
12478
  let entries;
12102
12479
  try {
12103
- entries = readdirSync22(root);
12480
+ entries = readdirSync23(root);
12104
12481
  } catch {
12105
12482
  return [];
12106
12483
  }
12107
12484
  return entries.filter((e) => {
12108
12485
  try {
12109
- return statSync13(join50(root, e)).isDirectory();
12486
+ return statSync14(join52(root, e)).isDirectory();
12110
12487
  } catch {
12111
12488
  return false;
12112
12489
  }
@@ -12117,15 +12494,15 @@ function walkFiles2(root, accept, skipDir) {
12117
12494
  const walk2 = (dir) => {
12118
12495
  let entries;
12119
12496
  try {
12120
- entries = readdirSync22(dir);
12497
+ entries = readdirSync23(dir);
12121
12498
  } catch {
12122
12499
  return;
12123
12500
  }
12124
12501
  for (const entry of entries) {
12125
- const p = join50(dir, entry);
12502
+ const p = join52(dir, entry);
12126
12503
  let st;
12127
12504
  try {
12128
- st = statSync13(p);
12505
+ st = statSync14(p);
12129
12506
  } catch {
12130
12507
  continue;
12131
12508
  }
@@ -12148,14 +12525,14 @@ function pluginPythonFiles(pluginDir2) {
12148
12525
  );
12149
12526
  }
12150
12527
  function pluginTerraformFiles(pluginDir2) {
12151
- const tfDir = join50(pluginDir2, "terraform");
12528
+ const tfDir = join52(pluginDir2, "terraform");
12152
12529
  let entries;
12153
12530
  try {
12154
- entries = readdirSync22(tfDir);
12531
+ entries = readdirSync23(tfDir);
12155
12532
  } catch {
12156
12533
  return [];
12157
12534
  }
12158
- 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();
12159
12536
  }
12160
12537
  function extractManifestTools(manifestText) {
12161
12538
  let parsed;
@@ -12407,8 +12784,8 @@ function isSnapshotStale(fetchedAt, now) {
12407
12784
  function normalizeModelId(id) {
12408
12785
  return id.endsWith(":online") ? id.slice(0, -":online".length) : id;
12409
12786
  }
12410
- var CONFIG_PY_PATH = join50("services", "api", "src", "api", "config.py");
12411
- var ORCHESTRATION_SCHEMA_PATH = join50(
12787
+ var CONFIG_PY_PATH = join52("services", "api", "src", "api", "config.py");
12788
+ var ORCHESTRATION_SCHEMA_PATH = join52(
12412
12789
  "services",
12413
12790
  "api",
12414
12791
  "src",
@@ -12420,10 +12797,10 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
12420
12797
  const knownModelIds = options.knownModelIds ?? OPENROUTER_MODEL_IDS;
12421
12798
  const snapshotFetchedAt = options.snapshotFetchedAt ?? OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT;
12422
12799
  const now = options.now ?? /* @__PURE__ */ new Date();
12423
- const configPath = join50(repoRoot, CONFIG_PY_PATH);
12424
- const orchestrationPath = join50(repoRoot, ORCHESTRATION_SCHEMA_PATH);
12425
- const configMissing = !existsSync41(configPath);
12426
- 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);
12427
12804
  const knownSet = new Set(knownModelIds);
12428
12805
  const snapshotEmpty = knownModelIds.length === 0;
12429
12806
  const snapshotStale = isSnapshotStale(snapshotFetchedAt, now);
@@ -12441,13 +12818,13 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
12441
12818
  };
12442
12819
  let settingsBlind = false;
12443
12820
  if (!configMissing) {
12444
- const settingsFields = extractSettingsModelFields(readFileSync36(configPath, "utf8"));
12821
+ const settingsFields = extractSettingsModelFields(readFileSync38(configPath, "utf8"));
12445
12822
  if (settingsFields.length === 0) settingsBlind = true;
12446
12823
  for (const { field, value } of settingsFields) record(`${CONFIG_PY_PATH}#${field}`, value);
12447
12824
  }
12448
12825
  let curatedFieldsBlind = false;
12449
12826
  if (!orchestrationSchemaMissing) {
12450
- const curated = extractCuratedModelFields(readFileSync36(orchestrationPath, "utf8"));
12827
+ const curated = extractCuratedModelFields(readFileSync38(orchestrationPath, "utf8"));
12451
12828
  if (curated.rawFieldCount > 0 && curated.fields.every((f) => f.defaultValue === null && f.optionValues.length === 0)) {
12452
12829
  curatedFieldsBlind = true;
12453
12830
  }
@@ -12494,7 +12871,7 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
12494
12871
  function discoverPluginDirs(pluginsRoot) {
12495
12872
  return listDirs(pluginsRoot).filter((name) => {
12496
12873
  try {
12497
- return statSync13(join50(pluginsRoot, name, "biffo.plugin.json")).isFile();
12874
+ return statSync14(join52(pluginsRoot, name, "biffo.plugin.json")).isFile();
12498
12875
  } catch {
12499
12876
  return false;
12500
12877
  }
@@ -12507,8 +12884,8 @@ function auditPluginToolSupply(pluginsRoot) {
12507
12884
  let terraformBlind = false;
12508
12885
  let totalDeclaredTools = 0;
12509
12886
  for (const name of pluginNames) {
12510
- const pluginDir2 = join50(pluginsRoot, name);
12511
- 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");
12512
12889
  const manifest = extractManifestTools(manifestText);
12513
12890
  if (manifest.parseError) {
12514
12891
  findings.push({
@@ -12526,13 +12903,13 @@ function auditPluginToolSupply(pluginsRoot) {
12526
12903
  totalDeclaredTools += manifest.tools.length;
12527
12904
  const pySources = pluginPythonFiles(pluginDir2).map((f) => ({
12528
12905
  file: f,
12529
- text: readFileSync36(f, "utf8")
12906
+ text: readFileSync38(f, "utf8")
12530
12907
  }));
12531
12908
  const resolver = buildSymbolResolver(pySources);
12532
12909
  const registry = extractToolRegistryEntries(pySources, resolver);
12533
12910
  if (registry.rawToolDefinitionCount > 0 && registry.entries.length === 0) registryBlind = true;
12534
12911
  const tfFiles = pluginTerraformFiles(pluginDir2);
12535
- const tfText = tfFiles.map((f) => readFileSync36(f, "utf8")).join("\n");
12912
+ const tfText = tfFiles.map((f) => readFileSync38(f, "utf8")).join("\n");
12536
12913
  const terraform = extractTerraformEnvKeys(tfText);
12537
12914
  if (terraform.rawMarkerCount > 0 && terraform.resolvedBlockCount === 0) terraformBlind = true;
12538
12915
  for (const toolName of manifest.tools) {
@@ -12606,7 +12983,7 @@ function auditPluginToolSupply(pluginsRoot) {
12606
12983
  requiredEnvVars: envResult.envVars,
12607
12984
  missingEnvVars: anyWired ? [] : envResult.envVars,
12608
12985
  status: anyWired ? "ok" : "missing-env",
12609
- 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`
12610
12987
  });
12611
12988
  }
12612
12989
  }
@@ -12637,10 +13014,10 @@ function auditPluginToolSupply(pluginsRoot) {
12637
13014
 
12638
13015
  // src/scripts/check-plugin-tool-supply.ts
12639
13016
  async function runPluginToolSupplyCheck() {
12640
- const root = (await execa18("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13017
+ const root = (await execa19("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12641
13018
  let allOk = true;
12642
- const pluginsRoot = join51(root, "services", "_plugins");
12643
- if (!existsSync42(pluginsRoot)) {
13019
+ const pluginsRoot = join53(root, "services", "_plugins");
13020
+ if (!existsSync44(pluginsRoot)) {
12644
13021
  console.log("\u2713 plugin tool-supply guard: no services/_plugins/ \u2014 nothing to audit");
12645
13022
  } else {
12646
13023
  const report = auditPluginToolSupply(pluginsRoot);
@@ -12670,8 +13047,8 @@ async function runPluginToolSupplyCheck() {
12670
13047
  console.log(`\u2713 plugin tool-supply guard: ${report.summary}`);
12671
13048
  }
12672
13049
  }
12673
- const servicesApiRoot = join51(root, "services", "api");
12674
- if (!existsSync42(servicesApiRoot)) {
13050
+ const servicesApiRoot = join53(root, "services", "api");
13051
+ if (!existsSync44(servicesApiRoot)) {
12675
13052
  console.log("\u2713 plugin model-id guard: no services/api/ \u2014 nothing to audit");
12676
13053
  } else {
12677
13054
  const modelReport = auditDeclaredModelIds(root);
@@ -12717,7 +13094,7 @@ async function runPluginToolSupplyCheck() {
12717
13094
  }
12718
13095
 
12719
13096
  // src/scripts/check-release-subject.ts
12720
- import { execa as execa19 } from "execa";
13097
+ import { execa as execa20 } from "execa";
12721
13098
 
12722
13099
  // src/lib/release-version.ts
12723
13100
  var MINOR_TYPES = /* @__PURE__ */ new Set(["feat"]);
@@ -12754,7 +13131,7 @@ async function fetchPrTitleViaGh({
12754
13131
  PR_NUMBER,
12755
13132
  GH_REPO
12756
13133
  }) {
12757
- const { stdout } = await execa19(
13134
+ const { stdout } = await execa20(
12758
13135
  "gh",
12759
13136
  ["pr", "view", PR_NUMBER, "--repo", GH_REPO, "--json", "title", "--jq", ".title"],
12760
13137
  { env: { ...process.env, GH_TOKEN } }
@@ -12790,7 +13167,7 @@ async function resolveReleaseSubject({
12790
13167
  );
12791
13168
  }
12792
13169
  }
12793
- return (await execa19("git", ["log", "-1", "--format=%s"], { cwd })).stdout.trim();
13170
+ return (await execa20("git", ["log", "-1", "--format=%s"], { cwd })).stdout.trim();
12794
13171
  }
12795
13172
  async function runReleaseSubjectCheck(argv) {
12796
13173
  const base = process.env["GITHUB_BASE_REF"] ?? argv[0];
@@ -12798,9 +13175,9 @@ async function runReleaseSubjectCheck(argv) {
12798
13175
  console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
12799
13176
  process.exit(2);
12800
13177
  }
12801
- const root = (await execa19("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12802
- await execa19("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
12803
- 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`], {
12804
13181
  cwd: root
12805
13182
  });
12806
13183
  const changedFiles = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
@@ -12848,13 +13225,13 @@ async function runReleaseSubjectCheck(argv) {
12848
13225
  }
12849
13226
 
12850
13227
  // src/scripts/check-skeleton-drift.ts
12851
- import { existsSync as existsSync43, readdirSync as readdirSync24 } from "fs";
12852
- import { join as join53 } from "path";
12853
- 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";
12854
13231
 
12855
13232
  // src/lib/skeleton-drift-guard.ts
12856
- import { readFileSync as readFileSync37, readdirSync as readdirSync23, statSync as statSync14 } from "fs";
12857
- 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";
12858
13235
  var isWorkflow = (rel) => rel.startsWith(".github/workflows/") && (rel.endsWith(".yml") || rel.endsWith(".yaml"));
12859
13236
  var isRootLayout = (rel) => rel.endsWith("src/app/layout.tsx");
12860
13237
  var uncommented = (contents) => contents.split("\n").filter((line) => !/^\s*(\/\/|\/\*|\*)/.test(line)).join("\n");
@@ -12912,16 +13289,16 @@ function walk(dir, base = dir) {
12912
13289
  const out = [];
12913
13290
  let entries;
12914
13291
  try {
12915
- entries = readdirSync23(dir);
13292
+ entries = readdirSync24(dir);
12916
13293
  } catch {
12917
13294
  return out;
12918
13295
  }
12919
13296
  for (const entry of entries) {
12920
13297
  if (entry === ".venv" || entry === "node_modules" || entry === ".git") continue;
12921
- const abs = join52(dir, entry);
13298
+ const abs = join54(dir, entry);
12922
13299
  let isDir;
12923
13300
  try {
12924
- isDir = statSync14(abs).isDirectory();
13301
+ isDir = statSync15(abs).isDirectory();
12925
13302
  } catch {
12926
13303
  continue;
12927
13304
  }
@@ -12940,7 +13317,7 @@ function auditSkeleton(skeletonRoot, name, rules = SKELETON_RULES) {
12940
13317
  if (!rule.appliesTo(rel)) continue;
12941
13318
  let contents;
12942
13319
  try {
12943
- contents = readFileSync37(join52(skeletonRoot, rel), "utf8");
13320
+ contents = readFileSync39(join54(skeletonRoot, rel), "utf8");
12944
13321
  } catch {
12945
13322
  continue;
12946
13323
  }
@@ -12969,23 +13346,23 @@ function formatViolations2(violations) {
12969
13346
 
12970
13347
  // src/scripts/check-skeleton-drift.ts
12971
13348
  function discoverSkeletons(root) {
12972
- const skeletonsDir = join53(root, "_skeletons");
13349
+ const skeletonsDir = join55(root, "_skeletons");
12973
13350
  let entries;
12974
13351
  try {
12975
- 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);
12976
13353
  } catch {
12977
13354
  return [];
12978
13355
  }
12979
- 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();
12980
13357
  }
12981
13358
  async function runSkeletonDriftCheck() {
12982
- const root = (await execa20("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13359
+ const root = (await execa21("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12983
13360
  const skeletons = discoverSkeletons(root);
12984
13361
  let filesConsidered = 0;
12985
13362
  for (const name of skeletons) {
12986
- const skeletonRoot = join53(root, "_skeletons", name);
13363
+ const skeletonRoot = join55(root, "_skeletons", name);
12987
13364
  filesConsidered += findWorkflowFiles(skeletonRoot).length;
12988
- if (existsSync43(join53(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
13365
+ if (existsSync45(join55(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
12989
13366
  filesConsidered += 1;
12990
13367
  }
12991
13368
  }
@@ -12999,7 +13376,7 @@ async function runSkeletonDriftCheck() {
12999
13376
  process.exit(1);
13000
13377
  }
13001
13378
  const violations = skeletons.flatMap(
13002
- (name) => auditSkeleton(join53(root, "_skeletons", name), name)
13379
+ (name) => auditSkeleton(join55(root, "_skeletons", name), name)
13003
13380
  );
13004
13381
  if (violations.length > 0) {
13005
13382
  console.error("\u2717 Skeleton-drift guard: drift found between this repo and its scaffolding\n");
@@ -13011,9 +13388,9 @@ async function runSkeletonDriftCheck() {
13011
13388
  }
13012
13389
 
13013
13390
  // src/scripts/check-terraform-input.ts
13014
- import { execa as execa21 } from "execa";
13391
+ import { execa as execa22 } from "execa";
13015
13392
  async function runTerraformInputCheck() {
13016
- const root = (await execa21("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13393
+ const root = (await execa22("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13017
13394
  const files = findWorkflowFiles(root);
13018
13395
  console.log(`audited ${files.length} workflow file(s) under ${root}`);
13019
13396
  if (files.length === 0) {
@@ -13035,8 +13412,8 @@ async function runTerraformInputCheck() {
13035
13412
  }
13036
13413
 
13037
13414
  // src/commands/check.ts
13038
- var checkCommand = new Command23("check").description(
13039
- "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)"
13040
13417
  );
13041
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 () => {
13042
13419
  await runOwnershipCheck(rawArgsAfter("ownership"));
@@ -13121,16 +13498,26 @@ checkCommand.command("branch-protection").description(
13121
13498
  ).action(async (opts) => {
13122
13499
  await runBranchProtectionCheck(opts.repo, { fix: opts.fix });
13123
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
+ });
13124
13511
  function rawArgsAfter(subcommand) {
13125
13512
  const at = process.argv.indexOf(subcommand);
13126
13513
  return at === -1 ? [] : process.argv.slice(at + 1);
13127
13514
  }
13128
13515
 
13129
13516
  // src/commands/doctor.ts
13130
- import { existsSync as existsSync44, readFileSync as readFileSync38 } from "fs";
13131
- 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";
13132
13519
  import chalk21 from "chalk";
13133
- import { Command as Command24 } from "commander";
13520
+ import { Command as Command25 } from "commander";
13134
13521
 
13135
13522
  // src/lib/doctor.ts
13136
13523
  function checkCheckoutCurrency(facts) {
@@ -13253,10 +13640,10 @@ function runDoctorChecks(facts) {
13253
13640
 
13254
13641
  // src/commands/doctor.ts
13255
13642
  var INTEGRATION_BRANCH = "dev";
13256
- var doctorCommand = new Command24("doctor").description(
13643
+ var doctorCommand = new Command25("doctor").description(
13257
13644
  "Report repo-state conditions that make everything read from this checkout unreliable"
13258
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) => {
13259
- const cwd = options.cwd ? resolve19(options.cwd) : process.cwd();
13646
+ const cwd = options.cwd ? resolve20(options.cwd) : process.cwd();
13260
13647
  try {
13261
13648
  const findings = await runDoctor({ cwd, fetch: options.fetch !== false });
13262
13649
  printFindings(findings);
@@ -13307,10 +13694,10 @@ async function runDoctor(options, deps = { git: new GitAdapter() }) {
13307
13694
  return runDoctorChecks(facts);
13308
13695
  }
13309
13696
  function readLocalCoreVersion(cwd) {
13310
- const path = join54(cwd, INSTANCE_CORE_FILE);
13311
- if (!existsSync44(path)) return null;
13697
+ const path = join56(cwd, INSTANCE_CORE_FILE);
13698
+ if (!existsSync46(path)) return null;
13312
13699
  try {
13313
- return extractVersionField(readFileSync38(path, "utf8"));
13700
+ return extractVersionField(readFileSync40(path, "utf8"));
13314
13701
  } catch {
13315
13702
  return null;
13316
13703
  }
@@ -13330,10 +13717,10 @@ function extractVersionField(contents) {
13330
13717
  return match?.[1] ?? null;
13331
13718
  }
13332
13719
  function readFossil(cwd) {
13333
- const path = join54(cwd, CORE_VERSION_FILE);
13334
- if (!existsSync44(path)) return null;
13720
+ const path = join56(cwd, CORE_VERSION_FILE);
13721
+ if (!existsSync46(path)) return null;
13335
13722
  try {
13336
- const value = readFileSync38(path, "utf8").trim();
13723
+ const value = readFileSync40(path, "utf8").trim();
13337
13724
  return value === "" ? null : value;
13338
13725
  } catch {
13339
13726
  return null;
@@ -13366,9 +13753,9 @@ function printFindings(findings) {
13366
13753
  import { execSync as execSync7 } from "child_process";
13367
13754
  import { GetCallerIdentityCommand as GetCallerIdentityCommand3, STSClient as STSClient3 } from "@aws-sdk/client-sts";
13368
13755
  import chalk22 from "chalk";
13369
- import { Command as Command25 } from "commander";
13756
+ import { Command as Command26 } from "commander";
13370
13757
  import inquirer8 from "inquirer";
13371
- var teardownCommand = new Command25("teardown").description(
13758
+ var teardownCommand = new Command26("teardown").description(
13372
13759
  "Destroy all infrastructure then remove the repo, IAM role, and state bucket \u2014 single command"
13373
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(
13374
13761
  "--confirm <name>",
@@ -13779,16 +14166,16 @@ function resolveGithubToken4() {
13779
14166
  import { spawnSync } from "child_process";
13780
14167
  import { dirname as dirname12 } from "path";
13781
14168
  import { fileURLToPath as fileURLToPath6 } from "url";
13782
- import { Command as Command26 } from "commander";
14169
+ import { Command as Command27 } from "commander";
13783
14170
 
13784
14171
  // src/lib/packaged-scripts.ts
13785
- import { existsSync as existsSync45 } from "fs";
13786
- 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";
13787
14174
  function findPackagedScript(startDir, relativePath) {
13788
14175
  let dir = startDir;
13789
14176
  for (; ; ) {
13790
- const candidate = join55(dir, relativePath);
13791
- if (existsSync45(candidate)) return candidate;
14177
+ const candidate = join57(dir, relativePath);
14178
+ if (existsSync47(candidate)) return candidate;
13792
14179
  const parent = dirname11(dir);
13793
14180
  if (parent === dir) return null;
13794
14181
  dir = parent;
@@ -13808,7 +14195,7 @@ function runPackagedScript(script, args, cwd) {
13808
14195
  return result.status === null ? 2 : result.status;
13809
14196
  }
13810
14197
  function packagedScriptCommand(spec) {
13811
- 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);
13812
14199
  if (spec.argument) command.argument(`<${spec.argument.name}>`, spec.argument.description);
13813
14200
  return command.action(() => {
13814
14201
  const here = dirname12(fileURLToPath6(import.meta.url));
@@ -13897,7 +14284,7 @@ var runnerDropForensicsCommand = packagedScriptCommand({
13897
14284
  });
13898
14285
 
13899
14286
  // src/index.ts
13900
- var program = new Command27();
14287
+ var program = new Command28();
13901
14288
  function cliVersion() {
13902
14289
  try {
13903
14290
  return getLatestCoreVersion();