@biffo/cli 0.213.0 → 0.214.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.
@@ -567,6 +567,7 @@ fi
567
567
  if [ -f scripts/biffo.sh ]; then
568
568
  run_check plugin-tf sh scripts/biffo.sh check plugin-terraform
569
569
  run_check plugin-names sh scripts/biffo.sh check plugin-collisions
570
+ run_check adr-numbering sh scripts/biffo.sh check adr-numbering
570
571
  else
571
572
  skip biffo-guards "no scripts/biffo.sh in this repo"
572
573
  fi
@@ -567,6 +567,7 @@ fi
567
567
  if [ -f scripts/biffo.sh ]; then
568
568
  run_check plugin-tf sh scripts/biffo.sh check plugin-terraform
569
569
  run_check plugin-names sh scripts/biffo.sh check plugin-collisions
570
+ run_check adr-numbering sh scripts/biffo.sh check adr-numbering
570
571
  else
571
572
  skip biffo-guards "no scripts/biffo.sh in this repo"
572
573
  fi
package/dist/index.js CHANGED
@@ -9124,9 +9124,61 @@ siblingCommand.addCommand(siblingCheckIdentityCommand);
9124
9124
  // src/commands/check.ts
9125
9125
  import { Command as Command23 } from "commander";
9126
9126
 
