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