@biffo/cli 0.295.2 → 0.296.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 +161 -110
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -11678,8 +11678,8 @@ async function runOwnershipCheck(argv) {
11678
11678
  const { stdout } = await execa14("git", ["diff", "--cached", "--name-status"], { cwd: root });
11679
11679
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
11680
11680
  if (messageFile) {
11681
- const { readFileSync: readFileSync45, existsSync: existsSync53 } = await import("fs");
11682
- if (existsSync53(messageFile)) commitMessage = readFileSync45(messageFile, "utf8");
11681
+ const { readFileSync: readFileSync45, existsSync: existsSync54 } = await import("fs");
11682
+ if (existsSync54(messageFile)) commitMessage = readFileSync45(messageFile, "utf8");
11683
11683
  }
11684
11684
  } else {
11685
11685
  const base = process.env["GITHUB_BASE_REF"] ?? args[0];
@@ -11993,16 +11993,56 @@ async function runEventBridgeLogPermissionCheck() {
11993
11993
  console.log(`\u2713 EventBridge log permission guard: ${report.summary}`);
11994
11994
  }
11995
11995
 
11996
- // src/scripts/check-lambda-output.ts
11996
+ // src/scripts/check-instance-adoption.ts
11997
+ import { existsSync as existsSync43 } from "fs";
11998
+ import { join as join48 } from "path";
11997
11999
  import { execa as execa16 } from "execa";
12000
+ async function runInstanceAdoptionCheck(opts = {}) {
12001
+ if (!opts.instanceDir) {
12002
+ console.error(
12003
+ "\u2717 instance-adoption guard: --instance-dir is required \u2014 the real instance tree to check adoption against. There is no self-check default: this repo IS the template, it has no instance tree of its own to examine, and a fabricated one would prove nothing about a real gap."
12004
+ );
12005
+ process.exit(2);
12006
+ }
12007
+ if (!existsSync43(opts.instanceDir)) {
12008
+ console.error(
12009
+ `\u2717 instance-adoption guard: --instance-dir ${opts.instanceDir} does not exist \u2014 cannot tell whether it is adopted, and that is not the same as a clean pass.`
12010
+ );
12011
+ process.exit(2);
12012
+ }
12013
+ const root = (await execa16("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12014
+ const theirsDir = opts.theirsDir ?? root;
12015
+ const instanceLabel = opts.instance ?? join48(opts.instanceDir).split("/").filter(Boolean).pop();
12016
+ const report = checkInstanceAdoption(theirsDir, opts.instanceDir);
12017
+ console.log(
12018
+ `examined ${report.examinedInstances} instance (${instanceLabel}) against ${report.registeredPairs} registered pair(s), ${report.applicablePairs} applicable to this instance, against template tree ${theirsDir}`
12019
+ );
12020
+ const unadopted = report.findings.filter((f) => f.status === "unadopted");
12021
+ if (unadopted.length === 0) {
12022
+ console.log(`\u2713 instance-adoption guard (${instanceLabel}): no adoption gaps`);
12023
+ return;
12024
+ }
12025
+ console.error(
12026
+ `\u2717 instance-adoption guard (${instanceLabel}): ${unadopted.length} adoption gap(s) \u2014 shipped but not consumed (#1538/#1570):`
12027
+ );
12028
+ for (const f of unadopted) {
12029
+ console.error(` ${f.pair.id} \u2014 ${f.pair.userFile} does not consume ${f.pair.templateFile}`);
12030
+ console.error(` ${f.pair.remedy}`);
12031
+ }
12032
+ console.error("\nSee biffo-template#1538/#1570/#1609.");
12033
+ process.exit(1);
12034
+ }
12035
+
12036
+ // src/scripts/check-lambda-output.ts
12037
+ import { execa as execa17 } from "execa";
11998
12038
 
11999
12039
  // src/lib/lambda-output-guard.ts
12000
12040
  import { readFileSync as readFileSync36 } from "fs";
12001
- import { join as join49 } from "path";
12041
+ import { join as join50 } from "path";
12002
12042
 
12003
12043
  // src/lib/terraform-input-guard.ts
12004
- import { existsSync as existsSync43, readdirSync as readdirSync21, readFileSync as readFileSync35, statSync as statSync13 } from "fs";
12005
- import { join as join48 } from "path";
12044
+ import { existsSync as existsSync44, readdirSync as readdirSync21, readFileSync as readFileSync35, statSync as statSync13 } from "fs";
12045
+ import { join as join49 } from "path";
12006
12046
  var GUARDED_SUBCOMMANDS = [
12007
12047
  "init",
12008
12048
  "plan",
@@ -12016,7 +12056,7 @@ function stripComments2(source) {
12016
12056
  return source.split("\n").map((line) => line.replace(/(^|\s)#.*$/, "$1")).join("\n");
12017
12057
  }
12018
12058
  function vendoredPluginServiceDirs(repoRoot) {
12019
- const servicesDir = join48(repoRoot, "services");
12059
+ const servicesDir = join49(repoRoot, "services");
12020
12060
  const result = /* @__PURE__ */ new Set();
12021
12061
  let entries;
12022
12062
  try {
@@ -12025,9 +12065,9 @@ function vendoredPluginServiceDirs(repoRoot) {
12025
12065
  return result;
12026
12066
  }
12027
12067
  for (const entry of entries) {
12028
- const full = join48(servicesDir, entry);
12029
- if (!existsSync43(full) || !statSync13(full).isDirectory()) continue;
12030
- if (existsSync43(join48(full, "biffo.plugin.json"))) {
12068
+ const full = join49(servicesDir, entry);
12069
+ if (!existsSync44(full) || !statSync13(full).isDirectory()) continue;
12070
+ if (existsSync44(join49(full, "biffo.plugin.json"))) {
12031
12071
  result.add(entry);
12032
12072
  }
12033
12073
  }
@@ -12048,7 +12088,7 @@ function findWorkflowFiles(repoRoot) {
12048
12088
  if (entry === ".github" && relative11.startsWith("services/") && vendoredPluginDirs.has(relative11.slice("services/".length))) {
12049
12089
  continue;
12050
12090
  }
12051
- const full = join48(dir, entry);
12091
+ const full = join49(dir, entry);
12052
12092
  const rel = relative11 ? `${relative11}/${entry}` : entry;
12053
12093
  if (statSync13(full).isDirectory()) {
12054
12094
  walk2(full, rel);
@@ -12092,7 +12132,7 @@ function checkWorkflowSource(file, rawSource) {
12092
12132
  }
12093
12133
  function checkTerraformInput(repoRoot) {
12094
12134
  return findWorkflowFiles(repoRoot).flatMap(
12095
- (file) => checkWorkflowSource(file, readFileSync35(join48(repoRoot, file), "utf8"))
12135
+ (file) => checkWorkflowSource(file, readFileSync35(join49(repoRoot, file), "utf8"))
12096
12136
  );
12097
12137
  }
12098
12138
 
@@ -12150,13 +12190,13 @@ function checkWorkflowSource2(file, rawSource) {
12150
12190
  }
12151
12191
  function checkLambdaOutput(repoRoot) {
12152
12192
  return findWorkflowFiles(repoRoot).flatMap(
12153
- (file) => checkWorkflowSource2(file, readFileSync36(join49(repoRoot, file), "utf8"))
12193
+ (file) => checkWorkflowSource2(file, readFileSync36(join50(repoRoot, file), "utf8"))
12154
12194
  );
12155
12195
  }
12156
12196
 
12157
12197
  // src/scripts/check-lambda-output.ts
12158
12198
  async function runLambdaOutputCheck() {
12159
- const root = (await execa16("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12199
+ const root = (await execa17("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12160
12200
  const files = findWorkflowFiles(root);
12161
12201
  console.log(`audited ${files.length} workflow file(s) under ${root}`);
12162
12202
  if (files.length === 0) {
@@ -12178,9 +12218,9 @@ async function runLambdaOutputCheck() {
12178
12218
  }
12179
12219
 
12180
12220
  // src/scripts/check-migration-body-change.ts
12181
- import { execa as execa17 } from "execa";
12182
- import { existsSync as existsSync44 } from "fs";
12183
- import { join as join50 } from "path";
12221
+ import { execa as execa18 } from "execa";
12222
+ import { existsSync as existsSync45 } from "fs";
12223
+ import { join as join51 } from "path";
12184
12224
 
12185
12225
  // src/lib/migration-body-change-guard.ts
12186
12226
  function checkMigrationBodyChangeMarkers(diffs) {
@@ -12235,18 +12275,18 @@ var GREEN = "\x1B[32m";
12235
12275
  var YELLOW2 = "\x1B[33m";
12236
12276
  var OFF2 = "\x1B[0m";
12237
12277
  async function showAt(ref, path, cwd) {
12238
- const result = await execa17("git", ["show", `${ref}:${path}`], { cwd, reject: false });
12278
+ const result = await execa18("git", ["show", `${ref}:${path}`], { cwd, reject: false });
12239
12279
  return result.exitCode === 0 ? result.stdout : null;
12240
12280
  }
12241
12281
  async function runMigrationBodyChangeCheck(argv) {
12242
- const root = (await execa17("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12282
+ const root = (await execa18("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12243
12283
  if (isInstanceRepo(root)) {
12244
12284
  console.log(
12245
12285
  `\u2713 migration body-change guard: skipped \u2014 this is an instance (${INSTANCE_CORE_FILE} present). Its migrations/versions/ is a carried, user-owned copy, not the template source this guard protects.`
12246
12286
  );
12247
12287
  return;
12248
12288
  }
12249
- if (!existsSync44(join50(root, MIGRATIONS_VERSIONS_DIR))) {
12289
+ if (!existsSync45(join51(root, MIGRATIONS_VERSIONS_DIR))) {
12250
12290
  console.log(`\u2713 migration body-change guard: no ${MIGRATIONS_VERSIONS_DIR} in this repo.`);
12251
12291
  return;
12252
12292
  }
@@ -12255,8 +12295,8 @@ async function runMigrationBodyChangeCheck(argv) {
12255
12295
  console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
12256
12296
  process.exit(2);
12257
12297
  }
12258
- await execa17("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
12259
- const { stdout } = await execa17(
12298
+ await execa18("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
12299
+ const { stdout } = await execa18(
12260
12300
  "git",
12261
12301
  [
12262
12302
  "diff",
@@ -12349,8 +12389,8 @@ ${BOLD2}What to do${OFF2}
12349
12389
 
12350
12390
  // src/scripts/check-pipe-trap.ts
12351
12391
  import { readFileSync as readFileSync37, readdirSync as readdirSync22 } from "fs";
12352
- import { join as join51, relative as relative9 } from "path";
12353
- import { execa as execa18 } from "execa";
12392
+ import { join as join52, relative as relative9 } from "path";
12393
+ import { execa as execa19 } from "execa";
12354
12394
 
12355
12395
  // src/lib/pipe-trap-guard.ts
12356
12396
  var STATUS_BEARING = [
@@ -12446,7 +12486,7 @@ function findPipeTraps(source) {
12446
12486
  function shellFiles(root) {
12447
12487
  const out = [];
12448
12488
  for (const dir of ["scripts", ".githooks"]) {
12449
- const full = join51(root, dir);
12489
+ const full = join52(root, dir);
12450
12490
  let entries;
12451
12491
  try {
12452
12492
  entries = readdirSync22(full, { withFileTypes: true });
@@ -12456,13 +12496,13 @@ function shellFiles(root) {
12456
12496
  for (const entry of entries) {
12457
12497
  if (!entry.isFile()) continue;
12458
12498
  if (dir === "scripts" && !entry.name.endsWith(".sh")) continue;
12459
- out.push(join51(full, entry.name));
12499
+ out.push(join52(full, entry.name));
12460
12500
  }
12461
12501
  }
12462
12502
  return out;
12463
12503
  }
12464
12504
  async function runPipeTrapCheck() {
12465
- const root = (await execa18("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12505
+ const root = (await execa19("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12466
12506
  const files = shellFiles(root);
12467
12507
  console.log(`audited ${files.length} shell file(s) under scripts/ and .githooks/ under ${root}`);
12468
12508
  if (files.length === 0) {
@@ -12489,11 +12529,11 @@ async function runPipeTrapCheck() {
12489
12529
  }
12490
12530
 
12491
12531
  // src/scripts/check-plugin-allowlist-convention.ts
12492
- import { execa as execa19 } from "execa";
12532
+ import { execa as execa20 } from "execa";
12493
12533
 
12494
12534
  // src/lib/plugin-allowlist-convention.ts
12495
12535
  import { readFileSync as readFileSync38 } from "fs";
12496
- import { join as join52 } from "path";
12536
+ import { join as join53 } from "path";
12497
12537
  var COMPUTE_MAIN_TF = "modules/cloud/aws/compute/main.tf";
12498
12538
  var PLUGIN_TEMPLATE_MAIN_TF = "modules/plugins/_template/main.tf";
12499
12539
  var ALLOWLIST_MAIN_TF = "modules/cloud/aws/plugin-allowlist/main.tf";
@@ -12504,7 +12544,7 @@ var PLUGIN = "<plugin>";
12504
12544
  var ACCOUNT = "<account>";
12505
12545
  function read(repoRoot, relative11) {
12506
12546
  try {
12507
- return readFileSync38(join52(repoRoot, relative11), "utf8");
12547
+ return readFileSync38(join53(repoRoot, relative11), "utf8");
12508
12548
  } catch {
12509
12549
  throw new Error(`plugin-allowlist drift guard: cannot read ${relative11}`);
12510
12550
  }
@@ -12600,7 +12640,7 @@ Plugins would be rejected by require_service_principal (ADR-0009). Fix the glob,
12600
12640
 
12601
12641
  // src/scripts/check-plugin-allowlist-convention.ts
12602
12642
  async function runPluginAllowlistConventionCheck() {
12603
- const root = (await execa19("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12643
+ const root = (await execa20("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12604
12644
  let violations;
12605
12645
  try {
12606
12646
  violations = checkAllowlistConvention(root);
@@ -12625,33 +12665,33 @@ async function runPluginAllowlistConventionCheck() {
12625
12665
  }
12626
12666
 
12627
12667
  // src/scripts/check-plugin-collisions.ts
12628
- import { existsSync as existsSync46 } from "fs";
12629
- import { join as join54 } from "path";
12630
- import { execa as execa20 } from "execa";
12668
+ import { existsSync as existsSync47 } from "fs";
12669
+ import { join as join55 } from "path";
12670
+ import { execa as execa21 } from "execa";
12631
12671
 
12632
12672
  // src/lib/plugin-collision-guard.ts
12633
- import { existsSync as existsSync45, readdirSync as readdirSync23, statSync as statSync14 } from "fs";
12634
- import { join as join53 } from "path";
12673
+ import { existsSync as existsSync46, readdirSync as readdirSync23, statSync as statSync14 } from "fs";
12674
+ import { join as join54 } from "path";
12635
12675
  var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
12636
12676
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
12637
12677
  function subdirectories(dir) {
12638
- if (!existsSync45(dir)) return [];
12678
+ if (!existsSync46(dir)) return [];
12639
12679
  return readdirSync23(dir).filter((entry) => {
12640
12680
  if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
12641
12681
  try {
12642
- return statSync14(join53(dir, entry)).isDirectory();
12682
+ return statSync14(join54(dir, entry)).isDirectory();
12643
12683
  } catch {
12644
12684
  return false;
12645
12685
  }
12646
12686
  });
12647
12687
  }
12648
12688
  function regularPackagesOf(pluginDir2) {
12649
- return subdirectories(pluginDir2).filter((name) => existsSync45(join53(pluginDir2, name, "__init__.py"))).sort();
12689
+ return subdirectories(pluginDir2).filter((name) => existsSync46(join54(pluginDir2, name, "__init__.py"))).sort();
12650
12690
  }
12651
12691
  function bareTestModulesOf(pluginDir2) {
12652
- const testsDir = join53(pluginDir2, "tests");
12653
- if (!existsSync45(testsDir)) return [];
12654
- if (existsSync45(join53(testsDir, "__init__.py"))) return [];
12692
+ const testsDir = join54(pluginDir2, "tests");
12693
+ if (!existsSync46(testsDir)) return [];
12694
+ if (existsSync46(join54(testsDir, "__init__.py"))) return [];
12655
12695
  return readdirSync23(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
12656
12696
  }
12657
12697
  function findCollisions(servicesDir, pluginDirs) {
@@ -12660,7 +12700,7 @@ function findCollisions(servicesDir, pluginDirs) {
12660
12700
  const gather = (kind, namesOf) => {
12661
12701
  const claims = /* @__PURE__ */ new Map();
12662
12702
  for (const plugin of plugins) {
12663
- for (const name of namesOf(join53(servicesDir, plugin))) {
12703
+ for (const name of namesOf(join54(servicesDir, plugin))) {
12664
12704
  claims.set(name, [...claims.get(name) ?? [], plugin]);
12665
12705
  }
12666
12706
  }
@@ -12697,9 +12737,9 @@ function formatCollisions(collisions) {
12697
12737
 
12698
12738
  // src/scripts/check-plugin-collisions.ts
12699
12739
  async function runPluginCollisionCheck() {
12700
- const root = (await execa20("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12701
- const servicesDir = join54(root, "services");
12702
- if (!existsSync46(servicesDir)) {
12740
+ const root = (await execa21("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12741
+ const servicesDir = join55(root, "services");
12742
+ if (!existsSync47(servicesDir)) {
12703
12743
  console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
12704
12744
  return;
12705
12745
  }
@@ -12716,11 +12756,11 @@ async function runPluginCollisionCheck() {
12716
12756
  }
12717
12757
 
12718
12758
  // src/scripts/check-plugin-terraform.ts
12719
- import { execa as execa21 } from "execa";
12759
+ import { execa as execa22 } from "execa";
12720
12760
 
12721
12761
  // src/lib/plugin-terraform-guard.ts
12722
- import { existsSync as existsSync47, readFileSync as readFileSync39, readdirSync as readdirSync24 } from "fs";
12723
- import { dirname as dirname10, join as join55, relative as relative10, sep as sep4 } from "path";
12762
+ import { existsSync as existsSync48, readFileSync as readFileSync39, readdirSync as readdirSync24 } from "fs";
12763
+ import { dirname as dirname10, join as join56, relative as relative10, sep as sep4 } from "path";
12724
12764
  var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
12725
12765
  var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
12726
12766
  function findPluginManifests(root) {
@@ -12735,9 +12775,9 @@ function findPluginManifests(root) {
12735
12775
  for (const entry of entries) {
12736
12776
  if (entry.isDirectory()) {
12737
12777
  if (SKIP_DIRS3.has(entry.name)) continue;
12738
- walk2(join55(dir, entry.name));
12778
+ walk2(join56(dir, entry.name));
12739
12779
  } else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
12740
- found.push(relative10(root, join55(dir, entry.name)).split(sep4).join("/"));
12780
+ found.push(relative10(root, join56(dir, entry.name)).split(sep4).join("/"));
12741
12781
  }
12742
12782
  }
12743
12783
  };
@@ -12762,14 +12802,14 @@ function readSubscriptions(absManifestPath) {
12762
12802
  }
12763
12803
  function checkPluginTerraform(root) {
12764
12804
  const violations = [];
12765
- const coreManifest = existsSync47(join55(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
12805
+ const coreManifest = existsSync48(join56(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
12766
12806
  for (const manifest of findPluginManifests(root)) {
12767
12807
  if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
12768
- const absManifest = join55(root, manifest);
12808
+ const absManifest = join56(root, manifest);
12769
12809
  const subscriptions = readSubscriptions(absManifest);
12770
12810
  if (subscriptions === null) continue;
12771
12811
  const pluginDir2 = dirname10(absManifest);
12772
- if (existsSync47(join55(pluginDir2, "terraform"))) continue;
12812
+ if (existsSync48(join56(pluginDir2, "terraform"))) continue;
12773
12813
  const relPluginDir = relative10(root, pluginDir2).split(sep4).join("/");
12774
12814
  violations.push({
12775
12815
  manifest,
@@ -12789,7 +12829,7 @@ function formatViolations(violations) {
12789
12829
 
12790
12830
  // src/scripts/check-plugin-terraform.ts
12791
12831
  async function runPluginTerraformCheck() {
12792
- const root = (await execa21("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12832
+ const root = (await execa22("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12793
12833
  const violations = checkPluginTerraform(root);
12794
12834
  if (violations.length > 0) {
12795
12835
  console.error("\u2717 plugin Terraform guard: event subscriptions with no infrastructure\n");
@@ -12800,13 +12840,13 @@ async function runPluginTerraformCheck() {
12800
12840
  }
12801
12841
 
12802
12842
  // src/scripts/check-plugin-tool-supply.ts
12803
- import { existsSync as existsSync49 } from "fs";
12804
- import { join as join57 } from "path";
12805
- import { execa as execa22 } from "execa";
12843
+ import { existsSync as existsSync50 } from "fs";
12844
+ import { join as join58 } from "path";
12845
+ import { execa as execa23 } from "execa";
12806
12846
 
12807
12847
  // src/lib/plugin-tool-supply-audit.ts
12808
- import { existsSync as existsSync48, readFileSync as readFileSync40, readdirSync as readdirSync25, statSync as statSync15 } from "fs";
12809
- import { join as join56 } from "path";
12848
+ import { existsSync as existsSync49, readFileSync as readFileSync40, readdirSync as readdirSync25, statSync as statSync15 } from "fs";
12849
+ import { join as join57 } from "path";
12810
12850
 
12811
12851
  // src/lib/openrouter-model-snapshot.ts
12812
12852
  var OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT = "2026-08-10T06:39:01Z";
@@ -13223,7 +13263,7 @@ function listDirs(root) {
13223
13263
  }
13224
13264
  return entries.filter((e) => {
13225
13265
  try {
13226
- return statSync15(join56(root, e)).isDirectory();
13266
+ return statSync15(join57(root, e)).isDirectory();
13227
13267
  } catch {
13228
13268
  return false;
13229
13269
  }
@@ -13239,7 +13279,7 @@ function walkFiles2(root, accept, skipDir) {
13239
13279
  return;
13240
13280
  }
13241
13281
  for (const entry of entries) {
13242
- const p = join56(dir, entry);
13282
+ const p = join57(dir, entry);
13243
13283
  let st;
13244
13284
  try {
13245
13285
  st = statSync15(p);
@@ -13265,14 +13305,14 @@ function pluginPythonFiles(pluginDir2) {
13265
13305
  );
13266
13306
  }
13267
13307
  function pluginTerraformFiles(pluginDir2) {
13268
- const tfDir = join56(pluginDir2, "terraform");
13308
+ const tfDir = join57(pluginDir2, "terraform");
13269
13309
  let entries;
13270
13310
  try {
13271
13311
  entries = readdirSync25(tfDir);
13272
13312
  } catch {
13273
13313
  return [];
13274
13314
  }
13275
- return entries.filter((e) => e.endsWith(".tf")).map((e) => join56(tfDir, e)).sort();
13315
+ return entries.filter((e) => e.endsWith(".tf")).map((e) => join57(tfDir, e)).sort();
13276
13316
  }
13277
13317
  function extractManifestTools(manifestText) {
13278
13318
  let parsed;
@@ -13524,8 +13564,8 @@ function isSnapshotStale(fetchedAt, now) {
13524
13564
  function normalizeModelId(id) {
13525
13565
  return id.endsWith(":online") ? id.slice(0, -":online".length) : id;
13526
13566
  }
13527
- var CONFIG_PY_PATH = join56("services", "api", "src", "api", "config.py");
13528
- var ORCHESTRATION_SCHEMA_PATH = join56(
13567
+ var CONFIG_PY_PATH = join57("services", "api", "src", "api", "config.py");
13568
+ var ORCHESTRATION_SCHEMA_PATH = join57(
13529
13569
  "services",
13530
13570
  "api",
13531
13571
  "src",
@@ -13537,10 +13577,10 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
13537
13577
  const knownModelIds = options.knownModelIds ?? OPENROUTER_MODEL_IDS;
13538
13578
  const snapshotFetchedAt = options.snapshotFetchedAt ?? OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT;
13539
13579
  const now = options.now ?? /* @__PURE__ */ new Date();
13540
- const configPath = join56(repoRoot, CONFIG_PY_PATH);
13541
- const orchestrationPath = join56(repoRoot, ORCHESTRATION_SCHEMA_PATH);
13542
- const configMissing = !existsSync48(configPath);
13543
- const orchestrationSchemaMissing = !existsSync48(orchestrationPath);
13580
+ const configPath = join57(repoRoot, CONFIG_PY_PATH);
13581
+ const orchestrationPath = join57(repoRoot, ORCHESTRATION_SCHEMA_PATH);
13582
+ const configMissing = !existsSync49(configPath);
13583
+ const orchestrationSchemaMissing = !existsSync49(orchestrationPath);
13544
13584
  const knownSet = new Set(knownModelIds);
13545
13585
  const snapshotEmpty = knownModelIds.length === 0;
13546
13586
  const snapshotStale = isSnapshotStale(snapshotFetchedAt, now);
@@ -13611,7 +13651,7 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
13611
13651
  function discoverPluginDirs(pluginsRoot) {
13612
13652
  return listDirs(pluginsRoot).filter((name) => {
13613
13653
  try {
13614
- return statSync15(join56(pluginsRoot, name, "biffo.plugin.json")).isFile();
13654
+ return statSync15(join57(pluginsRoot, name, "biffo.plugin.json")).isFile();
13615
13655
  } catch {
13616
13656
  return false;
13617
13657
  }
@@ -13624,8 +13664,8 @@ function auditPluginToolSupply(pluginsRoot) {
13624
13664
  let terraformBlind = false;
13625
13665
  let totalDeclaredTools = 0;
13626
13666
  for (const name of pluginNames) {
13627
- const pluginDir2 = join56(pluginsRoot, name);
13628
- const manifestText = readFileSync40(join56(pluginDir2, "biffo.plugin.json"), "utf8");
13667
+ const pluginDir2 = join57(pluginsRoot, name);
13668
+ const manifestText = readFileSync40(join57(pluginDir2, "biffo.plugin.json"), "utf8");
13629
13669
  const manifest = extractManifestTools(manifestText);
13630
13670
  if (manifest.parseError) {
13631
13671
  findings.push({
@@ -13723,7 +13763,7 @@ function auditPluginToolSupply(pluginsRoot) {
13723
13763
  requiredEnvVars: envResult.envVars,
13724
13764
  missingEnvVars: anyWired ? [] : envResult.envVars,
13725
13765
  status: anyWired ? "ok" : "missing-env",
13726
- 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 ${join56(pluginDir2, "terraform")}, so this deployment can never supply it`
13766
+ 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 ${join57(pluginDir2, "terraform")}, so this deployment can never supply it`
13727
13767
  });
13728
13768
  }
13729
13769
  }
@@ -13754,10 +13794,10 @@ function auditPluginToolSupply(pluginsRoot) {
13754
13794
 
13755
13795
  // src/scripts/check-plugin-tool-supply.ts
13756
13796
  async function runPluginToolSupplyCheck() {
13757
- const root = (await execa22("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13797
+ const root = (await execa23("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13758
13798
  let allOk = true;
13759
- const pluginsRoot = join57(root, "services", "_plugins");
13760
- if (!existsSync49(pluginsRoot)) {
13799
+ const pluginsRoot = join58(root, "services", "_plugins");
13800
+ if (!existsSync50(pluginsRoot)) {
13761
13801
  console.log("\u2713 plugin tool-supply guard: no services/_plugins/ \u2014 nothing to audit");
13762
13802
  } else {
13763
13803
  const report = auditPluginToolSupply(pluginsRoot);
@@ -13787,8 +13827,8 @@ async function runPluginToolSupplyCheck() {
13787
13827
  console.log(`\u2713 plugin tool-supply guard: ${report.summary}`);
13788
13828
  }
13789
13829
  }
13790
- const servicesApiRoot = join57(root, "services", "api");
13791
- if (!existsSync49(servicesApiRoot)) {
13830
+ const servicesApiRoot = join58(root, "services", "api");
13831
+ if (!existsSync50(servicesApiRoot)) {
13792
13832
  console.log("\u2713 plugin model-id guard: no services/api/ \u2014 nothing to audit");
13793
13833
  } else {
13794
13834
  const modelReport = auditDeclaredModelIds(root);
@@ -13834,7 +13874,7 @@ async function runPluginToolSupplyCheck() {
13834
13874
  }
13835
13875
 
13836
13876
  // src/scripts/check-release-subject.ts
13837
- import { execa as execa23 } from "execa";
13877
+ import { execa as execa24 } from "execa";
13838
13878
 
13839
13879
  // src/lib/release-version.ts
13840
13880
  var MINOR_TYPES = /* @__PURE__ */ new Set(["feat"]);
@@ -13871,7 +13911,7 @@ async function fetchPrTitleViaGh({
13871
13911
  PR_NUMBER,
13872
13912
  GH_REPO
13873
13913
  }) {
13874
- const { stdout } = await execa23(
13914
+ const { stdout } = await execa24(
13875
13915
  "gh",
13876
13916
  ["pr", "view", PR_NUMBER, "--repo", GH_REPO, "--json", "title", "--jq", ".title"],
13877
13917
  { env: { ...process.env, GH_TOKEN } }
@@ -13907,7 +13947,7 @@ async function resolveReleaseSubject({
13907
13947
  );
13908
13948
  }
13909
13949
  }
13910
- return (await execa23("git", ["log", "-1", "--format=%s"], { cwd })).stdout.trim();
13950
+ return (await execa24("git", ["log", "-1", "--format=%s"], { cwd })).stdout.trim();
13911
13951
  }
13912
13952
  async function runReleaseSubjectCheck(argv) {
13913
13953
  const base = process.env["GITHUB_BASE_REF"] ?? argv[0];
@@ -13915,9 +13955,9 @@ async function runReleaseSubjectCheck(argv) {
13915
13955
  console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
13916
13956
  process.exit(2);
13917
13957
  }
13918
- const root = (await execa23("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13919
- await execa23("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
13920
- const { stdout } = await execa23("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
13958
+ const root = (await execa24("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13959
+ await execa24("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
13960
+ const { stdout } = await execa24("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
13921
13961
  cwd: root
13922
13962
  });
13923
13963
  const changedFiles = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
@@ -14168,13 +14208,13 @@ async function runSharedFileReductionCheck(args) {
14168
14208
  }
14169
14209
 
14170
14210
  // src/scripts/check-skeleton-drift.ts
14171
- import { existsSync as existsSync50, readdirSync as readdirSync27 } from "fs";
14172
- import { join as join59 } from "path";
14173
- import { execa as execa24 } from "execa";
14211
+ import { existsSync as existsSync51, readdirSync as readdirSync27 } from "fs";
14212
+ import { join as join60 } from "path";
14213
+ import { execa as execa25 } from "execa";
14174
14214
 
14175
14215
  // src/lib/skeleton-drift-guard.ts
14176
14216
  import { readFileSync as readFileSync43, readdirSync as readdirSync26, statSync as statSync16 } from "fs";
14177
- import { join as join58 } from "path";
14217
+ import { join as join59 } from "path";
14178
14218
  var isWorkflow = (rel) => rel.startsWith(".github/workflows/") && (rel.endsWith(".yml") || rel.endsWith(".yaml"));
14179
14219
  var isRootLayout = (rel) => rel.endsWith("src/app/layout.tsx");
14180
14220
  var uncommented = (contents) => contents.split("\n").filter((line) => !/^\s*(\/\/|\/\*|\*)/.test(line)).join("\n");
@@ -14238,7 +14278,7 @@ function walk(dir, base = dir) {
14238
14278
  }
14239
14279
  for (const entry of entries) {
14240
14280
  if (entry === ".venv" || entry === "node_modules" || entry === ".git") continue;
14241
- const abs = join58(dir, entry);
14281
+ const abs = join59(dir, entry);
14242
14282
  let isDir;
14243
14283
  try {
14244
14284
  isDir = statSync16(abs).isDirectory();
@@ -14260,7 +14300,7 @@ function auditSkeleton(skeletonRoot, name, rules = SKELETON_RULES) {
14260
14300
  if (!rule.appliesTo(rel)) continue;
14261
14301
  let contents;
14262
14302
  try {
14263
- contents = readFileSync43(join58(skeletonRoot, rel), "utf8");
14303
+ contents = readFileSync43(join59(skeletonRoot, rel), "utf8");
14264
14304
  } catch {
14265
14305
  continue;
14266
14306
  }
@@ -14289,23 +14329,23 @@ function formatViolations2(violations) {
14289
14329
 
14290
14330
  // src/scripts/check-skeleton-drift.ts
14291
14331
  function discoverSkeletons(root) {
14292
- const skeletonsDir = join59(root, "_skeletons");
14332
+ const skeletonsDir = join60(root, "_skeletons");
14293
14333
  let entries;
14294
14334
  try {
14295
14335
  entries = readdirSync27(skeletonsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
14296
14336
  } catch {
14297
14337
  return [];
14298
14338
  }
14299
- return entries.filter((name) => existsSync50(join59(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
14339
+ return entries.filter((name) => existsSync51(join60(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
14300
14340
  }
14301
14341
  async function runSkeletonDriftCheck() {
14302
- const root = (await execa24("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
14342
+ const root = (await execa25("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
14303
14343
  const skeletons = discoverSkeletons(root);
14304
14344
  let filesConsidered = 0;
14305
14345
  for (const name of skeletons) {
14306
- const skeletonRoot = join59(root, "_skeletons", name);
14346
+ const skeletonRoot = join60(root, "_skeletons", name);
14307
14347
  filesConsidered += findWorkflowFiles(skeletonRoot).length;
14308
- if (existsSync50(join59(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
14348
+ if (existsSync51(join60(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
14309
14349
  filesConsidered += 1;
14310
14350
  }
14311
14351
  }
@@ -14319,7 +14359,7 @@ async function runSkeletonDriftCheck() {
14319
14359
  process.exit(1);
14320
14360
  }
14321
14361
  const violations = skeletons.flatMap(
14322
- (name) => auditSkeleton(join59(root, "_skeletons", name), name)
14362
+ (name) => auditSkeleton(join60(root, "_skeletons", name), name)
14323
14363
  );
14324
14364
  if (violations.length > 0) {
14325
14365
  console.error("\u2717 Skeleton-drift guard: drift found between this repo and its scaffolding\n");
@@ -14331,9 +14371,9 @@ async function runSkeletonDriftCheck() {
14331
14371
  }
14332
14372
 
14333
14373
  // src/scripts/check-terraform-input.ts
14334
- import { execa as execa25 } from "execa";
14374
+ import { execa as execa26 } from "execa";
14335
14375
  async function runTerraformInputCheck() {
14336
- const root = (await execa25("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
14376
+ const root = (await execa26("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
14337
14377
  const files = findWorkflowFiles(root);
14338
14378
  console.log(`audited ${files.length} workflow file(s) under ${root}`);
14339
14379
  if (files.length === 0) {
@@ -14356,7 +14396,7 @@ async function runTerraformInputCheck() {
14356
14396
 
14357
14397
  // src/commands/check.ts
14358
14398
  var checkCommand = new Command24("check").description(
14359
- "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, migration-body-change) run in CI and git hooks, plus out-of-band audits (branch protection, plugin-staleness)"
14399
+ "Repo guards (ownership, release subject, plugin terraform, plugin collisions, eventbridge-log-permissions, plugin-tool-supply, core-direct-paths, instance-adoption, cognito-invite-template, lambda-output, pipe-trap, codeql-suppression, skeleton-drift, terraform-input, plugin-allowlist-convention, migration-body-change) run in CI and git hooks, plus out-of-band audits (branch protection, plugin-staleness)"
14360
14400
  );
14361
14401
  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 () => {
14362
14402
  await runOwnershipCheck(rawArgsAfter("ownership"));
@@ -14408,6 +14448,17 @@ checkCommand.command("core-direct-paths").description(
14408
14448
  await runCoreDirectPathsCheck(opts);
14409
14449
  }
14410
14450
  );
14451
+ checkCommand.command("instance-adoption").description(
14452
+ "Refuse a real instance tree that has not consumed a registered adoption channel its target template ships (#1538/#1570/#1609) \u2014 checkInstanceAdoption previously ran only inside `biffo core upgrade`, so a gap in an instance nobody happened to be upgrading went undetected for days (keiranholloway/biffo-platform, PR #174). --instance-dir is REQUIRED and has no self-check default: this repo is the template, not an instance."
14453
+ ).option("--instance <name>", "Label for the report (defaults to the basename of --instance-dir)").option(
14454
+ "--instance-dir <dir>",
14455
+ "REQUIRED: the real instance tree to check adoption against (oursDir) \u2014 no self-check default exists (exits 2, cannot-tell, when omitted)"
14456
+ ).option(
14457
+ "--theirs-dir <dir>",
14458
+ "Template tree that ships the channel (theirsDir); defaults to this repo root"
14459
+ ).action(async (opts) => {
14460
+ await runInstanceAdoptionCheck(opts);
14461
+ });
14411
14462
  checkCommand.command("cognito-invite-template").description(
14412
14463
  "Refuse a Cognito invite_message_template missing a required member or placeholder (#356) \u2014 terraform validate is silent on this, so a fresh deploy fails on its first apply"
14413
14464
  ).action(async () => {
@@ -14474,8 +14525,8 @@ function rawArgsAfter(subcommand) {
14474
14525
  }
14475
14526
 
14476
14527
  // src/commands/doctor.ts
14477
- import { existsSync as existsSync51, readFileSync as readFileSync44 } from "fs";
14478
- import { join as join60, resolve as resolve20 } from "path";
14528
+ import { existsSync as existsSync52, readFileSync as readFileSync44 } from "fs";
14529
+ import { join as join61, resolve as resolve20 } from "path";
14479
14530
  import chalk21 from "chalk";
14480
14531
  import { Command as Command25 } from "commander";
14481
14532
 
@@ -14654,8 +14705,8 @@ async function runDoctor(options, deps = { git: new GitAdapter() }) {
14654
14705
  return runDoctorChecks(facts);
14655
14706
  }
14656
14707
  function readLocalCoreVersion(cwd) {
14657
- const path = join60(cwd, INSTANCE_CORE_FILE);
14658
- if (!existsSync51(path)) return null;
14708
+ const path = join61(cwd, INSTANCE_CORE_FILE);
14709
+ if (!existsSync52(path)) return null;
14659
14710
  try {
14660
14711
  return extractVersionField(readFileSync44(path, "utf8"));
14661
14712
  } catch {
@@ -14677,8 +14728,8 @@ function extractVersionField(contents) {
14677
14728
  return match?.[1] ?? null;
14678
14729
  }
14679
14730
  function readFossil(cwd) {
14680
- const path = join60(cwd, CORE_VERSION_FILE);
14681
- if (!existsSync51(path)) return null;
14731
+ const path = join61(cwd, CORE_VERSION_FILE);
14732
+ if (!existsSync52(path)) return null;
14682
14733
  try {
14683
14734
  const value = readFileSync44(path, "utf8").trim();
14684
14735
  return value === "" ? null : value;
@@ -15129,13 +15180,13 @@ import { fileURLToPath as fileURLToPath6 } from "url";
15129
15180
  import { Command as Command27 } from "commander";
15130
15181
 
15131
15182
  // src/lib/packaged-scripts.ts
15132
- import { existsSync as existsSync52 } from "fs";
15133
- import { dirname as dirname11, join as join61 } from "path";
15183
+ import { existsSync as existsSync53 } from "fs";
15184
+ import { dirname as dirname11, join as join62 } from "path";
15134
15185
  function findPackagedScript(startDir, relativePath) {
15135
15186
  let dir = startDir;
15136
15187
  for (; ; ) {
15137
- const candidate = join61(dir, relativePath);
15138
- if (existsSync52(candidate)) return candidate;
15188
+ const candidate = join62(dir, relativePath);
15189
+ if (existsSync53(candidate)) return candidate;
15139
15190
  const parent = dirname11(dir);
15140
15191
  if (parent === dir) return null;
15141
15192
  dir = parent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.295.2",
3
+ "version": "0.296.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",