@bensandee/tooling 0.93.1 → 0.95.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/README.md +20 -20
- package/dist/bin.mjs +1794 -493
- package/package.json +9 -9
- package/tooling.schema.json +76 -7
package/dist/bin.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { accessSync, appendFileSync, closeSync, constants, copyFileSync, existsSync, fstatSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
2
|
+
import fs, { accessSync, appendFileSync, closeSync, constants, copyFileSync, existsSync, fstatSync, globSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
3
3
|
import path, * as path$1 from "node:path";
|
|
4
4
|
import { parseArgs, parseEnv } from "node:util";
|
|
5
5
|
import { FatalError, TransientError, UnexpectedError, describeError, describeError as describeError$1, redact, redact as redact$1 } from "@bensandee/common";
|
|
@@ -15,7 +15,7 @@ import { isMap, isScalar, isSeq, parse as parse$1, parseAllDocuments, parseDocum
|
|
|
15
15
|
import { StringDecoder } from "node:string_decoder";
|
|
16
16
|
import { parse as parse$2 } from "smol-toml";
|
|
17
17
|
import picomatch from "picomatch";
|
|
18
|
-
import { tmpdir } from "node:os";
|
|
18
|
+
import os, { tmpdir } from "node:os";
|
|
19
19
|
import net from "node:net";
|
|
20
20
|
import { createHmac } from "node:crypto";
|
|
21
21
|
import { fileURLToPath } from "node:url";
|
|
@@ -175,7 +175,7 @@ function loadEnvFileForBin(rawArgs, bin, io = processIo) {
|
|
|
175
175
|
//#region src/env-file-preload.ts
|
|
176
176
|
const envFileLoad = loadEnvFileForBin(process.argv.slice(2), {
|
|
177
177
|
name: "bst",
|
|
178
|
-
version: "0.
|
|
178
|
+
version: "0.95.0"
|
|
179
179
|
});
|
|
180
180
|
//#endregion
|
|
181
181
|
//#region ../cli-kit/src/logger.ts
|
|
@@ -448,7 +448,8 @@ function resolveRepoUrlFrom(readOrigin, readPackageUrl) {
|
|
|
448
448
|
const originUrl = normalizeGitUrl(readOrigin() ?? "");
|
|
449
449
|
if (originUrl !== null && parseGitUrl(originUrl) !== null) return originUrl;
|
|
450
450
|
const pkgUrl = readPackageUrl();
|
|
451
|
-
|
|
451
|
+
if (pkgUrl === null || parseGitUrl(pkgUrl) === null) return null;
|
|
452
|
+
return normalizeGitUrl(pkgUrl);
|
|
452
453
|
}
|
|
453
454
|
function resolveForgejoCoordinates(env, repoUrl) {
|
|
454
455
|
const serverUrl = env["FORGEJO_SERVER_URL"];
|
|
@@ -554,10 +555,11 @@ function resolveIssueTarget(refTarget, repo) {
|
|
|
554
555
|
throw new RepoTargetMismatchError(`${repoTarget.serverUrl}/${repoTarget.repository}`, `${refTarget.serverUrl}/${refTarget.repository}`);
|
|
555
556
|
}
|
|
556
557
|
function normalizeGitUrl(raw) {
|
|
557
|
-
const
|
|
558
|
+
const value = raw.startsWith("git+") ? raw.slice(4) : raw;
|
|
559
|
+
const sshMatch = value.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
|
|
558
560
|
if (sshMatch) return `https://${sshMatch[1]}/${sshMatch[2]}`;
|
|
559
561
|
try {
|
|
560
|
-
const url = new URL(
|
|
562
|
+
const url = new URL(value);
|
|
561
563
|
url.pathname = url.pathname.replace(/\.git$/, "");
|
|
562
564
|
url.username = "";
|
|
563
565
|
url.password = "";
|
|
@@ -688,7 +690,10 @@ const HealthRecordSchema = HealthMetricsSchema.extend(HealthObservationsSchema.s
|
|
|
688
690
|
});
|
|
689
691
|
const RATCHETED_METRIC_KEY_SET = new Set(RATCHETED_METRIC_KEYS);
|
|
690
692
|
const ModuleCeilingsSchema = z.preprocess((value) => typeof value === "object" && value !== null ? Object.fromEntries(Object.entries(value).filter(([key]) => RATCHETED_METRIC_KEY_SET.has(key))) : value, z.partialRecord(z.enum(RATCHETED_METRIC_KEYS), z.record(z.string(), z.number().int().nonnegative())));
|
|
691
|
-
const HealthBaselineSchema = HealthMetricsSchema.extend({
|
|
693
|
+
const HealthBaselineSchema = HealthMetricsSchema.extend({
|
|
694
|
+
moduleCeilings: ModuleCeilingsSchema.optional(),
|
|
695
|
+
sizeBudgets: z.record(z.string(), z.number().int().nonnegative()).optional()
|
|
696
|
+
});
|
|
692
697
|
function parseHealthBaseline(raw) {
|
|
693
698
|
let json;
|
|
694
699
|
try {
|
|
@@ -757,6 +762,18 @@ function formatIssue$1(issue) {
|
|
|
757
762
|
}
|
|
758
763
|
return ` ${loc}: ${issue.message}`;
|
|
759
764
|
}
|
|
765
|
+
const SIZE_BUDGET_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
766
|
+
const SizeBudgetSchema = z.union([
|
|
767
|
+
z.object({ file: z.string().min(1).meta({ description: "Repo-relative path of the one file whose byte size is measured." }) }).meta({ description: "A size budget measuring the bytes of a single file." }),
|
|
768
|
+
z.object({
|
|
769
|
+
glob: z.string().min(1).meta({ description: "Glob matched against repo-relative paths under the target." }),
|
|
770
|
+
aggregate: z.enum(["total", "largest"]).meta({ description: "How the matched files become one number: the sum of their bytes, or the biggest single file." })
|
|
771
|
+
}).meta({ description: "A size budget measuring the bytes of every file a glob matches." }),
|
|
772
|
+
z.object({
|
|
773
|
+
script: z.string().min(1).meta({ description: "Package script run as `pnpm run --silent <script>`; it reports one number through the reserved measure marker." }),
|
|
774
|
+
files: z.array(z.string().min(1)).meta({ description: "Globs naming the code that computes the number: a match in the branch's diff marks the measurement as changed by this branch." })
|
|
775
|
+
}).meta({ description: "A size budget measured by running a package script that prints one number." })
|
|
776
|
+
]);
|
|
760
777
|
const HealthConfigSchema = z.strictObject({
|
|
761
778
|
enabled: z.boolean().optional().meta({ description: "Opts into the gate (default false; absent and false alike mean off). `health:check` and `health:summary` print a disabled notice and exit 0 rather than failing while off. Read at generation time as well as at check time, so enabling the gate takes a `repo:sync` as well as a config edit. Dominates `history`." }),
|
|
762
779
|
src: z.array(z.string()).optional().meta({ description: "Repo-relative directory roots the measurement is scoped to (default `[\"packages/*/src\"]` when a `packages/` directory exists at the repo root, else `[\"src\"]`). Scopes both the file-tree walk and the dependency-cruiser globs, but not the same way: for the file-tree walk, an entry is expanded against existing directories (one `*` segment only, nothing else). An entry the expander cannot represent at all (`src/**/*.ts`, any other `**`) and one that is well-formed but names no existing directory (a typo, a directory renamed after the config was written) are both reported by name, classified separately, and a run whose file-tree scope resolves to zero source files fails `health:check` before its baseline is read, created, updated, or reset -- `--no-fail-on-empty-src` downgrades that to a notice. For the dependency-cruiser globs the entry is spliced in verbatim, so a `**` entry like `packages/**/src` still cruises and leaves the graph metrics live even though `crowdedDirectories` and the linesOfCode observation see an empty tree. Use `packages/*/src`. The file-tree walk also drops test files, `.d.ts` declarations and `exclude` matches; the dependency-cruiser half drops only `exclude` matches, so a colocated test file or a `.d.ts` under a `src` root still carries graph edges." }),
|
|
@@ -766,11 +783,12 @@ const HealthConfigSchema = z.strictObject({
|
|
|
766
783
|
fanInLimit: z.number().int().nonnegative().optional().meta({ description: "A module is a fan-in outlier above this many importers, and only when it also exceeds `moduleSizeLimit` source lines (defaults 10 and 200) -- popularity alone is not coupling. Only value edges are counted: a type-only import is erased before the graph is built and never moves this count, and a third-party import resolves outside the graph entirely. A cross-package import counts unless `crossPackage` is off (default on) -- see that setting. Never duplicate a type or fork a helper to stay under the limit -- prefer a justified `exempt` entry, which names the hub, over raising the limit, which hides it." }),
|
|
767
784
|
fanOutLimit: z.number().int().nonnegative().optional().meta({ description: "A module is a fan-out outlier above this many first-party dependencies (default 10). Counted on the same value-edge basis as fanInLimit, `crossPackage` included, with the same consequence: an import you were about to inline or copy to protect this budget most likely costs nothing." }),
|
|
768
785
|
moduleSizeLimit: z.number().int().nonnegative().optional().meta({ description: "Raw source lines a module must exceed before its importer count can make it a fan-in outlier (default 200) -- comments and blanks included, since the question is how big the module is to read, not how much code it holds. A module with many importers and little surface is a leaf, not a junction: it has one job and no reason to change for any of its callers, so counting its importers reports popularity rather than coupling. Applies to fan-in only -- a small module with many dependencies is a barrel, which is a finding." }),
|
|
769
|
-
crossPackage: z.boolean().optional().meta({ description: "Count dependency edges that cross a workspace package boundary (default true). On by default because the alternative measures the wrong thing: with it off, moving a module into its own package removes its importers from every metric, so extracting a package hides a hub rather than fixing it. A monorepo adopting the default sees fan-in and fan-out rise once, and adopts that rise
|
|
786
|
+
crossPackage: z.boolean().optional().meta({ description: "Count dependency edges that cross a workspace package boundary (default true). On by default because the alternative measures the wrong thing: with it off, moving a module into its own package removes its importers from every metric, so extracting a package hides a hub rather than fixing it. A monorepo adopting the default sees fan-in and fan-out rise once, and adopts that rise with `bst health:raise` in the pull request that adopts the setting, which a person merges; a single-package repo has no workspace siblings to resolve, so no new edge enters its graph. Set it to false only where the package boundary is a curated API surface an edge across genuinely costs less, or to hold a baseline on the old measurement basis." }),
|
|
770
787
|
history: z.enum(["orphan-branch", "none"]).optional().meta({ description: "How history is persisted. \"orphan-branch\" (default) has `health:record` append one JSON line per commit to `historyBranch` and has `repo:sync` emit the CI record and status steps; \"none\" turns both off. Does not disable the gate, since the baseline is a committed file. Ignored when `enabled` is false." }),
|
|
771
788
|
historyBranch: z.string().optional().meta({ description: "The orphan branch `health:record` appends to and `health:status` reads (default \"bst/health-metrics\"). Never merges into the default branch and holds no source; separate from `coverage.historyBranch` and must not share a branch with it. `branch:prune`'s built-in never-touch list matches the default name exactly, so a custom name should be named in `branchPrune.exclude` as well." }),
|
|
772
789
|
tolerance: z.number().int().nonnegative().optional().meta({ description: "Slack in offender counts before an increase is a regression: a metric fails when the current count exceeds baseline + tolerance (default 0, so any increase fails). The same allowance applies to every per-module ceiling in health-baseline.json's moduleCeilings, not only the headline metrics. A count, not a percentage -- coverage's percentage-point slack is `coverage.tolerancePp`. Raising it un-tracks every module beneath the new line at once, which is why a justified `exempt` entry is the better answer to one stubborn hub." }),
|
|
773
|
-
|
|
790
|
+
sizeBudgets: z.record(z.string().regex(SIZE_BUDGET_ID_PATTERN), SizeBudgetSchema).optional().meta({ description: "Artifact size budgets: id -> what to measure. Each id records one ceiling in health-baseline.json's sizeBudgets, and `health:check` fails when the measured number exceeds it. Absent means no size metric at all. A budget with no recorded value fails as unrecorded until `--update-baseline` records it; a file or glob that measures nothing fails rather than reading as zero. Measured at the working tree only, never at the merge base, so a crossing always names this branch. Raising a recorded value is a raise: write it with `bst health:raise`, in the pull request whose change crossed the budget." }),
|
|
791
|
+
exempt: z.partialRecord(z.enum(EXEMPTABLE_METRIC_KEYS), z.record(z.string(), z.string().min(1).refine((reason) => reason.trim() !== "", { message: "must not be blank" }))).optional().meta({ description: "Per-metric, per-module exemption, keyed first by metric name (fanInOutliers, fanOutOutliers or crowdedDirectories) then by the repo-relative path exactly as `health:summary --explain` prints it: a module path for the two fan metrics, a directory path for crowdedDirectories. A key may instead be a picomatch glob (e.g. `packages/contract/src/core/**`), matched against the path itself and never its ancestors; an exact key wins over a glob that also matches, and otherwise the first matching glob's reason applies. A glob also covers modules added later, which still show in the summary with their counts. Each path maps to a required, non-empty justification string. An exempt module or directory is listed in the summary with its count and reason, carries no ceiling in health-baseline.json, and is counted in no metric -- it is out of the gate, not suppressed inside it. Adding one is a raise: write it with `bst health:raise --exempt`, in the pull request whose code made the module a hub. An exempt module stays fully in the graph -- its edges still count toward every other module's fan-in/fan-out." })
|
|
774
792
|
});
|
|
775
793
|
const CharsConfigSchema = z.strictObject({
|
|
776
794
|
enabled: z.boolean().optional().meta({ description: "Run the chars:check gate (default true). Set false to opt out -- the only off-switch: the gate scans tracked text files for LLM-tell punctuation and invisible or deceptive characters, and passes letters in any language." }),
|
|
@@ -838,8 +856,8 @@ const TolerantDockerCheckConfigSchema = z.object({
|
|
|
838
856
|
...DockerCheckConfigSchema.shape,
|
|
839
857
|
healthChecks: z.array(tolerantBlock(DeclarativeHealthCheckSchema)).optional()
|
|
840
858
|
});
|
|
841
|
-
const DEFAULT_BRANCH_NAME_PATTERN = /^(?!.*\/\/)(?!.*\.\.)(?!.*\/$)(?!.*\.lock$)[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
842
|
-
const DefaultBranchNameSchema = z.string().regex(DEFAULT_BRANCH_NAME_PATTERN, "must be a plain branch name: ASCII letters, digits, '.', '_', '-' and '/', starting with a letter or digit, with no '//', '..', trailing '/', or '.lock' suffix");
|
|
859
|
+
const DEFAULT_BRANCH_NAME_PATTERN = /^(?!refs\/)(?!.*\/\/)(?!.*\.\.)(?!.*\/$)(?!.*\.lock$)[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
860
|
+
const DefaultBranchNameSchema = z.string().regex(DEFAULT_BRANCH_NAME_PATTERN, "must be a plain branch name: ASCII letters, digits, '.', '_', '-' and '/', starting with a letter or digit, with no leading 'refs/', no '//', '..', trailing '/', or '.lock' suffix");
|
|
843
861
|
const AgentEnabledSliceSchema = z.looseObject({ agent: z.looseObject({ enabled: z.boolean().optional() }).optional() });
|
|
844
862
|
function agentEnabledIn(raw) {
|
|
845
863
|
try {
|
|
@@ -875,6 +893,9 @@ const JS_EXTENSIONS$1 = /* @__PURE__ */ new Set([
|
|
|
875
893
|
".mjs",
|
|
876
894
|
".cjs"
|
|
877
895
|
]);
|
|
896
|
+
function sourceGlob(stack, dir = "src") {
|
|
897
|
+
return `${dir}/**/*.{${[...stack === "typescript" ? TS_EXTENSIONS$1 : JS_EXTENSIONS$1].map((ext) => ext.slice(1)).join(",")}}`;
|
|
898
|
+
}
|
|
878
899
|
function isRootConfigFile(relPath) {
|
|
879
900
|
if (relPath.includes(path.sep)) return false;
|
|
880
901
|
return /\.config\.[cm]?[jt]sx?$/.test(relPath) || relPath.startsWith(".");
|
|
@@ -1041,6 +1062,12 @@ function detectProjectType(targetDir) {
|
|
|
1041
1062
|
if (pkg.exports || pkg.main || pkg.types || pkg.typings) return "library";
|
|
1042
1063
|
return "default";
|
|
1043
1064
|
}
|
|
1065
|
+
function resolveProjectType(packageType, structure, builder) {
|
|
1066
|
+
if (structure === "monorepo") return "default";
|
|
1067
|
+
if (packageType === "react") return "react";
|
|
1068
|
+
if (builder === "none") return "node";
|
|
1069
|
+
return packageType;
|
|
1070
|
+
}
|
|
1044
1071
|
const WEB_UI_DEPS = /* @__PURE__ */ new Set([
|
|
1045
1072
|
"react",
|
|
1046
1073
|
"react-dom",
|
|
@@ -1078,19 +1105,21 @@ function computeDefaults(targetDir) {
|
|
|
1078
1105
|
const detected = detectProject(targetDir);
|
|
1079
1106
|
const isMonorepo = detected.hasPnpmWorkspace;
|
|
1080
1107
|
const hasPrettier = detected.legacyConfigs.some((l) => l.tool === "prettier");
|
|
1108
|
+
const structure = isMonorepo ? "monorepo" : "single";
|
|
1109
|
+
const builder = detectBuilder(targetDir, readPackageJson(targetDir));
|
|
1081
1110
|
return {
|
|
1082
|
-
structure
|
|
1111
|
+
structure,
|
|
1083
1112
|
useEslintPlugin: true,
|
|
1084
1113
|
formatter: hasPrettier ? "prettier" : "oxfmt",
|
|
1085
1114
|
stack: detectStack(targetDir),
|
|
1086
|
-
builder
|
|
1115
|
+
builder,
|
|
1087
1116
|
setupVitest: !isMonorepo && !detected.hasVitestConfig,
|
|
1088
1117
|
ci: detectCiPlatform(targetDir),
|
|
1089
1118
|
setupRenovate: true,
|
|
1090
1119
|
releaseStrategy: "none",
|
|
1091
1120
|
publishNpm: false,
|
|
1092
1121
|
publishDocker: false,
|
|
1093
|
-
projectType:
|
|
1122
|
+
projectType: resolveProjectType(detectProjectType(targetDir), structure, builder),
|
|
1094
1123
|
detectPackageTypes: true
|
|
1095
1124
|
};
|
|
1096
1125
|
}
|
|
@@ -1159,7 +1188,7 @@ async function runInitPrompts(targetDir, saved, options) {
|
|
|
1159
1188
|
let ci = saved?.ci ?? defaults.ci;
|
|
1160
1189
|
const setupRenovate = saved?.setupRenovate ?? defaults.setupRenovate;
|
|
1161
1190
|
let releaseStrategy = saved?.releaseStrategy ?? defaults.releaseStrategy;
|
|
1162
|
-
|
|
1191
|
+
let projectType = saved?.projectType ?? defaults.projectType;
|
|
1163
1192
|
const detectPackageTypes = saved?.detectPackageTypes ?? defaults.detectPackageTypes;
|
|
1164
1193
|
const skipPrompts = (options?.skipStackPrompt ?? false) || Boolean(process.env["CI"]);
|
|
1165
1194
|
if (isFirstInit && defaults.stack === "none" && !skipPrompts) {
|
|
@@ -1216,6 +1245,7 @@ async function runInitPrompts(targetDir, saved, options) {
|
|
|
1216
1245
|
builder = builderAnswer;
|
|
1217
1246
|
}
|
|
1218
1247
|
}
|
|
1248
|
+
if (saved?.projectType === void 0) projectType = resolveProjectType(detectProjectType(targetDir), structure, builder);
|
|
1219
1249
|
if (detected.legacyConfigs.some((l) => l.tool === "prettier") && isFirstInit) {
|
|
1220
1250
|
const formatterAnswer = await clack.select({
|
|
1221
1251
|
message: "Existing Prettier config found. Keep Prettier or migrate to oxfmt?",
|
|
@@ -1576,12 +1606,15 @@ function createDryRunContext(config, dockerDiscovery) {
|
|
|
1576
1606
|
};
|
|
1577
1607
|
}
|
|
1578
1608
|
//#endregion
|
|
1609
|
+
//#region src/repo-full-name.ts
|
|
1610
|
+
const REPO_FULL_NAME = /^(?!\.\.?\/)[\w.-]+\/(?!\.\.?$)[\w.-]+$/u;
|
|
1611
|
+
//#endregion
|
|
1579
1612
|
//#region src/docs-check/config.ts
|
|
1580
1613
|
const DEFAULT_STALE_DAYS = 30;
|
|
1581
1614
|
const DocsConfigSchema = z.strictObject({
|
|
1582
1615
|
enforced: z.union([z.boolean(), z.array(z.string())]).optional().meta({ description: "Which discovered pairs fail the build. true = all; an array names active dirs (e.g. [\"docs/plans\"]); default false = every violation is advisory." }),
|
|
1583
1616
|
dirs: z.array(z.string()).optional().meta({ description: "Extra active doc dirs beyond convention (e.g. [\"docs/rfcs\"]). Each retires into its own retired/ subdir." }),
|
|
1584
|
-
staleDays: z.number().int().nonnegative().optional().meta({ description: "Days before an
|
|
1617
|
+
staleDays: z.number().int().nonnegative().optional().meta({ description: "Days before an unchanged active doc is flagged (default 30; 0 disables the rule)." }),
|
|
1585
1618
|
staleEnforced: z.boolean().optional().meta({ description: "Make the staleness rule fatal inside an enforced pair (default false: staleness is always advisory)." })
|
|
1586
1619
|
});
|
|
1587
1620
|
const DEFAULT_DOCS_CONFIG = {
|
|
@@ -1713,9 +1746,9 @@ const LintSweepConfigSchema = z.strictObject({
|
|
|
1713
1746
|
});
|
|
1714
1747
|
const AgentEgressConfigSchema = z.strictObject({ browseHosts: z.array(z.string().min(1)).optional().meta({ description: "Host patterns this repo's agent containers may fetch from, on top of the hosts every run needs: an exact host (`fastdl.mongodb.org`) or a wildcard over its leftmost label (`*.example.com`). No ports. These join the browse lane, which serves GET and HEAD only." }) });
|
|
1715
1748
|
const AgentConfigSchema = z.strictObject({
|
|
1716
|
-
enabled: z.boolean().optional().meta({ description: "The repo's explicit consent to the agent (default false). Only `true`, read from the repo's **default branch**, lets the agent work the repo: claim units, run sweeps, recover orphans, merge, act on a chat proposal or start a new spec. A missing key, `false`, a missing `.tooling.json` or a read that fails all mean not enabled, and the answer is re-read within about a minute. Every other `agent.*` key below has no effect without this one; `repo:sync` warns when the block sets one of them and this key is not `true`. Where this is `true`, `repo:sync` also installs `@bensandee/agent-cli`. `bst setup:agent` sets it to true and runs the sync. Provisioning the agent's labels is not consent, and is never gated by this key." }),
|
|
1749
|
+
enabled: z.boolean().optional().meta({ description: "The repo's explicit consent to the agent (default false). Only `true`, read from the repo's **default branch**, lets the agent work the repo: claim units, run sweeps, recover orphans, merge, act on a chat proposal or start a new spec. A missing key, `false`, a missing `.tooling.json` or a read that fails all mean not enabled, and the answer is re-read within about a minute. Every other `agent.*` key below has no effect without this one; `repo:sync` warns when the block sets one of them and this key is not `true`. Where this is `true`, `repo:sync` also installs `@bensandee/agent-cli`. `pnpm exec bst setup:agent` sets it to true and runs the sync. Provisioning the agent's labels is not consent, and is never gated by this key." }),
|
|
1717
1750
|
autonomy: z.boolean().optional().meta({ description: "Opt into interior autonomy (default false). When a phase's latest rationale reports readiness \"ready\" with an empty openQuestions[] and the review phase is clean, the agent applies the existing plan -> implement -> review -> park-at-mergeable advance itself instead of parking for a human. Never applies to the design/spec gate or the final merge from the mergeable terminal -- those always wait on a human. Also arms the trunk-red detector: when this repo's default branch combined status settles red, the agent files a tracking issue naming the failing checks and labels it agent/spec, starting the fix through the ordinary spec/plan/implement/review pipeline. It also closes that issue, with a comment, if the default branch recovers on its own before a human acts on it. Also arms the world-changed retrigger sweep: a pull request labelled agent-flags/autopilot that is parked at a -stuck or -failed terminal is re-queued when the fact its park recorded stops being true (the default branch went green, or moved past a conflicted base)." }),
|
|
1718
|
-
preRunRebase: z.boolean().optional().meta({ description: "Merge the current default branch into the work branch before the implement (execute)
|
|
1751
|
+
preRunRebase: z.boolean().optional().meta({ description: "Merge the current default branch into the work branch before the container starts (default true) -- for an implement (execute) run, and for a spec or plan rerun on an existing branch. A clean merge or an already-up-to-date branch runs the container as is; a conflicting merge is aborted, recorded on the run's status comment, and the run proceeds on the un-synced branch -- a conflict, timeout or failed merge never fails the run, for any phase. Only the execute phase keeps the two-strike rule: two consecutive conflicting attempts hand the unit to agent/repair before any container runs, while a docs run just tries again on its next slice. Set false to skip the attempt entirely for this repo, in every phase: no fetch, no merge, no abort, and the branch is worked exactly as cloned." }),
|
|
1719
1752
|
backlog: z.boolean().optional().meta({ description: "Whether the agent may self-trigger backlog runs consuming this repo's .agent/findings/ ledger (default: on wherever the ledger exists, which only happens in a repo whose own review loop wrote it). A backlog run opens a pull request that fixes or refutes the pending findings; false disarms it, leaving the ledger to accumulate for a human. A run starts only once at least three findings are pending or one is older than seven days." }),
|
|
1720
1753
|
lintSweep: LintSweepConfigSchema.optional().meta({ description: "The lint-warning sweep: a periodic agent job that measures this repo's oxlint warnings on its default branch in a container and, past minWarnings, opens a pull request carrying a generated plan for an agent/implement run to work off." }),
|
|
1721
1754
|
priority: z.record(z.string(), z.enum([
|
|
@@ -1726,13 +1759,15 @@ const AgentConfigSchema = z.strictObject({
|
|
|
1726
1759
|
])).optional().meta({ description: "Map this repo's own issue-label names onto the agent's four priority levels, e.g. {\"P1\": \"critical\", \"P4\": \"low\"}. The map UNIONS with the built-in priority/<level> names rather than replacing them, and the agent does not create aliased labels -- the repo owns names the agent did not choose. Priority affects claim order only: it never changes a run's model, effort, or any budget." }),
|
|
1727
1760
|
triageAnalysis: z.boolean().optional().meta({ description: "Whether the agent may route its own agent-note/human-triage-needed issues through the agent/analyze stage (default: off). Unlike agent.backlog, which defaults on, this defaults off: a backlog run batches every pending finding into one container, while triage routing spends one container per issue -- arming it is the reversible direction, not disarming it." }),
|
|
1728
1761
|
egress: AgentEgressConfigSchema.optional().meta({ description: "Extra egress hosts this repo's agent containers may reach. `browseHosts` joins the agent's browse lane (GET and HEAD only) on top of the hosts every run needs; the passthrough lane stays agent-owned. Each entry is a host pattern -- an exact host or a *.host wildcard, never a port. Read from the repo's DEFAULT BRANCH, never off a pull request's own tree, so a run can never widen its own egress: the declaration takes effect on the first container started after a human merges it. An entry whose pattern the agent cannot parse refuses the run rather than being dropped. Changing this block holds the pull request from automerge." }),
|
|
1729
|
-
findingHolds: FindingHoldsConfigSchema.optional().meta({ description: "Which of a pull request's linked open findings withhold its unattended merge. `rules` is ordered and the first `when` that matches decides; no match falls through to `default`; an absent block is { rules: [], default: true }, so every open linked finding holds by default. Read off the repo's DEFAULT BRANCH, never off the pull request's own tree -- a pull request cannot edit the rule deciding whether it is held. In a `mentions` entry a /.../flags pattern is a regular expression, a literal:<text> pattern is the substring <text> (how to name a path such as /src/utils), and anything else is a case-insensitive substring." })
|
|
1762
|
+
findingHolds: FindingHoldsConfigSchema.optional().meta({ description: "Which of a pull request's linked open findings withhold its unattended merge. `rules` is ordered and the first `when` that matches decides; no match falls through to `default`; an absent block is { rules: [], default: true }, so every open linked finding holds by default. Read off the repo's DEFAULT BRANCH, never off the pull request's own tree -- a pull request cannot edit the rule deciding whether it is held. In a `mentions` entry a /.../flags pattern is a regular expression, a literal:<text> pattern is the substring <text> (how to name a path such as /src/utils), and anything else is a case-insensitive substring." }),
|
|
1763
|
+
chatIssueRepos: z.array(z.string().regex(REPO_FULL_NAME)).optional().meta({ description: "Repositories with a DIFFERENT owner that a control-panel chat from this repository may file an issue in, each one a full `owner/name`, never a wildcard. A repository with the same owner needs no entry, and an entry grants nothing on its own: the target must have enabled the agent too. Read from the repo's DEFAULT BRANCH at the moment a card is applied, so a pull request cannot widen the reach of its own chat. Like every other `agent.*` key, this has no effect unless `agent.enabled` is true." })
|
|
1730
1764
|
});
|
|
1731
1765
|
const TolerantAgentConfigSchema = z.object({
|
|
1732
1766
|
...AgentConfigSchema.shape,
|
|
1733
1767
|
lintSweep: tolerantBlock(LintSweepConfigSchema).optional(),
|
|
1734
1768
|
findingHolds: z.unknown().optional(),
|
|
1735
|
-
egress: z.unknown().optional()
|
|
1769
|
+
egress: z.unknown().optional(),
|
|
1770
|
+
chatIssueRepos: z.unknown().optional()
|
|
1736
1771
|
});
|
|
1737
1772
|
const BranchPruneConfigSchema = z.strictObject({
|
|
1738
1773
|
mergedDays: z.number().int().nonnegative().optional().meta({ description: "Days after a work branch's PR merged before bst branch:prune deletes it (default 7). Zero is valid on purpose (an explicit 'delete everything merged already'), matching the --merged-days CLI flag's contract." }),
|
|
@@ -1783,7 +1818,7 @@ const ToolingConfigSchema = z.strictObject({
|
|
|
1783
1818
|
docs: z.union([z.literal(false), DocsConfigSchema]).optional().meta({ description: "Plan/spec lifecycle gate (`bst docs:check`). Overrides only -- dirs are discovered by convention. Set to false to disable. Violations are advisory until a pair is named in `enforced`." }),
|
|
1784
1819
|
deploy: DeployConfigSchema.optional().meta({ description: "Generate a deploy-dispatch step in the release workflow that triggers a workflow_dispatch event in another repo after a successful release. The resolved release token must have rights on `dispatchRepo`." }),
|
|
1785
1820
|
pnpmWorkspace: z.literal(false).optional().meta({ description: "Set to false to opt out of pnpm-workspace.yaml management (skips both generation and the repo:sync --check validation)." }),
|
|
1786
|
-
defaultBranch: DefaultBranchNameSchema.optional().meta({ description: "Repository default branch name
|
|
1821
|
+
defaultBranch: DefaultBranchNameSchema.optional().meta({ description: "Repository default branch name. Drives the generated CI `on.push` trigger and every default-branch gate in `ci.yml` and `release.yml`, the release path (`release:changesets`, the version PR, `release:trigger`) and `checks:run`'s coverage gate. Generated output uses 'main' when unset; `checks:run` still falls back to `origin/HEAD`. Must be a plain branch name: ASCII letters, digits, '.', '_', '-' and '/', with no leading 'refs/'." }),
|
|
1787
1822
|
unmanagedDevDependencies: UnmanagedDevDependenciesSchema.optional().meta({ description: "devDependency names repo:sync must never add or re-pin. Escape hatch for holding a tooling-managed dep (oxlint, vitest, knip, ...) at a deliberately off-canonical version; without it, repo:sync raises any managed devDep whose version has drifted below the canonical pin." }),
|
|
1788
1823
|
stack: StackSchema.optional().meta({ description: "Whether the repo has TS/JS source. Detected from source evidence; TypeScript is never inferred from absence. Setting this key pins its value, even one that equals detection, and repo:sync keeps it until the key is deleted." }),
|
|
1789
1824
|
builder: z.enum([
|
|
@@ -1903,7 +1938,10 @@ function pruneCharsDefaults(value) {
|
|
|
1903
1938
|
return kept;
|
|
1904
1939
|
}
|
|
1905
1940
|
function saveToolingConfig(ctx, config) {
|
|
1906
|
-
const defaults =
|
|
1941
|
+
const defaults = {
|
|
1942
|
+
...computeDefaults(config.targetDir),
|
|
1943
|
+
projectType: resolveProjectType(detectProjectType(config.targetDir), config.structure, config.builder)
|
|
1944
|
+
};
|
|
1907
1945
|
const isMonorepo = config.structure === "monorepo";
|
|
1908
1946
|
const existing = ctx.exists(CONFIG_FILE) ? ctx.read(CONFIG_FILE) : void 0;
|
|
1909
1947
|
const existingJson = parseExistingConfig(existing);
|
|
@@ -1918,7 +1956,7 @@ function saveToolingConfig(ctx, config) {
|
|
|
1918
1956
|
if (key === "chars" || key === "ascii") {
|
|
1919
1957
|
const pruned = pruneCharsDefaults(value);
|
|
1920
1958
|
if (pruned === void 0) continue;
|
|
1921
|
-
result[
|
|
1959
|
+
result["chars"] = pruned;
|
|
1922
1960
|
continue;
|
|
1923
1961
|
}
|
|
1924
1962
|
result[key] = value;
|
|
@@ -1937,21 +1975,23 @@ function saveToolingConfig(ctx, config) {
|
|
|
1937
1975
|
};
|
|
1938
1976
|
}
|
|
1939
1977
|
function mergeWithSavedConfig(detected, saved) {
|
|
1978
|
+
const structure = saved.structure ?? detected.structure;
|
|
1979
|
+
const builder = saved.builder ?? detected.builder;
|
|
1940
1980
|
return {
|
|
1941
1981
|
name: detected.name,
|
|
1942
1982
|
targetDir: detected.targetDir,
|
|
1943
|
-
structure
|
|
1983
|
+
structure,
|
|
1944
1984
|
useEslintPlugin: saved.useEslintPlugin ?? detected.useEslintPlugin,
|
|
1945
1985
|
formatter: saved.formatter ?? detected.formatter,
|
|
1946
1986
|
stack: saved.stack ?? detected.stack,
|
|
1947
|
-
builder
|
|
1987
|
+
builder,
|
|
1948
1988
|
setupVitest: saved.setupVitest ?? detected.setupVitest,
|
|
1949
1989
|
ci: saved.ci ?? detected.ci,
|
|
1950
1990
|
setupRenovate: saved.setupRenovate ?? detected.setupRenovate,
|
|
1951
1991
|
releaseStrategy: saved.releaseStrategy ?? detected.releaseStrategy,
|
|
1952
1992
|
publishNpm: saved.publishNpm ?? detected.publishNpm,
|
|
1953
1993
|
publishDocker: saved.publishDocker ?? detected.publishDocker,
|
|
1954
|
-
projectType: saved.projectType ?? detected.
|
|
1994
|
+
projectType: saved.projectType ?? resolveProjectType(detectProjectType(detected.targetDir), structure, builder),
|
|
1955
1995
|
detectPackageTypes: saved.detectPackageTypes ?? detected.detectPackageTypes
|
|
1956
1996
|
};
|
|
1957
1997
|
}
|
|
@@ -2088,10 +2128,7 @@ async function throwForgejoFailure(res, operation, notFoundMessage) {
|
|
|
2088
2128
|
if (res.status === 410) throw new FatalError(`${message}\nForgejo returned 410 Gone -- issues are likely disabled on this repository; retrying will not help.`);
|
|
2089
2129
|
throw new TransientError(message);
|
|
2090
2130
|
}
|
|
2091
|
-
z.object({
|
|
2092
|
-
secrets: z.array(z.object({ name: z.string() })),
|
|
2093
|
-
total_count: z.number()
|
|
2094
|
-
});
|
|
2131
|
+
z.array(z.object({ name: z.string() }));
|
|
2095
2132
|
const AccessTokenResponseSchema = z.object({ sha1: z.string() });
|
|
2096
2133
|
const NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2097
2134
|
const FORBIDDEN_PREFIX_PATTERN = /^(FORGEJO_|GITEA_|GITHUB_|[0-9])/i;
|
|
@@ -2903,8 +2940,11 @@ function tracedRunners(logger) {
|
|
|
2903
2940
|
if (result.signal !== null && result.signal !== void 0) throw new UnexpectedError(`Command killed by signal ${result.signal}: ${commandLine(file, args)}`);
|
|
2904
2941
|
if (result.status !== 0) throw new UnexpectedError(`Command failed with exit code ${String(result.status)}: ${commandLine(file, args)}`);
|
|
2905
2942
|
}
|
|
2906
|
-
} finally {
|
|
2907
2943
|
logger.trace(`execInherit [ok] ${file} (${sinceMs(startedAt)})`);
|
|
2944
|
+
} catch (error) {
|
|
2945
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2946
|
+
logger.trace(`execInherit [x] ${file} (${reason}, ${sinceMs(startedAt)})`);
|
|
2947
|
+
throw error;
|
|
2908
2948
|
}
|
|
2909
2949
|
},
|
|
2910
2950
|
async fetch(url, init, retry) {
|
|
@@ -4485,8 +4525,8 @@ function buildWorkflowYaml(options) {
|
|
|
4485
4525
|
}
|
|
4486
4526
|
//#endregion
|
|
4487
4527
|
//#region src/generators/mise-pin.ts
|
|
4488
|
-
const MISE_VERSION = "2026.10.
|
|
4489
|
-
const MISE_LINUX_X64_SHA256 = "
|
|
4528
|
+
const MISE_VERSION = "2026.10.15";
|
|
4529
|
+
const MISE_LINUX_X64_SHA256 = "e3b186d4c70627f27037be615becaf4acfb30e41fb6a1819486717dc94e4e2a9";
|
|
4490
4530
|
const SETUP_MISE_ACTION_URL = "https://code.orangebikelabs.com/bensandee/common/actions/setup-mise";
|
|
4491
4531
|
const SETUP_MISE_ACTION_TAG = "setup-mise@latest";
|
|
4492
4532
|
const SETUP_MISE_ACTION_SHA = "ebe622ef0d24c7efec4a5f6093ddb18500900e24";
|
|
@@ -4716,7 +4756,7 @@ function detectReleaseAssetsConventionDirs(ctx) {
|
|
|
4716
4756
|
if (ctx.config.structure === "monorepo") {
|
|
4717
4757
|
const packages = getMonorepoPackages(ctx.targetDir);
|
|
4718
4758
|
for (const pkg of packages) {
|
|
4719
|
-
const dirName =
|
|
4759
|
+
const dirName = path.basename(pkg.dir);
|
|
4720
4760
|
const pkgRaw = ctx.read(`packages/${dirName}/package.json`);
|
|
4721
4761
|
if (!pkgRaw) continue;
|
|
4722
4762
|
if (parsePackageJson(pkgRaw)?.scripts?.["build:release-assets"]) dirs.push(`packages/${dirName}`);
|
|
@@ -4761,7 +4801,7 @@ async function generateDeployCi(ctx) {
|
|
|
4761
4801
|
action: "skipped",
|
|
4762
4802
|
description: "Publish workflow not applicable"
|
|
4763
4803
|
};
|
|
4764
|
-
return retireManagedWorkflow(ctx, ctx.config.ci === "github" ? ".github/workflows/publish.yml" : ".forgejo/workflows/publish.yml", "run `bst docker:publish` to publish by hand") ?? {
|
|
4804
|
+
return retireManagedWorkflow(ctx, ctx.config.ci === "github" ? ".github/workflows/publish.yml" : ".forgejo/workflows/publish.yml", "run `pnpm exec bst docker:publish` to publish by hand") ?? {
|
|
4765
4805
|
filePath,
|
|
4766
4806
|
action: "skipped",
|
|
4767
4807
|
description: "No publish workflow to retire"
|
|
@@ -4806,7 +4846,8 @@ function resolveFromOverrides(overrides, isMonorepo, stack) {
|
|
|
4806
4846
|
historyBranch: o.historyBranch ?? DEFAULT_HEALTH_HISTORY_BRANCH,
|
|
4807
4847
|
tolerance: o.tolerance ?? 0,
|
|
4808
4848
|
graphMetricsEnabled: stack !== "none",
|
|
4809
|
-
exempt: o.exempt ?? {}
|
|
4849
|
+
exempt: o.exempt ?? {},
|
|
4850
|
+
sizeBudgets: o.sizeBudgets ?? {}
|
|
4810
4851
|
};
|
|
4811
4852
|
}
|
|
4812
4853
|
function resolveHealthConfigFromContext(reader, opts) {
|
|
@@ -5055,18 +5096,18 @@ function perPackageDevDeps(config) {
|
|
|
5055
5096
|
deps["typescript"] = {
|
|
5056
5097
|
"@changesets/cli": "3.0.3",
|
|
5057
5098
|
"@release-it/bumper": "8.0.1",
|
|
5058
|
-
"@types/node": "24.19.
|
|
5099
|
+
"@types/node": "24.19.2",
|
|
5059
5100
|
"@vitest/coverage-v8": "5.0.3",
|
|
5060
|
-
"commit-and-tag-version": "13.
|
|
5101
|
+
"commit-and-tag-version": "13.3.0",
|
|
5061
5102
|
"knip": "6.41.0",
|
|
5062
|
-
"lefthook": "2.2.
|
|
5063
|
-
"modern-web-guidance": "0.0.
|
|
5064
|
-
"oxfmt": "0.
|
|
5103
|
+
"lefthook": "2.2.2",
|
|
5104
|
+
"modern-web-guidance": "0.0.194",
|
|
5105
|
+
"oxfmt": "0.73.0",
|
|
5065
5106
|
"oxlint": "1.87.0",
|
|
5066
|
-
"oxlint-tsgolint": "7.0.
|
|
5067
|
-
"prettier": "3.9.
|
|
5107
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
5108
|
+
"prettier": "3.9.10",
|
|
5068
5109
|
"release-it": "21.1.1",
|
|
5069
|
-
"tsdown": "0.23.
|
|
5110
|
+
"tsdown": "0.23.1",
|
|
5070
5111
|
"typescript": "6.0.3",
|
|
5071
5112
|
"vitest": "5.0.3",
|
|
5072
5113
|
"pnpm": "11.28.5"
|
|
@@ -5074,18 +5115,18 @@ function perPackageDevDeps(config) {
|
|
|
5074
5115
|
deps["@types/node"] = {
|
|
5075
5116
|
"@changesets/cli": "3.0.3",
|
|
5076
5117
|
"@release-it/bumper": "8.0.1",
|
|
5077
|
-
"@types/node": "24.19.
|
|
5118
|
+
"@types/node": "24.19.2",
|
|
5078
5119
|
"@vitest/coverage-v8": "5.0.3",
|
|
5079
|
-
"commit-and-tag-version": "13.
|
|
5120
|
+
"commit-and-tag-version": "13.3.0",
|
|
5080
5121
|
"knip": "6.41.0",
|
|
5081
|
-
"lefthook": "2.2.
|
|
5082
|
-
"modern-web-guidance": "0.0.
|
|
5083
|
-
"oxfmt": "0.
|
|
5122
|
+
"lefthook": "2.2.2",
|
|
5123
|
+
"modern-web-guidance": "0.0.194",
|
|
5124
|
+
"oxfmt": "0.73.0",
|
|
5084
5125
|
"oxlint": "1.87.0",
|
|
5085
|
-
"oxlint-tsgolint": "7.0.
|
|
5086
|
-
"prettier": "3.9.
|
|
5126
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
5127
|
+
"prettier": "3.9.10",
|
|
5087
5128
|
"release-it": "21.1.1",
|
|
5088
|
-
"tsdown": "0.23.
|
|
5129
|
+
"tsdown": "0.23.1",
|
|
5089
5130
|
"typescript": "6.0.3",
|
|
5090
5131
|
"vitest": "5.0.3",
|
|
5091
5132
|
"pnpm": "11.28.5"
|
|
@@ -5094,18 +5135,18 @@ function perPackageDevDeps(config) {
|
|
|
5094
5135
|
if (config.builder === "tsdown") deps["tsdown"] = {
|
|
5095
5136
|
"@changesets/cli": "3.0.3",
|
|
5096
5137
|
"@release-it/bumper": "8.0.1",
|
|
5097
|
-
"@types/node": "24.19.
|
|
5138
|
+
"@types/node": "24.19.2",
|
|
5098
5139
|
"@vitest/coverage-v8": "5.0.3",
|
|
5099
|
-
"commit-and-tag-version": "13.
|
|
5140
|
+
"commit-and-tag-version": "13.3.0",
|
|
5100
5141
|
"knip": "6.41.0",
|
|
5101
|
-
"lefthook": "2.2.
|
|
5102
|
-
"modern-web-guidance": "0.0.
|
|
5103
|
-
"oxfmt": "0.
|
|
5142
|
+
"lefthook": "2.2.2",
|
|
5143
|
+
"modern-web-guidance": "0.0.194",
|
|
5144
|
+
"oxfmt": "0.73.0",
|
|
5104
5145
|
"oxlint": "1.87.0",
|
|
5105
|
-
"oxlint-tsgolint": "7.0.
|
|
5106
|
-
"prettier": "3.9.
|
|
5146
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
5147
|
+
"prettier": "3.9.10",
|
|
5107
5148
|
"release-it": "21.1.1",
|
|
5108
|
-
"tsdown": "0.23.
|
|
5149
|
+
"tsdown": "0.23.1",
|
|
5109
5150
|
"typescript": "6.0.3",
|
|
5110
5151
|
"vitest": "5.0.3",
|
|
5111
5152
|
"pnpm": "11.28.5"
|
|
@@ -5117,18 +5158,18 @@ function vitestDevDeps() {
|
|
|
5117
5158
|
vitest: {
|
|
5118
5159
|
"@changesets/cli": "3.0.3",
|
|
5119
5160
|
"@release-it/bumper": "8.0.1",
|
|
5120
|
-
"@types/node": "24.19.
|
|
5161
|
+
"@types/node": "24.19.2",
|
|
5121
5162
|
"@vitest/coverage-v8": "5.0.3",
|
|
5122
|
-
"commit-and-tag-version": "13.
|
|
5163
|
+
"commit-and-tag-version": "13.3.0",
|
|
5123
5164
|
"knip": "6.41.0",
|
|
5124
|
-
"lefthook": "2.2.
|
|
5125
|
-
"modern-web-guidance": "0.0.
|
|
5126
|
-
"oxfmt": "0.
|
|
5165
|
+
"lefthook": "2.2.2",
|
|
5166
|
+
"modern-web-guidance": "0.0.194",
|
|
5167
|
+
"oxfmt": "0.73.0",
|
|
5127
5168
|
"oxlint": "1.87.0",
|
|
5128
|
-
"oxlint-tsgolint": "7.0.
|
|
5129
|
-
"prettier": "3.9.
|
|
5169
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
5170
|
+
"prettier": "3.9.10",
|
|
5130
5171
|
"release-it": "21.1.1",
|
|
5131
|
-
"tsdown": "0.23.
|
|
5172
|
+
"tsdown": "0.23.1",
|
|
5132
5173
|
"typescript": "6.0.3",
|
|
5133
5174
|
"vitest": "5.0.3",
|
|
5134
5175
|
"pnpm": "11.28.5"
|
|
@@ -5136,18 +5177,18 @@ function vitestDevDeps() {
|
|
|
5136
5177
|
"@vitest/coverage-v8": {
|
|
5137
5178
|
"@changesets/cli": "3.0.3",
|
|
5138
5179
|
"@release-it/bumper": "8.0.1",
|
|
5139
|
-
"@types/node": "24.19.
|
|
5180
|
+
"@types/node": "24.19.2",
|
|
5140
5181
|
"@vitest/coverage-v8": "5.0.3",
|
|
5141
|
-
"commit-and-tag-version": "13.
|
|
5182
|
+
"commit-and-tag-version": "13.3.0",
|
|
5142
5183
|
"knip": "6.41.0",
|
|
5143
|
-
"lefthook": "2.2.
|
|
5144
|
-
"modern-web-guidance": "0.0.
|
|
5145
|
-
"oxfmt": "0.
|
|
5184
|
+
"lefthook": "2.2.2",
|
|
5185
|
+
"modern-web-guidance": "0.0.194",
|
|
5186
|
+
"oxfmt": "0.73.0",
|
|
5146
5187
|
"oxlint": "1.87.0",
|
|
5147
|
-
"oxlint-tsgolint": "7.0.
|
|
5148
|
-
"prettier": "3.9.
|
|
5188
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
5189
|
+
"prettier": "3.9.10",
|
|
5149
5190
|
"release-it": "21.1.1",
|
|
5150
|
-
"tsdown": "0.23.
|
|
5191
|
+
"tsdown": "0.23.1",
|
|
5151
5192
|
"typescript": "6.0.3",
|
|
5152
5193
|
"vitest": "5.0.3",
|
|
5153
5194
|
"pnpm": "11.28.5"
|
|
@@ -5158,18 +5199,18 @@ function rootDevDeps(config) {
|
|
|
5158
5199
|
const deps = { lefthook: {
|
|
5159
5200
|
"@changesets/cli": "3.0.3",
|
|
5160
5201
|
"@release-it/bumper": "8.0.1",
|
|
5161
|
-
"@types/node": "24.19.
|
|
5202
|
+
"@types/node": "24.19.2",
|
|
5162
5203
|
"@vitest/coverage-v8": "5.0.3",
|
|
5163
|
-
"commit-and-tag-version": "13.
|
|
5204
|
+
"commit-and-tag-version": "13.3.0",
|
|
5164
5205
|
"knip": "6.41.0",
|
|
5165
|
-
"lefthook": "2.2.
|
|
5166
|
-
"modern-web-guidance": "0.0.
|
|
5167
|
-
"oxfmt": "0.
|
|
5206
|
+
"lefthook": "2.2.2",
|
|
5207
|
+
"modern-web-guidance": "0.0.194",
|
|
5208
|
+
"oxfmt": "0.73.0",
|
|
5168
5209
|
"oxlint": "1.87.0",
|
|
5169
|
-
"oxlint-tsgolint": "7.0.
|
|
5170
|
-
"prettier": "3.9.
|
|
5210
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
5211
|
+
"prettier": "3.9.10",
|
|
5171
5212
|
"release-it": "21.1.1",
|
|
5172
|
-
"tsdown": "0.23.
|
|
5213
|
+
"tsdown": "0.23.1",
|
|
5173
5214
|
"typescript": "6.0.3",
|
|
5174
5215
|
"vitest": "5.0.3",
|
|
5175
5216
|
"pnpm": "11.28.5"
|
|
@@ -5178,18 +5219,18 @@ function rootDevDeps(config) {
|
|
|
5178
5219
|
deps["knip"] = {
|
|
5179
5220
|
"@changesets/cli": "3.0.3",
|
|
5180
5221
|
"@release-it/bumper": "8.0.1",
|
|
5181
|
-
"@types/node": "24.19.
|
|
5222
|
+
"@types/node": "24.19.2",
|
|
5182
5223
|
"@vitest/coverage-v8": "5.0.3",
|
|
5183
|
-
"commit-and-tag-version": "13.
|
|
5224
|
+
"commit-and-tag-version": "13.3.0",
|
|
5184
5225
|
"knip": "6.41.0",
|
|
5185
|
-
"lefthook": "2.2.
|
|
5186
|
-
"modern-web-guidance": "0.0.
|
|
5187
|
-
"oxfmt": "0.
|
|
5226
|
+
"lefthook": "2.2.2",
|
|
5227
|
+
"modern-web-guidance": "0.0.194",
|
|
5228
|
+
"oxfmt": "0.73.0",
|
|
5188
5229
|
"oxlint": "1.87.0",
|
|
5189
|
-
"oxlint-tsgolint": "7.0.
|
|
5190
|
-
"prettier": "3.9.
|
|
5230
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
5231
|
+
"prettier": "3.9.10",
|
|
5191
5232
|
"release-it": "21.1.1",
|
|
5192
|
-
"tsdown": "0.23.
|
|
5233
|
+
"tsdown": "0.23.1",
|
|
5193
5234
|
"typescript": "6.0.3",
|
|
5194
5235
|
"vitest": "5.0.3",
|
|
5195
5236
|
"pnpm": "11.28.5"
|
|
@@ -5197,18 +5238,18 @@ function rootDevDeps(config) {
|
|
|
5197
5238
|
deps["oxlint"] = {
|
|
5198
5239
|
"@changesets/cli": "3.0.3",
|
|
5199
5240
|
"@release-it/bumper": "8.0.1",
|
|
5200
|
-
"@types/node": "24.19.
|
|
5241
|
+
"@types/node": "24.19.2",
|
|
5201
5242
|
"@vitest/coverage-v8": "5.0.3",
|
|
5202
|
-
"commit-and-tag-version": "13.
|
|
5243
|
+
"commit-and-tag-version": "13.3.0",
|
|
5203
5244
|
"knip": "6.41.0",
|
|
5204
|
-
"lefthook": "2.2.
|
|
5205
|
-
"modern-web-guidance": "0.0.
|
|
5206
|
-
"oxfmt": "0.
|
|
5245
|
+
"lefthook": "2.2.2",
|
|
5246
|
+
"modern-web-guidance": "0.0.194",
|
|
5247
|
+
"oxfmt": "0.73.0",
|
|
5207
5248
|
"oxlint": "1.87.0",
|
|
5208
|
-
"oxlint-tsgolint": "7.0.
|
|
5209
|
-
"prettier": "3.9.
|
|
5249
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
5250
|
+
"prettier": "3.9.10",
|
|
5210
5251
|
"release-it": "21.1.1",
|
|
5211
|
-
"tsdown": "0.23.
|
|
5252
|
+
"tsdown": "0.23.1",
|
|
5212
5253
|
"typescript": "6.0.3",
|
|
5213
5254
|
"vitest": "5.0.3",
|
|
5214
5255
|
"pnpm": "11.28.5"
|
|
@@ -5218,18 +5259,18 @@ function rootDevDeps(config) {
|
|
|
5218
5259
|
if (config.stack === "typescript") deps["oxlint-tsgolint"] = {
|
|
5219
5260
|
"@changesets/cli": "3.0.3",
|
|
5220
5261
|
"@release-it/bumper": "8.0.1",
|
|
5221
|
-
"@types/node": "24.19.
|
|
5262
|
+
"@types/node": "24.19.2",
|
|
5222
5263
|
"@vitest/coverage-v8": "5.0.3",
|
|
5223
|
-
"commit-and-tag-version": "13.
|
|
5264
|
+
"commit-and-tag-version": "13.3.0",
|
|
5224
5265
|
"knip": "6.41.0",
|
|
5225
|
-
"lefthook": "2.2.
|
|
5226
|
-
"modern-web-guidance": "0.0.
|
|
5227
|
-
"oxfmt": "0.
|
|
5266
|
+
"lefthook": "2.2.2",
|
|
5267
|
+
"modern-web-guidance": "0.0.194",
|
|
5268
|
+
"oxfmt": "0.73.0",
|
|
5228
5269
|
"oxlint": "1.87.0",
|
|
5229
|
-
"oxlint-tsgolint": "7.0.
|
|
5230
|
-
"prettier": "3.9.
|
|
5270
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
5271
|
+
"prettier": "3.9.10",
|
|
5231
5272
|
"release-it": "21.1.1",
|
|
5232
|
-
"tsdown": "0.23.
|
|
5273
|
+
"tsdown": "0.23.1",
|
|
5233
5274
|
"typescript": "6.0.3",
|
|
5234
5275
|
"vitest": "5.0.3",
|
|
5235
5276
|
"pnpm": "11.28.5"
|
|
@@ -5242,18 +5283,18 @@ function addReleaseDeps(deps, ctx) {
|
|
|
5242
5283
|
deps["release-it"] = {
|
|
5243
5284
|
"@changesets/cli": "3.0.3",
|
|
5244
5285
|
"@release-it/bumper": "8.0.1",
|
|
5245
|
-
"@types/node": "24.19.
|
|
5286
|
+
"@types/node": "24.19.2",
|
|
5246
5287
|
"@vitest/coverage-v8": "5.0.3",
|
|
5247
|
-
"commit-and-tag-version": "13.
|
|
5288
|
+
"commit-and-tag-version": "13.3.0",
|
|
5248
5289
|
"knip": "6.41.0",
|
|
5249
|
-
"lefthook": "2.2.
|
|
5250
|
-
"modern-web-guidance": "0.0.
|
|
5251
|
-
"oxfmt": "0.
|
|
5290
|
+
"lefthook": "2.2.2",
|
|
5291
|
+
"modern-web-guidance": "0.0.194",
|
|
5292
|
+
"oxfmt": "0.73.0",
|
|
5252
5293
|
"oxlint": "1.87.0",
|
|
5253
|
-
"oxlint-tsgolint": "7.0.
|
|
5254
|
-
"prettier": "3.9.
|
|
5294
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
5295
|
+
"prettier": "3.9.10",
|
|
5255
5296
|
"release-it": "21.1.1",
|
|
5256
|
-
"tsdown": "0.23.
|
|
5297
|
+
"tsdown": "0.23.1",
|
|
5257
5298
|
"typescript": "6.0.3",
|
|
5258
5299
|
"vitest": "5.0.3",
|
|
5259
5300
|
"pnpm": "11.28.5"
|
|
@@ -5261,18 +5302,18 @@ function addReleaseDeps(deps, ctx) {
|
|
|
5261
5302
|
if (ctx.config.structure === "monorepo") deps["@release-it/bumper"] = {
|
|
5262
5303
|
"@changesets/cli": "3.0.3",
|
|
5263
5304
|
"@release-it/bumper": "8.0.1",
|
|
5264
|
-
"@types/node": "24.19.
|
|
5305
|
+
"@types/node": "24.19.2",
|
|
5265
5306
|
"@vitest/coverage-v8": "5.0.3",
|
|
5266
|
-
"commit-and-tag-version": "13.
|
|
5307
|
+
"commit-and-tag-version": "13.3.0",
|
|
5267
5308
|
"knip": "6.41.0",
|
|
5268
|
-
"lefthook": "2.2.
|
|
5269
|
-
"modern-web-guidance": "0.0.
|
|
5270
|
-
"oxfmt": "0.
|
|
5309
|
+
"lefthook": "2.2.2",
|
|
5310
|
+
"modern-web-guidance": "0.0.194",
|
|
5311
|
+
"oxfmt": "0.73.0",
|
|
5271
5312
|
"oxlint": "1.87.0",
|
|
5272
|
-
"oxlint-tsgolint": "7.0.
|
|
5273
|
-
"prettier": "3.9.
|
|
5313
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
5314
|
+
"prettier": "3.9.10",
|
|
5274
5315
|
"release-it": "21.1.1",
|
|
5275
|
-
"tsdown": "0.23.
|
|
5316
|
+
"tsdown": "0.23.1",
|
|
5276
5317
|
"typescript": "6.0.3",
|
|
5277
5318
|
"vitest": "5.0.3",
|
|
5278
5319
|
"pnpm": "11.28.5"
|
|
@@ -5282,18 +5323,18 @@ function addReleaseDeps(deps, ctx) {
|
|
|
5282
5323
|
if (wantsCommitAndTagVersion(ctx)) deps[COMMIT_AND_TAG_VERSION_PACKAGE] = {
|
|
5283
5324
|
"@changesets/cli": "3.0.3",
|
|
5284
5325
|
"@release-it/bumper": "8.0.1",
|
|
5285
|
-
"@types/node": "24.19.
|
|
5326
|
+
"@types/node": "24.19.2",
|
|
5286
5327
|
"@vitest/coverage-v8": "5.0.3",
|
|
5287
|
-
"commit-and-tag-version": "13.
|
|
5328
|
+
"commit-and-tag-version": "13.3.0",
|
|
5288
5329
|
"knip": "6.41.0",
|
|
5289
|
-
"lefthook": "2.2.
|
|
5290
|
-
"modern-web-guidance": "0.0.
|
|
5291
|
-
"oxfmt": "0.
|
|
5330
|
+
"lefthook": "2.2.2",
|
|
5331
|
+
"modern-web-guidance": "0.0.194",
|
|
5332
|
+
"oxfmt": "0.73.0",
|
|
5292
5333
|
"oxlint": "1.87.0",
|
|
5293
|
-
"oxlint-tsgolint": "7.0.
|
|
5294
|
-
"prettier": "3.9.
|
|
5334
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
5335
|
+
"prettier": "3.9.10",
|
|
5295
5336
|
"release-it": "21.1.1",
|
|
5296
|
-
"tsdown": "0.23.
|
|
5337
|
+
"tsdown": "0.23.1",
|
|
5297
5338
|
"typescript": "6.0.3",
|
|
5298
5339
|
"vitest": "5.0.3",
|
|
5299
5340
|
"pnpm": "11.28.5"
|
|
@@ -5302,18 +5343,18 @@ function addReleaseDeps(deps, ctx) {
|
|
|
5302
5343
|
case "changesets": deps["@changesets/cli"] = {
|
|
5303
5344
|
"@changesets/cli": "3.0.3",
|
|
5304
5345
|
"@release-it/bumper": "8.0.1",
|
|
5305
|
-
"@types/node": "24.19.
|
|
5346
|
+
"@types/node": "24.19.2",
|
|
5306
5347
|
"@vitest/coverage-v8": "5.0.3",
|
|
5307
|
-
"commit-and-tag-version": "13.
|
|
5348
|
+
"commit-and-tag-version": "13.3.0",
|
|
5308
5349
|
"knip": "6.41.0",
|
|
5309
|
-
"lefthook": "2.2.
|
|
5310
|
-
"modern-web-guidance": "0.0.
|
|
5311
|
-
"oxfmt": "0.
|
|
5350
|
+
"lefthook": "2.2.2",
|
|
5351
|
+
"modern-web-guidance": "0.0.194",
|
|
5352
|
+
"oxfmt": "0.73.0",
|
|
5312
5353
|
"oxlint": "1.87.0",
|
|
5313
|
-
"oxlint-tsgolint": "7.0.
|
|
5314
|
-
"prettier": "3.9.
|
|
5354
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
5355
|
+
"prettier": "3.9.10",
|
|
5315
5356
|
"release-it": "21.1.1",
|
|
5316
|
-
"tsdown": "0.23.
|
|
5357
|
+
"tsdown": "0.23.1",
|
|
5317
5358
|
"typescript": "6.0.3",
|
|
5318
5359
|
"vitest": "5.0.3",
|
|
5319
5360
|
"pnpm": "11.28.5"
|
|
@@ -5430,18 +5471,18 @@ function applyMetadataDefaults(pkg, ctx, changes) {
|
|
|
5430
5471
|
const pinnedPnpm = {
|
|
5431
5472
|
"@changesets/cli": "3.0.3",
|
|
5432
5473
|
"@release-it/bumper": "8.0.1",
|
|
5433
|
-
"@types/node": "24.19.
|
|
5474
|
+
"@types/node": "24.19.2",
|
|
5434
5475
|
"@vitest/coverage-v8": "5.0.3",
|
|
5435
|
-
"commit-and-tag-version": "13.
|
|
5476
|
+
"commit-and-tag-version": "13.3.0",
|
|
5436
5477
|
"knip": "6.41.0",
|
|
5437
|
-
"lefthook": "2.2.
|
|
5438
|
-
"modern-web-guidance": "0.0.
|
|
5439
|
-
"oxfmt": "0.
|
|
5478
|
+
"lefthook": "2.2.2",
|
|
5479
|
+
"modern-web-guidance": "0.0.194",
|
|
5480
|
+
"oxfmt": "0.73.0",
|
|
5440
5481
|
"oxlint": "1.87.0",
|
|
5441
|
-
"oxlint-tsgolint": "7.0.
|
|
5442
|
-
"prettier": "3.9.
|
|
5482
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
5483
|
+
"prettier": "3.9.10",
|
|
5443
5484
|
"release-it": "21.1.1",
|
|
5444
|
-
"tsdown": "0.23.
|
|
5485
|
+
"tsdown": "0.23.1",
|
|
5445
5486
|
"typescript": "6.0.3",
|
|
5446
5487
|
"vitest": "5.0.3",
|
|
5447
5488
|
"pnpm": "11.28.5"
|
|
@@ -5483,24 +5524,24 @@ function buildManagedDevDeps(ctx) {
|
|
|
5483
5524
|
const devDeps = { ...rootDevDeps(ctx.config) };
|
|
5484
5525
|
if (!isMonorepo) Object.assign(devDeps, perPackageDevDeps(ctx.config));
|
|
5485
5526
|
if (ctx.config.stack !== "none") devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.15.0";
|
|
5486
|
-
if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.
|
|
5527
|
+
if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.95.0";
|
|
5487
5528
|
if (ctx.config.useEslintPlugin && ctx.config.stack !== "none") devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.17.0";
|
|
5488
|
-
if (wantsAgentCli(ctx)) devDeps[AGENT_CLI_PACKAGE] = "0.1.
|
|
5529
|
+
if (wantsAgentCli(ctx)) devDeps[AGENT_CLI_PACKAGE] = "0.1.3";
|
|
5489
5530
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
5490
5531
|
"@changesets/cli": "3.0.3",
|
|
5491
5532
|
"@release-it/bumper": "8.0.1",
|
|
5492
|
-
"@types/node": "24.19.
|
|
5533
|
+
"@types/node": "24.19.2",
|
|
5493
5534
|
"@vitest/coverage-v8": "5.0.3",
|
|
5494
|
-
"commit-and-tag-version": "13.
|
|
5535
|
+
"commit-and-tag-version": "13.3.0",
|
|
5495
5536
|
"knip": "6.41.0",
|
|
5496
|
-
"lefthook": "2.2.
|
|
5497
|
-
"modern-web-guidance": "0.0.
|
|
5498
|
-
"oxfmt": "0.
|
|
5537
|
+
"lefthook": "2.2.2",
|
|
5538
|
+
"modern-web-guidance": "0.0.194",
|
|
5539
|
+
"oxfmt": "0.73.0",
|
|
5499
5540
|
"oxlint": "1.87.0",
|
|
5500
|
-
"oxlint-tsgolint": "7.0.
|
|
5501
|
-
"prettier": "3.9.
|
|
5541
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
5542
|
+
"prettier": "3.9.10",
|
|
5502
5543
|
"release-it": "21.1.1",
|
|
5503
|
-
"tsdown": "0.23.
|
|
5544
|
+
"tsdown": "0.23.1",
|
|
5504
5545
|
"typescript": "6.0.3",
|
|
5505
5546
|
"vitest": "5.0.3",
|
|
5506
5547
|
"pnpm": "11.28.5"
|
|
@@ -5508,18 +5549,18 @@ function buildManagedDevDeps(ctx) {
|
|
|
5508
5549
|
if (ctx.config.formatter === "prettier") devDeps["prettier"] = {
|
|
5509
5550
|
"@changesets/cli": "3.0.3",
|
|
5510
5551
|
"@release-it/bumper": "8.0.1",
|
|
5511
|
-
"@types/node": "24.19.
|
|
5552
|
+
"@types/node": "24.19.2",
|
|
5512
5553
|
"@vitest/coverage-v8": "5.0.3",
|
|
5513
|
-
"commit-and-tag-version": "13.
|
|
5554
|
+
"commit-and-tag-version": "13.3.0",
|
|
5514
5555
|
"knip": "6.41.0",
|
|
5515
|
-
"lefthook": "2.2.
|
|
5516
|
-
"modern-web-guidance": "0.0.
|
|
5517
|
-
"oxfmt": "0.
|
|
5556
|
+
"lefthook": "2.2.2",
|
|
5557
|
+
"modern-web-guidance": "0.0.194",
|
|
5558
|
+
"oxfmt": "0.73.0",
|
|
5518
5559
|
"oxlint": "1.87.0",
|
|
5519
|
-
"oxlint-tsgolint": "7.0.
|
|
5520
|
-
"prettier": "3.9.
|
|
5560
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
5561
|
+
"prettier": "3.9.10",
|
|
5521
5562
|
"release-it": "21.1.1",
|
|
5522
|
-
"tsdown": "0.23.
|
|
5563
|
+
"tsdown": "0.23.1",
|
|
5523
5564
|
"typescript": "6.0.3",
|
|
5524
5565
|
"vitest": "5.0.3",
|
|
5525
5566
|
"pnpm": "11.28.5"
|
|
@@ -5533,7 +5574,7 @@ async function generatePackageJson(ctx) {
|
|
|
5533
5574
|
const allScripts = buildManagedScripts(ctx);
|
|
5534
5575
|
const unmanaged = readUnmanagedDevDepNames(ctx);
|
|
5535
5576
|
const devDeps = buildManagedDevDeps(ctx);
|
|
5536
|
-
if (existing) {
|
|
5577
|
+
if (existing !== void 0 && existing.trim() !== "") {
|
|
5537
5578
|
const pkg = parsePackageJson(existing);
|
|
5538
5579
|
if (!pkg) return {
|
|
5539
5580
|
filePath,
|
|
@@ -5579,18 +5620,18 @@ async function generatePackageJson(ctx) {
|
|
|
5579
5620
|
packageManager: `pnpm@${{
|
|
5580
5621
|
"@changesets/cli": "3.0.3",
|
|
5581
5622
|
"@release-it/bumper": "8.0.1",
|
|
5582
|
-
"@types/node": "24.19.
|
|
5623
|
+
"@types/node": "24.19.2",
|
|
5583
5624
|
"@vitest/coverage-v8": "5.0.3",
|
|
5584
|
-
"commit-and-tag-version": "13.
|
|
5625
|
+
"commit-and-tag-version": "13.3.0",
|
|
5585
5626
|
"knip": "6.41.0",
|
|
5586
|
-
"lefthook": "2.2.
|
|
5587
|
-
"modern-web-guidance": "0.0.
|
|
5588
|
-
"oxfmt": "0.
|
|
5627
|
+
"lefthook": "2.2.2",
|
|
5628
|
+
"modern-web-guidance": "0.0.194",
|
|
5629
|
+
"oxfmt": "0.73.0",
|
|
5589
5630
|
"oxlint": "1.87.0",
|
|
5590
|
-
"oxlint-tsgolint": "7.0.
|
|
5591
|
-
"prettier": "3.9.
|
|
5631
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
5632
|
+
"prettier": "3.9.10",
|
|
5592
5633
|
"release-it": "21.1.1",
|
|
5593
|
-
"tsdown": "0.23.
|
|
5634
|
+
"tsdown": "0.23.1",
|
|
5594
5635
|
"typescript": "6.0.3",
|
|
5595
5636
|
"vitest": "5.0.3",
|
|
5596
5637
|
"pnpm": "11.28.5"
|
|
@@ -5651,29 +5692,56 @@ function describeInclude(include) {
|
|
|
5651
5692
|
return `added include: ${formatInclude(include)}`;
|
|
5652
5693
|
}
|
|
5653
5694
|
function warnIfNarrowInclude(ctx, filePath, include) {
|
|
5654
|
-
if (!Array.isArray(include) || include.length !==
|
|
5695
|
+
if (!Array.isArray(include) || include.length !== LEGACY_INCLUDE.length || !include.every((entry, index) => entry === LEGACY_INCLUDE[index])) return;
|
|
5655
5696
|
const testDir = path.join(path.dirname(filePath), "test");
|
|
5656
5697
|
if (!ctx.exists(testDir)) return;
|
|
5657
5698
|
log$1.warn(`${filePath}: "include" is ${formatInclude(include)}, so a test/ directory beside it sits outside tsc --noEmit and its types go unchecked. Consider "include": ${formatInclude(DEFAULT_INCLUDE)}.`);
|
|
5658
5699
|
}
|
|
5700
|
+
const NODE_PRESET = "@bensandee/config/tsconfig/node";
|
|
5701
|
+
function extendsNodePreset(extendsValue) {
|
|
5702
|
+
if (extendsValue === void 0) return false;
|
|
5703
|
+
if (Array.isArray(extendsValue)) return extendsValue.includes(NODE_PRESET);
|
|
5704
|
+
return extendsValue === NODE_PRESET;
|
|
5705
|
+
}
|
|
5706
|
+
const VERBATIM_MODULE_SYNTAX_KEY = "verbatimModuleSyntax";
|
|
5659
5707
|
function buildGeneratedTsconfig(ctx, extendsValue, options = {}) {
|
|
5660
5708
|
const dir = options.dir ?? ".";
|
|
5661
5709
|
const include = options.include ?? DEFAULT_INCLUDE;
|
|
5662
5710
|
const config = {
|
|
5663
5711
|
extends: extendsValue,
|
|
5712
|
+
...options.verbatimModuleSyntax ? { compilerOptions: { [VERBATIM_MODULE_SYNTAX_KEY]: true } } : {},
|
|
5664
5713
|
...ctx.exists(path.join(dir, "src")) ? { include: [...include] } : {}
|
|
5665
5714
|
};
|
|
5666
5715
|
return JSON.stringify(config, null, 2) + "\n";
|
|
5667
5716
|
}
|
|
5717
|
+
function withMergedVerbatimKey(template) {
|
|
5718
|
+
const parsed = parseTsconfig(template);
|
|
5719
|
+
parsed.compilerOptions = {
|
|
5720
|
+
...parsed.compilerOptions,
|
|
5721
|
+
[VERBATIM_MODULE_SYNTAX_KEY]: true
|
|
5722
|
+
};
|
|
5723
|
+
return JSON.stringify(parsed, null, 2) + "\n";
|
|
5724
|
+
}
|
|
5668
5725
|
async function generateTsconfig(ctx) {
|
|
5669
5726
|
const filePath = "tsconfig.json";
|
|
5670
5727
|
const existing = ctx.read(filePath);
|
|
5671
5728
|
if (ctx.config.structure === "monorepo") return [generateMonorepoRootTsconfig(ctx), ...ctx.config.detectPackageTypes ? generateMonorepoPackageTsconfigs(ctx) : []];
|
|
5672
5729
|
const extendsValue = `@bensandee/config/tsconfig/${ctx.config.projectType}`;
|
|
5673
5730
|
if (ctx.config.stack !== "typescript") {
|
|
5674
|
-
const
|
|
5675
|
-
const
|
|
5676
|
-
|
|
5731
|
+
const nodePreset = extendsNodePreset(extendsValue);
|
|
5732
|
+
const keylessCurrent = buildGeneratedTsconfig(ctx, extendsValue);
|
|
5733
|
+
const keylessLegacy = buildGeneratedTsconfig(ctx, extendsValue, { include: LEGACY_INCLUDE });
|
|
5734
|
+
const generated = /* @__PURE__ */ new Set([
|
|
5735
|
+
buildGeneratedTsconfig(ctx, extendsValue, { verbatimModuleSyntax: nodePreset }),
|
|
5736
|
+
buildGeneratedTsconfig(ctx, extendsValue, {
|
|
5737
|
+
verbatimModuleSyntax: nodePreset,
|
|
5738
|
+
include: LEGACY_INCLUDE
|
|
5739
|
+
}),
|
|
5740
|
+
keylessCurrent,
|
|
5741
|
+
keylessLegacy,
|
|
5742
|
+
...nodePreset ? [withMergedVerbatimKey(keylessCurrent), withMergedVerbatimKey(keylessLegacy)] : []
|
|
5743
|
+
]);
|
|
5744
|
+
if (existing !== void 0 && generated.has(existing)) return [reportStaleConfig(filePath, `stack is ${ctx.config.stack}`)];
|
|
5677
5745
|
return [{
|
|
5678
5746
|
filePath,
|
|
5679
5747
|
action: "skipped",
|
|
@@ -5681,7 +5749,7 @@ async function generateTsconfig(ctx) {
|
|
|
5681
5749
|
}];
|
|
5682
5750
|
}
|
|
5683
5751
|
if (!existing) {
|
|
5684
|
-
ctx.write(filePath, buildGeneratedTsconfig(ctx, extendsValue));
|
|
5752
|
+
ctx.write(filePath, buildGeneratedTsconfig(ctx, extendsValue, { verbatimModuleSyntax: extendsNodePreset(extendsValue) }));
|
|
5685
5753
|
return [{
|
|
5686
5754
|
filePath,
|
|
5687
5755
|
action: "created",
|
|
@@ -5734,6 +5802,13 @@ function mergeSingleTsconfig(ctx, filePath, extendsValue) {
|
|
|
5734
5802
|
parsed.extends = extendsValue;
|
|
5735
5803
|
changes.push(`added extends: ${extendsValue}`);
|
|
5736
5804
|
}
|
|
5805
|
+
if (extendsNodePreset(parsed.extends) && parsed.compilerOptions?.[VERBATIM_MODULE_SYNTAX_KEY] === void 0) {
|
|
5806
|
+
parsed.compilerOptions = {
|
|
5807
|
+
...parsed.compilerOptions,
|
|
5808
|
+
[VERBATIM_MODULE_SYNTAX_KEY]: true
|
|
5809
|
+
};
|
|
5810
|
+
changes.push(`added compilerOptions.${VERBATIM_MODULE_SYNTAX_KEY}: true`);
|
|
5811
|
+
}
|
|
5737
5812
|
if (!parsed.include) {
|
|
5738
5813
|
const tsconfigDir = path.dirname(filePath);
|
|
5739
5814
|
const srcDir = tsconfigDir === "." ? "src" : path.join(tsconfigDir, "src");
|
|
@@ -6206,7 +6281,7 @@ function warnIfMissingJsonSummary(ctx) {
|
|
|
6206
6281
|
if (content === void 0) continue;
|
|
6207
6282
|
const issue = findJsonSummaryIssue(content);
|
|
6208
6283
|
if (issue === void 0) return;
|
|
6209
|
-
log$1.warn(`${rel}: ${describeIssue(issue)} -- coverage tracking (bst coverage:summary / coverage:check) requires the "json-summary" reporter to read coverage/coverage-summary.json.`);
|
|
6284
|
+
log$1.warn(`${rel}: ${describeIssue(issue)} -- coverage tracking (pnpm exec bst coverage:summary / coverage:check) requires the "json-summary" reporter to read coverage/coverage-summary.json.`);
|
|
6210
6285
|
return;
|
|
6211
6286
|
}
|
|
6212
6287
|
}
|
|
@@ -6232,7 +6307,7 @@ function warnIfMissingInclude(ctx) {
|
|
|
6232
6307
|
const block = extractCoverageBlock(content);
|
|
6233
6308
|
if (block === void 0) return;
|
|
6234
6309
|
if (/\binclude\s*:/.test(block)) return;
|
|
6235
|
-
log$1.warn(`${rel}: coverage.include is not set -- Vitest 3+ instruments only files a test imports, so an untested source file is invisible to coverage-baseline.json / bst coverage:check, not just uncovered. Suggested: coverage.include: ${suggestedInclude(ctx.config.structure)}.`);
|
|
6310
|
+
log$1.warn(`${rel}: coverage.include is not set -- Vitest 3+ instruments only files a test imports, so an untested source file is invisible to coverage-baseline.json / pnpm exec bst coverage:check, not just uncovered. Suggested: coverage.include: ${suggestedInclude(ctx.config.structure)}.`);
|
|
6236
6311
|
return;
|
|
6237
6312
|
}
|
|
6238
6313
|
}
|
|
@@ -6619,7 +6694,7 @@ async function generateTsdown(ctx) {
|
|
|
6619
6694
|
description: "Monorepo: tsdown config belongs in individual packages"
|
|
6620
6695
|
};
|
|
6621
6696
|
if (ctx.config.builder !== "tsdown") {
|
|
6622
|
-
if (ctx.read(filePath) !== void 0 && isGeneratedTemplate(ctx, filePath,
|
|
6697
|
+
if (ctx.read(filePath) !== void 0 && isGeneratedTemplate(ctx, filePath, "import { defineConfig } from \"tsdown\";\n\nexport default defineConfig({\n entry: [\"src/index.ts\"],\n});\n")) return reportStaleConfig(filePath, `builder is ${ctx.config.builder}`);
|
|
6623
6698
|
return {
|
|
6624
6699
|
filePath,
|
|
6625
6700
|
action: "skipped",
|
|
@@ -7478,7 +7553,7 @@ function ciDefaultBranch(source) {
|
|
|
7478
7553
|
function refuseForgeDefaultMismatch(configured, source, command) {
|
|
7479
7554
|
const reported = ciDefaultBranch(source);
|
|
7480
7555
|
if (reported === void 0 || reported === configured) return;
|
|
7481
|
-
throw new FatalError(`${command}: .tooling.json#defaultBranch is "${configured}", but the forge reports "${reported}" as this repository's default branch. Set .tooling.json#defaultBranch to "${reported}" and run bst repo:sync.`);
|
|
7556
|
+
throw new FatalError(`${command}: .tooling.json#defaultBranch is "${configured}", but the forge reports "${reported}" as this repository's default branch. Set .tooling.json#defaultBranch to "${reported}" and run pnpm exec bst repo:sync.`);
|
|
7482
7557
|
}
|
|
7483
7558
|
//#endregion
|
|
7484
7559
|
//#region src/generators/release-ci.ts
|
|
@@ -8007,15 +8082,15 @@ function ignorableDependencies(ctx) {
|
|
|
8007
8082
|
function ignoreDependenciesField(dependencies) {
|
|
8008
8083
|
return dependencies.length > 0 ? `\n ignoreDependencies: ${JSON.stringify(dependencies)},` : "";
|
|
8009
8084
|
}
|
|
8010
|
-
function buildKnipConfigSingle(ignoreBinaries, ignoreDependencies) {
|
|
8085
|
+
function buildKnipConfigSingle(glob, ignoreBinaries, ignoreDependencies) {
|
|
8011
8086
|
return `import type { KnipConfig } from "knip";
|
|
8012
8087
|
|
|
8013
8088
|
export default {
|
|
8014
|
-
project: ["
|
|
8089
|
+
project: ["${glob}"],${ignoreBinariesField(ignoreBinaries)}${ignoreDependenciesField(ignoreDependencies)}
|
|
8015
8090
|
} satisfies KnipConfig;
|
|
8016
8091
|
`;
|
|
8017
8092
|
}
|
|
8018
|
-
function buildKnipConfigMonorepo(ignoreBinaries, ignoreDependencies) {
|
|
8093
|
+
function buildKnipConfigMonorepo(glob, ignoreBinaries, ignoreDependencies) {
|
|
8019
8094
|
return `import type { KnipConfig } from "knip";
|
|
8020
8095
|
|
|
8021
8096
|
export default {${ignoreBinariesField(ignoreBinaries)}${ignoreDependenciesField(ignoreDependencies)}
|
|
@@ -8025,7 +8100,7 @@ export default {${ignoreBinariesField(ignoreBinaries)}${ignoreDependenciesField(
|
|
|
8025
8100
|
project: [],
|
|
8026
8101
|
},
|
|
8027
8102
|
"packages/*": {
|
|
8028
|
-
project: ["
|
|
8103
|
+
project: ["${glob}"],
|
|
8029
8104
|
},
|
|
8030
8105
|
},
|
|
8031
8106
|
} satisfies KnipConfig;
|
|
@@ -8118,17 +8193,20 @@ function ensureIgnoreInJson(content, ignoreBinaries, ignoreDependencies, needsHo
|
|
|
8118
8193
|
if (!changed) return void 0;
|
|
8119
8194
|
return JSON.stringify(parsed, null, 2) + "\n";
|
|
8120
8195
|
}
|
|
8196
|
+
function arrayHasEntry(arrayContent, entry) {
|
|
8197
|
+
return arrayContent.includes(`"${entry}"`) || arrayContent.includes(`'${entry}'`);
|
|
8198
|
+
}
|
|
8121
8199
|
function missingIgnoreBinariesInTs(content, binaries) {
|
|
8122
8200
|
const match = /ignoreBinaries:\s*\[([^\]]*)\]/.exec(content);
|
|
8123
8201
|
if (!match) return binaries;
|
|
8124
8202
|
const arrayContent = match[1] ?? "";
|
|
8125
|
-
return binaries.filter((binary) => !arrayContent
|
|
8203
|
+
return binaries.filter((binary) => !arrayHasEntry(arrayContent, binary));
|
|
8126
8204
|
}
|
|
8127
8205
|
function missingIgnoreDependenciesInTs(content, dependencies) {
|
|
8128
8206
|
const match = /ignoreDependencies:\s*\[([^\]]*)\]/.exec(content);
|
|
8129
8207
|
if (!match) return dependencies;
|
|
8130
8208
|
const arrayContent = match[1] ?? "";
|
|
8131
|
-
return dependencies.filter((dependency) => !arrayContent
|
|
8209
|
+
return dependencies.filter((dependency) => !arrayHasEntry(arrayContent, dependency));
|
|
8132
8210
|
}
|
|
8133
8211
|
function insertIntoTsArray(content, field, entries) {
|
|
8134
8212
|
const quoted = entries.map((entry) => `"${entry}"`).join(", ");
|
|
@@ -8190,6 +8268,7 @@ async function generateKnip(ctx) {
|
|
|
8190
8268
|
const isMonorepo = ctx.config.structure === "monorepo";
|
|
8191
8269
|
const ignoreBinaries = foreignOwnedBinaries(ctx);
|
|
8192
8270
|
const ignoreDependencies = ignorableDependencies(ctx);
|
|
8271
|
+
const glob = sourceGlob(ctx.config.stack);
|
|
8193
8272
|
const buildConfig = isMonorepo ? buildKnipConfigMonorepo : buildKnipConfigSingle;
|
|
8194
8273
|
const existingPath = KNIP_CONFIG_PATHS.find((p) => ctx.exists(p));
|
|
8195
8274
|
if (existingPath) {
|
|
@@ -8200,7 +8279,7 @@ async function generateKnip(ctx) {
|
|
|
8200
8279
|
description: "Could not read existing knip config"
|
|
8201
8280
|
};
|
|
8202
8281
|
if (content.trim() === "") {
|
|
8203
|
-
const blank = isJsonConfig(existingPath) ? ensureIgnorePattern("{}", existingPath, ignoreBinaries, ignoreDependencies) ?? "{}\n" : formattedTemplate(ctx, existingPath, buildConfig(ignoreBinaries, ignoreDependencies));
|
|
8282
|
+
const blank = isJsonConfig(existingPath) ? ensureIgnorePattern("{}", existingPath, ignoreBinaries, ignoreDependencies) ?? "{}\n" : formattedTemplate(ctx, existingPath, buildConfig(glob, ignoreBinaries, ignoreDependencies));
|
|
8204
8283
|
ctx.write(existingPath, blank);
|
|
8205
8284
|
return {
|
|
8206
8285
|
filePath: existingPath,
|
|
@@ -8210,7 +8289,7 @@ async function generateKnip(ctx) {
|
|
|
8210
8289
|
}
|
|
8211
8290
|
const updated = ensureIgnorePattern(content, existingPath, ignoreBinaries, ignoreDependencies);
|
|
8212
8291
|
if (updated) {
|
|
8213
|
-
ctx.write(existingPath, updated);
|
|
8292
|
+
ctx.write(existingPath, ctx.format(existingPath, updated));
|
|
8214
8293
|
return {
|
|
8215
8294
|
filePath: existingPath,
|
|
8216
8295
|
action: "updated",
|
|
@@ -8223,7 +8302,7 @@ async function generateKnip(ctx) {
|
|
|
8223
8302
|
description: existingPath === filePath ? "Already configured" : `Existing config found at ${existingPath}`
|
|
8224
8303
|
};
|
|
8225
8304
|
}
|
|
8226
|
-
ctx.write(filePath, formattedTemplate(ctx, filePath, buildConfig(ignoreBinaries, ignoreDependencies)));
|
|
8305
|
+
ctx.write(filePath, formattedTemplate(ctx, filePath, buildConfig(glob, ignoreBinaries, ignoreDependencies)));
|
|
8227
8306
|
return {
|
|
8228
8307
|
filePath,
|
|
8229
8308
|
action: "created",
|
|
@@ -8342,7 +8421,7 @@ async function generatePnpmWorkspace(ctx) {
|
|
|
8342
8421
|
};
|
|
8343
8422
|
const isMonorepo = ctx.config.structure === "monorepo";
|
|
8344
8423
|
const existing = ctx.read(FILE_PATH);
|
|
8345
|
-
if (existing === void 0) {
|
|
8424
|
+
if (existing === void 0 || existing.trim() === "") {
|
|
8346
8425
|
ctx.write(FILE_PATH, isMonorepo ? FRESH_MONOREPO : FRESH_SINGLE);
|
|
8347
8426
|
return {
|
|
8348
8427
|
filePath: FILE_PATH,
|
|
@@ -8421,8 +8500,8 @@ function writeEmbedded(ctx, file) {
|
|
|
8421
8500
|
}
|
|
8422
8501
|
const NOT_BUILT = "run `pnpm build` from the repo root";
|
|
8423
8502
|
async function generateClaudeHooks(ctx, embedded = {
|
|
8424
|
-
hook: "/**\n * bst-hook -- Claude Code PreToolUse hook\n * @bensandee/claude-hooks\n * https://github.com/bensandee/tooling\n *\n * Auto-generated by @bensandee/tooling repo:sync -- do not edit manually.\n */\nimport * as fs from \"node:fs\";\nimport { existsSync, readFileSync, readdirSync, realpathSync, statSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { execFileSync } from \"node:child_process\";\n\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/util.js\nfunction joinValues(array, separator = \"|\") {\n return array.map((val) => stringifyPrimitive(val)).join(separator);\n}\nfunction jsonStringifyReplacer(_, value) {\n if (typeof value === \"bigint\") return value.toString();\n return value;\n}\nvar Cached = class {\n constructor(getter) {\n this._getter = getter;\n this._value = void 0;\n }\n get value() {\n const getter = this._getter;\n if (getter !== void 0) {\n this._value = getter();\n this._getter = void 0;\n }\n return this._value;\n }\n};\nfunction cached(getter) {\n return new Cached(getter);\n}\nfunction nullish(input) {\n return input === null || input === void 0;\n}\nfunction cleanRegex(source) {\n const start = source.startsWith(\"^\") ? 1 : 0;\n const end = source.endsWith(\"$\") ? source.length - 1 : source.length;\n return source.slice(start, end);\n}\nfunction assignProp(target, prop, value) {\n Object.defineProperty(target, prop, {\n value,\n writable: true,\n enumerable: true,\n configurable: true\n });\n}\nconst captureStackTrace = \"captureStackTrace\" in Error ? Error.captureStackTrace : (..._args) => {};\nfunction isObject(data) {\n return typeof data === \"object\" && data !== null && !Array.isArray(data);\n}\nfunction isPlainObject(o) {\n if (isObject(o) === false) return false;\n const ctor = o.constructor;\n if (ctor === void 0) return true;\n if (typeof ctor !== \"function\") return true;\n const prot = ctor.prototype;\n if (isObject(prot) === false) return false;\n if (Object.prototype.hasOwnProperty.call(prot, \"isPrototypeOf\") === false) return false;\n return true;\n}\nfunction shallowClone(o) {\n if (isPlainObject(o)) return { ...o };\n if (Array.isArray(o)) return [...o];\n if (o instanceof Map) return new Map(o);\n if (o instanceof Set) return new Set(o);\n return o;\n}\nfunction clone(inst, def, params) {\n const cl = new inst._zod.constr(def ?? inst._zod.def);\n if (!def || params?.parent) cl._zod.parent = inst;\n return cl;\n}\nfunction normalizeParams(_params) {\n const params = _params;\n if (!params) return {};\n if (typeof params === \"string\") return { error: () => params };\n if (params?.message !== void 0) {\n if (params?.error !== void 0) throw new Error(\"Cannot specify both `message` and `error` params\");\n params.error = params.message;\n }\n delete params.message;\n if (typeof params.error === \"string\") return {\n ...params,\n error: () => params.error\n };\n return params;\n}\nfunction stringifyPrimitive(value) {\n if (typeof value === \"bigint\") return value.toString() + \"n\";\n if (typeof value === \"string\") return `\"${value}\"`;\n return `${value}`;\n}\nfunction optionalKeys(shape) {\n return Object.keys(shape).filter((k) => {\n return shape[k]._zod.optin !== void 0 && shape[k]._zod.optout === \"optional\";\n });\n}\nfunction aborted(x, startIndex = 0) {\n if (x.aborted === true) return true;\n for (let i = startIndex; i < x.issues.length; i++) if (x.issues[i]?.continue !== true) return true;\n return false;\n}\nfunction explicitlyAborted(x, startIndex = 0) {\n if (x.aborted === true) return true;\n for (let i = startIndex; i < x.issues.length; i++) if (x.issues[i]?.continue === false) return true;\n return false;\n}\nfunction prefixIssues(path, issues) {\n return issues.map((iss) => {\n var _a;\n (_a = iss).path ?? (_a.path = []);\n iss.path.unshift(path);\n return iss;\n });\n}\nfunction unwrapMessage(message) {\n return typeof message === \"string\" ? message : message?.message;\n}\nfunction attachSchema(issues, start, inst) {\n var _a;\n for (let i = start; i < issues.length; i++) (_a = issues[i]).schema ?? (_a.schema = inst);\n}\nfunction finalizeIssue(iss, ctx, config) {\n var _a;\n const traits = iss.inst?._zod?.traits;\n if (traits?.has(\"$ZodType\")) {\n if (traits.has(\"$ZodCheck\")) (_a = iss).schema ?? (_a.schema = iss.inst);\n else iss.schema = iss.inst;\n }\n const schemaError = iss.schema !== iss.inst ? iss.schema?._zod.def?.error : void 0;\n const message = iss.message ? iss.message : unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ?? unwrapMessage(schemaError?.(iss)) ?? unwrapMessage(ctx?.error?.(iss)) ?? unwrapMessage(config.customError?.(iss)) ?? unwrapMessage(config.localeError?.(iss)) ?? \"Invalid input\";\n const full = {};\n for (const k of Object.keys(iss)) {\n if (k === \"inst\" || k === \"schema\" || k === \"continue\" || k === \"input\" || k === \"__proto__\") continue;\n full[k] = iss[k];\n }\n full.path ?? (full.path = []);\n full.message = message;\n if (ctx?.reportInput) full.input = iss.input;\n return full;\n}\nconst highSurrogate = /[\\uD800-\\uDBFF]/;\nfunction codePointLength(str) {\n const units = str.length;\n if (!highSurrogate.test(str)) return units;\n let count = units;\n for (let i = 0; i < units - 1; i++) if ((str.charCodeAt(i) & 64512) === 55296 && (str.charCodeAt(i + 1) & 64512) === 56320) {\n count--;\n i++;\n }\n return count;\n}\nfunction getLengthableOrigin(input) {\n if (Array.isArray(input)) return \"array\";\n if (typeof input === \"string\") return \"string\";\n return \"unknown\";\n}\nfunction parsedType(data) {\n const t = typeof data;\n switch (t) {\n case \"number\": return Number.isNaN(data) ? \"nan\" : \"number\";\n case \"object\": {\n if (data === null) return \"null\";\n if (Array.isArray(data)) return \"array\";\n const obj = data;\n if (obj && Object.getPrototypeOf(obj) !== Object.prototype && \"constructor\" in obj && obj.constructor) return obj.constructor.name;\n }\n }\n return t;\n}\nfunction members(proto, table) {\n for (const key in table) {\n const desc = Object.getOwnPropertyDescriptor(table, key);\n if (desc.get) Object.defineProperty(proto, key, {\n ...desc,\n enumerable: false\n });\n else defineBound(proto, key, desc.value);\n }\n}\nfunction own(inst, key, value, enumerable = true) {\n Object.defineProperty(inst, key, {\n configurable: true,\n writable: true,\n enumerable,\n value\n });\n return value;\n}\nfunction hide(inst, key, value) {\n return own(inst, key, value, false);\n}\nfunction defineBound(proto, key, fn) {\n Object.defineProperty(proto, key, {\n configurable: true,\n get() {\n return this == null ? fn : own(this, key, fn.bind(this));\n },\n set(value) {\n own(this, key, value);\n }\n });\n}\nfunction claim(inst, sentinel) {\n const proto = Object.getPrototypeOf(inst);\n return sentinel in proto ? void 0 : proto;\n}\nlet installing;\nlet broke = false;\nconst breaker = {\n configurable: true,\n get() {\n broke = true;\n }\n};\nfunction defineLazyInternal(inst, key, compute) {\n const proto = Object.getPrototypeOf(inst._zod);\n if (key in proto && installing !== inst._zod) {\n installing = void 0;\n return;\n }\n installing = inst._zod;\n Object.defineProperty(proto, key, {\n configurable: true,\n get() {\n Object.defineProperty(this, key, breaker);\n const outer = broke;\n broke = false;\n try {\n const value = compute(this);\n if (broke) delete this[key];\n else Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n value\n });\n broke = broke || outer;\n return value;\n } catch (err) {\n delete this[key];\n broke = broke || outer;\n throw err;\n }\n },\n set(value) {\n Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n value\n });\n }\n });\n}\nfunction installLazyProp(inst, key, make, enumerable) {\n const proto = claim(inst, key);\n if (!proto) return;\n Object.defineProperty(proto, key, {\n configurable: true,\n get() {\n const desc = {\n configurable: true,\n writable: true,\n enumerable,\n value: void 0\n };\n Object.defineProperty(this, key, desc);\n desc.value = make(this);\n Object.defineProperty(this, key, desc);\n return desc.value;\n },\n set(value) {\n Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n enumerable,\n value\n });\n }\n });\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/core.js\nvar _a;\nconst _zodDesc = {\n value: void 0,\n enumerable: false\n};\nlet _E = \"captureStackTrace\" in Error ? Error : null;\nfunction newError(Definition) {\n const E = _E;\n if (E) {\n const saved = E.stackTraceLimit;\n if (typeof saved === \"number\") {\n try {\n E.stackTraceLimit = 0;\n } catch {\n _E = null;\n return new Definition();\n }\n try {\n return new Definition();\n } finally {\n E.stackTraceLimit = saved;\n }\n }\n }\n return new Definition();\n}\nfunction $constructor(name, initializer, proto, params) {\n const zodProto = {};\n function Internals(def) {\n this.def = def;\n this.constr = _;\n this.traits = /* @__PURE__ */ new Set();\n }\n Internals.prototype = zodProto;\n const protoMembers = proto;\n const initialized = protoMembers && /* @__PURE__ */ new WeakSet();\n function init(inst, def) {\n if (!inst._zod) {\n _zodDesc.value = new Internals(def);\n try {\n Object.defineProperty(inst, \"_zod\", _zodDesc);\n } finally {\n _zodDesc.value = void 0;\n }\n } else if (inst._zod.traits.has(name)) return;\n inst._zod.traits.add(name);\n initializer(inst, def);\n if (initialized) {\n const own = Object.getPrototypeOf(inst);\n const ctorProto = inst._zod.constr.prototype;\n let up = own;\n while (up && up !== ctorProto) up = Object.getPrototypeOf(up);\n const target = up ?? own;\n if (!initialized.has(target)) {\n initialized.add(target);\n members(target, protoMembers);\n }\n }\n const proto = _.prototype;\n for (const k in proto) {\n if (!Object.prototype.hasOwnProperty.call(proto, k)) continue;\n if (!(k in inst)) inst[k] = proto[k].bind(inst);\n }\n }\n const Parent = params?.Parent ?? Object;\n class Definition extends Parent {}\n Object.defineProperty(Definition, \"name\", { value: name });\n function _(def) {\n const inst = params?.Parent ? newError(Definition) : this;\n init(inst, def);\n const deferred = inst._zod.deferred;\n if (deferred) {\n for (const fn of deferred) fn();\n inst._zod.deferred = void 0;\n }\n const pp = globalThis.__zod_globalConfig?.postProcessor;\n if (pp) pp(inst);\n return inst;\n }\n Object.defineProperty(_, \"init\", { value: init });\n Object.defineProperty(_, Symbol.hasInstance, { value: (inst) => {\n if (params?.Parent && inst instanceof params.Parent) return true;\n return inst?._zod?.traits?.has(name);\n } });\n Object.defineProperty(_, \"name\", { value: name });\n return _;\n}\nvar $ZodAsyncError = class extends Error {\n constructor() {\n super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`);\n }\n};\n(_a = globalThis).__zod_globalConfig ?? (_a.__zod_globalConfig = {});\nconst globalConfig = globalThis.__zod_globalConfig;\nfunction config(newConfig) {\n if (newConfig) Object.assign(globalConfig, newConfig);\n return globalConfig;\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/errors.js\nfunction _getMessage() {\n const internals = this._zod;\n internals.message ?? (internals.message = JSON.stringify(internals.def, jsonStringifyReplacer, 2));\n return internals.message;\n}\nfunction _setMessage(value) {\n this._zod.message = value;\n}\nconst _messageDesc = {\n get: _getMessage,\n set: _setMessage,\n enumerable: true,\n configurable: true\n};\nconst _issuesDesc = {\n value: void 0,\n enumerable: false\n};\nconst _installedToString = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]);\nconst initializer = (inst, def) => {\n inst.name = \"$ZodError\";\n _issuesDesc.value = def;\n Object.defineProperty(inst, \"issues\", _issuesDesc);\n _issuesDesc.value = void 0;\n Object.defineProperty(inst, \"message\", _messageDesc);\n const proto = Object.getPrototypeOf(inst);\n if (!_installedToString.has(proto)) {\n _installedToString.add(proto);\n Object.defineProperty(proto, \"toString\", {\n configurable: true,\n enumerable: false,\n get() {\n const value = () => this.message;\n Object.defineProperty(this, \"toString\", {\n value,\n configurable: true,\n writable: true\n });\n return value;\n },\n set(value) {\n Object.defineProperty(this, \"toString\", {\n value,\n configurable: true,\n writable: true\n });\n }\n });\n }\n};\nconst $ZodError = $constructor(\"$ZodError\", initializer);\nconst $ZodRealError = $constructor(\"$ZodError\", initializer, void 0, { Parent: Error });\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/parse.js\nconst _parse = (_Err) => {\n const fn = (schema, value, _ctx, _params) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: false\n } : { async: false };\n const result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) throw new $ZodAsyncError();\n if (result.issues.length) {\n const e = new ((_params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));\n captureStackTrace(e, _params?.callee ?? fn);\n throw e;\n }\n return result.value;\n };\n return fn;\n};\nconst parse = /* @__PURE__*/ _parse($ZodRealError);\nconst _parseAsync = (_Err) => {\n const fn = async (schema, value, _ctx, params) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: true\n } : { async: true };\n let result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) result = await result;\n if (result.issues.length) {\n const e = new ((params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));\n captureStackTrace(e, params?.callee ?? fn);\n throw e;\n }\n return result.value;\n };\n return fn;\n};\nconst parseAsync = /* @__PURE__*/ _parseAsync($ZodRealError);\nconst _safeParse = (_Err) => (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: false\n } : { async: false };\n const result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) throw new $ZodAsyncError();\n return result.issues.length ? failure(_Err, result.issues, ctx) : {\n success: true,\n data: result.value\n };\n};\nconst safeParse = /* @__PURE__*/ _safeParse($ZodRealError);\nfunction failure(Err, issues, ctx) {\n let error;\n return {\n success: false,\n get error() {\n if (!error) {\n error = new Err(issues.map((iss) => finalizeIssue(iss, ctx, config())));\n issues = void 0;\n ctx = void 0;\n }\n return error;\n },\n set error(e) {\n error = e;\n issues = void 0;\n ctx = void 0;\n }\n };\n}\nconst _safeParseAsync = (_Err) => async (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: true\n } : { async: true };\n let result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) result = await result;\n return result.issues.length ? failure(_Err, result.issues, ctx) : {\n success: true,\n data: result.value\n };\n};\nconst safeParseAsync = /* @__PURE__*/ _safeParseAsync($ZodRealError);\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/regexes.js\nconst anyString = /^[\\s\\S]{0,}$/;\nconst number = /^-?\\d+(?:\\.\\d+)?$/;\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/checks.js\nconst $ZodCheck = /*@__PURE__*/ $constructor(\"$ZodCheck\", (inst, def) => {\n var _a;\n inst._zod ?? (inst._zod = {});\n inst._zod.def = def;\n (_a = inst._zod).onattach ?? (_a.onattach = []);\n});\nconst _whenHasLength = (payload) => {\n const val = payload.value;\n return !nullish(val) && val.length !== void 0;\n};\nconst $ZodCheckMinLength = /*@__PURE__*/ $constructor(\"$ZodCheckMinLength\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = _whenHasLength);\n inst._zod.check = (payload) => {\n const input = payload.value;\n const units = input.length;\n if ((typeof input === \"string\" && units >= def.minimum && units < def.minimum * 2 ? codePointLength(input) : units) >= def.minimum) return;\n const origin = getLengthableOrigin(input);\n payload.issues.push({\n origin,\n code: \"too_small\",\n minimum: def.minimum,\n inclusive: true,\n input,\n inst,\n continue: !def.abort\n });\n };\n});\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/versions.js\nconst version = {\n major: 4,\n minor: 6,\n patch: 5\n};\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/schemas.js\nconst $ZodType = /*@__PURE__*/ $constructor(\"$ZodType\", (inst, def) => {\n var _a;\n inst ?? (inst = {});\n inst._zod.def = def;\n inst._zod.bag = inst._zod.bag || {};\n inst._zod.version = version;\n const defChecks = inst._zod.def.checks;\n const checks = inst._zod.traits.has(\"$ZodCheck\") ? [inst, ...defChecks ?? []] : defChecks?.length ? [...defChecks] : [];\n for (const ch of checks) for (const fn of ch._zod.onattach) fn(inst);\n if (checks.length === 0) {\n (_a = inst._zod).deferred ?? (_a.deferred = []);\n inst._zod.deferred?.push(() => {\n inst._zod.run = inst._zod.parse;\n });\n } else {\n const runChecks = (payload, checks, ctx) => {\n if (payload.memo) return payload;\n let isAborted = aborted(payload);\n let asyncResult;\n for (const ch of checks) {\n if (ch._zod.def.when) {\n if (explicitlyAborted(payload)) continue;\n if (!ch._zod.def.when(payload)) continue;\n } else if (isAborted) continue;\n const currLen = payload.issues.length;\n const _ = ch._zod.check(payload);\n if (_ instanceof Promise && ctx?.async === false) throw new $ZodAsyncError();\n if (asyncResult || _ instanceof Promise) asyncResult = (asyncResult ?? Promise.resolve()).then(async () => {\n await _;\n if (payload.issues.length === currLen) return;\n attachSchema(payload.issues, currLen, inst);\n if (!isAborted) isAborted = aborted(payload, currLen);\n });\n else {\n if (payload.issues.length === currLen) continue;\n attachSchema(payload.issues, currLen, inst);\n if (!isAborted) isAborted = aborted(payload, currLen);\n }\n }\n if (asyncResult) return asyncResult.then(() => {\n return payload;\n });\n return payload;\n };\n const handleCanaryResult = (canary, payload, ctx) => {\n if (aborted(canary)) {\n canary.aborted = true;\n return canary;\n }\n const checkResult = runChecks(payload, checks, ctx);\n if (checkResult instanceof Promise) {\n if (ctx.async === false) throw new $ZodAsyncError();\n return checkResult.then((checkResult) => inst._zod.parse(checkResult, ctx));\n }\n return inst._zod.parse(checkResult, ctx);\n };\n inst._zod.run = (payload, ctx) => {\n if (ctx.skipChecks) return inst._zod.parse(payload, ctx);\n if (ctx.direction === \"backward\") {\n const canary = inst._zod.parse({\n value: payload.value,\n issues: []\n }, {\n ...ctx,\n skipChecks: true\n });\n if (canary instanceof Promise) return canary.then((canary) => {\n return handleCanaryResult(canary, payload, ctx);\n });\n return handleCanaryResult(canary, payload, ctx);\n }\n const result = inst._zod.parse(payload, ctx);\n if (result instanceof Promise) {\n if (ctx.async === false) throw new $ZodAsyncError();\n return result.then((result) => runChecks(result, checks, ctx));\n }\n return runChecks(result, checks, ctx);\n };\n }\n}, {\n get \"~standard\"() {\n return hide(this, \"~standard\", standardProps(this));\n },\n set \"~standard\"(value) {\n own(this, \"~standard\", value);\n }\n});\nconst toStandardResult = (r, ctx) => r.issues.length ? { issues: r.issues.map((iss) => finalizeIssue(iss, ctx, config())) } : { value: r.value };\nasync function validateAsync(inst, value) {\n const ctx = { async: true };\n return toStandardResult(await inst._zod.run({\n value,\n issues: []\n }, ctx), ctx);\n}\nfunction standardProps(inst) {\n return {\n validate: (value) => {\n const ctx = { async: false };\n try {\n const r = inst._zod.run({\n value,\n issues: []\n }, ctx);\n if (!(r instanceof Promise)) return toStandardResult(r, ctx);\n } catch (_) {}\n return validateAsync(inst, value);\n },\n vendor: \"zod\",\n version: 1\n };\n}\nconst $ZodString = /*@__PURE__*/ $constructor(\"$ZodString\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.pattern = def.pattern ?? anyString;\n inst._zod.parse = (payload, _) => {\n if (def.coerce) try {\n payload.value = String(payload.value);\n } catch (_) {}\n if (typeof payload.value === \"string\") return payload;\n payload.issues.push({\n expected: \"string\",\n code: \"invalid_type\",\n input: payload.value,\n inst\n });\n return payload;\n };\n});\nconst $ZodUnknown = /*@__PURE__*/ $constructor(\"$ZodUnknown\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload) => payload;\n});\nfunction handleArrayResult(result, final, index) {\n if (result.issues.length) final.issues.push(...prefixIssues(index, result.issues));\n final.value[index] = result.value;\n}\nconst $ZodArray = /*@__PURE__*/ $constructor(\"$ZodArray\", (inst, def) => {\n $ZodType.init(inst, def);\n const memo = globalConfig.memoizer;\n memo?.attach(inst);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!Array.isArray(input)) {\n payload.issues.push({\n expected: \"array\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n payload.value = memo ? memo.alloc(inst, payload, Array(input.length), ctx) : Array(input.length);\n const proms = [];\n const abortEarly = ctx?.abortEarly;\n for (let i = 0; i < input.length; i++) {\n const item = input[i];\n const result = def.element._zod.run({\n value: item,\n issues: []\n }, ctx);\n if (result instanceof Promise) proms.push(result.then((result) => handleArrayResult(result, payload, i)));\n else {\n handleArrayResult(result, payload, i);\n if (abortEarly && result.issues.length !== 0 && aborted(result)) break;\n }\n }\n if (proms.length) return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handlePropertyResult(result, final, key, input, optin, optout) {\n const isPresent = key in input;\n const isOptionalOut = optout === \"optional\";\n if (!isPresent && isOptionalOut && optin === \"optional\") return;\n if (result.issues.length) {\n if (optin !== void 0 && isOptionalOut && !isPresent) return;\n final.issues.push(...prefixIssues(key, result.issues));\n }\n if (!isPresent && optin === void 0) {\n if (!result.issues.length) final.issues.push({\n code: \"invalid_type\",\n expected: \"nonoptional\",\n input: void 0,\n path: [key]\n });\n return;\n }\n if (result.value === void 0) {\n if (isPresent || optin === \"defaulted\" && !isOptionalOut) final.value[key] = void 0;\n } else final.value[key] = result.value;\n}\nconst NO_SYMBOL_KEYS = [];\nfunction normalizeDef(def) {\n const keys = Object.keys(def.shape);\n const ownSymbols = Object.getOwnPropertySymbols(def.shape);\n const symbolKeys = ownSymbols.length ? ownSymbols : NO_SYMBOL_KEYS;\n const allKeys = symbolKeys.length ? [...keys, ...symbolKeys] : keys;\n for (const k of allKeys) if (!def.shape?.[k]?._zod?.traits?.has(\"$ZodType\")) throw new Error(`Invalid element at key \"${String(k)}\": expected a Zod schema`);\n const okeys = optionalKeys(def.shape);\n return {\n ...def,\n allKeys,\n symbolKeys,\n keySet: new Set(keys),\n numKeys: keys.length,\n optionalKeys: new Set(okeys)\n };\n}\nfunction handleCatchall(proms, input, payload, ctx, def, inst, abortEarly) {\n const unrecognized = [];\n const keySet = def.keySet;\n const _catchall = def.catchall._zod;\n const t = _catchall.def.type;\n const optin = _catchall.optin;\n const optout = _catchall.optout;\n let seen = 0;\n for (const key in input) {\n if (abortEarly && payload.issues.length !== seen) {\n if (aborted(payload, seen)) break;\n seen = payload.issues.length;\n }\n if (keySet.has(key)) continue;\n if (key === \"__proto__\") {\n if (t === \"never\") unrecognized.push(key);\n continue;\n }\n if (t === \"never\") {\n unrecognized.push(key);\n continue;\n }\n const r = _catchall.run({\n value: input[key],\n issues: []\n }, ctx);\n if (r instanceof Promise) proms.push(r.then((r) => handlePropertyResult(r, payload, key, input, optin, optout)));\n else handlePropertyResult(r, payload, key, input, optin, optout);\n }\n if (unrecognized.length) payload.issues.push({\n code: \"unrecognized_keys\",\n keys: unrecognized,\n input,\n inst,\n continue: true\n });\n if (!proms.length) return payload;\n return Promise.all(proms).then(() => {\n return payload;\n });\n}\nconst $ZodObject = /*@__PURE__*/ $constructor(\"$ZodObject\", (inst, def) => {\n $ZodType.init(inst, def);\n const desc = Object.getOwnPropertyDescriptor(def, \"shape\");\n const sh = desc?.get ? desc.get.raw : def.shape ?? {};\n if (sh) {\n const get = () => {\n const newSh = { ...sh };\n Object.defineProperty(def, \"shape\", { value: newSh });\n get.raw = newSh;\n return newSh;\n };\n get.raw = sh;\n Object.defineProperty(def, \"shape\", { get });\n }\n const _normalized = cached(() => normalizeDef(def));\n defineLazyInternal(inst, \"propValues\", (zod) => {\n const shape = zod.def.shape;\n const propValues = {};\n for (const key in shape) {\n const field = shape[key]._zod;\n if (field.values) {\n if (!Object.prototype.hasOwnProperty.call(propValues, key)) assignProp(propValues, key, /* @__PURE__ */ new Set());\n for (const v of field.values) propValues[key].add(v);\n if (field.optin !== void 0) propValues[key].add(void 0);\n }\n }\n return propValues;\n });\n const isObject$2 = isObject;\n const catchall = def.catchall;\n let value;\n const memo = globalConfig.memoizer;\n memo?.attach(inst);\n inst._zod.parse = (payload, ctx) => {\n value ?? (value = _normalized.value);\n const input = payload.value;\n if (!isObject$2(input)) {\n payload.issues.push({\n expected: \"object\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};\n const proms = [];\n const shape = value.shape;\n const abortEarly = ctx?.abortEarly;\n let seen = payload.issues.length;\n for (const key of value.allKeys) {\n if (abortEarly && payload.issues.length !== seen) {\n if (aborted(payload, seen)) break;\n seen = payload.issues.length;\n }\n if (key === \"__proto__\") continue;\n const el = shape[key];\n const optin = el._zod.optin;\n const optout = el._zod.optout;\n const r = el._zod.run({\n value: input[key],\n issues: []\n }, ctx);\n if (r instanceof Promise) proms.push(r.then((r) => handlePropertyResult(r, payload, key, input, optin, optout)));\n else handlePropertyResult(r, payload, key, input, optin, optout);\n }\n if (!catchall) return proms.length ? Promise.all(proms).then(() => payload) : payload;\n return handleCatchall(proms, input, payload, ctx, _normalized.value, inst, abortEarly === true);\n };\n});\nconst $ZodRecord = /*@__PURE__*/ $constructor(\"$ZodRecord\", (inst, def) => {\n $ZodType.init(inst, def);\n const memo = globalConfig.memoizer;\n memo?.attach(inst);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!isPlainObject(input)) {\n payload.issues.push({\n expected: \"record\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n const proms = [];\n const values = def.keyType._zod.values;\n if (values && !def.partial) {\n payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};\n const recordKeys = /* @__PURE__ */ new Set();\n for (const key of values) if (typeof key === \"string\" || typeof key === \"number\" || typeof key === \"symbol\") {\n recordKeys.add(typeof key === \"number\" ? key.toString() : key);\n if (key === \"__proto__\") continue;\n const keyResult = def.keyType._zod.run({\n value: key,\n issues: []\n }, ctx);\n if (keyResult instanceof Promise) throw new Error(\"Async schemas not supported in object keys currently\");\n if (keyResult.issues.length) {\n payload.issues.push({\n code: \"invalid_key\",\n origin: \"record\",\n issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),\n input: key,\n path: [key],\n inst\n });\n continue;\n }\n const outKey = keyResult.value;\n if (outKey === \"__proto__\") continue;\n const result = def.valueType._zod.run({\n value: input[key],\n issues: []\n }, ctx);\n if (result instanceof Promise) proms.push(result.then((result) => {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }));\n else {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }\n }\n let unrecognized;\n for (const key in input) if (!recordKeys.has(key)) {\n if (def.mode === \"loose\") {\n if (key === \"__proto__\") continue;\n payload.value[key] = input[key];\n } else {\n unrecognized = unrecognized ?? [];\n unrecognized.push(key);\n }\n }\n if (unrecognized && unrecognized.length > 0) payload.issues.push({\n code: \"unrecognized_keys\",\n input,\n inst,\n keys: unrecognized,\n continue: true\n });\n } else {\n payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};\n let unrecognized;\n for (const key of Reflect.ownKeys(input)) {\n if (key === \"__proto__\") continue;\n if (!Object.prototype.propertyIsEnumerable.call(input, key)) continue;\n let keyResult = def.keyType._zod.run({\n value: key,\n issues: []\n }, ctx);\n if (keyResult instanceof Promise) throw new Error(\"Async schemas not supported in object keys currently\");\n if (typeof key === \"string\" && number.test(key) && keyResult.issues.length) {\n const retryResult = def.keyType._zod.run({\n value: Number(key),\n issues: []\n }, ctx);\n if (retryResult instanceof Promise) throw new Error(\"Async schemas not supported in object keys currently\");\n if (retryResult.issues.length === 0) keyResult = retryResult;\n }\n if (keyResult.issues.length) {\n if (def.mode === \"loose\") payload.value[key] = input[key];\n else if (values) {\n unrecognized = unrecognized ?? [];\n unrecognized.push(key);\n } else payload.issues.push({\n code: \"invalid_key\",\n origin: \"record\",\n issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),\n input: key,\n path: [key],\n inst\n });\n continue;\n }\n const outKey = keyResult.value;\n if (outKey === \"__proto__\") continue;\n const result = def.valueType._zod.run({\n value: input[key],\n issues: []\n }, ctx);\n if (result instanceof Promise) proms.push(result.then((result) => {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }));\n else {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }\n }\n if (unrecognized && unrecognized.length > 0) payload.issues.push({\n code: \"unrecognized_keys\",\n input,\n inst,\n keys: unrecognized,\n continue: true\n });\n }\n if (proms.length) return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handleOptionalResult(payload, result) {\n payload.value = result.issues.length ? void 0 : result.value;\n return payload;\n}\nconst $ZodOptional = /*@__PURE__*/ $constructor(\"$ZodOptional\", (inst, def) => {\n $ZodType.init(inst, def);\n defineLazyInternal(inst, \"optin\", (zod) => zod.def.innerType._zod.optin === \"defaulted\" ? \"defaulted\" : \"optional\");\n inst._zod.optout = \"optional\";\n defineLazyInternal(inst, \"values\", (zod) => {\n const values = zod.def.innerType._zod.values;\n return values ? /* @__PURE__ */ new Set([...values, void 0]) : void 0;\n });\n defineLazyInternal(inst, \"pattern\", (zod) => {\n const pattern = zod.def.innerType._zod.pattern;\n return pattern ? new RegExp(`^(${cleanRegex(pattern.source)})?$`) : void 0;\n });\n inst._zod.parse = (payload, ctx) => {\n if (payload.value === void 0) {\n if (def.innerType._zod.optin !== \"defaulted\") return payload;\n const result = def.innerType._zod.run({\n value: payload.value,\n issues: []\n }, ctx);\n if (result instanceof Promise) return result.then((result) => handleOptionalResult(payload, result));\n return handleOptionalResult(payload, result);\n }\n return def.innerType._zod.run(payload, ctx);\n };\n});\nconst $ZodDefault = /*@__PURE__*/ $constructor(\"$ZodDefault\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.optin = \"defaulted\";\n defineLazyInternal(inst, \"values\", (zod) => zod.def.innerType._zod.values);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") return def.innerType._zod.run(payload, ctx);\n if (payload.value === void 0) {\n payload.value = def.defaultValue;\n return payload;\n }\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) return result.then((result) => handleDefaultResult(result, def));\n return handleDefaultResult(result, def);\n };\n});\nfunction handleDefaultResult(payload, def) {\n if (payload.value === void 0) payload.value = def.defaultValue;\n return payload;\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/locales/en.js\nconst error = () => {\n const Sizable = {\n string: {\n unit: \"characters\",\n verb: \"to have\"\n },\n file: {\n unit: \"bytes\",\n verb: \"to have\"\n },\n array: {\n unit: \"items\",\n verb: \"to have\"\n },\n set: {\n unit: \"items\",\n verb: \"to have\"\n },\n map: {\n unit: \"entries\",\n verb: \"to have\"\n }\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const FormatDictionary = {\n regex: \"input\",\n email: \"email address\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO datetime\",\n date: \"ISO date\",\n time: \"ISO time\",\n duration: \"ISO duration\",\n ipv4: \"IPv4 address\",\n ipv6: \"IPv6 address\",\n mac: \"MAC address\",\n cidrv4: \"IPv4 range\",\n cidrv6: \"IPv6 range\",\n base64: \"base64-encoded string\",\n base64url: \"base64url-encoded string\",\n json_string: \"JSON string\",\n e164: \"E.164 number\",\n currency_code: \"currency code\",\n credit_card: \"credit card number\",\n iban: \"IBAN\",\n jwt: \"JWT\",\n template_literal: \"input\"\n };\n const TypeDictionary = { nan: \"NaN\" };\n function getTypeName(type, input) {\n if (type === \"number\" && typeof input === \"number\" && !Number.isFinite(input)) return String(input);\n return TypeDictionary[type] ?? type;\n }\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\": return `Invalid input: expected ${getTypeName(issue.expected)}, received ${getTypeName(parsedType(issue.input), issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1) return `Invalid input: expected ${stringifyPrimitive(issue.values[0])}`;\n return `Invalid option: expected one of ${joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.exact ? \"exactly \" : issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) return `Too big: expected ${issue.origin ?? \"value\"} to have ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elements\"}`;\n return `Too big: expected ${issue.origin ?? \"value\"} to be ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.exact ? \"exactly \" : issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) return `Too small: expected ${issue.origin} to have ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n return `Too small: expected ${issue.origin} to be ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") return `Invalid string: must start with \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\") return `Invalid string: must end with \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\") return `Invalid string: must include \"${_issue.includes}\"`;\n if (_issue.format === \"regex\") return `Invalid string: must match pattern ${_issue.pattern}`;\n return `Invalid ${FormatDictionary[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\": return `Invalid number: must be a multiple of ${issue.divisor}`;\n case \"unrecognized_keys\": return `Unrecognized key${issue.keys.length > 1 ? \"s\" : \"\"}: ${joinValues(issue.keys, \", \")}`;\n case \"invalid_key\": return `Invalid key in ${issue.origin}`;\n case \"invalid_union\":\n if (issue.options && Array.isArray(issue.options) && issue.options.length > 0) return `Invalid discriminator value. Expected ${issue.options.map((o) => `'${o}'`).join(\" | \")}`;\n if (issue.inclusive === false) return \"Invalid input: more than one option matched\";\n return \"Invalid input\";\n case \"invalid_element\": return `Invalid value in ${issue.origin}`;\n default: return `Invalid input`;\n }\n };\n};\nfunction en_default() {\n return { localeError: error() };\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/api.js\nfunction snapshotChecks(def) {\n if (def.checks) def.checks = [...def.checks];\n return def;\n}\n// @__NO_SIDE_EFFECTS__\nfunction _string(Class, params) {\n return new Class(snapshotChecks({\n type: \"string\",\n ...normalizeParams(params)\n }));\n}\n// @__NO_SIDE_EFFECTS__\nfunction _unknown(Class) {\n return new Class({ type: \"unknown\" });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _minLength(minimum, params) {\n return new $ZodCheckMinLength({\n check: \"min_length\",\n ...normalizeParams(params),\n minimum\n });\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/mini/schemas.js\nconst ZodMiniType = /*@__PURE__*/ $constructor(\"ZodMiniType\", (inst, def) => {\n if (!inst._zod) throw new Error(\"Uninitialized schema in ZodMiniType.\");\n $ZodType.init(inst, def);\n inst.def = def;\n inst.type = def.type;\n}, {\n get with() {\n return this.check;\n },\n set with(value) {\n own(this, \"with\", value);\n },\n parse(data, params) {\n return parse(this, data, params, { callee: this.parse });\n },\n parseAsync(data, params) {\n return parseAsync(this, data, params, { callee: this.parseAsync });\n },\n safeParse(data, params) {\n return safeParse(this, data, params);\n },\n safeParseAsync(data, params) {\n return safeParseAsync(this, data, params);\n },\n check(...checks) {\n const def = this.def;\n return this.clone({\n ...def,\n checks: [...def.checks ?? [], ...checks.map((ch) => typeof ch === \"function\" ? { _zod: {\n check: ch,\n def: { check: \"custom\" },\n onattach: []\n } } : ch)]\n }, { parent: true });\n },\n clone(_def, params) {\n return clone(this, _def, params);\n },\n brand() {\n return this;\n },\n register(reg, meta) {\n reg.add(this, meta);\n return this;\n },\n apply(fn, ...args) {\n return args.length === 0 ? fn(this) : fn(this, ...args);\n }\n});\nconst ZodMiniString = /*@__PURE__*/ $constructor(\"ZodMiniString\", (inst, def) => {\n $ZodString.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction string(params) {\n return _string(ZodMiniString, params);\n}\nconst ZodMiniUnknown = /*@__PURE__*/ $constructor(\"ZodMiniUnknown\", (inst, def) => {\n $ZodUnknown.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction unknown() {\n return _unknown(ZodMiniUnknown);\n}\nconst ZodMiniArray = /*@__PURE__*/ $constructor(\"ZodMiniArray\", (inst, def) => {\n $ZodArray.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction array(element, params) {\n return new ZodMiniArray({\n type: \"array\",\n element,\n ...normalizeParams(params)\n });\n}\nconst ZodMiniObject = /*@__PURE__*/ $constructor(\"ZodMiniObject\", (inst, def) => {\n $ZodObject.init(inst, def);\n ZodMiniType.init(inst, def);\n installLazyProp(inst, \"shape\", (self) => self._zod.def.shape, false);\n});\n// @__NO_SIDE_EFFECTS__\nfunction object(shape, params) {\n const def = {\n type: \"object\",\n shape: shape ?? {},\n ...normalizeParams(params)\n };\n return new ZodMiniObject(def);\n}\nconst ZodMiniRecord = /*@__PURE__*/ $constructor(\"ZodMiniRecord\", (inst, def) => {\n $ZodRecord.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction record(keyType, valueType, params) {\n if (!valueType || !valueType._zod) return new ZodMiniRecord({\n type: \"record\",\n keyType: /* @__PURE__ */ string(),\n valueType: keyType,\n ...normalizeParams(valueType)\n });\n return new ZodMiniRecord({\n type: \"record\",\n keyType,\n valueType,\n ...normalizeParams(params)\n });\n}\nconst ZodMiniOptional = /*@__PURE__*/ $constructor(\"ZodMiniOptional\", (inst, def) => {\n $ZodOptional.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction optional(innerType) {\n return new ZodMiniOptional({\n type: \"optional\",\n innerType\n });\n}\nconst ZodMiniDefault = /*@__PURE__*/ $constructor(\"ZodMiniDefault\", (inst, def) => {\n $ZodDefault.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction _default(innerType, defaultValue) {\n return new ZodMiniDefault({\n type: \"default\",\n innerType,\n get defaultValue() {\n return typeof defaultValue === \"function\" ? defaultValue() : shallowClone(defaultValue);\n }\n });\n}\n\n//#endregion\n//#region src/lib/hook-io.ts\nconfig(en_default());\nconst HookInputSchema = object({\n session_id: string(),\n transcript_path: string(),\n cwd: string(),\n permission_mode: string(),\n hook_event_name: string(),\n tool_name: string(),\n tool_input: record(string(), unknown())\n});\nasync function readHookInput() {\n const chunks = [];\n for await (const chunk of process.stdin) chunks.push(typeof chunk === \"string\" ? chunk : String(chunk));\n let parsed;\n try {\n parsed = JSON.parse(chunks.join(\"\"));\n } catch (_error) {\n return {\n ok: false,\n raw: chunks.join(\"\").slice(0, 200),\n error: \"invalid JSON\"\n };\n }\n const result = HookInputSchema.safeParse(parsed);\n if (!result.success) return {\n ok: false,\n raw: parsed,\n error: result.error.message\n };\n return {\n ok: true,\n data: result.data\n };\n}\nconst BashInputSchema = object({\n command: string(),\n description: optional(string())\n});\nconst FilePathInputSchema = object({ file_path: string() });\nconst SearchInputSchema = object({\n pattern: string(),\n path: optional(string())\n});\nfunction parseBashInput(toolInput) {\n const result = BashInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction parseFilePathInput(toolInput) {\n const result = FilePathInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction parseSearchInput(toolInput) {\n const result = SearchInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction writeDeny(reason) {\n const decision = { hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"deny\",\n ...reason ? { permissionDecisionReason: reason } : {}\n } };\n process.stdout.write(JSON.stringify(decision));\n}\nfunction writeAdvice(advice) {\n const decision = { hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n additionalContext: advice\n } };\n process.stdout.write(JSON.stringify(decision));\n}\n\n//#endregion\n//#region src/lib/audit.ts\nconst MAX_LOG_SIZE = 1048576;\nfunction getLogPath() {\n const hookDir = path.dirname(fileURLToPath(import.meta.url));\n return path.join(hookDir, \"bst-hook.audit.local.jsonl\");\n}\nfunction rotateIfNeeded(logPath) {\n try {\n if (fs.statSync(logPath).size > MAX_LOG_SIZE) {\n const rotatedPath = `${logPath}.1`;\n fs.renameSync(logPath, rotatedPath);\n }\n } catch (_error) {}\n}\nlet rotationChecked = false;\nfunction audit(entry) {\n try {\n const logPath = getLogPath();\n if (!rotationChecked) {\n rotateIfNeeded(logPath);\n rotationChecked = true;\n }\n const record = {\n timestamp: (/* @__PURE__ */ new Date()).toISOString(),\n ...entry\n };\n fs.appendFileSync(logPath, JSON.stringify(record) + \"\\n\");\n } catch (err) {\n process.stderr.write(`[bst-hook] audit write failed: ${err instanceof Error ? err.message : String(err)}\\n`);\n }\n}\n\n//#endregion\n//#region src/lib/shell-tokenizer.ts\nfunction scanDoubleQuoted(input, start) {\n let content = \"\";\n let i = start;\n while (i < input.length && input[i] !== \"\\\"\") {\n if (input[i] === \"\\\\\" && i + 1 < input.length) {\n const next = input[i + 1];\n if (next === \"\\\"\" || next === \"\\\\\" || next === \"$\" || next === \"`\") {\n content += next;\n i += 2;\n continue;\n }\n }\n content += input[i];\n i++;\n }\n if (i >= input.length) return null;\n return {\n content,\n next: i + 1\n };\n}\nfunction tokenize(input) {\n const tokens = [];\n let current = \"\";\n let i = 0;\n let inToken = false;\n while (i < input.length) {\n const ch = input[i];\n if (ch === \"'\") {\n inToken = true;\n i++;\n while (i < input.length && input[i] !== \"'\") {\n current += input[i];\n i++;\n }\n if (i >= input.length) return {\n ok: false,\n error: \"unterminated single quote\"\n };\n i++;\n } else if (ch === \"\\\"\") {\n inToken = true;\n const scanned = scanDoubleQuoted(input, i + 1);\n if (!scanned) return {\n ok: false,\n error: \"unterminated double quote\"\n };\n current += scanned.content;\n i = scanned.next;\n } else if (ch === \"\\\\\" && i + 1 < input.length) {\n inToken = true;\n current += input[i + 1];\n i += 2;\n } else if (ch === \" \" || ch === \" \") {\n if (inToken) {\n tokens.push(current);\n current = \"\";\n inToken = false;\n }\n i++;\n } else {\n inToken = true;\n current += ch;\n i++;\n }\n }\n if (inToken) tokens.push(current);\n return {\n ok: true,\n tokens\n };\n}\nconst SAFE_UNQUOTED_RE = /^[A-Za-z0-9_\\-./=:@%+,]+$/;\nfunction quoteToken(token) {\n if (token.length > 0 && SAFE_UNQUOTED_RE.test(token)) return token;\n return \"'\" + token.replace(/'/g, \"'\\\\''\") + \"'\";\n}\nfunction joinTokens(tokens) {\n return tokens.map(quoteToken).join(\" \");\n}\n\n//#endregion\n//#region src/lib/shell-splitter.ts\nfunction splitCommand(input) {\n if (!input || !input.trim()) return [];\n const segments = [];\n let current = \"\";\n let i = 0;\n let inSingleQuote = false;\n let inDoubleQuote = false;\n while (i < input.length) {\n const ch = input[i];\n if (ch === \"'\" && !inDoubleQuote) {\n inSingleQuote = !inSingleQuote;\n current += ch;\n i++;\n continue;\n }\n if (ch === \"\\\"\" && !inSingleQuote) {\n inDoubleQuote = !inDoubleQuote;\n current += ch;\n i++;\n continue;\n }\n if (ch === \"\\\\\" && !inSingleQuote && i + 1 < input.length) {\n current += ch + input[i + 1];\n i += 2;\n continue;\n }\n if (!inSingleQuote && !inDoubleQuote) {\n if (ch === \"&\" && i + 1 < input.length && input[i + 1] === \"&\") {\n pushSegment(segments, current, \"&&\");\n current = \"\";\n i += 2;\n continue;\n }\n if (ch === \"&\" && !(i + 1 < input.length && input[i + 1] === \">\") && !(i > 0 && (input[i - 1] === \">\" || input[i - 1] === \"<\"))) {\n pushSegment(segments, current, \"&\");\n current = \"\";\n i++;\n continue;\n }\n if (ch === \"|\" && i + 1 < input.length && input[i + 1] === \"|\") {\n pushSegment(segments, current, \"||\");\n current = \"\";\n i += 2;\n continue;\n }\n if (ch === \"|\") {\n pushSegment(segments, current, \"|\");\n current = \"\";\n i++;\n continue;\n }\n if (ch === \";\") {\n pushSegment(segments, current, \";\");\n current = \"\";\n i++;\n continue;\n }\n }\n current += ch;\n i++;\n }\n const trimmed = current.trim();\n if (trimmed) segments.push({\n command: trimmed,\n operator: null\n });\n return segments;\n}\nfunction pushSegment(segments, text, operator) {\n const trimmed = text.trim();\n if (trimmed) segments.push({\n command: trimmed,\n operator\n });\n}\n\n//#endregion\n//#region src/lib/injection-guard.ts\nfunction hasBraceExpansion(token) {\n let depth = 0;\n for (let i = 0; i < token.length; i++) {\n const ch = token[i];\n if (ch === \"{\") depth++;\n else if (ch === \"}\") {\n if (depth > 0) depth--;\n } else if (depth >= 1 && (ch === \",\" || ch === \".\" && token[i + 1] === \".\")) return true;\n }\n return false;\n}\nfunction checkInjection(tokens, rawSegment) {\n if (/[\\n\\r]/.test(rawSegment)) return {\n safe: false,\n reason: \"unquoted newline in command\"\n };\n for (const token of tokens) {\n if (token === \"`\") return {\n safe: false,\n reason: \"backtick command substitution\"\n };\n if (token.includes(\"`\")) return {\n safe: false,\n reason: \"backtick command substitution\"\n };\n if (token.includes(\"$\")) return {\n safe: false,\n reason: \"unresolved dollar-expansion\"\n };\n if (hasBraceExpansion(token)) return {\n safe: false,\n reason: \"brace expansion\"\n };\n if (token.includes(\"<(\")) return {\n safe: false,\n reason: \"process substitution\"\n };\n if (token.includes(\">(\")) return {\n safe: false,\n reason: \"process substitution\"\n };\n }\n return { safe: true };\n}\n\n//#endregion\n//#region src/lib/env-file.ts\nconst ENV_SAFE_SUFFIXES = [\n \".sample\",\n \".example\",\n \".template\"\n];\nconst ENV_TOOL_OWNED_SUFFIXES = [\".check.env\"];\nconst ENV_FILE_PATTERN = /(^|[/\\\\])(\\.env(\\..*)?|.*\\.env(\\..*)?|\\.env)$/;\nfunction isEnvSafeSuffix(filename) {\n return [...ENV_SAFE_SUFFIXES, ...ENV_TOOL_OWNED_SUFFIXES].some((suffix) => filename.endsWith(suffix));\n}\nfunction isEnvFile(filePath) {\n if (isEnvSafeSuffix(path.basename(filePath))) return false;\n return ENV_FILE_PATTERN.test(filePath);\n}\n\n//#endregion\n//#region src/lib/git-state.ts\nconst GIT_TIMEOUT_MS = 5e3;\nfunction getCurrentBranch(cwd) {\n try {\n const out = execFileSync(\"git\", [\n \"rev-parse\",\n \"--abbrev-ref\",\n \"HEAD\"\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!out || out === \"HEAD\") return null;\n return out;\n } catch {\n return null;\n }\n}\nfunction getRepoTopLevel(cwd) {\n try {\n const out = execFileSync(\"git\", [\"rev-parse\", \"--show-toplevel\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n return out === \"\" ? null : out;\n } catch {\n return null;\n }\n}\nfunction getRemoteDefaultBranch(cwd) {\n try {\n const out = execFileSync(\"git\", [\n \"symbolic-ref\",\n \"--short\",\n \"refs/remotes/origin/HEAD\"\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!out) return null;\n return out.startsWith(\"origin/\") ? out.slice(7) : out;\n } catch {\n return null;\n }\n}\nconst DefaultBranchSchema = object({ defaultBranch: optional(string().check(_minLength(1))) });\nfunction getConfiguredDefaultBranch(topLevel) {\n let parsed;\n try {\n parsed = JSON.parse(readFileSync(join(topLevel, \".tooling.json\"), \"utf-8\"));\n } catch {\n return null;\n }\n const result = DefaultBranchSchema.safeParse(parsed);\n return result.success ? result.data.defaultBranch ?? null : null;\n}\nconst ALWAYS_PROTECTED = [\"main\", \"master\"];\nfunction getProtectedBranches(cwd, extra) {\n const set = new Set(ALWAYS_PROTECTED);\n const remote = getRemoteDefaultBranch(cwd);\n if (remote) set.add(remote);\n for (const b of extra) set.add(b);\n return set;\n}\nfunction getUpstream(cwd, branch) {\n try {\n return execFileSync(\"git\", [\n \"rev-parse\",\n \"--abbrev-ref\",\n `${branch}@{upstream}`\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim() || null;\n } catch {\n return null;\n }\n}\nfunction resolveCommit(cwd, rev) {\n try {\n return execFileSync(\"git\", [\n \"rev-parse\",\n \"--verify\",\n \"--quiet\",\n `${rev}^{commit}`\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim() || null;\n } catch {\n return null;\n }\n}\nfunction branchCarriesOtherFiles(cwd, allowed) {\n const toplevel = getRepoTopLevel(cwd);\n if (toplevel === null) return null;\n try {\n const defaultBranch = getConfiguredDefaultBranch(toplevel) ?? getRemoteDefaultBranch(toplevel) ?? \"main\";\n const mergeBase = execFileSync(\"git\", [\n \"merge-base\",\n \"HEAD\",\n `origin/${defaultBranch}`\n ], {\n cwd: toplevel,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!mergeBase) return null;\n const names = (args) => execFileSync(\"git\", args, {\n cwd: toplevel,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().split(\"\\n\").filter((line) => line.length > 0);\n return [...names([\n \"diff\",\n \"--name-only\",\n mergeBase\n ]), ...names([\n \"ls-files\",\n \"--others\",\n \"--exclude-standard\"\n ])].some((name) => !allowed.includes(name));\n } catch {\n return null;\n }\n}\nfunction isPrimaryWorktree(cwd) {\n try {\n const gitDir = execFileSync(\"git\", [\"rev-parse\", \"--absolute-git-dir\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n const commonRaw = execFileSync(\"git\", [\"rev-parse\", \"--git-common-dir\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!gitDir || !commonRaw) return false;\n const commonDir = resolve(cwd, commonRaw);\n return realpathSync(gitDir) === realpathSync(commonDir);\n } catch {\n return false;\n }\n}\n\n//#endregion\n//#region src/lib/git-short-options.ts\nconst VALUE_TAKING_LETTERS = {\n switch: /* @__PURE__ */ new Set([\"c\", \"C\"]),\n checkout: /* @__PURE__ */ new Set([\"b\", \"B\"]),\n branch: /* @__PURE__ */ new Set([\"u\"]),\n fetch: /* @__PURE__ */ new Set([\"o\", \"j\"])\n};\nfunction splitShortOptionCluster(token, sub) {\n if (!token.startsWith(\"-\") || token.startsWith(\"--\") || token.length <= 1) return;\n const valueLetters = VALUE_TAKING_LETTERS[sub];\n const letters = [];\n const chars = token.slice(1);\n for (let i = 0; i < chars.length; i++) {\n const ch = chars[i];\n letters.push(ch);\n if (valueLetters.has(ch)) return {\n letters,\n attachedValue: chars.slice(i + 1)\n };\n }\n return { letters };\n}\n\n//#endregion\n//#region src/lib/path-scope.ts\nfunction pathFlavorFor(base) {\n return /^[a-zA-Z]:[\\\\/]/.test(base) ? path.win32 : path.posix;\n}\nfunction resolveIn(base, target) {\n return pathFlavorFor(base).resolve(base, target);\n}\nfunction isWithin(dir, target) {\n const pp = pathFlavorFor(dir);\n const rel = pp.relative(dir, target);\n if (rel === \"\") return true;\n if (rel === \"..\" || rel.startsWith(\"..\" + pp.sep)) return false;\n return !pp.isAbsolute(rel);\n}\nfunction samePath(a, b) {\n return pathFlavorFor(a).relative(a, b) === \"\";\n}\nfunction targetsGitDir(filePath, cwd) {\n const pp = pathFlavorFor(cwd);\n const resolved = resolveIn(cwd, filePath);\n if (!isWithin(cwd, resolved)) return false;\n const rel = pp.relative(cwd, resolved);\n if (rel === \"\") return false;\n return rel.split(pp.sep).includes(\".git\");\n}\nfunction targetsNodeModules(filePath, cwd) {\n return resolveIn(cwd, filePath).split(/[/\\\\]/).includes(\"node_modules\");\n}\n\n//#endregion\n//#region src/rules.ts\nconst GIT_FLAGS_WITH_ARG = /* @__PURE__ */ new Set([\n \"-C\",\n \"-c\",\n \"--git-dir\",\n \"--work-tree\"\n]);\nfunction getGitSubcommand(tokens) {\n const args = tokens.slice(1);\n for (let i = 0; i < args.length; i++) {\n const t = args[i];\n if (GIT_FLAGS_WITH_ARG.has(t)) {\n i++;\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n return t;\n }\n}\nconst WORKTREE_ADVICE = \"You're creating a feature branch in the primary worktree. An isolated worktree keeps this work independent of other windows, and with no commits on this branch yet, converting is cheap -- `git worktree add ../<dir> -b <branch>` (a sibling outside the repo, with its own dependency install). Claude Code keeps this session inside its own working tree, so the work continues in a new session opened in the sibling worktree. If you cannot open a session there, stay in the primary checkout on this branch and check `git branch --show-current` before each commit.\";\nfunction isGitBranchCreation(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"checkout\" && sub !== \"switch\") return false;\n const after = tokens.slice(tokens.indexOf(sub) + 1);\n const creationLetters = sub === \"checkout\" ? [\"b\", \"B\"] : [\"c\", \"C\"];\n return after.some((t) => {\n const cluster = splitShortOptionCluster(t, sub);\n return cluster !== void 0 && cluster.letters.some((l) => creationLetters.includes(l));\n });\n}\nfunction worktreeAdvice(tokens, ctx) {\n return isGitBranchCreation(tokens) && isPrimaryWorktree(ctx.cwd) ? WORKTREE_ADVICE : void 0;\n}\nconst GIT_FORCE_CREATE_LETTER = {\n switch: \"C\",\n checkout: \"B\"\n};\nconst GIT_FORCE_CREATE_LONG = {\n flag: \"--force-create\",\n shortest: \"--force-\"\n};\nfunction gitForceCreateTargets(after, letterSub) {\n const letter = GIT_FORCE_CREATE_LETTER[letterSub];\n const longFlag = GIT_FORCE_CREATE_LONG;\n const targets = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (letterSub === \"switch\") {\n const [flag, ...rest] = t.split(\"=\");\n if (flag !== void 0 && flag.length >= longFlag.shortest.length && longFlag.flag.startsWith(flag)) {\n const target = rest.length > 0 ? rest.join(\"=\") : after[i + 1];\n if (target !== void 0) targets.push(target);\n continue;\n }\n }\n const cluster = splitShortOptionCluster(t, letterSub);\n if (cluster === void 0 || !cluster.letters.includes(letter)) continue;\n if (cluster.attachedValue !== void 0 && cluster.attachedValue !== \"\") {\n targets.push(cluster.attachedValue);\n continue;\n }\n const next = after[i + 1];\n if (next !== void 0) targets.push(next);\n }\n return targets;\n}\nfunction gitCommandForceCreateTargets(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub === \"switch\" || sub === \"checkout\") return gitForceCreateTargets(tokens.slice(tokens.indexOf(sub) + 1), sub);\n if (sub === \"worktree\") {\n const worktreeIdx = tokens.indexOf(\"worktree\");\n if (tokens[worktreeIdx + 1] !== \"add\") return [];\n return gitForceCreateTargets(tokens.slice(worktreeIdx + 2), \"checkout\");\n }\n return [];\n}\nfunction isGitProtectedBranchForceCreate(tokens, ctx) {\n const targets = gitCommandForceCreateTargets(tokens);\n if (targets.length === 0) return false;\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n return targets.some((t) => protectedSet.has(t));\n}\nfunction isGitProtectedBranchRefWrite(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"branch\") return false;\n const { flags, positionals } = splitGitBranchArgs(tokens.slice(tokens.indexOf(sub) + 1));\n if (positionals.length === 0) return false;\n let deleteMode = false;\n let renameMode = false;\n let copyMode = false;\n let forceMode = false;\n for (const flag of flags) {\n if (flag.letters !== void 0) {\n if (flag.letters.includes(\"d\") || flag.letters.includes(\"D\")) deleteMode = true;\n if (flag.letters.includes(\"m\") || flag.letters.includes(\"M\")) renameMode = true;\n if (flag.letters.includes(\"c\") || flag.letters.includes(\"C\")) copyMode = true;\n if (flag.letters.includes(\"f\") || flag.letters.includes(\"M\") || flag.letters.includes(\"C\")) forceMode = true;\n continue;\n }\n if (flag.name === \"delete\") deleteMode = true;\n if (flag.name === \"move\") renameMode = true;\n if (flag.name === \"copy\") copyMode = true;\n if (flag.name === \"force\") forceMode = true;\n }\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n if (deleteMode && positionals.some((p) => protectedSet.has(p))) return true;\n if (renameMode && positionals.length >= 2 && protectedSet.has(positionals[0])) return true;\n if (forceMode) {\n const destination = renameMode || copyMode ? positionals[positionals.length - 1] : positionals[0];\n if (destination !== void 0 && protectedSet.has(destination)) return true;\n }\n return false;\n}\nfunction refspecDestination(refspec) {\n const spec = refspec.startsWith(\"+\") ? refspec.slice(1) : refspec;\n const colon = spec.lastIndexOf(\":\");\n return (colon === -1 ? spec : spec.slice(colon + 1)).replace(/^refs\\/heads\\//, \"\");\n}\nconst GIT_BRANCH_VALUE_FLAG_NAMES = /* @__PURE__ */ new Set([\n \"contains\",\n \"no-contains\",\n \"merged\",\n \"no-merged\",\n \"points-at\",\n \"sort\",\n \"format\",\n \"set-upstream-to\"\n]);\nfunction splitGitBranchArgs(after) {\n const flags = [];\n const positionals = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (t.startsWith(\"--\") && t !== \"--\") {\n const eq = t.indexOf(\"=\");\n const name = eq > 0 ? t.slice(2, eq) : t.slice(2);\n flags.push({\n raw: t,\n name\n });\n if (eq < 0 && GIT_BRANCH_VALUE_FLAG_NAMES.has(name)) i++;\n continue;\n }\n const cluster = splitShortOptionCluster(t, \"branch\");\n if (cluster === void 0) {\n flags.push({ raw: t });\n continue;\n }\n flags.push({\n raw: t,\n letters: cluster.letters\n });\n if (cluster.attachedValue === \"\") i++;\n }\n return {\n flags,\n positionals\n };\n}\nconst GIT_FETCH_VALUE_FLAG_NAMES = /* @__PURE__ */ new Set([\n \"depth\",\n \"deepen\",\n \"shallow-since\",\n \"shallow-exclude\",\n \"negotiation-tip\",\n \"filter\",\n \"jobs\",\n \"server-option\",\n \"upload-pack\",\n \"refmap\",\n \"submodule-prefix\",\n \"recurse-submodules-default\"\n]);\nfunction splitGitFetchArgs(after) {\n const flags = [];\n const positionals = [];\n const refmaps = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (t === \"--\") {\n flags.push({ raw: t });\n continue;\n }\n if (t.startsWith(\"--\")) {\n const eq = t.indexOf(\"=\");\n const name = eq > 0 ? t.slice(2, eq) : t.slice(2);\n flags.push({\n raw: t,\n name\n });\n if (name === \"refmap\") {\n if (eq > 0) refmaps.push(t.slice(eq + 1));\n else if (after[i + 1] !== void 0) refmaps.push(after[i + 1]);\n }\n if (eq < 0 && GIT_FETCH_VALUE_FLAG_NAMES.has(name)) i++;\n continue;\n }\n const cluster = splitShortOptionCluster(t, \"fetch\");\n if (cluster === void 0) {\n flags.push({ raw: t });\n continue;\n }\n flags.push({\n raw: t,\n letters: cluster.letters\n });\n if (cluster.attachedValue === \"\") i++;\n }\n return {\n flags,\n positionals,\n refmaps\n };\n}\nfunction fetchRefspecTarget(spec) {\n const body = spec.startsWith(\"+\") ? spec.slice(1) : spec;\n if (body.startsWith(\"^\")) return { kind: \"none\" };\n if (!body.includes(\":\")) return { kind: \"none\" };\n const destination = refspecDestination(body);\n if (destination === \"\") return { kind: \"none\" };\n if (destination.startsWith(\"refs/remotes/\")) return { kind: \"tracking\" };\n if (destination.startsWith(\"refs/\")) return { kind: \"other\" };\n return {\n kind: \"branch\",\n name: destination\n };\n}\nfunction globToAnchoredRegex(glob) {\n const escaped = glob.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return new RegExp(`^${escaped.replace(/\\\\\\*/g, \".*\")}$`);\n}\nfunction isProtectedFetchBranch(name, protectedSet) {\n if (protectedSet.has(name)) return true;\n if (!name.includes(\"*\")) return false;\n const pattern = globToAnchoredRegex(name);\n for (const member of protectedSet) if (pattern.test(member)) return true;\n return false;\n}\nfunction isGitProtectedBranchFetchWrite(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"fetch\") return false;\n const { positionals, refmaps } = splitGitFetchArgs(tokens.slice(tokens.indexOf(sub) + 1));\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n return [...positionals, ...refmaps].some((spec) => {\n const target = fetchRefspecTarget(spec);\n if (target.kind !== \"branch\" || target.name === void 0) return false;\n return isProtectedFetchBranch(target.name, protectedSet);\n });\n}\nfunction isGitProtectedUpdateRef(tokens, ctx) {\n const after = tokens.slice(tokens.indexOf(\"update-ref\") + 1);\n let ref;\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === \"-m\") {\n i++;\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n ref = t;\n break;\n }\n if (ref === void 0) return false;\n return getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []).has(ref.replace(/^refs\\/heads\\//, \"\"));\n}\nfunction gitEffectiveCwd(tokens, cwd) {\n let dir = cwd;\n const args = tokens.slice(1);\n for (let i = 0; i < args.length; i++) {\n const t = args[i];\n if (t === \"-C\") {\n const next = args[i + 1];\n if (next !== void 0) dir = path.resolve(dir, next);\n i++;\n continue;\n }\n if (GIT_FLAGS_WITH_ARG.has(t)) {\n i++;\n continue;\n }\n if (!t.startsWith(\"-\")) break;\n }\n return dir;\n}\nconst GIT_SWITCH_NEUTRAL_FLAGS = /* @__PURE__ */ new Set([\n \"-q\",\n \"--quiet\",\n \"-f\",\n \"--force\",\n \"--discard-changes\",\n \"-m\",\n \"--merge\",\n \"--no-track\",\n \"--progress\",\n \"--no-progress\",\n \"--guess\",\n \"--no-guess\",\n \"--ignore-other-worktrees\",\n \"--recurse-submodules\",\n \"--no-recurse-submodules\"\n]);\nconst GIT_SWITCH_CREATE_FLAGS = /* @__PURE__ */ new Set([\n \"--create\",\n \"--force-create\",\n \"--orphan\"\n]);\nconst GIT_CHECKOUT_PATH_FLAGS = /* @__PURE__ */ new Set([\n \"--ours\",\n \"--theirs\",\n \"-p\",\n \"--patch\",\n \"--conflict\",\n \"--pathspec-from-file\",\n \"--pathspec-file-nul\",\n \"--overlay\",\n \"--no-overlay\"\n]);\nconst GIT_SWITCH_NEUTRAL_LETTERS = /* @__PURE__ */ new Set([\n \"q\",\n \"f\",\n \"m\"\n]);\nfunction switchTarget(after, sub, isCheckoutBranch) {\n if (sub === \"checkout\" && after.includes(\"--\")) return void 0;\n const createLetters = sub === \"checkout\" ? [\"b\", \"B\"] : [\"c\", \"C\"];\n const known = (l) => GIT_SWITCH_NEUTRAL_LETTERS.has(l) || createLetters.includes(l);\n const positionals = [];\n let created;\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (GIT_SWITCH_NEUTRAL_FLAGS.has(t)) continue;\n const eq = t.indexOf(\"=\");\n if (sub === \"checkout\" && GIT_CHECKOUT_PATH_FLAGS.has(eq === -1 ? t : t.slice(0, eq))) return;\n if (GIT_SWITCH_CREATE_FLAGS.has(eq === -1 ? t : t.slice(0, eq))) {\n created = eq === -1 ? after[++i] : t.slice(eq + 1);\n continue;\n }\n const cluster = splitShortOptionCluster(t, sub);\n if (cluster === void 0 || !cluster.letters.every(known)) return null;\n if (cluster.letters.some((l) => createLetters.includes(l))) created = cluster.attachedValue ? cluster.attachedValue : after[++i];\n }\n if (created !== void 0) return created;\n if (positionals.length !== 1) return void 0;\n const target = positionals[0];\n if (target === \"HEAD\" || target === \"@\") return void 0;\n if (sub === \"checkout\" && !isCheckoutBranch(target)) return void 0;\n return target;\n}\nfunction branchSwitch(tokens, ctx, resolved) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"switch\" && sub !== \"checkout\") return void 0;\n const dir = gitEffectiveCwd(tokens, ctx.cwd);\n const isCheckoutBranch = (name) => !resolved || getProtectedBranches(dir, ctx.repoProtectedBranches ?? []).has(name) || resolveCommit(dir, name) !== null;\n const target = switchTarget(tokens.slice(tokens.indexOf(sub) + 1), sub, isCheckoutBranch);\n if (!resolved) return target === void 0 ? void 0 : {\n dir,\n branch: null\n };\n return target === void 0 ? void 0 : {\n dir,\n branch: target\n };\n}\nconst GIT_CHECKED_OUT_WRITES = /* @__PURE__ */ new Set([\n \"commit\",\n \"merge\",\n \"rebase\",\n \"cherry-pick\",\n \"revert\",\n \"am\",\n \"pull\"\n]);\nfunction subcommandArgs(tokens, sub, valueFlags = /* @__PURE__ */ new Set()) {\n const after = tokens.slice(tokens.indexOf(sub) + 1);\n const flags = [];\n const positionals = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n flags.push(t);\n if (valueFlags.has(t)) i++;\n }\n return {\n flags,\n positionals\n };\n}\nconst GIT_PUSH_VALUE_FLAGS = /* @__PURE__ */ new Set([\n \"-o\",\n \"--push-option\",\n \"--repo\",\n \"--receive-pack\",\n \"--exec\"\n]);\nfunction isMergeFastForwardFromUpstream(tokens, upstream) {\n const { flags, positionals } = subcommandArgs(tokens, \"merge\");\n if (!flags.includes(\"--ff-only\") || positionals.length !== 1) return false;\n const ref = positionals[0];\n return ref === \"@{u}\" || ref === \"@{upstream}\" || upstream !== null && ref === upstream;\n}\nconst GIT_PULL_NO_REBASE = /* @__PURE__ */ new Set([\n \"--rebase=false\",\n \"--rebase=no\",\n \"--rebase=off\",\n \"--rebase=0\"\n]);\nfunction isPullFromUpstream(tokens, upstream) {\n const { flags, positionals } = subcommandArgs(tokens, \"pull\");\n if (flags.some((t) => t.startsWith(\"--rebase\") && !GIT_PULL_NO_REBASE.has(t) || t === \"--no-ff\" || !t.startsWith(\"--\") && t.slice(1).includes(\"r\"))) return false;\n if (positionals.length === 0) return true;\n if (positionals.length !== 2 || upstream === null) return false;\n const [remote, branch] = positionals;\n return `${remote}/${branch.replace(/^refs\\/heads\\//, \"\")}` === upstream;\n}\nfunction isRefMovingReset(tokens, cwd, head) {\n if (tokens.slice(tokens.indexOf(\"reset\") + 1).includes(\"--\")) return false;\n const { positionals } = subcommandArgs(tokens, \"reset\");\n if (positionals.length !== 1) return false;\n if (head !== \"HEAD\" && /^(HEAD|@)/.test(positionals[0])) return false;\n const target = resolveCommit(cwd, positionals[0]);\n if (target === null) return false;\n return target !== resolveCommit(cwd, head);\n}\nfunction pushDestinations(tokens, current) {\n const { positionals } = subcommandArgs(tokens, \"push\", GIT_PUSH_VALUE_FLAGS);\n const refspecs = positionals.slice(1);\n if (refspecs.length === 0) return null;\n const names = [];\n for (const spec of refspecs) {\n const dst = refspecDestination(spec);\n if (dst === \"HEAD\" || dst === \"@\") {\n if (current !== null) names.push(current);\n continue;\n }\n names.push(dst);\n }\n return names;\n}\nconst GIT_PUSH_EVERY_BRANCH = /* @__PURE__ */ new Set([\n \"--all\",\n \"--branches\",\n \"--mirror\"\n]);\nfunction leavesRefInPlace(tokens, sub) {\n const { flags } = subcommandArgs(tokens, sub);\n if (flags.includes(\"--abort\") || flags.includes(\"--quit\")) return true;\n return sub === \"commit\" && flags.includes(\"--dry-run\");\n}\nfunction movesProtectedRef(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (sub === void 0) return false;\n if (sub === \"switch\" || sub === \"checkout\" || sub === \"worktree\") return isGitProtectedBranchForceCreate(tokens, ctx);\n if (sub === \"branch\") return isGitProtectedBranchRefWrite(tokens, ctx);\n if (sub === \"fetch\") return isGitProtectedBranchFetchWrite(tokens, ctx);\n if (sub === \"update-ref\") return isGitProtectedUpdateRef(tokens, ctx);\n if (sub !== \"push\" && sub !== \"reset\" && !GIT_CHECKED_OUT_WRITES.has(sub)) return false;\n const cwd = gitEffectiveCwd(tokens, ctx.cwd);\n const pending = ctx.pendingBranches?.[cwd];\n const current = pending ? pending.branch : getCurrentBranch(cwd);\n const protectedSet = getProtectedBranches(cwd, ctx.repoProtectedBranches ?? []);\n const onProtected = current !== null && protectedSet.has(current);\n if (sub === \"push\") {\n const { flags } = subcommandArgs(tokens, \"push\", GIT_PUSH_VALUE_FLAGS);\n if (flags.some((f) => GIT_PUSH_EVERY_BRANCH.has(f))) return true;\n const destinations = pushDestinations(tokens, current);\n if (destinations === null) {\n if (flags.includes(\"--tags\")) return false;\n return onProtected;\n }\n return destinations.some((d) => protectedSet.has(d));\n }\n if (!onProtected) return false;\n if (leavesRefInPlace(tokens, sub)) return false;\n if (sub === \"reset\") return isRefMovingReset(tokens, cwd, pending ? current : \"HEAD\");\n if (sub === \"merge\" || sub === \"pull\") {\n const upstream = getUpstream(cwd, current);\n if (sub === \"merge\" && isMergeFastForwardFromUpstream(tokens, upstream)) return false;\n if (sub === \"pull\" && isPullFromUpstream(tokens, upstream)) return false;\n }\n return true;\n}\nconst PNPM_BUILTINS = /* @__PURE__ */ new Set([\n \"install\",\n \"i\",\n \"add\",\n \"remove\",\n \"rm\",\n \"uninstall\",\n \"un\",\n \"update\",\n \"up\",\n \"upgrade\",\n \"list\",\n \"ls\",\n \"ll\",\n \"la\",\n \"why\",\n \"view\",\n \"info\",\n \"show\",\n \"outdated\",\n \"audit\",\n \"licenses\",\n \"init\",\n \"create\",\n \"link\",\n \"unlink\",\n \"pack\",\n \"publish\",\n \"config\",\n \"set\",\n \"get\",\n \"store\",\n \"cache\",\n \"rebuild\",\n \"rb\",\n \"prune\",\n \"dedupe\",\n \"dlx\",\n \"exec\",\n \"run\",\n \"patch\",\n \"patch-commit\",\n \"recursive\",\n \"-r\"\n]);\nconst NPM_READ_OPS = /* @__PURE__ */ new Set([\n \"view\",\n \"info\",\n \"show\"\n]);\nfunction isNpmReadOp(tokens) {\n const sub = tokens[1];\n return sub !== void 0 && NPM_READ_OPS.has(sub);\n}\nfunction getNpmRedirectReason(tokens) {\n const sub = tokens[1];\n const BASE = \"This project uses pnpm.\";\n if (!sub) return `${BASE} Use \\`pnpm\\` instead of npm.`;\n if (sub === \"install\" || sub === \"i\" || sub === \"ci\") {\n if (tokens.slice(2).some((t) => !t.startsWith(\"-\"))) return `${BASE} Use \\`pnpm add <pkg>\\` instead of \\`npm install <pkg>\\`.`;\n return `${BASE} Use \\`pnpm install\\` instead of npm.`;\n }\n if (sub === \"add\") return `${BASE} Use \\`pnpm add <pkg>\\` instead of npm.`;\n if (sub === \"uninstall\" || sub === \"remove\" || sub === \"rm\" || sub === \"un\" || sub === \"r\") return `${BASE} Use \\`pnpm remove <pkg>\\` instead of npm.`;\n if (sub === \"run\" || sub === \"run-script\") return `${BASE} Use \\`pnpm run <script>\\` or \\`pnpm <script>\\` instead of npm.`;\n if (sub === \"test\" || sub === \"t\" || sub === \"tst\") return `${BASE} Use \\`pnpm test\\` instead of npm.`;\n if (sub === \"start\") return `${BASE} Use \\`pnpm start\\` instead of npm.`;\n if (sub === \"stop\") return `${BASE} Use \\`pnpm stop\\` instead of npm.`;\n if (sub === \"restart\") return `${BASE} Use \\`pnpm restart\\` instead of npm.`;\n if (sub === \"exec\") return `${BASE} Use \\`pnpm exec <cmd>\\` instead of npm.`;\n if (sub === \"list\" || sub === \"ls\" || sub === \"ll\" || sub === \"la\") return `${BASE} Use \\`pnpm list\\` instead of npm.`;\n if (sub === \"why\") return `${BASE} Use \\`pnpm why\\` instead of npm.`;\n if (sub === \"outdated\") return `${BASE} Use \\`pnpm outdated\\` instead of npm.`;\n if (sub === \"audit\") return `${BASE} Use \\`pnpm audit\\` instead of npm.`;\n if (sub === \"update\" || sub === \"up\" || sub === \"upgrade\") return `${BASE} Use \\`pnpm update\\` instead of npm.`;\n if (sub === \"init\") return `${BASE} Use \\`pnpm init\\` instead of npm.`;\n if (sub === \"publish\") return `${BASE} Use \\`pnpm publish\\` instead of npm.`;\n if (sub === \"link\") return `${BASE} Use \\`pnpm link\\` instead of npm.`;\n if (sub === \"unlink\") return `${BASE} Use \\`pnpm unlink\\` instead of npm.`;\n if (sub === \"pack\") return `${BASE} Use \\`pnpm pack\\` instead of npm.`;\n if (sub === \"cache\") return `${BASE} Use \\`pnpm store\\` instead of \\`npm cache\\`.`;\n if (sub === \"config\" || sub === \"set\" || sub === \"get\") return `${BASE} Use \\`pnpm config\\` instead of npm.`;\n return `${BASE} Use the equivalent pnpm command instead of npm.`;\n}\nconst LAST_POSITIONAL_DEST = /* @__PURE__ */ new Set([\n \"cp\",\n \"mv\",\n \"rsync\",\n \"install\",\n \"ln\"\n]);\nconst ALL_POSITIONAL_DEST = /* @__PURE__ */ new Set([\"tee\", \"truncate\"]);\nfunction explicitTargetDirectory(rest) {\n for (const [i, t] of rest.entries()) {\n if (t === \"-t\" || t === \"--target-directory\") return rest[i + 1] ?? null;\n if (t.startsWith(\"--target-directory=\")) return t.slice(19);\n }\n return null;\n}\nconst OUTPUT_REDIRECT = /^(?:\\d*|&)>{1,2}$/;\nconst OUTPUT_REDIRECT_ATTACHED = /^(?:\\d*|&)>{1,2}(.+)$/;\nconst INPUT_REDIRECT = /^\\d*<$/;\nconst INPUT_REDIRECT_ATTACHED = /^\\d*<([^<].*)$/;\nfunction spacedRedirectDirection(token) {\n if (OUTPUT_REDIRECT.test(token)) return \"out\";\n if (INPUT_REDIRECT.test(token)) return \"in\";\n return null;\n}\nfunction splitRedirects(tokens) {\n const redirects = [];\n const rest = [];\n for (let i = 0; i < tokens.length; i++) {\n const t = tokens[i];\n const spaced = spacedRedirectDirection(t);\n if (spaced !== null) {\n const next = tokens[i + 1];\n if (next !== void 0) redirects.push({\n direction: spaced,\n target: next\n });\n i++;\n continue;\n }\n const out = OUTPUT_REDIRECT_ATTACHED.exec(t);\n if (out) {\n redirects.push({\n direction: \"out\",\n target: out[1]\n });\n continue;\n }\n const input = INPUT_REDIRECT_ATTACHED.exec(t);\n if (input) {\n redirects.push({\n direction: \"in\",\n target: input[1]\n });\n continue;\n }\n rest.push(t);\n }\n return {\n redirects,\n rest\n };\n}\nfunction redirectTargets(tokens, direction) {\n return splitRedirects(tokens).redirects.filter((r) => r.direction === direction).map((r) => r.target);\n}\nfunction writeDestinations(tokens) {\n const cmd = path.basename(tokens[0] ?? \"\");\n const rest = tokens.slice(1);\n const dests = redirectTargets(tokens, \"out\");\n if (cmd === \"dd\") dests.push(...rest.filter((t) => t.startsWith(\"of=\")).map((t) => t.slice(3)));\n else if (cmd === \"sed\") {\n if (rest.some((t) => t === \"--in-place\" || /^-[a-zA-Z]*i/.test(t))) dests.push(...rest.filter((t) => !t.startsWith(\"-\")));\n } else if (ALL_POSITIONAL_DEST.has(cmd)) dests.push(...rest.filter((t) => !t.startsWith(\"-\")));\n else if (LAST_POSITIONAL_DEST.has(cmd)) {\n if (cmd === \"cp\" && rest.includes(\"--remove-destination\")) return dests;\n const targetDir = explicitTargetDirectory(rest);\n if (targetDir !== null) dests.push(targetDir);\n else {\n const last = rest.findLast((t) => !t.startsWith(\"-\"));\n if (last !== void 0) dests.push(last);\n }\n }\n return dests;\n}\nfunction writesIntoNodeModules(tokens, ctx) {\n return writeDestinations(tokens).some((d) => targetsNodeModules(d, ctx.cwd));\n}\nconst NODE_MODULES_WRITE_REASON = \"Don't write into node_modules/. Its files are hardlinks into pnpm's global store, so overwriting one in place (plain `cp`, `>` redirect, `sed -i`, ...) writes through the link and corrupts that package for every project on this machine -- silently, since the directory you wrote into ends up fine. To test a local build as an installed dependency, run it directly (`node packages/<pkg>/dist/bin.mjs <cmd>`), or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`). If you must replace a file in place, `cp --remove-destination` is safe -- it unlinks the target first. Run `pnpm store status` to check whether the store is already damaged.\";\nfunction isChmod777(tokens) {\n return tokens.some((t) => t === \"777\" || t === \"0777\");\n}\nfunction isGitForcePush(tokens) {\n const pushIdx = tokens.indexOf(\"push\");\n if (pushIdx === -1) return false;\n return tokens.slice(pushIdx + 1).some((t) => t === \"--force\" || t === \"-f\");\n}\nconst ENV_READ_FILE_COMMANDS = /* @__PURE__ */ new Set([\n \"cat\",\n \"head\",\n \"tail\",\n \"less\",\n \"more\",\n \"source\",\n \".\"\n]);\nconst GREP_FAMILY_COMMANDS = /* @__PURE__ */ new Set([\n \"grep\",\n \"rg\",\n \"ag\"\n]);\nfunction grepFileOperands(rest) {\n const files = [];\n let patternAccountedFor = false;\n let optionsEnded = false;\n for (let i = 0; i < rest.length; i++) {\n const t = rest[i];\n if (!optionsEnded && t === \"--\") {\n optionsEnded = true;\n continue;\n }\n if (!optionsEnded) {\n if (t === \"-f\" || t === \"--file\") {\n const value = rest[i + 1];\n if (value !== void 0) {\n files.push(value);\n i++;\n }\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"--file=\")) {\n files.push(t.slice(7));\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"-f\") && t !== \"-f\" && !t.startsWith(\"--\")) {\n files.push(t.slice(2));\n patternAccountedFor = true;\n continue;\n }\n if (t === \"-e\" || t === \"--regexp\") {\n i++;\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"--regexp=\") || t.startsWith(\"-e\") && t !== \"-e\" && !t.startsWith(\"--\")) {\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n }\n if (!patternAccountedFor) {\n patternAccountedFor = true;\n continue;\n }\n files.push(t);\n }\n return files;\n}\nfunction envFileOperands(tokens) {\n const cmd = path.basename(tokens[0] ?? \"\");\n const { redirects, rest } = splitRedirects(tokens.slice(1));\n const operands = redirects.filter((r) => r.direction === \"in\").map((r) => r.target);\n if (GREP_FAMILY_COMMANDS.has(cmd)) operands.push(...grepFileOperands(rest));\n else if (ENV_READ_FILE_COMMANDS.has(cmd)) operands.push(...rest.filter((t) => !t.startsWith(\"-\")));\n return operands;\n}\nconst HEALTH_CEILING_FILES = [\"health-baseline.json\", \".tooling.json\"];\nconst HEALTH_BASELINE_FILE = \"health-baseline.json\";\nconst HEALTH_RAISE_REASON = \"A health ceiling raise travels alone: open a ceiling-only pull request -- only health-baseline.json and .tooling.json changed -- for a human to merge, or split the module. `health:check` refuses the raise beside code either way. To restore the default branch's copy run `git show origin/<default>:health-baseline.json > health-baseline.json`, and to adopt lower counts run `pnpm exec bst health:check --update-baseline`.\";\nfunction writesHealthBaseline(tokens, cwd) {\n const destinations = writeDestinations(tokens);\n if (!destinations.some((dest) => baseName(dest) === \"health-baseline.json\")) return false;\n const root = getRepoTopLevel(cwd) ?? cwd;\n return destinations.some((dest) => samePath(resolveIn(cwd, dest), path.join(root, HEALTH_BASELINE_FILE)));\n}\nfunction baseName(target) {\n const segments = target.split(/[/\\\\]/);\n return segments[segments.length - 1] ?? \"\";\n}\nfunction isBaselineRestore(tokens) {\n if (tokens[0] !== \"git\") return false;\n let i = 1;\n while (tokens[i]?.startsWith(\"-\") === true) i += GIT_FLAGS_WITH_ARG.has(tokens[i] ?? \"\") ? 2 : 1;\n if (tokens[i] !== \"show\") return false;\n return tokens.slice(i + 1).some((token) => token.includes(\":\") && baseName(token.slice(token.lastIndexOf(\":\") + 1)) === \"health-baseline.json\");\n}\nfunction isHealthBaselineReset(tokens) {\n return tokens.includes(\"health:check\") && tokens.includes(\"--update-baseline\") && tokens.includes(\"--reset\");\n}\nfunction raiseTravelsWithCode(cwd) {\n return branchCarriesOtherFiles(cwd, HEALTH_CEILING_FILES) === true;\n}\nconst denyRules = [\n {\n name: \"sudo\",\n match: /^sudo$/,\n action: \"deny\",\n reason: \"Don't use sudo. If you need elevated permissions, ask me to do it manually.\"\n },\n {\n name: \"git-force-push\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens) => isGitForcePush(tokens),\n reason: \"Use `git push --force-with-lease` instead of `--force`.\"\n },\n {\n name: \"git-protected-branch-write\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens, ctx) => movesProtectedRef(tokens, ctx),\n reason: \"This would move a protected branch (main, master, the remote's default branch, or one listed in .claude/hooks/bst-hook.settings.json#protectedBranches). Work on a feature branch in a worktree: `git worktree add ../<repo>-<topic> -b feat/<topic>`, then commit and push there. To bring `main` up to date, use `git pull --ff-only`.\"\n },\n {\n name: \"chmod-777\",\n match: /^chmod$/,\n action: \"deny\",\n when: (tokens) => isChmod777(tokens),\n reason: \"Don't use 777 permissions. Use more restrictive permissions.\"\n },\n {\n name: \"node-modules-write\",\n match: /./,\n action: \"deny\",\n when: (tokens, ctx) => writesIntoNodeModules(tokens, ctx),\n reason: NODE_MODULES_WRITE_REASON\n },\n {\n name: \"env-file-read\",\n match: /./,\n action: \"deny\",\n when: (tokens) => envFileOperands(tokens).some(isEnvFile),\n reason: \"Don't read .env files. These contain secrets that should not appear in the conversation.\"\n },\n {\n name: \"env-file-write\",\n match: /./,\n action: \"deny\",\n when: (tokens) => redirectTargets(tokens, \"out\").some(isEnvFile),\n reason: \"Don't write to .env files. These contain secrets.\"\n },\n {\n name: \"health-baseline-write\",\n match: /./,\n action: \"deny\",\n when: (tokens, ctx) => writesHealthBaseline(tokens, ctx.cwd) && !isBaselineRestore(tokens) && raiseTravelsWithCode(ctx.cwd),\n reason: HEALTH_RAISE_REASON\n },\n {\n name: \"health-baseline-reset\",\n match: /^bst$/,\n action: \"deny\",\n when: (tokens, ctx) => isHealthBaselineReset(tokens) && raiseTravelsWithCode(ctx.cwd),\n reason: HEALTH_RAISE_REASON\n },\n {\n name: \"npx-redirect\",\n match: /^npx$/,\n action: \"deny\",\n reason: \"This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\"\n },\n {\n name: \"mise-self-update\",\n match: /^mise$/,\n action: \"deny\",\n when: (tokens) => tokens[1] === \"self-update\",\n reason: \"Don't self-update mise. The version is Renovate-managed; ask a human to bump the pin.\"\n }\n];\n\n//#endregion\n//#region src/lib/script-resolver.ts\nconst PackageJsonSchema = object({ scripts: record(string(), unknown()) });\nfunction resolveScript(scriptName, cwd) {\n const scripts = readScripts(cwd);\n if (!scripts) return null;\n const main = scripts[scriptName];\n if (typeof main !== \"string\") return null;\n const pre = scripts[`pre${scriptName}`];\n const post = scripts[`post${scriptName}`];\n return {\n pre: typeof pre === \"string\" ? pre : null,\n main,\n post: typeof post === \"string\" ? post : null\n };\n}\nfunction resolveFilteredScript(scriptName, filterPkg, cwd) {\n const pkgDir = findWorkspacePackageDir(filterPkg, cwd);\n if (!pkgDir) return null;\n const script = resolveScript(scriptName, pkgDir);\n if (!script) return null;\n return {\n ...script,\n pkgDir\n };\n}\nfunction resolveRecursiveScripts(scriptName, rootDir) {\n const pkgDirs = listWorkspacePackageDirs(rootDir);\n if (!pkgDirs) return null;\n const results = [];\n for (const pkgDir of pkgDirs) {\n const resolved = resolveScript(scriptName, pkgDir);\n if (resolved) results.push({\n ...resolved,\n pkgDir\n });\n }\n return results;\n}\nfunction listWorkspacePackageDirs(rootDir) {\n const patterns = readWorkspacePatterns(rootDir);\n if (!patterns) return null;\n const dirs = [];\n for (const pattern of patterns) {\n if (!pattern.endsWith(\"/*\")) continue;\n const baseDir = pattern.slice(0, -2);\n const parentDir = resolve(rootDir, baseDir);\n let entries;\n try {\n entries = readdirSync(parentDir);\n } catch {\n continue;\n }\n for (const entry of entries) {\n const entryDir = join(parentDir, entry);\n try {\n if (!statSync(entryDir).isDirectory()) continue;\n } catch {\n continue;\n }\n dirs.push(entryDir);\n }\n }\n return dirs.length > 0 ? dirs : null;\n}\nfunction findWorkspacePackageDir(pkgName, rootDir) {\n const pkgDirs = listWorkspacePackageDirs(rootDir);\n if (!pkgDirs) return null;\n for (const entryDir of pkgDirs) {\n const name = readPackageName(entryDir);\n if (name === pkgName || name === `@${pkgName}`) return entryDir;\n if (entryDir.split(\"/\").pop() === pkgName) return entryDir;\n if (name) {\n if ((name.startsWith(\"@\") ? name.split(\"/\")[1] : name) === pkgName) return entryDir;\n }\n }\n return null;\n}\nfunction readWorkspacePatterns(rootDir) {\n try {\n const lines = readFileSync(join(rootDir, \"pnpm-workspace.yaml\"), \"utf-8\").split(\"\\n\");\n const packagesIdx = lines.findIndex((l) => /^packages\\s*:/.test(l));\n if (packagesIdx === -1) return null;\n const patterns = [];\n for (let i = packagesIdx + 1; i < lines.length; i++) {\n const line = lines[i];\n const match = /^\\s+-\\s+(.+)$/.exec(line);\n if (!match) break;\n patterns.push(match[1].trim().replace(/['\"`]/g, \"\"));\n }\n return patterns.length > 0 ? patterns : null;\n } catch {\n return null;\n }\n}\nconst PackageNameSchema = object({ name: string() });\nfunction readPackageName(dir) {\n try {\n const raw = readFileSync(join(dir, \"package.json\"), \"utf-8\");\n const result = PackageNameSchema.safeParse(JSON.parse(raw));\n if (!result.success) return null;\n return result.data.name;\n } catch {\n return null;\n }\n}\nfunction readScripts(cwd) {\n try {\n const raw = readFileSync(join(cwd, \"package.json\"), \"utf-8\");\n const result = PackageJsonSchema.safeParse(JSON.parse(raw));\n if (!result.success) return null;\n return result.data.scripts;\n } catch {\n return null;\n }\n}\n\n//#endregion\n//#region src/lib/workspace-root.ts\nfunction findWorkspaceRoot(cwd) {\n let dir = resolve(cwd);\n while (true) {\n if (existsSync(join(dir, \"pnpm-workspace.yaml\"))) return dir;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n//#endregion\n//#region src/evaluate.ts\nconst MAX_EVAL_DEPTH = 5;\nfunction subshellBody(command) {\n if (!command.startsWith(\"(\") || !command.endsWith(\")\")) return null;\n let depth = 0;\n let quote = null;\n for (let i = 0; i < command.length; i++) {\n const ch = command[i];\n if (quote !== null) {\n if (ch === quote) quote = null;\n else if (ch === \"\\\\\" && quote === \"\\\"\") i++;\n continue;\n }\n if (ch === \"\\\\\") i++;\n else if (ch === \"'\" || ch === \"\\\"\") quote = ch;\n else if (ch === \"(\") depth++;\n else if (ch === \")\") {\n depth--;\n if (depth === 0) return i === command.length - 1 ? command.slice(1, -1) : null;\n }\n }\n return null;\n}\nfunction extractCdDir(segment, ctx) {\n const tokenResult = tokenize(segment);\n if (!tokenResult.ok || tokenResult.tokens.length !== 2) return null;\n const tokens = tokenResult.tokens;\n if (tokens[0] !== \"cd\") return null;\n const dir = tokens[1];\n if (path.isAbsolute(dir)) return dir;\n return path.resolve(ctx.cwd, dir);\n}\nfunction extractBranchSwitch(segment, ctx) {\n const tokenResult = tokenize(segment);\n if (!tokenResult.ok) return void 0;\n let tokens = tokenResult.tokens;\n let envSkipped = 0;\n while (envSkipped < tokens.length && /^\\w+=/.test(tokens[envSkipped])) envSkipped++;\n tokens = tokens.slice(envSkipped);\n if (tokens.length === 0 || path.basename(tokens[0]) !== \"git\") return void 0;\n return branchSwitch(tokens, ctx, checkInjection(tokens, segment).safe);\n}\nfunction evaluateCommand(command, ctx) {\n if (ctx.depth >= MAX_EVAL_DEPTH) return {\n action: \"none\",\n rule: null,\n reason: \"max evaluation depth exceeded\"\n };\n const trimmed = command.trim();\n if (!trimmed) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n const body = subshellBody(trimmed);\n if (body !== null) return evaluateCommand(body, {\n ...ctx,\n depth: ctx.depth + 1\n });\n const segments = splitCommand(trimmed);\n if (segments.length === 0) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n let firstNone = null;\n const currentCtx = { ...ctx };\n let unknownAfterNext;\n for (const segment of segments) {\n if (segment.operator === \"&&\") {\n const cdDir = extractCdDir(segment.command, currentCtx);\n if (cdDir) currentCtx.cwd = cdDir;\n }\n const result = evaluateSegment(segment.command, currentCtx);\n if (result.action === \"deny\") return result;\n if (!firstNone) firstNone = result;\n if (unknownAfterNext) {\n currentCtx.pendingBranches = {\n ...currentCtx.pendingBranches,\n [unknownAfterNext.dir]: unknownAfterNext\n };\n unknownAfterNext = void 0;\n }\n const switched = extractBranchSwitch(segment.command, currentCtx);\n if (switched) {\n const unknown = {\n dir: switched.dir,\n branch: null\n };\n if (segment.operator === \"&&\") currentCtx.pendingBranches = {\n ...currentCtx.pendingBranches,\n [switched.dir]: switched\n };\n else if (segment.operator === \"||\") unknownAfterNext = unknown;\n else currentCtx.pendingBranches = {\n ...currentCtx.pendingBranches,\n [switched.dir]: unknown\n };\n }\n }\n return firstNone ?? {\n action: \"none\",\n rule: null,\n reason: null\n };\n}\nfunction evaluateResolvedScript(resolved, ctx) {\n if (resolved.pre) {\n const preResult = evaluateCommand(resolved.pre, ctx);\n if (preResult.action === \"deny\") return preResult;\n }\n const mainResult = evaluateCommand(resolved.main, ctx);\n if (mainResult.action === \"deny\") return mainResult;\n if (resolved.post) {\n const postResult = evaluateCommand(resolved.post, ctx);\n if (postResult.action === \"deny\") return postResult;\n }\n return {\n action: \"none\",\n rule: null,\n reason: null\n };\n}\nfunction evaluateSegment(rawSegment, ctx) {\n const trimmed = rawSegment.trim();\n const body = subshellBody(trimmed);\n if (body !== null) return evaluateCommand(body, {\n ...ctx,\n depth: ctx.depth + 1\n });\n if (trimmed.startsWith(\"(\") && trimmed.endsWith(\")\")) return {\n action: \"none\",\n rule: null,\n reason: \"subshell\"\n };\n const tokenResult = tokenize(rawSegment);\n if (!tokenResult.ok) return {\n action: \"none\",\n rule: null,\n reason: tokenResult.error\n };\n let tokens = tokenResult.tokens;\n if (tokens.length === 0) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n let envSkipped = 0;\n while (envSkipped < tokens.length && /^\\w+=/.test(tokens[envSkipped])) envSkipped++;\n if (envSkipped >= tokens.length) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n tokens = tokens.slice(envSkipped);\n if (writesIntoNodeModules(tokens, ctx)) return {\n action: \"deny\",\n rule: \"node-modules-write\",\n reason: NODE_MODULES_WRITE_REASON\n };\n const injection = checkInjection(tokens, rawSegment);\n if (!injection.safe) return {\n action: \"none\",\n rule: null,\n reason: injection.reason\n };\n const SAFE_REDIRECT_TOKENS = /* @__PURE__ */ new Set([\n \"2>&1\",\n \"2>/dev/null\",\n \">/dev/null\",\n \"1>/dev/null\"\n ]);\n tokens = tokens.filter((t, i) => {\n if (SAFE_REDIRECT_TOKENS.has(t)) return false;\n if ((t === \">\" || t === \">>\") && tokens[i + 1] === \"/dev/null\") return false;\n if (t === \"/dev/null\" && i > 0 && (tokens[i - 1] === \">\" || tokens[i - 1] === \">>\")) return false;\n return true;\n });\n const cmdName = path.basename(tokens[0]);\n if (cmdName === \"command\" && tokens.length > 1) return evaluateCommand(joinTokens(tokens.slice(1)), {\n ...ctx,\n depth: ctx.depth + 1\n });\n if (cmdName === \"env\") {\n let envIdx = 1;\n while (envIdx < tokens.length && /^\\w+=/.test(tokens[envIdx])) envIdx++;\n if (envIdx >= tokens.length) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n return evaluateCommand(joinTokens(tokens.slice(envIdx)), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n if (cmdName === \"pnpm\") return evaluatePnpm(tokens, ctx);\n if (cmdName === \"npm\") return evaluateNpm(tokens);\n if (cmdName === \"mise\" && tokens[1] === \"exec\") {\n const inner = miseExecInner(tokens);\n if (!inner) return {\n action: \"none\",\n rule: null,\n reason: \"bare mise exec\"\n };\n return evaluateCommand(joinTokens(inner), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n if (cmdName === \"bash\" || cmdName === \"sh\" || cmdName === \"zsh\") return evaluateShellC(tokens, ctx);\n if (cmdName === \"xargs\") return evaluateXargs(tokens, ctx);\n if (cmdName === \"node\" && (tokens.includes(\"-e\") || tokens.includes(\"-p\") || tokens.includes(\"--eval\") || tokens.includes(\"--print\"))) return {\n action: \"none\",\n rule: null,\n reason: \"node eval\"\n };\n for (const rule of denyRules) if (rule.match.test(cmdName)) {\n if (!rule.when || rule.when(tokens, ctx)) return {\n action: \"deny\",\n rule: rule.name,\n reason: rule.reason ?? null\n };\n }\n const advice = cmdName === \"git\" ? worktreeAdvice(tokens, ctx) : void 0;\n return {\n action: \"none\",\n rule: null,\n reason: `no rule matched: ${cmdName}`,\n ...advice ? { advice } : {}\n };\n}\nfunction miseExecInner(tokens) {\n const dashIdx = tokens.indexOf(\"--\");\n if (dashIdx === -1) return void 0;\n const inner = tokens.slice(dashIdx + 1);\n return inner.length > 0 ? inner : void 0;\n}\nfunction scanPnpmFlags(subTokens, from, state) {\n for (let i = from; i < subTokens.length; i++) {\n const t = subTokens[i];\n if (t === \"--filter\" || t.startsWith(\"--filter=\") || t === \"-F\" || t.startsWith(\"-F=\")) return {\n name: void 0,\n filterIdx: i\n };\n if (t === \"-r\" || t === \"--recursive\") {\n state.isRecursive = true;\n continue;\n }\n if (t === \"-w\" || t === \"--workspace-root\") {\n state.isWorkspaceRoot = true;\n continue;\n }\n if (t === \"-C\" || t === \"--dir\") {\n state.dirOverride = subTokens[++i];\n continue;\n }\n if (t.startsWith(\"-C=\") || t.startsWith(\"--dir=\")) {\n state.dirOverride = t.split(\"=\", 2)[1];\n continue;\n }\n if (t.startsWith(\"-\") && !PNPM_BUILTINS.has(t)) continue;\n return { name: t };\n }\n return { name: void 0 };\n}\nfunction evaluatePnpm(tokens, ctx) {\n const subTokens = tokens.slice(1);\n const state = {\n isRecursive: false,\n isWorkspaceRoot: false,\n dirOverride: void 0\n };\n const scan = scanPnpmFlags(subTokens, 0, state);\n if (scan.filterIdx !== void 0) return evaluatePnpmFilter(tokens, subTokens, scan.filterIdx, ctx);\n const subcommand = scan.name;\n const subcommandIdx = subcommand ? subTokens.indexOf(subcommand) : -1;\n if (!subcommand) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n if (subcommand === \"dlx\") return {\n action: \"none\",\n rule: null,\n reason: \"pnpm dlx\"\n };\n if (subcommand === \"add\") return {\n action: \"none\",\n rule: null,\n reason: \"pnpm add\"\n };\n if (subcommand === \"exec\") {\n const execArgs = subTokens.slice(subcommandIdx + 1);\n if (execArgs.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"bare pnpm exec\"\n };\n return evaluateCommand(joinTokens(execArgs), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n let scriptName;\n let { isRecursive, isWorkspaceRoot, dirOverride } = state;\n if (subcommand === \"run\") {\n const runScan = scanPnpmFlags(subTokens, subcommandIdx + 1, state);\n ({isRecursive, isWorkspaceRoot, dirOverride} = state);\n if (runScan.filterIdx !== void 0) return evaluatePnpmFilter(tokens, subTokens, runScan.filterIdx, ctx);\n if (!runScan.name) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm run without script name\"\n };\n scriptName = runScan.name;\n } else if (!PNPM_BUILTINS.has(subcommand)) scriptName = subcommand;\n else return {\n action: \"none\",\n rule: null,\n reason: null\n };\n if (isRecursive) return evaluatePnpmRecursive(scriptName, ctx);\n if (isWorkspaceRoot) {\n const root = findWorkspaceRoot(ctx.cwd);\n if (!root) return {\n action: \"none\",\n rule: null,\n reason: \"workspace root not found\"\n };\n return evaluatePnpmScript(scriptName, {\n ...ctx,\n cwd: root\n });\n }\n if (dirOverride) {\n const resolved = path.resolve(ctx.cwd, dirOverride);\n return evaluatePnpmScript(scriptName, {\n ...ctx,\n cwd: resolved\n });\n }\n return evaluatePnpmScript(scriptName, ctx);\n}\nfunction evaluatePnpmScript(scriptName, ctx) {\n const resolved = resolveScript(scriptName, ctx.cwd);\n if (!resolved) return {\n action: \"none\",\n rule: null,\n reason: \"script resolution failed\"\n };\n const res = evaluateResolvedScript(resolved, {\n ...ctx,\n depth: ctx.depth + 1\n });\n if (res.action === \"deny\") return res;\n return {\n action: \"none\",\n rule: `pnpm-script:${scriptName}`,\n reason: null\n };\n}\nfunction evaluatePnpmRecursive(scriptName, ctx) {\n const resolved = resolveRecursiveScripts(scriptName, ctx.cwd);\n if (!resolved || resolved.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm recursive\"\n };\n for (const pkg of resolved) {\n const res = evaluateResolvedScript(pkg, {\n ...ctx,\n depth: ctx.depth + 1,\n cwd: pkg.pkgDir ?? ctx.cwd\n });\n if (res.action === \"deny\") return res;\n }\n return {\n action: \"none\",\n rule: `pnpm-recursive:${scriptName}`,\n reason: null\n };\n}\nfunction evaluateNpm(tokens) {\n if (isNpmReadOp(tokens)) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n return {\n action: \"deny\",\n rule: \"npm-redirect\",\n reason: getNpmRedirectReason(tokens)\n };\n}\nfunction evaluatePnpmFilter(_tokens, subTokens, filterIdx, ctx) {\n const filterPkgs = [];\n let cursor = filterIdx;\n while (cursor < subTokens.length) {\n const t = subTokens[cursor];\n if (t === \"--filter\" || t === \"-F\") {\n const pkg = subTokens[cursor + 1];\n if (!pkg || pkg.startsWith(\"-\")) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without package\"\n };\n filterPkgs.push(pkg);\n cursor += 2;\n } else if (t.startsWith(\"--filter=\") || t.startsWith(\"-F=\")) {\n const pkg = t.startsWith(\"-F=\") ? t.slice(3) : t.slice(9);\n if (!pkg) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without package\"\n };\n filterPkgs.push(pkg);\n cursor += 1;\n } else break;\n }\n if (filterPkgs.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without package\"\n };\n let filterSubcommand;\n let subcmdIdx = -1;\n for (let i = cursor; i < subTokens.length; i++) {\n const t = subTokens[i];\n if (t === \"--filter\" || t === \"-F\") {\n i += 1;\n continue;\n }\n if (t.startsWith(\"--filter=\") || t.startsWith(\"-F=\")) continue;\n if (t.startsWith(\"-\") && !PNPM_BUILTINS.has(t)) continue;\n filterSubcommand = t;\n subcmdIdx = i;\n break;\n }\n if (!filterSubcommand) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without subcommand\"\n };\n if (PNPM_BUILTINS.has(filterSubcommand) && filterSubcommand !== \"run\" && filterSubcommand !== \"exec\") return {\n action: \"none\",\n rule: null,\n reason: null\n };\n if (filterSubcommand === \"exec\") {\n const execArgs = subTokens.slice(subcmdIdx + 1);\n if (execArgs.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"bare pnpm exec\"\n };\n return evaluateCommand(joinTokens(execArgs), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n const scriptName = filterSubcommand === \"run\" ? subTokens[subcmdIdx + 1] : filterSubcommand;\n if (!scriptName || scriptName.startsWith(\"-\")) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter run without script name\"\n };\n if (PNPM_BUILTINS.has(scriptName) && scriptName !== \"run\") return {\n action: \"none\",\n rule: null,\n reason: null\n };\n for (const pkg of filterPkgs) {\n const resolved = resolveFilteredScript(scriptName, pkg, ctx.cwd);\n if (!resolved) return {\n action: \"none\",\n rule: null,\n reason: \"filtered script resolution failed\"\n };\n const res = evaluateResolvedScript(resolved, {\n ...ctx,\n depth: ctx.depth + 1,\n cwd: resolved.pkgDir ?? ctx.cwd\n });\n if (res.action === \"deny\") return res;\n }\n return {\n action: \"none\",\n rule: `pnpm-filter:${filterPkgs.join(\",\")}:${scriptName}`,\n reason: null\n };\n}\nconst SHELL_LOGIN_FLAGS = /* @__PURE__ */ new Set([\"-l\", \"--login\"]);\nfunction evaluateShellC(tokens, ctx) {\n let i = 1;\n while (i < tokens.length && SHELL_LOGIN_FLAGS.has(tokens[i])) i++;\n const flag = tokens[i];\n if (flag === void 0 || !/^-[lc]+$/.test(flag) || !flag.includes(\"c\")) return {\n action: \"none\",\n rule: null,\n reason: \"shell without a plain -c\"\n };\n const cmdArg = tokens[i + 1];\n if (!cmdArg) return {\n action: \"none\",\n rule: null,\n reason: \"shell -c without command\"\n };\n if (tokens.length > i + 2) return {\n action: \"none\",\n rule: null,\n reason: \"shell -c with positional args\"\n };\n return evaluateCommand(cmdArg, {\n ...ctx,\n depth: ctx.depth + 1\n });\n}\nconst XARGS_ARG_FLAGS = /* @__PURE__ */ new Set([\n \"-I\",\n \"-d\",\n \"-n\",\n \"-P\",\n \"-s\",\n \"-L\",\n \"-E\",\n \"-a\",\n \"--max-args\",\n \"--max-procs\",\n \"--delimiter\",\n \"--replace\",\n \"--max-lines\",\n \"--max-chars\",\n \"--arg-file\",\n \"--process-slot-var\"\n]);\nconst XARGS_BOOL_FLAGS = /* @__PURE__ */ new Set([\n \"-0\",\n \"--null\",\n \"-t\",\n \"--verbose\",\n \"-r\",\n \"--no-run-if-empty\",\n \"-p\",\n \"--interactive\",\n \"-x\",\n \"--exit\",\n \"--show-limits\"\n]);\nfunction evaluateXargs(tokens, ctx) {\n let i = 1;\n while (i < tokens.length) {\n const t = tokens[i];\n if (!t.startsWith(\"-\")) break;\n if (XARGS_ARG_FLAGS.has(t)) {\n i += 2;\n continue;\n }\n const eqIdx = t.indexOf(\"=\");\n if (eqIdx !== -1) {\n const flagName = t.slice(0, eqIdx);\n if (XARGS_ARG_FLAGS.has(flagName)) {\n i++;\n continue;\n }\n }\n if (t.length > 2 && XARGS_ARG_FLAGS.has(t.slice(0, 2))) {\n i++;\n continue;\n }\n if (XARGS_BOOL_FLAGS.has(t)) {\n i++;\n continue;\n }\n return {\n action: \"none\",\n rule: null,\n reason: \"xargs with unrecognised flags\"\n };\n }\n if (i >= tokens.length) return {\n action: \"none\",\n rule: null,\n reason: \"xargs without command\"\n };\n return evaluateCommand(joinTokens(tokens.slice(i)), {\n ...ctx,\n depth: ctx.depth + 1\n });\n}\n\n//#endregion\n//#region src/file-rules.ts\nconst READ_TOOLS = /* @__PURE__ */ new Set([\n \"Read\",\n \"Glob\",\n \"Grep\"\n]);\nconst WRITE_TOOLS = /* @__PURE__ */ new Set([\"Write\", \"Edit\"]);\nconst fileDenyRules = [\n {\n name: \"env-file-read\",\n tools: READ_TOOLS,\n action: \"deny\",\n when: (filePath) => isEnvFile(filePath),\n reason: \"Don't read .env files. These contain secrets that should not appear in the conversation.\"\n },\n {\n name: \"env-file-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath) => isEnvFile(filePath),\n reason: \"Don't write to .env files. These contain secrets.\"\n },\n {\n name: \"hooks-folder-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n const hooksDir = path.join(ctx.cwd, \".claude\", \"hooks\");\n return isWithin(hooksDir, resolved);\n },\n reason: \"Don't write to .claude/hooks/. These files are managed by the project and should not be modified directly.\"\n },\n {\n name: \"git-dir-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => targetsGitDir(filePath, ctx.cwd),\n reason: \"Don't write into .git/ directly. Its internals (config, hooks, refs) are not plain project files -- a config edit can redirect remotes or push destinations that other rules trust, and a hooks/ script runs automatically on the next commit/push. Use git subcommands instead, or ask the user.\"\n },\n {\n name: \"node-modules-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => targetsNodeModules(filePath, ctx.cwd),\n reason: \"Don't write into node_modules/. Its files are hardlinks into pnpm's global store -- editing one in place corrupts that package for every project on this machine. Edit the source package and rebuild, or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`).\"\n },\n {\n name: \"claude-settings-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n return samePath(resolved, path.join(ctx.cwd, \".claude\", \"settings.json\"));\n },\n reason: \"Don't write to .claude/settings.json. This file is managed by the project and should not be modified directly.\"\n },\n {\n name: \"health-baseline-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx, toolInput) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n if (path.basename(resolved) !== \"health-baseline.json\") return false;\n const root = getRepoTopLevel(ctx.cwd) ?? ctx.cwd;\n if (!samePath(resolved, path.join(root, \"health-baseline.json\"))) return false;\n if (!writeRaisesBaseline(resolved, toolInput)) return false;\n return branchCarriesOtherFiles(ctx.cwd, HEALTH_CEILING_FILES) === true;\n },\n reason: HEALTH_RAISE_REASON\n }\n];\nfunction writeRaisesBaseline(file, toolInput) {\n let before;\n try {\n before = readFileSync(file, \"utf8\");\n } catch (_error) {\n return false;\n }\n const after = textAfterWrite(before, toolInput);\n if (after === void 0) return false;\n const beforeJson = parseJson(before);\n const afterJson = parseJson(after);\n if (beforeJson === void 0 || afterJson === void 0) return false;\n return numberRises(beforeJson, afterJson);\n}\nfunction textAfterWrite(before, toolInput) {\n const { content, old_string: oldText, new_string: newText, replace_all: all } = toolInput;\n if (typeof content === \"string\") return content;\n if (typeof oldText !== \"string\" || typeof newText !== \"string\" || oldText === \"\") return void 0;\n const at = before.indexOf(oldText);\n if (at === -1) return void 0;\n if (all === true) return before.split(oldText).join(newText);\n return before.slice(0, at) + newText + before.slice(at + oldText.length);\n}\nfunction parseJson(text) {\n try {\n const result = record(string(), unknown()).safeParse(JSON.parse(text));\n return result.success ? result.data : void 0;\n } catch (_error) {\n return;\n }\n}\nfunction numberRises(before, after) {\n if (typeof after === \"number\") return typeof before !== \"number\" || after > before;\n if (typeof after !== \"object\" || after === null) return false;\n const beforeObject = typeof before === \"object\" && before !== null ? before : {};\n return Object.entries(after).some(([key, value]) => numberRises(Object.entries(beforeObject).find(([k]) => k === key)?.[1], value));\n}\n\n//#endregion\n//#region src/evaluate-tool.ts\nconst FILE_PATH_TOOLS = /* @__PURE__ */ new Set([\n \"Read\",\n \"Write\",\n \"Edit\"\n]);\nconst SEARCH_TOOLS = /* @__PURE__ */ new Set([\"Glob\", \"Grep\"]);\nfunction extractPath(toolName, toolInput, cwd) {\n if (FILE_PATH_TOOLS.has(toolName)) return parseFilePathInput(toolInput)?.file_path ?? null;\n if (SEARCH_TOOLS.has(toolName)) return parseSearchInput(toolInput)?.path ?? cwd;\n return null;\n}\nfunction evaluateToolUse(toolName, toolInput, ctx) {\n const filePath = extractPath(toolName, toolInput, ctx.cwd);\n if (filePath === null) return {\n action: \"none\",\n rule: null,\n reason: \"unknown tool input shape\"\n };\n for (const rule of fileDenyRules) if (rule.tools.has(toolName) && rule.when(filePath, ctx, toolInput)) return {\n action: \"deny\",\n rule: rule.name,\n reason: rule.reason ?? null\n };\n return {\n action: \"none\",\n rule: null,\n reason: \"no file rule matched\"\n };\n}\n\n//#endregion\n//#region src/lib/settings.ts\nconst SETTINGS_PATH = \".claude/hooks/bst-hook.settings.json\";\nconst BstHookSettingsSchema = object({ protectedBranches: _default(array(string().check(_minLength(1))), []) });\nfunction isFileClean(cwd, relativePath) {\n try {\n execFileSync(\"git\", [\n \"ls-files\",\n \"--error-unmatch\",\n relativePath\n ], {\n cwd,\n stdio: \"ignore\"\n });\n execFileSync(\"git\", [\n \"diff\",\n \"--quiet\",\n \"HEAD\",\n \"--\",\n relativePath\n ], {\n cwd,\n stdio: \"ignore\"\n });\n return true;\n } catch {\n return false;\n }\n}\nfunction loadRepoSettings(cwd) {\n const root = findWorkspaceRoot(cwd) ?? cwd;\n const settingsFile = join(root, SETTINGS_PATH);\n const empty = { protectedBranches: [] };\n if (!existsSync(settingsFile)) return empty;\n if (!isFileClean(root, SETTINGS_PATH)) return empty;\n try {\n const raw = readFileSync(settingsFile, \"utf-8\");\n const parsed = JSON.parse(raw);\n const result = BstHookSettingsSchema.safeParse(parsed);\n if (!result.success) return empty;\n return { protectedBranches: result.data.protectedBranches };\n } catch {\n return empty;\n }\n}\n\n//#endregion\n//#region src/index.ts\nconst inputResult = await readHookInput();\nif (!inputResult.ok) {\n process.stderr.write(`[bst-hook] invalid hook input: ${inputResult.error}\\n`);\n audit({\n sessionId: \"unknown\",\n cwd: \"unknown\",\n command: `<invalid input: ${inputResult.error}>`,\n decision: \"none\",\n rule: null,\n reason: `validation failed: ${inputResult.error}`,\n permissionMode: void 0\n });\n process.exit(0);\n}\nconst input = inputResult.data;\nif (input.hook_event_name !== \"PreToolUse\") process.exit(0);\nif (!(/* @__PURE__ */ new Set([\n \"Bash\",\n \"Read\",\n \"Write\",\n \"Edit\",\n \"Glob\",\n \"Grep\"\n])).has(input.tool_name)) process.exit(0);\nconst repoSettings = loadRepoSettings(input.cwd);\nlet decision;\nlet auditCommand;\nif (input.tool_name === \"Bash\") {\n const bashInput = parseBashInput(input.tool_input);\n if (!bashInput) {\n audit({\n sessionId: input.session_id,\n cwd: input.cwd,\n toolName: input.tool_name,\n command: `<invalid Bash input: ${JSON.stringify(input.tool_input).slice(0, 200)}>`,\n decision: \"none\",\n rule: null,\n reason: \"Bash tool_input missing or invalid 'command' field\",\n permissionMode: input.permission_mode\n });\n process.exit(0);\n }\n decision = evaluateCommand(bashInput.command, {\n cwd: input.cwd,\n depth: 0,\n repoProtectedBranches: repoSettings.protectedBranches\n });\n auditCommand = bashInput.command;\n} else {\n const projectDir = findWorkspaceRoot(input.cwd) ?? process.env[\"CLAUDE_PROJECT_DIR\"] ?? input.cwd;\n decision = evaluateToolUse(input.tool_name, input.tool_input, {\n cwd: projectDir,\n sessionId: input.session_id\n });\n const filePath = String(input.tool_input[\"file_path\"] ?? input.tool_input[\"path\"] ?? \"\");\n auditCommand = `${input.tool_name} ${filePath}`;\n}\naudit({\n sessionId: input.session_id,\n cwd: input.cwd,\n toolName: input.tool_name,\n command: auditCommand,\n decision: decision.action,\n rule: decision.rule,\n reason: decision.reason,\n permissionMode: input.permission_mode\n});\nif (decision.action === \"deny\") writeDeny(decision.reason);\nelse if (decision.advice) writeAdvice(decision.advice);\n\n//#endregion\nexport { };",
|
|
8425
|
-
guide: "# bst-hook Permission Guide\n\nThis project uses a PreToolUse hook (`bst-hook`) that checks each Bash command\nand file tool call before it runs. The hook only ever blocks a command; it does\nnot approve anything on its own. A command that matches a rule below is denied,\nwith a message that says what to do instead.\n\n## Denied commands\n\nThese are blocked. The hook tells you what to do instead.\n\n### Bash\n\n- **sudo**: Don't use sudo. If you need elevated permissions, ask me to do it manually.\n- **git-force-push**: Use `git push --force-with-lease` instead of `--force`.\n- **git-protected-branch-write**: This would move a protected branch (main, master, the remote's default branch, or one listed in .claude/hooks/bst-hook.settings.json#protectedBranches). Work on a feature branch in a worktree: `git worktree add ../<repo>-<topic> -b feat/<topic>`, then commit and push there. To bring `main` up to date, use `git pull --ff-only`.\n- **chmod-777**: Don't use 777 permissions. Use more restrictive permissions.\n- **node-modules-write**: Don't write into node_modules/. Its files are hardlinks into pnpm's global store, so overwriting one in place (plain `cp`, `>` redirect, `sed -i`, ...) writes through the link and corrupts that package for every project on this machine -- silently, since the directory you wrote into ends up fine. To test a local build as an installed dependency, run it directly (`node packages/<pkg>/dist/bin.mjs <cmd>`), or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`). If you must replace a file in place, `cp --remove-destination` is safe -- it unlinks the target first. Run `pnpm store status` to check whether the store is already damaged.\n- **env-file-read**: Don't read .env files. These contain secrets that should not appear in the conversation.\n- **env-file-write**: Don't write to .env files. These contain secrets.\n- **health-baseline-
|
|
8503
|
+
hook: "/**\n * bst-hook -- Claude Code PreToolUse hook\n * @bensandee/claude-hooks\n * https://github.com/bensandee/tooling\n *\n * Auto-generated by @bensandee/tooling repo:sync -- do not edit manually.\n */\nimport * as fs from \"node:fs\";\nimport { existsSync, readFileSync, readdirSync, realpathSync, statSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { execFileSync } from \"node:child_process\";\n\n//#region ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/util.js\nfunction joinValues(array, separator = \"|\") {\n return array.map((val) => stringifyPrimitive(val)).join(separator);\n}\nfunction jsonStringifyReplacer(_, value) {\n if (typeof value === \"bigint\") return value.toString();\n return value;\n}\nvar Cached = class {\n constructor(getter) {\n this._getter = getter;\n this._value = void 0;\n }\n get value() {\n const getter = this._getter;\n if (getter !== void 0) {\n this._value = getter();\n this._getter = void 0;\n }\n return this._value;\n }\n};\nfunction cached(getter) {\n return new Cached(getter);\n}\nfunction nullish(input) {\n return input === null || input === void 0;\n}\nfunction cleanRegex(source) {\n const start = source.startsWith(\"^\") ? 1 : 0;\n const end = source.endsWith(\"$\") ? source.length - 1 : source.length;\n return source.slice(start, end);\n}\nfunction assignProp(target, prop, value) {\n Object.defineProperty(target, prop, {\n value,\n writable: true,\n enumerable: true,\n configurable: true\n });\n}\nconst captureStackTrace = \"captureStackTrace\" in Error ? Error.captureStackTrace : (..._args) => {};\nfunction isObject(data) {\n return typeof data === \"object\" && data !== null && !Array.isArray(data);\n}\nfunction isPlainObject(o) {\n if (isObject(o) === false) return false;\n const ctor = o.constructor;\n if (ctor === void 0) return true;\n if (typeof ctor !== \"function\") return true;\n const prot = ctor.prototype;\n if (isObject(prot) === false) return false;\n if (Object.prototype.hasOwnProperty.call(prot, \"isPrototypeOf\") === false) return false;\n return true;\n}\nfunction shallowClone(o) {\n if (isPlainObject(o)) return { ...o };\n if (Array.isArray(o)) return [...o];\n if (o instanceof Map) return new Map(o);\n if (o instanceof Set) return new Set(o);\n return o;\n}\nfunction clone(inst, def, params) {\n const cl = new inst._zod.constr(def ?? inst._zod.def);\n if (!def || params?.parent) cl._zod.parent = inst;\n return cl;\n}\nfunction normalizeParams(_params) {\n const params = _params;\n if (!params) return {};\n if (typeof params === \"string\") return { error: () => params };\n if (params?.message !== void 0) {\n if (params?.error !== void 0) throw new Error(\"Cannot specify both `message` and `error` params\");\n params.error = params.message;\n }\n delete params.message;\n if (typeof params.error === \"string\") return {\n ...params,\n error: () => params.error\n };\n return params;\n}\nfunction stringifyPrimitive(value) {\n if (typeof value === \"bigint\") return value.toString() + \"n\";\n if (typeof value === \"string\") return `\"${value}\"`;\n return `${value}`;\n}\nfunction optionalKeys(shape) {\n return Object.keys(shape).filter((k) => {\n return shape[k]._zod.optin !== void 0 && shape[k]._zod.optout === \"optional\";\n });\n}\nfunction aborted(x, startIndex = 0) {\n if (x.aborted === true) return true;\n for (let i = startIndex; i < x.issues.length; i++) if (x.issues[i]?.continue !== true) return true;\n return false;\n}\nfunction explicitlyAborted(x, startIndex = 0) {\n if (x.aborted === true) return true;\n for (let i = startIndex; i < x.issues.length; i++) if (x.issues[i]?.continue === false) return true;\n return false;\n}\nfunction prefixIssues(path, issues) {\n return issues.map((iss) => {\n var _a;\n (_a = iss).path ?? (_a.path = []);\n iss.path.unshift(path);\n return iss;\n });\n}\nfunction unwrapMessage(message) {\n return typeof message === \"string\" ? message : message?.message;\n}\nfunction sizeCaps(defChecks) {\n const caps = [];\n for (const ch of defChecks ?? []) {\n const d = ch._zod.def;\n if (!/_(length|size)$|_equals$/.test(d.check)) break;\n const max = d.maximum ?? d.length ?? d.size;\n if (max >= 0) caps.push([max, ch]);\n }\n return caps;\n}\nfunction attachSchema(issues, start, inst) {\n var _a;\n for (let i = start; i < issues.length; i++) (_a = issues[i]).schema ?? (_a.schema = inst);\n}\nfunction finalizeIssue(iss, ctx, config) {\n var _a;\n const traits = iss.inst?._zod?.traits;\n if (traits?.has(\"$ZodType\")) {\n if (traits.has(\"$ZodCheck\")) (_a = iss).schema ?? (_a.schema = iss.inst);\n else iss.schema = iss.inst;\n }\n const schemaError = iss.schema !== iss.inst ? iss.schema?._zod.def?.error : void 0;\n const message = iss.message ? iss.message : unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ?? unwrapMessage(schemaError?.(iss)) ?? unwrapMessage(ctx?.error?.(iss)) ?? unwrapMessage(config.customError?.(iss)) ?? unwrapMessage(config.localeError?.(iss)) ?? \"Invalid input\";\n const full = {};\n for (const k of Object.keys(iss)) {\n if (k === \"inst\" || k === \"schema\" || k === \"continue\" || k === \"input\" || k === \"__proto__\") continue;\n full[k] = iss[k];\n }\n full.path ?? (full.path = []);\n full.message = message;\n if (ctx?.reportInput) full.input = iss.input;\n return full;\n}\nconst highSurrogate = /[\\uD800-\\uDBFF]/;\nfunction codePointLength(str) {\n const units = str.length;\n if (!highSurrogate.test(str)) return units;\n let count = units;\n for (let i = 0; i < units - 1; i++) if ((str.charCodeAt(i) & 64512) === 55296 && (str.charCodeAt(i + 1) & 64512) === 56320) {\n count--;\n i++;\n }\n return count;\n}\nfunction getLengthableOrigin(input) {\n if (Array.isArray(input)) return \"array\";\n if (typeof input === \"string\") return \"string\";\n return \"unknown\";\n}\nfunction parsedType(data) {\n const t = typeof data;\n switch (t) {\n case \"number\": return Number.isNaN(data) ? \"nan\" : \"number\";\n case \"object\": {\n if (data === null) return \"null\";\n if (Array.isArray(data)) return \"array\";\n const obj = data;\n if (obj && Object.getPrototypeOf(obj) !== Object.prototype && \"constructor\" in obj && obj.constructor) return obj.constructor.name;\n }\n }\n return t;\n}\nfunction members(proto, table) {\n for (const key in table) {\n const desc = Object.getOwnPropertyDescriptor(table, key);\n if (desc.get) Object.defineProperty(proto, key, {\n ...desc,\n enumerable: false\n });\n else defineBound(proto, key, desc.value);\n }\n}\nfunction own(inst, key, value, enumerable = true) {\n Object.defineProperty(inst, key, {\n configurable: true,\n writable: true,\n enumerable,\n value\n });\n return value;\n}\nfunction hide(inst, key, value) {\n return own(inst, key, value, false);\n}\nfunction defineBound(proto, key, fn) {\n Object.defineProperty(proto, key, {\n configurable: true,\n get() {\n return this == null ? fn : own(this, key, fn.bind(this));\n },\n set(value) {\n own(this, key, value);\n }\n });\n}\nfunction claim(inst, sentinel) {\n const proto = Object.getPrototypeOf(inst);\n return sentinel in proto ? void 0 : proto;\n}\nlet installing;\nlet broke = false;\nconst breaker = {\n configurable: true,\n get() {\n broke = true;\n }\n};\nfunction defineLazyInternal(inst, key, compute) {\n const proto = Object.getPrototypeOf(inst._zod);\n if (key in proto && installing !== inst._zod) {\n installing = void 0;\n return;\n }\n installing = inst._zod;\n Object.defineProperty(proto, key, {\n configurable: true,\n get() {\n Object.defineProperty(this, key, breaker);\n const outer = broke;\n broke = false;\n try {\n const value = compute(this);\n if (broke) delete this[key];\n else Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n value\n });\n broke = broke || outer;\n return value;\n } catch (err) {\n delete this[key];\n broke = broke || outer;\n throw err;\n }\n },\n set(value) {\n Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n value\n });\n }\n });\n}\nfunction installLazyProp(inst, key, make, enumerable) {\n const proto = claim(inst, key);\n if (!proto) return;\n Object.defineProperty(proto, key, {\n configurable: true,\n get() {\n const desc = {\n configurable: true,\n writable: true,\n enumerable,\n value: void 0\n };\n Object.defineProperty(this, key, desc);\n desc.value = make(this);\n Object.defineProperty(this, key, desc);\n return desc.value;\n },\n set(value) {\n Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n enumerable,\n value\n });\n }\n });\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/core.js\nvar _a;\nconst _zodDesc = {\n value: void 0,\n enumerable: false\n};\nlet _E = \"captureStackTrace\" in Error ? Error : null;\nfunction newError(Definition) {\n const E = _E;\n if (E) {\n const saved = E.stackTraceLimit;\n if (typeof saved === \"number\") {\n try {\n E.stackTraceLimit = 0;\n } catch {\n _E = null;\n return new Definition();\n }\n try {\n return new Definition();\n } finally {\n E.stackTraceLimit = saved;\n }\n }\n }\n return new Definition();\n}\n/* @__NO_SIDE_EFFECTS__ */\nfunction $constructor(name, initializer, proto, params) {\n const zodProto = {};\n function Internals(def) {\n this.def = def;\n this.constr = _;\n this.traits = /* @__PURE__ */ new Set();\n }\n Internals.prototype = zodProto;\n const protoMembers = proto;\n const initialized = protoMembers && /* @__PURE__ */ new WeakSet();\n function init(inst, def) {\n if (!inst._zod) {\n _zodDesc.value = new Internals(def);\n try {\n Object.defineProperty(inst, \"_zod\", _zodDesc);\n } finally {\n _zodDesc.value = void 0;\n }\n } else if (inst._zod.traits.has(name)) return;\n inst._zod.traits.add(name);\n initializer(inst, def);\n if (initialized) {\n const own = Object.getPrototypeOf(inst);\n const ctorProto = inst._zod.constr.prototype;\n let up = own;\n while (up && up !== ctorProto) up = Object.getPrototypeOf(up);\n const target = up ?? own;\n if (!initialized.has(target)) {\n initialized.add(target);\n members(target, protoMembers);\n }\n }\n const proto = _.prototype;\n for (const k in proto) {\n if (!Object.prototype.hasOwnProperty.call(proto, k)) continue;\n if (!(k in inst)) inst[k] = proto[k].bind(inst);\n }\n }\n const Parent = params?.Parent ?? Object;\n class Definition extends Parent {}\n Object.defineProperty(Definition, \"name\", { value: name });\n function _(def) {\n const inst = params?.Parent ? newError(Definition) : this;\n init(inst, def);\n const deferred = inst._zod.deferred;\n if (deferred) {\n for (const fn of deferred) fn();\n inst._zod.deferred = void 0;\n }\n const pp = globalThis.__zod_globalConfig?.postProcessor;\n if (pp) pp(inst);\n return inst;\n }\n Object.defineProperty(_, \"init\", { value: init });\n Object.defineProperty(_, Symbol.hasInstance, { value: (inst) => {\n if (params?.Parent && inst instanceof params.Parent) return true;\n return inst?._zod?.traits?.has(name);\n } });\n Object.defineProperty(_, \"name\", { value: name });\n return _;\n}\nvar $ZodAsyncError = class extends Error {\n constructor() {\n super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`);\n }\n};\n(_a = globalThis).__zod_globalConfig ?? (_a.__zod_globalConfig = {});\nconst globalConfig = globalThis.__zod_globalConfig;\nfunction config(newConfig) {\n if (newConfig) Object.assign(globalConfig, newConfig);\n return globalConfig;\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/errors.js\nfunction _getMessage() {\n const internals = this._zod;\n internals.message ?? (internals.message = JSON.stringify(internals.def, jsonStringifyReplacer, 2));\n return internals.message;\n}\nfunction _setMessage(value) {\n this._zod.message = value;\n}\nconst _messageDesc = {\n get: _getMessage,\n set: _setMessage,\n enumerable: true,\n configurable: true\n};\nconst _issuesDesc = {\n value: void 0,\n enumerable: false\n};\nconst _installedToString = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]);\nconst initializer = (inst, def) => {\n inst.name = \"$ZodError\";\n _issuesDesc.value = def;\n Object.defineProperty(inst, \"issues\", _issuesDesc);\n _issuesDesc.value = void 0;\n Object.defineProperty(inst, \"message\", _messageDesc);\n const proto = Object.getPrototypeOf(inst);\n if (!_installedToString.has(proto)) {\n _installedToString.add(proto);\n Object.defineProperty(proto, \"toString\", {\n configurable: true,\n enumerable: false,\n get() {\n const value = () => this.message;\n Object.defineProperty(this, \"toString\", {\n value,\n configurable: true,\n writable: true\n });\n return value;\n },\n set(value) {\n Object.defineProperty(this, \"toString\", {\n value,\n configurable: true,\n writable: true\n });\n }\n });\n }\n};\nconst $ZodRealError = /* @__PURE__ */ $constructor(\"$ZodError\", initializer, void 0, { Parent: Error });\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/parse.js\nconst _parse = (_Err) => {\n const fn = (schema, value, _ctx, _params) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: false\n } : { async: false };\n const result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) throw new $ZodAsyncError();\n if (result.issues.length) {\n const e = new ((_params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));\n captureStackTrace(e, _params?.callee ?? fn);\n throw e;\n }\n return result.value;\n };\n return fn;\n};\nconst parse = /* @__PURE__*/ _parse($ZodRealError);\nconst _parseAsync = (_Err) => {\n const fn = async (schema, value, _ctx, params) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: true\n } : { async: true };\n let result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) result = await result;\n if (result.issues.length) {\n const e = new ((params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));\n captureStackTrace(e, params?.callee ?? fn);\n throw e;\n }\n return result.value;\n };\n return fn;\n};\nconst parseAsync = /* @__PURE__*/ _parseAsync($ZodRealError);\nconst _safeParse = (_Err) => (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: false\n } : { async: false };\n const result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) throw new $ZodAsyncError();\n return result.issues.length ? failure(_Err, result.issues, ctx) : {\n success: true,\n data: result.value\n };\n};\nconst safeParse = /* @__PURE__*/ _safeParse($ZodRealError);\nfunction failure(Err, issues, ctx) {\n let error;\n return {\n success: false,\n get error() {\n if (!error) {\n error = new Err(issues.map((iss) => finalizeIssue(iss, ctx, config())));\n issues = void 0;\n ctx = void 0;\n }\n return error;\n },\n set error(e) {\n error = e;\n issues = void 0;\n ctx = void 0;\n }\n };\n}\nconst _safeParseAsync = (_Err) => async (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: true\n } : { async: true };\n let result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) result = await result;\n return result.issues.length ? failure(_Err, result.issues, ctx) : {\n success: true,\n data: result.value\n };\n};\nconst safeParseAsync = /* @__PURE__*/ _safeParseAsync($ZodRealError);\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/regexes.js\nconst anyString = /^[\\s\\S]{0,}$/;\nconst number = /^-?\\d+(?:\\.\\d+)?$/;\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/checks.js\nconst $ZodCheck = /*@__PURE__*/ $constructor(\"$ZodCheck\", (inst, def) => {\n var _a;\n inst._zod ?? (inst._zod = {});\n inst._zod.def = def;\n (_a = inst._zod).onattach ?? (_a.onattach = []);\n});\nconst _whenHasLength = (payload) => {\n const val = payload.value;\n return !nullish(val) && val.length !== void 0;\n};\nconst $ZodCheckMinLength = /*@__PURE__*/ $constructor(\"$ZodCheckMinLength\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = _whenHasLength);\n inst._zod.check = (payload) => {\n const input = payload.value;\n const units = input.length;\n if ((typeof input === \"string\" && units >= def.minimum && units < def.minimum * 2 ? codePointLength(input) : units) >= def.minimum) return;\n const origin = getLengthableOrigin(input);\n payload.issues.push({\n origin,\n code: \"too_small\",\n minimum: def.minimum,\n inclusive: true,\n input,\n inst,\n continue: !def.abort\n });\n };\n});\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/versions.js\nconst version = {\n major: 4,\n minor: 6,\n patch: 7\n};\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/schemas.js\nconst $ZodType = /*@__PURE__*/ $constructor(\"$ZodType\", (inst, def) => {\n var _a;\n inst ?? (inst = {});\n inst._zod.def = def;\n inst._zod.bag = inst._zod.bag || {};\n inst._zod.version = version;\n const defChecks = inst._zod.def.checks;\n const checks = inst._zod.traits.has(\"$ZodCheck\") ? [inst, ...defChecks ?? []] : defChecks?.length ? [...defChecks] : [];\n for (const ch of checks) for (const fn of ch._zod.onattach) fn(inst);\n if (checks.length === 0) {\n (_a = inst._zod).deferred ?? (_a.deferred = []);\n inst._zod.deferred?.push(() => {\n inst._zod.run = inst._zod.parse;\n });\n } else {\n const runChecks = (payload, checks, ctx) => {\n if (payload.memo) return payload;\n let isAborted = aborted(payload);\n let asyncResult;\n for (const ch of checks) {\n if (ch._zod.def.when) {\n if (explicitlyAborted(payload)) continue;\n if (!ch._zod.def.when(payload)) continue;\n } else if (isAborted) continue;\n const currLen = payload.issues.length;\n const _ = ch._zod.check(payload);\n if (_ instanceof Promise && ctx?.async === false) throw new $ZodAsyncError();\n if (asyncResult || _ instanceof Promise) asyncResult = (asyncResult ?? Promise.resolve()).then(async () => {\n await _;\n if (payload.issues.length === currLen) return;\n attachSchema(payload.issues, currLen, inst);\n if (!isAborted) isAborted = aborted(payload, currLen);\n });\n else {\n if (payload.issues.length === currLen) continue;\n attachSchema(payload.issues, currLen, inst);\n if (!isAborted) isAborted = aborted(payload, currLen);\n }\n }\n if (asyncResult) return asyncResult.then(() => {\n return payload;\n });\n return payload;\n };\n const handleCanaryResult = (canary, payload, ctx) => {\n if (aborted(canary)) {\n canary.aborted = true;\n return canary;\n }\n const checkResult = runChecks(payload, checks, ctx);\n if (checkResult instanceof Promise) {\n if (ctx.async === false) throw new $ZodAsyncError();\n return checkResult.then((checkResult) => inst._zod.parse(checkResult, ctx));\n }\n return inst._zod.parse(checkResult, ctx);\n };\n inst._zod.run = (payload, ctx) => {\n if (ctx.skipChecks) return inst._zod.parse(payload, ctx);\n if (ctx.direction === \"backward\") {\n const canary = inst._zod.parse({\n value: payload.value,\n issues: []\n }, {\n ...ctx,\n skipChecks: true\n });\n if (canary instanceof Promise) return canary.then((canary) => {\n return handleCanaryResult(canary, payload, ctx);\n });\n return handleCanaryResult(canary, payload, ctx);\n }\n const result = inst._zod.parse(payload, ctx);\n if (result instanceof Promise) {\n if (ctx.async === false) throw new $ZodAsyncError();\n return result.then((result) => runChecks(result, checks, ctx));\n }\n return runChecks(result, checks, ctx);\n };\n }\n}, {\n get \"~standard\"() {\n return hide(this, \"~standard\", standardProps(this));\n },\n set \"~standard\"(value) {\n own(this, \"~standard\", value);\n }\n});\nconst toStandardResult = (r, ctx) => r.issues.length ? { issues: r.issues.map((iss) => finalizeIssue(iss, ctx, config())) } : { value: r.value };\nasync function validateAsync(inst, value) {\n const ctx = { async: true };\n return toStandardResult(await inst._zod.run({\n value,\n issues: []\n }, ctx), ctx);\n}\nfunction standardProps(inst) {\n return {\n validate: (value) => {\n const ctx = { async: false };\n try {\n const r = inst._zod.run({\n value,\n issues: []\n }, ctx);\n if (!(r instanceof Promise)) return toStandardResult(r, ctx);\n } catch (_) {}\n return validateAsync(inst, value);\n },\n vendor: \"zod\",\n version: 1\n };\n}\nconst $ZodString = /*@__PURE__*/ $constructor(\"$ZodString\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.pattern = def.pattern ?? anyString;\n inst._zod.parse = (payload, _) => {\n if (def.coerce) try {\n payload.value = String(payload.value);\n } catch (_) {}\n if (typeof payload.value === \"string\") return payload;\n payload.issues.push({\n expected: \"string\",\n code: \"invalid_type\",\n input: payload.value,\n inst\n });\n return payload;\n };\n});\nconst $ZodUnknown = /*@__PURE__*/ $constructor(\"$ZodUnknown\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload) => payload;\n});\nfunction capSize(inst, size) {\n const caps = sizeCaps(inst._zod.def.checks);\n if (!caps.length) return;\n const cap = Math.min(...caps.map((c) => c[0]));\n const parse = inst._zod.parse;\n inst._zod.parse = (payload, ctx) => {\n const n = size(payload.value);\n if (n > cap && !ctx?.skipChecks && ctx?.direction !== \"backward\") {\n for (const [max, ch] of sizeCaps(inst._zod.def.checks)) if (n > max && ch._zod.def.when(payload)) {\n const start = payload.issues.length;\n ch._zod.check(payload);\n payload.issues[start].continue = false;\n attachSchema(payload.issues, start, inst);\n return payload;\n }\n }\n return parse(payload, ctx);\n };\n}\nconst arraySize = (input) => Array.isArray(input) ? input.length : -1;\nfunction handleArrayResult(result, final, index) {\n if (result.issues.length) final.issues.push(...prefixIssues(index, result.issues));\n final.value[index] = result.value;\n}\nconst $ZodArray = /*@__PURE__*/ $constructor(\"$ZodArray\", (inst, def) => {\n $ZodType.init(inst, def);\n const memo = globalConfig.memoizer;\n memo?.attach(inst);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!Array.isArray(input)) {\n payload.issues.push({\n expected: \"array\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n payload.value = memo ? memo.alloc(inst, payload, Array(input.length), ctx) : Array(input.length);\n const proms = [];\n const abortEarly = ctx?.abortEarly;\n for (let i = 0; i < input.length; i++) {\n const item = input[i];\n const result = def.element._zod.run({\n value: item,\n issues: []\n }, ctx);\n if (result instanceof Promise) proms.push(result.then((result) => handleArrayResult(result, payload, i)));\n else {\n handleArrayResult(result, payload, i);\n if (abortEarly && result.issues.length !== 0 && aborted(result)) break;\n }\n }\n if (proms.length) return Promise.all(proms).then(() => payload);\n return payload;\n };\n capSize(inst, arraySize);\n});\nfunction handlePropertyResult(result, final, key, input, optin, optout) {\n const isPresent = key in input;\n const isOptionalOut = optout === \"optional\";\n if (!isPresent && isOptionalOut && optin === \"optional\") return;\n if (result.issues.length) {\n if (optin !== void 0 && isOptionalOut && !isPresent) return;\n final.issues.push(...prefixIssues(key, result.issues));\n }\n if (!isPresent && optin === void 0) {\n if (!result.issues.length) final.issues.push({\n code: \"invalid_type\",\n expected: \"nonoptional\",\n input: void 0,\n path: [key]\n });\n return;\n }\n if (result.value === void 0) {\n if (isPresent || optin === \"defaulted\" && !isOptionalOut) final.value[key] = void 0;\n } else final.value[key] = result.value;\n}\nconst NO_SYMBOL_KEYS = [];\nfunction normalizeDef(def) {\n const keys = Object.keys(def.shape);\n const ownSymbols = Object.getOwnPropertySymbols(def.shape);\n const symbolKeys = ownSymbols.length ? ownSymbols : NO_SYMBOL_KEYS;\n const allKeys = symbolKeys.length ? [...keys, ...symbolKeys] : keys;\n for (const k of allKeys) if (!def.shape?.[k]?._zod?.traits?.has(\"$ZodType\")) throw new Error(`Invalid element at key \"${String(k)}\": expected a Zod schema`);\n const okeys = optionalKeys(def.shape);\n return {\n ...def,\n allKeys,\n symbolKeys,\n keySet: new Set(keys),\n numKeys: keys.length,\n optionalKeys: new Set(okeys)\n };\n}\nfunction handleCatchall(proms, input, payload, ctx, def, inst, abortEarly) {\n const unrecognized = [];\n const keySet = def.keySet;\n const _catchall = def.catchall._zod;\n const t = _catchall.def.type;\n const optin = _catchall.optin;\n const optout = _catchall.optout;\n let seen = 0;\n for (const key in input) {\n if (abortEarly && payload.issues.length !== seen) {\n if (aborted(payload, seen)) break;\n seen = payload.issues.length;\n }\n if (keySet.has(key)) continue;\n if (key === \"__proto__\") {\n if (t === \"never\") unrecognized.push(key);\n continue;\n }\n if (t === \"never\") {\n unrecognized.push(key);\n continue;\n }\n const r = _catchall.run({\n value: input[key],\n issues: []\n }, ctx);\n if (r instanceof Promise) proms.push(r.then((r) => handlePropertyResult(r, payload, key, input, optin, optout)));\n else handlePropertyResult(r, payload, key, input, optin, optout);\n }\n if (unrecognized.length) payload.issues.push({\n code: \"unrecognized_keys\",\n keys: unrecognized,\n input,\n inst,\n continue: true\n });\n if (!proms.length) return payload;\n return Promise.all(proms).then(() => {\n return payload;\n });\n}\nconst $ZodObject = /*@__PURE__*/ $constructor(\"$ZodObject\", (inst, def) => {\n $ZodType.init(inst, def);\n const desc = Object.getOwnPropertyDescriptor(def, \"shape\");\n const sh = desc?.get ? desc.get.raw : def.shape ?? {};\n if (sh) {\n const get = () => {\n const newSh = { ...sh };\n Object.defineProperty(def, \"shape\", { value: newSh });\n get.raw = newSh;\n return newSh;\n };\n get.raw = sh;\n Object.defineProperty(def, \"shape\", { get });\n }\n const _normalized = cached(() => normalizeDef(def));\n defineLazyInternal(inst, \"propValues\", (zod) => {\n const shape = zod.def.shape;\n const propValues = {};\n for (const key in shape) {\n const field = shape[key]._zod;\n if (field.values) {\n if (!Object.prototype.hasOwnProperty.call(propValues, key)) assignProp(propValues, key, /* @__PURE__ */ new Set());\n for (const v of field.values) propValues[key].add(v);\n if (field.optin !== void 0) propValues[key].add(void 0);\n }\n }\n return propValues;\n });\n const isObject$1 = isObject;\n const catchall = def.catchall;\n let value;\n const memo = globalConfig.memoizer;\n memo?.attach(inst);\n inst._zod.parse = (payload, ctx) => {\n value ?? (value = _normalized.value);\n const input = payload.value;\n if (!isObject$1(input)) {\n payload.issues.push({\n expected: \"object\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};\n const proms = [];\n const shape = value.shape;\n const abortEarly = ctx?.abortEarly;\n let seen = payload.issues.length;\n for (const key of value.allKeys) {\n if (abortEarly && payload.issues.length !== seen) {\n if (aborted(payload, seen)) break;\n seen = payload.issues.length;\n }\n if (key === \"__proto__\") continue;\n const el = shape[key];\n const optin = el._zod.optin;\n const optout = el._zod.optout;\n const r = el._zod.run({\n value: input[key],\n issues: []\n }, ctx);\n if (r instanceof Promise) proms.push(r.then((r) => handlePropertyResult(r, payload, key, input, optin, optout)));\n else handlePropertyResult(r, payload, key, input, optin, optout);\n }\n if (!catchall) return proms.length ? Promise.all(proms).then(() => payload) : payload;\n return handleCatchall(proms, input, payload, ctx, _normalized.value, inst, abortEarly === true);\n };\n});\nconst $ZodRecord = /*@__PURE__*/ $constructor(\"$ZodRecord\", (inst, def) => {\n $ZodType.init(inst, def);\n const memo = globalConfig.memoizer;\n memo?.attach(inst);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!isPlainObject(input)) {\n payload.issues.push({\n expected: \"record\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n const proms = [];\n const values = def.keyType._zod.values;\n if (values && !def.partial) {\n payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};\n const recordKeys = /* @__PURE__ */ new Set();\n for (const key of values) if (typeof key === \"string\" || typeof key === \"number\" || typeof key === \"symbol\") {\n recordKeys.add(typeof key === \"number\" ? key.toString() : key);\n if (key === \"__proto__\") continue;\n const keyResult = def.keyType._zod.run({\n value: key,\n issues: []\n }, ctx);\n if (keyResult instanceof Promise) throw new Error(\"Async schemas not supported in object keys currently\");\n if (keyResult.issues.length) {\n payload.issues.push({\n code: \"invalid_key\",\n origin: \"record\",\n issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),\n input: key,\n path: [key],\n inst\n });\n continue;\n }\n const outKey = keyResult.value;\n if (outKey === \"__proto__\") continue;\n const result = def.valueType._zod.run({\n value: input[key],\n issues: []\n }, ctx);\n if (result instanceof Promise) proms.push(result.then((result) => {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }));\n else {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }\n }\n let unrecognized;\n for (const key in input) if (!recordKeys.has(key)) {\n if (def.mode === \"loose\") {\n if (key === \"__proto__\") continue;\n payload.value[key] = input[key];\n } else {\n unrecognized = unrecognized ?? [];\n unrecognized.push(key);\n }\n }\n if (unrecognized && unrecognized.length > 0) payload.issues.push({\n code: \"unrecognized_keys\",\n input,\n inst,\n keys: unrecognized,\n continue: true\n });\n } else {\n payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};\n let unrecognized;\n for (const key of Reflect.ownKeys(input)) {\n if (key === \"__proto__\") continue;\n if (!Object.prototype.propertyIsEnumerable.call(input, key)) continue;\n let keyResult = def.keyType._zod.run({\n value: key,\n issues: []\n }, ctx);\n if (keyResult instanceof Promise) throw new Error(\"Async schemas not supported in object keys currently\");\n if (typeof key === \"string\" && number.test(key) && keyResult.issues.length) {\n const retryResult = def.keyType._zod.run({\n value: Number(key),\n issues: []\n }, ctx);\n if (retryResult instanceof Promise) throw new Error(\"Async schemas not supported in object keys currently\");\n if (retryResult.issues.length === 0) keyResult = retryResult;\n }\n if (keyResult.issues.length) {\n if (def.mode === \"loose\") payload.value[key] = input[key];\n else if (values) {\n unrecognized = unrecognized ?? [];\n unrecognized.push(key);\n } else payload.issues.push({\n code: \"invalid_key\",\n origin: \"record\",\n issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),\n input: key,\n path: [key],\n inst\n });\n continue;\n }\n const outKey = keyResult.value;\n if (outKey === \"__proto__\") continue;\n const result = def.valueType._zod.run({\n value: input[key],\n issues: []\n }, ctx);\n if (result instanceof Promise) proms.push(result.then((result) => {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }));\n else {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }\n }\n if (unrecognized && unrecognized.length > 0) payload.issues.push({\n code: \"unrecognized_keys\",\n input,\n inst,\n keys: unrecognized,\n continue: true\n });\n }\n if (proms.length) return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handleOptionalResult(payload, result) {\n payload.value = result.issues.length ? void 0 : result.value;\n return payload;\n}\nconst $ZodOptional = /*@__PURE__*/ $constructor(\"$ZodOptional\", (inst, def) => {\n $ZodType.init(inst, def);\n defineLazyInternal(inst, \"optin\", (zod) => zod.def.innerType._zod.optin === \"defaulted\" ? \"defaulted\" : \"optional\");\n inst._zod.optout = \"optional\";\n defineLazyInternal(inst, \"values\", (zod) => {\n const values = zod.def.innerType._zod.values;\n return values ? /* @__PURE__ */ new Set([...values, void 0]) : void 0;\n });\n defineLazyInternal(inst, \"pattern\", (zod) => {\n const pattern = zod.def.innerType._zod.pattern;\n return pattern ? new RegExp(`^(${cleanRegex(pattern.source)})?$`) : void 0;\n });\n inst._zod.parse = (payload, ctx) => {\n if (payload.value === void 0) {\n if (def.innerType._zod.optin !== \"defaulted\") return payload;\n const result = def.innerType._zod.run({\n value: payload.value,\n issues: []\n }, ctx);\n if (result instanceof Promise) return result.then((result) => handleOptionalResult(payload, result));\n return handleOptionalResult(payload, result);\n }\n return def.innerType._zod.run(payload, ctx);\n };\n});\nconst $ZodDefault = /*@__PURE__*/ $constructor(\"$ZodDefault\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.optin = \"defaulted\";\n defineLazyInternal(inst, \"values\", (zod) => zod.def.innerType._zod.values);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") return def.innerType._zod.run(payload, ctx);\n if (payload.value === void 0) {\n payload.value = def.defaultValue;\n return payload;\n }\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) return result.then((result) => handleDefaultResult(result, def));\n return handleDefaultResult(result, def);\n };\n});\nfunction handleDefaultResult(payload, def) {\n if (payload.value === void 0) payload.value = def.defaultValue;\n return payload;\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/locales/en.js\nconst error = () => {\n const Sizable = {\n string: {\n unit: \"characters\",\n verb: \"to have\"\n },\n file: {\n unit: \"bytes\",\n verb: \"to have\"\n },\n array: {\n unit: \"items\",\n verb: \"to have\"\n },\n set: {\n unit: \"items\",\n verb: \"to have\"\n },\n map: {\n unit: \"entries\",\n verb: \"to have\"\n }\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const FormatDictionary = {\n regex: \"input\",\n email: \"email address\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO datetime\",\n date: \"ISO date\",\n time: \"ISO time\",\n duration: \"ISO duration\",\n ipv4: \"IPv4 address\",\n ipv6: \"IPv6 address\",\n mac: \"MAC address\",\n cidrv4: \"IPv4 range\",\n cidrv6: \"IPv6 range\",\n base64: \"base64-encoded string\",\n base64url: \"base64url-encoded string\",\n json_string: \"JSON string\",\n e164: \"E.164 number\",\n currency_code: \"currency code\",\n credit_card: \"credit card number\",\n iban: \"IBAN\",\n jwt: \"JWT\",\n template_literal: \"input\"\n };\n const TypeDictionary = { nan: \"NaN\" };\n function getTypeName(type, input) {\n if (type === \"number\" && typeof input === \"number\" && !Number.isFinite(input)) return String(input);\n return TypeDictionary[type] ?? type;\n }\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\": return `Invalid input: expected ${getTypeName(issue.expected)}, received ${getTypeName(parsedType(issue.input), issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1) return `Invalid input: expected ${stringifyPrimitive(issue.values[0])}`;\n return `Invalid option: expected one of ${joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.exact ? \"exactly \" : issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) return `Too big: expected ${issue.origin ?? \"value\"} to have ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elements\"}`;\n return `Too big: expected ${issue.origin ?? \"value\"} to be ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.exact ? \"exactly \" : issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) return `Too small: expected ${issue.origin} to have ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n return `Too small: expected ${issue.origin} to be ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") return `Invalid string: must start with \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\") return `Invalid string: must end with \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\") return `Invalid string: must include \"${_issue.includes}\"`;\n if (_issue.format === \"regex\") return `Invalid string: must match pattern ${_issue.pattern}`;\n return `Invalid ${FormatDictionary[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\": return `Invalid number: must be a multiple of ${issue.divisor}`;\n case \"unrecognized_keys\": return `Unrecognized key${issue.keys.length > 1 ? \"s\" : \"\"}: ${joinValues(issue.keys, \", \")}`;\n case \"invalid_key\": return `Invalid key in ${issue.origin}`;\n case \"invalid_union\":\n if (issue.options && Array.isArray(issue.options) && issue.options.length > 0) return `Invalid discriminator value. Expected ${issue.options.map((o) => `'${o}'`).join(\" | \")}`;\n if (issue.inclusive === false) return \"Invalid input: more than one option matched\";\n return \"Invalid input\";\n case \"invalid_element\": return `Invalid value in ${issue.origin}`;\n default: return `Invalid input`;\n }\n };\n};\nfunction en_default() {\n return { localeError: error() };\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/core/api.js\nfunction snapshotChecks(def) {\n if (def.checks) def.checks = [...def.checks];\n return def;\n}\n// @__NO_SIDE_EFFECTS__\nfunction _string(Class, params) {\n return new Class(snapshotChecks({\n type: \"string\",\n ...normalizeParams(params)\n }));\n}\n// @__NO_SIDE_EFFECTS__\nfunction _unknown(Class) {\n return new Class({ type: \"unknown\" });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _minLength(minimum, params) {\n return new $ZodCheckMinLength({\n check: \"min_length\",\n ...normalizeParams(params),\n minimum\n });\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/mini/schemas.js\nconst ZodMiniType = /*@__PURE__*/ $constructor(\"ZodMiniType\", (inst, def) => {\n if (!inst._zod) throw new Error(\"Uninitialized schema in ZodMiniType.\");\n $ZodType.init(inst, def);\n inst.def = def;\n inst.type = def.type;\n}, {\n get with() {\n return this.check;\n },\n set with(value) {\n own(this, \"with\", value);\n },\n parse(data, params) {\n return parse(this, data, params, { callee: this.parse });\n },\n parseAsync(data, params) {\n return parseAsync(this, data, params, { callee: this.parseAsync });\n },\n safeParse(data, params) {\n return safeParse(this, data, params);\n },\n safeParseAsync(data, params) {\n return safeParseAsync(this, data, params);\n },\n check(...checks) {\n const def = this.def;\n return this.clone({\n ...def,\n checks: [...def.checks ?? [], ...checks.map((ch) => typeof ch === \"function\" ? { _zod: {\n check: ch,\n def: { check: \"custom\" },\n onattach: []\n } } : ch)]\n }, { parent: true });\n },\n clone(_def, params) {\n return clone(this, _def, params);\n },\n brand() {\n return this;\n },\n register(reg, meta) {\n reg.add(this, meta);\n return this;\n },\n apply(fn, ...args) {\n return args.length === 0 ? fn(this) : fn(this, ...args);\n }\n});\nconst ZodMiniString = /*@__PURE__*/ $constructor(\"ZodMiniString\", (inst, def) => {\n $ZodString.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction string(params) {\n return _string(ZodMiniString, params);\n}\nconst ZodMiniUnknown = /*@__PURE__*/ $constructor(\"ZodMiniUnknown\", (inst, def) => {\n $ZodUnknown.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction unknown() {\n return _unknown(ZodMiniUnknown);\n}\nconst ZodMiniArray = /*@__PURE__*/ $constructor(\"ZodMiniArray\", (inst, def) => {\n $ZodArray.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction array(element, params) {\n return new ZodMiniArray({\n type: \"array\",\n element,\n ...normalizeParams(params)\n });\n}\nconst ZodMiniObject = /*@__PURE__*/ $constructor(\"ZodMiniObject\", (inst, def) => {\n $ZodObject.init(inst, def);\n ZodMiniType.init(inst, def);\n installLazyProp(inst, \"shape\", (self) => self._zod.def.shape, false);\n});\n// @__NO_SIDE_EFFECTS__\nfunction object(shape, params) {\n const def = {\n type: \"object\",\n shape: shape ?? {},\n ...normalizeParams(params)\n };\n return new ZodMiniObject(def);\n}\nconst ZodMiniRecord = /*@__PURE__*/ $constructor(\"ZodMiniRecord\", (inst, def) => {\n $ZodRecord.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction record(keyType, valueType, params) {\n if (!valueType || !valueType._zod) return new ZodMiniRecord({\n type: \"record\",\n keyType: /* @__PURE__ */ string(),\n valueType: keyType,\n ...normalizeParams(valueType)\n });\n return new ZodMiniRecord({\n type: \"record\",\n keyType,\n valueType,\n ...normalizeParams(params)\n });\n}\nconst ZodMiniOptional = /*@__PURE__*/ $constructor(\"ZodMiniOptional\", (inst, def) => {\n $ZodOptional.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction optional(innerType) {\n return new ZodMiniOptional({\n type: \"optional\",\n innerType\n });\n}\nconst ZodMiniDefault = /*@__PURE__*/ $constructor(\"ZodMiniDefault\", (inst, def) => {\n $ZodDefault.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction _default(innerType, defaultValue) {\n return new ZodMiniDefault({\n type: \"default\",\n innerType,\n get defaultValue() {\n return typeof defaultValue === \"function\" ? defaultValue() : shallowClone(defaultValue);\n }\n });\n}\n\n//#endregion\n//#region src/lib/hook-io.ts\nconfig(en_default());\nconst HookInputSchema = object({\n session_id: string(),\n transcript_path: string(),\n cwd: string(),\n permission_mode: string(),\n hook_event_name: string(),\n tool_name: string(),\n tool_input: record(string(), unknown())\n});\nasync function readHookInput() {\n const chunks = [];\n for await (const chunk of process.stdin) chunks.push(typeof chunk === \"string\" ? chunk : String(chunk));\n let parsed;\n try {\n parsed = JSON.parse(chunks.join(\"\"));\n } catch (_error) {\n return {\n ok: false,\n raw: chunks.join(\"\").slice(0, 200),\n error: \"invalid JSON\"\n };\n }\n const result = HookInputSchema.safeParse(parsed);\n if (!result.success) return {\n ok: false,\n raw: parsed,\n error: result.error.message\n };\n return {\n ok: true,\n data: result.data\n };\n}\nconst BashInputSchema = object({\n command: string(),\n description: optional(string())\n});\nconst FilePathInputSchema = object({ file_path: string() });\nconst SearchInputSchema = object({\n pattern: string(),\n path: optional(string())\n});\nfunction parseBashInput(toolInput) {\n const result = BashInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction parseFilePathInput(toolInput) {\n const result = FilePathInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction parseSearchInput(toolInput) {\n const result = SearchInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction writeDeny(reason) {\n const decision = { hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"deny\",\n ...reason ? { permissionDecisionReason: reason } : {}\n } };\n process.stdout.write(JSON.stringify(decision));\n}\nfunction writeAdvice(advice) {\n const decision = { hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n additionalContext: advice\n } };\n process.stdout.write(JSON.stringify(decision));\n}\n\n//#endregion\n//#region src/lib/audit.ts\nconst MAX_LOG_SIZE = 1048576;\nfunction getLogPath() {\n const hookDir = path.dirname(fileURLToPath(import.meta.url));\n return path.join(hookDir, \"bst-hook.audit.local.jsonl\");\n}\nfunction rotateIfNeeded(logPath) {\n try {\n if (fs.statSync(logPath).size > MAX_LOG_SIZE) {\n const rotatedPath = `${logPath}.1`;\n fs.renameSync(logPath, rotatedPath);\n }\n } catch (_error) {}\n}\nlet rotationChecked = false;\nfunction audit(entry) {\n try {\n const logPath = getLogPath();\n if (!rotationChecked) {\n rotateIfNeeded(logPath);\n rotationChecked = true;\n }\n const record = {\n timestamp: (/* @__PURE__ */ new Date()).toISOString(),\n ...entry\n };\n fs.appendFileSync(logPath, JSON.stringify(record) + \"\\n\");\n } catch (err) {\n process.stderr.write(`[bst-hook] audit write failed: ${err instanceof Error ? err.message : String(err)}\\n`);\n }\n}\n\n//#endregion\n//#region src/lib/shell-tokenizer.ts\nfunction scanDoubleQuoted(input, start) {\n let content = \"\";\n let i = start;\n while (i < input.length && input[i] !== \"\\\"\") {\n if (input[i] === \"\\\\\" && i + 1 < input.length) {\n const next = input[i + 1];\n if (next === \"\\\"\" || next === \"\\\\\" || next === \"$\" || next === \"`\") {\n content += next;\n i += 2;\n continue;\n }\n }\n content += input[i];\n i++;\n }\n if (i >= input.length) return null;\n return {\n content,\n next: i + 1\n };\n}\nfunction tokenize(input) {\n const tokens = [];\n let current = \"\";\n let i = 0;\n let inToken = false;\n while (i < input.length) {\n const ch = input[i];\n if (ch === \"'\") {\n inToken = true;\n i++;\n while (i < input.length && input[i] !== \"'\") {\n current += input[i];\n i++;\n }\n if (i >= input.length) return {\n ok: false,\n error: \"unterminated single quote\"\n };\n i++;\n } else if (ch === \"\\\"\") {\n inToken = true;\n const scanned = scanDoubleQuoted(input, i + 1);\n if (!scanned) return {\n ok: false,\n error: \"unterminated double quote\"\n };\n current += scanned.content;\n i = scanned.next;\n } else if (ch === \"\\\\\" && i + 1 < input.length) {\n inToken = true;\n current += input[i + 1];\n i += 2;\n } else if (ch === \" \" || ch === \" \") {\n if (inToken) {\n tokens.push(current);\n current = \"\";\n inToken = false;\n }\n i++;\n } else {\n inToken = true;\n current += ch;\n i++;\n }\n }\n if (inToken) tokens.push(current);\n return {\n ok: true,\n tokens\n };\n}\nconst SAFE_UNQUOTED_RE = /^[A-Za-z0-9_\\-./=:@%+,]+$/;\nfunction quoteToken(token) {\n if (token.length > 0 && SAFE_UNQUOTED_RE.test(token)) return token;\n return \"'\" + token.replace(/'/g, \"'\\\\''\") + \"'\";\n}\nfunction joinTokens(tokens) {\n return tokens.map(quoteToken).join(\" \");\n}\n\n//#endregion\n//#region src/lib/shell-splitter.ts\nfunction splitCommand(input) {\n if (!input || !input.trim()) return [];\n const segments = [];\n let current = \"\";\n let i = 0;\n let inSingleQuote = false;\n let inDoubleQuote = false;\n while (i < input.length) {\n const ch = input[i];\n if (ch === \"'\" && !inDoubleQuote) {\n inSingleQuote = !inSingleQuote;\n current += ch;\n i++;\n continue;\n }\n if (ch === \"\\\"\" && !inSingleQuote) {\n inDoubleQuote = !inDoubleQuote;\n current += ch;\n i++;\n continue;\n }\n if (ch === \"\\\\\" && !inSingleQuote && i + 1 < input.length) {\n current += ch + input[i + 1];\n i += 2;\n continue;\n }\n if (!inSingleQuote && !inDoubleQuote) {\n if (ch === \"&\" && i + 1 < input.length && input[i + 1] === \"&\") {\n pushSegment(segments, current, \"&&\");\n current = \"\";\n i += 2;\n continue;\n }\n if (ch === \"&\" && !(i + 1 < input.length && input[i + 1] === \">\") && !(i > 0 && (input[i - 1] === \">\" || input[i - 1] === \"<\"))) {\n pushSegment(segments, current, \"&\");\n current = \"\";\n i++;\n continue;\n }\n if (ch === \"|\" && i + 1 < input.length && input[i + 1] === \"|\") {\n pushSegment(segments, current, \"||\");\n current = \"\";\n i += 2;\n continue;\n }\n if (ch === \"|\") {\n pushSegment(segments, current, \"|\");\n current = \"\";\n i++;\n continue;\n }\n if (ch === \";\") {\n pushSegment(segments, current, \";\");\n current = \"\";\n i++;\n continue;\n }\n }\n current += ch;\n i++;\n }\n const trimmed = current.trim();\n if (trimmed) segments.push({\n command: trimmed,\n operator: null\n });\n return segments;\n}\nfunction pushSegment(segments, text, operator) {\n const trimmed = text.trim();\n if (trimmed) segments.push({\n command: trimmed,\n operator\n });\n}\n\n//#endregion\n//#region src/lib/injection-guard.ts\nfunction hasBraceExpansion(token) {\n let depth = 0;\n for (let i = 0; i < token.length; i++) {\n const ch = token[i];\n if (ch === \"{\") depth++;\n else if (ch === \"}\") {\n if (depth > 0) depth--;\n } else if (depth >= 1 && (ch === \",\" || ch === \".\" && token[i + 1] === \".\")) return true;\n }\n return false;\n}\nfunction checkInjection(tokens, rawSegment) {\n if (/[\\n\\r]/.test(rawSegment)) return {\n safe: false,\n reason: \"unquoted newline in command\"\n };\n for (const token of tokens) {\n if (token === \"`\") return {\n safe: false,\n reason: \"backtick command substitution\"\n };\n if (token.includes(\"`\")) return {\n safe: false,\n reason: \"backtick command substitution\"\n };\n if (token.includes(\"$\")) return {\n safe: false,\n reason: \"unresolved dollar-expansion\"\n };\n if (hasBraceExpansion(token)) return {\n safe: false,\n reason: \"brace expansion\"\n };\n if (token.includes(\"<(\")) return {\n safe: false,\n reason: \"process substitution\"\n };\n if (token.includes(\">(\")) return {\n safe: false,\n reason: \"process substitution\"\n };\n }\n return { safe: true };\n}\n\n//#endregion\n//#region src/lib/env-file.ts\nconst ENV_SAFE_SUFFIXES = [\n \".sample\",\n \".example\",\n \".template\"\n];\nconst ENV_TOOL_OWNED_SUFFIXES = [\".check.env\"];\nconst ENV_FILE_PATTERN = /(^|[/\\\\])(\\.env(\\..*)?|.*\\.env(\\..*)?|\\.env)$/;\nfunction isEnvSafeSuffix(filename) {\n return [...ENV_SAFE_SUFFIXES, ...ENV_TOOL_OWNED_SUFFIXES].some((suffix) => filename.endsWith(suffix));\n}\nfunction isEnvFile(filePath) {\n if (isEnvSafeSuffix(path.basename(filePath))) return false;\n return ENV_FILE_PATTERN.test(filePath);\n}\n\n//#endregion\n//#region src/lib/git-state.ts\nconst GIT_TIMEOUT_MS = 5e3;\nfunction getCurrentBranch(cwd) {\n try {\n const out = execFileSync(\"git\", [\n \"rev-parse\",\n \"--abbrev-ref\",\n \"HEAD\"\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!out || out === \"HEAD\") return null;\n return out;\n } catch {\n return null;\n }\n}\nfunction getRepoTopLevel(cwd) {\n try {\n const out = execFileSync(\"git\", [\"rev-parse\", \"--show-toplevel\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n return out === \"\" ? null : out;\n } catch {\n return null;\n }\n}\nfunction getRemoteDefaultBranch(cwd) {\n try {\n const out = execFileSync(\"git\", [\n \"symbolic-ref\",\n \"--short\",\n \"refs/remotes/origin/HEAD\"\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!out) return null;\n return out.startsWith(\"origin/\") ? out.slice(7) : out;\n } catch {\n return null;\n }\n}\nconst DefaultBranchSchema = object({ defaultBranch: optional(string().check(_minLength(1))) });\nfunction getConfiguredDefaultBranch(topLevel) {\n let parsed;\n try {\n parsed = JSON.parse(readFileSync(join(topLevel, \".tooling.json\"), \"utf-8\"));\n } catch {\n return null;\n }\n const result = DefaultBranchSchema.safeParse(parsed);\n return result.success ? result.data.defaultBranch ?? null : null;\n}\nconst ALWAYS_PROTECTED = [\"main\", \"master\"];\nfunction getProtectedBranches(cwd, extra) {\n const set = new Set(ALWAYS_PROTECTED);\n const remote = getRemoteDefaultBranch(cwd);\n if (remote) set.add(remote);\n for (const b of extra) set.add(b);\n return set;\n}\nfunction getUpstream(cwd, branch) {\n try {\n return execFileSync(\"git\", [\n \"rev-parse\",\n \"--abbrev-ref\",\n `${branch}@{upstream}`\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: 5e3\n }).toString().trim() || null;\n } catch {\n return null;\n }\n}\nfunction resolveCommit(cwd, rev) {\n try {\n return execFileSync(\"git\", [\n \"rev-parse\",\n \"--verify\",\n \"--quiet\",\n `${rev}^{commit}`\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: 5e3\n }).toString().trim() || null;\n } catch {\n return null;\n }\n}\nfunction branchCarriesOtherFiles(cwd, allowed) {\n const toplevel = getRepoTopLevel(cwd);\n if (toplevel === null) return null;\n try {\n const defaultBranch = getConfiguredDefaultBranch(toplevel) ?? getRemoteDefaultBranch(toplevel) ?? \"main\";\n const mergeBase = execFileSync(\"git\", [\n \"merge-base\",\n \"HEAD\",\n `origin/${defaultBranch}`\n ], {\n cwd: toplevel,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!mergeBase) return null;\n const names = (args) => execFileSync(\"git\", args, {\n cwd: toplevel,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().split(\"\\n\").filter((line) => line.length > 0);\n return [...names([\n \"diff\",\n \"--name-only\",\n mergeBase\n ]), ...names([\n \"ls-files\",\n \"--others\",\n \"--exclude-standard\"\n ])].some((name) => !allowed.includes(name));\n } catch {\n return null;\n }\n}\nfunction isPrimaryWorktree(cwd) {\n try {\n const gitDir = execFileSync(\"git\", [\"rev-parse\", \"--absolute-git-dir\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n const commonRaw = execFileSync(\"git\", [\"rev-parse\", \"--git-common-dir\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!gitDir || !commonRaw) return false;\n const commonDir = resolve(cwd, commonRaw);\n return realpathSync(gitDir) === realpathSync(commonDir);\n } catch {\n return false;\n }\n}\n\n//#endregion\n//#region src/lib/git-short-options.ts\nconst VALUE_TAKING_LETTERS = {\n switch: /* @__PURE__ */ new Set([\"c\", \"C\"]),\n checkout: /* @__PURE__ */ new Set([\"b\", \"B\"]),\n branch: /* @__PURE__ */ new Set([\"u\"]),\n fetch: /* @__PURE__ */ new Set([\"o\", \"j\"])\n};\nfunction splitShortOptionCluster(token, sub) {\n if (!token.startsWith(\"-\") || token.startsWith(\"--\") || token.length <= 1) return;\n const valueLetters = VALUE_TAKING_LETTERS[sub];\n const letters = [];\n const chars = token.slice(1);\n for (let i = 0; i < chars.length; i++) {\n const ch = chars[i];\n letters.push(ch);\n if (valueLetters.has(ch)) return {\n letters,\n attachedValue: chars.slice(i + 1)\n };\n }\n return { letters };\n}\n\n//#endregion\n//#region src/lib/path-scope.ts\nfunction pathFlavorFor(base) {\n return /^[a-zA-Z]:[\\\\/]/.test(base) ? path.win32 : path.posix;\n}\nfunction resolveIn(base, target) {\n return pathFlavorFor(base).resolve(base, target);\n}\nfunction isWithin(dir, target) {\n const pp = pathFlavorFor(dir);\n const rel = pp.relative(dir, target);\n if (rel === \"\") return true;\n if (rel === \"..\" || rel.startsWith(\"..\" + pp.sep)) return false;\n return !pp.isAbsolute(rel);\n}\nfunction samePath(a, b) {\n return pathFlavorFor(a).relative(a, b) === \"\";\n}\nfunction targetsGitDir(filePath, cwd) {\n const pp = pathFlavorFor(cwd);\n const resolved = resolveIn(cwd, filePath);\n if (!isWithin(cwd, resolved)) return false;\n const rel = pp.relative(cwd, resolved);\n if (rel === \"\") return false;\n return rel.split(pp.sep).includes(\".git\");\n}\nfunction targetsNodeModules(filePath, cwd) {\n return resolveIn(cwd, filePath).split(/[/\\\\]/).includes(\"node_modules\");\n}\n\n//#endregion\n//#region src/rules.ts\nconst GIT_FLAGS_WITH_ARG = /* @__PURE__ */ new Set([\n \"-C\",\n \"-c\",\n \"--git-dir\",\n \"--work-tree\"\n]);\nfunction getGitSubcommand(tokens) {\n const args = tokens.slice(1);\n for (let i = 0; i < args.length; i++) {\n const t = args[i];\n if (GIT_FLAGS_WITH_ARG.has(t)) {\n i++;\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n return t;\n }\n}\nconst WORKTREE_ADVICE = \"You're creating a feature branch in the primary worktree. An isolated worktree keeps this work independent of other windows, and with no commits on this branch yet, converting is cheap -- `git worktree add ../<dir> -b <branch>` (a sibling outside the repo, with its own dependency install). Claude Code keeps this session inside its own working tree, so the work continues in a new session opened in the sibling worktree. If you cannot open a session there, stay in the primary checkout on this branch and check `git branch --show-current` before each commit.\";\nfunction isGitBranchCreation(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"checkout\" && sub !== \"switch\") return false;\n const after = tokens.slice(tokens.indexOf(sub) + 1);\n const creationLetters = sub === \"checkout\" ? [\"b\", \"B\"] : [\"c\", \"C\"];\n return after.some((t) => {\n const cluster = splitShortOptionCluster(t, sub);\n return cluster !== void 0 && cluster.letters.some((l) => creationLetters.includes(l));\n });\n}\nfunction worktreeAdvice(tokens, ctx) {\n return isGitBranchCreation(tokens) && isPrimaryWorktree(ctx.cwd) ? WORKTREE_ADVICE : void 0;\n}\nconst GIT_FORCE_CREATE_LETTER = {\n switch: \"C\",\n checkout: \"B\"\n};\nconst GIT_FORCE_CREATE_LONG = {\n flag: \"--force-create\",\n shortest: \"--force-\"\n};\nfunction gitForceCreateTargets(after, letterSub) {\n const letter = GIT_FORCE_CREATE_LETTER[letterSub];\n const longFlag = GIT_FORCE_CREATE_LONG;\n const targets = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (letterSub === \"switch\") {\n const [flag, ...rest] = t.split(\"=\");\n if (flag !== void 0 && flag.length >= longFlag.shortest.length && longFlag.flag.startsWith(flag)) {\n const target = rest.length > 0 ? rest.join(\"=\") : after[i + 1];\n if (target !== void 0) targets.push(target);\n continue;\n }\n }\n const cluster = splitShortOptionCluster(t, letterSub);\n if (cluster === void 0 || !cluster.letters.includes(letter)) continue;\n if (cluster.attachedValue !== void 0 && cluster.attachedValue !== \"\") {\n targets.push(cluster.attachedValue);\n continue;\n }\n const next = after[i + 1];\n if (next !== void 0) targets.push(next);\n }\n return targets;\n}\nfunction gitCommandForceCreateTargets(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub === \"switch\" || sub === \"checkout\") return gitForceCreateTargets(tokens.slice(tokens.indexOf(sub) + 1), sub);\n if (sub === \"worktree\") {\n const worktreeIdx = tokens.indexOf(\"worktree\");\n if (tokens[worktreeIdx + 1] !== \"add\") return [];\n return gitForceCreateTargets(tokens.slice(worktreeIdx + 2), \"checkout\");\n }\n return [];\n}\nfunction isGitProtectedBranchForceCreate(tokens, ctx) {\n const targets = gitCommandForceCreateTargets(tokens);\n if (targets.length === 0) return false;\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n return targets.some((t) => protectedSet.has(t));\n}\nfunction isGitProtectedBranchRefWrite(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"branch\") return false;\n const { flags, positionals } = splitGitBranchArgs(tokens.slice(tokens.indexOf(sub) + 1));\n if (positionals.length === 0) return false;\n let deleteMode = false;\n let renameMode = false;\n let copyMode = false;\n let forceMode = false;\n for (const flag of flags) {\n if (flag.letters !== void 0) {\n if (flag.letters.includes(\"d\") || flag.letters.includes(\"D\")) deleteMode = true;\n if (flag.letters.includes(\"m\") || flag.letters.includes(\"M\")) renameMode = true;\n if (flag.letters.includes(\"c\") || flag.letters.includes(\"C\")) copyMode = true;\n if (flag.letters.includes(\"f\") || flag.letters.includes(\"M\") || flag.letters.includes(\"C\")) forceMode = true;\n continue;\n }\n if (flag.name === \"delete\") deleteMode = true;\n if (flag.name === \"move\") renameMode = true;\n if (flag.name === \"copy\") copyMode = true;\n if (flag.name === \"force\") forceMode = true;\n }\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n if (deleteMode && positionals.some((p) => protectedSet.has(p))) return true;\n if (renameMode && positionals.length >= 2 && protectedSet.has(positionals[0])) return true;\n if (forceMode) {\n const destination = renameMode || copyMode ? positionals[positionals.length - 1] : positionals[0];\n if (destination !== void 0 && protectedSet.has(destination)) return true;\n }\n return false;\n}\nfunction refspecDestination(refspec) {\n const spec = refspec.startsWith(\"+\") ? refspec.slice(1) : refspec;\n const colon = spec.lastIndexOf(\":\");\n return (colon === -1 ? spec : spec.slice(colon + 1)).replace(/^refs\\/heads\\//, \"\");\n}\nconst GIT_BRANCH_VALUE_FLAG_NAMES = /* @__PURE__ */ new Set([\n \"contains\",\n \"no-contains\",\n \"merged\",\n \"no-merged\",\n \"points-at\",\n \"sort\",\n \"format\",\n \"set-upstream-to\"\n]);\nfunction splitGitBranchArgs(after) {\n const flags = [];\n const positionals = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (t.startsWith(\"--\") && t !== \"--\") {\n const eq = t.indexOf(\"=\");\n const name = eq > 0 ? t.slice(2, eq) : t.slice(2);\n flags.push({\n raw: t,\n name\n });\n if (eq < 0 && GIT_BRANCH_VALUE_FLAG_NAMES.has(name)) i++;\n continue;\n }\n const cluster = splitShortOptionCluster(t, \"branch\");\n if (cluster === void 0) {\n flags.push({ raw: t });\n continue;\n }\n flags.push({\n raw: t,\n letters: cluster.letters\n });\n if (cluster.attachedValue === \"\") i++;\n }\n return {\n flags,\n positionals\n };\n}\nconst GIT_FETCH_VALUE_FLAG_NAMES = /* @__PURE__ */ new Set([\n \"depth\",\n \"deepen\",\n \"shallow-since\",\n \"shallow-exclude\",\n \"negotiation-tip\",\n \"filter\",\n \"jobs\",\n \"server-option\",\n \"upload-pack\",\n \"refmap\",\n \"submodule-prefix\",\n \"recurse-submodules-default\"\n]);\nfunction splitGitFetchArgs(after) {\n const flags = [];\n const positionals = [];\n const refmaps = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (t === \"--\") {\n flags.push({ raw: t });\n continue;\n }\n if (t.startsWith(\"--\")) {\n const eq = t.indexOf(\"=\");\n const name = eq > 0 ? t.slice(2, eq) : t.slice(2);\n flags.push({\n raw: t,\n name\n });\n if (name === \"refmap\") {\n if (eq > 0) refmaps.push(t.slice(eq + 1));\n else if (after[i + 1] !== void 0) refmaps.push(after[i + 1]);\n }\n if (eq < 0 && GIT_FETCH_VALUE_FLAG_NAMES.has(name)) i++;\n continue;\n }\n const cluster = splitShortOptionCluster(t, \"fetch\");\n if (cluster === void 0) {\n flags.push({ raw: t });\n continue;\n }\n flags.push({\n raw: t,\n letters: cluster.letters\n });\n if (cluster.attachedValue === \"\") i++;\n }\n return {\n flags,\n positionals,\n refmaps\n };\n}\nfunction fetchRefspecTarget(spec) {\n const body = spec.startsWith(\"+\") ? spec.slice(1) : spec;\n if (body.startsWith(\"^\")) return { kind: \"none\" };\n if (!body.includes(\":\")) return { kind: \"none\" };\n const destination = refspecDestination(body);\n if (destination === \"\") return { kind: \"none\" };\n if (destination.startsWith(\"refs/remotes/\")) return { kind: \"tracking\" };\n if (destination.startsWith(\"refs/\")) return { kind: \"other\" };\n return {\n kind: \"branch\",\n name: destination\n };\n}\nfunction globToAnchoredRegex(glob) {\n const escaped = glob.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return new RegExp(`^${escaped.replace(/\\\\\\*/g, \".*\")}$`);\n}\nfunction isProtectedFetchBranch(name, protectedSet) {\n if (protectedSet.has(name)) return true;\n if (!name.includes(\"*\")) return false;\n const pattern = globToAnchoredRegex(name);\n for (const member of protectedSet) if (pattern.test(member)) return true;\n return false;\n}\nfunction isGitProtectedBranchFetchWrite(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"fetch\") return false;\n const { positionals, refmaps } = splitGitFetchArgs(tokens.slice(tokens.indexOf(sub) + 1));\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n return [...positionals, ...refmaps].some((spec) => {\n const target = fetchRefspecTarget(spec);\n if (target.kind !== \"branch\" || target.name === void 0) return false;\n return isProtectedFetchBranch(target.name, protectedSet);\n });\n}\nfunction isGitProtectedUpdateRef(tokens, ctx) {\n const after = tokens.slice(tokens.indexOf(\"update-ref\") + 1);\n let ref;\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === \"-m\") {\n i++;\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n ref = t;\n break;\n }\n if (ref === void 0) return false;\n return getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []).has(ref.replace(/^refs\\/heads\\//, \"\"));\n}\nfunction gitEffectiveCwd(tokens, cwd) {\n let dir = cwd;\n const args = tokens.slice(1);\n for (let i = 0; i < args.length; i++) {\n const t = args[i];\n if (t === \"-C\") {\n const next = args[i + 1];\n if (next !== void 0) dir = path.resolve(dir, next);\n i++;\n continue;\n }\n if (GIT_FLAGS_WITH_ARG.has(t)) {\n i++;\n continue;\n }\n if (!t.startsWith(\"-\")) break;\n }\n return dir;\n}\nconst GIT_SWITCH_NEUTRAL_FLAGS = /* @__PURE__ */ new Set([\n \"-q\",\n \"--quiet\",\n \"-f\",\n \"--force\",\n \"--discard-changes\",\n \"-m\",\n \"--merge\",\n \"--no-track\",\n \"--progress\",\n \"--no-progress\",\n \"--guess\",\n \"--no-guess\",\n \"--ignore-other-worktrees\",\n \"--recurse-submodules\",\n \"--no-recurse-submodules\"\n]);\nconst GIT_SWITCH_CREATE_FLAGS = /* @__PURE__ */ new Set([\n \"--create\",\n \"--force-create\",\n \"--orphan\"\n]);\nconst GIT_CHECKOUT_PATH_FLAGS = /* @__PURE__ */ new Set([\n \"--ours\",\n \"--theirs\",\n \"-p\",\n \"--patch\",\n \"--conflict\",\n \"--pathspec-from-file\",\n \"--pathspec-file-nul\",\n \"--overlay\",\n \"--no-overlay\"\n]);\nconst GIT_SWITCH_NEUTRAL_LETTERS = /* @__PURE__ */ new Set([\n \"q\",\n \"f\",\n \"m\"\n]);\nfunction switchTarget(after, sub, isCheckoutBranch) {\n if (sub === \"checkout\" && after.includes(\"--\")) return void 0;\n const createLetters = sub === \"checkout\" ? [\"b\", \"B\"] : [\"c\", \"C\"];\n const known = (l) => GIT_SWITCH_NEUTRAL_LETTERS.has(l) || createLetters.includes(l);\n const positionals = [];\n let created;\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (GIT_SWITCH_NEUTRAL_FLAGS.has(t)) continue;\n const eq = t.indexOf(\"=\");\n if (sub === \"checkout\" && GIT_CHECKOUT_PATH_FLAGS.has(eq === -1 ? t : t.slice(0, eq))) return;\n if (GIT_SWITCH_CREATE_FLAGS.has(eq === -1 ? t : t.slice(0, eq))) {\n created = eq === -1 ? after[++i] : t.slice(eq + 1);\n continue;\n }\n const cluster = splitShortOptionCluster(t, sub);\n if (cluster === void 0 || !cluster.letters.every(known)) return null;\n if (cluster.letters.some((l) => createLetters.includes(l))) created = cluster.attachedValue ? cluster.attachedValue : after[++i];\n }\n if (created !== void 0) return created;\n if (positionals.length !== 1) return void 0;\n const target = positionals[0];\n if (target === \"HEAD\" || target === \"@\") return void 0;\n if (sub === \"checkout\" && !isCheckoutBranch(target)) return void 0;\n return target;\n}\nfunction branchSwitch(tokens, ctx, resolved) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"switch\" && sub !== \"checkout\") return void 0;\n const dir = gitEffectiveCwd(tokens, ctx.cwd);\n const isCheckoutBranch = (name) => !resolved || getProtectedBranches(dir, ctx.repoProtectedBranches ?? []).has(name) || resolveCommit(dir, name) !== null;\n const target = switchTarget(tokens.slice(tokens.indexOf(sub) + 1), sub, isCheckoutBranch);\n if (!resolved) return target === void 0 ? void 0 : {\n dir,\n branch: null\n };\n return target === void 0 ? void 0 : {\n dir,\n branch: target\n };\n}\nconst GIT_CHECKED_OUT_WRITES = /* @__PURE__ */ new Set([\n \"commit\",\n \"merge\",\n \"rebase\",\n \"cherry-pick\",\n \"revert\",\n \"am\",\n \"pull\"\n]);\nfunction subcommandArgs(tokens, sub, valueFlags = /* @__PURE__ */ new Set()) {\n const after = tokens.slice(tokens.indexOf(sub) + 1);\n const flags = [];\n const positionals = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n flags.push(t);\n if (valueFlags.has(t)) i++;\n }\n return {\n flags,\n positionals\n };\n}\nconst GIT_PUSH_VALUE_FLAGS = /* @__PURE__ */ new Set([\n \"-o\",\n \"--push-option\",\n \"--repo\",\n \"--receive-pack\",\n \"--exec\"\n]);\nfunction isMergeFastForwardFromUpstream(tokens, upstream) {\n const { flags, positionals } = subcommandArgs(tokens, \"merge\");\n if (!flags.includes(\"--ff-only\") || positionals.length !== 1) return false;\n const ref = positionals[0];\n return ref === \"@{u}\" || ref === \"@{upstream}\" || upstream !== null && ref === upstream;\n}\nconst GIT_PULL_NO_REBASE = /* @__PURE__ */ new Set([\n \"--rebase=false\",\n \"--rebase=no\",\n \"--rebase=off\",\n \"--rebase=0\"\n]);\nfunction isPullFromUpstream(tokens, upstream) {\n const { flags, positionals } = subcommandArgs(tokens, \"pull\");\n if (flags.some((t) => t.startsWith(\"--rebase\") && !GIT_PULL_NO_REBASE.has(t) || t === \"--no-ff\" || !t.startsWith(\"--\") && t.slice(1).includes(\"r\"))) return false;\n if (positionals.length === 0) return true;\n if (positionals.length !== 2 || upstream === null) return false;\n const [remote, branch] = positionals;\n return `${remote}/${branch.replace(/^refs\\/heads\\//, \"\")}` === upstream;\n}\nfunction isRefMovingReset(tokens, cwd, head) {\n if (tokens.slice(tokens.indexOf(\"reset\") + 1).includes(\"--\")) return false;\n const { positionals } = subcommandArgs(tokens, \"reset\");\n if (positionals.length !== 1) return false;\n if (head !== \"HEAD\" && /^(HEAD|@)/.test(positionals[0])) return false;\n const target = resolveCommit(cwd, positionals[0]);\n if (target === null) return false;\n return target !== resolveCommit(cwd, head);\n}\nfunction pushDestinations(tokens, current) {\n const { positionals } = subcommandArgs(tokens, \"push\", GIT_PUSH_VALUE_FLAGS);\n const refspecs = positionals.slice(1);\n if (refspecs.length === 0) return null;\n const names = [];\n for (const spec of refspecs) {\n const dst = refspecDestination(spec);\n if (dst === \"HEAD\" || dst === \"@\") {\n if (current !== null) names.push(current);\n continue;\n }\n names.push(dst);\n }\n return names;\n}\nconst GIT_PUSH_EVERY_BRANCH = /* @__PURE__ */ new Set([\n \"--all\",\n \"--branches\",\n \"--mirror\"\n]);\nfunction leavesRefInPlace(tokens, sub) {\n const { flags } = subcommandArgs(tokens, sub);\n if (flags.includes(\"--abort\") || flags.includes(\"--quit\")) return true;\n return sub === \"commit\" && flags.includes(\"--dry-run\");\n}\nfunction movesProtectedRef(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (sub === void 0) return false;\n if (sub === \"switch\" || sub === \"checkout\" || sub === \"worktree\") return isGitProtectedBranchForceCreate(tokens, ctx);\n if (sub === \"branch\") return isGitProtectedBranchRefWrite(tokens, ctx);\n if (sub === \"fetch\") return isGitProtectedBranchFetchWrite(tokens, ctx);\n if (sub === \"update-ref\") return isGitProtectedUpdateRef(tokens, ctx);\n if (sub !== \"push\" && sub !== \"reset\" && !GIT_CHECKED_OUT_WRITES.has(sub)) return false;\n const cwd = gitEffectiveCwd(tokens, ctx.cwd);\n const pending = ctx.pendingBranches?.[cwd];\n const current = pending ? pending.branch : getCurrentBranch(cwd);\n const protectedSet = getProtectedBranches(cwd, ctx.repoProtectedBranches ?? []);\n const onProtected = current !== null && protectedSet.has(current);\n if (sub === \"push\") {\n const { flags } = subcommandArgs(tokens, \"push\", GIT_PUSH_VALUE_FLAGS);\n if (flags.some((f) => GIT_PUSH_EVERY_BRANCH.has(f))) return true;\n const destinations = pushDestinations(tokens, current);\n if (destinations === null) {\n if (flags.includes(\"--tags\")) return false;\n return onProtected;\n }\n return destinations.some((d) => protectedSet.has(d));\n }\n if (!onProtected) return false;\n if (leavesRefInPlace(tokens, sub)) return false;\n if (sub === \"reset\") return isRefMovingReset(tokens, cwd, pending ? current : \"HEAD\");\n if (sub === \"merge\" || sub === \"pull\") {\n const upstream = getUpstream(cwd, current);\n if (sub === \"merge\" && isMergeFastForwardFromUpstream(tokens, upstream)) return false;\n if (sub === \"pull\" && isPullFromUpstream(tokens, upstream)) return false;\n }\n return true;\n}\nconst PNPM_BUILTINS = /* @__PURE__ */ new Set([\n \"install\",\n \"i\",\n \"add\",\n \"remove\",\n \"rm\",\n \"uninstall\",\n \"un\",\n \"update\",\n \"up\",\n \"upgrade\",\n \"list\",\n \"ls\",\n \"ll\",\n \"la\",\n \"why\",\n \"view\",\n \"info\",\n \"show\",\n \"outdated\",\n \"audit\",\n \"licenses\",\n \"init\",\n \"create\",\n \"link\",\n \"unlink\",\n \"pack\",\n \"publish\",\n \"config\",\n \"set\",\n \"get\",\n \"store\",\n \"cache\",\n \"rebuild\",\n \"rb\",\n \"prune\",\n \"dedupe\",\n \"dlx\",\n \"exec\",\n \"run\",\n \"patch\",\n \"patch-commit\",\n \"recursive\",\n \"-r\"\n]);\nconst NPM_READ_OPS = /* @__PURE__ */ new Set([\n \"view\",\n \"info\",\n \"show\"\n]);\nfunction isNpmReadOp(tokens) {\n const sub = tokens[1];\n return sub !== void 0 && NPM_READ_OPS.has(sub);\n}\nfunction getNpmRedirectReason(tokens) {\n const sub = tokens[1];\n const BASE = \"This project uses pnpm.\";\n if (!sub) return `${BASE} Use \\`pnpm\\` instead of npm.`;\n if (sub === \"install\" || sub === \"i\" || sub === \"ci\") {\n if (tokens.slice(2).some((t) => !t.startsWith(\"-\"))) return `${BASE} Use \\`pnpm add <pkg>\\` instead of \\`npm install <pkg>\\`.`;\n return `${BASE} Use \\`pnpm install\\` instead of npm.`;\n }\n if (sub === \"add\") return `${BASE} Use \\`pnpm add <pkg>\\` instead of npm.`;\n if (sub === \"uninstall\" || sub === \"remove\" || sub === \"rm\" || sub === \"un\" || sub === \"r\") return `${BASE} Use \\`pnpm remove <pkg>\\` instead of npm.`;\n if (sub === \"run\" || sub === \"run-script\") return `${BASE} Use \\`pnpm run <script>\\` or \\`pnpm <script>\\` instead of npm.`;\n if (sub === \"test\" || sub === \"t\" || sub === \"tst\") return `${BASE} Use \\`pnpm test\\` instead of npm.`;\n if (sub === \"start\") return `${BASE} Use \\`pnpm start\\` instead of npm.`;\n if (sub === \"stop\") return `${BASE} Use \\`pnpm stop\\` instead of npm.`;\n if (sub === \"restart\") return `${BASE} Use \\`pnpm restart\\` instead of npm.`;\n if (sub === \"exec\") return `${BASE} Use \\`pnpm exec <cmd>\\` instead of npm.`;\n if (sub === \"list\" || sub === \"ls\" || sub === \"ll\" || sub === \"la\") return `${BASE} Use \\`pnpm list\\` instead of npm.`;\n if (sub === \"why\") return `${BASE} Use \\`pnpm why\\` instead of npm.`;\n if (sub === \"outdated\") return `${BASE} Use \\`pnpm outdated\\` instead of npm.`;\n if (sub === \"audit\") return `${BASE} Use \\`pnpm audit\\` instead of npm.`;\n if (sub === \"update\" || sub === \"up\" || sub === \"upgrade\") return `${BASE} Use \\`pnpm update\\` instead of npm.`;\n if (sub === \"init\") return `${BASE} Use \\`pnpm init\\` instead of npm.`;\n if (sub === \"publish\") return `${BASE} Use \\`pnpm publish\\` instead of npm.`;\n if (sub === \"link\") return `${BASE} Use \\`pnpm link\\` instead of npm.`;\n if (sub === \"unlink\") return `${BASE} Use \\`pnpm unlink\\` instead of npm.`;\n if (sub === \"pack\") return `${BASE} Use \\`pnpm pack\\` instead of npm.`;\n if (sub === \"cache\") return `${BASE} Use \\`pnpm store\\` instead of \\`npm cache\\`.`;\n if (sub === \"config\" || sub === \"set\" || sub === \"get\") return `${BASE} Use \\`pnpm config\\` instead of npm.`;\n return `${BASE} Use the equivalent pnpm command instead of npm.`;\n}\nconst LAST_POSITIONAL_DEST = /* @__PURE__ */ new Set([\n \"cp\",\n \"mv\",\n \"rsync\",\n \"install\",\n \"ln\"\n]);\nconst ALL_POSITIONAL_DEST = /* @__PURE__ */ new Set([\"tee\", \"truncate\"]);\nfunction explicitTargetDirectory(rest) {\n for (const [i, t] of rest.entries()) {\n if (t === \"-t\" || t === \"--target-directory\") return rest[i + 1] ?? null;\n if (t.startsWith(\"--target-directory=\")) return t.slice(19);\n }\n return null;\n}\nconst OUTPUT_REDIRECT = /^(?:\\d*|&)>{1,2}$/;\nconst OUTPUT_REDIRECT_ATTACHED = /^(?:\\d*|&)>{1,2}(.+)$/;\nconst INPUT_REDIRECT = /^\\d*<$/;\nconst INPUT_REDIRECT_ATTACHED = /^\\d*<([^<].*)$/;\nfunction spacedRedirectDirection(token) {\n if (OUTPUT_REDIRECT.test(token)) return \"out\";\n if (INPUT_REDIRECT.test(token)) return \"in\";\n return null;\n}\nfunction splitRedirects(tokens) {\n const redirects = [];\n const rest = [];\n for (let i = 0; i < tokens.length; i++) {\n const t = tokens[i];\n const spaced = spacedRedirectDirection(t);\n if (spaced !== null) {\n const next = tokens[i + 1];\n if (next !== void 0) redirects.push({\n direction: spaced,\n target: next\n });\n i++;\n continue;\n }\n const out = OUTPUT_REDIRECT_ATTACHED.exec(t);\n if (out) {\n redirects.push({\n direction: \"out\",\n target: out[1]\n });\n continue;\n }\n const input = INPUT_REDIRECT_ATTACHED.exec(t);\n if (input) {\n redirects.push({\n direction: \"in\",\n target: input[1]\n });\n continue;\n }\n rest.push(t);\n }\n return {\n redirects,\n rest\n };\n}\nfunction redirectTargets(tokens, direction) {\n return splitRedirects(tokens).redirects.filter((r) => r.direction === direction).map((r) => r.target);\n}\nfunction writeDestinations(tokens) {\n const cmd = path.basename(tokens[0] ?? \"\");\n const rest = tokens.slice(1);\n const dests = redirectTargets(tokens, \"out\");\n if (cmd === \"dd\") dests.push(...rest.filter((t) => t.startsWith(\"of=\")).map((t) => t.slice(3)));\n else if (cmd === \"sed\") {\n if (rest.some((t) => t === \"--in-place\" || /^-[a-zA-Z]*i/.test(t))) dests.push(...rest.filter((t) => !t.startsWith(\"-\")));\n } else if (ALL_POSITIONAL_DEST.has(cmd)) dests.push(...rest.filter((t) => !t.startsWith(\"-\")));\n else if (LAST_POSITIONAL_DEST.has(cmd)) {\n if (cmd === \"cp\" && rest.includes(\"--remove-destination\")) return dests;\n const targetDir = explicitTargetDirectory(rest);\n if (targetDir !== null) dests.push(targetDir);\n else {\n const last = rest.findLast((t) => !t.startsWith(\"-\"));\n if (last !== void 0) dests.push(last);\n }\n }\n return dests;\n}\nfunction writesIntoNodeModules(tokens, ctx) {\n return writeDestinations(tokens).some((d) => targetsNodeModules(d, ctx.cwd));\n}\nconst NODE_MODULES_WRITE_REASON = \"Don't write into node_modules/. Its files are hardlinks into pnpm's global store, so overwriting one in place (plain `cp`, `>` redirect, `sed -i`, ...) writes through the link and corrupts that package for every project on this machine -- silently, since the directory you wrote into ends up fine. To test a local build as an installed dependency, run it directly (`node packages/<pkg>/dist/bin.mjs <cmd>`), or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`). If you must replace a file in place, `cp --remove-destination` is safe -- it unlinks the target first. Run `pnpm store status` to check whether the store is already damaged.\";\nfunction isChmod777(tokens) {\n return tokens.some((t) => t === \"777\" || t === \"0777\");\n}\nfunction isGitForcePush(tokens) {\n const pushIdx = tokens.indexOf(\"push\");\n if (pushIdx === -1) return false;\n return tokens.slice(pushIdx + 1).some((t) => t === \"--force\" || t === \"-f\");\n}\nconst ENV_READ_FILE_COMMANDS = /* @__PURE__ */ new Set([\n \"cat\",\n \"head\",\n \"tail\",\n \"less\",\n \"more\",\n \"source\",\n \".\"\n]);\nconst GREP_FAMILY_COMMANDS = /* @__PURE__ */ new Set([\n \"grep\",\n \"rg\",\n \"ag\"\n]);\nfunction grepFileOperands(rest) {\n const files = [];\n let patternAccountedFor = false;\n let optionsEnded = false;\n for (let i = 0; i < rest.length; i++) {\n const t = rest[i];\n if (!optionsEnded && t === \"--\") {\n optionsEnded = true;\n continue;\n }\n if (!optionsEnded) {\n if (t === \"-f\" || t === \"--file\") {\n const value = rest[i + 1];\n if (value !== void 0) {\n files.push(value);\n i++;\n }\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"--file=\")) {\n files.push(t.slice(7));\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"-f\") && t !== \"-f\" && !t.startsWith(\"--\")) {\n files.push(t.slice(2));\n patternAccountedFor = true;\n continue;\n }\n if (t === \"-e\" || t === \"--regexp\") {\n i++;\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"--regexp=\") || t.startsWith(\"-e\") && t !== \"-e\" && !t.startsWith(\"--\")) {\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n }\n if (!patternAccountedFor) {\n patternAccountedFor = true;\n continue;\n }\n files.push(t);\n }\n return files;\n}\nfunction envFileOperands(tokens) {\n const cmd = path.basename(tokens[0] ?? \"\");\n const { redirects, rest } = splitRedirects(tokens.slice(1));\n const operands = redirects.filter((r) => r.direction === \"in\").map((r) => r.target);\n if (GREP_FAMILY_COMMANDS.has(cmd)) operands.push(...grepFileOperands(rest));\n else if (ENV_READ_FILE_COMMANDS.has(cmd)) operands.push(...rest.filter((t) => !t.startsWith(\"-\")));\n return operands;\n}\nconst HEALTH_CEILING_FILES = [\"health-baseline.json\", \".tooling.json\"];\nconst HEALTH_RAISE_REASON = \"Run `pnpm exec bst health:raise` to raise only what this branch crossed. `--update-baseline --reset` records every outlier, including the ones already on the default branch.\";\nfunction isHealthBaselineReset(tokens) {\n return tokens.includes(\"health:check\") && tokens.includes(\"--update-baseline\") && tokens.includes(\"--reset\");\n}\nfunction raiseTravelsWithCode(cwd) {\n return branchCarriesOtherFiles(cwd, HEALTH_CEILING_FILES) === true;\n}\nconst denyRules = [\n {\n name: \"sudo\",\n match: /^sudo$/,\n action: \"deny\",\n reason: \"Don't use sudo. If you need elevated permissions, ask me to do it manually.\"\n },\n {\n name: \"git-force-push\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens) => isGitForcePush(tokens),\n reason: \"Use `git push --force-with-lease` instead of `--force`.\"\n },\n {\n name: \"git-protected-branch-write\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens, ctx) => movesProtectedRef(tokens, ctx),\n reason: \"This would move a protected branch (main, master, the remote's default branch, or one listed in .claude/hooks/bst-hook.settings.json#protectedBranches). Work on a feature branch in a worktree: `git worktree add ../<repo>-<topic> -b feat/<topic>`, then commit and push there. To bring `main` up to date, use `git pull --ff-only`.\"\n },\n {\n name: \"chmod-777\",\n match: /^chmod$/,\n action: \"deny\",\n when: (tokens) => isChmod777(tokens),\n reason: \"Don't use 777 permissions. Use more restrictive permissions.\"\n },\n {\n name: \"node-modules-write\",\n match: /./,\n action: \"deny\",\n when: (tokens, ctx) => writesIntoNodeModules(tokens, ctx),\n reason: NODE_MODULES_WRITE_REASON\n },\n {\n name: \"env-file-read\",\n match: /./,\n action: \"deny\",\n when: (tokens) => envFileOperands(tokens).some(isEnvFile),\n reason: \"Don't read .env files. These contain secrets that should not appear in the conversation.\"\n },\n {\n name: \"env-file-write\",\n match: /./,\n action: \"deny\",\n when: (tokens) => redirectTargets(tokens, \"out\").some(isEnvFile),\n reason: \"Don't write to .env files. These contain secrets.\"\n },\n {\n name: \"health-baseline-reset\",\n match: /^bst$/,\n action: \"deny\",\n when: (tokens, ctx) => isHealthBaselineReset(tokens) && raiseTravelsWithCode(ctx.cwd),\n reason: HEALTH_RAISE_REASON\n },\n {\n name: \"npx-redirect\",\n match: /^npx$/,\n action: \"deny\",\n reason: \"This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\"\n },\n {\n name: \"mise-self-update\",\n match: /^mise$/,\n action: \"deny\",\n when: (tokens) => tokens[1] === \"self-update\",\n reason: \"Don't self-update mise. The version is Renovate-managed; ask a human to bump the pin.\"\n }\n];\n\n//#endregion\n//#region src/lib/script-resolver.ts\nconst PackageJsonSchema = object({ scripts: record(string(), unknown()) });\nfunction resolveScript(scriptName, cwd) {\n const scripts = readScripts(cwd);\n if (!scripts) return null;\n const main = scripts[scriptName];\n if (typeof main !== \"string\") return null;\n const pre = scripts[`pre${scriptName}`];\n const post = scripts[`post${scriptName}`];\n return {\n pre: typeof pre === \"string\" ? pre : null,\n main,\n post: typeof post === \"string\" ? post : null\n };\n}\nfunction resolveFilteredScript(scriptName, filterPkg, cwd) {\n const pkgDir = findWorkspacePackageDir(filterPkg, cwd);\n if (!pkgDir) return null;\n const script = resolveScript(scriptName, pkgDir);\n if (!script) return null;\n return {\n ...script,\n pkgDir\n };\n}\nfunction resolveRecursiveScripts(scriptName, rootDir) {\n const pkgDirs = listWorkspacePackageDirs(rootDir);\n if (!pkgDirs) return null;\n const results = [];\n for (const pkgDir of pkgDirs) {\n const resolved = resolveScript(scriptName, pkgDir);\n if (resolved) results.push({\n ...resolved,\n pkgDir\n });\n }\n return results;\n}\nfunction listWorkspacePackageDirs(rootDir) {\n const patterns = readWorkspacePatterns(rootDir);\n if (!patterns) return null;\n const dirs = [];\n for (const pattern of patterns) {\n if (!pattern.endsWith(\"/*\")) continue;\n const baseDir = pattern.slice(0, -2);\n const parentDir = resolve(rootDir, baseDir);\n let entries;\n try {\n entries = readdirSync(parentDir);\n } catch {\n continue;\n }\n for (const entry of entries) {\n const entryDir = join(parentDir, entry);\n try {\n if (!statSync(entryDir).isDirectory()) continue;\n } catch {\n continue;\n }\n dirs.push(entryDir);\n }\n }\n return dirs.length > 0 ? dirs : null;\n}\nfunction findWorkspacePackageDir(pkgName, rootDir) {\n const pkgDirs = listWorkspacePackageDirs(rootDir);\n if (!pkgDirs) return null;\n for (const entryDir of pkgDirs) {\n const name = readPackageName(entryDir);\n if (name === pkgName || name === `@${pkgName}`) return entryDir;\n if (entryDir.split(\"/\").pop() === pkgName) return entryDir;\n if (name) {\n if ((name.startsWith(\"@\") ? name.split(\"/\")[1] : name) === pkgName) return entryDir;\n }\n }\n return null;\n}\nfunction readWorkspacePatterns(rootDir) {\n try {\n const lines = readFileSync(join(rootDir, \"pnpm-workspace.yaml\"), \"utf-8\").split(\"\\n\");\n const packagesIdx = lines.findIndex((l) => /^packages\\s*:/.test(l));\n if (packagesIdx === -1) return null;\n const patterns = [];\n for (let i = packagesIdx + 1; i < lines.length; i++) {\n const line = lines[i];\n const match = /^\\s+-\\s+(.+)$/.exec(line);\n if (!match) break;\n patterns.push(match[1].trim().replace(/['\"`]/g, \"\"));\n }\n return patterns.length > 0 ? patterns : null;\n } catch {\n return null;\n }\n}\nconst PackageNameSchema = object({ name: string() });\nfunction readPackageName(dir) {\n try {\n const raw = readFileSync(join(dir, \"package.json\"), \"utf-8\");\n const result = PackageNameSchema.safeParse(JSON.parse(raw));\n if (!result.success) return null;\n return result.data.name;\n } catch {\n return null;\n }\n}\nfunction readScripts(cwd) {\n try {\n const raw = readFileSync(join(cwd, \"package.json\"), \"utf-8\");\n const result = PackageJsonSchema.safeParse(JSON.parse(raw));\n if (!result.success) return null;\n return result.data.scripts;\n } catch {\n return null;\n }\n}\n\n//#endregion\n//#region src/lib/workspace-root.ts\nfunction findWorkspaceRoot(cwd) {\n let dir = resolve(cwd);\n while (true) {\n if (existsSync(join(dir, \"pnpm-workspace.yaml\"))) return dir;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n//#endregion\n//#region src/evaluate.ts\nconst MAX_EVAL_DEPTH = 5;\nfunction subshellBody(command) {\n if (!command.startsWith(\"(\") || !command.endsWith(\")\")) return null;\n let depth = 0;\n let quote = null;\n for (let i = 0; i < command.length; i++) {\n const ch = command[i];\n if (quote !== null) {\n if (ch === quote) quote = null;\n else if (ch === \"\\\\\" && quote === \"\\\"\") i++;\n continue;\n }\n if (ch === \"\\\\\") i++;\n else if (ch === \"'\" || ch === \"\\\"\") quote = ch;\n else if (ch === \"(\") depth++;\n else if (ch === \")\") {\n depth--;\n if (depth === 0) return i === command.length - 1 ? command.slice(1, -1) : null;\n }\n }\n return null;\n}\nfunction extractCdDir(segment, ctx) {\n const tokenResult = tokenize(segment);\n if (!tokenResult.ok || tokenResult.tokens.length !== 2) return null;\n const tokens = tokenResult.tokens;\n if (tokens[0] !== \"cd\") return null;\n const dir = tokens[1];\n if (path.isAbsolute(dir)) return dir;\n return path.resolve(ctx.cwd, dir);\n}\nfunction extractBranchSwitch(segment, ctx) {\n const tokenResult = tokenize(segment);\n if (!tokenResult.ok) return void 0;\n let tokens = tokenResult.tokens;\n let envSkipped = 0;\n while (envSkipped < tokens.length && /^\\w+=/.test(tokens[envSkipped])) envSkipped++;\n tokens = tokens.slice(envSkipped);\n if (tokens.length === 0 || path.basename(tokens[0]) !== \"git\") return void 0;\n return branchSwitch(tokens, ctx, checkInjection(tokens, segment).safe);\n}\nfunction evaluateCommand(command, ctx) {\n if (ctx.depth >= MAX_EVAL_DEPTH) return {\n action: \"none\",\n rule: null,\n reason: \"max evaluation depth exceeded\"\n };\n const trimmed = command.trim();\n if (!trimmed) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n const body = subshellBody(trimmed);\n if (body !== null) return evaluateCommand(body, {\n ...ctx,\n depth: ctx.depth + 1\n });\n const segments = splitCommand(trimmed);\n if (segments.length === 0) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n let firstNone = null;\n const currentCtx = { ...ctx };\n let unknownAfterNext;\n for (const segment of segments) {\n if (segment.operator === \"&&\") {\n const cdDir = extractCdDir(segment.command, currentCtx);\n if (cdDir) currentCtx.cwd = cdDir;\n }\n const result = evaluateSegment(segment.command, currentCtx);\n if (result.action === \"deny\") return result;\n if (!firstNone) firstNone = result;\n if (unknownAfterNext) {\n currentCtx.pendingBranches = {\n ...currentCtx.pendingBranches,\n [unknownAfterNext.dir]: unknownAfterNext\n };\n unknownAfterNext = void 0;\n }\n const switched = extractBranchSwitch(segment.command, currentCtx);\n if (switched) {\n const unknown = {\n dir: switched.dir,\n branch: null\n };\n if (segment.operator === \"&&\") currentCtx.pendingBranches = {\n ...currentCtx.pendingBranches,\n [switched.dir]: switched\n };\n else if (segment.operator === \"||\") unknownAfterNext = unknown;\n else currentCtx.pendingBranches = {\n ...currentCtx.pendingBranches,\n [switched.dir]: unknown\n };\n }\n }\n return firstNone ?? {\n action: \"none\",\n rule: null,\n reason: null\n };\n}\nfunction evaluateResolvedScript(resolved, ctx) {\n if (resolved.pre) {\n const preResult = evaluateCommand(resolved.pre, ctx);\n if (preResult.action === \"deny\") return preResult;\n }\n const mainResult = evaluateCommand(resolved.main, ctx);\n if (mainResult.action === \"deny\") return mainResult;\n if (resolved.post) {\n const postResult = evaluateCommand(resolved.post, ctx);\n if (postResult.action === \"deny\") return postResult;\n }\n return {\n action: \"none\",\n rule: null,\n reason: null\n };\n}\nfunction evaluateSegment(rawSegment, ctx) {\n const trimmed = rawSegment.trim();\n const body = subshellBody(trimmed);\n if (body !== null) return evaluateCommand(body, {\n ...ctx,\n depth: ctx.depth + 1\n });\n if (trimmed.startsWith(\"(\") && trimmed.endsWith(\")\")) return {\n action: \"none\",\n rule: null,\n reason: \"subshell\"\n };\n const tokenResult = tokenize(rawSegment);\n if (!tokenResult.ok) return {\n action: \"none\",\n rule: null,\n reason: tokenResult.error\n };\n let tokens = tokenResult.tokens;\n if (tokens.length === 0) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n let envSkipped = 0;\n while (envSkipped < tokens.length && /^\\w+=/.test(tokens[envSkipped])) envSkipped++;\n if (envSkipped >= tokens.length) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n tokens = tokens.slice(envSkipped);\n if (writesIntoNodeModules(tokens, ctx)) return {\n action: \"deny\",\n rule: \"node-modules-write\",\n reason: NODE_MODULES_WRITE_REASON\n };\n const injection = checkInjection(tokens, rawSegment);\n if (!injection.safe) return {\n action: \"none\",\n rule: null,\n reason: injection.reason\n };\n const SAFE_REDIRECT_TOKENS = /* @__PURE__ */ new Set([\n \"2>&1\",\n \"2>/dev/null\",\n \">/dev/null\",\n \"1>/dev/null\"\n ]);\n tokens = tokens.filter((t, i) => {\n if (SAFE_REDIRECT_TOKENS.has(t)) return false;\n if ((t === \">\" || t === \">>\") && tokens[i + 1] === \"/dev/null\") return false;\n if (t === \"/dev/null\" && i > 0 && (tokens[i - 1] === \">\" || tokens[i - 1] === \">>\")) return false;\n return true;\n });\n const cmdName = path.basename(tokens[0]);\n if (cmdName === \"command\" && tokens.length > 1) return evaluateCommand(joinTokens(tokens.slice(1)), {\n ...ctx,\n depth: ctx.depth + 1\n });\n if (cmdName === \"env\") {\n let envIdx = 1;\n while (envIdx < tokens.length && /^\\w+=/.test(tokens[envIdx])) envIdx++;\n if (envIdx >= tokens.length) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n return evaluateCommand(joinTokens(tokens.slice(envIdx)), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n if (cmdName === \"pnpm\") return evaluatePnpm(tokens, ctx);\n if (cmdName === \"npm\") return evaluateNpm(tokens);\n if (cmdName === \"mise\" && tokens[1] === \"exec\") {\n const inner = miseExecInner(tokens);\n if (!inner) return {\n action: \"none\",\n rule: null,\n reason: \"bare mise exec\"\n };\n return evaluateCommand(joinTokens(inner), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n if (cmdName === \"bash\" || cmdName === \"sh\" || cmdName === \"zsh\") return evaluateShellC(tokens, ctx);\n if (cmdName === \"xargs\") return evaluateXargs(tokens, ctx);\n if (cmdName === \"node\" && (tokens.includes(\"-e\") || tokens.includes(\"-p\") || tokens.includes(\"--eval\") || tokens.includes(\"--print\"))) return {\n action: \"none\",\n rule: null,\n reason: \"node eval\"\n };\n for (const rule of denyRules) if (rule.match.test(cmdName)) {\n if (!rule.when || rule.when(tokens, ctx)) return {\n action: \"deny\",\n rule: rule.name,\n reason: rule.reason ?? null\n };\n }\n const advice = cmdName === \"git\" ? worktreeAdvice(tokens, ctx) : void 0;\n return {\n action: \"none\",\n rule: null,\n reason: `no rule matched: ${cmdName}`,\n ...advice ? { advice } : {}\n };\n}\nfunction miseExecInner(tokens) {\n const dashIdx = tokens.indexOf(\"--\");\n if (dashIdx === -1) return void 0;\n const inner = tokens.slice(dashIdx + 1);\n return inner.length > 0 ? inner : void 0;\n}\nfunction scanPnpmFlags(subTokens, from, state) {\n for (let i = from; i < subTokens.length; i++) {\n const t = subTokens[i];\n if (t === \"--filter\" || t.startsWith(\"--filter=\") || t === \"-F\" || t.startsWith(\"-F=\")) return {\n name: void 0,\n filterIdx: i\n };\n if (t === \"-r\" || t === \"--recursive\") {\n state.isRecursive = true;\n continue;\n }\n if (t === \"-w\" || t === \"--workspace-root\") {\n state.isWorkspaceRoot = true;\n continue;\n }\n if (t === \"-C\" || t === \"--dir\") {\n state.dirOverride = subTokens[++i];\n continue;\n }\n if (t.startsWith(\"-C=\") || t.startsWith(\"--dir=\")) {\n state.dirOverride = t.split(\"=\", 2)[1];\n continue;\n }\n if (t.startsWith(\"-\") && !PNPM_BUILTINS.has(t)) continue;\n return { name: t };\n }\n return { name: void 0 };\n}\nfunction evaluatePnpm(tokens, ctx) {\n const subTokens = tokens.slice(1);\n const state = {\n isRecursive: false,\n isWorkspaceRoot: false,\n dirOverride: void 0\n };\n const scan = scanPnpmFlags(subTokens, 0, state);\n if (scan.filterIdx !== void 0) return evaluatePnpmFilter(tokens, subTokens, scan.filterIdx, ctx, state.dirOverride);\n const subcommand = scan.name;\n const subcommandIdx = subcommand ? subTokens.indexOf(subcommand) : -1;\n if (!subcommand) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n if (subcommand === \"dlx\") return {\n action: \"none\",\n rule: null,\n reason: \"pnpm dlx\"\n };\n if (subcommand === \"add\") return {\n action: \"none\",\n rule: null,\n reason: \"pnpm add\"\n };\n if (subcommand === \"exec\") {\n const execArgs = subTokens.slice(subcommandIdx + 1);\n if (execArgs.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"bare pnpm exec\"\n };\n return evaluateCommand(joinTokens(execArgs), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n let scriptName;\n let { isRecursive, isWorkspaceRoot, dirOverride } = state;\n if (subcommand === \"run\") {\n const runScan = scanPnpmFlags(subTokens, subcommandIdx + 1, state);\n ({isRecursive, isWorkspaceRoot, dirOverride} = state);\n if (runScan.filterIdx !== void 0) return evaluatePnpmFilter(tokens, subTokens, runScan.filterIdx, ctx, state.dirOverride);\n if (!runScan.name) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm run without script name\"\n };\n scriptName = runScan.name;\n } else if (!PNPM_BUILTINS.has(subcommand)) scriptName = subcommand;\n else return {\n action: \"none\",\n rule: null,\n reason: null\n };\n if (isRecursive) return evaluatePnpmRecursive(scriptName, ctx);\n if (isWorkspaceRoot) {\n const root = findWorkspaceRoot(ctx.cwd);\n if (!root) return {\n action: \"none\",\n rule: null,\n reason: \"workspace root not found\"\n };\n return evaluatePnpmScript(scriptName, {\n ...ctx,\n cwd: root\n });\n }\n if (dirOverride) {\n const resolved = path.resolve(ctx.cwd, dirOverride);\n return evaluatePnpmScript(scriptName, {\n ...ctx,\n cwd: resolved\n });\n }\n return evaluatePnpmScript(scriptName, ctx);\n}\nfunction evaluatePnpmScript(scriptName, ctx) {\n const resolved = resolveScript(scriptName, ctx.cwd);\n if (!resolved) return {\n action: \"none\",\n rule: null,\n reason: \"script resolution failed\"\n };\n const res = evaluateResolvedScript(resolved, {\n ...ctx,\n depth: ctx.depth + 1\n });\n if (res.action === \"deny\") return res;\n return {\n action: \"none\",\n rule: `pnpm-script:${scriptName}`,\n reason: null\n };\n}\nfunction evaluatePnpmRecursive(scriptName, ctx) {\n const resolved = resolveRecursiveScripts(scriptName, ctx.cwd);\n if (!resolved || resolved.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm recursive\"\n };\n for (const pkg of resolved) {\n const res = evaluateResolvedScript(pkg, {\n ...ctx,\n depth: ctx.depth + 1,\n cwd: pkg.pkgDir ?? ctx.cwd\n });\n if (res.action === \"deny\") return res;\n }\n return {\n action: \"none\",\n rule: `pnpm-recursive:${scriptName}`,\n reason: null\n };\n}\nfunction evaluateNpm(tokens) {\n if (isNpmReadOp(tokens)) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n return {\n action: \"deny\",\n rule: \"npm-redirect\",\n reason: getNpmRedirectReason(tokens)\n };\n}\nfunction collectFiltersAndScan(subTokens, from, filterPkgs) {\n let dirOverride;\n for (let i = from; i < subTokens.length; i += 1) {\n const t = subTokens[i];\n if (t === \"--filter\" || t === \"-F\") {\n const pkg = subTokens[i + 1];\n if (pkg === void 0 || pkg.startsWith(\"-\")) return void 0;\n filterPkgs.push(pkg);\n i += 1;\n continue;\n }\n if (t.startsWith(\"--filter=\") || t.startsWith(\"-F=\")) {\n const pkg = t.startsWith(\"-F=\") ? t.slice(3) : t.slice(9);\n if (pkg === \"\") return void 0;\n filterPkgs.push(pkg);\n continue;\n }\n if (t === \"-r\" || t === \"--recursive\" || t === \"-w\" || t === \"--workspace-root\") continue;\n if (t === \"-C\" || t === \"--dir\") {\n dirOverride = subTokens[i + 1];\n i += 1;\n continue;\n }\n if (t.startsWith(\"-C=\") || t.startsWith(\"--dir=\")) {\n dirOverride = t.split(\"=\", 2)[1];\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n return {\n name: t,\n index: i,\n dirOverride\n };\n }\n return {\n name: void 0,\n index: -1,\n dirOverride\n };\n}\nfunction evaluatePnpmFilter(_tokens, subTokens, filterIdx, ctx, stateDir) {\n const filterPkgs = [];\n const scan = collectFiltersAndScan(subTokens, filterIdx, filterPkgs);\n if (scan === void 0) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without package\"\n };\n let dirOverride = scan.dirOverride ?? stateDir;\n if (filterPkgs.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without package\"\n };\n const filterSubcommand = scan.name;\n const subcmdIdx = scan.index;\n if (filterSubcommand === void 0) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without subcommand\"\n };\n if (PNPM_BUILTINS.has(filterSubcommand) && filterSubcommand !== \"run\" && filterSubcommand !== \"exec\") return {\n action: \"none\",\n rule: null,\n reason: null\n };\n if (filterSubcommand === \"exec\") {\n const execArgs = subTokens.slice(subcmdIdx + 1);\n if (execArgs.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"bare pnpm exec\"\n };\n return evaluateCommand(joinTokens(execArgs), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n let scriptName = filterSubcommand;\n if (filterSubcommand === \"run\") {\n const runScan = collectFiltersAndScan(subTokens, subcmdIdx + 1, filterPkgs);\n if (runScan === void 0) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without package\"\n };\n scriptName = runScan.name;\n if (runScan.dirOverride !== void 0) dirOverride = runScan.dirOverride;\n }\n if (scriptName === void 0 || scriptName.startsWith(\"-\")) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter run without script name\"\n };\n if (PNPM_BUILTINS.has(scriptName) && scriptName !== \"run\") return {\n action: \"none\",\n rule: null,\n reason: null\n };\n const baseDir = dirOverride ? path.resolve(ctx.cwd, dirOverride) : ctx.cwd;\n for (const pkg of filterPkgs) {\n const resolved = resolveFilteredScript(scriptName, pkg, baseDir);\n if (!resolved) return {\n action: \"none\",\n rule: null,\n reason: \"filtered script resolution failed\"\n };\n const res = evaluateResolvedScript(resolved, {\n ...ctx,\n depth: ctx.depth + 1,\n cwd: resolved.pkgDir ?? baseDir\n });\n if (res.action === \"deny\") return res;\n }\n return {\n action: \"none\",\n rule: `pnpm-filter:${filterPkgs.join(\",\")}:${scriptName}`,\n reason: null\n };\n}\nconst SHELL_LOGIN_FLAGS = /* @__PURE__ */ new Set([\"-l\", \"--login\"]);\nfunction evaluateShellC(tokens, ctx) {\n let i = 1;\n while (i < tokens.length && SHELL_LOGIN_FLAGS.has(tokens[i])) i++;\n const flag = tokens[i];\n if (flag === void 0 || !/^-[lc]+$/.test(flag) || !flag.includes(\"c\")) return {\n action: \"none\",\n rule: null,\n reason: \"shell without a plain -c\"\n };\n const cmdArg = tokens[i + 1];\n if (!cmdArg) return {\n action: \"none\",\n rule: null,\n reason: \"shell -c without command\"\n };\n if (tokens.length > i + 2) return {\n action: \"none\",\n rule: null,\n reason: \"shell -c with positional args\"\n };\n return evaluateCommand(cmdArg, {\n ...ctx,\n depth: ctx.depth + 1\n });\n}\nconst XARGS_ARG_FLAGS = /* @__PURE__ */ new Set([\n \"-I\",\n \"-d\",\n \"-n\",\n \"-P\",\n \"-s\",\n \"-L\",\n \"-E\",\n \"-a\",\n \"--max-args\",\n \"--max-procs\",\n \"--delimiter\",\n \"--replace\",\n \"--max-lines\",\n \"--max-chars\",\n \"--arg-file\",\n \"--process-slot-var\"\n]);\nconst XARGS_BOOL_FLAGS = /* @__PURE__ */ new Set([\n \"-0\",\n \"--null\",\n \"-t\",\n \"--verbose\",\n \"-r\",\n \"--no-run-if-empty\",\n \"-p\",\n \"--interactive\",\n \"-x\",\n \"--exit\",\n \"--show-limits\"\n]);\nfunction evaluateXargs(tokens, ctx) {\n let i = 1;\n while (i < tokens.length) {\n const t = tokens[i];\n if (!t.startsWith(\"-\")) break;\n if (XARGS_ARG_FLAGS.has(t)) {\n i += 2;\n continue;\n }\n const eqIdx = t.indexOf(\"=\");\n if (eqIdx !== -1) {\n const flagName = t.slice(0, eqIdx);\n if (XARGS_ARG_FLAGS.has(flagName)) {\n i++;\n continue;\n }\n }\n if (t.length > 2 && XARGS_ARG_FLAGS.has(t.slice(0, 2))) {\n i++;\n continue;\n }\n if (XARGS_BOOL_FLAGS.has(t)) {\n i++;\n continue;\n }\n return {\n action: \"none\",\n rule: null,\n reason: \"xargs with unrecognised flags\"\n };\n }\n if (i >= tokens.length) return {\n action: \"none\",\n rule: null,\n reason: \"xargs without command\"\n };\n return evaluateCommand(joinTokens(tokens.slice(i)), {\n ...ctx,\n depth: ctx.depth + 1\n });\n}\n\n//#endregion\n//#region src/file-rules.ts\nconst READ_TOOLS = /* @__PURE__ */ new Set([\n \"Read\",\n \"Glob\",\n \"Grep\"\n]);\nconst WRITE_TOOLS = /* @__PURE__ */ new Set([\"Write\", \"Edit\"]);\nconst fileDenyRules = [\n {\n name: \"env-file-read\",\n tools: READ_TOOLS,\n action: \"deny\",\n when: (filePath) => isEnvFile(filePath),\n reason: \"Don't read .env files. These contain secrets that should not appear in the conversation.\"\n },\n {\n name: \"env-file-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath) => isEnvFile(filePath),\n reason: \"Don't write to .env files. These contain secrets.\"\n },\n {\n name: \"hooks-folder-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n const hooksDir = path.join(ctx.cwd, \".claude\", \"hooks\");\n return isWithin(hooksDir, resolved);\n },\n reason: \"Don't write to .claude/hooks/. These files are managed by the project and should not be modified directly.\"\n },\n {\n name: \"git-dir-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => targetsGitDir(filePath, ctx.cwd),\n reason: \"Don't write into .git/ directly. Its internals (config, hooks, refs) are not plain project files -- a config edit can redirect remotes or push destinations that other rules trust, and a hooks/ script runs automatically on the next commit/push. Use git subcommands instead, or ask the user.\"\n },\n {\n name: \"node-modules-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => targetsNodeModules(filePath, ctx.cwd),\n reason: \"Don't write into node_modules/. Its files are hardlinks into pnpm's global store -- editing one in place corrupts that package for every project on this machine. Edit the source package and rebuild, or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`).\"\n },\n {\n name: \"claude-settings-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n return samePath(resolved, path.join(ctx.cwd, \".claude\", \"settings.json\"));\n },\n reason: \"Don't write to .claude/settings.json. This file is managed by the project and should not be modified directly.\"\n }\n];\n\n//#endregion\n//#region src/evaluate-tool.ts\nconst FILE_PATH_TOOLS = /* @__PURE__ */ new Set([\n \"Read\",\n \"Write\",\n \"Edit\"\n]);\nconst SEARCH_TOOLS = /* @__PURE__ */ new Set([\"Glob\", \"Grep\"]);\nfunction extractPath(toolName, toolInput, cwd) {\n if (FILE_PATH_TOOLS.has(toolName)) return parseFilePathInput(toolInput)?.file_path ?? null;\n if (SEARCH_TOOLS.has(toolName)) return parseSearchInput(toolInput)?.path ?? cwd;\n return null;\n}\nfunction evaluateToolUse(toolName, toolInput, ctx) {\n const filePath = extractPath(toolName, toolInput, ctx.cwd);\n if (filePath === null) return {\n action: \"none\",\n rule: null,\n reason: \"unknown tool input shape\"\n };\n for (const rule of fileDenyRules) if (rule.tools.has(toolName) && rule.when(filePath, ctx, toolInput)) return {\n action: \"deny\",\n rule: rule.name,\n reason: rule.reason ?? null\n };\n return {\n action: \"none\",\n rule: null,\n reason: \"no file rule matched\"\n };\n}\n\n//#endregion\n//#region src/lib/settings.ts\nconst SETTINGS_PATH = \".claude/hooks/bst-hook.settings.json\";\nconst BstHookSettingsSchema = object({ protectedBranches: _default(array(string().check(_minLength(1))), []) });\nfunction isFileClean(cwd, relativePath) {\n try {\n execFileSync(\"git\", [\n \"ls-files\",\n \"--error-unmatch\",\n relativePath\n ], {\n cwd,\n stdio: \"ignore\",\n timeout: GIT_TIMEOUT_MS\n });\n execFileSync(\"git\", [\n \"diff\",\n \"--quiet\",\n \"HEAD\",\n \"--\",\n relativePath\n ], {\n cwd,\n stdio: \"ignore\",\n timeout: GIT_TIMEOUT_MS\n });\n return true;\n } catch {\n return false;\n }\n}\nfunction loadRepoSettings(cwd) {\n const root = findWorkspaceRoot(cwd) ?? cwd;\n const settingsFile = join(root, SETTINGS_PATH);\n const empty = { protectedBranches: [] };\n if (!existsSync(settingsFile)) return empty;\n if (!isFileClean(root, SETTINGS_PATH)) return empty;\n try {\n const raw = readFileSync(settingsFile, \"utf-8\");\n const parsed = JSON.parse(raw);\n const result = BstHookSettingsSchema.safeParse(parsed);\n if (!result.success) return empty;\n return { protectedBranches: result.data.protectedBranches };\n } catch {\n return empty;\n }\n}\n\n//#endregion\n//#region src/index.ts\nconst inputResult = await readHookInput();\nif (!inputResult.ok) {\n process.stderr.write(`[bst-hook] invalid hook input: ${inputResult.error}\\n`);\n audit({\n sessionId: \"unknown\",\n cwd: \"unknown\",\n command: `<invalid input: ${inputResult.error}>`,\n decision: \"none\",\n rule: null,\n reason: `validation failed: ${inputResult.error}`,\n permissionMode: void 0\n });\n process.exit(0);\n}\nconst input = inputResult.data;\nif (input.hook_event_name !== \"PreToolUse\") process.exit(0);\nif (!(/* @__PURE__ */ new Set([\n \"Bash\",\n \"Read\",\n \"Write\",\n \"Edit\",\n \"Glob\",\n \"Grep\"\n])).has(input.tool_name)) process.exit(0);\nconst repoSettings = loadRepoSettings(input.cwd);\nlet decision;\nlet auditCommand;\nif (input.tool_name === \"Bash\") {\n const bashInput = parseBashInput(input.tool_input);\n if (!bashInput) {\n audit({\n sessionId: input.session_id,\n cwd: input.cwd,\n toolName: input.tool_name,\n command: `<invalid Bash input: ${JSON.stringify(input.tool_input).slice(0, 200)}>`,\n decision: \"none\",\n rule: null,\n reason: \"Bash tool_input missing or invalid 'command' field\",\n permissionMode: input.permission_mode\n });\n process.exit(0);\n }\n decision = evaluateCommand(bashInput.command, {\n cwd: input.cwd,\n depth: 0,\n repoProtectedBranches: repoSettings.protectedBranches\n });\n auditCommand = bashInput.command;\n} else {\n const projectDir = findWorkspaceRoot(input.cwd) ?? process.env[\"CLAUDE_PROJECT_DIR\"] ?? input.cwd;\n decision = evaluateToolUse(input.tool_name, input.tool_input, {\n cwd: projectDir,\n sessionId: input.session_id\n });\n const filePath = String(input.tool_input[\"file_path\"] ?? input.tool_input[\"path\"] ?? \"\");\n auditCommand = `${input.tool_name} ${filePath}`;\n}\naudit({\n sessionId: input.session_id,\n cwd: input.cwd,\n toolName: input.tool_name,\n command: auditCommand,\n decision: decision.action,\n rule: decision.rule,\n reason: decision.reason,\n permissionMode: input.permission_mode\n});\nif (decision.action === \"deny\") writeDeny(decision.reason);\nelse if (decision.advice) writeAdvice(decision.advice);\n\n//#endregion\nexport { };",
|
|
8504
|
+
guide: "# bst-hook Permission Guide\n\nThis project uses a PreToolUse hook (`bst-hook`) that checks each Bash command\nand file tool call before it runs. The hook only ever blocks a command; it does\nnot approve anything on its own. A command that matches a rule below is denied,\nwith a message that says what to do instead.\n\n## Denied commands\n\nThese are blocked. The hook tells you what to do instead.\n\n### Bash\n\n- **sudo**: Don't use sudo. If you need elevated permissions, ask me to do it manually.\n- **git-force-push**: Use `git push --force-with-lease` instead of `--force`.\n- **git-protected-branch-write**: This would move a protected branch (main, master, the remote's default branch, or one listed in .claude/hooks/bst-hook.settings.json#protectedBranches). Work on a feature branch in a worktree: `git worktree add ../<repo>-<topic> -b feat/<topic>`, then commit and push there. To bring `main` up to date, use `git pull --ff-only`.\n- **chmod-777**: Don't use 777 permissions. Use more restrictive permissions.\n- **node-modules-write**: Don't write into node_modules/. Its files are hardlinks into pnpm's global store, so overwriting one in place (plain `cp`, `>` redirect, `sed -i`, ...) writes through the link and corrupts that package for every project on this machine -- silently, since the directory you wrote into ends up fine. To test a local build as an installed dependency, run it directly (`node packages/<pkg>/dist/bin.mjs <cmd>`), or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`). If you must replace a file in place, `cp --remove-destination` is safe -- it unlinks the target first. Run `pnpm store status` to check whether the store is already damaged.\n- **env-file-read**: Don't read .env files. These contain secrets that should not appear in the conversation.\n- **env-file-write**: Don't write to .env files. These contain secrets.\n- **health-baseline-reset**: Run `pnpm exec bst health:raise` to raise only what this branch crossed. `--update-baseline --reset` records every outlier, including the ones already on the default branch.\n- **npx-redirect**: This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\n- **mise-self-update**: Don't self-update mise. The version is Renovate-managed; ask a human to bump the pin.\n\n### File tools (Read, Write, Edit, Glob, Grep)\n\n- **env-file-read**: Don't read .env files. These contain secrets that should not appear in the conversation.\n- **env-file-write**: Don't write to .env files. These contain secrets.\n- **hooks-folder-write**: Don't write to .claude/hooks/. These files are managed by the project and should not be modified directly.\n- **git-dir-write**: Don't write into .git/ directly. Its internals (config, hooks, refs) are not plain project files -- a config edit can redirect remotes or push destinations that other rules trust, and a hooks/ script runs automatically on the next commit/push. Use git subcommands instead, or ask the user.\n- **node-modules-write**: Don't write into node_modules/. Its files are hardlinks into pnpm's global store -- editing one in place corrupts that package for every project on this machine. Edit the source package and rebuild, or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`).\n- **claude-settings-write**: Don't write to .claude/settings.json. This file is managed by the project and should not be modified directly.\n\n## How commands are evaluated\n\n- **Pipelines and chains**: every segment is checked; a deny on any segment blocks the whole command.\n- **pnpm scripts**: `pnpm <script>` resolves against `package.json` (including pre/post hooks and `--filter` workspaces) and evaluates the resolved command recursively.\n- **cd propagation**: `cd dir && cmd` evaluates `cmd` relative to the new directory.\n- **Branch switch propagation**: `git switch <branch> && cmd` (or `git checkout <branch>`) evaluates a git write in `cmd` against the branch it switched to, not the one checked out before. This holds only across `&&` and only in the same directory.\n- **Audit log**: all decisions are logged to `.claude/hooks/bst-hook.audit.local.jsonl`.\n\n## Everything else\n\nA command that matches none of the rules above is judged by Claude Code's own\nauto mode, not by this hook.\n"
|
|
8426
8505
|
}) {
|
|
8427
8506
|
const results = [{
|
|
8428
8507
|
filePath: HOOK_PATH,
|
|
@@ -8528,7 +8607,7 @@ function serializeSettings$1(settings) {
|
|
|
8528
8607
|
}
|
|
8529
8608
|
function writeOrMergeSettings(ctx, filePath, generated) {
|
|
8530
8609
|
const existing = ctx.read(filePath);
|
|
8531
|
-
if (existing) {
|
|
8610
|
+
if (existing !== void 0 && existing.trim() !== "") {
|
|
8532
8611
|
const parsed = parseClaudeSettings(existing);
|
|
8533
8612
|
if (!parsed) return {
|
|
8534
8613
|
filePath,
|
|
@@ -9399,7 +9478,7 @@ const GIT_GUIDE_BODY = [
|
|
|
9399
9478
|
"",
|
|
9400
9479
|
"**A merge with no conflicts is not yet a merge that works.** Read the incoming diff for what your branch builds on -- a field made required, a shared schema, a size budget -- and run the full check before you push the merge. Fix what it finds in the merge commit itself, so that commit passes on its own.",
|
|
9401
9480
|
"",
|
|
9402
|
-
"**Never trust git's merge of a baseline file** (`health-baseline.json`, `coverage-baseline.json`): a merge that reports no conflict can still drop the default branch's own entries. Take the default branch's copy, re-measure the
|
|
9481
|
+
"**Never trust git's merge of a baseline file** (`health-baseline.json`, `coverage-baseline.json`): a merge that reports no conflict can still drop the default branch's own entries. Take the default branch's copy, then re-measure: `pnpm exec bst health:raise` for `health-baseline.json`, the coverage command for the other. Adopt only what your branch changed."
|
|
9403
9482
|
].join("\n");
|
|
9404
9483
|
const PLAN_LIFECYCLE_GUIDE_BODY = [
|
|
9405
9484
|
"# Plan lifecycle",
|
|
@@ -9433,22 +9512,25 @@ const PLANNING_GUIDE_BODY = [
|
|
|
9433
9512
|
"",
|
|
9434
9513
|
"A plan may be written by you, by another session, or by a headless agent, and the run that executes it may be any of these. That run never asks which plan is meant: it finds the plan from the branch alone, so the name and the git history must say it.",
|
|
9435
9514
|
"",
|
|
9436
|
-
"- A branch
|
|
9515
|
+
"- A plan this branch **added** is found without a marker, counted at its new path when the branch moved it there with `git mv`. A plan the branch did not add is found by a marker in its path -- the marker of either half of the pair the run works, `issue-<n>` or `pr-<n>` -- or, when the branch added no plan at all, by being the one plan it modified that still has a task open. A plan with none of these is invisible to the run that should execute it, however clearly its text names the work.",
|
|
9437
9516
|
"- Name a new plan `docs/plans/<YYYY-MM-DD>-<topic>.md`, and when the work has an issue or pull request, put its marker in the name: `docs/plans/<YYYY-MM-DD>-issue-<n>-<topic>.md`. The marker is what keeps the plan findable when a second plan joins the branch.",
|
|
9438
9517
|
"- To refine a plan this branch already added, edit it in place and keep its name, so the branch never carries two plans for one piece of work.",
|
|
9439
|
-
"-
|
|
9440
|
-
"- After committing the plan, check that it is findable: `
|
|
9518
|
+
"- To work a plan the branch inherited from the default branch -- a deferred plan, or one that landed in an earlier pull request -- rename it with `git mv` to a name that carries the marker, in the first commit that touches it, then work it under that name. Use the marker of the object the run works: when an agent executes the plan on a pull request, that is the pull request's `pr-<n>`.",
|
|
9519
|
+
"- After committing the plan, check that it is findable: `pnpm exec bst docs:check` reports R9 for a plan on the branch that the lookup above could not find.",
|
|
9441
9520
|
"",
|
|
9442
9521
|
"### Writing the plan",
|
|
9443
9522
|
"",
|
|
9444
9523
|
"- Check the plan's premises against the tree before building on them. \"Today X is broken\" is a claim: read the code that handles X, not just the line that throws.",
|
|
9524
|
+
"- A plan or spec that must mention a character `chars:check` denies names it by its code point (`U+202E`) or by the escape its context uses, and never pastes the glyph: an active doc is inside the gate's scope and holds the branch red until it retires.",
|
|
9445
9525
|
"- Label each task `Task <n>:` at the start of its heading or checkbox, numbered from 1, so `plan:brief` can find it. To split a task after the plan is written, give the parts one lowercase letter (`Task 18a:`, `Task 18b:`) rather than renumbering, so later tasks and every cross-reference keep their numbers. A checkbox's first sentence is the task's name on the control panel: end it with a period inside one short line, and put the files, commands, and checks in the sentences after it.",
|
|
9446
9526
|
`- Give each task what makes it unambiguous and nothing more: a test's name and its assertions with the spec's exact values and why that test fails before the change, a code change's exact signature and file, a command with the output that means it passed. Write out a body only for an algorithm the signature and tests do not determine. Work that uses another task's result names that task's interfaces, never repeats its code. A task with no test to write says \`${TEST_EVIDENCE_PREFIXES.noTest} <reason>\` in the plan, so the executor inherits the call instead of making it cold.`,
|
|
9447
9527
|
"- Keep tasks small. Each one runs its own test cycle and ends in a result that can be checked on its own: write the failing test, watch it fail, make it pass, commit. A run cut off partway then loses one task, not the plan. A large plan is fine; a large task is not.",
|
|
9448
|
-
"- A plan that builds on earlier work opens with a task that checks the prerequisite exists, and records a `**Blocked:**` note if it does not, instead of rebuilding it.",
|
|
9528
|
+
"- A plan that builds on earlier work opens with a task that checks the prerequisite exists, and records a `**Blocked:**` note if it does not, instead of rebuilding it. It covers work a later change can supply and a run can retry once it lands; work only a person can do goes in a human phase, the next bullet.",
|
|
9529
|
+
"- Split the plan into phases, each a heading above its tasks (a `##` phase heading over `###` task headings), and mark each phase in its heading as autonomous or as needing a person; for a human phase, name what it needs: hardware, data not in the repository, access, a decision. A phase label is a heading, never a checkbox, because a checkbox reads as a task and gets claimed as work. Task numbers run across the whole plan: a phase never restarts them. The bullet above is for work a later change can supply; this one is for work only a person can do.",
|
|
9530
|
+
"- Put autonomous phases first: a human phase goes after every autonomous phase and before the retire task, and no autonomous task depends on a human phase, so the executor commits everything it can do alone before it needs a person. Open each human phase with what the person does, where they commit the result, and how the executor checks that the result is there; the check reads only what is committed, because an executor may not have the device, the data, or the access the person has, and the phase's boxes get ticked once that check passes on the committed result. Data too large to commit gets a committed stand-in the plan names, such as a sample, a summary, or recorded results; when no stand-in can do the job, the phase is hand work from start to finish and the person ticks its boxes.",
|
|
9449
9531
|
"- State a judgement that recurs across many places once, as a rule in the plan, so every task decides it the same way.",
|
|
9450
9532
|
"- Where the property that matters is out of the executor's reach (a container, a live process, a real network), say so and name the test that stands in for watching it.",
|
|
9451
|
-
"- The last task retires the plan (see the plan-lifecycle guide). Its checkbox text starts with the literal `Retire this plan`, not `Task <n>:`, so tooling can recognize it, and that first sentence ends there: `Retire this plan and its spec.`, then the command.",
|
|
9533
|
+
"- The last task retires the plan (see the plan-lifecycle guide). Its checkbox text starts with the literal `Retire this plan`, not `Task <n>:`, so tooling can recognize it, and that first sentence ends there: `Retire this plan and its spec.`, then the command. Put that box under its own heading after the last phase rather than straight under the last task's boxes, so it reads as its own unit and not as another step of that task.",
|
|
9452
9534
|
"- Reread the finished plan against the spec. A plan several times longer than its spec has written the program instead of the decisions: replace bodies with signatures, test names, and assertions. A line that decides nothing (\"TBD\", \"handle edge cases\", a type no task defines) is the opposite failure. Fix both.",
|
|
9453
9535
|
"",
|
|
9454
9536
|
"## Working through a plan",
|
|
@@ -9505,18 +9587,18 @@ function webUiGuideBody() {
|
|
|
9505
9587
|
const mwg = `pnpm dlx modern-web-guidance@${{
|
|
9506
9588
|
"@changesets/cli": "3.0.3",
|
|
9507
9589
|
"@release-it/bumper": "8.0.1",
|
|
9508
|
-
"@types/node": "24.19.
|
|
9590
|
+
"@types/node": "24.19.2",
|
|
9509
9591
|
"@vitest/coverage-v8": "5.0.3",
|
|
9510
|
-
"commit-and-tag-version": "13.
|
|
9592
|
+
"commit-and-tag-version": "13.3.0",
|
|
9511
9593
|
"knip": "6.41.0",
|
|
9512
|
-
"lefthook": "2.2.
|
|
9513
|
-
"modern-web-guidance": "0.0.
|
|
9514
|
-
"oxfmt": "0.
|
|
9594
|
+
"lefthook": "2.2.2",
|
|
9595
|
+
"modern-web-guidance": "0.0.194",
|
|
9596
|
+
"oxfmt": "0.73.0",
|
|
9515
9597
|
"oxlint": "1.87.0",
|
|
9516
|
-
"oxlint-tsgolint": "7.0.
|
|
9517
|
-
"prettier": "3.9.
|
|
9598
|
+
"oxlint-tsgolint": "7.0.2004",
|
|
9599
|
+
"prettier": "3.9.10",
|
|
9518
9600
|
"release-it": "21.1.1",
|
|
9519
|
-
"tsdown": "0.23.
|
|
9601
|
+
"tsdown": "0.23.1",
|
|
9520
9602
|
"typescript": "6.0.3",
|
|
9521
9603
|
"vitest": "5.0.3",
|
|
9522
9604
|
"pnpm": "11.28.5"
|
|
@@ -9613,11 +9695,13 @@ const HEALTH_RULE_TAIL = [
|
|
|
9613
9695
|
"",
|
|
9614
9696
|
"So never duplicate a type, or fork a helper, to protect a fan budget -- import it. Nor move a module out of its home directory to dodge crowding. The converse holds: turning a type-only import into a value import adds an edge, so a module that already imports another for its types gains a real edge the first time it imports a value from it.",
|
|
9615
9697
|
"",
|
|
9616
|
-
"**An exemption takes a module or directory out of the gate.** It is listed with its count and reason in `health:summary --explain`, carries no ceiling, and is counted in no metric. `crowdedDirectories` is exemptable by directory path. Adding one is a raise too.",
|
|
9698
|
+
"**An exemption takes a module or directory out of the gate.** It is listed with its count and reason in `health:summary --explain`, carries no ceiling, and is counted in no metric. `crowdedDirectories` is exemptable by directory path. A key may be a glob. Adding one is a raise too.",
|
|
9699
|
+
"",
|
|
9700
|
+
"**A raise travels with the code that caused it.** A raise is a higher count or ceiling, a ceiling for a module that has none, a new exemption, or a higher limit. Write it with `pnpm exec bst health:raise` in the pull request whose code crossed the budget; say in the commit why, and a person merges it. Its `--exempt <leaf> --reason \"<text>\"` is for a hub by construction. Never raise a limit to pass. `--update-baseline --reset` rebuilds a baseline, never one branch.",
|
|
9617
9701
|
"",
|
|
9618
|
-
"
|
|
9702
|
+
"Prefer an exemption to raising `fanInLimit` or `fanOutLimit`: an exemption names the hub in `health:summary --explain`, while a higher limit silently un-tracks every module beneath it. A per-module ceiling has no such shadow -- it ratchets one module at one count and fails again one edge later.",
|
|
9619
9703
|
"",
|
|
9620
|
-
"
|
|
9704
|
+
"**Never contort code to meet a budget.** Write the correct solution, then adjust the budget to match: a health ceiling as the raise rule above says, and a bundle or other size budget by a full 1%, rounded up, never to the byte."
|
|
9621
9705
|
];
|
|
9622
9706
|
function healthRuleBody(config) {
|
|
9623
9707
|
const lines = [...HEALTH_RULE_HEAD];
|
|
@@ -9752,7 +9836,7 @@ function agentGuides(config, coverageEnabled, healthEnabled, webUiEnabled) {
|
|
|
9752
9836
|
},
|
|
9753
9837
|
{
|
|
9754
9838
|
name: "health.md",
|
|
9755
|
-
trigger: "add a cross-module import, or when `health:check` fails",
|
|
9839
|
+
trigger: "add a cross-module import, or when `health:check` or a size budget fails",
|
|
9756
9840
|
body: healthRuleBody(config),
|
|
9757
9841
|
enabled: healthEnabled
|
|
9758
9842
|
}
|
|
@@ -10079,7 +10163,7 @@ function reportFailures(failed, command) {
|
|
|
10079
10163
|
function generateMigratePrompt(results, config, detected) {
|
|
10080
10164
|
const sections = [];
|
|
10081
10165
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
10082
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.
|
|
10166
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.95.0 repo:sync\` on ${timestamp}_\n`);
|
|
10083
10167
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
10084
10168
|
if (changed.length > 0) {
|
|
10085
10169
|
const items = changed.map((r) => {
|
|
@@ -10879,6 +10963,21 @@ function parseWarnMarkerLine(line) {
|
|
|
10879
10963
|
const count = Number.parseInt(match[1] ?? "", 10);
|
|
10880
10964
|
return Number.isNaN(count) ? null : count;
|
|
10881
10965
|
}
|
|
10966
|
+
const MEASURE_LINE = new RegExp(`^::bst-health:: measure (\\d+)\\s*$`);
|
|
10967
|
+
function parseMeasureMarkerLine(line) {
|
|
10968
|
+
const match = MEASURE_LINE.exec(line);
|
|
10969
|
+
if (match === null) return void 0;
|
|
10970
|
+
const value = Number.parseInt(match[1] ?? "", 10);
|
|
10971
|
+
return Number.isNaN(value) ? void 0 : value;
|
|
10972
|
+
}
|
|
10973
|
+
function readMeasureMarker(stdout) {
|
|
10974
|
+
let found;
|
|
10975
|
+
for (const line of stdout.split("\n")) {
|
|
10976
|
+
const parsed = parseMeasureMarkerLine(line);
|
|
10977
|
+
if (parsed !== void 0) found = parsed;
|
|
10978
|
+
}
|
|
10979
|
+
return found;
|
|
10980
|
+
}
|
|
10882
10981
|
//#endregion
|
|
10883
10982
|
//#region src/checks/step-exec.ts
|
|
10884
10983
|
function execStep(cmd, cwd, sink = process.stdout) {
|
|
@@ -10976,7 +11075,7 @@ function createRealHealthExecutor(rootDir) {
|
|
|
10976
11075
|
};
|
|
10977
11076
|
return {
|
|
10978
11077
|
stdout: "",
|
|
10979
|
-
stderr:
|
|
11078
|
+
stderr: describeError$1(err),
|
|
10980
11079
|
exitCode: 1
|
|
10981
11080
|
};
|
|
10982
11081
|
}
|
|
@@ -11027,17 +11126,13 @@ function createRealHealthExecutor(rootDir) {
|
|
|
11027
11126
|
}
|
|
11028
11127
|
//#endregion
|
|
11029
11128
|
//#region src/codebase-health/reference.ts
|
|
11030
|
-
const HEALTH_CEILING_FILES = ["health-baseline.json", ".tooling.json"];
|
|
11031
11129
|
const BASELINE_FILE$2 = "health-baseline.json";
|
|
11032
|
-
const TOOLING_FILE = ".tooling.json";
|
|
11130
|
+
const TOOLING_FILE$1 = ".tooling.json";
|
|
11033
11131
|
function repoPrefix(executor, cwd) {
|
|
11034
11132
|
const result = executor.exec("git", ["rev-parse", "--show-prefix"], { cwd });
|
|
11035
11133
|
if (result.exitCode !== 0) return "";
|
|
11036
11134
|
return result.stdout.trim();
|
|
11037
11135
|
}
|
|
11038
|
-
function ceilingFileNamesAt(prefix) {
|
|
11039
|
-
return HEALTH_CEILING_FILES.map((name) => `${prefix}${name}`);
|
|
11040
|
-
}
|
|
11041
11136
|
const DefaultBranchSchema = z.object({ defaultBranch: z.string().optional() });
|
|
11042
11137
|
function parseDefaultBranchOverride(raw) {
|
|
11043
11138
|
if (raw === void 0 || raw.trim() === "") return void 0;
|
|
@@ -11061,9 +11156,9 @@ function resolveReferenceBranch(executor, cwd, prefix, ciEvent) {
|
|
|
11061
11156
|
"--quiet",
|
|
11062
11157
|
ref
|
|
11063
11158
|
], cwd) !== void 0) {
|
|
11064
|
-
const branch = parseDefaultBranchOverride(showAt(executor, cwd, ref, `${prefix}${TOOLING_FILE}`)) ?? chosen;
|
|
11159
|
+
const branch = parseDefaultBranchOverride(showAt(executor, cwd, ref, `${prefix}${TOOLING_FILE$1}`)) ?? chosen;
|
|
11065
11160
|
if (anchor !== void 0) return { branch };
|
|
11066
|
-
const namedElsewhere = parseDefaultBranchOverride(executor.readFile(TOOLING_FILE));
|
|
11161
|
+
const namedElsewhere = parseDefaultBranchOverride(executor.readFile(TOOLING_FILE$1));
|
|
11067
11162
|
if (namedElsewhere === void 0 || namedElsewhere === branch) return { branch };
|
|
11068
11163
|
return {
|
|
11069
11164
|
branch,
|
|
@@ -11071,7 +11166,7 @@ function resolveReferenceBranch(executor, cwd, prefix, ciEvent) {
|
|
|
11071
11166
|
};
|
|
11072
11167
|
}
|
|
11073
11168
|
if (anchor !== void 0) return { branch: chosen };
|
|
11074
|
-
return { branch: parseDefaultBranchOverride(executor.readFile(TOOLING_FILE)) ?? "main" };
|
|
11169
|
+
return { branch: parseDefaultBranchOverride(executor.readFile(TOOLING_FILE$1)) ?? "main" };
|
|
11075
11170
|
}
|
|
11076
11171
|
function originHeadBranch(git, cwd) {
|
|
11077
11172
|
const head = git([
|
|
@@ -11113,10 +11208,10 @@ function readReference(executor, opts) {
|
|
|
11113
11208
|
reason: `health: cannot read the reference at the merge base with origin/${branch}. Run \`git fetch origin ${branch}\` so the default branch is present (a shallow clone or a missing remote-tracking ref makes it absent), or set .tooling.json#defaultBranch on the default branch if it has another name -- once that branch is fetched, its own copy of the setting decides, not this branch's. health:check fails closed without a reference.`
|
|
11114
11209
|
};
|
|
11115
11210
|
const baselineText = showAt(executor, opts.cwd, sha, `${prefix}${BASELINE_FILE$2}`);
|
|
11116
|
-
const toolingText = showAt(executor, opts.cwd, sha, `${prefix}${TOOLING_FILE}`);
|
|
11211
|
+
const toolingText = showAt(executor, opts.cwd, sha, `${prefix}${TOOLING_FILE$1}`);
|
|
11117
11212
|
const health = resolveHealthConfigFromContext({
|
|
11118
|
-
exists: (rel) => rel === TOOLING_FILE && toolingText !== void 0,
|
|
11119
|
-
read: (rel) => rel === TOOLING_FILE ? toolingText : void 0,
|
|
11213
|
+
exists: (rel) => rel === TOOLING_FILE$1 && toolingText !== void 0,
|
|
11214
|
+
read: (rel) => rel === TOOLING_FILE$1 ? toolingText : void 0,
|
|
11120
11215
|
config: { stack: opts.stack }
|
|
11121
11216
|
}, { isMonorepo: opts.isMonorepo });
|
|
11122
11217
|
const reference = {
|
|
@@ -11155,8 +11250,67 @@ const RAISABLE_LIMITS = [
|
|
|
11155
11250
|
"moduleSizeLimit",
|
|
11156
11251
|
"tolerance"
|
|
11157
11252
|
];
|
|
11253
|
+
const GATE_SWITCHES = ["enabled", "crossPackage"];
|
|
11254
|
+
const EXCLUDE_LEAF_PREFIX = "health.exclude.";
|
|
11255
|
+
function canonicalJson(value) {
|
|
11256
|
+
return JSON.stringify(sortKeys(value));
|
|
11257
|
+
}
|
|
11258
|
+
const JsonObjectSchema$1 = z.record(z.string(), z.unknown());
|
|
11259
|
+
function sortKeys(value) {
|
|
11260
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
11261
|
+
const object = JsonObjectSchema$1.safeParse(value);
|
|
11262
|
+
if (object.success) {
|
|
11263
|
+
const keys = Object.keys(object.data).toSorted();
|
|
11264
|
+
const sorted = {};
|
|
11265
|
+
for (const key of keys) sorted[key] = sortKeys(object.data[key]);
|
|
11266
|
+
return sorted;
|
|
11267
|
+
}
|
|
11268
|
+
return value;
|
|
11269
|
+
}
|
|
11270
|
+
function sizeRaises(reference, current) {
|
|
11271
|
+
const raises = [];
|
|
11272
|
+
for (const [id, base] of Object.entries(reference.baseline?.sizeBudgets ?? {})) {
|
|
11273
|
+
if (current.baseline === void 0) continue;
|
|
11274
|
+
const now = current.baseline.sizeBudgets?.[id];
|
|
11275
|
+
if (now === void 0) {
|
|
11276
|
+
if (current.health.sizeBudgets[id] !== void 0) raises.push({
|
|
11277
|
+
leaf: `sizeBudgets.${id}`,
|
|
11278
|
+
reference: String(base),
|
|
11279
|
+
current: "absent"
|
|
11280
|
+
});
|
|
11281
|
+
continue;
|
|
11282
|
+
}
|
|
11283
|
+
if (now > base) raises.push({
|
|
11284
|
+
leaf: `sizeBudgets.${id}`,
|
|
11285
|
+
reference: String(base),
|
|
11286
|
+
current: String(now)
|
|
11287
|
+
});
|
|
11288
|
+
}
|
|
11289
|
+
for (const [id, base] of Object.entries(reference.health.sizeBudgets)) {
|
|
11290
|
+
const now = current.health.sizeBudgets[id];
|
|
11291
|
+
if (now === void 0) {
|
|
11292
|
+
raises.push({
|
|
11293
|
+
leaf: `health.sizeBudgets.${id}`,
|
|
11294
|
+
reference: "present",
|
|
11295
|
+
current: "absent"
|
|
11296
|
+
});
|
|
11297
|
+
continue;
|
|
11298
|
+
}
|
|
11299
|
+
const baseJson = canonicalJson(base);
|
|
11300
|
+
if (baseJson !== canonicalJson(now)) raises.push({
|
|
11301
|
+
leaf: `health.sizeBudgets.${id}`,
|
|
11302
|
+
reference: baseJson,
|
|
11303
|
+
current: canonicalJson(now)
|
|
11304
|
+
});
|
|
11305
|
+
}
|
|
11306
|
+
return raises;
|
|
11307
|
+
}
|
|
11158
11308
|
function findRaises(reference, current) {
|
|
11159
|
-
return [
|
|
11309
|
+
return [
|
|
11310
|
+
...baselineRaises(reference, current),
|
|
11311
|
+
...sizeRaises(reference, current),
|
|
11312
|
+
...configRaises(reference.health, current.health)
|
|
11313
|
+
];
|
|
11160
11314
|
}
|
|
11161
11315
|
function baselineRaises(reference, current) {
|
|
11162
11316
|
const raises = [];
|
|
@@ -11168,7 +11322,7 @@ function baselineRaises(reference, current) {
|
|
|
11168
11322
|
reference: String(base),
|
|
11169
11323
|
current: String(now)
|
|
11170
11324
|
});
|
|
11171
|
-
else if (base !== void 0 && now === void 0 && current.baseline !== void 0 && current.measured
|
|
11325
|
+
else if (base !== void 0 && now === void 0 && current.baseline !== void 0 && (current.measured === void 0 || current.measured.metrics[key] !== void 0)) raises.push({
|
|
11172
11326
|
leaf: key,
|
|
11173
11327
|
reference: String(base),
|
|
11174
11328
|
current: "absent"
|
|
@@ -11179,7 +11333,7 @@ function baselineRaises(reference, current) {
|
|
|
11179
11333
|
for (const [module, base] of Object.entries(reference.baseline?.moduleCeilings?.[key] ?? {})) {
|
|
11180
11334
|
if (current.baseline === void 0) continue;
|
|
11181
11335
|
if (current.baseline.moduleCeilings?.[key]?.[module] !== void 0) continue;
|
|
11182
|
-
if (measured[module] === void 0) continue;
|
|
11336
|
+
if (current.measured !== void 0 && measured[module] === void 0) continue;
|
|
11183
11337
|
raises.push({
|
|
11184
11338
|
leaf: `moduleCeilings.${key}.${module}`,
|
|
11185
11339
|
reference: String(base),
|
|
@@ -11233,11 +11387,11 @@ function configRaises(reference, current) {
|
|
|
11233
11387
|
});
|
|
11234
11388
|
}
|
|
11235
11389
|
for (const pattern of current.exclude) if (!reference.exclude.includes(pattern)) raises.push({
|
|
11236
|
-
leaf:
|
|
11390
|
+
leaf: `${EXCLUDE_LEAF_PREFIX}${pattern}`,
|
|
11237
11391
|
reference: "absent",
|
|
11238
11392
|
current: "present"
|
|
11239
11393
|
});
|
|
11240
|
-
for (const name of
|
|
11394
|
+
for (const name of GATE_SWITCHES) if (reference[name] && !current[name]) raises.push({
|
|
11241
11395
|
leaf: `health.${name}`,
|
|
11242
11396
|
reference: "true",
|
|
11243
11397
|
current: "false"
|
|
@@ -11265,11 +11419,6 @@ function branchDiffNames(executor, cwd, mergeBase) {
|
|
|
11265
11419
|
}
|
|
11266
11420
|
return [...names].toSorted();
|
|
11267
11421
|
}
|
|
11268
|
-
function isCeilingOnly(names, prefix = "") {
|
|
11269
|
-
if (names.length === 0) return false;
|
|
11270
|
-
const files = ceilingFileNamesAt(prefix);
|
|
11271
|
-
return names.every((name) => files.includes(name));
|
|
11272
|
-
}
|
|
11273
11422
|
//#endregion
|
|
11274
11423
|
//#region ../forgejo/src/git.ts
|
|
11275
11424
|
const GIT_LOCAL_TIMEOUT_MS = 6e4;
|
|
@@ -11932,6 +12081,26 @@ function logCoverageChoice(vc, coverage, useCoverageScript, requested) {
|
|
|
11932
12081
|
if (coverage.enabled && !useCoverageScript && coverage.source === "config") log$1.warn("coverage.gateOnPullRequests is set but test:coverage script is not defined; falling back to test");
|
|
11933
12082
|
debug(vc, `Coverage: ${requested ?? "auto"} (source: ${coverage.source}) -> ${useCoverageScript ? "test:coverage" : "test"}`);
|
|
11934
12083
|
}
|
|
12084
|
+
function withLegacyCharsScript(definedScripts) {
|
|
12085
|
+
const scripts = new Set(definedScripts);
|
|
12086
|
+
if (!scripts.has(CHARS_CHECK.name) && definedScripts.has(LEGACY_CHARS_CHECK_SCRIPT)) scripts.add(CHARS_CHECK.name);
|
|
12087
|
+
return scripts;
|
|
12088
|
+
}
|
|
12089
|
+
function withAddedChecks(builtinChecks, add) {
|
|
12090
|
+
const builtinNames = new Set(builtinChecks.map((check) => check.name));
|
|
12091
|
+
function isBuiltInName(name) {
|
|
12092
|
+
if (builtinNames.has(name)) return true;
|
|
12093
|
+
const alias = legacyCharsAlias(name);
|
|
12094
|
+
return alias !== void 0 && builtinNames.has(alias);
|
|
12095
|
+
}
|
|
12096
|
+
return [...builtinChecks, ...add.filter((name) => !isBuiltInName(name)).map((name) => ({ name }))];
|
|
12097
|
+
}
|
|
12098
|
+
function scriptNameFor(checkName, definedScripts, useCoverageScript) {
|
|
12099
|
+
if (checkName === "test" && useCoverageScript) return "test:coverage";
|
|
12100
|
+
if (checkName === "lint" && definedScripts.has("lint:report")) return "lint:report";
|
|
12101
|
+
if (checkName === CHARS_CHECK.name && !definedScripts.has(CHARS_CHECK.name) && definedScripts.has(LEGACY_CHARS_CHECK_SCRIPT)) return LEGACY_CHARS_CHECK_SCRIPT;
|
|
12102
|
+
return checkName;
|
|
12103
|
+
}
|
|
11935
12104
|
async function runRunChecks(targetDir, options = {}) {
|
|
11936
12105
|
const exec = options.execCommand ?? execStep;
|
|
11937
12106
|
const getScripts = options.getScripts ?? defaultGetScripts;
|
|
@@ -11946,17 +12115,9 @@ async function runRunChecks(targetDir, options = {}) {
|
|
|
11946
12115
|
const reporter = isCI ? ciReporter : localReporter;
|
|
11947
12116
|
const vc = { verbose: options.verbose ?? false };
|
|
11948
12117
|
const definedScripts = getScripts(targetDir);
|
|
11949
|
-
const definedCheckScripts =
|
|
11950
|
-
if (!definedCheckScripts.has(CHARS_CHECK.name) && definedScripts.has(LEGACY_CHARS_CHECK_SCRIPT)) definedCheckScripts.add(CHARS_CHECK.name);
|
|
12118
|
+
const definedCheckScripts = withLegacyCharsScript(definedScripts);
|
|
11951
12119
|
const addedNames = new Set(add);
|
|
11952
|
-
const
|
|
11953
|
-
const builtinNames = new Set(builtinChecks.map((check) => check.name));
|
|
11954
|
-
function isBuiltInName(name) {
|
|
11955
|
-
if (builtinNames.has(name)) return true;
|
|
11956
|
-
const alias = legacyCharsAlias(name);
|
|
11957
|
-
return alias !== void 0 && builtinNames.has(alias);
|
|
11958
|
-
}
|
|
11959
|
-
const allChecks = [...builtinChecks, ...add.filter((name) => !isBuiltInName(name)).map((name) => ({ name }))];
|
|
12120
|
+
const allChecks = withAddedChecks(resolveRunBuiltins(targetDir, toolingConfig, options.referenceEnablesHealth), add);
|
|
11960
12121
|
const failures = [];
|
|
11961
12122
|
const warnedSteps = [];
|
|
11962
12123
|
const notDefined = [];
|
|
@@ -11980,10 +12141,7 @@ async function runRunChecks(targetDir, options = {}) {
|
|
|
11980
12141
|
} else notDefined.push(check.name);
|
|
11981
12142
|
continue;
|
|
11982
12143
|
}
|
|
11983
|
-
|
|
11984
|
-
if (check.name === "test" && useCoverageScript) scriptName = "test:coverage";
|
|
11985
|
-
if (check.name === "lint" && definedScripts.has("lint:report")) scriptName = "lint:report";
|
|
11986
|
-
if (check.name === CHARS_CHECK.name && !definedScripts.has(CHARS_CHECK.name) && definedScripts.has(LEGACY_CHARS_CHECK_SCRIPT)) scriptName = LEGACY_CHARS_CHECK_SCRIPT;
|
|
12144
|
+
const scriptName = scriptNameFor(check.name, definedScripts, useCoverageScript);
|
|
11987
12145
|
const cmd = check.args ? `pnpm run ${scriptName} ${check.args}` : `pnpm run ${scriptName}`;
|
|
11988
12146
|
debug(vc, `Running: ${cmd} (in ${targetDir})`);
|
|
11989
12147
|
reporter.groupStart(check.name);
|
|
@@ -12213,7 +12371,7 @@ function findInertAgentKeys(saved) {
|
|
|
12213
12371
|
function runInertAgentSettingsGate(saved) {
|
|
12214
12372
|
const keys = findInertAgentKeys(saved);
|
|
12215
12373
|
if (keys.length === 0) return [];
|
|
12216
|
-
log$1.warn(`.tooling.json sets ${keys.map((key) => `agent.${key}`).join(", ")}, but agent.enabled is not true, so the agent ignores them. Run \`bst setup:agent\` to enable the agent, or remove those keys.`);
|
|
12374
|
+
log$1.warn(`.tooling.json sets ${keys.map((key) => `agent.${key}`).join(", ")}, but agent.enabled is not true, so the agent ignores them. Run \`pnpm exec bst setup:agent\` to enable the agent, or remove those keys.`);
|
|
12217
12375
|
return keys;
|
|
12218
12376
|
}
|
|
12219
12377
|
function runPublishConfigGate(config, targetDir) {
|
|
@@ -12271,7 +12429,7 @@ function warnUnconfiguredDefaultBranch(targetDir, saved, git = defaultGitRunner)
|
|
|
12271
12429
|
if (head === void 0) return;
|
|
12272
12430
|
const name = head.startsWith("origin/") ? head.slice(7) : head;
|
|
12273
12431
|
if (name === "" || name === "main") return;
|
|
12274
|
-
log$1.warn(`origin/HEAD names "${name}", but .tooling.json sets no defaultBranch, so the generated CI workflow triggers on "${DEFAULT_BRANCH}" and gates its release steps there. Set .tooling.json#defaultBranch to "${name}" and run bst repo:sync.`);
|
|
12432
|
+
log$1.warn(`origin/HEAD names "${name}", but .tooling.json sets no defaultBranch, so the generated CI workflow triggers on "${DEFAULT_BRANCH}" and gates its release steps there. Set .tooling.json#defaultBranch to "${name}" and run pnpm exec bst repo:sync.`);
|
|
12275
12433
|
}
|
|
12276
12434
|
function warnCoveredChecksAdd(config, targetDir) {
|
|
12277
12435
|
const covered = coveredChecksAddEntries(config, readPackageJson(targetDir)?.scripts ?? {});
|
|
@@ -12483,7 +12641,7 @@ async function runUpdateReport(targetDir, flags = {}, options = {}) {
|
|
|
12483
12641
|
const dockerDiscovery = createDockerDiscoveryMemo();
|
|
12484
12642
|
const saved = loadToolingConfigStrict(targetDir);
|
|
12485
12643
|
if (!saved) {
|
|
12486
|
-
log$1.error("No .tooling.json found. Run `bst repo:sync` first to initialize the project.");
|
|
12644
|
+
log$1.error("No .tooling.json found. Run `pnpm exec bst repo:sync` first to initialize the project.");
|
|
12487
12645
|
process.exitCode = 1;
|
|
12488
12646
|
return {
|
|
12489
12647
|
status: "stopped",
|
|
@@ -13118,7 +13276,9 @@ async function runVersionMode(executor, config) {
|
|
|
13118
13276
|
debug(config, `Changed packages: ${changedDirs.length > 0 ? changedDirs.join(", ") : "(none)"}`);
|
|
13119
13277
|
stampChangelogDates(executor, config.cwd, changedDirs, true);
|
|
13120
13278
|
} else stampChangelogDates(executor, config.cwd, [], false);
|
|
13121
|
-
|
|
13279
|
+
const installResult = executor.exec("pnpm", ["install", "--no-frozen-lockfile"], { cwd: config.cwd });
|
|
13280
|
+
debugExec(config, "pnpm install --no-frozen-lockfile", installResult);
|
|
13281
|
+
if (installResult.exitCode !== 0) throw new FatalError(`pnpm install --no-frozen-lockfile failed (exit code ${String(installResult.exitCode)}):\n${childOutput(installResult)}`);
|
|
13122
13282
|
const { title, body } = buildPrContent(executor, config.cwd, packagesBefore);
|
|
13123
13283
|
debug(config, `PR title: ${title}`);
|
|
13124
13284
|
const addResult = executor.exec("git", ["add", "-A"], { cwd: config.cwd });
|
|
@@ -14650,7 +14810,7 @@ function ownerOf(names) {
|
|
|
14650
14810
|
}
|
|
14651
14811
|
//#endregion
|
|
14652
14812
|
//#region src/work-status/claim.ts
|
|
14653
|
-
const HANDOVER_ROUTES = "Claim it from the control panel's \"Claim for hand work\", or remove the agent label by hand and run bst work:start again.";
|
|
14813
|
+
const HANDOVER_ROUTES = "Claim it from the control panel's \"Claim for hand work\", or remove the agent label by hand and run pnpm exec bst work:start again.";
|
|
14654
14814
|
function decideClaim(report, enabled) {
|
|
14655
14815
|
if (!enabled) return {
|
|
14656
14816
|
kind: "answer",
|
|
@@ -14725,7 +14885,7 @@ async function writeClaim(executor, cwd, target, number) {
|
|
|
14725
14885
|
const flag = (await fetchAllForgejoLabels(executor, conn)).find((label) => label.name === HAND_AUTHORED_LABEL);
|
|
14726
14886
|
if (flag === void 0) return {
|
|
14727
14887
|
outcome: "failed",
|
|
14728
|
-
reason: `The forge has no label named ${HAND_AUTHORED_LABEL}. Provision the agent's labels first with bst-agent agent:labels --repo ${conn.repository}, or the control panel's Provision labels.`
|
|
14888
|
+
reason: `The forge has no label named ${HAND_AUTHORED_LABEL}. Provision the agent's labels first with pnpm exec bst-agent agent:labels --repo ${conn.repository}, or the control panel's Provision labels.`
|
|
14729
14889
|
};
|
|
14730
14890
|
await addForgejoIssueLabel(executor, conn, number, HAND_AUTHORED_LABEL, /* @__PURE__ */ new Map([[HAND_AUTHORED_LABEL, flag.id]]));
|
|
14731
14891
|
return {
|
|
@@ -14742,14 +14902,19 @@ async function writeClaim(executor, cwd, target, number) {
|
|
|
14742
14902
|
//#endregion
|
|
14743
14903
|
//#region src/work-status/consent.ts
|
|
14744
14904
|
async function readAgentConsent(git, defaultBranch) {
|
|
14745
|
-
const
|
|
14905
|
+
const base = await git.resolveBase(defaultBranch);
|
|
14906
|
+
if (base !== `origin/${defaultBranch}`) return {
|
|
14907
|
+
enabled: agentEnabledIn(readWorkingTree(git.topLevel)),
|
|
14908
|
+
source: "working-tree"
|
|
14909
|
+
};
|
|
14910
|
+
const onBranch = await git.show(base, ".tooling.json");
|
|
14746
14911
|
if (onBranch !== null) return {
|
|
14747
14912
|
enabled: agentEnabledIn(onBranch),
|
|
14748
14913
|
source: "default-branch"
|
|
14749
14914
|
};
|
|
14750
14915
|
return {
|
|
14751
|
-
enabled:
|
|
14752
|
-
source: "
|
|
14916
|
+
enabled: false,
|
|
14917
|
+
source: "default-branch"
|
|
14753
14918
|
};
|
|
14754
14919
|
}
|
|
14755
14920
|
function readWorkingTree(topLevel) {
|
|
@@ -15148,6 +15313,17 @@ function githubPrHost(executor, cwd, signal) {
|
|
|
15148
15313
|
}
|
|
15149
15314
|
};
|
|
15150
15315
|
}
|
|
15316
|
+
function parseGhJson(stdout, command) {
|
|
15317
|
+
let payload;
|
|
15318
|
+
try {
|
|
15319
|
+
payload = JSON.parse(stdout);
|
|
15320
|
+
} catch {
|
|
15321
|
+
throw new UnexpectedError(`'${command}' printed something that is not JSON: ${stdout.trim().slice(0, 200) || "no output"}`);
|
|
15322
|
+
}
|
|
15323
|
+
const value = z.json().safeParse(payload);
|
|
15324
|
+
if (!value.success) throw new UnexpectedError(`Unexpected '${command}' response: ${value.error.message} (${stdout.trim().slice(0, 200) || "no output"})`);
|
|
15325
|
+
return value.data;
|
|
15326
|
+
}
|
|
15151
15327
|
const GitHubPrContentSchema = z.object({
|
|
15152
15328
|
title: z.string(),
|
|
15153
15329
|
body: z.string().nullish()
|
|
@@ -15165,7 +15341,7 @@ async function readGitHubPrContent(executor, cwd, prNumber, signal) {
|
|
|
15165
15341
|
signal
|
|
15166
15342
|
});
|
|
15167
15343
|
if (result.exitCode !== 0) throw new FatalError(`Failed to read PR #${String(prNumber)}: ${result.stderr || result.stdout}`);
|
|
15168
|
-
const parsed = GitHubPrContentSchema.safeParse(
|
|
15344
|
+
const parsed = GitHubPrContentSchema.safeParse(parseGhJson(result.stdout, "gh pr view"));
|
|
15169
15345
|
if (!parsed.success) throw new UnexpectedError(`Unexpected 'gh pr view' response: ${parsed.error.message}`);
|
|
15170
15346
|
return {
|
|
15171
15347
|
title: parsed.data.title,
|
|
@@ -15194,7 +15370,7 @@ async function findOpenGitHubPr(executor, cwd, branch, signal) {
|
|
|
15194
15370
|
if (result.exitCode !== 0) throw new TransientError(`'gh pr list' exited ${String(result.exitCode)}: ${result.stderr.trim() || "no output"} -- cannot tell whether ${branch} has an open PR`);
|
|
15195
15371
|
const out = result.stdout.trim();
|
|
15196
15372
|
if (out === "") return null;
|
|
15197
|
-
const parsed = GitHubPrListSchema.safeParse(
|
|
15373
|
+
const parsed = GitHubPrListSchema.safeParse(parseGhJson(out, "gh pr list"));
|
|
15198
15374
|
if (!parsed.success) throw new UnexpectedError(`Unexpected 'gh pr list' response: ${parsed.error.message}`);
|
|
15199
15375
|
const first = parsed.data[0];
|
|
15200
15376
|
return first === void 0 ? null : {
|
|
@@ -15290,7 +15466,7 @@ function githubPrEditHost(executor, cwd, repository) {
|
|
|
15290
15466
|
"state,isDraft,title,headRefName,baseRefName,mergeable,url"
|
|
15291
15467
|
]);
|
|
15292
15468
|
if (result.exitCode !== 0) throw new FatalError(`Failed to read PR #${String(number)}: ${result.stderr || result.stdout}`);
|
|
15293
|
-
const parsed = GitHubPrEditStateSchema.safeParse(
|
|
15469
|
+
const parsed = GitHubPrEditStateSchema.safeParse(parseGhJson(result.stdout, "gh pr view"));
|
|
15294
15470
|
if (!parsed.success) throw new UnexpectedError(`Unexpected 'gh pr view' response: ${parsed.error.message}`);
|
|
15295
15471
|
const data = parsed.data;
|
|
15296
15472
|
return {
|
|
@@ -17226,7 +17402,9 @@ var MarkupLexer = class {
|
|
|
17226
17402
|
matchesIslandClose() {
|
|
17227
17403
|
const start = this.i + 2;
|
|
17228
17404
|
const end = start + this.islandCloseTag.length;
|
|
17229
|
-
|
|
17405
|
+
if (this.text.slice(start, end).toLowerCase() !== this.islandCloseTag) return false;
|
|
17406
|
+
const next = this.text[end] ?? "";
|
|
17407
|
+
return next === ">" || next === "/" || /\s/.test(next);
|
|
17230
17408
|
}
|
|
17231
17409
|
closeIsland() {
|
|
17232
17410
|
this.flushTo(this.i, "code");
|
|
@@ -18255,6 +18433,9 @@ function parsePlanBoxes(planText) {
|
|
|
18255
18433
|
};
|
|
18256
18434
|
});
|
|
18257
18435
|
}
|
|
18436
|
+
function countOutstandingBoxes(planText, options) {
|
|
18437
|
+
return parsePlanBoxes(planText).filter((box) => isOutstanding(box, options)).length;
|
|
18438
|
+
}
|
|
18258
18439
|
//#endregion
|
|
18259
18440
|
//#region src/plan-workspace/status.ts
|
|
18260
18441
|
const RETIRED_SEGMENT = /(^|\/)retired\//;
|
|
@@ -18392,14 +18573,14 @@ function unavailableWith(reason) {
|
|
|
18392
18573
|
reason
|
|
18393
18574
|
};
|
|
18394
18575
|
}
|
|
18395
|
-
function unavailable(error) {
|
|
18576
|
+
function unavailable$1(error) {
|
|
18396
18577
|
return unavailableWith(reasonText(error));
|
|
18397
18578
|
}
|
|
18398
18579
|
async function section(build) {
|
|
18399
18580
|
try {
|
|
18400
18581
|
return await build();
|
|
18401
18582
|
} catch (error) {
|
|
18402
|
-
return unavailable(error);
|
|
18583
|
+
return unavailable$1(error);
|
|
18403
18584
|
}
|
|
18404
18585
|
}
|
|
18405
18586
|
function reasonText(error) {
|
|
@@ -18613,7 +18794,7 @@ function liftedFactsOf(resolved, forgeDown) {
|
|
|
18613
18794
|
return {
|
|
18614
18795
|
runLive: complete || read.runLive.length > 0 ? read.runLive : null,
|
|
18615
18796
|
heldByHand: complete || read.heldByHand ? read.heldByHand : null,
|
|
18616
|
-
owner: complete || ownership.owner
|
|
18797
|
+
owner: complete || ownership.owner === "agent" ? ownership.owner : null
|
|
18617
18798
|
};
|
|
18618
18799
|
}
|
|
18619
18800
|
async function docsSection({ git, docsExtraDirs, resolved }) {
|
|
@@ -18758,6 +18939,26 @@ function objectRows(value) {
|
|
|
18758
18939
|
if (value.issueUnavailable !== void 0) rows.push(` issue unavailable -- ${value.issueUnavailable}`);
|
|
18759
18940
|
return rows;
|
|
18760
18941
|
}
|
|
18942
|
+
function docsRows(value) {
|
|
18943
|
+
const rows = [` base ${value.base}`];
|
|
18944
|
+
for (const spec of value.specs) rows.push(` spec ${spec.path}${spec.retired ? " (retired)" : ""}`);
|
|
18945
|
+
for (const plan of value.plans) rows.push(` plan ${plan.path}: ${String(plan.status.checked)} of ${String(plan.status.total)} checked, ${String(plan.status.outstanding)} outstanding${plan.status.declined > 0 ? `, ${String(plan.status.declined)} declined` : ""}`);
|
|
18946
|
+
if (value.specs.length === 0 && value.plans.length === 0) rows.push(" no spec or plan changed on this branch");
|
|
18947
|
+
return rows;
|
|
18948
|
+
}
|
|
18949
|
+
function labelRows(value) {
|
|
18950
|
+
const rows = [];
|
|
18951
|
+
for (const [where, groups] of [["pull request", value.pr], ["issue", value.issue]]) {
|
|
18952
|
+
if (groups.length === 0) continue;
|
|
18953
|
+
rows.push(` ${where}:`);
|
|
18954
|
+
for (const group of groups) for (const label of group.labels) {
|
|
18955
|
+
const described = label.description === null ? "" : ` -- ${label.description}`;
|
|
18956
|
+
rows.push(` ${label.name}${described} [${group.scope}]`);
|
|
18957
|
+
}
|
|
18958
|
+
}
|
|
18959
|
+
if (value.descriptionsUnavailable !== void 0) rows.push(` descriptions unavailable -- ${value.descriptionsUnavailable}`);
|
|
18960
|
+
return rows;
|
|
18961
|
+
}
|
|
18761
18962
|
function nothingInFlight(report) {
|
|
18762
18963
|
const { object, docs, commits } = report;
|
|
18763
18964
|
if (object.state !== "absent" || docs.state !== "ok" || commits.state !== "ok") return null;
|
|
@@ -18785,29 +18986,11 @@ function renderWorkStatus(report, options) {
|
|
|
18785
18986
|
if (report.heldByHand === true) lines.push("HELD BY HAND: agent-flags/hand-authored -- a human claimed this one; do not drive it.");
|
|
18786
18987
|
if (options?.agentEnabled === true && report.owner === "unclaimed") {
|
|
18787
18988
|
const number = claimableNumber(report);
|
|
18788
|
-
if (number !== null) lines.push(`UNCLAIMED: no agent label and no hand claim -- run \`bst work:start ${String(number)}\` before you work this.`);
|
|
18989
|
+
if (number !== null) lines.push(`UNCLAIMED: no agent label and no hand claim -- run \`pnpm exec bst work:start ${String(number)}\` before you work this.`);
|
|
18789
18990
|
}
|
|
18790
18991
|
lines.push(...block("object", report.object, objectRows));
|
|
18791
|
-
lines.push(...block("docs", report.docs,
|
|
18792
|
-
|
|
18793
|
-
for (const spec of value.specs) rows.push(` spec ${spec.path}${spec.retired ? " (retired)" : ""}`);
|
|
18794
|
-
for (const plan of value.plans) rows.push(` plan ${plan.path}: ${String(plan.status.checked)} of ${String(plan.status.total)} checked, ${String(plan.status.outstanding)} outstanding${plan.status.declined > 0 ? `, ${String(plan.status.declined)} declined` : ""}`);
|
|
18795
|
-
if (value.specs.length === 0 && value.plans.length === 0) rows.push(" no spec or plan changed on this branch");
|
|
18796
|
-
return rows;
|
|
18797
|
-
}));
|
|
18798
|
-
lines.push(...block("labels", report.labels, (value) => {
|
|
18799
|
-
const rows = [];
|
|
18800
|
-
for (const [where, groups] of [["pull request", value.pr], ["issue", value.issue]]) {
|
|
18801
|
-
if (groups.length === 0) continue;
|
|
18802
|
-
rows.push(` ${where}:`);
|
|
18803
|
-
for (const group of groups) for (const label of group.labels) {
|
|
18804
|
-
const described = label.description === null ? "" : ` -- ${label.description}`;
|
|
18805
|
-
rows.push(` ${label.name}${described} [${group.scope}]`);
|
|
18806
|
-
}
|
|
18807
|
-
}
|
|
18808
|
-
if (value.descriptionsUnavailable !== void 0) rows.push(` descriptions unavailable -- ${value.descriptionsUnavailable}`);
|
|
18809
|
-
return rows;
|
|
18810
|
-
}));
|
|
18992
|
+
lines.push(...block("docs", report.docs, docsRows));
|
|
18993
|
+
lines.push(...block("labels", report.labels, labelRows));
|
|
18811
18994
|
lines.push(...block("feedback", report.feedback, (value) => {
|
|
18812
18995
|
const from = value.cutoffSource === "agent-comment" ? "last agent comment" : "head commit";
|
|
18813
18996
|
const rows = [` since ${value.cutoff} (the ${from}), ${String(value.agentComments)} agent comment(s) held back`];
|
|
@@ -18868,7 +19051,7 @@ const workStartCommand = defineCommand({
|
|
|
18868
19051
|
async run({ args }) {
|
|
18869
19052
|
const { ref, executor, git, forge } = await prepareWorkStatus(typeof args.ref === "string" ? args.ref.trim() : "", process.cwd());
|
|
18870
19053
|
const number = ref?.number;
|
|
18871
|
-
if (number === void 0) throw new FatalError("Usage: bst work:start needs an issue or pull request number or URL");
|
|
19054
|
+
if (number === void 0) throw new FatalError("Usage: pnpm exec bst work:start needs an issue or pull request number or URL");
|
|
18872
19055
|
const docs = resolveDocsConfig(git.topLevel);
|
|
18873
19056
|
const defaultBranch = resolveDefaultBranch({ cwd: git.topLevel });
|
|
18874
19057
|
const report = await buildWorkStatus({
|
|
@@ -18954,6 +19137,7 @@ function collectInputArgs(rawArgs) {
|
|
|
18954
19137
|
for (let i = 0; i < rawArgs.length; i += 1) {
|
|
18955
19138
|
const arg = rawArgs[i];
|
|
18956
19139
|
if (arg === void 0) continue;
|
|
19140
|
+
if (arg === "--") break;
|
|
18957
19141
|
if (arg === INPUT_FLAG) {
|
|
18958
19142
|
const next = rawArgs[i + 1];
|
|
18959
19143
|
if (next === void 0) throw new FatalError("Missing value for --input: expected `--input key=value`");
|
|
@@ -19633,8 +19817,8 @@ function formatPrUpdate(result, rewrites, flags) {
|
|
|
19633
19817
|
//#endregion
|
|
19634
19818
|
//#region src/release/pull-request.ts
|
|
19635
19819
|
const BST_COMMANDS = {
|
|
19636
|
-
create: "bst pr:create",
|
|
19637
|
-
update: "bst pr:update"
|
|
19820
|
+
create: "pnpm exec bst pr:create",
|
|
19821
|
+
update: "pnpm exec bst pr:update"
|
|
19638
19822
|
};
|
|
19639
19823
|
const BST_FLAGS = {
|
|
19640
19824
|
body: "--body",
|
|
@@ -20748,7 +20932,7 @@ function requireDockerCredentials(registryHost) {
|
|
|
20748
20932
|
};
|
|
20749
20933
|
const missing = [...username ? [] : ["DOCKER_USERNAME"], ...password ? [] : ["DOCKER_PASSWORD"]].join(" / ");
|
|
20750
20934
|
const { username: secretUsername, password: secretPassword } = dockerSecretNames(registryHost);
|
|
20751
|
-
throw new FatalError(`Missing required environment variable(s): ${missing}. On CI, these env vars are populated from registry-specific secrets -- for registry "${registryHost}", that's secrets.${secretUsername} / secrets.${secretPassword}. Set those on the repository (try \`bst setup:secrets\`), or export DOCKER_USERNAME / DOCKER_PASSWORD locally.`);
|
|
20935
|
+
throw new FatalError(`Missing required environment variable(s): ${missing}. On CI, these env vars are populated from registry-specific secrets -- for registry "${registryHost}", that's secrets.${secretUsername} / secrets.${secretPassword}. Set those on the repository (try \`pnpm exec bst setup:secrets\`), or export DOCKER_USERNAME / DOCKER_PASSWORD locally.`);
|
|
20752
20936
|
}
|
|
20753
20937
|
function resolveRegistryConfig(cwd) {
|
|
20754
20938
|
const envHost = process.env["DOCKER_REGISTRY_HOST"];
|
|
@@ -21098,7 +21282,7 @@ function composePs(executor, config) {
|
|
|
21098
21282
|
//#region src/docker-check/health.ts
|
|
21099
21283
|
const DOCKER_RUN_START_FAILURE_EXIT_CODE = 125;
|
|
21100
21284
|
const PROBE_TIMEOUT_SECONDS = "5";
|
|
21101
|
-
const PROBE_IMAGE = "curlimages/curl:8.
|
|
21285
|
+
const PROBE_IMAGE = "curlimages/curl:8.23.0@sha256:a134550be58ab783810e613ae96fd253c38f7bdd16d9bf9be350700c7a29ac06";
|
|
21102
21286
|
function getContainerHealth(containers, serviceName) {
|
|
21103
21287
|
return containers.find((c) => c.Service === serviceName)?.Health ?? "unknown";
|
|
21104
21288
|
}
|
|
@@ -21561,6 +21745,11 @@ function countByMode(entries) {
|
|
|
21561
21745
|
for (const entry of entries) if (entry.permissionMode !== void 0) counts[entry.permissionMode] = (counts[entry.permissionMode] ?? 0) + 1;
|
|
21562
21746
|
return counts;
|
|
21563
21747
|
}
|
|
21748
|
+
function parseTailCount(raw) {
|
|
21749
|
+
const value = Number(raw);
|
|
21750
|
+
if (!Number.isInteger(value) || value <= 0) throw new FatalError(`--tail must be a positive whole number of entries, got "${raw}"`);
|
|
21751
|
+
return value;
|
|
21752
|
+
}
|
|
21564
21753
|
function parseAuditLog(logPath) {
|
|
21565
21754
|
if (!existsSync(logPath)) return [];
|
|
21566
21755
|
const lines = readFileSync(logPath, "utf-8").split("\n").filter(Boolean);
|
|
@@ -21622,7 +21811,9 @@ const hooksAuditCommand = defineCommand({
|
|
|
21622
21811
|
async run({ args }) {
|
|
21623
21812
|
const out = createCliOutput(args.json === true);
|
|
21624
21813
|
const targetDir = args.dir ? path.resolve(args.dir) : findProjectRoot(process.cwd());
|
|
21625
|
-
|
|
21814
|
+
const logPath = path.join(targetDir, ".claude/hooks/bst-hook.audit.local.jsonl");
|
|
21815
|
+
const tailCount = args.tail === void 0 ? null : parseTailCount(args.tail);
|
|
21816
|
+
let entries = parseAuditLog(logPath);
|
|
21626
21817
|
if (entries.length === 0) {
|
|
21627
21818
|
out.json({ entries: [] });
|
|
21628
21819
|
out.text("No audit log entries found.");
|
|
@@ -21631,17 +21822,15 @@ const hooksAuditCommand = defineCommand({
|
|
|
21631
21822
|
const decisionFilter = args.decision === "passthrough" ? "none" : args.decision;
|
|
21632
21823
|
if (decisionFilter) entries = entries.filter((e) => e.decision === decisionFilter);
|
|
21633
21824
|
if (args.json === true) {
|
|
21634
|
-
const tail = args.tail ? Number.parseInt(args.tail, 10) : entries.length;
|
|
21635
21825
|
const byMode = countByMode(entries);
|
|
21636
21826
|
out.json({
|
|
21637
|
-
entries: entries.slice(-
|
|
21827
|
+
entries: tailCount === null ? entries : entries.slice(-tailCount),
|
|
21638
21828
|
counts: countDecisions(entries),
|
|
21639
21829
|
...Object.keys(byMode).length > 0 ? { byMode } : {}
|
|
21640
21830
|
});
|
|
21641
21831
|
return;
|
|
21642
21832
|
}
|
|
21643
|
-
if (
|
|
21644
|
-
const tailCount = Number.parseInt(args.tail, 10);
|
|
21833
|
+
if (tailCount !== null) {
|
|
21645
21834
|
for (const entry of entries.slice(-tailCount)) {
|
|
21646
21835
|
const ts = new Date(entry.timestamp).toLocaleString();
|
|
21647
21836
|
const rule = entry.rule ? ` [${entry.rule}]` : "";
|
|
@@ -21822,7 +22011,7 @@ const CHANGE_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
21822
22011
|
]);
|
|
21823
22012
|
async function runSetupAgent(targetDir, options) {
|
|
21824
22013
|
const saved = loadToolingConfigStrict(targetDir);
|
|
21825
|
-
if (saved === void 0) throw new FatalError(`No .tooling.json in ${targetDir}. Run bst repo:sync first to create one, so the repo's own conventions are chosen before the agent is enabled.`);
|
|
22014
|
+
if (saved === void 0) throw new FatalError(`No .tooling.json in ${targetDir}. Run pnpm exec bst repo:sync first to create one, so the repo's own conventions are chosen before the agent is enabled.`);
|
|
21826
22015
|
const previous = previousState(saved);
|
|
21827
22016
|
let consent = "already-enabled";
|
|
21828
22017
|
if (previous !== "true") {
|
|
@@ -21854,7 +22043,7 @@ async function runSetupAgent(targetDir, options) {
|
|
|
21854
22043
|
if (report.status === "stopped" || failed.length > 0) status = "failed";
|
|
21855
22044
|
else if (changed.length > 0) status = "changed";
|
|
21856
22045
|
else status = "up-to-date";
|
|
21857
|
-
if (status === "failed") log$1.error("The sync failed. Fix what it reported, then run bst setup:agent again.");
|
|
22046
|
+
if (status === "failed") log$1.error("The sync failed. Fix what it reported, then run pnpm exec bst setup:agent again.");
|
|
21858
22047
|
else log$1.info("Commit these changes and land them on the default branch; the agent reads consent there within about a minute.");
|
|
21859
22048
|
return {
|
|
21860
22049
|
dir: targetDir,
|
|
@@ -22699,7 +22888,7 @@ function serializeBaseline(baseline) {
|
|
|
22699
22888
|
//#region src/commands/coverage-check.ts
|
|
22700
22889
|
const COVERAGE_SUMMARY_FILE$1 = "coverage/coverage-summary.json";
|
|
22701
22890
|
const BASELINE_FILE$1 = "coverage-baseline.json";
|
|
22702
|
-
const MIGRATION_NOTICE = `${BASELINE_FILE$1} predates coverage.include scoping (no scope marker). Totals may have shifted because previously-untested files entered the denominator for the first time -- that is not necessarily a regression. This comparison is advisory only until the baseline is migrated: run \`pnpm test:coverage && bst coverage:check --update-baseline --reset\` once to adopt the honest, marked baseline. Normal gating resumes after that.`;
|
|
22891
|
+
const MIGRATION_NOTICE = `${BASELINE_FILE$1} predates coverage.include scoping (no scope marker). Totals may have shifted because previously-untested files entered the denominator for the first time -- that is not necessarily a regression. This comparison is advisory only until the baseline is migrated: run \`pnpm test:coverage && pnpm exec bst coverage:check --update-baseline --reset\` once to adopt the honest, marked baseline. Normal gating resumes after that.`;
|
|
22703
22892
|
function applyBaselineUpdate$1(out, baselinePath, baseline, current, options) {
|
|
22704
22893
|
const { reset, floorPp } = options;
|
|
22705
22894
|
if (reset) {
|
|
@@ -22736,7 +22925,7 @@ const coverageCheckCommand = defineCommand({
|
|
|
22736
22925
|
},
|
|
22737
22926
|
reset: {
|
|
22738
22927
|
type: "boolean",
|
|
22739
|
-
description: "With --update-baseline, rewrite coverage-baseline.json unconditionally, even when totals dropped. The one-shot migration to adopt an honest coverage.include-scoped baseline: pnpm test:coverage && bst coverage:check --update-baseline --reset."
|
|
22928
|
+
description: "With --update-baseline, rewrite coverage-baseline.json unconditionally, even when totals dropped. The one-shot migration to adopt an honest coverage.include-scoped baseline: pnpm test:coverage && pnpm exec bst coverage:check --update-baseline --reset."
|
|
22740
22929
|
},
|
|
22741
22930
|
"fail-on-missing-baseline": {
|
|
22742
22931
|
type: "boolean",
|
|
@@ -22921,13 +23110,14 @@ const coverageStatusCommand = defineCommand({
|
|
|
22921
23110
|
});
|
|
22922
23111
|
//#endregion
|
|
22923
23112
|
//#region src/codebase-health/baseline.ts
|
|
22924
|
-
function serializeHealthBaseline(metrics, ceilings) {
|
|
23113
|
+
function serializeHealthBaseline(metrics, ceilings, sizeBudgets) {
|
|
22925
23114
|
const ordered = {};
|
|
22926
23115
|
for (const key of METRIC_KEYS) {
|
|
22927
23116
|
const value = metrics[key];
|
|
22928
23117
|
if (value !== void 0) ordered[key] = value;
|
|
22929
23118
|
}
|
|
22930
23119
|
if (ceilings !== void 0 && Object.keys(ceilings).length > 0) ordered["moduleCeilings"] = ceilings;
|
|
23120
|
+
if (sizeBudgets !== void 0 && Object.keys(sizeBudgets).length > 0) ordered["sizeBudgets"] = sizeBudgets;
|
|
22931
23121
|
return JSON.stringify(ordered, null, 2) + "\n";
|
|
22932
23122
|
}
|
|
22933
23123
|
//#endregion
|
|
@@ -22957,6 +23147,20 @@ function createExcludeMatcher(patterns) {
|
|
|
22957
23147
|
};
|
|
22958
23148
|
}
|
|
22959
23149
|
//#endregion
|
|
23150
|
+
//#region src/codebase-health/exempt-match.ts
|
|
23151
|
+
function exemptKeyMatcher(key) {
|
|
23152
|
+
if (!picomatch.scan(key).isGlob) return (path) => path === key;
|
|
23153
|
+
const isMatch = picomatch(key, { dot: true });
|
|
23154
|
+
return (path) => isMatch(path);
|
|
23155
|
+
}
|
|
23156
|
+
function createExemptLookup(reasons) {
|
|
23157
|
+
const globs = Object.entries(reasons).filter(([key]) => picomatch.scan(key).isGlob).map(([key, reason]) => ({
|
|
23158
|
+
matches: exemptKeyMatcher(key),
|
|
23159
|
+
reason
|
|
23160
|
+
}));
|
|
23161
|
+
return (path) => reasons[path] ?? globs.find((glob) => glob.matches(path))?.reason;
|
|
23162
|
+
}
|
|
23163
|
+
//#endregion
|
|
22960
23164
|
//#region src/codebase-health/depcruise.ts
|
|
22961
23165
|
const DependencySchema = z.object({
|
|
22962
23166
|
module: z.string(),
|
|
@@ -22980,10 +23184,11 @@ function partitionExempt(list, reasons) {
|
|
|
22980
23184
|
kept: list,
|
|
22981
23185
|
exempt: []
|
|
22982
23186
|
};
|
|
23187
|
+
const reasonFor = createExemptLookup(reasons);
|
|
22983
23188
|
const kept = [];
|
|
22984
23189
|
const exempt = [];
|
|
22985
23190
|
for (const entry of list) {
|
|
22986
|
-
const reason =
|
|
23191
|
+
const reason = reasonFor(entry.module);
|
|
22987
23192
|
if (reason === void 0) kept.push(entry);
|
|
22988
23193
|
else exempt.push({
|
|
22989
23194
|
...entry,
|
|
@@ -23563,7 +23768,40 @@ function offenderEntries(details, key) {
|
|
|
23563
23768
|
//#endregion
|
|
23564
23769
|
//#region src/codebase-health/compare.ts
|
|
23565
23770
|
const DEFAULT_TOLERANCE = 0;
|
|
23566
|
-
function
|
|
23771
|
+
function compareSizeBudgets(baseline, measurements) {
|
|
23772
|
+
const results = [];
|
|
23773
|
+
for (const measurement of measurements) {
|
|
23774
|
+
const ceiling = baseline.sizeBudgets?.[measurement.id];
|
|
23775
|
+
if (measurement.measured === void 0) {
|
|
23776
|
+
results.push({
|
|
23777
|
+
id: measurement.id,
|
|
23778
|
+
ceiling,
|
|
23779
|
+
pass: false,
|
|
23780
|
+
reason: measurement.unmeasured ?? "not measured"
|
|
23781
|
+
});
|
|
23782
|
+
continue;
|
|
23783
|
+
}
|
|
23784
|
+
if (ceiling === void 0) {
|
|
23785
|
+
results.push({
|
|
23786
|
+
id: measurement.id,
|
|
23787
|
+
measured: measurement.measured,
|
|
23788
|
+
pass: false,
|
|
23789
|
+
reason: "unrecorded",
|
|
23790
|
+
measurerChanged: measurement.measurerChanged
|
|
23791
|
+
});
|
|
23792
|
+
continue;
|
|
23793
|
+
}
|
|
23794
|
+
results.push({
|
|
23795
|
+
id: measurement.id,
|
|
23796
|
+
ceiling,
|
|
23797
|
+
measured: measurement.measured,
|
|
23798
|
+
pass: measurement.measured <= ceiling,
|
|
23799
|
+
measurerChanged: measurement.measurerChanged
|
|
23800
|
+
});
|
|
23801
|
+
}
|
|
23802
|
+
return results;
|
|
23803
|
+
}
|
|
23804
|
+
function compareMetrics(baseline, current, tolerance = DEFAULT_TOLERANCE, currentCeilings = {}, sizes = []) {
|
|
23567
23805
|
const metrics = METRIC_KEYS.map((key) => {
|
|
23568
23806
|
const b = baseline[key];
|
|
23569
23807
|
const c = current[key];
|
|
@@ -23598,23 +23836,65 @@ function compareMetrics(baseline, current, tolerance = DEFAULT_TOLERANCE, curren
|
|
|
23598
23836
|
});
|
|
23599
23837
|
}
|
|
23600
23838
|
}
|
|
23601
|
-
const
|
|
23839
|
+
const uncoveredOutliers = [];
|
|
23602
23840
|
if (baseline.moduleCeilings !== void 0) {
|
|
23603
|
-
for (const key of RATCHETED_METRIC_KEYS) for (const [module, count] of Object.entries(currentCeilings[key] ?? {})) if (baseline.moduleCeilings[key]?.[module] === void 0)
|
|
23841
|
+
for (const key of RATCHETED_METRIC_KEYS) for (const [module, count] of Object.entries(currentCeilings[key] ?? {})) if (baseline.moduleCeilings[key]?.[module] === void 0) uncoveredOutliers.push({
|
|
23604
23842
|
key,
|
|
23605
23843
|
module,
|
|
23606
23844
|
current: count
|
|
23607
23845
|
});
|
|
23608
23846
|
}
|
|
23847
|
+
const sizeResults = compareSizeBudgets(baseline, sizes);
|
|
23609
23848
|
return {
|
|
23610
|
-
pass: metrics.every((m) => m.pass) && ceilings.every((c) => c.pass),
|
|
23849
|
+
pass: metrics.every((m) => m.pass) && ceilings.every((c) => c.pass) && sizeResults.every((s) => s.pass),
|
|
23611
23850
|
tolerance,
|
|
23612
23851
|
metrics,
|
|
23613
23852
|
ceilings,
|
|
23614
|
-
|
|
23853
|
+
uncoveredOutliers,
|
|
23854
|
+
sizeResults
|
|
23855
|
+
};
|
|
23856
|
+
}
|
|
23857
|
+
function needsBaseMeasurement(result) {
|
|
23858
|
+
const failedKeys = new Set(result.metrics.filter((m) => m.pass === false).map((m) => m.key));
|
|
23859
|
+
return RATCHETED_METRIC_KEYS.some((key) => failedKeys.has(key) && result.uncoveredOutliers.some((o) => o.key === key));
|
|
23860
|
+
}
|
|
23861
|
+
function attributeOutliers(uncovered, base) {
|
|
23862
|
+
if (base.status !== "measured") return {
|
|
23863
|
+
outlierBase: base.status === "unavailable" ? {
|
|
23864
|
+
status: "unavailable",
|
|
23865
|
+
mergeBase: base.mergeBase,
|
|
23866
|
+
reason: base.reason
|
|
23867
|
+
} : { status: "not-needed" },
|
|
23868
|
+
newOutliers: [],
|
|
23869
|
+
baseOutliers: [],
|
|
23870
|
+
unattributedOutliers: [...uncovered]
|
|
23871
|
+
};
|
|
23872
|
+
const newOutliers = [];
|
|
23873
|
+
const baseOutliers = [];
|
|
23874
|
+
for (const outlier of uncovered) {
|
|
23875
|
+
const baseCount = base.ceilings[outlier.key]?.[outlier.module];
|
|
23876
|
+
if (baseCount === void 0) {
|
|
23877
|
+
newOutliers.push(outlier);
|
|
23878
|
+
continue;
|
|
23879
|
+
}
|
|
23880
|
+
baseOutliers.push({
|
|
23881
|
+
key: outlier.key,
|
|
23882
|
+
module: outlier.module,
|
|
23883
|
+
base: baseCount,
|
|
23884
|
+
current: outlier.current
|
|
23885
|
+
});
|
|
23886
|
+
}
|
|
23887
|
+
return {
|
|
23888
|
+
outlierBase: {
|
|
23889
|
+
status: "measured",
|
|
23890
|
+
mergeBase: base.mergeBase
|
|
23891
|
+
},
|
|
23892
|
+
newOutliers,
|
|
23893
|
+
baseOutliers,
|
|
23894
|
+
unattributedOutliers: []
|
|
23615
23895
|
};
|
|
23616
23896
|
}
|
|
23617
|
-
function isFlatOrBetter(baseline, current, currentCeilings = {}) {
|
|
23897
|
+
function isFlatOrBetter(baseline, current, currentCeilings = {}, sizes = []) {
|
|
23618
23898
|
const metricsFlat = METRIC_KEYS.every((key) => {
|
|
23619
23899
|
const b = baseline[key];
|
|
23620
23900
|
const c = current[key];
|
|
@@ -23630,7 +23910,133 @@ function isFlatOrBetter(baseline, current, currentCeilings = {}) {
|
|
|
23630
23910
|
return curr <= base;
|
|
23631
23911
|
});
|
|
23632
23912
|
});
|
|
23633
|
-
|
|
23913
|
+
const sizesFlat = sizes.every((s) => {
|
|
23914
|
+
const recorded = baseline.sizeBudgets?.[s.id];
|
|
23915
|
+
if (recorded === void 0) return true;
|
|
23916
|
+
if (s.measured === void 0) return false;
|
|
23917
|
+
return s.measured <= recorded;
|
|
23918
|
+
});
|
|
23919
|
+
return metricsFlat && ceilingsFlat && sizesFlat;
|
|
23920
|
+
}
|
|
23921
|
+
//#endregion
|
|
23922
|
+
//#region src/codebase-health/base-checkout.ts
|
|
23923
|
+
const TEMP_PREFIX = "bst-health-base-";
|
|
23924
|
+
function withMergeBaseCheckout(executor, targetDir, mergeBase, use) {
|
|
23925
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), TEMP_PREFIX));
|
|
23926
|
+
const tree = path.join(tmp, "tree");
|
|
23927
|
+
const index = path.join(tmp, "index");
|
|
23928
|
+
try {
|
|
23929
|
+
fs.mkdirSync(tree, { recursive: true });
|
|
23930
|
+
const gitDir = gitOutput(executor.exec("git", ["rev-parse", "--absolute-git-dir"], { cwd: targetDir }), "git rev-parse --absolute-git-dir").trim();
|
|
23931
|
+
const workRoot = gitOutput(executor.exec("git", ["rev-parse", "--show-toplevel"], { cwd: targetDir }), "git rev-parse --show-toplevel").trim();
|
|
23932
|
+
const intoCheckout = (args) => executor.exec("git", [
|
|
23933
|
+
`--git-dir=${gitDir}`,
|
|
23934
|
+
`--work-tree=${tree}`,
|
|
23935
|
+
...args
|
|
23936
|
+
], {
|
|
23937
|
+
cwd: tree,
|
|
23938
|
+
env: { GIT_INDEX_FILE: index }
|
|
23939
|
+
});
|
|
23940
|
+
gitOutput(intoCheckout(["read-tree", mergeBase]), `git read-tree ${mergeBase.slice(0, 12)}`);
|
|
23941
|
+
gitOutput(intoCheckout(["checkout-index", "-a"]), "git checkout-index -a");
|
|
23942
|
+
mirrorNodeModules(workRoot, tree);
|
|
23943
|
+
return use(tree);
|
|
23944
|
+
} finally {
|
|
23945
|
+
fs.rmSync(tmp, {
|
|
23946
|
+
recursive: true,
|
|
23947
|
+
force: true
|
|
23948
|
+
});
|
|
23949
|
+
}
|
|
23950
|
+
}
|
|
23951
|
+
function gitOutput(result, command) {
|
|
23952
|
+
if (result.exitCode === 0) return result.stdout;
|
|
23953
|
+
const detail = result.stderr.trim().split("\n")[0] ?? "";
|
|
23954
|
+
throw new TransientError(`${command} failed: ${detail === "" ? `exit ${String(result.exitCode)}` : detail}`);
|
|
23955
|
+
}
|
|
23956
|
+
function mirrorNodeModules(workingRoot, checkoutRoot) {
|
|
23957
|
+
mirrorTree(workingRoot, checkoutRoot, "");
|
|
23958
|
+
}
|
|
23959
|
+
function mirrorTree(workingRoot, checkoutRoot, rel) {
|
|
23960
|
+
mirrorEntries(path.join(workingRoot, rel, "node_modules"), path.join(checkoutRoot, rel, "node_modules"));
|
|
23961
|
+
let entries;
|
|
23962
|
+
try {
|
|
23963
|
+
entries = fs.readdirSync(path.join(checkoutRoot, rel), { withFileTypes: true });
|
|
23964
|
+
} catch (_error) {
|
|
23965
|
+
return;
|
|
23966
|
+
}
|
|
23967
|
+
for (const entry of entries) {
|
|
23968
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
23969
|
+
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
23970
|
+
mirrorTree(workingRoot, checkoutRoot, path.join(rel, entry.name));
|
|
23971
|
+
}
|
|
23972
|
+
}
|
|
23973
|
+
function mirrorEntries(source, dest) {
|
|
23974
|
+
let entries;
|
|
23975
|
+
try {
|
|
23976
|
+
entries = fs.readdirSync(source, { withFileTypes: true });
|
|
23977
|
+
} catch (_error) {
|
|
23978
|
+
return;
|
|
23979
|
+
}
|
|
23980
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
23981
|
+
for (const entry of entries) {
|
|
23982
|
+
const src = path.join(source, entry.name);
|
|
23983
|
+
const target = path.join(dest, entry.name);
|
|
23984
|
+
if (entry.isSymbolicLink()) {
|
|
23985
|
+
const link = safeReadlink(src);
|
|
23986
|
+
if (link !== void 0 && !path.isAbsolute(link)) {
|
|
23987
|
+
linkRelative(link, target);
|
|
23988
|
+
continue;
|
|
23989
|
+
}
|
|
23990
|
+
} else if (entry.isDirectory() && entry.name.startsWith("@")) {
|
|
23991
|
+
mirrorEntries(src, target);
|
|
23992
|
+
continue;
|
|
23993
|
+
}
|
|
23994
|
+
try {
|
|
23995
|
+
fs.symlinkSync(src, target, entry.isDirectory() ? "dir" : "file");
|
|
23996
|
+
} catch (_error) {}
|
|
23997
|
+
}
|
|
23998
|
+
}
|
|
23999
|
+
function safeReadlink(file) {
|
|
24000
|
+
try {
|
|
24001
|
+
return fs.readlinkSync(file);
|
|
24002
|
+
} catch (_error) {
|
|
24003
|
+
return;
|
|
24004
|
+
}
|
|
24005
|
+
}
|
|
24006
|
+
function linkRelative(link, target) {
|
|
24007
|
+
try {
|
|
24008
|
+
fs.symlinkSync(link, target);
|
|
24009
|
+
} catch (_error) {}
|
|
24010
|
+
}
|
|
24011
|
+
//#endregion
|
|
24012
|
+
//#region src/codebase-health/base-outliers.ts
|
|
24013
|
+
function unavailable(reason) {
|
|
24014
|
+
return {
|
|
24015
|
+
status: "unavailable",
|
|
24016
|
+
reason: firstLine(reason)
|
|
24017
|
+
};
|
|
24018
|
+
}
|
|
24019
|
+
function firstLine(text) {
|
|
24020
|
+
return text.trim().split("\n")[0] ?? "";
|
|
24021
|
+
}
|
|
24022
|
+
function measureBaseOutliers(opts) {
|
|
24023
|
+
const collect = opts.collect ?? collectDetails;
|
|
24024
|
+
try {
|
|
24025
|
+
return withMergeBaseCheckout(opts.executor, opts.targetDir, opts.mergeBase, (checkoutRoot) => {
|
|
24026
|
+
const baseTarget = path.join(checkoutRoot, opts.prefix);
|
|
24027
|
+
const collectConfig = buildCollectConfig(opts.config, { tsConfig: existsSync(path.join(baseTarget, "tsconfig.json")) ? "tsconfig.json" : void 0 });
|
|
24028
|
+
const details = collect(createRealHealthExecutor(baseTarget), collectConfig);
|
|
24029
|
+
const unresolved = details.unresolvedFirstParty;
|
|
24030
|
+
if (unresolved !== void 0 && unresolved.length > 0) return unavailable(`the base cruise could not resolve ${String(unresolved.length)} first-party imports`);
|
|
24031
|
+
if (opts.config.graphMetricsEnabled && details.circularDependencies === void 0) return unavailable("the base cruise produced no graph metrics");
|
|
24032
|
+
return {
|
|
24033
|
+
status: "measured",
|
|
24034
|
+
ceilings: ceilingsFromDetails(details)
|
|
24035
|
+
};
|
|
24036
|
+
});
|
|
24037
|
+
} catch (error) {
|
|
24038
|
+
return unavailable(describeError$1(error));
|
|
24039
|
+
}
|
|
23634
24040
|
}
|
|
23635
24041
|
//#endregion
|
|
23636
24042
|
//#region src/codebase-health/render.ts
|
|
@@ -23718,7 +24124,7 @@ function formatExplain(details) {
|
|
|
23718
24124
|
}
|
|
23719
24125
|
return lines.join("\n");
|
|
23720
24126
|
}
|
|
23721
|
-
function formatCompareDiff(result, details) {
|
|
24127
|
+
function formatCompareDiff(result, details, attribution) {
|
|
23722
24128
|
const lines = [];
|
|
23723
24129
|
for (const m of result.metrics) {
|
|
23724
24130
|
if (m.baseline === void 0 && m.current === void 0) continue;
|
|
@@ -23736,10 +24142,34 @@ function formatCompareDiff(result, details) {
|
|
|
23736
24142
|
if (c.pass) continue;
|
|
23737
24143
|
lines.push(` ${CEILING_LABELS[c.key]}: ${c.module} ${c.baseline} -> ${c.current} (ceiling)`);
|
|
23738
24144
|
}
|
|
24145
|
+
for (const s of result.sizeResults) if (!s.pass) {
|
|
24146
|
+
let line;
|
|
24147
|
+
if (s.measured === void 0) line = ` size: ${s.id} unmeasured (${s.reason ?? "not measured"})`;
|
|
24148
|
+
else if (s.ceiling === void 0) line = ` size: ${s.id} -- -> ${s.measured} (unrecorded)`;
|
|
24149
|
+
else line = ` size: ${s.id} ${s.ceiling} -> ${s.measured} (ceiling)`;
|
|
24150
|
+
lines.push(line);
|
|
24151
|
+
if (s.measurerChanged === true) lines.push(` size: ${s.id} measured by code this branch changed`);
|
|
24152
|
+
}
|
|
23739
24153
|
const failedKeys = new Set(result.metrics.filter((m) => m.pass === false).map((m) => m.key));
|
|
23740
|
-
|
|
24154
|
+
let printedAttribution = false;
|
|
24155
|
+
for (const o of attribution.newOutliers) {
|
|
23741
24156
|
if (!failedKeys.has(o.key)) continue;
|
|
23742
24157
|
lines.push(` ${CEILING_LABELS[o.key]}: ${o.module} -- -> ${o.current} (new)`);
|
|
24158
|
+
printedAttribution = true;
|
|
24159
|
+
}
|
|
24160
|
+
for (const o of attribution.baseOutliers) {
|
|
24161
|
+
if (!failedKeys.has(o.key)) continue;
|
|
24162
|
+
lines.push(` ${CEILING_LABELS[o.key]}: ${o.module} ${o.base} -> ${o.current} (on base)`);
|
|
24163
|
+
printedAttribution = true;
|
|
24164
|
+
}
|
|
24165
|
+
for (const o of attribution.unattributedOutliers) {
|
|
24166
|
+
if (!failedKeys.has(o.key)) continue;
|
|
24167
|
+
lines.push(` ${CEILING_LABELS[o.key]}: ${o.module} -- -> ${o.current} (no ceiling)`);
|
|
24168
|
+
printedAttribution = true;
|
|
24169
|
+
}
|
|
24170
|
+
if (printedAttribution) {
|
|
24171
|
+
if (attribution.outlierBase.status === "measured") lines.push(` outliers attributed against the merge base ${attribution.outlierBase.mergeBase}`);
|
|
24172
|
+
else if (attribution.outlierBase.status === "unavailable") lines.push(` outliers not attributed: the merge base ${attribution.outlierBase.mergeBase} could not be measured (${attribution.outlierBase.reason})`);
|
|
23743
24173
|
}
|
|
23744
24174
|
lines.push(result.pass ? "health: PASS" : "health: FAIL");
|
|
23745
24175
|
return lines.join("\n");
|
|
@@ -23760,7 +24190,7 @@ function formatGraphUnavailableForSummary(details, graphMetricsEnabled, diagnost
|
|
|
23760
24190
|
}
|
|
23761
24191
|
function formatStaleEntries(stale) {
|
|
23762
24192
|
return stale.map((entry) => {
|
|
23763
|
-
if (entry.kind === "exemption") return `health.exempt: "${entry.path}"
|
|
24193
|
+
if (entry.kind === "exemption") return `health.exempt: "${entry.path}" names no module or directory this run measured, so it suppresses nothing. Delete the entry, or repoint it at wherever ${entry.path} moved to.`;
|
|
23764
24194
|
return `health-baseline: the recorded ${CEILING_LABELS[entry.metric] ?? entry.metric} ceiling for "${entry.path}" (ceiling ${String(entry.count ?? 0)}) names a path this run did not measure, so that ratchet is switched off. If the path moved, re-baseline it; if it is gone, --update-baseline drops it.`;
|
|
23765
24195
|
});
|
|
23766
24196
|
}
|
|
@@ -23788,7 +24218,7 @@ function formatSourceDiagnosis(sources, graphMetricsEnabled) {
|
|
|
23788
24218
|
`Resolved roots: ${resolvedRoots}.`,
|
|
23789
24219
|
graphClause,
|
|
23790
24220
|
filtersClause,
|
|
23791
|
-
`Run "bst health:summary --explain" for the full breakdown.`
|
|
24221
|
+
`Run "pnpm exec bst health:summary --explain" for the full breakdown.`
|
|
23792
24222
|
].filter((part) => part.length > 0).join(" ")
|
|
23793
24223
|
};
|
|
23794
24224
|
}
|
|
@@ -23797,69 +24227,90 @@ function formatUnresolvedFirstParty(entries) {
|
|
|
23797
24227
|
lines.push(` An unresolved first-party import leaves the dependency graph, improving every health`, ` metric. Fix the import, or the resolve options that were supposed to follow it.`);
|
|
23798
24228
|
return lines.join("\n");
|
|
23799
24229
|
}
|
|
23800
|
-
const
|
|
23801
|
-
function
|
|
23802
|
-
|
|
23803
|
-
const shown = otherFiles.slice(0, REFUSAL_FILE_CAP);
|
|
23804
|
-
if (shown.length > 0) {
|
|
23805
|
-
const more = otherFiles.length - shown.length;
|
|
23806
|
-
const suffix = more > 0 ? ` (+${String(more)} more)` : "";
|
|
23807
|
-
lines.push(` this branch also changes: ${shown.join(", ")}${suffix}`);
|
|
23808
|
-
}
|
|
23809
|
-
lines.push(` Split the module so the graph does not grow, or open a ceiling-only pull request --`, ` only health-baseline.json and .tooling.json changed -- for a human to merge.`);
|
|
23810
|
-
return lines.join("\n");
|
|
24230
|
+
const CARRIED_RAISES_FIRST_LINE = "raises carried by this branch:";
|
|
24231
|
+
function formatCarriedRaises(raises) {
|
|
24232
|
+
return [CARRIED_RAISES_FIRST_LINE, ...raises.map((r) => ` ${r.leaf}: ${r.reference} -> ${r.current}`)].join("\n");
|
|
23811
24233
|
}
|
|
23812
24234
|
//#endregion
|
|
23813
|
-
//#region src/codebase-health/
|
|
23814
|
-
function
|
|
23815
|
-
|
|
23816
|
-
|
|
24235
|
+
//#region src/codebase-health/size.ts
|
|
24236
|
+
function byteLength(absolute) {
|
|
24237
|
+
try {
|
|
24238
|
+
const stats = statSync(absolute);
|
|
24239
|
+
return stats.isFile() ? stats.size : void 0;
|
|
24240
|
+
} catch (_error) {
|
|
24241
|
+
return;
|
|
24242
|
+
}
|
|
23817
24243
|
}
|
|
23818
|
-
function
|
|
23819
|
-
const
|
|
23820
|
-
|
|
23821
|
-
|
|
23822
|
-
|
|
23823
|
-
|
|
23824
|
-
|
|
23825
|
-
|
|
23826
|
-
|
|
23827
|
-
|
|
23828
|
-
|
|
23829
|
-
|
|
23830
|
-
|
|
23831
|
-
|
|
24244
|
+
function measurerChanged(budget, changedNames) {
|
|
24245
|
+
const patterns = [...budget.files, "package.json"];
|
|
24246
|
+
const matches = picomatch(patterns, { dot: true });
|
|
24247
|
+
return changedNames.some((name) => matches(name));
|
|
24248
|
+
}
|
|
24249
|
+
function measureSizeBudgets(targetDir, budgets, options) {
|
|
24250
|
+
const measurements = [];
|
|
24251
|
+
for (const [id, budget] of Object.entries(budgets).toSorted(([a], [b]) => a.localeCompare(b))) measurements.push(measureOne(targetDir, id, budget, options));
|
|
24252
|
+
return measurements;
|
|
24253
|
+
}
|
|
24254
|
+
function measureOne(targetDir, id, budget, options) {
|
|
24255
|
+
if ("file" in budget) {
|
|
24256
|
+
const bytes = byteLength(path.join(targetDir, budget.file));
|
|
24257
|
+
if (bytes === void 0) return {
|
|
24258
|
+
id,
|
|
24259
|
+
unmeasured: `no file at ${budget.file}`
|
|
24260
|
+
};
|
|
24261
|
+
return {
|
|
24262
|
+
id,
|
|
24263
|
+
measured: bytes
|
|
24264
|
+
};
|
|
23832
24265
|
}
|
|
23833
|
-
|
|
23834
|
-
|
|
23835
|
-
|
|
23836
|
-
|
|
23837
|
-
|
|
23838
|
-
|
|
23839
|
-
|
|
24266
|
+
if ("glob" in budget) {
|
|
24267
|
+
const sizes = [];
|
|
24268
|
+
for (const match of globSync(budget.glob, { cwd: targetDir })) {
|
|
24269
|
+
const bytes = byteLength(path.join(targetDir, match));
|
|
24270
|
+
if (bytes !== void 0) sizes.push(bytes);
|
|
24271
|
+
}
|
|
24272
|
+
if (sizes.length === 0) return {
|
|
24273
|
+
id,
|
|
24274
|
+
unmeasured: `glob ${budget.glob} matched no files`
|
|
24275
|
+
};
|
|
24276
|
+
let total = 0;
|
|
24277
|
+
let largest = 0;
|
|
24278
|
+
for (const bytes of sizes) {
|
|
24279
|
+
total += bytes;
|
|
24280
|
+
if (bytes > largest) largest = bytes;
|
|
24281
|
+
}
|
|
24282
|
+
return {
|
|
24283
|
+
id,
|
|
24284
|
+
measured: budget.aggregate === "largest" ? largest : total
|
|
24285
|
+
};
|
|
23840
24286
|
}
|
|
23841
|
-
|
|
24287
|
+
const result = options.executor.exec("pnpm", [
|
|
24288
|
+
"run",
|
|
24289
|
+
"--silent",
|
|
24290
|
+
budget.script
|
|
24291
|
+
], { cwd: targetDir });
|
|
24292
|
+
if (result.exitCode !== 0) return {
|
|
24293
|
+
id,
|
|
24294
|
+
unmeasured: `script ${budget.script} exited ${String(result.exitCode)}`
|
|
24295
|
+
};
|
|
24296
|
+
const reported = readMeasureMarker(result.stdout);
|
|
24297
|
+
if (reported === void 0) return {
|
|
24298
|
+
id,
|
|
24299
|
+
unmeasured: `script ${budget.script} printed no measure marker`
|
|
24300
|
+
};
|
|
24301
|
+
if (!measurerChanged(budget, options.changedNames)) return {
|
|
24302
|
+
id,
|
|
24303
|
+
measured: reported
|
|
24304
|
+
};
|
|
24305
|
+
return {
|
|
24306
|
+
id,
|
|
24307
|
+
measured: reported,
|
|
24308
|
+
measurerChanged: true
|
|
24309
|
+
};
|
|
23842
24310
|
}
|
|
23843
24311
|
//#endregion
|
|
23844
|
-
//#region src/
|
|
24312
|
+
//#region src/codebase-health/measure.ts
|
|
23845
24313
|
const BASELINE_FILE = "health-baseline.json";
|
|
23846
|
-
function assessRaises(executor, targetDir, current, out) {
|
|
23847
|
-
const read = readTargetReference(executor, targetDir, out);
|
|
23848
|
-
if (read.kind === "unavailable") throw new FatalError(read.reason);
|
|
23849
|
-
const reference = read.reference;
|
|
23850
|
-
if (reference.baseline === void 0) {
|
|
23851
|
-
out.notice(`${BASELINE_FILE} has no counterpart on the default branch yet, so there is nothing to raise against.`);
|
|
23852
|
-
return {
|
|
23853
|
-
reference: {
|
|
23854
|
-
status: "no-baseline",
|
|
23855
|
-
mergeBase: reference.mergeBase
|
|
23856
|
-
},
|
|
23857
|
-
raises: [],
|
|
23858
|
-
raiseRefused: false
|
|
23859
|
-
};
|
|
23860
|
-
}
|
|
23861
|
-
return refuseBesideCode(executor, targetDir, reference, findRaises(reference, current), out);
|
|
23862
|
-
}
|
|
23863
24314
|
function readTargetReference(executor, targetDir, out) {
|
|
23864
24315
|
const context = detectHealthContext(targetDir);
|
|
23865
24316
|
const read = readReference(executor, {
|
|
@@ -23871,32 +24322,171 @@ function readTargetReference(executor, targetDir, out) {
|
|
|
23871
24322
|
if (read.kind === "read" && read.warning !== void 0) out.notice(read.warning);
|
|
23872
24323
|
return read;
|
|
23873
24324
|
}
|
|
23874
|
-
function
|
|
23875
|
-
if (raises.length === 0) return
|
|
23876
|
-
|
|
23877
|
-
|
|
23878
|
-
|
|
23879
|
-
|
|
23880
|
-
|
|
23881
|
-
|
|
23882
|
-
|
|
23883
|
-
|
|
23884
|
-
|
|
23885
|
-
|
|
23886
|
-
|
|
23887
|
-
|
|
23888
|
-
|
|
23889
|
-
|
|
24325
|
+
function reportCarriedRaises(raises, out) {
|
|
24326
|
+
if (raises.length === 0) return;
|
|
24327
|
+
out.text(formatCarriedRaises(raises));
|
|
24328
|
+
out.text(formatWarnMarker(raises.length));
|
|
24329
|
+
}
|
|
24330
|
+
function assessRaises(executor, targetDir, current, out) {
|
|
24331
|
+
const read = readTargetReference(executor, targetDir, out);
|
|
24332
|
+
if (read.kind === "unavailable") throw new FatalError(read.reason);
|
|
24333
|
+
const reference = read.reference;
|
|
24334
|
+
if (reference.baseline === void 0) {
|
|
24335
|
+
out.notice(`${BASELINE_FILE} has no counterpart on the default branch yet, so there is nothing to raise against.`);
|
|
24336
|
+
return {
|
|
24337
|
+
fields: {
|
|
24338
|
+
reference: {
|
|
24339
|
+
status: "no-baseline",
|
|
24340
|
+
mergeBase: reference.mergeBase
|
|
24341
|
+
},
|
|
24342
|
+
raises: []
|
|
24343
|
+
},
|
|
24344
|
+
prefix: reference.prefix
|
|
24345
|
+
};
|
|
23890
24346
|
}
|
|
23891
24347
|
return {
|
|
23892
|
-
|
|
23893
|
-
|
|
23894
|
-
|
|
24348
|
+
fields: {
|
|
24349
|
+
reference: {
|
|
24350
|
+
status: "read",
|
|
24351
|
+
mergeBase: reference.mergeBase
|
|
24352
|
+
},
|
|
24353
|
+
raises: findRaises(reference, current)
|
|
23895
24354
|
},
|
|
23896
|
-
|
|
23897
|
-
|
|
24355
|
+
prefix: reference.prefix
|
|
24356
|
+
};
|
|
24357
|
+
}
|
|
24358
|
+
function attributeRun(run) {
|
|
24359
|
+
const { result, executor, targetDir, fields, prefix, config } = run;
|
|
24360
|
+
if (!needsBaseMeasurement(result)) return attributeOutliers(result.uncoveredOutliers, { status: "not-needed" });
|
|
24361
|
+
const mergeBase = fields.reference.mergeBase;
|
|
24362
|
+
const measurement = measureBaseOutliers({
|
|
24363
|
+
executor,
|
|
24364
|
+
targetDir,
|
|
24365
|
+
mergeBase,
|
|
24366
|
+
prefix,
|
|
24367
|
+
config
|
|
24368
|
+
});
|
|
24369
|
+
const base = measurement.status === "measured" ? {
|
|
24370
|
+
status: "measured",
|
|
24371
|
+
mergeBase,
|
|
24372
|
+
ceilings: measurement.ceilings
|
|
24373
|
+
} : {
|
|
24374
|
+
status: "unavailable",
|
|
24375
|
+
mergeBase,
|
|
24376
|
+
reason: measurement.reason
|
|
23898
24377
|
};
|
|
24378
|
+
return attributeOutliers(result.uncoveredOutliers, base);
|
|
24379
|
+
}
|
|
24380
|
+
function changedPathsForMeasurers(executor, targetDir, sizeBudgets, mergeBase, prefix) {
|
|
24381
|
+
if (!Object.values(sizeBudgets).some((budget) => "script" in budget)) return [];
|
|
24382
|
+
return branchDiffNames(executor, targetDir, mergeBase).filter((name) => name.startsWith(prefix)).map((name) => name.slice(prefix.length)).filter((name) => name !== "");
|
|
24383
|
+
}
|
|
24384
|
+
function refuseUnresolvedImports(details) {
|
|
24385
|
+
const entries = details.unresolvedFirstParty;
|
|
24386
|
+
if (entries !== void 0 && entries.length > 0) throw new FatalError(formatUnresolvedFirstParty(entries));
|
|
23899
24387
|
}
|
|
24388
|
+
function refuseEmptyScope(details, config, failOnEmptySrc, out) {
|
|
24389
|
+
const { warnings, emptyScope } = formatSourceDiagnosis(details.sources, config.graphMetricsEnabled);
|
|
24390
|
+
for (const warning of warnings) out.notice(warning);
|
|
24391
|
+
if (emptyScope !== void 0 && failOnEmptySrc !== false) throw new FatalError(emptyScope);
|
|
24392
|
+
if (emptyScope !== void 0) out.notice(emptyScope);
|
|
24393
|
+
}
|
|
24394
|
+
function measureHealth(targetDir, config, out, opts = {}) {
|
|
24395
|
+
const executor = createRealHealthExecutor(targetDir);
|
|
24396
|
+
const details = collectDetails(executor, buildCollectConfig(config, { tsConfig: existsSync(path.join(targetDir, "tsconfig.json")) ? "tsconfig.json" : void 0 }));
|
|
24397
|
+
const current = metricsFromDetails(details);
|
|
24398
|
+
const ceilings = ceilingsFromDetails(details);
|
|
24399
|
+
refuseUnresolvedImports(details);
|
|
24400
|
+
refuseEmptyScope(details, config, opts.failOnEmptySrc, out);
|
|
24401
|
+
const baselinePath = path.join(targetDir, BASELINE_FILE);
|
|
24402
|
+
const treeBaseline = existsSync(baselinePath) ? parseHealthBaseline(readFileSync(baselinePath, "utf-8")) : void 0;
|
|
24403
|
+
const raiseAssessment = assessRaises(executor, targetDir, {
|
|
24404
|
+
baseline: treeBaseline,
|
|
24405
|
+
health: config,
|
|
24406
|
+
measured: {
|
|
24407
|
+
metrics: current,
|
|
24408
|
+
ceilings
|
|
24409
|
+
}
|
|
24410
|
+
}, out);
|
|
24411
|
+
const sizes = measureSizeBudgets(targetDir, config.sizeBudgets, {
|
|
24412
|
+
executor,
|
|
24413
|
+
changedNames: changedPathsForMeasurers(executor, targetDir, config.sizeBudgets, raiseAssessment.fields.reference.mergeBase, raiseAssessment.prefix)
|
|
24414
|
+
});
|
|
24415
|
+
if (treeBaseline === void 0) return {
|
|
24416
|
+
executor,
|
|
24417
|
+
details,
|
|
24418
|
+
current,
|
|
24419
|
+
ceilings,
|
|
24420
|
+
sizes,
|
|
24421
|
+
baselinePath,
|
|
24422
|
+
referenceFields: raiseAssessment.fields,
|
|
24423
|
+
prefix: raiseAssessment.prefix,
|
|
24424
|
+
existing: void 0
|
|
24425
|
+
};
|
|
24426
|
+
const result = compareMetrics(treeBaseline, current, config.tolerance, ceilings, sizes);
|
|
24427
|
+
const attribution = attributeRun({
|
|
24428
|
+
result,
|
|
24429
|
+
executor,
|
|
24430
|
+
targetDir,
|
|
24431
|
+
fields: raiseAssessment.fields,
|
|
24432
|
+
prefix: raiseAssessment.prefix,
|
|
24433
|
+
config
|
|
24434
|
+
});
|
|
24435
|
+
return {
|
|
24436
|
+
executor,
|
|
24437
|
+
details,
|
|
24438
|
+
current,
|
|
24439
|
+
ceilings,
|
|
24440
|
+
sizes,
|
|
24441
|
+
baselinePath,
|
|
24442
|
+
referenceFields: raiseAssessment.fields,
|
|
24443
|
+
prefix: raiseAssessment.prefix,
|
|
24444
|
+
existing: {
|
|
24445
|
+
baseline: treeBaseline,
|
|
24446
|
+
result,
|
|
24447
|
+
attribution
|
|
24448
|
+
}
|
|
24449
|
+
};
|
|
24450
|
+
}
|
|
24451
|
+
//#endregion
|
|
24452
|
+
//#region src/codebase-health/staleness.ts
|
|
24453
|
+
function wasMeasured(metric, path, details) {
|
|
24454
|
+
if (metric === "crowdedDirectories") return details.directories.has(path);
|
|
24455
|
+
return details.graphModules?.has(path) ?? true;
|
|
24456
|
+
}
|
|
24457
|
+
function exemptionWasMeasured(metric, key, details) {
|
|
24458
|
+
const measured = metric === "crowdedDirectories" ? details.directories : details.graphModules;
|
|
24459
|
+
if (measured === void 0) return true;
|
|
24460
|
+
const matches = exemptKeyMatcher(key);
|
|
24461
|
+
return [...measured].some(matches);
|
|
24462
|
+
}
|
|
24463
|
+
function findStaleEntries(baseline, details, exempt) {
|
|
24464
|
+
const stale = [];
|
|
24465
|
+
for (const key of RATCHETED_METRIC_KEYS) {
|
|
24466
|
+
const recorded = baseline.moduleCeilings?.[key];
|
|
24467
|
+
if (recorded === void 0) continue;
|
|
24468
|
+
for (const [path, count] of Object.entries(recorded)) {
|
|
24469
|
+
if (wasMeasured(key, path, details)) continue;
|
|
24470
|
+
stale.push({
|
|
24471
|
+
kind: "ceiling",
|
|
24472
|
+
metric: key,
|
|
24473
|
+
path,
|
|
24474
|
+
count
|
|
24475
|
+
});
|
|
24476
|
+
}
|
|
24477
|
+
}
|
|
24478
|
+
for (const key of EXEMPTABLE_METRIC_KEYS) for (const path of Object.keys(exempt[key] ?? {})) {
|
|
24479
|
+
if (exemptionWasMeasured(key, path, details)) continue;
|
|
24480
|
+
stale.push({
|
|
24481
|
+
kind: "exemption",
|
|
24482
|
+
metric: key,
|
|
24483
|
+
path
|
|
24484
|
+
});
|
|
24485
|
+
}
|
|
24486
|
+
return stale;
|
|
24487
|
+
}
|
|
24488
|
+
//#endregion
|
|
24489
|
+
//#region src/commands/health-check.ts
|
|
23900
24490
|
function assessSwitchOff(executor, targetDir, config, out) {
|
|
23901
24491
|
const read = readTargetReference(executor, targetDir, out);
|
|
23902
24492
|
if (read.kind === "unavailable") return void 0;
|
|
@@ -23907,17 +24497,28 @@ function assessSwitchOff(executor, targetDir, config, out) {
|
|
|
23907
24497
|
const named = healthOverridesAt(targetDir);
|
|
23908
24498
|
const limitsOver = { ...config };
|
|
23909
24499
|
for (const name of RAISABLE_LIMITS) if (named?.[name] === void 0) limitsOver[name] = reference.health[name];
|
|
23910
|
-
return
|
|
23911
|
-
|
|
23912
|
-
|
|
23913
|
-
|
|
24500
|
+
return {
|
|
24501
|
+
reference: {
|
|
24502
|
+
status: "read",
|
|
24503
|
+
mergeBase: reference.mergeBase
|
|
24504
|
+
},
|
|
24505
|
+
raises: findRaises(reference, {
|
|
24506
|
+
baseline: treeBaseline,
|
|
24507
|
+
health: limitsOver
|
|
24508
|
+
})
|
|
24509
|
+
};
|
|
23914
24510
|
}
|
|
23915
|
-
function
|
|
23916
|
-
const
|
|
23917
|
-
|
|
24511
|
+
function sizeValuesToRecord(baseline, sizes, reset) {
|
|
24512
|
+
const recorded = { ...baseline?.sizeBudgets };
|
|
24513
|
+
for (const measurement of sizes) {
|
|
24514
|
+
if (measurement.measured === void 0) continue;
|
|
24515
|
+
if (!reset && recorded[measurement.id] !== void 0) continue;
|
|
24516
|
+
recorded[measurement.id] = measurement.measured;
|
|
24517
|
+
}
|
|
24518
|
+
return Object.keys(recorded).length > 0 ? recorded : void 0;
|
|
23918
24519
|
}
|
|
23919
24520
|
function handleMissingBaseline(state, updateBaseline, failOnMissingBaseline) {
|
|
23920
|
-
const { baselinePath, config, details, current, ceilings, out } = state;
|
|
24521
|
+
const { baselinePath, config, details, current, ceilings, sizes, out } = state;
|
|
23921
24522
|
if (updateBaseline === true) {
|
|
23922
24523
|
if (config.graphMetricsEnabled && details.circularDependencies === void 0) {
|
|
23923
24524
|
out.notice(`Skipped baseline creation: graph metrics are unavailable this run. Fix the cruise before creating ${BASELINE_FILE}.`);
|
|
@@ -23930,7 +24531,7 @@ function handleMissingBaseline(state, updateBaseline, failOnMissingBaseline) {
|
|
|
23930
24531
|
process.exitCode = 1;
|
|
23931
24532
|
return;
|
|
23932
24533
|
}
|
|
23933
|
-
writeFileSync(baselinePath, serializeHealthBaseline(current, ceilings));
|
|
24534
|
+
writeFileSync(baselinePath, serializeHealthBaseline(current, ceilings, sizeValuesToRecord(void 0, sizes, false)));
|
|
23934
24535
|
out.notice(`Created ${BASELINE_FILE} (no prior baseline).`);
|
|
23935
24536
|
out.json({
|
|
23936
24537
|
status: "baseline-created",
|
|
@@ -23954,18 +24555,23 @@ function handleMissingBaseline(state, updateBaseline, failOnMissingBaseline) {
|
|
|
23954
24555
|
throw new FatalError(message);
|
|
23955
24556
|
}
|
|
23956
24557
|
function applyBaselineUpdate(state, baseline, reset) {
|
|
23957
|
-
const { baselinePath, config, details, current, ceilings, out } = state;
|
|
24558
|
+
const { baselinePath, config, details, current, ceilings, sizes, out } = state;
|
|
23958
24559
|
if (config.graphMetricsEnabled && details.circularDependencies === void 0) {
|
|
23959
24560
|
out.notice(`Skipped baseline update: graph metrics are unavailable this run. Fix the cruise before updating.`);
|
|
23960
24561
|
process.exitCode = 1;
|
|
23961
24562
|
} else if (reset === true) {
|
|
23962
|
-
writeFileSync(baselinePath, serializeHealthBaseline(current, ceilings));
|
|
24563
|
+
writeFileSync(baselinePath, serializeHealthBaseline(current, ceilings, sizeValuesToRecord(baseline, sizes, true)));
|
|
23963
24564
|
out.notice(`Reset ${BASELINE_FILE} to the current run.`);
|
|
23964
|
-
} else if (isFlatOrBetter(baseline, current, ceilings)) {
|
|
23965
|
-
writeFileSync(baselinePath, serializeHealthBaseline(current, ceilings));
|
|
24565
|
+
} else if (isFlatOrBetter(baseline, current, ceilings, sizes)) {
|
|
24566
|
+
writeFileSync(baselinePath, serializeHealthBaseline(current, ceilings, sizeValuesToRecord(baseline, sizes, false)));
|
|
23966
24567
|
out.notice(`Updated ${BASELINE_FILE} (no regressions).`);
|
|
23967
24568
|
} else out.notice(`Skipped baseline update: a metric or ceiling regressed. Fix the regression before updating.`);
|
|
23968
24569
|
}
|
|
24570
|
+
function collectRegressions(result, details) {
|
|
24571
|
+
const regressions = {};
|
|
24572
|
+
for (const m of result.metrics) if (!m.pass) regressions[m.key] = offenderEntries(details, m.key);
|
|
24573
|
+
return regressions;
|
|
24574
|
+
}
|
|
23969
24575
|
const healthCheckCommand = defineCommand({
|
|
23970
24576
|
meta: {
|
|
23971
24577
|
name: "health:check",
|
|
@@ -23998,7 +24604,7 @@ const healthCheckCommand = defineCommand({
|
|
|
23998
24604
|
},
|
|
23999
24605
|
json: {
|
|
24000
24606
|
type: "boolean",
|
|
24001
|
-
description: "Emit one JSON document: status (compared | disabled | baseline-created | missing-baseline | graph-unavailable). disabled carries reference
|
|
24607
|
+
description: "Emit one JSON document: status (compared | disabled | baseline-created | missing-baseline | graph-unavailable). disabled carries reference and raises only when the default branch has health enabled -- switching it off is a raise -- and nothing otherwise. Every other status carries reference ({status (read | no-baseline), mergeBase}) and raises ([{leaf, reference, current}]: every way this branch's two health files are higher than the default branch's, which it carries with its own code and a person merges). baseline-created, missing-baseline and graph-unavailable carry metrics ({crowdedDirectories, circularDependencies?, fanInOutliers?, fanOutOutliers?}) and sources. compared carries pass, tolerance, metrics (a list: [{key (crowdedDirectories | circularDependencies | fanInOutliers | fanOutOutliers), baseline?, current?, delta?, pass}]), ceilings ([{key (fanInOutliers | fanOutOutliers | crowdedDirectories), module, baseline, current, pass}]), sizeBudgets ([{id, ceiling?, measured?, pass, reason?}] -- every budget .tooling.json#health.sizeBudgets declares; reason is `unrecorded` for a declared budget with no recorded value, or the measurer's own explanation when it measured nothing), outlierBase ({status (measured | not-needed | unavailable), mergeBase?, reason?}), newOutliers ([{key, module, current}], outliers not on the merge base; empty unless measured), baseOutliers ([{key, module, base, current}]) and unattributedOutliers ([{key, module, current}], every outlier with no ceiling when the base was not measured), graphMetricsEnabled, graphMetricsMissing, staleEntries ([{kind (ceiling | exemption), metric, path, count?}]), regressions ({<failing metric key>: [{module, count} | {from, to}]}) and sources. sources is {entries ([{pattern, roots, status (resolved | unmatched | unrepresentable)}]), roots, fileCount}."
|
|
24002
24608
|
}
|
|
24003
24609
|
},
|
|
24004
24610
|
run({ args }) {
|
|
@@ -24007,69 +24613,365 @@ const healthCheckCommand = defineCommand({
|
|
|
24007
24613
|
const config = resolveHealthConfig(targetDir);
|
|
24008
24614
|
if (!config.enabled) {
|
|
24009
24615
|
out.notice("health: disabled (set .tooling.json#health.enabled to true to enable).");
|
|
24616
|
+
const switchOff = assessSwitchOff(createRealHealthExecutor(targetDir), targetDir, config, out);
|
|
24010
24617
|
out.json({
|
|
24011
24618
|
status: "disabled",
|
|
24012
|
-
...
|
|
24619
|
+
...switchOff
|
|
24013
24620
|
});
|
|
24621
|
+
reportCarriedRaises(switchOff?.raises ?? [], out);
|
|
24014
24622
|
return;
|
|
24015
24623
|
}
|
|
24016
24624
|
if (!config.graphMetricsEnabled) out.notice("health: graph metrics disabled (no JS/TS source to cruise) -- crowdedDirectories only.");
|
|
24017
|
-
const
|
|
24018
|
-
const details = collectDetails(executor, buildCollectConfig(config, { tsConfig: existsSync(path.join(targetDir, "tsconfig.json")) ? "tsconfig.json" : void 0 }));
|
|
24019
|
-
const current = metricsFromDetails(details);
|
|
24020
|
-
const ceilings = ceilingsFromDetails(details);
|
|
24021
|
-
refuseUnresolvedImports(details);
|
|
24022
|
-
const { warnings, emptyScope } = formatSourceDiagnosis(details.sources, config.graphMetricsEnabled);
|
|
24023
|
-
for (const warning of warnings) out.notice(warning);
|
|
24024
|
-
if (emptyScope !== void 0 && args["fail-on-empty-src"] !== false) throw new FatalError(emptyScope);
|
|
24025
|
-
if (emptyScope !== void 0) out.notice(emptyScope);
|
|
24026
|
-
const baselinePath = path.join(targetDir, BASELINE_FILE);
|
|
24027
|
-
const treeBaseline = existsSync(baselinePath) ? parseHealthBaseline(readFileSync(baselinePath, "utf-8")) : void 0;
|
|
24028
|
-
const referenceFields = assessRaises(executor, targetDir, {
|
|
24029
|
-
baseline: treeBaseline,
|
|
24030
|
-
health: config,
|
|
24031
|
-
measured: {
|
|
24032
|
-
metrics: current,
|
|
24033
|
-
ceilings
|
|
24034
|
-
}
|
|
24035
|
-
}, out);
|
|
24625
|
+
const run = measureHealth(targetDir, config, out, { failOnEmptySrc: args["fail-on-empty-src"] });
|
|
24036
24626
|
const state = {
|
|
24037
|
-
baselinePath,
|
|
24627
|
+
baselinePath: run.baselinePath,
|
|
24038
24628
|
config,
|
|
24039
|
-
details,
|
|
24040
|
-
current,
|
|
24041
|
-
ceilings,
|
|
24629
|
+
details: run.details,
|
|
24630
|
+
current: run.current,
|
|
24631
|
+
ceilings: run.ceilings,
|
|
24632
|
+
sizes: run.sizes,
|
|
24042
24633
|
out,
|
|
24043
|
-
referenceFields
|
|
24634
|
+
referenceFields: run.referenceFields
|
|
24044
24635
|
};
|
|
24045
|
-
if (
|
|
24636
|
+
if (run.existing === void 0) {
|
|
24046
24637
|
handleMissingBaseline(state, args["update-baseline"], args["fail-on-missing-baseline"]);
|
|
24047
24638
|
return;
|
|
24048
24639
|
}
|
|
24049
|
-
const baseline =
|
|
24050
|
-
|
|
24051
|
-
|
|
24052
|
-
const regressions =
|
|
24053
|
-
|
|
24054
|
-
const graphUnavailable = formatGraphUnavailable(baseline, details, config.graphMetricsEnabled, details.graphDiagnostic);
|
|
24640
|
+
const { baseline, result, attribution } = run.existing;
|
|
24641
|
+
out.text(formatCompareDiff(result, run.details, attribution));
|
|
24642
|
+
reportCarriedRaises(run.referenceFields.raises, out);
|
|
24643
|
+
const regressions = collectRegressions(result, run.details);
|
|
24644
|
+
const graphUnavailable = formatGraphUnavailable(baseline, run.details, config.graphMetricsEnabled, run.details.graphDiagnostic);
|
|
24055
24645
|
if (graphUnavailable !== void 0) out.notice(graphUnavailable);
|
|
24056
|
-
const stale = findStaleEntries(baseline, details, config.exempt);
|
|
24646
|
+
const stale = findStaleEntries(baseline, run.details, config.exempt);
|
|
24057
24647
|
for (const warning of formatStaleEntries(stale)) out.notice(warning);
|
|
24058
24648
|
out.json({
|
|
24059
24649
|
status: "compared",
|
|
24060
|
-
...referenceFields,
|
|
24061
|
-
|
|
24650
|
+
...run.referenceFields,
|
|
24651
|
+
pass: result.pass,
|
|
24652
|
+
tolerance: result.tolerance,
|
|
24653
|
+
metrics: result.metrics,
|
|
24654
|
+
ceilings: result.ceilings,
|
|
24655
|
+
sizeBudgets: result.sizeResults,
|
|
24656
|
+
outlierBase: attribution.outlierBase,
|
|
24657
|
+
newOutliers: attribution.newOutliers,
|
|
24658
|
+
baseOutliers: attribution.baseOutliers,
|
|
24659
|
+
unattributedOutliers: attribution.unattributedOutliers,
|
|
24062
24660
|
graphMetricsEnabled: config.graphMetricsEnabled,
|
|
24063
24661
|
graphMetricsMissing: graphUnavailable !== void 0,
|
|
24064
24662
|
staleEntries: stale,
|
|
24065
24663
|
regressions,
|
|
24066
|
-
sources: details.sources
|
|
24664
|
+
sources: run.details.sources
|
|
24067
24665
|
});
|
|
24068
24666
|
if (args["update-baseline"] === true) applyBaselineUpdate(state, baseline, args.reset);
|
|
24069
24667
|
if (!result.pass || graphUnavailable !== void 0) process.exitCode = 1;
|
|
24070
24668
|
}
|
|
24071
24669
|
});
|
|
24072
24670
|
//#endregion
|
|
24671
|
+
//#region src/codebase-health/raise.ts
|
|
24672
|
+
const SIZE_RAISE_FACTOR = 1.01;
|
|
24673
|
+
function sizeRaiseValue(measured) {
|
|
24674
|
+
return Math.floor(measured * SIZE_RAISE_FACTOR) + 1;
|
|
24675
|
+
}
|
|
24676
|
+
const JsonObjectSchema = z.record(z.string(), z.unknown());
|
|
24677
|
+
const EXEMPT_LEAF_KEYS = [
|
|
24678
|
+
{
|
|
24679
|
+
prefix: "fan-in",
|
|
24680
|
+
key: "fanInOutliers"
|
|
24681
|
+
},
|
|
24682
|
+
{
|
|
24683
|
+
prefix: "fan-out",
|
|
24684
|
+
key: "fanOutOutliers"
|
|
24685
|
+
},
|
|
24686
|
+
{
|
|
24687
|
+
prefix: "crowding",
|
|
24688
|
+
key: "crowdedDirectories"
|
|
24689
|
+
}
|
|
24690
|
+
];
|
|
24691
|
+
function refuseDeniableReason(leaf, reason) {
|
|
24692
|
+
const fixed = fixText(`// ${reason}`, {
|
|
24693
|
+
offenders: resolveOffenders(),
|
|
24694
|
+
path: "health-raise.ts"
|
|
24695
|
+
});
|
|
24696
|
+
const refused = fixed.decisions.find((decision) => !decision.applied);
|
|
24697
|
+
if (refused !== void 0) throw new FatalError(`health:raise: the reason for --exempt "${leaf}" carries ${refused.hit.offender.codepoint} (${refused.hit.offender.name}): ${refused.reason ?? "this gate denies it"}. Rewrite it in ASCII.`);
|
|
24698
|
+
return fixed.text.slice(3);
|
|
24699
|
+
}
|
|
24700
|
+
function parseExemptLeaf(leaf) {
|
|
24701
|
+
const separator = leaf.indexOf(":");
|
|
24702
|
+
const prefix = separator < 0 ? "" : leaf.slice(0, separator).trim();
|
|
24703
|
+
const path = separator < 0 ? "" : leaf.slice(separator + 1).trim();
|
|
24704
|
+
if (prefix === "size") throw new FatalError(`health:raise: --exempt "${leaf}" names a size budget, which has no module to exempt. Run without --exempt: "health:raise" widens a grown size budget by measurement.`);
|
|
24705
|
+
const named = EXEMPT_LEAF_KEYS.find((entry) => entry.prefix === prefix);
|
|
24706
|
+
if (named === void 0 || path === "") throw new FatalError(`health:raise: --exempt "${leaf}" names no exemptable leaf. Pass one "health:check" printed: "fan-in: <path>", "fan-out: <path>", "crowding: <path>".`);
|
|
24707
|
+
return {
|
|
24708
|
+
key: named.key,
|
|
24709
|
+
path
|
|
24710
|
+
};
|
|
24711
|
+
}
|
|
24712
|
+
function copyCeilings(ceilings) {
|
|
24713
|
+
const copy = {};
|
|
24714
|
+
for (const key of RATCHETED_METRIC_KEYS) {
|
|
24715
|
+
const byModule = ceilings?.[key];
|
|
24716
|
+
if (byModule !== void 0) copy[key] = { ...byModule };
|
|
24717
|
+
}
|
|
24718
|
+
return copy;
|
|
24719
|
+
}
|
|
24720
|
+
function writeCeiling(ceilings, key, module, to) {
|
|
24721
|
+
const byModule = ceilings[key] ?? {};
|
|
24722
|
+
byModule[module] = to;
|
|
24723
|
+
ceilings[key] = byModule;
|
|
24724
|
+
}
|
|
24725
|
+
function headlineFor(baseline, comparison, key, exemptCount) {
|
|
24726
|
+
const measured = comparison.metrics.find((metric) => metric.key === key)?.current;
|
|
24727
|
+
if (measured === void 0) return void 0;
|
|
24728
|
+
const remaining = Math.max(0, measured - exemptCount);
|
|
24729
|
+
return baseline[key] === void 0 ? remaining : Math.max(baseline[key] ?? 0, remaining);
|
|
24730
|
+
}
|
|
24731
|
+
function exemptionWrites(input) {
|
|
24732
|
+
const writes = [];
|
|
24733
|
+
const notes = [];
|
|
24734
|
+
for (const request of input.exemptions) {
|
|
24735
|
+
const parsed = parseExemptLeaf(request.leaf);
|
|
24736
|
+
if (parsed === void 0) continue;
|
|
24737
|
+
const recorded = input.config.exempt[parsed.key]?.[parsed.path];
|
|
24738
|
+
if (recorded !== void 0) {
|
|
24739
|
+
notes.push(`health.exempt.${parsed.key}.${parsed.path} already reads "${recorded}"; the reason was not overwritten.`);
|
|
24740
|
+
continue;
|
|
24741
|
+
}
|
|
24742
|
+
writes.push({
|
|
24743
|
+
key: parsed.key,
|
|
24744
|
+
path: parsed.path,
|
|
24745
|
+
reason: refuseDeniableReason(request.leaf, request.reason)
|
|
24746
|
+
});
|
|
24747
|
+
}
|
|
24748
|
+
return {
|
|
24749
|
+
writes,
|
|
24750
|
+
notes
|
|
24751
|
+
};
|
|
24752
|
+
}
|
|
24753
|
+
function objectAt(parent, key, where) {
|
|
24754
|
+
const existing = parent[key];
|
|
24755
|
+
if (existing === void 0) return {};
|
|
24756
|
+
const parsed = JsonObjectSchema.safeParse(existing);
|
|
24757
|
+
if (!parsed.success) throw new FatalError(`health:raise: ${where}#${key} is not an object, so an exemption cannot be written there.`);
|
|
24758
|
+
return { ...parsed.data };
|
|
24759
|
+
}
|
|
24760
|
+
function applyExemptions(tooling, writes) {
|
|
24761
|
+
if (writes.length === 0) return void 0;
|
|
24762
|
+
const file = JsonObjectSchema.safeParse(tooling ?? {});
|
|
24763
|
+
if (!file.success) throw new FatalError(`health:raise: .tooling.json is not a JSON object, so no exemption can be written.`);
|
|
24764
|
+
const result = { ...file.data };
|
|
24765
|
+
for (const write of writes) {
|
|
24766
|
+
const health = objectAt(result, "health", ".tooling.json");
|
|
24767
|
+
const exempt = objectAt(health, "exempt", ".tooling.json#health");
|
|
24768
|
+
const table = objectAt(exempt, write.key, ".tooling.json#health.exempt");
|
|
24769
|
+
table[write.path] = write.reason;
|
|
24770
|
+
exempt[write.key] = table;
|
|
24771
|
+
health["exempt"] = exempt;
|
|
24772
|
+
result["health"] = health;
|
|
24773
|
+
}
|
|
24774
|
+
return result;
|
|
24775
|
+
}
|
|
24776
|
+
function planHealthRaise(input) {
|
|
24777
|
+
const ceilings = copyCeilings(input.baseline.moduleCeilings);
|
|
24778
|
+
const ceilingWrites = [];
|
|
24779
|
+
const headlines = {};
|
|
24780
|
+
const notes = [];
|
|
24781
|
+
const exemptions = exemptionWrites(input);
|
|
24782
|
+
notes.push(...exemptions.notes);
|
|
24783
|
+
const isExcused = (key, module) => exemptions.writes.some((write) => write.key === key && exemptKeyMatcher(write.path)(module));
|
|
24784
|
+
const exemptedCount = /* @__PURE__ */ new Map();
|
|
24785
|
+
const countExcused = (key, module) => {
|
|
24786
|
+
if (isExcused(key, module)) exemptedCount.set(key, (exemptedCount.get(key) ?? 0) + 1);
|
|
24787
|
+
};
|
|
24788
|
+
for (const outlier of input.attribution.newOutliers) countExcused(outlier.key, outlier.module);
|
|
24789
|
+
for (const ceiling of input.comparison.ceilings) if (!ceiling.pass) countExcused(ceiling.key, ceiling.module);
|
|
24790
|
+
const outlierBase = input.attribution.outlierBase;
|
|
24791
|
+
if (outlierBase.status === "unavailable") notes.push(`no "(new)" ceiling was written: the merge base ${outlierBase.mergeBase} could not be measured (${outlierBase.reason}), so nothing could be called a first crossing.`);
|
|
24792
|
+
for (const outlier of input.attribution.newOutliers) {
|
|
24793
|
+
if (isExcused(outlier.key, outlier.module)) continue;
|
|
24794
|
+
writeCeiling(ceilings, outlier.key, outlier.module, outlier.current);
|
|
24795
|
+
const headline = headlineFor(input.baseline, input.comparison, outlier.key, exemptedCount.get(outlier.key) ?? 0);
|
|
24796
|
+
if (headline !== void 0) headlines[outlier.key] = headline;
|
|
24797
|
+
ceilingWrites.push({
|
|
24798
|
+
key: outlier.key,
|
|
24799
|
+
module: outlier.module,
|
|
24800
|
+
value: outlier.current,
|
|
24801
|
+
...headline === void 0 ? {} : { headline }
|
|
24802
|
+
});
|
|
24803
|
+
}
|
|
24804
|
+
for (const ceiling of input.comparison.ceilings) {
|
|
24805
|
+
if (ceiling.pass) continue;
|
|
24806
|
+
if (isExcused(ceiling.key, ceiling.module)) continue;
|
|
24807
|
+
writeCeiling(ceilings, ceiling.key, ceiling.module, ceiling.current);
|
|
24808
|
+
ceilingWrites.push({
|
|
24809
|
+
key: ceiling.key,
|
|
24810
|
+
module: ceiling.module,
|
|
24811
|
+
value: ceiling.current
|
|
24812
|
+
});
|
|
24813
|
+
}
|
|
24814
|
+
const sizeBudgets = { ...input.baseline.sizeBudgets };
|
|
24815
|
+
const sizeWrites = [];
|
|
24816
|
+
for (const size of input.comparison.sizeResults) {
|
|
24817
|
+
if (size.pass || size.measured === void 0 || size.ceiling === void 0) continue;
|
|
24818
|
+
if (size.measured <= size.ceiling) continue;
|
|
24819
|
+
const value = sizeRaiseValue(size.measured);
|
|
24820
|
+
sizeBudgets[size.id] = value;
|
|
24821
|
+
sizeWrites.push({
|
|
24822
|
+
id: size.id,
|
|
24823
|
+
value
|
|
24824
|
+
});
|
|
24825
|
+
}
|
|
24826
|
+
return {
|
|
24827
|
+
baseline: widenedBaseline(input.baseline, headlines, ceilings, sizeBudgets),
|
|
24828
|
+
tooling: applyExemptions(input.tooling, exemptions.writes),
|
|
24829
|
+
ceilings: ceilingWrites,
|
|
24830
|
+
sizes: sizeWrites,
|
|
24831
|
+
exemptions: exemptions.writes,
|
|
24832
|
+
notes
|
|
24833
|
+
};
|
|
24834
|
+
}
|
|
24835
|
+
function widenedBaseline(baseline, headlines, ceilings, sizeBudgets) {
|
|
24836
|
+
const widened = { ...baseline };
|
|
24837
|
+
for (const key of METRIC_KEYS) {
|
|
24838
|
+
const headline = headlines[key];
|
|
24839
|
+
if (headline !== void 0) widened[key] = headline;
|
|
24840
|
+
}
|
|
24841
|
+
widened.moduleCeilings = Object.keys(ceilings).length > 0 ? ceilings : void 0;
|
|
24842
|
+
widened.sizeBudgets = Object.keys(sizeBudgets).length > 0 ? sizeBudgets : void 0;
|
|
24843
|
+
return widened;
|
|
24844
|
+
}
|
|
24845
|
+
function planHasWrites(plan) {
|
|
24846
|
+
return plan.ceilings.length > 0 || plan.sizes.length > 0 || plan.exemptions.length > 0;
|
|
24847
|
+
}
|
|
24848
|
+
//#endregion
|
|
24849
|
+
//#region src/commands/health-raise.ts
|
|
24850
|
+
const JsonValueSchema = z.json();
|
|
24851
|
+
const TOOLING_FILE = ".tooling.json";
|
|
24852
|
+
function collectExemptPairs(rawArgs) {
|
|
24853
|
+
const leaves = collectFlagValues(rawArgs, "--exempt");
|
|
24854
|
+
const reasons = collectFlagValues(rawArgs, "--reason");
|
|
24855
|
+
if (leaves.length === 0 && reasons.length > 0) throw new FatalError(`health:raise: ${String(reasons.length)} --reason given with no --exempt to explain.`);
|
|
24856
|
+
if (reasons.length < leaves.length) throw new FatalError(`health:raise: ${String(leaves.length)} --exempt given with only ${String(reasons.length)} --reason. Every exemption owes its own reason: it is the line a later reader judges.`);
|
|
24857
|
+
return leaves.map((leaf, index) => ({
|
|
24858
|
+
leaf,
|
|
24859
|
+
reason: reasons[index] ?? ""
|
|
24860
|
+
}));
|
|
24861
|
+
}
|
|
24862
|
+
function formatterFor(targetDir) {
|
|
24863
|
+
const detected = computeDefaults(targetDir);
|
|
24864
|
+
return createContentFormatter({
|
|
24865
|
+
targetDir,
|
|
24866
|
+
formatter: loadToolingConfig(targetDir)?.formatter ?? detected.formatter
|
|
24867
|
+
});
|
|
24868
|
+
}
|
|
24869
|
+
function readToolingFile(targetDir) {
|
|
24870
|
+
const full = path.join(targetDir, TOOLING_FILE);
|
|
24871
|
+
if (!existsSync(full)) return void 0;
|
|
24872
|
+
const text = readFileSync(full, "utf-8");
|
|
24873
|
+
let json;
|
|
24874
|
+
try {
|
|
24875
|
+
json = JSON.parse(text);
|
|
24876
|
+
} catch (error) {
|
|
24877
|
+
const reason = error instanceof Error ? error.message : "unknown error";
|
|
24878
|
+
throw new FatalError(`health:raise: ${TOOLING_FILE} is not JSON: ${reason}. Fix the file and re-run.`);
|
|
24879
|
+
}
|
|
24880
|
+
const parsed = JsonValueSchema.safeParse(json);
|
|
24881
|
+
if (!parsed.success) throw new FatalError(`health:raise: ${TOOLING_FILE} is not JSON: ${parsed.error.message}`);
|
|
24882
|
+
return parsed.data;
|
|
24883
|
+
}
|
|
24884
|
+
function describePlan(plan) {
|
|
24885
|
+
const lines = [];
|
|
24886
|
+
for (const ceiling of plan.ceilings) {
|
|
24887
|
+
const headline = ceiling.headline === void 0 ? "" : `, ${ceiling.key} set to ${String(ceiling.headline)}`;
|
|
24888
|
+
lines.push(`health-baseline: ${ceiling.key} ${ceiling.module} ceiling ${String(ceiling.value)}${headline}`);
|
|
24889
|
+
}
|
|
24890
|
+
for (const size of plan.sizes) lines.push(`health-baseline: size budget ${size.id} widened to ${String(size.value)}`);
|
|
24891
|
+
for (const exemption of plan.exemptions) lines.push(`${TOOLING_FILE}: health.exempt.${exemption.key}.${exemption.path} exempted -- ${exemption.reason}`);
|
|
24892
|
+
for (const note of plan.notes) lines.push(`health: ${note}`);
|
|
24893
|
+
return lines;
|
|
24894
|
+
}
|
|
24895
|
+
const healthRaiseCommand = defineCommand({
|
|
24896
|
+
meta: {
|
|
24897
|
+
name: "health:raise",
|
|
24898
|
+
description: "Widen the health budgets this branch crossed, by measurement: no number is an argument. Writes health-baseline.json (a new outlier's ceiling plus its headline count, a grown module ceiling, a grown size budget) and, for --exempt pairs, .tooling.json#health.exempt. Does nothing unless .tooling.json#health.enabled is true."
|
|
24899
|
+
},
|
|
24900
|
+
args: {
|
|
24901
|
+
dir: {
|
|
24902
|
+
type: "positional",
|
|
24903
|
+
valueHint: "dir",
|
|
24904
|
+
description: "Project directory (default: current directory)",
|
|
24905
|
+
required: false
|
|
24906
|
+
},
|
|
24907
|
+
"dry-run": {
|
|
24908
|
+
type: "boolean",
|
|
24909
|
+
description: "Print what would be written and write nothing"
|
|
24910
|
+
},
|
|
24911
|
+
exempt: {
|
|
24912
|
+
type: "string",
|
|
24913
|
+
description: "Exempt this leaf instead of recording a ceiling (repeatable), as \"health:check\" printed it: \"fan-in: src/a.ts\""
|
|
24914
|
+
},
|
|
24915
|
+
reason: {
|
|
24916
|
+
type: "string",
|
|
24917
|
+
description: "Why this exemption is justified (repeatable; one per --exempt)"
|
|
24918
|
+
},
|
|
24919
|
+
json: {
|
|
24920
|
+
type: "boolean",
|
|
24921
|
+
description: "Emit one JSON document: status (raised | nothing-to-raise | disabled), dryRun, wrote ([path]), ceilings ([{key, module, value, headline?}]), sizeBudgets ([{id, value}]), exemptions ([{key, path, reason}]), notes ([string]), reference ({status (read | no-baseline), mergeBase})."
|
|
24922
|
+
}
|
|
24923
|
+
},
|
|
24924
|
+
run({ args, rawArgs }) {
|
|
24925
|
+
const targetDir = path.resolve(args.dir ?? ".");
|
|
24926
|
+
const out = createCliOutput(args.json === true);
|
|
24927
|
+
const config = resolveHealthConfig(targetDir);
|
|
24928
|
+
if (!config.enabled) {
|
|
24929
|
+
out.notice("health: disabled (set .tooling.json#health.enabled to true to enable).");
|
|
24930
|
+
out.json({
|
|
24931
|
+
status: "disabled",
|
|
24932
|
+
dryRun: args["dry-run"] === true
|
|
24933
|
+
});
|
|
24934
|
+
return;
|
|
24935
|
+
}
|
|
24936
|
+
const run = measureHealth(targetDir, config, out);
|
|
24937
|
+
if (run.existing === void 0) throw new FatalError("health:raise: no health-baseline.json to widen. Run \"health:check --update-baseline\" first: a raise widens a recorded number, and there is nothing recorded.");
|
|
24938
|
+
const plan = planHealthRaise({
|
|
24939
|
+
comparison: run.existing.result,
|
|
24940
|
+
attribution: run.existing.attribution,
|
|
24941
|
+
baseline: run.existing.baseline,
|
|
24942
|
+
config,
|
|
24943
|
+
tooling: readToolingFile(targetDir),
|
|
24944
|
+
exemptions: collectExemptPairs(rawArgs)
|
|
24945
|
+
});
|
|
24946
|
+
const dryRun = args["dry-run"] === true;
|
|
24947
|
+
const wrote = dryRun ? [] : writePlan(targetDir, plan);
|
|
24948
|
+
for (const line of describePlan(plan)) out.text(line);
|
|
24949
|
+
out.json({
|
|
24950
|
+
status: planHasWrites(plan) ? "raised" : "nothing-to-raise",
|
|
24951
|
+
dryRun,
|
|
24952
|
+
wrote,
|
|
24953
|
+
ceilings: plan.ceilings,
|
|
24954
|
+
sizeBudgets: plan.sizes,
|
|
24955
|
+
exemptions: plan.exemptions,
|
|
24956
|
+
notes: plan.notes,
|
|
24957
|
+
reference: run.referenceFields.reference
|
|
24958
|
+
});
|
|
24959
|
+
}
|
|
24960
|
+
});
|
|
24961
|
+
function writePlan(targetDir, plan) {
|
|
24962
|
+
const format = formatterFor(targetDir);
|
|
24963
|
+
const wrote = [];
|
|
24964
|
+
if (plan.ceilings.length > 0 || plan.sizes.length > 0) {
|
|
24965
|
+
writeFileSync(path.join(targetDir, "health-baseline.json"), format("health-baseline.json", serializeHealthBaseline(plan.baseline, plan.baseline.moduleCeilings, plan.baseline.sizeBudgets)));
|
|
24966
|
+
wrote.push("health-baseline.json");
|
|
24967
|
+
}
|
|
24968
|
+
if (plan.tooling !== void 0) {
|
|
24969
|
+
writeFileSync(path.join(targetDir, TOOLING_FILE), format(TOOLING_FILE, JSON.stringify(plan.tooling, null, 2)));
|
|
24970
|
+
wrote.push(TOOLING_FILE);
|
|
24971
|
+
}
|
|
24972
|
+
return wrote;
|
|
24973
|
+
}
|
|
24974
|
+
//#endregion
|
|
24073
24975
|
//#region src/codebase-health/module-report.ts
|
|
24074
24976
|
function normalizeModulePath(raw) {
|
|
24075
24977
|
let normalized = raw.replace(/\\/g, "/");
|
|
@@ -24989,6 +25891,144 @@ const branchPruneCommand = defineCommand({
|
|
|
24989
25891
|
}
|
|
24990
25892
|
});
|
|
24991
25893
|
//#endregion
|
|
25894
|
+
//#region src/docs-check/plan-locate.ts
|
|
25895
|
+
const SCORED_STATUS = /^[A-Z][0-9]+$/;
|
|
25896
|
+
const RENAME_STATUS = /^R[0-9]*$/;
|
|
25897
|
+
const COPY_STATUS = /^C[0-9]*$/;
|
|
25898
|
+
function pathCountFor(token) {
|
|
25899
|
+
if (RENAME_STATUS.test(token) || COPY_STATUS.test(token)) return 2;
|
|
25900
|
+
if (SCORED_STATUS.test(token)) return 2;
|
|
25901
|
+
return 1;
|
|
25902
|
+
}
|
|
25903
|
+
function singlePathStatus(token) {
|
|
25904
|
+
if (token === "A" || token === "M" || token === "D" || token === "T") return token;
|
|
25905
|
+
return null;
|
|
25906
|
+
}
|
|
25907
|
+
function normalizeRecord(token, paths) {
|
|
25908
|
+
const [source, destination] = paths;
|
|
25909
|
+
if (RENAME_STATUS.test(token)) {
|
|
25910
|
+
if (source === void 0 || destination === void 0) return [];
|
|
25911
|
+
return [{
|
|
25912
|
+
status: "D",
|
|
25913
|
+
path: source
|
|
25914
|
+
}, {
|
|
25915
|
+
status: "A",
|
|
25916
|
+
path: destination
|
|
25917
|
+
}];
|
|
25918
|
+
}
|
|
25919
|
+
if (COPY_STATUS.test(token)) {
|
|
25920
|
+
if (destination === void 0) return [];
|
|
25921
|
+
return [{
|
|
25922
|
+
status: "A",
|
|
25923
|
+
path: destination
|
|
25924
|
+
}];
|
|
25925
|
+
}
|
|
25926
|
+
const status = singlePathStatus(token);
|
|
25927
|
+
if (status === null || source === void 0) return [];
|
|
25928
|
+
return [{
|
|
25929
|
+
status,
|
|
25930
|
+
path: source
|
|
25931
|
+
}];
|
|
25932
|
+
}
|
|
25933
|
+
function parseNameStatus(stdout) {
|
|
25934
|
+
const fields = stdout.split("\0");
|
|
25935
|
+
while (fields.length > 0 && fields.at(-1) === "") fields.pop();
|
|
25936
|
+
const entries = [];
|
|
25937
|
+
let index = 0;
|
|
25938
|
+
while (index < fields.length) {
|
|
25939
|
+
const token = fields[index] ?? "";
|
|
25940
|
+
const paths = fields.slice(index + 1, index + 1 + pathCountFor(token));
|
|
25941
|
+
index += 1 + paths.length;
|
|
25942
|
+
entries.push(...normalizeRecord(token, paths));
|
|
25943
|
+
}
|
|
25944
|
+
return entries;
|
|
25945
|
+
}
|
|
25946
|
+
function planDiffArgs(defaultRef) {
|
|
25947
|
+
return [
|
|
25948
|
+
"diff",
|
|
25949
|
+
"--name-status",
|
|
25950
|
+
"-z",
|
|
25951
|
+
`${defaultRef}...HEAD`
|
|
25952
|
+
];
|
|
25953
|
+
}
|
|
25954
|
+
const ANY_MARKER = /(?<![a-zA-Z0-9_])(?:issue|pr)-[1-9][0-9]*(?![0-9])/;
|
|
25955
|
+
function markerFor(ref) {
|
|
25956
|
+
return new RegExp(`(?<![a-zA-Z0-9_])${ref.kind}-${String(ref.number)}(?![0-9])`);
|
|
25957
|
+
}
|
|
25958
|
+
function matchMarkerPaths(paths, refs) {
|
|
25959
|
+
if (refs === null) return paths.filter((path) => path.startsWith("docs/") && ANY_MARKER.test(path));
|
|
25960
|
+
const markers = refs.map(markerFor);
|
|
25961
|
+
return paths.filter((path) => path.startsWith("docs/") && markers.some((m) => m.test(path)));
|
|
25962
|
+
}
|
|
25963
|
+
function isPlanPath(path) {
|
|
25964
|
+
const doc = classifyConventionalDoc(path);
|
|
25965
|
+
return doc !== null && doc.kind !== "spec";
|
|
25966
|
+
}
|
|
25967
|
+
function livePlanPaths(paths) {
|
|
25968
|
+
return paths.filter((path) => isPlanPath(path) && !classifyConventionalDoc(path)?.retired);
|
|
25969
|
+
}
|
|
25970
|
+
function modifiedLivePlans(entries) {
|
|
25971
|
+
const modified = [];
|
|
25972
|
+
for (const item of entries) {
|
|
25973
|
+
if (item.status !== "M") continue;
|
|
25974
|
+
if (isPlanPath(item.path) && !classifyConventionalDoc(item.path)?.retired) modified.push(item.path);
|
|
25975
|
+
}
|
|
25976
|
+
return modified;
|
|
25977
|
+
}
|
|
25978
|
+
function addedPlans(entries) {
|
|
25979
|
+
const live = [];
|
|
25980
|
+
const retired = [];
|
|
25981
|
+
for (const item of entries) {
|
|
25982
|
+
if (item.status !== "A") continue;
|
|
25983
|
+
const doc = classifyConventionalDoc(item.path);
|
|
25984
|
+
if (doc === null || doc.kind === "spec") continue;
|
|
25985
|
+
if (doc.retired) retired.push(item.path);
|
|
25986
|
+
else live.push(item.path);
|
|
25987
|
+
}
|
|
25988
|
+
return {
|
|
25989
|
+
live,
|
|
25990
|
+
retired
|
|
25991
|
+
};
|
|
25992
|
+
}
|
|
25993
|
+
function touchedLivePlans(entries) {
|
|
25994
|
+
const touched = [];
|
|
25995
|
+
for (const item of entries) {
|
|
25996
|
+
if (item.status === "D") continue;
|
|
25997
|
+
if (isPlanPath(item.path) && !classifyConventionalDoc(item.path)?.retired) touched.push(item.path);
|
|
25998
|
+
}
|
|
25999
|
+
return touched;
|
|
26000
|
+
}
|
|
26001
|
+
function locatePlan(input) {
|
|
26002
|
+
const { entries, trackedPaths, refs, isOpen } = input;
|
|
26003
|
+
const [marked] = matchMarkerPaths(refs === null ? touchedLivePlans(entries) : livePlanPaths(trackedPaths), refs);
|
|
26004
|
+
if (marked !== void 0) return {
|
|
26005
|
+
kind: "found",
|
|
26006
|
+
path: marked
|
|
26007
|
+
};
|
|
26008
|
+
const added = addedPlans(entries);
|
|
26009
|
+
const [firstAdded, secondAdded] = added.live;
|
|
26010
|
+
if (firstAdded !== void 0 && secondAdded === void 0) return {
|
|
26011
|
+
kind: "found",
|
|
26012
|
+
path: firstAdded
|
|
26013
|
+
};
|
|
26014
|
+
if (firstAdded !== void 0 && secondAdded !== void 0) return {
|
|
26015
|
+
kind: "ambiguous",
|
|
26016
|
+
candidates: added.live
|
|
26017
|
+
};
|
|
26018
|
+
if (added.retired.length > 0) return { kind: "none" };
|
|
26019
|
+
const modified = modifiedLivePlans(entries).filter(isOpen);
|
|
26020
|
+
const [firstModified, secondModified] = modified;
|
|
26021
|
+
if (firstModified !== void 0 && secondModified === void 0) return {
|
|
26022
|
+
kind: "found",
|
|
26023
|
+
path: firstModified
|
|
26024
|
+
};
|
|
26025
|
+
if (firstModified !== void 0 && secondModified !== void 0) return {
|
|
26026
|
+
kind: "ambiguous",
|
|
26027
|
+
candidates: modified
|
|
26028
|
+
};
|
|
26029
|
+
return { kind: "none" };
|
|
26030
|
+
}
|
|
26031
|
+
//#endregion
|
|
24992
26032
|
//#region src/docs-check/rules.ts
|
|
24993
26033
|
function claimsPartialDelivery(body) {
|
|
24994
26034
|
const status = statusOf(body);
|
|
@@ -25033,7 +26073,7 @@ function checkStatus(input, status) {
|
|
|
25033
26073
|
file,
|
|
25034
26074
|
rule: "R8 missing-historical-banner",
|
|
25035
26075
|
message: "Retired doc has no historical banner saying it is not current state.",
|
|
25036
|
-
fix: "Run `bst docs:delink` on the retired directory -- it inserts the banner and de-links source paths in one pass. Do not hand-write it: the wording is fixed so it stays greppable."
|
|
26076
|
+
fix: "Run `pnpm exec bst docs:delink` on the retired directory -- it inserts the banner and de-links source paths in one pass. Do not hand-write it: the wording is fixed so it stays greppable."
|
|
25037
26077
|
});
|
|
25038
26078
|
return violations;
|
|
25039
26079
|
}
|
|
@@ -25046,7 +26086,7 @@ function checkLinks(input) {
|
|
|
25046
26086
|
file: input.file,
|
|
25047
26087
|
rule: "R7 retired-source-link",
|
|
25048
26088
|
message: `Retired doc links out of \`docs/\` (\`${link.raw}\` -> ${link.target}). A file move elsewhere breaks it.`,
|
|
25049
|
-
fix: "Run `bst docs:delink` on the retired directory -- it converts the link to a plain code span. A retired doc records what was decided; it should not track where the code lives now."
|
|
26089
|
+
fix: "Run `pnpm exec bst docs:delink` on the retired directory -- it converts the link to a plain code span. A retired doc records what was decided; it should not track where the code lives now."
|
|
25050
26090
|
});
|
|
25051
26091
|
continue;
|
|
25052
26092
|
}
|
|
@@ -25073,7 +26113,7 @@ function checkDoc(input) {
|
|
|
25073
26113
|
if (!input.isRetired && input.staleDays > 0 && input.ageDays !== void 0 && input.ageDays > input.staleDays) violations.push({
|
|
25074
26114
|
file: input.file,
|
|
25075
26115
|
rule: "R5 stale",
|
|
25076
|
-
message: `Active doc, but
|
|
26116
|
+
message: `Active doc, but its content hasn't changed in ${String(input.ageDays)} days (limit ${String(input.staleDays)}).`,
|
|
25077
26117
|
fix: "Check it against the tree. If the work shipped, retire it. If it's still open, edit the doc to say where it stands -- that resets the clock."
|
|
25078
26118
|
});
|
|
25079
26119
|
return violations;
|
|
@@ -25084,14 +26124,14 @@ const MS_PER_DAY$1 = 864e5;
|
|
|
25084
26124
|
const SPEC_DIR_BASENAME = "specs";
|
|
25085
26125
|
const PLAN_DIR_BASENAME = "plans";
|
|
25086
26126
|
function ageDays(exec, file, now) {
|
|
25087
|
-
const iso = exec.
|
|
26127
|
+
const iso = exec.lastContentChangeISO(file);
|
|
25088
26128
|
if (iso === void 0) return void 0;
|
|
25089
26129
|
const then = new Date(iso).getTime();
|
|
25090
26130
|
if (Number.isNaN(then)) return void 0;
|
|
25091
26131
|
return Math.floor((now.getTime() - then) / MS_PER_DAY$1);
|
|
25092
26132
|
}
|
|
25093
26133
|
function severityOf(violation, enforced, staleEnforced) {
|
|
25094
|
-
if (violation.rule.startsWith("R7")) return "advisory";
|
|
26134
|
+
if (violation.rule.startsWith("R7") || violation.rule.startsWith("R9")) return "advisory";
|
|
25095
26135
|
if (!enforced) return "advisory";
|
|
25096
26136
|
if (violation.rule.startsWith("R5") && !staleEnforced) return "advisory";
|
|
25097
26137
|
return "fatal";
|
|
@@ -25176,7 +26216,7 @@ function collectSpecRetirementViolations(exec, pairs, config, opts) {
|
|
|
25176
26216
|
file: spec,
|
|
25177
26217
|
rule: "R6 spec-not-retired",
|
|
25178
26218
|
message: `Implementing plan(s) have landed but this spec is still active: ${implementing.map((p) => p.file).join(", ")}.`,
|
|
25179
|
-
fix: "If those plans completed this spec, retire it too: `bst docs:retire <spec> --status <verb> --shipped \"<text>\"`. If further phases are still planned, leave it open -- that is why it stays active."
|
|
26219
|
+
fix: "If those plans completed this spec, retire it too: `pnpm exec bst docs:retire <spec> --status <verb> --shipped \"<text>\"`. If further phases are still planned, leave it open -- that is why it stays active."
|
|
25180
26220
|
};
|
|
25181
26221
|
violations.push({
|
|
25182
26222
|
...raw,
|
|
@@ -25186,6 +26226,45 @@ function collectSpecRetirementViolations(exec, pairs, config, opts) {
|
|
|
25186
26226
|
}
|
|
25187
26227
|
return violations;
|
|
25188
26228
|
}
|
|
26229
|
+
const R9_FIX = "If this branch works this plan, rename it with `git mv` to a name carrying `issue-<n>` or `pr-<n>`. If it only edits a plan another branch works, ignore this.";
|
|
26230
|
+
function collectPlanLocateViolations(exec) {
|
|
26231
|
+
const entries = exec.branchNameStatus();
|
|
26232
|
+
if (entries === void 0) return [];
|
|
26233
|
+
const touched = touchedLivePlans(entries);
|
|
26234
|
+
if (touched.length === 0) return [];
|
|
26235
|
+
const located = locatePlan({
|
|
26236
|
+
entries,
|
|
26237
|
+
trackedPaths: [],
|
|
26238
|
+
refs: null,
|
|
26239
|
+
isOpen: (file) => {
|
|
26240
|
+
const text = exec.readFile(file);
|
|
26241
|
+
return text !== void 0 && countOutstandingBoxes(text) > 0;
|
|
26242
|
+
}
|
|
26243
|
+
});
|
|
26244
|
+
const reports = [];
|
|
26245
|
+
if (located.kind === "none") reports.push(...touched.map((file) => ({
|
|
26246
|
+
file,
|
|
26247
|
+
message: `The execute lookup finds no plan for this branch: ${file} was touched but not added, its name carries no issue-/pr- marker, and either it has no outstanding task or the branch added a retired plan beside it, which the execute lookup works instead.`
|
|
26248
|
+
})));
|
|
26249
|
+
else if (located.kind === "ambiguous") reports.push(...located.candidates.map((file) => ({
|
|
26250
|
+
file,
|
|
26251
|
+
message: `The execute lookup cannot choose a plan for this branch: ${String(located.candidates.length)} live plans carry no issue-/pr- marker.`
|
|
26252
|
+
})));
|
|
26253
|
+
const violations = [];
|
|
26254
|
+
for (const report of reports) {
|
|
26255
|
+
const raw = {
|
|
26256
|
+
file: report.file,
|
|
26257
|
+
rule: "R9 plan-unlocatable",
|
|
26258
|
+
message: report.message,
|
|
26259
|
+
fix: R9_FIX
|
|
26260
|
+
};
|
|
26261
|
+
violations.push({
|
|
26262
|
+
...raw,
|
|
26263
|
+
severity: severityOf(raw, true, true)
|
|
26264
|
+
});
|
|
26265
|
+
}
|
|
26266
|
+
return violations;
|
|
26267
|
+
}
|
|
25189
26268
|
function collectViolations(exec, config, opts) {
|
|
25190
26269
|
const pairs = discoverPairs(exec, config);
|
|
25191
26270
|
const violations = [];
|
|
@@ -25198,6 +26277,7 @@ function collectViolations(exec, config, opts) {
|
|
|
25198
26277
|
retiredCount += result.retired;
|
|
25199
26278
|
}
|
|
25200
26279
|
violations.push(...collectSpecRetirementViolations(exec, pairs, config, opts));
|
|
26280
|
+
violations.push(...collectPlanLocateViolations(exec));
|
|
25201
26281
|
const activeDirs = matchableActiveDirs(exec, config);
|
|
25202
26282
|
const unmatchedEnforced = Array.isArray(config.enforced) ? config.enforced.filter((entry) => !activeDirs.includes(entry)) : [];
|
|
25203
26283
|
return {
|
|
@@ -25211,9 +26291,236 @@ function collectViolations(exec, config, opts) {
|
|
|
25211
26291
|
};
|
|
25212
26292
|
}
|
|
25213
26293
|
//#endregion
|
|
26294
|
+
//#region src/docs-check/content-age.ts
|
|
26295
|
+
const HEX_HASH = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/;
|
|
26296
|
+
function parseIgnoreRevs(text) {
|
|
26297
|
+
const hashes = /* @__PURE__ */ new Set();
|
|
26298
|
+
for (const rawLine of text.split("\n")) {
|
|
26299
|
+
const commentStart = rawLine.indexOf("#");
|
|
26300
|
+
const value = (commentStart === -1 ? rawLine : rawLine.slice(0, commentStart)).trim().toLowerCase();
|
|
26301
|
+
if (HEX_HASH.test(value)) hashes.add(value);
|
|
26302
|
+
}
|
|
26303
|
+
return hashes;
|
|
26304
|
+
}
|
|
26305
|
+
const FOLLOW_LOG_FORMAT = "%x1e%H%x1f%P%x1f%aI";
|
|
26306
|
+
function parseStatusLine(body) {
|
|
26307
|
+
for (const line of body.split("\n")) {
|
|
26308
|
+
if (line.trim() === "") continue;
|
|
26309
|
+
const fields = line.split(" ");
|
|
26310
|
+
const code = (fields[0] ?? "").charAt(0);
|
|
26311
|
+
const firstPath = fields[1] ?? "";
|
|
26312
|
+
if (code === "" || firstPath === "") return void 0;
|
|
26313
|
+
const secondPath = fields[2];
|
|
26314
|
+
if (code === "R" && secondPath !== void 0) return {
|
|
26315
|
+
code,
|
|
26316
|
+
oldPath: firstPath,
|
|
26317
|
+
newPath: secondPath
|
|
26318
|
+
};
|
|
26319
|
+
return {
|
|
26320
|
+
code,
|
|
26321
|
+
oldPath: firstPath,
|
|
26322
|
+
newPath: firstPath
|
|
26323
|
+
};
|
|
26324
|
+
}
|
|
26325
|
+
}
|
|
26326
|
+
function parseFollowLog(stdout) {
|
|
26327
|
+
const entries = [];
|
|
26328
|
+
for (const record of stdout.split("")) {
|
|
26329
|
+
if (record.trim() === "") continue;
|
|
26330
|
+
const lineEnd = record.indexOf("\n");
|
|
26331
|
+
const header = lineEnd === -1 ? record : record.slice(0, lineEnd);
|
|
26332
|
+
const body = lineEnd === -1 ? "" : record.slice(lineEnd + 1);
|
|
26333
|
+
const fields = header.split("");
|
|
26334
|
+
const commit = (fields[0] ?? "").trim();
|
|
26335
|
+
if (commit === "") continue;
|
|
26336
|
+
const parentsText = (fields[1] ?? "").trim();
|
|
26337
|
+
const parents = parentsText === "" ? [] : parentsText.split(/\s+/);
|
|
26338
|
+
const authorDate = (fields[2] ?? "").trim();
|
|
26339
|
+
entries.push({
|
|
26340
|
+
commit,
|
|
26341
|
+
parents,
|
|
26342
|
+
authorDate,
|
|
26343
|
+
status: parseStatusLine(body)
|
|
26344
|
+
});
|
|
26345
|
+
}
|
|
26346
|
+
return entries;
|
|
26347
|
+
}
|
|
26348
|
+
const FIRST_WINDOW = 4;
|
|
26349
|
+
const WINDOW_GROWTH = 8;
|
|
26350
|
+
function entryOutcome(git, context, entry) {
|
|
26351
|
+
if (entry.parents.length > 1) return {
|
|
26352
|
+
kind: "date",
|
|
26353
|
+
date: entry.authorDate
|
|
26354
|
+
};
|
|
26355
|
+
if (entry.parents.length === 0 || entry.status?.code === "A") {
|
|
26356
|
+
if (context.shallow.has(entry.commit)) return { kind: "unknown" };
|
|
26357
|
+
return {
|
|
26358
|
+
kind: "date",
|
|
26359
|
+
date: entry.authorDate
|
|
26360
|
+
};
|
|
26361
|
+
}
|
|
26362
|
+
if (context.ignore.has(entry.commit)) return { kind: "skip" };
|
|
26363
|
+
const status = entry.status;
|
|
26364
|
+
if (status !== void 0 && (status.code === "M" || status.code === "R")) {
|
|
26365
|
+
const diff = git.blobDiffStatus(entry.commit, status.oldPath, status.newPath);
|
|
26366
|
+
if (diff === 0) return { kind: "skip" };
|
|
26367
|
+
if (diff === 1) return {
|
|
26368
|
+
kind: "date",
|
|
26369
|
+
date: entry.authorDate
|
|
26370
|
+
};
|
|
26371
|
+
return { kind: "unknown" };
|
|
26372
|
+
}
|
|
26373
|
+
return {
|
|
26374
|
+
kind: "date",
|
|
26375
|
+
date: entry.authorDate
|
|
26376
|
+
};
|
|
26377
|
+
}
|
|
26378
|
+
function lastContentChange(git, rel, context) {
|
|
26379
|
+
let maxCount = FIRST_WINDOW;
|
|
26380
|
+
let walked = 0;
|
|
26381
|
+
for (;;) {
|
|
26382
|
+
const stdout = git.log(rel, maxCount);
|
|
26383
|
+
if (stdout === void 0) return void 0;
|
|
26384
|
+
const entries = parseFollowLog(stdout);
|
|
26385
|
+
for (const entry of entries.slice(walked)) {
|
|
26386
|
+
const outcome = entryOutcome(git, context, entry);
|
|
26387
|
+
if (outcome.kind === "date") return outcome.date;
|
|
26388
|
+
if (outcome.kind === "unknown") return void 0;
|
|
26389
|
+
}
|
|
26390
|
+
if (entries.length < maxCount) return void 0;
|
|
26391
|
+
walked = entries.length;
|
|
26392
|
+
maxCount *= WINDOW_GROWTH;
|
|
26393
|
+
}
|
|
26394
|
+
}
|
|
26395
|
+
//#endregion
|
|
25214
26396
|
//#region src/docs-check/executor.ts
|
|
26397
|
+
function gitOut$1(rootDir, args) {
|
|
26398
|
+
try {
|
|
26399
|
+
return execFileSync("git", args, {
|
|
26400
|
+
cwd: rootDir,
|
|
26401
|
+
encoding: "utf-8",
|
|
26402
|
+
stdio: [
|
|
26403
|
+
"ignore",
|
|
26404
|
+
"pipe",
|
|
26405
|
+
"ignore"
|
|
26406
|
+
]
|
|
26407
|
+
});
|
|
26408
|
+
} catch (_error) {
|
|
26409
|
+
return;
|
|
26410
|
+
}
|
|
26411
|
+
}
|
|
26412
|
+
function gitStatus(rootDir, args) {
|
|
26413
|
+
try {
|
|
26414
|
+
execFileSync("git", args, {
|
|
26415
|
+
cwd: rootDir,
|
|
26416
|
+
stdio: [
|
|
26417
|
+
"ignore",
|
|
26418
|
+
"ignore",
|
|
26419
|
+
"ignore"
|
|
26420
|
+
]
|
|
26421
|
+
});
|
|
26422
|
+
return 0;
|
|
26423
|
+
} catch (error) {
|
|
26424
|
+
if (error instanceof Error && "status" in error && typeof error.status === "number") return error.status;
|
|
26425
|
+
return;
|
|
26426
|
+
}
|
|
26427
|
+
}
|
|
26428
|
+
function readShallowCommits(rootDir) {
|
|
26429
|
+
if (gitOut$1(rootDir, ["rev-parse", "--is-shallow-repository"])?.trim() !== "true") return /* @__PURE__ */ new Set();
|
|
26430
|
+
const listed = gitOut$1(rootDir, [
|
|
26431
|
+
"rev-parse",
|
|
26432
|
+
"--git-path",
|
|
26433
|
+
"shallow"
|
|
26434
|
+
])?.trim();
|
|
26435
|
+
if (listed === void 0 || listed === "") return /* @__PURE__ */ new Set();
|
|
26436
|
+
const file = path.isAbsolute(listed) ? listed : path.resolve(rootDir, listed);
|
|
26437
|
+
let text;
|
|
26438
|
+
try {
|
|
26439
|
+
text = readFileSync(file, "utf-8");
|
|
26440
|
+
} catch (_error) {
|
|
26441
|
+
return /* @__PURE__ */ new Set();
|
|
26442
|
+
}
|
|
26443
|
+
const hashes = /* @__PURE__ */ new Set();
|
|
26444
|
+
for (const line of text.split("\n")) {
|
|
26445
|
+
const hash = line.trim();
|
|
26446
|
+
if (hash !== "") hashes.add(hash);
|
|
26447
|
+
}
|
|
26448
|
+
return hashes;
|
|
26449
|
+
}
|
|
26450
|
+
function buildContentAgeSource(rootDir) {
|
|
26451
|
+
const topLevel = gitOut$1(rootDir, ["rev-parse", "--show-toplevel"])?.trim();
|
|
26452
|
+
if (topLevel === void 0 || topLevel === "") return void 0;
|
|
26453
|
+
let ignoreText = "";
|
|
26454
|
+
try {
|
|
26455
|
+
ignoreText = readFileSync(path.join(topLevel, ".git-blame-ignore-revs"), "utf-8");
|
|
26456
|
+
} catch (_error) {}
|
|
26457
|
+
return {
|
|
26458
|
+
git: {
|
|
26459
|
+
log: (rel, maxCount) => gitOut$1(rootDir, [
|
|
26460
|
+
"-c",
|
|
26461
|
+
"core.quotePath=false",
|
|
26462
|
+
"log",
|
|
26463
|
+
"--follow",
|
|
26464
|
+
`--max-count=${String(maxCount)}`,
|
|
26465
|
+
`--format=${FOLLOW_LOG_FORMAT}`,
|
|
26466
|
+
"--name-status",
|
|
26467
|
+
"--",
|
|
26468
|
+
rel
|
|
26469
|
+
]),
|
|
26470
|
+
blobDiffStatus: (commit, oldPath, newPath) => gitStatus(rootDir, [
|
|
26471
|
+
"diff",
|
|
26472
|
+
"--quiet",
|
|
26473
|
+
"-w",
|
|
26474
|
+
"--ignore-blank-lines",
|
|
26475
|
+
`${commit}^:${oldPath}`,
|
|
26476
|
+
`${commit}:${newPath}`
|
|
26477
|
+
])
|
|
26478
|
+
},
|
|
26479
|
+
context: {
|
|
26480
|
+
ignore: parseIgnoreRevs(ignoreText),
|
|
26481
|
+
shallow: readShallowCommits(rootDir)
|
|
26482
|
+
}
|
|
26483
|
+
};
|
|
26484
|
+
}
|
|
26485
|
+
function resolveCommit$1(rootDir, ref) {
|
|
26486
|
+
const commit = gitOut$1(rootDir, [
|
|
26487
|
+
"rev-parse",
|
|
26488
|
+
"--verify",
|
|
26489
|
+
"-q",
|
|
26490
|
+
`${ref}^{commit}`
|
|
26491
|
+
])?.trim();
|
|
26492
|
+
return commit === void 0 || commit === "" ? void 0 : commit;
|
|
26493
|
+
}
|
|
26494
|
+
function branchNameStatus(rootDir) {
|
|
26495
|
+
let defaultBranch;
|
|
26496
|
+
try {
|
|
26497
|
+
defaultBranch = readConfiguredDefaultBranch(rootDir);
|
|
26498
|
+
} catch (_error) {
|
|
26499
|
+
return;
|
|
26500
|
+
}
|
|
26501
|
+
const ref = [`origin/${defaultBranch}`, defaultBranch].find((candidate) => resolveCommit$1(rootDir, candidate) !== void 0);
|
|
26502
|
+
if (ref === void 0) return void 0;
|
|
26503
|
+
const head = gitOut$1(rootDir, ["rev-parse", "HEAD"])?.trim();
|
|
26504
|
+
if (gitOut$1(rootDir, [
|
|
26505
|
+
"symbolic-ref",
|
|
26506
|
+
"-q",
|
|
26507
|
+
"--short",
|
|
26508
|
+
"HEAD"
|
|
26509
|
+
])?.trim() === defaultBranch) return void 0;
|
|
26510
|
+
if (head === void 0 || head === "") return void 0;
|
|
26511
|
+
if (head === resolveCommit$1(rootDir, ref)) return void 0;
|
|
26512
|
+
if (gitOut$1(rootDir, [
|
|
26513
|
+
"merge-base",
|
|
26514
|
+
ref,
|
|
26515
|
+
"HEAD"
|
|
26516
|
+
]) === void 0) return void 0;
|
|
26517
|
+
const stdout = gitOut$1(rootDir, planDiffArgs(ref));
|
|
26518
|
+
return stdout === void 0 ? void 0 : parseNameStatus(stdout);
|
|
26519
|
+
}
|
|
25215
26520
|
function createRealDocsExecutor(rootDir) {
|
|
25216
26521
|
const full = (rel) => path.join(rootDir, ...rel.split("/"));
|
|
26522
|
+
let contentAgeSource;
|
|
26523
|
+
let contentAgeRead = false;
|
|
25217
26524
|
return {
|
|
25218
26525
|
exists: (rel) => existsSync(full(rel)),
|
|
25219
26526
|
readFile(rel) {
|
|
@@ -25233,30 +26540,15 @@ function createRealDocsExecutor(rootDir) {
|
|
|
25233
26540
|
return;
|
|
25234
26541
|
}
|
|
25235
26542
|
},
|
|
25236
|
-
|
|
25237
|
-
|
|
25238
|
-
|
|
25239
|
-
|
|
25240
|
-
"log",
|
|
25241
|
-
"-1",
|
|
25242
|
-
"--format=%cI",
|
|
25243
|
-
"--",
|
|
25244
|
-
rel
|
|
25245
|
-
], {
|
|
25246
|
-
cwd: rootDir,
|
|
25247
|
-
encoding: "utf-8",
|
|
25248
|
-
stdio: [
|
|
25249
|
-
"ignore",
|
|
25250
|
-
"pipe",
|
|
25251
|
-
"ignore"
|
|
25252
|
-
]
|
|
25253
|
-
});
|
|
25254
|
-
} catch (_error) {
|
|
25255
|
-
return;
|
|
26543
|
+
lastContentChangeISO(rel) {
|
|
26544
|
+
if (!contentAgeRead) {
|
|
26545
|
+
contentAgeRead = true;
|
|
26546
|
+
contentAgeSource = buildContentAgeSource(rootDir);
|
|
25256
26547
|
}
|
|
25257
|
-
|
|
25258
|
-
return
|
|
25259
|
-
}
|
|
26548
|
+
if (contentAgeSource === void 0) return void 0;
|
|
26549
|
+
return lastContentChange(contentAgeSource.git, rel, contentAgeSource.context);
|
|
26550
|
+
},
|
|
26551
|
+
branchNameStatus: () => branchNameStatus(rootDir)
|
|
25260
26552
|
};
|
|
25261
26553
|
}
|
|
25262
26554
|
//#endregion
|
|
@@ -25304,7 +26596,8 @@ function runDocsCheck(exec, config, opts) {
|
|
|
25304
26596
|
return {
|
|
25305
26597
|
payload: { enabled: false },
|
|
25306
26598
|
text: "",
|
|
25307
|
-
exitCode: 0
|
|
26599
|
+
exitCode: 0,
|
|
26600
|
+
warnCount: 0
|
|
25308
26601
|
};
|
|
25309
26602
|
}
|
|
25310
26603
|
const result = collectViolations(exec, config, {
|
|
@@ -25312,21 +26605,24 @@ function runDocsCheck(exec, config, opts) {
|
|
|
25312
26605
|
strict: opts.strict
|
|
25313
26606
|
});
|
|
25314
26607
|
debug(vc, `docs: ${String(result.pairCount)} pair(s), ${String(result.violations.length)} violation(s)`);
|
|
26608
|
+
const warnCount = result.violations.filter((v) => v.rule.startsWith("R9")).length;
|
|
25315
26609
|
if (result.pairCount === 0) return {
|
|
25316
26610
|
payload: result,
|
|
25317
26611
|
text: "",
|
|
25318
|
-
exitCode: 0
|
|
26612
|
+
exitCode: 0,
|
|
26613
|
+
warnCount
|
|
25319
26614
|
};
|
|
25320
26615
|
return {
|
|
25321
26616
|
payload: result,
|
|
25322
26617
|
text: formatReport(result),
|
|
25323
|
-
exitCode: result.pass ? 0 : 1
|
|
26618
|
+
exitCode: result.pass ? 0 : 1,
|
|
26619
|
+
warnCount
|
|
25324
26620
|
};
|
|
25325
26621
|
}
|
|
25326
26622
|
const docsCheckCommand = defineCommand({
|
|
25327
26623
|
meta: {
|
|
25328
26624
|
name: "docs:check",
|
|
25329
|
-
description: "Enforce the plan/spec lifecycle: a doc's directory is the source of truth for whether it is done. Checks that statuses agree with directories, retirements are dated and say where the truth went, relative links resolve, and active docs aren't stale. Findings are advisory (exit 0) unless the directory is in .tooling.json#docs.enforced (a list, or true for all) or --strict is passed; staleness stays advisory unless .tooling.json#docs.staleEnforced is set, and a link out of docs/ in a retired doc is always advisory. A retired doc should also carry the historical banner and have no links out of docs/; bst docs:delink fixes both. It CANNOT tell whether the code a plan describes actually shipped -- it verifies that a doc's metadata is self-consistent, not that it is true. Deciding a plan is done still means reading the source it names."
|
|
26625
|
+
description: "Enforce the plan/spec lifecycle: a doc's directory is the source of truth for whether it is done. Checks that statuses agree with directories, retirements are dated and say where the truth went, relative links resolve, and active docs aren't stale. On a branch, it also warns (rule R9) when the branch works a plan that the execute lookup cannot find; that finding is always advisory. Findings are advisory (exit 0) unless the directory is in .tooling.json#docs.enforced (a list, or true for all) or --strict is passed; staleness stays advisory unless .tooling.json#docs.staleEnforced is set, and a link out of docs/ in a retired doc is always advisory. A retired doc should also carry the historical banner and have no links out of docs/; pnpm exec bst docs:delink fixes both. It CANNOT tell whether the code a plan describes actually shipped -- it verifies that a doc's metadata is self-consistent, not that it is true. Deciding a plan is done still means reading the source it names."
|
|
25330
26626
|
},
|
|
25331
26627
|
args: {
|
|
25332
26628
|
dir: {
|
|
@@ -25337,7 +26633,7 @@ const docsCheckCommand = defineCommand({
|
|
|
25337
26633
|
},
|
|
25338
26634
|
strict: {
|
|
25339
26635
|
type: "boolean",
|
|
25340
|
-
description: "Treat every discovered pair as enforced, ignoring .tooling.json#docs.enforced (a retired doc's link out of docs/, rule R7,
|
|
26636
|
+
description: "Treat every discovered pair as enforced, ignoring .tooling.json#docs.enforced (a retired doc's link out of docs/, rule R7, and an unlocatable plan, rule R9, stay advisory)"
|
|
25341
26637
|
},
|
|
25342
26638
|
json: {
|
|
25343
26639
|
type: "boolean",
|
|
@@ -25351,13 +26647,14 @@ const docsCheckCommand = defineCommand({
|
|
|
25351
26647
|
run({ args }) {
|
|
25352
26648
|
const targetDir = path.resolve(args.dir ?? ".");
|
|
25353
26649
|
const out = createCliOutput(args.json === true);
|
|
25354
|
-
const { payload, text, exitCode } = runDocsCheck(createRealDocsExecutor(targetDir), resolveDocsConfig(targetDir), {
|
|
26650
|
+
const { payload, text, exitCode, warnCount } = runDocsCheck(createRealDocsExecutor(targetDir), resolveDocsConfig(targetDir), {
|
|
25355
26651
|
now: /* @__PURE__ */ new Date(),
|
|
25356
26652
|
strict: args.strict === true,
|
|
25357
26653
|
verbose: args.verbose === true || isEnvVerbose()
|
|
25358
26654
|
});
|
|
25359
26655
|
out.json(payload);
|
|
25360
26656
|
if (text !== "") out.text(text);
|
|
26657
|
+
if (warnCount > 0) out.text(formatWarnMarker(warnCount));
|
|
25361
26658
|
process.exitCode = exitCode;
|
|
25362
26659
|
}
|
|
25363
26660
|
});
|
|
@@ -25618,7 +26915,7 @@ function runDocsDelink(exec, dirs, opts) {
|
|
|
25618
26915
|
const docsDelinkCommand = defineCommand({
|
|
25619
26916
|
meta: {
|
|
25620
26917
|
name: "docs:delink",
|
|
25621
|
-
description: "Make retired docs inert: convert every markdown link that resolves outside docs/ into a plain code span, and insert the historical banner. Idempotent. It accepts only a retired/ (or legacy completed/) directory. bst docs:delink <retired-dir>..."
|
|
26918
|
+
description: "Make retired docs inert: convert every markdown link that resolves outside docs/ into a plain code span, and insert the historical banner. Idempotent. It accepts only a retired/ (or legacy completed/) directory. pnpm exec bst docs:delink <retired-dir>..."
|
|
25622
26919
|
},
|
|
25623
26920
|
args: {
|
|
25624
26921
|
"dry-run": {
|
|
@@ -25820,7 +27117,7 @@ function runDocsRelocate(exec, sources, dest, opts) {
|
|
|
25820
27117
|
const docsRelocateCommand = defineCommand({
|
|
25821
27118
|
meta: {
|
|
25822
27119
|
name: "docs:relocate",
|
|
25823
|
-
description: "Move one or more .md files and rewrite the markdown links that break as a result -- both the moved files' own relative links and every inbound link elsewhere in the repo that pointed at them. Idempotent. Argument grammar mirrors POSIX mv: bst docs:relocate <src>... <dest>."
|
|
27120
|
+
description: "Move one or more .md files and rewrite the markdown links that break as a result -- both the moved files' own relative links and every inbound link elsewhere in the repo that pointed at them. Idempotent. Argument grammar mirrors POSIX mv: pnpm exec bst docs:relocate <src>... <dest>."
|
|
25824
27121
|
},
|
|
25825
27122
|
args: {
|
|
25826
27123
|
"dry-run": {
|
|
@@ -25839,9 +27136,9 @@ const docsRelocateCommand = defineCommand({
|
|
|
25839
27136
|
run({ args }) {
|
|
25840
27137
|
const out = createCliOutput(args.json === true);
|
|
25841
27138
|
const positionals = args._;
|
|
25842
|
-
if (positionals.length < 2) throw new FatalError("Usage: bst docs:relocate <src>... <dest>");
|
|
27139
|
+
if (positionals.length < 2) throw new FatalError("Usage: pnpm exec bst docs:relocate <src>... <dest>");
|
|
25843
27140
|
const dest = positionals.at(-1);
|
|
25844
|
-
if (dest === void 0) throw new FatalError("Usage: bst docs:relocate <src>... <dest>");
|
|
27141
|
+
if (dest === void 0) throw new FatalError("Usage: pnpm exec bst docs:relocate <src>... <dest>");
|
|
25845
27142
|
const sources = positionals.slice(0, -1);
|
|
25846
27143
|
const { payload, text, exitCode } = runDocsRelocate(createRealRelocateExecutor(process.cwd()), sources, dest, {
|
|
25847
27144
|
dryRun: args["dry-run"] === true,
|
|
@@ -26030,7 +27327,7 @@ function runDocsRetire(exec, src, opts) {
|
|
|
26030
27327
|
const docsRetireCommand = defineCommand({
|
|
26031
27328
|
meta: {
|
|
26032
27329
|
name: "docs:retire",
|
|
26033
|
-
description: "Retire a plan/spec doc: move it to its retired/ location (computed by convention, or --dest), rewrite the markdown links that break as a result, set a dated terminal Status: line, and ensure the retire paragraph. After the lifecycle edits it also de-links links out of docs/ and adds the historical banner, as docs:delink does. bst docs:retire <plan.md> --status <verb> --shipped <text>."
|
|
27330
|
+
description: "Retire a plan/spec doc: move it to its retired/ location (computed by convention, or --dest), rewrite the markdown links that break as a result, set a dated terminal Status: line, and ensure the retire paragraph. After the lifecycle edits it also de-links links out of docs/ and adds the historical banner, as docs:delink does. pnpm exec bst docs:retire <plan.md> --status <verb> --shipped <text>."
|
|
26034
27331
|
},
|
|
26035
27332
|
args: {
|
|
26036
27333
|
status: {
|
|
@@ -26066,7 +27363,7 @@ const docsRetireCommand = defineCommand({
|
|
|
26066
27363
|
run({ args }) {
|
|
26067
27364
|
const out = createCliOutput(args.json === true);
|
|
26068
27365
|
const [src] = args._;
|
|
26069
|
-
if (src === void 0) throw new FatalError("Usage: bst docs:retire <plan.md> --status <verb> --shipped <text>");
|
|
27366
|
+
if (src === void 0) throw new FatalError("Usage: pnpm exec bst docs:retire <plan.md> --status <verb> --shipped <text>");
|
|
26070
27367
|
const { payload, text, exitCode } = runDocsRetire(createRealRelocateExecutor(process.cwd()), src, {
|
|
26071
27368
|
status: args.status,
|
|
26072
27369
|
date: args.date,
|
|
@@ -26134,7 +27431,7 @@ async function runIssueView(executor, options) {
|
|
|
26134
27431
|
const resolve = resolverFor(executor, options.cwd, () => {
|
|
26135
27432
|
debug(options, `issue:view #${String(parseIssueRef(options.ref).number)}`);
|
|
26136
27433
|
});
|
|
26137
|
-
const view = await withNumberExample("bst issue:view 273", () => viewIssue(resolve, {
|
|
27434
|
+
const view = await withNumberExample("pnpm exec bst issue:view 273", () => viewIssue(resolve, {
|
|
26138
27435
|
ref: options.ref,
|
|
26139
27436
|
...options.repo === void 0 ? {} : { repo: options.repo }
|
|
26140
27437
|
}));
|
|
@@ -26164,7 +27461,7 @@ async function runIssueComment(executor, options) {
|
|
|
26164
27461
|
const resolve = resolverFor(executor, options.cwd, () => {
|
|
26165
27462
|
debug(options, `issue:comment #${String(parseIssueRef(options.ref).number)}`);
|
|
26166
27463
|
});
|
|
26167
|
-
return withNumberExample("bst issue:comment 273", () => commentOnIssue(resolve, {
|
|
27464
|
+
return withNumberExample("pnpm exec bst issue:comment 273", () => commentOnIssue(resolve, {
|
|
26168
27465
|
ref: options.ref,
|
|
26169
27466
|
body: options.body,
|
|
26170
27467
|
dryRun: options.dryRun,
|
|
@@ -26177,7 +27474,7 @@ async function runIssueUpdate(executor, options) {
|
|
|
26177
27474
|
debug(options, `issue:update #${String(parseIssueRef(options.ref).number)} [${changed.join(", ")}]`);
|
|
26178
27475
|
});
|
|
26179
27476
|
try {
|
|
26180
|
-
return await withNumberExample("bst issue:update 273 --title '...'", () => updateIssue(resolve, {
|
|
27477
|
+
return await withNumberExample("pnpm exec bst issue:update 273 --title '...'", () => updateIssue(resolve, {
|
|
26181
27478
|
ref: options.ref,
|
|
26182
27479
|
dryRun: options.dryRun,
|
|
26183
27480
|
...options.title === void 0 ? {} : { title: options.title },
|
|
@@ -26624,7 +27921,7 @@ async function runIssueDownload(executor, options) {
|
|
|
26624
27921
|
...options.repo === void 0 ? {} : { repo: options.repo }
|
|
26625
27922
|
});
|
|
26626
27923
|
} catch (error) {
|
|
26627
|
-
if (error instanceof IssueNumberRequiredError) throw new FatalError(describeIssueNumberRequired("bst issue:download 273"));
|
|
27924
|
+
if (error instanceof IssueNumberRequiredError) throw new FatalError(describeIssueNumberRequired("pnpm exec bst issue:download 273"));
|
|
26628
27925
|
if (error instanceof AttachmentExistsError) throw new FatalError(describeAttachmentExists(error, "--force"));
|
|
26629
27926
|
throw error;
|
|
26630
27927
|
}
|
|
@@ -26775,7 +28072,10 @@ async function runIssueLabel(executor, options) {
|
|
|
26775
28072
|
...options.repo === void 0 ? {} : { repo: options.repo }
|
|
26776
28073
|
});
|
|
26777
28074
|
} catch (error) {
|
|
26778
|
-
if (error instanceof IssueNumberRequiredError)
|
|
28075
|
+
if (error instanceof IssueNumberRequiredError) {
|
|
28076
|
+
const command = options.action === "add" ? "issue:label" : "issue:unlabel";
|
|
28077
|
+
throw new FatalError(describeIssueNumberRequired(`pnpm exec bst ${command} 273 agent/implement`));
|
|
28078
|
+
}
|
|
26779
28079
|
throw error;
|
|
26780
28080
|
}
|
|
26781
28081
|
}
|
|
@@ -26855,7 +28155,7 @@ async function runIssueSetLabels(executor, options) {
|
|
|
26855
28155
|
...options.repo === void 0 ? {} : { repo: options.repo }
|
|
26856
28156
|
});
|
|
26857
28157
|
} catch (error) {
|
|
26858
|
-
if (error instanceof IssueNumberRequiredError) throw new FatalError(describeIssueNumberRequired("bst issue:set-labels 273 agent/plan"));
|
|
28158
|
+
if (error instanceof IssueNumberRequiredError) throw new FatalError(describeIssueNumberRequired("pnpm exec bst issue:set-labels 273 agent/plan"));
|
|
26859
28159
|
if (error instanceof NoLabelsGivenError) throw new FatalError(describeNoLabelsGiven(error, "--clear"));
|
|
26860
28160
|
throw error;
|
|
26861
28161
|
}
|
|
@@ -27030,7 +28330,7 @@ async function runIssueState(executor, options) {
|
|
|
27030
28330
|
...options.repo === void 0 ? {} : { repo: options.repo }
|
|
27031
28331
|
});
|
|
27032
28332
|
} catch (error) {
|
|
27033
|
-
if (error instanceof IssueNumberRequiredError) throw new FatalError(describeIssueNumberRequired(`bst issue:${options.action} 273`));
|
|
28333
|
+
if (error instanceof IssueNumberRequiredError) throw new FatalError(describeIssueNumberRequired(`pnpm exec bst issue:${options.action} 273`));
|
|
27034
28334
|
if (error instanceof EmptyCommentError) throw new FatalError(describeEmptyComment({
|
|
27035
28335
|
text: "--comment",
|
|
27036
28336
|
file: "--comment-file"
|
|
@@ -27176,7 +28476,7 @@ function resolveBody(bodyFile) {
|
|
|
27176
28476
|
const findingsAddCommand = defineCommand({
|
|
27177
28477
|
meta: {
|
|
27178
28478
|
name: "findings:add",
|
|
27179
|
-
description: "Author one finding into .agent/findings/, correct-by-construction. Usage: bst findings:add --class <nit|pre-existing> --source <pr|issue>#<n> --title <t>. The first finding creates .agent/findings/, which turns on the agent's backlog run for the repo; set .tooling.json#agent.backlog to false to stop it."
|
|
28479
|
+
description: "Author one finding into .agent/findings/, correct-by-construction. Usage: pnpm exec bst findings:add --class <nit|pre-existing> --source <pr|issue>#<n> --title <t>. The first finding creates .agent/findings/, which turns on the agent's backlog run for the repo; set .tooling.json#agent.backlog to false to stop it."
|
|
27180
28480
|
},
|
|
27181
28481
|
args: {
|
|
27182
28482
|
class: {
|
|
@@ -27360,7 +28660,7 @@ function runFindingsResolve(rootDir, names, opts) {
|
|
|
27360
28660
|
const findingsResolveCommand = defineCommand({
|
|
27361
28661
|
meta: {
|
|
27362
28662
|
name: "findings:resolve",
|
|
27363
|
-
description: "Delete finding file(s) from .agent/findings/, or --defer to append a note instead. bst findings:resolve <name>... [--defer <note>]."
|
|
28663
|
+
description: "Delete finding file(s) from .agent/findings/, or --defer to append a note instead. pnpm exec bst findings:resolve <name>... [--defer <note>]."
|
|
27364
28664
|
},
|
|
27365
28665
|
args: {
|
|
27366
28666
|
defer: {
|
|
@@ -27891,7 +29191,7 @@ async function runCli(config) {
|
|
|
27891
29191
|
//#region src/cli-main.ts
|
|
27892
29192
|
setLogLevel(resolveLogLevel({}));
|
|
27893
29193
|
setForgejoLogger(log$1);
|
|
27894
|
-
const displayVersion = "0.
|
|
29194
|
+
const displayVersion = "0.95.0";
|
|
27895
29195
|
const bstSubCommands = {
|
|
27896
29196
|
"repo:sync": syncCommand,
|
|
27897
29197
|
"checks:run": runChecksCommand,
|
|
@@ -27921,6 +29221,7 @@ const bstSubCommands = {
|
|
|
27921
29221
|
"coverage:record": coverageRecordCommand,
|
|
27922
29222
|
"coverage:status": coverageStatusCommand,
|
|
27923
29223
|
"health:check": healthCheckCommand,
|
|
29224
|
+
"health:raise": healthRaiseCommand,
|
|
27924
29225
|
"health:summary": healthSummaryCommand,
|
|
27925
29226
|
"health:record": healthRecordCommand,
|
|
27926
29227
|
"health:status": healthStatusCommand,
|