@bensandee/tooling 0.92.0 → 0.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bin.mjs +725 -199
- package/package.json +1 -1
- package/tooling.schema.json +44 -5
package/dist/bin.mjs
CHANGED
|
@@ -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.93.0"
|
|
179
179
|
});
|
|
180
180
|
//#endregion
|
|
181
181
|
//#region ../cli-kit/src/logger.ts
|
|
@@ -772,13 +772,16 @@ const HealthConfigSchema = z.strictObject({
|
|
|
772
772
|
tolerance: z.number().int().nonnegative().optional().meta({ description: "Slack in offender counts before an increase is a regression: a metric fails when the current count exceeds baseline + tolerance (default 0, so any increase fails). The same allowance applies to every per-module ceiling in health-baseline.json's moduleCeilings, not only the headline metrics. A count, not a percentage -- coverage's percentage-point slack is `coverage.tolerancePp`. Raising it un-tracks every module beneath the new line at once, which is why a justified `exempt` entry is the better answer to one stubborn hub." }),
|
|
773
773
|
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. 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: it travels alone in a ceiling-only pull request, never beside code. An exempt module stays fully in the graph -- its edges still count toward every other module's fan-in/fan-out." })
|
|
774
774
|
});
|
|
775
|
-
const
|
|
776
|
-
enabled: z.boolean().optional().meta({ description: "
|
|
777
|
-
exclude: z.array(z.string()).optional().meta({ description: "Additional path globs
|
|
775
|
+
const CharsConfigSchema = z.strictObject({
|
|
776
|
+
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." }),
|
|
777
|
+
exclude: z.array(z.string()).optional().meta({ description: "Additional path globs the chars:check scan skips. Added to the built-in defaults (docs/**/retired/**, **/CHANGELOG.md, and the two generated .claude/hooks/bst-hook files), not a replacement for them." }),
|
|
778
778
|
deny: z.array(z.string()).optional().meta({ description: "Named offender categories to add to the default deny-list (e.g. \"math\")" }),
|
|
779
779
|
allow: z.array(z.string()).optional().meta({ description: "Codepoints (as U+XXXX) to drop from the effective deny-list" }),
|
|
780
780
|
treatAsProse: z.array(z.string()).optional().meta({ description: "Path globs (matched the same way as exclude) whose files --fix classifies as prose wholesale regardless of extension, flattening rather than flagging. The opt-back-in for an extension with no dedicated lexical-context classifier where blind flattening is known safe. No built-in defaults." })
|
|
781
781
|
});
|
|
782
|
+
function isCharsGateOn(block) {
|
|
783
|
+
return block?.enabled !== false;
|
|
784
|
+
}
|
|
782
785
|
const LintConfigSchema = z.strictObject({ strictTypeAware: z.boolean().optional().meta({ description: "Make a missing oxlint-tsgolint binary a hard failure for `bst lint:types` (the type-aware oxlint gate) rather than a notice (default false): arming it is what turns a platform gap into a build break, deliberately, because the repo that armed it is the one that asked for it. Findings block in every repo regardless of this setting." }) });
|
|
783
786
|
const ChecksConfigSchema = z.strictObject({
|
|
784
787
|
add: z.array(z.string()).optional().meta({ description: "Extra package.json scripts to run as first-class check steps, appended to the built-in list (e.g. ['lint:php']). An added step with no matching script is a hard failure." }),
|
|
@@ -1775,19 +1778,20 @@ const ToolingConfigSchema = z.strictObject({
|
|
|
1775
1778
|
coverage: z.union([z.literal(false), CoverageConfigSchema]).optional().meta({ description: "Coverage tracking configuration. Set to false to disable; omit for default-on (when a vitest config is present)." }),
|
|
1776
1779
|
health: HealthConfigSchema.optional().meta({ description: "Codebase-health gate configuration. Opt-in: set enabled true to gate structural metrics (crowded directories, circular deps, fan-in/out) against health-baseline.json." }),
|
|
1777
1780
|
egress: EgressConfigSchema.optional().meta({ description: "Egress-audit history configuration, consumed by the bst-agent egress:* commands. There is no enabled key: egress:summary and egress:check read the transcripts root unconditionally, so history: 'none' is the only off-switch, and it disables egress:record alone." }),
|
|
1778
|
-
|
|
1781
|
+
chars: CharsConfigSchema.optional().meta({ description: "Character gate (bst chars:check) configuration. On by default: the gate scans tracked text files for LLM-tell punctuation and invisible or deceptive characters, and passes letters in any language. Set enabled false to opt out." }),
|
|
1782
|
+
ascii: CharsConfigSchema.optional().meta({ description: "Deprecated alias of chars, read until 2026-12-15" }),
|
|
1779
1783
|
docs: z.union([z.literal(false), DocsConfigSchema]).optional().meta({ description: "Plan/spec lifecycle gate (`bst docs:check`). Overrides only -- dirs are discovered by convention. Set to false to disable. Violations are advisory until a pair is named in `enforced`." }),
|
|
1780
1784
|
deploy: DeployConfigSchema.optional().meta({ description: "Generate a deploy-dispatch step in the release workflow that triggers a workflow_dispatch event in another repo after a successful release. The resolved release token must have rights on `dispatchRepo`." }),
|
|
1781
1785
|
pnpmWorkspace: z.literal(false).optional().meta({ description: "Set to false to opt out of pnpm-workspace.yaml management (skips both generation and the repo:sync --check validation)." }),
|
|
1782
1786
|
defaultBranch: DefaultBranchNameSchema.optional().meta({ description: "Repository default branch name, B. Drives the generated CI `on.push` trigger and every default-branch gate in `ci.yml` and `release.yml`, the release path (`release:changesets`, the version PR, `release:trigger`) and `checks:run`'s coverage gate. Generated output uses 'main' when unset; `checks:run` still falls back to `origin/HEAD`. Must be a plain branch name: ASCII letters, digits, '.', '_', '-' and '/'." }),
|
|
1783
1787
|
unmanagedDevDependencies: UnmanagedDevDependenciesSchema.optional().meta({ description: "devDependency names repo:sync must never add or re-pin. Escape hatch for holding a tooling-managed dep (oxlint, vitest, knip, ...) at a deliberately off-canonical version; without it, repo:sync raises any managed devDep whose version has drifted below the canonical pin." }),
|
|
1784
|
-
stack: StackSchema.optional().meta({ description: "Whether the repo has TS/JS source. Detected from source evidence; TypeScript is never inferred from absence." }),
|
|
1788
|
+
stack: StackSchema.optional().meta({ description: "Whether the repo has TS/JS source. Detected from source evidence; TypeScript is never inferred from absence. Setting this key pins its value, even one that equals detection, and repo:sync keeps it until the key is deleted." }),
|
|
1785
1789
|
builder: z.enum([
|
|
1786
1790
|
"tsdown",
|
|
1787
1791
|
"vite",
|
|
1788
1792
|
"custom",
|
|
1789
1793
|
"none"
|
|
1790
|
-
]).optional().meta({ description: "What compiles the project. Detected from the package.json build script, then config-file presence." }),
|
|
1794
|
+
]).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." }),
|
|
1791
1795
|
checks: ChecksConfigSchema.optional().meta({ description: "Check-pipeline overrides: extra steps to add, and steps to skip (unioned with CI defaults)." }),
|
|
1792
1796
|
lint: LintConfigSchema.optional().meta({ description: "bst lint:types (the type-aware oxlint gate) overrides. See lint.strictTypeAware." }),
|
|
1793
1797
|
agent: AgentConfigSchema.optional().meta({ description: "agent:start behaviour overrides. Opt-in: see agent.autonomy for the interior-autonomy gate." }),
|
|
@@ -1801,7 +1805,8 @@ const TolerantToolingConfigSchema = z.object({
|
|
|
1801
1805
|
dockerCheck: z.union([z.literal(false), TolerantDockerCheckConfigSchema]).optional(),
|
|
1802
1806
|
coverage: z.union([z.literal(false), tolerantBlock(CoverageConfigSchema)]).optional(),
|
|
1803
1807
|
health: tolerantBlock(HealthConfigSchema).optional(),
|
|
1804
|
-
|
|
1808
|
+
chars: tolerantBlock(CharsConfigSchema).optional(),
|
|
1809
|
+
ascii: tolerantBlock(CharsConfigSchema).optional(),
|
|
1805
1810
|
docs: z.union([z.literal(false), tolerantBlock(DocsConfigSchema)]).optional(),
|
|
1806
1811
|
deploy: tolerantBlock(DeployConfigSchema).optional(),
|
|
1807
1812
|
checks: tolerantBlock(ChecksConfigSchema).optional(),
|
|
@@ -1851,7 +1856,13 @@ function parseToolingConfigStrict(raw, displayPath) {
|
|
|
1851
1856
|
function loadToolingConfigStrict(targetDir) {
|
|
1852
1857
|
const file = readConfigFile(targetDir);
|
|
1853
1858
|
if (!file) return void 0;
|
|
1854
|
-
|
|
1859
|
+
const config = parseToolingConfigStrict(file.raw, file.displayPath);
|
|
1860
|
+
refuseRenamedCharsKey(config, file.displayPath);
|
|
1861
|
+
return config;
|
|
1862
|
+
}
|
|
1863
|
+
function refuseRenamedCharsKey(config, displayPath) {
|
|
1864
|
+
if (config.chars === void 0 || config.ascii === void 0) return;
|
|
1865
|
+
throw new FatalError(`${displayPath} sets both chars and ascii; rename ascii to chars`);
|
|
1855
1866
|
}
|
|
1856
1867
|
const OVERRIDE_KEYS = [
|
|
1857
1868
|
"structure",
|
|
@@ -1879,6 +1890,18 @@ function parseExistingConfig(raw) {
|
|
|
1879
1890
|
} catch (_error) {}
|
|
1880
1891
|
return {};
|
|
1881
1892
|
}
|
|
1893
|
+
function pruneCharsDefaults(value) {
|
|
1894
|
+
const parsed = RawJsonObjectSchema.safeParse(value);
|
|
1895
|
+
if (!parsed.success) return value;
|
|
1896
|
+
if (parsed.data["enabled"] !== true) return value;
|
|
1897
|
+
const kept = {};
|
|
1898
|
+
for (const [key, entry] of Object.entries(parsed.data)) {
|
|
1899
|
+
if (key === "enabled") continue;
|
|
1900
|
+
kept[key] = entry;
|
|
1901
|
+
}
|
|
1902
|
+
if (Object.keys(kept).length === 0) return void 0;
|
|
1903
|
+
return kept;
|
|
1904
|
+
}
|
|
1882
1905
|
function saveToolingConfig(ctx, config) {
|
|
1883
1906
|
const defaults = computeDefaults(config.targetDir);
|
|
1884
1907
|
const isMonorepo = config.structure === "monorepo";
|
|
@@ -1888,10 +1911,16 @@ function saveToolingConfig(ctx, config) {
|
|
|
1888
1911
|
const result = { $schema: SCHEMA_REF };
|
|
1889
1912
|
for (const key of OVERRIDE_KEYS) {
|
|
1890
1913
|
if (isMonorepo && MONOREPO_IGNORED_KEYS.has(key)) continue;
|
|
1891
|
-
if (config[key] !== defaults[key]) result[key] = config[key];
|
|
1914
|
+
if (Object.hasOwn(existingJson, key) && existingJson[key] === config[key] || config[key] !== defaults[key]) result[key] = config[key];
|
|
1892
1915
|
}
|
|
1893
1916
|
for (const [key, value] of Object.entries(existingJson)) {
|
|
1894
1917
|
if (key === "$schema" || overrideKeySet.has(key)) continue;
|
|
1918
|
+
if (key === "chars" || key === "ascii") {
|
|
1919
|
+
const pruned = pruneCharsDefaults(value);
|
|
1920
|
+
if (pruned === void 0) continue;
|
|
1921
|
+
result[key] = pruned;
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1895
1924
|
result[key] = value;
|
|
1896
1925
|
}
|
|
1897
1926
|
const content = JSON.stringify(result, null, 2) + "\n";
|
|
@@ -1900,7 +1929,7 @@ function saveToolingConfig(ctx, config) {
|
|
|
1900
1929
|
action: "skipped",
|
|
1901
1930
|
description: "Already up to date"
|
|
1902
1931
|
};
|
|
1903
|
-
ctx.write(CONFIG_FILE, content);
|
|
1932
|
+
ctx.write(CONFIG_FILE, ctx.format(CONFIG_FILE, content));
|
|
1904
1933
|
return {
|
|
1905
1934
|
filePath: CONFIG_FILE,
|
|
1906
1935
|
action: existing ? "updated" : "created",
|
|
@@ -5349,8 +5378,24 @@ function reclaimManagedDevDeps(ctx, pkg, managedDevDeps, unmanaged, changes) {
|
|
|
5349
5378
|
}
|
|
5350
5379
|
pkg.devDependencies = existingDevDeps;
|
|
5351
5380
|
}
|
|
5352
|
-
function
|
|
5353
|
-
|
|
5381
|
+
function migrateAsciiCheckScript(pkg, changes) {
|
|
5382
|
+
const scripts = pkg.scripts;
|
|
5383
|
+
if (scripts === void 0) return;
|
|
5384
|
+
const legacy = scripts["ascii:check"];
|
|
5385
|
+
if (legacy === void 0) return;
|
|
5386
|
+
if (legacy === "bst ascii:check") {
|
|
5387
|
+
delete scripts["ascii:check"];
|
|
5388
|
+
changes.push("renamed script: ascii:check -> chars:check");
|
|
5389
|
+
return;
|
|
5390
|
+
}
|
|
5391
|
+
log$1.warn(`package.json: the ascii:check script is not the generated one, so the rename to chars:check leaves it alone. Rename it by hand: "ascii:check": "${legacy}" becomes "chars:check": "bst chars:check".`);
|
|
5392
|
+
}
|
|
5393
|
+
function isCharsCheckEnabled(ctx) {
|
|
5394
|
+
const slice = parseToolingConfigSlice(ctx.read(".tooling.json"), z.object({
|
|
5395
|
+
chars: CharsConfigSchema.optional(),
|
|
5396
|
+
ascii: CharsConfigSchema.optional()
|
|
5397
|
+
}), { block: "chars" });
|
|
5398
|
+
return isCharsGateOn(slice?.chars ?? slice?.ascii);
|
|
5354
5399
|
}
|
|
5355
5400
|
function applyMetadataDefaults(pkg, ctx, changes) {
|
|
5356
5401
|
const existingDeps = pkg.dependencies ?? {};
|
|
@@ -5425,7 +5470,7 @@ function buildManagedScripts(ctx) {
|
|
|
5425
5470
|
allScripts["docker:check"] = "bst docker:check";
|
|
5426
5471
|
}
|
|
5427
5472
|
if (resolveHealthConfigFromContext(ctx, { isMonorepo: ctx.config.structure === "monorepo" }).enabled) allScripts["health:check"] = "bst health:check";
|
|
5428
|
-
if (
|
|
5473
|
+
if (isCharsCheckEnabled(ctx)) allScripts["chars:check"] = "bst chars:check";
|
|
5429
5474
|
return allScripts;
|
|
5430
5475
|
}
|
|
5431
5476
|
function buildManagedDevDeps(ctx) {
|
|
@@ -5433,7 +5478,7 @@ function buildManagedDevDeps(ctx) {
|
|
|
5433
5478
|
const devDeps = { ...rootDevDeps(ctx.config) };
|
|
5434
5479
|
if (!isMonorepo) Object.assign(devDeps, perPackageDevDeps(ctx.config));
|
|
5435
5480
|
if (ctx.config.stack !== "none" || ctx.config.ci === "forgejo") devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.15.0";
|
|
5436
|
-
if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.
|
|
5481
|
+
if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.93.0";
|
|
5437
5482
|
if (ctx.config.useEslintPlugin && ctx.config.stack !== "none") devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.17.0";
|
|
5438
5483
|
if (wantsAgentCli(ctx)) devDeps[AGENT_CLI_PACKAGE] = "0.1.2";
|
|
5439
5484
|
if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
|
|
@@ -5495,6 +5540,7 @@ async function generatePackageJson(ctx) {
|
|
|
5495
5540
|
pkg.type = "module";
|
|
5496
5541
|
changes.push("set type: \"module\"");
|
|
5497
5542
|
}
|
|
5543
|
+
migrateAsciiCheckScript(pkg, changes);
|
|
5498
5544
|
mergeManagedScripts(pkg, allScripts, changes);
|
|
5499
5545
|
const forwardingHint = diagnoseCheckScriptForwarding(pkg.scripts ?? {});
|
|
5500
5546
|
if (forwardingHint) log$1.warn(`package.json: pnpm-forwarded check flags may be dropped. ${forwardingHint}`);
|
|
@@ -5587,10 +5633,30 @@ function reportStaleConfig(filePath, reason) {
|
|
|
5587
5633
|
}
|
|
5588
5634
|
//#endregion
|
|
5589
5635
|
//#region src/generators/tsconfig.ts
|
|
5590
|
-
|
|
5636
|
+
const DEFAULT_INCLUDE = [
|
|
5637
|
+
"src",
|
|
5638
|
+
"test",
|
|
5639
|
+
"*.ts"
|
|
5640
|
+
];
|
|
5641
|
+
const LEGACY_INCLUDE = ["src"];
|
|
5642
|
+
function formatInclude(include) {
|
|
5643
|
+
return `[${include.map((item) => JSON.stringify(item)).join(", ")}]`;
|
|
5644
|
+
}
|
|
5645
|
+
function describeInclude(include) {
|
|
5646
|
+
return `added include: ${formatInclude(include)}`;
|
|
5647
|
+
}
|
|
5648
|
+
function warnIfNarrowInclude(ctx, filePath, include) {
|
|
5649
|
+
if (!Array.isArray(include) || include.length !== 1 || include[0] !== "src") return;
|
|
5650
|
+
const testDir = path.join(path.dirname(filePath), "test");
|
|
5651
|
+
if (!ctx.exists(testDir)) return;
|
|
5652
|
+
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)}.`);
|
|
5653
|
+
}
|
|
5654
|
+
function buildGeneratedTsconfig(ctx, extendsValue, options = {}) {
|
|
5655
|
+
const dir = options.dir ?? ".";
|
|
5656
|
+
const include = options.include ?? DEFAULT_INCLUDE;
|
|
5591
5657
|
const config = {
|
|
5592
5658
|
extends: extendsValue,
|
|
5593
|
-
...ctx.exists("src") ? { include: [
|
|
5659
|
+
...ctx.exists(path.join(dir, "src")) ? { include: [...include] } : {}
|
|
5594
5660
|
};
|
|
5595
5661
|
return JSON.stringify(config, null, 2) + "\n";
|
|
5596
5662
|
}
|
|
@@ -5600,7 +5666,9 @@ async function generateTsconfig(ctx) {
|
|
|
5600
5666
|
if (ctx.config.structure === "monorepo") return [generateMonorepoRootTsconfig(ctx), ...ctx.config.detectPackageTypes ? generateMonorepoPackageTsconfigs(ctx) : []];
|
|
5601
5667
|
const extendsValue = `@bensandee/config/tsconfig/${ctx.config.projectType}`;
|
|
5602
5668
|
if (ctx.config.stack !== "typescript") {
|
|
5603
|
-
|
|
5669
|
+
const current = buildGeneratedTsconfig(ctx, extendsValue);
|
|
5670
|
+
const legacy = buildGeneratedTsconfig(ctx, extendsValue, { include: LEGACY_INCLUDE });
|
|
5671
|
+
if (existing !== void 0 && (existing === current || existing === legacy)) return [reportStaleConfig(filePath, `stack is ${ctx.config.stack}`)];
|
|
5604
5672
|
return [{
|
|
5605
5673
|
filePath,
|
|
5606
5674
|
action: "skipped",
|
|
@@ -5655,6 +5723,7 @@ function mergeSingleTsconfig(ctx, filePath, extendsValue) {
|
|
|
5655
5723
|
description: "Ignored via tooling:ignore comment"
|
|
5656
5724
|
};
|
|
5657
5725
|
const parsed = parseTsconfig(existing);
|
|
5726
|
+
warnIfNarrowInclude(ctx, filePath, parsed.include);
|
|
5658
5727
|
const changes = [];
|
|
5659
5728
|
if (!parsed.extends) {
|
|
5660
5729
|
parsed.extends = extendsValue;
|
|
@@ -5664,8 +5733,8 @@ function mergeSingleTsconfig(ctx, filePath, extendsValue) {
|
|
|
5664
5733
|
const tsconfigDir = path.dirname(filePath);
|
|
5665
5734
|
const srcDir = tsconfigDir === "." ? "src" : path.join(tsconfigDir, "src");
|
|
5666
5735
|
if (ctx.exists(srcDir)) {
|
|
5667
|
-
parsed.include = [
|
|
5668
|
-
changes.push(
|
|
5736
|
+
parsed.include = [...DEFAULT_INCLUDE];
|
|
5737
|
+
changes.push(describeInclude(DEFAULT_INCLUDE));
|
|
5669
5738
|
}
|
|
5670
5739
|
}
|
|
5671
5740
|
if (changes.length === 0) return {
|
|
@@ -5724,6 +5793,7 @@ function generateMonorepoPackageTsconfigs(ctx) {
|
|
|
5724
5793
|
}
|
|
5725
5794
|
continue;
|
|
5726
5795
|
}
|
|
5796
|
+
warnIfNarrowInclude(ctx, filePath, parsed.include);
|
|
5727
5797
|
const changes = [];
|
|
5728
5798
|
if (parsed.extends !== extendsValue) {
|
|
5729
5799
|
const prev = parsed.extends;
|
|
@@ -5731,8 +5801,8 @@ function generateMonorepoPackageTsconfigs(ctx) {
|
|
|
5731
5801
|
changes.push(prev ? `changed extends: ${String(prev)} -> ${extendsValue}` : `added extends: ${extendsValue}`);
|
|
5732
5802
|
}
|
|
5733
5803
|
if (!parsed.include && ctx.exists(path.join(relDir, "src"))) {
|
|
5734
|
-
parsed.include = [
|
|
5735
|
-
changes.push(
|
|
5804
|
+
parsed.include = [...DEFAULT_INCLUDE];
|
|
5805
|
+
changes.push(describeInclude(DEFAULT_INCLUDE));
|
|
5736
5806
|
}
|
|
5737
5807
|
if (changes.length === 0) {
|
|
5738
5808
|
results.push({
|
|
@@ -5749,11 +5819,7 @@ function generateMonorepoPackageTsconfigs(ctx) {
|
|
|
5749
5819
|
description: changes.join(", ")
|
|
5750
5820
|
});
|
|
5751
5821
|
} else {
|
|
5752
|
-
|
|
5753
|
-
extends: extendsValue,
|
|
5754
|
-
...ctx.exists(path.join(relDir, "src")) ? { include: ["src"] } : {}
|
|
5755
|
-
};
|
|
5756
|
-
ctx.write(filePath, JSON.stringify(config, null, 2) + "\n");
|
|
5822
|
+
ctx.write(filePath, buildGeneratedTsconfig(ctx, extendsValue, { dir: relDir }));
|
|
5757
5823
|
results.push({
|
|
5758
5824
|
filePath,
|
|
5759
5825
|
action: "created",
|
|
@@ -8349,8 +8415,8 @@ function writeEmbedded(ctx, file) {
|
|
|
8349
8415
|
}
|
|
8350
8416
|
const NOT_BUILT = "run `pnpm build` from the repo root";
|
|
8351
8417
|
async function generateClaudeHooks(ctx, embedded = {
|
|
8352
|
-
hook: "/**\n * bst-hook -- Claude Code PreToolUse hook\n * @bensandee/claude-hooks\n * https://github.com/bensandee/tooling\n *\n * Auto-generated by @bensandee/tooling repo:sync -- do not edit manually.\n */\nimport * as fs from \"node:fs\";\nimport { existsSync, readFileSync, readdirSync, realpathSync, statSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { execFileSync } from \"node:child_process\";\n\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/util.js\nfunction joinValues(array, separator = \"|\") {\n return array.map((val) => stringifyPrimitive(val)).join(separator);\n}\nfunction jsonStringifyReplacer(_, value) {\n if (typeof value === \"bigint\") return value.toString();\n return value;\n}\nvar Cached = class {\n constructor(getter) {\n this._getter = getter;\n this._value = void 0;\n }\n get value() {\n const getter = this._getter;\n if (getter !== void 0) {\n this._value = getter();\n this._getter = void 0;\n }\n return this._value;\n }\n};\nfunction cached(getter) {\n return new Cached(getter);\n}\nfunction nullish(input) {\n return input === null || input === void 0;\n}\nfunction cleanRegex(source) {\n const start = source.startsWith(\"^\") ? 1 : 0;\n const end = source.endsWith(\"$\") ? source.length - 1 : source.length;\n return source.slice(start, end);\n}\nfunction assignProp(target, prop, value) {\n Object.defineProperty(target, prop, {\n value,\n writable: true,\n enumerable: true,\n configurable: true\n });\n}\nconst captureStackTrace = \"captureStackTrace\" in Error ? Error.captureStackTrace : (..._args) => {};\nfunction isObject(data) {\n return typeof data === \"object\" && data !== null && !Array.isArray(data);\n}\nfunction isPlainObject(o) {\n if (isObject(o) === false) return false;\n const ctor = o.constructor;\n if (ctor === void 0) return true;\n if (typeof ctor !== \"function\") return true;\n const prot = ctor.prototype;\n if (isObject(prot) === false) return false;\n if (Object.prototype.hasOwnProperty.call(prot, \"isPrototypeOf\") === false) return false;\n return true;\n}\nfunction shallowClone(o) {\n if (isPlainObject(o)) return { ...o };\n if (Array.isArray(o)) return [...o];\n if (o instanceof Map) return new Map(o);\n if (o instanceof Set) return new Set(o);\n return o;\n}\nfunction clone(inst, def, params) {\n const cl = new inst._zod.constr(def ?? inst._zod.def);\n if (!def || params?.parent) cl._zod.parent = inst;\n return cl;\n}\nfunction normalizeParams(_params) {\n const params = _params;\n if (!params) return {};\n if (typeof params === \"string\") return { error: () => params };\n if (params?.message !== void 0) {\n if (params?.error !== void 0) throw new Error(\"Cannot specify both `message` and `error` params\");\n params.error = params.message;\n }\n delete params.message;\n if (typeof params.error === \"string\") return {\n ...params,\n error: () => params.error\n };\n return params;\n}\nfunction stringifyPrimitive(value) {\n if (typeof value === \"bigint\") return value.toString() + \"n\";\n if (typeof value === \"string\") return `\"${value}\"`;\n return `${value}`;\n}\nfunction optionalKeys(shape) {\n return Object.keys(shape).filter((k) => {\n return shape[k]._zod.optin !== void 0 && shape[k]._zod.optout === \"optional\";\n });\n}\nfunction aborted(x, startIndex = 0) {\n if (x.aborted === true) return true;\n for (let i = startIndex; i < x.issues.length; i++) if (x.issues[i]?.continue !== true) return true;\n return false;\n}\nfunction explicitlyAborted(x, startIndex = 0) {\n if (x.aborted === true) return true;\n for (let i = startIndex; i < x.issues.length; i++) if (x.issues[i]?.continue === false) return true;\n return false;\n}\nfunction prefixIssues(path, issues) {\n return issues.map((iss) => {\n var _a;\n (_a = iss).path ?? (_a.path = []);\n iss.path.unshift(path);\n return iss;\n });\n}\nfunction unwrapMessage(message) {\n return typeof message === \"string\" ? message : message?.message;\n}\nfunction attachSchema(issues, start, inst) {\n var _a;\n for (let i = start; i < issues.length; i++) (_a = issues[i]).schema ?? (_a.schema = inst);\n}\nfunction finalizeIssue(iss, ctx, config) {\n var _a;\n const traits = iss.inst?._zod?.traits;\n if (traits?.has(\"$ZodType\")) {\n if (traits.has(\"$ZodCheck\")) (_a = iss).schema ?? (_a.schema = iss.inst);\n else iss.schema = iss.inst;\n }\n const schemaError = iss.schema !== iss.inst ? iss.schema?._zod.def?.error : void 0;\n const message = iss.message ? iss.message : unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ?? unwrapMessage(schemaError?.(iss)) ?? unwrapMessage(ctx?.error?.(iss)) ?? unwrapMessage(config.customError?.(iss)) ?? unwrapMessage(config.localeError?.(iss)) ?? \"Invalid input\";\n const full = {};\n for (const k of Object.keys(iss)) {\n if (k === \"inst\" || k === \"schema\" || k === \"continue\" || k === \"input\" || k === \"__proto__\") continue;\n full[k] = iss[k];\n }\n full.path ?? (full.path = []);\n full.message = message;\n if (ctx?.reportInput) full.input = iss.input;\n return full;\n}\nconst highSurrogate = /[\\uD800-\\uDBFF]/;\nfunction codePointLength(str) {\n const units = str.length;\n if (!highSurrogate.test(str)) return units;\n let count = units;\n for (let i = 0; i < units - 1; i++) if ((str.charCodeAt(i) & 64512) === 55296 && (str.charCodeAt(i + 1) & 64512) === 56320) {\n count--;\n i++;\n }\n return count;\n}\nfunction getLengthableOrigin(input) {\n if (Array.isArray(input)) return \"array\";\n if (typeof input === \"string\") return \"string\";\n return \"unknown\";\n}\nfunction parsedType(data) {\n const t = typeof data;\n switch (t) {\n case \"number\": return Number.isNaN(data) ? \"nan\" : \"number\";\n case \"object\": {\n if (data === null) return \"null\";\n if (Array.isArray(data)) return \"array\";\n const obj = data;\n if (obj && Object.getPrototypeOf(obj) !== Object.prototype && \"constructor\" in obj && obj.constructor) return obj.constructor.name;\n }\n }\n return t;\n}\nfunction members(proto, table) {\n for (const key in table) {\n const desc = Object.getOwnPropertyDescriptor(table, key);\n if (desc.get) Object.defineProperty(proto, key, {\n ...desc,\n enumerable: false\n });\n else defineBound(proto, key, desc.value);\n }\n}\nfunction own(inst, key, value, enumerable = true) {\n Object.defineProperty(inst, key, {\n configurable: true,\n writable: true,\n enumerable,\n value\n });\n return value;\n}\nfunction hide(inst, key, value) {\n return own(inst, key, value, false);\n}\nfunction defineBound(proto, key, fn) {\n Object.defineProperty(proto, key, {\n configurable: true,\n get() {\n return this == null ? fn : own(this, key, fn.bind(this));\n },\n set(value) {\n own(this, key, value);\n }\n });\n}\nfunction claim(inst, sentinel) {\n const proto = Object.getPrototypeOf(inst);\n return sentinel in proto ? void 0 : proto;\n}\nlet installing;\nlet broke = false;\nconst breaker = {\n configurable: true,\n get() {\n broke = true;\n }\n};\nfunction defineLazyInternal(inst, key, compute) {\n const proto = Object.getPrototypeOf(inst._zod);\n if (key in proto && installing !== inst._zod) {\n installing = void 0;\n return;\n }\n installing = inst._zod;\n Object.defineProperty(proto, key, {\n configurable: true,\n get() {\n Object.defineProperty(this, key, breaker);\n const outer = broke;\n broke = false;\n try {\n const value = compute(this);\n if (broke) delete this[key];\n else Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n value\n });\n broke = broke || outer;\n return value;\n } catch (err) {\n delete this[key];\n broke = broke || outer;\n throw err;\n }\n },\n set(value) {\n Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n value\n });\n }\n });\n}\nfunction installLazyProp(inst, key, make, enumerable) {\n const proto = claim(inst, key);\n if (!proto) return;\n Object.defineProperty(proto, key, {\n configurable: true,\n get() {\n const desc = {\n configurable: true,\n writable: true,\n enumerable,\n value: void 0\n };\n Object.defineProperty(this, key, desc);\n desc.value = make(this);\n Object.defineProperty(this, key, desc);\n return desc.value;\n },\n set(value) {\n Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n enumerable,\n value\n });\n }\n });\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/core.js\nvar _a;\nconst _zodDesc = {\n value: void 0,\n enumerable: false\n};\nlet _E = \"captureStackTrace\" in Error ? Error : null;\nfunction newError(Definition) {\n const E = _E;\n if (E) {\n const saved = E.stackTraceLimit;\n if (typeof saved === \"number\") {\n try {\n E.stackTraceLimit = 0;\n } catch {\n _E = null;\n return new Definition();\n }\n try {\n return new Definition();\n } finally {\n E.stackTraceLimit = saved;\n }\n }\n }\n return new Definition();\n}\nfunction $constructor(name, initializer, proto, params) {\n const zodProto = {};\n function Internals(def) {\n this.def = def;\n this.constr = _;\n this.traits = /* @__PURE__ */ new Set();\n }\n Internals.prototype = zodProto;\n const protoMembers = proto;\n const initialized = protoMembers && /* @__PURE__ */ new WeakSet();\n function init(inst, def) {\n if (!inst._zod) {\n _zodDesc.value = new Internals(def);\n try {\n Object.defineProperty(inst, \"_zod\", _zodDesc);\n } finally {\n _zodDesc.value = void 0;\n }\n } else if (inst._zod.traits.has(name)) return;\n inst._zod.traits.add(name);\n initializer(inst, def);\n if (initialized) {\n const own = Object.getPrototypeOf(inst);\n const ctorProto = inst._zod.constr.prototype;\n let up = own;\n while (up && up !== ctorProto) up = Object.getPrototypeOf(up);\n const target = up ?? own;\n if (!initialized.has(target)) {\n initialized.add(target);\n members(target, protoMembers);\n }\n }\n const proto = _.prototype;\n for (const k in proto) {\n if (!Object.prototype.hasOwnProperty.call(proto, k)) continue;\n if (!(k in inst)) inst[k] = proto[k].bind(inst);\n }\n }\n const Parent = params?.Parent ?? Object;\n class Definition extends Parent {}\n Object.defineProperty(Definition, \"name\", { value: name });\n function _(def) {\n const inst = params?.Parent ? newError(Definition) : this;\n init(inst, def);\n const deferred = inst._zod.deferred;\n if (deferred) {\n for (const fn of deferred) fn();\n inst._zod.deferred = void 0;\n }\n const pp = globalThis.__zod_globalConfig?.postProcessor;\n if (pp) pp(inst);\n return inst;\n }\n Object.defineProperty(_, \"init\", { value: init });\n Object.defineProperty(_, Symbol.hasInstance, { value: (inst) => {\n if (params?.Parent && inst instanceof params.Parent) return true;\n return inst?._zod?.traits?.has(name);\n } });\n Object.defineProperty(_, \"name\", { value: name });\n return _;\n}\nvar $ZodAsyncError = class extends Error {\n constructor() {\n super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`);\n }\n};\n(_a = globalThis).__zod_globalConfig ?? (_a.__zod_globalConfig = {});\nconst globalConfig = globalThis.__zod_globalConfig;\nfunction config(newConfig) {\n if (newConfig) Object.assign(globalConfig, newConfig);\n return globalConfig;\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/errors.js\nfunction _getMessage() {\n const internals = this._zod;\n internals.message ?? (internals.message = JSON.stringify(internals.def, jsonStringifyReplacer, 2));\n return internals.message;\n}\nfunction _setMessage(value) {\n this._zod.message = value;\n}\nconst _messageDesc = {\n get: _getMessage,\n set: _setMessage,\n enumerable: true,\n configurable: true\n};\nconst _issuesDesc = {\n value: void 0,\n enumerable: false\n};\nconst _installedToString = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]);\nconst initializer = (inst, def) => {\n inst.name = \"$ZodError\";\n _issuesDesc.value = def;\n Object.defineProperty(inst, \"issues\", _issuesDesc);\n _issuesDesc.value = void 0;\n Object.defineProperty(inst, \"message\", _messageDesc);\n const proto = Object.getPrototypeOf(inst);\n if (!_installedToString.has(proto)) {\n _installedToString.add(proto);\n Object.defineProperty(proto, \"toString\", {\n configurable: true,\n enumerable: false,\n get() {\n const value = () => this.message;\n Object.defineProperty(this, \"toString\", {\n value,\n configurable: true,\n writable: true\n });\n return value;\n },\n set(value) {\n Object.defineProperty(this, \"toString\", {\n value,\n configurable: true,\n writable: true\n });\n }\n });\n }\n};\nconst $ZodError = $constructor(\"$ZodError\", initializer);\nconst $ZodRealError = $constructor(\"$ZodError\", initializer, void 0, { Parent: Error });\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/parse.js\nconst _parse = (_Err) => {\n const fn = (schema, value, _ctx, _params) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: false\n } : { async: false };\n const result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) throw new $ZodAsyncError();\n if (result.issues.length) {\n const e = new ((_params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));\n captureStackTrace(e, _params?.callee ?? fn);\n throw e;\n }\n return result.value;\n };\n return fn;\n};\nconst parse = /* @__PURE__*/ _parse($ZodRealError);\nconst _parseAsync = (_Err) => {\n const fn = async (schema, value, _ctx, params) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: true\n } : { async: true };\n let result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) result = await result;\n if (result.issues.length) {\n const e = new ((params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));\n captureStackTrace(e, params?.callee ?? fn);\n throw e;\n }\n return result.value;\n };\n return fn;\n};\nconst parseAsync = /* @__PURE__*/ _parseAsync($ZodRealError);\nconst _safeParse = (_Err) => (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: false\n } : { async: false };\n const result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) throw new $ZodAsyncError();\n return result.issues.length ? failure(_Err, result.issues, ctx) : {\n success: true,\n data: result.value\n };\n};\nconst safeParse = /* @__PURE__*/ _safeParse($ZodRealError);\nfunction failure(Err, issues, ctx) {\n let error;\n return {\n success: false,\n get error() {\n if (!error) {\n error = new Err(issues.map((iss) => finalizeIssue(iss, ctx, config())));\n issues = void 0;\n ctx = void 0;\n }\n return error;\n },\n set error(e) {\n error = e;\n issues = void 0;\n ctx = void 0;\n }\n };\n}\nconst _safeParseAsync = (_Err) => async (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: true\n } : { async: true };\n let result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) result = await result;\n return result.issues.length ? failure(_Err, result.issues, ctx) : {\n success: true,\n data: result.value\n };\n};\nconst safeParseAsync = /* @__PURE__*/ _safeParseAsync($ZodRealError);\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/regexes.js\nconst anyString = /^[\\s\\S]{0,}$/;\nconst number = /^-?\\d+(?:\\.\\d+)?$/;\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/checks.js\nconst $ZodCheck = /*@__PURE__*/ $constructor(\"$ZodCheck\", (inst, def) => {\n var _a;\n inst._zod ?? (inst._zod = {});\n inst._zod.def = def;\n (_a = inst._zod).onattach ?? (_a.onattach = []);\n});\nconst _whenHasLength = (payload) => {\n const val = payload.value;\n return !nullish(val) && val.length !== void 0;\n};\nconst $ZodCheckMinLength = /*@__PURE__*/ $constructor(\"$ZodCheckMinLength\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = _whenHasLength);\n inst._zod.check = (payload) => {\n const input = payload.value;\n const units = input.length;\n if ((typeof input === \"string\" && units >= def.minimum && units < def.minimum * 2 ? codePointLength(input) : units) >= def.minimum) return;\n const origin = getLengthableOrigin(input);\n payload.issues.push({\n origin,\n code: \"too_small\",\n minimum: def.minimum,\n inclusive: true,\n input,\n inst,\n continue: !def.abort\n });\n };\n});\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/versions.js\nconst version = {\n major: 4,\n minor: 6,\n patch: 5\n};\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/schemas.js\nconst $ZodType = /*@__PURE__*/ $constructor(\"$ZodType\", (inst, def) => {\n var _a;\n inst ?? (inst = {});\n inst._zod.def = def;\n inst._zod.bag = inst._zod.bag || {};\n inst._zod.version = version;\n const defChecks = inst._zod.def.checks;\n const checks = inst._zod.traits.has(\"$ZodCheck\") ? [inst, ...defChecks ?? []] : defChecks?.length ? [...defChecks] : [];\n for (const ch of checks) for (const fn of ch._zod.onattach) fn(inst);\n if (checks.length === 0) {\n (_a = inst._zod).deferred ?? (_a.deferred = []);\n inst._zod.deferred?.push(() => {\n inst._zod.run = inst._zod.parse;\n });\n } else {\n const runChecks = (payload, checks, ctx) => {\n if (payload.memo) return payload;\n let isAborted = aborted(payload);\n let asyncResult;\n for (const ch of checks) {\n if (ch._zod.def.when) {\n if (explicitlyAborted(payload)) continue;\n if (!ch._zod.def.when(payload)) continue;\n } else if (isAborted) continue;\n const currLen = payload.issues.length;\n const _ = ch._zod.check(payload);\n if (_ instanceof Promise && ctx?.async === false) throw new $ZodAsyncError();\n if (asyncResult || _ instanceof Promise) asyncResult = (asyncResult ?? Promise.resolve()).then(async () => {\n await _;\n if (payload.issues.length === currLen) return;\n attachSchema(payload.issues, currLen, inst);\n if (!isAborted) isAborted = aborted(payload, currLen);\n });\n else {\n if (payload.issues.length === currLen) continue;\n attachSchema(payload.issues, currLen, inst);\n if (!isAborted) isAborted = aborted(payload, currLen);\n }\n }\n if (asyncResult) return asyncResult.then(() => {\n return payload;\n });\n return payload;\n };\n const handleCanaryResult = (canary, payload, ctx) => {\n if (aborted(canary)) {\n canary.aborted = true;\n return canary;\n }\n const checkResult = runChecks(payload, checks, ctx);\n if (checkResult instanceof Promise) {\n if (ctx.async === false) throw new $ZodAsyncError();\n return checkResult.then((checkResult) => inst._zod.parse(checkResult, ctx));\n }\n return inst._zod.parse(checkResult, ctx);\n };\n inst._zod.run = (payload, ctx) => {\n if (ctx.skipChecks) return inst._zod.parse(payload, ctx);\n if (ctx.direction === \"backward\") {\n const canary = inst._zod.parse({\n value: payload.value,\n issues: []\n }, {\n ...ctx,\n skipChecks: true\n });\n if (canary instanceof Promise) return canary.then((canary) => {\n return handleCanaryResult(canary, payload, ctx);\n });\n return handleCanaryResult(canary, payload, ctx);\n }\n const result = inst._zod.parse(payload, ctx);\n if (result instanceof Promise) {\n if (ctx.async === false) throw new $ZodAsyncError();\n return result.then((result) => runChecks(result, checks, ctx));\n }\n return runChecks(result, checks, ctx);\n };\n }\n}, {\n get \"~standard\"() {\n return hide(this, \"~standard\", standardProps(this));\n },\n set \"~standard\"(value) {\n own(this, \"~standard\", value);\n }\n});\nconst toStandardResult = (r, ctx) => r.issues.length ? { issues: r.issues.map((iss) => finalizeIssue(iss, ctx, config())) } : { value: r.value };\nasync function validateAsync(inst, value) {\n const ctx = { async: true };\n return toStandardResult(await inst._zod.run({\n value,\n issues: []\n }, ctx), ctx);\n}\nfunction standardProps(inst) {\n return {\n validate: (value) => {\n const ctx = { async: false };\n try {\n const r = inst._zod.run({\n value,\n issues: []\n }, ctx);\n if (!(r instanceof Promise)) return toStandardResult(r, ctx);\n } catch (_) {}\n return validateAsync(inst, value);\n },\n vendor: \"zod\",\n version: 1\n };\n}\nconst $ZodString = /*@__PURE__*/ $constructor(\"$ZodString\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.pattern = def.pattern ?? anyString;\n inst._zod.parse = (payload, _) => {\n if (def.coerce) try {\n payload.value = String(payload.value);\n } catch (_) {}\n if (typeof payload.value === \"string\") return payload;\n payload.issues.push({\n expected: \"string\",\n code: \"invalid_type\",\n input: payload.value,\n inst\n });\n return payload;\n };\n});\nconst $ZodUnknown = /*@__PURE__*/ $constructor(\"$ZodUnknown\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload) => payload;\n});\nfunction handleArrayResult(result, final, index) {\n if (result.issues.length) final.issues.push(...prefixIssues(index, result.issues));\n final.value[index] = result.value;\n}\nconst $ZodArray = /*@__PURE__*/ $constructor(\"$ZodArray\", (inst, def) => {\n $ZodType.init(inst, def);\n const memo = globalConfig.memoizer;\n memo?.attach(inst);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!Array.isArray(input)) {\n payload.issues.push({\n expected: \"array\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n payload.value = memo ? memo.alloc(inst, payload, Array(input.length), ctx) : Array(input.length);\n const proms = [];\n const abortEarly = ctx?.abortEarly;\n for (let i = 0; i < input.length; i++) {\n const item = input[i];\n const result = def.element._zod.run({\n value: item,\n issues: []\n }, ctx);\n if (result instanceof Promise) proms.push(result.then((result) => handleArrayResult(result, payload, i)));\n else {\n handleArrayResult(result, payload, i);\n if (abortEarly && result.issues.length !== 0 && aborted(result)) break;\n }\n }\n if (proms.length) return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handlePropertyResult(result, final, key, input, optin, optout) {\n const isPresent = key in input;\n const isOptionalOut = optout === \"optional\";\n if (!isPresent && isOptionalOut && optin === \"optional\") return;\n if (result.issues.length) {\n if (optin !== void 0 && isOptionalOut && !isPresent) return;\n final.issues.push(...prefixIssues(key, result.issues));\n }\n if (!isPresent && optin === void 0) {\n if (!result.issues.length) final.issues.push({\n code: \"invalid_type\",\n expected: \"nonoptional\",\n input: void 0,\n path: [key]\n });\n return;\n }\n if (result.value === void 0) {\n if (isPresent || optin === \"defaulted\" && !isOptionalOut) final.value[key] = void 0;\n } else final.value[key] = result.value;\n}\nconst NO_SYMBOL_KEYS = [];\nfunction normalizeDef(def) {\n const keys = Object.keys(def.shape);\n const ownSymbols = Object.getOwnPropertySymbols(def.shape);\n const symbolKeys = ownSymbols.length ? ownSymbols : NO_SYMBOL_KEYS;\n const allKeys = symbolKeys.length ? [...keys, ...symbolKeys] : keys;\n for (const k of allKeys) if (!def.shape?.[k]?._zod?.traits?.has(\"$ZodType\")) throw new Error(`Invalid element at key \"${String(k)}\": expected a Zod schema`);\n const okeys = optionalKeys(def.shape);\n return {\n ...def,\n allKeys,\n symbolKeys,\n keySet: new Set(keys),\n numKeys: keys.length,\n optionalKeys: new Set(okeys)\n };\n}\nfunction handleCatchall(proms, input, payload, ctx, def, inst, abortEarly) {\n const unrecognized = [];\n const keySet = def.keySet;\n const _catchall = def.catchall._zod;\n const t = _catchall.def.type;\n const optin = _catchall.optin;\n const optout = _catchall.optout;\n let seen = 0;\n for (const key in input) {\n if (abortEarly && payload.issues.length !== seen) {\n if (aborted(payload, seen)) break;\n seen = payload.issues.length;\n }\n if (keySet.has(key)) continue;\n if (key === \"__proto__\") {\n if (t === \"never\") unrecognized.push(key);\n continue;\n }\n if (t === \"never\") {\n unrecognized.push(key);\n continue;\n }\n const r = _catchall.run({\n value: input[key],\n issues: []\n }, ctx);\n if (r instanceof Promise) proms.push(r.then((r) => handlePropertyResult(r, payload, key, input, optin, optout)));\n else handlePropertyResult(r, payload, key, input, optin, optout);\n }\n if (unrecognized.length) payload.issues.push({\n code: \"unrecognized_keys\",\n keys: unrecognized,\n input,\n inst,\n continue: true\n });\n if (!proms.length) return payload;\n return Promise.all(proms).then(() => {\n return payload;\n });\n}\nconst $ZodObject = /*@__PURE__*/ $constructor(\"$ZodObject\", (inst, def) => {\n $ZodType.init(inst, def);\n const desc = Object.getOwnPropertyDescriptor(def, \"shape\");\n const sh = desc?.get ? desc.get.raw : def.shape ?? {};\n if (sh) {\n const get = () => {\n const newSh = { ...sh };\n Object.defineProperty(def, \"shape\", { value: newSh });\n get.raw = newSh;\n return newSh;\n };\n get.raw = sh;\n Object.defineProperty(def, \"shape\", { get });\n }\n const _normalized = cached(() => normalizeDef(def));\n defineLazyInternal(inst, \"propValues\", (zod) => {\n const shape = zod.def.shape;\n const propValues = {};\n for (const key in shape) {\n const field = shape[key]._zod;\n if (field.values) {\n if (!Object.prototype.hasOwnProperty.call(propValues, key)) assignProp(propValues, key, /* @__PURE__ */ new Set());\n for (const v of field.values) propValues[key].add(v);\n if (field.optin !== void 0) propValues[key].add(void 0);\n }\n }\n return propValues;\n });\n const isObject$2 = isObject;\n const catchall = def.catchall;\n let value;\n const memo = globalConfig.memoizer;\n memo?.attach(inst);\n inst._zod.parse = (payload, ctx) => {\n value ?? (value = _normalized.value);\n const input = payload.value;\n if (!isObject$2(input)) {\n payload.issues.push({\n expected: \"object\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};\n const proms = [];\n const shape = value.shape;\n const abortEarly = ctx?.abortEarly;\n let seen = payload.issues.length;\n for (const key of value.allKeys) {\n if (abortEarly && payload.issues.length !== seen) {\n if (aborted(payload, seen)) break;\n seen = payload.issues.length;\n }\n if (key === \"__proto__\") continue;\n const el = shape[key];\n const optin = el._zod.optin;\n const optout = el._zod.optout;\n const r = el._zod.run({\n value: input[key],\n issues: []\n }, ctx);\n if (r instanceof Promise) proms.push(r.then((r) => handlePropertyResult(r, payload, key, input, optin, optout)));\n else handlePropertyResult(r, payload, key, input, optin, optout);\n }\n if (!catchall) return proms.length ? Promise.all(proms).then(() => payload) : payload;\n return handleCatchall(proms, input, payload, ctx, _normalized.value, inst, abortEarly === true);\n };\n});\nconst $ZodRecord = /*@__PURE__*/ $constructor(\"$ZodRecord\", (inst, def) => {\n $ZodType.init(inst, def);\n const memo = globalConfig.memoizer;\n memo?.attach(inst);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!isPlainObject(input)) {\n payload.issues.push({\n expected: \"record\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n const proms = [];\n const values = def.keyType._zod.values;\n if (values && !def.partial) {\n payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};\n const recordKeys = /* @__PURE__ */ new Set();\n for (const key of values) if (typeof key === \"string\" || typeof key === \"number\" || typeof key === \"symbol\") {\n recordKeys.add(typeof key === \"number\" ? key.toString() : key);\n if (key === \"__proto__\") continue;\n const keyResult = def.keyType._zod.run({\n value: key,\n issues: []\n }, ctx);\n if (keyResult instanceof Promise) throw new Error(\"Async schemas not supported in object keys currently\");\n if (keyResult.issues.length) {\n payload.issues.push({\n code: \"invalid_key\",\n origin: \"record\",\n issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),\n input: key,\n path: [key],\n inst\n });\n continue;\n }\n const outKey = keyResult.value;\n if (outKey === \"__proto__\") continue;\n const result = def.valueType._zod.run({\n value: input[key],\n issues: []\n }, ctx);\n if (result instanceof Promise) proms.push(result.then((result) => {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }));\n else {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }\n }\n let unrecognized;\n for (const key in input) if (!recordKeys.has(key)) {\n if (def.mode === \"loose\") {\n if (key === \"__proto__\") continue;\n payload.value[key] = input[key];\n } else {\n unrecognized = unrecognized ?? [];\n unrecognized.push(key);\n }\n }\n if (unrecognized && unrecognized.length > 0) payload.issues.push({\n code: \"unrecognized_keys\",\n input,\n inst,\n keys: unrecognized,\n continue: true\n });\n } else {\n payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};\n let unrecognized;\n for (const key of Reflect.ownKeys(input)) {\n if (key === \"__proto__\") continue;\n if (!Object.prototype.propertyIsEnumerable.call(input, key)) continue;\n let keyResult = def.keyType._zod.run({\n value: key,\n issues: []\n }, ctx);\n if (keyResult instanceof Promise) throw new Error(\"Async schemas not supported in object keys currently\");\n if (typeof key === \"string\" && number.test(key) && keyResult.issues.length) {\n const retryResult = def.keyType._zod.run({\n value: Number(key),\n issues: []\n }, ctx);\n if (retryResult instanceof Promise) throw new Error(\"Async schemas not supported in object keys currently\");\n if (retryResult.issues.length === 0) keyResult = retryResult;\n }\n if (keyResult.issues.length) {\n if (def.mode === \"loose\") payload.value[key] = input[key];\n else if (values) {\n unrecognized = unrecognized ?? [];\n unrecognized.push(key);\n } else payload.issues.push({\n code: \"invalid_key\",\n origin: \"record\",\n issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),\n input: key,\n path: [key],\n inst\n });\n continue;\n }\n const outKey = keyResult.value;\n if (outKey === \"__proto__\") continue;\n const result = def.valueType._zod.run({\n value: input[key],\n issues: []\n }, ctx);\n if (result instanceof Promise) proms.push(result.then((result) => {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }));\n else {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }\n }\n if (unrecognized && unrecognized.length > 0) payload.issues.push({\n code: \"unrecognized_keys\",\n input,\n inst,\n keys: unrecognized,\n continue: true\n });\n }\n if (proms.length) return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handleOptionalResult(payload, result) {\n payload.value = result.issues.length ? void 0 : result.value;\n return payload;\n}\nconst $ZodOptional = /*@__PURE__*/ $constructor(\"$ZodOptional\", (inst, def) => {\n $ZodType.init(inst, def);\n defineLazyInternal(inst, \"optin\", (zod) => zod.def.innerType._zod.optin === \"defaulted\" ? \"defaulted\" : \"optional\");\n inst._zod.optout = \"optional\";\n defineLazyInternal(inst, \"values\", (zod) => {\n const values = zod.def.innerType._zod.values;\n return values ? /* @__PURE__ */ new Set([...values, void 0]) : void 0;\n });\n defineLazyInternal(inst, \"pattern\", (zod) => {\n const pattern = zod.def.innerType._zod.pattern;\n return pattern ? new RegExp(`^(${cleanRegex(pattern.source)})?$`) : void 0;\n });\n inst._zod.parse = (payload, ctx) => {\n if (payload.value === void 0) {\n if (def.innerType._zod.optin !== \"defaulted\") return payload;\n const result = def.innerType._zod.run({\n value: payload.value,\n issues: []\n }, ctx);\n if (result instanceof Promise) return result.then((result) => handleOptionalResult(payload, result));\n return handleOptionalResult(payload, result);\n }\n return def.innerType._zod.run(payload, ctx);\n };\n});\nconst $ZodDefault = /*@__PURE__*/ $constructor(\"$ZodDefault\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.optin = \"defaulted\";\n defineLazyInternal(inst, \"values\", (zod) => zod.def.innerType._zod.values);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") return def.innerType._zod.run(payload, ctx);\n if (payload.value === void 0) {\n payload.value = def.defaultValue;\n return payload;\n }\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) return result.then((result) => handleDefaultResult(result, def));\n return handleDefaultResult(result, def);\n };\n});\nfunction handleDefaultResult(payload, def) {\n if (payload.value === void 0) payload.value = def.defaultValue;\n return payload;\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/locales/en.js\nconst error = () => {\n const Sizable = {\n string: {\n unit: \"characters\",\n verb: \"to have\"\n },\n file: {\n unit: \"bytes\",\n verb: \"to have\"\n },\n array: {\n unit: \"items\",\n verb: \"to have\"\n },\n set: {\n unit: \"items\",\n verb: \"to have\"\n },\n map: {\n unit: \"entries\",\n verb: \"to have\"\n }\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const FormatDictionary = {\n regex: \"input\",\n email: \"email address\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO datetime\",\n date: \"ISO date\",\n time: \"ISO time\",\n duration: \"ISO duration\",\n ipv4: \"IPv4 address\",\n ipv6: \"IPv6 address\",\n mac: \"MAC address\",\n cidrv4: \"IPv4 range\",\n cidrv6: \"IPv6 range\",\n base64: \"base64-encoded string\",\n base64url: \"base64url-encoded string\",\n json_string: \"JSON string\",\n e164: \"E.164 number\",\n currency_code: \"currency code\",\n credit_card: \"credit card number\",\n iban: \"IBAN\",\n jwt: \"JWT\",\n template_literal: \"input\"\n };\n const TypeDictionary = { nan: \"NaN\" };\n function getTypeName(type, input) {\n if (type === \"number\" && typeof input === \"number\" && !Number.isFinite(input)) return String(input);\n return TypeDictionary[type] ?? type;\n }\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\": return `Invalid input: expected ${getTypeName(issue.expected)}, received ${getTypeName(parsedType(issue.input), issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1) return `Invalid input: expected ${stringifyPrimitive(issue.values[0])}`;\n return `Invalid option: expected one of ${joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.exact ? \"exactly \" : issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) return `Too big: expected ${issue.origin ?? \"value\"} to have ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elements\"}`;\n return `Too big: expected ${issue.origin ?? \"value\"} to be ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.exact ? \"exactly \" : issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) return `Too small: expected ${issue.origin} to have ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n return `Too small: expected ${issue.origin} to be ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") return `Invalid string: must start with \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\") return `Invalid string: must end with \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\") return `Invalid string: must include \"${_issue.includes}\"`;\n if (_issue.format === \"regex\") return `Invalid string: must match pattern ${_issue.pattern}`;\n return `Invalid ${FormatDictionary[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\": return `Invalid number: must be a multiple of ${issue.divisor}`;\n case \"unrecognized_keys\": return `Unrecognized key${issue.keys.length > 1 ? \"s\" : \"\"}: ${joinValues(issue.keys, \", \")}`;\n case \"invalid_key\": return `Invalid key in ${issue.origin}`;\n case \"invalid_union\":\n if (issue.options && Array.isArray(issue.options) && issue.options.length > 0) return `Invalid discriminator value. Expected ${issue.options.map((o) => `'${o}'`).join(\" | \")}`;\n if (issue.inclusive === false) return \"Invalid input: more than one option matched\";\n return \"Invalid input\";\n case \"invalid_element\": return `Invalid value in ${issue.origin}`;\n default: return `Invalid input`;\n }\n };\n};\nfunction en_default() {\n return { localeError: error() };\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/api.js\nfunction snapshotChecks(def) {\n if (def.checks) def.checks = [...def.checks];\n return def;\n}\n// @__NO_SIDE_EFFECTS__\nfunction _string(Class, params) {\n return new Class(snapshotChecks({\n type: \"string\",\n ...normalizeParams(params)\n }));\n}\n// @__NO_SIDE_EFFECTS__\nfunction _unknown(Class) {\n return new Class({ type: \"unknown\" });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _minLength(minimum, params) {\n return new $ZodCheckMinLength({\n check: \"min_length\",\n ...normalizeParams(params),\n minimum\n });\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/mini/schemas.js\nconst ZodMiniType = /*@__PURE__*/ $constructor(\"ZodMiniType\", (inst, def) => {\n if (!inst._zod) throw new Error(\"Uninitialized schema in ZodMiniType.\");\n $ZodType.init(inst, def);\n inst.def = def;\n inst.type = def.type;\n}, {\n get with() {\n return this.check;\n },\n set with(value) {\n own(this, \"with\", value);\n },\n parse(data, params) {\n return parse(this, data, params, { callee: this.parse });\n },\n parseAsync(data, params) {\n return parseAsync(this, data, params, { callee: this.parseAsync });\n },\n safeParse(data, params) {\n return safeParse(this, data, params);\n },\n safeParseAsync(data, params) {\n return safeParseAsync(this, data, params);\n },\n check(...checks) {\n const def = this.def;\n return this.clone({\n ...def,\n checks: [...def.checks ?? [], ...checks.map((ch) => typeof ch === \"function\" ? { _zod: {\n check: ch,\n def: { check: \"custom\" },\n onattach: []\n } } : ch)]\n }, { parent: true });\n },\n clone(_def, params) {\n return clone(this, _def, params);\n },\n brand() {\n return this;\n },\n register(reg, meta) {\n reg.add(this, meta);\n return this;\n },\n apply(fn, ...args) {\n return args.length === 0 ? fn(this) : fn(this, ...args);\n }\n});\nconst ZodMiniString = /*@__PURE__*/ $constructor(\"ZodMiniString\", (inst, def) => {\n $ZodString.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction string(params) {\n return _string(ZodMiniString, params);\n}\nconst ZodMiniUnknown = /*@__PURE__*/ $constructor(\"ZodMiniUnknown\", (inst, def) => {\n $ZodUnknown.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction unknown() {\n return _unknown(ZodMiniUnknown);\n}\nconst ZodMiniArray = /*@__PURE__*/ $constructor(\"ZodMiniArray\", (inst, def) => {\n $ZodArray.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction array(element, params) {\n return new ZodMiniArray({\n type: \"array\",\n element,\n ...normalizeParams(params)\n });\n}\nconst ZodMiniObject = /*@__PURE__*/ $constructor(\"ZodMiniObject\", (inst, def) => {\n $ZodObject.init(inst, def);\n ZodMiniType.init(inst, def);\n installLazyProp(inst, \"shape\", (self) => self._zod.def.shape, false);\n});\n// @__NO_SIDE_EFFECTS__\nfunction object(shape, params) {\n const def = {\n type: \"object\",\n shape: shape ?? {},\n ...normalizeParams(params)\n };\n return new ZodMiniObject(def);\n}\nconst ZodMiniRecord = /*@__PURE__*/ $constructor(\"ZodMiniRecord\", (inst, def) => {\n $ZodRecord.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction record(keyType, valueType, params) {\n if (!valueType || !valueType._zod) return new ZodMiniRecord({\n type: \"record\",\n keyType: /* @__PURE__ */ string(),\n valueType: keyType,\n ...normalizeParams(valueType)\n });\n return new ZodMiniRecord({\n type: \"record\",\n keyType,\n valueType,\n ...normalizeParams(params)\n });\n}\nconst ZodMiniOptional = /*@__PURE__*/ $constructor(\"ZodMiniOptional\", (inst, def) => {\n $ZodOptional.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction optional(innerType) {\n return new ZodMiniOptional({\n type: \"optional\",\n innerType\n });\n}\nconst ZodMiniDefault = /*@__PURE__*/ $constructor(\"ZodMiniDefault\", (inst, def) => {\n $ZodDefault.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction _default(innerType, defaultValue) {\n return new ZodMiniDefault({\n type: \"default\",\n innerType,\n get defaultValue() {\n return typeof defaultValue === \"function\" ? defaultValue() : shallowClone(defaultValue);\n }\n });\n}\n\n//#endregion\n//#region src/lib/hook-io.ts\nconfig(en_default());\nconst HookInputSchema = object({\n session_id: string(),\n transcript_path: string(),\n cwd: string(),\n permission_mode: string(),\n hook_event_name: string(),\n tool_name: string(),\n tool_input: record(string(), unknown())\n});\nasync function readHookInput() {\n const chunks = [];\n for await (const chunk of process.stdin) chunks.push(typeof chunk === \"string\" ? chunk : String(chunk));\n let parsed;\n try {\n parsed = JSON.parse(chunks.join(\"\"));\n } catch (_error) {\n return {\n ok: false,\n raw: chunks.join(\"\").slice(0, 200),\n error: \"invalid JSON\"\n };\n }\n const result = HookInputSchema.safeParse(parsed);\n if (!result.success) return {\n ok: false,\n raw: parsed,\n error: result.error.message\n };\n return {\n ok: true,\n data: result.data\n };\n}\nconst BashInputSchema = object({\n command: string(),\n description: optional(string())\n});\nconst FilePathInputSchema = object({ file_path: string() });\nconst SearchInputSchema = object({\n pattern: string(),\n path: optional(string())\n});\nfunction parseBashInput(toolInput) {\n const result = BashInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction parseFilePathInput(toolInput) {\n const result = FilePathInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction parseSearchInput(toolInput) {\n const result = SearchInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction writeDeny(reason) {\n const decision = { hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"deny\",\n ...reason ? { permissionDecisionReason: reason } : {}\n } };\n process.stdout.write(JSON.stringify(decision));\n}\nfunction writeAdvice(advice) {\n const decision = { hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n additionalContext: advice\n } };\n process.stdout.write(JSON.stringify(decision));\n}\n\n//#endregion\n//#region src/lib/audit.ts\nconst MAX_LOG_SIZE = 1048576;\nfunction getLogPath() {\n const hookDir = path.dirname(fileURLToPath(import.meta.url));\n return path.join(hookDir, \"bst-hook.audit.local.jsonl\");\n}\nfunction rotateIfNeeded(logPath) {\n try {\n if (fs.statSync(logPath).size > MAX_LOG_SIZE) {\n const rotatedPath = `${logPath}.1`;\n fs.renameSync(logPath, rotatedPath);\n }\n } catch (_error) {}\n}\nlet rotationChecked = false;\nfunction audit(entry) {\n try {\n const logPath = getLogPath();\n if (!rotationChecked) {\n rotateIfNeeded(logPath);\n rotationChecked = true;\n }\n const record = {\n timestamp: (/* @__PURE__ */ new Date()).toISOString(),\n ...entry\n };\n fs.appendFileSync(logPath, JSON.stringify(record) + \"\\n\");\n } catch (err) {\n process.stderr.write(`[bst-hook] audit write failed: ${err instanceof Error ? err.message : String(err)}\\n`);\n }\n}\n\n//#endregion\n//#region src/lib/shell-tokenizer.ts\nfunction scanDoubleQuoted(input, start) {\n let content = \"\";\n let i = start;\n while (i < input.length && input[i] !== \"\\\"\") {\n if (input[i] === \"\\\\\" && i + 1 < input.length) {\n const next = input[i + 1];\n if (next === \"\\\"\" || next === \"\\\\\" || next === \"$\" || next === \"`\") {\n content += next;\n i += 2;\n continue;\n }\n }\n content += input[i];\n i++;\n }\n if (i >= input.length) return null;\n return {\n content,\n next: i + 1\n };\n}\nfunction tokenize(input) {\n const tokens = [];\n let current = \"\";\n let i = 0;\n let inToken = false;\n while (i < input.length) {\n const ch = input[i];\n if (ch === \"'\") {\n inToken = true;\n i++;\n while (i < input.length && input[i] !== \"'\") {\n current += input[i];\n i++;\n }\n if (i >= input.length) return {\n ok: false,\n error: \"unterminated single quote\"\n };\n i++;\n } else if (ch === \"\\\"\") {\n inToken = true;\n const scanned = scanDoubleQuoted(input, i + 1);\n if (!scanned) return {\n ok: false,\n error: \"unterminated double quote\"\n };\n current += scanned.content;\n i = scanned.next;\n } else if (ch === \"\\\\\" && i + 1 < input.length) {\n inToken = true;\n current += input[i + 1];\n i += 2;\n } else if (ch === \" \" || ch === \" \") {\n if (inToken) {\n tokens.push(current);\n current = \"\";\n inToken = false;\n }\n i++;\n } else {\n inToken = true;\n current += ch;\n i++;\n }\n }\n if (inToken) tokens.push(current);\n return {\n ok: true,\n tokens\n };\n}\nconst SAFE_UNQUOTED_RE = /^[A-Za-z0-9_\\-./=:@%+,]+$/;\nfunction quoteToken(token) {\n if (token.length > 0 && SAFE_UNQUOTED_RE.test(token)) return token;\n return \"'\" + token.replace(/'/g, \"'\\\\''\") + \"'\";\n}\nfunction joinTokens(tokens) {\n return tokens.map(quoteToken).join(\" \");\n}\n\n//#endregion\n//#region src/lib/shell-splitter.ts\nfunction splitCommand(input) {\n if (!input || !input.trim()) return [];\n const segments = [];\n let current = \"\";\n let i = 0;\n let inSingleQuote = false;\n let inDoubleQuote = false;\n while (i < input.length) {\n const ch = input[i];\n if (ch === \"'\" && !inDoubleQuote) {\n inSingleQuote = !inSingleQuote;\n current += ch;\n i++;\n continue;\n }\n if (ch === \"\\\"\" && !inSingleQuote) {\n inDoubleQuote = !inDoubleQuote;\n current += ch;\n i++;\n continue;\n }\n if (ch === \"\\\\\" && !inSingleQuote && i + 1 < input.length) {\n current += ch + input[i + 1];\n i += 2;\n continue;\n }\n if (!inSingleQuote && !inDoubleQuote) {\n if (ch === \"&\" && i + 1 < input.length && input[i + 1] === \"&\") {\n pushSegment(segments, current, \"&&\");\n current = \"\";\n i += 2;\n continue;\n }\n if (ch === \"&\" && !(i + 1 < input.length && input[i + 1] === \">\") && !(i > 0 && (input[i - 1] === \">\" || input[i - 1] === \"<\"))) {\n pushSegment(segments, current, \"&\");\n current = \"\";\n i++;\n continue;\n }\n if (ch === \"|\" && i + 1 < input.length && input[i + 1] === \"|\") {\n pushSegment(segments, current, \"||\");\n current = \"\";\n i += 2;\n continue;\n }\n if (ch === \"|\") {\n pushSegment(segments, current, \"|\");\n current = \"\";\n i++;\n continue;\n }\n if (ch === \";\") {\n pushSegment(segments, current, \";\");\n current = \"\";\n i++;\n continue;\n }\n }\n current += ch;\n i++;\n }\n const trimmed = current.trim();\n if (trimmed) segments.push({\n command: trimmed,\n operator: null\n });\n return segments;\n}\nfunction pushSegment(segments, text, operator) {\n const trimmed = text.trim();\n if (trimmed) segments.push({\n command: trimmed,\n operator\n });\n}\n\n//#endregion\n//#region src/lib/injection-guard.ts\nfunction hasBraceExpansion(token) {\n let depth = 0;\n for (let i = 0; i < token.length; i++) {\n const ch = token[i];\n if (ch === \"{\") depth++;\n else if (ch === \"}\") {\n if (depth > 0) depth--;\n } else if (depth >= 1 && (ch === \",\" || ch === \".\" && token[i + 1] === \".\")) return true;\n }\n return false;\n}\nfunction checkInjection(tokens, rawSegment) {\n if (/[\\n\\r]/.test(rawSegment)) return {\n safe: false,\n reason: \"unquoted newline in command\"\n };\n for (const token of tokens) {\n if (token === \"`\") return {\n safe: false,\n reason: \"backtick command substitution\"\n };\n if (token.includes(\"`\")) return {\n safe: false,\n reason: \"backtick command substitution\"\n };\n if (token.includes(\"$\")) return {\n safe: false,\n reason: \"unresolved dollar-expansion\"\n };\n if (hasBraceExpansion(token)) return {\n safe: false,\n reason: \"brace expansion\"\n };\n if (token.includes(\"<(\")) return {\n safe: false,\n reason: \"process substitution\"\n };\n if (token.includes(\">(\")) return {\n safe: false,\n reason: \"process substitution\"\n };\n }\n return { safe: true };\n}\n\n//#endregion\n//#region src/lib/env-file.ts\nconst ENV_SAFE_SUFFIXES = [\n \".sample\",\n \".example\",\n \".template\"\n];\nconst ENV_TOOL_OWNED_SUFFIXES = [\".check.env\"];\nconst ENV_FILE_PATTERN = /(^|[/\\\\])(\\.env(\\..*)?|.*\\.env(\\..*)?|\\.env)$/;\nfunction isEnvSafeSuffix(filename) {\n return [...ENV_SAFE_SUFFIXES, ...ENV_TOOL_OWNED_SUFFIXES].some((suffix) => filename.endsWith(suffix));\n}\nfunction isEnvFile(filePath) {\n if (isEnvSafeSuffix(path.basename(filePath))) return false;\n return ENV_FILE_PATTERN.test(filePath);\n}\n\n//#endregion\n//#region src/lib/git-state.ts\nconst GIT_TIMEOUT_MS = 5e3;\nfunction getCurrentBranch(cwd) {\n try {\n const out = execFileSync(\"git\", [\n \"rev-parse\",\n \"--abbrev-ref\",\n \"HEAD\"\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!out || out === \"HEAD\") return null;\n return out;\n } catch {\n return null;\n }\n}\nfunction getRepoTopLevel(cwd) {\n try {\n const out = execFileSync(\"git\", [\"rev-parse\", \"--show-toplevel\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n return out === \"\" ? null : out;\n } catch {\n return null;\n }\n}\nfunction getRemoteDefaultBranch(cwd) {\n try {\n const out = execFileSync(\"git\", [\n \"symbolic-ref\",\n \"--short\",\n \"refs/remotes/origin/HEAD\"\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!out) return null;\n return out.startsWith(\"origin/\") ? out.slice(7) : out;\n } catch {\n return null;\n }\n}\nconst DefaultBranchSchema = object({ defaultBranch: optional(string().check(_minLength(1))) });\nfunction getConfiguredDefaultBranch(topLevel) {\n let parsed;\n try {\n parsed = JSON.parse(readFileSync(join(topLevel, \".tooling.json\"), \"utf-8\"));\n } catch {\n return null;\n }\n const result = DefaultBranchSchema.safeParse(parsed);\n return result.success ? result.data.defaultBranch ?? null : null;\n}\nconst ALWAYS_PROTECTED = [\"main\", \"master\"];\nfunction getProtectedBranches(cwd, extra) {\n const set = new Set(ALWAYS_PROTECTED);\n const remote = getRemoteDefaultBranch(cwd);\n if (remote) set.add(remote);\n for (const b of extra) set.add(b);\n return set;\n}\nfunction getUpstream(cwd, branch) {\n try {\n return execFileSync(\"git\", [\n \"rev-parse\",\n \"--abbrev-ref\",\n `${branch}@{upstream}`\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim() || null;\n } catch {\n return null;\n }\n}\nfunction resolveCommit(cwd, rev) {\n try {\n return execFileSync(\"git\", [\n \"rev-parse\",\n \"--verify\",\n \"--quiet\",\n `${rev}^{commit}`\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim() || null;\n } catch {\n return null;\n }\n}\nfunction branchCarriesOtherFiles(cwd, allowed) {\n const toplevel = getRepoTopLevel(cwd);\n if (toplevel === null) return null;\n try {\n const defaultBranch = getConfiguredDefaultBranch(toplevel) ?? getRemoteDefaultBranch(toplevel) ?? \"main\";\n const mergeBase = execFileSync(\"git\", [\n \"merge-base\",\n \"HEAD\",\n `origin/${defaultBranch}`\n ], {\n cwd: toplevel,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!mergeBase) return null;\n const names = (args) => execFileSync(\"git\", args, {\n cwd: toplevel,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().split(\"\\n\").filter((line) => line.length > 0);\n return [...names([\n \"diff\",\n \"--name-only\",\n mergeBase\n ]), ...names([\n \"ls-files\",\n \"--others\",\n \"--exclude-standard\"\n ])].some((name) => !allowed.includes(name));\n } catch {\n return null;\n }\n}\nfunction isPrimaryWorktree(cwd) {\n try {\n const gitDir = execFileSync(\"git\", [\"rev-parse\", \"--absolute-git-dir\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n const commonRaw = execFileSync(\"git\", [\"rev-parse\", \"--git-common-dir\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!gitDir || !commonRaw) return false;\n const commonDir = resolve(cwd, commonRaw);\n return realpathSync(gitDir) === realpathSync(commonDir);\n } catch {\n return false;\n }\n}\n\n//#endregion\n//#region src/lib/git-short-options.ts\nconst VALUE_TAKING_LETTERS = {\n switch: /* @__PURE__ */ new Set([\"c\", \"C\"]),\n checkout: /* @__PURE__ */ new Set([\"b\", \"B\"]),\n branch: /* @__PURE__ */ new Set([\"u\"]),\n fetch: /* @__PURE__ */ new Set([\"o\", \"j\"])\n};\nfunction splitShortOptionCluster(token, sub) {\n if (!token.startsWith(\"-\") || token.startsWith(\"--\") || token.length <= 1) return;\n const valueLetters = VALUE_TAKING_LETTERS[sub];\n const letters = [];\n const chars = token.slice(1);\n for (let i = 0; i < chars.length; i++) {\n const ch = chars[i];\n letters.push(ch);\n if (valueLetters.has(ch)) return {\n letters,\n attachedValue: chars.slice(i + 1)\n };\n }\n return { letters };\n}\n\n//#endregion\n//#region src/lib/path-scope.ts\nfunction pathFlavorFor(base) {\n return /^[a-zA-Z]:[\\\\/]/.test(base) ? path.win32 : path.posix;\n}\nfunction resolveIn(base, target) {\n return pathFlavorFor(base).resolve(base, target);\n}\nfunction isWithin(dir, target) {\n const pp = pathFlavorFor(dir);\n const rel = pp.relative(dir, target);\n if (rel === \"\") return true;\n if (rel === \"..\" || rel.startsWith(\"..\" + pp.sep)) return false;\n return !pp.isAbsolute(rel);\n}\nfunction samePath(a, b) {\n return pathFlavorFor(a).relative(a, b) === \"\";\n}\nfunction targetsGitDir(filePath, cwd) {\n const pp = pathFlavorFor(cwd);\n const resolved = resolveIn(cwd, filePath);\n if (!isWithin(cwd, resolved)) return false;\n const rel = pp.relative(cwd, resolved);\n if (rel === \"\") return false;\n return rel.split(pp.sep).includes(\".git\");\n}\nfunction targetsNodeModules(filePath, cwd) {\n return resolveIn(cwd, filePath).split(/[/\\\\]/).includes(\"node_modules\");\n}\n\n//#endregion\n//#region src/rules.ts\nconst GIT_FLAGS_WITH_ARG = /* @__PURE__ */ new Set([\n \"-C\",\n \"-c\",\n \"--git-dir\",\n \"--work-tree\"\n]);\nfunction getGitSubcommand(tokens) {\n const args = tokens.slice(1);\n for (let i = 0; i < args.length; i++) {\n const t = args[i];\n if (GIT_FLAGS_WITH_ARG.has(t)) {\n i++;\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n return t;\n }\n}\nconst WORKTREE_ADVICE = \"You're creating a feature branch in the primary worktree. An isolated worktree keeps this work independent of other windows, and with no commits on this branch yet, converting is cheap -- `git worktree add ../<dir> -b <branch>` (a sibling outside the repo, with its own dependency install). Claude Code keeps this session inside its own working tree, so the work continues in a new session opened in the sibling worktree. If you cannot open a session there, stay in the primary checkout on this branch and check `git branch --show-current` before each commit.\";\nfunction isGitBranchCreation(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"checkout\" && sub !== \"switch\") return false;\n const after = tokens.slice(tokens.indexOf(sub) + 1);\n const creationLetters = sub === \"checkout\" ? [\"b\", \"B\"] : [\"c\", \"C\"];\n return after.some((t) => {\n const cluster = splitShortOptionCluster(t, sub);\n return cluster !== void 0 && cluster.letters.some((l) => creationLetters.includes(l));\n });\n}\nfunction worktreeAdvice(tokens, ctx) {\n return isGitBranchCreation(tokens) && isPrimaryWorktree(ctx.cwd) ? WORKTREE_ADVICE : void 0;\n}\nconst GIT_FORCE_CREATE_LETTER = {\n switch: \"C\",\n checkout: \"B\"\n};\nconst GIT_FORCE_CREATE_LONG = {\n flag: \"--force-create\",\n shortest: \"--force-\"\n};\nfunction gitForceCreateTargets(after, letterSub) {\n const letter = GIT_FORCE_CREATE_LETTER[letterSub];\n const longFlag = GIT_FORCE_CREATE_LONG;\n const targets = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (letterSub === \"switch\") {\n const [flag, ...rest] = t.split(\"=\");\n if (flag !== void 0 && flag.length >= longFlag.shortest.length && longFlag.flag.startsWith(flag)) {\n const target = rest.length > 0 ? rest.join(\"=\") : after[i + 1];\n if (target !== void 0) targets.push(target);\n continue;\n }\n }\n const cluster = splitShortOptionCluster(t, letterSub);\n if (cluster === void 0 || !cluster.letters.includes(letter)) continue;\n if (cluster.attachedValue !== void 0 && cluster.attachedValue !== \"\") {\n targets.push(cluster.attachedValue);\n continue;\n }\n const next = after[i + 1];\n if (next !== void 0) targets.push(next);\n }\n return targets;\n}\nfunction gitCommandForceCreateTargets(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub === \"switch\" || sub === \"checkout\") return gitForceCreateTargets(tokens.slice(tokens.indexOf(sub) + 1), sub);\n if (sub === \"worktree\") {\n const worktreeIdx = tokens.indexOf(\"worktree\");\n if (tokens[worktreeIdx + 1] !== \"add\") return [];\n return gitForceCreateTargets(tokens.slice(worktreeIdx + 2), \"checkout\");\n }\n return [];\n}\nfunction isGitProtectedBranchForceCreate(tokens, ctx) {\n const targets = gitCommandForceCreateTargets(tokens);\n if (targets.length === 0) return false;\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n return targets.some((t) => protectedSet.has(t));\n}\nfunction isGitProtectedBranchRefWrite(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"branch\") return false;\n const { flags, positionals } = splitGitBranchArgs(tokens.slice(tokens.indexOf(sub) + 1));\n if (positionals.length === 0) return false;\n let deleteMode = false;\n let renameMode = false;\n let copyMode = false;\n let forceMode = false;\n for (const flag of flags) {\n if (flag.letters !== void 0) {\n if (flag.letters.includes(\"d\") || flag.letters.includes(\"D\")) deleteMode = true;\n if (flag.letters.includes(\"m\") || flag.letters.includes(\"M\")) renameMode = true;\n if (flag.letters.includes(\"c\") || flag.letters.includes(\"C\")) copyMode = true;\n if (flag.letters.includes(\"f\") || flag.letters.includes(\"M\") || flag.letters.includes(\"C\")) forceMode = true;\n continue;\n }\n if (flag.name === \"delete\") deleteMode = true;\n if (flag.name === \"move\") renameMode = true;\n if (flag.name === \"copy\") copyMode = true;\n if (flag.name === \"force\") forceMode = true;\n }\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n if (deleteMode && positionals.some((p) => protectedSet.has(p))) return true;\n if (renameMode && positionals.length >= 2 && protectedSet.has(positionals[0])) return true;\n if (forceMode) {\n const destination = renameMode || copyMode ? positionals[positionals.length - 1] : positionals[0];\n if (destination !== void 0 && protectedSet.has(destination)) return true;\n }\n return false;\n}\nfunction refspecDestination(refspec) {\n const spec = refspec.startsWith(\"+\") ? refspec.slice(1) : refspec;\n const colon = spec.lastIndexOf(\":\");\n return (colon === -1 ? spec : spec.slice(colon + 1)).replace(/^refs\\/heads\\//, \"\");\n}\nconst GIT_BRANCH_VALUE_FLAG_NAMES = /* @__PURE__ */ new Set([\n \"contains\",\n \"no-contains\",\n \"merged\",\n \"no-merged\",\n \"points-at\",\n \"sort\",\n \"format\",\n \"set-upstream-to\"\n]);\nfunction splitGitBranchArgs(after) {\n const flags = [];\n const positionals = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (t.startsWith(\"--\") && t !== \"--\") {\n const eq = t.indexOf(\"=\");\n const name = eq > 0 ? t.slice(2, eq) : t.slice(2);\n flags.push({\n raw: t,\n name\n });\n if (eq < 0 && GIT_BRANCH_VALUE_FLAG_NAMES.has(name)) i++;\n continue;\n }\n const cluster = splitShortOptionCluster(t, \"branch\");\n if (cluster === void 0) {\n flags.push({ raw: t });\n continue;\n }\n flags.push({\n raw: t,\n letters: cluster.letters\n });\n if (cluster.attachedValue === \"\") i++;\n }\n return {\n flags,\n positionals\n };\n}\nconst GIT_FETCH_VALUE_FLAG_NAMES = /* @__PURE__ */ new Set([\n \"depth\",\n \"deepen\",\n \"shallow-since\",\n \"shallow-exclude\",\n \"negotiation-tip\",\n \"filter\",\n \"jobs\",\n \"server-option\",\n \"upload-pack\",\n \"refmap\",\n \"submodule-prefix\",\n \"recurse-submodules-default\"\n]);\nfunction splitGitFetchArgs(after) {\n const flags = [];\n const positionals = [];\n const refmaps = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (t === \"--\") {\n flags.push({ raw: t });\n continue;\n }\n if (t.startsWith(\"--\")) {\n const eq = t.indexOf(\"=\");\n const name = eq > 0 ? t.slice(2, eq) : t.slice(2);\n flags.push({\n raw: t,\n name\n });\n if (name === \"refmap\") {\n if (eq > 0) refmaps.push(t.slice(eq + 1));\n else if (after[i + 1] !== void 0) refmaps.push(after[i + 1]);\n }\n if (eq < 0 && GIT_FETCH_VALUE_FLAG_NAMES.has(name)) i++;\n continue;\n }\n const cluster = splitShortOptionCluster(t, \"fetch\");\n if (cluster === void 0) {\n flags.push({ raw: t });\n continue;\n }\n flags.push({\n raw: t,\n letters: cluster.letters\n });\n if (cluster.attachedValue === \"\") i++;\n }\n return {\n flags,\n positionals,\n refmaps\n };\n}\nfunction fetchRefspecTarget(spec) {\n const body = spec.startsWith(\"+\") ? spec.slice(1) : spec;\n if (body.startsWith(\"^\")) return { kind: \"none\" };\n if (!body.includes(\":\")) return { kind: \"none\" };\n const destination = refspecDestination(body);\n if (destination === \"\") return { kind: \"none\" };\n if (destination.startsWith(\"refs/remotes/\")) return { kind: \"tracking\" };\n if (destination.startsWith(\"refs/\")) return { kind: \"other\" };\n return {\n kind: \"branch\",\n name: destination\n };\n}\nfunction globToAnchoredRegex(glob) {\n const escaped = glob.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return new RegExp(`^${escaped.replace(/\\\\\\*/g, \".*\")}$`);\n}\nfunction isProtectedFetchBranch(name, protectedSet) {\n if (protectedSet.has(name)) return true;\n if (!name.includes(\"*\")) return false;\n const pattern = globToAnchoredRegex(name);\n for (const member of protectedSet) if (pattern.test(member)) return true;\n return false;\n}\nfunction isGitProtectedBranchFetchWrite(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"fetch\") return false;\n const { positionals, refmaps } = splitGitFetchArgs(tokens.slice(tokens.indexOf(sub) + 1));\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n return [...positionals, ...refmaps].some((spec) => {\n const target = fetchRefspecTarget(spec);\n if (target.kind !== \"branch\" || target.name === void 0) return false;\n return isProtectedFetchBranch(target.name, protectedSet);\n });\n}\nfunction isGitProtectedUpdateRef(tokens, ctx) {\n const after = tokens.slice(tokens.indexOf(\"update-ref\") + 1);\n let ref;\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === \"-m\") {\n i++;\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n ref = t;\n break;\n }\n if (ref === void 0) return false;\n return getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []).has(ref.replace(/^refs\\/heads\\//, \"\"));\n}\nfunction gitEffectiveCwd(tokens, cwd) {\n let dir = cwd;\n const args = tokens.slice(1);\n for (let i = 0; i < args.length; i++) {\n const t = args[i];\n if (t === \"-C\") {\n const next = args[i + 1];\n if (next !== void 0) dir = path.resolve(dir, next);\n i++;\n continue;\n }\n if (GIT_FLAGS_WITH_ARG.has(t)) {\n i++;\n continue;\n }\n if (!t.startsWith(\"-\")) break;\n }\n return dir;\n}\nconst GIT_SWITCH_NEUTRAL_FLAGS = /* @__PURE__ */ new Set([\n \"-q\",\n \"--quiet\",\n \"-f\",\n \"--force\",\n \"--discard-changes\",\n \"-m\",\n \"--merge\",\n \"--no-track\",\n \"--progress\",\n \"--no-progress\",\n \"--guess\",\n \"--no-guess\",\n \"--ignore-other-worktrees\",\n \"--recurse-submodules\",\n \"--no-recurse-submodules\"\n]);\nconst GIT_SWITCH_CREATE_FLAGS = /* @__PURE__ */ new Set([\n \"--create\",\n \"--force-create\",\n \"--orphan\"\n]);\nconst GIT_CHECKOUT_PATH_FLAGS = /* @__PURE__ */ new Set([\n \"--ours\",\n \"--theirs\",\n \"-p\",\n \"--patch\",\n \"--conflict\",\n \"--pathspec-from-file\",\n \"--pathspec-file-nul\",\n \"--overlay\",\n \"--no-overlay\"\n]);\nconst GIT_SWITCH_NEUTRAL_LETTERS = /* @__PURE__ */ new Set([\n \"q\",\n \"f\",\n \"m\"\n]);\nfunction switchTarget(after, sub, isCheckoutBranch) {\n if (sub === \"checkout\" && after.includes(\"--\")) return void 0;\n const createLetters = sub === \"checkout\" ? [\"b\", \"B\"] : [\"c\", \"C\"];\n const known = (l) => GIT_SWITCH_NEUTRAL_LETTERS.has(l) || createLetters.includes(l);\n const positionals = [];\n let created;\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (GIT_SWITCH_NEUTRAL_FLAGS.has(t)) continue;\n const eq = t.indexOf(\"=\");\n if (sub === \"checkout\" && GIT_CHECKOUT_PATH_FLAGS.has(eq === -1 ? t : t.slice(0, eq))) return;\n if (GIT_SWITCH_CREATE_FLAGS.has(eq === -1 ? t : t.slice(0, eq))) {\n created = eq === -1 ? after[++i] : t.slice(eq + 1);\n continue;\n }\n const cluster = splitShortOptionCluster(t, sub);\n if (cluster === void 0 || !cluster.letters.every(known)) return null;\n if (cluster.letters.some((l) => createLetters.includes(l))) created = cluster.attachedValue ? cluster.attachedValue : after[++i];\n }\n if (created !== void 0) return created;\n if (positionals.length !== 1) return void 0;\n const target = positionals[0];\n if (target === \"HEAD\" || target === \"@\") return void 0;\n if (sub === \"checkout\" && !isCheckoutBranch(target)) return void 0;\n return target;\n}\nfunction branchSwitch(tokens, ctx, resolved) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"switch\" && sub !== \"checkout\") return void 0;\n const dir = gitEffectiveCwd(tokens, ctx.cwd);\n const isCheckoutBranch = (name) => !resolved || getProtectedBranches(dir, ctx.repoProtectedBranches ?? []).has(name) || resolveCommit(dir, name) !== null;\n const target = switchTarget(tokens.slice(tokens.indexOf(sub) + 1), sub, isCheckoutBranch);\n if (!resolved) return target === void 0 ? void 0 : {\n dir,\n branch: null\n };\n return target === void 0 ? void 0 : {\n dir,\n branch: target\n };\n}\nconst GIT_CHECKED_OUT_WRITES = /* @__PURE__ */ new Set([\n \"commit\",\n \"merge\",\n \"rebase\",\n \"cherry-pick\",\n \"revert\",\n \"am\",\n \"pull\"\n]);\nfunction subcommandArgs(tokens, sub, valueFlags = /* @__PURE__ */ new Set()) {\n const after = tokens.slice(tokens.indexOf(sub) + 1);\n const flags = [];\n const positionals = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n flags.push(t);\n if (valueFlags.has(t)) i++;\n }\n return {\n flags,\n positionals\n };\n}\nconst GIT_PUSH_VALUE_FLAGS = /* @__PURE__ */ new Set([\n \"-o\",\n \"--push-option\",\n \"--repo\",\n \"--receive-pack\",\n \"--exec\"\n]);\nfunction isMergeFastForwardFromUpstream(tokens, upstream) {\n const { flags, positionals } = subcommandArgs(tokens, \"merge\");\n if (!flags.includes(\"--ff-only\") || positionals.length !== 1) return false;\n const ref = positionals[0];\n return ref === \"@{u}\" || ref === \"@{upstream}\" || upstream !== null && ref === upstream;\n}\nconst GIT_PULL_NO_REBASE = /* @__PURE__ */ new Set([\n \"--rebase=false\",\n \"--rebase=no\",\n \"--rebase=off\",\n \"--rebase=0\"\n]);\nfunction isPullFromUpstream(tokens, upstream) {\n const { flags, positionals } = subcommandArgs(tokens, \"pull\");\n if (flags.some((t) => t.startsWith(\"--rebase\") && !GIT_PULL_NO_REBASE.has(t) || t === \"--no-ff\" || !t.startsWith(\"--\") && t.slice(1).includes(\"r\"))) return false;\n if (positionals.length === 0) return true;\n if (positionals.length !== 2 || upstream === null) return false;\n const [remote, branch] = positionals;\n return `${remote}/${branch.replace(/^refs\\/heads\\//, \"\")}` === upstream;\n}\nfunction isRefMovingReset(tokens, cwd, head) {\n if (tokens.slice(tokens.indexOf(\"reset\") + 1).includes(\"--\")) return false;\n const { positionals } = subcommandArgs(tokens, \"reset\");\n if (positionals.length !== 1) return false;\n if (head !== \"HEAD\" && /^(HEAD|@)/.test(positionals[0])) return false;\n const target = resolveCommit(cwd, positionals[0]);\n if (target === null) return false;\n return target !== resolveCommit(cwd, head);\n}\nfunction pushDestinations(tokens, current) {\n const { positionals } = subcommandArgs(tokens, \"push\", GIT_PUSH_VALUE_FLAGS);\n const refspecs = positionals.slice(1);\n if (refspecs.length === 0) return null;\n const names = [];\n for (const spec of refspecs) {\n const dst = refspecDestination(spec);\n if (dst === \"HEAD\" || dst === \"@\") {\n if (current !== null) names.push(current);\n continue;\n }\n names.push(dst);\n }\n return names;\n}\nconst GIT_PUSH_EVERY_BRANCH = /* @__PURE__ */ new Set([\n \"--all\",\n \"--branches\",\n \"--mirror\"\n]);\nfunction leavesRefInPlace(tokens, sub) {\n const { flags } = subcommandArgs(tokens, sub);\n if (flags.includes(\"--abort\") || flags.includes(\"--quit\")) return true;\n return sub === \"commit\" && flags.includes(\"--dry-run\");\n}\nfunction movesProtectedRef(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (sub === void 0) return false;\n if (sub === \"switch\" || sub === \"checkout\" || sub === \"worktree\") return isGitProtectedBranchForceCreate(tokens, ctx);\n if (sub === \"branch\") return isGitProtectedBranchRefWrite(tokens, ctx);\n if (sub === \"fetch\") return isGitProtectedBranchFetchWrite(tokens, ctx);\n if (sub === \"update-ref\") return isGitProtectedUpdateRef(tokens, ctx);\n if (sub !== \"push\" && sub !== \"reset\" && !GIT_CHECKED_OUT_WRITES.has(sub)) return false;\n const cwd = gitEffectiveCwd(tokens, ctx.cwd);\n const pending = ctx.pendingBranches?.[cwd];\n const current = pending ? pending.branch : getCurrentBranch(cwd);\n const protectedSet = getProtectedBranches(cwd, ctx.repoProtectedBranches ?? []);\n const onProtected = current !== null && protectedSet.has(current);\n if (sub === \"push\") {\n const { flags } = subcommandArgs(tokens, \"push\", GIT_PUSH_VALUE_FLAGS);\n if (flags.some((f) => GIT_PUSH_EVERY_BRANCH.has(f))) return true;\n const destinations = pushDestinations(tokens, current);\n if (destinations === null) {\n if (flags.includes(\"--tags\")) return false;\n return onProtected;\n }\n return destinations.some((d) => protectedSet.has(d));\n }\n if (!onProtected) return false;\n if (leavesRefInPlace(tokens, sub)) return false;\n if (sub === \"reset\") return isRefMovingReset(tokens, cwd, pending ? current : \"HEAD\");\n if (sub === \"merge\" || sub === \"pull\") {\n const upstream = getUpstream(cwd, current);\n if (sub === \"merge\" && isMergeFastForwardFromUpstream(tokens, upstream)) return false;\n if (sub === \"pull\" && isPullFromUpstream(tokens, upstream)) return false;\n }\n return true;\n}\nconst PNPM_BUILTINS = /* @__PURE__ */ new Set([\n \"install\",\n \"i\",\n \"add\",\n \"remove\",\n \"rm\",\n \"uninstall\",\n \"un\",\n \"update\",\n \"up\",\n \"upgrade\",\n \"list\",\n \"ls\",\n \"ll\",\n \"la\",\n \"why\",\n \"view\",\n \"info\",\n \"show\",\n \"outdated\",\n \"audit\",\n \"licenses\",\n \"init\",\n \"create\",\n \"link\",\n \"unlink\",\n \"pack\",\n \"publish\",\n \"config\",\n \"set\",\n \"get\",\n \"store\",\n \"cache\",\n \"rebuild\",\n \"rb\",\n \"prune\",\n \"dedupe\",\n \"dlx\",\n \"exec\",\n \"run\",\n \"patch\",\n \"patch-commit\",\n \"recursive\",\n \"-r\"\n]);\nconst NPM_READ_OPS = /* @__PURE__ */ new Set([\n \"view\",\n \"info\",\n \"show\"\n]);\nfunction isNpmReadOp(tokens) {\n const sub = tokens[1];\n return sub !== void 0 && NPM_READ_OPS.has(sub);\n}\nfunction getNpmRedirectReason(tokens) {\n const sub = tokens[1];\n const BASE = \"This project uses pnpm.\";\n if (!sub) return `${BASE} Use \\`pnpm\\` instead of npm.`;\n if (sub === \"install\" || sub === \"i\" || sub === \"ci\") {\n if (tokens.slice(2).some((t) => !t.startsWith(\"-\"))) return `${BASE} Use \\`pnpm add <pkg>\\` instead of \\`npm install <pkg>\\`.`;\n return `${BASE} Use \\`pnpm install\\` instead of npm.`;\n }\n if (sub === \"add\") return `${BASE} Use \\`pnpm add <pkg>\\` instead of npm.`;\n if (sub === \"uninstall\" || sub === \"remove\" || sub === \"rm\" || sub === \"un\" || sub === \"r\") return `${BASE} Use \\`pnpm remove <pkg>\\` instead of npm.`;\n if (sub === \"run\" || sub === \"run-script\") return `${BASE} Use \\`pnpm run <script>\\` or \\`pnpm <script>\\` instead of npm.`;\n if (sub === \"test\" || sub === \"t\" || sub === \"tst\") return `${BASE} Use \\`pnpm test\\` instead of npm.`;\n if (sub === \"start\") return `${BASE} Use \\`pnpm start\\` instead of npm.`;\n if (sub === \"stop\") return `${BASE} Use \\`pnpm stop\\` instead of npm.`;\n if (sub === \"restart\") return `${BASE} Use \\`pnpm restart\\` instead of npm.`;\n if (sub === \"exec\") return `${BASE} Use \\`pnpm exec <cmd>\\` instead of npm.`;\n if (sub === \"list\" || sub === \"ls\" || sub === \"ll\" || sub === \"la\") return `${BASE} Use \\`pnpm list\\` instead of npm.`;\n if (sub === \"why\") return `${BASE} Use \\`pnpm why\\` instead of npm.`;\n if (sub === \"outdated\") return `${BASE} Use \\`pnpm outdated\\` instead of npm.`;\n if (sub === \"audit\") return `${BASE} Use \\`pnpm audit\\` instead of npm.`;\n if (sub === \"update\" || sub === \"up\" || sub === \"upgrade\") return `${BASE} Use \\`pnpm update\\` instead of npm.`;\n if (sub === \"init\") return `${BASE} Use \\`pnpm init\\` instead of npm.`;\n if (sub === \"publish\") return `${BASE} Use \\`pnpm publish\\` instead of npm.`;\n if (sub === \"link\") return `${BASE} Use \\`pnpm link\\` instead of npm.`;\n if (sub === \"unlink\") return `${BASE} Use \\`pnpm unlink\\` instead of npm.`;\n if (sub === \"pack\") return `${BASE} Use \\`pnpm pack\\` instead of npm.`;\n if (sub === \"cache\") return `${BASE} Use \\`pnpm store\\` instead of \\`npm cache\\`.`;\n if (sub === \"config\" || sub === \"set\" || sub === \"get\") return `${BASE} Use \\`pnpm config\\` instead of npm.`;\n return `${BASE} Use the equivalent pnpm command instead of npm.`;\n}\nconst LAST_POSITIONAL_DEST = /* @__PURE__ */ new Set([\n \"cp\",\n \"mv\",\n \"rsync\",\n \"install\",\n \"ln\"\n]);\nconst ALL_POSITIONAL_DEST = /* @__PURE__ */ new Set([\"tee\", \"truncate\"]);\nfunction explicitTargetDirectory(rest) {\n for (const [i, t] of rest.entries()) {\n if (t === \"-t\" || t === \"--target-directory\") return rest[i + 1] ?? null;\n if (t.startsWith(\"--target-directory=\")) return t.slice(19);\n }\n return null;\n}\nconst OUTPUT_REDIRECT = /^(?:\\d*|&)>{1,2}$/;\nconst OUTPUT_REDIRECT_ATTACHED = /^(?:\\d*|&)>{1,2}(.+)$/;\nconst INPUT_REDIRECT = /^\\d*<$/;\nconst INPUT_REDIRECT_ATTACHED = /^\\d*<([^<].*)$/;\nfunction spacedRedirectDirection(token) {\n if (OUTPUT_REDIRECT.test(token)) return \"out\";\n if (INPUT_REDIRECT.test(token)) return \"in\";\n return null;\n}\nfunction splitRedirects(tokens) {\n const redirects = [];\n const rest = [];\n for (let i = 0; i < tokens.length; i++) {\n const t = tokens[i];\n const spaced = spacedRedirectDirection(t);\n if (spaced !== null) {\n const next = tokens[i + 1];\n if (next !== void 0) redirects.push({\n direction: spaced,\n target: next\n });\n i++;\n continue;\n }\n const out = OUTPUT_REDIRECT_ATTACHED.exec(t);\n if (out) {\n redirects.push({\n direction: \"out\",\n target: out[1]\n });\n continue;\n }\n const input = INPUT_REDIRECT_ATTACHED.exec(t);\n if (input) {\n redirects.push({\n direction: \"in\",\n target: input[1]\n });\n continue;\n }\n rest.push(t);\n }\n return {\n redirects,\n rest\n };\n}\nfunction redirectTargets(tokens, direction) {\n return splitRedirects(tokens).redirects.filter((r) => r.direction === direction).map((r) => r.target);\n}\nfunction writeDestinations(tokens) {\n const cmd = path.basename(tokens[0] ?? \"\");\n const rest = tokens.slice(1);\n const dests = redirectTargets(tokens, \"out\");\n if (cmd === \"dd\") dests.push(...rest.filter((t) => t.startsWith(\"of=\")).map((t) => t.slice(3)));\n else if (cmd === \"sed\") {\n if (rest.some((t) => t === \"--in-place\" || /^-[a-zA-Z]*i/.test(t))) dests.push(...rest.filter((t) => !t.startsWith(\"-\")));\n } else if (ALL_POSITIONAL_DEST.has(cmd)) dests.push(...rest.filter((t) => !t.startsWith(\"-\")));\n else if (LAST_POSITIONAL_DEST.has(cmd)) {\n if (cmd === \"cp\" && rest.includes(\"--remove-destination\")) return dests;\n const targetDir = explicitTargetDirectory(rest);\n if (targetDir !== null) dests.push(targetDir);\n else {\n const last = rest.findLast((t) => !t.startsWith(\"-\"));\n if (last !== void 0) dests.push(last);\n }\n }\n return dests;\n}\nfunction writesIntoNodeModules(tokens, ctx) {\n return writeDestinations(tokens).some((d) => targetsNodeModules(d, ctx.cwd));\n}\nconst NODE_MODULES_WRITE_REASON = \"Don't write into node_modules/. Its files are hardlinks into pnpm's global store, so overwriting one in place (plain `cp`, `>` redirect, `sed -i`, ...) writes through the link and corrupts that package for every project on this machine -- silently, since the directory you wrote into ends up fine. To test a local build as an installed dependency, run it directly (`node packages/<pkg>/dist/bin.mjs <cmd>`), or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`). If you must replace a file in place, `cp --remove-destination` is safe -- it unlinks the target first. Run `pnpm store status` to check whether the store is already damaged.\";\nfunction isChmod777(tokens) {\n return tokens.some((t) => t === \"777\" || t === \"0777\");\n}\nfunction isGitForcePush(tokens) {\n const pushIdx = tokens.indexOf(\"push\");\n if (pushIdx === -1) return false;\n return tokens.slice(pushIdx + 1).some((t) => t === \"--force\" || t === \"-f\");\n}\nconst ENV_READ_FILE_COMMANDS = /* @__PURE__ */ new Set([\n \"cat\",\n \"head\",\n \"tail\",\n \"less\",\n \"more\",\n \"source\",\n \".\"\n]);\nconst GREP_FAMILY_COMMANDS = /* @__PURE__ */ new Set([\n \"grep\",\n \"rg\",\n \"ag\"\n]);\nfunction grepFileOperands(rest) {\n const files = [];\n let patternAccountedFor = false;\n let optionsEnded = false;\n for (let i = 0; i < rest.length; i++) {\n const t = rest[i];\n if (!optionsEnded && t === \"--\") {\n optionsEnded = true;\n continue;\n }\n if (!optionsEnded) {\n if (t === \"-f\" || t === \"--file\") {\n const value = rest[i + 1];\n if (value !== void 0) {\n files.push(value);\n i++;\n }\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"--file=\")) {\n files.push(t.slice(7));\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"-f\") && t !== \"-f\" && !t.startsWith(\"--\")) {\n files.push(t.slice(2));\n patternAccountedFor = true;\n continue;\n }\n if (t === \"-e\" || t === \"--regexp\") {\n i++;\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"--regexp=\") || t.startsWith(\"-e\") && t !== \"-e\" && !t.startsWith(\"--\")) {\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n }\n if (!patternAccountedFor) {\n patternAccountedFor = true;\n continue;\n }\n files.push(t);\n }\n return files;\n}\nfunction envFileOperands(tokens) {\n const cmd = path.basename(tokens[0] ?? \"\");\n const { redirects, rest } = splitRedirects(tokens.slice(1));\n const operands = redirects.filter((r) => r.direction === \"in\").map((r) => r.target);\n if (GREP_FAMILY_COMMANDS.has(cmd)) operands.push(...grepFileOperands(rest));\n else if (ENV_READ_FILE_COMMANDS.has(cmd)) operands.push(...rest.filter((t) => !t.startsWith(\"-\")));\n return operands;\n}\nconst HEALTH_CEILING_FILES = [\"health-baseline.json\", \".tooling.json\"];\nconst HEALTH_BASELINE_FILE = \"health-baseline.json\";\nconst HEALTH_RAISE_REASON = \"A health ceiling raise travels alone: open a ceiling-only pull request -- only health-baseline.json and .tooling.json changed -- for a human to merge, or split the module. `bst 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 `bst health:check --update-baseline`.\";\nfunction writesHealthBaseline(tokens, cwd) {\n const destinations = writeDestinations(tokens);\n if (!destinations.some((dest) => baseName(dest) === \"health-baseline.json\")) return false;\n const root = getRepoTopLevel(cwd) ?? cwd;\n return destinations.some((dest) => samePath(resolveIn(cwd, dest), path.join(root, HEALTH_BASELINE_FILE)));\n}\nfunction baseName(target) {\n const segments = target.split(/[/\\\\]/);\n return segments[segments.length - 1] ?? \"\";\n}\nfunction isBaselineRestore(tokens) {\n if (tokens[0] !== \"git\") return false;\n let i = 1;\n while (tokens[i]?.startsWith(\"-\") === true) i += GIT_FLAGS_WITH_ARG.has(tokens[i] ?? \"\") ? 2 : 1;\n if (tokens[i] !== \"show\") return false;\n return tokens.slice(i + 1).some((token) => token.includes(\":\") && baseName(token.slice(token.lastIndexOf(\":\") + 1)) === \"health-baseline.json\");\n}\nfunction isHealthBaselineReset(tokens) {\n return tokens.includes(\"health:check\") && tokens.includes(\"--update-baseline\") && tokens.includes(\"--reset\");\n}\nfunction raiseTravelsWithCode(cwd) {\n return branchCarriesOtherFiles(cwd, HEALTH_CEILING_FILES) === true;\n}\nconst denyRules = [\n {\n name: \"sudo\",\n match: /^sudo$/,\n action: \"deny\",\n reason: \"Don't use sudo. If you need elevated permissions, ask me to do it manually.\"\n },\n {\n name: \"git-force-push\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens) => isGitForcePush(tokens),\n reason: \"Use `git push --force-with-lease` instead of `--force`.\"\n },\n {\n name: \"git-protected-branch-write\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens, ctx) => movesProtectedRef(tokens, ctx),\n reason: \"This would move a protected branch (main, master, the remote's default branch, or one listed in .claude/hooks/bst-hook.settings.json#protectedBranches). Work on a feature branch in a worktree: `git worktree add ../<repo>-<topic> -b feat/<topic>`, then commit and push there. To bring `main` up to date, use `git pull --ff-only`.\"\n },\n {\n name: \"chmod-777\",\n match: /^chmod$/,\n action: \"deny\",\n when: (tokens) => isChmod777(tokens),\n reason: \"Don't use 777 permissions. Use more restrictive permissions.\"\n },\n {\n name: \"node-modules-write\",\n match: /./,\n action: \"deny\",\n when: (tokens, ctx) => writesIntoNodeModules(tokens, ctx),\n reason: NODE_MODULES_WRITE_REASON\n },\n {\n name: \"env-file-read\",\n match: /./,\n action: \"deny\",\n when: (tokens) => envFileOperands(tokens).some(isEnvFile),\n reason: \"Don't read .env files. These contain secrets that should not appear in the conversation.\"\n },\n {\n name: \"env-file-write\",\n match: /./,\n action: \"deny\",\n when: (tokens) => redirectTargets(tokens, \"out\").some(isEnvFile),\n reason: \"Don't write to .env files. These contain secrets.\"\n },\n {\n name: \"health-baseline-write\",\n match: /./,\n action: \"deny\",\n when: (tokens, ctx) => writesHealthBaseline(tokens, ctx.cwd) && !isBaselineRestore(tokens) && raiseTravelsWithCode(ctx.cwd),\n reason: HEALTH_RAISE_REASON\n },\n {\n name: \"health-baseline-reset\",\n match: /^bst$/,\n action: \"deny\",\n when: (tokens, ctx) => isHealthBaselineReset(tokens) && raiseTravelsWithCode(ctx.cwd),\n reason: HEALTH_RAISE_REASON\n },\n {\n name: \"npx-redirect\",\n match: /^npx$/,\n action: \"deny\",\n reason: \"This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\"\n },\n {\n name: \"mise-self-update\",\n match: /^mise$/,\n action: \"deny\",\n when: (tokens) => tokens[1] === \"self-update\",\n reason: \"Don't self-update mise. The version is Renovate-managed; ask a human to bump the pin.\"\n }\n];\n\n//#endregion\n//#region src/lib/script-resolver.ts\nconst PackageJsonSchema = object({ scripts: record(string(), unknown()) });\nfunction resolveScript(scriptName, cwd) {\n const scripts = readScripts(cwd);\n if (!scripts) return null;\n const main = scripts[scriptName];\n if (typeof main !== \"string\") return null;\n const pre = scripts[`pre${scriptName}`];\n const post = scripts[`post${scriptName}`];\n return {\n pre: typeof pre === \"string\" ? pre : null,\n main,\n post: typeof post === \"string\" ? post : null\n };\n}\nfunction resolveFilteredScript(scriptName, filterPkg, cwd) {\n const pkgDir = findWorkspacePackageDir(filterPkg, cwd);\n if (!pkgDir) return null;\n const script = resolveScript(scriptName, pkgDir);\n if (!script) return null;\n return {\n ...script,\n pkgDir\n };\n}\nfunction resolveRecursiveScripts(scriptName, rootDir) {\n const pkgDirs = listWorkspacePackageDirs(rootDir);\n if (!pkgDirs) return null;\n const results = [];\n for (const pkgDir of pkgDirs) {\n const resolved = resolveScript(scriptName, pkgDir);\n if (resolved) results.push({\n ...resolved,\n pkgDir\n });\n }\n return results;\n}\nfunction listWorkspacePackageDirs(rootDir) {\n const patterns = readWorkspacePatterns(rootDir);\n if (!patterns) return null;\n const dirs = [];\n for (const pattern of patterns) {\n if (!pattern.endsWith(\"/*\")) continue;\n const baseDir = pattern.slice(0, -2);\n const parentDir = resolve(rootDir, baseDir);\n let entries;\n try {\n entries = readdirSync(parentDir);\n } catch {\n continue;\n }\n for (const entry of entries) {\n const entryDir = join(parentDir, entry);\n try {\n if (!statSync(entryDir).isDirectory()) continue;\n } catch {\n continue;\n }\n dirs.push(entryDir);\n }\n }\n return dirs.length > 0 ? dirs : null;\n}\nfunction findWorkspacePackageDir(pkgName, rootDir) {\n const pkgDirs = listWorkspacePackageDirs(rootDir);\n if (!pkgDirs) return null;\n for (const entryDir of pkgDirs) {\n const name = readPackageName(entryDir);\n if (name === pkgName || name === `@${pkgName}`) return entryDir;\n if (entryDir.split(\"/\").pop() === pkgName) return entryDir;\n if (name) {\n if ((name.startsWith(\"@\") ? name.split(\"/\")[1] : name) === pkgName) return entryDir;\n }\n }\n return null;\n}\nfunction readWorkspacePatterns(rootDir) {\n try {\n const lines = readFileSync(join(rootDir, \"pnpm-workspace.yaml\"), \"utf-8\").split(\"\\n\");\n const packagesIdx = lines.findIndex((l) => /^packages\\s*:/.test(l));\n if (packagesIdx === -1) return null;\n const patterns = [];\n for (let i = packagesIdx + 1; i < lines.length; i++) {\n const line = lines[i];\n const match = /^\\s+-\\s+(.+)$/.exec(line);\n if (!match) break;\n patterns.push(match[1].trim().replace(/['\"`]/g, \"\"));\n }\n return patterns.length > 0 ? patterns : null;\n } catch {\n return null;\n }\n}\nconst PackageNameSchema = object({ name: string() });\nfunction readPackageName(dir) {\n try {\n const raw = readFileSync(join(dir, \"package.json\"), \"utf-8\");\n const result = PackageNameSchema.safeParse(JSON.parse(raw));\n if (!result.success) return null;\n return result.data.name;\n } catch {\n return null;\n }\n}\nfunction readScripts(cwd) {\n try {\n const raw = readFileSync(join(cwd, \"package.json\"), \"utf-8\");\n const result = PackageJsonSchema.safeParse(JSON.parse(raw));\n if (!result.success) return null;\n return result.data.scripts;\n } catch {\n return null;\n }\n}\n\n//#endregion\n//#region src/lib/workspace-root.ts\nfunction findWorkspaceRoot(cwd) {\n let dir = resolve(cwd);\n while (true) {\n if (existsSync(join(dir, \"pnpm-workspace.yaml\"))) return dir;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n//#endregion\n//#region src/evaluate.ts\nconst MAX_EVAL_DEPTH = 5;\nfunction subshellBody(command) {\n if (!command.startsWith(\"(\") || !command.endsWith(\")\")) return null;\n let depth = 0;\n let quote = null;\n for (let i = 0; i < command.length; i++) {\n const ch = command[i];\n if (quote !== null) {\n if (ch === quote) quote = null;\n else if (ch === \"\\\\\" && quote === \"\\\"\") i++;\n continue;\n }\n if (ch === \"\\\\\") i++;\n else if (ch === \"'\" || ch === \"\\\"\") quote = ch;\n else if (ch === \"(\") depth++;\n else if (ch === \")\") {\n depth--;\n if (depth === 0) return i === command.length - 1 ? command.slice(1, -1) : null;\n }\n }\n return null;\n}\nfunction extractCdDir(segment, ctx) {\n const tokenResult = tokenize(segment);\n if (!tokenResult.ok || tokenResult.tokens.length !== 2) return null;\n const tokens = tokenResult.tokens;\n if (tokens[0] !== \"cd\") return null;\n const dir = tokens[1];\n if (path.isAbsolute(dir)) return dir;\n return path.resolve(ctx.cwd, dir);\n}\nfunction extractBranchSwitch(segment, ctx) {\n const tokenResult = tokenize(segment);\n if (!tokenResult.ok) return void 0;\n let tokens = tokenResult.tokens;\n let envSkipped = 0;\n while (envSkipped < tokens.length && /^\\w+=/.test(tokens[envSkipped])) envSkipped++;\n tokens = tokens.slice(envSkipped);\n if (tokens.length === 0 || path.basename(tokens[0]) !== \"git\") return void 0;\n return branchSwitch(tokens, ctx, checkInjection(tokens, segment).safe);\n}\nfunction evaluateCommand(command, ctx) {\n if (ctx.depth >= MAX_EVAL_DEPTH) return {\n action: \"none\",\n rule: null,\n reason: \"max evaluation depth exceeded\"\n };\n const trimmed = command.trim();\n if (!trimmed) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n const body = subshellBody(trimmed);\n if (body !== null) return evaluateCommand(body, {\n ...ctx,\n depth: ctx.depth + 1\n });\n const segments = splitCommand(trimmed);\n if (segments.length === 0) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n let firstNone = null;\n const currentCtx = { ...ctx };\n let unknownAfterNext;\n for (const segment of segments) {\n if (segment.operator === \"&&\") {\n const cdDir = extractCdDir(segment.command, currentCtx);\n if (cdDir) currentCtx.cwd = cdDir;\n }\n const result = evaluateSegment(segment.command, currentCtx);\n if (result.action === \"deny\") return result;\n if (!firstNone) firstNone = result;\n if (unknownAfterNext) {\n currentCtx.pendingBranches = {\n ...currentCtx.pendingBranches,\n [unknownAfterNext.dir]: unknownAfterNext\n };\n unknownAfterNext = void 0;\n }\n const switched = extractBranchSwitch(segment.command, currentCtx);\n if (switched) {\n const unknown = {\n dir: switched.dir,\n branch: null\n };\n if (segment.operator === \"&&\") currentCtx.pendingBranches = {\n ...currentCtx.pendingBranches,\n [switched.dir]: switched\n };\n else if (segment.operator === \"||\") unknownAfterNext = unknown;\n else currentCtx.pendingBranches = {\n ...currentCtx.pendingBranches,\n [switched.dir]: unknown\n };\n }\n }\n return firstNone ?? {\n action: \"none\",\n rule: null,\n reason: null\n };\n}\nfunction evaluateResolvedScript(resolved, ctx) {\n if (resolved.pre) {\n const preResult = evaluateCommand(resolved.pre, ctx);\n if (preResult.action === \"deny\") return preResult;\n }\n const mainResult = evaluateCommand(resolved.main, ctx);\n if (mainResult.action === \"deny\") return mainResult;\n if (resolved.post) {\n const postResult = evaluateCommand(resolved.post, ctx);\n if (postResult.action === \"deny\") return postResult;\n }\n return {\n action: \"none\",\n rule: null,\n reason: null\n };\n}\nfunction evaluateSegment(rawSegment, ctx) {\n const trimmed = rawSegment.trim();\n const body = subshellBody(trimmed);\n if (body !== null) return evaluateCommand(body, {\n ...ctx,\n depth: ctx.depth + 1\n });\n if (trimmed.startsWith(\"(\") && trimmed.endsWith(\")\")) return {\n action: \"none\",\n rule: null,\n reason: \"subshell\"\n };\n const tokenResult = tokenize(rawSegment);\n if (!tokenResult.ok) return {\n action: \"none\",\n rule: null,\n reason: tokenResult.error\n };\n let tokens = tokenResult.tokens;\n if (tokens.length === 0) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n let envSkipped = 0;\n while (envSkipped < tokens.length && /^\\w+=/.test(tokens[envSkipped])) envSkipped++;\n if (envSkipped >= tokens.length) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n tokens = tokens.slice(envSkipped);\n if (writesIntoNodeModules(tokens, ctx)) return {\n action: \"deny\",\n rule: \"node-modules-write\",\n reason: NODE_MODULES_WRITE_REASON\n };\n const injection = checkInjection(tokens, rawSegment);\n if (!injection.safe) return {\n action: \"none\",\n rule: null,\n reason: injection.reason\n };\n const SAFE_REDIRECT_TOKENS = /* @__PURE__ */ new Set([\n \"2>&1\",\n \"2>/dev/null\",\n \">/dev/null\",\n \"1>/dev/null\"\n ]);\n tokens = tokens.filter((t, i) => {\n if (SAFE_REDIRECT_TOKENS.has(t)) return false;\n if ((t === \">\" || t === \">>\") && tokens[i + 1] === \"/dev/null\") return false;\n if (t === \"/dev/null\" && i > 0 && (tokens[i - 1] === \">\" || tokens[i - 1] === \">>\")) return false;\n return true;\n });\n const cmdName = path.basename(tokens[0]);\n if (cmdName === \"command\" && tokens.length > 1) return evaluateCommand(joinTokens(tokens.slice(1)), {\n ...ctx,\n depth: ctx.depth + 1\n });\n if (cmdName === \"env\") {\n let envIdx = 1;\n while (envIdx < tokens.length && /^\\w+=/.test(tokens[envIdx])) envIdx++;\n if (envIdx >= tokens.length) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n return evaluateCommand(joinTokens(tokens.slice(envIdx)), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n if (cmdName === \"pnpm\") return evaluatePnpm(tokens, ctx);\n if (cmdName === \"npm\") return evaluateNpm(tokens);\n if (cmdName === \"mise\" && tokens[1] === \"exec\") {\n const inner = miseExecInner(tokens);\n if (!inner) return {\n action: \"none\",\n rule: null,\n reason: \"bare mise exec\"\n };\n return evaluateCommand(joinTokens(inner), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n if (cmdName === \"bash\" || cmdName === \"sh\" || cmdName === \"zsh\") return evaluateShellC(tokens, ctx);\n if (cmdName === \"xargs\") return evaluateXargs(tokens, ctx);\n if (cmdName === \"node\" && (tokens.includes(\"-e\") || tokens.includes(\"-p\") || tokens.includes(\"--eval\") || tokens.includes(\"--print\"))) return {\n action: \"none\",\n rule: null,\n reason: \"node eval\"\n };\n for (const rule of denyRules) if (rule.match.test(cmdName)) {\n if (!rule.when || rule.when(tokens, ctx)) return {\n action: \"deny\",\n rule: rule.name,\n reason: rule.reason ?? null\n };\n }\n const advice = cmdName === \"git\" ? worktreeAdvice(tokens, ctx) : void 0;\n return {\n action: \"none\",\n rule: null,\n reason: `no rule matched: ${cmdName}`,\n ...advice ? { advice } : {}\n };\n}\nfunction miseExecInner(tokens) {\n const dashIdx = tokens.indexOf(\"--\");\n if (dashIdx === -1) return void 0;\n const inner = tokens.slice(dashIdx + 1);\n return inner.length > 0 ? inner : void 0;\n}\nfunction scanPnpmFlags(subTokens, from, state) {\n for (let i = from; i < subTokens.length; i++) {\n const t = subTokens[i];\n if (t === \"--filter\" || t.startsWith(\"--filter=\") || t === \"-F\" || t.startsWith(\"-F=\")) return {\n name: void 0,\n filterIdx: i\n };\n if (t === \"-r\" || t === \"--recursive\") {\n state.isRecursive = true;\n continue;\n }\n if (t === \"-w\" || t === \"--workspace-root\") {\n state.isWorkspaceRoot = true;\n continue;\n }\n if (t === \"-C\" || t === \"--dir\") {\n state.dirOverride = subTokens[++i];\n continue;\n }\n if (t.startsWith(\"-C=\") || t.startsWith(\"--dir=\")) {\n state.dirOverride = t.split(\"=\", 2)[1];\n continue;\n }\n if (t.startsWith(\"-\") && !PNPM_BUILTINS.has(t)) continue;\n return { name: t };\n }\n return { name: void 0 };\n}\nfunction evaluatePnpm(tokens, ctx) {\n const subTokens = tokens.slice(1);\n const state = {\n isRecursive: false,\n isWorkspaceRoot: false,\n dirOverride: void 0\n };\n const scan = scanPnpmFlags(subTokens, 0, state);\n if (scan.filterIdx !== void 0) return evaluatePnpmFilter(tokens, subTokens, scan.filterIdx, ctx);\n const subcommand = scan.name;\n const subcommandIdx = subcommand ? subTokens.indexOf(subcommand) : -1;\n if (!subcommand) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n if (subcommand === \"dlx\") return {\n action: \"none\",\n rule: null,\n reason: \"pnpm dlx\"\n };\n if (subcommand === \"add\") return {\n action: \"none\",\n rule: null,\n reason: \"pnpm add\"\n };\n if (subcommand === \"exec\") {\n const execArgs = subTokens.slice(subcommandIdx + 1);\n if (execArgs.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"bare pnpm exec\"\n };\n return evaluateCommand(joinTokens(execArgs), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n let scriptName;\n let { isRecursive, isWorkspaceRoot, dirOverride } = state;\n if (subcommand === \"run\") {\n const runScan = scanPnpmFlags(subTokens, subcommandIdx + 1, state);\n ({isRecursive, isWorkspaceRoot, dirOverride} = state);\n if (runScan.filterIdx !== void 0) return evaluatePnpmFilter(tokens, subTokens, runScan.filterIdx, ctx);\n if (!runScan.name) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm run without script name\"\n };\n scriptName = runScan.name;\n } else if (!PNPM_BUILTINS.has(subcommand)) scriptName = subcommand;\n else return {\n action: \"none\",\n rule: null,\n reason: null\n };\n if (isRecursive) return evaluatePnpmRecursive(scriptName, ctx);\n if (isWorkspaceRoot) {\n const root = findWorkspaceRoot(ctx.cwd);\n if (!root) return {\n action: \"none\",\n rule: null,\n reason: \"workspace root not found\"\n };\n return evaluatePnpmScript(scriptName, {\n ...ctx,\n cwd: root\n });\n }\n if (dirOverride) {\n const resolved = path.resolve(ctx.cwd, dirOverride);\n return evaluatePnpmScript(scriptName, {\n ...ctx,\n cwd: resolved\n });\n }\n return evaluatePnpmScript(scriptName, ctx);\n}\nfunction evaluatePnpmScript(scriptName, ctx) {\n const resolved = resolveScript(scriptName, ctx.cwd);\n if (!resolved) return {\n action: \"none\",\n rule: null,\n reason: \"script resolution failed\"\n };\n const res = evaluateResolvedScript(resolved, {\n ...ctx,\n depth: ctx.depth + 1\n });\n if (res.action === \"deny\") return res;\n return {\n action: \"none\",\n rule: `pnpm-script:${scriptName}`,\n reason: null\n };\n}\nfunction evaluatePnpmRecursive(scriptName, ctx) {\n const resolved = resolveRecursiveScripts(scriptName, ctx.cwd);\n if (!resolved || resolved.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm recursive\"\n };\n for (const pkg of resolved) {\n const res = evaluateResolvedScript(pkg, {\n ...ctx,\n depth: ctx.depth + 1,\n cwd: pkg.pkgDir ?? ctx.cwd\n });\n if (res.action === \"deny\") return res;\n }\n return {\n action: \"none\",\n rule: `pnpm-recursive:${scriptName}`,\n reason: null\n };\n}\nfunction evaluateNpm(tokens) {\n if (isNpmReadOp(tokens)) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n return {\n action: \"deny\",\n rule: \"npm-redirect\",\n reason: getNpmRedirectReason(tokens)\n };\n}\nfunction evaluatePnpmFilter(_tokens, subTokens, filterIdx, ctx) {\n const filterPkgs = [];\n let cursor = filterIdx;\n while (cursor < subTokens.length) {\n const t = subTokens[cursor];\n if (t === \"--filter\" || t === \"-F\") {\n const pkg = subTokens[cursor + 1];\n if (!pkg || pkg.startsWith(\"-\")) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without package\"\n };\n filterPkgs.push(pkg);\n cursor += 2;\n } else if (t.startsWith(\"--filter=\") || t.startsWith(\"-F=\")) {\n const pkg = t.startsWith(\"-F=\") ? t.slice(3) : t.slice(9);\n if (!pkg) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without package\"\n };\n filterPkgs.push(pkg);\n cursor += 1;\n } else break;\n }\n if (filterPkgs.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without package\"\n };\n let filterSubcommand;\n let subcmdIdx = -1;\n for (let i = cursor; i < subTokens.length; i++) {\n const t = subTokens[i];\n if (t === \"--filter\" || t === \"-F\") {\n i += 1;\n continue;\n }\n if (t.startsWith(\"--filter=\") || t.startsWith(\"-F=\")) continue;\n if (t.startsWith(\"-\") && !PNPM_BUILTINS.has(t)) continue;\n filterSubcommand = t;\n subcmdIdx = i;\n break;\n }\n if (!filterSubcommand) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without subcommand\"\n };\n if (PNPM_BUILTINS.has(filterSubcommand) && filterSubcommand !== \"run\" && filterSubcommand !== \"exec\") return {\n action: \"none\",\n rule: null,\n reason: null\n };\n if (filterSubcommand === \"exec\") {\n const execArgs = subTokens.slice(subcmdIdx + 1);\n if (execArgs.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"bare pnpm exec\"\n };\n return evaluateCommand(joinTokens(execArgs), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n const scriptName = filterSubcommand === \"run\" ? subTokens[subcmdIdx + 1] : filterSubcommand;\n if (!scriptName || scriptName.startsWith(\"-\")) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter run without script name\"\n };\n if (PNPM_BUILTINS.has(scriptName) && scriptName !== \"run\") return {\n action: \"none\",\n rule: null,\n reason: null\n };\n for (const pkg of filterPkgs) {\n const resolved = resolveFilteredScript(scriptName, pkg, ctx.cwd);\n if (!resolved) return {\n action: \"none\",\n rule: null,\n reason: \"filtered script resolution failed\"\n };\n const res = evaluateResolvedScript(resolved, {\n ...ctx,\n depth: ctx.depth + 1,\n cwd: resolved.pkgDir ?? ctx.cwd\n });\n if (res.action === \"deny\") return res;\n }\n return {\n action: \"none\",\n rule: `pnpm-filter:${filterPkgs.join(\",\")}:${scriptName}`,\n reason: null\n };\n}\nconst SHELL_LOGIN_FLAGS = /* @__PURE__ */ new Set([\"-l\", \"--login\"]);\nfunction evaluateShellC(tokens, ctx) {\n let i = 1;\n while (i < tokens.length && SHELL_LOGIN_FLAGS.has(tokens[i])) i++;\n const flag = tokens[i];\n if (flag === void 0 || !/^-[lc]+$/.test(flag) || !flag.includes(\"c\")) return {\n action: \"none\",\n rule: null,\n reason: \"shell without a plain -c\"\n };\n const cmdArg = tokens[i + 1];\n if (!cmdArg) return {\n action: \"none\",\n rule: null,\n reason: \"shell -c without command\"\n };\n if (tokens.length > i + 2) return {\n action: \"none\",\n rule: null,\n reason: \"shell -c with positional args\"\n };\n return evaluateCommand(cmdArg, {\n ...ctx,\n depth: ctx.depth + 1\n });\n}\nconst XARGS_ARG_FLAGS = /* @__PURE__ */ new Set([\n \"-I\",\n \"-d\",\n \"-n\",\n \"-P\",\n \"-s\",\n \"-L\",\n \"-E\",\n \"-a\",\n \"--max-args\",\n \"--max-procs\",\n \"--delimiter\",\n \"--replace\",\n \"--max-lines\",\n \"--max-chars\",\n \"--arg-file\",\n \"--process-slot-var\"\n]);\nconst XARGS_BOOL_FLAGS = /* @__PURE__ */ new Set([\n \"-0\",\n \"--null\",\n \"-t\",\n \"--verbose\",\n \"-r\",\n \"--no-run-if-empty\",\n \"-p\",\n \"--interactive\",\n \"-x\",\n \"--exit\",\n \"--show-limits\"\n]);\nfunction evaluateXargs(tokens, ctx) {\n let i = 1;\n while (i < tokens.length) {\n const t = tokens[i];\n if (!t.startsWith(\"-\")) break;\n if (XARGS_ARG_FLAGS.has(t)) {\n i += 2;\n continue;\n }\n const eqIdx = t.indexOf(\"=\");\n if (eqIdx !== -1) {\n const flagName = t.slice(0, eqIdx);\n if (XARGS_ARG_FLAGS.has(flagName)) {\n i++;\n continue;\n }\n }\n if (t.length > 2 && XARGS_ARG_FLAGS.has(t.slice(0, 2))) {\n i++;\n continue;\n }\n if (XARGS_BOOL_FLAGS.has(t)) {\n i++;\n continue;\n }\n return {\n action: \"none\",\n rule: null,\n reason: \"xargs with unrecognised flags\"\n };\n }\n if (i >= tokens.length) return {\n action: \"none\",\n rule: null,\n reason: \"xargs without command\"\n };\n return evaluateCommand(joinTokens(tokens.slice(i)), {\n ...ctx,\n depth: ctx.depth + 1\n });\n}\n\n//#endregion\n//#region src/file-rules.ts\nconst READ_TOOLS = /* @__PURE__ */ new Set([\n \"Read\",\n \"Glob\",\n \"Grep\"\n]);\nconst WRITE_TOOLS = /* @__PURE__ */ new Set([\"Write\", \"Edit\"]);\nconst fileDenyRules = [\n {\n name: \"env-file-read\",\n tools: READ_TOOLS,\n action: \"deny\",\n when: (filePath) => isEnvFile(filePath),\n reason: \"Don't read .env files. These contain secrets that should not appear in the conversation.\"\n },\n {\n name: \"env-file-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath) => isEnvFile(filePath),\n reason: \"Don't write to .env files. These contain secrets.\"\n },\n {\n name: \"hooks-folder-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n const hooksDir = path.join(ctx.cwd, \".claude\", \"hooks\");\n return isWithin(hooksDir, resolved);\n },\n reason: \"Don't write to .claude/hooks/. These files are managed by the project and should not be modified directly.\"\n },\n {\n name: \"git-dir-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => targetsGitDir(filePath, ctx.cwd),\n reason: \"Don't write into .git/ directly. Its internals (config, hooks, refs) are not plain project files -- a config edit can redirect remotes or push destinations that other rules trust, and a hooks/ script runs automatically on the next commit/push. Use git subcommands instead, or ask the user.\"\n },\n {\n name: \"node-modules-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => targetsNodeModules(filePath, ctx.cwd),\n reason: \"Don't write into node_modules/. Its files are hardlinks into pnpm's global store -- editing one in place corrupts that package for every project on this machine. Edit the source package and rebuild, or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`).\"\n },\n {\n name: \"claude-settings-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n return samePath(resolved, path.join(ctx.cwd, \".claude\", \"settings.json\"));\n },\n reason: \"Don't write to .claude/settings.json. This file is managed by the project and should not be modified directly.\"\n },\n {\n name: \"health-baseline-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx, toolInput) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n if (path.basename(resolved) !== \"health-baseline.json\") return false;\n const root = getRepoTopLevel(ctx.cwd) ?? ctx.cwd;\n if (!samePath(resolved, path.join(root, \"health-baseline.json\"))) return false;\n if (!writeRaisesBaseline(resolved, toolInput)) return false;\n return branchCarriesOtherFiles(ctx.cwd, HEALTH_CEILING_FILES) === true;\n },\n reason: HEALTH_RAISE_REASON\n }\n];\nfunction writeRaisesBaseline(file, toolInput) {\n let before;\n try {\n before = readFileSync(file, \"utf8\");\n } catch (_error) {\n return false;\n }\n const after = textAfterWrite(before, toolInput);\n if (after === void 0) return false;\n const beforeJson = parseJson(before);\n const afterJson = parseJson(after);\n if (beforeJson === void 0 || afterJson === void 0) return false;\n return numberRises(beforeJson, afterJson);\n}\nfunction textAfterWrite(before, toolInput) {\n const { content, old_string: oldText, new_string: newText, replace_all: all } = toolInput;\n if (typeof content === \"string\") return content;\n if (typeof oldText !== \"string\" || typeof newText !== \"string\" || oldText === \"\") return void 0;\n const at = before.indexOf(oldText);\n if (at === -1) return void 0;\n if (all === true) return before.split(oldText).join(newText);\n return before.slice(0, at) + newText + before.slice(at + oldText.length);\n}\nfunction parseJson(text) {\n try {\n const result = record(string(), unknown()).safeParse(JSON.parse(text));\n return result.success ? result.data : void 0;\n } catch (_error) {\n return;\n }\n}\nfunction numberRises(before, after) {\n if (typeof after === \"number\") return typeof before !== \"number\" || after > before;\n if (typeof after !== \"object\" || after === null) return false;\n const beforeObject = typeof before === \"object\" && before !== null ? before : {};\n return Object.entries(after).some(([key, value]) => numberRises(Object.entries(beforeObject).find(([k]) => k === key)?.[1], value));\n}\n\n//#endregion\n//#region src/evaluate-tool.ts\nconst FILE_PATH_TOOLS = /* @__PURE__ */ new Set([\n \"Read\",\n \"Write\",\n \"Edit\"\n]);\nconst SEARCH_TOOLS = /* @__PURE__ */ new Set([\"Glob\", \"Grep\"]);\nfunction extractPath(toolName, toolInput, cwd) {\n if (FILE_PATH_TOOLS.has(toolName)) return parseFilePathInput(toolInput)?.file_path ?? null;\n if (SEARCH_TOOLS.has(toolName)) return parseSearchInput(toolInput)?.path ?? cwd;\n return null;\n}\nfunction evaluateToolUse(toolName, toolInput, ctx) {\n const filePath = extractPath(toolName, toolInput, ctx.cwd);\n if (filePath === null) return {\n action: \"none\",\n rule: null,\n reason: \"unknown tool input shape\"\n };\n for (const rule of fileDenyRules) if (rule.tools.has(toolName) && rule.when(filePath, ctx, toolInput)) return {\n action: \"deny\",\n rule: rule.name,\n reason: rule.reason ?? null\n };\n return {\n action: \"none\",\n rule: null,\n reason: \"no file rule matched\"\n };\n}\n\n//#endregion\n//#region src/lib/settings.ts\nconst SETTINGS_PATH = \".claude/hooks/bst-hook.settings.json\";\nconst BstHookSettingsSchema = object({ protectedBranches: _default(array(string().check(_minLength(1))), []) });\nfunction isFileClean(cwd, relativePath) {\n try {\n execFileSync(\"git\", [\n \"ls-files\",\n \"--error-unmatch\",\n relativePath\n ], {\n cwd,\n stdio: \"ignore\"\n });\n execFileSync(\"git\", [\n \"diff\",\n \"--quiet\",\n \"HEAD\",\n \"--\",\n relativePath\n ], {\n cwd,\n stdio: \"ignore\"\n });\n return true;\n } catch {\n return false;\n }\n}\nfunction loadRepoSettings(cwd) {\n const root = findWorkspaceRoot(cwd) ?? cwd;\n const settingsFile = join(root, SETTINGS_PATH);\n const empty = { protectedBranches: [] };\n if (!existsSync(settingsFile)) return empty;\n if (!isFileClean(root, SETTINGS_PATH)) return empty;\n try {\n const raw = readFileSync(settingsFile, \"utf-8\");\n const parsed = JSON.parse(raw);\n const result = BstHookSettingsSchema.safeParse(parsed);\n if (!result.success) return empty;\n return { protectedBranches: result.data.protectedBranches };\n } catch {\n return empty;\n }\n}\n\n//#endregion\n//#region src/index.ts\nconst inputResult = await readHookInput();\nif (!inputResult.ok) {\n process.stderr.write(`[bst-hook] invalid hook input: ${inputResult.error}\\n`);\n audit({\n sessionId: \"unknown\",\n cwd: \"unknown\",\n command: `<invalid input: ${inputResult.error}>`,\n decision: \"none\",\n rule: null,\n reason: `validation failed: ${inputResult.error}`,\n permissionMode: void 0\n });\n process.exit(0);\n}\nconst input = inputResult.data;\nif (input.hook_event_name !== \"PreToolUse\") process.exit(0);\nif (!(/* @__PURE__ */ new Set([\n \"Bash\",\n \"Read\",\n \"Write\",\n \"Edit\",\n \"Glob\",\n \"Grep\"\n])).has(input.tool_name)) process.exit(0);\nconst repoSettings = loadRepoSettings(input.cwd);\nlet decision;\nlet auditCommand;\nif (input.tool_name === \"Bash\") {\n const bashInput = parseBashInput(input.tool_input);\n if (!bashInput) {\n audit({\n sessionId: input.session_id,\n cwd: input.cwd,\n toolName: input.tool_name,\n command: `<invalid Bash input: ${JSON.stringify(input.tool_input).slice(0, 200)}>`,\n decision: \"none\",\n rule: null,\n reason: \"Bash tool_input missing or invalid 'command' field\",\n permissionMode: input.permission_mode\n });\n process.exit(0);\n }\n decision = evaluateCommand(bashInput.command, {\n cwd: input.cwd,\n depth: 0,\n repoProtectedBranches: repoSettings.protectedBranches\n });\n auditCommand = bashInput.command;\n} else {\n const projectDir = findWorkspaceRoot(input.cwd) ?? process.env[\"CLAUDE_PROJECT_DIR\"] ?? input.cwd;\n decision = evaluateToolUse(input.tool_name, input.tool_input, {\n cwd: projectDir,\n sessionId: input.session_id\n });\n const filePath = String(input.tool_input[\"file_path\"] ?? input.tool_input[\"path\"] ?? \"\");\n auditCommand = `${input.tool_name} ${filePath}`;\n}\naudit({\n sessionId: input.session_id,\n cwd: input.cwd,\n toolName: input.tool_name,\n command: auditCommand,\n decision: decision.action,\n rule: decision.rule,\n reason: decision.reason,\n permissionMode: input.permission_mode\n});\nif (decision.action === \"deny\") writeDeny(decision.reason);\nelse if (decision.advice) writeAdvice(decision.advice);\n\n//#endregion\nexport { };",
|
|
8353
|
-
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-write**: 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. `
|
|
8418
|
+
hook: "/**\n * bst-hook -- Claude Code PreToolUse hook\n * @bensandee/claude-hooks\n * https://github.com/bensandee/tooling\n *\n * Auto-generated by @bensandee/tooling repo:sync -- do not edit manually.\n */\nimport * as fs from \"node:fs\";\nimport { existsSync, readFileSync, readdirSync, realpathSync, statSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { execFileSync } from \"node:child_process\";\n\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/util.js\nfunction joinValues(array, separator = \"|\") {\n return array.map((val) => stringifyPrimitive(val)).join(separator);\n}\nfunction jsonStringifyReplacer(_, value) {\n if (typeof value === \"bigint\") return value.toString();\n return value;\n}\nvar Cached = class {\n constructor(getter) {\n this._getter = getter;\n this._value = void 0;\n }\n get value() {\n const getter = this._getter;\n if (getter !== void 0) {\n this._value = getter();\n this._getter = void 0;\n }\n return this._value;\n }\n};\nfunction cached(getter) {\n return new Cached(getter);\n}\nfunction nullish(input) {\n return input === null || input === void 0;\n}\nfunction cleanRegex(source) {\n const start = source.startsWith(\"^\") ? 1 : 0;\n const end = source.endsWith(\"$\") ? source.length - 1 : source.length;\n return source.slice(start, end);\n}\nfunction assignProp(target, prop, value) {\n Object.defineProperty(target, prop, {\n value,\n writable: true,\n enumerable: true,\n configurable: true\n });\n}\nconst captureStackTrace = \"captureStackTrace\" in Error ? Error.captureStackTrace : (..._args) => {};\nfunction isObject(data) {\n return typeof data === \"object\" && data !== null && !Array.isArray(data);\n}\nfunction isPlainObject(o) {\n if (isObject(o) === false) return false;\n const ctor = o.constructor;\n if (ctor === void 0) return true;\n if (typeof ctor !== \"function\") return true;\n const prot = ctor.prototype;\n if (isObject(prot) === false) return false;\n if (Object.prototype.hasOwnProperty.call(prot, \"isPrototypeOf\") === false) return false;\n return true;\n}\nfunction shallowClone(o) {\n if (isPlainObject(o)) return { ...o };\n if (Array.isArray(o)) return [...o];\n if (o instanceof Map) return new Map(o);\n if (o instanceof Set) return new Set(o);\n return o;\n}\nfunction clone(inst, def, params) {\n const cl = new inst._zod.constr(def ?? inst._zod.def);\n if (!def || params?.parent) cl._zod.parent = inst;\n return cl;\n}\nfunction normalizeParams(_params) {\n const params = _params;\n if (!params) return {};\n if (typeof params === \"string\") return { error: () => params };\n if (params?.message !== void 0) {\n if (params?.error !== void 0) throw new Error(\"Cannot specify both `message` and `error` params\");\n params.error = params.message;\n }\n delete params.message;\n if (typeof params.error === \"string\") return {\n ...params,\n error: () => params.error\n };\n return params;\n}\nfunction stringifyPrimitive(value) {\n if (typeof value === \"bigint\") return value.toString() + \"n\";\n if (typeof value === \"string\") return `\"${value}\"`;\n return `${value}`;\n}\nfunction optionalKeys(shape) {\n return Object.keys(shape).filter((k) => {\n return shape[k]._zod.optin !== void 0 && shape[k]._zod.optout === \"optional\";\n });\n}\nfunction aborted(x, startIndex = 0) {\n if (x.aborted === true) return true;\n for (let i = startIndex; i < x.issues.length; i++) if (x.issues[i]?.continue !== true) return true;\n return false;\n}\nfunction explicitlyAborted(x, startIndex = 0) {\n if (x.aborted === true) return true;\n for (let i = startIndex; i < x.issues.length; i++) if (x.issues[i]?.continue === false) return true;\n return false;\n}\nfunction prefixIssues(path, issues) {\n return issues.map((iss) => {\n var _a;\n (_a = iss).path ?? (_a.path = []);\n iss.path.unshift(path);\n return iss;\n });\n}\nfunction unwrapMessage(message) {\n return typeof message === \"string\" ? message : message?.message;\n}\nfunction attachSchema(issues, start, inst) {\n var _a;\n for (let i = start; i < issues.length; i++) (_a = issues[i]).schema ?? (_a.schema = inst);\n}\nfunction finalizeIssue(iss, ctx, config) {\n var _a;\n const traits = iss.inst?._zod?.traits;\n if (traits?.has(\"$ZodType\")) {\n if (traits.has(\"$ZodCheck\")) (_a = iss).schema ?? (_a.schema = iss.inst);\n else iss.schema = iss.inst;\n }\n const schemaError = iss.schema !== iss.inst ? iss.schema?._zod.def?.error : void 0;\n const message = iss.message ? iss.message : unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ?? unwrapMessage(schemaError?.(iss)) ?? unwrapMessage(ctx?.error?.(iss)) ?? unwrapMessage(config.customError?.(iss)) ?? unwrapMessage(config.localeError?.(iss)) ?? \"Invalid input\";\n const full = {};\n for (const k of Object.keys(iss)) {\n if (k === \"inst\" || k === \"schema\" || k === \"continue\" || k === \"input\" || k === \"__proto__\") continue;\n full[k] = iss[k];\n }\n full.path ?? (full.path = []);\n full.message = message;\n if (ctx?.reportInput) full.input = iss.input;\n return full;\n}\nconst highSurrogate = /[\\uD800-\\uDBFF]/;\nfunction codePointLength(str) {\n const units = str.length;\n if (!highSurrogate.test(str)) return units;\n let count = units;\n for (let i = 0; i < units - 1; i++) if ((str.charCodeAt(i) & 64512) === 55296 && (str.charCodeAt(i + 1) & 64512) === 56320) {\n count--;\n i++;\n }\n return count;\n}\nfunction getLengthableOrigin(input) {\n if (Array.isArray(input)) return \"array\";\n if (typeof input === \"string\") return \"string\";\n return \"unknown\";\n}\nfunction parsedType(data) {\n const t = typeof data;\n switch (t) {\n case \"number\": return Number.isNaN(data) ? \"nan\" : \"number\";\n case \"object\": {\n if (data === null) return \"null\";\n if (Array.isArray(data)) return \"array\";\n const obj = data;\n if (obj && Object.getPrototypeOf(obj) !== Object.prototype && \"constructor\" in obj && obj.constructor) return obj.constructor.name;\n }\n }\n return t;\n}\nfunction members(proto, table) {\n for (const key in table) {\n const desc = Object.getOwnPropertyDescriptor(table, key);\n if (desc.get) Object.defineProperty(proto, key, {\n ...desc,\n enumerable: false\n });\n else defineBound(proto, key, desc.value);\n }\n}\nfunction own(inst, key, value, enumerable = true) {\n Object.defineProperty(inst, key, {\n configurable: true,\n writable: true,\n enumerable,\n value\n });\n return value;\n}\nfunction hide(inst, key, value) {\n return own(inst, key, value, false);\n}\nfunction defineBound(proto, key, fn) {\n Object.defineProperty(proto, key, {\n configurable: true,\n get() {\n return this == null ? fn : own(this, key, fn.bind(this));\n },\n set(value) {\n own(this, key, value);\n }\n });\n}\nfunction claim(inst, sentinel) {\n const proto = Object.getPrototypeOf(inst);\n return sentinel in proto ? void 0 : proto;\n}\nlet installing;\nlet broke = false;\nconst breaker = {\n configurable: true,\n get() {\n broke = true;\n }\n};\nfunction defineLazyInternal(inst, key, compute) {\n const proto = Object.getPrototypeOf(inst._zod);\n if (key in proto && installing !== inst._zod) {\n installing = void 0;\n return;\n }\n installing = inst._zod;\n Object.defineProperty(proto, key, {\n configurable: true,\n get() {\n Object.defineProperty(this, key, breaker);\n const outer = broke;\n broke = false;\n try {\n const value = compute(this);\n if (broke) delete this[key];\n else Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n value\n });\n broke = broke || outer;\n return value;\n } catch (err) {\n delete this[key];\n broke = broke || outer;\n throw err;\n }\n },\n set(value) {\n Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n value\n });\n }\n });\n}\nfunction installLazyProp(inst, key, make, enumerable) {\n const proto = claim(inst, key);\n if (!proto) return;\n Object.defineProperty(proto, key, {\n configurable: true,\n get() {\n const desc = {\n configurable: true,\n writable: true,\n enumerable,\n value: void 0\n };\n Object.defineProperty(this, key, desc);\n desc.value = make(this);\n Object.defineProperty(this, key, desc);\n return desc.value;\n },\n set(value) {\n Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n enumerable,\n value\n });\n }\n });\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/core.js\nvar _a;\nconst _zodDesc = {\n value: void 0,\n enumerable: false\n};\nlet _E = \"captureStackTrace\" in Error ? Error : null;\nfunction newError(Definition) {\n const E = _E;\n if (E) {\n const saved = E.stackTraceLimit;\n if (typeof saved === \"number\") {\n try {\n E.stackTraceLimit = 0;\n } catch {\n _E = null;\n return new Definition();\n }\n try {\n return new Definition();\n } finally {\n E.stackTraceLimit = saved;\n }\n }\n }\n return new Definition();\n}\nfunction $constructor(name, initializer, proto, params) {\n const zodProto = {};\n function Internals(def) {\n this.def = def;\n this.constr = _;\n this.traits = /* @__PURE__ */ new Set();\n }\n Internals.prototype = zodProto;\n const protoMembers = proto;\n const initialized = protoMembers && /* @__PURE__ */ new WeakSet();\n function init(inst, def) {\n if (!inst._zod) {\n _zodDesc.value = new Internals(def);\n try {\n Object.defineProperty(inst, \"_zod\", _zodDesc);\n } finally {\n _zodDesc.value = void 0;\n }\n } else if (inst._zod.traits.has(name)) return;\n inst._zod.traits.add(name);\n initializer(inst, def);\n if (initialized) {\n const own = Object.getPrototypeOf(inst);\n const ctorProto = inst._zod.constr.prototype;\n let up = own;\n while (up && up !== ctorProto) up = Object.getPrototypeOf(up);\n const target = up ?? own;\n if (!initialized.has(target)) {\n initialized.add(target);\n members(target, protoMembers);\n }\n }\n const proto = _.prototype;\n for (const k in proto) {\n if (!Object.prototype.hasOwnProperty.call(proto, k)) continue;\n if (!(k in inst)) inst[k] = proto[k].bind(inst);\n }\n }\n const Parent = params?.Parent ?? Object;\n class Definition extends Parent {}\n Object.defineProperty(Definition, \"name\", { value: name });\n function _(def) {\n const inst = params?.Parent ? newError(Definition) : this;\n init(inst, def);\n const deferred = inst._zod.deferred;\n if (deferred) {\n for (const fn of deferred) fn();\n inst._zod.deferred = void 0;\n }\n const pp = globalThis.__zod_globalConfig?.postProcessor;\n if (pp) pp(inst);\n return inst;\n }\n Object.defineProperty(_, \"init\", { value: init });\n Object.defineProperty(_, Symbol.hasInstance, { value: (inst) => {\n if (params?.Parent && inst instanceof params.Parent) return true;\n return inst?._zod?.traits?.has(name);\n } });\n Object.defineProperty(_, \"name\", { value: name });\n return _;\n}\nvar $ZodAsyncError = class extends Error {\n constructor() {\n super(`Encountered Promise during synchronous parse. Use .parseAsync() instead.`);\n }\n};\n(_a = globalThis).__zod_globalConfig ?? (_a.__zod_globalConfig = {});\nconst globalConfig = globalThis.__zod_globalConfig;\nfunction config(newConfig) {\n if (newConfig) Object.assign(globalConfig, newConfig);\n return globalConfig;\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/errors.js\nfunction _getMessage() {\n const internals = this._zod;\n internals.message ?? (internals.message = JSON.stringify(internals.def, jsonStringifyReplacer, 2));\n return internals.message;\n}\nfunction _setMessage(value) {\n this._zod.message = value;\n}\nconst _messageDesc = {\n get: _getMessage,\n set: _setMessage,\n enumerable: true,\n configurable: true\n};\nconst _issuesDesc = {\n value: void 0,\n enumerable: false\n};\nconst _installedToString = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]);\nconst initializer = (inst, def) => {\n inst.name = \"$ZodError\";\n _issuesDesc.value = def;\n Object.defineProperty(inst, \"issues\", _issuesDesc);\n _issuesDesc.value = void 0;\n Object.defineProperty(inst, \"message\", _messageDesc);\n const proto = Object.getPrototypeOf(inst);\n if (!_installedToString.has(proto)) {\n _installedToString.add(proto);\n Object.defineProperty(proto, \"toString\", {\n configurable: true,\n enumerable: false,\n get() {\n const value = () => this.message;\n Object.defineProperty(this, \"toString\", {\n value,\n configurable: true,\n writable: true\n });\n return value;\n },\n set(value) {\n Object.defineProperty(this, \"toString\", {\n value,\n configurable: true,\n writable: true\n });\n }\n });\n }\n};\nconst $ZodError = $constructor(\"$ZodError\", initializer);\nconst $ZodRealError = $constructor(\"$ZodError\", initializer, void 0, { Parent: Error });\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/parse.js\nconst _parse = (_Err) => {\n const fn = (schema, value, _ctx, _params) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: false\n } : { async: false };\n const result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) throw new $ZodAsyncError();\n if (result.issues.length) {\n const e = new ((_params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));\n captureStackTrace(e, _params?.callee ?? fn);\n throw e;\n }\n return result.value;\n };\n return fn;\n};\nconst parse = /* @__PURE__*/ _parse($ZodRealError);\nconst _parseAsync = (_Err) => {\n const fn = async (schema, value, _ctx, params) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: true\n } : { async: true };\n let result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) result = await result;\n if (result.issues.length) {\n const e = new ((params?.Err) ?? _Err)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())));\n captureStackTrace(e, params?.callee ?? fn);\n throw e;\n }\n return result.value;\n };\n return fn;\n};\nconst parseAsync = /* @__PURE__*/ _parseAsync($ZodRealError);\nconst _safeParse = (_Err) => (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: false\n } : { async: false };\n const result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) throw new $ZodAsyncError();\n return result.issues.length ? failure(_Err, result.issues, ctx) : {\n success: true,\n data: result.value\n };\n};\nconst safeParse = /* @__PURE__*/ _safeParse($ZodRealError);\nfunction failure(Err, issues, ctx) {\n let error;\n return {\n success: false,\n get error() {\n if (!error) {\n error = new Err(issues.map((iss) => finalizeIssue(iss, ctx, config())));\n issues = void 0;\n ctx = void 0;\n }\n return error;\n },\n set error(e) {\n error = e;\n issues = void 0;\n ctx = void 0;\n }\n };\n}\nconst _safeParseAsync = (_Err) => async (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n async: true\n } : { async: true };\n let result = schema._zod.run({\n value,\n issues: []\n }, ctx);\n if (result instanceof Promise) result = await result;\n return result.issues.length ? failure(_Err, result.issues, ctx) : {\n success: true,\n data: result.value\n };\n};\nconst safeParseAsync = /* @__PURE__*/ _safeParseAsync($ZodRealError);\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/regexes.js\nconst anyString = /^[\\s\\S]{0,}$/;\nconst number = /^-?\\d+(?:\\.\\d+)?$/;\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/checks.js\nconst $ZodCheck = /*@__PURE__*/ $constructor(\"$ZodCheck\", (inst, def) => {\n var _a;\n inst._zod ?? (inst._zod = {});\n inst._zod.def = def;\n (_a = inst._zod).onattach ?? (_a.onattach = []);\n});\nconst _whenHasLength = (payload) => {\n const val = payload.value;\n return !nullish(val) && val.length !== void 0;\n};\nconst $ZodCheckMinLength = /*@__PURE__*/ $constructor(\"$ZodCheckMinLength\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = _whenHasLength);\n inst._zod.check = (payload) => {\n const input = payload.value;\n const units = input.length;\n if ((typeof input === \"string\" && units >= def.minimum && units < def.minimum * 2 ? codePointLength(input) : units) >= def.minimum) return;\n const origin = getLengthableOrigin(input);\n payload.issues.push({\n origin,\n code: \"too_small\",\n minimum: def.minimum,\n inclusive: true,\n input,\n inst,\n continue: !def.abort\n });\n };\n});\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/versions.js\nconst version = {\n major: 4,\n minor: 6,\n patch: 5\n};\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/schemas.js\nconst $ZodType = /*@__PURE__*/ $constructor(\"$ZodType\", (inst, def) => {\n var _a;\n inst ?? (inst = {});\n inst._zod.def = def;\n inst._zod.bag = inst._zod.bag || {};\n inst._zod.version = version;\n const defChecks = inst._zod.def.checks;\n const checks = inst._zod.traits.has(\"$ZodCheck\") ? [inst, ...defChecks ?? []] : defChecks?.length ? [...defChecks] : [];\n for (const ch of checks) for (const fn of ch._zod.onattach) fn(inst);\n if (checks.length === 0) {\n (_a = inst._zod).deferred ?? (_a.deferred = []);\n inst._zod.deferred?.push(() => {\n inst._zod.run = inst._zod.parse;\n });\n } else {\n const runChecks = (payload, checks, ctx) => {\n if (payload.memo) return payload;\n let isAborted = aborted(payload);\n let asyncResult;\n for (const ch of checks) {\n if (ch._zod.def.when) {\n if (explicitlyAborted(payload)) continue;\n if (!ch._zod.def.when(payload)) continue;\n } else if (isAborted) continue;\n const currLen = payload.issues.length;\n const _ = ch._zod.check(payload);\n if (_ instanceof Promise && ctx?.async === false) throw new $ZodAsyncError();\n if (asyncResult || _ instanceof Promise) asyncResult = (asyncResult ?? Promise.resolve()).then(async () => {\n await _;\n if (payload.issues.length === currLen) return;\n attachSchema(payload.issues, currLen, inst);\n if (!isAborted) isAborted = aborted(payload, currLen);\n });\n else {\n if (payload.issues.length === currLen) continue;\n attachSchema(payload.issues, currLen, inst);\n if (!isAborted) isAborted = aborted(payload, currLen);\n }\n }\n if (asyncResult) return asyncResult.then(() => {\n return payload;\n });\n return payload;\n };\n const handleCanaryResult = (canary, payload, ctx) => {\n if (aborted(canary)) {\n canary.aborted = true;\n return canary;\n }\n const checkResult = runChecks(payload, checks, ctx);\n if (checkResult instanceof Promise) {\n if (ctx.async === false) throw new $ZodAsyncError();\n return checkResult.then((checkResult) => inst._zod.parse(checkResult, ctx));\n }\n return inst._zod.parse(checkResult, ctx);\n };\n inst._zod.run = (payload, ctx) => {\n if (ctx.skipChecks) return inst._zod.parse(payload, ctx);\n if (ctx.direction === \"backward\") {\n const canary = inst._zod.parse({\n value: payload.value,\n issues: []\n }, {\n ...ctx,\n skipChecks: true\n });\n if (canary instanceof Promise) return canary.then((canary) => {\n return handleCanaryResult(canary, payload, ctx);\n });\n return handleCanaryResult(canary, payload, ctx);\n }\n const result = inst._zod.parse(payload, ctx);\n if (result instanceof Promise) {\n if (ctx.async === false) throw new $ZodAsyncError();\n return result.then((result) => runChecks(result, checks, ctx));\n }\n return runChecks(result, checks, ctx);\n };\n }\n}, {\n get \"~standard\"() {\n return hide(this, \"~standard\", standardProps(this));\n },\n set \"~standard\"(value) {\n own(this, \"~standard\", value);\n }\n});\nconst toStandardResult = (r, ctx) => r.issues.length ? { issues: r.issues.map((iss) => finalizeIssue(iss, ctx, config())) } : { value: r.value };\nasync function validateAsync(inst, value) {\n const ctx = { async: true };\n return toStandardResult(await inst._zod.run({\n value,\n issues: []\n }, ctx), ctx);\n}\nfunction standardProps(inst) {\n return {\n validate: (value) => {\n const ctx = { async: false };\n try {\n const r = inst._zod.run({\n value,\n issues: []\n }, ctx);\n if (!(r instanceof Promise)) return toStandardResult(r, ctx);\n } catch (_) {}\n return validateAsync(inst, value);\n },\n vendor: \"zod\",\n version: 1\n };\n}\nconst $ZodString = /*@__PURE__*/ $constructor(\"$ZodString\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.pattern = def.pattern ?? anyString;\n inst._zod.parse = (payload, _) => {\n if (def.coerce) try {\n payload.value = String(payload.value);\n } catch (_) {}\n if (typeof payload.value === \"string\") return payload;\n payload.issues.push({\n expected: \"string\",\n code: \"invalid_type\",\n input: payload.value,\n inst\n });\n return payload;\n };\n});\nconst $ZodUnknown = /*@__PURE__*/ $constructor(\"$ZodUnknown\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload) => payload;\n});\nfunction handleArrayResult(result, final, index) {\n if (result.issues.length) final.issues.push(...prefixIssues(index, result.issues));\n final.value[index] = result.value;\n}\nconst $ZodArray = /*@__PURE__*/ $constructor(\"$ZodArray\", (inst, def) => {\n $ZodType.init(inst, def);\n const memo = globalConfig.memoizer;\n memo?.attach(inst);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!Array.isArray(input)) {\n payload.issues.push({\n expected: \"array\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n payload.value = memo ? memo.alloc(inst, payload, Array(input.length), ctx) : Array(input.length);\n const proms = [];\n const abortEarly = ctx?.abortEarly;\n for (let i = 0; i < input.length; i++) {\n const item = input[i];\n const result = def.element._zod.run({\n value: item,\n issues: []\n }, ctx);\n if (result instanceof Promise) proms.push(result.then((result) => handleArrayResult(result, payload, i)));\n else {\n handleArrayResult(result, payload, i);\n if (abortEarly && result.issues.length !== 0 && aborted(result)) break;\n }\n }\n if (proms.length) return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handlePropertyResult(result, final, key, input, optin, optout) {\n const isPresent = key in input;\n const isOptionalOut = optout === \"optional\";\n if (!isPresent && isOptionalOut && optin === \"optional\") return;\n if (result.issues.length) {\n if (optin !== void 0 && isOptionalOut && !isPresent) return;\n final.issues.push(...prefixIssues(key, result.issues));\n }\n if (!isPresent && optin === void 0) {\n if (!result.issues.length) final.issues.push({\n code: \"invalid_type\",\n expected: \"nonoptional\",\n input: void 0,\n path: [key]\n });\n return;\n }\n if (result.value === void 0) {\n if (isPresent || optin === \"defaulted\" && !isOptionalOut) final.value[key] = void 0;\n } else final.value[key] = result.value;\n}\nconst NO_SYMBOL_KEYS = [];\nfunction normalizeDef(def) {\n const keys = Object.keys(def.shape);\n const ownSymbols = Object.getOwnPropertySymbols(def.shape);\n const symbolKeys = ownSymbols.length ? ownSymbols : NO_SYMBOL_KEYS;\n const allKeys = symbolKeys.length ? [...keys, ...symbolKeys] : keys;\n for (const k of allKeys) if (!def.shape?.[k]?._zod?.traits?.has(\"$ZodType\")) throw new Error(`Invalid element at key \"${String(k)}\": expected a Zod schema`);\n const okeys = optionalKeys(def.shape);\n return {\n ...def,\n allKeys,\n symbolKeys,\n keySet: new Set(keys),\n numKeys: keys.length,\n optionalKeys: new Set(okeys)\n };\n}\nfunction handleCatchall(proms, input, payload, ctx, def, inst, abortEarly) {\n const unrecognized = [];\n const keySet = def.keySet;\n const _catchall = def.catchall._zod;\n const t = _catchall.def.type;\n const optin = _catchall.optin;\n const optout = _catchall.optout;\n let seen = 0;\n for (const key in input) {\n if (abortEarly && payload.issues.length !== seen) {\n if (aborted(payload, seen)) break;\n seen = payload.issues.length;\n }\n if (keySet.has(key)) continue;\n if (key === \"__proto__\") {\n if (t === \"never\") unrecognized.push(key);\n continue;\n }\n if (t === \"never\") {\n unrecognized.push(key);\n continue;\n }\n const r = _catchall.run({\n value: input[key],\n issues: []\n }, ctx);\n if (r instanceof Promise) proms.push(r.then((r) => handlePropertyResult(r, payload, key, input, optin, optout)));\n else handlePropertyResult(r, payload, key, input, optin, optout);\n }\n if (unrecognized.length) payload.issues.push({\n code: \"unrecognized_keys\",\n keys: unrecognized,\n input,\n inst,\n continue: true\n });\n if (!proms.length) return payload;\n return Promise.all(proms).then(() => {\n return payload;\n });\n}\nconst $ZodObject = /*@__PURE__*/ $constructor(\"$ZodObject\", (inst, def) => {\n $ZodType.init(inst, def);\n const desc = Object.getOwnPropertyDescriptor(def, \"shape\");\n const sh = desc?.get ? desc.get.raw : def.shape ?? {};\n if (sh) {\n const get = () => {\n const newSh = { ...sh };\n Object.defineProperty(def, \"shape\", { value: newSh });\n get.raw = newSh;\n return newSh;\n };\n get.raw = sh;\n Object.defineProperty(def, \"shape\", { get });\n }\n const _normalized = cached(() => normalizeDef(def));\n defineLazyInternal(inst, \"propValues\", (zod) => {\n const shape = zod.def.shape;\n const propValues = {};\n for (const key in shape) {\n const field = shape[key]._zod;\n if (field.values) {\n if (!Object.prototype.hasOwnProperty.call(propValues, key)) assignProp(propValues, key, /* @__PURE__ */ new Set());\n for (const v of field.values) propValues[key].add(v);\n if (field.optin !== void 0) propValues[key].add(void 0);\n }\n }\n return propValues;\n });\n const isObject$2 = isObject;\n const catchall = def.catchall;\n let value;\n const memo = globalConfig.memoizer;\n memo?.attach(inst);\n inst._zod.parse = (payload, ctx) => {\n value ?? (value = _normalized.value);\n const input = payload.value;\n if (!isObject$2(input)) {\n payload.issues.push({\n expected: \"object\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};\n const proms = [];\n const shape = value.shape;\n const abortEarly = ctx?.abortEarly;\n let seen = payload.issues.length;\n for (const key of value.allKeys) {\n if (abortEarly && payload.issues.length !== seen) {\n if (aborted(payload, seen)) break;\n seen = payload.issues.length;\n }\n if (key === \"__proto__\") continue;\n const el = shape[key];\n const optin = el._zod.optin;\n const optout = el._zod.optout;\n const r = el._zod.run({\n value: input[key],\n issues: []\n }, ctx);\n if (r instanceof Promise) proms.push(r.then((r) => handlePropertyResult(r, payload, key, input, optin, optout)));\n else handlePropertyResult(r, payload, key, input, optin, optout);\n }\n if (!catchall) return proms.length ? Promise.all(proms).then(() => payload) : payload;\n return handleCatchall(proms, input, payload, ctx, _normalized.value, inst, abortEarly === true);\n };\n});\nconst $ZodRecord = /*@__PURE__*/ $constructor(\"$ZodRecord\", (inst, def) => {\n $ZodType.init(inst, def);\n const memo = globalConfig.memoizer;\n memo?.attach(inst);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n if (!isPlainObject(input)) {\n payload.issues.push({\n expected: \"record\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n const proms = [];\n const values = def.keyType._zod.values;\n if (values && !def.partial) {\n payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};\n const recordKeys = /* @__PURE__ */ new Set();\n for (const key of values) if (typeof key === \"string\" || typeof key === \"number\" || typeof key === \"symbol\") {\n recordKeys.add(typeof key === \"number\" ? key.toString() : key);\n if (key === \"__proto__\") continue;\n const keyResult = def.keyType._zod.run({\n value: key,\n issues: []\n }, ctx);\n if (keyResult instanceof Promise) throw new Error(\"Async schemas not supported in object keys currently\");\n if (keyResult.issues.length) {\n payload.issues.push({\n code: \"invalid_key\",\n origin: \"record\",\n issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),\n input: key,\n path: [key],\n inst\n });\n continue;\n }\n const outKey = keyResult.value;\n if (outKey === \"__proto__\") continue;\n const result = def.valueType._zod.run({\n value: input[key],\n issues: []\n }, ctx);\n if (result instanceof Promise) proms.push(result.then((result) => {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }));\n else {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }\n }\n let unrecognized;\n for (const key in input) if (!recordKeys.has(key)) {\n if (def.mode === \"loose\") {\n if (key === \"__proto__\") continue;\n payload.value[key] = input[key];\n } else {\n unrecognized = unrecognized ?? [];\n unrecognized.push(key);\n }\n }\n if (unrecognized && unrecognized.length > 0) payload.issues.push({\n code: \"unrecognized_keys\",\n input,\n inst,\n keys: unrecognized,\n continue: true\n });\n } else {\n payload.value = memo ? memo.alloc(inst, payload, {}, ctx) : {};\n let unrecognized;\n for (const key of Reflect.ownKeys(input)) {\n if (key === \"__proto__\") continue;\n if (!Object.prototype.propertyIsEnumerable.call(input, key)) continue;\n let keyResult = def.keyType._zod.run({\n value: key,\n issues: []\n }, ctx);\n if (keyResult instanceof Promise) throw new Error(\"Async schemas not supported in object keys currently\");\n if (typeof key === \"string\" && number.test(key) && keyResult.issues.length) {\n const retryResult = def.keyType._zod.run({\n value: Number(key),\n issues: []\n }, ctx);\n if (retryResult instanceof Promise) throw new Error(\"Async schemas not supported in object keys currently\");\n if (retryResult.issues.length === 0) keyResult = retryResult;\n }\n if (keyResult.issues.length) {\n if (def.mode === \"loose\") payload.value[key] = input[key];\n else if (values) {\n unrecognized = unrecognized ?? [];\n unrecognized.push(key);\n } else payload.issues.push({\n code: \"invalid_key\",\n origin: \"record\",\n issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),\n input: key,\n path: [key],\n inst\n });\n continue;\n }\n const outKey = keyResult.value;\n if (outKey === \"__proto__\") continue;\n const result = def.valueType._zod.run({\n value: input[key],\n issues: []\n }, ctx);\n if (result instanceof Promise) proms.push(result.then((result) => {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }));\n else {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[outKey] = result.value;\n }\n }\n if (unrecognized && unrecognized.length > 0) payload.issues.push({\n code: \"unrecognized_keys\",\n input,\n inst,\n keys: unrecognized,\n continue: true\n });\n }\n if (proms.length) return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handleOptionalResult(payload, result) {\n payload.value = result.issues.length ? void 0 : result.value;\n return payload;\n}\nconst $ZodOptional = /*@__PURE__*/ $constructor(\"$ZodOptional\", (inst, def) => {\n $ZodType.init(inst, def);\n defineLazyInternal(inst, \"optin\", (zod) => zod.def.innerType._zod.optin === \"defaulted\" ? \"defaulted\" : \"optional\");\n inst._zod.optout = \"optional\";\n defineLazyInternal(inst, \"values\", (zod) => {\n const values = zod.def.innerType._zod.values;\n return values ? /* @__PURE__ */ new Set([...values, void 0]) : void 0;\n });\n defineLazyInternal(inst, \"pattern\", (zod) => {\n const pattern = zod.def.innerType._zod.pattern;\n return pattern ? new RegExp(`^(${cleanRegex(pattern.source)})?$`) : void 0;\n });\n inst._zod.parse = (payload, ctx) => {\n if (payload.value === void 0) {\n if (def.innerType._zod.optin !== \"defaulted\") return payload;\n const result = def.innerType._zod.run({\n value: payload.value,\n issues: []\n }, ctx);\n if (result instanceof Promise) return result.then((result) => handleOptionalResult(payload, result));\n return handleOptionalResult(payload, result);\n }\n return def.innerType._zod.run(payload, ctx);\n };\n});\nconst $ZodDefault = /*@__PURE__*/ $constructor(\"$ZodDefault\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.optin = \"defaulted\";\n defineLazyInternal(inst, \"values\", (zod) => zod.def.innerType._zod.values);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") return def.innerType._zod.run(payload, ctx);\n if (payload.value === void 0) {\n payload.value = def.defaultValue;\n return payload;\n }\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) return result.then((result) => handleDefaultResult(result, def));\n return handleDefaultResult(result, def);\n };\n});\nfunction handleDefaultResult(payload, def) {\n if (payload.value === void 0) payload.value = def.defaultValue;\n return payload;\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/locales/en.js\nconst error = () => {\n const Sizable = {\n string: {\n unit: \"characters\",\n verb: \"to have\"\n },\n file: {\n unit: \"bytes\",\n verb: \"to have\"\n },\n array: {\n unit: \"items\",\n verb: \"to have\"\n },\n set: {\n unit: \"items\",\n verb: \"to have\"\n },\n map: {\n unit: \"entries\",\n verb: \"to have\"\n }\n };\n function getSizing(origin) {\n return Sizable[origin] ?? null;\n }\n const FormatDictionary = {\n regex: \"input\",\n email: \"email address\",\n url: \"URL\",\n emoji: \"emoji\",\n uuid: \"UUID\",\n uuidv4: \"UUIDv4\",\n uuidv6: \"UUIDv6\",\n nanoid: \"nanoid\",\n guid: \"GUID\",\n cuid: \"cuid\",\n cuid2: \"cuid2\",\n ulid: \"ULID\",\n xid: \"XID\",\n ksuid: \"KSUID\",\n datetime: \"ISO datetime\",\n date: \"ISO date\",\n time: \"ISO time\",\n duration: \"ISO duration\",\n ipv4: \"IPv4 address\",\n ipv6: \"IPv6 address\",\n mac: \"MAC address\",\n cidrv4: \"IPv4 range\",\n cidrv6: \"IPv6 range\",\n base64: \"base64-encoded string\",\n base64url: \"base64url-encoded string\",\n json_string: \"JSON string\",\n e164: \"E.164 number\",\n currency_code: \"currency code\",\n credit_card: \"credit card number\",\n iban: \"IBAN\",\n jwt: \"JWT\",\n template_literal: \"input\"\n };\n const TypeDictionary = { nan: \"NaN\" };\n function getTypeName(type, input) {\n if (type === \"number\" && typeof input === \"number\" && !Number.isFinite(input)) return String(input);\n return TypeDictionary[type] ?? type;\n }\n return (issue) => {\n switch (issue.code) {\n case \"invalid_type\": return `Invalid input: expected ${getTypeName(issue.expected)}, received ${getTypeName(parsedType(issue.input), issue.input)}`;\n case \"invalid_value\":\n if (issue.values.length === 1) return `Invalid input: expected ${stringifyPrimitive(issue.values[0])}`;\n return `Invalid option: expected one of ${joinValues(issue.values, \"|\")}`;\n case \"too_big\": {\n const adj = issue.exact ? \"exactly \" : issue.inclusive ? \"<=\" : \"<\";\n const sizing = getSizing(issue.origin);\n if (sizing) return `Too big: expected ${issue.origin ?? \"value\"} to have ${adj}${issue.maximum.toString()} ${sizing.unit ?? \"elements\"}`;\n return `Too big: expected ${issue.origin ?? \"value\"} to be ${adj}${issue.maximum.toString()}`;\n }\n case \"too_small\": {\n const adj = issue.exact ? \"exactly \" : issue.inclusive ? \">=\" : \">\";\n const sizing = getSizing(issue.origin);\n if (sizing) return `Too small: expected ${issue.origin} to have ${adj}${issue.minimum.toString()} ${sizing.unit}`;\n return `Too small: expected ${issue.origin} to be ${adj}${issue.minimum.toString()}`;\n }\n case \"invalid_format\": {\n const _issue = issue;\n if (_issue.format === \"starts_with\") return `Invalid string: must start with \"${_issue.prefix}\"`;\n if (_issue.format === \"ends_with\") return `Invalid string: must end with \"${_issue.suffix}\"`;\n if (_issue.format === \"includes\") return `Invalid string: must include \"${_issue.includes}\"`;\n if (_issue.format === \"regex\") return `Invalid string: must match pattern ${_issue.pattern}`;\n return `Invalid ${FormatDictionary[_issue.format] ?? issue.format}`;\n }\n case \"not_multiple_of\": return `Invalid number: must be a multiple of ${issue.divisor}`;\n case \"unrecognized_keys\": return `Unrecognized key${issue.keys.length > 1 ? \"s\" : \"\"}: ${joinValues(issue.keys, \", \")}`;\n case \"invalid_key\": return `Invalid key in ${issue.origin}`;\n case \"invalid_union\":\n if (issue.options && Array.isArray(issue.options) && issue.options.length > 0) return `Invalid discriminator value. Expected ${issue.options.map((o) => `'${o}'`).join(\" | \")}`;\n if (issue.inclusive === false) return \"Invalid input: more than one option matched\";\n return \"Invalid input\";\n case \"invalid_element\": return `Invalid value in ${issue.origin}`;\n default: return `Invalid input`;\n }\n };\n};\nfunction en_default() {\n return { localeError: error() };\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/core/api.js\nfunction snapshotChecks(def) {\n if (def.checks) def.checks = [...def.checks];\n return def;\n}\n// @__NO_SIDE_EFFECTS__\nfunction _string(Class, params) {\n return new Class(snapshotChecks({\n type: \"string\",\n ...normalizeParams(params)\n }));\n}\n// @__NO_SIDE_EFFECTS__\nfunction _unknown(Class) {\n return new Class({ type: \"unknown\" });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _minLength(minimum, params) {\n return new $ZodCheckMinLength({\n check: \"min_length\",\n ...normalizeParams(params),\n minimum\n });\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/mini/schemas.js\nconst ZodMiniType = /*@__PURE__*/ $constructor(\"ZodMiniType\", (inst, def) => {\n if (!inst._zod) throw new Error(\"Uninitialized schema in ZodMiniType.\");\n $ZodType.init(inst, def);\n inst.def = def;\n inst.type = def.type;\n}, {\n get with() {\n return this.check;\n },\n set with(value) {\n own(this, \"with\", value);\n },\n parse(data, params) {\n return parse(this, data, params, { callee: this.parse });\n },\n parseAsync(data, params) {\n return parseAsync(this, data, params, { callee: this.parseAsync });\n },\n safeParse(data, params) {\n return safeParse(this, data, params);\n },\n safeParseAsync(data, params) {\n return safeParseAsync(this, data, params);\n },\n check(...checks) {\n const def = this.def;\n return this.clone({\n ...def,\n checks: [...def.checks ?? [], ...checks.map((ch) => typeof ch === \"function\" ? { _zod: {\n check: ch,\n def: { check: \"custom\" },\n onattach: []\n } } : ch)]\n }, { parent: true });\n },\n clone(_def, params) {\n return clone(this, _def, params);\n },\n brand() {\n return this;\n },\n register(reg, meta) {\n reg.add(this, meta);\n return this;\n },\n apply(fn, ...args) {\n return args.length === 0 ? fn(this) : fn(this, ...args);\n }\n});\nconst ZodMiniString = /*@__PURE__*/ $constructor(\"ZodMiniString\", (inst, def) => {\n $ZodString.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction string(params) {\n return _string(ZodMiniString, params);\n}\nconst ZodMiniUnknown = /*@__PURE__*/ $constructor(\"ZodMiniUnknown\", (inst, def) => {\n $ZodUnknown.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction unknown() {\n return _unknown(ZodMiniUnknown);\n}\nconst ZodMiniArray = /*@__PURE__*/ $constructor(\"ZodMiniArray\", (inst, def) => {\n $ZodArray.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction array(element, params) {\n return new ZodMiniArray({\n type: \"array\",\n element,\n ...normalizeParams(params)\n });\n}\nconst ZodMiniObject = /*@__PURE__*/ $constructor(\"ZodMiniObject\", (inst, def) => {\n $ZodObject.init(inst, def);\n ZodMiniType.init(inst, def);\n installLazyProp(inst, \"shape\", (self) => self._zod.def.shape, false);\n});\n// @__NO_SIDE_EFFECTS__\nfunction object(shape, params) {\n const def = {\n type: \"object\",\n shape: shape ?? {},\n ...normalizeParams(params)\n };\n return new ZodMiniObject(def);\n}\nconst ZodMiniRecord = /*@__PURE__*/ $constructor(\"ZodMiniRecord\", (inst, def) => {\n $ZodRecord.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction record(keyType, valueType, params) {\n if (!valueType || !valueType._zod) return new ZodMiniRecord({\n type: \"record\",\n keyType: /* @__PURE__ */ string(),\n valueType: keyType,\n ...normalizeParams(valueType)\n });\n return new ZodMiniRecord({\n type: \"record\",\n keyType,\n valueType,\n ...normalizeParams(params)\n });\n}\nconst ZodMiniOptional = /*@__PURE__*/ $constructor(\"ZodMiniOptional\", (inst, def) => {\n $ZodOptional.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction optional(innerType) {\n return new ZodMiniOptional({\n type: \"optional\",\n innerType\n });\n}\nconst ZodMiniDefault = /*@__PURE__*/ $constructor(\"ZodMiniDefault\", (inst, def) => {\n $ZodDefault.init(inst, def);\n ZodMiniType.init(inst, def);\n});\n// @__NO_SIDE_EFFECTS__\nfunction _default(innerType, defaultValue) {\n return new ZodMiniDefault({\n type: \"default\",\n innerType,\n get defaultValue() {\n return typeof defaultValue === \"function\" ? defaultValue() : shallowClone(defaultValue);\n }\n });\n}\n\n//#endregion\n//#region src/lib/hook-io.ts\nconfig(en_default());\nconst HookInputSchema = object({\n session_id: string(),\n transcript_path: string(),\n cwd: string(),\n permission_mode: string(),\n hook_event_name: string(),\n tool_name: string(),\n tool_input: record(string(), unknown())\n});\nasync function readHookInput() {\n const chunks = [];\n for await (const chunk of process.stdin) chunks.push(typeof chunk === \"string\" ? chunk : String(chunk));\n let parsed;\n try {\n parsed = JSON.parse(chunks.join(\"\"));\n } catch (_error) {\n return {\n ok: false,\n raw: chunks.join(\"\").slice(0, 200),\n error: \"invalid JSON\"\n };\n }\n const result = HookInputSchema.safeParse(parsed);\n if (!result.success) return {\n ok: false,\n raw: parsed,\n error: result.error.message\n };\n return {\n ok: true,\n data: result.data\n };\n}\nconst BashInputSchema = object({\n command: string(),\n description: optional(string())\n});\nconst FilePathInputSchema = object({ file_path: string() });\nconst SearchInputSchema = object({\n pattern: string(),\n path: optional(string())\n});\nfunction parseBashInput(toolInput) {\n const result = BashInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction parseFilePathInput(toolInput) {\n const result = FilePathInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction parseSearchInput(toolInput) {\n const result = SearchInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction writeDeny(reason) {\n const decision = { hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: \"deny\",\n ...reason ? { permissionDecisionReason: reason } : {}\n } };\n process.stdout.write(JSON.stringify(decision));\n}\nfunction writeAdvice(advice) {\n const decision = { hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n additionalContext: advice\n } };\n process.stdout.write(JSON.stringify(decision));\n}\n\n//#endregion\n//#region src/lib/audit.ts\nconst MAX_LOG_SIZE = 1048576;\nfunction getLogPath() {\n const hookDir = path.dirname(fileURLToPath(import.meta.url));\n return path.join(hookDir, \"bst-hook.audit.local.jsonl\");\n}\nfunction rotateIfNeeded(logPath) {\n try {\n if (fs.statSync(logPath).size > MAX_LOG_SIZE) {\n const rotatedPath = `${logPath}.1`;\n fs.renameSync(logPath, rotatedPath);\n }\n } catch (_error) {}\n}\nlet rotationChecked = false;\nfunction audit(entry) {\n try {\n const logPath = getLogPath();\n if (!rotationChecked) {\n rotateIfNeeded(logPath);\n rotationChecked = true;\n }\n const record = {\n timestamp: (/* @__PURE__ */ new Date()).toISOString(),\n ...entry\n };\n fs.appendFileSync(logPath, JSON.stringify(record) + \"\\n\");\n } catch (err) {\n process.stderr.write(`[bst-hook] audit write failed: ${err instanceof Error ? err.message : String(err)}\\n`);\n }\n}\n\n//#endregion\n//#region src/lib/shell-tokenizer.ts\nfunction scanDoubleQuoted(input, start) {\n let content = \"\";\n let i = start;\n while (i < input.length && input[i] !== \"\\\"\") {\n if (input[i] === \"\\\\\" && i + 1 < input.length) {\n const next = input[i + 1];\n if (next === \"\\\"\" || next === \"\\\\\" || next === \"$\" || next === \"`\") {\n content += next;\n i += 2;\n continue;\n }\n }\n content += input[i];\n i++;\n }\n if (i >= input.length) return null;\n return {\n content,\n next: i + 1\n };\n}\nfunction tokenize(input) {\n const tokens = [];\n let current = \"\";\n let i = 0;\n let inToken = false;\n while (i < input.length) {\n const ch = input[i];\n if (ch === \"'\") {\n inToken = true;\n i++;\n while (i < input.length && input[i] !== \"'\") {\n current += input[i];\n i++;\n }\n if (i >= input.length) return {\n ok: false,\n error: \"unterminated single quote\"\n };\n i++;\n } else if (ch === \"\\\"\") {\n inToken = true;\n const scanned = scanDoubleQuoted(input, i + 1);\n if (!scanned) return {\n ok: false,\n error: \"unterminated double quote\"\n };\n current += scanned.content;\n i = scanned.next;\n } else if (ch === \"\\\\\" && i + 1 < input.length) {\n inToken = true;\n current += input[i + 1];\n i += 2;\n } else if (ch === \" \" || ch === \" \") {\n if (inToken) {\n tokens.push(current);\n current = \"\";\n inToken = false;\n }\n i++;\n } else {\n inToken = true;\n current += ch;\n i++;\n }\n }\n if (inToken) tokens.push(current);\n return {\n ok: true,\n tokens\n };\n}\nconst SAFE_UNQUOTED_RE = /^[A-Za-z0-9_\\-./=:@%+,]+$/;\nfunction quoteToken(token) {\n if (token.length > 0 && SAFE_UNQUOTED_RE.test(token)) return token;\n return \"'\" + token.replace(/'/g, \"'\\\\''\") + \"'\";\n}\nfunction joinTokens(tokens) {\n return tokens.map(quoteToken).join(\" \");\n}\n\n//#endregion\n//#region src/lib/shell-splitter.ts\nfunction splitCommand(input) {\n if (!input || !input.trim()) return [];\n const segments = [];\n let current = \"\";\n let i = 0;\n let inSingleQuote = false;\n let inDoubleQuote = false;\n while (i < input.length) {\n const ch = input[i];\n if (ch === \"'\" && !inDoubleQuote) {\n inSingleQuote = !inSingleQuote;\n current += ch;\n i++;\n continue;\n }\n if (ch === \"\\\"\" && !inSingleQuote) {\n inDoubleQuote = !inDoubleQuote;\n current += ch;\n i++;\n continue;\n }\n if (ch === \"\\\\\" && !inSingleQuote && i + 1 < input.length) {\n current += ch + input[i + 1];\n i += 2;\n continue;\n }\n if (!inSingleQuote && !inDoubleQuote) {\n if (ch === \"&\" && i + 1 < input.length && input[i + 1] === \"&\") {\n pushSegment(segments, current, \"&&\");\n current = \"\";\n i += 2;\n continue;\n }\n if (ch === \"&\" && !(i + 1 < input.length && input[i + 1] === \">\") && !(i > 0 && (input[i - 1] === \">\" || input[i - 1] === \"<\"))) {\n pushSegment(segments, current, \"&\");\n current = \"\";\n i++;\n continue;\n }\n if (ch === \"|\" && i + 1 < input.length && input[i + 1] === \"|\") {\n pushSegment(segments, current, \"||\");\n current = \"\";\n i += 2;\n continue;\n }\n if (ch === \"|\") {\n pushSegment(segments, current, \"|\");\n current = \"\";\n i++;\n continue;\n }\n if (ch === \";\") {\n pushSegment(segments, current, \";\");\n current = \"\";\n i++;\n continue;\n }\n }\n current += ch;\n i++;\n }\n const trimmed = current.trim();\n if (trimmed) segments.push({\n command: trimmed,\n operator: null\n });\n return segments;\n}\nfunction pushSegment(segments, text, operator) {\n const trimmed = text.trim();\n if (trimmed) segments.push({\n command: trimmed,\n operator\n });\n}\n\n//#endregion\n//#region src/lib/injection-guard.ts\nfunction hasBraceExpansion(token) {\n let depth = 0;\n for (let i = 0; i < token.length; i++) {\n const ch = token[i];\n if (ch === \"{\") depth++;\n else if (ch === \"}\") {\n if (depth > 0) depth--;\n } else if (depth >= 1 && (ch === \",\" || ch === \".\" && token[i + 1] === \".\")) return true;\n }\n return false;\n}\nfunction checkInjection(tokens, rawSegment) {\n if (/[\\n\\r]/.test(rawSegment)) return {\n safe: false,\n reason: \"unquoted newline in command\"\n };\n for (const token of tokens) {\n if (token === \"`\") return {\n safe: false,\n reason: \"backtick command substitution\"\n };\n if (token.includes(\"`\")) return {\n safe: false,\n reason: \"backtick command substitution\"\n };\n if (token.includes(\"$\")) return {\n safe: false,\n reason: \"unresolved dollar-expansion\"\n };\n if (hasBraceExpansion(token)) return {\n safe: false,\n reason: \"brace expansion\"\n };\n if (token.includes(\"<(\")) return {\n safe: false,\n reason: \"process substitution\"\n };\n if (token.includes(\">(\")) return {\n safe: false,\n reason: \"process substitution\"\n };\n }\n return { safe: true };\n}\n\n//#endregion\n//#region src/lib/env-file.ts\nconst ENV_SAFE_SUFFIXES = [\n \".sample\",\n \".example\",\n \".template\"\n];\nconst ENV_TOOL_OWNED_SUFFIXES = [\".check.env\"];\nconst ENV_FILE_PATTERN = /(^|[/\\\\])(\\.env(\\..*)?|.*\\.env(\\..*)?|\\.env)$/;\nfunction isEnvSafeSuffix(filename) {\n return [...ENV_SAFE_SUFFIXES, ...ENV_TOOL_OWNED_SUFFIXES].some((suffix) => filename.endsWith(suffix));\n}\nfunction isEnvFile(filePath) {\n if (isEnvSafeSuffix(path.basename(filePath))) return false;\n return ENV_FILE_PATTERN.test(filePath);\n}\n\n//#endregion\n//#region src/lib/git-state.ts\nconst GIT_TIMEOUT_MS = 5e3;\nfunction getCurrentBranch(cwd) {\n try {\n const out = execFileSync(\"git\", [\n \"rev-parse\",\n \"--abbrev-ref\",\n \"HEAD\"\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!out || out === \"HEAD\") return null;\n return out;\n } catch {\n return null;\n }\n}\nfunction getRepoTopLevel(cwd) {\n try {\n const out = execFileSync(\"git\", [\"rev-parse\", \"--show-toplevel\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n return out === \"\" ? null : out;\n } catch {\n return null;\n }\n}\nfunction getRemoteDefaultBranch(cwd) {\n try {\n const out = execFileSync(\"git\", [\n \"symbolic-ref\",\n \"--short\",\n \"refs/remotes/origin/HEAD\"\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!out) return null;\n return out.startsWith(\"origin/\") ? out.slice(7) : out;\n } catch {\n return null;\n }\n}\nconst DefaultBranchSchema = object({ defaultBranch: optional(string().check(_minLength(1))) });\nfunction getConfiguredDefaultBranch(topLevel) {\n let parsed;\n try {\n parsed = JSON.parse(readFileSync(join(topLevel, \".tooling.json\"), \"utf-8\"));\n } catch {\n return null;\n }\n const result = DefaultBranchSchema.safeParse(parsed);\n return result.success ? result.data.defaultBranch ?? null : null;\n}\nconst ALWAYS_PROTECTED = [\"main\", \"master\"];\nfunction getProtectedBranches(cwd, extra) {\n const set = new Set(ALWAYS_PROTECTED);\n const remote = getRemoteDefaultBranch(cwd);\n if (remote) set.add(remote);\n for (const b of extra) set.add(b);\n return set;\n}\nfunction getUpstream(cwd, branch) {\n try {\n return execFileSync(\"git\", [\n \"rev-parse\",\n \"--abbrev-ref\",\n `${branch}@{upstream}`\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim() || null;\n } catch {\n return null;\n }\n}\nfunction resolveCommit(cwd, rev) {\n try {\n return execFileSync(\"git\", [\n \"rev-parse\",\n \"--verify\",\n \"--quiet\",\n `${rev}^{commit}`\n ], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim() || null;\n } catch {\n return null;\n }\n}\nfunction branchCarriesOtherFiles(cwd, allowed) {\n const toplevel = getRepoTopLevel(cwd);\n if (toplevel === null) return null;\n try {\n const defaultBranch = getConfiguredDefaultBranch(toplevel) ?? getRemoteDefaultBranch(toplevel) ?? \"main\";\n const mergeBase = execFileSync(\"git\", [\n \"merge-base\",\n \"HEAD\",\n `origin/${defaultBranch}`\n ], {\n cwd: toplevel,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!mergeBase) return null;\n const names = (args) => execFileSync(\"git\", args, {\n cwd: toplevel,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().split(\"\\n\").filter((line) => line.length > 0);\n return [...names([\n \"diff\",\n \"--name-only\",\n mergeBase\n ]), ...names([\n \"ls-files\",\n \"--others\",\n \"--exclude-standard\"\n ])].some((name) => !allowed.includes(name));\n } catch {\n return null;\n }\n}\nfunction isPrimaryWorktree(cwd) {\n try {\n const gitDir = execFileSync(\"git\", [\"rev-parse\", \"--absolute-git-dir\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n const commonRaw = execFileSync(\"git\", [\"rev-parse\", \"--git-common-dir\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ],\n timeout: GIT_TIMEOUT_MS\n }).toString().trim();\n if (!gitDir || !commonRaw) return false;\n const commonDir = resolve(cwd, commonRaw);\n return realpathSync(gitDir) === realpathSync(commonDir);\n } catch {\n return false;\n }\n}\n\n//#endregion\n//#region src/lib/git-short-options.ts\nconst VALUE_TAKING_LETTERS = {\n switch: /* @__PURE__ */ new Set([\"c\", \"C\"]),\n checkout: /* @__PURE__ */ new Set([\"b\", \"B\"]),\n branch: /* @__PURE__ */ new Set([\"u\"]),\n fetch: /* @__PURE__ */ new Set([\"o\", \"j\"])\n};\nfunction splitShortOptionCluster(token, sub) {\n if (!token.startsWith(\"-\") || token.startsWith(\"--\") || token.length <= 1) return;\n const valueLetters = VALUE_TAKING_LETTERS[sub];\n const letters = [];\n const chars = token.slice(1);\n for (let i = 0; i < chars.length; i++) {\n const ch = chars[i];\n letters.push(ch);\n if (valueLetters.has(ch)) return {\n letters,\n attachedValue: chars.slice(i + 1)\n };\n }\n return { letters };\n}\n\n//#endregion\n//#region src/lib/path-scope.ts\nfunction pathFlavorFor(base) {\n return /^[a-zA-Z]:[\\\\/]/.test(base) ? path.win32 : path.posix;\n}\nfunction resolveIn(base, target) {\n return pathFlavorFor(base).resolve(base, target);\n}\nfunction isWithin(dir, target) {\n const pp = pathFlavorFor(dir);\n const rel = pp.relative(dir, target);\n if (rel === \"\") return true;\n if (rel === \"..\" || rel.startsWith(\"..\" + pp.sep)) return false;\n return !pp.isAbsolute(rel);\n}\nfunction samePath(a, b) {\n return pathFlavorFor(a).relative(a, b) === \"\";\n}\nfunction targetsGitDir(filePath, cwd) {\n const pp = pathFlavorFor(cwd);\n const resolved = resolveIn(cwd, filePath);\n if (!isWithin(cwd, resolved)) return false;\n const rel = pp.relative(cwd, resolved);\n if (rel === \"\") return false;\n return rel.split(pp.sep).includes(\".git\");\n}\nfunction targetsNodeModules(filePath, cwd) {\n return resolveIn(cwd, filePath).split(/[/\\\\]/).includes(\"node_modules\");\n}\n\n//#endregion\n//#region src/rules.ts\nconst GIT_FLAGS_WITH_ARG = /* @__PURE__ */ new Set([\n \"-C\",\n \"-c\",\n \"--git-dir\",\n \"--work-tree\"\n]);\nfunction getGitSubcommand(tokens) {\n const args = tokens.slice(1);\n for (let i = 0; i < args.length; i++) {\n const t = args[i];\n if (GIT_FLAGS_WITH_ARG.has(t)) {\n i++;\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n return t;\n }\n}\nconst WORKTREE_ADVICE = \"You're creating a feature branch in the primary worktree. An isolated worktree keeps this work independent of other windows, and with no commits on this branch yet, converting is cheap -- `git worktree add ../<dir> -b <branch>` (a sibling outside the repo, with its own dependency install). Claude Code keeps this session inside its own working tree, so the work continues in a new session opened in the sibling worktree. If you cannot open a session there, stay in the primary checkout on this branch and check `git branch --show-current` before each commit.\";\nfunction isGitBranchCreation(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"checkout\" && sub !== \"switch\") return false;\n const after = tokens.slice(tokens.indexOf(sub) + 1);\n const creationLetters = sub === \"checkout\" ? [\"b\", \"B\"] : [\"c\", \"C\"];\n return after.some((t) => {\n const cluster = splitShortOptionCluster(t, sub);\n return cluster !== void 0 && cluster.letters.some((l) => creationLetters.includes(l));\n });\n}\nfunction worktreeAdvice(tokens, ctx) {\n return isGitBranchCreation(tokens) && isPrimaryWorktree(ctx.cwd) ? WORKTREE_ADVICE : void 0;\n}\nconst GIT_FORCE_CREATE_LETTER = {\n switch: \"C\",\n checkout: \"B\"\n};\nconst GIT_FORCE_CREATE_LONG = {\n flag: \"--force-create\",\n shortest: \"--force-\"\n};\nfunction gitForceCreateTargets(after, letterSub) {\n const letter = GIT_FORCE_CREATE_LETTER[letterSub];\n const longFlag = GIT_FORCE_CREATE_LONG;\n const targets = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (letterSub === \"switch\") {\n const [flag, ...rest] = t.split(\"=\");\n if (flag !== void 0 && flag.length >= longFlag.shortest.length && longFlag.flag.startsWith(flag)) {\n const target = rest.length > 0 ? rest.join(\"=\") : after[i + 1];\n if (target !== void 0) targets.push(target);\n continue;\n }\n }\n const cluster = splitShortOptionCluster(t, letterSub);\n if (cluster === void 0 || !cluster.letters.includes(letter)) continue;\n if (cluster.attachedValue !== void 0 && cluster.attachedValue !== \"\") {\n targets.push(cluster.attachedValue);\n continue;\n }\n const next = after[i + 1];\n if (next !== void 0) targets.push(next);\n }\n return targets;\n}\nfunction gitCommandForceCreateTargets(tokens) {\n const sub = getGitSubcommand(tokens);\n if (sub === \"switch\" || sub === \"checkout\") return gitForceCreateTargets(tokens.slice(tokens.indexOf(sub) + 1), sub);\n if (sub === \"worktree\") {\n const worktreeIdx = tokens.indexOf(\"worktree\");\n if (tokens[worktreeIdx + 1] !== \"add\") return [];\n return gitForceCreateTargets(tokens.slice(worktreeIdx + 2), \"checkout\");\n }\n return [];\n}\nfunction isGitProtectedBranchForceCreate(tokens, ctx) {\n const targets = gitCommandForceCreateTargets(tokens);\n if (targets.length === 0) return false;\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n return targets.some((t) => protectedSet.has(t));\n}\nfunction isGitProtectedBranchRefWrite(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"branch\") return false;\n const { flags, positionals } = splitGitBranchArgs(tokens.slice(tokens.indexOf(sub) + 1));\n if (positionals.length === 0) return false;\n let deleteMode = false;\n let renameMode = false;\n let copyMode = false;\n let forceMode = false;\n for (const flag of flags) {\n if (flag.letters !== void 0) {\n if (flag.letters.includes(\"d\") || flag.letters.includes(\"D\")) deleteMode = true;\n if (flag.letters.includes(\"m\") || flag.letters.includes(\"M\")) renameMode = true;\n if (flag.letters.includes(\"c\") || flag.letters.includes(\"C\")) copyMode = true;\n if (flag.letters.includes(\"f\") || flag.letters.includes(\"M\") || flag.letters.includes(\"C\")) forceMode = true;\n continue;\n }\n if (flag.name === \"delete\") deleteMode = true;\n if (flag.name === \"move\") renameMode = true;\n if (flag.name === \"copy\") copyMode = true;\n if (flag.name === \"force\") forceMode = true;\n }\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n if (deleteMode && positionals.some((p) => protectedSet.has(p))) return true;\n if (renameMode && positionals.length >= 2 && protectedSet.has(positionals[0])) return true;\n if (forceMode) {\n const destination = renameMode || copyMode ? positionals[positionals.length - 1] : positionals[0];\n if (destination !== void 0 && protectedSet.has(destination)) return true;\n }\n return false;\n}\nfunction refspecDestination(refspec) {\n const spec = refspec.startsWith(\"+\") ? refspec.slice(1) : refspec;\n const colon = spec.lastIndexOf(\":\");\n return (colon === -1 ? spec : spec.slice(colon + 1)).replace(/^refs\\/heads\\//, \"\");\n}\nconst GIT_BRANCH_VALUE_FLAG_NAMES = /* @__PURE__ */ new Set([\n \"contains\",\n \"no-contains\",\n \"merged\",\n \"no-merged\",\n \"points-at\",\n \"sort\",\n \"format\",\n \"set-upstream-to\"\n]);\nfunction splitGitBranchArgs(after) {\n const flags = [];\n const positionals = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (t.startsWith(\"--\") && t !== \"--\") {\n const eq = t.indexOf(\"=\");\n const name = eq > 0 ? t.slice(2, eq) : t.slice(2);\n flags.push({\n raw: t,\n name\n });\n if (eq < 0 && GIT_BRANCH_VALUE_FLAG_NAMES.has(name)) i++;\n continue;\n }\n const cluster = splitShortOptionCluster(t, \"branch\");\n if (cluster === void 0) {\n flags.push({ raw: t });\n continue;\n }\n flags.push({\n raw: t,\n letters: cluster.letters\n });\n if (cluster.attachedValue === \"\") i++;\n }\n return {\n flags,\n positionals\n };\n}\nconst GIT_FETCH_VALUE_FLAG_NAMES = /* @__PURE__ */ new Set([\n \"depth\",\n \"deepen\",\n \"shallow-since\",\n \"shallow-exclude\",\n \"negotiation-tip\",\n \"filter\",\n \"jobs\",\n \"server-option\",\n \"upload-pack\",\n \"refmap\",\n \"submodule-prefix\",\n \"recurse-submodules-default\"\n]);\nfunction splitGitFetchArgs(after) {\n const flags = [];\n const positionals = [];\n const refmaps = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === void 0) continue;\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (t === \"--\") {\n flags.push({ raw: t });\n continue;\n }\n if (t.startsWith(\"--\")) {\n const eq = t.indexOf(\"=\");\n const name = eq > 0 ? t.slice(2, eq) : t.slice(2);\n flags.push({\n raw: t,\n name\n });\n if (name === \"refmap\") {\n if (eq > 0) refmaps.push(t.slice(eq + 1));\n else if (after[i + 1] !== void 0) refmaps.push(after[i + 1]);\n }\n if (eq < 0 && GIT_FETCH_VALUE_FLAG_NAMES.has(name)) i++;\n continue;\n }\n const cluster = splitShortOptionCluster(t, \"fetch\");\n if (cluster === void 0) {\n flags.push({ raw: t });\n continue;\n }\n flags.push({\n raw: t,\n letters: cluster.letters\n });\n if (cluster.attachedValue === \"\") i++;\n }\n return {\n flags,\n positionals,\n refmaps\n };\n}\nfunction fetchRefspecTarget(spec) {\n const body = spec.startsWith(\"+\") ? spec.slice(1) : spec;\n if (body.startsWith(\"^\")) return { kind: \"none\" };\n if (!body.includes(\":\")) return { kind: \"none\" };\n const destination = refspecDestination(body);\n if (destination === \"\") return { kind: \"none\" };\n if (destination.startsWith(\"refs/remotes/\")) return { kind: \"tracking\" };\n if (destination.startsWith(\"refs/\")) return { kind: \"other\" };\n return {\n kind: \"branch\",\n name: destination\n };\n}\nfunction globToAnchoredRegex(glob) {\n const escaped = glob.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n return new RegExp(`^${escaped.replace(/\\\\\\*/g, \".*\")}$`);\n}\nfunction isProtectedFetchBranch(name, protectedSet) {\n if (protectedSet.has(name)) return true;\n if (!name.includes(\"*\")) return false;\n const pattern = globToAnchoredRegex(name);\n for (const member of protectedSet) if (pattern.test(member)) return true;\n return false;\n}\nfunction isGitProtectedBranchFetchWrite(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"fetch\") return false;\n const { positionals, refmaps } = splitGitFetchArgs(tokens.slice(tokens.indexOf(sub) + 1));\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n return [...positionals, ...refmaps].some((spec) => {\n const target = fetchRefspecTarget(spec);\n if (target.kind !== \"branch\" || target.name === void 0) return false;\n return isProtectedFetchBranch(target.name, protectedSet);\n });\n}\nfunction isGitProtectedUpdateRef(tokens, ctx) {\n const after = tokens.slice(tokens.indexOf(\"update-ref\") + 1);\n let ref;\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (t === \"-m\") {\n i++;\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n ref = t;\n break;\n }\n if (ref === void 0) return false;\n return getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []).has(ref.replace(/^refs\\/heads\\//, \"\"));\n}\nfunction gitEffectiveCwd(tokens, cwd) {\n let dir = cwd;\n const args = tokens.slice(1);\n for (let i = 0; i < args.length; i++) {\n const t = args[i];\n if (t === \"-C\") {\n const next = args[i + 1];\n if (next !== void 0) dir = path.resolve(dir, next);\n i++;\n continue;\n }\n if (GIT_FLAGS_WITH_ARG.has(t)) {\n i++;\n continue;\n }\n if (!t.startsWith(\"-\")) break;\n }\n return dir;\n}\nconst GIT_SWITCH_NEUTRAL_FLAGS = /* @__PURE__ */ new Set([\n \"-q\",\n \"--quiet\",\n \"-f\",\n \"--force\",\n \"--discard-changes\",\n \"-m\",\n \"--merge\",\n \"--no-track\",\n \"--progress\",\n \"--no-progress\",\n \"--guess\",\n \"--no-guess\",\n \"--ignore-other-worktrees\",\n \"--recurse-submodules\",\n \"--no-recurse-submodules\"\n]);\nconst GIT_SWITCH_CREATE_FLAGS = /* @__PURE__ */ new Set([\n \"--create\",\n \"--force-create\",\n \"--orphan\"\n]);\nconst GIT_CHECKOUT_PATH_FLAGS = /* @__PURE__ */ new Set([\n \"--ours\",\n \"--theirs\",\n \"-p\",\n \"--patch\",\n \"--conflict\",\n \"--pathspec-from-file\",\n \"--pathspec-file-nul\",\n \"--overlay\",\n \"--no-overlay\"\n]);\nconst GIT_SWITCH_NEUTRAL_LETTERS = /* @__PURE__ */ new Set([\n \"q\",\n \"f\",\n \"m\"\n]);\nfunction switchTarget(after, sub, isCheckoutBranch) {\n if (sub === \"checkout\" && after.includes(\"--\")) return void 0;\n const createLetters = sub === \"checkout\" ? [\"b\", \"B\"] : [\"c\", \"C\"];\n const known = (l) => GIT_SWITCH_NEUTRAL_LETTERS.has(l) || createLetters.includes(l);\n const positionals = [];\n let created;\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (GIT_SWITCH_NEUTRAL_FLAGS.has(t)) continue;\n const eq = t.indexOf(\"=\");\n if (sub === \"checkout\" && GIT_CHECKOUT_PATH_FLAGS.has(eq === -1 ? t : t.slice(0, eq))) return;\n if (GIT_SWITCH_CREATE_FLAGS.has(eq === -1 ? t : t.slice(0, eq))) {\n created = eq === -1 ? after[++i] : t.slice(eq + 1);\n continue;\n }\n const cluster = splitShortOptionCluster(t, sub);\n if (cluster === void 0 || !cluster.letters.every(known)) return null;\n if (cluster.letters.some((l) => createLetters.includes(l))) created = cluster.attachedValue ? cluster.attachedValue : after[++i];\n }\n if (created !== void 0) return created;\n if (positionals.length !== 1) return void 0;\n const target = positionals[0];\n if (target === \"HEAD\" || target === \"@\") return void 0;\n if (sub === \"checkout\" && !isCheckoutBranch(target)) return void 0;\n return target;\n}\nfunction branchSwitch(tokens, ctx, resolved) {\n const sub = getGitSubcommand(tokens);\n if (sub !== \"switch\" && sub !== \"checkout\") return void 0;\n const dir = gitEffectiveCwd(tokens, ctx.cwd);\n const isCheckoutBranch = (name) => !resolved || getProtectedBranches(dir, ctx.repoProtectedBranches ?? []).has(name) || resolveCommit(dir, name) !== null;\n const target = switchTarget(tokens.slice(tokens.indexOf(sub) + 1), sub, isCheckoutBranch);\n if (!resolved) return target === void 0 ? void 0 : {\n dir,\n branch: null\n };\n return target === void 0 ? void 0 : {\n dir,\n branch: target\n };\n}\nconst GIT_CHECKED_OUT_WRITES = /* @__PURE__ */ new Set([\n \"commit\",\n \"merge\",\n \"rebase\",\n \"cherry-pick\",\n \"revert\",\n \"am\",\n \"pull\"\n]);\nfunction subcommandArgs(tokens, sub, valueFlags = /* @__PURE__ */ new Set()) {\n const after = tokens.slice(tokens.indexOf(sub) + 1);\n const flags = [];\n const positionals = [];\n for (let i = 0; i < after.length; i++) {\n const t = after[i];\n if (!t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n flags.push(t);\n if (valueFlags.has(t)) i++;\n }\n return {\n flags,\n positionals\n };\n}\nconst GIT_PUSH_VALUE_FLAGS = /* @__PURE__ */ new Set([\n \"-o\",\n \"--push-option\",\n \"--repo\",\n \"--receive-pack\",\n \"--exec\"\n]);\nfunction isMergeFastForwardFromUpstream(tokens, upstream) {\n const { flags, positionals } = subcommandArgs(tokens, \"merge\");\n if (!flags.includes(\"--ff-only\") || positionals.length !== 1) return false;\n const ref = positionals[0];\n return ref === \"@{u}\" || ref === \"@{upstream}\" || upstream !== null && ref === upstream;\n}\nconst GIT_PULL_NO_REBASE = /* @__PURE__ */ new Set([\n \"--rebase=false\",\n \"--rebase=no\",\n \"--rebase=off\",\n \"--rebase=0\"\n]);\nfunction isPullFromUpstream(tokens, upstream) {\n const { flags, positionals } = subcommandArgs(tokens, \"pull\");\n if (flags.some((t) => t.startsWith(\"--rebase\") && !GIT_PULL_NO_REBASE.has(t) || t === \"--no-ff\" || !t.startsWith(\"--\") && t.slice(1).includes(\"r\"))) return false;\n if (positionals.length === 0) return true;\n if (positionals.length !== 2 || upstream === null) return false;\n const [remote, branch] = positionals;\n return `${remote}/${branch.replace(/^refs\\/heads\\//, \"\")}` === upstream;\n}\nfunction isRefMovingReset(tokens, cwd, head) {\n if (tokens.slice(tokens.indexOf(\"reset\") + 1).includes(\"--\")) return false;\n const { positionals } = subcommandArgs(tokens, \"reset\");\n if (positionals.length !== 1) return false;\n if (head !== \"HEAD\" && /^(HEAD|@)/.test(positionals[0])) return false;\n const target = resolveCommit(cwd, positionals[0]);\n if (target === null) return false;\n return target !== resolveCommit(cwd, head);\n}\nfunction pushDestinations(tokens, current) {\n const { positionals } = subcommandArgs(tokens, \"push\", GIT_PUSH_VALUE_FLAGS);\n const refspecs = positionals.slice(1);\n if (refspecs.length === 0) return null;\n const names = [];\n for (const spec of refspecs) {\n const dst = refspecDestination(spec);\n if (dst === \"HEAD\" || dst === \"@\") {\n if (current !== null) names.push(current);\n continue;\n }\n names.push(dst);\n }\n return names;\n}\nconst GIT_PUSH_EVERY_BRANCH = /* @__PURE__ */ new Set([\n \"--all\",\n \"--branches\",\n \"--mirror\"\n]);\nfunction leavesRefInPlace(tokens, sub) {\n const { flags } = subcommandArgs(tokens, sub);\n if (flags.includes(\"--abort\") || flags.includes(\"--quit\")) return true;\n return sub === \"commit\" && flags.includes(\"--dry-run\");\n}\nfunction movesProtectedRef(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (sub === void 0) return false;\n if (sub === \"switch\" || sub === \"checkout\" || sub === \"worktree\") return isGitProtectedBranchForceCreate(tokens, ctx);\n if (sub === \"branch\") return isGitProtectedBranchRefWrite(tokens, ctx);\n if (sub === \"fetch\") return isGitProtectedBranchFetchWrite(tokens, ctx);\n if (sub === \"update-ref\") return isGitProtectedUpdateRef(tokens, ctx);\n if (sub !== \"push\" && sub !== \"reset\" && !GIT_CHECKED_OUT_WRITES.has(sub)) return false;\n const cwd = gitEffectiveCwd(tokens, ctx.cwd);\n const pending = ctx.pendingBranches?.[cwd];\n const current = pending ? pending.branch : getCurrentBranch(cwd);\n const protectedSet = getProtectedBranches(cwd, ctx.repoProtectedBranches ?? []);\n const onProtected = current !== null && protectedSet.has(current);\n if (sub === \"push\") {\n const { flags } = subcommandArgs(tokens, \"push\", GIT_PUSH_VALUE_FLAGS);\n if (flags.some((f) => GIT_PUSH_EVERY_BRANCH.has(f))) return true;\n const destinations = pushDestinations(tokens, current);\n if (destinations === null) {\n if (flags.includes(\"--tags\")) return false;\n return onProtected;\n }\n return destinations.some((d) => protectedSet.has(d));\n }\n if (!onProtected) return false;\n if (leavesRefInPlace(tokens, sub)) return false;\n if (sub === \"reset\") return isRefMovingReset(tokens, cwd, pending ? current : \"HEAD\");\n if (sub === \"merge\" || sub === \"pull\") {\n const upstream = getUpstream(cwd, current);\n if (sub === \"merge\" && isMergeFastForwardFromUpstream(tokens, upstream)) return false;\n if (sub === \"pull\" && isPullFromUpstream(tokens, upstream)) return false;\n }\n return true;\n}\nconst PNPM_BUILTINS = /* @__PURE__ */ new Set([\n \"install\",\n \"i\",\n \"add\",\n \"remove\",\n \"rm\",\n \"uninstall\",\n \"un\",\n \"update\",\n \"up\",\n \"upgrade\",\n \"list\",\n \"ls\",\n \"ll\",\n \"la\",\n \"why\",\n \"view\",\n \"info\",\n \"show\",\n \"outdated\",\n \"audit\",\n \"licenses\",\n \"init\",\n \"create\",\n \"link\",\n \"unlink\",\n \"pack\",\n \"publish\",\n \"config\",\n \"set\",\n \"get\",\n \"store\",\n \"cache\",\n \"rebuild\",\n \"rb\",\n \"prune\",\n \"dedupe\",\n \"dlx\",\n \"exec\",\n \"run\",\n \"patch\",\n \"patch-commit\",\n \"recursive\",\n \"-r\"\n]);\nconst NPM_READ_OPS = /* @__PURE__ */ new Set([\n \"view\",\n \"info\",\n \"show\"\n]);\nfunction isNpmReadOp(tokens) {\n const sub = tokens[1];\n return sub !== void 0 && NPM_READ_OPS.has(sub);\n}\nfunction getNpmRedirectReason(tokens) {\n const sub = tokens[1];\n const BASE = \"This project uses pnpm.\";\n if (!sub) return `${BASE} Use \\`pnpm\\` instead of npm.`;\n if (sub === \"install\" || sub === \"i\" || sub === \"ci\") {\n if (tokens.slice(2).some((t) => !t.startsWith(\"-\"))) return `${BASE} Use \\`pnpm add <pkg>\\` instead of \\`npm install <pkg>\\`.`;\n return `${BASE} Use \\`pnpm install\\` instead of npm.`;\n }\n if (sub === \"add\") return `${BASE} Use \\`pnpm add <pkg>\\` instead of npm.`;\n if (sub === \"uninstall\" || sub === \"remove\" || sub === \"rm\" || sub === \"un\" || sub === \"r\") return `${BASE} Use \\`pnpm remove <pkg>\\` instead of npm.`;\n if (sub === \"run\" || sub === \"run-script\") return `${BASE} Use \\`pnpm run <script>\\` or \\`pnpm <script>\\` instead of npm.`;\n if (sub === \"test\" || sub === \"t\" || sub === \"tst\") return `${BASE} Use \\`pnpm test\\` instead of npm.`;\n if (sub === \"start\") return `${BASE} Use \\`pnpm start\\` instead of npm.`;\n if (sub === \"stop\") return `${BASE} Use \\`pnpm stop\\` instead of npm.`;\n if (sub === \"restart\") return `${BASE} Use \\`pnpm restart\\` instead of npm.`;\n if (sub === \"exec\") return `${BASE} Use \\`pnpm exec <cmd>\\` instead of npm.`;\n if (sub === \"list\" || sub === \"ls\" || sub === \"ll\" || sub === \"la\") return `${BASE} Use \\`pnpm list\\` instead of npm.`;\n if (sub === \"why\") return `${BASE} Use \\`pnpm why\\` instead of npm.`;\n if (sub === \"outdated\") return `${BASE} Use \\`pnpm outdated\\` instead of npm.`;\n if (sub === \"audit\") return `${BASE} Use \\`pnpm audit\\` instead of npm.`;\n if (sub === \"update\" || sub === \"up\" || sub === \"upgrade\") return `${BASE} Use \\`pnpm update\\` instead of npm.`;\n if (sub === \"init\") return `${BASE} Use \\`pnpm init\\` instead of npm.`;\n if (sub === \"publish\") return `${BASE} Use \\`pnpm publish\\` instead of npm.`;\n if (sub === \"link\") return `${BASE} Use \\`pnpm link\\` instead of npm.`;\n if (sub === \"unlink\") return `${BASE} Use \\`pnpm unlink\\` instead of npm.`;\n if (sub === \"pack\") return `${BASE} Use \\`pnpm pack\\` instead of npm.`;\n if (sub === \"cache\") return `${BASE} Use \\`pnpm store\\` instead of \\`npm cache\\`.`;\n if (sub === \"config\" || sub === \"set\" || sub === \"get\") return `${BASE} Use \\`pnpm config\\` instead of npm.`;\n return `${BASE} Use the equivalent pnpm command instead of npm.`;\n}\nconst LAST_POSITIONAL_DEST = /* @__PURE__ */ new Set([\n \"cp\",\n \"mv\",\n \"rsync\",\n \"install\",\n \"ln\"\n]);\nconst ALL_POSITIONAL_DEST = /* @__PURE__ */ new Set([\"tee\", \"truncate\"]);\nfunction explicitTargetDirectory(rest) {\n for (const [i, t] of rest.entries()) {\n if (t === \"-t\" || t === \"--target-directory\") return rest[i + 1] ?? null;\n if (t.startsWith(\"--target-directory=\")) return t.slice(19);\n }\n return null;\n}\nconst OUTPUT_REDIRECT = /^(?:\\d*|&)>{1,2}$/;\nconst OUTPUT_REDIRECT_ATTACHED = /^(?:\\d*|&)>{1,2}(.+)$/;\nconst INPUT_REDIRECT = /^\\d*<$/;\nconst INPUT_REDIRECT_ATTACHED = /^\\d*<([^<].*)$/;\nfunction spacedRedirectDirection(token) {\n if (OUTPUT_REDIRECT.test(token)) return \"out\";\n if (INPUT_REDIRECT.test(token)) return \"in\";\n return null;\n}\nfunction splitRedirects(tokens) {\n const redirects = [];\n const rest = [];\n for (let i = 0; i < tokens.length; i++) {\n const t = tokens[i];\n const spaced = spacedRedirectDirection(t);\n if (spaced !== null) {\n const next = tokens[i + 1];\n if (next !== void 0) redirects.push({\n direction: spaced,\n target: next\n });\n i++;\n continue;\n }\n const out = OUTPUT_REDIRECT_ATTACHED.exec(t);\n if (out) {\n redirects.push({\n direction: \"out\",\n target: out[1]\n });\n continue;\n }\n const input = INPUT_REDIRECT_ATTACHED.exec(t);\n if (input) {\n redirects.push({\n direction: \"in\",\n target: input[1]\n });\n continue;\n }\n rest.push(t);\n }\n return {\n redirects,\n rest\n };\n}\nfunction redirectTargets(tokens, direction) {\n return splitRedirects(tokens).redirects.filter((r) => r.direction === direction).map((r) => r.target);\n}\nfunction writeDestinations(tokens) {\n const cmd = path.basename(tokens[0] ?? \"\");\n const rest = tokens.slice(1);\n const dests = redirectTargets(tokens, \"out\");\n if (cmd === \"dd\") dests.push(...rest.filter((t) => t.startsWith(\"of=\")).map((t) => t.slice(3)));\n else if (cmd === \"sed\") {\n if (rest.some((t) => t === \"--in-place\" || /^-[a-zA-Z]*i/.test(t))) dests.push(...rest.filter((t) => !t.startsWith(\"-\")));\n } else if (ALL_POSITIONAL_DEST.has(cmd)) dests.push(...rest.filter((t) => !t.startsWith(\"-\")));\n else if (LAST_POSITIONAL_DEST.has(cmd)) {\n if (cmd === \"cp\" && rest.includes(\"--remove-destination\")) return dests;\n const targetDir = explicitTargetDirectory(rest);\n if (targetDir !== null) dests.push(targetDir);\n else {\n const last = rest.findLast((t) => !t.startsWith(\"-\"));\n if (last !== void 0) dests.push(last);\n }\n }\n return dests;\n}\nfunction writesIntoNodeModules(tokens, ctx) {\n return writeDestinations(tokens).some((d) => targetsNodeModules(d, ctx.cwd));\n}\nconst NODE_MODULES_WRITE_REASON = \"Don't write into node_modules/. Its files are hardlinks into pnpm's global store, so overwriting one in place (plain `cp`, `>` redirect, `sed -i`, ...) writes through the link and corrupts that package for every project on this machine -- silently, since the directory you wrote into ends up fine. To test a local build as an installed dependency, run it directly (`node packages/<pkg>/dist/bin.mjs <cmd>`), or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`). If you must replace a file in place, `cp --remove-destination` is safe -- it unlinks the target first. Run `pnpm store status` to check whether the store is already damaged.\";\nfunction isChmod777(tokens) {\n return tokens.some((t) => t === \"777\" || t === \"0777\");\n}\nfunction isGitForcePush(tokens) {\n const pushIdx = tokens.indexOf(\"push\");\n if (pushIdx === -1) return false;\n return tokens.slice(pushIdx + 1).some((t) => t === \"--force\" || t === \"-f\");\n}\nconst ENV_READ_FILE_COMMANDS = /* @__PURE__ */ new Set([\n \"cat\",\n \"head\",\n \"tail\",\n \"less\",\n \"more\",\n \"source\",\n \".\"\n]);\nconst GREP_FAMILY_COMMANDS = /* @__PURE__ */ new Set([\n \"grep\",\n \"rg\",\n \"ag\"\n]);\nfunction grepFileOperands(rest) {\n const files = [];\n let patternAccountedFor = false;\n let optionsEnded = false;\n for (let i = 0; i < rest.length; i++) {\n const t = rest[i];\n if (!optionsEnded && t === \"--\") {\n optionsEnded = true;\n continue;\n }\n if (!optionsEnded) {\n if (t === \"-f\" || t === \"--file\") {\n const value = rest[i + 1];\n if (value !== void 0) {\n files.push(value);\n i++;\n }\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"--file=\")) {\n files.push(t.slice(7));\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"-f\") && t !== \"-f\" && !t.startsWith(\"--\")) {\n files.push(t.slice(2));\n patternAccountedFor = true;\n continue;\n }\n if (t === \"-e\" || t === \"--regexp\") {\n i++;\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"--regexp=\") || t.startsWith(\"-e\") && t !== \"-e\" && !t.startsWith(\"--\")) {\n patternAccountedFor = true;\n continue;\n }\n if (t.startsWith(\"-\")) continue;\n }\n if (!patternAccountedFor) {\n patternAccountedFor = true;\n continue;\n }\n files.push(t);\n }\n return files;\n}\nfunction envFileOperands(tokens) {\n const cmd = path.basename(tokens[0] ?? \"\");\n const { redirects, rest } = splitRedirects(tokens.slice(1));\n const operands = redirects.filter((r) => r.direction === \"in\").map((r) => r.target);\n if (GREP_FAMILY_COMMANDS.has(cmd)) operands.push(...grepFileOperands(rest));\n else if (ENV_READ_FILE_COMMANDS.has(cmd)) operands.push(...rest.filter((t) => !t.startsWith(\"-\")));\n return operands;\n}\nconst HEALTH_CEILING_FILES = [\"health-baseline.json\", \".tooling.json\"];\nconst HEALTH_BASELINE_FILE = \"health-baseline.json\";\nconst HEALTH_RAISE_REASON = \"A health ceiling raise travels alone: open a ceiling-only pull request -- only health-baseline.json and .tooling.json changed -- for a human to merge, or split the module. `health:check` refuses the raise beside code either way. To restore the default branch's copy run `git show origin/<default>:health-baseline.json > health-baseline.json`, and to adopt lower counts run `pnpm exec bst health:check --update-baseline`.\";\nfunction writesHealthBaseline(tokens, cwd) {\n const destinations = writeDestinations(tokens);\n if (!destinations.some((dest) => baseName(dest) === \"health-baseline.json\")) return false;\n const root = getRepoTopLevel(cwd) ?? cwd;\n return destinations.some((dest) => samePath(resolveIn(cwd, dest), path.join(root, HEALTH_BASELINE_FILE)));\n}\nfunction baseName(target) {\n const segments = target.split(/[/\\\\]/);\n return segments[segments.length - 1] ?? \"\";\n}\nfunction isBaselineRestore(tokens) {\n if (tokens[0] !== \"git\") return false;\n let i = 1;\n while (tokens[i]?.startsWith(\"-\") === true) i += GIT_FLAGS_WITH_ARG.has(tokens[i] ?? \"\") ? 2 : 1;\n if (tokens[i] !== \"show\") return false;\n return tokens.slice(i + 1).some((token) => token.includes(\":\") && baseName(token.slice(token.lastIndexOf(\":\") + 1)) === \"health-baseline.json\");\n}\nfunction isHealthBaselineReset(tokens) {\n return tokens.includes(\"health:check\") && tokens.includes(\"--update-baseline\") && tokens.includes(\"--reset\");\n}\nfunction raiseTravelsWithCode(cwd) {\n return branchCarriesOtherFiles(cwd, HEALTH_CEILING_FILES) === true;\n}\nconst denyRules = [\n {\n name: \"sudo\",\n match: /^sudo$/,\n action: \"deny\",\n reason: \"Don't use sudo. If you need elevated permissions, ask me to do it manually.\"\n },\n {\n name: \"git-force-push\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens) => isGitForcePush(tokens),\n reason: \"Use `git push --force-with-lease` instead of `--force`.\"\n },\n {\n name: \"git-protected-branch-write\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens, ctx) => movesProtectedRef(tokens, ctx),\n reason: \"This would move a protected branch (main, master, the remote's default branch, or one listed in .claude/hooks/bst-hook.settings.json#protectedBranches). Work on a feature branch in a worktree: `git worktree add ../<repo>-<topic> -b feat/<topic>`, then commit and push there. To bring `main` up to date, use `git pull --ff-only`.\"\n },\n {\n name: \"chmod-777\",\n match: /^chmod$/,\n action: \"deny\",\n when: (tokens) => isChmod777(tokens),\n reason: \"Don't use 777 permissions. Use more restrictive permissions.\"\n },\n {\n name: \"node-modules-write\",\n match: /./,\n action: \"deny\",\n when: (tokens, ctx) => writesIntoNodeModules(tokens, ctx),\n reason: NODE_MODULES_WRITE_REASON\n },\n {\n name: \"env-file-read\",\n match: /./,\n action: \"deny\",\n when: (tokens) => envFileOperands(tokens).some(isEnvFile),\n reason: \"Don't read .env files. These contain secrets that should not appear in the conversation.\"\n },\n {\n name: \"env-file-write\",\n match: /./,\n action: \"deny\",\n when: (tokens) => redirectTargets(tokens, \"out\").some(isEnvFile),\n reason: \"Don't write to .env files. These contain secrets.\"\n },\n {\n name: \"health-baseline-write\",\n match: /./,\n action: \"deny\",\n when: (tokens, ctx) => writesHealthBaseline(tokens, ctx.cwd) && !isBaselineRestore(tokens) && raiseTravelsWithCode(ctx.cwd),\n reason: HEALTH_RAISE_REASON\n },\n {\n name: \"health-baseline-reset\",\n match: /^bst$/,\n action: \"deny\",\n when: (tokens, ctx) => isHealthBaselineReset(tokens) && raiseTravelsWithCode(ctx.cwd),\n reason: HEALTH_RAISE_REASON\n },\n {\n name: \"npx-redirect\",\n match: /^npx$/,\n action: \"deny\",\n reason: \"This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\"\n },\n {\n name: \"mise-self-update\",\n match: /^mise$/,\n action: \"deny\",\n when: (tokens) => tokens[1] === \"self-update\",\n reason: \"Don't self-update mise. The version is Renovate-managed; ask a human to bump the pin.\"\n }\n];\n\n//#endregion\n//#region src/lib/script-resolver.ts\nconst PackageJsonSchema = object({ scripts: record(string(), unknown()) });\nfunction resolveScript(scriptName, cwd) {\n const scripts = readScripts(cwd);\n if (!scripts) return null;\n const main = scripts[scriptName];\n if (typeof main !== \"string\") return null;\n const pre = scripts[`pre${scriptName}`];\n const post = scripts[`post${scriptName}`];\n return {\n pre: typeof pre === \"string\" ? pre : null,\n main,\n post: typeof post === \"string\" ? post : null\n };\n}\nfunction resolveFilteredScript(scriptName, filterPkg, cwd) {\n const pkgDir = findWorkspacePackageDir(filterPkg, cwd);\n if (!pkgDir) return null;\n const script = resolveScript(scriptName, pkgDir);\n if (!script) return null;\n return {\n ...script,\n pkgDir\n };\n}\nfunction resolveRecursiveScripts(scriptName, rootDir) {\n const pkgDirs = listWorkspacePackageDirs(rootDir);\n if (!pkgDirs) return null;\n const results = [];\n for (const pkgDir of pkgDirs) {\n const resolved = resolveScript(scriptName, pkgDir);\n if (resolved) results.push({\n ...resolved,\n pkgDir\n });\n }\n return results;\n}\nfunction listWorkspacePackageDirs(rootDir) {\n const patterns = readWorkspacePatterns(rootDir);\n if (!patterns) return null;\n const dirs = [];\n for (const pattern of patterns) {\n if (!pattern.endsWith(\"/*\")) continue;\n const baseDir = pattern.slice(0, -2);\n const parentDir = resolve(rootDir, baseDir);\n let entries;\n try {\n entries = readdirSync(parentDir);\n } catch {\n continue;\n }\n for (const entry of entries) {\n const entryDir = join(parentDir, entry);\n try {\n if (!statSync(entryDir).isDirectory()) continue;\n } catch {\n continue;\n }\n dirs.push(entryDir);\n }\n }\n return dirs.length > 0 ? dirs : null;\n}\nfunction findWorkspacePackageDir(pkgName, rootDir) {\n const pkgDirs = listWorkspacePackageDirs(rootDir);\n if (!pkgDirs) return null;\n for (const entryDir of pkgDirs) {\n const name = readPackageName(entryDir);\n if (name === pkgName || name === `@${pkgName}`) return entryDir;\n if (entryDir.split(\"/\").pop() === pkgName) return entryDir;\n if (name) {\n if ((name.startsWith(\"@\") ? name.split(\"/\")[1] : name) === pkgName) return entryDir;\n }\n }\n return null;\n}\nfunction readWorkspacePatterns(rootDir) {\n try {\n const lines = readFileSync(join(rootDir, \"pnpm-workspace.yaml\"), \"utf-8\").split(\"\\n\");\n const packagesIdx = lines.findIndex((l) => /^packages\\s*:/.test(l));\n if (packagesIdx === -1) return null;\n const patterns = [];\n for (let i = packagesIdx + 1; i < lines.length; i++) {\n const line = lines[i];\n const match = /^\\s+-\\s+(.+)$/.exec(line);\n if (!match) break;\n patterns.push(match[1].trim().replace(/['\"`]/g, \"\"));\n }\n return patterns.length > 0 ? patterns : null;\n } catch {\n return null;\n }\n}\nconst PackageNameSchema = object({ name: string() });\nfunction readPackageName(dir) {\n try {\n const raw = readFileSync(join(dir, \"package.json\"), \"utf-8\");\n const result = PackageNameSchema.safeParse(JSON.parse(raw));\n if (!result.success) return null;\n return result.data.name;\n } catch {\n return null;\n }\n}\nfunction readScripts(cwd) {\n try {\n const raw = readFileSync(join(cwd, \"package.json\"), \"utf-8\");\n const result = PackageJsonSchema.safeParse(JSON.parse(raw));\n if (!result.success) return null;\n return result.data.scripts;\n } catch {\n return null;\n }\n}\n\n//#endregion\n//#region src/lib/workspace-root.ts\nfunction findWorkspaceRoot(cwd) {\n let dir = resolve(cwd);\n while (true) {\n if (existsSync(join(dir, \"pnpm-workspace.yaml\"))) return dir;\n const parent = dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\n//#endregion\n//#region src/evaluate.ts\nconst MAX_EVAL_DEPTH = 5;\nfunction subshellBody(command) {\n if (!command.startsWith(\"(\") || !command.endsWith(\")\")) return null;\n let depth = 0;\n let quote = null;\n for (let i = 0; i < command.length; i++) {\n const ch = command[i];\n if (quote !== null) {\n if (ch === quote) quote = null;\n else if (ch === \"\\\\\" && quote === \"\\\"\") i++;\n continue;\n }\n if (ch === \"\\\\\") i++;\n else if (ch === \"'\" || ch === \"\\\"\") quote = ch;\n else if (ch === \"(\") depth++;\n else if (ch === \")\") {\n depth--;\n if (depth === 0) return i === command.length - 1 ? command.slice(1, -1) : null;\n }\n }\n return null;\n}\nfunction extractCdDir(segment, ctx) {\n const tokenResult = tokenize(segment);\n if (!tokenResult.ok || tokenResult.tokens.length !== 2) return null;\n const tokens = tokenResult.tokens;\n if (tokens[0] !== \"cd\") return null;\n const dir = tokens[1];\n if (path.isAbsolute(dir)) return dir;\n return path.resolve(ctx.cwd, dir);\n}\nfunction extractBranchSwitch(segment, ctx) {\n const tokenResult = tokenize(segment);\n if (!tokenResult.ok) return void 0;\n let tokens = tokenResult.tokens;\n let envSkipped = 0;\n while (envSkipped < tokens.length && /^\\w+=/.test(tokens[envSkipped])) envSkipped++;\n tokens = tokens.slice(envSkipped);\n if (tokens.length === 0 || path.basename(tokens[0]) !== \"git\") return void 0;\n return branchSwitch(tokens, ctx, checkInjection(tokens, segment).safe);\n}\nfunction evaluateCommand(command, ctx) {\n if (ctx.depth >= MAX_EVAL_DEPTH) return {\n action: \"none\",\n rule: null,\n reason: \"max evaluation depth exceeded\"\n };\n const trimmed = command.trim();\n if (!trimmed) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n const body = subshellBody(trimmed);\n if (body !== null) return evaluateCommand(body, {\n ...ctx,\n depth: ctx.depth + 1\n });\n const segments = splitCommand(trimmed);\n if (segments.length === 0) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n let firstNone = null;\n const currentCtx = { ...ctx };\n let unknownAfterNext;\n for (const segment of segments) {\n if (segment.operator === \"&&\") {\n const cdDir = extractCdDir(segment.command, currentCtx);\n if (cdDir) currentCtx.cwd = cdDir;\n }\n const result = evaluateSegment(segment.command, currentCtx);\n if (result.action === \"deny\") return result;\n if (!firstNone) firstNone = result;\n if (unknownAfterNext) {\n currentCtx.pendingBranches = {\n ...currentCtx.pendingBranches,\n [unknownAfterNext.dir]: unknownAfterNext\n };\n unknownAfterNext = void 0;\n }\n const switched = extractBranchSwitch(segment.command, currentCtx);\n if (switched) {\n const unknown = {\n dir: switched.dir,\n branch: null\n };\n if (segment.operator === \"&&\") currentCtx.pendingBranches = {\n ...currentCtx.pendingBranches,\n [switched.dir]: switched\n };\n else if (segment.operator === \"||\") unknownAfterNext = unknown;\n else currentCtx.pendingBranches = {\n ...currentCtx.pendingBranches,\n [switched.dir]: unknown\n };\n }\n }\n return firstNone ?? {\n action: \"none\",\n rule: null,\n reason: null\n };\n}\nfunction evaluateResolvedScript(resolved, ctx) {\n if (resolved.pre) {\n const preResult = evaluateCommand(resolved.pre, ctx);\n if (preResult.action === \"deny\") return preResult;\n }\n const mainResult = evaluateCommand(resolved.main, ctx);\n if (mainResult.action === \"deny\") return mainResult;\n if (resolved.post) {\n const postResult = evaluateCommand(resolved.post, ctx);\n if (postResult.action === \"deny\") return postResult;\n }\n return {\n action: \"none\",\n rule: null,\n reason: null\n };\n}\nfunction evaluateSegment(rawSegment, ctx) {\n const trimmed = rawSegment.trim();\n const body = subshellBody(trimmed);\n if (body !== null) return evaluateCommand(body, {\n ...ctx,\n depth: ctx.depth + 1\n });\n if (trimmed.startsWith(\"(\") && trimmed.endsWith(\")\")) return {\n action: \"none\",\n rule: null,\n reason: \"subshell\"\n };\n const tokenResult = tokenize(rawSegment);\n if (!tokenResult.ok) return {\n action: \"none\",\n rule: null,\n reason: tokenResult.error\n };\n let tokens = tokenResult.tokens;\n if (tokens.length === 0) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n let envSkipped = 0;\n while (envSkipped < tokens.length && /^\\w+=/.test(tokens[envSkipped])) envSkipped++;\n if (envSkipped >= tokens.length) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n tokens = tokens.slice(envSkipped);\n if (writesIntoNodeModules(tokens, ctx)) return {\n action: \"deny\",\n rule: \"node-modules-write\",\n reason: NODE_MODULES_WRITE_REASON\n };\n const injection = checkInjection(tokens, rawSegment);\n if (!injection.safe) return {\n action: \"none\",\n rule: null,\n reason: injection.reason\n };\n const SAFE_REDIRECT_TOKENS = /* @__PURE__ */ new Set([\n \"2>&1\",\n \"2>/dev/null\",\n \">/dev/null\",\n \"1>/dev/null\"\n ]);\n tokens = tokens.filter((t, i) => {\n if (SAFE_REDIRECT_TOKENS.has(t)) return false;\n if ((t === \">\" || t === \">>\") && tokens[i + 1] === \"/dev/null\") return false;\n if (t === \"/dev/null\" && i > 0 && (tokens[i - 1] === \">\" || tokens[i - 1] === \">>\")) return false;\n return true;\n });\n const cmdName = path.basename(tokens[0]);\n if (cmdName === \"command\" && tokens.length > 1) return evaluateCommand(joinTokens(tokens.slice(1)), {\n ...ctx,\n depth: ctx.depth + 1\n });\n if (cmdName === \"env\") {\n let envIdx = 1;\n while (envIdx < tokens.length && /^\\w+=/.test(tokens[envIdx])) envIdx++;\n if (envIdx >= tokens.length) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n return evaluateCommand(joinTokens(tokens.slice(envIdx)), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n if (cmdName === \"pnpm\") return evaluatePnpm(tokens, ctx);\n if (cmdName === \"npm\") return evaluateNpm(tokens);\n if (cmdName === \"mise\" && tokens[1] === \"exec\") {\n const inner = miseExecInner(tokens);\n if (!inner) return {\n action: \"none\",\n rule: null,\n reason: \"bare mise exec\"\n };\n return evaluateCommand(joinTokens(inner), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n if (cmdName === \"bash\" || cmdName === \"sh\" || cmdName === \"zsh\") return evaluateShellC(tokens, ctx);\n if (cmdName === \"xargs\") return evaluateXargs(tokens, ctx);\n if (cmdName === \"node\" && (tokens.includes(\"-e\") || tokens.includes(\"-p\") || tokens.includes(\"--eval\") || tokens.includes(\"--print\"))) return {\n action: \"none\",\n rule: null,\n reason: \"node eval\"\n };\n for (const rule of denyRules) if (rule.match.test(cmdName)) {\n if (!rule.when || rule.when(tokens, ctx)) return {\n action: \"deny\",\n rule: rule.name,\n reason: rule.reason ?? null\n };\n }\n const advice = cmdName === \"git\" ? worktreeAdvice(tokens, ctx) : void 0;\n return {\n action: \"none\",\n rule: null,\n reason: `no rule matched: ${cmdName}`,\n ...advice ? { advice } : {}\n };\n}\nfunction miseExecInner(tokens) {\n const dashIdx = tokens.indexOf(\"--\");\n if (dashIdx === -1) return void 0;\n const inner = tokens.slice(dashIdx + 1);\n return inner.length > 0 ? inner : void 0;\n}\nfunction scanPnpmFlags(subTokens, from, state) {\n for (let i = from; i < subTokens.length; i++) {\n const t = subTokens[i];\n if (t === \"--filter\" || t.startsWith(\"--filter=\") || t === \"-F\" || t.startsWith(\"-F=\")) return {\n name: void 0,\n filterIdx: i\n };\n if (t === \"-r\" || t === \"--recursive\") {\n state.isRecursive = true;\n continue;\n }\n if (t === \"-w\" || t === \"--workspace-root\") {\n state.isWorkspaceRoot = true;\n continue;\n }\n if (t === \"-C\" || t === \"--dir\") {\n state.dirOverride = subTokens[++i];\n continue;\n }\n if (t.startsWith(\"-C=\") || t.startsWith(\"--dir=\")) {\n state.dirOverride = t.split(\"=\", 2)[1];\n continue;\n }\n if (t.startsWith(\"-\") && !PNPM_BUILTINS.has(t)) continue;\n return { name: t };\n }\n return { name: void 0 };\n}\nfunction evaluatePnpm(tokens, ctx) {\n const subTokens = tokens.slice(1);\n const state = {\n isRecursive: false,\n isWorkspaceRoot: false,\n dirOverride: void 0\n };\n const scan = scanPnpmFlags(subTokens, 0, state);\n if (scan.filterIdx !== void 0) return evaluatePnpmFilter(tokens, subTokens, scan.filterIdx, ctx);\n const subcommand = scan.name;\n const subcommandIdx = subcommand ? subTokens.indexOf(subcommand) : -1;\n if (!subcommand) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n if (subcommand === \"dlx\") return {\n action: \"none\",\n rule: null,\n reason: \"pnpm dlx\"\n };\n if (subcommand === \"add\") return {\n action: \"none\",\n rule: null,\n reason: \"pnpm add\"\n };\n if (subcommand === \"exec\") {\n const execArgs = subTokens.slice(subcommandIdx + 1);\n if (execArgs.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"bare pnpm exec\"\n };\n return evaluateCommand(joinTokens(execArgs), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n let scriptName;\n let { isRecursive, isWorkspaceRoot, dirOverride } = state;\n if (subcommand === \"run\") {\n const runScan = scanPnpmFlags(subTokens, subcommandIdx + 1, state);\n ({isRecursive, isWorkspaceRoot, dirOverride} = state);\n if (runScan.filterIdx !== void 0) return evaluatePnpmFilter(tokens, subTokens, runScan.filterIdx, ctx);\n if (!runScan.name) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm run without script name\"\n };\n scriptName = runScan.name;\n } else if (!PNPM_BUILTINS.has(subcommand)) scriptName = subcommand;\n else return {\n action: \"none\",\n rule: null,\n reason: null\n };\n if (isRecursive) return evaluatePnpmRecursive(scriptName, ctx);\n if (isWorkspaceRoot) {\n const root = findWorkspaceRoot(ctx.cwd);\n if (!root) return {\n action: \"none\",\n rule: null,\n reason: \"workspace root not found\"\n };\n return evaluatePnpmScript(scriptName, {\n ...ctx,\n cwd: root\n });\n }\n if (dirOverride) {\n const resolved = path.resolve(ctx.cwd, dirOverride);\n return evaluatePnpmScript(scriptName, {\n ...ctx,\n cwd: resolved\n });\n }\n return evaluatePnpmScript(scriptName, ctx);\n}\nfunction evaluatePnpmScript(scriptName, ctx) {\n const resolved = resolveScript(scriptName, ctx.cwd);\n if (!resolved) return {\n action: \"none\",\n rule: null,\n reason: \"script resolution failed\"\n };\n const res = evaluateResolvedScript(resolved, {\n ...ctx,\n depth: ctx.depth + 1\n });\n if (res.action === \"deny\") return res;\n return {\n action: \"none\",\n rule: `pnpm-script:${scriptName}`,\n reason: null\n };\n}\nfunction evaluatePnpmRecursive(scriptName, ctx) {\n const resolved = resolveRecursiveScripts(scriptName, ctx.cwd);\n if (!resolved || resolved.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm recursive\"\n };\n for (const pkg of resolved) {\n const res = evaluateResolvedScript(pkg, {\n ...ctx,\n depth: ctx.depth + 1,\n cwd: pkg.pkgDir ?? ctx.cwd\n });\n if (res.action === \"deny\") return res;\n }\n return {\n action: \"none\",\n rule: `pnpm-recursive:${scriptName}`,\n reason: null\n };\n}\nfunction evaluateNpm(tokens) {\n if (isNpmReadOp(tokens)) return {\n action: \"none\",\n rule: null,\n reason: null\n };\n return {\n action: \"deny\",\n rule: \"npm-redirect\",\n reason: getNpmRedirectReason(tokens)\n };\n}\nfunction evaluatePnpmFilter(_tokens, subTokens, filterIdx, ctx) {\n const filterPkgs = [];\n let cursor = filterIdx;\n while (cursor < subTokens.length) {\n const t = subTokens[cursor];\n if (t === \"--filter\" || t === \"-F\") {\n const pkg = subTokens[cursor + 1];\n if (!pkg || pkg.startsWith(\"-\")) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without package\"\n };\n filterPkgs.push(pkg);\n cursor += 2;\n } else if (t.startsWith(\"--filter=\") || t.startsWith(\"-F=\")) {\n const pkg = t.startsWith(\"-F=\") ? t.slice(3) : t.slice(9);\n if (!pkg) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without package\"\n };\n filterPkgs.push(pkg);\n cursor += 1;\n } else break;\n }\n if (filterPkgs.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without package\"\n };\n let filterSubcommand;\n let subcmdIdx = -1;\n for (let i = cursor; i < subTokens.length; i++) {\n const t = subTokens[i];\n if (t === \"--filter\" || t === \"-F\") {\n i += 1;\n continue;\n }\n if (t.startsWith(\"--filter=\") || t.startsWith(\"-F=\")) continue;\n if (t.startsWith(\"-\") && !PNPM_BUILTINS.has(t)) continue;\n filterSubcommand = t;\n subcmdIdx = i;\n break;\n }\n if (!filterSubcommand) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter without subcommand\"\n };\n if (PNPM_BUILTINS.has(filterSubcommand) && filterSubcommand !== \"run\" && filterSubcommand !== \"exec\") return {\n action: \"none\",\n rule: null,\n reason: null\n };\n if (filterSubcommand === \"exec\") {\n const execArgs = subTokens.slice(subcmdIdx + 1);\n if (execArgs.length === 0) return {\n action: \"none\",\n rule: null,\n reason: \"bare pnpm exec\"\n };\n return evaluateCommand(joinTokens(execArgs), {\n ...ctx,\n depth: ctx.depth + 1\n });\n }\n const scriptName = filterSubcommand === \"run\" ? subTokens[subcmdIdx + 1] : filterSubcommand;\n if (!scriptName || scriptName.startsWith(\"-\")) return {\n action: \"none\",\n rule: null,\n reason: \"pnpm --filter run without script name\"\n };\n if (PNPM_BUILTINS.has(scriptName) && scriptName !== \"run\") return {\n action: \"none\",\n rule: null,\n reason: null\n };\n for (const pkg of filterPkgs) {\n const resolved = resolveFilteredScript(scriptName, pkg, ctx.cwd);\n if (!resolved) return {\n action: \"none\",\n rule: null,\n reason: \"filtered script resolution failed\"\n };\n const res = evaluateResolvedScript(resolved, {\n ...ctx,\n depth: ctx.depth + 1,\n cwd: resolved.pkgDir ?? ctx.cwd\n });\n if (res.action === \"deny\") return res;\n }\n return {\n action: \"none\",\n rule: `pnpm-filter:${filterPkgs.join(\",\")}:${scriptName}`,\n reason: null\n };\n}\nconst SHELL_LOGIN_FLAGS = /* @__PURE__ */ new Set([\"-l\", \"--login\"]);\nfunction evaluateShellC(tokens, ctx) {\n let i = 1;\n while (i < tokens.length && SHELL_LOGIN_FLAGS.has(tokens[i])) i++;\n const flag = tokens[i];\n if (flag === void 0 || !/^-[lc]+$/.test(flag) || !flag.includes(\"c\")) return {\n action: \"none\",\n rule: null,\n reason: \"shell without a plain -c\"\n };\n const cmdArg = tokens[i + 1];\n if (!cmdArg) return {\n action: \"none\",\n rule: null,\n reason: \"shell -c without command\"\n };\n if (tokens.length > i + 2) return {\n action: \"none\",\n rule: null,\n reason: \"shell -c with positional args\"\n };\n return evaluateCommand(cmdArg, {\n ...ctx,\n depth: ctx.depth + 1\n });\n}\nconst XARGS_ARG_FLAGS = /* @__PURE__ */ new Set([\n \"-I\",\n \"-d\",\n \"-n\",\n \"-P\",\n \"-s\",\n \"-L\",\n \"-E\",\n \"-a\",\n \"--max-args\",\n \"--max-procs\",\n \"--delimiter\",\n \"--replace\",\n \"--max-lines\",\n \"--max-chars\",\n \"--arg-file\",\n \"--process-slot-var\"\n]);\nconst XARGS_BOOL_FLAGS = /* @__PURE__ */ new Set([\n \"-0\",\n \"--null\",\n \"-t\",\n \"--verbose\",\n \"-r\",\n \"--no-run-if-empty\",\n \"-p\",\n \"--interactive\",\n \"-x\",\n \"--exit\",\n \"--show-limits\"\n]);\nfunction evaluateXargs(tokens, ctx) {\n let i = 1;\n while (i < tokens.length) {\n const t = tokens[i];\n if (!t.startsWith(\"-\")) break;\n if (XARGS_ARG_FLAGS.has(t)) {\n i += 2;\n continue;\n }\n const eqIdx = t.indexOf(\"=\");\n if (eqIdx !== -1) {\n const flagName = t.slice(0, eqIdx);\n if (XARGS_ARG_FLAGS.has(flagName)) {\n i++;\n continue;\n }\n }\n if (t.length > 2 && XARGS_ARG_FLAGS.has(t.slice(0, 2))) {\n i++;\n continue;\n }\n if (XARGS_BOOL_FLAGS.has(t)) {\n i++;\n continue;\n }\n return {\n action: \"none\",\n rule: null,\n reason: \"xargs with unrecognised flags\"\n };\n }\n if (i >= tokens.length) return {\n action: \"none\",\n rule: null,\n reason: \"xargs without command\"\n };\n return evaluateCommand(joinTokens(tokens.slice(i)), {\n ...ctx,\n depth: ctx.depth + 1\n });\n}\n\n//#endregion\n//#region src/file-rules.ts\nconst READ_TOOLS = /* @__PURE__ */ new Set([\n \"Read\",\n \"Glob\",\n \"Grep\"\n]);\nconst WRITE_TOOLS = /* @__PURE__ */ new Set([\"Write\", \"Edit\"]);\nconst fileDenyRules = [\n {\n name: \"env-file-read\",\n tools: READ_TOOLS,\n action: \"deny\",\n when: (filePath) => isEnvFile(filePath),\n reason: \"Don't read .env files. These contain secrets that should not appear in the conversation.\"\n },\n {\n name: \"env-file-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath) => isEnvFile(filePath),\n reason: \"Don't write to .env files. These contain secrets.\"\n },\n {\n name: \"hooks-folder-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n const hooksDir = path.join(ctx.cwd, \".claude\", \"hooks\");\n return isWithin(hooksDir, resolved);\n },\n reason: \"Don't write to .claude/hooks/. These files are managed by the project and should not be modified directly.\"\n },\n {\n name: \"git-dir-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => targetsGitDir(filePath, ctx.cwd),\n reason: \"Don't write into .git/ directly. Its internals (config, hooks, refs) are not plain project files -- a config edit can redirect remotes or push destinations that other rules trust, and a hooks/ script runs automatically on the next commit/push. Use git subcommands instead, or ask the user.\"\n },\n {\n name: \"node-modules-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => targetsNodeModules(filePath, ctx.cwd),\n reason: \"Don't write into node_modules/. Its files are hardlinks into pnpm's global store -- editing one in place corrupts that package for every project on this machine. Edit the source package and rebuild, or install into a scratch tree with an isolated store (`pnpm add --store-dir <scratch> <pkg>`).\"\n },\n {\n name: \"claude-settings-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n return samePath(resolved, path.join(ctx.cwd, \".claude\", \"settings.json\"));\n },\n reason: \"Don't write to .claude/settings.json. This file is managed by the project and should not be modified directly.\"\n },\n {\n name: \"health-baseline-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx, toolInput) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n if (path.basename(resolved) !== \"health-baseline.json\") return false;\n const root = getRepoTopLevel(ctx.cwd) ?? ctx.cwd;\n if (!samePath(resolved, path.join(root, \"health-baseline.json\"))) return false;\n if (!writeRaisesBaseline(resolved, toolInput)) return false;\n return branchCarriesOtherFiles(ctx.cwd, HEALTH_CEILING_FILES) === true;\n },\n reason: HEALTH_RAISE_REASON\n }\n];\nfunction writeRaisesBaseline(file, toolInput) {\n let before;\n try {\n before = readFileSync(file, \"utf8\");\n } catch (_error) {\n return false;\n }\n const after = textAfterWrite(before, toolInput);\n if (after === void 0) return false;\n const beforeJson = parseJson(before);\n const afterJson = parseJson(after);\n if (beforeJson === void 0 || afterJson === void 0) return false;\n return numberRises(beforeJson, afterJson);\n}\nfunction textAfterWrite(before, toolInput) {\n const { content, old_string: oldText, new_string: newText, replace_all: all } = toolInput;\n if (typeof content === \"string\") return content;\n if (typeof oldText !== \"string\" || typeof newText !== \"string\" || oldText === \"\") return void 0;\n const at = before.indexOf(oldText);\n if (at === -1) return void 0;\n if (all === true) return before.split(oldText).join(newText);\n return before.slice(0, at) + newText + before.slice(at + oldText.length);\n}\nfunction parseJson(text) {\n try {\n const result = record(string(), unknown()).safeParse(JSON.parse(text));\n return result.success ? result.data : void 0;\n } catch (_error) {\n return;\n }\n}\nfunction numberRises(before, after) {\n if (typeof after === \"number\") return typeof before !== \"number\" || after > before;\n if (typeof after !== \"object\" || after === null) return false;\n const beforeObject = typeof before === \"object\" && before !== null ? before : {};\n return Object.entries(after).some(([key, value]) => numberRises(Object.entries(beforeObject).find(([k]) => k === key)?.[1], value));\n}\n\n//#endregion\n//#region src/evaluate-tool.ts\nconst FILE_PATH_TOOLS = /* @__PURE__ */ new Set([\n \"Read\",\n \"Write\",\n \"Edit\"\n]);\nconst SEARCH_TOOLS = /* @__PURE__ */ new Set([\"Glob\", \"Grep\"]);\nfunction extractPath(toolName, toolInput, cwd) {\n if (FILE_PATH_TOOLS.has(toolName)) return parseFilePathInput(toolInput)?.file_path ?? null;\n if (SEARCH_TOOLS.has(toolName)) return parseSearchInput(toolInput)?.path ?? cwd;\n return null;\n}\nfunction evaluateToolUse(toolName, toolInput, ctx) {\n const filePath = extractPath(toolName, toolInput, ctx.cwd);\n if (filePath === null) return {\n action: \"none\",\n rule: null,\n reason: \"unknown tool input shape\"\n };\n for (const rule of fileDenyRules) if (rule.tools.has(toolName) && rule.when(filePath, ctx, toolInput)) return {\n action: \"deny\",\n rule: rule.name,\n reason: rule.reason ?? null\n };\n return {\n action: \"none\",\n rule: null,\n reason: \"no file rule matched\"\n };\n}\n\n//#endregion\n//#region src/lib/settings.ts\nconst SETTINGS_PATH = \".claude/hooks/bst-hook.settings.json\";\nconst BstHookSettingsSchema = object({ protectedBranches: _default(array(string().check(_minLength(1))), []) });\nfunction isFileClean(cwd, relativePath) {\n try {\n execFileSync(\"git\", [\n \"ls-files\",\n \"--error-unmatch\",\n relativePath\n ], {\n cwd,\n stdio: \"ignore\"\n });\n execFileSync(\"git\", [\n \"diff\",\n \"--quiet\",\n \"HEAD\",\n \"--\",\n relativePath\n ], {\n cwd,\n stdio: \"ignore\"\n });\n return true;\n } catch {\n return false;\n }\n}\nfunction loadRepoSettings(cwd) {\n const root = findWorkspaceRoot(cwd) ?? cwd;\n const settingsFile = join(root, SETTINGS_PATH);\n const empty = { protectedBranches: [] };\n if (!existsSync(settingsFile)) return empty;\n if (!isFileClean(root, SETTINGS_PATH)) return empty;\n try {\n const raw = readFileSync(settingsFile, \"utf-8\");\n const parsed = JSON.parse(raw);\n const result = BstHookSettingsSchema.safeParse(parsed);\n if (!result.success) return empty;\n return { protectedBranches: result.data.protectedBranches };\n } catch {\n return empty;\n }\n}\n\n//#endregion\n//#region src/index.ts\nconst inputResult = await readHookInput();\nif (!inputResult.ok) {\n process.stderr.write(`[bst-hook] invalid hook input: ${inputResult.error}\\n`);\n audit({\n sessionId: \"unknown\",\n cwd: \"unknown\",\n command: `<invalid input: ${inputResult.error}>`,\n decision: \"none\",\n rule: null,\n reason: `validation failed: ${inputResult.error}`,\n permissionMode: void 0\n });\n process.exit(0);\n}\nconst input = inputResult.data;\nif (input.hook_event_name !== \"PreToolUse\") process.exit(0);\nif (!(/* @__PURE__ */ new Set([\n \"Bash\",\n \"Read\",\n \"Write\",\n \"Edit\",\n \"Glob\",\n \"Grep\"\n])).has(input.tool_name)) process.exit(0);\nconst repoSettings = loadRepoSettings(input.cwd);\nlet decision;\nlet auditCommand;\nif (input.tool_name === \"Bash\") {\n const bashInput = parseBashInput(input.tool_input);\n if (!bashInput) {\n audit({\n sessionId: input.session_id,\n cwd: input.cwd,\n toolName: input.tool_name,\n command: `<invalid Bash input: ${JSON.stringify(input.tool_input).slice(0, 200)}>`,\n decision: \"none\",\n rule: null,\n reason: \"Bash tool_input missing or invalid 'command' field\",\n permissionMode: input.permission_mode\n });\n process.exit(0);\n }\n decision = evaluateCommand(bashInput.command, {\n cwd: input.cwd,\n depth: 0,\n repoProtectedBranches: repoSettings.protectedBranches\n });\n auditCommand = bashInput.command;\n} else {\n const projectDir = findWorkspaceRoot(input.cwd) ?? process.env[\"CLAUDE_PROJECT_DIR\"] ?? input.cwd;\n decision = evaluateToolUse(input.tool_name, input.tool_input, {\n cwd: projectDir,\n sessionId: input.session_id\n });\n const filePath = String(input.tool_input[\"file_path\"] ?? input.tool_input[\"path\"] ?? \"\");\n auditCommand = `${input.tool_name} ${filePath}`;\n}\naudit({\n sessionId: input.session_id,\n cwd: input.cwd,\n toolName: input.tool_name,\n command: auditCommand,\n decision: decision.action,\n rule: decision.rule,\n reason: decision.reason,\n permissionMode: input.permission_mode\n});\nif (decision.action === \"deny\") writeDeny(decision.reason);\nelse if (decision.advice) writeAdvice(decision.advice);\n\n//#endregion\nexport { };",
|
|
8419
|
+
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-write**: 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`.\n- **health-baseline-reset**: 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`.\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- **health-baseline-write**: 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`.\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"
|
|
8354
8420
|
}) {
|
|
8355
8421
|
const results = [{
|
|
8356
8422
|
filePath: HOOK_PATH,
|
|
@@ -9217,21 +9283,21 @@ const AGENT_RULES_HEAD = [
|
|
|
9217
9283
|
"### Agent rules",
|
|
9218
9284
|
"",
|
|
9219
9285
|
"- Use pnpm, not npm/yarn/npx. Run binaries with `pnpm exec`.",
|
|
9220
|
-
"-
|
|
9286
|
+
"- `mise install` provides the toolchain (`mise.toml`); a non-interactive session finds it only through mise's shims (`mise activate --shims` in the login profile)."
|
|
9221
9287
|
];
|
|
9222
9288
|
const TS_TYPECAST_RULE = "- No typecasts (`as`/`any`). Use zod schemas, type guards, or narrowing.";
|
|
9223
9289
|
const AGENT_RULES_MID = [
|
|
9224
9290
|
"- Fix lint violations; don't suppress them. For `no-empty-function`, use a `{ /* no-op */ }` body, not a disable comment.",
|
|
9225
|
-
"-
|
|
9291
|
+
"- Write text in any language; letters, accents and other scripts are fine. Keep out what `chars:check` denies: LLM-tell punctuation (curly quotes, ellipsis, en and em dashes, arrows, box-drawing, status glyphs) and invisible or deceptive characters (bidi controls, zero-width and tag characters, odd spaces). Write `...`, `-`/`--`, straight quotes, `->` and `-`/`|`/`+` instead. Where a denied character must survive to output, escape it for its context: `\\uXXXX` in a JS/TS/Kotlin/Java string, char or regex; `&#xXXXX;` in JSX or XML; a spliced `${'\\uXXXX'}` in a Kotlin raw string. `pnpm exec bst chars:check --fix` applies the safe rewrites (`--fix-ambiguous` for em dashes and status glyphs) and flags the rest.",
|
|
9226
9292
|
"- Write in plain language, per ISO 24495-1 -- every word you produce, code comments and commit bodies included."
|
|
9227
9293
|
];
|
|
9228
9294
|
const TS_EXTENSIONLESS_RULE = "- Prefer extensionless imports; if an extension is required, use `.ts` over `.js`.";
|
|
9229
9295
|
const CHANGESETS_RULE = "- A user-facing change owes a changeset in `.changeset/`, written with the change: `patch` for a fix, `minor` for a feature, naming every package it affects. One file per smallest unit of work.";
|
|
9230
|
-
const WORK_STATUS_RULE = "- Before starting or resuming work, run `bst work:status`.";
|
|
9231
|
-
const WORK_START_RULE = "- Before starting or resuming work, run `bst work:start <n>`.";
|
|
9232
|
-
const FIND_COMMAND_RULE = "- Find a command with `bst --help`, its flags with `bst <command> --help`, before guessing.";
|
|
9233
|
-
const FORGEJO_CLI_RULE_SUFFIX = " For Forgejo API work, run `bst-fj --help`.";
|
|
9234
|
-
const AGENT_CLI_RULE_SUFFIX = " For the agent's runs and transcripts, run `bst-agent --help`.";
|
|
9296
|
+
const WORK_STATUS_RULE = "- Before starting or resuming work, run `pnpm exec bst work:status`.";
|
|
9297
|
+
const WORK_START_RULE = "- Before starting or resuming work, run `pnpm exec bst work:start <n>`.";
|
|
9298
|
+
const FIND_COMMAND_RULE = "- Find a command with `pnpm exec bst --help`, its flags with `pnpm exec bst <command> --help`, before guessing.";
|
|
9299
|
+
const FORGEJO_CLI_RULE_SUFFIX = " For Forgejo API work, run `pnpm exec bst-fj --help`.";
|
|
9300
|
+
const AGENT_CLI_RULE_SUFFIX = " For the agent's runs and transcripts, run `pnpm exec bst-agent --help`.";
|
|
9235
9301
|
function declaresAny(packageJson, names) {
|
|
9236
9302
|
if (packageJson === void 0) return false;
|
|
9237
9303
|
return [
|
|
@@ -9252,7 +9318,7 @@ function findCommandRule(packageJson) {
|
|
|
9252
9318
|
function agentRulesTail(agentWorked) {
|
|
9253
9319
|
return [
|
|
9254
9320
|
"- The bst-hook blocks a short list of dangerous commands (`.claude/hooks/bst-hook-guide.md` lists them); Claude Code's auto mode judges everything else.",
|
|
9255
|
-
"- Machine-reading a
|
|
9321
|
+
"- Machine-reading a bst command? Run `pnpm exec bst <cmd> --json`, never `--json` before `<cmd>`; capture `2>&1` and parse it as one JSON document, failures included as `{ok: false, error}`. `--verbose` breaks this.",
|
|
9256
9322
|
agentWorked ? WORK_START_RULE : WORK_STATUS_RULE,
|
|
9257
9323
|
FIND_COMMAND_RULE,
|
|
9258
9324
|
"",
|
|
@@ -9262,7 +9328,7 @@ function agentRulesTail(agentWorked) {
|
|
|
9262
9328
|
"",
|
|
9263
9329
|
"**Small change** -- a fix, a doc edit, a one-file tweak: work on the default branch and leave it unstaged. Don't commit; surface the commit message as text for the user to run.",
|
|
9264
9330
|
"",
|
|
9265
|
-
"**Large effort** -- anything that warranted a written plan or spec (spec, then plan, then code, each approved before the next): start it in a worktree (see the Git guide), commit as you go, finish with `bst pr:create`. Push later commits with `bst pr:update`, which rewrites nothing unless given `--title`/`--body`. The one exception: a headless agent run (a spec, plan, execute, or review container) never starts a worktree -- it works and commits on the branch it was started on."
|
|
9331
|
+
"**Large effort** -- anything that warranted a written plan or spec (spec, then plan, then code, each approved before the next): start it in a worktree (see the Git guide), commit as you go, finish with `pnpm exec bst pr:create`. Push later commits with `pnpm exec bst pr:update`, which rewrites nothing unless given `--title`/`--body`. The one exception: a headless agent run (a spec, plan, execute, or review container) never starts a worktree -- it works and commits on the branch it was started on."
|
|
9266
9332
|
];
|
|
9267
9333
|
}
|
|
9268
9334
|
const COMMENTS_GUIDE_BODY = [
|
|
@@ -9303,9 +9369,11 @@ const GIT_GUIDE_BODY = [
|
|
|
9303
9369
|
"",
|
|
9304
9370
|
"## Claiming work by hand",
|
|
9305
9371
|
"",
|
|
9306
|
-
"If the bst agent drives this repo, claim an issue or pull request before you work it by hand, so the agent does not start a run beside you: run `bst work:start <n>`. It prints the same orientation `
|
|
9372
|
+
"If the bst agent drives this repo, claim an issue or pull request before you work it by hand, so the agent does not start a run beside you: run `pnpm exec bst work:start <n>`. It prints the same orientation `work:status` does and then puts `agent-flags/hand-authored` on the object. It claims only where the repo's default branch sets `agent.enabled: true`, and does nothing anywhere else. **Exit 1 means do not start**: a run is live on the pair, or the agent holds it -- the output names which. Exit 0 means the pair is yours, whether the command wrote the flag or found it there already.",
|
|
9307
9373
|
"",
|
|
9308
|
-
"
|
|
9374
|
+
"The flag records who is working an object now, not who wrote it. Do not claim an issue or pull request you only file or open for later, even one a person or a coding session wrote by hand: leave it unclaimed so the agent can take it if asked. Claim it when you are about to work it.",
|
|
9375
|
+
"",
|
|
9376
|
+
"A pull request you open yourself that closes no claimed issue needs its own `pnpm exec bst work:start <n>` once it exists -- the flag on either half of a pair holds both, but there is no pair until something closes something.",
|
|
9309
9377
|
"",
|
|
9310
9378
|
"Hand the work back with the control panel's **Release to the agent**, or by removing `agent-flags/hand-authored` from both halves. Removing it does not queue anything: the agent picks the object up on its next cycle only if a label or a comment asks it to.",
|
|
9311
9379
|
"",
|
|
@@ -9325,7 +9393,7 @@ const GIT_GUIDE_BODY = [
|
|
|
9325
9393
|
"",
|
|
9326
9394
|
"**Drop `WIP:` and fix the stem when the work is done.** Once the pull request carries finished work you are handing over for review -- a spec or a plan ready to approve, or an implementation whose check passes -- remove the `WIP:` marker and make the stem match what it now carries: no stem for an implementation. Do it in the same step that reports the work done, without being asked. A marker left behind tells a reviewer the work is still moving.",
|
|
9327
9395
|
"",
|
|
9328
|
-
"`pr:update` never changes a pull request's draft state or its base. `bst pr:ready <n>` marks a draft ready and drops its `WIP:` marker on either forge; `--title \"<title>\"` fixes the stem in the same step. `bst pr:retarget <n> --base <branch>` repoints a pull request, such as a stacked one whose parent has merged. Both confirm the change by re-reading the forge.",
|
|
9396
|
+
"`pr:update` never changes a pull request's draft state or its base. `pnpm exec bst pr:ready <n>` marks a draft ready and drops its `WIP:` marker on either forge; `--title \"<title>\"` fixes the stem in the same step. `pnpm exec bst pr:retarget <n> --base <branch>` repoints a pull request, such as a stacked one whose parent has merged. Both confirm the change by re-reading the forge.",
|
|
9329
9397
|
"",
|
|
9330
9398
|
"## Merging the default branch in",
|
|
9331
9399
|
"",
|
|
@@ -9340,13 +9408,13 @@ const PLAN_LIFECYCLE_GUIDE_BODY = [
|
|
|
9340
9408
|
"",
|
|
9341
9409
|
"`.superpowers/` (no `docs/` prefix) is scratch state, not a plan's home -- a plan doc lives under `docs/plans/` (older ones under `docs/superpowers/plans/`), even one drafted while running a plan through subagents.",
|
|
9342
9410
|
"",
|
|
9343
|
-
"Retiring the plan is the last task of the plan, done in the PR that delivers the work: \"in the tree\" means committed on this branch, not merged to a default branch. Run `bst docs:retire <plan.md> --status <verb> --shipped \"<text>\"` -- it moves the doc, relinks what the move breaks, de-links paths pointing outside `docs/`, and writes the dated terminal `Status:` line, the historical banner, and the \"Shipped:\" paragraph. You supply the judgement: the honest verb -- `completed` / `superseded` / `abandoned` / `rejected` -- and a truthful `--shipped` naming the delivering PR; never fabricate it for uncommitted code or claim a merge that has not happened. A plan that lands one phase of a larger spec retires the plan and leaves the spec open.",
|
|
9411
|
+
"Retiring the plan is the last task of the plan, done in the PR that delivers the work: \"in the tree\" means committed on this branch, not merged to a default branch. Run `pnpm exec bst docs:retire <plan.md> --status <verb> --shipped \"<text>\"` -- it moves the doc, relinks what the move breaks, de-links paths pointing outside `docs/`, and writes the dated terminal `Status:` line, the historical banner, and the \"Shipped:\" paragraph. You supply the judgement: the honest verb -- `completed` / `superseded` / `abandoned` / `rejected` -- and a truthful `--shipped` naming the delivering PR; never fabricate it for uncommitted code or claim a merge that has not happened. A plan that lands one phase of a larger spec retires the plan and leaves the spec open.",
|
|
9344
9412
|
"",
|
|
9345
|
-
"`
|
|
9413
|
+
"`docs:check` enforces the mechanical half (status <-> directory, dated retirements, links, staleness). It cannot tell whether the code shipped -- that's your job.",
|
|
9346
9414
|
"",
|
|
9347
|
-
`An active doc's \`Status:\` may say anything that is not a retirement word; \`draft\`, \`approved\`, and \`planned\` are the usual ones before anything ships. \`
|
|
9415
|
+
`An active doc's \`Status:\` may say anything that is not a retirement word; \`draft\`, \`approved\`, and \`planned\` are the usual ones before anything ships. \`docs:check\` reads a status as retired when it contains one of ${codeList(TERMINAL_STATUS_WORDS)} and none of ${codeList(OPEN_STATUS_WORDS)}, so a status naming a shipped phase stays active while it also says an open word. A spec whose status says ${codeList(PARTIAL_STATUS_WORDS)} is read as partly delivered, and \`docs:check\` stops warning that every plan implementing it has retired.`,
|
|
9348
9416
|
"",
|
|
9349
|
-
"**A retired doc is a historical record, not documentation you maintain.** Do not read one for staleness or check its claims against the tree -- that misreading has already caused real defects -- and keeping one up to date is not your job. One question decides whether an edit is allowed: would it have been right the day the doc retired? Yes means the doc misrecords its own moment and you may fix it where you already are -- an unchecked box for work that PR delivered, a `Shipped:` naming the wrong PR. No means the tree moved afterwards and the doc is merely old -- a renamed path, a superseded claim -- so leave it. The move, status line, banner, and delinking stay `
|
|
9417
|
+
"**A retired doc is a historical record, not documentation you maintain.** Do not read one for staleness or check its claims against the tree -- that misreading has already caused real defects -- and keeping one up to date is not your job. One question decides whether an edit is allowed: would it have been right the day the doc retired? Yes means the doc misrecords its own moment and you may fix it where you already are -- an unchecked box for work that PR delivered, a `Shipped:` naming the wrong PR. No means the tree moved afterwards and the doc is merely old -- a renamed path, a superseded claim -- so leave it. The move, status line, banner, and delinking stay `docs:retire`/`docs:delink`'s; never hand-write those."
|
|
9350
9418
|
].join("\n");
|
|
9351
9419
|
const PLANNING_GUIDE_BODY = [
|
|
9352
9420
|
"# Specs and plans",
|
|
@@ -9359,10 +9427,22 @@ const PLANNING_GUIDE_BODY = [
|
|
|
9359
9427
|
"",
|
|
9360
9428
|
"## The plan",
|
|
9361
9429
|
"",
|
|
9362
|
-
"Write for an executor who has not seen this repo or the spec. They write idiomatic code once they know the exact interface and the exact test, and make a reasonable choice wherever the plan leaves one open; what they cannot know is what you decided. A plan records those decisions -- it is not a transcript of the code. Save it under `docs/plans
|
|
9430
|
+
"Write for an executor who has not seen this repo or the spec. They write idiomatic code once they know the exact interface and the exact test, and make a reasonable choice wherever the plan leaves one open; what they cannot know is what you decided. A plan records those decisions -- it is not a transcript of the code. Save it under `docs/plans/`, named by the rules below.",
|
|
9431
|
+
"",
|
|
9432
|
+
"### Which plan a branch works",
|
|
9433
|
+
"",
|
|
9434
|
+
"A plan may be written by you, by another session, or by a headless agent, and the run that executes it may be any of these. That run never asks which plan is meant: it finds the plan from the branch alone, so the name and the git history must say it.",
|
|
9435
|
+
"",
|
|
9436
|
+
"- A branch has one live plan. Tooling looks first for a path under `docs/` that carries the marker of the issue or pull request the run works (`issue-<n>` or `pr-<n>`), then for the one live plan the branch **added**. A plan the branch only edited or renamed is not an added file, so a plan that has neither the marker nor the add is invisible to the run that should execute it, however clearly its text names the work.",
|
|
9437
|
+
"- Name a new plan `docs/plans/<YYYY-MM-DD>-<topic>.md`, and when the work has an issue or pull request, put its marker in the name: `docs/plans/<YYYY-MM-DD>-issue-<n>-<topic>.md`. The marker is what keeps the plan findable when a second plan joins the branch.",
|
|
9438
|
+
"- To refine a plan this branch already added, edit it in place and keep its name, so the branch never carries two plans for one piece of work.",
|
|
9439
|
+
"- A plan the branch inherited from the default branch -- a deferred plan, or a plan that landed in an earlier pull request -- is found only by its marker. To work one, whether you rewrite it or execute it, rename it with `git mv` to a name that carries the marker, in the first commit that touches it. Use the marker of the object the run works: when an agent executes the plan on a pull request, that is the pull request's `pr-<n>`, not the issue's. Do not edit an inherited plan under its old name.",
|
|
9440
|
+
"- After committing the plan, check that it is findable: `git diff --name-status <default branch>...HEAD -- docs/plans` lists it with `A`, or its path carries the marker.",
|
|
9441
|
+
"",
|
|
9442
|
+
"### Writing the plan",
|
|
9363
9443
|
"",
|
|
9364
9444
|
"- 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.",
|
|
9365
|
-
"- Label each task `Task <n>:` at the start of its heading or checkbox, numbered from 1, so `
|
|
9445
|
+
"- 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.",
|
|
9366
9446
|
`- 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.`,
|
|
9367
9447
|
"- 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.",
|
|
9368
9448
|
"- A plan that builds on earlier work opens with a task that checks the prerequisite exists, and records a `**Blocked:**` note if it does not, instead of rebuilding it.",
|
|
@@ -9400,10 +9480,10 @@ const SUBAGENTS_GUIDE_BODY = [
|
|
|
9400
9480
|
"",
|
|
9401
9481
|
"## Running a plan through subagents",
|
|
9402
9482
|
"",
|
|
9403
|
-
"- The plan's checkboxes are the progress record. Tick a task's box and commit once its review is clean. After a context reset, `bst plan:status <plan>` names the next open task; trust it and `git log` over your memory, and never re-run a ticked task.",
|
|
9404
|
-
"- `bst plan:workspace <plan>` prints the plan's own git-ignored scratch directory for briefs, reports, review packages, and fix-round notes. Nothing that must outlive the run goes there.",
|
|
9483
|
+
"- The plan's checkboxes are the progress record. Tick a task's box and commit once its review is clean. After a context reset, `pnpm exec bst plan:status <plan>` names the next open task; trust it and `git log` over your memory, and never re-run a ticked task.",
|
|
9484
|
+
"- `pnpm exec bst plan:workspace <plan>` prints the plan's own git-ignored scratch directory for briefs, reports, review packages, and fix-round notes. Nothing that must outlive the run goes there.",
|
|
9405
9485
|
"- Before the first task, check the plan for tasks that contradict each other or the spec. Rule on each (the spec wins) and record the ruling.",
|
|
9406
|
-
"- Per task: note the base commit, run `bst plan:brief <plan> <n>`, and give one implementer the brief file it writes plus a report file to write back. Then run `bst plan:review-package <plan> <base> <head>` and have a separate reviewer judge that file against the brief, for both spec compliance and quality. Never tell a reviewer what not to flag.",
|
|
9486
|
+
"- Per task: note the base commit, run `pnpm exec bst plan:brief <plan> <n>`, and give one implementer the brief file it writes plus a report file to write back. Then run `pnpm exec bst plan:review-package <plan> <base> <head>` and have a separate reviewer judge that file against the brief, for both spec compliance and quality. Never tell a reviewer what not to flag.",
|
|
9407
9487
|
"- Findings go back to the same implementer, followed by a review of the fix diff only. Cap it at five rounds, switching to a stronger model from round four. At the cap, rule on each open finding and record why. Never fix in your own session: that skips review. A minor finding nobody fixes in this run goes to the findings ledger, not a scratch note.",
|
|
9408
9488
|
"- Do not stop between tasks to ask. Stop only for something irreversible or destructive, a security-sensitive action, a side effect outside the working tree (a push, a merge, a publish), or a plan so broken that every way forward is a guess. Anything else, decide, and record the ruling with what it costs if wrong.",
|
|
9409
9489
|
"- Keep every ruling where the human will read it: list them all, each with what it costs if wrong, in your final message and in the pull request body.",
|
|
@@ -9486,14 +9566,14 @@ const CODE_REVIEW_GUIDE_BODY = [
|
|
|
9486
9566
|
const FINDINGS_LEDGER_BODY = [
|
|
9487
9567
|
"# Findings ledger",
|
|
9488
9568
|
"",
|
|
9489
|
-
"A defect you notice but are not fixing in the current change -- a nit, or something pre-existing -- goes in `.agent/findings/`, one file per finding, via `bst findings:add` (never hand-write the filename: a name the parser cannot read back is silently dropped from every listing). A finding names the PR or issue that surfaced it. The file IS the pending state: merging it enqueues it for the automated backlog run, fixing or refuting it means deleting the file in the same commit (`bst findings:resolve`), and deferring it appends a note (`bst findings:resolve --defer`). A finding only a human can decide leaves the ledger as an issue: the backlog run escalates it through the `human-decision` follow-up route and deletes the file; by hand, file the issue yourself and `bst findings:resolve` the entry. `bst findings:list` shows what is pending."
|
|
9569
|
+
"A defect you notice but are not fixing in the current change -- a nit, or something pre-existing -- goes in `.agent/findings/`, one file per finding, via `pnpm exec bst findings:add` (never hand-write the filename: a name the parser cannot read back is silently dropped from every listing). A finding names the PR or issue that surfaced it. The file IS the pending state: merging it enqueues it for the automated backlog run, fixing or refuting it means deleting the file in the same commit (`pnpm exec bst findings:resolve`), and deferring it appends a note (`pnpm exec bst findings:resolve --defer`). A finding only a human can decide leaves the ledger as an issue: the backlog run escalates it through the `human-decision` follow-up route and deletes the file; by hand, file the issue yourself and `pnpm exec bst findings:resolve` the entry. `pnpm exec bst findings:list` shows what is pending."
|
|
9490
9570
|
].join("\n");
|
|
9491
9571
|
const COVERAGE_RULE_BODY = [
|
|
9492
9572
|
"# Coverage baseline",
|
|
9493
9573
|
"",
|
|
9494
|
-
"Touched `src/`? Run `pnpm test:coverage`, then `pnpm exec bst coverage:check`. If a total regressed beyond tolerance, add tests until parity -- or exclude the file via `coverage.exclude` in `.tooling.json` and say why in the commit
|
|
9574
|
+
"Touched `src/`? Run `pnpm test:coverage`, then `pnpm exec bst coverage:check`. If a total regressed beyond tolerance, add tests until parity -- or exclude the file via `coverage.exclude` in `.tooling.json` and say why in the commit. If no total dropped, run `pnpm exec bst coverage:check --update-baseline` and stage `coverage-baseline.json`. Skip for docs-, config-, dependency-, or test-only changes.",
|
|
9495
9575
|
"",
|
|
9496
|
-
"If the total dropped only because previously-untested code entered the denominator -- a first test, or `coverage.include` newly enabled -- that is expected, not a regression: do not exclude the file you just tested. Adopt the baseline instead: `bst coverage:check --update-baseline --reset` (
|
|
9576
|
+
"If the total dropped only because previously-untested code entered the denominator -- a first test, or `coverage.include` newly enabled -- that is expected, not a regression: do not exclude the file you just tested. Adopt the baseline instead: `pnpm exec bst coverage:check --update-baseline --reset` (a downward move needs `--reset`).",
|
|
9497
9577
|
"",
|
|
9498
9578
|
"Totals at or above `coverage.floorPp` pass and re-baseline freely; the ratchet applies only below the floor."
|
|
9499
9579
|
].join("\n");
|
|
@@ -9524,7 +9604,7 @@ function checkCadenceBody(config) {
|
|
|
9524
9604
|
const HEALTH_RULE_HEAD = [
|
|
9525
9605
|
"# Codebase health",
|
|
9526
9606
|
"",
|
|
9527
|
-
"`
|
|
9607
|
+
"`health:check` gates four structural metrics against `health-baseline.json`: any increase fails, as does a module or directory past its recorded ceiling. `pnpm exec bst health:summary --explain` names the offenders.",
|
|
9528
9608
|
""
|
|
9529
9609
|
];
|
|
9530
9610
|
const HEALTH_RULE_SAME_PACKAGE = "**The fan-in, fan-out, and cycle metrics count value edges only.** A type-only import is erased before the graph is built and never moves a metric. An import across a workspace package boundary counts at the `health.crossPackage` default and is invisible when it is off.";
|
|
@@ -9533,11 +9613,11 @@ const HEALTH_RULE_TAIL = [
|
|
|
9533
9613
|
"",
|
|
9534
9614
|
"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.",
|
|
9535
9615
|
"",
|
|
9536
|
-
"**An exemption takes a module or directory out of the gate.** It is listed with its count and reason in `
|
|
9616
|
+
"**An exemption takes a module or directory out of the gate.** It is listed with its count and reason in `health:summary --explain`, carries no ceiling, and is counted in no metric. `crowdedDirectories` is exemptable by directory path. Adding one is a raise too.",
|
|
9537
9617
|
"",
|
|
9538
|
-
"**A raise travels alone.** A raise is a higher count or ceiling, a ceiling for a module that has none, a new exemption, or a higher limit. It lands only as a ceiling-only pull request -- one whose diff changes nothing but `health-baseline.json` and `.tooling.json` -- which a human merges. When `
|
|
9618
|
+
"**A raise travels alone.** A raise is a higher count or ceiling, a ceiling for a module that has none, a new exemption, or a higher limit. It lands only as a ceiling-only pull request -- one whose diff changes nothing but `health-baseline.json` and `.tooling.json` -- which a human merges. When `health:check` refuses, split the module or open that pull request, and stop. Never run `pnpm exec bst health:check --update-baseline --reset` on a branch that carries code.",
|
|
9539
9619
|
"",
|
|
9540
|
-
"Prefer an exemption to raising `fanInLimit` or `fanOutLimit`: an exemption names the hub in `
|
|
9620
|
+
"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."
|
|
9541
9621
|
];
|
|
9542
9622
|
function healthRuleBody(config) {
|
|
9543
9623
|
const lines = [...HEALTH_RULE_HEAD];
|
|
@@ -9672,7 +9752,7 @@ function agentGuides(config, coverageEnabled, healthEnabled, webUiEnabled) {
|
|
|
9672
9752
|
},
|
|
9673
9753
|
{
|
|
9674
9754
|
name: "health.md",
|
|
9675
|
-
trigger: "add a cross-module import, or when `
|
|
9755
|
+
trigger: "add a cross-module import, or when `health:check` fails",
|
|
9676
9756
|
body: healthRuleBody(config),
|
|
9677
9757
|
enabled: healthEnabled
|
|
9678
9758
|
}
|
|
@@ -9682,7 +9762,7 @@ function guideIndex(guides) {
|
|
|
9682
9762
|
const lines = [
|
|
9683
9763
|
"#### Guides",
|
|
9684
9764
|
"",
|
|
9685
|
-
"Each guide below is a file `
|
|
9765
|
+
"Each guide below is a file `repo:sync` keeps current. Read it at the moment it names, before you act. Where an installed plugin or skill covers the same ground as a guide, the guide wins.",
|
|
9686
9766
|
""
|
|
9687
9767
|
];
|
|
9688
9768
|
for (const guide of guides.filter((g) => g.enabled)) lines.push(`- Before you ${guide.trigger}: \`${GUIDES_DIR}/${guide.name}\`.`);
|
|
@@ -9999,7 +10079,7 @@ function reportFailures(failed, command) {
|
|
|
9999
10079
|
function generateMigratePrompt(results, config, detected) {
|
|
10000
10080
|
const sections = [];
|
|
10001
10081
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
10002
|
-
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.
|
|
10082
|
+
sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.93.0 repo:sync\` on ${timestamp}_\n`);
|
|
10003
10083
|
const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
|
|
10004
10084
|
if (changed.length > 0) {
|
|
10005
10085
|
const items = changed.map((r) => {
|
|
@@ -11657,19 +11737,32 @@ function withHealthCheck(checks, step = HEALTH_CHECK) {
|
|
|
11657
11737
|
...checks.slice(at)
|
|
11658
11738
|
];
|
|
11659
11739
|
}
|
|
11660
|
-
const
|
|
11661
|
-
|
|
11740
|
+
const CHARS_CHECK = { name: "chars:check" };
|
|
11741
|
+
const LEGACY_CHARS_CHECK_SCRIPT = "ascii:check";
|
|
11742
|
+
function legacyCharsAlias(name) {
|
|
11743
|
+
return name === LEGACY_CHARS_CHECK_SCRIPT ? CHARS_CHECK.name : void 0;
|
|
11744
|
+
}
|
|
11745
|
+
function withLegacyCharsSkip(entries) {
|
|
11746
|
+
const skip = new Set(entries);
|
|
11747
|
+
const aliases = [...skip].map(legacyCharsAlias).filter((name) => name !== void 0);
|
|
11748
|
+
for (const alias of aliases) skip.add(alias);
|
|
11749
|
+
return skip;
|
|
11750
|
+
}
|
|
11751
|
+
function withCharsCheck(checks) {
|
|
11662
11752
|
const idx = checks.findIndex((c) => c.name === "lint");
|
|
11663
11753
|
const at = idx === -1 ? checks.length : idx + 1;
|
|
11664
11754
|
return [
|
|
11665
11755
|
...checks.slice(0, at),
|
|
11666
|
-
|
|
11756
|
+
CHARS_CHECK,
|
|
11667
11757
|
...checks.slice(at)
|
|
11668
11758
|
];
|
|
11669
11759
|
}
|
|
11760
|
+
function charsBlock(config) {
|
|
11761
|
+
return config?.chars ?? config?.ascii;
|
|
11762
|
+
}
|
|
11670
11763
|
function resolveBuiltinChecks(config) {
|
|
11671
11764
|
let builtin = CHECKS;
|
|
11672
|
-
if (config
|
|
11765
|
+
if (isCharsGateOn(charsBlock(config))) builtin = withCharsCheck(builtin);
|
|
11673
11766
|
if (config?.health?.enabled === true) builtin = withHealthCheck(builtin);
|
|
11674
11767
|
return builtin;
|
|
11675
11768
|
}
|
|
@@ -11688,7 +11781,7 @@ function resolveRunBuiltins(targetDir, config, referenceHealthOn = referenceEnab
|
|
|
11688
11781
|
function staleChecksAddNames(config, add = config?.checks?.add ?? []) {
|
|
11689
11782
|
if (add.length === 0) return [];
|
|
11690
11783
|
const names = new Set(resolveBuiltinChecks(config).map((check) => check.name));
|
|
11691
|
-
return add.filter((name) => names.has(name));
|
|
11784
|
+
return add.filter((name) => names.has(name) || names.has(legacyCharsAlias(name) ?? ""));
|
|
11692
11785
|
}
|
|
11693
11786
|
function shouldSkip(name, patterns) {
|
|
11694
11787
|
if (patterns.size === 0) return false;
|
|
@@ -11844,8 +11937,8 @@ async function runRunChecks(targetDir, options = {}) {
|
|
|
11844
11937
|
const getScripts = options.getScripts ?? defaultGetScripts;
|
|
11845
11938
|
const toolingConfig = options.toolingConfig ?? loadToolingConfig(targetDir);
|
|
11846
11939
|
const configChecks = toolingConfig?.checks;
|
|
11847
|
-
const flagSkip =
|
|
11848
|
-
const configSkip =
|
|
11940
|
+
const flagSkip = withLegacyCharsSkip(options.skip ?? []);
|
|
11941
|
+
const configSkip = withLegacyCharsSkip(configChecks?.skip ?? []);
|
|
11849
11942
|
const skip = /* @__PURE__ */ new Set([...flagSkip, ...configSkip]);
|
|
11850
11943
|
const add = [.../* @__PURE__ */ new Set([...options.add ?? [], ...configChecks?.add ?? []])];
|
|
11851
11944
|
const isCI = Boolean(process.env["CI"]);
|
|
@@ -11853,17 +11946,24 @@ async function runRunChecks(targetDir, options = {}) {
|
|
|
11853
11946
|
const reporter = isCI ? ciReporter : localReporter;
|
|
11854
11947
|
const vc = { verbose: options.verbose ?? false };
|
|
11855
11948
|
const definedScripts = getScripts(targetDir);
|
|
11949
|
+
const definedCheckScripts = new Set(definedScripts);
|
|
11950
|
+
if (!definedCheckScripts.has(CHARS_CHECK.name) && definedScripts.has(LEGACY_CHARS_CHECK_SCRIPT)) definedCheckScripts.add(CHARS_CHECK.name);
|
|
11856
11951
|
const addedNames = new Set(add);
|
|
11857
11952
|
const builtinChecks = resolveRunBuiltins(targetDir, toolingConfig, options.referenceEnablesHealth);
|
|
11858
11953
|
const builtinNames = new Set(builtinChecks.map((check) => check.name));
|
|
11859
|
-
|
|
11954
|
+
function isBuiltInName(name) {
|
|
11955
|
+
if (builtinNames.has(name)) return true;
|
|
11956
|
+
const alias = legacyCharsAlias(name);
|
|
11957
|
+
return alias !== void 0 && builtinNames.has(alias);
|
|
11958
|
+
}
|
|
11959
|
+
const allChecks = [...builtinChecks, ...add.filter((name) => !isBuiltInName(name)).map((name) => ({ name }))];
|
|
11860
11960
|
const failures = [];
|
|
11861
11961
|
const warnedSteps = [];
|
|
11862
11962
|
const notDefined = [];
|
|
11863
11963
|
const coverage = decideCoverage(targetDir, options, toolingConfig);
|
|
11864
11964
|
const useCoverageScript = coverage.enabled && definedScripts.has("test:coverage");
|
|
11865
11965
|
logCoverageChoice(vc, coverage, useCoverageScript, options.coverage);
|
|
11866
|
-
const skippedSteps = reportSkippedUnlessRefused(reporter, allChecks,
|
|
11966
|
+
const skippedSteps = reportSkippedUnlessRefused(reporter, allChecks, definedCheckScripts, flagSkip, configSkip);
|
|
11867
11967
|
for (const check of allChecks) {
|
|
11868
11968
|
if (shouldSkip(check.name, skip)) {
|
|
11869
11969
|
if (requiredSkipIsRefused(check, flagSkip, configSkip)) {
|
|
@@ -11873,7 +11973,7 @@ async function runRunChecks(targetDir, options = {}) {
|
|
|
11873
11973
|
}
|
|
11874
11974
|
continue;
|
|
11875
11975
|
}
|
|
11876
|
-
if (!
|
|
11976
|
+
if (!definedCheckScripts.has(check.name)) {
|
|
11877
11977
|
if (addedNames.has(check.name) || check.required === true) {
|
|
11878
11978
|
reporter.undefinedCheck(check.name);
|
|
11879
11979
|
failures.push(check.name);
|
|
@@ -11883,6 +11983,7 @@ async function runRunChecks(targetDir, options = {}) {
|
|
|
11883
11983
|
let scriptName = check.name;
|
|
11884
11984
|
if (check.name === "test" && useCoverageScript) scriptName = "test:coverage";
|
|
11885
11985
|
if (check.name === "lint" && definedScripts.has("lint:report")) scriptName = "lint:report";
|
|
11986
|
+
if (check.name === CHARS_CHECK.name && !definedScripts.has(CHARS_CHECK.name) && definedScripts.has(LEGACY_CHARS_CHECK_SCRIPT)) scriptName = LEGACY_CHARS_CHECK_SCRIPT;
|
|
11886
11987
|
const cmd = check.args ? `pnpm run ${scriptName} ${check.args}` : `pnpm run ${scriptName}`;
|
|
11887
11988
|
debug(vc, `Running: ${cmd} (in ${targetDir})`);
|
|
11888
11989
|
reporter.groupStart(check.name);
|
|
@@ -11915,11 +12016,11 @@ async function runRunChecks(targetDir, options = {}) {
|
|
|
11915
12016
|
reporter.allPassed(warnedSteps, skippedSteps);
|
|
11916
12017
|
return 0;
|
|
11917
12018
|
}
|
|
11918
|
-
const HELP_CHECK_NAMES =
|
|
12019
|
+
const HELP_CHECK_NAMES = withCharsCheck(withHealthCheck(CHECKS)).map((check) => check.name).join(", ");
|
|
11919
12020
|
const runChecksCommand = defineCommand({
|
|
11920
12021
|
meta: {
|
|
11921
12022
|
name: "checks:run",
|
|
11922
|
-
description: `Run the repo's standard check pipeline. The steps are ${HELP_CHECK_NAMES} (
|
|
12023
|
+
description: `Run the repo's standard check pipeline. The steps are ${HELP_CHECK_NAMES} (chars:check runs unless .tooling.json#chars.enabled is false, and health:check only where .tooling.json enables it). A step that exits 0 and prints ::bst-checks:: warn <count> on stdout is reported as warned; no exit code changes.`
|
|
11923
12024
|
},
|
|
11924
12025
|
args: {
|
|
11925
12026
|
dir: {
|
|
@@ -15713,7 +15814,7 @@ function describeForgeError(error) {
|
|
|
15713
15814
|
if (error instanceof RepoTargetMismatchError) return `--repo disagrees with the issue URL: --repo names ${error.given}, the issue URL names ${error.fromReference}.`;
|
|
15714
15815
|
}
|
|
15715
15816
|
//#endregion
|
|
15716
|
-
//#region src/
|
|
15817
|
+
//#region src/chars-check/offenders.ts
|
|
15717
15818
|
const TIER1_OFFENDERS = [
|
|
15718
15819
|
{
|
|
15719
15820
|
codepoint: "U+2018",
|
|
@@ -15995,12 +16096,349 @@ const OUTPUT_GLYPH_OFFENDERS = [
|
|
|
15995
16096
|
replacement: "delta"
|
|
15996
16097
|
}
|
|
15997
16098
|
];
|
|
16099
|
+
function rangeOffenders(first, last, name, group) {
|
|
16100
|
+
const offenders = [];
|
|
16101
|
+
for (let codePoint = first; codePoint <= last; codePoint += 1) offenders.push({
|
|
16102
|
+
codepoint: `U+${codePoint.toString(16).toUpperCase().padStart(4, "0")}`,
|
|
16103
|
+
name,
|
|
16104
|
+
tier: 1,
|
|
16105
|
+
char: String.fromCodePoint(codePoint),
|
|
16106
|
+
group
|
|
16107
|
+
});
|
|
16108
|
+
return offenders;
|
|
16109
|
+
}
|
|
16110
|
+
const INVISIBLE_OFFENDERS = [
|
|
16111
|
+
{
|
|
16112
|
+
codepoint: "U+202A",
|
|
16113
|
+
name: "LEFT-TO-RIGHT EMBEDDING",
|
|
16114
|
+
tier: 1,
|
|
16115
|
+
char: "",
|
|
16116
|
+
group: "invisible"
|
|
16117
|
+
},
|
|
16118
|
+
{
|
|
16119
|
+
codepoint: "U+202B",
|
|
16120
|
+
name: "RIGHT-TO-LEFT EMBEDDING",
|
|
16121
|
+
tier: 1,
|
|
16122
|
+
char: "",
|
|
16123
|
+
group: "invisible"
|
|
16124
|
+
},
|
|
16125
|
+
{
|
|
16126
|
+
codepoint: "U+202C",
|
|
16127
|
+
name: "POP DIRECTIONAL FORMATTING",
|
|
16128
|
+
tier: 1,
|
|
16129
|
+
char: "",
|
|
16130
|
+
group: "invisible"
|
|
16131
|
+
},
|
|
16132
|
+
{
|
|
16133
|
+
codepoint: "U+202D",
|
|
16134
|
+
name: "LEFT-TO-RIGHT OVERRIDE",
|
|
16135
|
+
tier: 1,
|
|
16136
|
+
char: "",
|
|
16137
|
+
group: "invisible"
|
|
16138
|
+
},
|
|
16139
|
+
{
|
|
16140
|
+
codepoint: "U+202E",
|
|
16141
|
+
name: "RIGHT-TO-LEFT OVERRIDE",
|
|
16142
|
+
tier: 1,
|
|
16143
|
+
char: "",
|
|
16144
|
+
group: "invisible"
|
|
16145
|
+
},
|
|
16146
|
+
{
|
|
16147
|
+
codepoint: "U+2066",
|
|
16148
|
+
name: "LEFT-TO-RIGHT ISOLATE",
|
|
16149
|
+
tier: 1,
|
|
16150
|
+
char: "",
|
|
16151
|
+
group: "invisible"
|
|
16152
|
+
},
|
|
16153
|
+
{
|
|
16154
|
+
codepoint: "U+2067",
|
|
16155
|
+
name: "RIGHT-TO-LEFT ISOLATE",
|
|
16156
|
+
tier: 1,
|
|
16157
|
+
char: "",
|
|
16158
|
+
group: "invisible"
|
|
16159
|
+
},
|
|
16160
|
+
{
|
|
16161
|
+
codepoint: "U+2068",
|
|
16162
|
+
name: "FIRST STRONG ISOLATE",
|
|
16163
|
+
tier: 1,
|
|
16164
|
+
char: "",
|
|
16165
|
+
group: "invisible"
|
|
16166
|
+
},
|
|
16167
|
+
{
|
|
16168
|
+
codepoint: "U+2069",
|
|
16169
|
+
name: "POP DIRECTIONAL ISOLATE",
|
|
16170
|
+
tier: 1,
|
|
16171
|
+
char: "",
|
|
16172
|
+
group: "invisible"
|
|
16173
|
+
},
|
|
16174
|
+
{
|
|
16175
|
+
codepoint: "U+200B",
|
|
16176
|
+
name: "ZERO WIDTH SPACE",
|
|
16177
|
+
tier: 1,
|
|
16178
|
+
char: "",
|
|
16179
|
+
group: "invisible"
|
|
16180
|
+
},
|
|
16181
|
+
{
|
|
16182
|
+
codepoint: "U+2060",
|
|
16183
|
+
name: "WORD JOINER",
|
|
16184
|
+
tier: 1,
|
|
16185
|
+
char: "",
|
|
16186
|
+
group: "invisible"
|
|
16187
|
+
},
|
|
16188
|
+
{
|
|
16189
|
+
codepoint: "U+2061",
|
|
16190
|
+
name: "FUNCTION APPLICATION",
|
|
16191
|
+
tier: 1,
|
|
16192
|
+
char: "",
|
|
16193
|
+
group: "invisible"
|
|
16194
|
+
},
|
|
16195
|
+
{
|
|
16196
|
+
codepoint: "U+2062",
|
|
16197
|
+
name: "INVISIBLE TIMES",
|
|
16198
|
+
tier: 1,
|
|
16199
|
+
char: "",
|
|
16200
|
+
group: "invisible"
|
|
16201
|
+
},
|
|
16202
|
+
{
|
|
16203
|
+
codepoint: "U+2063",
|
|
16204
|
+
name: "INVISIBLE SEPARATOR",
|
|
16205
|
+
tier: 1,
|
|
16206
|
+
char: "",
|
|
16207
|
+
group: "invisible"
|
|
16208
|
+
},
|
|
16209
|
+
{
|
|
16210
|
+
codepoint: "U+2064",
|
|
16211
|
+
name: "INVISIBLE PLUS",
|
|
16212
|
+
tier: 1,
|
|
16213
|
+
char: "",
|
|
16214
|
+
group: "invisible"
|
|
16215
|
+
},
|
|
16216
|
+
{
|
|
16217
|
+
codepoint: "U+FEFF",
|
|
16218
|
+
name: "ZERO WIDTH NO-BREAK SPACE",
|
|
16219
|
+
tier: 1,
|
|
16220
|
+
char: "",
|
|
16221
|
+
group: "invisible"
|
|
16222
|
+
},
|
|
16223
|
+
{
|
|
16224
|
+
codepoint: "U+00AD",
|
|
16225
|
+
name: "SOFT HYPHEN",
|
|
16226
|
+
tier: 1,
|
|
16227
|
+
char: "",
|
|
16228
|
+
group: "invisible"
|
|
16229
|
+
},
|
|
16230
|
+
{
|
|
16231
|
+
codepoint: "U+115F",
|
|
16232
|
+
name: "HANGUL CHOSEONG FILLER",
|
|
16233
|
+
tier: 1,
|
|
16234
|
+
char: "ᅟ",
|
|
16235
|
+
group: "invisible"
|
|
16236
|
+
},
|
|
16237
|
+
{
|
|
16238
|
+
codepoint: "U+1160",
|
|
16239
|
+
name: "HANGUL JUNGSEONG FILLER",
|
|
16240
|
+
tier: 1,
|
|
16241
|
+
char: "ᅠ",
|
|
16242
|
+
group: "invisible"
|
|
16243
|
+
},
|
|
16244
|
+
{
|
|
16245
|
+
codepoint: "U+3164",
|
|
16246
|
+
name: "HANGUL FILLER",
|
|
16247
|
+
tier: 1,
|
|
16248
|
+
char: "ㅤ",
|
|
16249
|
+
group: "invisible"
|
|
16250
|
+
},
|
|
16251
|
+
{
|
|
16252
|
+
codepoint: "U+FFA0",
|
|
16253
|
+
name: "HALFWIDTH HANGUL FILLER",
|
|
16254
|
+
tier: 1,
|
|
16255
|
+
char: "ᅠ",
|
|
16256
|
+
group: "invisible"
|
|
16257
|
+
},
|
|
16258
|
+
...rangeOffenders(917504, 917631, "TAG CHARACTER", "invisible")
|
|
16259
|
+
];
|
|
16260
|
+
const SCRIPT_DEPENDENT_OFFENDERS = [
|
|
16261
|
+
{
|
|
16262
|
+
codepoint: "U+200C",
|
|
16263
|
+
name: "ZERO WIDTH NON-JOINER",
|
|
16264
|
+
tier: 1,
|
|
16265
|
+
char: "",
|
|
16266
|
+
group: "script"
|
|
16267
|
+
},
|
|
16268
|
+
{
|
|
16269
|
+
codepoint: "U+200D",
|
|
16270
|
+
name: "ZERO WIDTH JOINER",
|
|
16271
|
+
tier: 1,
|
|
16272
|
+
char: "",
|
|
16273
|
+
group: "script"
|
|
16274
|
+
},
|
|
16275
|
+
{
|
|
16276
|
+
codepoint: "U+200E",
|
|
16277
|
+
name: "LEFT-TO-RIGHT MARK",
|
|
16278
|
+
tier: 1,
|
|
16279
|
+
char: "",
|
|
16280
|
+
group: "script"
|
|
16281
|
+
},
|
|
16282
|
+
{
|
|
16283
|
+
codepoint: "U+200F",
|
|
16284
|
+
name: "RIGHT-TO-LEFT MARK",
|
|
16285
|
+
tier: 1,
|
|
16286
|
+
char: "",
|
|
16287
|
+
group: "script"
|
|
16288
|
+
},
|
|
16289
|
+
{
|
|
16290
|
+
codepoint: "U+061C",
|
|
16291
|
+
name: "ARABIC LETTER MARK",
|
|
16292
|
+
tier: 1,
|
|
16293
|
+
char: "",
|
|
16294
|
+
group: "script"
|
|
16295
|
+
},
|
|
16296
|
+
{
|
|
16297
|
+
codepoint: "U+3000",
|
|
16298
|
+
name: "IDEOGRAPHIC SPACE",
|
|
16299
|
+
tier: 1,
|
|
16300
|
+
char: " ",
|
|
16301
|
+
group: "script"
|
|
16302
|
+
}
|
|
16303
|
+
];
|
|
16304
|
+
const ODD_SPACE_OFFENDERS = [
|
|
16305
|
+
{
|
|
16306
|
+
codepoint: "U+2000",
|
|
16307
|
+
name: "EN QUAD",
|
|
16308
|
+
tier: 1,
|
|
16309
|
+
char: " ",
|
|
16310
|
+
group: "space",
|
|
16311
|
+
replacement: " "
|
|
16312
|
+
},
|
|
16313
|
+
{
|
|
16314
|
+
codepoint: "U+2001",
|
|
16315
|
+
name: "EM QUAD",
|
|
16316
|
+
tier: 1,
|
|
16317
|
+
char: " ",
|
|
16318
|
+
group: "space",
|
|
16319
|
+
replacement: " "
|
|
16320
|
+
},
|
|
16321
|
+
{
|
|
16322
|
+
codepoint: "U+2002",
|
|
16323
|
+
name: "EN SPACE",
|
|
16324
|
+
tier: 1,
|
|
16325
|
+
char: " ",
|
|
16326
|
+
group: "space",
|
|
16327
|
+
replacement: " "
|
|
16328
|
+
},
|
|
16329
|
+
{
|
|
16330
|
+
codepoint: "U+2003",
|
|
16331
|
+
name: "EM SPACE",
|
|
16332
|
+
tier: 1,
|
|
16333
|
+
char: " ",
|
|
16334
|
+
group: "space",
|
|
16335
|
+
replacement: " "
|
|
16336
|
+
},
|
|
16337
|
+
{
|
|
16338
|
+
codepoint: "U+2004",
|
|
16339
|
+
name: "THREE-PER-EM SPACE",
|
|
16340
|
+
tier: 1,
|
|
16341
|
+
char: " ",
|
|
16342
|
+
group: "space",
|
|
16343
|
+
replacement: " "
|
|
16344
|
+
},
|
|
16345
|
+
{
|
|
16346
|
+
codepoint: "U+2005",
|
|
16347
|
+
name: "FOUR-PER-EM SPACE",
|
|
16348
|
+
tier: 1,
|
|
16349
|
+
char: " ",
|
|
16350
|
+
group: "space",
|
|
16351
|
+
replacement: " "
|
|
16352
|
+
},
|
|
16353
|
+
{
|
|
16354
|
+
codepoint: "U+2006",
|
|
16355
|
+
name: "SIX-PER-EM SPACE",
|
|
16356
|
+
tier: 1,
|
|
16357
|
+
char: " ",
|
|
16358
|
+
group: "space",
|
|
16359
|
+
replacement: " "
|
|
16360
|
+
},
|
|
16361
|
+
{
|
|
16362
|
+
codepoint: "U+2007",
|
|
16363
|
+
name: "FIGURE SPACE",
|
|
16364
|
+
tier: 1,
|
|
16365
|
+
char: " ",
|
|
16366
|
+
group: "space",
|
|
16367
|
+
replacement: " "
|
|
16368
|
+
},
|
|
16369
|
+
{
|
|
16370
|
+
codepoint: "U+2008",
|
|
16371
|
+
name: "PUNCTUATION SPACE",
|
|
16372
|
+
tier: 1,
|
|
16373
|
+
char: " ",
|
|
16374
|
+
group: "space",
|
|
16375
|
+
replacement: " "
|
|
16376
|
+
},
|
|
16377
|
+
{
|
|
16378
|
+
codepoint: "U+2009",
|
|
16379
|
+
name: "THIN SPACE",
|
|
16380
|
+
tier: 1,
|
|
16381
|
+
char: " ",
|
|
16382
|
+
group: "space",
|
|
16383
|
+
replacement: " "
|
|
16384
|
+
},
|
|
16385
|
+
{
|
|
16386
|
+
codepoint: "U+200A",
|
|
16387
|
+
name: "HAIR SPACE",
|
|
16388
|
+
tier: 1,
|
|
16389
|
+
char: " ",
|
|
16390
|
+
group: "space",
|
|
16391
|
+
replacement: " "
|
|
16392
|
+
},
|
|
16393
|
+
{
|
|
16394
|
+
codepoint: "U+205F",
|
|
16395
|
+
name: "MEDIUM MATHEMATICAL SPACE",
|
|
16396
|
+
tier: 1,
|
|
16397
|
+
char: " ",
|
|
16398
|
+
group: "space",
|
|
16399
|
+
replacement: " "
|
|
16400
|
+
}
|
|
16401
|
+
];
|
|
16402
|
+
const HYPHEN_OFFENDERS = [
|
|
16403
|
+
{
|
|
16404
|
+
codepoint: "U+2010",
|
|
16405
|
+
name: "HYPHEN",
|
|
16406
|
+
tier: 1,
|
|
16407
|
+
char: "‐",
|
|
16408
|
+
replacement: "-"
|
|
16409
|
+
},
|
|
16410
|
+
{
|
|
16411
|
+
codepoint: "U+2011",
|
|
16412
|
+
name: "NON-BREAKING HYPHEN",
|
|
16413
|
+
tier: 1,
|
|
16414
|
+
char: "‑",
|
|
16415
|
+
replacement: "-"
|
|
16416
|
+
},
|
|
16417
|
+
{
|
|
16418
|
+
codepoint: "U+2012",
|
|
16419
|
+
name: "FIGURE DASH",
|
|
16420
|
+
tier: 1,
|
|
16421
|
+
char: "‒",
|
|
16422
|
+
replacement: "-"
|
|
16423
|
+
},
|
|
16424
|
+
{
|
|
16425
|
+
codepoint: "U+2015",
|
|
16426
|
+
name: "HORIZONTAL BAR",
|
|
16427
|
+
tier: 2,
|
|
16428
|
+
char: "―",
|
|
16429
|
+
replacement: "--"
|
|
16430
|
+
}
|
|
16431
|
+
];
|
|
15998
16432
|
const DEFAULT_OFFENDERS = [
|
|
15999
16433
|
...TIER1_OFFENDERS,
|
|
16000
16434
|
...BOX_DRAWING_OFFENDERS,
|
|
16001
16435
|
...ARROW_OFFENDERS,
|
|
16002
16436
|
...OUTPUT_GLYPH_OFFENDERS,
|
|
16003
|
-
...TIER2_OFFENDERS
|
|
16437
|
+
...TIER2_OFFENDERS,
|
|
16438
|
+
...INVISIBLE_OFFENDERS,
|
|
16439
|
+
...SCRIPT_DEPENDENT_OFFENDERS,
|
|
16440
|
+
...ODD_SPACE_OFFENDERS,
|
|
16441
|
+
...HYPHEN_OFFENDERS
|
|
16004
16442
|
];
|
|
16005
16443
|
const OFFENDER_CATEGORIES = { math: [
|
|
16006
16444
|
{
|
|
@@ -16052,7 +16490,7 @@ function offenderNameByCodepoint(codepoint) {
|
|
|
16052
16490
|
return nameByCodepoint.get(codepoint);
|
|
16053
16491
|
}
|
|
16054
16492
|
//#endregion
|
|
16055
|
-
//#region src/
|
|
16493
|
+
//#region src/chars-check/context-js-ts.ts
|
|
16056
16494
|
const EXPRESSION_START_PUNCTUATION = /* @__PURE__ */ new Set([
|
|
16057
16495
|
"(",
|
|
16058
16496
|
"[",
|
|
@@ -16443,7 +16881,7 @@ const jsxFamily = {
|
|
|
16443
16881
|
definition: { classify: (text) => classifyJsTs(text, true) }
|
|
16444
16882
|
};
|
|
16445
16883
|
//#endregion
|
|
16446
|
-
//#region src/
|
|
16884
|
+
//#region src/chars-check/context-kt-java.ts
|
|
16447
16885
|
var KtJavaLexer = class {
|
|
16448
16886
|
text;
|
|
16449
16887
|
kotlin;
|
|
@@ -16599,7 +17037,7 @@ const javaFamily = {
|
|
|
16599
17037
|
definition: { classify: (text) => classifyKtJava(text, false) }
|
|
16600
17038
|
};
|
|
16601
17039
|
//#endregion
|
|
16602
|
-
//#region src/
|
|
17040
|
+
//#region src/chars-check/context-markup.ts
|
|
16603
17041
|
const TAG_START = /[A-Za-z!?]/;
|
|
16604
17042
|
const TAG_NAME_CHAR = /[A-Za-z0-9:_-]/;
|
|
16605
17043
|
var MarkupLexer = class {
|
|
@@ -16760,7 +17198,7 @@ var MarkupLexer = class {
|
|
|
16760
17198
|
this.i += 1;
|
|
16761
17199
|
}
|
|
16762
17200
|
stepIsland() {
|
|
16763
|
-
this.flushTo(this.i, "
|
|
17201
|
+
this.flushTo(this.i, "unclassified");
|
|
16764
17202
|
if (this.char() === "<" && this.char(1) === "/" && this.matchesIslandClose()) return this.closeIsland();
|
|
16765
17203
|
this.i += 1;
|
|
16766
17204
|
}
|
|
@@ -16770,6 +17208,7 @@ var MarkupLexer = class {
|
|
|
16770
17208
|
return this.text.slice(start, end).toLowerCase() === this.islandCloseTag;
|
|
16771
17209
|
}
|
|
16772
17210
|
closeIsland() {
|
|
17211
|
+
this.flushTo(this.i, "code");
|
|
16773
17212
|
let j = this.i;
|
|
16774
17213
|
while (j < this.n && this.text[j] !== ">") j += 1;
|
|
16775
17214
|
this.i = Math.min(j + 1, this.n);
|
|
@@ -16788,7 +17227,7 @@ const htmlFamily = {
|
|
|
16788
17227
|
definition: { classify: (text) => classifyMarkup(text, true) }
|
|
16789
17228
|
};
|
|
16790
17229
|
//#endregion
|
|
16791
|
-
//#region src/
|
|
17230
|
+
//#region src/chars-check/context-structured.ts
|
|
16792
17231
|
function triesJsonParse(text) {
|
|
16793
17232
|
try {
|
|
16794
17233
|
JSON.parse(text);
|
|
@@ -16934,7 +17373,7 @@ function classifyAsProse(text) {
|
|
|
16934
17373
|
}];
|
|
16935
17374
|
}
|
|
16936
17375
|
//#endregion
|
|
16937
|
-
//#region src/
|
|
17376
|
+
//#region src/chars-check/families.ts
|
|
16938
17377
|
const FAMILIES = [
|
|
16939
17378
|
jsTsFamily,
|
|
16940
17379
|
jsxFamily,
|
|
@@ -16972,7 +17411,7 @@ const FAMILIES = [
|
|
|
16972
17411
|
}
|
|
16973
17412
|
];
|
|
16974
17413
|
//#endregion
|
|
16975
|
-
//#region src/
|
|
17414
|
+
//#region src/chars-check/context.ts
|
|
16976
17415
|
const proseSpan = (text) => [{
|
|
16977
17416
|
start: 0,
|
|
16978
17417
|
end: text.length,
|
|
@@ -17009,6 +17448,12 @@ function contextAt(spans, offset) {
|
|
|
17009
17448
|
function spanCovering(spans, offset) {
|
|
17010
17449
|
for (const span of spans) if (offset >= span.start && offset < span.end) return span;
|
|
17011
17450
|
}
|
|
17451
|
+
//#endregion
|
|
17452
|
+
//#region src/chars-check/scan.ts
|
|
17453
|
+
const IGNORE_LINE_MARKERS = ["chars-ignore-line", "ascii-ignore-line"];
|
|
17454
|
+
function hasIgnoreMarker(line) {
|
|
17455
|
+
return IGNORE_LINE_MARKERS.some((marker) => line.includes(marker));
|
|
17456
|
+
}
|
|
17012
17457
|
const BINARY_SNIFF_BYTES = 8e3;
|
|
17013
17458
|
function looksBinary(buf) {
|
|
17014
17459
|
const end = Math.min(buf.length, BINARY_SNIFF_BYTES);
|
|
@@ -17022,14 +17467,14 @@ function scanWithOffsets(rel, text, offenders) {
|
|
|
17022
17467
|
let cumulative = 0;
|
|
17023
17468
|
for (let lineIdx = 0; lineIdx < lines.length; lineIdx += 1) {
|
|
17024
17469
|
const line = lines[lineIdx] ?? "";
|
|
17025
|
-
if (!
|
|
17026
|
-
|
|
17027
|
-
|
|
17028
|
-
offset: cumulative +
|
|
17470
|
+
if (!hasIgnoreMarker(line)) for (const { column, offender } of lineOffenders(line, byChar)) {
|
|
17471
|
+
if (cumulative + column === 0 && offender.codepoint === "U+FEFF") continue;
|
|
17472
|
+
out.push({
|
|
17473
|
+
offset: cumulative + column,
|
|
17029
17474
|
hit: {
|
|
17030
17475
|
path: rel,
|
|
17031
17476
|
line: lineIdx + 1,
|
|
17032
|
-
column:
|
|
17477
|
+
column: column + 1,
|
|
17033
17478
|
offender
|
|
17034
17479
|
}
|
|
17035
17480
|
});
|
|
@@ -17038,6 +17483,20 @@ function scanWithOffsets(rel, text, offenders) {
|
|
|
17038
17483
|
}
|
|
17039
17484
|
return out;
|
|
17040
17485
|
}
|
|
17486
|
+
function lineOffenders(line, byChar) {
|
|
17487
|
+
const found = [];
|
|
17488
|
+
let column = 0;
|
|
17489
|
+
while (column < line.length) {
|
|
17490
|
+
const char = String.fromCodePoint(line.codePointAt(column) ?? 0);
|
|
17491
|
+
const offender = byChar.get(char);
|
|
17492
|
+
if (offender !== void 0) found.push({
|
|
17493
|
+
column,
|
|
17494
|
+
offender
|
|
17495
|
+
});
|
|
17496
|
+
column += char.length;
|
|
17497
|
+
}
|
|
17498
|
+
return found;
|
|
17499
|
+
}
|
|
17041
17500
|
function createRealScanExecutor(rootDir) {
|
|
17042
17501
|
return {
|
|
17043
17502
|
listTrackedFiles() {
|
|
@@ -17079,7 +17538,7 @@ function createRealScanExecutor(rootDir) {
|
|
|
17079
17538
|
};
|
|
17080
17539
|
}
|
|
17081
17540
|
//#endregion
|
|
17082
|
-
//#region src/
|
|
17541
|
+
//#region src/chars-check/placement.ts
|
|
17083
17542
|
const ESCAPE_TOKEN_RE = /\\u\{[0-9a-fA-F]{1,6}\}|\\u[0-9a-fA-F]{4}|\\x[0-9a-fA-F]{2}/g;
|
|
17084
17543
|
const ENTITY_TOKEN_RE = /&#(?:[xX][0-9a-fA-F]{1,6}|[0-9]{1,7});/g;
|
|
17085
17544
|
const MARKUP_CONTEXTS = /* @__PURE__ */ new Set([
|
|
@@ -17144,7 +17603,7 @@ function scanPlacement(rel, text, treatAsProse = []) {
|
|
|
17144
17603
|
let cumulative = 0;
|
|
17145
17604
|
for (let lineIdx = 0; lineIdx < lines.length; lineIdx += 1) {
|
|
17146
17605
|
const line = lines[lineIdx] ?? "";
|
|
17147
|
-
if (!
|
|
17606
|
+
if (!hasIgnoreMarker(line)) {
|
|
17148
17607
|
const site = {
|
|
17149
17608
|
rel,
|
|
17150
17609
|
spans,
|
|
@@ -17239,9 +17698,18 @@ function finishFinding(site, parts, rule) {
|
|
|
17239
17698
|
};
|
|
17240
17699
|
}
|
|
17241
17700
|
//#endregion
|
|
17242
|
-
//#region src/
|
|
17701
|
+
//#region src/chars-check/rewrite.ts
|
|
17243
17702
|
function unicodeEscape(offender) {
|
|
17244
|
-
|
|
17703
|
+
let out = "";
|
|
17704
|
+
for (let i = 0; i < offender.char.length; i += 1) {
|
|
17705
|
+
const unit = offender.char.charCodeAt(i).toString(16).toUpperCase().padStart(4, "0");
|
|
17706
|
+
out += `\\u${unit}`;
|
|
17707
|
+
}
|
|
17708
|
+
return out;
|
|
17709
|
+
}
|
|
17710
|
+
function kotlinSplice(offender) {
|
|
17711
|
+
const quote = offender.char.length > 1 ? "\"" : "'";
|
|
17712
|
+
return `\${${quote}${unicodeEscape(offender)}${quote}}`;
|
|
17245
17713
|
}
|
|
17246
17714
|
function xmlEntity(offender) {
|
|
17247
17715
|
return `&#x${offender.codepoint.slice(2)};`;
|
|
@@ -17264,14 +17732,23 @@ function decideAction(offender, context) {
|
|
|
17264
17732
|
case "prose":
|
|
17265
17733
|
case "comment":
|
|
17266
17734
|
case "xml-comment":
|
|
17267
|
-
case "cdata":
|
|
17268
|
-
|
|
17269
|
-
|
|
17270
|
-
|
|
17735
|
+
case "cdata":
|
|
17736
|
+
if (offender.group === "invisible") return {
|
|
17737
|
+
action: "flag",
|
|
17738
|
+
reason: "an invisible or deceptive character; removing it can change what the line means"
|
|
17739
|
+
};
|
|
17740
|
+
if (offender.replacement === void 0) return {
|
|
17741
|
+
action: "flag",
|
|
17742
|
+
reason: "no ASCII replacement is safe to pick unattended; edit it by hand"
|
|
17743
|
+
};
|
|
17744
|
+
return {
|
|
17745
|
+
action: "flatten",
|
|
17746
|
+
replacement: offender.replacement
|
|
17747
|
+
};
|
|
17271
17748
|
case "raw-string": return {
|
|
17272
17749
|
action: "flag",
|
|
17273
17750
|
reason: "Kotlin raw strings do not process \\u escapes",
|
|
17274
|
-
suggestion:
|
|
17751
|
+
suggestion: kotlinSplice(offender)
|
|
17275
17752
|
};
|
|
17276
17753
|
case "text-block": return {
|
|
17277
17754
|
action: "flag",
|
|
@@ -17281,13 +17758,18 @@ function decideAction(offender, context) {
|
|
|
17281
17758
|
action: "flag",
|
|
17282
17759
|
reason: "a tagged template's tag may read .raw, where an escape is not the same value"
|
|
17283
17760
|
};
|
|
17284
|
-
case "code":
|
|
17285
|
-
|
|
17286
|
-
|
|
17287
|
-
|
|
17761
|
+
case "code":
|
|
17762
|
+
if (offender.group === "script") return {
|
|
17763
|
+
action: "flag",
|
|
17764
|
+
reason: "a character some scripts need, in a code position; not this tool's job"
|
|
17765
|
+
};
|
|
17766
|
+
return {
|
|
17767
|
+
action: "flag",
|
|
17768
|
+
reason: "non-ASCII character in a code position (e.g. an identifier); not this tool's job"
|
|
17769
|
+
};
|
|
17288
17770
|
case "unclassified": return {
|
|
17289
17771
|
action: "flag",
|
|
17290
|
-
reason: "
|
|
17772
|
+
reason: "this position was not classified with confidence; hand-edit required"
|
|
17291
17773
|
};
|
|
17292
17774
|
default: throw new UnexpectedError(`unhandled ascii context: ${String(context)}`);
|
|
17293
17775
|
}
|
|
@@ -17302,7 +17784,7 @@ function violatesXmlCommentInvariant(commentBody) {
|
|
|
17302
17784
|
return commentBody.includes("--") || commentBody.endsWith("-");
|
|
17303
17785
|
}
|
|
17304
17786
|
//#endregion
|
|
17305
|
-
//#region src/
|
|
17787
|
+
//#region src/chars-check/fix.ts
|
|
17306
17788
|
function precedingBackslashRun(text, offset) {
|
|
17307
17789
|
let count = 0;
|
|
17308
17790
|
for (let i = offset - 1; i >= 0 && text[i] === "\\"; i -= 1) count += 1;
|
|
@@ -17333,7 +17815,7 @@ function splice(text, planned) {
|
|
|
17333
17815
|
for (const p of planned) {
|
|
17334
17816
|
if (!p.applied || p.replacement === void 0) continue;
|
|
17335
17817
|
result += text.slice(cursor, p.offset) + p.replacement;
|
|
17336
|
-
cursor = p.offset +
|
|
17818
|
+
cursor = p.offset + p.hit.offender.char.length;
|
|
17337
17819
|
}
|
|
17338
17820
|
result += text.slice(cursor);
|
|
17339
17821
|
return result;
|
|
@@ -17357,8 +17839,10 @@ var XmlCommentGuard = class {
|
|
|
17357
17839
|
if (!span || span.context !== "xml-comment") return decision;
|
|
17358
17840
|
const prior = this.committed.get(span) ?? [];
|
|
17359
17841
|
const before = spliceWithin(text, span.start, span.end, prior);
|
|
17842
|
+
const length = String.fromCodePoint(text.codePointAt(offset) ?? 0).length;
|
|
17360
17843
|
const tentative = [...prior, {
|
|
17361
17844
|
offset,
|
|
17845
|
+
length,
|
|
17362
17846
|
replacement: decision.replacement
|
|
17363
17847
|
}].toSorted((a, b) => a.offset - b.offset);
|
|
17364
17848
|
if (violatesXmlCommentInvariant(spliceWithin(text, span.start, span.end, tentative)) && !violatesXmlCommentInvariant(before)) {
|
|
@@ -17378,7 +17862,7 @@ function spliceWithin(text, start, end, replacements) {
|
|
|
17378
17862
|
let cursor = start;
|
|
17379
17863
|
for (const r of replacements) {
|
|
17380
17864
|
result += text.slice(cursor, r.offset) + r.replacement;
|
|
17381
|
-
cursor = r.offset +
|
|
17865
|
+
cursor = r.offset + r.length;
|
|
17382
17866
|
}
|
|
17383
17867
|
return result + text.slice(cursor, end);
|
|
17384
17868
|
}
|
|
@@ -17396,6 +17880,7 @@ function fixText(text, options) {
|
|
|
17396
17880
|
const pending = [];
|
|
17397
17881
|
for (const { offset, hit } of rawHits) {
|
|
17398
17882
|
const context = contextAt(spans, offset);
|
|
17883
|
+
if (hit.offender.group === "script" && context !== "code") continue;
|
|
17399
17884
|
const preGuardDecision = decideAction(hit.offender, context);
|
|
17400
17885
|
const willApply = isFixedNow(hit.offender, preGuardDecision, fixAmbiguous);
|
|
17401
17886
|
const decision = guardAgainstDoubleEscape(text, offset, xmlCommentGuard.check(spans, text, offset, preGuardDecision, willApply));
|
|
@@ -17417,7 +17902,7 @@ function fixText(text, options) {
|
|
|
17417
17902
|
} : p.decision;
|
|
17418
17903
|
const applied = isFixedNow(p.hit.offender, decision, fixAmbiguous);
|
|
17419
17904
|
const newOffset = p.offset + delta;
|
|
17420
|
-
if (applied && decision.replacement !== void 0) delta += decision.replacement.length -
|
|
17905
|
+
if (applied && decision.replacement !== void 0) delta += decision.replacement.length - p.hit.offender.char.length;
|
|
17421
17906
|
planned.push({
|
|
17422
17907
|
offset: p.offset,
|
|
17423
17908
|
newOffset,
|
|
@@ -17454,6 +17939,34 @@ function fixText(text, options) {
|
|
|
17454
17939
|
decisions: planned.map(toDecision)
|
|
17455
17940
|
};
|
|
17456
17941
|
}
|
|
17942
|
+
const strictUtf8 = new TextDecoder("utf-8", { fatal: true });
|
|
17943
|
+
function isValidUtf8(buf) {
|
|
17944
|
+
try {
|
|
17945
|
+
strictUtf8.decode(buf);
|
|
17946
|
+
return true;
|
|
17947
|
+
} catch {
|
|
17948
|
+
return false;
|
|
17949
|
+
}
|
|
17950
|
+
}
|
|
17951
|
+
const INVALID_UTF8_OFFENDER = {
|
|
17952
|
+
codepoint: "U+FFFD",
|
|
17953
|
+
name: "REPLACEMENT CHARACTER (invalid UTF-8 byte)",
|
|
17954
|
+
tier: 1,
|
|
17955
|
+
char: "�"
|
|
17956
|
+
};
|
|
17957
|
+
function refuseLossyDecode(result) {
|
|
17958
|
+
return {
|
|
17959
|
+
text: result.text,
|
|
17960
|
+
changed: false,
|
|
17961
|
+
decisions: result.decisions.map((d) => ({
|
|
17962
|
+
...d,
|
|
17963
|
+
action: "flag",
|
|
17964
|
+
applied: false,
|
|
17965
|
+
replacement: void 0,
|
|
17966
|
+
reason: "the file is not valid UTF-8, so no rewrite of it is safe; fix its encoding first"
|
|
17967
|
+
}))
|
|
17968
|
+
};
|
|
17969
|
+
}
|
|
17457
17970
|
function fixFiles(executor, options) {
|
|
17458
17971
|
const isExcluded = picomatch(options.exclude ?? []);
|
|
17459
17972
|
const results = [];
|
|
@@ -17464,12 +17977,14 @@ function fixFiles(executor, options) {
|
|
|
17464
17977
|
if (!buf || looksBinary(buf)) continue;
|
|
17465
17978
|
const original = buf.toString("utf-8");
|
|
17466
17979
|
const placement = scanPlacement(rel, original, options.treatAsProse);
|
|
17467
|
-
const
|
|
17468
|
-
|
|
17980
|
+
const validUtf8 = isValidUtf8(buf);
|
|
17981
|
+
const fixed = fixText(original, {
|
|
17982
|
+
offenders: validUtf8 ? options.offenders : [...options.offenders, INVALID_UTF8_OFFENDER],
|
|
17469
17983
|
path: rel,
|
|
17470
17984
|
fixAmbiguous: options.fixAmbiguous,
|
|
17471
17985
|
treatAsProse: options.treatAsProse
|
|
17472
17986
|
});
|
|
17987
|
+
const { text, changed, decisions } = validUtf8 ? fixed : refuseLossyDecode(fixed);
|
|
17473
17988
|
if (changed && options.dryRun !== true) executor.writeFileBuffer(rel, Buffer.from(text, "utf-8"));
|
|
17474
17989
|
const remainingHits = decisions.filter((d) => !d.applied).map((d) => d.hit);
|
|
17475
17990
|
results.push({
|
|
@@ -17521,22 +18036,29 @@ function isManual(provenance) {
|
|
|
17521
18036
|
function composeFindingFile(entry, provenance) {
|
|
17522
18037
|
const sourceLabel = provenance.sourceKind === "pr" ? "PR" : "issue";
|
|
17523
18038
|
const lines = [
|
|
17524
|
-
`# ${
|
|
18039
|
+
`# ${toGateText(entry.title)}`,
|
|
17525
18040
|
"",
|
|
17526
18041
|
`- Class: ${entry.class}`,
|
|
17527
18042
|
`- Phase: ${provenance.phase}`
|
|
17528
18043
|
];
|
|
17529
18044
|
if (isManual(provenance)) lines.push(`- Source ${sourceLabel}: #${String(provenance.sourceIssueNumber)}`);
|
|
17530
18045
|
else lines.push(`- Source ${sourceLabel}: #${String(provenance.sourceIssueNumber)} (${provenance.sourceIssueUrl})`, `- Pull request: ${provenance.prUrl ?? "none -- the run produced no commits"}`, `- Transcript directory: ${provenance.transcriptDir}`);
|
|
17531
|
-
lines.push("",
|
|
18046
|
+
lines.push("", toGateText(entry.body));
|
|
17532
18047
|
return lines.join("\n") + "\n";
|
|
17533
18048
|
}
|
|
17534
|
-
function
|
|
17535
|
-
|
|
18049
|
+
function toGateText(text) {
|
|
18050
|
+
const flattened = fixText(text, {
|
|
17536
18051
|
offenders: DEFAULT_OFFENDERS,
|
|
17537
18052
|
path: "finding.md",
|
|
17538
18053
|
fixAmbiguous: true
|
|
17539
18054
|
}).text;
|
|
18055
|
+
return nameInvisibleCharacters(flattened);
|
|
18056
|
+
}
|
|
18057
|
+
function nameInvisibleCharacters(text) {
|
|
18058
|
+
const labels = new Map(DEFAULT_OFFENDERS.filter((o) => o.group === "invisible").map((o) => [o.char, o.codepoint]));
|
|
18059
|
+
let out = "";
|
|
18060
|
+
for (const char of Array.from(text)) out += labels.get(char) ?? char;
|
|
18061
|
+
return out;
|
|
17540
18062
|
}
|
|
17541
18063
|
const TITLE_RE = /^#\s+(.+)$/m;
|
|
17542
18064
|
const CLASS_RE = /^-\s*Class:\s*(.+)$/m;
|
|
@@ -17636,27 +18158,6 @@ function firstSentence$1(paragraph) {
|
|
|
17636
18158
|
}
|
|
17637
18159
|
return paragraph;
|
|
17638
18160
|
}
|
|
17639
|
-
const LABEL_REGEX = /^[A-Za-z]+\s+\d+[a-z]?:\s+/;
|
|
17640
|
-
function firstClause(title) {
|
|
17641
|
-
const label = LABEL_REGEX.exec(title)?.[0].length ?? 0;
|
|
17642
|
-
let inCode = false;
|
|
17643
|
-
for (let index = label; index < title.length; index++) {
|
|
17644
|
-
const char = title[index];
|
|
17645
|
-
if (char === "`") inCode = !inCode;
|
|
17646
|
-
if (inCode || char !== ";" && char !== ":") continue;
|
|
17647
|
-
if (index + 1 < title.length && !/\s/.test(title[index + 1] ?? "")) continue;
|
|
17648
|
-
return title.slice(0, index).trimEnd();
|
|
17649
|
-
}
|
|
17650
|
-
return title;
|
|
17651
|
-
}
|
|
17652
|
-
function shortTitle(title) {
|
|
17653
|
-
if (title.length <= 100) return title;
|
|
17654
|
-
const clause = firstClause(title);
|
|
17655
|
-
if (clause.length <= 100) return clause;
|
|
17656
|
-
const room = clause.slice(0, 97);
|
|
17657
|
-
const space = room.lastIndexOf(" ");
|
|
17658
|
-
return `${(space > 0 ? room.slice(0, space) : room).trimEnd()}...`;
|
|
17659
|
-
}
|
|
17660
18161
|
function stripStrong(text) {
|
|
17661
18162
|
return text.split(/(`[^`]*`)/).map((part) => part.startsWith("`") ? part : part.replaceAll("**", "")).join("").trim();
|
|
17662
18163
|
}
|
|
@@ -17689,7 +18190,7 @@ function parsePlanBoxes(planText) {
|
|
|
17689
18190
|
break;
|
|
17690
18191
|
}
|
|
17691
18192
|
}
|
|
17692
|
-
const title =
|
|
18193
|
+
const title = stripStrong(unwrapEmphasis(firstSentence$1(boxParagraph(lines, fenced, box.index, blockEnd, box.rawTitle))));
|
|
17693
18194
|
const retireTask = RETIRE_TASK_REGEX.test(title);
|
|
17694
18195
|
if (box.checked) return {
|
|
17695
18196
|
line: box.index,
|
|
@@ -23876,32 +24377,40 @@ const healthStatusCommand = defineCommand({
|
|
|
23876
24377
|
}
|
|
23877
24378
|
});
|
|
23878
24379
|
//#endregion
|
|
23879
|
-
//#region src/
|
|
23880
|
-
const ToolingShallowSchema = z.object({
|
|
23881
|
-
|
|
24380
|
+
//#region src/chars-check/config.ts
|
|
24381
|
+
const ToolingShallowSchema = z.object({
|
|
24382
|
+
chars: tolerantBlock(CharsConfigSchema).optional(),
|
|
24383
|
+
ascii: tolerantBlock(CharsConfigSchema).optional()
|
|
24384
|
+
});
|
|
24385
|
+
const DEFAULT_CHARS_EXCLUDE = [
|
|
23882
24386
|
"docs/**/retired/**",
|
|
23883
24387
|
"**/CHANGELOG.md",
|
|
23884
24388
|
".claude/hooks/bst-hook.mjs",
|
|
23885
24389
|
".claude/hooks/bst-hook-guide.md"
|
|
23886
24390
|
];
|
|
23887
|
-
function
|
|
23888
|
-
const
|
|
23889
|
-
block: "
|
|
24391
|
+
function parseCharsConfig(raw, displayPath) {
|
|
24392
|
+
const blocks = parseToolingConfigSlice(raw, ToolingShallowSchema, {
|
|
24393
|
+
block: "chars",
|
|
23890
24394
|
path: displayPath
|
|
23891
|
-
})
|
|
24395
|
+
});
|
|
24396
|
+
const chars = blocks?.chars ?? blocks?.ascii;
|
|
23892
24397
|
return {
|
|
23893
|
-
exclude: [.../* @__PURE__ */ new Set([...
|
|
23894
|
-
deny:
|
|
23895
|
-
allow:
|
|
23896
|
-
treatAsProse:
|
|
24398
|
+
exclude: [.../* @__PURE__ */ new Set([...DEFAULT_CHARS_EXCLUDE, ...chars?.exclude ?? []])],
|
|
24399
|
+
deny: chars?.deny ?? [],
|
|
24400
|
+
allow: chars?.allow ?? [],
|
|
24401
|
+
treatAsProse: chars?.treatAsProse ?? []
|
|
23897
24402
|
};
|
|
23898
24403
|
}
|
|
23899
|
-
function
|
|
24404
|
+
function resolveCharsConfig(targetDir) {
|
|
23900
24405
|
const full = path.join(targetDir, ".tooling.json");
|
|
23901
|
-
return
|
|
24406
|
+
return parseCharsConfig(existsSync(full) ? readFileSync(full, "utf-8") : void 0, path.relative(process.cwd(), full) || full);
|
|
23902
24407
|
}
|
|
23903
24408
|
//#endregion
|
|
23904
|
-
//#region src/
|
|
24409
|
+
//#region src/chars-check/report.ts
|
|
24410
|
+
function displayChar(offender) {
|
|
24411
|
+
if (offender.group === void 0) return offender.char;
|
|
24412
|
+
return `<${offender.codepoint}>`;
|
|
24413
|
+
}
|
|
23905
24414
|
function formatHitLine(decision) {
|
|
23906
24415
|
const { hit, context, action } = decision;
|
|
23907
24416
|
const { path, line, column, offender } = hit;
|
|
@@ -23909,18 +24418,18 @@ function formatHitLine(decision) {
|
|
|
23909
24418
|
if (action === "flag") {
|
|
23910
24419
|
const reason = decision.reason ?? "no safe mechanical rewrite for this context";
|
|
23911
24420
|
const suggestionPart = decision.suggestion !== void 0 ? `; suggested: ${decision.suggestion}` : "";
|
|
23912
|
-
return `${head} '${offender
|
|
24421
|
+
return `${head} '${displayChar(offender)}' [${context}] flagged: ${reason}${suggestionPart}`;
|
|
23913
24422
|
}
|
|
23914
|
-
const replacement = decision.replacement ?? offender.replacement;
|
|
24423
|
+
const replacement = decision.replacement ?? offender.replacement ?? "";
|
|
23915
24424
|
const ambiguous = offender.tier === 2 && action === "flatten" ? " (ambiguous)" : "";
|
|
23916
|
-
return `${head} '${offender
|
|
24425
|
+
return `${head} '${displayChar(offender)}' -> '${replacement}' [${context}/${action}]${ambiguous}`;
|
|
23917
24426
|
}
|
|
23918
24427
|
function formatPlacementLine(finding) {
|
|
23919
24428
|
const { path, line, column, codepoint, name, found, context, expected } = finding;
|
|
23920
24429
|
return `${`${path}:${String(line)}:${String(column)} ${codepoint}${name !== void 0 ? ` ${name}` : ""}`} '${found}' in ${context} is not decoded there; write '${expected}'`;
|
|
23921
24430
|
}
|
|
23922
24431
|
function formatReport$1(decisions, placement = []) {
|
|
23923
|
-
if (decisions.length === 0 && placement.length === 0) return "
|
|
24432
|
+
if (decisions.length === 0 && placement.length === 0) return "chars:check: no offenders found.";
|
|
23924
24433
|
return [...decisions.map(formatHitLine), ...placement.map(formatPlacementLine)].join("\n");
|
|
23925
24434
|
}
|
|
23926
24435
|
function toJsonReport(decisions, placement = []) {
|
|
@@ -23950,7 +24459,7 @@ function toJsonReport(decisions, placement = []) {
|
|
|
23950
24459
|
};
|
|
23951
24460
|
}
|
|
23952
24461
|
//#endregion
|
|
23953
|
-
//#region src/commands/
|
|
24462
|
+
//#region src/commands/chars-check.ts
|
|
23954
24463
|
function reportDecisions(decisions, placement = []) {
|
|
23955
24464
|
return {
|
|
23956
24465
|
payload: toJsonReport(decisions, placement),
|
|
@@ -23958,7 +24467,7 @@ function reportDecisions(decisions, placement = []) {
|
|
|
23958
24467
|
exitCode: decisions.length + placement.length === 0 ? 0 : 1
|
|
23959
24468
|
};
|
|
23960
24469
|
}
|
|
23961
|
-
function
|
|
24470
|
+
function runCharsCheck(executor, offenders, exclude, opts, treatAsProse = []) {
|
|
23962
24471
|
const writes = opts.fix === true && opts.dryRun !== true;
|
|
23963
24472
|
const results = fixFiles(executor, {
|
|
23964
24473
|
offenders,
|
|
@@ -23974,7 +24483,7 @@ function runAsciiCheck(executor, offenders, exclude, opts, treatAsProse = []) {
|
|
|
23974
24483
|
if (opts.dryRun !== true) return reportDecisions(remaining, placement);
|
|
23975
24484
|
const planned = decisions.filter((d) => d.applied);
|
|
23976
24485
|
const exitCode = planned.length === 0 && remaining.length === 0 && placement.length === 0 ? 0 : 1;
|
|
23977
|
-
const lines = [planned.length === 0 ? "
|
|
24486
|
+
const lines = [planned.length === 0 ? "chars:check --fix --dry-run: no fixable offenders found." : `chars:check --fix --dry-run: would fix ${String(planned.length)} offender(s):`, ...planned.map(formatHitLine)];
|
|
23978
24487
|
if (remaining.length > 0 || placement.length > 0) lines.push("remaining (not fixed):", ...remaining.map(formatHitLine), ...placement.map(formatPlacementLine));
|
|
23979
24488
|
return {
|
|
23980
24489
|
payload: {
|
|
@@ -23988,54 +24497,70 @@ function runAsciiCheck(executor, offenders, exclude, opts, treatAsProse = []) {
|
|
|
23988
24497
|
exitCode
|
|
23989
24498
|
};
|
|
23990
24499
|
}
|
|
23991
|
-
const
|
|
24500
|
+
const CHARS_CHECK_ARGS = {
|
|
24501
|
+
dir: {
|
|
24502
|
+
type: "positional",
|
|
24503
|
+
valueHint: "dir",
|
|
24504
|
+
description: "Project directory (default: current directory)",
|
|
24505
|
+
required: false
|
|
24506
|
+
},
|
|
24507
|
+
fix: {
|
|
24508
|
+
type: "boolean",
|
|
24509
|
+
description: "Rewrite tier-1 offenders in place"
|
|
24510
|
+
},
|
|
24511
|
+
"fix-ambiguous": {
|
|
24512
|
+
type: "boolean",
|
|
24513
|
+
description: "With --fix, also rewrite tier-2 offenders (e.g. em dash -> --)"
|
|
24514
|
+
},
|
|
24515
|
+
"dry-run": {
|
|
24516
|
+
type: "boolean",
|
|
24517
|
+
description: "With --fix, print the planned edits without writing"
|
|
24518
|
+
},
|
|
24519
|
+
json: {
|
|
24520
|
+
type: "boolean",
|
|
24521
|
+
description: "Emit one JSON document: status (clean | violations), count, hits ([{path, line, column, codepoint, name, char, replacement?, tier (1 | 2), ambiguous, context, action (flatten | escape | flag), reason?, suggestion?}]), placementCount, placement ([{path, line, column, rule (js-escape-in-markup | entity-in-js-string), context, found, codepoint, expected, name?}]). context is one of prose | comment | xml-comment | string | template | tagged-template | raw-string | text-block | regex | jsx-text | jsx-attribute | markup-text | markup-attribute | cdata | code | unclassified. With --fix --dry-run the document is instead dryRun (true), plannedFixes (hits), remaining (hits), placementCount, placement."
|
|
24522
|
+
},
|
|
24523
|
+
verbose: {
|
|
24524
|
+
type: "boolean",
|
|
24525
|
+
description: "Emit detailed debug logging"
|
|
24526
|
+
}
|
|
24527
|
+
};
|
|
24528
|
+
function runCharsCheckCli(args) {
|
|
24529
|
+
const out = createCliOutput(args.json === true);
|
|
24530
|
+
const targetDir = path.resolve(args.dir ?? ".");
|
|
24531
|
+
const vc = { verbose: args.verbose === true || isEnvVerbose() };
|
|
24532
|
+
const config = resolveCharsConfig(targetDir);
|
|
24533
|
+
const offenders = resolveOffenders(config);
|
|
24534
|
+
debug(vc, `chars:check: ${String(offenders.length)} offender(s) in the effective deny-list`);
|
|
24535
|
+
const { payload, text, exitCode } = runCharsCheck(createRealFixExecutor(targetDir), offenders, config.exclude, {
|
|
24536
|
+
fix: args.fix === true,
|
|
24537
|
+
fixAmbiguous: args["fix-ambiguous"] === true,
|
|
24538
|
+
dryRun: args["dry-run"] === true
|
|
24539
|
+
}, config.treatAsProse);
|
|
24540
|
+
out.json(payload);
|
|
24541
|
+
if (text !== "") out.text(text);
|
|
24542
|
+
process.exitCode = exitCode;
|
|
24543
|
+
}
|
|
24544
|
+
const charsCheckCommand = defineCommand({
|
|
23992
24545
|
meta: {
|
|
23993
|
-
name: "
|
|
23994
|
-
description: "Scan tracked text files for
|
|
24546
|
+
name: "chars:check",
|
|
24547
|
+
description: "Scan tracked text files for the characters this gate denies: LLM-tell punctuation (curly quotes, ellipsis, en and em dashes, arrows, box-drawing, status glyphs) and invisible or deceptive characters (bidi controls, zero-width and tag characters, odd spaces). Letters in any language pass -- an accented e, Persian, Hindi, an emoji sequence and CJK text are all fine. Each hit is reported with its context-correct fix: an escape in a string, an entity in markup, an ASCII lookalike in prose, or a flag naming why no mechanical rewrite is safe. Skipped on purpose: excluded globs (the built-in docs/**/retired/**, **/CHANGELOG.md and the generated bst-hook files, plus .tooling.json#chars.exclude), files .gitattributes marks binary, and files with a null byte in their first 8000 bytes. It also reports an escape (such as \\uXXXX or an entity) written where its context does not decode it; that report always exits 1 and is never auto-fixed. It runs in checks:run unless .tooling.json#chars.enabled is false, and always runs when invoked directly. To keep one character, add its code point to .tooling.json#chars.allow. To skip files, add globs to .tooling.json#chars.exclude. To skip one line, put the text chars-ignore-line on it, in any comment syntax."
|
|
23995
24548
|
},
|
|
23996
|
-
args:
|
|
23997
|
-
|
|
23998
|
-
|
|
23999
|
-
|
|
24000
|
-
|
|
24001
|
-
|
|
24002
|
-
|
|
24003
|
-
|
|
24004
|
-
|
|
24005
|
-
|
|
24006
|
-
},
|
|
24007
|
-
"fix-ambiguous": {
|
|
24008
|
-
type: "boolean",
|
|
24009
|
-
description: "With --fix, also rewrite tier-2 offenders (e.g. em dash -> --)"
|
|
24010
|
-
},
|
|
24011
|
-
"dry-run": {
|
|
24012
|
-
type: "boolean",
|
|
24013
|
-
description: "With --fix, print the planned edits without writing"
|
|
24014
|
-
},
|
|
24015
|
-
json: {
|
|
24016
|
-
type: "boolean",
|
|
24017
|
-
description: "Emit one JSON document: status (clean | violations), count, hits ([{path, line, column, codepoint, name, char, replacement?, tier (1 | 2), ambiguous, context, action (flatten | escape | flag), reason?, suggestion?}]), placementCount, placement ([{path, line, column, rule (js-escape-in-markup | entity-in-js-string), context, found, codepoint, expected, name?}]). context is one of prose | comment | xml-comment | string | template | tagged-template | raw-string | text-block | regex | jsx-text | jsx-attribute | markup-text | markup-attribute | cdata | code | unclassified. With --fix --dry-run the document is instead dryRun (true), plannedFixes (hits), remaining (hits), placementCount, placement."
|
|
24018
|
-
},
|
|
24019
|
-
verbose: {
|
|
24020
|
-
type: "boolean",
|
|
24021
|
-
description: "Emit detailed debug logging"
|
|
24022
|
-
}
|
|
24549
|
+
args: CHARS_CHECK_ARGS,
|
|
24550
|
+
run({ args }) {
|
|
24551
|
+
runCharsCheckCli(args);
|
|
24552
|
+
}
|
|
24553
|
+
});
|
|
24554
|
+
const asciiCheckAliasCommand = defineCommand({
|
|
24555
|
+
meta: {
|
|
24556
|
+
name: "ascii:check",
|
|
24557
|
+
description: "Deprecated alias of chars:check",
|
|
24558
|
+
hidden: true
|
|
24023
24559
|
},
|
|
24560
|
+
args: CHARS_CHECK_ARGS,
|
|
24024
24561
|
run({ args }) {
|
|
24025
|
-
|
|
24026
|
-
|
|
24027
|
-
const vc = { verbose: args.verbose === true || isEnvVerbose() };
|
|
24028
|
-
const config = resolveAsciiConfig(targetDir);
|
|
24029
|
-
const offenders = resolveOffenders(config);
|
|
24030
|
-
debug(vc, `ascii:check: ${String(offenders.length)} offender(s) in the effective deny-list`);
|
|
24031
|
-
const { payload, text, exitCode } = runAsciiCheck(createRealFixExecutor(targetDir), offenders, config.exclude, {
|
|
24032
|
-
fix: args.fix === true,
|
|
24033
|
-
fixAmbiguous: args["fix-ambiguous"] === true,
|
|
24034
|
-
dryRun: args["dry-run"] === true
|
|
24035
|
-
}, config.treatAsProse);
|
|
24036
|
-
out.json(payload);
|
|
24037
|
-
if (text !== "") out.text(text);
|
|
24038
|
-
process.exitCode = exitCode;
|
|
24562
|
+
createCliOutput(args.json === true).text("ascii:check is now chars:check; the old name works until 2026-12-15.");
|
|
24563
|
+
runCharsCheckCli(args);
|
|
24039
24564
|
}
|
|
24040
24565
|
});
|
|
24041
24566
|
z.object({
|
|
@@ -27345,7 +27870,7 @@ async function runCli(config) {
|
|
|
27345
27870
|
//#region src/cli-main.ts
|
|
27346
27871
|
setLogLevel(resolveLogLevel({}));
|
|
27347
27872
|
setForgejoLogger(log$1);
|
|
27348
|
-
const displayVersion = "0.
|
|
27873
|
+
const displayVersion = "0.93.0";
|
|
27349
27874
|
const bstSubCommands = {
|
|
27350
27875
|
"repo:sync": syncCommand,
|
|
27351
27876
|
"checks:run": runChecksCommand,
|
|
@@ -27378,7 +27903,8 @@ const bstSubCommands = {
|
|
|
27378
27903
|
"health:summary": healthSummaryCommand,
|
|
27379
27904
|
"health:record": healthRecordCommand,
|
|
27380
27905
|
"health:status": healthStatusCommand,
|
|
27381
|
-
"
|
|
27906
|
+
"chars:check": charsCheckCommand,
|
|
27907
|
+
"ascii:check": asciiCheckAliasCommand,
|
|
27382
27908
|
"branch:prune": branchPruneCommand,
|
|
27383
27909
|
"docs:check": docsCheckCommand,
|
|
27384
27910
|
"docs:delink": docsDelinkCommand,
|