@biffo/cli 0.289.0 → 0.290.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +283 -90
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4122,6 +4122,24 @@ function printMigrationBodyDrift(migrations, pairings) {
|
|
|
4122
4122
|
" The template has changed this migration since you carried it.\n An applied migration cannot be re-run, so the carry left yours\n alone and this change will NOT reach you."
|
|
4123
4123
|
)
|
|
4124
4124
|
);
|
|
4125
|
+
if (d.declared) {
|
|
4126
|
+
console.log(
|
|
4127
|
+
chalk4.dim(" declared: ") + chalk4.cyan(d.declared.classification) + chalk4.dim(` \u2014 ${d.declared.reason}`)
|
|
4128
|
+
);
|
|
4129
|
+
const handPortText = "- port the body change into your copy by hand";
|
|
4130
|
+
const followOnText = "- ask upstream for a follow-on migration";
|
|
4131
|
+
if (d.declared.classification === "replay-safe") {
|
|
4132
|
+
console.log(chalk4.dim(` ${handPortText}`));
|
|
4133
|
+
console.log(
|
|
4134
|
+
chalk4.strikethrough.dim(` ${followOnText}`) + chalk4.dim(" (rules out \u2014 replay-safe means your database is already correct)")
|
|
4135
|
+
);
|
|
4136
|
+
} else {
|
|
4137
|
+
console.log(
|
|
4138
|
+
chalk4.strikethrough.dim(` ${handPortText}`) + chalk4.dim(" (rules out \u2014 restating this body cannot fix an already-wrong schema)")
|
|
4139
|
+
);
|
|
4140
|
+
console.log(chalk4.dim(` ${followOnText}`));
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4125
4143
|
}
|
|
4126
4144
|
if (pairings.length === 0) {
|
|
4127
4145
|
console.log(
|
|
@@ -11659,8 +11677,8 @@ async function runOwnershipCheck(argv) {
|
|
|
11659
11677
|
const { stdout } = await execa14("git", ["diff", "--cached", "--name-status"], { cwd: root });
|
|
11660
11678
|
({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
|
|
11661
11679
|
if (messageFile) {
|
|
11662
|
-
const { readFileSync: readFileSync45, existsSync:
|
|
11663
|
-
if (
|
|
11680
|
+
const { readFileSync: readFileSync45, existsSync: existsSync53 } = await import("fs");
|
|
11681
|
+
if (existsSync53(messageFile)) commitMessage = readFileSync45(messageFile, "utf8");
|
|
11664
11682
|
}
|
|
11665
11683
|
} else {
|
|
11666
11684
|
const base = process.env["GITHUB_BASE_REF"] ?? args[0];
|
|
@@ -12158,10 +12176,180 @@ async function runLambdaOutputCheck() {
|
|
|
12158
12176
|
console.log(`\u2713 Lambda output guard: every aws lambda update-function-* call suppresses output`);
|
|
12159
12177
|
}
|
|
12160
12178
|
|
|
12179
|
+
// src/scripts/check-migration-body-change.ts
|
|
12180
|
+
import { execa as execa17 } from "execa";
|
|
12181
|
+
import { existsSync as existsSync44 } from "fs";
|
|
12182
|
+
import { join as join50 } from "path";
|
|
12183
|
+
|
|
12184
|
+
// src/lib/migration-body-change-guard.ts
|
|
12185
|
+
function checkMigrationBodyChangeMarkers(diffs) {
|
|
12186
|
+
const exemptAdded = [];
|
|
12187
|
+
const unchanged = [];
|
|
12188
|
+
const declared = [];
|
|
12189
|
+
const violations = [];
|
|
12190
|
+
for (const d of diffs) {
|
|
12191
|
+
if (d.status === "added") {
|
|
12192
|
+
exemptAdded.push(d.file);
|
|
12193
|
+
continue;
|
|
12194
|
+
}
|
|
12195
|
+
if (d.status !== "modified") {
|
|
12196
|
+
continue;
|
|
12197
|
+
}
|
|
12198
|
+
const oldHash = migrationBodyHash(d.oldContent ?? "");
|
|
12199
|
+
const newHash = migrationBodyHash(d.newContent ?? "");
|
|
12200
|
+
if (oldHash === newHash) {
|
|
12201
|
+
unchanged.push(d.file);
|
|
12202
|
+
continue;
|
|
12203
|
+
}
|
|
12204
|
+
let decl;
|
|
12205
|
+
try {
|
|
12206
|
+
decl = parseBodyChangeDeclaration(d.newContent ?? "");
|
|
12207
|
+
} catch (err) {
|
|
12208
|
+
violations.push({ file: d.file, reason: err.message });
|
|
12209
|
+
continue;
|
|
12210
|
+
}
|
|
12211
|
+
if (decl) {
|
|
12212
|
+
declared.push({ file: d.file, classification: decl.classification });
|
|
12213
|
+
} else {
|
|
12214
|
+
violations.push({
|
|
12215
|
+
file: d.file,
|
|
12216
|
+
reason: `this edit changes the migration's hashed body (DDL, not just a docstring or comment) with no \`${BODY_CHANGE_MARKER}\` declaration.`
|
|
12217
|
+
});
|
|
12218
|
+
}
|
|
12219
|
+
}
|
|
12220
|
+
return {
|
|
12221
|
+
examined: unchanged.length + declared.length + violations.length,
|
|
12222
|
+
exemptAdded,
|
|
12223
|
+
unchanged,
|
|
12224
|
+
declared,
|
|
12225
|
+
violations
|
|
12226
|
+
};
|
|
12227
|
+
}
|
|
12228
|
+
|
|
12229
|
+
// src/scripts/check-migration-body-change.ts
|
|
12230
|
+
var BOLD2 = "\x1B[1m";
|
|
12231
|
+
var DIM2 = "\x1B[2m";
|
|
12232
|
+
var RED2 = "\x1B[31m";
|
|
12233
|
+
var GREEN = "\x1B[32m";
|
|
12234
|
+
var YELLOW2 = "\x1B[33m";
|
|
12235
|
+
var OFF2 = "\x1B[0m";
|
|
12236
|
+
async function showAt(ref, path, cwd) {
|
|
12237
|
+
const result = await execa17("git", ["show", `${ref}:${path}`], { cwd, reject: false });
|
|
12238
|
+
return result.exitCode === 0 ? result.stdout : null;
|
|
12239
|
+
}
|
|
12240
|
+
async function runMigrationBodyChangeCheck(argv) {
|
|
12241
|
+
const root = (await execa17("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
12242
|
+
if (isInstanceRepo(root)) {
|
|
12243
|
+
console.log(
|
|
12244
|
+
`\u2713 migration body-change guard: skipped \u2014 this is an instance (${INSTANCE_CORE_FILE} present). Its migrations/versions/ is a carried, user-owned copy, not the template source this guard protects.`
|
|
12245
|
+
);
|
|
12246
|
+
return;
|
|
12247
|
+
}
|
|
12248
|
+
if (!existsSync44(join50(root, MIGRATIONS_VERSIONS_DIR))) {
|
|
12249
|
+
console.log(`\u2713 migration body-change guard: no ${MIGRATIONS_VERSIONS_DIR} in this repo.`);
|
|
12250
|
+
return;
|
|
12251
|
+
}
|
|
12252
|
+
const base = process.env["GITHUB_BASE_REF"] ?? argv[0];
|
|
12253
|
+
if (!base) {
|
|
12254
|
+
console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
|
|
12255
|
+
process.exit(2);
|
|
12256
|
+
}
|
|
12257
|
+
await execa17("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
|
|
12258
|
+
const { stdout } = await execa17(
|
|
12259
|
+
"git",
|
|
12260
|
+
[
|
|
12261
|
+
"diff",
|
|
12262
|
+
"--no-renames",
|
|
12263
|
+
"--name-status",
|
|
12264
|
+
`origin/${base}...HEAD`,
|
|
12265
|
+
"--",
|
|
12266
|
+
MIGRATIONS_VERSIONS_DIR
|
|
12267
|
+
],
|
|
12268
|
+
{ cwd: root }
|
|
12269
|
+
);
|
|
12270
|
+
const candidates = [];
|
|
12271
|
+
for (const line of stdout.split("\n")) {
|
|
12272
|
+
const parts = line.split(" ").filter(Boolean);
|
|
12273
|
+
const status = parts[0];
|
|
12274
|
+
const path = parts[parts.length - 1];
|
|
12275
|
+
if (!status || !path || parts.length < 2) continue;
|
|
12276
|
+
if (!path.endsWith(".py") || path.endsWith("/__init__.py")) continue;
|
|
12277
|
+
candidates.push({ file: path, status });
|
|
12278
|
+
}
|
|
12279
|
+
const diffs = [];
|
|
12280
|
+
for (const { file, status } of candidates) {
|
|
12281
|
+
if (status.startsWith("A")) {
|
|
12282
|
+
diffs.push({
|
|
12283
|
+
file,
|
|
12284
|
+
status: "added",
|
|
12285
|
+
oldContent: null,
|
|
12286
|
+
newContent: await showAt("HEAD", file, root)
|
|
12287
|
+
});
|
|
12288
|
+
} else if (status.startsWith("M")) {
|
|
12289
|
+
const [oldContent, newContent] = await Promise.all([
|
|
12290
|
+
showAt(`origin/${base}`, file, root),
|
|
12291
|
+
showAt("HEAD", file, root)
|
|
12292
|
+
]);
|
|
12293
|
+
diffs.push({ file, status: "modified", oldContent, newContent });
|
|
12294
|
+
} else if (status.startsWith("D")) {
|
|
12295
|
+
diffs.push({
|
|
12296
|
+
file,
|
|
12297
|
+
status: "deleted",
|
|
12298
|
+
oldContent: await showAt(`origin/${base}`, file, root),
|
|
12299
|
+
newContent: null
|
|
12300
|
+
});
|
|
12301
|
+
}
|
|
12302
|
+
}
|
|
12303
|
+
const result = checkMigrationBodyChangeMarkers(diffs);
|
|
12304
|
+
console.log(
|
|
12305
|
+
`${DIM2}migration body-change guard: examined ${result.examined} already-released migration file(s) changed in this PR` + (result.exemptAdded.length > 0 ? ` (+${result.exemptAdded.length} newly added, exempt)` : "") + `.${OFF2}`
|
|
12306
|
+
);
|
|
12307
|
+
for (const { file, classification } of result.declared) {
|
|
12308
|
+
console.log(` ${GREEN}declared${OFF2} ${file} ${DIM2}\u2192 ${classification}${OFF2}`);
|
|
12309
|
+
}
|
|
12310
|
+
if (result.violations.length === 0) {
|
|
12311
|
+
console.log(`${GREEN}\u2713 migration body-change guard: no undeclared body changes.${OFF2}`);
|
|
12312
|
+
return;
|
|
12313
|
+
}
|
|
12314
|
+
console.error(
|
|
12315
|
+
`
|
|
12316
|
+
${RED2}${BOLD2}\u2717 This PR changes an already-released migration's body with no declaration.${OFF2}
|
|
12317
|
+
`
|
|
12318
|
+
);
|
|
12319
|
+
for (const { file, reason } of result.violations) {
|
|
12320
|
+
console.error(` ${RED2}${file}${OFF2}
|
|
12321
|
+
${DIM2}${reason}${OFF2}`);
|
|
12322
|
+
}
|
|
12323
|
+
console.error(`
|
|
12324
|
+
${BOLD2}Why this is blocked${OFF2}
|
|
12325
|
+
An applied migration cannot be re-run, so a body edit here never reaches an
|
|
12326
|
+
instance that already carried it (#739) \u2014 silently, unless a template author
|
|
12327
|
+
says whether that is safe (docs/guides/core-upgrade.md, "When the template
|
|
12328
|
+
edits a migration you already have"). ${YELLOW2}#931${OFF2} shows the safe case: a
|
|
12329
|
+
docstring-only addendum, which this guard never flags, because it compares
|
|
12330
|
+
the same hashed body \`core-upgrade\` itself compares \u2014 DDL, not prose.
|
|
12331
|
+
|
|
12332
|
+
${BOLD2}What to do${OFF2}
|
|
12333
|
+
Add a marker to the migration, above or beside the edited DDL:
|
|
12334
|
+
|
|
12335
|
+
${DIM2}# biffo:body-change: replay-safe \u2014 <why an applied database is already correct>${OFF2}
|
|
12336
|
+
${DIM2}# biffo:body-change: outcome-changing \u2014 <why an applied database is now wrong>${OFF2}
|
|
12337
|
+
|
|
12338
|
+
Use ${DIM2}replay-safe${OFF2} when re-stating the DDL changes nothing about a database
|
|
12339
|
+
that already ran the old body (a guard, an idempotency check, #670's own
|
|
12340
|
+
fix). Use ${DIM2}outcome-changing${OFF2} when it does not \u2014 an already-applied
|
|
12341
|
+
instance's schema is now actually wrong, and only a follow-on migration
|
|
12342
|
+
converges it. Neither label is enforced yet (#751 is reporting-only, pending
|
|
12343
|
+
more examples) \u2014 but recording it now is what lets that decision be made
|
|
12344
|
+
later instead of never.
|
|
12345
|
+
`);
|
|
12346
|
+
process.exit(1);
|
|
12347
|
+
}
|
|
12348
|
+
|
|
12161
12349
|
// src/scripts/check-pipe-trap.ts
|
|
12162
12350
|
import { readFileSync as readFileSync37, readdirSync as readdirSync22 } from "fs";
|
|
12163
|
-
import { join as
|
|
12164
|
-
import { execa as
|
|
12351
|
+
import { join as join51, relative as relative9 } from "path";
|
|
12352
|
+
import { execa as execa18 } from "execa";
|
|
12165
12353
|
|
|
12166
12354
|
// src/lib/pipe-trap-guard.ts
|
|
12167
12355
|
var STATUS_BEARING = [
|
|
@@ -12257,7 +12445,7 @@ function findPipeTraps(source) {
|
|
|
12257
12445
|
function shellFiles(root) {
|
|
12258
12446
|
const out = [];
|
|
12259
12447
|
for (const dir of ["scripts", ".githooks"]) {
|
|
12260
|
-
const full =
|
|
12448
|
+
const full = join51(root, dir);
|
|
12261
12449
|
let entries;
|
|
12262
12450
|
try {
|
|
12263
12451
|
entries = readdirSync22(full, { withFileTypes: true });
|
|
@@ -12267,13 +12455,13 @@ function shellFiles(root) {
|
|
|
12267
12455
|
for (const entry of entries) {
|
|
12268
12456
|
if (!entry.isFile()) continue;
|
|
12269
12457
|
if (dir === "scripts" && !entry.name.endsWith(".sh")) continue;
|
|
12270
|
-
out.push(
|
|
12458
|
+
out.push(join51(full, entry.name));
|
|
12271
12459
|
}
|
|
12272
12460
|
}
|
|
12273
12461
|
return out;
|
|
12274
12462
|
}
|
|
12275
12463
|
async function runPipeTrapCheck() {
|
|
12276
|
-
const root = (await
|
|
12464
|
+
const root = (await execa18("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
12277
12465
|
const files = shellFiles(root);
|
|
12278
12466
|
console.log(`audited ${files.length} shell file(s) under scripts/ and .githooks/ under ${root}`);
|
|
12279
12467
|
if (files.length === 0) {
|
|
@@ -12300,11 +12488,11 @@ async function runPipeTrapCheck() {
|
|
|
12300
12488
|
}
|
|
12301
12489
|
|
|
12302
12490
|
// src/scripts/check-plugin-allowlist-convention.ts
|
|
12303
|
-
import { execa as
|
|
12491
|
+
import { execa as execa19 } from "execa";
|
|
12304
12492
|
|
|
12305
12493
|
// src/lib/plugin-allowlist-convention.ts
|
|
12306
12494
|
import { readFileSync as readFileSync38 } from "fs";
|
|
12307
|
-
import { join as
|
|
12495
|
+
import { join as join52 } from "path";
|
|
12308
12496
|
var COMPUTE_MAIN_TF = "modules/cloud/aws/compute/main.tf";
|
|
12309
12497
|
var PLUGIN_TEMPLATE_MAIN_TF = "modules/plugins/_template/main.tf";
|
|
12310
12498
|
var ALLOWLIST_MAIN_TF = "modules/cloud/aws/plugin-allowlist/main.tf";
|
|
@@ -12315,7 +12503,7 @@ var PLUGIN = "<plugin>";
|
|
|
12315
12503
|
var ACCOUNT = "<account>";
|
|
12316
12504
|
function read(repoRoot, relative11) {
|
|
12317
12505
|
try {
|
|
12318
|
-
return readFileSync38(
|
|
12506
|
+
return readFileSync38(join52(repoRoot, relative11), "utf8");
|
|
12319
12507
|
} catch {
|
|
12320
12508
|
throw new Error(`plugin-allowlist drift guard: cannot read ${relative11}`);
|
|
12321
12509
|
}
|
|
@@ -12411,7 +12599,7 @@ Plugins would be rejected by require_service_principal (ADR-0009). Fix the glob,
|
|
|
12411
12599
|
|
|
12412
12600
|
// src/scripts/check-plugin-allowlist-convention.ts
|
|
12413
12601
|
async function runPluginAllowlistConventionCheck() {
|
|
12414
|
-
const root = (await
|
|
12602
|
+
const root = (await execa19("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
12415
12603
|
let violations;
|
|
12416
12604
|
try {
|
|
12417
12605
|
violations = checkAllowlistConvention(root);
|
|
@@ -12436,33 +12624,33 @@ async function runPluginAllowlistConventionCheck() {
|
|
|
12436
12624
|
}
|
|
12437
12625
|
|
|
12438
12626
|
// src/scripts/check-plugin-collisions.ts
|
|
12439
|
-
import { existsSync as
|
|
12440
|
-
import { join as
|
|
12441
|
-
import { execa as
|
|
12627
|
+
import { existsSync as existsSync46 } from "fs";
|
|
12628
|
+
import { join as join54 } from "path";
|
|
12629
|
+
import { execa as execa20 } from "execa";
|
|
12442
12630
|
|
|
12443
12631
|
// src/lib/plugin-collision-guard.ts
|
|
12444
|
-
import { existsSync as
|
|
12445
|
-
import { join as
|
|
12632
|
+
import { existsSync as existsSync45, readdirSync as readdirSync23, statSync as statSync14 } from "fs";
|
|
12633
|
+
import { join as join53 } from "path";
|
|
12446
12634
|
var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
|
|
12447
12635
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
|
|
12448
12636
|
function subdirectories(dir) {
|
|
12449
|
-
if (!
|
|
12637
|
+
if (!existsSync45(dir)) return [];
|
|
12450
12638
|
return readdirSync23(dir).filter((entry) => {
|
|
12451
12639
|
if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
|
|
12452
12640
|
try {
|
|
12453
|
-
return statSync14(
|
|
12641
|
+
return statSync14(join53(dir, entry)).isDirectory();
|
|
12454
12642
|
} catch {
|
|
12455
12643
|
return false;
|
|
12456
12644
|
}
|
|
12457
12645
|
});
|
|
12458
12646
|
}
|
|
12459
12647
|
function regularPackagesOf(pluginDir2) {
|
|
12460
|
-
return subdirectories(pluginDir2).filter((name) =>
|
|
12648
|
+
return subdirectories(pluginDir2).filter((name) => existsSync45(join53(pluginDir2, name, "__init__.py"))).sort();
|
|
12461
12649
|
}
|
|
12462
12650
|
function bareTestModulesOf(pluginDir2) {
|
|
12463
|
-
const testsDir =
|
|
12464
|
-
if (!
|
|
12465
|
-
if (
|
|
12651
|
+
const testsDir = join53(pluginDir2, "tests");
|
|
12652
|
+
if (!existsSync45(testsDir)) return [];
|
|
12653
|
+
if (existsSync45(join53(testsDir, "__init__.py"))) return [];
|
|
12466
12654
|
return readdirSync23(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
|
|
12467
12655
|
}
|
|
12468
12656
|
function findCollisions(servicesDir, pluginDirs) {
|
|
@@ -12471,7 +12659,7 @@ function findCollisions(servicesDir, pluginDirs) {
|
|
|
12471
12659
|
const gather = (kind, namesOf) => {
|
|
12472
12660
|
const claims = /* @__PURE__ */ new Map();
|
|
12473
12661
|
for (const plugin of plugins) {
|
|
12474
|
-
for (const name of namesOf(
|
|
12662
|
+
for (const name of namesOf(join53(servicesDir, plugin))) {
|
|
12475
12663
|
claims.set(name, [...claims.get(name) ?? [], plugin]);
|
|
12476
12664
|
}
|
|
12477
12665
|
}
|
|
@@ -12508,9 +12696,9 @@ function formatCollisions(collisions) {
|
|
|
12508
12696
|
|
|
12509
12697
|
// src/scripts/check-plugin-collisions.ts
|
|
12510
12698
|
async function runPluginCollisionCheck() {
|
|
12511
|
-
const root = (await
|
|
12512
|
-
const servicesDir =
|
|
12513
|
-
if (!
|
|
12699
|
+
const root = (await execa20("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
12700
|
+
const servicesDir = join54(root, "services");
|
|
12701
|
+
if (!existsSync46(servicesDir)) {
|
|
12514
12702
|
console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
|
|
12515
12703
|
return;
|
|
12516
12704
|
}
|
|
@@ -12527,11 +12715,11 @@ async function runPluginCollisionCheck() {
|
|
|
12527
12715
|
}
|
|
12528
12716
|
|
|
12529
12717
|
// src/scripts/check-plugin-terraform.ts
|
|
12530
|
-
import { execa as
|
|
12718
|
+
import { execa as execa21 } from "execa";
|
|
12531
12719
|
|
|
12532
12720
|
// src/lib/plugin-terraform-guard.ts
|
|
12533
|
-
import { existsSync as
|
|
12534
|
-
import { dirname as dirname10, join as
|
|
12721
|
+
import { existsSync as existsSync47, readFileSync as readFileSync39, readdirSync as readdirSync24 } from "fs";
|
|
12722
|
+
import { dirname as dirname10, join as join55, relative as relative10, sep as sep4 } from "path";
|
|
12535
12723
|
var SKIP_DIRS3 = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
|
|
12536
12724
|
var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
|
|
12537
12725
|
function findPluginManifests(root) {
|
|
@@ -12546,9 +12734,9 @@ function findPluginManifests(root) {
|
|
|
12546
12734
|
for (const entry of entries) {
|
|
12547
12735
|
if (entry.isDirectory()) {
|
|
12548
12736
|
if (SKIP_DIRS3.has(entry.name)) continue;
|
|
12549
|
-
walk2(
|
|
12737
|
+
walk2(join55(dir, entry.name));
|
|
12550
12738
|
} else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
|
|
12551
|
-
found.push(relative10(root,
|
|
12739
|
+
found.push(relative10(root, join55(dir, entry.name)).split(sep4).join("/"));
|
|
12552
12740
|
}
|
|
12553
12741
|
}
|
|
12554
12742
|
};
|
|
@@ -12573,14 +12761,14 @@ function readSubscriptions(absManifestPath) {
|
|
|
12573
12761
|
}
|
|
12574
12762
|
function checkPluginTerraform(root) {
|
|
12575
12763
|
const violations = [];
|
|
12576
|
-
const coreManifest =
|
|
12764
|
+
const coreManifest = existsSync47(join55(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
|
|
12577
12765
|
for (const manifest of findPluginManifests(root)) {
|
|
12578
12766
|
if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
|
|
12579
|
-
const absManifest =
|
|
12767
|
+
const absManifest = join55(root, manifest);
|
|
12580
12768
|
const subscriptions = readSubscriptions(absManifest);
|
|
12581
12769
|
if (subscriptions === null) continue;
|
|
12582
12770
|
const pluginDir2 = dirname10(absManifest);
|
|
12583
|
-
if (
|
|
12771
|
+
if (existsSync47(join55(pluginDir2, "terraform"))) continue;
|
|
12584
12772
|
const relPluginDir = relative10(root, pluginDir2).split(sep4).join("/");
|
|
12585
12773
|
violations.push({
|
|
12586
12774
|
manifest,
|
|
@@ -12600,7 +12788,7 @@ function formatViolations(violations) {
|
|
|
12600
12788
|
|
|
12601
12789
|
// src/scripts/check-plugin-terraform.ts
|
|
12602
12790
|
async function runPluginTerraformCheck() {
|
|
12603
|
-
const root = (await
|
|
12791
|
+
const root = (await execa21("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
12604
12792
|
const violations = checkPluginTerraform(root);
|
|
12605
12793
|
if (violations.length > 0) {
|
|
12606
12794
|
console.error("\u2717 plugin Terraform guard: event subscriptions with no infrastructure\n");
|
|
@@ -12611,13 +12799,13 @@ async function runPluginTerraformCheck() {
|
|
|
12611
12799
|
}
|
|
12612
12800
|
|
|
12613
12801
|
// src/scripts/check-plugin-tool-supply.ts
|
|
12614
|
-
import { existsSync as
|
|
12615
|
-
import { join as
|
|
12616
|
-
import { execa as
|
|
12802
|
+
import { existsSync as existsSync49 } from "fs";
|
|
12803
|
+
import { join as join57 } from "path";
|
|
12804
|
+
import { execa as execa22 } from "execa";
|
|
12617
12805
|
|
|
12618
12806
|
// src/lib/plugin-tool-supply-audit.ts
|
|
12619
|
-
import { existsSync as
|
|
12620
|
-
import { join as
|
|
12807
|
+
import { existsSync as existsSync48, readFileSync as readFileSync40, readdirSync as readdirSync25, statSync as statSync15 } from "fs";
|
|
12808
|
+
import { join as join56 } from "path";
|
|
12621
12809
|
|
|
12622
12810
|
// src/lib/openrouter-model-snapshot.ts
|
|
12623
12811
|
var OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT = "2026-08-10T06:39:01Z";
|
|
@@ -13034,7 +13222,7 @@ function listDirs(root) {
|
|
|
13034
13222
|
}
|
|
13035
13223
|
return entries.filter((e) => {
|
|
13036
13224
|
try {
|
|
13037
|
-
return statSync15(
|
|
13225
|
+
return statSync15(join56(root, e)).isDirectory();
|
|
13038
13226
|
} catch {
|
|
13039
13227
|
return false;
|
|
13040
13228
|
}
|
|
@@ -13050,7 +13238,7 @@ function walkFiles2(root, accept, skipDir) {
|
|
|
13050
13238
|
return;
|
|
13051
13239
|
}
|
|
13052
13240
|
for (const entry of entries) {
|
|
13053
|
-
const p =
|
|
13241
|
+
const p = join56(dir, entry);
|
|
13054
13242
|
let st;
|
|
13055
13243
|
try {
|
|
13056
13244
|
st = statSync15(p);
|
|
@@ -13076,14 +13264,14 @@ function pluginPythonFiles(pluginDir2) {
|
|
|
13076
13264
|
);
|
|
13077
13265
|
}
|
|
13078
13266
|
function pluginTerraformFiles(pluginDir2) {
|
|
13079
|
-
const tfDir =
|
|
13267
|
+
const tfDir = join56(pluginDir2, "terraform");
|
|
13080
13268
|
let entries;
|
|
13081
13269
|
try {
|
|
13082
13270
|
entries = readdirSync25(tfDir);
|
|
13083
13271
|
} catch {
|
|
13084
13272
|
return [];
|
|
13085
13273
|
}
|
|
13086
|
-
return entries.filter((e) => e.endsWith(".tf")).map((e) =>
|
|
13274
|
+
return entries.filter((e) => e.endsWith(".tf")).map((e) => join56(tfDir, e)).sort();
|
|
13087
13275
|
}
|
|
13088
13276
|
function extractManifestTools(manifestText) {
|
|
13089
13277
|
let parsed;
|
|
@@ -13335,8 +13523,8 @@ function isSnapshotStale(fetchedAt, now) {
|
|
|
13335
13523
|
function normalizeModelId(id) {
|
|
13336
13524
|
return id.endsWith(":online") ? id.slice(0, -":online".length) : id;
|
|
13337
13525
|
}
|
|
13338
|
-
var CONFIG_PY_PATH =
|
|
13339
|
-
var ORCHESTRATION_SCHEMA_PATH =
|
|
13526
|
+
var CONFIG_PY_PATH = join56("services", "api", "src", "api", "config.py");
|
|
13527
|
+
var ORCHESTRATION_SCHEMA_PATH = join56(
|
|
13340
13528
|
"services",
|
|
13341
13529
|
"api",
|
|
13342
13530
|
"src",
|
|
@@ -13348,10 +13536,10 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
|
|
|
13348
13536
|
const knownModelIds = options.knownModelIds ?? OPENROUTER_MODEL_IDS;
|
|
13349
13537
|
const snapshotFetchedAt = options.snapshotFetchedAt ?? OPENROUTER_MODEL_SNAPSHOT_FETCHED_AT;
|
|
13350
13538
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
13351
|
-
const configPath =
|
|
13352
|
-
const orchestrationPath =
|
|
13353
|
-
const configMissing = !
|
|
13354
|
-
const orchestrationSchemaMissing = !
|
|
13539
|
+
const configPath = join56(repoRoot, CONFIG_PY_PATH);
|
|
13540
|
+
const orchestrationPath = join56(repoRoot, ORCHESTRATION_SCHEMA_PATH);
|
|
13541
|
+
const configMissing = !existsSync48(configPath);
|
|
13542
|
+
const orchestrationSchemaMissing = !existsSync48(orchestrationPath);
|
|
13355
13543
|
const knownSet = new Set(knownModelIds);
|
|
13356
13544
|
const snapshotEmpty = knownModelIds.length === 0;
|
|
13357
13545
|
const snapshotStale = isSnapshotStale(snapshotFetchedAt, now);
|
|
@@ -13422,7 +13610,7 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
|
|
|
13422
13610
|
function discoverPluginDirs(pluginsRoot) {
|
|
13423
13611
|
return listDirs(pluginsRoot).filter((name) => {
|
|
13424
13612
|
try {
|
|
13425
|
-
return statSync15(
|
|
13613
|
+
return statSync15(join56(pluginsRoot, name, "biffo.plugin.json")).isFile();
|
|
13426
13614
|
} catch {
|
|
13427
13615
|
return false;
|
|
13428
13616
|
}
|
|
@@ -13435,8 +13623,8 @@ function auditPluginToolSupply(pluginsRoot) {
|
|
|
13435
13623
|
let terraformBlind = false;
|
|
13436
13624
|
let totalDeclaredTools = 0;
|
|
13437
13625
|
for (const name of pluginNames) {
|
|
13438
|
-
const pluginDir2 =
|
|
13439
|
-
const manifestText = readFileSync40(
|
|
13626
|
+
const pluginDir2 = join56(pluginsRoot, name);
|
|
13627
|
+
const manifestText = readFileSync40(join56(pluginDir2, "biffo.plugin.json"), "utf8");
|
|
13440
13628
|
const manifest = extractManifestTools(manifestText);
|
|
13441
13629
|
if (manifest.parseError) {
|
|
13442
13630
|
findings.push({
|
|
@@ -13534,7 +13722,7 @@ function auditPluginToolSupply(pluginsRoot) {
|
|
|
13534
13722
|
requiredEnvVars: envResult.envVars,
|
|
13535
13723
|
missingEnvVars: anyWired ? [] : envResult.envVars,
|
|
13536
13724
|
status: anyWired ? "ok" : "missing-env",
|
|
13537
|
-
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 ${
|
|
13725
|
+
detail: anyWired ? `${entry.predicate}() is satisfiable: at least one of ${JSON.stringify(envResult.envVars)} is wired in Terraform` : `${entry.predicate}() reads ${JSON.stringify(envResult.envVars)} \u2014 NONE of these are wired by any environment_variables block under ${join56(pluginDir2, "terraform")}, so this deployment can never supply it`
|
|
13538
13726
|
});
|
|
13539
13727
|
}
|
|
13540
13728
|
}
|
|
@@ -13565,10 +13753,10 @@ function auditPluginToolSupply(pluginsRoot) {
|
|
|
13565
13753
|
|
|
13566
13754
|
// src/scripts/check-plugin-tool-supply.ts
|
|
13567
13755
|
async function runPluginToolSupplyCheck() {
|
|
13568
|
-
const root = (await
|
|
13756
|
+
const root = (await execa22("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
13569
13757
|
let allOk = true;
|
|
13570
|
-
const pluginsRoot =
|
|
13571
|
-
if (!
|
|
13758
|
+
const pluginsRoot = join57(root, "services", "_plugins");
|
|
13759
|
+
if (!existsSync49(pluginsRoot)) {
|
|
13572
13760
|
console.log("\u2713 plugin tool-supply guard: no services/_plugins/ \u2014 nothing to audit");
|
|
13573
13761
|
} else {
|
|
13574
13762
|
const report = auditPluginToolSupply(pluginsRoot);
|
|
@@ -13598,8 +13786,8 @@ async function runPluginToolSupplyCheck() {
|
|
|
13598
13786
|
console.log(`\u2713 plugin tool-supply guard: ${report.summary}`);
|
|
13599
13787
|
}
|
|
13600
13788
|
}
|
|
13601
|
-
const servicesApiRoot =
|
|
13602
|
-
if (!
|
|
13789
|
+
const servicesApiRoot = join57(root, "services", "api");
|
|
13790
|
+
if (!existsSync49(servicesApiRoot)) {
|
|
13603
13791
|
console.log("\u2713 plugin model-id guard: no services/api/ \u2014 nothing to audit");
|
|
13604
13792
|
} else {
|
|
13605
13793
|
const modelReport = auditDeclaredModelIds(root);
|
|
@@ -13645,7 +13833,7 @@ async function runPluginToolSupplyCheck() {
|
|
|
13645
13833
|
}
|
|
13646
13834
|
|
|
13647
13835
|
// src/scripts/check-release-subject.ts
|
|
13648
|
-
import { execa as
|
|
13836
|
+
import { execa as execa23 } from "execa";
|
|
13649
13837
|
|
|
13650
13838
|
// src/lib/release-version.ts
|
|
13651
13839
|
var MINOR_TYPES = /* @__PURE__ */ new Set(["feat"]);
|
|
@@ -13682,7 +13870,7 @@ async function fetchPrTitleViaGh({
|
|
|
13682
13870
|
PR_NUMBER,
|
|
13683
13871
|
GH_REPO
|
|
13684
13872
|
}) {
|
|
13685
|
-
const { stdout } = await
|
|
13873
|
+
const { stdout } = await execa23(
|
|
13686
13874
|
"gh",
|
|
13687
13875
|
["pr", "view", PR_NUMBER, "--repo", GH_REPO, "--json", "title", "--jq", ".title"],
|
|
13688
13876
|
{ env: { ...process.env, GH_TOKEN } }
|
|
@@ -13718,7 +13906,7 @@ async function resolveReleaseSubject({
|
|
|
13718
13906
|
);
|
|
13719
13907
|
}
|
|
13720
13908
|
}
|
|
13721
|
-
return (await
|
|
13909
|
+
return (await execa23("git", ["log", "-1", "--format=%s"], { cwd })).stdout.trim();
|
|
13722
13910
|
}
|
|
13723
13911
|
async function runReleaseSubjectCheck(argv) {
|
|
13724
13912
|
const base = process.env["GITHUB_BASE_REF"] ?? argv[0];
|
|
@@ -13726,9 +13914,9 @@ async function runReleaseSubjectCheck(argv) {
|
|
|
13726
13914
|
console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
|
|
13727
13915
|
process.exit(2);
|
|
13728
13916
|
}
|
|
13729
|
-
const root = (await
|
|
13730
|
-
await
|
|
13731
|
-
const { stdout } = await
|
|
13917
|
+
const root = (await execa23("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
13918
|
+
await execa23("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
|
|
13919
|
+
const { stdout } = await execa23("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
|
|
13732
13920
|
cwd: root
|
|
13733
13921
|
});
|
|
13734
13922
|
const changedFiles = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
@@ -13979,13 +14167,13 @@ async function runSharedFileReductionCheck(args) {
|
|
|
13979
14167
|
}
|
|
13980
14168
|
|
|
13981
14169
|
// src/scripts/check-skeleton-drift.ts
|
|
13982
|
-
import { existsSync as
|
|
13983
|
-
import { join as
|
|
13984
|
-
import { execa as
|
|
14170
|
+
import { existsSync as existsSync50, readdirSync as readdirSync27 } from "fs";
|
|
14171
|
+
import { join as join59 } from "path";
|
|
14172
|
+
import { execa as execa24 } from "execa";
|
|
13985
14173
|
|
|
13986
14174
|
// src/lib/skeleton-drift-guard.ts
|
|
13987
14175
|
import { readFileSync as readFileSync43, readdirSync as readdirSync26, statSync as statSync16 } from "fs";
|
|
13988
|
-
import { join as
|
|
14176
|
+
import { join as join58 } from "path";
|
|
13989
14177
|
var isWorkflow = (rel) => rel.startsWith(".github/workflows/") && (rel.endsWith(".yml") || rel.endsWith(".yaml"));
|
|
13990
14178
|
var isRootLayout = (rel) => rel.endsWith("src/app/layout.tsx");
|
|
13991
14179
|
var uncommented = (contents) => contents.split("\n").filter((line) => !/^\s*(\/\/|\/\*|\*)/.test(line)).join("\n");
|
|
@@ -14049,7 +14237,7 @@ function walk(dir, base = dir) {
|
|
|
14049
14237
|
}
|
|
14050
14238
|
for (const entry of entries) {
|
|
14051
14239
|
if (entry === ".venv" || entry === "node_modules" || entry === ".git") continue;
|
|
14052
|
-
const abs =
|
|
14240
|
+
const abs = join58(dir, entry);
|
|
14053
14241
|
let isDir;
|
|
14054
14242
|
try {
|
|
14055
14243
|
isDir = statSync16(abs).isDirectory();
|
|
@@ -14071,7 +14259,7 @@ function auditSkeleton(skeletonRoot, name, rules = SKELETON_RULES) {
|
|
|
14071
14259
|
if (!rule.appliesTo(rel)) continue;
|
|
14072
14260
|
let contents;
|
|
14073
14261
|
try {
|
|
14074
|
-
contents = readFileSync43(
|
|
14262
|
+
contents = readFileSync43(join58(skeletonRoot, rel), "utf8");
|
|
14075
14263
|
} catch {
|
|
14076
14264
|
continue;
|
|
14077
14265
|
}
|
|
@@ -14100,23 +14288,23 @@ function formatViolations2(violations) {
|
|
|
14100
14288
|
|
|
14101
14289
|
// src/scripts/check-skeleton-drift.ts
|
|
14102
14290
|
function discoverSkeletons(root) {
|
|
14103
|
-
const skeletonsDir =
|
|
14291
|
+
const skeletonsDir = join59(root, "_skeletons");
|
|
14104
14292
|
let entries;
|
|
14105
14293
|
try {
|
|
14106
14294
|
entries = readdirSync27(skeletonsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
14107
14295
|
} catch {
|
|
14108
14296
|
return [];
|
|
14109
14297
|
}
|
|
14110
|
-
return entries.filter((name) =>
|
|
14298
|
+
return entries.filter((name) => existsSync50(join59(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
|
|
14111
14299
|
}
|
|
14112
14300
|
async function runSkeletonDriftCheck() {
|
|
14113
|
-
const root = (await
|
|
14301
|
+
const root = (await execa24("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
14114
14302
|
const skeletons = discoverSkeletons(root);
|
|
14115
14303
|
let filesConsidered = 0;
|
|
14116
14304
|
for (const name of skeletons) {
|
|
14117
|
-
const skeletonRoot =
|
|
14305
|
+
const skeletonRoot = join59(root, "_skeletons", name);
|
|
14118
14306
|
filesConsidered += findWorkflowFiles(skeletonRoot).length;
|
|
14119
|
-
if (
|
|
14307
|
+
if (existsSync50(join59(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
|
|
14120
14308
|
filesConsidered += 1;
|
|
14121
14309
|
}
|
|
14122
14310
|
}
|
|
@@ -14130,7 +14318,7 @@ async function runSkeletonDriftCheck() {
|
|
|
14130
14318
|
process.exit(1);
|
|
14131
14319
|
}
|
|
14132
14320
|
const violations = skeletons.flatMap(
|
|
14133
|
-
(name) => auditSkeleton(
|
|
14321
|
+
(name) => auditSkeleton(join59(root, "_skeletons", name), name)
|
|
14134
14322
|
);
|
|
14135
14323
|
if (violations.length > 0) {
|
|
14136
14324
|
console.error("\u2717 Skeleton-drift guard: drift found between this repo and its scaffolding\n");
|
|
@@ -14142,9 +14330,9 @@ async function runSkeletonDriftCheck() {
|
|
|
14142
14330
|
}
|
|
14143
14331
|
|
|
14144
14332
|
// src/scripts/check-terraform-input.ts
|
|
14145
|
-
import { execa as
|
|
14333
|
+
import { execa as execa25 } from "execa";
|
|
14146
14334
|
async function runTerraformInputCheck() {
|
|
14147
|
-
const root = (await
|
|
14335
|
+
const root = (await execa25("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
14148
14336
|
const files = findWorkflowFiles(root);
|
|
14149
14337
|
console.log(`audited ${files.length} workflow file(s) under ${root}`);
|
|
14150
14338
|
if (files.length === 0) {
|
|
@@ -14167,11 +14355,16 @@ async function runTerraformInputCheck() {
|
|
|
14167
14355
|
|
|
14168
14356
|
// src/commands/check.ts
|
|
14169
14357
|
var checkCommand = new Command24("check").description(
|
|
14170
|
-
"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)"
|
|
14358
|
+
"Repo guards (ownership, release subject, plugin terraform, plugin collisions, eventbridge-log-permissions, plugin-tool-supply, core-direct-paths, cognito-invite-template, lambda-output, pipe-trap, codeql-suppression, skeleton-drift, terraform-input, plugin-allowlist-convention, migration-body-change) run in CI and git hooks, plus out-of-band audits (branch protection, plugin-staleness)"
|
|
14171
14359
|
);
|
|
14172
14360
|
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 () => {
|
|
14173
14361
|
await runOwnershipCheck(rawArgsAfter("ownership"));
|
|
14174
14362
|
});
|
|
14363
|
+
checkCommand.command("migration-body-change").description(
|
|
14364
|
+
"Refuse a PR that changes an already-released migration's HASHED body (DDL, not a docstring or comment) with no `# biffo:body-change:` marker (#751) \u2014 mirrors migrationBodyHash's normalisation, so a docstring-only edit (#931) stays silent. Reporting-only classification, not enforcement: this only requires the marker exist, it does not act on what it declares."
|
|
14365
|
+
).argument("[base]", "Base branch to diff against; defaults to $GITHUB_BASE_REF").action(async () => {
|
|
14366
|
+
await runMigrationBodyChangeCheck(rawArgsAfter("migration-body-change"));
|
|
14367
|
+
});
|
|
14175
14368
|
checkCommand.command("release-subject").description("Require a Conventional Commits PR title on template-owned changes (#423)").argument("[base]", "Base branch to diff against; defaults to $GITHUB_BASE_REF").action(async () => {
|
|
14176
14369
|
await runReleaseSubjectCheck(rawArgsAfter("release-subject"));
|
|
14177
14370
|
});
|
|
@@ -14280,8 +14473,8 @@ function rawArgsAfter(subcommand) {
|
|
|
14280
14473
|
}
|
|
14281
14474
|
|
|
14282
14475
|
// src/commands/doctor.ts
|
|
14283
|
-
import { existsSync as
|
|
14284
|
-
import { join as
|
|
14476
|
+
import { existsSync as existsSync51, readFileSync as readFileSync44 } from "fs";
|
|
14477
|
+
import { join as join60, resolve as resolve20 } from "path";
|
|
14285
14478
|
import chalk21 from "chalk";
|
|
14286
14479
|
import { Command as Command25 } from "commander";
|
|
14287
14480
|
|
|
@@ -14460,8 +14653,8 @@ async function runDoctor(options, deps = { git: new GitAdapter() }) {
|
|
|
14460
14653
|
return runDoctorChecks(facts);
|
|
14461
14654
|
}
|
|
14462
14655
|
function readLocalCoreVersion(cwd) {
|
|
14463
|
-
const path =
|
|
14464
|
-
if (!
|
|
14656
|
+
const path = join60(cwd, INSTANCE_CORE_FILE);
|
|
14657
|
+
if (!existsSync51(path)) return null;
|
|
14465
14658
|
try {
|
|
14466
14659
|
return extractVersionField(readFileSync44(path, "utf8"));
|
|
14467
14660
|
} catch {
|
|
@@ -14483,8 +14676,8 @@ function extractVersionField(contents) {
|
|
|
14483
14676
|
return match?.[1] ?? null;
|
|
14484
14677
|
}
|
|
14485
14678
|
function readFossil(cwd) {
|
|
14486
|
-
const path =
|
|
14487
|
-
if (!
|
|
14679
|
+
const path = join60(cwd, CORE_VERSION_FILE);
|
|
14680
|
+
if (!existsSync51(path)) return null;
|
|
14488
14681
|
try {
|
|
14489
14682
|
const value = readFileSync44(path, "utf8").trim();
|
|
14490
14683
|
return value === "" ? null : value;
|
|
@@ -14935,13 +15128,13 @@ import { fileURLToPath as fileURLToPath6 } from "url";
|
|
|
14935
15128
|
import { Command as Command27 } from "commander";
|
|
14936
15129
|
|
|
14937
15130
|
// src/lib/packaged-scripts.ts
|
|
14938
|
-
import { existsSync as
|
|
14939
|
-
import { dirname as dirname11, join as
|
|
15131
|
+
import { existsSync as existsSync52 } from "fs";
|
|
15132
|
+
import { dirname as dirname11, join as join61 } from "path";
|
|
14940
15133
|
function findPackagedScript(startDir, relativePath) {
|
|
14941
15134
|
let dir = startDir;
|
|
14942
15135
|
for (; ; ) {
|
|
14943
|
-
const candidate =
|
|
14944
|
-
if (
|
|
15136
|
+
const candidate = join61(dir, relativePath);
|
|
15137
|
+
if (existsSync52(candidate)) return candidate;
|
|
14945
15138
|
const parent = dirname11(dir);
|
|
14946
15139
|
if (parent === dir) return null;
|
|
14947
15140
|
dir = parent;
|