@biffo/cli 0.213.0 → 0.214.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.
@@ -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,94 @@ 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 join32 } 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, readFileSync as readFileSync24 } from "fs";
9134
+ import { join as join31 } from "path";
9135
+ var ADR_FILENAME = /^(\d{4})-.+\.md$/;
9136
+ var ALLOWLIST_FILENAME = ".numbering-allowlist";
9137
+ function readAdrNumberingAllowlist(adrDir) {
9138
+ const path = join31(adrDir, ALLOWLIST_FILENAME);
9139
+ if (!existsSync31(path)) return /* @__PURE__ */ new Set();
9140
+ const numbers = /* @__PURE__ */ new Set();
9141
+ for (const rawLine of readFileSync24(path, "utf8").split("\n")) {
9142
+ const line = rawLine.split("#")[0].trim();
9143
+ if (line) numbers.add(line);
9144
+ }
9145
+ return numbers;
9146
+ }
9147
+ function adrNumbersIn(adrDir) {
9148
+ const claims = /* @__PURE__ */ new Map();
9149
+ if (!existsSync31(adrDir)) return claims;
9150
+ for (const entry of readdirSync13(adrDir).sort()) {
9151
+ const match = ADR_FILENAME.exec(entry);
9152
+ if (!match) continue;
9153
+ const number = match[1];
9154
+ claims.set(number, [...claims.get(number) ?? [], entry]);
9155
+ }
9156
+ return claims;
9157
+ }
9158
+ function findAdrNumberCollisions(adrDir) {
9159
+ const allowlist = readAdrNumberingAllowlist(adrDir);
9160
+ const collisions = [];
9161
+ for (const [number, files] of [...adrNumbersIn(adrDir).entries()].sort()) {
9162
+ if (files.length > 1 && !allowlist.has(number)) {
9163
+ collisions.push({ number, files: [...files].sort() });
9164
+ }
9165
+ }
9166
+ return collisions;
9167
+ }
9168
+ function findStaleAdrNumberingAllowlistEntries(adrDir) {
9169
+ const allowlist = readAdrNumberingAllowlist(adrDir);
9170
+ const claims = adrNumbersIn(adrDir);
9171
+ return [...allowlist].filter((number) => (claims.get(number)?.length ?? 0) < 2).sort();
9172
+ }
9173
+ function formatAdrNumberCollisions(collisions) {
9174
+ return collisions.map(
9175
+ (c) => ` ADR-${c.number} is claimed by: ${c.files.join(", ")}
9176
+ Pick a different number for the newer one \u2014 citing "ADR-${c.number}" is
9177
+ ambiguous while both exist.`
9178
+ ).join("\n");
9179
+ }
9180
+
9181
+ // src/scripts/check-adr-numbering.ts
9182
+ async function runAdrNumberingCheck() {
9183
+ const root = (await execa5("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9184
+ const adrDir = join32(root, "docs", "ADR");
9185
+ if (!existsSync32(adrDir)) {
9186
+ console.log("\u2713 ADR numbering guard: no docs/ADR/ directory \u2014 nothing to compare");
9187
+ return;
9188
+ }
9189
+ const collisions = findAdrNumberCollisions(adrDir);
9190
+ const stale = findStaleAdrNumberingAllowlistEntries(adrDir);
9191
+ let failed = false;
9192
+ if (collisions.length > 0) {
9193
+ failed = true;
9194
+ console.error("\u2717 ADR numbering guard: two ADRs in docs/ADR/ share a number\n");
9195
+ console.error(formatAdrNumberCollisions(collisions));
9196
+ console.error(
9197
+ `
9198
+ Already accepted? List it in docs/ADR/${ALLOWLIST_FILENAME} instead of leaving this red forever. See tabsii-platform#449 for how this class of collision happens.`
9199
+ );
9200
+ }
9201
+ if (stale.length > 0) {
9202
+ failed = true;
9203
+ console.error(
9204
+ `\u2717 ADR numbering guard: docs/ADR/${ALLOWLIST_FILENAME} names a number that no longer collides: ${stale.join(", ")}
9205
+ Remove the stale entry \u2014 an allowlist nothing checks against just hides the next real one.`
9206
+ );
9207
+ }
9208
+ if (failed) process.exit(1);
9209
+ console.log("\u2713 ADR numbering guard: OK");
9210
+ }
9211
+
9127
9212
  // src/scripts/check-branch-protection.ts
9128
9213
  import { Octokit as Octokit2 } from "@octokit/rest";
9129
- import { execa as execa5 } from "execa";
9214
+ import { execa as execa6 } from "execa";
9130
9215
 
9131
9216
  // src/lib/branch-protection-apply.ts
9132
9217
  var CONTEXT_CONSISTENCY_THRESHOLD = 2 / 3;
@@ -9253,7 +9338,7 @@ async function resolveRepo(explicit) {
9253
9338
  }
9254
9339
  return { owner, repo };
9255
9340
  }
9256
- const { stdout } = await execa5("git", ["remote", "get-url", "origin"]);
9341
+ const { stdout } = await execa6("git", ["remote", "get-url", "origin"]);
9257
9342
  const m = /github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/.exec(stdout.trim());
9258
9343
  if (!m?.[1] || !m[2]) {
9259
9344
  console.error(
@@ -9385,7 +9470,7 @@ async function runBranchProtectionCheck(explicitRepo, options = {}) {
9385
9470
  }
9386
9471
 
9387
9472
  // src/scripts/check-core-ownership.ts
9388
- import { execa as execa6 } from "execa";
9473
+ import { execa as execa7 } from "execa";
9389
9474
  var BOLD = "\x1B[1m";
9390
9475
  var DIM = "\x1B[2m";
9391
9476
  var RED = "\x1B[31m";
@@ -9396,7 +9481,7 @@ async function runOwnershipCheck(argv) {
9396
9481
  const stagedFlag = args.indexOf("--staged");
9397
9482
  const staged = stagedFlag !== -1;
9398
9483
  const messageFile = staged ? args[stagedFlag + 1] : void 0;
9399
- const root = (await execa6("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9484
+ const root = (await execa7("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9400
9485
  if (!isInstanceRepo(root)) {
9401
9486
  console.log("\u2713 core ownership guard: skipped \u2014 this is the template, which owns these paths.");
9402
9487
  return;
@@ -9405,11 +9490,11 @@ async function runOwnershipCheck(argv) {
9405
9490
  let deletedFiles = [];
9406
9491
  let commitMessage = "";
9407
9492
  if (staged) {
9408
- const { stdout } = await execa6("git", ["diff", "--cached", "--name-status"], { cwd: root });
9493
+ const { stdout } = await execa7("git", ["diff", "--cached", "--name-status"], { cwd: root });
9409
9494
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
9410
9495
  if (messageFile) {
9411
- const { readFileSync: readFileSync26, existsSync: existsSync35 } = await import("fs");
9412
- if (existsSync35(messageFile)) commitMessage = readFileSync26(messageFile, "utf8");
9496
+ const { readFileSync: readFileSync27, existsSync: existsSync37 } = await import("fs");
9497
+ if (existsSync37(messageFile)) commitMessage = readFileSync27(messageFile, "utf8");
9413
9498
  }
9414
9499
  } else {
9415
9500
  const base = process.env["GITHUB_BASE_REF"] ?? args[0];
@@ -9417,18 +9502,18 @@ async function runOwnershipCheck(argv) {
9417
9502
  console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
9418
9503
  process.exit(2);
9419
9504
  }
9420
- await execa6("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
9421
- const { stdout } = await execa6("git", ["diff", "--name-status", `origin/${base}...HEAD`], {
9505
+ await execa7("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
9506
+ const { stdout } = await execa7("git", ["diff", "--name-status", `origin/${base}...HEAD`], {
9422
9507
  cwd: root
9423
9508
  });
9424
9509
  ({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
9425
- const { stdout: log2 } = await execa6("git", ["log", "--format=%B", `origin/${base}..HEAD`], {
9510
+ const { stdout: log2 } = await execa7("git", ["log", "--format=%B", `origin/${base}..HEAD`], {
9426
9511
  cwd: root,
9427
9512
  reject: false
9428
9513
  });
9429
9514
  commitMessage = log2;
9430
9515
  }
9431
- const { stdout: gitBranch } = await execa6("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
9516
+ const { stdout: gitBranch } = await execa7("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
9432
9517
  cwd: root,
9433
9518
  reject: false
9434
9519
  });
@@ -9510,34 +9595,34 @@ ${BOLD}If the divergence is deliberate${OFF}
9510
9595
  }
9511
9596
 
9512
9597
  // 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";
9598
+ import { existsSync as existsSync34 } from "fs";
9599
+ import { join as join34 } from "path";
9600
+ import { execa as execa8 } from "execa";
9516
9601
 
9517
9602
  // 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";
9603
+ import { existsSync as existsSync33, readdirSync as readdirSync14, statSync as statSync7 } from "fs";
9604
+ import { join as join33 } from "path";
9520
9605
  var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
9521
9606
  var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
9522
9607
  function subdirectories(dir) {
9523
- if (!existsSync31(dir)) return [];
9524
- return readdirSync13(dir).filter((entry) => {
9608
+ if (!existsSync33(dir)) return [];
9609
+ return readdirSync14(dir).filter((entry) => {
9525
9610
  if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
9526
9611
  try {
9527
- return statSync7(join31(dir, entry)).isDirectory();
9612
+ return statSync7(join33(dir, entry)).isDirectory();
9528
9613
  } catch {
9529
9614
  return false;
9530
9615
  }
9531
9616
  });
9532
9617
  }
9533
9618
  function regularPackagesOf(pluginDir2) {
9534
- return subdirectories(pluginDir2).filter((name) => existsSync31(join31(pluginDir2, name, "__init__.py"))).sort();
9619
+ return subdirectories(pluginDir2).filter((name) => existsSync33(join33(pluginDir2, name, "__init__.py"))).sort();
9535
9620
  }
9536
9621
  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();
9622
+ const testsDir = join33(pluginDir2, "tests");
9623
+ if (!existsSync33(testsDir)) return [];
9624
+ if (existsSync33(join33(testsDir, "__init__.py"))) return [];
9625
+ return readdirSync14(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
9541
9626
  }
9542
9627
  function findCollisions(servicesDir, pluginDirs) {
9543
9628
  const plugins = (pluginDirs ?? subdirectories(servicesDir)).filter((name) => !name.startsWith("_")).filter((name) => name !== "api").sort();
@@ -9545,7 +9630,7 @@ function findCollisions(servicesDir, pluginDirs) {
9545
9630
  const gather = (kind, namesOf) => {
9546
9631
  const claims = /* @__PURE__ */ new Map();
9547
9632
  for (const plugin of plugins) {
9548
- for (const name of namesOf(join31(servicesDir, plugin))) {
9633
+ for (const name of namesOf(join33(servicesDir, plugin))) {
9549
9634
  claims.set(name, [...claims.get(name) ?? [], plugin]);
9550
9635
  }
9551
9636
  }
@@ -9582,9 +9667,9 @@ function formatCollisions(collisions) {
9582
9667
 
9583
9668
  // src/scripts/check-plugin-collisions.ts
9584
9669
  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)) {
9670
+ const root = (await execa8("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9671
+ const servicesDir = join34(root, "services");
9672
+ if (!existsSync34(servicesDir)) {
9588
9673
  console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
9589
9674
  return;
9590
9675
  }
@@ -9601,11 +9686,11 @@ async function runPluginCollisionCheck() {
9601
9686
  }
9602
9687
 
9603
9688
  // src/scripts/check-plugin-terraform.ts
9604
- import { execa as execa8 } from "execa";
9689
+ import { execa as execa9 } from "execa";
9605
9690
 
9606
9691
  // 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";
9692
+ import { existsSync as existsSync35, readFileSync as readFileSync25, readdirSync as readdirSync15 } from "fs";
9693
+ import { dirname as dirname9, join as join35, relative as relative6, sep as sep3 } from "path";
9609
9694
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
9610
9695
  var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
9611
9696
  function findPluginManifests(root) {
@@ -9613,16 +9698,16 @@ function findPluginManifests(root) {
9613
9698
  const walk = (dir) => {
9614
9699
  let entries;
9615
9700
  try {
9616
- entries = readdirSync14(dir, { withFileTypes: true });
9701
+ entries = readdirSync15(dir, { withFileTypes: true });
9617
9702
  } catch {
9618
9703
  return;
9619
9704
  }
9620
9705
  for (const entry of entries) {
9621
9706
  if (entry.isDirectory()) {
9622
9707
  if (SKIP_DIRS.has(entry.name)) continue;
9623
- walk(join33(dir, entry.name));
9708
+ walk(join35(dir, entry.name));
9624
9709
  } else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
9625
- found.push(relative6(root, join33(dir, entry.name)).split(sep3).join("/"));
9710
+ found.push(relative6(root, join35(dir, entry.name)).split(sep3).join("/"));
9626
9711
  }
9627
9712
  }
9628
9713
  };
@@ -9632,7 +9717,7 @@ function findPluginManifests(root) {
9632
9717
  function readSubscriptions(absManifestPath) {
9633
9718
  let parsed;
9634
9719
  try {
9635
- parsed = JSON.parse(readFileSync24(absManifestPath, "utf8"));
9720
+ parsed = JSON.parse(readFileSync25(absManifestPath, "utf8"));
9636
9721
  } catch {
9637
9722
  return null;
9638
9723
  }
@@ -9647,14 +9732,14 @@ function readSubscriptions(absManifestPath) {
9647
9732
  }
9648
9733
  function checkPluginTerraform(root) {
9649
9734
  const violations = [];
9650
- const coreManifest = existsSync33(join33(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
9735
+ const coreManifest = existsSync35(join35(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
9651
9736
  for (const manifest of findPluginManifests(root)) {
9652
9737
  if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
9653
- const absManifest = join33(root, manifest);
9738
+ const absManifest = join35(root, manifest);
9654
9739
  const subscriptions = readSubscriptions(absManifest);
9655
9740
  if (subscriptions === null) continue;
9656
9741
  const pluginDir2 = dirname9(absManifest);
9657
- if (existsSync33(join33(pluginDir2, "terraform"))) continue;
9742
+ if (existsSync35(join35(pluginDir2, "terraform"))) continue;
9658
9743
  const relPluginDir = relative6(root, pluginDir2).split(sep3).join("/");
9659
9744
  violations.push({
9660
9745
  manifest,
@@ -9674,7 +9759,7 @@ function formatViolations(violations) {
9674
9759
 
9675
9760
  // src/scripts/check-plugin-terraform.ts
9676
9761
  async function runPluginTerraformCheck() {
9677
- const root = (await execa8("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9762
+ const root = (await execa9("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9678
9763
  const violations = checkPluginTerraform(root);
9679
9764
  if (violations.length > 0) {
9680
9765
  console.error("\u2717 plugin Terraform guard: event subscriptions with no infrastructure\n");
@@ -9685,7 +9770,7 @@ async function runPluginTerraformCheck() {
9685
9770
  }
9686
9771
 
9687
9772
  // src/scripts/check-release-subject.ts
9688
- import { execa as execa9 } from "execa";
9773
+ import { execa as execa10 } from "execa";
9689
9774
 
9690
9775
  // src/lib/release-version.ts
9691
9776
  var MINOR_TYPES = /* @__PURE__ */ new Set(["feat"]);
@@ -9723,13 +9808,13 @@ async function runReleaseSubjectCheck(argv) {
9723
9808
  console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
9724
9809
  process.exit(2);
9725
9810
  }
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`], {
9811
+ const root = (await execa10("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
9812
+ await execa10("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
9813
+ const { stdout } = await execa10("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
9729
9814
  cwd: root
9730
9815
  });
9731
9816
  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();
9817
+ const subject = process.env["PR_TITLE"]?.trim() || (await execa10("git", ["log", "-1", "--format=%s"], { cwd: root })).stdout.trim();
9733
9818
  const manifest = readCoreManifest(root);
9734
9819
  const { unparseable, bump, templateOwnedChanges, skippedAsInstance } = checkReleaseSubject(
9735
9820
  changedFiles,
@@ -9788,6 +9873,11 @@ checkCommand.command("plugin-collisions").description("Refuse two vendored plugi
9788
9873
  checkCommand.command("plugin-terraform").description("Verify every template-owned plugin declaring infra ships a Terraform module").action(async () => {
9789
9874
  await runPluginTerraformCheck();
9790
9875
  });
9876
+ checkCommand.command("adr-numbering").description(
9877
+ "Refuse two ADRs in this repo's own docs/ADR/ claiming the same number (tabsii-platform#449)"
9878
+ ).action(async () => {
9879
+ await runAdrNumberingCheck();
9880
+ });
9791
9881
  checkCommand.command("branch-protection").description(
9792
9882
  "Verify dev/staging/main are actually protected \u2014 scaffolding skips this on a 403 (#715)"
9793
9883
  ).option("--repo <owner/name>", "Repo to audit; defaults to this checkout's origin remote").option(
@@ -9802,8 +9892,8 @@ function rawArgsAfter(subcommand) {
9802
9892
  }
9803
9893
 
9804
9894
  // src/commands/doctor.ts
9805
- import { existsSync as existsSync34, readFileSync as readFileSync25 } from "fs";
9806
- import { join as join34, resolve as resolve18 } from "path";
9895
+ import { existsSync as existsSync36, readFileSync as readFileSync26 } from "fs";
9896
+ import { join as join36, resolve as resolve18 } from "path";
9807
9897
  import chalk21 from "chalk";
9808
9898
  import { Command as Command24 } from "commander";
9809
9899
 
@@ -9978,10 +10068,10 @@ async function runDoctor(options, deps = { git: new GitAdapter() }) {
9978
10068
  return runDoctorChecks(facts);
9979
10069
  }
9980
10070
  function readLocalCoreVersion(cwd) {
9981
- const path = join34(cwd, INSTANCE_CORE_FILE);
9982
- if (!existsSync34(path)) return null;
10071
+ const path = join36(cwd, INSTANCE_CORE_FILE);
10072
+ if (!existsSync36(path)) return null;
9983
10073
  try {
9984
- return parseCoreRecord(readFileSync25(path, "utf8"));
10074
+ return parseCoreRecord(readFileSync26(path, "utf8"));
9985
10075
  } catch {
9986
10076
  return null;
9987
10077
  }
@@ -9996,10 +10086,10 @@ function parseCoreRecord(contents) {
9996
10086
  }
9997
10087
  }
9998
10088
  function readFossil(cwd) {
9999
- const path = join34(cwd, CORE_VERSION_FILE);
10000
- if (!existsSync34(path)) return null;
10089
+ const path = join36(cwd, CORE_VERSION_FILE);
10090
+ if (!existsSync36(path)) return null;
10001
10091
  try {
10002
- const value = readFileSync25(path, "utf8").trim();
10092
+ const value = readFileSync26(path, "utf8").trim();
10003
10093
  return value === "" ? null : value;
10004
10094
  } catch {
10005
10095
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.213.0",
3
+ "version": "0.214.1",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",