@bensandee/tooling 0.94.0 → 0.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.mjs +1458 -288
- package/package.json +1 -1
- package/tooling.schema.json +79 -6
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.96.0"
|
|
179
179
|
});
|
|
180
180
|
//#endregion
|
|
181
181
|
//#region ../cli-kit/src/logger.ts
|
|
@@ -690,7 +690,10 @@ const HealthRecordSchema = HealthMetricsSchema.extend(HealthObservationsSchema.s
|
|
|
690
690
|
});
|
|
691
691
|
const RATCHETED_METRIC_KEY_SET = new Set(RATCHETED_METRIC_KEYS);
|
|
692
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())));
|
|
693
|
-
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
|
+
});
|
|
694
697
|
function parseHealthBaseline(raw) {
|
|
695
698
|
let json;
|
|
696
699
|
try {
|
|
@@ -759,6 +762,18 @@ function formatIssue$1(issue) {
|
|
|
759
762
|
}
|
|
760
763
|
return ` ${loc}: ${issue.message}`;
|
|
761
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
|
+
]);
|
|
762
777
|
const HealthConfigSchema = z.strictObject({
|
|
763
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`." }),
|
|
764
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." }),
|
|
@@ -768,11 +783,12 @@ const HealthConfigSchema = z.strictObject({
|
|
|
768
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." }),
|
|
769
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." }),
|
|
770
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." }),
|
|
771
|
-
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." }),
|
|
772
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." }),
|
|
773
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." }),
|
|
774
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." }),
|
|
775
|
-
|
|
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." })
|
|
776
792
|
});
|
|
777
793
|
const CharsConfigSchema = z.strictObject({
|
|
778
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." }),
|
|
@@ -877,6 +893,9 @@ const JS_EXTENSIONS$1 = /* @__PURE__ */ new Set([
|
|
|
877
893
|
".mjs",
|
|
878
894
|
".cjs"
|
|
879
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
|
+
}
|
|
880
899
|
function isRootConfigFile(relPath) {
|
|
881
900
|
if (relPath.includes(path.sep)) return false;
|
|
882
901
|
return /\.config\.[cm]?[jt]sx?$/.test(relPath) || relPath.startsWith(".");
|
|
@@ -1587,12 +1606,15 @@ function createDryRunContext(config, dockerDiscovery) {
|
|
|
1587
1606
|
};
|
|
1588
1607
|
}
|
|
1589
1608
|
//#endregion
|
|
1609
|
+
//#region src/repo-full-name.ts
|
|
1610
|
+
const REPO_FULL_NAME = /^(?!\.\.?\/)[\w.-]+\/(?!\.\.?$)[\w.-]+$/u;
|
|
1611
|
+
//#endregion
|
|
1590
1612
|
//#region src/docs-check/config.ts
|
|
1591
1613
|
const DEFAULT_STALE_DAYS = 30;
|
|
1592
1614
|
const DocsConfigSchema = z.strictObject({
|
|
1593
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." }),
|
|
1594
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." }),
|
|
1595
|
-
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)." }),
|
|
1596
1618
|
staleEnforced: z.boolean().optional().meta({ description: "Make the staleness rule fatal inside an enforced pair (default false: staleness is always advisory)." })
|
|
1597
1619
|
});
|
|
1598
1620
|
const DEFAULT_DOCS_CONFIG = {
|
|
@@ -1724,8 +1746,9 @@ const LintSweepConfigSchema = z.strictObject({
|
|
|
1724
1746
|
});
|
|
1725
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." }) });
|
|
1726
1748
|
const AgentConfigSchema = z.strictObject({
|
|
1727
|
-
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." }),
|
|
1728
|
-
autonomy: z.boolean().optional().meta({ description: "
|
|
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." }),
|
|
1750
|
+
autonomy: z.boolean().optional().meta({ description: "Retired and ignored -- it armed neither the advance rung nor the sweeps it once did. Advance is now the unit's own `agent-flags/advance` or `agent-flags/autopilot` label, and the trunk-red detector and world-changed retrigger sweep read `agent.trunkRed`. The key is still accepted so an existing config keeps syncing; the agent warns once per launch for a repo that still sets it." }),
|
|
1751
|
+
trunkRed: z.boolean().optional().meta({ description: "Whether this repo's trunk-red detector and world-changed retrigger sweep run (default: on). The detector files a tracking issue when the default branch's combined status settles red and labels it agent/spec, and closes that issue with a comment if the branch recovers on its own before a human acts on it. The retrigger sweep re-queues a pull request labelled agent-flags/autopilot parked at a -stuck or -failed terminal when the fact its park recorded stops being true (the default branch went green, or moved past a conflicted base). A missing key means on; false disarms both." }),
|
|
1729
1752
|
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." }),
|
|
1730
1753
|
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." }),
|
|
1731
1754
|
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." }),
|
|
@@ -1737,13 +1760,15 @@ const AgentConfigSchema = z.strictObject({
|
|
|
1737
1760
|
])).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." }),
|
|
1738
1761
|
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." }),
|
|
1739
1762
|
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." }),
|
|
1740
|
-
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
|
+
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." }),
|
|
1764
|
+
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." })
|
|
1741
1765
|
});
|
|
1742
1766
|
const TolerantAgentConfigSchema = z.object({
|
|
1743
1767
|
...AgentConfigSchema.shape,
|
|
1744
1768
|
lintSweep: tolerantBlock(LintSweepConfigSchema).optional(),
|
|
1745
1769
|
findingHolds: z.unknown().optional(),
|
|
1746
|
-
egress: z.unknown().optional()
|
|
1770
|
+
egress: z.unknown().optional(),
|
|
1771
|
+
chatIssueRepos: z.unknown().optional()
|
|
1747
1772
|
});
|
|
1748
1773
|
const BranchPruneConfigSchema = z.strictObject({
|
|
1749
1774
|
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." }),
|
|
@@ -1805,7 +1830,7 @@ const ToolingConfigSchema = z.strictObject({
|
|
|
1805
1830
|
]).optional().meta({ description: "What compiles the project. Detected from the package.json build script, then config-file presence. Setting this key pins its value, even one that equals detection, and repo:sync keeps it until the key is deleted." }),
|
|
1806
1831
|
checks: ChecksConfigSchema.optional().meta({ description: "Check-pipeline overrides: extra steps to add, and steps to skip (unioned with CI defaults)." }),
|
|
1807
1832
|
lint: LintConfigSchema.optional().meta({ description: "bst lint:types (the type-aware oxlint gate) overrides. See lint.strictTypeAware." }),
|
|
1808
|
-
agent: AgentConfigSchema.optional().meta({ description: "agent:start behaviour overrides. Opt-in:
|
|
1833
|
+
agent: AgentConfigSchema.optional().meta({ description: "agent:start behaviour overrides. Opt-in: agent.enabled is the consent gate -- every other key in the block does nothing without it." }),
|
|
1809
1834
|
branchPrune: BranchPruneConfigSchema.optional().meta({ description: "bst branch:prune overrides: mergedDays/backupDays windows, and extra never-touch exclusions layered on top of the hard-coded list." }),
|
|
1810
1835
|
issues: IssuesConfigSchema.optional().meta({ description: "Repo-level issue policy -- currently just the default reviewer(s) `bst issue:create` assigns. The natural home for any future issue-policy key." })
|
|
1811
1836
|
});
|
|
@@ -1932,7 +1957,7 @@ function saveToolingConfig(ctx, config) {
|
|
|
1932
1957
|
if (key === "chars" || key === "ascii") {
|
|
1933
1958
|
const pruned = pruneCharsDefaults(value);
|
|
1934
1959
|
if (pruned === void 0) continue;
|
|
1935
|
-
result[
|
|
1960
|
+
result["chars"] = pruned;
|
|
1936
1961
|
continue;
|
|
1937
1962
|
}
|
|
1938
1963
|
result[key] = value;
|
|
@@ -2589,6 +2614,13 @@ function resolveComposeFileSet(readFile, projectRoot, composeFiles) {
|
|
|
2589
2614
|
//#endregion
|
|
2590
2615
|
//#region ../forgejo/src/fetcher.ts
|
|
2591
2616
|
const LONG_FETCH_TIMEOUT_MS = 6e5;
|
|
2617
|
+
function morePagesFollow(res, pageLength, received, pageSize) {
|
|
2618
|
+
if (pageLength === 0) return false;
|
|
2619
|
+
const raw = res.headers.get("x-total-count")?.trim() ?? "";
|
|
2620
|
+
const total = raw === "" ? NaN : Number(raw);
|
|
2621
|
+
if (!Number.isSafeInteger(total) || total < 0) return pageLength >= pageSize;
|
|
2622
|
+
return received < total;
|
|
2623
|
+
}
|
|
2592
2624
|
//#endregion
|
|
2593
2625
|
//#region ../forgejo/src/fetch-retry.ts
|
|
2594
2626
|
const DEFAULT_RETRY_POLICY = {
|
|
@@ -4777,7 +4809,7 @@ async function generateDeployCi(ctx) {
|
|
|
4777
4809
|
action: "skipped",
|
|
4778
4810
|
description: "Publish workflow not applicable"
|
|
4779
4811
|
};
|
|
4780
|
-
return retireManagedWorkflow(ctx, ctx.config.ci === "github" ? ".github/workflows/publish.yml" : ".forgejo/workflows/publish.yml", "run `bst docker:publish` to publish by hand") ?? {
|
|
4812
|
+
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") ?? {
|
|
4781
4813
|
filePath,
|
|
4782
4814
|
action: "skipped",
|
|
4783
4815
|
description: "No publish workflow to retire"
|
|
@@ -4822,7 +4854,8 @@ function resolveFromOverrides(overrides, isMonorepo, stack) {
|
|
|
4822
4854
|
historyBranch: o.historyBranch ?? DEFAULT_HEALTH_HISTORY_BRANCH,
|
|
4823
4855
|
tolerance: o.tolerance ?? 0,
|
|
4824
4856
|
graphMetricsEnabled: stack !== "none",
|
|
4825
|
-
exempt: o.exempt ?? {}
|
|
4857
|
+
exempt: o.exempt ?? {},
|
|
4858
|
+
sizeBudgets: o.sizeBudgets ?? {}
|
|
4826
4859
|
};
|
|
4827
4860
|
}
|
|
4828
4861
|
function resolveHealthConfigFromContext(reader, opts) {
|
|
@@ -5499,9 +5532,9 @@ function buildManagedDevDeps(ctx) {
|
|
|
5499
5532
|
const devDeps = { ...rootDevDeps(ctx.config) };
|
|
5500
5533
|
if (!isMonorepo) Object.assign(devDeps, perPackageDevDeps(ctx.config));
|
|
5501
5534
|
if (ctx.config.stack !== "none") devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.15.0";
|
|
5502
|
-
if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.
|
|
5535
|
+
if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.96.0";
|
|
5503
5536
|
if (ctx.config.useEslintPlugin && ctx.config.stack !== "none") devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.17.0";
|
|
5504
|
-
if (wantsAgentCli(ctx)) devDeps[AGENT_CLI_PACKAGE] = "0.1.
|
|
5537
|
+
if (wantsAgentCli(ctx)) devDeps[AGENT_CLI_PACKAGE] = "0.1.3";
|
|
5505
5538
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
5506
5539
|
"@changesets/cli": "3.0.3",
|
|
5507
5540
|
"@release-it/bumper": "8.0.1",
|
|
@@ -5667,14 +5700,16 @@ function describeInclude(include) {
|
|
|
5667
5700
|
return `added include: ${formatInclude(include)}`;
|
|
5668
5701
|
}
|
|
5669
5702
|
function warnIfNarrowInclude(ctx, filePath, include) {
|
|
5670
|
-
if (!Array.isArray(include) || include.length !==
|
|
5703
|
+
if (!Array.isArray(include) || include.length !== LEGACY_INCLUDE.length || !include.every((entry, index) => entry === LEGACY_INCLUDE[index])) return;
|
|
5671
5704
|
const testDir = path.join(path.dirname(filePath), "test");
|
|
5672
5705
|
if (!ctx.exists(testDir)) return;
|
|
5673
5706
|
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)}.`);
|
|
5674
5707
|
}
|
|
5675
|
-
const
|
|
5708
|
+
const NODE_PRESET = "@bensandee/config/tsconfig/node";
|
|
5676
5709
|
function extendsNodePreset(extendsValue) {
|
|
5677
|
-
|
|
5710
|
+
if (extendsValue === void 0) return false;
|
|
5711
|
+
if (Array.isArray(extendsValue)) return extendsValue.includes(NODE_PRESET);
|
|
5712
|
+
return extendsValue === NODE_PRESET;
|
|
5678
5713
|
}
|
|
5679
5714
|
const VERBATIM_MODULE_SYNTAX_KEY = "verbatimModuleSyntax";
|
|
5680
5715
|
function buildGeneratedTsconfig(ctx, extendsValue, options = {}) {
|
|
@@ -5775,7 +5810,7 @@ function mergeSingleTsconfig(ctx, filePath, extendsValue) {
|
|
|
5775
5810
|
parsed.extends = extendsValue;
|
|
5776
5811
|
changes.push(`added extends: ${extendsValue}`);
|
|
5777
5812
|
}
|
|
5778
|
-
if (extendsNodePreset(
|
|
5813
|
+
if (extendsNodePreset(parsed.extends) && parsed.compilerOptions?.[VERBATIM_MODULE_SYNTAX_KEY] === void 0) {
|
|
5779
5814
|
parsed.compilerOptions = {
|
|
5780
5815
|
...parsed.compilerOptions,
|
|
5781
5816
|
[VERBATIM_MODULE_SYNTAX_KEY]: true
|
|
@@ -6254,7 +6289,7 @@ function warnIfMissingJsonSummary(ctx) {
|
|
|
6254
6289
|
if (content === void 0) continue;
|
|
6255
6290
|
const issue = findJsonSummaryIssue(content);
|
|
6256
6291
|
if (issue === void 0) return;
|
|
6257
|
-
log$1.warn(`${rel}: ${describeIssue(issue)} -- coverage tracking (bst coverage:summary / coverage:check) requires the "json-summary" reporter to read coverage/coverage-summary.json.`);
|
|
6292
|
+
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.`);
|
|
6258
6293
|
return;
|
|
6259
6294
|
}
|
|
6260
6295
|
}
|
|
@@ -6280,7 +6315,7 @@ function warnIfMissingInclude(ctx) {
|
|
|
6280
6315
|
const block = extractCoverageBlock(content);
|
|
6281
6316
|
if (block === void 0) return;
|
|
6282
6317
|
if (/\binclude\s*:/.test(block)) return;
|
|
6283
|
-
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)}.`);
|
|
6318
|
+
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)}.`);
|
|
6284
6319
|
return;
|
|
6285
6320
|
}
|
|
6286
6321
|
}
|
|
@@ -7526,7 +7561,7 @@ function ciDefaultBranch(source) {
|
|
|
7526
7561
|
function refuseForgeDefaultMismatch(configured, source, command) {
|
|
7527
7562
|
const reported = ciDefaultBranch(source);
|
|
7528
7563
|
if (reported === void 0 || reported === configured) return;
|
|
7529
|
-
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.`);
|
|
7564
|
+
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.`);
|
|
7530
7565
|
}
|
|
7531
7566
|
//#endregion
|
|
7532
7567
|
//#region src/generators/release-ci.ts
|
|
@@ -8055,15 +8090,15 @@ function ignorableDependencies(ctx) {
|
|
|
8055
8090
|
function ignoreDependenciesField(dependencies) {
|
|
8056
8091
|
return dependencies.length > 0 ? `\n ignoreDependencies: ${JSON.stringify(dependencies)},` : "";
|
|
8057
8092
|
}
|
|
8058
|
-
function buildKnipConfigSingle(ignoreBinaries, ignoreDependencies) {
|
|
8093
|
+
function buildKnipConfigSingle(glob, ignoreBinaries, ignoreDependencies) {
|
|
8059
8094
|
return `import type { KnipConfig } from "knip";
|
|
8060
8095
|
|
|
8061
8096
|
export default {
|
|
8062
|
-
project: ["
|
|
8097
|
+
project: ["${glob}"],${ignoreBinariesField(ignoreBinaries)}${ignoreDependenciesField(ignoreDependencies)}
|
|
8063
8098
|
} satisfies KnipConfig;
|
|
8064
8099
|
`;
|
|
8065
8100
|
}
|
|
8066
|
-
function buildKnipConfigMonorepo(ignoreBinaries, ignoreDependencies) {
|
|
8101
|
+
function buildKnipConfigMonorepo(glob, ignoreBinaries, ignoreDependencies) {
|
|
8067
8102
|
return `import type { KnipConfig } from "knip";
|
|
8068
8103
|
|
|
8069
8104
|
export default {${ignoreBinariesField(ignoreBinaries)}${ignoreDependenciesField(ignoreDependencies)}
|
|
@@ -8073,7 +8108,7 @@ export default {${ignoreBinariesField(ignoreBinaries)}${ignoreDependenciesField(
|
|
|
8073
8108
|
project: [],
|
|
8074
8109
|
},
|
|
8075
8110
|
"packages/*": {
|
|
8076
|
-
project: ["
|
|
8111
|
+
project: ["${glob}"],
|
|
8077
8112
|
},
|
|
8078
8113
|
},
|
|
8079
8114
|
} satisfies KnipConfig;
|
|
@@ -8241,6 +8276,7 @@ async function generateKnip(ctx) {
|
|
|
8241
8276
|
const isMonorepo = ctx.config.structure === "monorepo";
|
|
8242
8277
|
const ignoreBinaries = foreignOwnedBinaries(ctx);
|
|
8243
8278
|
const ignoreDependencies = ignorableDependencies(ctx);
|
|
8279
|
+
const glob = sourceGlob(ctx.config.stack);
|
|
8244
8280
|
const buildConfig = isMonorepo ? buildKnipConfigMonorepo : buildKnipConfigSingle;
|
|
8245
8281
|
const existingPath = KNIP_CONFIG_PATHS.find((p) => ctx.exists(p));
|
|
8246
8282
|
if (existingPath) {
|
|
@@ -8251,7 +8287,7 @@ async function generateKnip(ctx) {
|
|
|
8251
8287
|
description: "Could not read existing knip config"
|
|
8252
8288
|
};
|
|
8253
8289
|
if (content.trim() === "") {
|
|
8254
|
-
const blank = isJsonConfig(existingPath) ? ensureIgnorePattern("{}", existingPath, ignoreBinaries, ignoreDependencies) ?? "{}\n" : formattedTemplate(ctx, existingPath, buildConfig(ignoreBinaries, ignoreDependencies));
|
|
8290
|
+
const blank = isJsonConfig(existingPath) ? ensureIgnorePattern("{}", existingPath, ignoreBinaries, ignoreDependencies) ?? "{}\n" : formattedTemplate(ctx, existingPath, buildConfig(glob, ignoreBinaries, ignoreDependencies));
|
|
8255
8291
|
ctx.write(existingPath, blank);
|
|
8256
8292
|
return {
|
|
8257
8293
|
filePath: existingPath,
|
|
@@ -8274,7 +8310,7 @@ async function generateKnip(ctx) {
|
|
|
8274
8310
|
description: existingPath === filePath ? "Already configured" : `Existing config found at ${existingPath}`
|
|
8275
8311
|
};
|
|
8276
8312
|
}
|
|
8277
|
-
ctx.write(filePath, formattedTemplate(ctx, filePath, buildConfig(ignoreBinaries, ignoreDependencies)));
|
|
8313
|
+
ctx.write(filePath, formattedTemplate(ctx, filePath, buildConfig(glob, ignoreBinaries, ignoreDependencies)));
|
|
8278
8314
|
return {
|
|
8279
8315
|
filePath,
|
|
8280
8316
|
action: "created",
|
|
@@ -8472,8 +8508,8 @@ function writeEmbedded(ctx, file) {
|
|
|
8472
8508
|
}
|
|
8473
8509
|
const NOT_BUILT = "run `pnpm build` from the repo root";
|
|
8474
8510
|
async function generateClaudeHooks(ctx, embedded = {
|
|
8475
|
-
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_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 (path.basename(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, 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 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 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 { };",
|
|
8476
|
-
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-
|
|
8511
|
+
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 { };",
|
|
8512
|
+
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"
|
|
8477
8513
|
}) {
|
|
8478
8514
|
const results = [{
|
|
8479
8515
|
filePath: HOOK_PATH,
|
|
@@ -9450,7 +9486,7 @@ const GIT_GUIDE_BODY = [
|
|
|
9450
9486
|
"",
|
|
9451
9487
|
"**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.",
|
|
9452
9488
|
"",
|
|
9453
|
-
"**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
|
|
9489
|
+
"**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."
|
|
9454
9490
|
].join("\n");
|
|
9455
9491
|
const PLAN_LIFECYCLE_GUIDE_BODY = [
|
|
9456
9492
|
"# Plan lifecycle",
|
|
@@ -9493,6 +9529,7 @@ const PLANNING_GUIDE_BODY = [
|
|
|
9493
9529
|
"### Writing the plan",
|
|
9494
9530
|
"",
|
|
9495
9531
|
"- 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.",
|
|
9532
|
+
"- 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.",
|
|
9496
9533
|
"- 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.",
|
|
9497
9534
|
`- 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.`,
|
|
9498
9535
|
"- 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.",
|
|
@@ -9506,11 +9543,12 @@ const PLANNING_GUIDE_BODY = [
|
|
|
9506
9543
|
"",
|
|
9507
9544
|
"## Working through a plan",
|
|
9508
9545
|
"",
|
|
9509
|
-
`Do the tasks in order, commit after each, and tick a box only once its result is verified. Each task starts red -- write its test, watch it fail, make it pass -- and its commit body carries the testing guide's \`${TEST_EVIDENCE_PREFIXES.red}\` or \`${TEST_EVIDENCE_PREFIXES.noTest}\` line. A box you cannot finish keeps its text exactly as written and gets one
|
|
9546
|
+
`Do the tasks in order, commit after each, and tick a box only once its result is verified. Each task starts red -- write its test, watch it fail, make it pass -- and its commit body carries the testing guide's \`${TEST_EVIDENCE_PREFIXES.red}\` or \`${TEST_EVIDENCE_PREFIXES.noTest}\` line. A box you cannot finish, or cannot finish in full, keeps its text exactly as written and gets one of four notes underneath:`,
|
|
9510
9547
|
"",
|
|
9511
9548
|
"- `**Progress:**` -- partly done: what landed, what remains, where to pick up. The next pass resumes it.",
|
|
9512
9549
|
"- `**Blocked:**` -- tried and failing: the error. The next pass retries it.",
|
|
9513
9550
|
"- `**Declined:**` -- must not be done, with the reason: a standing rule, a repo convention, or a check that found it moot. No pass touches it again. \"Too hard\" or \"out of budget\" is Progress, not Declined.",
|
|
9551
|
+
"- `**Deferred:** Task <n> -- <reason>` -- part of the box waits on a later task. Split that box first, so the work that waits is a box of its own, and defer only that one; deferring the whole box drops the part that does not wait. The note says which task and what in it the box waits on, both on that one line. The same plan-only commit adds `- [ ] **Carried from Task <source>:** <the work>` to that task, which is the work going on there; the target cannot be the retire task. The deferral then resolves the box: it says \"later\", not \"never\". With no carried box in the target, nothing is deferred and the box stays outstanding, counted against every later pass.",
|
|
9514
9552
|
"",
|
|
9515
9553
|
"To run a plan through subagents, see the subagents guide."
|
|
9516
9554
|
].join("\n");
|
|
@@ -9666,11 +9704,13 @@ const HEALTH_RULE_TAIL = [
|
|
|
9666
9704
|
"",
|
|
9667
9705
|
"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.",
|
|
9668
9706
|
"",
|
|
9669
|
-
"**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.",
|
|
9707
|
+
"**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.",
|
|
9708
|
+
"",
|
|
9709
|
+
"**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.",
|
|
9670
9710
|
"",
|
|
9671
|
-
"
|
|
9711
|
+
"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.",
|
|
9672
9712
|
"",
|
|
9673
|
-
"
|
|
9713
|
+
"**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."
|
|
9674
9714
|
];
|
|
9675
9715
|
function healthRuleBody(config) {
|
|
9676
9716
|
const lines = [...HEALTH_RULE_HEAD];
|
|
@@ -9805,7 +9845,7 @@ function agentGuides(config, coverageEnabled, healthEnabled, webUiEnabled) {
|
|
|
9805
9845
|
},
|
|
9806
9846
|
{
|
|
9807
9847
|
name: "health.md",
|
|
9808
|
-
trigger: "add a cross-module import, or when `health:check` fails",
|
|
9848
|
+
trigger: "add a cross-module import, or when `health:check` or a size budget fails",
|
|
9809
9849
|
body: healthRuleBody(config),
|
|
9810
9850
|
enabled: healthEnabled
|
|
9811
9851
|
}
|
|
@@ -10132,7 +10172,7 @@ function reportFailures(failed, command) {
|
|
|
10132
10172
|
function generateMigratePrompt(results, config, detected) {
|
|
10133
10173
|
const sections = [];
|
|
10134
10174
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
10135
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.
|
|
10175
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.96.0 repo:sync\` on ${timestamp}_\n`);
|
|
10136
10176
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
10137
10177
|
if (changed.length > 0) {
|
|
10138
10178
|
const items = changed.map((r) => {
|
|
@@ -10932,6 +10972,21 @@ function parseWarnMarkerLine(line) {
|
|
|
10932
10972
|
const count = Number.parseInt(match[1] ?? "", 10);
|
|
10933
10973
|
return Number.isNaN(count) ? null : count;
|
|
10934
10974
|
}
|
|
10975
|
+
const MEASURE_LINE = new RegExp(`^::bst-health:: measure (\\d+)\\s*$`);
|
|
10976
|
+
function parseMeasureMarkerLine(line) {
|
|
10977
|
+
const match = MEASURE_LINE.exec(line);
|
|
10978
|
+
if (match === null) return void 0;
|
|
10979
|
+
const value = Number.parseInt(match[1] ?? "", 10);
|
|
10980
|
+
return Number.isNaN(value) ? void 0 : value;
|
|
10981
|
+
}
|
|
10982
|
+
function readMeasureMarker(stdout) {
|
|
10983
|
+
let found;
|
|
10984
|
+
for (const line of stdout.split("\n")) {
|
|
10985
|
+
const parsed = parseMeasureMarkerLine(line);
|
|
10986
|
+
if (parsed !== void 0) found = parsed;
|
|
10987
|
+
}
|
|
10988
|
+
return found;
|
|
10989
|
+
}
|
|
10935
10990
|
//#endregion
|
|
10936
10991
|
//#region src/checks/step-exec.ts
|
|
10937
10992
|
function execStep(cmd, cwd, sink = process.stdout) {
|
|
@@ -11080,17 +11135,13 @@ function createRealHealthExecutor(rootDir) {
|
|
|
11080
11135
|
}
|
|
11081
11136
|
//#endregion
|
|
11082
11137
|
//#region src/codebase-health/reference.ts
|
|
11083
|
-
const HEALTH_CEILING_FILES = ["health-baseline.json", ".tooling.json"];
|
|
11084
11138
|
const BASELINE_FILE$2 = "health-baseline.json";
|
|
11085
|
-
const TOOLING_FILE = ".tooling.json";
|
|
11139
|
+
const TOOLING_FILE$1 = ".tooling.json";
|
|
11086
11140
|
function repoPrefix(executor, cwd) {
|
|
11087
11141
|
const result = executor.exec("git", ["rev-parse", "--show-prefix"], { cwd });
|
|
11088
11142
|
if (result.exitCode !== 0) return "";
|
|
11089
11143
|
return result.stdout.trim();
|
|
11090
11144
|
}
|
|
11091
|
-
function ceilingFileNamesAt(prefix) {
|
|
11092
|
-
return HEALTH_CEILING_FILES.map((name) => `${prefix}${name}`);
|
|
11093
|
-
}
|
|
11094
11145
|
const DefaultBranchSchema = z.object({ defaultBranch: z.string().optional() });
|
|
11095
11146
|
function parseDefaultBranchOverride(raw) {
|
|
11096
11147
|
if (raw === void 0 || raw.trim() === "") return void 0;
|
|
@@ -11114,9 +11165,9 @@ function resolveReferenceBranch(executor, cwd, prefix, ciEvent) {
|
|
|
11114
11165
|
"--quiet",
|
|
11115
11166
|
ref
|
|
11116
11167
|
], cwd) !== void 0) {
|
|
11117
|
-
const branch = parseDefaultBranchOverride(showAt(executor, cwd, ref, `${prefix}${TOOLING_FILE}`)) ?? chosen;
|
|
11168
|
+
const branch = parseDefaultBranchOverride(showAt(executor, cwd, ref, `${prefix}${TOOLING_FILE$1}`)) ?? chosen;
|
|
11118
11169
|
if (anchor !== void 0) return { branch };
|
|
11119
|
-
const namedElsewhere = parseDefaultBranchOverride(executor.readFile(TOOLING_FILE));
|
|
11170
|
+
const namedElsewhere = parseDefaultBranchOverride(executor.readFile(TOOLING_FILE$1));
|
|
11120
11171
|
if (namedElsewhere === void 0 || namedElsewhere === branch) return { branch };
|
|
11121
11172
|
return {
|
|
11122
11173
|
branch,
|
|
@@ -11124,7 +11175,7 @@ function resolveReferenceBranch(executor, cwd, prefix, ciEvent) {
|
|
|
11124
11175
|
};
|
|
11125
11176
|
}
|
|
11126
11177
|
if (anchor !== void 0) return { branch: chosen };
|
|
11127
|
-
return { branch: parseDefaultBranchOverride(executor.readFile(TOOLING_FILE)) ?? "main" };
|
|
11178
|
+
return { branch: parseDefaultBranchOverride(executor.readFile(TOOLING_FILE$1)) ?? "main" };
|
|
11128
11179
|
}
|
|
11129
11180
|
function originHeadBranch(git, cwd) {
|
|
11130
11181
|
const head = git([
|
|
@@ -11166,10 +11217,10 @@ function readReference(executor, opts) {
|
|
|
11166
11217
|
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.`
|
|
11167
11218
|
};
|
|
11168
11219
|
const baselineText = showAt(executor, opts.cwd, sha, `${prefix}${BASELINE_FILE$2}`);
|
|
11169
|
-
const toolingText = showAt(executor, opts.cwd, sha, `${prefix}${TOOLING_FILE}`);
|
|
11220
|
+
const toolingText = showAt(executor, opts.cwd, sha, `${prefix}${TOOLING_FILE$1}`);
|
|
11170
11221
|
const health = resolveHealthConfigFromContext({
|
|
11171
|
-
exists: (rel) => rel === TOOLING_FILE && toolingText !== void 0,
|
|
11172
|
-
read: (rel) => rel === TOOLING_FILE ? toolingText : void 0,
|
|
11222
|
+
exists: (rel) => rel === TOOLING_FILE$1 && toolingText !== void 0,
|
|
11223
|
+
read: (rel) => rel === TOOLING_FILE$1 ? toolingText : void 0,
|
|
11173
11224
|
config: { stack: opts.stack }
|
|
11174
11225
|
}, { isMonorepo: opts.isMonorepo });
|
|
11175
11226
|
const reference = {
|
|
@@ -11208,8 +11259,67 @@ const RAISABLE_LIMITS = [
|
|
|
11208
11259
|
"moduleSizeLimit",
|
|
11209
11260
|
"tolerance"
|
|
11210
11261
|
];
|
|
11262
|
+
const GATE_SWITCHES = ["enabled", "crossPackage"];
|
|
11263
|
+
const EXCLUDE_LEAF_PREFIX = "health.exclude.";
|
|
11264
|
+
function canonicalJson(value) {
|
|
11265
|
+
return JSON.stringify(sortKeys(value));
|
|
11266
|
+
}
|
|
11267
|
+
const JsonObjectSchema$1 = z.record(z.string(), z.unknown());
|
|
11268
|
+
function sortKeys(value) {
|
|
11269
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
11270
|
+
const object = JsonObjectSchema$1.safeParse(value);
|
|
11271
|
+
if (object.success) {
|
|
11272
|
+
const keys = Object.keys(object.data).toSorted();
|
|
11273
|
+
const sorted = {};
|
|
11274
|
+
for (const key of keys) sorted[key] = sortKeys(object.data[key]);
|
|
11275
|
+
return sorted;
|
|
11276
|
+
}
|
|
11277
|
+
return value;
|
|
11278
|
+
}
|
|
11279
|
+
function sizeRaises(reference, current) {
|
|
11280
|
+
const raises = [];
|
|
11281
|
+
for (const [id, base] of Object.entries(reference.baseline?.sizeBudgets ?? {})) {
|
|
11282
|
+
if (current.baseline === void 0) continue;
|
|
11283
|
+
const now = current.baseline.sizeBudgets?.[id];
|
|
11284
|
+
if (now === void 0) {
|
|
11285
|
+
if (current.health.sizeBudgets[id] !== void 0) raises.push({
|
|
11286
|
+
leaf: `sizeBudgets.${id}`,
|
|
11287
|
+
reference: String(base),
|
|
11288
|
+
current: "absent"
|
|
11289
|
+
});
|
|
11290
|
+
continue;
|
|
11291
|
+
}
|
|
11292
|
+
if (now > base) raises.push({
|
|
11293
|
+
leaf: `sizeBudgets.${id}`,
|
|
11294
|
+
reference: String(base),
|
|
11295
|
+
current: String(now)
|
|
11296
|
+
});
|
|
11297
|
+
}
|
|
11298
|
+
for (const [id, base] of Object.entries(reference.health.sizeBudgets)) {
|
|
11299
|
+
const now = current.health.sizeBudgets[id];
|
|
11300
|
+
if (now === void 0) {
|
|
11301
|
+
raises.push({
|
|
11302
|
+
leaf: `health.sizeBudgets.${id}`,
|
|
11303
|
+
reference: "present",
|
|
11304
|
+
current: "absent"
|
|
11305
|
+
});
|
|
11306
|
+
continue;
|
|
11307
|
+
}
|
|
11308
|
+
const baseJson = canonicalJson(base);
|
|
11309
|
+
if (baseJson !== canonicalJson(now)) raises.push({
|
|
11310
|
+
leaf: `health.sizeBudgets.${id}`,
|
|
11311
|
+
reference: baseJson,
|
|
11312
|
+
current: canonicalJson(now)
|
|
11313
|
+
});
|
|
11314
|
+
}
|
|
11315
|
+
return raises;
|
|
11316
|
+
}
|
|
11211
11317
|
function findRaises(reference, current) {
|
|
11212
|
-
return [
|
|
11318
|
+
return [
|
|
11319
|
+
...baselineRaises(reference, current),
|
|
11320
|
+
...sizeRaises(reference, current),
|
|
11321
|
+
...configRaises(reference.health, current.health)
|
|
11322
|
+
];
|
|
11213
11323
|
}
|
|
11214
11324
|
function baselineRaises(reference, current) {
|
|
11215
11325
|
const raises = [];
|
|
@@ -11221,7 +11331,7 @@ function baselineRaises(reference, current) {
|
|
|
11221
11331
|
reference: String(base),
|
|
11222
11332
|
current: String(now)
|
|
11223
11333
|
});
|
|
11224
|
-
else if (base !== void 0 && now === void 0 && current.baseline !== void 0 && current.measured
|
|
11334
|
+
else if (base !== void 0 && now === void 0 && current.baseline !== void 0 && (current.measured === void 0 || current.measured.metrics[key] !== void 0)) raises.push({
|
|
11225
11335
|
leaf: key,
|
|
11226
11336
|
reference: String(base),
|
|
11227
11337
|
current: "absent"
|
|
@@ -11232,7 +11342,7 @@ function baselineRaises(reference, current) {
|
|
|
11232
11342
|
for (const [module, base] of Object.entries(reference.baseline?.moduleCeilings?.[key] ?? {})) {
|
|
11233
11343
|
if (current.baseline === void 0) continue;
|
|
11234
11344
|
if (current.baseline.moduleCeilings?.[key]?.[module] !== void 0) continue;
|
|
11235
|
-
if (measured[module] === void 0) continue;
|
|
11345
|
+
if (current.measured !== void 0 && measured[module] === void 0) continue;
|
|
11236
11346
|
raises.push({
|
|
11237
11347
|
leaf: `moduleCeilings.${key}.${module}`,
|
|
11238
11348
|
reference: String(base),
|
|
@@ -11286,11 +11396,11 @@ function configRaises(reference, current) {
|
|
|
11286
11396
|
});
|
|
11287
11397
|
}
|
|
11288
11398
|
for (const pattern of current.exclude) if (!reference.exclude.includes(pattern)) raises.push({
|
|
11289
|
-
leaf:
|
|
11399
|
+
leaf: `${EXCLUDE_LEAF_PREFIX}${pattern}`,
|
|
11290
11400
|
reference: "absent",
|
|
11291
11401
|
current: "present"
|
|
11292
11402
|
});
|
|
11293
|
-
for (const name of
|
|
11403
|
+
for (const name of GATE_SWITCHES) if (reference[name] && !current[name]) raises.push({
|
|
11294
11404
|
leaf: `health.${name}`,
|
|
11295
11405
|
reference: "true",
|
|
11296
11406
|
current: "false"
|
|
@@ -11318,11 +11428,6 @@ function branchDiffNames(executor, cwd, mergeBase) {
|
|
|
11318
11428
|
}
|
|
11319
11429
|
return [...names].toSorted();
|
|
11320
11430
|
}
|
|
11321
|
-
function isCeilingOnly(names, prefix = "") {
|
|
11322
|
-
if (names.length === 0) return false;
|
|
11323
|
-
const files = ceilingFileNamesAt(prefix);
|
|
11324
|
-
return names.every((name) => files.includes(name));
|
|
11325
|
-
}
|
|
11326
11431
|
//#endregion
|
|
11327
11432
|
//#region ../forgejo/src/git.ts
|
|
11328
11433
|
const GIT_LOCAL_TIMEOUT_MS = 6e4;
|
|
@@ -12275,7 +12380,7 @@ function findInertAgentKeys(saved) {
|
|
|
12275
12380
|
function runInertAgentSettingsGate(saved) {
|
|
12276
12381
|
const keys = findInertAgentKeys(saved);
|
|
12277
12382
|
if (keys.length === 0) return [];
|
|
12278
|
-
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.`);
|
|
12383
|
+
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.`);
|
|
12279
12384
|
return keys;
|
|
12280
12385
|
}
|
|
12281
12386
|
function runPublishConfigGate(config, targetDir) {
|
|
@@ -12333,7 +12438,7 @@ function warnUnconfiguredDefaultBranch(targetDir, saved, git = defaultGitRunner)
|
|
|
12333
12438
|
if (head === void 0) return;
|
|
12334
12439
|
const name = head.startsWith("origin/") ? head.slice(7) : head;
|
|
12335
12440
|
if (name === "" || name === "main") return;
|
|
12336
|
-
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.`);
|
|
12441
|
+
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.`);
|
|
12337
12442
|
}
|
|
12338
12443
|
function warnCoveredChecksAdd(config, targetDir) {
|
|
12339
12444
|
const covered = coveredChecksAddEntries(config, readPackageJson(targetDir)?.scripts ?? {});
|
|
@@ -12545,7 +12650,7 @@ async function runUpdateReport(targetDir, flags = {}, options = {}) {
|
|
|
12545
12650
|
const dockerDiscovery = createDockerDiscoveryMemo();
|
|
12546
12651
|
const saved = loadToolingConfigStrict(targetDir);
|
|
12547
12652
|
if (!saved) {
|
|
12548
|
-
log$1.error("No .tooling.json found. Run `bst repo:sync` first to initialize the project.");
|
|
12653
|
+
log$1.error("No .tooling.json found. Run `pnpm exec bst repo:sync` first to initialize the project.");
|
|
12549
12654
|
process.exitCode = 1;
|
|
12550
12655
|
return {
|
|
12551
12656
|
status: "stopped",
|
|
@@ -14043,7 +14148,7 @@ const LabelSchema$1 = z.object({
|
|
|
14043
14148
|
exclusive: z.boolean().optional()
|
|
14044
14149
|
});
|
|
14045
14150
|
const LabelListSchema = z.array(LabelSchema$1);
|
|
14046
|
-
const LABELS_PAGE_SIZE =
|
|
14151
|
+
const LABELS_PAGE_SIZE = 50;
|
|
14047
14152
|
const LABELS_MAX_PAGES = 20;
|
|
14048
14153
|
async function fetchAllForgejoLabels(executor, conn) {
|
|
14049
14154
|
const labels = [];
|
|
@@ -14056,7 +14161,7 @@ async function fetchAllForgejoLabels(executor, conn) {
|
|
|
14056
14161
|
const parsed = LabelListSchema.safeParse(await res.json());
|
|
14057
14162
|
if (!parsed.success) throw new UnexpectedError(`Unexpected label-list response: ${parsed.error.message}`);
|
|
14058
14163
|
labels.push(...parsed.data);
|
|
14059
|
-
if (parsed.data.length
|
|
14164
|
+
if (!morePagesFollow(res, parsed.data.length, labels.length, LABELS_PAGE_SIZE)) break;
|
|
14060
14165
|
}
|
|
14061
14166
|
return labels;
|
|
14062
14167
|
}
|
|
@@ -14180,7 +14285,7 @@ async function listForgejoComments(executor, conn, number, options = {}) {
|
|
|
14180
14285
|
}, issueNotFoundMessage(conn, number));
|
|
14181
14286
|
const parsed = parseCommentListOrThrow(await res.json());
|
|
14182
14287
|
comments.push(...parsed.map(normalizeComment));
|
|
14183
|
-
if (parsed.length
|
|
14288
|
+
if (!morePagesFollow(res, parsed.length, comments.length, COMMENTS_PAGE_SIZE)) break;
|
|
14184
14289
|
if (page === COMMENTS_MAX_PAGES) {
|
|
14185
14290
|
log.warn(`Comment listing for #${String(number)} hit the ${String(COMMENTS_MAX_PAGES)}-page cap; results may be truncated`);
|
|
14186
14291
|
options.onPageCap?.();
|
|
@@ -14296,7 +14401,10 @@ async function fetchIssueListPage(executor, conn, params) {
|
|
|
14296
14401
|
verb: "list issues",
|
|
14297
14402
|
resource: conn.repository
|
|
14298
14403
|
}, repoNotFoundMessage(conn));
|
|
14299
|
-
return
|
|
14404
|
+
return {
|
|
14405
|
+
raw: parseIssueListOrThrow(await res.json()),
|
|
14406
|
+
res
|
|
14407
|
+
};
|
|
14300
14408
|
}
|
|
14301
14409
|
function normalizeDetailedIssues(raw, type) {
|
|
14302
14410
|
const normalized = raw.map(normalizeIssue);
|
|
@@ -14307,7 +14415,7 @@ const DETAILED_LIST_MAX_PAGES = 20;
|
|
|
14307
14415
|
async function listForgejoIssuesDetailedPaged(executor, conn, query, options) {
|
|
14308
14416
|
const issues = [];
|
|
14309
14417
|
for (let page = 1; page <= DETAILED_LIST_MAX_PAGES; page++) {
|
|
14310
|
-
const raw = await fetchIssueListPage(executor, conn, issueListParams(query.type, {
|
|
14418
|
+
const { raw } = await fetchIssueListPage(executor, conn, issueListParams(query.type, {
|
|
14311
14419
|
...query,
|
|
14312
14420
|
limit: DETAILED_LIST_PAGE_SIZE,
|
|
14313
14421
|
page
|
|
@@ -14714,7 +14822,7 @@ function ownerOf(names) {
|
|
|
14714
14822
|
}
|
|
14715
14823
|
//#endregion
|
|
14716
14824
|
//#region src/work-status/claim.ts
|
|
14717
|
-
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.";
|
|
14825
|
+
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.";
|
|
14718
14826
|
function decideClaim(report, enabled) {
|
|
14719
14827
|
if (!enabled) return {
|
|
14720
14828
|
kind: "answer",
|
|
@@ -14789,7 +14897,7 @@ async function writeClaim(executor, cwd, target, number) {
|
|
|
14789
14897
|
const flag = (await fetchAllForgejoLabels(executor, conn)).find((label) => label.name === HAND_AUTHORED_LABEL);
|
|
14790
14898
|
if (flag === void 0) return {
|
|
14791
14899
|
outcome: "failed",
|
|
14792
|
-
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.`
|
|
14900
|
+
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.`
|
|
14793
14901
|
};
|
|
14794
14902
|
await addForgejoIssueLabel(executor, conn, number, HAND_AUTHORED_LABEL, /* @__PURE__ */ new Map([[HAND_AUTHORED_LABEL, flag.id]]));
|
|
14795
14903
|
return {
|
|
@@ -18220,22 +18328,90 @@ function taskLabelId(text) {
|
|
|
18220
18328
|
return `${digits}${match?.[2] ?? ""}`;
|
|
18221
18329
|
}
|
|
18222
18330
|
//#endregion
|
|
18331
|
+
//#region src/plan-workspace/plan-units.ts
|
|
18332
|
+
const AFTER_LABEL = /^[ \t]*(?::|\.|--?)?[ \t]*/;
|
|
18333
|
+
function planTask(headingText, label) {
|
|
18334
|
+
const unwrapped = headingText.replace(/^(\*\*|__)/, "").replace(/(\*\*|__)$/, "");
|
|
18335
|
+
return {
|
|
18336
|
+
label,
|
|
18337
|
+
title: unwrapped.slice(unwrapped.indexOf(label) + label.length).replace(AFTER_LABEL, "").trim()
|
|
18338
|
+
};
|
|
18339
|
+
}
|
|
18340
|
+
function groupPlanUnits(text, boxes) {
|
|
18341
|
+
const lines = text.split("\n");
|
|
18342
|
+
const fenced = fencedLines(lines);
|
|
18343
|
+
const boxAtLine = /* @__PURE__ */ new Map();
|
|
18344
|
+
for (const box of boxes) boxAtLine.set(box.line, box);
|
|
18345
|
+
const units = [];
|
|
18346
|
+
const open = [];
|
|
18347
|
+
for (const [index, rawLine] of lines.entries()) {
|
|
18348
|
+
if (fenced[index] === true) continue;
|
|
18349
|
+
const line = rawLine ?? "";
|
|
18350
|
+
const heading = HEADING.exec(line);
|
|
18351
|
+
if (heading !== null) {
|
|
18352
|
+
const level = (heading[1] ?? "").length;
|
|
18353
|
+
while (open.length > 0 && (open[open.length - 1]?.level ?? 0) >= level) open.pop();
|
|
18354
|
+
const headingText = (heading[2] ?? "").trim();
|
|
18355
|
+
const label = taskLabelId(headingText);
|
|
18356
|
+
if (label === null) continue;
|
|
18357
|
+
const unit = {
|
|
18358
|
+
key: headingText,
|
|
18359
|
+
task: planTask(headingText, label),
|
|
18360
|
+
boxes: []
|
|
18361
|
+
};
|
|
18362
|
+
units.push(unit);
|
|
18363
|
+
open.push({
|
|
18364
|
+
level,
|
|
18365
|
+
unit
|
|
18366
|
+
});
|
|
18367
|
+
continue;
|
|
18368
|
+
}
|
|
18369
|
+
const box = boxAtLine.get(index);
|
|
18370
|
+
if (box === void 0) continue;
|
|
18371
|
+
const section = open[open.length - 1];
|
|
18372
|
+
if (section !== void 0) {
|
|
18373
|
+
section.unit.boxes.push(box);
|
|
18374
|
+
continue;
|
|
18375
|
+
}
|
|
18376
|
+
units.push({
|
|
18377
|
+
key: box.title,
|
|
18378
|
+
task: null,
|
|
18379
|
+
boxes: [box]
|
|
18380
|
+
});
|
|
18381
|
+
}
|
|
18382
|
+
return units;
|
|
18383
|
+
}
|
|
18384
|
+
//#endregion
|
|
18223
18385
|
//#region src/plan-workspace/plan-boxes.ts
|
|
18224
18386
|
const BOX_LINE_REGEX = /^[ \t]*[-*] \[([ xX])\][ \t]*(.*)$/;
|
|
18225
18387
|
const HEADING_REGEX = /^#{1,6} /;
|
|
18226
18388
|
const DECLINED_LINE_REGEX = /^[ \t]*(?:[-*][ \t]+)?\*\*Declined:\*\*[ \t]*(.*)$/;
|
|
18227
|
-
const NOTE_LINE_REGEX = /^[ \t]*(?:[-*][ \t]+)?\*\*(Progress|Blocked|Declined):\*\*[ \t]*(.*)$/;
|
|
18389
|
+
const NOTE_LINE_REGEX = /^[ \t]*(?:[-*][ \t]+)?\*\*(Progress|Blocked|Declined|Deferred):\*\*[ \t]*(.*)$/;
|
|
18390
|
+
const DEFERRED_LINE_REGEX = /^[ \t]*(?:[-*][ \t]+)?\*\*Deferred:\*\*[ \t]*(.*)$/;
|
|
18391
|
+
const DEFERRAL_SEPARATOR_REGEX = /^[ \t]*(?::|\.|--|-)?[ \t]*/;
|
|
18228
18392
|
const RETIRE_TASK_REGEX = /^(?:\*\*|__)?Retire this plan(?![A-Za-z0-9])/;
|
|
18393
|
+
function describeDeferralCheck(check) {
|
|
18394
|
+
switch (check) {
|
|
18395
|
+
case "empty-reason": return "the note names no reason";
|
|
18396
|
+
case "no-target-label": return "the note names no target task";
|
|
18397
|
+
case "no-own-task": return "the box names no task of its own";
|
|
18398
|
+
case "target-not-found": return "no task in the plan carries that label";
|
|
18399
|
+
case "target-not-later": return "the target task does not come later in the plan";
|
|
18400
|
+
case "target-is-retire": return "the target is the retire task";
|
|
18401
|
+
case "no-carried-box": return "the target task holds no box carried from this one";
|
|
18402
|
+
}
|
|
18403
|
+
}
|
|
18229
18404
|
function isOutstanding(box, options) {
|
|
18230
|
-
if (box.checked || box.declined) return false;
|
|
18405
|
+
if (box.checked || box.declined || box.deferred) return false;
|
|
18231
18406
|
return !(box.retireTask && options?.planRetired === true);
|
|
18232
18407
|
}
|
|
18233
18408
|
function noteKind(label) {
|
|
18234
18409
|
if (label === "Progress") return "progress";
|
|
18235
18410
|
if (label === "Blocked") return "blocked";
|
|
18411
|
+
if (label === "Deferred") return "deferred";
|
|
18236
18412
|
return "declined";
|
|
18237
18413
|
}
|
|
18238
|
-
const PARAGRAPH_BREAK_REGEX = /^[ \t]*(?:[-*+][ \t]|[0-9]+[.)][ \t]|\*\*(?:Progress|Blocked|Declined):\*\*|<!--)/;
|
|
18414
|
+
const PARAGRAPH_BREAK_REGEX = /^[ \t]*(?:[-*+][ \t]|[0-9]+[.)][ \t]|\*\*(?:Progress|Blocked|Declined|Deferred):\*\*|<!--)/;
|
|
18239
18415
|
const ABBREVIATION_REGEX = /(?:^|[^A-Za-z])(?:e\.g|i\.e|etc|vs|cf)$/i;
|
|
18240
18416
|
function boxParagraph(lines, fenced, start, end, first) {
|
|
18241
18417
|
const parts = [first];
|
|
@@ -18269,6 +18445,100 @@ function unwrapEmphasis(rawTitle) {
|
|
|
18269
18445
|
if (emphasis === void 0 || !rawTitle.endsWith(emphasis)) return rawTitle;
|
|
18270
18446
|
return rawTitle.slice(emphasis.length, -emphasis.length).trim();
|
|
18271
18447
|
}
|
|
18448
|
+
function deferredLineOf(lines, fenced, start, blockEnd) {
|
|
18449
|
+
for (let index = start + 1; index < blockEnd; index++) {
|
|
18450
|
+
if (fenced[index] === true) continue;
|
|
18451
|
+
const match = DEFERRED_LINE_REGEX.exec(lines[index] ?? "");
|
|
18452
|
+
if (match === null) continue;
|
|
18453
|
+
return (match[1] ?? "").trim();
|
|
18454
|
+
}
|
|
18455
|
+
return null;
|
|
18456
|
+
}
|
|
18457
|
+
function readDeferralNote(text) {
|
|
18458
|
+
if (text === "") return {
|
|
18459
|
+
target: null,
|
|
18460
|
+
reason: "",
|
|
18461
|
+
failed: "empty-reason"
|
|
18462
|
+
};
|
|
18463
|
+
const target = taskLabelId(text);
|
|
18464
|
+
if (target === null) return {
|
|
18465
|
+
target: null,
|
|
18466
|
+
reason: "",
|
|
18467
|
+
failed: "no-target-label"
|
|
18468
|
+
};
|
|
18469
|
+
const reason = text.slice(text.indexOf(target) + target.length).replace(DEFERRAL_SEPARATOR_REGEX, "").trim();
|
|
18470
|
+
if (reason === "") return {
|
|
18471
|
+
target,
|
|
18472
|
+
reason: "",
|
|
18473
|
+
failed: "empty-reason"
|
|
18474
|
+
};
|
|
18475
|
+
return {
|
|
18476
|
+
target,
|
|
18477
|
+
reason,
|
|
18478
|
+
failed: null
|
|
18479
|
+
};
|
|
18480
|
+
}
|
|
18481
|
+
function ownTaskLabel(box, units) {
|
|
18482
|
+
const unit = units.find((candidate) => candidate.boxes.some((other) => other.line === box.line));
|
|
18483
|
+
const fromSection = unit?.task?.label ?? null;
|
|
18484
|
+
if (fromSection !== null) return fromSection;
|
|
18485
|
+
return unit?.task === void 0 ? null : taskLabelId(box.title);
|
|
18486
|
+
}
|
|
18487
|
+
function holdsCarriedBox(unit, sourceLabel) {
|
|
18488
|
+
const escaped = sourceLabel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
18489
|
+
const carried = new RegExp(`^Carried from Task ${escaped}(?![0-9A-Za-z]):`);
|
|
18490
|
+
return unit.boxes.some((box) => carried.test(box.title));
|
|
18491
|
+
}
|
|
18492
|
+
function resolveDeferrals(boxes, units) {
|
|
18493
|
+
for (const { box, deferredLine } of boxes) {
|
|
18494
|
+
if (deferredLine === null || box.checked || box.declined) continue;
|
|
18495
|
+
const deferral = readDeferralNote(deferredLine);
|
|
18496
|
+
box.deferral = deferral;
|
|
18497
|
+
if (deferral.failed !== null || deferral.target === null) continue;
|
|
18498
|
+
const ownIndex = units.findIndex((unit) => unit.boxes.some((other) => other.line === box.line));
|
|
18499
|
+
const sourceLabel = ownTaskLabel(box, units);
|
|
18500
|
+
if (sourceLabel === null) {
|
|
18501
|
+
box.deferral = {
|
|
18502
|
+
...deferral,
|
|
18503
|
+
failed: "no-own-task"
|
|
18504
|
+
};
|
|
18505
|
+
continue;
|
|
18506
|
+
}
|
|
18507
|
+
const targetIndexes = units.flatMap((unit, index) => unit.task?.label === deferral.target ? [index] : []);
|
|
18508
|
+
if (targetIndexes.length !== 1) {
|
|
18509
|
+
box.deferral = {
|
|
18510
|
+
...deferral,
|
|
18511
|
+
failed: "target-not-found"
|
|
18512
|
+
};
|
|
18513
|
+
continue;
|
|
18514
|
+
}
|
|
18515
|
+
const targetIndex = targetIndexes[0] ?? -1;
|
|
18516
|
+
if (targetIndex <= ownIndex) {
|
|
18517
|
+
box.deferral = {
|
|
18518
|
+
...deferral,
|
|
18519
|
+
failed: "target-not-later"
|
|
18520
|
+
};
|
|
18521
|
+
continue;
|
|
18522
|
+
}
|
|
18523
|
+
const target = units[targetIndex];
|
|
18524
|
+
if (target === void 0) continue;
|
|
18525
|
+
if (target.boxes.some((other) => other.retireTask) || target.task !== null && RETIRE_TASK_REGEX.test(target.task.title)) {
|
|
18526
|
+
box.deferral = {
|
|
18527
|
+
...deferral,
|
|
18528
|
+
failed: "target-is-retire"
|
|
18529
|
+
};
|
|
18530
|
+
continue;
|
|
18531
|
+
}
|
|
18532
|
+
if (!holdsCarriedBox(target, sourceLabel)) {
|
|
18533
|
+
box.deferral = {
|
|
18534
|
+
...deferral,
|
|
18535
|
+
failed: "no-carried-box"
|
|
18536
|
+
};
|
|
18537
|
+
continue;
|
|
18538
|
+
}
|
|
18539
|
+
box.deferred = true;
|
|
18540
|
+
}
|
|
18541
|
+
}
|
|
18272
18542
|
function parsePlanBoxes(planText) {
|
|
18273
18543
|
const lines = planText.split("\n");
|
|
18274
18544
|
const fenced = fencedLines(lines);
|
|
@@ -18283,7 +18553,7 @@ function parsePlanBoxes(planText) {
|
|
|
18283
18553
|
rawTitle: (match[2] ?? "").trim()
|
|
18284
18554
|
});
|
|
18285
18555
|
}
|
|
18286
|
-
|
|
18556
|
+
const built = boxStarts.map((box, position) => {
|
|
18287
18557
|
const nextBoxIndex = boxStarts[position + 1]?.index ?? lines.length;
|
|
18288
18558
|
let blockEnd = nextBoxIndex;
|
|
18289
18559
|
for (let index = box.index + 1; index < nextBoxIndex; index++) {
|
|
@@ -18296,13 +18566,18 @@ function parsePlanBoxes(planText) {
|
|
|
18296
18566
|
const title = stripStrong(unwrapEmphasis(firstSentence$1(boxParagraph(lines, fenced, box.index, blockEnd, box.rawTitle))));
|
|
18297
18567
|
const retireTask = RETIRE_TASK_REGEX.test(title);
|
|
18298
18568
|
if (box.checked) return {
|
|
18299
|
-
|
|
18300
|
-
|
|
18301
|
-
|
|
18302
|
-
|
|
18303
|
-
|
|
18304
|
-
|
|
18305
|
-
|
|
18569
|
+
box: {
|
|
18570
|
+
line: box.index,
|
|
18571
|
+
checked: true,
|
|
18572
|
+
declined: false,
|
|
18573
|
+
reason: null,
|
|
18574
|
+
title,
|
|
18575
|
+
retireTask,
|
|
18576
|
+
notes: [],
|
|
18577
|
+
deferred: false,
|
|
18578
|
+
deferral: null
|
|
18579
|
+
},
|
|
18580
|
+
deferredLine: null
|
|
18306
18581
|
};
|
|
18307
18582
|
const notes = [];
|
|
18308
18583
|
for (let index = box.index + 1; index < blockEnd; index++) {
|
|
@@ -18327,15 +18602,22 @@ function parsePlanBoxes(planText) {
|
|
|
18327
18602
|
}
|
|
18328
18603
|
}
|
|
18329
18604
|
return {
|
|
18330
|
-
|
|
18331
|
-
|
|
18332
|
-
|
|
18333
|
-
|
|
18334
|
-
|
|
18335
|
-
|
|
18336
|
-
|
|
18605
|
+
box: {
|
|
18606
|
+
line: box.index,
|
|
18607
|
+
checked: false,
|
|
18608
|
+
declined: reason !== null,
|
|
18609
|
+
reason,
|
|
18610
|
+
title,
|
|
18611
|
+
retireTask,
|
|
18612
|
+
notes,
|
|
18613
|
+
deferred: false,
|
|
18614
|
+
deferral: null
|
|
18615
|
+
},
|
|
18616
|
+
deferredLine: deferredLineOf(lines, fenced, box.index, blockEnd)
|
|
18337
18617
|
};
|
|
18338
18618
|
});
|
|
18619
|
+
if (built.some((entry) => entry.deferredLine !== null)) resolveDeferrals(built, groupPlanUnits(planText, built.map((entry) => entry.box)));
|
|
18620
|
+
return built.map((entry) => entry.box);
|
|
18339
18621
|
}
|
|
18340
18622
|
function countOutstandingBoxes(planText, options) {
|
|
18341
18623
|
return parsePlanBoxes(planText).filter((box) => isOutstanding(box, options)).length;
|
|
@@ -18366,6 +18648,11 @@ function planStatus(planText, planPath) {
|
|
|
18366
18648
|
kind: note.kind,
|
|
18367
18649
|
text: note.text
|
|
18368
18650
|
})));
|
|
18651
|
+
const deferrals = boxes.flatMap((box) => box.deferral === null ? [] : [{
|
|
18652
|
+
task: box.title,
|
|
18653
|
+
target: box.deferral.target,
|
|
18654
|
+
failed: box.deferral.failed
|
|
18655
|
+
}]);
|
|
18369
18656
|
const nextBox = open[0];
|
|
18370
18657
|
return {
|
|
18371
18658
|
plan: planPath,
|
|
@@ -18373,6 +18660,8 @@ function planStatus(planText, planPath) {
|
|
|
18373
18660
|
checked: boxes.filter((box) => box.checked).length,
|
|
18374
18661
|
outstanding: open.length,
|
|
18375
18662
|
declined: boxes.filter((box) => box.declined).length,
|
|
18663
|
+
deferred: boxes.filter((box) => box.deferred).length,
|
|
18664
|
+
deferrals,
|
|
18376
18665
|
next: nextBox === void 0 || nextBox.title === "" ? null : nextBox.title,
|
|
18377
18666
|
nextTask: nextBox === void 0 ? null : enclosingTaskLabel(planText, nextBox.line),
|
|
18378
18667
|
onlyRetireLeft: open.length > 0 && open.every((box) => box.retireTask),
|
|
@@ -18477,14 +18766,14 @@ function unavailableWith(reason) {
|
|
|
18477
18766
|
reason
|
|
18478
18767
|
};
|
|
18479
18768
|
}
|
|
18480
|
-
function unavailable(error) {
|
|
18769
|
+
function unavailable$1(error) {
|
|
18481
18770
|
return unavailableWith(reasonText(error));
|
|
18482
18771
|
}
|
|
18483
18772
|
async function section(build) {
|
|
18484
18773
|
try {
|
|
18485
18774
|
return await build();
|
|
18486
18775
|
} catch (error) {
|
|
18487
|
-
return unavailable(error);
|
|
18776
|
+
return unavailable$1(error);
|
|
18488
18777
|
}
|
|
18489
18778
|
}
|
|
18490
18779
|
function reasonText(error) {
|
|
@@ -18725,7 +19014,7 @@ async function docsSection({ git, docsExtraDirs, resolved }) {
|
|
|
18725
19014
|
path,
|
|
18726
19015
|
status: {
|
|
18727
19016
|
...status,
|
|
18728
|
-
total: status.checked + status.outstanding + status.declined
|
|
19017
|
+
total: status.checked + status.outstanding + status.declined + status.deferred
|
|
18729
19018
|
}
|
|
18730
19019
|
});
|
|
18731
19020
|
}
|
|
@@ -18890,7 +19179,7 @@ function renderWorkStatus(report, options) {
|
|
|
18890
19179
|
if (report.heldByHand === true) lines.push("HELD BY HAND: agent-flags/hand-authored -- a human claimed this one; do not drive it.");
|
|
18891
19180
|
if (options?.agentEnabled === true && report.owner === "unclaimed") {
|
|
18892
19181
|
const number = claimableNumber(report);
|
|
18893
|
-
if (number !== null) lines.push(`UNCLAIMED: no agent label and no hand claim -- run \`bst work:start ${String(number)}\` before you work this.`);
|
|
19182
|
+
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.`);
|
|
18894
19183
|
}
|
|
18895
19184
|
lines.push(...block("object", report.object, objectRows));
|
|
18896
19185
|
lines.push(...block("docs", report.docs, docsRows));
|
|
@@ -18955,7 +19244,7 @@ const workStartCommand = defineCommand({
|
|
|
18955
19244
|
async run({ args }) {
|
|
18956
19245
|
const { ref, executor, git, forge } = await prepareWorkStatus(typeof args.ref === "string" ? args.ref.trim() : "", process.cwd());
|
|
18957
19246
|
const number = ref?.number;
|
|
18958
|
-
if (number === void 0) throw new FatalError("Usage: bst work:start needs an issue or pull request number or URL");
|
|
19247
|
+
if (number === void 0) throw new FatalError("Usage: pnpm exec bst work:start needs an issue or pull request number or URL");
|
|
18959
19248
|
const docs = resolveDocsConfig(git.topLevel);
|
|
18960
19249
|
const defaultBranch = resolveDefaultBranch({ cwd: git.topLevel });
|
|
18961
19250
|
const report = await buildWorkStatus({
|
|
@@ -19721,8 +20010,8 @@ function formatPrUpdate(result, rewrites, flags) {
|
|
|
19721
20010
|
//#endregion
|
|
19722
20011
|
//#region src/release/pull-request.ts
|
|
19723
20012
|
const BST_COMMANDS = {
|
|
19724
|
-
create: "bst pr:create",
|
|
19725
|
-
update: "bst pr:update"
|
|
20013
|
+
create: "pnpm exec bst pr:create",
|
|
20014
|
+
update: "pnpm exec bst pr:update"
|
|
19726
20015
|
};
|
|
19727
20016
|
const BST_FLAGS = {
|
|
19728
20017
|
body: "--body",
|
|
@@ -20836,7 +21125,7 @@ function requireDockerCredentials(registryHost) {
|
|
|
20836
21125
|
};
|
|
20837
21126
|
const missing = [...username ? [] : ["DOCKER_USERNAME"], ...password ? [] : ["DOCKER_PASSWORD"]].join(" / ");
|
|
20838
21127
|
const { username: secretUsername, password: secretPassword } = dockerSecretNames(registryHost);
|
|
20839
|
-
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.`);
|
|
21128
|
+
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.`);
|
|
20840
21129
|
}
|
|
20841
21130
|
function resolveRegistryConfig(cwd) {
|
|
20842
21131
|
const envHost = process.env["DOCKER_REGISTRY_HOST"];
|
|
@@ -21915,7 +22204,7 @@ const CHANGE_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
21915
22204
|
]);
|
|
21916
22205
|
async function runSetupAgent(targetDir, options) {
|
|
21917
22206
|
const saved = loadToolingConfigStrict(targetDir);
|
|
21918
|
-
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.`);
|
|
22207
|
+
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.`);
|
|
21919
22208
|
const previous = previousState(saved);
|
|
21920
22209
|
let consent = "already-enabled";
|
|
21921
22210
|
if (previous !== "true") {
|
|
@@ -21947,7 +22236,7 @@ async function runSetupAgent(targetDir, options) {
|
|
|
21947
22236
|
if (report.status === "stopped" || failed.length > 0) status = "failed";
|
|
21948
22237
|
else if (changed.length > 0) status = "changed";
|
|
21949
22238
|
else status = "up-to-date";
|
|
21950
|
-
if (status === "failed") log$1.error("The sync failed. Fix what it reported, then run bst setup:agent again.");
|
|
22239
|
+
if (status === "failed") log$1.error("The sync failed. Fix what it reported, then run pnpm exec bst setup:agent again.");
|
|
21951
22240
|
else log$1.info("Commit these changes and land them on the default branch; the agent reads consent there within about a minute.");
|
|
21952
22241
|
return {
|
|
21953
22242
|
dir: targetDir,
|
|
@@ -22792,7 +23081,7 @@ function serializeBaseline(baseline) {
|
|
|
22792
23081
|
//#region src/commands/coverage-check.ts
|
|
22793
23082
|
const COVERAGE_SUMMARY_FILE$1 = "coverage/coverage-summary.json";
|
|
22794
23083
|
const BASELINE_FILE$1 = "coverage-baseline.json";
|
|
22795
|
-
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.`;
|
|
23084
|
+
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.`;
|
|
22796
23085
|
function applyBaselineUpdate$1(out, baselinePath, baseline, current, options) {
|
|
22797
23086
|
const { reset, floorPp } = options;
|
|
22798
23087
|
if (reset) {
|
|
@@ -22829,7 +23118,7 @@ const coverageCheckCommand = defineCommand({
|
|
|
22829
23118
|
},
|
|
22830
23119
|
reset: {
|
|
22831
23120
|
type: "boolean",
|
|
22832
|
-
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."
|
|
23121
|
+
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."
|
|
22833
23122
|
},
|
|
22834
23123
|
"fail-on-missing-baseline": {
|
|
22835
23124
|
type: "boolean",
|
|
@@ -23014,13 +23303,14 @@ const coverageStatusCommand = defineCommand({
|
|
|
23014
23303
|
});
|
|
23015
23304
|
//#endregion
|
|
23016
23305
|
//#region src/codebase-health/baseline.ts
|
|
23017
|
-
function serializeHealthBaseline(metrics, ceilings) {
|
|
23306
|
+
function serializeHealthBaseline(metrics, ceilings, sizeBudgets) {
|
|
23018
23307
|
const ordered = {};
|
|
23019
23308
|
for (const key of METRIC_KEYS) {
|
|
23020
23309
|
const value = metrics[key];
|
|
23021
23310
|
if (value !== void 0) ordered[key] = value;
|
|
23022
23311
|
}
|
|
23023
23312
|
if (ceilings !== void 0 && Object.keys(ceilings).length > 0) ordered["moduleCeilings"] = ceilings;
|
|
23313
|
+
if (sizeBudgets !== void 0 && Object.keys(sizeBudgets).length > 0) ordered["sizeBudgets"] = sizeBudgets;
|
|
23024
23314
|
return JSON.stringify(ordered, null, 2) + "\n";
|
|
23025
23315
|
}
|
|
23026
23316
|
//#endregion
|
|
@@ -23050,6 +23340,20 @@ function createExcludeMatcher(patterns) {
|
|
|
23050
23340
|
};
|
|
23051
23341
|
}
|
|
23052
23342
|
//#endregion
|
|
23343
|
+
//#region src/codebase-health/exempt-match.ts
|
|
23344
|
+
function exemptKeyMatcher(key) {
|
|
23345
|
+
if (!picomatch.scan(key).isGlob) return (path) => path === key;
|
|
23346
|
+
const isMatch = picomatch(key, { dot: true });
|
|
23347
|
+
return (path) => isMatch(path);
|
|
23348
|
+
}
|
|
23349
|
+
function createExemptLookup(reasons) {
|
|
23350
|
+
const globs = Object.entries(reasons).filter(([key]) => picomatch.scan(key).isGlob).map(([key, reason]) => ({
|
|
23351
|
+
matches: exemptKeyMatcher(key),
|
|
23352
|
+
reason
|
|
23353
|
+
}));
|
|
23354
|
+
return (path) => reasons[path] ?? globs.find((glob) => glob.matches(path))?.reason;
|
|
23355
|
+
}
|
|
23356
|
+
//#endregion
|
|
23053
23357
|
//#region src/codebase-health/depcruise.ts
|
|
23054
23358
|
const DependencySchema = z.object({
|
|
23055
23359
|
module: z.string(),
|
|
@@ -23073,10 +23377,11 @@ function partitionExempt(list, reasons) {
|
|
|
23073
23377
|
kept: list,
|
|
23074
23378
|
exempt: []
|
|
23075
23379
|
};
|
|
23380
|
+
const reasonFor = createExemptLookup(reasons);
|
|
23076
23381
|
const kept = [];
|
|
23077
23382
|
const exempt = [];
|
|
23078
23383
|
for (const entry of list) {
|
|
23079
|
-
const reason =
|
|
23384
|
+
const reason = reasonFor(entry.module);
|
|
23080
23385
|
if (reason === void 0) kept.push(entry);
|
|
23081
23386
|
else exempt.push({
|
|
23082
23387
|
...entry,
|
|
@@ -23656,7 +23961,40 @@ function offenderEntries(details, key) {
|
|
|
23656
23961
|
//#endregion
|
|
23657
23962
|
//#region src/codebase-health/compare.ts
|
|
23658
23963
|
const DEFAULT_TOLERANCE = 0;
|
|
23659
|
-
function
|
|
23964
|
+
function compareSizeBudgets(baseline, measurements) {
|
|
23965
|
+
const results = [];
|
|
23966
|
+
for (const measurement of measurements) {
|
|
23967
|
+
const ceiling = baseline.sizeBudgets?.[measurement.id];
|
|
23968
|
+
if (measurement.measured === void 0) {
|
|
23969
|
+
results.push({
|
|
23970
|
+
id: measurement.id,
|
|
23971
|
+
ceiling,
|
|
23972
|
+
pass: false,
|
|
23973
|
+
reason: measurement.unmeasured ?? "not measured"
|
|
23974
|
+
});
|
|
23975
|
+
continue;
|
|
23976
|
+
}
|
|
23977
|
+
if (ceiling === void 0) {
|
|
23978
|
+
results.push({
|
|
23979
|
+
id: measurement.id,
|
|
23980
|
+
measured: measurement.measured,
|
|
23981
|
+
pass: false,
|
|
23982
|
+
reason: "unrecorded",
|
|
23983
|
+
measurerChanged: measurement.measurerChanged
|
|
23984
|
+
});
|
|
23985
|
+
continue;
|
|
23986
|
+
}
|
|
23987
|
+
results.push({
|
|
23988
|
+
id: measurement.id,
|
|
23989
|
+
ceiling,
|
|
23990
|
+
measured: measurement.measured,
|
|
23991
|
+
pass: measurement.measured <= ceiling,
|
|
23992
|
+
measurerChanged: measurement.measurerChanged
|
|
23993
|
+
});
|
|
23994
|
+
}
|
|
23995
|
+
return results;
|
|
23996
|
+
}
|
|
23997
|
+
function compareMetrics(baseline, current, tolerance = DEFAULT_TOLERANCE, currentCeilings = {}, sizes = []) {
|
|
23660
23998
|
const metrics = METRIC_KEYS.map((key) => {
|
|
23661
23999
|
const b = baseline[key];
|
|
23662
24000
|
const c = current[key];
|
|
@@ -23691,23 +24029,65 @@ function compareMetrics(baseline, current, tolerance = DEFAULT_TOLERANCE, curren
|
|
|
23691
24029
|
});
|
|
23692
24030
|
}
|
|
23693
24031
|
}
|
|
23694
|
-
const
|
|
24032
|
+
const uncoveredOutliers = [];
|
|
23695
24033
|
if (baseline.moduleCeilings !== void 0) {
|
|
23696
|
-
for (const key of RATCHETED_METRIC_KEYS) for (const [module, count] of Object.entries(currentCeilings[key] ?? {})) if (baseline.moduleCeilings[key]?.[module] === void 0)
|
|
24034
|
+
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({
|
|
23697
24035
|
key,
|
|
23698
24036
|
module,
|
|
23699
24037
|
current: count
|
|
23700
24038
|
});
|
|
23701
24039
|
}
|
|
24040
|
+
const sizeResults = compareSizeBudgets(baseline, sizes);
|
|
23702
24041
|
return {
|
|
23703
|
-
pass: metrics.every((m) => m.pass) && ceilings.every((c) => c.pass),
|
|
24042
|
+
pass: metrics.every((m) => m.pass) && ceilings.every((c) => c.pass) && sizeResults.every((s) => s.pass),
|
|
23704
24043
|
tolerance,
|
|
23705
24044
|
metrics,
|
|
23706
24045
|
ceilings,
|
|
23707
|
-
|
|
24046
|
+
uncoveredOutliers,
|
|
24047
|
+
sizeResults
|
|
24048
|
+
};
|
|
24049
|
+
}
|
|
24050
|
+
function needsBaseMeasurement(result) {
|
|
24051
|
+
const failedKeys = new Set(result.metrics.filter((m) => m.pass === false).map((m) => m.key));
|
|
24052
|
+
return RATCHETED_METRIC_KEYS.some((key) => failedKeys.has(key) && result.uncoveredOutliers.some((o) => o.key === key));
|
|
24053
|
+
}
|
|
24054
|
+
function attributeOutliers(uncovered, base) {
|
|
24055
|
+
if (base.status !== "measured") return {
|
|
24056
|
+
outlierBase: base.status === "unavailable" ? {
|
|
24057
|
+
status: "unavailable",
|
|
24058
|
+
mergeBase: base.mergeBase,
|
|
24059
|
+
reason: base.reason
|
|
24060
|
+
} : { status: "not-needed" },
|
|
24061
|
+
newOutliers: [],
|
|
24062
|
+
baseOutliers: [],
|
|
24063
|
+
unattributedOutliers: [...uncovered]
|
|
24064
|
+
};
|
|
24065
|
+
const newOutliers = [];
|
|
24066
|
+
const baseOutliers = [];
|
|
24067
|
+
for (const outlier of uncovered) {
|
|
24068
|
+
const baseCount = base.ceilings[outlier.key]?.[outlier.module];
|
|
24069
|
+
if (baseCount === void 0) {
|
|
24070
|
+
newOutliers.push(outlier);
|
|
24071
|
+
continue;
|
|
24072
|
+
}
|
|
24073
|
+
baseOutliers.push({
|
|
24074
|
+
key: outlier.key,
|
|
24075
|
+
module: outlier.module,
|
|
24076
|
+
base: baseCount,
|
|
24077
|
+
current: outlier.current
|
|
24078
|
+
});
|
|
24079
|
+
}
|
|
24080
|
+
return {
|
|
24081
|
+
outlierBase: {
|
|
24082
|
+
status: "measured",
|
|
24083
|
+
mergeBase: base.mergeBase
|
|
24084
|
+
},
|
|
24085
|
+
newOutliers,
|
|
24086
|
+
baseOutliers,
|
|
24087
|
+
unattributedOutliers: []
|
|
23708
24088
|
};
|
|
23709
24089
|
}
|
|
23710
|
-
function isFlatOrBetter(baseline, current, currentCeilings = {}) {
|
|
24090
|
+
function isFlatOrBetter(baseline, current, currentCeilings = {}, sizes = []) {
|
|
23711
24091
|
const metricsFlat = METRIC_KEYS.every((key) => {
|
|
23712
24092
|
const b = baseline[key];
|
|
23713
24093
|
const c = current[key];
|
|
@@ -23723,7 +24103,133 @@ function isFlatOrBetter(baseline, current, currentCeilings = {}) {
|
|
|
23723
24103
|
return curr <= base;
|
|
23724
24104
|
});
|
|
23725
24105
|
});
|
|
23726
|
-
|
|
24106
|
+
const sizesFlat = sizes.every((s) => {
|
|
24107
|
+
const recorded = baseline.sizeBudgets?.[s.id];
|
|
24108
|
+
if (recorded === void 0) return true;
|
|
24109
|
+
if (s.measured === void 0) return false;
|
|
24110
|
+
return s.measured <= recorded;
|
|
24111
|
+
});
|
|
24112
|
+
return metricsFlat && ceilingsFlat && sizesFlat;
|
|
24113
|
+
}
|
|
24114
|
+
//#endregion
|
|
24115
|
+
//#region src/codebase-health/base-checkout.ts
|
|
24116
|
+
const TEMP_PREFIX = "bst-health-base-";
|
|
24117
|
+
function withMergeBaseCheckout(executor, targetDir, mergeBase, use) {
|
|
24118
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), TEMP_PREFIX));
|
|
24119
|
+
const tree = path.join(tmp, "tree");
|
|
24120
|
+
const index = path.join(tmp, "index");
|
|
24121
|
+
try {
|
|
24122
|
+
fs.mkdirSync(tree, { recursive: true });
|
|
24123
|
+
const gitDir = gitOutput(executor.exec("git", ["rev-parse", "--absolute-git-dir"], { cwd: targetDir }), "git rev-parse --absolute-git-dir").trim();
|
|
24124
|
+
const workRoot = gitOutput(executor.exec("git", ["rev-parse", "--show-toplevel"], { cwd: targetDir }), "git rev-parse --show-toplevel").trim();
|
|
24125
|
+
const intoCheckout = (args) => executor.exec("git", [
|
|
24126
|
+
`--git-dir=${gitDir}`,
|
|
24127
|
+
`--work-tree=${tree}`,
|
|
24128
|
+
...args
|
|
24129
|
+
], {
|
|
24130
|
+
cwd: tree,
|
|
24131
|
+
env: { GIT_INDEX_FILE: index }
|
|
24132
|
+
});
|
|
24133
|
+
gitOutput(intoCheckout(["read-tree", mergeBase]), `git read-tree ${mergeBase.slice(0, 12)}`);
|
|
24134
|
+
gitOutput(intoCheckout(["checkout-index", "-a"]), "git checkout-index -a");
|
|
24135
|
+
mirrorNodeModules(workRoot, tree);
|
|
24136
|
+
return use(tree);
|
|
24137
|
+
} finally {
|
|
24138
|
+
fs.rmSync(tmp, {
|
|
24139
|
+
recursive: true,
|
|
24140
|
+
force: true
|
|
24141
|
+
});
|
|
24142
|
+
}
|
|
24143
|
+
}
|
|
24144
|
+
function gitOutput(result, command) {
|
|
24145
|
+
if (result.exitCode === 0) return result.stdout;
|
|
24146
|
+
const detail = result.stderr.trim().split("\n")[0] ?? "";
|
|
24147
|
+
throw new TransientError(`${command} failed: ${detail === "" ? `exit ${String(result.exitCode)}` : detail}`);
|
|
24148
|
+
}
|
|
24149
|
+
function mirrorNodeModules(workingRoot, checkoutRoot) {
|
|
24150
|
+
mirrorTree(workingRoot, checkoutRoot, "");
|
|
24151
|
+
}
|
|
24152
|
+
function mirrorTree(workingRoot, checkoutRoot, rel) {
|
|
24153
|
+
mirrorEntries(path.join(workingRoot, rel, "node_modules"), path.join(checkoutRoot, rel, "node_modules"));
|
|
24154
|
+
let entries;
|
|
24155
|
+
try {
|
|
24156
|
+
entries = fs.readdirSync(path.join(checkoutRoot, rel), { withFileTypes: true });
|
|
24157
|
+
} catch (_error) {
|
|
24158
|
+
return;
|
|
24159
|
+
}
|
|
24160
|
+
for (const entry of entries) {
|
|
24161
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
24162
|
+
if (entry.name === ".git" || entry.name === "node_modules") continue;
|
|
24163
|
+
mirrorTree(workingRoot, checkoutRoot, path.join(rel, entry.name));
|
|
24164
|
+
}
|
|
24165
|
+
}
|
|
24166
|
+
function mirrorEntries(source, dest) {
|
|
24167
|
+
let entries;
|
|
24168
|
+
try {
|
|
24169
|
+
entries = fs.readdirSync(source, { withFileTypes: true });
|
|
24170
|
+
} catch (_error) {
|
|
24171
|
+
return;
|
|
24172
|
+
}
|
|
24173
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
24174
|
+
for (const entry of entries) {
|
|
24175
|
+
const src = path.join(source, entry.name);
|
|
24176
|
+
const target = path.join(dest, entry.name);
|
|
24177
|
+
if (entry.isSymbolicLink()) {
|
|
24178
|
+
const link = safeReadlink(src);
|
|
24179
|
+
if (link !== void 0 && !path.isAbsolute(link)) {
|
|
24180
|
+
linkRelative(link, target);
|
|
24181
|
+
continue;
|
|
24182
|
+
}
|
|
24183
|
+
} else if (entry.isDirectory() && entry.name.startsWith("@")) {
|
|
24184
|
+
mirrorEntries(src, target);
|
|
24185
|
+
continue;
|
|
24186
|
+
}
|
|
24187
|
+
try {
|
|
24188
|
+
fs.symlinkSync(src, target, entry.isDirectory() ? "dir" : "file");
|
|
24189
|
+
} catch (_error) {}
|
|
24190
|
+
}
|
|
24191
|
+
}
|
|
24192
|
+
function safeReadlink(file) {
|
|
24193
|
+
try {
|
|
24194
|
+
return fs.readlinkSync(file);
|
|
24195
|
+
} catch (_error) {
|
|
24196
|
+
return;
|
|
24197
|
+
}
|
|
24198
|
+
}
|
|
24199
|
+
function linkRelative(link, target) {
|
|
24200
|
+
try {
|
|
24201
|
+
fs.symlinkSync(link, target);
|
|
24202
|
+
} catch (_error) {}
|
|
24203
|
+
}
|
|
24204
|
+
//#endregion
|
|
24205
|
+
//#region src/codebase-health/base-outliers.ts
|
|
24206
|
+
function unavailable(reason) {
|
|
24207
|
+
return {
|
|
24208
|
+
status: "unavailable",
|
|
24209
|
+
reason: firstLine(reason)
|
|
24210
|
+
};
|
|
24211
|
+
}
|
|
24212
|
+
function firstLine(text) {
|
|
24213
|
+
return text.trim().split("\n")[0] ?? "";
|
|
24214
|
+
}
|
|
24215
|
+
function measureBaseOutliers(opts) {
|
|
24216
|
+
const collect = opts.collect ?? collectDetails;
|
|
24217
|
+
try {
|
|
24218
|
+
return withMergeBaseCheckout(opts.executor, opts.targetDir, opts.mergeBase, (checkoutRoot) => {
|
|
24219
|
+
const baseTarget = path.join(checkoutRoot, opts.prefix);
|
|
24220
|
+
const collectConfig = buildCollectConfig(opts.config, { tsConfig: existsSync(path.join(baseTarget, "tsconfig.json")) ? "tsconfig.json" : void 0 });
|
|
24221
|
+
const details = collect(createRealHealthExecutor(baseTarget), collectConfig);
|
|
24222
|
+
const unresolved = details.unresolvedFirstParty;
|
|
24223
|
+
if (unresolved !== void 0 && unresolved.length > 0) return unavailable(`the base cruise could not resolve ${String(unresolved.length)} first-party imports`);
|
|
24224
|
+
if (opts.config.graphMetricsEnabled && details.circularDependencies === void 0) return unavailable("the base cruise produced no graph metrics");
|
|
24225
|
+
return {
|
|
24226
|
+
status: "measured",
|
|
24227
|
+
ceilings: ceilingsFromDetails(details)
|
|
24228
|
+
};
|
|
24229
|
+
});
|
|
24230
|
+
} catch (error) {
|
|
24231
|
+
return unavailable(describeError$1(error));
|
|
24232
|
+
}
|
|
23727
24233
|
}
|
|
23728
24234
|
//#endregion
|
|
23729
24235
|
//#region src/codebase-health/render.ts
|
|
@@ -23811,7 +24317,7 @@ function formatExplain(details) {
|
|
|
23811
24317
|
}
|
|
23812
24318
|
return lines.join("\n");
|
|
23813
24319
|
}
|
|
23814
|
-
function formatCompareDiff(result, details) {
|
|
24320
|
+
function formatCompareDiff(result, details, attribution) {
|
|
23815
24321
|
const lines = [];
|
|
23816
24322
|
for (const m of result.metrics) {
|
|
23817
24323
|
if (m.baseline === void 0 && m.current === void 0) continue;
|
|
@@ -23829,10 +24335,34 @@ function formatCompareDiff(result, details) {
|
|
|
23829
24335
|
if (c.pass) continue;
|
|
23830
24336
|
lines.push(` ${CEILING_LABELS[c.key]}: ${c.module} ${c.baseline} -> ${c.current} (ceiling)`);
|
|
23831
24337
|
}
|
|
24338
|
+
for (const s of result.sizeResults) if (!s.pass) {
|
|
24339
|
+
let line;
|
|
24340
|
+
if (s.measured === void 0) line = ` size: ${s.id} unmeasured (${s.reason ?? "not measured"})`;
|
|
24341
|
+
else if (s.ceiling === void 0) line = ` size: ${s.id} -- -> ${s.measured} (unrecorded)`;
|
|
24342
|
+
else line = ` size: ${s.id} ${s.ceiling} -> ${s.measured} (ceiling)`;
|
|
24343
|
+
lines.push(line);
|
|
24344
|
+
if (s.measurerChanged === true) lines.push(` size: ${s.id} measured by code this branch changed`);
|
|
24345
|
+
}
|
|
23832
24346
|
const failedKeys = new Set(result.metrics.filter((m) => m.pass === false).map((m) => m.key));
|
|
23833
|
-
|
|
24347
|
+
let printedAttribution = false;
|
|
24348
|
+
for (const o of attribution.newOutliers) {
|
|
23834
24349
|
if (!failedKeys.has(o.key)) continue;
|
|
23835
24350
|
lines.push(` ${CEILING_LABELS[o.key]}: ${o.module} -- -> ${o.current} (new)`);
|
|
24351
|
+
printedAttribution = true;
|
|
24352
|
+
}
|
|
24353
|
+
for (const o of attribution.baseOutliers) {
|
|
24354
|
+
if (!failedKeys.has(o.key)) continue;
|
|
24355
|
+
lines.push(` ${CEILING_LABELS[o.key]}: ${o.module} ${o.base} -> ${o.current} (on base)`);
|
|
24356
|
+
printedAttribution = true;
|
|
24357
|
+
}
|
|
24358
|
+
for (const o of attribution.unattributedOutliers) {
|
|
24359
|
+
if (!failedKeys.has(o.key)) continue;
|
|
24360
|
+
lines.push(` ${CEILING_LABELS[o.key]}: ${o.module} -- -> ${o.current} (no ceiling)`);
|
|
24361
|
+
printedAttribution = true;
|
|
24362
|
+
}
|
|
24363
|
+
if (printedAttribution) {
|
|
24364
|
+
if (attribution.outlierBase.status === "measured") lines.push(` outliers attributed against the merge base ${attribution.outlierBase.mergeBase}`);
|
|
24365
|
+
else if (attribution.outlierBase.status === "unavailable") lines.push(` outliers not attributed: the merge base ${attribution.outlierBase.mergeBase} could not be measured (${attribution.outlierBase.reason})`);
|
|
23836
24366
|
}
|
|
23837
24367
|
lines.push(result.pass ? "health: PASS" : "health: FAIL");
|
|
23838
24368
|
return lines.join("\n");
|
|
@@ -23853,7 +24383,7 @@ function formatGraphUnavailableForSummary(details, graphMetricsEnabled, diagnost
|
|
|
23853
24383
|
}
|
|
23854
24384
|
function formatStaleEntries(stale) {
|
|
23855
24385
|
return stale.map((entry) => {
|
|
23856
|
-
if (entry.kind === "exemption") return `health.exempt: "${entry.path}"
|
|
24386
|
+
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.`;
|
|
23857
24387
|
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.`;
|
|
23858
24388
|
});
|
|
23859
24389
|
}
|
|
@@ -23881,7 +24411,7 @@ function formatSourceDiagnosis(sources, graphMetricsEnabled) {
|
|
|
23881
24411
|
`Resolved roots: ${resolvedRoots}.`,
|
|
23882
24412
|
graphClause,
|
|
23883
24413
|
filtersClause,
|
|
23884
|
-
`Run "bst health:summary --explain" for the full breakdown.`
|
|
24414
|
+
`Run "pnpm exec bst health:summary --explain" for the full breakdown.`
|
|
23885
24415
|
].filter((part) => part.length > 0).join(" ")
|
|
23886
24416
|
};
|
|
23887
24417
|
}
|
|
@@ -23890,69 +24420,90 @@ function formatUnresolvedFirstParty(entries) {
|
|
|
23890
24420
|
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.`);
|
|
23891
24421
|
return lines.join("\n");
|
|
23892
24422
|
}
|
|
23893
|
-
const
|
|
23894
|
-
function
|
|
23895
|
-
|
|
23896
|
-
const shown = otherFiles.slice(0, REFUSAL_FILE_CAP);
|
|
23897
|
-
if (shown.length > 0) {
|
|
23898
|
-
const more = otherFiles.length - shown.length;
|
|
23899
|
-
const suffix = more > 0 ? ` (+${String(more)} more)` : "";
|
|
23900
|
-
lines.push(` this branch also changes: ${shown.join(", ")}${suffix}`);
|
|
23901
|
-
}
|
|
23902
|
-
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.`);
|
|
23903
|
-
return lines.join("\n");
|
|
24423
|
+
const CARRIED_RAISES_FIRST_LINE = "raises carried by this branch:";
|
|
24424
|
+
function formatCarriedRaises(raises) {
|
|
24425
|
+
return [CARRIED_RAISES_FIRST_LINE, ...raises.map((r) => ` ${r.leaf}: ${r.reference} -> ${r.current}`)].join("\n");
|
|
23904
24426
|
}
|
|
23905
24427
|
//#endregion
|
|
23906
|
-
//#region src/codebase-health/
|
|
23907
|
-
function
|
|
23908
|
-
|
|
23909
|
-
|
|
23910
|
-
|
|
23911
|
-
|
|
23912
|
-
|
|
23913
|
-
for (const key of RATCHETED_METRIC_KEYS) {
|
|
23914
|
-
const recorded = baseline.moduleCeilings?.[key];
|
|
23915
|
-
if (recorded === void 0) continue;
|
|
23916
|
-
for (const [path, count] of Object.entries(recorded)) {
|
|
23917
|
-
if (wasMeasured(key, path, details)) continue;
|
|
23918
|
-
stale.push({
|
|
23919
|
-
kind: "ceiling",
|
|
23920
|
-
metric: key,
|
|
23921
|
-
path,
|
|
23922
|
-
count
|
|
23923
|
-
});
|
|
23924
|
-
}
|
|
23925
|
-
}
|
|
23926
|
-
for (const key of EXEMPTABLE_METRIC_KEYS) for (const path of Object.keys(exempt[key] ?? {})) {
|
|
23927
|
-
if (wasMeasured(key, path, details)) continue;
|
|
23928
|
-
stale.push({
|
|
23929
|
-
kind: "exemption",
|
|
23930
|
-
metric: key,
|
|
23931
|
-
path
|
|
23932
|
-
});
|
|
24428
|
+
//#region src/codebase-health/size.ts
|
|
24429
|
+
function byteLength(absolute) {
|
|
24430
|
+
try {
|
|
24431
|
+
const stats = statSync(absolute);
|
|
24432
|
+
return stats.isFile() ? stats.size : void 0;
|
|
24433
|
+
} catch (_error) {
|
|
24434
|
+
return;
|
|
23933
24435
|
}
|
|
23934
|
-
return stale;
|
|
23935
24436
|
}
|
|
23936
|
-
|
|
23937
|
-
|
|
23938
|
-
const
|
|
23939
|
-
|
|
23940
|
-
|
|
23941
|
-
|
|
23942
|
-
const
|
|
23943
|
-
|
|
23944
|
-
|
|
24437
|
+
function measurerChanged(budget, changedNames) {
|
|
24438
|
+
const patterns = [...budget.files, "package.json"];
|
|
24439
|
+
const matches = picomatch(patterns, { dot: true });
|
|
24440
|
+
return changedNames.some((name) => matches(name));
|
|
24441
|
+
}
|
|
24442
|
+
function measureSizeBudgets(targetDir, budgets, options) {
|
|
24443
|
+
const measurements = [];
|
|
24444
|
+
for (const [id, budget] of Object.entries(budgets).toSorted(([a], [b]) => a.localeCompare(b))) measurements.push(measureOne(targetDir, id, budget, options));
|
|
24445
|
+
return measurements;
|
|
24446
|
+
}
|
|
24447
|
+
function measureOne(targetDir, id, budget, options) {
|
|
24448
|
+
if ("file" in budget) {
|
|
24449
|
+
const bytes = byteLength(path.join(targetDir, budget.file));
|
|
24450
|
+
if (bytes === void 0) return {
|
|
24451
|
+
id,
|
|
24452
|
+
unmeasured: `no file at ${budget.file}`
|
|
24453
|
+
};
|
|
23945
24454
|
return {
|
|
23946
|
-
|
|
23947
|
-
|
|
23948
|
-
|
|
23949
|
-
|
|
23950
|
-
|
|
23951
|
-
|
|
24455
|
+
id,
|
|
24456
|
+
measured: bytes
|
|
24457
|
+
};
|
|
24458
|
+
}
|
|
24459
|
+
if ("glob" in budget) {
|
|
24460
|
+
const sizes = [];
|
|
24461
|
+
for (const match of globSync(budget.glob, { cwd: targetDir })) {
|
|
24462
|
+
const bytes = byteLength(path.join(targetDir, match));
|
|
24463
|
+
if (bytes !== void 0) sizes.push(bytes);
|
|
24464
|
+
}
|
|
24465
|
+
if (sizes.length === 0) return {
|
|
24466
|
+
id,
|
|
24467
|
+
unmeasured: `glob ${budget.glob} matched no files`
|
|
24468
|
+
};
|
|
24469
|
+
let total = 0;
|
|
24470
|
+
let largest = 0;
|
|
24471
|
+
for (const bytes of sizes) {
|
|
24472
|
+
total += bytes;
|
|
24473
|
+
if (bytes > largest) largest = bytes;
|
|
24474
|
+
}
|
|
24475
|
+
return {
|
|
24476
|
+
id,
|
|
24477
|
+
measured: budget.aggregate === "largest" ? largest : total
|
|
23952
24478
|
};
|
|
23953
24479
|
}
|
|
23954
|
-
|
|
24480
|
+
const result = options.executor.exec("pnpm", [
|
|
24481
|
+
"run",
|
|
24482
|
+
"--silent",
|
|
24483
|
+
budget.script
|
|
24484
|
+
], { cwd: targetDir });
|
|
24485
|
+
if (result.exitCode !== 0) return {
|
|
24486
|
+
id,
|
|
24487
|
+
unmeasured: `script ${budget.script} exited ${String(result.exitCode)}`
|
|
24488
|
+
};
|
|
24489
|
+
const reported = readMeasureMarker(result.stdout);
|
|
24490
|
+
if (reported === void 0) return {
|
|
24491
|
+
id,
|
|
24492
|
+
unmeasured: `script ${budget.script} printed no measure marker`
|
|
24493
|
+
};
|
|
24494
|
+
if (!measurerChanged(budget, options.changedNames)) return {
|
|
24495
|
+
id,
|
|
24496
|
+
measured: reported
|
|
24497
|
+
};
|
|
24498
|
+
return {
|
|
24499
|
+
id,
|
|
24500
|
+
measured: reported,
|
|
24501
|
+
measurerChanged: true
|
|
24502
|
+
};
|
|
23955
24503
|
}
|
|
24504
|
+
//#endregion
|
|
24505
|
+
//#region src/codebase-health/measure.ts
|
|
24506
|
+
const BASELINE_FILE = "health-baseline.json";
|
|
23956
24507
|
function readTargetReference(executor, targetDir, out) {
|
|
23957
24508
|
const context = detectHealthContext(targetDir);
|
|
23958
24509
|
const read = readReference(executor, {
|
|
@@ -23964,32 +24515,171 @@ function readTargetReference(executor, targetDir, out) {
|
|
|
23964
24515
|
if (read.kind === "read" && read.warning !== void 0) out.notice(read.warning);
|
|
23965
24516
|
return read;
|
|
23966
24517
|
}
|
|
23967
|
-
function
|
|
23968
|
-
if (raises.length === 0) return
|
|
23969
|
-
|
|
23970
|
-
|
|
23971
|
-
|
|
23972
|
-
|
|
23973
|
-
|
|
23974
|
-
|
|
23975
|
-
|
|
23976
|
-
|
|
23977
|
-
|
|
23978
|
-
|
|
23979
|
-
|
|
23980
|
-
|
|
23981
|
-
|
|
23982
|
-
|
|
24518
|
+
function reportCarriedRaises(raises, out) {
|
|
24519
|
+
if (raises.length === 0) return;
|
|
24520
|
+
out.text(formatCarriedRaises(raises));
|
|
24521
|
+
out.text(formatWarnMarker(raises.length));
|
|
24522
|
+
}
|
|
24523
|
+
function assessRaises(executor, targetDir, current, out) {
|
|
24524
|
+
const read = readTargetReference(executor, targetDir, out);
|
|
24525
|
+
if (read.kind === "unavailable") throw new FatalError(read.reason);
|
|
24526
|
+
const reference = read.reference;
|
|
24527
|
+
if (reference.baseline === void 0) {
|
|
24528
|
+
out.notice(`${BASELINE_FILE} has no counterpart on the default branch yet, so there is nothing to raise against.`);
|
|
24529
|
+
return {
|
|
24530
|
+
fields: {
|
|
24531
|
+
reference: {
|
|
24532
|
+
status: "no-baseline",
|
|
24533
|
+
mergeBase: reference.mergeBase
|
|
24534
|
+
},
|
|
24535
|
+
raises: []
|
|
24536
|
+
},
|
|
24537
|
+
prefix: reference.prefix
|
|
24538
|
+
};
|
|
23983
24539
|
}
|
|
23984
24540
|
return {
|
|
23985
|
-
|
|
23986
|
-
|
|
23987
|
-
|
|
24541
|
+
fields: {
|
|
24542
|
+
reference: {
|
|
24543
|
+
status: "read",
|
|
24544
|
+
mergeBase: reference.mergeBase
|
|
24545
|
+
},
|
|
24546
|
+
raises: findRaises(reference, current)
|
|
23988
24547
|
},
|
|
23989
|
-
|
|
23990
|
-
raiseRefused
|
|
24548
|
+
prefix: reference.prefix
|
|
23991
24549
|
};
|
|
23992
24550
|
}
|
|
24551
|
+
function attributeRun(run) {
|
|
24552
|
+
const { result, executor, targetDir, fields, prefix, config } = run;
|
|
24553
|
+
if (!needsBaseMeasurement(result)) return attributeOutliers(result.uncoveredOutliers, { status: "not-needed" });
|
|
24554
|
+
const mergeBase = fields.reference.mergeBase;
|
|
24555
|
+
const measurement = measureBaseOutliers({
|
|
24556
|
+
executor,
|
|
24557
|
+
targetDir,
|
|
24558
|
+
mergeBase,
|
|
24559
|
+
prefix,
|
|
24560
|
+
config
|
|
24561
|
+
});
|
|
24562
|
+
const base = measurement.status === "measured" ? {
|
|
24563
|
+
status: "measured",
|
|
24564
|
+
mergeBase,
|
|
24565
|
+
ceilings: measurement.ceilings
|
|
24566
|
+
} : {
|
|
24567
|
+
status: "unavailable",
|
|
24568
|
+
mergeBase,
|
|
24569
|
+
reason: measurement.reason
|
|
24570
|
+
};
|
|
24571
|
+
return attributeOutliers(result.uncoveredOutliers, base);
|
|
24572
|
+
}
|
|
24573
|
+
function changedPathsForMeasurers(executor, targetDir, sizeBudgets, mergeBase, prefix) {
|
|
24574
|
+
if (!Object.values(sizeBudgets).some((budget) => "script" in budget)) return [];
|
|
24575
|
+
return branchDiffNames(executor, targetDir, mergeBase).filter((name) => name.startsWith(prefix)).map((name) => name.slice(prefix.length)).filter((name) => name !== "");
|
|
24576
|
+
}
|
|
24577
|
+
function refuseUnresolvedImports(details) {
|
|
24578
|
+
const entries = details.unresolvedFirstParty;
|
|
24579
|
+
if (entries !== void 0 && entries.length > 0) throw new FatalError(formatUnresolvedFirstParty(entries));
|
|
24580
|
+
}
|
|
24581
|
+
function refuseEmptyScope(details, config, failOnEmptySrc, out) {
|
|
24582
|
+
const { warnings, emptyScope } = formatSourceDiagnosis(details.sources, config.graphMetricsEnabled);
|
|
24583
|
+
for (const warning of warnings) out.notice(warning);
|
|
24584
|
+
if (emptyScope !== void 0 && failOnEmptySrc !== false) throw new FatalError(emptyScope);
|
|
24585
|
+
if (emptyScope !== void 0) out.notice(emptyScope);
|
|
24586
|
+
}
|
|
24587
|
+
function measureHealth(targetDir, config, out, opts = {}) {
|
|
24588
|
+
const executor = createRealHealthExecutor(targetDir);
|
|
24589
|
+
const details = collectDetails(executor, buildCollectConfig(config, { tsConfig: existsSync(path.join(targetDir, "tsconfig.json")) ? "tsconfig.json" : void 0 }));
|
|
24590
|
+
const current = metricsFromDetails(details);
|
|
24591
|
+
const ceilings = ceilingsFromDetails(details);
|
|
24592
|
+
refuseUnresolvedImports(details);
|
|
24593
|
+
refuseEmptyScope(details, config, opts.failOnEmptySrc, out);
|
|
24594
|
+
const baselinePath = path.join(targetDir, BASELINE_FILE);
|
|
24595
|
+
const treeBaseline = existsSync(baselinePath) ? parseHealthBaseline(readFileSync(baselinePath, "utf-8")) : void 0;
|
|
24596
|
+
const raiseAssessment = assessRaises(executor, targetDir, {
|
|
24597
|
+
baseline: treeBaseline,
|
|
24598
|
+
health: config,
|
|
24599
|
+
measured: {
|
|
24600
|
+
metrics: current,
|
|
24601
|
+
ceilings
|
|
24602
|
+
}
|
|
24603
|
+
}, out);
|
|
24604
|
+
const sizes = measureSizeBudgets(targetDir, config.sizeBudgets, {
|
|
24605
|
+
executor,
|
|
24606
|
+
changedNames: changedPathsForMeasurers(executor, targetDir, config.sizeBudgets, raiseAssessment.fields.reference.mergeBase, raiseAssessment.prefix)
|
|
24607
|
+
});
|
|
24608
|
+
if (treeBaseline === void 0) return {
|
|
24609
|
+
executor,
|
|
24610
|
+
details,
|
|
24611
|
+
current,
|
|
24612
|
+
ceilings,
|
|
24613
|
+
sizes,
|
|
24614
|
+
baselinePath,
|
|
24615
|
+
referenceFields: raiseAssessment.fields,
|
|
24616
|
+
prefix: raiseAssessment.prefix,
|
|
24617
|
+
existing: void 0
|
|
24618
|
+
};
|
|
24619
|
+
const result = compareMetrics(treeBaseline, current, config.tolerance, ceilings, sizes);
|
|
24620
|
+
const attribution = attributeRun({
|
|
24621
|
+
result,
|
|
24622
|
+
executor,
|
|
24623
|
+
targetDir,
|
|
24624
|
+
fields: raiseAssessment.fields,
|
|
24625
|
+
prefix: raiseAssessment.prefix,
|
|
24626
|
+
config
|
|
24627
|
+
});
|
|
24628
|
+
return {
|
|
24629
|
+
executor,
|
|
24630
|
+
details,
|
|
24631
|
+
current,
|
|
24632
|
+
ceilings,
|
|
24633
|
+
sizes,
|
|
24634
|
+
baselinePath,
|
|
24635
|
+
referenceFields: raiseAssessment.fields,
|
|
24636
|
+
prefix: raiseAssessment.prefix,
|
|
24637
|
+
existing: {
|
|
24638
|
+
baseline: treeBaseline,
|
|
24639
|
+
result,
|
|
24640
|
+
attribution
|
|
24641
|
+
}
|
|
24642
|
+
};
|
|
24643
|
+
}
|
|
24644
|
+
//#endregion
|
|
24645
|
+
//#region src/codebase-health/staleness.ts
|
|
24646
|
+
function wasMeasured(metric, path, details) {
|
|
24647
|
+
if (metric === "crowdedDirectories") return details.directories.has(path);
|
|
24648
|
+
return details.graphModules?.has(path) ?? true;
|
|
24649
|
+
}
|
|
24650
|
+
function exemptionWasMeasured(metric, key, details) {
|
|
24651
|
+
const measured = metric === "crowdedDirectories" ? details.directories : details.graphModules;
|
|
24652
|
+
if (measured === void 0) return true;
|
|
24653
|
+
const matches = exemptKeyMatcher(key);
|
|
24654
|
+
return [...measured].some(matches);
|
|
24655
|
+
}
|
|
24656
|
+
function findStaleEntries(baseline, details, exempt) {
|
|
24657
|
+
const stale = [];
|
|
24658
|
+
for (const key of RATCHETED_METRIC_KEYS) {
|
|
24659
|
+
const recorded = baseline.moduleCeilings?.[key];
|
|
24660
|
+
if (recorded === void 0) continue;
|
|
24661
|
+
for (const [path, count] of Object.entries(recorded)) {
|
|
24662
|
+
if (wasMeasured(key, path, details)) continue;
|
|
24663
|
+
stale.push({
|
|
24664
|
+
kind: "ceiling",
|
|
24665
|
+
metric: key,
|
|
24666
|
+
path,
|
|
24667
|
+
count
|
|
24668
|
+
});
|
|
24669
|
+
}
|
|
24670
|
+
}
|
|
24671
|
+
for (const key of EXEMPTABLE_METRIC_KEYS) for (const path of Object.keys(exempt[key] ?? {})) {
|
|
24672
|
+
if (exemptionWasMeasured(key, path, details)) continue;
|
|
24673
|
+
stale.push({
|
|
24674
|
+
kind: "exemption",
|
|
24675
|
+
metric: key,
|
|
24676
|
+
path
|
|
24677
|
+
});
|
|
24678
|
+
}
|
|
24679
|
+
return stale;
|
|
24680
|
+
}
|
|
24681
|
+
//#endregion
|
|
24682
|
+
//#region src/commands/health-check.ts
|
|
23993
24683
|
function assessSwitchOff(executor, targetDir, config, out) {
|
|
23994
24684
|
const read = readTargetReference(executor, targetDir, out);
|
|
23995
24685
|
if (read.kind === "unavailable") return void 0;
|
|
@@ -24000,17 +24690,28 @@ function assessSwitchOff(executor, targetDir, config, out) {
|
|
|
24000
24690
|
const named = healthOverridesAt(targetDir);
|
|
24001
24691
|
const limitsOver = { ...config };
|
|
24002
24692
|
for (const name of RAISABLE_LIMITS) if (named?.[name] === void 0) limitsOver[name] = reference.health[name];
|
|
24003
|
-
return
|
|
24004
|
-
|
|
24005
|
-
|
|
24006
|
-
|
|
24693
|
+
return {
|
|
24694
|
+
reference: {
|
|
24695
|
+
status: "read",
|
|
24696
|
+
mergeBase: reference.mergeBase
|
|
24697
|
+
},
|
|
24698
|
+
raises: findRaises(reference, {
|
|
24699
|
+
baseline: treeBaseline,
|
|
24700
|
+
health: limitsOver
|
|
24701
|
+
})
|
|
24702
|
+
};
|
|
24007
24703
|
}
|
|
24008
|
-
function
|
|
24009
|
-
const
|
|
24010
|
-
|
|
24704
|
+
function sizeValuesToRecord(baseline, sizes, reset) {
|
|
24705
|
+
const recorded = { ...baseline?.sizeBudgets };
|
|
24706
|
+
for (const measurement of sizes) {
|
|
24707
|
+
if (measurement.measured === void 0) continue;
|
|
24708
|
+
if (!reset && recorded[measurement.id] !== void 0) continue;
|
|
24709
|
+
recorded[measurement.id] = measurement.measured;
|
|
24710
|
+
}
|
|
24711
|
+
return Object.keys(recorded).length > 0 ? recorded : void 0;
|
|
24011
24712
|
}
|
|
24012
24713
|
function handleMissingBaseline(state, updateBaseline, failOnMissingBaseline) {
|
|
24013
|
-
const { baselinePath, config, details, current, ceilings, out } = state;
|
|
24714
|
+
const { baselinePath, config, details, current, ceilings, sizes, out } = state;
|
|
24014
24715
|
if (updateBaseline === true) {
|
|
24015
24716
|
if (config.graphMetricsEnabled && details.circularDependencies === void 0) {
|
|
24016
24717
|
out.notice(`Skipped baseline creation: graph metrics are unavailable this run. Fix the cruise before creating ${BASELINE_FILE}.`);
|
|
@@ -24023,7 +24724,7 @@ function handleMissingBaseline(state, updateBaseline, failOnMissingBaseline) {
|
|
|
24023
24724
|
process.exitCode = 1;
|
|
24024
24725
|
return;
|
|
24025
24726
|
}
|
|
24026
|
-
writeFileSync(baselinePath, serializeHealthBaseline(current, ceilings));
|
|
24727
|
+
writeFileSync(baselinePath, serializeHealthBaseline(current, ceilings, sizeValuesToRecord(void 0, sizes, false)));
|
|
24027
24728
|
out.notice(`Created ${BASELINE_FILE} (no prior baseline).`);
|
|
24028
24729
|
out.json({
|
|
24029
24730
|
status: "baseline-created",
|
|
@@ -24047,18 +24748,23 @@ function handleMissingBaseline(state, updateBaseline, failOnMissingBaseline) {
|
|
|
24047
24748
|
throw new FatalError(message);
|
|
24048
24749
|
}
|
|
24049
24750
|
function applyBaselineUpdate(state, baseline, reset) {
|
|
24050
|
-
const { baselinePath, config, details, current, ceilings, out } = state;
|
|
24751
|
+
const { baselinePath, config, details, current, ceilings, sizes, out } = state;
|
|
24051
24752
|
if (config.graphMetricsEnabled && details.circularDependencies === void 0) {
|
|
24052
24753
|
out.notice(`Skipped baseline update: graph metrics are unavailable this run. Fix the cruise before updating.`);
|
|
24053
24754
|
process.exitCode = 1;
|
|
24054
24755
|
} else if (reset === true) {
|
|
24055
|
-
writeFileSync(baselinePath, serializeHealthBaseline(current, ceilings));
|
|
24756
|
+
writeFileSync(baselinePath, serializeHealthBaseline(current, ceilings, sizeValuesToRecord(baseline, sizes, true)));
|
|
24056
24757
|
out.notice(`Reset ${BASELINE_FILE} to the current run.`);
|
|
24057
|
-
} else if (isFlatOrBetter(baseline, current, ceilings)) {
|
|
24058
|
-
writeFileSync(baselinePath, serializeHealthBaseline(current, ceilings));
|
|
24758
|
+
} else if (isFlatOrBetter(baseline, current, ceilings, sizes)) {
|
|
24759
|
+
writeFileSync(baselinePath, serializeHealthBaseline(current, ceilings, sizeValuesToRecord(baseline, sizes, false)));
|
|
24059
24760
|
out.notice(`Updated ${BASELINE_FILE} (no regressions).`);
|
|
24060
24761
|
} else out.notice(`Skipped baseline update: a metric or ceiling regressed. Fix the regression before updating.`);
|
|
24061
24762
|
}
|
|
24763
|
+
function collectRegressions(result, details) {
|
|
24764
|
+
const regressions = {};
|
|
24765
|
+
for (const m of result.metrics) if (!m.pass) regressions[m.key] = offenderEntries(details, m.key);
|
|
24766
|
+
return regressions;
|
|
24767
|
+
}
|
|
24062
24768
|
const healthCheckCommand = defineCommand({
|
|
24063
24769
|
meta: {
|
|
24064
24770
|
name: "health:check",
|
|
@@ -24091,7 +24797,7 @@ const healthCheckCommand = defineCommand({
|
|
|
24091
24797
|
},
|
|
24092
24798
|
json: {
|
|
24093
24799
|
type: "boolean",
|
|
24094
|
-
description: "Emit one JSON document: status (compared | disabled | baseline-created | missing-baseline | graph-unavailable). disabled carries reference
|
|
24800
|
+
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}."
|
|
24095
24801
|
}
|
|
24096
24802
|
},
|
|
24097
24803
|
run({ args }) {
|
|
@@ -24100,69 +24806,365 @@ const healthCheckCommand = defineCommand({
|
|
|
24100
24806
|
const config = resolveHealthConfig(targetDir);
|
|
24101
24807
|
if (!config.enabled) {
|
|
24102
24808
|
out.notice("health: disabled (set .tooling.json#health.enabled to true to enable).");
|
|
24809
|
+
const switchOff = assessSwitchOff(createRealHealthExecutor(targetDir), targetDir, config, out);
|
|
24103
24810
|
out.json({
|
|
24104
24811
|
status: "disabled",
|
|
24105
|
-
...
|
|
24812
|
+
...switchOff
|
|
24106
24813
|
});
|
|
24814
|
+
reportCarriedRaises(switchOff?.raises ?? [], out);
|
|
24107
24815
|
return;
|
|
24108
24816
|
}
|
|
24109
24817
|
if (!config.graphMetricsEnabled) out.notice("health: graph metrics disabled (no JS/TS source to cruise) -- crowdedDirectories only.");
|
|
24110
|
-
const
|
|
24111
|
-
const details = collectDetails(executor, buildCollectConfig(config, { tsConfig: existsSync(path.join(targetDir, "tsconfig.json")) ? "tsconfig.json" : void 0 }));
|
|
24112
|
-
const current = metricsFromDetails(details);
|
|
24113
|
-
const ceilings = ceilingsFromDetails(details);
|
|
24114
|
-
refuseUnresolvedImports(details);
|
|
24115
|
-
const { warnings, emptyScope } = formatSourceDiagnosis(details.sources, config.graphMetricsEnabled);
|
|
24116
|
-
for (const warning of warnings) out.notice(warning);
|
|
24117
|
-
if (emptyScope !== void 0 && args["fail-on-empty-src"] !== false) throw new FatalError(emptyScope);
|
|
24118
|
-
if (emptyScope !== void 0) out.notice(emptyScope);
|
|
24119
|
-
const baselinePath = path.join(targetDir, BASELINE_FILE);
|
|
24120
|
-
const treeBaseline = existsSync(baselinePath) ? parseHealthBaseline(readFileSync(baselinePath, "utf-8")) : void 0;
|
|
24121
|
-
const referenceFields = assessRaises(executor, targetDir, {
|
|
24122
|
-
baseline: treeBaseline,
|
|
24123
|
-
health: config,
|
|
24124
|
-
measured: {
|
|
24125
|
-
metrics: current,
|
|
24126
|
-
ceilings
|
|
24127
|
-
}
|
|
24128
|
-
}, out);
|
|
24818
|
+
const run = measureHealth(targetDir, config, out, { failOnEmptySrc: args["fail-on-empty-src"] });
|
|
24129
24819
|
const state = {
|
|
24130
|
-
baselinePath,
|
|
24820
|
+
baselinePath: run.baselinePath,
|
|
24131
24821
|
config,
|
|
24132
|
-
details,
|
|
24133
|
-
current,
|
|
24134
|
-
ceilings,
|
|
24822
|
+
details: run.details,
|
|
24823
|
+
current: run.current,
|
|
24824
|
+
ceilings: run.ceilings,
|
|
24825
|
+
sizes: run.sizes,
|
|
24135
24826
|
out,
|
|
24136
|
-
referenceFields
|
|
24827
|
+
referenceFields: run.referenceFields
|
|
24137
24828
|
};
|
|
24138
|
-
if (
|
|
24829
|
+
if (run.existing === void 0) {
|
|
24139
24830
|
handleMissingBaseline(state, args["update-baseline"], args["fail-on-missing-baseline"]);
|
|
24140
24831
|
return;
|
|
24141
24832
|
}
|
|
24142
|
-
const baseline =
|
|
24143
|
-
|
|
24144
|
-
|
|
24145
|
-
const regressions =
|
|
24146
|
-
|
|
24147
|
-
const graphUnavailable = formatGraphUnavailable(baseline, details, config.graphMetricsEnabled, details.graphDiagnostic);
|
|
24833
|
+
const { baseline, result, attribution } = run.existing;
|
|
24834
|
+
out.text(formatCompareDiff(result, run.details, attribution));
|
|
24835
|
+
reportCarriedRaises(run.referenceFields.raises, out);
|
|
24836
|
+
const regressions = collectRegressions(result, run.details);
|
|
24837
|
+
const graphUnavailable = formatGraphUnavailable(baseline, run.details, config.graphMetricsEnabled, run.details.graphDiagnostic);
|
|
24148
24838
|
if (graphUnavailable !== void 0) out.notice(graphUnavailable);
|
|
24149
|
-
const stale = findStaleEntries(baseline, details, config.exempt);
|
|
24839
|
+
const stale = findStaleEntries(baseline, run.details, config.exempt);
|
|
24150
24840
|
for (const warning of formatStaleEntries(stale)) out.notice(warning);
|
|
24151
24841
|
out.json({
|
|
24152
24842
|
status: "compared",
|
|
24153
|
-
...referenceFields,
|
|
24154
|
-
|
|
24843
|
+
...run.referenceFields,
|
|
24844
|
+
pass: result.pass,
|
|
24845
|
+
tolerance: result.tolerance,
|
|
24846
|
+
metrics: result.metrics,
|
|
24847
|
+
ceilings: result.ceilings,
|
|
24848
|
+
sizeBudgets: result.sizeResults,
|
|
24849
|
+
outlierBase: attribution.outlierBase,
|
|
24850
|
+
newOutliers: attribution.newOutliers,
|
|
24851
|
+
baseOutliers: attribution.baseOutliers,
|
|
24852
|
+
unattributedOutliers: attribution.unattributedOutliers,
|
|
24155
24853
|
graphMetricsEnabled: config.graphMetricsEnabled,
|
|
24156
24854
|
graphMetricsMissing: graphUnavailable !== void 0,
|
|
24157
24855
|
staleEntries: stale,
|
|
24158
24856
|
regressions,
|
|
24159
|
-
sources: details.sources
|
|
24857
|
+
sources: run.details.sources
|
|
24160
24858
|
});
|
|
24161
24859
|
if (args["update-baseline"] === true) applyBaselineUpdate(state, baseline, args.reset);
|
|
24162
24860
|
if (!result.pass || graphUnavailable !== void 0) process.exitCode = 1;
|
|
24163
24861
|
}
|
|
24164
24862
|
});
|
|
24165
24863
|
//#endregion
|
|
24864
|
+
//#region src/codebase-health/raise.ts
|
|
24865
|
+
const SIZE_RAISE_FACTOR = 1.01;
|
|
24866
|
+
function sizeRaiseValue(measured) {
|
|
24867
|
+
return Math.floor(measured * SIZE_RAISE_FACTOR) + 1;
|
|
24868
|
+
}
|
|
24869
|
+
const JsonObjectSchema = z.record(z.string(), z.unknown());
|
|
24870
|
+
const EXEMPT_LEAF_KEYS = [
|
|
24871
|
+
{
|
|
24872
|
+
prefix: "fan-in",
|
|
24873
|
+
key: "fanInOutliers"
|
|
24874
|
+
},
|
|
24875
|
+
{
|
|
24876
|
+
prefix: "fan-out",
|
|
24877
|
+
key: "fanOutOutliers"
|
|
24878
|
+
},
|
|
24879
|
+
{
|
|
24880
|
+
prefix: "crowding",
|
|
24881
|
+
key: "crowdedDirectories"
|
|
24882
|
+
}
|
|
24883
|
+
];
|
|
24884
|
+
function refuseDeniableReason(leaf, reason) {
|
|
24885
|
+
const fixed = fixText(`// ${reason}`, {
|
|
24886
|
+
offenders: resolveOffenders(),
|
|
24887
|
+
path: "health-raise.ts"
|
|
24888
|
+
});
|
|
24889
|
+
const refused = fixed.decisions.find((decision) => !decision.applied);
|
|
24890
|
+
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.`);
|
|
24891
|
+
return fixed.text.slice(3);
|
|
24892
|
+
}
|
|
24893
|
+
function parseExemptLeaf(leaf) {
|
|
24894
|
+
const separator = leaf.indexOf(":");
|
|
24895
|
+
const prefix = separator < 0 ? "" : leaf.slice(0, separator).trim();
|
|
24896
|
+
const path = separator < 0 ? "" : leaf.slice(separator + 1).trim();
|
|
24897
|
+
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.`);
|
|
24898
|
+
const named = EXEMPT_LEAF_KEYS.find((entry) => entry.prefix === prefix);
|
|
24899
|
+
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>".`);
|
|
24900
|
+
return {
|
|
24901
|
+
key: named.key,
|
|
24902
|
+
path
|
|
24903
|
+
};
|
|
24904
|
+
}
|
|
24905
|
+
function copyCeilings(ceilings) {
|
|
24906
|
+
const copy = {};
|
|
24907
|
+
for (const key of RATCHETED_METRIC_KEYS) {
|
|
24908
|
+
const byModule = ceilings?.[key];
|
|
24909
|
+
if (byModule !== void 0) copy[key] = { ...byModule };
|
|
24910
|
+
}
|
|
24911
|
+
return copy;
|
|
24912
|
+
}
|
|
24913
|
+
function writeCeiling(ceilings, key, module, to) {
|
|
24914
|
+
const byModule = ceilings[key] ?? {};
|
|
24915
|
+
byModule[module] = to;
|
|
24916
|
+
ceilings[key] = byModule;
|
|
24917
|
+
}
|
|
24918
|
+
function headlineFor(baseline, comparison, key, exemptCount) {
|
|
24919
|
+
const measured = comparison.metrics.find((metric) => metric.key === key)?.current;
|
|
24920
|
+
if (measured === void 0) return void 0;
|
|
24921
|
+
const remaining = Math.max(0, measured - exemptCount);
|
|
24922
|
+
return baseline[key] === void 0 ? remaining : Math.max(baseline[key] ?? 0, remaining);
|
|
24923
|
+
}
|
|
24924
|
+
function exemptionWrites(input) {
|
|
24925
|
+
const writes = [];
|
|
24926
|
+
const notes = [];
|
|
24927
|
+
for (const request of input.exemptions) {
|
|
24928
|
+
const parsed = parseExemptLeaf(request.leaf);
|
|
24929
|
+
if (parsed === void 0) continue;
|
|
24930
|
+
const recorded = input.config.exempt[parsed.key]?.[parsed.path];
|
|
24931
|
+
if (recorded !== void 0) {
|
|
24932
|
+
notes.push(`health.exempt.${parsed.key}.${parsed.path} already reads "${recorded}"; the reason was not overwritten.`);
|
|
24933
|
+
continue;
|
|
24934
|
+
}
|
|
24935
|
+
writes.push({
|
|
24936
|
+
key: parsed.key,
|
|
24937
|
+
path: parsed.path,
|
|
24938
|
+
reason: refuseDeniableReason(request.leaf, request.reason)
|
|
24939
|
+
});
|
|
24940
|
+
}
|
|
24941
|
+
return {
|
|
24942
|
+
writes,
|
|
24943
|
+
notes
|
|
24944
|
+
};
|
|
24945
|
+
}
|
|
24946
|
+
function objectAt(parent, key, where) {
|
|
24947
|
+
const existing = parent[key];
|
|
24948
|
+
if (existing === void 0) return {};
|
|
24949
|
+
const parsed = JsonObjectSchema.safeParse(existing);
|
|
24950
|
+
if (!parsed.success) throw new FatalError(`health:raise: ${where}#${key} is not an object, so an exemption cannot be written there.`);
|
|
24951
|
+
return { ...parsed.data };
|
|
24952
|
+
}
|
|
24953
|
+
function applyExemptions(tooling, writes) {
|
|
24954
|
+
if (writes.length === 0) return void 0;
|
|
24955
|
+
const file = JsonObjectSchema.safeParse(tooling ?? {});
|
|
24956
|
+
if (!file.success) throw new FatalError(`health:raise: .tooling.json is not a JSON object, so no exemption can be written.`);
|
|
24957
|
+
const result = { ...file.data };
|
|
24958
|
+
for (const write of writes) {
|
|
24959
|
+
const health = objectAt(result, "health", ".tooling.json");
|
|
24960
|
+
const exempt = objectAt(health, "exempt", ".tooling.json#health");
|
|
24961
|
+
const table = objectAt(exempt, write.key, ".tooling.json#health.exempt");
|
|
24962
|
+
table[write.path] = write.reason;
|
|
24963
|
+
exempt[write.key] = table;
|
|
24964
|
+
health["exempt"] = exempt;
|
|
24965
|
+
result["health"] = health;
|
|
24966
|
+
}
|
|
24967
|
+
return result;
|
|
24968
|
+
}
|
|
24969
|
+
function planHealthRaise(input) {
|
|
24970
|
+
const ceilings = copyCeilings(input.baseline.moduleCeilings);
|
|
24971
|
+
const ceilingWrites = [];
|
|
24972
|
+
const headlines = {};
|
|
24973
|
+
const notes = [];
|
|
24974
|
+
const exemptions = exemptionWrites(input);
|
|
24975
|
+
notes.push(...exemptions.notes);
|
|
24976
|
+
const isExcused = (key, module) => exemptions.writes.some((write) => write.key === key && exemptKeyMatcher(write.path)(module));
|
|
24977
|
+
const exemptedCount = /* @__PURE__ */ new Map();
|
|
24978
|
+
const countExcused = (key, module) => {
|
|
24979
|
+
if (isExcused(key, module)) exemptedCount.set(key, (exemptedCount.get(key) ?? 0) + 1);
|
|
24980
|
+
};
|
|
24981
|
+
for (const outlier of input.attribution.newOutliers) countExcused(outlier.key, outlier.module);
|
|
24982
|
+
for (const ceiling of input.comparison.ceilings) if (!ceiling.pass) countExcused(ceiling.key, ceiling.module);
|
|
24983
|
+
const outlierBase = input.attribution.outlierBase;
|
|
24984
|
+
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.`);
|
|
24985
|
+
for (const outlier of input.attribution.newOutliers) {
|
|
24986
|
+
if (isExcused(outlier.key, outlier.module)) continue;
|
|
24987
|
+
writeCeiling(ceilings, outlier.key, outlier.module, outlier.current);
|
|
24988
|
+
const headline = headlineFor(input.baseline, input.comparison, outlier.key, exemptedCount.get(outlier.key) ?? 0);
|
|
24989
|
+
if (headline !== void 0) headlines[outlier.key] = headline;
|
|
24990
|
+
ceilingWrites.push({
|
|
24991
|
+
key: outlier.key,
|
|
24992
|
+
module: outlier.module,
|
|
24993
|
+
value: outlier.current,
|
|
24994
|
+
...headline === void 0 ? {} : { headline }
|
|
24995
|
+
});
|
|
24996
|
+
}
|
|
24997
|
+
for (const ceiling of input.comparison.ceilings) {
|
|
24998
|
+
if (ceiling.pass) continue;
|
|
24999
|
+
if (isExcused(ceiling.key, ceiling.module)) continue;
|
|
25000
|
+
writeCeiling(ceilings, ceiling.key, ceiling.module, ceiling.current);
|
|
25001
|
+
ceilingWrites.push({
|
|
25002
|
+
key: ceiling.key,
|
|
25003
|
+
module: ceiling.module,
|
|
25004
|
+
value: ceiling.current
|
|
25005
|
+
});
|
|
25006
|
+
}
|
|
25007
|
+
const sizeBudgets = { ...input.baseline.sizeBudgets };
|
|
25008
|
+
const sizeWrites = [];
|
|
25009
|
+
for (const size of input.comparison.sizeResults) {
|
|
25010
|
+
if (size.pass || size.measured === void 0 || size.ceiling === void 0) continue;
|
|
25011
|
+
if (size.measured <= size.ceiling) continue;
|
|
25012
|
+
const value = sizeRaiseValue(size.measured);
|
|
25013
|
+
sizeBudgets[size.id] = value;
|
|
25014
|
+
sizeWrites.push({
|
|
25015
|
+
id: size.id,
|
|
25016
|
+
value
|
|
25017
|
+
});
|
|
25018
|
+
}
|
|
25019
|
+
return {
|
|
25020
|
+
baseline: widenedBaseline(input.baseline, headlines, ceilings, sizeBudgets),
|
|
25021
|
+
tooling: applyExemptions(input.tooling, exemptions.writes),
|
|
25022
|
+
ceilings: ceilingWrites,
|
|
25023
|
+
sizes: sizeWrites,
|
|
25024
|
+
exemptions: exemptions.writes,
|
|
25025
|
+
notes
|
|
25026
|
+
};
|
|
25027
|
+
}
|
|
25028
|
+
function widenedBaseline(baseline, headlines, ceilings, sizeBudgets) {
|
|
25029
|
+
const widened = { ...baseline };
|
|
25030
|
+
for (const key of METRIC_KEYS) {
|
|
25031
|
+
const headline = headlines[key];
|
|
25032
|
+
if (headline !== void 0) widened[key] = headline;
|
|
25033
|
+
}
|
|
25034
|
+
widened.moduleCeilings = Object.keys(ceilings).length > 0 ? ceilings : void 0;
|
|
25035
|
+
widened.sizeBudgets = Object.keys(sizeBudgets).length > 0 ? sizeBudgets : void 0;
|
|
25036
|
+
return widened;
|
|
25037
|
+
}
|
|
25038
|
+
function planHasWrites(plan) {
|
|
25039
|
+
return plan.ceilings.length > 0 || plan.sizes.length > 0 || plan.exemptions.length > 0;
|
|
25040
|
+
}
|
|
25041
|
+
//#endregion
|
|
25042
|
+
//#region src/commands/health-raise.ts
|
|
25043
|
+
const JsonValueSchema = z.json();
|
|
25044
|
+
const TOOLING_FILE = ".tooling.json";
|
|
25045
|
+
function collectExemptPairs(rawArgs) {
|
|
25046
|
+
const leaves = collectFlagValues(rawArgs, "--exempt");
|
|
25047
|
+
const reasons = collectFlagValues(rawArgs, "--reason");
|
|
25048
|
+
if (leaves.length === 0 && reasons.length > 0) throw new FatalError(`health:raise: ${String(reasons.length)} --reason given with no --exempt to explain.`);
|
|
25049
|
+
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.`);
|
|
25050
|
+
return leaves.map((leaf, index) => ({
|
|
25051
|
+
leaf,
|
|
25052
|
+
reason: reasons[index] ?? ""
|
|
25053
|
+
}));
|
|
25054
|
+
}
|
|
25055
|
+
function formatterFor(targetDir) {
|
|
25056
|
+
const detected = computeDefaults(targetDir);
|
|
25057
|
+
return createContentFormatter({
|
|
25058
|
+
targetDir,
|
|
25059
|
+
formatter: loadToolingConfig(targetDir)?.formatter ?? detected.formatter
|
|
25060
|
+
});
|
|
25061
|
+
}
|
|
25062
|
+
function readToolingFile(targetDir) {
|
|
25063
|
+
const full = path.join(targetDir, TOOLING_FILE);
|
|
25064
|
+
if (!existsSync(full)) return void 0;
|
|
25065
|
+
const text = readFileSync(full, "utf-8");
|
|
25066
|
+
let json;
|
|
25067
|
+
try {
|
|
25068
|
+
json = JSON.parse(text);
|
|
25069
|
+
} catch (error) {
|
|
25070
|
+
const reason = error instanceof Error ? error.message : "unknown error";
|
|
25071
|
+
throw new FatalError(`health:raise: ${TOOLING_FILE} is not JSON: ${reason}. Fix the file and re-run.`);
|
|
25072
|
+
}
|
|
25073
|
+
const parsed = JsonValueSchema.safeParse(json);
|
|
25074
|
+
if (!parsed.success) throw new FatalError(`health:raise: ${TOOLING_FILE} is not JSON: ${parsed.error.message}`);
|
|
25075
|
+
return parsed.data;
|
|
25076
|
+
}
|
|
25077
|
+
function describePlan(plan) {
|
|
25078
|
+
const lines = [];
|
|
25079
|
+
for (const ceiling of plan.ceilings) {
|
|
25080
|
+
const headline = ceiling.headline === void 0 ? "" : `, ${ceiling.key} set to ${String(ceiling.headline)}`;
|
|
25081
|
+
lines.push(`health-baseline: ${ceiling.key} ${ceiling.module} ceiling ${String(ceiling.value)}${headline}`);
|
|
25082
|
+
}
|
|
25083
|
+
for (const size of plan.sizes) lines.push(`health-baseline: size budget ${size.id} widened to ${String(size.value)}`);
|
|
25084
|
+
for (const exemption of plan.exemptions) lines.push(`${TOOLING_FILE}: health.exempt.${exemption.key}.${exemption.path} exempted -- ${exemption.reason}`);
|
|
25085
|
+
for (const note of plan.notes) lines.push(`health: ${note}`);
|
|
25086
|
+
return lines;
|
|
25087
|
+
}
|
|
25088
|
+
const healthRaiseCommand = defineCommand({
|
|
25089
|
+
meta: {
|
|
25090
|
+
name: "health:raise",
|
|
25091
|
+
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."
|
|
25092
|
+
},
|
|
25093
|
+
args: {
|
|
25094
|
+
dir: {
|
|
25095
|
+
type: "positional",
|
|
25096
|
+
valueHint: "dir",
|
|
25097
|
+
description: "Project directory (default: current directory)",
|
|
25098
|
+
required: false
|
|
25099
|
+
},
|
|
25100
|
+
"dry-run": {
|
|
25101
|
+
type: "boolean",
|
|
25102
|
+
description: "Print what would be written and write nothing"
|
|
25103
|
+
},
|
|
25104
|
+
exempt: {
|
|
25105
|
+
type: "string",
|
|
25106
|
+
description: "Exempt this leaf instead of recording a ceiling (repeatable), as \"health:check\" printed it: \"fan-in: src/a.ts\""
|
|
25107
|
+
},
|
|
25108
|
+
reason: {
|
|
25109
|
+
type: "string",
|
|
25110
|
+
description: "Why this exemption is justified (repeatable; one per --exempt)"
|
|
25111
|
+
},
|
|
25112
|
+
json: {
|
|
25113
|
+
type: "boolean",
|
|
25114
|
+
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})."
|
|
25115
|
+
}
|
|
25116
|
+
},
|
|
25117
|
+
run({ args, rawArgs }) {
|
|
25118
|
+
const targetDir = path.resolve(args.dir ?? ".");
|
|
25119
|
+
const out = createCliOutput(args.json === true);
|
|
25120
|
+
const config = resolveHealthConfig(targetDir);
|
|
25121
|
+
if (!config.enabled) {
|
|
25122
|
+
out.notice("health: disabled (set .tooling.json#health.enabled to true to enable).");
|
|
25123
|
+
out.json({
|
|
25124
|
+
status: "disabled",
|
|
25125
|
+
dryRun: args["dry-run"] === true
|
|
25126
|
+
});
|
|
25127
|
+
return;
|
|
25128
|
+
}
|
|
25129
|
+
const run = measureHealth(targetDir, config, out);
|
|
25130
|
+
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.");
|
|
25131
|
+
const plan = planHealthRaise({
|
|
25132
|
+
comparison: run.existing.result,
|
|
25133
|
+
attribution: run.existing.attribution,
|
|
25134
|
+
baseline: run.existing.baseline,
|
|
25135
|
+
config,
|
|
25136
|
+
tooling: readToolingFile(targetDir),
|
|
25137
|
+
exemptions: collectExemptPairs(rawArgs)
|
|
25138
|
+
});
|
|
25139
|
+
const dryRun = args["dry-run"] === true;
|
|
25140
|
+
const wrote = dryRun ? [] : writePlan(targetDir, plan);
|
|
25141
|
+
for (const line of describePlan(plan)) out.text(line);
|
|
25142
|
+
out.json({
|
|
25143
|
+
status: planHasWrites(plan) ? "raised" : "nothing-to-raise",
|
|
25144
|
+
dryRun,
|
|
25145
|
+
wrote,
|
|
25146
|
+
ceilings: plan.ceilings,
|
|
25147
|
+
sizeBudgets: plan.sizes,
|
|
25148
|
+
exemptions: plan.exemptions,
|
|
25149
|
+
notes: plan.notes,
|
|
25150
|
+
reference: run.referenceFields.reference
|
|
25151
|
+
});
|
|
25152
|
+
}
|
|
25153
|
+
});
|
|
25154
|
+
function writePlan(targetDir, plan) {
|
|
25155
|
+
const format = formatterFor(targetDir);
|
|
25156
|
+
const wrote = [];
|
|
25157
|
+
if (plan.ceilings.length > 0 || plan.sizes.length > 0) {
|
|
25158
|
+
writeFileSync(path.join(targetDir, "health-baseline.json"), format("health-baseline.json", serializeHealthBaseline(plan.baseline, plan.baseline.moduleCeilings, plan.baseline.sizeBudgets)));
|
|
25159
|
+
wrote.push("health-baseline.json");
|
|
25160
|
+
}
|
|
25161
|
+
if (plan.tooling !== void 0) {
|
|
25162
|
+
writeFileSync(path.join(targetDir, TOOLING_FILE), format(TOOLING_FILE, JSON.stringify(plan.tooling, null, 2)));
|
|
25163
|
+
wrote.push(TOOLING_FILE);
|
|
25164
|
+
}
|
|
25165
|
+
return wrote;
|
|
25166
|
+
}
|
|
25167
|
+
//#endregion
|
|
24166
25168
|
//#region src/codebase-health/module-report.ts
|
|
24167
25169
|
function normalizeModulePath(raw) {
|
|
24168
25170
|
let normalized = raw.replace(/\\/g, "/");
|
|
@@ -25196,16 +26198,17 @@ function locatePlan(input) {
|
|
|
25196
26198
|
kind: "found",
|
|
25197
26199
|
path: marked
|
|
25198
26200
|
};
|
|
25199
|
-
const added = addedPlans(entries)
|
|
25200
|
-
const [firstAdded, secondAdded] = added;
|
|
26201
|
+
const added = addedPlans(entries);
|
|
26202
|
+
const [firstAdded, secondAdded] = added.live;
|
|
25201
26203
|
if (firstAdded !== void 0 && secondAdded === void 0) return {
|
|
25202
26204
|
kind: "found",
|
|
25203
26205
|
path: firstAdded
|
|
25204
26206
|
};
|
|
25205
26207
|
if (firstAdded !== void 0 && secondAdded !== void 0) return {
|
|
25206
26208
|
kind: "ambiguous",
|
|
25207
|
-
candidates: added
|
|
26209
|
+
candidates: added.live
|
|
25208
26210
|
};
|
|
26211
|
+
if (added.retired.length > 0) return { kind: "none" };
|
|
25209
26212
|
const modified = modifiedLivePlans(entries).filter(isOpen);
|
|
25210
26213
|
const [firstModified, secondModified] = modified;
|
|
25211
26214
|
if (firstModified !== void 0 && secondModified === void 0) return {
|
|
@@ -25263,7 +26266,7 @@ function checkStatus(input, status) {
|
|
|
25263
26266
|
file,
|
|
25264
26267
|
rule: "R8 missing-historical-banner",
|
|
25265
26268
|
message: "Retired doc has no historical banner saying it is not current state.",
|
|
25266
|
-
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."
|
|
26269
|
+
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."
|
|
25267
26270
|
});
|
|
25268
26271
|
return violations;
|
|
25269
26272
|
}
|
|
@@ -25276,7 +26279,7 @@ function checkLinks(input) {
|
|
|
25276
26279
|
file: input.file,
|
|
25277
26280
|
rule: "R7 retired-source-link",
|
|
25278
26281
|
message: `Retired doc links out of \`docs/\` (\`${link.raw}\` -> ${link.target}). A file move elsewhere breaks it.`,
|
|
25279
|
-
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."
|
|
26282
|
+
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."
|
|
25280
26283
|
});
|
|
25281
26284
|
continue;
|
|
25282
26285
|
}
|
|
@@ -25303,7 +26306,7 @@ function checkDoc(input) {
|
|
|
25303
26306
|
if (!input.isRetired && input.staleDays > 0 && input.ageDays !== void 0 && input.ageDays > input.staleDays) violations.push({
|
|
25304
26307
|
file: input.file,
|
|
25305
26308
|
rule: "R5 stale",
|
|
25306
|
-
message: `Active doc, but
|
|
26309
|
+
message: `Active doc, but its content hasn't changed in ${String(input.ageDays)} days (limit ${String(input.staleDays)}).`,
|
|
25307
26310
|
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."
|
|
25308
26311
|
});
|
|
25309
26312
|
return violations;
|
|
@@ -25314,7 +26317,7 @@ const MS_PER_DAY$1 = 864e5;
|
|
|
25314
26317
|
const SPEC_DIR_BASENAME = "specs";
|
|
25315
26318
|
const PLAN_DIR_BASENAME = "plans";
|
|
25316
26319
|
function ageDays(exec, file, now) {
|
|
25317
|
-
const iso = exec.
|
|
26320
|
+
const iso = exec.lastContentChangeISO(file);
|
|
25318
26321
|
if (iso === void 0) return void 0;
|
|
25319
26322
|
const then = new Date(iso).getTime();
|
|
25320
26323
|
if (Number.isNaN(then)) return void 0;
|
|
@@ -25406,7 +26409,7 @@ function collectSpecRetirementViolations(exec, pairs, config, opts) {
|
|
|
25406
26409
|
file: spec,
|
|
25407
26410
|
rule: "R6 spec-not-retired",
|
|
25408
26411
|
message: `Implementing plan(s) have landed but this spec is still active: ${implementing.map((p) => p.file).join(", ")}.`,
|
|
25409
|
-
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."
|
|
26412
|
+
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."
|
|
25410
26413
|
};
|
|
25411
26414
|
violations.push({
|
|
25412
26415
|
...raw,
|
|
@@ -25434,7 +26437,7 @@ function collectPlanLocateViolations(exec) {
|
|
|
25434
26437
|
const reports = [];
|
|
25435
26438
|
if (located.kind === "none") reports.push(...touched.map((file) => ({
|
|
25436
26439
|
file,
|
|
25437
|
-
message: `The execute lookup finds no plan for this branch: ${file} was touched but not added, its name carries no issue-/pr- marker, and it has no outstanding task
|
|
26440
|
+
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.`
|
|
25438
26441
|
})));
|
|
25439
26442
|
else if (located.kind === "ambiguous") reports.push(...located.candidates.map((file) => ({
|
|
25440
26443
|
file,
|
|
@@ -25481,6 +26484,108 @@ function collectViolations(exec, config, opts) {
|
|
|
25481
26484
|
};
|
|
25482
26485
|
}
|
|
25483
26486
|
//#endregion
|
|
26487
|
+
//#region src/docs-check/content-age.ts
|
|
26488
|
+
const HEX_HASH = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/;
|
|
26489
|
+
function parseIgnoreRevs(text) {
|
|
26490
|
+
const hashes = /* @__PURE__ */ new Set();
|
|
26491
|
+
for (const rawLine of text.split("\n")) {
|
|
26492
|
+
const commentStart = rawLine.indexOf("#");
|
|
26493
|
+
const value = (commentStart === -1 ? rawLine : rawLine.slice(0, commentStart)).trim().toLowerCase();
|
|
26494
|
+
if (HEX_HASH.test(value)) hashes.add(value);
|
|
26495
|
+
}
|
|
26496
|
+
return hashes;
|
|
26497
|
+
}
|
|
26498
|
+
const FOLLOW_LOG_FORMAT = "%x1e%H%x1f%P%x1f%aI";
|
|
26499
|
+
function parseStatusLine(body) {
|
|
26500
|
+
for (const line of body.split("\n")) {
|
|
26501
|
+
if (line.trim() === "") continue;
|
|
26502
|
+
const fields = line.split(" ");
|
|
26503
|
+
const code = (fields[0] ?? "").charAt(0);
|
|
26504
|
+
const firstPath = fields[1] ?? "";
|
|
26505
|
+
if (code === "" || firstPath === "") return void 0;
|
|
26506
|
+
const secondPath = fields[2];
|
|
26507
|
+
if (code === "R" && secondPath !== void 0) return {
|
|
26508
|
+
code,
|
|
26509
|
+
oldPath: firstPath,
|
|
26510
|
+
newPath: secondPath
|
|
26511
|
+
};
|
|
26512
|
+
return {
|
|
26513
|
+
code,
|
|
26514
|
+
oldPath: firstPath,
|
|
26515
|
+
newPath: firstPath
|
|
26516
|
+
};
|
|
26517
|
+
}
|
|
26518
|
+
}
|
|
26519
|
+
function parseFollowLog(stdout) {
|
|
26520
|
+
const entries = [];
|
|
26521
|
+
for (const record of stdout.split("")) {
|
|
26522
|
+
if (record.trim() === "") continue;
|
|
26523
|
+
const lineEnd = record.indexOf("\n");
|
|
26524
|
+
const header = lineEnd === -1 ? record : record.slice(0, lineEnd);
|
|
26525
|
+
const body = lineEnd === -1 ? "" : record.slice(lineEnd + 1);
|
|
26526
|
+
const fields = header.split("");
|
|
26527
|
+
const commit = (fields[0] ?? "").trim();
|
|
26528
|
+
if (commit === "") continue;
|
|
26529
|
+
const parentsText = (fields[1] ?? "").trim();
|
|
26530
|
+
const parents = parentsText === "" ? [] : parentsText.split(/\s+/);
|
|
26531
|
+
const authorDate = (fields[2] ?? "").trim();
|
|
26532
|
+
entries.push({
|
|
26533
|
+
commit,
|
|
26534
|
+
parents,
|
|
26535
|
+
authorDate,
|
|
26536
|
+
status: parseStatusLine(body)
|
|
26537
|
+
});
|
|
26538
|
+
}
|
|
26539
|
+
return entries;
|
|
26540
|
+
}
|
|
26541
|
+
const FIRST_WINDOW = 4;
|
|
26542
|
+
const WINDOW_GROWTH = 8;
|
|
26543
|
+
function entryOutcome(git, context, entry) {
|
|
26544
|
+
if (entry.parents.length > 1) return {
|
|
26545
|
+
kind: "date",
|
|
26546
|
+
date: entry.authorDate
|
|
26547
|
+
};
|
|
26548
|
+
if (entry.parents.length === 0 || entry.status?.code === "A") {
|
|
26549
|
+
if (context.shallow.has(entry.commit)) return { kind: "unknown" };
|
|
26550
|
+
return {
|
|
26551
|
+
kind: "date",
|
|
26552
|
+
date: entry.authorDate
|
|
26553
|
+
};
|
|
26554
|
+
}
|
|
26555
|
+
if (context.ignore.has(entry.commit)) return { kind: "skip" };
|
|
26556
|
+
const status = entry.status;
|
|
26557
|
+
if (status !== void 0 && (status.code === "M" || status.code === "R")) {
|
|
26558
|
+
const diff = git.blobDiffStatus(entry.commit, status.oldPath, status.newPath);
|
|
26559
|
+
if (diff === 0) return { kind: "skip" };
|
|
26560
|
+
if (diff === 1) return {
|
|
26561
|
+
kind: "date",
|
|
26562
|
+
date: entry.authorDate
|
|
26563
|
+
};
|
|
26564
|
+
return { kind: "unknown" };
|
|
26565
|
+
}
|
|
26566
|
+
return {
|
|
26567
|
+
kind: "date",
|
|
26568
|
+
date: entry.authorDate
|
|
26569
|
+
};
|
|
26570
|
+
}
|
|
26571
|
+
function lastContentChange(git, rel, context) {
|
|
26572
|
+
let maxCount = FIRST_WINDOW;
|
|
26573
|
+
let walked = 0;
|
|
26574
|
+
for (;;) {
|
|
26575
|
+
const stdout = git.log(rel, maxCount);
|
|
26576
|
+
if (stdout === void 0) return void 0;
|
|
26577
|
+
const entries = parseFollowLog(stdout);
|
|
26578
|
+
for (const entry of entries.slice(walked)) {
|
|
26579
|
+
const outcome = entryOutcome(git, context, entry);
|
|
26580
|
+
if (outcome.kind === "date") return outcome.date;
|
|
26581
|
+
if (outcome.kind === "unknown") return void 0;
|
|
26582
|
+
}
|
|
26583
|
+
if (entries.length < maxCount) return void 0;
|
|
26584
|
+
walked = entries.length;
|
|
26585
|
+
maxCount *= WINDOW_GROWTH;
|
|
26586
|
+
}
|
|
26587
|
+
}
|
|
26588
|
+
//#endregion
|
|
25484
26589
|
//#region src/docs-check/executor.ts
|
|
25485
26590
|
function gitOut$1(rootDir, args) {
|
|
25486
26591
|
try {
|
|
@@ -25497,6 +26602,79 @@ function gitOut$1(rootDir, args) {
|
|
|
25497
26602
|
return;
|
|
25498
26603
|
}
|
|
25499
26604
|
}
|
|
26605
|
+
function gitStatus(rootDir, args) {
|
|
26606
|
+
try {
|
|
26607
|
+
execFileSync("git", args, {
|
|
26608
|
+
cwd: rootDir,
|
|
26609
|
+
stdio: [
|
|
26610
|
+
"ignore",
|
|
26611
|
+
"ignore",
|
|
26612
|
+
"ignore"
|
|
26613
|
+
]
|
|
26614
|
+
});
|
|
26615
|
+
return 0;
|
|
26616
|
+
} catch (error) {
|
|
26617
|
+
if (error instanceof Error && "status" in error && typeof error.status === "number") return error.status;
|
|
26618
|
+
return;
|
|
26619
|
+
}
|
|
26620
|
+
}
|
|
26621
|
+
function readShallowCommits(rootDir) {
|
|
26622
|
+
if (gitOut$1(rootDir, ["rev-parse", "--is-shallow-repository"])?.trim() !== "true") return /* @__PURE__ */ new Set();
|
|
26623
|
+
const listed = gitOut$1(rootDir, [
|
|
26624
|
+
"rev-parse",
|
|
26625
|
+
"--git-path",
|
|
26626
|
+
"shallow"
|
|
26627
|
+
])?.trim();
|
|
26628
|
+
if (listed === void 0 || listed === "") return /* @__PURE__ */ new Set();
|
|
26629
|
+
const file = path.isAbsolute(listed) ? listed : path.resolve(rootDir, listed);
|
|
26630
|
+
let text;
|
|
26631
|
+
try {
|
|
26632
|
+
text = readFileSync(file, "utf-8");
|
|
26633
|
+
} catch (_error) {
|
|
26634
|
+
return /* @__PURE__ */ new Set();
|
|
26635
|
+
}
|
|
26636
|
+
const hashes = /* @__PURE__ */ new Set();
|
|
26637
|
+
for (const line of text.split("\n")) {
|
|
26638
|
+
const hash = line.trim();
|
|
26639
|
+
if (hash !== "") hashes.add(hash);
|
|
26640
|
+
}
|
|
26641
|
+
return hashes;
|
|
26642
|
+
}
|
|
26643
|
+
function buildContentAgeSource(rootDir) {
|
|
26644
|
+
const topLevel = gitOut$1(rootDir, ["rev-parse", "--show-toplevel"])?.trim();
|
|
26645
|
+
if (topLevel === void 0 || topLevel === "") return void 0;
|
|
26646
|
+
let ignoreText = "";
|
|
26647
|
+
try {
|
|
26648
|
+
ignoreText = readFileSync(path.join(topLevel, ".git-blame-ignore-revs"), "utf-8");
|
|
26649
|
+
} catch (_error) {}
|
|
26650
|
+
return {
|
|
26651
|
+
git: {
|
|
26652
|
+
log: (rel, maxCount) => gitOut$1(rootDir, [
|
|
26653
|
+
"-c",
|
|
26654
|
+
"core.quotePath=false",
|
|
26655
|
+
"log",
|
|
26656
|
+
"--follow",
|
|
26657
|
+
`--max-count=${String(maxCount)}`,
|
|
26658
|
+
`--format=${FOLLOW_LOG_FORMAT}`,
|
|
26659
|
+
"--name-status",
|
|
26660
|
+
"--",
|
|
26661
|
+
rel
|
|
26662
|
+
]),
|
|
26663
|
+
blobDiffStatus: (commit, oldPath, newPath) => gitStatus(rootDir, [
|
|
26664
|
+
"diff",
|
|
26665
|
+
"--quiet",
|
|
26666
|
+
"-w",
|
|
26667
|
+
"--ignore-blank-lines",
|
|
26668
|
+
`${commit}^:${oldPath}`,
|
|
26669
|
+
`${commit}:${newPath}`
|
|
26670
|
+
])
|
|
26671
|
+
},
|
|
26672
|
+
context: {
|
|
26673
|
+
ignore: parseIgnoreRevs(ignoreText),
|
|
26674
|
+
shallow: readShallowCommits(rootDir)
|
|
26675
|
+
}
|
|
26676
|
+
};
|
|
26677
|
+
}
|
|
25500
26678
|
function resolveCommit$1(rootDir, ref) {
|
|
25501
26679
|
const commit = gitOut$1(rootDir, [
|
|
25502
26680
|
"rev-parse",
|
|
@@ -25534,6 +26712,8 @@ function branchNameStatus(rootDir) {
|
|
|
25534
26712
|
}
|
|
25535
26713
|
function createRealDocsExecutor(rootDir) {
|
|
25536
26714
|
const full = (rel) => path.join(rootDir, ...rel.split("/"));
|
|
26715
|
+
let contentAgeSource;
|
|
26716
|
+
let contentAgeRead = false;
|
|
25537
26717
|
return {
|
|
25538
26718
|
exists: (rel) => existsSync(full(rel)),
|
|
25539
26719
|
readFile(rel) {
|
|
@@ -25553,29 +26733,13 @@ function createRealDocsExecutor(rootDir) {
|
|
|
25553
26733
|
return;
|
|
25554
26734
|
}
|
|
25555
26735
|
},
|
|
25556
|
-
|
|
25557
|
-
|
|
25558
|
-
|
|
25559
|
-
|
|
25560
|
-
"log",
|
|
25561
|
-
"-1",
|
|
25562
|
-
"--format=%cI",
|
|
25563
|
-
"--",
|
|
25564
|
-
rel
|
|
25565
|
-
], {
|
|
25566
|
-
cwd: rootDir,
|
|
25567
|
-
encoding: "utf-8",
|
|
25568
|
-
stdio: [
|
|
25569
|
-
"ignore",
|
|
25570
|
-
"pipe",
|
|
25571
|
-
"ignore"
|
|
25572
|
-
]
|
|
25573
|
-
});
|
|
25574
|
-
} catch (_error) {
|
|
25575
|
-
return;
|
|
26736
|
+
lastContentChangeISO(rel) {
|
|
26737
|
+
if (!contentAgeRead) {
|
|
26738
|
+
contentAgeRead = true;
|
|
26739
|
+
contentAgeSource = buildContentAgeSource(rootDir);
|
|
25576
26740
|
}
|
|
25577
|
-
|
|
25578
|
-
return
|
|
26741
|
+
if (contentAgeSource === void 0) return void 0;
|
|
26742
|
+
return lastContentChange(contentAgeSource.git, rel, contentAgeSource.context);
|
|
25579
26743
|
},
|
|
25580
26744
|
branchNameStatus: () => branchNameStatus(rootDir)
|
|
25581
26745
|
};
|
|
@@ -25651,7 +26815,7 @@ function runDocsCheck(exec, config, opts) {
|
|
|
25651
26815
|
const docsCheckCommand = defineCommand({
|
|
25652
26816
|
meta: {
|
|
25653
26817
|
name: "docs:check",
|
|
25654
|
-
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/; 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."
|
|
26818
|
+
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."
|
|
25655
26819
|
},
|
|
25656
26820
|
args: {
|
|
25657
26821
|
dir: {
|
|
@@ -25944,7 +27108,7 @@ function runDocsDelink(exec, dirs, opts) {
|
|
|
25944
27108
|
const docsDelinkCommand = defineCommand({
|
|
25945
27109
|
meta: {
|
|
25946
27110
|
name: "docs:delink",
|
|
25947
|
-
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>..."
|
|
27111
|
+
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>..."
|
|
25948
27112
|
},
|
|
25949
27113
|
args: {
|
|
25950
27114
|
"dry-run": {
|
|
@@ -26146,7 +27310,7 @@ function runDocsRelocate(exec, sources, dest, opts) {
|
|
|
26146
27310
|
const docsRelocateCommand = defineCommand({
|
|
26147
27311
|
meta: {
|
|
26148
27312
|
name: "docs:relocate",
|
|
26149
|
-
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>."
|
|
27313
|
+
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>."
|
|
26150
27314
|
},
|
|
26151
27315
|
args: {
|
|
26152
27316
|
"dry-run": {
|
|
@@ -26165,9 +27329,9 @@ const docsRelocateCommand = defineCommand({
|
|
|
26165
27329
|
run({ args }) {
|
|
26166
27330
|
const out = createCliOutput(args.json === true);
|
|
26167
27331
|
const positionals = args._;
|
|
26168
|
-
if (positionals.length < 2) throw new FatalError("Usage: bst docs:relocate <src>... <dest>");
|
|
27332
|
+
if (positionals.length < 2) throw new FatalError("Usage: pnpm exec bst docs:relocate <src>... <dest>");
|
|
26169
27333
|
const dest = positionals.at(-1);
|
|
26170
|
-
if (dest === void 0) throw new FatalError("Usage: bst docs:relocate <src>... <dest>");
|
|
27334
|
+
if (dest === void 0) throw new FatalError("Usage: pnpm exec bst docs:relocate <src>... <dest>");
|
|
26171
27335
|
const sources = positionals.slice(0, -1);
|
|
26172
27336
|
const { payload, text, exitCode } = runDocsRelocate(createRealRelocateExecutor(process.cwd()), sources, dest, {
|
|
26173
27337
|
dryRun: args["dry-run"] === true,
|
|
@@ -26356,7 +27520,7 @@ function runDocsRetire(exec, src, opts) {
|
|
|
26356
27520
|
const docsRetireCommand = defineCommand({
|
|
26357
27521
|
meta: {
|
|
26358
27522
|
name: "docs:retire",
|
|
26359
|
-
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>."
|
|
27523
|
+
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>."
|
|
26360
27524
|
},
|
|
26361
27525
|
args: {
|
|
26362
27526
|
status: {
|
|
@@ -26392,7 +27556,7 @@ const docsRetireCommand = defineCommand({
|
|
|
26392
27556
|
run({ args }) {
|
|
26393
27557
|
const out = createCliOutput(args.json === true);
|
|
26394
27558
|
const [src] = args._;
|
|
26395
|
-
if (src === void 0) throw new FatalError("Usage: bst docs:retire <plan.md> --status <verb> --shipped <text>");
|
|
27559
|
+
if (src === void 0) throw new FatalError("Usage: pnpm exec bst docs:retire <plan.md> --status <verb> --shipped <text>");
|
|
26396
27560
|
const { payload, text, exitCode } = runDocsRetire(createRealRelocateExecutor(process.cwd()), src, {
|
|
26397
27561
|
status: args.status,
|
|
26398
27562
|
date: args.date,
|
|
@@ -26460,7 +27624,7 @@ async function runIssueView(executor, options) {
|
|
|
26460
27624
|
const resolve = resolverFor(executor, options.cwd, () => {
|
|
26461
27625
|
debug(options, `issue:view #${String(parseIssueRef(options.ref).number)}`);
|
|
26462
27626
|
});
|
|
26463
|
-
const view = await withNumberExample("bst issue:view 273", () => viewIssue(resolve, {
|
|
27627
|
+
const view = await withNumberExample("pnpm exec bst issue:view 273", () => viewIssue(resolve, {
|
|
26464
27628
|
ref: options.ref,
|
|
26465
27629
|
...options.repo === void 0 ? {} : { repo: options.repo }
|
|
26466
27630
|
}));
|
|
@@ -26490,7 +27654,7 @@ async function runIssueComment(executor, options) {
|
|
|
26490
27654
|
const resolve = resolverFor(executor, options.cwd, () => {
|
|
26491
27655
|
debug(options, `issue:comment #${String(parseIssueRef(options.ref).number)}`);
|
|
26492
27656
|
});
|
|
26493
|
-
return withNumberExample("bst issue:comment 273", () => commentOnIssue(resolve, {
|
|
27657
|
+
return withNumberExample("pnpm exec bst issue:comment 273", () => commentOnIssue(resolve, {
|
|
26494
27658
|
ref: options.ref,
|
|
26495
27659
|
body: options.body,
|
|
26496
27660
|
dryRun: options.dryRun,
|
|
@@ -26503,7 +27667,7 @@ async function runIssueUpdate(executor, options) {
|
|
|
26503
27667
|
debug(options, `issue:update #${String(parseIssueRef(options.ref).number)} [${changed.join(", ")}]`);
|
|
26504
27668
|
});
|
|
26505
27669
|
try {
|
|
26506
|
-
return await withNumberExample("bst issue:update 273 --title '...'", () => updateIssue(resolve, {
|
|
27670
|
+
return await withNumberExample("pnpm exec bst issue:update 273 --title '...'", () => updateIssue(resolve, {
|
|
26507
27671
|
ref: options.ref,
|
|
26508
27672
|
dryRun: options.dryRun,
|
|
26509
27673
|
...options.title === void 0 ? {} : { title: options.title },
|
|
@@ -26950,7 +28114,7 @@ async function runIssueDownload(executor, options) {
|
|
|
26950
28114
|
...options.repo === void 0 ? {} : { repo: options.repo }
|
|
26951
28115
|
});
|
|
26952
28116
|
} catch (error) {
|
|
26953
|
-
if (error instanceof IssueNumberRequiredError) throw new FatalError(describeIssueNumberRequired("bst issue:download 273"));
|
|
28117
|
+
if (error instanceof IssueNumberRequiredError) throw new FatalError(describeIssueNumberRequired("pnpm exec bst issue:download 273"));
|
|
26954
28118
|
if (error instanceof AttachmentExistsError) throw new FatalError(describeAttachmentExists(error, "--force"));
|
|
26955
28119
|
throw error;
|
|
26956
28120
|
}
|
|
@@ -27103,7 +28267,7 @@ async function runIssueLabel(executor, options) {
|
|
|
27103
28267
|
} catch (error) {
|
|
27104
28268
|
if (error instanceof IssueNumberRequiredError) {
|
|
27105
28269
|
const command = options.action === "add" ? "issue:label" : "issue:unlabel";
|
|
27106
|
-
throw new FatalError(describeIssueNumberRequired(`bst ${command} 273 agent/implement`));
|
|
28270
|
+
throw new FatalError(describeIssueNumberRequired(`pnpm exec bst ${command} 273 agent/implement`));
|
|
27107
28271
|
}
|
|
27108
28272
|
throw error;
|
|
27109
28273
|
}
|
|
@@ -27184,7 +28348,7 @@ async function runIssueSetLabels(executor, options) {
|
|
|
27184
28348
|
...options.repo === void 0 ? {} : { repo: options.repo }
|
|
27185
28349
|
});
|
|
27186
28350
|
} catch (error) {
|
|
27187
|
-
if (error instanceof IssueNumberRequiredError) throw new FatalError(describeIssueNumberRequired("bst issue:set-labels 273 agent/plan"));
|
|
28351
|
+
if (error instanceof IssueNumberRequiredError) throw new FatalError(describeIssueNumberRequired("pnpm exec bst issue:set-labels 273 agent/plan"));
|
|
27188
28352
|
if (error instanceof NoLabelsGivenError) throw new FatalError(describeNoLabelsGiven(error, "--clear"));
|
|
27189
28353
|
throw error;
|
|
27190
28354
|
}
|
|
@@ -27359,7 +28523,7 @@ async function runIssueState(executor, options) {
|
|
|
27359
28523
|
...options.repo === void 0 ? {} : { repo: options.repo }
|
|
27360
28524
|
});
|
|
27361
28525
|
} catch (error) {
|
|
27362
|
-
if (error instanceof IssueNumberRequiredError) throw new FatalError(describeIssueNumberRequired(`bst issue:${options.action} 273`));
|
|
28526
|
+
if (error instanceof IssueNumberRequiredError) throw new FatalError(describeIssueNumberRequired(`pnpm exec bst issue:${options.action} 273`));
|
|
27363
28527
|
if (error instanceof EmptyCommentError) throw new FatalError(describeEmptyComment({
|
|
27364
28528
|
text: "--comment",
|
|
27365
28529
|
file: "--comment-file"
|
|
@@ -27505,7 +28669,7 @@ function resolveBody(bodyFile) {
|
|
|
27505
28669
|
const findingsAddCommand = defineCommand({
|
|
27506
28670
|
meta: {
|
|
27507
28671
|
name: "findings:add",
|
|
27508
|
-
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."
|
|
28672
|
+
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."
|
|
27509
28673
|
},
|
|
27510
28674
|
args: {
|
|
27511
28675
|
class: {
|
|
@@ -27689,7 +28853,7 @@ function runFindingsResolve(rootDir, names, opts) {
|
|
|
27689
28853
|
const findingsResolveCommand = defineCommand({
|
|
27690
28854
|
meta: {
|
|
27691
28855
|
name: "findings:resolve",
|
|
27692
|
-
description: "Delete finding file(s) from .agent/findings/, or --defer to append a note instead. bst findings:resolve <name>... [--defer <note>]."
|
|
28856
|
+
description: "Delete finding file(s) from .agent/findings/, or --defer to append a note instead. pnpm exec bst findings:resolve <name>... [--defer <note>]."
|
|
27693
28857
|
},
|
|
27694
28858
|
args: {
|
|
27695
28859
|
defer: {
|
|
@@ -28035,7 +29199,12 @@ function renderStatus(report) {
|
|
|
28035
29199
|
const next = report.next ?? "(none)";
|
|
28036
29200
|
const task = report.nextTask === null || taskLabelId(next) === report.nextTask ? "" : `Task ${report.nextTask} -- `;
|
|
28037
29201
|
const lines = [`${report.plan}${report.retired ? " (retired)" : ""}: ${String(report.checked)} done, ${String(report.outstanding)} outstanding, ${String(report.declined)} declined`, `next: ${task}${next}${report.onlyRetireLeft ? " -- only the retire task is left" : ""}`];
|
|
29202
|
+
if (report.deferred > 0) lines.splice(1, 0, `${String(report.deferred)} deferred to a later task`);
|
|
28038
29203
|
for (const note of report.notes) lines.push(` [${note.kind}] ${note.task}: ${note.text}`);
|
|
29204
|
+
for (const deferral of report.deferrals) {
|
|
29205
|
+
if (deferral.failed === null) continue;
|
|
29206
|
+
lines.push(` [deferred -- not counted] ${deferral.task}: ${describeDeferralCheck(deferral.failed)}.`);
|
|
29207
|
+
}
|
|
28039
29208
|
return lines.join("\n");
|
|
28040
29209
|
}
|
|
28041
29210
|
const planStatusCommand = defineCommand({
|
|
@@ -28047,7 +29216,7 @@ const planStatusCommand = defineCommand({
|
|
|
28047
29216
|
plan: PLAN_ARG,
|
|
28048
29217
|
json: {
|
|
28049
29218
|
type: "boolean",
|
|
28050
|
-
description: "Emit one JSON document: plan, retired, checked, outstanding, declined, next (or null), nextTask (a task label string such as \"3\" or \"18a\", or null), onlyRetireLeft, notes ([{task, kind (progress | blocked | declined), text}]). next is null when there is none; nextTask is null when no task label encloses it."
|
|
29219
|
+
description: "Emit one JSON document: plan, retired, checked, outstanding, declined, deferred, deferrals ([{task, target (a task label string or null), failed (empty-reason | no-target-label | no-own-task | target-not-found | target-not-later | target-is-retire | no-carried-box, or null when valid)}]), next (or null), nextTask (a task label string such as \"3\" or \"18a\", or null), onlyRetireLeft, notes ([{task, kind (progress | blocked | declined | deferred), text}]). next is null when there is none; nextTask is null when no task label encloses it."
|
|
28051
29220
|
}
|
|
28052
29221
|
},
|
|
28053
29222
|
run({ args }) {
|
|
@@ -28220,7 +29389,7 @@ async function runCli(config) {
|
|
|
28220
29389
|
//#region src/cli-main.ts
|
|
28221
29390
|
setLogLevel(resolveLogLevel({}));
|
|
28222
29391
|
setForgejoLogger(log$1);
|
|
28223
|
-
const displayVersion = "0.
|
|
29392
|
+
const displayVersion = "0.96.0";
|
|
28224
29393
|
const bstSubCommands = {
|
|
28225
29394
|
"repo:sync": syncCommand,
|
|
28226
29395
|
"checks:run": runChecksCommand,
|
|
@@ -28250,6 +29419,7 @@ const bstSubCommands = {
|
|
|
28250
29419
|
"coverage:record": coverageRecordCommand,
|
|
28251
29420
|
"coverage:status": coverageStatusCommand,
|
|
28252
29421
|
"health:check": healthCheckCommand,
|
|
29422
|
+
"health:raise": healthRaiseCommand,
|
|
28253
29423
|
"health:summary": healthSummaryCommand,
|
|
28254
29424
|
"health:record": healthRecordCommand,
|
|
28255
29425
|
"health:status": healthStatusCommand,
|