@biffo/cli 0.267.4 → 0.267.6
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.
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Inert placeholder — see `_skeletons/sibling-template`'s copy for the real guard.
|
|
2
|
+
|
|
3
|
+
biffo-template#1330. `shared-files.json`'s `filesFromSkeleton` parity check
|
|
4
|
+
(`cli/src/lib/shared-files-parity.test.ts`) requires every entry to exist in
|
|
5
|
+
every skeleton the marker table can select, so a missing copy can never be a
|
|
6
|
+
silent empty-copy hazard — the same reasoning already applied to
|
|
7
|
+
`.prettierrc`/`.prettierignore` for #1343, where a plugin repo carries an
|
|
8
|
+
inert copy of a file it has no functional use for.
|
|
9
|
+
|
|
10
|
+
This guard has no meaning for a plugin repo. The real file asserts that every
|
|
11
|
+
literal `/api/v1/...` call a sibling app's frontend makes resolves to a route
|
|
12
|
+
that app's own BFF (`services/api`) registers, reading both halves from
|
|
13
|
+
`api.main.app` and `apps/frontend/src/**` — a plugin repo has neither of
|
|
14
|
+
those trees: it mounts into a host application rather than owning a
|
|
15
|
+
frontend-to-BFF seam of its own.
|
|
16
|
+
|
|
17
|
+
Deliberately NOT the real file's content. The real guard imports `fastapi`
|
|
18
|
+
and `starlette` and does `from api.main import app`; none of those are a
|
|
19
|
+
dependency or a module this skeleton ships, so a verbatim copy would fail
|
|
20
|
+
`pyright` (which walks the whole tree, `[tool.pyright]` in `pyproject.toml`
|
|
21
|
+
carries no exclude for this path) on every plugin repo scaffolded from it.
|
|
22
|
+
`pytest` is not at risk either way — `[tool.pytest.ini_options]`'s
|
|
23
|
+
`testpaths = ["tests"]` already excludes this directory from collection — but
|
|
24
|
+
`pyright` and `ruff check .` are not scoped the same way, so this stub has to
|
|
25
|
+
be syntactically inert on its own: no imports, no names, nothing to check.
|
|
26
|
+
"""
|
package/dist/index.js
CHANGED
|
@@ -10006,7 +10006,7 @@ import { join as join38 } from "path";
|
|
|
10006
10006
|
import { execa as execa8 } from "execa";
|
|
10007
10007
|
|
|
10008
10008
|
// src/lib/core-direct-paths-audit.ts
|
|
10009
|
-
import { readFileSync as readFileSync28, readdirSync as readdirSync15, statSync as statSync8 } from "fs";
|
|
10009
|
+
import { existsSync as existsSync36, readFileSync as readFileSync28, readdirSync as readdirSync15, statSync as statSync8 } from "fs";
|
|
10010
10010
|
import { join as join37 } from "path";
|
|
10011
10011
|
var EXTERNAL_BASE_IDENTIFIERS = ["CORE_API_URL"];
|
|
10012
10012
|
var API_ROUTE_PREFIX = "/api/v1";
|
|
@@ -10264,6 +10264,39 @@ function pathMatchesAnyCorePrefix(normalized, corePrefixes, apiRoutePrefix = API
|
|
|
10264
10264
|
}
|
|
10265
10265
|
return false;
|
|
10266
10266
|
}
|
|
10267
|
+
function resolveSiblingCoreSrc(params) {
|
|
10268
|
+
const { estateDir, sibling } = params;
|
|
10269
|
+
const configPath = join37(estateDir, sibling, "biffo.sibling.json");
|
|
10270
|
+
let raw;
|
|
10271
|
+
try {
|
|
10272
|
+
raw = readFileSync28(configPath, "utf8");
|
|
10273
|
+
} catch (err) {
|
|
10274
|
+
throw new Error(
|
|
10275
|
+
`cannot resolve ${sibling}'s core: ${configPath} does not exist or is unreadable (${err.message}) -- refusing to guess which core serves this sibling.`
|
|
10276
|
+
);
|
|
10277
|
+
}
|
|
10278
|
+
let parsed;
|
|
10279
|
+
try {
|
|
10280
|
+
parsed = JSON.parse(raw);
|
|
10281
|
+
} catch (err) {
|
|
10282
|
+
throw new Error(
|
|
10283
|
+
`cannot resolve ${sibling}'s core: ${configPath} is not valid JSON (${err.message}).`
|
|
10284
|
+
);
|
|
10285
|
+
}
|
|
10286
|
+
const coreProject = parsed !== null && typeof parsed === "object" && "core_project" in parsed ? parsed.core_project : void 0;
|
|
10287
|
+
if (typeof coreProject !== "string" || coreProject.trim() === "") {
|
|
10288
|
+
throw new Error(
|
|
10289
|
+
`cannot resolve ${sibling}'s core: ${configPath} has no non-empty "core_project" field.`
|
|
10290
|
+
);
|
|
10291
|
+
}
|
|
10292
|
+
const coreApiSrcDir = join37(estateDir, coreProject, "services", "api", "src");
|
|
10293
|
+
if (!existsSync36(coreApiSrcDir)) {
|
|
10294
|
+
throw new Error(
|
|
10295
|
+
`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.`
|
|
10296
|
+
);
|
|
10297
|
+
}
|
|
10298
|
+
return { sibling, coreProject, coreApiSrcDir };
|
|
10299
|
+
}
|
|
10267
10300
|
function auditSiblingCoreDirectPaths(params) {
|
|
10268
10301
|
const externalBases = params.externalBases ?? EXTERNAL_BASE_IDENTIFIERS;
|
|
10269
10302
|
const frontend = auditFrontendExtraction(params.frontendSrcDir, externalBases);
|
|
@@ -10301,10 +10334,26 @@ async function runCoreDirectPathsCheck(opts = {}) {
|
|
|
10301
10334
|
const root = (await execa8("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
10302
10335
|
const sibling = opts.sibling ?? "sibling-template (self-check)";
|
|
10303
10336
|
const frontendSrcDir = opts.frontendSrc ?? join38(root, "_skeletons", "sibling-template", "apps", "frontend", "src");
|
|
10304
|
-
|
|
10337
|
+
let coreApiSrcDir;
|
|
10338
|
+
let coreProject = null;
|
|
10339
|
+
if (opts.coreSrc) {
|
|
10340
|
+
coreApiSrcDir = opts.coreSrc;
|
|
10341
|
+
} else if (opts.sibling && opts.estate) {
|
|
10342
|
+
let resolution;
|
|
10343
|
+
try {
|
|
10344
|
+
resolution = resolveSiblingCoreSrc({ estateDir: opts.estate, sibling: opts.sibling });
|
|
10345
|
+
} catch (err) {
|
|
10346
|
+
console.error(`\u2717 core-direct-paths guard (${sibling}): ${err.message}`);
|
|
10347
|
+
process.exit(1);
|
|
10348
|
+
}
|
|
10349
|
+
coreApiSrcDir = resolution.coreApiSrcDir;
|
|
10350
|
+
coreProject = resolution.coreProject;
|
|
10351
|
+
} else {
|
|
10352
|
+
coreApiSrcDir = join38(root, "services", "api", "src");
|
|
10353
|
+
}
|
|
10305
10354
|
const report = auditSiblingCoreDirectPaths({ sibling, frontendSrcDir, coreApiSrcDir });
|
|
10306
10355
|
console.log(
|
|
10307
|
-
`audited ${report.extractedCount} core-direct call site(s) across ${report.frontendFiles} frontend file(s) under ${frontendSrcDir}, against ${report.corePrefixCount} route prefix(es) from ${report.coreFiles} core file(s) under ${coreApiSrcDir}`
|
|
10356
|
+
`audited ${report.extractedCount} core-direct call site(s) across ${report.frontendFiles} frontend file(s) under ${frontendSrcDir}, against ${report.corePrefixCount} route prefix(es) from ${report.coreFiles} core file(s) under ${coreApiSrcDir}` + (coreProject ? ` (core project: ${coreProject})` : "")
|
|
10308
10357
|
);
|
|
10309
10358
|
if (!report.ok) {
|
|
10310
10359
|
console.error(`\u2717 core-direct-paths guard (${sibling}): unmatched or unresolved call site(s)
|
|
@@ -10366,8 +10415,8 @@ async function runOwnershipCheck(argv) {
|
|
|
10366
10415
|
const { stdout } = await execa9("git", ["diff", "--cached", "--name-status"], { cwd: root });
|
|
10367
10416
|
({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
|
|
10368
10417
|
if (messageFile) {
|
|
10369
|
-
const { readFileSync: readFileSync37, existsSync:
|
|
10370
|
-
if (
|
|
10418
|
+
const { readFileSync: readFileSync37, existsSync: existsSync45 } = await import("fs");
|
|
10419
|
+
if (existsSync45(messageFile)) commitMessage = readFileSync37(messageFile, "utf8");
|
|
10371
10420
|
}
|
|
10372
10421
|
} else {
|
|
10373
10422
|
const base = process.env["GITHUB_BASE_REF"] ?? args[0];
|
|
@@ -10985,17 +11034,17 @@ async function runPipeTrapCheck() {
|
|
|
10985
11034
|
}
|
|
10986
11035
|
|
|
10987
11036
|
// src/scripts/check-plugin-collisions.ts
|
|
10988
|
-
import { existsSync as
|
|
11037
|
+
import { existsSync as existsSync38 } from "fs";
|
|
10989
11038
|
import { join as join44 } from "path";
|
|
10990
11039
|
import { execa as execa13 } from "execa";
|
|
10991
11040
|
|
|
10992
11041
|
// src/lib/plugin-collision-guard.ts
|
|
10993
|
-
import { existsSync as
|
|
11042
|
+
import { existsSync as existsSync37, readdirSync as readdirSync19, statSync as statSync11 } from "fs";
|
|
10994
11043
|
import { join as join43 } from "path";
|
|
10995
11044
|
var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
|
|
10996
11045
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
|
|
10997
11046
|
function subdirectories(dir) {
|
|
10998
|
-
if (!
|
|
11047
|
+
if (!existsSync37(dir)) return [];
|
|
10999
11048
|
return readdirSync19(dir).filter((entry) => {
|
|
11000
11049
|
if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
|
|
11001
11050
|
try {
|
|
@@ -11006,12 +11055,12 @@ function subdirectories(dir) {
|
|
|
11006
11055
|
});
|
|
11007
11056
|
}
|
|
11008
11057
|
function regularPackagesOf(pluginDir2) {
|
|
11009
|
-
return subdirectories(pluginDir2).filter((name) =>
|
|
11058
|
+
return subdirectories(pluginDir2).filter((name) => existsSync37(join43(pluginDir2, name, "__init__.py"))).sort();
|
|
11010
11059
|
}
|
|
11011
11060
|
function bareTestModulesOf(pluginDir2) {
|
|
11012
11061
|
const testsDir = join43(pluginDir2, "tests");
|
|
11013
|
-
if (!
|
|
11014
|
-
if (
|
|
11062
|
+
if (!existsSync37(testsDir)) return [];
|
|
11063
|
+
if (existsSync37(join43(testsDir, "__init__.py"))) return [];
|
|
11015
11064
|
return readdirSync19(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
|
|
11016
11065
|
}
|
|
11017
11066
|
function findCollisions(servicesDir, pluginDirs) {
|
|
@@ -11059,7 +11108,7 @@ function formatCollisions(collisions) {
|
|
|
11059
11108
|
async function runPluginCollisionCheck() {
|
|
11060
11109
|
const root = (await execa13("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
11061
11110
|
const servicesDir = join44(root, "services");
|
|
11062
|
-
if (!
|
|
11111
|
+
if (!existsSync38(servicesDir)) {
|
|
11063
11112
|
console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
|
|
11064
11113
|
return;
|
|
11065
11114
|
}
|
|
@@ -11079,7 +11128,7 @@ async function runPluginCollisionCheck() {
|
|
|
11079
11128
|
import { execa as execa14 } from "execa";
|
|
11080
11129
|
|
|
11081
11130
|
// src/lib/plugin-terraform-guard.ts
|
|
11082
|
-
import { existsSync as
|
|
11131
|
+
import { existsSync as existsSync39, readFileSync as readFileSync33, readdirSync as readdirSync20 } from "fs";
|
|
11083
11132
|
import { dirname as dirname9, join as join45, relative as relative7, sep as sep3 } from "path";
|
|
11084
11133
|
var SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
|
|
11085
11134
|
var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
|
|
@@ -11122,14 +11171,14 @@ function readSubscriptions(absManifestPath) {
|
|
|
11122
11171
|
}
|
|
11123
11172
|
function checkPluginTerraform(root) {
|
|
11124
11173
|
const violations = [];
|
|
11125
|
-
const coreManifest =
|
|
11174
|
+
const coreManifest = existsSync39(join45(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
|
|
11126
11175
|
for (const manifest of findPluginManifests(root)) {
|
|
11127
11176
|
if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
|
|
11128
11177
|
const absManifest = join45(root, manifest);
|
|
11129
11178
|
const subscriptions = readSubscriptions(absManifest);
|
|
11130
11179
|
if (subscriptions === null) continue;
|
|
11131
11180
|
const pluginDir2 = dirname9(absManifest);
|
|
11132
|
-
if (
|
|
11181
|
+
if (existsSync39(join45(pluginDir2, "terraform"))) continue;
|
|
11133
11182
|
const relPluginDir = relative7(root, pluginDir2).split(sep3).join("/");
|
|
11134
11183
|
violations.push({
|
|
11135
11184
|
manifest,
|
|
@@ -11160,12 +11209,12 @@ async function runPluginTerraformCheck() {
|
|
|
11160
11209
|
}
|
|
11161
11210
|
|
|
11162
11211
|
// src/scripts/check-plugin-tool-supply.ts
|
|
11163
|
-
import { existsSync as
|
|
11212
|
+
import { existsSync as existsSync41 } from "fs";
|
|
11164
11213
|
import { join as join47 } from "path";
|
|
11165
11214
|
import { execa as execa15 } from "execa";
|
|
11166
11215
|
|
|
11167
11216
|
// src/lib/plugin-tool-supply-audit.ts
|
|
11168
|
-
import { existsSync as
|
|
11217
|
+
import { existsSync as existsSync40, readFileSync as readFileSync34, readdirSync as readdirSync21, statSync as statSync12 } from "fs";
|
|
11169
11218
|
import { join as join46 } from "path";
|
|
11170
11219
|
|
|
11171
11220
|
// src/lib/openrouter-model-snapshot.ts
|
|
@@ -11899,8 +11948,8 @@ function auditDeclaredModelIds(repoRoot, options = {}) {
|
|
|
11899
11948
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
11900
11949
|
const configPath = join46(repoRoot, CONFIG_PY_PATH);
|
|
11901
11950
|
const orchestrationPath = join46(repoRoot, ORCHESTRATION_SCHEMA_PATH);
|
|
11902
|
-
const configMissing = !
|
|
11903
|
-
const orchestrationSchemaMissing = !
|
|
11951
|
+
const configMissing = !existsSync40(configPath);
|
|
11952
|
+
const orchestrationSchemaMissing = !existsSync40(orchestrationPath);
|
|
11904
11953
|
const knownSet = new Set(knownModelIds);
|
|
11905
11954
|
const snapshotEmpty = knownModelIds.length === 0;
|
|
11906
11955
|
const snapshotStale = isSnapshotStale(snapshotFetchedAt, now);
|
|
@@ -12117,7 +12166,7 @@ async function runPluginToolSupplyCheck() {
|
|
|
12117
12166
|
const root = (await execa15("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
12118
12167
|
let allOk = true;
|
|
12119
12168
|
const pluginsRoot = join47(root, "services", "_plugins");
|
|
12120
|
-
if (!
|
|
12169
|
+
if (!existsSync41(pluginsRoot)) {
|
|
12121
12170
|
console.log("\u2713 plugin tool-supply guard: no services/_plugins/ \u2014 nothing to audit");
|
|
12122
12171
|
} else {
|
|
12123
12172
|
const report = auditPluginToolSupply(pluginsRoot);
|
|
@@ -12148,7 +12197,7 @@ async function runPluginToolSupplyCheck() {
|
|
|
12148
12197
|
}
|
|
12149
12198
|
}
|
|
12150
12199
|
const servicesApiRoot = join47(root, "services", "api");
|
|
12151
|
-
if (!
|
|
12200
|
+
if (!existsSync41(servicesApiRoot)) {
|
|
12152
12201
|
console.log("\u2713 plugin model-id guard: no services/api/ \u2014 nothing to audit");
|
|
12153
12202
|
} else {
|
|
12154
12203
|
const modelReport = auditDeclaredModelIds(root);
|
|
@@ -12325,7 +12374,7 @@ async function runReleaseSubjectCheck(argv) {
|
|
|
12325
12374
|
}
|
|
12326
12375
|
|
|
12327
12376
|
// src/scripts/check-skeleton-drift.ts
|
|
12328
|
-
import { existsSync as
|
|
12377
|
+
import { existsSync as existsSync42, readdirSync as readdirSync23 } from "fs";
|
|
12329
12378
|
import { join as join49 } from "path";
|
|
12330
12379
|
import { execa as execa17 } from "execa";
|
|
12331
12380
|
|
|
@@ -12453,7 +12502,7 @@ function discoverSkeletons(root) {
|
|
|
12453
12502
|
} catch {
|
|
12454
12503
|
return [];
|
|
12455
12504
|
}
|
|
12456
|
-
return entries.filter((name) =>
|
|
12505
|
+
return entries.filter((name) => existsSync42(join49(skeletonsDir, name, ".github", "workflows", "ci.yml"))).sort();
|
|
12457
12506
|
}
|
|
12458
12507
|
async function runSkeletonDriftCheck() {
|
|
12459
12508
|
const root = (await execa17("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
@@ -12462,7 +12511,7 @@ async function runSkeletonDriftCheck() {
|
|
|
12462
12511
|
for (const name of skeletons) {
|
|
12463
12512
|
const skeletonRoot = join49(root, "_skeletons", name);
|
|
12464
12513
|
filesConsidered += findWorkflowFiles(skeletonRoot).length;
|
|
12465
|
-
if (
|
|
12514
|
+
if (existsSync42(join49(skeletonRoot, "apps", "frontend", "src", "app", "layout.tsx"))) {
|
|
12466
12515
|
filesConsidered += 1;
|
|
12467
12516
|
}
|
|
12468
12517
|
}
|
|
@@ -12543,10 +12592,18 @@ checkCommand.command("plugin-tool-supply").description(
|
|
|
12543
12592
|
await runPluginToolSupplyCheck();
|
|
12544
12593
|
});
|
|
12545
12594
|
checkCommand.command("core-direct-paths").description(
|
|
12546
|
-
"Refuse a frontend's core-direct call site (bypassing its own BFF) naming a route prefix core does not register (#1377). Defaults to a self-check of the sibling skeleton against this repo's own services/api/src; --sibling/--frontend-src
|
|
12547
|
-
).option("--sibling <name>", "Label for the report").option("--frontend-src <dir>", "Sibling's frontend source directory to scan").option(
|
|
12548
|
-
|
|
12549
|
-
|
|
12595
|
+
"Refuse a frontend's core-direct call site (bypassing its own BFF) naming a route prefix core does not register (#1377). Defaults to a self-check of the sibling skeleton against this repo's own services/api/src; --sibling/--frontend-src plus either --estate (resolve the sibling's OWN core from its biffo.sibling.json) or --core-src (an explicit override) point it at a real checked-out sibling instead"
|
|
12596
|
+
).option("--sibling <name>", "Label for the report").option("--frontend-src <dir>", "Sibling's frontend source directory to scan").option(
|
|
12597
|
+
"--estate <dir>",
|
|
12598
|
+
"Directory holding every cloned repo; resolves --sibling's OWN core from its biffo.sibling.json core_project field (ignored if --core-src is also given)"
|
|
12599
|
+
).option(
|
|
12600
|
+
"--core-src <dir>",
|
|
12601
|
+
"Core API's source directory (ground truth for route prefixes) \u2014 explicit override, takes precedence over --estate resolution"
|
|
12602
|
+
).action(
|
|
12603
|
+
async (opts) => {
|
|
12604
|
+
await runCoreDirectPathsCheck(opts);
|
|
12605
|
+
}
|
|
12606
|
+
);
|
|
12550
12607
|
checkCommand.command("cognito-invite-template").description(
|
|
12551
12608
|
"Refuse a Cognito invite_message_template missing a required member or placeholder (#356) \u2014 terraform validate is silent on this, so a fresh deploy fails on its first apply"
|
|
12552
12609
|
).action(async () => {
|
|
@@ -12586,7 +12643,7 @@ function rawArgsAfter(subcommand) {
|
|
|
12586
12643
|
}
|
|
12587
12644
|
|
|
12588
12645
|
// src/commands/doctor.ts
|
|
12589
|
-
import { existsSync as
|
|
12646
|
+
import { existsSync as existsSync43, readFileSync as readFileSync36 } from "fs";
|
|
12590
12647
|
import { join as join50, resolve as resolve18 } from "path";
|
|
12591
12648
|
import chalk21 from "chalk";
|
|
12592
12649
|
import { Command as Command24 } from "commander";
|
|
@@ -12763,7 +12820,7 @@ async function runDoctor(options, deps = { git: new GitAdapter() }) {
|
|
|
12763
12820
|
}
|
|
12764
12821
|
function readLocalCoreVersion(cwd) {
|
|
12765
12822
|
const path = join50(cwd, INSTANCE_CORE_FILE);
|
|
12766
|
-
if (!
|
|
12823
|
+
if (!existsSync43(path)) return null;
|
|
12767
12824
|
try {
|
|
12768
12825
|
return parseCoreRecord(readFileSync36(path, "utf8"));
|
|
12769
12826
|
} catch {
|
|
@@ -12781,7 +12838,7 @@ function parseCoreRecord(contents) {
|
|
|
12781
12838
|
}
|
|
12782
12839
|
function readFossil(cwd) {
|
|
12783
12840
|
const path = join50(cwd, CORE_VERSION_FILE);
|
|
12784
|
-
if (!
|
|
12841
|
+
if (!existsSync43(path)) return null;
|
|
12785
12842
|
try {
|
|
12786
12843
|
const value = readFileSync36(path, "utf8").trim();
|
|
12787
12844
|
return value === "" ? null : value;
|
|
@@ -13232,13 +13289,13 @@ import { fileURLToPath as fileURLToPath6 } from "url";
|
|
|
13232
13289
|
import { Command as Command26 } from "commander";
|
|
13233
13290
|
|
|
13234
13291
|
// src/lib/packaged-scripts.ts
|
|
13235
|
-
import { existsSync as
|
|
13292
|
+
import { existsSync as existsSync44 } from "fs";
|
|
13236
13293
|
import { dirname as dirname10, join as join51 } from "path";
|
|
13237
13294
|
function findPackagedScript(startDir, relativePath) {
|
|
13238
13295
|
let dir = startDir;
|
|
13239
13296
|
for (; ; ) {
|
|
13240
13297
|
const candidate = join51(dir, relativePath);
|
|
13241
|
-
if (
|
|
13298
|
+
if (existsSync44(candidate)) return candidate;
|
|
13242
13299
|
const parent = dirname10(dir);
|
|
13243
13300
|
if (parent === dir) return null;
|
|
13244
13301
|
dir = parent;
|