@biffo/cli 0.295.2 → 0.296.1

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 +183 -110
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -8647,8 +8647,30 @@ function printEntry(entry) {
8647
8647
  const summary = entry.ui_components.map((c) => `${c.label} (${c.type})`).join(", ");
8648
8648
  console.log(` UI components: ${summary}`);
8649
8649
  }
8650
+ printScopeSeamEntitlement(entry.name);
8650
8651
  console.log("");
8651
8652
  }
8653
+ function printScopeSeamEntitlement(name) {
8654
+ console.log("");
8655
+ console.log(chalk14.bold(" Scope-seam entitlement"));
8656
+ console.log(" Declared by the INSTANCE, never by this plugin (ADR-0029, #1653).");
8657
+ console.log(" A plugin manifest cannot grant it \u2014 biffo.plugin.json is not consulted.");
8658
+ console.log(" To let this plugin call GET /internal/scopes or POST /internal/scope-check,");
8659
+ console.log(" the instance operator adds it beside their own scope authorizer:");
8660
+ console.log("");
8661
+ console.log(chalk14.dim(" register_scope_authorizer("));
8662
+ console.log(chalk14.dim(" authorizer,"));
8663
+ console.log(
8664
+ chalk14.dim(` entitlements={"system:${name}": frozenset({"<permission_code>"})},`)
8665
+ );
8666
+ console.log(chalk14.dim(" )"));
8667
+ console.log("");
8668
+ console.log(
8669
+ chalk14.dim(
8670
+ " <permission_code> is one of the INSTANCE's own codes, so ask the operator which\n one covers this plugin's surface. This command cannot report what an instance\n has actually entitled: the map is evaluated in the instance's API at import\n time, and re-deriving it here would be a second authority that can drift from\n the one enforcing the 403."
8671
+ )
8672
+ );
8673
+ }
8652
8674
 
8653
8675
  // src/commands/plugin-install.ts
8654
8676
  import { cpSync as cpSync5, existsSync as existsSync31, mkdirSync as mkdirSync11, readFileSync as readFileSync23, statSync as statSync7 } from "fs";
