@biffo/cli 0.280.0 → 0.282.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/_skeletons/plugin-template/README.md +41 -4
- package/_skeletons/plugin-template/biffo.plugin.json +4 -0
- package/_skeletons/plugin-template/db/seed/000_default_widget.sql +47 -0
- package/_skeletons/plugin-template/registry-schema.json +19 -0
- package/_skeletons/plugin-template/src/example_plugin/plugin.py +15 -4
- package/_skeletons/registry/registry-schema.json +19 -0
- package/dist/index.js +834 -355
- 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";
|
|
@@ -7739,6 +7756,13 @@ var ToolDeclarationSchema = z7.object({
|
|
|
7739
7756
|
description: z7.string(),
|
|
7740
7757
|
parameters: z7.record(z7.string(), z7.unknown()).default({})
|
|
7741
7758
|
});
|
|
7759
|
+
var SeedDeclarationSchema = z7.object({
|
|
7760
|
+
dir: z7.string().regex(
|
|
7761
|
+
REL_DIR,
|
|
7762
|
+
"must be a plugin-relative path with no leading slash or traversal, e.g. db/seed"
|
|
7763
|
+
),
|
|
7764
|
+
baseline_tables: z7.array(z7.string()).default([])
|
|
7765
|
+
}).strict();
|
|
7742
7766
|
var ChatAgentDeclarationSchema = z7.object({
|
|
7743
7767
|
key: z7.string().regex(/^[a-z][a-z0-9-]*$/, "must be a lowercase kebab-case slug"),
|
|
7744
7768
|
agent_name: z7.string().optional(),
|
|
@@ -7777,7 +7801,11 @@ var PluginManifestSchema = z7.object({
|
|
|
7777
7801
|
tools: z7.array(ToolDeclarationSchema).default([]),
|
|
7778
7802
|
// Chat agents the plugin registers with Core (ADR-0017). Default empty — an
|
|
7779
7803
|
// ordinary plugin declares none.
|
|
7780
|
-
chat_agents: z7.array(ChatAgentDeclarationSchema).default([])
|
|
7804
|
+
chat_agents: z7.array(ChatAgentDeclarationSchema).default([]),
|
|
7805
|
+
// The plugin's tenant-scoped baseline-row seed (ADR-0005, biffo-template#1554).
|
|
7806
|
+
// Optional — a plugin with no baseline data omits this entirely, and
|
|
7807
|
+
// `biffo plugin install`/`upgrade` vendor nothing for it.
|
|
7808
|
+
seed: SeedDeclarationSchema.optional()
|
|
7781
7809
|
}).superRefine((manifest, ctx) => {
|
|
7782
7810
|
const tableNames = new Set(manifest.tables.map((t) => t.name));
|
|
7783
7811
|
for (const route of manifest.api_routes) {
|
|
@@ -7788,6 +7816,16 @@ var PluginManifestSchema = z7.object({
|
|
|
7788
7816
|
});
|
|
7789
7817
|
}
|
|
7790
7818
|
}
|
|
7819
|
+
if (manifest.seed) {
|
|
7820
|
+
for (const table of manifest.seed.baseline_tables) {
|
|
7821
|
+
if (!tableNames.has(table)) {
|
|
7822
|
+
ctx.addIssue({
|
|
7823
|
+
code: z7.ZodIssueCode.custom,
|
|
7824
|
+
message: `seed.baseline_tables references table '${table}', which is not declared in this manifest's 'tables' (${[...tableNames].sort().join(", ") || "none"})`
|
|
7825
|
+
});
|
|
7826
|
+
}
|
|
7827
|
+
}
|
|
7828
|
+
}
|
|
7791
7829
|
});
|
|
7792
7830
|
function validateManifest(raw) {
|
|
7793
7831
|
const result = PluginManifestSchema.safeParse(raw);
|
|
@@ -8302,6 +8340,14 @@ var RegistryPluginEntrySchema = z8.object({
|
|
|
8302
8340
|
description: z8.string().optional(),
|
|
8303
8341
|
author: z8.string().optional(),
|
|
8304
8342
|
tags: z8.array(z8.string()).optional(),
|
|
8343
|
+
// Summary-form mirror of the manifest's `seed.baseline_tables` (see
|
|
8344
|
+
// ../../lib/plugin-manifest.ts's SeedDeclarationSchema and
|
|
8345
|
+
// _skeletons/registry/registry-schema.json's `seed`, biffo-template#1554).
|
|
8346
|
+
// The registry entry only ever needs to know WHICH tables a plugin promises
|
|
8347
|
+
// baseline rows for, never the seed `dir` itself — that only matters to the
|
|
8348
|
+
// install/upgrade vendoring step, which reads it from the plugin's own
|
|
8349
|
+
// biffo.plugin.json after cloning, not from this summary.
|
|
8350
|
+
baseline_tables: z8.array(z8.string()).optional(),
|
|
8305
8351
|
required_core_version: z8.string().optional(),
|
|
8306
8352
|
infra_modules: z8.array(z8.string()).optional(),
|
|
8307
8353
|
api_routes: z8.array(z8.string()).optional(),
|
|
@@ -8428,8 +8474,8 @@ function printEntry(entry) {
|
|
|
8428
8474
|
}
|
|
8429
8475
|
|
|
8430
8476
|
// src/commands/plugin-install.ts
|
|
8431
|
-
import { cpSync as
|
|
8432
|
-
import { join as
|
|
8477
|
+
import { cpSync as cpSync5, existsSync as existsSync30, mkdirSync as mkdirSync11, readFileSync as readFileSync22, statSync as statSync6 } from "fs";
|
|
8478
|
+
import { join as join32, relative as relative3, resolve as resolve12 } from "path";
|
|
8433
8479
|
import chalk15 from "chalk";
|
|
8434
8480
|
import { Command as Command15 } from "commander";
|
|
8435
8481
|
|
|
@@ -8475,10 +8521,129 @@ var PluginMigrationsAdapter = class {
|
|
|
8475
8521
|
}
|
|
8476
8522
|
};
|
|
8477
8523
|
|
|
8478
|
-
// src/lib/plugin-
|
|
8479
|
-
import {
|
|
8480
|
-
import {
|
|
8524
|
+
// src/lib/plugin-provenance.ts
|
|
8525
|
+
import { existsSync as existsSync27, readFileSync as readFileSync20, writeFileSync as writeFileSync10 } from "fs";
|
|
8526
|
+
import { join as join28 } from "path";
|
|
8481
8527
|
import { execa as execa5 } from "execa";
|
|
8528
|
+
var PLUGIN_PROVENANCE_FILENAME = ".biffo-plugin-provenance.json";
|
|
8529
|
+
function isPluginProvenance(value) {
|
|
8530
|
+
if (typeof value !== "object" || value === null) return false;
|
|
8531
|
+
const v = value;
|
|
8532
|
+
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";
|
|
8533
|
+
}
|
|
8534
|
+
function readProvenance(pluginDir2) {
|
|
8535
|
+
const path = join28(pluginDir2, PLUGIN_PROVENANCE_FILENAME);
|
|
8536
|
+
if (!existsSync27(path)) return { status: "absent" };
|
|
8537
|
+
let parsed;
|
|
8538
|
+
try {
|
|
8539
|
+
parsed = JSON.parse(readFileSync20(path, "utf8"));
|
|
8540
|
+
} catch (err) {
|
|
8541
|
+
return {
|
|
8542
|
+
status: "invalid",
|
|
8543
|
+
reason: `could not parse ${PLUGIN_PROVENANCE_FILENAME}: ${err.message}`
|
|
8544
|
+
};
|
|
8545
|
+
}
|
|
8546
|
+
if (!isPluginProvenance(parsed)) {
|
|
8547
|
+
return {
|
|
8548
|
+
status: "invalid",
|
|
8549
|
+
reason: `${PLUGIN_PROVENANCE_FILENAME} does not have the expected shape`
|
|
8550
|
+
};
|
|
8551
|
+
}
|
|
8552
|
+
return { status: "present", record: parsed };
|
|
8553
|
+
}
|
|
8554
|
+
function writePluginProvenance(pluginDir2, record) {
|
|
8555
|
+
writeFileSync10(join28(pluginDir2, PLUGIN_PROVENANCE_FILENAME), `${JSON.stringify(record, null, 2)}
|
|
8556
|
+
`);
|
|
8557
|
+
}
|
|
8558
|
+
function reconcileProvenance(previous, next) {
|
|
8559
|
+
if (previous.status === "present" && sameProvenance(previous.record, next)) return previous.record;
|
|
8560
|
+
return next;
|
|
8561
|
+
}
|
|
8562
|
+
function sameProvenance(a, b) {
|
|
8563
|
+
return a.origin === b.origin && a.ref === b.ref && a.sha === b.sha && a.inTree === b.inTree;
|
|
8564
|
+
}
|
|
8565
|
+
function inTreePluginProvenance(relTargetDir) {
|
|
8566
|
+
return {
|
|
8567
|
+
origin: relTargetDir,
|
|
8568
|
+
ref: null,
|
|
8569
|
+
sha: null,
|
|
8570
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8571
|
+
inTree: true
|
|
8572
|
+
};
|
|
8573
|
+
}
|
|
8574
|
+
async function resolveLocalProvenance(sourceDir, origin) {
|
|
8575
|
+
const recordedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8576
|
+
if (!await isGitWorkingTree(sourceDir)) {
|
|
8577
|
+
return { origin, ref: null, sha: null, recordedAt, inTree: false };
|
|
8578
|
+
}
|
|
8579
|
+
const sha = await tryGit(sourceDir, ["rev-parse", "HEAD"]);
|
|
8580
|
+
const rawRef = await tryGit(sourceDir, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
8581
|
+
const ref = rawRef && rawRef !== "HEAD" ? rawRef : null;
|
|
8582
|
+
return { origin, ref, sha, recordedAt, inTree: false };
|
|
8583
|
+
}
|
|
8584
|
+
function resolveRegistryProvenance(repoUrl, sha) {
|
|
8585
|
+
return { origin: repoUrl, ref: null, sha, recordedAt: (/* @__PURE__ */ new Date()).toISOString(), inTree: false };
|
|
8586
|
+
}
|
|
8587
|
+
async function isGitWorkingTree(dir) {
|
|
8588
|
+
try {
|
|
8589
|
+
await execa5("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir });
|
|
8590
|
+
return true;
|
|
8591
|
+
} catch {
|
|
8592
|
+
return false;
|
|
8593
|
+
}
|
|
8594
|
+
}
|
|
8595
|
+
async function tryGit(cwd, args) {
|
|
8596
|
+
try {
|
|
8597
|
+
const { stdout } = await execa5("git", args, { cwd });
|
|
8598
|
+
return stdout.trim() || null;
|
|
8599
|
+
} catch {
|
|
8600
|
+
return null;
|
|
8601
|
+
}
|
|
8602
|
+
}
|
|
8603
|
+
|
|
8604
|
+
// src/lib/plugin-seed-vendor.ts
|
|
8605
|
+
import { cpSync as cpSync3, existsSync as existsSync28, mkdirSync as mkdirSync9, readdirSync as readdirSync12, rmSync as rmSync8 } from "fs";
|
|
8606
|
+
import { join as join29 } from "path";
|
|
8607
|
+
var VENDOR_PREFIX = "_plugin-";
|
|
8608
|
+
function pluginSeedImportDir(pluginName) {
|
|
8609
|
+
return `db/imports/${VENDOR_PREFIX}${pluginName}`;
|
|
8610
|
+
}
|
|
8611
|
+
function vendorPluginSeed(pluginSourceDir, manifest, cwd) {
|
|
8612
|
+
if (!manifest.seed) {
|
|
8613
|
+
return { vendored: false };
|
|
8614
|
+
}
|
|
8615
|
+
const sourceSeedDir = join29(pluginSourceDir, manifest.seed.dir);
|
|
8616
|
+
if (!existsSync28(sourceSeedDir)) {
|
|
8617
|
+
throw new Error(
|
|
8618
|
+
`${manifest.name}'s manifest declares seed.dir '${manifest.seed.dir}', but ${sourceSeedDir} does not exist in the plugin's source.`
|
|
8619
|
+
);
|
|
8620
|
+
}
|
|
8621
|
+
const sqlFiles = readdirSync12(sourceSeedDir).filter((f) => f.endsWith(".sql"));
|
|
8622
|
+
if (sqlFiles.length === 0) {
|
|
8623
|
+
throw new Error(
|
|
8624
|
+
`${manifest.name}'s manifest declares seed.dir '${manifest.seed.dir}', but ${sourceSeedDir} contains no *.sql files.`
|
|
8625
|
+
);
|
|
8626
|
+
}
|
|
8627
|
+
const relTargetDir = pluginSeedImportDir(manifest.name);
|
|
8628
|
+
const targetDir = join29(cwd, relTargetDir);
|
|
8629
|
+
rmSync8(targetDir, { recursive: true, force: true });
|
|
8630
|
+
mkdirSync9(targetDir, { recursive: true });
|
|
8631
|
+
for (const file of sqlFiles) {
|
|
8632
|
+
cpSync3(join29(sourceSeedDir, file), join29(targetDir, file));
|
|
8633
|
+
}
|
|
8634
|
+
log.success(
|
|
8635
|
+
`Vendored ${sqlFiles.length} seed file(s) to ${relTargetDir}/ (baseline_tables: ${manifest.seed.baseline_tables.join(", ") || "none declared"})`
|
|
8636
|
+
);
|
|
8637
|
+
log.info(
|
|
8638
|
+
`${relTargetDir}/*.sql are checksum-tracked once applied (ADR-0005 section 4) \u2014 a later version must ship a new, additively-numbered file for a changed seed, never edit one already released, or the next deploy fails loudly.`
|
|
8639
|
+
);
|
|
8640
|
+
return { vendored: true, stagedPath: relTargetDir };
|
|
8641
|
+
}
|
|
8642
|
+
|
|
8643
|
+
// src/lib/plugin-source-copy.ts
|
|
8644
|
+
import { copyFileSync as copyFileSync2, cpSync as cpSync4, mkdirSync as mkdirSync10 } from "fs";
|
|
8645
|
+
import { basename, dirname as dirname9, join as join30 } from "path";
|
|
8646
|
+
import { execa as execa6 } from "execa";
|
|
8482
8647
|
var LOCAL_COPY_EXCLUDES = /* @__PURE__ */ new Set([
|
|
8483
8648
|
".git",
|
|
8484
8649
|
".venv",
|
|
@@ -8491,35 +8656,35 @@ var LOCAL_COPY_EXCLUDES = /* @__PURE__ */ new Set([
|
|
|
8491
8656
|
".terraform"
|
|
8492
8657
|
]);
|
|
8493
8658
|
async function copyPluginSource(sourceDir, targetDir) {
|
|
8494
|
-
if (await
|
|
8659
|
+
if (await isGitWorkingTree2(sourceDir)) {
|
|
8495
8660
|
const files = await listGitFiles(sourceDir);
|
|
8496
8661
|
for (const relPath of files) {
|
|
8497
|
-
const destPath =
|
|
8498
|
-
|
|
8499
|
-
copyFileSync2(
|
|
8662
|
+
const destPath = join30(targetDir, relPath);
|
|
8663
|
+
mkdirSync10(dirname9(destPath), { recursive: true });
|
|
8664
|
+
copyFileSync2(join30(sourceDir, relPath), destPath);
|
|
8500
8665
|
}
|
|
8501
8666
|
return { usedGitIgnoreRules: true };
|
|
8502
8667
|
}
|
|
8503
8668
|
log.warn(
|
|
8504
8669
|
`${sourceDir} is not a git working tree \u2014 cannot honour .gitignore. Falling back to a fixed exclude list (.git, .venv, node_modules, caches); anything else it does not know about (e.g. an unfamiliar cache directory) will be copied.`
|
|
8505
8670
|
);
|
|
8506
|
-
|
|
8507
|
-
|
|
8671
|
+
mkdirSync10(targetDir, { recursive: true });
|
|
8672
|
+
cpSync4(sourceDir, targetDir, {
|
|
8508
8673
|
recursive: true,
|
|
8509
8674
|
filter: (src) => !LOCAL_COPY_EXCLUDES.has(basename(src))
|
|
8510
8675
|
});
|
|
8511
8676
|
return { usedGitIgnoreRules: false };
|
|
8512
8677
|
}
|
|
8513
|
-
async function
|
|
8678
|
+
async function isGitWorkingTree2(dir) {
|
|
8514
8679
|
try {
|
|
8515
|
-
await
|
|
8680
|
+
await execa6("git", ["rev-parse", "--is-inside-work-tree"], { cwd: dir });
|
|
8516
8681
|
return true;
|
|
8517
8682
|
} catch {
|
|
8518
8683
|
return false;
|
|
8519
8684
|
}
|
|
8520
8685
|
}
|
|
8521
8686
|
async function listGitFiles(dir) {
|
|
8522
|
-
const { stdout } = await
|
|
8687
|
+
const { stdout } = await execa6(
|
|
8523
8688
|
"git",
|
|
8524
8689
|
["ls-files", "--cached", "--others", "--exclude-standard", "-z"],
|
|
8525
8690
|
{ cwd: dir }
|
|
@@ -8528,8 +8693,8 @@ async function listGitFiles(dir) {
|
|
|
8528
8693
|
}
|
|
8529
8694
|
|
|
8530
8695
|
// src/lib/plugin-workspace-sources.ts
|
|
8531
|
-
import { existsSync as
|
|
8532
|
-
import { join as
|
|
8696
|
+
import { existsSync as existsSync29, readdirSync as readdirSync13, readFileSync as readFileSync21, writeFileSync as writeFileSync11 } from "fs";
|
|
8697
|
+
import { join as join31 } from "path";
|
|
8533
8698
|
function readTomlStringArray(text, key) {
|
|
8534
8699
|
const open = new RegExp(`^${key}\\s*=\\s*\\[`, "m").exec(text);
|
|
8535
8700
|
if (!open) return [];
|
|
@@ -8573,9 +8738,9 @@ function readDependencyNames(text) {
|
|
|
8573
8738
|
return readTomlStringArray(text, "dependencies").map((dep) => /^\s*([A-Za-z0-9._-]+)/.exec(dep)?.[1] ?? "").filter(Boolean);
|
|
8574
8739
|
}
|
|
8575
8740
|
function workspaceMemberNames(instanceRoot) {
|
|
8576
|
-
const rootPyproject =
|
|
8577
|
-
if (!
|
|
8578
|
-
const text =
|
|
8741
|
+
const rootPyproject = join31(instanceRoot, "pyproject.toml");
|
|
8742
|
+
if (!existsSync29(rootPyproject)) return /* @__PURE__ */ new Set();
|
|
8743
|
+
const text = readFileSync21(rootPyproject, "utf8");
|
|
8579
8744
|
const members = readTomlStringArray(text, "members");
|
|
8580
8745
|
const excluded = new Set(readTomlStringArray(text, "exclude"));
|
|
8581
8746
|
const dirs = [];
|
|
@@ -8584,7 +8749,7 @@ function workspaceMemberNames(instanceRoot) {
|
|
|
8584
8749
|
const base = member.slice(0, -2);
|
|
8585
8750
|
let entries;
|
|
8586
8751
|
try {
|
|
8587
|
-
entries =
|
|
8752
|
+
entries = readdirSync13(join31(instanceRoot, base), { withFileTypes: true });
|
|
8588
8753
|
} catch {
|
|
8589
8754
|
continue;
|
|
8590
8755
|
}
|
|
@@ -8598,9 +8763,9 @@ function workspaceMemberNames(instanceRoot) {
|
|
|
8598
8763
|
}
|
|
8599
8764
|
const names = /* @__PURE__ */ new Set();
|
|
8600
8765
|
for (const dir of dirs) {
|
|
8601
|
-
const pp =
|
|
8602
|
-
if (!
|
|
8603
|
-
const name = readProjectName(
|
|
8766
|
+
const pp = join31(instanceRoot, dir, "pyproject.toml");
|
|
8767
|
+
if (!existsSync29(pp)) continue;
|
|
8768
|
+
const name = readProjectName(readFileSync21(pp, "utf8"));
|
|
8604
8769
|
if (name) names.add(name);
|
|
8605
8770
|
}
|
|
8606
8771
|
return names;
|
|
@@ -8611,8 +8776,8 @@ function existingWorkspaceSources(text) {
|
|
|
8611
8776
|
);
|
|
8612
8777
|
}
|
|
8613
8778
|
function ensureWorkspaceSources(pluginPyprojectPath, memberNames) {
|
|
8614
|
-
if (!
|
|
8615
|
-
const text =
|
|
8779
|
+
if (!existsSync29(pluginPyprojectPath) || memberNames.size === 0) return [];
|
|
8780
|
+
const text = readFileSync21(pluginPyprojectPath, "utf8");
|
|
8616
8781
|
const already = existingWorkspaceSources(text);
|
|
8617
8782
|
const toAdd = readDependencyNames(text).filter((n) => memberNames.has(n) && !already.has(n));
|
|
8618
8783
|
if (toAdd.length === 0) return [];
|
|
@@ -8632,12 +8797,12 @@ ${lines.join("\n")}${text.slice(insertAt)}`;
|
|
|
8632
8797
|
${lines.join("\n")}
|
|
8633
8798
|
`;
|
|
8634
8799
|
}
|
|
8635
|
-
|
|
8800
|
+
writeFileSync11(pluginPyprojectPath, updated);
|
|
8636
8801
|
return toAdd;
|
|
8637
8802
|
}
|
|
8638
8803
|
function applyWorkspaceSources(targetDir, cwd, relTargetDir) {
|
|
8639
|
-
const pluginPyproject =
|
|
8640
|
-
if (!
|
|
8804
|
+
const pluginPyproject = join31(targetDir, "pyproject.toml");
|
|
8805
|
+
if (!existsSync29(pluginPyproject)) return;
|
|
8641
8806
|
const sourced = ensureWorkspaceSources(pluginPyproject, workspaceMemberNames(cwd));
|
|
8642
8807
|
if (sourced.length > 0) {
|
|
8643
8808
|
log.info(
|
|
@@ -8677,14 +8842,14 @@ var pluginInstallCommand = new Command15("install").description(
|
|
|
8677
8842
|
}
|
|
8678
8843
|
);
|
|
8679
8844
|
function resolveLocalPlugin(localPath) {
|
|
8680
|
-
if (!
|
|
8845
|
+
if (!existsSync30(localPath)) {
|
|
8681
8846
|
throw new Error(`--local path does not exist: ${localPath}`);
|
|
8682
8847
|
}
|
|
8683
8848
|
if (!statSync6(localPath).isDirectory()) {
|
|
8684
8849
|
throw new Error(`--local path is not a directory: ${localPath}`);
|
|
8685
8850
|
}
|
|
8686
|
-
const manifestPath =
|
|
8687
|
-
if (!
|
|
8851
|
+
const manifestPath = join32(localPath, "biffo.plugin.json");
|
|
8852
|
+
if (!existsSync30(manifestPath)) {
|
|
8688
8853
|
throw new Error(
|
|
8689
8854
|
`${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>\`.)`
|
|
8690
8855
|
);
|
|
@@ -8710,8 +8875,8 @@ function parsePluginTarget(target) {
|
|
|
8710
8875
|
async function cloneAndValidatePlugin(entry, git) {
|
|
8711
8876
|
const tmpDir = await git.cloneToTemp(entry.repo, `biffo-plugin-${entry.name}`);
|
|
8712
8877
|
try {
|
|
8713
|
-
const manifestPath =
|
|
8714
|
-
if (!
|
|
8878
|
+
const manifestPath = join32(tmpDir, "biffo.plugin.json");
|
|
8879
|
+
if (!existsSync30(manifestPath)) {
|
|
8715
8880
|
throw new Error(
|
|
8716
8881
|
`Plugin repo ${entry.repo} does not contain a biffo.plugin.json manifest at its root.`
|
|
8717
8882
|
);
|
|
@@ -8739,8 +8904,8 @@ async function runPluginInstall(target, options, deps) {
|
|
|
8739
8904
|
`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\`).`
|
|
8740
8905
|
);
|
|
8741
8906
|
}
|
|
8742
|
-
const servicesDir =
|
|
8743
|
-
if (!
|
|
8907
|
+
const servicesDir = join32(options.cwd, "services");
|
|
8908
|
+
if (!existsSync30(servicesDir)) {
|
|
8744
8909
|
throw new Error(
|
|
8745
8910
|
`${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
|
|
8746
8911
|
);
|
|
@@ -8758,10 +8923,10 @@ async function runPluginInstall(target, options, deps) {
|
|
|
8758
8923
|
}
|
|
8759
8924
|
const pluginName = entry ? entry.name : source.name;
|
|
8760
8925
|
const relTargetDir = pluginDir(pluginName, "third-party");
|
|
8761
|
-
const targetDir =
|
|
8762
|
-
const modulesDir =
|
|
8926
|
+
const targetDir = join32(options.cwd, relTargetDir);
|
|
8927
|
+
const modulesDir = join32(options.cwd, "modules", "plugins", pluginName);
|
|
8763
8928
|
const inTreeSource = options.local !== void 0 && resolve12(options.local) === resolve12(targetDir);
|
|
8764
|
-
if (
|
|
8929
|
+
if (existsSync30(targetDir) && !inTreeSource) {
|
|
8765
8930
|
throw new Error(
|
|
8766
8931
|
`Plugin '${pluginName}' is already installed at ${relTargetDir}/. Remove it first, or wait for a future 'biffo plugin upgrade' command.`
|
|
8767
8932
|
);
|
|
@@ -8796,16 +8961,19 @@ async function runPluginInstall(target, options, deps) {
|
|
|
8796
8961
|
if (inTreeSource) {
|
|
8797
8962
|
log.info(`${relTargetDir}/ is already in this checkout \u2014 installing in place.`);
|
|
8798
8963
|
} else {
|
|
8799
|
-
|
|
8964
|
+
mkdirSync11(targetDir, { recursive: true });
|
|
8800
8965
|
await copyPluginSource(source.sourceDir, targetDir);
|
|
8801
8966
|
log.success(`Installed plugin source at ${relTargetDir}/`);
|
|
8802
8967
|
}
|
|
8968
|
+
const previousProvenance = readProvenance(targetDir);
|
|
8969
|
+
const nextProvenance = inTreeSource ? inTreePluginProvenance(relTargetDir) : entry ? resolveRegistryProvenance(entry.repo, await deps.git.resolveDefaultBranchSha(entry.repo)) : await resolveLocalProvenance(source.sourceDir, source.origin);
|
|
8970
|
+
writePluginProvenance(targetDir, reconcileProvenance(previousProvenance, nextProvenance));
|
|
8803
8971
|
applyWorkspaceSources(targetDir, options.cwd, relTargetDir);
|
|
8804
8972
|
const stagePaths = [relTargetDir];
|
|
8805
|
-
const tfSourceDir =
|
|
8806
|
-
if (
|
|
8807
|
-
|
|
8808
|
-
|
|
8973
|
+
const tfSourceDir = join32(targetDir, "terraform");
|
|
8974
|
+
if (existsSync30(tfSourceDir)) {
|
|
8975
|
+
mkdirSync11(modulesDir, { recursive: true });
|
|
8976
|
+
cpSync5(tfSourceDir, modulesDir, { recursive: true });
|
|
8809
8977
|
stagePaths.push(`modules/plugins/${pluginName}`);
|
|
8810
8978
|
log.success(`Copied Terraform module to modules/plugins/${pluginName}/`);
|
|
8811
8979
|
const wiring = syncPluginTerraform(options.cwd);
|
|
@@ -8844,6 +9012,10 @@ async function runPluginInstall(target, options, deps) {
|
|
|
8844
9012
|
} else {
|
|
8845
9013
|
log.info(`${pluginName} declares no tables \u2014 nothing to migrate.`);
|
|
8846
9014
|
}
|
|
9015
|
+
const seedResult = vendorPluginSeed(targetDir, manifest, options.cwd);
|
|
9016
|
+
if (seedResult.vendored) {
|
|
9017
|
+
stagePaths.push(seedResult.stagedPath);
|
|
9018
|
+
}
|
|
8847
9019
|
const commitMessage = `feat(plugins): install ${pluginName}@${source.version}`;
|
|
8848
9020
|
await deps.git.add(options.cwd, stagePaths);
|
|
8849
9021
|
await deps.git.commit(options.cwd, commitMessage);
|
|
@@ -8860,7 +9032,7 @@ async function runPluginInstall(target, options, deps) {
|
|
|
8860
9032
|
}
|
|
8861
9033
|
function parseManifestFile(path) {
|
|
8862
9034
|
try {
|
|
8863
|
-
return JSON.parse(
|
|
9035
|
+
return JSON.parse(readFileSync22(path, "utf8"));
|
|
8864
9036
|
} catch (err) {
|
|
8865
9037
|
throw new Error(`Could not parse ${path} as JSON: ${err.message}`);
|
|
8866
9038
|
}
|
|
@@ -8892,13 +9064,18 @@ function printDryRun4(entry, source, relTargetDir, inTreeSource) {
|
|
|
8892
9064
|
` Would generate a migration for ${source.manifest.tables.length} table(s) into services/api/migrations/versions/`
|
|
8893
9065
|
);
|
|
8894
9066
|
}
|
|
9067
|
+
if (source && source.manifest.seed) {
|
|
9068
|
+
console.log(
|
|
9069
|
+
` Would vendor seed DDL into: ${pluginSeedImportDir(name)}/ (baseline_tables: ${source.manifest.seed.baseline_tables.join(", ") || "none declared"})`
|
|
9070
|
+
);
|
|
9071
|
+
}
|
|
8895
9072
|
console.log(` Would commit: feat(plugins): install ${name}@${version}
|
|
8896
9073
|
`);
|
|
8897
9074
|
}
|
|
8898
9075
|
|
|
8899
9076
|
// src/commands/plugin-list.ts
|
|
8900
|
-
import { existsSync as
|
|
8901
|
-
import { join as
|
|
9077
|
+
import { existsSync as existsSync31, readFileSync as readFileSync23 } from "fs";
|
|
9078
|
+
import { join as join33, resolve as resolve13 } from "path";
|
|
8902
9079
|
import chalk16 from "chalk";
|
|
8903
9080
|
import { Command as Command16 } from "commander";
|
|
8904
9081
|
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) => {
|
|
@@ -8911,8 +9088,8 @@ var pluginListCommand = new Command16("list").description("List plugins installe
|
|
|
8911
9088
|
}
|
|
8912
9089
|
});
|
|
8913
9090
|
async function runPluginList(options) {
|
|
8914
|
-
const servicesDir =
|
|
8915
|
-
if (!
|
|
9091
|
+
const servicesDir = join33(options.cwd, "services");
|
|
9092
|
+
if (!existsSync31(servicesDir)) {
|
|
8916
9093
|
throw new Error(
|
|
8917
9094
|
`${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
|
|
8918
9095
|
);
|
|
@@ -8920,7 +9097,7 @@ async function runPluginList(options) {
|
|
|
8920
9097
|
const plugins = [];
|
|
8921
9098
|
for (const location of findInstalledPlugins(options.cwd)) {
|
|
8922
9099
|
try {
|
|
8923
|
-
const manifest = validateManifest(JSON.parse(
|
|
9100
|
+
const manifest = validateManifest(JSON.parse(readFileSync23(location.manifestPath, "utf8")));
|
|
8924
9101
|
plugins.push({
|
|
8925
9102
|
name: manifest.name,
|
|
8926
9103
|
version: manifest.version,
|
|
@@ -8956,16 +9133,275 @@ async function runPluginList(options) {
|
|
|
8956
9133
|
);
|
|
8957
9134
|
}
|
|
8958
9135
|
|
|
9136
|
+
// src/commands/plugin-staleness.ts
|
|
9137
|
+
import { resolve as resolve14 } from "path";
|
|
9138
|
+
import { Command as Command17 } from "commander";
|
|
9139
|
+
|
|
9140
|
+
// src/lib/plugin-staleness.ts
|
|
9141
|
+
import { existsSync as existsSync32, readFileSync as readFileSync24, readdirSync as readdirSync14, statSync as statSync7 } from "fs";
|
|
9142
|
+
import { join as join34, relative as relative4 } from "path";
|
|
9143
|
+
function discoverVendoredPlugins(servicesDir) {
|
|
9144
|
+
if (!existsSync32(servicesDir)) return [];
|
|
9145
|
+
return readdirSync14(servicesDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith("_") && e.name !== "api").map((e) => e.name).filter((name) => existsSync32(join34(servicesDir, name, "biffo.plugin.json"))).sort();
|
|
9146
|
+
}
|
|
9147
|
+
async function checkPluginStaleness(cwd, deps) {
|
|
9148
|
+
const servicesDir = join34(cwd, "services");
|
|
9149
|
+
const names = discoverVendoredPlugins(servicesDir);
|
|
9150
|
+
let registryRepoByName = null;
|
|
9151
|
+
const resolveRegistryRepo = async (name) => {
|
|
9152
|
+
if (registryRepoByName === null) {
|
|
9153
|
+
registryRepoByName = /* @__PURE__ */ new Map();
|
|
9154
|
+
try {
|
|
9155
|
+
const reg = await deps.registry.fetchRegistry();
|
|
9156
|
+
for (const entry of reg.plugins) registryRepoByName.set(entry.name, entry.repo);
|
|
9157
|
+
} catch {
|
|
9158
|
+
}
|
|
9159
|
+
}
|
|
9160
|
+
return registryRepoByName.get(name) ?? null;
|
|
9161
|
+
};
|
|
9162
|
+
const results = [];
|
|
9163
|
+
for (const name of names) {
|
|
9164
|
+
results.push(await checkOnePlugin(join34(servicesDir, name), name, resolveRegistryRepo, deps.git));
|
|
9165
|
+
}
|
|
9166
|
+
return results;
|
|
9167
|
+
}
|
|
9168
|
+
async function checkOnePlugin(pluginDir2, name, resolveRegistryRepo, git) {
|
|
9169
|
+
const provenance = readProvenance(pluginDir2);
|
|
9170
|
+
if (provenance.status === "invalid") {
|
|
9171
|
+
return {
|
|
9172
|
+
name,
|
|
9173
|
+
status: "cannot-tell",
|
|
9174
|
+
method: "unresolvable",
|
|
9175
|
+
detail: `provenance file is unreadable \u2014 ${provenance.reason}`
|
|
9176
|
+
};
|
|
9177
|
+
}
|
|
9178
|
+
const record = provenance.status === "present" ? provenance.record : null;
|
|
9179
|
+
if (record?.inTree) {
|
|
9180
|
+
return {
|
|
9181
|
+
name,
|
|
9182
|
+
status: "cannot-tell",
|
|
9183
|
+
method: "unresolvable",
|
|
9184
|
+
detail: `installed --local straight into services/${name}/ (in-tree) \u2014 there is no external plugin repo to compare against`
|
|
9185
|
+
};
|
|
9186
|
+
}
|
|
9187
|
+
if (record?.sha && isFetchableUrl(record.origin)) {
|
|
9188
|
+
return checkViaProvenance(name, record, record.origin, git);
|
|
9189
|
+
}
|
|
9190
|
+
const localOrigin = record && !isFetchableUrl(record.origin) && existsSync32(record.origin) ? record.origin : null;
|
|
9191
|
+
if (localOrigin) {
|
|
9192
|
+
return checkViaContentDiff(name, pluginDir2, localOrigin, { isLocalDir: true }, git);
|
|
9193
|
+
}
|
|
9194
|
+
const registryRepo = await resolveRegistryRepo(name);
|
|
9195
|
+
if (!registryRepo) {
|
|
9196
|
+
return {
|
|
9197
|
+
name,
|
|
9198
|
+
status: "cannot-tell",
|
|
9199
|
+
method: "unresolvable",
|
|
9200
|
+
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`
|
|
9201
|
+
};
|
|
9202
|
+
}
|
|
9203
|
+
if (record?.sha) {
|
|
9204
|
+
return checkViaProvenance(name, record, registryRepo, git);
|
|
9205
|
+
}
|
|
9206
|
+
return checkViaContentDiff(name, pluginDir2, registryRepo, { isLocalDir: false }, git);
|
|
9207
|
+
}
|
|
9208
|
+
function isFetchableUrl(origin) {
|
|
9209
|
+
return /^(https?|git|ssh):\/\//.test(origin) || /^[^/\\]+@[^:]+:/.test(origin);
|
|
9210
|
+
}
|
|
9211
|
+
async function checkViaProvenance(name, record, repoUrl, git) {
|
|
9212
|
+
const remoteHeadSha = await git.resolveDefaultBranchSha(repoUrl);
|
|
9213
|
+
if (!remoteHeadSha) {
|
|
9214
|
+
return {
|
|
9215
|
+
name,
|
|
9216
|
+
status: "cannot-tell",
|
|
9217
|
+
method: "unresolvable",
|
|
9218
|
+
detail: `could not reach ${repoUrl} (network or authentication failure)`
|
|
9219
|
+
};
|
|
9220
|
+
}
|
|
9221
|
+
if (remoteHeadSha === record.sha) {
|
|
9222
|
+
return {
|
|
9223
|
+
name,
|
|
9224
|
+
status: "up-to-date",
|
|
9225
|
+
method: "provenance",
|
|
9226
|
+
detail: `matches ${repoUrl}'s default branch (${shortSha(remoteHeadSha)})`
|
|
9227
|
+
};
|
|
9228
|
+
}
|
|
9229
|
+
let clone;
|
|
9230
|
+
try {
|
|
9231
|
+
clone = await git.cloneForEditing(repoUrl, "biffo-plugin-staleness-full");
|
|
9232
|
+
} catch {
|
|
9233
|
+
return {
|
|
9234
|
+
name,
|
|
9235
|
+
status: "cannot-tell",
|
|
9236
|
+
method: "unresolvable",
|
|
9237
|
+
detail: `could not clone ${repoUrl} to count commits behind (network or authentication failure)`
|
|
9238
|
+
};
|
|
9239
|
+
}
|
|
9240
|
+
try {
|
|
9241
|
+
const commitsBehind = await git.countBehind(clone, record.sha, "HEAD");
|
|
9242
|
+
if (commitsBehind === null) {
|
|
9243
|
+
return {
|
|
9244
|
+
name,
|
|
9245
|
+
status: "cannot-tell",
|
|
9246
|
+
method: "unresolvable",
|
|
9247
|
+
detail: `recorded commit ${shortSha(record.sha)} was not found in ${repoUrl}'s history (rebased or force-pushed?) \u2014 cannot count commits behind`
|
|
9248
|
+
};
|
|
9249
|
+
}
|
|
9250
|
+
if (commitsBehind === 0) {
|
|
9251
|
+
return {
|
|
9252
|
+
name,
|
|
9253
|
+
status: "up-to-date",
|
|
9254
|
+
method: "provenance",
|
|
9255
|
+
detail: `matches ${repoUrl}'s default branch (${shortSha(remoteHeadSha)})`
|
|
9256
|
+
};
|
|
9257
|
+
}
|
|
9258
|
+
return {
|
|
9259
|
+
name,
|
|
9260
|
+
status: "behind",
|
|
9261
|
+
commitsBehind,
|
|
9262
|
+
method: "provenance",
|
|
9263
|
+
detail: `${commitsBehind} commit(s) behind ${repoUrl}'s default branch`
|
|
9264
|
+
};
|
|
9265
|
+
} finally {
|
|
9266
|
+
git.cleanup(clone);
|
|
9267
|
+
}
|
|
9268
|
+
}
|
|
9269
|
+
async function checkViaContentDiff(name, pluginDir2, sourceDir, opts, git) {
|
|
9270
|
+
let cloneDir = null;
|
|
9271
|
+
let effectiveSourceDir = sourceDir;
|
|
9272
|
+
if (!opts.isLocalDir) {
|
|
9273
|
+
try {
|
|
9274
|
+
cloneDir = await git.cloneToTemp(sourceDir, "biffo-plugin-staleness-content");
|
|
9275
|
+
} catch {
|
|
9276
|
+
return {
|
|
9277
|
+
name,
|
|
9278
|
+
status: "cannot-tell",
|
|
9279
|
+
method: "unresolvable",
|
|
9280
|
+
detail: `could not clone ${sourceDir} for a content comparison (network or authentication failure)`
|
|
9281
|
+
};
|
|
9282
|
+
}
|
|
9283
|
+
effectiveSourceDir = cloneDir;
|
|
9284
|
+
}
|
|
9285
|
+
try {
|
|
9286
|
+
const filesDiffering = await countDifferingFiles(effectiveSourceDir, pluginDir2);
|
|
9287
|
+
const originDescription = opts.isLocalDir ? effectiveSourceDir : sourceDir;
|
|
9288
|
+
if (filesDiffering === 0) {
|
|
9289
|
+
return {
|
|
9290
|
+
name,
|
|
9291
|
+
status: "up-to-date",
|
|
9292
|
+
method: "content-diff",
|
|
9293
|
+
filesDiffering: 0,
|
|
9294
|
+
detail: `byte-identical to ${originDescription} (no provenance recorded, so an exact commit could not be named)`
|
|
9295
|
+
};
|
|
9296
|
+
}
|
|
9297
|
+
return {
|
|
9298
|
+
name,
|
|
9299
|
+
status: "behind",
|
|
9300
|
+
filesDiffering,
|
|
9301
|
+
method: "content-diff",
|
|
9302
|
+
detail: `${filesDiffering} file(s) differ from ${originDescription} (no provenance recorded, so an exact commit count could not be determined)`
|
|
9303
|
+
};
|
|
9304
|
+
} finally {
|
|
9305
|
+
if (cloneDir) git.cleanup(cloneDir);
|
|
9306
|
+
}
|
|
9307
|
+
}
|
|
9308
|
+
function shortSha(sha) {
|
|
9309
|
+
return sha.slice(0, 7);
|
|
9310
|
+
}
|
|
9311
|
+
async function countDifferingFiles(sourceDir, pluginDir2) {
|
|
9312
|
+
const sourceFiles = await sourceFileList(sourceDir);
|
|
9313
|
+
const vendorFiles = vendorFileList(pluginDir2);
|
|
9314
|
+
const allPaths = /* @__PURE__ */ new Set([...sourceFiles, ...vendorFiles]);
|
|
9315
|
+
let differing = 0;
|
|
9316
|
+
for (const relPath of allPaths) {
|
|
9317
|
+
if (relPath === PLUGIN_PROVENANCE_FILENAME) continue;
|
|
9318
|
+
const inSource = sourceFiles.has(relPath);
|
|
9319
|
+
const inVendor = vendorFiles.has(relPath);
|
|
9320
|
+
if (!inSource || !inVendor) {
|
|
9321
|
+
differing++;
|
|
9322
|
+
continue;
|
|
9323
|
+
}
|
|
9324
|
+
const a = readFileSync24(join34(sourceDir, relPath));
|
|
9325
|
+
const b = readFileSync24(join34(pluginDir2, relPath));
|
|
9326
|
+
if (!a.equals(b)) differing++;
|
|
9327
|
+
}
|
|
9328
|
+
return differing;
|
|
9329
|
+
}
|
|
9330
|
+
async function sourceFileList(dir) {
|
|
9331
|
+
if (await isGitWorkingTree2(dir)) {
|
|
9332
|
+
return new Set(await listGitFiles(dir));
|
|
9333
|
+
}
|
|
9334
|
+
return new Set(walkExcluding(dir, dir, LOCAL_COPY_EXCLUDES));
|
|
9335
|
+
}
|
|
9336
|
+
function vendorFileList(dir) {
|
|
9337
|
+
return new Set(walkExcluding(dir, dir, LOCAL_COPY_EXCLUDES));
|
|
9338
|
+
}
|
|
9339
|
+
function walkExcluding(root, dir, excludes) {
|
|
9340
|
+
if (!existsSync32(dir)) return [];
|
|
9341
|
+
const out = [];
|
|
9342
|
+
for (const entry of readdirSync14(dir)) {
|
|
9343
|
+
if (excludes.has(entry) || entry === ".git") continue;
|
|
9344
|
+
const full = join34(dir, entry);
|
|
9345
|
+
const stat = statSync7(full);
|
|
9346
|
+
if (stat.isDirectory()) {
|
|
9347
|
+
out.push(...walkExcluding(root, full, excludes));
|
|
9348
|
+
} else {
|
|
9349
|
+
out.push(relative4(root, full));
|
|
9350
|
+
}
|
|
9351
|
+
}
|
|
9352
|
+
return out;
|
|
9353
|
+
}
|
|
9354
|
+
function exitCodeForStaleness(results) {
|
|
9355
|
+
if (results.some((r) => r.status === "cannot-tell")) return 2;
|
|
9356
|
+
if (results.some((r) => r.status === "behind")) return 1;
|
|
9357
|
+
return 0;
|
|
9358
|
+
}
|
|
9359
|
+
function formatStalenessReport(results) {
|
|
9360
|
+
if (results.length === 0) {
|
|
9361
|
+
return " No vendored plugins under services/ \u2014 nothing to check.";
|
|
9362
|
+
}
|
|
9363
|
+
const lines = [""];
|
|
9364
|
+
for (const r of results) {
|
|
9365
|
+
const icon = r.status === "up-to-date" ? "\u2713" : r.status === "behind" ? "\u26A0" : "?";
|
|
9366
|
+
lines.push(` ${icon} ${r.name}: ${labelFor(r.status)} \u2014 ${r.detail}`);
|
|
9367
|
+
}
|
|
9368
|
+
lines.push("");
|
|
9369
|
+
return lines.join("\n");
|
|
9370
|
+
}
|
|
9371
|
+
function labelFor(status) {
|
|
9372
|
+
switch (status) {
|
|
9373
|
+
case "up-to-date":
|
|
9374
|
+
return "up to date";
|
|
9375
|
+
case "behind":
|
|
9376
|
+
return "BEHIND";
|
|
9377
|
+
case "cannot-tell":
|
|
9378
|
+
return "CANNOT TELL";
|
|
9379
|
+
}
|
|
9380
|
+
}
|
|
9381
|
+
|
|
9382
|
+
// src/commands/plugin-staleness.ts
|
|
9383
|
+
var pluginStalenessCommand = new Command17("staleness").description(
|
|
9384
|
+
"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."
|
|
9385
|
+
).option("--cwd <path>", "Project root to check (defaults to the current directory)").action(async (options) => {
|
|
9386
|
+
const cwd = options.cwd ? resolve14(options.cwd) : process.cwd();
|
|
9387
|
+
const results = await checkPluginStaleness(cwd, {
|
|
9388
|
+
registry: new RegistryAdapter(),
|
|
9389
|
+
git: new GitAdapter()
|
|
9390
|
+
});
|
|
9391
|
+
console.log(formatStalenessReport(results));
|
|
9392
|
+
process.exit(exitCodeForStaleness(results));
|
|
9393
|
+
});
|
|
9394
|
+
|
|
8959
9395
|
// src/commands/plugin-sync-migrations.ts
|
|
8960
|
-
import { existsSync as
|
|
8961
|
-
import { join as
|
|
9396
|
+
import { existsSync as existsSync33 } from "fs";
|
|
9397
|
+
import { join as join35, relative as relative5, resolve as resolve15 } from "path";
|
|
8962
9398
|
import chalk17 from "chalk";
|
|
8963
|
-
import { Command as
|
|
8964
|
-
var pluginSyncMigrationsCommand = new
|
|
9399
|
+
import { Command as Command18 } from "commander";
|
|
9400
|
+
var pluginSyncMigrationsCommand = new Command18("sync-migrations").description(
|
|
8965
9401
|
"Generate real, committed migration file(s) for installed-but-not-yet-migrated plugin(s): biffo plugin sync-migrations [name]"
|
|
8966
9402
|
).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(
|
|
8967
9403
|
async (name, options) => {
|
|
8968
|
-
const cwd = options.cwd ?
|
|
9404
|
+
const cwd = options.cwd ? resolve15(options.cwd) : process.cwd();
|
|
8969
9405
|
try {
|
|
8970
9406
|
await runPluginSyncMigrations(
|
|
8971
9407
|
name,
|
|
@@ -8979,11 +9415,11 @@ var pluginSyncMigrationsCommand = new Command17("sync-migrations").description(
|
|
|
8979
9415
|
}
|
|
8980
9416
|
);
|
|
8981
9417
|
async function runPluginSyncMigrations(name, options, deps) {
|
|
8982
|
-
const servicesDir =
|
|
8983
|
-
if (!
|
|
9418
|
+
const servicesDir = join35(options.cwd, "services");
|
|
9419
|
+
if (!existsSync33(servicesDir)) {
|
|
8984
9420
|
throw new Error(`${servicesDir} does not exist \u2014 is ${options.cwd} a Biffo project checkout?`);
|
|
8985
9421
|
}
|
|
8986
|
-
if (name && !
|
|
9422
|
+
if (name && !existsSync33(join35(servicesDir, name, "biffo.plugin.json"))) {
|
|
8987
9423
|
throw new Error(`Plugin '${name}' is not installed at services/${name}/.`);
|
|
8988
9424
|
}
|
|
8989
9425
|
if (options.dryRun) {
|
|
@@ -8999,7 +9435,7 @@ async function runPluginSyncMigrations(name, options, deps) {
|
|
|
8999
9435
|
);
|
|
9000
9436
|
return;
|
|
9001
9437
|
}
|
|
9002
|
-
const relativePaths = generated.map((p) =>
|
|
9438
|
+
const relativePaths = generated.map((p) => relative5(options.cwd, p));
|
|
9003
9439
|
for (const p of relativePaths) {
|
|
9004
9440
|
log.success(`Generated ${p}`);
|
|
9005
9441
|
}
|
|
@@ -9019,18 +9455,18 @@ async function runPluginSyncMigrations(name, options, deps) {
|
|
|
9019
9455
|
}
|
|
9020
9456
|
|
|
9021
9457
|
// src/commands/plugin-uninstall.ts
|
|
9022
|
-
import { existsSync as
|
|
9023
|
-
import { join as
|
|
9458
|
+
import { existsSync as existsSync34, readFileSync as readFileSync25, rmSync as rmSync9 } from "fs";
|
|
9459
|
+
import { join as join36, resolve as resolve16 } from "path";
|
|
9024
9460
|
import chalk18 from "chalk";
|
|
9025
|
-
import { Command as
|
|
9461
|
+
import { Command as Command19 } from "commander";
|
|
9026
9462
|
import inquirer6 from "inquirer";
|
|
9027
9463
|
var NAME_PATTERN2 = /^[a-z][a-z0-9-]*$/;
|
|
9028
|
-
var pluginUninstallCommand = new
|
|
9464
|
+
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(
|
|
9029
9465
|
"--keep-data",
|
|
9030
9466
|
"No-op today (see notes) \u2014 the CLI never drops plugin data regardless of this flag"
|
|
9031
9467
|
).option("--cwd <path>", "Project root to uninstall from (defaults to the current directory)").action(
|
|
9032
9468
|
async (name, options) => {
|
|
9033
|
-
const cwd = options.cwd ?
|
|
9469
|
+
const cwd = options.cwd ? resolve16(options.cwd) : process.cwd();
|
|
9034
9470
|
try {
|
|
9035
9471
|
await runPluginUninstall(
|
|
9036
9472
|
name,
|
|
@@ -9052,16 +9488,16 @@ async function runPluginUninstall(name, options, deps) {
|
|
|
9052
9488
|
if (!NAME_PATTERN2.test(name)) {
|
|
9053
9489
|
throw new Error(`Invalid plugin name '${name}'. Expected a lowercase kebab-case slug.`);
|
|
9054
9490
|
}
|
|
9055
|
-
const servicesDir =
|
|
9056
|
-
if (!
|
|
9491
|
+
const servicesDir = join36(options.cwd, "services");
|
|
9492
|
+
if (!existsSync34(servicesDir)) {
|
|
9057
9493
|
throw new Error(
|
|
9058
9494
|
`${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
|
|
9059
9495
|
);
|
|
9060
9496
|
}
|
|
9061
|
-
const targetDir =
|
|
9062
|
-
if (!
|
|
9063
|
-
const firstParty =
|
|
9064
|
-
if (
|
|
9497
|
+
const targetDir = join36(servicesDir, name);
|
|
9498
|
+
if (!existsSync34(targetDir)) {
|
|
9499
|
+
const firstParty = join36(servicesDir, FIRST_PARTY_PLUGINS_DIR, name);
|
|
9500
|
+
if (existsSync34(firstParty)) {
|
|
9065
9501
|
throw new Error(
|
|
9066
9502
|
`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.`
|
|
9067
9503
|
);
|
|
@@ -9069,9 +9505,9 @@ async function runPluginUninstall(name, options, deps) {
|
|
|
9069
9505
|
throw new Error(`Plugin '${name}' is not installed at services/${name}/.`);
|
|
9070
9506
|
}
|
|
9071
9507
|
const version = readInstalledVersion(targetDir);
|
|
9072
|
-
const modulesDir =
|
|
9508
|
+
const modulesDir = join36(options.cwd, "modules", "plugins", name);
|
|
9073
9509
|
const stagePaths = [`services/${name}`];
|
|
9074
|
-
if (
|
|
9510
|
+
if (existsSync34(modulesDir)) {
|
|
9075
9511
|
stagePaths.push(`modules/plugins/${name}`);
|
|
9076
9512
|
}
|
|
9077
9513
|
if (options.dryRun) {
|
|
@@ -9091,10 +9527,10 @@ async function runPluginUninstall(name, options, deps) {
|
|
|
9091
9527
|
`${options.cwd} is not a git repository \u2014 biffo plugin uninstall must be run from a Biffo project checkout.`
|
|
9092
9528
|
);
|
|
9093
9529
|
}
|
|
9094
|
-
|
|
9530
|
+
rmSync9(targetDir, { recursive: true, force: true });
|
|
9095
9531
|
log.success(`Removed services/${name}/`);
|
|
9096
|
-
if (
|
|
9097
|
-
|
|
9532
|
+
if (existsSync34(modulesDir)) {
|
|
9533
|
+
rmSync9(modulesDir, { recursive: true, force: true });
|
|
9098
9534
|
log.success(`Removed modules/plugins/${name}/`);
|
|
9099
9535
|
const wiring = syncPluginTerraform(options.cwd);
|
|
9100
9536
|
stagePaths.push(...wiring.changedPaths);
|
|
@@ -9128,12 +9564,17 @@ async function runPluginUninstall(name, options, deps) {
|
|
|
9128
9564
|
"Any tables this plugin created remain in the database, and its migration file at services/api/migrations/versions/ is NOT removed (it is a permanent historical record \u2014 see notes). Dropping tables, if desired, requires a manual Alembic migration written against the Core API."
|
|
9129
9565
|
);
|
|
9130
9566
|
}
|
|
9567
|
+
if (existsSync34(join36(options.cwd, pluginSeedImportDir(name)))) {
|
|
9568
|
+
log.warn(
|
|
9569
|
+
`${pluginSeedImportDir(name)}/ (this plugin's vendored baseline-row seed, biffo-template#1554) was NOT removed either, for the same reason \u2014 see notes. Delete it by hand if you are certain the rows it applied should go too, but note nothing drops rows already applied to the database; that still needs a manual migration.`
|
|
9570
|
+
);
|
|
9571
|
+
}
|
|
9131
9572
|
}
|
|
9132
9573
|
function readInstalledVersion(targetDir) {
|
|
9133
|
-
const manifestPath =
|
|
9134
|
-
if (!
|
|
9574
|
+
const manifestPath = join36(targetDir, "biffo.plugin.json");
|
|
9575
|
+
if (!existsSync34(manifestPath)) return void 0;
|
|
9135
9576
|
try {
|
|
9136
|
-
return validateManifest(JSON.parse(
|
|
9577
|
+
return validateManifest(JSON.parse(readFileSync25(manifestPath, "utf8"))).version;
|
|
9137
9578
|
} catch {
|
|
9138
9579
|
return void 0;
|
|
9139
9580
|
}
|
|
@@ -9166,12 +9607,12 @@ function printDryRun5(name, version, stagePaths, keepData) {
|
|
|
9166
9607
|
}
|
|
9167
9608
|
|
|
9168
9609
|
// src/commands/plugin-upgrade.ts
|
|
9169
|
-
import { cpSync as
|
|
9170
|
-
import { join as
|
|
9610
|
+
import { cpSync as cpSync6, existsSync as existsSync35, mkdirSync as mkdirSync12, readFileSync as readFileSync26, rmSync as rmSync10 } from "fs";
|
|
9611
|
+
import { join as join37, relative as relative6, resolve as resolve17 } from "path";
|
|
9171
9612
|
import chalk19 from "chalk";
|
|
9172
|
-
import { Command as
|
|
9613
|
+
import { Command as Command20 } from "commander";
|
|
9173
9614
|
import inquirer7 from "inquirer";
|
|
9174
|
-
var pluginUpgradeCommand = new
|
|
9615
|
+
var pluginUpgradeCommand = new Command20("upgrade").description(
|
|
9175
9616
|
"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>)"
|
|
9176
9617
|
).argument(
|
|
9177
9618
|
"[target]",
|
|
@@ -9181,12 +9622,12 @@ var pluginUpgradeCommand = new Command19("upgrade").description(
|
|
|
9181
9622
|
"Refresh the installed plugin from a local, unpublished checkout instead of the registry"
|
|
9182
9623
|
).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(
|
|
9183
9624
|
async (target, options) => {
|
|
9184
|
-
const cwd = options.cwd ?
|
|
9625
|
+
const cwd = options.cwd ? resolve17(options.cwd) : process.cwd();
|
|
9185
9626
|
try {
|
|
9186
9627
|
await runPluginUpgrade(
|
|
9187
9628
|
target,
|
|
9188
9629
|
{
|
|
9189
|
-
...options.local ? { local:
|
|
9630
|
+
...options.local ? { local: resolve17(options.local) } : {},
|
|
9190
9631
|
dryRun: options.dryRun ?? false,
|
|
9191
9632
|
force: options.force ?? false,
|
|
9192
9633
|
cwd
|
|
@@ -9214,8 +9655,8 @@ async function runPluginUpgrade(target, options, deps) {
|
|
|
9214
9655
|
`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\`).`
|
|
9215
9656
|
);
|
|
9216
9657
|
}
|
|
9217
|
-
const servicesDir =
|
|
9218
|
-
if (!
|
|
9658
|
+
const servicesDir = join37(options.cwd, "services");
|
|
9659
|
+
if (!existsSync35(servicesDir)) {
|
|
9219
9660
|
throw new Error(
|
|
9220
9661
|
`${servicesDir} does not exist \u2014 is ${options.cwd} the root of a Biffo project checkout?`
|
|
9221
9662
|
);
|
|
@@ -9224,8 +9665,8 @@ async function runPluginUpgrade(target, options, deps) {
|
|
|
9224
9665
|
return runLocalPluginRefresh(options.local, options, deps);
|
|
9225
9666
|
}
|
|
9226
9667
|
const { name, minor } = parsePluginTarget(target);
|
|
9227
|
-
const targetDir =
|
|
9228
|
-
if (!
|
|
9668
|
+
const targetDir = join37(servicesDir, name);
|
|
9669
|
+
if (!existsSync35(targetDir)) {
|
|
9229
9670
|
throw new Error(
|
|
9230
9671
|
`Plugin '${name}' is not installed at services/${name}/. Use 'biffo plugin install ${name}@${minor}' instead.`
|
|
9231
9672
|
);
|
|
@@ -9239,7 +9680,7 @@ async function runPluginUpgrade(target, options, deps) {
|
|
|
9239
9680
|
`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.`
|
|
9240
9681
|
);
|
|
9241
9682
|
}
|
|
9242
|
-
const modulesDir =
|
|
9683
|
+
const modulesDir = join37(options.cwd, "modules", "plugins", entry.name);
|
|
9243
9684
|
if (options.dryRun) {
|
|
9244
9685
|
printDryRun6(entry, currentVersion);
|
|
9245
9686
|
return;
|
|
@@ -9267,19 +9708,25 @@ async function runPluginUpgrade(target, options, deps) {
|
|
|
9267
9708
|
log.success(
|
|
9268
9709
|
`Manifest valid \u2014 ${manifest.tables.length} table(s), ${manifest.api_routes.length} route(s)`
|
|
9269
9710
|
);
|
|
9270
|
-
|
|
9271
|
-
|
|
9272
|
-
|
|
9711
|
+
const previousProvenance = readProvenance(targetDir);
|
|
9712
|
+
rmSync10(targetDir, { recursive: true, force: true });
|
|
9713
|
+
mkdirSync12(targetDir, { recursive: true });
|
|
9714
|
+
cpSync6(tmpDir, targetDir, { recursive: true });
|
|
9273
9715
|
log.success(`Upgraded plugin source at services/${entry.name}/`);
|
|
9716
|
+
const nextProvenance = resolveRegistryProvenance(
|
|
9717
|
+
entry.repo,
|
|
9718
|
+
await deps.git.resolveDefaultBranchSha(entry.repo)
|
|
9719
|
+
);
|
|
9720
|
+
writePluginProvenance(targetDir, reconcileProvenance(previousProvenance, nextProvenance));
|
|
9274
9721
|
applyWorkspaceSources(targetDir, options.cwd, `services/${entry.name}`);
|
|
9275
9722
|
const stagePaths = [`services/${entry.name}`];
|
|
9276
|
-
if (
|
|
9277
|
-
|
|
9723
|
+
if (existsSync35(modulesDir)) {
|
|
9724
|
+
rmSync10(modulesDir, { recursive: true, force: true });
|
|
9278
9725
|
}
|
|
9279
|
-
const tfSourceDir =
|
|
9280
|
-
if (
|
|
9281
|
-
|
|
9282
|
-
|
|
9726
|
+
const tfSourceDir = join37(targetDir, "terraform");
|
|
9727
|
+
if (existsSync35(tfSourceDir)) {
|
|
9728
|
+
mkdirSync12(modulesDir, { recursive: true });
|
|
9729
|
+
cpSync6(tfSourceDir, modulesDir, { recursive: true });
|
|
9283
9730
|
stagePaths.push(`modules/plugins/${entry.name}`);
|
|
9284
9731
|
log.success(`Copied Terraform module to modules/plugins/${entry.name}/`);
|
|
9285
9732
|
}
|
|
@@ -9289,16 +9736,20 @@ async function runPluginUpgrade(target, options, deps) {
|
|
|
9289
9736
|
);
|
|
9290
9737
|
const generatedPaths = await deps.migrations.generate(options.cwd, [entry.name]);
|
|
9291
9738
|
for (const absPath of generatedPaths) {
|
|
9292
|
-
stagePaths.push(
|
|
9739
|
+
stagePaths.push(relative6(options.cwd, absPath));
|
|
9293
9740
|
}
|
|
9294
9741
|
if (generatedPaths.length > 0) {
|
|
9295
|
-
log.success(`Generated migration: ${
|
|
9742
|
+
log.success(`Generated migration: ${relative6(options.cwd, generatedPaths[0])}`);
|
|
9296
9743
|
} else {
|
|
9297
9744
|
log.info(
|
|
9298
9745
|
`${entry.name}'s tables and columns already match the manifest \u2014 no migration needed.`
|
|
9299
9746
|
);
|
|
9300
9747
|
}
|
|
9301
9748
|
}
|
|
9749
|
+
const seedResult = vendorPluginSeed(targetDir, manifest, options.cwd);
|
|
9750
|
+
if (seedResult.vendored) {
|
|
9751
|
+
stagePaths.push(seedResult.stagedPath);
|
|
9752
|
+
}
|
|
9302
9753
|
const label = currentVersion ? `${entry.name} ${currentVersion} -> ${entry.version}` : `${entry.name} to ${entry.version}`;
|
|
9303
9754
|
const commitMessage = `feat(plugins): upgrade ${label}`;
|
|
9304
9755
|
await deps.git.add(options.cwd, stagePaths);
|
|
@@ -9317,16 +9768,16 @@ async function runPluginUpgrade(target, options, deps) {
|
|
|
9317
9768
|
async function runLocalPluginRefresh(localPath, options, deps) {
|
|
9318
9769
|
const source = resolveLocalPlugin(localPath);
|
|
9319
9770
|
log.success(`Resolved ${source.name}@${source.version} from ${source.origin}`);
|
|
9320
|
-
const servicesDir =
|
|
9321
|
-
const targetDir =
|
|
9322
|
-
if (!
|
|
9771
|
+
const servicesDir = join37(options.cwd, "services");
|
|
9772
|
+
const targetDir = join37(servicesDir, source.name);
|
|
9773
|
+
if (!existsSync35(targetDir)) {
|
|
9323
9774
|
throw new Error(
|
|
9324
9775
|
`Plugin '${source.name}' is not installed at services/${source.name}/. Use 'biffo plugin install --local ${localPath}' instead.`
|
|
9325
9776
|
);
|
|
9326
9777
|
}
|
|
9327
|
-
const inTreeSource =
|
|
9778
|
+
const inTreeSource = resolve17(source.sourceDir) === resolve17(targetDir);
|
|
9328
9779
|
const currentVersion = readInstalledVersion2(targetDir);
|
|
9329
|
-
const modulesDir =
|
|
9780
|
+
const modulesDir = join37(options.cwd, "modules", "plugins", source.name);
|
|
9330
9781
|
if (options.dryRun) {
|
|
9331
9782
|
printLocalDryRun(source, currentVersion, inTreeSource);
|
|
9332
9783
|
return;
|
|
@@ -9349,25 +9800,28 @@ async function runLocalPluginRefresh(localPath, options, deps) {
|
|
|
9349
9800
|
log.success(
|
|
9350
9801
|
`Manifest valid \u2014 ${manifest.tables.length} table(s), ${manifest.api_routes.length} route(s)`
|
|
9351
9802
|
);
|
|
9803
|
+
const previousProvenance = readProvenance(targetDir);
|
|
9352
9804
|
if (inTreeSource) {
|
|
9353
9805
|
log.info(
|
|
9354
9806
|
`services/${source.name}/ is already the local checkout \u2014 nothing to copy; re-syncing its Terraform module and checking for a migration.`
|
|
9355
9807
|
);
|
|
9356
9808
|
} else {
|
|
9357
|
-
|
|
9358
|
-
|
|
9809
|
+
rmSync10(targetDir, { recursive: true, force: true });
|
|
9810
|
+
mkdirSync12(targetDir, { recursive: true });
|
|
9359
9811
|
await copyPluginSource(source.sourceDir, targetDir);
|
|
9360
9812
|
log.success(`Refreshed plugin source at services/${source.name}/ from ${source.origin}`);
|
|
9361
9813
|
}
|
|
9814
|
+
const nextProvenance = inTreeSource ? inTreePluginProvenance(`services/${source.name}`) : await resolveLocalProvenance(source.sourceDir, source.origin);
|
|
9815
|
+
writePluginProvenance(targetDir, reconcileProvenance(previousProvenance, nextProvenance));
|
|
9362
9816
|
applyWorkspaceSources(targetDir, options.cwd, `services/${source.name}`);
|
|
9363
9817
|
const stagePaths = [`services/${source.name}`];
|
|
9364
|
-
if (
|
|
9365
|
-
|
|
9818
|
+
if (existsSync35(modulesDir)) {
|
|
9819
|
+
rmSync10(modulesDir, { recursive: true, force: true });
|
|
9366
9820
|
}
|
|
9367
|
-
const tfSourceDir =
|
|
9368
|
-
if (
|
|
9369
|
-
|
|
9370
|
-
|
|
9821
|
+
const tfSourceDir = join37(targetDir, "terraform");
|
|
9822
|
+
if (existsSync35(tfSourceDir)) {
|
|
9823
|
+
mkdirSync12(modulesDir, { recursive: true });
|
|
9824
|
+
cpSync6(tfSourceDir, modulesDir, { recursive: true });
|
|
9371
9825
|
stagePaths.push(`modules/plugins/${source.name}`);
|
|
9372
9826
|
log.success(`Refreshed Terraform module at modules/plugins/${source.name}/`);
|
|
9373
9827
|
}
|
|
@@ -9377,10 +9831,10 @@ async function runLocalPluginRefresh(localPath, options, deps) {
|
|
|
9377
9831
|
);
|
|
9378
9832
|
const generatedPaths = await deps.migrations.generate(options.cwd, [source.name]);
|
|
9379
9833
|
for (const absPath of generatedPaths) {
|
|
9380
|
-
stagePaths.push(
|
|
9834
|
+
stagePaths.push(relative6(options.cwd, absPath));
|
|
9381
9835
|
}
|
|
9382
9836
|
if (generatedPaths.length > 0) {
|
|
9383
|
-
log.success(`Generated migration: ${
|
|
9837
|
+
log.success(`Generated migration: ${relative6(options.cwd, generatedPaths[0])}`);
|
|
9384
9838
|
} else {
|
|
9385
9839
|
log.info(
|
|
9386
9840
|
`${source.name}'s tables and columns already match the manifest \u2014 no migration needed.`
|
|
@@ -9389,6 +9843,10 @@ async function runLocalPluginRefresh(localPath, options, deps) {
|
|
|
9389
9843
|
} else {
|
|
9390
9844
|
log.info(`${source.name} declares no tables \u2014 nothing to migrate.`);
|
|
9391
9845
|
}
|
|
9846
|
+
const seedResult = vendorPluginSeed(targetDir, manifest, options.cwd);
|
|
9847
|
+
if (seedResult.vendored) {
|
|
9848
|
+
stagePaths.push(seedResult.stagedPath);
|
|
9849
|
+
}
|
|
9392
9850
|
await deps.git.add(options.cwd, stagePaths);
|
|
9393
9851
|
if (!await deps.git.hasUncommittedChanges(options.cwd)) {
|
|
9394
9852
|
log.warn(`services/${source.name}/ already matches ${source.origin} \u2014 nothing to commit.`);
|
|
@@ -9408,10 +9866,10 @@ async function runLocalPluginRefresh(localPath, options, deps) {
|
|
|
9408
9866
|
}
|
|
9409
9867
|
}
|
|
9410
9868
|
function readInstalledVersion2(targetDir) {
|
|
9411
|
-
const manifestPath =
|
|
9412
|
-
if (!
|
|
9869
|
+
const manifestPath = join37(targetDir, "biffo.plugin.json");
|
|
9870
|
+
if (!existsSync35(manifestPath)) return void 0;
|
|
9413
9871
|
try {
|
|
9414
|
-
return validateManifest(JSON.parse(
|
|
9872
|
+
return validateManifest(JSON.parse(readFileSync26(manifestPath, "utf8"))).version;
|
|
9415
9873
|
} catch {
|
|
9416
9874
|
return void 0;
|
|
9417
9875
|
}
|
|
@@ -9446,6 +9904,11 @@ function printDryRun6(entry, currentVersion) {
|
|
|
9446
9904
|
` Would replace Terraform module at: modules/plugins/${entry.name}/ (if the repo has one)`
|
|
9447
9905
|
);
|
|
9448
9906
|
}
|
|
9907
|
+
if (entry.baseline_tables && entry.baseline_tables.length > 0) {
|
|
9908
|
+
console.log(
|
|
9909
|
+
` Would re-vendor seed DDL into: ${pluginSeedImportDir(entry.name)}/ (baseline_tables: ${entry.baseline_tables.join(", ")})`
|
|
9910
|
+
);
|
|
9911
|
+
}
|
|
9449
9912
|
console.log(` Would commit: feat(plugins): upgrade ${entry.name} to ${entry.version}
|
|
9450
9913
|
`);
|
|
9451
9914
|
}
|
|
@@ -9465,12 +9928,17 @@ function printLocalDryRun(source, currentVersion, inTreeSource) {
|
|
|
9465
9928
|
` Would check for a migration for ${source.manifest.tables.length} table(s) (generated for a new table or an added column on an already-migrated table; a removed/retyped/nullability-changed column stops the refresh instead \u2014 #1539)`
|
|
9466
9929
|
);
|
|
9467
9930
|
}
|
|
9931
|
+
if (source.manifest.seed) {
|
|
9932
|
+
console.log(
|
|
9933
|
+
` Would re-vendor seed DDL into: ${pluginSeedImportDir(source.name)}/ (baseline_tables: ${source.manifest.seed.baseline_tables.join(", ") || "none declared"})`
|
|
9934
|
+
);
|
|
9935
|
+
}
|
|
9468
9936
|
console.log(` Would commit: chore(plugins): refresh ${source.name} from local checkout
|
|
9469
9937
|
`);
|
|
9470
9938
|
}
|
|
9471
9939
|
|
|
9472
9940
|
// src/commands/plugin.ts
|
|
9473
|
-
var pluginCommand = new
|
|
9941
|
+
var pluginCommand = new Command21("plugin").description("Manage Biffo plugins");
|
|
9474
9942
|
pluginCommand.addCommand(pluginCreateCommand);
|
|
9475
9943
|
pluginCommand.addCommand(pluginListCommand);
|
|
9476
9944
|
pluginCommand.addCommand(pluginInstallCommand);
|
|
@@ -9478,15 +9946,16 @@ pluginCommand.addCommand(pluginUninstallCommand);
|
|
|
9478
9946
|
pluginCommand.addCommand(pluginUpgradeCommand);
|
|
9479
9947
|
pluginCommand.addCommand(pluginSyncMigrationsCommand);
|
|
9480
9948
|
pluginCommand.addCommand(pluginInfoCommand);
|
|
9949
|
+
pluginCommand.addCommand(pluginStalenessCommand);
|
|
9481
9950
|
|
|
9482
9951
|
// src/commands/sibling.ts
|
|
9483
|
-
import { Command as
|
|
9952
|
+
import { Command as Command23 } from "commander";
|
|
9484
9953
|
|
|
9485
9954
|
// src/commands/sibling-check-identity.ts
|
|
9486
|
-
import { existsSync as
|
|
9487
|
-
import { resolve as
|
|
9955
|
+
import { existsSync as existsSync36, readFileSync as readFileSync27 } from "fs";
|
|
9956
|
+
import { resolve as resolve18 } from "path";
|
|
9488
9957
|
import chalk20 from "chalk";
|
|
9489
|
-
import { Command as
|
|
9958
|
+
import { Command as Command22 } from "commander";
|
|
9490
9959
|
|
|
9491
9960
|
// src/lib/sibling-identity-check.ts
|
|
9492
9961
|
function checkSiblingIdentity(envs) {
|
|
@@ -9538,7 +10007,7 @@ function checkSiblingIdentity(envs) {
|
|
|
9538
10007
|
// src/commands/sibling-check-identity.ts
|
|
9539
10008
|
var VALID_ENVIRONMENTS2 = ["dev", "staging", "prod"];
|
|
9540
10009
|
var SIBLING_CORE_POOL_VAR = "CORE_COGNITO_USER_POOL_ID";
|
|
9541
|
-
var siblingCheckIdentityCommand = new
|
|
10010
|
+
var siblingCheckIdentityCommand = new Command22("check-identity").description(
|
|
9542
10011
|
"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."
|
|
9543
10012
|
).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) => {
|
|
9544
10013
|
if (options.env && !VALID_ENVIRONMENTS2.includes(options.env)) {
|
|
@@ -9677,7 +10146,7 @@ async function fetchPublishedIdentity(portalUrl) {
|
|
|
9677
10146
|
}
|
|
9678
10147
|
async function resolveConfig4(options) {
|
|
9679
10148
|
if (options.config) {
|
|
9680
|
-
const raw = JSON.parse(
|
|
10149
|
+
const raw = JSON.parse(readFileSync27(resolve18(options.config), "utf8"));
|
|
9681
10150
|
const result = BiffoConfigSchema.safeParse(raw);
|
|
9682
10151
|
if (!result.success) {
|
|
9683
10152
|
log.error(`Invalid config at ${options.config}:`);
|
|
@@ -9696,9 +10165,9 @@ async function resolveConfig4(options) {
|
|
|
9696
10165
|
}
|
|
9697
10166
|
return cfg;
|
|
9698
10167
|
}
|
|
9699
|
-
const localConfigPath =
|
|
9700
|
-
if (
|
|
9701
|
-
const raw = JSON.parse(
|
|
10168
|
+
const localConfigPath = resolve18(process.cwd(), "biffo.config.json");
|
|
10169
|
+
if (existsSync36(localConfigPath)) {
|
|
10170
|
+
const raw = JSON.parse(readFileSync27(localConfigPath, "utf8"));
|
|
9702
10171
|
const result = BiffoConfigSchema.safeParse(raw);
|
|
9703
10172
|
if (result.success) return result.data;
|
|
9704
10173
|
if (isTemplatePlaceholderConfig(raw)) {
|
|
@@ -9734,31 +10203,31 @@ async function resolveConfig4(options) {
|
|
|
9734
10203
|
}
|
|
9735
10204
|
|
|
9736
10205
|
// src/commands/sibling.ts
|
|
9737
|
-
var siblingCommand = new
|
|
10206
|
+
var siblingCommand = new Command23("sibling").description(
|
|
9738
10207
|
"Create and manage sibling apps that share a Biffo core project (ADR-0007)"
|
|
9739
10208
|
);
|
|
9740
10209
|
siblingCommand.addCommand(siblingCreateCommand);
|
|
9741
10210
|
siblingCommand.addCommand(siblingCheckIdentityCommand);
|
|
9742
10211
|
|
|
9743
10212
|
// src/commands/check.ts
|
|
9744
|
-
import { Command as
|
|
10213
|
+
import { Command as Command24 } from "commander";
|
|
9745
10214
|
|
|
9746
10215
|
// src/scripts/check-adr-numbering.ts
|
|
9747
|
-
import { existsSync as
|
|
9748
|
-
import { join as
|
|
9749
|
-
import { execa as
|
|
10216
|
+
import { existsSync as existsSync38 } from "fs";
|
|
10217
|
+
import { join as join39 } from "path";
|
|
10218
|
+
import { execa as execa7 } from "execa";
|
|
9750
10219
|
|
|
9751
10220
|
// src/lib/adr-numbering-guard.ts
|
|
9752
|
-
import { existsSync as
|
|
9753
|
-
import { join as
|
|
10221
|
+
import { existsSync as existsSync37, readdirSync as readdirSync15, readFileSync as readFileSync28 } from "fs";
|
|
10222
|
+
import { join as join38 } from "path";
|
|
9754
10223
|
var ADR_FILENAME = /^(\d{4})-.+\.md$/;
|
|
9755
10224
|
var ALLOWLIST_FILENAME = ".numbering-allowlist";
|
|
9756
10225
|
var TEMPLATE_ADR_RESERVED_UPTO = "0099";
|
|
9757
10226
|
function readAdrNumberingAllowlist(adrDir) {
|
|
9758
|
-
const path =
|
|
9759
|
-
if (!
|
|
10227
|
+
const path = join38(adrDir, ALLOWLIST_FILENAME);
|
|
10228
|
+
if (!existsSync37(path)) return /* @__PURE__ */ new Set();
|
|
9760
10229
|
const numbers = /* @__PURE__ */ new Set();
|
|
9761
|
-
for (const rawLine of
|
|
10230
|
+
for (const rawLine of readFileSync28(path, "utf8").split("\n")) {
|
|
9762
10231
|
const line = rawLine.split("#")[0].trim();
|
|
9763
10232
|
if (line) numbers.add(line);
|
|
9764
10233
|
}
|
|
@@ -9766,8 +10235,8 @@ function readAdrNumberingAllowlist(adrDir) {
|
|
|
9766
10235
|
}
|
|
9767
10236
|
function adrNumbersIn(adrDir) {
|
|
9768
10237
|
const claims = /* @__PURE__ */ new Map();
|
|
9769
|
-
if (!
|
|
9770
|
-
for (const entry of
|
|
10238
|
+
if (!existsSync37(adrDir)) return claims;
|
|
10239
|
+
for (const entry of readdirSync15(adrDir).sort()) {
|
|
9771
10240
|
const match = ADR_FILENAME.exec(entry);
|
|
9772
10241
|
if (!match) continue;
|
|
9773
10242
|
const number = match[1];
|
|
@@ -9820,9 +10289,9 @@ function formatAdrReservedRangeViolations(violations, reservedUpTo = TEMPLATE_AD
|
|
|
9820
10289
|
|
|
9821
10290
|
// src/scripts/check-adr-numbering.ts
|
|
9822
10291
|
async function runAdrNumberingCheck() {
|
|
9823
|
-
const root = (await
|
|
9824
|
-
const adrDir =
|
|
9825
|
-
if (!
|
|
10292
|
+
const root = (await execa7("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
10293
|
+
const adrDir = join39(root, "docs", "ADR");
|
|
10294
|
+
if (!existsSync38(adrDir)) {
|
|
9826
10295
|
console.log("\u2713 ADR numbering guard: no docs/ADR/ directory \u2014 nothing to compare");
|
|
9827
10296
|
return;
|
|
9828
10297
|
}
|
|
@@ -9861,7 +10330,7 @@ Already accepted? List it in docs/ADR/${ALLOWLIST_FILENAME} instead of leaving t
|
|
|
9861
10330
|
|
|
9862
10331
|
// src/scripts/check-branch-protection.ts
|
|
9863
10332
|
import { Octokit as Octokit2 } from "@octokit/rest";
|
|
9864
|
-
import { execa as
|
|
10333
|
+
import { execa as execa8 } from "execa";
|
|
9865
10334
|
|
|
9866
10335
|
// src/lib/branch-protection-apply.ts
|
|
9867
10336
|
var CONTEXT_CONSISTENCY_THRESHOLD = 2 / 3;
|
|
@@ -9988,7 +10457,7 @@ async function resolveRepo(explicit) {
|
|
|
9988
10457
|
}
|
|
9989
10458
|
return { owner, repo };
|
|
9990
10459
|
}
|
|
9991
|
-
const { stdout } = await
|
|
10460
|
+
const { stdout } = await execa8("git", ["remote", "get-url", "origin"]);
|
|
9992
10461
|
const m = /github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/.exec(stdout.trim());
|
|
9993
10462
|
if (!m?.[1] || !m[2]) {
|
|
9994
10463
|
console.error(
|
|
@@ -10120,13 +10589,13 @@ async function runBranchProtectionCheck(explicitRepo, options = {}) {
|
|
|
10120
10589
|
}
|
|
10121
10590
|
|
|
10122
10591
|
// src/scripts/check-codeql-suppression.ts
|
|
10123
|
-
import { existsSync as
|
|
10124
|
-
import { join as
|
|
10125
|
-
import { execa as
|
|
10592
|
+
import { existsSync as existsSync39 } from "fs";
|
|
10593
|
+
import { join as join41, relative as relative7 } from "path";
|
|
10594
|
+
import { execa as execa9 } from "execa";
|
|
10126
10595
|
|
|
10127
10596
|
// src/lib/codeql-suppression-guard.ts
|
|
10128
|
-
import { readdirSync as
|
|
10129
|
-
import { join as
|
|
10597
|
+
import { readdirSync as readdirSync16, readFileSync as readFileSync29, statSync as statSync8 } from "fs";
|
|
10598
|
+
import { join as join40 } from "path";
|
|
10130
10599
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
10131
10600
|
".git",
|
|
10132
10601
|
".worktrees",
|
|
@@ -10152,15 +10621,15 @@ function walkSourceFiles(root) {
|
|
|
10152
10621
|
const walk2 = (dir) => {
|
|
10153
10622
|
let entries;
|
|
10154
10623
|
try {
|
|
10155
|
-
entries =
|
|
10624
|
+
entries = readdirSync16(dir);
|
|
10156
10625
|
} catch {
|
|
10157
10626
|
return;
|
|
10158
10627
|
}
|
|
10159
10628
|
for (const entry of entries) {
|
|
10160
|
-
const p =
|
|
10629
|
+
const p = join40(dir, entry);
|
|
10161
10630
|
let st;
|
|
10162
10631
|
try {
|
|
10163
|
-
st =
|
|
10632
|
+
st = statSync8(p);
|
|
10164
10633
|
} catch {
|
|
10165
10634
|
continue;
|
|
10166
10635
|
}
|
|
@@ -10183,7 +10652,7 @@ function countSourceFiles(root) {
|
|
|
10183
10652
|
function sweepCodeqlSuppressionComments(root) {
|
|
10184
10653
|
const hits = [];
|
|
10185
10654
|
for (const path of walkSourceFiles(root)) {
|
|
10186
|
-
const text =
|
|
10655
|
+
const text = readFileSync29(path, "utf8");
|
|
10187
10656
|
for (const line of findCodeqlSuppressionComments(text)) {
|
|
10188
10657
|
hits.push({ path, line, text: text.split("\n")[line - 1] ?? "" });
|
|
10189
10658
|
}
|
|
@@ -10193,9 +10662,9 @@ function sweepCodeqlSuppressionComments(root) {
|
|
|
10193
10662
|
|
|
10194
10663
|
// src/scripts/check-codeql-suppression.ts
|
|
10195
10664
|
async function runCodeqlSuppressionCheck() {
|
|
10196
|
-
const root = (await
|
|
10197
|
-
const scanRoot =
|
|
10198
|
-
if (!
|
|
10665
|
+
const root = (await execa9("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
10666
|
+
const scanRoot = join41(root, "cli", "src");
|
|
10667
|
+
if (!existsSync39(scanRoot)) {
|
|
10199
10668
|
console.log(
|
|
10200
10669
|
"\u2014 codeql-suppression guard: skipped \u2014 no cli/src in this repo, so there is no CLI source to scan."
|
|
10201
10670
|
);
|
|
@@ -10207,7 +10676,7 @@ async function runCodeqlSuppressionCheck() {
|
|
|
10207
10676
|
"\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"
|
|
10208
10677
|
);
|
|
10209
10678
|
for (const hit of hits) {
|
|
10210
|
-
console.error(` ${
|
|
10679
|
+
console.error(` ${relative7(root, hit.path)}:${hit.line} ${hit.text.trim()}`);
|
|
10211
10680
|
}
|
|
10212
10681
|
process.exit(1);
|
|
10213
10682
|
}
|
|
@@ -10219,11 +10688,11 @@ async function runCodeqlSuppressionCheck() {
|
|
|
10219
10688
|
}
|
|
10220
10689
|
|
|
10221
10690
|
// src/scripts/check-cognito-invite-template.ts
|
|
10222
|
-
import { execa as
|
|
10691
|
+
import { execa as execa10 } from "execa";
|
|
10223
10692
|
|
|
10224
10693
|
// src/lib/cognito-invite-template-guard.ts
|
|
10225
|
-
import { readdirSync as
|
|
10226
|
-
import { join as
|
|
10694
|
+
import { readdirSync as readdirSync17, readFileSync as readFileSync30, statSync as statSync9 } from "fs";
|
|
10695
|
+
import { join as join42 } from "path";
|
|
10227
10696
|
var REQUIRED_INVITE_MEMBERS = ["email_subject", "email_message", "sms_message"];
|
|
10228
10697
|
var REQUIRED_INVITE_PLACEHOLDERS = ["{username}", "{####}"];
|
|
10229
10698
|
var PLACEHOLDER_MEMBERS = ["email_message", "sms_message"];
|
|
@@ -10297,36 +10766,36 @@ function memberBody(blockBody, member) {
|
|
|
10297
10766
|
}
|
|
10298
10767
|
function findModuleTerraformFiles(repoRoot) {
|
|
10299
10768
|
const found = [];
|
|
10300
|
-
const walk2 = (dir,
|
|
10769
|
+
const walk2 = (dir, relative10) => {
|
|
10301
10770
|
let entries;
|
|
10302
10771
|
try {
|
|
10303
|
-
entries =
|
|
10772
|
+
entries = readdirSync17(dir);
|
|
10304
10773
|
} catch {
|
|
10305
10774
|
return;
|
|
10306
10775
|
}
|
|
10307
10776
|
for (const entry of entries) {
|
|
10308
10777
|
if (entry === "node_modules" || entry === ".git" || entry === ".worktrees") continue;
|
|
10309
|
-
const full =
|
|
10310
|
-
const rel = `${
|
|
10311
|
-
if (
|
|
10778
|
+
const full = join42(dir, entry);
|
|
10779
|
+
const rel = `${relative10}/${entry}`;
|
|
10780
|
+
if (statSync9(full).isDirectory()) {
|
|
10312
10781
|
walk2(full, rel);
|
|
10313
10782
|
} else if (entry.endsWith(".tf")) {
|
|
10314
10783
|
found.push(rel);
|
|
10315
10784
|
}
|
|
10316
10785
|
}
|
|
10317
10786
|
};
|
|
10318
|
-
walk2(
|
|
10787
|
+
walk2(join42(repoRoot, "modules"), "modules");
|
|
10319
10788
|
return found.sort();
|
|
10320
10789
|
}
|
|
10321
10790
|
function checkCognitoInviteTemplates(repoRoot) {
|
|
10322
10791
|
return findModuleTerraformFiles(repoRoot).flatMap(
|
|
10323
|
-
(file) => checkInviteTemplateSource(file,
|
|
10792
|
+
(file) => checkInviteTemplateSource(file, readFileSync30(join42(repoRoot, file), "utf8"))
|
|
10324
10793
|
);
|
|
10325
10794
|
}
|
|
10326
10795
|
|
|
10327
10796
|
// src/scripts/check-cognito-invite-template.ts
|
|
10328
10797
|
async function runCognitoInviteTemplateCheck() {
|
|
10329
|
-
const root = (await
|
|
10798
|
+
const root = (await execa10("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
10330
10799
|
const files = findModuleTerraformFiles(root);
|
|
10331
10800
|
console.log(`audited ${files.length} .tf file(s) under modules/ under ${root}`);
|
|
10332
10801
|
if (files.length === 0) {
|
|
@@ -10348,12 +10817,12 @@ async function runCognitoInviteTemplateCheck() {
|
|
|
10348
10817
|
}
|
|
10349
10818
|
|
|
10350
10819
|
// src/scripts/check-core-direct-paths.ts
|
|
10351
|
-
import { join as
|
|
10352
|
-
import { execa as
|
|
10820
|
+
import { join as join44 } from "path";
|
|
10821
|
+
import { execa as execa11 } from "execa";
|
|
10353
10822
|
|
|
10354
10823
|
// src/lib/core-direct-paths-audit.ts
|
|
10355
|
-
import { existsSync as
|
|
10356
|
-
import { join as
|
|
10824
|
+
import { existsSync as existsSync40, readFileSync as readFileSync31, readdirSync as readdirSync18, statSync as statSync10 } from "fs";
|
|
10825
|
+
import { join as join43 } from "path";
|
|
10357
10826
|
var EXTERNAL_BASE_IDENTIFIERS = ["CORE_API_URL"];
|
|
10358
10827
|
var API_ROUTE_PREFIX = "/api/v1";
|
|
10359
10828
|
var TEST_FILE_SUFFIXES = [".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx"];
|
|
@@ -10512,15 +10981,15 @@ function walkFiles(root, accept, skipDir) {
|
|
|
10512
10981
|
const walk2 = (dir) => {
|
|
10513
10982
|
let entries;
|
|
10514
10983
|
try {
|
|
10515
|
-
entries =
|
|
10984
|
+
entries = readdirSync18(dir);
|
|
10516
10985
|
} catch {
|
|
10517
10986
|
return;
|
|
10518
10987
|
}
|
|
10519
10988
|
for (const entry of entries) {
|
|
10520
|
-
const p =
|
|
10989
|
+
const p = join43(dir, entry);
|
|
10521
10990
|
let st;
|
|
10522
10991
|
try {
|
|
10523
|
-
st =
|
|
10992
|
+
st = statSync10(p);
|
|
10524
10993
|
} catch {
|
|
10525
10994
|
continue;
|
|
10526
10995
|
}
|
|
@@ -10547,7 +11016,7 @@ function auditFrontendExtraction(frontendSrcDir, externalBases = EXTERNAL_BASE_I
|
|
|
10547
11016
|
const extracted = [];
|
|
10548
11017
|
let rawTotal = 0;
|
|
10549
11018
|
for (const file of files) {
|
|
10550
|
-
const text =
|
|
11019
|
+
const text = readFileSync31(file, "utf8");
|
|
10551
11020
|
rawTotal += countRawExternalOccurrences(text, externalBases);
|
|
10552
11021
|
extracted.push(...extractCoreDirectPaths(text, file, externalBases));
|
|
10553
11022
|
}
|
|
@@ -10596,7 +11065,7 @@ function auditCoreRouteExtraction(apiSrcDir) {
|
|
|
10596
11065
|
const prefixSet = /* @__PURE__ */ new Set();
|
|
10597
11066
|
let rawApiRouterCount = 0;
|
|
10598
11067
|
for (const file of files) {
|
|
10599
|
-
const text =
|
|
11068
|
+
const text = readFileSync31(file, "utf8");
|
|
10600
11069
|
const extraction = extractCoreRoutePrefixes(text);
|
|
10601
11070
|
rawApiRouterCount += extraction.rawApiRouterCount;
|
|
10602
11071
|
for (const p of extraction.prefixes) prefixSet.add(normalizePrefix(p));
|
|
@@ -10612,10 +11081,10 @@ function pathMatchesAnyCorePrefix(normalized, corePrefixes, apiRoutePrefix = API
|
|
|
10612
11081
|
}
|
|
10613
11082
|
function resolveSiblingCoreSrc(params) {
|
|
10614
11083
|
const { estateDir, sibling } = params;
|
|
10615
|
-
const configPath =
|
|
11084
|
+
const configPath = join43(estateDir, sibling, "biffo.sibling.json");
|
|
10616
11085
|
let raw;
|
|
10617
11086
|
try {
|
|
10618
|
-
raw =
|
|
11087
|
+
raw = readFileSync31(configPath, "utf8");
|
|
10619
11088
|
} catch (err) {
|
|
10620
11089
|
throw new Error(
|
|
10621
11090
|
`cannot resolve ${sibling}'s core: ${configPath} does not exist or is unreadable (${err.message}) -- refusing to guess which core serves this sibling.`
|
|
@@ -10635,8 +11104,8 @@ function resolveSiblingCoreSrc(params) {
|
|
|
10635
11104
|
`cannot resolve ${sibling}'s core: ${configPath} has no non-empty "core_project" field.`
|
|
10636
11105
|
);
|
|
10637
11106
|
}
|
|
10638
|
-
const coreApiSrcDir =
|
|
10639
|
-
if (!
|
|
11107
|
+
const coreApiSrcDir = join43(estateDir, coreProject, "services", "api", "src");
|
|
11108
|
+
if (!existsSync40(coreApiSrcDir)) {
|
|
10640
11109
|
throw new Error(
|
|
10641
11110
|
`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.`
|
|
10642
11111
|
);
|
|
@@ -10677,9 +11146,9 @@ function auditSiblingCoreDirectPaths(params) {
|
|
|
10677
11146
|
|
|
10678
11147
|
// src/scripts/check-core-direct-paths.ts
|
|
10679
11148
|
async function runCoreDirectPathsCheck(opts = {}) {
|
|
10680
|
-
const root = (await
|
|
11149
|
+
const root = (await execa11("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
10681
11150
|
const sibling = opts.sibling ?? "sibling-template (self-check)";
|
|
10682
|
-
const frontendSrcDir = opts.frontendSrc ??
|
|
11151
|
+
const frontendSrcDir = opts.frontendSrc ?? join44(root, "_skeletons", "sibling-template", "apps", "frontend", "src");
|
|
10683
11152
|
let coreApiSrcDir;
|
|
10684
11153
|
let coreProject = null;
|
|
10685
11154
|
if (opts.coreSrc) {
|
|
@@ -10695,7 +11164,7 @@ async function runCoreDirectPathsCheck(opts = {}) {
|
|
|
10695
11164
|
coreApiSrcDir = resolution.coreApiSrcDir;
|
|
10696
11165
|
coreProject = resolution.coreProject;
|
|
10697
11166
|
} else {
|
|
10698
|
-
coreApiSrcDir =
|
|
11167
|
+
coreApiSrcDir = join44(root, "services", "api", "src");
|
|
10699
11168
|
}
|
|
10700
11169
|
const report = auditSiblingCoreDirectPaths({ sibling, frontendSrcDir, coreApiSrcDir });
|
|
10701
11170
|
console.log(
|
|
@@ -10731,7 +11200,7 @@ async function runCoreDirectPathsCheck(opts = {}) {
|
|
|
10731
11200
|
}
|
|
10732
11201
|
|
|
10733
11202
|
// src/scripts/check-core-ownership.ts
|
|
10734
|
-
import { execa as
|
|
11203
|
+
import { execa as execa12 } from "execa";
|
|
10735
11204
|
var BOLD = "\x1B[1m";
|
|
10736
11205
|
var DIM = "\x1B[2m";
|
|
10737
11206
|
var RED = "\x1B[31m";
|
|
@@ -10742,7 +11211,7 @@ async function runOwnershipCheck(argv) {
|
|
|
10742
11211
|
const stagedFlag = args.indexOf("--staged");
|
|
10743
11212
|
const staged = stagedFlag !== -1;
|
|
10744
11213
|
const messageFile = staged ? args[stagedFlag + 1] : void 0;
|
|
10745
|
-
const root = (await
|
|
11214
|
+
const root = (await execa12("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
10746
11215
|
const ownership = classifyRepoOwnership(root);
|
|
10747
11216
|
if (ownership === "template") {
|
|
10748
11217
|
console.log("\u2713 core ownership guard: skipped \u2014 this is the template, which owns these paths.");
|
|
@@ -10758,11 +11227,11 @@ async function runOwnershipCheck(argv) {
|
|
|
10758
11227
|
let deletedFiles = [];
|
|
10759
11228
|
let commitMessage = "";
|
|
10760
11229
|
if (staged) {
|
|
10761
|
-
const { stdout } = await
|
|
11230
|
+
const { stdout } = await execa12("git", ["diff", "--cached", "--name-status"], { cwd: root });
|
|
10762
11231
|
({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
|
|
10763
11232
|
if (messageFile) {
|
|
10764
|
-
const { readFileSync:
|
|
10765
|
-
if (
|
|
11233
|
+
const { readFileSync: readFileSync41, existsSync: existsSync49 } = await import("fs");
|
|
11234
|
+
if (existsSync49(messageFile)) commitMessage = readFileSync41(messageFile, "utf8");
|
|
10766
11235
|
}
|
|
10767
11236
|
} else {
|
|
10768
11237
|
const base = process.env["GITHUB_BASE_REF"] ?? args[0];
|
|
@@ -10770,18 +11239,18 @@ async function runOwnershipCheck(argv) {
|
|
|
10770
11239
|
console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
|
|
10771
11240
|
process.exit(2);
|
|
10772
11241
|
}
|
|
10773
|
-
await
|
|
10774
|
-
const { stdout } = await
|
|
11242
|
+
await execa12("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
|
|
11243
|
+
const { stdout } = await execa12("git", ["diff", "--name-status", `origin/${base}...HEAD`], {
|
|
10775
11244
|
cwd: root
|
|
10776
11245
|
});
|
|
10777
11246
|
({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
|
|
10778
|
-
const { stdout: log2 } = await
|
|
11247
|
+
const { stdout: log2 } = await execa12("git", ["log", "--format=%B", `origin/${base}..HEAD`], {
|
|
10779
11248
|
cwd: root,
|
|
10780
11249
|
reject: false
|
|
10781
11250
|
});
|
|
10782
11251
|
commitMessage = log2;
|
|
10783
11252
|
}
|
|
10784
|
-
const { stdout: gitBranch } = await
|
|
11253
|
+
const { stdout: gitBranch } = await execa12("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
10785
11254
|
cwd: root,
|
|
10786
11255
|
reject: false
|
|
10787
11256
|
});
|
|
@@ -10863,11 +11332,11 @@ ${BOLD}If the divergence is deliberate${OFF}
|
|
|
10863
11332
|
}
|
|
10864
11333
|
|
|
10865
11334
|
// src/scripts/check-eventbridge-log-permissions.ts
|
|
10866
|
-
import { execa as
|
|
11335
|
+
import { execa as execa13 } from "execa";
|
|
10867
11336
|
|
|
10868
11337
|
// src/lib/eventbridge-log-permission-guard.ts
|
|
10869
|
-
import { readFileSync as
|
|
10870
|
-
import { join as
|
|
11338
|
+
import { readFileSync as readFileSync32, readdirSync as readdirSync19, statSync as statSync11 } from "fs";
|
|
11339
|
+
import { join as join45 } from "path";
|
|
10871
11340
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", ".terraform", ".worktrees", "dist"]);
|
|
10872
11341
|
var EVENT_TARGET_TYPE = "aws_cloudwatch_event_target";
|
|
10873
11342
|
var LOG_RESOURCE_POLICY_TYPE = "aws_cloudwatch_log_resource_policy";
|
|
@@ -10939,15 +11408,15 @@ function walkTerraformFiles(root) {
|
|
|
10939
11408
|
const walk2 = (dir) => {
|
|
10940
11409
|
let entries;
|
|
10941
11410
|
try {
|
|
10942
|
-
entries =
|
|
11411
|
+
entries = readdirSync19(dir);
|
|
10943
11412
|
} catch {
|
|
10944
11413
|
return;
|
|
10945
11414
|
}
|
|
10946
11415
|
for (const entry of entries) {
|
|
10947
|
-
const p =
|
|
11416
|
+
const p = join45(dir, entry);
|
|
10948
11417
|
let st;
|
|
10949
11418
|
try {
|
|
10950
|
-
st =
|
|
11419
|
+
st = statSync11(p);
|
|
10951
11420
|
} catch {
|
|
10952
11421
|
continue;
|
|
10953
11422
|
}
|
|
@@ -10987,7 +11456,7 @@ function auditEventBridgeLogPermissions(root) {
|
|
|
10987
11456
|
let rawEventTargetCount = 0;
|
|
10988
11457
|
let rawLogPolicyCount = 0;
|
|
10989
11458
|
for (const file of files) {
|
|
10990
|
-
const text =
|
|
11459
|
+
const text = readFileSync32(file, "utf8");
|
|
10991
11460
|
rawEventTargetCount += countRawResourceDeclarations(text, EVENT_TARGET_TYPE);
|
|
10992
11461
|
rawLogPolicyCount += countRawResourceDeclarations(text, LOG_RESOURCE_POLICY_TYPE);
|
|
10993
11462
|
eventTargetBlocks.push(...findResourceBlocks(text, file, EVENT_TARGET_TYPE));
|
|
@@ -11040,7 +11509,7 @@ function auditEventBridgeLogPermissions(root) {
|
|
|
11040
11509
|
|
|
11041
11510
|
// src/scripts/check-eventbridge-log-permissions.ts
|
|
11042
11511
|
async function runEventBridgeLogPermissionCheck() {
|
|
11043
|
-
const root = (await
|
|
11512
|
+
const root = (await execa13("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
11044
11513
|
let report;
|
|
11045
11514
|
try {
|
|
11046
11515
|
report = auditEventBridgeLogPermissions(root);
|
|
@@ -11077,15 +11546,15 @@ async function runEventBridgeLogPermissionCheck() {
|
|
|
11077
11546
|
}
|
|
11078
11547
|
|
|
11079
11548
|
// src/scripts/check-lambda-output.ts
|
|
11080
|
-
import { execa as
|
|
11549
|
+
import { execa as execa14 } from "execa";
|
|
11081
11550
|
|
|
11082
11551
|
// src/lib/lambda-output-guard.ts
|
|
11083
|
-
import { readFileSync as
|
|
11084
|
-
import { join as
|
|
11552
|
+
import { readFileSync as readFileSync34 } from "fs";
|
|
11553
|
+
import { join as join47 } from "path";
|
|
11085
11554
|
|
|
11086
11555
|
// src/lib/terraform-input-guard.ts
|
|
11087
|
-
import { readdirSync as
|
|
11088
|
-
import { join as
|
|
11556
|
+
import { readdirSync as readdirSync20, readFileSync as readFileSync33, statSync as statSync12 } from "fs";
|
|
11557
|
+
import { join as join46 } from "path";
|
|
11089
11558
|
var GUARDED_SUBCOMMANDS = [
|
|
11090
11559
|
"init",
|
|
11091
11560
|
"plan",
|
|
@@ -11100,20 +11569,20 @@ function stripComments2(source) {
|
|
|
11100
11569
|
}
|
|
11101
11570
|
function findWorkflowFiles(repoRoot) {
|
|
11102
11571
|
const found = [];
|
|
11103
|
-
const walk2 = (dir,
|
|
11572
|
+
const walk2 = (dir, relative10) => {
|
|
11104
11573
|
let entries;
|
|
11105
11574
|
try {
|
|
11106
|
-
entries =
|
|
11575
|
+
entries = readdirSync20(dir);
|
|
11107
11576
|
} catch {
|
|
11108
11577
|
return;
|
|
11109
11578
|
}
|
|
11110
11579
|
for (const entry of entries) {
|
|
11111
11580
|
if (entry === "node_modules" || entry === ".git" || entry === ".worktrees") continue;
|
|
11112
|
-
const full =
|
|
11113
|
-
const rel =
|
|
11114
|
-
if (
|
|
11581
|
+
const full = join46(dir, entry);
|
|
11582
|
+
const rel = relative10 ? `${relative10}/${entry}` : entry;
|
|
11583
|
+
if (statSync12(full).isDirectory()) {
|
|
11115
11584
|
walk2(full, rel);
|
|
11116
|
-
} else if (/\.ya?ml$/.test(entry) &&
|
|
11585
|
+
} else if (/\.ya?ml$/.test(entry) && relative10.endsWith(".github/workflows")) {
|
|
11117
11586
|
found.push(rel);
|
|
11118
11587
|
}
|
|
11119
11588
|
}
|
|
@@ -11153,7 +11622,7 @@ function checkWorkflowSource(file, rawSource) {
|
|
|
11153
11622
|
}
|
|
11154
11623
|
function checkTerraformInput(repoRoot) {
|
|
11155
11624
|
return findWorkflowFiles(repoRoot).flatMap(
|
|
11156
|
-
(file) => checkWorkflowSource(file,
|
|
11625
|
+
(file) => checkWorkflowSource(file, readFileSync33(join46(repoRoot, file), "utf8"))
|
|
11157
11626
|
);
|
|
11158
11627
|
}
|
|
11159
11628
|
|
|
@@ -11211,13 +11680,13 @@ function checkWorkflowSource2(file, rawSource) {
|
|
|
11211
11680
|
}
|
|
11212
11681
|
function checkLambdaOutput(repoRoot) {
|
|
11213
11682
|
return findWorkflowFiles(repoRoot).flatMap(
|
|
11214
|
-
(file) => checkWorkflowSource2(file,
|
|
11683
|
+
(file) => checkWorkflowSource2(file, readFileSync34(join47(repoRoot, file), "utf8"))
|
|
11215
11684
|
);
|
|
11216
11685
|
}
|
|
11217
11686
|
|
|
11218
11687
|
// src/scripts/check-lambda-output.ts
|
|
11219
11688
|
async function runLambdaOutputCheck() {
|
|
11220
|
-
const root = (await
|
|
11689
|
+
const root = (await execa14("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
11221
11690
|
const files = findWorkflowFiles(root);
|
|
11222
11691
|
console.log(`audited ${files.length} workflow file(s) under ${root}`);
|
|
11223
11692
|
if (files.length === 0) {
|
|
@@ -11239,9 +11708,9 @@ async function runLambdaOutputCheck() {
|
|
|
11239
11708
|
}
|
|
11240
11709
|
|
|
11241
11710
|
// src/scripts/check-pipe-trap.ts
|
|
11242
|
-
import { readFileSync as
|
|
11243
|
-
import { join as
|
|
11244
|
-
import { execa as
|
|
11711
|
+
import { readFileSync as readFileSync35, readdirSync as readdirSync21 } from "fs";
|
|
11712
|
+
import { join as join48, relative as relative8 } from "path";
|
|
11713
|
+
import { execa as execa15 } from "execa";
|
|
11245
11714
|
|
|
11246
11715
|
// src/lib/pipe-trap-guard.ts
|
|
11247
11716
|
var STATUS_BEARING = [
|
|
@@ -11337,23 +11806,23 @@ function findPipeTraps(source) {
|
|
|
11337
11806
|
function shellFiles(root) {
|
|
11338
11807
|
const out = [];
|
|
11339
11808
|
for (const dir of ["scripts", ".githooks"]) {
|
|
11340
|
-
const full =
|
|
11809
|
+
const full = join48(root, dir);
|
|
11341
11810
|
let entries;
|
|
11342
11811
|
try {
|
|
11343
|
-
entries =
|
|
11812
|
+
entries = readdirSync21(full, { withFileTypes: true });
|
|
11344
11813
|
} catch {
|
|
11345
11814
|
continue;
|
|
11346
11815
|
}
|
|
11347
11816
|
for (const entry of entries) {
|
|
11348
11817
|
if (!entry.isFile()) continue;
|
|
11349
11818
|
if (dir === "scripts" && !entry.name.endsWith(".sh")) continue;
|
|
11350
|
-
out.push(
|
|
11819
|
+
out.push(join48(full, entry.name));
|
|
11351
11820
|
}
|
|
11352
11821
|
}
|
|
11353
11822
|
return out;
|
|
11354
11823
|
}
|
|
11355
11824
|
async function runPipeTrapCheck() {
|
|
11356
|
-
const root = (await
|
|
11825
|
+
const root = (await execa15("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
11357
11826
|
const files = shellFiles(root);
|
|
11358
11827
|
console.log(`audited ${files.length} shell file(s) under scripts/ and .githooks/ under ${root}`);
|
|
11359
11828
|
if (files.length === 0) {
|
|
@@ -11363,8 +11832,8 @@ async function runPipeTrapCheck() {
|
|
|
11363
11832
|
process.exit(1);
|
|
11364
11833
|
}
|
|
11365
11834
|
const findings = files.flatMap(
|
|
11366
|
-
(file) => findPipeTraps(
|
|
11367
|
-
(t) => `${
|
|
11835
|
+
(file) => findPipeTraps(readFileSync35(file, "utf8")).map(
|
|
11836
|
+
(t) => `${relative8(root, file)}:${t.line} ${t.text}
|
|
11368
11837
|
${t.reason}`
|
|
11369
11838
|
)
|
|
11370
11839
|
);
|
|
@@ -11380,11 +11849,11 @@ async function runPipeTrapCheck() {
|
|
|
11380
11849
|
}
|
|
11381
11850
|
|
|
11382
11851
|
// src/scripts/check-plugin-allowlist-convention.ts
|
|
11383
|
-
import { execa as
|
|
11852
|
+
import { execa as execa16 } from "execa";
|
|
11384
11853
|
|
|
11385
11854
|
// src/lib/plugin-allowlist-convention.ts
|
|
11386
|
-
import { readFileSync as
|
|
11387
|
-
import { join as
|
|
11855
|
+
import { readFileSync as readFileSync36 } from "fs";
|
|
11856
|
+
import { join as join49 } from "path";
|
|
11388
11857
|
var COMPUTE_MAIN_TF = "modules/cloud/aws/compute/main.tf";
|
|
11389
11858
|
var PLUGIN_TEMPLATE_MAIN_TF = "modules/plugins/_template/main.tf";
|
|
11390
11859
|
var ALLOWLIST_MAIN_TF = "modules/cloud/aws/plugin-allowlist/main.tf";
|
|
@@ -11393,18 +11862,18 @@ var PROJECT = "<project>";
|
|
|
11393
11862
|
var ENV = "<env>";
|
|
11394
11863
|
var PLUGIN = "<plugin>";
|
|
11395
11864
|
var ACCOUNT = "<account>";
|
|
11396
|
-
function read(repoRoot,
|
|
11865
|
+
function read(repoRoot, relative10) {
|
|
11397
11866
|
try {
|
|
11398
|
-
return
|
|
11867
|
+
return readFileSync36(join49(repoRoot, relative10), "utf8");
|
|
11399
11868
|
} catch {
|
|
11400
|
-
throw new Error(`plugin-allowlist drift guard: cannot read ${
|
|
11869
|
+
throw new Error(`plugin-allowlist drift guard: cannot read ${relative10}`);
|
|
11401
11870
|
}
|
|
11402
11871
|
}
|
|
11403
11872
|
function assignedString(source, name) {
|
|
11404
11873
|
const match = new RegExp(`^\\s*${name}\\s*=\\s*"((?:[^"\\\\]|\\\\.)*)"\\s*$`, "m").exec(source);
|
|
11405
11874
|
return match?.[1];
|
|
11406
11875
|
}
|
|
11407
|
-
function
|
|
11876
|
+
function resolve19(expression, bindings) {
|
|
11408
11877
|
let current = expression;
|
|
11409
11878
|
for (let pass = 0; pass < 10; pass += 1) {
|
|
11410
11879
|
const next = current.replace(/\$\{([^}]+)\}/g, (whole, ref) => {
|
|
@@ -11441,13 +11910,13 @@ function composeExpectedRoleName(repoRoot) {
|
|
|
11441
11910
|
"var.project_name": PROJECT,
|
|
11442
11911
|
"var.environment": ENV,
|
|
11443
11912
|
"var.plugin_name": PLUGIN,
|
|
11444
|
-
"var.function_name":
|
|
11913
|
+
"var.function_name": resolve19(pluginFunctionName, {
|
|
11445
11914
|
"var.plugin_name": PLUGIN
|
|
11446
11915
|
})
|
|
11447
11916
|
};
|
|
11448
|
-
bindings["local.name_prefix"] =
|
|
11449
|
-
bindings["local.function_name"] =
|
|
11450
|
-
return
|
|
11917
|
+
bindings["local.name_prefix"] = resolve19(namePrefix, bindings);
|
|
11918
|
+
bindings["local.function_name"] = resolve19(functionName, bindings);
|
|
11919
|
+
return resolve19(roleName, bindings);
|
|
11451
11920
|
}
|
|
11452
11921
|
function readAllowlistGlob(repoRoot) {
|
|
11453
11922
|
const allowlist = read(repoRoot, ALLOWLIST_MAIN_TF);
|
|
@@ -11457,7 +11926,7 @@ function readAllowlistGlob(repoRoot) {
|
|
|
11457
11926
|
`plugin-allowlist drift guard: could not find the "for name in var.enabled_plugins" glob in ${ALLOWLIST_MAIN_TF}.`
|
|
11458
11927
|
);
|
|
11459
11928
|
}
|
|
11460
|
-
return
|
|
11929
|
+
return resolve19(glob, {
|
|
11461
11930
|
"data.aws_caller_identity.current.account_id": ACCOUNT,
|
|
11462
11931
|
"var.project_name": PROJECT,
|
|
11463
11932
|
"var.environment": ENV,
|
|
@@ -11491,7 +11960,7 @@ Plugins would be rejected by require_service_principal (ADR-0009). Fix the glob,
|
|
|
11491
11960
|
|
|
11492
11961
|
// src/scripts/check-plugin-allowlist-convention.ts
|
|
11493
11962
|
async function runPluginAllowlistConventionCheck() {
|
|
11494
|
-
const root = (await
|
|
11963
|
+
const root = (await execa16("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
11495
11964
|
let violations;
|
|
11496
11965
|
try {
|
|
11497
11966
|
violations = checkAllowlistConvention(root);
|
|
@@ -11516,34 +11985,34 @@ async function runPluginAllowlistConventionCheck() {
|
|
|
11516
11985
|
}
|
|
11517
11986
|
|
|
11518
11987
|
// src/scripts/check-plugin-collisions.ts
|
|
11519
|
-
import { existsSync as
|
|
11520
|
-
import { join as
|
|
11521
|
-
import { execa as
|
|
11988
|
+
import { existsSync as existsSync42 } from "fs";
|
|
11989
|
+
import { join as join51 } from "path";
|
|
11990
|
+
import { execa as execa17 } from "execa";
|
|
11522
11991
|
|
|
11523
11992
|
// src/lib/plugin-collision-guard.ts
|
|
11524
|
-
import { existsSync as
|
|
11525
|
-
import { join as
|
|
11993
|
+
import { existsSync as existsSync41, readdirSync as readdirSync22, statSync as statSync13 } from "fs";
|
|
11994
|
+
import { join as join50 } from "path";
|
|
11526
11995
|
var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
|
|
11527
11996
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
|
|
11528
11997
|
function subdirectories(dir) {
|
|
11529
|
-
if (!
|
|
11530
|
-
return
|
|
11998
|
+
if (!existsSync41(dir)) return [];
|
|
11999
|
+
return readdirSync22(dir).filter((entry) => {
|
|
11531
12000
|
if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
|
|
11532
12001
|
try {
|
|
11533
|
-
return
|
|
12002
|
+
return statSync13(join50(dir, entry)).isDirectory();
|
|
11534
12003
|
} catch {
|
|
11535
12004
|
return false;
|
|
11536
12005
|
}
|
|
11537
12006
|
});
|
|
11538
12007
|
}
|
|
11539
12008
|
function regularPackagesOf(pluginDir2) {
|
|
11540
|
-
return subdirectories(pluginDir2).filter((name) =>
|
|
12009
|
+
return subdirectories(pluginDir2).filter((name) => existsSync41(join50(pluginDir2, name, "__init__.py"))).sort();
|
|
11541
12010
|
}
|
|
11542
12011
|
function bareTestModulesOf(pluginDir2) {
|
|
11543
|
-
const testsDir =
|
|
11544
|
-
if (!
|
|
11545
|
-
if (
|
|
11546
|
-
return
|
|
12012
|
+
const testsDir = join50(pluginDir2, "tests");
|
|
12013
|
+
if (!existsSync41(testsDir)) return [];
|
|
12014
|
+
if (existsSync41(join50(testsDir, "__init__.py"))) return [];
|
|
12015
|
+
return readdirSync22(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
|
|
11547
12016
|
}
|
|
11548
12017
|
function findCollisions(servicesDir, pluginDirs) {
|
|
11549
12018
|
const plugins = (pluginDirs ?? subdirectories(servicesDir)).filter((name) => !name.startsWith("_")).filter((name) => name !== "api").sort();
|
|
@@ -11551,7 +12020,7 @@ function findCollisions(servicesDir, pluginDirs) {
|
|
|
11551
12020
|
const gather = (kind, namesOf) => {
|
|
11552
12021
|
const claims = /* @__PURE__ */ new Map();
|
|
11553
12022
|
for (const plugin of plugins) {
|
|
11554
|
-
for (const name of namesOf(
|
|
12023
|
+
for (const name of namesOf(join50(servicesDir, plugin))) {
|
|
11555
12024
|
claims.set(name, [...claims.get(name) ?? [], plugin]);
|
|
11556
12025
|
}
|
|
11557
12026
|
}
|
|
@@ -11588,9 +12057,9 @@ function formatCollisions(collisions) {
|
|
|
11588
12057
|
|
|
11589
12058
|
// src/scripts/check-plugin-collisions.ts
|
|
11590
12059
|
async function runPluginCollisionCheck() {
|
|
11591
|
-
const root = (await
|
|
11592
|
-
const servicesDir =
|
|
11593
|
-
if (!
|
|
12060
|
+
const root = (await execa17("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
12061
|
+
const servicesDir = join51(root, "services");
|
|
12062
|
+
if (!existsSync42(servicesDir)) {
|
|
11594
12063
|
console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
|
|
11595
12064
|
return;
|
|
11596
12065
|
}
|
|
@@ -11607,11 +12076,11 @@ async function runPluginCollisionCheck() {
|
|
|
11607
12076
|
}
|
|
11608
12077
|
|
|
11609
12078
|
// src/scripts/check-plugin-terraform.ts
|
|
11610
|
-
import { execa as
|
|
12079
|
+
import { execa as execa18 } from "execa";
|
|
11611
12080
|
|
|
11612
12081
|
// src/lib/plugin-terraform-guard.ts
|
|
11613
|
-
import { existsSync as
|
|
11614
|
-
import { dirname as dirname10, join as
|
|
12082
|
+
import { existsSync as existsSync43, readFileSync as readFileSync37, readdirSync as readdirSync23 } from "fs";
|
|
12083
|
+
import { dirname as dirname10, join as join52, relative as relative9, sep as sep3 } from "path";
|
|
11615
12084
|
var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
|
|
11616
12085
|
var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
|
|
11617
12086
|
function findPluginManifests(root) {
|
|
@@ -11619,16 +12088,16 @@ function findPluginManifests(root) {
|
|
|
11619
12088
|
const walk2 = (dir) => {
|
|
11620
12089
|
let entries;
|
|
11621
12090
|
try {
|
|
11622
|
-
entries =
|
|
12091
|
+
entries = readdirSync23(dir, { withFileTypes: true });
|
|
11623
12092
|
} catch {
|
|
11624
12093
|
return;
|
|
11625
12094
|
}
|
|
11626
12095
|
for (const entry of entries) {
|
|
11627
12096
|
if (entry.isDirectory()) {
|
|
11628
12097
|
if (SKIP_DIRS3.has(entry.name)) continue;
|
|
11629
|
-
walk2(
|
|
12098
|
+
walk2(join52(dir, entry.name));
|
|
11630
12099
|
} else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
|
|
11631
|
-
found.push(
|
|
12100
|
+
found.push(relative9(root, join52(dir, entry.name)).split(sep3).join("/"));
|
|
11632
12101
|
}
|
|
11633
12102
|
}
|
|
11634
12103
|
};
|
|
@@ -11638,7 +12107,7 @@ function findPluginManifests(root) {
|
|
|
11638
12107
|
function readSubscriptions(absManifestPath) {
|
|
11639
12108
|
let parsed;
|
|
11640
12109
|
try {
|
|
11641
|
-
parsed = JSON.parse(
|
|
12110
|
+
parsed = JSON.parse(readFileSync37(absManifestPath, "utf8"));
|
|
11642
12111
|
} catch {
|
|
11643
12112
|
return null;
|
|
11644
12113
|
}
|
|
@@ -11653,15 +12122,15 @@ function readSubscriptions(absManifestPath) {
|
|
|
11653
12122
|
}
|
|
11654
12123
|
function checkPluginTerraform(root) {
|
|
11655
12124
|
const violations = [];
|
|
11656
|
-
const coreManifest =
|
|
12125
|
+
const coreManifest = existsSync43(join52(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
|
|
11657
12126
|
for (const manifest of findPluginManifests(root)) {
|
|
11658
12127
|
if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
|
|
11659
|
-
const absManifest =
|
|
12128
|
+
const absManifest = join52(root, manifest);
|
|
11660
12129
|
const subscriptions = readSubscriptions(absManifest);
|
|
11661
12130
|
if (subscriptions === null) continue;
|
|
11662
12131
|
const pluginDir2 = dirname10(absManifest);
|
|
11663
|
-
if (
|
|
11664
|
-
const relPluginDir =
|
|
12132
|
+
if (existsSync43(join52(pluginDir2, "terraform"))) continue;
|
|
12133
|
+
const relPluginDir = relative9(root, pluginDir2).split(sep3).join("/");
|
|
11665
12134
|
violations.push({
|
|
11666
12135
|
manifest,
|
|
11667
12136
|
expectedTerraformDir: relPluginDir ? `${relPluginDir}/terraform` : "terraform",
|
|
@@ -11680,7 +12149,7 @@ function formatViolations(violations) {
|
|
|
11680
12149
|
|
|
11681
12150
|
// src/scripts/check-plugin-terraform.ts
|
|
11682
12151
|
async function runPluginTerraformCheck() {
|
|
11683
|
-
const root = (await
|
|
12152
|
+
const root = (await execa18("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
11684
12153
|
const violations = checkPluginTerraform(root);
|
|
11685
12154
|
if (violations.length > 0) {
|
|
11686
12155
|
console.error("\u2717 plugin Terraform guard: event subscriptions with no infrastructure\n");
|
|
@@ -11691,13 +12160,13 @@ async function runPluginTerraformCheck() {
|
|
|
11691
12160
|
}
|
|
11692
12161
|
|
|
11693
12162
|
// src/scripts/check-plugin-tool-supply.ts
|
|
11694
|
-
import { existsSync as
|
|
11695
|
-
import { join as
|
|
11696
|
-
import { execa as
|
|
12163
|
+
import { existsSync as existsSync45 } from "fs";
|
|
12164
|
+
import { join as join54 } from "path";
|
|
12165
|
+
import { execa as execa19 } from "execa";
|
|
11697
12166
|
|
|
11698
12167
|
// src/lib/plugin-tool-supply-audit.ts
|
|
11699
|
-
import { existsSync as
|
|
11700
|
-
import { join as
|
|
12168
|
+
import { existsSync as existsSync44, readFileSync as readFileSync38, readdirSync as readdirSync24, statSync as statSync14 } from "fs";
|
|
12169
|
+
import { join as join53 } from "path";
|
|
11701
12170
|
|
|
11702
12171
|
// src/lib/openrouter-model-snapshot.ts
|
|
11703
12172
|
var OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT = "2026-08-10T06:39:01Z";
|
|
@@ -12108,13 +12577,13 @@ var OPENROUTER_MODEL_IDS = [
|
|
|
12108
12577
|
function listDirs(root) {
|
|
12109
12578
|
let entries;
|
|
12110
12579
|
try {
|
|
12111
|
-
entries =
|
|
12580
|
+
entries = readdirSync24(root);
|
|
12112
12581
|
} catch {
|
|
12113
12582
|
return [];
|
|
12114
12583
|
}
|
|
12115
12584
|
return entries.filter((e) => {
|
|
12116
12585
|
try {
|
|
12117
|
-
return
|
|
12586
|
+
return statSync14(join53(root, e)).isDirectory();
|
|
12118
12587
|
} catch {
|
|
12119
12588
|
return false;
|
|
12120
12589
|
}
|
|
@@ -12125,15 +12594,15 @@ function walkFiles2(root, accept, skipDir) {
|
|
|
12125
12594
|
const walk2 = (dir) => {
|
|
12126
12595
|
let entries;
|
|
12127
12596
|
try {
|
|
12128
|
-
entries =
|
|
12597
|
+
entries = readdirSync24(dir);
|
|
12129
12598
|
} catch {
|
|
12130
12599
|
return;
|
|
12131
12600
|
}
|
|
12132
12601
|
for (const entry of entries) {
|
|
12133
|
-
const p =
|
|
12602
|
+
const p = join53(dir, entry);
|
|
12134
12603
|
let st;
|
|
12135
12604
|
try {
|
|
12136
|
-
st =
|
|
12605
|
+
st = statSync14(p);
|
|
12137
12606
|
} catch {
|
|
12138
12607
|
continue;
|
|
12139
12608
|
}
|
|
@@ -12156,14 +12625,14 @@ function pluginPythonFiles(pluginDir2) {
|
|
|
12156
12625
|
);
|
|
12157
12626
|
}
|
|
12158
12627
|
function pluginTerraformFiles(pluginDir2) {
|
|
12159
|
-
const tfDir =
|
|
12628
|
+
const tfDir = join53(pluginDir2, "terraform");
|
|
12160
12629
|
let entries;
|
|
12161
12630
|
try {
|
|
12162
|
-
entries =
|
|
12631
|
+
entries = readdirSync24(tfDir);
|
|
12163
12632
|
} catch {
|
|
12164
12633
|
return [];
|
|
12165
12634
|
}
|
|
12166
|
-
return entries.filter((e) => e.endsWith(".tf")).map((e) =>
|
|
12635
|
+
return entries.filter((e) => e.endsWith(".tf")).map((e) => join53(tfDir, e)).sort();
|
|
12167
12636
|
}
|
|
12168
12637
|
function extractManifestTools(manifestText) {
|
|
12169
12638
|
let parsed;
|
|
@@ -12415,8 +12884,8 @@ function isSnapshotStale(fetchedAt, now) {
|
|
|
12415
12884
|
function normalizeModelId(id) {
|
|
12416
12885
|
return id.endsWith(":online") ? id.slice(0, -":online".length) : id;
|
|
12417
12886
|
}
|
|
12418
|
-
var CONFIG_PY_PATH =
|
|
12419
|
-
var ORCHESTRATION_SCHEMA_PATH =
|
|
12887
|
+
var CONFIG_PY_PATH = join53("services", "api", "src", "api", "config.py");
|
|
12888
|
+
var ORCHESTRATION_SCHEMA_PATH = join53(
|
|
12420
12889
|
"services",
|
|
12421
12890
|
"api",
|
|
12422
12891
|
"src",
|
|
@@ -12428,10 +12897,10 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
|
|
|
12428
12897
|
const knownModelIds = options.knownModelIds ?? OPENROUTER_MODEL_IDS;
|
|
12429
12898
|
const snapshotFetchedAt = options.snapshotFetchedAt ?? OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT;
|
|
12430
12899
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
12431
|
-
const configPath =
|
|
12432
|
-
const orchestrationPath =
|
|
12433
|
-
const configMissing = !
|
|
12434
|
-
const orchestrationSchemaMissing = !
|
|
12900
|
+
const configPath = join53(repoRoot, CONFIG_PY_PATH);
|
|
12901
|
+
const orchestrationPath = join53(repoRoot, ORCHESTRATION_SCHEMA_PATH);
|
|
12902
|
+
const configMissing = !existsSync44(configPath);
|
|
12903
|
+
const orchestrationSchemaMissing = !existsSync44(orchestrationPath);
|
|
12435
12904
|
const knownSet = new Set(knownModelIds);
|
|
12436
12905
|
const snapshotEmpty = knownModelIds.length === 0;
|
|
12437
12906
|
const snapshotStale = isSnapshotStale(snapshotFetchedAt, now);
|
|
@@ -12449,13 +12918,13 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
|
|
|
12449
12918
|
};
|
|
12450
12919
|
let settingsBlind = false;
|
|
12451
12920
|
if (!configMissing) {
|
|
12452
|
-
const settingsFields = extractSettingsModelFields(
|
|
12921
|
+
const settingsFields = extractSettingsModelFields(readFileSync38(configPath, "utf8"));
|
|
12453
12922
|
if (settingsFields.length === 0) settingsBlind = true;
|
|
12454
12923
|
for (const { field, value } of settingsFields) record(`${CONFIG_PY_PATH}#${field}`, value);
|
|
12455
12924
|
}
|
|
12456
12925
|
let curatedFieldsBlind = false;
|
|
12457
12926
|
if (!orchestrationSchemaMissing) {
|
|
12458
|
-
const curated = extractCuratedModelFields(
|
|
12927
|
+
const curated = extractCuratedModelFields(readFileSync38(orchestrationPath, "utf8"));
|
|
12459
12928
|
if (curated.rawFieldCount > 0 && curated.fields.every((f) => f.defaultValue === null && f.optionValues.length === 0)) {
|
|
12460
12929
|
curatedFieldsBlind = true;
|
|
12461
12930
|
}
|
|
@@ -12502,7 +12971,7 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
|
|
|
12502
12971
|
function discoverPluginDirs(pluginsRoot) {
|
|
12503
12972
|
return listDirs(pluginsRoot).filter((name) => {
|
|
12504
12973
|
try {
|
|
12505
|
-
return
|
|
12974
|
+
return statSync14(join53(pluginsRoot, name, "biffo.plugin.json")).isFile();
|
|
12506
12975
|
} catch {
|
|
12507
12976
|
return false;
|
|
12508
12977
|
}
|
|
@@ -12515,8 +12984,8 @@ function auditPluginToolSupply(pluginsRoot) {
|
|
|
12515
12984
|
let terraformBlind = false;
|
|
12516
12985
|
let totalDeclaredTools = 0;
|
|
12517
12986
|
for (const name of pluginNames) {
|
|
12518
|
-
const pluginDir2 =
|
|
12519
|
-
const manifestText =
|
|
12987
|
+
const pluginDir2 = join53(pluginsRoot, name);
|
|
12988
|
+
const manifestText = readFileSync38(join53(pluginDir2, "biffo.plugin.json"), "utf8");
|
|
12520
12989
|
const manifest = extractManifestTools(manifestText);
|
|
12521
12990
|
if (manifest.parseError) {
|
|
12522
12991
|
findings.push({
|
|
@@ -12534,13 +13003,13 @@ function auditPluginToolSupply(pluginsRoot) {
|
|
|
12534
13003
|
totalDeclaredTools += manifest.tools.length;
|
|
12535
13004
|
const pySources = pluginPythonFiles(pluginDir2).map((f) => ({
|
|
12536
13005
|
file: f,
|
|
12537
|
-
text:
|
|
13006
|
+
text: readFileSync38(f, "utf8")
|
|
12538
13007
|
}));
|
|
12539
13008
|
const resolver = buildSymbolResolver(pySources);
|
|
12540
13009
|
const registry = extractToolRegistryEntries(pySources, resolver);
|
|
12541
13010
|
if (registry.rawToolDefinitionCount > 0 && registry.entries.length === 0) registryBlind = true;
|
|
12542
13011
|
const tfFiles = pluginTerraformFiles(pluginDir2);
|
|
12543
|
-
const tfText = tfFiles.map((f) =>
|
|
13012
|
+
const tfText = tfFiles.map((f) => readFileSync38(f, "utf8")).join("\n");
|
|
12544
13013
|
const terraform = extractTerraformEnvKeys(tfText);
|
|
12545
13014
|
if (terraform.rawMarkerCount > 0 && terraform.resolvedBlockCount === 0) terraformBlind = true;
|
|
12546
13015
|
for (const toolName of manifest.tools) {
|
|
@@ -12614,7 +13083,7 @@ function auditPluginToolSupply(pluginsRoot) {
|
|
|
12614
13083
|
requiredEnvVars: envResult.envVars,
|
|
12615
13084
|
missingEnvVars: anyWired ? [] : envResult.envVars,
|
|
12616
13085
|
status: anyWired ? "ok" : "missing-env",
|
|
12617
|
-
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 ${
|
|
13086
|
+
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 ${join53(pluginDir2, "terraform")}, so this deployment can never supply it`
|
|
12618
13087
|
});
|
|
12619
13088
|
}
|
|
12620
13089
|
}
|
|
@@ -12645,10 +13114,10 @@ function auditPluginToolSupply(pluginsRoot) {
|
|
|
12645
13114
|
|
|
12646
13115
|
// src/scripts/check-plugin-tool-supply.ts
|
|
12647
13116
|
async function runPluginToolSupplyCheck() {
|
|
12648
|
-
const root = (await
|
|
13117
|
+
const root = (await execa19("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
12649
13118
|
let allOk = true;
|
|
12650
|
-
const pluginsRoot =
|
|
12651
|
-
if (!
|
|
13119
|
+
const pluginsRoot = join54(root, "services", "_plugins");
|
|
13120
|
+
if (!existsSync45(pluginsRoot)) {
|
|
12652
13121
|
console.log("\u2713 plugin tool-supply guard: no services/_plugins/ \u2014 nothing to audit");
|
|
12653
13122
|
} else {
|
|
12654
13123
|
const report = auditPluginToolSupply(pluginsRoot);
|
|
@@ -12678,8 +13147,8 @@ async function runPluginToolSupplyCheck() {
|
|
|
12678
13147
|
console.log(`\u2713 plugin tool-supply guard: ${report.summary}`);
|
|
12679
13148
|
}
|
|
12680
13149
|
}
|
|
12681
|
-
const servicesApiRoot =
|
|
12682
|
-
if (!
|
|
13150
|
+
const servicesApiRoot = join54(root, "services", "api");
|
|
13151
|
+
if (!existsSync45(servicesApiRoot)) {
|
|
12683
13152
|
console.log("\u2713 plugin model-id guard: no services/api/ \u2014 nothing to audit");
|
|
12684
13153
|
} else {
|
|
12685
13154
|
const modelReport = auditDeclaredModelIds(root);
|
|
@@ -12725,7 +13194,7 @@ async function runPluginToolSupplyCheck() {
|
|
|
12725
13194
|
}
|
|
12726
13195
|
|
|
12727
13196
|
// src/scripts/check-release-subject.ts
|
|
12728
|
-
import { execa as
|
|
13197
|
+
import { execa as execa20 } from "execa";
|
|
12729
13198
|
|
|
12730
13199
|
// src/lib/release-version.ts
|
|
12731
13200
|
var MINOR_TYPES = /* @__PURE__ */ new Set(["feat"]);
|
|
@@ -12762,7 +13231,7 @@ async function fetchPrTitleViaGh({
|
|
|
12762
13231
|
PR_NUMBER,
|
|
12763
13232
|
GH_REPO
|
|
12764
13233
|
}) {
|
|
12765
|
-
const { stdout } = await
|
|
13234
|
+
const { stdout } = await execa20(
|
|
12766
13235
|
"gh",
|
|
12767
13236
|
["pr", "view", PR_NUMBER, "--repo", GH_REPO, "--json", "title", "--jq", ".title"],
|
|
12768
13237
|
{ env: { ...process.env, GH_TOKEN } }
|
|
@@ -12798,7 +13267,7 @@ async function resolveReleaseSubject({
|
|
|
12798
13267
|
);
|
|
12799
13268
|
}
|
|
12800
13269
|
}
|
|
12801
|
-
return (await
|
|
13270
|
+
return (await execa20("git", ["log", "-1", "--format=%s"], { cwd })).stdout.trim();
|
|
12802
13271
|
}
|
|
12803
13272
|
async function runReleaseSubjectCheck(argv) {
|
|
12804
13273
|
const base = process.env["GITHUB_BASE_REF"] ?? argv[0];
|
|
@@ -12806,9 +13275,9 @@ async function runReleaseSubjectCheck(argv) {
|
|
|
12806
13275
|
console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
|
|
12807
13276
|
process.exit(2);
|
|
12808
13277
|
}
|
|
12809
|
-
const root = (await
|
|
12810
|
-
await
|
|
12811
|
-
const { stdout } = await
|
|
13278
|
+
const root = (await execa20("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
13279
|
+
await execa20("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
|
|
13280
|
+
const { stdout } = await execa20("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
|
|
12812
13281
|
cwd: root
|
|
12813
13282
|
});
|
|
12814
13283
|
const changedFiles = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
@@ -12856,13 +13325,13 @@ async function runReleaseSubjectCheck(argv) {
|
|
|
12856
13325
|
}
|
|
12857
13326
|
|
|
12858
13327
|
// src/scripts/check-skeleton-drift.ts
|
|
12859
|
-
import { existsSync as
|
|
12860
|
-
import { join as
|
|
12861
|
-
import { execa as
|
|
13328
|
+
import { existsSync as existsSync46, readdirSync as readdirSync26 } from "fs";
|
|
13329
|
+
import { join as join56 } from "path";
|
|
13330
|
+
import { execa as execa21 } from "execa";
|
|
12862
13331
|
|
|
12863
13332
|
// src/lib/skeleton-drift-guard.ts
|
|
12864
|
-
import { readFileSync as
|
|
12865
|
-
import { join as
|
|
13333
|
+
import { readFileSync as readFileSync39, readdirSync as readdirSync25, statSync as statSync15 } from "fs";
|
|
13334
|
+
import { join as join55 } from "path";
|
|
12866
13335
|
var isWorkflow = (rel) => rel.startsWith(".github/workflows/") && (rel.endsWith(".yml") || rel.endsWith(".yaml"));
|
|
12867
13336
|
var isRootLayout = (rel) => rel.endsWith("src/app/layout.tsx");
|
|
12868
13337
|
var uncommented = (contents) => contents.split("\n").filter((line) => !/^\s*(\/\/|\/\*|\*)/.test(line)).join("\n");
|
|
@@ -12920,16 +13389,16 @@ function walk(dir, base = dir) {
|
|
|
12920
13389
|
const out = [];
|
|
12921
13390
|
let entries;
|
|
12922
13391
|
try {
|
|
12923
|
-
entries =
|
|
13392
|
+
entries = readdirSync25(dir);
|
|
12924
13393
|
} catch {
|
|
12925
13394
|
return out;
|
|
12926
13395
|
}
|
|
12927
13396
|
for (const entry of entries) {
|
|
12928
13397
|
if (entry === ".venv" || entry === "node_modules" || entry === ".git") continue;
|
|
12929
|
-
const abs =
|
|
13398
|
+
const abs = join55(dir, entry);
|
|
12930
13399
|
let isDir;
|
|
12931
13400
|
try {
|
|
12932
|
-
isDir =
|
|
13401
|
+
isDir = statSync15(abs).isDirectory();
|
|
12933
13402
|
} catch {
|
|
12934
13403
|
continue;
|
|
12935
13404
|
}
|
|
@@ -12948,7 +13417,7 @@ function auditSkeleton(skeletonRoot, name, rules = SKELETON_RULES) {
|
|
|
12948
13417
|
if (!rule.appliesTo(rel)) continue;
|
|
12949
13418
|
let contents;
|
|
12950
13419
|
try {
|
|
12951
|
-
contents =
|
|
13420
|
+
contents = readFileSync39(join55(skeletonRoot, rel), "utf8");
|
|
12952
13421
|
} catch {
|
|
12953
13422
|
continue;
|
|
12954
13423
|
}
|
|
@@ -12977,23 +13446,23 @@ function formatViolations2(violations) {
|
|
|
12977
13446
|
|
|
12978
13447
|
// src/scripts/check-skeleton-drift.ts
|
|
12979
13448
|
function discoverSkeletons(root) {
|
|
12980
|
-
const skeletonsDir =
|
|
13449
|
+
const skeletonsDir = join56(root, "_skeletons");
|
|
12981
13450
|
let entries;
|
|
12982
13451
|
try {
|
|
12983
|
-
entries =
|
|
13452
|
+
entries = readdirSync26(skeletonsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
12984
13453
|
} catch {
|
|
12985
13454
|
return [];
|
|
12986
13455
|
}
|
|
12987
|
-
return entries.filter((name) =>
|
|
13456
|
+
return entries.filter((name) => existsSync46(join56(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
|
|
12988
13457
|
}
|
|
12989
13458
|
async function runSkeletonDriftCheck() {
|
|
12990
|
-
const root = (await
|
|
13459
|
+
const root = (await execa21("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
12991
13460
|
const skeletons = discoverSkeletons(root);
|
|
12992
13461
|
let filesConsidered = 0;
|
|
12993
13462
|
for (const name of skeletons) {
|
|
12994
|
-
const skeletonRoot =
|
|
13463
|
+
const skeletonRoot = join56(root, "_skeletons", name);
|
|
12995
13464
|
filesConsidered += findWorkflowFiles(skeletonRoot).length;
|
|
12996
|
-
if (
|
|
13465
|
+
if (existsSync46(join56(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
|
|
12997
13466
|
filesConsidered += 1;
|
|
12998
13467
|
}
|
|
12999
13468
|
}
|
|
@@ -13007,7 +13476,7 @@ async function runSkeletonDriftCheck() {
|
|
|
13007
13476
|
process.exit(1);
|
|
13008
13477
|
}
|
|
13009
13478
|
const violations = skeletons.flatMap(
|
|
13010
|
-
(name) => auditSkeleton(
|
|
13479
|
+
(name) => auditSkeleton(join56(root, "_skeletons", name), name)
|
|
13011
13480
|
);
|
|
13012
13481
|
if (violations.length > 0) {
|
|
13013
13482
|
console.error("\u2717 Skeleton-drift guard: drift found between this repo and its scaffolding\n");
|
|
@@ -13019,9 +13488,9 @@ async function runSkeletonDriftCheck() {
|
|
|
13019
13488
|
}
|
|
13020
13489
|
|
|
13021
13490
|
// src/scripts/check-terraform-input.ts
|
|
13022
|
-
import { execa as
|
|
13491
|
+
import { execa as execa22 } from "execa";
|
|
13023
13492
|
async function runTerraformInputCheck() {
|
|
13024
|
-
const root = (await
|
|
13493
|
+
const root = (await execa22("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
13025
13494
|
const files = findWorkflowFiles(root);
|
|
13026
13495
|
console.log(`audited ${files.length} workflow file(s) under ${root}`);
|
|
13027
13496
|
if (files.length === 0) {
|
|
@@ -13043,8 +13512,8 @@ async function runTerraformInputCheck() {
|
|
|
13043
13512
|
}
|
|
13044
13513
|
|
|
13045
13514
|
// src/commands/check.ts
|
|
13046
|
-
var checkCommand = new
|
|
13047
|
-
"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)"
|
|
13515
|
+
var checkCommand = new Command24("check").description(
|
|
13516
|
+
"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)"
|
|
13048
13517
|
);
|
|
13049
13518
|
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 () => {
|
|
13050
13519
|
await runOwnershipCheck(rawArgsAfter("ownership"));
|
|
@@ -13129,16 +13598,26 @@ checkCommand.command("branch-protection").description(
|
|
|
13129
13598
|
).action(async (opts) => {
|
|
13130
13599
|
await runBranchProtectionCheck(opts.repo, { fix: opts.fix });
|
|
13131
13600
|
});
|
|
13601
|
+
checkCommand.command("plugin-staleness").description(
|
|
13602
|
+
"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."
|
|
13603
|
+
).action(async () => {
|
|
13604
|
+
const cwd = process.env["BIFFO_ORIGINAL_CWD"] || process.cwd();
|
|
13605
|
+
const results = await checkPluginStaleness(cwd, {
|
|
13606
|
+
registry: new RegistryAdapter(),
|
|
13607
|
+
git: new GitAdapter()
|
|
13608
|
+
});
|
|
13609
|
+
console.log(formatStalenessReport(results));
|
|
13610
|
+
});
|
|
13132
13611
|
function rawArgsAfter(subcommand) {
|
|
13133
13612
|
const at = process.argv.indexOf(subcommand);
|
|
13134
13613
|
return at === -1 ? [] : process.argv.slice(at + 1);
|
|
13135
13614
|
}
|
|
13136
13615
|
|
|
13137
13616
|
// src/commands/doctor.ts
|
|
13138
|
-
import { existsSync as
|
|
13139
|
-
import { join as
|
|
13617
|
+
import { existsSync as existsSync47, readFileSync as readFileSync40 } from "fs";
|
|
13618
|
+
import { join as join57, resolve as resolve20 } from "path";
|
|
13140
13619
|
import chalk21 from "chalk";
|
|
13141
|
-
import { Command as
|
|
13620
|
+
import { Command as Command25 } from "commander";
|
|
13142
13621
|
|
|
13143
13622
|
// src/lib/doctor.ts
|
|
13144
13623
|
function checkCheckoutCurrency(facts) {
|
|
@@ -13261,10 +13740,10 @@ function runDoctorChecks(facts) {
|
|
|
13261
13740
|
|
|
13262
13741
|
// src/commands/doctor.ts
|
|
13263
13742
|
var INTEGRATION_BRANCH = "dev";
|
|
13264
|
-
var doctorCommand = new
|
|
13743
|
+
var doctorCommand = new Command25("doctor").description(
|
|
13265
13744
|
"Report repo-state conditions that make everything read from this checkout unreliable"
|
|
13266
13745
|
).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) => {
|
|
13267
|
-
const cwd = options.cwd ?
|
|
13746
|
+
const cwd = options.cwd ? resolve20(options.cwd) : process.cwd();
|
|
13268
13747
|
try {
|
|
13269
13748
|
const findings = await runDoctor({ cwd, fetch: options.fetch !== false });
|
|
13270
13749
|
printFindings(findings);
|
|
@@ -13315,10 +13794,10 @@ async function runDoctor(options, deps = { git: new GitAdapter() }) {
|
|
|
13315
13794
|
return runDoctorChecks(facts);
|
|
13316
13795
|
}
|
|
13317
13796
|
function readLocalCoreVersion(cwd) {
|
|
13318
|
-
const path =
|
|
13319
|
-
if (!
|
|
13797
|
+
const path = join57(cwd, INSTANCE_CORE_FILE);
|
|
13798
|
+
if (!existsSync47(path)) return null;
|
|
13320
13799
|
try {
|
|
13321
|
-
return extractVersionField(
|
|
13800
|
+
return extractVersionField(readFileSync40(path, "utf8"));
|
|
13322
13801
|
} catch {
|
|
13323
13802
|
return null;
|
|
13324
13803
|
}
|
|
@@ -13338,10 +13817,10 @@ function extractVersionField(contents) {
|
|
|
13338
13817
|
return match?.[1] ?? null;
|
|
13339
13818
|
}
|
|
13340
13819
|
function readFossil(cwd) {
|
|
13341
|
-
const path =
|
|
13342
|
-
if (!
|
|
13820
|
+
const path = join57(cwd, CORE_VERSION_FILE);
|
|
13821
|
+
if (!existsSync47(path)) return null;
|
|
13343
13822
|
try {
|
|
13344
|
-
const value =
|
|
13823
|
+
const value = readFileSync40(path, "utf8").trim();
|
|
13345
13824
|
return value === "" ? null : value;
|
|
13346
13825
|
} catch {
|
|
13347
13826
|
return null;
|
|
@@ -13374,9 +13853,9 @@ function printFindings(findings) {
|
|
|
13374
13853
|
import { execSync as execSync7 } from "child_process";
|
|
13375
13854
|
import { GetCallerIdentityCommand as GetCallerIdentityCommand3, STSClient as STSClient3 } from "@aws-sdk/client-sts";
|
|
13376
13855
|
import chalk22 from "chalk";
|
|
13377
|
-
import { Command as
|
|
13856
|
+
import { Command as Command26 } from "commander";
|
|
13378
13857
|
import inquirer8 from "inquirer";
|
|
13379
|
-
var teardownCommand = new
|
|
13858
|
+
var teardownCommand = new Command26("teardown").description(
|
|
13380
13859
|
"Destroy all infrastructure then remove the repo, IAM role, and state bucket \u2014 single command"
|
|
13381
13860
|
).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(
|
|
13382
13861
|
"--confirm <name>",
|
|
@@ -13787,16 +14266,16 @@ function resolveGithubToken4() {
|
|
|
13787
14266
|
import { spawnSync } from "child_process";
|
|
13788
14267
|
import { dirname as dirname12 } from "path";
|
|
13789
14268
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
13790
|
-
import { Command as
|
|
14269
|
+
import { Command as Command27 } from "commander";
|
|
13791
14270
|
|
|
13792
14271
|
// src/lib/packaged-scripts.ts
|
|
13793
|
-
import { existsSync as
|
|
13794
|
-
import { dirname as dirname11, join as
|
|
14272
|
+
import { existsSync as existsSync48 } from "fs";
|
|
14273
|
+
import { dirname as dirname11, join as join58 } from "path";
|
|
13795
14274
|
function findPackagedScript(startDir, relativePath) {
|
|
13796
14275
|
let dir = startDir;
|
|
13797
14276
|
for (; ; ) {
|
|
13798
|
-
const candidate =
|
|
13799
|
-
if (
|
|
14277
|
+
const candidate = join58(dir, relativePath);
|
|
14278
|
+
if (existsSync48(candidate)) return candidate;
|
|
13800
14279
|
const parent = dirname11(dir);
|
|
13801
14280
|
if (parent === dir) return null;
|
|
13802
14281
|
dir = parent;
|
|
@@ -13816,7 +14295,7 @@ function runPackagedScript(script, args, cwd) {
|
|
|
13816
14295
|
return result.status === null ? 2 : result.status;
|
|
13817
14296
|
}
|
|
13818
14297
|
function packagedScriptCommand(spec) {
|
|
13819
|
-
const command = new
|
|
14298
|
+
const command = new Command27(spec.name).description(spec.description).allowExcessArguments(true).allowUnknownOption(true);
|
|
13820
14299
|
if (spec.argument) command.argument(`<${spec.argument.name}>`, spec.argument.description);
|
|
13821
14300
|
return command.action(() => {
|
|
13822
14301
|
const here = dirname12(fileURLToPath6(import.meta.url));
|
|
@@ -13905,7 +14384,7 @@ var runnerDropForensicsCommand = packagedScriptCommand({
|
|
|
13905
14384
|
});
|
|
13906
14385
|
|
|
13907
14386
|
// src/index.ts
|
|
13908
|
-
var program = new
|
|
14387
|
+
var program = new Command28();
|
|
13909
14388
|
function cliVersion() {
|
|
13910
14389
|
try {
|
|
13911
14390
|
return getLatestCoreVersion();
|