9127
+ // src/scripts/check-adr-numbering.ts
9128
+ import { existsSync as existsSync32 } from "fs";
9129
+ import { join as join31 } from "path";
9130
+ import { execa as execa5 } from "execa";
9131
+
9132
+ // src/lib/adr-numbering-guard.ts
9133
+ import { existsSync as existsSync31, readdirSync as readdirSync13 } from "fs";
9134
+ var ADR_FILENAME = /^(\d{4})-.+\.md$/;
9135
+ function adrNumbersIn(adrDir) {
9136
+ const claims = /* @__PURE__ */ new Map();
9137
+ if (!existsSync31(adrDir)) return claims;
9138
+ for (const entry of readdirSync13(adrDir).sort()) {
9139
+ const match = ADR_FILENAME.exec(entry);
9140
+ if (!match) continue;
9141
+ const number = match[1];
9142
+ claims.set(number, [...claims.get(number) ?? [], entry]);
9143
+ }
9144
+ return claims;
9145
+ }
9146
+ function findAdrNumberCollisions(adrDir) {
9147
+ const collisions = [];
9148
+ for (const [number, files] of [...adrNumbersIn(adrDir).entries()].sort()) {
9149
+ if (files.length > 1) collisions.push({ number, files: [...files].sort() });
9150
+ }
9151
+ return collisions;
9152
+ }
9153
+ function formatAdrNumberCollisions(collisions) {
9154
+ return collisions.map(
9155
+ (c) => ` ADR-${c.number} is claimed by: ${c.files.join(", ")}
9156
+ Pick a different number for the newer one \u2014 citing "ADR-${c.number}" is
9157
+ ambiguous while both exist.`
9158
+ ).join("\n");
9159
+ }
9160
+
9161
+ // src/scripts/check-adr-numbering.ts
9162
+ async function runAdrNumberingCheck() {
9163
+ const root = (await execa5("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9164
+ const adrDir = join31(root, "docs", "ADR");
9165
+ if (!existsSync32(adrDir)) {
9166
+ console.log("\u2713 ADR numbering guard: no docs/ADR/ directory \u2014 nothing to compare");
9167
+ return;
9168
+ }
9169
+ const collisions = findAdrNumberCollisions(adrDir);
9170
+ if (collisions.length > 0) {
9171
+ console.error("\u2717 ADR numbering guard: two ADRs in docs/ADR/ share a number\n");
9172
+ console.error(formatAdrNumberCollisions(collisions));
9173
+ console.error("\nSee tabsii-platform#449 for how this class of collision happens.");
9174
+ process.exit(1);
9175
+ }
9176
+ console.log("\u2713 ADR numbering guard: OK");
9177
+ }
9178
+
9127
9179
  // src/scripts/check-branch-protection.ts
9128
9180
  import { Octokit as Octokit2 } from "@octokit/rest";
9129
- import { execa as execa5 } from "execa";
9181
+ import { execa as execa6 } from "execa";
9130
9182
 
9131
9183
  // src/lib/branch-protection-apply.ts
9132
9184
  var CONTEXT_CONSISTENCY_THRESHOLD = 2 / 3;
@@ -9253,7 +9305,7 @@ async function resolveRepo(explicit) {
9253
9305
  }
9254
9306
  return { owner, repo };
9255
9307
  }
9256
- const { stdout } = await execa5("git", ["remote", "get-url", "origin"]);
9308
+ const { stdout } = await execa6("git", ["remote", "get-url", "origin"]);
9257
9309
  const m = /github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/.exec(stdout.trim());
9258
9310
  if (!m?.[1] || !m[2]) {
9259
9311
  console.error(
@@ -9385,7 +9437,7 @@ async function runBranchProtectionCheck(explicitRepo, options = {}) {
9385
9437
  }
9386
9438
 
9387
9439
  // src/scripts/check-core-ownership.ts
9388
- import { execa as execa6 } from "execa";
9440
+ import { execa as execa7 } from "execa";
9389
9441
  var BOLD = "\x1B[1m";
9390
9442
  var DIM = "\x1B[2m";
9391
9443
  var RED = "\x1B[31m";
@@ -9396,7 +9448,7 @@ async function runOwnershipCheck(argv) {
9396
9448
  const stagedFlag = args.indexOf("--staged");
9397
9449
  const staged = stagedFlag !== -1;
9398
9450
  const messageFile = staged ? args[stagedFlag + 1] : void 0;
9399
- const root = (await execa6("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9451
+ const root = (await execa7("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9400
9452
  if (!isInstanceRepo(root)) {
9401
9453
  console.log("\u2713 core ownership guard: skipped \u2014 this is the template, which owns these paths.");
9402
9454
  return;
@@ -9405,11 +9457,11 @@ async function runOwnershipCheck(argv) {
9405
9457
  let deletedFiles = [];
9406
9458
  let commitMessage = "";
9407
9459
  if (staged) {
9408
- const { stdout } = await execa6("git", ["diff", "--cached", "--name-status"], { cwd: root });
9460
+ const { stdout } = await execa7("git", ["diff", "--cached", "--name-status"], { cwd: root });
9409
9461
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
9410
9462
  if (messageFile) {
9411
- const { readFileSync: readFileSync26, existsSync: existsSync35 } = await import("fs");
9412
- if (existsSync35(messageFile)) commitMessage = readFileSync26(messageFile, "utf8");
9463
+ const { readFileSync: readFileSync26, existsSync: existsSync37 } = await import("fs");
9464
+ if (existsSync37(messageFile)) commitMessage = readFileSync26(messageFile, "utf8");
9413
9465
  }
9414
9466
  } else {
9415
9467
  const base = process.env["GITHUB_BASE_REF"] ?? args[0];
@@ -9417,18 +9469,18 @@ async function runOwnershipCheck(argv) {
9417
9469
  console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
9418
9470
  process.exit(2);
9419
9471
  }
9420
- await execa6("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
9421
- const { stdout } = await execa6("git", ["diff", "--name-status", `origin/${base}...HEAD`], {
9472
+ await execa7("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
9473
+ const { stdout } = await execa7("git", ["diff", "--name-status", `origin/${base}...HEAD`], {
9422
9474
  cwd: root
9423
9475
  });
9424
9476
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
9425
- const { stdout: log2 } = await execa6("git", ["log", "--format=%B", `origin/${base}..HEAD`], {
9477
+ const { stdout: log2 } = await execa7("git", ["log", "--format=%B", `origin/${base}..HEAD`], {
9426
9478
  cwd: root,
9427
9479
  reject: false
9428
9480
  });
9429
9481
  commitMessage = log2;
9430
9482
  }
9431
- const { stdout: gitBranch } = await execa6("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
9483
+ const { stdout: gitBranch } = await execa7("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
9432
9484
  cwd: root,
9433
9485
  reject: false
9434
9486
  });
@@ -9510,34 +9562,34 @@ ${BOLD}If the divergence is deliberate${OFF}
9510
9562
  }
9511
9563
 
9512
9564
  // src/scripts/check-plugin-collisions.ts
9513
- import { existsSync as existsSync32 } from "fs";
9514
- import { join as join32 } from "path";
9515
- import { execa as execa7 } from "execa";
9565
+ import { existsSync as existsSync34 } from "fs";
9566
+ import { join as join33 } from "path";
9567
+ import { execa as execa8 } from "execa";
9516
9568
 
9517
9569
  // src/lib/plugin-collision-guard.ts
9518
- import { existsSync as existsSync31, readdirSync as readdirSync13, statSync as statSync7 } from "fs";
9519
- import { join as join31 } from "path";
9570
+ import { existsSync as existsSync33, readdirSync as readdirSync14, statSync as statSync7 } from "fs";
9571
+ import { join as join32 } from "path";
9520
9572
  var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
9521
9573
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
9522
9574
  function subdirectories(dir) {
9523
- if (!existsSync31(dir)) return [];
9524
- return readdirSync13(dir).filter((entry) => {
9575
+ if (!existsSync33(dir)) return [];
9576
+ return readdirSync14(dir).filter((entry) => {
9525
9577
  if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
9526
9578
  try {
9527
- return statSync7(join31(dir, entry)).isDirectory();
9579
+ return statSync7(join32(dir, entry)).isDirectory();
9528
9580
  } catch {
9529
9581
  return false;
9530
9582
  }
9531
9583
  });
9532
9584
  }
9533
9585
  function regularPackagesOf(pluginDir2) {
9534
- return subdirectories(pluginDir2).filter((name) => existsSync31(join31(pluginDir2, name, "__init__.py"))).sort();
9586
+ return subdirectories(pluginDir2).filter((name) => existsSync33(join32(pluginDir2, name, "__init__.py"))).sort();
9535
9587
  }
9536
9588
  function bareTestModulesOf(pluginDir2) {
9537
- const testsDir = join31(pluginDir2, "tests");
9538
- if (!existsSync31(testsDir)) return [];
9539
- if (existsSync31(join31(testsDir, "__init__.py"))) return [];
9540
- return readdirSync13(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
9589
+ const testsDir = join32(pluginDir2, "tests");
9590
+ if (!existsSync33(testsDir)) return [];
9591
+ if (existsSync33(join32(testsDir, "__init__.py"))) return [];
9592
+ return readdirSync14(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
9541
9593
  }
9542
9594
  function findCollisions(servicesDir, pluginDirs) {
9543
9595
  const plugins = (pluginDirs ?? subdirectories(servicesDir)).filter((name) => !name.startsWith("_")).filter((name) => name !== "api").sort();
@@ -9545,7 +9597,7 @@ function findCollisions(servicesDir, pluginDirs) {
9545
9597
  const gather = (kind, namesOf) => {
9546
9598
  const claims = /* @__PURE__ */ new Map();
9547
9599
  for (const plugin of plugins) {
9548
- for (const name of namesOf(join31(servicesDir, plugin))) {
9600
+ for (const name of namesOf(join32(servicesDir, plugin))) {
9549
9601
  claims.set(name, [...claims.get(name) ?? [], plugin]);
9550
9602
  }
9551
9603
  }
@@ -9582,9 +9634,9 @@ function formatCollisions(collisions) {
9582
9634
 
9583
9635
  // src/scripts/check-plugin-collisions.ts
9584
9636
  async function runPluginCollisionCheck() {
9585
- const root = (await execa7("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9586
- const servicesDir = join32(root, "services");
9587
- if (!existsSync32(servicesDir)) {
9637
+ const root = (await execa8("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9638
+ const servicesDir = join33(root, "services");
9639
+ if (!existsSync34(servicesDir)) {
9588
9640
  console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
9589
9641
  return;
9590
9642
  }
@@ -9601,11 +9653,11 @@ async function runPluginCollisionCheck() {
9601
9653
  }
9602
9654
 
9603
9655
  // src/scripts/check-plugin-terraform.ts
9604
- import { execa as execa8 } from "execa";
9656
+ import { execa as execa9 } from "execa";
9605
9657
 
9606
9658
  // src/lib/plugin-terraform-guard.ts
9607
- import { existsSync as existsSync33, readFileSync as readFileSync24, readdirSync as readdirSync14 } from "fs";
9608
- import { dirname as dirname9, join as join33, relative as relative6, sep as sep3 } from "path";
9659
+ import { existsSync as existsSync35, readFileSync as readFileSync24, readdirSync as readdirSync15 } from "fs";
9660
+ import { dirname as dirname9, join as join34, relative as relative6, sep as sep3 } from "path";
9609
9661
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
9610
9662
  var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
9611
9663
  function findPluginManifests(root) {
@@ -9613,16 +9665,16 @@ function findPluginManifests(root) {
9613
9665
  const walk = (dir) => {
9614
9666
  let entries;
9615
9667
  try {
9616
- entries = readdirSync14(dir, { withFileTypes: true });
9668
+ entries = readdirSync15(dir, { withFileTypes: true });
9617
9669
  } catch {
9618
9670
  return;
9619
9671
  }
9620
9672
  for (const entry of entries) {
9621
9673
  if (entry.isDirectory()) {
9622
9674
  if (SKIP_DIRS.has(entry.name)) continue;
9623
- walk(join33(dir, entry.name));
9675
+ walk(join34(dir, entry.name));
9624
9676
  } else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
9625
- found.push(relative6(root, join33(dir, entry.name)).split(sep3).join("/"));
9677
+ found.push(relative6(root, join34(dir, entry.name)).split(sep3).join("/"));
9626
9678
  }
9627
9679
  }
9628
9680
  };
@@ -9647,14 +9699,14 @@ function readSubscriptions(absManifestPath) {
9647
9699
  }
9648
9700
  function checkPluginTerraform(root) {
9649
9701
  const violations = [];
9650
- const coreManifest = existsSync33(join33(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
9702
+ const coreManifest = existsSync35(join34(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
9651
9703
  for (const manifest of findPluginManifests(root)) {
9652
9704
  if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
9653
- const absManifest = join33(root, manifest);
9705
+ const absManifest = join34(root, manifest);
9654
9706
  const subscriptions = readSubscriptions(absManifest);
9655
9707
  if (subscriptions === null) continue;
9656
9708
  const pluginDir2 = dirname9(absManifest);
9657
- if (existsSync33(join33(pluginDir2, "terraform"))) continue;
9709
+ if (existsSync35(join34(pluginDir2, "terraform"))) continue;
9658
9710
  const relPluginDir = relative6(root, pluginDir2).split(sep3).join("/");
9659
9711
  violations.push({
9660
9712
  manifest,
@@ -9674,7 +9726,7 @@ function formatViolations(violations) {
9674
9726
 
9675
9727
  // src/scripts/check-plugin-terraform.ts
9676
9728
  async function runPluginTerraformCheck() {
9677
- const root = (await execa8("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9729
+ const root = (await execa9("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9678
9730
  const violations = checkPluginTerraform(root);
9679
9731
  if (violations.length > 0) {
9680
9732
  console.error("\u2717 plugin Terraform guard: event subscriptions with no infrastructure\n");
@@ -9685,7 +9737,7 @@ async function runPluginTerraformCheck() {
9685
9737
  }
9686
9738
 
9687
9739
  // src/scripts/check-release-subject.ts
9688
- import { execa as execa9 } from "execa";
9740
+ import { execa as execa10 } from "execa";
9689
9741
 
9690
9742
  // src/lib/release-version.ts
9691
9743
  var MINOR_TYPES = /* @__PURE__ */ new Set(["feat"]);
@@ -9723,13 +9775,13 @@ async function runReleaseSubjectCheck(argv) {
9723
9775
  console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
9724
9776
  process.exit(2);
9725
9777
  }
9726
- const root = (await execa9("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9727
- await execa9("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
9728
- const { stdout } = await execa9("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
9778
+ const root = (await execa10("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9779
+ await execa10("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
9780
+ const { stdout } = await execa10("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
9729
9781
  cwd: root
9730
9782
  });
9731
9783
  const changedFiles = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
9732
- const subject = process.env["PR_TITLE"]?.trim() || (await execa9("git", ["log", "-1", "--format=%s"], { cwd: root })).stdout.trim();
9784
+ const subject = process.env["PR_TITLE"]?.trim() || (await execa10("git", ["log", "-1", "--format=%s"], { cwd: root })).stdout.trim();
9733
9785
  const manifest = readCoreManifest(root);
9734
9786
  const { unparseable, bump, templateOwnedChanges, skippedAsInstance } = checkReleaseSubject(
9735
9787
  changedFiles,
@@ -9788,6 +9840,11 @@ checkCommand.command("plugin-collisions").description("Refuse two vendored plugi
9788
9840
  checkCommand.command("plugin-terraform").description("Verify every template-owned plugin declaring infra ships a Terraform module").action(async () => {
9789
9841
  await runPluginTerraformCheck();
9790
9842
  });
9843
+ checkCommand.command("adr-numbering").description(
9844
+ "Refuse two ADRs in this repo's own docs/ADR/ claiming the same number (tabsii-platform#449)"
9845
+ ).action(async () => {
9846
+ await runAdrNumberingCheck();
9847
+ });
9791
9848
  checkCommand.command("branch-protection").description(
9792
9849
  "Verify dev/staging/main are actually protected \u2014 scaffolding skips this on a 403 (#715)"
9793
9850
  ).option("--repo <owner/name>", "Repo to audit; defaults to this checkout's origin remote").option(
@@ -9802,8 +9859,8 @@ function rawArgsAfter(subcommand) {
9802
9859
  }
9803
9860
 
9804
9861
  // src/commands/doctor.ts
9805
- import { existsSync as existsSync34, readFileSync as readFileSync25 } from "fs";
9806
- import { join as join34, resolve as resolve18 } from "path";
9862
+ import { existsSync as existsSync36, readFileSync as readFileSync25 } from "fs";
9863
+ import { join as join35, resolve as resolve18 } from "path";
9807
9864
  import chalk21 from "chalk";
9808
9865
  import { Command as Command24 } from "commander";
9809
9866
 
@@ -9978,8 +10035,8 @@ async function runDoctor(options, deps = { git: new GitAdapter() }) {
9978
10035
  return runDoctorChecks(facts);
9979
10036
  }
9980
10037
  function readLocalCoreVersion(cwd) {
9981
- const path = join34(cwd, INSTANCE_CORE_FILE);
9982
- if (!existsSync34(path)) return null;
10038
+ const path = join35(cwd, INSTANCE_CORE_FILE);
10039
+ if (!existsSync36(path)) return null;
9983
10040
  try {
9984
10041
  return parseCoreRecord(readFileSync25(path, "utf8"));
9985
10042
  } catch {
@@ -9996,8 +10053,8 @@ function parseCoreRecord(contents) {
9996
10053
  }
9997
10054
  }
9998
10055
  function readFossil(cwd) {
9999
- const path = join34(cwd, CORE_VERSION_FILE);
10000
- if (!existsSync34(path)) return null;
10056
+ const path = join35(cwd, CORE_VERSION_FILE);
10057
+ if (!existsSync36(path)) return null;
10001
10058
  try {
10002
10059
  const value = readFileSync25(path, "utf8").trim();
10003
10060
  return value === "" ? null : value;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.213.0",
3
+ "version": "0.214.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",