@@ -11678,8 +11700,8 @@ async function runOwnershipCheck(argv) {
11678
11700
  const { stdout } = await execa14("git", ["diff", "--cached", "--name-status"], { cwd: root });
11679
11701
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
11680
11702
  if (messageFile) {
11681
- const { readFileSync: readFileSync45, existsSync: existsSync53 } = await import("fs");
11682
- if (existsSync53(messageFile)) commitMessage = readFileSync45(messageFile, "utf8");
11703
+ const { readFileSync: readFileSync45, existsSync: existsSync54 } = await import("fs");
11704
+ if (existsSync54(messageFile)) commitMessage = readFileSync45(messageFile, "utf8");
11683
11705
  }
11684
11706
  } else {
11685
11707
  const base = process.env["GITHUB_BASE_REF"] ?? args[0];
@@ -11993,16 +12015,56 @@ async function runEventBridgeLogPermissionCheck() {
11993
12015
  console.log(`\u2713 EventBridge log permission guard: ${report.summary}`);
11994
12016
  }
11995
12017
 
11996
- // src/scripts/check-lambda-output.ts
12018
+ // src/scripts/check-instance-adoption.ts
12019
+ import { existsSync as existsSync43 } from "fs";
12020
+ import { join as join48 } from "path";
11997
12021
  import { execa as execa16 } from "execa";
12022
+ async function runInstanceAdoptionCheck(opts = {}) {
12023
+ if (!opts.instanceDir) {
12024
+ console.error(
12025
+ "\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."
12026
+ );
12027
+ process.exit(2);
12028
+ }
12029
+ if (!existsSync43(opts.instanceDir)) {
12030
+ console.error(
12031
+ `\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.`
12032
+ );
12033
+ process.exit(2);
12034
+ }
12035
+ const root = (await execa16("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12036
+ const theirsDir = opts.theirsDir ?? root;
12037
+ const instanceLabel = opts.instance ?? join48(opts.instanceDir).split("/").filter(Boolean).pop();
12038
+ const report = checkInstanceAdoption(theirsDir, opts.instanceDir);
12039
+ console.log(
12040
+ `examined ${report.examinedInstances} instance (${instanceLabel}) against ${report.registeredPairs} registered pair(s), ${report.applicablePairs} applicable to this instance, against template tree ${theirsDir}`
12041
+ );
12042
+ const unadopted = report.findings.filter((f) => f.status === "unadopted");
12043
+ if (unadopted.length === 0) {
12044
+ console.log(`\u2713 instance-adoption guard (${instanceLabel}): no adoption gaps`);
12045
+ return;
12046
+ }
12047
+ console.error(
12048
+ `\u2717 instance-adoption guard (${instanceLabel}): ${unadopted.length} adoption gap(s) \u2014 shipped but not consumed (#1538/#1570):`
12049
+ );
12050
+ for (const f of unadopted) {
12051
+ console.error(` ${f.pair.id} \u2014 ${f.pair.userFile} does not consume ${f.pair.templateFile}`);
12052
+ console.error(` ${f.pair.remedy}`);
12053
+ }
12054
+ console.error("\nSee biffo-template#1538/#1570/#1609.");
12055
+ process.exit(1);
12056
+ }
12057
+
12058
+ // src/scripts/check-lambda-output.ts
12059
+ import { execa as execa17 } from "execa";
11998
12060
 
11999
12061
  // src/lib/lambda-output-guard.ts
12000
12062
  import { readFileSync as readFileSync36 } from "fs";
12001
- import { join as join49 } from "path";
12063
+ import { join as join50 } from "path";
12002
12064
 
12003
12065
  // 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";
12066
+ import { existsSync as existsSync44, readdirSync as readdirSync21, readFileSync as readFileSync35, statSync as statSync13 } from "fs";
12067
+ import { join as join49 } from "path";
12006
12068
  var GUARDED_SUBCOMMANDS = [
12007
12069
  "init",
12008
12070
  "plan",
@@ -12016,7 +12078,7 @@ function stripComments2(source) {
12016
12078
  return source.split("\n").map((line) => line.replace(/(^|\s)#.*$/, "$1")).join("\n");
12017
12079
  }
12018
12080
  function vendoredPluginServiceDirs(repoRoot) {
12019
- const servicesDir = join48(repoRoot, "services");
12081
+ const servicesDir = join49(repoRoot, "services");
12020
12082
  const result = /* @__PURE__ */ new Set();
12021
12083
  let entries;
12022
12084
  try {
@@ -12025,9 +12087,9 @@ function vendoredPluginServiceDirs(repoRoot) {
12025
12087
  return result;
12026
12088
  }
12027
12089
  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"))) {
12090
+ const full = join49(servicesDir, entry);
12091
+ if (!existsSync44(full) || !statSync13(full).isDirectory()) continue;
12092
+ if (existsSync44(join49(full, "biffo.plugin.json"))) {
12031
12093
  result.add(entry);
12032
12094
  }
12033
12095
  }
@@ -12048,7 +12110,7 @@ function findWorkflowFiles(repoRoot) {
12048
12110
  if (entry === ".github" && relative11.startsWith("services/") && vendoredPluginDirs.has(relative11.slice("services/".length))) {
12049
12111
  continue;
12050
12112
  }
12051
- const full = join48(dir, entry);
12113
+ const full = join49(dir, entry);
12052
12114
  const rel = relative11 ? `${relative11}/${entry}` : entry;
12053
12115
  if (statSync13(full).isDirectory()) {
12054
12116
  walk2(full, rel);
@@ -12092,7 +12154,7 @@ function checkWorkflowSource(file, rawSource) {
12092
12154
  }
12093
12155
  function checkTerraformInput(repoRoot) {
12094
12156
  return findWorkflowFiles(repoRoot).flatMap(
12095
- (file) => checkWorkflowSource(file, readFileSync35(join48(repoRoot, file), "utf8"))
12157
+ (file) => checkWorkflowSource(file, readFileSync35(join49(repoRoot, file), "utf8"))
12096
12158
  );
12097
12159
  }
12098
12160
 
@@ -12150,13 +12212,13 @@ function checkWorkflowSource2(file, rawSource) {
12150
12212
  }
12151
12213
  function checkLambdaOutput(repoRoot) {
12152
12214
  return findWorkflowFiles(repoRoot).flatMap(
12153
- (file) => checkWorkflowSource2(file, readFileSync36(join49(repoRoot, file), "utf8"))
12215
+ (file) => checkWorkflowSource2(file, readFileSync36(join50(repoRoot, file), "utf8"))
12154
12216
  );
12155
12217
  }
12156
12218
 
12157
12219
  // src/scripts/check-lambda-output.ts
12158
12220
  async function runLambdaOutputCheck() {
12159
- const root = (await execa16("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12221
+ const root = (await execa17("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12160
12222
  const files = findWorkflowFiles(root);
12161
12223
  console.log(`audited ${files.length} workflow file(s) under ${root}`);
12162
12224
  if (files.length === 0) {
@@ -12178,9 +12240,9 @@ async function runLambdaOutputCheck() {
12178
12240
  }
12179
12241
 
12180
12242
  // 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";
12243
+ import { execa as execa18 } from "execa";
12244
+ import { existsSync as existsSync45 } from "fs";
12245
+ import { join as join51 } from "path";
12184
12246
 
12185
12247
  // src/lib/migration-body-change-guard.ts
12186
12248
  function checkMigrationBodyChangeMarkers(diffs) {
@@ -12235,18 +12297,18 @@ var GREEN = "\x1B[32m";
12235
12297
  var YELLOW2 = "\x1B[33m";
12236
12298
  var OFF2 = "\x1B[0m";
12237
12299
  async function showAt(ref, path, cwd) {
12238
- const result = await execa17("git", ["show", `${ref}:${path}`], { cwd, reject: false });
12300
+ const result = await execa18("git", ["show", `${ref}:${path}`], { cwd, reject: false });
12239
12301
  return result.exitCode === 0 ? result.stdout : null;
12240
12302
  }
12241
12303
  async function runMigrationBodyChangeCheck(argv) {
12242
- const root = (await execa17("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12304
+ const root = (await execa18("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12243
12305
  if (isInstanceRepo(root)) {
12244
12306
  console.log(
12245
12307
  `\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
12308
  );
12247
12309
  return;
12248
12310
  }
12249
- if (!existsSync44(join50(root, MIGRATIONS_VERSIONS_DIR))) {
12311
+ if (!existsSync45(join51(root, MIGRATIONS_VERSIONS_DIR))) {
12250
12312
  console.log(`\u2713 migration body-change guard: no ${MIGRATIONS_VERSIONS_DIR} in this repo.`);
12251
12313
  return;
12252
12314
  }
@@ -12255,8 +12317,8 @@ async function runMigrationBodyChangeCheck(argv) {
12255
12317
  console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
12256
12318
  process.exit(2);
12257
12319
  }
12258
- await execa17("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
12259
- const { stdout } = await execa17(
12320
+ await execa18("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
12321
+ const { stdout } = await execa18(
12260
12322
  "git",
12261
12323
  [
12262
12324
  "diff",
@@ -12349,8 +12411,8 @@ ${BOLD2}What to do${OFF2}
12349
12411
 
12350
12412
  // src/scripts/check-pipe-trap.ts
12351
12413
  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";
12414
+ import { join as join52, relative as relative9 } from "path";
12415
+ import { execa as execa19 } from "execa";
12354
12416
 
12355
12417
  // src/lib/pipe-trap-guard.ts
12356
12418
  var STATUS_BEARING = [
@@ -12446,7 +12508,7 @@ function findPipeTraps(source) {
12446
12508
  function shellFiles(root) {
12447
12509
  const out = [];
12448
12510
  for (const dir of ["scripts", ".githooks"]) {
12449
- const full = join51(root, dir);
12511
+ const full = join52(root, dir);
12450
12512
  let entries;
12451
12513
  try {
12452
12514
  entries = readdirSync22(full, { withFileTypes: true });
@@ -12456,13 +12518,13 @@ function shellFiles(root) {
12456
12518
  for (const entry of entries) {
12457
12519
  if (!entry.isFile()) continue;
12458
12520
  if (dir === "scripts" && !entry.name.endsWith(".sh")) continue;
12459
- out.push(join51(full, entry.name));
12521
+ out.push(join52(full, entry.name));
12460
12522
  }
12461
12523
  }
12462
12524
  return out;
12463
12525
  }
12464
12526
  async function runPipeTrapCheck() {
12465
- const root = (await execa18("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12527
+ const root = (await execa19("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12466
12528
  const files = shellFiles(root);
12467
12529
  console.log(`audited ${files.length} shell file(s) under scripts/ and .githooks/ under ${root}`);
12468
12530
  if (files.length === 0) {
@@ -12489,11 +12551,11 @@ async function runPipeTrapCheck() {
12489
12551
  }
12490
12552
 
12491
12553
  // src/scripts/check-plugin-allowlist-convention.ts
12492
- import { execa as execa19 } from "execa";
12554
+ import { execa as execa20 } from "execa";
12493
12555
 
12494
12556
  // src/lib/plugin-allowlist-convention.ts
12495
12557
  import { readFileSync as readFileSync38 } from "fs";
12496
- import { join as join52 } from "path";
12558
+ import { join as join53 } from "path";
12497
12559
  var COMPUTE_MAIN_TF = "modules/cloud/aws/compute/main.tf";
12498
12560
  var PLUGIN_TEMPLATE_MAIN_TF = "modules/plugins/_template/main.tf";
12499
12561
  var ALLOWLIST_MAIN_TF = "modules/cloud/aws/plugin-allowlist/main.tf";
@@ -12504,7 +12566,7 @@ var PLUGIN = "<plugin>";
12504
12566
  var ACCOUNT = "<account>";
12505
12567
  function read(repoRoot, relative11) {
12506
12568
  try {
12507
- return readFileSync38(join52(repoRoot, relative11), "utf8");
12569
+ return readFileSync38(join53(repoRoot, relative11), "utf8");
12508
12570
  } catch {
12509
12571
  throw new Error(`plugin-allowlist drift guard: cannot read ${relative11}`);
12510
12572
  }
@@ -12600,7 +12662,7 @@ Plugins would be rejected by require_service_principal (ADR-0009). Fix the glob,
12600
12662
 
12601
12663
  // src/scripts/check-plugin-allowlist-convention.ts
12602
12664
  async function runPluginAllowlistConventionCheck() {
12603
- const root = (await execa19("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12665
+ const root = (await execa20("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12604
12666
  let violations;
12605
12667
  try {
12606
12668
  violations = checkAllowlistConvention(root);
@@ -12625,33 +12687,33 @@ async function runPluginAllowlistConventionCheck() {
12625
12687
  }
12626
12688
 
12627
12689
  // 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";
12690
+ import { existsSync as existsSync47 } from "fs";
12691
+ import { join as join55 } from "path";
12692
+ import { execa as execa21 } from "execa";
12631
12693
 
12632
12694
  // 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";
12695
+ import { existsSync as existsSync46, readdirSync as readdirSync23, statSync as statSync14 } from "fs";
12696
+ import { join as join54 } from "path";
12635
12697
  var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
12636
12698
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
12637
12699
  function subdirectories(dir) {
12638
- if (!existsSync45(dir)) return [];
12700
+ if (!existsSync46(dir)) return [];
12639
12701
  return readdirSync23(dir).filter((entry) => {
12640
12702
  if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
12641
12703
  try {
12642
- return statSync14(join53(dir, entry)).isDirectory();
12704
+ return statSync14(join54(dir, entry)).isDirectory();
12643
12705
  } catch {
12644
12706
  return false;
12645
12707
  }
12646
12708
  });
12647
12709
  }
12648
12710
  function regularPackagesOf(pluginDir2) {
12649
- return subdirectories(pluginDir2).filter((name) => existsSync45(join53(pluginDir2, name, "__init__.py"))).sort();
12711
+ return subdirectories(pluginDir2).filter((name) => existsSync46(join54(pluginDir2, name, "__init__.py"))).sort();
12650
12712
  }
12651
12713
  function bareTestModulesOf(pluginDir2) {
12652
- const testsDir = join53(pluginDir2, "tests");
12653
- if (!existsSync45(testsDir)) return [];
12654
- if (existsSync45(join53(testsDir, "__init__.py"))) return [];
12714
+ const testsDir = join54(pluginDir2, "tests");
12715
+ if (!existsSync46(testsDir)) return [];
12716
+ if (existsSync46(join54(testsDir, "__init__.py"))) return [];
12655
12717
  return readdirSync23(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
12656
12718
  }
12657
12719
  function findCollisions(servicesDir, pluginDirs) {
@@ -12660,7 +12722,7 @@ function findCollisions(servicesDir, pluginDirs) {
12660
12722
  const gather = (kind, namesOf) => {
12661
12723
  const claims = /* @__PURE__ */ new Map();
12662
12724
  for (const plugin of plugins) {
12663
- for (const name of namesOf(join53(servicesDir, plugin))) {
12725
+ for (const name of namesOf(join54(servicesDir, plugin))) {
12664
12726
  claims.set(name, [...claims.get(name) ?? [], plugin]);
12665
12727
  }
12666
12728
  }
@@ -12697,9 +12759,9 @@ function formatCollisions(collisions) {
12697
12759
 
12698
12760
  // src/scripts/check-plugin-collisions.ts
12699
12761
  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)) {
12762
+ const root = (await execa21("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12763
+ const servicesDir = join55(root, "services");
12764
+ if (!existsSync47(servicesDir)) {
12703
12765
  console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
12704
12766
  return;
12705
12767
  }
@@ -12716,11 +12778,11 @@ async function runPluginCollisionCheck() {
12716
12778
  }
12717
12779
 
12718
12780
  // src/scripts/check-plugin-terraform.ts
12719
- import { execa as execa21 } from "execa";
12781
+ import { execa as execa22 } from "execa";
12720
12782
 
12721
12783
  // 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";
12784
+ import { existsSync as existsSync48, readFileSync as readFileSync39, readdirSync as readdirSync24 } from "fs";
12785
+ import { dirname as dirname10, join as join56, relative as relative10, sep as sep4 } from "path";
12724
12786
  var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
12725
12787
  var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
12726
12788
  function findPluginManifests(root) {
@@ -12735,9 +12797,9 @@ function findPluginManifests(root) {
12735
12797
  for (const entry of entries) {
12736
12798
  if (entry.isDirectory()) {
12737
12799
  if (SKIP_DIRS3.has(entry.name)) continue;
12738
- walk2(join55(dir, entry.name));
12800
+ walk2(join56(dir, entry.name));
12739
12801
  } else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
12740
- found.push(relative10(root, join55(dir, entry.name)).split(sep4).join("/"));
12802
+ found.push(relative10(root, join56(dir, entry.name)).split(sep4).join("/"));
12741
12803
  }
12742
12804
  }
12743
12805
  };
@@ -12762,14 +12824,14 @@ function readSubscriptions(absManifestPath) {
12762
12824
  }
12763
12825
  function checkPluginTerraform(root) {
12764
12826
  const violations = [];
12765
- const coreManifest = existsSync47(join55(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
12827
+ const coreManifest = existsSync48(join56(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
12766
12828
  for (const manifest of findPluginManifests(root)) {
12767
12829
  if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
12768
- const absManifest = join55(root, manifest);
12830
+ const absManifest = join56(root, manifest);
12769
12831
  const subscriptions = readSubscriptions(absManifest);
12770
12832
  if (subscriptions === null) continue;
12771
12833
  const pluginDir2 = dirname10(absManifest);
12772
- if (existsSync47(join55(pluginDir2, "terraform"))) continue;
12834
+ if (existsSync48(join56(pluginDir2, "terraform"))) continue;
12773
12835
  const relPluginDir = relative10(root, pluginDir2).split(sep4).join("/");
12774
12836
  violations.push({
12775
12837
  manifest,
@@ -12789,7 +12851,7 @@ function formatViolations(violations) {
12789
12851
 
12790
12852
  // src/scripts/check-plugin-terraform.ts
12791
12853
  async function runPluginTerraformCheck() {
12792
- const root = (await execa21("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12854
+ const root = (await execa22("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
12793
12855
  const violations = checkPluginTerraform(root);
12794
12856
  if (violations.length > 0) {
12795
12857
  console.error("\u2717 plugin Terraform guard: event subscriptions with no infrastructure\n");
@@ -12800,13 +12862,13 @@ async function runPluginTerraformCheck() {
12800
12862
  }
12801
12863
 
12802
12864
  // 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";
12865
+ import { existsSync as existsSync50 } from "fs";
12866
+ import { join as join58 } from "path";
12867
+ import { execa as execa23 } from "execa";
12806
12868
 
12807
12869
  // 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";
12870
+ import { existsSync as existsSync49, readFileSync as readFileSync40, readdirSync as readdirSync25, statSync as statSync15 } from "fs";
12871
+ import { join as join57 } from "path";
12810
12872
 
12811
12873
  // src/lib/openrouter-model-snapshot.ts
12812
12874
  var OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT = "2026-08-10T06:39:01Z";
@@ -13223,7 +13285,7 @@ function listDirs(root) {
13223
13285
  }
13224
13286
  return entries.filter((e) => {
13225
13287
  try {
13226
- return statSync15(join56(root, e)).isDirectory();
13288
+ return statSync15(join57(root, e)).isDirectory();
13227
13289
  } catch {
13228
13290
  return false;
13229
13291
  }
@@ -13239,7 +13301,7 @@ function walkFiles2(root, accept, skipDir) {
13239
13301
  return;
13240
13302
  }
13241
13303
  for (const entry of entries) {
13242
- const p = join56(dir, entry);
13304
+ const p = join57(dir, entry);
13243
13305
  let st;
13244
13306
  try {
13245
13307
  st = statSync15(p);
@@ -13265,14 +13327,14 @@ function pluginPythonFiles(pluginDir2) {
13265
13327
  );
13266
13328
  }
13267
13329
  function pluginTerraformFiles(pluginDir2) {
13268
- const tfDir = join56(pluginDir2, "terraform");
13330
+ const tfDir = join57(pluginDir2, "terraform");
13269
13331
  let entries;
13270
13332
  try {
13271
13333
  entries = readdirSync25(tfDir);
13272
13334
  } catch {
13273
13335
  return [];
13274
13336
  }
13275
- return entries.filter((e) => e.endsWith(".tf")).map((e) => join56(tfDir, e)).sort();
13337
+ return entries.filter((e) => e.endsWith(".tf")).map((e) => join57(tfDir, e)).sort();
13276
13338
  }
13277
13339
  function extractManifestTools(manifestText) {
13278
13340
  let parsed;
@@ -13524,8 +13586,8 @@ function isSnapshotStale(fetchedAt, now) {
13524
13586
  function normalizeModelId(id) {
13525
13587
  return id.endsWith(":online") ? id.slice(0, -":online".length) : id;
13526
13588
  }
13527
- var CONFIG_PY_PATH = join56("services", "api", "src", "api", "config.py");
13528
- var ORCHESTRATION_SCHEMA_PATH = join56(
13589
+ var CONFIG_PY_PATH = join57("services", "api", "src", "api", "config.py");
13590
+ var ORCHESTRATION_SCHEMA_PATH = join57(
13529
13591
  "services",
13530
13592
  "api",
13531
13593
  "src",
@@ -13537,10 +13599,10 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
13537
13599
  const knownModelIds = options.knownModelIds ?? OPENROUTER_MODEL_IDS;
13538
13600
  const snapshotFetchedAt = options.snapshotFetchedAt ?? OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT;
13539
13601
  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);
13602
+ const configPath = join57(repoRoot, CONFIG_PY_PATH);
13603
+ const orchestrationPath = join57(repoRoot, ORCHESTRATION_SCHEMA_PATH);
13604
+ const configMissing = !existsSync49(configPath);
13605
+ const orchestrationSchemaMissing = !existsSync49(orchestrationPath);
13544
13606
  const knownSet = new Set(knownModelIds);
13545
13607
  const snapshotEmpty = knownModelIds.length === 0;
13546
13608
  const snapshotStale = isSnapshotStale(snapshotFetchedAt, now);
@@ -13611,7 +13673,7 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
13611
13673
  function discoverPluginDirs(pluginsRoot) {
13612
13674
  return listDirs(pluginsRoot).filter((name) => {
13613
13675
  try {
13614
- return statSync15(join56(pluginsRoot, name, "biffo.plugin.json")).isFile();
13676
+ return statSync15(join57(pluginsRoot, name, "biffo.plugin.json")).isFile();
13615
13677
  } catch {
13616
13678
  return false;
13617
13679
  }
@@ -13624,8 +13686,8 @@ function auditPluginToolSupply(pluginsRoot) {
13624
13686
  let terraformBlind = false;
13625
13687
  let totalDeclaredTools = 0;
13626
13688
  for (const name of pluginNames) {
13627
- const pluginDir2 = join56(pluginsRoot, name);
13628
- const manifestText = readFileSync40(join56(pluginDir2, "biffo.plugin.json"), "utf8");
13689
+ const pluginDir2 = join57(pluginsRoot, name);
13690
+ const manifestText = readFileSync40(join57(pluginDir2, "biffo.plugin.json"), "utf8");
13629
13691
  const manifest = extractManifestTools(manifestText);
13630
13692
  if (manifest.parseError) {
13631
13693
  findings.push({
@@ -13723,7 +13785,7 @@ function auditPluginToolSupply(pluginsRoot) {
13723
13785
  requiredEnvVars: envResult.envVars,
13724
13786
  missingEnvVars: anyWired ? [] : envResult.envVars,
13725
13787
  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`
13788
+ 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
13789
  });
13728
13790
  }
13729
13791
  }
@@ -13754,10 +13816,10 @@ function auditPluginToolSupply(pluginsRoot) {
13754
13816
 
13755
13817
  // src/scripts/check-plugin-tool-supply.ts
13756
13818
  async function runPluginToolSupplyCheck() {
13757
- const root = (await execa22("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13819
+ const root = (await execa23("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13758
13820
  let allOk = true;
13759
- const pluginsRoot = join57(root, "services", "_plugins");
13760
- if (!existsSync49(pluginsRoot)) {
13821
+ const pluginsRoot = join58(root, "services", "_plugins");
13822
+ if (!existsSync50(pluginsRoot)) {
13761
13823
  console.log("\u2713 plugin tool-supply guard: no services/_plugins/ \u2014 nothing to audit");
13762
13824
  } else {
13763
13825
  const report = auditPluginToolSupply(pluginsRoot);
@@ -13787,8 +13849,8 @@ async function runPluginToolSupplyCheck() {
13787
13849
  console.log(`\u2713 plugin tool-supply guard: ${report.summary}`);
13788
13850
  }
13789
13851
  }
13790
- const servicesApiRoot = join57(root, "services", "api");
13791
- if (!existsSync49(servicesApiRoot)) {
13852
+ const servicesApiRoot = join58(root, "services", "api");
13853
+ if (!existsSync50(servicesApiRoot)) {
13792
13854
  console.log("\u2713 plugin model-id guard: no services/api/ \u2014 nothing to audit");
13793
13855
  } else {
13794
13856
  const modelReport = auditDeclaredModelIds(root);
@@ -13834,7 +13896,7 @@ async function runPluginToolSupplyCheck() {
13834
13896
  }
13835
13897
 
13836
13898
  // src/scripts/check-release-subject.ts
13837
- import { execa as execa23 } from "execa";
13899
+ import { execa as execa24 } from "execa";
13838
13900
 
13839
13901
  // src/lib/release-version.ts
13840
13902
  var MINOR_TYPES = /* @__PURE__ */ new Set(["feat"]);
@@ -13871,7 +13933,7 @@ async function fetchPrTitleViaGh({
13871
13933
  PR_NUMBER,
13872
13934
  GH_REPO
13873
13935
  }) {
13874
- const { stdout } = await execa23(
13936
+ const { stdout } = await execa24(
13875
13937
  "gh",
13876
13938
  ["pr", "view", PR_NUMBER, "--repo", GH_REPO, "--json", "title", "--jq", ".title"],
13877
13939
  { env: { ...process.env, GH_TOKEN } }
@@ -13907,7 +13969,7 @@ async function resolveReleaseSubject({
13907
13969
  );
13908
13970
  }
13909
13971
  }
13910
- return (await execa23("git", ["log", "-1", "--format=%s"], { cwd })).stdout.trim();
13972
+ return (await execa24("git", ["log", "-1", "--format=%s"], { cwd })).stdout.trim();
13911
13973
  }
13912
13974
  async function runReleaseSubjectCheck(argv) {
13913
13975
  const base = process.env["GITHUB_BASE_REF"] ?? argv[0];
@@ -13915,9 +13977,9 @@ async function runReleaseSubjectCheck(argv) {
13915
13977
  console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
13916
13978
  process.exit(2);
13917
13979
  }
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`], {
13980
+ const root = (await execa24("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
13981
+ await execa24("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
13982
+ const { stdout } = await execa24("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
13921
13983
  cwd: root
13922
13984
  });
13923
13985
  const changedFiles = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
@@ -14168,13 +14230,13 @@ async function runSharedFileReductionCheck(args) {
14168
14230
  }
14169
14231
 
14170
14232
  // 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";
14233
+ import { existsSync as existsSync51, readdirSync as readdirSync27 } from "fs";
14234
+ import { join as join60 } from "path";
14235
+ import { execa as execa25 } from "execa";
14174
14236
 
14175
14237
  // src/lib/skeleton-drift-guard.ts
14176
14238
  import { readFileSync as readFileSync43, readdirSync as readdirSync26, statSync as statSync16 } from "fs";
14177
- import { join as join58 } from "path";
14239
+ import { join as join59 } from "path";
14178
14240
  var isWorkflow = (rel) => rel.startsWith(".github/workflows/") && (rel.endsWith(".yml") || rel.endsWith(".yaml"));
14179
14241
  var isRootLayout = (rel) => rel.endsWith("src/app/layout.tsx");
14180
14242
  var uncommented = (contents) => contents.split("\n").filter((line) => !/^\s*(\/\/|\/\*|\*)/.test(line)).join("\n");
@@ -14238,7 +14300,7 @@ function walk(dir, base = dir) {
14238
14300
  }
14239
14301
  for (const entry of entries) {
14240
14302
  if (entry === ".venv" || entry === "node_modules" || entry === ".git") continue;
14241
- const abs = join58(dir, entry);
14303
+ const abs = join59(dir, entry);
14242
14304
  let isDir;
14243
14305
  try {
14244
14306
  isDir = statSync16(abs).isDirectory();
@@ -14260,7 +14322,7 @@ function auditSkeleton(skeletonRoot, name, rules = SKELETON_RULES) {
14260
14322
  if (!rule.appliesTo(rel)) continue;
14261
14323
  let contents;
14262
14324
  try {
14263
- contents = readFileSync43(join58(skeletonRoot, rel), "utf8");
14325
+ contents = readFileSync43(join59(skeletonRoot, rel), "utf8");
14264
14326
  } catch {
14265
14327
  continue;
14266
14328
  }
@@ -14289,23 +14351,23 @@ function formatViolations2(violations) {
14289
14351
 
14290
14352
  // src/scripts/check-skeleton-drift.ts
14291
14353
  function discoverSkeletons(root) {
14292
- const skeletonsDir = join59(root, "_skeletons");
14354
+ const skeletonsDir = join60(root, "_skeletons");
14293
14355
  let entries;
14294
14356
  try {
14295
14357
  entries = readdirSync27(skeletonsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
14296
14358
  } catch {
14297
14359
  return [];
14298
14360
  }
14299
- return entries.filter((name) => existsSync50(join59(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
14361
+ return entries.filter((name) => existsSync51(join60(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
14300
14362
  }
14301
14363
  async function runSkeletonDriftCheck() {
14302
- const root = (await execa24("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
14364
+ const root = (await execa25("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
14303
14365
  const skeletons = discoverSkeletons(root);
14304
14366
  let filesConsidered = 0;
14305
14367
  for (const name of skeletons) {
14306
- const skeletonRoot = join59(root, "_skeletons", name);
14368
+ const skeletonRoot = join60(root, "_skeletons", name);
14307
14369
  filesConsidered += findWorkflowFiles(skeletonRoot).length;
14308
- if (existsSync50(join59(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
14370
+ if (existsSync51(join60(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
14309
14371
  filesConsidered += 1;
14310
14372
  }
14311
14373
  }
@@ -14319,7 +14381,7 @@ async function runSkeletonDriftCheck() {
14319
14381
  process.exit(1);
14320
14382
  }
14321
14383
  const violations = skeletons.flatMap(
14322
- (name) => auditSkeleton(join59(root, "_skeletons", name), name)
14384
+ (name) => auditSkeleton(join60(root, "_skeletons", name), name)
14323
14385
  );
14324
14386
  if (violations.length > 0) {
14325
14387
  console.error("\u2717 Skeleton-drift guard: drift found between this repo and its scaffolding\n");
@@ -14331,9 +14393,9 @@ async function runSkeletonDriftCheck() {
14331
14393
  }
14332
14394
 
14333
14395
  // src/scripts/check-terraform-input.ts
14334
- import { execa as execa25 } from "execa";
14396
+ import { execa as execa26 } from "execa";
14335
14397
  async function runTerraformInputCheck() {
14336
- const root = (await execa25("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
14398
+ const root = (await execa26("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
14337
14399
  const files = findWorkflowFiles(root);
14338
14400
  console.log(`audited ${files.length} workflow file(s) under ${root}`);
14339
14401
  if (files.length === 0) {
@@ -14356,7 +14418,7 @@ async function runTerraformInputCheck() {
14356
14418
 
14357
14419
  // src/commands/check.ts
14358
14420
  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)"
14421
+ "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
14422
  );
14361
14423
  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
14424
  await runOwnershipCheck(rawArgsAfter("ownership"));
@@ -14408,6 +14470,17 @@ checkCommand.command("core-direct-paths").description(
14408
14470
  await runCoreDirectPathsCheck(opts);
14409
14471
  }
14410
14472
  );
14473
+ checkCommand.command("instance-adoption").description(
14474
+ "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."
14475
+ ).option("--instance <name>", "Label for the report (defaults to the basename of --instance-dir)").option(
14476
+ "--instance-dir <dir>",
14477
+ "REQUIRED: the real instance tree to check adoption against (oursDir) \u2014 no self-check default exists (exits 2, cannot-tell, when omitted)"
14478
+ ).option(
14479
+ "--theirs-dir <dir>",
14480
+ "Template tree that ships the channel (theirsDir); defaults to this repo root"
14481
+ ).action(async (opts) => {
14482
+ await runInstanceAdoptionCheck(opts);
14483
+ });
14411
14484
  checkCommand.command("cognito-invite-template").description(
14412
14485
  "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
14486
  ).action(async () => {
@@ -14474,8 +14547,8 @@ function rawArgsAfter(subcommand) {
14474
14547
  }
14475
14548
 
14476
14549
  // src/commands/doctor.ts
14477
- import { existsSync as existsSync51, readFileSync as readFileSync44 } from "fs";
14478
- import { join as join60, resolve as resolve20 } from "path";
14550
+ import { existsSync as existsSync52, readFileSync as readFileSync44 } from "fs";
14551
+ import { join as join61, resolve as resolve20 } from "path";
14479
14552
  import chalk21 from "chalk";
14480
14553
  import { Command as Command25 } from "commander";
14481
14554
 
@@ -14654,8 +14727,8 @@ async function runDoctor(options, deps = { git: new GitAdapter() }) {
14654
14727
  return runDoctorChecks(facts);
14655
14728
  }
14656
14729
  function readLocalCoreVersion(cwd) {
14657
- const path = join60(cwd, INSTANCE_CORE_FILE);
14658
- if (!existsSync51(path)) return null;
14730
+ const path = join61(cwd, INSTANCE_CORE_FILE);
14731
+ if (!existsSync52(path)) return null;
14659
14732
  try {
14660
14733
  return extractVersionField(readFileSync44(path, "utf8"));
14661
14734
  } catch {
@@ -14677,8 +14750,8 @@ function extractVersionField(contents) {
14677
14750
  return match?.[1] ?? null;
14678
14751
  }
14679
14752
  function readFossil(cwd) {
14680
- const path = join60(cwd, CORE_VERSION_FILE);
14681
- if (!existsSync51(path)) return null;
14753
+ const path = join61(cwd, CORE_VERSION_FILE);
14754
+ if (!existsSync52(path)) return null;
14682
14755
  try {
14683
14756
  const value = readFileSync44(path, "utf8").trim();
14684
14757
  return value === "" ? null : value;
@@ -15129,13 +15202,13 @@ import { fileURLToPath as fileURLToPath6 } from "url";
15129
15202
  import { Command as Command27 } from "commander";
15130
15203
 
15131
15204
  // src/lib/packaged-scripts.ts
15132
- import { existsSync as existsSync52 } from "fs";
15133
- import { dirname as dirname11, join as join61 } from "path";
15205
+ import { existsSync as existsSync53 } from "fs";
15206
+ import { dirname as dirname11, join as join62 } from "path";
15134
15207
  function findPackagedScript(startDir, relativePath) {
15135
15208
  let dir = startDir;
15136
15209
  for (; ; ) {
15137
- const candidate = join61(dir, relativePath);
15138
- if (existsSync52(candidate)) return candidate;
15210
+ const candidate = join62(dir, relativePath);
15211
+ if (existsSync53(candidate)) return candidate;
15139
15212
  const parent = dirname11(dir);
15140
15213
  if (parent === dir) return null;
15141
15214
  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.1",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",